This commit is contained in:
kbx81
2026-02-26 14:30:31 -06:00
parent 908c47bb5e
commit 156c2a8cb0
4 changed files with 71 additions and 56 deletions

View File

@@ -18,10 +18,10 @@ _DEFAULT_HW_INITIAL_TIMEOUT = 1600
_DEFAULT_HW_MIN_TIMEOUT = 400
_DEFAULT_HW_MAX_TIMEOUT = 3200
# Optimized ACK timeout values for USB CDC ACM paths (~20-30 ms round-trip)
_DEFAULT_USB_INITIAL_TIMEOUT = 200
_DEFAULT_USB_MIN_TIMEOUT = 50
_DEFAULT_USB_MAX_TIMEOUT = 500
# 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)
@@ -47,13 +47,13 @@ CONFIG_SCHEMA = cv.All(
esp8266=512,
default=1024,
),
# When usb_uart_id is present the component selects USB-optimized ACK
# timeout defaults (~20-30 ms round-trip) instead of the hardware UART
# defaults (~2-5 ms round-trip). Explicit timeout keys always win.
# 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=100, max=10000),
cv.Optional(CONF_MIN_TIMEOUT): cv.int_range(min=100, max=5000),
cv.Optional(CONF_MAX_TIMEOUT): cv.int_range(min=500, max=10000),
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)
@@ -78,10 +78,15 @@ async def to_code(config):
cg.add_define("ZIGBEE_PROXY_BUFFER_SIZE", config[CONF_BUFFER_SIZE])
# Select timeout defaults based on UART transport type.
# USB CDC ACM has higher round-trip latency (~20-30 ms) than hardware UART
# (~2-5 ms), so the adaptive ACK timeout algorithm needs different starting
# points to avoid unnecessary stalls at boot.
# 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,

View File

