/ firmware / src / programs / shell / console_impl.cpp
console_impl.cpp
  1  /*
  2   * Console Arduino Library — implementation
  3   *
  4   * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
  5   * SPDX-License-Identifier: LGPL-2.1-or-later
  6   */
  7  
  8  #include "Arduino.h"
  9  #include "Console.h"
 10  #include "esp_err.h"
 11  #include "esp_log.h"
 12  #include "freertos/FreeRTOS.h"
 13  #include "freertos/task.h"
 14  #include "esp_console.h"
 15  #include "linenoise/linenoise.h"
 16  #include "argtable3/argtable3.h"
 17  #include "esp_idf_version.h"
 18  
 19  #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
 20  
 21  // Linenoise read function that reads from Arduino Serial instead of VFS stdin.
 22  // Serial.begin() installs the UART/USB driver whose ISR drains the hardware
 23  // FIFO into a ring buffer.  VFS simplified read() also reads from the hardware
 24  // FIFO, causing a race that loses bytes.  By reading through Serial we go via
 25  // the same ring buffer the ISR fills, eliminating the conflict and working on
 26  // every transport (UART, USB CDC, HWCDC) without transport-specific VFS calls.
 27  //
 28  // Line-ending normalization: \r → \n, and \n immediately after \r is dropped.
 29  // This handles terminals that send \r only, \n only, or \r\n.
 30  //
 31  // Returns -1 when the REPL is being stopped so linenoise returns NULL
 32  // immediately, allowing the task loop to observe _replStarted == false and
 33  // exit cleanly instead of blocking indefinitely waiting for serial input.
 34  static ssize_t _serialRead(int fd, void *buf, size_t count) {
 35    (void)fd;
 36    static bool s_prevCR = false;
 37    uint8_t *p = (uint8_t *)buf;
 38    size_t n = 0;
 39    while (n < count) {
 40      if (!Console.isAttachedToSerial()) {
 41        return -1;
 42      }
 43      if (Serial.available()) {
 44        uint8_t c = Serial.read();
 45        if (c == '\n' && s_prevCR) {
 46          s_prevCR = false;
 47          continue;
 48        }
 49        s_prevCR = (c == '\r');
 50        if (c == '\r') {
 51          c = '\n';
 52        }
 53        p[n++] = c;
 54      } else {
 55        vTaskDelay(pdMS_TO_TICKS(1));
 56      }
 57    }
 58    return (ssize_t)n;
 59  }
 60  
 61  // VT100 probe that reads through Serial instead of VFS stdin.
 62  // linenoiseProbe() uses raw read() on stdin which races with the UART ISR,
 63  // so we implement our own using Serial for both write and read.
 64  static bool _probeVT100() {
 65    // Drain any stale bytes in Serial's buffer
 66    while (Serial.available()) {
 67      Serial.read();
 68    }
 69  
 70    // Send Device Status Request (ESC[5n)
 71    Serial.print("\x1b[5n");
 72    Serial.flush();
 73  
 74    // Wait for response: ESC[0n (OK) or ESC[3n (not OK)
 75    const int retry_ms = 10;
 76    int timeout_ms = 500;
 77    size_t read_bytes = 0;
 78    char response[4] = {};
 79  
 80    while (timeout_ms > 0 && read_bytes < 4) {
 81      vTaskDelay(pdMS_TO_TICKS(retry_ms));
 82      timeout_ms -= retry_ms;
 83      while (Serial.available() && read_bytes < 4) {
 84        char c = Serial.read();
 85        if (read_bytes == 0 && c != '\x1b') {
 86          continue;
 87        }
 88        response[read_bytes++] = c;
 89      }
 90    }
 91  
 92    return (read_bytes >= 4 && response[0] == '\x1b' && response[1] == '[' && response[2] == '0' && response[3] == 'n');
 93  }
 94  
 95  #endif
 96  
 97  // ---------------------------------------------------------------------------
 98  // Constructor
 99  // ---------------------------------------------------------------------------
