/** * @file cliWrapper.hpp * @author Gustice * @brief Wrapping Command-Line-Interfaces with custom endpoints * @details Command-Line-Interface is based on the implementation of * https://github.com/funbiscuit/embedded-cli * * @copyright Copyright (c) 2024 */ #pragma once #include "embedded_cli.h" #include #include #include #include "simpleStreams.hpp" /* Module configuration */ static constexpr int CLI_RX_BUFFER_SIZE = 64; static constexpr int CLI_CMD_BUFFER_SIZE = 64; // to also accept URLs for Updates static constexpr int CLI_HISTORY_SIZE = 16; static constexpr int CLI_BINDING_COUNT = 8; static constexpr std::string_view CliWelcomeString{ "** Smart-DMX-Bridge **\n" " send 'help' for help-page\n" " NOTE: Syslog-messages are sent to client of this session\n"}; /** * @brief Wrapper for c-implementation of CLI */ class Cli { public: /// @brief Type for callback arguments using CArgs = std::vector &; /// @brief Type for bind callback using BindCb = std::function; /// @brief Type for command callback using CommandCb = std::function; /// @brief Parameter structure for command-binding struct Binding { std::string_view name; std::string_view help_output; BindCb callback; }; /// @brief Constructor /// @param port Reference to serial port /// @param cmd command callback Cli(CharStream &port, CommandCb cmd) : _port(port), _commandCb(cmd) { EmbeddedCliConfig *config = embeddedCliDefaultConfig(); // NOTE: Note setting cliBuffer allows Cli to allocate needed buffer in correct size // config->cliBuffer = cliBuffer; // config->cliBufferSize = CLI_BUFFER_SIZE; config->rxBufferSize = CLI_RX_BUFFER_SIZE; config->cmdBufferSize = CLI_CMD_BUFFER_SIZE; config->historyBufferSize = CLI_HISTORY_SIZE; config->maxBindingCount = CLI_BINDING_COUNT; cli = embeddedCliNew(config); if (cli == NULL) { ESP_LOGE("CLI", "Cli was not created, requires %d bytes. Check sizes!", embeddedCliRequiredSize(config)); return; } cli->onCommand = genericCallback; cli->writeChar = writeToken; // appContext is a user-end variable and is bound to current instance cli->appContext = this; } /// @brief Destructor ~Cli() { embeddedCliFree(cli); } /// @brief Create command binding bool addBinding(Binding bind) { BindCb *cb = new BindCb{bind.callback}; CliCommandBinding cBinding{.name = bind.name.begin(), .help = bind.help_output.begin(), .tokenizeArgs = true, // NOTE: this hard reference only works because we only provide // an add-function without remove .context = (void *)(cb), .binding = boundCallback}; return embeddedCliAddBinding(cli, cBinding); } /// @brief Static method to generate c-compatible function-pointer for bound-command-callback /// @details - receives and wraps arguments in string, /// - determines object according to appContext /// - calls appropriate method of appropriate object static void boundCallback(EmbeddedCli *cli, char *args, void *context) { if (context == nullptr) { return; ESP_LOGE("CLI", "No context in callback available"); } ESP_LOGD("CLI", "Received command:"); BindCb &cb = *((BindCb *)context); uint len = embeddedCliGetTokenCount(args); std::vector sArgs; while (len-- > 0) { const char *const arg = embeddedCliGetToken(args, 1); ESP_LOGD("CLI", " arg: %s", arg); sArgs.emplace_back(arg); // shift position to end of this element to save time args += sArgs.back().size() + 1; // needs also to be shifted by null-delimiter } cb(sArgs); } /// @brief Static method to generate c-compatible function-pointer for command-callback /// @details - receives and wraps arguments in string, /// - determines object according to appContext /// - calls command-callback of appropriate object static void genericCallback(EmbeddedCli *cli, CliCommand *command) { std::string name(command->name); ESP_LOGI("CLI", "Received generic command: %s", name.c_str()); embeddedCliTokenizeArgs(command->args); std::vector sArgs; for (int i = 1; i <= embeddedCliGetTokenCount(command->args); ++i) { sArgs.emplace_back(embeddedCliGetToken(command->args, i)); ESP_LOGI("CLI", " arg %d : %s", i, sArgs.back().c_str()); } auto &commandCb = static_cast(cli->appContext)->_commandCb; commandCb(name, sArgs); } /// @brief Process buffer and trigger commands void process() { auto r = _port.read(); for (auto &&c : r) { embeddedCliReceiveChar(cli, c); } embeddedCliProcess(cli); } /// @brief publicly exposed write method void write(std::string output) { _port.write(output); } private: /// @brief Reference to hardware port CharStream &_port; /// @brief Callback for generic command CommandCb _commandCb; EmbeddedCli *cli = nullptr; /// @brief Write callback static void writeToken(EmbeddedCli *embeddedCli, char c) { static_cast(embeddedCli->appContext)->_port.write(c); } };