mirror of
https://github.com/esphome/esphome.git
synced 2026-10-07 11:26:39 +00:00
Handle more of the EZSP protocol and try to detect the bootloader
This commit is contained in:
@@ -47,6 +47,7 @@ void SerialProxy::loop() {
|
||||
!api_is_connected()) {
|
||||
ESP_LOGW(TAG, "Subscriber disconnected");
|
||||
this->api_connection_ = nullptr;
|
||||
this->parent_->release(this);
|
||||
this->disable_loop();
|
||||
return;
|
||||
}
|
||||
@@ -126,17 +127,26 @@ void SerialProxy::configure(api::APIConnection *api_connection, uint32_t baudrat
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply validated parameters
|
||||
uart_comp->set_baud_rate(baudrate);
|
||||
uart_comp->set_stop_bits(stop_bits);
|
||||
uart_comp->set_data_bits(data_size);
|
||||
|
||||
// Map parity value to UARTParityOptions
|
||||
// Skip a no-op reconfigure. Clients routinely re-send identical settings on every
|
||||
// port open, and on a USB UART each apply is a CDC SET_LINE_CODING control transfer.
|
||||
// Some bridges watch line-coding changes as a signalling channel (a magic baud
|
||||
// sequence to enter a bootloader, say), so redundant applies are not harmless.
|
||||
static const uart::UARTParityOptions PARITY_MAP[] = {
|
||||
uart::UART_CONFIG_PARITY_NONE,
|
||||
uart::UART_CONFIG_PARITY_EVEN,
|
||||
uart::UART_CONFIG_PARITY_ODD,
|
||||
};
|
||||
if (uart_comp->get_baud_rate() == baudrate && uart_comp->get_stop_bits() == stop_bits &&
|
||||
uart_comp->get_data_bits() == data_size && uart_comp->get_parity() == PARITY_MAP[parity]) {
|
||||
ESP_LOGV(TAG, "Settings unchanged, skipping reconfigure [%" PRIu32 "]", this->instance_index_);
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply validated parameters
|
||||
uart_comp->set_baud_rate(baudrate);
|
||||
uart_comp->set_stop_bits(stop_bits);
|
||||
uart_comp->set_data_bits(data_size);
|
||||
|
||||
uart_comp->set_parity(PARITY_MAP[parity]);
|
||||
|
||||
// load_settings() is available on ESP8266 and ESP32 platforms
|
||||
@@ -218,6 +228,10 @@ void SerialProxy::serial_proxy_request(api::APIConnection *api_connection, api::
|
||||
ESP_LOGW(TAG, "Previous subscriber disconnected; taking over subscription");
|
||||
}
|
||||
this->api_connection_ = api_connection;
|
||||
// Take the UART unconditionally. Another device may be bound to it (a protocol
|
||||
// proxy over the same radio, say); raw serial access is the more explicit, more
|
||||
// destructive operation -- typically a firmware update -- so it wins.
|
||||
this->parent_->claim(this);
|
||||
this->enable_loop();
|
||||
ESP_LOGV(TAG, "API connection subscribed to serial proxy [%" PRIu32 "]", this->instance_index_);
|
||||
break;
|
||||
@@ -227,6 +241,7 @@ void SerialProxy::serial_proxy_request(api::APIConnection *api_connection, api::
|
||||
return;
|
||||
}
|
||||
this->api_connection_ = nullptr;
|
||||
this->parent_->release(this);
|
||||
this->disable_loop();
|
||||
ESP_LOGV(TAG, "API connection unsubscribed from serial proxy [%" PRIu32 "]", this->instance_index_);
|
||||
break;
|
||||
|
||||
@@ -166,6 +166,26 @@ class UARTComponent {
|
||||
// @return Baud rate in bits per second.
|
||||
uint32_t get_baud_rate() const { return baud_rate_; }
|
||||
|
||||
/** Exclusive access for devices that share one bus.
|
||||
*
|
||||
* Several UARTDevices may be bound to the same UART while only one may drive it at a
|
||||
* time -- a protocol-aware proxy and a raw serial proxy over the same radio, say. The
|
||||
* claim is advisory: each device must check `is_claimed_by_other()` before reading or
|
||||
* writing.
|
||||
*
|
||||
* Claiming is unconditional so a claimant is never blocked by a holder that went away
|
||||
* without releasing (a client whose connection died silently, for instance). Callers
|
||||
* that want to defer to a live holder must check before claiming.
|
||||
*/
|
||||
void claim(void *owner) { this->exclusive_owner_ = owner; }
|
||||
void release(void *owner) {
|
||||
if (this->exclusive_owner_ == owner)
|
||||
this->exclusive_owner_ = nullptr;
|
||||
}
|
||||
bool is_claimed_by_other(void *owner) const {
|
||||
return this->exclusive_owner_ != nullptr && this->exclusive_owner_ != owner;
|
||||
}
|
||||
|
||||
#if defined(USE_ESP8266) || defined(USE_ESP32)
|
||||
/**
|
||||
* Load the UART settings.
|
||||
@@ -210,6 +230,7 @@ class UARTComponent {
|
||||
size_t rx_full_threshold_{RX_FULL_THRESHOLD_UNSET};
|
||||
size_t rx_timeout_{0};
|
||||
uint32_t baud_rate_{0};
|
||||
void *exclusive_owner_{nullptr}; // see claim()
|
||||
uint8_t stop_bits_{0};
|
||||
uint8_t data_bits_{0};
|
||||
UARTParityOptions parity_{UART_CONFIG_PARITY_NONE};
|
||||
|
||||
@@ -33,7 +33,14 @@ static constexpr uint16_t EZSP_NETWORK_INIT = 0x0017; // Initialize n
|
||||
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
|
||||
static constexpr uint16_t EZSP_LAUNCH_STANDALONE_BOOTLOADER = 0x008F; // Reboot into the bootloader
|
||||
static constexpr uint16_t EZSP_SET_CONFIGURATION_VALUE = 0x0053; // Set a stack config value
|
||||
|
||||
// Stack configuration. CONFIG_STACK_PROFILE must be set to ZigBee PRO before
|
||||
// networkInit, or the NCP answers NOT_JOINED for a node that is in fact joined
|
||||
// and getNetworkParameters then fails, leaving PAN ID and channel unreadable.
|
||||
// The NCP boots with stack profile 0, so this is not optional.
|
||||
static constexpr uint8_t EZSP_CONFIG_STACK_PROFILE = 0x0C;
|
||||
static constexpr uint16_t STACK_PROFILE_ZIGBEE_PRO = 2;
|
||||
|
||||
// EZSP Frame IDs - Callbacks (NCP to host, async)
|
||||
static constexpr uint16_t EZSP_STACK_STATUS_HANDLER = 0x0019; // Stack status callback
|
||||
|
||||
@@ -32,6 +32,10 @@ ZigbeeProxy::ZigbeeProxy() { global_zigbee_proxy = this; }
|
||||
|
||||
void ZigbeeProxy::setup() {
|
||||
this->setup_time_ = millis();
|
||||
// Remember the configured line rate. Another device sharing this UART may change it
|
||||
// (a flasher stepping through baud rates to reach a bootloader, say) and has no way
|
||||
// to know what to restore, so we put it back ourselves when we take the bus again.
|
||||
this->configured_baud_rate_ = this->parent_->get_baud_rate();
|
||||
|
||||
// Initialize state
|
||||
this->ash_state_ = AshState::DISCONNECTED;
|
||||
@@ -44,6 +48,38 @@ void ZigbeeProxy::setup() {
|
||||
}
|
||||
|
||||
void ZigbeeProxy::loop() {
|
||||
// Own the UART only while harvesting network info or serving a subscriber. Idling on
|
||||
// the bus otherwise would fight whichever device holds it -- a serial proxy carrying a
|
||||
// firmware update, say -- and the autonomous recovery below would inject ASH resets
|
||||
// into the middle of someone else's transfer.
|
||||
if (!this->should_own_uart_()) {
|
||||
if (this->owns_uart_) {
|
||||
this->owns_uart_ = false;
|
||||
if (this->api_connection_ != nullptr) {
|
||||
ESP_LOGW(TAG, "UART claimed by another device, dropping subscriber");
|
||||
this->unsubscribe_api_connection(this->api_connection_);
|
||||
}
|
||||
ESP_LOGD(TAG, "Released UART");
|
||||
this->boot_sequence_active_ = false;
|
||||
// The NCP may be reflashed while we are away, so nothing about the link can be
|
||||
// assumed on return.
|
||||
this->ash_state_ = AshState::FAILED;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this->owns_uart_) {
|
||||
ESP_LOGI(TAG, "Acquired UART, resetting NCP link");
|
||||
this->owns_uart_ = true;
|
||||
if (this->parent_->get_baud_rate() != this->configured_baud_rate_) {
|
||||
ESP_LOGI(TAG, "Restoring baud rate %" PRIu32 " (was %" PRIu32 ")", this->configured_baud_rate_,
|
||||
this->parent_->get_baud_rate());
|
||||
this->parent_->set_baud_rate(this->configured_baud_rate_);
|
||||
this->parent_->load_settings(false);
|
||||
}
|
||||
this->reset_ash_protocol_();
|
||||
}
|
||||
|
||||
// Process incoming UART data
|
||||
this->process_uart_();
|
||||
|
||||
@@ -72,9 +108,10 @@ void ZigbeeProxy::loop() {
|
||||
// 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 &&
|
||||
// Autonomous recovery: periodically retry a failed NCP link so a late-powered NCP does
|
||||
// not require a client RST. Only while subscribed -- with nobody listening there is
|
||||
// nothing to recover for, and resetting could disturb another device's use of the bus.
|
||||
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) {
|
||||
@@ -166,8 +203,15 @@ void ZigbeeProxy::zigbee_proxy_request(api::APIConnection *api_connection, const
|
||||
switch (msg.type) {
|
||||
case api::enums::ZIGBEE_PROXY_REQUEST_TYPE_SUBSCRIBE:
|
||||
if (this->api_connection_ != nullptr && this->api_connection_ != api_connection) {
|
||||
ESP_LOGW(TAG, "Another client is already subscribed");
|
||||
return;
|
||||
// A living subscriber keeps exclusive access. Its connection may be dead without
|
||||
// loop() having noticed yet (e.g. the client crashed and reconnected quickly);
|
||||
// in that case let the new client take over instead of locking it out for the
|
||||
// full API keepalive timeout.
|
||||
if (this->api_connection_->is_connection_setup()) {
|
||||
ESP_LOGW(TAG, "Another client is already subscribed");
|
||||
return;
|
||||
}
|
||||
ESP_LOGW(TAG, "Previous subscriber disconnected; taking over subscription");
|
||||
}
|
||||
ESP_LOGD(TAG, "Client subscribed");
|
||||
this->api_connection_ = api_connection;
|
||||
@@ -208,13 +252,6 @@ void ZigbeeProxy::zigbee_proxy_frame(api::APIConnection *api_connection, const a
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->in_raw_relay_()) {
|
||||
// The NCP is in the bootloader, so the client is speaking its menu/XMODEM
|
||||
// protocol rather than ASH. Pass it straight through.
|
||||
this->write_array(msg.data, msg.data_len);
|
||||
return;
|
||||
}
|
||||
|
||||
// Feed raw bytes into the client-side ASH parser
|
||||
for (size_t i = 0; i < msg.data_len; i++) {
|
||||
this->client_parse_byte_(msg.data[i]);
|
||||
@@ -281,7 +318,6 @@ void ZigbeeProxy::set_usb_uart_channel(usb_uart::USBUartChannel *channel) {
|
||||
// ASH Protocol State Machine
|
||||
void ZigbeeProxy::reset_ash_protocol_() {
|
||||
ESP_LOGV(TAG, "Resetting ASH protocol");
|
||||
this->raw_buffer_index_ = 0;
|
||||
this->ash_state_ = AshState::CONNECTING;
|
||||
this->tx_sequence_ = 0;
|
||||
this->rx_sequence_ = 0;
|
||||
@@ -534,8 +570,16 @@ void ZigbeeProxy::handle_retransmission_() {
|
||||
}
|
||||
|
||||
// Boot-time NCP initialization sequence
|
||||
// Sequence: RST -> RSTACK -> version() -> networkInit() -> stackStatus ->
|
||||
// getNetworkParameters() -> getEui64() -> RST -> RSTACK
|
||||
// Sequence: RST -> RSTACK -> version() -> setConfigurationValue(STACK_PROFILE) ->
|
||||
// networkInit() -> stackStatus -> getNetworkParameters() -> getEui64() ->
|
||||
// RST -> RSTACK
|
||||
//
|
||||
// The stack profile must be set before networkInit. The NCP boots with profile 0,
|
||||
// and on profile 0 it reports NOT_JOINED for a node that is joined, which makes
|
||||
// getNetworkParameters fail and leaves PAN ID and channel unreadable. Verified by
|
||||
// bisecting bellows' config writes against a ZBT-2 on a known network: profile 2
|
||||
// alone flips networkInit from NOT_JOINED to OK, and none of the other values
|
||||
// bellows writes make any difference.
|
||||
//
|
||||
// getEui64 comes last, after networkInit has brought the stack up: asking earlier
|
||||
// makes the NCP answer with error frame 0x0058 instead of the address. bellows
|
||||
@@ -553,6 +597,11 @@ void ZigbeeProxy::advance_boot_state_() {
|
||||
this->boot_state_ = BootState::WAIT_EUI64;
|
||||
break;
|
||||
|
||||
case BootState::SEND_STACK_PROFILE:
|
||||
this->send_stack_profile_();
|
||||
this->boot_state_ = BootState::WAIT_STACK_PROFILE;
|
||||
break;
|
||||
|
||||
case BootState::SEND_NETWORK_INIT:
|
||||
this->send_network_init_();
|
||||
this->boot_state_ = BootState::WAIT_STACK_STATUS;
|
||||
@@ -635,20 +684,39 @@ void ZigbeeProxy::handle_boot_data_frame_(const uint8_t *data, size_t length) {
|
||||
}
|
||||
break;
|
||||
|
||||
case BootState::WAIT_STACK_PROFILE:
|
||||
if (frame_id == EZSP_SET_CONFIGURATION_VALUE && is_response) {
|
||||
// Response is an EzspStatus, not an sl_status_t. A failure here is not fatal:
|
||||
// networkInit will report NOT_JOINED and the harvest still yields the IEEE
|
||||
// address, so log it and carry on rather than abandoning the sequence.
|
||||
if (payload_length >= 1 && payload[0] != 0x00) {
|
||||
ESP_LOGW(TAG, "setConfigurationValue(CONFIG_STACK_PROFILE) failed: 0x%02X", payload[0]);
|
||||
}
|
||||
this->boot_state_ = BootState::SEND_NETWORK_INIT;
|
||||
this->advance_boot_state_();
|
||||
}
|
||||
break;
|
||||
|
||||
case BootState::WAIT_STACK_STATUS:
|
||||
if (frame_id == EZSP_STACK_STATUS_HANDLER && is_callback) {
|
||||
this->handle_stack_status_(payload, payload_length);
|
||||
} else if (frame_id == EZSP_NETWORK_INIT && is_response) {
|
||||
// networkInit response contains a 32-bit sl_status_t
|
||||
// Some NCPs proceed directly without stackStatusHandler callback
|
||||
// networkInit response carries a 32-bit sl_status_t. It only reports that the
|
||||
// request was accepted -- the stack comes up asynchronously afterwards and
|
||||
// announces itself with a stackStatusHandler callback. Querying network
|
||||
// parameters here, before that callback, races the stack and the NCP answers
|
||||
// NOT_JOINED even when the radio is on a network.
|
||||
if (payload_length >= 1) {
|
||||
uint8_t status = payload[0];
|
||||
ESP_LOGV(TAG, "networkInit response: status=0x%02X", status);
|
||||
// Proceed to getNetworkParameters regardless of status
|
||||
// getNetworkParameters will tell us if there's actually a network
|
||||
ESP_LOGV(TAG, "networkInit complete, querying network parameters");
|
||||
this->boot_state_ = BootState::SEND_GET_NETWORK_PARAMS;
|
||||
this->advance_boot_state_();
|
||||
if (status == static_cast<uint8_t>(SlStatus::NOT_JOINED)) {
|
||||
// No network to bring up, so no callback is coming
|
||||
ESP_LOGD(TAG, "NCP has no network configured");
|
||||
this->boot_state_ = BootState::SEND_GET_EUI64;
|
||||
this->advance_boot_state_();
|
||||
}
|
||||
// Otherwise stay in WAIT_STACK_STATUS for the callback; the boot timeout
|
||||
// guarantees forward progress if it never arrives.
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -696,6 +764,22 @@ void ZigbeeProxy::send_get_eui64_() {
|
||||
this->send_data_frame_(cmd, sizeof(cmd), false);
|
||||
}
|
||||
|
||||
void ZigbeeProxy::send_stack_profile_() {
|
||||
uint8_t cmd[] = {
|
||||
this->ezsp_sequence_++, // Sequence
|
||||
EZSP_FRAME_CONTROL_COMMAND, // Frame control (low)
|
||||
EZSP_FRAME_CONTROL_EXTENDED, // Frame control (high)
|
||||
EZSP_SET_CONFIGURATION_VALUE & 0xFF, // Frame ID (low)
|
||||
(EZSP_SET_CONFIGURATION_VALUE >> 8) & 0xFF, // Frame ID (high)
|
||||
EZSP_CONFIG_STACK_PROFILE, // configId
|
||||
STACK_PROFILE_ZIGBEE_PRO & 0xFF, // value (low)
|
||||
(STACK_PROFILE_ZIGBEE_PRO >> 8) & 0xFF, // value (high)
|
||||
};
|
||||
ash_randomize(cmd, sizeof(cmd));
|
||||
ESP_LOGV(TAG, "Sending EZSP setConfigurationValue(CONFIG_STACK_PROFILE, 2)");
|
||||
this->send_data_frame_(cmd, sizeof(cmd), false);
|
||||
}
|
||||
|
||||
void ZigbeeProxy::send_network_init_() {
|
||||
// networkInitStruct: [bitmask (2 bytes)] - use 0x0000 for default
|
||||
uint8_t cmd[] = {
|
||||
@@ -751,7 +835,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;
|
||||
this->boot_state_ = BootState::SEND_NETWORK_INIT;
|
||||
this->boot_state_ = BootState::SEND_STACK_PROFILE;
|
||||
this->advance_boot_state_();
|
||||
return;
|
||||
}
|
||||
@@ -817,7 +901,7 @@ void ZigbeeProxy::handle_version_response_(const uint8_t *data, size_t length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this->boot_state_ = BootState::SEND_NETWORK_INIT;
|
||||
this->boot_state_ = BootState::SEND_STACK_PROFILE;
|
||||
this->advance_boot_state_();
|
||||
}
|
||||
|
||||
@@ -1012,99 +1096,31 @@ void ZigbeeProxy::check_wifi_zigbee_conflict_() {
|
||||
#endif
|
||||
}
|
||||
|
||||
// Bootloader detection, fed consecutive raw byte pairs. The Gecko bootloader speaks
|
||||
// neither ASH nor EZSP, so once detected the NCP link is relayed verbatim in both
|
||||
// directions (see process_uart_slow_ and zigbee_proxy_frame) and normal ASH parsing
|
||||
// is suspended -- a flasher needs the raw menu and XMODEM streams to reach the client.
|
||||
// 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). Detection is
|
||||
// advisory only: raw bootloader traffic is carried by a `serial_proxy` bound to the same
|
||||
// UART, not by this component.
|
||||
void ZigbeeProxy::check_bootloader_mode_(uint8_t prev_byte, uint8_t byte) {
|
||||
// An ASH RSTACK means the application is running again, so ASH resumes. This is the
|
||||
// only way out of the relay: while it is active nothing else parses the NCP stream.
|
||||
if (prev_byte == ASH_FLAG_BYTE && byte == static_cast<uint8_t>(AshFrameType::RSTACK)) {
|
||||
if (this->in_raw_relay_()) {
|
||||
ESP_LOGI(TAG, "NCP returned to application mode, resuming ASH");
|
||||
this->bootloader_state_ = BootloaderState::NORMAL;
|
||||
// Clear the failure that put us in the relay, so the RSTACK below is parsed
|
||||
// as ASH and the link comes back up on its own.
|
||||
this->ash_state_ = AshState::CONNECTING;
|
||||
this->parsing_state_ = ParsingState::WAIT_FLAG_START;
|
||||
this->raw_buffer_index_ = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Silicon Labs bootloader menu prompt (0xC1 0x0D)
|
||||
// Check for Silicon Labs bootloader menu prompt (0xC1 0x0D)
|
||||
if (prev_byte == 0xC1 && byte == 0x0D) {
|
||||
if (this->bootloader_state_ != BootloaderState::MENU) {
|
||||
ESP_LOGW(TAG, "NCP in bootloader menu mode, relaying raw bytes to client");
|
||||
ESP_LOGW(TAG, "NCP in bootloader menu mode detected\n"
|
||||
" Flash NCP firmware via the serial proxy, or power cycle the device");
|
||||
this->bootloader_state_ = BootloaderState::MENU;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// XMODEM-CRC poll ('C'), only meaningful once the bootloader is already talking:
|
||||
// treating it as an entry condition on its own would false-positive on ASH payload.
|
||||
if (byte == 0x43 && this->bootloader_state_ == BootloaderState::MENU) {
|
||||
ESP_LOGW(TAG, "NCP bootloader upload mode detected");
|
||||
this->bootloader_state_ = BootloaderState::DETECTED;
|
||||
// Check for upload begin (0x43)
|
||||
if (byte == 0x43) {
|
||||
if (this->bootloader_state_ != BootloaderState::DETECTED) {
|
||||
ESP_LOGW(TAG, "NCP bootloader upload mode detected");
|
||||
this->bootloader_state_ = BootloaderState::DETECTED;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// The NCP is not speaking ASH: either it is in its bootloader, or the ASH link gave
|
||||
// up entirely. Relaying raw in both cases is what lets a flasher reach -- and recover
|
||||
// -- a device that is already sitting in the bootloader when the proxy starts.
|
||||
bool ZigbeeProxy::in_raw_relay_() const {
|
||||
return this->bootloader_state_ != BootloaderState::NORMAL || this->ash_state_ == AshState::FAILED;
|
||||
}
|
||||
|
||||
// True if a client-bound EZSP payload is `launchStandaloneBootloader`. The payload is
|
||||
// still randomized here (it is forwarded to the NCP untouched), so only the frame ID
|
||||
// bytes are unmasked, using the fixed prefix of the ASH pseudo-random sequence.
|
||||
bool ZigbeeProxy::is_launch_bootloader_command_(const uint8_t *payload, size_t length) {
|
||||
// Extended EZSP header: [seq] [fc_lo] [fc_hi] [id_lo] [id_hi]
|
||||
if (length < 5) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t header[5];
|
||||
memcpy(header, payload, sizeof(header));
|
||||
ash_randomize(header, sizeof(header));
|
||||
|
||||
// Commands only; a response or callback carrying the same ID is the NCP's reply
|
||||
if ((header[1] & (EZSP_FRAME_CONTROL_RESPONSE | EZSP_FRAME_CONTROL_CALLBACK)) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t frame_id = header[3] | (static_cast<uint16_t>(header[4]) << 8);
|
||||
return frame_id == EZSP_LAUNCH_STANDALONE_BOOTLOADER;
|
||||
}
|
||||
|
||||
// Relay raw NCP bytes to the client, batching them so an XMODEM transfer does not
|
||||
// become one API message per byte. Flushed when the UART drains or the buffer fills.
|
||||
void ZigbeeProxy::queue_raw_to_client_(uint8_t byte) {
|
||||
if (this->api_connection_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
this->raw_buffer_[this->raw_buffer_index_++] = byte;
|
||||
if (this->raw_buffer_index_ >= this->raw_buffer_.size()) {
|
||||
this->flush_raw_to_client_();
|
||||
}
|
||||
}
|
||||
|
||||
void ZigbeeProxy::flush_raw_to_client_() {
|
||||
if (this->raw_buffer_index_ == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Best effort: the bootloader protocols carry their own retries, and there is no
|
||||
// ASH session to resynchronize here.
|
||||
if (!this->send_to_client_(this->raw_buffer_.data(), this->raw_buffer_index_)) {
|
||||
ESP_LOGW(TAG, "Dropped %u raw bytes to client (API TX buffer full)", this->raw_buffer_index_);
|
||||
}
|
||||
this->raw_buffer_index_ = 0;
|
||||
}
|
||||
|
||||
// UART processing (precondition: available() > 0, see inline process_uart_ in the header)
|
||||
void ZigbeeProxy::process_uart_slow_() {
|
||||
do {
|
||||
@@ -1116,23 +1132,17 @@ void ZigbeeProxy::process_uart_slow_() {
|
||||
// Verbose logging for debugging (ESP_LOGV already checks log level)
|
||||
ESP_LOGV(TAG, "RX: 0x%02X", byte);
|
||||
|
||||
// Runs unconditionally: a client can launch the bootloader over EZSP while the
|
||||
// ASH link is up, so detection cannot be gated on the link being down.
|
||||
this->check_bootloader_mode_(this->last_rx_byte_, byte);
|
||||
this->last_rx_byte_ = byte;
|
||||
|
||||
if (this->in_raw_relay_()) {
|
||||
// Bootloader traffic is not ASH. Relay it untouched and keep the parser out of
|
||||
// it: feeding menu text or XMODEM to parse_byte_ would fail CRC and NAK the NCP
|
||||
// mid-transfer.
|
||||
this->queue_raw_to_client_(byte);
|
||||
continue;
|
||||
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());
|
||||
|
||||
this->flush_raw_to_client_();
|
||||
}
|
||||
|
||||
// ==================== Client-side ASH session ====================
|
||||
@@ -1413,21 +1423,10 @@ void ZigbeeProxy::client_parse_control_byte_(uint8_t control) {
|
||||
// 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);
|
||||
bool launching_bootloader = is_launch_bootloader_command_(payload, payload_length);
|
||||
if (!this->send_frame(payload, payload_length)) {
|
||||
this->client_send_nak_frame_(this->client_rx_sequence_);
|
||||
return;
|
||||
}
|
||||
|
||||
if (launching_bootloader) {
|
||||
// The NCP is about to reboot into its bootloader, which speaks neither ASH
|
||||
// nor EZSP. Switch to the raw relay now: waiting to recognize bootloader
|
||||
// output would deadlock, since that output only appears once the client's
|
||||
// (non-ASH) bytes reach the NCP, and those are exactly what the relay
|
||||
// carries. Cleared again when an ASH RSTACK shows the app is back.
|
||||
ESP_LOGI(TAG, "Client launched NCP bootloader, relaying raw bytes");
|
||||
this->bootloader_state_ = BootloaderState::MENU;
|
||||
}
|
||||
}
|
||||
|
||||
// Accepted: advance the sequence and ACK immediately rather than relying on the
|
||||
|
||||
@@ -53,6 +53,8 @@ enum class BootState : uint8_t {
|
||||
WAIT_VERSION, // Waiting for version response
|
||||
SEND_GET_EUI64, // Send getEui64 command
|
||||
WAIT_EUI64, // Waiting for EUI64 response
|
||||
SEND_STACK_PROFILE, // Send setConfigurationValue(CONFIG_STACK_PROFILE)
|
||||
WAIT_STACK_PROFILE, // Waiting for setConfigurationValue response
|
||||
SEND_NETWORK_INIT, // Send networkInit command
|
||||
WAIT_STACK_STATUS, // Waiting for stackStatusHandler callback
|
||||
SEND_GET_NETWORK_PARAMS, // Send getNetworkParameters command
|
||||
@@ -155,6 +157,7 @@ class ZigbeeProxy : public uart::UARTDevice, public Component {
|
||||
void handle_boot_data_frame_(const uint8_t *data, size_t length);
|
||||
void send_ezsp_version_();
|
||||
void send_get_eui64_();
|
||||
void send_stack_profile_();
|
||||
void send_network_init_();
|
||||
void send_get_network_params_();
|
||||
void handle_version_response_(const uint8_t *data, size_t length);
|
||||
@@ -169,19 +172,27 @@ class ZigbeeProxy : public uart::UARTDevice, public Component {
|
||||
// WiFi/Zigbee channel conflict detection
|
||||
void check_wifi_zigbee_conflict_();
|
||||
|
||||
// Bootloader detection (fed consecutive raw byte pairs)
|
||||
// Bootloader detection (fed consecutive raw byte pairs while not CONNECTED)
|
||||
void check_bootloader_mode_(uint8_t prev_byte, uint8_t byte);
|
||||
// Raw NCP <-> client relay used while the NCP is in its bootloader
|
||||
bool in_raw_relay_() const;
|
||||
static bool is_launch_bootloader_command_(const uint8_t *payload, size_t length);
|
||||
void queue_raw_to_client_(uint8_t byte);
|
||||
void flush_raw_to_client_();
|
||||
|
||||
// True when this component should be driving the shared UART: nobody else holds a
|
||||
// claim on it, and we have either a subscriber to serve or a boot harvest to finish.
|
||||
bool should_own_uart_() const {
|
||||
return !this->parent_->is_claimed_by_other(const_cast<ZigbeeProxy *>(this)) &&
|
||||
(this->boot_sequence_active_ || this->api_connection_ != nullptr);
|
||||
}
|
||||
|
||||
// UART processing
|
||||
// Inline fast-path: UART::available() is cheap (ring-buffer head/tail compare on most
|
||||
// backends), so an idle loop tick skips the out-of-line drain entirely. When bytes are
|
||||
// pending the slow path drains with do/while so available() is checked once per byte.
|
||||
ESPHOME_ALWAYS_INLINE void process_uart_() {
|
||||
// Checked here rather than only in loop(): on a USB UART the RX callback calls this
|
||||
// directly from the USB component's loop, so a guard in loop() alone would still let
|
||||
// us consume bytes belonging to whichever device holds the claim.
|
||||
if (!this->owns_uart_ && !this->boot_sequence_active_) {
|
||||
return;
|
||||
}
|
||||
if (!this->available()) {
|
||||
return;
|
||||
}
|
||||
@@ -299,9 +310,9 @@ class ZigbeeProxy : public uart::UARTDevice, public Component {
|
||||
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 owns_uart_{false}; // True while this component drives the UART
|
||||
uint32_t configured_baud_rate_{0}; // Line rate to restore after another device
|
||||
|
||||
std::array<uint8_t, 128> raw_buffer_{}; // Bootloader relay batching buffer
|
||||
size_t raw_buffer_index_{0};
|
||||
bool boot_sequence_active_{false}; // True during boot-time init
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user