100  
101  ConsoleClass::ConsoleClass()
102    : _initialized(false), _replStarted(false), _replTaskHandle(NULL), _prompt("esp> "), _maxHistory(32), _historyVfsPath(NULL), _taskStackSize(4096),
103      _taskPriority(2), _taskCore(tskNO_AFFINITY), _usePsram(true), _taskStackInPsram(false), _forceMode(false), _cmds{}, _cmdCount(0) {}
104  
105  // ---------------------------------------------------------------------------
106  // Lifecycle
107  // ---------------------------------------------------------------------------
108  
109  bool ConsoleClass::begin(size_t maxCmdLen, size_t maxArgs) {
110    if (_initialized) {
111      log_w("Console already initialized");
112      return true;
113    }
114  
115    if (_usePsram && !psramFound()) {
116      log_w("PSRAM not available, falling back to internal RAM");
117      _usePsram = false;
118    }
119  
120    esp_console_config_t cfg = ESP_CONSOLE_CONFIG_DEFAULT();
121    cfg.max_cmdline_length = maxCmdLen;
122    cfg.max_cmdline_args = maxArgs;
123    if (_usePsram) {
124      cfg.heap_alloc_caps = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT;
125    }
126  
127    esp_err_t err = esp_console_init(&cfg);
128    if (err != ESP_OK) {
129      log_e("esp_console_init failed: %s", esp_err_to_name(err));
130      return false;
131    }
132  
133    // Wire up linenoise tab-completion and hints from esp_console
134    linenoiseSetCompletionCallback(&esp_console_get_completion);
135    linenoiseSetHintsCallback((linenoiseHintsCallback *)&esp_console_get_hint);
136  
137    // History
138    linenoiseHistorySetMaxLen((int)_maxHistory);
139    if (_historyVfsPath) {
140      linenoiseHistoryLoad(_historyVfsPath);
141    }
142  
143    _initialized = true;
144    return true;
145  }
146  
147  void ConsoleClass::end() {
148    if (!_initialized) {
149      return;
150    }
151  
152    stopRepl();
153  
154    if (_historyVfsPath) {
155      linenoiseHistorySave(_historyVfsPath);
156    }
157    linenoiseHistoryFree();
158  
159    esp_err_t err = esp_console_deinit();
160    if (err != ESP_OK) {
161      log_e("esp_console_deinit failed: %s", esp_err_to_name(err));
162    }
163  
164    free(_historyVfsPath);
165    _historyVfsPath = NULL;
166    _initialized = false;
167  }
168  
169  // ---------------------------------------------------------------------------
170  // Configuration
171  // ---------------------------------------------------------------------------
172  
173  void ConsoleClass::setPrompt(const char *prompt) {
174    _prompt = prompt;
175  }
176  
177  void ConsoleClass::setMaxHistory(uint32_t maxLen) {
178    _maxHistory = maxLen;
179    if (_initialized) {
180      linenoiseHistorySetMaxLen((int)maxLen);
181    }
182  }
183  
184  void ConsoleClass::setHistoryFile(fs::FS &fs, const char *path) {
185    free(_historyVfsPath);
186    _historyVfsPath = NULL;
187  
188    const char *mp = fs.mountpoint();
189    if (!mp || !path) {
190      return;
191    }
192  
193    size_t mpLen = strlen(mp);
194    size_t pathLen = strlen(path);
195    _historyVfsPath = (char *)malloc(mpLen + pathLen + 1);
196    if (_historyVfsPath) {
197      snprintf(_historyVfsPath, mpLen + pathLen + 1, "%s%s", mp, path);
198    }
199  }
200  
201  void ConsoleClass::setTaskStackSize(uint32_t size) {
202    if (_initialized) {
203      log_e("Console already initialized — call before begin()");
204      return;
205    }
206    _taskStackSize = size;
207  }
208  
209  void ConsoleClass::setTaskPriority(uint32_t priority) {
210    if (_initialized) {
211      log_e("Console already initialized — call before begin()");
212      return;
213    }
214    _taskPriority = priority;
215  }
216  
217  void ConsoleClass::setTaskCore(BaseType_t core) {
218    if (_initialized) {
219      log_e("Console already initialized — call before begin()");
220      return;
221    }
222    _taskCore = core;
223  }
224  
225  void ConsoleClass::usePsram(bool enable) {
226    if (_initialized) {
227      log_e("Console already initialized — call before begin()");
228      return;
229    }
230    _usePsram = enable;
231  }
232  
233  // ---------------------------------------------------------------------------
234  // Command registration
235  // ---------------------------------------------------------------------------
236  
237  bool ConsoleClass::addCmd(const char *name, const char *help, ConsoleCommandFunc func) {
238    return addCmd(name, help, (const char *)NULL, func);
239  }
240  
241  bool ConsoleClass::addCmd(const char *name, const char *help, const char *hint, ConsoleCommandFunc func) {
242    if (!_initialized) {
243      log_e("Console not initialized");
244      return false;
245    }
246    esp_console_cmd_t cmd;
247    memset(&cmd, 0, sizeof(esp_console_cmd_t));
248    cmd.command = name;
249    cmd.help = help;
250    cmd.hint = hint;
251    cmd.func = func;
252    esp_err_t err = esp_console_cmd_register(&cmd);
253    if (err != ESP_OK) {
254      log_e("Failed to register command '%s': %s", name, esp_err_to_name(err));
255      return false;
256    }
257    _trackCmd(name, help, hint);
258    return true;
259  }
260  
261  void ConsoleClass::_trackCmd(const char *name, const char *help, const char *hint) {
262    if (_cmdCount < MAX_CMDS) {
263      _cmds[_cmdCount++] = {name, hint, help};
264    }
265  }
266  
267  bool ConsoleClass::addCmd(const char *name, const char *help, void *argtable, ConsoleCommandFunc func) {
268    if (!_initialized) {
269      log_e("Console not initialized");
270      return false;
271    }
272    esp_console_cmd_t cmd;
273    memset(&cmd, 0, sizeof(esp_console_cmd_t));
274    cmd.command = name;
275    cmd.help = help;
276    cmd.hint = NULL;  // auto-generated from argtable
277    cmd.func = func;
278    cmd.argtable = argtable;
279    esp_err_t err = esp_console_cmd_register(&cmd);
280    if (err != ESP_OK) {
281      log_e("Failed to register command '%s': %s", name, esp_err_to_name(err));
282      return false;
283    }
284    return true;
285  }
286  
287  bool ConsoleClass::addCmdWithContext(const char *name, const char *help, ConsoleCommandFuncWithCtx func, void *ctx) {
288    return addCmdWithContext(name, help, (const char *)NULL, func, ctx);
289  }
290  
291  bool ConsoleClass::addCmdWithContext(const char *name, const char *help, const char *hint, ConsoleCommandFuncWithCtx func, void *ctx) {
292    if (!_initialized) {
293      log_e("Console not initialized");
294      return false;
295    }
296    esp_console_cmd_t cmd;
297    memset(&cmd, 0, sizeof(esp_console_cmd_t));
298    cmd.command = name;
299    cmd.help = help;
300    cmd.hint = hint;
301    cmd.func_w_context = func;
302    cmd.context = ctx;
303    esp_err_t err = esp_console_cmd_register(&cmd);
304    if (err != ESP_OK) {
305      log_e("Failed to register command '%s': %s", name, esp_err_to_name(err));
306      return false;
307    }
308    return true;
309  }
310  
311  bool ConsoleClass::addCmdWithContext(const char *name, const char *help, void *argtable, ConsoleCommandFuncWithCtx func, void *ctx) {
312    if (!_initialized) {
313      log_e("Console not initialized");
314      return false;
315    }
316    esp_console_cmd_t cmd;
317    memset(&cmd, 0, sizeof(esp_console_cmd_t));
318    cmd.command = name;
319    cmd.help = help;
320    cmd.hint = NULL;
321    cmd.func_w_context = func;
322    cmd.context = ctx;
323    cmd.argtable = argtable;
324    esp_err_t err = esp_console_cmd_register(&cmd);
325    if (err != ESP_OK) {
326      log_e("Failed to register command '%s': %s", name, esp_err_to_name(err));
327      return false;
328    }
329    return true;
330  }
331  
332  bool ConsoleClass::removeCmd(const char *name) {
333    if (!_initialized) {
334      log_e("Console not initialized");
335      return false;
336    }
337  #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 0)
338    esp_err_t err = esp_console_cmd_deregister(name);
339    if (err != ESP_OK) {
340      log_e("Failed to deregister command '%s': %s", name, esp_err_to_name(err));
341      return false;
342    }
343    return true;
344  #else
345    log_e("Commands cannot be deregistered in this version of ESP-IDF");
346    return false;
347  #endif
348  }
349  
350  // ---------------------------------------------------------------------------
351  // Built-in help command
352  // ---------------------------------------------------------------------------
353  
354  int ConsoleClass::_helpHandler(int argc, char **argv) {
355    if (argc == 2) {
356      for (size_t i = 0; i < Console._cmdCount; i++) {
357        if (strcmp(Console._cmds[i].name, argv[1]) == 0) {
358          printf("  %s", Console._cmds[i].name);
359          if (Console._cmds[i].hint) printf("  %s", Console._cmds[i].hint);
360          printf("\n    %s\n", Console._cmds[i].help);
361          return 0;
362        }
363      }
364      printf("unknown command: %s\n", argv[1]);
365      return 1;
366    }
367  
368    printf("\n");
369    for (size_t i = 0; i < Console._cmdCount; i++) {
370      printf("  \x1b[32m%-20s\x1b[0m", Console._cmds[i].name);
371      if (Console._cmds[i].hint)
372        printf(" %-28s", Console._cmds[i].hint);
373      else
374        printf(" %-28s", "");
375      printf(" %s\n", Console._cmds[i].help);
376    }
377    printf("\n");
378    return 0;
379  }
380  
381  bool ConsoleClass::addHelpCmd() {
382    return addCmd("help", "show available commands", "[command]", _helpHandler);
383  }
384  
385  bool ConsoleClass::removeHelpCmd() {
386    if (!_initialized) {
387      log_e("Console not initialized");
388      return false;
389    }
390  #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
391    esp_err_t err = esp_console_deregister_help_command();
392    if (err != ESP_OK) {
393      log_e("Failed to deregister help command: %s", esp_err_to_name(err));
394      return false;
395    }
396    return true;
397  #else
398    log_e("Help command cannot be deregistered in this version of ESP-IDF");
399    return false;
400  #endif
401  }
402  
403  bool ConsoleClass::setHelpVerboseLevel(int level) {
404    if (!_initialized) {
405      log_e("Console not initialized");
406      return false;
407    }
408  #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 0)
409    if (level < 0 || level >= ESP_CONSOLE_HELP_VERBOSE_LEVEL_MAX_NUM) {
410      log_e("Invalid verbose level: %d", level);
411      return false;
412    }
413    esp_err_t err = esp_console_set_help_verbose_level((esp_console_help_verbose_level_e)level);
414    if (err != ESP_OK) {
415      log_e("Failed to set help verbose level: %s", esp_err_to_name(err));
416      return false;
417    }
418    return true;
419  #else
420    log_e("Help verbose level cannot be set in this version of ESP-IDF");
421    return false;
422  #endif
423  }
424  
425  // ---------------------------------------------------------------------------
426  // REPL task
427  // ---------------------------------------------------------------------------
428  
429  void ConsoleClass::_replTask(void *arg) {
430    ConsoleClass *self = (ConsoleClass *)arg;
431  
432    // Auto-detect VT100 support unless the user already forced plain mode.
433    // Uses a custom probe that reads through Serial instead of VFS stdin,
434    // avoiding the race between the UART ISR and VFS read().
435    if (!self->_forceMode) {
436      log_d("Probing for VT100 support");
437      bool vt100;
438  #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
439      vt100 = _probeVT100();
440  #else
441      vt100 = linenoiseProbe() == 0;
442  #endif
443      log_d("VT100 probe result: %s", vt100 ? "supported" : "not supported");
444      linenoiseSetDumbMode(!vt100);
445    }
446  
447  #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
448    // Install our Serial-based read function for linenoise.  This bypasses the
449    // VFS stdin path entirely, reading from the same ring buffer that the
450    // UART/USB driver ISR fills.  This avoids the race between the ISR and VFS
451    // simplified read() that causes lost bytes, and works for UART, USB CDC,
452    // and HWCDC.
453  
454    // For older versions of ESP-IDF the read function cannot be overridden.
455    // This means that the console will only work with regular UART and or HWCDC
456    // depending on the sdkconfig.
457    linenoiseSetReadFunction(_serialRead);
458  #endif
459  
460    while (self->_replStarted) {
461      char *line = linenoise(self->_prompt);
462      if (line == NULL) {
463        // EOF or error — yield and retry
464        vTaskDelay(pdMS_TO_TICKS(10));
465        continue;
466      }
467  
468      if (strlen(line) > 0) {
469        linenoiseHistoryAdd(line);
470        if (self->_historyVfsPath) {
471          linenoiseHistorySave(self->_historyVfsPath);
472        }
473  
474        int ret = 0;
475        esp_err_t err = esp_console_run(line, &ret);
476        switch (err) {
477          case ESP_OK:              break;
478          case ESP_ERR_NOT_FOUND:   printf("Unknown command. Type 'help' for a list of commands.\n"); break;
479          case ESP_ERR_INVALID_ARG: break;  // empty line
480          default:                  printf("Command error: %s\n", esp_err_to_name(err)); break;
481        }
482  
483        // Flush any command output that did not end with a newline. stdout is
484        // line-buffered by default in ESP-IDF, so partial lines would otherwise
485        // stay invisible until the next newline or buffer fill.
486        fflush(stdout);
487      }
488  
489      linenoiseFree(line);
490    }
491  
492    if (self->_usePsram && self->_taskStackInPsram) {
493      vTaskDeleteWithCaps(NULL);
494    } else {
495      vTaskDelete(NULL);
496    }
497  }
498  
499  bool ConsoleClass::startRepl() {
500    if (!_initialized) {
501      log_e("Console not initialized — call begin() first");
502      return false;
503    }
504    if (_replStarted) {
505      log_w("REPL already running");
506      return true;
507    }
508  
509    _replStarted = true;
510  
511    BaseType_t ret;
512  
513  #ifdef CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM
514    _taskStackInPsram = _usePsram;
515  #else
516    _taskStackInPsram = false;
517  #endif
518  
519    if (_taskStackInPsram) {
520      ret = xTaskCreatePinnedToCoreWithCaps(
521        _replTask, "console_repl", _taskStackSize, this, _taskPriority, &_replTaskHandle, _taskCore, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT
522      );
523    } else {
524      ret = xTaskCreatePinnedToCore(_replTask, "console_repl", _taskStackSize, this, _taskPriority, &_replTaskHandle, _taskCore);
525    }
526    if (ret != pdPASS) {
527      log_e("Failed to create REPL task");
528      _replStarted = false;
529      return false;
530    }
531    return true;
532  }
533  
534  bool ConsoleClass::stopRepl() {
535    if (!_replStarted) {
536      return true;
537    }
538    // Setting _replStarted = false causes _serialRead to return -1 immediately,
539    // which makes linenoise return NULL and the task loop exit on its very next
540    // iteration (~1-11 ms). Without this, the task would stay blocked in
541    // _serialRead indefinitely if no serial bytes arrive, and would outlive
542    // stopRepl(), potentially accessing state freed by end().
543    _replStarted = false;
544    // Give the task time to observe _replStarted and call vTaskDelete().
545    vTaskDelay(pdMS_TO_TICKS(100));
546    _replTaskHandle = NULL;
547    return true;
548  }
549  
550  bool ConsoleClass::attachToSerial(bool enable) {
551    return enable ? startRepl() : stopRepl();
552  }
553  
554  bool ConsoleClass::isAttachedToSerial() {
555    return _replStarted;
556  }
557  
558  // ---------------------------------------------------------------------------
559  // Manual command execution
560  // ---------------------------------------------------------------------------
561  
562  int ConsoleClass::run(const char *cmdline) {
563    if (!_initialized) {
564      log_e("Console not initialized");
565      return -1;
566    }
567    int ret = 0;
568    esp_err_t err = esp_console_run(cmdline, &ret);
569    if (err == ESP_ERR_NOT_FOUND) {
570      log_w("Unknown command: %s", cmdline);
571      return -1;
572    } else if (err == ESP_ERR_INVALID_ARG) {
573      return 0;  // empty line
574    } else if (err != ESP_OK) {
575      log_e("esp_console_run error: %s", esp_err_to_name(err));
576      return -1;
577    }
578    return ret;
579  }
580  
581  // ---------------------------------------------------------------------------
582  // linenoise utilities
583  // ---------------------------------------------------------------------------
584  
585  void ConsoleClass::clearScreen() {
586    linenoiseClearScreen();
587  }
588  
589  void ConsoleClass::setMultiLine(bool enable) {
590    linenoiseSetMultiLine(enable ? 1 : 0);
591  }
592  
593  void ConsoleClass::setPlainMode(bool enable, bool force) {
594    _forceMode = force;
595    linenoiseSetDumbMode(enable ? 1 : 0);
596  }
597  
598  bool ConsoleClass::isPlainMode() {
599    return linenoiseIsDumbMode();
600  }
601  
602  // ---------------------------------------------------------------------------
603  // Argument splitting
604  // ---------------------------------------------------------------------------
605  
606  size_t ConsoleClass::splitArgv(char *line, char **argv, size_t argv_size) {
607    return esp_console_split_argv(line, argv, argv_size);
608  }
609  
610  // ---------------------------------------------------------------------------
611  // Global instance
612  // ---------------------------------------------------------------------------
613  
614  ConsoleClass Console;