- /*
-
- 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 {
-
- Terminal::Commands Terminal::_commands;
-
- // TODO: `name` is never checked for uniqueness, unlike the previous implementation with the `unordered_map`
- // (and note that the map used hash IDs instead of direct string comparison)
- //
- // One possible workaround is to delegate command matching to a callback function of a module:
- //
- // > addCommandMatcher([](const String& argv0) -> Callback {
- // > if (argv0.equalsIgnoreCase(F("one")) {
- // > return cmd_one;
- // > } else if (argv0.equalsIgnoreCase(F("two")) {
- // > return cmd_two;
- // > }
- // > return nullptr;
- // > });
- //
- // Or, using a PROGMEM static array of `{progmem_name, callback_ptr}` pairs.
- // There would be a lot of PROGMEM boilerplate, though, since PROGMEM strings cannot be
- // written inline with the array itself, and must be declared with a symbol name beforehand.
- // (unless, progmem_name is a fixed-size char[], but then it must have a special limit for command length)
-
- void Terminal::addCommand(const __FlashStringHelper* name, CommandFunc func) {
- if (func) {
- _commands.emplace_front(std::make_pair(name, func));
- }
- }
-
- size_t Terminal::commands() {
- return std::distance(_commands.begin(), _commands.end());
- }
-
- Terminal::Names Terminal::names() {
- Terminal::Names out;
- out.reserve(commands());
- 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 aka command
- auto cmdline = parsing::parse_commandline(_buffer.data());
- _buffer.clear();
- if ((cmdline.argc >= 1) && (cmdline.argv[0].length())) {
- auto found = std::find_if(_commands.begin(), _commands.end(), [&cmdline](const Command& command) {
- // note that `String::equalsIgnoreCase(const __FlashStringHelper*)` does not exist, and will create a temporary `String`
- // both use read-1-byte-at-a-time for PROGMEM, however this variant saves around 200μs in time since there's no temporary object
- auto* lhs = cmdline.argv[0].c_str();
- auto* rhs = reinterpret_cast<const char*>(command.first);
- auto len = strlen_P(rhs);
-
- return (cmdline.argv[0].length() == len) && (0 == strncasecmp_P(lhs, rhs, len));
- });
- if (found == _commands.end()) return Result::CommandNotFound;
- (*found).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())) {
- }
- }
-
- } // namespace terminal
|