Initial commit

This commit is contained in:
David F 2026-02-09 05:08:15 -05:00
commit bf1f8e41d1
4 changed files with 376 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.pio
.cache

18
library.json Normal file
View file

@ -0,0 +1,18 @@
{
"name": "SerialCmd",
"version": "0.1.0",
"description": "Lightweight serial command parser with flags, quoted strings, and auto-sync for Arduino",
"keywords": "serial, command, parser, cli, uart",
"repository": {
"type": "git",
"url": ""
},
"frameworks": "arduino",
"platforms": [
"espressif32",
"atmelavr",
"atmelsam",
"ststm32",
"raspberrypi"
]
}

269
src/SerialCmd.cpp Normal file
View file

@ -0,0 +1,269 @@
#include "SerialCmd.h"
#include <cstring>
#include <cstdlib>
// --- CmdArgs ---
void CmdArgs::parse(char* line) {
_command = nullptr;
_rawCount = 0;
_posCount = 0;
// In-place tokenization with quoted string support
bool inToken = false;
bool inQuote = false;
char* tokenStart = nullptr;
for (char* p = line; ; ++p) {
char c = *p;
if (c == '\0') {
if (inToken && _rawCount < SERIAL_CMD_MAX_ARGS) {
_tokens[_rawCount++] = tokenStart;
}
break;
}
if (inQuote) {
if (c == '"') {
*p = '\0';
inQuote = false;
inToken = false;
if (_rawCount < SERIAL_CMD_MAX_ARGS) {
_tokens[_rawCount++] = tokenStart;
}
}
continue;
}
if (c == '"') {
if (inToken && _rawCount < SERIAL_CMD_MAX_ARGS) {
*p = '\0';
_tokens[_rawCount++] = tokenStart;
}
inQuote = true;
inToken = true;
tokenStart = p + 1;
continue;
}
if (c == ' ' || c == '\t') {
if (inToken) {
*p = '\0';
if (_rawCount < SERIAL_CMD_MAX_ARGS) {
_tokens[_rawCount++] = tokenStart;
}
inToken = false;
}
} else if (!inToken) {
inToken = true;
tokenStart = p;
}
}
// First token is the command name
if (_rawCount > 0) {
_command = _tokens[0];
}
// Classify remaining tokens: flags vs positional
for (int i = 1; i < _rawCount; ++i) {
if (_tokens[i][0] == '-') {
// It's a flag — skip for positional list
} else {
if (_posCount < SERIAL_CMD_MAX_ARGS) {
_positional[_posCount++] = _tokens[i];
}
}
}
}
const char* CmdArgs::arg(int index) const {
if (index < 0 || index >= _posCount) return nullptr;
return _positional[index];
}
int CmdArgs::intArg(int index, int fallback) const {
const char* v = arg(index);
return v ? atoi(v) : fallback;
}
float CmdArgs::floatArg(int index, float fallback) const {
const char* v = arg(index);
return v ? atof(v) : fallback;
}
bool CmdArgs::has(const char* flag) const {
for (int i = 1; i < _rawCount; ++i) {
if (strcmp(_tokens[i], flag) == 0) return true;
}
return false;
}
const char* CmdArgs::raw(int index) const {
if (index < 0 || index >= _rawCount) return nullptr;
return _tokens[index];
}
// --- SerialCmd ---
SerialCmd::SerialCmd(Stream& stream) : _stream(stream) {
_line[0] = '\0';
}
bool SerialCmd::on(const char* name, Handler handler) {
return on(name, nullptr, handler);
}
bool SerialCmd::on(const char* name, const char* usage, Handler handler) {
if (_commandCount >= SERIAL_CMD_MAX_COMMANDS) return false;
_commands[_commandCount++] = { name, usage, handler };
return true;
}
void SerialCmd::onDefault(Handler handler) {
_defaultHandler = handler;
}
bool SerialCmd::poll() {
while (_stream.available()) {
feedChar(_stream.read());
if (_lineReady) {
dispatch();
return true;
}
}
return false;
}
void SerialCmd::wait() {
_lineReady = false;
while (!_lineReady) {
if (_stream.available()) {
feedChar(_stream.read());
} else {
yield();
}
}
dispatch();
}
void SerialCmd::feedChar(char c) {
// Handle \r\n sequences — ignore \n right after \r
if (c == '\n' && _prevCR) {
_prevCR = false;
return;
}
_prevCR = (c == '\r');
// End of line
if (c == '\r' || c == '\n') {
if (_echo) _stream.println();
_line[_linePos] = '\0';
if (_linePos > 0) {
_lineReady = true;
}
return;
}
// Backspace (BS or DEL)
if (c == '\b' || c == 127) {
if (_linePos > 0) {
_linePos--;
if (_echo) _stream.print("\b \b");
}
return;
}
// Accumulate printable characters
if (_linePos < SERIAL_CMD_MAX_LINE - 1) {
_line[_linePos++] = c;
if (_echo) _stream.print(c);
}
}
void SerialCmd::dispatch() {
_lineReady = false;
CmdArgs args;
args.parse(_line);
if (!args.command()) {
_linePos = 0;
return;
}
// Built-in /sync — checked before user commands
if (strcmp(args.command(), "/sync") == 0) {
sendSyncList();
_linePos = 0;
return;
}
for (int i = 0; i < _commandCount; ++i) {
if (strcmp(_commands[i].name, args.command()) == 0) {
_commands[i].handler(args);
_linePos = 0;
return;
}
}
if (_defaultHandler) {
_defaultHandler(args);
}
_linePos = 0;
}
void SerialCmd::sendSyncList() {
_stream.println("#sync-begin");
for (int i = 0; i < _commandCount; ++i) {
_stream.print(_commands[i].name);
if (_commands[i].usage) {
_stream.print(' ');
_stream.print(_commands[i].usage);
}
_stream.println();
}
_stream.println("#sync-end");
}
bool SerialCmd::sync(unsigned long timeout_ms) {
sendSyncList();
unsigned long deadline = millis() + timeout_ms;
char buf[SERIAL_CMD_MAX_LINE];
int pos = 0;
bool prevCR = false;
while (millis() < deadline) {
if (!_stream.available()) {
yield();
continue;
}
char c = _stream.read();
// Handle \r\n sequences
if (c == '\n' && prevCR) {
prevCR = false;
continue;
}
prevCR = (c == '\r');
if (c == '\r' || c == '\n') {
buf[pos] = '\0';
if (pos > 0 && strcmp(buf, "#acknowledge-sync") == 0) {
return true;
}
pos = 0;
continue;
}
if (pos < SERIAL_CMD_MAX_LINE - 1) {
buf[pos++] = c;
}
}
return false;
}

