From a3f279c1cf150f6908b84df9976e72821995e7b0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 19:21:14 -0600 Subject: [PATCH] [usb_host] Implement disable_loop/enable_loop pattern for USB components (#14163) --- esphome/components/usb_host/usb_host.h | 21 +++++++++++-------- .../components/usb_host/usb_host_client.cpp | 17 ++++++++++++++- esphome/components/usb_uart/usb_uart.cpp | 11 +++++++++- 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/esphome/components/usb_host/usb_host.h b/esphome/components/usb_host/usb_host.h index d1ec356613..a6a97d0bd7 100644 --- a/esphome/components/usb_host/usb_host.h +++ b/esphome/components/usb_host/usb_host.h @@ -73,12 +73,12 @@ static constexpr UBaseType_t USB_TASK_PRIORITY = 5; // Higher priority than mai // used to report a transfer status struct TransferStatus { - bool success; - uint16_t error_code; uint8_t *data; size_t data_len; - uint8_t endpoint; void *user_data; + uint16_t error_code; + uint8_t endpoint; + bool success; }; using transfer_cb_t = std::function; @@ -127,7 +127,7 @@ class USBClient : public Component { friend class USBHost; public: - USBClient(uint16_t vid, uint16_t pid) : vid_(vid), pid_(pid), trq_in_use_(0) {} + USBClient(uint16_t vid, uint16_t pid) : trq_in_use_(0), vid_(vid), pid_(pid) {} void setup() override; void loop() override; // setup must happen after the host bus has been setup @@ -148,6 +148,10 @@ class USBClient : public Component { EventPool event_pool; protected: + // Process USB events from the queue. Returns true if any work was done. + // Subclasses should call this instead of USBClient::loop() to combine + // with their own work check for a single disable_loop() decision. + bool process_usb_events_(); void handle_open_state_(); TransferRequest *get_trq_(); // Lock-free allocation using atomic bitmask (multi-consumer safe) virtual void disconnect(); @@ -161,20 +165,19 @@ class USBClient : public Component { static void usb_task_fn(void *arg); [[noreturn]] void usb_task_loop() const; + // Members ordered to minimize struct padding on 32-bit platforms + TransferRequest requests_[MAX_REQUESTS]{}; TaskHandle_t usb_task_handle_{nullptr}; - usb_host_client_handle_t handle_{}; usb_device_handle_t device_handle_{}; int device_addr_{-1}; int state_{USB_CLIENT_INIT}; - uint16_t vid_{}; - uint16_t pid_{}; // Lock-free pool management using atomic bitmask (no dynamic allocation) // Bit i = 1: requests_[i] is in use, Bit i = 0: requests_[i] is available // Supports multiple concurrent consumers and producers (both threads can allocate/deallocate) - // Bitmask type automatically selected: uint16_t for <= 16 slots, uint32_t for 17-32 slots std::atomic trq_in_use_; - TransferRequest requests_[MAX_REQUESTS]{}; + uint16_t vid_{}; + uint16_t pid_{}; }; class USBHost : public Component { public: diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index 0612d7a841..a9be38fb03 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -197,6 +197,9 @@ static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void * // Push to lock-free queue (always succeeds since pool size == queue size) client->event_queue.push(event); + // Re-enable component loop to process the queued event + client->enable_loop_soon_any_context(); + // Wake main loop immediately to process USB event instead of waiting for select() timeout #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); @@ -243,10 +246,13 @@ void USBClient::usb_task_loop() const { } } -void USBClient::loop() { +bool USBClient::process_usb_events_() { + bool had_work = false; + // Process any events from the USB task UsbEvent *event; while ((event = this->event_queue.pop()) != nullptr) { + had_work = true; switch (event->type) { case EVENT_DEVICE_NEW: this->on_opened(event->data.device_new.address); @@ -266,8 +272,17 @@ void USBClient::loop() { } if (this->state_ == USB_CLIENT_OPEN) { + had_work = true; this->handle_open_state_(); } + + return had_work; +} + +void USBClient::loop() { + if (!this->process_usb_events_()) { + this->disable_loop(); + } } void USBClient::handle_open_state_() { diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index edd01c26c6..5c2806c456 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -172,11 +172,12 @@ bool USBUartChannel::read_array(uint8_t *data, size_t len) { } void USBUartComponent::setup() { USBClient::setup(); } void USBUartComponent::loop() { - USBClient::loop(); + bool had_work = this->process_usb_events_(); // Process USB data from the lock-free queue UsbDataChunk *chunk; while ((chunk = this->usb_data_queue_.pop()) != nullptr) { + had_work = true; auto *channel = chunk->channel; #ifdef USE_UART_DEBUGGER @@ -198,6 +199,11 @@ void USBUartComponent::loop() { if (dropped > 0) { ESP_LOGW(TAG, "Dropped %u USB data chunks due to buffer overflow", dropped); } + + // Disable loop when idle. Callbacks re-enable via enable_loop_soon_any_context(). + if (!had_work) { + this->disable_loop(); + } } void USBUartComponent::dump_config() { USBClient::dump_config(); @@ -264,6 +270,9 @@ void USBUartComponent::start_input(USBUartChannel *channel) { // Push always succeeds because pool size == queue size this->usb_data_queue_.push(chunk); + // Re-enable component loop to process the queued data + this->enable_loop_soon_any_context(); + // Wake main loop immediately to process USB data instead of waiting for select() timeout #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe();