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;