diff --git a/CODEOWNERS b/CODEOWNERS index 2cd1453e12..af926d2d61 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -524,6 +524,7 @@ esphome/components/ufire_ise/* @pvizeli esphome/components/ultrasonic/* @OttoWinter esphome/components/update/* @jesserockz esphome/components/uponor_smatrix/* @kroimon +esphome/components/usb_cdc_acm/* @kbx81 esphome/components/usb_host/* @clydebarrow esphome/components/usb_uart/* @clydebarrow esphome/components/valve/* @esphome/core diff --git a/esphome/components/usb_cdc_acm/__init__.py b/esphome/components/usb_cdc_acm/__init__.py new file mode 100644 index 0000000000..6693d8e75e --- /dev/null +++ b/esphome/components/usb_cdc_acm/__init__.py @@ -0,0 +1,76 @@ +import esphome.codegen as cg +from esphome.components import esp32, uart +from esphome.components.esp32 import ( + VARIANT_ESP32P4, + VARIANT_ESP32S2, + VARIANT_ESP32S3, + add_idf_sdkconfig_option, +) +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_RX_BUFFER_SIZE, CONF_TX_BUFFER_SIZE +from esphome.types import ConfigType + +CODEOWNERS = ["@kbx81"] +AUTO_LOAD = ["uart"] +DEPENDENCIES = ["tinyusb"] + +CONF_INTERFACES = "interfaces" + +usb_cdc_acm_ns = cg.esphome_ns.namespace("usb_cdc_acm") +USBCDCACMComponent = usb_cdc_acm_ns.class_("USBCDCACMComponent", cg.Component) +USBCDCACMInstance = usb_cdc_acm_ns.class_( + "USBCDCACMInstance", uart.UARTComponent, cg.Parented.template(USBCDCACMComponent) +) + + +# Schema for individual CDC ACM interface instances +INTERFACE_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(USBCDCACMInstance), + } +) + +# Main component schema +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(USBCDCACMComponent), + cv.Optional(CONF_RX_BUFFER_SIZE, default=256): cv.All( + cv.validate_bytes, cv.uint16_t + ), + cv.Optional(CONF_TX_BUFFER_SIZE, default=256): cv.All( + cv.validate_bytes, cv.uint16_t + ), + cv.Optional(CONF_INTERFACES, default=[{}]): cv.All( + cv.ensure_list(INTERFACE_SCHEMA), + cv.Length(min=1, max=2), # At least 1, at most 2 interfaces + ), + } + ).extend(cv.COMPONENT_SCHEMA), + esp32.only_on_variant( + supported=[VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3], + ), +) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + # Create and register interface instances + for interface_index, interface_conf in enumerate(config[CONF_INTERFACES]): + interface = cg.new_Pvariable(interface_conf[CONF_ID]) + await cg.register_parented(interface, var) + cg.add(interface.set_interface_number(interface_index)) + cg.add(var.add_interface(interface)) + + # Configure TinyUSB with the correct number of CDC interfaces + num_interfaces = len(config[CONF_INTERFACES]) + add_idf_sdkconfig_option("CONFIG_TINYUSB_CDC_ENABLED", True) + add_idf_sdkconfig_option("CONFIG_TINYUSB_CDC_COUNT", num_interfaces) + add_idf_sdkconfig_option( + "CONFIG_TINYUSB_CDC_RX_BUFSIZE", config[CONF_RX_BUFFER_SIZE] + ) + add_idf_sdkconfig_option( + "CONFIG_TINYUSB_CDC_TX_BUFSIZE", config[CONF_TX_BUFFER_SIZE] + ) diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp b/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp new file mode 100644 index 0000000000..1cf614286f --- /dev/null +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp @@ -0,0 +1,495 @@ +#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 "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"; + +static constexpr size_t USB_TX_TASK_STACK_SIZE = 4096; +static constexpr size_t USB_TX_TASK_STACK_SIZE_VV = 8192; + +// 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); + ESP_LOGVV(TAG, "rx_buf = %s", format_hex_pretty(rx_buf, rx_size).c_str()); + + 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 = 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(); + if (event == nullptr) { + ESP_LOGW(TAG, "Event pool exhausted, line state event dropped (itf=%d)", this->itf_); + return; + } + + event->type = CDC_EVENT_LINE_STATE_CHANGED; + event->data.line_state.dtr = dtr; + event->data.line_state.rts = rts; + + if (!this->event_queue_.push(event)) { + ESP_LOGW(TAG, "Event queue full, line state event dropped (itf=%d)", this->itf_); + // Return event to pool since we couldn't queue it + this->event_pool_.release(event); + } else { + // Wake main loop immediately to process event +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + App.wake_loop_threadsafe(); +#endif + } +} + +void USBCDCACMInstance::queue_line_coding_event(uint32_t bit_rate, uint8_t stop_bits, uint8_t parity, + uint8_t data_bits) { + // Allocate event from pool + CDCEvent *event = this->event_pool_.allocate(); + if (event == nullptr) { + ESP_LOGW(TAG, "Event pool exhausted, line coding event dropped (itf=%d)", this->itf_); + return; + } + + event->type = CDC_EVENT_LINE_CODING_CHANGED; + event->data.line_coding.bit_rate = bit_rate; + event->data.line_coding.stop_bits = stop_bits; + event->data.line_coding.parity = parity; + event->data.line_coding.data_bits = data_bits; + + if (!this->event_queue_.push(event)) { + ESP_LOGW(TAG, "Event queue full, line coding event dropped (itf=%d)", this->itf_); + // Return event to pool since we couldn't queue it + this->event_pool_.release(event); + } else { + // Wake main loop immediately to process event +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + App.wake_loop_threadsafe(); +#endif + } +} + +void USBCDCACMInstance::process_events_() { + // Process all pending events from the queue + CDCEvent *event; + while ((event = this->event_queue_.pop()) != nullptr) { + switch (event->type) { + case CDC_EVENT_LINE_STATE_CHANGED: { + bool dtr = event->data.line_state.dtr; + bool rts = event->data.line_state.rts; + + // Invoke user callback in main loop context + if (this->line_state_callback_ != nullptr) { + this->line_state_callback_(dtr, rts); + } + break; + } + case CDC_EVENT_LINE_CODING_CHANGED: { + uint32_t bit_rate = event->data.line_coding.bit_rate; + uint8_t stop_bits = event->data.line_coding.stop_bits; + uint8_t parity = event->data.line_coding.parity; + uint8_t data_bits = event->data.line_coding.data_bits; + + // Update UART configuration based on CDC line coding + this->baud_rate_ = bit_rate; + this->data_bits_ = data_bits; + + // Convert CDC stop bits to UART stop bits format + // CDC: 0=1 stop bit, 1=1.5 stop bits, 2=2 stop bits + this->stop_bits_ = (stop_bits == 0) ? 1 : (stop_bits == 1) ? 1 : 2; + + // Convert CDC parity to UART parity format + // CDC: 0=None, 1=Odd, 2=Even, 3=Mark, 4=Space + switch (parity) { + case 0: + this->parity_ = uart::UART_CONFIG_PARITY_NONE; + break; + case 1: + this->parity_ = uart::UART_CONFIG_PARITY_ODD; + break; + case 2: + this->parity_ = uart::UART_CONFIG_PARITY_EVEN; + break; + default: + // Mark and Space parity are not commonly supported, default to None + this->parity_ = uart::UART_CONFIG_PARITY_NONE; + break; + } + + // Invoke user callback in main loop context + if (this->line_coding_callback_ != nullptr) { + this->line_coding_callback_(bit_rate, stop_bits, parity, data_bits); + } + break; + } + } + // Return event to pool for reuse + this->event_pool_.release(event); + } +} + +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); + ESP_LOGVV(TAG, "data = %s", format_hex_pretty(data, tx_data_size).c_str()); + + // 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 +//============================================================================== + +USBCDCACMComponent::USBCDCACMComponent() { global_usb_cdc_component = this; } + +void USBCDCACMComponent::setup() { + // Setup all registered interfaces + for (auto interface : this->interfaces_) { + if (interface != nullptr) { + interface->setup(); + } + } +} + +void USBCDCACMComponent::loop() { + // Call loop() on all registered interfaces to process events + for (auto interface : this->interfaces_) { + if (interface != nullptr) { + interface->loop(); + } + } +} + +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); +} + +void USBCDCACMComponent::add_interface(USBCDCACMInstance *interface) { + uint8_t itf_num = static_cast(interface->get_itf()); + if (itf_num < MAX_USB_CDC_INSTANCES) { + this->interfaces_[itf_num] = interface; + } else { + ESP_LOGE(TAG, "Interface number must be less than %u", 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))) { + return interface; + } + } + return nullptr; +} + +} // namespace esphome::usb_cdc_acm +#endif diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm.h b/esphome/components/usb_cdc_acm/usb_cdc_acm.h new file mode 100644 index 0000000000..8c00f5d52f --- /dev/null +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm.h @@ -0,0 +1,135 @@ +#pragma once +#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + +#include "esphome/core/component.h" +#include "esphome/core/event_pool.h" +#include "esphome/core/lock_free_queue.h" +#include "esphome/components/uart/uart_component.h" + +#include +#include "freertos/ringbuf.h" +#include "tusb_cdc_acm.h" + +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; +using LineStateCallback = std::function; + +// Event types +enum CDCEventType : uint8_t { + CDC_EVENT_LINE_STATE_CHANGED, + CDC_EVENT_LINE_CODING_CHANGED, +}; + +// Event structure for the queue +struct CDCEvent { + CDCEventType type; + union { + struct { + bool dtr; + bool rts; + } line_state; + struct { + uint32_t bit_rate; + uint8_t stop_bits; + uint8_t parity; + uint8_t data_bits; + } line_coding; + } data; + + // Required by EventPool - called before returning to pool + void release() { + // No dynamic memory to clean up, data is stored inline + } +}; + +// Forward declaration +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(); + + // Get the CDC port number for this instance + tinyusb_cdcacm_itf_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_; } + + // Task handle accessor for notifying TX task + TaskHandle_t get_tx_task_handle() const { return this->usb_tx_task_handle_; } + + // Callback registration for line coding and line state changes + void set_line_coding_callback(LineCodingCallback callback) { this->line_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 + 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); + + static void usb_tx_task_fn(void *arg); + void usb_tx_task(); + + // UARTComponent interface implementation + void write_array(const uint8_t *data, size_t len) override; + bool peek_byte(uint8_t *data) override; + bool read_array(uint8_t *data, size_t len) override; + int available() override; + void flush() override; + + protected: + void check_logger_conflict() override {} + + // Process queued events and invoke callbacks (called from main loop) + void process_events_(); + + TaskHandle_t usb_tx_task_handle_{nullptr}; + tinyusb_cdcacm_itf_t itf_{TINYUSB_CDC_ACM_0}; + + RingbufHandle_t usb_tx_ringbuf_{nullptr}; + RingbufHandle_t usb_rx_ringbuf_{nullptr}; + + // User-registered callbacks (called from main loop) + LineCodingCallback line_coding_callback_{nullptr}; + LineStateCallback line_state_callback_{nullptr}; + + // Lock-free queue and event pool for cross-task event passing + EventPool 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 +class USBCDCACMComponent : public Component { + public: + USBCDCACMComponent(); + + void setup() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::IO; } + + // Interface management + void add_interface(USBCDCACMInstance *interface); + USBCDCACMInstance *get_interface_by_number(uint8_t itf); + + protected: + std::array interfaces_{nullptr, nullptr}; +}; + +extern USBCDCACMComponent *global_usb_cdc_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace esphome::usb_cdc_acm +#endif diff --git a/tests/components/usb_cdc_acm/test.esp32-p4-idf.yaml b/tests/components/usb_cdc_acm/test.esp32-p4-idf.yaml new file mode 100644 index 0000000000..4786c96bcc --- /dev/null +++ b/tests/components/usb_cdc_acm/test.esp32-p4-idf.yaml @@ -0,0 +1,5 @@ +<<: !include tinyusb_common.yaml + +usb_cdc_acm: + interfaces: + id: usb_cdc_acm1 diff --git a/tests/components/usb_cdc_acm/test.esp32-s2-idf.yaml b/tests/components/usb_cdc_acm/test.esp32-s2-idf.yaml new file mode 100644 index 0000000000..f159b38ff6 --- /dev/null +++ b/tests/components/usb_cdc_acm/test.esp32-s2-idf.yaml @@ -0,0 +1,5 @@ +<<: !include tinyusb_common.yaml + +usb_cdc_acm: + interfaces: + - id: usb_cdc_acm1 diff --git a/tests/components/usb_cdc_acm/test.esp32-s3-idf.yaml b/tests/components/usb_cdc_acm/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..6913fe21d5 --- /dev/null +++ b/tests/components/usb_cdc_acm/test.esp32-s3-idf.yaml @@ -0,0 +1,6 @@ +<<: !include tinyusb_common.yaml + +usb_cdc_acm: + interfaces: + - id: usb_cdc_acm1 + - id: usb_cdc_acm2 diff --git a/tests/components/usb_cdc_acm/tinyusb_common.yaml b/tests/components/usb_cdc_acm/tinyusb_common.yaml new file mode 100644 index 0000000000..cb3f48836a --- /dev/null +++ b/tests/components/usb_cdc_acm/tinyusb_common.yaml @@ -0,0 +1,8 @@ +tinyusb: + id: tinyusb_test + usb_lang_id: 0x0123 + usb_manufacturer_str: ESPHomeTestManufacturer + usb_product_id: 0x1234 + usb_product_str: ESPHomeTestProduct + usb_serial_str: ESPHomeTestSerialNumber + usb_vendor_id: 0x2345