Skip to content

Commit 8b863dd

Browse files
feat(websocket): Added remote console example over websocket
1 parent e9b21ea commit 8b863dd

File tree

8 files changed

+577
-0
lines changed

8 files changed

+577
-0
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# For more information about build system see
2+
# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html
3+
# The following five lines of boilerplate have to be in your project's
4+
# CMakeLists in this exact order for cmake to work correctly
5+
cmake_minimum_required(VERSION 3.16)
6+
set(COMPONENTS main)
7+
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
8+
project(websocket-client-console_exp)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# ESP32 Remote Console over WebSocket
2+
3+
This project provides a remote console for ESP32, redirecting standard I/O (`stdin`, `stdout`, `stderr`) to a WebSocket via a VFS driver. It uses a Python WebSocket server (`console.py`) for remote interaction with the ESP32's command-line interface.
4+
5+
## Features
6+
- WebSocket-based remote console with low-latency bidirectional communication.
7+
- VFS driver redirects I/O to WebSocket, supporting standard C library functions.
8+
- Console REPL for command execution, extensible via ESP-IDF console component.
9+
- WebSocket URI configurable via NVS (default: `ws://192.168.50.231:8080`).
10+
11+
## Components
12+
- **ESP32 Firmware**:
13+
- `main.c`: Initializes network, WebSocket client, VFS, and console.
14+
- `websocket_client_vfs.c/h`: VFS driver for WebSocket I/O redirection.
15+
- **Python Server** (`console.py`): WebSocket server for console interaction using `websockets` and `aioconsole`.
16+
17+
## Prerequisites
18+
- ESP-IDF v5.0+ configured.
19+
- ESP32 board with Wi-Fi access.
20+
- Python 3.7+ with `websockets` and `aioconsole` (`pip install websockets aioconsole`).
21+
- Host machine on the same network as the ESP32.
22+
23+
## Setup
24+
25+
### ESP32
26+
1. Clone the repository and navigate to the project directory.
27+
2. Configure Wi-Fi:
28+
```bash
29+
idf.py menuconfig
30+
```
31+
- Set `Example Connection Configuration > WiFi SSID` and `WiFi Password`.
32+
3. Build and flash:
33+
```bash
34+
idf.py -p /dev/ttyUSB0 build flash monitor
35+
```
36+
37+
### Python Server
38+
1. Run the WebSocket server:
39+
```bash
40+
python console.py --host 0.0.0.0 --port 8080
41+
```
42+
- Note the host’s IP (e.g., `192.168.50.231`) for ESP32 configuration.
43+
44+
## Usage
45+
1. Start the Python server (`python console.py --host 0.0.0.0 --port 8080`).
46+
2. Power on the ESP32; it connects to Wi-Fi and the WebSocket server.
47+
3. In the Python server terminal, enter commands (e.g., `help`, `nvs`) and view responses.
48+
4. Stop the server with `Ctrl+C` or reset the ESP32 to halt.
49+
50+
## Configuration
51+
- **WebSocket URI**:
52+
- Default: `ws://192.168.50.231:8080`.
53+
- Or edit `DEFAULT_WS_URI` in `main.c` and reflash.
54+
- **Wi-Fi**: Update via `menuconfig` or use provisioning (e.g., softAP).
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#!/usr/bin/env python
2+
# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
3+
# SPDX-License-Identifier: Unlicense OR CC0-1.0
4+
5+
import argparse
6+
import asyncio
7+
8+
import aioconsole
9+
import websockets
10+
11+
cur_websocket = None
12+
last_input = ''
13+
14+
15+
async def handler(websocket):
16+
global cur_websocket
17+
global last_input
18+
cur_websocket = websocket
19+
await aioconsole.aprint('Connected!')
20+
while True:
21+
try:
22+
message = await websocket.recv()
23+
if last_input and message.startswith(last_input):
24+
message = message[len(last_input):]
25+
last_input = ''
26+
await aioconsole.aprint(message.decode('utf-8'), end='')
27+
except websockets.exceptions.ConnectionClosedError:
28+
return
29+
30+
31+
async def websocket_loop(host: str, port: int):
32+
async with websockets.serve(handler, host, port):
33+
await asyncio.Future()
34+
35+
36+
async def console_loop():
37+
while True:
38+
cmd = await aioconsole.ainput('') + '\n'
39+
if cur_websocket is not None:
40+
await cur_websocket.send(cmd.encode('utf-8'))
41+
42+
43+
def main():
44+
parser = argparse.ArgumentParser()
45+
parser.add_argument(
46+
'--host', type=str, help='Host to listen on', default='localhost'
47+
)
48+
parser.add_argument(
49+
'-p', '--port', type=int, help='Port to listen on', default=8080
50+
)
51+
args = parser.parse_args()
52+
loop = asyncio.get_event_loop()
53+
loop.create_task(websocket_loop(args.host, args.port))
54+
loop.create_task(console_loop())
55+
try:
56+
loop.run_forever()
57+
except KeyboardInterrupt:
58+
pass
59+
60+
61+
if __name__ == '__main__':
62+
main()
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
idf_component_register(SRCS "websocket_client_vfs.c"
2+
"main.c"
3+
INCLUDE_DIRS "."
4+
PRIV_REQUIRES console vfs nvs_flash esp_event esp_netif esp_wifi esp_ringbuf esp_eth)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
dependencies:
2+
idf: ">=5.0"
3+
espressif/esp_websocket_client:
4+
version: "^1.0.1"
5+
override_path: ../../../
6+
protocol_examples_common:
7+
path: ${IDF_PATH}/examples/common_components/protocol_examples_common
8+
cmd_nvs:
9+
path: ${IDF_PATH}/examples/system/console/advanced/components/cmd_nvs
10+
cmd_system:
11+
path: ${IDF_PATH}/examples/system/console/advanced/components/cmd_system
12+
espressif/console_cmd_ifconfig:
13+
version: "^1.0.0"
14+
espressif/console_cmd_ping:
15+
version: "^1.1.0"
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
3+
*
4+
* SPDX-License-Identifier: Unlicense OR CC0-1.0
5+
*/
6+
7+
/**
8+
* @file main.c
9+
* @brief Main application for ESP32 remote console over WebSocket.
10+
*
11+
* This application initializes a WebSocket client, redirects standard I/O to a WebSocket
12+
* via a VFS driver, and runs a console REPL for remote command execution.
13+
*/
14+
15+
#include <stdio.h>
16+
#include "esp_log.h"
17+
#include "esp_vfs.h"
18+
#include "esp_console.h"
19+
#include "nvs_flash.h"
20+
#include "protocol_examples_common.h"
21+
#include "freertos/FreeRTOS.h"
22+
#include "freertos/task.h"
23+
#include "freertos/ringbuf.h"
24+
#include "linenoise/linenoise.h"
25+
#include "argtable3/argtable3.h"
26+
#include "esp_websocket_client.h"
27+
#include "websocket_client_vfs.h"
28+
#include "cmd_nvs.h"
29+
#include "console_simple_init.h"
30+
31+
static const char *TAG = "remote_console";
32+
static const char *DEFAULT_WS_URI = "ws://192.168.50.231:8080";
33+
34+
static void websocket_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data);
35+
static esp_websocket_client_handle_t websocket_app_init(void);
36+
static void websocket_app_exit(esp_websocket_client_handle_t client);
37+
static void run_console_task(void);
38+
static void console_task(void* arg);
39+
static void vfs_exit(FILE* websocket_io);
40+
static FILE* vfs_init(void);
41+
42+
void app_main(void)
43+
{
44+
ESP_ERROR_CHECK(nvs_flash_init());
45+
ESP_ERROR_CHECK(esp_netif_init());
46+
ESP_ERROR_CHECK(esp_event_loop_create_default());
47+
ESP_ERROR_CHECK(example_connect());
48+
49+
esp_websocket_client_handle_t client = websocket_app_init();
50+
if (client == NULL) {
51+
ESP_LOGE(TAG, "Failed to initialize websocket client");
52+
return;
53+
}
54+
ESP_LOGI(TAG, "Websocket client initialized");
55+
56+
FILE* websocket_io = vfs_init();
57+
if (websocket_io == NULL) {
58+
ESP_LOGE(TAG, "Failed to open websocket I/O file");
59+
return;
60+
}
61+
62+
run_console_task();
63+
64+
while (true) {
65+
vTaskDelay(pdMS_TO_TICKS(1000));
66+
}
67+
68+
vfs_exit(websocket_io);
69+
websocket_app_exit(client);
70+
}
71+
72+
static esp_websocket_client_handle_t websocket_app_init(void)
73+
{
74+
websocket_client_vfs_config_t config = {
75+
.base_path = "/websocket",
76+
.send_timeout_ms = 10000,
77+
.recv_timeout_ms = 10000,
78+
.recv_buffer_size = 256,
79+
.fallback_stdout = stdout
80+
};
81+
ESP_ERROR_CHECK(websocket_client_vfs_register(&config));
82+
83+
esp_websocket_client_config_t websocket_cfg = {};
84+
//websocket_cfg.uri = "ws://192.168.50.231:8080";
85+
websocket_cfg.uri = DEFAULT_WS_URI;
86+
websocket_cfg.reconnect_timeout_ms = 1000;
87+
websocket_cfg.network_timeout_ms = 10000;
88+
89+
ESP_LOGI(TAG, "Connecting to %s...", websocket_cfg.uri);
90+
91+
esp_websocket_client_handle_t client = esp_websocket_client_init(&websocket_cfg);
92+
esp_websocket_register_events(client, WEBSOCKET_EVENT_ANY, websocket_event_handler, (void *)client);
93+
esp_websocket_client_start(client);
94+
websocket_client_vfs_add_client(client, 0);
95+
96+
return client;
97+
}
98+
99+
static void websocket_app_exit(esp_websocket_client_handle_t client)
100+
{
101+
esp_websocket_client_close(client, portMAX_DELAY);
102+
ESP_LOGI(TAG, "Websocket Stopped");
103+
esp_websocket_client_destroy(client);
104+
}
105+
106+
107+
/**
108+
* @brief Initialize VFS for WebSocket I/O redirection.
109+
* @return FILE pointer for WebSocket I/O, or NULL on failure.
110+
*/
111+
static FILE* vfs_init(void)
112+
{
113+
FILE *websocket_io = fopen("/websocket/0", "r+");
114+
if (!websocket_io) {
115+
ESP_LOGE(TAG, "Failed to open websocket I/O file");
116+
return NULL;
117+
}
118+
119+
stdin = websocket_io;
120+
stdout = websocket_io;
121+
setvbuf(stdin, NULL, _IONBF, 0);
122+
setvbuf(stdout, NULL, _IONBF, 0);
123+
124+
_GLOBAL_REENT->_stdin = websocket_io;
125+
_GLOBAL_REENT->_stdout = websocket_io;
126+
_GLOBAL_REENT->_stderr = websocket_io;
127+
128+
return websocket_io;
129+
}
130+
131+
/**
132+
* @brief Clean up VFS resources.
133+
* @param websocket_io FILE pointer for WebSocket I/O.
134+
*/
135+
static void vfs_exit(FILE* websocket_io)
136+
{
137+
if (websocket_io) {
138+
fclose(websocket_io);
139+
websocket_io = NULL;
140+
}
141+
}
142+
143+
/**
144+
* @brief WebSocket event handler.
145+
* @param handler_args User-defined arguments.
146+
* @param base Event base.
147+
* @param event_id Event ID.
148+
* @param event_data Event data.
149+
*/
150+
static void websocket_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data)
151+
{
152+
esp_websocket_event_data_t *data = (esp_websocket_event_data_t *)event_data;
153+
154+
switch (event_id) {
155+
case WEBSOCKET_EVENT_CONNECTED:
156+
ESP_LOGI(TAG, "Websocket connected");
157+
break;
158+
case WEBSOCKET_EVENT_DISCONNECTED:
159+
ESP_LOGI(TAG, "Websocket disconnected");
160+
break;
161+
case WEBSOCKET_EVENT_DATA:
162+
if (data->op_code == 0x08 && data->data_len == 2) {
163+
ESP_LOGI(TAG, "Received closed message with code=%d", 256 * data->data_ptr[0] + data->data_ptr[1]);
164+
}
165+
break;
166+
case WEBSOCKET_EVENT_ERROR:
167+
ESP_LOGI(TAG, "Websocket error");
168+
break;
169+
}
170+
171+
websocket_client_vfs_event_handler(data->client, event_id, event_data);
172+
}
173+
174+
175+
static void run_console_task(void)
176+
{
177+
vTaskDelay(pdMS_TO_TICKS(1000));
178+
xTaskCreate(console_task, "console_task", 16 * 1024, NULL, 5, NULL);
179+
}
180+
181+
182+
static void console_task(void* arg)
183+
{
184+
// Initialize console REPL
185+
ESP_ERROR_CHECK(console_cmd_init());
186+
ESP_ERROR_CHECK(console_cmd_all_register());
187+
188+
// start console REPL
189+
ESP_ERROR_CHECK(console_cmd_start());
190+
191+
while (true) {
192+
//fprintf(websocket_io, "From: %s(%d)\n", __func__, __LINE__);
193+
vTaskDelay(pdMS_TO_TICKS(5000));
194+
}
195+
196+
vTaskDelete(NULL);
197+
}

0 commit comments

Comments
 (0)