Simplify startup state machine by using direct NVRAM access

This commit is contained in:
puddly
2026-08-05 14:40:38 -04:00
parent e80aa9579b
commit 973da47da6
3 changed files with 100 additions and 222 deletions
+30 -38
View File
@@ -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
+55 -162
View File
@@ -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<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;
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<uint8_t>(SlStatus::NETWORK_UP) || status == static_cast<uint8_t>(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<uint8_t>(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<uint8_t>(SlStatus::OK)) {
ESP_LOGW(TAG, "getNetworkParameters failed with status: 0x%02X", status);
if (data[0] != static_cast<uint8_t>(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<uint16_t>(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<uint16_t>(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_();
}
+15 -22
View File
@@ -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)