Skip to content

Commit

Permalink
New telnet feature - intermediate commit
Browse files Browse the repository at this point in the history
  • Loading branch information
tuxpoldo committed Jun 2, 2023
1 parent 323a012 commit 3bda143
Show file tree
Hide file tree
Showing 4 changed files with 316 additions and 2 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,7 @@ See [Example SerialBridge](https://github.com/muwerk/examples/tree/master/serial
History
-------

- 0.5.0 (2023-06-02): Telnet access to `muwerk` console
- 0.4.1 (2022-10-17): Support for ESP32C3 (platform define `__ESP32_RISC__`).
- 0.4.0 (2021-01-30): **Breaking change** for ustd library include: ustd include-files have now `ustd_` prefix to prevent name-clashes with various platform-sdks. [queue.h clashed with ESP8266-Wifi, platform.h clashed with
RISC-V SDK, hence new names `ustd_queue.h` and `ustd_platform.h` etc.]
Expand Down
2 changes: 1 addition & 1 deletion library.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"frameworks": "arduino"
}
],
"version": "0.4.1",
"version": "0.5.0",
"frameworks": "arduino",
"platforms": "*"
}
2 changes: 1 addition & 1 deletion library.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name=munet
version=0.4.1
version=0.5.0
author=Dominik Schlösser, Leo Moll
maintainer=Dominik Schlösser, <dsc@dosc.net>
sentence=Modules for Wifi connectivity, NTP, OTA, MQTT on ESP32/ESP8266 compatible with muwerk scheduler, serial link for other platforms
Expand Down
313 changes: 313 additions & 0 deletions telnet.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
// telnet.h - munet

#pragma once

#include "ustd_platform.h"
#include "ustd_array.h"
#include "scheduler.h"
#include <Arduino_JSON.h>

#include "console.h"

