mirror of
https://github.com/esphome/esphome.git
synced 2026-10-02 09:20:22 +00:00
[usb_host] Read the interface descriptor string, for full Linux parity
This commit is contained in:
@@ -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 ====================
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user