@@ -31,8 +31,8 @@ static const uint16_t CRC_TABLE[256] = {
0x1CE0, 0x0CC1, 0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8, 0x6E17, 0x7E36, 0x4E55, 0x5E74,
0x2E93, 0x3EB2, 0x0ED1, 0x1EF0};
uint16_t ZigbeeProxy::calculate_crc_(const uint8_t *data, size_t length) {
uint16_t crc = ASH_CRC_INIT;
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]];
}
@@ -241,8 +241,6 @@ bool ZigbeeProxy::parse_byte_(uint8_t byte) {
this->parsing_state_ = ParsingState::WAIT_DATA;
ESP_LOGV(TAG, "Frame start detected (control byte 0x%02X)", byte);
}
// Check for bootloader patterns
this->check_bootloader_mode_(&byte, 1);
break;
case ParsingState::WAIT_CONTROL:
@@ -275,8 +273,9 @@ bool ZigbeeProxy::parse_byte_(uint8_t byte) {
if (this->validate_frame_crc_()) {
this->parse_control_byte_(this->rx_buffer_[0]);
} else {
// CRC failed - log frame contents for debugging
ESP_LOGW(TAG, "CRC failed, frame (%u bytes): %s", this->rx_buffer_index_,
// 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_);
}
@@ -354,13 +353,6 @@ size_t ZigbeeProxy::build_frame_(uint8_t *output, const uint8_t *data, size_t le
output[pos++] = control;
}
// Prepare CRC calculation buffer (control + data)
uint8_t crc_buffer[MAX_ASH_FRAME_SIZE];
crc_buffer[0] = control;
if (length > 0) {
memcpy(crc_buffer + 1, data, length);
}
// Add data payload with stuffing
for (size_t i = 0; i < length; i++) {
uint8_t byte = data[i];
@@ -373,8 +365,11 @@ size_t ZigbeeProxy::build_frame_(uint8_t *output, const uint8_t *data, size_t le
}
}
// Calculate CRC
uint16_t crc = this->calculate_crc_(crc_buffer, 1 + length);
// 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;

View File

@@ -11,6 +11,10 @@
#include "esphome/components/wifi/wifi_component.h"
#endif
#ifdef USE_ZIGBEE_PROXY_USB_UART
#include "esphome/components/usb_uart/usb_uart.h"
#endif
namespace esphome::zigbee_proxy {
static const char *const TAG = "zigbee_proxy";
@@ -184,6 +188,13 @@ void ZigbeeProxy::set_timeout_config(uint32_t initial_ms, uint32_t min_ms, uint3
ESP_LOGV(TAG, "Timeout config updated: initial=%u, min=%u, max=%u", initial_ms, min_ms, max_ms);
}
#ifdef USE_ZIGBEE_PROXY_USB_UART
void ZigbeeProxy::set_usb_uart_channel(usb_uart::USBUartChannel *channel) {
channel->set_rx_callback([this]() { this->process_uart_(); });
ESP_LOGD(TAG, "Registered USB UART RX callback for low-latency processing");
}
#endif
// ASH Protocol State Machine
void ZigbeeProxy::reset_ash_protocol_() {
ESP_LOGV(TAG, "Resetting ASH protocol");
@@ -232,17 +243,15 @@ void ZigbeeProxy::handle_rstack_frame_(const uint8_t *data, size_t length) {
ESP_LOGV(TAG, "Received RSTACK, starting EZSP initialization");
this->ash_state_ = AshState::CONNECTED;
// Non-blocking drain: give the NCP 10 ms to settle and flush any stale bytes.
// yield() lets the USB task deliver buffered bytes into the ring buffer so they
// can be consumed here rather than contaminating the next EZSP exchange.
uint32_t drain_deadline = millis() + 10;
while (millis() < drain_deadline) {
while (this->available()) {
uint8_t discard;
this->read_byte(&discard);
ESP_LOGV(TAG, "Draining post-RSTACK byte: 0x%02X", discard);
}
yield();
// Drain any stale bytes that arrived before the RSTACK (e.g. leftover
// UART FIFO bytes on HW UART, or a partial prior frame on USB CDC).
// For USB CDC the input_buffer_ is already fully up-to-date at this point
// (the RX callback just moved all pending chunks into it), so this loop
// completes immediately rather than spinning with yield().
while (this->available()) {
uint8_t discard;
this->read_byte(&discard);
ESP_LOGV(TAG, "Draining post-RSTACK byte: 0x%02X", discard);
}
this->boot_state_ = BootState::SEND_VERSION;
@@ -329,7 +338,6 @@ bool ZigbeeProxy::send_ack_frame_(uint8_t ack_num) {
uint8_t frame[8];
size_t length = this->build_frame_(frame, nullptr, 0, AshFrameType::ACK, 0, ack_num);
this->write_array(frame, length);
this->flush();
this->last_ack_sent_ = ack_num;
ESP_LOGV(TAG, "Sent ACK for frame %d", ack_num);
return true;
@@ -339,7 +347,6 @@ bool ZigbeeProxy::send_nak_frame_(uint8_t ack_num) {
uint8_t frame[8];
size_t length = this->build_frame_(frame, nullptr, 0, AshFrameType::NAK, 0, ack_num);
this->write_array(frame, length);
this->flush();
ESP_LOGW(TAG, "Sent NAK for frame %d", ack_num);
return true;
}
@@ -368,7 +375,6 @@ bool ZigbeeProxy::send_data_frame_(const uint8_t *data, size_t length, bool retr
// Send frame
this->write_array(this->tx_buffer_.data(), frame_length);
this->flush();
// Start ACK timer
this->tx_buffer_pending_ = true;
@@ -431,7 +437,6 @@ void ZigbeeProxy::handle_retransmission_() {
// Resend the pending frame
this->write_array(this->tx_pending_buffer_.data(), this->tx_pending_length_);
this->flush();
this->start_ack_timer_();
}
@@ -749,14 +754,7 @@ void ZigbeeProxy::handle_network_params_response_(const uint8_t *data, size_t le
}
bool ZigbeeProxy::set_ieee_address_(const uint8_t *new_address) {
bool changed = false;
for (size_t i = 0; i < ZIGBEE_IEEE_ADDR_SIZE; i++) {
if (this->network_info_.ieee_address[i] != new_address[i]) {
changed = true;
break;
}
}
bool changed = memcmp(this->network_info_.ieee_address.data(), new_address, ZIGBEE_IEEE_ADDR_SIZE) != 0;
if (changed) {
memcpy(this->network_info_.ieee_address.data(), new_address, ZIGBEE_IEEE_ADDR_SIZE);
@@ -917,11 +915,10 @@ void ZigbeeProxy::client_send_rstack_frame_(uint8_t reset_code) {
}
void ZigbeeProxy::client_send_data_frame_(const uint8_t *data, size_t length) {
uint8_t frame[MAX_ASH_FRAME_SIZE];
size_t frame_length =
this->build_frame_(frame, data, length, AshFrameType::DATA, this->client_tx_sequence_, this->client_rx_sequence_);
size_t frame_length = this->build_frame_(this->client_tx_buffer_.data(), data, length, AshFrameType::DATA,
this->client_tx_sequence_, this->client_rx_sequence_);
this->client_tx_sequence_ = (this->client_tx_sequence_ + 1) & ASH_MAX_SEQUENCE;
this->client_send_raw_frame_(frame, frame_length);
this->client_send_raw_frame_(this->client_tx_buffer_.data(), frame_length);
ESP_LOGV(TAG, "Sent client DATA frame, payload %u bytes", length);
}

View File

@@ -12,6 +12,16 @@
#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
@@ -83,6 +93,14 @@ class ZigbeeProxy : public uart::UARTDevice, public Component {
void set_min_timeout(uint32_t timeout_ms) { this->timeout_config_.min_timeout_ms = timeout_ms; }
void set_max_timeout(uint32_t timeout_ms) { this->timeout_config_.max_timeout_ms = timeout_ms; }
#ifdef USE_ZIGBEE_PROXY_USB_UART
/// Called from generated code when 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_();
@@ -99,7 +117,7 @@ class ZigbeeProxy : public uart::UARTDevice, public Component {
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 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; }