diff --git a/esphome/components/usb_host/usb_host.h b/esphome/components/usb_host/usb_host.h index c5466eb1f0..3625100e4a 100644 --- a/esphome/components/usb_host/usb_host.h +++ b/esphome/components/usb_host/usb_host.h @@ -5,7 +5,10 @@ #include "esphome/core/component.h" #include #include "usb/usb_host.h" - +#include +#include +#include +#include #include namespace esphome { @@ -13,6 +16,10 @@ namespace usb_host { static const char *const TAG = "usb_host"; +// Forward declarations +struct TransferRequest; +class USBClient; + // constants for setup packet type static const uint8_t USB_RECIP_DEVICE = 0; static const uint8_t USB_RECIP_INTERFACE = 1; @@ -49,6 +56,30 @@ struct TransferRequest { USBClient *client; }; +// Lightweight event types for queue +enum EventType { + EVENT_DEVICE_NEW, + EVENT_DEVICE_GONE, + EVENT_TRANSFER_COMPLETE, + EVENT_CONTROL_COMPLETE, +}; + +struct UsbEvent { + EventType type; + union { + struct { + uint8_t address; + } device_new; + struct { + usb_device_handle_t handle; + } device_gone; + struct { + TransferRequest *trq; + bool callback_executed; // Flag to indicate callback was already executed in USB task + } transfer; + } data; +}; + // callback function type. enum ClientState { @@ -83,6 +114,7 @@ class USBClient : public Component { void release_trq(TransferRequest *trq); bool control_transfer(uint8_t type, uint8_t request, uint16_t value, uint16_t index, const transfer_cb_t &callback, const std::vector &data = {}); + QueueHandle_t get_event_queue() { return event_queue_; } protected: bool register_(); @@ -91,6 +123,13 @@ class USBClient : public Component { virtual void on_connected() {} virtual void on_disconnected() { this->init_pool(); } + // USB task management + static void usb_task_fn(void *arg); + void usb_task_loop(); + + TaskHandle_t usb_task_handle_{nullptr}; + QueueHandle_t event_queue_{nullptr}; // Queue of UsbEvent structs + usb_host_client_handle_t handle_{}; usb_device_handle_t device_handle_{}; int device_addr_{-1}; diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index 4c0c12fa18..98a1f6178b 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -139,18 +139,25 @@ static std::string get_descriptor_string(const usb_str_desc_t *desc) { return {buffer}; } +// CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task) static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void *ptr) { auto *client = static_cast(ptr); + UsbEvent event; + + // Queue events to be processed in main loop switch (event_msg->event) { case USB_HOST_CLIENT_EVENT_NEW_DEV: { - auto addr = event_msg->new_dev.address; ESP_LOGD(TAG, "New device %d", event_msg->new_dev.address); - client->on_opened(addr); + event.type = EVENT_DEVICE_NEW; + event.data.device_new.address = event_msg->new_dev.address; + xQueueSend(client->get_event_queue(), &event, portMAX_DELAY); break; } case USB_HOST_CLIENT_EVENT_DEV_GONE: { - client->on_removed(event_msg->dev_gone.dev_hdl); - ESP_LOGD(TAG, "Device gone %d", event_msg->new_dev.address); + ESP_LOGD(TAG, "Device gone"); + event.type = EVENT_DEVICE_GONE; + event.data.device_gone.handle = event_msg->dev_gone.dev_hdl; + xQueueSend(client->get_event_queue(), &event, portMAX_DELAY); break; } default: @@ -173,9 +180,66 @@ void USBClient::setup() { usb_host_transfer_alloc(64, 0, &trq->transfer); trq->client = this; } + + // Create event queue for communication between USB task and main loop + this->event_queue_ = xQueueCreate(32, sizeof(UsbEvent)); + if (this->event_queue_ == nullptr) { + ESP_LOGE(TAG, "Failed to create event queue"); + this->mark_failed(); + return; + } + + // Create and start USB task + xTaskCreatePinnedToCore(usb_task_fn, "usb_task", + 2048, // Stack size (minimal - just handles USB events) + this, // Task parameter + 5, // Priority (higher than main loop) + &this->usb_task_handle_, + 1 // Core 1 + ); + + if (this->usb_task_handle_ == nullptr) { + ESP_LOGE(TAG, "Failed to create USB task"); + this->mark_failed(); + } +} + +void USBClient::usb_task_fn(void *arg) { + auto *client = static_cast(arg); + client->usb_task_loop(); +} + +void USBClient::usb_task_loop() { + ESP_LOGI(TAG, "USB task started on core %d", xPortGetCoreID()); + + // Run forever - ESPHome reboots rather than shutting down cleanly + while (true) { + // Handle USB events with a timeout to prevent blocking forever + usb_host_client_handle_events(this->handle_, pdMS_TO_TICKS(10)); + } } void USBClient::loop() { + // Process any events from the USB task + UsbEvent event; + while (xQueueReceive(this->event_queue_, &event, 0) == pdTRUE) { + switch (event.type) { + case EVENT_DEVICE_NEW: + this->on_opened(event.data.device_new.address); + break; + case EVENT_DEVICE_GONE: + this->on_removed(event.data.device_gone.handle); + break; + case EVENT_TRANSFER_COMPLETE: + case EVENT_CONTROL_COMPLETE: { + auto *trq = event.data.transfer.trq; + // Callback was already executed in USB task, just cleanup + this->release_trq(trq); + break; + } + } + } + switch (this->state_) { case USB_CLIENT_OPEN: { int err; @@ -228,7 +292,7 @@ void USBClient::loop() { } default: - usb_host_client_handle_events(this->handle_, 0); + // USB events are now handled in the dedicated task break; } } @@ -245,6 +309,7 @@ void USBClient::on_removed(usb_device_handle_t handle) { } } +// CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task) static void control_callback(const usb_transfer_t *xfer) { auto *trq = static_cast(xfer->context); trq->status.error_code = xfer->status; @@ -252,9 +317,18 @@ static void control_callback(const usb_transfer_t *xfer) { trq->status.endpoint = xfer->bEndpointAddress; trq->status.data = xfer->data_buffer; trq->status.data_len = xfer->actual_num_bytes; - if (trq->callback != nullptr) + + // Execute callback in USB task context + if (trq->callback != nullptr) { trq->callback(trq->status); - trq->client->release_trq(trq); + } + + // Queue cleanup to main loop + UsbEvent event; + event.type = EVENT_CONTROL_COMPLETE; + event.data.transfer.trq = trq; + event.data.transfer.callback_executed = true; + xQueueSend(trq->client->get_event_queue(), &event, portMAX_DELAY); } TransferRequest *USBClient::get_trq_() { @@ -315,6 +389,7 @@ bool USBClient::control_transfer(uint8_t type, uint8_t request, uint16_t value, return true; } +// CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task) static void transfer_callback(usb_transfer_t *xfer) { auto *trq = static_cast(xfer->context); trq->status.error_code = xfer->status; @@ -322,9 +397,19 @@ static void transfer_callback(usb_transfer_t *xfer) { trq->status.endpoint = xfer->bEndpointAddress; trq->status.data = xfer->data_buffer; trq->status.data_len = xfer->actual_num_bytes; - if (trq->callback != nullptr) + + // Always execute callback in USB task context + // Callbacks should be fast and non-blocking (e.g., copy data to queue) + if (trq->callback != nullptr) { trq->callback(trq->status); - trq->client->release_trq(trq); + } + + // Queue cleanup to main loop + UsbEvent event; + event.type = EVENT_TRANSFER_COMPLETE; + event.data.transfer.trq = trq; + event.data.transfer.callback_executed = true; + xQueueSend(trq->client->get_event_queue(), &event, portMAX_DELAY); } /** * Performs a transfer input operation. @@ -345,6 +430,7 @@ void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, u trq->transfer->callback = transfer_callback; trq->transfer->bEndpointAddress = ep_address | USB_DIR_IN; trq->transfer->num_bytes = length; + auto err = usb_host_transfer_submit(trq->transfer); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err); diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index bf1c9086f1..4b2464fd59 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -170,7 +170,37 @@ bool USBUartChannel::read_array(uint8_t *data, size_t len) { return status; } void USBUartComponent::setup() { USBClient::setup(); } -void USBUartComponent::loop() { USBClient::loop(); } +void USBUartComponent::loop() { + USBClient::loop(); + + // Process USB data from the lock-free queue + UsbDataChunk *chunk; + int chunks_processed = 0; + while ((chunk = this->usb_data_queue_.pop()) != nullptr) { + chunks_processed++; + auto *channel = chunk->channel; + +#ifdef USE_UART_DEBUGGER + if (channel->debug_) { + uart::UARTDebug::log_hex(uart::UART_DIRECTION_RX, std::vector(chunk->data, chunk->data + chunk->length), + ','); // NOLINT() + } +#endif + + // Push data to ring buffer (now safe in main loop) + for (size_t i = 0; i < chunk->length; i++) { + channel->input_buffer_.push(chunk->data[i]); + } + + // Return chunk to pool for reuse + this->free_chunks_.push(chunk); + } + + static constexpr int LOG_CHUNK_THRESHOLD = 5; + if (chunks_processed > LOG_CHUNK_THRESHOLD) { + ESP_LOGV(TAG, "Processed %d chunks from USB queue", chunks_processed); + } +} void USBUartComponent::dump_config() { USBClient::dump_config(); for (auto &channel : this->channels_) { @@ -187,31 +217,46 @@ void USBUartComponent::dump_config() { } } void USBUartComponent::start_input(USBUartChannel *channel) { - if (!channel->initialised_ || channel->input_started_ || - channel->input_buffer_.get_free_space() < channel->cdc_dev_.in_ep->wMaxPacketSize) + if (!channel->initialised_ || channel->input_started_) return; + // Note: We no longer check ring buffer space here since this may be called from USB task + // The lock-free queue provides backpressure instead const auto *ep = channel->cdc_dev_.in_ep; + // CALLBACK CONTEXT: This lambda is executed in USB task via transfer_callback auto callback = [this, channel](const usb_host::TransferStatus &status) { ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code); if (!status.success) { ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code)); return; } -#ifdef USE_UART_DEBUGGER - if (channel->debug_) { - uart::UARTDebug::log_hex(uart::UART_DIRECTION_RX, - std::vector(status.data, status.data + status.data_len), ','); // NOLINT() - } -#endif - channel->input_started_ = false; - if (!channel->dummy_receiver_) { - for (size_t i = 0; i != status.data_len; i++) { - channel->input_buffer_.push(status.data[i]); + + if (!channel->dummy_receiver_ && status.data_len > 0) { + // Get a free chunk from the pool + UsbDataChunk *chunk = this->free_chunks_.pop(); + if (chunk == nullptr) { + ESP_LOGW(TAG, "No free chunks available, dropping %u bytes", status.data_len); + // Mark input as not started so we can retry + channel->input_started_ = false; + return; + } + + // Copy data to chunk (this is fast, happens in USB task) + memcpy(chunk->data, status.data, status.data_len); + chunk->length = status.data_len; + chunk->channel = channel; + + // Push to lock-free queue for main loop processing + if (!this->usb_data_queue_.push(chunk)) { + ESP_LOGW(TAG, "USB data queue full, dropping %u bytes", status.data_len); + // Return chunk to pool + this->free_chunks_.push(chunk); } } - if (channel->input_buffer_.get_free_space() >= channel->cdc_dev_.in_ep->wMaxPacketSize) { - this->defer([this, channel] { this->start_input(channel); }); - } + + // Always restart input immediately from USB task + // The lock-free queue will handle backpressure + channel->input_started_ = false; + this->start_input(channel); }; channel->input_started_ = true; this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize); @@ -224,9 +269,12 @@ void USBUartComponent::start_output(USBUartChannel *channel) { return; } const auto *ep = channel->cdc_dev_.out_ep; + // CALLBACK CONTEXT: This lambda is stored in TransferRequest and will be executed + // in MAIN LOOP after being queued by transfer_callback in USB task auto callback = [this, channel](const usb_host::TransferStatus &status) { ESP_LOGV(TAG, "Output Transfer result: length: %u; status %X", status.data_len, status.error_code); channel->output_started_ = false; + // DEFERRED CONTEXT: Main loop (restart output in main loop) this->defer([this, channel] { this->start_output(channel); }); }; channel->output_started_ = true; diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index a103c51add..c1affe2bc9 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -5,11 +5,13 @@ #include "esphome/core/helpers.h" #include "esphome/components/uart/uart_component.h" #include "esphome/components/usb_host/usb_host.h" +#include "esphome/core/lock_free_queue.h" namespace esphome { namespace usb_uart { class USBUartTypeCdcAcm; class USBUartComponent; +class USBUartChannel; static const char *const TAG = "usb_uart"; @@ -68,6 +70,14 @@ class RingBuffer { uint8_t *buffer_; }; +// Structure for queuing received USB data chunks +struct UsbDataChunk { + static constexpr size_t MAX_CHUNK_SIZE = 64; // USB packet size + uint8_t data[MAX_CHUNK_SIZE]; + size_t length; + USBUartChannel *channel; +}; + class USBUartChannel : public uart::UARTComponent, public Parented { friend class USBUartComponent; friend class USBUartTypeCdcAcm; @@ -104,7 +114,18 @@ class USBUartChannel : public uart::UARTComponent, public Parenteddata_chunk_pool_[i] = new UsbDataChunk(); + this->free_chunks_.push(this->data_chunk_pool_[i]); + } + } + ~USBUartComponent() { + for (int i = 0; i < MAX_DATA_CHUNKS; i++) { + delete this->data_chunk_pool_[i]; + } + } void setup() override; void loop() override; void dump_config() override; @@ -115,8 +136,16 @@ class USBUartComponent : public usb_host::USBClient { void start_input(USBUartChannel *channel); void start_output(USBUartChannel *channel); + // Lock-free data transfer from USB task to main loop + LockFreeQueue usb_data_queue_; + protected: std::vector channels_{}; + + // Pool of pre-allocated data chunks to avoid dynamic allocation + static constexpr int MAX_DATA_CHUNKS = 32; + UsbDataChunk *data_chunk_pool_[MAX_DATA_CHUNKS]; + LockFreeQueue free_chunks_; }; class USBUartTypeCdcAcm : public USBUartComponent {