mirror of
https://github.com/esphome/esphome.git
synced 2026-09-20 03:28:41 +00:00
[zigbee_proxy] New component
This commit is contained in:
@@ -260,6 +260,10 @@ message DeviceInfoResponse {
|
||||
// Indicates if Z-Wave proxy support is available and features supported
|
||||
uint32 zwave_proxy_feature_flags = 23 [(field_ifdef) = "USE_ZWAVE_PROXY"];
|
||||
uint32 zwave_home_id = 24 [(field_ifdef) = "USE_ZWAVE_PROXY"];
|
||||
|
||||
// Indicates if Zigbee proxy support is available and features supported
|
||||
uint32 zigbee_proxy_feature_flags = 25 [(field_ifdef) = "USE_ZIGBEE_PROXY"];
|
||||
uint64 zigbee_ieee_address = 26 [(field_ifdef) = "USE_ZIGBEE_PROXY"];
|
||||
}
|
||||
|
||||
message ListEntitiesRequest {
|
||||
@@ -2443,6 +2447,32 @@ message ZWaveProxyRequest {
|
||||
bytes data = 2;
|
||||
}
|
||||
|
||||
// ==================== ZIGBEE ====================
|
||||
|
||||
message ZigbeeProxyFrame {
|
||||
option (id) = 130;
|
||||
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;
|
||||
}
|
||||
|
||||
message ZigbeeProxyRequest {
|
||||
option (id) = 131;
|
||||
option (source) = SOURCE_BOTH;
|
||||
option (ifdef) = "USE_ZIGBEE_PROXY";
|
||||
|
||||
ZigbeeProxyRequestType type = 1;
|
||||
bytes data = 2;
|
||||
}
|
||||
|
||||
// ==================== INFRARED ====================
|
||||
// Note: Feature and capability flag enums are defined in
|
||||
// esphome/components/infrared/infrared.h
|
||||
|
||||
@@ -43,6 +43,9 @@
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
#include "esphome/components/zwave_proxy/zwave_proxy.h"
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
#include "esphome/components/zigbee_proxy/zigbee_proxy.h"
|
||||
#endif
|
||||
#ifdef USE_WATER_HEATER
|
||||
#include "esphome/components/water_heater/water_heater.h"
|
||||
#endif
|
||||
@@ -1267,6 +1270,16 @@ 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);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
bool APIConnection::send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) {
|
||||
return this->send_message_smart_(a_alarm_control_panel, AlarmControlPanelStateResponse::MESSAGE_TYPE,
|
||||
@@ -1502,6 +1515,11 @@ void APIConnection::complete_authentication_() {
|
||||
zwave_proxy::global_zwave_proxy->api_connection_authenticated(this);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
if (zigbee_proxy::global_zigbee_proxy != nullptr) {
|
||||
zigbee_proxy::global_zigbee_proxy->api_connection_authenticated(this);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool APIConnection::send_hello_response_(const HelloRequest &msg) {
|
||||
@@ -1627,6 +1645,10 @@ bool APIConnection::send_device_info_response_() {
|
||||
resp.zwave_proxy_feature_flags = zwave_proxy::global_zwave_proxy->get_feature_flags();
|
||||
resp.zwave_home_id = zwave_proxy::global_zwave_proxy->get_home_id();
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
resp.zigbee_proxy_feature_flags = zigbee_proxy::global_zigbee_proxy->get_feature_flags();
|
||||
resp.zigbee_ieee_address = zigbee_proxy::global_zigbee_proxy->get_ieee_address();
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
resp.api_encryption_supported = true;
|
||||
#endif
|
||||
|
||||
@@ -167,6 +167,12 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override;
|
||||
#endif
|
||||
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
void on_zigbee_proxy_frame(const ZigbeeProxyFrame &msg) override;
|
||||
void on_zigbee_proxy_request(const ZigbeeProxyRequest &msg) override;
|
||||
void send_zigbee_proxy_frame(const ZigbeeProxyFrame &msg) { this->send_message(msg, ZigbeeProxyFrame::MESSAGE_TYPE); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
|
||||
void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) override;
|
||||
|
||||
@@ -119,6 +119,12 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
buffer.encode_uint32(24, this->zwave_home_id);
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
buffer.encode_uint32(25, this->zigbee_proxy_feature_flags);
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
buffer.encode_uint64(26, this->zigbee_ieee_address);
|
||||
#endif
|
||||
}
|
||||
void DeviceInfoResponse::calculate_size(ProtoSize &size) const {
|
||||
size.add_length(1, this->name.size());
|
||||
@@ -174,6 +180,12 @@ void DeviceInfoResponse::calculate_size(ProtoSize &size) const {
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
size.add_uint32(2, this->zwave_home_id);
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
size.add_uint32(2, this->zigbee_proxy_feature_flags);
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
size.add_uint64(2, this->zigbee_ieee_address);
|
||||
#endif
|
||||
}
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
@@ -3347,6 +3359,52 @@ void ZWaveProxyRequest::calculate_size(ProtoSize &size) const {
|
||||
size.add_length(1, this->data_len);
|
||||
}
|
||||
#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;
|
||||
}
|
||||
void ZigbeeProxyFrame::encode(ProtoWriteBuffer buffer) const { buffer.encode_bytes(1, this->data, this->data_len); }
|
||||
void ZigbeeProxyFrame::calculate_size(ProtoSize &size) const { size.add_length(1, this->data_len); }
|
||||
bool ZigbeeProxyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
switch (field_id) {
|
||||
case 1:
|
||||
this->type = static_cast<enums::ZigbeeProxyRequestType>(value.as_uint32());
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool ZigbeeProxyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 2: {
|
||||
this->data = value.data();
|
||||
this->data_len = value.size();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
void ZigbeeProxyRequest::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_uint32(1, static_cast<uint32_t>(this->type));
|
||||
buffer.encode_bytes(2, this->data, this->data_len);
|
||||
}
|
||||
void ZigbeeProxyRequest::calculate_size(ProtoSize &size) const {
|
||||
size.add_uint32(1, static_cast<uint32_t>(this->type));
|
||||
size.add_length(1, this->data_len);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_INFRARED
|
||||
void ListEntitiesInfraredResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_string(1, this->object_id);
|
||||
|
||||
@@ -311,6 +311,13 @@ enum ZWaveProxyRequestType : uint32_t {
|
||||
ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE = 2,
|
||||
};
|
||||
#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,
|
||||
};
|
||||
#endif
|
||||
|
||||
} // namespace enums
|
||||
|
||||
@@ -474,7 +481,7 @@ class DeviceInfo final : public ProtoMessage {
|
||||
class DeviceInfoResponse final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 10;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 255;
|
||||
static constexpr uint16_t ESTIMATED_SIZE = 265;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "device_info_response"; }
|
||||
#endif
|
||||
@@ -526,6 +533,12 @@ class DeviceInfoResponse final : public ProtoMessage {
|
||||
#endif
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
uint32_t zwave_home_id{0};
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
uint32_t zigbee_proxy_feature_flags{0};
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
uint64_t zigbee_ieee_address{0};
|
||||
#endif
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
@@ -2960,6 +2973,46 @@ class ZWaveProxyRequest final : public ProtoDecodableMessage {
|
||||
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
class ZigbeeProxyFrame final : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 130;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 19;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "zigbee_proxy_frame"; }
|
||||
#endif
|
||||
const uint8_t *data{nullptr};
|
||||
uint16_t data_len{0};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#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 = 131;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 21;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "zigbee_proxy_request"; }
|
||||
#endif
|
||||
enums::ZigbeeProxyRequestType type{};
|
||||
const uint8_t *data{nullptr};
|
||||
uint16_t data_len{0};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#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;
|
||||
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_INFRARED
|
||||
class ListEntitiesInfraredResponse final : public InfoResponseProtoMessage {
|
||||
public:
|
||||
|
||||
@@ -736,6 +736,20 @@ template<> const char *proto_enum_to_string<enums::ZWaveProxyRequestType>(enums:
|
||||
}
|
||||
}
|
||||
#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 "ZIGBEE_PROXY_REQUEST_TYPE_SUBSCRIBE";
|
||||
case enums::ZIGBEE_PROXY_REQUEST_TYPE_UNSUBSCRIBE:
|
||||
return "ZIGBEE_PROXY_REQUEST_TYPE_UNSUBSCRIBE";
|
||||
case enums::ZIGBEE_PROXY_REQUEST_TYPE_NETWORK_INFO:
|
||||
return "ZIGBEE_PROXY_REQUEST_TYPE_NETWORK_INFO";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
const char *HelloRequest::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "HelloRequest");
|
||||
@@ -845,6 +859,12 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const {
|
||||
#endif
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
dump_field(out, "zwave_home_id", this->zwave_home_id);
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
dump_field(out, "zigbee_proxy_feature_flags", this->zigbee_proxy_feature_flags);
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
dump_field(out, "zigbee_ieee_address", this->zigbee_ieee_address);
|
||||
#endif
|
||||
return out.c_str();
|
||||
}
|
||||
@@ -2422,6 +2442,19 @@ const char *ZWaveProxyRequest::dump_to(DumpBuffer &out) const {
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
const char *ZigbeeProxyFrame::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "ZigbeeProxyFrame");
|
||||
dump_bytes_field(out, "data", this->data, this->data_len);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *ZigbeeProxyRequest::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "ZigbeeProxyRequest");
|
||||
dump_field(out, "type", static_cast<enums::ZigbeeProxyRequestType>(this->type));
|
||||
dump_bytes_field(out, "data", this->data, this->data_len);
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_INFRARED
|
||||
const char *ListEntitiesInfraredResponse::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "ListEntitiesInfraredResponse");
|
||||
|
||||
@@ -602,14 +602,25 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
case HomeassistantActionResponse::MESSAGE_TYPE: {
|
||||
HomeassistantActionResponse msg;
|
||||
#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_homeassistant_action_response"), msg);
|
||||
this->log_receive_message_(LOG_STR("on_zigbee_proxy_frame"), msg);
|
||||
#endif
|
||||
this->on_homeassistant_action_response(msg);
|
||||
this->on_zigbee_proxy_frame(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
case ZigbeeProxyRequest::MESSAGE_TYPE: {
|
||||
ZigbeeProxyRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_zigbee_proxy_request"), msg);
|
||||
#endif
|
||||
this->on_zigbee_proxy_request(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -219,6 +219,12 @@ class APIServerConnectionBase : public ProtoService {
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
virtual void on_z_wave_proxy_request(const ZWaveProxyRequest &value){};
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
virtual void on_zigbee_proxy_frame(const ZigbeeProxyFrame &value){};
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
virtual void on_zigbee_proxy_request(const ZigbeeProxyRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_IR_RF
|
||||
virtual void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value){};
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_POWER_SAVE_MODE, CONF_WIFI
|
||||
import esphome.final_validate as fv
|
||||
|
||||
CODEOWNERS = ["@kbx81"]
|
||||
DEPENDENCIES = ["api", "uart"]
|
||||
|
||||
CONF_BUFFER_SIZE = "buffer_size"
|
||||
CONF_INITIAL_TIMEOUT = "initial_timeout"
|
||||
CONF_MIN_TIMEOUT = "min_timeout"
|
||||
CONF_MAX_TIMEOUT = "max_timeout"
|
||||
|
||||
zigbee_proxy_ns = cg.esphome_ns.namespace("zigbee_proxy")
|
||||
ZigbeeProxy = zigbee_proxy_ns.class_("ZigbeeProxy", cg.Component, uart.UARTDevice)
|
||||
|
||||
|
||||
def final_validate(config):
|
||||
full_config = fv.full_config.get()
|
||||
if (wifi_conf := full_config.get(CONF_WIFI)) and (
|
||||
wifi_conf.get(CONF_POWER_SAVE_MODE, "").lower() != "none"
|
||||
):
|
||||
raise cv.Invalid(
|
||||
f"{CONF_WIFI} {CONF_POWER_SAVE_MODE} must be set to 'none' when using Zigbee proxy"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ZigbeeProxy),
|
||||
cv.Optional(CONF_BUFFER_SIZE): cv.SplitDefault(
|
||||
cv.int_range(min=256, max=2048),
|
||||
esp8266=512,
|
||||
default=1024,
|
||||
),
|
||||
cv.Optional(CONF_INITIAL_TIMEOUT, default=1600): cv.int_range(
|
||||
min=100, max=10000
|
||||
),
|
||||
cv.Optional(CONF_MIN_TIMEOUT, default=400): cv.int_range(min=100, max=5000),
|
||||
cv.Optional(CONF_MAX_TIMEOUT, default=3200): cv.int_range(
|
||||
min=500, max=10000
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(uart.UART_DEVICE_SCHEMA),
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
cg.add_define("USE_ZIGBEE_PROXY")
|
||||
|
||||
# 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])
|
||||
|
||||
# Set timeout values
|
||||
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]))
|
||||
@@ -0,0 +1,413 @@
|
||||
#include "zigbee_proxy.h"
|
||||
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace zigbee_proxy {
|
||||
|
||||
static const char *const TAG = "zigbee_proxy";
|
||||
|
||||
// CRC-CCITT lookup table for polynomial 0x1021 (x^16 + x^12 + x^5 + 1)
|
||||
static const uint16_t CRC_TABLE[256] = {
|
||||
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD,
|
||||
0xE1CE, 0xF1EF, 0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6, 0x9339, 0x8318, 0xB37B, 0xA35A,
|
||||
0xD3BD, 0xC39C, 0xF3FF, 0xE3DE, 0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485, 0xA56A, 0xB54B,
|
||||
0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D, 0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4,
|
||||
0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC, 0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861,
|
||||
0x2802, 0x3823, 0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B, 0x5AF5, 0x4AD4, 0x7AB7, 0x6A96,
|
||||
0x1A71, 0x0A50, 0x3A33, 0x2A12, 0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A, 0x6CA6, 0x7C87,
|
||||
0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41, 0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49,
|
||||
0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70, 0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A,
|
||||
0x9F59, 0x8F78, 0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F, 0x1080, 0x00A1, 0x30C2, 0x20E3,
|
||||
0x5004, 0x4025, 0x7046, 0x6067, 0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E, 0x02B1, 0x1290,
|
||||
0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256, 0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D,
|
||||
0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E,
|
||||
0xC71D, 0xD73C, 0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634, 0xD94C, 0xC96D, 0xF90E, 0xE92F,
|
||||
0x99C8, 0x89E9, 0xB98A, 0xA9AB, 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3, 0xCB7D, 0xDB5C,
|
||||
0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A, 0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92,
|
||||
0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9, 0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83,
|
||||
0x1CE0, 0x0CC1, 0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8, 0x6E17, 0x7E36, 0x4E55, 0x5E74,
|
||||
0x2E93, 0x3EB2, 0x0ED1, 0x1EF0};
|
||||
|
||||
uint16_t ZigbeeProxy::calculate_crc_(const uint8_t *data, size_t length) {
|
||||
uint16_t crc = ASH_CRC_INIT;
|
||||
for (size_t i = 0; i < length; i++) {
|
||||
crc = (crc << 8) ^ CRC_TABLE[(crc >> 8) ^ data[i]];
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
bool ZigbeeProxy::validate_frame_crc_() {
|
||||
// CRC is calculated over control byte + data
|
||||
// rx_buffer_[0] contains control byte, rx_buffer_[1..rx_buffer_index_-3] contains data
|
||||
// rx_buffer_[rx_buffer_index_-2] and rx_buffer_[rx_buffer_index_-1] contain CRC
|
||||
if (this->rx_buffer_index_ < 3) {
|
||||
// Frame too short to contain CRC
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate CRC over control + data (exclude CRC bytes)
|
||||
uint16_t calculated = this->calculate_crc_(this->rx_buffer_.data(), this->rx_buffer_index_ - 2);
|
||||
|
||||
// Extract received CRC (big-endian)
|
||||
uint16_t received = (static_cast<uint16_t>(this->rx_buffer_[this->rx_buffer_index_ - 2]) << 8) |
|
||||
this->rx_buffer_[this->rx_buffer_index_ - 1];
|
||||
|
||||
if (calculated != received) {
|
||||
ESP_LOGW(TAG, "CRC validation failed: calculated=0x%04X, received=0x%04X", calculated, received);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ZigbeeProxy::parse_control_byte_(uint8_t control) {
|
||||
// Decode frame type based on bit patterns:
|
||||
// DATA: 0xxxxxxx (bit 7 = 0)
|
||||
// ACK: 10x0xxxx (bits 7-6 = 10, bit 5 = 0)
|
||||
// NAK: 10x1xxxx (bits 7-6 = 10, bit 5 = 1)
|
||||
// RST: 11000000 (0xC0)
|
||||
// RSTACK: 11000001 (0xC1)
|
||||
// ERROR: 11000010 (0xC2)
|
||||
|
||||
AshFrameType frame_type;
|
||||
if ((control & 0x80) == 0) {
|
||||
// Bit 7 = 0: DATA frame
|
||||
frame_type = AshFrameType::DATA;
|
||||
} else if ((control & 0xC0) == 0x80) {
|
||||
// Bits 7-6 = 10: ACK or NAK
|
||||
// ACK format: 100nrPPP (bit 5 = 0)
|
||||
// NAK format: 101nrPPP (bit 5 = 1)
|
||||
if ((control & 0x20) == 0) {
|
||||
frame_type = AshFrameType::ACK;
|
||||
} else {
|
||||
frame_type = AshFrameType::NAK;
|
||||
}
|
||||
} else {
|
||||
// Bits 7-6 = 11: control frames (RST, RSTACK, ERROR)
|
||||
uint8_t control_bits = control & 0x07;
|
||||
if (control_bits == 0x00) {
|
||||
frame_type = AshFrameType::RST;
|
||||
} else if (control_bits == 0x01) {
|
||||
frame_type = AshFrameType::RSTACK;
|
||||
} else if (control_bits == 0x02) {
|
||||
frame_type = AshFrameType::ERROR;
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Unknown control frame type: 0x%02X", control);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract sequence numbers from DATA frame format: 0ffrPPPP
|
||||
// Bits 6-4 = frmNum, bit 3 = reTx, bits 2-0 = ackNum
|
||||
uint8_t frame_num = (control >> 4) & 0x07; // Bits 6-4
|
||||
uint8_t ack_num = control & 0x07; // Bits 2-0
|
||||
bool retx = (control & 0x08) != 0; // Bit 3 (for DATA frames)
|
||||
|
||||
ESP_LOGV(TAG, "Parsed control byte: type=%d, frmNum=%d, ackNum=%d, reTx=%d", static_cast<int>(frame_type), frame_num,
|
||||
ack_num, retx);
|
||||
|
||||
// Handle frame based on type
|
||||
switch (frame_type) {
|
||||
case AshFrameType::DATA: {
|
||||
// Check sequence number
|
||||
if (frame_num != this->rx_sequence_) {
|
||||
ESP_LOGW(TAG, "Out of sequence DATA frame: expected %d, got %d", this->rx_sequence_, frame_num);
|
||||
this->send_nak_frame_(this->rx_sequence_);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for ACK in DATA frame (piggybacked ACK) BEFORE processing
|
||||
// This must happen first because the handler may send new frames
|
||||
if (this->tx_buffer_pending_ && ack_num == ((this->tx_pending_frame_num_ + 1) & ASH_MAX_SEQUENCE)) {
|
||||
// ackNum means "I expect frame N next" = "I received up to N-1"
|
||||
// So if ackNum == pending+1, our pending frame was received
|
||||
uint32_t rtt = millis() - this->ack_timer_start_;
|
||||
this->update_adaptive_timeout_(rtt);
|
||||
this->clear_tx_buffer_();
|
||||
ESP_LOGD(TAG, "ACK received (piggybacked in DATA), RTT: %u ms", rtt);
|
||||
}
|
||||
|
||||
// Send ACK immediately
|
||||
this->send_ack_frame_(frame_num);
|
||||
|
||||
// Increment RX sequence for next frame
|
||||
this->increment_rx_sequence_();
|
||||
|
||||
// Extract payload (skip control byte, exclude CRC)
|
||||
size_t payload_length = this->rx_buffer_index_ > 3 ? this->rx_buffer_index_ - 3 : 0;
|
||||
const uint8_t *payload = this->rx_buffer_.data() + 1;
|
||||
|
||||
// During boot sequence, route to boot handler
|
||||
if (this->boot_sequence_active_ && payload_length > 0) {
|
||||
this->handle_boot_data_frame_(payload, payload_length);
|
||||
} else if (this->api_connection_ != nullptr && payload_length > 0) {
|
||||
// Forward EZSP payload to API client
|
||||
this->outgoing_proto_msg_.data = payload;
|
||||
this->outgoing_proto_msg_.data_len = payload_length;
|
||||
this->api_connection_->send_zigbee_proxy_frame(this->outgoing_proto_msg_);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case AshFrameType::ACK:
|
||||
// Check if this ACKs our pending frame
|
||||
// ackNum means "I expect frame N next" = "I received all frames up to N-1"
|
||||
// So if ackNum == pending+1, our pending frame was acknowledged
|
||||
if (this->tx_buffer_pending_ && ack_num == ((this->tx_pending_frame_num_ + 1) & ASH_MAX_SEQUENCE)) {
|
||||
uint32_t rtt = millis() - this->ack_timer_start_;
|
||||
this->update_adaptive_timeout_(rtt);
|
||||
this->clear_tx_buffer_();
|
||||
ESP_LOGD(TAG, "ACK received for frame %d, RTT: %u ms", this->tx_pending_frame_num_, rtt);
|
||||
}
|
||||
break;
|
||||
|
||||
case AshFrameType::NAK:
|
||||
ESP_LOGW(TAG, "NAK received for frame %d, retransmitting", ack_num);
|
||||
if (this->tx_buffer_pending_) {
|
||||
this->handle_retransmission_();
|
||||
}
|
||||
break;
|
||||
|
||||
case AshFrameType::RST: {
|
||||
ESP_LOGW(TAG, "Received RST frame from NCP, sending RSTACK");
|
||||
// Send RSTACK response
|
||||
uint8_t rstack_data[] = {0x02, 0x01, 0x00}; // RSTACK with reset code
|
||||
this->handle_rstack_frame_(rstack_data, sizeof(rstack_data));
|
||||
break;
|
||||
}
|
||||
|
||||
case AshFrameType::RSTACK:
|
||||
this->handle_rstack_frame_(this->rx_buffer_.data() + 1, this->rx_buffer_index_ - 3);
|
||||
break;
|
||||
|
||||
case AshFrameType::ERROR:
|
||||
this->handle_error_frame_(this->rx_buffer_.data() + 1, this->rx_buffer_index_ - 3);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool ZigbeeProxy::parse_byte_(uint8_t byte) {
|
||||
// ASH_CAN (0x1A) resets the parser state - discard any partial frame
|
||||
static constexpr uint8_t ASH_CAN_BYTE = 0x1A;
|
||||
if (byte == ASH_CAN_BYTE) {
|
||||
this->rx_buffer_index_ = 0;
|
||||
this->escape_next_byte_ = false;
|
||||
this->parsing_state_ = ParsingState::WAIT_FLAG_START;
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (this->parsing_state_) {
|
||||
case ParsingState::WAIT_FLAG_START:
|
||||
// Handle escape sequences - NCP may send escaped control byte at frame start
|
||||
if (byte == ASH_ESCAPE_BYTE) {
|
||||
this->escape_next_byte_ = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this->escape_next_byte_) {
|
||||
byte ^= ASH_XOR_BYTE;
|
||||
this->escape_next_byte_ = false;
|
||||
// After unescaping, check if it's a CAN byte (0x1A)
|
||||
if (byte == ASH_CAN_BYTE) {
|
||||
this->rx_buffer_index_ = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (byte == ASH_FLAG_BYTE) {
|
||||
// Start of frame with FLAG delimiter
|
||||
this->rx_buffer_index_ = 0;
|
||||
this->escape_next_byte_ = false;
|
||||
this->parsing_state_ = ParsingState::WAIT_CONTROL;
|
||||
ESP_LOGV(TAG, "Frame start detected (FLAG)");
|
||||
} else if (this->ash_state_ == AshState::CONNECTED) {
|
||||
// When connected, NCP often omits leading FLAG on responses
|
||||
// Any byte could be a control byte:
|
||||
// - DATA frames: 0x00-0x7F (bit 7 = 0)
|
||||
// - ACK frames: 0x80-0x9F (bits 7-6 = 10, bit 5 = 0)
|
||||
// - NAK frames: 0xA0-0xBF (bits 7-6 = 10, bit 5 = 1)
|
||||
// - RST/RSTACK/ERROR: 0xC0-0xC2 (bits 7-6 = 11)
|
||||
// Skip reserved bytes that cannot be valid control bytes
|
||||
if (byte != 0x11 && byte != 0x13) {
|
||||
this->rx_buffer_index_ = 0;
|
||||
this->rx_buffer_[this->rx_buffer_index_++] = byte;
|
||||
this->parsing_state_ = ParsingState::WAIT_DATA;
|
||||
ESP_LOGV(TAG, "Frame start detected (control byte 0x%02X)", byte);
|
||||
}
|
||||
} else if ((byte & 0x80) != 0) {
|
||||
// Before connected, only accept control/management frames (bit 7 set)
|
||||
// This handles RSTACK (0xC1), ACK (0x8X), NAK (0xAX), ERROR (0xC2)
|
||||
this->rx_buffer_index_ = 0;
|
||||
this->rx_buffer_[this->rx_buffer_index_++] = byte;
|
||||
this->parsing_state_ = ParsingState::WAIT_DATA;
|
||||
ESP_LOGV(TAG, "Frame start detected (control byte 0x%02X)", byte);
|
||||
}
|
||||
// Check for bootloader patterns
|
||||
this->check_bootloader_mode_(&byte, 1);
|
||||
break;
|
||||
|
||||
case ParsingState::WAIT_CONTROL:
|
||||
if (byte == ASH_FLAG_BYTE) {
|
||||
// Empty frame or repeated FLAG
|
||||
ESP_LOGV(TAG, "Empty frame or repeated FLAG, restarting");
|
||||
this->rx_buffer_index_ = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (byte == ASH_ESCAPE_BYTE) {
|
||||
this->escape_next_byte_ = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this->escape_next_byte_) {
|
||||
byte ^= ASH_XOR_BYTE;
|
||||
this->escape_next_byte_ = false;
|
||||
}
|
||||
|
||||
// Store control byte
|
||||
this->rx_buffer_[this->rx_buffer_index_++] = byte;
|
||||
this->parsing_state_ = ParsingState::WAIT_DATA;
|
||||
break;
|
||||
|
||||
case ParsingState::WAIT_DATA:
|
||||
if (byte == ASH_FLAG_BYTE) {
|
||||
// End of frame - validate and process
|
||||
ESP_LOGV(TAG, "Frame complete, %u bytes in buffer", this->rx_buffer_index_);
|
||||
if (this->validate_frame_crc_()) {
|
||||
this->parse_control_byte_(this->rx_buffer_[0]);
|
||||
} else {
|
||||
// CRC failed - log frame contents for debugging
|
||||
ESP_LOGW(TAG, "CRC failed, frame (%u bytes): %s", this->rx_buffer_index_,
|
||||
format_hex_pretty(this->rx_buffer_.data(), this->rx_buffer_index_).c_str());
|
||||
this->send_nak_frame_(this->rx_sequence_);
|
||||
}
|
||||
this->parsing_state_ = ParsingState::WAIT_FLAG_START;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (byte == ASH_ESCAPE_BYTE) {
|
||||
this->escape_next_byte_ = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this->escape_next_byte_) {
|
||||
byte ^= ASH_XOR_BYTE;
|
||||
this->escape_next_byte_ = false;
|
||||
}
|
||||
|
||||
// Check buffer overflow
|
||||
if (this->rx_buffer_index_ >= MAX_ASH_FRAME_SIZE) {
|
||||
ESP_LOGE(TAG, "RX buffer overflow, frame too large");
|
||||
this->parsing_state_ = ParsingState::WAIT_FLAG_START;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Store data byte
|
||||
this->rx_buffer_[this->rx_buffer_index_++] = byte;
|
||||
break;
|
||||
|
||||
default:
|
||||
this->parsing_state_ = ParsingState::WAIT_FLAG_START;
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t ZigbeeProxy::build_frame_(uint8_t *output, const uint8_t *data, size_t length, AshFrameType type,
|
||||
uint8_t frame_num, uint8_t ack_num, bool retx) {
|
||||
size_t pos = 0;
|
||||
|
||||
// Start with FLAG
|
||||
output[pos++] = ASH_FLAG_BYTE;
|
||||
|
||||
// Build control byte
|
||||
uint8_t control = 0;
|
||||
switch (type) {
|
||||
case AshFrameType::DATA:
|
||||
// DATA frame format: 0ffrPPPP
|
||||
// Bit 7 = 0 (DATA indicator), bits 6-4 = frmNum, bit 3 = reTx, bits 2-0 = ackNum
|
||||
control = (frame_num << 4) | (retx ? 0x08 : 0x00) | ack_num;
|
||||
break;
|
||||
case AshFrameType::ACK:
|
||||
control = 0x80 | ack_num;
|
||||
break;
|
||||
case AshFrameType::NAK:
|
||||
control = 0xA0 | ack_num;
|
||||
break;
|
||||
case AshFrameType::RST:
|
||||
control = 0xC0;
|
||||
break;
|
||||
case AshFrameType::RSTACK:
|
||||
control = 0xC1;
|
||||
break;
|
||||
case AshFrameType::ERROR:
|
||||
control = 0xC2;
|
||||
break;
|
||||
}
|
||||
|
||||
// Add control byte with stuffing
|
||||
if (control == ASH_FLAG_BYTE || control == ASH_ESCAPE_BYTE || control == 0x11 || control == 0x13 || control == 0x93 ||
|
||||
control == 0xA3) {
|
||||
output[pos++] = ASH_ESCAPE_BYTE;
|
||||
output[pos++] = control ^ ASH_XOR_BYTE;
|
||||
} else {
|
||||
output[pos++] = control;
|
||||
}
|
||||
|
||||
// Prepare CRC calculation buffer (control + data)
|
||||
uint8_t crc_buffer[MAX_ASH_FRAME_SIZE];
|
||||
crc_buffer[0] = control;
|
||||
if (length > 0) {
|
||||
memcpy(crc_buffer + 1, data, length);
|
||||
}
|
||||
|
||||
// Add data payload with stuffing
|
||||
for (size_t i = 0; i < length; i++) {
|
||||
uint8_t byte = data[i];
|
||||
if (byte == ASH_FLAG_BYTE || byte == ASH_ESCAPE_BYTE || byte == 0x11 || byte == 0x13 || byte == 0x93 ||
|
||||
byte == 0xA3) {
|
||||
output[pos++] = ASH_ESCAPE_BYTE;
|
||||
output[pos++] = byte ^ ASH_XOR_BYTE;
|
||||
} else {
|
||||
output[pos++] = byte;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate CRC
|
||||
uint16_t crc = this->calculate_crc_(crc_buffer, 1 + length);
|
||||
|
||||
// Add CRC with stuffing (big-endian)
|
||||
uint8_t crc_high = (crc >> 8) & 0xFF;
|
||||
uint8_t crc_low = crc & 0xFF;
|
||||
|
||||
if (crc_high == ASH_FLAG_BYTE || crc_high == ASH_ESCAPE_BYTE || crc_high == 0x11 || crc_high == 0x13 ||
|
||||
crc_high == 0x93 || crc_high == 0xA3) {
|
||||
output[pos++] = ASH_ESCAPE_BYTE;
|
||||
output[pos++] = crc_high ^ ASH_XOR_BYTE;
|
||||
} else {
|
||||
output[pos++] = crc_high;
|
||||
}
|
||||
|
||||
if (crc_low == ASH_FLAG_BYTE || crc_low == ASH_ESCAPE_BYTE || crc_low == 0x11 || crc_low == 0x13 || crc_low == 0x93 ||
|
||||
crc_low == 0xA3) {
|
||||
output[pos++] = ASH_ESCAPE_BYTE;
|
||||
output[pos++] = crc_low ^ ASH_XOR_BYTE;
|
||||
} else {
|
||||
output[pos++] = crc_low;
|
||||
}
|
||||
|
||||
// End with FLAG
|
||||
output[pos++] = ASH_FLAG_BYTE;
|
||||
|
||||
return pos;
|
||||
}
|
||||
|
||||
} // namespace zigbee_proxy
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ZIGBEE_PROXY
|
||||
@@ -0,0 +1,90 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
|
||||
namespace esphome {
|
||||
namespace zigbee_proxy {
|
||||
|
||||
// ASH Protocol Constants
|
||||
static constexpr uint8_t ASH_FLAG_BYTE = 0x7E; // Frame delimiter
|
||||
static constexpr uint8_t ASH_ESCAPE_BYTE = 0x7D; // Escape/substitution byte
|
||||
static constexpr uint8_t ASH_XOR_BYTE = 0x20; // XOR mask for escaped bytes
|
||||
static constexpr uint8_t ASH_SUBSTITUTE_BYTE = 0x18; // Substitution for invalid bytes
|
||||
|
||||
// Reserved bytes that must be escaped
|
||||
static constexpr uint8_t ASH_RESERVED_BYTES[] = {0x7E, 0x7D, 0x11, 0x13, 0x93, 0xA3};
|
||||
|
||||
// Buffer size configuration
|
||||
#ifdef ZIGBEE_PROXY_BUFFER_SIZE
|
||||
static constexpr size_t MAX_ASH_FRAME_SIZE = ZIGBEE_PROXY_BUFFER_SIZE;
|
||||
#else
|
||||
#ifdef USE_ESP8266
|
||||
static constexpr size_t MAX_ASH_FRAME_SIZE = 512; // Limited RAM on ESP8266
|
||||
#else
|
||||
static constexpr size_t MAX_ASH_FRAME_SIZE = 1024; // Full buffer on ESP32/RP2040
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// Protocol limits
|
||||
static constexpr uint8_t ASH_MAX_SEQUENCE = 7; // 3-bit sequence number (0-7)
|
||||
static constexpr uint8_t ASH_TX_WINDOW_SIZE = 1; // Only 1 unacknowledged frame allowed
|
||||
static constexpr uint8_t ASH_MAX_RETRIES = 5; // Maximum retransmission attempts
|
||||
static constexpr uint16_t ASH_CRC_INIT = 0xFFFF; // CRC-CCITT initial value
|
||||
static constexpr uint32_t ASH_RESET_TIMEOUT = 3000; // RST/RSTACK timeout in milliseconds
|
||||
|
||||
// IEEE address size
|
||||
static constexpr size_t ZIGBEE_IEEE_ADDR_SIZE = 8; // 64-bit IEEE address
|
||||
|
||||
// ASH Frame Types (encoded in control byte)
|
||||
// DATA format: 0ffrPPPP - bit 7=0, bits 6-4=frmNum, bit 3=reTx, bits 2-0=ackNum
|
||||
// ACK/NAK format: 10XnrPPP - bit 5 distinguishes ACK(0) from NAK(1)
|
||||
enum class AshFrameType : uint8_t {
|
||||
DATA = 0x00, // Data frame (bit 7 = 0)
|
||||
ACK = 0x80, // Acknowledge frame (100nrPPP, bit 5 = 0)
|
||||
NAK = 0xA0, // Negative acknowledge (101nrPPP, bit 5 = 1)
|
||||
RST = 0xC0, // Reset request (bits 7-6 = 11, bits 2-0 = 000)
|
||||
RSTACK = 0xC1, // Reset acknowledgment (bits 7-6 = 11, bits 2-0 = 001)
|
||||
ERROR = 0xC2, // Error indication (bits 7-6 = 11, bits 2-0 = 010)
|
||||
};
|
||||
|
||||
// ASH Connection State
|
||||
enum class AshState : uint8_t {
|
||||
DISCONNECTED, // Initial state, no connection
|
||||
CONNECTING, // Sent RST, waiting for RSTACK
|
||||
CONNECTED, // Normal operation
|
||||
FAILED, // Too many errors/timeouts, requires reset
|
||||
};
|
||||
|
||||
// Frame Parsing State Machine
|
||||
enum class ParsingState : uint8_t {
|
||||
WAIT_FLAG_START, // Looking for frame start FLAG (0x7E)
|
||||
WAIT_CONTROL, // Reading control byte
|
||||
WAIT_DATA, // Reading data payload
|
||||
WAIT_CRC_HIGH, // Reading CRC high byte
|
||||
WAIT_CRC_LOW, // Reading CRC low byte
|
||||
WAIT_FLAG_END, // Expecting end FLAG (0x7E)
|
||||
};
|
||||
|
||||
// Bootloader detection states
|
||||
enum class BootloaderState : uint8_t {
|
||||
NORMAL, // Normal operation
|
||||
DETECTED, // Bootloader mode detected
|
||||
MENU, // In bootloader menu
|
||||
};
|
||||
|
||||
// EZSP Error Codes (from ERROR frame)
|
||||
enum class EzspError : uint8_t {
|
||||
VERSION_NOT_SET = 0x00,
|
||||
RESET_UNKNOWN = 0x01,
|
||||
RESET_EXTERNAL = 0x02,
|
||||
RESET_POWER_ON = 0x03,
|
||||
RESET_WATCHDOG = 0x04,
|
||||
RESET_ASSERT = 0x05,
|
||||
RESET_BOOTLOADER = 0x06,
|
||||
RESET_SOFTWARE = 0x07,
|
||||
EXCEEDED_MAXIMUM_ACK_TIMEOUT_COUNT = 0x51,
|
||||
};
|
||||
|
||||
} // namespace zigbee_proxy
|
||||
} // namespace esphome
|
||||
@@ -0,0 +1,58 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome {
|
||||
namespace zigbee_proxy {
|
||||
|
||||
// EZSP Protocol Versions
|
||||
static constexpr uint8_t EZSP_MIN_VERSION = 8; // Minimum supported version
|
||||
static constexpr uint8_t EZSP_MAX_VERSION = 13; // Maximum version we request
|
||||
|
||||
// EZSP Frame Control bits
|
||||
static constexpr uint8_t EZSP_FRAME_CONTROL_COMMAND = 0x00; // Host to NCP
|
||||
static constexpr uint8_t EZSP_FRAME_CONTROL_RESPONSE = 0x80; // NCP to Host
|
||||
static constexpr uint8_t EZSP_FRAME_CONTROL_CALLBACK = 0x90; // Async callback from NCP
|
||||
|
||||
// Legacy EZSP frame format (v4-v7): [sequence] [frame_control] [frame_id]
|
||||
// Extended EZSP frame format (v8+): [sequence] [frame_control_low] [frame_control_high] [frame_id_low] [frame_id_high]
|
||||
|
||||
// EZSP Frame IDs - Commands (host to NCP)
|
||||
static constexpr uint16_t EZSP_VERSION = 0x0000; // Version negotiation
|
||||
static constexpr uint16_t EZSP_NETWORK_INIT = 0x0017; // Initialize network
|
||||
static constexpr uint16_t EZSP_NETWORK_STATE = 0x0018; // Get network state
|
||||
static constexpr uint16_t EZSP_GET_EUI64 = 0x0026; // Get IEEE address
|
||||
static constexpr uint16_t EZSP_GET_NETWORK_PARAMETERS = 0x0028; // Get network parameters
|
||||
|
||||
// EZSP Frame IDs - Callbacks (NCP to host, async)
|
||||
static constexpr uint16_t EZSP_STACK_STATUS_HANDLER = 0x0019; // Stack status callback
|
||||
|
||||
// EZSP Network Status
|
||||
enum class EzspNetworkStatus : uint8_t {
|
||||
NO_NETWORK = 0x00,
|
||||
JOINING_NETWORK = 0x01,
|
||||
JOINED_NETWORK = 0x02,
|
||||
JOINED_NETWORK_NO_PARENT = 0x03,
|
||||
LEAVING_NETWORK = 0x04,
|
||||
};
|
||||
|
||||
// Ember Status codes (subset)
|
||||
enum class EmberStatus : uint8_t {
|
||||
SUCCESS = 0x00,
|
||||
NETWORK_UP = 0x90,
|
||||
NETWORK_DOWN = 0x91,
|
||||
NOT_JOINED = 0x93,
|
||||
};
|
||||
|
||||
// Network parameters structure offsets (in getNetworkParameters response)
|
||||
// Response format: [status] [nodeType] [parameters...]
|
||||
// Parameters: [extendedPanId (8)] [panId (2)] [radioTxPower] [radioChannel] [joinMethod] ...
|
||||
static constexpr size_t NETWORK_PARAMS_STATUS_OFFSET = 0;
|
||||
static constexpr size_t NETWORK_PARAMS_NODE_TYPE_OFFSET = 1;
|
||||
static constexpr size_t NETWORK_PARAMS_EXT_PAN_ID_OFFSET = 2;
|
||||
static constexpr size_t NETWORK_PARAMS_PAN_ID_OFFSET = 10;
|
||||
static constexpr size_t NETWORK_PARAMS_CHANNEL_OFFSET = 13;
|
||||
|
||||
} // namespace zigbee_proxy
|
||||
} // namespace esphome
|
||||
@@ -0,0 +1,866 @@
|
||||
#include "zigbee_proxy.h"
|
||||
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/components/api/api_server.h"
|
||||
#include "ezsp_commands.h"
|
||||
|
||||
#ifdef USE_WIFI
|
||||
#include "esphome/components/wifi/wifi_component.h"
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace zigbee_proxy {
|
||||
|
||||
static const char *const TAG = "zigbee_proxy";
|
||||
|
||||
ZigbeeProxy *global_zigbee_proxy = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
ZigbeeProxy::ZigbeeProxy() { global_zigbee_proxy = this; }
|
||||
|
||||
void ZigbeeProxy::setup() {
|
||||
this->setup_time_ = millis();
|
||||
|
||||
// Initialize state
|
||||
this->ash_state_ = AshState::DISCONNECTED;
|
||||
this->parsing_state_ = ParsingState::WAIT_FLAG_START;
|
||||
this->tx_sequence_ = 0;
|
||||
this->rx_sequence_ = 0;
|
||||
|
||||
// Send RST frame to initialize NCP
|
||||
this->reset_ash_protocol_();
|
||||
}
|
||||
|
||||
void ZigbeeProxy::loop() {
|
||||
// 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_();
|
||||
}
|
||||
|
||||
// Check for boot sequence timeout
|
||||
if (this->boot_sequence_active_) {
|
||||
uint32_t elapsed = millis() - this->setup_time_;
|
||||
if (elapsed > 10000) { // 10 second timeout for entire boot sequence
|
||||
ESP_LOGE(TAG, "Boot sequence timeout (state: %d)", static_cast<int>(this->boot_state_));
|
||||
this->boot_state_ = BootState::FAILED;
|
||||
this->boot_sequence_active_ = false;
|
||||
// Still mark as connected so proxy can work without network info
|
||||
if (this->ash_state_ != AshState::CONNECTED) {
|
||||
this->ash_state_ = AshState::FAILED;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we're stuck in CONNECTING state (before boot sequence starts)
|
||||
if (this->ash_state_ == AshState::CONNECTING && !this->boot_sequence_active_) {
|
||||
if (millis() - this->setup_time_ > ASH_RESET_TIMEOUT) {
|
||||
ESP_LOGE(TAG, "RSTACK timeout, NCP not responding");
|
||||
this->ash_state_ = AshState::FAILED;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ZigbeeProxy::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Zigbee Proxy:\n"
|
||||
" Buffer Size: %u bytes\n"
|
||||
" Initial Timeout: %u ms\n"
|
||||
" Min Timeout: %u ms\n"
|
||||
" Max Timeout: %u ms",
|
||||
MAX_ASH_FRAME_SIZE, this->timeout_config_.initial_timeout_ms, this->timeout_config_.min_timeout_ms,
|
||||
this->timeout_config_.max_timeout_ms);
|
||||
|
||||
if (this->network_info_.valid) {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" IEEE Address: %02X:%02X:%02X:%02X:%02X:%02X:%02X:%02X\n"
|
||||
" PAN ID: 0x%04X\n"
|
||||
" Channel: %u",
|
||||
this->network_info_.ieee_address[7], this->network_info_.ieee_address[6],
|
||||
this->network_info_.ieee_address[5], this->network_info_.ieee_address[4],
|
||||
this->network_info_.ieee_address[3], this->network_info_.ieee_address[2],
|
||||
this->network_info_.ieee_address[1], this->network_info_.ieee_address[0], this->network_info_.pan_id,
|
||||
this->network_info_.channel);
|
||||
}
|
||||
|
||||
if (this->ash_state_ == AshState::FAILED) {
|
||||
ESP_LOGCONFIG(TAG, " Status: Failed (NCP communication error)");
|
||||
} else if (this->ash_state_ == AshState::CONNECTED) {
|
||||
ESP_LOGCONFIG(TAG, " Status: Connected");
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Status: Connecting...");
|
||||
}
|
||||
}
|
||||
|
||||
float ZigbeeProxy::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
|
||||
|
||||
bool ZigbeeProxy::can_proceed() { return this->ash_state_ == AshState::CONNECTED; }
|
||||
|
||||
void ZigbeeProxy::api_connection_authenticated(api::APIConnection *conn) {
|
||||
// Notify client of network info if available
|
||||
if (this->network_info_.valid) {
|
||||
this->send_network_info_changed_msg_(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) {
|
||||
ESP_LOGW(TAG, "Another client is already subscribed");
|
||||
return;
|
||||
}
|
||||
ESP_LOGI(TAG, "Client subscribed");
|
||||
this->api_connection_ = api_connection;
|
||||
break;
|
||||
|
||||
case api::enums::ZIGBEE_PROXY_REQUEST_TYPE_UNSUBSCRIBE:
|
||||
if (this->api_connection_ == api_connection) {
|
||||
ESP_LOGI(TAG, "Client unsubscribed");
|
||||
this->api_connection_ = nullptr;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unknown request type: %d", static_cast<int>(msg.type));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (this->ash_state_ != AshState::CONNECTED) {
|
||||
ESP_LOGW(TAG, "Cannot send frame, NCP not connected");
|
||||
return;
|
||||
}
|
||||
|
||||
// Forward EZSP payload to NCP
|
||||
this->send_frame(msg.data, msg.data_len);
|
||||
}
|
||||
|
||||
uint64_t ZigbeeProxy::get_ieee_address() const {
|
||||
uint64_t addr = 0;
|
||||
for (size_t i = 0; i < ZIGBEE_IEEE_ADDR_SIZE; i++) {
|
||||
addr |= static_cast<uint64_t>(this->network_info_.ieee_address[i]) << (i * 8);
|
||||
}
|
||||
return addr;
|
||||
}
|
||||
|
||||
void ZigbeeProxy::send_frame(const uint8_t *data, size_t length) {
|
||||
if (this->ash_state_ != AshState::CONNECTED) {
|
||||
ESP_LOGW(TAG, "Cannot send frame, not connected");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->tx_buffer_pending_) {
|
||||
ESP_LOGW(TAG, "Cannot send frame, previous frame still pending");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate frame size
|
||||
if (length + 4 > MAX_ASH_FRAME_SIZE) { // +4 for control, CRC, FLAGS
|
||||
ESP_LOGE(TAG, "Frame too large: %u bytes (max %u)", length, MAX_ASH_FRAME_SIZE - 4);
|
||||
return;
|
||||
}
|
||||
|
||||
this->send_data_frame_(data, length, false);
|
||||
}
|
||||
|
||||
void ZigbeeProxy::set_timeout_config(uint32_t initial_ms, uint32_t min_ms, uint32_t max_ms) {
|
||||
this->timeout_config_.initial_timeout_ms = initial_ms;
|
||||
this->timeout_config_.min_timeout_ms = min_ms;
|
||||
this->timeout_config_.max_timeout_ms = max_ms;
|
||||
this->timeout_config_.current_timeout_ms = initial_ms;
|
||||
ESP_LOGI(TAG, "Timeout config updated: initial=%u, min=%u, max=%u", initial_ms, min_ms, max_ms);
|
||||
}
|
||||
|
||||
// ASH Protocol State Machine
|
||||
void ZigbeeProxy::reset_ash_protocol_() {
|
||||
ESP_LOGI(TAG, "Resetting ASH protocol");
|
||||
this->ash_state_ = AshState::CONNECTING;
|
||||
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->setup_time_ = millis();
|
||||
|
||||
// Start boot sequence
|
||||
this->boot_state_ = BootState::WAIT_RSTACK;
|
||||
this->boot_sequence_active_ = true;
|
||||
this->ezsp_sequence_ = 0;
|
||||
|
||||
this->send_rst_frame_();
|
||||
}
|
||||
|
||||
void ZigbeeProxy::send_rst_frame_() {
|
||||
// Send CANCEL bytes (0x1A) to clear NCP's receive buffer before RST
|
||||
// This is required by ASH protocol to ensure NCP ignores any partial frames
|
||||
// Note: 0x1A is ASH_CAN (cancel), NOT 0x18 which is ASH_SUB (substitute)
|
||||
static constexpr uint8_t ASH_CAN_BYTE = 0x1A;
|
||||
for (int i = 0; i < 32; i++) {
|
||||
this->write_byte(ASH_CAN_BYTE);
|
||||
}
|
||||
this->flush();
|
||||
|
||||
// Small delay to allow NCP to process CANCEL bytes
|
||||
delay(10);
|
||||
|
||||
// Now send the RST frame
|
||||
uint8_t frame[8];
|
||||
size_t length = this->build_frame_(frame, nullptr, 0, AshFrameType::RST);
|
||||
|
||||
// Debug: log exact bytes being sent
|
||||
ESP_LOGV(TAG, "RST frame bytes (%u): %s", length, format_hex_pretty(frame, length).c_str());
|
||||
|
||||
this->write_array(frame, length);
|
||||
this->flush();
|
||||
ESP_LOGD(TAG, "Sent RST frame (with 32 CAN bytes prefix)");
|
||||
}
|
||||
|
||||
void ZigbeeProxy::handle_rstack_frame_(const uint8_t *data, size_t length) {
|
||||
// Reset sequence numbers on any RSTACK
|
||||
this->tx_sequence_ = 0;
|
||||
this->rx_sequence_ = 0;
|
||||
this->clear_tx_buffer_();
|
||||
|
||||
if (this->boot_state_ == BootState::WAIT_RSTACK) {
|
||||
// Initial RSTACK - start boot sequence
|
||||
ESP_LOGI(TAG, "Received RSTACK, starting EZSP initialization");
|
||||
this->ash_state_ = AshState::CONNECTED;
|
||||
|
||||
// Small delay to let NCP settle and clear any pending data
|
||||
delay(50);
|
||||
|
||||
// Drain any garbage bytes from UART buffer
|
||||
while (this->available()) {
|
||||
uint8_t byte;
|
||||
this->read_byte(&byte);
|
||||
ESP_LOGV(TAG, "Draining post-RSTACK byte: 0x%02X", byte);
|
||||
}
|
||||
|
||||
this->boot_state_ = BootState::SEND_VERSION;
|
||||
this->advance_boot_state_();
|
||||
} else if (this->boot_state_ == BootState::WAIT_FINAL_RSTACK) {
|
||||
// Final RSTACK after harvesting - boot complete
|
||||
ESP_LOGI(TAG, "Boot sequence complete, NCP reset to clean state");
|
||||
this->ash_state_ = AshState::CONNECTED;
|
||||
this->boot_state_ = BootState::COMPLETE;
|
||||
this->boot_sequence_active_ = false;
|
||||
|
||||
// Now check for WiFi/Zigbee channel conflicts
|
||||
this->check_wifi_zigbee_conflict_();
|
||||
} else if (this->ash_state_ == AshState::CONNECTING) {
|
||||
// Unexpected but valid RSTACK during connecting
|
||||
ESP_LOGI(TAG, "Received RSTACK, NCP ready");
|
||||
this->ash_state_ = AshState::CONNECTED;
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Unexpected RSTACK received (boot_state=%d)", static_cast<int>(this->boot_state_));
|
||||
}
|
||||
}
|
||||
|
||||
void ZigbeeProxy::handle_error_frame_(const uint8_t *data, size_t length) {
|
||||
if (length < 1) {
|
||||
ESP_LOGE(TAG, "ERROR frame too short");
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t error_code = data[0];
|
||||
const char *error_str = "Unknown error";
|
||||
|
||||
switch (static_cast<EzspError>(error_code)) {
|
||||
case EzspError::VERSION_NOT_SET:
|
||||
error_str = "Version not set";
|
||||
break;
|
||||
case EzspError::RESET_UNKNOWN:
|
||||
error_str = "Reset (unknown)";
|
||||
break;
|
||||
case EzspError::RESET_EXTERNAL:
|
||||
error_str = "External reset";
|
||||
break;
|
||||
case EzspError::RESET_POWER_ON:
|
||||
error_str = "Power-on reset";
|
||||
break;
|
||||
case EzspError::RESET_WATCHDOG:
|
||||
error_str = "Watchdog reset";
|
||||
break;
|
||||
case EzspError::RESET_ASSERT:
|
||||
error_str = "Assert reset";
|
||||
break;
|
||||
case EzspError::RESET_BOOTLOADER:
|
||||
error_str = "Bootloader reset";
|
||||
break;
|
||||
case EzspError::RESET_SOFTWARE:
|
||||
error_str = "Software reset";
|
||||
break;
|
||||
case EzspError::EXCEEDED_MAXIMUM_ACK_TIMEOUT_COUNT:
|
||||
error_str = "Exceeded maximum ACK timeout count";
|
||||
break;
|
||||
}
|
||||
|
||||
ESP_LOGE(TAG, "NCP ERROR: %s (0x%02X)", error_str, error_code);
|
||||
|
||||
// Attempt recovery
|
||||
ESP_LOGI(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, nullptr, 0, AshFrameType::ACK, 0, ack_num);
|
||||
this->write_array(frame, length);
|
||||
this->flush();
|
||||
this->last_ack_sent_ = ack_num;
|
||||
ESP_LOGV(TAG, "Sent ACK for frame %d", ack_num);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ZigbeeProxy::send_nak_frame_(uint8_t ack_num) {
|
||||
uint8_t frame[8];
|
||||
size_t length = this->build_frame_(frame, nullptr, 0, AshFrameType::NAK, 0, ack_num);
|
||||
this->write_array(frame, length);
|
||||
this->flush();
|
||||
ESP_LOGW(TAG, "Sent NAK for frame %d", ack_num);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ZigbeeProxy::send_data_frame_(const uint8_t *data, size_t length, bool retransmit) {
|
||||
// Validate frame size
|
||||
if (length + 4 > MAX_ASH_FRAME_SIZE) {
|
||||
ESP_LOGE(TAG, "Frame too large: %u bytes", length);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Build frame
|
||||
size_t frame_length = this->build_frame_(this->tx_buffer_.data(), data, length, AshFrameType::DATA,
|
||||
this->tx_sequence_, this->rx_sequence_, retransmit);
|
||||
|
||||
// Store for potential retransmission
|
||||
if (!retransmit) {
|
||||
memcpy(this->tx_pending_buffer_.data(), this->tx_buffer_.data(), frame_length);
|
||||
this->tx_pending_length_ = frame_length;
|
||||
this->tx_pending_frame_num_ = this->tx_sequence_;
|
||||
}
|
||||
|
||||
// Debug: log exact bytes being sent
|
||||
ESP_LOGV(TAG, "TX DATA frame (%u bytes): %s", frame_length,
|
||||
format_hex_pretty(this->tx_buffer_.data(), frame_length).c_str());
|
||||
|
||||
// Send frame
|
||||
this->write_array(this->tx_buffer_.data(), frame_length);
|
||||
this->flush();
|
||||
|
||||
// Start ACK timer
|
||||
this->tx_buffer_pending_ = true;
|
||||
this->start_ack_timer_();
|
||||
|
||||
ESP_LOGV(TAG, "Sent DATA frame %d (%s), length: %u", this->tx_sequence_, retransmit ? "retransmit" : "new", length);
|
||||
|
||||
// Increment TX sequence for next frame (only for new frames)
|
||||
if (!retransmit) {
|
||||
this->increment_tx_sequence_();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Timeout management
|
||||
void ZigbeeProxy::update_adaptive_timeout_(uint32_t measured_rtt_ms) {
|
||||
// Formula: new_timeout = (7/8) * current + (1/2) * measured_rtt
|
||||
uint32_t new_timeout = (this->timeout_config_.current_timeout_ms * 7 + measured_rtt_ms * 4) / 8;
|
||||
|
||||
// Clamp to configured bounds
|
||||
if (new_timeout < this->timeout_config_.min_timeout_ms) {
|
||||
new_timeout = this->timeout_config_.min_timeout_ms;
|
||||
} else if (new_timeout > this->timeout_config_.max_timeout_ms) {
|
||||
new_timeout = this->timeout_config_.max_timeout_ms;
|
||||
}
|
||||
|
||||
this->timeout_config_.current_timeout_ms = new_timeout;
|
||||
this->last_rtt_ms_ = measured_rtt_ms;
|
||||
|
||||
ESP_LOGV(TAG, "Updated timeout: %u ms (RTT: %u ms)", new_timeout, measured_rtt_ms);
|
||||
}
|
||||
|
||||
bool ZigbeeProxy::check_ack_timeout_() {
|
||||
if (!this->tx_buffer_pending_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t elapsed = millis() - this->ack_timer_start_;
|
||||
return elapsed >= this->timeout_config_.current_timeout_ms;
|
||||
}
|
||||
|
||||
// Retransmission
|
||||
void ZigbeeProxy::handle_retransmission_() {
|
||||
if (!this->tx_buffer_pending_) {
|
||||
return;
|
||||
}
|
||||
|
||||
this->tx_retry_count_++;
|
||||
|
||||
if (this->tx_retry_count_ > ASH_MAX_RETRIES) {
|
||||
ESP_LOGE(TAG, "Max retries exceeded, entering FAILED state");
|
||||
this->ash_state_ = AshState::FAILED;
|
||||
this->clear_tx_buffer_();
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "Retransmitting frame %d (attempt %d/%d)", this->tx_pending_frame_num_, this->tx_retry_count_,
|
||||
ASH_MAX_RETRIES);
|
||||
|
||||
// Resend the pending frame
|
||||
this->write_array(this->tx_pending_buffer_.data(), this->tx_pending_length_);
|
||||
this->flush();
|
||||
this->start_ack_timer_();
|
||||
}
|
||||
|
||||
// Boot-time NCP initialization sequence
|
||||
// Sequence: RST -> RSTACK -> version() -> networkInit() -> stackStatus -> getNetworkParameters() -> RST -> RSTACK
|
||||
|
||||
void ZigbeeProxy::start_boot_sequence_() {
|
||||
ESP_LOGI(TAG, "Starting boot sequence to harvest network info");
|
||||
this->boot_state_ = BootState::WAIT_RSTACK;
|
||||
this->boot_sequence_active_ = true;
|
||||
this->ezsp_sequence_ = 0;
|
||||
this->reset_ash_protocol_();
|
||||
}
|
||||
|
||||
void ZigbeeProxy::advance_boot_state_() {
|
||||
switch (this->boot_state_) {
|
||||
case BootState::SEND_VERSION:
|
||||
this->send_ezsp_version_();
|
||||
this->boot_state_ = BootState::WAIT_VERSION;
|
||||
break;
|
||||
|
||||
case BootState::SEND_NETWORK_INIT:
|
||||
this->send_network_init_();
|
||||
this->boot_state_ = BootState::WAIT_STACK_STATUS;
|
||||
break;
|
||||
|
||||
case BootState::SEND_GET_NETWORK_PARAMS:
|
||||
this->send_get_network_params_();
|
||||
this->boot_state_ = BootState::WAIT_NETWORK_PARAMS;
|
||||
break;
|
||||
|
||||
case BootState::SEND_FINAL_RST:
|
||||
ESP_LOGI(TAG, "Sending final RST to reset NCP to clean state");
|
||||
this->boot_state_ = BootState::WAIT_FINAL_RSTACK;
|
||||
this->send_rst_frame_();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void ZigbeeProxy::handle_boot_data_frame_(const uint8_t *data, size_t length) {
|
||||
// EZSP frame format depends on negotiated version:
|
||||
// Legacy (v4-v7): [sequence] [frame_control] [frame_id] [data...] (3-byte header)
|
||||
// Extended (v8+): [sequence] [frame_control_low] [frame_control_high] [frame_id_low] [frame_id_high] [data...]
|
||||
//
|
||||
// Note: Some NCPs may respond in legacy format for a few frames after version negotiation
|
||||
// before fully switching to extended format. We handle this by falling back to legacy
|
||||
// parsing if the frame is too short for extended format.
|
||||
|
||||
if (length < 3) {
|
||||
ESP_LOGW(TAG, "Boot frame too short: %u bytes", length);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t frame_control;
|
||||
uint16_t frame_id;
|
||||
const uint8_t *payload;
|
||||
size_t payload_length;
|
||||
|
||||
// Determine format: prefer extended for v8+, but fall back to legacy if frame is too short
|
||||
bool use_extended = (this->ezsp_version_ >= 8) && (length >= 5);
|
||||
|
||||
if (use_extended) {
|
||||
frame_control = data[1];
|
||||
frame_id = data[3] | (static_cast<uint16_t>(data[4]) << 8);
|
||||
payload = data + 5;
|
||||
payload_length = length - 5;
|
||||
} else {
|
||||
frame_control = data[1];
|
||||
frame_id = data[2];
|
||||
payload = data + 3;
|
||||
payload_length = length - 3;
|
||||
}
|
||||
|
||||
// Check if this is a response (not a callback)
|
||||
bool is_response = (frame_control & 0x80) != 0;
|
||||
bool is_callback = (frame_control & 0x10) != 0;
|
||||
|
||||
ESP_LOGV(TAG, "Boot EZSP frame (%s): id=0x%04X, response=%d, callback=%d, payload_len=%u",
|
||||
use_extended ? "extended" : "legacy", frame_id, is_response, is_callback, payload_length);
|
||||
|
||||
// Handle based on current boot state
|
||||
switch (this->boot_state_) {
|
||||
case BootState::WAIT_VERSION:
|
||||
if (frame_id == EZSP_VERSION && is_response) {
|
||||
this->handle_version_response_(payload, payload_length);
|
||||
}
|
||||
break;
|
||||
|
||||
case BootState::WAIT_STACK_STATUS:
|
||||
if (frame_id == EZSP_STACK_STATUS_HANDLER && is_callback) {
|
||||
this->handle_stack_status_(payload, payload_length);
|
||||
} else if (frame_id == EZSP_NETWORK_INIT && is_response) {
|
||||
// networkInit response contains EmberStatus
|
||||
// Some NCPs proceed directly without stackStatusHandler callback
|
||||
if (payload_length >= 1) {
|
||||
uint8_t status = payload[0];
|
||||
ESP_LOGD(TAG, "networkInit response: status=0x%02X", status);
|
||||
// Proceed to getNetworkParameters regardless of status
|
||||
// getNetworkParameters will tell us if there's actually a network
|
||||
ESP_LOGI(TAG, "networkInit complete, querying network parameters");
|
||||
this->boot_state_ = BootState::SEND_GET_NETWORK_PARAMS;
|
||||
this->advance_boot_state_();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case BootState::WAIT_NETWORK_PARAMS:
|
||||
if (frame_id == EZSP_GET_NETWORK_PARAMETERS && is_response) {
|
||||
this->handle_network_params_response_(payload, payload_length);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void ZigbeeProxy::send_ezsp_version_() {
|
||||
// EZSP version command must use LEGACY format (v4-v7) for initial handshake
|
||||
// because NCP starts in legacy mode until version negotiation completes.
|
||||
// Legacy format: [sequence] [frame_control] [frame_id] [desiredProtocolVersion]
|
||||
this->ezsp_requested_version_ = EZSP_MAX_VERSION;
|
||||
uint8_t cmd[] = {
|
||||
this->ezsp_sequence_++, // Sequence
|
||||
EZSP_FRAME_CONTROL_COMMAND, // Frame control (command, no callback)
|
||||
0x00, // Frame ID (version command = 0x00)
|
||||
EZSP_MAX_VERSION // Desired protocol version
|
||||
};
|
||||
|
||||
ESP_LOGD(TAG, "Sending EZSP version command (legacy format, requesting v%d)", EZSP_MAX_VERSION);
|
||||
this->send_data_frame_(cmd, sizeof(cmd), false);
|
||||
}
|
||||
|
||||
void ZigbeeProxy::send_network_init_() {
|
||||
// networkInit command - use legacy format for compatibility
|
||||
// Some NCPs need a command or two in legacy format after version negotiation
|
||||
// before fully switching to extended format.
|
||||
// networkInitStruct: [bitmask (2 bytes)] - use 0x0000 for default
|
||||
uint8_t cmd[] = {
|
||||
this->ezsp_sequence_++, // Sequence
|
||||
EZSP_FRAME_CONTROL_COMMAND, // Frame control
|
||||
EZSP_NETWORK_INIT & 0xFF, // Frame ID
|
||||
0x00, 0x00 // networkInitStruct bitmask (default)
|
||||
};
|
||||
ESP_LOGD(TAG, "Sending EZSP networkInit command (legacy format)");
|
||||
this->send_data_frame_(cmd, sizeof(cmd), false);
|
||||
}
|
||||
|
||||
void ZigbeeProxy::send_get_network_params_() {
|
||||
// getNetworkParameters command - use legacy format for boot sequence compatibility
|
||||
uint8_t cmd[] = {
|
||||
this->ezsp_sequence_++, // Sequence
|
||||
EZSP_FRAME_CONTROL_COMMAND, // Frame control
|
||||
EZSP_GET_NETWORK_PARAMETERS & 0xFF // Frame ID
|
||||
};
|
||||
ESP_LOGD(TAG, "Sending EZSP getNetworkParameters command (legacy format)");
|
||||
this->send_data_frame_(cmd, sizeof(cmd), false);
|
||||
}
|
||||
|
||||
void ZigbeeProxy::handle_version_response_(const uint8_t *data, size_t length) {
|
||||
// Version response format depends on whether NCP supports requested version:
|
||||
// - If supported: [protocolVersion] [stackType] [stackVersion (2 bytes)]
|
||||
// - If NOT supported: [protocolVersion] only (NCP's supported version)
|
||||
//
|
||||
// When NCP doesn't support the requested version, it responds with just
|
||||
// its supported version, indicating we should re-negotiate.
|
||||
|
||||
if (length < 1) {
|
||||
ESP_LOGW(TAG, "Version response empty");
|
||||
this->boot_state_ = BootState::FAILED;
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t ncp_version = data[0];
|
||||
|
||||
if (length == 1) {
|
||||
// NCP responded with just its version
|
||||
// This happens when:
|
||||
// 1. We requested a version the NCP doesn't support -> re-negotiate
|
||||
// 2. We requested the NCP's version and it accepted -> treat as success
|
||||
|
||||
if (ncp_version == this->ezsp_requested_version_) {
|
||||
// NCP accepted our requested version - treat as success
|
||||
ESP_LOGI(TAG, "NCP accepted EZSP v%d", ncp_version);
|
||||
this->ezsp_version_ = ncp_version;
|
||||
// Proceed to networkInit
|
||||
this->boot_state_ = BootState::SEND_NETWORK_INIT;
|
||||
this->advance_boot_state_();
|
||||
return;
|
||||
}
|
||||
|
||||
// NCP doesn't support our version - re-negotiate
|
||||
ESP_LOGI(TAG, "NCP supports EZSP v%d, re-negotiating", ncp_version);
|
||||
this->ezsp_requested_version_ = ncp_version;
|
||||
|
||||
// Re-send version command with NCP's supported version
|
||||
// Use legacy format for re-negotiation (NCP stays in legacy until handshake completes)
|
||||
uint8_t cmd[] = {
|
||||
this->ezsp_sequence_++, // Sequence
|
||||
EZSP_FRAME_CONTROL_COMMAND, // Frame control
|
||||
0x00, // Frame ID (version)
|
||||
ncp_version // Use NCP's version
|
||||
};
|
||||
ESP_LOGD(TAG, "Re-sending EZSP version command (requesting v%d)", ncp_version);
|
||||
this->send_data_frame_(cmd, sizeof(cmd), false);
|
||||
// Stay in WAIT_VERSION state
|
||||
return;
|
||||
}
|
||||
|
||||
// Full response with stack info
|
||||
if (length < 4) {
|
||||
ESP_LOGW(TAG, "Version response too short: %u bytes", length);
|
||||
this->boot_state_ = BootState::FAILED;
|
||||
return;
|
||||
}
|
||||
|
||||
this->ezsp_version_ = data[0];
|
||||
uint8_t stack_type = data[1];
|
||||
uint16_t stack_version = data[2] | (static_cast<uint16_t>(data[3]) << 8);
|
||||
|
||||
ESP_LOGI(TAG, "NCP EZSP version: %d, stack type: %d, stack version: 0x%04X", this->ezsp_version_, stack_type,
|
||||
stack_version);
|
||||
|
||||
if (this->ezsp_version_ < EZSP_MIN_VERSION) {
|
||||
ESP_LOGE(TAG, "EZSP version %d not supported (minimum: %d)", this->ezsp_version_, EZSP_MIN_VERSION);
|
||||
this->boot_state_ = BootState::FAILED;
|
||||
return;
|
||||
}
|
||||
|
||||
// Proceed to networkInit
|
||||
this->boot_state_ = BootState::SEND_NETWORK_INIT;
|
||||
this->advance_boot_state_();
|
||||
}
|
||||
|
||||
void ZigbeeProxy::handle_stack_status_(const uint8_t *data, size_t length) {
|
||||
// stackStatusHandler callback: [status]
|
||||
if (length < 1) {
|
||||
ESP_LOGW(TAG, "Stack status too short");
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t status = data[0];
|
||||
ESP_LOGD(TAG, "Stack status: 0x%02X", status);
|
||||
|
||||
// Check for network up status
|
||||
if (status == static_cast<uint8_t>(EmberStatus::NETWORK_UP) || status == static_cast<uint8_t>(EmberStatus::SUCCESS)) {
|
||||
ESP_LOGI(TAG, "Network is up, querying parameters");
|
||||
this->boot_state_ = BootState::SEND_GET_NETWORK_PARAMS;
|
||||
this->advance_boot_state_();
|
||||
} else if (status == static_cast<uint8_t>(EmberStatus::NOT_JOINED)) {
|
||||
// No network configured - that's fine, we just won't have network info
|
||||
ESP_LOGI(TAG, "No network configured on NCP");
|
||||
this->boot_state_ = BootState::SEND_FINAL_RST;
|
||||
this->advance_boot_state_();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Unexpected stack status: 0x%02X, continuing anyway", status);
|
||||
// Try to get network params anyway
|
||||
this->boot_state_ = BootState::SEND_GET_NETWORK_PARAMS;
|
||||
this->advance_boot_state_();
|
||||
}
|
||||
}
|
||||
|
||||
void ZigbeeProxy::handle_network_params_response_(const uint8_t *data, size_t length) {
|
||||
// getNetworkParameters response:
|
||||
// [status] [nodeType] [extendedPanId (8)] [panId (2)] [radioTxPower] [radioChannel] ...
|
||||
if (length < 14) {
|
||||
ESP_LOGW(TAG, "Network params response too short: %u bytes", length);
|
||||
this->boot_state_ = BootState::SEND_FINAL_RST;
|
||||
this->advance_boot_state_();
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t status = data[NETWORK_PARAMS_STATUS_OFFSET];
|
||||
if (status != static_cast<uint8_t>(EmberStatus::SUCCESS)) {
|
||||
ESP_LOGW(TAG, "getNetworkParameters failed with status: 0x%02X", status);
|
||||
this->boot_state_ = BootState::SEND_FINAL_RST;
|
||||
this->advance_boot_state_();
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract Extended PAN ID (8 bytes, little-endian)
|
||||
memcpy(this->network_info_.extended_pan_id.data(), data + NETWORK_PARAMS_EXT_PAN_ID_OFFSET, 8);
|
||||
|
||||
// Extract PAN ID (2 bytes, little-endian)
|
||||
this->network_info_.pan_id =
|
||||
data[NETWORK_PARAMS_PAN_ID_OFFSET] | (static_cast<uint16_t>(data[NETWORK_PARAMS_PAN_ID_OFFSET + 1]) << 8);
|
||||
|
||||
// Extract channel
|
||||
this->network_info_.channel = data[NETWORK_PARAMS_CHANNEL_OFFSET];
|
||||
|
||||
this->network_info_.valid = true;
|
||||
|
||||
ESP_LOGI(TAG,
|
||||
"Network info harvested:\n"
|
||||
" Extended PAN ID: %02X%02X%02X%02X%02X%02X%02X%02X\n"
|
||||
" PAN ID: 0x%04X\n"
|
||||
" Channel: %u",
|
||||
this->network_info_.extended_pan_id[7], this->network_info_.extended_pan_id[6],
|
||||
this->network_info_.extended_pan_id[5], this->network_info_.extended_pan_id[4],
|
||||
this->network_info_.extended_pan_id[3], this->network_info_.extended_pan_id[2],
|
||||
this->network_info_.extended_pan_id[1], this->network_info_.extended_pan_id[0], this->network_info_.pan_id,
|
||||
this->network_info_.channel);
|
||||
|
||||
// Now reset NCP to clean state
|
||||
this->boot_state_ = BootState::SEND_FINAL_RST;
|
||||
this->advance_boot_state_();
|
||||
}
|
||||
|
||||
bool ZigbeeProxy::set_ieee_address_(const uint8_t *new_address) {
|
||||
bool changed = false;
|
||||
|
||||
for (size_t i = 0; i < ZIGBEE_IEEE_ADDR_SIZE; i++) {
|
||||
if (this->network_info_.ieee_address[i] != new_address[i]) {
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
memcpy(this->network_info_.ieee_address.data(), new_address, ZIGBEE_IEEE_ADDR_SIZE);
|
||||
this->network_info_.valid = true;
|
||||
ESP_LOGI(TAG, "IEEE address updated: %02X:%02X:%02X:%02X:%02X:%02X:%02X:%02X", new_address[7], new_address[6],
|
||||
new_address[5], new_address[4], new_address[3], new_address[2], new_address[1], new_address[0]);
|
||||
this->send_network_info_changed_msg_();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void ZigbeeProxy::send_network_info_changed_msg_(api::APIConnection *conn) {
|
||||
// This would send network info to the API client
|
||||
// For now, we'll log it
|
||||
ESP_LOGD(TAG, "Network info changed notification");
|
||||
}
|
||||
|
||||
// WiFi/Zigbee channel conflict detection
|
||||
void ZigbeeProxy::check_wifi_zigbee_conflict_() {
|
||||
#ifdef USE_WIFI
|
||||
if (wifi::global_wifi_component == nullptr || !this->network_info_.valid || this->network_info_.channel == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t wifi_channel = wifi::global_wifi_component->get_wifi_channel();
|
||||
if (wifi_channel == 0) {
|
||||
return; // WiFi not connected yet
|
||||
}
|
||||
|
||||
uint8_t zigbee_channel = this->network_info_.channel;
|
||||
|
||||
// Check for overlap
|
||||
bool conflict = false;
|
||||
const char *recommendation = "";
|
||||
|
||||
if (zigbee_channel >= 11 && zigbee_channel <= 14) {
|
||||
// Zigbee 11-14 overlaps WiFi 1
|
||||
if (wifi_channel == 1) {
|
||||
conflict = true;
|
||||
recommendation = "Use Zigbee channel 25-26 or WiFi channel 6/11";
|
||||
}
|
||||
} else if (zigbee_channel >= 15 && zigbee_channel <= 18) {
|
||||
// Zigbee 15-18 overlaps WiFi 6
|
||||
if (wifi_channel == 6) {
|
||||
conflict = true;
|
||||
recommendation = "Use Zigbee channel 25-26 or WiFi channel 1/11";
|
||||
}
|
||||
} else if (zigbee_channel >= 19 && zigbee_channel <= 22) {
|
||||
// Zigbee 19-22 overlaps WiFi 11
|
||||
if (wifi_channel == 11) {
|
||||
conflict = true;
|
||||
recommendation = "Use Zigbee channel 25-26 or WiFi channel 1/6";
|
||||
}
|
||||
} else if (zigbee_channel >= 23 && zigbee_channel <= 26) {
|
||||
// Zigbee 23-26 overlaps WiFi 13-14
|
||||
if (wifi_channel >= 13) {
|
||||
conflict = true;
|
||||
recommendation = "Use Zigbee channel 15-20 or WiFi channel 1/6/11";
|
||||
}
|
||||
}
|
||||
|
||||
if (conflict) {
|
||||
ESP_LOGW(TAG,
|
||||
"WiFi/Zigbee channel conflict detected\n"
|
||||
" WiFi channel: %u, Zigbee channel: %u\n"
|
||||
" Recommendation: %s",
|
||||
wifi_channel, zigbee_channel, recommendation);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "No WiFi/Zigbee channel conflict (WiFi: %u, Zigbee: %u)", wifi_channel, zigbee_channel);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Bootloader detection
|
||||
void ZigbeeProxy::check_bootloader_mode_(const uint8_t *data, size_t length) {
|
||||
if (length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for Silicon Labs bootloader menu prompt (0xC1 0x0D)
|
||||
if (data[0] == 0xC1 && data[1] == 0x0D) {
|
||||
if (this->bootloader_state_ != BootloaderState::MENU) {
|
||||
ESP_LOGW(TAG, "NCP in bootloader menu mode detected\n"
|
||||
"Please flash NCP firmware or power cycle the device");
|
||||
this->bootloader_state_ = BootloaderState::MENU;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for upload begin (0x43)
|
||||
if (data[0] == 0x43) {
|
||||
if (this->bootloader_state_ != BootloaderState::DETECTED) {
|
||||
ESP_LOGW(TAG, "NCP bootloader upload mode detected");
|
||||
this->bootloader_state_ = BootloaderState::DETECTED;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset bootloader state if we see normal traffic
|
||||
if (this->bootloader_state_ != BootloaderState::NORMAL && this->ash_state_ == AshState::CONNECTED) {
|
||||
ESP_LOGI(TAG, "NCP returned to normal operation");
|
||||
this->bootloader_state_ = BootloaderState::NORMAL;
|
||||
}
|
||||
}
|
||||
|
||||
// UART processing
|
||||
void ZigbeeProxy::process_uart_() {
|
||||
while (this->available()) {
|
||||
uint8_t byte;
|
||||
this->read_byte(&byte);
|
||||
|
||||
// Verbose logging for debugging (ESP_LOGV already checks log level)
|
||||
ESP_LOGV(TAG, "RX: 0x%02X", byte);
|
||||
|
||||
this->parse_byte_(byte);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace zigbee_proxy
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ZIGBEE_PROXY
|
||||
@@ -0,0 +1,200 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
|
||||
#include "esphome/components/api/api_connection.h"
|
||||
#include "esphome/components/api/api_pb2.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "ash_protocol.h"
|
||||
|
||||
#include <array>
|
||||
|
||||
namespace esphome {
|
||||
namespace zigbee_proxy {
|
||||
|
||||
// Timeout configuration structure
|
||||
struct TimeoutConfig {
|
||||
uint32_t initial_timeout_ms{1600}; // Initial ACK timeout
|
||||
uint32_t min_timeout_ms{400}; // Minimum adaptive timeout
|
||||
uint32_t max_timeout_ms{3200}; // Maximum adaptive timeout
|
||||
uint32_t current_timeout_ms{1600}; // Current adaptive timeout
|
||||
};
|
||||
|
||||
// Network information structure
|
||||
struct NetworkInfo {
|
||||
std::array<uint8_t, ZIGBEE_IEEE_ADDR_SIZE> ieee_address{};
|
||||
uint16_t pan_id{0};
|
||||
std::array<uint8_t, 8> extended_pan_id{};
|
||||
uint8_t channel{0};
|
||||
bool valid{false};
|
||||
};
|
||||
|
||||
enum ZigbeeProxyFeature : uint32_t {
|
||||
FEATURE_ZIGBEE_PROXY_ENABLED = 1 << 0,
|
||||
};
|
||||
|
||||
// Boot-time initialization state machine
|
||||
enum class BootState : uint8_t {
|
||||
IDLE, // Not initializing
|
||||
WAIT_RSTACK, // Sent RST, waiting for RSTACK
|
||||
SEND_VERSION, // Send EZSP version command
|
||||
WAIT_VERSION, // Waiting for version response
|
||||
SEND_NETWORK_INIT, // Send networkInit command
|
||||
WAIT_STACK_STATUS, // Waiting for stackStatusHandler callback
|
||||
SEND_GET_NETWORK_PARAMS, // Send getNetworkParameters command
|
||||
WAIT_NETWORK_PARAMS, // Waiting for network parameters response
|
||||
SEND_FINAL_RST, // Send final RST to reset NCP
|
||||
WAIT_FINAL_RSTACK, // Waiting for final RSTACK
|
||||
COMPLETE, // Boot sequence complete
|
||||
FAILED, // Boot sequence failed
|
||||
};
|
||||
|
||||
class ZigbeeProxy : public uart::UARTDevice, public Component {
|
||||
public:
|
||||
ZigbeeProxy();
|
||||
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
bool can_proceed() override;
|
||||
|
||||
// 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_; }
|
||||
|
||||
// Feature flags
|
||||
uint32_t get_feature_flags() const { return ZigbeeProxyFeature::FEATURE_ZIGBEE_PROXY_ENABLED; }
|
||||
|
||||
// Network information accessors
|
||||
const NetworkInfo &get_network_info() const { return this->network_info_; }
|
||||
uint64_t get_ieee_address() const;
|
||||
|
||||
// Frame sending (from API client to NCP)
|
||||
void send_frame(const uint8_t *data, size_t length);
|
||||
|
||||
// Timeout configuration (callable from Python/API)
|
||||
void set_timeout_config(uint32_t initial_ms, uint32_t min_ms, uint32_t max_ms);
|
||||
void set_initial_timeout(uint32_t timeout_ms) { this->timeout_config_.initial_timeout_ms = timeout_ms; }
|
||||
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; }
|
||||
|
||||
protected:
|
||||
// ASH Protocol State Machine
|
||||
void reset_ash_protocol_();
|
||||
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);
|
||||
bool send_ack_frame_(uint8_t ack_num);
|
||||
bool send_nak_frame_(uint8_t ack_num);
|
||||
bool send_data_frame_(const uint8_t *data, size_t length, bool retransmit = false);
|
||||
|
||||
// Frame parsing and building (implemented in ash_protocol.cpp)
|
||||
bool parse_byte_(uint8_t byte);
|
||||
void parse_control_byte_(uint8_t control);
|
||||
bool validate_frame_crc_();
|
||||
size_t build_frame_(uint8_t *output, const uint8_t *data, size_t length, AshFrameType type, uint8_t frame_num = 0,
|
||||
uint8_t ack_num = 0, bool retx = false);
|
||||
uint16_t calculate_crc_(const uint8_t *data, size_t length);
|
||||
|
||||
// Sequence number management
|
||||
void increment_tx_sequence_() { this->tx_sequence_ = (this->tx_sequence_ + 1) & ASH_MAX_SEQUENCE; }
|
||||
void increment_rx_sequence_() { this->rx_sequence_ = (this->rx_sequence_ + 1) & ASH_MAX_SEQUENCE; }
|
||||
|
||||
// Timeout management
|
||||
void update_adaptive_timeout_(uint32_t measured_rtt_ms);
|
||||
void start_ack_timer_() { this->ack_timer_start_ = millis(); }
|
||||
bool check_ack_timeout_();
|
||||
|
||||
// Retransmission
|
||||
void handle_retransmission_();
|
||||
void clear_tx_buffer_() {
|
||||
this->tx_buffer_pending_ = false;
|
||||
this->tx_retry_count_ = 0;
|
||||
}
|
||||
|
||||
// Boot-time NCP initialization
|
||||
void start_boot_sequence_();
|
||||
void advance_boot_state_();
|
||||
void handle_boot_data_frame_(const uint8_t *data, size_t length);
|
||||
void send_ezsp_version_();
|
||||
void send_network_init_();
|
||||
void send_get_network_params_();
|
||||
void handle_version_response_(const uint8_t *data, size_t length);
|
||||
void handle_stack_status_(const uint8_t *data, size_t length);
|
||||
void handle_network_params_response_(const uint8_t *data, size_t length);
|
||||
|
||||
// IEEE address and network info
|
||||
bool set_ieee_address_(const uint8_t *new_address);
|
||||
void send_network_info_changed_msg_(api::APIConnection *conn = nullptr);
|
||||
|
||||
// WiFi/Zigbee channel conflict detection
|
||||
void check_wifi_zigbee_conflict_();
|
||||
|
||||
// Bootloader detection
|
||||
void check_bootloader_mode_(const uint8_t *data, size_t length);
|
||||
|
||||
// UART processing
|
||||
void process_uart_();
|
||||
|
||||
// Pre-allocated message - always ready to send
|
||||
api::ZigbeeProxyFrame outgoing_proto_msg_;
|
||||
|
||||
// Fixed-size buffers for incoming and outgoing data
|
||||
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
|
||||
|
||||
// Network information
|
||||
NetworkInfo network_info_;
|
||||
|
||||
// Timeout configuration
|
||||
TimeoutConfig timeout_config_;
|
||||
|
||||
// Pointers (aligned together)
|
||||
api::APIConnection *api_connection_{nullptr}; // Current subscribed client
|
||||
|
||||
// 32-bit values
|
||||
uint32_t setup_time_{0}; // Time when setup() was called
|
||||
uint32_t ack_timer_start_{0}; // Time when ACK timer started
|
||||
uint32_t last_rtt_ms_{0}; // Last measured round-trip time
|
||||
|
||||
// 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
|
||||
|
||||
// 8-bit values (grouped together to minimize padding)
|
||||
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
|
||||
uint8_t tx_pending_frame_num_{0}; // Frame number of pending TX frame
|
||||
uint8_t last_ack_sent_{0}; // Last ACK number sent
|
||||
|
||||
// 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};
|
||||
|
||||
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)
|
||||
|
||||
bool tx_buffer_pending_{false}; // True if waiting for ACK
|
||||
bool escape_next_byte_{false}; // True if next byte should be unescaped
|
||||
bool network_info_ready_{false}; // True when network info retrieved
|
||||
bool boot_sequence_active_{false}; // True during boot-time init
|
||||
};
|
||||
|
||||
extern ZigbeeProxy *global_zigbee_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
} // namespace zigbee_proxy
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ZIGBEE_PROXY
|
||||
@@ -122,6 +122,7 @@
|
||||
#define USE_VALVE
|
||||
#define USE_WATER_HEATER
|
||||
#define USE_WATER_HEATER_VISUAL_OVERRIDES
|
||||
#define USE_ZIGBEE_PROXY
|
||||
#define USE_ZWAVE_PROXY
|
||||
|
||||
// Feature flags which do not work for zephyr
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
wifi:
|
||||
ssid: test
|
||||
password: password
|
||||
power_save_mode: none
|
||||
|
||||
api:
|
||||
|
||||
uart:
|
||||
- id: zigbee_uart
|
||||
tx_pin: ${tx_pin}
|
||||
rx_pin: ${rx_pin}
|
||||
baud_rate: 115200
|
||||
|
||||
zigbee_proxy:
|
||||
uart_id: zigbee_uart
|
||||
@@ -0,0 +1,14 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO17
|
||||
rx_pin: GPIO16
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
zigbee_proxy:
|
||||
buffer_size: 1024
|
||||
initial_timeout: 1600
|
||||
min_timeout: 400
|
||||
max_timeout: 3200
|
||||
@@ -0,0 +1,11 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO1
|
||||
rx_pin: GPIO3
|
||||
|
||||
esp8266:
|
||||
board: nodemcuv2
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
zigbee_proxy:
|
||||
buffer_size: 512
|
||||
@@ -0,0 +1,8 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO0
|
||||
rx_pin: GPIO1
|
||||
|
||||
rp2040:
|
||||
board: rpipico
|
||||
|
||||
<<: !include common.yaml
|
||||
Reference in New Issue
Block a user