87
src/SerialCmd.h Normal file
View file

@ -0,0 +1,87 @@
#ifndef SERIAL_CMD_H
#define SERIAL_CMD_H
#include <Arduino.h>
#include <functional>
#ifndef SERIAL_CMD_MAX_LINE
#define SERIAL_CMD_MAX_LINE 128
#endif
#ifndef SERIAL_CMD_MAX_ARGS
#define SERIAL_CMD_MAX_ARGS 16
#endif
#ifndef SERIAL_CMD_MAX_COMMANDS
#define SERIAL_CMD_MAX_COMMANDS 16
#endif
class CmdArgs {
public:
void parse(char* line);
const char* command() const { return _command; }
// Positional args (skips flags)
const char* arg(int index) const;
int intArg(int index, int fallback = 0) const;
float floatArg(int index, float fallback = 0.0f) const;
int count() const { return _posCount; }
// Flag check
bool has(const char* flag) const;
// Raw token access (includes flags)
const char* raw(int index) const;
int rawCount() const { return _rawCount; }
private:
const char* _command = nullptr;
const char* _tokens[SERIAL_CMD_MAX_ARGS];
int _rawCount = 0;
const char* _positional[SERIAL_CMD_MAX_ARGS];
int _posCount = 0;
};
class SerialCmd {
public:
using Handler = std::function<void(CmdArgs&)>;
explicit SerialCmd(Stream& stream);
bool on(const char* name, Handler handler);
bool on(const char* name, const char* usage, Handler handler);
void onDefault(Handler handler);
bool poll();
void wait();
bool sync(unsigned long timeout_ms = 5000);
void setEcho(bool enabled) { _echo = enabled; }
private:
void feedChar(char c);
void dispatch();
void sendSyncList();
Stream& _stream;
bool _echo = false;
char _line[SERIAL_CMD_MAX_LINE];
int _linePos = 0;
bool _lineReady = false;
bool _prevCR = false;
struct Command {
const char* name;
const char* usage;
Handler handler;
};
Command _commands[SERIAL_CMD_MAX_COMMANDS];
int _commandCount = 0;
Handler _defaultHandler;
};
#endif