feat(esp_stdio): add custom IO registration

This commit is contained in:
Guillaume Souchere
2026-09-09 09:00:45 +02:00
parent 4cf9d611e6
commit 654db440cb
30 changed files with 1093 additions and 220 deletions
@@ -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
}
+3 -1
View File
@@ -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)
@@ -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()
@@ -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
}
@@ -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
+21 -9
View File
@@ -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()
+104 -4
View File
@@ -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
}
+2 -1
View File
@@ -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)
@@ -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
}
@@ -0,0 +1,60 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdbool.h>
#include <fcntl.h>
#include <sys/lock.h>
#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
+198
View File
@@ -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 <string.h>
#include <fcntl.h>
#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
+27 -6
View File
@@ -4,6 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include <stdbool.h>
#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;
}
+148 -180
View File
@@ -16,6 +16,14 @@
#include "esp_vfs.h"
#include <sys/errno.h>
/**
* 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 <sys/lock.h>
#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
@@ -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
@@ -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)
@@ -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
```
@@ -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)
@@ -0,0 +1,98 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#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;
}
@@ -0,0 +1,53 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stddef.h>
#include <sys/types.h>
#include <sys/stat.h>
#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
@@ -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();
}
@@ -0,0 +1,268 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#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));
}
@@ -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)
@@ -0,0 +1,3 @@
CONFIG_IDF_TARGET="linux"
CONFIG_VFS_SUPPORT_IO=y
CONFIG_ESP_STDIO_MAX_VFS_ENTRIES=3
@@ -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
}
@@ -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
@@ -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
+5 -3
View File
@@ -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")
+9 -3
View File
@@ -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
+2 -2
View File
@@ -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);
@@ -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)
{