diff --git a/components/console/CMakeLists.txt b/components/console/CMakeLists.txt index a6c7632830..2647e14675 100644 --- a/components/console/CMakeLists.txt +++ b/components/console/CMakeLists.txt @@ -1,5 +1,8 @@ idf_build_get_property(target IDF_TARGET) +# Note: Almost all source files in this component include console_stdio_private.h. +# This header only affects LIBC_PICOLIBC builds, not LIBC_NEWLIB builds. +# It enables thread-local stdio streams, which are required for console functionality in some cases. set(srcs "commands.c" "esp_console_common.c" "esp_console_repl_internal.c" @@ -40,6 +43,14 @@ idf_component_register(SRCS ${srcs} esp_driver_usb_serial_jtag ) +if(CONFIG_LIBC_PICOLIBC) + list(APPEND srcs_include_stdio_private ${srcs}) + list(APPEND srcs_include_stdio_private ${argtable_srcs}) + list(REMOVE_ITEM srcs_include_stdio_private "esp_console_repl_chip.c" "esp_console_repl_linux.c") + set_source_files_properties(${srcs_include_stdio_private} + PROPERTIES COMPILE_FLAGS "--include=console_stdio_private.h") +endif() + if(CONFIG_COMPILER_STATIC_ANALYZER AND CMAKE_C_COMPILER_ID STREQUAL "GNU") # TODO IDF-10085 target_compile_options(${COMPONENT_LIB} PRIVATE "-fno-analyzer") endif() diff --git a/components/console/esp_console_common.c b/components/console/esp_console_common.c index ce5c61b4a7..17c0ec62ba 100644 --- a/components/console/esp_console_common.c +++ b/components/console/esp_console_common.c @@ -30,6 +30,19 @@ esp_err_t esp_console_setup_prompt(const char *prompt, esp_console_repl_com_t *r snprintf(repl_com->prompt, CONSOLE_PROMPT_MAX_LEN - 1, LOG_COLOR_I "%s " LOG_RESET_COLOR, prompt_temp); /* Figure out if the terminal supports escape sequences */ + /* TODO IDF-14901: It is not appropriate to probe the current thread's console here. + * The esp_console_repl_task can open its own stdin/stdout for use. + * However, linenoiseProbe() cannot be moved to esp_console_repl_task + * to preserve user expectations. Consider the following usage pattern: + * esp_console_start_repl(repl); + * printf("!!!ready!!!"); + * Users expect that when "!!!ready!!!" is printed, the console is already available. + * If linenoiseProbe() were moved to esp_console_repl_task, race conditions + * between threads combined with usleep() calls inside linenoiseProbe() could + * change this behavior. Currently, there is already a race between threads, + * but since esp_console_repl_task does not call any sleep functions, everything + * works as users expect. + */ int probe_status = linenoiseProbe(); if (probe_status) { /* zero indicates success */ @@ -159,7 +172,6 @@ void esp_console_repl_task(void *args) { esp_console_repl_universal_t *repl_conf = (esp_console_repl_universal_t *) args; esp_console_repl_com_t *repl_com = &repl_conf->repl_com; - const int uart_channel = repl_conf->uart_channel; /* Waiting for task notify. This happens when `esp_console_start_repl()` * function is called. */ @@ -173,6 +185,16 @@ void esp_console_repl_task(void *args) /* Change standard input and output of the task if the requested UART is * NOT the default one. This block will replace stdin, stdout and stderr. */ +#if CONFIG_LIBC_PICOLIBC + // TODO IDF-14901 + if (repl_com->_stdin) { + stdin = repl_com->_stdin; + stdout = stderr = repl_com->_stdout; + } else { + linenoise_init_with_global_stdio(); + } +#else + const int uart_channel = repl_conf->uart_channel; if (uart_channel != CONFIG_ESP_CONSOLE_UART_NUM) { char path[CONSOLE_PATH_MAX_LEN] = { 0 }; snprintf(path, CONSOLE_PATH_MAX_LEN, "/dev/uart/%d", uart_channel); @@ -181,6 +203,7 @@ void esp_console_repl_task(void *args) stdout = fopen(path, "w"); stderr = stdout; } +#endif /* Disable buffering on stdin of the current task. * If the console is ran on a different UART than the default one, @@ -232,6 +255,10 @@ void esp_console_repl_task(void *args) linenoiseFree(line); } +#if CONFIG_LIBC_PICOLIBC + linenoise_close_stdio(); +#endif + if (repl_com->state_mux != NULL) { xSemaphoreGive(repl_com->state_mux); } diff --git a/components/console/esp_console_repl_chip.c b/components/console/esp_console_repl_chip.c index 912fa90a66..96132fa984 100644 --- a/components/console/esp_console_repl_chip.c +++ b/components/console/esp_console_repl_chip.c @@ -252,9 +252,42 @@ esp_err_t esp_console_new_repl_uart(const esp_console_dev_uart_config_t *dev_con goto _exit; } +#if CONFIG_LIBC_PICOLIBC // TODO IDF-14901 +#if !CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY +#define tls_stdin linenoise_stdin +#define tls_stdout linenoise_stdout +#endif + extern __thread FILE *tls_stdin; + extern __thread FILE *tls_stdout; + + // Workaround for Picolibc to use thread-local stdio streams when the console is not the default one. + // Need to set linenoise_stdin/linenoise_stdout to correct values that will be used by the esp_console_repl_task + // before esp_console_setup_prompt() call, because it uses them. After that, we can restore the original values. + if (dev_config->channel != CONFIG_ESP_CONSOLE_UART_NUM) { + char path[CONSOLE_PATH_MAX_LEN] = { 0 }; + snprintf(path, CONSOLE_PATH_MAX_LEN, "/dev/uart/%d", dev_config->channel); + uart_repl->repl_com._stdin = fopen(path, "r"); + uart_repl->repl_com._stdout = fopen(path, "w"); + } + FILE *tmp_stdin = stdin; + FILE *tmp_stdout = stdout; + if (uart_repl->repl_com._stdin) { + tls_stdin = uart_repl->repl_com._stdin; + tls_stdout = uart_repl->repl_com._stdout; + } +#endif + // setup prompt esp_console_setup_prompt(repl_config->prompt, &uart_repl->repl_com); +#if CONFIG_LIBC_PICOLIBC // TODO IDF-14901 + if (uart_repl->repl_com._stdin) { + // Restore the original values of tls_stdin and tls_stdout just in case. + tls_stdin = tmp_stdin; + tls_stdout = tmp_stdout; + } +#endif + /* Fill the structure here as it will be used directly by the created task. */ uart_repl->uart_channel = dev_config->channel; uart_repl->repl_com.state = CONSOLE_REPL_STATE_INIT; diff --git a/components/console/linenoise/linenoise.c b/components/console/linenoise/linenoise.c index 4e051cf6a8..64fcf23a12 100644 --- a/components/console/linenoise/linenoise.c +++ b/components/console/linenoise/linenoise.c @@ -124,6 +124,14 @@ #include #include #include "linenoise.h" +#if CONFIG_LIBC_PICOLIBC +#include +#endif + +#if CONFIG_LIBC_PICOLIBC && !CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY +__thread FILE *linenoise_stdin; +__thread FILE *linenoise_stdout; +#endif #define LINENOISE_DEFAULT_HISTORY_MAX_LEN 100 #define LINENOISE_DEFAULT_MAX_LINE 4096 diff --git a/components/console/private_include/console_private.h b/components/console/private_include/console_private.h index cbcca4d448..2e326fcb93 100644 --- a/components/console/private_include/console_private.h +++ b/components/console/private_include/console_private.h @@ -31,6 +31,10 @@ typedef enum { typedef struct { esp_console_repl_t repl_core; // base class +#if CONFIG_LIBC_PICOLIBC + FILE *_stdin; + FILE *_stdout; +#endif char prompt[CONSOLE_PROMPT_MAX_LEN]; // Prompt to be printed before each line repl_state_t state; SemaphoreHandle_t state_mux; diff --git a/components/console/private_include/console_stdio_private.h b/components/console/private_include/console_stdio_private.h new file mode 100644 index 0000000000..f7f4d46b17 --- /dev/null +++ b/components/console/private_include/console_stdio_private.h @@ -0,0 +1,79 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +/** + * This file provides thread-local storage for stdin/stdout streams when using + * picolibc to have thread-local stdio streams instead of global ones. + * It enables per-thread stdio redirection by: + * - Defining thread-local FILE* variables (tls_stdin, tls_stdout) + * - Redefining standard I/O macros (stdin, stdout, printf, scanf, etc.) to use TLS streams + * - Providing initialization and cleanup functions for TLS stdio streams + */ + +#pragma once +#include +#include "sdkconfig.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#if CONFIG_LIBC_PICOLIBC && !CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY +#define tls_stdin linenoise_stdin +#define tls_stdout linenoise_stdout +#endif + +#if CONFIG_LIBC_PICOLIBC +extern __thread FILE *tls_stdin; +extern __thread FILE *tls_stdout; +#endif + +static inline void linenoise_init_with_global_stdio(void) +{ +#if CONFIG_LIBC_PICOLIBC + tls_stdin = stdin; + tls_stdout = stdout; +#endif +} + +static inline void linenoise_close_stdio(void) +{ +#if CONFIG_LIBC_PICOLIBC + if (tls_stdin != stdin) { + fclose(tls_stdin); + } + if (tls_stdout != stdout) { + fclose(tls_stdout); + } +#endif +} + +#if CONFIG_LIBC_PICOLIBC +#undef stdin +#define stdin tls_stdin + +#undef stdout +#define stdout tls_stdout + +#define printf(...) fprintf(tls_stdout, __VA_ARGS__) +#define vprintf(fmt, ap) vfprintf(tls_stdout, fmt, ap) +#ifdef putchar +#undef putchar +#endif +#define putchar(c) fputc((c), tls_stdout) +#define puts(s) fputs((s), tls_stdout) + +#define scanf(...) fscanf(tls_stdin, __VA_ARGS__) +#define vscanf(fmt, ap) vfscanf(tls_stdin, fmt, ap) +#ifdef getchar +#undef getchar +#endif +#define getchar() fgetc(tls_stdin) +#define gets(buf) fgets((buf), sizeof(buf), tls_stdin) +#endif + +#ifdef __cplusplus +} +#endif diff --git a/components/console/test_apps/console/main/CMakeLists.txt b/components/console/test_apps/console/main/CMakeLists.txt index b253135b6c..0039ab47ae 100644 --- a/components/console/test_apps/console/main/CMakeLists.txt +++ b/components/console/test_apps/console/main/CMakeLists.txt @@ -1,4 +1,10 @@ +set(priv_requires unity console) + +if(NOT CONFIG_IDF_TARGET_LINUX) + list(APPEND priv_requires esp_driver_uart) +endif() + idf_component_register(SRCS "test_app_main.c" "test_console.c" INCLUDE_DIRS "." - PRIV_REQUIRES unity console + PRIV_REQUIRES ${priv_requires} WHOLE_ARCHIVE) diff --git a/components/console/test_apps/console/main/test_app_main.c b/components/console/test_apps/console/main/test_app_main.c index 2336f50da5..c475fd72d1 100644 --- a/components/console/test_apps/console/main/test_app_main.c +++ b/components/console/test_apps/console/main/test_app_main.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -8,6 +8,9 @@ #include "unity_test_runner.h" #include "unity_test_utils_memory.h" #include +#if !CONFIG_IDF_TARGET_LINUX +#include "driver/uart.h" +#endif // Some resources are lazy allocated (newlib locks) in the console code, the threshold is left for that case #define TEST_MEMORY_LEAK_THRESHOLD_DEFAULT (150) @@ -36,6 +39,15 @@ void app_main(void) struct timeval tv = { 0 }; gettimeofday(&tv, NULL); +#if !CONFIG_IDF_TARGET_LINUX + /* Preallocate UART1 memory */ + fileno(stdin); + uart_driver_install(UART_NUM_1, 256, 0, 0, NULL, 0); + FILE *f = fopen("/dev/uart/1", "rw"); + fclose(f); + uart_driver_delete(UART_NUM_1); +#endif + printf("Running console component tests\n"); unity_run_menu(); } diff --git a/components/console/test_apps/console/main/test_console.c b/components/console/test_apps/console/main/test_console.c index 71e96f12ea..bd6bc37168 100644 --- a/components/console/test_apps/console/main/test_console.c +++ b/components/console/test_apps/console/main/test_console.c @@ -13,6 +13,9 @@ #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/semphr.h" +#if !CONFIG_IDF_TARGET_LINUX +#include "driver/uart.h" +#endif /* * NOTE: Most of these unit tests DO NOT work standalone. They require pytest to control @@ -404,3 +407,33 @@ TEST_CASE("esp console re-register commands", "[console][ignore]") TEST_ESP_OK(esp_console_start_repl(s_repl)); vTaskDelay(pdMS_TO_TICKS(5000)); } + +#if !CONFIG_IDF_TARGET_LINUX + +TEST_CASE("esp console repl custom_uart test", "[console][ignore]") +{ + set_leak_threshold(248); + + printf("Running repl on UART1\n"); + + esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT(); + esp_console_dev_uart_config_t uart_config = ESP_CONSOLE_DEV_UART_CONFIG_DEFAULT(); + uart_config.channel = UART_NUM_1; // Set UART1 for repl task + + TEST_ESP_OK(esp_console_new_repl_uart(&uart_config, &repl_config, &s_repl)); + + TEST_ESP_OK(esp_console_cmd_register(&s_quit_cmd)); + + TEST_ESP_OK(esp_console_start_repl(s_repl)); + + /* Wait a little for repl console initialization on UART1 */ + vTaskDelay(pdMS_TO_TICKS(300)); + + TEST_ESP_OK(esp_console_stop_repl(s_repl)); + + /* Let scheduler clean task internals */ + vTaskDelay(pdMS_TO_TICKS(50)); + + printf("ByeBye\r\n"); +} +#endif // !CONFIG_IDF_TARGET_LINUX