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;