Compare commits

...

16 Commits

Author SHA1 Message Date
kbx81
f8bec0813d fix 2026-03-13 16:48:56 -05:00
kbx81
84762e6ae0 oops 2026-03-13 16:46:13 -05:00
kbx81
2edf313ee3 Merge remote-tracking branch 'upstream/dev' into 20260218-zigbee-proxy 2026-03-13 16:45:23 -05:00
kbx81
ae9c999052 fix 2026-02-28 23:21:30 -06:00
kbx81
7d2f6fbf55 Merge remote-tracking branch 'upstream/dev' into 20260218-zigbee-proxy 2026-02-28 23:12:31 -06:00
kbx81
608bef86cc Merge remote-tracking branch 'upstream/dev' into 20260218-zigbee-proxy 2026-02-26 23:42:43 -06:00
kbx81
6514dc2fe1 Merge remote-tracking branch 'upstream/dev' into 20260218-zigbee-proxy 2026-02-26 20:55:50 -06:00
kbx81
240afd23b3 ... 2026-02-26 14:31:17 -06:00
kbx81
156c2a8cb0 optimize 2026-02-26 14:30:31 -06:00
kbx81
908c47bb5e preen, tune 2026-02-25 23:28:44 -06:00
kbx81
6df3a30740 Merge remote-tracking branch 'upstream/dev' into 20260218-zigbee-proxy 2026-02-25 17:33:27 -06:00
kbx81
0aaf59dbed Merge remote-tracking branch 'upstream/dev' into 20260218-zigbee-proxy 2026-02-24 16:51:04 -06:00
kbx81
249c5bb724 Merge remote-tracking branch 'upstream/dev' into 20260218-zigbee-proxy 2026-02-23 18:01:56 -06:00
kbx81
54ea8dd207 Merge remote-tracking branch 'upstream/dev' into 20260218-zigbee-proxy 2026-02-19 18:31:15 -06:00
puddly
4cfb794b62 WIP 2026-02-19 18:22:03 -05:00
kbx81
917af8ff31 [zigbee_proxy] New component 2026-02-19 14:34:29 -06:00
20 changed files with 2327 additions and 3 deletions

View File

@@ -69,6 +69,9 @@ 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) {}
rpc serial_proxy_configure(SerialProxyConfigureRequest) returns (void) {}
@@ -281,6 +284,10 @@ message DeviceInfoResponse {
// Serial proxy instance metadata
repeated SerialProxyInfo serial_proxies = 25 [(field_ifdef) = "USE_SERIAL_PROXY", (fixed_array_size_define) = "SERIAL_PROXY_COUNT"];
// Indicates if Zigbee proxy support is available and features supported
uint32 zigbee_proxy_feature_flags = 26 [(field_ifdef) = "USE_ZIGBEE_PROXY"];
uint64 zigbee_ieee_address = 27 [(field_ifdef) = "USE_ZIGBEE_PROXY"];
}
message ListEntitiesRequest {
@@ -2669,3 +2676,29 @@ message BluetoothSetConnectionParamsResponse {
uint64 address = 1;
int32 error = 2;
}
// ==================== ZIGBEE ====================
message ZigbeeProxyFrame {
option (id) = 148;
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) = 149;
option (source) = SOURCE_BOTH;
option (ifdef) = "USE_ZIGBEE_PROXY";
ZigbeeProxyRequestType type = 1;
bytes data = 2;
}

View File

@@ -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
@@ -1317,6 +1320,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,
@@ -1630,6 +1643,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) {
@@ -1771,6 +1789,10 @@ bool APIConnection::send_device_info_response_() {
info.port_type = proxy->get_port_type();
}
#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

View File

@@ -180,6 +180,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); }
#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;

View File

