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/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 76a516d90f..0525c93096 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1,6 +1,7 @@ #include "web_server.h" #ifdef USE_WEBSERVER #include "esphome/components/json/json_util.h" +#include "esphome/core/progmem.h" #include "esphome/components/network/util.h" #include "esphome/core/application.h" #include "esphome/core/defines.h" @@ -679,11 +680,11 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM enum SwitchAction { NONE, TOGGLE, TURN_ON, TURN_OFF }; SwitchAction action = NONE; - if (match.method_equals("toggle")) { + if (match.method_equals(ESPHOME_F("toggle"))) { action = TOGGLE; - } else if (match.method_equals("turn_on")) { + } else if (match.method_equals(ESPHOME_F("turn_on"))) { action = TURN_ON; - } else if (match.method_equals("turn_off")) { + } else if (match.method_equals(ESPHOME_F("turn_off"))) { action = TURN_OFF; } @@ -741,7 +742,7 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM auto detail = get_request_detail(request); std::string data = this->button_json_(obj, detail); request->send(200, "application/json", data.c_str()); - } else if (match.method_equals("press")) { + } else if (match.method_equals(ESPHOME_F("press"))) { this->defer([obj]() { obj->press(); }); request->send(200); return; @@ -829,12 +830,12 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc auto detail = get_request_detail(request); std::string data = this->fan_json_(obj, detail); request->send(200, "application/json", data.c_str()); - } else if (match.method_equals("toggle")) { + } else if (match.method_equals(ESPHOME_F("toggle"))) { this->defer([obj]() { obj->toggle().perform(); }); request->send(200); } else { - bool is_on = match.method_equals("turn_on"); - bool is_off = match.method_equals("turn_off"); + bool is_on = match.method_equals(ESPHOME_F("turn_on")); + bool is_off = match.method_equals(ESPHOME_F("turn_off")); if (!is_on && !is_off) { request->send(404); return; @@ -910,12 +911,12 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa auto detail = get_request_detail(request); std::string data = this->light_json_(obj, detail); request->send(200, "application/json", data.c_str()); - } else if (match.method_equals("toggle")) { + } else if (match.method_equals(ESPHOME_F("toggle"))) { this->defer([obj]() { obj->toggle().perform(); }); request->send(200); } else { - bool is_on = match.method_equals("turn_on"); - bool is_off = match.method_equals("turn_off"); + bool is_on = match.method_equals(ESPHOME_F("turn_on")); + bool is_off = match.method_equals(ESPHOME_F("turn_off")); if (!is_on && !is_off) { request->send(404); return; @@ -1014,7 +1015,7 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa } } - if (!found && !match.method_equals("set")) { + if (!found && !match.method_equals(ESPHOME_F("set"))) { request->send(404); return; } @@ -1080,7 +1081,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM request->send(200, "application/json", data.c_str()); return; } - if (!match.method_equals("set")) { + if (!match.method_equals(ESPHOME_F("set"))) { request->send(404); return; } @@ -1147,7 +1148,7 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat request->send(200, "application/json", data.c_str()); return; } - if (!match.method_equals("set")) { + if (!match.method_equals(ESPHOME_F("set"))) { request->send(404); return; } @@ -1211,7 +1212,7 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat request->send(200, "application/json", data.c_str()); return; } - if (!match.method_equals("set")) { + if (!match.method_equals(ESPHOME_F("set"))) { request->send(404); return; } @@ -1274,7 +1275,7 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur request->send(200, "application/json", data.c_str()); return; } - if (!match.method_equals("set")) { + if (!match.method_equals(ESPHOME_F("set"))) { request->send(404); return; } @@ -1340,7 +1341,7 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat request->send(200, "application/json", data.c_str()); return; } - if (!match.method_equals("set")) { + if (!match.method_equals(ESPHOME_F("set"))) { request->send(404); return; } @@ -1398,7 +1399,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM return; } - if (!match.method_equals("set")) { + if (!match.method_equals(ESPHOME_F("set"))) { request->send(404); return; } @@ -1457,7 +1458,7 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url return; } - if (!match.method_equals("set")) { + if (!match.method_equals(ESPHOME_F("set"))) { request->send(404); return; } @@ -1613,11 +1614,11 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat enum LockAction { NONE, LOCK, UNLOCK, OPEN }; LockAction action = NONE; - if (match.method_equals("lock")) { + if (match.method_equals(ESPHOME_F("lock"))) { action = LOCK; - } else if (match.method_equals("unlock")) { + } else if (match.method_equals(ESPHOME_F("unlock"))) { action = UNLOCK; - } else if (match.method_equals("open")) { + } else if (match.method_equals(ESPHOME_F("open"))) { action = OPEN; } @@ -1706,7 +1707,7 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa } } - if (!found && !match.method_equals("set")) { + if (!found && !match.method_equals(ESPHOME_F("set"))) { request->send(404); return; } @@ -1849,7 +1850,7 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons request->send(200, "application/json", data.c_str()); return; } - if (!match.method_equals("set")) { + if (!match.method_equals(ESPHOME_F("set"))) { request->send(404); return; } @@ -2029,7 +2030,7 @@ void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlM return; } - if (!match.method_equals("install")) { + if (!match.method_equals(ESPHOME_F("install"))) { request->send(404); return; } @@ -2244,102 +2245,102 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { if (false) { // Start chain for else-if macro pattern } #ifdef USE_SENSOR - else if (match.domain_equals("sensor")) { + else if (match.domain_equals(ESPHOME_F("sensor"))) { this->handle_sensor_request(request, match); } #endif #ifdef USE_SWITCH - else if (match.domain_equals("switch")) { + else if (match.domain_equals(ESPHOME_F("switch"))) { this->handle_switch_request(request, match); } #endif #ifdef USE_BUTTON - else if (match.domain_equals("button")) { + else if (match.domain_equals(ESPHOME_F("button"))) { this->handle_button_request(request, match); } #endif #ifdef USE_BINARY_SENSOR - else if (match.domain_equals("binary_sensor")) { + else if (match.domain_equals(ESPHOME_F("binary_sensor"))) { this->handle_binary_sensor_request(request, match); } #endif #ifdef USE_FAN - else if (match.domain_equals("fan")) { + else if (match.domain_equals(ESPHOME_F("fan"))) { this->handle_fan_request(request, match); } #endif #ifdef USE_LIGHT - else if (match.domain_equals("light")) { + else if (match.domain_equals(ESPHOME_F("light"))) { this->handle_light_request(request, match); } #endif #ifdef USE_TEXT_SENSOR - else if (match.domain_equals("text_sensor")) { + else if (match.domain_equals(ESPHOME_F("text_sensor"))) { this->handle_text_sensor_request(request, match); } #endif #ifdef USE_COVER - else if (match.domain_equals("cover")) { + else if (match.domain_equals(ESPHOME_F("cover"))) { this->handle_cover_request(request, match); } #endif #ifdef USE_NUMBER - else if (match.domain_equals("number")) { + else if (match.domain_equals(ESPHOME_F("number"))) { this->handle_number_request(request, match); } #endif #ifdef USE_DATETIME_DATE - else if (match.domain_equals("date")) { + else if (match.domain_equals(ESPHOME_F("date"))) { this->handle_date_request(request, match); } #endif #ifdef USE_DATETIME_TIME - else if (match.domain_equals("time")) { + else if (match.domain_equals(ESPHOME_F("time"))) { this->handle_time_request(request, match); } #endif #ifdef USE_DATETIME_DATETIME - else if (match.domain_equals("datetime")) { + else if (match.domain_equals(ESPHOME_F("datetime"))) { this->handle_datetime_request(request, match); } #endif #ifdef USE_TEXT - else if (match.domain_equals("text")) { + else if (match.domain_equals(ESPHOME_F("text"))) { this->handle_text_request(request, match); } #endif #ifdef USE_SELECT - else if (match.domain_equals("select")) { + else if (match.domain_equals(ESPHOME_F("select"))) { this->handle_select_request(request, match); } #endif #ifdef USE_CLIMATE - else if (match.domain_equals("climate")) { + else if (match.domain_equals(ESPHOME_F("climate"))) { this->handle_climate_request(request, match); } #endif #ifdef USE_LOCK - else if (match.domain_equals("lock")) { + else if (match.domain_equals(ESPHOME_F("lock"))) { this->handle_lock_request(request, match); } #endif #ifdef USE_VALVE - else if (match.domain_equals("valve")) { + else if (match.domain_equals(ESPHOME_F("valve"))) { this->handle_valve_request(request, match); } #endif #ifdef USE_ALARM_CONTROL_PANEL - else if (match.domain_equals("alarm_control_panel")) { + else if (match.domain_equals(ESPHOME_F("alarm_control_panel"))) { this->handle_alarm_control_panel_request(request, match); } #endif #ifdef USE_UPDATE - else if (match.domain_equals("update")) { + else if (match.domain_equals(ESPHOME_F("update"))) { this->handle_update_request(request, match); } #endif #ifdef USE_WATER_HEATER - else if (match.domain_equals("water_heater")) { + else if (match.domain_equals(ESPHOME_F("water_heater"))) { this->handle_water_heater_request(request, match); } #endif diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 55fa89679e..91625476f4 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -62,6 +62,12 @@ struct UrlMatch { bool domain_equals(const char *str) const { return this->domain == str; } bool method_equals(const char *str) const { return this->method == str; } +#ifdef USE_ESP8266 + // Overloads for flash strings on ESP8266 + bool domain_equals(const __FlashStringHelper *str) const { return this->domain == str; } + bool method_equals(const __FlashStringHelper *str) const { return this->method == str; } +#endif + /// Match entity by name first, then fall back to object_id with deprecation warning /// Returns EntityMatchResult with match status and whether action segment is empty EntityMatchResult match_entity(EntityBase *entity) const; 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 diff --git a/esphome/core/string_ref.h b/esphome/core/string_ref.h index 505fdd906a..5be4a63183 100644 --- a/esphome/core/string_ref.h +++ b/esphome/core/string_ref.h @@ -11,6 +11,10 @@ #include "esphome/components/json/json_util.h" #endif // USE_JSON +#ifdef USE_ESP8266 +#include +#endif // USE_ESP8266 + namespace esphome { /** @@ -107,6 +111,22 @@ inline bool operator!=(const StringRef &lhs, const char *rhs) { return !(lhs == inline bool operator!=(const char *lhs, const StringRef &rhs) { return !(rhs == lhs); } +#ifdef USE_ESP8266 +inline bool operator==(const StringRef &lhs, const __FlashStringHelper *rhs) { + PGM_P p = reinterpret_cast(rhs); + size_t rhs_len = strlen_P(p); + if (lhs.size() != rhs_len) + return false; + return memcmp_P(lhs.c_str(), p, rhs_len) == 0; +} + +inline bool operator==(const __FlashStringHelper *lhs, const StringRef &rhs) { return rhs == lhs; } + +inline bool operator!=(const StringRef &lhs, const __FlashStringHelper *rhs) { return !(lhs == rhs); } + +inline bool operator!=(const __FlashStringHelper *lhs, const StringRef &rhs) { return !(rhs == lhs); } +#endif // USE_ESP8266 + inline bool operator<(const StringRef &lhs, const StringRef &rhs) { return std::lexicographical_compare(std::begin(lhs), std::end(lhs), std::begin(rhs), std::end(rhs)); }