Command-line interface system for ESP32 with support for untyped and strongly-typed commands, password protection, and multi-stream I/O.
Works with any Arduino Stream: Serial, Serial2, Bluetooth serial, or network streams such as NetSerial.
Add to your platformio.ini:
lib_deps =
https://github.com/FelipeDadison/CommandInterface.gitRequires C++17, the default on the ESP32 Arduino core 3.x. On core 2.x add:
build_unflags = -std=gnu++11
build_flags = -std=gnu++17Download the ZIP from the latest release, then use Sketch → Include Library → Add .ZIP Library....
Requires the esp32 board package 3.x or newer (Tools → Board → Boards Manager), which builds with C++17.
#include "CommandInterface.h"
const int LED_PIN = 2;
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
// Add streams for input/output
Cli.addInputStream(Serial);
Cli.addOutputStream(Serial);
// Set project info (shown by the '?' help command)
Cli.setProjectInfo("MyProject", "1.0.0", "ESP32-Device");
// Register commands
Cli.registerCommand("hello", "Say hello", [](const String& args) {
Cli.println("Hello from ESP32!");
});
Cli.registerCommand("led", "Toggle LED", [](const String& args) {
digitalWrite(LED_PIN, !digitalRead(LED_PIN));
Cli.println("LED toggled");
});
}
void loop() {
Cli.update(); // Process incoming commands
delay(10);
}Commands end with a newline (\n or \r\n). Leading and trailing spaces are ignored.
Arduino IDE: set the Serial Monitor line ending to New Line or Both NL & CR. With No Line Ending, commands are never executed.
Cli.registerTypedCommand<int, String>(
"setvalue",
"Set a value (int value, String name)",
{
{"value", "int"},
{"name", "String"}
},
[](int value, String name) {
Cli.printf("Set %s to %d\n", name.c_str(), value);
}
);
// Usage from CLI: setvalue\42\myvarSupported argument types: int, long, unsigned int, unsigned long, float, double, bool (1/0, true/false, on/off, yes/no) and String. Any other type is a compile-time error. Numbers out of range for their type are rejected.
Cli is a Print, so print(), println() and printf() work like on Serial:
- Inside a command,
Cli.print(),Cli.println()andCli.printf()reply only to the stream that sent the command. Cli.printToAll(),Cli.printlnToAll()andCli.printfToAll()broadcast to every output stream.- Outside a command (e.g. in
loop()),Cli.print()also broadcasts.
void setup() {
// Set password for CLI access
Cli.setPassword("1234");
// Commands won't execute until password is entered
// User must type: PASSWORD\1234
}- Each input stream is unlocked separately: entering the password on
Serialdoesn't unlock the WebSocket. lock()/unlock()act on all streams,lock(stream)/unlock(stream)on one.unlock()also applies to streams added later, untillock()is called. Useful to open the CLI from your firmware, e.g. on a trusted network.- Inside a command,
Cli.currentStream()returns the stream that sent it, e.g.Cli.lock(*Cli.currentStream()).
Brute-force protection is enabled by default: after 3 wrong passwords on a stream, each new failure doubles the wait before the next attempt (1 s, 2 s, 4 s...), up to 5 minutes. During the wait, attempts are refused without being checked. A correct password resets the counter. Each stream has its own counter, so attempts over the network don't block Serial.
Cli.setLoginLimits(3, 5 * 60 * 1000); // free attempts, max wait (ms); (0, 0) disables
Cli.setAutoLock(5 * 60 * 1000); // lock a password session after 5 min without commands (default: off)
Cli.onLoginFailed([](Stream& stream, uint8_t attempts) {
Serial.printf("Wrong password, attempt %u\n", attempts);
});Auto-lock only applies to streams unlocked by typing the password. Streams unlocked from code with unlock() / unlock(stream) stay open until your code locks them.
#include "NetSerial.h"
NetSerial wsSerial(server, "/ws");
void setup() {
// Add multiple input/output streams
Cli.addInputStream(Serial);
Cli.addInputStream(wsSerial);
Cli.addOutputStream(Serial);
Cli.addOutputStream(wsSerial);
// All commands now work on both Serial and WebSocket!
}Each input stream has its own command buffer, so commands typed on different streams at the same time never mix.
Complete sketches are in examples/. The multi_stream example requires NetSerial; the library itself has no dependencies.
CommandInterface()- Create new interface instance (or use globalCli)
void addInputStream(Stream& stream)- Add input streamvoid addOutputStream(Stream& stream)- Add output streamvoid removeInputStream(Stream& stream)- Remove input streamvoid removeOutputStream(Stream& stream)- Remove output stream
void update()- Process incoming commands (call in main loop)void setMaxCommandLength(length)- Maximum command length including arguments (default: 256)void registerCommand(name, description, callback)- Register untyped commandvoid registerTypedCommand<Args...>(name, description, argSpecs, callback)- Register typed commandstatic std::vector<String> splitArgs(input)- Split arguments by separator (empty arguments are kept)Stream* currentStream()- Stream that sent the command being executed (nullptroutside a command)
print(),println(),printf()- Reply to the current stream (broadcast outside a command)void printToAll(msg)- Print to all output streams (no newline)void printlnToAll(msg)- Print to all output streams (with newline)void printfToAll(format, ...)- Formatted print to all output streams
void setPassword(password)- Set access password and lock all streams (empty string disables protection)void lock()/void lock(stream)- Lock all streams / one streamvoid unlock()/void unlock(stream)- Unlock all streams (including streams added later) / one stream; not affected by auto-lockbool isLocked()-trueif any stream is lockedbool isLocked(stream)- Lock status of one streamvoid setLoginLimits(freeAttempts, maxDelayMs)- Brute-force protection (default: 3, 300000)void setAutoLock(timeoutMs)- Lock password sessions after inactivity (default: 0, disabled)void onLoginFailed(callback)- Called on every wrong password with the stream and attempt count
void setProjectInfo(name, version, hostName)- Set info for help display
Commands and arguments are separated by \ character:
command\arg1\arg2\arg3
Two consecutive separators produce an empty argument: command\a\\c has the arguments a, an empty string and c.
- Commands longer than 256 characters are discarded with
Command too long.Raise the limit withCli.setMaxCommandLength(2048), e.g. for commands carrying certificates or keys. - The help command is
?; while a stream is locked, only?andPASSWORDare accepted.
The password is sent in plain text, like everything else on the stream. Over a network transport anyone who can capture the traffic can read it, so only use network streams on trusted networks, and choose a password longer than 4 digits.
A network stream such as NetSerial is a single Stream shared by all its clients: once one client unlocks it, every client connected through it is unlocked. Use setAutoLock() to limit how long a session stays open.
- Arduino Framework
- C++17 standard library (functional, map, vector, tuple)
MIT