Initial commit
This commit is contained in:
commit
bf1f8e41d1
4 changed files with 376 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
.pio
|
||||
.cache
|
||||
18
library.json
Normal file
18
library.json
Normal 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
269
src/SerialCmd.cpp
Normal 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
87
src/SerialCmd.h
Normal 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
|
||||
Loading…
Reference in a new issue