* fix(zigbee): Replace assert with error log to solve immediate crash * ci(pre-commit): Apply automatic fixes --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
806 lines
34 KiB
C++
806 lines
34 KiB
C++
/* Zigbee Core Functions */
|
||
|
||
#include "ZigbeeCore.h"
|
||
#if CONFIG_ZB_ENABLED
|
||
|
||
#include "ZigbeeHandlers.cpp"
|
||
#include "Arduino.h"
|
||
#include <set>
|
||
|
||
#ifdef __cplusplus
|
||
extern "C" {
|
||
#endif
|
||
#include "zboss_api.h"
|
||
extern zb_ret_t zb_nvram_write_dataset(zb_nvram_dataset_types_t t); // rejoin scanning workaround
|
||
extern void zb_set_ed_node_descriptor(bool power_src, bool rx_on_when_idle, bool alloc_addr); // sleepy device power mode workaround
|
||
#ifdef __cplusplus
|
||
}
|
||
#endif
|
||
|
||
static bool edBatteryPowered = false;
|
||
|
||
ZigbeeCore::ZigbeeCore() {
|
||
_radio_config.radio_mode = ZB_RADIO_MODE_NATIVE; // Use the native 15.4 radio
|
||
_host_config.host_connection_mode = ZB_HOST_CONNECTION_MODE_NONE; // Disable host connection
|
||
_zb_ep_list = esp_zb_ep_list_create();
|
||
_primary_channel_mask = ESP_ZB_TRANSCEIVER_ALL_CHANNELS_MASK;
|
||
_open_network = 0;
|
||
_scan_status = ZB_SCAN_FAILED;
|
||
_begin_timeout = ZB_BEGIN_TIMEOUT_DEFAULT;
|
||
_started = false;
|
||
_connected = false;
|
||
_scan_duration = 3; // default scan duration
|
||
_rx_on_when_idle = true;
|
||
_debug = false;
|
||
_global_default_response_cb = nullptr; // Initialize global callback to nullptr
|
||
if (!lock) {
|
||
lock = xSemaphoreCreateBinary();
|
||
if (lock == NULL) {
|
||
log_e("Semaphore creation failed");
|
||
}
|
||
}
|
||
}
|
||
|
||
//forward declaration
|
||
static esp_err_t zb_action_handler(esp_zb_core_action_callback_id_t callback_id, const void *message);
|
||
bool zb_apsde_data_indication_handler(esp_zb_apsde_data_ind_t ind);
|
||
|
||
bool ZigbeeCore::begin(esp_zb_cfg_t *role_cfg, bool erase_nvs) {
|
||
if (!zigbeeInit(role_cfg, erase_nvs)) {
|
||
log_e("ZigbeeCore begin failed");
|
||
return false;
|
||
}
|
||
_role = (zigbee_role_t)role_cfg->esp_zb_role;
|
||
if (xSemaphoreTake(lock, _begin_timeout) != pdTRUE) {
|
||
log_e("ZigbeeCore begin failed or timeout");
|
||
if (_role != ZIGBEE_COORDINATOR) { // Only End Device and Router can rejoin
|
||
resetNVRAMChannelMask();
|
||
}
|
||
}
|
||
return started();
|
||
}
|
||
|
||
bool ZigbeeCore::begin(zigbee_role_t role, bool erase_nvs) {
|
||
bool status = true;
|
||
switch (role) {
|
||
case ZIGBEE_COORDINATOR:
|
||
{
|
||
_role = ZIGBEE_COORDINATOR;
|
||
esp_zb_cfg_t zb_nwk_cfg = ZIGBEE_DEFAULT_COORDINATOR_CONFIG();
|
||
status = zigbeeInit(&zb_nwk_cfg, erase_nvs);
|
||
break;
|
||
}
|
||
case ZIGBEE_ROUTER:
|
||
{
|
||
_role = ZIGBEE_ROUTER;
|
||
esp_zb_cfg_t zb_nwk_cfg = ZIGBEE_DEFAULT_ROUTER_CONFIG();
|
||
status = zigbeeInit(&zb_nwk_cfg, erase_nvs);
|
||
break;
|
||
}
|
||
case ZIGBEE_END_DEVICE:
|
||
{
|
||
_role = ZIGBEE_END_DEVICE;
|
||
esp_zb_cfg_t zb_nwk_cfg = ZIGBEE_DEFAULT_ED_CONFIG();
|
||
status = zigbeeInit(&zb_nwk_cfg, erase_nvs);
|
||
break;
|
||
}
|
||
default: log_e("Invalid Zigbee Role"); return false;
|
||
}
|
||
if (!status || xSemaphoreTake(lock, _begin_timeout) != pdTRUE) {
|
||
log_e("ZigbeeCore begin failed or timeout");
|
||
if (_role != ZIGBEE_COORDINATOR) { // Only End Device and Router can rejoin
|
||
resetNVRAMChannelMask();
|
||
}
|
||
}
|
||
return started();
|
||
}
|
||
|
||
bool ZigbeeCore::addEndpoint(ZigbeeEP *ep) {
|
||
ep_objects.push_back(ep);
|
||
|
||
log_d("Endpoint: %d, Device ID: 0x%04x", ep->_endpoint, ep->_device_id);
|
||
//Register clusters and ep_list to the ZigbeeCore class's ep_list
|
||
if (ep->_ep_config.endpoint == 0 || ep->_cluster_list == nullptr) {
|
||
log_e("Endpoint config or Cluster list is not initialized, EP not added to ZigbeeCore's EP list");
|
||
return false;
|
||
}
|
||
esp_err_t ret = ESP_OK;
|
||
if (ep->_device_id == ESP_ZB_HA_HOME_GATEWAY_DEVICE_ID) {
|
||
ret = esp_zb_ep_list_add_gateway_ep(_zb_ep_list, ep->_cluster_list, ep->_ep_config);
|
||
} else {
|
||
ret = esp_zb_ep_list_add_ep(_zb_ep_list, ep->_cluster_list, ep->_ep_config);
|
||
}
|
||
if (ret != ESP_OK) {
|
||
log_e("Failed to add endpoint: 0x%x: %s", ret, esp_err_to_name(ret));
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
static void esp_zb_task(void *pvParameters) {
|
||
esp_zb_bdb_set_scan_duration(Zigbee.getScanDuration());
|
||
|
||
/* initialize Zigbee stack */
|
||
ESP_ERROR_CHECK(esp_zb_start(false));
|
||
|
||
//NOTE: This is a workaround to make battery powered devices to be discovered as battery powered
|
||
if (((zigbee_role_t)Zigbee.getRole() == ZIGBEE_END_DEVICE) && edBatteryPowered) {
|
||
zb_set_ed_node_descriptor(0, Zigbee.getRxOnWhenIdle(), 1);
|
||
}
|
||
|
||
esp_zb_stack_main_loop();
|
||
}
|
||
|
||
// Zigbee core init function
|
||
bool ZigbeeCore::zigbeeInit(esp_zb_cfg_t *zb_cfg, bool erase_nvs) {
|
||
// Zigbee platform configuration
|
||
esp_zb_platform_config_t platform_config = {
|
||
.radio_config = _radio_config,
|
||
.host_config = _host_config,
|
||
};
|
||
|
||
esp_err_t err = esp_zb_platform_config(&platform_config);
|
||
if (err != ESP_OK) {
|
||
log_e("Failed to configure Zigbee platform");
|
||
return false;
|
||
}
|
||
|
||
// Initialize Zigbee stack
|
||
log_d("Initialize Zigbee stack");
|
||
esp_zb_init(zb_cfg);
|
||
|
||
// Register all Zigbee EPs in list
|
||
if (ep_objects.empty()) {
|
||
log_w("No Zigbee EPs to register");
|
||
} else {
|
||
log_d("Register all Zigbee EPs in list");
|
||
err = esp_zb_device_register(_zb_ep_list);
|
||
if (err != ESP_OK) {
|
||
log_e("Failed to register Zigbee EPs");
|
||
return false;
|
||
}
|
||
|
||
//print the list of Zigbee EPs from ep_objects
|
||
log_i("List of registered Zigbee EPs:");
|
||
for (std::list<ZigbeeEP *>::iterator it = ep_objects.begin(); it != ep_objects.end(); ++it) {
|
||
log_i("Device type: %s, Endpoint: %d, Device ID: 0x%04x", getDeviceTypeString((*it)->_device_id), (*it)->_endpoint, (*it)->_device_id);
|
||
if ((*it)->_power_source == ZB_POWER_SOURCE_BATTERY) {
|
||
edBatteryPowered = true;
|
||
}
|
||
}
|
||
}
|
||
// Register Zigbee action handler
|
||
esp_zb_core_action_handler_register(zb_action_handler);
|
||
err = esp_zb_set_primary_network_channel_set(_primary_channel_mask);
|
||
if (err != ESP_OK) {
|
||
log_e("Failed to set primary network channel mask");
|
||
return false;
|
||
}
|
||
|
||
// Register APSDATA INDICATION handler to catch bind/unbind requests
|
||
esp_zb_aps_data_indication_handler_register(zb_apsde_data_indication_handler);
|
||
|
||
//Erase NVRAM before creating connection to new Coordinator
|
||
if (erase_nvs) {
|
||
esp_zb_nvram_erase_at_start(true);
|
||
}
|
||
|
||
// Create Zigbee task and start Zigbee stack
|
||
xTaskCreate(esp_zb_task, "Zigbee_main", 8192, NULL, 5, NULL);
|
||
|
||
return true;
|
||
}
|
||
|
||
void ZigbeeCore::setRadioConfig(esp_zb_radio_config_t config) {
|
||
_radio_config = config;
|
||
}
|
||
|
||
esp_zb_radio_config_t ZigbeeCore::getRadioConfig() {
|
||
return _radio_config;
|
||
}
|
||
|
||
void ZigbeeCore::setHostConfig(esp_zb_host_config_t config) {
|
||
_host_config = config;
|
||
}
|
||
|
||
esp_zb_host_config_t ZigbeeCore::getHostConfig() {
|
||
return _host_config;
|
||
}
|
||
|
||
void ZigbeeCore::setPrimaryChannelMask(uint32_t mask) {
|
||
_primary_channel_mask = mask;
|
||
}
|
||
|
||
void ZigbeeCore::setScanDuration(uint8_t duration) {
|
||
if (duration < 1 || duration > 4) {
|
||
log_e("Invalid scan duration, must be between 1 and 4");
|
||
return;
|
||
}
|
||
_scan_duration = duration;
|
||
}
|
||
|
||
void ZigbeeCore::setRebootOpenNetwork(uint8_t time) {
|
||
_open_network = time;
|
||
}
|
||
|
||
void ZigbeeCore::openNetwork(uint8_t time) {
|
||
if (started()) {
|
||
log_v("Opening network for joining for %d seconds", time);
|
||
esp_zb_bdb_open_network(time);
|
||
}
|
||
}
|
||
|
||
void ZigbeeCore::closeNetwork() {
|
||
if (started()) {
|
||
log_v("Closing network");
|
||
esp_zb_bdb_close_network();
|
||
}
|
||
}
|
||
|
||
static void bdb_start_top_level_commissioning_cb(uint8_t mode_mask) {
|
||
if (esp_zb_bdb_start_top_level_commissioning(mode_mask) != ESP_OK) {
|
||
log_e("Failed to start Zigbee commissioning");
|
||
}
|
||
}
|
||
|
||
void esp_zb_app_signal_handler(esp_zb_app_signal_t *signal_struct) {
|
||
//common variables
|
||
uint32_t *p_sg_p = signal_struct->p_app_signal;
|
||
esp_err_t err_status = signal_struct->esp_err_status;
|
||
esp_zb_app_signal_type_t sig_type = (esp_zb_app_signal_type_t)*p_sg_p;
|
||
//coordinator variables
|
||
esp_zb_zdo_signal_device_annce_params_t *dev_annce_params = NULL;
|
||
esp_zb_zdo_signal_leave_params_t *leave_params = NULL;
|
||
//router variables
|
||
esp_zb_zdo_signal_device_update_params_t *dev_update_params = NULL;
|
||
|
||
//main switch
|
||
switch (sig_type) {
|
||
case ESP_ZB_ZDO_SIGNAL_SKIP_STARTUP: // Common
|
||
log_i("Zigbee stack initialized");
|
||
log_d("Zigbee channel mask: 0x%08x", esp_zb_get_channel_mask());
|
||
esp_zb_bdb_start_top_level_commissioning(ESP_ZB_BDB_MODE_INITIALIZATION);
|
||
break;
|
||
case ESP_ZB_BDB_SIGNAL_DEVICE_FIRST_START: // Common
|
||
case ESP_ZB_BDB_SIGNAL_DEVICE_REBOOT: // Common
|
||
if (err_status == ESP_OK) {
|
||
log_i("Device started up in %s factory-reset mode", esp_zb_bdb_is_factory_new() ? "" : "non");
|
||
if (esp_zb_bdb_is_factory_new()) {
|
||
// Role specific code
|
||
if ((zigbee_role_t)Zigbee.getRole() == ZIGBEE_COORDINATOR) {
|
||
log_i("Start network formation");
|
||
esp_zb_bdb_start_top_level_commissioning(ESP_ZB_BDB_MODE_NETWORK_FORMATION);
|
||
} else {
|
||
log_i("Start network steering");
|
||
esp_zb_bdb_start_top_level_commissioning(ESP_ZB_BDB_MODE_NETWORK_STEERING);
|
||
Zigbee._started = true;
|
||
xSemaphoreGive(Zigbee.lock);
|
||
}
|
||
} else {
|
||
log_i("Device rebooted");
|
||
Zigbee._started = true;
|
||
xSemaphoreGive(Zigbee.lock);
|
||
if ((zigbee_role_t)Zigbee.getRole() == ZIGBEE_COORDINATOR && Zigbee._open_network > 0) {
|
||
log_i("Opening network for joining for %d seconds", Zigbee._open_network);
|
||
esp_zb_bdb_open_network(Zigbee._open_network);
|
||
} else {
|
||
// Save the channel mask to NVRAM in case of reboot which may be on a different channel after a change in the network
|
||
Zigbee.setNVRAMChannelMask(1 << esp_zb_get_current_channel());
|
||
Zigbee._connected = true; // Coordinator is always connected
|
||
}
|
||
Zigbee.searchBindings();
|
||
}
|
||
} else {
|
||
/* commissioning failed */
|
||
log_w("Commissioning failed, trying again...", esp_err_to_name(err_status));
|
||
esp_zb_scheduler_alarm((esp_zb_callback_t)bdb_start_top_level_commissioning_cb, ESP_ZB_BDB_MODE_INITIALIZATION, 500);
|
||
}
|
||
break;
|
||
case ESP_ZB_BDB_SIGNAL_FORMATION: // Coordinator
|
||
if ((zigbee_role_t)Zigbee.getRole() == ZIGBEE_COORDINATOR) {
|
||
if (err_status == ESP_OK) {
|
||
esp_zb_ieee_addr_t extended_pan_id;
|
||
esp_zb_get_extended_pan_id(extended_pan_id);
|
||
log_i(
|
||
"Formed network successfully (Extended PAN ID: %02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x, PAN ID: 0x%04hx, Channel:%d, Short Address: 0x%04hx)",
|
||
extended_pan_id[7], extended_pan_id[6], extended_pan_id[5], extended_pan_id[4], extended_pan_id[3], extended_pan_id[2], extended_pan_id[1],
|
||
extended_pan_id[0], esp_zb_get_pan_id(), esp_zb_get_current_channel(), esp_zb_get_short_address()
|
||
);
|
||
Zigbee._connected = true;
|
||
esp_zb_bdb_start_top_level_commissioning(ESP_ZB_BDB_MODE_NETWORK_STEERING);
|
||
} else {
|
||
log_i("Restart network formation (status: %s)", esp_err_to_name(err_status));
|
||
esp_zb_scheduler_alarm((esp_zb_callback_t)bdb_start_top_level_commissioning_cb, ESP_ZB_BDB_MODE_NETWORK_FORMATION, 1000);
|
||
}
|
||
}
|
||
break;
|
||
case ESP_ZB_BDB_SIGNAL_STEERING: // Router and End Device
|
||
if ((zigbee_role_t)Zigbee.getRole() == ZIGBEE_COORDINATOR) {
|
||
if (err_status == ESP_OK) {
|
||
log_i("Network steering started");
|
||
}
|
||
Zigbee._started = true;
|
||
xSemaphoreGive(Zigbee.lock);
|
||
} else {
|
||
if (err_status == ESP_OK) {
|
||
esp_zb_ieee_addr_t extended_pan_id;
|
||
esp_zb_get_extended_pan_id(extended_pan_id);
|
||
log_i(
|
||
"Joined network successfully (Extended PAN ID: %02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x, PAN ID: 0x%04hx, Channel:%d, Short Address: 0x%04hx)",
|
||
extended_pan_id[7], extended_pan_id[6], extended_pan_id[5], extended_pan_id[4], extended_pan_id[3], extended_pan_id[2], extended_pan_id[1],
|
||
extended_pan_id[0], esp_zb_get_pan_id(), esp_zb_get_current_channel(), esp_zb_get_short_address()
|
||
);
|
||
Zigbee._connected = true;
|
||
// Set channel mask and write to NVRAM, so that the device will re-join the network faster after reboot (scan only on the current channel)
|
||
Zigbee.setNVRAMChannelMask(1 << esp_zb_get_current_channel());
|
||
} else {
|
||
log_i("Network steering was not successful (status: %s)", esp_err_to_name(err_status));
|
||
esp_zb_scheduler_alarm((esp_zb_callback_t)bdb_start_top_level_commissioning_cb, ESP_ZB_BDB_MODE_NETWORK_STEERING, 1000);
|
||
}
|
||
}
|
||
break;
|
||
case ESP_ZB_ZDO_SIGNAL_DEVICE_ANNCE: // Coordinator
|
||
if ((zigbee_role_t)Zigbee.getRole() == ZIGBEE_COORDINATOR) {
|
||
dev_annce_params = (esp_zb_zdo_signal_device_annce_params_t *)esp_zb_app_signal_get_params(p_sg_p);
|
||
log_i("New device commissioned or rejoined (short: 0x%04hx)", dev_annce_params->device_short_addr);
|
||
esp_zb_zdo_match_desc_req_param_t cmd_req;
|
||
cmd_req.dst_nwk_addr = dev_annce_params->device_short_addr;
|
||
cmd_req.addr_of_interest = dev_annce_params->device_short_addr;
|
||
log_v("Device capabilities: 0x%02x", dev_annce_params->capability);
|
||
/*
|
||
capability:
|
||
Bit 0 – Alternate PAN Coordinator
|
||
Bit 1 – Device type: 1- ZigBee Router; 0 – End Device
|
||
Bit 2 – Power Source: 1 Main powered
|
||
Bit 3 – Receiver on when Idle
|
||
Bit 4 – Reserved
|
||
Bit 5 – Reserved
|
||
Bit 6 – Security capability
|
||
Bit 7 – Reserved
|
||
*/
|
||
// for each endpoint in the list call the findEndpoint function if not bounded or allowed to bind multiple devices
|
||
for (std::list<ZigbeeEP *>::iterator it = Zigbee.ep_objects.begin(); it != Zigbee.ep_objects.end(); ++it) {
|
||
log_d("Checking endpoint %d", (*it)->getEndpoint());
|
||
if (!(*it)->epUseManualBinding()) {
|
||
if (!(*it)->bound() || (*it)->epAllowMultipleBinding()) {
|
||
// Check if the device is already bound
|
||
bool found = false;
|
||
// Get the list of devices bound to the EP
|
||
std::list<zb_device_params_t *> bound_devices = (*it)->getBoundDevices();
|
||
for (std::list<zb_device_params_t *>::iterator device = bound_devices.begin(); device != bound_devices.end(); ++device) {
|
||
if (((*device)->short_addr == dev_annce_params->device_short_addr) || (memcmp((*device)->ieee_addr, dev_annce_params->ieee_addr, 8) == 0)) {
|
||
found = true;
|
||
log_d("Device already bound to endpoint %d", (*it)->getEndpoint());
|
||
break;
|
||
}
|
||
}
|
||
if (!found) {
|
||
log_d("Device not bound to endpoint %d and it is free to bound!", (*it)->getEndpoint());
|
||
(*it)->findEndpoint(&cmd_req);
|
||
log_d("Endpoint %d is searching for device", (*it)->getEndpoint());
|
||
break; // Only one endpoint per device
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
break;
|
||
case ESP_ZB_ZDO_SIGNAL_DEVICE_UPDATE: // Router
|
||
if ((zigbee_role_t)Zigbee.getRole() == ZIGBEE_ROUTER) {
|
||
dev_update_params = (esp_zb_zdo_signal_device_update_params_t *)esp_zb_app_signal_get_params(p_sg_p);
|
||
log_i("New device commissioned or rejoined (short: 0x%04hx)", dev_update_params->short_addr);
|
||
esp_zb_zdo_match_desc_req_param_t cmd_req;
|
||
cmd_req.dst_nwk_addr = dev_update_params->short_addr;
|
||
cmd_req.addr_of_interest = dev_update_params->short_addr;
|
||
// for each endpoint in the list call the findEndpoint function if not bounded or allowed to bind multiple devices
|
||
for (std::list<ZigbeeEP *>::iterator it = Zigbee.ep_objects.begin(); it != Zigbee.ep_objects.end(); ++it) {
|
||
log_d("Checking endpoint %d", (*it)->getEndpoint());
|
||
if (!(*it)->epUseManualBinding()) {
|
||
if (!(*it)->bound() || (*it)->epAllowMultipleBinding()) {
|
||
// Check if the device is already bound
|
||
bool found = false;
|
||
// Get the list of devices bound to the EP
|
||
std::list<zb_device_params_t *> bound_devices = (*it)->getBoundDevices();
|
||
for (std::list<zb_device_params_t *>::iterator device = bound_devices.begin(); device != bound_devices.end(); ++device) {
|
||
if (((*device)->short_addr == dev_update_params->short_addr) || (memcmp((*device)->ieee_addr, dev_update_params->long_addr, 8) == 0)) {
|
||
found = true;
|
||
log_d("Device already bound to endpoint %d", (*it)->getEndpoint());
|
||
break;
|
||
}
|
||
}
|
||
log_d("Device not bound to endpoint %d and it is free to bound!", (*it)->getEndpoint());
|
||
if (!found) {
|
||
(*it)->findEndpoint(&cmd_req);
|
||
log_d("Endpoint %d is searching for device", (*it)->getEndpoint());
|
||
break; // Only one endpoint per device
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
break;
|
||
case ESP_ZB_NWK_SIGNAL_PERMIT_JOIN_STATUS: // Coordinator
|
||
if ((zigbee_role_t)Zigbee.getRole() == ZIGBEE_COORDINATOR) {
|
||
if (err_status == ESP_OK) {
|
||
if (*(uint8_t *)esp_zb_app_signal_get_params(p_sg_p)) {
|
||
log_i("Network(0x%04hx) is open for %d seconds", esp_zb_get_pan_id(), *(uint8_t *)esp_zb_app_signal_get_params(p_sg_p));
|
||
} else {
|
||
log_i("Network(0x%04hx) closed, devices joining not allowed.", esp_zb_get_pan_id());
|
||
}
|
||
}
|
||
}
|
||
break;
|
||
case ESP_ZB_ZDO_SIGNAL_LEAVE: // End Device + Router
|
||
// Received signal to leave the network
|
||
if ((zigbee_role_t)Zigbee.getRole() != ZIGBEE_COORDINATOR) {
|
||
leave_params = (esp_zb_zdo_signal_leave_params_t *)esp_zb_app_signal_get_params(p_sg_p);
|
||
log_v("Signal to leave the network, leave type: %d", leave_params->leave_type);
|
||
if (leave_params->leave_type == ESP_ZB_NWK_LEAVE_TYPE_RESET) { // Leave without rejoin -> Factory reset
|
||
log_i("Leave without rejoin, factory reset the device");
|
||
Zigbee.factoryReset(true);
|
||
} else { // Leave with rejoin -> Rejoin the network, only reboot the device
|
||
log_i("Leave with rejoin, only reboot the device");
|
||
ESP.restart();
|
||
}
|
||
}
|
||
break;
|
||
default: log_v("ZDO signal: %s (0x%x), status: %s", esp_zb_zdo_signal_to_string(sig_type), sig_type, esp_err_to_name(err_status)); break;
|
||
}
|
||
}
|
||
|
||
// APS DATA INDICATION HANDLER to catch bind/unbind requests
|
||
bool zb_apsde_data_indication_handler(esp_zb_apsde_data_ind_t ind) {
|
||
if (Zigbee.getDebugMode()) {
|
||
log_d("APSDE INDICATION - Received APSDE-DATA indication, status: %d", ind.status);
|
||
log_d(
|
||
"APSDE INDICATION - dst_endpoint: %d, src_endpoint: %d, dst_addr_mode: %d, src_addr_mode: %d, cluster_id: 0x%04x, asdu_length: %d", ind.dst_endpoint,
|
||
ind.src_endpoint, ind.dst_addr_mode, ind.src_addr_mode, ind.cluster_id, ind.asdu_length
|
||
);
|
||
log_d(
|
||
"APSDE INDICATION - dst_short_addr: 0x%04x, src_short_addr: 0x%04x, profile_id: 0x%04x, security_status: %d, lqi: %d, rx_time: %d", ind.dst_short_addr,
|
||
ind.src_short_addr, ind.profile_id, ind.security_status, ind.lqi, ind.rx_time
|
||
);
|
||
}
|
||
if (ind.status == 0x00) {
|
||
// Catch bind/unbind requests to update the bound devices list
|
||
if (ind.cluster_id == 0x21 || ind.cluster_id == 0x22) {
|
||
Zigbee.searchBindings();
|
||
}
|
||
} else {
|
||
log_e("APSDE INDICATION - Invalid status of APSDE-DATA indication, error code: %d", ind.status);
|
||
}
|
||
return false; //False to let the stack process the message as usual
|
||
}
|
||
|
||
void ZigbeeCore::factoryReset(bool restart) {
|
||
if (restart) {
|
||
log_v("Factory resetting Zigbee stack, device will reboot");
|
||
esp_zb_factory_reset();
|
||
} else {
|
||
log_v("Factory resetting Zigbee NVRAM to factory default");
|
||
log_w("The device will not reboot, to take effect please reboot the device manually");
|
||
esp_zb_zcl_reset_nvram_to_factory_default();
|
||
}
|
||
}
|
||
|
||
void ZigbeeCore::scanCompleteCallback(esp_zb_zdp_status_t zdo_status, uint8_t count, esp_zb_network_descriptor_t *nwk_descriptor) {
|
||
log_v("Zigbee network scan complete");
|
||
if (zdo_status == ESP_ZB_ZDP_STATUS_SUCCESS) {
|
||
log_v("Found %d networks", count);
|
||
//print Zigbee networks
|
||
for (int i = 0; i < count; i++) {
|
||
log_v(
|
||
"Network %d: PAN ID: 0x%04hx, Permit Joining: %s, Extended PAN ID: %02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x, Channel: %d, Router Capacity: %s, End "
|
||
"Device Capacity: %s",
|
||
i, nwk_descriptor[i].short_pan_id, nwk_descriptor[i].permit_joining ? "Yes" : "No", nwk_descriptor[i].extended_pan_id[7],
|
||
nwk_descriptor[i].extended_pan_id[6], nwk_descriptor[i].extended_pan_id[5], nwk_descriptor[i].extended_pan_id[4], nwk_descriptor[i].extended_pan_id[3],
|
||
nwk_descriptor[i].extended_pan_id[2], nwk_descriptor[i].extended_pan_id[1], nwk_descriptor[i].extended_pan_id[0], nwk_descriptor[i].logic_channel,
|
||
nwk_descriptor[i].router_capacity ? "Yes" : "No", nwk_descriptor[i].end_device_capacity ? "Yes" : "No"
|
||
);
|
||
}
|
||
//save scan result and update scan status
|
||
//copy network descriptor to _scan_result to keep the data after the callback
|
||
Zigbee._scan_result = (esp_zb_network_descriptor_t *)malloc(count * sizeof(esp_zb_network_descriptor_t));
|
||
memcpy(Zigbee._scan_result, nwk_descriptor, count * sizeof(esp_zb_network_descriptor_t));
|
||
Zigbee._scan_status = count;
|
||
} else {
|
||
log_e("Failed to scan Zigbee network (status: 0x%x)", zdo_status);
|
||
Zigbee._scan_status = ZB_SCAN_FAILED;
|
||
Zigbee._scan_result = nullptr;
|
||
}
|
||
}
|
||
|
||
void ZigbeeCore::scanNetworks(u_int32_t channel_mask, u_int8_t scan_duration) {
|
||
if (!started()) {
|
||
log_e("Zigbee stack is not started, cannot scan networks");
|
||
return;
|
||
}
|
||
log_v("Scanning Zigbee networks");
|
||
esp_zb_zdo_active_scan_request(channel_mask, scan_duration, scanCompleteCallback);
|
||
_scan_status = ZB_SCAN_RUNNING;
|
||
}
|
||
|
||
int16_t ZigbeeCore::scanComplete() {
|
||
return _scan_status;
|
||
}
|
||
|
||
zigbee_scan_result_t *ZigbeeCore::getScanResult() {
|
||
return _scan_result;
|
||
}
|
||
|
||
void ZigbeeCore::scanDelete() {
|
||
if (_scan_result != nullptr) {
|
||
free(_scan_result);
|
||
_scan_result = nullptr;
|
||
}
|
||
_scan_status = ZB_SCAN_FAILED;
|
||
}
|
||
|
||
// Recall bounded devices from the binding table after reboot or when requested
|
||
void ZigbeeCore::bindingTableCb(const esp_zb_zdo_binding_table_info_t *table_info, void *user_ctx) {
|
||
esp_zb_zdo_mgmt_bind_param_t *req = (esp_zb_zdo_mgmt_bind_param_t *)user_ctx;
|
||
esp_zb_zdp_status_t zdo_status = (esp_zb_zdp_status_t)table_info->status;
|
||
log_d("Binding table callback for address 0x%04x with status %d", req->dst_addr, zdo_status);
|
||
|
||
if (zdo_status == ESP_ZB_ZDP_STATUS_SUCCESS) {
|
||
// Print binding table log simple
|
||
log_d("Binding table info: total %d, index %d, count %d", table_info->total, table_info->index, table_info->count);
|
||
|
||
if (table_info->total == 0) {
|
||
log_d("No binding table entries found");
|
||
// Clear all bound devices since there are no entries
|
||
for (std::list<ZigbeeEP *>::iterator it = Zigbee.ep_objects.begin(); it != Zigbee.ep_objects.end(); ++it) {
|
||
log_d("Clearing bound devices for EP %d", (*it)->getEndpoint());
|
||
(*it)->clearBoundDevices();
|
||
}
|
||
free(req);
|
||
return;
|
||
}
|
||
|
||
// Create a set to track found devices using both short and IEEE addresses
|
||
struct DeviceIdentifier {
|
||
uint8_t endpoint;
|
||
uint16_t short_addr;
|
||
esp_zb_ieee_addr_t ieee_addr;
|
||
bool is_ieee;
|
||
|
||
bool operator<(const DeviceIdentifier &other) const {
|
||
if (endpoint != other.endpoint) {
|
||
return endpoint < other.endpoint;
|
||
}
|
||
if (is_ieee != other.is_ieee) {
|
||
return is_ieee < other.is_ieee;
|
||
}
|
||
if (is_ieee) {
|
||
return memcmp(ieee_addr, other.ieee_addr, sizeof(esp_zb_ieee_addr_t)) < 0;
|
||
}
|
||
return short_addr < other.short_addr;
|
||
}
|
||
};
|
||
static std::set<DeviceIdentifier> found_devices;
|
||
static std::vector<esp_zb_zdo_binding_table_record_t> all_records;
|
||
|
||
// If this is the first chunk (index 0), clear the previous data
|
||
if (table_info->index == 0) {
|
||
found_devices.clear();
|
||
all_records.clear();
|
||
}
|
||
|
||
// Add current records to our collection
|
||
esp_zb_zdo_binding_table_record_t *record = table_info->record;
|
||
for (int i = 0; i < table_info->count; i++) {
|
||
log_d(
|
||
"Processing record %d: src_endp %d, dst_endp %d, cluster_id 0x%04x, dst_addr_mode %d", i, record->src_endp, record->dst_endp, record->cluster_id,
|
||
record->dst_addr_mode
|
||
);
|
||
all_records.push_back(*record);
|
||
record = record->next;
|
||
}
|
||
|
||
// If this is not the last chunk, request the next one
|
||
if (table_info->index + table_info->count < table_info->total) {
|
||
log_d("Requesting next chunk of binding table (current index: %d, count: %d, total: %d)", table_info->index, table_info->count, table_info->total);
|
||
req->start_index = table_info->index + table_info->count;
|
||
esp_zb_zdo_binding_table_req(req, bindingTableCb, req);
|
||
} else {
|
||
// This is the last chunk, process all records
|
||
log_d("Processing final chunk of binding table, total records: %d", all_records.size());
|
||
for (const auto &record : all_records) {
|
||
|
||
DeviceIdentifier dev_id;
|
||
dev_id.endpoint = record.src_endp;
|
||
dev_id.is_ieee = (record.dst_addr_mode == ESP_ZB_APS_ADDR_MODE_64_ENDP_PRESENT);
|
||
|
||
if (dev_id.is_ieee) {
|
||
memcpy(dev_id.ieee_addr, record.dst_address.addr_long, sizeof(esp_zb_ieee_addr_t));
|
||
dev_id.short_addr = 0xFFFF; // Invalid short address
|
||
} else {
|
||
dev_id.short_addr = record.dst_address.addr_short;
|
||
memset(dev_id.ieee_addr, 0, sizeof(esp_zb_ieee_addr_t));
|
||
}
|
||
|
||
// Track this device as found
|
||
found_devices.insert(dev_id);
|
||
}
|
||
|
||
// Now process each endpoint and update its bound devices
|
||
for (std::list<ZigbeeEP *>::iterator it = Zigbee.ep_objects.begin(); it != Zigbee.ep_objects.end(); ++it) {
|
||
log_d("Processing endpoint %d", (*it)->getEndpoint());
|
||
std::list<zb_device_params_t *> bound_devices = (*it)->getBoundDevices();
|
||
std::list<zb_device_params_t *> devices_to_remove;
|
||
|
||
// First, identify devices that need to be removed
|
||
for (std::list<zb_device_params_t *>::iterator dev_it = bound_devices.begin(); dev_it != bound_devices.end(); ++dev_it) {
|
||
DeviceIdentifier dev_id;
|
||
dev_id.endpoint = (*it)->getEndpoint();
|
||
|
||
// Create both short and IEEE address identifiers for the device
|
||
bool found = false;
|
||
|
||
// Check if device exists with short address
|
||
if ((*dev_it)->short_addr != 0xFFFF) {
|
||
dev_id.is_ieee = false;
|
||
dev_id.short_addr = (*dev_it)->short_addr;
|
||
memset(dev_id.ieee_addr, 0, sizeof(esp_zb_ieee_addr_t));
|
||
if (found_devices.find(dev_id) != found_devices.end()) {
|
||
found = true;
|
||
}
|
||
}
|
||
|
||
// Check if device exists with IEEE address
|
||
if (!found) {
|
||
dev_id.is_ieee = true;
|
||
memcpy(dev_id.ieee_addr, (*dev_it)->ieee_addr, sizeof(esp_zb_ieee_addr_t));
|
||
dev_id.short_addr = 0xFFFF;
|
||
if (found_devices.find(dev_id) != found_devices.end()) {
|
||
found = true;
|
||
}
|
||
}
|
||
|
||
if (!found) {
|
||
devices_to_remove.push_back(*dev_it);
|
||
}
|
||
}
|
||
|
||
// Remove devices that are no longer in the binding table
|
||
for (std::list<zb_device_params_t *>::iterator dev_it = devices_to_remove.begin(); dev_it != devices_to_remove.end(); ++dev_it) {
|
||
(*it)->removeBoundDevice(*dev_it);
|
||
free(*dev_it);
|
||
}
|
||
|
||
// Now add new devices from the binding table
|
||
for (const auto &record : all_records) {
|
||
if (record.src_endp == (*it)->getEndpoint()) {
|
||
log_d("Processing binding record for EP %d", record.src_endp);
|
||
zb_device_params_t *device = (zb_device_params_t *)calloc(1, sizeof(zb_device_params_t));
|
||
if (!device) {
|
||
log_e("Failed to allocate memory for device params");
|
||
continue;
|
||
}
|
||
device->endpoint = record.dst_endp;
|
||
|
||
bool is_ieee = (record.dst_addr_mode == ESP_ZB_APS_ADDR_MODE_64_ENDP_PRESENT);
|
||
if (is_ieee) {
|
||
memcpy(device->ieee_addr, record.dst_address.addr_long, sizeof(esp_zb_ieee_addr_t));
|
||
device->short_addr = 0xFFFF;
|
||
} else {
|
||
device->short_addr = record.dst_address.addr_short;
|
||
memset(device->ieee_addr, 0, sizeof(esp_zb_ieee_addr_t));
|
||
}
|
||
|
||
// Check if device already exists
|
||
bool device_exists = false;
|
||
for (std::list<zb_device_params_t *>::iterator dev_it = bound_devices.begin(); dev_it != bound_devices.end(); ++dev_it) {
|
||
if (is_ieee) {
|
||
if (memcmp((*dev_it)->ieee_addr, device->ieee_addr, sizeof(esp_zb_ieee_addr_t)) == 0) {
|
||
device_exists = true;
|
||
break;
|
||
}
|
||
} else {
|
||
if ((*dev_it)->short_addr == device->short_addr) {
|
||
device_exists = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!device_exists) {
|
||
(*it)->addBoundDevice(device);
|
||
log_d(
|
||
"Device bound to EP %d -> device endpoint: %d, %s: %s", record.src_endp, device->endpoint, is_ieee ? "ieee addr" : "short addr",
|
||
is_ieee ? formatIEEEAddress(device->ieee_addr) : formatShortAddress(device->short_addr)
|
||
);
|
||
} else {
|
||
log_d("Device already exists, freeing allocated memory");
|
||
free(device); // Free the device if it already exists
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Print bound devices
|
||
log_d("Filling bounded devices finished");
|
||
free(req);
|
||
}
|
||
} else {
|
||
log_e("Binding table request failed with status: %d", zdo_status);
|
||
free(req);
|
||
}
|
||
}
|
||
|
||
void ZigbeeCore::searchBindings() {
|
||
esp_zb_zdo_mgmt_bind_param_t *mb_req = (esp_zb_zdo_mgmt_bind_param_t *)malloc(sizeof(esp_zb_zdo_mgmt_bind_param_t));
|
||
mb_req->dst_addr = esp_zb_get_short_address();
|
||
mb_req->start_index = 0;
|
||
log_d("Requesting binding table for address 0x%04x", mb_req->dst_addr);
|
||
esp_zb_zdo_binding_table_req(mb_req, bindingTableCb, (void *)mb_req);
|
||
}
|
||
|
||
void ZigbeeCore::resetNVRAMChannelMask() {
|
||
_primary_channel_mask = ESP_ZB_TRANSCEIVER_ALL_CHANNELS_MASK;
|
||
esp_zb_set_channel_mask(_primary_channel_mask);
|
||
zb_nvram_write_dataset(ZB_NVRAM_COMMON_DATA);
|
||
log_v("Channel mask reset to all channels");
|
||
}
|
||
|
||
void ZigbeeCore::setNVRAMChannelMask(uint32_t mask) {
|
||
_primary_channel_mask = mask;
|
||
esp_zb_set_channel_mask(_primary_channel_mask);
|
||
zb_nvram_write_dataset(ZB_NVRAM_COMMON_DATA);
|
||
log_v("Channel mask set to 0x%08x", mask);
|
||
}
|
||
|
||
// Function to convert enum value to string
|
||
const char *ZigbeeCore::getDeviceTypeString(esp_zb_ha_standard_devices_t deviceId) {
|
||
switch (deviceId) {
|
||
case ESP_ZB_HA_ON_OFF_SWITCH_DEVICE_ID: return "General On/Off switch";
|
||
case ESP_ZB_HA_LEVEL_CONTROL_SWITCH_DEVICE_ID: return "Level Control Switch";
|
||
case ESP_ZB_HA_ON_OFF_OUTPUT_DEVICE_ID: return "General On/Off output";
|
||
case ESP_ZB_HA_LEVEL_CONTROLLABLE_OUTPUT_DEVICE_ID: return "Level Controllable Output";
|
||
case ESP_ZB_HA_SCENE_SELECTOR_DEVICE_ID: return "Scene Selector";
|
||
case ESP_ZB_HA_CONFIGURATION_TOOL_DEVICE_ID: return "Configuration Tool";
|
||
case ESP_ZB_HA_REMOTE_CONTROL_DEVICE_ID: return "Remote Control";
|
||
case ESP_ZB_HA_COMBINED_INTERFACE_DEVICE_ID: return "Combined Interface";
|
||
case ESP_ZB_HA_RANGE_EXTENDER_DEVICE_ID: return "Range Extender";
|
||
case ESP_ZB_HA_MAINS_POWER_OUTLET_DEVICE_ID: return "Mains Power Outlet";
|
||
case ESP_ZB_HA_DOOR_LOCK_DEVICE_ID: return "Door lock client";
|
||
case ESP_ZB_HA_DOOR_LOCK_CONTROLLER_DEVICE_ID: return "Door lock controller";
|
||
case ESP_ZB_HA_SIMPLE_SENSOR_DEVICE_ID: return "Simple Sensor device";
|
||
case ESP_ZB_HA_CONSUMPTION_AWARENESS_DEVICE_ID: return "Consumption Awareness Device";
|
||
case ESP_ZB_HA_HOME_GATEWAY_DEVICE_ID: return "Home Gateway";
|
||
case ESP_ZB_HA_SMART_PLUG_DEVICE_ID: return "Smart plug";
|
||
case ESP_ZB_HA_WHITE_GOODS_DEVICE_ID: return "White Goods";
|
||
case ESP_ZB_HA_METER_INTERFACE_DEVICE_ID: return "Meter Interface";
|
||
case ESP_ZB_HA_ON_OFF_LIGHT_DEVICE_ID: return "On/Off Light Device";
|
||
case ESP_ZB_HA_DIMMABLE_LIGHT_DEVICE_ID: return "Dimmable Light Device";
|
||
case ESP_ZB_HA_COLOR_DIMMABLE_LIGHT_DEVICE_ID: return "Color Dimmable Light Device";
|
||
case ESP_ZB_HA_DIMMER_SWITCH_DEVICE_ID: return "Dimmer Switch Device";
|
||
case ESP_ZB_HA_COLOR_DIMMER_SWITCH_DEVICE_ID: return "Color Dimmer Switch Device";
|
||
case ESP_ZB_HA_LIGHT_SENSOR_DEVICE_ID: return "Light Sensor";
|
||
case ESP_ZB_HA_SHADE_DEVICE_ID: return "Shade";
|
||
case ESP_ZB_HA_SHADE_CONTROLLER_DEVICE_ID: return "Shade controller";
|
||
case ESP_ZB_HA_WINDOW_COVERING_DEVICE_ID: return "Window Covering client";
|
||
case ESP_ZB_HA_WINDOW_COVERING_CONTROLLER_DEVICE_ID: return "Window Covering controller";
|
||
case ESP_ZB_HA_HEATING_COOLING_UNIT_DEVICE_ID: return "Heating/Cooling Unit device";
|
||
case ESP_ZB_HA_THERMOSTAT_DEVICE_ID: return "Thermostat Device";
|
||
case ESP_ZB_HA_TEMPERATURE_SENSOR_DEVICE_ID: return "Temperature Sensor";
|
||
case ESP_ZB_HA_IAS_CONTROL_INDICATING_EQUIPMENT_ID: return "IAS Control and Indicating Equipment";
|
||
case ESP_ZB_HA_IAS_ANCILLARY_CONTROL_EQUIPMENT_ID: return "IAS Ancillary Control Equipment";
|
||
case ESP_ZB_HA_IAS_ZONE_ID: return "IAS Zone";
|
||
case ESP_ZB_HA_IAS_WARNING_DEVICE_ID: return "IAS Warning Device";
|
||
case ESP_ZB_HA_TEST_DEVICE_ID: return "Custom HA device for test";
|
||
case ESP_ZB_HA_CUSTOM_TUNNEL_DEVICE_ID: return "Custom Tunnel device";
|
||
case ESP_ZB_HA_CUSTOM_ATTR_DEVICE_ID: return "Custom Attributes Device";
|
||
default: return "Unknown device type";
|
||
}
|
||
}
|
||
|
||
void ZigbeeCore::callDefaultResponseCallback(zb_cmd_type_t resp_to_cmd, esp_zb_zcl_status_t status, uint8_t endpoint, uint16_t cluster) {
|
||
if (_global_default_response_cb) {
|
||
_global_default_response_cb(resp_to_cmd, status, endpoint, cluster);
|
||
}
|
||
}
|
||
|
||
ZigbeeCore Zigbee = ZigbeeCore();
|
||
|
||
#endif // CONFIG_ZB_ENABLED
|