From 9c5f4e52881efa404a853e2338db82ef937a3218 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Wed, 14 Jan 2026 11:07:18 +0100 Subject: [PATCH] [usb_cdc_acm] move esp32 implementation to new file (#12824) Co-authored-by: Keith Burzinski --- esphome/components/usb_cdc_acm/__init__.py | 1 + .../components/usb_cdc_acm/usb_cdc_acm.cpp | 353 +----------------- esphome/components/usb_cdc_acm/usb_cdc_acm.h | 25 +- .../usb_cdc_acm/usb_cdc_acm_esp32.cpp | 349 +++++++++++++++++ esphome/core/defines.h | 1 + 5 files changed, 377 insertions(+), 352 deletions(-) create mode 100644 esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp diff --git a/esphome/components/usb_cdc_acm/__init__.py b/esphome/components/usb_cdc_acm/__init__.py index 6693d8e75e..bfe177a4da 100644 --- a/esphome/components/usb_cdc_acm/__init__.py +++ b/esphome/components/usb_cdc_acm/__init__.py @@ -74,3 +74,4 @@ async def to_code(config: ConfigType) -> None: add_idf_sdkconfig_option( "CONFIG_TINYUSB_CDC_TX_BUFSIZE", config[CONF_TX_BUFFER_SIZE] ) + cg.add_define("ESPHOME_MAX_USB_CDC_INSTANCES", num_interfaces) diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp b/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp index 29120a3d0b..a4c2e6c4a4 100644 --- a/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp @@ -3,181 +3,17 @@ #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" - -#include -#include "freertos/FreeRTOS.h" -#include "freertos/ringbuf.h" -#include "freertos/task.h" -#include "esp_log.h" - -#include "tusb.h" -#include "tusb_cdc_acm.h" - namespace esphome::usb_cdc_acm { -static const char *TAG = "usb_cdc_acm"; - -// Maximum bytes to log in very verbose hex output (168 * 3 = 504, under TX buffer size of 512) -static constexpr size_t USB_CDC_MAX_LOG_BYTES = 168; - -static constexpr size_t USB_TX_TASK_STACK_SIZE = 4096; -static constexpr size_t USB_TX_TASK_STACK_SIZE_VV = 8192; +static const char *const TAG = "usb_cdc_acm"; // Global component instance for managing USB device -USBCDCACMComponent *global_usb_cdc_component = nullptr; - -static USBCDCACMInstance *get_instance_by_itf(int itf) { - if (global_usb_cdc_component == nullptr) { - return nullptr; - } - return global_usb_cdc_component->get_interface_by_number(itf); -} - -static void tinyusb_cdc_rx_callback(int itf, cdcacm_event_t *event) { - USBCDCACMInstance *instance = get_instance_by_itf(itf); - if (instance == nullptr) { - ESP_LOGE(TAG, "RX callback: invalid interface %d", itf); - return; - } - - size_t rx_size = 0; - static uint8_t rx_buf[CONFIG_TINYUSB_CDC_RX_BUFSIZE] = {0}; - - // read from USB - esp_err_t ret = - tinyusb_cdcacm_read(static_cast(itf), rx_buf, CONFIG_TINYUSB_CDC_RX_BUFSIZE, &rx_size); - ESP_LOGV(TAG, "tinyusb_cdc_rx_callback itf=%d (size: %u)", itf, rx_size); -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE - char rx_hex_buf[format_hex_pretty_size(USB_CDC_MAX_LOG_BYTES)]; -#endif - ESP_LOGVV(TAG, "rx_buf = %s", format_hex_pretty_to(rx_hex_buf, rx_buf, rx_size)); - - if (ret == ESP_OK && rx_size > 0) { - RingbufHandle_t rx_ringbuf = instance->get_rx_ringbuf(); - if (rx_ringbuf != nullptr) { - BaseType_t send_res = xRingbufferSend(rx_ringbuf, rx_buf, rx_size, 0); - if (send_res != pdTRUE) { - ESP_LOGE(TAG, "USB RX itf=%d: buffer full, %u bytes lost", itf, rx_size); - } else { - ESP_LOGV(TAG, "USB RX itf=%d: queued %u bytes", itf, rx_size); - } - } - } -} - -static void tinyusb_cdc_line_state_changed_callback(int itf, cdcacm_event_t *event) { - USBCDCACMInstance *instance = get_instance_by_itf(itf); - if (instance == nullptr) { - ESP_LOGE(TAG, "Line state callback: invalid interface %d", itf); - return; - } - - int dtr = event->line_state_changed_data.dtr; - int rts = event->line_state_changed_data.rts; - ESP_LOGV(TAG, "Line state itf=%d: DTR=%d, RTS=%d", itf, dtr, rts); - - // Queue event for processing in main loop - instance->queue_line_state_event(dtr != 0, rts != 0); -} - -static void tinyusb_cdc_line_coding_changed_callback(int itf, cdcacm_event_t *event) { - USBCDCACMInstance *instance = get_instance_by_itf(itf); - if (instance == nullptr) { - ESP_LOGE(TAG, "Line coding callback: invalid interface %d", itf); - return; - } - - uint32_t bit_rate = event->line_coding_changed_data.p_line_coding->bit_rate; - uint8_t stop_bits = event->line_coding_changed_data.p_line_coding->stop_bits; - uint8_t parity = event->line_coding_changed_data.p_line_coding->parity; - uint8_t data_bits = event->line_coding_changed_data.p_line_coding->data_bits; - ESP_LOGV(TAG, "Line coding itf=%d: bit_rate=%" PRIu32 " stop_bits=%u parity=%u data_bits=%u", itf, bit_rate, - stop_bits, parity, data_bits); - - // Queue event for processing in main loop - instance->queue_line_coding_event(bit_rate, stop_bits, parity, data_bits); -} - -static esp_err_t ringbuf_read_bytes(RingbufHandle_t ring_buf, uint8_t *out_buf, size_t out_buf_sz, size_t *rx_data_size, - TickType_t xTicksToWait) { - size_t read_sz; - uint8_t *buf = static_cast(xRingbufferReceiveUpTo(ring_buf, &read_sz, xTicksToWait, out_buf_sz)); - - if (buf == nullptr) { - return ESP_FAIL; - } - - memcpy(out_buf, buf, read_sz); - vRingbufferReturnItem(ring_buf, (void *) buf); - *rx_data_size = read_sz; - - // Buffer's data can be wrapped, in which case we should perform another read - buf = static_cast(xRingbufferReceiveUpTo(ring_buf, &read_sz, 0, out_buf_sz - *rx_data_size)); - if (buf != nullptr) { - memcpy(out_buf + *rx_data_size, buf, read_sz); - vRingbufferReturnItem(ring_buf, (void *) buf); - *rx_data_size += read_sz; - } - - return ESP_OK; -} +USBCDCACMComponent *global_usb_cdc_component = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) //============================================================================== // USBCDCACMInstance Implementation //============================================================================== -void USBCDCACMInstance::setup() { - this->usb_tx_ringbuf_ = xRingbufferCreate(CONFIG_TINYUSB_CDC_TX_BUFSIZE, RINGBUF_TYPE_BYTEBUF); - if (this->usb_tx_ringbuf_ == nullptr) { - ESP_LOGE(TAG, "USB TX buffer creation error for itf %d", this->itf_); - this->parent_->mark_failed(); - return; - } - - this->usb_rx_ringbuf_ = xRingbufferCreate(CONFIG_TINYUSB_CDC_RX_BUFSIZE, RINGBUF_TYPE_BYTEBUF); - if (this->usb_rx_ringbuf_ == nullptr) { - ESP_LOGE(TAG, "USB RX buffer creation error for itf %d", this->itf_); - this->parent_->mark_failed(); - return; - } - - // Configure this CDC interface - const tinyusb_config_cdcacm_t acm_cfg = { - .usb_dev = TINYUSB_USBDEV_0, - .cdc_port = this->itf_, - .callback_rx = &tinyusb_cdc_rx_callback, - .callback_rx_wanted_char = NULL, - .callback_line_state_changed = &tinyusb_cdc_line_state_changed_callback, - .callback_line_coding_changed = &tinyusb_cdc_line_coding_changed_callback, - }; - - esp_err_t result = tusb_cdc_acm_init(&acm_cfg); - if (result != ESP_OK) { - ESP_LOGE(TAG, "tusb_cdc_acm_init failed: %d", result); - this->parent_->mark_failed(); - return; - } - - // Use a larger stack size for (very) verbose logging - const size_t stack_size = esp_log_level_get(TAG) > ESP_LOG_DEBUG ? USB_TX_TASK_STACK_SIZE_VV : USB_TX_TASK_STACK_SIZE; - - // Create a simple, unique task name per interface - char task_name[] = "usb_tx_0"; - task_name[sizeof(task_name) - 1] = format_hex_char(static_cast(this->itf_)); - xTaskCreate(usb_tx_task_fn, task_name, stack_size, this, 4, &this->usb_tx_task_handle_); - - if (this->usb_tx_task_handle_ == nullptr) { - ESP_LOGE(TAG, "Failed to create USB TX task for itf %d", this->itf_); - this->parent_->mark_failed(); - return; - } -} - -void USBCDCACMInstance::loop() { - // Process events from the lock-free queue - this->process_events_(); -} - void USBCDCACMInstance::queue_line_state_event(bool dtr, bool rts) { // Allocate event from pool CDCEvent *event = this->event_pool_.allocate(); @@ -288,170 +124,6 @@ void USBCDCACMInstance::process_events_() { } } -void USBCDCACMInstance::usb_tx_task_fn(void *arg) { - auto *instance = static_cast(arg); - instance->usb_tx_task(); -} - -void USBCDCACMInstance::usb_tx_task() { - uint8_t data[CONFIG_TINYUSB_CDC_TX_BUFSIZE] = {0}; - size_t tx_data_size = 0; - - while (1) { - // Wait for a notification from the bridge component - ulTaskNotifyTake(pdTRUE, portMAX_DELAY); - - // When we do wake up, we can be sure there is data in the ring buffer - esp_err_t ret = ringbuf_read_bytes(this->usb_tx_ringbuf_, data, CONFIG_TINYUSB_CDC_TX_BUFSIZE, &tx_data_size, 0); - - if (ret != ESP_OK) { - ESP_LOGE(TAG, "USB TX itf=%d: RingBuf read failed", this->itf_); - continue; - } else if (tx_data_size == 0) { - ESP_LOGD(TAG, "USB TX itf=%d: RingBuf empty, skipping", this->itf_); - continue; - } - - ESP_LOGV(TAG, "USB TX itf=%d: Read %d bytes from buffer", this->itf_, tx_data_size); -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE - char tx_hex_buf[format_hex_pretty_size(USB_CDC_MAX_LOG_BYTES)]; -#endif - ESP_LOGVV(TAG, "data = %s", format_hex_pretty_to(tx_hex_buf, data, tx_data_size)); - - // Serial data will be split up into 64 byte chunks to be sent over USB so this - // usually will take multiple iterations - uint8_t *data_head = &data[0]; - - while (tx_data_size > 0) { - size_t queued = tinyusb_cdcacm_write_queue(this->itf_, data_head, tx_data_size); - ESP_LOGV(TAG, "USB TX itf=%d: enqueued: size=%d, queued=%u", this->itf_, tx_data_size, queued); - - tx_data_size -= queued; - data_head += queued; - - ESP_LOGV(TAG, "USB TX itf=%d: waiting 10ms for flush", this->itf_); - esp_err_t flush_ret = tinyusb_cdcacm_write_flush(this->itf_, pdMS_TO_TICKS(10)); - - if (flush_ret != ESP_OK) { - ESP_LOGE(TAG, "USB TX itf=%d: flush failed", this->itf_); - tud_cdc_n_write_clear(this->itf_); - break; - } - } - } -} - -//============================================================================== -// UARTComponent Interface Implementation -//============================================================================== - -void USBCDCACMInstance::write_array(const uint8_t *data, size_t len) { - if (len == 0) { - return; - } - - // Write data to TX ring buffer - BaseType_t send_res = xRingbufferSend(this->usb_tx_ringbuf_, data, len, 0); - if (send_res != pdTRUE) { - ESP_LOGW(TAG, "USB TX itf=%d: buffer full, %u bytes dropped", this->itf_, len); - return; - } - - // Notify TX task that data is available - if (this->usb_tx_task_handle_ != nullptr) { - xTaskNotifyGive(this->usb_tx_task_handle_); - } -} - -bool USBCDCACMInstance::peek_byte(uint8_t *data) { - if (this->has_peek_) { - *data = this->peek_buffer_; - return true; - } - - if (this->read_byte(&this->peek_buffer_)) { - *data = this->peek_buffer_; - this->has_peek_ = true; - return true; - } - - return false; -} - -bool USBCDCACMInstance::read_array(uint8_t *data, size_t len) { - if (len == 0) { - return true; - } - - size_t original_len = len; - size_t bytes_read = 0; - - // First, use the peek buffer if available - if (this->has_peek_) { - data[0] = this->peek_buffer_; - this->has_peek_ = false; - bytes_read = 1; - data++; - if (--len == 0) { // Decrement len first, then check it... - return true; // No more to read - } - } - - // Read remaining bytes from RX ring buffer - size_t rx_size = 0; - uint8_t *buf = static_cast(xRingbufferReceiveUpTo(this->usb_rx_ringbuf_, &rx_size, 0, len)); - if (buf == nullptr) { - return false; - } - - memcpy(data, buf, rx_size); - vRingbufferReturnItem(this->usb_rx_ringbuf_, (void *) buf); - bytes_read += rx_size; - data += rx_size; - len -= rx_size; - if (len == 0) { - return true; // No more to read - } - - // Buffer's data may wrap around, in which case we should perform another read - buf = static_cast(xRingbufferReceiveUpTo(this->usb_rx_ringbuf_, &rx_size, 0, len)); - if (buf == nullptr) { - return false; - } - - memcpy(data, buf, rx_size); - vRingbufferReturnItem(this->usb_rx_ringbuf_, (void *) buf); - bytes_read += rx_size; - - return bytes_read == original_len; -} - -int USBCDCACMInstance::available() { - UBaseType_t waiting = 0; - if (this->usb_rx_ringbuf_ != nullptr) { - vRingbufferGetInfo(this->usb_rx_ringbuf_, nullptr, nullptr, nullptr, nullptr, &waiting); - } - return static_cast(waiting) + (this->has_peek_ ? 1 : 0); -} - -void USBCDCACMInstance::flush() { - // Wait for TX ring buffer to be empty - if (this->usb_tx_ringbuf_ == nullptr) { - return; - } - - UBaseType_t waiting = 1; - while (waiting > 0) { - vRingbufferGetInfo(this->usb_tx_ringbuf_, nullptr, nullptr, nullptr, nullptr, &waiting); - if (waiting > 0) { - vTaskDelay(pdMS_TO_TICKS(1)); - } - } - - // Also wait for USB to finish transmitting - tinyusb_cdcacm_write_flush(this->itf_, pdMS_TO_TICKS(100)); -} - //============================================================================== // USBCDCACMComponent Implementation //============================================================================== @@ -460,7 +132,7 @@ USBCDCACMComponent::USBCDCACMComponent() { global_usb_cdc_component = this; } void USBCDCACMComponent::setup() { // Setup all registered interfaces - for (auto interface : this->interfaces_) { + for (auto *interface : this->interfaces_) { if (interface != nullptr) { interface->setup(); } @@ -469,7 +141,7 @@ void USBCDCACMComponent::setup() { void USBCDCACMComponent::loop() { // Call loop() on all registered interfaces to process events - for (auto interface : this->interfaces_) { + for (auto *interface : this->interfaces_) { if (interface != nullptr) { interface->loop(); } @@ -480,21 +152,28 @@ void USBCDCACMComponent::dump_config() { ESP_LOGCONFIG(TAG, "USB CDC-ACM:\n" " Number of Interfaces: %d", - this->interfaces_[MAX_USB_CDC_INSTANCES - 1] != nullptr ? MAX_USB_CDC_INSTANCES : 1); + ESPHOME_MAX_USB_CDC_INSTANCES); + for (uint8_t i = 0; i < ESPHOME_MAX_USB_CDC_INSTANCES; ++i) { + if (this->interfaces_[i] != nullptr) { + this->interfaces_[i]->dump_config(); + } else { + ESP_LOGCONFIG(TAG, " Interface %u is disabled", i); + } + } } void USBCDCACMComponent::add_interface(USBCDCACMInstance *interface) { uint8_t itf_num = static_cast(interface->get_itf()); - if (itf_num < MAX_USB_CDC_INSTANCES) { + if (itf_num < ESPHOME_MAX_USB_CDC_INSTANCES) { this->interfaces_[itf_num] = interface; } else { - ESP_LOGE(TAG, "Interface number must be less than %u", MAX_USB_CDC_INSTANCES); + ESP_LOGE(TAG, "Interface number must be less than %u", ESPHOME_MAX_USB_CDC_INSTANCES); } } USBCDCACMInstance *USBCDCACMComponent::get_interface_by_number(uint8_t itf) { - for (auto interface : this->interfaces_) { - if ((interface != nullptr) && (interface->get_itf() == static_cast(itf))) { + for (auto *interface : this->interfaces_) { + if ((interface != nullptr) && (interface->get_itf() == itf)) { return interface; } } diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm.h b/esphome/components/usb_cdc_acm/usb_cdc_acm.h index 8c00f5d52f..065d7282d5 100644 --- a/esphome/components/usb_cdc_acm/usb_cdc_acm.h +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm.h @@ -13,7 +13,6 @@ namespace esphome::usb_cdc_acm { static const uint8_t EVENT_QUEUE_SIZE = 12; -static const uint8_t MAX_USB_CDC_INSTANCES = 2; // Callback types for line coding and line state changes using LineCodingCallback = std::function; @@ -53,14 +52,13 @@ class USBCDCACMComponent; /// Represents a single CDC ACM interface instance class USBCDCACMInstance : public uart::UARTComponent, public Parented { public: - void set_interface_number(uint8_t itf) { this->itf_ = static_cast(itf); } - void setup(); void loop(); + void dump_config(); + void set_interface_number(uint8_t itf) { this->itf_ = itf; } // Get the CDC port number for this instance - tinyusb_cdcacm_itf_t get_itf() const { return this->itf_; } - + uint8_t get_itf() const { return this->itf_; } // Ring buffer accessors for bridge components RingbufHandle_t get_tx_ringbuf() const { return this->usb_tx_ringbuf_; } RingbufHandle_t get_rx_ringbuf() const { return this->usb_rx_ringbuf_; } @@ -72,7 +70,7 @@ class USBCDCACMInstance : public uart::UARTComponent, public Parentedline_coding_callback_ = std::move(callback); } void set_line_state_callback(LineStateCallback callback) { this->line_state_callback_ = std::move(callback); } - // Called from TinyUSB task context (SPSC producer) - queues event for processing in main loop + // Called from USB core task context queues event for processing in main loop void queue_line_coding_event(uint32_t bit_rate, uint8_t stop_bits, uint8_t parity, uint8_t data_bits); void queue_line_state_event(bool dtr, bool rts); @@ -87,17 +85,18 @@ class USBCDCACMInstance : public uart::UARTComponent, public Parented event_pool_; LockFreeQueue event_queue_; - - // RX buffer for peek functionality - uint8_t peek_buffer_{0}; - bool has_peek_{false}; }; /// Main USB CDC ACM component that manages the USB device and all CDC interfaces @@ -126,7 +121,7 @@ class USBCDCACMComponent : public Component { USBCDCACMInstance *get_interface_by_number(uint8_t itf); protected: - std::array interfaces_{nullptr, nullptr}; + std::array interfaces_{}; }; extern USBCDCACMComponent *global_usb_cdc_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp b/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp new file mode 100644 index 0000000000..5c91150f30 --- /dev/null +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp @@ -0,0 +1,349 @@ +#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#include "usb_cdc_acm.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" + +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/ringbuf.h" +#include "freertos/task.h" +#include "esp_log.h" + +#include "tusb.h" +#include "tusb_cdc_acm.h" + +namespace esphome::usb_cdc_acm { + +static const char *const TAG = "usb_cdc_acm"; + +// Maximum bytes to log in very verbose hex output (168 * 3 = 504, under TX buffer size of 512) +static constexpr size_t USB_CDC_MAX_LOG_BYTES = 168; + +static constexpr size_t USB_TX_TASK_STACK_SIZE = 4096; +static constexpr size_t USB_TX_TASK_STACK_SIZE_VV = 8192; + +static USBCDCACMInstance *get_instance_by_itf(int itf) { + if (global_usb_cdc_component == nullptr) { + return nullptr; + } + return global_usb_cdc_component->get_interface_by_number(itf); +} + +static void tinyusb_cdc_rx_callback(int itf, cdcacm_event_t *event) { + USBCDCACMInstance *instance = get_instance_by_itf(itf); + if (instance == nullptr) { + ESP_LOGE(TAG, "RX callback: invalid interface %d", itf); + return; + } + + size_t rx_size = 0; + static uint8_t rx_buf[CONFIG_TINYUSB_CDC_RX_BUFSIZE] = {0}; + + // read from USB + esp_err_t ret = + tinyusb_cdcacm_read(static_cast(itf), rx_buf, CONFIG_TINYUSB_CDC_RX_BUFSIZE, &rx_size); + ESP_LOGV(TAG, "tinyusb_cdc_rx_callback itf=%d (size: %u)", itf, rx_size); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + char rx_hex_buf[format_hex_pretty_size(USB_CDC_MAX_LOG_BYTES)]; +#endif + ESP_LOGVV(TAG, "rx_buf = %s", format_hex_pretty_to(rx_hex_buf, rx_buf, rx_size)); + + if (ret == ESP_OK && rx_size > 0) { + RingbufHandle_t rx_ringbuf = instance->get_rx_ringbuf(); + if (rx_ringbuf != nullptr) { + BaseType_t send_res = xRingbufferSend(rx_ringbuf, rx_buf, rx_size, 0); + if (send_res != pdTRUE) { + ESP_LOGE(TAG, "USB RX itf=%d: buffer full, %u bytes lost", itf, rx_size); + } else { + ESP_LOGV(TAG, "USB RX itf=%d: queued %u bytes", itf, rx_size); + } + } + } +} + +static void tinyusb_cdc_line_state_changed_callback(int itf, cdcacm_event_t *event) { + USBCDCACMInstance *instance = get_instance_by_itf(itf); + if (instance == nullptr) { + ESP_LOGE(TAG, "Line state callback: invalid interface %d", itf); + return; + } + + int dtr = event->line_state_changed_data.dtr; + int rts = event->line_state_changed_data.rts; + ESP_LOGV(TAG, "Line state itf=%d: DTR=%d, RTS=%d", itf, dtr, rts); + + // Queue event for processing in main loop + instance->queue_line_state_event(dtr != 0, rts != 0); +} + +static void tinyusb_cdc_line_coding_changed_callback(int itf, cdcacm_event_t *event) { + USBCDCACMInstance *instance = get_instance_by_itf(itf); + if (instance == nullptr) { + ESP_LOGE(TAG, "Line coding callback: invalid interface %d", itf); + return; + } + + uint32_t bit_rate = event->line_coding_changed_data.p_line_coding->bit_rate; + uint8_t stop_bits = event->line_coding_changed_data.p_line_coding->stop_bits; + uint8_t parity = event->line_coding_changed_data.p_line_coding->parity; + uint8_t data_bits = event->line_coding_changed_data.p_line_coding->data_bits; + ESP_LOGV(TAG, "Line coding itf=%d: bit_rate=%" PRIu32 " stop_bits=%u parity=%u data_bits=%u", itf, bit_rate, + stop_bits, parity, data_bits); + + // Queue event for processing in main loop + instance->queue_line_coding_event(bit_rate, stop_bits, parity, data_bits); +} + +static esp_err_t ringbuf_read_bytes(RingbufHandle_t ring_buf, uint8_t *out_buf, size_t out_buf_sz, size_t *rx_data_size, + TickType_t xTicksToWait) { + size_t read_sz; + uint8_t *buf = static_cast(xRingbufferReceiveUpTo(ring_buf, &read_sz, xTicksToWait, out_buf_sz)); + + if (buf == nullptr) { + return ESP_FAIL; + } + + memcpy(out_buf, buf, read_sz); + vRingbufferReturnItem(ring_buf, (void *) buf); + *rx_data_size = read_sz; + + // Buffer's data can be wrapped, in which case we should perform another read + buf = static_cast(xRingbufferReceiveUpTo(ring_buf, &read_sz, 0, out_buf_sz - *rx_data_size)); + if (buf != nullptr) { + memcpy(out_buf + *rx_data_size, buf, read_sz); + vRingbufferReturnItem(ring_buf, (void *) buf); + *rx_data_size += read_sz; + } + + return ESP_OK; +} + +//============================================================================== +// USBCDCACMInstance Implementation +//============================================================================== + +void USBCDCACMInstance::setup() { + this->usb_tx_ringbuf_ = xRingbufferCreate(CONFIG_TINYUSB_CDC_TX_BUFSIZE, RINGBUF_TYPE_BYTEBUF); + if (this->usb_tx_ringbuf_ == nullptr) { + ESP_LOGE(TAG, "USB TX buffer creation error for itf %d", this->itf_); + this->parent_->mark_failed(); + return; + } + + this->usb_rx_ringbuf_ = xRingbufferCreate(CONFIG_TINYUSB_CDC_RX_BUFSIZE, RINGBUF_TYPE_BYTEBUF); + if (this->usb_rx_ringbuf_ == nullptr) { + ESP_LOGE(TAG, "USB RX buffer creation error for itf %d", this->itf_); + this->parent_->mark_failed(); + return; + } + + // Configure this CDC interface + const tinyusb_config_cdcacm_t acm_cfg = { + .usb_dev = TINYUSB_USBDEV_0, + .cdc_port = static_cast(this->itf_), + .callback_rx = &tinyusb_cdc_rx_callback, + .callback_rx_wanted_char = NULL, + .callback_line_state_changed = &tinyusb_cdc_line_state_changed_callback, + .callback_line_coding_changed = &tinyusb_cdc_line_coding_changed_callback, + }; + + esp_err_t result = tusb_cdc_acm_init(&acm_cfg); + if (result != ESP_OK) { + ESP_LOGE(TAG, "tusb_cdc_acm_init failed: %d", result); + this->parent_->mark_failed(); + return; + } + + // Use a larger stack size for (very) verbose logging + const size_t stack_size = esp_log_level_get(TAG) > ESP_LOG_DEBUG ? USB_TX_TASK_STACK_SIZE_VV : USB_TX_TASK_STACK_SIZE; + + // Create a simple, unique task name per interface + char task_name[] = "usb_tx_0"; + task_name[sizeof(task_name) - 1] = format_hex_char(static_cast(this->itf_)); + xTaskCreate(usb_tx_task_fn, task_name, stack_size, this, 4, &this->usb_tx_task_handle_); + + if (this->usb_tx_task_handle_ == nullptr) { + ESP_LOGE(TAG, "Failed to create USB TX task for itf %d", this->itf_); + this->parent_->mark_failed(); + return; + } +} + +void USBCDCACMInstance::loop() { + // Process events from the lock-free queue + this->process_events_(); +} + +void USBCDCACMInstance::dump_config() {} + +void USBCDCACMInstance::usb_tx_task_fn(void *arg) { + auto *instance = static_cast(arg); + instance->usb_tx_task(); +} + +void USBCDCACMInstance::usb_tx_task() { + uint8_t data[CONFIG_TINYUSB_CDC_TX_BUFSIZE] = {0}; + size_t tx_data_size = 0; + + while (1) { + // Wait for a notification from the bridge component + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + + // When we do wake up, we can be sure there is data in the ring buffer + esp_err_t ret = ringbuf_read_bytes(this->usb_tx_ringbuf_, data, CONFIG_TINYUSB_CDC_TX_BUFSIZE, &tx_data_size, 0); + + if (ret != ESP_OK) { + ESP_LOGE(TAG, "USB TX itf=%d: RingBuf read failed", this->itf_); + continue; + } else if (tx_data_size == 0) { + ESP_LOGD(TAG, "USB TX itf=%d: RingBuf empty, skipping", this->itf_); + continue; + } + + ESP_LOGV(TAG, "USB TX itf=%d: Read %d bytes from buffer", this->itf_, tx_data_size); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + char tx_hex_buf[format_hex_pretty_size(USB_CDC_MAX_LOG_BYTES)]; +#endif + ESP_LOGVV(TAG, "data = %s", format_hex_pretty_to(tx_hex_buf, data, tx_data_size)); + + // Serial data will be split up into 64 byte chunks to be sent over USB so this + // usually will take multiple iterations + uint8_t *data_head = &data[0]; + + while (tx_data_size > 0) { + size_t queued = + tinyusb_cdcacm_write_queue(static_cast(this->itf_), data_head, tx_data_size); + ESP_LOGV(TAG, "USB TX itf=%d: enqueued: size=%d, queued=%u", this->itf_, tx_data_size, queued); + + tx_data_size -= queued; + data_head += queued; + + ESP_LOGV(TAG, "USB TX itf=%d: waiting 10ms for flush", this->itf_); + esp_err_t flush_ret = + tinyusb_cdcacm_write_flush(static_cast(this->itf_), pdMS_TO_TICKS(10)); + + if (flush_ret != ESP_OK) { + ESP_LOGE(TAG, "USB TX itf=%d: flush failed", this->itf_); + tud_cdc_n_write_clear(this->itf_); + break; + } + } + } +} + +//============================================================================== +// UARTComponent Interface Implementation +//============================================================================== + +void USBCDCACMInstance::write_array(const uint8_t *data, size_t len) { + if (len == 0) { + return; + } + + // Write data to TX ring buffer + BaseType_t send_res = xRingbufferSend(this->usb_tx_ringbuf_, data, len, 0); + if (send_res != pdTRUE) { + ESP_LOGW(TAG, "USB TX itf=%d: buffer full, %u bytes dropped", this->itf_, len); + return; + } + + // Notify TX task that data is available + if (this->usb_tx_task_handle_ != nullptr) { + xTaskNotifyGive(this->usb_tx_task_handle_); + } +} + +bool USBCDCACMInstance::peek_byte(uint8_t *data) { + if (this->has_peek_) { + *data = this->peek_buffer_; + return true; + } + + if (this->read_byte(&this->peek_buffer_)) { + *data = this->peek_buffer_; + this->has_peek_ = true; + return true; + } + + return false; +} + +bool USBCDCACMInstance::read_array(uint8_t *data, size_t len) { + if (len == 0) { + return true; + } + + size_t original_len = len; + size_t bytes_read = 0; + + // First, use the peek buffer if available + if (this->has_peek_) { + data[0] = this->peek_buffer_; + this->has_peek_ = false; + bytes_read = 1; + data++; + if (--len == 0) { // Decrement len first, then check it... + return true; // No more to read + } + } + + // Read remaining bytes from RX ring buffer + size_t rx_size = 0; + uint8_t *buf = static_cast(xRingbufferReceiveUpTo(this->usb_rx_ringbuf_, &rx_size, 0, len)); + if (buf == nullptr) { + return false; + } + + memcpy(data, buf, rx_size); + vRingbufferReturnItem(this->usb_rx_ringbuf_, (void *) buf); + bytes_read += rx_size; + data += rx_size; + len -= rx_size; + if (len == 0) { + return true; // No more to read + } + + // Buffer's data may wrap around, in which case we should perform another read + buf = static_cast(xRingbufferReceiveUpTo(this->usb_rx_ringbuf_, &rx_size, 0, len)); + if (buf == nullptr) { + return false; + } + + memcpy(data, buf, rx_size); + vRingbufferReturnItem(this->usb_rx_ringbuf_, (void *) buf); + bytes_read += rx_size; + + return bytes_read == original_len; +} + +int USBCDCACMInstance::available() { + UBaseType_t waiting = 0; + if (this->usb_rx_ringbuf_ != nullptr) { + vRingbufferGetInfo(this->usb_rx_ringbuf_, nullptr, nullptr, nullptr, nullptr, &waiting); + } + return static_cast(waiting) + (this->has_peek_ ? 1 : 0); +} + +void USBCDCACMInstance::flush() { + // Wait for TX ring buffer to be empty + if (this->usb_tx_ringbuf_ == nullptr) { + return; + } + + UBaseType_t waiting = 1; + while (waiting > 0) { + vRingbufferGetInfo(this->usb_tx_ringbuf_, nullptr, nullptr, nullptr, nullptr, &waiting); + if (waiting > 0) { + vTaskDelay(pdMS_TO_TICKS(1)); + } + } + + // Also wait for USB to finish transmitting + tinyusb_cdcacm_write_flush(static_cast(this->itf_), pdMS_TO_TICKS(100)); +} + +void USBCDCACMInstance::check_logger_conflict() {} + +} // namespace esphome::usb_cdc_acm +#endif diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 673397fa31..bb40cd4ad1 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -358,3 +358,4 @@ #define ESPHOME_ENTITY_UPDATE_COUNT 1 #define ESPHOME_ENTITY_VALVE_COUNT 1 #define ESPHOME_ENTITY_WATER_HEATER_COUNT 1 +#define ESPHOME_MAX_USB_CDC_INSTANCES 1