/ firmware / include / Console.h
Console.h
  1  /*
  2   * Console Arduino Library — wrapper for the ESP-IDF console component
  3   *
  4   * Provides an Arduino-style API for interactive command-line consoles with
  5   * full REPL support, tab completion, command history, argtable3 argument
  6   * parsing, and context-aware commands.
  7   *
  8   * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
  9   * SPDX-License-Identifier: LGPL-2.1-or-later
 10   */
 11  
 12  #pragma once
 13  
 14  #include "Arduino.h"
 15  #include "FS.h"
 16  #include "freertos/FreeRTOS.h"
 17  #include "freertos/task.h"
 18  
 19  /**
 20   * @brief Callback type for a console command.
 21   *
 22   * @param argc  Number of arguments (including the command name at argv[0]).
 23   * @param argv  Array of null-terminated argument strings.
 24   * @return      0 on success, non-zero on error.
 25   */
 26  typedef int (*ConsoleCommandFunc)(int argc, char **argv);
 27  
 28  /**
 29   * @brief Callback type for a context-aware console command.
 30   *
 31   * @param context  User-supplied context pointer passed at registration.
 32   * @param argc     Number of arguments.
 33   * @param argv     Array of null-terminated argument strings.
 34   * @return         0 on success, non-zero on error.
 35   */
 36  typedef int (*ConsoleCommandFuncWithCtx)(void *context, int argc, char **argv);
 37  
 38  class ConsoleClass {
 39  public:
 40    ConsoleClass();
 41  
 42    // ---------------------------------------------------------------------------
 43    // Lifecycle
 44    // ---------------------------------------------------------------------------
 45  
 46    /**
 47     * @brief Initialize the console.
 48     *
 49     * Must be called before any other method. Sets up the esp_console module and
 50     * configures linenoise tab-completion and hints.
 51     *
 52     * @param maxCmdLen  Maximum length of a single command line (bytes). Default 256.
 53     * @param maxArgs    Maximum number of whitespace-separated tokens per line. Default 32.
 54     * @return           true on success.
 55     */
 56    bool begin(size_t maxCmdLen = 256, size_t maxArgs = 32);
 57  
 58    /**
 59     * @brief De-initialize the console.
 60     *
 61     * Stops the REPL task if running, saves history, and releases esp_console resources.
 62     */
 63    void end();
 64  
 65    // ---------------------------------------------------------------------------
 66    // Configuration — call before begin()
 67    // ---------------------------------------------------------------------------
 68  
 69    /** @brief Set the REPL prompt string. Default: "esp> ". */
 70    void setPrompt(const char *prompt);
 71    void setPrompt(const String &prompt) {
 72      setPrompt(prompt.c_str());
 73    }
 74  
 75    /** @brief Set the maximum number of history entries kept in RAM. Default: 32. */
 76    void setMaxHistory(uint32_t maxLen);
 77  
 78    /**
 79     * @brief Set a filesystem and path for persistent command history.
 80     *
 81     * When set, history is loaded at begin() and saved after every command.
 82     * The filesystem must be mounted before calling begin().
 83     *
 84     * @param fs    Arduino filesystem object. Also accepts SPIFFS, FFat, etc.
 85     * @param path  File path relative to the filesystem root (e.g. "/history.txt").
 86     */
 87    void setHistoryFile(fs::FS &fs, const char *path);
 88    void setHistoryFile(fs::FS &fs, const String &path) {
 89      setHistoryFile(fs, path.c_str());
 90    }
 91  
 92    /** @brief Set the REPL background task stack size in bytes. Default: 4096. */
 93    void setTaskStackSize(uint32_t size);
 94  
 95    /** @brief Set the REPL background task priority. Default: 2. */
 96    void setTaskPriority(uint32_t priority);
 97  
 98    /** @brief Pin the REPL task to a specific core. Default: tskNO_AFFINITY. */
 99    void setTaskCore(BaseType_t core);
100  
101    /**
102     * @brief Route all Console PSRAM-eligible allocations to external SPI RAM.
103     *
104     * When enabled:
105     * - The esp_console command registry (@c heap_alloc_caps in
106     *   @c esp_console_config_t) is allocated from PSRAM.
107     * - The REPL FreeRTOS task stack is allocated from PSRAM via
108     *   @c xTaskCreatePinnedToCoreWithCaps.
109     *
110     * This frees internal SRAM for other uses at the cost of slightly higher
111     * latency for stack and heap accesses. If PSRAM is not available at
112     * runtime, the library automatically falls back to internal RAM.
113     *
114     * Must be called before begin() to affect the heap allocation, and before
115     * attachToSerial(true) to affect the task stack.
116     *
117     * @param enable  true to use PSRAM, false for internal RAM (default: true).
118     */
119    void usePsram(bool enable);
120  
121    // ---------------------------------------------------------------------------
122    // Command registration
123    // ---------------------------------------------------------------------------
124  
125    /**
126     * @brief Register a command with a simple callback.
127     *
128     * @param name  Command name (no spaces). Must remain valid until end().
129     * @param help  Help text shown by the built-in "help" command.
130     * @param func  Command handler.
131     * @return      true on success.
132     */
133    bool addCmd(const char *name, const char *help, ConsoleCommandFunc func);
134    bool addCmd(const String &name, const String &help, ConsoleCommandFunc func) {
135      return addCmd(name.c_str(), help.c_str(), func);
136    }
137  
138    /**
139     * @brief Register a command with an explicit hint string.
140     *
141     * @param hint  Short argument hint shown inline during typing (e.g. "<pin> <value>").
142     */
143    bool addCmd(const char *name, const char *help, const char *hint, ConsoleCommandFunc func);
144    bool addCmd(const String &name, const String &help, const String &hint, ConsoleCommandFunc func) {
145      return addCmd(name.c_str(), help.c_str(), hint.c_str(), func);
146    }
147  
148    /**
149     * @brief Register a command with an argtable3 argument table.
150     *
151     * The argtable is used to auto-generate the hint string.  Declare the table
152     * as a struct ending with arg_end() and pass a pointer to it.
153     *
154     * @param argtable  Pointer to an argtable3 argument table (ends with arg_end).
155     */
156    bool addCmd(const char *name, const char *help, void *argtable, ConsoleCommandFunc func);
157  
158    /**
159     * @brief Register a context-aware command.
160     *
161     * @param ctx  Pointer passed verbatim as the first argument to func.
162     */
163    bool addCmdWithContext(const char *name, const char *help, ConsoleCommandFuncWithCtx func, void *ctx);
164    bool addCmdWithContext(const String &name, const String &help, ConsoleCommandFuncWithCtx func, void *ctx) {
165      return addCmdWithContext(name.c_str(), help.c_str(), func, ctx);
166    }
167  
168    bool addCmdWithContext(const char *name, const char *help, const char *hint, ConsoleCommandFuncWithCtx func, void *ctx);
169    bool addCmdWithContext(const String &name, const String &help, const String &hint, ConsoleCommandFuncWithCtx func, void *ctx) {
170      return addCmdWithContext(name.c_str(), help.c_str(), hint.c_str(), func, ctx);
171    }
172  
173    bool addCmdWithContext(const char *name, const char *help, void *argtable, ConsoleCommandFuncWithCtx func, void *ctx);
174  
175    /**
176     * @brief Unregister a previously registered command.
177     *
178     * @param name  Command name to remove.
179     * @return      true on success.
180     */
181    bool removeCmd(const char *name);
182    bool removeCmd(const String &name) {
183      return removeCmd(name.c_str());
184    }
185  
186    // ---------------------------------------------------------------------------
187    // Built-in help command
188    // ---------------------------------------------------------------------------
189  
190    /**
191     * @brief Register the built-in "help" command.
192     *
193     * When called with no arguments, prints all registered commands with their
194     * hints and help text. When called with a command name, prints help for
195     * that command only.
196     *
197     * @return true on success.
198     */
199    bool addHelpCmd();
200  
201    /** @brief Remove the built-in "help" command. */
202    bool removeHelpCmd();
203  
204    /**
205     * @brief Control how much detail the "help" command prints.
206     *
207     * @param level  0 = brief (name + hint only), 1 = verbose (includes help text).
208     * @return       true on success.
209     */
210    bool setHelpVerboseLevel(int level);
211  
212    // ---------------------------------------------------------------------------
213    // REPL — background FreeRTOS task
214    // ---------------------------------------------------------------------------
215  
216    /**
217     * @brief Attach the console to the serial port and start the REPL.
218     *
219     * When enabled this will start the Read-Eval-Print Loop in a background FreeRTOS task
220     * and read input through @c Serial and write output to @c stdout.
221     *
222     * When disabled the REPL will stop and the background task will be stopped.
223     * The serial port will be available for other use.
224     *
225     * Works with UART and HWCDC automatically: the REPL reads input through
226     * @c Serial and writes output to @c stdout. No transport-specific
227     * configuration is required.
228     *
229     * @note USB OTG (CDC via TinyUSB / USBSerial) is not currently supported.
230     *
231     * At startup the task probes the terminal for VT100 support (by sending a
232     * one-time Device Status Request). If the terminal does not respond, plain
233     * mode is enabled automatically. Use setPlainMode() to override the probe.
234     *
235     * Requires begin() to have been called first.
236     *
237     * @param enable  true to attach, false to detach.
238     *
239     * @return true on success.
240     */
241    bool attachToSerial(bool enable);
242  
243    /**
244     * @brief Check if the console is attached to the serial port.
245     *
246     * @return true if the console is attached to the serial port.
247     */
248    bool isAttachedToSerial();
249  
250    // ---------------------------------------------------------------------------
251    // Manual command execution (no REPL task)
252    // ---------------------------------------------------------------------------
253  
254    /**
255     * @brief Execute a command line string directly.
256     *
257     * Useful when building a custom input loop instead of using attachToSerial(true),
258     * or for invoking commands from within other command handlers.
259     *
260     * @note When calling run() from inside a command handler, the caller's
261     *       @c argv pointers are invalidated because the underlying IDF
262     *       function shares a single parse buffer. Copy any @c argv values
263     *       you need into local variables @b before calling run().
264     *
265     * @param cmdline  Full command line (command name + arguments).
266     * @return         The command's return code, or -1 on error.
267     */
268    int run(const char *cmdline);
269    int run(const String &cmdline) {
270      return run(cmdline.c_str());
271    }
272  
273    // ---------------------------------------------------------------------------
274    // linenoise utilities
275    // ---------------------------------------------------------------------------
276  
277    /** @brief Clear the terminal screen. */
278    void clearScreen();
279  
280    /**
281     * @brief Enable or disable multi-line editing mode.
282     *
283     * In multi-line mode the prompt and input wrap across terminal rows.
284     * Disabled by default.
285     */
286    void setMultiLine(bool enable);
287  
288    /**
289     * @brief Enable or disable plain (non-VT100) input mode.
290     *
291     * In plain mode linenoise falls back to basic line input without cursor
292     * movement, arrow-key history, or tab completion rendering. Useful for
293     * terminals that do not support ANSI/VT100 escape sequences.
294     *
295     * By default, attachToSerial(true) probes the terminal at startup (sending a
296     * one-time Device Status Request) and enables plain mode automatically if the
297     * terminal does not respond.
298     *
299     * @param enable  true to enable plain mode, false to restore VT100 mode.
300     * @param force   When true (default), the automatic VT100 probe at
301     *                attachToSerial(true) startup is skipped and the mode set
302     *                here is kept unconditionally. When false, the mode is set
303     *                immediately but attachToSerial(true) may still override it
304     *                based on the probe result.
305     */
306    void setPlainMode(bool enable, bool force = true);
307  
308    /**
309     * @brief Check whether plain (non-VT100) input mode is currently active.
310     *
311     * @return true if plain mode is enabled.
312     */
313    bool isPlainMode();
314  
315    // ---------------------------------------------------------------------------
316    // Argument splitting utility
317    // ---------------------------------------------------------------------------
318  
319    /**
320     * @brief Split a command line string into argv-style tokens in place.
321     *
322     * Handles quoted strings and backslash escapes. Modifies the input buffer.
323     *
324     * @param line       Null-terminated input string (modified in place).
325     * @param argv       Output array of pointers to tokens.
326     * @param argv_size  Size of the argv array (max tokens + 1 for NULL sentinel).
327     * @return           Number of tokens found (argc).
328     */
329    static size_t splitArgv(char *line, char **argv, size_t argv_size);
330  
331  private:
332    /**
333     * @brief Start the REPL in a background FreeRTOS task.
334     *
335     * Works with UART and HWCDC automatically: the REPL reads input through
336     * @c Serial and writes output to @c stdout. No transport-specific
337     * configuration is required.
338     *
339     * @note USB OTG (CDC via TinyUSB / USBSerial) is not currently supported.
340     *
341     * At startup the task probes the terminal for VT100 support (by sending a
342     * one-time Device Status Request). If the terminal does not respond, plain
343     * mode is enabled automatically. Use setPlainMode() to override the probe.
344     *
345     * Requires begin() to have been called first.
346     *
347     * @return true on success.
348     */
349    bool startRepl();
350  
351    /**
352     * @brief Stop the REPL task.
353     *
354     * @return true on success.
355     */
356    bool stopRepl();
357    static void _replTask(void *arg);
358  
359    static constexpr size_t MAX_CMDS = 64;
360    struct CmdEntry {
361      const char *name;
362      const char *hint;
363      const char *help;
364    };
365  
366    void _trackCmd(const char *name, const char *help, const char *hint);
367    static int _helpHandler(int argc, char **argv);
368  
369    bool _initialized;
370    bool _replStarted;
371    TaskHandle_t _replTaskHandle;
372    const char *_prompt;
373    uint32_t _maxHistory;
374    char *_historyVfsPath;
375    uint32_t _taskStackSize;
376    uint32_t _taskPriority;
377    BaseType_t _taskCore;
378    bool _usePsram;
379    bool _taskStackInPsram;
380    bool _forceMode;
381  
382    CmdEntry _cmds[MAX_CMDS];
383    size_t _cmdCount;
384  };
385  
386  extern ConsoleClass Console;