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