feat(usb_cdc): Add select functionality

This commit is contained in:
Guillaume Souchere
2025-07-29 10:14:19 +02:00
parent 8e094e7087
commit d5dca1a78d
13 changed files with 368 additions and 7 deletions
@@ -0,0 +1,4 @@
# Documentation: .gitlab/ci/README.md#manifest-file-to-control-the-buildtest-apps
components/esp_vfs_console/test_apps/usb_cdc_vfs:
enable:
- if: IDF_TARGET in ["esp32s3"] # reason: console components is only implemented on these targets. TODO P4: IDF-9120
@@ -0,0 +1,9 @@
# This is the project CMakeLists.txt file for the test subproject
cmake_minimum_required(VERSION 3.5)
list(PREPEND SDKCONFIG_DEFAULTS "$ENV{IDF_PATH}/tools/test_apps/configs/sdkconfig.debug_helpers" "sdkconfig.defaults")
set(COMPONENTS main)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(usb_cdc_vfs_test)
@@ -0,0 +1,2 @@
| Supported Targets | ESP32-S3 |
| ----------------- | -------- |
@@ -0,0 +1,7 @@
set(src "test_app_main.c")
idf_component_register(SRCS ${src}
PRIV_INCLUDE_DIRS .
PRIV_REQUIRES esp_system esp_vfs_console unity
WHOLE_ARCHIVE
)
@@ -25,6 +25,41 @@ static void test_setup(const char* func_name, const size_t func_name_size)
flush_write();
}
static int read_bytes_with_select(FILE *stream, void *buf, size_t buf_len, struct timeval* tv)
{
int fd = fileno(stream);
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(fd, &read_fds);
/* call select to wait for either a read ready or an except to happen */
int nread = select(fd + 1, &read_fds, NULL, NULL, tv);
if (nread < 0) {
return -1;
} else if (FD_ISSET(fd, &read_fds)) {
int read_count = 0;
int total_read = 0;
do {
read_count = read(fd, buf + total_read, buf_len - total_read);
if (read_count < 0 && errno != EWOULDBLOCK) {
return -1;
} else if (read_count > 0) {
total_read += read_count;
if (total_read > buf_len) {
fflush(stream);
break;
}
}
} while (read_count > 0);
return total_read;
} else {
/* select timed out */
return -2;
}
return nread;
}
static bool wait_for_read_ready(FILE *stream)
{
int fd = fileno(stream);
@@ -233,6 +268,7 @@ static void test_usb_cdc_read_no_exit_on_newline_reception(void)
*/
void app_main(void)
{
test_usb_cdc_select();
test_usb_cdc_read_non_blocking();
test_usb_cdc_read_blocking();
test_usb_cdc_read_no_exit_on_newline_reception();
@@ -2,18 +2,27 @@
# SPDX-License-Identifier: CC0-1.0
import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
@pytest.mark.esp32s3
@pytest.mark.usb_device
@pytest.mark.parametrize(
'port, flash_port, config',
[
pytest.param('/dev/serial_ports/ttyACM-esp32', '/dev/serial_ports/ttyUSB-esp32', 'release'),
],
indirect=True,)
@pytest.mark.parametrize('test_message', ['test123456789!@#%^&*'])
def test_usb_cdc_vfs_default(dut: Dut, test_message: str) -> None:
indirect=True,
)
@idf_parametrize('target', ['esp32s3'], indirect=['target'])
def test_usb_cdc_vfs_default(dut: Dut) -> None:
test_message = 'test123456789!@#%^&*'
# test run: test_usb_cdc_select
dut.expect_exact('test_usb_cdc_select', timeout=2)
dut.expect_exact('select timed out', timeout=2)
dut.write(test_message)
dut.expect_exact(test_message, timeout=2)
# test run: test_usb_cdc_read_non_blocking
dut.expect_exact('test_usb_cdc_read_non_blocking', timeout=2)
dut.expect_exact('send_bytes', timeout=2)
@@ -0,0 +1,6 @@
CONFIG_PM_ENABLE=y
CONFIG_FREERTOS_USE_TICKLESS_IDLE=y
CONFIG_COMPILER_OPTIMIZATION_SIZE=y
CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_SIZE=y
CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_SILENT=y
CONFIG_COMPILER_OPTIMIZATION_NONE=y
@@ -0,0 +1,8 @@
# Enable Unity fixture support
CONFIG_UNITY_ENABLE_FIXTURE=n
CONFIG_UNITY_ENABLE_IDF_TEST_RUNNER=y
# Custom partition table for this test app
CONFIG_ESP_TASK_WDT_INIT=n
CONFIG_ESP_CONSOLE_USB_CDC=y
+203 -2
View File
@@ -15,9 +15,13 @@
#include "esp_vfs_cdcacm.h"
#include "esp_attr.h"
#include "sdkconfig.h"
#include "esp_heap_caps.h"
#include "esp_private/esp_vfs_cdcacm_select.h"
#include "esp_private/usb_console.h"
#define USB_CDC_LOCAL_FD 0
// Newline conversion mode when transmitting
static esp_line_endings_t s_tx_mode =
#if CONFIG_NEWLIB_STDOUT_LINE_ENDING_CRLF
@@ -38,6 +42,12 @@ static esp_line_endings_t s_rx_mode =
ESP_LINE_ENDINGS_LF;
#endif
#if CONFIG_VFS_SELECT_IN_RAM
#define CDCACM_VFS_MALLOC_FLAGS (MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT)
#else
#define CDCACM_VFS_MALLOC_FLAGS MALLOC_CAP_DEFAULT
#endif
#define NONE -1
//Read and write lock, lazily initialized
@@ -48,6 +58,26 @@ static bool s_blocking;
static SemaphoreHandle_t s_rx_semaphore;
static SemaphoreHandle_t s_tx_semaphore;
#ifdef CONFIG_VFS_SUPPORT_SELECT
typedef struct {
esp_vfs_select_sem_t select_sem;
fd_set *readfds;
fd_set *writefds;
fd_set *errorfds;
fd_set readfds_orig;
fd_set writefds_orig;
fd_set errorfds_orig;
} cdcacm_select_args_t;
static cdcacm_select_args_t **s_registered_selects = NULL;
static int s_registered_select_num = 0;
static portMUX_TYPE s_registered_select_lock = portMUX_INITIALIZER_UNLOCKED;
static esp_err_t cdcacm_end_select(void *end_select_args);
#endif // CONFIG_VFS_SUPPORT_SELECT
static ssize_t cdcacm_write(int fd, const void *data, size_t size)
{
assert(fd == 0);
@@ -82,7 +112,7 @@ static int cdcacm_fsync(int fd)
static int cdcacm_open(const char *path, int flags, int mode)
{
return 0; // fd 0
return USB_CDC_LOCAL_FD; // fd 0
}
static int cdcacm_fstat(int fd, struct stat *st)
@@ -283,6 +313,167 @@ static int cdcacm_fcntl(int fd, int cmd, int arg)
return result;
}
#ifdef CONFIG_VFS_SUPPORT_SELECT
static void select_notif_callback_isr(cdcacm_select_notif_t cdcacm_select_notif, BaseType_t *task_woken)
{
portENTER_CRITICAL_ISR(&s_registered_select_lock);
for (int i = 0; i < s_registered_select_num; ++i) {
cdcacm_select_args_t *args = s_registered_selects[i];
if (args) {
switch (cdcacm_select_notif) {
case CDCACM_SELECT_READ_NOTIF:
if (FD_ISSET(USB_CDC_LOCAL_FD, &args->readfds_orig)) {
FD_SET(USB_CDC_LOCAL_FD, args->readfds);
esp_vfs_select_triggered_isr(args->select_sem, task_woken);
}
break;
case CDCACM_SELECT_WRITE_NOTIF:
if (FD_ISSET(USB_CDC_LOCAL_FD, &args->writefds_orig)) {
FD_SET(USB_CDC_LOCAL_FD, args->writefds);
esp_vfs_select_triggered_isr(args->select_sem, task_woken);
}
break;
case CDCACM_SELECT_ERROR_NOTIF:
if (FD_ISSET(USB_CDC_LOCAL_FD, &args->errorfds_orig)) {
FD_SET(USB_CDC_LOCAL_FD, args->errorfds);
esp_vfs_select_triggered_isr(args->select_sem, task_woken);
}
break;
}
}
}
portEXIT_CRITICAL_ISR(&s_registered_select_lock);
}
static esp_err_t register_select(cdcacm_select_args_t *args)
{
esp_err_t ret = ESP_ERR_INVALID_ARG;
if (args) {
portENTER_CRITICAL(&s_registered_select_lock);
const int new_size = s_registered_select_num + 1;
cdcacm_select_args_t **new_selects;
if ((new_selects = heap_caps_realloc(s_registered_selects, new_size * sizeof(cdcacm_select_args_t *), CDCACM_VFS_MALLOC_FLAGS)) == NULL) {
ret = ESP_ERR_NO_MEM;
} else {
/* on first select registration register the callback */
if (s_registered_select_num == 0) {
cdcacm_set_select_notif_callback(select_notif_callback_isr);
}
s_registered_selects = new_selects;
s_registered_selects[s_registered_select_num] = args;
s_registered_select_num = new_size;
ret = ESP_OK;
}
portEXIT_CRITICAL(&s_registered_select_lock);
}
return ret;
}
static esp_err_t unregister_select(cdcacm_select_args_t *args)
{
esp_err_t ret = ESP_OK;
if (args) {
ret = ESP_ERR_INVALID_STATE;
portENTER_CRITICAL(&s_registered_select_lock);
for (int i = 0; i < s_registered_select_num; ++i) {
if (s_registered_selects[i] == args) {
const int new_size = s_registered_select_num - 1;
// The item is removed by overwriting it with the last item. The subsequent rellocation will drop the
// last item.
s_registered_selects[i] = s_registered_selects[new_size];
s_registered_selects = heap_caps_realloc(s_registered_selects, new_size * sizeof(cdcacm_select_args_t *), CDCACM_VFS_MALLOC_FLAGS);
// Shrinking a buffer with realloc is guaranteed to succeed.
s_registered_select_num = new_size;
/* when the last select is unregistered, also unregister the callback */
if (s_registered_select_num == 0) {
cdcacm_set_select_notif_callback(NULL);
}
ret = ESP_OK;
break;
}
}
portEXIT_CRITICAL(&s_registered_select_lock);
}
return ret;
}
static esp_err_t cdcacm_start_select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
esp_vfs_select_sem_t select_sem, void **end_select_args)
{
(void)nfds; /* Since there is only 1 USB OTG, this parameter is useless */
*end_select_args = NULL;
if (!esp_usb_console_is_installed()) {
return ESP_ERR_INVALID_STATE;
}
cdcacm_select_args_t *args = heap_caps_malloc(sizeof(cdcacm_select_args_t), CDCACM_VFS_MALLOC_FLAGS);
if (args == NULL) {
return ESP_ERR_NO_MEM;
}
args->select_sem = select_sem;
args->readfds = readfds;
args->writefds = writefds;
args->errorfds = exceptfds;
args->readfds_orig = *readfds; // store the original values because they will be set to zero
args->writefds_orig = *writefds;
args->errorfds_orig = *exceptfds;
FD_ZERO(readfds);
FD_ZERO(writefds);
FD_ZERO(exceptfds);
esp_err_t ret = register_select(args);
if (ret != ESP_OK) {
free(args);
return ret;
}
bool trigger_select = false;
if (FD_ISSET(USB_CDC_LOCAL_FD, &args->readfds_orig) &&
esp_usb_console_available_for_read() > 0) {
// signalize immediately when read is ready
FD_SET(USB_CDC_LOCAL_FD, readfds);
trigger_select = true;
}
if (FD_ISSET(USB_CDC_LOCAL_FD, &args->writefds_orig) &&
esp_usb_console_write_available()) {
// signalize immediately when write is ready
FD_SET(USB_CDC_LOCAL_FD, writefds);
trigger_select = true;
}
if (trigger_select) {
esp_vfs_select_triggered(args->select_sem);
}
*end_select_args = args;
return ESP_OK;
}
static esp_err_t cdcacm_end_select(void *end_select_args)
{
cdcacm_select_args_t *args = end_select_args;
esp_err_t ret = unregister_select(args);
if (args) {
free(args);
}
return ret;
}
#endif // CONFIG_VFS_SUPPORT_SELECT
void esp_vfs_dev_cdcacm_set_tx_line_endings(esp_line_endings_t mode)
{
s_tx_mode = mode;
@@ -293,6 +484,13 @@ void esp_vfs_dev_cdcacm_set_rx_line_endings(esp_line_endings_t mode)
s_rx_mode = mode;
}
#ifdef CONFIG_VFS_SUPPORT_SELECT
static const esp_vfs_select_ops_t s_cdcacm_vfs_select = {
.start_select = &cdcacm_start_select,
.end_select = &cdcacm_end_select,
};
#endif // CONFIG_VFS_SUPPORT_SELECT
static const esp_vfs_fs_ops_t s_cdcacm_vfs = {
.write = &cdcacm_write,
.open = &cdcacm_open,
@@ -300,7 +498,10 @@ static const esp_vfs_fs_ops_t s_cdcacm_vfs = {
.close = &cdcacm_close,
.read = &cdcacm_read,
.fcntl = &cdcacm_fcntl,
.fsync = &cdcacm_fsync
.fsync = &cdcacm_fsync,
#ifdef CONFIG_VFS_SUPPORT_SELECT
.select = &s_cdcacm_vfs_select,
#endif // CONFIG_VFS_SUPPORT_SELECT
};
const esp_vfs_fs_ops_t *esp_vfs_cdcacm_get_vfs(void)