[usb_host] Read the interface descriptor string, for full Linux parity

This commit is contained in:
puddly
2026-09-10 18:54:51 +00:00
parent 90f428b0fb
commit a0643429c1
11 changed files with 168 additions and 6 deletions
+1
View File
@@ -2908,6 +2908,7 @@ message SerialProxyUsbInfo {
string manufacturer = 8;
string product = 9;
string serial_number = 10;
string interface_description = 11; // iInterface string of that interface; empty when the device has none
}
// ==================== BLUETOOTH CONNECTION PARAMS ====================
+3 -1
View File
@@ -4288,9 +4288,10 @@ uint8_t *SerialProxyUsbInfo::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 8, this->manufacturer);
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 9, this->product);
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 10, this->serial_number);
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 11, this->interface_description);
return pos;
}
uint32_t SerialProxyGetUsbInfoResponse::calculate_size() const {
uint32_t SerialProxyUsbInfo::calculate_size() const {
uint32_t size = 0;
size += ProtoSize::calc_uint32(1, this->instance);
size += this->status ? 2 : 0;
@@ -4302,6 +4303,7 @@ uint32_t SerialProxyGetUsbInfoResponse::calculate_size() const {
size += ProtoSize::calc_length(1, this->manufacturer.size());
size += ProtoSize::calc_length(1, this->product.size());
size += ProtoSize::calc_length(1, this->serial_number.size());
size += ProtoSize::calc_length(1, this->interface_description.size());
return size;
}
#endif
+2 -1
View File
@@ -3443,7 +3443,7 @@ class SerialProxyGetUsbInfoRequest final : public ProtoDecodableMessage {
class SerialProxyUsbInfo final : public ProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 154;
static constexpr uint8_t ESTIMATED_SIZE = 51;
static constexpr uint8_t ESTIMATED_SIZE = 60;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("serial_proxy_usb_info"); }
#endif
@@ -3457,6 +3457,7 @@ class SerialProxyUsbInfo final : public ProtoMessage {
StringRef manufacturer{};
StringRef product{};
StringRef serial_number{};
StringRef interface_description{};
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
+1
View File
@@ -2842,6 +2842,7 @@ const char *SerialProxyUsbInfo::dump_to(DumpBuffer &out) const {
dump_field(out, ESPHOME_PSTR("manufacturer"), this->manufacturer);
dump_field(out, ESPHOME_PSTR("product"), this->product);
dump_field(out, ESPHOME_PSTR("serial_number"), this->serial_number);
dump_field(out, ESPHOME_PSTR("interface_description"), this->interface_description);
return out.c_str();
}
#endif
+21
View File
@@ -5,6 +5,7 @@
defined(USE_ESP32_VARIANT_ESP32S31) || defined(USE_ESP32_VARIANT_ESP32H4)
#include "esphome/core/defines.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include <vector>
#include "usb/usb_host.h"
#include <freertos/FreeRTOS.h>
@@ -12,6 +13,7 @@
#include "esphome/core/lock_free_queue.h"
#include "esphome/core/event_pool.h"
#include <atomic>
#include <span>
namespace esphome::usb_host {
@@ -131,6 +133,10 @@ struct UsbDeviceInfo {
char serial_number[DESC_STRING_BUF_SIZE];
};
/// Copy a USB string descriptor into a NUL-terminated buffer, dropping characters outside
/// Latin-1. A missing descriptor copies as an empty string.
void copy_descriptor_string(const usb_str_desc_t *desc, std::span<char, DESC_STRING_BUF_SIZE> buffer);
enum ClientState {
USB_CLIENT_INIT = 0,
USB_CLIENT_OPEN,
@@ -162,6 +168,14 @@ class USBClient : public Component {
/// Returns false when no device is connected.
bool get_device_info(UsbDeviceInfo &info) const;
/// Read a string descriptor the host stack does not cache, an interface string say, into
/// buffer. Uses a transfer of its own: the pooled ones hold one packet, and a string
/// descriptor can be four times that. The callback runs on the USB task once buffer holds
/// the string (empty on failure). Returns false when nothing was started. One read at a
/// time; main loop only.
bool read_string_descriptor(uint8_t index, std::span<char, DESC_STRING_BUF_SIZE> buffer,
const transfer_cb_t &callback);
/// Narrow which device this client claims, beyond the VID/PID it was constructed
/// with, by requiring a descriptor string to match exactly.
void set_manufacturer_filter(const char *manufacturer) { this->manufacturer_filter_ = manufacturer; }
@@ -208,6 +222,8 @@ class USBClient : public Component {
this->trq_in_use_.store(0);
}
static void string_descriptor_callback(usb_transfer_t *xfer);
// USB task management
static void usb_task_fn(void *arg);
[[noreturn]] void usb_task_loop_() const;
@@ -215,6 +231,11 @@ class USBClient : public Component {
// Members ordered to minimize struct padding on 32-bit platforms
TransferRequest requests_[MAX_REQUESTS]{};
TaskHandle_t usb_task_handle_{nullptr};
// Dedicated transfer for read_string_descriptor(), allocated on first use and kept
usb_transfer_t *string_transfer_{nullptr};
transfer_cb_t string_callback_;
char *string_buffer_{nullptr};
std::atomic<bool> string_read_busy_{false};
usb_host_client_handle_t handle_{};
usb_device_handle_t device_handle_{};
int device_addr_{-1};
@@ -162,7 +162,7 @@ static const char *get_descriptor_string(const usb_str_desc_t *desc, std::span<c
// A missing descriptor copies as an empty string, unlike the "(unspecified)"
// placeholder the logging helper above uses
static void copy_descriptor_string(const usb_str_desc_t *desc, std::span<char, DESC_STRING_BUF_SIZE> buffer) {
void copy_descriptor_string(const usb_str_desc_t *desc, std::span<char, DESC_STRING_BUF_SIZE> buffer) {
buffer[0] = '\0';
if (desc == nullptr || desc->bLength < 2)
return;
@@ -215,6 +215,72 @@ bool USBClient::get_device_info(UsbDeviceInfo &info) const {
return true;
}
static constexpr size_t MAX_STRING_DESC_SIZE = 255; // bLength is one byte
static constexpr uint16_t LANGID_EN_US = 0x0409;
// CALLBACK CONTEXT: USB task
void USBClient::string_descriptor_callback(usb_transfer_t *xfer) {
auto *client = static_cast<USBClient *>(xfer->context);
TransferStatus status{};
status.error_code = xfer->status;
status.success = xfer->status == USB_TRANSFER_STATUS_COMPLETED;
status.endpoint = xfer->bEndpointAddress;
// The buffer starts with the setup packet; the descriptor follows it
status.data = xfer->data_buffer + SETUP_PACKET_SIZE;
status.data_len =
xfer->actual_num_bytes > static_cast<int>(SETUP_PACKET_SIZE) ? xfer->actual_num_bytes - SETUP_PACKET_SIZE : 0;
const auto *desc = reinterpret_cast<const usb_str_desc_t *>(status.data);
// A short read leaves bLength claiming bytes that never arrived; treat it as missing
const bool complete = status.success && status.data_len >= 2 && desc->bLength <= status.data_len;
copy_descriptor_string(complete ? desc : nullptr,
std::span<char, DESC_STRING_BUF_SIZE>(client->string_buffer_, DESC_STRING_BUF_SIZE));
client->string_read_busy_.store(false);
if (client->string_callback_ != nullptr) {
client->string_callback_(status);
}
}
bool USBClient::read_string_descriptor(uint8_t index, std::span<char, DESC_STRING_BUF_SIZE> buffer,
const transfer_cb_t &callback) {
buffer[0] = '\0';
if (this->state_ != USB_CLIENT_CONNECTED || index == 0) {
return false;
}
if (this->string_transfer_ == nullptr) {
if (usb_host_transfer_alloc(SETUP_PACKET_SIZE + MAX_STRING_DESC_SIZE, 0, &this->string_transfer_) != ESP_OK) {
ESP_LOGE(TAG, "String descriptor transfer alloc failed");
return false;
}
this->string_transfer_->context = this;
this->string_transfer_->callback = string_descriptor_callback;
}
bool idle = false;
if (!this->string_read_busy_.compare_exchange_strong(idle, true)) {
ESP_LOGW(TAG, "String descriptor read already in progress");
return false;
}
auto *setup = reinterpret_cast<usb_setup_packet_t *>(this->string_transfer_->data_buffer);
setup->bmRequestType = USB_DIR_IN | USB_TYPE_STANDARD | USB_RECIP_DEVICE;
setup->bRequest = USB_B_REQUEST_GET_DESCRIPTOR;
setup->wValue = (USB_B_DESCRIPTOR_TYPE_STRING << 8) | index;
// The host stack read the device strings in US English; Linux asks for it first as well,
// so all three report the same text
setup->wIndex = LANGID_EN_US;
setup->wLength = MAX_STRING_DESC_SIZE;
this->string_transfer_->num_bytes = SETUP_PACKET_SIZE + MAX_STRING_DESC_SIZE;
this->string_transfer_->bEndpointAddress = USB_DIR_IN;
this->string_transfer_->device_handle = this->device_handle_;
this->string_buffer_ = buffer.data();
this->string_callback_ = callback;
auto err = usb_host_transfer_submit_control(this->handle_, this->string_transfer_);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to submit string descriptor read, err=%s", esp_err_to_name(err));
this->string_read_busy_.store(false);
return false;
}
return true;
}
// CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task)
static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void *ptr) {
auto *client = static_cast<USBClient *>(ptr);
+3 -2
View File
@@ -88,10 +88,11 @@ std::vector<CdcEps> USBUartTypeCP210X::parse_descriptors(usb_device_handle_t dev
ESP_LOGE(TAG, "in_ep: usb_parse_endpoint_descriptor_by_index failed");
continue;
}
// No communication interface: the data interface is the one a host binds to
if (in_ep->bEndpointAddress & usb_host::USB_DIR_IN) {
cdc_devs.push_back({CdcEps{nullptr, in_ep, out_ep, data_desc->bInterfaceNumber}});
cdc_devs.push_back({CdcEps{nullptr, in_ep, out_ep, data_desc->bInterfaceNumber, 0xFF, 0, data_desc->iInterface}});
} else {
cdc_devs.push_back({CdcEps{nullptr, out_ep, in_ep, data_desc->bInterfaceNumber}});
cdc_devs.push_back({CdcEps{nullptr, out_ep, in_ep, data_desc->bInterfaceNumber, 0xFF, 0, data_desc->iInterface}});
}
}
return cdc_devs;
+3
View File
@@ -217,6 +217,9 @@ static optional<CdcEps> get_uart(const usb_config_desc_t *config_desc, uint8_t i
}
eps.bulk_interface_number = intf_desc->bInterfaceNumber;
eps.bulk_interface_string_index = intf_desc->iInterface;
// No communication interface: the data interface is the one a host binds to
eps.interrupt_interface_number = 0xFF;
return eps;
}
+2 -1
View File
@@ -189,7 +189,8 @@ std::vector<CdcEps> USBUartTypePL2303::parse_descriptors(usb_device_handle_t dev
}
if (in_ep && out_ep) {
cdc_devs.push_back(CdcEps{notify_ep, in_ep, out_ep, intf->bInterfaceNumber, intf->bInterfaceNumber});
cdc_devs.push_back(CdcEps{notify_ep, in_ep, out_ep, intf->bInterfaceNumber, intf->bInterfaceNumber,
intf->iInterface, intf->iInterface});
break; // PL2303 is single-port
}
}
+52
View File
@@ -43,14 +43,17 @@ static optional<CdcEps> get_cdc(const usb_config_desc_t *config_desc, uint8_t in
if (ep->bmAttributes == USB_BM_ATTRIBUTES_XFER_INT) {
eps.notify_ep = ep;
eps.interrupt_interface_number = intf_desc->bInterfaceNumber;
eps.interrupt_interface_string_index = intf_desc->iInterface;
} else if (ep->bmAttributes == USB_BM_ATTRIBUTES_XFER_BULK && ep->bEndpointAddress & usb_host::USB_DIR_IN &&
(eps.bulk_interface_number == 0xFF || eps.bulk_interface_number == intf_desc->bInterfaceNumber)) {
eps.in_ep = ep;
eps.bulk_interface_number = intf_desc->bInterfaceNumber;
eps.bulk_interface_string_index = intf_desc->iInterface;
} else if (ep->bmAttributes == USB_BM_ATTRIBUTES_XFER_BULK && !(ep->bEndpointAddress & usb_host::USB_DIR_IN) &&
(eps.bulk_interface_number == 0xFF || eps.bulk_interface_number == intf_desc->bInterfaceNumber)) {
eps.out_ep = ep;
eps.bulk_interface_number = intf_desc->bInterfaceNumber;
eps.bulk_interface_string_index = intf_desc->iInterface;
} else {
ESP_LOGE(TAG, "Unexpected endpoint attributes: %02X", ep->bmAttributes);
continue;
@@ -479,6 +482,7 @@ void USBUartTypeCdcAcm::on_disconnected() {
channel->input_started_.store(true);
channel->output_started_.store(true);
channel->input_buffer_.clear();
channel->interface_string_[0] = '\0';
// Drain any pending output chunks and return them to the pool
{
UsbOutputChunk *chunk;
@@ -557,11 +561,38 @@ void USBUartComponent::start_config_(bool reload) {
this->cfg_step_ = 0;
this->cfg_ok_ = true;
this->cfg_in_flight_ = false;
this->cfg_string_done_ = false;
this->cfg_string_in_flight_ = false;
this->cfg_done_.store(false);
this->cfg_active_ = true;
this->enable_loop();
}
bool USBUartComponent::fetch_interface_string_(USBUartChannelBase *channel) {
const CdcEps &eps = channel->cdc_dev_;
const uint8_t index =
eps.interrupt_interface_number != 0xFF ? eps.interrupt_interface_string_index : eps.bulk_interface_string_index;
channel->interface_string_[0] = '\0';
if (index == 0) {
return false;
}
this->cfg_done_.store(false);
const bool submitted =
this->read_string_descriptor(index, channel->interface_string_, [this](const usb_host::TransferStatus &status) {
if (!status.success) {
ESP_LOGW(TAG, "Interface string read failed: %s", esp_err_to_name(status.error_code));
}
// Release: publishes the string 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();
});
if (!submitted) {
ESP_LOGW(TAG, "Interface string read submit failed");
}
return submitted;
}
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);
@@ -595,6 +626,14 @@ bool USBUartComponent::run_config_machine_() {
if (!this->cfg_active_)
return false;
if (this->cfg_string_in_flight_) {
// Acquire: pairs with the release in fetch_interface_string_'s callback.
if (!this->cfg_done_.load(std::memory_order_acquire))
return false;
this->cfg_string_in_flight_ = false;
this->cfg_done_.store(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))
@@ -625,6 +664,16 @@ bool USBUartComponent::run_config_machine_() {
? this->cfg_single_
: (this->cfg_channel_idx_ < this->channels_.size() ? this->channels_[this->cfg_channel_idx_] : nullptr);
// Once per channel on init, before its settings: the interface string is part of the
// identity reported to clients, and the connected report waits for this machine.
if (channel != nullptr && !this->cfg_reload_ && !this->cfg_string_done_) {
this->cfg_string_done_ = true;
if (this->fetch_interface_string_(channel)) {
this->cfg_string_in_flight_ = true;
return true;
}
}
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,
@@ -648,11 +697,14 @@ bool USBUartComponent::run_config_machine_() {
// Advance to the next channel (or finish).
this->cfg_step_ = 0;
this->cfg_ok_ = true;
this->cfg_string_done_ = false;
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;
// Init is done and the line settings are on the wire: now the device is ready to use
this->report_connected_();
}
// If the machine just went idle and a reload was requested while it was busy, start it now.
+13
View File
@@ -35,6 +35,9 @@ struct CdcEps {
const usb_ep_desc_t *out_ep;
uint8_t bulk_interface_number;
uint8_t interrupt_interface_number;
// iInterface of each interface; 0 when the device provides no string for it
uint8_t interrupt_interface_string_index;
uint8_t bulk_interface_string_index;
};
enum CH34xChipType : uint8_t {
@@ -174,11 +177,16 @@ class USBUartChannelBase : public uart::UARTComponent, public Parented<USBUartCo
: this->cdc_dev_.bulk_interface_number;
}
/// iInterface string of that interface; empty when the device has none, or until it has
/// been read after the device connected
const char *get_interface_string() const { return this->interface_string_; }
protected:
// Not directly instantiable; construct a concrete channel type instead.
USBUartChannelBase(uint8_t index, uint16_t buffer_size) : input_buffer_(RingBuffer(buffer_size)), index_(index) {}
void check_logger_conflict() override {}
// Larger structures first (8+ bytes)
char interface_string_[usb_host::DESC_STRING_BUF_SIZE]{};
RingBuffer input_buffer_;
LockFreeQueue<UsbOutputChunk, USB_OUTPUT_CHUNK_COUNT> output_queue_;
// Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring
@@ -248,6 +256,9 @@ class USBUartComponent : public usb_host::USBClient {
void start_config_(bool reload);
// Advance the config state machine; called from loop(). Returns true if it did work.
bool run_config_machine_();
// Ask the device for the channel's interface string. Returns true when a transfer was
// submitted; its completion is signalled through cfg_done_ like a config step's.
bool fetch_interface_string_(USBUartChannelBase *channel);
// Per-subclass per-channel settings sequence. For the given zero-based step, issue the
// next control transfer via config_transfer_() and return true, or return false when the
@@ -277,6 +288,8 @@ class USBUartComponent : public usb_host::USBClient {
bool cfg_device_phase_{false};
bool cfg_in_flight_{false};
bool cfg_ok_{true};
bool cfg_string_done_{false};
bool cfg_string_in_flight_{false};
};
class USBUartTypeCdcAcm : public USBUartComponent {