[serial_proxy] Add USB identity query for USB-bridged ports

Add SERIAL_PROXY_PORT_TYPE_USB_SERIAL, derived automatically when a
port's uart_id resolves to a usb_uart channel (never set by the user),
and a SerialProxyGetUsbInfoRequest/Response pair (IDs 153/154) that
reads VID/PID/bcdDevice and the manufacturer/product/serial strings
live from the descriptors the USB host stack caches, so a client can
identify the attached device before subscribing. Ports that are not
USB_SERIAL answer NOT_SUPPORTED; an unplugged device answers with
connected=false. ZigbeeProxyRequest moves to ID 155 (expected merge
order: set_mode, USB info, zigbee).
This commit is contained in:
kbx81
2026-09-03 22:07:52 -05:00
parent 6a9791609f
commit 590a4a6268
18 changed files with 359 additions and 14 deletions
+37 -1
View File
@@ -78,6 +78,7 @@ service APIConnection {
rpc serial_proxy_write(SerialProxyWriteRequest) returns (void) {}
rpc serial_proxy_set_modem_pins(SerialProxySetModemPinsRequest) returns (void) {}
rpc serial_proxy_get_modem_pins(SerialProxyGetModemPinsRequest) returns (void) {}
rpc serial_proxy_get_usb_info(SerialProxyGetUsbInfoRequest) returns (void) {}
rpc serial_proxy_request(SerialProxyRequest) returns (void) {}
rpc serial_proxy_set_mode(SerialProxySetModeRequest) returns (void) {}
}
@@ -230,6 +231,11 @@ enum SerialProxyPortType {
SERIAL_PROXY_PORT_TYPE_TTL = 0;
SERIAL_PROXY_PORT_TYPE_RS232 = 1;
SERIAL_PROXY_PORT_TYPE_RS485 = 2;
// A serial device attached through a USB bridge. Set by the device configuration, never
// by the user; identifies ports whose USB identity can be read with
// SerialProxyGetUsbInfoRequest. Deliberately not a USB endpoint type: serial_proxy
// carries serial devices only, whatever bridge chip connects them.
SERIAL_PROXY_PORT_TYPE_USB_SERIAL = 3;
}
message SerialProxyInfo {
@@ -2873,6 +2879,36 @@ message SerialProxySetModeRequest {
SerialProxyMode mode = 2;
}
// Ask for the USB identity of the device behind a USB_SERIAL port. Read-only, so no
// subscription is required -- a client typically uses this to decide which port to
// subscribe to. Answered with NOT_SUPPORTED on ports that are not USB_SERIAL.
message SerialProxyGetUsbInfoRequest {
option (id) = 153;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1;
}
// The USB identity of the device currently behind a port, read live from the cached
// USB descriptors. Fields are zero/empty while no device is connected.
message SerialProxyGetUsbInfoResponse {
option (id) = 154;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1;
SerialProxyStatus status = 2; // NOT_SUPPORTED when the port is not USB_SERIAL
bool connected = 3; // True when a USB device is currently attached
uint32 vendor_id = 4;
uint32 product_id = 5;
uint32 bcd_device = 6;
uint32 interface_number = 7; // Channel index on multi-port bridges
string manufacturer = 8;
string product = 9;
string serial_number = 10;
}
// ==================== BLUETOOTH CONNECTION PARAMS ====================
message BluetoothSetConnectionParamsRequest {
option (id) = 145;
@@ -2902,7 +2938,7 @@ enum ZigbeeProxyRequestType {
}
message ZigbeeProxyRequest {
option (id) = 153;
option (id) = 155;
option (source) = SOURCE_BOTH;
option (ifdef) = "USE_ZIGBEE_PROXY";
+24
View File
@@ -51,6 +51,9 @@
#ifdef USE_ZIGBEE_PROXY
#include "esphome/components/zigbee_proxy/zigbee_proxy.h"
#endif
#ifdef USE_SERIAL_PROXY_USB_INFO
#include "esphome/components/usb_host/usb_host.h"
#endif
#ifdef USE_WATER_HEATER
#include "esphome/components/water_heater/water_heater.h"
#endif
@@ -1651,6 +1654,27 @@ void APIConnection::on_serial_proxy_get_modem_pins_request(const SerialProxyGetM
}
}
void APIConnection::on_serial_proxy_get_usb_info_request(const SerialProxyGetUsbInfoRequest &msg) {
auto &proxies = App.get_serial_proxies();
SerialProxyGetUsbInfoResponse resp{};
resp.instance = msg.instance;
if (msg.instance >= proxies.size()) {
ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance);
resp.status = enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT;
} else {
#ifdef USE_SERIAL_PROXY_USB_INFO
// The response's strings are views into this buffer, which outlives the send below
usb_host::UsbDeviceInfo info;
proxies[msg.instance]->get_usb_info(info, resp);
#else
resp.status = enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED;
#endif
}
if (!this->send_message(resp)) {
API_LOG_MSG_DROPPED(TAG, "Serial proxy response");
}
}
void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
auto &proxies = App.get_serial_proxies();
if (msg.instance >= proxies.size()) {
+1
View File
@@ -247,6 +247,7 @@ class APIConnection final : public APIServerConnectionBase {
void on_serial_proxy_write_request(const SerialProxyWriteRequest &msg);
void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg);
void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg);
void on_serial_proxy_get_usb_info_request(const SerialProxyGetUsbInfoRequest &msg);
void on_serial_proxy_request(const SerialProxyRequest &msg);
void on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &msg);
void send_serial_proxy_data(const SerialProxyDataReceived &msg);
+38
View File
@@ -4278,6 +4278,44 @@ bool SerialProxySetModeRequest::decode_varint(uint32_t field_id, proto_varint_va
}
return true;
}
bool SerialProxyGetUsbInfoRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
switch (field_id) {
case 1:
this->instance = value;
break;
default:
return false;
}
return true;
}
uint8_t *SerialProxyGetUsbInfoResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
uint8_t *__restrict__ pos = buffer.get_pos();
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->instance);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast<uint32_t>(this->status));
ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 3, this->connected);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, this->vendor_id);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 5, this->product_id);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, this->bcd_device);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, this->interface_number);
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);
return pos;
}
uint32_t SerialProxyGetUsbInfoResponse::calculate_size() const {
uint32_t size = 0;
size += ProtoSize::calc_uint32(1, this->instance);
size += this->status ? 2 : 0;
size += ProtoSize::calc_bool(1, this->connected);
size += ProtoSize::calc_uint32(1, this->vendor_id);
size += ProtoSize::calc_uint32(1, this->product_id);
size += ProtoSize::calc_uint32(1, this->bcd_device);
size += ProtoSize::calc_uint32(1, this->interface_number);
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());
return size;
}
#endif
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
bool BluetoothSetConnectionParamsRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
+42 -1
View File
@@ -23,6 +23,7 @@ enum SerialProxyPortType : uint32_t {
SERIAL_PROXY_PORT_TYPE_TTL = 0,
SERIAL_PROXY_PORT_TYPE_RS232 = 1,
SERIAL_PROXY_PORT_TYPE_RS485 = 2,
SERIAL_PROXY_PORT_TYPE_USB_SERIAL = 3,
};
enum EntityCategory : uint32_t {
ENTITY_CATEGORY_NONE = 0,
@@ -3435,6 +3436,46 @@ class SerialProxySetModeRequest final : public ProtoDecodableMessage {
protected:
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
class SerialProxyGetUsbInfoRequest final : public ProtoDecodableMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 153;
static constexpr uint8_t ESTIMATED_SIZE = 4;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("serial_proxy_get_usb_info_request"); }
#endif
uint32_t instance{0};
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
class SerialProxyGetUsbInfoResponse final : public ProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 154;
static constexpr uint8_t ESTIMATED_SIZE = 51;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("serial_proxy_get_usb_info_response"); }
#endif
uint32_t instance{0};
enums::SerialProxyStatus status{};
bool connected{false};
uint32_t vendor_id{0};
uint32_t product_id{0};
uint32_t bcd_device{0};
uint32_t interface_number{0};
StringRef manufacturer{};
StringRef product{};
StringRef serial_number{};
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
#endif
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
class BluetoothSetConnectionParamsRequest final : public ProtoDecodableMessage {
@@ -3477,7 +3518,7 @@ class BluetoothSetConnectionParamsResponse final : public ProtoMessage {
#ifdef USE_ZIGBEE_PROXY
class ZigbeeProxyRequest final : public ProtoDecodableMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 153;
static constexpr uint16_t MESSAGE_TYPE = 155;
static constexpr uint8_t ESTIMATED_SIZE = 21;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("zigbee_proxy_request"); }
+21
View File
@@ -143,6 +143,8 @@ template<> const char *proto_enum_to_string<enums::SerialProxyPortType>(enums::S
return ESPHOME_PSTR("SERIAL_PROXY_PORT_TYPE_RS232");
case enums::SERIAL_PROXY_PORT_TYPE_RS485:
return ESPHOME_PSTR("SERIAL_PROXY_PORT_TYPE_RS485");
case enums::SERIAL_PROXY_PORT_TYPE_USB_SERIAL:
return ESPHOME_PSTR("SERIAL_PROXY_PORT_TYPE_USB_SERIAL");
default:
return ESPHOME_PSTR("UNKNOWN");
}
@@ -2839,6 +2841,25 @@ const char *SerialProxySetModeRequest::dump_to(DumpBuffer &out) const {
dump_field(out, ESPHOME_PSTR("mode"), static_cast<enums::SerialProxyMode>(this->mode));
return out.c_str();
}
const char *SerialProxyGetUsbInfoRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyGetUsbInfoRequest"));
dump_field(out, ESPHOME_PSTR("instance"), this->instance);
return out.c_str();
}
const char *SerialProxyGetUsbInfoResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyGetUsbInfoResponse"));
dump_field(out, ESPHOME_PSTR("instance"), this->instance);
dump_field(out, ESPHOME_PSTR("status"), static_cast<enums::SerialProxyStatus>(this->status));
dump_field(out, ESPHOME_PSTR("connected"), this->connected);
dump_field(out, ESPHOME_PSTR("vendor_id"), this->vendor_id);
dump_field(out, ESPHOME_PSTR("product_id"), this->product_id);
dump_field(out, ESPHOME_PSTR("bcd_device"), this->bcd_device);
dump_field(out, ESPHOME_PSTR("interface_number"), this->interface_number);
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);
return out.c_str();
}
#endif
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
const char *BluetoothSetConnectionParamsRequest::dump_to(DumpBuffer &out) const {
@@ -723,6 +723,17 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui
break;
}
#endif
#ifdef USE_SERIAL_PROXY
case SerialProxyGetUsbInfoRequest::MESSAGE_TYPE: {
SerialProxyGetUsbInfoRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_serial_proxy_get_usb_info_request"), msg);
#endif
this->on_serial_proxy_get_usb_info_request(msg);
break;
}
#endif
#ifdef USE_ZIGBEE_PROXY
case ZigbeeProxyRequest::MESSAGE_TYPE: {
ZigbeeProxyRequest msg;
+4
View File
@@ -238,6 +238,10 @@ class APIServerConnectionBase {
#ifdef USE_SERIAL_PROXY
void on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &value){};
#endif
#ifdef USE_SERIAL_PROXY
void on_serial_proxy_get_usb_info_request(const SerialProxyGetUsbInfoRequest &value){};
#endif
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){};
#endif
+33 -3
View File
@@ -18,9 +18,10 @@ from esphome import pins
import esphome.codegen as cg
from esphome.components import uart
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_NAME
from esphome.const import CONF_ID, CONF_NAME, CONF_UART_ID
from esphome.core import CORE, coroutine_with_priority
from esphome.coroutine import CoroPriority
import esphome.final_validate as fv
from esphome.types import ConfigType
CODEOWNERS = ["@kbx81"]
@@ -34,11 +35,14 @@ SerialProxyTap = serial_proxy_ns.class_("SerialProxyTap")
api_enums_ns = cg.esphome_ns.namespace("api").namespace("enums")
SerialProxyPortType = api_enums_ns.enum("SerialProxyPortType")
# User-selectable electrical types. USB_SERIAL is deliberately absent: it is derived
# from the uart_id pointing at a usb_uart channel, never set by the user.
SERIAL_PROXY_PORT_TYPES = {
"TTL": SerialProxyPortType.SERIAL_PROXY_PORT_TYPE_TTL,
"RS232": SerialProxyPortType.SERIAL_PROXY_PORT_TYPE_RS232,
"RS485": SerialProxyPortType.SERIAL_PROXY_PORT_TYPE_RS485,
}
PORT_TYPE_USB_SERIAL = SerialProxyPortType.SERIAL_PROXY_PORT_TYPE_USB_SERIAL
CONF_DTR_PIN = "dtr_pin"
CONF_PORT_TYPE = "port_type"
@@ -63,7 +67,7 @@ CONFIG_SCHEMA = (
{
cv.GenerateID(): cv.declare_id(SerialProxy),
cv.Required(CONF_NAME): cv.string_strict,
cv.Required(CONF_PORT_TYPE): cv.enum(SERIAL_PROXY_PORT_TYPES, upper=True),
cv.Optional(CONF_PORT_TYPE): cv.enum(SERIAL_PROXY_PORT_TYPES, upper=True),
cv.Optional(CONF_RTS_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_DTR_PIN): pins.gpio_output_pin_schema,
}
@@ -73,6 +77,26 @@ CONFIG_SCHEMA = (
)
def _uses_usb_uart(config: ConfigType, full_config: ConfigType) -> bool:
from esphome.components.usb_uart import is_usb_uart_channel
return is_usb_uart_channel(config[CONF_UART_ID], full_config)
def _final_validate(config: ConfigType) -> ConfigType:
if _uses_usb_uart(config, fv.full_config.get()):
if CONF_PORT_TYPE in config:
raise cv.Invalid(
f"{CONF_PORT_TYPE} is set automatically for USB serial ports"
)
elif CONF_PORT_TYPE not in config:
raise cv.Invalid(f"{CONF_PORT_TYPE} is required")
return config
FINAL_VALIDATE_SCHEMA = _final_validate
@coroutine_with_priority(CoroPriority.FINAL)
async def _add_serial_proxy_count_define() -> None:
"""Emit the SERIAL_PROXY_COUNT define once with the final instance count."""
@@ -87,7 +111,13 @@ async def to_code(config: ConfigType) -> None:
await uart.register_uart_device(var, config)
cg.add(cg.App.register_serial_proxy(var))
cg.add(var.set_name(config[CONF_NAME]))
cg.add(var.set_port_type(config[CONF_PORT_TYPE]))
if _uses_usb_uart(config, CORE.config):
cg.add(var.set_port_type(PORT_TYPE_USB_SERIAL))
channel = await cg.get_variable(config[CONF_UART_ID])
cg.add(var.set_usb_channel(channel))
cg.add_define("USE_SERIAL_PROXY_USB_INFO")
else:
cg.add(var.set_port_type(config[CONF_PORT_TYPE]))
cg.add_define("USE_SERIAL_PROXY")
# Track instance count for the FINAL priority define
@@ -12,6 +12,10 @@
#include "esphome/components/api/api_server.h"
#endif
#ifdef USE_SERIAL_PROXY_USB_INFO
#include "esphome/components/usb_uart/usb_uart.h"
#endif
namespace esphome::serial_proxy {
static const char *const TAG = "serial_proxy";
@@ -156,9 +160,10 @@ void SerialProxy::dump_config() {
" RTS Pin: %s\n"
" DTR Pin: %s",
this->instance_index_, this->name_ != nullptr ? this->name_ : "",
this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS485 ? LOG_STR_LITERAL("RS485")
: this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS232 ? LOG_STR_LITERAL("RS232")
: LOG_STR_LITERAL("TTL"),
this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS485 ? LOG_STR_LITERAL("RS485")
: this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS232 ? LOG_STR_LITERAL("RS232")
: this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_USB_SERIAL ? LOG_STR_LITERAL("USB_SERIAL")
: LOG_STR_LITERAL("TTL"),
this->rts_pin_ != nullptr ? LOG_STR_LITERAL("configured") : LOG_STR_LITERAL("not configured"),
this->dtr_pin_ != nullptr ? LOG_STR_LITERAL("configured") : LOG_STR_LITERAL("not configured"));
}
@@ -333,6 +338,27 @@ SerialProxyResult SerialProxy::set_modem_pins(api::APIConnection *api_connection
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
}
#if defined(USE_SERIAL_PROXY_USB_INFO) && defined(USE_API)
void SerialProxy::get_usb_info(usb_host::UsbDeviceInfo &info, api::SerialProxyGetUsbInfoResponse &resp) const {
if (this->usb_channel_ == nullptr) {
resp.status = api::enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED;
return;
}
resp.interface_number = this->usb_channel_->get_index();
if (!this->usb_channel_->get_parent()->get_device_info(info)) {
// No device attached right now; not an error
return;
}
resp.connected = true;
resp.vendor_id = info.vendor_id;
resp.product_id = info.product_id;
resp.bcd_device = info.bcd_device;
resp.manufacturer = StringRef(info.manufacturer);
resp.product = StringRef(info.product);
resp.serial_number = StringRef(info.serial_number);
}
#endif
uint32_t SerialProxy::get_modem_pins() const {
return (this->rts_state_ ? static_cast<uint32_t>(SERIAL_PROXY_LINE_STATE_FLAG_RTS) : 0u) |
(this->dtr_state_ ? static_cast<uint32_t>(SERIAL_PROXY_LINE_STATE_FLAG_DTR) : 0u);
@@ -20,6 +20,15 @@
#include "esphome/components/api/api_pb2.h"
#endif
#ifdef USE_SERIAL_PROXY_USB_INFO
namespace esphome::usb_uart {
class USBUartChannel;
} // namespace esphome::usb_uart
namespace esphome::usb_host {
struct UsbDeviceInfo;
} // namespace esphome::usb_host
#endif
// Forward-declare types needed outside the USE_API guard.
namespace esphome::api {
class APIConnection;
@@ -155,6 +164,17 @@ class SerialProxy final : public uart::UARTDevice, public Component {
/// Set the DTR GPIO pin (from YAML configuration)
void set_dtr_pin(GPIOPin *pin) { this->dtr_pin_ = pin; }
#ifdef USE_SERIAL_PROXY_USB_INFO
/// Attach the USB UART channel behind this port (from code generation)
void set_usb_channel(usb_uart::USBUartChannel *channel) { this->usb_channel_ = channel; }
#ifdef USE_API
/// Fill a USB info response for this port. The response's strings are views into
/// info, so info must outlive the send.
void get_usb_info(usb_host::UsbDeviceInfo &info, api::SerialProxyGetUsbInfoResponse &resp) const;
#endif
#endif
#ifdef USE_SERIAL_PROXY_TAP
/// Attach a traffic observer. At most one, set once at setup time.
void set_tap(SerialProxyTap *tap) { this->tap_ = tap; }
@@ -251,6 +271,11 @@ class SerialProxy final : public uart::UARTDevice, public Component {
#ifdef USE_SERIAL_PROXY_TAP
SerialProxyTap *tap_{nullptr};
#endif
#ifdef USE_SERIAL_PROXY_USB_INFO
/// The USB UART channel behind this port; nullptr on non-USB ports
usb_uart::USBUartChannel *usb_channel_{nullptr};
#endif
};
} // namespace esphome::serial_proxy
+18
View File
@@ -117,6 +117,20 @@ struct UsbEvent {
// callback function type.
// USB string descriptors hold at most 126 characters; one more for the terminator
static constexpr size_t DESC_STRING_BUF_SIZE = 128;
/// Identity of a connected USB device, copied out of the descriptors the USB host
/// stack caches for the lifetime of the connection
struct UsbDeviceInfo {
uint16_t vendor_id;
uint16_t product_id;
uint16_t bcd_device;
char manufacturer[DESC_STRING_BUF_SIZE];
char product[DESC_STRING_BUF_SIZE];
char serial_number[DESC_STRING_BUF_SIZE];
};
enum ClientState {
USB_CLIENT_INIT = 0,
USB_CLIENT_OPEN,
@@ -144,6 +158,10 @@ class USBClient : public Component {
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 = {});
/// Copy the connected device's identity out of the cached USB descriptors.
/// Returns false when no device is connected.
bool get_device_info(UsbDeviceInfo &info) const;
// Lock-free event queue and pool for USB task to main loop communication
// Must be public for access from static callbacks
LockFreeQueue<UsbEvent, USB_EVENT_QUEUE_SIZE> event_queue;
@@ -143,10 +143,8 @@ static void usb_client_print_config_descriptor(const usb_config_desc_t *cfg_desc
} while (next_desc != NULL);
}
#endif
// USB string descriptors: bLength (uint8_t, max 255) includes the 2-byte header (bLength and bDescriptorType).
// Character count = (bLength - 2) / 2, max 126 chars + null terminator.
static constexpr size_t DESC_STRING_BUF_SIZE = 128;
// bLength (uint8_t, max 255) includes the 2-byte header (bLength and bDescriptorType),
// so character count = (bLength - 2) / 2.
static const char *get_descriptor_string(const usb_str_desc_t *desc, std::span<char, DESC_STRING_BUF_SIZE> buffer) {
if (desc == nullptr || desc->bLength < 2)
return "(unspecified)";
@@ -162,6 +160,41 @@ static const char *get_descriptor_string(const usb_str_desc_t *desc, std::span<c
return buffer.data();
}
// 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) {
buffer[0] = '\0';
if (desc == nullptr || desc->bLength < 2)
return;
int char_count = (desc->bLength - 2) / 2;
char *p = buffer.data();
char *end = p + buffer.size() - 1;
for (int i = 0; i != char_count && p < end; i++) {
auto c = desc->wData[i];
if (c < 0x100)
*p++ = static_cast<char>(c);
}
*p = '\0';
}
bool USBClient::get_device_info(UsbDeviceInfo &info) const {
if (this->state_ != USB_CLIENT_CONNECTED)
return false;
const usb_device_desc_t *desc;
if (usb_host_get_device_descriptor(this->device_handle_, &desc) != ESP_OK)
return false;
info.vendor_id = desc->idVendor;
info.product_id = desc->idProduct;
info.bcd_device = desc->bcdDevice;
usb_device_info_t dev_info;
if (usb_host_device_info(this->device_handle_, &dev_info) != ESP_OK)
return false;
copy_descriptor_string(dev_info.str_desc_manufacturer, info.manufacturer);
copy_descriptor_string(dev_info.str_desc_product, info.product);
copy_descriptor_string(dev_info.str_desc_serial_num, info.serial_number);
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);
+11 -1
View File
@@ -16,7 +16,7 @@ from esphome.const import (
CONF_DUMMY_RECEIVER,
CONF_ID,
)
from esphome.core import CORE
from esphome.core import CORE, ID
from esphome.cpp_types import Component
from esphome.types import ConfigType
@@ -27,6 +27,16 @@ usb_uart_ns = cg.esphome_ns.namespace("usb_uart")
USBUartComponent = usb_uart_ns.class_("USBUartComponent", Component)
USBUartChannel = usb_uart_ns.class_("USBUartChannel", UARTComponent)
def is_usb_uart_channel(uart_id: ID, full_config: ConfigType) -> bool:
"""Return True if the given ID refers to a channel of a configured usb_uart device."""
return any(
channel[CONF_ID] == uart_id
for device in full_config.get("usb_uart") or []
for channel in device[CONF_CHANNELS]
)
UARTParityOptions = usb_uart_ns.enum("UARTParityOptions")
UART_PARITY_OPTIONS = {
"NONE": UARTParityOptions.UART_CONFIG_PARITY_NONE,
+3
View File
@@ -164,6 +164,9 @@ class USBUartChannelBase : public uart::UARTComponent, public Parented<USBUartCo
/// they arrive, eliminating one full main-loop-wakeup cycle of latency.
void set_rx_callback(std::function<void()> cb) { this->rx_callback_ = std::move(cb); }
/// Channel index on the bridge (interface number on multi-port bridges)
uint8_t get_index() const { return this->index_; }
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) {}
+4
View File
@@ -395,6 +395,10 @@
#define USB_HOST_MAX_REQUESTS 16
#define USB_HOST_MAX_PACKET_SIZE 64
#define USB_UART_OUTPUT_CHUNK_COUNT 5
// USB identity on serial proxy ports needs the usb_host stack
#ifdef USE_ESP32
#define USE_SERIAL_PROXY_USB_INFO
#endif
#ifdef USE_ARDUINO
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 7)
@@ -0,0 +1,21 @@
wifi:
ssid: MySSID
password: password1
api:
usb_host:
# port_type is omitted deliberately: a port on a USB UART channel derives USB_SERIAL
usb_uart:
- type: CDC_ACM
vid: 0x303A
pid: 0x831A
channels:
- id: usb_serial_channel
baud_rate: 460800
serial_proxy:
- id: serial_proxy_usb
uart_id: usb_serial_channel
name: USB Serial Port
@@ -21,7 +21,6 @@ serial_proxy:
- id: zigbee_usb_serial
uart_id: zigbee_usb_channel
name: Zigbee
port_type: TTL
zigbee_proxy:
serial_proxy_id: zigbee_usb_serial