ESPHome 2026.8.0-dev
Loading...
Searching...
No Matches
usb_uart.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 defined(USE_ESP32_VARIANT_ESP32S31) || defined(USE_ESP32_VARIANT_ESP32H4)
4#include "usb_uart.h"
5#include "esphome/core/log.h"
7
8#include <cinttypes>
9#include <cstring>
10
11namespace esphome::usb_uart {
12
20static optional<CdcEps> get_cdc(const usb_config_desc_t *config_desc, uint8_t intf_idx) {
21 int conf_offset, ep_offset;
22 // look for an interface with an interrupt endpoint (notify), and one with two bulk endpoints (data in/out)
23 CdcEps eps{};
24 eps.bulk_interface_number = 0xFF;
25 eps.interrupt_interface_number = 0xFF;
26 for (;;) {
27 const auto *intf_desc = usb_parse_interface_descriptor(config_desc, intf_idx++, 0, &conf_offset);
28 if (!intf_desc) {
29 ESP_LOGE(TAG, "usb_parse_interface_descriptor failed");
30 return nullopt;
31 }
32 ESP_LOGD(TAG, "intf_desc: bInterfaceClass=%02X, bInterfaceSubClass=%02X, bInterfaceProtocol=%02X, bNumEndpoints=%d",
33 intf_desc->bInterfaceClass, intf_desc->bInterfaceSubClass, intf_desc->bInterfaceProtocol,
34 intf_desc->bNumEndpoints);
35 for (uint8_t i = 0; i != intf_desc->bNumEndpoints; i++) {
36 ep_offset = conf_offset;
37 const auto *ep = usb_parse_endpoint_descriptor_by_index(intf_desc, i, config_desc->wTotalLength, &ep_offset);
38 if (!ep) {
39 ESP_LOGE(TAG, "Ran out of interfaces at %d before finding all endpoints", i);
40 return nullopt;
41 }
42 ESP_LOGD(TAG, "ep: bEndpointAddress=%02X, bmAttributes=%02X", ep->bEndpointAddress, ep->bmAttributes);
43 if (ep->bmAttributes == USB_BM_ATTRIBUTES_XFER_INT) {
44 eps.notify_ep = ep;
45 eps.interrupt_interface_number = intf_desc->bInterfaceNumber;
46 } else if (ep->bmAttributes == USB_BM_ATTRIBUTES_XFER_BULK && ep->bEndpointAddress & usb_host::USB_DIR_IN &&
47 (eps.bulk_interface_number == 0xFF || eps.bulk_interface_number == intf_desc->bInterfaceNumber)) {
48 eps.in_ep = ep;
49 eps.bulk_interface_number = intf_desc->bInterfaceNumber;
50 } else if (ep->bmAttributes == USB_BM_ATTRIBUTES_XFER_BULK && !(ep->bEndpointAddress & usb_host::USB_DIR_IN) &&
51 (eps.bulk_interface_number == 0xFF || eps.bulk_interface_number == intf_desc->bInterfaceNumber)) {
52 eps.out_ep = ep;
53 eps.bulk_interface_number = intf_desc->bInterfaceNumber;
54 } else {
55 ESP_LOGE(TAG, "Unexpected endpoint attributes: %02X", ep->bmAttributes);
56 continue;
57 }
58 }
59 if (eps.in_ep != nullptr && eps.out_ep != nullptr && eps.notify_ep != nullptr)
60 return eps;
61 }
62}
63
64std::vector<CdcEps> USBUartTypeCdcAcm::parse_descriptors(usb_device_handle_t dev_hdl) {
65 const usb_config_desc_t *config_desc;
66 const usb_device_desc_t *device_desc;
67 int desc_offset = 0;
68 std::vector<CdcEps> cdc_devs{};
69
70 // Get required descriptors
71 if (usb_host_get_device_descriptor(dev_hdl, &device_desc) != ESP_OK) {
72 ESP_LOGE(TAG, "get_device_descriptor failed");
73 return {};
74 }
75 if (usb_host_get_active_config_descriptor(dev_hdl, &config_desc) != ESP_OK) {
76 ESP_LOGE(TAG, "get_active_config_descriptor failed");
77 return {};
78 }
79 if (device_desc->bDeviceClass == USB_CLASS_COMM || device_desc->bDeviceClass == USB_CLASS_VENDOR_SPEC) {
80 // single CDC-ACM device
81 if (auto eps = get_cdc(config_desc, 0)) {
82 ESP_LOGV(TAG, "Found CDC-ACM device");
83 cdc_devs.push_back(*eps);
84 }
85 return cdc_devs;
86 }
87 if (((device_desc->bDeviceClass == USB_CLASS_MISC) && (device_desc->bDeviceSubClass == USB_SUBCLASS_COMMON) &&
88 (device_desc->bDeviceProtocol == USB_DEVICE_PROTOCOL_IAD)) ||
89 ((device_desc->bDeviceClass == USB_CLASS_PER_INTERFACE) && (device_desc->bDeviceSubClass == USB_SUBCLASS_NULL) &&
90 (device_desc->bDeviceProtocol == USB_PROTOCOL_NULL))) {
91 // This is a composite device, that uses Interface Association Descriptor
92 const auto *this_desc = reinterpret_cast<const usb_standard_desc_t *>(config_desc);
93 for (;;) {
94 this_desc = usb_parse_next_descriptor_of_type(this_desc, config_desc->wTotalLength,
95 USB_B_DESCRIPTOR_TYPE_INTERFACE_ASSOCIATION, &desc_offset);
96 if (!this_desc)
97 break;
98 const auto *iad_desc = reinterpret_cast<const usb_iad_desc_t *>(this_desc);
99
100 if (iad_desc->bFunctionClass == USB_CLASS_COMM && iad_desc->bFunctionSubClass == USB_CDC_SUBCLASS_ACM) {
101 ESP_LOGV(TAG, "Found CDC-ACM device in composite device");
102 if (auto eps = get_cdc(config_desc, iad_desc->bFirstInterface))
103 cdc_devs.push_back(*eps);
104 }
105 }
106 }
107 return cdc_devs;
108}
109
110void RingBuffer::push(uint8_t item) {
111 if (this->get_free_space() == 0)
112 return;
113 this->buffer_[this->insert_pos_] = item;
114 this->insert_pos_ = (this->insert_pos_ + 1) % this->buffer_size_;
115}
116void RingBuffer::push(const uint8_t *data, size_t len) {
117 size_t free = this->get_free_space();
118 if (len > free)
119 len = free;
120 for (size_t i = 0; i != len; i++) {
121 this->buffer_[this->insert_pos_] = *data++;
122 this->insert_pos_ = (this->insert_pos_ + 1) % this->buffer_size_;
123 }
124}
125
127 uint8_t item = this->buffer_[this->read_pos_];
128 this->read_pos_ = (this->read_pos_ + 1) % this->buffer_size_;
129 return item;
130}
131size_t RingBuffer::pop(uint8_t *data, size_t len) {
132 len = std::min(len, this->get_available());
133 for (size_t i = 0; i != len; i++) {
134 *data++ = this->buffer_[this->read_pos_];
135 this->read_pos_ = (this->read_pos_ + 1) % this->buffer_size_;
136 }
137 return len;
138}
139void USBUartChannel::write_array(const uint8_t *data, size_t len) {
140 if (!this->initialised_.load()) {
141 ESP_LOGD(TAG, "Channel not initialised - write ignored");
142 return;
143 }
144#ifdef USE_UART_DEBUGGER
145 if (this->debug_) {
146 constexpr size_t batch = 16;
147 char buf[format_hex_pretty_size(batch)]; // "XX,XX,...,XX\0"
148 for (size_t off = 0; off < len; off += batch) {
149 size_t n = std::min(len - off, batch);
150 format_hex_pretty_to(buf, data + off, n, ',');
151 ESP_LOGD(TAG, "%s>>> %s", this->debug_prefix_.c_str(), buf);
152 }
153 }
154#endif
155 while (len > 0) {
156 UsbOutputChunk *chunk = this->output_pool_.allocate();
157 if (chunk == nullptr) {
158 ESP_LOGE(TAG, "Output pool full - lost %zu bytes", len);
159 break;
160 }
161 uint16_t chunk_len = std::min(len, UsbOutputChunk::MAX_CHUNK_SIZE);
162 memcpy(chunk->data, data, chunk_len);
163 chunk->length = chunk_len;
164 // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
165 // allocate() returned non-null, the queue cannot be full.
166 this->output_queue_.push(chunk);
167 data += chunk_len;
168 len -= chunk_len;
169 }
170 this->parent_->start_output(this);
171}
172
174 // Spin until the output queue is drained and the last USB transfer completes.
175 // Safe to call from the main loop only.
176 // The flush_timeout_ms_ timeout guards against a device that stops responding mid-flush;
177 // in that case the main loop is blocked for the full duration.
178 uint32_t start = millis();
179 while ((!this->output_queue_.empty() || this->output_started_.load()) && millis() - start < this->flush_timeout_ms_) {
180 // Kick start_output() in case data arrived but no transfer is in flight yet.
181 this->parent_->start_output(this);
182 yield();
183 }
184 if (!this->output_queue_.empty() || this->output_started_.load())
187}
188
189bool USBUartChannel::peek_byte(uint8_t *data) {
190 if (this->input_buffer_.is_empty()) {
191 return false;
192 }
193 *data = this->input_buffer_.peek();
194 return true;
195}
196bool USBUartChannel::read_array(uint8_t *data, size_t len) {
197 if (!this->initialised_.load()) {
198 ESP_LOGV(TAG, "Channel not initialised - read ignored");
199 return false;
200 }
201 auto available = this->available();
202 bool status = true;
203 if (len > available) {
204 ESP_LOGV(TAG, "underflow: requested %zu but returned %d, bytes", len, available);
205 len = available;
206 status = false;
207 }
208 for (size_t i = 0; i != len; i++) {
209 *data++ = this->input_buffer_.pop();
210 }
211 this->parent_->start_input(this);
212 return status;
213}
214void USBUartComponent::setup() { USBClient::setup(); }
216 bool had_work = this->process_usb_events_();
217 had_work |= this->run_config_machine_();
218
219 // Process USB data from the lock-free queue
220 UsbDataChunk *chunk;
221 while ((chunk = this->usb_data_queue_.pop()) != nullptr) {
222 had_work = true;
223 auto *channel = chunk->channel;
224
225#ifdef USE_UART_DEBUGGER
226 if (channel->debug_) {
227 char buf[format_hex_pretty_size(usb_host::USB_MAX_PACKET_SIZE)]; // "XX,XX,...,XX\0"
228 format_hex_pretty_to(buf, chunk->data, chunk->length, ',');
229 ESP_LOGD(TAG, "%s<<< %s", channel->debug_prefix_.c_str(), buf);
230 }
231#endif
232
233 // If there is not enough space for the full chunk, let the device subclass
234 // handle it (e.g. FTDI clears the buffer to prevent mid-telegram corruption).
235 if (channel->input_buffer_.get_free_space() < chunk->length) {
236 this->on_rx_overflow(channel);
237 }
238 // Push data to ring buffer (now safe in main loop)
239 channel->input_buffer_.push(chunk->data, chunk->length);
240
241 // Return chunk to pool for reuse
242 this->chunk_pool_.release(chunk);
243
244 // Invoke the RX callback (if registered) immediately after data lands in the
245 // ring buffer. This lets consumers such as ZigbeeProxy process incoming bytes
246 // in the same loop iteration they are delivered, avoiding an extra wakeup cycle.
247 if (channel->rx_callback_) {
248 channel->rx_callback_();
249 }
250 }
251
252 // Log dropped USB data periodically
253 uint16_t dropped = this->usb_data_queue_.get_and_reset_dropped_count();
254 if (dropped > 0) {
255 ESP_LOGW(TAG, "Dropped %u USB data chunks due to buffer overflow", dropped);
256 }
257
258 // Disable loop when idle. Callbacks re-enable via enable_loop_soon_any_context().
259 if (!had_work) {
260 this->disable_loop();
261 }
262}
264 USBClient::dump_config();
265 for (auto &channel : this->channels_) {
266 ESP_LOGCONFIG(TAG,
267 " UART Channel %d\n"
268 " Baud Rate: %" PRIu32 " baud\n"
269 " Data Bits: %u\n"
270 " Parity: %s\n"
271 " Stop bits: %s\n"
272 " Flush Timeout: %" PRIu32 " ms\n"
273 " Debug: %s\n"
274 " Dummy receiver: %s",
275 channel->index_, channel->baud_rate_, channel->data_bits_, PARITY_NAMES[channel->parity_],
276 STOP_BITS_NAMES[channel->stop_bits_], channel->flush_timeout_ms_, YESNO(channel->debug_),
277 YESNO(channel->dummy_receiver_));
278 }
279}
281 if (!channel->initialised_.load())
282 return;
283 // THREAD CONTEXT: Called from both USB task and main loop threads
284 // - USB task: Immediate restart after successful transfer for continuous data flow
285 // - Main loop: Controlled restart after consuming data (backpressure mechanism)
286 //
287 // This dual-thread access is intentional for performance:
288 // - USB task restarts avoid context switch delays for high-speed data
289 // - Main loop restarts provide flow control when buffers are full
290 //
291 // The underlying transfer_in() uses lock-free atomic allocation from the
292 // TransferRequest pool, making this multi-threaded access safe
293
294 // Use compare_exchange_strong to avoid spurious failures: a missed submit here is
295 // never retried by read_array() because no data will ever arrive to trigger it.
296 auto started = false;
297 if (!channel->input_started_.compare_exchange_strong(started, true))
298 return;
299 const auto *ep = channel->cdc_dev_.in_ep;
300 // CALLBACK CONTEXT: This lambda is executed in USB task via transfer_callback
301 auto callback = [this, channel](const usb_host::TransferStatus &status) {
302 ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code);
303 if (!status.success) {
304 ESP_LOGE(TAG, "Input transfer failed, status=%s", esp_err_to_name(status.error_code));
305 // On failure, don't restart - let next read_array() trigger it
306 channel->input_started_.store(false);
307 return;
308 }
309
310 if (!channel->dummy_receiver_ && status.data_len > 0) {
311 // Allocate a chunk from the pool
312 UsbDataChunk *chunk = this->chunk_pool_.allocate();
313 if (chunk == nullptr) {
314 // No chunks available - queue is full or we're out of memory
315 this->usb_data_queue_.increment_dropped_count();
316 // Mark input as not started so we can retry
317 channel->input_started_.store(false);
318 return;
319 }
320
321 // Copy data to chunk (this is fast, happens in USB task)
322 memcpy(chunk->data, status.data, status.data_len);
323 chunk->length = status.data_len;
324 chunk->channel = channel;
325
326 // Push to lock-free queue for main loop processing
327 // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
328 // allocate() returned non-null, the queue cannot be full.
329 this->usb_data_queue_.push(chunk);
330
331 // Re-enable component loop to process the queued data
333
334 // Wake main loop immediately to process USB data
336 }
337
338 // On success, restart input immediately from USB task for performance
339 // The lock-free queue will handle backpressure
340 channel->input_started_.store(false);
341 this->start_input(channel);
342 };
343 if (!this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize)) {
344 ESP_LOGE(TAG, "IN transfer submission failed for ep=0x%02X", ep->bEndpointAddress);
345 channel->input_started_.store(false);
346 }
347}
348
350 // THREAD CONTEXT: Called from both main loop and USB task threads.
351 // The output_queue_ is a lock-free SPSC queue, so pop() is safe from either thread.
352 // The output_started_ atomic flag is claimed via compare_exchange to guarantee that
353 // only one thread starts a transfer at a time.
354
355 // Atomically claim the "output in progress" flag. If already set, another thread
356 // is handling the transfer; return immediately.
357 bool expected = false;
358 if (!channel->output_started_.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) {
359 return;
360 }
361
362 UsbOutputChunk *chunk = channel->output_queue_.pop();
363 if (chunk == nullptr) {
364 // Nothing to send — release the flag and return.
365 channel->output_started_.store(false, std::memory_order_release);
366 return;
367 }
368
369 const auto *ep = channel->cdc_dev_.out_ep;
370 // CALLBACK CONTEXT: This lambda is executed in the USB task via transfer_callback.
371 // It releases the chunk, clears the flag, and directly restarts output without
372 // going through defer() — eliminating one full main-loop-wakeup cycle of latency.
373 auto callback = [this, channel, chunk](const usb_host::TransferStatus &status) {
374 if (!status.success) {
375 ESP_LOGW(TAG, "Output transfer failed: status %X", status.error_code);
376 } else {
377 ESP_LOGV(TAG, "Output Transfer result: length: %u; status %X", status.data_len, status.error_code);
378 }
379 channel->output_pool_.release(chunk);
380 channel->output_started_.store(false, std::memory_order_release);
381 // Restart directly from USB task — safe because output_queue_ is lock-free
382 // and transfer_out() uses thread-safe atomic slot allocation.
383 this->start_output(channel);
384 };
385
386 const auto len = chunk->length;
387 if (!this->transfer_out(ep->bEndpointAddress, callback, chunk->data, len)) {
388 // Transfer submission failed — return chunk and release flag so callers can retry.
389 channel->output_pool_.release(chunk);
390 channel->output_started_.store(false, std::memory_order_release);
391 return;
392 }
393 ESP_LOGV(TAG, "Output %u bytes started", len);
394}
395
400static void fix_mps(const usb_ep_desc_t *ep) {
401 if (ep != nullptr) {
402 auto *ep_mutable = const_cast<usb_ep_desc_t *>(ep);
403 if (ep->wMaxPacketSize > usb_host::USB_MAX_PACKET_SIZE) {
404 ESP_LOGW(TAG, "Corrected MPS of EP 0x%02X from %u to %u", static_cast<uint8_t>(ep->bEndpointAddress & 0xFF),
405 ep->wMaxPacketSize, usb_host::USB_MAX_PACKET_SIZE);
406 ep_mutable->wMaxPacketSize = usb_host::USB_MAX_PACKET_SIZE;
407 }
408 }
409}
411 auto cdc_devs = this->parse_descriptors(this->device_handle_);
412 if (cdc_devs.empty()) {
413 this->status_set_error(LOG_STR("No CDC-ACM device found"));
414 this->disconnect();
415 return;
416 }
417 ESP_LOGD(TAG, "Found %zu CDC-ACM devices", cdc_devs.size());
418 size_t i = 0;
419 for (auto *channel : this->channels_) {
420 if (i == cdc_devs.size()) {
421 ESP_LOGE(TAG, "No configuration found for channel %d", channel->index_);
422 this->status_set_warning(LOG_STR("No configuration found for channel"));
423 break;
424 }
425 channel->cdc_dev_ = cdc_devs[i++];
426 fix_mps(channel->cdc_dev_.in_ep);
427 fix_mps(channel->cdc_dev_.out_ep);
428 channel->initialised_.store(true);
429 // Claim the communication (interrupt) interface so CDC class requests are accepted
430 // by the device. Some CDC ACM implementations (e.g. EFR32 NCP) require this before
431 // they enable data flow on the bulk endpoints.
432 if (channel->cdc_dev_.interrupt_interface_number != 0xFF &&
433 channel->cdc_dev_.interrupt_interface_number != channel->cdc_dev_.bulk_interface_number) {
434 auto err_comm = usb_host_interface_claim(this->handle_, this->device_handle_,
435 channel->cdc_dev_.interrupt_interface_number, 0);
436 if (err_comm != ESP_OK) {
437 ESP_LOGW(TAG, "Could not claim comm interface %d: %s", channel->cdc_dev_.interrupt_interface_number,
438 esp_err_to_name(err_comm));
439 channel->cdc_dev_.interrupt_interface_number = 0xFF; // Mark as unavailable, but continue anyway
440 } else {
441 ESP_LOGD(TAG, "Claimed comm interface %d", channel->cdc_dev_.interrupt_interface_number);
442 }
443 }
444 auto err =
445 usb_host_interface_claim(this->handle_, this->device_handle_, channel->cdc_dev_.bulk_interface_number, 0);
446 if (err != ESP_OK) {
447 ESP_LOGE(TAG, "usb_host_interface_claim failed: %s, channel=%d, intf=%d", esp_err_to_name(err), channel->index_,
448 channel->cdc_dev_.bulk_interface_number);
449 this->status_set_error(LOG_STR("usb_host_interface_claim failed"));
450 this->disconnect();
451 return;
452 }
453 }
454 this->status_clear_error();
455 this->enable_channels();
456}
457
459 for (auto *channel : this->channels_) {
460 if (channel->cdc_dev_.in_ep != nullptr) {
461 usb_host_endpoint_halt(this->device_handle_, channel->cdc_dev_.in_ep->bEndpointAddress);
462 usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.in_ep->bEndpointAddress);
463 }
464 if (channel->cdc_dev_.out_ep != nullptr) {
465 usb_host_endpoint_halt(this->device_handle_, channel->cdc_dev_.out_ep->bEndpointAddress);
466 usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.out_ep->bEndpointAddress);
467 }
468 if (channel->cdc_dev_.notify_ep != nullptr) {
469 usb_host_endpoint_halt(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress);
470 usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress);
471 }
472 if (channel->cdc_dev_.interrupt_interface_number != 0xFF &&
473 channel->cdc_dev_.interrupt_interface_number != channel->cdc_dev_.bulk_interface_number) {
474 usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.interrupt_interface_number);
475 channel->cdc_dev_.interrupt_interface_number = 0xFF;
476 }
477 usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.bulk_interface_number);
478 // Reset the input and output started flags to their initial state to avoid the possibility of spurious restarts
479 channel->input_started_.store(true);
480 channel->output_started_.store(true);
481 channel->input_buffer_.clear();
482 // Drain any pending output chunks and return them to the pool
483 {
484 UsbOutputChunk *chunk;
485 while ((chunk = channel->output_queue_.pop()) != nullptr) {
486 channel->output_pool_.release(chunk);
487 }
488 }
489 channel->initialised_.store(false);
490 }
491 USBClient::on_disconnected();
492}
493
494bool USBUartTypeCdcAcm::config_step(USBUartChannel *channel, uint8_t step, bool reload, bool ok,
495 const uint8_t *response) {
496 static constexpr uint8_t CDC_REQUEST_TYPE = usb_host::USB_TYPE_CLASS | usb_host::USB_RECIP_INTERFACE;
497 static constexpr uint8_t CDC_SET_LINE_CODING = 0x20;
498 static constexpr uint8_t CDC_SET_CONTROL_LINE_STATE = 0x22;
499 static constexpr uint16_t CDC_DTR_RTS = 0x0003; // D0=DTR, D1=RTS
500
501 switch (step) {
502 case 0: {
503 // Configure the bridge's UART parameters. A USB-UART bridge will not forward data
504 // at the correct speed until SET_LINE_CODING is sent; without it the UART may run
505 // at an indeterminate default rate so the NCP receives garbled bytes and never
506 // sends RSTACK.
507 uint32_t baud = channel->baud_rate_;
508 std::vector<uint8_t> line_coding = {
509 static_cast<uint8_t>(baud & 0xFF), static_cast<uint8_t>((baud >> 8) & 0xFF),
510 static_cast<uint8_t>((baud >> 16) & 0xFF), static_cast<uint8_t>((baud >> 24) & 0xFF),
511 static_cast<uint8_t>(channel->stop_bits_), // bCharFormat: 0=1stop, 1=1.5stop, 2=2stop
512 static_cast<uint8_t>(channel->parity_), // bParityType: 0=None, 1=Odd, 2=Even, 3=Mark, 4=Space
513 static_cast<uint8_t>(channel->data_bits_), // bDataBits
514 };
515 ESP_LOGD(TAG, "SET_LINE_CODING: baud=%u stop=%u parity=%u data=%u", (unsigned) baud, channel->stop_bits_,
516 (unsigned) channel->parity_, channel->data_bits_);
517 this->config_transfer_(CDC_REQUEST_TYPE, CDC_SET_LINE_CODING, 0, channel->cdc_dev_.interrupt_interface_number,
518 line_coding);
519 return true;
520 }
521 case 1:
522 // Assert DTR+RTS to signal DTE is present (init only).
523 if (reload)
524 return false;
525 this->config_transfer_(CDC_REQUEST_TYPE, CDC_SET_CONTROL_LINE_STATE, CDC_DTR_RTS,
527 return true;
528 default:
529 return false;
530 }
531}
532
534 this->cfg_single_ = nullptr;
535 this->cfg_pending_reload_ = nullptr;
536 this->cfg_channel_idx_ = 0;
537 this->start_config_(false);
538}
539
541 if (this->cfg_active_) {
542 // A config sequence is already running. Defer this reload until it finishes to preserve
543 // the one-control-transfer-at-a-time guarantee (restarting mid-flight would let an
544 // in-flight callback complete against fresh state). The pending slot coalesces multiple
545 // requests; the channel's live settings are read when the reload eventually runs.
546 // Note: multiple channel reloads are not queued; only one pending reload is supported at a time.
547 this->cfg_pending_reload_ = channel;
548 return;
549 }
550 this->cfg_single_ = channel;
551 this->start_config_(true);
552}
553
555 this->cfg_reload_ = reload;
556 this->cfg_device_phase_ = !reload;
557 this->cfg_step_ = 0;
558 this->cfg_ok_ = true;
559 this->cfg_in_flight_ = false;
560 this->cfg_done_.store(false);
561 this->cfg_active_ = true;
562 this->enable_loop();
563}
564
565void USBUartComponent::config_transfer_(uint8_t type, uint8_t request, uint16_t value, uint16_t index,
566 const std::vector<uint8_t> &data) {
567 this->cfg_done_.store(false);
568 // The completion callback runs in the USB-task context: it only records the result and
569 // wakes the loop. The next transfer is issued from run_config_machine_() on the loop thread.
570 bool submitted = this->control_transfer(
571 type, request, value, index,
572 [this](const usb_host::TransferStatus &status) {
573 this->cfg_ok_ = status.success;
574 if (!status.success) {
575 ESP_LOGW(TAG, "Config control transfer failed: %s", esp_err_to_name(status.error_code));
576 } else if (status.data_len > 0) {
577 memcpy(this->cfg_response_, status.data, std::min<size_t>(status.data_len, sizeof(this->cfg_response_)));
578 }
579 // Release: publishes cfg_ok_/cfg_response_ before the loop observes cfg_done_.
580 this->cfg_done_.store(true, std::memory_order_release);
583 },
584 data);
585 if (!submitted) {
586 // Submission failed (e.g. no free transfer request). No callback will fire, so synthesize
587 // a failed completion here so the state machine advances/aborts instead of hanging.
588 ESP_LOGW(TAG, "Config control transfer submit failed");
589 this->cfg_ok_ = false;
590 this->cfg_done_.store(true, std::memory_order_release);
591 }
592}
593
595 if (!this->cfg_active_)
596 return false;
597
598 if (this->cfg_in_flight_) {
599 // Acquire: pairs with the release in config_transfer_'s callback.
600 if (!this->cfg_done_.load(std::memory_order_acquire))
601 return false; // still waiting; the callback will re-wake the loop (no busy spin)
602 this->cfg_in_flight_ = false;
603 this->cfg_done_.store(false);
604 this->cfg_step_++;
605 }
606
607 // cfg_ok_ is now synchronized (we only get here on the initial entry or after observing
608 // cfg_done_ with acquire ordering), so it is safe to read.
609 ESP_LOGV(TAG, "Config machine: device_phase=%d channel_idx=%d step=%d reload=%d ok=%d", this->cfg_device_phase_,
610 this->cfg_channel_idx_, this->cfg_step_, this->cfg_reload_, this->cfg_ok_);
611
612 // One-time device-level phase (init only). config_device_step() inspects cfg_ok_ itself.
613 if (this->cfg_device_phase_) {
614 if (this->config_device_step(this->cfg_step_, this->cfg_ok_, this->cfg_response_)) {
615 this->cfg_in_flight_ = true;
616 return true;
617 }
618 this->cfg_device_phase_ = false;
619 this->cfg_step_ = 0;
620 this->cfg_ok_ = true;
621 }
622
623 USBUartChannel *channel =
624 this->cfg_single_ != nullptr
625 ? this->cfg_single_
626 : (this->cfg_channel_idx_ < this->channels_.size() ? this->channels_[this->cfg_channel_idx_] : nullptr);
627
628 if (channel != nullptr && channel->initialised_.load()) {
629 if (!this->cfg_ok_) {
630 // A previous step in this channel's sequence failed. Abort the rest. On a full init,
631 // mark the channel uninitialised so data flow isn't started on a misconfigured channel;
632 // on a reload, leave the already-working channel as it was.
633 if (!this->cfg_reload_)
634 channel->initialised_.store(false);
635 } else if (this->config_step(channel, this->cfg_step_, this->cfg_reload_, this->cfg_ok_, this->cfg_response_)) {
636 this->cfg_in_flight_ = true;
637 return true;
638 }
639 }
640
641 // Channel finished (or aborted). On full init, kick off data flow if still initialised.
642 if (channel != nullptr && !this->cfg_reload_ && channel->initialised_.load()) {
643 channel->input_started_.store(false);
644 channel->output_started_.store(false);
645 this->start_input(channel);
646 }
647
648 // Advance to the next channel (or finish).
649 this->cfg_step_ = 0;
650 this->cfg_ok_ = true;
651 if (this->cfg_single_ != nullptr) {
652 this->cfg_active_ = false;
653 this->cfg_single_ = nullptr;
654 } else if (++this->cfg_channel_idx_ >= this->channels_.size()) {
655 this->cfg_active_ = false;
656 }
657
658 // If the machine just went idle and a reload was requested while it was busy, start it now.
659 if (!this->cfg_active_ && this->cfg_pending_reload_ != nullptr) {
660 this->cfg_single_ = this->cfg_pending_reload_;
661 this->cfg_pending_reload_ = nullptr;
662 this->start_config_(true);
663 }
664 return true;
665}
666
667void USBUartChannel::load_settings(bool /*dump_config*/) {
668 // The per-channel control transfers already log their values at debug level.
669 this->parent_->apply_channel_settings(this);
670}
671
672} // namespace esphome::usb_uart
673
674#endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 ||
675 // USE_ESP32_VARIANT_ESP32S31 || USE_ESP32_VARIANT_ESP32H4
uint8_t status
Definition bl0942.h:8
void wake_loop_threadsafe()
Wake the main event loop from another thread or callback.
void status_clear_error()
Definition component.h:295
void enable_loop_soon_any_context()
Thread and ISR-safe version of enable_loop() that can be called from any context.
void enable_loop()
Enable this component's loop.
Definition component.h:246
void disable_loop()
Disable this component's loop.
constexpr const char * c_str() const
Definition string_ref.h:73
void load_settings()
Load the UART settings.
usb_host_client_handle_t handle_
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.
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.
usb_device_handle_t device_handle_
Definition usb_host.h:177
void push(uint8_t item)
Definition usb_uart.cpp:110
size_t get_free_space() const
Definition usb_uart.h:94
size_t get_available() const
Definition usb_uart.h:91
EventPool< UsbOutputChunk, USB_OUTPUT_CHUNK_COUNT - 1 > output_pool_
Definition usb_uart.h:172
std::atomic< bool > input_started_
Definition usb_uart.h:180
std::atomic< bool > initialised_
Definition usb_uart.h:182
LockFreeQueue< UsbOutputChunk, USB_OUTPUT_CHUNK_COUNT > output_queue_
Definition usb_uart.h:168
bool peek_byte(uint8_t *data) override
Definition usb_uart.cpp:189
void write_array(const uint8_t *data, size_t len) override
Definition usb_uart.cpp:139
uart::UARTFlushResult flush() override
Definition usb_uart.cpp:173
bool read_array(uint8_t *data, size_t len) override
Definition usb_uart.cpp:196
std::atomic< bool > output_started_
Definition usb_uart.h:181
void config_transfer_(uint8_t type, uint8_t request, uint16_t value, uint16_t index, const std::vector< uint8_t > &data={})
Definition usb_uart.cpp:565
USBUartChannel * cfg_pending_reload_
Definition usb_uart.h:243
virtual bool config_step(USBUartChannel *channel, uint8_t step, bool reload, bool ok, const uint8_t *response)=0
std::atomic< bool > cfg_done_
Definition usb_uart.h:244
std::vector< USBUartChannel * > channels_
Definition usb_uart.h:239
LockFreeQueue< UsbDataChunk, USB_DATA_QUEUE_SIZE > usb_data_queue_
Definition usb_uart.h:213
virtual void on_rx_overflow(USBUartChannel *channel)
Definition usb_uart.h:209
void start_output(USBUartChannel *channel)
Definition usb_uart.cpp:349
virtual bool config_device_step(uint8_t step, bool ok, const uint8_t *response)
Definition usb_uart.h:237
virtual void start_input(USBUartChannel *channel)
Definition usb_uart.cpp:280
void apply_channel_settings(USBUartChannel *channel)
Definition usb_uart.cpp:540
EventPool< UsbDataChunk, USB_DATA_QUEUE_SIZE - 1 > chunk_pool_
Definition usb_uart.h:215
virtual std::vector< CdcEps > parse_descriptors(usb_device_handle_t dev_hdl)
Definition usb_uart.cpp:64
bool config_step(USBUartChannel *channel, uint8_t step, bool reload, bool ok, const uint8_t *response) override
Definition usb_uart.cpp:494
void yield(void)
uint16_t type
UARTFlushResult
Result of a flush() call.
@ UART_FLUSH_RESULT_SUCCESS
Confirmed: all bytes left the TX FIFO.
@ UART_FLUSH_RESULT_TIMEOUT
Confirmed: timed out before TX completed.
const void size_t len
Definition hal.h:64
char * format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator)
Format byte array as uppercase hex to buffer (base implementation).
Definition helpers.cpp:340
constexpr size_t format_hex_pretty_size(size_t byte_count)
Calculate buffer size needed for format_hex_pretty_to with separator: "XX:XX:...:XX\0".
Definition helpers.h:1400
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:28
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t
const usb_ep_desc_t * out_ep
Definition usb_uart.h:35
const usb_ep_desc_t * in_ep
Definition usb_uart.h:34
uint8_t interrupt_interface_number
Definition usb_uart.h:37
uint8_t data[usb_host::USB_MAX_PACKET_SIZE]
Definition usb_uart.h:111
uint8_t data[MAX_CHUNK_SIZE]
Definition usb_uart.h:122
static constexpr size_t MAX_CHUNK_SIZE
Definition usb_uart.h:121