From d4d98ebbbe23e89a02bba0c776f464b4ba13b470 Mon Sep 17 00:00:00 2001 From: Piero Andreini Date: Mon, 30 Mar 2026 15:31:14 +0200 Subject: [PATCH] Add TCP console for remote management via telnet/netcat --- examples/companion_radio/main.cpp | 20 ++- examples/simple_repeater/main.cpp | 13 ++ examples/simple_room_server/main.cpp | 19 ++- src/helpers/esp32/TCPConsole.h | 142 ++++++++++++++++++ .../lilygo_t_eth_elite_sx1262/platformio.ini | 8 +- .../lilygo_t_eth_elite_sx1276/platformio.ini | 8 +- 6 files changed, 200 insertions(+), 10 deletions(-) create mode 100644 src/helpers/esp32/TCPConsole.h diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 6bf09022..d9a1c3df 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -2,6 +2,11 @@ #include #include "MyMesh.h" +#if defined(ESP32) && defined(TCP_CONSOLE_PORT) && defined(ADMIN_PASSWORD) + #include + TCPConsole tcp_console(ADMIN_PASSWORD, ADVERT_NAME); +#endif + // Believe it or not, this std C function is busted on some platforms! static uint32_t _atoi(const char* sp) { uint32_t n = 0; @@ -109,6 +114,10 @@ void setup() { Serial.begin(115200); board.begin(); + + #if defined(ESP32) && defined(TCP_CONSOLE_PORT) && defined(ADMIN_PASSWORD) + tcp_console.begin(); + #endif #ifdef DISPLAY_CLASS DisplayDriver* disp = NULL; @@ -227,9 +236,14 @@ void setup() { void loop() { the_mesh.loop(); + #if defined(ESP32) && defined(TCP_CONSOLE_PORT) && defined(ADMIN_PASSWORD) + tcp_console.loop(the_mesh); + #endif + sensors.loop(); -#ifdef DISPLAY_CLASS - ui_task.loop(); -#endif + #ifdef DISPLAY_CLASS + ui_task.loop(); + #endif + rtc_clock.tick(); } diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index e37078ce..f00b1219 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -3,6 +3,11 @@ #include "MyMesh.h" +#if defined(ESP32) && defined(TCP_CONSOLE_PORT) + #include + TCPConsole tcp_console(ADMIN_PASSWORD, ADVERT_NAME); +#endif + #ifdef DISPLAY_CLASS #include "UITask.h" static UITask ui_task(display); @@ -34,6 +39,10 @@ void setup() { board.begin(); + #if defined(ESP32) && defined(TCP_CONSOLE_PORT) + tcp_console.begin(); + #endif + #if defined(MESH_DEBUG) && defined(NRF52_PLATFORM) // give some extra time for serial to settle so // boot debug messages can be seen on terminal @@ -148,6 +157,10 @@ void loop() { #endif the_mesh.loop(); + #if defined(ESP32) && defined(TCP_CONSOLE_PORT) + tcp_console.loop(the_mesh); + #endif + sensors.loop(); #ifdef DISPLAY_CLASS ui_task.loop(); diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index 825fb007..1237c575 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -3,6 +3,11 @@ #include "MyMesh.h" +#if defined(ESP32) && defined(TCP_CONSOLE_PORT) + #include + TCPConsole tcp_console(ADMIN_PASSWORD, ADVERT_NAME); +#endif + #ifdef DISPLAY_CLASS #include "UITask.h" static UITask ui_task(display); @@ -24,6 +29,10 @@ void setup() { board.begin(); + #if defined(ESP32) && defined(TCP_CONSOLE_PORT) + tcp_console.begin(); + #endif + #ifdef DISPLAY_CLASS if (display.begin()) { display.startFrame(); @@ -108,9 +117,13 @@ void loop() { } the_mesh.loop(); + #if defined(ESP32) && defined(TCP_CONSOLE_PORT) + tcp_console.loop(the_mesh); + #endif + sensors.loop(); -#ifdef DISPLAY_CLASS - ui_task.loop(); -#endif + #ifdef DISPLAY_CLASS + ui_task.loop(); + #endif rtc_clock.tick(); } diff --git a/src/helpers/esp32/TCPConsole.h b/src/helpers/esp32/TCPConsole.h new file mode 100644 index 00000000..cbc98d26 --- /dev/null +++ b/src/helpers/esp32/TCPConsole.h @@ -0,0 +1,142 @@ +#pragma once + +#if defined(ESP32) && defined(TCP_CONSOLE_PORT) && defined(ADMIN_PASSWORD) + +#include +#include +#include +#include + +#ifndef TCP_CONSOLE_MAX_CLIENTS + #define TCP_CONSOLE_MAX_CLIENTS 2 +#endif + +#ifndef TCP_CONSOLE_TIMEOUT_MS + #define TCP_CONSOLE_TIMEOUT_MS 300000 // 5 minutes inactivity timeout +#endif + +class TCPConsole { + WiFiServer _server; + WiFiClient _clients[TCP_CONSOLE_MAX_CLIENTS]; + bool _authenticated[TCP_CONSOLE_MAX_CLIENTS]; + unsigned long _last_active[TCP_CONSOLE_MAX_CLIENTS]; + char _cmd_buf[TCP_CONSOLE_MAX_CLIENTS][160]; + int _cmd_len[TCP_CONSOLE_MAX_CLIENTS]; + const char* _password; + const char* _node_name; + + void sendToClient(int i, const char* msg) { + if (_clients[i] && _clients[i].connected()) { + _clients[i].print(msg); + } + } + + void disconnectClient(int i) { + _clients[i].stop(); + _authenticated[i] = false; + _cmd_buf[i][0] = 0; + _cmd_len[i] = 0; + } + +public: + TCPConsole(const char* password, const char* node_name) + : _server(TCP_CONSOLE_PORT), _password(password), _node_name(node_name) { + for (int i = 0; i < TCP_CONSOLE_MAX_CLIENTS; i++) { + _authenticated[i] = false; + _cmd_buf[i][0] = 0; + _cmd_len[i] = 0; + _last_active[i] = 0; + } + } + + void begin() { + _server.begin(); + Serial.printf("TCP Console listening on port %d\n", TCP_CONSOLE_PORT); + } + + // Call this from loop(), passing the mesh's handleCommand function + template + void loop(T& mesh) { + // Accept new clients + WiFiClient newClient = _server.available(); + if (newClient) { + for (int i = 0; i < TCP_CONSOLE_MAX_CLIENTS; i++) { + if (!_clients[i] || !_clients[i].connected()) { + _clients[i] = newClient; + _authenticated[i] = false; + _cmd_buf[i][0] = 0; + _cmd_len[i] = 0; + _last_active[i] = millis(); + sendToClient(i, "MeshCore Console\r\nPassword: "); + break; + } + } + } + + // Handle connected clients + for (int i = 0; i < TCP_CONSOLE_MAX_CLIENTS; i++) { + if (!_clients[i] || !_clients[i].connected()) continue; + + // Inactivity timeout + if (millis() - _last_active[i] > TCP_CONSOLE_TIMEOUT_MS) { + sendToClient(i, "\r\nTimeout. Disconnecting.\r\n"); + disconnectClient(i); + continue; + } + + // Read available data + while (_clients[i].available()) { + _last_active[i] = millis(); + char c = _clients[i].read(); + + if (c == '\n') continue; // ignore LF, handle CR only + + if (c != '\r' && _cmd_len[i] < 158) { + _cmd_buf[i][_cmd_len[i]++] = c; + _cmd_buf[i][_cmd_len[i]] = 0; + if (!_authenticated[i]) { + sendToClient(i, "*"); // mask password input + } else { + _clients[i].print(c); // echo command + } + continue; + } + + // Got CR — process command + sendToClient(i, "\r\n"); + _cmd_buf[i][_cmd_len[i]] = 0; + + if (!_authenticated[i]) { + // Authentication + if (strcmp(_cmd_buf[i], _password) == 0) { + _authenticated[i] = true; + char welcome[80]; + snprintf(welcome, sizeof(welcome), "Welcome to %s console.\r\n> ", _node_name); + sendToClient(i, welcome); + } else { + sendToClient(i, "Wrong password. Disconnecting.\r\n"); + disconnectClient(i); + } + } else { + // Execute command + if (strlen(_cmd_buf[i]) > 0) { + char reply[160]; + reply[0] = 0; + mesh.handleCommand(0, _cmd_buf[i], reply); + if (reply[0]) { + sendToClient(i, " -> "); + sendToClient(i, reply); + sendToClient(i, "\r\n"); + } + } + sendToClient(i, "> "); + } + + _cmd_buf[i][0] = 0; + _cmd_len[i] = 0; + } + } + } +}; + +#endif // ESP32 && TCP_CONSOLE_PORT diff --git a/variants/lilygo_t_eth_elite_sx1262/platformio.ini b/variants/lilygo_t_eth_elite_sx1262/platformio.ini index f3f0c2cb..08410cde 100644 --- a/variants/lilygo_t_eth_elite_sx1262/platformio.ini +++ b/variants/lilygo_t_eth_elite_sx1262/platformio.ini @@ -49,7 +49,7 @@ lib_deps = extends = LilyGo_T_ETH_Elite_SX1262 build_flags = ${LilyGo_T_ETH_Elite_SX1262.build_flags} - -D ADVERT_NAME='"T-ETH Elite SX1262 Repeater"' + -D ADVERT_NAME='"T-ETH Elite SX1262 Repeater eth"' -D ADVERT_LAT=0 -D ADVERT_LON=0 -D ADMIN_PASSWORD='"password"' @@ -59,6 +59,7 @@ build_flags = -D ETH_GATEWAY=192,168,254,254 -D ETH_SUBNET=255,255,255,0 -D ETH_DNS=8,8,8,8 + -D TCP_CONSOLE_PORT=4242 build_src_filter = ${LilyGo_T_ETH_Elite_SX1262.build_src_filter} +<../examples/simple_repeater> lib_deps = @@ -101,7 +102,7 @@ lib_deps = extends = LilyGo_T_ETH_Elite_SX1262 build_flags = ${LilyGo_T_ETH_Elite_SX1262.build_flags} - -D ADVERT_NAME='"T-ETH Elite SX1262 Room"' + -D ADVERT_NAME='"T-ETH Elite SX1262 Room eth"' -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' @@ -111,6 +112,7 @@ build_flags = -D ETH_GATEWAY=192,168,254,254 -D ETH_SUBNET=255,255,255,0 -D ETH_DNS=8,8,8,8 + -D TCP_CONSOLE_PORT=4242 build_src_filter = ${LilyGo_T_ETH_Elite_SX1262.build_src_filter} +<../examples/simple_room_server> lib_deps = @@ -130,6 +132,7 @@ build_flags = -D ETH_GATEWAY=192,168,254,254 -D ETH_SUBNET=255,255,255,0 -D ETH_DNS=8,8,8,8 + -D TCP_CONSOLE_PORT=4242 build_src_filter = ${LilyGo_T_ETH_Elite_SX1262.build_src_filter} + + @@ -149,6 +152,7 @@ build_flags = -D OFFLINE_QUEUE_SIZE=256 -D WIFI_SSID='"WIFI_SSID"' -D WIFI_PWD='"Password"' + -D TCP_CONSOLE_PORT=4242 build_src_filter = ${LilyGo_T_ETH_Elite_SX1262.build_src_filter} + + diff --git a/variants/lilygo_t_eth_elite_sx1276/platformio.ini b/variants/lilygo_t_eth_elite_sx1276/platformio.ini index 512c650b..3b6184cd 100644 --- a/variants/lilygo_t_eth_elite_sx1276/platformio.ini +++ b/variants/lilygo_t_eth_elite_sx1276/platformio.ini @@ -48,7 +48,7 @@ lib_deps = extends = LilyGo_T_ETH_Elite_SX1276 build_flags = ${LilyGo_T_ETH_Elite_SX1276.build_flags} - -D ADVERT_NAME='"T-ETH Elite SX1276 Repeater"' + -D ADVERT_NAME='"T-ETH Elite SX1276 Repeater eth"' -D ADVERT_LAT=0 -D ADVERT_LON=0 -D ADMIN_PASSWORD='"password"' @@ -58,6 +58,7 @@ build_flags = -D ETH_GATEWAY=192,168,254,254 -D ETH_SUBNET=255,255,255,0 -D ETH_DNS=8,8,8,8 + -D TCP_CONSOLE_PORT=4242 build_src_filter = ${LilyGo_T_ETH_Elite_SX1276.build_src_filter} +<../examples/simple_repeater> lib_deps = @@ -100,7 +101,7 @@ lib_deps = extends = LilyGo_T_ETH_Elite_SX1276 build_flags = ${LilyGo_T_ETH_Elite_SX1276.build_flags} - -D ADVERT_NAME='"T-ETH Elite SX1276 Room"' + -D ADVERT_NAME='"T-ETH Elite SX1276 Room eth"' -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' @@ -110,6 +111,7 @@ build_flags = -D ETH_GATEWAY=192,168,254,254 -D ETH_SUBNET=255,255,255,0 -D ETH_DNS=8,8,8,8 + -D TCP_CONSOLE_PORT=4242 build_src_filter = ${LilyGo_T_ETH_Elite_SX1276.build_src_filter} +<../examples/simple_room_server> lib_deps = @@ -129,6 +131,7 @@ build_flags = -D ETH_GATEWAY=192,168,254,254 -D ETH_SUBNET=255,255,255,0 -D ETH_DNS=8,8,8,8 + -D TCP_CONSOLE_PORT=4242 build_src_filter = ${LilyGo_T_ETH_Elite_SX1276.build_src_filter} + + @@ -148,6 +151,7 @@ build_flags = -D OFFLINE_QUEUE_SIZE=256 -D WIFI_SSID='"WIFI_SSID"' -D WIFI_PWD='"Password"' + -D TCP_CONSOLE_PORT=4242 build_src_filter = ${LilyGo_T_ETH_Elite_SX1276.build_src_filter} + +