@@ -142,6 +142,12 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer &buffer) const {
buffer.encode_sub_message(25, it);
}
#endif
#ifdef USE_ZIGBEE_PROXY
buffer.encode_uint32(26, this->zigbee_proxy_feature_flags);
#endif
#ifdef USE_ZIGBEE_PROXY
buffer.encode_uint64(27, this->zigbee_ieee_address);
#endif
}
uint32_t DeviceInfoResponse::calculate_size() const {
uint32_t size = 0;
@@ -202,6 +208,12 @@ uint32_t DeviceInfoResponse::calculate_size() const {
for (const auto &it : this->serial_proxies) {
size += ProtoSize::calc_message_force(2, it.calculate_size());
}
#endif
#ifdef USE_ZIGBEE_PROXY
size += ProtoSize::calc_uint32(2, this->zigbee_proxy_feature_flags);
#endif
#ifdef USE_ZIGBEE_PROXY
size += ProtoSize::calc_uint64(2, this->zigbee_ieee_address);
#endif
return size;
}
@@ -3889,5 +3901,57 @@ uint32_t BluetoothSetConnectionParamsResponse::calculate_size() const {
return size;
}
#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); }
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:
this->type = static_cast<enums::ZigbeeProxyRequestType>(value);
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);
}
uint32_t ZigbeeProxyRequest::calculate_size() const {
uint32_t size = 0;
size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->type));
size += ProtoSize::calc_length(1, this->data_len);
return size;
}
#endif
} // namespace esphome::api

View File

@@ -341,6 +341,13 @@ enum SerialProxyStatus : uint32_t {
SERIAL_PROXY_STATUS_NOT_SUPPORTED = 4,
};
#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
@@ -518,7 +525,7 @@ class SerialProxyInfo final : public ProtoMessage {
class DeviceInfoResponse final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 10;
static constexpr uint16_t ESTIMATED_SIZE = 309;
static constexpr uint16_t ESTIMATED_SIZE = 319;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "device_info_response"; }
#endif
@@ -573,6 +580,12 @@ class DeviceInfoResponse final : public ProtoMessage {
#endif
#ifdef USE_SERIAL_PROXY
std::array<SerialProxyInfo, SERIAL_PROXY_COUNT> serial_proxies{};
#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;
uint32_t calculate_size() const;
@@ -3285,5 +3298,45 @@ class BluetoothSetConnectionParamsResponse final : public ProtoMessage {
protected:
};
#endif
#ifdef USE_ZIGBEE_PROXY
class ZigbeeProxyFrame final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 148;
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;
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 = 149;
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;
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;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
#endif
} // namespace esphome::api

View File

@@ -3,10 +3,8 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_BLUETOOTH_PROXY
#ifndef USE_API_VARINT64
#define USE_API_VARINT64
#endif
#endif
namespace esphome::api {} // namespace esphome::api

View File

@@ -806,6 +806,20 @@ template<> const char *proto_enum_to_string<enums::SerialProxyStatus>(enums::Ser
}
}
#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");
@@ -930,6 +944,12 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const {
it.dump_to(out);
out.append("\n");
}
#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();
}
@@ -2651,6 +2671,19 @@ const char *BluetoothSetConnectionParamsResponse::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
} // namespace esphome::api

View File

@@ -700,6 +700,28 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_bluetooth_set_connection_params_request(msg);
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;
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
default:
break;

View File

@@ -238,6 +238,12 @@ class APIServerConnectionBase : public ProtoService {
virtual void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &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
protected:
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
};

View File

@@ -0,0 +1,102 @@
import esphome.codegen as cg
from esphome.components import uart, usb_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"
CONF_USB_UART_ID = "usb_uart_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
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,
),
# When usb_uart_id is present the component registers an RX callback
# for zero-wakeup-cycle data delivery and selects USB-optimized ACK
# timeout defaults. Explicit timeout keys always win.
cv.Optional(CONF_USB_UART_ID): cv.use_id(usb_uart.USBUartChannel),
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),
}
)
.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])
# Select timeout defaults based on UART transport type.
# 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 = CONF_USB_UART_ID in config
if is_usb:
cg.add_define("USE_ZIGBEE_PROXY_USB_UART")
usb_ch = await cg.get_variable(config[CONF_USB_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))

View File

