From 3c1e75706f8e5d45f8a372e5a5594f27e3ba51e1 Mon Sep 17 00:00:00 2001 From: Guillaume Souchere Date: Wed, 9 Sep 2026 13:21:26 +0200 Subject: [PATCH] feat(esp_stdio): Revert breaking change - also revert the modification in the singature of uninstall function in all the concerned esp_driver* components and esp_stdio. - added proper documentation of the feature in the programming guide --- .../include/driver/esp_private/uart_vfs.h | 3 +- components/esp_driver_uart/src/uart_vfs.c | 4 +- .../driver/esp_private/usb_serial_jtag_vfs.h | 3 +- .../src/usb_serial_jtag_vfs.c | 3 +- components/esp_stdio/include/esp_stdio.h | 14 +++- components/esp_stdio/linux/esp_stdio_linux.c | 3 +- .../esp_stdio/linux/include/esp_stdio_linux.h | 3 +- components/esp_stdio/stdio_port.c | 14 ++-- components/esp_stdio/stdio_vfs.c | 2 +- .../test_apps/custom_io/main/test_custom_io.c | 2 +- .../pytest_esp_system_unity_tests.py | 2 +- .../include/esp_private/esp_vfs_cdcacm.h | 3 +- .../esp_usb_cdc_rom_console/vfs_cdcacm.c | 3 +- components/fatfs/host_test/sdkconfig.defaults | 1 - docs/en/api-guides/stdio.rst | 69 +++++++++++++++++++ 15 files changed, 99 insertions(+), 30 deletions(-) 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 8d57c726050..88981632854 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,9 +50,8 @@ 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. */ -esp_err_t uart_vfs_dev_port_deinit(const esp_console_dev_uart_config_t *config); +void 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 1c2b8116d84..69fef254ed5 100644 --- a/components/esp_driver_uart/src/uart_vfs.c +++ b/components/esp_driver_uart/src/uart_vfs.c @@ -1163,7 +1163,6 @@ 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) @@ -1219,11 +1218,10 @@ esp_err_t uart_vfs_dev_port_init(const esp_console_dev_uart_config_t *config, return ESP_OK; } -esp_err_t uart_vfs_dev_port_deinit(const esp_console_dev_uart_config_t *config) +void 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/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 39a8a37eb4c..3588c158607 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,9 +51,8 @@ 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. */ -esp_err_t usb_serial_jtag_vfs_dev_port_deinit(const esp_console_dev_usb_serial_jtag_config_t *config); +void 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 78e3124a840..0247f0efe57 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,12 +704,11 @@ esp_err_t usb_serial_jtag_vfs_dev_port_init(const esp_console_dev_usb_serial_jta return ESP_OK; } -esp_err_t usb_serial_jtag_vfs_dev_port_deinit(const esp_console_dev_usb_serial_jtag_config_t *config) +void 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 diff --git a/components/esp_stdio/include/esp_stdio.h b/components/esp_stdio/include/esp_stdio.h index fcc0fbaf096..7f6d114740f 100644 --- a/components/esp_stdio/include/esp_stdio.h +++ b/components/esp_stdio/include/esp_stdio.h @@ -14,6 +14,18 @@ extern "C" { #define ESP_VFS_DEV_CONSOLE "/dev/console" +/** + * @brief Register the default console VFS backend(s) selected in Kconfig. + * + * Sets up the primary (and any Kconfig-selected secondary) console sink and + * mounts them under /dev/console. This function is automatically called from + * startup code to enable serial output; applications normally do not need to + * call it themselves. + * + * @return ESP_OK on success, or an error code from the underlying VFS registration. + */ +esp_err_t esp_stdio_register(void); + #if CONFIG_VFS_SUPPORT_IO #include "esp_vfs_common.h" @@ -134,7 +146,7 @@ esp_err_t esp_stdio_install_io_driver(void); * If a user has taken over the primary console with esp_stdio_push_primary(), * this function is a no-op. */ -esp_err_t esp_stdio_uninstall_io_driver(void); +void 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 bbc7ab64ad3..37993a8139f 100644 --- a/components/esp_stdio/linux/esp_stdio_linux.c +++ b/components/esp_stdio/linux/esp_stdio_linux.c @@ -21,10 +21,9 @@ static void disable_raw_mode(void) assert(tcsetattr(STDIN_FILENO, TCSAFLUSH, &s_orig_termios) == 0); } -esp_err_t linux_vfs_dev_port_deinit(linux_port_config_t *config) +void 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 7279d1b49ee..356663f6758 100644 --- a/components/esp_stdio/linux/include/esp_stdio_linux.h +++ b/components/esp_stdio/linux/include/esp_stdio_linux.h @@ -43,9 +43,8 @@ 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. */ -esp_err_t linux_vfs_dev_port_deinit(linux_port_config_t *config); +void linux_vfs_dev_port_deinit(linux_port_config_t *config); #ifdef __cplusplus } diff --git a/components/esp_stdio/stdio_port.c b/components/esp_stdio/stdio_port.c index b27198e0075..c35dd8534ca 100644 --- a/components/esp_stdio/stdio_port.c +++ b/components/esp_stdio/stdio_port.c @@ -73,30 +73,28 @@ esp_err_t esp_stdio_install_io_driver(void) return ret; } -esp_err_t esp_stdio_uninstall_io_driver(void) +void 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; + return; } #endif // CONFIG_VFS_SUPPORT_IO - esp_err_t ret = ESP_FAIL; #if CONFIG_IDF_TARGET_LINUX linux_port_config_t config = ESP_CONSOLE_DEV_LINUX_CONFIG_DEFAULT(); - ret = linux_vfs_dev_port_deinit(&config); + 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(); - ret = uart_vfs_dev_port_deinit(&config); + 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(); - ret = usb_serial_jtag_vfs_dev_port_deinit(&config); + 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(); - ret = cdcacm_vfs_dev_port_deinit(&config); + 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 3670816115a..e7c248c3b37 100644 --- a/components/esp_stdio/stdio_vfs.c +++ b/components/esp_stdio/stdio_vfs.c @@ -358,7 +358,7 @@ static const esp_vfs_fs_ops_t s_vfs_console = { #endif }; -static esp_err_t esp_stdio_register(void) +esp_err_t esp_stdio_register(void) { _lock_init(&s_lock); 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 index 480ab6e7894..89a48a4bbd3 100644 --- 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 @@ -205,7 +205,7 @@ TEST_CASE("install/uninstall are no-ops with user primary", "[esp_stdio]") 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()); + 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)); diff --git a/components/esp_system/test_apps/esp_system_unity_tests/pytest_esp_system_unity_tests.py b/components/esp_system/test_apps/esp_system_unity_tests/pytest_esp_system_unity_tests.py index e0261c02b94..53dff26b29f 100644 --- a/components/esp_system/test_apps/esp_system_unity_tests/pytest_esp_system_unity_tests.py +++ b/components/esp_system/test_apps/esp_system_unity_tests/pytest_esp_system_unity_tests.py @@ -30,7 +30,7 @@ from pytest_embedded_idf.utils import soc_filtered_targets ) def test_esp_system(dut: Dut) -> None: # esp32p4 32MB PSRAM initialize in startup takes more than 30 sec - dut.run_all_single_board_cases(timeout=120) + dut.run_all_single_board_cases(timeout=60) def esp_reset_and_wait_ready(dut: Dut) -> None: 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 39335ca2b0c..5435be1530d 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,9 +49,8 @@ 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. */ -esp_err_t cdcacm_vfs_dev_port_deinit(const esp_console_dev_usb_cdc_config_t *config); +void 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 40661b6a221..eb6f55fc975 100644 --- a/components/esp_usb_cdc_rom_console/vfs_cdcacm.c +++ b/components/esp_usb_cdc_rom_console/vfs_cdcacm.c @@ -547,10 +547,9 @@ esp_err_t cdcacm_vfs_dev_port_init(const esp_console_dev_usb_cdc_config_t *confi return ESP_OK; } -esp_err_t cdcacm_vfs_dev_port_deinit(const esp_console_dev_usb_cdc_config_t *config) +void 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 fd403d17ab0..53e7687b770 100644 --- a/components/fatfs/host_test/sdkconfig.defaults +++ b/components/fatfs/host_test/sdkconfig.defaults @@ -1,5 +1,4 @@ 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/docs/en/api-guides/stdio.rst b/docs/en/api-guides/stdio.rst index ea0b31b1d54..7e658c0e227 100644 --- a/docs/en/api-guides/stdio.rst +++ b/docs/en/api-guides/stdio.rst @@ -170,3 +170,72 @@ Once you have created a custom VFS driver, use :cpp:func:`esp_vfs_register_fs()` stderr = f; Note that logging functions (``ESP_LOGE()``, etc.) write their output to ``stdout``. Keep this in mind when using logging within the implementation of your custom VFS (or any components which it calls). For example, if the custom VFS driver's ``write()`` operation fails and uses ``ESP_LOGE()`` to log the error, this will cause the output to be sent to ``stdout``, which would again call the custom VFS driver's ``write()`` operation. This would result in an infinite loop. It is recommended to keep track of this re-entry condition in the VFS driver's ``write()`` implementation, and return immediately if the write operation is still in progress. + +Console I/O multiplexer +----------------------- + +In addition to redirecting ``stdout`` and ``stderr`` manually (as shown above), ESP-IDF provides a console I/O multiplexer that lets an application register one or more custom VFS backends and switch between them at runtime, without reassigning the ``stdin``/``stdout``/``stderr`` streams itself. + +The multiplexer is exposed on ``/dev/console`` and manages backends in two roles: + +- **Primary** — the active read and write backend. Both application input (``stdin``) and output (``stdout``/``stderr``) go through it. Primaries are kept on a stack: pushing a new primary suspends the previous one, and popping restores it. The default console selected in Kconfig sits at the base of the stack and is always kept as the ultimate fallback. +- **Auxiliary** — a write-only sink. Every byte written to ``stdout`` and ``stderr`` is fanned out to all registered auxiliaries in addition to the primary. This is useful, for example, to mirror console output to a log file or a network connection while keeping the interactive console on the physical interface. + +The maximum number of backends that can be registered at once (primaries plus auxiliaries, including the Kconfig default) is set by :ref:`CONFIG_ESP_STDIO_MAX_VFS_ENTRIES`. + +API overview +^^^^^^^^^^^^ + +The multiplexer API is declared in ``esp_stdio.h`` and is available when ``CONFIG_VFS_SUPPORT_IO`` is enabled: + +- ``esp_stdio_register_io()`` — register a VFS backend as a write-only auxiliary and return an opaque handle. The backend starts receiving the write fan-out immediately. +- ``esp_stdio_push_primary()`` — promote a previously registered handle to the active read and write primary. The current primary is suspended on the stack. +- ``esp_stdio_pop_primary()`` — remove a primary from the stack, returning it to auxiliary status. Passing ``NULL`` removes the current (top) primary; passing a specific handle removes it from wherever it sits in the stack. +- ``esp_stdio_unregister_io()`` — remove a backend from the multiplexer entirely. If it is currently the active primary, it is popped first. + +The backend is described by an ``esp_stdio_io_config_t`` structure: + +.. list:: + + - ``vfs_ops`` — pointer to the VFS operations table (must not be ``NULL``). + - ``vfs_ctx`` — context pointer forwarded to every VFS callback. + - ``path`` — path passed to ``open()`` inside the driver, for example ``"/"``. + +.. note:: + + The ops table, context, and path string passed in the configuration must remain valid until the backend is removed with ``esp_stdio_unregister_io()``. + +Example +^^^^^^^ + +The following example registers a custom backend, makes it the active console, and later restores the previous one: + +.. code-block:: c + + #include "esp_stdio.h" + + // A VFS operations table implemented by the application. + extern const esp_vfs_fs_ops_t my_console_vfs; + + esp_stdio_handle_t handle; + esp_stdio_io_config_t config = { + .vfs_ops = &my_console_vfs, + .vfs_ctx = NULL, + .path = "/", + }; + + // Register as a write-only auxiliary: output is now mirrored to it. + ESP_ERROR_CHECK(esp_stdio_register_io(&config, &handle)); + + // Promote it to the active read + write primary. + ESP_ERROR_CHECK(esp_stdio_push_primary(handle)); + + // ... application uses the custom console for stdin/stdout/stderr ... + + // Restore the previous primary (the custom backend reverts to auxiliary). + ESP_ERROR_CHECK(esp_stdio_pop_primary(handle)); + + // Remove the backend from the multiplexer entirely. + ESP_ERROR_CHECK(esp_stdio_unregister_io(handle)); + +The same re-entrancy caveat described above for logging applies here: the backend's ``write()`` implementation must not trigger logging that would recursively write to the console.