This is a non-blocking, async REPL (Read-Evaluate-Print-Loop) console component for ESP-IDF that allows ESP_LOG messages to cleanly interleave with the active serial prompt without screen corruption or dropped input.
This component is built on a fork of the ESP-IDF Console component, primarily re-writing linenoise as a character-feed state machine so the console REPL runs in a FreeRTOS task without blocking on read().
- Non-blocking I/O - the REPL task polls for input via
linenoiseEditFeed(), yielding to the scheduler between characters - Clean log interleaving -
ESP_LOGoutput displays above the prompt without corrupting your current input- Under the hood it hooks
esp_log_set_vprintf()to erase the prompt line, print the log, and then restore the prompt line
- Under the hood it hooks
- "Smart" terminal auto-detection - probes for VT100/ANSI capabilities and upgrades/falls back to dumb mode as needed.
- 3-mode terminal control -
AUTO(default),SMART(forced), orDUMB(forced) - Full line editing - history, tab completion, inline hints, Home/End, Delete, Ctrl+W, Ctrl+L, Ctrl+A/E/C/D
- Easy setup - registers commands via the standard
esp_console_cmd_register()API. Thehelpcommand is registered automatically
- ESP-IDF ≥ 5.3 (tested on v5.3.5 & v6.0.2)
idf.py add-dependency "jwidess/esp-async-console^0.1.0"Or add manually to your project's idf_component.yml:
dependencies:
jwidess/esp-async-console: "^0.1.0"#include "async_console.h"
#include "esp_console.h"
#include "nvs_flash.h"
void app_main(void)
{
ESP_ERROR_CHECK(nvs_flash_init());
ESP_ERROR_CHECK(async_console_init(UART_NUM_0, 115200, "esp32> "));
// Register commands using the standard ESP-IDF API
const esp_console_cmd_t cmd = {
.command = "hello",
.help = "Say hello",
.func = &cmd_hello,
};
ESP_ERROR_CHECK(esp_console_cmd_register(&cmd));
}async_console_init() handles UART driver installation, esp_console_init(), the log hook, and spawning the REPL FreeRTOS task. The help command is registered automatically.
The component registers esp_console_get_completion by default for command-name completion. To add argument-level completion, override the callback after async_console_init():
#include "linenoise/async_linenoise.h"
static void my_completion(const char *buf, linenoiseCompletions *lc) {
esp_console_get_completion(buf, lc); // keep command-name completions
if (strncmp(buf, "mycommand ", 10) == 0) {
linenoiseAddCompletion(lc, "mycommand option_a");
linenoiseAddCompletion(lc, "mycommand option_b");
}
}
// After async_console_init():
linenoiseSetCompletionCallback(my_completion);The component registers esp_console_get_hint by default to display argument hints provided in the esp_console_cmd_t struct. To customize the hint string or apply ANSI color codes, override the callback, for example:
static const char *my_hints(const char *buf, int *color, int *bold) {
// Get standard ESP-IDF hint string
const char *hint = esp_console_get_hint(buf, color, bold);
if (hint) {
// Change hint color to Magenta (35) if typing "mycommand"
if (strncmp(buf, "mycommand ", 10) == 0) {
*color = 35;
*bold = 1;
}
}
return hint;
}
// After async_console_init():
linenoiseSetHintsCallback(my_hints);The console defaults to LINENOISE_MODE_AUTO. Under AUTO mode, it auto detects if the connected terminal supports smart VT100/ANSI capabilities. If detection fails, the console falls back to dumb mode.
In dumb mode, the console assumes a basic serial terminal without VT100/ANSI escape sequence support. Features like command history (Up/Down arrows), hints, tab completion, and cursor movement (Left/Right arrows) do not work. The console simply echoes printable characters, handles basic backspaces, and submits the line upon receiving a newline. This can be useful for raw logging, automation, or legacy serial monitors.
Under AUTO mode, the console auto-detects and upgrades to smart (VT100/ANSI) mode via two mechanisms:
- Active Probing (Boot & Empty Line Submissions):
An active cursor-position query (
ESC [ 6n) is sent to the terminal on boot, and whenever the user presses Enter on an empty line. If the terminal emulator responds, the console immediately upgrades to smart mode. - Passive ESC Sequence Detection: If the user presses a key that transmits an escape sequence (such as an arrow, Home, End, PageUp/Down, etc.) while in dumb mode, the console immediately upgrades to smart mode.
You can override this with:
linenoiseSetTerminalMode(LINENOISE_MODE_SMART); // Force smart mode
linenoiseSetTerminalMode(LINENOISE_MODE_DUMB); // Force dumb mode
linenoiseSetTerminalMode(LINENOISE_MODE_AUTO); // Auto modeShort transition messages (--- Upgraded to smart terminal ---) are printed by default when the detected mode changes. To disable them:
linenoiseSetModeMessages(false);The async console relies on standard VT100/ANSI escape sequences for its "smart" mode features (history, cursor movement, line clearing, etc.). Recommended serial terminal emulators include: WindTerm, TeraTerm, PuTTY, Minicom, etc.
Note
idf.py monitor: The standard Espressif IDF Monitor currently has a bug regarding ANSI escape sequences which forces dumb mode as getColumns times out. I have a PR that fixes this, but hasn't been merged yet. In the meantime, please use another terminal emulator. See esp-idf-monitor PR #43.
The console relies on the ESP32 sending ANSI color codes. If your logs are printing without color (or losing color once the REPL starts), make sure that CONFIG_LOG_COLORS=y is defined in your project's sdkconfig. When disabled, the ESP-IDF monitor automatic coloring does not work as the line clearing operations (\r\x1b[0K) contain new line characters, causing logs to render in plain text.
To fix this, ensure Component config Log is checked in idf.py menuconfig (or set CONFIG_LOG_COLORS=y in your sdkconfig.defaults). In classic menuconfig: Component config -> Log -> Format -> Color
Enables verbose logging of raw bytes received and probe timing:
esp_console_set_debug_mode(true);By default, the console supports command lines up to 256 characters and 8 arguments (including the command name). You can increase these limits via idf.py menuconfig:
Component config->Async Console ConfigurationMaximum command line arguments(Note: ESP-IDF reserves 1 slot for the command name and 1 for a NULL terminator. E.g. 16 allows the command + 14 arguments.)Maximum command line length(e.g., 512)
(Note: Standard ESP-IDF console settings like CONFIG_CONSOLE_SORTED_HELP, are used by this component.)
| Function | Description |
|---|---|
async_console_init(uart, baud, prompt) |
Initialize and start the async REPL task |
esp_console_set_debug_mode(bool) |
Enable/disable raw RX debug logging |
Additions to the standard linenoise API:
| Function | Description |
|---|---|
linenoiseSetTerminalMode(mode) |
Set AUTO / SMART / DUMB mode |
linenoiseGetTerminalMode() |
Get configured terminal mode |
linenoiseSetModeMessages(bool) |
Toggle mode change notifications |
linenoiseGetModeMessages() |
Get notification state |
linenoiseEditStart(l, buf, len, prompt) |
Begin a non-blocking edit session |
linenoiseEditFeed(l) |
Feed one byte; returns linenoiseEditMore until a line is complete |
linenoiseEditStop(l) |
End the edit session |
linenoiseHide(l) |
Erase the prompt (for async log output) |
linenoiseShow(l) |
Restore the prompt after log output |
The basic_repl example demonstrates all features:
- Background task with
ESP_LOGmessages every few seconds, cleanly interleaved with the prompt terminalmode [auto\|smart\|dumb]- view or force terminal modemodemessages- toggle mode change notificationsdebugmode [on\|off]- toggle debugtable- prints a large tableecho <args>,hello,reboot- Argument tab completion for
terminalmodeanddebugmode
app_main()
└─ async_console_init()
├─ UART driver + VFS setup
├─ esp_console_init() (registers 'help', argtable3)
├─ async_console_io_init() (creates g_log_mutex)
├─ async_console_log_hook_install() (hooks esp_log_set_vprintf)
└─ xTaskCreate(console_repl_task)
└─ loop: linenoiseEditFeed() -> esp_console_run()
ESP_LOG* from any task
└─ async_console_log_vprintf()
├─ xSemaphoreTake(g_log_mutex)
├─ linenoiseHide() (erase prompt line)
├─ fwrite(log)
├─ linenoiseShow() (restore prompt line)
└─ xSemaphoreGive(g_log_mutex)
Warning
Do not use raw printf() in background tasks!
Because this component guarantees clean log interleaving by hooking esp_log_set_vprintf, it only intercepts logs routed through ESP_LOGx .
If a background task calls printf(), it bypasses the console's mutex and line-clearing operations. The text will write directly to stdout and corrupt screen. printf() is only safe to use inside a registered console command's execution function (where the prompt is inactive).
- Reintegrate
test_apps/for automated testing - Test and verify QEMU ESP-IDF emulation, no idea if this works emulated yet
This project was developed with AI assistance tools. Do note I have put much effort into reviewing and testing any generated content, however issues may still arise.
- Component code: Apache License 2.0
linenoisefork (linenoise/): 2-Clause BSD License (original copyright Salvatore Sanfilippo & Pieter Noordhuis)- Note: This component depends on the ESP-IDF
consolecomponent (Apache-2.0) and itsargtable3dependency (3-Clause BSD).
