diff --git a/esphome/components/zigbee_proxy/ezsp_commands.h b/esphome/components/zigbee_proxy/ezsp_commands.h index 9575e3019f..08805ba6df 100644 --- a/esphome/components/zigbee_proxy/ezsp_commands.h +++ b/esphome/components/zigbee_proxy/ezsp_commands.h @@ -28,55 +28,47 @@ static constexpr uint8_t EZSP_FRAME_CONTROL_EXTENDED = 0x01; // Everything after that is extended, with no per-NCP exceptions. // EZSP Frame IDs - Commands (host to NCP) -static constexpr uint16_t EZSP_VERSION = 0x0000; // Version negotiation -static constexpr uint16_t EZSP_NETWORK_INIT = 0x0017; // Initialize network -static constexpr uint16_t EZSP_NETWORK_STATE = 0x0018; // Get network state -static constexpr uint16_t EZSP_GET_EUI64 = 0x0026; // Get IEEE address -static constexpr uint16_t EZSP_GET_NETWORK_PARAMETERS = 0x0028; // Get network parameters -static constexpr uint16_t EZSP_SET_CONFIGURATION_VALUE = 0x0053; // Set a stack config value +static constexpr uint16_t EZSP_VERSION = 0x0000; // Version negotiation +static constexpr uint16_t EZSP_GET_EUI64 = 0x0026; // Get IEEE address +static constexpr uint16_t EZSP_GET_TOKEN_DATA = 0x0102; // Read an NVM3 token -// 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; +// Network metadata comes straight out of NVM3 instead of from a running stack. +// NVM3KEY_STACK_NODE_DATA holds the PAN ID, channel, extended PAN ID and node type of +// the network this radio is commissioned onto, and reading it requires nothing beyond a +// completed version negotiation: no stack configuration, no networkInit, no waiting on +// stackStatusHandler, and above all no joining the network -- so simply plugging the +// device in never brings the radio up. +// +// Note the 0x0001 domain prefix on the NVM3 object key. The bare creator ID +// 0x0000EE64 is a different thing and getTokenData answers FAIL for it. +static constexpr uint32_t NVM3KEY_STACK_NODE_DATA = 0x0001EE64; -// EZSP Frame IDs - Callbacks (NCP to host, async) -static constexpr uint16_t EZSP_STACK_STATUS_HANDLER = 0x0019; // Stack status callback +// getTokenData response: [status (4)] [length (4)] [value (length)] +static constexpr size_t TOKEN_DATA_LENGTH_OFFSET = 4; +static constexpr size_t TOKEN_DATA_VALUE_OFFSET = 8; -// EZSP Network Status -enum class EzspNetworkStatus : uint8_t { - NO_NETWORK = 0x00, - JOINING_NETWORK = 0x01, - JOINED_NETWORK = 0x02, - JOINED_NETWORK_NO_PARENT = 0x03, - LEAVING_NETWORK = 0x04, -}; +// NV3StackNodeData value layout (16 bytes, little-endian): +// [panId (2)] [radioTxPower (1)] [radioFreqChannel (1)] [stackProfile (1)] +// [nodeType (1)] [zigbeeNodeId (2)] [extendedPanId (8)] +static constexpr size_t NV3_NODE_DATA_SIZE = 16; +static constexpr size_t NV3_NODE_DATA_PAN_ID_OFFSET = 0; +static constexpr size_t NV3_NODE_DATA_CHANNEL_OFFSET = 3; +static constexpr size_t NV3_NODE_DATA_NODE_TYPE_OFFSET = 5; +static constexpr size_t NV3_NODE_DATA_EXT_PAN_ID_OFFSET = 8; + +// A radio with no network still has the token, holding a sentinel rather than being +// absent: panId reads 0xFFFF and nodeType reads UNKNOWN_DEVICE. Detecting "no network" +// therefore means inspecting nodeType, not treating the read as failed. +static constexpr uint8_t NV3_NODE_TYPE_UNKNOWN_DEVICE = 0x00; // Status codes (subset). EZSP v13+ / EmberZNet 8.x report sl_status_t, not the -// legacy 8-bit EmberStatus, and the two disagree on every value that matters -// here: legacy NETWORK_UP/NOT_JOINED were 0x90/0x93. +// legacy 8-bit EmberStatus. enum class SlStatus : uint8_t { OK = 0x00, - NETWORK_UP = 0x15, - NETWORK_DOWN = 0x16, - NOT_JOINED = 0x17, }; // sl_status_t is 32-bit little-endian on the wire, so a status field occupies // four bytes even though every code used here fits in the first one. static constexpr size_t SL_STATUS_SIZE = 4; -// Network parameters structure offsets (in getNetworkParameters response) -// Response format: [status (4)] [nodeType (1)] [parameters (20)] = 25 bytes -// Parameters: [extendedPanId (8)] [panId (2)] [radioTxPower (1)] [radioChannel (1)] -// [joinMethod (1)] [nwkManagerId (2)] [nwkUpdateId (1)] [channels (4)] -static constexpr size_t NETWORK_PARAMS_STATUS_OFFSET = 0; -static constexpr size_t NETWORK_PARAMS_NODE_TYPE_OFFSET = 4; -static constexpr size_t NETWORK_PARAMS_EXT_PAN_ID_OFFSET = 5; -static constexpr size_t NETWORK_PARAMS_PAN_ID_OFFSET = 13; -static constexpr size_t NETWORK_PARAMS_CHANNEL_OFFSET = 16; -static constexpr size_t NETWORK_PARAMS_RESPONSE_SIZE = 25; - } // namespace esphome::zigbee_proxy diff --git a/esphome/components/zigbee_proxy/zigbee_proxy.cpp b/esphome/components/zigbee_proxy/zigbee_proxy.cpp index eac4075000..2dd87412f4 100644 --- a/esphome/components/zigbee_proxy/zigbee_proxy.cpp +++ b/esphome/components/zigbee_proxy/zigbee_proxy.cpp @@ -569,21 +569,16 @@ void ZigbeeProxy::handle_retransmission_() { this->start_ack_timer_(); } -// Boot-time NCP initialization sequence -// Sequence: RST -> RSTACK -> version() -> setConfigurationValue(STACK_PROFILE) -> -// networkInit() -> stackStatus -> getNetworkParameters() -> getEui64() -> -// RST -> RSTACK +// Boot-time NCP metadata harvest +// Sequence: RST -> RSTACK -> version() -> getTokenData(NVM3KEY_STACK_NODE_DATA) -> +// 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 -// orders it the same way. +// Both values are read with the stack left down. The alternative -- set the stack +// profile, call networkInit, wait for a stackStatusHandler callback announcing +// NETWORK_UP, then call getNetworkParameters -- does work, but it joins the network +// just to read four fields, so merely powering the device on brings the radio up. +// Reading the NVM3 token needs none of it, and getEui64 answers with the stack down +// as well, so nothing in this sequence starts the stack. void ZigbeeProxy::advance_boot_state_() { switch (this->boot_state_) { @@ -592,26 +587,16 @@ void ZigbeeProxy::advance_boot_state_() { this->boot_state_ = BootState::WAIT_VERSION; break; + case BootState::SEND_TOKEN_DATA: + this->send_get_token_data_(); + this->boot_state_ = BootState::WAIT_TOKEN_DATA; + break; + case BootState::SEND_GET_EUI64: this->send_get_eui64_(); 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; - break; - - case BootState::SEND_GET_NETWORK_PARAMS: - this->send_get_network_params_(); - this->boot_state_ = BootState::WAIT_NETWORK_PARAMS; - break; - case BootState::SEND_FINAL_RST: ESP_LOGV(TAG, "Sending final RST to reset NCP to clean state"); this->boot_state_ = BootState::WAIT_FINAL_RSTACK; @@ -684,46 +669,9 @@ 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 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); - if (status == static_cast(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; - - case BootState::WAIT_NETWORK_PARAMS: - if (frame_id == EZSP_GET_NETWORK_PARAMETERS && is_response) { - this->handle_network_params_response_(payload, payload_length); + case BootState::WAIT_TOKEN_DATA: + if (frame_id == EZSP_GET_TOKEN_DATA && is_response) { + this->handle_token_data_response_(payload, payload_length); } break; @@ -764,48 +712,25 @@ void ZigbeeProxy::send_get_eui64_() { this->send_data_frame_(cmd, sizeof(cmd), false); } -void ZigbeeProxy::send_stack_profile_() { +void ZigbeeProxy::send_get_token_data_() { + // getTokenData takes a 32-bit NVM3 key and a 32-bit index, both little-endian. 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) + this->ezsp_sequence_++, // Sequence + EZSP_FRAME_CONTROL_COMMAND, // Frame control (low) + EZSP_FRAME_CONTROL_EXTENDED, // Frame control (high) + EZSP_GET_TOKEN_DATA & 0xFF, // Frame ID (low) + (EZSP_GET_TOKEN_DATA >> 8) & 0xFF, // Frame ID (high) + NVM3KEY_STACK_NODE_DATA & 0xFF, // token + (NVM3KEY_STACK_NODE_DATA >> 8) & 0xFF, // + (NVM3KEY_STACK_NODE_DATA >> 16) & 0xFF, // + (NVM3KEY_STACK_NODE_DATA >> 24) & 0xFF, // + 0x00, // index + 0x00, // + 0x00, // + 0x00, // }; 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[] = { - this->ezsp_sequence_++, // Sequence - EZSP_FRAME_CONTROL_COMMAND, // Frame control (low) - EZSP_FRAME_CONTROL_EXTENDED, // Frame control (high) - EZSP_NETWORK_INIT & 0xFF, // Frame ID (low) - (EZSP_NETWORK_INIT >> 8) & 0xFF, // Frame ID (high) - 0x00, // networkInitStruct bitmask (default) - 0x00, - }; - ash_randomize(cmd, sizeof(cmd)); - ESP_LOGV(TAG, "Sending EZSP networkInit command"); - this->send_data_frame_(cmd, sizeof(cmd), false); -} - -void ZigbeeProxy::send_get_network_params_() { - uint8_t cmd[] = { - this->ezsp_sequence_++, // Sequence - EZSP_FRAME_CONTROL_COMMAND, // Frame control (low) - EZSP_FRAME_CONTROL_EXTENDED, // Frame control (high) - EZSP_GET_NETWORK_PARAMETERS & 0xFF, // Frame ID (low) - (EZSP_GET_NETWORK_PARAMETERS >> 8) & 0xFF, // Frame ID (high) - }; - ash_randomize(cmd, sizeof(cmd)); - ESP_LOGV(TAG, "Sending EZSP getNetworkParameters command"); + ESP_LOGV(TAG, "Sending EZSP getTokenData(NVM3KEY_STACK_NODE_DATA)"); this->send_data_frame_(cmd, sizeof(cmd), false); } @@ -835,7 +760,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_STACK_PROFILE; + this->boot_state_ = BootState::SEND_TOKEN_DATA; this->advance_boot_state_(); return; } @@ -901,7 +826,7 @@ void ZigbeeProxy::handle_version_response_(const uint8_t *data, size_t length) { return; } - this->boot_state_ = BootState::SEND_STACK_PROFILE; + this->boot_state_ = BootState::SEND_TOKEN_DATA; this->advance_boot_state_(); } @@ -917,69 +842,38 @@ void ZigbeeProxy::handle_eui64_response_(const uint8_t *data, size_t length) { this->advance_boot_state_(); } -void ZigbeeProxy::handle_stack_status_(const uint8_t *data, size_t length) { - // stackStatusHandler callback: [status] - if (length < 1) { - ESP_LOGW(TAG, "Stack status too short"); - return; - } - - uint8_t status = data[0]; - ESP_LOGV(TAG, "Stack status: 0x%02X", status); - - // Check for network up status - if (status == static_cast(SlStatus::NETWORK_UP) || status == static_cast(SlStatus::OK)) { - ESP_LOGV(TAG, "Network is up, querying parameters"); - this->boot_state_ = BootState::SEND_GET_NETWORK_PARAMS; - this->advance_boot_state_(); - } else if (status == static_cast(SlStatus::NOT_JOINED)) { - // No network configured, so there are no parameters to read -- but still read - // the EUI64. It is a hardware address that exists regardless of membership, and - // it is what lets a client tell an unformed radio apart from an unreachable one. - ESP_LOGD(TAG, "No network configured on NCP"); - this->boot_state_ = BootState::SEND_GET_EUI64; - this->advance_boot_state_(); - } else { - ESP_LOGW(TAG, "Unexpected stack status: 0x%02X, continuing anyway", status); - // Try to get network params anyway - this->boot_state_ = BootState::SEND_GET_NETWORK_PARAMS; - this->advance_boot_state_(); - } -} - -void ZigbeeProxy::handle_network_params_response_(const uint8_t *data, size_t length) { - // getNetworkParameters response: - // [status] [nodeType] [extendedPanId (8)] [panId (2)] [radioTxPower] [radioChannel] ... - // A 1-byte response (status only) is normal when the NCP has no network configured. - if (length < NETWORK_PARAMS_RESPONSE_SIZE) { - if (length <= SL_STATUS_SIZE) { - ESP_LOGD(TAG, "NCP has no network configured (status=0x%02X)", data[0]); - } else { - ESP_LOGW(TAG, "getNetworkParameters response unexpected length: %u bytes", length); - } +void ZigbeeProxy::handle_token_data_response_(const uint8_t *data, size_t length) { + // getTokenData response: [status (4)] [length (4)] [value (length)] + // The EUI64 read follows regardless of what happens here: it is a hardware address + // that exists whether or not the radio is commissioned, and it is what lets a client + // tell an unformed radio apart from an unreachable one. + if (length < TOKEN_DATA_VALUE_OFFSET + NV3_NODE_DATA_SIZE) { + ESP_LOGW(TAG, "getTokenData response too short: %u bytes", length); this->boot_state_ = BootState::SEND_GET_EUI64; this->advance_boot_state_(); return; } - uint8_t status = data[NETWORK_PARAMS_STATUS_OFFSET]; - if (status != static_cast(SlStatus::OK)) { - ESP_LOGW(TAG, "getNetworkParameters failed with status: 0x%02X", status); + if (data[0] != static_cast(SlStatus::OK)) { + ESP_LOGW(TAG, "getTokenData(NVM3KEY_STACK_NODE_DATA) failed: 0x%02X", data[0]); this->boot_state_ = BootState::SEND_GET_EUI64; this->advance_boot_state_(); return; } - // Extract Extended PAN ID (8 bytes, little-endian) - memcpy(this->network_info_.extended_pan_id.data(), data + NETWORK_PARAMS_EXT_PAN_ID_OFFSET, 8); + const uint8_t *node_data = data + TOKEN_DATA_VALUE_OFFSET; - // Extract PAN ID (2 bytes, little-endian) + if (node_data[NV3_NODE_DATA_NODE_TYPE_OFFSET] == NV3_NODE_TYPE_UNKNOWN_DEVICE) { + ESP_LOGD(TAG, "NCP has no network configured"); + this->boot_state_ = BootState::SEND_GET_EUI64; + this->advance_boot_state_(); + return; + } + + memcpy(this->network_info_.extended_pan_id.data(), node_data + NV3_NODE_DATA_EXT_PAN_ID_OFFSET, 8); this->network_info_.pan_id = - data[NETWORK_PARAMS_PAN_ID_OFFSET] | (static_cast(data[NETWORK_PARAMS_PAN_ID_OFFSET + 1]) << 8); - - // Extract channel - this->network_info_.channel = data[NETWORK_PARAMS_CHANNEL_OFFSET]; - + node_data[NV3_NODE_DATA_PAN_ID_OFFSET] | (static_cast(node_data[NV3_NODE_DATA_PAN_ID_OFFSET + 1]) << 8); + this->network_info_.channel = node_data[NV3_NODE_DATA_CHANNEL_OFFSET]; this->network_info_.valid = true; this->send_network_info_changed_msg_(); @@ -994,7 +888,6 @@ void ZigbeeProxy::handle_network_params_response_(const uint8_t *data, size_t le this->network_info_.extended_pan_id[1], this->network_info_.extended_pan_id[0], this->network_info_.pan_id, this->network_info_.channel); - // The stack is up, so the EUI64 can finally be read this->boot_state_ = BootState::SEND_GET_EUI64; this->advance_boot_state_(); } diff --git a/esphome/components/zigbee_proxy/zigbee_proxy.h b/esphome/components/zigbee_proxy/zigbee_proxy.h index b12b3d550d..66ff4d8627 100644 --- a/esphome/components/zigbee_proxy/zigbee_proxy.h +++ b/esphome/components/zigbee_proxy/zigbee_proxy.h @@ -47,22 +47,18 @@ enum ZigbeeProxyFeature : uint32_t { // Boot-time initialization state machine enum class BootState : uint8_t { - IDLE, // Not initializing - WAIT_RSTACK, // Sent RST, waiting for RSTACK - SEND_VERSION, // Send EZSP version command - WAIT_VERSION, // Waiting for version response - SEND_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 - WAIT_NETWORK_PARAMS, // Waiting for network parameters response - SEND_FINAL_RST, // Send final RST to reset NCP - WAIT_FINAL_RSTACK, // Waiting for final RSTACK - COMPLETE, // Boot sequence complete - FAILED, // Boot sequence failed + IDLE, // Not initializing + WAIT_RSTACK, // Sent RST, waiting for RSTACK + SEND_VERSION, // Send EZSP version command + WAIT_VERSION, // Waiting for version response + SEND_TOKEN_DATA, // Send getTokenData(NVM3KEY_STACK_NODE_DATA) + WAIT_TOKEN_DATA, // Waiting for token data response + SEND_GET_EUI64, // Send getEui64 command + WAIT_EUI64, // Waiting for EUI64 response + SEND_FINAL_RST, // Send final RST to reset NCP + WAIT_FINAL_RSTACK, // Waiting for final RSTACK + COMPLETE, // Boot sequence complete + FAILED, // Boot sequence failed }; class ZigbeeProxy : public uart::UARTDevice, public Component { @@ -157,13 +153,10 @@ 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 send_get_token_data_(); 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); + void handle_token_data_response_(const uint8_t *data, size_t length); // IEEE address and network info bool set_ieee_address_(const uint8_t *new_address); @@ -313,7 +306,7 @@ class ZigbeeProxy : public uart::UARTDevice, public Component { bool owns_uart_{false}; // True while this component drives the UART uint32_t configured_baud_rate_{0}; // Line rate to restore after another device - bool boot_sequence_active_{false}; // True during boot-time init + bool boot_sequence_active_{false}; // True during boot-time init }; extern ZigbeeProxy *global_zigbee_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)