From 256403fe73b6a0332a8b2ec0a3812811d8d77331 Mon Sep 17 00:00:00 2001 From: "xieqinan@espressif.com" Date: Wed, 9 Sep 2026 11:34:28 +0800 Subject: [PATCH] feat(zigbee): upgrade zigbee example to v2.0 --- examples/zigbee/.build-test-rules.yml | 15 +- examples/zigbee/README.md | 11 + examples/zigbee/esp_zigbee_gateway/README.md | 222 +++++----- .../images/esp-zigbee-gateway-board.webp | Bin 0 -> 67684 bytes .../images/esp-zigbee-standalone-modules.webp | Bin 0 -> 26888 bytes .../esp_zigbee_gateway/main/CMakeLists.txt | 8 +- .../esp_zigbee_gateway/main/Kconfig.projbuild | 17 + .../main/esp_zigbee_gateway.c | 353 +++++++++------ .../main/esp_zigbee_gateway.h | 123 +++--- .../esp_zigbee_gateway/main/idf_component.yml | 12 +- .../esp_zigbee_gateway/main/zigbee_rcp.c | 124 ++++++ .../esp_zigbee_gateway/main/zigbee_rcp.h | 22 + .../zigbee/esp_zigbee_gateway/partitions.csv | 12 +- .../esp_zigbee_gateway/sdkconfig.defaults | 24 +- .../HA_on_off_light/CMakeLists.txt | 3 + .../light_sample/HA_on_off_light/README.md | 52 +-- .../HA_on_off_light/main/CMakeLists.txt | 8 +- .../HA_on_off_light/main/esp_zb_light.c | 231 ++++++---- .../HA_on_off_light/main/esp_zb_light.h | 61 +-- .../HA_on_off_light/main/idf_component.yml | 13 +- .../HA_on_off_light/main/light_driver.c | 62 --- .../HA_on_off_light/main/light_driver.h | 70 --- .../HA_on_off_light/partitions.csv | 10 +- .../HA_on_off_light/sdkconfig.defaults | 9 +- .../HA_on_off_switch/CMakeLists.txt | 3 + .../light_sample/HA_on_off_switch/README.md | 73 ++-- .../HA_on_off_switch/main/CMakeLists.txt | 8 +- .../HA_on_off_switch/main/esp_zb_switch.c | 403 +++++++++++------- .../HA_on_off_switch/main/esp_zb_switch.h | 61 +-- .../HA_on_off_switch/main/idf_component.yml | 12 +- .../HA_on_off_switch/main/switch_driver.c | 178 -------- .../HA_on_off_switch/main/switch_driver.h | 94 ---- .../HA_on_off_switch/partitions.csv | 10 +- .../HA_on_off_switch/sdkconfig.defaults | 10 +- examples/zigbee/light_sample/README.md | 9 +- .../zigbee/light_sample/pytest_esp_zigbee.py | 40 +- .../example_common/CMakeLists.txt | 4 + .../example_common/Kconfig | 12 + .../example_common/include/alarm_timer.h | 47 ++ .../example_common/sdkconfig.mbedtls_minimal | 51 +++ .../example_common/src/alarm_timer.c | 69 +++ .../examples_utils/CMakeLists.txt | 3 - .../examples_utils/include/zcl_utility.h | 50 --- .../examples_utils/zcl_utility.c | 40 -- .../light_driver/CMakeLists.txt | 3 + .../zb_common_components/light_driver/Kconfig | 13 + .../light_driver/idf_component.yml | 3 + .../light_driver/include/light_driver.h | 151 +++++++ .../light_driver/src/light_driver.c | 87 ++++ .../switch_driver/CMakeLists.txt | 13 + .../switch_driver/Kconfig | 27 ++ .../switch_driver/include/switch_driver.h | 79 ++++ .../switch_driver/src/switch_driver.c | 116 +++++ 53 files changed, 1880 insertions(+), 1251 deletions(-) create mode 100644 examples/zigbee/README.md create mode 100644 examples/zigbee/esp_zigbee_gateway/images/esp-zigbee-gateway-board.webp create mode 100644 examples/zigbee/esp_zigbee_gateway/images/esp-zigbee-standalone-modules.webp create mode 100644 examples/zigbee/esp_zigbee_gateway/main/Kconfig.projbuild create mode 100644 examples/zigbee/esp_zigbee_gateway/main/zigbee_rcp.c create mode 100644 examples/zigbee/esp_zigbee_gateway/main/zigbee_rcp.h delete mode 100644 examples/zigbee/light_sample/HA_on_off_light/main/light_driver.c delete mode 100644 examples/zigbee/light_sample/HA_on_off_light/main/light_driver.h delete mode 100644 examples/zigbee/light_sample/HA_on_off_switch/main/switch_driver.c delete mode 100644 examples/zigbee/light_sample/HA_on_off_switch/main/switch_driver.h create mode 100644 examples/zigbee/zb_common_components/example_common/CMakeLists.txt create mode 100644 examples/zigbee/zb_common_components/example_common/Kconfig create mode 100644 examples/zigbee/zb_common_components/example_common/include/alarm_timer.h create mode 100644 examples/zigbee/zb_common_components/example_common/sdkconfig.mbedtls_minimal create mode 100644 examples/zigbee/zb_common_components/example_common/src/alarm_timer.c delete mode 100644 examples/zigbee/zb_common_components/examples_utils/CMakeLists.txt delete mode 100644 examples/zigbee/zb_common_components/examples_utils/include/zcl_utility.h delete mode 100644 examples/zigbee/zb_common_components/examples_utils/zcl_utility.c create mode 100644 examples/zigbee/zb_common_components/light_driver/CMakeLists.txt create mode 100644 examples/zigbee/zb_common_components/light_driver/Kconfig create mode 100644 examples/zigbee/zb_common_components/light_driver/idf_component.yml create mode 100644 examples/zigbee/zb_common_components/light_driver/include/light_driver.h create mode 100644 examples/zigbee/zb_common_components/light_driver/src/light_driver.c create mode 100644 examples/zigbee/zb_common_components/switch_driver/CMakeLists.txt create mode 100644 examples/zigbee/zb_common_components/switch_driver/Kconfig create mode 100644 examples/zigbee/zb_common_components/switch_driver/include/switch_driver.h create mode 100644 examples/zigbee/zb_common_components/switch_driver/src/switch_driver.c diff --git a/examples/zigbee/.build-test-rules.yml b/examples/zigbee/.build-test-rules.yml index 838a83e2e49..d708848456e 100644 --- a/examples/zigbee/.build-test-rules.yml +++ b/examples/zigbee/.build-test-rules.yml @@ -6,23 +6,22 @@ - openthread - protocol_examples_common depends_filepatterns: + - examples/zigbee/esp_zigbee_gateway/* - examples/zigbee/light_sample/**/* examples/zigbee/esp_zigbee_gateway: enable: - - if: SOC_WIFI_SUPPORTED == 1 and IDF_TARGET not in ["esp32c2", "esp32c61"] - reason: not supported esp32c2 and esp32c61 - disable: - - if: IDF_TARGET == "esp32s31" - temporary: true - reason: Zigbee gateway example does not support esp32s31 yet + - if: (SOC_WIFI_SUPPORTED == 1 and IDF_TARGET not in ["esp32c61", "esp32c2"]) or IDF_TARGET in ["esp32p4"] + disable_test: + - if: IDF_TARGET not in ["esp32s3"] + reason: only test on esp32s3 <<: *zigbee_dependencies examples/zigbee/light_sample: enable: - - if: SOC_IEEE802154_SUPPORTED == 1 and IDF_TARGET not in ["esp32h4", "esp32s31", "esp32h21"] + - if: SOC_IEEE802154_SUPPORTED == 1 disable_test: - - if: IDF_TARGET != "esp32h2" + - if: IDF_TARGET not in ["esp32h2"] temporary: true reason: only test on esp32h2 <<: *zigbee_dependencies diff --git a/examples/zigbee/README.md b/examples/zigbee/README.md new file mode 100644 index 00000000000..449716cbfc6 --- /dev/null +++ b/examples/zigbee/README.md @@ -0,0 +1,11 @@ +# Zigbee Examples + +See the [README.md](../README.md) file in the upper level [examples](../) directory for more information about examples. + +## Overview + +In this folder, it contains following Zigbee examples: + +* [esp_zigbee_gateway](esp_zigbee_gateway) is a Zigbee gateway example. It runs on a Wi-Fi SoC such as ESP32, ESP32-C3 and ESP32-S3. It needs an 802.15.4 SoC like ESP32-H2 running [ot_rcp](../openthread/ot_rcp) example to provide 802.15.4 radio. For more details see [README](esp_zigbee_gateway/README.md). + +* [light_sample](light_sample) It is a pair of typical Zigbee Light On/Off switch and bulb examples. They run on an 802.15.4 SoC like ESP32-H2. For more details see [README](light_sample/README.md). diff --git a/examples/zigbee/esp_zigbee_gateway/README.md b/examples/zigbee/esp_zigbee_gateway/README.md index e313e611e9c..e6cb29eee3d 100644 --- a/examples/zigbee/esp_zigbee_gateway/README.md +++ b/examples/zigbee/esp_zigbee_gateway/README.md @@ -1,46 +1,68 @@ -| Supported Targets | ESP32 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-S2 | ESP32-S3 | -| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- | +| Supported Targets | ESP32 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-P4 | ESP32-S2 | ESP32-S3 | ESP32-S31 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- | -------- | --------- | # Gateway Example -This example demonstrates how to build a Zigbee Gateway device. +This example demonstrates how to build a Zigbee Gateway device. It runs on a Wi-Fi SoC such as ESP32, ESP32-C3 and ESP32-S3, with an 802.15.4 SoC like ESP32-H2 +running [ot_rcp](https://github.com/espressif/esp-idf/tree/master/examples/openthread/ot_rcp) to provide 802.15.4 radio. -The ESP Zigbee SDK provides more examples and tools for productization: -* [ESP Zigbee SDK Docs](https://docs.espressif.com/projects/esp-zigbee-sdk) -* [ESP Zigbee SDK Repo](https://github.com/espressif/esp-zigbee-sdk) +## Hardware Platforms -## Hardware Required +### Wi-Fi based ESP Zigbee Gateway -By default, two SoCs are required to run this example: -* An ESP32 series Wi-Fi SoC (ESP32, ESP32-C, ESP32-S, etc) loaded with this esp_zigbee_gateway example. -* An ESP32-H2 802.15.4 SoC loaded with [ot_rcp](../../openthread/ot_rcp/) example +The Wi-Fi based ESP Zigbee Gateway consists of two SoCs: -Connect the two SoCs via UART, below is an example setup with ESP32-DevKitC and ESP32-H2-DevKitC: -![Zigbee_gateway](../../openthread/ot_br/image/thread-border-router-esp32-esp32h2.jpg) +* An ESP32 series Wi-Fi SoC (ESP32, ESP32-C, ESP32-S, etc) loaded with this example. +* An 802.15.4 enabled SoC (e.g., ESP32-H2 or ESP32-C6) loaded with [OpenThread RCP](https://github.com/espressif/esp-idf/tree/master/examples/openthread/ot_rcp) example. + +#### ESP Zigbee Gateway Board + +The ESP Zigbee gateway board provides an integrated module of an ESP32-S3 SoC and an ESP32-H2 RCP. + +![br_dev_kit](images/esp-zigbee-gateway-board.webp) + +The two SoCs are connected with following interfaces: +* UART for serial communication +* RESET and BOOT pins for RCP Update +* 3-Wires PTA for RF coexistence + +No jumper wires needed. No `idf.py menuconfig` under menu "OpenThread RCP Update" configure to change. + +#### Standalone Modules + +The SDK also supports manually connecting an ESP32-H2 RCP to an ESP32 series Wi-Fi SoC. As one way of connection is following: ESP32 pin | ESP32-H2 pin ------------- |------------- GND | G GPIO4 (RX) | TX GPIO5 (TX) | RX + GPI017 | RST + GPIO18 | GPIO9(BOOT) -* TX, RX pin can be also configured by user in esp_zigbee_gateway.h +TX, RX, RST and BOOT pin from ESP32 side can be configured by user in `idf.py menuconfig` under menu "ESP Zigbee gateway rcp update". -The example could also run on a single SoC which supports both Wi-Fi and Zigbee (e.g., ESP32-C6), but since there is only one RF path in ESP32-C6, which means Wi-Fi and Zigbee can't receive simultaneously, it has a significant impact on performance. Hence the two SoCs solution is recommended. +TX, RX pins from ESP32-H2 should be configured in [ot_rcp example](https://github.com/espressif/esp-idf/tree/master/examples/openthread/ot_rcp) via "OpenThread RCP Example > Configure RCP UART pin manually" options. -## Configure the RCP +Other pin number is also available for user to configure if needed. -The `OPENTHREAD_NCP_VENDOR_HOOK` of `ot_rcp` should be selected via menuconfig when the [ot_rcp](../../openthread/ot_rcp/) example is built. Then use `idf.py -p PORT erase-flash` to flash the RCP firmware to ESP32-H2-DevKitC. +The following image shows an example connection between ESP32 DevKitC and ESP32-H2 DevKitC: -## Configure the project +![Zigbee_gateway](images/esp-zigbee-standalone-modules.webp) -Before project configuration and build, make sure to set the correct chip target using `idf.py set-target `. +## Erase the NVRAM -In order to run the example on single SoC which supports both Wi-Fi and Thread, the option `CONFIG_ESP_COEX_SW_COEXIST_ENABLE` and option `CONFIG_ZB_RADIO_NATIVE` should be enabled. The two options are enabled by default for ESP32-C6 target. +Before flash it to the board, it is recommended to erase NVRAM if user doesn't want to keep the previous examples or other projects stored info +using `idf.py -p PORT erase-flash` -## Erase the NVRAM +## Create the RCP firmware image -Before flash it to the board, it is recommended to erase NVRAM if user doesn't want to keep the previous examples or other projects stored info using `idf.py -p PORT erase-flash` +The esp_zigbee_gateway supports updating the RCP image. Generate rcp image before building the esp_zigbee_gateway example. + +Build the [ot_rcp](https://github.com/espressif/esp-idf/tree/master/examples/openthread/ot_rcp) example. Later in the esp_zigbee_gateway building process, the built RCP image will be automatically packed into the esp_zigbee_gateway firmware. See detail in [CMakeLists.txt](main/CMakeLists.txt). + +Notes: +- `OPENTHREAD_NCP_VENDOR_HOOK` of `ot_rcp` should be selected via menuconfig. ## Build and Flash @@ -48,86 +70,94 @@ Build the project, flash it to the board, and start the monitor tool to view the (To exit the serial monitor, type ``Ctrl-]``.) -## Example Output +## Application Functions -As you run the example, you will see the following log: - -esp_zigbee_gateway: +- When the program starts, the board will attempt to read the RCP version from the remote radio via UART. ``` -I (499) main_task: Calling app_main() -I (519) ESP_RADIO_SPINEL: spinel UART interface initialization completed -I (519) ESP_RADIO_SPINEL: Spinel UART interface has been successfully enabled -I (519) ZB_ESP_SPINEL: Spinel UART interface enable successfully -I (529) main_task: Returned from app_main() -I(529) OPENTHREAD:[I] P-RadioSpinel-: RCP reset: RESET_POWER_ON -I(539) OPENTHREAD:[I] P-RadioSpinel-: Software reset RCP successfully -I (569) ZB_ESP_SPINEL: Radio spinel workflow register successfully -I (769) ESP_ZB_GATEWAY: Production configuration is ready -W (769) ESP_ZB_GATEWAY: Production configuration is not present -I (769) example_connect: Start example_connect. -I (779) pp: pp rom version: e7ae62f -I (779) net80211: net80211 rom version: e7ae62f -I (799) wifi:wifi driver task: 3fca80d8, prio:23, stack:6656, core=0 -I (799) wifi:wifi firmware version: 3ce09e5 -I (799) wifi:wifi certification version: v7.0 -I (799) wifi:config NVS flash: enabled -I (799) wifi:config nano formatting: disabled -I (809) wifi:Init data frame dynamic rx buffer num: 32 -I (809) wifi:Init static rx mgmt buffer num: 5 -I (819) wifi:Init management short buffer num: 32 -I (819) wifi:Init dynamic tx buffer num: 32 -I (819) wifi:Init static tx FG buffer num: 2 -I (829) wifi:Init static rx buffer size: 1600 -I (829) wifi:Init static rx buffer num: 10 -I (839) wifi:Init dynamic rx buffer num: 32 -I (839) wifi_init: rx ba win: 6 -I (839) wifi_init: tcpip mbox: 32 -I (849) wifi_init: udp mbox: 6 -I (849) wifi_init: tcp mbox: 6 -I (849) wifi_init: tcp tx win: 5760 -I (859) wifi_init: tcp rx win: 5760 -I (859) wifi_init: tcp mss: 1440 -I (869) wifi_init: WiFi IRAM OP enabled -I (869) wifi_init: WiFi RX IRAM OP enabled -I (879) phy_init: phy_version 670,b7bc9b9,Apr 30 2024,10:54:13 -W (879) phy_init: failed to load RF calibration data (0x1102), falling back to full calibration -I (989) wifi:mode : sta (f4:12:fa:41:a7:f4) -I (989) wifi:enable tsf -I (999) example_connect: Connecting to esp-office-2.4G... -I (999) example_connect: Waiting for IP(s) -I (3409) wifi:new:<13,0>, old:<1,0>, ap:<255,255>, sta:<13,0>, prof:1 -I (3649) wifi:state: init -> auth (b0) -I (3719) wifi:state: auth -> assoc (0) -I (3759) wifi:state: assoc -> run (10) -I (3769) wifi:connected with esp-office-2.4G, aid = 1, channel 13, BW20, bssid = 9c:3a:9a:04:18:92 -I (3769) wifi:security: WPA2-PSK, phy: bgn, rssi: -42 -I (3769) wifi:pm start, type: 1 - -I (3779) wifi:dp: 1, bi: 102400, li: 3, scale listen interval from 307200 us to 307200 us -I (3789) wifi:set rx beacon pti, rx_bcn_pti: 0, bcn_timeout: 25000, mt_pti: 0, mt_time: 10000 -I (3819) wifi:AP's beacon interval = 102400 us, DTIM period = 1 -I (3849) wifi:idx:0 (ifx:0, 9c:3a:9a:04:18:92), tid:0, ssn:0, winSize:64 -I (4799) esp_netif_handlers: example_netif_sta ip: 192.168.200.133, mask: 255.255.252.0, gw: 192.168.200.1 -I (4799) example_connect: Got IPv4 event: Interface "example_netif_sta" address: 192.168.200.133 -I (5509) example_connect: Got IPv6 event: Interface "example_netif_sta" address: fe80:0000:0000:0000:f612:faff:fe41:a7f4, type: ESP_IP6_ADDR_IS_LINK_LOCAL -I (5509) example_common: Connected to example_netif_sta -I (5519) example_common: - IPv4 address: 192.168.200.133, -I (5519) example_common: - IPv6 address: fe80:0000:0000:0000:f612:faff:fe41:a7f4, type: ESP_IP6_ADDR_IS_LINK_LOCAL -I (5529) wifi:Set ps type: 0, coexist: 0 - -I (5539) ESP_ZB_GATEWAY: Initialize Zigbee stack -I (5549) ESP_ZB_GATEWAY: Device started up in factory-reset mode -I (5549) ESP_ZB_GATEWAY: Start network formation -W (5729) ESP_ZB_GATEWAY: Network(0xb8e9) closed, devices joining not allowed. -I (5729) ESP_ZB_GATEWAY: Formed network successfully (Extended PAN ID: 60:55:f9:ff:fe:f7:73:e8, PAN ID: 0xb8e9, Channel:13, Short Address: 0x0000) -I (6339) ESP_ZB_GATEWAY: Network(0xb8e9) is open for 180 seconds -I (6339) ESP_ZB_GATEWAY: Network steering started +I (433) main_task: Started on CPU0 +I (443) main_task: Calling app_main() +I (453) ZIGBEE_GATEWAY: Start ESP Zigbee Stack +I (493) main_task: Returned from app_main() +I (503) RCP_UPDATE: RCP: using update sequence 0 +I (503) ESP_RADIO_SPINEL: spinel UART interface initialization completed +I (503) ESP_RADIO_SPINEL: Spinel UART interface has been successfully enabled +I (503) ESP_ZIGBEE_RADIO_SPINEL_UART: Spinel UART interface enable successfully +I(513) OPENTHREAD:[I] P-SpinelDrive-: co-processor reset: RESET_POWER_ON +E(523) OPENTHREAD:[C] P-SpinelDrive-: Software reset co-processor successfully +I (573) ESP_ZIGBEE_RCP: Running RCP version: openthread-esp32/fcae32885b029-b945928d7; esp32h2; 2026-03-11 02:30:22 UTC ``` -## Gateway Functions +- If `CONFIG_AUTO_UPDATE_RCP` is enabled and the version does not match the stored one, the board will automatically update the RCP using the stored version. - * When the device starts up, it will attempt to connect to a Wi-Fi network and then interface with the OT-RCP via UART to form a Zigbee network. - * For more Gateway functionalities, please refer to [matter zigbee bridge](https://github.com/espressif/esp-matter/tree/main/examples/bridge_apps/zigbee_bridge/) and [Rainmaker Zigbee Gateway](https://github.com/espressif/esp-rainmaker/tree/master/examples/zigbee_gateway) examples. +- After confirming that the remote radio is functioning properly, the board will connect to Wi-Fi as configured in menuconfig. +``` + +I (573) example_connect: Start example_connect. +I (583) pp: pp rom version: e7ae62f +I (583) net80211: net80211 rom version: e7ae62f +I (593) wifi:wifi driver task: 3fcb86d0, prio:23, stack:6656, core=0 +I (593) wifi:wifi firmware version: 14da9b7 +I (593) wifi:wifi certification version: v7.0 +I (603) wifi:config NVS flash: enabled +I (603) wifi:config nano formatting: disabled +I (603) wifi:Init data frame dynamic rx buffer num: 32 +I (613) wifi:Init static rx mgmt buffer num: 5 +I (613) wifi:Init management short buffer num: 32 +I (623) wifi:Init dynamic tx buffer num: 32 +I (623) wifi:Init static tx FG buffer num: 2 +I (633) wifi:Init static rx buffer size: 1600 +I (633) wifi:Init static rx buffer num: 10 +I (633) wifi:Init dynamic rx buffer num: 32 +I (643) wifi_init: rx ba win: 6 +I (643) wifi_init: accept mbox: 6 +I (643) wifi_init: tcpip mbox: 32 +I (653) wifi_init: udp mbox: 6 +I (653) wifi_init: tcp mbox: 6 +I (653) wifi_init: tcp tx win: 5760 +I (663) wifi_init: tcp rx win: 5760 +I (663) wifi_init: tcp mss: 1440 +I (663) wifi_init: WiFi IRAM OP enabled +I (673) wifi_init: WiFi RX IRAM OP enabled +I (673) phy_init: phy_version 701,f4f1da3a,Mar 3 2025,15:50:10 +W (673) phy_init: failed to load RF calibration data (0x1102), falling back to full calibration +I (783) phy_init: Saving new calibration data due to checksum failure or outdated calibration data, mode(2) +I (793) wifi:mode : sta (48:27:e2:14:43:38) +I (793) wifi:enable tsf +I (793) example_connect: Connecting to esp-guest... +W (793) wifi:Password length matches WPA2 standards, authmode threshold changes from OPEN to WPA2 +I (803) example_connect: Waiting for IP(s) +I (3293) wifi:new:<6,0>, old:<1,0>, ap:<255,255>, sta:<6,0>, prof:1, snd_ch_cfg:0x0 +I (3303) wifi:state: init -> auth (0xb0) +I (3303) wifi:state: auth -> assoc (0x0) +I (3323) wifi:state: assoc -> run (0x10) +I (3413) wifi:connected with esp-guest, aid = 1, channel 6, BW20, bssid = 84:78:48:86:87:31 +I (3423) wifi:security: WPA2-PSK, phy: bgn, rssi: -30 +I (3423) wifi:pm start, type: 1 + +I (3423) wifi:dp: 1, bi: 102400, li: 3, scale listen interval from 307200 us to 307200 us +I (3433) wifi:set rx beacon pti, rx_bcn_pti: 0, bcn_timeout: 25000, mt_pti: 0, mt_time: 10000 +I (3453) wifi:AP's beacon interval = 102400 us, DTIM period = 1 +I (3503) wifi:idx:0 (ifx:0, 84:78:48:86:87:31), tid:0, ssn:0, winSize:64 +I (4573) example_connect: Got IPv6 event: Interface "example_netif_sta" address: fe80:0000:0000:0000:4a27:e2ff:fe14:4338, type: ESP_IP6_ADDR_IS_LINK_LOCAL +I (4843) esp_netif_handlers: example_netif_sta ip: 10.18.40.174, mask: 255.255.255.0, gw: 10.18.40.1 +I (4843) example_connect: Got IPv4 event: Interface "example_netif_sta" address: 10.18.40.174 +I (4843) example_common: Connected to example_netif_sta +I (4853) example_common: - IPv4 address: 10.18.40.174, +I (4853) example_common: - IPv6 address: fe80:0000:0000:0000:4a27:e2ff:fe14:4338, type: ESP_IP6_ADDR_IS_LINK_LOCAL +I (4863) wifi:Set ps type: 0, coexist: 0 +``` + +- Finally, the board will act as the `Zigbee Gateway`, forming a network and handling commands from the network. +``` +I (4883) ZIGBEE_GATEWAY: Initialize Zigbee stack +I (4883) ZIGBEE_GATEWAY: Deferred driver initialization successful +I (4883) ZIGBEE_GATEWAY: Device started up in factory-reset mode +I (5303) ZIGBEE_GATEWAY: Formed network successfully: PAN ID(0x4494, EXT: 0x6055f9fffef7a2ad), Channel(13), Short Address(0x0000) +I (5573) ZIGBEE_GATEWAY: Network(0x4494) is open for 180 seconds +I (5573) ZIGBEE_GATEWAY: Network steering completed +``` + +- Gateway functionalities supporting Wi-Fi interaction will be added in future updates. ## Troubleshooting diff --git a/examples/zigbee/esp_zigbee_gateway/images/esp-zigbee-gateway-board.webp b/examples/zigbee/esp_zigbee_gateway/images/esp-zigbee-gateway-board.webp new file mode 100644 index 0000000000000000000000000000000000000000..02285eff657227bfbfe0677e90cdeb822ba1ffac GIT binary patch literal 67684 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

