Files
esp32-core/libraries/Zigbee/src/ZigbeeCore.cpp
T

850 lines
36 KiB
C++
Raw Normal View History

2026-05-22 21:52:50 +03:00
// Copyright 2025 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/* 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;
_allow_multi_endpoint_binding = 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());
if (!Zigbee.allowMultiEndpointBinding()) { // If multi endpoint binding is not allowed, break the loop to keep backwards compatibility
break;
}
}
}
}
}
}
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;
}
}
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());
if (!Zigbee.allowMultiEndpointBinding()) { // If multi endpoint binding is not allowed, break the loop to keep backwards compatibility
break;
}
}
}
}
}
}
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;
}
if (_scan_status == ZB_SCAN_RUNNING) {
log_w("Scan already in progress, ignoring new scan request");
return;
}
log_v("Scanning Zigbee networks");
esp_zb_lock_acquire(portMAX_DELAY);
esp_zb_zdo_active_scan_request(channel_mask, scan_duration, scanCompleteCallback);
esp_zb_lock_release();
_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);
}
void ZigbeeCore::stop() {
if (started()) {
vTaskSuspend(xTaskGetHandle("Zigbee_main"));
log_v("Zigbee stack stopped");
_started = false;
}
return;
}
void ZigbeeCore::start() {
if (!started()) {
vTaskResume(xTaskGetHandle("Zigbee_main"));
log_v("Zigbee stack started");
_started = true;
}
return;
}
// 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