[uart][usb_uart] Implement runtime settings update (#16990)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
This commit is contained in:
Clyde Stubbs
2026-07-08 13:30:49 -04:00
committed by GitHub
co-authored by Claude Opus 4.8 Keith Burzinski
parent e7933a5387
commit ce46895270
13 changed files with 534 additions and 419 deletions
+166 -42
View File
@@ -6,6 +6,7 @@
#include "esphome/core/application.h"
#include <cinttypes>
#include <cstring>
namespace esphome::usb_uart {
@@ -213,6 +214,7 @@ bool USBUartChannel::read_array(uint8_t *data, size_t len) {
void USBUartComponent::setup() { USBClient::setup(); }
void USBUartComponent::loop() {
bool had_work = this->process_usb_events_();
had_work |= this->run_config_machine_();
// Process USB data from the lock-free queue
UsbDataChunk *chunk;
@@ -489,60 +491,182 @@ void USBUartTypeCdcAcm::on_disconnected() {
USBClient::on_disconnected();
}
void USBUartTypeCdcAcm::enable_channels() {
bool USBUartTypeCdcAcm::config_step(USBUartChannel *channel, uint8_t step, bool reload, bool ok,
const uint8_t *response) {
static constexpr uint8_t CDC_REQUEST_TYPE = usb_host::USB_TYPE_CLASS | usb_host::USB_RECIP_INTERFACE;
static constexpr uint8_t CDC_SET_LINE_CODING = 0x20;
static constexpr uint8_t CDC_SET_CONTROL_LINE_STATE = 0x22;
static constexpr uint16_t CDC_DTR_RTS = 0x0003; // D0=DTR, D1=RTS
for (auto *channel : this->channels_) {
if (!channel->initialised_.load())
continue;
// Configure the bridge's UART parameters. A USB-UART bridge will not forward data
// at the correct speed until SET_LINE_CODING is sent; without it the UART may run
// at an indeterminate default rate so the NCP receives garbled bytes and never
// sends RSTACK.
uint32_t baud = channel->baud_rate_;
std::vector<uint8_t> line_coding = {
static_cast<uint8_t>(baud & 0xFF), static_cast<uint8_t>((baud >> 8) & 0xFF),
static_cast<uint8_t>((baud >> 16) & 0xFF), static_cast<uint8_t>((baud >> 24) & 0xFF),
static_cast<uint8_t>(channel->stop_bits_), // bCharFormat: 0=1stop, 1=1.5stop, 2=2stop
static_cast<uint8_t>(channel->parity_), // bParityType: 0=None, 1=Odd, 2=Even, 3=Mark, 4=Space
static_cast<uint8_t>(channel->data_bits_), // bDataBits
};
ESP_LOGD(TAG, "SET_LINE_CODING: baud=%u stop=%u parity=%u data=%u", (unsigned) baud, channel->stop_bits_,
(unsigned) channel->parity_, channel->data_bits_);
this->control_transfer(
CDC_REQUEST_TYPE, CDC_SET_LINE_CODING, 0, channel->cdc_dev_.interrupt_interface_number,
[](const usb_host::TransferStatus &status) {
if (!status.success) {
ESP_LOGW(TAG, "SET_LINE_CODING failed: %X", status.error_code);
} else {
ESP_LOGD(TAG, "SET_LINE_CODING OK");
}
},
line_coding);
// Assert DTR+RTS to signal DTE is present.
this->control_transfer(CDC_REQUEST_TYPE, CDC_SET_CONTROL_LINE_STATE, CDC_DTR_RTS,
channel->cdc_dev_.interrupt_interface_number, [](const usb_host::TransferStatus &status) {
if (!status.success) {
ESP_LOGW(TAG, "SET_CONTROL_LINE_STATE failed: %X", status.error_code);
} else {
ESP_LOGD(TAG, "SET_CONTROL_LINE_STATE (DTR+RTS) OK");
}
});
switch (step) {
case 0: {
// Configure the bridge's UART parameters. A USB-UART bridge will not forward data
// at the correct speed until SET_LINE_CODING is sent; without it the UART may run
// at an indeterminate default rate so the NCP receives garbled bytes and never
// sends RSTACK.
uint32_t baud = channel->baud_rate_;
std::vector<uint8_t> line_coding = {
static_cast<uint8_t>(baud & 0xFF), static_cast<uint8_t>((baud >> 8) & 0xFF),
static_cast<uint8_t>((baud >> 16) & 0xFF), static_cast<uint8_t>((baud >> 24) & 0xFF),
static_cast<uint8_t>(channel->stop_bits_), // bCharFormat: 0=1stop, 1=1.5stop, 2=2stop
static_cast<uint8_t>(channel->parity_), // bParityType: 0=None, 1=Odd, 2=Even, 3=Mark, 4=Space
static_cast<uint8_t>(channel->data_bits_), // bDataBits
};
ESP_LOGD(TAG, "SET_LINE_CODING: baud=%u stop=%u parity=%u data=%u", (unsigned) baud, channel->stop_bits_,
(unsigned) channel->parity_, channel->data_bits_);
this->config_transfer_(CDC_REQUEST_TYPE, CDC_SET_LINE_CODING, 0, channel->cdc_dev_.interrupt_interface_number,
line_coding);
return true;
}
case 1:
// Assert DTR+RTS to signal DTE is present (init only).
if (reload)
return false;
this->config_transfer_(CDC_REQUEST_TYPE, CDC_SET_CONTROL_LINE_STATE, CDC_DTR_RTS,
channel->cdc_dev_.interrupt_interface_number);
return true;
default:
return false;
}
this->start_channels_();
}
void USBUartTypeCdcAcm::start_channels_() {
for (auto *channel : this->channels_) {
if (!channel->initialised_.load())
continue;
void USBUartComponent::enable_channels() {
this->cfg_single_ = nullptr;
this->cfg_pending_reload_ = nullptr;
this->cfg_channel_idx_ = 0;
this->start_config_(false);
}
void USBUartComponent::apply_channel_settings(USBUartChannel *channel) {
if (this->cfg_active_) {
// A config sequence is already running. Defer this reload until it finishes to preserve
// the one-control-transfer-at-a-time guarantee (restarting mid-flight would let an
// in-flight callback complete against fresh state). The pending slot coalesces multiple
// requests; the channel's live settings are read when the reload eventually runs.
// Note: multiple channel reloads are not queued; only one pending reload is supported at a time.
this->cfg_pending_reload_ = channel;
return;
}
this->cfg_single_ = channel;
this->start_config_(true);
}
void USBUartComponent::start_config_(bool reload) {
this->cfg_reload_ = reload;
this->cfg_device_phase_ = !reload;
this->cfg_step_ = 0;
this->cfg_ok_ = true;
this->cfg_in_flight_ = false;
this->cfg_done_.store(false);
this->cfg_active_ = true;
this->enable_loop();
}
void USBUartComponent::config_transfer_(uint8_t type, uint8_t request, uint16_t value, uint16_t index,
const std::vector<uint8_t> &data) {
this->cfg_done_.store(false);
// The completion callback runs in the USB-task context: it only records the result and
// wakes the loop. The next transfer is issued from run_config_machine_() on the loop thread.
bool submitted = this->control_transfer(
type, request, value, index,
[this](const usb_host::TransferStatus &status) {
this->cfg_ok_ = status.success;
if (!status.success) {
ESP_LOGW(TAG, "Config control transfer failed: %s", esp_err_to_name(status.error_code));
} else if (status.data_len > 0) {
memcpy(this->cfg_response_, status.data, std::min<size_t>(status.data_len, sizeof(this->cfg_response_)));
}
// Release: publishes cfg_ok_/cfg_response_ before the loop observes cfg_done_.
this->cfg_done_.store(true, std::memory_order_release);
this->enable_loop_soon_any_context();
App.wake_loop_threadsafe();
},
data);
if (!submitted) {
// Submission failed (e.g. no free transfer request). No callback will fire, so synthesize
// a failed completion here so the state machine advances/aborts instead of hanging.
ESP_LOGW(TAG, "Config control transfer submit failed");
this->cfg_ok_ = false;
this->cfg_done_.store(true, std::memory_order_release);
}
}
bool USBUartComponent::run_config_machine_() {
if (!this->cfg_active_)
return false;
if (this->cfg_in_flight_) {
// Acquire: pairs with the release in config_transfer_'s callback.
if (!this->cfg_done_.load(std::memory_order_acquire))
return false; // still waiting; the callback will re-wake the loop (no busy spin)
this->cfg_in_flight_ = false;
this->cfg_done_.store(false);
this->cfg_step_++;
}
// cfg_ok_ is now synchronized (we only get here on the initial entry or after observing
// cfg_done_ with acquire ordering), so it is safe to read.
ESP_LOGV(TAG, "Config machine: device_phase=%d channel_idx=%d step=%d reload=%d ok=%d", this->cfg_device_phase_,
this->cfg_channel_idx_, this->cfg_step_, this->cfg_reload_, this->cfg_ok_);
// One-time device-level phase (init only). config_device_step() inspects cfg_ok_ itself.
if (this->cfg_device_phase_) {
if (this->config_device_step(this->cfg_step_, this->cfg_ok_, this->cfg_response_)) {
this->cfg_in_flight_ = true;
return true;
}
this->cfg_device_phase_ = false;
this->cfg_step_ = 0;
this->cfg_ok_ = true;
}
USBUartChannel *channel =
this->cfg_single_ != nullptr
? this->cfg_single_
: (this->cfg_channel_idx_ < this->channels_.size() ? this->channels_[this->cfg_channel_idx_] : nullptr);
if (channel != nullptr && channel->initialised_.load()) {
if (!this->cfg_ok_) {
// A previous step in this channel's sequence failed. Abort the rest. On a full init,
// mark the channel uninitialised so data flow isn't started on a misconfigured channel;
// on a reload, leave the already-working channel as it was.
if (!this->cfg_reload_)
channel->initialised_.store(false);
} else if (this->config_step(channel, this->cfg_step_, this->cfg_reload_, this->cfg_ok_, this->cfg_response_)) {
this->cfg_in_flight_ = true;
return true;
}
}
// Channel finished (or aborted). On full init, kick off data flow if still initialised.
if (channel != nullptr && !this->cfg_reload_ && channel->initialised_.load()) {
channel->input_started_.store(false);
channel->output_started_.store(false);
this->start_input(channel);
}
// Advance to the next channel (or finish).
this->cfg_step_ = 0;
this->cfg_ok_ = true;
if (this->cfg_single_ != nullptr) {
this->cfg_active_ = false;
this->cfg_single_ = nullptr;
} else if (++this->cfg_channel_idx_ >= this->channels_.size()) {
this->cfg_active_ = false;
}
// If the machine just went idle and a reload was requested while it was busy, start it now.
if (!this->cfg_active_ && this->cfg_pending_reload_ != nullptr) {
this->cfg_single_ = this->cfg_pending_reload_;
this->cfg_pending_reload_ = nullptr;
this->start_config_(true);
}
return true;
}
void USBUartChannel::load_settings(bool /*dump_config*/) {
// The per-channel control transfers already log their values at debug level.
this->parent_->apply_channel_settings(this);
}
} // namespace esphome::usb_uart