{-t-20zyrU)B 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 oZo3_o*gydifB*mh0000000000000000000000000000000IBQ%cK`qY literal 0 HcmV?d00001 diff --git a/examples/zigbee/esp_zigbee_gateway/images/esp-zigbee-standalone-modules.webp b/examples/zigbee/esp_zigbee_gateway/images/esp-zigbee-standalone-modules.webp new file mode 100644 index 0000000000000000000000000000000000000000..e1a50ba0b22b38fef093860da4eb7975f75e32a6 GIT binary patch literal 26888 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 -#include -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" -#include "driver/usb_serial_jtag.h" -#include "esp_coexist.h" + #include "esp_check.h" +#include "esp_err.h" #include "esp_log.h" -#include "esp_netif.h" -#include "esp_vfs_dev.h" -#include "esp_vfs_eventfd.h" -#include "esp_wifi.h" #include "nvs_flash.h" + +#if CONFIG_EXAMPLE_CONNECT_WIFI || CONFIG_EXAMPLE_CONNECT_ETHERNET +#include "esp_event.h" #include "protocol_examples_common.h" -#include "esp_zigbee_gateway.h" -#include "zb_config_platform.h" - -static const char *TAG = "ESP_ZB_GATEWAY"; - -/* Note: Please select the correct console output port based on the development board in menuconfig */ -#if CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG -esp_err_t esp_zb_gateway_console_init(void) -{ - esp_err_t ret = ESP_OK; - /* Disable buffering on stdin */ - setvbuf(stdin, NULL, _IONBF, 0); - - /* Minicom, screen, idf_monitor send CR when ENTER key is pressed */ - usb_serial_jtag_vfs_set_rx_line_endings(ESP_LINE_ENDINGS_CR); - /* Move the caret to the beginning of the next line on '\n' */ - usb_serial_jtag_vfs_set_tx_line_endings(ESP_LINE_ENDINGS_CRLF); - - /* Enable non-blocking mode on stdin and stdout */ - fcntl(fileno(stdout), F_SETFL, O_NONBLOCK); - fcntl(fileno(stdin), F_SETFL, O_NONBLOCK); - - usb_serial_jtag_driver_config_t usb_serial_jtag_config = USB_SERIAL_JTAG_DRIVER_CONFIG_DEFAULT(); - ret = usb_serial_jtag_driver_install(&usb_serial_jtag_config); - usb_serial_jtag_vfs_use_driver(); - uart_vfs_dev_register(); - return ret; -} +#if CONFIG_EXAMPLE_CONNECT_WIFI +#include "esp_wifi.h" +#endif #endif -static void bdb_start_top_level_commissioning_cb(uint8_t mode_mask) +#if CONFIG_AUTO_UPDATE_RCP +#include "zigbee_rcp.h" +#endif + +#if CONFIG_ESP_COEX_SW_COEXIST_ENABLE +#include "esp_coexist.h" +#endif + +#include "light_driver.h" +#include "alarm_timer.h" + +#include "esp_zigbee.h" + +#include "ezbee/zha.h" +#include "esp_zigbee_gateway.h" + +static const char *TAG = "ZIGBEE_GATEWAY"; + +#if (CONFIG_EXAMPLE_CONNECT_WIFI || CONFIG_EXAMPLE_CONNECT_ETHERNET) +static esp_err_t esp_zigbee_connect_netif(void) { - ESP_RETURN_ON_FALSE(esp_zb_bdb_start_top_level_commissioning(mode_mask) == ESP_OK, , TAG, "Failed to start Zigbee bdb commissioning"); + esp_err_t ret = ESP_OK; +#if CONFIG_EXAMPLE_CONNECT_WIFI + wifi_ps_type_t ps_type = WIFI_PS_NONE; +#if CONFIG_ESP_COEX_SW_COEXIST_ENABLE + ps_type = WIFI_PS_MIN_MODEM; +#endif /* CONFIG_ESP_COEX_SW_COEXIST_ENABLE */ +#endif /* CONFIG_EXAMPLE_CONNECT_WIFI */ + + ESP_RETURN_ON_ERROR(esp_netif_init(), TAG, "Failed to initialize network interface"); + ESP_RETURN_ON_ERROR(esp_event_loop_create_default(), TAG, "Failed to create event loop"); + +#if CONFIG_ESP_COEX_SW_COEXIST_ENABLE + esp_coex_wifi_i154_enable(); +#endif /* CONFIG_ESP_COEX_SW_COEXIST_ENABLE*/ + + ESP_RETURN_ON_ERROR(example_connect(), TAG, "Failed to connect to Network Interface"); + +#if CONFIG_EXAMPLE_CONNECT_WIFI + ESP_RETURN_ON_ERROR(esp_wifi_set_ps(ps_type), TAG, "Failed to set power save type for Wi-Fi"); +#endif /* CONFIG_EXAMPLE_CONNECT_WIFI */ + return ret; +} +#endif /* (CONFIG_EXAMPLE_CONNECT_WIFI || CONFIG_EXAMPLE_CONNECT_ETHERNET) */ + +static esp_err_t deferred_driver_init(void) +{ + static bool is_inited = false; + + ESP_RETURN_ON_FALSE(!is_inited, ESP_OK, TAG, "Deferred driver already initialized"); + + light_driver_init(false); + is_inited = true; + + return ESP_OK; } -void esp_zb_app_signal_handler(esp_zb_app_signal_t *signal_struct) +static void esp_zigbee_alarm_bdb_commissioning(alarm_timer_arg_t arg) { - 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 = *p_sg_p; - esp_zb_zdo_signal_device_annce_params_t *dev_annce_params = NULL; + esp_zigbee_lock_acquire(portMAX_DELAY); + (void)ezb_bdb_start_top_level_commissioning(arg); + esp_zigbee_lock_release(); +} - switch (sig_type) { - case ESP_ZB_ZDO_SIGNAL_SKIP_STARTUP: -#if CONFIG_EXAMPLE_CONNECT_WIFI -#if CONFIG_ESP_COEX_SW_COEXIST_ENABLE - esp_coex_wifi_i154_enable(); -#endif /* CONFIG_ESP_COEX_SW_COEXIST_ENABLE */ - ESP_RETURN_ON_FALSE(example_connect() == ESP_OK, , TAG, "Failed to connect to Wi-Fi"); - ESP_RETURN_ON_FALSE(esp_wifi_set_ps(WIFI_PS_MIN_MODEM) == ESP_OK, , TAG, "Failed to set Wi-Fi minimum modem power save type"); -#endif /* CONFIG_EXAMPLE_CONNECT_WIFI */ +static bool esp_zigbee_app_signal_handler(const ezb_app_signal_t *app_signal) +{ + ezb_app_signal_type_t signal_type = ezb_app_signal_get_type(app_signal); + + switch (signal_type) { + case EZB_ZDO_SIGNAL_SKIP_STARTUP: ESP_LOGI(TAG, "Initialize Zigbee stack"); - esp_zb_bdb_start_top_level_commissioning(ESP_ZB_BDB_MODE_INITIALIZATION); + ezb_bdb_start_top_level_commissioning(EZB_BDB_MODE_INITIALIZATION); break; - case ESP_ZB_BDB_SIGNAL_DEVICE_FIRST_START: - case ESP_ZB_BDB_SIGNAL_DEVICE_REBOOT: - if (err_status == ESP_OK) { - ESP_LOGI(TAG, "Device started up in %s factory-reset mode", esp_zb_bdb_is_factory_new() ? "" : "non"); - if (esp_zb_bdb_is_factory_new()) { - ESP_LOGI(TAG, "Start network formation"); - esp_zb_bdb_start_top_level_commissioning(ESP_ZB_BDB_MODE_NETWORK_FORMATION); + case EZB_BDB_SIGNAL_DEVICE_FIRST_START: + case EZB_BDB_SIGNAL_DEVICE_REBOOT: { + ezb_bdb_comm_status_t status = *((ezb_bdb_comm_status_t *)ezb_app_signal_get_params(app_signal)); + if (status == EZB_BDB_STATUS_SUCCESS) { + ESP_LOGI(TAG, "Deferred driver initialization %s", deferred_driver_init() ? "failed" : "successful"); + ESP_LOGI(TAG, "Device started up in%s factory-reset mode", ezb_bdb_is_factory_new() ? "" : " non"); + if (ezb_bdb_is_factory_new()) { + ESP_ERROR_CHECK(ezb_bdb_start_top_level_commissioning(EZB_BDB_MODE_NETWORK_FORMATION)); } else { - esp_zb_bdb_open_network(180); - ESP_LOGI(TAG, "Device rebooted"); + ezb_bdb_open_network(180); + ESP_LOGI(TAG, "Device reboot"); } } else { - ESP_LOGE(TAG, "Failed to initialize Zigbee stack (status: %s)", esp_err_to_name(err_status)); + ESP_LOGW(TAG, "The %s failed with status(0x%02x), please retry", ezb_app_signal_to_string(signal_type), status); + alarm_timer_schedule(esp_zigbee_alarm_bdb_commissioning, EZB_BDB_MODE_INITIALIZATION, 1000); } - break; - case ESP_ZB_BDB_SIGNAL_FORMATION: - if (err_status == ESP_OK) { - esp_zb_ieee_addr_t ieee_address; - esp_zb_get_long_address(ieee_address); - ESP_LOGI(TAG, "Formed network successfully (Extended PAN ID: %02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x, PAN ID: 0x%04hx, Channel:%d, Short Address: 0x%04hx)", - ieee_address[7], ieee_address[6], ieee_address[5], ieee_address[4], - ieee_address[3], ieee_address[2], ieee_address[1], ieee_address[0], - esp_zb_get_pan_id(), esp_zb_get_current_channel(), esp_zb_get_short_address()); - esp_zb_bdb_start_top_level_commissioning(ESP_ZB_BDB_MODE_NETWORK_STEERING); + } break; + case EZB_BDB_SIGNAL_FORMATION: { + ezb_bdb_comm_status_t status = *((ezb_bdb_comm_status_t *)ezb_app_signal_get_params(app_signal)); + if (status == EZB_BDB_STATUS_SUCCESS) { + ezb_extpanid_t extended_pan_id; + ezb_nwk_get_extended_panid(&extended_pan_id); + ESP_LOGI(TAG, "Formed network successfully: PAN ID(0x%04hx, EXT: 0x%llx), Channel(%d), Short Address(0x%04hx)", + ezb_nwk_get_panid(), extended_pan_id.u64, ezb_nwk_get_current_channel(), ezb_nwk_get_short_address()); + ezb_bdb_start_top_level_commissioning(EZB_BDB_MODE_NETWORK_STEERING); } else { - ESP_LOGI(TAG, "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); + ESP_LOGW(TAG, "Failed to form network with status(0x%02x)", status); + alarm_timer_schedule(esp_zigbee_alarm_bdb_commissioning, EZB_BDB_MODE_NETWORK_FORMATION, 1000); } - break; - case ESP_ZB_BDB_SIGNAL_STEERING: - if (err_status == ESP_OK) { - ESP_LOGI(TAG, "Network steering started"); + } break; + case EZB_BDB_SIGNAL_STEERING: { + ezb_bdb_comm_status_t status = *((ezb_bdb_comm_status_t *)ezb_app_signal_get_params(app_signal)); + if (status == EZB_BDB_STATUS_SUCCESS) { + ESP_LOGI(TAG, "Network steering completed"); + } else { + ESP_LOGW(TAG, "Failed to steering network with status(0x%02x)", status); + alarm_timer_schedule(esp_zigbee_alarm_bdb_commissioning, EZB_BDB_MODE_NETWORK_FORMATION, 1000); } - break; - case ESP_ZB_ZDO_SIGNAL_DEVICE_ANNCE: - dev_annce_params = (esp_zb_zdo_signal_device_annce_params_t *)esp_zb_app_signal_get_params(p_sg_p); - ESP_LOGI(TAG, "New device commissioned or rejoined (short: 0x%04hx)", dev_annce_params->device_short_addr); - break; - case ESP_ZB_NWK_SIGNAL_PERMIT_JOIN_STATUS: - if (err_status == ESP_OK) { - if (*(uint8_t *)esp_zb_app_signal_get_params(p_sg_p)) { - ESP_LOGI(TAG, "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 { - ESP_LOGW(TAG, "Network(0x%04hx) closed, devices joining not allowed.", esp_zb_get_pan_id()); - } + } break; + case EZB_ZDO_SIGNAL_DEVICE_ANNCE: { + const ezb_zdo_signal_device_annce_params_t *dev_annce_params = ezb_app_signal_get_params(app_signal); + ESP_LOGI(TAG, "New device commissioned or rejoined (short: 0x%04hx)", dev_annce_params->short_addr); + } break; + case EZB_ZDO_SIGNAL_LEAVE_INDICATION: { + const ezb_zdo_signal_leave_indication_params_t *leave_ind_params = ezb_app_signal_get_params(app_signal); + ESP_LOGI(TAG, "Zigbee Node(0x%04hx) is leaving network", leave_ind_params->short_addr); + } break; + case EZB_NWK_SIGNAL_PERMIT_JOIN_STATUS: { + uint8_t duration = *(uint8_t *)ezb_app_signal_get_params(app_signal); + if (duration) { + ESP_LOGI(TAG, "Network(0x%04hx) is open for %d seconds", ezb_nwk_get_panid(), duration); + } else { + ESP_LOGW(TAG, "Network(0x%04hx) closed, devices joining not allowed.", ezb_nwk_get_panid()); } - break; - case ESP_ZB_ZDO_SIGNAL_PRODUCTION_CONFIG_READY: - ESP_LOGI(TAG, "Production configuration is %s", err_status == ESP_OK ? "ready" : "not present"); - esp_zb_set_node_descriptor_manufacturer_code(ESP_MANUFACTURER_CODE); - break; + } break; default: - ESP_LOGI(TAG, "ZDO signal: %s (0x%x), status: %s", esp_zb_zdo_signal_to_string(sig_type), sig_type, - esp_err_to_name(err_status)); + ESP_LOGI(TAG, "Zigbee APP Signal: %s(type: 0x%02x)", ezb_app_signal_to_string(signal_type), signal_type); + break; + } + return true; +} + +static void zcl_core_set_attr_value_handler(ezb_zcl_set_attr_value_message_t *message) +{ + ESP_RETURN_ON_FALSE(message, , TAG, "message is empty"); + ESP_LOGI(TAG, "ZCL SetAttributeValue message for endpoint(%d) cluster(0x%04x) %s with status(0x%02x)", message->info.dst_ep, + message->info.cluster_id, message->info.cluster_role == EZB_ZCL_CLUSTER_SERVER ? "server" : "client", + message->info.status); + if (message->info.cluster_id == EZB_ZCL_CLUSTER_ID_ON_OFF) { + light_driver_set_power(*(uint8_t *)message->in.attribute.data.value); + ESP_LOGI(TAG, "Set On/Off: %d", *(uint8_t *)message->in.attribute.data.value); + } else { + ESP_LOGW(TAG, "Unsupported cluster ID(0x%04x)", message->info.cluster_id); + } +} + +static void esp_zigbee_zcl_core_action_handler(ezb_zcl_core_action_callback_id_t callback_id, void *message) +{ + switch (callback_id) { + case EZB_ZCL_CORE_SET_ATTR_VALUE_CB_ID: + zcl_core_set_attr_value_handler(message); + break; + case EZB_ZCL_CORE_DEFAULT_RSP_CB_ID: { + ezb_zcl_cmd_default_rsp_message_t *default_rsp = (ezb_zcl_cmd_default_rsp_message_t *)message; + ESP_LOGI(TAG, "Received ZCL Default Response with status(0x%02x)", default_rsp->in.status_code); + } break; + default: + ESP_LOGW(TAG, "ZCL Core Action: ID(0x%04lx)", callback_id); break; } } -static void esp_zb_task(void *pvParameters) +esp_err_t esp_zigbee_create_zha_gateway_device(void) { - /* initialize Zigbee stack */ - esp_zb_cfg_t zb_nwk_cfg = ESP_ZB_ZC_CONFIG(); - esp_zb_init(&zb_nwk_cfg); - esp_zb_set_primary_network_channel_set(ESP_ZB_PRIMARY_CHANNEL_MASK); - esp_zb_ep_list_t *ep_list = esp_zb_ep_list_create(); - esp_zb_cluster_list_t *cluster_list = esp_zb_zcl_cluster_list_create(); - esp_zb_endpoint_config_t endpoint_config = { - .endpoint = ESP_ZB_GATEWAY_ENDPOINT, - .app_profile_id = ESP_ZB_AF_HA_PROFILE_ID, - .app_device_id = ESP_ZB_HA_REMOTE_CONTROL_DEVICE_ID, - .app_device_version = 0, - }; + ezb_af_device_desc_t dev_desc = ezb_af_create_device_desc(); + ezb_zha_custom_gateway_config_t gateway_cfg = EZB_ZHA_CUSTOM_GATEWAY_CONFIG(); + ezb_af_ep_desc_t ep_desc = ezb_zha_create_custom_gateway(ESP_ZIGBEE_CUSTOM_GATEWAY_EP_ID, &gateway_cfg); + ezb_zcl_cluster_desc_t basic_desc = {0}; + + basic_desc = ezb_af_endpoint_get_cluster_desc(ep_desc, EZB_ZCL_CLUSTER_ID_BASIC, EZB_ZCL_CLUSTER_SERVER); + ezb_zcl_basic_cluster_desc_add_attr(basic_desc, EZB_ZCL_ATTR_BASIC_MANUFACTURER_NAME_ID, (void *)ESP_MANUFACTURER_NAME); + ezb_zcl_basic_cluster_desc_add_attr(basic_desc, EZB_ZCL_ATTR_BASIC_MODEL_IDENTIFIER_ID, (void *)ESP_MODEL_IDENTIFIER); + ezb_af_endpoint_add_cluster_desc(ep_desc, ezb_zcl_on_off_create_cluster_desc(NULL, EZB_ZCL_CLUSTER_SERVER)); + + ESP_ERROR_CHECK(ezb_af_device_add_endpoint_desc(dev_desc, ep_desc)); + ESP_ERROR_CHECK(ezb_af_device_desc_register(dev_desc)); + + ezb_zcl_core_action_handler_register(esp_zigbee_zcl_core_action_handler); + + return ESP_OK; +} + +esp_err_t esp_zigbee_setup_commissioning(void) +{ + ezb_aps_secur_enable_distributed_security(false); + ESP_ERROR_CHECK(ezb_bdb_set_primary_channel_set(ESP_ZIGBEE_PRIMARY_CHANNEL_MASK)); + ESP_ERROR_CHECK(ezb_bdb_set_secondary_channel_set(ESP_ZIGBEE_SECONDARY_CHANNEL_MASK)); + ESP_ERROR_CHECK(ezb_app_signal_add_handler(esp_zigbee_app_signal_handler)); + + return ESP_OK; +} + +static void esp_zigbee_stack_main_task(void *pvParameters) +{ + esp_zigbee_config_t zigbee_config = ESP_ZIGBEE_DEFAULT_CONFIG(); + +#if CONFIG_AUTO_UPDATE_RCP + esp_rcp_update_config_t rcp_config = ESP_ZIGBEE_RCP_CONFIG(); + ESP_ERROR_CHECK(esp_zigbee_rcp_init(&rcp_config)); +#endif + + ESP_ERROR_CHECK(esp_zigbee_init(&zigbee_config)); + +#if CONFIG_AUTO_UPDATE_RCP + ESP_ERROR_CHECK(esp_zigbee_rcp_update()); +#endif + +#if (CONFIG_EXAMPLE_CONNECT_WIFI || CONFIG_EXAMPLE_CONNECT_ETHERNET) + ESP_ERROR_CHECK(esp_zigbee_connect_netif()); +#endif + + ESP_ERROR_CHECK(esp_zigbee_setup_commissioning()); + + ESP_ERROR_CHECK(esp_zigbee_create_zha_gateway_device()); + + ESP_ERROR_CHECK(esp_zigbee_start(false)); + + esp_zigbee_launch_mainloop(); + + esp_zigbee_deinit(); + +#if CONFIG_AUTO_UPDATE_RCP + esp_zigbee_rcp_deinit(); +#endif - esp_zb_attribute_list_t *basic_cluser = esp_zb_basic_cluster_create(NULL); - esp_zb_basic_cluster_add_attr(basic_cluser, ESP_ZB_ZCL_ATTR_BASIC_MANUFACTURER_NAME_ID, ESP_MANUFACTURER_NAME); - esp_zb_basic_cluster_add_attr(basic_cluser, ESP_ZB_ZCL_ATTR_BASIC_MODEL_IDENTIFIER_ID, ESP_MODEL_IDENTIFIER); - esp_zb_cluster_list_add_basic_cluster(cluster_list, basic_cluser, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE); - esp_zb_cluster_list_add_identify_cluster(cluster_list, esp_zb_identify_cluster_create(NULL), ESP_ZB_ZCL_CLUSTER_SERVER_ROLE); - esp_zb_ep_list_add_gateway_ep(ep_list, cluster_list, endpoint_config); - esp_zb_device_register(ep_list); - ESP_ERROR_CHECK(esp_zb_start(false)); - esp_zb_stack_main_loop(); vTaskDelete(NULL); } void app_main(void) { - esp_zb_platform_config_t config = { - .radio_config = ESP_ZB_DEFAULT_RADIO_CONFIG(), - .host_config = ESP_ZB_DEFAULT_HOST_CONFIG(), - }; - - ESP_ERROR_CHECK(esp_zb_platform_config(&config)); ESP_ERROR_CHECK(nvs_flash_init()); - ESP_ERROR_CHECK(esp_netif_init()); - ESP_ERROR_CHECK(esp_event_loop_create_default()); -#if CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG - ESP_ERROR_CHECK(esp_zb_gateway_console_init()); -#endif - xTaskCreate(esp_zb_task, "Zigbee_main", 8192, NULL, 5, NULL); + + ESP_LOGI(TAG, "Start ESP Zigbee Stack"); + xTaskCreate(esp_zigbee_stack_main_task, "Zigbee_main", 4096 * 2, NULL, 5, NULL); } diff --git a/examples/zigbee/esp_zigbee_gateway/main/esp_zigbee_gateway.h b/examples/zigbee/esp_zigbee_gateway/main/esp_zigbee_gateway.h index 03a880bb59c..169b36017ac 100644 --- a/examples/zigbee/esp_zigbee_gateway/main/esp_zigbee_gateway.h +++ b/examples/zigbee/esp_zigbee_gateway/main/esp_zigbee_gateway.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: LicenseRef-Included * @@ -12,62 +12,87 @@ * CONDITIONS OF ANY KIND, either express or implied. */ -#include "esp_err.h" -#include "esp_zigbee_core.h" +#pragma once -/* Zigbee Configuration */ -#define MAX_CHILDREN 10 /* the max amount of connected devices */ -#define INSTALLCODE_POLICY_ENABLE false /* enable the install code policy for security */ -#define ESP_ZB_PRIMARY_CHANNEL_MASK (1l << 13) /* Zigbee primary channel mask use in the example */ -#define ESP_ZB_GATEWAY_ENDPOINT 1 /* Gateway endpoint identifier */ -#define APP_PROD_CFG_CURRENT_VERSION 0x0001 /* Production configuration version */ +#define ESP_ZIGBEE_PRIMARY_CHANNEL_MASK (1U << CONFIG_ZB_EXAMPLE_PRIMARY_CHANNEL) +#define ESP_ZIGBEE_SECONDARY_CHANNEL_MASK CONFIG_ZB_EXAMPLE_SECONDARY_CHANNEL_MASK -/* Basic manufacturer information */ -#define ESP_MANUFACTURER_CODE 0x131B /* Customized manufacturer code */ -#define ESP_MANUFACTURER_NAME "\x09""ESPRESSIF" /* Customized manufacturer name */ -#define ESP_MODEL_IDENTIFIER "\x07"CONFIG_IDF_TARGET /* Customized model identifier */ +#define ESP_ZIGBEE_CUSTOM_GATEWAY_EP_ID (10) -/* RCP connection pins */ -#define HOST_RX_PIN_TO_RCP_TX 4 -#define HOST_TX_PIN_TO_RCP_RX 5 +#define ESP_ZIGBEE_STORAGE_PARTITION_NAME "nvs" -#define ESP_ZB_ZC_CONFIG() \ - { \ - .esp_zb_role = ESP_ZB_DEVICE_TYPE_COORDINATOR, \ - .install_code_policy = INSTALLCODE_POLICY_ENABLE, \ - .nwk_cfg.zczr_cfg = { \ - .max_children = MAX_CHILDREN, \ - }, \ +#define ESP_MANUFACTURER_NAME "\x09""ESPRESSIF" +#define ESP_MODEL_IDENTIFIER "\x07"CONFIG_IDF_TARGET + +#if defined(CONFIG_ZB_GW_RCP_CHIP_ESP32C6) +#define ESP_ZIGBEE_RCP_TARGET_CHIP ESP32C6_CHIP +#elif defined(CONFIG_ZB_GW_RCP_CHIP_ESP32H2) +#define ESP_ZIGBEE_RCP_TARGET_CHIP ESP32H2_CHIP +#else +#define ESP_ZIGBEE_RCP_TARGET_CHIP ESP_UNKNOWN_CHIP +#endif + +#define ESP_ZIGBEE_ZC_CONFIG() \ + { \ + .device_type = EZB_NWK_DEVICE_TYPE_COORDINATOR, \ + .install_code_policy = false, \ + .zczr_config = { \ + .max_children = 10, \ + }, \ } -#if CONFIG_ZB_RADIO_NATIVE -#define ESP_ZB_DEFAULT_RADIO_CONFIG() \ - { \ - .radio_mode = ZB_RADIO_MODE_NATIVE, \ +#if CONFIG_SOC_IEEE802154_SUPPORTED +#define ESP_ZIGBEE_PLATFORM_CONFIG() \ + { \ + .storage_partition_name = ESP_ZIGBEE_STORAGE_PARTITION_NAME, \ + .radio_config = { \ + .radio_mode = ESP_ZIGBEE_RADIO_MODE_NATIVE, \ + }, \ } #else -#define ESP_ZB_DEFAULT_RADIO_CONFIG() \ - { \ - .radio_mode = ZB_RADIO_MODE_UART_RCP, \ - .radio_uart_config = { \ - .port = 1, \ - .uart_config = \ - { \ - .baud_rate = 460800, \ - .data_bits = UART_DATA_8_BITS, \ - .parity = UART_PARITY_DISABLE, \ - .stop_bits = UART_STOP_BITS_1, \ - .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, \ - .rx_flow_ctrl_thresh = 0, \ - .source_clk = UART_SCLK_DEFAULT, \ - }, \ - .rx_pin = HOST_RX_PIN_TO_RCP_TX, \ - .tx_pin = HOST_TX_PIN_TO_RCP_RX, \ - }, \ +#define ESP_ZIGBEE_UART_CONFIG() \ + { \ + .port = 1, \ + .uart_config = \ + { \ + .baud_rate = 460800, \ + .data_bits = UART_DATA_8_BITS, \ + .parity = UART_PARITY_DISABLE, \ + .stop_bits = UART_STOP_BITS_1, \ + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, \ + .rx_flow_ctrl_thresh = 0, \ + .source_clk = UART_SCLK_DEFAULT, \ + }, \ + .rx_pin = CONFIG_DEFAULT_PIN_TO_RCP_TX, \ + .tx_pin = CONFIG_DEFAULT_PIN_TO_RCP_RX, \ + } + +#define ESP_ZIGBEE_PLATFORM_CONFIG() \ + { \ + .storage_partition_name = ESP_ZIGBEE_STORAGE_PARTITION_NAME, \ + .radio_config = { \ + .radio_mode = ESP_ZIGBEE_RADIO_MODE_UART_RCP, \ + .radio_uart_config = ESP_ZIGBEE_UART_CONFIG(), \ + }, \ } #endif -#define ESP_ZB_DEFAULT_HOST_CONFIG() \ - { \ - .host_connection_mode = ZB_HOST_CONNECTION_MODE_NONE, \ - } +#define ESP_ZIGBEE_DEFAULT_CONFIG() \ + { \ + .device_config = ESP_ZIGBEE_ZC_CONFIG(), \ + .platform_config = ESP_ZIGBEE_PLATFORM_CONFIG(), \ + }; + +#define ESP_ZIGBEE_RCP_CONFIG() \ + { \ + .rcp_type = RCP_TYPE_UART, \ + .uart_rx_pin = CONFIG_DEFAULT_PIN_TO_RCP_TX, \ + .uart_tx_pin = CONFIG_DEFAULT_PIN_TO_RCP_RX, \ + .uart_port = 1, \ + .uart_baudrate = 115200, \ + .reset_pin = CONFIG_DEFAULT_PIN_TO_RCP_RESET, \ + .boot_pin = CONFIG_DEFAULT_PIN_TO_RCP_BOOT, \ + .update_baudrate = 460800, \ + .firmware_dir = "/" CONFIG_RCP_PARTITION_NAME "/ot_rcp", \ + .target_chip = ESP_ZIGBEE_RCP_TARGET_CHIP, \ + } diff --git a/examples/zigbee/esp_zigbee_gateway/main/idf_component.yml b/examples/zigbee/esp_zigbee_gateway/main/idf_component.yml index d3a34266983..8ba0e508e57 100644 --- a/examples/zigbee/esp_zigbee_gateway/main/idf_component.yml +++ b/examples/zigbee/esp_zigbee_gateway/main/idf_component.yml @@ -1,9 +1,13 @@ ## IDF Component Manager Manifest File dependencies: - espressif/esp-zboss-lib: "~1.6.0" - espressif/esp-zigbee-lib: "~1.6.0" - ## Required IDF version + espressif/esp_rcp_update: "~1.6.0" + espressif/esp-zigbee-lib: + version: ">=2.0.0" idf: - version: ">=5.0.0" + version: ">=5.2" + example_common: + path: ${IDF_PATH}/examples/zigbee/zb_common_components/example_common + light_driver: + path: ${IDF_PATH}/examples/zigbee/zb_common_components/light_driver protocol_examples_common: path: ${IDF_PATH}/examples/common_components/protocol_examples_common diff --git a/examples/zigbee/esp_zigbee_gateway/main/zigbee_rcp.c b/examples/zigbee/esp_zigbee_gateway/main/zigbee_rcp.c new file mode 100644 index 00000000000..67d1ebd21b4 --- /dev/null +++ b/examples/zigbee/esp_zigbee_gateway/main/zigbee_rcp.c @@ -0,0 +1,124 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: LicenseRef-Included + * + * This example code is in the Public Domain (or CC0 licensed, at your option.) + * + * Unless required by applicable law or agreed to in writing, this + * software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. + */ + +#include +#include "esp_log.h" +#include "esp_check.h" +#include "sdkconfig.h" +#if CONFIG_AUTO_UPDATE_RCP +#include "esp_rcp_update.h" +#include "esp_spiffs.h" +#endif + +#if CONFIG_OPENTHREAD_SPINEL_ONLY +#include "esp_radio_spinel.h" +#define ESP_ZIGBEE_RCP_VERSION_MAX_SIZE 80 +#endif + +#include "esp_zigbee.h" + +#include "zigbee_rcp.h" + +#define TAG "ZIGBEE_RCP" + +#if CONFIG_AUTO_UPDATE_RCP + +static void rcp_update(void) +{ + ESP_LOGI(TAG, "Update RCP"); + /* Stop Zigbee stack during RCP update */ + esp_zigbee_deinit(); + if (esp_rcp_update() != ESP_OK) { + esp_rcp_mark_image_verified(false); + } + esp_restart(); +} + +static void rcp_fatal_failure_handler(void) +{ +#if CONFIG_AUTO_UPDATE_RCP + ESP_LOGW(TAG, "RCP failure, re-flashing RCP"); + rcp_update(); +#endif + esp_restart(); +} + +static void rcp_failure_handler(void) +{ + ESP_LOGW(TAG, "RCP failure, reset RCP"); + esp_rcp_reset(); +} + +esp_err_t esp_zigbee_rcp_update(void) +{ + char rcp_version[ESP_ZIGBEE_RCP_VERSION_MAX_SIZE] = {0}; + ESP_RETURN_ON_ERROR(esp_radio_spinel_rcp_version_get(rcp_version, ESP_RADIO_SPINEL_ZIGBEE), TAG, + "Failed to get rcp version from radio spinel"); +#if (CONFIG_AUTO_UPDATE_RCP) + char storage_rcp_version[ESP_ZIGBEE_RCP_VERSION_MAX_SIZE] = {0}; + if (esp_rcp_load_version_in_storage(storage_rcp_version, sizeof(storage_rcp_version)) == ESP_OK) { + if (strcmp(storage_rcp_version, rcp_version)) { + ESP_LOGI(TAG, "Update the RCP version from %s to %s", storage_rcp_version, rcp_version); + rcp_update(); + } else { + ESP_LOGI(TAG, "Running RCP version: %s", rcp_version); + esp_rcp_mark_image_verified(true); + } + } else { + ESP_LOGI(TAG, "No RCP firmware found in storage, reboot to try next image"); + esp_rcp_mark_image_verified(false); + esp_restart(); + } +#endif + return ESP_OK; +} + +static esp_err_t spiffs_init(void) +{ + esp_vfs_spiffs_conf_t rcp_fw_conf = { + .base_path = "/" CONFIG_RCP_PARTITION_NAME, + .partition_label = CONFIG_RCP_PARTITION_NAME, + .max_files = 10, + .format_if_mount_failed = false, + }; + return esp_vfs_spiffs_register(&rcp_fw_conf); +} + +static void spiffs_deinit(void) +{ + esp_vfs_spiffs_unregister(CONFIG_RCP_PARTITION_NAME); +} + +esp_err_t esp_zigbee_rcp_init(esp_rcp_update_config_t *config) +{ + ESP_RETURN_ON_FALSE(config != NULL, ESP_ERR_INVALID_ARG, TAG, "config is NULL"); + ESP_ERROR_CHECK(spiffs_init()); + ESP_ERROR_CHECK(esp_rcp_update_init(config)); +#if CONFIG_OPENTHREAD_SPINEL_ONLY + esp_radio_spinel_register_rcp_failure_handler(rcp_failure_handler, ESP_RADIO_SPINEL_ZIGBEE); + esp_radio_spinel_set_compatibility_error_callback(rcp_fatal_failure_handler); + esp_radio_spinel_set_coprocessor_reset_failure_callback(rcp_fatal_failure_handler); +#endif + return ESP_OK; +} + +void esp_zigbee_rcp_deinit(void) +{ +#if CONFIG_OPENTHREAD_SPINEL_ONLY + esp_radio_spinel_set_coprocessor_reset_failure_callback(NULL); + esp_radio_spinel_set_compatibility_error_callback(NULL); + esp_radio_spinel_register_rcp_failure_handler(NULL, ESP_RADIO_SPINEL_ZIGBEE); +#endif + esp_rcp_update_deinit(); + spiffs_deinit(); +} +#endif diff --git a/examples/zigbee/esp_zigbee_gateway/main/zigbee_rcp.h b/examples/zigbee/esp_zigbee_gateway/main/zigbee_rcp.h new file mode 100644 index 00000000000..1af0b43303d --- /dev/null +++ b/examples/zigbee/esp_zigbee_gateway/main/zigbee_rcp.h @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: LicenseRef-Included + * + * This example code is in the Public Domain (or CC0 licensed, at your option.) + * + * Unless required by applicable law or agreed to in writing, this + * software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. + */ + +#pragma once + +#include "esp_err.h" +#include "esp_rcp_update.h" + +esp_err_t esp_zigbee_rcp_init(esp_rcp_update_config_t *config); + +void esp_zigbee_rcp_deinit(void); + +esp_err_t esp_zigbee_rcp_update(void); diff --git a/examples/zigbee/esp_zigbee_gateway/partitions.csv b/examples/zigbee/esp_zigbee_gateway/partitions.csv index 055152f11a6..63a03fb79c1 100644 --- a/examples/zigbee/esp_zigbee_gateway/partitions.csv +++ b/examples/zigbee/esp_zigbee_gateway/partitions.csv @@ -1,8 +1,8 @@ # Name, Type, SubType, Offset, Size, Flags # Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap -nvs, data, nvs, 0x9000, 0x6000, -phy_init, data, phy, 0xf000, 0x1000, -factory, app, factory, 0x10000, 1400K, -zb_storage, data, fat, 0x16e000,16K, -zb_fct, data, fat, 0x172000,1K, -rcp_fw, data, spiffs, 0x173000,500k, +nvs, data, nvs, , 0x6000, +otadata, data, ota, , 0x2000, +phy_init, data, phy, , 0x1000, +factory, app, factory, , 1708K, +zb_fct, data, fat, , 1K, +rcp_fw, data, spiffs, , 640k, diff --git a/examples/zigbee/esp_zigbee_gateway/sdkconfig.defaults b/examples/zigbee/esp_zigbee_gateway/sdkconfig.defaults index d2995026210..e4887059c4d 100644 --- a/examples/zigbee/esp_zigbee_gateway/sdkconfig.defaults +++ b/examples/zigbee/esp_zigbee_gateway/sdkconfig.defaults @@ -8,12 +8,28 @@ CONFIG_PARTITION_TABLE_OFFSET=0x8000 CONFIG_PARTITION_TABLE_MD5=y # end of Partition Table -CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096 +# +# Serial flasher config +# +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y +# end of Serial flasher config # -# Zboss +# Event +# +CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096 +#end of Event + + +# +# Zigbee # CONFIG_ZB_ENABLED=y CONFIG_ZB_ZCZR=y -# end of Zboss -# end of Component config +# end of Zigbee + +# +# ESP Zigbee gateway RCP update +# +CONFIG_AUTO_UPDATE_RCP=n +# end of ESP Zigbee gateway RCP update diff --git a/examples/zigbee/light_sample/HA_on_off_light/CMakeLists.txt b/examples/zigbee/light_sample/HA_on_off_light/CMakeLists.txt index 1c706695df5..e1b695bacd2 100644 --- a/examples/zigbee/light_sample/HA_on_off_light/CMakeLists.txt +++ b/examples/zigbee/light_sample/HA_on_off_light/CMakeLists.txt @@ -3,6 +3,9 @@ cmake_minimum_required(VERSION 3.22) include($ENV{IDF_PATH}/tools/cmake/project.cmake) +list(PREPEND SDKCONFIG_DEFAULTS + "${IDF_PATH}/examples/zigbee/zb_common_components/example_common/sdkconfig.mbedtls_minimal" + "sdkconfig.defaults") # "Trim" the build. Include the minimal set of components, main, and anything it depends on. idf_build_set_property(MINIMAL_BUILD ON) project(light_bulb) diff --git a/examples/zigbee/light_sample/HA_on_off_light/README.md b/examples/zigbee/light_sample/HA_on_off_light/README.md index 1078416c3ba..c5cf96b392c 100644 --- a/examples/zigbee/light_sample/HA_on_off_light/README.md +++ b/examples/zigbee/light_sample/HA_on_off_light/README.md @@ -1,9 +1,9 @@ -| Supported Targets | ESP32-C5 | ESP32-C6 | ESP32-H2 | -| ----------------- | -------- | -------- | -------- | +| Supported Targets | ESP32-C5 | ESP32-C6 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-S31 | +| ----------------- | -------- | -------- | -------- | --------- | -------- | --------- | # Light Bulb Example -This test code shows how to configure Zigbee end device and use it as HA on/off light bulb. +This example demonstrates how to configure a Home Automation on/off light on a Zigbee Coordinator using ESP Zigbee SDK v2. The ESP Zigbee SDK provides more examples and tools for productization: * [ESP Zigbee SDK Docs](https://docs.espressif.com/projects/esp-zigbee-sdk) @@ -11,13 +11,13 @@ The ESP Zigbee SDK provides more examples and tools for productization: ## Hardware Required -* One development board with ESP32-H2 SoC acting as Zigbee end-device (loaded with HA_on_off_light example) +* One 802.15.4-enabled development board running this example as a Zigbee Coordinator * A USB cable for power supply and programming -* Choose another ESP32-H2 as Zigbee coordinator (see [HA_on_off_switch example](../HA_on_off_switch)) +* A second board running as a Zigbee Router/End Device (see [HA_on_off_switch](../HA_on_off_switch)) ## Configure the project -Before project configuration and build, make sure to set the correct chip target using `idf.py --preview set-target TARGET` command. +Before project configuration and build, make sure to set the correct chip target using `idf.py set-target TARGET`. ## Erase the NVRAM @@ -31,36 +31,28 @@ Build the project, flash it to the board, and start the monitor tool to view the ## Example Output -As you run the example, you will see the following log: +When the program starts, the board forms a Zigbee network and opens it for joining. As you run the example, you will see output similar to: ``` -I (403) app_start: Starting scheduler on CPU0 -I (408) main_task: Started on CPU0 -I (408) main_task: Calling app_main() -I (428) phy: phy_version: 230,2, 9aae6ea, Jan 15 2024, 11:17:12 -I (428) phy: libbtbb version: 944f18e, Jan 15 2024, 11:17:25 -I (438) main_task: Returned from app_main() -I (548) ESP_ZB_ON_OFF_LIGHT: ZDO signal: ZDO Config Ready (0x17), status: ESP_FAIL -I (548) ESP_ZB_ON_OFF_LIGHT: Initialize Zigbee stack -W (548) rmt: channel resolution loss, real=10666666 -I (558) gpio: GPIO[8]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 -I (548) ESP_ZB_ON_OFF_LIGHT: Deferred driver initialization successful -I (568) ESP_ZB_ON_OFF_LIGHT: Device started up in factory-reset mode -I (578) ESP_ZB_ON_OFF_LIGHT: Start network steering -I (3558) ESP_ZB_ON_OFF_LIGHT: Joined network successfully (Extended PAN ID: 74:4d:bd:ff:fe:63:f7:30, PAN ID: 0x13af, Channel:13, Short Address: 0x7c16) -I (10238) ESP_ZB_ON_OFF_LIGHT: Received message: endpoint(10), cluster(0x6), attribute(0x0), data size(1) -I (10238) ESP_ZB_ON_OFF_LIGHT: Light sets to On -I (10798) ESP_ZB_ON_OFF_LIGHT: Received message: endpoint(10), cluster(0x6), attribute(0x0), data size(1) -I (10798) ESP_ZB_ON_OFF_LIGHT: Light sets to Off -I (11228) ESP_ZB_ON_OFF_LIGHT: Received message: endpoint(10), cluster(0x6), attribute(0x0), data size(1) -I (11228) ESP_ZB_ON_OFF_LIGHT: Light sets to On -I (11618) ESP_ZB_ON_OFF_LIGHT: Received message: endpoint(10), cluster(0x6), attribute(0x0), data size(1) -I (11618) ESP_ZB_ON_OFF_LIGHT: Light sets to Off +I (435) ON_OFF_LIGHT: Start ESP Zigbee Stack +I (475) ON_OFF_LIGHT: Initialize Zigbee stack +I (475) ON_OFF_LIGHT: Deferred driver initialization successful +I (485) ON_OFF_LIGHT: Device started up in factory-reset mode +I (795) ON_OFF_LIGHT: Formed network successfully: PAN ID(0x3051, EXT: 0x4831b7fffec183f0), Channel(13), Short Address(0x0000) +I (1175) ON_OFF_LIGHT: Network steering completed +I (3705) ON_OFF_LIGHT: New device commissioned or rejoined(short: 0xa515) ``` ## Light Control Functions - * By toggling the switch button (BOOT) on the ESP32-H2 board loaded with the `HA_on_off_switch` example, the LED on this board loaded with `HA_on_off_light` example will be on and off. +* By toggling the switch button (BOOT) on the board loaded with the `HA_on_off_switch` example, the LED on this board will turn on and off. + +``` +I (26905) ON_OFF_LIGHT: ZCL SetAttributeValue message for endpoint(10) cluster(0x0006) server with status(0x00) +I (26905) ON_OFF_LIGHT: Set On/Off: 1 +I (27535) ON_OFF_LIGHT: ZCL SetAttributeValue message for endpoint(10) cluster(0x0006) server with status(0x00) +I (27535) ON_OFF_LIGHT: Set On/Off: 0 +``` ## Troubleshooting diff --git a/examples/zigbee/light_sample/HA_on_off_light/main/CMakeLists.txt b/examples/zigbee/light_sample/HA_on_off_light/main/CMakeLists.txt index 8c00feb33ad..1c98b7bfbb6 100644 --- a/examples/zigbee/light_sample/HA_on_off_light/main/CMakeLists.txt +++ b/examples/zigbee/light_sample/HA_on_off_light/main/CMakeLists.txt @@ -1,5 +1,3 @@ -idf_component_register( - SRC_DIRS "." - INCLUDE_DIRS "." - PRIV_REQUIRES nvs_flash esp_driver_uart ieee802154 -) +idf_component_register(SRC_DIRS "." + PRIV_INCLUDE_DIRS "." + PRIV_REQUIRES nvs_flash esp_driver_uart ieee802154) diff --git a/examples/zigbee/light_sample/HA_on_off_light/main/esp_zb_light.c b/examples/zigbee/light_sample/HA_on_off_light/main/esp_zb_light.c index 916baa2fc5e..119dd66fd5b 100644 --- a/examples/zigbee/light_sample/HA_on_off_light/main/esp_zb_light.c +++ b/examples/zigbee/light_sample/HA_on_off_light/main/esp_zb_light.c @@ -1,7 +1,7 @@ /* - * SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD * - * SPDX-License-Identifier: LicenseRef-Included + * SPDX-License-Identifier: LicenseRef-Included * * Zigbee HA_on_off_light Example * @@ -11,141 +11,188 @@ * software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR * CONDITIONS OF ANY KIND, either express or implied. */ -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" + #include "esp_check.h" +#include "esp_err.h" #include "esp_log.h" #include "nvs_flash.h" -#include "ha/esp_zigbee_ha_standard.h" -#include "zcl_utility.h" + +#include "light_driver.h" +#include "alarm_timer.h" + +#include "esp_zigbee.h" +#include "ezbee/zha.h" + #include "esp_zb_light.h" -#if !defined ZB_ED_ROLE -#error Define ZB_ED_ROLE in idf.py menuconfig to compile light (End Device) source code. -#endif +static const char *TAG = "ON_OFF_LIGHT"; -static const char *TAG = "ESP_ZB_ON_OFF_LIGHT"; -/********************* Define functions **************************/ -static esp_err_t deferred_driver_init(void) +esp_err_t deferred_driver_init(void) { - light_driver_init(LIGHT_DEFAULT_OFF); + static bool is_inited = false; + + ESP_RETURN_ON_FALSE(!is_inited, ESP_OK, TAG, "Deferred driver already initialized"); + + light_driver_init(false); + is_inited = true; + return ESP_OK; } -static void bdb_start_top_level_commissioning_cb(uint8_t mode_mask) +static void esp_zigbee_alarm_bdb_commissioning(alarm_timer_arg_t arg) { - ESP_RETURN_ON_FALSE(esp_zb_bdb_start_top_level_commissioning(mode_mask) == ESP_OK, , TAG, "Failed to start Zigbee commissioning"); + esp_zigbee_lock_acquire(portMAX_DELAY); + (void)ezb_bdb_start_top_level_commissioning(arg); + esp_zigbee_lock_release(); } -void esp_zb_app_signal_handler(esp_zb_app_signal_t *signal_struct) +static bool esp_zigbee_app_signal_handler(const ezb_app_signal_t *app_signal) { - 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 = *p_sg_p; - switch (sig_type) { - case ESP_ZB_ZDO_SIGNAL_SKIP_STARTUP: + ezb_app_signal_type_t signal_type = ezb_app_signal_get_type(app_signal); + + switch (signal_type) { + case EZB_ZDO_SIGNAL_SKIP_STARTUP: ESP_LOGI(TAG, "Initialize Zigbee stack"); - esp_zb_bdb_start_top_level_commissioning(ESP_ZB_BDB_MODE_INITIALIZATION); + ezb_bdb_start_top_level_commissioning(EZB_BDB_MODE_INITIALIZATION); break; - case ESP_ZB_BDB_SIGNAL_DEVICE_FIRST_START: - case ESP_ZB_BDB_SIGNAL_DEVICE_REBOOT: - if (err_status == ESP_OK) { + case EZB_BDB_SIGNAL_DEVICE_FIRST_START: + case EZB_BDB_SIGNAL_DEVICE_REBOOT: { + ezb_bdb_comm_status_t status = *((ezb_bdb_comm_status_t *)ezb_app_signal_get_params(app_signal)); + if (status == EZB_BDB_STATUS_SUCCESS) { ESP_LOGI(TAG, "Deferred driver initialization %s", deferred_driver_init() ? "failed" : "successful"); - ESP_LOGI(TAG, "Device started up in %s factory-reset mode", esp_zb_bdb_is_factory_new() ? "" : "non"); - if (esp_zb_bdb_is_factory_new()) { - ESP_LOGI(TAG, "Start network steering"); - esp_zb_bdb_start_top_level_commissioning(ESP_ZB_BDB_MODE_NETWORK_STEERING); + ESP_LOGI(TAG, "Device started up in%s factory-reset mode", ezb_bdb_is_factory_new() ? "" : " non"); + if (ezb_bdb_is_factory_new()) { + ezb_bdb_start_top_level_commissioning(EZB_BDB_MODE_NETWORK_FORMATION); } else { - ESP_LOGI(TAG, "Device rebooted"); + ezb_bdb_open_network(180); + ESP_LOGI(TAG, "Device reboot"); } } else { - /* commissioning failed */ - ESP_LOGW(TAG, "Failed to initialize Zigbee stack (status: %s)", esp_err_to_name(err_status)); + ESP_LOGW(TAG, "%s failed with status(0x%02x), please retry", ezb_app_signal_to_string(signal_type), status); + alarm_timer_schedule(esp_zigbee_alarm_bdb_commissioning, EZB_BDB_MODE_INITIALIZATION, 1000); } - break; - case ESP_ZB_BDB_SIGNAL_STEERING: - if (err_status == ESP_OK) { - esp_zb_ieee_addr_t extended_pan_id; - esp_zb_get_extended_pan_id(extended_pan_id); - ESP_LOGI(TAG, "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()); + } break; + case EZB_BDB_SIGNAL_FORMATION: { + ezb_bdb_comm_status_t status = *((ezb_bdb_comm_status_t *)ezb_app_signal_get_params(app_signal)); + if (status == EZB_BDB_STATUS_SUCCESS) { + ezb_extpanid_t extended_pan_id; + ezb_nwk_get_extended_panid(&extended_pan_id); + ESP_LOGI(TAG, "Formed network successfully: PAN ID(0x%04hx, EXT: 0x%llx), Channel(%d), Short Address(0x%04hx)", + ezb_nwk_get_panid(), extended_pan_id.u64, ezb_nwk_get_current_channel(), ezb_nwk_get_short_address()); + ezb_bdb_start_top_level_commissioning(EZB_BDB_MODE_NETWORK_STEERING); } else { - ESP_LOGI(TAG, "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); + ESP_LOGW(TAG, "Failed to form network with status(0x%02x)", status); + alarm_timer_schedule(esp_zigbee_alarm_bdb_commissioning, EZB_BDB_MODE_NETWORK_FORMATION, 1000); } - break; + } break; + case EZB_BDB_SIGNAL_STEERING: { + ezb_bdb_comm_status_t status = *((ezb_bdb_comm_status_t *)ezb_app_signal_get_params(app_signal)); + if (status == EZB_BDB_STATUS_SUCCESS) { + ESP_LOGI(TAG, "Network steering completed"); + } else { + ESP_LOGW(TAG, "Failed to steering network with status(0x%02x)", status); + alarm_timer_schedule(esp_zigbee_alarm_bdb_commissioning, EZB_BDB_MODE_NETWORK_STEERING, 1000); + } + } break; + case EZB_ZDO_SIGNAL_DEVICE_ANNCE: { + const ezb_zdo_signal_device_annce_params_t *dev_annce_params = ezb_app_signal_get_params(app_signal); + ESP_LOGI(TAG, "New device commissioned or rejoined(short: 0x%04hx)", dev_annce_params->short_addr); + } break; + case EZB_NWK_SIGNAL_PERMIT_JOIN_STATUS: { + uint8_t duration = *(uint8_t *)ezb_app_signal_get_params(app_signal); + if (duration) { + ESP_LOGI(TAG, "Network(0x%04hx) is open for %d seconds", ezb_nwk_get_panid(), duration); + } else { + ESP_LOGW(TAG, "Network(0x%04hx) closed, devices joining not allowed.", ezb_nwk_get_panid()); + } + } break; default: - ESP_LOGI(TAG, "ZDO signal: %s (0x%x), status: %s", esp_zb_zdo_signal_to_string(sig_type), sig_type, - esp_err_to_name(err_status)); + ESP_LOGI(TAG, "Zigbee APP Signal: %s(type: 0x%02x)", ezb_app_signal_to_string(signal_type), signal_type); break; } + return true; } -static esp_err_t zb_attribute_handler(const esp_zb_zcl_set_attr_value_message_t *message) +static void zcl_core_set_attr_value_handler(ezb_zcl_set_attr_value_message_t *message) { - esp_err_t ret = ESP_OK; - bool light_state = 0; - - ESP_RETURN_ON_FALSE(message, ESP_FAIL, TAG, "Empty message"); - ESP_RETURN_ON_FALSE(message->info.status == ESP_ZB_ZCL_STATUS_SUCCESS, ESP_ERR_INVALID_ARG, TAG, "Received message: error status(%d)", - message->info.status); - ESP_LOGI(TAG, "Received message: endpoint(%d), cluster(0x%x), attribute(0x%x), data size(%d)", message->info.dst_endpoint, message->info.cluster, - message->attribute.id, message->attribute.data.size); - if (message->info.dst_endpoint == HA_ESP_LIGHT_ENDPOINT) { - if (message->info.cluster == ESP_ZB_ZCL_CLUSTER_ID_ON_OFF) { - if (message->attribute.id == ESP_ZB_ZCL_ATTR_ON_OFF_ON_OFF_ID && message->attribute.data.type == ESP_ZB_ZCL_ATTR_TYPE_BOOL) { - light_state = message->attribute.data.value ? *(bool *)message->attribute.data.value : light_state; - ESP_LOGI(TAG, "Light sets to %s", light_state ? "On" : "Off"); - light_driver_set_power(light_state); - } - } + ESP_RETURN_ON_FALSE(message, , TAG, "message is empty"); + ESP_LOGI(TAG, "ZCL SetAttributeValue message for endpoint(%d) cluster(0x%04x) %s with status(0x%02x)", message->info.dst_ep, + message->info.cluster_id, message->info.cluster_role == EZB_ZCL_CLUSTER_SERVER ? "server" : "client", + message->info.status); + if (message->info.cluster_id == EZB_ZCL_CLUSTER_ID_ON_OFF) { + light_driver_set_power(*(uint8_t *)message->in.attribute.data.value); + ESP_LOGI(TAG, "Set On/Off: %d", *(uint8_t *)message->in.attribute.data.value); + } else { + ESP_LOGW(TAG, "Unsupported cluster ID(0x%04x)", message->info.cluster_id); } - return ret; } -static esp_err_t zb_action_handler(esp_zb_core_action_callback_id_t callback_id, const void *message) +static void esp_zigbee_zcl_core_action_handler(ezb_zcl_core_action_callback_id_t callback_id, void *message) { - esp_err_t ret = ESP_OK; switch (callback_id) { - case ESP_ZB_CORE_SET_ATTR_VALUE_CB_ID: - ret = zb_attribute_handler((esp_zb_zcl_set_attr_value_message_t *)message); + case EZB_ZCL_CORE_SET_ATTR_VALUE_CB_ID: + zcl_core_set_attr_value_handler(message); break; + case EZB_ZCL_CORE_DEFAULT_RSP_CB_ID: { + ezb_zcl_cmd_default_rsp_message_t *default_rsp = (ezb_zcl_cmd_default_rsp_message_t *)message; + ESP_LOGI(TAG, "Received ZCL Default Response with status(0x%02x)", default_rsp->in.status_code); + } break; default: - ESP_LOGW(TAG, "Receive Zigbee action(0x%x) callback", callback_id); + ESP_LOGW(TAG, "ZCL Core Action: ID(0x%04lx)", callback_id); break; } - return ret; } -static void esp_zb_task(void *pvParameters) +esp_err_t esp_zigbee_create_zha_on_off_light_device(void) { - /* initialize Zigbee stack */ - esp_zb_cfg_t zb_nwk_cfg = ESP_ZB_ZED_CONFIG(); - esp_zb_init(&zb_nwk_cfg); - esp_zb_on_off_light_cfg_t light_cfg = ESP_ZB_DEFAULT_ON_OFF_LIGHT_CONFIG(); - esp_zb_ep_list_t *esp_zb_on_off_light_ep = esp_zb_on_off_light_ep_create(HA_ESP_LIGHT_ENDPOINT, &light_cfg); - zcl_basic_manufacturer_info_t info = { - .manufacturer_name = ESP_MANUFACTURER_NAME, - .model_identifier = ESP_MODEL_IDENTIFIER, - }; + ezb_af_device_desc_t dev_desc = ezb_af_create_device_desc(); + ezb_zha_on_off_light_config_t light_cfg = EZB_ZHA_ON_OFF_LIGHT_CONFIG(); + ezb_af_ep_desc_t ep_desc = ezb_zha_create_on_off_light(ESP_ZIGBEE_HA_ON_OFF_LIGHT_EP_ID, &light_cfg); + ezb_zcl_cluster_desc_t basic_desc = {0}; - esp_zcl_utility_add_ep_basic_manufacturer_info(esp_zb_on_off_light_ep, HA_ESP_LIGHT_ENDPOINT, &info); - esp_zb_device_register(esp_zb_on_off_light_ep); - esp_zb_core_action_handler_register(zb_action_handler); - esp_zb_set_primary_network_channel_set(ESP_ZB_PRIMARY_CHANNEL_MASK); - ESP_ERROR_CHECK(esp_zb_start(false)); - esp_zb_stack_main_loop(); + basic_desc = ezb_af_endpoint_get_cluster_desc(ep_desc, EZB_ZCL_CLUSTER_ID_BASIC, EZB_ZCL_CLUSTER_SERVER); + ezb_zcl_basic_cluster_desc_add_attr(basic_desc, EZB_ZCL_ATTR_BASIC_MANUFACTURER_NAME_ID, (void *)ESP_MANUFACTURER_NAME); + ezb_zcl_basic_cluster_desc_add_attr(basic_desc, EZB_ZCL_ATTR_BASIC_MODEL_IDENTIFIER_ID, (void *)ESP_MODEL_IDENTIFIER); + ESP_ERROR_CHECK(ezb_af_device_add_endpoint_desc(dev_desc, ep_desc)); + ESP_ERROR_CHECK(ezb_af_device_desc_register(dev_desc)); + + ezb_zcl_core_action_handler_register(esp_zigbee_zcl_core_action_handler); + + return ESP_OK; +} + +esp_err_t esp_zigbee_setup_commissioning(void) +{ + ezb_aps_secur_enable_distributed_security(false); + ESP_ERROR_CHECK(ezb_bdb_set_primary_channel_set(ESP_ZIGBEE_PRIMARY_CHANNEL_MASK)); + ESP_ERROR_CHECK(ezb_bdb_set_secondary_channel_set(ESP_ZIGBEE_SECONDARY_CHANNEL_MASK)); + ESP_ERROR_CHECK(ezb_app_signal_add_handler(esp_zigbee_app_signal_handler)); + + return ESP_OK; +} + +static void esp_zigbee_stack_main_task(void *pvParameters) +{ + esp_zigbee_config_t config = ESP_ZIGBEE_DEFAULT_CONFIG(); + + ESP_ERROR_CHECK(esp_zigbee_init(&config)); + + ESP_ERROR_CHECK(esp_zigbee_setup_commissioning()); + + ESP_ERROR_CHECK(esp_zigbee_create_zha_on_off_light_device()); + + ESP_ERROR_CHECK(esp_zigbee_start(false)); + + esp_zigbee_launch_mainloop(); + + esp_zigbee_deinit(); + + vTaskDelete(NULL); } void app_main(void) { - esp_zb_platform_config_t config = { - .radio_config = ESP_ZB_DEFAULT_RADIO_CONFIG(), - .host_config = ESP_ZB_DEFAULT_HOST_CONFIG(), - }; ESP_ERROR_CHECK(nvs_flash_init()); - ESP_ERROR_CHECK(esp_zb_platform_config(&config)); - xTaskCreate(esp_zb_task, "Zigbee_main", 4096, NULL, 5, NULL); + ESP_LOGI(TAG, "Start ESP Zigbee Stack"); + xTaskCreate(esp_zigbee_stack_main_task, "Zigbee_main", 4096, NULL, 5, NULL); } diff --git a/examples/zigbee/light_sample/HA_on_off_light/main/esp_zb_light.h b/examples/zigbee/light_sample/HA_on_off_light/main/esp_zb_light.h index ed2da9d632d..f7aeb106c56 100644 --- a/examples/zigbee/light_sample/HA_on_off_light/main/esp_zb_light.h +++ b/examples/zigbee/light_sample/HA_on_off_light/main/esp_zb_light.h @@ -1,7 +1,7 @@ /* - * SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD * - * SPDX-License-Identifier: LicenseRef-Included + * SPDX-License-Identifier: LicenseRef-Included * * Zigbee HA_on_off_light Example * @@ -12,36 +12,41 @@ * CONDITIONS OF ANY KIND, either express or implied. */ -#include "esp_zigbee_core.h" -#include "light_driver.h" +#pragma once -/* Zigbee configuration */ -#define INSTALLCODE_POLICY_ENABLE false /* enable the install code policy for security */ -#define ED_AGING_TIMEOUT ESP_ZB_ED_AGING_TIMEOUT_64MIN /* aging timeout of device */ -#define ED_KEEP_ALIVE 3000 /* 3000 millisecond */ -#define HA_ESP_LIGHT_ENDPOINT 10 /* esp light bulb device endpoint, used to process light controlling commands */ -#define ESP_ZB_PRIMARY_CHANNEL_MASK ESP_ZB_TRANSCEIVER_ALL_CHANNELS_MASK /* Zigbee primary channel mask use in the example */ +#define ESP_ZIGBEE_PRIMARY_CHANNEL_MASK (1U << CONFIG_ZB_EXAMPLE_PRIMARY_CHANNEL) +#define ESP_ZIGBEE_SECONDARY_CHANNEL_MASK CONFIG_ZB_EXAMPLE_SECONDARY_CHANNEL_MASK -/* Basic manufacturer information */ -#define ESP_MANUFACTURER_NAME "\x09""ESPRESSIF" /* Customized manufacturer name */ -#define ESP_MODEL_IDENTIFIER "\x07"CONFIG_IDF_TARGET /* Customized model identifier */ +#define ESP_ZIGBEE_HA_ON_OFF_LIGHT_EP_ID (10) -#define ESP_ZB_ZED_CONFIG() \ - { \ - .esp_zb_role = ESP_ZB_DEVICE_TYPE_ED, \ - .install_code_policy = INSTALLCODE_POLICY_ENABLE, \ - .nwk_cfg.zed_cfg = { \ - .ed_timeout = ED_AGING_TIMEOUT, \ - .keep_alive = ED_KEEP_ALIVE, \ - }, \ +#define ESP_ZIGBEE_STORAGE_PARTITION_NAME "nvs" + +#define ESP_MANUFACTURER_NAME "\x09""ESPRESSIF" +#define ESP_MODEL_IDENTIFIER "\x07"CONFIG_IDF_TARGET + +#define ESP_ZIGBEE_ZC_CONFIG() \ + { \ + .device_type = EZB_NWK_DEVICE_TYPE_COORDINATOR, \ + .install_code_policy = false, \ + .zczr_config = { \ + .max_children = 10, \ + }, \ } -#define ESP_ZB_DEFAULT_RADIO_CONFIG() \ - { \ - .radio_mode = ZB_RADIO_MODE_NATIVE, \ +#if CONFIG_SOC_IEEE802154_SUPPORTED +#define ESP_ZIGBEE_PLATFORM_CONFIG() \ + { \ + .storage_partition_name = ESP_ZIGBEE_STORAGE_PARTITION_NAME, \ + .radio_config = { \ + .radio_mode = ESP_ZIGBEE_RADIO_MODE_NATIVE, \ + }, \ } +#else +#warning "The example is not for IEEE 802.15.4-disabled SoC usage, please refer to esp_zigbee_gateway for RCP configuration" +#endif -#define ESP_ZB_DEFAULT_HOST_CONFIG() \ - { \ - .host_connection_mode = ZB_HOST_CONNECTION_MODE_NONE, \ - } +#define ESP_ZIGBEE_DEFAULT_CONFIG() \ + { \ + .device_config = ESP_ZIGBEE_ZC_CONFIG(), \ + .platform_config = ESP_ZIGBEE_PLATFORM_CONFIG(), \ + }; diff --git a/examples/zigbee/light_sample/HA_on_off_light/main/idf_component.yml b/examples/zigbee/light_sample/HA_on_off_light/main/idf_component.yml index b025d290a53..b85c1d36688 100644 --- a/examples/zigbee/light_sample/HA_on_off_light/main/idf_component.yml +++ b/examples/zigbee/light_sample/HA_on_off_light/main/idf_component.yml @@ -1,10 +1,11 @@ ## IDF Component Manager Manifest File dependencies: - espressif/esp-zboss-lib: "~1.6.0" - espressif/esp-zigbee-lib: "~1.6.0" - espressif/led_strip: "~3.0.0" ## Required IDF version idf: - version: ">=5.0.0" - examples_utils: - path: ${IDF_PATH}/examples/zigbee/zb_common_components/examples_utils + version: ">=5.2" + espressif/esp-zigbee-lib: + version: ">=2.0.0" + example_common: + path: ${IDF_PATH}/examples/zigbee/zb_common_components/example_common + light_driver: + path: ${IDF_PATH}/examples/zigbee/zb_common_components/light_driver diff --git a/examples/zigbee/light_sample/HA_on_off_light/main/light_driver.c b/examples/zigbee/light_sample/HA_on_off_light/main/light_driver.c deleted file mode 100644 index 72ff6b9e372..00000000000 --- a/examples/zigbee/light_sample/HA_on_off_light/main/light_driver.c +++ /dev/null @@ -1,62 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: LicenseRef-Included - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form, except as embedded into a Espressif Systems - * integrated circuit in a product or a software update for such product, - * must reproduce the above copyright notice, this list of conditions and - * the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * 4. Any software provided in binary form under this license must not be reverse - * engineered, decompiled, modified and/or disassembled. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -#include "esp_log.h" -#include "led_strip.h" -#include "light_driver.h" - -static led_strip_handle_t s_led_strip; -static uint8_t s_red = 255, s_green = 255, s_blue = 255; - -void light_driver_set_power(bool power) -{ - ESP_ERROR_CHECK(led_strip_set_pixel(s_led_strip, 0, s_red * power, s_green * power, s_blue * power)); - ESP_ERROR_CHECK(led_strip_refresh(s_led_strip)); -} - -void light_driver_init(bool power) -{ - led_strip_config_t led_strip_conf = { - .max_leds = CONFIG_EXAMPLE_STRIP_LED_NUMBER, - .strip_gpio_num = CONFIG_EXAMPLE_STRIP_LED_GPIO, - }; - led_strip_rmt_config_t rmt_conf = { - .resolution_hz = 10 * 1000 * 1000, // 10MHz - }; - ESP_ERROR_CHECK(led_strip_new_rmt_device(&led_strip_conf, &rmt_conf, &s_led_strip)); - light_driver_set_power(power); -} diff --git a/examples/zigbee/light_sample/HA_on_off_light/main/light_driver.h b/examples/zigbee/light_sample/HA_on_off_light/main/light_driver.h deleted file mode 100644 index b9f734fdde8..00000000000 --- a/examples/zigbee/light_sample/HA_on_off_light/main/light_driver.h +++ /dev/null @@ -1,70 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: LicenseRef-Included - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form, except as embedded into a Espressif Systems - * integrated circuit in a product or a software update for such product, - * must reproduce the above copyright notice, this list of conditions and - * the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * 4. Any software provided in binary form under this license must not be reverse - * engineered, decompiled, modified and/or disassembled. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -#pragma once - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -/* light intensity level */ -#define LIGHT_DEFAULT_ON 1 -#define LIGHT_DEFAULT_OFF 0 - -/* LED strip configuration */ -#define CONFIG_EXAMPLE_STRIP_LED_GPIO 8 -#define CONFIG_EXAMPLE_STRIP_LED_NUMBER 1 - -/** -* @brief Set light power (on/off). -* -* @param power The light power to be set -*/ -void light_driver_set_power(bool power); - -/** -* @brief color light driver init, be invoked where you want to use color light -* -* @param power power on/off -*/ -void light_driver_init(bool power); - -#ifdef __cplusplus -} // extern "C" -#endif diff --git a/examples/zigbee/light_sample/HA_on_off_light/partitions.csv b/examples/zigbee/light_sample/HA_on_off_light/partitions.csv index a56c0be7802..fb012212e45 100644 --- a/examples/zigbee/light_sample/HA_on_off_light/partitions.csv +++ b/examples/zigbee/light_sample/HA_on_off_light/partitions.csv @@ -1,7 +1,7 @@ # Name, Type, SubType, Offset, Size, Flags # Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap -nvs, data, nvs, 0x9000, 0x6000, -phy_init, data, phy, 0xf000, 0x1000, -factory, app, factory, 0x10000, 900K, -zb_storage, data, fat, 0xf1000, 16K, -zb_fct, data, fat, 0xf5000, 1K, +nvs, data, nvs, , 0x6000, +otadata, data, ota, , 0x2000, +phy_init, data, phy, , 0x1000, +factory, app, factory, , 940K, +zb_fct, data, fat, , 1K, diff --git a/examples/zigbee/light_sample/HA_on_off_light/sdkconfig.defaults b/examples/zigbee/light_sample/HA_on_off_light/sdkconfig.defaults index 5ae45b073a7..a79dbf9d326 100644 --- a/examples/zigbee/light_sample/HA_on_off_light/sdkconfig.defaults +++ b/examples/zigbee/light_sample/HA_on_off_light/sdkconfig.defaults @@ -3,15 +3,10 @@ # CONFIG_PARTITION_TABLE_CUSTOM=y CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" -CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" -CONFIG_PARTITION_TABLE_OFFSET=0x8000 -CONFIG_PARTITION_TABLE_MD5=y # end of Partition Table # -# Zboss +# Zigbee # CONFIG_ZB_ENABLED=y -CONFIG_ZB_ZED=y -# end of Zboss -# end of Component config +# end of Zigbee diff --git a/examples/zigbee/light_sample/HA_on_off_switch/CMakeLists.txt b/examples/zigbee/light_sample/HA_on_off_switch/CMakeLists.txt index 7352039602a..d768bb9eb9b 100644 --- a/examples/zigbee/light_sample/HA_on_off_switch/CMakeLists.txt +++ b/examples/zigbee/light_sample/HA_on_off_switch/CMakeLists.txt @@ -2,6 +2,9 @@ # in this exact order for cmake to work correctly cmake_minimum_required(VERSION 3.22) include($ENV{IDF_PATH}/tools/cmake/project.cmake) +list(PREPEND SDKCONFIG_DEFAULTS + "${IDF_PATH}/examples/zigbee/zb_common_components/example_common/sdkconfig.mbedtls_minimal" + "sdkconfig.defaults") # "Trim" the build. Include the minimal set of components, main, and anything it depends on. idf_build_set_property(MINIMAL_BUILD ON) project(light_switch) diff --git a/examples/zigbee/light_sample/HA_on_off_switch/README.md b/examples/zigbee/light_sample/HA_on_off_switch/README.md index 61f4128bf1a..d2464103321 100644 --- a/examples/zigbee/light_sample/HA_on_off_switch/README.md +++ b/examples/zigbee/light_sample/HA_on_off_switch/README.md @@ -1,9 +1,9 @@ -| Supported Targets | ESP32-C5 | ESP32-C6 | ESP32-H2 | -| ----------------- | -------- | -------- | -------- | +| Supported Targets | ESP32-C5 | ESP32-C6 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-S31 | +| ----------------- | -------- | -------- | -------- | --------- | -------- | --------- | # Light Switch Example -This test code shows how to configure Zigbee Coordinator and use it as an HA on/off_switch. +This example demonstrates how to configure a Home Automation On/Off switch on a Zigbee End Device. The ESP Zigbee SDK provides more examples and tools for productization: * [ESP Zigbee SDK Docs](https://docs.espressif.com/projects/esp-zigbee-sdk) @@ -11,64 +11,53 @@ The ESP Zigbee SDK provides more examples and tools for productization: ## Hardware Required -* One development board with ESP32-H2 SoC acting as Zigbee Coordinator (loaded with HA_on_off_switch) -* A USB cable for power supply and programming -* Choose another ESP32-H2 as Zigbee end-device (see [HA_on_off_light](../HA_on_off_light/)) +* One 802.15.4-enabled development board running this example. +* A second board running as a Zigbee Coordinator (see the [HA_on_off_light](../HA_on_off_light/) example). +* A USB cable for power supply and programming. ## Configure the project -Before project configuration and build, make sure to set the correct chip target using `idf.py --preview set-target TARGET` command. +Before project configuration and build, set the correct chip target using `idf.py set-target TARGET`. ## Erase the NVRAM -Before flash it to the board, it is recommended to erase NVRAM if user doesn't want to keep the previous examples or other projects stored info using `idf.py -p PORT erase-flash` +Before flashing the board, erase the NVRAM with `idf.py -p PORT erase-flash` if you do not want to retain +information stored by previous examples or projects. ## Build and Flash -Build the project, flash it to the board, and start the monitor tool to view the serial output by running `idf.py -p PORT flash monitor`. +Build the project, flash it to the board, and start the monitor tool to view the serial output by running +`idf.py -p PORT flash monitor`. (To exit the serial monitor, type ``Ctrl-]``.) -## Example Output +## Application Functions -As you run the example, you will see the following log: +When the program starts, the board attempts to join an available Zigbee network: ``` -I (441) main_task: Started on CPU0 -I (441) main_task: Calling app_main() -I (461) phy: phy_version: 230,2, 9aae6ea, Jan 15 2024, 11:17:12 -I (461) phy: libbtbb version: 944f18e, Jan 15 2024, 11:17:25 -I (471) main_task: Returned from app_main() -I (601) ESP_ZB_ON_OFF_SWITCH: ZDO signal: ZDO Config Ready (0x17), status: ESP_FAIL -I (601) ESP_ZB_ON_OFF_SWITCH: Initialize Zigbee stack -I (611) gpio: GPIO[9]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:2 -I (611) ESP_ZB_ON_OFF_SWITCH: Deferred driver initialization successful -I (621) ESP_ZB_ON_OFF_SWITCH: Device started up in factory-reset mode -I (621) ESP_ZB_ON_OFF_SWITCH: Start network formation -W (781) ESP_ZB_ON_OFF_SWITCH: Network(0x13af) closed, devices joining not allowed. -I (781) ESP_ZB_ON_OFF_SWITCH: Formed network successfully (Extended PAN ID: 74:4d:bd:ff:fe:63:f7:30, PAN ID: 0x13af, Channel:13, Short Address: 0x0000) -I (1391) ESP_ZB_ON_OFF_SWITCH: Network(0x13af) is open for 180 seconds -I (1391) ESP_ZB_ON_OFF_SWITCH: Network steering started -I (9561) ESP_ZB_ON_OFF_SWITCH: ZDO signal: NWK Device Associated (0x12), status: ESP_OK -I (9561) ESP_ZB_ON_OFF_SWITCH: ZDO signal: ZDO Device Update (0x30), status: ESP_OK -I (9601) ESP_ZB_ON_OFF_SWITCH: New device commissioned or rejoined (short: 0x7c16) -I (9671) ESP_ZB_ON_OFF_SWITCH: Found light -I (9671) ESP_ZB_ON_OFF_SWITCH: Try to bind On/Off -I (9681) ESP_ZB_ON_OFF_SWITCH: Bound successfully! -I (9681) ESP_ZB_ON_OFF_SWITCH: The light originating from address(0x7c16) on endpoint(10) -I (9751) ESP_ZB_ON_OFF_SWITCH: ZDO signal: ZDO Device Authorized (0x2f), status: ESP_OK -I (9781) ESP_ZB_ON_OFF_SWITCH: Network(0x13af) is open for 180 seconds -I (16451) ESP_ZB_ON_OFF_SWITCH: Send 'on_off toggle' command -I (17011) ESP_ZB_ON_OFF_SWITCH: Send 'on_off toggle' command -I (17441) ESP_ZB_ON_OFF_SWITCH: Send 'on_off toggle' command -I (17831) ESP_ZB_ON_OFF_SWITCH: Send 'on_off toggle' command +I (427) ON_OFF_SWITCH: Start ESP Zigbee Stack +I (467) ON_OFF_SWITCH: Initialize Zigbee stack +I (467) ON_OFF_SWITCH: Deferred driver initialization successful +I (467) ON_OFF_SWITCH: Device started up in factory-reset mode +I (1727) ON_OFF_SWITCH: Joined network successfully: PAN ID(0x3051, EXT: 0x4831b7fffec183f0), Channel(13), Short Address(0xe5e0) ``` -## Light Control Functions +After joining, the switch finds and binds a Home Automation light: - * By toggling the switch button (BOOT) on this board, the LED on the board loaded with the `HA_on_off_light` example will turn on and off. +``` +I (1727) ON_OFF_SWITCH: Attempt to find HA light device +I (1827) ON_OFF_SWITCH: Attempt to bind HA light device (short address: 0x0000) +I (1847) ON_OFF_SWITCH: Bound HA light device successfully +``` +Pressing the `BOOT` button sends an On/Off Toggle command to the bound device: + +``` +I (24447) ON_OFF_SWITCH: Sent ZCL On/Off Toggle request +I (24517) ON_OFF_SWITCH: Received ZCL Default Response with status(0x00) +``` ## Troubleshooting -For any technical queries, please open an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you soon. +For any technical queries, please open an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. diff --git a/examples/zigbee/light_sample/HA_on_off_switch/main/CMakeLists.txt b/examples/zigbee/light_sample/HA_on_off_switch/main/CMakeLists.txt index 79e119440f2..bd4a51ba995 100644 --- a/examples/zigbee/light_sample/HA_on_off_switch/main/CMakeLists.txt +++ b/examples/zigbee/light_sample/HA_on_off_switch/main/CMakeLists.txt @@ -1,5 +1,3 @@ -idf_component_register( - SRC_DIRS "." - INCLUDE_DIRS "." - PRIV_REQUIRES nvs_flash esp_driver_gpio esp_driver_uart ieee802154 -) +idf_component_register(SRC_DIRS "." + PRIV_INCLUDE_DIRS "." + PRIV_REQUIRES nvs_flash esp_driver_gpio esp_driver_uart ieee802154) diff --git a/examples/zigbee/light_sample/HA_on_off_switch/main/esp_zb_switch.c b/examples/zigbee/light_sample/HA_on_off_switch/main/esp_zb_switch.c index fbf140207c7..67da06758c2 100644 --- a/examples/zigbee/light_sample/HA_on_off_switch/main/esp_zb_switch.c +++ b/examples/zigbee/light_sample/HA_on_off_switch/main/esp_zb_switch.c @@ -1,7 +1,7 @@ /* - * SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD * - * SPDX-License-Identifier: LicenseRef-Included + * SPDX-License-Identifier: LicenseRef-Included * * Zigbee HA_on_off_switch Example * @@ -12,189 +12,272 @@ * CONDITIONS OF ANY KIND, either express or implied. */ -#include "string.h" -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" -#include "esp_err.h" #include "esp_check.h" +#include "esp_err.h" #include "esp_log.h" #include "nvs_flash.h" -#include "ha/esp_zigbee_ha_standard.h" -#include "zcl_utility.h" + +#include "switch_driver.h" +#include "alarm_timer.h" + +#include "esp_zigbee.h" +#include "ezbee/zha.h" + #include "esp_zb_switch.h" -#if defined ZB_ED_ROLE -#error Define ZB_COORDINATOR_ROLE in idf.py menuconfig to compile light switch source code. -#endif -typedef struct light_bulb_device_params_s { - esp_zb_ieee_addr_t ieee_addr; - uint8_t endpoint; - uint16_t short_addr; -} light_bulb_device_params_t; +static const char *TAG = "ON_OFF_SWITCH"; -static switch_func_pair_t button_func_pair[] = { - {GPIO_INPUT_IO_TOGGLE_SWITCH, SWITCH_ONOFF_TOGGLE_CONTROL} -}; - -static const char *TAG = "ESP_ZB_ON_OFF_SWITCH"; - -static void zb_buttons_handler(switch_func_pair_t *button_func_pair) +static void button_event_handler(switch_driver_handle_t handle) { - if (button_func_pair->func == SWITCH_ONOFF_TOGGLE_CONTROL) { - /* implemented light switch toggle functionality */ - esp_zb_zcl_on_off_cmd_t cmd_req; - cmd_req.zcl_basic_cmd.src_endpoint = HA_ONOFF_SWITCH_ENDPOINT; - cmd_req.address_mode = ESP_ZB_APS_ADDR_MODE_DST_ADDR_ENDP_NOT_PRESENT; - cmd_req.on_off_cmd_id = ESP_ZB_ZCL_CMD_ON_OFF_TOGGLE_ID; - esp_zb_lock_acquire(portMAX_DELAY); - esp_zb_zcl_on_off_cmd_req(&cmd_req); - esp_zb_lock_release(); - ESP_EARLY_LOGI(TAG, "Send 'on_off toggle' command"); - } + ESP_RETURN_ON_FALSE(handle != SWITCH_INV_HANDLE, , TAG, "Invalid switch handle"); + + ezb_zcl_on_off_cmd_t cmd_req = { + .cmd_ctrl = + { + .dst_addr.addr_mode = EZB_ADDR_MODE_NONE, + .src_ep = ESP_ZIGBEE_HA_ON_OFF_SWITCH_EP_ID, + }, + }; + esp_zigbee_lock_acquire(portMAX_DELAY); + ezb_zcl_on_off_toggle_cmd_req(&cmd_req); + esp_zigbee_lock_release(); + ESP_EARLY_LOGI(TAG, "Sent ZCL On/Off Toggle request"); } static esp_err_t deferred_driver_init(void) { - ESP_RETURN_ON_FALSE(switch_driver_init(button_func_pair, PAIR_SIZE(button_func_pair), zb_buttons_handler), ESP_FAIL, TAG, - "Failed to initialize switch driver"); + static bool is_inited = false; + + ESP_RETURN_ON_FALSE(!is_inited, ESP_OK, TAG, "Deferred driver already initialized"); + + switch_driver_config_t config = { + .gpio_num = CONFIG_GPIO_BOOT_ON_DEVKIT, + .event_cb = button_event_handler, + }; + switch_driver_handle_t handle = switch_driver_init(&config); + + is_inited = handle == SWITCH_INV_HANDLE ? false : true; + + return is_inited ? ESP_OK : ESP_FAIL; +} + +static void zdo_bind_ha_light_device_result(const ezb_zdp_bind_req_result_t *result, void *user_ctx) +{ + assert(result); + if (result->error == EZB_ERR_NONE) { + if (result->rsp && result->rsp->status == EZB_ZDP_STATUS_SUCCESS) { + ESP_LOGI(TAG, "Bound HA light device successfully"); + } else { + ESP_LOGE(TAG, "Failed to bind HA light device with status (0x%02x)", result->rsp->status); + } + } else { + ESP_LOGE(TAG, "Failed to bind HA light device with error (0x%04x)", result->error); + } +} + +static ezb_err_t zdo_bind_ha_light_device(uint16_t dst_short_addr, uint8_t dst_ep) +{ + ezb_err_t ret = EZB_ERR_FAIL; + ezb_zdo_bind_req_t bind_req = { + .dst_nwk_addr = ezb_nwk_get_short_address(), + .field = + { + .src_ep = ESP_ZIGBEE_HA_ON_OFF_SWITCH_EP_ID, + .cluster_id = EZB_ZCL_CLUSTER_ID_ON_OFF, + .dst_addr_mode = EZB_ADDR_MODE_EXT, + .dst_ep = dst_ep, + }, + .cb = zdo_bind_ha_light_device_result, + .user_ctx = NULL, + }; + ezb_nwk_get_extended_address(&bind_req.field.src_addr); + + ESP_RETURN_ON_ERROR(ezb_address_extended_by_short(dst_short_addr, &bind_req.field.dst_addr.extended_addr), TAG, + "Failed to get extended address for destination device(0x%04hx)", dst_short_addr); + + ret = ezb_zdo_bind_req(&bind_req); + if (ret == EZB_ERR_NONE) { + ESP_LOGI(TAG, "Attempt to bind HA light device (short address: 0x%04hx)", dst_short_addr); + } else { + ESP_LOGE(TAG, "Failed to bind HA light device (short address: 0x%04hx) with error(0x%04x)", dst_short_addr, ret); + } + return ret; +} + +static void zdo_find_ha_light_device_result(const ezb_zdo_match_desc_req_result_t *result, void *user_ctx) +{ + assert(result); + if (result->error == EZB_ERR_NONE) { + if (result->rsp && result->rsp->status == EZB_ZDP_STATUS_SUCCESS && result->rsp->match_length > 0 && + result->rsp->match_list) { + for (size_t i = 0; i < result->rsp->match_length; i++) { + zdo_bind_ha_light_device(result->rsp->nwk_addr_of_interest, result->rsp->match_list[i]); + } + } + } else { + ESP_LOGE(TAG, "Failed to find HA light device in the network with error(0x%04x)", result->error); + } +} + +static ezb_err_t zdo_find_ha_light_device(void) +{ + ezb_err_t ret = EZB_ERR_FAIL; + uint16_t cluster_list[1] = {EZB_ZCL_CLUSTER_ID_ON_OFF}; + + ezb_zdo_match_desc_req_t req = { + .dst_nwk_addr = 0xFFFD, + .field = + { + .nwk_addr_of_interest = 0xFFFD, + .profile_id = EZB_AF_HA_PROFILE_ID, + .num_in_clusters = 1, + .num_out_clusters = 0, + .cluster_list = cluster_list, + }, + .cb = zdo_find_ha_light_device_result, + .user_ctx = NULL, + }; + ret = ezb_zdo_match_desc_req(&req); + if (ret == EZB_ERR_NONE) { + ESP_LOGI(TAG, "Attempt to find HA light device"); + } else { + ESP_LOGE(TAG, "Failed to find HA light device with error(0x%04x)", ret); + } + return ret; +} + +static void esp_zigbee_alarm_bdb_commissioning(alarm_timer_arg_t arg) +{ + esp_zigbee_lock_acquire(portMAX_DELAY); + (void)ezb_bdb_start_top_level_commissioning(arg); + esp_zigbee_lock_release(); +} + +static bool esp_zigbee_app_signal_handler(const ezb_app_signal_t *app_signal) +{ + ezb_app_signal_type_t signal_type = ezb_app_signal_get_type(app_signal); + + switch (signal_type) { + case EZB_ZDO_SIGNAL_SKIP_STARTUP: + ESP_LOGI(TAG, "Initialize Zigbee stack"); + ezb_bdb_start_top_level_commissioning(EZB_BDB_MODE_INITIALIZATION); + break; + case EZB_BDB_SIGNAL_DEVICE_FIRST_START: + case EZB_BDB_SIGNAL_DEVICE_REBOOT: { + ezb_bdb_comm_status_t status = *((ezb_bdb_comm_status_t *)ezb_app_signal_get_params(app_signal)); + if (status == EZB_BDB_STATUS_SUCCESS) { + ESP_LOGI(TAG, "Deferred driver initialization %s", deferred_driver_init() ? "failed" : "successful"); + ESP_LOGI(TAG, "Device started up in%s factory-reset mode", ezb_bdb_is_factory_new() ? "" : " non"); + if (ezb_bdb_is_factory_new()) { + ezb_bdb_start_top_level_commissioning(EZB_BDB_MODE_NETWORK_STEERING); + } else { + ESP_LOGI(TAG, "Device reboot"); + } + } else { + ESP_LOGW(TAG, "The %s failed with status(0x%02x), please retry", ezb_app_signal_to_string(signal_type), status); + alarm_timer_schedule(esp_zigbee_alarm_bdb_commissioning, EZB_BDB_MODE_INITIALIZATION, 1000); + } + } break; + case EZB_BDB_SIGNAL_STEERING: { + ezb_bdb_comm_status_t status = *((ezb_bdb_comm_status_t *)ezb_app_signal_get_params(app_signal)); + if (status == EZB_BDB_STATUS_SUCCESS) { + ezb_extpanid_t extended_pan_id; + ezb_nwk_get_extended_panid(&extended_pan_id); + ESP_LOGI(TAG, "Joined network successfully: PAN ID(0x%04hx, EXT: 0x%llx), Channel(%d), Short Address(0x%04hx)", + ezb_nwk_get_panid(), extended_pan_id.u64, ezb_nwk_get_current_channel(), ezb_nwk_get_short_address()); + zdo_find_ha_light_device(); + } else { + ESP_LOGW(TAG, "Failed to join network with status(0x%02x)", status); + alarm_timer_schedule(esp_zigbee_alarm_bdb_commissioning, EZB_BDB_MODE_NETWORK_STEERING, 1000); + } + } break; + case EZB_ZDO_SIGNAL_LEAVE: { + const ezb_zdo_signal_leave_params_t *leave_params = ezb_app_signal_get_params(app_signal); + ESP_LOGI(TAG, "Left network successfully with type(0x%02x)", leave_params->leave_type); + } break; + case EZB_NWK_SIGNAL_PERMIT_JOIN_STATUS: { + uint8_t duration = *(uint8_t *)ezb_app_signal_get_params(app_signal); + if (duration) { + ESP_LOGI(TAG, "Network(0x%04hx) is open for %d seconds", ezb_nwk_get_panid(), duration); + } else { + ESP_LOGW(TAG, "Network(0x%04hx) closed, devices joining not allowed.", ezb_nwk_get_panid()); + } + } break; + case EZB_ZDO_SIGNAL_LEAVE_INDICATION: { + const ezb_zdo_signal_leave_indication_params_t *leave_ind_params = ezb_app_signal_get_params(app_signal); + ESP_LOGI(TAG, "Zigbee Node(0x%04hx) is leaving network", leave_ind_params->short_addr); + } break; + default: + ESP_LOGI(TAG, "Zigbee APP Signal: %s(type: 0x%02x)", ezb_app_signal_to_string(signal_type), signal_type); + break; + } + return true; +} + +static void esp_zigbee_zcl_core_action_handler(ezb_zcl_core_action_callback_id_t callback_id, void *message) +{ + switch (callback_id) { + case EZB_ZCL_CORE_DEFAULT_RSP_CB_ID: { + ezb_zcl_cmd_default_rsp_message_t *default_rsp = (ezb_zcl_cmd_default_rsp_message_t *)message; + ESP_LOGI(TAG, "Received ZCL Default Response with status(0x%02x)", default_rsp->in.status_code); + } break; + default: + ESP_LOGW(TAG, "ZCL Core Action: ID(0x%04lx)", callback_id); + break; + } +} + +esp_err_t esp_zigbee_create_zha_on_off_switch_device(void) +{ + ezb_af_device_desc_t dev_desc = ezb_af_create_device_desc(); + ezb_zha_on_off_switch_config_t switch_cfg = EZB_ZHA_ON_OFF_SWITCH_CONFIG(); + ezb_af_ep_desc_t ep_desc = ezb_zha_create_on_off_switch(ESP_ZIGBEE_HA_ON_OFF_SWITCH_EP_ID, &switch_cfg); + ezb_zcl_cluster_desc_t basic_desc = {0}; + + basic_desc = ezb_af_endpoint_get_cluster_desc(ep_desc, EZB_ZCL_CLUSTER_ID_BASIC, EZB_ZCL_CLUSTER_SERVER); + ezb_zcl_basic_cluster_desc_add_attr(basic_desc, EZB_ZCL_ATTR_BASIC_MANUFACTURER_NAME_ID, (void *)ESP_MANUFACTURER_NAME); + ezb_zcl_basic_cluster_desc_add_attr(basic_desc, EZB_ZCL_ATTR_BASIC_MODEL_IDENTIFIER_ID, (void *)ESP_MODEL_IDENTIFIER); + + ESP_ERROR_CHECK(ezb_af_device_add_endpoint_desc(dev_desc, ep_desc)); + ESP_ERROR_CHECK(ezb_af_device_desc_register(dev_desc)); + + ezb_zcl_core_action_handler_register(esp_zigbee_zcl_core_action_handler); + return ESP_OK; } -static void bdb_start_top_level_commissioning_cb(uint8_t mode_mask) +esp_err_t esp_zigbee_setup_commissioning(void) { - ESP_RETURN_ON_FALSE(esp_zb_bdb_start_top_level_commissioning(mode_mask) == ESP_OK, , TAG, "Failed to start Zigbee bdb commissioning"); + ezb_aps_secur_enable_distributed_security(false); + ESP_ERROR_CHECK(ezb_bdb_set_primary_channel_set(ESP_ZIGBEE_PRIMARY_CHANNEL_MASK)); + ESP_ERROR_CHECK(ezb_bdb_set_secondary_channel_set(ESP_ZIGBEE_SECONDARY_CHANNEL_MASK)); + ESP_ERROR_CHECK(ezb_app_signal_add_handler(esp_zigbee_app_signal_handler)); + + return ESP_OK; } -static void bind_cb(esp_zb_zdp_status_t zdo_status, void *user_ctx) +static void esp_zigbee_stack_main_task(void *pvParameters) { - if (zdo_status == ESP_ZB_ZDP_STATUS_SUCCESS) { - ESP_LOGI(TAG, "Bound successfully!"); - if (user_ctx) { - light_bulb_device_params_t *light = (light_bulb_device_params_t *)user_ctx; - ESP_LOGI(TAG, "The light originating from address(0x%x) on endpoint(%d)", light->short_addr, light->endpoint); - free(light); - } - } -} + esp_zigbee_config_t config = ESP_ZIGBEE_DEFAULT_CONFIG(); -static void user_find_cb(esp_zb_zdp_status_t zdo_status, uint16_t addr, uint8_t endpoint, void *user_ctx) -{ - if (zdo_status == ESP_ZB_ZDP_STATUS_SUCCESS) { - ESP_LOGI(TAG, "Found light"); - esp_zb_zdo_bind_req_param_t bind_req; - light_bulb_device_params_t *light = (light_bulb_device_params_t *)malloc(sizeof(light_bulb_device_params_t)); - light->endpoint = endpoint; - light->short_addr = addr; - esp_zb_ieee_address_by_short(light->short_addr, light->ieee_addr); - esp_zb_get_long_address(bind_req.src_address); - bind_req.src_endp = HA_ONOFF_SWITCH_ENDPOINT; - bind_req.cluster_id = ESP_ZB_ZCL_CLUSTER_ID_ON_OFF; - bind_req.dst_addr_mode = ESP_ZB_ZDO_BIND_DST_ADDR_MODE_64_BIT_EXTENDED; - memcpy(bind_req.dst_address_u.addr_long, light->ieee_addr, sizeof(esp_zb_ieee_addr_t)); - bind_req.dst_endp = endpoint; - bind_req.req_dst_addr = esp_zb_get_short_address(); - ESP_LOGI(TAG, "Try to bind On/Off"); - esp_zb_zdo_device_bind_req(&bind_req, bind_cb, (void *)light); - } -} + ESP_ERROR_CHECK(esp_zigbee_init(&config)); -void esp_zb_app_signal_handler(esp_zb_app_signal_t *signal_struct) -{ - 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 = *p_sg_p; - esp_zb_zdo_signal_device_annce_params_t *dev_annce_params = NULL; - switch (sig_type) { - case ESP_ZB_ZDO_SIGNAL_SKIP_STARTUP: - ESP_LOGI(TAG, "Initialize Zigbee stack"); - esp_zb_bdb_start_top_level_commissioning(ESP_ZB_BDB_MODE_INITIALIZATION); - break; - case ESP_ZB_BDB_SIGNAL_DEVICE_FIRST_START: - case ESP_ZB_BDB_SIGNAL_DEVICE_REBOOT: - if (err_status == ESP_OK) { - ESP_LOGI(TAG, "Deferred driver initialization %s", deferred_driver_init() ? "failed" : "successful"); - ESP_LOGI(TAG, "Device started up in %s factory-reset mode", esp_zb_bdb_is_factory_new() ? "" : "non"); - if (esp_zb_bdb_is_factory_new()) { - ESP_LOGI(TAG, "Start network formation"); - esp_zb_bdb_start_top_level_commissioning(ESP_ZB_BDB_MODE_NETWORK_FORMATION); - } else { - esp_zb_bdb_open_network(180); - ESP_LOGI(TAG, "Device rebooted"); - } - } else { - ESP_LOGE(TAG, "Failed to initialize Zigbee stack (status: %s)", esp_err_to_name(err_status)); - } - break; - case ESP_ZB_BDB_SIGNAL_FORMATION: - if (err_status == ESP_OK) { - esp_zb_ieee_addr_t extended_pan_id; - esp_zb_get_extended_pan_id(extended_pan_id); - ESP_LOGI(TAG, "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()); - esp_zb_bdb_start_top_level_commissioning(ESP_ZB_BDB_MODE_NETWORK_STEERING); - } else { - ESP_LOGI(TAG, "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: - if (err_status == ESP_OK) { - ESP_LOGI(TAG, "Network steering started"); - } - break; - case ESP_ZB_ZDO_SIGNAL_DEVICE_ANNCE: - dev_annce_params = (esp_zb_zdo_signal_device_annce_params_t *)esp_zb_app_signal_get_params(p_sg_p); - ESP_LOGI(TAG, "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; - esp_zb_zdo_find_on_off_light(&cmd_req, user_find_cb, NULL); - break; - case ESP_ZB_NWK_SIGNAL_PERMIT_JOIN_STATUS: - if (err_status == ESP_OK) { - if (*(uint8_t *)esp_zb_app_signal_get_params(p_sg_p)) { - ESP_LOGI(TAG, "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 { - ESP_LOGW(TAG, "Network(0x%04hx) closed, devices joining not allowed.", esp_zb_get_pan_id()); - } - } - break; - default: - ESP_LOGI(TAG, "ZDO signal: %s (0x%x), status: %s", esp_zb_zdo_signal_to_string(sig_type), sig_type, - esp_err_to_name(err_status)); - break; - } -} + ESP_ERROR_CHECK(esp_zigbee_setup_commissioning()); -static void esp_zb_task(void *pvParameters) -{ - /* initialize Zigbee stack */ - esp_zb_cfg_t zb_nwk_cfg = ESP_ZB_ZC_CONFIG(); - esp_zb_init(&zb_nwk_cfg); - esp_zb_on_off_switch_cfg_t switch_cfg = ESP_ZB_DEFAULT_ON_OFF_SWITCH_CONFIG(); - esp_zb_ep_list_t *esp_zb_on_off_switch_ep = esp_zb_on_off_switch_ep_create(HA_ONOFF_SWITCH_ENDPOINT, &switch_cfg); - zcl_basic_manufacturer_info_t info = { - .manufacturer_name = ESP_MANUFACTURER_NAME, - .model_identifier = ESP_MODEL_IDENTIFIER, - }; + ESP_ERROR_CHECK(esp_zigbee_create_zha_on_off_switch_device()); - esp_zcl_utility_add_ep_basic_manufacturer_info(esp_zb_on_off_switch_ep, HA_ONOFF_SWITCH_ENDPOINT, &info); - esp_zb_device_register(esp_zb_on_off_switch_ep); - esp_zb_set_primary_network_channel_set(ESP_ZB_PRIMARY_CHANNEL_MASK); - ESP_ERROR_CHECK(esp_zb_start(false)); - esp_zb_stack_main_loop(); + ESP_ERROR_CHECK(esp_zigbee_start(false)); + + esp_zigbee_launch_mainloop(); + + esp_zigbee_deinit(); + + vTaskDelete(NULL); } void app_main(void) { - esp_zb_platform_config_t config = { - .radio_config = ESP_ZB_DEFAULT_RADIO_CONFIG(), - .host_config = ESP_ZB_DEFAULT_HOST_CONFIG(), - }; ESP_ERROR_CHECK(nvs_flash_init()); - ESP_ERROR_CHECK(esp_zb_platform_config(&config)); - - xTaskCreate(esp_zb_task, "Zigbee_main", 4096, NULL, 5, NULL); + ESP_LOGI(TAG, "Start ESP Zigbee Stack"); + xTaskCreate(esp_zigbee_stack_main_task, "Zigbee_main", 4096, NULL, 5, NULL); } diff --git a/examples/zigbee/light_sample/HA_on_off_switch/main/esp_zb_switch.h b/examples/zigbee/light_sample/HA_on_off_switch/main/esp_zb_switch.h index 8993eb969e6..2af33f72ce0 100644 --- a/examples/zigbee/light_sample/HA_on_off_switch/main/esp_zb_switch.h +++ b/examples/zigbee/light_sample/HA_on_off_switch/main/esp_zb_switch.h @@ -1,7 +1,7 @@ /* - * SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD * - * SPDX-License-Identifier: LicenseRef-Included + * SPDX-License-Identifier: LicenseRef-Included * * Zigbee HA_on_off_switch Example * @@ -11,34 +11,43 @@ * software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR * CONDITIONS OF ANY KIND, either express or implied. */ -#include "esp_zigbee_core.h" -#include "switch_driver.h" -/* Zigbee configuration */ -#define MAX_CHILDREN 10 /* the max amount of connected devices */ -#define INSTALLCODE_POLICY_ENABLE false /* enable the install code policy for security */ -#define HA_ONOFF_SWITCH_ENDPOINT 1 /* esp light switch device endpoint */ -#define ESP_ZB_PRIMARY_CHANNEL_MASK (1l << 13) /* Zigbee primary channel mask use in the example */ +#pragma once -/* Basic manufacturer information */ -#define ESP_MANUFACTURER_NAME "\x09""ESPRESSIF" /* Customized manufacturer name */ -#define ESP_MODEL_IDENTIFIER "\x07"CONFIG_IDF_TARGET /* Customized model identifier */ +#define ESP_ZIGBEE_PRIMARY_CHANNEL_MASK (1U << CONFIG_ZB_EXAMPLE_PRIMARY_CHANNEL) +#define ESP_ZIGBEE_SECONDARY_CHANNEL_MASK CONFIG_ZB_EXAMPLE_SECONDARY_CHANNEL_MASK -#define ESP_ZB_ZC_CONFIG() \ - { \ - .esp_zb_role = ESP_ZB_DEVICE_TYPE_COORDINATOR, \ - .install_code_policy = INSTALLCODE_POLICY_ENABLE, \ - .nwk_cfg.zczr_cfg = { \ - .max_children = MAX_CHILDREN, \ - }, \ +#define ESP_ZIGBEE_HA_ON_OFF_SWITCH_EP_ID (1) + +#define ESP_ZIGBEE_STORAGE_PARTITION_NAME "nvs" + +#define ESP_MANUFACTURER_NAME "\x09""ESPRESSIF" +#define ESP_MODEL_IDENTIFIER "\x07" CONFIG_IDF_TARGET + +#define ESP_ZIGBEE_ZED_CONFIG() \ + { \ + .device_type = EZB_NWK_DEVICE_TYPE_END_DEVICE, \ + .install_code_policy = false, \ + .zed_config = { \ + .ed_timeout = EZB_NWK_ED_TIMEOUT_64MIN, \ + .keep_alive = 4000, \ + }, \ } -#define ESP_ZB_DEFAULT_RADIO_CONFIG() \ - { \ - .radio_mode = ZB_RADIO_MODE_NATIVE, \ +#if CONFIG_SOC_IEEE802154_SUPPORTED +#define ESP_ZIGBEE_PLATFORM_CONFIG() \ + { \ + .storage_partition_name = ESP_ZIGBEE_STORAGE_PARTITION_NAME, \ + .radio_config = { \ + .radio_mode = ESP_ZIGBEE_RADIO_MODE_NATIVE, \ + }, \ } +#else +#warning "The example is not for IEEE 802.15.4-disabled SoC usage, please refer to esp_zigbee_gateway for RCP configuration" +#endif -#define ESP_ZB_DEFAULT_HOST_CONFIG() \ - { \ - .host_connection_mode = ZB_HOST_CONNECTION_MODE_NONE, \ - } +#define ESP_ZIGBEE_DEFAULT_CONFIG() \ + { \ + .device_config = ESP_ZIGBEE_ZED_CONFIG(), \ + .platform_config = ESP_ZIGBEE_PLATFORM_CONFIG(), \ + }; diff --git a/examples/zigbee/light_sample/HA_on_off_switch/main/idf_component.yml b/examples/zigbee/light_sample/HA_on_off_switch/main/idf_component.yml index bfb3027106a..119743043fb 100644 --- a/examples/zigbee/light_sample/HA_on_off_switch/main/idf_component.yml +++ b/examples/zigbee/light_sample/HA_on_off_switch/main/idf_component.yml @@ -1,9 +1,11 @@ ## IDF Component Manager Manifest File dependencies: - espressif/esp-zboss-lib: "~1.6.0" - espressif/esp-zigbee-lib: "~1.6.0" ## Required IDF version idf: - version: ">=5.0.0" - examples_utils: - path: ${IDF_PATH}/examples/zigbee/zb_common_components/examples_utils + version: ">=5.2" + espressif/esp-zigbee-lib: + version: ">=2.0.0" + example_common: + path: ${IDF_PATH}/examples/zigbee/zb_common_components/example_common + switch_driver: + path: ${IDF_PATH}/examples/zigbee/zb_common_components/switch_driver diff --git a/examples/zigbee/light_sample/HA_on_off_switch/main/switch_driver.c b/examples/zigbee/light_sample/HA_on_off_switch/main/switch_driver.c deleted file mode 100644 index 198459efa8f..00000000000 --- a/examples/zigbee/light_sample/HA_on_off_switch/main/switch_driver.c +++ /dev/null @@ -1,178 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: LicenseRef-Included - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form, except as embedded into a Espressif Systems - * integrated circuit in a product or a software update for such product, - * must reproduce the above copyright notice, this list of conditions and - * the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * 4. Any software provided in binary form under this license must not be reverse - * engineered, decompiled, modified and/or disassembled. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -#include "esp_log.h" -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" -#include "freertos/queue.h" -#include "switch_driver.h" - -/** - * @brief: - * This example code shows how to configure light switch with attribute as well as button switch handler. - * - * @note: - Currently only support toggle switch functionality available - * - * @note: - * For other possible switch functions (on/off,level up/down,step up/down). User need to implement and create them by themselves - */ - -static QueueHandle_t gpio_evt_queue = NULL; -/* button function pair, should be defined in switch example source file */ -static switch_func_pair_t *switch_func_pair; -/* call back function pointer */ -static esp_switch_callback_t func_ptr; -/* which button is pressed */ -static uint8_t switch_num; -static const char *TAG = "ESP_ZB_SWITCH"; - -static void IRAM_ATTR gpio_isr_handler(void *arg) -{ - xQueueSendFromISR(gpio_evt_queue, (switch_func_pair_t *)arg, NULL); -} - -/** - * @brief Enable GPIO (switches refer to) isr - * - * @param enabled enable isr if true. - */ -static void switch_driver_gpios_intr_enabled(bool enabled) -{ - for (int i = 0; i < switch_num; ++i) { - if (enabled) { - gpio_intr_enable((switch_func_pair + i)->pin); - } else { - gpio_intr_disable((switch_func_pair + i)->pin); - } - } -} - -/** - * @brief Tasks for checking the button event and debounce the switch state - * - * @param arg Unused value. - */ -static void switch_driver_button_detected(void *arg) -{ - gpio_num_t io_num = GPIO_NUM_NC; - switch_func_pair_t button_func_pair; - static switch_state_t switch_state = SWITCH_IDLE; - bool evt_flag = false; - - for (;;) { - /* check if there is any queue received, if yes read out the button_func_pair */ - if (xQueueReceive(gpio_evt_queue, &button_func_pair, portMAX_DELAY)) { - io_num = button_func_pair.pin; - switch_driver_gpios_intr_enabled(false); - evt_flag = true; - } - while (evt_flag) { - bool value = gpio_get_level(io_num); - switch (switch_state) { - case SWITCH_IDLE: - switch_state = (value == GPIO_INPUT_LEVEL_ON) ? SWITCH_PRESS_DETECTED : SWITCH_IDLE; - break; - case SWITCH_PRESS_DETECTED: - switch_state = (value == GPIO_INPUT_LEVEL_ON) ? SWITCH_PRESS_DETECTED : SWITCH_RELEASE_DETECTED; - break; - case SWITCH_RELEASE_DETECTED: - switch_state = SWITCH_IDLE; - /* callback to button_handler */ - (*func_ptr)(&button_func_pair); - break; - default: - break; - } - if (switch_state == SWITCH_IDLE) { - switch_driver_gpios_intr_enabled(true); - evt_flag = false; - break; - } - vTaskDelay(10 / portTICK_PERIOD_MS); - } - } -} - -/** - * @brief init GPIO configuration as well as isr - * - * @param button_func_pair pointer of the button pair. - * @param button_num number of button pair. - */ -static bool switch_driver_gpio_init(switch_func_pair_t *button_func_pair, uint8_t button_num) -{ - gpio_config_t io_conf = {}; - switch_func_pair = button_func_pair; - switch_num = button_num; - uint64_t pin_bit_mask = 0; - - /* set up button func pair pin mask */ - for (int i = 0; i < button_num; ++i) { - pin_bit_mask |= (1ULL << (button_func_pair + i)->pin); - } - /* interrupt of falling edge */ - io_conf.intr_type = GPIO_INTR_NEGEDGE; - io_conf.pin_bit_mask = pin_bit_mask; - io_conf.mode = GPIO_MODE_INPUT; - io_conf.pull_up_en = GPIO_PULLUP_ENABLE; - /* configure GPIO with the given settings */ - gpio_config(&io_conf); - /* create a queue to handle gpio event from isr */ - gpio_evt_queue = xQueueCreate(10, sizeof(switch_func_pair_t)); - if ( gpio_evt_queue == 0) { - ESP_LOGE(TAG, "Queue was not created and must not be used"); - return false; - } - /* start gpio task */ - xTaskCreate(switch_driver_button_detected, "button_detected", 4096, NULL, 10, NULL); - /* install gpio isr service */ - gpio_install_isr_service(ESP_INTR_FLAG_DEFAULT); - for (int i = 0; i < button_num; ++i) { - gpio_isr_handler_add((button_func_pair + i)->pin, gpio_isr_handler, (void *) (button_func_pair + i)); - } - return true; -} - -bool switch_driver_init(switch_func_pair_t *button_func_pair, uint8_t button_num, esp_switch_callback_t cb) -{ - if (!switch_driver_gpio_init(button_func_pair, button_num)) { - return false; - } - func_ptr = cb; - return true; -} diff --git a/examples/zigbee/light_sample/HA_on_off_switch/main/switch_driver.h b/examples/zigbee/light_sample/HA_on_off_switch/main/switch_driver.h deleted file mode 100644 index 10d332cd6e1..00000000000 --- a/examples/zigbee/light_sample/HA_on_off_switch/main/switch_driver.h +++ /dev/null @@ -1,94 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: LicenseRef-Included - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form, except as embedded into a Espressif Systems - * integrated circuit in a product or a software update for such product, - * must reproduce the above copyright notice, this list of conditions and - * the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * 4. Any software provided in binary form under this license must not be reverse - * engineered, decompiled, modified and/or disassembled. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -#pragma once - -#include "driver/gpio.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/* user should configure which I/O port as toggle switch input, default is GPIO9 */ -#define GPIO_INPUT_IO_TOGGLE_SWITCH GPIO_NUM_9 - -/* config button level depends on the pull up/down setting - push button level is on level = 1 when pull-down enable - push button level is on level = 0 when pull-up enable -*/ -#define GPIO_INPUT_LEVEL_ON 0 - -#define ESP_INTR_FLAG_DEFAULT 0 - -#define PAIR_SIZE(TYPE_STR_PAIR) (sizeof(TYPE_STR_PAIR) / sizeof(TYPE_STR_PAIR[0])) - -typedef enum { - SWITCH_IDLE, - SWITCH_PRESS_ARMED, - SWITCH_PRESS_DETECTED, - SWITCH_PRESSED, - SWITCH_RELEASE_DETECTED, -} switch_state_t; - -typedef enum { - SWITCH_ON_CONTROL, - SWITCH_OFF_CONTROL, - SWITCH_ONOFF_TOGGLE_CONTROL, - SWITCH_LEVEL_UP_CONTROL, - SWITCH_LEVEL_DOWN_CONTROL, - SWITCH_LEVEL_CYCLE_CONTROL, - SWITCH_COLOR_CONTROL, -} switch_func_t; - -typedef struct { - uint32_t pin; - switch_func_t func; -} switch_func_pair_t; - -typedef void (*esp_switch_callback_t)(switch_func_pair_t *param); - -/** - * @brief init function for switch and callback setup - * - * @param button_func_pair pointer of the button pair. - * @param button_num number of button pair. - * @param cb callback pointer. - */ -bool switch_driver_init(switch_func_pair_t *button_func_pair, uint8_t button_num, esp_switch_callback_t cb); - -#ifdef __cplusplus -} // extern "C" -#endif diff --git a/examples/zigbee/light_sample/HA_on_off_switch/partitions.csv b/examples/zigbee/light_sample/HA_on_off_switch/partitions.csv index a56c0be7802..3050a3d6053 100644 --- a/examples/zigbee/light_sample/HA_on_off_switch/partitions.csv +++ b/examples/zigbee/light_sample/HA_on_off_switch/partitions.csv @@ -1,7 +1,7 @@ # Name, Type, SubType, Offset, Size, Flags # Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap -nvs, data, nvs, 0x9000, 0x6000, -phy_init, data, phy, 0xf000, 0x1000, -factory, app, factory, 0x10000, 900K, -zb_storage, data, fat, 0xf1000, 16K, -zb_fct, data, fat, 0xf5000, 1K, +nvs, data, nvs, , 0x6000, +otadata, data, ota, , 0x2000, +phy_init, data, phy, , 0x1000, +factory, app, factory, , 940K, +zb_fct, data, fat, , 1K, diff --git a/examples/zigbee/light_sample/HA_on_off_switch/sdkconfig.defaults b/examples/zigbee/light_sample/HA_on_off_switch/sdkconfig.defaults index a5e62b5807f..fd13a569304 100644 --- a/examples/zigbee/light_sample/HA_on_off_switch/sdkconfig.defaults +++ b/examples/zigbee/light_sample/HA_on_off_switch/sdkconfig.defaults @@ -3,15 +3,11 @@ # CONFIG_PARTITION_TABLE_CUSTOM=y CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" -CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" -CONFIG_PARTITION_TABLE_OFFSET=0x8000 -CONFIG_PARTITION_TABLE_MD5=y # end of Partition Table # -# Zboss +# Zigbee # CONFIG_ZB_ENABLED=y -CONFIG_ZB_ZCZR=y -# end of Zboss -# end of Component config +CONFIG_ZB_ZED=y +# end of Zigbee diff --git a/examples/zigbee/light_sample/README.md b/examples/zigbee/light_sample/README.md index 2ef76b5b03a..a523ab1ea5c 100644 --- a/examples/zigbee/light_sample/README.md +++ b/examples/zigbee/light_sample/README.md @@ -1,12 +1,11 @@ # Light Switch and Bulb Control Example -(See the `README.md` file in the upper level 'examples' directory for more information about examples.) +See the [README.md](../README.md) file in the upper level [examples](../) directory for more information about examples. ## Overview -The folder contains examples demonstrating Zigbee Coordinator and End-Device roles +The folder contains two Zigbee examples demonstrating Coordinator and End Device roles. -* [HA_on_off_light](HA_on_off_light) is a standard HA on-off light bulb example demonstrating Zigbee End-device. It provides a simple on/off condition for a Zigbee light. It runs on an 802.15.4 SoC like ESP32-H2. For more details see the example readme file. - -* [HA_on_off_switch](HA_on_off_switch) is a standard HA on-off switch example demonstrating Zigbee Coordinator role. It provides an on/off toggle to control a Zigbee HA on off light. It runs on an 802.15.4 SoC like ESP32-H2. For more details to see the example readme file. +* [HA_on_off_light](HA_on_off_light) is a standard Zigbee coordinator acting as Home Automation On/Off light bulb. It runs on an 802.15.4 SoC like ESP32-H2. For more details see [README](HA_on_off_light/README.md). +* [HA_on_off_switch](HA_on_off_switch) is a standard Zigbee End-device acting as Home Automation On/Off switch. It runs on an 802.15.4 SoC like ESP32-H2. For more details to see [README](HA_on_off_switch/README.md). diff --git a/examples/zigbee/light_sample/pytest_esp_zigbee.py b/examples/zigbee/light_sample/pytest_esp_zigbee.py index 868fd0d2fc5..d193778c86a 100644 --- a/examples/zigbee/light_sample/pytest_esp_zigbee.py +++ b/examples/zigbee/light_sample/pytest_esp_zigbee.py @@ -1,9 +1,8 @@ -# SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD +# SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 # !/usr/bin/env python3 import pathlib import time -from typing import Tuple import pytest from pytest_embedded import Dut @@ -24,24 +23,25 @@ pytest_build_dir = CURRENT_DIR_LIGHT + '|' + CURRENT_DIR_SWITCH ) # config Zigbee network @idf_parametrize('target', ['esp32h2'], indirect=['target']) -def test_config_zigbee_network(dut: Tuple[Dut, Dut]) -> None: +def test_config_zigbee_network(dut: tuple[Dut, Dut]) -> None: light = dut[0] switch = dut[1] time.sleep(3) - switch.expect('ESP_ZB_ON_OFF_SWITCH: Formed network successfully', timeout=30) - # get the switch extpanid - switch_node_expanid = switch.expect(r'Extended PAN ID: (([a-z0-9]{2}:?){8})', timeout=3)[1].decode() - switch_node_expanid = switch_node_expanid.replace(':', '') - # get the switch panid - switch_node_panid = switch.expect(r'PAN ID: 0x([a-z0-9]+:?)', timeout=2)[1].decode() - # new device commissioned successfully - switch.expect(r'New device commissioned or rejoined \(short: 0x([a-z0-9]+)[^a-z0-9]', timeout=30)[1].decode() - # get the light node extpanid - light.expect('ESP_ZB_ON_OFF_LIGHT: Joined network successfully', timeout=20) - light_node_expanid = light.expect(r'Extended PAN ID: (([a-z0-9]{2}:?){8})', timeout=3)[1].decode() - light_node_expanid = light_node_expanid.replace(':', '') - # get the light panid - light_node_panid = light.expect(r'PAN ID: 0x([a-z0-9]+:?)', timeout=2)[1].decode() - # make sure the light node join the network that switch node formed (same expanid) - if (light_node_expanid != switch_node_expanid) or (light_node_panid != switch_node_panid): - assert False + light_network = light.expect( + r'ON_OFF_LIGHT: Formed network successfully: PAN ID\(0x([a-fA-F0-9]+), EXT: 0x([a-fA-F0-9]+)\)', + timeout=30, + ) + light_panid = light_network[1].decode() + light_extpanid = light_network[2].decode() + + switch_network = switch.expect( + r'ON_OFF_SWITCH: Joined network successfully: PAN ID\(0x([a-fA-F0-9]+), EXT: 0x([a-fA-F0-9]+)\)', + timeout=30, + ) + switch_panid = switch_network[1].decode() + switch_extpanid = switch_network[2].decode() + + light.expect(r'New device commissioned or rejoined ?\(short: 0x([a-fA-F0-9]+)\)', timeout=30) + + assert switch_panid == light_panid + assert switch_extpanid == light_extpanid diff --git a/examples/zigbee/zb_common_components/example_common/CMakeLists.txt b/examples/zigbee/zb_common_components/example_common/CMakeLists.txt new file mode 100644 index 00000000000..f07ce4cf482 --- /dev/null +++ b/examples/zigbee/zb_common_components/example_common/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register(SRC_DIRS "src" + INCLUDE_DIRS "include" + REQUIRES esp_timer +) diff --git a/examples/zigbee/zb_common_components/example_common/Kconfig b/examples/zigbee/zb_common_components/example_common/Kconfig new file mode 100644 index 00000000000..16e86dcfff9 --- /dev/null +++ b/examples/zigbee/zb_common_components/example_common/Kconfig @@ -0,0 +1,12 @@ +menu "Zigbee Example Configuration" + + config ZB_EXAMPLE_PRIMARY_CHANNEL + int "Zigbee primary channel" + range 11 26 + default 13 + + config ZB_EXAMPLE_SECONDARY_CHANNEL_MASK + hex "Zigbee secondary channel mask" + default 0x07FFF800 + +endmenu diff --git a/examples/zigbee/zb_common_components/example_common/include/alarm_timer.h b/examples/zigbee/zb_common_components/example_common/include/alarm_timer.h new file mode 100644 index 00000000000..220618167ad --- /dev/null +++ b/examples/zigbee/zb_common_components/example_common/include/alarm_timer.h @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: LicenseRef-Included + * + * Alarm driver + * + * Unless required by applicable law or agreed to in writing, this + * software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. + */ + +#pragma once + +#include +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief The argument type for the alarm callback. + * + */ +typedef uintptr_t alarm_timer_arg_t; + +/** + * @brief The callback for the timer. + * + */ +typedef void (*alarm_timer_callback_t)(alarm_timer_arg_t arg); + +/** + * @brief Schedule an alarm to fire at a given time. + * + * @param cb The callback to execute when the alarm fires. + * @param arg The argument to pass to the callback. + * @param time_ms The time to fire the timer in milliseconds. + * + * @return ESP_OK on success, error code otherwise. + */ +esp_err_t alarm_timer_schedule(alarm_timer_callback_t cb, alarm_timer_arg_t arg, uint32_t time_ms); + +#ifdef __cplusplus +} +#endif diff --git a/examples/zigbee/zb_common_components/example_common/sdkconfig.mbedtls_minimal b/examples/zigbee/zb_common_components/example_common/sdkconfig.mbedtls_minimal new file mode 100644 index 00000000000..9d212c4f5c4 --- /dev/null +++ b/examples/zigbee/zb_common_components/example_common/sdkconfig.mbedtls_minimal @@ -0,0 +1,51 @@ +# This config file optimizes the mbedTLS code size by disabling +# unused features by ESP-Zigbee-SDK. + +# +# mbedTLS +# + +# Enable required +CONFIG_MBEDTLS_AES_C=y +CONFIG_MBEDTLS_CCM_C=y + +# Disable unused +CONFIG_MBEDTLS_GCM_C=n +CONFIG_MBEDTLS_CMAC_C=n +CONFIG_MBEDTLS_SHA1_C=n +CONFIG_MBEDTLS_SHA3_C=n +CONFIG_MBEDTLS_SHA512_C=n +CONFIG_MBEDTLS_ECP_DP_SECP384R1_ENABLED=n +CONFIG_MBEDTLS_ECP_DP_SECP521R1_ENABLED=n +CONFIG_MBEDTLS_ECP_DP_SECP256K1_ENABLED=n +CONFIG_MBEDTLS_ECP_DP_BP256R1_ENABLED=n +CONFIG_MBEDTLS_ECP_DP_BP384R1_ENABLED=n +CONFIG_MBEDTLS_ECP_DP_BP512R1_ENABLED=n +CONFIG_MBEDTLS_KEY_EXCHANGE_RSA=n +CONFIG_MBEDTLS_KEY_EXCHANGE_RSA_PSK=n +CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_RSA=n + +## v5.0 +CONFIG_MBEDTLS_ECP_DP_SECP192R1_ENABLED=n +CONFIG_MBEDTLS_ECP_DP_SECP224R1_ENABLED=n +CONFIG_MBEDTLS_ECP_DP_SECP192K1_ENABLED=n +CONFIG_MBEDTLS_ECP_DP_SECP224K1_ENABLED=n +CONFIG_MBEDTLS_KEY_EXCHANGE_DHE_RSA=n +CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_RSA=n + +## v6.0 +CONFIG_MBEDTLS_AES_ONLY_128_BIT_KEY_LENGTH=y +CONFIG_MBEDTLS_CTR_DRBG_C=y +CONFIG_MBEDTLS_HMAC_DRBG_C=n +CONFIG_MBEDTLS_X509_RSASSA_PSS_SUPPORT=n +CONFIG_MBEDTLS_CIPHER_MODE_CBC=n +CONFIG_MBEDTLS_CIPHER_MODE_CFB=n +CONFIG_MBEDTLS_CIPHER_MODE_CTR=n +CONFIG_MBEDTLS_CIPHER_MODE_OFB=n +CONFIG_MBEDTLS_CIPHER_MODE_XTS=n +# PSA_WANT_ALG_RSA_PSS needs CONFIG_MBEDTLS_RSA_C=y on ESP-IDF master +CONFIG_MBEDTLS_RSA_C=y +CONFIG_MBEDTLS_MD5_C=n +CONFIG_MBEDTLS_SHA384_C=n + +# end of mbedTLS diff --git a/examples/zigbee/zb_common_components/example_common/src/alarm_timer.c b/examples/zigbee/zb_common_components/example_common/src/alarm_timer.c new file mode 100644 index 00000000000..51fb2c629fc --- /dev/null +++ b/examples/zigbee/zb_common_components/example_common/src/alarm_timer.c @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: LicenseRef-Included + * + * Alarm Timer + * + * Unless required by applicable law or agreed to in writing, this + * software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. + */ + +#include + +#include "esp_timer.h" + +#include "alarm_timer.h" + +typedef struct alarm_timer_s { + esp_timer_handle_t timer; + alarm_timer_callback_t cb; + alarm_timer_arg_t arg; +} alarm_timer_t; + +static void alarm_timer_cb(void *arg) +{ + alarm_timer_t *alarm = (alarm_timer_t *)arg; + + esp_timer_delete(alarm->timer); + alarm->cb(alarm->arg); + free(alarm); +} + +esp_err_t alarm_timer_schedule(alarm_timer_callback_t cb, alarm_timer_arg_t arg, uint32_t time_ms) +{ + if (cb == NULL) { + return ESP_ERR_INVALID_ARG; + } + + alarm_timer_t *alarm = malloc(sizeof(alarm_timer_t)); + if (alarm == NULL) { + return ESP_ERR_NO_MEM; + } + + alarm->cb = cb; + alarm->arg = arg; + alarm->timer = NULL; + + esp_timer_create_args_t timer_args = { + .callback = alarm_timer_cb, + .arg = alarm, + }; + + esp_err_t err = esp_timer_create(&timer_args, &alarm->timer); + if (err != ESP_OK) { + free(alarm); + return err; + } + + uint64_t time_us = (uint64_t)time_ms * 1000; + err = esp_timer_start_once(alarm->timer, time_us); + if (err != ESP_OK) { + esp_timer_delete(alarm->timer); + free(alarm); + return err; + } + + return ESP_OK; +} diff --git a/examples/zigbee/zb_common_components/examples_utils/CMakeLists.txt b/examples/zigbee/zb_common_components/examples_utils/CMakeLists.txt deleted file mode 100644 index 7eafd7b4d21..00000000000 --- a/examples/zigbee/zb_common_components/examples_utils/CMakeLists.txt +++ /dev/null @@ -1,3 +0,0 @@ -idf_component_register(SRCS "zcl_utility.c" - INCLUDE_DIRS "include" - PRIV_REQUIRES espressif__esp-zigbee-lib) diff --git a/examples/zigbee/zb_common_components/examples_utils/include/zcl_utility.h b/examples/zigbee/zb_common_components/examples_utils/include/zcl_utility.h deleted file mode 100644 index afd1293a869..00000000000 --- a/examples/zigbee/zb_common_components/examples_utils/include/zcl_utility.h +++ /dev/null @@ -1,50 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: LicenseRef-Included - * - * ZCL utility functions for Zigbee examples - * - * This example code is in the Public Domain (or CC0 licensed, at your option.) - * - * Unless required by applicable law or agreed to in writing, this - * software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. -*/ - -#pragma once - -#include "esp_err.h" -#include "esp_zigbee_core.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/*! Maximum length of ManufacturerName string field */ -#define ESP_ZB_ZCL_CLUSTER_ID_BASIC_MANUFACTURER_NAME_MAX_LEN 32 - -/*! Maximum length of ModelIdentifier string field */ -#define ESP_ZB_ZCL_CLUSTER_ID_BASIC_MODEL_IDENTIFIER_MAX_LEN 32 - -/** optional basic manufacturer information */ -typedef struct zcl_basic_manufacturer_info_s { - char *manufacturer_name; - char *model_identifier; -} zcl_basic_manufacturer_info_t; - -/** - * @brief Adds manufacturer information to the ZCL basic cluster of endpoint - * - * @param ep_list The pointer to the endpoint list with @p endpoint_id - * @param endpoint_id The endpoint identifier indicating where the ZCL basic cluster resides - * @param info The pointer to the basic manufacturer information, @see zcl_basic_manufacturer_info_t - * @return - * - ESP_OK: On success - * - ESP_ERR_INVALID_ARG: Invalid argument - */ -esp_err_t esp_zcl_utility_add_ep_basic_manufacturer_info(esp_zb_ep_list_t *ep_list, uint8_t endpoint_id, zcl_basic_manufacturer_info_t *info); - -#ifdef __cplusplus -} // extern "C" -#endif diff --git a/examples/zigbee/zb_common_components/examples_utils/zcl_utility.c b/examples/zigbee/zb_common_components/examples_utils/zcl_utility.c deleted file mode 100644 index 65c6fdec9d6..00000000000 --- a/examples/zigbee/zb_common_components/examples_utils/zcl_utility.c +++ /dev/null @@ -1,40 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: LicenseRef-Included - * - * ZCL utility functions for Zigbee examples - * - * This example code is in the Public Domain (or CC0 licensed, at your option.) - * - * Unless required by applicable law or agreed to in writing, this - * software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. -*/ - -#include -#include - -#include "esp_check.h" -#include "esp_zigbee_core.h" - -#include "zcl_utility.h" - -static const char *TAG = "ZCL_UTILITY"; - -esp_err_t esp_zcl_utility_add_ep_basic_manufacturer_info(esp_zb_ep_list_t *ep_list, uint8_t endpoint_id, zcl_basic_manufacturer_info_t *info) -{ - esp_err_t ret = ESP_OK; - esp_zb_cluster_list_t *cluster_list = NULL; - esp_zb_attribute_list_t *basic_cluster = NULL; - - cluster_list = esp_zb_ep_list_get_ep(ep_list, endpoint_id); - ESP_RETURN_ON_FALSE(cluster_list, ESP_ERR_INVALID_ARG, TAG, "Failed to find endpoint id: %d in list: %p", endpoint_id, ep_list); - basic_cluster = esp_zb_cluster_list_get_cluster(cluster_list, ESP_ZB_ZCL_CLUSTER_ID_BASIC, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE); - ESP_RETURN_ON_FALSE(basic_cluster, ESP_ERR_INVALID_ARG, TAG, "Failed to find basic cluster in endpoint: %d", endpoint_id); - ESP_RETURN_ON_FALSE((info && info->manufacturer_name), ESP_ERR_INVALID_ARG, TAG, "Invalid manufacturer name"); - ESP_ERROR_CHECK(esp_zb_basic_cluster_add_attr(basic_cluster, ESP_ZB_ZCL_ATTR_BASIC_MANUFACTURER_NAME_ID, info->manufacturer_name)); - ESP_RETURN_ON_FALSE((info && info->model_identifier), ESP_ERR_INVALID_ARG, TAG, "Invalid model identifier"); - ESP_ERROR_CHECK(esp_zb_basic_cluster_add_attr(basic_cluster, ESP_ZB_ZCL_ATTR_BASIC_MODEL_IDENTIFIER_ID, info->model_identifier)); - return ret; -} diff --git a/examples/zigbee/zb_common_components/light_driver/CMakeLists.txt b/examples/zigbee/zb_common_components/light_driver/CMakeLists.txt new file mode 100644 index 00000000000..b865c652ff4 --- /dev/null +++ b/examples/zigbee/zb_common_components/light_driver/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRC_DIRS "src" + INCLUDE_DIRS "include" + REQUIRES led_strip) diff --git a/examples/zigbee/zb_common_components/light_driver/Kconfig b/examples/zigbee/zb_common_components/light_driver/Kconfig new file mode 100644 index 00000000000..f89d061cddb --- /dev/null +++ b/examples/zigbee/zb_common_components/light_driver/Kconfig @@ -0,0 +1,13 @@ +menu "ESP Zigbee DevKit LED Configuration" + + config GPIO_LED_ON_DEVKIT + int "LED GPIO on devkit" + default 8 if IDF_TARGET_ESP32H2 + default 8 if IDF_TARGET_ESP32C6 + default 27 if IDF_TARGET_ESP32C5 + default 8 + + help + The GPIO connected to the LED on the devkits + +endmenu diff --git a/examples/zigbee/zb_common_components/light_driver/idf_component.yml b/examples/zigbee/zb_common_components/light_driver/idf_component.yml new file mode 100644 index 00000000000..82ebf2bc72e --- /dev/null +++ b/examples/zigbee/zb_common_components/light_driver/idf_component.yml @@ -0,0 +1,3 @@ +## IDF Component Manager Manifest File +dependencies: + espressif/led_strip: "~3.0.0" diff --git a/examples/zigbee/zb_common_components/light_driver/include/light_driver.h b/examples/zigbee/zb_common_components/light_driver/include/light_driver.h new file mode 100644 index 00000000000..b3aa17e5f41 --- /dev/null +++ b/examples/zigbee/zb_common_components/light_driver/include/light_driver.h @@ -0,0 +1,151 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: LicenseRef-Included + * + * Zigbee light driver example + * + * This example code is in the Public Domain (or CC0 licensed, at your option.) + * + * Unless required by applicable law or agreed to in writing, this + * software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. + */ + +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* light intensity level */ +#define LIGHT_DEFAULT_ON 1 +#define LIGHT_DEFAULT_OFF 0 + +/* LED strip configuration */ +#define CONFIG_EXAMPLE_STRIP_LED_GPIO CONFIG_GPIO_LED_ON_DEVKIT +#define CONFIG_EXAMPLE_STRIP_LED_NUMBER 1 + +/** Convert Hue,Saturation,V to RGB + * RGB - [0..0xffff] + * hue - [0..0xff] + * Sat - [0..0xff] + * V always = (ZB_UINT16_MAX-1) + */ +#define HSV_to_RGB(h, s, v, r, g, b) \ + { \ + uint8_t i; \ + uint8_t sector = UINT8_MAX / 6; \ + float f, p, q, t; \ + if (s == 0) { /* achromatic (grey)*/ \ + r = g = b = (v); \ + } else { \ + i = h / sector; /* sector 0 to 5 */ \ + f = h % sector; /* factorial part of h*/ \ + p = (float)(v * (1.0 - (float)s / UINT8_MAX)); \ + q = (float)(v * (1.0 - (float)s / UINT8_MAX * f / (float)sector)); \ + t = (float)(v * (1.0 - (float)s / UINT8_MAX * (1 - f / (float)sector))); \ + switch (i) { \ + case 0: \ + r = (v); \ + g = t; \ + b = p; \ + break; \ + case 1: \ + r = q; \ + g = (v); \ + b = p; \ + break; \ + case 2: \ + r = p; \ + g = (v); \ + b = t; \ + break; \ + case 3: \ + r = p; \ + g = q; \ + b = (v); \ + break; \ + case 4: \ + r = t; \ + g = p; \ + b = (v); \ + break; \ + case 5: \ + default: \ + r = (v); \ + g = p; \ + b = q; \ + break; \ + } \ + } \ + } + +#define XYZ_to_RGB(X, Y, Z, r, g, b) \ + { \ + r = (float)(3.240479 * (X)-1.537150 * (Y)-0.498535 * (Z)); \ + g = (float)(-0.969256 * (X) + 1.875992 * (Y) + 0.041556 * (Z)); \ + b = (float)(0.055648 * (X)-0.204043 * (Y) + 1.057311 * (Z)); \ + if (r > 1) { \ + r = 1; \ + } \ + if (g > 1) { \ + g = 1; \ + } \ + if (b > 1) { \ + b = 1; \ + } \ + } + +/** + * @brief Set light power (on/off). + * + * @param power The light power value to be set + */ +void light_driver_set_power(bool power); + +/** + * @brief color light driver init, be invoked where you want to use color light + * + * @param power power on/off + */ +void light_driver_init(bool power); + +/** + * @brief Set light level + * + * @param level The light level value to be set + */ +void light_driver_set_level(uint8_t level); + +/** + * @brief Set light color from RGB + * + * @param red The red color value to be set + * @param green The green color value to be set + * @param blue The blue color value to be set + */ +void light_driver_set_color_RGB(uint8_t red, uint8_t green, uint8_t blue); + +/** + * @brief Set light color from color xy + * + * @param color_current_x The color x value to be set + * @param color_current_y The color y value to be set + */ +void light_driver_set_color_xy(uint16_t color_current_x, uint16_t color_current_y); + +/** + * @brief Set light color from hue saturation + * + * @param hue The hue value to be set + * @param sat The saturation value to be set + */ +void light_driver_set_color_hue_sat(uint8_t hue, uint8_t sat); + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/examples/zigbee/zb_common_components/light_driver/src/light_driver.c b/examples/zigbee/zb_common_components/light_driver/src/light_driver.c new file mode 100644 index 00000000000..4273ace70ab --- /dev/null +++ b/examples/zigbee/zb_common_components/light_driver/src/light_driver.c @@ -0,0 +1,87 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: LicenseRef-Included + * + * Zigbee light driver example + * + * This example code is in the Public Domain (or CC0 licensed, at your option.) + * + * Unless required by applicable law or agreed to in writing, this + * software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. + */ + +#include "led_strip.h" +#include "light_driver.h" + +static led_strip_handle_t s_led_strip; +static uint8_t s_red = 255, s_green = 255, s_blue = 255, s_level = 255; + +void light_driver_set_color_xy(uint16_t color_current_x, uint16_t color_current_y) +{ + float red_f = 0, green_f = 0, blue_f = 0, color_x, color_y; + color_x = (float)color_current_x / 65535; + color_y = (float)color_current_y / 65535; + /* assume color_Y is full light level value 1 (0-1.0) */ + float color_X = color_x / color_y; + float color_Z = (1 - color_x - color_y) / color_y; + /* change from xy to linear RGB NOT sRGB */ + XYZ_to_RGB(color_X, 1, color_Z, red_f, green_f, blue_f); + float ratio = (float)s_level / 255; + s_red = (uint8_t)(red_f * (float)255); + s_green = (uint8_t)(green_f * (float)255); + s_blue = (uint8_t)(blue_f * (float)255); + ESP_ERROR_CHECK(led_strip_set_pixel(s_led_strip, 0, s_red * ratio, s_green * ratio, s_blue * ratio)); + ESP_ERROR_CHECK(led_strip_refresh(s_led_strip)); +} + +void light_driver_set_color_hue_sat(uint8_t hue, uint8_t sat) +{ + float red_f, green_f, blue_f; + HSV_to_RGB(hue, sat, UINT8_MAX, red_f, green_f, blue_f); + float ratio = (float)s_level / 255; + s_red = (uint8_t)red_f; + s_green = (uint8_t)green_f; + s_blue = (uint8_t)blue_f; + ESP_ERROR_CHECK(led_strip_set_pixel(s_led_strip, 0, s_red * ratio, s_green * ratio, s_blue * ratio)); + ESP_ERROR_CHECK(led_strip_refresh(s_led_strip)); +} + +void light_driver_set_color_RGB(uint8_t red, uint8_t green, uint8_t blue) +{ + float ratio = (float)s_level / 255; + s_red = red; + s_green = green; + s_blue = blue; + ESP_ERROR_CHECK(led_strip_set_pixel(s_led_strip, 0, red * ratio, green * ratio, blue * ratio)); + ESP_ERROR_CHECK(led_strip_refresh(s_led_strip)); +} + +void light_driver_set_power(bool power) +{ + ESP_ERROR_CHECK(led_strip_set_pixel(s_led_strip, 0, s_red * power, s_green * power, s_blue * power)); + ESP_ERROR_CHECK(led_strip_refresh(s_led_strip)); +} + +void light_driver_set_level(uint8_t level) +{ + s_level = level; + float ratio = (float)s_level / 255; + ESP_ERROR_CHECK(led_strip_set_pixel(s_led_strip, 0, s_red * ratio, s_green * ratio, s_blue * ratio)); + ESP_ERROR_CHECK(led_strip_refresh(s_led_strip)); +} + +void light_driver_init(bool power) +{ + led_strip_config_t led_strip_conf = { + .max_leds = CONFIG_EXAMPLE_STRIP_LED_NUMBER, + .strip_gpio_num = CONFIG_EXAMPLE_STRIP_LED_GPIO, + }; + led_strip_rmt_config_t rmt_conf = { + .resolution_hz = 10 * 1000 * 1000, // 10MHz + }; + ESP_ERROR_CHECK(led_strip_new_rmt_device(&led_strip_conf, &rmt_conf, &s_led_strip)); + + light_driver_set_power(power); +} diff --git a/examples/zigbee/zb_common_components/switch_driver/CMakeLists.txt b/examples/zigbee/zb_common_components/switch_driver/CMakeLists.txt new file mode 100644 index 00000000000..aaa61c566a1 --- /dev/null +++ b/examples/zigbee/zb_common_components/switch_driver/CMakeLists.txt @@ -0,0 +1,13 @@ +include($ENV{IDF_PATH}/tools/cmake/version.cmake) + +set(public_requires "freertos") + +if("${IDF_VERSION_MAJOR}.${IDF_VERSION_MINOR}" VERSION_GREATER_EQUAL "5.3") + list(APPEND public_requires "esp_driver_gpio") +else() + list(APPEND public_requires "driver") +endif() + +idf_component_register(SRC_DIRS "src" + INCLUDE_DIRS "include" + REQUIRES ${public_requires}) diff --git a/examples/zigbee/zb_common_components/switch_driver/Kconfig b/examples/zigbee/zb_common_components/switch_driver/Kconfig new file mode 100644 index 00000000000..1a09f65af0a --- /dev/null +++ b/examples/zigbee/zb_common_components/switch_driver/Kconfig @@ -0,0 +1,27 @@ +menu "ESP Zigbee DevKit GPIO Configuration" + config GPIO_BOOT_ON_DEVKIT + int "BOOT GPIO on devkit" + default 9 if IDF_TARGET_ESP32H2 + default 9 if IDF_TARGET_ESP32C6 + default 28 if IDF_TARGET_ESP32C5 + default 9 + + help + The GPIO connected to the BOOT button on the devkits + + config GPIO_EXT1_WAKEUP_SOURCE + int "External wakeup GPIO" + default 9 if IDF_TARGET_ESP32H2 + default 7 if IDF_TARGET_ESP32C6 + default 7 if IDF_TARGET_ESP32S31 + default 6 if IDF_TARGET_ESP32C5 + range 7 14 if IDF_TARGET_ESP32H2 + range 0 7 if IDF_TARGET_ESP32C6 + range 0 7 if IDF_TARGET_ESP32S31 + range 0 6 if IDF_TARGET_ESP32C5 + default 9 + + help + The GPIO can be used as external wake up source for sleep device + +endmenu diff --git a/examples/zigbee/zb_common_components/switch_driver/include/switch_driver.h b/examples/zigbee/zb_common_components/switch_driver/include/switch_driver.h new file mode 100644 index 00000000000..74e05253b9f --- /dev/null +++ b/examples/zigbee/zb_common_components/switch_driver/include/switch_driver.h @@ -0,0 +1,79 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: LicenseRef-Included + * + * Zigbee switch driver + * + * This driver reads a physical GPIO switch/button (e.g. BOOT button on devkit), + * uses GPIO interrupt and a queue to report press/release events to the application + * via a callback. Typical use: Zigbee HA On/Off Switch sending commands on button press. + * + * Unless required by applicable law or agreed to in writing, this + * software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. + */ + +#pragma once + +#include + +#include "driver/gpio.h" +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Opaque handle for the switch driver instance. + */ +typedef void* switch_driver_handle_t; + +/** + * @brief Invalid handle value. + */ +#define SWITCH_INV_HANDLE NULL + +/** + * @brief Callback invoked from GPIO ISR (low-level) when the switch level changes. + * + * Runs in interrupt context: keep it minimal (e.g. set a flag or post to a queue). + * Do not block or call blocking / non-ISR-safe APIs. + * + * @param user_ctx User context passed at init + */ +typedef void (*switch_event_cb_t)(switch_driver_handle_t handle); + +/** + * @brief Switch driver configuration. + */ +typedef struct { + gpio_num_t gpio_num; /*!< GPIO number used as switch input (e.g. BOOT button) */ + switch_event_cb_t event_cb; /*!< Callback on level change (invoked from ISR); may be NULL if only polling */ +} switch_driver_config_t; + +/** + * @brief Initialize the switch driver. + * + * Configures the GPIO in switch_driver_init() as input with pull-up enabled, + * installs the GPIO ISR, and hooks the callback. The callback is triggered in low-level + * (GPIO ISR) context on level change. + * + * @param config Driver configuration (gpio_num required; event_cb optional) + * @return Handle + */ +switch_driver_handle_t switch_driver_init(const switch_driver_config_t *config); + +/** + * @brief Deinitialize the switch driver. + * + * Removes ISR handler and resets GPIO. + * + * @param handle Handle from switch_driver_init; may be NULL + */ +void switch_driver_deinit(switch_driver_handle_t handle); + +#ifdef __cplusplus +} +#endif diff --git a/examples/zigbee/zb_common_components/switch_driver/src/switch_driver.c b/examples/zigbee/zb_common_components/switch_driver/src/switch_driver.c new file mode 100644 index 00000000000..7bb35cab09f --- /dev/null +++ b/examples/zigbee/zb_common_components/switch_driver/src/switch_driver.c @@ -0,0 +1,116 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: LicenseRef-Included + * + * Zigbee switch driver + * + * Unless required by applicable law or agreed to in writing, this + * software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. + */ + +#include +#include + +#include "switch_driver.h" +#include "driver/gpio.h" +#include "freertos/FreeRTOS.h" + +#include "esp_attr.h" +#include "esp_log.h" + +static QueueHandle_t gpio_event_queue = NULL; + +static void switch_driver_gpio_intr_enabled(gpio_num_t gpio_num, bool enabled) +{ + if (enabled) { + gpio_intr_enable(gpio_num); + } else { + gpio_intr_disable(gpio_num); + } +} + +static void IRAM_ATTR gpio_isr_handler(void *arg) +{ + switch_driver_config_t *config = (switch_driver_config_t *)arg; + switch_driver_gpio_intr_enabled(config->gpio_num, false); + xQueueSendFromISR(gpio_event_queue, &config, NULL); +} + +static void switch_driver_button_detected(void *arg) +{ + (void)arg; + switch_driver_config_t *config = NULL; + while (1) { + if (xQueueReceive(gpio_event_queue, &config, portMAX_DELAY) == pdTRUE) { + if (config) { + if (config->event_cb) { + config->event_cb(config); + } + } + } + vTaskDelay(pdMS_TO_TICKS(200)); + switch_driver_gpio_intr_enabled(config->gpio_num, true); + } +} +switch_driver_handle_t switch_driver_init(const switch_driver_config_t *config) +{ + if (config == NULL || config->gpio_num < 0) { + return NULL; + } + + switch_driver_config_t *handle = (switch_driver_config_t *)malloc(sizeof(switch_driver_config_t)); + if (handle == NULL) { + return NULL; + } + memcpy(handle, (void *)config, sizeof(switch_driver_config_t)); + + /* Configure GPIO in init: input, pull-up, and negative-edge interrupt */ + gpio_config_t io_conf = { + .pin_bit_mask = (1ULL << config->gpio_num), + .mode = GPIO_MODE_INPUT, + .pull_up_en = GPIO_PULLUP_ENABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_LOW_LEVEL, + }; + esp_err_t ret = gpio_config(&io_conf); + if (ret != ESP_OK) { + free(handle); + return NULL; + } + gpio_event_queue = xQueueCreate(10, sizeof(switch_driver_config_t *)); + if (gpio_event_queue == 0) { + gpio_reset_pin(config->gpio_num); + free(handle); + return NULL; + } + + xTaskCreate(switch_driver_button_detected, "button_detected", 4096, NULL, 10, NULL); + ret = gpio_install_isr_service(0); + if (ret != ESP_OK) { + gpio_reset_pin(config->gpio_num); + free(handle); + return NULL; + } + + ret = gpio_isr_handler_add(config->gpio_num, gpio_isr_handler, (void *)handle); + if (ret != ESP_OK) { + gpio_reset_pin(config->gpio_num); + free(handle); + return NULL; + } + + return handle; +} + +void switch_driver_deinit(switch_driver_handle_t handle) +{ + if (handle == NULL) { + return; + } + switch_driver_config_t *config = (switch_driver_config_t *)handle; + gpio_isr_handler_remove(config->gpio_num); + gpio_reset_pin(config->gpio_num); + free(config); +}