Skip to content

feat(websocket): Added remote console example over websocket #839

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# For more information about build system see
# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html
# The following five lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16)
set(COMPONENTS main)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(websocket-client-console_exp)
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# ESP32 Remote Console over WebSocket

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.

## Features
- WebSocket-based remote console with low-latency bidirectional communication.
- VFS driver redirects I/O to WebSocket, supporting standard C library functions.
- Console REPL for command execution, extensible via ESP-IDF console component.
- WebSocket URI configurable via NVS (default: `ws://192.168.50.231:8080`).

## Components
- **ESP32 Firmware**:
- `main.c`: Initializes network, WebSocket client, VFS, and console.
- `websocket_client_vfs.c/h`: VFS driver for WebSocket I/O redirection.
- **Python Server** (`console.py`): WebSocket server for console interaction using `websockets` and `aioconsole`.

## Prerequisites
- ESP-IDF v5.0+ configured.
- ESP32 board with Wi-Fi access.
- Python 3.7+ with `websockets` and `aioconsole` (`pip install websockets aioconsole`).
- Host machine on the same network as the ESP32.

## Setup

### ESP32
1. Clone the repository and navigate to the project directory.
2. Configure Wi-Fi:
```bash
idf.py menuconfig
```
- Set `Example Connection Configuration > WiFi SSID` and `WiFi Password`.
3. Build and flash:
```bash
idf.py -p /dev/ttyUSB0 build flash monitor
```

### Python Server
1. Run the WebSocket server:
```bash
python console.py --host 0.0.0.0 --port 8080
```
- Note the host’s IP (e.g., `192.168.50.231`) for ESP32 configuration.

## Usage
1. Start the Python server (`python console.py --host 0.0.0.0 --port 8080`).
2. Power on the ESP32; it connects to Wi-Fi and the WebSocket server.
3. In the Python server terminal, enter commands (e.g., `help`, `nvs`) and view responses.
4. Stop the server with `Ctrl+C` or reset the ESP32 to halt.

## Configuration
- **WebSocket URI**:
- Default: `ws://192.168.50.231:8080`.
- Or edit `DEFAULT_WS_URI` in `main.c` and reflash.
- **Wi-Fi**: Update via `menuconfig` or use provisioning (e.g., softAP).
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/usr/bin/env python
# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Unlicense OR CC0-1.0

import argparse
import asyncio

import aioconsole
import websockets

cur_websocket = None
last_input = ''


async def handler(websocket):
global cur_websocket
global last_input
cur_websocket = websocket
await aioconsole.aprint('Connected!')
while True:
try:
message = await websocket.recv()
if last_input and message.startswith(last_input):
message = message[len(last_input):]
last_input = ''
await aioconsole.aprint(message.decode('utf-8'), end='')
except websockets.exceptions.ConnectionClosedError:
return


async def websocket_loop(host: str, port: int):
async with websockets.serve(handler, host, port):
await asyncio.Future()


async def console_loop():
while True:
cmd = await aioconsole.ainput('') + '\n'
if cur_websocket is not None:
await cur_websocket.send(cmd.encode('utf-8'))


def main():
parser = argparse.ArgumentParser()
parser.add_argument(
'--host', type=str, help='Host to listen on', default='localhost'
)
parser.add_argument(
'-p', '--port', type=int, help='Port to listen on', default=8080
)
args = parser.parse_args()
loop = asyncio.get_event_loop()
loop.create_task(websocket_loop(args.host, args.port))
loop.create_task(console_loop())
try:
loop.run_forever()
except KeyboardInterrupt:
pass


if __name__ == '__main__':
main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
idf_component_register(SRCS "websocket_client_vfs.c"
"main.c"
INCLUDE_DIRS "."
PRIV_REQUIRES console vfs nvs_flash esp_event esp_netif esp_wifi esp_ringbuf esp_eth)
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
dependencies:
idf: ">=5.0"
espressif/esp_websocket_client:
version: "^1.0.1"
override_path: ../../../
protocol_examples_common:
path: ${IDF_PATH}/examples/common_components/protocol_examples_common
cmd_nvs:
path: ${IDF_PATH}/examples/system/console/advanced/components/cmd_nvs
cmd_system:
path: ${IDF_PATH}/examples/system/console/advanced/components/cmd_system
espressif/console_cmd_ifconfig:
version: "^1.0.0"
espressif/console_cmd_ping:
version: "^1.1.0"
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/

/**
* @file main.c
* @brief Main application for ESP32 remote console over WebSocket.
*
* This application initializes a WebSocket client, redirects standard I/O to a WebSocket
* via a VFS driver, and runs a console REPL for remote command execution.
*/

#include <stdio.h>
#include "esp_log.h"
#include "esp_vfs.h"
#include "esp_console.h"
#include "nvs_flash.h"
#include "protocol_examples_common.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/ringbuf.h"
#include "linenoise/linenoise.h"
#include "argtable3/argtable3.h"
#include "esp_websocket_client.h"
#include "websocket_client_vfs.h"
#include "cmd_nvs.h"
#include "console_simple_init.h"

