From 52004f00f14041af379d1709a910ac79bfba7909 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Fri, 14 Nov 2025 10:30:12 -0600 Subject: [PATCH] feat(cli): Add support for USB CDC to `cli` component, for use on ESP32-S2 for example --- components/cli/CMakeLists.txt | 2 +- .../cli/example/sdkconfig.defaults.esp32s2 | 3 + components/cli/include/cli.hpp | 58 ++++++++++++++++--- components/cli/src/cli.cpp | 1 + 4 files changed, 54 insertions(+), 10 deletions(-) create mode 100644 components/cli/example/sdkconfig.defaults.esp32s2 diff --git a/components/cli/CMakeLists.txt b/components/cli/CMakeLists.txt index 86b6a8d5f..dde0c216c 100644 --- a/components/cli/CMakeLists.txt +++ b/components/cli/CMakeLists.txt @@ -1,4 +1,4 @@ idf_component_register( INCLUDE_DIRS "include" "detail/cli/include" SRC_DIRS "src" - REQUIRES driver esp_driver_uart esp_driver_usb_serial_jtag vfs logger) + REQUIRES driver esp_driver_uart esp_driver_usb_serial_jtag vfs esp_vfs_console logger) diff --git a/components/cli/example/sdkconfig.defaults.esp32s2 b/components/cli/example/sdkconfig.defaults.esp32s2 new file mode 100644 index 000000000..ea3b12d1c --- /dev/null +++ b/components/cli/example/sdkconfig.defaults.esp32s2 @@ -0,0 +1,3 @@ +# on the ESP32S2, which has native USB, we need to set the console so that the +# CLI can be configured correctly: +CONFIG_ESP_CONSOLE_USB_CDC=y diff --git a/components/cli/include/cli.hpp b/components/cli/include/cli.hpp index 4b808c46f..9509b00d3 100644 --- a/components/cli/include/cli.hpp +++ b/components/cli/include/cli.hpp @@ -11,16 +11,14 @@ #include "driver/usb_serial_jtag_vfs.h" #include "esp_err.h" #include "esp_system.h" +#include +#include "esp_vfs_cdcacm.h" #include "esp_vfs_dev.h" #include "esp_vfs_usb_serial_jtag.h" #include "line_input.hpp" -#ifdef CONFIG_ESP_CONSOLE_USB_CDC -#error The cli component is currently incompatible with CONFIG ESP_CONSOLE_USB_CDC console. -#endif // CONFIG_ESP_CONSOLE_USB_CDC - #ifndef STRINGIFY #define STRINGIFY(s) STRINGIFY2(s) #define STRINGIFY2(s) #s @@ -38,9 +36,9 @@ namespace espp { * * @note You should call configure_stdin_stdout() before creating a Cli object * to ensure that std::cin works as needed. If you do not want to use the - * Cli over the ESP CONSOLE (e.g. the ESP's UART, USB Serial/JTAG) and - * instead want to run it over a different UART port, VFS, or some other - * configuration, then you should call one of + * Cli over the ESP CONSOLE (e.g. the ESP's UART, USB Serial/JTAG, USB + * CDC) and instead want to run it over a different UART port, VFS, or + * some other configuration, then you should call one of * - configure_stdin_stdout_uart() * - configure_stdin_stdout_vfs() * - configure_stdin_stdout_custom() @@ -58,6 +56,7 @@ class Cli : private cli::CliSession { * compiled to use. This will only work if the ESP_CONSOLE was * configured to use one of the following: * - CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG + * - CONFIG_ESP_CONSOLE_USB_CDC * - CONFIG_ESP_CONSOLE_UART * * If you want to use a different console, you should use one of the @@ -77,6 +76,8 @@ class Cli : private cli::CliSession { #if CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG configure_stdin_stdout_usb_serial_jtag(); +#elif CONFIG_ESP_CONSOLE_USB_CDC + configure_stdin_stdout_usb_cdc(); #elif CONFIG_ESP_CONSOLE_UART configure_stdin_stdout_uart((uart_port_t)CONFIG_ESP_CONSOLE_UART_NUM, CONFIG_ESP_CONSOLE_UART_BAUDRATE); @@ -182,6 +183,47 @@ class Cli : private cli::CliSession { configured_ = true; } + /** + * @brief Configure the USB CDC driver to support blocking input read, so + * that std::cin (which assumes a blocking read) will function. This + * should be primarily used when you want to use the std::cin/std::getline + * and other std input functions or you want to use the cli library. + */ + static void configure_stdin_stdout_usb_cdc(void) { + if (configured_) { + return; + } + + // drain stdout before reconfiguring it + fflush(stdout); + fsync(fileno(stdout)); + + const std::string_view dev_name = "/dev/cdcacm"; + + // redirect stdin, stdout, stderr to the USB CDC interface + console_.in = freopen(dev_name.data(), "r", stdin); + console_.out = freopen(dev_name.data(), "w", stdout); + console_.err = freopen(dev_name.data(), "w", stderr); + + esp_vfs_dev_cdcacm_register(); + + esp_vfs_dev_cdcacm_set_rx_line_endings(ESP_LINE_ENDINGS_CR); + esp_vfs_dev_cdcacm_set_tx_line_endings(ESP_LINE_ENDINGS_CRLF); + + // Enable blocking mode on stdin and stdout + fcntl(fileno(stdout), F_SETFL, 0); + fcntl(fileno(stdin), F_SETFL, 0); + + // Initialize VFS & UART so we can use std::cout/cin + // _IOFBF = full buffering + // _IOLBF = line buffering + // _IONBF = no buffering + // disable buffering on stdin + setvbuf(stdin, nullptr, _IONBF, 0); + + configured_ = true; + } + /** * @brief Configure stdin/stdout to use a custom VFS driver. This should be * used when you have a custom VFS driver that you want to use for @@ -218,8 +260,6 @@ class Cli : private cli::CliSession { // Register the USB CDC interface [[maybe_unused]] auto err = esp_vfs_register(dev_name.data(), &vfs, NULL); - // TODO: this function is mostly untested, so we should probably add some - // error handling here and store the resultant pointers for later use // redirect stdin, stdout, stderr to the USB CDC interface console_.in = freopen(dev_name.data(), "r", stdin); console_.out = freopen(dev_name.data(), "w", stdout); diff --git a/components/cli/src/cli.cpp b/components/cli/src/cli.cpp index 0a04d442f..c84b82780 100644 --- a/components/cli/src/cli.cpp +++ b/components/cli/src/cli.cpp @@ -1,3 +1,4 @@ #include "cli.hpp" bool espp::Cli::configured_ = false; +espp::Cli::console_handle_t espp::Cli::console_ = {nullptr, nullptr, nullptr};