Skip to content

Commit fbb752d

Browse files
committed
Add Telnet server using command Telnet <0|1|port>[,<IP filter>]
1 parent fb6640b commit fbb752d

File tree

12 files changed

+264
-18
lines changed

12 files changed

+264
-18
lines changed

BUILDS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Note: the `minimal` variant is not listed as it shouldn't be used outside of the
1919
| USE_4K_RSA | - | - / - | - | - | - | - |
2020
| USE_TELEGRAM | - | - / - | - | - | - | - |
2121
| USE_KNX | - | - / x | x | - | - | - |
22+
| USE_TELNET | - | - / - | - | - | - | - |
2223
| USE_WEBSERVER | x | x / x | x | x | x | x |
2324
| USE_WEBSEND_RESPONSE | - | - / - | - | - | - | - |
2425
| USE_EMULATION_HUE | x | x / x | - | x | - | - |

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file.
1010
- Support for HLK-LD2402 24GHz smart wave motion sensor (#23133)
1111
- Matter prepare for ICD cluster (#23158)
1212
- Berry `re.dump()` (#23162)
13+
- Telnet server using command `Telnet <0|1|port>[,<IP filter>]`
1314

1415
### Breaking Changed
1516
- Berry remove `Leds.create_matrix` from the standard library waiting for reimplementation (#23114)

CODE_OWNERS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,9 @@ In addition to @arendst the following code is mainly owned by:
8989
| xdrv_75_dali | @eeak, @arendst
9090
| xdrv_76_serial_i2c | @s-hadinger
9191
| xdrv_77_wizmote | @arendst
92-
| xdrv_78 |
92+
| xdrv_78_telnet | @arendst
9393
| xdrv_79_esp32_ble | @staars, @btsimonh
94+
| xdrv_80 |
9495
| xdrv_81_esp32_webcam | @gemu, @philrich
9596
| xdrv_82_esp32_ethernet | @arendst
9697
| xdrv_83_esp32_watch | @gemu

RELEASENOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ The latter links can be used for OTA upgrades too like ``OtaUrl https://ota.tasm
116116

117117
## Changelog v14.5.0.2
118118
### Added
119+
- Telnet server using command `Telnet <0|1|port>[,<IP filter>]`
119120
- Support Vango Technologies V924x ultralow power, single-phase, power measurement [#23127](https://github.com/arendst/Tasmota/issues/23127)
120121
- Support for HLK-LD2402 24GHz smart wave motion sensor [#23133](https://github.com/arendst/Tasmota/issues/23133)
121122
- Allow acl in mqtt when client certificate is in use with `#define USE_MQTT_CLIENT_CERT` [#22998](https://github.com/arendst/Tasmota/issues/22998)

tasmota/include/tasmota_globals.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,11 @@ extern "C" int startWaveformClockCycles(uint8_t pin, uint32_t highCcys, uint32_t
4747
uint32_t runTimeCcys, int8_t alignPhase, uint32_t phaseOffsetCcys, bool autoPwm);
4848
extern "C" void setTimer1Callback(uint32_t (*fn)());
4949
#ifdef USE_SERIAL_BRIDGE
50-
void SerialBridgePrintf(PGM_P formatP, ...);
50+
void SerialBridgePrint(char *data);
5151
#endif
52+
#ifdef USE_TELNET
53+
void TelnetPrint(char *data);
54+
#endif // USE_TELNET
5255
#ifdef USE_INFLUXDB
5356
void InfluxDbProcess(bool use_copy = false);
5457
#endif

tasmota/my_user_config.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,9 @@
485485
//#define USE_KNX // Enable KNX IP Protocol Support (+9.4k code, +3k7 mem)
486486
#define USE_KNX_WEB_MENU // Enable KNX WEB MENU (+8.3k code, +144 mem)
487487

488+
// -- Telnet --------------------------------------
489+
//#define USE_TELNET // Add support for telnet (+1k3 code)
490+
488491
// -- HTTP ----------------------------------------
489492
#define USE_WEBSERVER // Enable web server and Wi-Fi Manager (+66k code, +8k mem)
490493
#define WEB_PORT 80 // Web server Port for User and Admin mode

tasmota/tasmota_support/support.ino

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2639,16 +2639,26 @@ void AddLogData(uint32_t loglevel, const char* log_data, const char* log_data_pa
26392639

26402640
if ((loglevel <= TasmotaGlobal.seriallog_level) &&
26412641
(TasmotaGlobal.masterlog_level <= TasmotaGlobal.seriallog_level)) {
2642-
TasConsole.printf("%s%s%s%s\r\n", mxtime, log_data, log_data_payload, log_data_retained);
2642+
char* data = ext_snprintf_malloc_P("%s%s%s%s\r\n", mxtime, log_data, log_data_payload, log_data_retained);
2643+
if (data) {
2644+
TasConsole.print(data);
26432645
#ifdef USE_SERIAL_BRIDGE
2644-
SerialBridgePrintf("%s%s%s%s\r\n", mxtime, log_data, log_data_payload, log_data_retained);
2646+
SerialBridgePrint(data);
26452647
#endif // USE_SERIAL_BRIDGE
2648+
#ifdef USE_TELNET
2649+
TelnetPrint(data);
2650+
#endif // USE_TELNET
2651+
free(data);
2652+
}
26462653
}
26472654

26482655
if (!TasmotaGlobal.log_buffer) { return; } // Leave now if there is no buffer available
26492656

2650-
uint32_t highest_loglevel = Settings->weblog_level;
2657+
uint32_t highest_loglevel = Settings->seriallog_level; // Need this for Telnet
26512658
if (Settings->mqttlog_level > highest_loglevel) { highest_loglevel = Settings->mqttlog_level; }
2659+
#ifdef USE_WEBSERVER
2660+
if (Settings->weblog_level > highest_loglevel) { highest_loglevel = Settings->weblog_level; }
2661+
#endif // USE_WEBSERVER
26522662
#ifdef USE_UFILESYS
26532663
uint32_t filelog_level = Settings->filelog_level % 10;
26542664
if (filelog_level > highest_loglevel) { highest_loglevel = filelog_level; }

tasmota/tasmota_support/support_features.ino

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -945,8 +945,10 @@ constexpr uint32_t feature[] = {
945945
#endif
946946
#if defined(USE_ENERGY_SENSOR) && defined(USE_V9240)
947947
0x00004000 | // xnrg_25_v9240.ino
948-
#endif
949-
// 0x00008000 | //
948+
#endif
949+
#ifdef USE_TELNET
950+
0x00008000 | // xdrv_80_telnet.ino
951+
#endif
950952
// 0x00010000 | //
951953
// 0x00020000 | //
952954
// 0x00040000 | //

tasmota/tasmota_xdrv_driver/xdrv_08_serial_bridge.ino

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -99,18 +99,11 @@ void SetSSerialConfig(uint32_t serial_config) {
9999
}
100100
}
101101

102-
void SerialBridgePrintf(PGM_P formatP, ...) {
102+
void SerialBridgePrint(char *data) {
103103
#ifdef USE_SERIAL_BRIDGE_TEE
104104
if ((SB_TEE == Settings->sserial_mode) && serial_bridge_buffer) {
105-
va_list arg;
106-
va_start(arg, formatP);
107-
char* data = ext_vsnprintf_malloc_P(formatP, arg);
108-
va_end(arg);
109-
if (data == nullptr) { return; }
110-
111105
// SerialBridgeSerial->printf(data); // This resolves "MqttClientMask":"DVES_%06X" into "DVES_000002"
112106
SerialBridgeSerial->print(data); // This does not resolve "DVES_%06X"
113-
free(data);
114107
}
115108
#endif // USE_SERIAL_BRIDGE_TEE
116109
}
@@ -274,7 +267,8 @@ void SerialBridgeInit(void) {
274267
AddLog(LOG_LEVEL_DEBUG, PSTR("SBR: Serial UART%d"), SerialBridgeSerial->getUart());
275268
#endif
276269
SerialBridgeSerial->flush();
277-
SerialBridgePrintf("\r\n");
270+
char data[] = "\r\n";
271+
SerialBridgePrint(data);
278272
}
279273
}
280274
}

tasmota/tasmota_xdrv_driver/xdrv_52_3_berry_tasmota.ino

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1089,8 +1089,11 @@ extern "C" {
10891089
if (len+3 > LOGSZ) { strcat(log_data, "..."); } // Actual data is more
10901090
TasConsole.printf(log_data);
10911091
#ifdef USE_SERIAL_BRIDGE
1092-
SerialBridgePrintf(log_data);
1092+
SerialBridgePrint(log_data);
10931093
#endif // USE_SERIAL_BRIDGE
1094+
#ifdef USE_TELNET
1095+
TelnetPrint(log_data);
1096+
#endif // USE_TELNET
10941097
}
10951098

10961099
void berry_log_C(const char * berry_buf, ...) {
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
/*
2+
xdrv_78_telnet.ino - Telnet console support for Tasmota
3+
4+
SPDX-FileCopyrightText: 2025 Theo Arends
5+
6+
SPDX-License-Identifier: GPL-3.0-only
7+
*/
8+
9+
#ifdef USE_TELNET
10+
/*********************************************************************************************\
11+
* Telnet console support for a single connection
12+
\*********************************************************************************************/
13+
14+
#define XDRV_78 78
15+
16+
#ifndef TELNET_BUF_SIZE
17+
#define TELNET_BUF_SIZE 255 // size of the buffer
18+
#endif
19+
20+
struct {
21+
WiFiServer *server = nullptr;
22+
WiFiClient client;
23+
IPAddress ip_filter;
24+
char *buffer = nullptr; // data transfer buffer
25+
uint16_t port;
26+
uint16_t buffer_size = TELNET_BUF_SIZE;
27+
bool ip_filter_enabled = false;
28+
} Telnet;
29+
30+
/********************************************************************************************/
31+
32+
void TelnetPrint(char *data) {
33+
if (Telnet.server) {
34+
WiFiClient &client = Telnet.client;
35+
if (client) {
36+
// client.printf(data); // This resolves "MqttClientMask":"DVES_%06X" into "DVES_000002"
37+
client.print(data); // This does not resolve "DVES_%06X"
38+
}
39+
}
40+
}
41+
42+
/********************************************************************************************/
43+
44+
void TelnetLoop(void) {
45+
// check for a new client connection
46+
if ((Telnet.server) && (Telnet.server->hasClient())) {
47+
WiFiClient new_client = Telnet.server->available();
48+
49+
AddLog(LOG_LEVEL_INFO, PSTR("TLN: Connection from %s"), new_client.remoteIP().toString().c_str());
50+
51+
if (Telnet.ip_filter_enabled) { // Check for IP filtering if it's enabled
52+
if (Telnet.ip_filter != new_client.remoteIP()) {
53+
AddLog(LOG_LEVEL_INFO, PSTR("TLN: Rejected due to filtering"));
54+
new_client.stop();
55+
} else {
56+
AddLog(LOG_LEVEL_INFO, PSTR("TLN: Allowed through filter"));
57+
}
58+
}
59+
60+
WiFiClient &client = Telnet.client;
61+
if (client) {
62+
client.stop();
63+
}
64+
client = new_client;
65+
if (client) {
66+
client.printf("Tasmota %s %s (%s)\r\n\n", TasmotaGlobal.hostname, TasmotaGlobal.version, GetBuildDateAndTime().c_str());
67+
uint32_t index = 1;
68+
char* line;
69+
size_t len;
70+
while (GetLog(TasmotaGlobal.seriallog_level, &index, &line, &len)) {
71+
// [14:49:36.123 MQTT: stat/wemos5/RESULT = {"POWER":"OFF"}] > [{"POWER":"OFF"}]
72+
client.write(line, len -1);
73+
client.write("\r\n");
74+
}
75+
client.printf("%s:# ", TasmotaGlobal.hostname);
76+
}
77+
}
78+
79+
bool busy;
80+
uint32_t buf_len = 0;
81+
do {
82+
busy = false; // exit loop if no data was transferred
83+
WiFiClient &client = Telnet.client;
84+
bool overrun = false;
85+
while (client && (client.available())) {
86+
uint8_t c = client.read();
87+
if (c >= 0) {
88+
busy = true;
89+
if (isprint(c)) { // Any char between 32 and 127
90+
if (buf_len < Telnet.buffer_size -1) { // Add char to string if it still fits
91+
Telnet.buffer[buf_len++] = c;
92+
} else {
93+
overrun = true; // Signal overrun but continue reading input to flush until '\n' (EOL)
94+
}
95+
}
96+
else if (c == '\n') {
97+
Telnet.buffer[buf_len] = 0; // Telnet data completed
98+
TasmotaGlobal.seriallog_level = (Settings->seriallog_level < LOG_LEVEL_INFO) ? (uint8_t)LOG_LEVEL_INFO : Settings->seriallog_level;
99+
if (overrun) {
100+
AddLog(LOG_LEVEL_INFO, PSTR("TLN: buffer overrun"));
101+
} else {
102+
AddLog(LOG_LEVEL_INFO, PSTR("TLN: %s"), Telnet.buffer);
103+
ExecuteCommand(Telnet.buffer, SRC_REMOTE);
104+
}
105+
client.flush();
106+
client.printf("%s:# ", TasmotaGlobal.hostname);
107+
return;
108+
}
109+
}
110+
}
111+
yield(); // avoid WDT if heavy traffic
112+
} while (busy);
113+
}
114+
115+
/*********************************************************************************************\
116+
* Commands
117+
\*********************************************************************************************/
118+
119+
void TelnetStop(void) {
120+
Telnet.server->stop();
121+
delete Telnet.server;
122+
Telnet.server = nullptr;
123+
124+
WiFiClient &client = Telnet.client;
125+
client.stop();
126+
127+
free(Telnet.buffer);
128+
Telnet.buffer = nullptr;
129+
}
130+
131+
const char kTelnetCommands[] PROGMEM = "Telnet|" // prefix
132+
"|Buffer";
133+
134+
void (* const TelnetCommand[])(void) PROGMEM = {
135+
&CmndTelnet, &CmndTelnetBuffer };
136+
137+
void CmndTelnet(void) {
138+
// Telnet - Show telnet server state
139+
// Telnet 0 - Disable telnet server
140+
// Telnet 1 - Enable telnet server on port 23
141+
// Telnet 23 - Enable telnet server on port 23
142+
// Telnet 1, 192.168.2.1 - Enable telnet server and only allow connection from 192.168.2.1
143+
if (!TasmotaGlobal.global_state.network_down) {
144+
if (XdrvMailbox.data_len) {
145+
Telnet.port = XdrvMailbox.payload;
146+
147+
if (ArgC() == 2) {
148+
char sub_string[XdrvMailbox.data_len];
149+
Telnet.ip_filter.fromString(ArgV(sub_string, 2));
150+
Telnet.ip_filter_enabled = true;
151+
} else {
152+
// Disable whitelist if previously set
153+
Telnet.ip_filter_enabled = false;
154+
}
155+
156+
if (Telnet.server) {
157+
TelnetStop();
158+
}
159+
160+
if (Telnet.port > 0) {
161+
if (!Telnet.buffer) {
162+
Telnet.buffer = (char*)malloc(Telnet.buffer_size);
163+
if (!Telnet.buffer) { return; }
164+
165+
if (1 == Telnet.port) { Telnet.port = 23; }
166+
Telnet.server = new WiFiServer(Telnet.port);
167+
Telnet.server->begin(); // start TCP server
168+
Telnet.server->setNoDelay(true);
169+
}
170+
}
171+
}
172+
if (Telnet.server) {
173+
ResponseCmndChar_P(PSTR("Started"));
174+
} else {
175+
ResponseCmndChar_P(PSTR("Stopped"));
176+
}
177+
}
178+
}
179+
180+
void CmndTelnetBuffer(void) {
181+
// TelnetBuffer - Show current input buffer size (default 255)
182+
// TelnetBuffer 300 - Change input buffer size to 300 characters
183+
if (XdrvMailbox.data_len > 0) {
184+
uint16_t bsize = Telnet.buffer_size;
185+
Telnet.buffer_size = XdrvMailbox.payload;
186+
if (XdrvMailbox.payload < MIN_INPUT_BUFFER_SIZE) {
187+
Telnet.buffer_size = MIN_INPUT_BUFFER_SIZE; // 256 / 256
188+
}
189+
else if (XdrvMailbox.payload > INPUT_BUFFER_SIZE) {
190+
Telnet.buffer_size = INPUT_BUFFER_SIZE; // 256 / 800
191+
}
192+
193+
if (Telnet.buffer && (bsize != Telnet.buffer_size)) {
194+
Telnet.buffer = (char*)realloc(Telnet.buffer, Telnet.buffer_size);
195+
if (!Telnet.buffer) {
196+
TelnetStop();
197+
ResponseCmndChar_P(PSTR("Stopped"));
198+
return;
199+
}
200+
}
201+
}
202+
ResponseCmndNumber(Telnet.buffer_size);
203+
}
204+
205+
/*********************************************************************************************\
206+
* Interface
207+
\*********************************************************************************************/
208+
209+
bool Xdrv78(uint32_t function) {
210+
bool result = false;
211+
212+
if (FUNC_COMMAND == function) {
213+
result = DecodeCommand(kTelnetCommands, TelnetCommand);
214+
} else if (Telnet.buffer) {
215+
switch (function) {
216+
case FUNC_LOOP:
217+
TelnetLoop();
218+
break;
219+
case FUNC_ACTIVE:
220+
result = true;
221+
break;
222+
}
223+
}
224+
return result;
225+
}
226+
227+
#endif // USE_TELNET

tools/decode-status.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@
311311
"USE_MAGIC_SWITCH","USE_PIPSOLAR","USE_GPIO_VIEWER","USE_AMSX915",
312312
"USE_SPI_LORA","USE_SPL06_007","USE_QMP6988","USE_WOOLIIS",
313313
"USE_HX711_M5SCALES","USE_RX8010","USE_PCF85063","USE_ESP32_TWAI",
314-
"USE_C8_CO2_5K","USE_WIZMOTE","USE_V9240","",
314+
"USE_C8_CO2_5K","USE_WIZMOTE","USE_V9240","USE_TELNET",
315315
"","","","",
316316
"","","","",
317317
"","","","",

0 commit comments

Comments
 (0)