static const char *TAG = "remote_console";
static const char *DEFAULT_WS_URI = "ws://192.168.50.231:8080";

static void websocket_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data);
static esp_websocket_client_handle_t websocket_app_init(void);
static void websocket_app_exit(esp_websocket_client_handle_t client);
static void run_console_task(void);
static void console_task(void* arg);
static void vfs_exit(FILE* websocket_io);
static FILE* vfs_init(void);

void app_main(void)
{
ESP_ERROR_CHECK(nvs_flash_init());
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
ESP_ERROR_CHECK(example_connect());

esp_websocket_client_handle_t client = websocket_app_init();
if (client == NULL) {
ESP_LOGE(TAG, "Failed to initialize websocket client");
return;
}
ESP_LOGI(TAG, "Websocket client initialized");

FILE* websocket_io = vfs_init();
if (websocket_io == NULL) {
ESP_LOGE(TAG, "Failed to open websocket I/O file");
return;
}

run_console_task();

while (true) {
vTaskDelay(pdMS_TO_TICKS(1000));
}

vfs_exit(websocket_io);
websocket_app_exit(client);
}

static esp_websocket_client_handle_t websocket_app_init(void)
{
websocket_client_vfs_config_t config = {
.base_path = "/websocket",
.send_timeout_ms = 10000,
.recv_timeout_ms = 10000,
.recv_buffer_size = 256,
.fallback_stdout = stdout
};
ESP_ERROR_CHECK(websocket_client_vfs_register(&config));

esp_websocket_client_config_t websocket_cfg = {};
//websocket_cfg.uri = "ws://192.168.50.231:8080";
websocket_cfg.uri = DEFAULT_WS_URI;
websocket_cfg.reconnect_timeout_ms = 1000;
websocket_cfg.network_timeout_ms = 10000;

ESP_LOGI(TAG, "Connecting to %s...", websocket_cfg.uri);

esp_websocket_client_handle_t client = esp_websocket_client_init(&websocket_cfg);
esp_websocket_register_events(client, WEBSOCKET_EVENT_ANY, websocket_event_handler, (void *)client);
esp_websocket_client_start(client);
websocket_client_vfs_add_client(client, 0);

return client;
}

static void websocket_app_exit(esp_websocket_client_handle_t client)
{
esp_websocket_client_close(client, portMAX_DELAY);
ESP_LOGI(TAG, "Websocket Stopped");
esp_websocket_client_destroy(client);
}


/**
* @brief Initialize VFS for WebSocket I/O redirection.
* @return FILE pointer for WebSocket I/O, or NULL on failure.
*/
static FILE* vfs_init(void)
{
FILE *websocket_io = fopen("/websocket/0", "r+");
if (!websocket_io) {
ESP_LOGE(TAG, "Failed to open websocket I/O file");
return NULL;
}

stdin = websocket_io;
stdout = websocket_io;
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);

_GLOBAL_REENT->_stdin = websocket_io;
_GLOBAL_REENT->_stdout = websocket_io;
_GLOBAL_REENT->_stderr = websocket_io;

return websocket_io;
}

/**
* @brief Clean up VFS resources.
* @param websocket_io FILE pointer for WebSocket I/O.
*/
static void vfs_exit(FILE* websocket_io)
{
if (websocket_io) {
fclose(websocket_io);
websocket_io = NULL;
}
}

/**
* @brief WebSocket event handler.
* @param handler_args User-defined arguments.
* @param base Event base.
* @param event_id Event ID.
* @param event_data Event data.
*/
static void websocket_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data)
{
esp_websocket_event_data_t *data = (esp_websocket_event_data_t *)event_data;

switch (event_id) {
case WEBSOCKET_EVENT_CONNECTED:
ESP_LOGI(TAG, "Websocket connected");
break;
case WEBSOCKET_EVENT_DISCONNECTED:
ESP_LOGI(TAG, "Websocket disconnected");
break;
case WEBSOCKET_EVENT_DATA:
if (data->op_code == 0x08 && data->data_len == 2) {
ESP_LOGI(TAG, "Received closed message with code=%d", 256 * data->data_ptr[0] + data->data_ptr[1]);
}
break;
case WEBSOCKET_EVENT_ERROR:
ESP_LOGI(TAG, "Websocket error");
break;
}

websocket_client_vfs_event_handler(data->client, event_id, event_data);
}


static void run_console_task(void)
{
vTaskDelay(pdMS_TO_TICKS(1000));
xTaskCreate(console_task, "console_task", 16 * 1024, NULL, 5, NULL);
}


static void console_task(void* arg)
{
// Initialize console REPL
ESP_ERROR_CHECK(console_cmd_init());
ESP_ERROR_CHECK(console_cmd_all_register());

// start console REPL
ESP_ERROR_CHECK(console_cmd_start());

while (true) {
//fprintf(websocket_io, "From: %s(%d)\n", __func__, __LINE__);
vTaskDelay(pdMS_TO_TICKS(5000));
}

vTaskDelete(NULL);
}
Loading