mirror of
https://github.com/esphome/esphome.git
synced 2026-10-07 19:44:08 +00:00
[serial_proxy] Add USB_SERIAL port type and device identity (#19308)
Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
This commit is contained in:
co-authored by
J. Nick Koston
J. Nick Koston
Keith Burzinski
parent
ae6ebd2d9d
commit
1c0d406cbb
@@ -76,6 +76,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 subscribe_serial_proxy_identity(SubscribeSerialProxyIdentityRequest) returns (void) {}
|
||||
rpc serial_proxy_request(SerialProxyRequest) returns (void) {}
|
||||
rpc serial_proxy_set_mode(SerialProxySetModeRequest) returns (void) {}
|
||||
}
|
||||
@@ -233,6 +234,7 @@ enum SerialProxyPortType {
|
||||
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; // since API 1.18
|
||||
}
|
||||
|
||||
message SerialProxyInfo {
|
||||
@@ -2907,6 +2909,56 @@ message SerialProxySetModeRequest {
|
||||
SerialProxyMode mode = 2;
|
||||
}
|
||||
|
||||
// Subscribe to the identity of every serial proxy port. The device answers with one
|
||||
// SerialProxyIdentity per port, then sends another whenever a port's identity changes,
|
||||
// for the life of the connection (since API 1.18).
|
||||
message SubscribeSerialProxyIdentityRequest {
|
||||
option (id) = 154;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
option (ifdef) = "USE_SERIAL_PROXY";
|
||||
}
|
||||
|
||||
// Where a port's identity comes from
|
||||
enum SerialProxyIdentitySource {
|
||||
SERIAL_PROXY_IDENTITY_SOURCE_NONE = 0; // The port carries no identity
|
||||
SERIAL_PROXY_IDENTITY_SOURCE_CONFIGURED = 1; // Reserved for identities stated in the device configuration; not sent yet
|
||||
SERIAL_PROXY_IDENTITY_SOURCE_USB = 2; // Read from the descriptors of the USB device behind the port;
|
||||
// changes when a device is attached or removed
|
||||
}
|
||||
|
||||
enum SerialProxyIdentityFlag {
|
||||
SERIAL_PROXY_IDENTITY_FLAG_NONE = 0;
|
||||
SERIAL_PROXY_IDENTITY_FLAG_CONNECTED = 1; // The backend believes the device is present and usable on this port
|
||||
SERIAL_PROXY_IDENTITY_FLAG_ERROR = 2; // The USB host stack refused the descriptor query; the strings and
|
||||
// IDs below are empty
|
||||
}
|
||||
|
||||
// The descriptor fields of a USB device as seen by the host stack
|
||||
message UsbDeviceDescriptor {
|
||||
uint32 vendor_id = 1;
|
||||
uint32 product_id = 2;
|
||||
uint32 bcd_device = 3;
|
||||
uint32 interface_number = 4; // bInterfaceNumber the host driver binds to
|
||||
}
|
||||
|
||||
// The identity of the device behind a port. Identifies one physical device among others of
|
||||
// the same kind, so a client matches on manufacturer and product regardless of source. The
|
||||
// strings are empty for source NONE, and for source USB while nothing is connected
|
||||
// (since API 1.18).
|
||||
message SerialProxyIdentity {
|
||||
option (id) = 155;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_SERIAL_PROXY";
|
||||
|
||||
uint32 instance = 1;
|
||||
SerialProxyIdentitySource source = 2;
|
||||
uint32 flags = 3; // Bitmask of SerialProxyIdentityFlag
|
||||
string manufacturer = 4;
|
||||
string product = 5;
|
||||
string serial_number = 6;
|
||||
UsbDeviceDescriptor usb = 7; // Only sent for source USB
|
||||
}
|
||||
|
||||
// ==================== BLUETOOTH CONNECTION PARAMS ====================
|
||||
message BluetoothSetConnectionParamsRequest {
|
||||
option (id) = 145;
|
||||
|
||||
@@ -1685,6 +1685,22 @@ void APIConnection::on_serial_proxy_get_modem_pins_request(const SerialProxyGetM
|
||||
}
|
||||
}
|
||||
|
||||
void APIConnection::on_subscribe_serial_proxy_identity_request() {
|
||||
#ifdef USE_SERIAL_PROXY_USB_IDENTITY
|
||||
// Only USB ports change identity after this snapshot
|
||||
this->flags_.serial_proxy_identity_subscription = true;
|
||||
#endif
|
||||
for (auto *proxy : App.get_serial_proxies()) {
|
||||
proxy->send_identity(this);
|
||||
}
|
||||
}
|
||||
|
||||
void APIConnection::send_serial_proxy_identity(const SerialProxyIdentity &msg) {
|
||||
if (!this->send_message(msg)) {
|
||||
API_LOG_MSG_DROPPED(TAG, "Serial proxy identity");
|
||||
}
|
||||
}
|
||||
|
||||
void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
|
||||
auto &proxies = App.get_serial_proxies();
|
||||
if (msg.instance >= proxies.size()) {
|
||||
|
||||
@@ -246,6 +246,9 @@ 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_subscribe_serial_proxy_identity_request();
|
||||
/// Send a port identity to this client
|
||||
void send_serial_proxy_identity(const SerialProxyIdentity &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);
|
||||
@@ -760,9 +763,12 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
uint8_t log_only_mode : 1;
|
||||
#endif
|
||||
} flags_{}; // 2 bytes total
|
||||
#ifdef USE_SERIAL_PROXY_USB_IDENTITY
|
||||
uint8_t serial_proxy_identity_subscription : 1;
|
||||
#endif
|
||||
} flags_{}; // 2 bytes; 3 with HAS_PROTO_MESSAGE_DUMP + USE_API_OUTGOING_CONNECTION + USE_SERIAL_PROXY_USB_IDENTITY
|
||||
|
||||
// 2-byte type immediately after flags_ (no padding between them)
|
||||
// 2-byte type immediately after flags_ (one padding byte when flags_ is 3 bytes)
|
||||
uint16_t batch_message_type_{0}; // Current message type during batch encoding
|
||||
// 1-byte types to fill remaining space before next 4-byte boundary
|
||||
// Client API versions are clamped to 255 on receive (see send_hello_response_)
|
||||
|
||||
@@ -4199,6 +4199,48 @@ void SerialProxySetModeRequest::decode_field(void *self, uint32_t tag, const uin
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t *UsbDeviceDescriptor::encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) {
|
||||
const auto &msg = *static_cast<const UsbDeviceDescriptor *>(self);
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.vendor_id);
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, msg.product_id);
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.bcd_device);
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, msg.interface_number);
|
||||
return pos;
|
||||
}
|
||||
uint32_t UsbDeviceDescriptor::calc_size_msg(const void *self) {
|
||||
const auto &msg = *static_cast<const UsbDeviceDescriptor *>(self);
|
||||
uint32_t size = 0;
|
||||
size += ProtoSize::calc_uint32(1, msg.vendor_id);
|
||||
size += ProtoSize::calc_uint32(1, msg.product_id);
|
||||
size += ProtoSize::calc_uint32(1, msg.bcd_device);
|
||||
size += ProtoSize::calc_uint32(1, msg.interface_number);
|
||||
return size;
|
||||
}
|
||||
uint8_t *SerialProxyIdentity::encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) {
|
||||
const auto &msg = *static_cast<const SerialProxyIdentity *>(self);
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.instance);
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast<uint32_t>(msg.source));
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.flags);
|
||||
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 4, msg.manufacturer);
|
||||
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, msg.product);
|
||||
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 6, msg.serial_number);
|
||||
pos = ProtoEncode::encode_optional_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 7, msg.usb);
|
||||
return pos;
|
||||
}
|
||||
uint32_t SerialProxyIdentity::calc_size_msg(const void *self) {
|
||||
const auto &msg = *static_cast<const SerialProxyIdentity *>(self);
|
||||
uint32_t size = 0;
|
||||
size += ProtoSize::calc_uint32(1, msg.instance);
|
||||
size += msg.source ? 2 : 0;
|
||||
size += ProtoSize::calc_uint32(1, msg.flags);
|
||||
size += ProtoSize::calc_length(1, msg.manufacturer.size());
|
||||
size += ProtoSize::calc_length(1, msg.product.size());
|
||||
size += ProtoSize::calc_length(1, msg.serial_number.size());
|
||||
size += ProtoSize::calc_message(1, msg.usb.calculate_size());
|
||||
return size;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
||||
void BluetoothSetConnectionParamsRequest::decode_field(void *self, uint32_t tag, const uint8_t *data,
|
||||
|
||||
@@ -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,
|
||||
@@ -371,7 +372,17 @@ enum SerialProxyMode : uint32_t {
|
||||
SERIAL_PROXY_MODE_RAW = 0,
|
||||
SERIAL_PROXY_MODE_PROTOCOL = 1,
|
||||
};
|
||||
enum SerialProxyIdentitySource : uint32_t {
|
||||
SERIAL_PROXY_IDENTITY_SOURCE_NONE = 0,
|
||||
SERIAL_PROXY_IDENTITY_SOURCE_CONFIGURED = 1,
|
||||
SERIAL_PROXY_IDENTITY_SOURCE_USB = 2,
|
||||
};
|
||||
#endif
|
||||
enum SerialProxyIdentityFlag : uint32_t {
|
||||
SERIAL_PROXY_IDENTITY_FLAG_NONE = 0,
|
||||
SERIAL_PROXY_IDENTITY_FLAG_CONNECTED = 1,
|
||||
SERIAL_PROXY_IDENTITY_FLAG_ERROR = 2,
|
||||
};
|
||||
|
||||
} // namespace enums
|
||||
|
||||
@@ -3973,6 +3984,50 @@ class SerialProxySetModeRequest final : public ProtoDecodableMessage {
|
||||
protected:
|
||||
static void decode_field(void *self, uint32_t tag, const uint8_t *data, proto_varint_value_t scalar);
|
||||
};
|
||||
class UsbDeviceDescriptor final : public ProtoMessage {
|
||||
public:
|
||||
uint32_t vendor_id{0};
|
||||
uint32_t product_id{0};
|
||||
uint32_t bcd_device{0};
|
||||
uint32_t interface_number{0};
|
||||
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);
|
||||
}
|
||||
static uint32_t calc_size_msg(const void *self);
|
||||
uint32_t calculate_size() const { return calc_size_msg(this); }
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
class SerialProxyIdentity final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint16_t MESSAGE_TYPE = 155;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 54;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("serial_proxy_identity"); }
|
||||
#endif
|
||||
uint32_t instance{0};
|
||||
enums::SerialProxyIdentitySource source{};
|
||||
uint32_t flags{0};
|
||||
StringRef manufacturer{nullptr, 0}; // null until set, encode only
|
||||
StringRef product{nullptr, 0}; // null until set, encode only
|
||||
StringRef serial_number{nullptr, 0}; // null until set, encode only
|
||||
UsbDeviceDescriptor usb{};
|
||||
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);
|
||||
}
|
||||
static uint32_t calc_size_msg(const void *self);
|
||||
uint32_t calculate_size() const { return calc_size_msg(this); }
|
||||
#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 {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -890,7 +892,31 @@ template<> const char *proto_enum_to_string<enums::SerialProxyMode>(enums::Seria
|
||||
return ESPHOME_PSTR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
template<> const char *proto_enum_to_string<enums::SerialProxyIdentitySource>(enums::SerialProxyIdentitySource value) {
|
||||
switch (value) {
|
||||
case enums::SERIAL_PROXY_IDENTITY_SOURCE_NONE:
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_IDENTITY_SOURCE_NONE");
|
||||
case enums::SERIAL_PROXY_IDENTITY_SOURCE_CONFIGURED:
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_IDENTITY_SOURCE_CONFIGURED");
|
||||
case enums::SERIAL_PROXY_IDENTITY_SOURCE_USB:
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_IDENTITY_SOURCE_USB");
|
||||
default:
|
||||
return ESPHOME_PSTR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
template<> const char *proto_enum_to_string<enums::SerialProxyIdentityFlag>(enums::SerialProxyIdentityFlag value) {
|
||||
switch (value) {
|
||||
case enums::SERIAL_PROXY_IDENTITY_FLAG_NONE:
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_IDENTITY_FLAG_NONE");
|
||||
case enums::SERIAL_PROXY_IDENTITY_FLAG_CONNECTED:
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_IDENTITY_FLAG_CONNECTED");
|
||||
case enums::SERIAL_PROXY_IDENTITY_FLAG_ERROR:
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_IDENTITY_FLAG_ERROR");
|
||||
default:
|
||||
return ESPHOME_PSTR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
const char *HelloRequest::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("HelloRequest"));
|
||||
@@ -2841,6 +2867,27 @@ 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 *UsbDeviceDescriptor::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("UsbDeviceDescriptor"));
|
||||
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);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *SerialProxyIdentity::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyIdentity"));
|
||||
dump_field(out, ESPHOME_PSTR("instance"), this->instance);
|
||||
dump_field(out, ESPHOME_PSTR("source"), static_cast<enums::SerialProxyIdentitySource>(this->source));
|
||||
dump_field(out, ESPHOME_PSTR("flags"), this->flags);
|
||||
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);
|
||||
out.append(2, ' ').append_p(ESPHOME_PSTR("usb")).append(": ");
|
||||
this->usb.dump_to(out);
|
||||
out.append("\n");
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
||||
const char *BluetoothSetConnectionParamsRequest::dump_to(DumpBuffer &out) const {
|
||||
|
||||
@@ -722,6 +722,15 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui
|
||||
this->on_serial_proxy_set_mode_request(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
case 154 /* SubscribeSerialProxyIdentityRequest is empty */: {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_subscribe_serial_proxy_identity_request"));
|
||||
#endif
|
||||
this->on_subscribe_serial_proxy_identity_request();
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -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_subscribe_serial_proxy_identity_request(){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
||||
void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){};
|
||||
#endif
|
||||
|
||||
@@ -504,6 +504,15 @@ void APIServer::on_zwave_proxy_request(const ZWaveProxyRequest &msg) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY_USB_IDENTITY
|
||||
void APIServer::send_serial_proxy_identity(const SerialProxyIdentity &msg) {
|
||||
for (auto &c : this->active_clients()) {
|
||||
if (c->flags_.serial_proxy_identity_subscription)
|
||||
c->send_serial_proxy_identity(msg);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_IR_RF
|
||||
void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_id, uint32_t key,
|
||||
const std::vector<int32_t> *timings) {
|
||||
|
||||
@@ -203,6 +203,10 @@ class APIServer final : public Component
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
void on_zwave_proxy_request(const ZWaveProxyRequest &msg);
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY_USB_IDENTITY
|
||||
/// Tell every subscribed client that a serial proxy port's identity changed
|
||||
void send_serial_proxy_identity(const SerialProxyIdentity &msg);
|
||||
#endif
|
||||
#ifdef USE_IR_RF
|
||||
void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector<int32_t> *timings);
|
||||
#endif
|
||||
|
||||
@@ -17,10 +17,12 @@ from dataclasses import dataclass
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import uart
|
||||
from esphome.components.usb_uart import is_usb_uart_channel
|
||||
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"]
|
||||
@@ -38,6 +40,7 @@ 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,
|
||||
"USB_SERIAL": SerialProxyPortType.SERIAL_PROXY_PORT_TYPE_USB_SERIAL,
|
||||
}
|
||||
|
||||
CONF_DTR_PIN = "dtr_pin"
|
||||
@@ -73,6 +76,18 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> ConfigType:
|
||||
is_usb = is_usb_uart_channel(config[CONF_UART_ID], fv.full_config.get())
|
||||
if config[CONF_PORT_TYPE] == "USB_SERIAL" and not is_usb:
|
||||
raise cv.Invalid(
|
||||
f"{CONF_PORT_TYPE} USB_SERIAL requires {CONF_UART_ID} to be a usb_uart channel"
|
||||
)
|
||||
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."""
|
||||
@@ -88,6 +103,12 @@ async def to_code(config: ConfigType) -> None:
|
||||
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]))
|
||||
# port_type names the electrical interface (a USB RS485 adapter is RS485), so every
|
||||
# usb_uart channel reports USB identity whatever port type it declares
|
||||
if is_usb_uart_channel(config[CONF_UART_ID], CORE.config):
|
||||
channel = await cg.get_variable(config[CONF_UART_ID])
|
||||
cg.add(var.set_usb_channel(channel))
|
||||
cg.add_define("USE_SERIAL_PROXY_USB_IDENTITY")
|
||||
cg.add_define("USE_SERIAL_PROXY")
|
||||
|
||||
# Track instance count for the FINAL priority define
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
#include "esphome/components/api/api_server.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY_USB_IDENTITY
|
||||
#include "esphome/components/usb_uart/usb_uart.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::serial_proxy {
|
||||
|
||||
static const char *const TAG = "serial_proxy";
|
||||
@@ -35,6 +39,12 @@ void SerialProxy::setup() {
|
||||
// instance_index_ is fixed at registration time; pre-set it so loop() only needs to update data
|
||||
this->outgoing_msg_.instance = this->instance_index_;
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY_USB_IDENTITY
|
||||
if (this->usb_channel_ != nullptr) {
|
||||
this->usb_channel_->get_parent()->add_on_connection_callback(
|
||||
[this](bool connected) { this->on_usb_connection_changed_(connected); });
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY_TAP
|
||||
// A tap sets itself up before this runs (its setup priority is higher), so it may
|
||||
// already be waiting on the port -- a boot-time handshake with the device, say. Leaving
|
||||
@@ -161,9 +171,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"));
|
||||
}
|
||||
@@ -373,6 +384,60 @@ SerialProxyResult SerialProxy::set_modem_pins(api::APIConnection *api_connection
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
|
||||
}
|
||||
|
||||
#ifdef USE_API
|
||||
void SerialProxy::send_identity(api::APIConnection *api_connection) {
|
||||
IdentityScratch scratch;
|
||||
api::SerialProxyIdentity msg{};
|
||||
this->fill_identity_(scratch, msg);
|
||||
api_connection->send_serial_proxy_identity(msg);
|
||||
}
|
||||
|
||||
void SerialProxy::fill_identity_([[maybe_unused]] IdentityScratch &scratch, api::SerialProxyIdentity &msg) const {
|
||||
msg.instance = this->instance_index_;
|
||||
#ifdef USE_SERIAL_PROXY_USB_IDENTITY
|
||||
// The define is global, so a hardware UART port in the same config also gets here
|
||||
if (this->usb_channel_ != nullptr) {
|
||||
msg.source = api::enums::SERIAL_PROXY_IDENTITY_SOURCE_USB;
|
||||
// Covers a removed device, a channel the device has no CDC function for and a failed channel setup
|
||||
if (!this->usb_channel_->is_connected()) {
|
||||
return;
|
||||
}
|
||||
auto *client = this->usb_channel_->get_parent();
|
||||
msg.flags = api::enums::SERIAL_PROXY_IDENTITY_FLAG_CONNECTED;
|
||||
if (!client->get_device_info(scratch)) {
|
||||
msg.flags |= api::enums::SERIAL_PROXY_IDENTITY_FLAG_ERROR;
|
||||
return;
|
||||
}
|
||||
msg.usb.vendor_id = scratch.vendor_id;
|
||||
msg.usb.product_id = scratch.product_id;
|
||||
msg.usb.bcd_device = scratch.bcd_device;
|
||||
msg.usb.interface_number = this->usb_channel_->get_interface_number();
|
||||
msg.manufacturer = StringRef(scratch.manufacturer);
|
||||
msg.product = StringRef(scratch.product);
|
||||
msg.serial_number = StringRef(scratch.serial_number);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
// Zero-initialized message: source NONE, no flags
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY_USB_IDENTITY
|
||||
void SerialProxy::on_usb_connection_changed_(bool connected) {
|
||||
ESP_LOGD(TAG, "USB device %s serial proxy [%" PRIu32 "]",
|
||||
connected ? LOG_STR_LITERAL("attached to") : LOG_STR_LITERAL("removed from"), this->instance_index_);
|
||||
#ifdef USE_API
|
||||
if (api::global_api_server == nullptr) {
|
||||
return;
|
||||
}
|
||||
IdentityScratch scratch;
|
||||
api::SerialProxyIdentity msg{};
|
||||
this->fill_identity_(scratch, msg);
|
||||
api::global_api_server->send_serial_proxy_identity(msg);
|
||||
#endif
|
||||
}
|
||||
#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_IDENTITY
|
||||
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;
|
||||
@@ -160,6 +169,16 @@ 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_IDENTITY
|
||||
/// Attach the USB UART channel behind this port (from code generation)
|
||||
void set_usb_channel(usb_uart::USBUartChannel *channel) { this->usb_channel_ = channel; }
|
||||
#endif
|
||||
|
||||
#ifdef USE_API
|
||||
/// Send this port's identity to one client
|
||||
void send_identity(api::APIConnection *api_connection);
|
||||
#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; }
|
||||
@@ -226,6 +245,23 @@ class SerialProxy final : public uart::UARTDevice, public Component {
|
||||
bool tap_observing_() const;
|
||||
#endif
|
||||
|
||||
#ifdef USE_API
|
||||
#ifdef USE_SERIAL_PROXY_USB_IDENTITY
|
||||
using IdentityScratch = usb_host::UsbDeviceInfo;
|
||||
#else
|
||||
struct IdentityScratch {};
|
||||
#endif
|
||||
/// Fill an identity message for this port. The message's strings are views into scratch,
|
||||
/// so it must outlive the send.
|
||||
void fill_identity_(IdentityScratch &scratch, api::SerialProxyIdentity &msg) const;
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY_USB_IDENTITY
|
||||
/// The USB device behind this port was attached or removed; report the port's new
|
||||
/// identity to every subscribed API client
|
||||
void on_usb_connection_changed_(bool connected);
|
||||
#endif
|
||||
|
||||
/// Instance index for identifying this proxy in API messages
|
||||
uint32_t instance_index_{0};
|
||||
|
||||
@@ -268,6 +304,11 @@ class SerialProxy final : public uart::UARTDevice, public Component {
|
||||
#ifdef USE_SERIAL_PROXY_TAP
|
||||
SerialProxyTap *tap_{nullptr};
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY_USB_IDENTITY
|
||||
/// The USB UART channel behind this port; nullptr on non-USB ports
|
||||
usb_uart::USBUartChannel *usb_channel_{nullptr};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace esphome::serial_proxy
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -117,6 +119,25 @@ 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];
|
||||
};
|
||||
|
||||
/// Copy a USB string descriptor into a NUL-terminated buffer. A missing descriptor copies as
|
||||
/// an empty string. Returns false when a descriptor contains non-ASCII characters,
|
||||
/// UTF-16 to UTF-8 conversion is not currently implemented.
|
||||
bool 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,
|
||||
@@ -146,6 +167,23 @@ class USBClient : public Component {
|
||||
void set_manufacturer_filter(const char16_t *manufacturer) { this->manufacturer_filter_ = manufacturer; }
|
||||
void set_product_filter(const char16_t *product) { this->product_filter_ = product; }
|
||||
|
||||
/// Whether a device has been opened and its setup by the subclass has finished
|
||||
bool is_connected() const { return this->connection_reported_; }
|
||||
|
||||
/// Copy the connected device's identity out of the cached USB descriptors.
|
||||
/// Returns false when no device is connected or the host stack refused the query.
|
||||
bool get_device_info(UsbDeviceInfo &info) const;
|
||||
|
||||
/// Register a callback for the device this client claims being connected (true) or
|
||||
/// removed (false). Fires only for a device that was fully opened, so a device another
|
||||
/// client claims is never reported. Called from the main loop: connected once the device
|
||||
/// has been enumerated and the subclass has finished its setup of it (whether or not that
|
||||
/// setup succeeded), removed after on_disconnected() has run. This tracks the device's
|
||||
/// presence, not whether a given channel is usable.
|
||||
template<typename F> void add_on_connection_callback(F &&callback) {
|
||||
this->connection_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
// 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;
|
||||
@@ -163,6 +201,13 @@ class USBClient : public Component {
|
||||
TransferRequest *get_trq_(); // Lock-free allocation using atomic bitmask (multi-consumer safe)
|
||||
virtual void disconnect();
|
||||
virtual void on_connected() {}
|
||||
|
||||
/// Whether the subclass reports the device as connected itself, once its own setup of
|
||||
/// the device has finished, rather than as soon as the device has been opened
|
||||
virtual bool reports_connection_itself() const { return false; }
|
||||
/// Report the claimed device to the connection callbacks. Idempotent; a subclass that
|
||||
/// reports itself calls this once the device is ready to use.
|
||||
void report_connected_();
|
||||
virtual void on_disconnected() {
|
||||
// Reset all requests to available (all bits to 0)
|
||||
this->trq_in_use_.store(0);
|
||||
@@ -179,6 +224,7 @@ class USBClient : public Component {
|
||||
usb_device_handle_t device_handle_{};
|
||||
int device_addr_{-1};
|
||||
int state_{USB_CLIENT_INIT};
|
||||
LazyCallbackManager<void(bool)> connection_callback_;
|
||||
// Lock-free pool management using atomic bitmask (no dynamic allocation)
|
||||
// Bit i = 1: requests_[i] is in use, Bit i = 0: requests_[i] is available
|
||||
// Supports multiple concurrent consumers and producers (both threads can allocate/deallocate)
|
||||
@@ -187,6 +233,9 @@ class USBClient : public Component {
|
||||
const char16_t *product_filter_{nullptr};
|
||||
uint16_t vid_{};
|
||||
uint16_t pid_{};
|
||||
// Whether the connection callbacks were told about the current device, so a removal is
|
||||
// only ever reported for a device that was reported connected
|
||||
bool connection_reported_{false};
|
||||
};
|
||||
class USBHost final : public Component {
|
||||
public:
|
||||
|
||||
@@ -145,9 +145,27 @@ 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.
|
||||
bool 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 true;
|
||||
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];
|
||||
// TODO: encode non-ASCII code units as UTF-8 if a device with such descriptors turns up
|
||||
if (c >= 0x80) {
|
||||
buffer[0] = '\0';
|
||||
return false;
|
||||
}
|
||||
*p++ = static_cast<char>(c);
|
||||
}
|
||||
*p = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
// Folds UTF-16 to Latin-1 for logging, dropping anything that does not fit
|
||||
template<typename T>
|
||||
@@ -183,6 +201,36 @@ static bool descriptor_string_equals(const usb_str_desc_t *desc, const char16_t
|
||||
return expected[char_count] == u'\0';
|
||||
}
|
||||
|
||||
bool USBClient::get_device_info(UsbDeviceInfo &info) const {
|
||||
if (!this->is_connected())
|
||||
return false;
|
||||
const usb_device_desc_t *desc;
|
||||
esp_err_t err = usb_host_get_device_descriptor(this->device_handle_, &desc);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Device descriptor query failed: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
info.vendor_id = desc->idVendor;
|
||||
info.product_id = desc->idProduct;
|
||||
info.bcd_device = desc->bcdDevice;
|
||||
usb_device_info_t dev_info;
|
||||
err = usb_host_device_info(this->device_handle_, &dev_info);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Device info query failed: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
if (!copy_descriptor_string(dev_info.str_desc_manufacturer, info.manufacturer)) {
|
||||
ESP_LOGW(TAG, "Manufacturer string descriptor is not ASCII");
|
||||
}
|
||||
if (!copy_descriptor_string(dev_info.str_desc_product, info.product)) {
|
||||
ESP_LOGW(TAG, "Product string descriptor is not ASCII");
|
||||
}
|
||||
if (!copy_descriptor_string(dev_info.str_desc_serial_num, info.serial_number)) {
|
||||
ESP_LOGW(TAG, "Serial number string descriptor is not ASCII");
|
||||
}
|
||||
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);
|
||||
@@ -367,6 +415,18 @@ void USBClient::handle_open_state_() {
|
||||
usb_client_print_config_descriptor(config_desc, nullptr);
|
||||
#endif
|
||||
this->on_connected();
|
||||
// on_connected() may have rejected the device (no usable interface, say) and closed it
|
||||
if (this->state_ == USB_CLIENT_CONNECTED && !this->reports_connection_itself()) {
|
||||
this->report_connected_();
|
||||
}
|
||||
}
|
||||
|
||||
void USBClient::report_connected_() {
|
||||
if (this->state_ != USB_CLIENT_CONNECTED || this->connection_reported_) {
|
||||
return;
|
||||
}
|
||||
this->connection_reported_ = true;
|
||||
this->connection_callback_.call(true);
|
||||
}
|
||||
|
||||
void USBClient::on_opened(uint8_t addr) {
|
||||
@@ -437,6 +497,10 @@ TransferRequest *USBClient::get_trq_() {
|
||||
}
|
||||
|
||||
void USBClient::disconnect() {
|
||||
// Also reached for a device this client opened and then declined, or lost before it was
|
||||
// ready; neither was reported as connected, so neither is reported as removed
|
||||
const bool was_reported = this->connection_reported_;
|
||||
this->connection_reported_ = false;
|
||||
this->on_disconnected();
|
||||
auto err = usb_host_device_close(this->handle_, this->device_handle_);
|
||||
if (err != ESP_OK) {
|
||||
@@ -445,6 +509,9 @@ void USBClient::disconnect() {
|
||||
this->state_ = USB_CLIENT_INIT;
|
||||
this->device_handle_ = nullptr;
|
||||
this->device_addr_ = -1;
|
||||
if (was_reported) {
|
||||
this->connection_callback_.call(false);
|
||||
}
|
||||
}
|
||||
|
||||
// THREAD CONTEXT: Called from main loop thread only
|
||||
|
||||
@@ -18,7 +18,7 @@ from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_TYPE,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.core import CORE, ID
|
||||
from esphome.cpp_types import Component
|
||||
from esphome.types import ConfigType
|
||||
|
||||
@@ -29,6 +29,15 @@ 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 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,
|
||||
|
||||
@@ -21,8 +21,6 @@ static optional<CdcEps> get_cdc(const usb_config_desc_t *config_desc, uint8_t in
|
||||
int conf_offset, ep_offset;
|
||||
// look for an interface with an interrupt endpoint (notify), and one with two bulk endpoints (data in/out)
|
||||
CdcEps eps{};
|
||||
eps.bulk_interface_number = 0xFF;
|
||||
eps.interrupt_interface_number = 0xFF;
|
||||
for (;;) {
|
||||
const auto *intf_desc = usb_parse_interface_descriptor(config_desc, intf_idx++, 0, &conf_offset);
|
||||
if (!intf_desc) {
|
||||
@@ -664,6 +662,8 @@ bool USBUartComponent::run_config_machine_() {
|
||||
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.
|
||||
|
||||
@@ -33,10 +33,11 @@ struct CdcEps {
|
||||
const usb_ep_desc_t *notify_ep;
|
||||
const usb_ep_desc_t *in_ep;
|
||||
const usb_ep_desc_t *out_ep;
|
||||
uint8_t bulk_interface_number;
|
||||
// 0xFF marks a channel that was never matched to a CDC function on the device
|
||||
uint8_t bulk_interface_number{0xFF};
|
||||
// Also the wIndex target for CDC class requests (SET_LINE_CODING etc.), so it
|
||||
// must remain valid even when the interface itself is not claimed.
|
||||
uint8_t interrupt_interface_number;
|
||||
uint8_t interrupt_interface_number{0xFF};
|
||||
bool interrupt_interface_claimed{false};
|
||||
};
|
||||
|
||||
@@ -182,6 +183,13 @@ 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); }
|
||||
|
||||
/// USB interface number a host driver binds to for this channel: the communication
|
||||
/// interface of a CDC ACM function, otherwise the data interface.
|
||||
uint8_t get_interface_number() const {
|
||||
return this->cdc_dev_.interrupt_interface_number != 0xFF ? this->cdc_dev_.interrupt_interface_number
|
||||
: this->cdc_dev_.bulk_interface_number;
|
||||
}
|
||||
|
||||
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) {}
|
||||
@@ -267,6 +275,10 @@ class USBUartComponent : public usb_host::USBClient {
|
||||
// (e.g. CH34x chip detection). Same contract as config_step_(). Default: no steps.
|
||||
virtual bool config_device_step(uint8_t step, bool ok, const uint8_t *response) { return false; }
|
||||
|
||||
// The device is only usable once the config machine has applied every channel's line
|
||||
// settings, so the connected report waits for run_config_machine_() to finish the init
|
||||
bool reports_connection_itself() const override { return true; }
|
||||
|
||||
std::vector<USBUartChannelBase *> channels_{};
|
||||
|
||||
// Config state machine
|
||||
|
||||
@@ -467,6 +467,11 @@
|
||||
#define USE_USB_UART_CP210X
|
||||
#define USE_USB_UART_FT23XX
|
||||
#define USE_USB_UART_PL2303
|
||||
// USB identity on serial proxy ports needs the usb_host stack
|
||||
#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32S31) || defined(USE_ESP32_VARIANT_ESP32H4)
|
||||
#define USE_SERIAL_PROXY_USB_IDENTITY
|
||||
#endif
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 7)
|
||||
|
||||
@@ -49,6 +49,7 @@ class SerialProxy {
|
||||
uint32_t get_modem_pins() const { return 0; }
|
||||
uint32_t get_configured_modem_pins() const { return 0; }
|
||||
SerialProxyResult flush_port(api::APIConnection *api_connection) { return SerialProxyResult::SERIAL_PROXY_RESULT_OK; }
|
||||
void send_identity(api::APIConnection *api_connection) {}
|
||||
|
||||
protected:
|
||||
uint32_t instance_index_{0};
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# A hardware UART port alongside the USB one, so the USB identity define is exercised on a
|
||||
# port that has no USB channel
|
||||
packages:
|
||||
uart: !include ../../test_build_components/common/uart/esp32-idf.yaml
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
api:
|
||||
|
||||
usb_host:
|
||||
|
||||
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
|
||||
port_type: USB_SERIAL
|
||||
- id: serial_proxy_hw
|
||||
uart_id: uart_bus
|
||||
name: Hardware Serial Port
|
||||
port_type: TTL
|
||||
Reference in New Issue
Block a user