Files
espurna/code/espurna/terminal_commands.cpp
Max Prokhorov b8fc8cd1fd Terminal: change command-line parser (#2247)
Change the underlying command line handling:
- switch to a custom parser, inspired by redis / sds
- update terminalRegisterCommand signature, pass only bare minimum
- clean-up `help` & `commands`. update settings `set`, `get` and `del`
- allow our custom test suite to run command-line tests
- clean-up Stream IO to allow us to print large things into debug stream (for example, `eeprom.dump`)
- send parsing errors to the debug log

As a proof of concept, introduce `TERMINAL_MQTT_SUPPORT` and `TERMINAL_WEB_API_SUPPORT`
- MQTT subscribes to the `<root>/cmd/set` and sends response to the `<root>/cmd`. We can't output too much, as we don't have any large-send API.
- Web API listens to the `/api/cmd?apikey=...&line=...` (or PUT, params inside the body). This one is intended as a possible replacement of the `API_SUPPORT`. Internals introduce a 'task' around the AsyncWebServerRequest object that will simulate what WiFiClient does and push data into it continuously, switching between CONT and SYS.

Both are experimental. We only accept a single command and not every command is updated to use Print `ctx.output` object. We are also somewhat limited by the Print / Stream overall, perhaps I am overestimating the usefulness of Arduino compatibility to such an extent :)
Web API handler can also sometimes show only part of the result, whenever the command tries to yield() by itself waiting for something. Perhaps we would need to create a custom request handler for that specific use-case.
2020-05-25 23:41:37 +03:00

94 lines
2.4 KiB
C++

/*
Part of the TERMINAL MODULE
Copyright (C) 2020 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
Heavily inspired by the Embedis design:
- https://github.com/thingSoC/embedis
*/
#include <Arduino.h>
#include "terminal_commands.h"
#include <memory>
namespace terminal {
std::unordered_map<String, Terminal::CommandFunc,
parsing::LowercaseFnv1Hash<String>,
parsing::LowercaseEquals<String>> Terminal::commands;
void Terminal::addCommand(const String& name, CommandFunc func) {
if (!func) return;
commands.emplace(std::make_pair(name, func));
}
size_t Terminal::commandsSize() {
return commands.size();
}
std::vector<String> Terminal::commandNames() {
std::vector<String> out;
out.reserve(commands.size());
for (auto& command : commands) {
out.push_back(command.first);
}
return out;
}
Terminal::Result Terminal::processLine() {
// Arduino stream API returns either `char` >= 0 or -1 on error
int c = -1;
while ((c = stream.read()) >= 0) {
if (buffer.size() >= (buffer_size - 1)) {
buffer.clear();
return Result::BufferOverflow;
}
buffer.push_back(c);
if (c == '\n') {
// in case we see \r\n, offset minus one and overwrite \r
auto end = buffer.end() - 1;
if (*(end - 1) == '\r') {
--end;
}
*end = '\0';
// parser should pick out at least one arg (command)
auto cmdline = parsing::parse_commandline(buffer.data());
buffer.clear();
if (cmdline.argc >= 1) {
auto command = commands.find(cmdline.argv[0]);
if (command == commands.end()) return Result::CommandNotFound;
(*command).second(CommandContext{std::move(cmdline.argv), cmdline.argc, stream});
return Result::Command;
}
}
}
// we need to notify about the fixable things
if (buffer.size() && (c < 0)) {
return Result::Pending;
} else if (!buffer.size() && (c < 0)) {
return Result::NoInput;
// ... and some unexpected conditions
} else {
return Result::Error;
}
}
bool Terminal::defaultProcessFunc(Result result) {
return (result != Result::Error) && (result != Result::NoInput);
}
void Terminal::process(ProcessFunc func) {
while (func(processLine())) {
}
}
} // ns terminal