From cd8f64eab023aa308cf2b4b61598473884143996 Mon Sep 17 00:00:00 2001 From: Jimmy Wennlund Date: Sun, 2 Nov 2025 00:53:41 +0100 Subject: [PATCH 1/6] feat(esp_stdio): add Ctrl+D (EOF) support for console input - Add termios state tracking in console VFS layer - Implement EOF character detection in console_read() - Store and synchronize c_lflag and c_cc[] between tcgetattr/tcsetattr - Enable canonical mode and EOF handling in console example - Return 0 (EOF) when configured EOF character is detected in canonical mode This enables proper Ctrl+D handling for interactive console applications, allowing users to send EOF signals to terminate input or exit programs gracefully using standard POSIX terminal behavior. --- components/esp_stdio/stdio_vfs.c | 32 +++++++++++++++++-- .../advanced/main/console_example_main.c | 11 +++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/components/esp_stdio/stdio_vfs.c b/components/esp_stdio/stdio_vfs.c index 0ba2d2542ff6..83accd283d1e 100644 --- a/components/esp_stdio/stdio_vfs.c +++ b/components/esp_stdio/stdio_vfs.c @@ -34,6 +34,10 @@ typedef struct { int fd_primary; int fd_secondary; +#ifdef CONFIG_VFS_SUPPORT_TERMIOS + tcflag_t c_lflag; /** Local modes */ + cc_t c_cc[NCCS]; /** Control characters */ +#endif } vfs_console_context_t; #if CONFIG_VFS_SUPPORT_IO @@ -117,7 +121,26 @@ int console_close(int fd) ssize_t console_read(int fd, void * dst, size_t size) { - return read(vfs_console.fd_primary, dst, size); + ssize_t ret = read(vfs_console.fd_primary, dst, size); + + // Handle Ctrl+D (EOF) - only in canonical mode +#ifdef CONFIG_VFS_SUPPORT_TERMIOS + if (vfs_console.c_lflag & ICANON) { + if (ret > 0) { + char *buffer = (char *)dst; + char eof_char = vfs_console.c_cc[VEOF]; + for (ssize_t i = 0; i < ret; i++) { + if (buffer[i] == eof_char) { // Check for configured EOF character + // Return EOF by returning 0 bytes read + errno = EPIPE; + return 0; + } + } + } + } +#endif + + return ret; } int console_fcntl(int fd, int cmd, int arg) @@ -170,12 +193,17 @@ esp_err_t console_end_select(void *end_select_args) int console_tcsetattr(int fd, int optional_actions, const struct termios *p) { + vfs_console.c_lflag = p->c_lflag; + memcpy(vfs_console.c_cc, p->c_cc, sizeof(vfs_console.c_cc)); return tcsetattr(vfs_console.fd_primary, optional_actions, p); } int console_tcgetattr(int fd, struct termios *p) { - return tcgetattr(vfs_console.fd_primary, p); + int res = tcgetattr(vfs_console.fd_primary, p); + p->c_lflag = vfs_console.c_lflag; + memcpy(p->c_cc, vfs_console.c_cc, sizeof(vfs_console.c_cc)); + return res; } int console_tcdrain(int fd) diff --git a/examples/system/console/advanced/main/console_example_main.c b/examples/system/console/advanced/main/console_example_main.c index 7bcc8c7ee9bc..d533f4424dfe 100644 --- a/examples/system/console/advanced/main/console_example_main.c +++ b/examples/system/console/advanced/main/console_example_main.c @@ -20,6 +20,7 @@ #include "cmd_wifi.h" #include "cmd_nvs.h" #include "console_settings.h" +#include "sys/termios.h" /* * We warn if a secondary serial console is enabled. A secondary serial console is always output-only and @@ -85,6 +86,16 @@ void app_main(void) /* Initialize console output periheral (UART, USB_OTG, USB_JTAG) */ initialize_console_peripheral(); +#ifdef CONFIG_VFS_SUPPORT_TERMIOS + // Enable Ctrl+D to generate EOF on stdin + struct termios term; + tcgetattr(fileno(stdin), &term); + term.c_lflag |= ICANON; // Enable canonical mode for proper EOF handling + term.c_cc[VEOF] = 4; // Set Ctrl+D (ASCII 4) as EOF character + tcsetattr(fileno(stdin), TCSANOW, &term); +#endif + + /* Initialize linenoise library and esp_console*/ initialize_console_library(HISTORY_PATH); From 6461d9a2d63dd8ad6b1c9e9dda7fa9830da6d675 Mon Sep 17 00:00:00 2001 From: Jimmy Wennlund Date: Thu, 16 Oct 2025 22:01:51 +0200 Subject: [PATCH 2/6] feat(vfs): add thread-safe pipe implementation Add VFS layer support for POSIX-compatible pipes with: - Standard pipe() syscall implementation - Blocking/non-blocking I/O with select() support - Thread-safe operations using FreeRTOS primitives - Proper resource management and error handling --- components/vfs/CMakeLists.txt | 1 + components/vfs/include/esp_vfs_pipe.h | 57 +++ components/vfs/vfs_pipe.c | 629 ++++++++++++++++++++++++++ 3 files changed, 687 insertions(+) create mode 100644 components/vfs/include/esp_vfs_pipe.h create mode 100644 components/vfs/vfs_pipe.c diff --git a/components/vfs/CMakeLists.txt b/components/vfs/CMakeLists.txt index 7b2cf57fc365..43e79b8f64fd 100644 --- a/components/vfs/CMakeLists.txt +++ b/components/vfs/CMakeLists.txt @@ -18,6 +18,7 @@ list(APPEND pr esp_driver_uart esp_driver_usb_serial_jtag esp_usb_cdc_rom_consol list(APPEND sources "vfs.c" "vfs_eventfd.c" + "vfs_pipe.c" "vfs_semihost.c" "nullfs.c" ) diff --git a/components/vfs/include/esp_vfs_pipe.h b/components/vfs/include/esp_vfs_pipe.h new file mode 100644 index 000000000000..2002c09ec3cc --- /dev/null +++ b/components/vfs/include/esp_vfs_pipe.h @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include + +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Pipe vfs initialization settings + */ +typedef struct { + size_t max_fds; /*!< The maximum number of pipes supported */ +} esp_vfs_pipe_config_t; + +#define ESP_VFS_PIPE_CONFIG_DEFAULT() (esp_vfs_pipe_config_t) { \ + .max_fds = 20, \ +}; + +/** + * @brief Registers the pipe vfs. + * + * @return ESP_OK if successful, ESP_ERR_NO_MEM if too many VFSes are + * registered. + */ +esp_err_t esp_vfs_pipe_register(const esp_vfs_pipe_config_t *config); + +/** + * @brief Unregisters the pipe vfs. + * + * @return ESP_OK if successful, ESP_ERR_INVALID_STATE if VFS for given prefix + * hasn't been registered + */ +esp_err_t esp_vfs_pipe_unregister(void); + +/* + * @brief Creates a pair of pipe file descriptor. + * + * A pipe is a unidirectional data channel that can be used for interprocess + * It uses blocking read and write operations, so it can't be used in interrupt context. + * + * @return The file descriptor if successful, -1 if error happens. + */ +int pipe(int fildes[2]); + +#ifdef __cplusplus +} +#endif diff --git a/components/vfs/vfs_pipe.c b/components/vfs/vfs_pipe.c new file mode 100644 index 000000000000..fb5f503462af --- /dev/null +++ b/components/vfs/vfs_pipe.c @@ -0,0 +1,629 @@ +/* + * SPDX-FileCopyrightText: 2021-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +#include "esp_vfs_pipe.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "freertos/idf_additions.h" +#include "sys/queue.h" + +#include "esp_err.h" +#include "esp_log.h" +#include "esp_vfs.h" +#include "freertos/FreeRTOS.h" +#include "freertos/portmacro.h" + +#define FD_INVALID -1 + +#define VFS_PIPE_DEBUG(tag, fmt, ...) +//#define VFS_PIPE_DEBUG(tag, fmt, ...) fprintf(stderr, "[%s] " fmt "\n", pcTaskGetName(NULL), ##__VA_ARGS__) + + +/* + * This struct holds the arguments for a pending select() call on a pipe fd. + * There can be one on the write part of the pipe, and one on the read part. + * Its part of two linked lists: + * - a single linked list in end_select_args containing all pending selects in this select call + * - a double linked list in pipe_context_t::select_args containing all pending selects on this fd + * + */ +typedef struct event_select_args_t { + int local_fd; // Encoded file descriptor value (with WRITE_BIT); index in s_pipes is derived via FD_TO_IDX(local_fd) + fd_set *read_fds; // This is where to set the fd if data is available + fd_set *error_fds; // This is where to set the fd if an error occurs + + esp_vfs_select_sem_t signal_sem; + + // linked list node in pipe_context_t::select_args + LIST_ENTRY(event_select_args_t) in_fd; + + // linked list node in end_select_args + SLIST_ENTRY(event_select_args_t) in_args; +} pipe_select_args_t; + +// Offset to add to internal fd to get the fd returned by eventfd() +#define WRITE_BIT 0x1 + +#define IS_WRITE(fd) ((fd) & WRITE_BIT) +#define IS_READ(fd) (!IS_WRITE(fd)) +#define FD_TO_IDX(fd) ((fd) >> 1) +#define IDX_TO_FD_READ(fd) (((fd) << 1) | (0 & WRITE_BIT)) +#define IDX_TO_FD_WRITE(fd) (((fd) << 1) | (1 & WRITE_BIT)) + +typedef struct { + int read_fd; + int write_fd; + int read_flags; + int write_flags; + const void *data; + size_t data_size; + // a double-linked list head for all pending select args with this fd + LIST_HEAD(select_args_list, event_select_args_t) select_args; + _lock_t lock; + TaskHandle_t writer_task; + TaskHandle_t reader_task; +} pipe_context_t; + +esp_vfs_id_t s_pipe_vfs_id = -1; + +static pipe_context_t *s_pipes; +static size_t s_pipes_size; + +static void trigger_select_for_event(pipe_context_t *pipe) +{ + pipe_select_args_t *select_args; + LIST_FOREACH(select_args, &pipe->select_args, in_fd) { + esp_vfs_select_triggered(select_args->signal_sem); + } +} + +#ifdef CONFIG_VFS_SUPPORT_SELECT +static esp_err_t pipe_start_select(int nfds, + fd_set *readfds, + fd_set *writefds, + fd_set *exceptfds, + esp_vfs_select_sem_t signal_sem, + void **end_select_args) +{ + esp_err_t error = ESP_OK; + bool should_trigger_directly = false; + nfds = nfds < s_pipes_size ? nfds : (int)s_pipes_size; + + // Use the first entry as the head of the list + pipe_select_args_t *select_args_list = NULL; + + VFS_PIPE_DEBUG(TAG, "pipe_start_select(nfds=%d)", nfds); + + for (int local_fd = 0; local_fd < nfds; local_fd++) { + size_t pipe_idx = FD_TO_IDX(local_fd); + if (pipe_idx >= s_pipes_size) { + continue; + } + if (!FD_ISSET(local_fd, readfds) && + !FD_ISSET(local_fd, writefds) && + !FD_ISSET(local_fd, exceptfds)) { + continue; + } + + pipe_context_t *pipe = &s_pipes[pipe_idx]; + _lock_acquire(&pipe->lock); + + // This is the context for this specific fd in this select() call + pipe_select_args_t *event_select_args = + (pipe_select_args_t *)calloc(1, sizeof(pipe_select_args_t)); + if (!event_select_args) { + error = ESP_ERR_NO_MEM; + _lock_release(&pipe->lock); + break; + } + event_select_args->local_fd = local_fd; + event_select_args->signal_sem = signal_sem; + + // Exceptions occurs if you wait for a pipe that is closed + if (FD_ISSET(local_fd, exceptfds)) { + event_select_args->error_fds = exceptfds; // Copy, so we can update later + // If any sides of the pipe is closed, or we are waiting on the write pipe, we need to trigger directly + if ((pipe->read_fd == FD_INVALID || pipe->write_fd == FD_INVALID) || IS_WRITE(local_fd)) { + should_trigger_directly = true; + } + } + + + // event fds are always writable, so trigger directly + // marking writefd as ready. + if (FD_ISSET(local_fd, writefds)) { + should_trigger_directly = true; + } + + // If we are waiting for read, check if there is data already available + if (FD_ISSET(local_fd, readfds)) { + event_select_args->read_fds = readfds; // Copy, so we can update later + + // If there is data already, or we are reading from the write end, we need to trigger directly + if (pipe->data_size > 0 || IS_WRITE(local_fd)) { + should_trigger_directly = true; + } + } + + // Insert into the per-fd linked list (LIST) + // This is used for triggering the select when a write occur on this + // event fd. This is so we know what to trigger when a write occurs + LIST_INSERT_HEAD(&pipe->select_args, event_select_args, in_fd); + + // Insert into the per-select-call singly-linked list + // Use the first entry as the head - simple pointer chaining + SLIST_NEXT(event_select_args, in_args) = select_args_list; + select_args_list = event_select_args; + + + _lock_release(&pipe->lock); + } + + if (error != ESP_OK) { + // Cleanup any allocated select args + pipe_select_args_t *select_args = select_args_list; + while (select_args != NULL) { + size_t pipe_idx = FD_TO_IDX(select_args->local_fd); + if (pipe_idx < s_pipes_size) { + pipe_context_t *pipe = &s_pipes[pipe_idx]; + _lock_acquire(&pipe->lock); + // Remove from the per-fd list + LIST_REMOVE(select_args, in_fd); + _lock_release(&pipe->lock); + } + pipe_select_args_t *next = SLIST_NEXT(select_args, in_args); + free(select_args); + select_args = next; + } + select_args_list = NULL; + } + + + *end_select_args = select_args_list; + + if (should_trigger_directly) { + esp_vfs_select_triggered(signal_sem); + } + + return error; +} + +static esp_err_t pipe_end_select(void *end_select_args) +{ + pipe_select_args_t *select_args = (pipe_select_args_t *)end_select_args; + + while (select_args != NULL) { + size_t pipe_idx = FD_TO_IDX(select_args->local_fd); + if (pipe_idx >= s_pipes_size) { + continue; + } + VFS_PIPE_DEBUG(TAG, "pipe_end_select: ending select local_fd: %d pipe_idx: %zu is_write: %d", select_args->local_fd, pipe_idx, IS_WRITE(select_args->local_fd)); + pipe_context_t *pipe = &s_pipes[pipe_idx]; + + _lock_acquire(&pipe->lock); + VFS_PIPE_DEBUG(TAG, "pipe_end_select: pipe read_fd: %d write_fd: %d", pipe->read_fd, pipe->write_fd); + + if (select_args->error_fds) { + // If any of the ends of the pipe is closed, set error + if ((pipe->read_fd == FD_INVALID || pipe->write_fd == FD_INVALID) || IS_WRITE(select_args->local_fd)) { + + VFS_PIPE_DEBUG(TAG, "pipe_end_select: setting error fd local_fd: %d pipe_idx: %zu", select_args->local_fd, pipe_idx); + FD_SET(select_args->local_fd, select_args->error_fds); + } else { + VFS_PIPE_DEBUG(TAG, "pipe_end_select: clearing error fd local_fd: %d pipe_idx: %zu", select_args->local_fd, pipe_idx); + FD_CLR(select_args->local_fd, select_args->error_fds); + } + } + + // Select fd from the arg, as it might be closed now + if (select_args->read_fds) { + if (pipe->data_size > 0 || IS_WRITE(select_args->local_fd)) { + + VFS_PIPE_DEBUG(TAG, "pipe_end_select setting read fd local_fd: %d pipe_idx: %zu", select_args->local_fd, pipe_idx); + FD_SET(select_args->local_fd, select_args->read_fds); + } else { + VFS_PIPE_DEBUG(TAG, "pipe_end_select clearing read fd local_fd: %d pipe_idx: %zu", select_args->local_fd, pipe_idx); + FD_CLR(select_args->local_fd, select_args->read_fds); + } + } + + // Remove from the per-fd list + LIST_REMOVE(select_args, in_fd); + + _lock_release(&pipe->lock); + + // Get next before freeing current + pipe_select_args_t *next = SLIST_NEXT(select_args, in_args); + free(select_args); + select_args = next; + } + + return ESP_OK; +} +#endif // CONFIG_VFS_SUPPORT_SELECT + + +// fd, will be the internal fd of the eventfd +static ssize_t pipe_write(int fd, const void *data, size_t size) +{ + + if (!IS_WRITE(fd)) { + errno = EBADF; + return -1; + } + + // get the internal fd, by removing the offset + const size_t idx = FD_TO_IDX(fd); + + if (idx >= s_pipes_size || data == NULL) { + errno = EINVAL; + return -1; + } + + // NOTE! This will cause deadlock if output goes to this pipe_write + VFS_PIPE_DEBUG(TAG, "pipe_write(local_fd=%d, idx=%zu size=%zu)", fd, idx, size); + + pipe_context_t *pipe = &s_pipes[idx]; + _lock_acquire(&pipe->lock); + + if (pipe->read_fd == FD_INVALID || pipe->write_fd == FD_INVALID) { + // Reader has closed the pipe + errno = EPIPE; + VFS_PIPE_DEBUG(TAG, "pipe_write: pipe closed"); + _lock_release(&pipe->lock); + return -1; + } + + if (pipe->writer_task != NULL) { + // There is already a writer blocked on this pipe + errno = EBUSY; + VFS_PIPE_DEBUG(TAG, "pipe_write: pipe busy"); + _lock_release(&pipe->lock); + return -1; + } + + // If we run in non-blocking mode, we need a reader to stand by reading. + if (pipe->write_flags & O_NONBLOCK && pipe->reader_task == NULL) { + // No reader is waiting, and we are in non-blocking mode + errno = EAGAIN; + _lock_release(&pipe->lock); + VFS_PIPE_DEBUG(TAG, "pipe_write: no reader"); + return -1; + } + + pipe->data = data; + pipe->data_size = size; + pipe->writer_task = xTaskGetCurrentTaskHandle(); + + if (pipe->reader_task) { + // Notify the reader that data is available + const TaskHandle_t reader_task = pipe->reader_task; + pipe->reader_task = NULL; + VFS_PIPE_DEBUG(TAG, "pipe_write: notifying reader xTaskNotifyGive"); + xTaskNotifyGive(reader_task); + } + + // Wake up the select waiting for this pipe + trigger_select_for_event(pipe); + + _lock_release(&pipe->lock); + + VFS_PIPE_DEBUG(TAG, "pipe_write: waiting for reader to read data ulTaskNotifyTake"); + + // Block this task here, until the reader has read the data + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + + VFS_PIPE_DEBUG(TAG, "pipe_write: data read completed"); + return size; +} + +static ssize_t pipe_read(int fd, void *data, size_t size) +{ + if (!IS_READ(fd)) { + errno = EBADF; + VFS_PIPE_DEBUG(TAG, "pipe_read: bad file descriptor"); + return -1; + } + + // get the internal fd, by removing the offset + const size_t idx = FD_TO_IDX(fd); + + if (idx >= s_pipes_size || data == NULL) { + errno = EINVAL; + VFS_PIPE_DEBUG(TAG, "pipe_read: invalid argument"); + return -1; + } + + VFS_PIPE_DEBUG(TAG, "pipe_read(local_fd=%d, idx=%zu size=%zu)", fd, idx, size); + + pipe_context_t *pipe = &s_pipes[idx]; + + _lock_acquire(&pipe->lock); + +pipe_read_restart: + + // Make sure socket is still open + if (pipe->write_fd == FD_INVALID || pipe->read_fd == FD_INVALID) { + // Writer has closed the pipe + _lock_release(&pipe->lock); + errno = EPIPE; + VFS_PIPE_DEBUG(TAG, "pipe_read: writer has closed the pipe"); + return -1; + } + + if (pipe->data_size == 0) { + // No data available + if (pipe->read_flags & O_NONBLOCK) { + // Non-blocking mode, return EAGAIN + _lock_release(&pipe->lock); + errno = EAGAIN; + VFS_PIPE_DEBUG(TAG, "pipe_read: no data available (non-blocking)"); + return -1; + } else { + // Blocking mode, wait for data to be written + pipe->reader_task = xTaskGetCurrentTaskHandle(); + _lock_release(&pipe->lock); + VFS_PIPE_DEBUG(TAG, "pipe_read: waiting for data to be written ulTaskNotifyTake"); + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + _lock_acquire(&pipe->lock); + pipe->reader_task = NULL; + + // Things might have changed while we were blocked, so restart all checks + goto pipe_read_restart; + } + } + + ssize_t ret = (pipe->data_size < size) ? pipe->data_size : size; + memcpy(data, pipe->data, ret); + + // Move the pointer offset forward as we read. + pipe->data += ret; + pipe->data_size -= ret; + + if (pipe->data_size == 0) { + pipe->data = NULL; + // Notify the writer that the data has been read + if (pipe->writer_task) { + VFS_PIPE_DEBUG(TAG, "pipe_read: notifying writer xTaskNotifyGive"); + const TaskHandle_t writer_task = pipe->writer_task; + pipe->writer_task = NULL; + xTaskNotifyGive(writer_task); + } + } + + VFS_PIPE_DEBUG(TAG, "pipe_read: data read %zu bytes completed, %zu bytes left", ret, pipe->data_size); + _lock_release(&pipe->lock); + + return ret; +} + +static int pipe_fcntl(int fd, int cmd, int flags) +{ + + // get the internal fd, by removing the offset + const size_t idx = FD_TO_IDX(fd); + + if (idx >= s_pipes_size) { + errno = EINVAL; + return -1; + } + + VFS_PIPE_DEBUG(TAG, "pipe_fcntl(local_fd=%d, idx=%zu flags=%d)", fd, idx, flags); + pipe_context_t *pipe = &s_pipes[idx]; + _lock_acquire(&pipe->lock); + + int res = -1; + if (cmd == F_GETFL) { + if (IS_READ(fd)) { + res = pipe->read_flags; + } else if (IS_WRITE(fd)) { + res = pipe->write_flags; + } else { + errno = EINVAL; + } + } else if (cmd == F_SETFL && (flags & O_NONBLOCK)) { + if (IS_READ(fd)) { + pipe->read_flags |= O_NONBLOCK; + res = 0; + } else { + pipe->write_flags |= O_NONBLOCK; + res = 0; + } + } else if (cmd == F_SETFL && !(flags & O_NONBLOCK)) { + if (IS_READ(fd)) { + pipe->read_flags &= ~O_NONBLOCK; + res = 0; + } else { + pipe->write_flags &= ~O_NONBLOCK; + res = 0; + } + } else { + errno = EINVAL; + } + + _lock_release(&pipe->lock); + return res; +} + +static int pipe_close(int fd) +{ + + // get the internal fd, by removing the offset + const size_t idx = FD_TO_IDX(fd); + + if (idx >= s_pipes_size) { + errno = EINVAL; + return -1; + } + + VFS_PIPE_DEBUG(TAG, "pipe_close(local_fd=%d, idx=%zu)", fd, idx); + + pipe_context_t *pipe = &s_pipes[idx]; + _lock_acquire(&pipe->lock); + int res = 0; + + if (IS_WRITE(fd)) { + if (pipe->write_fd == FD_INVALID) { + VFS_PIPE_DEBUG(TAG, "pipe_close: write fd=%d is already closed", pipe->write_fd); + res = -1; + } + pipe->write_fd = FD_INVALID; + + } else { + if (pipe->read_fd == FD_INVALID) { + VFS_PIPE_DEBUG(TAG, "pipe_close: read fd=%d is already closed", pipe->read_fd); + res = -1; + } + pipe->read_fd = FD_INVALID; + } + + if (pipe->writer_task) { + // Notify the writer that the pipe is closed + const TaskHandle_t writer_task = pipe->writer_task; + pipe->writer_task = NULL; + xTaskNotifyGive(writer_task); + } + if (pipe->reader_task) { + // Notify the reader that the pipe is closed + const TaskHandle_t reader_task = pipe->reader_task; + pipe->reader_task = NULL; + xTaskNotifyGive(reader_task); + } + + + trigger_select_for_event(pipe); + + _lock_release(&pipe->lock); + + return res; +} + +esp_err_t esp_vfs_pipe_register(const esp_vfs_pipe_config_t *config) +{ + if (config == NULL || config->max_fds >= MAX_FDS) { + return ESP_ERR_INVALID_ARG; + } + if (s_pipe_vfs_id != -1) { + return ESP_ERR_INVALID_STATE; + } + + s_pipes_size = config->max_fds; + s_pipes = (pipe_context_t *)calloc(s_pipes_size, sizeof(pipe_context_t)); + for (size_t i = 0; i < s_pipes_size; i++) { + _lock_init(&s_pipes[i].lock); + s_pipes[i].read_fd = FD_INVALID; + s_pipes[i].write_fd = FD_INVALID; + } + + esp_vfs_t vfs = { + .flags = ESP_VFS_FLAG_DEFAULT, + .write = &pipe_write, + .close = &pipe_close, + .read = &pipe_read, + .fcntl = &pipe_fcntl, +#ifdef CONFIG_VFS_SUPPORT_SELECT + .start_select = &pipe_start_select, + .end_select = &pipe_end_select, +#endif + }; + return esp_vfs_register_with_id(&vfs, NULL, &s_pipe_vfs_id); +} + +esp_err_t esp_vfs_pipe_unregister(void) +{ + if (s_pipe_vfs_id == -1) { + return ESP_ERR_INVALID_STATE; + } + esp_err_t error = esp_vfs_unregister_with_id(s_pipe_vfs_id); + if (error == ESP_OK) { + s_pipe_vfs_id = -1; + } + for (size_t i = 0; i < s_pipes_size; i++) { + _lock_close(&s_pipes[i].lock); + } + free(s_pipes); + return error; +} + +int pipe(int fildes[2]) +{ + fildes[0] = FD_INVALID; + fildes[1] = FD_INVALID; + + int global_read_fd = FD_INVALID; + int global_write_fd = FD_INVALID; + esp_err_t error = ESP_OK; + + if (s_pipe_vfs_id == -1) { + errno = EACCES; + return FD_INVALID; + } + + for (size_t i = 0; i < s_pipes_size; i++) { + _lock_acquire(&s_pipes[i].lock); + + // Make sure both ends of the pipe are free + if (s_pipes[i].read_fd == FD_INVALID && s_pipes[i].write_fd == FD_INVALID) { + + error = esp_vfs_register_fd_with_local_fd(s_pipe_vfs_id, IDX_TO_FD_READ(i), false, &global_read_fd); + if (error != ESP_OK) { + _lock_release(&s_pipes[i].lock); + break; + } + error = esp_vfs_register_fd_with_local_fd(s_pipe_vfs_id, IDX_TO_FD_WRITE(i), false, &global_write_fd); + if (error != ESP_OK) { + // Cleanup the read fd registration on failure + esp_vfs_unregister_fd(s_pipe_vfs_id, global_read_fd); + _lock_release(&s_pipes[i].lock); + break; + } + VFS_PIPE_DEBUG(TAG, "pipe() -> read fd=%d local_fd=%d, write fd=%d local_fd=%d", global_read_fd, IDX_TO_FD_READ(i), global_write_fd, IDX_TO_FD_WRITE(i)); + + // Mark pipe as used BEFORE setting the global fd values + // This prevents another thread from grabbing this slot + s_pipes[i].read_fd = global_read_fd; + s_pipes[i].write_fd = global_write_fd; + + fildes[0] = global_read_fd; + fildes[1] = global_write_fd; + s_pipes[i].data = NULL; + s_pipes[i].data_size = 0; + LIST_INIT(&s_pipes[i].select_args); + s_pipes[i].writer_task = NULL; + s_pipes[i].reader_task = NULL; + s_pipes[i].read_flags = O_RDONLY; + s_pipes[i].write_flags = O_WRONLY; + _lock_release(&s_pipes[i].lock); + break; + } + _lock_release(&s_pipes[i].lock); + } + + switch (error) { + case ESP_OK: + errno = 0; + return 0; + case ESP_ERR_NO_MEM: + errno = ENOMEM; + return -1; + case ESP_ERR_INVALID_ARG: + errno = EINVAL; + return -1; + default: + errno = EIO; + return -1; + } +} From 847cbf511f8f19d7c37847b634ae53f40a89f536 Mon Sep 17 00:00:00 2001 From: Jimmy Wennlund Date: Thu, 10 Apr 2025 11:21:39 +0200 Subject: [PATCH 3/6] feat(console): add API to access registered commands Add new functions to retrieve and iterate through registered console commands: - esp_console_get_by_name(): Find command by name - esp_console_get_iterate(): Iterate through all commands This enables programmatic access to the console command registry, useful for dynamic command introspection and tooling. Refactored internal cmd_item_t structure to embed esp_console_cmd_t for cleaner API implementation. --- components/console/commands.c | 95 ++++++++++++++++---------------- components/console/esp_console.h | 23 ++++++++ 2 files changed, 70 insertions(+), 48 deletions(-) diff --git a/components/console/commands.c b/components/console/commands.c index b3bb082c6d3c..b681264c3b1a 100644 --- a/components/console/commands.c +++ b/components/console/commands.c @@ -19,24 +19,9 @@ #define ANSI_COLOR_DEFAULT 39 /** Default foreground color */ typedef struct cmd_item_ { - /** - * Command name (statically allocated by application) - */ - const char *command; - /** - * Help text (statically allocated by application), may be NULL. - */ - const char *help; - /** - * Hint text, usually lists possible arguments, dynamically allocated. - * May be NULL. - */ + esp_console_cmd_t def; char *hint; - esp_console_cmd_func_t func; //!< pointer to the command handler (without user context) - esp_console_cmd_func_with_context_t func_w_context; //!< pointer to the command handler (with user context) - void *argtable; //!< optional pointer to arg table - void *context; //!< optional pointer to user context - SLIST_ENTRY(cmd_item_) next; //!< next command in the list + SLIST_ENTRY(cmd_item_) next; //!< next command in the list } cmd_item_t; typedef void (*const fn_print_arg_t)(cmd_item_t*); @@ -56,6 +41,29 @@ static const cmd_item_t *find_command_by_name(const char *name); static esp_console_help_verbose_level_e s_verbose_level = ESP_CONSOLE_HELP_VERBOSE_LEVEL_1; +const esp_console_cmd_t *esp_console_get_by_name(const char *name) +{ + const cmd_item_t *cmd = find_command_by_name(name); + if (cmd) { + return &cmd->def; + } + return NULL; +} + +const esp_console_cmd_t *esp_console_get_iterate(const esp_console_cmd_t *prev) +{ + const cmd_item_t *cmd = NULL; + if (prev == NULL) { + cmd = SLIST_FIRST(&s_cmd_list); + } else { + cmd = SLIST_NEXT((cmd_item_t *)prev, next); + } + if (cmd) { + return &cmd->def; + } + return NULL; +} + esp_err_t esp_console_init(const esp_console_config_t *config) { if (!config) { @@ -135,14 +143,14 @@ esp_err_t esp_console_cmd_register(const esp_console_cmd_t *cmd) // remove from list and free the old hint, because we will alloc new hint for the command esp_console_rm_item_free_hint(item); } - item->command = cmd->command; - item->help = cmd->help; + + item->def = *cmd; if (cmd->hint) { /* Prepend a space before the hint. It separates command name and * the hint. arg_print_syntax below adds this space as well. */ int unused __attribute__((unused)); - unused = asprintf(&item->hint, " %s", cmd->hint); + unused = asprintf((char **)&item->hint, " %s", cmd->hint); } else if (cmd->argtable) { /* Generate hint based on cmd->argtable */ arg_dstr_t ds = arg_dstr_create(); @@ -150,22 +158,13 @@ esp_err_t esp_console_cmd_register(const esp_console_cmd_t *cmd) item->hint = strdup(arg_dstr_cstr(ds)); arg_dstr_destroy(ds); } - item->argtable = cmd->argtable; - - if (cmd->func) { - item->func = cmd->func; - } else { - // cmd->func_w_context is valid here according to check above - item->func_w_context = cmd->func_w_context; - item->context = cmd->context; - } cmd_item_t *last; cmd_item_t *it; #if CONFIG_CONSOLE_SORTED_HELP last = NULL; SLIST_FOREACH(it, &s_cmd_list, next) { - if (strcmp(it->command, item->command) > 0) { + if (strcmp(it->def.command, item->def.command) > 0) { break; } last = it; @@ -195,8 +194,8 @@ void esp_console_get_completion(const char *buf, linenoiseCompletions *lc) cmd_item_t *it; SLIST_FOREACH(it, &s_cmd_list, next) { /* Check if command starts with buf */ - if (strncmp(buf, it->command, len) == 0) { - linenoiseAddCompletion(lc, it->command); + if (strncmp(buf, it->def.command, len) == 0) { + linenoiseAddCompletion(lc, it->def.command); } } } @@ -206,8 +205,8 @@ const char *esp_console_get_hint(const char *buf, int *color, int *bold) size_t len = strlen(buf); cmd_item_t *it; SLIST_FOREACH(it, &s_cmd_list, next) { - if (strlen(it->command) == len && - strncmp(buf, it->command, len) == 0) { + if (strlen(it->def.command) == len && + strncmp(buf, it->def.command, len) == 0) { *color = s_config.hint_color; *bold = s_config.hint_bold; return it->hint; @@ -222,8 +221,8 @@ static const cmd_item_t *find_command_by_name(const char *name) cmd_item_t *it; size_t len = strlen(name); SLIST_FOREACH(it, &s_cmd_list, next) { - if (strlen(it->command) == len && - strcmp(name, it->command) == 0) { + if (strlen(it->def.command) == len && + strcmp(name, it->def.command) == 0) { cmd = it; break; } @@ -253,11 +252,11 @@ esp_err_t esp_console_run(const char *cmdline, int *cmd_ret) free(argv); return ESP_ERR_NOT_FOUND; } - if (cmd->func) { - *cmd_ret = (*cmd->func)(argc, argv); + if (cmd->def.func) { + *cmd_ret = (*cmd->def.func)(argc, argv); } - if (cmd->func_w_context) { - *cmd_ret = (*cmd->func_w_context)(cmd->context, argc, argv); + if (cmd->def.func_w_context) { + *cmd_ret = (*cmd->def.func_w_context)(cmd->def.context, argc, argv); } free(argv); return ESP_OK; @@ -275,16 +274,16 @@ static void print_arg_help(cmd_item_t *it) * Pad all the hints to the same column */ const char *hint = (it->hint) ? it->hint : ""; - printf("%-s %s\n", it->command, hint); + printf("%-s %s\n", it->def.command, hint); /* Second line: print help. * Argtable has a nice helper function for this which does line * wrapping. */ printf(" "); // arg_print_formatted does not indent the first line - arg_print_formatted(stdout, 2, 78, it->help); + arg_print_formatted(stdout, 2, 78, it->def.help); /* Finally, print the list of arguments */ - if (it->argtable) { - arg_print_glossary(stdout, (void **) it->argtable, " %12s %s\n"); + if (it->def.argtable) { + arg_print_glossary(stdout, (void **) it->def.argtable, " %12s %s\n"); } printf("\n"); } @@ -292,7 +291,7 @@ static void print_arg_help(cmd_item_t *it) static void print_arg_command(cmd_item_t *it) { const char *hint = (it->hint) ? it->hint : ""; - printf("%-s %s\n\n", it->command, hint); + printf("%-s %s\n\n", it->def.command, hint); } static fn_print_arg_t print_verbose_level_arr[ESP_CONSOLE_HELP_VERBOSE_LEVEL_MAX_NUM] = { @@ -330,7 +329,7 @@ static int help_command(int argc, char **argv) /* Print info of each command based on verbose level */ SLIST_FOREACH(it, &s_cmd_list, next) { - if (it->help == NULL) { + if (it->def.help == NULL) { continue; } print_verbose_level_arr[verbose_level](it); @@ -340,10 +339,10 @@ static int help_command(int argc, char **argv) /* Print summary of given command, verbose option will be ignored */ bool found_command = false; SLIST_FOREACH(it, &s_cmd_list, next) { - if (it->help == NULL) { + if (it->def.help == NULL) { continue; } - if (strcmp(help_args.help_cmd->sval[0], it->command) == 0) { + if (strcmp(help_args.help_cmd->sval[0], it->def.command) == 0) { print_arg_help(it); found_command = true; ret_value = 0; diff --git a/components/console/esp_console.h b/components/console/esp_console.h index c13234ce5140..1778302dc24f 100644 --- a/components/console/esp_console.h +++ b/components/console/esp_console.h @@ -236,6 +236,29 @@ typedef struct { */ esp_err_t esp_console_cmd_register(const esp_console_cmd_t *cmd); +/** + * @brief Retrieve a registered console command by its name. + * + * This function searches for a console command that matches the given name + * and returns a pointer to its description structure. + * + * @param name Name of the command to search for. + * @return Pointer to the `esp_console_cmd_t` structure if the command is found, + * or NULL if no command with the given name is registered. + */ +const esp_console_cmd_t *esp_console_get_by_name(const char *name); + +/** + * @brief Iterate through registered console commands. + * + * This function retrieves the next registered console command in the list. + * If `prev` is NULL, it returns the first registered command. + * + * @param prev Pointer to the previous command. Pass NULL to get the first command. + * @return Pointer to the next `esp_console_cmd_t` structure, or NULL if no more commands are available. + */ +const esp_console_cmd_t *esp_console_get_iterate(const esp_console_cmd_t *prev); + /** * @brief Deregister console command * @param cmd_name Name of the command to be deregistered. Must not be NULL, must not contain spaces. From 0657b05743583835470264654feac3ba8979371c Mon Sep 17 00:00:00 2001 From: Jimmy Wennlund Date: Thu, 16 Oct 2025 22:02:07 +0200 Subject: [PATCH 4/6] feat(console): add support for running commands on separate tasks Add asynchronous command execution with I/O redirection: - New esp_console_run_on_task() API to execute commands on FreeRTOS tasks - Support for custom stdin/stdout/stderr file pointers - Configurable stack size and priority per command - Task lifecycle management functions (wait, query status, cleanup) - Optional CONSOLE_COMMAND_ON_TASK config to enable feature This enables non-blocking command execution, pipelines, and better resource management for long-running console commands. --- components/console/Kconfig | 21 ++++ components/console/commands.c | 174 +++++++++++++++++++++++++++++++ components/console/esp_console.h | 103 ++++++++++++++++++ 3 files changed, 298 insertions(+) diff --git a/components/console/Kconfig b/components/console/Kconfig index 6f375200f2ae..738bd1115f6e 100644 --- a/components/console/Kconfig +++ b/components/console/Kconfig @@ -7,4 +7,25 @@ menu "Console Library" Instead of listing the commands in the order of registration, the help command lists the available commands in sorted order, if this option is enabled. + config CONSOLE_COMMAND_ON_TASK + bool "Enable command on task" + default n + help + In addition to run the command on current task, the console can also run commands on a dedicated + task. This allows for longer running commands without blocking the console input, pipelines, and + keeping the original task stack smaller. + + config CONSOLE_COMMAND_DEFAULT_TASK_STACK_SIZE + int "Console default task stack size" + default 4096 + depends on CONSOLE_COMMAND_ON_TASK + help + Default stack size for the console command task, unless overridden. + + config CONSOLE_COMMAND_DEFAULT_TASK_PRIORITY + int "Console default task priority" + default 5 + depends on CONSOLE_COMMAND_ON_TASK + help + Default priority for the console command task, unless overridden. endmenu diff --git a/components/console/commands.c b/components/console/commands.c index b681264c3b1a..916f9cd6c31c 100644 --- a/components/console/commands.c +++ b/components/console/commands.c @@ -8,10 +8,12 @@ #include #include #include +#include #include "esp_heap_caps.h" #include "esp_log.h" #include "esp_console.h" #include "esp_system.h" +#include "freertos/idf_additions.h" #include "linenoise/linenoise.h" #include "argtable3/argtable3.h" #include "sys/queue.h" @@ -262,6 +264,178 @@ esp_err_t esp_console_run(const char *cmdline, int *cmd_ret) return ESP_OK; } +#ifdef CONFIG_CONSOLE_COMMAND_ON_TASK + +typedef struct esp_console_task_handle { + const cmd_item_t *cmd; //!< Pointer to the command definition + TaskHandle_t task_handle; //!< Handle of the created task (protected by lock) + FILE *_stdin; //!< Pipe for command input + FILE *_stdout; //!< Pipe for command output + FILE *_stderr; //!< Pipe for command error output + uint8_t flags; //!< Flags for task execution + int exit_code; //!< Exit code of the command (protected by lock) + _lock_t lock; //!< Lock to protect task_handle and exit_code + size_t argc; //!< Number of command line arguments + char *argv[0]; //!< Command line arguments (flexible array member) +} esp_console_task_handle_t; + +static void task_cmd(void *arg) +{ + esp_console_task_handle_t *task = (esp_console_task_handle_t *)arg; + + if (task->_stdin) { + __getreent()->_stdin = task->_stdin; + } + if (task->_stdout) { + __getreent()->_stdout = task->_stdout; + } + if (task->_stderr) { + __getreent()->_stderr = task->_stderr; + } + + int exit_code = -1; + if (task->cmd->def.func) { + exit_code = task->cmd->def.func(task->argc, task->argv); + } + if (task->cmd->def.func_w_context) { + exit_code = (*task->cmd->def.func_w_context)(task->cmd->def.context, task->argc, task->argv); + } + + if (task->flags & ESP_CONSOLE_TASK_CLOSE_STDIN) { + fclose(__getreent()->_stdin); + } + + if (task->flags & ESP_CONSOLE_TASK_CLOSE_STDOUT) { + fclose(__getreent()->_stdout); + } + + if (task->flags & ESP_CONSOLE_TASK_CLOSE_STDERR) { + fclose(__getreent()->_stderr); + } + __getreent()->_stdin = _REENT_STDIN(_GLOBAL_REENT); + __getreent()->_stdout = _REENT_STDOUT(_GLOBAL_REENT); + __getreent()->_stderr = _REENT_STDERR(_GLOBAL_REENT); + + __lock_acquire(task->lock); + task->exit_code = exit_code; + task->task_handle = NULL; + __lock_release(task->lock); + + vTaskDelete(NULL); +} + +esp_err_t esp_console_run_on_task(const char *cmdline, FILE *_stdin, FILE *_stdout, FILE *_stderr, uint8_t flags, esp_console_task_handle_t **out_task) +{ + const size_t cmd_len = strlen(cmdline); + if (!cmd_len || cmd_len >= s_config.max_cmdline_length) { + return ESP_ERR_INVALID_ARG; + } + + // Try to do all memory allocations in one go + // Calculate the size of each component for clarity and maintainability + const size_t task_struct_size = sizeof(esp_console_task_handle_t); // Size of the task struct + const size_t argv_array_size = sizeof(char *) * s_config.max_cmdline_args; // Size of argv array + const size_t cmdline_buf_size = cmd_len + 1; // Size of command line buffer (including null terminator) + const size_t total_size = task_struct_size + argv_array_size + cmdline_buf_size; + + esp_console_task_handle_t *task = (esp_console_task_handle_t *) heap_caps_calloc(1, total_size, s_config.heap_alloc_caps); + if (task == NULL) { + return ESP_ERR_NO_MEM; + } + + // The line buffer is placed after the struct and argv array + char *line_buf = (char *)(((char *)task) + task_struct_size + argv_array_size); + strlcpy(line_buf, cmdline, cmd_len + 1); + + task->argc = esp_console_split_argv(line_buf, task->argv, + s_config.max_cmdline_args); + if (task->argc == 0) { + free(task); + return ESP_ERR_INVALID_ARG; + } + + task->exit_code = -1; + task->cmd = find_command_by_name(task->argv[0]); + if (task->cmd == NULL) { + free(task); + return ESP_ERR_NOT_FOUND; + } + + task->_stdin = _stdin; + task->_stdout = _stdout; + task->_stderr = _stderr; + task->flags = flags; + __lock_init(task->lock); + + uint32_t stack_size = task->cmd->def.stack_size ? task->cmd->def.stack_size : (CONFIG_CONSOLE_COMMAND_DEFAULT_TASK_STACK_SIZE); + UBaseType_t priority = task->cmd->def.priority ? task->cmd->def.priority : (CONFIG_CONSOLE_COMMAND_DEFAULT_TASK_PRIORITY); + BaseType_t handle = xTaskCreate(&task_cmd, task->argv[0], stack_size, task, priority, &task->task_handle); + + if (handle != pdPASS) { + __lock_close(task->lock); + free(task); + return ESP_ERR_NO_MEM; + } + *out_task = task; + + return ESP_OK; +} + +void esp_console_task_free(esp_console_task_handle_t *task) +{ + __lock_acquire(task->lock); + TaskHandle_t handle = task->task_handle; + __lock_release(task->lock); + + if (handle) { + // Wait for the task to finish before deleting + esp_console_wait_task(task, NULL); + vTaskDelete(handle); + + __lock_acquire(task->lock); + task->task_handle = NULL; + __lock_release(task->lock); + } + __lock_close(task->lock); + free(task); +} + +bool esp_console_task_is_running(esp_console_task_handle_t *task) +{ + __lock_acquire(task->lock); + TaskHandle_t handle = task->task_handle; + __lock_release(task->lock); + + if (handle) { + return eTaskGetState(handle) != eDeleted; + } + return false; +} + +void esp_console_wait_task(esp_console_task_handle_t *task, int *cmd_ret) +{ + TaskHandle_t handle; + + __lock_acquire(task->lock); + handle = task->task_handle; + __lock_release(task->lock); + + if (handle) { + // Wait until task is done + while (eTaskGetState(handle) != eDeleted) { + vTaskDelay(10 / portTICK_PERIOD_MS); + } + } + + if (cmd_ret) { + __lock_acquire(task->lock); + *cmd_ret = task->exit_code; + __lock_release(task->lock); + } +} + +#endif // CONFIG_CONSOLE_COMMAND_ON_TASK + static struct { struct arg_str *help_cmd; struct arg_int *verbose_level; diff --git a/components/console/esp_console.h b/components/console/esp_console.h index 1778302dc24f..c1afac3d8ec5 100644 --- a/components/console/esp_console.h +++ b/components/console/esp_console.h @@ -190,6 +190,18 @@ typedef struct { * If not set, the command will not be listed in 'help' output. */ const char *help; + +#ifdef CONFIG_CONSOLE_COMMAND_ON_TASK + /** + * Stack size, if command is run on a separate task + */ + size_t stack_size; + /** + * Task priority, if command is run on a separate task + */ + UBaseType_t priority; +#endif + /** * Hint text, usually lists possible arguments. * If set to NULL, and 'argtable' field is non-NULL, hint will be generated @@ -491,6 +503,97 @@ esp_err_t esp_console_start_repl(esp_console_repl_t *repl); */ esp_err_t esp_console_stop_repl(esp_console_repl_t *repl); +#ifdef CONFIG_CONSOLE_COMMAND_ON_TASK + +/** + * @brief Opaque handle for a console task + */ +typedef struct esp_console_task_handle esp_console_task_handle_t; + +#define ESP_CONSOLE_TASK_CLOSE_STDIN (1 << 0) //!< Close stdin pipe when command task ends +#define ESP_CONSOLE_TASK_CLOSE_STDOUT (1 << 1) //!< Close stdout pipe when command task ends +#define ESP_CONSOLE_TASK_CLOSE_STDERR (1 << 2) //!< Close stderr pipe when command task ends +#define ESP_CONSOLE_TASK_CLOSE_ALL (ESP_CONSOLE_TASK_CLOSE_STDIN | ESP_CONSOLE_TASK_CLOSE_STDOUT | ESP_CONSOLE_TASK_CLOSE_STDERR) + +/** + * @brief Run a console command on a separate FreeRTOS task + * + * This function executes a console command in a new FreeRTOS task, allowing + * asynchronous execution with I/O redirection via file pointers. + * + * @param[in] cmdline Command line string (command name followed by arguments) + * @param[in] _stdin FILE pointer for standard input (or NULL to use default) + * @param[in] _stdout FILE pointer for standard output (or NULL to use default) + * @param[in] _stderr FILE pointer for standard error (or NULL to use default) + * @param[in] flags Flags to control the behavior of the task (e.g., close stdin/stdout/stderr) + * @param[out] out_task Pointer to receive the task handle. Use this handle with + * esp_console_task_is_running() and esp_console_wait_task(). + * Must be freed with esp_console_task_free() when done. + * + * @note The task will run with the stack size and priority specified in the + * command's esp_console_cmd_t structure, or defaults if not specified. + * @note The provided file pointers will be closed when the task completes, + * allowing callers to be notified of task completion (e.g., via pipe EOF). + * @note The caller is responsible for creating any pipes and managing file descriptors. + * + * @return + * - ESP_OK on success (task created successfully) + * - ESP_ERR_INVALID_STATE if esp_console_init wasn't called + * - ESP_ERR_INVALID_ARG if the command line is empty or only whitespace + * - ESP_ERR_NOT_FOUND if command with given name wasn't registered + * - ESP_ERR_NO_MEM if out of memory + */ +esp_err_t esp_console_run_on_task(const char *cmdline, FILE *_stdin, FILE *_stdout, FILE *_stderr, uint8_t flags, esp_console_task_handle_t **out_task); + +/** + * @brief Free resources associated with a console task + * + * This function frees the memory allocated for the task handle and associated + * resources. If the task is still running, it will be deleted. + * + * @param[in] task Task handle returned by esp_console_run_on_task() + * + * @note Always call this function after you're done with a task handle to + * prevent memory leaks. + * @note It's safe to call this on a task that has already finished. + */ +void esp_console_task_free(esp_console_task_handle_t *task); + +/** + * @brief Wait for a console task to complete + * + * This function blocks until the specified console task has finished execution. + * + * @param[in] task Task handle returned by esp_console_run_on_task() + * @param[out] cmd_ret Pointer to store the command's return code. Can be NULL + * if return code is not needed. + * + * @note This function will block until the task completes. Use + * esp_console_task_is_running() for non-blocking status checks. + * @note The task handle is not freed by this function, so call + * esp_console_task_free() afterwards. + */ +void esp_console_wait_task(esp_console_task_handle_t *task, int *cmd_ret); + +/** + * @brief Check if a console task is still running + * + * This function provides a non-blocking way to check the status of a console task. + * + * @param[in] task Task handle returned by esp_console_run_on_task() + * + * @return + * - true if the task is still running + * - false if the task has completed or the handle is invalid + * + * @note This function does not block and returns immediately. + * @note Use this in a loop with vTaskDelay() to poll for task completion, or + * use esp_console_wait_task() for blocking wait. + */ +bool esp_console_task_is_running(esp_console_task_handle_t *task); + +#endif // CONFIG_CONSOLE_COMMAND_ON_TASK + #ifdef __cplusplus } #endif From 91006c31c2923cf45f54324abb8a3014bcdb6608 Mon Sep 17 00:00:00 2001 From: Jimmy Wennlund Date: Wed, 29 Oct 2025 15:29:38 +0100 Subject: [PATCH 5/6] feat(console): add shell component with pipeline and operator support Add new esp_shell component providing Unix-like shell functionality: - Pipeline support with '|' operator for command chaining - File output with support for '>' or '>>' for append. - Multiple command operators: ';' (continue), '&&' (break on fail), '||' (break on success) - Asynchronous command execution using console task API - Real-time I/O handling with select() for stdin/stdout/stderr - Proper resource management and error handling - Support for Ctrl+D EOF detection, will support escaping of if a program like grep Enables complex command sequences and data flow between commands similar to traditional Unix shells. --- components/console/CMakeLists.txt | 4 + components/console/Kconfig | 8 + components/console/commands.c | 12 +- components/console/esp_shell.h | 59 +++++ components/console/shell.c | 389 ++++++++++++++++++++++++++++++ 5 files changed, 461 insertions(+), 11 deletions(-) create mode 100644 components/console/esp_shell.h create mode 100644 components/console/shell.c diff --git a/components/console/CMakeLists.txt b/components/console/CMakeLists.txt index bba6418e00ca..7753eb6ec3bb 100644 --- a/components/console/CMakeLists.txt +++ b/components/console/CMakeLists.txt @@ -6,6 +6,10 @@ set(srcs "commands.c" "split_argv.c" "linenoise/linenoise.c") +if(CONFIG_CONSOLE_SHELL_ENABLE) + list(APPEND srcs "shell.c") +endif() + set(requires vfs) if(${target} STREQUAL "linux") diff --git a/components/console/Kconfig b/components/console/Kconfig index 738bd1115f6e..78bffe9126a6 100644 --- a/components/console/Kconfig +++ b/components/console/Kconfig @@ -28,4 +28,12 @@ menu "Console Library" depends on CONSOLE_COMMAND_ON_TASK help Default priority for the console command task, unless overridden. + + config CONSOLE_SHELL_ENABLE + bool "Enable console shell" + default n + depends on CONSOLE_COMMAND_ON_TASK + help + Enable the console shell component, which provides a command line shell interface + for interacting with the console library. endmenu diff --git a/components/console/commands.c b/components/console/commands.c index 916f9cd6c31c..f5cb254972fd 100644 --- a/components/console/commands.c +++ b/components/console/commands.c @@ -384,18 +384,8 @@ esp_err_t esp_console_run_on_task(const char *cmdline, FILE *_stdin, FILE *_stdo void esp_console_task_free(esp_console_task_handle_t *task) { __lock_acquire(task->lock); - TaskHandle_t handle = task->task_handle; + assert(task->task_handle == NULL); __lock_release(task->lock); - - if (handle) { - // Wait for the task to finish before deleting - esp_console_wait_task(task, NULL); - vTaskDelete(handle); - - __lock_acquire(task->lock); - task->task_handle = NULL; - __lock_release(task->lock); - } __lock_close(task->lock); free(task); } diff --git a/components/console/esp_shell.h b/components/console/esp_shell.h new file mode 100644 index 000000000000..806bdfaa41b8 --- /dev/null +++ b/components/console/esp_shell.h @@ -0,0 +1,59 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include + +#ifdef CONFIG_CONSOLE_SHELL_ENABLE + +/** + * @brief Run a shell command line with advanced features + * + * This function provides a Unix-like shell interface with support for: + * - Command pipelines using '|' operator + * - Sequential execution using ';' operator + * - Conditional execution using '&&' (AND) and '||' (OR) operators + * - Input redirection using '<' operator + * - Output redirection using '>' and '>>' operators + * + * @param command_line Null-terminated string containing the command(s) to execute. + * The string will be modified during parsing (tokens will be null-terminated). + * @param cmd_ret Pointer to integer where the combined exit code will be stored. + * For multiple commands, non-zero exit codes are accumulated. + * + * @return + * - ESP_OK: Commands executed successfully (individual command exit codes in cmd_ret) + * - ESP_FAIL: Shell parsing or execution error + * - ESP_ERR_NO_MEM: Memory allocation failure + * - ESP_ERR_NOT_FOUND: Command not found + * - ESP_ERR_INVALID_ARG: Invalid command line arguments + * + * @note The command_line string is modified during execution and should not be reused. + * + * Example usage: + * @code{c} + * int exit_code; + * esp_err_t ret; + * + * // Simple command + * ret = esp_shell_run("help", &exit_code); + * + * // Pipeline + * ret = esp_shell_run("tasks | grep running", &exit_code); + * + * // Input/Output redirection + * ret = esp_shell_run("cat < /data/input.txt > /data/output.txt", &exit_code); + * + * // Conditional execution + * ret = esp_shell_run("mkdir /data/logs && echo 'Directory created'", &exit_code); + * + * // Sequential commands + * ret = esp_shell_run("echo 'Starting'; sleep 1; echo 'Done'", &exit_code); + * @endcode + */ +esp_err_t esp_shell_run(char *command_line, int *cmd_ret); + +#endif diff --git a/components/console/shell.c b/components/console/shell.c new file mode 100644 index 000000000000..3c4f315f5af8 --- /dev/null +++ b/components/console/shell.c @@ -0,0 +1,389 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include +#include +#include + +#include "esp_console.h" +#include "esp_err.h" +#include "esp_shell.h" + +#include "freertos/task.h" +#include "linenoise/linenoise.h" + +#define READ_BUFFER_SIZE 256 +#define MAX_TASK_DEPTH 5 +#define MAX(a,b) ((a) > (b) ? (a) : (b)) +#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof(arr[0])) + +typedef struct { + const char *command_line; + esp_console_task_handle_t *task_handle; + +} cli_task_stack_entry_t; + +static int fpipe_close(FILE *pipes[2]) +{ + int ret = 0; + if (pipes[0]) { + if (fclose(pipes[0]) != 0) { + ret = -1; + } + } + if (pipes[1]) { + if (fclose(pipes[1]) != 0) { + ret = -1; + } + } + return ret; +} + +static int fpipe(FILE *pipes[2]) +{ + int fds[2]; + if (pipe(fds) != 0) { + return -1; + } + FILE *read_end = fdopen(fds[0], "r"); + FILE *write_end = fdopen(fds[1], "w"); + if (!read_end || !write_end) { + fpipe_close(pipes); + return -1; + } + pipes[0] = read_end; + pipes[1] = write_end; + return 0; +} + +/* + * This function runs a single command line pipeline with optional output redirection. + * It handles the actual pipeline execution and I/O multiplexing. + */ +static esp_err_t esp_shell_run_pipeline(char *command_line, FILE *const input_file, FILE *const output_file, int *cmd_ret) +{ + cli_task_stack_entry_t cli_stack[MAX_TASK_DEPTH]; + int stack_index = 0; + *cmd_ret = 0; + esp_err_t err = ESP_OK; + + // Split by '|' to get individual commands in pipeline + char *saveptr = NULL; + char *token = strtok_r(command_line, "|", &saveptr); + + FILE *input_stream = input_file; + while (token) { + + // Skip leading spaces. + while (*token == ' ') { + token++; + } + + // Check if this is the last token by peeking ahead + char *saveptr_peek = saveptr; + char *next_token = strtok_r(NULL, "|", &saveptr_peek); + bool is_pipe = (next_token != NULL); + + // Skip empty tokens + if (*token == '\0') { + token = strtok_r(NULL, "|", &saveptr); + continue; + } + + if (stack_index >= ARRAY_SIZE(cli_stack)) { + fprintf(stderr, "Command stack overflow\n"); + err = ESP_FAIL; + break; + } + + // Prepare next task entry, from here we increment stack_index so we need to be careful on error handling + cli_task_stack_entry_t *task = &cli_stack[stack_index++]; + *task = (cli_task_stack_entry_t) { + .command_line = token, + .task_handle = NULL, + }; + + FILE *stdout_pipe[2] = { NULL, NULL }; + + // Create pipes for stdout, if not last command that will be directed to stdout + FILE *output_stream = output_file; + uint8_t flags = 0; + if (is_pipe) { + if (fpipe(stdout_pipe) != 0) { + fprintf(stderr, "Failed to create output pipe: %s\n", strerror(errno)); + err = ESP_ERR_NO_MEM; + stack_index--; + break; + } + output_stream = stdout_pipe[1]; + flags |= ESP_CONSOLE_TASK_CLOSE_STDOUT; + } + + if (input_stream != input_file) { + flags |= ESP_CONSOLE_TASK_CLOSE_STDIN; + } + + // If successful, we account on this function to close stdin and stdout when task is completed + err = esp_console_run_on_task(token, input_stream, output_stream, stderr, flags, &task->task_handle); + + if (err != ESP_OK) { + switch (err) { + case ESP_ERR_NOT_FOUND: + fprintf(stderr, "Unrecognized command '%s'\n", token); + break; + default: + fprintf(stderr, "Command '%s' Internal error: %s\n", token, esp_err_to_name(err)); + break; + } + fpipe_close(stdout_pipe); + stack_index--; + break; + } + + // Make sure next command's input comes from this command's output + if (is_pipe) { + input_stream = stdout_pipe[0]; + } + token = strtok_r(NULL, "|", &saveptr); + } + + // As long as there are tasks running, monitor their output and stdin + while (stack_index > 0) { + cli_task_stack_entry_t *tail_entry = &cli_stack[stack_index - 1]; + + // Use the wait function to get exit code, it will be instant if already terminated (because of exception) + int exit_code; + esp_console_wait_task(tail_entry->task_handle, &exit_code); + + if (exit_code == 0) { + //printf("Command '%s' executed successfully\n", tail_entry->command_line); + } else { + fprintf(stderr, "Command '%s' returned exit code: %d\n", tail_entry->command_line, exit_code); + // Accumulate non-zero exit codes for now, so we report failure if any command fails + *cmd_ret += exit_code; + } + + esp_console_task_free(tail_entry->task_handle); + stack_index--; + } + + return err; +} + +/* + * This function runs a single command line, which may be part of a pipeline. + * It sets up output redirection and delegates pipeline execution to esp_shell_run_pipeline. + */ +static esp_err_t esp_shell_run_single(char *command_line, int *cmd_ret) +{ + // Check for input and output redirection + char *output_redirect_pos = NULL; + char *input_redirect_pos = strstr(command_line, "<"); + char *append_pos = strstr(command_line, ">>"); + char *write_pos = strstr(command_line, ">"); + bool append_mode = false; + FILE *input_file = stdin; + FILE *output_file = stdout; + char *input_filename = NULL; + char *output_filename = NULL; + + // Handle input redirection first + if (input_redirect_pos) { + // Null-terminate the command part before '<' + *input_redirect_pos = '\0'; + + // Find the filename after '<' + input_filename = input_redirect_pos + 1; + while (*input_filename == ' ') { + input_filename++; // Skip spaces + } + + // Find end of input filename (stop at output redirection or end of string) + char *input_end = input_filename; + while (*input_end && *input_end != '>' && *input_end != ' ') { + input_end++; + } + + // Create a copy of the input filename + size_t input_len = input_end - input_filename; + char input_file_buffer[input_len + 1]; + strncpy(input_file_buffer, input_filename, input_len); + input_file_buffer[input_len] = '\0'; + + if (input_file_buffer[0] != '\0') { + + input_file = fopen(input_file_buffer, "r"); + if (!input_file) { + fprintf(stderr, "Failed to open file '%s' for reading: %s\n", input_file_buffer, strerror(errno)); + return ESP_FAIL; + } + } else { + fprintf(stderr, "No filename specified after '<'\n"); + return ESP_FAIL; + } + + // Update positions to search for output redirection after input redirection + if (*input_end) { + append_pos = strstr(input_end, ">>"); + write_pos = strstr(input_end, ">"); + } else { + append_pos = NULL; + write_pos = NULL; + } + } + + // Check for >> first (longer pattern), then > + if (append_pos) { + output_redirect_pos = append_pos; + append_mode = true; + } else if (write_pos) { + output_redirect_pos = write_pos; + append_mode = false; + } + + if (output_redirect_pos) { + // Null-terminate the command part before '>' or '>>' + *output_redirect_pos = '\0'; + + // Find the filename after '>' or '>>' + output_filename = output_redirect_pos + (append_mode ? 2 : 1); + while (*output_filename == ' ') { + output_filename++; // Skip spaces + } + + // Remove trailing spaces/newlines from filename + char *end = output_filename + strlen(output_filename) - 1; + while (end > output_filename && (*end == ' ' || *end == '\n' || *end == '\r')) { + *end-- = '\0'; + } + + if (*output_filename != '\0') { + + output_file = fopen(output_filename, append_mode ? "a" : "w"); + if (!output_file) { + fprintf(stderr, "Failed to open file '%s' for %s: %s\n", output_filename, + append_mode ? "appending" : "writing", strerror(errno)); + if (input_file != stdin) { + fclose(input_file); + } + return ESP_FAIL; + } + } else { + fprintf(stderr, "No filename specified after '%s'\n", append_mode ? ">>" : ">"); + if (input_file != stdin) { + fclose(input_file); + } + return ESP_FAIL; + } + } + + // Run the pipeline with the specified input and output files + esp_err_t result = esp_shell_run_pipeline(command_line, input_file, output_file, cmd_ret); + + // Close files if they were opened + if (input_file != stdin) { + fclose(input_file); + } + if (output_file != stdout) { + fclose(output_file); + } + + return result; +} + +/* + * This function runs a command line, which may contain multiple commands separated by ';', '&&', or '||'. + * ';' - Execute commands sequentially, continue on failure + * '&&' - Execute commands sequentially, break on failure + * '||' - Execute commands sequentially, skip next command on success + */ +esp_err_t esp_shell_run(char *command_line, int *cmd_ret) +{ + char *current_pos = command_line; + *cmd_ret = 0; + enum { OP_SEMICOLON, OP_AND, OP_OR } operator_type; + + while (*current_pos != '\0') { + // Find the next separator (';', '&&', or '||') + char *separator_pos = NULL; + operator_type = OP_SEMICOLON; // Default to semicolon + + // Look for '&&' and '||' first (longer patterns) + char *and_pos = strstr(current_pos, "&&"); + char *or_pos = strstr(current_pos, "||"); + char *semicolon_pos = strchr(current_pos, ';'); + + // Find the earliest separator + char *earliest_pos = NULL; + if (and_pos && (!earliest_pos || and_pos < earliest_pos)) { + earliest_pos = and_pos; + operator_type = OP_AND; + } + if (or_pos && (!earliest_pos || or_pos < earliest_pos)) { + earliest_pos = or_pos; + operator_type = OP_OR; + } + if (semicolon_pos && (!earliest_pos || semicolon_pos < earliest_pos)) { + earliest_pos = semicolon_pos; + operator_type = OP_SEMICOLON; + } + + separator_pos = earliest_pos; + + // Extract the current command + char *command_end = separator_pos ? separator_pos : current_pos + strlen(current_pos); + size_t command_len = command_end - current_pos; + + // Create a null-terminated copy of the command + char command_buffer[command_len + 1]; + strncpy(command_buffer, current_pos, command_len); + command_buffer[command_len] = '\0'; + + // Skip leading spaces + char *command = command_buffer; + while (*command == ' ') { + command++; + } + + // Skip trailing spaces + char *end = command + strlen(command) - 1; + while (end > command && *end == ' ') { + *end-- = '\0'; + } + + // Skip empty commands + if (*command != '\0') { + esp_err_t ret = esp_shell_run_single(command, cmd_ret); + bool command_failed = (ret != ESP_OK || *cmd_ret != 0); + + // Handle failure/success based on operator type + if (operator_type == OP_AND) { + // '&&' operator: break on failure + if (command_failed) { + return ret != ESP_OK ? ret : ESP_OK; + } + } else if (operator_type == OP_OR) { + // '||' operator: break on success + if (!command_failed) { + return ESP_OK; + } + } + // ';' operator: continue regardless (do nothing) + } + + // Move to next command + if (separator_pos) { + int separator_len = (operator_type == OP_AND || operator_type == OP_OR) ? 2 : 1; + current_pos = separator_pos + separator_len; + } else { + break; + } + } + + return ESP_OK; +} From f4857ccd8af5a24cbe2301ead229823f768c1a3f Mon Sep 17 00:00:00 2001 From: Jimmy Wennlund Date: Wed, 29 Oct 2025 15:30:56 +0100 Subject: [PATCH 6/6] feat(examples): add shell-like experience to advanced console example Add Unix-like shell functionality to demonstrate console capabilities: - Implement tee, cat, echo, and other shell commands - Add esp_shell component for shell command execution - Enable task-based command execution for pipelines - Demonstrate pipe usage and I/O redirection Shows practical usage of console task API and VFS pipe features. --- .../components/cmd_system/CMakeLists.txt | 2 +- .../components/cmd_system/cmd_system.h | 8 + .../cmd_system/cmd_system_shell_common.c | 370 ++++++++++++++++++ .../advanced/main/console_example_main.c | 18 +- .../console/advanced/sdkconfig.defaults | 12 + 5 files changed, 408 insertions(+), 2 deletions(-) create mode 100644 examples/system/console/advanced/components/cmd_system/cmd_system_shell_common.c diff --git a/examples/system/console/advanced/components/cmd_system/CMakeLists.txt b/examples/system/console/advanced/components/cmd_system/CMakeLists.txt index ff01df1e9875..6dc4202b94d5 100644 --- a/examples/system/console/advanced/components/cmd_system/CMakeLists.txt +++ b/examples/system/console/advanced/components/cmd_system/CMakeLists.txt @@ -1,4 +1,4 @@ -idf_component_register(SRCS "cmd_system.c" "cmd_system_common.c" +idf_component_register(SRCS "cmd_system.c" "cmd_system_common.c" "cmd_system_shell_common.c" INCLUDE_DIRS . REQUIRES console spi_flash esp_driver_uart esp_driver_gpio) diff --git a/examples/system/console/advanced/components/cmd_system/cmd_system.h b/examples/system/console/advanced/components/cmd_system/cmd_system.h index dbf4cd8422b4..f051c84790f5 100644 --- a/examples/system/console/advanced/components/cmd_system/cmd_system.h +++ b/examples/system/console/advanced/components/cmd_system/cmd_system.h @@ -22,6 +22,14 @@ void register_system_common(void); void register_system_deep_sleep(void); void register_system_light_sleep(void); +// Register common tools used in shell: cat, echo, grep, tail, tee +void register_system_shell_common(void); + +void register_system_shell_tee(void); +void register_system_shell_cat(void); +void register_system_shell_grep(void); +void register_system_shell_echo(void); + #ifdef __cplusplus } #endif diff --git a/examples/system/console/advanced/components/cmd_system/cmd_system_shell_common.c b/examples/system/console/advanced/components/cmd_system/cmd_system_shell_common.c new file mode 100644 index 000000000000..1c658d103cf8 --- /dev/null +++ b/examples/system/console/advanced/components/cmd_system/cmd_system_shell_common.c @@ -0,0 +1,370 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#include +#include +#include +#include +#include +#include + +#include +#include "esp_console.h" +#include "argtable3/argtable3.h" +#include "freertos/task.h" +#include "cmd_system.h" + +static struct { + struct arg_str *path; + struct arg_lit *append; + struct arg_end *end; +} tee_args; + + +static int cmd_tee(int argc, char **argv) +{ + int nerrors = arg_parse(argc, argv, (void **)&tee_args); + if (nerrors != 0) { + arg_print_errors(stderr, tee_args.end, argv[0]); + return 1; + } + + if (tee_args.path->count != 1) { + fprintf(stderr, "Please provide a single file path\n"); + return 1; + } + + const char *path = tee_args.path->sval[0]; + const char *mode = (tee_args.append->count > 0) ? "a" : "w"; + + FILE *file = fopen(path, mode); + if (!file) { + fprintf(stderr, "tee: %s: %s\n", path, strerror(errno)); + return 1; + } + + char buf[256]; + while (fgets(buf, sizeof(buf), stdin)) { + // Write to stdout + fputs(buf, stdout); + fflush(stdout); + + // Write to file + fputs(buf, file); + } + + fclose(file); + return 0; +} + +void register_system_shell_tee(void) { + tee_args.path = arg_str1(NULL, NULL, "", "File to write to"); + tee_args.append = arg_lit0("a", "append", "Append to file instead of overwriting"); + tee_args.end = arg_end(2); + const esp_console_cmd_t tee_cmd = { + .command = "tee", + .help = "Read from stdin and write to file and stdout", + .hint = NULL, + .func = &cmd_tee, + .argtable = &tee_args, + }; + ESP_ERROR_CHECK(esp_console_cmd_register(&tee_cmd)); +} + + +static struct { + struct arg_str *path; + struct arg_end *end; +} cat_args; + +int cmd_cat(int argc, char **argv) +{ + int nerrors = arg_parse(argc, argv, (void **)&cat_args); + if (nerrors != 0) { + arg_print_errors(stderr, cat_args.end, argv[0]); + return 1; + } + + if (cat_args.path->count != 1) { + fprintf(stderr, "Please provide a single file path\n"); + return 1; + } + const char *path = cat_args.path->sval[0]; + + FILE *file = fopen(path, "r"); + if (!file) { + fprintf(stderr, "File %s not found\n", path); + return 2; + } + + while (true) { + char buf[32]; + size_t bytes = fread(buf, 1, sizeof(buf), file); + if (bytes < 1) + break; + fwrite(buf, 1, bytes, stdout); + } + + if (file) { + fclose(file); + } + + return 0; +} + +void register_system_shell_cat(void) { + cat_args.path = arg_str1(NULL, NULL, "", "File to cat"); + cat_args.end = arg_end(2); + const esp_console_cmd_t cat_cmd = {.command = "cat", .help = "cat file", .hint = NULL, .func = &cmd_cat, .argtable = &cat_args}; + ESP_ERROR_CHECK(esp_console_cmd_register(&cat_cmd)); +} + +static struct { + struct arg_str *pattern; + struct arg_file *files; + struct arg_lit *ignore_case; + struct arg_lit *line_number; + struct arg_lit *invert_match; + struct arg_end *end; +} grep_args; + +static int strstr_case_insensitive(const char *haystack, const char *needle) +{ + if (!*needle) { + return 1; // empty needle matches + } + + for (const char *h = haystack; *h; h++) { + const char *n = needle; + const char *h_temp = h; + + while (*h_temp && *n && (tolower((unsigned char)*h_temp) == tolower((unsigned char)*n))) { + h_temp++; + n++; + } + + if (!*n) { + return 1; // found match + } + } + return 0; // no match +} + +static int grep_line(const char *line, const char *pattern, bool ignore_case, bool invert_match) +{ + int match; + if (ignore_case) { + match = strstr_case_insensitive(line, pattern); + } else { + match = (strstr(line, pattern) != NULL); + } + + if (invert_match) { + return !match; + } + return match; +} + +static int grep_file(FILE *f, const char *pattern, bool ignore_case, bool line_number, bool invert_match, const char *filename_prefix) +{ + char line[256]; + int line_num = 0; + int match_count = 0; + + while (fgets(line, sizeof(line), f)) { + line_num++; + // Remove trailing newline + size_t len = strlen(line); + if (len > 0 && line[len - 1] == '\n') { + line[len - 1] = '\0'; + } + + if (grep_line(line, pattern, ignore_case, invert_match)) { + if (filename_prefix != NULL) { + printf("%s:", filename_prefix); + } + if (line_number) { + printf("%d:", line_num); + } + printf("%s\n", line); + fflush(stdout); + match_count++; + } + } + if (ferror(f)) { + // Don't report pipe errors (EPIPE) - that's normal ending when input is finished + if (errno != EPIPE) { + fprintf(stderr, "Error %d reading file\n", ferror(f)); + return 1; + } + } + + return (match_count > 0) ? 0 : 1; +} + +static int cmd_grep(int argc, char **argv) +{ + int nerrors = arg_parse(argc, argv, (void **)&grep_args); + if (nerrors != 0) { + arg_print_errors(stderr, grep_args.end, argv[0]); + return 1; + } + + const char *pattern = grep_args.pattern->sval[0]; + bool ignore_case = grep_args.ignore_case->count > 0; + bool line_number = grep_args.line_number->count > 0; + bool invert_match = grep_args.invert_match->count > 0; + + // If no files specified, read from stdin + if (grep_args.files->count == 0) { + return grep_file(stdin, pattern, ignore_case, line_number, invert_match, NULL); + } + + // Process files + int ret = 0; + bool show_filename = (grep_args.files->count > 1); + + for (int i = 0; i < grep_args.files->count; i++) { + const char *filename = grep_args.files->filename[i]; + FILE *f = fopen(filename, "r"); + if (f == NULL) { + fprintf(stderr, "grep: %s: No such file or directory\n", filename); + ret = 1; + continue; + } + + int file_ret = grep_file(f, pattern, ignore_case, line_number, invert_match, show_filename ? filename : NULL); + fclose(f); + + if (file_ret != 0) { + ret = file_ret; + } + } + + return ret; +} + +void register_system_shell_grep(void) { + grep_args.pattern = arg_str1(NULL, NULL, "PATTERN", "Search pattern"); + grep_args.files = arg_filen(NULL, NULL, "FILE", 0, 10, "Files to search (stdin if omitted)"); + grep_args.ignore_case = arg_lit0("i", "ignore-case", "Case-insensitive search"); + grep_args.line_number = arg_lit0("n", "line-number", "Print line numbers"); + grep_args.invert_match = arg_lit0("v", "invert-match", "Select non-matching lines"); + grep_args.end = arg_end(2); + + const esp_console_cmd_t cmd = { + .command = "grep", + .help = "Search for PATTERN in files or stdin", + .hint = NULL, + .func = &cmd_grep, + .argtable = &grep_args, + }; + esp_console_cmd_register(&cmd); +} + +static int cmd_echo(int argc, char **argv) +{ + for (int i = 1; i < argc; i++) { + if (i == argc - 1) + printf("%s\n", argv[i]); + else + printf("%s ", argv[i]); + } + return 0; +} + +void register_system_shell_echo(void) { + const esp_console_cmd_t cmd = { + .command = "echo", + .help = "Echo arguments to stdout", + .hint = NULL, + .func = &cmd_echo, + }; + esp_console_cmd_register(&cmd); +} + +static int cmd_true(int argc, char **argv) { + return 0; +} + +void register_system_shell_true(void) { + const esp_console_cmd_t cmd = { + .command = "true", + .help = "Do nothing, successfully", + .hint = NULL, + .func = &cmd_true, + }; + esp_console_cmd_register(&cmd); +} + +static int cmd_false(int argc, char **argv) { + return 1; +} + +void register_system_shell_false(void) { + const esp_console_cmd_t cmd = { + .command = "false", + .help = "Do nothing, unsuccessfully", + .hint = NULL, + .func = &cmd_false, + }; + esp_console_cmd_register(&cmd); +} + +static struct { + struct arg_str *path; + struct arg_end *end; +} ls_args; + +int cmd_ls(int argc, char **argv) +{ + int nerrors = arg_parse(argc, argv, (void **)&ls_args); + if (nerrors != 0) { + arg_print_errors(stderr, ls_args.end, argv[0]); + return 1; + } + + const char *path = ls_args.path->sval[0]; + if (!path) + return 1; + + DIR *dir = opendir(path); + if (!dir) { + printf("Directory %s not found\n", path); + return 2; + } + while (true) { + struct dirent *de = readdir(dir); + if (!de) { + break; + } + char file_path[512]; + struct stat file_stat = {}; + snprintf(file_path, sizeof(file_path), "%s/%s", path, de->d_name); + stat(file_path, &file_stat); + printf(" %s %s %ld\n", file_path, de->d_type == DT_REG ? "" : "", file_stat.st_size); + } + closedir(dir); + + return 0; +} + +void register_system_shell_ls(void) { + ls_args.path = arg_str1(NULL, NULL, "", "Path to list files in"); + ls_args.end = arg_end(2); + const esp_console_cmd_t ls_cmd = {.command = "ls", .help = "list files", .hint = NULL, .func = &cmd_ls, .argtable = &ls_args}; + esp_console_cmd_register(&ls_cmd); +} +void register_system_shell_common(void) { + + register_system_shell_tee(); + register_system_shell_cat(); + register_system_shell_grep(); + register_system_shell_echo(); + register_system_shell_true(); + register_system_shell_false(); + register_system_shell_ls(); +} diff --git a/examples/system/console/advanced/main/console_example_main.c b/examples/system/console/advanced/main/console_example_main.c index d533f4424dfe..9f2483b6ba4c 100644 --- a/examples/system/console/advanced/main/console_example_main.c +++ b/examples/system/console/advanced/main/console_example_main.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Unlicense OR CC0-1.0 */ @@ -10,9 +10,12 @@ #include "esp_system.h" #include "esp_log.h" #include "esp_console.h" +#include "esp_shell.h" +#include "esp_vfs_pipe.h" #include "linenoise/linenoise.h" #include "argtable3/argtable3.h" #include "esp_vfs_fat.h" +#include "esp_vfs_pipe.h" #include "nvs.h" #include "nvs_flash.h" #include "soc/soc_caps.h" @@ -83,6 +86,12 @@ void app_main(void) ESP_LOGI(TAG, "Command history disabled"); #endif +#ifdef CONFIG_CONSOLE_SHELL_ENABLE + /* Configure VFS to use pipe for console I/O */ + esp_vfs_pipe_config_t cfs_config = ESP_VFS_PIPE_CONFIG_DEFAULT(); + esp_vfs_pipe_register(&cfs_config); +#endif + /* Initialize console output periheral (UART, USB_OTG, USB_JTAG) */ initialize_console_peripheral(); @@ -107,6 +116,9 @@ void app_main(void) /* Register commands */ esp_console_register_help_command(); register_system_common(); +#ifdef CONFIG_CONSOLE_COMMAND_ON_TASK + register_system_shell_common(); +#endif #if SOC_LIGHT_SLEEP_SUPPORTED register_system_light_sleep(); #endif @@ -160,7 +172,11 @@ void app_main(void) /* Try to run the command */ int ret; +#ifdef CONFIG_CONSOLE_SHELL_ENABLE + esp_err_t err = esp_shell_run(line, &ret); +#else esp_err_t err = esp_console_run(line, &ret); +#endif if (err == ESP_ERR_NOT_FOUND) { printf("Unrecognized command\n"); } else if (err == ESP_ERR_INVALID_ARG) { diff --git a/examples/system/console/advanced/sdkconfig.defaults b/examples/system/console/advanced/sdkconfig.defaults index e4dfabc50b2d..7a5930651efa 100644 --- a/examples/system/console/advanced/sdkconfig.defaults +++ b/examples/system/console/advanced/sdkconfig.defaults @@ -18,3 +18,15 @@ CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y # On chips with USB serial, disable secondary console which does not make sense when using console component CONFIG_ESP_CONSOLE_SECONDARY_NONE=y + +# Enable support for Ctrl + D, to generate an EOF on stdin +CONFIG_VFS_SUPPORT_TERMIOS=y + +# Will also enable filesystem +CONFIG_CONSOLE_STORE_HISTORY=y + +# Enable running commands on separate tasks +CONFIG_CONSOLE_COMMAND_ON_TASK=y + +# Enable shell support +CONFIG_CONSOLE_SHELL_ENABLE=y