diff --git a/components/esp_driver_uart/include/driver/esp_private/uart_vfs.h b/components/esp_driver_uart/include/driver/esp_private/uart_vfs.h index 88981632854..8d57c726050 100644 --- a/components/esp_driver_uart/include/driver/esp_private/uart_vfs.h +++ b/components/esp_driver_uart/include/driver/esp_private/uart_vfs.h @@ -50,8 +50,9 @@ esp_err_t uart_vfs_dev_port_init(const esp_console_dev_uart_config_t *config, * console backend. * * @param config Pointer to the UART VFS device configuration. + * @return ESP_OK if deinitialization completes successfully, or an error code if it fails. */ -void uart_vfs_dev_port_deinit(const esp_console_dev_uart_config_t *config); +esp_err_t uart_vfs_dev_port_deinit(const esp_console_dev_uart_config_t *config); #ifdef __cplusplus } diff --git a/components/esp_driver_uart/src/uart_vfs.c b/components/esp_driver_uart/src/uart_vfs.c index 69fef254ed5..1c2b8116d84 100644 --- a/components/esp_driver_uart/src/uart_vfs.c +++ b/components/esp_driver_uart/src/uart_vfs.c @@ -1163,6 +1163,7 @@ void uart_vfs_dev_use_driver(int uart_num) } #if CONFIG_ESP_CONSOLE_UART + esp_err_t uart_vfs_dev_port_init(const esp_console_dev_uart_config_t *config, esp_line_endings_t rx_mode, esp_line_endings_t tx_mode) @@ -1218,10 +1219,11 @@ esp_err_t uart_vfs_dev_port_init(const esp_console_dev_uart_config_t *config, return ESP_OK; } -void uart_vfs_dev_port_deinit(const esp_console_dev_uart_config_t *config) +esp_err_t uart_vfs_dev_port_deinit(const esp_console_dev_uart_config_t *config) { uart_vfs_dev_use_nonblocking(config->channel); uart_driver_delete(config->channel); + return ESP_OK; } ESP_SYSTEM_INIT_FN(init_vfs_uart, CORE, BIT(0), 110) diff --git a/components/esp_driver_usb_serial_jtag/CMakeLists.txt b/components/esp_driver_usb_serial_jtag/CMakeLists.txt index e9b99f50840..18ed900cca4 100644 --- a/components/esp_driver_usb_serial_jtag/CMakeLists.txt +++ b/components/esp_driver_usb_serial_jtag/CMakeLists.txt @@ -21,8 +21,10 @@ idf_component_register(SRCS ${srcs} LDFRAGMENTS "linker.lf" ) -if(CONFIG_VFS_SUPPORT_IO AND CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG_ENABLED) - target_link_libraries(${COMPONENT_LIB} PUBLIC idf::vfs) +if(CONFIG_VFS_SUPPORT_IO) + if(CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG_ENABLED) + target_link_libraries(${COMPONENT_LIB} PUBLIC idf::vfs) + endif() target_sources(${COMPONENT_LIB} PRIVATE "src/usb_serial_jtag_vfs.c") target_link_libraries(${COMPONENT_LIB} INTERFACE "-u usb_serial_jtag_vfs_include_dev_init") endif() diff --git a/components/esp_driver_usb_serial_jtag/include/driver/esp_private/usb_serial_jtag_vfs.h b/components/esp_driver_usb_serial_jtag/include/driver/esp_private/usb_serial_jtag_vfs.h index 3588c158607..39a8a37eb4c 100644 --- a/components/esp_driver_usb_serial_jtag/include/driver/esp_private/usb_serial_jtag_vfs.h +++ b/components/esp_driver_usb_serial_jtag/include/driver/esp_private/usb_serial_jtag_vfs.h @@ -51,8 +51,9 @@ esp_err_t usb_serial_jtag_vfs_dev_port_init(const esp_console_dev_usb_serial_jta * console backend. * * @param config Pointer to the USB Serial JTAG VFS device configuration. + * @return ESP_OK if the driver was successfully uninstalled, or an error otherwise. */ -void usb_serial_jtag_vfs_dev_port_deinit(const esp_console_dev_usb_serial_jtag_config_t *config); +esp_err_t usb_serial_jtag_vfs_dev_port_deinit(const esp_console_dev_usb_serial_jtag_config_t *config); #ifdef __cplusplus } diff --git a/components/esp_driver_usb_serial_jtag/src/usb_serial_jtag_vfs.c b/components/esp_driver_usb_serial_jtag/src/usb_serial_jtag_vfs.c index ba3a312084e..78e3124a840 100644 --- a/components/esp_driver_usb_serial_jtag/src/usb_serial_jtag_vfs.c +++ b/components/esp_driver_usb_serial_jtag/src/usb_serial_jtag_vfs.c @@ -704,11 +704,12 @@ esp_err_t usb_serial_jtag_vfs_dev_port_init(const esp_console_dev_usb_serial_jta return ESP_OK; } -void usb_serial_jtag_vfs_dev_port_deinit(const esp_console_dev_usb_serial_jtag_config_t *config) +esp_err_t usb_serial_jtag_vfs_dev_port_deinit(const esp_console_dev_usb_serial_jtag_config_t *config) { (void)config; usb_serial_jtag_vfs_use_nonblocking(); usb_serial_jtag_driver_uninstall(); + return ESP_OK; } #endif @@ -716,8 +717,7 @@ void usb_serial_jtag_vfs_dev_port_deinit(const esp_console_dev_usb_serial_jtag_c #if CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG ESP_SYSTEM_INIT_FN(init_vfs_usj_sec, CORE, BIT(0), 112) { - // "/dev/seccondary_usb_serial_jtag" unfortunately is too long for vfs - esp_vfs_register_fs("/dev/secondary", &s_vfs_jtag, ESP_VFS_FLAG_STATIC | ESP_VFS_FLAG_CONTEXT_PTR, NULL); + esp_vfs_register_fs("/dev/usj", &s_vfs_jtag, ESP_VFS_FLAG_STATIC | ESP_VFS_FLAG_CONTEXT_PTR, NULL); return ESP_OK; } #endif diff --git a/components/esp_stdio/CMakeLists.txt b/components/esp_stdio/CMakeLists.txt index e3d15f06693..2870729c2aa 100644 --- a/components/esp_stdio/CMakeLists.txt +++ b/components/esp_stdio/CMakeLists.txt @@ -9,12 +9,18 @@ if(non_os_build) endif() set(srcs) set(includes) +set(priv_includes) if(${target} STREQUAL "linux") list(APPEND srcs "stdio_port.c" "linux/esp_stdio_linux.c") list(APPEND includes "include" "linux/include") + + if(CONFIG_VFS_SUPPORT_IO) + list(APPEND srcs "stdio_vfs.c" "stdio_mux_api.c") + list(APPEND priv_includes "private_include") + endif() else() list(APPEND srcs "stdio_port.c" "stdio_simple.c" @@ -22,17 +28,24 @@ else() list(APPEND includes "include") if(CONFIG_VFS_SUPPORT_IO) - list(APPEND srcs "stdio_vfs.c") + list(APPEND srcs "stdio_vfs.c" "stdio_mux_api.c") + list(APPEND priv_includes "private_include") endif() endif() idf_component_register(SRCS ${srcs} - INCLUDE_DIRS ${includes}) + INCLUDE_DIRS ${includes} + PRIV_INCLUDE_DIRS ${priv_includes} + REQUIRES vfs) + +if(CONFIG_VFS_SUPPORT_IO) + # The public header esp_stdio.h includes VFS types when VFS is enabled. + # Make sure vfs include dirs are available on all targets. + idf_component_optional_requires(PUBLIC vfs) +endif() if(CONFIG_VFS_SUPPORT_IO AND NOT ${target} STREQUAL "linux") if(IDF_BUILD_V2) - idf_component_include(vfs) - if(CONFIG_ESP_CONSOLE_UART) idf_component_include(esp_driver_uart) target_link_libraries(${COMPONENT_TARGET} PRIVATE @@ -65,9 +78,8 @@ if(CONFIG_VFS_SUPPORT_IO AND NOT ${target} STREQUAL "linux") endif() target_link_libraries(${COMPONENT_LIB} PRIVATE idf::vfs) - # Make sure esp_stdio_register gets called at startup stage. - # The referenced symbol is defined in stdio_vfs.c, which is only added to - # the source list above on non-Linux targets; the force-undef is gated on - # the same condition for the link line to remain resolvable. - target_link_libraries(${COMPONENT_LIB} INTERFACE "-u esp_vfs_include_console_register") + # Make sure esp_stdio_register gets called at startup stage + if(NOT ${target} STREQUAL "linux") + target_link_libraries(${COMPONENT_LIB} INTERFACE "-u esp_vfs_include_console_register") + endif() endif() diff --git a/components/esp_stdio/include/esp_stdio.h b/components/esp_stdio/include/esp_stdio.h index c980fee7071..f5f1834a094 100644 --- a/components/esp_stdio/include/esp_stdio.h +++ b/components/esp_stdio/include/esp_stdio.h @@ -7,6 +7,7 @@ #include "esp_err.h" #include "esp_stdio_cli_config.h" +#include "esp_vfs_common.h" #ifdef __cplusplus extern "C" { @@ -14,15 +15,107 @@ extern "C" { #define ESP_VFS_DEV_CONSOLE "/dev/console" +#if CONFIG_VFS_SUPPORT_IO + +#include "esp_vfs_common.h" +#include "esp_vfs_ops.h" + /** - * @brief add uart/usb_serial_jtag/usb_otg_acmcdc virtual filesystem driver + * @brief Configuration for registering a console I/O backend in the mux. * - * This function is called from startup code to enable serial output + * The caller must keep the ops table, context, and path string valid until + * esp_stdio_unregister_io() is called. + */ +typedef struct { + const esp_vfs_fs_ops_t *vfs_ops; /*!< VFS operations table (must not be NULL) */ + void *vfs_ctx; /*!< Context pointer forwarded to every VFS callback */ + const char *path; /*!< Path used for open() inside the driver (e.g. "/0", "/") */ +} esp_stdio_io_config_t; + +/** @brief Opaque handle representing a registered console I/O backend. */ +typedef struct esp_stdio_entry *esp_stdio_handle_t; + +/** + * @brief Register the default console VFS backends from Kconfig. + * + * Called from startup code. Sets up the primary and any Kconfig-selected + * auxiliary sinks, and mounts them under /dev/console. */ esp_err_t esp_stdio_register(void); /** - * @brief Install and enable the default stdio driver. + * @brief Register a VFS backend as a write-only auxiliary sink in the console mux. + * + * The backend starts receiving fan-out writes immediately (once /dev/console + * is open). To make it the active read+write primary, pass the returned + * handle to esp_stdio_push_primary(). + * + * The caller is responsible for initialising the driver before calling this + * function. The ops table, context, and path string must remain valid until + * esp_stdio_unregister_io() is called. + * + * @param config Pointer to the I/O configuration (must not be NULL; vfs_ops + * and path must not be NULL). + * @param out_handle Receives the opaque handle on success (must not be NULL). + * @return ESP_OK on success + * ESP_ERR_INVALID_ARG if any required pointer is NULL + * ESP_ERR_NO_MEM if the entry pool is exhausted + */ +esp_err_t esp_stdio_register_io(const esp_stdio_io_config_t *config, + esp_stdio_handle_t *out_handle); + +/** + * @brief Push an existing handle onto the primary stack. + * + * Removes the entry from the auxiliary list and makes it the active + * read+write backend. The previous primary is suspended on a stack and + * resumes when esp_stdio_pop_primary() is called. + * + * @param handle A valid handle returned by esp_stdio_register_io(). + * @return ESP_OK on success + * ESP_ERR_INVALID_ARG if handle is NULL or not currently registered as auxiliary + * ESP_ERR_NO_MEM if the primary stack is full + */ +esp_err_t esp_stdio_push_primary(esp_stdio_handle_t handle); + +/** + * @brief Remove a primary from the stack. + * + * The removed entry is returned to the auxiliary list. When it was the active + * (top) primary, the next-most-recent primary becomes active; the + * system-registered primary at the base of the stack always remains as the + * ultimate fallback and can never be removed. + * + * @param handle The primary to remove. Pass NULL to remove the current active + * (top) primary. Pass a specific handle to remove it from + * wherever it sits in the stack (owner-keyed removal, which lets + * independent owners release their own primary without disturbing + * a more-recently-pushed one). + * @return ESP_OK on success + * ESP_ERR_INVALID_STATE if there is no user-pushed primary to remove + * (NULL), or if @p handle is not currently on the + * primary stack + */ +esp_err_t esp_stdio_pop_primary(esp_stdio_handle_t handle); + +/** + * @brief Unregister a backend from the console mux entirely. + * + * The handle's backend fd is closed if it was open. If the handle is + * currently the active primary it is implicitly popped first (the previous + * primary is restored). If it is buried in the primary stack it is removed + * from wherever it sits. After this call the handle is invalid. + * + * @param handle A valid handle returned by esp_stdio_register_io(). + * @return ESP_OK on success + * ESP_ERR_INVALID_ARG if handle is NULL + */ +esp_err_t esp_stdio_unregister_io(esp_stdio_handle_t handle); + +#endif // CONFIG_VFS_SUPPORT_IO + +/** + * @brief Install and enable the stdio driver. * * Initializes the selected console backend and registers it as the active * input and output stream source. After calling this function, standard I/O @@ -32,6 +125,10 @@ esp_err_t esp_stdio_register(void); * - RX: CR (terminals send CR when Enter is pressed) * - TX: CRLF (move cursor to beginning of next line on newline) * + * If a user has taken over the primary console with esp_stdio_push_primary(), + * this function is a no-op (the user backend is assumed to be already + * initialised by the caller). + * * @return ESP_OK if the driver is successfully installed, or an appropriate error code otherwise. */ esp_err_t esp_stdio_install_io_driver(void); @@ -42,8 +139,11 @@ esp_err_t esp_stdio_install_io_driver(void); * Restores driver state and detaches the console backend from the standard I/O * streams. Call this when shutting down or when replacing the current console * driver with a different one. + * + * If a user has taken over the primary console with esp_stdio_push_primary(), + * this function is a no-op. */ -void esp_stdio_uninstall_io_driver(void); +esp_err_t esp_stdio_uninstall_io_driver(void); #ifdef __cplusplus } diff --git a/components/esp_stdio/linux/esp_stdio_linux.c b/components/esp_stdio/linux/esp_stdio_linux.c index 37993a8139f..bbc7ab64ad3 100644 --- a/components/esp_stdio/linux/esp_stdio_linux.c +++ b/components/esp_stdio/linux/esp_stdio_linux.c @@ -21,9 +21,10 @@ static void disable_raw_mode(void) assert(tcsetattr(STDIN_FILENO, TCSAFLUSH, &s_orig_termios) == 0); } -void linux_vfs_dev_port_deinit(linux_port_config_t *config) +esp_err_t linux_vfs_dev_port_deinit(linux_port_config_t *config) { (void)config; + return ESP_OK; } esp_err_t linux_vfs_dev_port_init(linux_port_config_t *config) diff --git a/components/esp_stdio/linux/include/esp_stdio_linux.h b/components/esp_stdio/linux/include/esp_stdio_linux.h index 356663f6758..7279d1b49ee 100644 --- a/components/esp_stdio/linux/include/esp_stdio_linux.h +++ b/components/esp_stdio/linux/include/esp_stdio_linux.h @@ -43,8 +43,9 @@ esp_err_t linux_vfs_dev_port_init(linux_port_config_t *config); * switching to another console interface. * * @param config Pointer to the Linux console port configuration. + * @return ESP_OK if the driver was successfully uninstalled, or an error otherwise. */ -void linux_vfs_dev_port_deinit(linux_port_config_t *config); +esp_err_t linux_vfs_dev_port_deinit(linux_port_config_t *config); #ifdef __cplusplus } diff --git a/components/esp_stdio/private_include/esp_stdio_private.h b/components/esp_stdio/private_include/esp_stdio_private.h new file mode 100644 index 00000000000..755c44967f9 --- /dev/null +++ b/components/esp_stdio/private_include/esp_stdio_private.h @@ -0,0 +1,60 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include +#include +#include "sdkconfig.h" +#include "esp_vfs.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#if CONFIG_VFS_SUPPORT_IO + +#define STDIO_MAX_ENTRIES CONFIG_ESP_STDIO_MAX_VFS_ENTRIES + +typedef struct esp_stdio_entry { + const esp_vfs_fs_ops_t *ops; + void *vfs_ctx; + const char *path; + int fd; /* backend fd; -1 when closed */ + bool in_use; /* false = slot available */ +} esp_stdio_entry_t; + +/* + * Shared mux state — defined in stdio_vfs.c, used by stdio_mux_api.c. + * + * s_primary_stack[0] is the immovable system-registered sentinel. + * s_primary_stack[s_primary_index] is the active primary. + * Auxiliaries are any in-use pool entries not on the stack. + */ +extern esp_stdio_entry_t s_entry_pool[STDIO_MAX_ENTRIES]; +extern esp_stdio_entry_t *s_primary_stack[STDIO_MAX_ENTRIES]; +extern int s_primary_index; +extern int s_open_count; +extern _lock_t s_lock; + +/* Low-level helpers — defined in stdio_vfs.c, called from stdio_mux_api.c. */ +void entry_open(esp_stdio_entry_t *e, int flags); +void entry_close(esp_stdio_entry_t *e); + +/** + * @brief Return true if the active primary is NOT the system-registered backend. + * + * Used by stdio_port.c to decide whether to skip hardware init/deinit + * (the caller of esp_stdio_push_primary() is responsible for their own driver + * lifecycle). + */ +bool esp_stdio_has_user_primary(void); + +#endif // CONFIG_VFS_SUPPORT_IO + +#ifdef __cplusplus +} +#endif diff --git a/components/esp_stdio/stdio_mux_api.c b/components/esp_stdio/stdio_mux_api.c new file mode 100644 index 00000000000..e4d530815f0 --- /dev/null +++ b/components/esp_stdio/stdio_mux_api.c @@ -0,0 +1,198 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* + * Public mux API: esp_stdio_register_io / unregister_io / push_primary / + * pop_primary / has_user_primary. + * + * This file is intentionally separate from stdio_vfs.c so that it is only + * linked into a binary when the application actually calls one of these + * functions. stdio_vfs.c is always pulled in (via the + * -u esp_vfs_include_console_register linker hook) but this file is not, so + * apps that never use the custom-IO API do not pay for the code here. + */ + +#include "sdkconfig.h" + +#if CONFIG_VFS_SUPPORT_IO + +#include +#include +#include "esp_err.h" +#include "esp_log.h" +#include "esp_stdio.h" +#include "esp_stdio_private.h" + +static const char *TAG = "esp_stdio"; + +/* True if the entry is currently on the primary stack (at any depth). */ +static bool on_stack(const esp_stdio_entry_t *e) +{ + for (int i = 0; i <= s_primary_index; i++) { + if (s_primary_stack[i] == e) { + return true; + } + } + return false; +} + +/* Remove an entry from the primary stack at any depth (does not close fd). + * Returns true if found. */ +static bool stack_remove(esp_stdio_entry_t *e) +{ + for (int i = 0; i <= s_primary_index; i++) { + if (s_primary_stack[i] == e) { + /* Shift everything above it down so the entry below becomes the + * new active primary. */ + memmove(&s_primary_stack[i], &s_primary_stack[i + 1], + (s_primary_index - i) * sizeof(s_primary_stack[0])); + s_primary_stack[s_primary_index--] = NULL; + return true; + } + } + return false; +} + +bool esp_stdio_has_user_primary(void) +{ + /* The system-registered primary is always at the base of the stack, so a + * user primary is active exactly when something has been pushed on top. */ + return s_primary_index > 0; +} + +esp_err_t esp_stdio_register_io(const esp_stdio_io_config_t *config, + esp_stdio_handle_t *out_handle) +{ + if (!config || !config->vfs_ops || !config->path || !out_handle) { + return ESP_ERR_INVALID_ARG; + } + + _lock_acquire(&s_lock); + + esp_stdio_entry_t *e = NULL; + for (int i = 0; i < STDIO_MAX_ENTRIES; i++) { + if (!s_entry_pool[i].in_use) { + e = &s_entry_pool[i]; + break; + } + } + if (!e) { + _lock_release(&s_lock); + ESP_EARLY_LOGE(TAG, "No space left for stdio registration"); + return ESP_ERR_NO_MEM; + } + + *e = (esp_stdio_entry_t) { + .ops = config->vfs_ops, + .vfs_ctx = config->vfs_ctx, + .path = config->path, + .fd = -1, + .in_use = true, + }; + + /* A freshly registered entry is an auxiliary. If /dev/console is already + * open, open it now so it starts receiving the write/fsync fan-out. */ + if (s_open_count > 0) { + entry_open(e, O_WRONLY); + } + + _lock_release(&s_lock); + *out_handle = e; + return ESP_OK; +} + +esp_err_t esp_stdio_unregister_io(esp_stdio_handle_t handle) +{ + if (!handle) { + return ESP_ERR_INVALID_ARG; + } + + _lock_acquire(&s_lock); + + if (!handle->in_use) { + _lock_release(&s_lock); + return ESP_ERR_INVALID_ARG; + } + + /* If it is on the primary stack at any depth, remove it so the previous + * primary takes over. Auxiliaries are not on the stack: no-op for them. */ + stack_remove(handle); + + entry_close(handle); + handle->in_use = false; + handle->ops = NULL; + + _lock_release(&s_lock); + return ESP_OK; +} + +esp_err_t esp_stdio_push_primary(esp_stdio_handle_t handle) +{ + if (!handle) { + return ESP_ERR_INVALID_ARG; + } + + _lock_acquire(&s_lock); + + if (!handle->in_use || on_stack(handle)) { + _lock_release(&s_lock); + return ESP_ERR_INVALID_ARG; + } + + if (s_primary_index + 1 >= STDIO_MAX_ENTRIES) { + _lock_release(&s_lock); + return ESP_ERR_NO_MEM; + } + + /* The previously active primary stays suspended on the stack (still open, + * so it receives the write/fsync fan-out). Open the new primary if the + * console is already open. + * + * If the entry was previously used as an auxiliary it will already have + * an fd opened O_WRONLY. entry_open() would return early in that case + * without upgrading to O_RDWR, causing reads from the new primary to + * fail. Close first so the re-open below picks up the correct flags. */ + if (s_open_count > 0) { + if (handle->fd >= 0) { + entry_close(handle); + } + entry_open(handle, O_RDWR); + } + + s_primary_stack[++s_primary_index] = handle; + + _lock_release(&s_lock); + return ESP_OK; +} + +esp_err_t esp_stdio_pop_primary(esp_stdio_handle_t handle) +{ + _lock_acquire(&s_lock); + + /* s_primary_stack[0] is the system-registered sentinel: it can never be + * removed. Its handle is never exposed to callers, but guard against it + * explicitly. */ + if (handle == NULL) { + /* Remove the active (top) primary. */ + if (s_primary_index <= 0) { + _lock_release(&s_lock); + return ESP_ERR_INVALID_STATE; + } + handle = s_primary_stack[s_primary_index]; + } else if (handle == s_primary_stack[0] || !on_stack(handle)) { + _lock_release(&s_lock); + return ESP_ERR_INVALID_STATE; + } + + /* Remove from the stack. The entry stays in_use and open, reverting to + * auxiliary status. No fd juggling required. */ + stack_remove(handle); + + _lock_release(&s_lock); + return ESP_OK; +} + +#endif // CONFIG_VFS_SUPPORT_IO diff --git a/components/esp_stdio/stdio_port.c b/components/esp_stdio/stdio_port.c index 4b22f429b39..e3269254cd1 100644 --- a/components/esp_stdio/stdio_port.c +++ b/components/esp_stdio/stdio_port.c @@ -4,6 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +#include +#include #include "sdkconfig.h" #include "esp_stdio.h" @@ -30,8 +32,19 @@ #endif // CONFIG_VFS_SUPPORT_IO +#if CONFIG_VFS_SUPPORT_IO +#include "esp_stdio_private.h" +#endif + esp_err_t esp_stdio_install_io_driver(void) { +#if CONFIG_VFS_SUPPORT_IO + /* If a user primary is active, it was initialised by the caller. Skip HW init. */ + if (esp_stdio_has_user_primary()) { + return ESP_OK; + } + + /* No user primary — proceed with Kconfig default HW init */ esp_err_t ret = ESP_FAIL; #if CONFIG_IDF_TARGET_LINUX @@ -39,7 +52,7 @@ esp_err_t esp_stdio_install_io_driver(void) ret = linux_vfs_dev_port_init(&config); #elif CONFIG_VFS_SUPPORT_IO - /* - set rx_mode to ESP_LINE_ENDINGS_CRLF as minicom, screen, idf_monitor + /* - set rx_mode to ESP_LINE_ENDINGS_CR as minicom, screen, idf_monitor * send CR when ENTER key is pressed. * - set tx_mode to move the caret to the beginning of the next line on '\n' */ @@ -59,21 +72,29 @@ esp_err_t esp_stdio_install_io_driver(void) return ret; } -void esp_stdio_uninstall_io_driver(void) +esp_err_t esp_stdio_uninstall_io_driver(void) { +#if CONFIG_VFS_SUPPORT_IO + /* If a user primary is registered, deinit is the caller's responsibility. */ + if (esp_stdio_has_user_primary()) { + return ESP_OK; + } + + esp_err_t ret = ESP_FAIL; #if CONFIG_IDF_TARGET_LINUX linux_port_config_t config = ESP_CONSOLE_DEV_LINUX_CONFIG_DEFAULT(); - linux_vfs_dev_port_deinit(&config); + ret = linux_vfs_dev_port_deinit(&config); #elif CONFIG_VFS_SUPPORT_IO #if CONFIG_ESP_CONSOLE_UART esp_console_dev_uart_config_t config = ESP_CONSOLE_DEV_UART_CONFIG_DEFAULT(); - uart_vfs_dev_port_deinit(&config); + ret = uart_vfs_dev_port_deinit(&config); #elif CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG esp_console_dev_usb_serial_jtag_config_t config = ESP_CONSOLE_DEV_USB_SERIAL_JTAG_CONFIG_DEFAULT(); - usb_serial_jtag_vfs_dev_port_deinit(&config); + ret = usb_serial_jtag_vfs_dev_port_deinit(&config); #elif CONFIG_ESP_CONSOLE_USB_CDC esp_console_dev_usb_cdc_config_t config = ESP_CONSOLE_DEV_CDC_CONFIG_DEFAULT(); - cdcacm_vfs_dev_port_deinit(&config); + ret = cdcacm_vfs_dev_port_deinit(&config); #endif #endif + return ret; } diff --git a/components/esp_stdio/stdio_vfs.c b/components/esp_stdio/stdio_vfs.c index fb0ce9efb56..3670816115a 100644 --- a/components/esp_stdio/stdio_vfs.c +++ b/components/esp_stdio/stdio_vfs.c @@ -16,6 +16,14 @@ #include "esp_vfs.h" #include +/** + * This file is to concentrate all the vfs(UART, USB_SERIAL_JTAG, CDCACM) console into one single file. + * Get the vfs information from their component (i.e. uart_vfs.c), + * which can help us to output some string to two different ports(i.e both through uart and usb_serial_jtag). + * Usually, we set a port as primary and another as secondary. For primary, it is used for all the features + * supported by each vfs implementation, while the secondary is only used for output. + */ + #if CONFIG_VFS_SUPPORT_IO #if CONFIG_ESP_CONSOLE_USB_CDC @@ -28,111 +36,96 @@ #include "driver/usb_serial_jtag_vfs.h" #endif // CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG_ENABLED -#if CONFIG_ESP_CONSOLE_UART +#if CONFIG_ESP_CONSOLE_UART && !CONFIG_IDF_TARGET_LINUX #include "driver/esp_private/uart_vfs.h" #include "driver/uart_vfs.h" -#endif // CONFIG_ESP_CONSOLE_UART +#endif // CONFIG_ESP_CONSOLE_UART && !CONFIG_IDF_TARGET_LINUX #include "esp_private/startup_internal.h" #include "esp_private/nullfs.h" +#include "esp_stdio_private.h" #include -#endif +#endif // CONFIG_VFS_SUPPORT_IO #define STRINGIFY(s) STRINGIFY2(s) #define STRINGIFY2(s) #s -/** - * This file is to concentrate all the vfs(UART, USB_SERIAL_JTAG, CDCACM) console into one single file. - * Get the vfs information from their component (i.e. uart_vfs.c), - * which can help us to output some string to two different ports(i.e both through uart and usb_serial_jtag). - * Usually, we set a port as primary and another as secondary. For primary, it is used for all the features supported by each vfs implementation, - * while the secondary is only used for output. - */ +/* Pool, stack and lock — declared extern in esp_stdio_private.h so that + * stdio_mux_api.c can access them without pulling in this object file. */ +esp_stdio_entry_t s_entry_pool[STDIO_MAX_ENTRIES]; +esp_stdio_entry_t *s_primary_stack[STDIO_MAX_ENTRIES]; +int s_primary_index = -1; +int s_open_count; +_lock_t s_lock; -#if CONFIG_VFS_SUPPORT_IO +/* The active primary is always the top of the stack. */ +static inline esp_stdio_entry_t *active_primary(void) +{ + assert(s_primary_index >= 0); + return s_primary_stack[s_primary_index]; +} -typedef struct { - const esp_vfs_fs_ops_t *ops; /* NULL = slot is free */ - void *vfs_ctx; - const char *path; - int fd; /* -1 if not opened (used by auxiliaries) */ -} mux_entry_t; +/* Open the backend for an entry and store the fd. + * Non-static: also called from stdio_mux_api.c. */ +void entry_open(esp_stdio_entry_t *e, int flags) +{ + if (!e->ops || !e->ops->open_p || e->fd >= 0) { + return; + } + e->fd = e->ops->open_p(e->vfs_ctx, e->path, flags, 0); +} -/* Primary backend (all ops forwarded except open + write + fsync) */ -static mux_entry_t s_primary = { .fd = -1 }; - -/* Auxiliary sinks (write-only fan-out) */ -#define STDIO_MAX_AUXILIARY (CONFIG_ESP_STDIO_MAX_VFS_ENTRIES - 1) -static mux_entry_t s_auxiliary[STDIO_MAX_AUXILIARY] = { [0 ... STDIO_MAX_AUXILIARY - 1] = { .fd = -1 } }; -static int s_open_count = 0; -static _lock_t s_lock; - -static const char *TAG = "esp_stdio"; +/* Close the backend fd for an entry. + * Non-static: also called from stdio_mux_api.c. */ +void entry_close(esp_stdio_entry_t *e) +{ + if (e->fd < 0) { + return; + } + if (e->ops && e->ops->close_p) { + e->ops->close_p(e->vfs_ctx, e->fd); + } + e->fd = -1; +} #ifdef CONFIG_VFS_SUPPORT_TERMIOS -static const mux_entry_t *get_primary_termios_entry(int fd, const esp_vfs_termios_ops_t **out_ops) +static esp_stdio_entry_t *get_primary_termios_entry(int fd, const esp_vfs_termios_ops_t **out_ops) { - const mux_entry_t *entry = &s_primary; - assert(entry); - - const esp_vfs_termios_ops_t *termios = entry->ops->termios; - if (!termios) { + esp_stdio_entry_t *entry = active_primary(); + if (!entry || !entry->ops || !entry->ops->termios) { errno = ENOSYS; return NULL; } - if (out_ops) { - *out_ops = termios; + *out_ops = entry->ops->termios; } - return entry; } #endif // CONFIG_VFS_SUPPORT_TERMIOS -static esp_err_t __attribute__((unused)) register_auxiliary(const esp_vfs_fs_ops_t *ops, void *ctx, const char *path) -{ - if (!ops || !path || !ops->write_p) { - return ESP_ERR_INVALID_ARG; - } - _lock_acquire(&s_lock); - for (size_t i = 0; i < STDIO_MAX_AUXILIARY; i++) { - if (s_auxiliary[i].ops == NULL) { - s_auxiliary[i] = (mux_entry_t) { - .ops = ops, - .vfs_ctx = ctx, - .path = path, - .fd = -1, - }; - _lock_release(&s_lock); - return ESP_OK; - } - } - _lock_release(&s_lock); - ESP_EARLY_LOGE(TAG, "Too many auxiliary sinks"); - return ESP_ERR_NO_MEM; -} - -int console_open(__attribute__((unused)) void *ctx, const char * path, int flags, int mode) +int console_open(__attribute__((unused)) void *ctx, const char *path, int flags, int mode) { (void)path; - const mux_entry_t *entry = &s_primary; - if (!entry->ops || !entry->ops->open_p) { + esp_stdio_entry_t *primary = active_primary(); + if (!primary->ops || !primary->ops->open_p) { errno = ENOSYS; return -1; } - int local_fd = entry->ops->open_p(entry->vfs_ctx, entry->path, flags, mode); + int local_fd = primary->ops->open_p(primary->vfs_ctx, primary->path, flags, mode); if (local_fd < 0) { return -1; } + primary->fd = local_fd; - /* Lazily open auxiliaries on first console open */ + /* Lazily open every other in-use entry (the auxiliaries) on first console + * open so they receive the write/fsync fan-out. */ _lock_acquire(&s_lock); if (s_open_count == 0) { - for (size_t i = 0; i < STDIO_MAX_AUXILIARY; i++) { - mux_entry_t *aux = &s_auxiliary[i]; - if (aux->ops && aux->ops->open_p && aux->fd < 0) { - aux->fd = aux->ops->open_p(aux->vfs_ctx, aux->path, O_WRONLY, 0); + for (int i = 0; i < STDIO_MAX_ENTRIES; i++) { + esp_stdio_entry_t *e = &s_entry_pool[i]; + if (e->in_use && e != primary) { + entry_open(e, O_WRONLY); } } } @@ -144,28 +137,30 @@ int console_open(__attribute__((unused)) void *ctx, const char * path, int flags int console_close(__attribute__((unused)) void *ctx, int fd) { - const mux_entry_t *entry = &s_primary; - if (!entry->ops || !entry->ops->close_p) { + esp_stdio_entry_t *primary = active_primary(); + if (!primary->ops || !primary->ops->close_p) { errno = ENOSYS; return -1; } - int ret = entry->ops->close_p(entry->vfs_ctx, fd); + /* Route to the active backend's own fd: after a push/pop/unregister the + * active primary may differ from the backend that originally produced the + * caller-visible fd, and each backend validates fds in its own namespace. */ + int ret = primary->ops->close_p(primary->vfs_ctx, primary->fd >= 0 ? primary->fd : fd); if (ret != 0) { return ret; } + primary->fd = -1; - /* Close auxiliaries when last console fd is closed */ _lock_acquire(&s_lock); if (s_open_count > 0) { s_open_count--; } if (s_open_count == 0) { - for (size_t i = 0; i < STDIO_MAX_AUXILIARY; i++) { - mux_entry_t *aux = &s_auxiliary[i]; - if (aux->ops && aux->ops->close_p && aux->fd >= 0) { - aux->ops->close_p(aux->vfs_ctx, aux->fd); - aux->fd = -1; + for (int i = 0; i < STDIO_MAX_ENTRIES; i++) { + esp_stdio_entry_t *e = &s_entry_pool[i]; + if (e->in_use && e != primary) { + entry_close(e); } } } @@ -176,120 +171,112 @@ int console_close(__attribute__((unused)) void *ctx, int fd) ssize_t console_write(__attribute__((unused)) void *ctx, int fd, const void *data, size_t size) { - const mux_entry_t *primary = &s_primary; - ssize_t ret_val = primary->ops->write_p(primary->vfs_ctx, fd, data, size); + esp_stdio_entry_t *primary = active_primary(); + ssize_t ret = primary->ops->write_p(primary->vfs_ctx, primary->fd >= 0 ? primary->fd : fd, data, size); _lock_acquire(&s_lock); - for (size_t i = 0; i < STDIO_MAX_AUXILIARY; i++) { - const mux_entry_t *entry = &s_auxiliary[i]; - if (entry->ops && entry->ops->write_p && entry->fd >= 0) { - (void) entry->ops->write_p(entry->vfs_ctx, entry->fd, data, size); + for (int i = 0; i < STDIO_MAX_ENTRIES; i++) { + esp_stdio_entry_t *e = &s_entry_pool[i]; + if (e->in_use && e != primary && e->ops && e->ops->write_p && e->fd >= 0) { + (void)e->ops->write_p(e->vfs_ctx, e->fd, data, size); } } _lock_release(&s_lock); - return ret_val; + return ret; } -int console_fstat(__attribute__((unused)) void *ctx, int fd, struct stat * st) +int console_fstat(__attribute__((unused)) void *ctx, int fd, struct stat *st) { - const mux_entry_t *entry = &s_primary; - if (!entry->ops->fstat_p) { + esp_stdio_entry_t *primary = active_primary(); + if (!primary->ops->fstat_p) { errno = ENOSYS; return -1; } - return entry->ops->fstat_p(entry->vfs_ctx, fd, st); + return primary->ops->fstat_p(primary->vfs_ctx, primary->fd >= 0 ? primary->fd : fd, st); } -ssize_t console_read(__attribute__((unused)) void *ctx, int fd, void * dst, size_t size) +ssize_t console_read(__attribute__((unused)) void *ctx, int fd, void *dst, size_t size) { - const mux_entry_t *entry = &s_primary; - if (!entry->ops->read_p) { + esp_stdio_entry_t *primary = active_primary(); + if (!primary->ops->read_p) { errno = ENOSYS; return -1; } - return entry->ops->read_p(entry->vfs_ctx, fd, dst, size); + return primary->ops->read_p(primary->vfs_ctx, primary->fd >= 0 ? primary->fd : fd, dst, size); } int console_fcntl(__attribute__((unused)) void *ctx, int fd, int cmd, int arg) { - const mux_entry_t *entry = &s_primary; - if (!entry->ops->fcntl_p) { + esp_stdio_entry_t *primary = active_primary(); + if (!primary->ops->fcntl_p) { errno = ENOSYS; return -1; } - return entry->ops->fcntl_p(entry->vfs_ctx, fd, cmd, arg); + return primary->ops->fcntl_p(primary->vfs_ctx, primary->fd >= 0 ? primary->fd : fd, cmd, arg); } int console_fsync(__attribute__((unused)) void *ctx, int fd) { - const mux_entry_t *primary = &s_primary; + esp_stdio_entry_t *primary = active_primary(); if (!primary->ops->fsync_p) { errno = ENOSYS; return -1; } - - int ret_val = primary->ops->fsync_p(primary->vfs_ctx, fd); + int ret = primary->ops->fsync_p(primary->vfs_ctx, primary->fd >= 0 ? primary->fd : fd); _lock_acquire(&s_lock); - for (size_t i = 0; i < STDIO_MAX_AUXILIARY; i++) { - const mux_entry_t *entry = &s_auxiliary[i]; - if (entry->ops && entry->ops->fsync_p && entry->fd >= 0) { - (void) entry->ops->fsync_p(entry->vfs_ctx, entry->fd); + for (int i = 0; i < STDIO_MAX_ENTRIES; i++) { + esp_stdio_entry_t *e = &s_entry_pool[i]; + if (e->in_use && e != primary && e->ops && e->ops->fsync_p && e->fd >= 0) { + (void)e->ops->fsync_p(e->vfs_ctx, e->fd); } } _lock_release(&s_lock); - return ret_val; + return ret; } #ifdef CONFIG_VFS_SUPPORT_DIR int console_access(__attribute__((unused)) void *ctx, const char *path, int amode) { - const mux_entry_t *entry = &s_primary; - assert(entry); - if (!entry->ops->dir || !entry->ops->dir->access_p) { + esp_stdio_entry_t *primary = active_primary(); + if (!primary->ops->dir || !primary->ops->dir->access_p) { errno = ENOSYS; return -1; } - (void) path; - return entry->ops->dir->access_p(entry->vfs_ctx, entry->path, amode); + (void)path; + return primary->ops->dir->access_p(primary->vfs_ctx, primary->path, amode); } #endif // CONFIG_VFS_SUPPORT_DIR #ifdef CONFIG_VFS_SUPPORT_SELECT - static esp_err_t console_start_select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, esp_vfs_select_sem_t select_sem, void **end_select_args) { - const mux_entry_t *entry = &s_primary; - if (!entry->ops || !entry->ops->select || !entry->ops->select->start_select) { + const esp_vfs_fs_ops_t *ops = active_primary()->ops; + if (!ops || !ops->select || !ops->select->start_select) { return ESP_ERR_NOT_SUPPORTED; } - return entry->ops->select->start_select(nfds, readfds, writefds, exceptfds, select_sem, end_select_args); + return ops->select->start_select(nfds, readfds, writefds, exceptfds, select_sem, end_select_args); } static esp_err_t console_end_select(void *end_select_args) { - const mux_entry_t *entry = &s_primary; - if (!entry->ops || !entry->ops->select || !entry->ops->select->end_select) { + const esp_vfs_fs_ops_t *ops = active_primary()->ops; + if (!ops || !ops->select || !ops->select->end_select) { return ESP_ERR_NOT_SUPPORTED; } - return entry->ops->select->end_select(end_select_args); + return ops->select->end_select(end_select_args); } - #endif // CONFIG_VFS_SUPPORT_SELECT #ifdef CONFIG_VFS_SUPPORT_TERMIOS - int console_tcsetattr(__attribute__((unused)) void *ctx, int fd, int optional_actions, const struct termios *p) { const esp_vfs_termios_ops_t *termios = NULL; - const mux_entry_t *entry = get_primary_termios_entry(fd, &termios); - if (!entry) { - return -1; - } - if (!termios->tcsetattr_p) { + esp_stdio_entry_t *entry = get_primary_termios_entry(fd, &termios); + if (!entry || !termios->tcsetattr_p) { errno = ENOSYS; return -1; } @@ -299,11 +286,8 @@ int console_tcsetattr(__attribute__((unused)) void *ctx, int fd, int optional_ac int console_tcgetattr(__attribute__((unused)) void *ctx, int fd, struct termios *p) { const esp_vfs_termios_ops_t *termios = NULL; - const mux_entry_t *entry = get_primary_termios_entry(fd, &termios); - if (!entry) { - return -1; - } - if (!termios->tcgetattr_p) { + esp_stdio_entry_t *entry = get_primary_termios_entry(fd, &termios); + if (!entry || !termios->tcgetattr_p) { errno = ENOSYS; return -1; } @@ -313,11 +297,8 @@ int console_tcgetattr(__attribute__((unused)) void *ctx, int fd, struct termios int console_tcdrain(__attribute__((unused)) void *ctx, int fd) { const esp_vfs_termios_ops_t *termios = NULL; - const mux_entry_t *entry = get_primary_termios_entry(fd, &termios); - if (!entry) { - return -1; - } - if (!termios->tcdrain_p) { + esp_stdio_entry_t *entry = get_primary_termios_entry(fd, &termios); + if (!entry || !termios->tcdrain_p) { errno = ENOSYS; return -1; } @@ -327,11 +308,8 @@ int console_tcdrain(__attribute__((unused)) void *ctx, int fd) int console_tcflush(__attribute__((unused)) void *ctx, int fd, int select) { const esp_vfs_termios_ops_t *termios = NULL; - const mux_entry_t *entry = get_primary_termios_entry(fd, &termios); - if (!entry) { - return -1; - } - if (!termios->tcflush_p) { + esp_stdio_entry_t *entry = get_primary_termios_entry(fd, &termios); + if (!entry || !termios->tcflush_p) { errno = ENOSYS; return -1; } @@ -369,69 +347,61 @@ static const esp_vfs_fs_ops_t s_vfs_console = { .read_p = &console_read, .fcntl_p = &console_fcntl, .fsync_p = &console_fsync, - #ifdef CONFIG_VFS_SUPPORT_DIR .dir = &s_vfs_console_dir, -#endif // CONFIG_VFS_SUPPORT_DIR - +#endif #ifdef CONFIG_VFS_SUPPORT_SELECT .select = &s_vfs_console_select, -#endif // CONFIG_VFS_SUPPORT_SELECT - +#endif #ifdef CONFIG_VFS_SUPPORT_TERMIOS .termios = &s_vfs_console_termios, -#endif // CONFIG_VFS_SUPPORT_TERMIOS +#endif }; -esp_err_t esp_stdio_register(void) +static esp_err_t esp_stdio_register(void) { _lock_init(&s_lock); -// Primary vfs part. -#if CONFIG_ESP_CONSOLE_UART - s_primary = (mux_entry_t) { - .ops = esp_vfs_uart_get_vfs(), + /* Directly initialise the system primary at pool slot 0 and push it as the + * immovable sentinel at stack index 0. We bypass the public API here to + * avoid a link-time dependency on stdio_mux_api.c so that binaries which + * never call the mux API don't pay for it. */ + esp_stdio_entry_t *system_primary = &s_entry_pool[0]; + *system_primary = (esp_stdio_entry_t) { .vfs_ctx = NULL, - .path = "/" STRINGIFY(CONFIG_ESP_CONSOLE_UART_NUM), + .path = "/", .fd = -1, + .in_use = true, }; + +#if CONFIG_ESP_CONSOLE_UART && !CONFIG_IDF_TARGET_LINUX + /* no need to check vfs_ops for NULL: there is no config in which it can be NULL */ + system_primary->ops = esp_vfs_uart_get_vfs(); + system_primary->path = "/" STRINGIFY(CONFIG_ESP_CONSOLE_UART_NUM); #elif CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG - s_primary = (mux_entry_t) { + system_primary->ops = esp_vfs_usb_serial_jtag_get_vfs(); +#elif CONFIG_ESP_CONSOLE_USB_CDC + system_primary->ops = esp_vfs_cdcacm_get_vfs(); +#else + system_primary->ops = esp_vfs_null_get_vfs(); +#endif + + s_primary_stack[0] = system_primary; + s_primary_index = 0; + +#if CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG + esp_stdio_entry_t *secondary = &s_entry_pool[1]; + *secondary = (esp_stdio_entry_t) { .ops = esp_vfs_usb_serial_jtag_get_vfs(), .vfs_ctx = NULL, .path = "/", .fd = -1, - }; -#elif CONFIG_ESP_CONSOLE_USB_CDC - s_primary = (mux_entry_t) { - .ops = esp_vfs_cdcacm_get_vfs(), - .vfs_ctx = NULL, - .path = "/", - .fd = -1, - }; -#else - s_primary = (mux_entry_t) { - .ops = esp_vfs_null_get_vfs(), - .vfs_ctx = NULL, - .path = "/", - .fd = -1, + .in_use = true, }; #endif - if (!s_primary.ops) { - ESP_EARLY_LOGE(TAG, "No primary console backend available"); - return ESP_FAIL; - } - -// Auxiliary sinks (write-only fan-out). -#if CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG - esp_err_t err = register_auxiliary(esp_vfs_usb_serial_jtag_get_vfs(), NULL, "/"); - if (err != ESP_OK) { - return err; - } -#endif - - return esp_vfs_register_fs(ESP_VFS_DEV_CONSOLE, &s_vfs_console, ESP_VFS_FLAG_STATIC | ESP_VFS_FLAG_CONTEXT_PTR, NULL); + return esp_vfs_register_fs(ESP_VFS_DEV_CONSOLE, &s_vfs_console, + ESP_VFS_FLAG_STATIC | ESP_VFS_FLAG_CONTEXT_PTR, NULL); } ESP_SYSTEM_INIT_FN(init_vfs_console, CORE, BIT(0), 119) @@ -439,8 +409,6 @@ ESP_SYSTEM_INIT_FN(init_vfs_console, CORE, BIT(0), 119) return esp_stdio_register(); } -#endif // CONFIG_VFS_SUPPORT_IO - void esp_vfs_include_console_register(void) { // Linker hook function, exists to make the linker examine this file diff --git a/components/esp_stdio/test_apps/.build-test-rules.yml b/components/esp_stdio/test_apps/.build-test-rules.yml index 8fcdfd8a335..f4fc7047f0f 100644 --- a/components/esp_stdio/test_apps/.build-test-rules.yml +++ b/components/esp_stdio/test_apps/.build-test-rules.yml @@ -1,4 +1,11 @@ # Documentation: .gitlab/ci/README.md#manifest-file-to-control-the-buildtest-apps +components/esp_stdio/test_apps/custom_io: + enable: + - if: IDF_TARGET == "linux" + depends_components: + - esp_stdio + - vfs + components/esp_stdio/test_apps/stdio: disable: - if: CONFIG_NAME == "serial_jtag_only" and SOC_USB_SERIAL_JTAG_SUPPORTED != 1 diff --git a/components/esp_stdio/test_apps/custom_io/CMakeLists.txt b/components/esp_stdio/test_apps/custom_io/CMakeLists.txt new file mode 100644 index 00000000000..834314c252a --- /dev/null +++ b/components/esp_stdio/test_apps/custom_io/CMakeLists.txt @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +set(COMPONENTS main) +project(test_esp_stdio_custom_io) diff --git a/components/esp_stdio/test_apps/custom_io/README.md b/components/esp_stdio/test_apps/custom_io/README.md new file mode 100644 index 00000000000..6cf23136df5 --- /dev/null +++ b/components/esp_stdio/test_apps/custom_io/README.md @@ -0,0 +1,24 @@ +| Supported Targets | Linux | +| ----------------- | ----- | + +# esp_stdio custom I/O (Linux host test) + +Linux-only host test for the runtime console-mux registration API in `esp_stdio`: +`esp_stdio_register_custom_io()` / `esp_stdio_unregister_custom_io()` for both +`ESP_STDIO_PRIMARY` and `ESP_STDIO_AUXILIARY` roles. The mux logic is pure C +(dispatch through `esp_vfs_fs_ops_t` function pointers), so a small in-memory +mock VFS backend is enough — no MCU peripheral required. + +## Run + +``` +idf.py --preview set-target linux +idf.py build +build/test_esp_stdio_custom_io.elf +``` + +Or via pytest: + +``` +pytest pytest_esp_stdio_custom_io.py --target linux +``` diff --git a/components/esp_stdio/test_apps/custom_io/main/CMakeLists.txt b/components/esp_stdio/test_apps/custom_io/main/CMakeLists.txt new file mode 100644 index 00000000000..0ff584d8870 --- /dev/null +++ b/components/esp_stdio/test_apps/custom_io/main/CMakeLists.txt @@ -0,0 +1,6 @@ +idf_component_register(SRCS "test_app_main.c" + "test_custom_io.c" + "mock_vfs.c" + INCLUDE_DIRS "." + PRIV_REQUIRES unity esp_stdio vfs + WHOLE_ARCHIVE) diff --git a/components/esp_stdio/test_apps/custom_io/main/mock_vfs.c b/components/esp_stdio/test_apps/custom_io/main/mock_vfs.c new file mode 100644 index 00000000000..b2d578bb00c --- /dev/null +++ b/components/esp_stdio/test_apps/custom_io/main/mock_vfs.c @@ -0,0 +1,98 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include "mock_vfs.h" + +mock_vfs_state_t mock_a; +mock_vfs_state_t mock_b; + +static int mock_open(void *ctx, const char *path, int flags, int mode) +{ + (void)path; + (void)flags; + (void)mode; + mock_vfs_state_t *s = (mock_vfs_state_t *)ctx; + s->open_count++; + return s->next_fd; +} + +static int mock_close(void *ctx, int fd) +{ + mock_vfs_state_t *s = (mock_vfs_state_t *)ctx; + s->close_count++; + s->last_closed_fd = fd; + return 0; +} + +static ssize_t mock_write(void *ctx, int fd, const void *data, size_t size) +{ + (void)fd; + mock_vfs_state_t *s = (mock_vfs_state_t *)ctx; + s->write_count++; + size_t room = MOCK_VFS_BUF_SIZE - s->write_len; + size_t n = size < room ? size : room; + memcpy(s->write_buf + s->write_len, data, n); + s->write_len += n; + return (ssize_t)size; +} + +static ssize_t mock_read(void *ctx, int fd, void *dst, size_t size) +{ + (void)fd; + mock_vfs_state_t *s = (mock_vfs_state_t *)ctx; + s->read_count++; + size_t avail = s->read_len - s->read_pos; + size_t n = size < avail ? size : avail; + memcpy(dst, s->read_buf + s->read_pos, n); + s->read_pos += n; + return (ssize_t)n; +} + +static int mock_fstat(void *ctx, int fd, struct stat *st) +{ + (void)ctx; + (void)fd; + if (st) { + memset(st, 0, sizeof(*st)); + } + return 0; +} + +static int mock_fsync(void *ctx, int fd) +{ + (void)fd; + mock_vfs_state_t *s = (mock_vfs_state_t *)ctx; + s->fsync_count++; + return 0; +} + +const esp_vfs_fs_ops_t mock_vfs_ops_a = { + .write_p = &mock_write, + .open_p = &mock_open, + .close_p = &mock_close, + .read_p = &mock_read, + .fstat_p = &mock_fstat, + .fsync_p = &mock_fsync, +}; + +const esp_vfs_fs_ops_t mock_vfs_ops_b = { + .write_p = &mock_write, + .open_p = &mock_open, + .close_p = &mock_close, + .read_p = &mock_read, + .fstat_p = &mock_fstat, + .fsync_p = &mock_fsync, +}; + +void mock_vfs_reset(void) +{ + memset(&mock_a, 0, sizeof(mock_a)); + memset(&mock_b, 0, sizeof(mock_b)); + mock_a.next_fd = 10; + mock_b.next_fd = 20; + mock_a.last_closed_fd = -1; + mock_b.last_closed_fd = -1; +} diff --git a/components/esp_stdio/test_apps/custom_io/main/mock_vfs.h b/components/esp_stdio/test_apps/custom_io/main/mock_vfs.h new file mode 100644 index 00000000000..f859600cd53 --- /dev/null +++ b/components/esp_stdio/test_apps/custom_io/main/mock_vfs.h @@ -0,0 +1,53 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include +#include +#include "esp_vfs_ops.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define MOCK_VFS_BUF_SIZE 256 + +/** + * @brief In-memory fake VFS backend used to exercise the esp_stdio console mux. + * + * Every callback records that it was invoked so tests can assert exactly how + * the mux dispatched the operation. Writes are appended to @c write_buf; reads + * are served from @c read_buf. + */ +typedef struct { + int open_count; /*!< number of open_p() calls */ + int close_count; /*!< number of close_p() calls */ + int write_count; /*!< number of write_p() calls */ + int read_count; /*!< number of read_p() calls */ + int fsync_count; /*!< number of fsync_p() calls */ + int next_fd; /*!< fd returned by next open_p() */ + int last_closed_fd; /*!< fd passed to the last close_p() */ + char write_buf[MOCK_VFS_BUF_SIZE]; /*!< accumulated written bytes */ + size_t write_len; /*!< bytes stored in write_buf */ + char read_buf[MOCK_VFS_BUF_SIZE]; /*!< bytes served by read_p() */ + size_t read_len; /*!< bytes available in read_buf */ + size_t read_pos; /*!< read cursor into read_buf */ +} mock_vfs_state_t; + +/* Two independent backend instances + ops tables, enough for primary + aux tests. */ +extern mock_vfs_state_t mock_a; +extern mock_vfs_state_t mock_b; + +extern const esp_vfs_fs_ops_t mock_vfs_ops_a; +extern const esp_vfs_fs_ops_t mock_vfs_ops_b; + +/** @brief Reset both mock backends to a clean state. */ +void mock_vfs_reset(void); + +#ifdef __cplusplus +} +#endif diff --git a/components/esp_stdio/test_apps/custom_io/main/test_app_main.c b/components/esp_stdio/test_apps/custom_io/main/test_app_main.c new file mode 100644 index 00000000000..1d3c3edad92 --- /dev/null +++ b/components/esp_stdio/test_apps/custom_io/main/test_app_main.c @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "unity.h" +#include "unity_test_runner.h" +#include "mock_vfs.h" + +void setUp(void) +{ + mock_vfs_reset(); +} + +void app_main(void) +{ + unity_run_menu(); +} diff --git a/components/esp_stdio/test_apps/custom_io/main/test_custom_io.c b/components/esp_stdio/test_apps/custom_io/main/test_custom_io.c new file mode 100644 index 00000000000..480ab6e7894 --- /dev/null +++ b/components/esp_stdio/test_apps/custom_io/main/test_custom_io.c @@ -0,0 +1,268 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include "unity.h" +#include "unity_test_runner.h" +#include "esp_stdio.h" +#include "mock_vfs.h" + +static const esp_stdio_io_config_t cfg_a = { + .vfs_ops = &mock_vfs_ops_a, .vfs_ctx = &mock_a, .path = "/", +}; +static const esp_stdio_io_config_t cfg_b = { + .vfs_ops = &mock_vfs_ops_b, .vfs_ctx = &mock_b, .path = "/", +}; + +TEST_CASE("register_io/push+pop primary: write and read routed correctly", "[esp_stdio]") +{ + esp_stdio_handle_t h; + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_register_io(&cfg_a, &h)); + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_push_primary(h)); + + int fd = open("/dev/console", O_RDWR); + TEST_ASSERT_GREATER_OR_EQUAL(0, fd); + TEST_ASSERT_EQUAL(1, mock_a.open_count); + + const char *msg = "hello"; + TEST_ASSERT_EQUAL((ssize_t)strlen(msg), write(fd, msg, strlen(msg))); + TEST_ASSERT_EQUAL(1, mock_a.write_count); + TEST_ASSERT_EQUAL_STRING_LEN(msg, mock_a.write_buf, strlen(msg)); + + TEST_ASSERT_EQUAL(0, close(fd)); + TEST_ASSERT_EQUAL(1, mock_a.close_count); + + /* pop_primary: system primary (null VFS) takes over; mock_a receives no more traffic */ + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_pop_primary(NULL)); + + fd = open("/dev/console", O_RDWR); + TEST_ASSERT_GREATER_OR_EQUAL(0, fd); + TEST_ASSERT_EQUAL(1, write(fd, "x", 1)); /* goes to null VFS + mock_a as aux */ + TEST_ASSERT_EQUAL(2, mock_a.write_count); /* mock_a is now aux again */ + close(fd); + + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_unregister_io(h)); +} + +TEST_CASE("primary read path", "[esp_stdio]") +{ + strcpy(mock_a.read_buf, "abc"); + mock_a.read_len = 3; + + esp_stdio_handle_t h; + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_register_io(&cfg_a, &h)); + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_push_primary(h)); + + int fd = open("/dev/console", O_RDWR); + TEST_ASSERT_GREATER_OR_EQUAL(0, fd); + + char buf[8] = {0}; + TEST_ASSERT_EQUAL(3, read(fd, buf, sizeof(buf))); + TEST_ASSERT_EQUAL_STRING("abc", buf); + TEST_ASSERT_EQUAL(1, mock_a.read_count); + + close(fd); + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_pop_primary(NULL)); + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_unregister_io(h)); +} + +TEST_CASE("auxiliary fan-out", "[esp_stdio]") +{ + esp_stdio_handle_t h; + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_register_io(&cfg_b, &h)); + /* stays as auxiliary */ + + int fd = open("/dev/console", O_RDWR); + TEST_ASSERT_GREATER_OR_EQUAL(0, fd); + TEST_ASSERT_EQUAL(1, mock_b.open_count); + + const char *msg = "fan"; + TEST_ASSERT_EQUAL((ssize_t)strlen(msg), write(fd, msg, strlen(msg))); + TEST_ASSERT_EQUAL(1, mock_b.write_count); + TEST_ASSERT_EQUAL_STRING_LEN(msg, mock_b.write_buf, strlen(msg)); + + TEST_ASSERT_EQUAL(0, fsync(fd)); + TEST_ASSERT_EQUAL(1, mock_b.fsync_count); + + close(fd); + TEST_ASSERT_EQUAL(1, mock_b.close_count); + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_unregister_io(h)); +} + +TEST_CASE("entry pool exhaustion", "[esp_stdio]") +{ + /* Pool size = CONFIG_ESP_STDIO_MAX_VFS_ENTRIES=3; system uses 1 -> 2 free */ + esp_stdio_handle_t h1, h2, h3; + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_register_io(&cfg_a, &h1)); + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_register_io(&cfg_b, &h2)); + TEST_ASSERT_EQUAL(ESP_ERR_NO_MEM, esp_stdio_register_io(&cfg_a, &h3)); + + int fd = open("/dev/console", O_RDWR); + TEST_ASSERT_EQUAL(1, write(fd, "x", 1)); + TEST_ASSERT_EQUAL(1, mock_a.write_count); + TEST_ASSERT_EQUAL(1, mock_b.write_count); + close(fd); + + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_unregister_io(h1)); + /* freed a slot */ + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_register_io(&cfg_a, &h1)); + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_unregister_io(h1)); + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_unregister_io(h2)); +} + +TEST_CASE("invalid arguments", "[esp_stdio]") +{ + esp_stdio_handle_t h; + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, esp_stdio_register_io(NULL, &h)); + + esp_stdio_io_config_t bad = { .vfs_ops = NULL, .path = "/" }; + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, esp_stdio_register_io(&bad, &h)); + + bad.vfs_ops = &mock_vfs_ops_a; + bad.path = NULL; + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, esp_stdio_register_io(&bad, &h)); + + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, esp_stdio_register_io(&cfg_a, NULL)); + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, esp_stdio_push_primary(NULL)); + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, esp_stdio_unregister_io(NULL)); +} + +TEST_CASE("pop_primary with no user primary returns INVALID_STATE", "[esp_stdio]") +{ + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_STATE, esp_stdio_pop_primary(NULL)); +} + +TEST_CASE("push_primary reroutes writes, pop restores previous", "[esp_stdio]") +{ + esp_stdio_handle_t ha, hb; + strcpy(mock_a.read_buf, "AAA"); + mock_a.read_len = 3; + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_register_io(&cfg_a, &ha)); + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_push_primary(ha)); + + int fd = open("/dev/console", O_RDWR); + char buf[8] = {0}; + TEST_ASSERT_EQUAL(3, read(fd, buf, sizeof(buf))); + TEST_ASSERT_EQUAL_STRING("AAA", buf); + TEST_ASSERT_EQUAL(2, write(fd, "a1", 2)); + TEST_ASSERT_EQUAL(1, mock_a.write_count); + close(fd); + + /* Push B on top — A drops back to aux */ + strcpy(mock_b.read_buf, "BBB"); + mock_b.read_len = 3; + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_register_io(&cfg_b, &hb)); + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_push_primary(hb)); + + fd = open("/dev/console", O_RDWR); + memset(buf, 0, sizeof(buf)); + TEST_ASSERT_EQUAL(3, read(fd, buf, sizeof(buf))); + TEST_ASSERT_EQUAL_STRING("BBB", buf); + TEST_ASSERT_EQUAL(2, write(fd, "b1", 2)); + TEST_ASSERT_EQUAL(1, mock_b.write_count); + TEST_ASSERT_EQUAL(2, mock_a.write_count); /* A is aux, gets fan-out */ + close(fd); + + /* Pop B — A is primary again */ + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_pop_primary(NULL)); + fd = open("/dev/console", O_RDWR); + TEST_ASSERT_EQUAL(1, write(fd, "y", 1)); + TEST_ASSERT_EQUAL(3, mock_a.write_count); /* primary write */ + TEST_ASSERT_EQUAL(2, mock_b.write_count); /* B now aux, gets fan-out */ + close(fd); + + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_pop_primary(NULL)); + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_unregister_io(ha)); + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_unregister_io(hb)); +} + +TEST_CASE("unregister active primary succeeds (implicit pop)", "[esp_stdio]") +{ + esp_stdio_handle_t h; + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_register_io(&cfg_a, &h)); + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_push_primary(h)); + + int fd = open("/dev/console", O_RDWR); + TEST_ASSERT_EQUAL(1, write(fd, "z", 1)); + TEST_ASSERT_EQUAL(1, mock_a.write_count); + + /* Unregister while it is the active primary — must succeed */ + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_unregister_io(h)); + + /* Console still works via the restored system primary */ + TEST_ASSERT_EQUAL(1, write(fd, "q", 1)); + close(fd); +} + +TEST_CASE("install/uninstall are no-ops with user primary", "[esp_stdio]") +{ + esp_stdio_handle_t h; + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_register_io(&cfg_a, &h)); + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_push_primary(h)); + + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_install_io_driver()); + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_uninstall_io_driver()); + TEST_ASSERT_EQUAL(0, mock_a.open_count); /* driver not initialised by install */ + + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_pop_primary(NULL)); + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_unregister_io(h)); +} + +TEST_CASE("unregister one auxiliary keeps others working", "[esp_stdio]") +{ + esp_stdio_handle_t ha, hb; + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_register_io(&cfg_a, &ha)); + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_register_io(&cfg_b, &hb)); + + int fd = open("/dev/console", O_RDWR); + TEST_ASSERT_EQUAL(2, write(fd, "hi", 2)); + TEST_ASSERT_EQUAL(1, mock_a.write_count); + TEST_ASSERT_EQUAL(1, mock_b.write_count); + + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_unregister_io(ha)); + TEST_ASSERT_EQUAL(1, mock_a.close_count); + + TEST_ASSERT_EQUAL(2, write(fd, "yo", 2)); + TEST_ASSERT_EQUAL(1, mock_a.write_count); /* A gone */ + TEST_ASSERT_EQUAL(2, mock_b.write_count); /* B still there */ + + close(fd); + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_unregister_io(hb)); +} + +TEST_CASE("pop_primary(handle) removes a buried primary without disturbing the active one", "[esp_stdio]") +{ + /* Models two independent owners: A pushes its primary, then B pushes on + * top. A releases its own (now buried) primary by handle — B must stay + * active and A must drop back to being an auxiliary sink. */ + esp_stdio_handle_t ha, hb; + strcpy(mock_b.read_buf, "BBB"); + mock_b.read_len = 3; + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_register_io(&cfg_a, &ha)); + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_push_primary(ha)); + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_register_io(&cfg_b, &hb)); + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_push_primary(hb)); + + /* Remove the buried primary A (not the active top B) */ + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_pop_primary(ha)); + + int fd = open("/dev/console", O_RDWR); + char buf[8] = {0}; + TEST_ASSERT_EQUAL(3, read(fd, buf, sizeof(buf))); + TEST_ASSERT_EQUAL_STRING("BBB", buf); /* B is still the active primary */ + TEST_ASSERT_EQUAL(1, write(fd, "x", 1)); + TEST_ASSERT_EQUAL(1, mock_b.write_count); /* B primary write */ + TEST_ASSERT_EQUAL(1, mock_a.write_count); /* A demoted to aux, gets fan-out */ + close(fd); + + /* A is no longer on the stack — popping it again is INVALID_STATE */ + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_STATE, esp_stdio_pop_primary(ha)); + + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_pop_primary(NULL)); + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_unregister_io(ha)); + TEST_ASSERT_EQUAL(ESP_OK, esp_stdio_unregister_io(hb)); +} diff --git a/components/esp_stdio/test_apps/custom_io/pytest_esp_stdio_custom_io.py b/components/esp_stdio/test_apps/custom_io/pytest_esp_stdio_custom_io.py new file mode 100644 index 00000000000..0259a45045b --- /dev/null +++ b/components/esp_stdio/test_apps/custom_io/pytest_esp_stdio_custom_io.py @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Unlicense OR CC0-1.0 +import pytest +from pytest_embedded import Dut +from pytest_embedded_idf.utils import idf_parametrize + + +@pytest.mark.host_test +@idf_parametrize('target', ['linux'], indirect=['target']) +def test_esp_stdio_custom_io_linux(dut: Dut) -> None: + dut.run_all_single_board_cases(timeout=60) diff --git a/components/esp_stdio/test_apps/custom_io/sdkconfig.defaults b/components/esp_stdio/test_apps/custom_io/sdkconfig.defaults new file mode 100644 index 00000000000..3c7fbf5fbaa --- /dev/null +++ b/components/esp_stdio/test_apps/custom_io/sdkconfig.defaults @@ -0,0 +1,3 @@ +CONFIG_IDF_TARGET="linux" +CONFIG_VFS_SUPPORT_IO=y +CONFIG_ESP_STDIO_MAX_VFS_ENTRIES=3 diff --git a/components/esp_usb_cdc_rom_console/include/esp_private/esp_vfs_cdcacm.h b/components/esp_usb_cdc_rom_console/include/esp_private/esp_vfs_cdcacm.h index 5435be1530d..39335ca2b0c 100644 --- a/components/esp_usb_cdc_rom_console/include/esp_private/esp_vfs_cdcacm.h +++ b/components/esp_usb_cdc_rom_console/include/esp_private/esp_vfs_cdcacm.h @@ -49,8 +49,9 @@ esp_err_t cdcacm_vfs_dev_port_init(const esp_console_dev_usb_cdc_config_t *confi * another console backend. * * @param config Pointer to the USB CDC-ACM VFS device configuration. + * @return ESP_OK if deinitialization completes successfully, or an error code if it fails. */ -void cdcacm_vfs_dev_port_deinit(const esp_console_dev_usb_cdc_config_t *config); +esp_err_t cdcacm_vfs_dev_port_deinit(const esp_console_dev_usb_cdc_config_t *config); #ifdef __cplusplus } diff --git a/components/esp_usb_cdc_rom_console/vfs_cdcacm.c b/components/esp_usb_cdc_rom_console/vfs_cdcacm.c index eb6f55fc975..40661b6a221 100644 --- a/components/esp_usb_cdc_rom_console/vfs_cdcacm.c +++ b/components/esp_usb_cdc_rom_console/vfs_cdcacm.c @@ -547,9 +547,10 @@ esp_err_t cdcacm_vfs_dev_port_init(const esp_console_dev_usb_cdc_config_t *confi return ESP_OK; } -void cdcacm_vfs_dev_port_deinit(const esp_console_dev_usb_cdc_config_t *config) +esp_err_t cdcacm_vfs_dev_port_deinit(const esp_console_dev_usb_cdc_config_t *config) { (void)config; + return ESP_OK; } #endif diff --git a/components/fatfs/host_test/sdkconfig.defaults b/components/fatfs/host_test/sdkconfig.defaults index 53e7687b770..fd403d17ab0 100644 --- a/components/fatfs/host_test/sdkconfig.defaults +++ b/components/fatfs/host_test/sdkconfig.defaults @@ -1,4 +1,5 @@ CONFIG_IDF_TARGET="linux" +CONFIG_VFS_SUPPORT_IO=y CONFIG_COMPILER_CXX_EXCEPTIONS=y CONFIG_UNITY_ENABLE_IDF_TEST_RUNNER=n CONFIG_WL_SECTOR_SIZE=4096 diff --git a/components/vfs/CMakeLists.txt b/components/vfs/CMakeLists.txt index 1574b183e69..bb1ad7bc984 100644 --- a/components/vfs/CMakeLists.txt +++ b/components/vfs/CMakeLists.txt @@ -6,11 +6,13 @@ if(${target} STREQUAL "linux") set(inc) set(priv_inc) - list(APPEND inc include) + # linux_include provides stubs (sys/reent.h, dirent.h, errno.h) required by + # esp_vfs.h unconditionally, so expose it for all Linux builds regardless of + # VFS_SUPPORT_IO, which now defaults to n on Linux. + list(APPEND inc include linux_include) if(CONFIG_VFS_SUPPORT_IO) - list(APPEND inc linux_include) list(APPEND priv_inc private_include) - list(APPEND srcs "vfs_linux.c" "vfs_linux_default_coop.c" "vfs.c" "vfs_calls.c") + list(APPEND srcs "vfs_linux.c" "vfs_linux_default_coop.c" "vfs.c" "vfs_calls.c" "nullfs.c") endif() if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux") diff --git a/components/vfs/linux_include/dirent.h b/components/vfs/linux_include/dirent.h index a4c9c64803d..897035206ef 100644 --- a/components/vfs/linux_include/dirent.h +++ b/components/vfs/linux_include/dirent.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -32,10 +32,16 @@ typedef struct { /** * @brief Directory entry structure + * + * Fields match the glibc layout so that this header is binary-compatible with + * glibc's readdir() when CONFIG_VFS_SUPPORT_IO is disabled on Linux (in which + * case glibc's readdir() fills the struct, not the VFS wrapper). */ struct dirent { - ino_t d_ino; /*!< file number */ - uint8_t d_type; /*!< not defined in POSIX, but present in BSD and Linux */ + ino_t d_ino; /*!< file number */ + off_t d_off; /*!< implementation-defined offset (glibc extension) */ + unsigned short d_reclen; /*!< length of this record (glibc extension) */ + uint8_t d_type; /*!< not defined in POSIX, but present in BSD and Linux */ #define DT_UNKNOWN 0 #define DT_REG 1 #define DT_DIR 2 diff --git a/components/vfs/nullfs.c b/components/vfs/nullfs.c index f67656e3365..dd4fbcfa8f0 100644 --- a/components/vfs/nullfs.c +++ b/components/vfs/nullfs.c @@ -148,7 +148,7 @@ static ssize_t vfs_null_read(__attribute__((unused)) void *ctx, int fd, void *da return -1; } -static int vfs_null_pread(__attribute__((unused)) void *ctx, int fd, void *data, size_t size, off_t offset) +static ssize_t vfs_null_pread(__attribute__((unused)) void *ctx, int fd, void *data, size_t size, off_t offset) { UNUSED(data); UNUSED(size); @@ -165,7 +165,7 @@ static int vfs_null_pread(__attribute__((unused)) void *ctx, int fd, void *data, } -static int vfs_null_pwrite(__attribute__((unused)) void *ctx, int fd, const void *data, size_t size, off_t offset) +static ssize_t vfs_null_pwrite(__attribute__((unused)) void *ctx, int fd, const void *data, size_t size, off_t offset) { UNUSED(data); UNUSED(offset); diff --git a/tools/test_apps/system/panic/common/main/test_panic.c b/tools/test_apps/system/panic/common/main/test_panic.c index bb82d279b58..e0ac418e6c2 100644 --- a/tools/test_apps/system/panic/common/main/test_panic.c +++ b/tools/test_apps/system/panic/common/main/test_panic.c @@ -69,7 +69,7 @@ void test_task_wdt_cpu0(void) } #if CONFIG_ESP_SYSTEM_HW_STACK_GUARD -#define HWSG_TASK_SIZE 1024 +#define HWSG_TASK_SIZE 2048 __attribute__((optimize("-O0"))) static void test_hw_stack_guard_cpu(void* arg) {