mirror of
https://github.com/esphome/esphome.git
synced 2026-08-26 08:06:21 +00:00
[zigbee_proxy] Harden ASH sessions, fix UAF/boot-stall/overflows, reduce latency
- Unsubscribe on API disconnect (use-after-free) + loop() subscriber guard - Bounds-checked frame building; cap forwarded RSTACK/ERROR payloads - Explicit client ACKs, duplicate re-ACK, NAK on reject (both ASH sides) - Client->NCP TX queue with NAK overflow; retry client frames on API backpressure - Harvest EUI64 during boot; implement NETWORK_INFO request/response and push - Proceed after boot timeout instead of stalling setup; periodic NCP recovery - zwave-style inline UART fast path; process piggybacked ACKs before sequence check - Wire up bootloader detection; heap-free hex logging
This commit is contained in:
@@ -192,6 +192,11 @@ APIConnection::~APIConnection() {
|
||||
zwave_proxy::global_zwave_proxy->zwave_proxy_request(this, enums::ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
if (zigbee_proxy::global_zigbee_proxy != nullptr && zigbee_proxy::global_zigbee_proxy->get_api_connection() == this) {
|
||||
zigbee_proxy::global_zigbee_proxy->unsubscribe_api_connection(this);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
for (auto *proxy : App.get_serial_proxies()) {
|
||||
if (proxy->get_api_connection() == this) {
|
||||
|
||||
@@ -221,7 +221,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
void on_zigbee_proxy_frame(const ZigbeeProxyFrame &msg);
|
||||
void on_zigbee_proxy_request(const ZigbeeProxyRequest &msg);
|
||||
void send_zigbee_proxy_frame(const ZigbeeProxyFrame &msg) { this->send_message(msg); }
|
||||
bool send_zigbee_proxy_frame(const ZigbeeProxyFrame &msg) { return this->send_message(msg); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
|
||||
@@ -399,6 +399,14 @@ void APIServer::on_zwave_proxy_request(const ZWaveProxyRequest &msg) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
void APIServer::on_zigbee_proxy_request(const ZigbeeProxyRequest &msg) {
|
||||
// Very infrequent and small - send to all clients rather than tracking a subscription
|
||||
for (auto &c : this->active_clients())
|
||||
c->send_message(msg);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
|
||||
void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_id, uint32_t key,
|
||||
const std::vector<int32_t> *timings) {
|
||||
|
||||
@@ -186,6 +186,9 @@ class APIServer final : public Component,
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
void on_zwave_proxy_request(const ZWaveProxyRequest &msg);
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
void on_zigbee_proxy_request(const ZigbeeProxyRequest &msg);
|
||||
#endif
|
||||
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
|
||||
void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector<int32_t> *timings);
|
||||
#endif
|
||||
|
||||
@@ -9,6 +9,8 @@ namespace esphome::zigbee_proxy {
|
||||
|
||||
static const char *const TAG = "zigbee_proxy";
|
||||
|
||||
static constexpr size_t ASH_MAX_LOG_BYTES = 168; // Cap verbose hex dumps (168 * 3 = 504 byte buffer)
|
||||
|
||||
// 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,
|
||||
@@ -112,22 +114,34 @@ void ZigbeeProxy::parse_control_byte_(uint8_t control) {
|
||||
// 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
|
||||
// Process the piggybacked ACK first: ackNum means "I expect frame N next" = "I received
|
||||
// up to N-1", and it is valid regardless of the DATA frame's own sequence ordering
|
||||
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);
|
||||
this->drain_ncp_tx_queue_();
|
||||
}
|
||||
|
||||
// Check sequence number
|
||||
if (frame_num != this->rx_sequence_) {
|
||||
if (retx && frame_num == ((this->rx_sequence_ - 1) & ASH_MAX_SEQUENCE)) {
|
||||
// Retransmission of a frame we already ACKed (our ACK was lost) - re-ACK and discard
|
||||
ESP_LOGV(TAG, "Duplicate DATA frame %d, re-sending ACK", frame_num);
|
||||
this->send_ack_frame_(this->rx_sequence_);
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
|
||||
// Backpressure: a client-bound frame is still waiting on API TX buffer space, so we
|
||||
// cannot forward another one. Do not ACK or consume - the NCP will retransmit.
|
||||
if (!this->boot_sequence_active_ && this->api_connection_ != nullptr && this->client_tx_pending_length_ > 0) {
|
||||
ESP_LOGV(TAG, "Client TX pending, deferring DATA frame %d to NCP retransmit", frame_num);
|
||||
return;
|
||||
}
|
||||
|
||||
// Increment RX sequence and send ACK (ack_num = next expected frame)
|
||||
@@ -157,6 +171,7 @@ void ZigbeeProxy::parse_control_byte_(uint8_t control) {
|
||||
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);
|
||||
this->drain_ncp_tx_queue_();
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -168,9 +183,10 @@ void ZigbeeProxy::parse_control_byte_(uint8_t control) {
|
||||
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
|
||||
// An NCP never sends RST in normal operation; treat it as a reset indication
|
||||
// and run the RSTACK handling to resynchronize state (nothing is transmitted here)
|
||||
ESP_LOGW(TAG, "Received unexpected RST frame from NCP, resynchronizing");
|
||||
uint8_t rstack_data[] = {0x02, 0x01, 0x00}; // Synthesized RSTACK payload
|
||||
this->handle_rstack_frame_(rstack_data, sizeof(rstack_data));
|
||||
break;
|
||||
}
|
||||
@@ -273,10 +289,13 @@ bool ZigbeeProxy::parse_byte_(uint8_t byte) {
|
||||
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
|
||||
// CRC failed - WARN logs byte count only; hex dump at VERBOSE (truncated to ASH_MAX_LOG_BYTES)
|
||||
ESP_LOGW(TAG, "CRC failed (%u bytes)", this->rx_buffer_index_);
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(ASH_MAX_LOG_BYTES)];
|
||||
#endif
|
||||
ESP_LOGV(TAG, "CRC failed frame: %s",
|
||||
format_hex_pretty(this->rx_buffer_.data(), this->rx_buffer_index_).c_str());
|
||||
format_hex_pretty_to(hex_buf, this->rx_buffer_.data(), this->rx_buffer_index_));
|
||||
this->send_nak_frame_(this->rx_sequence_);
|
||||
}
|
||||
this->parsing_state_ = ParsingState::WAIT_FLAG_START;
|
||||
@@ -312,11 +331,31 @@ bool ZigbeeProxy::parse_byte_(uint8_t byte) {
|
||||
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) {
|
||||
// Appends a byte with ASH stuffing (reserved: FLAG, ESCAPE, XON, XOFF, SUB, CAN);
|
||||
// returns false if it would exceed capacity
|
||||
static bool append_byte_stuffed(uint8_t *output, size_t capacity, size_t &pos, uint8_t byte) {
|
||||
const bool reserved = byte == ASH_FLAG_BYTE || byte == ASH_ESCAPE_BYTE || byte == 0x11 || byte == 0x13 ||
|
||||
byte == ASH_SUBSTITUTE_BYTE || byte == 0x1A;
|
||||
if (pos + (reserved ? 2 : 1) > capacity) {
|
||||
return false;
|
||||
}
|
||||
if (reserved) {
|
||||
output[pos++] = ASH_ESCAPE_BYTE;
|
||||
output[pos++] = byte ^ ASH_XOR_BYTE;
|
||||
} else {
|
||||
output[pos++] = byte;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t ZigbeeProxy::build_frame_(uint8_t *output, size_t capacity, 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
|
||||
if (capacity < 1) {
|
||||
return 0;
|
||||
}
|
||||
output[pos++] = ASH_FLAG_BYTE;
|
||||
|
||||
// Build control byte
|
||||
@@ -344,24 +383,17 @@ size_t ZigbeeProxy::build_frame_(uint8_t *output, const uint8_t *data, size_t le
|
||||
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 control byte with stuffing
|
||||
if (!append_byte_stuffed(output, capacity, pos, control)) {
|
||||
ESP_LOGE(TAG, "Frame too large for buffer (%u byte payload, %u byte buffer)", length, capacity);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 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;
|
||||
if (!append_byte_stuffed(output, capacity, pos, data[i])) {
|
||||
ESP_LOGE(TAG, "Frame too large for buffer (%u byte payload, %u byte buffer)", length, capacity);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,27 +403,12 @@ size_t ZigbeeProxy::build_frame_(uint8_t *output, const uint8_t *data, size_t le
|
||||
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;
|
||||
// Add CRC with stuffing (big-endian), then the end FLAG
|
||||
if (!append_byte_stuffed(output, capacity, pos, (crc >> 8) & 0xFF) ||
|
||||
!append_byte_stuffed(output, capacity, pos, crc & 0xFF) || pos + 1 > capacity) {
|
||||
ESP_LOGE(TAG, "Frame too large for buffer (%u byte payload, %u byte buffer)", length, capacity);
|
||||
return 0;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -32,6 +32,10 @@ static constexpr uint8_t ASH_MAX_RETRIES = 5; // Maximum retransmission a
|
||||
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
|
||||
|
||||
// Client -> NCP queue depth: frames accepted while the single ASH TX window is occupied.
|
||||
// Overflow beyond this is NAKed to the client, which retransmits.
|
||||
static constexpr uint8_t NCP_TX_QUEUE_SIZE = 2;
|
||||
|
||||
// IEEE address size
|
||||
static constexpr size_t ZIGBEE_IEEE_ADDR_SIZE = 8; // 64-bit IEEE address
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/util.h"
|
||||
#include "esphome/components/api/api_server.h"
|
||||
#include "ezsp_commands.h"
|
||||
|
||||
@@ -19,6 +20,12 @@ namespace esphome::zigbee_proxy {
|
||||
|
||||
static const char *const TAG = "zigbee_proxy";
|
||||
|
||||
static constexpr uint32_t BOOT_SEQUENCE_TIMEOUT_MS = 10000; // Overall boot-harvest timeout
|
||||
static constexpr uint32_t RECOVERY_RETRY_INTERVAL_MS = 30000; // Retry interval for a failed NCP link
|
||||
static constexpr uint32_t CLIENT_TX_RETRY_TIMEOUT_MS = 5000; // Give up on a backpressured client frame
|
||||
static constexpr size_t NETWORK_INFO_PAYLOAD_SIZE = 19; // ieee(8) + extended_pan(8) + pan_id(2) + channel(1)
|
||||
static constexpr size_t ZIGBEE_MAX_LOG_BYTES = 168; // Cap verbose hex dumps (168 * 3 = 504 byte buffer)
|
||||
|
||||
ZigbeeProxy *global_zigbee_proxy = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
ZigbeeProxy::ZigbeeProxy() { global_zigbee_proxy = this; }
|
||||
@@ -45,31 +52,54 @@ void ZigbeeProxy::loop() {
|
||||
this->handle_retransmission_();
|
||||
}
|
||||
|
||||
// Check for boot sequence timeout
|
||||
if (this->boot_sequence_active_) {
|
||||
uint32_t total_elapsed = millis() - this->boot_start_time_;
|
||||
if (total_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;
|
||||
}
|
||||
} else if (this->boot_state_ == BootState::WAIT_RSTACK && (millis() - this->setup_time_) > ASH_RESET_TIMEOUT) {
|
||||
// RST was sent before USB device finished enumeration — retry
|
||||
ESP_LOGD(TAG, "No RSTACK received within %u ms, retrying RST", ASH_RESET_TIMEOUT);
|
||||
this->setup_time_ = millis();
|
||||
this->send_rst_frame_();
|
||||
}
|
||||
this->check_boot_timeouts_();
|
||||
} else if (this->ash_state_ == AshState::CONNECTING && millis() - this->setup_time_ > ASH_RESET_TIMEOUT) {
|
||||
// Stuck in CONNECTING state (client-triggered RST, not the boot sequence)
|
||||
ESP_LOGE(TAG, "RSTACK timeout, NCP not responding");
|
||||
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");
|
||||
// Guard against a subscriber that disconnected without unsubscribing
|
||||
if (this->api_connection_ != nullptr && (!this->api_connection_->is_connection_setup() || !api_is_connected())) {
|
||||
ESP_LOGW(TAG, "Subscriber disconnected");
|
||||
this->unsubscribe_api_connection(this->api_connection_);
|
||||
}
|
||||
|
||||
// Retry any client-bound frame that hit API TX buffer backpressure
|
||||
this->try_send_pending_client_frame_();
|
||||
|
||||
// Send any queued client frames if the ASH TX window opened up
|
||||
this->drain_ncp_tx_queue_();
|
||||
|
||||
// Autonomous recovery: with no client subscribed, periodically retry a failed NCP link
|
||||
// (or a failed boot harvest) so a late-powered NCP does not require a client RST
|
||||
if (this->api_connection_ == nullptr &&
|
||||
(this->ash_state_ == AshState::FAILED ||
|
||||
(this->boot_state_ == BootState::FAILED && !this->boot_sequence_active_)) &&
|
||||
millis() - this->last_recovery_attempt_ > RECOVERY_RETRY_INTERVAL_MS) {
|
||||
ESP_LOGI(TAG, "Attempting NCP recovery");
|
||||
this->last_recovery_attempt_ = millis();
|
||||
this->reset_ash_protocol_();
|
||||
}
|
||||
}
|
||||
|
||||
void ZigbeeProxy::check_boot_timeouts_() {
|
||||
uint32_t total_elapsed = millis() - this->boot_start_time_;
|
||||
if (total_elapsed > BOOT_SEQUENCE_TIMEOUT_MS) {
|
||||
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;
|
||||
}
|
||||
} else if ((this->boot_state_ == BootState::WAIT_RSTACK || this->boot_state_ == BootState::WAIT_FINAL_RSTACK) &&
|
||||
(millis() - this->setup_time_) > ASH_RESET_TIMEOUT) {
|
||||
// RST was sent before USB device finished enumeration — retry
|
||||
ESP_LOGD(TAG, "No RSTACK received within %u ms, retrying RST", ASH_RESET_TIMEOUT);
|
||||
this->setup_time_ = millis();
|
||||
this->send_rst_frame_();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +136,24 @@ void ZigbeeProxy::dump_config() {
|
||||
|
||||
float ZigbeeProxy::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
|
||||
|
||||
bool ZigbeeProxy::can_proceed() { return this->ash_state_ == AshState::CONNECTED; }
|
||||
bool ZigbeeProxy::can_proceed() {
|
||||
// Block setup only while the boot harvest is running so network info (IEEE address,
|
||||
// PAN ID) is ready when the API starts. check_boot_timeouts_() guarantees forward
|
||||
// progress: a dead NCP flips the sequence to FAILED after BOOT_SEQUENCE_TIMEOUT_MS and
|
||||
// the device boots normally (recovery then happens from loop()).
|
||||
if (!this->boot_sequence_active_) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// loop() is not called while setup is blocked, so run the boot machinery here
|
||||
this->process_uart_();
|
||||
if (this->tx_buffer_pending_ && this->check_ack_timeout_()) {
|
||||
this->handle_retransmission_();
|
||||
}
|
||||
this->check_boot_timeouts_();
|
||||
|
||||
return !this->boot_sequence_active_;
|
||||
}
|
||||
|
||||
void ZigbeeProxy::api_connection_authenticated(api::APIConnection *conn) {
|
||||
// Notify client of network info if available
|
||||
@@ -118,7 +165,7 @@ void ZigbeeProxy::api_connection_authenticated(api::APIConnection *conn) {
|
||||
void ZigbeeProxy::zigbee_proxy_request(api::APIConnection *api_connection, const api::ZigbeeProxyRequest &msg) {
|
||||
switch (msg.type) {
|
||||
case api::enums::ZIGBEE_PROXY_REQUEST_TYPE_SUBSCRIBE:
|
||||
if (this->api_connection_ != nullptr) {
|
||||
if (this->api_connection_ != nullptr && this->api_connection_ != api_connection) {
|
||||
ESP_LOGW(TAG, "Another client is already subscribed");
|
||||
return;
|
||||
}
|
||||
@@ -130,16 +177,31 @@ void ZigbeeProxy::zigbee_proxy_request(api::APIConnection *api_connection, const
|
||||
case api::enums::ZIGBEE_PROXY_REQUEST_TYPE_UNSUBSCRIBE:
|
||||
if (this->api_connection_ == api_connection) {
|
||||
ESP_LOGD(TAG, "Client unsubscribed");
|
||||
this->api_connection_ = nullptr;
|
||||
this->unsubscribe_api_connection(api_connection);
|
||||
}
|
||||
break;
|
||||
|
||||
case api::enums::ZIGBEE_PROXY_REQUEST_TYPE_NETWORK_INFO:
|
||||
this->send_network_info_response_(api_connection);
|
||||
break;
|
||||
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unknown request type: %d", static_cast<int>(msg.type));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void ZigbeeProxy::unsubscribe_api_connection(api::APIConnection *conn) {
|
||||
if (this->api_connection_ != conn) {
|
||||
return;
|
||||
}
|
||||
this->api_connection_ = nullptr;
|
||||
// Frames belonging to the departed client's session must not linger
|
||||
this->client_tx_pending_length_ = 0;
|
||||
this->ncp_tx_queue_count_ = 0;
|
||||
this->client_reset_session_();
|
||||
}
|
||||
|
||||
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");
|
||||
@@ -160,24 +222,38 @@ uint64_t ZigbeeProxy::get_ieee_address() const {
|
||||
return addr;
|
||||
}
|
||||
|
||||
void ZigbeeProxy::send_frame(const uint8_t *data, size_t length) {
|
||||
bool 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;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this->tx_buffer_pending_) {
|
||||
ESP_LOGW(TAG, "Cannot send frame, previous frame still pending");
|
||||
return;
|
||||
// Transmit directly when the ASH window is free and nothing is queued ahead
|
||||
if (!this->tx_buffer_pending_ && this->ncp_tx_queue_count_ == 0) {
|
||||
return this->send_data_frame_(data, length, false);
|
||||
}
|
||||
|
||||
// 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);
|
||||
// Window occupied - queue for transmission when the pending frame is ACKed
|
||||
if (this->ncp_tx_queue_count_ >= NCP_TX_QUEUE_SIZE || length > MAX_ASH_FRAME_SIZE) {
|
||||
return false; // Caller NAKs the client, which retransmits
|
||||
}
|
||||
QueuedTxFrame &entry =
|
||||
this->ncp_tx_queue_[(this->ncp_tx_queue_head_ + this->ncp_tx_queue_count_) % NCP_TX_QUEUE_SIZE];
|
||||
memcpy(entry.data.data(), data, length);
|
||||
entry.length = length;
|
||||
this->ncp_tx_queue_count_++;
|
||||
ESP_LOGV(TAG, "Queued frame (%u bytes, %u queued)", length, this->ncp_tx_queue_count_);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ZigbeeProxy::drain_ncp_tx_queue_() {
|
||||
if (this->tx_buffer_pending_ || this->ncp_tx_queue_count_ == 0 || this->ash_state_ != AshState::CONNECTED) {
|
||||
return;
|
||||
}
|
||||
|
||||
this->send_data_frame_(data, length, false);
|
||||
QueuedTxFrame &entry = this->ncp_tx_queue_[this->ncp_tx_queue_head_];
|
||||
this->ncp_tx_queue_head_ = (this->ncp_tx_queue_head_ + 1) % NCP_TX_QUEUE_SIZE;
|
||||
this->ncp_tx_queue_count_--;
|
||||
this->send_data_frame_(entry.data.data(), entry.length, false);
|
||||
}
|
||||
|
||||
void ZigbeeProxy::set_timeout_config(uint32_t initial_ms, uint32_t min_ms, uint32_t max_ms) {
|
||||
@@ -203,6 +279,7 @@ void ZigbeeProxy::reset_ash_protocol_() {
|
||||
this->rx_sequence_ = 0;
|
||||
this->tx_buffer_pending_ = false;
|
||||
this->tx_retry_count_ = 0;
|
||||
this->ncp_tx_queue_count_ = 0;
|
||||
this->parsing_state_ = ParsingState::WAIT_FLAG_START;
|
||||
this->setup_time_ = millis();
|
||||
this->boot_start_time_ = this->setup_time_;
|
||||
@@ -224,9 +301,12 @@ void ZigbeeProxy::send_rst_frame_() {
|
||||
static constexpr size_t MAX_RST_FRAME_SIZE = 8;
|
||||
uint8_t combined[CAN_COUNT + MAX_RST_FRAME_SIZE];
|
||||
memset(combined, ASH_CAN_BYTE, CAN_COUNT);
|
||||
size_t rst_len = this->build_frame_(combined + CAN_COUNT, nullptr, 0, AshFrameType::RST);
|
||||
size_t rst_len = this->build_frame_(combined + CAN_COUNT, MAX_RST_FRAME_SIZE, nullptr, 0, AshFrameType::RST);
|
||||
|
||||
ESP_LOGV(TAG, "RST frame bytes (%u): %s", rst_len, format_hex_pretty(combined + CAN_COUNT, rst_len).c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(MAX_RST_FRAME_SIZE)];
|
||||
#endif
|
||||
ESP_LOGV(TAG, "RST frame bytes (%u): %s", rst_len, format_hex_pretty_to(hex_buf, combined + CAN_COUNT, rst_len));
|
||||
this->write_array(combined, CAN_COUNT + rst_len);
|
||||
this->flush();
|
||||
ESP_LOGV(TAG, "Sent RST frame (with %u CAN bytes prefix)", CAN_COUNT);
|
||||
@@ -336,7 +416,7 @@ void ZigbeeProxy::handle_error_frame_(const uint8_t *data, size_t length) {
|
||||
|
||||
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);
|
||||
size_t length = this->build_frame_(frame, sizeof(frame), nullptr, 0, AshFrameType::ACK, 0, ack_num);
|
||||
this->write_array(frame, length);
|
||||
this->last_ack_sent_ = ack_num;
|
||||
ESP_LOGV(TAG, "Sent ACK for frame %d", ack_num);
|
||||
@@ -345,23 +425,20 @@ bool ZigbeeProxy::send_ack_frame_(uint8_t ack_num) {
|
||||
|
||||
bool ZigbeeProxy::send_nak_frame_(uint8_t ack_num) {
|
||||
uint8_t frame[8];
|
||||
size_t length = this->build_frame_(frame, nullptr, 0, AshFrameType::NAK, 0, ack_num);
|
||||
size_t length = this->build_frame_(frame, sizeof(frame), nullptr, 0, AshFrameType::NAK, 0, ack_num);
|
||||
this->write_array(frame, length);
|
||||
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);
|
||||
// Build frame (returns 0 if the stuffed frame would exceed the buffer)
|
||||
size_t frame_length = this->build_frame_(this->tx_buffer_.data(), this->tx_buffer_.size(), data, length,
|
||||
AshFrameType::DATA, this->tx_sequence_, this->rx_sequence_, retransmit);
|
||||
if (frame_length == 0) {
|
||||
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);
|
||||
@@ -369,9 +446,12 @@ bool ZigbeeProxy::send_data_frame_(const uint8_t *data, size_t length, bool retr
|
||||
this->tx_pending_frame_num_ = this->tx_sequence_;
|
||||
}
|
||||
|
||||
// Debug: log exact bytes being sent
|
||||
// Debug: log exact bytes being sent (truncated to ZIGBEE_MAX_LOG_BYTES)
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(ZIGBEE_MAX_LOG_BYTES)];
|
||||
#endif
|
||||
ESP_LOGV(TAG, "TX DATA frame (%u bytes): %s", frame_length,
|
||||
format_hex_pretty(this->tx_buffer_.data(), frame_length).c_str());
|
||||
format_hex_pretty_to(hex_buf, this->tx_buffer_.data(), frame_length));
|
||||
|
||||
// Send frame
|
||||
this->write_array(this->tx_buffer_.data(), frame_length);
|
||||
@@ -441,15 +521,8 @@ void ZigbeeProxy::handle_retransmission_() {
|
||||
}
|
||||
|
||||
// Boot-time NCP initialization sequence
|
||||
// Sequence: RST -> RSTACK -> version() -> networkInit() -> stackStatus -> getNetworkParameters() -> RST -> RSTACK
|
||||
|
||||
void ZigbeeProxy::start_boot_sequence_() {
|
||||
ESP_LOGV(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_();
|
||||
}
|
||||
// Sequence: RST -> RSTACK -> version() -> getEui64() -> networkInit() -> stackStatus ->
|
||||
// getNetworkParameters() -> RST -> RSTACK
|
||||
|
||||
void ZigbeeProxy::advance_boot_state_() {
|
||||
switch (this->boot_state_) {
|
||||
@@ -458,6 +531,11 @@ void ZigbeeProxy::advance_boot_state_() {
|
||||
this->boot_state_ = BootState::WAIT_VERSION;
|
||||
break;
|
||||
|
||||
case BootState::SEND_GET_EUI64:
|
||||
this->send_get_eui64_();
|
||||
this->boot_state_ = BootState::WAIT_EUI64;
|
||||
break;
|
||||
|
||||
case BootState::SEND_NETWORK_INIT:
|
||||
this->send_network_init_();
|
||||
this->boot_state_ = BootState::WAIT_STACK_STATUS;
|
||||
@@ -486,7 +564,10 @@ void ZigbeeProxy::handle_boot_data_frame_(const uint8_t *data, size_t length) {
|
||||
//
|
||||
// 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.
|
||||
// parsing if the frame is too short for extended format. This heuristic (extended iff
|
||||
// negotiated v8+ AND frame >= 5 bytes) is validated against EFR32 EmberZNet 7.x NCPs;
|
||||
// a legacy-format response of 5+ bytes would be misparsed, but such NCPs have not been
|
||||
// observed in practice.
|
||||
|
||||
if (length < 3) {
|
||||
ESP_LOGW(TAG, "Boot frame too short: %u bytes", length);
|
||||
@@ -528,6 +609,12 @@ void ZigbeeProxy::handle_boot_data_frame_(const uint8_t *data, size_t length) {
|
||||
}
|
||||
break;
|
||||
|
||||
case BootState::WAIT_EUI64:
|
||||
if (frame_id == EZSP_GET_EUI64 && is_response) {
|
||||
this->handle_eui64_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);
|
||||
@@ -573,6 +660,17 @@ void ZigbeeProxy::send_ezsp_version_() {
|
||||
this->send_data_frame_(cmd, sizeof(cmd), false);
|
||||
}
|
||||
|
||||
void ZigbeeProxy::send_get_eui64_() {
|
||||
// getEui64 command - use legacy format for boot sequence compatibility
|
||||
uint8_t cmd[] = {
|
||||
this->ezsp_sequence_++, // Sequence
|
||||
EZSP_FRAME_CONTROL_COMMAND, // Frame control
|
||||
EZSP_GET_EUI64 & 0xFF // Frame ID
|
||||
};
|
||||
ESP_LOGV(TAG, "Sending EZSP getEui64 command (legacy format)");
|
||||
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
|
||||
@@ -625,8 +723,7 @@ void ZigbeeProxy::handle_version_response_(const uint8_t *data, size_t length) {
|
||||
// NCP accepted our requested version - treat as success
|
||||
ESP_LOGV(TAG, "NCP accepted EZSP v%d", ncp_version);
|
||||
this->ezsp_version_ = ncp_version;
|
||||
// Proceed to networkInit
|
||||
this->boot_state_ = BootState::SEND_NETWORK_INIT;
|
||||
this->boot_state_ = BootState::SEND_GET_EUI64;
|
||||
this->advance_boot_state_();
|
||||
return;
|
||||
}
|
||||
@@ -669,7 +766,18 @@ void ZigbeeProxy::handle_version_response_(const uint8_t *data, size_t length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Proceed to networkInit
|
||||
this->boot_state_ = BootState::SEND_GET_EUI64;
|
||||
this->advance_boot_state_();
|
||||
}
|
||||
|
||||
void ZigbeeProxy::handle_eui64_response_(const uint8_t *data, size_t length) {
|
||||
// getEui64 response: [eui64 (8 bytes, little-endian)]
|
||||
if (length >= ZIGBEE_IEEE_ADDR_SIZE) {
|
||||
this->set_ieee_address_(data);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "getEui64 response too short: %u bytes", length);
|
||||
}
|
||||
// Proceed to networkInit either way; the proxy works without an IEEE address
|
||||
this->boot_state_ = BootState::SEND_NETWORK_INIT;
|
||||
this->advance_boot_state_();
|
||||
}
|
||||
@@ -736,6 +844,7 @@ void ZigbeeProxy::handle_network_params_response_(const uint8_t *data, size_t le
|
||||
this->network_info_.channel = data[NETWORK_PARAMS_CHANNEL_OFFSET];
|
||||
|
||||
this->network_info_.valid = true;
|
||||
this->send_network_info_changed_msg_();
|
||||
|
||||
ESP_LOGD(TAG,
|
||||
"Network info:\n"
|
||||
@@ -768,12 +877,32 @@ bool ZigbeeProxy::set_ieee_address_(const uint8_t *new_address) {
|
||||
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_LOGV(TAG, "Network info changed notification");
|
||||
// Packed network info payload: ieee(8) + extended_pan(8) + pan_id(2) + channel(1), little-endian
|
||||
static void pack_network_info(const NetworkInfo &info, uint8_t *out) {
|
||||
memcpy(out, info.ieee_address.data(), ZIGBEE_IEEE_ADDR_SIZE);
|
||||
memcpy(out + 8, info.extended_pan_id.data(), 8);
|
||||
out[16] = info.pan_id & 0xFF;
|
||||
out[17] = (info.pan_id >> 8) & 0xFF;
|
||||
out[18] = info.channel;
|
||||
}
|
||||
|
||||
void ZigbeeProxy::send_network_info_changed_msg_(api::APIConnection *conn) {
|
||||
uint8_t payload[NETWORK_INFO_PAYLOAD_SIZE];
|
||||
pack_network_info(this->network_info_, payload);
|
||||
api::ZigbeeProxyRequest msg;
|
||||
msg.type = api::enums::ZIGBEE_PROXY_REQUEST_TYPE_NETWORK_INFO;
|
||||
msg.data = payload;
|
||||
msg.data_len = sizeof(payload);
|
||||
if (conn != nullptr) {
|
||||
conn->send_message(msg);
|
||||
} else if (api::global_api_server != nullptr) {
|
||||
// Very infrequent and small - send to all clients rather than tracking a subscription
|
||||
api::global_api_server->on_zigbee_proxy_request(msg);
|
||||
}
|
||||
}
|
||||
|
||||
void ZigbeeProxy::send_network_info_response_(api::APIConnection *conn) { this->send_network_info_changed_msg_(conn); }
|
||||
|
||||
// WiFi/Zigbee channel conflict detection
|
||||
void ZigbeeProxy::check_wifi_zigbee_conflict_() {
|
||||
#ifdef USE_WIFI
|
||||
@@ -830,49 +959,51 @@ void ZigbeeProxy::check_wifi_zigbee_conflict_() {
|
||||
#endif
|
||||
}
|
||||
|
||||
// Bootloader detection
|
||||
void ZigbeeProxy::check_bootloader_mode_(const uint8_t *data, size_t length) {
|
||||
if (length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Bootloader detection - fed consecutive raw byte pairs while the ASH link is not CONNECTED
|
||||
// (bootloader output only ever appears in place of the RSTACK after a reset)
|
||||
void ZigbeeProxy::check_bootloader_mode_(uint8_t prev_byte, uint8_t byte) {
|
||||
// Check for Silicon Labs bootloader menu prompt (0xC1 0x0D)
|
||||
if (data[0] == 0xC1 && data[1] == 0x0D) {
|
||||
if (prev_byte == 0xC1 && byte == 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");
|
||||
" 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 (byte == 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_LOGV(TAG, "NCP returned to normal operation");
|
||||
this->bootloader_state_ = BootloaderState::NORMAL;
|
||||
}
|
||||
}
|
||||
|
||||
// UART processing
|
||||
void ZigbeeProxy::process_uart_() {
|
||||
while (this->available()) {
|
||||
// UART processing (precondition: available() > 0, see inline process_uart_ in the header)
|
||||
void ZigbeeProxy::process_uart_slow_() {
|
||||
do {
|
||||
uint8_t byte;
|
||||
this->read_byte(&byte);
|
||||
if (!this->read_byte(&byte)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Verbose logging for debugging (ESP_LOGV already checks log level)
|
||||
ESP_LOGV(TAG, "RX: 0x%02X", byte);
|
||||
|
||||
if (this->ash_state_ != AshState::CONNECTED) {
|
||||
this->check_bootloader_mode_(this->last_rx_byte_, byte);
|
||||
this->last_rx_byte_ = byte;
|
||||
} else if (this->bootloader_state_ != BootloaderState::NORMAL) {
|
||||
// Normal traffic while connected clears any stale bootloader detection
|
||||
ESP_LOGV(TAG, "NCP returned to normal operation");
|
||||
this->bootloader_state_ = BootloaderState::NORMAL;
|
||||
}
|
||||
|
||||
this->parse_byte_(byte);
|
||||
}
|
||||
} while (this->available());
|
||||
}
|
||||
|
||||
// ==================== Client-side ASH session ====================
|
||||
@@ -882,50 +1013,93 @@ void ZigbeeProxy::client_reset_session_() {
|
||||
this->client_rx_sequence_ = 0;
|
||||
this->client_rx_buffer_index_ = 0;
|
||||
this->client_escape_next_byte_ = false;
|
||||
this->client_tx_pending_length_ = 0;
|
||||
this->client_ash_state_ = AshState::DISCONNECTED;
|
||||
this->client_parsing_state_ = ParsingState::WAIT_FLAG_START;
|
||||
ESP_LOGV(TAG, "Client ASH session reset");
|
||||
}
|
||||
|
||||
void ZigbeeProxy::send_to_client_(const uint8_t *data, size_t length) {
|
||||
bool ZigbeeProxy::send_to_client_(const uint8_t *data, size_t length) {
|
||||
if (this->api_connection_ == nullptr) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
this->outgoing_proto_msg_.data = data;
|
||||
this->outgoing_proto_msg_.data_len = length;
|
||||
this->api_connection_->send_zigbee_proxy_frame(this->outgoing_proto_msg_);
|
||||
return this->api_connection_->send_zigbee_proxy_frame(this->outgoing_proto_msg_);
|
||||
}
|
||||
|
||||
void ZigbeeProxy::client_send_raw_frame_(const uint8_t *frame, size_t length) { this->send_to_client_(frame, length); }
|
||||
void ZigbeeProxy::client_send_raw_frame_(const uint8_t *frame, size_t length) {
|
||||
// Failure here means API TX buffer backpressure. Losing a control frame is recoverable:
|
||||
// an unsent ACK triggers a client retransmit, which the duplicate-frame path re-ACKs.
|
||||
if (!this->send_to_client_(frame, length)) {
|
||||
ESP_LOGV(TAG, "Dropped %u byte control frame to client (API TX buffer full)", length);
|
||||
}
|
||||
}
|
||||
|
||||
void ZigbeeProxy::client_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);
|
||||
size_t length = this->build_frame_(frame, sizeof(frame), nullptr, 0, AshFrameType::ACK, 0, ack_num);
|
||||
this->client_send_raw_frame_(frame, length);
|
||||
ESP_LOGV(TAG, "Sent client ACK for frame %d", ack_num);
|
||||
}
|
||||
|
||||
void ZigbeeProxy::client_send_nak_frame_(uint8_t ack_num) {
|
||||
uint8_t frame[8];
|
||||
size_t length = this->build_frame_(frame, sizeof(frame), nullptr, 0, AshFrameType::NAK, 0, ack_num);
|
||||
this->client_send_raw_frame_(frame, length);
|
||||
ESP_LOGV(TAG, "Sent client NAK for frame %d", ack_num);
|
||||
}
|
||||
|
||||
void ZigbeeProxy::client_send_rstack_frame_(uint8_t reset_code) {
|
||||
// RSTACK payload: [version] [reset_code]
|
||||
uint8_t payload[] = {0x02, reset_code};
|
||||
uint8_t frame[16];
|
||||
size_t length = this->build_frame_(frame, payload, sizeof(payload), AshFrameType::RSTACK);
|
||||
size_t length = this->build_frame_(frame, sizeof(frame), payload, sizeof(payload), AshFrameType::RSTACK);
|
||||
this->client_send_raw_frame_(frame, length);
|
||||
ESP_LOGV(TAG, "Sent client RSTACK (code=0x%02X)", reset_code);
|
||||
}
|
||||
|
||||
void ZigbeeProxy::client_send_data_frame_(const uint8_t *data, size_t length) {
|
||||
size_t frame_length = this->build_frame_(this->client_tx_buffer_.data(), data, length, AshFrameType::DATA,
|
||||
this->client_tx_sequence_, this->client_rx_sequence_);
|
||||
size_t frame_length = this->build_frame_(this->client_tx_buffer_.data(), this->client_tx_buffer_.size(), data, length,
|
||||
AshFrameType::DATA, this->client_tx_sequence_, this->client_rx_sequence_);
|
||||
if (frame_length == 0) {
|
||||
return; // build_frame_ logged the error; payload cannot be represented in the buffer
|
||||
}
|
||||
this->client_tx_sequence_ = (this->client_tx_sequence_ + 1) & ASH_MAX_SEQUENCE;
|
||||
this->client_send_raw_frame_(this->client_tx_buffer_.data(), frame_length);
|
||||
if (!this->send_to_client_(this->client_tx_buffer_.data(), frame_length)) {
|
||||
// API TX buffer backpressure: keep the frame and retry from loop(). While a frame is
|
||||
// pending, incoming NCP DATA frames are left unACKed so the NCP provides flow control.
|
||||
ESP_LOGV(TAG, "Client DATA frame deferred (API TX buffer full)");
|
||||
this->client_tx_pending_length_ = frame_length;
|
||||
this->client_tx_pending_since_ = millis();
|
||||
return;
|
||||
}
|
||||
ESP_LOGV(TAG, "Sent client DATA frame, payload %u bytes", length);
|
||||
}
|
||||
|
||||
void ZigbeeProxy::try_send_pending_client_frame_() {
|
||||
if (this->client_tx_pending_length_ == 0) {
|
||||
return;
|
||||
}
|
||||
if (this->send_to_client_(this->client_tx_buffer_.data(), this->client_tx_pending_length_)) {
|
||||
ESP_LOGV(TAG, "Sent deferred client DATA frame");
|
||||
this->client_tx_pending_length_ = 0;
|
||||
return;
|
||||
}
|
||||
if (millis() - this->client_tx_pending_since_ > CLIENT_TX_RETRY_TIMEOUT_MS) {
|
||||
// The API connection is not draining; abandon the frame and force the client to
|
||||
// re-establish a clean ASH session (best effort - the ERROR frame may also fail)
|
||||
ESP_LOGE(TAG, "Client TX stalled for %u ms, resetting client session", CLIENT_TX_RETRY_TIMEOUT_MS);
|
||||
this->client_tx_pending_length_ = 0;
|
||||
this->client_send_error_frame_(static_cast<uint8_t>(EzspError::EXCEEDED_MAXIMUM_ACK_TIMEOUT_COUNT));
|
||||
this->client_reset_session_();
|
||||
}
|
||||
}
|
||||
|
||||
void ZigbeeProxy::client_send_error_frame_(uint8_t error_code) {
|
||||
uint8_t payload[] = {0x02, error_code};
|
||||
uint8_t frame[16];
|
||||
size_t length = this->build_frame_(frame, payload, sizeof(payload), AshFrameType::ERROR);
|
||||
size_t length = this->build_frame_(frame, sizeof(frame), payload, sizeof(payload), AshFrameType::ERROR);
|
||||
this->client_send_raw_frame_(frame, length);
|
||||
ESP_LOGV(TAG, "Sent client ERROR (code=0x%02X)", error_code);
|
||||
}
|
||||
@@ -935,21 +1109,26 @@ void ZigbeeProxy::forward_ncp_data_to_client_(const uint8_t *payload, size_t len
|
||||
}
|
||||
|
||||
void ZigbeeProxy::forward_ncp_rstack_to_client_(const uint8_t *data, size_t length) {
|
||||
// Build and send an RSTACK frame to the client with NCP's RSTACK data
|
||||
// RSTACK payload is [version] [reset_code] per spec; cap defensively so a malformed
|
||||
// NCP frame cannot overflow the stack buffer
|
||||
length = std::min(length, static_cast<size_t>(2));
|
||||
uint8_t frame[16];
|
||||
size_t frame_length = this->build_frame_(frame, data, length, AshFrameType::RSTACK);
|
||||
size_t frame_length = this->build_frame_(frame, sizeof(frame), data, length, AshFrameType::RSTACK);
|
||||
this->client_send_raw_frame_(frame, frame_length);
|
||||
|
||||
// Reset client-side sequence numbers since RSTACK means new session
|
||||
this->client_tx_sequence_ = 0;
|
||||
this->client_rx_sequence_ = 0;
|
||||
this->client_tx_pending_length_ = 0;
|
||||
this->client_ash_state_ = AshState::CONNECTED;
|
||||
ESP_LOGV(TAG, "Forwarded RSTACK to client");
|
||||
}
|
||||
|
||||
void ZigbeeProxy::forward_ncp_error_to_client_(const uint8_t *data, size_t length) {
|
||||
// ERROR payload is [version] [error_code] per spec; cap defensively (see RSTACK above)
|
||||
length = std::min(length, static_cast<size_t>(2));
|
||||
uint8_t frame[16];
|
||||
size_t frame_length = this->build_frame_(frame, data, length, AshFrameType::ERROR);
|
||||
size_t frame_length = this->build_frame_(frame, sizeof(frame), data, length, AshFrameType::ERROR);
|
||||
this->client_send_raw_frame_(frame, frame_length);
|
||||
ESP_LOGV(TAG, "Forwarded ERROR to client");
|
||||
}
|
||||
@@ -1075,21 +1254,37 @@ void ZigbeeProxy::client_parse_control_byte_(uint8_t control) {
|
||||
case AshFrameType::DATA: {
|
||||
// Verify sequence number
|
||||
if (frame_num != this->client_rx_sequence_) {
|
||||
ESP_LOGW(TAG, "Client: out of sequence DATA frame: expected %d, got %d", this->client_rx_sequence_, frame_num);
|
||||
uint8_t retx_bit = this->client_rx_buffer_[0] & 0x08;
|
||||
if (retx_bit != 0 && frame_num == ((this->client_rx_sequence_ - 1) & ASH_MAX_SEQUENCE)) {
|
||||
// Retransmission of a frame we already accepted (our ACK was lost) - re-ACK and discard
|
||||
ESP_LOGV(TAG, "Client: duplicate DATA frame %d, re-sending ACK", frame_num);
|
||||
this->client_send_ack_frame_(this->client_rx_sequence_);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Client: out of sequence DATA frame: expected %d, got %d", this->client_rx_sequence_,
|
||||
frame_num);
|
||||
this->client_send_nak_frame_(this->client_rx_sequence_);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this->client_rx_sequence_ = (this->client_rx_sequence_ + 1) & ASH_MAX_SEQUENCE;
|
||||
|
||||
// Extract EZSP payload (skip control byte, exclude CRC)
|
||||
size_t payload_length = this->client_rx_buffer_index_ > 3 ? this->client_rx_buffer_index_ - 3 : 0;
|
||||
const uint8_t *payload = this->client_rx_buffer_.data() + 1;
|
||||
|
||||
if (payload_length > 0) {
|
||||
// Forward EZSP payload to NCP via right-side ASH
|
||||
// Forward EZSP payload to NCP via right-side ASH; NAK without consuming if the
|
||||
// NCP link is down or the TX queue is full so the client retransmits
|
||||
ESP_LOGV(TAG, "Client DATA → NCP, EZSP payload %u bytes", payload_length);
|
||||
this->send_frame(payload, payload_length);
|
||||
if (!this->send_frame(payload, payload_length)) {
|
||||
this->client_send_nak_frame_(this->client_rx_sequence_);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Accepted: advance the sequence and ACK immediately rather than relying on the
|
||||
// piggybacked ACK of an eventual NCP response
|
||||
this->client_rx_sequence_ = (this->client_rx_sequence_ + 1) & ASH_MAX_SEQUENCE;
|
||||
this->client_send_ack_frame_(this->client_rx_sequence_);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1112,6 +1307,7 @@ void ZigbeeProxy::client_parse_control_byte_(uint8_t control) {
|
||||
this->rx_sequence_ = 0;
|
||||
this->tx_buffer_pending_ = false;
|
||||
this->tx_retry_count_ = 0;
|
||||
this->ncp_tx_queue_count_ = 0;
|
||||
this->parsing_state_ = ParsingState::WAIT_FLAG_START;
|
||||
this->client_reset_session_();
|
||||
this->send_rst_frame_();
|
||||
|
||||
@@ -51,6 +51,8 @@ enum class BootState : uint8_t {
|
||||
WAIT_RSTACK, // Sent RST, waiting for RSTACK
|
||||
SEND_VERSION, // Send EZSP version command
|
||||
WAIT_VERSION, // Waiting for version response
|
||||
SEND_GET_EUI64, // Send getEui64 command
|
||||
WAIT_EUI64, // Waiting for EUI64 response
|
||||
SEND_NETWORK_INIT, // Send networkInit command
|
||||
WAIT_STACK_STATUS, // Waiting for stackStatusHandler callback
|
||||
SEND_GET_NETWORK_PARAMS, // Send getNetworkParameters command
|
||||
@@ -76,6 +78,8 @@ class ZigbeeProxy : public uart::UARTDevice, public Component {
|
||||
void zigbee_proxy_request(api::APIConnection *api_connection, const api::ZigbeeProxyRequest &msg);
|
||||
void zigbee_proxy_frame(api::APIConnection *api_connection, const api::ZigbeeProxyFrame &msg);
|
||||
api::APIConnection *get_api_connection() { return this->api_connection_; }
|
||||
// Drop the subscription of a disconnecting client (called from APIConnection teardown)
|
||||
void unsubscribe_api_connection(api::APIConnection *conn);
|
||||
|
||||
// Feature flags
|
||||
uint32_t get_feature_flags() const { return ZigbeeProxyFeature::FEATURE_ZIGBEE_PROXY_ENABLED; }
|
||||
@@ -84,8 +88,10 @@ class ZigbeeProxy : public uart::UARTDevice, public Component {
|
||||
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);
|
||||
// Send an EZSP payload to the NCP: transmits immediately when the ASH link is idle,
|
||||
// otherwise queues it. Returns false if the link is down or the queue is full (caller
|
||||
// should NAK the client so it retransmits).
|
||||
bool 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);
|
||||
@@ -115,8 +121,10 @@ class ZigbeeProxy : public uart::UARTDevice, public Component {
|
||||
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);
|
||||
// Builds a stuffed frame into output; returns 0 if the frame (worst case 2*length + 8
|
||||
// bytes after byte stuffing) would exceed capacity.
|
||||
size_t build_frame_(uint8_t *output, size_t capacity, 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
|
||||
@@ -135,14 +143,19 @@ class ZigbeeProxy : public uart::UARTDevice, public Component {
|
||||
this->tx_retry_count_ = 0;
|
||||
}
|
||||
|
||||
// Client -> NCP pending frame queue (absorbs frames arriving while a TX is unacknowledged)
|
||||
void drain_ncp_tx_queue_();
|
||||
|
||||
// Boot-time NCP initialization
|
||||
void start_boot_sequence_();
|
||||
void advance_boot_state_();
|
||||
void check_boot_timeouts_();
|
||||
void handle_boot_data_frame_(const uint8_t *data, size_t length);
|
||||
void send_ezsp_version_();
|
||||
void send_get_eui64_();
|
||||
void send_network_init_();
|
||||
void send_get_network_params_();
|
||||
void handle_version_response_(const uint8_t *data, size_t length);
|
||||
void handle_eui64_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);
|
||||
|
||||
@@ -153,25 +166,42 @@ class ZigbeeProxy : public uart::UARTDevice, public Component {
|
||||
// WiFi/Zigbee channel conflict detection
|
||||
void check_wifi_zigbee_conflict_();
|
||||
|
||||
// Bootloader detection
|
||||
void check_bootloader_mode_(const uint8_t *data, size_t length);
|
||||
// Bootloader detection (fed consecutive raw byte pairs while not CONNECTED)
|
||||
void check_bootloader_mode_(uint8_t prev_byte, uint8_t byte);
|
||||
|
||||
// UART processing
|
||||
void process_uart_();
|
||||
// Inline fast-path: UART::available() is cheap (ring-buffer head/tail compare on most
|
||||
// backends), so an idle loop tick skips the out-of-line drain entirely. When bytes are
|
||||
// pending the slow path drains with do/while so available() is checked once per byte.
|
||||
ESPHOME_ALWAYS_INLINE void process_uart_() {
|
||||
if (!this->available()) {
|
||||
return;
|
||||
}
|
||||
this->process_uart_slow_();
|
||||
}
|
||||
// Precondition: caller must guarantee available() > 0 (see inline process_uart_ above)
|
||||
void process_uart_slow_();
|
||||
|
||||
// Client-side (left) ASH session
|
||||
void client_parse_byte_(uint8_t byte);
|
||||
void client_parse_control_byte_(uint8_t control);
|
||||
bool client_validate_frame_crc_();
|
||||
void client_send_ack_frame_(uint8_t ack_num);
|
||||
void client_send_nak_frame_(uint8_t ack_num);
|
||||
void client_send_rstack_frame_(uint8_t reset_code);
|
||||
void client_send_data_frame_(const uint8_t *data, size_t length);
|
||||
void client_send_error_frame_(uint8_t error_code);
|
||||
void client_send_raw_frame_(const uint8_t *frame, size_t length);
|
||||
void client_reset_session_();
|
||||
|
||||
// Send raw bytes to API client
|
||||
void send_to_client_(const uint8_t *data, size_t length);
|
||||
// Retry a client-bound DATA frame that failed to send (API TX buffer backpressure)
|
||||
void try_send_pending_client_frame_();
|
||||
|
||||
// Send raw bytes to API client; returns false if the API TX buffer rejected the message
|
||||
bool send_to_client_(const uint8_t *data, size_t length);
|
||||
|
||||
// Network info request/push (packed: ieee[8] + extended_pan[8] + pan_id[2] + channel[1], little-endian)
|
||||
void send_network_info_response_(api::APIConnection *conn);
|
||||
|
||||
// Forward NCP frames to client
|
||||
void forward_ncp_data_to_client_(const uint8_t *payload, size_t length);
|
||||
@@ -190,6 +220,13 @@ class ZigbeeProxy : public uart::UARTDevice, public Component {
|
||||
std::array<uint8_t, MAX_ASH_FRAME_SIZE> client_rx_buffer_;
|
||||
std::array<uint8_t, MAX_ASH_FRAME_SIZE> client_tx_buffer_;
|
||||
|
||||
// Client -> NCP queue: EZSP payloads accepted while the ASH TX window is occupied
|
||||
struct QueuedTxFrame {
|
||||
uint16_t length;
|
||||
std::array<uint8_t, MAX_ASH_FRAME_SIZE> data;
|
||||
};
|
||||
std::array<QueuedTxFrame, NCP_TX_QUEUE_SIZE> ncp_tx_queue_;
|
||||
|
||||
// Network information
|
||||
NetworkInfo network_info_;
|
||||
|
||||
@@ -200,10 +237,14 @@ class ZigbeeProxy : public uart::UARTDevice, public Component {
|
||||
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
|
||||
uint32_t setup_time_{0}; // Time when last RST frame was sent
|
||||
uint32_t boot_start_time_{0}; // Time when the boot sequence began (for overall timeout)
|
||||
uint32_t ack_timer_start_{0}; // Time when ACK timer started
|
||||
uint32_t last_rtt_ms_{0}; // Last measured round-trip time
|
||||
uint32_t last_recovery_attempt_{0}; // Time of last automatic reset attempt from FAILED
|
||||
|
||||
// Client-side (left) 32-bit values
|
||||
uint32_t client_tx_pending_since_{0}; // Time the pending client frame first failed to send
|
||||
|
||||
// NCP-side (right) 16-bit values
|
||||
uint16_t rx_buffer_index_{0}; // Index for populating rx_buffer_
|
||||
@@ -212,6 +253,7 @@ class ZigbeeProxy : public uart::UARTDevice, public Component {
|
||||
|
||||
// Client-side (left) 16-bit values
|
||||
uint16_t client_rx_buffer_index_{0};
|
||||
uint16_t client_tx_pending_length_{0}; // >0: client_tx_buffer_ holds an unsent DATA frame
|
||||
|
||||
// NCP-side (right) 8-bit values
|
||||
uint8_t tx_sequence_{0}; // TX sequence number (0-7)
|
||||
@@ -219,6 +261,9 @@ class ZigbeeProxy : public uart::UARTDevice, public Component {
|
||||
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
|
||||
uint8_t ncp_tx_queue_head_{0}; // Oldest entry in ncp_tx_queue_
|
||||
uint8_t ncp_tx_queue_count_{0}; // Number of queued entries
|
||||
uint8_t last_rx_byte_{0}; // Previous raw RX byte (bootloader detection)
|
||||
|
||||
// Client-side (left) 8-bit values
|
||||
uint8_t client_tx_sequence_{0}; // Client-facing TX sequence (proxy → client)
|
||||
|
||||
Reference in New Issue
Block a user