mirror of
https://github.com/esphome/esphome.git
synced 2026-10-01 00:40:21 +00:00
Have zigbee proxying piggyback off of serial proxying?
This commit is contained in:
@@ -69,7 +69,6 @@ service APIConnection {
|
||||
rpc zwave_proxy_frame(ZWaveProxyFrame) returns (void) {}
|
||||
rpc zwave_proxy_request(ZWaveProxyRequest) returns (void) {}
|
||||
|
||||
rpc zigbee_proxy_frame(ZigbeeProxyFrame) returns (void) {}
|
||||
rpc zigbee_proxy_request(ZigbeeProxyRequest) returns (void) {}
|
||||
|
||||
rpc infrared_rf_transmit_raw_timings(InfraredRFTransmitRawTimingsRequest) returns (void) {}
|
||||
@@ -79,6 +78,7 @@ service APIConnection {
|
||||
rpc serial_proxy_set_modem_pins(SerialProxySetModemPinsRequest) returns (void) {}
|
||||
rpc serial_proxy_get_modem_pins(SerialProxyGetModemPinsRequest) returns (void) {}
|
||||
rpc serial_proxy_request(SerialProxyRequest) returns (void) {}
|
||||
rpc serial_proxy_set_mode(SerialProxySetModeRequest) returns (void) {}
|
||||
}
|
||||
|
||||
|
||||
@@ -2738,6 +2738,23 @@ message SerialProxyRequestResponse {
|
||||
string error_message = 4; // Additional detail on failure (optional)
|
||||
}
|
||||
|
||||
// How a port treats the bytes passing through it. RAW is a plain byte pipe; EZSP_ASH lets
|
||||
// a protocol-aware tap acknowledge NCP frames and read network metadata. A client that is
|
||||
// about to flash firmware selects RAW first, which definitively disables that injection.
|
||||
enum SerialProxyMode {
|
||||
SERIAL_PROXY_MODE_RAW = 0;
|
||||
SERIAL_PROXY_MODE_EZSP_ASH = 1;
|
||||
}
|
||||
|
||||
message SerialProxySetModeRequest {
|
||||
option (id) = 151;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
option (ifdef) = "USE_SERIAL_PROXY";
|
||||
|
||||
uint32 instance = 1;
|
||||
SerialProxyMode mode = 2;
|
||||
}
|
||||
|
||||
// ==================== BLUETOOTH CONNECTION PARAMS ====================
|
||||
message BluetoothSetConnectionParamsRequest {
|
||||
option (id) = 145;
|
||||
@@ -2762,19 +2779,8 @@ message BluetoothSetConnectionParamsResponse {
|
||||
|
||||
// ==================== ZIGBEE ====================
|
||||
|
||||
message ZigbeeProxyFrame {
|
||||
option (id) = 149;
|
||||
option (source) = SOURCE_BOTH;
|
||||
option (ifdef) = "USE_ZIGBEE_PROXY";
|
||||
option (no_delay) = true;
|
||||
|
||||
bytes data = 1;
|
||||
}
|
||||
|
||||
enum ZigbeeProxyRequestType {
|
||||
ZIGBEE_PROXY_REQUEST_TYPE_SUBSCRIBE = 0;
|
||||
ZIGBEE_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1;
|
||||
ZIGBEE_PROXY_REQUEST_TYPE_NETWORK_INFO = 2;
|
||||
ZIGBEE_PROXY_REQUEST_TYPE_NETWORK_INFO = 0;
|
||||
}
|
||||
|
||||
message ZigbeeProxyRequest {
|
||||
|
||||
@@ -192,11 +192,6 @@ APIConnection::~APIConnection() {
|
||||
zwave_proxy::global_zwave_proxy->zwave_proxy_request(this, enums::ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
if (zigbee_proxy::global_zigbee_proxy != nullptr && zigbee_proxy::global_zigbee_proxy->get_api_connection() == this) {
|
||||
zigbee_proxy::global_zigbee_proxy->unsubscribe_api_connection(this);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
for (auto *proxy : App.get_serial_proxies()) {
|
||||
if (proxy->get_api_connection() == this) {
|
||||
@@ -1386,10 +1381,6 @@ void APIConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) {
|
||||
#endif
|
||||
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
void APIConnection::on_zigbee_proxy_frame(const ZigbeeProxyFrame &msg) {
|
||||
zigbee_proxy::global_zigbee_proxy->zigbee_proxy_frame(this, msg);
|
||||
}
|
||||
|
||||
void APIConnection::on_zigbee_proxy_request(const ZigbeeProxyRequest &msg) {
|
||||
zigbee_proxy::global_zigbee_proxy->zigbee_proxy_request(this, msg);
|
||||
}
|
||||
@@ -1633,6 +1624,15 @@ void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
|
||||
}
|
||||
}
|
||||
|
||||
void APIConnection::on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &msg) {
|
||||
auto &proxies = App.get_serial_proxies();
|
||||
if (msg.instance >= proxies.size()) {
|
||||
ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance);
|
||||
return;
|
||||
}
|
||||
proxies[msg.instance]->set_mode(this, msg.mode);
|
||||
}
|
||||
|
||||
void APIConnection::send_serial_proxy_data(const SerialProxyDataReceived &msg) { this->send_message(msg); }
|
||||
#endif
|
||||
|
||||
|
||||
@@ -219,9 +219,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
#endif
|
||||
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
void on_zigbee_proxy_frame(const ZigbeeProxyFrame &msg);
|
||||
void on_zigbee_proxy_request(const ZigbeeProxyRequest &msg);
|
||||
bool send_zigbee_proxy_frame(const ZigbeeProxyFrame &msg) { return this->send_message(msg); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
@@ -245,6 +243,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg);
|
||||
void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg);
|
||||
void on_serial_proxy_request(const SerialProxyRequest &msg);
|
||||
void on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &msg);
|
||||
void send_serial_proxy_data(const SerialProxyDataReceived &msg);
|
||||
#endif
|
||||
|
||||
|
||||
@@ -4156,6 +4156,19 @@ uint32_t SerialProxyRequestResponse::calculate_size() const {
|
||||
size += ProtoSize::calc_length(1, this->error_message.size());
|
||||
return size;
|
||||
}
|
||||
bool SerialProxySetModeRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
|
||||
switch (field_id) {
|
||||
case 1:
|
||||
this->instance = value;
|
||||
break;
|
||||
case 2:
|
||||
this->mode = static_cast<enums::SerialProxyMode>(value);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
bool BluetoothSetConnectionParamsRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
|
||||
@@ -4194,28 +4207,6 @@ uint32_t BluetoothSetConnectionParamsResponse::calculate_size() const {
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
bool ZigbeeProxyFrame::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 1: {
|
||||
this->data = value.data();
|
||||
this->data_len = value.size();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
uint8_t *ZigbeeProxyFrame::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 1, this->data, this->data_len);
|
||||
return pos;
|
||||
}
|
||||
uint32_t ZigbeeProxyFrame::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
size += ProtoSize::calc_length(1, this->data_len);
|
||||
return size;
|
||||
}
|
||||
bool ZigbeeProxyRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
|
||||
switch (field_id) {
|
||||
case 1:
|
||||
|
||||
@@ -351,12 +351,14 @@ enum SerialProxyStatus : uint32_t {
|
||||
SERIAL_PROXY_STATUS_TIMEOUT = 3,
|
||||
SERIAL_PROXY_STATUS_NOT_SUPPORTED = 4,
|
||||
};
|
||||
enum SerialProxyMode : uint32_t {
|
||||
SERIAL_PROXY_MODE_RAW = 0,
|
||||
SERIAL_PROXY_MODE_EZSP_ASH = 1,
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
enum ZigbeeProxyRequestType : uint32_t {
|
||||
ZIGBEE_PROXY_REQUEST_TYPE_SUBSCRIBE = 0,
|
||||
ZIGBEE_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1,
|
||||
ZIGBEE_PROXY_REQUEST_TYPE_NETWORK_INFO = 2,
|
||||
ZIGBEE_PROXY_REQUEST_TYPE_NETWORK_INFO = 0,
|
||||
};
|
||||
#endif
|
||||
|
||||
@@ -3302,6 +3304,22 @@ class SerialProxyRequestResponse final : public ProtoMessage {
|
||||
|
||||
protected:
|
||||
};
|
||||
class SerialProxySetModeRequest final : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 151;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 6;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("serial_proxy_set_mode_request"); }
|
||||
#endif
|
||||
uint32_t instance{0};
|
||||
enums::SerialProxyMode mode{};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
class BluetoothSetConnectionParamsRequest final : public ProtoDecodableMessage {
|
||||
@@ -3342,24 +3360,6 @@ class BluetoothSetConnectionParamsResponse final : public ProtoMessage {
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
class ZigbeeProxyFrame final : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 149;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 19;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("zigbee_proxy_frame"); }
|
||||
#endif
|
||||
const uint8_t *data{nullptr};
|
||||
uint16_t data_len{0};
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
|
||||
uint32_t calculate_size() const;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
|
||||
};
|
||||
class ZigbeeProxyRequest final : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 150;
|
||||
|
||||
@@ -856,14 +856,20 @@ template<> const char *proto_enum_to_string<enums::SerialProxyStatus>(enums::Ser
|
||||
return ESPHOME_PSTR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
template<> const char *proto_enum_to_string<enums::SerialProxyMode>(enums::SerialProxyMode value) {
|
||||
switch (value) {
|
||||
case enums::SERIAL_PROXY_MODE_RAW:
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_MODE_RAW");
|
||||
case enums::SERIAL_PROXY_MODE_EZSP_ASH:
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_MODE_EZSP_ASH");
|
||||
default:
|
||||
return ESPHOME_PSTR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
template<> const char *proto_enum_to_string<enums::ZigbeeProxyRequestType>(enums::ZigbeeProxyRequestType value) {
|
||||
switch (value) {
|
||||
case enums::ZIGBEE_PROXY_REQUEST_TYPE_SUBSCRIBE:
|
||||
return ESPHOME_PSTR("ZIGBEE_PROXY_REQUEST_TYPE_SUBSCRIBE");
|
||||
case enums::ZIGBEE_PROXY_REQUEST_TYPE_UNSUBSCRIBE:
|
||||
return ESPHOME_PSTR("ZIGBEE_PROXY_REQUEST_TYPE_UNSUBSCRIBE");
|
||||
case enums::ZIGBEE_PROXY_REQUEST_TYPE_NETWORK_INFO:
|
||||
return ESPHOME_PSTR("ZIGBEE_PROXY_REQUEST_TYPE_NETWORK_INFO");
|
||||
default:
|
||||
@@ -2734,6 +2740,12 @@ const char *SerialProxyRequestResponse::dump_to(DumpBuffer &out) const {
|
||||
dump_field(out, ESPHOME_PSTR("error_message"), this->error_message);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *SerialProxySetModeRequest::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxySetModeRequest"));
|
||||
dump_field(out, ESPHOME_PSTR("instance"), this->instance);
|
||||
dump_field(out, ESPHOME_PSTR("mode"), static_cast<enums::SerialProxyMode>(this->mode));
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
const char *BluetoothSetConnectionParamsRequest::dump_to(DumpBuffer &out) const {
|
||||
@@ -2753,11 +2765,6 @@ const char *BluetoothSetConnectionParamsResponse::dump_to(DumpBuffer &out) const
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
const char *ZigbeeProxyFrame::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("ZigbeeProxyFrame"));
|
||||
dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *ZigbeeProxyRequest::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("ZigbeeProxyRequest"));
|
||||
dump_field(out, ESPHOME_PSTR("type"), static_cast<enums::ZigbeeProxyRequestType>(this->type));
|
||||
|
||||
@@ -705,17 +705,6 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
case ZigbeeProxyFrame::MESSAGE_TYPE: {
|
||||
ZigbeeProxyFrame msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_zigbee_proxy_frame"), msg);
|
||||
#endif
|
||||
this->on_zigbee_proxy_frame(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
case ZigbeeProxyRequest::MESSAGE_TYPE: {
|
||||
ZigbeeProxyRequest msg;
|
||||
@@ -726,6 +715,17 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui
|
||||
this->on_zigbee_proxy_request(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
case SerialProxySetModeRequest::MESSAGE_TYPE: {
|
||||
SerialProxySetModeRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_serial_proxy_set_mode_request"), msg);
|
||||
#endif
|
||||
this->on_serial_proxy_set_mode_request(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -233,13 +233,13 @@ class APIServerConnectionBase {
|
||||
void on_serial_proxy_request(const SerialProxyRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
void on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &value){};
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
void on_zigbee_proxy_frame(const ZigbeeProxyFrame &value){};
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
void on_zigbee_proxy_request(const ZigbeeProxyRequest &value){};
|
||||
#endif
|
||||
|
||||
@@ -18,7 +18,7 @@ from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_NAME
|
||||
from esphome.const import CONF_ID, CONF_MODE, CONF_NAME
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.coroutine import CoroPriority
|
||||
|
||||
@@ -29,6 +29,7 @@ MULTI_CONF = True
|
||||
|
||||
serial_proxy_ns = cg.esphome_ns.namespace("serial_proxy")
|
||||
SerialProxy = serial_proxy_ns.class_("SerialProxy", cg.Component, uart.UARTDevice)
|
||||
SerialProxyTap = serial_proxy_ns.class_("SerialProxyTap")
|
||||
|
||||
api_enums_ns = cg.esphome_ns.namespace("api").namespace("enums")
|
||||
SerialProxyPortType = api_enums_ns.enum("SerialProxyPortType")
|
||||
@@ -38,6 +39,15 @@ SERIAL_PROXY_PORT_TYPES = {
|
||||
"RS485": SerialProxyPortType.SERIAL_PROXY_PORT_TYPE_RS485,
|
||||
}
|
||||
|
||||
SerialProxyMode = api_enums_ns.enum("SerialProxyMode")
|
||||
# The mode a port starts in. `raw` is a plain byte pipe; `ezsp_ash` lets a tap
|
||||
# acknowledge NCP frames and read network metadata off the wire. Clients may change
|
||||
# it at runtime, so this only decides what the device boots into.
|
||||
SERIAL_PROXY_MODES = {
|
||||
"RAW": SerialProxyMode.SERIAL_PROXY_MODE_RAW,
|
||||
"EZSP_ASH": SerialProxyMode.SERIAL_PROXY_MODE_EZSP_ASH,
|
||||
}
|
||||
|
||||
CONF_DTR_PIN = "dtr_pin"
|
||||
CONF_PORT_TYPE = "port_type"
|
||||
CONF_RTS_PIN = "rts_pin"
|
||||
@@ -62,6 +72,9 @@ CONFIG_SCHEMA = (
|
||||
cv.GenerateID(): cv.declare_id(SerialProxy),
|
||||
cv.Required(CONF_NAME): cv.string_strict,
|
||||
cv.Required(CONF_PORT_TYPE): cv.enum(SERIAL_PROXY_PORT_TYPES, upper=True),
|
||||
cv.Optional(CONF_MODE, default="RAW"): cv.enum(
|
||||
SERIAL_PROXY_MODES, upper=True
|
||||
),
|
||||
cv.Optional(CONF_RTS_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_DTR_PIN): pins.gpio_output_pin_schema,
|
||||
}
|
||||
@@ -86,6 +99,7 @@ async def to_code(config):
|
||||
cg.add(cg.App.register_serial_proxy(var))
|
||||
cg.add(var.set_name(config[CONF_NAME]))
|
||||
cg.add(var.set_port_type(config[CONF_PORT_TYPE]))
|
||||
cg.add(var.set_mode(config[CONF_MODE]))
|
||||
cg.add_define("USE_SERIAL_PROXY")
|
||||
|
||||
# Track instance count for the FINAL priority define
|
||||
|
||||
@@ -29,20 +29,41 @@ void SerialProxy::setup() {
|
||||
#ifdef USE_API
|
||||
// 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_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
|
||||
// the loop enabled is what lets that finish; without it the tap would stall until a
|
||||
// client happened to subscribe.
|
||||
if (this->tap_ != nullptr && this->tap_->tap_needs_port()) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
// No subscriber at startup; disable loop until a client subscribes
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
void SerialProxy::reset_mode_() {
|
||||
// The mode belongs to a session, not to the port. Carrying a departed client's choice
|
||||
// over to the next one would inject protocol bytes into a stream that never asked for
|
||||
// them -- a firmware upload, or any client built before this request existed and so
|
||||
// unable to turn it off. Guessing RAW is the safe direction: a client that wanted
|
||||
// protocol handling and did not ask for it merely sends its own acknowledgements.
|
||||
if (this->mode_ == api::enums::SERIAL_PROXY_MODE_RAW) {
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Session ended, returning serial proxy [%" PRIu32 "] to RAW mode", this->instance_index_);
|
||||
this->mode_ = api::enums::SERIAL_PROXY_MODE_RAW;
|
||||
}
|
||||
|
||||
void SerialProxy::loop() {
|
||||
#ifdef USE_API
|
||||
// Detect subscriber disconnect
|
||||
if (this->api_connection_ != nullptr &&
|
||||
(this->api_connection_->is_marked_for_removal() || !this->api_connection_->is_connection_setup() ||
|
||||
!api_is_connected())) {
|
||||
if (this->api_connection_ != nullptr && (this->api_connection_->is_marked_for_removal() ||
|
||||
!this->api_connection_->is_connection_setup() || !api_is_connected())) {
|
||||
ESP_LOGW(TAG, "Subscriber disconnected");
|
||||
this->api_connection_ = nullptr;
|
||||
this->parent_->release(this);
|
||||
this->reset_mode_();
|
||||
}
|
||||
|
||||
// With no subscriber there is normally nothing to do, but a tap may still need the port
|
||||
@@ -80,7 +101,7 @@ void __attribute__((noinline)) SerialProxy::read_and_send_(size_t available) {
|
||||
#ifdef USE_SERIAL_PROXY_TAP
|
||||
// Before forwarding, so a tap that answers the device (an acknowledgement, say) is not
|
||||
// waiting on the network round trip to a subscriber that may not even exist.
|
||||
if (this->tap_ != nullptr) {
|
||||
if (this->tap_observing_()) {
|
||||
this->tap_->on_device_rx(buffer, to_read);
|
||||
}
|
||||
#endif
|
||||
@@ -94,6 +115,24 @@ void __attribute__((noinline)) SerialProxy::read_and_send_(size_t available) {
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY_TAP
|
||||
|
||||
bool SerialProxy::tap_observing_() const {
|
||||
if (this->tap_ == nullptr) {
|
||||
return false;
|
||||
}
|
||||
// A tap that needs the port is mid-protocol-work of its own -- the boot-time handshake
|
||||
// with the device, which runs before any client has connected and so before anyone could
|
||||
// have chosen a mode. Withholding bytes from it there would strand it, so it is served
|
||||
// regardless of mode.
|
||||
if (this->tap_->tap_needs_port()) {
|
||||
return true;
|
||||
}
|
||||
// Otherwise the mode decides. RAW must be inert: a client that flips to RAW before
|
||||
// flashing firmware is entitled to a byte pipe with nothing injecting protocol bytes
|
||||
// into it, and "the tap turned out not to recognise the stream" is not good enough.
|
||||
return this->mode_ == api::enums::SERIAL_PROXY_MODE_EZSP_ASH;
|
||||
}
|
||||
|
||||
void SerialProxy::tap_pump() {
|
||||
#ifdef USE_API
|
||||
const size_t available = this->available();
|
||||
@@ -109,12 +148,14 @@ void SerialProxy::dump_config() {
|
||||
"Serial Proxy [%" PRIu32 "]:\n"
|
||||
" Name: %s\n"
|
||||
" Port Type: %s\n"
|
||||
" Mode: %s\n"
|
||||
" 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 ? "RS485"
|
||||
: this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS232 ? "RS232"
|
||||
: "TTL",
|
||||
this->mode_ == api::enums::SERIAL_PROXY_MODE_EZSP_ASH ? "EZSP_ASH" : "RAW",
|
||||
this->rts_pin_ != nullptr ? "configured" : "not configured",
|
||||
this->dtr_pin_ != nullptr ? "configured" : "not configured");
|
||||
}
|
||||
@@ -188,6 +229,29 @@ void SerialProxy::configure(api::APIConnection *api_connection, uint32_t baudrat
|
||||
}
|
||||
}
|
||||
|
||||
void SerialProxy::set_mode(api::APIConnection *api_connection, api::enums::SerialProxyMode mode) {
|
||||
#ifdef USE_API
|
||||
if (this->port_claimed_by_other_(api_connection)) {
|
||||
ESP_LOGW(TAG, "Ignoring mode request from client without port access [%" PRIu32 "]", this->instance_index_);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
ESP_LOGD(TAG, "Serial proxy [%" PRIu32 "] mode set to %s", this->instance_index_,
|
||||
mode == api::enums::SERIAL_PROXY_MODE_EZSP_ASH ? "EZSP_ASH" : "RAW");
|
||||
const bool leaving_protocol_mode =
|
||||
this->mode_ != api::enums::SERIAL_PROXY_MODE_RAW && mode == api::enums::SERIAL_PROXY_MODE_RAW;
|
||||
this->mode_ = mode;
|
||||
|
||||
#ifdef USE_SERIAL_PROXY_TAP
|
||||
// Only for an explicit client request, not for reset_mode_() at the end of a session:
|
||||
// an ordinary disconnect says nothing about the device, whereas a client deliberately
|
||||
// asking for raw bytes usually precedes changing what the device is.
|
||||
if (leaving_protocol_mode && this->tap_ != nullptr) {
|
||||
this->tap_->on_protocol_disabled();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void SerialProxy::write_from_client(api::APIConnection *api_connection, const uint8_t *data, size_t len) {
|
||||
#ifdef USE_API
|
||||
// Bytes from a client other than the live subscriber would interleave with the
|
||||
@@ -203,7 +267,7 @@ void SerialProxy::write_from_client(api::APIConnection *api_connection, const ui
|
||||
|
||||
#ifdef USE_SERIAL_PROXY_TAP
|
||||
// After the write, so the tap observes the same ordering the device does
|
||||
if (this->tap_ != nullptr) {
|
||||
if (this->tap_observing_()) {
|
||||
this->tap_->on_client_tx(data, len);
|
||||
}
|
||||
#endif
|
||||
@@ -264,10 +328,6 @@ void SerialProxy::serial_proxy_request(api::APIConnection *api_connection, api::
|
||||
ESP_LOGW(TAG, "Previous subscriber disconnected; taking over subscription");
|
||||
}
|
||||
this->api_connection_ = api_connection;
|
||||
// Take the UART unconditionally. Another device may be bound to it (a protocol
|
||||
// proxy over the same radio, say); raw serial access is the more explicit, more
|
||||
// destructive operation -- typically a firmware update -- so it wins.
|
||||
this->parent_->claim(this);
|
||||
this->enable_loop();
|
||||
ESP_LOGV(TAG, "API connection subscribed to serial proxy [%" PRIu32 "]", this->instance_index_);
|
||||
break;
|
||||
@@ -277,7 +337,7 @@ void SerialProxy::serial_proxy_request(api::APIConnection *api_connection, api::
|
||||
return;
|
||||
}
|
||||
this->api_connection_ = nullptr;
|
||||
this->parent_->release(this);
|
||||
this->reset_mode_();
|
||||
this->disable_loop();
|
||||
ESP_LOGV(TAG, "API connection unsubscribed from serial proxy [%" PRIu32 "]", this->instance_index_);
|
||||
break;
|
||||
|
||||
@@ -26,6 +26,7 @@ class APIConnection;
|
||||
namespace enums {
|
||||
enum SerialProxyPortType : uint32_t;
|
||||
enum SerialProxyRequestType : uint32_t;
|
||||
enum SerialProxyMode : uint32_t;
|
||||
} // namespace enums
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -61,6 +62,12 @@ class SerialProxyTap {
|
||||
/// True when the port must keep reading even with no subscriber attached, so a tap can
|
||||
/// do its own protocol work while nobody is listening.
|
||||
virtual bool tap_needs_port() const = 0;
|
||||
|
||||
/// A client explicitly turned protocol handling off for this port. Distinct from the
|
||||
/// automatic reset when a session ends: this one means a client intends to do something
|
||||
/// 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;
|
||||
};
|
||||
#endif
|
||||
|
||||
@@ -89,6 +96,15 @@ class SerialProxy final : public uart::UARTDevice, public Component {
|
||||
/// Get the port type
|
||||
api::enums::SerialProxyPortType get_port_type() const { return this->port_type_; }
|
||||
|
||||
/// Set the initial mode (from YAML configuration)
|
||||
void set_mode(api::enums::SerialProxyMode mode) { this->mode_ = mode; }
|
||||
|
||||
/// Get the current mode
|
||||
api::enums::SerialProxyMode get_mode() const { return this->mode_; }
|
||||
|
||||
/// Handle a mode change requested by an API client
|
||||
void set_mode(api::APIConnection *api_connection, api::enums::SerialProxyMode mode);
|
||||
|
||||
/// Configure UART parameters and apply them
|
||||
/// @param api_connection The API connection requesting the change
|
||||
/// @param baudrate Baud rate in bits per second
|
||||
@@ -139,6 +155,10 @@ class SerialProxy final : public uart::UARTDevice, public Component {
|
||||
/// must ask for it back.
|
||||
void tap_request_port() { this->enable_loop(); }
|
||||
|
||||
/// Whether the underlying device is present. On a USB UART this tracks enumeration, so
|
||||
/// a tap can notice the device being unplugged and plugged back in.
|
||||
bool is_device_connected() const { return this->parent_->is_connected(); }
|
||||
|
||||
/// Run one read-and-dispatch cycle immediately. Lets a tap make progress before the
|
||||
/// main loop is running -- during setup, for instance, while a component is still
|
||||
/// blocking on can_proceed().
|
||||
@@ -155,6 +175,15 @@ class SerialProxy final : public uart::UARTDevice, public Component {
|
||||
bool port_claimed_by_other_(api::APIConnection *api_connection) const;
|
||||
#endif
|
||||
|
||||
/// Return the port to RAW when a subscriber goes away, so the mode never outlives it.
|
||||
/// Not tap-gated: the mode is a client-visible property whether or not a tap acts on it.
|
||||
void reset_mode_();
|
||||
|
||||
#ifdef USE_SERIAL_PROXY_TAP
|
||||
/// True when the tap should be shown the traffic passing through this port
|
||||
bool tap_observing_() const;
|
||||
#endif
|
||||
|
||||
/// Instance index for identifying this proxy in API messages
|
||||
uint32_t instance_index_{0};
|
||||
|
||||
@@ -172,6 +201,9 @@ class SerialProxy final : public uart::UARTDevice, public Component {
|
||||
/// Port type
|
||||
api::enums::SerialProxyPortType port_type_{};
|
||||
|
||||
/// How the bytes passing through are treated; zero is SERIAL_PROXY_MODE_RAW
|
||||
api::enums::SerialProxyMode mode_{};
|
||||
|
||||
/// Optional GPIO pins for modem control
|
||||
GPIOPin *rts_pin_{nullptr};
|
||||
GPIOPin *dtr_pin_{nullptr};
|
||||
|
||||
@@ -166,26 +166,6 @@ class UARTComponent {
|
||||
// @return Baud rate in bits per second.
|
||||
uint32_t get_baud_rate() const { return baud_rate_; }
|
||||
|
||||
/** Exclusive access for devices that share one bus.
|
||||
*
|
||||
* Several UARTDevices may be bound to the same UART while only one may drive it at a
|
||||
* time -- a protocol-aware proxy and a raw serial proxy over the same radio, say. The
|
||||
* claim is advisory: each device must check `is_claimed_by_other()` before reading or
|
||||
* writing.
|
||||
*
|
||||
* Claiming is unconditional so a claimant is never blocked by a holder that went away
|
||||
* without releasing (a client whose connection died silently, for instance). Callers
|
||||
* that want to defer to a live holder must check before claiming.
|
||||
*/
|
||||
void claim(void *owner) { this->exclusive_owner_ = owner; }
|
||||
void release(void *owner) {
|
||||
if (this->exclusive_owner_ == owner)
|
||||
this->exclusive_owner_ = nullptr;
|
||||
}
|
||||
bool is_claimed_by_other(void *owner) const {
|
||||
return this->exclusive_owner_ != nullptr && this->exclusive_owner_ != owner;
|
||||
}
|
||||
|
||||
#if defined(USE_ESP8266) || defined(USE_ESP32)
|
||||
/**
|
||||
* Load the UART settings.
|
||||
@@ -230,7 +210,6 @@ class UARTComponent {
|
||||
size_t rx_full_threshold_{RX_FULL_THRESHOLD_UNSET};
|
||||
size_t rx_timeout_{0};
|
||||
uint32_t baud_rate_{0};
|
||||
void *exclusive_owner_{nullptr}; // see claim()
|
||||
uint8_t stop_bits_{0};
|
||||
uint8_t data_bits_{0};
|
||||
UARTParityOptions parity_{UART_CONFIG_PARITY_NONE};
|
||||
|
||||
@@ -1,34 +1,26 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import uart, usb_uart
|
||||
from esphome.components import serial_proxy
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BUFFER_SIZE,
|
||||
CONF_ID,
|
||||
CONF_POWER_SAVE_MODE,
|
||||
CONF_UART_ID,
|
||||
CONF_WIFI,
|
||||
)
|
||||
from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_POWER_SAVE_MODE, CONF_WIFI
|
||||
import esphome.final_validate as fv
|
||||
|
||||
CODEOWNERS = ["@kbx81"]
|
||||
DEPENDENCIES = ["api", "uart"]
|
||||
DEPENDENCIES = ["api", "serial_proxy"]
|
||||
|
||||
CONF_INITIAL_TIMEOUT = "initial_timeout"
|
||||
CONF_MIN_TIMEOUT = "min_timeout"
|
||||
CONF_MAX_TIMEOUT = "max_timeout"
|
||||
CONF_SERIAL_PROXY_ID = "serial_proxy_id"
|
||||
|
||||
# Default ACK timeout values calibrated for hardware UART (460800 baud, ~2-5 ms round-trip)
|
||||
_DEFAULT_HW_INITIAL_TIMEOUT = 1600
|
||||
_DEFAULT_HW_MIN_TIMEOUT = 400
|
||||
_DEFAULT_HW_MAX_TIMEOUT = 3200
|
||||
|
||||
# Optimized ACK timeout values for USB CDC ACM paths (~3-5 ms round-trip with RX callback)
|
||||
_DEFAULT_USB_INITIAL_TIMEOUT = 30
|
||||
_DEFAULT_USB_MIN_TIMEOUT = 15
|
||||
_DEFAULT_USB_MAX_TIMEOUT = 200
|
||||
# Default ACK timeout values for the boot-time metadata harvest
|
||||
_DEFAULT_INITIAL_TIMEOUT = 1600
|
||||
_DEFAULT_MIN_TIMEOUT = 400
|
||||
_DEFAULT_MAX_TIMEOUT = 3200
|
||||
|
||||
zigbee_proxy_ns = cg.esphome_ns.namespace("zigbee_proxy")
|
||||
ZigbeeProxy = zigbee_proxy_ns.class_("ZigbeeProxy", cg.Component, uart.UARTDevice)
|
||||
ZigbeeProxy = zigbee_proxy_ns.class_(
|
||||
"ZigbeeProxy", cg.Component, serial_proxy.SerialProxyTap
|
||||
)
|
||||
|
||||
|
||||
def final_validate(config):
|
||||
@@ -46,18 +38,23 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ZigbeeProxy),
|
||||
cv.Required(CONF_SERIAL_PROXY_ID): cv.use_id(serial_proxy.SerialProxy),
|
||||
cv.Optional(CONF_BUFFER_SIZE): cv.SplitDefault(
|
||||
cv.int_range(min=256, max=2048),
|
||||
esp8266=512,
|
||||
default=1024,
|
||||
),
|
||||
cv.Optional(CONF_INITIAL_TIMEOUT): cv.int_range(min=10, max=10000),
|
||||
cv.Optional(CONF_MIN_TIMEOUT): cv.int_range(min=10, max=5000),
|
||||
cv.Optional(CONF_MAX_TIMEOUT): cv.int_range(min=50, max=10000),
|
||||
cv.Optional(
|
||||
CONF_INITIAL_TIMEOUT, default=_DEFAULT_INITIAL_TIMEOUT
|
||||
): cv.int_range(min=10, max=10000),
|
||||
cv.Optional(CONF_MIN_TIMEOUT, default=_DEFAULT_MIN_TIMEOUT): cv.int_range(
|
||||
min=10, max=5000
|
||||
),
|
||||
cv.Optional(CONF_MAX_TIMEOUT, default=_DEFAULT_MAX_TIMEOUT): cv.int_range(
|
||||
min=50, max=10000
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(uart.UART_DEVICE_SCHEMA),
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = final_validate
|
||||
@@ -66,39 +63,18 @@ FINAL_VALIDATE_SCHEMA = final_validate
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
sp = await cg.get_variable(config[CONF_SERIAL_PROXY_ID])
|
||||
cg.add(var.set_serial_proxy(sp))
|
||||
|
||||
cg.add_define("USE_ZIGBEE_PROXY")
|
||||
# Compiles the tap interface into serial_proxy; without it the port is a plain byte pipe
|
||||
cg.add_define("USE_SERIAL_PROXY_TAP")
|
||||
|
||||
# Set buffer size via define for compile-time allocation
|
||||
if CONF_BUFFER_SIZE in config:
|
||||
cg.add_define("ZIGBEE_PROXY_BUFFER_SIZE", config[CONF_BUFFER_SIZE])
|
||||
|
||||
# A uart_id pointing at a USB UART channel is detected automatically: the
|
||||
# component then registers an RX callback for zero-wakeup-cycle data delivery
|
||||
# and selects USB-optimized ACK timeout defaults. Explicit timeout keys always
|
||||
# win. USB CDC ACM with the RX callback has ~3-5 ms round-trip latency;
|
||||
# hardware UART is similar (~2-5 ms). Different defaults are kept so that
|
||||
# future non-callback USB paths still get conservative starting values.
|
||||
is_usb = usb_uart.is_usb_uart_channel(config[CONF_UART_ID])
|
||||
if is_usb:
|
||||
cg.add_define("USE_ZIGBEE_PROXY_USB_UART")
|
||||
usb_ch = await cg.get_variable(config[CONF_UART_ID])
|
||||
cg.add(var.set_usb_uart_channel(usb_ch))
|
||||
|
||||
initial_timeout = config.get(
|
||||
CONF_INITIAL_TIMEOUT,
|
||||
_DEFAULT_USB_INITIAL_TIMEOUT if is_usb else _DEFAULT_HW_INITIAL_TIMEOUT,
|
||||
)
|
||||
min_timeout = config.get(
|
||||
CONF_MIN_TIMEOUT,
|
||||
_DEFAULT_USB_MIN_TIMEOUT if is_usb else _DEFAULT_HW_MIN_TIMEOUT,
|
||||
)
|
||||
max_timeout = config.get(
|
||||
CONF_MAX_TIMEOUT,
|
||||
_DEFAULT_USB_MAX_TIMEOUT if is_usb else _DEFAULT_HW_MAX_TIMEOUT,
|
||||
)
|
||||
|
||||
cg.add(var.set_initial_timeout(initial_timeout))
|
||||
cg.add(var.set_min_timeout(min_timeout))
|
||||
cg.add(var.set_max_timeout(max_timeout))
|
||||
cg.add(var.set_initial_timeout(config[CONF_INITIAL_TIMEOUT]))
|
||||
cg.add(var.set_min_timeout(config[CONF_MIN_TIMEOUT]))
|
||||
cg.add(var.set_max_timeout(config[CONF_MAX_TIMEOUT]))
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/util.h"
|
||||
#include "esphome/components/api/api_server.h"
|
||||
#include "ezsp_commands.h"
|
||||
|
||||
@@ -12,19 +10,18 @@
|
||||
#include "esphome/components/wifi/wifi_component.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_ZIGBEE_PROXY_USB_UART
|
||||
#include "esphome/components/usb_uart/usb_uart.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::zigbee_proxy {
|
||||
|
||||
static const char *const TAG = "zigbee_proxy";
|
||||
|
||||
static constexpr uint32_t BOOT_SEQUENCE_TIMEOUT_MS = 10000; // Overall boot-harvest timeout
|
||||
static constexpr uint32_t RECOVERY_RETRY_INTERVAL_MS = 30000; // Retry interval for a failed NCP link
|
||||
static constexpr uint32_t CLIENT_TX_RETRY_TIMEOUT_MS = 5000; // Give up on a backpressured client frame
|
||||
static constexpr size_t NETWORK_INFO_PAYLOAD_SIZE = 19; // ieee(8) + extended_pan(8) + pan_id(2) + channel(1)
|
||||
static constexpr size_t ZIGBEE_MAX_LOG_BYTES = 168; // Cap verbose hex dumps (168 * 3 = 504 byte buffer)
|
||||
// A freshly attached USB device answers its enumeration before its CDC endpoints will
|
||||
// actually carry bytes, so an RST sent the instant it appears is written into a void and
|
||||
// is only recovered by the 3 s RSTACK retry. zwave_proxy defers its own first query for
|
||||
// the same reason.
|
||||
static constexpr uint32_t DEVICE_SETTLE_MS = 500;
|
||||
static constexpr uint32_t BOOT_SEQUENCE_TIMEOUT_MS = 10000; // Overall boot-harvest timeout
|
||||
static constexpr size_t NETWORK_INFO_PAYLOAD_SIZE = 19; // ieee(8) + extended_pan(8) + pan_id(2) + channel(1)
|
||||
static constexpr size_t ZIGBEE_MAX_LOG_BYTES = 168; // Cap verbose hex dumps (168 * 3 = 504 byte buffer)
|
||||
|
||||
ZigbeeProxy *global_zigbee_proxy = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
@@ -32,10 +29,12 @@ ZigbeeProxy::ZigbeeProxy() { global_zigbee_proxy = this; }
|
||||
|
||||
void ZigbeeProxy::setup() {
|
||||
this->setup_time_ = millis();
|
||||
// Remember the configured line rate. Another device sharing this UART may change it
|
||||
// (a flasher stepping through baud rates to reach a bootloader, say) and has no way
|
||||
// to know what to restore, so we put it back ourselves when we take the bus again.
|
||||
this->configured_baud_rate_ = this->parent_->get_baud_rate();
|
||||
|
||||
// The port reads and forwards on its own; we only observe what passes and inject the
|
||||
// occasional acknowledgement. The harvest below runs before any client connects, so the
|
||||
// port has to keep reading with nobody subscribed -- hence the explicit request.
|
||||
this->parent_->set_tap(this);
|
||||
this->parent_->tap_request_port();
|
||||
|
||||
// Initialize state
|
||||
this->ash_state_ = AshState::DISCONNECTED;
|
||||
@@ -48,73 +47,94 @@ void ZigbeeProxy::setup() {
|
||||
}
|
||||
|
||||
void ZigbeeProxy::loop() {
|
||||
// Own the UART only while harvesting network info or serving a subscriber. Idling on
|
||||
// the bus otherwise would fight whichever device holds it -- a serial proxy carrying a
|
||||
// firmware update, say -- and the autonomous recovery below would inject ASH resets
|
||||
// into the middle of someone else's transfer.
|
||||
if (!this->should_own_uart_()) {
|
||||
if (this->owns_uart_) {
|
||||
this->owns_uart_ = false;
|
||||
if (this->api_connection_ != nullptr) {
|
||||
ESP_LOGW(TAG, "UART claimed by another device, dropping subscriber");
|
||||
this->unsubscribe_api_connection(this->api_connection_);
|
||||
}
|
||||
ESP_LOGD(TAG, "Released UART");
|
||||
this->boot_sequence_active_ = false;
|
||||
// The NCP may be reflashed while we are away, so nothing about the link can be
|
||||
// assumed on return.
|
||||
this->ash_state_ = AshState::FAILED;
|
||||
}
|
||||
// Watch for the radio being unplugged and plugged back in. The whole point of the
|
||||
// metadata is that a stick moved from another host is recognised here, and that move is
|
||||
// a hot-plug: harvesting only at boot would miss it entirely and leave the device
|
||||
// advertising nothing for a radio that is sitting right there.
|
||||
const bool connected = this->parent_->is_device_connected();
|
||||
if (connected != this->was_connected_) {
|
||||
this->was_connected_ = connected;
|
||||
this->on_device_presence_changed_(connected);
|
||||
}
|
||||
|
||||
// A re-harvest owed from on_protocol_disabled(), now that the port is idle again
|
||||
if (this->reharvest_pending_ && !this->boot_sequence_active_ && this->parent_->get_api_connection() == nullptr) {
|
||||
ESP_LOGI(TAG, "Port idle again, re-reading network info");
|
||||
this->reharvest_pending_ = false;
|
||||
this->parent_->tap_request_port();
|
||||
this->reset_ash_protocol_();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this->owns_uart_) {
|
||||
ESP_LOGI(TAG, "Acquired UART, resetting NCP link");
|
||||
this->owns_uart_ = true;
|
||||
if (this->parent_->get_baud_rate() != this->configured_baud_rate_) {
|
||||
ESP_LOGI(TAG, "Restoring baud rate %" PRIu32 " (was %" PRIu32 ")", this->configured_baud_rate_,
|
||||
this->parent_->get_baud_rate());
|
||||
this->parent_->set_baud_rate(this->configured_baud_rate_);
|
||||
this->parent_->load_settings(false);
|
||||
}
|
||||
// A subscriber drives its own session: it opens with an RST and negotiates its own
|
||||
// EZSP version. Harvesting here would put a second RST on the wire alongside the
|
||||
// client's and renegotiate the NCP underneath it, so only harvest when there is
|
||||
// something left to learn.
|
||||
if (this->api_connection_ != nullptr && this->network_info_.valid) {
|
||||
this->reset_ncp_link_();
|
||||
} else {
|
||||
this->reset_ash_protocol_();
|
||||
}
|
||||
// Bytes arrive through on_device_rx(), so the only work left on an idle tick is the
|
||||
// presence check above -- an atomic load and a compare. The loop deliberately stays
|
||||
// enabled for it: disabling it would mean a stick plugged in later is never noticed.
|
||||
if (!this->boot_sequence_active_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Process incoming UART data
|
||||
this->process_uart_();
|
||||
|
||||
// Check for ACK timeout and handle retransmission
|
||||
if (this->tx_buffer_pending_ && this->check_ack_timeout_()) {
|
||||
this->handle_retransmission_();
|
||||
}
|
||||
|
||||
if (this->boot_sequence_active_) {
|
||||
this->check_boot_timeouts_();
|
||||
} else if (this->api_connection_ == nullptr && this->ash_state_ == AshState::CONNECTING &&
|
||||
millis() - this->setup_time_ > ASH_RESET_TIMEOUT) {
|
||||
ESP_LOGE(TAG, "RSTACK timeout, NCP not responding");
|
||||
this->ash_state_ = AshState::FAILED;
|
||||
}
|
||||
this->check_boot_timeouts_();
|
||||
}
|
||||
|
||||
// Guard against a subscriber that disconnected without unsubscribing
|
||||
if (this->api_connection_ != nullptr && (!this->api_connection_->is_connection_setup() || !api_is_connected())) {
|
||||
ESP_LOGW(TAG, "Subscriber disconnected");
|
||||
this->unsubscribe_api_connection(this->api_connection_);
|
||||
}
|
||||
void ZigbeeProxy::on_device_rx(const uint8_t *data, size_t len) {
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
const uint8_t byte = data[i];
|
||||
ESP_LOGV(TAG, "RX: 0x%02X", byte);
|
||||
|
||||
// No autonomous recovery while a client is subscribed. A subscriber owns the link: it
|
||||
// opens with its own RST and resets whenever it decides it needs to. Resetting on its
|
||||
// behalf relays an RSTACK it never asked for, which bellows treats as fatal -- and if it
|
||||
// happens to be driving a bootloader over this interface, injecting ASH into the
|
||||
// transfer is worse still. A broken link is the client's to notice and repair.
|
||||
if (this->ash_state_ != AshState::CONNECTED) {
|
||||
this->check_bootloader_mode_(this->last_rx_byte_, byte);
|
||||
this->last_rx_byte_ = byte;
|
||||
} else if (this->bootloader_state_ != BootloaderState::NORMAL) {
|
||||
// Normal traffic while connected clears any stale bootloader detection
|
||||
ESP_LOGV(TAG, "NCP returned to normal operation");
|
||||
this->bootloader_state_ = BootloaderState::NORMAL;
|
||||
}
|
||||
|
||||
if (this->boot_sequence_active_) {
|
||||
// Harvest: this component is the ASH endpoint and consumes the frames itself
|
||||
this->parse_byte_(byte);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Observation only: the detector never gates forwarding, so it adds no latency and a
|
||||
// frame it cannot parse still reaches the client, which judges it for itself.
|
||||
this->detector_.from_ncp(byte);
|
||||
|
||||
// Outside the harvest the detector is the only thing watching the link, so its
|
||||
// progress is what tells us the NCP is alive -- and hence that any earlier bootloader
|
||||
// detection is stale.
|
||||
if (this->detector_.state() != AshDetectState::IDLE) {
|
||||
this->ash_state_ = AshState::CONNECTED;
|
||||
}
|
||||
|
||||
uint8_t ack_num;
|
||||
if (this->detector_.take_pending_ack(ack_num)) {
|
||||
// The client suppresses its own ACKs, so this is the only acknowledgement the NCP
|
||||
// will see. Only ever sent for a frame that passed CRC and arrived in sequence.
|
||||
this->send_ack_frame_(ack_num);
|
||||
const uint8_t *ezsp = this->detector_.last_ezsp_frame();
|
||||
const size_t ezsp_length = this->detector_.last_ezsp_frame_length();
|
||||
this->sniff_network_info_(ezsp, ezsp_length);
|
||||
this->sniff_stack_status_(ezsp, ezsp_length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ZigbeeProxy::on_client_tx(const uint8_t *data, size_t len) {
|
||||
// Scanning this direction only matters while waiting for the version command that
|
||||
// completes the handshake. Outside that window it is skipped entirely -- which is what
|
||||
// makes a firmware upload, all of which flows this way, essentially free.
|
||||
if (!this->detector_.needs_host_scan()) {
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
this->detector_.from_host(data[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void ZigbeeProxy::check_boot_timeouts_() {
|
||||
@@ -179,7 +199,7 @@ bool ZigbeeProxy::can_proceed() {
|
||||
}
|
||||
|
||||
// loop() is not called while setup is blocked, so run the boot machinery here
|
||||
this->process_uart_();
|
||||
this->parent_->tap_pump();
|
||||
if (this->tx_buffer_pending_ && this->check_ack_timeout_()) {
|
||||
this->handle_retransmission_();
|
||||
}
|
||||
@@ -197,38 +217,6 @@ void ZigbeeProxy::api_connection_authenticated(api::APIConnection *conn) {
|
||||
|
||||
void ZigbeeProxy::zigbee_proxy_request(api::APIConnection *api_connection, const api::ZigbeeProxyRequest &msg) {
|
||||
switch (msg.type) {
|
||||
case api::enums::ZIGBEE_PROXY_REQUEST_TYPE_SUBSCRIBE:
|
||||
if (this->api_connection_ != nullptr && this->api_connection_ != api_connection) {
|
||||
// A living subscriber keeps exclusive access. Its connection may be dead without
|
||||
// loop() having noticed yet (e.g. the client crashed and reconnected quickly);
|
||||
// in that case let the new client take over instead of locking it out for the
|
||||
// full API keepalive timeout.
|
||||
if (this->api_connection_->is_connection_setup()) {
|
||||
ESP_LOGW(TAG, "Another client is already subscribed");
|
||||
return;
|
||||
}
|
||||
ESP_LOGW(TAG, "Previous subscriber disconnected; taking over subscription");
|
||||
}
|
||||
ESP_LOGD(TAG, "Client subscribed");
|
||||
this->api_connection_ = api_connection;
|
||||
// A subscriber owns the link from here on, so abandon any harvest in flight rather
|
||||
// than interleaving our own EZSP commands with the client's session. Metadata for
|
||||
// this session comes from watching the client's own traffic instead.
|
||||
if (this->boot_sequence_active_) {
|
||||
ESP_LOGD(TAG, "Abandoning boot harvest, client owns the link");
|
||||
this->boot_sequence_active_ = false;
|
||||
this->boot_state_ = BootState::IDLE;
|
||||
}
|
||||
this->detector_.reset();
|
||||
break;
|
||||
|
||||
case api::enums::ZIGBEE_PROXY_REQUEST_TYPE_UNSUBSCRIBE:
|
||||
if (this->api_connection_ == api_connection) {
|
||||
ESP_LOGD(TAG, "Client unsubscribed");
|
||||
this->unsubscribe_api_connection(api_connection);
|
||||
}
|
||||
break;
|
||||
|
||||
case api::enums::ZIGBEE_PROXY_REQUEST_TYPE_NETWORK_INFO:
|
||||
this->send_network_info_changed_msg_(api_connection);
|
||||
break;
|
||||
@@ -239,36 +227,6 @@ void ZigbeeProxy::zigbee_proxy_request(api::APIConnection *api_connection, const
|
||||
}
|
||||
}
|
||||
|
||||
void ZigbeeProxy::unsubscribe_api_connection(api::APIConnection *conn) {
|
||||
if (this->api_connection_ != conn) {
|
||||
return;
|
||||
}
|
||||
this->api_connection_ = nullptr;
|
||||
// Anything already buffered belongs to the departed session
|
||||
this->relay_length_ = 0;
|
||||
this->detector_.reset();
|
||||
}
|
||||
|
||||
void ZigbeeProxy::zigbee_proxy_frame(api::APIConnection *api_connection, const api::ZigbeeProxyFrame &msg) {
|
||||
if (this->api_connection_ != api_connection) {
|
||||
ESP_LOGW(TAG, "Frame received from non-subscribed client");
|
||||
return;
|
||||
}
|
||||
|
||||
// Transparent relay: the client's ASH bytes reach the NCP untouched, so the two share
|
||||
// one sequence space and nothing here can desynchronize it.
|
||||
this->write_array(msg.data, msg.data_len);
|
||||
|
||||
// Scanning this direction only matters while waiting for the version command that
|
||||
// completes the handshake. Outside that window it is a pure passthrough -- which is
|
||||
// what makes a firmware upload, all of which flows this way, essentially free.
|
||||
if (this->detector_.needs_host_scan()) {
|
||||
for (size_t i = 0; i < msg.data_len; i++) {
|
||||
this->detector_.from_host(msg.data[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t ZigbeeProxy::get_ieee_address() const {
|
||||
uint64_t addr = 0;
|
||||
for (size_t i = 0; i < ZIGBEE_IEEE_ADDR_SIZE; i++) {
|
||||
@@ -285,13 +243,6 @@ void ZigbeeProxy::set_timeout_config(uint32_t initial_ms, uint32_t min_ms, uint3
|
||||
ESP_LOGV(TAG, "Timeout config updated: initial=%u, min=%u, max=%u", initial_ms, min_ms, max_ms);
|
||||
}
|
||||
|
||||
#ifdef USE_ZIGBEE_PROXY_USB_UART
|
||||
void ZigbeeProxy::set_usb_uart_channel(usb_uart::USBUartChannel *channel) {
|
||||
channel->set_rx_callback([this]() { this->process_uart_(); });
|
||||
ESP_LOGD(TAG, "Registered USB UART RX callback for low-latency processing");
|
||||
}
|
||||
#endif
|
||||
|
||||
// ASH Protocol State Machine
|
||||
void ZigbeeProxy::reset_ash_protocol_() {
|
||||
ESP_LOGV(TAG, "Resetting ASH protocol");
|
||||
@@ -314,33 +265,12 @@ void ZigbeeProxy::reset_ash_protocol_() {
|
||||
this->boot_sequence_active_ = true;
|
||||
this->ezsp_sequence_ = 0;
|
||||
|
||||
// Abandon RSTACKs owed from a previous attempt: they can no longer arrive in a state
|
||||
// where suppressing them is correct, and a stale count would swallow a real reset.
|
||||
this->own_rst_outstanding_ = 0;
|
||||
|
||||
this->send_rst_frame_();
|
||||
}
|
||||
|
||||
void ZigbeeProxy::reset_ncp_link_() {
|
||||
this->ash_state_ = AshState::CONNECTING;
|
||||
this->setup_time_ = millis(); // Reset timeout reference for the RSTACK wait
|
||||
this->tx_sequence_ = 0;
|
||||
this->rx_sequence_ = 0;
|
||||
this->tx_buffer_pending_ = false;
|
||||
this->tx_retry_count_ = 0;
|
||||
this->parsing_state_ = ParsingState::WAIT_FLAG_START;
|
||||
this->relay_length_ = 0;
|
||||
this->detector_.reset();
|
||||
this->send_rst_frame_(false);
|
||||
}
|
||||
|
||||
void ZigbeeProxy::send_rst_frame_(bool own_reset) {
|
||||
if (own_reset) {
|
||||
this->own_rst_outstanding_++;
|
||||
}
|
||||
|
||||
void ZigbeeProxy::send_rst_frame_() {
|
||||
// Build a combined buffer: 32 CAN bytes followed immediately by the RST frame,
|
||||
// sent as a single write_array call. This ensures correct byte ordering and
|
||||
// sent as a single write. This ensures correct byte ordering and
|
||||
// minimizes the number of USB bulk transfers (all bytes fit in one USB FS packet).
|
||||
static constexpr uint8_t ASH_CAN_BYTE = 0x1A;
|
||||
static constexpr size_t CAN_COUNT = 32;
|
||||
@@ -353,8 +283,7 @@ void ZigbeeProxy::send_rst_frame_(bool own_reset) {
|
||||
char hex_buf[format_hex_pretty_size(MAX_RST_FRAME_SIZE)];
|
||||
#endif
|
||||
ESP_LOGV(TAG, "RST frame bytes (%u): %s", rst_len, format_hex_pretty_to(hex_buf, combined + CAN_COUNT, rst_len));
|
||||
this->write_array(combined, CAN_COUNT + rst_len);
|
||||
this->flush();
|
||||
this->parent_->write_from_tap(combined, CAN_COUNT + rst_len);
|
||||
ESP_LOGV(TAG, "Sent RST frame (with %u CAN bytes prefix)", CAN_COUNT);
|
||||
}
|
||||
|
||||
@@ -364,29 +293,14 @@ void ZigbeeProxy::handle_rstack_frame_(const uint8_t *data, size_t length) {
|
||||
this->rx_sequence_ = 0;
|
||||
this->clear_tx_buffer_();
|
||||
|
||||
// Account for this RSTACK before deciding whether the client should see it. Only a
|
||||
// reset we did not cause is news to the client; relaying one of ours makes bellows
|
||||
// call enter_failed_state() and cancel every command it has in flight.
|
||||
bool solicited_by_us = this->own_rst_outstanding_ > 0;
|
||||
if (solicited_by_us) {
|
||||
this->own_rst_outstanding_--;
|
||||
}
|
||||
|
||||
if (this->boot_state_ == BootState::WAIT_RSTACK) {
|
||||
// Initial RSTACK - start boot sequence
|
||||
ESP_LOGV(TAG, "Received RSTACK, starting EZSP initialization");
|
||||
this->ash_state_ = AshState::CONNECTED;
|
||||
|
||||
// Drain any stale bytes that arrived before the RSTACK (e.g. leftover
|
||||
// UART FIFO bytes on HW UART, or a partial prior frame on USB CDC).
|
||||
// For USB CDC the input_buffer_ is already fully up-to-date at this point
|
||||
// (the RX callback just moved all pending chunks into it), so this loop
|
||||
// completes immediately rather than spinning with yield().
|
||||
while (this->available()) {
|
||||
uint8_t discard;
|
||||
this->read_byte(&discard);
|
||||
ESP_LOGV(TAG, "Draining post-RSTACK byte: 0x%02X", discard);
|
||||
}
|
||||
// Stale bytes preceding the RSTACK (leftover UART FIFO content, or a partial prior
|
||||
// frame) need no draining: the port owns the read side now, so anything before the
|
||||
// RSTACK has already passed through the parser and been discarded by frame delimiting.
|
||||
|
||||
this->boot_state_ = BootState::SEND_VERSION;
|
||||
this->advance_boot_state_();
|
||||
@@ -399,28 +313,11 @@ void ZigbeeProxy::handle_rstack_frame_(const uint8_t *data, size_t length) {
|
||||
|
||||
// Now check for WiFi/Zigbee channel conflicts
|
||||
this->check_wifi_zigbee_conflict_();
|
||||
} else if (this->ash_state_ == AshState::CONNECTING) {
|
||||
if (solicited_by_us) {
|
||||
// One of our own resets answered while a client reset is still outstanding. Stay in
|
||||
// CONNECTING and keep waiting for the RSTACK the client is actually owed.
|
||||
ESP_LOGV(TAG, "Consumed own RSTACK while awaiting the client's");
|
||||
return;
|
||||
}
|
||||
// RSTACK during connecting (triggered by client RST forwarding)
|
||||
ESP_LOGV(TAG, "Received RSTACK, NCP ready");
|
||||
this->ash_state_ = AshState::CONNECTED;
|
||||
} else if (solicited_by_us) {
|
||||
// Surplus RSTACK from one of our own resets, most often an RST retry racing a reply
|
||||
// that was merely slow. The client never asked for it, so swallow it.
|
||||
ESP_LOGV(TAG, "Consumed surplus RSTACK from own reset");
|
||||
this->ash_state_ = AshState::CONNECTED;
|
||||
} else if (this->api_connection_ != nullptr) {
|
||||
// A reset we did not cause: the NCP rebooted on its own, which invalidates the
|
||||
// client's session, so it has to hear about it.
|
||||
ESP_LOGW(TAG, "NCP reset unexpectedly, notifying client");
|
||||
this->ash_state_ = AshState::CONNECTED;
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Unexpected RSTACK received (boot_state=%d)", static_cast<int>(this->boot_state_));
|
||||
// An RSTACK outside the harvest belongs to whoever reset the NCP -- a client opening
|
||||
// its own session, most likely. Nothing to do but note that the link is alive.
|
||||
ESP_LOGV(TAG, "RSTACK received outside boot sequence (boot_state=%d)", static_cast<int>(this->boot_state_));
|
||||
this->ash_state_ = AshState::CONNECTED;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,21 +360,16 @@ void ZigbeeProxy::handle_error_frame_(const uint8_t *data, size_t length) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Reported only. This frame is only ever seen during the boot harvest, whose overall
|
||||
// timeout already guarantees forward progress; resetting the NCP here would restart that
|
||||
// timeout and could block startup indefinitely on a link that keeps erroring.
|
||||
ESP_LOGE(TAG, "NCP error: %s (0x%02X)", error_str, error_code);
|
||||
|
||||
if (this->api_connection_ != nullptr) {
|
||||
// Forward error to client
|
||||
} else {
|
||||
// No client, attempt recovery ourselves
|
||||
ESP_LOGV(TAG, "Attempting recovery");
|
||||
this->reset_ash_protocol_();
|
||||
}
|
||||
}
|
||||
|
||||
bool ZigbeeProxy::send_ack_frame_(uint8_t ack_num) {
|
||||
uint8_t frame[8];
|
||||
size_t length = this->build_frame_(frame, sizeof(frame), nullptr, 0, AshFrameType::ACK, 0, ack_num);
|
||||
this->write_array(frame, length);
|
||||
this->parent_->write_from_tap(frame, length);
|
||||
this->last_ack_sent_ = ack_num;
|
||||
ESP_LOGV(TAG, "Sent ACK for frame %d", ack_num);
|
||||
return true;
|
||||
@@ -486,7 +378,7 @@ bool ZigbeeProxy::send_ack_frame_(uint8_t ack_num) {
|
||||
bool ZigbeeProxy::send_nak_frame_(uint8_t ack_num) {
|
||||
uint8_t frame[8];
|
||||
size_t length = this->build_frame_(frame, sizeof(frame), nullptr, 0, AshFrameType::NAK, 0, ack_num);
|
||||
this->write_array(frame, length);
|
||||
this->parent_->write_from_tap(frame, length);
|
||||
ESP_LOGW(TAG, "Sent NAK for frame %d", ack_num);
|
||||
return true;
|
||||
}
|
||||
@@ -514,7 +406,7 @@ bool ZigbeeProxy::send_data_frame_(const uint8_t *data, size_t length, bool retr
|
||||
format_hex_pretty_to(hex_buf, this->tx_buffer_.data(), frame_length));
|
||||
|
||||
// Send frame
|
||||
this->write_array(this->tx_buffer_.data(), frame_length);
|
||||
this->parent_->write_from_tap(this->tx_buffer_.data(), frame_length);
|
||||
|
||||
// Start ACK timer
|
||||
this->tx_buffer_pending_ = true;
|
||||
@@ -576,7 +468,7 @@ void ZigbeeProxy::handle_retransmission_() {
|
||||
ASH_MAX_RETRIES);
|
||||
|
||||
// Resend the pending frame
|
||||
this->write_array(this->tx_pending_buffer_.data(), this->tx_pending_length_);
|
||||
this->parent_->write_from_tap(this->tx_pending_buffer_.data(), this->tx_pending_length_);
|
||||
this->start_ack_timer_();
|
||||
}
|
||||
|
||||
@@ -1023,73 +915,53 @@ void ZigbeeProxy::check_bootloader_mode_(uint8_t prev_byte, uint8_t byte) {
|
||||
}
|
||||
}
|
||||
|
||||
// UART processing (precondition: available() > 0, see inline process_uart_ in the header)
|
||||
void ZigbeeProxy::process_uart_slow_() {
|
||||
do {
|
||||
uint8_t byte;
|
||||
if (!this->read_byte(&byte)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Verbose logging for debugging (ESP_LOGV already checks log level)
|
||||
ESP_LOGV(TAG, "RX: 0x%02X", byte);
|
||||
|
||||
if (this->ash_state_ != AshState::CONNECTED) {
|
||||
this->check_bootloader_mode_(this->last_rx_byte_, byte);
|
||||
this->last_rx_byte_ = byte;
|
||||
} else if (this->bootloader_state_ != BootloaderState::NORMAL) {
|
||||
// Normal traffic while connected clears any stale bootloader detection
|
||||
ESP_LOGV(TAG, "NCP returned to normal operation");
|
||||
this->bootloader_state_ = BootloaderState::NORMAL;
|
||||
}
|
||||
|
||||
if (this->boot_sequence_active_) {
|
||||
// Harvest: this component is the ASH endpoint and consumes the frames itself
|
||||
this->parse_byte_(byte);
|
||||
} else {
|
||||
this->relay_ncp_byte_(byte);
|
||||
}
|
||||
} while (this->available());
|
||||
|
||||
this->relay_flush_();
|
||||
}
|
||||
|
||||
// ==================== Transparent relay ====================
|
||||
|
||||
void ZigbeeProxy::relay_ncp_byte_(uint8_t byte) {
|
||||
if (this->relay_length_ >= sizeof(this->relay_buffer_)) {
|
||||
this->relay_flush_();
|
||||
}
|
||||
this->relay_buffer_[this->relay_length_++] = byte;
|
||||
|
||||
// Observation only: the detector never gates forwarding, so it adds no latency and a
|
||||
// frame it cannot parse still reaches the client, which judges it for itself.
|
||||
this->detector_.from_ncp(byte);
|
||||
|
||||
// In relay mode the detector is the only thing watching the link, so its progress is
|
||||
// what tells us the NCP is alive. Without this ash_state_ sits at CONNECTING, times out
|
||||
// into FAILED, and autonomous recovery resets the NCP underneath a working session --
|
||||
// relaying an RSTACK the client never asked for, which kills it outright.
|
||||
if (this->detector_.state() != AshDetectState::IDLE) {
|
||||
this->ash_state_ = AshState::CONNECTED;
|
||||
}
|
||||
|
||||
uint8_t ack_num;
|
||||
if (this->detector_.take_pending_ack(ack_num)) {
|
||||
// The client suppresses its own ACKs, so this is the only acknowledgement the NCP
|
||||
// will see. Only ever sent for a frame that passed CRC and arrived in sequence.
|
||||
this->send_ack_frame_(ack_num);
|
||||
const uint8_t *ezsp = this->detector_.last_ezsp_frame();
|
||||
const size_t ezsp_length = this->detector_.last_ezsp_frame_length();
|
||||
this->sniff_network_info_(ezsp, ezsp_length);
|
||||
this->sniff_stack_status_(ezsp, ezsp_length);
|
||||
}
|
||||
}
|
||||
|
||||
// A proxied getNetworkParameters response is the only authoritative view of the network
|
||||
// available while a client owns the link, so metadata is refreshed from the client's own
|
||||
// traffic rather than by injecting commands. Read-only: a frame that fails any check
|
||||
// simply leaves the previous values in place.
|
||||
void ZigbeeProxy::on_device_presence_changed_(bool connected) {
|
||||
if (!connected) {
|
||||
ESP_LOGD(TAG, "Radio disconnected, discarding network info");
|
||||
this->boot_sequence_active_ = false;
|
||||
this->boot_state_ = BootState::IDLE;
|
||||
this->reharvest_pending_ = false;
|
||||
if (this->network_info_.valid) {
|
||||
this->network_info_ = {};
|
||||
this->send_network_info_changed_msg_();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// A radio just appeared. Whatever we knew described a different one, so start over.
|
||||
ESP_LOGI(TAG, "Radio connected, reading network info");
|
||||
if (this->network_info_.valid) {
|
||||
this->network_info_ = {};
|
||||
this->send_network_info_changed_msg_();
|
||||
}
|
||||
this->reharvest_pending_ = true;
|
||||
this->reharvest_after_ = millis() + DEVICE_SETTLE_MS;
|
||||
}
|
||||
|
||||
void ZigbeeProxy::on_protocol_disabled() {
|
||||
// Everything here was read from a radio that a client is now taking over, so none of it
|
||||
// can be trusted: it survives a reflash to Thread, or to nothing at all, and would leave
|
||||
// us advertising a network that no longer exists. Reporting nothing is the honest answer
|
||||
// until a fresh harvest says otherwise.
|
||||
//
|
||||
// The harvest cannot run now -- the client holds the port -- so it is deferred. Once the
|
||||
// client goes away the port stays open for us (tap_needs_port) and loop() picks it up.
|
||||
this->reharvest_pending_ = true;
|
||||
this->reharvest_after_ = 0;
|
||||
this->enable_loop();
|
||||
|
||||
if (!this->network_info_.valid) {
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Protocol handling disabled, discarding network info");
|
||||
this->network_info_ = {};
|
||||
this->send_network_info_changed_msg_();
|
||||
}
|
||||
|
||||
void ZigbeeProxy::sniff_network_info_(const uint8_t *frame, size_t length) {
|
||||
// Every frame after the version handshake uses extended framing, so the header size is
|
||||
// fixed and needs no knowledge of the negotiated version.
|
||||
@@ -1097,7 +969,7 @@ void ZigbeeProxy::sniff_network_info_(const uint8_t *frame, size_t length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The relay never derandomizes, so work on a copy: the original bytes are already on
|
||||
// The observed bytes are the port's, not ours, so work on a copy: they are already on
|
||||
// their way to the client and must stay untouched.
|
||||
uint8_t decoded[EZSP_EXTENDED_HEADER_SIZE + NETWORK_PARAMS_RESPONSE_SIZE];
|
||||
memcpy(decoded, frame, sizeof(decoded));
|
||||
@@ -1166,25 +1038,6 @@ void ZigbeeProxy::sniff_stack_status_(const uint8_t *frame, size_t length) {
|
||||
this->send_network_info_changed_msg_();
|
||||
}
|
||||
|
||||
void ZigbeeProxy::relay_flush_() {
|
||||
if (this->relay_length_ == 0) {
|
||||
return;
|
||||
}
|
||||
const size_t length = this->relay_length_;
|
||||
this->relay_length_ = 0;
|
||||
if (this->api_connection_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
this->outgoing_proto_msg_.data = this->relay_buffer_;
|
||||
this->outgoing_proto_msg_.data_len = length;
|
||||
if (!this->api_connection_->send_zigbee_proxy_frame(this->outgoing_proto_msg_)) {
|
||||
// API TX backpressure. Dropping bytes is recoverable: the client sees a truncated
|
||||
// frame, fails its CRC and NAKs, and the NCP retransmits. Withholding our ACK would
|
||||
// achieve the same thing more slowly, and buffering risks unbounded growth.
|
||||
ESP_LOGW(TAG, "Dropped %u relayed bytes (API TX buffer full)", length);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::zigbee_proxy
|
||||
|
||||
#endif // USE_ZIGBEE_PROXY
|
||||
|
||||
@@ -5,24 +5,14 @@
|
||||
|
||||
#include "esphome/components/api/api_connection.h"
|
||||
#include "esphome/components/api/api_pb2.h"
|
||||
#include "esphome/components/serial_proxy/serial_proxy.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "ash_protocol.h"
|
||||
#include "ash_detector.h"
|
||||
|
||||
#include <array>
|
||||
|
||||
// Forward-declare USBUartChannel so the set_usb_uart_channel() setter can be declared
|
||||
// without pulling usb_uart.h into every translation unit that includes this header.
|
||||
// USE_ZIGBEE_PROXY_USB_UART is defined by the Python to_code() only when uart_id
|
||||
// resolves to a USB UART channel, ensuring the header is actually in the build path.
|
||||
#ifdef USE_ZIGBEE_PROXY_USB_UART
|
||||
namespace esphome::usb_uart {
|
||||
class USBUartChannel;
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace esphome::zigbee_proxy {
|
||||
|
||||
// Timeout configuration structure
|
||||
@@ -44,6 +34,11 @@ struct NetworkInfo {
|
||||
|
||||
enum ZigbeeProxyFeature : uint32_t {
|
||||
FEATURE_ZIGBEE_PROXY_ENABLED = 1 << 0,
|
||||
// Set only when the harvest actually read a network off the radio. Without it a client
|
||||
// cannot tell "a Zigbee radio with no network formed" from "not a Zigbee radio at all"
|
||||
// -- both otherwise present as ENABLED with an all-zero payload, and the second happens
|
||||
// whenever the NCP has been reflashed to Thread or is simply not responding.
|
||||
FEATURE_ZIGBEE_NETWORK_INFO_VALID = 1 << 1,
|
||||
};
|
||||
|
||||
// Boot-time initialization state machine
|
||||
@@ -62,7 +57,12 @@ enum class BootState : uint8_t {
|
||||
FAILED, // Boot sequence failed
|
||||
};
|
||||
|
||||
class ZigbeeProxy : public uart::UARTDevice, public Component {
|
||||
// Watches a `serial_proxy` port carrying an EZSP NCP and reports what it learns about the
|
||||
// Zigbee network. It never carries client traffic: the serial proxy owns the port and the
|
||||
// bytes, and this component only observes them, plus two exceptions where it writes to the
|
||||
// port itself -- the boot-time metadata harvest, which runs before any client connects, and
|
||||
// the ASH acknowledgements a client asks it to send on its behalf.
|
||||
class ZigbeeProxy : public serial_proxy::SerialProxyTap, public Component {
|
||||
public:
|
||||
ZigbeeProxy();
|
||||
|
||||
@@ -72,16 +72,40 @@ class ZigbeeProxy : public uart::UARTDevice, public Component {
|
||||
float get_setup_priority() const override;
|
||||
bool can_proceed() override;
|
||||
|
||||
void set_serial_proxy(serial_proxy::SerialProxy *parent) { this->parent_ = parent; }
|
||||
|
||||
// SerialProxyTap
|
||||
void on_device_rx(const uint8_t *data, size_t len) override;
|
||||
void on_client_tx(const uint8_t *data, size_t len) override;
|
||||
bool tap_needs_port() const override {
|
||||
if (this->boot_sequence_active_) {
|
||||
return true;
|
||||
}
|
||||
// A pending re-harvest waits for the port to go idle. Starting one under a subscriber
|
||||
// would inject our own ASH frames into whatever it is doing -- most likely the very
|
||||
// firmware upload that invalidated the metadata.
|
||||
return this->reharvest_pending_ && this->parent_->get_api_connection() == nullptr;
|
||||
}
|
||||
|
||||
/// The port stopped handling our protocol, so whatever we know about the radio may no
|
||||
/// longer be true -- a client asking for raw bytes is usually about to reflash it.
|
||||
void on_protocol_disabled() override;
|
||||
|
||||
/// The radio was unplugged or a new one appeared; metadata describes neither.
|
||||
void on_device_presence_changed_(bool connected);
|
||||
|
||||
// API integration
|
||||
void api_connection_authenticated(api::APIConnection *conn);
|
||||
void zigbee_proxy_request(api::APIConnection *api_connection, const api::ZigbeeProxyRequest &msg);
|
||||
void zigbee_proxy_frame(api::APIConnection *api_connection, const api::ZigbeeProxyFrame &msg);
|
||||
api::APIConnection *get_api_connection() { return this->api_connection_; }
|
||||
// Drop the subscription of a disconnecting client (called from APIConnection teardown)
|
||||
void unsubscribe_api_connection(api::APIConnection *conn);
|
||||
|
||||
// Feature flags
|
||||
uint32_t get_feature_flags() const { return ZigbeeProxyFeature::FEATURE_ZIGBEE_PROXY_ENABLED; }
|
||||
uint32_t get_feature_flags() const {
|
||||
uint32_t flags = ZigbeeProxyFeature::FEATURE_ZIGBEE_PROXY_ENABLED;
|
||||
if (this->network_info_.valid) {
|
||||
flags |= ZigbeeProxyFeature::FEATURE_ZIGBEE_NETWORK_INFO_VALID;
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
// Network information accessors
|
||||
const NetworkInfo &get_network_info() const { return this->network_info_; }
|
||||
@@ -93,25 +117,10 @@ class ZigbeeProxy : public uart::UARTDevice, public Component {
|
||||
void set_min_timeout(uint32_t timeout_ms) { this->timeout_config_.min_timeout_ms = timeout_ms; }
|
||||
void set_max_timeout(uint32_t timeout_ms) { this->timeout_config_.max_timeout_ms = timeout_ms; }
|
||||
|
||||
#ifdef USE_ZIGBEE_PROXY_USB_UART
|
||||
/// Called from generated code when uart_id resolves to a USB UART channel.
|
||||
/// Registers an RX callback on the channel so incoming bytes are processed
|
||||
/// immediately in the same USBUartComponent::loop() iteration they arrive,
|
||||
/// without waiting for the next ZigbeeProxy::loop() call.
|
||||
void set_usb_uart_channel(usb_uart::USBUartChannel *channel);
|
||||
#endif
|
||||
|
||||
protected:
|
||||
// ASH Protocol State Machine
|
||||
void reset_ash_protocol_();
|
||||
// Resets the NCP link without running the metadata harvest, leaving the client to
|
||||
// negotiate its own session. The resulting RSTACK is relayed rather than consumed.
|
||||
void reset_ncp_link_();
|
||||
// `own_reset` marks a reset we originate ourselves (harvest, retry, final RST) so its
|
||||
// RSTACK is consumed here instead of being relayed to a client that never asked for
|
||||
// one -- bellows treats an unsolicited RSTACK as fatal and cancels every pending
|
||||
// command. Pass false when relaying a client's own RST.
|
||||
void send_rst_frame_(bool own_reset = true);
|
||||
void send_rst_frame_();
|
||||
void handle_rstack_frame_(const uint8_t *data, size_t length);
|
||||
void handle_error_frame_(const uint8_t *data, size_t length);
|
||||
// Applies a frame's ackNum to the pending TX frame. Returns true if it
|
||||
@@ -168,89 +177,36 @@ class ZigbeeProxy : public uart::UARTDevice, public Component {
|
||||
// Bootloader detection (fed consecutive raw byte pairs while not CONNECTED)
|
||||
void check_bootloader_mode_(uint8_t prev_byte, uint8_t byte);
|
||||
|
||||
// True when this component should be driving the shared UART: nobody else holds a
|
||||
// claim on it, and we have either a subscriber to serve or a boot harvest to finish.
|
||||
bool should_own_uart_() const {
|
||||
return !this->parent_->is_claimed_by_other(const_cast<ZigbeeProxy *>(this)) &&
|
||||
(this->boot_sequence_active_ || this->api_connection_ != nullptr);
|
||||
}
|
||||
|
||||
// UART processing
|
||||
// Inline fast-path: UART::available() is cheap (ring-buffer head/tail compare on most
|
||||
// backends), so an idle loop tick skips the out-of-line drain entirely. When bytes are
|
||||
// pending the slow path drains with do/while so available() is checked once per byte.
|
||||
ESPHOME_ALWAYS_INLINE void process_uart_() {
|
||||
// Checked here rather than only in loop(): on a USB UART the RX callback calls this
|
||||
// directly from the USB component's loop, so a guard in loop() alone would still let
|
||||
// us consume bytes belonging to whichever device holds the claim.
|
||||
if (!this->owns_uart_ && !this->boot_sequence_active_) {
|
||||
return;
|
||||
}
|
||||
if (!this->available()) {
|
||||
return;
|
||||
}
|
||||
this->process_uart_slow_();
|
||||
}
|
||||
// Precondition: caller must guarantee available() > 0 (see inline process_uart_ above)
|
||||
void process_uart_slow_();
|
||||
|
||||
// Client-side (left) ASH session
|
||||
void client_parse_byte_(uint8_t byte);
|
||||
void client_parse_control_byte_(uint8_t control);
|
||||
bool client_validate_frame_crc_();
|
||||
void client_send_ack_frame_(uint8_t ack_num);
|
||||
void client_send_nak_frame_(uint8_t ack_num);
|
||||
void client_send_rstack_frame_(uint8_t reset_code);
|
||||
void client_send_data_frame_(const uint8_t *data, size_t length);
|
||||
void client_send_error_frame_(uint8_t error_code);
|
||||
void client_send_raw_frame_(const uint8_t *frame, size_t length);
|
||||
// Transparent relay. NCP bytes are forwarded to the client verbatim and in bulk; the
|
||||
// detector only observes them, so it never gates or delays forwarding.
|
||||
void relay_ncp_byte_(uint8_t byte);
|
||||
void relay_flush_();
|
||||
// Reads network metadata out of a proxied getNetworkParameters response. Read-only, so a
|
||||
// misparse costs a missed update rather than corrupting anything.
|
||||
void sniff_network_info_(const uint8_t *frame, size_t length);
|
||||
// Invalidates network metadata when the stack reports it has left the network.
|
||||
void sniff_stack_status_(const uint8_t *frame, size_t length);
|
||||
|
||||
// Pre-allocated message - always ready to send
|
||||
api::ZigbeeProxyFrame outgoing_proto_msg_;
|
||||
|
||||
// NCP-side (right) ASH buffers
|
||||
// NCP-side ASH buffers
|
||||
std::array<uint8_t, MAX_ASH_FRAME_SIZE> rx_buffer_;
|
||||
std::array<uint8_t, MAX_ASH_FRAME_SIZE> tx_buffer_;
|
||||
std::array<uint8_t, MAX_ASH_FRAME_SIZE> tx_pending_buffer_; // For retransmission
|
||||
|
||||
// Client-side (left) ASH buffers
|
||||
|
||||
// Client -> NCP queue: EZSP payloads accepted while the ASH TX window is occupied
|
||||
// Network information
|
||||
NetworkInfo network_info_;
|
||||
|
||||
// Timeout configuration
|
||||
TimeoutConfig timeout_config_;
|
||||
|
||||
// Pointers (aligned together)
|
||||
api::APIConnection *api_connection_{nullptr}; // Current subscribed client
|
||||
// The port this component observes. Owns the UART and the bytes; every write we make
|
||||
// goes through it.
|
||||
serial_proxy::SerialProxy *parent_{nullptr};
|
||||
|
||||
// NCP-side (right) 32-bit values
|
||||
uint32_t setup_time_{0}; // Time when last RST frame was sent
|
||||
uint32_t boot_start_time_{0}; // Time when the boot sequence began (for overall timeout)
|
||||
uint32_t ack_timer_start_{0}; // Time when ACK timer started
|
||||
uint32_t last_rtt_ms_{0}; // Last measured round-trip time
|
||||
uint32_t last_recovery_attempt_{0}; // Time of last automatic reset attempt from FAILED
|
||||
uint32_t setup_time_{0}; // Time when last RST frame was sent
|
||||
uint32_t boot_start_time_{0}; // Time when the boot sequence began (for overall timeout)
|
||||
uint32_t ack_timer_start_{0}; // Time when ACK timer started
|
||||
uint32_t last_rtt_ms_{0}; // Last measured round-trip time
|
||||
|
||||
// Client-side (left) 32-bit values
|
||||
|
||||
// NCP-side (right) 16-bit values
|
||||
uint16_t rx_buffer_index_{0}; // Index for populating rx_buffer_
|
||||
uint16_t tx_pending_length_{0}; // Length of pending TX frame for retransmission
|
||||
uint16_t calculated_crc_{0}; // CRC calculated during frame reception
|
||||
|
||||
// Client-side (left) 16-bit values
|
||||
|
||||
// NCP-side (right) 8-bit values
|
||||
uint8_t tx_sequence_{0}; // TX sequence number (0-7)
|
||||
uint8_t rx_sequence_{0}; // RX sequence number (0-7)
|
||||
uint8_t tx_retry_count_{0}; // Number of retransmission attempts
|
||||
@@ -258,16 +214,11 @@ class ZigbeeProxy : public uart::UARTDevice, public Component {
|
||||
uint8_t last_ack_sent_{0}; // Last ACK number sent
|
||||
uint8_t last_rx_byte_{0}; // Previous raw RX byte (bootloader detection)
|
||||
|
||||
// Client-side (left) 8-bit values
|
||||
|
||||
// NCP-side enums and booleans
|
||||
AshState ash_state_{AshState::DISCONNECTED};
|
||||
ParsingState parsing_state_{ParsingState::WAIT_FLAG_START};
|
||||
BootloaderState bootloader_state_{BootloaderState::NORMAL};
|
||||
BootState boot_state_{BootState::IDLE};
|
||||
|
||||
// Client-side enums and booleans
|
||||
|
||||
uint8_t ezsp_version_{0}; // NCP's EZSP protocol version
|
||||
uint8_t ezsp_sequence_{0}; // EZSP frame sequence number
|
||||
uint8_t ezsp_requested_version_{0}; // Version we last requested (for re-negotiation)
|
||||
@@ -276,28 +227,20 @@ class ZigbeeProxy : public uart::UARTDevice, public Component {
|
||||
// with frame ID 0x0058. Tracks whether that second handshake has happened.
|
||||
bool ezsp_version_confirmed_{false};
|
||||
|
||||
bool tx_buffer_pending_{false}; // True if waiting for ACK from NCP
|
||||
bool escape_next_byte_{false}; // True if next NCP byte should be unescaped
|
||||
bool network_info_ready_{false}; // True when network info retrieved
|
||||
bool owns_uart_{false}; // True while this component drives the UART
|
||||
uint32_t configured_baud_rate_{0}; // Line rate to restore after another device
|
||||
bool tx_buffer_pending_{false}; // True if waiting for ACK from NCP
|
||||
bool escape_next_byte_{false}; // True if next NCP byte should be unescaped
|
||||
|
||||
bool boot_sequence_active_{false}; // True during boot-time init
|
||||
// Set when the metadata was discarded and a fresh harvest is owed once the port frees up
|
||||
bool reharvest_pending_{false};
|
||||
// Last observed device presence, for spotting a hot-plug
|
||||
bool was_connected_{false};
|
||||
// Earliest millis() at which a pending re-harvest may start
|
||||
uint32_t reharvest_after_{0};
|
||||
|
||||
// Decides when acknowledging on the client's behalf is safe. Armed only by the ASH
|
||||
// session handshake, so a bootloader or Thread NCP never triggers it.
|
||||
AshDetector detector_;
|
||||
|
||||
// Bytes staged for the client. Forwarding in bulk once per UART drain avoids an API
|
||||
// message per byte; the size only bounds latency, not correctness.
|
||||
static constexpr size_t RELAY_BUFFER_SIZE = 256;
|
||||
uint8_t relay_buffer_[RELAY_BUFFER_SIZE];
|
||||
size_t relay_length_{0};
|
||||
|
||||
// RSTACKs still owed to us for resets we sent ourselves. A retry can put two RSTs on
|
||||
// the wire when the first RSTACK was only slow rather than lost, so the NCP answers
|
||||
// with more RSTACKs than we asked for; the surplus must not reach a client.
|
||||
uint8_t own_rst_outstanding_{0};
|
||||
};
|
||||
|
||||
extern ZigbeeProxy *global_zigbee_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
@@ -166,6 +166,7 @@
|
||||
#define USE_SENSOR
|
||||
#define USE_SENSOR_FILTER
|
||||
#define USE_SERIAL_PROXY
|
||||
#define USE_SERIAL_PROXY_TAP
|
||||
#define USE_SETUP_PRIORITY_OVERRIDE
|
||||
#define USE_STATUS_LED
|
||||
#define USE_STATUS_SENSOR
|
||||
@@ -182,7 +183,6 @@
|
||||
#define USE_WATER_HEATER
|
||||
#define USE_WATER_HEATER_VISUAL_OVERRIDES
|
||||
#define USE_ZIGBEE_PROXY
|
||||
#define USE_ZIGBEE_PROXY_USB_UART
|
||||
#define USE_ZWAVE_PROXY
|
||||
|
||||
// Feature flags which do not work for zephyr
|
||||
|
||||
@@ -14,5 +14,10 @@ uart:
|
||||
rx_pin: ${rx_pin}
|
||||
baud_rate: 115200
|
||||
|
||||
zigbee_proxy:
|
||||
uart_id: zigbee_uart
|
||||
# The port owns the UART and carries every byte; zigbee_proxy only taps it
|
||||
serial_proxy:
|
||||
- id: zigbee_serial
|
||||
uart_id: zigbee_uart
|
||||
name: Zigbee
|
||||
port_type: TTL
|
||||
mode: ezsp_ash
|
||||
|
||||
@@ -15,7 +15,14 @@ usb_uart:
|
||||
- id: zigbee_usb_channel
|
||||
baud_rate: 460800
|
||||
|
||||
# uart_id points at a USB UART channel; the component must detect this
|
||||
# automatically and enable the RX callback fast path (USE_ZIGBEE_PROXY_USB_UART)
|
||||
# The tapped port may be a USB CDC ACM channel just as well as a hardware UART:
|
||||
# zigbee_proxy never touches the UART itself, so it does not care which it is.
|
||||
serial_proxy:
|
||||
- id: zigbee_usb_serial
|
||||
uart_id: zigbee_usb_channel
|
||||
name: Zigbee
|
||||
port_type: TTL
|
||||
mode: ezsp_ash
|
||||
|
||||
zigbee_proxy:
|
||||
uart_id: zigbee_usb_channel
|
||||
serial_proxy_id: zigbee_usb_serial
|
||||
|
||||
@@ -8,6 +8,7 @@ esp32:
|
||||
<<: !include common.yaml
|
||||
|
||||
zigbee_proxy:
|
||||
serial_proxy_id: zigbee_serial
|
||||
buffer_size: 1024
|
||||
initial_timeout: 1600
|
||||
min_timeout: 400
|
||||
|
||||
@@ -8,4 +8,5 @@ esp8266:
|
||||
<<: !include common.yaml
|
||||
|
||||
zigbee_proxy:
|
||||
serial_proxy_id: zigbee_serial
|
||||
buffer_size: 512
|
||||
|
||||
@@ -3,3 +3,6 @@ substitutions:
|
||||
rx_pin: GPIO1
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
zigbee_proxy:
|
||||
serial_proxy_id: zigbee_serial
|
||||
|
||||
Reference in New Issue
Block a user