namespace ustd {

/*! \brief munet Telnet Console Class
The telnet console class implements a simple but effective telnet console shell that
allows to communicate to the device via a TCP network connection. See \ref ustd::Console for a
list of supported commands.
This class is not instantiated directly but managed by the \ref TelnetServer class.
*/

class TelnetConsole : public Console {
public:
Scheduler *pSched;
int tID;
WiFiClient client;
bool connected;
bool finished;
char buffer[64];

TelnetConsole(WiFiClient client)
: Console("telnet", null), client(client) {
connected = client.connected();
finished = false;
printer = &this->client;
}

const String getRemoteIP() {
return client.remoteIP().toString();
}

const uint16_t getRemotePort() {
return client.remotePort();
}

void begin(Scheduler *_pSched) {
pSched = _pSched;
tID = pSched->add([this]() { this->loop(); }, "telnet", 60000); // 60ms
client.println("Welcome to the machine!");
prompt();
}

void end() {
client.stop();
pSched->remove(tID);
}

void loop() {
connected = client.connected();
if (connected) {
int icount = client.available();
while (icount > 0) {
int iread = min(icount, (int)(sizeof(buffer) / sizeof(char)) - 1);
int idone = client.read((uint8_t *)buffer, iread);
if (idone) {
const char *pStart = buffer;
// our buffer is ALWAYS zero terminated!
buffer[idone] = 0;
for (char *pPtr = buffer; *pPtr; pPtr++) {
switch (*pPtr) {
case 9: // tab
// treat like space
*pPtr = 32;
break;
case 10: // line feed
case 13: // enter
// execute the command
*pPtr = 0;
DBGF("Executing %s\r\n", pStart);
args += pStart;
pStart = pPtr + 1;
DBGF("Rest is now at pos:%i v:%i s:%s\r\n", (int)(pStart - buffer), (int)*pStart, pStart);
execute();
prompt();
break;
}
}
if (*pStart) {
DBGF("Appending rest at pos:%i v:%i s:%s\r\n", (int)(pStart - buffer), (int)*pStart, pStart);
args += pStart;
}
icount -= idone;
}
}
} else {
if (!finished) {
DBGF("\rTelnet client disconnected\r\n");
client.stop();
finished = true;
}
}
}
};

/*! \brief munet Console Telnet Server Class
The console telnet server class implements a network server listening on the configured port
and managing \ref TelnetConsole instances for each incoming connection.
## Sample of telnet server remote console:
~~~{.cpp}
#include "scheduler.h"
#include "console.h"
#include "telnet.h"
ustd::Scheduler sched( 10, 16, 32 );
ustd::SerialConsole con;
ustd::TelnetServer telnet;
void apploop() {}
void setup() {
// initialize the serial interface
Serial.begin( 115200 );
// extend console
con.extend( "hurz", []( String cmd, String args ) {
printer->println( "Der Wolf... Das Lamm.... Auf der grünen Wiese.... HURZ!" );
while ( args.length() ) {
String arg = ustd::shift( args );
printer->println( arg + " HURZ!" );
}
} );
con.begin( &sched );
int tID = sched.add( apploop, "main", 50000 );
}
void loop() {
sched.loop();
}
~~~
*/
class TelnetServer {
public:
typedef struct {
int id;
char *command;
T_COMMANDFN fn;
} T_COMMAND;
ustd::array<T_COMMAND> commands;
int commandHandle = 0;
Scheduler *pSched;
int tID;
WiFiServer server;
bool connected = false;
array<TelnetConsole *> consoles;
uint8_t max_clients;

int port;

TelnetServer(uint16_t port = 23, uint8_t max_clients = 4)
: server(port), consoles(max_clients), max_clients(max_clients) {
}

~TelnetServer() {
for (unsigned int i = 0; i < consoles.length(); i++) {
delete consoles[i];
}
consoles.erase();
for (unsigned int i = 0; i < commands.length(); i++) {
free(commands[i].command);
}
commands.erase();
}

void addUserCommand(Console *pCon) {
pCon->extend("users", [this](String cmd, String args, Print *printer) {
printer->printf("ID IP Address Port\n");
printer->printf("---------------------------------\n");
for (unsigned int i = 0; i < consoles.length(); i++) {
TelnetConsole *pCon = consoles[i];
if (pCon->finished) {
printer->printf("%-3d Connection is terminated\n", i);
} else {
printer->printf("%-3d %-20s %-5d\n", i, pCon->getRemoteIP().c_str(), pCon->getRemotePort());
}
}
});
}

int extend(String command, T_COMMANDFN handler) {
/*! Extend the console with a custom command
*
* @param command The name of the command
* @param handler Callback of type void myCallback(String command, String
* args) that is called, if a matching command is entered.
* @return commandHandle on success (needed for unextend), or -1
* on error.
*/
T_COMMAND cmd = {};
cmd.id = ++commandHandle;
cmd.fn = handler;
cmd.command = (char *)malloc(command.length() + 1);
strcpy(cmd.command, command.c_str());
return commands.add(cmd) != -1 ? commandHandle : -1;
}

bool unextend(String command) {
/*! Removes a custom command from the console
* @param command The name of the command to remove.
* @return true on success.
*/
for (unsigned int i = 0; i < commands.length(); i++) {
if (command == (const char *)commands[i].command) {
for (unsigned int j = 0; j < consoles.length(); j++) {
TelnetConsole *pCon = consoles[j];
pCon->unextend(commands[i].command);
}
return commands.erase(i);
}
}
return false;
}

bool unextend(int commandHandle) {
/*! Removes a custom command from the console
* @param commandHandle The commandHandle of the command to remove.
* @return true on success.
*/
for (unsigned int i = 0; i < commands.length(); i++) {
if (commands[i].id == commandHandle) {
for (unsigned int j = 0; j < consoles.length(); j++) {
TelnetConsole *pCon = consoles[j];
pCon->unextend(commands[i].command);
}
return commands.erase(i);
}
}
return false;
}

void begin(Scheduler *_pSched, Console *pCon = nullptr) {
pSched = _pSched;
tID = pSched->add([this]() { this->loop(); }, "telnet", 60000); // 60ms
pSched->subscribe(tID, "net/network", [this](String topic, String msg, String originator) {
JSONVar jm = JSON.parse(msg);
if (JSON.typeof(jm) == "object" && JSON.typeof(jm["state"]) == "string") {
bool newconnected = !strcmp(jm["state"], "connected");
if (connected != newconnected) {
connected = newconnected;
if (connected) {
DBG("Start listening...");
this->server.begin();
} else {
DBG("Stop listening...");
// this->server.end();
}
}
}
});
if (pCon) {
addUserCommand(pCon);
}
}

void loop() {
// cleanup finished consoles
for (unsigned int i = 0; i < consoles.length(); i++) {
TelnetConsole *pCon = consoles[i];
if (pCon->finished) {
DBGF("\rCleaning up console %i\r\n", i);
pCon->end();
delete pCon;
consoles.erase(i);
--i;
continue;
}
}
// accept connections
if (connected) {
for (WiFiClient client = server.accept(); client; client = server.accept()) {
if (consoles.length() < max_clients) {
DBGF("\rNew telnet connection from %s:%u\r\n", client.remoteIP().toString().c_str(),
client.remotePort());
TelnetConsole *pCon = new TelnetConsole(client);
addUserCommand(pCon);
pCon->extend("quit", [this, client](String cmd, String args, Print *printer) {
printer->print("Have a nice day!\n");
printer->flush();
((WiFiClient *)printer)->stop();
});
for (unsigned int i = 0; i < commands.length(); i++) {
pCon->extend(commands[i].command, commands[i].fn);
}
consoles.add(pCon);
pCon->begin(pSched);
} else {
client.printf("Sorry - maximum connections limit reached. Bye!\n");
client.flush();
client.stop();
}
}
}
}
};

} // namespace ustd

0 comments on commit 3bda143

Please sign in to comment.