mirror of
https://github.com/esphome/esphome.git
synced 2026-09-11 15:27:33 +00:00
Merge remote-tracking branch 'puddly/20260218-zigbee-proxy' into 20260218-zigbee-proxy
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,10 +2904,11 @@ 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;
|
||||
string interface_description = 11; // iInterface string of that interface; empty when the device has none
|
||||
}
|
||||
|
||||
// ==================== BLUETOOTH CONNECTION PARAMS ====================
|
||||
|
||||
@@ -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));
|
||||
@@ -4288,9 +4288,10 @@ uint8_t *SerialProxyGetUsbInfoResponse::encode(ProtoWriteBuffer &buffer PROTO_EN
|
||||
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
|
||||
|
||||
@@ -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;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 60;
|
||||
#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{};
|
||||
@@ -3457,6 +3457,7 @@ class SerialProxyGetUsbInfoResponse 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
|
||||
|
||||
@@ -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);
|
||||
@@ -2842,6 +2842,7 @@ const char *SerialProxyGetUsbInfoResponse::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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -194,6 +194,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
|
||||
@@ -205,11 +212,6 @@ SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uin
|
||||
ESP_LOGW(TAG, "Invalid parity: %u (must be 0-2)", parity);
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_INVALID_ARGUMENT;
|
||||
}
|
||||
if (flow_control) {
|
||||
ESP_LOGW(TAG, "Hardware flow control requested but is not yet supported");
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
// Skip a no-op reconfigure. Clients routinely re-send identical settings on every
|
||||
// port open, and on a USB UART each apply is a CDC SET_LINE_CODING control transfer.
|
||||
// Some bridges watch line-coding changes as a signalling channel (a magic baud
|
||||
@@ -220,7 +222,8 @@ SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uin
|
||||
uart::UART_CONFIG_PARITY_ODD,
|
||||
};
|
||||
if (uart_comp->get_baud_rate() == baudrate && uart_comp->get_stop_bits() == stop_bits &&
|
||||
uart_comp->get_data_bits() == data_size && uart_comp->get_parity() == PARITY_MAP[parity]) {
|
||||
uart_comp->get_data_bits() == data_size && uart_comp->get_parity() == PARITY_MAP[parity] &&
|
||||
uart_comp->get_flow_control() == flow_control) {
|
||||
ESP_LOGV(TAG, "Settings unchanged, skipping reconfigure [%" PRIu32 "]", this->instance_index_);
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
|
||||
}
|
||||
@@ -229,6 +232,7 @@ SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uin
|
||||
uart_comp->set_baud_rate(baudrate);
|
||||
uart_comp->set_stop_bits(stop_bits);
|
||||
uart_comp->set_data_bits(data_size);
|
||||
uart_comp->set_flow_control(flow_control);
|
||||
|
||||
uart_comp->set_parity(PARITY_MAP[parity]);
|
||||
|
||||
@@ -338,26 +342,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
|
||||
|
||||
@@ -123,7 +128,7 @@ class SerialProxy final : public uart::UARTDevice, public Component {
|
||||
/// Configure UART parameters and apply them
|
||||
/// @param api_connection The API connection requesting the change
|
||||
/// @param baudrate Baud rate in bits per second
|
||||
/// @param flow_control True to enable hardware flow control
|
||||
/// @param flow_control True to request hardware flow control
|
||||
/// @param parity Parity setting (0=none, 1=even, 2=odd)
|
||||
/// @param stop_bits Number of stop bits (1 or 2)
|
||||
/// @param data_size Number of data bits (5-8)
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -166,6 +166,14 @@ class UARTComponent {
|
||||
// @return Baud rate in bits per second.
|
||||
uint32_t get_baud_rate() const { return baud_rate_; }
|
||||
|
||||
// Requests hardware (RTS/CTS) flow control.
|
||||
// @param flow_control True to request hardware flow control.
|
||||
void set_flow_control(bool flow_control) { this->flow_control_ = flow_control; }
|
||||
|
||||
// Gets whether hardware flow control was requested.
|
||||
// @return True when hardware flow control was requested.
|
||||
bool get_flow_control() const { return this->flow_control_; }
|
||||
|
||||
#if defined(USE_ESP8266) || defined(USE_ESP32)
|
||||
/**
|
||||
* Load the UART settings.
|
||||
@@ -213,6 +221,7 @@ class UARTComponent {
|
||||
uint8_t stop_bits_{0};
|
||||
uint8_t data_bits_{0};
|
||||
UARTParityOptions parity_{UART_CONFIG_PARITY_NONE};
|
||||
bool flow_control_{false};
|
||||
#ifdef USE_UART_DEBUGGER
|
||||
CallbackManager<void(UARTDirection, uint8_t)> debug_callback_{};
|
||||
#endif
|
||||
|
||||
@@ -68,13 +68,11 @@ def validate_usb_clients(configs: list[ConfigType]) -> list[ConfigType]:
|
||||
for index, first in enumerate(configs):
|
||||
# Ensure matching logic does not overlap between entries
|
||||
for second in configs[index + 1 :]:
|
||||
if (
|
||||
not (first[CONF_VID] == 0 and first[CONF_PID] == 0)
|
||||
and not (second[CONF_VID] == 0 and second[CONF_PID] == 0)
|
||||
and (
|
||||
first[CONF_VID] != second[CONF_VID]
|
||||
or first[CONF_PID] != second[CONF_PID]
|
||||
)
|
||||
# A zero VID or PID is a wildcard for that field, so only a differing non-zero
|
||||
# value separates two entries
|
||||
if not all(
|
||||
first[key] == 0 or second[key] == 0 or first[key] == second[key]
|
||||
for key in (CONF_VID, CONF_PID)
|
||||
):
|
||||
continue
|
||||
|
||||
|
||||
@@ -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,11 +168,28 @@ 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; }
|
||||
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,11 +210,20 @@ 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);
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -199,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};
|
||||
@@ -207,11 +244,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:
|
||||
|
||||
@@ -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);
|
||||
@@ -355,12 +421,10 @@ void USBClient::handle_open_state_() {
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Device descriptor: vid %X pid %X", desc->idVendor, desc->idProduct);
|
||||
if (desc->idVendor != this->vid_ || desc->idProduct != this->pid_) {
|
||||
if (this->vid_ != 0 || this->pid_ != 0) {
|
||||
ESP_LOGD(TAG, "Not our device, closing");
|
||||
this->disconnect();
|
||||
return;
|
||||
}
|
||||
if ((this->vid_ != 0 && desc->idVendor != this->vid_) || (this->pid_ != 0 && desc->idProduct != this->pid_)) {
|
||||
ESP_LOGD(TAG, "Not our device, closing");
|
||||
this->disconnect();
|
||||
return;
|
||||
}
|
||||
usb_device_info_t dev_info;
|
||||
err = usb_host_device_info(this->device_handle_, &dev_info);
|
||||
@@ -399,6 +463,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) {
|
||||
@@ -469,6 +545,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) {
|
||||
@@ -477,6 +557,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
|
||||
|
||||
@@ -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;
|
||||
@@ -481,6 +484,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;
|
||||
@@ -559,11 +563,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);
|
||||
@@ -597,6 +628,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))
|
||||
@@ -627,6 +666,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,
|
||||
@@ -650,11 +699,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.
|
||||
|
||||
@@ -37,6 +37,9 @@ struct CdcEps {
|
||||
// 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;
|
||||
// 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;
|
||||
bool interrupt_interface_claimed{false};
|
||||
};
|
||||
|
||||
@@ -167,14 +170,26 @@ 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;
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -244,6 +259,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
|
||||
@@ -255,6 +273,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
|
||||
@@ -269,6 +291,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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -70,6 +70,36 @@ from esphome.types import ConfigType
|
||||
},
|
||||
id="different_manufacturer",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"id": "a",
|
||||
"vid": 0x303A,
|
||||
"pid": 0,
|
||||
"manufacturer": "Nabu Casa",
|
||||
"product": "ZBT-2",
|
||||
},
|
||||
{
|
||||
"id": "b",
|
||||
"vid": 0x303A,
|
||||
"pid": 0x4001,
|
||||
"manufacturer": "Nabu Casa",
|
||||
"product": "ZWA-2",
|
||||
},
|
||||
id="wildcard_pid_separated_by_product",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"id": "a",
|
||||
"vid": 0,
|
||||
"pid": 0x4001,
|
||||
},
|
||||
{
|
||||
"id": "b",
|
||||
"vid": 0x303A,
|
||||
"pid": 0x4002,
|
||||
},
|
||||
id="wildcard_vid_different_pid",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_disjoint_clients_are_accepted(first: ConfigType, second: ConfigType) -> None:
|
||||
@@ -140,6 +170,34 @@ def test_disjoint_clients_are_accepted(first: ConfigType, second: ConfigType) ->
|
||||
},
|
||||
id="zero_ids_match_every_device",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"id": "a",
|
||||
"vid": 0x303A,
|
||||
"pid": 0,
|
||||
},
|
||||
{
|
||||
"id": "b",
|
||||
"vid": 0x303A,
|
||||
"pid": 0x4001,
|
||||
"manufacturer": "Nabu Casa",
|
||||
"product": "ZBT-2",
|
||||
},
|
||||
id="wildcard_pid_shadows_filtered",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"id": "a",
|
||||
"vid": 0,
|
||||
"pid": 0x4001,
|
||||
},
|
||||
{
|
||||
"id": "b",
|
||||
"vid": 0x303A,
|
||||
"pid": 0,
|
||||
},
|
||||
id="wildcards_on_different_fields",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_overlapping_clients_are_rejected(
|
||||
|
||||
Reference in New Issue
Block a user