ESPHome 2026.1.4
Loading...
Searching...
No Matches
usb_host_client.cpp
Go to the documentation of this file.
1// Should not be needed, but it's required to pass CI clang-tidy checks
2#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
3#include "usb_host.h"
4#include "esphome/core/log.h"
5#include "esphome/core/hal.h"
8
9#include <cinttypes>
10#include <cstring>
11#include <atomic>
12namespace esphome {
13namespace usb_host {
14
15#pragma GCC diagnostic ignored "-Wparentheses"
16
17using namespace bytebuffer;
18
19#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
20static void print_ep_desc(const usb_ep_desc_t *ep_desc) {
21 const char *ep_type_str;
22 int type = ep_desc->bmAttributes & USB_BM_ATTRIBUTES_XFERTYPE_MASK;
23
24 switch (type) {
25 case USB_BM_ATTRIBUTES_XFER_CONTROL:
26 ep_type_str = "CTRL";
27 break;
28 case USB_BM_ATTRIBUTES_XFER_ISOC:
29 ep_type_str = "ISOC";
30 break;
31 case USB_BM_ATTRIBUTES_XFER_BULK:
32 ep_type_str = "BULK";
33 break;
34 case USB_BM_ATTRIBUTES_XFER_INT:
35 ep_type_str = "INT";
36 break;
37 default:
38 ep_type_str = NULL;
39 break;
40 }
41
42 ESP_LOGV(TAG,
43 "\t\t*** Endpoint descriptor ***\n"
44 "\t\tbLength %d\n"
45 "\t\tbDescriptorType %d\n"
46 "\t\tbEndpointAddress 0x%x\tEP %d %s\n"
47 "\t\tbmAttributes 0x%x\t%s\n"
48 "\t\twMaxPacketSize %d\n"
49 "\t\tbInterval %d",
50 ep_desc->bLength, ep_desc->bDescriptorType, ep_desc->bEndpointAddress, USB_EP_DESC_GET_EP_NUM(ep_desc),
51 USB_EP_DESC_GET_EP_DIR(ep_desc) ? "IN" : "OUT", ep_desc->bmAttributes, ep_type_str, ep_desc->wMaxPacketSize,
52 ep_desc->bInterval);
53}
54
55static void usbh_print_intf_desc(const usb_intf_desc_t *intf_desc) {
56 ESP_LOGV(TAG,
57 "\t*** Interface descriptor ***\n"
58 "\tbLength %d\n"
59 "\tbDescriptorType %d\n"
60 "\tbInterfaceNumber %d\n"
61 "\tbAlternateSetting %d\n"
62 "\tbNumEndpoints %d\n"
63 "\tbInterfaceClass 0x%x\n"
64 "\tiInterface %d",
65 intf_desc->bLength, intf_desc->bDescriptorType, intf_desc->bInterfaceNumber, intf_desc->bAlternateSetting,
66 intf_desc->bNumEndpoints, intf_desc->bInterfaceProtocol, intf_desc->iInterface);
67}
68
69static void usbh_print_cfg_desc(const usb_config_desc_t *cfg_desc) {
70 ESP_LOGV(TAG,
71 "*** Configuration descriptor ***\n"
72 "bLength %d\n"
73 "bDescriptorType %d\n"
74 "wTotalLength %d\n"
75 "bNumInterfaces %d\n"
76 "bConfigurationValue %d\n"
77 "iConfiguration %d\n"
78 "bmAttributes 0x%x\n"
79 "bMaxPower %dmA",
80 cfg_desc->bLength, cfg_desc->bDescriptorType, cfg_desc->wTotalLength, cfg_desc->bNumInterfaces,
81 cfg_desc->bConfigurationValue, cfg_desc->iConfiguration, cfg_desc->bmAttributes, cfg_desc->bMaxPower * 2);
82}
83
84static void usb_client_print_device_descriptor(const usb_device_desc_t *devc_desc) {
85 if (devc_desc == NULL) {
86 return;
87 }
88
89 ESP_LOGV(TAG,
90 "*** Device descriptor ***\n"
91 "bLength %d\n"
92 "bDescriptorType %d\n"
93 "bcdUSB %d.%d0\n"
94 "bDeviceClass 0x%x\n"
95 "bDeviceSubClass 0x%x\n"
96 "bDeviceProtocol 0x%x\n"
97 "bMaxPacketSize0 %d\n"
98 "idVendor 0x%x\n"
99 "idProduct 0x%x\n"
100 "bcdDevice %d.%d0\n"
101 "iManufacturer %d\n"
102 "iProduct %d\n"
103 "iSerialNumber %d\n"
104 "bNumConfigurations %d",
105 devc_desc->bLength, devc_desc->bDescriptorType, ((devc_desc->bcdUSB >> 8) & 0xF),
106 ((devc_desc->bcdUSB >> 4) & 0xF), devc_desc->bDeviceClass, devc_desc->bDeviceSubClass,
107 devc_desc->bDeviceProtocol, devc_desc->bMaxPacketSize0, devc_desc->idVendor, devc_desc->idProduct,
108 ((devc_desc->bcdDevice >> 8) & 0xF), ((devc_desc->bcdDevice >> 4) & 0xF), devc_desc->iManufacturer,
109 devc_desc->iProduct, devc_desc->iSerialNumber, devc_desc->bNumConfigurations);
110}
111
112static void usb_client_print_config_descriptor(const usb_config_desc_t *cfg_desc,
113 print_class_descriptor_cb class_specific_cb) {
114 if (cfg_desc == nullptr) {
115 return;
116 }
117
118 int offset = 0;
119 uint16_t w_total_length = cfg_desc->wTotalLength;
120 const usb_standard_desc_t *next_desc = (const usb_standard_desc_t *) cfg_desc;
121
122 do {
123 switch (next_desc->bDescriptorType) {
124 case USB_W_VALUE_DT_CONFIG:
125 usbh_print_cfg_desc((const usb_config_desc_t *) next_desc);
126 break;
127 case USB_W_VALUE_DT_INTERFACE:
128 usbh_print_intf_desc((const usb_intf_desc_t *) next_desc);
129 break;
130 case USB_W_VALUE_DT_ENDPOINT:
131 print_ep_desc((const usb_ep_desc_t *) next_desc);
132 break;
133 default:
134 if (class_specific_cb) {
135 class_specific_cb(next_desc);
136 }
137 break;
138 }
139
140 next_desc = usb_parse_next_descriptor(next_desc, w_total_length, &offset);
141
142 } while (next_desc != NULL);
143}
144#endif
145static std::string get_descriptor_string(const usb_str_desc_t *desc) {
146 char buffer[256];
147 if (desc == nullptr)
148 return "(unspecified)";
149 char *p = buffer;
150 for (int i = 0; i != desc->bLength / 2; i++) {
151 auto c = desc->wData[i];
152 if (c < 0x100)
153 *p++ = static_cast<char>(c);
154 }
155 *p = '\0';
156 return {buffer};
157}
158
159// CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task)
160static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void *ptr) {
161 auto *client = static_cast<USBClient *>(ptr);
162
163 // Allocate event from pool
164 UsbEvent *event = client->event_pool.allocate();
165 if (event == nullptr) {
166 // No events available - increment counter for periodic logging
167 client->event_queue.increment_dropped_count();
168 return;
169 }
170
171 // Queue events to be processed in main loop
172 switch (event_msg->event) {
173 case USB_HOST_CLIENT_EVENT_NEW_DEV: {
174 ESP_LOGD(TAG, "New device %d", event_msg->new_dev.address);
175 event->type = EVENT_DEVICE_NEW;
176 event->data.device_new.address = event_msg->new_dev.address;
177 break;
178 }
179 case USB_HOST_CLIENT_EVENT_DEV_GONE: {
180 ESP_LOGD(TAG, "Device gone");
181 event->type = EVENT_DEVICE_GONE;
182 event->data.device_gone.handle = event_msg->dev_gone.dev_hdl;
183 break;
184 }
185 default:
186 ESP_LOGD(TAG, "Unknown event %d", event_msg->event);
187 client->event_pool.release(event);
188 return;
189 }
190
191 // Push to lock-free queue (always succeeds since pool size == queue size)
192 client->event_queue.push(event);
193
194 // Wake main loop immediately to process USB event instead of waiting for select() timeout
195#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
197#endif
198}
200 usb_host_client_config_t config{.is_synchronous = false,
201 .max_num_event_msg = 5,
202 .async = {.client_event_callback = client_event_cb, .callback_arg = this}};
203 auto err = usb_host_client_register(&config, &this->handle_);
204 if (err != ESP_OK) {
205 ESP_LOGE(TAG, "client register failed: %s", esp_err_to_name(err));
206 this->status_set_error(LOG_STR("Client register failed"));
207 this->mark_failed();
208 return;
209 }
210 // Pre-allocate USB transfer buffers for all slots at startup
211 // This avoids any dynamic allocation during runtime
212 for (auto &request : this->requests_) {
213 usb_host_transfer_alloc(64, 0, &request.transfer);
214 request.client = this; // Set once, never changes
215 }
216
217 // Create and start USB task
218 xTaskCreate(usb_task_fn, "usb_task",
219 USB_TASK_STACK_SIZE, // Stack size
220 this, // Task parameter
221 USB_TASK_PRIORITY, // Priority (higher than main loop)
222 &this->usb_task_handle_);
223
224 if (this->usb_task_handle_ == nullptr) {
225 ESP_LOGE(TAG, "Failed to create USB task");
226 this->mark_failed();
227 }
228}
229
230void USBClient::usb_task_fn(void *arg) {
231 auto *client = static_cast<USBClient *>(arg);
232 client->usb_task_loop();
233}
235 while (true) {
236 usb_host_client_handle_events(this->handle_, portMAX_DELAY);
237 }
238}
239
241 // Process any events from the USB task
242 UsbEvent *event;
243 while ((event = this->event_queue.pop()) != nullptr) {
244 switch (event->type) {
245 case EVENT_DEVICE_NEW:
246 this->on_opened(event->data.device_new.address);
247 break;
249 this->on_removed(event->data.device_gone.handle);
250 break;
251 }
252 // Return event to pool for reuse
253 this->event_pool.release(event);
254 }
255
256 // Log dropped events periodically
257 uint16_t dropped = this->event_queue.get_and_reset_dropped_count();
258 if (dropped > 0) {
259 ESP_LOGW(TAG, "Dropped %u USB events due to queue overflow", dropped);
260 }
261
262 switch (this->state_) {
263 case USB_CLIENT_OPEN: {
264 int err;
265 ESP_LOGD(TAG, "Open device %d", this->device_addr_);
266 err = usb_host_device_open(this->handle_, this->device_addr_, &this->device_handle_);
267 if (err != ESP_OK) {
268 ESP_LOGW(TAG, "Device open failed: %s", esp_err_to_name(err));
269 this->state_ = USB_CLIENT_INIT;
270 break;
271 }
272 ESP_LOGD(TAG, "Get descriptor device %d", this->device_addr_);
273 const usb_device_desc_t *desc;
274 err = usb_host_get_device_descriptor(this->device_handle_, &desc);
275 if (err != ESP_OK) {
276 ESP_LOGW(TAG, "Device get_desc failed: %s", esp_err_to_name(err));
277 this->disconnect();
278 } else {
279 ESP_LOGD(TAG, "Device descriptor: vid %X pid %X", desc->idVendor, desc->idProduct);
280 if (desc->idVendor == this->vid_ && desc->idProduct == this->pid_ || this->vid_ == 0 && this->pid_ == 0) {
281 usb_device_info_t dev_info;
282 err = usb_host_device_info(this->device_handle_, &dev_info);
283 if (err != ESP_OK) {
284 ESP_LOGW(TAG, "Device info failed: %s", esp_err_to_name(err));
285 this->disconnect();
286 break;
287 }
289 ESP_LOGD(TAG, "Device connected: Manuf: %s; Prod: %s; Serial: %s",
290 get_descriptor_string(dev_info.str_desc_manufacturer).c_str(),
291 get_descriptor_string(dev_info.str_desc_product).c_str(),
292 get_descriptor_string(dev_info.str_desc_serial_num).c_str());
293
294#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
295 const usb_device_desc_t *device_desc;
296 err = usb_host_get_device_descriptor(this->device_handle_, &device_desc);
297 if (err == ESP_OK)
298 usb_client_print_device_descriptor(device_desc);
299 const usb_config_desc_t *config_desc;
300 err = usb_host_get_active_config_descriptor(this->device_handle_, &config_desc);
301 if (err == ESP_OK)
302 usb_client_print_config_descriptor(config_desc, nullptr);
303#endif
304 this->on_connected();
305 } else {
306 ESP_LOGD(TAG, "Not our device, closing");
307 this->disconnect();
308 }
309 }
310 break;
311 }
312
313 default:
314 break;
315 }
316}
317
318void USBClient::on_opened(uint8_t addr) {
319 if (this->state_ == USB_CLIENT_INIT) {
320 this->device_addr_ = addr;
321 this->state_ = USB_CLIENT_OPEN;
322 }
323}
324void USBClient::on_removed(usb_device_handle_t handle) {
325 if (this->device_handle_ == handle) {
326 this->disconnect();
327 }
328}
329
330// CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task)
331static void control_callback(const usb_transfer_t *xfer) {
332 auto *trq = static_cast<TransferRequest *>(xfer->context);
333 trq->status.error_code = xfer->status;
334 trq->status.success = xfer->status == USB_TRANSFER_STATUS_COMPLETED;
335 trq->status.endpoint = xfer->bEndpointAddress;
336 trq->status.data = xfer->data_buffer;
337 trq->status.data_len = xfer->actual_num_bytes;
338
339 // Execute callback in USB task context
340 if (trq->callback != nullptr) {
341 trq->callback(trq->status);
342 }
343
344 // Release transfer slot immediately in USB task
345 // The release_trq() uses thread-safe atomic operations
346 trq->client->release_trq(trq);
347}
348
349// THREAD CONTEXT: Called from both USB task and main loop threads (multi-consumer)
350// - USB task: USB UART input callbacks restart transfers for immediate data reception
351// - Main loop: Output transfers and flow-controlled input restarts after consuming data
352//
353// THREAD SAFETY: Lock-free using atomic compare-and-swap on bitmask
354// This multi-threaded access is intentional for performance - USB task can
355// immediately restart transfers without waiting for main loop scheduling.
357 trq_bitmask_t mask = this->trq_in_use_.load(std::memory_order_acquire);
358
359 // Find first available slot (bit = 0) and try to claim it atomically
360 // We use a while loop to allow retrying the same slot after CAS failure
361 for (;;) {
362 if (mask == ALL_REQUESTS_IN_USE) {
363 ESP_LOGE(TAG, "All %zu transfer slots in use", MAX_REQUESTS);
364 return nullptr;
365 }
366 // find the least significant zero bit
367 trq_bitmask_t lsb = ~mask & (mask + 1);
368
369 // Slot i appears available, try to claim it atomically
370 trq_bitmask_t desired = mask | lsb;
371
372 if (this->trq_in_use_.compare_exchange_weak(mask, desired, std::memory_order::acquire)) {
373 auto i = __builtin_ctz(lsb); // count trailing zeroes
374 // Successfully claimed slot i - prepare the TransferRequest
375 auto *trq = &this->requests_[i];
376 trq->transfer->context = trq;
377 trq->transfer->device_handle = this->device_handle_;
378 return trq;
379 }
380 // CAS failed - another thread modified the bitmask
381 // mask was already updated by compare_exchange_weak with the current value
382 }
383}
384
386 this->on_disconnected();
387 auto err = usb_host_device_close(this->handle_, this->device_handle_);
388 if (err != ESP_OK) {
389 ESP_LOGE(TAG, "Device close failed: %s", esp_err_to_name(err));
390 }
391 this->state_ = USB_CLIENT_INIT;
392 this->device_handle_ = nullptr;
393 this->device_addr_ = -1;
394}
395
396// THREAD CONTEXT: Called from main loop thread only
397// - Used for device configuration and control operations
398bool USBClient::control_transfer(uint8_t type, uint8_t request, uint16_t value, uint16_t index,
399 const transfer_cb_t &callback, const std::vector<uint8_t> &data) {
400 auto *trq = this->get_trq_();
401 if (trq == nullptr)
402 return false;
403 auto length = data.size();
404 if (length > sizeof(trq->transfer->data_buffer_size) - SETUP_PACKET_SIZE) {
405 ESP_LOGE(TAG, "Control transfer data size too large: %u > %u", length,
406 sizeof(trq->transfer->data_buffer_size) - sizeof(usb_setup_packet_t));
407 this->release_trq(trq);
408 return false;
409 }
410 auto control_packet = ByteBuffer(SETUP_PACKET_SIZE, LITTLE);
411 control_packet.put_uint8(type);
412 control_packet.put_uint8(request);
413 control_packet.put_uint16(value);
414 control_packet.put_uint16(index);
415 control_packet.put_uint16(length);
416 memcpy(trq->transfer->data_buffer, control_packet.get_data().data(), SETUP_PACKET_SIZE);
417 if (length != 0 && !(type & USB_DIR_IN)) {
418 memcpy(trq->transfer->data_buffer + SETUP_PACKET_SIZE, data.data(), length);
419 }
420 trq->callback = callback;
421 trq->transfer->bEndpointAddress = type & USB_DIR_MASK;
422 trq->transfer->num_bytes = static_cast<int>(length + SETUP_PACKET_SIZE);
423 trq->transfer->callback = reinterpret_cast<usb_transfer_cb_t>(control_callback);
424 auto err = usb_host_transfer_submit_control(this->handle_, trq->transfer);
425 if (err != ESP_OK) {
426 ESP_LOGE(TAG, "Failed to submit control transfer, err=%s", esp_err_to_name(err));
427 this->release_trq(trq);
428 return false;
429 }
430 return true;
431}
432
433// CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task)
434static void transfer_callback(usb_transfer_t *xfer) {
435 auto *trq = static_cast<TransferRequest *>(xfer->context);
436 trq->status.error_code = xfer->status;
437 trq->status.success = xfer->status == USB_TRANSFER_STATUS_COMPLETED;
438 trq->status.endpoint = xfer->bEndpointAddress;
439 trq->status.data = xfer->data_buffer;
440 trq->status.data_len = xfer->actual_num_bytes;
441
442 // Always execute callback in USB task context
443 // Callbacks should be fast and non-blocking (e.g., copy data to queue)
444 if (trq->callback != nullptr) {
445 trq->callback(trq->status);
446 }
447
448 // Release transfer slot AFTER callback completes to prevent slot exhaustion
449 // This is critical for high-throughput transfers (e.g., USB UART at 115200 baud)
450 // The callback has finished accessing xfer->data_buffer, so it's safe to release
451 // The release_trq() uses thread-safe atomic operations
452 trq->client->release_trq(trq);
453}
466bool USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length) {
467 auto *trq = this->get_trq_();
468 if (trq == nullptr) {
469 ESP_LOGE(TAG, "Too many requests queued");
470 return false;
471 }
472 trq->callback = callback;
473 trq->transfer->callback = transfer_callback;
474 trq->transfer->bEndpointAddress = ep_address | USB_DIR_IN;
475 trq->transfer->num_bytes = length;
476 auto err = usb_host_transfer_submit(trq->transfer);
477 if (err != ESP_OK) {
478 ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err);
479 this->release_trq(trq);
480 return false;
481 }
482 return true;
483}
484
498bool USBClient::transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length) {
499 auto *trq = this->get_trq_();
500 if (trq == nullptr) {
501 ESP_LOGE(TAG, "Too many requests queued");
502 return false;
503 }
504 trq->callback = callback;
505 trq->transfer->callback = transfer_callback;
506 trq->transfer->bEndpointAddress = ep_address | USB_DIR_OUT;
507 trq->transfer->num_bytes = length;
508 memcpy(trq->transfer->data_buffer, data, length);
509 auto err = usb_host_transfer_submit(trq->transfer);
510 if (err != ESP_OK) {
511 ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err);
512 this->release_trq(trq);
513 return false;
514 }
515 return true;
516}
518 ESP_LOGCONFIG(TAG,
519 "USBClient\n"
520 " Vendor id %04X\n"
521 " Product id %04X",
522 this->vid_, this->pid_);
523}
524// THREAD CONTEXT: Called from both USB task and main loop threads
525// - USB task: Immediately after transfer callback completes
526// - Main loop: When transfer submission fails
527//
528// THREAD SAFETY: Lock-free using atomic AND to clear bit
529// Thread-safe atomic operation allows multithreaded deallocation
531 if (trq == nullptr)
532 return;
533
534 // Calculate index from pointer arithmetic
535 size_t index = trq - this->requests_;
536 if (index >= MAX_REQUESTS) {
537 ESP_LOGE(TAG, "Invalid TransferRequest pointer");
538 return;
539 }
540
541 // Atomically clear the bit to mark slot as available
542 // fetch_and with inverted bitmask clears the bit atomically
543 trq_bitmask_t mask = ~(static_cast<trq_bitmask_t>(1) << index);
544 this->trq_in_use_.fetch_and(mask, std::memory_order_release);
545}
546
547} // namespace usb_host
548} // namespace esphome
549#endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3
void wake_loop_threadsafe()
Wake the main event loop from a FreeRTOS task Thread-safe, can be called from task context to immedia...
virtual void mark_failed()
Mark this component as failed.
A class modelled on the Java ByteBuffer class.
Definition bytebuffer.h:38
usb_host_client_handle_t handle_
Definition usb_host.h:165
TransferRequest requests_[MAX_REQUESTS]
Definition usb_host.h:176
bool transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length)
Performs an output transfer operation.
void release_trq(TransferRequest *trq)
static void usb_task_fn(void *arg)
virtual void on_connected()
Definition usb_host.h:153
TaskHandle_t usb_task_handle_
Definition usb_host.h:163
EventPool< UsbEvent, USB_EVENT_QUEUE_SIZE > event_pool
Definition usb_host.h:148
virtual void on_disconnected()
Definition usb_host.h:154
bool control_transfer(uint8_t type, uint8_t request, uint16_t value, uint16_t index, const transfer_cb_t &callback, const std::vector< uint8_t > &data={})
bool transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length)
Performs a transfer input operation.
void on_removed(usb_device_handle_t handle)
usb_device_handle_t device_handle_
Definition usb_host.h:166
std::atomic< trq_bitmask_t > trq_in_use_
Definition usb_host.h:175
LockFreeQueue< UsbEvent, USB_EVENT_QUEUE_SIZE > event_queue
Definition usb_host.h:147
uint16_t type
std::conditional<(MAX_REQUESTS<=16), uint16_t, uint32_t >::type trq_bitmask_t
Definition usb_host.h:67
std::function< void(const TransferStatus &)> transfer_cb_t
Definition usb_host.h:84
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
Application App
Global storage of Application pointer - only one Application can exist.
struct esphome::usb_host::UsbEvent::@162::@163 device_new
usb_device_handle_t handle
Definition usb_host.h:108
struct esphome::usb_host::UsbEvent::@162::@164 device_gone
union esphome::usb_host::UsbEvent::@162 data
uint16_t length
Definition tt21100.cpp:0