@@ -0,0 +1,402 @@
#include "zigbee_proxy.h"
#ifdef USE_ZIGBEE_PROXY
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
namespace esphome::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 init) {
uint16_t 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_LOGV(TAG, "ACK received (piggybacked in DATA), RTT: %u ms", rtt);
}
// Increment RX sequence and send ACK (ack_num = next expected frame)
this->increment_rx_sequence_();
this->send_ack_frame_(this->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 client via client-side ASH DATA frame
this->forward_ncp_data_to_client_(payload, payload_length);
}
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_LOGV(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);
}
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 - WARN logs byte count only; hex dump at VERBOSE to avoid heap allocation in production
ESP_LOGW(TAG, "CRC failed (%u bytes)", this->rx_buffer_index_);
ESP_LOGV(TAG, "CRC failed frame: %s",
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 (reserved: FLAG, ESCAPE, XON, XOFF, SUB, CAN)
if (control == ASH_FLAG_BYTE || control == ASH_ESCAPE_BYTE || control == 0x11 || control == 0x13 || control == 0x18 ||
control == 0x1A) {
output[pos++] = ASH_ESCAPE_BYTE;
output[pos++] = control ^ ASH_XOR_BYTE;
} else {
output[pos++] = control;
}
// 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 == 0x18 ||
byte == 0x1A) {
output[pos++] = ASH_ESCAPE_BYTE;
output[pos++] = byte ^ ASH_XOR_BYTE;
} else {
output[pos++] = byte;
}
}
// Calculate CRC incrementally over control byte then data (avoids a MAX_ASH_FRAME_SIZE stack copy)
uint16_t crc = this->calculate_crc_(&control, 1);
if (length > 0) {
crc = this->calculate_crc_(data, length, crc);
}
// 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 == 0x18 || crc_high == 0x1A) {
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 == 0x18 ||
crc_low == 0x1A) {
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 esphome::zigbee_proxy
#endif // USE_ZIGBEE_PROXY

View File

@@ -0,0 +1,88 @@
#pragma once
#include <cstdint>
#include <cstddef>
namespace esphome::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 esphome::zigbee_proxy

View File

@@ -0,0 +1,56 @@
#pragma once
#include <cstddef>
#include <cstdint>
namespace esphome::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 esphome::zigbee_proxy

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,252 @@
#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>
// 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 usb_uart_id
// is present in the YAML, 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
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; }
#ifdef USE_ZIGBEE_PROXY_USB_UART
/// Called from generated code when usb_uart_id is configured.
/// 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_();
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, uint16_t init = ASH_CRC_INIT);
// 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_();
// 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_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);
void client_reset_session_();
// Send raw bytes to API client
void send_to_client_(const uint8_t *data, size_t length);
// Forward NCP frames to client
void forward_ncp_data_to_client_(const uint8_t *payload, size_t length);
void forward_ncp_rstack_to_client_(const uint8_t *data, size_t length);
void forward_ncp_error_to_client_(const uint8_t *data, size_t length);
// Pre-allocated message - always ready to send
api::ZigbeeProxyFrame outgoing_proto_msg_;
// NCP-side (right) 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
std::array<uint8_t, MAX_ASH_FRAME_SIZE> client_rx_buffer_;
std::array<uint8_t, MAX_ASH_FRAME_SIZE> client_tx_buffer_;
// Network information
NetworkInfo network_info_;
// Timeout configuration
TimeoutConfig timeout_config_;
// Pointers (aligned together)
api::APIConnection *api_connection_{nullptr}; // Current subscribed client
// 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
// 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
uint16_t client_rx_buffer_index_{0};
// 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
uint8_t tx_pending_frame_num_{0}; // Frame number of pending TX frame
uint8_t last_ack_sent_{0}; // Last ACK number sent
// Client-side (left) 8-bit values
uint8_t client_tx_sequence_{0}; // Client-facing TX sequence (proxy → client)
uint8_t client_rx_sequence_{0}; // Client-facing RX sequence (client → proxy)
// 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
AshState client_ash_state_{AshState::DISCONNECTED};
ParsingState client_parsing_state_{ParsingState::WAIT_FLAG_START};
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 from NCP
bool escape_next_byte_{false}; // True if next NCP byte should be unescaped
bool client_escape_next_byte_{false}; // True if next client 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 esphome::zigbee_proxy
#endif // USE_ZIGBEE_PROXY

View File

@@ -138,6 +138,8 @@
#define USE_VALVE
#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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,11 @@
substitutions:
tx_pin: GPIO1
rx_pin: GPIO3
esp8266:
board: nodemcuv2
<<: !include common.yaml
zigbee_proxy:
buffer_size: 512

View File

@@ -0,0 +1,8 @@
substitutions:
tx_pin: GPIO0
rx_pin: GPIO1
rp2040:
board: rpipico
<<: !include common.yaml