mirror of
https://github.com/esphome/esphome.git
synced 2026-09-11 07:17:33 +00:00
[usb_host] Unsolicited USB info notifications
[usb_host] Interface number
This commit is contained in:
@@ -2886,7 +2886,14 @@ message SerialProxyGetUsbInfoRequest {
|
||||
|
||||
// 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 {
|
||||
//
|
||||
// Sent in reply to SerialProxyGetUsbInfoRequest, and also unsolicited to every connected
|
||||
// client whenever a USB device is attached to or removed from a USB_SERIAL port. A client
|
||||
// therefore subscribes to this message before querying the current state, so the
|
||||
// connect-time query and later hotplug events are handled by one path. The port's
|
||||
// subscription is untouched by a hotplug: the session belongs to the client, which decides
|
||||
// whether to keep it.
|
||||
message SerialProxyUsbInfo {
|
||||
option (id) = 154;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_SERIAL_PROXY";
|
||||
@@ -2897,7 +2904,7 @@ message SerialProxyGetUsbInfoResponse {
|
||||
uint32 vendor_id = 4;
|
||||
uint32 product_id = 5;
|
||||
uint32 bcd_device = 6;
|
||||
uint32 interface_number = 7; // Channel index on multi-port bridges
|
||||
uint32 interface_number = 7; // bInterfaceNumber a host driver binds to, as Linux reports it
|
||||
string manufacturer = 8;
|
||||
string product = 9;
|
||||
string serial_number = 10;
|
||||
|
||||
@@ -1647,14 +1647,14 @@ 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{};
|
||||
SerialProxyUsbInfo 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
|
||||
// The message'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
|
||||
|
||||
@@ -4276,7 +4276,7 @@ bool SerialProxyGetUsbInfoRequest::decode_varint(uint32_t field_id, proto_varint
|
||||
}
|
||||
return true;
|
||||
}
|
||||
uint8_t *SerialProxyGetUsbInfoResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *SerialProxyUsbInfo::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));
|
||||
|
||||
@@ -3440,12 +3440,12 @@ class SerialProxyGetUsbInfoRequest final : public ProtoDecodableMessage {
|
||||
protected:
|
||||
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
|
||||
};
|
||||
class SerialProxyGetUsbInfoResponse final : public ProtoMessage {
|
||||
class SerialProxyUsbInfo 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"); }
|
||||
const LogString *message_name() const override { return LOG_STR("serial_proxy_usb_info"); }
|
||||
#endif
|
||||
uint32_t instance{0};
|
||||
enums::SerialProxyStatus status{};
|
||||
|
||||
@@ -2830,8 +2830,8 @@ const char *SerialProxyGetUsbInfoRequest::dump_to(DumpBuffer &out) const {
|
||||
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"));
|
||||
const char *SerialProxyUsbInfo::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyUsbInfo"));
|
||||
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);
|
||||
|
||||
@@ -404,6 +404,18 @@ void APIServer::on_zwave_proxy_request(const ZWaveProxyRequest &msg) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY_USB_INFO
|
||||
void APIServer::send_serial_proxy_usb_info(const SerialProxyUsbInfo &msg) {
|
||||
// Unsolicited: a hotplug is rare and the message small, so every client hears of it
|
||||
// rather than maintaining a subscription for the one client there normally is
|
||||
for (auto &c : this->active_clients()) {
|
||||
if (!c->send_message(msg)) {
|
||||
API_LOG_MSG_DROPPED(TAG, "USB info notification");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
|
||||
void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_id, uint32_t key,
|
||||
const std::vector<int32_t> *timings) {
|
||||
|
||||
@@ -189,6 +189,10 @@ class APIServer final : public Component,
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
void on_zwave_proxy_request(const ZWaveProxyRequest &msg);
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY_USB_INFO
|
||||
/// Tell every client that the USB device behind a serial proxy port changed
|
||||
void send_serial_proxy_usb_info(const SerialProxyUsbInfo &msg);
|
||||
#endif
|
||||
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
|
||||
void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector<int32_t> *timings);
|
||||
#endif
|
||||
|
||||
@@ -34,6 +34,13 @@ 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_INFO
|
||||
// The define is global, so a hardware UART port in the same config also gets here
|
||||
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
|
||||
@@ -338,26 +345,54 @@ 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 {
|
||||
#ifdef USE_SERIAL_PROXY_USB_INFO
|
||||
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_SERIAL_PROXY_TAP
|
||||
// Before telling clients, so a tap never acknowledges a frame from the old device after
|
||||
// a client has been told it is gone. The subscriber and the mode stay: both belong to
|
||||
// the client's session, and only the client knows whether that session is over.
|
||||
if (!connected && this->tap_ != nullptr) {
|
||||
this->tap_->on_device_disconnected();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API
|
||||
if (api::global_api_server == nullptr) {
|
||||
return;
|
||||
}
|
||||
// The message's strings are views into this buffer, which outlives the send below
|
||||
usb_host::UsbDeviceInfo info;
|
||||
api::SerialProxyUsbInfo msg{};
|
||||
msg.instance = this->instance_index_;
|
||||
this->get_usb_info(info, msg);
|
||||
api::global_api_server->send_serial_proxy_usb_info(msg);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_API
|
||||
void SerialProxy::get_usb_info(usb_host::UsbDeviceInfo &info, api::SerialProxyUsbInfo &msg) const {
|
||||
if (this->usb_channel_ == nullptr) {
|
||||
resp.status = api::enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED;
|
||||
msg.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);
|
||||
msg.connected = true;
|
||||
msg.vendor_id = info.vendor_id;
|
||||
msg.product_id = info.product_id;
|
||||
msg.bcd_device = info.bcd_device;
|
||||
msg.interface_number = this->usb_channel_->get_interface_number();
|
||||
msg.manufacturer = StringRef(info.manufacturer);
|
||||
msg.product = StringRef(info.product);
|
||||
msg.serial_number = StringRef(info.serial_number);
|
||||
// Lives in the channel for as long as the device is attached
|
||||
msg.interface_description = StringRef(this->usb_channel_->get_interface_string());
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
uint32_t SerialProxy::get_modem_pins() const {
|
||||
return (this->rts_state_ ? static_cast<uint32_t>(SERIAL_PROXY_LINE_STATE_FLAG_RTS) : 0u) |
|
||||
|
||||
@@ -89,6 +89,11 @@ class SerialProxyTap {
|
||||
/// else with the device -- reflash it, most likely -- so anything the tap believes about
|
||||
/// it should be treated as suspect.
|
||||
virtual void on_protocol_disabled() = 0;
|
||||
|
||||
/// The device behind the port went away -- unplugged, or its power was cut. Whatever
|
||||
/// session the tap had observed ended with it; the device that appears next starts from
|
||||
/// scratch and may not even be the same one. Only a USB UART can report this.
|
||||
virtual void on_device_disconnected() = 0;
|
||||
};
|
||||
#endif
|
||||
|
||||
@@ -169,9 +174,9 @@ class SerialProxy final : public uart::UARTDevice, public Component {
|
||||
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;
|
||||
/// Fill a USB info message for this port. The message's strings are views into info,
|
||||
/// so info must outlive the send.
|
||||
void get_usb_info(usb_host::UsbDeviceInfo &info, api::SerialProxyUsbInfo &msg) const;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -238,6 +243,12 @@ class SerialProxy final : public uart::UARTDevice, public Component {
|
||||
bool tap_observing_() const;
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY_USB_INFO
|
||||
/// The USB device behind this port was attached or removed. Reports the port's new USB
|
||||
/// identity to every API client, and ends the tap's view of the old device.
|
||||
void on_usb_connection_changed_(bool connected);
|
||||
#endif
|
||||
|
||||
/// Instance index for identifying this proxy in API messages
|
||||
uint32_t instance_index_{0};
|
||||
|
||||
|
||||
@@ -167,6 +167,15 @@ class USBClient : public Component {
|
||||
void set_manufacturer_filter(const char *manufacturer) { this->manufacturer_filter_ = manufacturer; }
|
||||
void set_product_filter(const char *product) { this->product_filter_ = product; }
|
||||
|
||||
/// Register a callback for the device this client claims being connected (true) or
|
||||
/// removed (false). Fires only for a device that passed every filter and was fully
|
||||
/// opened, so a device another client claims is never reported. Called from the main
|
||||
/// loop: connected once the device is ready to use (a subclass may hold this back until
|
||||
/// its own setup of the device has finished), removed after on_disconnected() has run.
|
||||
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;
|
||||
@@ -187,6 +196,13 @@ class USBClient : public Component {
|
||||
|
||||
/// Whether the device's descriptor strings satisfy every filter that is set.
|
||||
bool descriptor_strings_match_(const usb_device_info_t &dev_info) const;
|
||||
|
||||
/// 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);
|
||||
@@ -207,11 +223,15 @@ class USBClient : public Component {
|
||||
// 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)
|
||||
std::atomic<trq_bitmask_t> trq_in_use_;
|
||||
LazyCallbackManager<void(bool)> connection_callback_;
|
||||
// Descriptor strings a device must report to be claimed; nullptr means no constraint
|
||||
const char *manufacturer_filter_{nullptr};
|
||||
const char *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:
|
||||
|
||||
@@ -397,6 +397,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) {
|
||||
@@ -467,6 +479,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) {
|
||||
@@ -475,6 +491,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
|
||||
|
||||
@@ -164,9 +164,16 @@ 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)
|
||||
/// Channel index on the bridge
|
||||
uint8_t get_index() const { return this->index_; }
|
||||
|
||||
/// 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) {}
|
||||
@@ -252,6 +259,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
|
||||
|
||||
@@ -56,6 +56,16 @@ void ZigbeeProxyTap::on_protocol_disabled() {
|
||||
this->detector_.reset();
|
||||
}
|
||||
|
||||
void ZigbeeProxyTap::on_device_disconnected() {
|
||||
// The ASH session died with the NCP's power. Staying armed would acknowledge frames from
|
||||
// whatever boots next, before its own RSTACK has proven it speaks ASH at all.
|
||||
if (this->was_armed_) {
|
||||
ESP_LOGD(TAG, "Device removed, no longer acknowledging frames");
|
||||
this->was_armed_ = false;
|
||||
}
|
||||
this->detector_.reset();
|
||||
}
|
||||
|
||||
} // namespace esphome::zigbee_proxy_tap
|
||||
|
||||
#endif // USE_ZIGBEE_PROXY_TAP
|
||||
|
||||
@@ -31,6 +31,7 @@ class ZigbeeProxyTap : public serial_proxy::SerialProxyTap, public Component {
|
||||
// there is nothing to do and the port need not be read.
|
||||
bool tap_needs_port() const override { return false; }
|
||||
void on_protocol_disabled() override;
|
||||
void on_device_disconnected() override;
|
||||
|
||||
protected:
|
||||
// The port this component observes. Owns the UART and the bytes; every write we make
|
||||
|
||||
@@ -57,6 +57,16 @@ void ZWaveProxyTap::on_protocol_disabled() {
|
||||
this->detector_.reset();
|
||||
}
|
||||
|
||||
void ZWaveProxyTap::on_device_disconnected() {
|
||||
// The controller lost power, so the exchange we saw belongs to a session that no longer
|
||||
// exists. Whatever appears next has to prove itself again.
|
||||
if (this->was_armed_) {
|
||||
ESP_LOGD(TAG, "Device removed, no longer acknowledging frames");
|
||||
this->was_armed_ = false;
|
||||
}
|
||||
this->detector_.reset();
|
||||
}
|
||||
|
||||
} // namespace esphome::zwave_proxy_tap
|
||||
|
||||
#endif // USE_ZWAVE_PROXY_TAP
|
||||
|
||||
@@ -34,6 +34,7 @@ class ZWaveProxyTap : public serial_proxy::SerialProxyTap, public Component {
|
||||
// there is nothing to do and the port need not be read.
|
||||
bool tap_needs_port() const override { return false; }
|
||||
void on_protocol_disabled() override;
|
||||
void on_device_disconnected() override;
|
||||
|
||||
protected:
|
||||
// The port this component observes. Owns the UART and the bytes; every write we make
|
||||
|
||||
Reference in New Issue
Block a user