From fbb752d8c217ed9f236cfb115a9d12867f9085b5 Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Thu, 20 Mar 2025 23:11:30 +0100 Subject: [PATCH] Add Telnet server using command `Telnet <0|1|port>[,]` --- BUILDS.md | 1 + CHANGELOG.md | 1 + CODE_OWNERS.md | 3 +- RELEASENOTES.md | 1 + tasmota/include/tasmota_globals.h | 5 +- tasmota/my_user_config.h | 3 + tasmota/tasmota_support/support.ino | 16 +- tasmota/tasmota_support/support_features.ino | 6 +- .../xdrv_08_serial_bridge.ino | 12 +- .../xdrv_52_3_berry_tasmota.ino | 5 +- .../tasmota_xdrv_driver/xdrv_78_telnet.ino | 227 ++++++++++++++++++ tools/decode-status.py | 2 +- 12 files changed, 264 insertions(+), 18 deletions(-) create mode 100644 tasmota/tasmota_xdrv_driver/xdrv_78_telnet.ino diff --git a/BUILDS.md b/BUILDS.md index a76078765..01290816c 100644 --- a/BUILDS.md +++ b/BUILDS.md @@ -19,6 +19,7 @@ Note: the `minimal` variant is not listed as it shouldn't be used outside of the | USE_4K_RSA | - | - / - | - | - | - | - | | USE_TELEGRAM | - | - / - | - | - | - | - | | USE_KNX | - | - / x | x | - | - | - | +| USE_TELNET | - | - / - | - | - | - | - | | USE_WEBSERVER | x | x / x | x | x | x | x | | USE_WEBSEND_RESPONSE | - | - / - | - | - | - | - | | USE_EMULATION_HUE | x | x / x | - | x | - | - | diff --git a/CHANGELOG.md b/CHANGELOG.md index 094612a7e..b533d9528 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file. - Support for HLK-LD2402 24GHz smart wave motion sensor (#23133) - Matter prepare for ICD cluster (#23158) - Berry `re.dump()` (#23162) +- Telnet server using command `Telnet <0|1|port>[,]` ### Breaking Changed - Berry remove `Leds.create_matrix` from the standard library waiting for reimplementation (#23114) diff --git a/CODE_OWNERS.md b/CODE_OWNERS.md index b2de285ee..f75deeeeb 100644 --- a/CODE_OWNERS.md +++ b/CODE_OWNERS.md @@ -89,8 +89,9 @@ In addition to @arendst the following code is mainly owned by: | xdrv_75_dali | @eeak, @arendst | xdrv_76_serial_i2c | @s-hadinger | xdrv_77_wizmote | @arendst -| xdrv_78 | +| xdrv_78_telnet | @arendst | xdrv_79_esp32_ble | @staars, @btsimonh +| xdrv_80 | | xdrv_81_esp32_webcam | @gemu, @philrich | xdrv_82_esp32_ethernet | @arendst | xdrv_83_esp32_watch | @gemu diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 74c4313de..ec4c20c36 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -116,6 +116,7 @@ The latter links can be used for OTA upgrades too like ``OtaUrl https://ota.tasm ## Changelog v14.5.0.2 ### Added +- Telnet server using command `Telnet <0|1|port>[,]` - Support Vango Technologies V924x ultralow power, single-phase, power measurement [#23127](https://github.com/arendst/Tasmota/issues/23127) - Support for HLK-LD2402 24GHz smart wave motion sensor [#23133](https://github.com/arendst/Tasmota/issues/23133) - Allow acl in mqtt when client certificate is in use with `#define USE_MQTT_CLIENT_CERT` [#22998](https://github.com/arendst/Tasmota/issues/22998) diff --git a/tasmota/include/tasmota_globals.h b/tasmota/include/tasmota_globals.h index 7c02d9f7f..542d71228 100644 --- a/tasmota/include/tasmota_globals.h +++ b/tasmota/include/tasmota_globals.h @@ -47,8 +47,11 @@ extern "C" int startWaveformClockCycles(uint8_t pin, uint32_t highCcys, uint32_t uint32_t runTimeCcys, int8_t alignPhase, uint32_t phaseOffsetCcys, bool autoPwm); extern "C" void setTimer1Callback(uint32_t (*fn)()); #ifdef USE_SERIAL_BRIDGE -void SerialBridgePrintf(PGM_P formatP, ...); +void SerialBridgePrint(char *data); #endif +#ifdef USE_TELNET +void TelnetPrint(char *data); +#endif // USE_TELNET #ifdef USE_INFLUXDB void InfluxDbProcess(bool use_copy = false); #endif diff --git a/tasmota/my_user_config.h b/tasmota/my_user_config.h index 0d4885e37..a97507729 100644 --- a/tasmota/my_user_config.h +++ b/tasmota/my_user_config.h @@ -485,6 +485,9 @@ //#define USE_KNX // Enable KNX IP Protocol Support (+9.4k code, +3k7 mem) #define USE_KNX_WEB_MENU // Enable KNX WEB MENU (+8.3k code, +144 mem) +// -- Telnet -------------------------------------- +//#define USE_TELNET // Add support for telnet (+1k3 code) + // -- HTTP ---------------------------------------- #define USE_WEBSERVER // Enable web server and Wi-Fi Manager (+66k code, +8k mem) #define WEB_PORT 80 // Web server Port for User and Admin mode diff --git a/tasmota/tasmota_support/support.ino b/tasmota/tasmota_support/support.ino index 659bf8b4d..e705c78a7 100755 --- a/tasmota/tasmota_support/support.ino +++ b/tasmota/tasmota_support/support.ino @@ -2639,16 +2639,26 @@ void AddLogData(uint32_t loglevel, const char* log_data, const char* log_data_pa if ((loglevel <= TasmotaGlobal.seriallog_level) && (TasmotaGlobal.masterlog_level <= TasmotaGlobal.seriallog_level)) { - TasConsole.printf("%s%s%s%s\r\n", mxtime, log_data, log_data_payload, log_data_retained); + char* data = ext_snprintf_malloc_P("%s%s%s%s\r\n", mxtime, log_data, log_data_payload, log_data_retained); + if (data) { + TasConsole.print(data); #ifdef USE_SERIAL_BRIDGE - SerialBridgePrintf("%s%s%s%s\r\n", mxtime, log_data, log_data_payload, log_data_retained); + SerialBridgePrint(data); #endif // USE_SERIAL_BRIDGE +#ifdef USE_TELNET + TelnetPrint(data); +#endif // USE_TELNET + free(data); + } } if (!TasmotaGlobal.log_buffer) { return; } // Leave now if there is no buffer available - uint32_t highest_loglevel = Settings->weblog_level; + uint32_t highest_loglevel = Settings->seriallog_level; // Need this for Telnet if (Settings->mqttlog_level > highest_loglevel) { highest_loglevel = Settings->mqttlog_level; } +#ifdef USE_WEBSERVER + if (Settings->weblog_level > highest_loglevel) { highest_loglevel = Settings->weblog_level; } +#endif // USE_WEBSERVER #ifdef USE_UFILESYS uint32_t filelog_level = Settings->filelog_level % 10; if (filelog_level > highest_loglevel) { highest_loglevel = filelog_level; } diff --git a/tasmota/tasmota_support/support_features.ino b/tasmota/tasmota_support/support_features.ino index 0f70a3c2a..06398a9ad 100644 --- a/tasmota/tasmota_support/support_features.ino +++ b/tasmota/tasmota_support/support_features.ino @@ -945,8 +945,10 @@ constexpr uint32_t feature[] = { #endif #if defined(USE_ENERGY_SENSOR) && defined(USE_V9240) 0x00004000 | // xnrg_25_v9240.ino -#endif -// 0x00008000 | // +#endif +#ifdef USE_TELNET + 0x00008000 | // xdrv_80_telnet.ino +#endif // 0x00010000 | // // 0x00020000 | // // 0x00040000 | // diff --git a/tasmota/tasmota_xdrv_driver/xdrv_08_serial_bridge.ino b/tasmota/tasmota_xdrv_driver/xdrv_08_serial_bridge.ino index 0e6bbf279..d56a637d6 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_08_serial_bridge.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_08_serial_bridge.ino @@ -99,18 +99,11 @@ void SetSSerialConfig(uint32_t serial_config) { } } -void SerialBridgePrintf(PGM_P formatP, ...) { +void SerialBridgePrint(char *data) { #ifdef USE_SERIAL_BRIDGE_TEE if ((SB_TEE == Settings->sserial_mode) && serial_bridge_buffer) { - va_list arg; - va_start(arg, formatP); - char* data = ext_vsnprintf_malloc_P(formatP, arg); - va_end(arg); - if (data == nullptr) { return; } - // SerialBridgeSerial->printf(data); // This resolves "MqttClientMask":"DVES_%06X" into "DVES_000002" SerialBridgeSerial->print(data); // This does not resolve "DVES_%06X" - free(data); } #endif // USE_SERIAL_BRIDGE_TEE } @@ -274,7 +267,8 @@ void SerialBridgeInit(void) { AddLog(LOG_LEVEL_DEBUG, PSTR("SBR: Serial UART%d"), SerialBridgeSerial->getUart()); #endif SerialBridgeSerial->flush(); - SerialBridgePrintf("\r\n"); + char data[] = "\r\n"; + SerialBridgePrint(data); } } } diff --git a/tasmota/tasmota_xdrv_driver/xdrv_52_3_berry_tasmota.ino b/tasmota/tasmota_xdrv_driver/xdrv_52_3_berry_tasmota.ino index 08d160b99..de294dc0a 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_52_3_berry_tasmota.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_52_3_berry_tasmota.ino @@ -1089,8 +1089,11 @@ extern "C" { if (len+3 > LOGSZ) { strcat(log_data, "..."); } // Actual data is more TasConsole.printf(log_data); #ifdef USE_SERIAL_BRIDGE - SerialBridgePrintf(log_data); + SerialBridgePrint(log_data); #endif // USE_SERIAL_BRIDGE +#ifdef USE_TELNET + TelnetPrint(log_data); +#endif // USE_TELNET } void berry_log_C(const char * berry_buf, ...) { diff --git a/tasmota/tasmota_xdrv_driver/xdrv_78_telnet.ino b/tasmota/tasmota_xdrv_driver/xdrv_78_telnet.ino new file mode 100644 index 000000000..28a3ffba2 --- /dev/null +++ b/tasmota/tasmota_xdrv_driver/xdrv_78_telnet.ino @@ -0,0 +1,227 @@ +/* + xdrv_78_telnet.ino - Telnet console support for Tasmota + + SPDX-FileCopyrightText: 2025 Theo Arends + + SPDX-License-Identifier: GPL-3.0-only +*/ + +#ifdef USE_TELNET +/*********************************************************************************************\ + * Telnet console support for a single connection +\*********************************************************************************************/ + +#define XDRV_78 78 + +#ifndef TELNET_BUF_SIZE +#define TELNET_BUF_SIZE 255 // size of the buffer +#endif + +struct { + WiFiServer *server = nullptr; + WiFiClient client; + IPAddress ip_filter; + char *buffer = nullptr; // data transfer buffer + uint16_t port; + uint16_t buffer_size = TELNET_BUF_SIZE; + bool ip_filter_enabled = false; +} Telnet; + +/********************************************************************************************/ + +void TelnetPrint(char *data) { + if (Telnet.server) { + WiFiClient &client = Telnet.client; + if (client) { +// client.printf(data); // This resolves "MqttClientMask":"DVES_%06X" into "DVES_000002" + client.print(data); // This does not resolve "DVES_%06X" + } + } +} + +/********************************************************************************************/ + +void TelnetLoop(void) { + // check for a new client connection + if ((Telnet.server) && (Telnet.server->hasClient())) { + WiFiClient new_client = Telnet.server->available(); + + AddLog(LOG_LEVEL_INFO, PSTR("TLN: Connection from %s"), new_client.remoteIP().toString().c_str()); + + if (Telnet.ip_filter_enabled) { // Check for IP filtering if it's enabled + if (Telnet.ip_filter != new_client.remoteIP()) { + AddLog(LOG_LEVEL_INFO, PSTR("TLN: Rejected due to filtering")); + new_client.stop(); + } else { + AddLog(LOG_LEVEL_INFO, PSTR("TLN: Allowed through filter")); + } + } + + WiFiClient &client = Telnet.client; + if (client) { + client.stop(); + } + client = new_client; + if (client) { + client.printf("Tasmota %s %s (%s)\r\n\n", TasmotaGlobal.hostname, TasmotaGlobal.version, GetBuildDateAndTime().c_str()); + uint32_t index = 1; + char* line; + size_t len; + while (GetLog(TasmotaGlobal.seriallog_level, &index, &line, &len)) { + // [14:49:36.123 MQTT: stat/wemos5/RESULT = {"POWER":"OFF"}] > [{"POWER":"OFF"}] + client.write(line, len -1); + client.write("\r\n"); + } + client.printf("%s:# ", TasmotaGlobal.hostname); + } + } + + bool busy; + uint32_t buf_len = 0; + do { + busy = false; // exit loop if no data was transferred + WiFiClient &client = Telnet.client; + bool overrun = false; + while (client && (client.available())) { + uint8_t c = client.read(); + if (c >= 0) { + busy = true; + if (isprint(c)) { // Any char between 32 and 127 + if (buf_len < Telnet.buffer_size -1) { // Add char to string if it still fits + Telnet.buffer[buf_len++] = c; + } else { + overrun = true; // Signal overrun but continue reading input to flush until '\n' (EOL) + } + } + else if (c == '\n') { + Telnet.buffer[buf_len] = 0; // Telnet data completed + TasmotaGlobal.seriallog_level = (Settings->seriallog_level < LOG_LEVEL_INFO) ? (uint8_t)LOG_LEVEL_INFO : Settings->seriallog_level; + if (overrun) { + AddLog(LOG_LEVEL_INFO, PSTR("TLN: buffer overrun")); + } else { + AddLog(LOG_LEVEL_INFO, PSTR("TLN: %s"), Telnet.buffer); + ExecuteCommand(Telnet.buffer, SRC_REMOTE); + } + client.flush(); + client.printf("%s:# ", TasmotaGlobal.hostname); + return; + } + } + } + yield(); // avoid WDT if heavy traffic + } while (busy); +} + +/*********************************************************************************************\ + * Commands +\*********************************************************************************************/ + +void TelnetStop(void) { + Telnet.server->stop(); + delete Telnet.server; + Telnet.server = nullptr; + + WiFiClient &client = Telnet.client; + client.stop(); + + free(Telnet.buffer); + Telnet.buffer = nullptr; +} + +const char kTelnetCommands[] PROGMEM = "Telnet|" // prefix + "|Buffer"; + +void (* const TelnetCommand[])(void) PROGMEM = { + &CmndTelnet, &CmndTelnetBuffer }; + +void CmndTelnet(void) { + // Telnet - Show telnet server state + // Telnet 0 - Disable telnet server + // Telnet 1 - Enable telnet server on port 23 + // Telnet 23 - Enable telnet server on port 23 + // Telnet 1, 192.168.2.1 - Enable telnet server and only allow connection from 192.168.2.1 + if (!TasmotaGlobal.global_state.network_down) { + if (XdrvMailbox.data_len) { + Telnet.port = XdrvMailbox.payload; + + if (ArgC() == 2) { + char sub_string[XdrvMailbox.data_len]; + Telnet.ip_filter.fromString(ArgV(sub_string, 2)); + Telnet.ip_filter_enabled = true; + } else { + // Disable whitelist if previously set + Telnet.ip_filter_enabled = false; + } + + if (Telnet.server) { + TelnetStop(); + } + + if (Telnet.port > 0) { + if (!Telnet.buffer) { + Telnet.buffer = (char*)malloc(Telnet.buffer_size); + if (!Telnet.buffer) { return; } + + if (1 == Telnet.port) { Telnet.port = 23; } + Telnet.server = new WiFiServer(Telnet.port); + Telnet.server->begin(); // start TCP server + Telnet.server->setNoDelay(true); + } + } + } + if (Telnet.server) { + ResponseCmndChar_P(PSTR("Started")); + } else { + ResponseCmndChar_P(PSTR("Stopped")); + } + } +} + +void CmndTelnetBuffer(void) { + // TelnetBuffer - Show current input buffer size (default 255) + // TelnetBuffer 300 - Change input buffer size to 300 characters + if (XdrvMailbox.data_len > 0) { + uint16_t bsize = Telnet.buffer_size; + Telnet.buffer_size = XdrvMailbox.payload; + if (XdrvMailbox.payload < MIN_INPUT_BUFFER_SIZE) { + Telnet.buffer_size = MIN_INPUT_BUFFER_SIZE; // 256 / 256 + } + else if (XdrvMailbox.payload > INPUT_BUFFER_SIZE) { + Telnet.buffer_size = INPUT_BUFFER_SIZE; // 256 / 800 + } + + if (Telnet.buffer && (bsize != Telnet.buffer_size)) { + Telnet.buffer = (char*)realloc(Telnet.buffer, Telnet.buffer_size); + if (!Telnet.buffer) { + TelnetStop(); + ResponseCmndChar_P(PSTR("Stopped")); + return; + } + } + } + ResponseCmndNumber(Telnet.buffer_size); +} + +/*********************************************************************************************\ + * Interface +\*********************************************************************************************/ + +bool Xdrv78(uint32_t function) { + bool result = false; + + if (FUNC_COMMAND == function) { + result = DecodeCommand(kTelnetCommands, TelnetCommand); + } else if (Telnet.buffer) { + switch (function) { + case FUNC_LOOP: + TelnetLoop(); + break; + case FUNC_ACTIVE: + result = true; + break; + } + } + return result; +} + +#endif // USE_TELNET diff --git a/tools/decode-status.py b/tools/decode-status.py index ff90a8631..5395d784b 100755 --- a/tools/decode-status.py +++ b/tools/decode-status.py @@ -311,7 +311,7 @@ a_features = [[ "USE_MAGIC_SWITCH","USE_PIPSOLAR","USE_GPIO_VIEWER","USE_AMSX915", "USE_SPI_LORA","USE_SPL06_007","USE_QMP6988","USE_WOOLIIS", "USE_HX711_M5SCALES","USE_RX8010","USE_PCF85063","USE_ESP32_TWAI", - "USE_C8_CO2_5K","USE_WIZMOTE","USE_V9240","", + "USE_C8_CO2_5K","USE_WIZMOTE","USE_V9240","USE_TELNET", "","","","", "","","","", "","","","",