mirror of
https://github.com/espressif/esp-idf.git
synced 2026-10-01 10:40:47 +03:00
feat(esp_stdio): add custom IO registration
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user