diff --git a/components/esp-zigbee-console/CMakeLists.txt b/components/esp-zigbee-console/CMakeLists.txt new file mode 100644 index 0000000..f018fc0 --- /dev/null +++ b/components/esp-zigbee-console/CMakeLists.txt @@ -0,0 +1,9 @@ +if (CONFIG_ZB_ZED) + message(FATAL_ERROR "${COMPONENT_NAME} doesn't support CONFIG_ZB_ZED=y") +endif() + +idf_component_register(SRC_DIRS "src" "src/zb_data" + INCLUDE_DIRS "include" + PRIV_REQUIRES esp-zigbee-lib console + LDFRAGMENTS linker.lf + WHOLE_ARCHIVE) diff --git a/components/esp-zigbee-console/README.md b/components/esp-zigbee-console/README.md new file mode 100644 index 0000000..86528ab --- /dev/null +++ b/components/esp-zigbee-console/README.md @@ -0,0 +1,813 @@ +# ESP-Zigbee-Console Reference + +The ESP-Zigbee-Console implements a command line interface to operate the ESP-Zigbee-SDK. The CLI +provides the flexibility to configure and manage the stack at runtime. + +## ESP-Zigbee-Console Command Conventions + +The commands are consisted as ` [] [options]`, the `options` part are parsed using +[`argtable3`](https://www.argtable.org/) provided by `console` component in ESP-IDF. +Therefore the `options` follows +[POSIX Utility Conventions](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html). +For most of the command, type the command can display the help strings of the command: + +```bash +esp> zdo +zdo: Zigbee Device Object management + request Request information from node + annce Announce current node + match Get matched devices + bind Request the node to bind to device + nwk_open Request the node to open the network + nwk_leave Request the node to leave the network + +esp> zdo match +Usage: zdo match [-i ]... [-o ]... [-p ] -d + -i, --in= in cluster ID + -o, --out= out cluster ID + -p, --profile= profile id (PID) to match, defaule: HA + -d, --dst-addr= network address this request is to +``` + +The option arguments are shown as `` to indicate the argument type and name of the option. +For specific type of argument, correct format should be provided so that it can be correctly parsed. + +- `hex`: HEX, start with `0x` prefix. +- `u8`: HEX or NUMBER allowed, in range 0 - 0xFF. +- `u16`: HEX or NUMBER allowed, in range 0 - 0xFFFF. +- `u32`: HEX or NUMBER allowed, in range 0 - 0xFFFFFFFF. +- `eui64`: IEEE address in HEX format, MUST be extactly 64-bit data. +- `addr`: Equivalent to `u16|eui64`, short address or IEEE address determined by the data length. + +## ESP-Zigbee-Console Command List + +- [`address`](#address): Get/Set the (extended) address of the node. +- [`bdb_comm`](#bdb_comm): Perform BDB Commissioning. +- [`channel`](#channel): Get/Set 802.15.4 channels for network +- [`dm`](#dm): ZigBee Cluster Library data model management. +- [`factoryreset`](#factoryreset): Reset the device to factory new. +- [`ic`](#ic): Install code configuration. +- [`network`](#network): Network configuration. +- [`panid`](#panid): Get/Set the (extended) PAN ID of the node. +- [`radio`](#radio): Enable/Disable the radio. +- [`reboot`](#reboot): Reboot the device. +- [`role`](#role): Get/Set the Zigbee role of a device. +- [`start`](#start): Start Zigbee stack. +- [`tl`](#tl): TouchLink configuration. +- [`zcl`](#zcl): ZigBee Cluster Library management. +- [`zdo`](#zdo): Zigbee Device Object management. +- [`zgp`](#zgp): ZigBee Green Power Profile management. +- [`zha`](#zha): ZigBee Home Automation Profile. + +## ESP-Zigbee-Console Command Details + +### address +Get/Set the (extended) address of the Zigbee device node. + +> **Note:** Set short address is not supported. + +#### `address [-x]` +Get the address or extended address of the Zigbee device node. + +```bash +esp> address +0x0d29 +``` + +```bash +esp> address -x +0x4831b7fffec02bbc +``` + +#### `address -x ` +Set the extended address of the Zigbee device node. + +```bash +esp> address -x 0x0123456789abcdef +``` + + +### bdb_comm +Perform BDB Commissioning. + +#### `bdb_comm channel` +Get primary and secondary channels to perform BDB commissioning. The channels are displayed as channel masks. + +Get the operating channels for BDB commissioning. + +```bash +esp> bdb_comm channel +Primary Channel: 0x07fff800 +Secondary Channel: 0x07fff800 +``` + +#### `bdb_comm channel [-m] []` +Set the operating channels for BDB commissioning. + +```bash +esp> bdb_comm channel 13 14 +esp> bdb_comm channel +Primary Channel: 0x00002000 +Secondary Channel: 0x00004000 +``` + +Use `-m` to indicate the channels are given as channel masks. + +```bash +esp> bdb_comm channel -m 0x800 0xf800 +esp> bdb_comm channel +Primary Channel: 0x00000800 +Secondary Channel: 0x0000f800 +``` + +#### `bdb_comm mode` +Get current/last BDB commissioning mode. + +Displayed value is a combination of [`esp_zb_bdb_commissioning_mode_mask_t`](https://docs.espressif.com/projects/esp-zigbee-sdk/en/latest/esp32/api-reference/bdb/esp_zigbee_bdb_touchlink.html#_CPPv436esp_zb_bdb_commissioning_mode_mask_s). + +```bash +esp> bdb_comm mode +0x04 +``` + +#### `bdb_comm start []...` +Start the top level commissioning of BDB. + +> **Note:** The command will start the Zigbee stack if it hasn't started. + +Supported modes are: + +- `steer`: Perform the network steering procedure. +- `form`: Perform the network formation procedure. +- `bind`: Perform finding & binding procedure. +- `initiator`: Perform Touchlink procedure as initiator. +- `target`: Perform Touchlink procedure as target. + +```bash +esp> bdb_comm start form +I (15585) cli_cmd_bdb: ZDO signal: ZDO Config Ready (0x17), status: ESP_FAIL +I (15585) cli_cmd_bdb: Zigbee stack initialized +W (17975) cli_cmd_bdb: Network(0x23ea) closed, devices joining not allowed. +I (17985) cli_cmd_bdb: Formed network successfully (Extended PAN ID: 0x4831b7fffec02bbc, PAN ID: 0x23ea, Channel:26, Short Address: 0x0000) +``` + +#### `bdb_comm cancel ` +Cancel commissioning process. + +Supported modes are: +- `steer`: Cancel the network steering procedure. +- `form`: Cancel the network formation procedure. +- `target`: Cancel Touchlink procedure as target. + + +### channel +Get/Set 802.15.4 channels for network. + +Get allowed channels and current channel if the device node is on a network: + +```bash +esp> channel +Allowed Channel: 0x07fff800 +Current Channel: 19 +``` + +Set the allowed channels for network operation: + +```bash +esp> channel 13 +``` + +Use `-m` to indicate the channels are provided as channel mask: + +```bash +esp> channel -m 0x06ef0000 +``` + + +### dm +ZigBee Cluster Library data model management. + +The sub-commands operate on the `ep_list` registered/created by `esp_zb_console_manage_ep_list()`: + +- [`dm show`](#dm-show) +- [`dm add`](#dm-add) +- [`dm register`](#dm-register) + +The sub-commands operate on registered data model: + +- [`dm read`](#dm-read) +- [`dm write`](#dm-write) + +#### `dm show` +Show current data model. + +> **Note:** The command is only available before `dm register`. + +```bash +esp> zha add 1 on_off_light +I (13055) cli_cmd_zha: on_off_light created with endpoint_id 1 +esp> dm show ++-- ep:1, prfl:0x0104, dev_id:0x0100 + |-- cluster:0x0000, S, manuf:0x0000 + | |-- attr:0xfffd, type:0x21, access:0x01, manuf:0xffff + | |-- attr:0x0000, type:0x20, access:0x01, manuf:0xffff + | +-- attr:0x0007, type:0x30, access:0x01, manuf:0xffff + |-- cluster:0x0003, S, manuf:0x0000 + | |-- attr:0xfffd, type:0x21, access:0x01, manuf:0xffff + | +-- attr:0x0000, type:0x21, access:0x03, manuf:0xffff + |-- cluster:0x0004, S, manuf:0x0000 + | |-- attr:0xfffd, type:0x21, access:0x01, manuf:0xffff + | +-- attr:0x0000, type:0x18, access:0x01, manuf:0xffff + |-- cluster:0x0005, S, manuf:0x0000 + | |-- attr:0xfffd, type:0x21, access:0x01, manuf:0xffff + | |-- attr:0x0000, type:0x20, access:0x01, manuf:0xffff + | |-- attr:0x0001, type:0x20, access:0x01, manuf:0xffff + | |-- attr:0x0002, type:0x21, access:0x01, manuf:0xffff + | |-- attr:0x0003, type:0x10, access:0x01, manuf:0xffff + | +-- attr:0x0004, type:0x18, access:0x01, manuf:0xffff + +-- cluster:0x0006, S, manuf:0x0000 + |-- attr:0xfffd, type:0x21, access:0x01, manuf:0xffff + +-- attr:0x0000, type:0x10, access:0x15, manuf:0xffff +``` +#### `dm add` +Add items (endpoint/cluster/attribute) in ZCL data model. The command only creates missing items and skip the +already existed ones. + +> **Note:** The command can only be used before data model registered. + +Use `-e ` to specify the endpoint id to be added, the options only take effect when `-e` specified: +- `--device `: The device id of the endpoint. +- `--version=`: The device version of the endpoint. +- `--profile `: The profile id of the endpoint. + +Use `-c ` to specify the cluster id to be added, the options only take effect when `-c` specified: +- `-r, --role=`: The role of the cluster. +- `--manuf=`: The manufacture code of the cluster. + +Use `-a ` to specify the attribute id to be added, the options only take effect when `-a` specified: +- `--access=`: Access of the attribute, default: RW +- `--type=`: The type id of the attribute, defined by ZCL specification. +- `-v, --value=`: The default value of the attribute, in HEX format. + +Use `-f` to add items as it is specified in the command. + +To create an `HA on_off_light` on endpoint 1: + +1. Add empty endpoint `1`: + ```bash + esp> dm add -e 1 + ``` + +2. Add `basic` server cluster with mandatory attributes: + ```bash + esp> dm add -e 1 -c 0 + ``` + Add optional attribute `ManufacturerName` with value `"\x09""ESPRESSIF"` to `basic` cluster: + ```bash + esp> dm add -e 1 -c 0 -a 0x04 -v 0x09455350524553534946 + ``` + +3. Add all other clusters for `HA on_off_light` device: + ```bash + esp> dm add -e 1 -c 3 + esp> dm add -e 1 -c 4 + esp> dm add -e 1 -c 5 + esp> dm add -e 1 -c 6 + ``` + Add optional client cluster: + ```bash + esp> dm add -e 1 -c 0x0406 -r C + ``` + + +#### `dm register` +Register current data model. The data model can't be changed after registration. + +```bash +esp> dm register +``` + +#### `dm read` +Read attribute value in data model. + +> **Note:** The command can only be used after the data model registered. + +Use `-r ` to specify the role of the cluster. `S` for server, `C` for client, default: `S`. +Use `--manuf ` to specify the manufucture code if any. + +```bash +esp> dm read -e 1 -c 6 -a 0 +I (43445) : 0x40816308 00 |.| +``` + +#### `dm write` +Write value to attribute in data model. + +Use `-f` to skip the access check of the attribute. + +> **Note:** The command can only be used after the data model registered. + +```bash +esp> dm write -e 1 -c 6 -a 0 -v 0x01 +Fail to write attribute: 136 +Command returned non-zero error code: 0xffffffff (ESP_FAIL) + +esp> dm write -e 1 -c 6 -a 0 -v 0x01 -f +``` + + +### factoryreset +Reset the device to factory new immediately. + +```bash +esp> factoryreset +Erasing Erasing NVRAM of Zigbee stack ... Done +Reboot the device +``` + +### ic +Install code configuration. + +#### `ic add ` +Add install code for a remote device. + +> **Note:** +> - The command should be used after Zigbee stack starts. + +```bash +esp> ic add 0xbbbbbbbbbbbbbbbb 0x83FED3407A939723A5C639B26916D505C3B5 +``` + +#### `ic remove ` +Remove install code for a remote device. + +```bash +esp> ic remove 0xbbbbbbbbbbbbbbbb +``` + +#### `ic set ` +Set the install code on local device. + +```bash +esp> ic set 0x83FED3407A939723A5C639B26916D505C3B5 +``` + +#### `ic get` +Get the install code configured on local device. + +```bash +esp> ic get +I (837619) : 0x4080fa50 83 fe d3 40 7a 93 97 23 a5 c6 39 b2 69 16 d5 05 |...@z..#..9.i...| +I (837619) : 0x4080fa60 c3 b5 |..| +``` + + +### network +Network configuration. + +#### `network type []` +Get/Set the network type. + +Supported types: +- `c`: Centralized network, need be formed by Zigbee coordinator. +- `d`: Distributed network, need be formed by Zigbee router. + +```bash +esp> network type d +``` + +```bash +esp> network type +setting: distributed +current on: distributed +``` + +#### `network key []` +Get/Set the network key. + +Set network key: + +```bash +network key 0x0123456789abcdeffedcba9876543210 +``` + +Get network key: + +```bash +esp> network key +I (77200) : 0x4084f6f4 83 38 66 7a d8 b6 b4 b4 63 17 12 39 0f 83 f8 6a |.8fz....c..9...j| +``` + +#### `network legacy` +Enable/Disable legacy device support. + +> Note: Getting the current state has not been supported yet. + +```bash +esp> network legacy enable +``` + +```bash +esp> network legacy disable +``` + +#### `network childmax []` +Get/Set max children number. + +```bash +esp> network childmax 20 +``` + +```bash +esp> network childmax +20 +``` + +#### `network open -t ` +Open local network for specified timeout in seconds. + +```bash +esp> network open -t 100 +esp> I (332981) cli_cmd_bdb: Network(0x34a4) is open for 100 seconds +``` + +#### `network close` +Close local network immediately. + +```bash +esp> network close +esp> W (354641) cli_cmd_bdb: Network(0x34a4) closed, devices joining not allowed. +``` + +#### `network scan [-m] [-t ]` +Scan channels for networks (network discovery). + +```bash +esp> network scan -m 0x07fff800 +|Chnl| PanID | ExtPanID |Status |Cap| ++----+--------+--------------------+-------+---+ +| 23 | 0x8910 | 0x744dbdfffe602d57 | Close | RE| +| 17 | 0x8a41 | 0x4831b7fffec02bbc | Close | RE| +| 11 | 0xc2ab | 0x6e6ec31131b063da | Close | RE| +``` + +#### `network ed_scan [-m] [-t ]` +Perform energy detection scan on channels. + +```bash +esp> network ed_scan -m 0x07fff800 +|Chnl| RSSI | ++----+------+ +| 11 | -112 | +| 12 | -102 | +| 13 | -100 | +| 14 | -113 | +| 15 | -91 | +| 16 | -105 | +| 17 | -56 | +| 18 | -78 | +| 19 | -77 | +| 20 | -101 | +| 21 | -91 | +| 22 | -114 | +| 23 | -108 | +| 24 | -72 | +| 25 | -75 | +| 26 | -84 | +``` + +### panid +Get/Set the (extended) PAN ID of the node. + +#### `panid [-x]` +Get the PAN ID or extended PAN ID of the Zigbee device node. + +```bash +esp> panid +0x74e5 +``` + +```bash +esp> panid -x +0x744dbdfffe602d57 +``` + +#### `panid [-x] ` +Set the PAN ID or extended PAN ID of the Zigbee device node. + +```bash +esp> panid 0xabcd +``` + +```bash +esp> panid -x 0x0123456789abcdef +``` + + +### reboot +Reboot the device immediately. + +```bash +esp> reboot +Reboot the device +``` + +### role +Get/Set the Zigbee role of a device. + +Supported device role: + +- `zc`: Zigbee coordinator +- `zr`: Zigbee router +- `zed`: Zigbee end device + +Get the role of the Zigbee device. + +```bash +esp> role +zr +``` + +Set the role of the Zigbee device. + +```bash +esp> role zc +esp> role +zc +``` + +### start +Start Zigbee stack. + +```bash +esp> start +Start Zigbee stack +``` + + +### tl +TouchLink configuration. + +#### `tl timeout []` +Set touchlink target timeout. + +```bash +esp> tl timeout 100 +``` + +#### `tl rssi []` +Get/Set touchlink target rssi threshold. + +```bash +esp> tl rssi +-64 +``` + +> **Note:** To correctly set the RSSI to negative values, please use `--` to indicate the end of options. + +```bash +esp> tl rssi -- -53 +``` + +#### `tl key []` +Set touchlink master key. + +```bash +tl key 0x0123456789abcdeffedcba9876543210 +``` + + +### zcl +ZigBee Cluster Library management. + +Common options: +- `-d `: Destination address of the command. +- `--dst-ep `: Destination endpoint of the command. +- `-e `: Source endpoint of the command. +- `--profile=`: Profile id of the command. +- `-c `: Cluster id of the command. +- `-r `: Role of the cluster the command sent to. `S` for server, `C` for client, default: `S`. + +Destination address mode selection: +- If neither endpoint nor address of the destination were specified, the destination would be determined + by bindings. (DstAddrMode 0x00 of APSDE-DATA.request) +- If only the short address is specified, the command will be sent by group addressing. + (DstAddrMode 0x01 of APSDE-DATA.request) +- If both endpoint and short address is specified, the command will be sent by short address. + (DstAddrMode 0x02 of APSDE-DATA.request) +- If both endpoint and IEEE address is specified, the command will be sent by IEEE address. + (DstAddrMode 0x03 of APSDE-DATA.request) + +#### `zcl send_gen [options]` +Send general command. + +Use `-a ` to specify the attribute id the command is operating on. + +Use `-t ` to specify the attribute type id. + +Use `-v ` to specify the attribute value, raw data in HEX format. + +Supported commands: + +- `read`: Read attribute. + ```bash + esp> zcl send_gen read -d 0xb55c --dst-ep 1 -e 2 -c 6 -a 0 + I (69915) cli_cmd_zcl: Read response: endpoint(2), cluster(0x06) + I (69925) cli_cmd_zcl: attribute(0x00), type(0x10), data size(1) + I (69925) : 0x408188d6 00 |.| + ``` +- `write`: Write attribute. + ```bash + esp> zcl send_gen write -d 0xb55c --dst-ep 1 -e 2 -c 6 -a 0 -t 0x10 -v 0x01 + I (788775) cli_cmd_zcl: Write response: endpoint(2), cluster(0x06) + I (788775) cli_cmd_zcl: attribute(0x00), status(0x88) + ``` +- `report`: Report attribute. + ```bash + esp> zcl report -d 0xdc59 --dst-ep 2 -e 1 -c 6 -a 0 + ``` +- `config_rp`: Configure reporting. + ```bash + esp> zcl send_gen config_rp -d 0xcb9a --dst-ep 1 -e 2 -c 6 -a 0 -t 0x10 + I (277959) cli_cmd_zcl: Config report response: endpoint(2), cluster(0x06) + I (277959) cli_cmd_zcl: attribute(0xffff), status(0x0), direction(255) + ``` +- `read_rp_cfg`: Read reporting configuration. + ```bash + esp> zcl send_gen read_rp_cfg -d 0xcb9a --dst-ep 1 -e 2 -c 6 -a 0 + I (368059) cli_cmd_zcl: Read report configure response: endpoint(2), cluster(0x06), attribute(0x00) + ``` +- `disc_attr`: Discover attributes. + ```bash + esp> zcl send_gen disc_attr -d 0xb55c --dst-ep 1 -e 2 -c 6 + I (885865) cli_cmd_zcl: Discover attribute response: endpoint(2), cluster(0x06) + I (885865) cli_cmd_zcl: attribute(0x00), data type(0x10) + ``` + + +#### `zcl send_raw [options]` +Send cluster specific raw command. + +Use `--cmd ` to specify the command id. + +Use `-p ` to specify the raw payload data of the command. + +Use `-n` to perform a dry run. Do not send the command, just dump the content. + +```bash +esp> zcl send_raw -n -d 0x0000 --dst-ep 1 -e 2 -c 2 --cmd 0x00 -p 0x1234567890 +Send request: +Mode[2]: e:2 -> e:1, addr:0x0000 +prfl:0x0104, c:0x0002, dir:S, cmd:0x00 +I (13859) : 0x40815970 12 34 56 78 90 |.4Vx.| +``` + +```bash +esp> zcl send_raw -d 0xb55c --dst-ep 1 -e 2 -c 6 --cmd 0x01 +``` + +### zdo +Zigbee Device Object management. + +#### `zdo request -d [-e ]` +Request information from Zigbee device node. + +Supported information types: + +- `node_desc`: Get the node descriptor of a device node. + ```bash + esp> zdo request node_desc -d 0x3ed5 + node_desc request to [addr:0x3ed5] success: 0 + I (1335839) : 0x40815b34 01 40 8e 34 12 6c 4d 06 00 2c 4d 06 00 |.@.4.lM..,M..| + ``` +- `active_ep`: Get the list of endpoints on a device node with simple descriptors. + ```bash + esp> zdo request active_ep -d 0x3ed5 + active_ep request to [addr:0x3ed5] success: 0 + active ep: [1, 242] + ``` +- `simple_desc`: Get the simple descriptor of a Zigbee device node on a specified endpoint. \ + `-e ` is required to specify the endpoint. + ```bash + esp> zdo request simple_desc -e 1 -d 0x3ed5 + simple_desc request to [addr:0x3ed5] success: 0 + ep:1 profile:0x0104 dev:0x100 dev_ver:0x0 + in: [0x0000, 0x0003, 0x0004, 0x0005, 0x0006] + out: [] + ``` +- `ieee_addr`: Get the 64-bit IEEE address of a device node based on its know short address. + ```bash + esp> zdo request ieee_addr -d 0x3ed5 + ieee_addr request to [addr:0x3ed5] success: 0 + ieee address: 0x744dbdfffe602d57 + ``` +- `bindings`: Get the Binding Table of a device node. + ```bash + esp> zdo bind -c 6 -S 1 -D 1 -s 0x3ed5 -d 0x744dbdfffe602d57 + bind request to [addr:0x3ed5] success: 0 + esp> zdo bind -c 6 -S 1 -D 1 -s 0x3ed5 -d 0x4831b7fffec02bbc + bind request to [addr:0x3ed5] success: 0 + esp> zdo request bindings -d 0x3ed5 + bindings request to [addr:0x3ed5] success: 0 + |Index| Src_Addr |Src_E|Cluster | Dst_Addr |Dst_E| + +-----+--------------------+-----+--------+--------------------+-----+ + | 0 | 0x744dbdfffe602d57 | 1 | 0x0006 | 0x744dbdfffe602d57 | 1 | + | 1 | 0x744dbdfffe602d57 | 1 | 0x0006 | 0x4831b7fffec02bbc | 1 | + esp> + ``` + +#### `zdo annce` +Announce current node (by broadcasting `Device_annce`). + +#### `zdo match [-i ]... [-o ]... [-p ] -d ` +Get matched devices (supporting a specific simple descriptor criterion). + +If there were matched devices, the information would be printed as `matched device: :`. + +```bash +esp> zdo match -i 0 -i 3 -i 4 -i 5 -i 6 -p 0xFFFF -d 0xFFFF +match request to [addr:0xffff] success: 0 +matched device: 0x3ed5:1 +``` + +#### `zdo bind [-r] -c -S [-D ] -s -d ` +Request the node to bind/unbind to device. + +Use `-r` to remove specific bindings. + +If no destination endpoint is specified via (`-D `) and the destination address provided by +`-d ` is short address, the destination address mode of the binding would be set to group +addressing mode. + +Assume that we have a pair of device with the following setup: +| Configuration | Remote | Local | +| :------------ | -----------------: | -----------------: | +| Short Address | 0x3ed5 | 0x6db5 | +| IEEE Address | 0x744dbdfffe602d57 | 0x4831b7fffec02bbc | +| Endpoint | 1 | 1 | +| Device on EP | HA on_off_light | HA on_off_switch | +| Endpoint | 2 | 2 | +| Device on EP | HA on_off_switch | HA on_off_light | + +Request the remote device bind `on_off cluster(0x06)` on endpoint 1 with that on endpoint 2 at local device: +```bash +esp> zdo bind -c 6 -S 1 -D 2 -s 0x3ed5 -d 0x4831b7fffec02bbc +bind request to [addr:0x3ed5] success: 0 +``` +Request the remote device bind `on_off cluster(0x06)` on endpoint 1 with that on endpoint 2: +```bash +esp> zdo bind -c 6 -S 1 -D 2 -s 0x3ed5 -d 0x744dbdfffe602d57 +bind request to [addr:0x3ed5] success: 0 +``` +After the two binding requests, check the binding table on remote device: +```bash +esp> zdo request bindings -d 0x3ed5 +bindings request to [addr:0x3ed5] success: 0 +|Index| Src_Addr |Src_E|Cluster | Dst_Addr |Dst_E| ++-----+--------------------+-----+--------+--------------------+-----+ +| 0 | 0x744dbdfffe602d57 | 1 | 0x0006 | 0x4831b7fffec02bbc | 2 | +| 1 | 0x744dbdfffe602d57 | 1 | 0x0006 | 0x744dbdfffe602d57 | 2 | +esp> +``` + +#### `zdo nwk_open [-t ] -d ` +Request the node to open the network for specific duration. + +Use `-t ` to specify the duration for network opening. + +#### `zdo nwk_leave [-cr] -d ` +Request the node to leave the network. + +Use `-c` to request the node remove all its children when leaving the network. + +Use `-r` to request the node rejoin after leaving the network. + + +### zgp +ZigBee Green Power Profile management. + + +### zha +ZigBee Home Automation Profile. + +#### `zha add ` +Add device by device type name. + +Example: [`dm show`](#dm-show) + +Supported devices: +- on_off_switch +- configuration_tool +- mains_power_outlet +- door_lock +- door_lock_controller +- on_off_light +- color_dimmable_light +- color_dimmable_switch +- light_sensor +- shade +- shade_controller +- window_covering +- window_covering_controller +- thermostat +- temperature_sensor diff --git a/components/esp-zigbee-console/idf_component.yml b/components/esp-zigbee-console/idf_component.yml new file mode 100644 index 0000000..e175678 --- /dev/null +++ b/components/esp-zigbee-console/idf_component.yml @@ -0,0 +1,7 @@ +version: "1.0.0" +description: Espressif Zigbee Console Component +url: https://github.com/espressif/esp-zigbee-sdk +dependencies: + espressif/esp-zboss-lib: ">=1.5.0" + espressif/esp-zigbee-lib: ">=1.5.0" + idf: ">=5.0" diff --git a/components/esp-zigbee-console/include/esp_zigbee_console.h b/components/esp-zigbee-console/include/esp_zigbee_console.h new file mode 100644 index 0000000..61ff3db --- /dev/null +++ b/components/esp-zigbee-console/include/esp_zigbee_console.h @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "esp_zigbee_core.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Initialize ESP Zigbee Console. + * + * @return ESP_OK on success, error code otherwise. + */ +esp_err_t esp_zb_console_init(void); + +/** + * @brief Start ESP Zigbee Console. + * + * @return ESP_OK on success, error code otherwise. + */ +esp_err_t esp_zb_console_start(void); + +/** + * @brief Stop and de-init ESP Zigbee Console. + * + * @return ESP_OK on success, error code otherwise. + */ +esp_err_t esp_zb_console_deinit(void); + +/** + * @brief Enable ESP Zigbee Console to manage the endpoint list. + * + * The function enables the ESP Zigbee Console to add/show/register + * ZCL data model with `dm` command. + * + * @param ep_list User defined endpoint list, NULL means an empty list. + * @return ESP_OK on success, error code otherwise. + */ +esp_err_t esp_zb_console_manage_ep_list(esp_zb_ep_list_t *ep_list); + +#ifdef __cplusplus +} +#endif diff --git a/components/esp-zigbee-console/linker.lf b/components/esp-zigbee-console/linker.lf new file mode 100644 index 0000000..e0a998c --- /dev/null +++ b/components/esp-zigbee-console/linker.lf @@ -0,0 +1,15 @@ +[sections:esp_zb_cli_cmd_desc] +entries: + .esp_zb_cli_cmd_desc+ + +[scheme:esp_zb_cli_cmd_desc_default] +entries: + esp_zb_cli_cmd_desc -> flash_rodata + +# Collect all the command descriptors declared in ".esp_zb_cli_cmd_desc." sections, +# so that we could automatically register them with unified logic. +[mapping:esp_zb_cli_cmd_desc] +archive: * +entries: + * (esp_zb_cli_cmd_desc_default); + esp_zb_cli_cmd_desc -> flash_rodata KEEP() SORT(name) SURROUND(esp_zb_cli_cmd_array) diff --git a/components/esp-zigbee-console/src/argtable_ext.c b/components/esp-zigbee-console/src/argtable_ext.c new file mode 100644 index 0000000..c118be1 --- /dev/null +++ b/components/esp-zigbee-console/src/argtable_ext.c @@ -0,0 +1,370 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * This file implements the extensions of argtable3 used by esp-zigbee-console. + */ + +#include + +#include "esp_check.h" + +#include "cmdline_parser.h" + +#include "argtable_ext.h" + +static void arg_commom_resetfn(struct arg_lit* parent) +{ + parent->count = 0; +} + +static esp_err_t arg_common_checkfn(struct arg_lit* parent) +{ + esp_err_t errorcode = (parent->count < parent->hdr.mincount) ? ESP_ERR_NOT_FOUND : ESP_OK; + return errorcode; +} + +static void arg_common_errorfn(struct arg_lit* parent, arg_dstr_t ds, int errorcode, const char* argval, const char* progname) +{ + const char* shortopts = parent->hdr.shortopts; + const char* longopts = parent->hdr.longopts; + const char* datatype = parent->hdr.datatype; + + /* make argval NULL safe */ + argval = argval ? argval : ""; + + arg_dstr_catf(ds, "%s: ", progname); + switch (errorcode) { + case ESP_ERR_NOT_FOUND: + arg_dstr_cat(ds, "missing option "); + arg_print_option_ds(ds, shortopts, longopts, datatype, "\n"); + break; + + case ESP_ERR_INVALID_STATE: + arg_dstr_cat(ds, "excess option "); + arg_print_option_ds(ds, shortopts, longopts, argval, "\n"); + break; + + case ESP_ERR_INVALID_ARG: + arg_dstr_catf(ds, "invalid argument \"%s\" to option ", argval); + arg_print_option_ds(ds, shortopts, longopts, datatype, "\n"); + break; + + case ESP_ERR_INVALID_SIZE: + arg_dstr_cat(ds, "invalid value size at option "); + arg_print_option_ds(ds, shortopts, longopts, datatype, " "); + arg_dstr_catf(ds, "(%s does not fit)\n", argval); + break; + } +} + +static esp_err_t arg_u8_scanfn(arg_u8_t* parent, const char* argval) +{ + esp_err_t ret = ESP_OK; + + if (parent->count == parent->hdr.maxcount) { + ret = ESP_ERR_INVALID_STATE; + } else if (!argval) { + parent->count++; + } else { + uint8_t val; + ret = parse_u8(argval, &val); + if (ret == ESP_OK) { + parent->val[parent->count++] = val; + } + } + + return ret; +} + +arg_u8_t *arg_u8n(const char* shortopts, const char* longopts, const char* datatype, int mincount, int maxcount, const char* glossary) +{ + size_t nbytes; + arg_u8_t* result; + + /* foolproof things by ensuring maxcount is not less than mincount */ + maxcount = (maxcount < mincount) ? mincount : maxcount; + + nbytes = sizeof(arg_u8_t) + (size_t)maxcount * sizeof(uint8_t); + + result = (arg_u8_t*)malloc(nbytes); + + /* init the arg_hdr struct */ + result->hdr.flag = ARG_HASVALUE; + result->hdr.shortopts = shortopts; + result->hdr.longopts = longopts; + result->hdr.datatype = datatype ? datatype : ""; + result->hdr.glossary = glossary; + result->hdr.mincount = mincount; + result->hdr.maxcount = maxcount; + result->hdr.parent = result; + result->hdr.resetfn = (arg_resetfn*)arg_commom_resetfn; + result->hdr.scanfn = (arg_scanfn*)arg_u8_scanfn; + result->hdr.checkfn = (arg_checkfn*)arg_common_checkfn; + result->hdr.errorfn = (arg_errorfn*)arg_common_errorfn; + + result->val = (uint8_t*)(result + 1); + result->count = 0; + + return result; +} + +static esp_err_t arg_u16_scanfn(arg_u16_t* parent, const char* argval) +{ + esp_err_t ret = ESP_OK; + + if (parent->count == parent->hdr.maxcount) { + ret = ESP_ERR_INVALID_STATE; + } else if (!argval) { + parent->count++; + } else { + uint16_t val; + ret = parse_u16(argval, &val); + if (ret == ESP_OK) { + parent->val[parent->count++] = val; + } + } + + return ret; +} + +arg_u16_t *arg_u16n(const char* shortopts, const char* longopts, const char* datatype, int mincount, int maxcount, const char* glossary) +{ + size_t nbytes; + arg_u16_t* result; + + /* foolproof things by ensuring maxcount is not less than mincount */ + maxcount = (maxcount < mincount) ? mincount : maxcount; + + nbytes = sizeof(arg_u16_t) + (size_t)maxcount * sizeof(uint16_t); + + result = (arg_u16_t*)malloc(nbytes); + + /* init the arg_hdr struct */ + result->hdr.flag = ARG_HASVALUE; + result->hdr.shortopts = shortopts; + result->hdr.longopts = longopts; + result->hdr.datatype = datatype ? datatype : ""; + result->hdr.glossary = glossary; + result->hdr.mincount = mincount; + result->hdr.maxcount = maxcount; + result->hdr.parent = result; + result->hdr.resetfn = (arg_resetfn*)arg_commom_resetfn; + result->hdr.scanfn = (arg_scanfn*)arg_u16_scanfn; + result->hdr.checkfn = (arg_checkfn*)arg_common_checkfn; + result->hdr.errorfn = (arg_errorfn*)arg_common_errorfn; + + result->val = (uint16_t*)(result + 1); + result->count = 0; + + return result; +} + +static esp_err_t arg_u32_scanfn(arg_u32_t* parent, const char* argval) +{ + esp_err_t ret = ESP_OK; + + if (parent->count == parent->hdr.maxcount) { + ret = ESP_ERR_INVALID_STATE; + } else if (!argval) { + parent->count++; + } else { + uint32_t val; + ret = parse_u32(argval, &val); + if (ret == ESP_OK) { + parent->val[parent->count++] = val; + } + } + + return ret; +} + +arg_u32_t *arg_u32n(const char* shortopts, const char* longopts, const char* datatype, int mincount, int maxcount, const char* glossary) +{ + size_t nbytes; + arg_u32_t* result; + + /* foolproof things by ensuring maxcount is not less than mincount */ + maxcount = (maxcount < mincount) ? mincount : maxcount; + + nbytes = sizeof(arg_u32_t) + (size_t)maxcount * sizeof(uint32_t); + + result = (arg_u32_t*)malloc(nbytes); + + /* init the arg_hdr struct */ + result->hdr.flag = ARG_HASVALUE; + result->hdr.shortopts = shortopts; + result->hdr.longopts = longopts; + result->hdr.datatype = datatype ? datatype : ""; + result->hdr.glossary = glossary; + result->hdr.mincount = mincount; + result->hdr.maxcount = maxcount; + result->hdr.parent = result; + result->hdr.resetfn = (arg_resetfn*)arg_commom_resetfn; + result->hdr.scanfn = (arg_scanfn*)arg_u32_scanfn; + result->hdr.checkfn = (arg_checkfn*)arg_common_checkfn; + result->hdr.errorfn = (arg_errorfn*)arg_common_errorfn; + + result->val = (uint32_t*)(result + 1); + result->count = 0; + + return result; +} + +static esp_err_t arg_hex_scanfn(arg_hex_t* parent, const char* argval) +{ + esp_err_t ret = ESP_OK; + + if (parent->count == parent->hdr.maxcount) { + ret = ESP_ERR_INVALID_STATE; + } else if (!argval) { + parent->count++; + } else { + size_t buffer_len = (strlen(argval) - 1) / 2; + uint8_t *buffer = NULL; + if (!(0 < buffer_len && buffer_len < UINT16_MAX)) { + return ESP_ERR_INVALID_SIZE; + } + buffer = malloc(buffer_len); + if (buffer == NULL){ + return ESP_ERR_NO_MEM; + } + ret = parse_hex_str(argval, buffer, buffer_len, &buffer_len); + if (ret == ESP_OK) { + parent->hsize[parent->count] = buffer_len; + parent->hval[parent->count] = buffer; + parent->count++; + } else { + free(buffer); + } + } + + return ret; +} + +arg_hex_t *arg_hexn(const char* shortopts, const char* longopts, const char* datatype, int mincount, int maxcount, const char* glossary) +{ + size_t nbytes; + arg_hex_t* result; + + /* foolproof things by ensuring maxcount is not less than mincount */ + maxcount = (maxcount < mincount) ? mincount : maxcount; + + nbytes = sizeof(arg_hex_t) + (size_t)maxcount * (sizeof(uint16_t) + sizeof(uint8_t *)); + + result = (arg_hex_t*)malloc(nbytes); + + /* init the arg_hdr struct */ + result->hdr.flag = ARG_HASVALUE; + result->hdr.shortopts = shortopts; + result->hdr.longopts = longopts; + result->hdr.datatype = datatype ? datatype : ""; + result->hdr.glossary = glossary; + result->hdr.mincount = mincount; + result->hdr.maxcount = maxcount; + result->hdr.parent = result; + result->hdr.resetfn = (arg_resetfn*)arg_commom_resetfn; + result->hdr.scanfn = (arg_scanfn*)arg_hex_scanfn; + result->hdr.checkfn = (arg_checkfn*)arg_common_checkfn; + result->hdr.errorfn = (arg_errorfn*)arg_common_errorfn; + + result->hsize = (uint16_t *)(result + 1); + result->hval = (uint8_t **)(result->hsize + maxcount); + result->count = 0; + + return result; +} + +void arg_hex_free(arg_hex_t* parent) +{ + for (int i = 0; i < parent->count; i++) { + if (parent->hval[i] != NULL) { + free(parent->hval[i]); + } + } +} + +static esp_err_t arg_devid_scanfn(arg_devid_t* parent, const char* argval) +{ + esp_err_t ret = arg_u16_scanfn(parent, argval); + + /* Try to parse the device name */ + if (ret == ESP_ERR_INVALID_ARG) { + uint16_t esp_zb_get_device_id_by_name(const char *name); + uint16_t device_id = esp_zb_get_device_id_by_name(argval); + if (device_id != 0xFFFF) { + parent->val[parent->count++] = device_id; + ret = ESP_OK; + } + } + + return ret; +} + +arg_devid_t *arg_devidn(const char* shortopts, const char* longopts, const char* datatype, int mincount, int maxcount, const char* glossary) +{ + arg_devid_t *result = (arg_devid_t*)arg_u16n(shortopts, longopts, datatype, mincount, maxcount, glossary); + result->hdr.scanfn = (arg_scanfn*)arg_devid_scanfn; + + return result; +} + +static esp_err_t arg_addr_scanfn(arg_addr_t* parent, const char* argval) +{ + esp_err_t ret = ESP_OK; + + if (parent->count == parent->hdr.maxcount) { + ret = ESP_ERR_INVALID_STATE; + } else if (!argval) { + parent->count++; + } else { + ret = parse_zcl_addr(argval, &parent->addr[parent->count]); + if (ret == ESP_OK) { + parent->count++; + } + } + + return ret; +} + +arg_addr_t* arg_addrn(const char* shortopts, const char* longopts, const char* datatype, int mincount, int maxcount, const char* glossary) +{ + size_t nbytes; + arg_addr_t* result; + + /* foolproof things by ensuring maxcount is not less than mincount */ + maxcount = (maxcount < mincount) ? mincount : maxcount; + + nbytes = sizeof(arg_addr_t) + (size_t)maxcount * sizeof(esp_zb_zcl_addr_t); + + result = (arg_addr_t*)malloc(nbytes); + + /* init the arg_hdr struct */ + result->hdr.flag = ARG_HASVALUE; + result->hdr.shortopts = shortopts; + result->hdr.longopts = longopts; + result->hdr.datatype = datatype ? datatype : ""; + result->hdr.glossary = glossary; + result->hdr.mincount = mincount; + result->hdr.maxcount = maxcount; + result->hdr.parent = result; + result->hdr.resetfn = (arg_resetfn*)arg_commom_resetfn; + result->hdr.scanfn = (arg_scanfn*)arg_addr_scanfn; + result->hdr.checkfn = (arg_checkfn*)arg_common_checkfn; + result->hdr.errorfn = (arg_errorfn*)arg_common_errorfn; + + result->addr = (esp_zb_zcl_addr_t*)(result + 1); + result->count = 0; + + return result; +} + +void arg_print_help(void** argtable, const char *program) +{ + printf("Usage: %s", program); + arg_print_syntax(stdout, argtable, "\n"); + arg_print_glossary(stdout, argtable, " %-25s %s\n"); +} diff --git a/components/esp-zigbee-console/src/argtable_ext.h b/components/esp-zigbee-console/src/argtable_ext.h new file mode 100644 index 0000000..5f785e0 --- /dev/null +++ b/components/esp-zigbee-console/src/argtable_ext.h @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * This file includes the extensions of argtable3 used by esp-zigbee-console. + */ + +#pragma once + +#include + +#include "argtable3/argtable3.h" +#include "esp_zigbee_type.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define ESP_ZB_CLI_FREE_ARGSTRUCT(p_args) arg_freetable((void**)p_args, sizeof(*p_args) / sizeof(void*)) + +typedef struct arg_u8 { + struct arg_hdr hdr; /* The mandatory argtable header struct */ + int count; /* Number of matching command line args */ + uint8_t* val; /* Array of parsed argument values */ +} arg_u8_t; + +typedef struct arg_u16 { + struct arg_hdr hdr; /* The mandatory argtable header struct */ + int count; /* Number of matching command line args */ + uint16_t* val; /* Array of parsed argument values */ +} arg_u16_t; + +typedef struct arg_u32 { + struct arg_hdr hdr; /* The mandatory argtable header struct */ + int count; /* Number of matching command line args */ + uint32_t* val; /* Array of parsed argument values */ +} arg_u32_t; + +typedef struct arg_hex { + struct arg_hdr hdr; /* The mandatory argtable header struct */ + int count; /* Number of matching command line args */ + uint16_t *hsize; /* length of the hex in bytes */ + uint8_t **hval; /* Array of parsed argument values */ +} arg_hex_t; + +typedef struct arg_addr { + struct arg_hdr hdr; /* The mandatory argtable header struct */ + int count; /* Number of matching command line args */ + esp_zb_zcl_addr_t* addr; /* Array of parsed argument values */ +} arg_addr_t; + +typedef arg_u16_t arg_devid_t; + +arg_u8_t *arg_u8n(const char* shortopts, const char* longopts, const char* datatype, int mincount, int maxcount, const char* glossary); +arg_u16_t *arg_u16n(const char* shortopts, const char* longopts, const char* datatype, int mincount, int maxcount, const char* glossary); +arg_u32_t *arg_u32n(const char* shortopts, const char* longopts, const char* datatype, int mincount, int maxcount, const char* glossary); +arg_hex_t *arg_hexn(const char* shortopts, const char* longopts, const char* datatype, int mincount, int maxcount, const char* glossary); +void arg_hex_free(arg_hex_t* parent); +arg_devid_t *arg_devidn(const char* shortopts, const char* longopts, const char* datatype, int mincount, int maxcount, const char* glossary); +arg_addr_t *arg_addrn(const char* shortopts, const char* longopts, const char* datatype, int mincount, int maxcount, const char* glossary); + +void arg_print_help(void** argtable, const char *program); + +#ifdef __cplusplus +} +#endif diff --git a/components/esp-zigbee-console/src/cli_cmd.c b/components/esp-zigbee-console/src/cli_cmd.c new file mode 100644 index 0000000..fced1f5 --- /dev/null +++ b/components/esp-zigbee-console/src/cli_cmd.c @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include "cli_cmd.h" + +static void cli_output_cmd_help(uint8_t indent_size, esp_zb_cli_cmd_t *cmd) +{ + if ((cmd->help != NULL) && (strlen(cmd->help) != 0)) { + if (cmd->sub_cmd_count == 0) { + cli_output("%*s%-15s %s\n", indent_size, "", cmd->name, cmd->help); + } else { + cli_output("%*s%s: %s\n", indent_size, "", cmd->name, cmd->help); + } + } + for (int i = 0; i < cmd->sub_cmd_count; i++) { + cli_output_cmd_help(indent_size + 4, &cmd->sub_cmds[i]); + } +} + +esp_err_t esp_zb_cli_process_cmd(esp_zb_cli_cmd_t *cmd, int argc, char **argv) +{ + esp_err_t ret = ESP_FAIL; + /* argv[0] is the name of main command */ + if (argc > 1 && argv[1][0] != '-') { + /* We got sub commands */ + for (int i = 0; i < cmd->sub_cmd_count; i++) { + esp_zb_cli_cmd_t *sub_cmd= cmd->sub_cmds + i; + if (!strcmp(sub_cmd->name, argv[1])) { + /* Construct 'program name' for the sub-command. */ + argv[1][-1] = ' '; + argv[1] = argv[0]; + return esp_zb_cli_process_cmd(sub_cmd, argc - 1, &argv[1]); + } + } + } + + /* It's not a sub command, process the main command */ + if (cmd->operation != NULL) { + ret = cmd->operation(cmd, argc, argv); + } else { + cli_output_cmd_help(0, cmd); + ret = ESP_OK; + } + + return ret; +} diff --git a/components/esp-zigbee-console/src/cli_cmd.h b/components/esp-zigbee-console/src/cli_cmd.h new file mode 100644 index 0000000..4bcc319 --- /dev/null +++ b/components/esp-zigbee-console/src/cli_cmd.h @@ -0,0 +1,106 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "esp_err.h" + +#include "cli_util.h" +#include "argtable_ext.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct esp_zb_cli_cmd_s { + const char *name; + const char *help; + esp_err_t (*operation)(struct esp_zb_cli_cmd_s *self, int argc, char **argv); + struct esp_zb_cli_cmd_s *sub_cmds; + int sub_cmd_count; +} esp_zb_cli_cmd_t; + +#define _cmd_section(sec) ".esp_zb_cli_cmd_desc." # sec +#define _section(sec) __attribute__((__section__(sec))) +#define _used __attribute__((__used__)) +#define ESP_ZB_CLI_CMD_DESC(cmd) _used _section(_cmd_section(cmd)) + +/* Format: s__subcmd_list */ +#define _subcmd_list_name(_name) s_ ## _name ## _subcmd_list + +/* Format: s_cmd_ */ +#define _cmd_var_name(_name) s_cmd_ ## _name + +/* Expand `_macro` when `VA_ARG` is not empty. */ +#define _expand_or_non(_macro, ...) __VA_OPT__(_macro(__VA_ARGS__)) + +#define ESP_ZB_CLI_CMD_SUBCMD_FEILDS(_sub) \ + .sub_cmds = _sub, .sub_cmd_count = ARRAY_SIZE(_sub), + +#define ESP_ZB_CLI_CMD_OP_FEILDS(_op) \ + .operation = _op, + +#define ESP_ZB_CLI_CMD(_name, _op, _sub, _help) \ + { \ + .name = #_name, \ + .help = _help, \ + _expand_or_non(ESP_ZB_CLI_CMD_OP_FEILDS, _op) \ + _expand_or_non(ESP_ZB_CLI_CMD_SUBCMD_FEILDS, _sub) \ + } + +#define ESP_ZB_CLI_CMD_WITH_SUB(_name, _sub, _help) ESP_ZB_CLI_CMD(_name,, _subcmd_list_name(_sub), _help) +#define ESP_ZB_CLI_CMD_WITH_OP(_name, _op, _help) ESP_ZB_CLI_CMD(_name, _op,, _help) +#define ESP_ZB_CLI_SUBCMD(_name, _op, _help) ESP_ZB_CLI_CMD(_name, _op,, _help) + +/** + * @brief Declare sub-command list of parent command. + * + * @param _parent Name of the parent command. + * @param ... Sub-command definations (should be valid initiallizer of `esp_zb_cli_cmd_t`). + * + */ +#define DECLARE_ESP_ZB_CLI_SUBCMD_LIST(_parent, ...) \ + static esp_zb_cli_cmd_t _subcmd_list_name(_parent)[] = { \ + __VA_ARGS__ \ + } + +/** + * @brief Declare a cli command. + * + * @note The command structure will be placed in `.esp_zb_cli_cmd_desc.` section. + * + * @param _name Name of the command. + * @param _op Operation function of the command. + * @param _sub Sub-command list. + * @param _help Help string of command. + * + */ +#define DECLARE_ESP_ZB_CLI_CMD(_name, _op, _sub, _help) \ + static const esp_zb_cli_cmd_t _cmd_var_name(_name) \ + ESP_ZB_CLI_CMD_DESC(_name) \ + = ESP_ZB_CLI_CMD(_name, _op, _sub, _help) + +/** + * @brief Declare a cli command and its sub-commands list. + * + * @note The command structure will be placed in `.esp_zb_cli_cmd_desc.` section. + * + * @param _name Name of the main command. + * @param _help Help string of the main command. + * @param ... Sub-command definations (should be valid initiallizer of `esp_zb_cli_cmd_t`). + * + */ +#define DECLARE_ESP_ZB_CLI_CMD_WITH_SUB(_name, _help, ...) \ + DECLARE_ESP_ZB_CLI_SUBCMD_LIST(_name, __VA_ARGS__); \ + static const esp_zb_cli_cmd_t _cmd_var_name(_name) \ + ESP_ZB_CLI_CMD_DESC(_name) \ + = ESP_ZB_CLI_CMD_WITH_SUB(_name, _name, _help) + +esp_err_t esp_zb_cli_process_cmd(esp_zb_cli_cmd_t *cmd, int argc, char **argv); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/components/esp-zigbee-console/src/cli_cmd_bdb.c b/components/esp-zigbee-console/src/cli_cmd_bdb.c new file mode 100644 index 0000000..cfb8b05 --- /dev/null +++ b/components/esp-zigbee-console/src/cli_cmd_bdb.c @@ -0,0 +1,891 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include "esp_check.h" +#include "esp_zigbee_core.h" + +#include "esp_zigbee_console.h" +#include "cli_cmd.h" +#include "cmdline_parser.h" + +#define TAG "cli_cmd_bdb" + +typedef struct cli_bdb_context_s { + bool distributed; + bool ic_only; +} cli_bdb_context_t; + +static cli_bdb_context_t s_bdb_ctx = { + .distributed = true, + .ic_only = false, +}; + +static esp_err_t cli_role(esp_zb_cli_cmd_t *self, int argc, char **argv) +{ + static const char *role_string[] = { + [ESP_ZB_DEVICE_TYPE_COORDINATOR] = "zc", + [ESP_ZB_DEVICE_TYPE_ROUTER] = "zr", + [ESP_ZB_DEVICE_TYPE_ED] = "zed", + [ESP_ZB_DEVICE_TYPE_NONE] = "unknown", + }; + struct { + arg_str_t *role; + arg_lit_t *help; + arg_end_t *end; + } argtable = { + .role = arg_str0(NULL, NULL, "", "role of the node"), + .help = arg_lit0(NULL, "help", "print this help message"), + .end = arg_end(2), + }; + esp_err_t ret = ESP_OK; + + /* Parse command line arguments */ + int nerrors = arg_parse(argc, argv, (void**)&argtable); + EXIT_ON_FALSE(argtable.help->count == 0, ESP_OK, arg_print_help((void**)&argtable, argv[0])); + EXIT_ON_FALSE(nerrors == 0, ESP_ERR_INVALID_ARG, arg_print_errors(stdout, argtable.end, argv[0])); + + if (argtable.role->count > 0) { + for (esp_zb_nwk_device_type_t role = ESP_ZB_DEVICE_TYPE_COORDINATOR; role < ESP_ZB_DEVICE_TYPE_NONE; role++) { + if (!strcmp(argtable.role->sval[0], role_string[role])) { + EXIT_NOW(ret = esp_zb_set_network_device_role(role)); + } + } + cli_output("unknown device role: %s\n", argtable.role->sval[0]); + ret = ESP_ERR_NOT_SUPPORTED; + } else { + cli_output_line(role_string[esp_zb_get_network_device_role()]); + } + +exit: + ESP_ZB_CLI_FREE_ARGSTRUCT(&argtable); + return ret; +} + +static esp_err_t cli_panid(esp_zb_cli_cmd_t *self, int argc, char **argv) +{ + struct { + arg_lit_t *extended; + arg_addr_t *address; + arg_lit_t *help; + arg_end_t *end; + } argtable = { + .extended = arg_lit0("x", NULL, "operate on extended PAN ID"), + .address = arg_addrn(NULL, NULL, "", 0, 1, "(extended) PAN ID"), + .help = arg_lit0(NULL, "help", "Print this help message"), + .end = arg_end(2), + }; + esp_err_t ret = ESP_OK; + + /* Parse command line arguments */ + int nerrors = arg_parse(argc, argv, (void**)&argtable); + EXIT_ON_FALSE(argtable.help->count == 0, ESP_OK, arg_print_help((void**)&argtable, argv[0])); + EXIT_ON_FALSE(nerrors == 0, ESP_ERR_INVALID_ARG, arg_print_errors(stdout, argtable.end, argv[0])); + + if (argtable.extended->count > 0) { + if (argtable.address->count > 0) { + EXIT_ON_FALSE(argtable.address->addr[0].addr_type == ESP_ZB_ZCL_ADDR_TYPE_IEEE, ESP_ERR_INVALID_ARG); + esp_zb_set_extended_pan_id(argtable.address->addr[0].u.ieee_addr); + } else { + esp_zb_ieee_addr_t ext_pan_id; + esp_zb_get_extended_pan_id(ext_pan_id); + cli_output("0x%016" PRIx64 "\n", *(uint64_t *)&ext_pan_id); + } + } else { + if (argtable.address->count > 0) { + EXIT_ON_FALSE(argtable.address->addr[0].addr_type == ESP_ZB_ZCL_ADDR_TYPE_SHORT, ESP_ERR_INVALID_ARG); + esp_zb_set_pan_id(argtable.address->addr[0].u.short_addr); + } else { + cli_output("0x%04hx\n", esp_zb_get_pan_id()); + } + } + +exit: + ESP_ZB_CLI_FREE_ARGSTRUCT(&argtable); + return ret; +} + +static esp_err_t cli_address(esp_zb_cli_cmd_t *self, int argc, char **argv) +{ + struct { + arg_lit_t *extended; + arg_addr_t *address; + arg_lit_t *help; + arg_end_t *end; + } argtable = { + .extended = arg_lit0("x", NULL, "operate on extended address"), + .address = arg_addrn(NULL, NULL, "", 0, 1, "(extended) address"), + .help = arg_lit0(NULL, "help", "Print this help message"), + .end = arg_end(2), + }; + esp_err_t ret = ESP_OK; + + /* Parse command line arguments */ + int nerrors = arg_parse(argc, argv, (void**)&argtable); + EXIT_ON_FALSE(argtable.help->count == 0, ESP_OK, arg_print_help((void**)&argtable, argv[0])); + EXIT_ON_FALSE(nerrors == 0, ESP_ERR_INVALID_ARG, arg_print_errors(stdout, argtable.end, argv[0])); + + if (argtable.extended->count > 0) { + if (argtable.address->count > 0) { + EXIT_ON_FALSE(argtable.address->addr[0].addr_type == ESP_ZB_ZCL_ADDR_TYPE_IEEE, ESP_ERR_INVALID_ARG); + EXIT_ON_ERROR(esp_zb_set_long_address(argtable.address->addr[0].u.ieee_addr)); + } else { + esp_zb_ieee_addr_t ext_pan_id; + esp_zb_get_long_address(ext_pan_id); + cli_output("0x%016" PRIx64 "\n", *(uint64_t *)&ext_pan_id); + } + } else { + if (argtable.address->count > 0) { + EXIT_ON_FALSE(0, ESP_ERR_NOT_SUPPORTED, cli_output_line("set short address is not supported")); + } else { + cli_output("0x%04hx\n", esp_zb_get_short_address()); + } + } + +exit: + ESP_ZB_CLI_FREE_ARGSTRUCT(&argtable); + return ret; +} + +static esp_err_t cli_channel(esp_zb_cli_cmd_t *self, int argc, char **argv) +{ + struct { + arg_lit_t *mask; + arg_u32_t *channel; + arg_lit_t *help; + arg_end_t *end; + } argtable = { + .mask = arg_lit0("m", NULL, "use channel mask"), + .channel = arg_u32n(NULL, NULL, "", 0, 1, "allowed channel (mask)"), + .help = arg_lit0(NULL, "help", "Print this help message"), + .end = arg_end(2), + }; + esp_err_t ret = ESP_OK; + + /* Parse command line arguments */ + int nerrors = arg_parse(argc, argv, (void**)&argtable); + EXIT_ON_FALSE(argtable.help->count == 0, ESP_OK, arg_print_help((void**)&argtable, argv[0])); + EXIT_ON_FALSE(nerrors == 0, ESP_ERR_INVALID_ARG, arg_print_errors(stdout, argtable.end, argv[0])); + + /* Handle set / get */ + if (argc > 1) { + uint32_t channel_mask = 0; + if (argtable.channel->count > 0) { + channel_mask = argtable.channel->val[0]; + } + if (argtable.mask->count == 0) { + channel_mask = 1 << channel_mask; + } + EXIT_ON_ERROR(esp_zb_set_channel_mask(channel_mask)); + /* Also set the channels for BDB commissioning */ + EXIT_ON_ERROR(esp_zb_set_primary_network_channel_set(channel_mask)); + EXIT_ON_ERROR(esp_zb_set_secondary_network_channel_set(channel_mask)); + } else { + cli_output("Allowed Channel: 0x%08" PRIx32 "\n", esp_zb_get_channel_mask()); + cli_output("Current Channel: %d\n", esp_zb_get_current_channel()); + } + +exit: + ESP_ZB_CLI_FREE_ARGSTRUCT(&argtable); + return ret; +} + +/* Sub-commands of `network` */ + +static esp_err_t cli_nwk_type(esp_zb_cli_cmd_t *self, int argc, char **argv) +{ + struct { + arg_str_t *role; + arg_lit_t *help; + arg_end_t *end; + } argtable = { + .role = arg_str0(NULL, NULL, "", "set the network type. Default: c"), + .help = arg_lit0(NULL, "help", "Print this help message"), + .end = arg_end(2), + }; + esp_err_t ret = ESP_OK; + + /* Parse command line arguments */ + int nerrors = arg_parse(argc, argv, (void**)&argtable); + EXIT_ON_FALSE(argtable.help->count == 0, ESP_OK, arg_print_help((void**)&argtable, argv[0])); + EXIT_ON_FALSE(nerrors == 0, ESP_ERR_INVALID_ARG, arg_print_errors(stdout, argtable.end, argv[0])); + + if (argtable.role->count > 0) { + switch (argtable.role->sval[0][0]){ + case 'd': + s_bdb_ctx.distributed = true; + break; + case 'c': + s_bdb_ctx.distributed = false; + break; + default: + EXIT_ON_ERROR(ESP_ERR_INVALID_ARG, cli_output("unknown network type: %s\n", argtable.role->sval[0])); + break; + } + } else { + cli_output("setting: %s\n", s_bdb_ctx.distributed ? "distributed" : "centralized"); + if (esp_zb_bdb_dev_joined()) { + cli_output("current on: %s\n", esp_zb_network_is_distributed() ? "distributed" : "centralized"); + } + } + +exit: + ESP_ZB_CLI_FREE_ARGSTRUCT(&argtable); + return ret; +} + +static esp_err_t cli_nwk_legacy(esp_zb_cli_cmd_t *self, int argc, char **argv) +{ + esp_err_t ret = ESP_OK; + + if (argc < 2) { + /* TODO: Add support to get the SDK status. */ + cli_output_line("Get the legacy support status is not supported"); + ret = ESP_ERR_NOT_SUPPORTED; + } else if (argc > 3) { + ret = ESP_ERR_INVALID_ARG; + } else if (!strcmp(argv[1], "enable")) { + esp_zb_secur_link_key_exchange_required_set(true); + } else if (!strcmp(argv[1], "disable")) { + esp_zb_secur_link_key_exchange_required_set(false); + } else { + cli_output("Invalid option: %s, use \"enable\" or \"disable\"\n", argv[1]); + ret = ESP_ERR_INVALID_ARG; + } + + return ret; +} + +static esp_err_t cli_nwk_key(esp_zb_cli_cmd_t *self, int argc, char **argv) +{ + struct { + arg_hex_t *key; + arg_lit_t *help; + arg_end_t *end; + } argtable = { + .key = arg_hexn(NULL, NULL, "", 0, 1, "network key, in HEX format"), + .help = arg_lit0(NULL, "help", "Print this help message"), + .end = arg_end(2), + }; + esp_err_t ret = ESP_OK; + + /* Parse command line arguments */ + int nerrors = arg_parse(argc, argv, (void**)&argtable); + EXIT_ON_FALSE(argtable.help->count == 0, ESP_OK, arg_print_help((void**)&argtable, argv[0])); + EXIT_ON_FALSE(nerrors == 0, ESP_ERR_INVALID_ARG, arg_print_errors(stdout, argtable.end, argv[0])); + + if (argtable.key->count > 0) { + EXIT_ON_FALSE(argtable.key->hsize[0] == 16, ESP_ERR_INVALID_ARG); + EXIT_ON_ERROR(esp_zb_secur_network_key_set(argtable.key->hval[0])); + } else { + uint8_t key[ESP_ZB_CCM_KEY_SIZE]; + EXIT_ON_ERROR(esp_zb_secur_primary_network_key_get(key), cli_output_line("Fail to get network key.")); + cli_output_buffer(key, ESP_ZB_CCM_KEY_SIZE); + } + +exit: + arg_hex_free(argtable.key); + ESP_ZB_CLI_FREE_ARGSTRUCT(&argtable); + return ret; +} + +static esp_err_t cli_nwk_childmax(esp_zb_cli_cmd_t *self, int argc, char **argv) +{ + struct { + arg_u8_t *childmax; + arg_lit_t *help; + arg_end_t *end; + } argtable = { + .childmax = arg_u8n(NULL, NULL, "", 0, 1, "max children number"), + .help = arg_lit0(NULL, "help", "Print this help message"), + .end = arg_end(2), + }; + esp_err_t ret = ESP_OK; + + /* Parse command line arguments */ + int nerrors = arg_parse(argc, argv, (void**)&argtable); + EXIT_ON_FALSE(argtable.help->count == 0, ESP_OK, arg_print_help((void**)&argtable, argv[0])); + EXIT_ON_FALSE(nerrors == 0, ESP_ERR_INVALID_ARG, arg_print_errors(stdout, argtable.end, argv[0])); + + if (argtable.childmax->count > 0) { + esp_zb_nwk_set_max_children(argtable.childmax->val[0]); + } else { + cli_output("%d\n", esp_zb_nwk_get_max_children()); + } + +exit: + ESP_ZB_CLI_FREE_ARGSTRUCT(&argtable); + return ret; +} + +static esp_err_t cli_nwk_open(esp_zb_cli_cmd_t *self, int argc, char **argv) +{ + struct { + arg_u8_t *timeout; + arg_end_t *end; + } argtable = { + .timeout = arg_u8n("t", NULL, "", 1, 1, "timeouts in second for opening"), + .end = arg_end(2), + }; + esp_err_t ret = ESP_OK; + + /* Parse command line arguments */ + EXIT_ON_FALSE(argc > 1, ESP_OK, arg_print_help((void**)&argtable, argv[0])); + int nerrors = arg_parse(argc, argv, (void**)&argtable); + EXIT_ON_FALSE(nerrors == 0, ESP_ERR_INVALID_ARG, arg_print_errors(stdout, argtable.end, argv[0])); + + EXIT_ON_ERROR(esp_zb_bdb_open_network(argtable.timeout->val[0]), cli_output_line("Fail to open network.")); + +exit: + ESP_ZB_CLI_FREE_ARGSTRUCT(&argtable); + return ret; +} + +static esp_err_t cli_nwk_close(esp_zb_cli_cmd_t *self, int argc, char **argv) +{ + esp_err_t ret = ESP_OK; + + EXIT_ON_FALSE(argc == 1, ESP_ERR_INVALID_ARG); + + EXIT_ON_ERROR(esp_zb_bdb_close_network(), cli_output_line("Fail to close network.")); + +exit: + return ret; +} + +static void cli_output_active_scan_results(esp_zb_network_descriptor_t *nwk_descriptor, uint16_t count) +{ + static const char *titles[] = {"Chnl", "PanID", "ExtPanID", "Status", "Cap"}; + static const uint8_t widths[] = {4, 8, 20, 7, 3}; + + cli_output_table_header(ARRAY_SIZE(widths), titles, widths); + for (int i = 0; i < count; i++) { + cli_output("| %2d | 0x%04hx | 0x%016" PRIx64 " | %5s | %c%c|\n", + nwk_descriptor[i].logic_channel, + nwk_descriptor[i].short_pan_id, + *(uint64_t *)nwk_descriptor[i].extended_pan_id, + nwk_descriptor[i].permit_joining ? "Open" : "Close", + nwk_descriptor[i].router_capacity ? 'R' : ' ', + nwk_descriptor[i].end_device_capacity ? 'E' : ' '); + } +} + +static void cli_nwk_scan_cb(esp_zb_zdp_status_t status, uint8_t count, esp_zb_network_descriptor_t *nwk_descriptor) +{ + esp_err_t ret = ESP_OK; + EXIT_ON_FALSE(status == ESP_ZB_ZDP_STATUS_SUCCESS, ESP_FAIL, + cli_output("ED Scan failed: %d\n", status)); + + cli_output_active_scan_results(nwk_descriptor, count); + +exit: + esp_zb_console_notify_result(ret); +} + +static esp_err_t cli_nwk_scan(esp_zb_cli_cmd_t *self, int argc, char **argv) +{ + struct { + arg_lit_t *mask; + arg_u32_t *channel; + arg_u8_t *duration; + arg_end_t *end; + } argtable = { + .mask = arg_lit0("m", NULL, "use channel mask"), + .channel = arg_u32n(NULL, NULL, "", 1, 1, "channel (mask) to perform scan on"), + .duration = arg_u8n("t", NULL, "", 0, 1, "timeout in sec of scan, default: 1s"), + .end = arg_end(2), + }; + esp_err_t ret = ESP_ERR_NOT_FINISHED; + + /* Parse command line arguments */ + EXIT_ON_FALSE(argc > 1, ESP_OK, arg_print_help((void**)&argtable, argv[0])); + int nerrors = arg_parse(argc, argv, (void**)&argtable); + EXIT_ON_FALSE(nerrors == 0, ESP_ERR_INVALID_ARG, arg_print_errors(stdout, argtable.end, argv[0])); + + uint32_t channel_mask = 0; + if (argtable.channel->count > 0) { + channel_mask = argtable.channel->val[0]; + } + if (argtable.mask->count == 0) { + channel_mask = 1 << channel_mask; + } + esp_zb_zdo_active_scan_request(channel_mask, + argtable.duration->count > 0 ? argtable.duration->val[0] : 1, + cli_nwk_scan_cb); + +exit: + ESP_ZB_CLI_FREE_ARGSTRUCT(&argtable); + return ret; +} + +static void cli_output_ed_scan_results(esp_zb_energy_detect_channel_info_t *channel_info, uint16_t count) +{ + static const char *titles[] = {"Chnl", "RSSI"}; + static const uint8_t widths[] = {4, 6}; + + cli_output_table_header(ARRAY_SIZE(widths), titles, widths); + for (int i = 0; i < count; i++) { + cli_output("| %2d | %4d |\n", channel_info[i].channel_number, channel_info[i].energy_detected); + } +} + +static void cli_nwk_ed_scan_cb(esp_zb_zdp_status_t status, uint16_t count, + esp_zb_energy_detect_channel_info_t *channel_info) +{ + esp_err_t ret = ESP_OK; + EXIT_ON_FALSE(status == ESP_ZB_ZDP_STATUS_SUCCESS, ESP_FAIL, + cli_output("ED Scan failed: %d\n", status)); + + cli_output_ed_scan_results(channel_info, count); + +exit: + esp_zb_console_notify_result(ret); +} + +static esp_err_t cli_nwk_ed_scan(esp_zb_cli_cmd_t *self, int argc, char **argv) +{ + struct { + arg_lit_t *mask; + arg_u32_t *channel; + arg_u8_t *duration; + arg_end_t *end; + } argtable = { + .mask = arg_lit0("m", NULL, "use channel mask"), + .channel = arg_u32n(NULL, NULL, "", 1, 1, "channel (mask) to perform scan on"), + .duration = arg_u8n("t", NULL, "", 0, 1, "timeout in sec of scan, default: 1s"), + .end = arg_end(2), + }; + esp_err_t ret = ESP_ERR_NOT_FINISHED; + + /* Parse command line arguments */ + EXIT_ON_FALSE(argc > 1, ESP_OK, arg_print_help((void**)&argtable, argv[0])); + int nerrors = arg_parse(argc, argv, (void**)&argtable); + EXIT_ON_FALSE(nerrors == 0, ESP_ERR_INVALID_ARG, arg_print_errors(stdout, argtable.end, argv[0])); + + uint32_t channel_mask = 0; + if (argtable.channel->count > 0) { + channel_mask = argtable.channel->val[0]; + } + if (argtable.mask->count == 0) { + channel_mask = 1 << channel_mask; + } + esp_zb_zdo_energy_detect_request(channel_mask, + argtable.duration->count > 0 ? argtable.duration->val[0] : 1, + cli_nwk_ed_scan_cb); + +exit: + ESP_ZB_CLI_FREE_ARGSTRUCT(&argtable); + return ret; +} + +/* Sub-commands of `ic` */ + +static esp_err_t cli_ic_add(esp_zb_cli_cmd_t *self, int argc, char **argv) +{ + struct { + arg_addr_t *add_addr; + arg_hex_t *ic_code; + arg_end_t *end; + } argtable = { + .add_addr = arg_addrn(NULL, NULL, "", 1, 1, "IEEE address of the device"), + .ic_code = arg_hexn(NULL, NULL, "", 1, 1, "install code"), + .end = arg_end(2), + }; + esp_err_t ret = ESP_OK; + uint8_t ic_type; + + /* Parse command line arguments */ + EXIT_ON_FALSE(argc > 1, ESP_OK, arg_print_help((void**)&argtable, argv[0])); + int nerrors = arg_parse(argc, argv, (void**)&argtable); + EXIT_ON_FALSE(nerrors == 0, ESP_ERR_INVALID_ARG, arg_print_errors(stdout, argtable.end, argv[0])); + + EXIT_ON_FALSE(esp_zb_is_started(), ESP_ERR_INVALID_SIZE, cli_output_line("Please start Zigbee stack first")); + EXIT_ON_FALSE(argtable.add_addr->addr->addr_type == ESP_ZB_ZCL_ADDR_TYPE_IEEE, ESP_ERR_INVALID_ARG, + cli_output_line("IEEE address is required.")); + + switch (argtable.ic_code->hsize[0]) { + case 8: ic_type = ESP_ZB_IC_TYPE_48; break; + case 10: ic_type = ESP_ZB_IC_TYPE_64; break; + case 14: ic_type = ESP_ZB_IC_TYPE_96; break; + case 18: ic_type = ESP_ZB_IC_TYPE_128; break; + default: + EXIT_NOW(ret = ESP_ERR_INVALID_ARG; cli_output("Install code incorrect length %d\n", argtable.ic_code->hsize[0])); + break; + } + + EXIT_ON_ERROR(esp_zb_secur_ic_add(argtable.add_addr->addr[0].u.ieee_addr, ic_type, argtable.ic_code->hval[0]), + cli_output_line("Fail to add install code.")); + +exit: + arg_hex_free(argtable.ic_code); + ESP_ZB_CLI_FREE_ARGSTRUCT(&argtable); + return ret; +} + +static esp_err_t cli_ic_remove(esp_zb_cli_cmd_t *self, int argc, char **argv) +{ + struct { + arg_addr_t *add_addr; + arg_end_t *end; + } argtable = { + .add_addr = arg_addrn(NULL, NULL, "", 1, 1, "IEEE address of the device"), + .end = arg_end(2), + }; + esp_err_t ret = ESP_OK; + + /* Parse command line arguments */ + EXIT_ON_FALSE(argc > 1, ESP_OK, arg_print_help((void**)&argtable, argv[0])); + int nerrors = arg_parse(argc, argv, (void**)&argtable); + EXIT_ON_FALSE(nerrors == 0, ESP_ERR_INVALID_ARG, arg_print_errors(stdout, argtable.end, argv[0])); + + EXIT_ON_FALSE(argtable.add_addr->addr->addr_type == ESP_ZB_ZCL_ADDR_TYPE_IEEE, ESP_ERR_INVALID_ARG, + cli_output_line("IEEE address is required.")); + EXIT_ON_ERROR(esp_zb_secur_ic_remove_req(argtable.add_addr->addr->u.ieee_addr), + cli_output_line("Fail to remove install code.")); + +exit: + ESP_ZB_CLI_FREE_ARGSTRUCT(&argtable); + return ret; +} + +static esp_err_t cli_ic_set(esp_zb_cli_cmd_t *self, int argc, char **argv) +{ + struct { + arg_hex_t *ic_code; + arg_end_t *end; + } argtable = { + .ic_code = arg_hexn(NULL, NULL, "", 1, 1, "install code"), + .end = arg_end(2), + }; + esp_err_t ret = ESP_OK; + uint8_t ic_type; + + /* Parse command line arguments */ + EXIT_ON_FALSE(argc > 1, ESP_OK, arg_print_help((void**)&argtable, argv[0])); + int nerrors = arg_parse(argc, argv, (void**)&argtable); + EXIT_ON_FALSE(nerrors == 0, ESP_ERR_INVALID_ARG, arg_print_errors(stdout, argtable.end, argv[0])); + + switch (argtable.ic_code->hsize[0]) { + case 8: ic_type = ESP_ZB_IC_TYPE_48; break; + case 10: ic_type = ESP_ZB_IC_TYPE_64; break; + case 14: ic_type = ESP_ZB_IC_TYPE_96; break; + case 18: ic_type = ESP_ZB_IC_TYPE_128; break; + default: + EXIT_NOW(ret = ESP_ERR_INVALID_ARG; cli_output("Install code incorrect length %d\n", argtable.ic_code->hsize[0])); + break; + } + + EXIT_ON_ERROR(esp_zb_secur_ic_set(ic_type, argtable.ic_code->hval[0]), cli_output_line("Fail to set install code\n")); + +exit: + arg_hex_free(argtable.ic_code); + ESP_ZB_CLI_FREE_ARGSTRUCT(&argtable); + return ret; +} + +static esp_err_t cli_ic_get(esp_zb_cli_cmd_t *self, int argc, char **argv) +{ + esp_err_t ret = ESP_OK; + EXIT_ON_FALSE(argc == 1, ESP_ERR_INVALID_ARG); + + uint8_t ic_size[] = { 8, 10, 14, 18 }; + uint8_t ic_type = ESP_ZB_IC_TYPE_MAX; + uint8_t *ic = esp_zb_secur_ic_get(&ic_type); + + if (ic == NULL) { + cli_output_line("No install code configured."); + } else { + cli_output_buffer(ic, ic_size[ic_type]); + } + +exit: + return ret; +} + +/* Sub-commands of `bdb_comm` */ + +static esp_err_t cli_bdb_channel(esp_zb_cli_cmd_t *self, int argc, char **argv) +{ + struct { + arg_lit_t *mask; + arg_u32_t *primary_chn; + arg_u32_t *secondary_chn; + arg_lit_t *help; + arg_end_t *end; + } argtable = { + .mask = arg_lit0("m", NULL, "use channel mask"), + .primary_chn = arg_u32n(NULL, NULL, "", 0, 1, "primary channel (mask)"), + .secondary_chn = arg_u32n(NULL, NULL, "", 0, 1, "secondary channel (mask)"), + .help = arg_lit0(NULL, "help", "Print this help message"), + .end = arg_end(2), + }; + esp_err_t ret = ESP_OK; + + /* Parse command line arguments */ + int nerrors = arg_parse(argc, argv, (void**)&argtable); + EXIT_ON_FALSE(argtable.help->count == 0, ESP_OK, arg_print_help((void**)&argtable, argv[0])); + EXIT_ON_FALSE(nerrors == 0, ESP_ERR_INVALID_ARG, arg_print_errors(stdout, argtable.end, argv[0])); + + /* Handle set / get */ + if (argc > 1) { + uint32_t primary_channel_mask = 0; + uint32_t secondary_channel_mask = 0; + if (argtable.primary_chn->count > 0) { + primary_channel_mask = argtable.primary_chn->val[0]; + } + if (argtable.secondary_chn->count > 0) { + secondary_channel_mask = argtable.secondary_chn->val[0]; + } + if (argtable.mask->count == 0) { + primary_channel_mask = 1 << primary_channel_mask; + secondary_channel_mask = 1 << secondary_channel_mask; + } + EXIT_ON_ERROR(esp_zb_set_primary_network_channel_set(primary_channel_mask)); + EXIT_ON_ERROR(esp_zb_set_secondary_network_channel_set(secondary_channel_mask)); + } else { + cli_output("Primary Channel: 0x%08" PRIx32 "\n", esp_zb_get_primary_network_channel_set()); + cli_output("Secondary Channel: 0x%08" PRIx32 "\n", esp_zb_get_secondary_network_channel_set()); + } + +exit: + ESP_ZB_CLI_FREE_ARGSTRUCT(&argtable); + return ret; +} + +static esp_err_t cli_bdb_mode(esp_zb_cli_cmd_t *self, int argc, char **argv) +{ + esp_err_t ret = ESP_OK; + + EXIT_ON_FALSE(argc == 1, ESP_ERR_INVALID_ARG); + + cli_output("0x%02hx\n", (uint8_t)esp_zb_get_bdb_commissioning_mode()); + +exit: + return ret; +} + +static esp_err_t cli_bdb_start(esp_zb_cli_cmd_t *self, int argc, char **argv) +{ + struct { + arg_str_t *mode; + arg_rem_t *rem_mode; + arg_end_t *end; + } argtable = { + .mode = arg_strn(NULL, NULL, "", 1, 4, "bdb commissioning mode"), + .rem_mode = arg_rem("", "steer|form|bind|initiator|target"), + .end = arg_end(2), + }; + esp_err_t ret = ESP_OK; + + /* Parse command line arguments */ + EXIT_ON_FALSE(argc > 1, ESP_OK, arg_print_help((void**)&argtable, argv[0])); + int nerrors = arg_parse(argc, argv, (void**)&argtable); + EXIT_ON_FALSE(nerrors == 0, ESP_ERR_INVALID_ARG, arg_print_errors(stdout, argtable.end, argv[0])); + + esp_zb_bdb_commissioning_mode_t bdb_comm_mode = ESP_ZB_BDB_MODE_INITIALIZATION; + + if (argtable.mode->count > 0) { + for (int i = 0; i < argtable.mode->count; i++) { + if (!strcmp(argtable.mode->sval[i], "steer")) { + if (s_bdb_ctx.distributed) { + esp_zb_enable_joining_to_distributed(true); + } + bdb_comm_mode = bdb_comm_mode | ESP_ZB_BDB_NETWORK_STEERING; + } else if (!strcmp(argtable.mode->sval[i], "form")) { + if (s_bdb_ctx.distributed) { + esp_zb_enable_distributed_network(true); + esp_zb_zdo_setup_network_as_distributed(); + esp_zb_tc_policy_set_distributed_security(true); + } + bdb_comm_mode = bdb_comm_mode | ESP_ZB_BDB_NETWORK_FORMATION; + } else if (!strcmp(argtable.mode->sval[i], "bind")) { + bdb_comm_mode = bdb_comm_mode | ESP_ZB_BDB_FINDING_N_BINDING; + } else if (!strcmp(argtable.mode->sval[i], "initiator")) { + uint32_t channel_mask = esp_zb_get_primary_network_channel_set(); + bdb_comm_mode = bdb_comm_mode | ESP_ZB_BDB_TOUCHLINK_COMMISSIONING; + if (channel_mask != 0) { + esp_zb_zdo_touchlink_set_nwk_channel(__builtin_ctz(channel_mask)); + } else { + cli_output("Invalid primary channel: 0x%08" PRIx32 "\n", channel_mask); + } + } else if (!strcmp(argtable.mode->sval[i], "target")) { + bdb_comm_mode = ESP_ZB_BDB_TOUCHLINK_TARGET; + esp_zb_set_channel_mask(esp_zb_get_primary_network_channel_set()); + } else { + cli_output("Skip %s bdb commissioning mode: %s\n", "unknown", argtable.mode->sval[i]); + } + } + } + + if (!esp_zb_is_started()) { + EXIT_ON_ERROR(esp_zb_start(false), cli_output_line("Fail to start Zigbee stack.")); + } + + EXIT_ON_ERROR(esp_zb_bdb_start_top_level_commissioning(bdb_comm_mode), + cli_output_line("Fail to start bdb top level commissioning.")); + +exit: + ESP_ZB_CLI_FREE_ARGSTRUCT(&argtable); + return ret; +} + +static esp_err_t cli_bdb_cancel(esp_zb_cli_cmd_t *self, int argc, char **argv) +{ + struct { + arg_str_t *mode; + arg_end_t *end; + } argtable = { + .mode = arg_strn(NULL, NULL, "", 1, 1, "commissioning mode to cancel"), + .end = arg_end(2), + }; + esp_err_t ret = ESP_OK; + + /* Parse command line arguments */ + EXIT_ON_FALSE(argc > 1, ESP_OK, arg_print_help((void**)&argtable, argv[0])); + int nerrors = arg_parse(argc, argv, (void**)&argtable); + EXIT_ON_FALSE(nerrors == 0, ESP_ERR_INVALID_ARG, arg_print_errors(stdout, argtable.end, argv[0])); + + if (argtable.mode->count > 0) { + if (!strcmp(argtable.mode->sval[0], "steer")) { + ret = esp_zb_bdb_cancel_steering(); + } else if (!strcmp(argtable.mode->sval[0], "form")) { + ret = esp_zb_bdb_cancel_formation(); + } else if (!strcmp(argtable.mode->sval[0], "target")) { + ret = esp_zb_bdb_cancel_touchlink_target(); + } else { + cli_output("%s mode for cancelling:%s\n", "unsupported", argtable.mode->sval[0]); + } + } + +exit: + ESP_ZB_CLI_FREE_ARGSTRUCT(&argtable); + return ret; +} + +/* Sub-commands of `tl` */ + +static esp_err_t cli_tl_timeout(esp_zb_cli_cmd_t *self, int argc, char **argv) +{ + struct { + arg_u32_t *timeout; + arg_lit_t *help; + arg_end_t *end; + } argtable = { + .timeout = arg_u32n(NULL, NULL, "", 0, 1, "touchlink target timeout, in second"), + .help = arg_lit0(NULL, "help", "Print this help message"), + .end = arg_end(2), + }; + esp_err_t ret = ESP_OK; + + /* Parse command line arguments */ + int nerrors = arg_parse(argc, argv, (void**)&argtable); + EXIT_ON_FALSE(argtable.help->count == 0, ESP_OK, arg_print_help((void**)&argtable, argv[0])); + EXIT_ON_FALSE(nerrors == 0, ESP_ERR_INVALID_ARG, arg_print_errors(stdout, argtable.end, argv[0])); + + if (argtable.timeout->count > 0) { + esp_zb_zdo_touchlink_target_set_timeout(argtable.timeout->val[0]); + } else { + EXIT_ON_ERROR(ESP_ERR_NOT_SUPPORTED, cli_output_line("Get value is not supported.")); + } + +exit: + ESP_ZB_CLI_FREE_ARGSTRUCT(&argtable); + return ret; +} + +static esp_err_t cli_tl_rssi(esp_zb_cli_cmd_t *self, int argc, char **argv) +{ + struct { + arg_int_t *rssi; + arg_lit_t *help; + arg_end_t *end; + } argtable = { + .rssi = arg_intn(NULL, NULL, "", 0, 1, "touchlink target rssi threshold"), + .help = arg_lit0(NULL, "help", "Print this help message"), + .end = arg_end(2), + }; + esp_err_t ret = ESP_OK; + + /* Parse command line arguments */ + int nerrors = arg_parse(argc, argv, (void**)&argtable); + EXIT_ON_FALSE(argtable.help->count == 0, ESP_OK, arg_print_help((void**)&argtable, argv[0])); + EXIT_ON_FALSE(nerrors == 0, ESP_ERR_INVALID_ARG, arg_print_errors(stdout, argtable.end, argv[0])); + + if (argtable.rssi->count > 0) { + esp_zb_zdo_touchlink_set_rssi_threshold(argtable.rssi->ival[0]); + } else { + cli_output("%d\n", esp_zb_zdo_touchlink_get_rssi_threshold()); + } + +exit: + ESP_ZB_CLI_FREE_ARGSTRUCT(&argtable); + return ret; +} + +static esp_err_t cli_tl_key(esp_zb_cli_cmd_t *self, int argc, char **argv) +{ + struct { + arg_hex_t *key; + arg_lit_t *help; + arg_end_t *end; + } argtable = { + .key = arg_hexn(NULL, NULL, "", 0, 1, "network key, in HEX format"), + .help = arg_lit0(NULL, "help", "Print this help message"), + .end = arg_end(2), + }; + esp_err_t ret = ESP_OK; + + /* Parse command line arguments */ + int nerrors = arg_parse(argc, argv, (void**)&argtable); + EXIT_ON_FALSE(argtable.help->count == 0, ESP_OK, arg_print_help((void**)&argtable, argv[0])); + EXIT_ON_FALSE(nerrors == 0, ESP_ERR_INVALID_ARG, arg_print_errors(stdout, argtable.end, argv[0])); + + if (argtable.key->count > 0) { + EXIT_ON_FALSE(argtable.key->hsize[0] == 16, ESP_ERR_INVALID_ARG); + esp_zb_zdo_touchlink_set_master_key(argtable.key->hval[0]); + } else { + EXIT_ON_ERROR(ESP_ERR_NOT_SUPPORTED, cli_output_line("Get value is not supported.")); + } + +exit: + arg_hex_free(argtable.key); + ESP_ZB_CLI_FREE_ARGSTRUCT(&argtable); + return ret; +} + +DECLARE_ESP_ZB_CLI_CMD(role, cli_role,, "Get/Set the Zigbee role of a device"); +DECLARE_ESP_ZB_CLI_CMD(panid, cli_panid,, "Get/Set the (extended) PAN ID of the node"); +DECLARE_ESP_ZB_CLI_CMD(address, cli_address,, "Get/Set the (extended) address of the node"); +DECLARE_ESP_ZB_CLI_CMD(channel, cli_channel,, "Get/Set 802.15.4 channels for network"); +DECLARE_ESP_ZB_CLI_CMD_WITH_SUB(network, "Network configuration", + ESP_ZB_CLI_SUBCMD(type, cli_nwk_type, "Get/Set the network type"), + ESP_ZB_CLI_SUBCMD(key, cli_nwk_key, "Get/Set the network key"), + ESP_ZB_CLI_SUBCMD(legacy, cli_nwk_legacy, "Enable/Disable legacy device support"), + ESP_ZB_CLI_SUBCMD(childmax, cli_nwk_childmax, "Get/Set max children number"), + ESP_ZB_CLI_SUBCMD(open, cli_nwk_open, "Open local network"), + ESP_ZB_CLI_SUBCMD(close, cli_nwk_close, "Close local network"), + ESP_ZB_CLI_SUBCMD(scan, cli_nwk_scan, "Scan for network"), + ESP_ZB_CLI_SUBCMD(ed_scan, cli_nwk_ed_scan, "Scan for energy detect on channels"), +); +DECLARE_ESP_ZB_CLI_CMD_WITH_SUB(ic, "Install code configuration", + ESP_ZB_CLI_SUBCMD(add, cli_ic_add, "Add install code for a device"), + ESP_ZB_CLI_SUBCMD(remove, cli_ic_remove, "Remove install code for a device"), + ESP_ZB_CLI_SUBCMD(set, cli_ic_set, "Set install code on device"), + ESP_ZB_CLI_SUBCMD(get, cli_ic_get, "Get install code on device"), +); +DECLARE_ESP_ZB_CLI_CMD_WITH_SUB(bdb_comm, "Perform BDB Commissioning", + ESP_ZB_CLI_SUBCMD(channel, cli_bdb_channel, "Get/Set channels to perform commissioning"), + ESP_ZB_CLI_SUBCMD(mode, cli_bdb_mode, "Get current mode of commissioning"), + ESP_ZB_CLI_SUBCMD(start, cli_bdb_start, "Start bdb commissioning"), + ESP_ZB_CLI_SUBCMD(cancel, cli_bdb_cancel, "Cancel commissioning process"), +); +DECLARE_ESP_ZB_CLI_CMD_WITH_SUB(tl, "TouchLink configuration", + ESP_ZB_CLI_SUBCMD(timeout, cli_tl_timeout, "Get/Set touchlink target timeout"), + ESP_ZB_CLI_SUBCMD(rssi, cli_tl_rssi, "Get/Set touchlink target rssi threshold"), + ESP_ZB_CLI_SUBCMD(key, cli_tl_key, "Get/Set touchlink master key"), +); \ No newline at end of file diff --git a/components/esp-zigbee-console/src/cli_cmd_misc.c b/components/esp-zigbee-console/src/cli_cmd_misc.c new file mode 100644 index 0000000..90ac53c --- /dev/null +++ b/components/esp-zigbee-console/src/cli_cmd_misc.c @@ -0,0 +1,64 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include "esp_check.h" +#include "esp_zigbee_core.h" + +#include "esp_zigbee_console.h" +#include "cli_cmd.h" + +#define TAG "cli_cmd_misc" + +static esp_err_t cli_factoryreset(esp_zb_cli_cmd_t *self, int argc, char *argv[]) +{ + if (argc > 1) { + return ESP_ERR_INVALID_ARG; + } + cli_output("Erasing NVRAM of Zigbee stack ... "); + esp_zb_zcl_reset_nvram_to_factory_default(); + cli_output_line("Done"); + cli_output_line("Reboot the device"); + esp_restart(); + + /* Never reached, esp_restart are not expect to return. */ + return ESP_FAIL; +} + +static esp_err_t cli_reboot(esp_zb_cli_cmd_t *self, int argc, char *argv[]) +{ + if (argc > 1) { + return ESP_ERR_INVALID_ARG; + } + cli_output_line("Reboot the device"); + esp_restart(); + + /* Never reached, esp_restart are not expect to return. */ + return ESP_FAIL; +} + +static esp_err_t cli_start(esp_zb_cli_cmd_t *self, int argc, char *argv[]) +{ + if (argc > 1) { + return ESP_ERR_INVALID_ARG; + } + cli_output_line("Start Zigbee stack"); + + return esp_zb_start(false); +} + +static esp_err_t cli_radio(esp_zb_cli_cmd_t *self, int argc, char *argv[]) +{ + return ESP_ERR_NOT_SUPPORTED; +} + +DECLARE_ESP_ZB_CLI_CMD(factoryreset, cli_factoryreset,, "Reset the device to factory new immediately"); +DECLARE_ESP_ZB_CLI_CMD(reboot, cli_reboot,, "Reboot the device immediately"); +DECLARE_ESP_ZB_CLI_CMD(radio, cli_radio,, "Enable/Disable the radio"); +DECLARE_ESP_ZB_CLI_CMD(start, cli_start,, "Start Zigbee stack"); diff --git a/components/esp-zigbee-console/src/cli_cmd_zcl.c b/components/esp-zigbee-console/src/cli_cmd_zcl.c new file mode 100644 index 0000000..57ac8cc --- /dev/null +++ b/components/esp-zigbee-console/src/cli_cmd_zcl.c @@ -0,0 +1,958 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include "esp_check.h" +#include "esp_zigbee_core.h" + +#include "esp_zigbee_console.h" +#include "cli_cmd.h" +#include "cmdline_parser.h" +#include "zb_data/zcl.h" + +#define TAG "cli_cmd_zcl" + +static esp_err_t zcl_read_attr_resp_handler(const esp_zb_zcl_cmd_read_attr_resp_message_t *message) +{ + 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, "Read message: error status(%d)", message->info.status); + + ESP_LOGI(TAG, "Read response: endpoint(%d), cluster(0x%02x)", message->info.dst_endpoint, message->info.cluster); + + for (esp_zb_zcl_read_attr_resp_variable_t *variables = message->variables; variables != NULL; variables = variables->next) { + ESP_LOGI(TAG, "attribute(0x%02x), type(0x%x), data size(%d)", variables->attribute.id, variables->attribute.data.type, variables->attribute.data.size); + ESP_LOG_BUFFER_HEXDUMP("", variables->attribute.data.value, variables->attribute.data.size, ESP_LOG_INFO); + } + return ESP_OK; +} + +static esp_err_t zcl_write_attr_resp_handler(const esp_zb_zcl_cmd_write_attr_resp_message_t *message) +{ + 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, "Write message: error status(%d)", message->info.status); + + ESP_LOGI(TAG, "Write response: endpoint(%d), cluster(0x%02x)", message->info.dst_endpoint, message->info.cluster); + + for (esp_zb_zcl_write_attr_resp_variable_t *variables = message->variables; variables != NULL; variables = variables->next) { + ESP_LOGI(TAG, "attribute(0x%02x), status(0x%x)",variables->attribute_id, variables->status); + } + return ESP_OK; +} + +static esp_err_t zcl_report_cfg_resp_handler(const esp_zb_zcl_cmd_config_report_resp_message_t *message) +{ + 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, "Config report message: error status(%d)", message->info.status); + + ESP_LOGI(TAG, "Config report response: endpoint(%d), cluster(0x%02x)", message->info.dst_endpoint, message->info.cluster); + + for (esp_zb_zcl_config_report_resp_variable_t *variables = message->variables; variables != NULL; variables = variables->next) { + ESP_LOGI(TAG, "attribute(0x%02x), status(0x%x), direction(%d)", variables->attribute_id, variables->status, variables->direction); + } + return ESP_OK; +} + +static esp_err_t zcl_read_report_cfg_resp_handler(const esp_zb_zcl_cmd_read_report_config_resp_message_t *message) +{ + 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, "Read report configure message: error status(%d)", message->info.status); + + ESP_LOGI(TAG, "Read report configure response: endpoint(%d), cluster(0x%02x), attribute(0x%02x)", message->info.dst_endpoint, message->info.cluster, message->attribute_id); + + return ESP_OK; +} + +static esp_err_t zcl_disc_attr_resp_handler(const esp_zb_zcl_cmd_discover_attributes_resp_message_t *message) +{ + 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, "Discover message: error status(%d)", message->info.status); + + ESP_LOGI(TAG, "Discover attribute response: endpoint(%d), cluster(0x%02x)", message->info.dst_endpoint, message->info.cluster); + + for (esp_zb_zcl_disc_attr_variable_t *variables = message->variables; variables != NULL; variables = variables->next) { + ESP_LOGI(TAG, "attribute(0x%02x), data type(0x%0x)", variables->attr_id, variables->data_type); + } + return ESP_OK; +} + +esp_err_t cli_zcl_core_action_handler(esp_zb_core_action_callback_id_t callback_id, const void *message) +{ + esp_err_t ret = ESP_ERR_NOT_SUPPORTED; + switch (callback_id) { + case ESP_ZB_CORE_CMD_READ_ATTR_RESP_CB_ID: + ret = zcl_read_attr_resp_handler((esp_zb_zcl_cmd_read_attr_resp_message_t *)message); + break; + case ESP_ZB_CORE_CMD_WRITE_ATTR_RESP_CB_ID: + ret = zcl_write_attr_resp_handler((esp_zb_zcl_cmd_write_attr_resp_message_t *)message); + break; + case ESP_ZB_CORE_CMD_REPORT_CONFIG_RESP_CB_ID: + ret = zcl_report_cfg_resp_handler((esp_zb_zcl_cmd_config_report_resp_message_t *)message); + break; + case ESP_ZB_CORE_CMD_READ_REPORT_CFG_RESP_CB_ID: + ret = zcl_read_report_cfg_resp_handler((esp_zb_zcl_cmd_read_report_config_resp_message_t *)message); + break; + case ESP_ZB_CORE_CMD_DISC_ATTR_RESP_CB_ID: + ret = zcl_disc_attr_resp_handler((esp_zb_zcl_cmd_discover_attributes_resp_message_t *)message); + break; + default: + break; + } + return ret; +} + +/* Implementation of ``dm add`` command */ + +typedef struct attribute_config_s { + uint16_t attr_id; + uint8_t attr_type; + uint8_t attr_access; + uint16_t manuf_code; + void* attr_value_p; +} attribute_config_t; + +typedef struct cluster_config_s { + uint16_t cluster_id; + uint16_t role_mask; + uint16_t manuf_code; + attribute_config_t *attr_cfg; +} cluster_config_t; + +typedef struct endpoint_config_s { + uint8_t ep_id; + uint8_t device_ver; + uint16_t profile_id; + uint16_t device_id; + cluster_config_t *cluster_cfg; +} endpoint_config_t; + +typedef struct cli_zcl_add_params_s { + bool force; + endpoint_config_t *endpoint_cfg; +} cli_zcl_add_params_t; + +static esp_err_t zcl_add_attribute(esp_zb_attribute_list_t *attr_list, attribute_config_t* attr_cfg, bool force) +{ + esp_err_t ret = ESP_OK; + EXIT_ON_FALSE(attr_cfg, ESP_OK); /* Exit with success, do thing */ + EXIT_ON_FALSE(attr_cfg->attr_id != 0xFFFF, ESP_ERR_INVALID_ARG); + EXIT_ON_FALSE(attr_list, ESP_ERR_INVALID_ARG); + EXIT_ON_FALSE(attr_cfg->attr_value_p, ESP_ERR_INVALID_ARG, cli_output_line("Invalid attribute value")); + + EXIT_ON_ERROR(force ? esp_zb_cluster_add_attr(attr_list, attr_list->cluster_id, attr_cfg->attr_id, + attr_cfg->attr_type, attr_cfg->attr_access, attr_cfg->attr_value_p) + : esp_zb_cluster_add_std_attr(attr_list, attr_cfg->attr_id, attr_cfg->attr_value_p), + cli_output_line("Fail to add attribute")); + +exit: + return ret; +} + +static esp_err_t zcl_add_cluster(esp_zb_cluster_list_t *cluster_list, cluster_config_t *cluster_cfg, bool force) +{ + esp_err_t ret = ESP_OK; + esp_zb_attribute_list_t *attr_list = NULL; + bool new_created = false; + + EXIT_ON_FALSE(cluster_cfg, ESP_OK); /* Exit with success, do thing */ + EXIT_ON_FALSE(cluster_cfg->cluster_id != 0xFFFF, ESP_ERR_INVALID_ARG); + EXIT_ON_FALSE(cluster_list, ESP_ERR_INVALID_ARG); + + attr_list = esp_zb_cluster_list_get_cluster(cluster_list, cluster_cfg->cluster_id, cluster_cfg->role_mask); + + if (attr_list == NULL) { + attr_list = force ? esp_zb_zcl_attr_list_create(cluster_cfg->cluster_id) + : esp_zb_cluster_create_default(cluster_cfg->cluster_id); + new_created = true; + } + + EXIT_ON_ERROR(zcl_add_attribute(attr_list, cluster_cfg->attr_cfg, force)); + + if (new_created) { + EXIT_ON_ERROR(esp_zb_cluster_register(cluster_list, attr_list, cluster_cfg->role_mask), + cli_output_line("Fail to add created cluster")); + } + + return ESP_OK; + +exit: + if (new_created && attr_list) { + free(attr_list); + } + return ret; +} + +static esp_err_t zcl_add_endpoint(esp_zb_ep_list_t *ep_list, endpoint_config_t* endpoint_cfg, bool force) +{ + esp_err_t ret = ESP_OK; + esp_zb_cluster_list_t *cluster_list = NULL; + bool new_created = false; + + EXIT_ON_FALSE(endpoint_cfg, ESP_OK); /* Exit with success, do thing */ + EXIT_ON_FALSE(endpoint_cfg->ep_id != 0 && endpoint_cfg->ep_id != 0xFF, ESP_ERR_INVALID_ARG); + EXIT_ON_FALSE(ep_list, ESP_ERR_INVALID_ARG); + + cluster_list = esp_zb_ep_list_get_ep(ep_list, endpoint_cfg->ep_id); + + if (cluster_list == NULL) { + cluster_list = esp_zb_zcl_cluster_list_create(); + new_created = true; + } + + EXIT_ON_ERROR(zcl_add_cluster(cluster_list, endpoint_cfg->cluster_cfg, force)); + + if (new_created) { + esp_zb_endpoint_config_t ep_config = { + .endpoint = endpoint_cfg->ep_id, + .app_profile_id = endpoint_cfg->profile_id, + .app_device_id = endpoint_cfg->device_id, + .app_device_version = endpoint_cfg->device_ver, + }; + EXIT_ON_ERROR(esp_zb_ep_list_add_ep(ep_list, cluster_list, ep_config), + cli_output_line("Fail to add created endpoint")); + } + + return ESP_OK; + +exit: + if (new_created && cluster_list) { + free(cluster_list); + } + return ret; +} + +static esp_err_t cli_dm_add_parse_params(int argc, char *argv[], cli_zcl_add_params_t *params) +{ + struct { + arg_lit_t *force; + arg_u8_t *ep_id; + arg_u16_t *cluster_id; + arg_u16_t *attr_id; + arg_devid_t *device_id; + arg_u8_t *device_ver; + arg_u16_t *profile_id; + arg_u16_t *manuf_code; + arg_str_t *role_mask; + arg_str_t *attr_access; + arg_u8_t *attr_type; + arg_hex_t *attr_value; + arg_end_t *end; + } argtable = { + .force = arg_lit0("f", "force", "force add, no checks"), + .ep_id = arg_u8n("e", "endpoint", "", 0, 1, "EID, id of the operating endpoint"), + .cluster_id = arg_u16n("c", "cluster", "", 0, 1, "CID, id of the operating cluster"), + .attr_id = arg_u16n("a", "attr", "", 0, 1, "AID, id of the operating attribute"), + .device_id = arg_devidn(NULL, "device", "", 0, 1, "set the device id (DID) of the endpoint"), + .device_ver = arg_u8n(NULL, "version", "", 0, 1, "set the device version of the endpoint"), + .profile_id = arg_u16n(NULL, "profile", "", 0, 1, "set the profile id (PID) of the endpoint"), + .manuf_code = arg_u16n(NULL, "manuf", "", 0, 1, "set CODE of the manufacture"), + .role_mask = arg_str0("r", "role", "", "set the role the cluster, default: S"), + .attr_access = arg_str0(NULL, "access", "", "set access of the attribute, default: RW"), + .attr_type = arg_u8n(NULL, "type", "", 0, 1, "TID, ZCL attribute type id"), + .attr_value = arg_hexn("v", "value", "", 0, 1, "set value of the attribute, raw data in HEX"), + .end = arg_end(2), + }; + + esp_err_t ret = ESP_OK; + endpoint_config_t *endpoint_cfg = NULL; + cluster_config_t *cluster_cfg = NULL; + attribute_config_t *attribute_cfg = NULL; + + /* Parse command line arguments */ + EXIT_ON_FALSE(argc > 1, ESP_OK, arg_print_help((void**)&argtable, argv[0])); + int nerrors = arg_parse(argc, argv, (void**)&argtable); + EXIT_ON_FALSE(nerrors == 0, ESP_ERR_INVALID_ARG, arg_print_errors(stdout, argtable.end, argv[0])); + + /* Endpoint related options */ + if (argtable.ep_id->count > 0) { + endpoint_cfg = malloc(sizeof(endpoint_config_t)); + endpoint_cfg->ep_id = argtable.ep_id->val[0]; + endpoint_cfg->profile_id = ESP_ZB_AF_HA_PROFILE_ID; + endpoint_cfg->device_id = ESP_ZB_HA_TEST_DEVICE_ID; + endpoint_cfg->device_ver = 0; + endpoint_cfg->cluster_cfg = NULL; + } + if (argtable.device_id->count > 0) { + if (endpoint_cfg) { + endpoint_cfg->device_id = argtable.device_id->val[0]; + } + } + if (argtable.device_ver->count > 0) { + if (endpoint_cfg) { + endpoint_cfg->device_ver = argtable.device_ver->val[0]; + } + } + if (argtable.profile_id->count > 0) { + if (endpoint_cfg) { + endpoint_cfg->profile_id = argtable.profile_id->val[0]; + } + } + + /* Cluster related options */ + if (argtable.cluster_id->count > 0) { + if (endpoint_cfg) { + cluster_cfg = malloc(sizeof(cluster_config_t)); + cluster_cfg->cluster_id = argtable.cluster_id->val[0]; + cluster_cfg->manuf_code = EZP_ZB_ZCL_CLUSTER_NON_MANUFACTURER_SPECIFIC; + cluster_cfg->role_mask = ESP_ZB_ZCL_CLUSTER_SERVER_ROLE; + cluster_cfg->attr_cfg = NULL; + endpoint_cfg->cluster_cfg = cluster_cfg; + } + } + if (argtable.role_mask->count > 0) { + if (cluster_cfg) { + switch (argtable.role_mask->sval[0][0]) { + case 'C': + case 'c': + cluster_cfg->role_mask = ESP_ZB_ZCL_CLUSTER_CLIENT_ROLE; + break; + case 'S': + case 's': + cluster_cfg->role_mask = ESP_ZB_ZCL_CLUSTER_SERVER_ROLE; + break; + default: + EXIT_ON_ERROR(ESP_ERR_INVALID_ARG, cli_output_line("zcl add: invalid argument to option --role")); + break; + } + } + } + + /* Attribute related options */ + if (argtable.attr_id->count > 0) { + if (cluster_cfg) { + attribute_cfg = malloc(sizeof(attribute_config_t)); + attribute_cfg->attr_id = argtable.attr_id->val[0]; + attribute_cfg->attr_type = ESP_ZB_ZCL_ATTR_TYPE_INVALID; + attribute_cfg->attr_access = ESP_ZB_ZCL_ATTR_ACCESS_READ_WRITE; + attribute_cfg->manuf_code = ESP_ZB_ZCL_ATTR_NON_MANUFACTURER_SPECIFIC; + attribute_cfg->attr_value_p = NULL; + cluster_cfg->attr_cfg = attribute_cfg; + } + } + if (argtable.attr_access->count > 0) { + if (attribute_cfg) { + EXIT_ON_ERROR(parse_attr_access(argtable.attr_access->sval[0], &attribute_cfg->attr_access), + cli_output_line("zcl add: invalid argument to option --access")); + } + } + if (argtable.attr_type->count > 0) { + if (attribute_cfg) { + attribute_cfg->attr_type = argtable.attr_type->val[0]; + } + } + if (argtable.attr_value->count > 0) { + if (attribute_cfg) { + attribute_cfg->attr_value_p = argtable.attr_value->hval[0]; + argtable.attr_value->hval[0] = NULL; + } + } + + /* Other common options */ + if (argtable.manuf_code->count > 0) { + if (cluster_cfg) { + cluster_cfg->manuf_code = argtable.manuf_code->val[0]; + } + if (attribute_cfg) { + attribute_cfg->manuf_code = argtable.manuf_code->val[0]; + } + } + if (argtable.force->count > 0) { + params->force = true; + } + +exit: + params->endpoint_cfg = endpoint_cfg; + arg_hex_free(argtable.attr_value); + ESP_ZB_CLI_FREE_ARGSTRUCT(&argtable); + return ret; +} + +static esp_err_t cli_dm_add(esp_zb_cli_cmd_t *self, int argc, char *argv[]) +{ + esp_err_t ret = ESP_OK; + cli_zcl_add_params_t params = { 0 }; + esp_zb_ep_list_t *ep_list = CLI_CTX().ep_list; + EXIT_ON_FALSE(CLI_CTX().ep_list, ESP_ERR_NOT_SUPPORTED, cli_output_line("Data model has been registered")); + + EXIT_ON_ERROR(cli_dm_add_parse_params(argc, argv, ¶ms)); + EXIT_ON_ERROR(zcl_add_endpoint(ep_list, params.endpoint_cfg, params.force)); + +exit: + if (params.endpoint_cfg) { + if (params.endpoint_cfg->cluster_cfg) { + if (params.endpoint_cfg->cluster_cfg->attr_cfg) { + if (params.endpoint_cfg->cluster_cfg->attr_cfg->attr_value_p) { + free(params.endpoint_cfg->cluster_cfg->attr_cfg->attr_value_p); + } + free(params.endpoint_cfg->cluster_cfg->attr_cfg); + } + free(params.endpoint_cfg->cluster_cfg); + } + free(params.endpoint_cfg); + } + return ret; +} + +/* Implementation of ``dm show`` command */ + +#define LIST_ITERATE(head, node) \ + for ((node) = (head); (node); (node) = (node)->next) + +static const char s_level_prefix[] = {'\0', ' ', ' ', ' ', '\0'}; + +static inline void zcl_attr_println(const esp_zb_zcl_attr_t *attr) +{ + cli_output("attr:0x%04x, type:0x%02x, access:0x%02x, manuf:0x%04x\n", attr->id, attr->type, attr->access, attr->manuf_code); +} + +static inline void zcl_cluster_println(const esp_zb_zcl_cluster_t *cluster) +{ + cli_output("cluster:0x%04x, %c, manuf:0x%04x\n", + cluster->cluster_id, cluster->role_mask == 1 ? 'S' : 'C', cluster->manuf_code); +} + +static inline void zcl_ep_println(const esp_zb_endpoint_t *ep) +{ + cli_output("ep:%d, prfl:0x%04x, dev_id:0x%04x, dev_ver:%d\n", + ep->ep_id, ep->profile_id, ((esp_zb_endpoint_config_t*)(ep->reserved_ptr))->app_device_id, + ((esp_zb_endpoint_config_t*)(ep->reserved_ptr))->app_device_version); +} + +static void dm_show_attr_list(const esp_zb_attribute_list_t *attr_list, const char *prefix) +{ + LIST_ITERATE(attr_list->next, attr_list) { + const esp_zb_zcl_attr_t *attr = &attr_list->attribute; + cli_output(prefix); + if (attr_list->next) { + cli_output("|-- "); + } else { + cli_output("+-- "); + } + zcl_attr_println(attr); + } +} + +static void dm_show_cluster_list(const esp_zb_cluster_list_t *cluster_list, char *prefix) +{ + assert(prefix != NULL); + int prefix_len = strlen(prefix); + memcpy(prefix + prefix_len, s_level_prefix, sizeof(s_level_prefix)); + LIST_ITERATE(cluster_list->next, cluster_list) { + const esp_zb_zcl_cluster_t *cluster = &cluster_list->cluster; + cli_output(prefix); + if (cluster_list->next) { + prefix[prefix_len] = '|'; + cli_output("|-- "); + } else { + prefix[prefix_len] = ' '; + cli_output("+-- "); + } + zcl_cluster_println(cluster); + dm_show_attr_list(cluster->attr_list, prefix); + prefix[prefix_len] = '\0'; + } +} + +static void dm_show_ep_list(const esp_zb_ep_list_t *ep_list, char *prefix) +{ + assert(prefix != NULL); + int prefix_len = strlen(prefix); + memcpy(prefix + prefix_len, s_level_prefix, sizeof(s_level_prefix)); + LIST_ITERATE(ep_list->next, ep_list) { + const esp_zb_endpoint_t *ep = &ep_list->endpoint; + cli_output(prefix); + if (ep_list->next) { + prefix[prefix_len] = '|'; + cli_output("|-- "); + } else { + prefix[prefix_len] = ' '; + cli_output("+-- "); + } + zcl_ep_println(ep); + dm_show_cluster_list(ep->cluster_list, prefix); + prefix[prefix_len] = '\0'; + } +} + +/** + * @brief Show current ZCL data model. + * + */ +static esp_err_t cli_dm_show(esp_zb_cli_cmd_t *self, int argc, char **argv) +{ + ESP_UNUSED(argv); + + esp_err_t ret = ESP_OK; + EXIT_ON_FALSE(argc == 1, ESP_ERR_INVALID_ARG); + + /* Dump the data model */ + esp_zb_ep_list_t *ep_list = CLI_CTX().ep_list; + + if (ep_list) { + char prefix[10] = {0}; + dm_show_ep_list(ep_list, prefix); + } else { + /* TODO: Dump the registered data model */ + } + +exit: + return ret; +} + +/* Implementation of ``dm register`` command */ + +/** + * @brief Register current ZCL data model. + * + */ +static esp_err_t cli_dm_register(esp_zb_cli_cmd_t *self, int argc, char **argv) +{ + (void)argv; + esp_err_t ret = ESP_OK; + EXIT_ON_FALSE(argc == 1, ESP_ERR_INVALID_ARG); + + EXIT_ON_FALSE(CLI_CTX().ep_list, ESP_ERR_NOT_SUPPORTED, cli_output_line("Data model has been registered")); + /* We forbid registration after stack started for time being. */ + EXIT_ON_FALSE(!esp_zb_is_started(), ESP_ERR_NOT_SUPPORTED, cli_output_line("Can not register after stack started")); + EXIT_ON_ERROR(esp_zb_device_register(CLI_CTX().ep_list), cli_output_line("Fail to register the ZCL data model")); + CLI_CTX().ep_list = NULL; + +exit: + return ret; +} + +static esp_err_t cli_dm_read(esp_zb_cli_cmd_t *self, int argc, char **argv) +{ + struct { + arg_u8_t *ep_id; + arg_u16_t *cluster_id; + arg_u16_t *attr_id; + arg_u16_t *manuf_code; + arg_str_t *role_mask; + arg_end_t *end; + } argtable = { + .ep_id = arg_u8n("e", "endpoint", "", 1, 1, "EID, id of the operating endpoint"), + .cluster_id = arg_u16n("c", "cluster", "", 1, 1, "CID, id of the operating cluster"), + .attr_id = arg_u16n("a", "attr", "", 1, 1, "AID, id of the operating attribute"), + .manuf_code = arg_u16n(NULL, "manuf", "", 0, 1, "manufacture code"), + .role_mask = arg_str0("r", "role", "", "role of the cluster, default: S"), + .end = arg_end(2), + }; + esp_err_t ret = ESP_OK; + + /* Parse command line arguments */ + EXIT_ON_FALSE(argc > 1, ESP_OK, arg_print_help((void**)&argtable, argv[0])); + int nerrors = arg_parse(argc, argv, (void**)&argtable); + EXIT_ON_FALSE(nerrors == 0, ESP_ERR_INVALID_ARG, arg_print_errors(stdout, argtable.end, argv[0])); + + uint16_t manuf_code = ESP_ZB_ZCL_ATTR_NON_MANUFACTURER_SPECIFIC; + uint8_t role_mask = ESP_ZB_ZCL_CLUSTER_SERVER_ROLE; + if (argtable.manuf_code->count > 0) { + manuf_code = argtable.manuf_code->val[0]; + } + if (argtable.role_mask->count > 0) { + switch (argtable.role_mask->sval[0][0]) { + case 'C': + case 'c': + role_mask = ESP_ZB_ZCL_CLUSTER_CLIENT_ROLE; + break; + case 'S': + case 's': + role_mask = ESP_ZB_ZCL_CLUSTER_SERVER_ROLE; + break; + default: + EXIT_ON_ERROR(ESP_ERR_INVALID_ARG, cli_output("%s: invalid argument to option --role\n", argv[0])); + break; + } + } + + EXIT_ON_FALSE(CLI_CTX().ep_list == NULL, ESP_ERR_INVALID_STATE, cli_output_line("Please register the data model first")); + esp_zb_zcl_attr_t *attr = esp_zb_zcl_get_manufacturer_attribute(argtable.ep_id->val[0], + argtable.cluster_id->val[0], + role_mask, + argtable.attr_id->val[0], + manuf_code); + EXIT_ON_FALSE(attr, ESP_ERR_NOT_FOUND, cli_output_line("Attribute not found")); + cli_output_buffer(attr->data_p, esp_zb_zcl_get_attribute_size(attr->type, attr->data_p)); + +exit: + ESP_ZB_CLI_FREE_ARGSTRUCT(&argtable); + return ret; +} + +static esp_err_t cli_dm_write(esp_zb_cli_cmd_t *self, int argc, char **argv) +{ + struct { + arg_lit_t *force; + arg_u8_t *ep_id; + arg_u16_t *cluster_id; + arg_u16_t *attr_id; + arg_u16_t *manuf_code; + arg_str_t *role_mask; + arg_hex_t *attr_value; + arg_end_t *end; + } argtable = { + .force = arg_lit0("f", "force", "force add, no checks"), + .ep_id = arg_u8n("e", "endpoint", "", 1, 1, "EID, id of the operating endpoint"), + .cluster_id = arg_u16n("c", "cluster", "", 1, 1, "CID, id of the operating cluster"), + .attr_id = arg_u16n("a", "attr", "", 1, 1, "AID, id of the operating attribute"), + .manuf_code = arg_u16n(NULL, "manuf", "", 0, 1, "manufacture code"), + .role_mask = arg_str0("r", "role", "", "role of the cluster, default: S"), + .attr_value = arg_hexn("v", "value", "", 1, 1, "value of the attribute, raw data in HEX"), + .end = arg_end(2), + }; + + esp_err_t ret = ESP_OK; + + /* Parse command line arguments */ + EXIT_ON_FALSE(argc > 1, ESP_OK, arg_print_help((void**)&argtable, argv[0])); + int nerrors = arg_parse(argc, argv, (void**)&argtable); + EXIT_ON_FALSE(nerrors == 0, ESP_ERR_INVALID_ARG, arg_print_errors(stdout, argtable.end, argv[0])); + + uint16_t manuf_code = ESP_ZB_ZCL_ATTR_NON_MANUFACTURER_SPECIFIC; + uint8_t role_mask = ESP_ZB_ZCL_CLUSTER_SERVER_ROLE; + if (argtable.manuf_code->count > 0) { + manuf_code = argtable.manuf_code->val[0]; + } + if (argtable.role_mask->count > 0) { + switch (argtable.role_mask->sval[0][0]) { + case 'C': + case 'c': + role_mask = ESP_ZB_ZCL_CLUSTER_CLIENT_ROLE; + break; + case 'S': + case 's': + role_mask = ESP_ZB_ZCL_CLUSTER_SERVER_ROLE; + break; + default: + EXIT_ON_ERROR(ESP_ERR_INVALID_ARG, cli_output("%s: invalid argument to option --role\n", argv[0])); + break; + } + } + + EXIT_ON_FALSE(CLI_CTX().ep_list == NULL, ESP_ERR_INVALID_STATE, cli_output_line("Please register the data model first")); + esp_zb_zcl_status_t result = esp_zb_zcl_set_manufacturer_attribute_val(argtable.ep_id->val[0], + argtable.cluster_id->val[0], + role_mask, + manuf_code, + argtable.attr_id->val[0], + argtable.attr_value->hval[0], + !(argtable.force->count > 0)); + + EXIT_ON_FALSE(result == ESP_ZB_ZCL_STATUS_SUCCESS, ESP_FAIL, cli_output("Fail to write attribute: %d\n", result)); + +exit: + arg_hex_free(argtable.attr_value); + ESP_ZB_CLI_FREE_ARGSTRUCT(&argtable); + return ret; +} + +/* Implementation of ``zcl `` commands */ + +typedef struct esp_zb_cli_aps_argtable_s { + arg_addr_t *dst_addr; + arg_u8_t *dst_ep; + arg_u8_t *src_ep; + arg_u16_t *profile; + arg_u16_t *cluster; +} esp_zb_cli_aps_argtable_t; + +static void esp_zb_cli_fill_aps_argtable(esp_zb_cli_aps_argtable_t *aps) +{ + aps->dst_addr = arg_addrn("d", "dst-addr", "", 0, 1, "destination address"); + aps->dst_ep = arg_u8n(NULL, "dst-ep", "", 0, 1, "destination endpoint id"); + aps->src_ep = arg_u8n("e", "src-ep", "", 1, 1, "source endpoint id"); + aps->profile = arg_u16n(NULL, "profile", "", 0, 1, "profile id of the command"); + aps->cluster = arg_u16n("c", "cluster", "", 1, 1, "cluster id of the command"); +} + +static esp_err_t esp_zb_cli_parse_aps_dst(esp_zb_cli_aps_argtable_t *parsed_argtable, esp_zb_addr_u *dst_addr_u, + uint8_t *dst_endpoint, esp_zb_zcl_address_mode_t *address_mode, + uint8_t *src_endpoint, uint16_t *cluster_id, uint16_t *profile_id) +{ + esp_err_t ret = ESP_OK; + /* Fill "dst_addr", "dst_ep" and "addr_mode" */ + if (parsed_argtable->dst_addr->count > 0) { + esp_zb_zcl_addr_t *dst_addr = &parsed_argtable->dst_addr->addr[0]; + esp_zb_aps_address_mode_t address_mode_select[][2] = { + [ESP_ZB_ZCL_ADDR_TYPE_SHORT][0] = ESP_ZB_APS_ADDR_MODE_16_GROUP_ENDP_NOT_PRESENT, + [ESP_ZB_ZCL_ADDR_TYPE_IEEE][0] = ESP_ZB_APS_ADDR_MODE_64_PRESENT_ENDP_NOT_PRESENT, + [ESP_ZB_ZCL_ADDR_TYPE_SHORT][1] = ESP_ZB_APS_ADDR_MODE_16_ENDP_PRESENT, + [ESP_ZB_ZCL_ADDR_TYPE_IEEE][1] = ESP_ZB_APS_ADDR_MODE_64_ENDP_PRESENT, + }; + /* Copy parse address */ + memcpy(dst_addr_u, &dst_addr->u, sizeof(esp_zb_addr_u)); + if (parsed_argtable->dst_ep->count > 0) { + *dst_endpoint = parsed_argtable->dst_ep->val[0]; + } + *address_mode = address_mode_select[dst_addr->addr_type][parsed_argtable->dst_ep->count > 0 ? 1 : 0]; + EXIT_ON_FALSE(*address_mode != ESP_ZB_APS_ADDR_MODE_64_PRESENT_ENDP_NOT_PRESENT, + ESP_ERR_NOT_SUPPORTED, cli_output_line("Unsupported address mode, dst-ep is required")); + } + + if (parsed_argtable->profile->count > 0 && profile_id) { + *profile_id = parsed_argtable->profile->val[0]; + } + if (parsed_argtable->src_ep->count > 0 && src_endpoint) { + *src_endpoint = parsed_argtable->src_ep->val[0]; + } + if (parsed_argtable->cluster->count > 0 && cluster_id) { + *cluster_id = parsed_argtable->cluster->val[0]; + } + +exit: + return ret; +} + +static esp_err_t cli_zcl_attr_cmd(esp_zb_cli_cmd_t *self, int argc, char **argv) +{ + struct { + esp_zb_cli_aps_argtable_t aps; + arg_str_t *peer_role; + arg_u16_t *attr_id; + arg_u8_t *attr_type; + arg_hex_t *attr_value; + arg_end_t *end; + } argtable = { + .peer_role = arg_strn("r", "role", "", 0, 1, "role of the peer cluster, default: S"), + .attr_id = arg_u16n("a", "attr", "", 0, 1, "id of the operating attribute"), + .attr_type = arg_u8n("t", "type", "", 0, 1, "ZCL attribute type id"), + .attr_value = arg_hexn("v", "value", "", 0, 1, "value of the attribute, raw data in HEX"), + .end = arg_end(2), + }; + esp_zb_cli_fill_aps_argtable(&argtable.aps); + + esp_err_t ret = ESP_OK; + const char *cmd = self->name; + + /* Parse command line arguments */ + EXIT_ON_FALSE(argc > 1, ESP_OK, arg_print_help((void**)&argtable, argv[0])); + int nerrors = arg_parse(argc, argv, (void**)&argtable); + EXIT_ON_FALSE(nerrors == 0, ESP_ERR_INVALID_ARG, arg_print_errors(stdout, argtable.end, argv[0])); + + /* Default requst settings */ + union { + struct { + esp_zb_zcl_basic_cmd_t zcl_basic_cmd; + esp_zb_zcl_address_mode_t address_mode; + uint16_t cluster_id; + }; + esp_zb_zcl_read_attr_cmd_t read_req; + esp_zb_zcl_write_attr_cmd_t write_req; + esp_zb_zcl_report_attr_cmd_t report_req; + esp_zb_zcl_config_report_cmd_t config_report_req; + esp_zb_zcl_read_report_config_cmd_t read_report_config_req; + esp_zb_zcl_disc_attr_cmd_t disc_attr; + } req_params = { + .address_mode = ESP_ZB_APS_ADDR_MODE_DST_ADDR_ENDP_NOT_PRESENT, + }; + + EXIT_ON_ERROR(esp_zb_cli_parse_aps_dst(&argtable.aps, + &req_params.zcl_basic_cmd.dst_addr_u, + &req_params.zcl_basic_cmd.dst_endpoint, + &req_params.address_mode, + &req_params.zcl_basic_cmd.src_endpoint, + &req_params.cluster_id, NULL)); + + esp_zb_zcl_cmd_direction_t direction = ESP_ZB_ZCL_CMD_DIRECTION_TO_SRV; + if (argtable.peer_role->count > 0) { + switch (argtable.peer_role->sval[0][0]) { + case 'C': + case 'c': + direction = ESP_ZB_ZCL_CMD_DIRECTION_TO_CLI; + break; + case 'S': + case 's': + direction = ESP_ZB_ZCL_CMD_DIRECTION_TO_SRV; + break; + default: + EXIT_ON_ERROR(ESP_ERR_INVALID_ARG, cli_output_line("invalid argument to option -r")); + break; + } + } + + if (!strcmp(cmd, "read")) { + req_params.read_req.attr_number = argtable.attr_id->count; + req_params.read_req.attr_field = argtable.attr_id->val; + esp_zb_zcl_read_attr_cmd_req(&req_params.read_req); + } else if (!strcmp(cmd, "write")) { + int n = argtable.attr_id->count; + esp_zb_zcl_attribute_t attr_field[n]; /* VLA */ + EXIT_ON_FALSE(n == argtable.attr_type->count && + n == argtable.attr_value->count, ESP_ERR_INVALID_ARG, + cli_output("%s: unbalanced options of --attr, --type, --value\n", cmd)); + for (int i = 0; i < n; i++) { + attr_field[i].id = argtable.attr_id->val[i]; + attr_field[i].data.type = argtable.attr_type->val[i]; + attr_field[i].data.value = argtable.attr_value->hval[i]; + attr_field[i].data.size = argtable.attr_value->hsize[i]; + } + req_params.write_req.attr_number = n; + req_params.write_req.attr_field = attr_field; + esp_zb_zcl_write_attr_cmd_req(&req_params.write_req); + } else if (!strcmp(cmd, "report")) { + EXIT_ON_FALSE(argtable.attr_id->count > 0, ESP_ERR_INVALID_ARG, cli_output("%s: -a is required\n", cmd)); + /* TODO: Support report client attribute */ + req_params.report_req.cluster_role = ESP_ZB_ZCL_CLUSTER_SERVER_ROLE; + req_params.report_req.attributeID = argtable.attr_id->val[0]; + ret = esp_zb_zcl_report_attr_cmd_req(&req_params.report_req); + } else if (!strcmp(cmd, "config_rp")) { + int n = argtable.attr_id->count; + bool report_change = 0; + esp_zb_zcl_config_report_record_t rprt_cfg_records[n]; + EXIT_ON_FALSE(n == argtable.attr_type->count, ESP_ERR_INVALID_ARG, + cli_output("%s: unbalanced options of --attr and --type\n", cmd)); + for (int i = 0; i < n; i++) { + rprt_cfg_records[i].direction = direction; + rprt_cfg_records[i].attributeID = argtable.attr_id->val[i]; + rprt_cfg_records[i].attrType = argtable.attr_type->val[i]; + /* TODO: Support configuring the report intervals */ + rprt_cfg_records[i].min_interval = 0; + rprt_cfg_records[i].max_interval = 30; + rprt_cfg_records[i].reportable_change = &report_change; + } + req_params.config_report_req.record_number = n; + req_params.config_report_req.record_field = rprt_cfg_records; + esp_zb_zcl_config_report_cmd_req(&req_params.config_report_req); + } else if (!strcmp(cmd, "read_rp_cfg")) { + int n = argtable.attr_id->count; + esp_zb_zcl_attribute_record_t attr_records[n]; /* VLA */ + for (int i = 0; i < n; i++) { + attr_records[i].report_direction = direction; + attr_records[i].attributeID = argtable.attr_id->val[i]; + } + req_params.read_report_config_req.record_number = n; + req_params.read_report_config_req.record_field = attr_records; + esp_zb_zcl_read_report_config_cmd_req(&req_params.read_report_config_req); + } else if (!strcmp(cmd, "disc_attr")) { + req_params.disc_attr.direction = direction; + req_params.disc_attr.start_attr_id = 0x0000; + req_params.disc_attr.max_attr_number = 30; + esp_zb_zcl_disc_attr_cmd_req(&req_params.disc_attr); + } else { + ret = ESP_ERR_NOT_SUPPORTED; + } + +exit: + arg_hex_free(argtable.attr_value); + ESP_ZB_CLI_FREE_ARGSTRUCT(&argtable); + return ret; +} + +/* Implementation of ``zcl send_raw`` command */ + +static esp_err_t cli_zcl_send_raw(esp_zb_cli_cmd_t *self, int argc, char **argv) +{ + struct { + esp_zb_cli_aps_argtable_t aps; + arg_str_t *peer_role; + arg_u8_t *command; + arg_hex_t *payload; + arg_lit_t *dry_run; + arg_end_t *end; + } argtable = { + .peer_role = arg_strn("r", "role", "", 0, 1, "role of the peer cluster, default: S"), + .command = arg_u8n(NULL, "cmd", "", 1, 1, "identifier of the command"), + .payload = arg_hexn("p", "payload", "", 0, 1, "ZCL payload of the command, raw HEX data"), + .dry_run = arg_lit0("n", "dry-run", "print the request being sent"), + .end = arg_end(2), + }; + esp_zb_cli_fill_aps_argtable(&argtable.aps); + + esp_err_t ret = ESP_OK; + /* Default request settings */ + esp_zb_zcl_custom_cluster_cmd_req_t req_params = { + .address_mode = ESP_ZB_APS_ADDR_MODE_DST_ADDR_ENDP_NOT_PRESENT, + .profile_id = ESP_ZB_AF_HA_PROFILE_ID, + .direction = ESP_ZB_ZCL_CMD_DIRECTION_TO_SRV, + }; + + /* Parse command line arguments */ + EXIT_ON_FALSE(argc > 1, ESP_OK, arg_print_help((void**)&argtable, argv[0])); + int nerrors = arg_parse(argc, argv, (void**)&argtable); + EXIT_ON_FALSE(nerrors == 0, ESP_ERR_INVALID_ARG, arg_print_errors(stdout, argtable.end, argv[0])); + + EXIT_ON_ERROR(esp_zb_cli_parse_aps_dst(&argtable.aps, + &req_params.zcl_basic_cmd.dst_addr_u, + &req_params.zcl_basic_cmd.dst_endpoint, + &req_params.address_mode, + &req_params.zcl_basic_cmd.src_endpoint, + &req_params.cluster_id, + &req_params.profile_id)); + + if (argtable.peer_role->count > 0) { + switch (argtable.peer_role->sval[0][0]) { + case 'C': + case 'c': + req_params.direction = ESP_ZB_ZCL_CMD_DIRECTION_TO_CLI; + break; + case 'S': + case 's': + req_params.direction = ESP_ZB_ZCL_CMD_DIRECTION_TO_SRV; + break; + default: + EXIT_ON_ERROR(ESP_ERR_INVALID_ARG, cli_output_line("invalid argument to option -r")); + break; + } + } + if (argtable.payload->count > 0) { + req_params.data.value = argtable.payload->hval[0]; + req_params.data.size = argtable.payload->hsize[0]; + req_params.data.type = ESP_ZB_ZCL_ATTR_TYPE_INVALID; + } + + /* DO NOT need a check, this option is mandatory */ + req_params.custom_cmd_id = argtable.command->val[0]; + + if (argtable.dry_run->count == 0) { + esp_zb_zcl_custom_cluster_cmd_req(&req_params); + } else { + cli_output_line("Send request:"); + cli_output("Mode[%d]: e:%d -> e:%d, ", + req_params.address_mode, + req_params.zcl_basic_cmd.src_endpoint, req_params.zcl_basic_cmd.dst_endpoint); + if(req_params.address_mode == ESP_ZB_APS_ADDR_MODE_64_ENDP_PRESENT) { + cli_output("addr:0x%016" PRIx64, *(uint64_t*)(&req_params.zcl_basic_cmd.dst_addr_u.addr_long)); + } else { + cli_output("addr:0x%04" PRIx16, req_params.zcl_basic_cmd.dst_addr_u.addr_short); + } + cli_output_line(""); + cli_output("prfl:0x%04x, c:0x%04x, dir:%c, cmd:0x%02x\n", req_params.profile_id, req_params.cluster_id, + req_params.direction == ESP_ZB_ZCL_CMD_DIRECTION_TO_SRV ? 'S' : 'C', + req_params.custom_cmd_id); + if (req_params.data.value) { + cli_output_buffer(req_params.data.value, req_params.data.size); + } + cli_output_line(""); + } + +exit: + arg_hex_free(argtable.payload); + ESP_ZB_CLI_FREE_ARGSTRUCT(&argtable); + return ret; +} + +DECLARE_ESP_ZB_CLI_CMD_WITH_SUB(dm, "ZigBee Cluster Library data model management", + ESP_ZB_CLI_SUBCMD(show, cli_dm_show, "Show current data model"), + ESP_ZB_CLI_SUBCMD(add, cli_dm_add, "Add items in ZCL data model"), + ESP_ZB_CLI_SUBCMD(register, cli_dm_register, "Register current data model"), + ESP_ZB_CLI_SUBCMD(read, cli_dm_read, "Read attribute value in data model"), + ESP_ZB_CLI_SUBCMD(write, cli_dm_write, "Write value to attribute in data model"), +); + +DECLARE_ESP_ZB_CLI_SUBCMD_LIST(zcl_send_gen, + ESP_ZB_CLI_SUBCMD(read, cli_zcl_attr_cmd, "Read attribute"), + ESP_ZB_CLI_SUBCMD(write, cli_zcl_attr_cmd, "Write attribute"), + ESP_ZB_CLI_SUBCMD(report, cli_zcl_attr_cmd, "Report attribute"), + ESP_ZB_CLI_SUBCMD(config_rp, cli_zcl_attr_cmd, "Configure reporting"), + ESP_ZB_CLI_SUBCMD(read_rp_cfg, cli_zcl_attr_cmd, "Read reporting configuration"), + ESP_ZB_CLI_SUBCMD(disc_attr, cli_zcl_attr_cmd, "Discover attributes"), +); +DECLARE_ESP_ZB_CLI_CMD_WITH_SUB(zcl, "ZigBee Cluster Library management", + ESP_ZB_CLI_CMD_WITH_SUB(send_gen, zcl_send_gen, "Send general command"), + ESP_ZB_CLI_SUBCMD(send_raw, cli_zcl_send_raw, "Send cluster specific raw command"), +); diff --git a/components/esp-zigbee-console/src/cli_cmd_zcl.h b/components/esp-zigbee-console/src/cli_cmd_zcl.h new file mode 100644 index 0000000..d63f4a2 --- /dev/null +++ b/components/esp-zigbee-console/src/cli_cmd_zcl.h @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include "esp_zigbee_core.h" + +#ifdef __cplusplus +extern "C" { +#endif + +esp_err_t cli_zcl_core_action_handler(esp_zb_core_action_callback_id_t callback_id, const void *message); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/components/esp-zigbee-console/src/cli_cmd_zdo.c b/components/esp-zigbee-console/src/cli_cmd_zdo.c new file mode 100644 index 0000000..669eb2d --- /dev/null +++ b/components/esp-zigbee-console/src/cli_cmd_zdo.c @@ -0,0 +1,461 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include "esp_check.h" +#include "esp_zigbee_core.h" + +#include "esp_zigbee_console.h" +#include "cli_cmd.h" +#include "cmdline_parser.h" +#include "zb_data/zcl.h" + +#define TAG "cli_cmd_zdo" + +static inline void cli_output_request_status(const char *request_name, uint16_t dest_addr, esp_zb_zdp_status_t status) +{ + cli_output("%s request to [addr:0x%04x] status: %d\n", request_name, dest_addr, status); +} + +/* Implementation of "zdo request" command */ + +static void cli_zdo_node_desc_cb(esp_zb_zdp_status_t zdo_status, uint16_t addr, esp_zb_af_node_desc_t *node_desc, void *user_ctx) +{ + static const char *request_name = "node_desc"; + esp_zb_zdo_node_desc_req_param_t *req = user_ctx; + cli_output_request_status(request_name, req->dst_nwk_addr, zdo_status); + if (zdo_status == ESP_ZB_ZDP_STATUS_SUCCESS) { + cli_output_buffer(node_desc, sizeof(esp_zb_af_node_desc_t)); + } + esp_zb_console_notify_result(ESP_OK); + free(req); +} + +static void cli_zdo_simple_desc_cb(esp_zb_zdp_status_t zdo_status, esp_zb_af_simple_desc_1_1_t *simple_desc, void *user_ctx) +{ + static const char *request_name = "simple_desc"; + esp_zb_zdo_simple_desc_req_param_t *req = user_ctx; + cli_output_request_status(request_name, req->addr_of_interest, zdo_status); + if (zdo_status == ESP_ZB_ZDP_STATUS_SUCCESS) { + cli_output("ep:%d profile:0x%04hx dev:0x%0hx dev_ver:0x%0hx\n", simple_desc->endpoint, + simple_desc->app_profile_id, + simple_desc->app_device_id, + simple_desc->app_device_version); +#pragma GCC diagnostic push +#if __GNUC__ >= 9 +#pragma GCC diagnostic ignored "-Waddress-of-packed-member" +#endif + cli_output_array_u16("in", &(simple_desc->app_cluster_list[0]), simple_desc->app_input_cluster_count, "0x%04x"); + cli_output_array_u16("out", &(simple_desc->app_cluster_list[0]) + simple_desc->app_input_cluster_count, + simple_desc->app_output_cluster_count, "0x%04x"); +#pragma GCC diagnostic pop + } + esp_zb_console_notify_result(ESP_OK); + free(req); +} + +static void cli_zdo_active_ep_cb(esp_zb_zdp_status_t zdo_status, uint8_t ep_count, uint8_t *ep_id_list, void *user_ctx) +{ + static const char *request_name = "active_ep"; + esp_zb_zdo_active_ep_req_param_t *req = user_ctx; + cli_output_request_status(request_name, req->addr_of_interest, zdo_status); + if (zdo_status == ESP_ZB_ZDP_STATUS_SUCCESS) { + cli_output_array_u8("active ep", ep_id_list, ep_count, "%hhu"); + } + esp_zb_console_notify_result(ESP_OK); + free(req); +} + +static void cli_zdo_ieee_addr_cb(esp_zb_zdp_status_t zdo_status, esp_zb_ieee_addr_t ieee_addr, void *user_ctx) +{ + static const char *request_name = "ieee_addr"; + esp_zb_zdo_ieee_addr_req_param_t *req = user_ctx; + cli_output_request_status(request_name, req->addr_of_interest, zdo_status); + if (zdo_status == ESP_ZB_ZDP_STATUS_SUCCESS) { + cli_output("ieee address: 0x%016" PRIx64 "\n", *(uint64_t *)ieee_addr); + } + esp_zb_console_notify_result(ESP_OK); + free(req); +} + +static void cli_output_binding_table(const esp_zb_zdo_binding_table_info_t *table_info) +{ + static const char *titles[] = {"Index", "Src_Addr", "Src_EP", "Cluster", "Dst_Addr", "Dst_EP"}; + static const uint8_t widths[] = {5, 20, 5, 8, 20, 5}; + + uint8_t start_idx = table_info->index; + esp_zb_zdo_binding_table_record_t *record = table_info->record; + cli_output_table_header(ARRAY_SIZE(widths), titles, widths); + for (int i = 0; i < table_info->count; i++) { + cli_output("| %3d | 0x%016" PRIx64 " | %3d | 0x%04hx ", + start_idx + i, *(uint64_t *)record->src_address, record->src_endp, record->cluster_id); + switch (record->dst_addr_mode) { + case ESP_ZB_APS_ADDR_MODE_16_GROUP_ENDP_NOT_PRESENT: + cli_output("| 0x%04hx | %3s |", record->dst_address.addr_short, "N/A"); + break; + case ESP_ZB_APS_ADDR_MODE_64_ENDP_PRESENT: + cli_output("| 0x%016" PRIx64 " | %3d |", *(uint64_t*)record->dst_address.addr_long, record->dst_endp); + break; + default: + /* Never reached */ + break; + } + cli_output("\n"); + + record = record->next; + } +} + +static void cli_zdo_binding_table_cb(const esp_zb_zdo_binding_table_info_t *table_info, void *user_ctx) +{ + static const char *request_name = "bindings"; + bool done = true; + esp_zb_zdo_mgmt_bind_param_t *req = user_ctx; + esp_zb_zdp_status_t zdo_status = table_info->status; + cli_output_request_status(request_name, req->dst_addr, zdo_status); + if (zdo_status == ESP_ZB_ZDP_STATUS_SUCCESS) { + cli_output_binding_table(table_info); + + if (table_info->index + table_info->count < table_info->total) { + /* There are unreported binding table entries, request for them. */ + req->start_index = table_info->index + table_info->count; + esp_zb_zdo_binding_table_req(req, cli_zdo_binding_table_cb, req); + done = false; + } + } + + if (done) { + esp_zb_console_notify_result(ESP_OK); + free(req); + } +} + +static esp_err_t cli_zdo_request(esp_zb_cli_cmd_t *self, int argc, char **argv) +{ + struct { + arg_str_t *request; + arg_rem_t *rem_req; + arg_addr_t *address; + arg_u8_t *endpoint; + arg_end_t *end; + } argtable = { + .request = arg_strn(NULL, NULL, "", 1, 1, "information to request"), + .rem_req = arg_rem("", "node_desc|simple_desc|active_ep|nwk_addr|ieee_addr|neighbors|routes|bindings"), + .address = arg_addrn("d", "dst-addr", "", 1, 1, "address of interest"), + .endpoint = arg_u8n("e", "endpoint", "", 0, 1, "endpoint ID of interest"), + .end = arg_end(2), + }; + esp_err_t ret = ESP_ERR_NOT_FINISHED; + + /* Parse command line arguments */ + EXIT_ON_FALSE(argc > 1, ESP_OK, arg_print_help((void**)&argtable, argv[0])); + int nerrors = arg_parse(argc, argv, (void**)&argtable); + EXIT_ON_FALSE(nerrors == 0, ESP_ERR_INVALID_ARG, arg_print_errors(stdout, argtable.end, argv[0])); + + EXIT_ON_FALSE(argtable.address->addr->addr_type == ESP_ZB_ZCL_ADDR_TYPE_SHORT, ESP_ERR_INVALID_ARG, + cli_output("%s %s:only short address is supported\n", argv[0], argv[1])); + + if (!strcmp(argtable.request->sval[0], "node_desc")) { + esp_zb_zdo_node_desc_req_param_t *nd_req = malloc(sizeof(esp_zb_zdo_node_desc_req_param_t)); + nd_req->dst_nwk_addr = argtable.address->addr[0].u.short_addr; + esp_zb_zdo_node_desc_req(nd_req, cli_zdo_node_desc_cb, nd_req); + } else if (!strcmp(argtable.request->sval[0], "simple_desc")) { + esp_zb_zdo_simple_desc_req_param_t *sd_req = malloc(sizeof(esp_zb_zdo_simple_desc_req_param_t)); + sd_req->addr_of_interest = argtable.address->addr[0].u.short_addr, + sd_req->endpoint = argtable.endpoint->val[0]; + esp_zb_zdo_simple_desc_req(sd_req, cli_zdo_simple_desc_cb, sd_req); + } else if (!strcmp(argtable.request->sval[0], "active_ep")) { + esp_zb_zdo_active_ep_req_param_t *ae_req = malloc(sizeof(esp_zb_zdo_active_ep_req_param_t)); + ae_req->addr_of_interest = argtable.address->addr[0].u.short_addr; + esp_zb_zdo_active_ep_req(ae_req, cli_zdo_active_ep_cb, ae_req); + } else if (!strcmp(argtable.request->sval[0], "nwk_addr")) { + ret = ESP_ERR_NOT_SUPPORTED; + } else if (!strcmp(argtable.request->sval[0], "ieee_addr")) { + esp_zb_zdo_ieee_addr_req_param_t *ia_req = malloc(sizeof(esp_zb_zdo_ieee_addr_req_param_t)); + ia_req->dst_nwk_addr = argtable.address->addr[0].u.short_addr; + ia_req->addr_of_interest = ia_req->dst_nwk_addr; + ia_req->request_type = 0; + ia_req->start_index = 0; + esp_zb_zdo_ieee_addr_req(ia_req, cli_zdo_ieee_addr_cb, ia_req); + } else if (!strcmp(argtable.request->sval[0], "neighbors")) { + ret = ESP_ERR_NOT_SUPPORTED; + } else if (!strcmp(argtable.request->sval[0], "routes")) { + ret = ESP_ERR_NOT_SUPPORTED; + } else if (!strcmp(argtable.request->sval[0], "bindings")) { + esp_zb_zdo_mgmt_bind_param_t *mb_req = malloc(sizeof(esp_zb_zdo_mgmt_bind_param_t)); + mb_req->dst_addr = argtable.address->addr[0].u.short_addr; + mb_req->start_index = 0; + esp_zb_zdo_binding_table_req(mb_req, cli_zdo_binding_table_cb, mb_req); + } else { + ret = ESP_ERR_NOT_FOUND; + } + + if (ret == ESP_ERR_NOT_SUPPORTED) { + cli_output("%s info type: %s\n", "unsupported", argtable.request->sval[0]); + } else if (ret == ESP_ERR_NOT_FOUND) { + cli_output("%s info type: %s\n", "unknown", argtable.request->sval[0]); + } + +exit: + ESP_ZB_CLI_FREE_ARGSTRUCT(&argtable); + return ret; +} + +/* Implementation of "zdo annce" command */ + +static esp_err_t cli_zdo_annce(esp_zb_cli_cmd_t *self, int argc, char **argv) +{ + esp_err_t ret = ESP_OK; + EXIT_ON_FALSE(argc == 1, ESP_ERR_INVALID_ARG); + EXIT_ON_FALSE(esp_zb_bdb_dev_joined(), ESP_ERR_INVALID_STATE, cli_output_line("Not on a network")); + esp_zb_zdo_device_announcement_req(); + +exit: + return ret; +} + +/* Implementation of "zdo match" command */ + +static void cli_zdo_match_desc_cb(esp_zb_zdp_status_t zdo_status, uint16_t addr, uint8_t endpoint, void *user_ctx) +{ + static const char *request_name = "match"; + esp_zb_zdo_match_desc_req_param_t *req = user_ctx; + cli_output_request_status(request_name, req->addr_of_interest, zdo_status); + if (zdo_status == ESP_ZB_ZDP_STATUS_SUCCESS) { + cli_output("matched device: 0x%04hx:%d\n", addr, endpoint); + } + esp_zb_console_notify_result(ESP_OK); + free(req); +} + +static esp_err_t cli_zdo_match(esp_zb_cli_cmd_t *self, int argc, char **argv) +{ + struct { + arg_u16_t *in_cluster; + arg_u16_t *out_cluster; + arg_u16_t *profile_id; + arg_addr_t *address; + arg_end_t *end; + } argtable = { + .in_cluster = arg_u16n("i", "in", "", 0, 10, "in cluster ID"), + .out_cluster = arg_u16n("o", "out", "", 0, 10, "out cluster ID"), + .profile_id = arg_u16n("p", "profile", "", 0, 1, "profile id (PID) to match, default: HA"), + .address = arg_addrn("d", "dst-addr", "", 1, 1, "network address this request is to"), + .end = arg_end(2), + }; + esp_err_t ret = ESP_ERR_NOT_FINISHED; + + /* Parse command line arguments */ + EXIT_ON_FALSE(argc > 1, ESP_OK, arg_print_help((void**)&argtable, argv[0])); + int nerrors = arg_parse(argc, argv, (void**)&argtable); + EXIT_ON_FALSE(nerrors == 0, ESP_ERR_INVALID_ARG, arg_print_errors(stdout, argtable.end, argv[0])); + + EXIT_ON_FALSE(argtable.address->addr->addr_type == ESP_ZB_ZCL_ADDR_TYPE_SHORT, ESP_ERR_INVALID_ARG); + + esp_zb_zdo_match_desc_req_param_t *req = malloc(sizeof(esp_zb_zdo_match_desc_req_param_t)); + uint8_t in_num = argtable.in_cluster->count; + uint8_t out_num = argtable.out_cluster->count; + + req->profile_id = ESP_ZB_AF_HA_PROFILE_ID; + req->num_in_clusters = in_num; + req->num_out_clusters = out_num; + req->cluster_list = malloc(in_num + out_num); + memcpy(req->cluster_list, argtable.in_cluster->val, in_num); + memcpy(req->cluster_list + in_num, argtable.out_cluster->val, out_num); + if (argtable.profile_id->count > 0) { + req->profile_id = argtable.profile_id->val[0]; + } + req->dst_nwk_addr = argtable.address->addr[0].u.short_addr; + req->addr_of_interest = req->dst_nwk_addr; + EXIT_ON_ERROR(esp_zb_zdo_match_cluster(req, cli_zdo_match_desc_cb, req)); + +exit: + ESP_ZB_CLI_FREE_ARGSTRUCT(&argtable); + return ret; +} + +/* Implementation of "zdo nwk_open" command */ + +static void cli_zdo_permit_join_cb(esp_zb_zdp_status_t zdo_status, void *user_ctx) +{ + static const char *request_name = "permit_join"; + esp_zb_zdo_permit_joining_req_param_t *req = user_ctx; + cli_output_request_status(request_name, req->dst_nwk_addr, zdo_status); + esp_zb_console_notify_result(ESP_OK); + free(req); +} + +static esp_err_t cli_zdo_nwk_open(esp_zb_cli_cmd_t *self, int argc, char **argv) +{ + struct { + arg_u8_t *timeout; + arg_addr_t *address; + arg_end_t *end; + } argtable = { + .timeout = arg_u8n("t", "timeout", "", 0, 1, "timeout in seconds for network opening, default: 60"), + .address = arg_addrn("d", "dst-addr", "", 1, 1, "network address this request is to"), + .end = arg_end(2), + }; + esp_err_t ret = ESP_ERR_NOT_FINISHED; + + /* Parse command line arguments */ + EXIT_ON_FALSE(argc > 1, ESP_OK, arg_print_help((void**)&argtable, argv[0])); + int nerrors = arg_parse(argc, argv, (void**)&argtable); + EXIT_ON_FALSE(nerrors == 0, ESP_ERR_INVALID_ARG, arg_print_errors(stdout, argtable.end, argv[0])); + + EXIT_ON_FALSE(argtable.address->addr->addr_type == ESP_ZB_ZCL_ADDR_TYPE_SHORT, ESP_ERR_INVALID_ARG); + + esp_zb_zdo_permit_joining_req_param_t *req = malloc(sizeof(esp_zb_zdo_permit_joining_req_param_t)); + req->permit_duration = 60; + if (argtable.timeout->count > 0) { + req->permit_duration = argtable.timeout->val[0]; + } + req->tc_significance = 0x01; /* This field should always be '0x01'. */ + req->dst_nwk_addr = argtable.address->addr[0].u.short_addr; + esp_zb_zdo_permit_joining_req(req, cli_zdo_permit_join_cb, req); + +exit: + ESP_ZB_CLI_FREE_ARGSTRUCT(&argtable); + return ret; +} + +/* Implementation of "zdo nwk_leave" command */ + +static void cli_zdo_leave_cb(esp_zb_zdp_status_t zdo_status, void *user_ctx) +{ + static const char *request_name = "leave"; + esp_zb_zdo_mgmt_leave_req_param_t *req = user_ctx; + cli_output_request_status(request_name, req->dst_nwk_addr, zdo_status); + esp_zb_console_notify_result(ESP_OK); + free(req); +} + +static esp_err_t cli_zdo_nwk_leave(esp_zb_cli_cmd_t *self, int argc, char **argv) +{ + struct { + arg_lit_t *remove; + arg_lit_t *rejoin; + arg_addr_t *address; + arg_end_t *end; + } argtable = { + .remove = arg_lit0("c", "remove", "Remove children"), + .rejoin = arg_lit0("r", "rejoin", "Rejoin after leave"), + .address = arg_addrn("d", "dst-addr", "", 1, 1, "network address this request is to"), + .end = arg_end(2), + }; + esp_err_t ret = ESP_ERR_NOT_FINISHED; + + /* Parse command line arguments */ + EXIT_ON_FALSE(argc > 1, ESP_OK, arg_print_help((void**)&argtable, argv[0])); + int nerrors = arg_parse(argc, argv, (void**)&argtable); + EXIT_ON_FALSE(nerrors == 0, ESP_ERR_INVALID_ARG, arg_print_errors(stdout, argtable.end, argv[0])); + + EXIT_ON_FALSE(argtable.address->addr->addr_type == ESP_ZB_ZCL_ADDR_TYPE_SHORT, ESP_ERR_INVALID_ARG); + + esp_zb_zdo_mgmt_leave_req_param_t *req = malloc(sizeof(esp_zb_zdo_mgmt_leave_req_param_t)); + if (argtable.rejoin->count > 0) { + req->rejoin = 1; + } + if (argtable.remove->count > 0) { + req->remove_children = 1; + } + req->dst_nwk_addr = argtable.address->addr[0].u.short_addr; + esp_zb_zdo_device_leave_req(req, cli_zdo_leave_cb, req); + +exit: + ESP_ZB_CLI_FREE_ARGSTRUCT(&argtable); + return ret; +} + +/* Implementation of "zdo bind" command */ + +static void cli_zdo_bind_cb(esp_zb_zdp_status_t zdo_status, void *user_ctx) +{ + static const char *request_name = "bind"; + esp_zb_zdo_bind_req_param_t *req = user_ctx; + cli_output_request_status(request_name, req->req_dst_addr, zdo_status); + esp_zb_console_notify_result(ESP_OK); + free(req); +} + +static void cli_zdo_unbind_cb(esp_zb_zdp_status_t zdo_status, void *user_ctx) +{ + static const char *request_name = "unbind"; + esp_zb_zdo_bind_req_param_t *req = user_ctx; + cli_output_request_status(request_name, req->req_dst_addr, zdo_status); + esp_zb_console_notify_result(ESP_OK); + free(req); +} + +static esp_err_t cli_zdo_bind(esp_zb_cli_cmd_t *self, int argc, char **argv) +{ + struct { + arg_lit_t *remove; + arg_u16_t *cluster; + arg_u8_t *src_ep; + arg_u8_t *dst_ep; + arg_addr_t *src_addr; + arg_addr_t *dst_addr; + arg_end_t *end; + } argtable = { + .remove = arg_lit0("r", "remove", "Remove the binding"), + .cluster = arg_u16n("c", "cluster", "", 1, 1, "cluster id of binding"), + .src_ep = arg_u8n("S", "src-ep", "", 1, 1, "endpoint id of binding source"), + .src_addr = arg_addrn("s", "src-addr", "", 1, 1, "address of binding source"), + .dst_ep = arg_u8n("D", "dst-ep", "", 0, 1, "endpoint id of binding destination"), + .dst_addr = arg_addrn("d", "dst-addr", "", 1, 1, "address of binding destination"), + .end = arg_end(2), + }; + esp_err_t ret = ESP_ERR_NOT_FINISHED; + + /* Parse command line arguments */ + EXIT_ON_FALSE(argc > 1, ESP_OK, arg_print_help((void**)&argtable, argv[0])); + int nerrors = arg_parse(argc, argv, (void**)&argtable); + EXIT_ON_FALSE(nerrors == 0, ESP_ERR_INVALID_ARG, arg_print_errors(stdout, argtable.end, argv[0])); + + esp_zb_zdo_bind_req_param_t *req = malloc(sizeof(esp_zb_zdo_bind_req_param_t)); + req->cluster_id = argtable.cluster->val[0]; + /* Populate dst information of binding */ + if (argtable.dst_addr->addr[0].addr_type == ESP_ZB_ZCL_ADDR_TYPE_SHORT) { + req->dst_endp = 0; + req->dst_addr_mode = ESP_ZB_ZDO_BIND_DST_ADDR_MODE_16_BIT_GROUP; + req->dst_address_u.addr_short = argtable.dst_addr->addr[0].u.short_addr; + } else { + EXIT_ON_FALSE(argtable.dst_ep->count > 0, ESP_ERR_INVALID_ARG, cli_output("dst-ep is required\n")); + req->dst_endp = argtable.dst_ep->val[0]; + req->dst_addr_mode = ESP_ZB_ZDO_BIND_DST_ADDR_MODE_64_BIT_EXTENDED; + memcpy(req->dst_address_u.addr_long, argtable.dst_addr->addr[0].u.ieee_addr, sizeof(esp_zb_ieee_addr_t)); + } + + /* Populate src information of binding */ + req->src_endp = argtable.src_ep->val[0]; + if (argtable.src_addr->addr[0].addr_type == ESP_ZB_ZCL_ADDR_TYPE_SHORT) { + req->req_dst_addr = argtable.src_addr->addr[0].u.short_addr; + } else { + req->req_dst_addr = esp_zb_address_short_by_ieee(argtable.src_addr->addr[0].u.ieee_addr); + } + EXIT_ON_ERROR(esp_zb_ieee_address_by_short(req->req_dst_addr, req->src_address)); + if (argtable.remove->count > 0) { + esp_zb_zdo_device_unbind_req(req, cli_zdo_unbind_cb, req); + } else { + esp_zb_zdo_device_bind_req(req, cli_zdo_bind_cb, req); + } + +exit: + ESP_ZB_CLI_FREE_ARGSTRUCT(&argtable); + return ret; +} + +DECLARE_ESP_ZB_CLI_CMD_WITH_SUB(zdo, "Zigbee Device Object management", + ESP_ZB_CLI_SUBCMD(request, cli_zdo_request, "Request information from node"), + ESP_ZB_CLI_SUBCMD(annce, cli_zdo_annce, "Announce current node"), + ESP_ZB_CLI_SUBCMD(match, cli_zdo_match, "Get matched devices"), + ESP_ZB_CLI_SUBCMD(bind, cli_zdo_bind, "Request the node to bind to device"), + ESP_ZB_CLI_SUBCMD(nwk_open, cli_zdo_nwk_open, "Request the node to open the network"), + ESP_ZB_CLI_SUBCMD(nwk_leave, cli_zdo_nwk_leave, "Request the node to leave the network"), +); diff --git a/components/esp-zigbee-console/src/cli_cmd_zgp.c b/components/esp-zigbee-console/src/cli_cmd_zgp.c new file mode 100644 index 0000000..c3c8519 --- /dev/null +++ b/components/esp-zigbee-console/src/cli_cmd_zgp.c @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include "esp_zigbee_core.h" + +#include "esp_zigbee_console.h" +#include "cli_cmd.h" + +#define TAG "cli_cmd_zgp" + +static esp_err_t cli_zgp_proxy(esp_zb_cli_cmd_t *self, int argc, char *argv[]) +{ + return ESP_ERR_NOT_SUPPORTED; +} + +static esp_err_t cli_zgp_sink(esp_zb_cli_cmd_t *self, int argc, char *argv[]) +{ + return ESP_ERR_NOT_SUPPORTED; +} + +DECLARE_ESP_ZB_CLI_CMD_WITH_SUB(zgp, "ZigBee Green Power Profile management", + ESP_ZB_CLI_SUBCMD(proxy, cli_zgp_proxy, "Get/Set green power proxy"), + ESP_ZB_CLI_SUBCMD(sink, cli_zgp_sink, "Get/Set green power sink"), +); diff --git a/components/esp-zigbee-console/src/cli_cmd_zha.c b/components/esp-zigbee-console/src/cli_cmd_zha.c new file mode 100644 index 0000000..df0646b --- /dev/null +++ b/components/esp-zigbee-console/src/cli_cmd_zha.c @@ -0,0 +1,116 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include "esp_check.h" +#include "esp_zigbee_core.h" + +#include "esp_zigbee_console.h" +#include "cli_cmd.h" +#include "zb_data/ha.h" + +#define TAG "cli_cmd_zha" + +typedef struct cli_zha_device_params_s { + uint8_t ep_id; + uint16_t device_id; + const char *device_name; +} cli_zha_device_params_t; + +static esp_err_t zha_add_device(esp_zb_ep_list_t *ep_list, cli_zha_device_params_t *device_params) +{ + esp_err_t ret = ESP_OK; + EXIT_ON_FALSE(device_params->device_name != NULL, ESP_OK); + EXIT_ON_FALSE(ep_list, ESP_ERR_NOT_SUPPORTED, cli_output_line("Data model has been registered")); + esp_zb_cluster_list_t *cluster_list = esp_zb_ep_create_ha_default(device_params->device_id); + + EXIT_ON_FALSE(cluster_list, ESP_ERR_NOT_SUPPORTED, + cli_output("Unsupported device %s\n", device_params->device_name)); + + /* Add created endpoint (cluster_list) to endpoint list */ + esp_zb_endpoint_config_t endpoint_config = { + .endpoint = device_params->ep_id, + .app_profile_id = ESP_ZB_AF_HA_PROFILE_ID, + .app_device_id = device_params->device_id, + .app_device_version = 0 + }; + EXIT_ON_ERROR(esp_zb_ep_list_add_ep(ep_list, cluster_list, endpoint_config), + cli_output_line("Fail to add the device to endpoint list")); + + ESP_LOGI(TAG, "%s created with endpoint_id %d", device_params->device_name, device_params->ep_id); + +exit: + return ret; +} + +static void cli_output_device_name(void) +{ + int idx = 0; + const char *empty = ""; + const char *name; + cli_output_line("\nSupported device:"); + while (empty != (name = esp_zb_get_device_name_by_idx(idx++))) { + if (esp_zb_is_device_supported(esp_zb_get_device_id_by_name(name))) { + cli_output_line(name); + } + } +} + +static esp_err_t cli_zha_add_parse_params(int argc, char *argv[], cli_zha_device_params_t *dev_param) +{ + struct { + arg_int_t *ep_id; + arg_str_t *device; + arg_end_t *end; + } argtable = { + .ep_id = arg_int1(NULL, NULL, "", "set the endpoint id of the added device"), + .device = arg_str1(NULL, NULL, "", "HA standard device to be added"), + .end = arg_end(2), + }; + + esp_err_t ret = ESP_OK; + + /* Parse command line arguments */ + EXIT_ON_FALSE(argc > 1, ESP_OK, arg_print_help((void**)&argtable, argv[0]);cli_output_device_name()); + int nerrors = arg_parse(argc, argv, (void**)&argtable); + EXIT_ON_FALSE(nerrors == 0, ESP_ERR_INVALID_ARG, arg_print_errors(stdout, argtable.end, argv[0])); + + int ep_id = argtable.ep_id->ival[0]; + uint16_t device_id = esp_zb_get_device_id_by_name(argtable.device->sval[0]); + + EXIT_ON_FALSE((0 < ep_id && ep_id < 255), ESP_ERR_INVALID_ARG, + cli_output(" should in range 1 - 254, got %d\n", ep_id)); + EXIT_ON_FALSE((device_id != 0xFFFF), ESP_ERR_INVALID_ARG, + cli_output("Unknown , got %s\n", argtable.device->sval[0]); + cli_output_device_name()); + + dev_param->device_id = device_id; + dev_param->ep_id = ep_id; + dev_param->device_name = esp_zb_get_device_name_by_id(device_id); + +exit: + ESP_ZB_CLI_FREE_ARGSTRUCT(&argtable); + return ret; +} + +static esp_err_t cli_zha_add(esp_zb_cli_cmd_t *self, int argc, char *argv[]) +{ + esp_err_t ret = ESP_OK; + cli_zha_device_params_t device_params = { 0 }; + esp_zb_ep_list_t *ep_list = CLI_CTX().ep_list; + EXIT_ON_ERROR(cli_zha_add_parse_params(argc, argv, &device_params)); + EXIT_ON_ERROR(zha_add_device(ep_list, &device_params)); + +exit: + return ret; +} + +DECLARE_ESP_ZB_CLI_CMD_WITH_SUB(zha, "ZigBee Home Automation Profile", + ESP_ZB_CLI_SUBCMD(add, cli_zha_add, "Add device by device type name"), +); diff --git a/components/esp-zigbee-console/src/cli_output.c b/components/esp-zigbee-console/src/cli_output.c new file mode 100644 index 0000000..c5c1da0 --- /dev/null +++ b/components/esp-zigbee-console/src/cli_output.c @@ -0,0 +1,124 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include "esp_log.h" + +#include "cli_util.h" + +void cli_output(const char *format, ...) +{ + va_list args; + + va_start(args, format); + vprintf(format, args); + va_end(args); +} + +void cli_output_newline() +{ + cli_output("\n"); +} + +void cli_output_line(const char *line) +{ + cli_output("%s\n", line); +} + +void cli_output_array(const char *arr_name, void *array, size_t arr_cnt, size_t element_size, + cli_output_element_func_t write_element, void *user_ctx) +{ +#define LINE_WIDTH 128 +#define LINE_REMAIN(s, c) (LINE_WIDTH - (c - s)) + + char line[LINE_WIDTH]; + char *cur = line; + + uint8_t *element = array; + + cur += snprintf(line, LINE_REMAIN(line, cur), "%s: [", arr_name); + + for (int idx = 0; idx < arr_cnt; idx++) { + cur += write_element(element, cur, LINE_REMAIN(line, cur), user_ctx); + if (LINE_REMAIN(line, cur) < 2) { + cur -= 5; /* spaces for "...]\0" */ + cur += snprintf(line, LINE_REMAIN(line, cur), "..."); + break; + } + element += element_size; + *cur++ = ','; + *cur++ = ' '; + } + + if (arr_cnt > 0) { + cur -= 2; + } + + *cur++ = ']'; + *cur++ = '\0'; + + cli_output_line(line); +} + +static int write_u8(const uint8_t *pval, char *buf, size_t buf_size, const char *format) +{ + return snprintf(buf, buf_size, format, *pval); +} + +static int write_u16(const uint16_t *pval, char *buf, size_t buf_size, const char *format) +{ + return snprintf(buf, buf_size, format, *pval); +} + +void cli_output_array_u8(const char *arr_name, uint8_t *array, size_t arr_cnt, const char *format) +{ + cli_output_array(arr_name, array, arr_cnt, sizeof(*array), (cli_output_element_func_t)&write_u8, (void*)format); +} + +void cli_output_array_u16(const char *arr_name, uint16_t *array, size_t arr_cnt, const char *format) +{ + cli_output_array(arr_name, array, arr_cnt, sizeof(*array), (cli_output_element_func_t)&write_u16, (void*)format); +} + +void cli_output_table_separator(uint8_t column_num, const uint8_t *widths) +{ + for (uint8_t index = 0; index < column_num; index++) { + cli_output("+"); + for (uint8_t width = widths[index]; width != 0; width--) { + cli_output("-"); + } + } + cli_output_line("+"); +} + +void cli_output_table_header(uint8_t column_num, const char *const *titles, const uint8_t *widths) +{ + for (uint8_t index = 0; index < column_num; index++) { + const char *title = titles[index]; + uint8_t width = widths[index]; + size_t title_length = strlen(title); + + if (title_length + 2 <= width) { + /* Use left aligned style with spaces when there are enough spaces + * for `title` ("| Title |"). */ + cli_output("| %*s", -(int)(width - 1), title); + } else { + /* Use narrow style and write as many chars from `title` as it can + * fit in the given column width ("|Title|"). */ + cli_output("|%*.*s", -(int)(width), width, title); + } + } + + cli_output_line("|"); + cli_output_table_separator(column_num, widths); +} + +void cli_output_buffer(const void *buffer, uint16_t buff_len) +{ + ESP_LOG_BUFFER_HEXDUMP("", buffer, buff_len, ESP_LOG_INFO); +} diff --git a/components/esp-zigbee-console/src/cli_util.h b/components/esp-zigbee-console/src/cli_util.h new file mode 100644 index 0000000..b7a8bfb --- /dev/null +++ b/components/esp-zigbee-console/src/cli_util.h @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define ARRAY_SIZE(arr) (sizeof(arr)/sizeof(arr[0])) + +#define EXIT_ON_FALSE(a, err_code, ...) do { \ + if (unlikely(!(a))) { \ + ret = err_code; \ + __VA_ARGS__; \ + goto exit; \ + } \ + } while(0); + +#define EXIT_ON_ERROR(x, ...) do { \ + esp_err_t err_rc_ = (x); \ + if (unlikely(err_rc_ != ESP_OK)) { \ + ret = err_rc_; \ + __VA_ARGS__; \ + goto exit; \ + } \ + } while(0); + +#define EXIT_NOW(...) do { \ + __VA_ARGS__; \ + goto exit; \ + } while(0); + +typedef int (*cli_output_element_func_t)(const void *, char *, size_t, void *); + +void cli_output(const char *format, ...) __attribute__ ((format (printf, 1, 2))); +void cli_output_line(const char *line); +void cli_output_array(const char *arr_name, void *array, size_t arr_cnt, size_t element_size, + cli_output_element_func_t write_element, void *user_ctx); +void cli_output_array_u8(const char *arr_name, uint8_t *array, size_t arr_cnt, const char *format); +void cli_output_array_u16(const char *arr_name, uint16_t *array, size_t arr_cnt, const char *format); +void cli_output_table_header(uint8_t column_num, const char *const *titles, const uint8_t *widths); +void cli_output_buffer(const void *buffer, uint16_t buff_len); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/components/esp-zigbee-console/src/cmdline_parser.c b/components/esp-zigbee-console/src/cmdline_parser.c new file mode 100644 index 0000000..f29fc18 --- /dev/null +++ b/components/esp-zigbee-console/src/cmdline_parser.c @@ -0,0 +1,206 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include "esp_check.h" +#include "esp_zigbee_core.h" + +#include "cmdline_parser.h" + +#define TAG "" + +static esp_err_t parse_digit(char digit_char, uint8_t *value) +{ + ESP_RETURN_ON_FALSE(('0' <= digit_char) && (digit_char <= '9'), ESP_ERR_INVALID_ARG, TAG, "Invalid digit char"); + *value = (uint8_t)(digit_char - '0'); + + return ESP_OK; +} + +static esp_err_t parse_hex_digit(char hex_char, uint8_t *value) +{ + esp_err_t error = ESP_OK; + + if (('A' <= hex_char) && (hex_char <= 'F')) { + *value = (uint8_t)(hex_char - 'A' + 10); + } else if (('a' <= hex_char) && (hex_char <= 'f')) { + *value = (uint8_t)(hex_char - 'a' + 10); + } else { + error = parse_digit(hex_char, value); + } + + return error; +} + +esp_err_t parse_u64(const char *string, uint64_t *value_u64) +{ + uint64_t value = 0; + const char *cur = string; + bool is_hex = false; + + enum { + Max_Hex_Before_Overflow = (0xffffffffffffffffULL / 16), + Max_Dec_Before_Overflow = (0xffffffffffffffffULL / 10), + }; + + ESP_RETURN_ON_FALSE(string != NULL, ESP_ERR_INVALID_ARG, TAG, "Invalid string"); + + if (cur[0] == '0' && (cur[1] == 'x' || cur[1] == 'X')) { + cur += 2; + is_hex = true; + } + + do { + uint8_t digit; + + ESP_RETURN_ON_ERROR(is_hex ? parse_hex_digit(*cur, &digit) : parse_digit(*cur, &digit), + TAG, "Fail to parse single char, wrong format"); + ESP_RETURN_ON_FALSE(value <= (is_hex ? Max_Hex_Before_Overflow : Max_Dec_Before_Overflow), ESP_ERR_INVALID_SIZE, + TAG, "Overflow uint64"); + value = (is_hex ? (value << 4) : (value * 10)) + digit; + cur++; + } while (*cur != '\0'); + + *value_u64 = value; + + return ESP_OK; +} + +esp_err_t parse_u32(const char *string, uint32_t *value_u32) +{ + uint64_t value; + ESP_RETURN_ON_ERROR(parse_u64(string, &value), TAG, "Fail to parse"); + ESP_RETURN_ON_FALSE(value <= UINT32_MAX, ESP_ERR_INVALID_SIZE, TAG, "Overflow uint32"); + *value_u32 = value; + return ESP_OK; +} + +esp_err_t parse_u16(const char *string, uint16_t *value_u16) +{ + uint64_t value; + ESP_RETURN_ON_ERROR(parse_u64(string, &value), TAG, "Fail to parse"); + ESP_RETURN_ON_FALSE(value <= UINT16_MAX, ESP_ERR_INVALID_SIZE, TAG, "Overflow uint16"); + *value_u16 = value; + return ESP_OK; +} + +esp_err_t parse_u8(const char *string, uint8_t *value_u8) +{ + uint64_t value; + ESP_RETURN_ON_ERROR(parse_u64(string, &value), TAG, "Fail to parse"); + ESP_RETURN_ON_FALSE(value <= UINT8_MAX, ESP_ERR_INVALID_SIZE, TAG, "Overflow uint8"); + *value_u8 = value; + return ESP_OK; +} + +esp_err_t parse_hex_str(const char *string, uint8_t *buffer, size_t buffer_size, size_t* out_size) +{ + esp_err_t ret = ESP_OK; + int idx = 0; + const char *cur = string; + + ESP_GOTO_ON_FALSE(string, ESP_ERR_INVALID_ARG, exit, TAG, "Invalid string"); + /* We require HEX string to have prefix '0x' or '0X' */ + ESP_GOTO_ON_FALSE(cur[0] == '0' && (cur[1] == 'x' || cur[1] == 'x'), ESP_ERR_INVALID_ARG, exit, TAG, "Invalid HEX string prefix"); + ESP_GOTO_ON_FALSE(buffer, ESP_ERR_INVALID_ARG, exit, TAG, "Invalid buffer"); + ESP_GOTO_ON_FALSE(buffer_size, ESP_ERR_INVALID_ARG, exit, TAG, "Invalid buffer size"); + + cur += 2; + memset(buffer, 0, buffer_size); + + do { + uint8_t nibble_l = 0; + uint8_t nibble_h = 0; + + ESP_GOTO_ON_ERROR(parse_hex_digit(*cur++, &nibble_h), exit, TAG, "Invalid HEX format"); + ESP_GOTO_ON_ERROR(parse_hex_digit(*cur++, &nibble_l), exit, TAG, "Invalid HEX format"); + if (idx < buffer_size) { + buffer[idx] = (nibble_h << 4) + nibble_l; + } + idx++; + } while (*cur != '\0'); + + if (idx > buffer_size ) { + ret = ESP_ERR_INVALID_SIZE; + } + +exit: + if (out_size != NULL) { + *out_size = idx; + } + return ret; +} + +esp_err_t parse_ieee_addr(const char *string, esp_zb_ieee_addr_t ieee_addr) +{ +#define DATA_SIZE sizeof(esp_zb_ieee_addr_t) + uint8_t temp[DATA_SIZE] = {0}; + size_t parsed = 0; + ESP_RETURN_ON_ERROR(parse_hex_str(string, temp, DATA_SIZE, &parsed), TAG, "Fail to parse HEX data"); + if (parsed != DATA_SIZE) { + return ESP_ERR_INVALID_SIZE; + } + /* Reverse the result */ + for (int i = 0; i < DATA_SIZE; i++) { + ((uint8_t*)ieee_addr)[i] = temp[DATA_SIZE - 1 - i]; + } + return ESP_OK; +} + +esp_err_t parse_zcl_addr(const char *string, esp_zb_zcl_addr_t *addr) +{ + esp_err_t ret = parse_ieee_addr(string, addr->u.ieee_addr); + if (ret == ESP_OK) { + addr->addr_type = ESP_ZB_ZCL_ADDR_TYPE_IEEE; + } else { + uint16_t short_addr = 0; + ret = parse_u16(string, &short_addr); + if (ret == ESP_OK) { + addr->u.short_addr = short_addr; + addr->addr_type = ESP_ZB_ZCL_ADDR_TYPE_SHORT; + } + } + + return ret; +} + +esp_err_t parse_attr_access(const char *string, uint8_t *access) +{ + uint8_t tmp_access = 0; + const char *cur = string; + + ESP_RETURN_ON_FALSE(string != NULL, ESP_ERR_INVALID_ARG, TAG, "Invalid string"); + + do { + switch (*cur) { + case 'R': + case 'r': + tmp_access |= ESP_ZB_ZCL_ATTR_ACCESS_READ_ONLY; + break; + case 'W': + case 'w': + tmp_access |= ESP_ZB_ZCL_ATTR_ACCESS_WRITE_ONLY; + break; + case 'P': + case 'p': + tmp_access |= ESP_ZB_ZCL_ATTR_ACCESS_REPORTING; + break; + case 'S': + case 's': + tmp_access |= ESP_ZB_ZCL_ATTR_ACCESS_SCENE; + break; + default: + ESP_RETURN_ON_ERROR(ESP_ERR_INVALID_ARG, TAG, "Invalid attr access: '%c'", *cur); + break; + } + cur++; + } while (*cur != '\0'); + + *access = tmp_access; + + return ESP_OK; +} diff --git a/components/esp-zigbee-console/src/cmdline_parser.h b/components/esp-zigbee-console/src/cmdline_parser.h new file mode 100644 index 0000000..c30f947 --- /dev/null +++ b/components/esp-zigbee-console/src/cmdline_parser.h @@ -0,0 +1,114 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "esp_err.h" +#include "esp_zigbee_type.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Parse string to uint64_t + * + * @param[in] string A string contains the content to be parsed. + * @param[out] value_u64 Parsed value + * @return + * - ESP_OK: Success + * - ESP_ERR_INVALID_ARG: Wrong format + * - ESP_ERR_INVALID_SIZE: Value overflows + */ +esp_err_t parse_u64(const char *string, uint64_t *value_u64); + +/** + * @brief Parse string to uint32_t + * + * @param[in] string A string contains the content to be parsed. + * @param[out] value_u32 Parsed value + * @return + * - ESP_OK: Success + * - ESP_ERR_INVALID_ARG: Wrong format + * - ESP_ERR_INVALID_SIZE: Value overflows + */ +esp_err_t parse_u32(const char *string, uint32_t *value_u32); + +/** + * @brief Parse string to uint16_t + * + * @param[in] string A string contains the content to be parsed. + * @param[out] value_u16 Parsed value + * @return + * - ESP_OK: Success + * - ESP_ERR_INVALID_ARG: Wrong format + * - ESP_ERR_INVALID_SIZE: Value overflows + */ +esp_err_t parse_u16(const char *string, uint16_t *value_u16); + +/** + * @brief Parse string to uint8_t + * + * @param[in] string A string contains the content to be parsed. + * @param[out] value_u8 Parsed value + * @return + * - ESP_OK: Success + * - ESP_ERR_INVALID_ARG: Wrong format + * - ESP_ERR_INVALID_SIZE: Value overflows + */ +esp_err_t parse_u8(const char *string, uint8_t *value_u8); + +/** + * @brief Parse HEX string to data in the buffer + * + * @param[in] string A string contains the data in HEX. + * @param[out] buffer Buffer to put the parsed data. + * @param[in] buffer_size Buffer size of the provided buffer. + * @param[out] out_size Length of data. + * @return + * - ESP_OK: Success + * - ESP_ERR_INVALID_ARG: Invalid arguments + * - ESP_ERR_INVALID_SIZE: Insufficient buffer provided + */ +esp_err_t parse_hex_str(const char *string, uint8_t *buffer, size_t buffer_size, size_t* out_size); + +/** + * @brief Parse HEX string to IEEE address + * + * @param[in] string A string contains the data in HEX. + * @param[out] ieee_addr Parsed IEEE address. + * @return + * - ESP_OK: Success + * - ESP_ERR_INVALID_ARG: Invalid arguments + * - ESP_ERR_INVALID_SIZE: Wrong string length + */ +esp_err_t parse_ieee_addr(const char *string, esp_zb_ieee_addr_t ieee_addr); + +/** + * @brief Parse HEX string to @ref esp_zb_zcl_addr_t + * + * @param[in] string A string contains the data in HEX. + * @param[out] addr Parsed esp_zb_zcl_addr_t structure. + * @return + * - ESP_OK: Success + * - ESP_ERR_INVALID_ARG: Invalid arguments + */ +esp_err_t parse_zcl_addr(const char *string, esp_zb_zcl_addr_t *addr); + +/** + * @brief Parse setting string to attribute access value + * + * @param[in] string A string contains the attribute access settings. + * @param[out] access Parsed attribute access value + * @return + * - ESP_OK: Success + * - ESP_ERR_INVALID_ARG: Wrong setting + */ +esp_err_t parse_attr_access(const char *string, uint8_t *access); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/components/esp-zigbee-console/src/esp_zigbee_console.c b/components/esp-zigbee-console/src/esp_zigbee_console.c new file mode 100644 index 0000000..68e0e69 --- /dev/null +++ b/components/esp-zigbee-console/src/esp_zigbee_console.c @@ -0,0 +1,202 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include "freertos/FreeRTOS.h" +#include "esp_check.h" +#include "esp_console.h" + +#include "cli_cmd.h" +#include "cli_cmd_zcl.h" +#include "esp_zigbee_console.h" + +#define TAG "esp-zigbee-console" + +typedef struct esp_zigbee_console_context_s { + esp_zb_cli_context_t cli_ctx; + esp_console_repl_t *repl; + TaskHandle_t repl_task_hdl; +} esp_zigbee_console_context_t; + +static esp_zigbee_console_context_t *s_console_ctx = NULL; + +static esp_err_t esp_zb_console_init_ctx(void) +{ + s_console_ctx = calloc(1, sizeof(esp_zigbee_console_context_t)); + ESP_RETURN_ON_FALSE(s_console_ctx, ESP_ERR_NO_MEM, TAG, "No memory for console context"); + return ESP_OK; +} + +static esp_err_t esp_zb_console_core_action_handler(esp_zb_core_action_callback_id_t callback_id, const void *message) +{ + esp_err_t ret = ESP_ERR_NOT_SUPPORTED; + ret = cli_zcl_core_action_handler(callback_id, message); + if (ret != ESP_OK) { + ESP_LOGW(TAG, "Receive Zigbee action(0x%x) callback", callback_id); + } + return ret; +} + +esp_zb_cli_context_t *esp_zb_console_get_cli_ctx(void) +{ + return &s_console_ctx->cli_ctx; +} + +esp_err_t esp_zb_console_manage_ep_list(esp_zb_ep_list_t *ep_list) +{ + esp_err_t ret = ESP_OK; + ESP_GOTO_ON_FALSE(s_console_ctx, ESP_ERR_INVALID_STATE, exit, TAG, "Console is not initialized"); + + esp_zb_cli_context_t *cli_ctx = &s_console_ctx->cli_ctx; + ESP_GOTO_ON_FALSE(!(cli_ctx->ep_list), ESP_ERR_INVALID_STATE, exit, TAG, "Already has managed endpoint list"); + + if (ep_list == NULL) { + cli_ctx->ep_list = esp_zb_ep_list_create(); + } else { + cli_ctx->ep_list = ep_list; + } + +exit: + return ret; +} + +/* This function is used to block the REPL task to wait for + * the result of asynchronous operation. It is expected to + * be called only from the REPL task. + */ +static esp_err_t esp_zb_console_wait_result(void) { + esp_err_t ret = ESP_OK; + if (unlikely(s_console_ctx->repl_task_hdl == NULL)) { + s_console_ctx->repl_task_hdl = xTaskGetCurrentTaskHandle(); + } + if (xTaskNotifyWait(0, ULONG_MAX, (uint32_t*)&ret, portMAX_DELAY) != pdPASS) { + ret = ESP_FAIL; + } + return ret; +} + +/* This function is used to notify the blocked the REPL task of + * the result of asynchronous operation. It is expected to be + * called only from the Zigbee Main task (by callbacks of requests). + */ +esp_err_t esp_zb_console_notify_result(esp_err_t result) { + esp_err_t ret = ESP_OK; + ESP_RETURN_ON_FALSE(s_console_ctx->repl_task_hdl, ESP_ERR_INVALID_STATE, TAG, "Task handle is NULL"); + if (xTaskNotify(s_console_ctx->repl_task_hdl, result, eSetValueWithOverwrite) != pdPASS) { + ret = ESP_FAIL; + } + + return ret; +} + +static esp_err_t esp_zb_console_cmd_handler(int argc, char **argv) +{ + extern const esp_zb_cli_cmd_t _esp_zb_cli_cmd_array_start; + extern const esp_zb_cli_cmd_t _esp_zb_cli_cmd_array_end; + + esp_err_t ret = ESP_ERR_NOT_FOUND; + for (const esp_zb_cli_cmd_t *cmd = &_esp_zb_cli_cmd_array_start; cmd != &_esp_zb_cli_cmd_array_end; cmd++) { + if (!strcmp(argv[0], cmd->name)) { + esp_zb_lock_acquire(portMAX_DELAY); + ret = esp_zb_cli_process_cmd((esp_zb_cli_cmd_t *)cmd, argc, argv); + esp_zb_lock_release(); + if (ret == ESP_ERR_NOT_FINISHED) { + ret = esp_zb_console_wait_result(); + } + break; + } + } + + return ret; +} + +static esp_err_t esp_zb_console_cmd_register_all(void) +{ + extern const esp_zb_cli_cmd_t _esp_zb_cli_cmd_array_start; + extern const esp_zb_cli_cmd_t _esp_zb_cli_cmd_array_end; + + ESP_LOGI(TAG, "List of ESP Zigbee Console commands:"); + for (const esp_zb_cli_cmd_t *cmd = &_esp_zb_cli_cmd_array_start; cmd != &_esp_zb_cli_cmd_array_end; cmd++) { + esp_console_cmd_t command = { + .command = cmd->name, + .help = cmd->help, + .func = esp_zb_console_cmd_handler, + }; + ESP_RETURN_ON_ERROR(esp_console_cmd_register(&command), TAG, "Unable to register %s cmd", cmd->name); + ESP_LOGI(TAG, " - Command '%s'", cmd->name); + } + + return ESP_OK; +} + +static esp_err_t esp_zb_console_repl_init(void) +{ +/* Task name used by esp_console for REPL task creation. + * The name MUST be aligned with esp_console_repl.c + */ +#define REPL_TASK_NAME "console_repl" + + esp_err_t ret = ESP_OK; + esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT(); + + /* Install console REPL environment */ +#if defined(CONFIG_ESP_CONSOLE_UART_DEFAULT) || defined(CONFIG_ESP_CONSOLE_UART_CUSTOM) + esp_console_dev_uart_config_t hw_config = ESP_CONSOLE_DEV_UART_CONFIG_DEFAULT(); + ESP_GOTO_ON_ERROR(esp_console_new_repl_uart(&hw_config, &repl_config, &s_console_ctx->repl), exit, + TAG, "Fail to install console REPL environment"); +#elif defined(CONFIG_ESP_CONSOLE_USB_CDC) + esp_console_dev_usb_cdc_config_t hw_config = ESP_CONSOLE_DEV_CDC_CONFIG_DEFAULT(); + ESP_GOTO_ON_ERROR(esp_console_new_repl_usb_cdc(&hw_config, &repl_config, &s_console_ctx->repl), exit, + TAG, "Fail to install console REPL environment"); +#elif defined(CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG) + esp_console_dev_usb_serial_jtag_config_t hw_config = ESP_CONSOLE_DEV_USB_SERIAL_JTAG_CONFIG_DEFAULT(); + ESP_GOTO_ON_ERROR(esp_console_new_repl_usb_serial_jtag(&hw_config, &repl_config, &s_console_ctx->repl), exit, + TAG, "Fail to install console REPL environment"); +#else +#error Unsupported console type +#endif + + s_console_ctx->repl_task_hdl = xTaskGetHandle(REPL_TASK_NAME); + ESP_GOTO_ON_FALSE(s_console_ctx->repl_task_hdl, ESP_FAIL, exit, + TAG, "Fail to get REPL task handle by name: %s", REPL_TASK_NAME); + +exit: + return ret; +} + +esp_err_t esp_zb_console_start(void) +{ + ESP_RETURN_ON_FALSE(s_console_ctx, ESP_ERR_INVALID_STATE, TAG, "Console is not initialized"); + return esp_console_start_repl(s_console_ctx->repl); +} + +esp_err_t esp_zb_console_init(void) +{ + esp_err_t ret = ESP_OK; + + ESP_GOTO_ON_ERROR(esp_zb_console_init_ctx(), exit, TAG, "Fail to init console context"); + ESP_GOTO_ON_ERROR(esp_zb_console_cmd_register_all(), exit, TAG, "Fail to register all commands"); + ESP_GOTO_ON_ERROR(esp_zb_console_repl_init(), exit, TAG, "Fail to init console REPL"); + esp_zb_core_action_handler_register(esp_zb_console_core_action_handler); + +exit: + return ret; +} + +esp_err_t esp_zb_console_deinit(void) +{ + esp_err_t ret = ESP_OK; + ESP_GOTO_ON_FALSE(s_console_ctx, ESP_ERR_INVALID_STATE, exit, TAG, "Console is not initialized"); + ESP_GOTO_ON_ERROR(s_console_ctx->repl->del(s_console_ctx->repl), exit, TAG, "Fail to deinit console"); + s_console_ctx->repl = NULL; + s_console_ctx->repl_task_hdl = NULL; + free(s_console_ctx); + s_console_ctx = NULL; + +exit: + return ret; +} diff --git a/components/esp-zigbee-console/src/esp_zigbee_console.h b/components/esp-zigbee-console/src/esp_zigbee_console.h new file mode 100644 index 0000000..1e54b3e --- /dev/null +++ b/components/esp-zigbee-console/src/esp_zigbee_console.h @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include_next "esp_zigbee_console.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct esp_zb_cli_context_s { + esp_zb_ep_list_t *ep_list; +} esp_zb_cli_context_t; + +esp_zb_cli_context_t *esp_zb_console_get_cli_ctx(void); + +#define CLI_CTX() (*esp_zb_console_get_cli_ctx()) + +esp_err_t esp_zb_console_notify_result(esp_err_t result); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/components/esp-zigbee-console/src/zb_data/ha.c b/components/esp-zigbee-console/src/zb_data/ha.c new file mode 100644 index 0000000..b38821c --- /dev/null +++ b/components/esp-zigbee-console/src/zb_data/ha.c @@ -0,0 +1,166 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include "ha/esp_zigbee_ha_standard.h" + +#include "../cli_util.h" + +typedef esp_zb_cluster_list_t* (*ep_create_fn_t)(void *); + +typedef struct esp_zcl_ha_device_type_s { + const char *device_name; + uint16_t device_id; + ep_create_fn_t create_fn; +} esp_zcl_ha_device_t; + +/** + * @brief Define an HA device entry in the table + * + * @param name Type name of the HA device + * @param device_id ID of the HA device type + * + */ +#define HA_DEVICE_ENTRY(name, device_id) \ + { \ + #name, device_id, \ + (ep_create_fn_t)esp_zb_ ## name ## _clusters_create, \ + } + +/** + * @brief Define an HA device (not support) entry in the table + * + * @param name Type name of the HA device + * @param device_id ID of the HA device type + * + */ +#define HA_DEVICE_ENTRY_UNSUPPORT(name, device_id) \ + { \ + #name, device_id, NULL, \ + } + +/* The table contains the information of HA standard device supported by esp-zigbee-sdk. + * + * Note: The entries in the table MUST be arranged in ascending order of device type ID. + */ +static const esp_zcl_ha_device_t s_ha_device_table[] = { + HA_DEVICE_ENTRY(on_off_switch, ESP_ZB_HA_ON_OFF_SWITCH_DEVICE_ID), + HA_DEVICE_ENTRY_UNSUPPORT(level_control_switch, ESP_ZB_HA_LEVEL_CONTROL_SWITCH_DEVICE_ID), + HA_DEVICE_ENTRY_UNSUPPORT(on_off_output, ESP_ZB_HA_ON_OFF_OUTPUT_DEVICE_ID), + HA_DEVICE_ENTRY_UNSUPPORT(level_controllable_output, ESP_ZB_HA_LEVEL_CONTROLLABLE_OUTPUT_DEVICE_ID), + HA_DEVICE_ENTRY_UNSUPPORT(scene_selector, ESP_ZB_HA_SCENE_SELECTOR_DEVICE_ID), + HA_DEVICE_ENTRY(configuration_tool, ESP_ZB_HA_CONFIGURATION_TOOL_DEVICE_ID), + HA_DEVICE_ENTRY_UNSUPPORT(remote_control, ESP_ZB_HA_REMOTE_CONTROL_DEVICE_ID), + HA_DEVICE_ENTRY_UNSUPPORT(combined_interface, ESP_ZB_HA_COMBINED_INTERFACE_DEVICE_ID), + HA_DEVICE_ENTRY_UNSUPPORT(range_extender, ESP_ZB_HA_RANGE_EXTENDER_DEVICE_ID), + HA_DEVICE_ENTRY(mains_power_outlet, ESP_ZB_HA_MAINS_POWER_OUTLET_DEVICE_ID), + HA_DEVICE_ENTRY(door_lock, ESP_ZB_HA_DOOR_LOCK_DEVICE_ID), + HA_DEVICE_ENTRY(door_lock_controller, ESP_ZB_HA_DOOR_LOCK_CONTROLLER_DEVICE_ID), + HA_DEVICE_ENTRY_UNSUPPORT(simple_sensor, ESP_ZB_HA_SIMPLE_SENSOR_DEVICE_ID), + HA_DEVICE_ENTRY_UNSUPPORT(consumption_awareness, ESP_ZB_HA_CONSUMPTION_AWARENESS_DEVICE_ID), + HA_DEVICE_ENTRY_UNSUPPORT(home_gateway, ESP_ZB_HA_HOME_GATEWAY_DEVICE_ID), + HA_DEVICE_ENTRY_UNSUPPORT(smart_plug, ESP_ZB_HA_SMART_PLUG_DEVICE_ID), + HA_DEVICE_ENTRY_UNSUPPORT(white_goods, ESP_ZB_HA_WHITE_GOODS_DEVICE_ID), + HA_DEVICE_ENTRY_UNSUPPORT(meter_interface, ESP_ZB_HA_METER_INTERFACE_DEVICE_ID), + HA_DEVICE_ENTRY(on_off_light, ESP_ZB_HA_ON_OFF_LIGHT_DEVICE_ID), + HA_DEVICE_ENTRY_UNSUPPORT(dimmable_light, ESP_ZB_HA_DIMMABLE_LIGHT_DEVICE_ID), + HA_DEVICE_ENTRY(color_dimmable_light, ESP_ZB_HA_COLOR_DIMMABLE_LIGHT_DEVICE_ID), + HA_DEVICE_ENTRY_UNSUPPORT(dimmable_switch, ESP_ZB_HA_DIMMER_SWITCH_DEVICE_ID), + HA_DEVICE_ENTRY(color_dimmable_switch, ESP_ZB_HA_COLOR_DIMMER_SWITCH_DEVICE_ID), + HA_DEVICE_ENTRY(light_sensor, ESP_ZB_HA_LIGHT_SENSOR_DEVICE_ID), + HA_DEVICE_ENTRY(shade, ESP_ZB_HA_SHADE_DEVICE_ID), + HA_DEVICE_ENTRY(shade_controller, ESP_ZB_HA_SHADE_CONTROLLER_DEVICE_ID), + HA_DEVICE_ENTRY(window_covering, ESP_ZB_HA_WINDOW_COVERING_DEVICE_ID), + HA_DEVICE_ENTRY(window_covering_controller, ESP_ZB_HA_WINDOW_COVERING_CONTROLLER_DEVICE_ID), + HA_DEVICE_ENTRY_UNSUPPORT(heating_cooling_unit, ESP_ZB_HA_HEATING_COOLING_UNIT_DEVICE_ID), + HA_DEVICE_ENTRY(thermostat, ESP_ZB_HA_THERMOSTAT_DEVICE_ID), + HA_DEVICE_ENTRY(temperature_sensor, ESP_ZB_HA_TEMPERATURE_SENSOR_DEVICE_ID), + HA_DEVICE_ENTRY_UNSUPPORT(ias_control_indicating, ESP_ZB_HA_IAS_CONTROL_INDICATING_EQUIPMENT_ID), + HA_DEVICE_ENTRY_UNSUPPORT(ias_ancillary_control, ESP_ZB_HA_IAS_ANCILLARY_CONTROL_EQUIPMENT_ID), + HA_DEVICE_ENTRY_UNSUPPORT(ias_zone, ESP_ZB_HA_IAS_ZONE_ID), + HA_DEVICE_ENTRY_UNSUPPORT(ias_warning, ESP_ZB_HA_IAS_WARNING_DEVICE_ID), + HA_DEVICE_ENTRY_UNSUPPORT(test_device, ESP_ZB_HA_TEST_DEVICE_ID), + HA_DEVICE_ENTRY_UNSUPPORT(custom_tunnel, ESP_ZB_HA_CUSTOM_TUNNEL_DEVICE_ID), + HA_DEVICE_ENTRY_UNSUPPORT(custom_attr, ESP_ZB_HA_CUSTOM_ATTR_DEVICE_ID), +}; + +static const esp_zcl_ha_device_t* device_table_get_by_idx(uint16_t idx) +{ + if (idx < ARRAY_SIZE(s_ha_device_table)) { + return &s_ha_device_table[idx]; + } else { + return NULL; + } +} + +static const esp_zcl_ha_device_t* device_table_get_by_id(uint16_t device_id) +{ + const esp_zcl_ha_device_t *entry; + uint16_t left = 0; + uint16_t right = ARRAY_SIZE(s_ha_device_table); + + while (left < right) { + uint16_t middle = (left + right) / 2; + + entry = &s_ha_device_table[middle]; + + if (entry->device_id == device_id) { + return entry; + } else if (entry->device_id < device_id) { + left = middle + 1; + } else { + right = middle; + } + } + + return NULL; +} + +static const esp_zcl_ha_device_t* device_table_get_by_name(const char *name) +{ + for (int idx = 0; idx < ARRAY_SIZE(s_ha_device_table); idx++) { + if (!strcmp(s_ha_device_table[idx].device_name, name)) { + return &s_ha_device_table[idx]; + } + } + + return NULL; +} + +const char* esp_zb_get_device_name_by_idx(uint16_t idx) +{ + const esp_zcl_ha_device_t *ent = device_table_get_by_idx(idx); + return ent == NULL ? "" : ent->device_name; +} + +const char* esp_zb_get_device_name_by_id(uint16_t device_id) +{ + const esp_zcl_ha_device_t *ent = device_table_get_by_id(device_id); + return ent == NULL ? "" : ent->device_name; +} + +uint16_t esp_zb_get_device_id_by_name(const char *name) +{ + const esp_zcl_ha_device_t *ent = device_table_get_by_name(name); + return ent == NULL ? 0xFFFF : ent->device_id; +} + +bool esp_zb_is_device_supported(uint16_t device_id) +{ + const esp_zcl_ha_device_t *ent = device_table_get_by_id(device_id); + return ent != NULL && ent->create_fn != NULL; +} + +esp_zb_cluster_list_t *esp_zb_ep_create_ha_default(uint16_t ha_device_id) +{ + esp_zb_cluster_list_t *ep = NULL; + const esp_zcl_ha_device_t *ent = device_table_get_by_id(ha_device_id); + if (ent != NULL && ent->create_fn != NULL) { + ep = ent->create_fn(NULL); + } + return ep; +} diff --git a/components/esp-zigbee-console/src/zb_data/ha.h b/components/esp-zigbee-console/src/zb_data/ha.h new file mode 100644 index 0000000..448c589 --- /dev/null +++ b/components/esp-zigbee-console/src/zb_data/ha.h @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include "esp_err.h" +#include "esp_zigbee_type.h" + +#ifdef __cplusplus +extern "C" { +#endif + +const char* esp_zb_get_device_name_by_idx(uint16_t idx); + +const char* esp_zb_get_device_name_by_id(uint16_t device_id); + +uint16_t esp_zb_get_device_id_by_name(const char *name); + +bool esp_zb_is_device_supported(uint16_t device_id); + +esp_zb_cluster_list_t *esp_zb_ep_create_ha_default(uint16_t ha_device_id); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/components/esp-zigbee-console/src/zb_data/zcl.c b/components/esp-zigbee-console/src/zb_data/zcl.c new file mode 100644 index 0000000..581bff0 --- /dev/null +++ b/components/esp-zigbee-console/src/zb_data/zcl.c @@ -0,0 +1,226 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include "esp_zigbee_core.h" + +#include "../cli_util.h" + +typedef esp_err_t (*register_fn_t)(esp_zb_cluster_list_t *cluster_list, esp_zb_attribute_list_t *attr_list, uint8_t role_mask); +typedef esp_zb_attribute_list_t* (*create_fn_t)(void *); +typedef esp_err_t (*add_attr_fn_t)(esp_zb_attribute_list_t *attr_list, uint16_t attr_id, void *value_p); + +typedef struct esp_zcl_cluster_fn_s { + const char *cluster_name; + uint16_t cluster_id; + register_fn_t register_fn; /*!< function to add cluster (attribute list) to cluster list */ + create_fn_t create_fn; /*!< function to create default cluster (attribute list) with mandatory attributes */ + add_attr_fn_t add_attr_fn; /*!< function to add attibute to cluster (attribute list) */ +} esp_zcl_cluster_fn_t; + +/** + * @brief Define a cluster fn entry in the table + * + * @param name Common name of the cluster + * @param cluster_id ID of the cluster + * + */ +#define CLUSTER_FN_ENTRY(name, cluster_id) \ + { \ + #name, cluster_id, \ + esp_zb_cluster_list_add_ ## name ## _cluster, \ + (create_fn_t)esp_zb_ ## name ## _cluster_create, \ + esp_zb_ ## name ## _cluster_add_attr, \ + } + +/** + * @brief Define a cluster (with no attribute) fn entry in the table + * + * @param name Common name of the cluster + * @param cluster_id ID of the cluster + * + */ +#define CLUSTER_NO_ATTR_FN_ENTRY(name, cluster_id) \ + { \ + #name, cluster_id, \ + esp_zb_cluster_list_add_ ## name ## _cluster, \ + (create_fn_t)esp_zb_ ## name ## _cluster_create, \ + NULL, \ + } + +/** + * @brief Define a cluster (not support) fn entry in the table + * + * @param name Common name of the cluster + * @param cluster_id ID of the cluster + * + */ +#define CLUSTER_NON_SUPPORT_FN_ENTRY(name, cluster_id) \ + { \ + #name, cluster_id, \ + NULL, NULL, NULL, \ + } + +/* The table contains the information of clusters supported by esp-zigbee-sdk. + * + * Note: The entries in the table MUST be arranged in ascending order of cluster ID. + */ +const static esp_zcl_cluster_fn_t s_cluster_fn_table[] = { + CLUSTER_FN_ENTRY(basic, ESP_ZB_ZCL_CLUSTER_ID_BASIC), + CLUSTER_FN_ENTRY(power_config, ESP_ZB_ZCL_CLUSTER_ID_POWER_CONFIG), + CLUSTER_NON_SUPPORT_FN_ENTRY(device_temp_config, ESP_ZB_ZCL_CLUSTER_ID_DEVICE_TEMP_CONFIG), + CLUSTER_FN_ENTRY(identify, ESP_ZB_ZCL_CLUSTER_ID_IDENTIFY), + CLUSTER_FN_ENTRY(groups, ESP_ZB_ZCL_CLUSTER_ID_GROUPS), + CLUSTER_FN_ENTRY(scenes, ESP_ZB_ZCL_CLUSTER_ID_SCENES), + CLUSTER_FN_ENTRY(on_off, ESP_ZB_ZCL_CLUSTER_ID_ON_OFF), + CLUSTER_FN_ENTRY(on_off_switch_config, ESP_ZB_ZCL_CLUSTER_ID_ON_OFF_SWITCH_CONFIG), + CLUSTER_FN_ENTRY(level, ESP_ZB_ZCL_CLUSTER_ID_LEVEL_CONTROL), + CLUSTER_NON_SUPPORT_FN_ENTRY(alarms, ESP_ZB_ZCL_CLUSTER_ID_ALARMS), + CLUSTER_FN_ENTRY(time, ESP_ZB_ZCL_CLUSTER_ID_TIME), + CLUSTER_NON_SUPPORT_FN_ENTRY(rssi_location, ESP_ZB_ZCL_CLUSTER_ID_RSSI_LOCATION), + CLUSTER_FN_ENTRY(analog_input, ESP_ZB_ZCL_CLUSTER_ID_ANALOG_INPUT), + CLUSTER_FN_ENTRY(analog_output, ESP_ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT), + CLUSTER_FN_ENTRY(analog_value, ESP_ZB_ZCL_CLUSTER_ID_ANALOG_VALUE), + CLUSTER_FN_ENTRY(binary_input, ESP_ZB_ZCL_CLUSTER_ID_BINARY_INPUT), + CLUSTER_NON_SUPPORT_FN_ENTRY(binary_output, ESP_ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT), + CLUSTER_NON_SUPPORT_FN_ENTRY(binary_value, ESP_ZB_ZCL_CLUSTER_ID_BINARY_VALUE), + CLUSTER_NON_SUPPORT_FN_ENTRY(multistate_input, ESP_ZB_ZCL_CLUSTER_ID_MULTI_INPUT), + CLUSTER_NON_SUPPORT_FN_ENTRY(multistate_output, ESP_ZB_ZCL_CLUSTER_ID_MULTI_OUTPUT), + CLUSTER_FN_ENTRY(multistate_value, ESP_ZB_ZCL_CLUSTER_ID_MULTI_VALUE), + CLUSTER_FN_ENTRY(commissioning, ESP_ZB_ZCL_CLUSTER_ID_COMMISSIONING), + CLUSTER_FN_ENTRY(ota, ESP_ZB_ZCL_CLUSTER_ID_OTA_UPGRADE), + CLUSTER_NON_SUPPORT_FN_ENTRY(poll_control, ESP_ZB_ZCL_CLUSTER_ID_POLL_CONTROL), + CLUSTER_NON_SUPPORT_FN_ENTRY(green_power, ESP_ZB_ZCL_CLUSTER_ID_GREEN_POWER), /* No API provided */ + CLUSTER_NON_SUPPORT_FN_ENTRY(keep_alive, ESP_ZB_ZCL_CLUSTER_ID_KEEP_ALIVE), + CLUSTER_FN_ENTRY(shade_config, ESP_ZB_ZCL_CLUSTER_ID_SHADE_CONFIG), + CLUSTER_FN_ENTRY(door_lock, ESP_ZB_ZCL_CLUSTER_ID_DOOR_LOCK), + CLUSTER_FN_ENTRY(window_covering, ESP_ZB_ZCL_CLUSTER_ID_WINDOW_COVERING), + CLUSTER_NON_SUPPORT_FN_ENTRY(pump_config_control, ESP_ZB_ZCL_CLUSTER_ID_PUMP_CONFIG_CONTROL), + CLUSTER_FN_ENTRY(thermostat, ESP_ZB_ZCL_CLUSTER_ID_THERMOSTAT), + CLUSTER_FN_ENTRY(fan_control, ESP_ZB_ZCL_CLUSTER_ID_FAN_CONTROL), + CLUSTER_NON_SUPPORT_FN_ENTRY(dehumid_control, ESP_ZB_ZCL_CLUSTER_ID_DEHUMID_CONTROL), + CLUSTER_FN_ENTRY(thermostat_ui_config, ESP_ZB_ZCL_CLUSTER_ID_THERMOSTAT_UI_CONFIG), + CLUSTER_FN_ENTRY(color_control, ESP_ZB_ZCL_CLUSTER_ID_COLOR_CONTROL), + CLUSTER_NON_SUPPORT_FN_ENTRY(ballast_config, ESP_ZB_ZCL_CLUSTER_ID_BALLAST_CONFIG), + CLUSTER_FN_ENTRY(illuminance_meas, ESP_ZB_ZCL_CLUSTER_ID_ILLUMINANCE_MEASUREMENT), + CLUSTER_FN_ENTRY(temperature_meas, ESP_ZB_ZCL_CLUSTER_ID_TEMP_MEASUREMENT), + CLUSTER_FN_ENTRY(pressure_meas, ESP_ZB_ZCL_CLUSTER_ID_PRESSURE_MEASUREMENT), + CLUSTER_FN_ENTRY(flow_meas, ESP_ZB_ZCL_CLUSTER_ID_FLOW_MEASUREMENT), + CLUSTER_FN_ENTRY(humidity_meas, ESP_ZB_ZCL_CLUSTER_ID_REL_HUMIDITY_MEASUREMENT), + CLUSTER_FN_ENTRY(occupancy_sensing, ESP_ZB_ZCL_CLUSTER_ID_OCCUPANCY_SENSING), + CLUSTER_FN_ENTRY(ph_measurement, ESP_ZB_ZCL_CLUSTER_ID_PH_MEASUREMENT), + CLUSTER_FN_ENTRY(ec_measurement, ESP_ZB_ZCL_CLUSTER_ID_EC_MEASUREMENT), + CLUSTER_FN_ENTRY(wind_speed_measurement, ESP_ZB_ZCL_CLUSTER_ID_WIND_SPEED_MEASUREMENT), + CLUSTER_FN_ENTRY(carbon_dioxide_measurement, ESP_ZB_ZCL_CLUSTER_ID_CARBON_DIOXIDE_MEASUREMENT), + CLUSTER_FN_ENTRY(pm2_5_measurement, ESP_ZB_ZCL_CLUSTER_ID_PM2_5_MEASUREMENT), + CLUSTER_FN_ENTRY(ias_zone, ESP_ZB_ZCL_CLUSTER_ID_IAS_ZONE), + CLUSTER_NO_ATTR_FN_ENTRY(ias_ace, ESP_ZB_ZCL_CLUSTER_ID_IAS_ACE), /* Only have zone table internal */ + CLUSTER_FN_ENTRY(ias_wd, ESP_ZB_ZCL_CLUSTER_ID_IAS_WD), + CLUSTER_NO_ATTR_FN_ENTRY(price, ESP_ZB_ZCL_CLUSTER_ID_PRICE), /* Too many attributes */ + CLUSTER_NO_ATTR_FN_ENTRY(metering, ESP_ZB_ZCL_CLUSTER_ID_METERING), /* Too many attributes */ + CLUSTER_FN_ENTRY(meter_identification, ESP_ZB_ZCL_CLUSTER_ID_METER_IDENTIFICATION), + CLUSTER_FN_ENTRY(electrical_meas, ESP_ZB_ZCL_CLUSTER_ID_ELECTRICAL_MEASUREMENT), + CLUSTER_FN_ENTRY(diagnostics, ESP_ZB_ZCL_CLUSTER_ID_DIAGNOSTICS), + CLUSTER_NO_ATTR_FN_ENTRY(touchlink_commissioning, 0x1000), +}; + +static const esp_zcl_cluster_fn_t* cluster_fn_table_get_by_idx(uint16_t idx) +{ + if (idx < ARRAY_SIZE(s_cluster_fn_table)) { + return &s_cluster_fn_table[idx]; + } else { + return NULL; + } +} + +static const esp_zcl_cluster_fn_t* cluster_fn_table_get_by_id(uint16_t cluster_id) +{ + const esp_zcl_cluster_fn_t *entry; + uint16_t left = 0; + uint16_t right = ARRAY_SIZE(s_cluster_fn_table); + + while (left < right) { + uint16_t middle = (left + right) / 2; + + entry = &s_cluster_fn_table[middle]; + + if (entry->cluster_id == cluster_id) { + return entry; + } else if (entry->cluster_id < cluster_id) { + left = middle + 1; + } else { + right = middle; + } + } + + return NULL; +} + +static const esp_zcl_cluster_fn_t* cluster_fn_table_get_by_name(const char *name) +{ + for (int idx = 0; idx < ARRAY_SIZE(s_cluster_fn_table); idx++) { + if (!strcmp(s_cluster_fn_table[idx].cluster_name, name)) { + return &s_cluster_fn_table[idx]; + } + } + + return NULL; +} + +const char* esp_zb_get_cluster_name_by_idx(uint16_t idx) +{ + const esp_zcl_cluster_fn_t *ent = cluster_fn_table_get_by_idx(idx); + return ent == NULL ? "" : ent->cluster_name; +} + +const char* esp_zb_get_cluster_name_by_id(uint16_t cluster_id) +{ + const esp_zcl_cluster_fn_t *ent = cluster_fn_table_get_by_id(cluster_id); + return ent == NULL ? "" : ent->cluster_name; +} + +uint16_t esp_zb_get_cluster_id_by_name(const char *name) +{ + const esp_zcl_cluster_fn_t *ent = cluster_fn_table_get_by_name(name); + return ent == NULL ? 0xFFFF : ent->cluster_id; +} + +esp_zb_attribute_list_t *esp_zb_cluster_create_default(uint16_t cluster_id) +{ + esp_zb_attribute_list_t *cluster = NULL; + const esp_zcl_cluster_fn_t *ent = cluster_fn_table_get_by_id(cluster_id); + if (ent != NULL && ent->create_fn != NULL) { + cluster = ent->create_fn(NULL); + } else { + cluster = esp_zb_zcl_attr_list_create(cluster_id); + } + return cluster; +} + +esp_err_t esp_zb_cluster_add_std_attr(esp_zb_attribute_list_t *attr_list, uint16_t attr_id, void *value_p) +{ + esp_err_t ret = ESP_OK; + const esp_zcl_cluster_fn_t *ent = cluster_fn_table_get_by_id(attr_list->next->cluster_id); + if (ent != NULL && ent->add_attr_fn != NULL) { + ret = ent->add_attr_fn(attr_list, attr_id, value_p); + } else { + ret = ESP_ERR_NOT_SUPPORTED; + } + return ret; +} + +esp_err_t esp_zb_cluster_register(esp_zb_cluster_list_t *cluster_list, esp_zb_attribute_list_t *attr_list, uint8_t role_mask) +{ + esp_err_t ret = ESP_OK; + const esp_zcl_cluster_fn_t *ent = cluster_fn_table_get_by_id(attr_list->next->cluster_id); + if (ent != NULL && ent->register_fn != NULL) { + ret = ent->register_fn(cluster_list, attr_list, role_mask); + } else { + ret = ESP_ERR_NOT_SUPPORTED; + } + return ret; +} diff --git a/components/esp-zigbee-console/src/zb_data/zcl.h b/components/esp-zigbee-console/src/zb_data/zcl.h new file mode 100644 index 0000000..cfe09ea --- /dev/null +++ b/components/esp-zigbee-console/src/zb_data/zcl.h @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include "esp_err.h" +#include "esp_zigbee_type.h" + +#ifdef __cplusplus +extern "C" { +#endif + +const char* esp_zb_get_cluster_name_by_idx(uint16_t idx); + +const char* esp_zb_get_cluster_name_by_id(uint16_t cluster_id); + +uint16_t esp_zb_get_cluster_id_by_name(const char *name); + +esp_zb_attribute_list_t *esp_zb_cluster_create_default(uint16_t cluster_id); + +esp_err_t esp_zb_cluster_add_std_attr(esp_zb_attribute_list_t *attr_list, uint16_t attr_id, void *value_p); + +esp_err_t esp_zb_cluster_register(esp_zb_cluster_list_t *cluster_list, esp_zb_attribute_list_t *attr_list, uint8_t role_mask); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/examples/.build-rules.yml b/examples/.build-rules.yml index 5c30fa4..0f367ae 100644 --- a/examples/.build-rules.yml +++ b/examples/.build-rules.yml @@ -1,5 +1,11 @@ # Documentation: .gitlab/ci/README.md#manifest-file-to-control-the-buildtest-apps +examples/esp_zigbee_all_device_types_app: + enable: + - if: IDF_TARGET in ["esp32c6", "esp32h2"] + temporary: true + reason: the other targets are not tested yet + examples/esp_zigbee_cli: enable: - if: IDF_TARGET in ["esp32", "esp32s2", "esp32c3", "esp32s3", "esp32c6", "esp32h2"] diff --git a/examples/esp_zigbee_all_device_types_app/CMakeLists.txt b/examples/esp_zigbee_all_device_types_app/CMakeLists.txt new file mode 100644 index 0000000..b427a61 --- /dev/null +++ b/examples/esp_zigbee_all_device_types_app/CMakeLists.txt @@ -0,0 +1,6 @@ +# The following lines of boilerplate have to be in your project's CMakeLists +# in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(esp_zigbee_all_device_types_app) diff --git a/examples/esp_zigbee_all_device_types_app/README.md b/examples/esp_zigbee_all_device_types_app/README.md new file mode 100644 index 0000000..b66ff8d --- /dev/null +++ b/examples/esp_zigbee_all_device_types_app/README.md @@ -0,0 +1,242 @@ +| Supported Targets | ESP32-H2 | ESP32-C6 | +| ----------------- | -------- | -------- | + +# All Device Types App Example + +This test code shows how to configure Zigbee device and use it as HA Device Types, such as On/Off Switch, Window Covering, Door Lock, On/Off Light Device and so on. + +## Hardware Required +* One development board with ESP32-H2 SoC (loaded with esp_zigbee_all_device_types_app example). +* Another development board with ESP32-H2 SoC as communication peer (loaded with esp_zigbee_all_device_types_app example or other examples). + +## Configure the Project + +Before project configuration and build, make sure to set the correct chip target using `idf.py set-target ` command. + +## 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 erase-flash` + +## Build and Flash + +Build the project, flash it to the board by running `idf.py -p build flash`. + +## Example Output + +As you run the example, you will see the following log: + +``` +I (298) cpu_start: Unicore app +I (298) cpu_start: Pro cpu up. +W (307) clk: esp_perip_clk_init() has not been implemented yet +I (314) cpu_start: Pro cpu start user code +I (314) cpu_start: cpu freq: 96000000 Hz +I (314) cpu_start: Application information: +I (317) cpu_start: Project name: esp_zigbee_all_device_types_app +I (324) cpu_start: App version: b8aed66-dirty +I (329) cpu_start: Compile time: Aug 15 2024 17:38:49 +I (335) cpu_start: ELF file SHA256: d1357f7216520fbf... +I (341) cpu_start: ESP-IDF: v5.1.3 +I (346) cpu_start: Min chip rev: v0.0 +I (351) cpu_start: Max chip rev: v0.99 +I (356) cpu_start: Chip rev: v0.1 +I (360) heap_init: Initializing. RAM available for dynamic allocation: +I (368) heap_init: At 40813A30 len 00039950 (230 KiB): D/IRAM +I (374) heap_init: At 4084D380 len 00002B60 (10 KiB): STACK/DIRAM +I (382) spi_flash: detected chip: generic +I (385) spi_flash: flash io: dio +W (389) spi_flash: Detected size(4096k) larger than the size in the binary image header(2048k). Using the size in the binary image header. +I (403) sleep: Configure to isolate all GPIO pins in sleep state +I (409) sleep: Enable automatic switching of GPIO sleep configuration +I (417) app_start: Starting scheduler on CPU0 +I (421) main_task: Started on CPU0 +I (421) main_task: Calling app_main() +I (431) esp-zigbee-console: List of ESP Zigbee Console commands: +I (431) esp-zigbee-console: - Command 'address' +I (441) esp-zigbee-console: - Command 'bdb_comm' +I (441) esp-zigbee-console: - Command 'channel' +I (441) esp-zigbee-console: - Command 'dm' +I (451) esp-zigbee-console: - Command 'factoryreset' +I (451) esp-zigbee-console: - Command 'ic' +I (461) esp-zigbee-console: - Command 'network' +I (461) esp-zigbee-console: - Command 'panid' +I (471) esp-zigbee-console: - Command 'radio' +I (471) esp-zigbee-console: - Command 'reboot' +I (481) esp-zigbee-console: - Command 'role' +I (481) esp-zigbee-console: - Command 'start' +I (491) esp-zigbee-console: - Command 'tl' +I (491) esp-zigbee-console: - Command 'zcl' +I (501) esp-zigbee-console: - Command 'zdo' +I (501) esp-zigbee-console: - Command 'zgp' +I (511) esp-zigbee-console: - Command 'zha' +I (591) phy: phy_version: 230,2, 9aae6ea, Jan 15 2024, 11:17:12 +I (591) phy: libbtbb version: 944f18e, Jan 15 2024, 11:17:25 +I (591) ESP_ZB_CONSOLE_APP: Start ESP Zigbee Console + +Type 'help' to get the list of commands. +Use UP/DOWN arrows to navigate through command history. +Press TAB when typing command name to auto-complete. +I (601) main_task: Returned from app_main() +esp> +``` + +When type `help`, you will see the following outputs about the supported commands: + +``` +address + Get/Set the (extended) address of the node + +bdb_comm + Perform BDB Commissioning + +channel + Get/Set 802.15.4 channels for network + +dm + ZigBee Cluster Library data model management + +factoryreset + Reset the device to factory new + +ic + Install code configuration + +network + Network configuration + +panid + Get/Set the (extended) PAN ID of the node + +radio + Enable/Disable the radio + +reboot + Reboot the device + +role + Get/Set the Zigbee role of a device + +start + Start Zigbee stack + +tl + TouchLink configuration + +zcl + ZigBee Cluster Library management + +zdo + Zigbee Device Object management + +zgp + ZigBee Green Power Profile management + +zha + ZigBee Home Automation Profile + +help + Print the list of registered commands +``` + +For most of the commands, you can execute them without any parameters to see the usage: + +``` +esp> zdo +zdo: Zigbee Device Object management + request Request information from node + annce Announce current node + match Get matched devices + bind Request the node to bind to device + nwk_open Request the node to open the network + nwk_leave Request the node to leave the network +``` +``` +esp> zdo match +Usage: zdo match [-i ]... [-o ]... [-p ] -d + -i, --in= in cluster ID + -o, --out= out cluster ID + -p, --profile= profile id (PID) to match, defaule: HA + -d, --dst-addr= network address this request is to +``` + +For more information about the supported commands and usages, please refer to [ESP-Zigbee-Console Reference](../../components/esp-zigbee-console/README.md) + +## Play with All Device Type Example +This section demonstrate the process to create HA on_off_light and on_off_switch devices. + +### Start Node 1 as on_off_light + +Create and register the HA standard on_off_light data model: + +```bash +esp> zha add 1 on_off_light +I (518033) cli_cmd_zha: on_off_light created with endpoint_id 1 +esp> dm register +esp> +``` + +Form the network and open the network for joining: + +```bash +esp> bdb_comm start form +I (839703) cli_cmd_bdb: ZDO signal: ZDO Config Ready (0x17), status: ESP_FAIL +I (839703) cli_cmd_bdb: Zigbee stack initialized +W (842093) cli_cmd_bdb: Network(0x79d0) closed, devices joining not allowed. +I (842103) cli_cmd_bdb: Formed network successfully (Extended PAN ID: 0x744dbdfffe602dfd, PAN ID: 0x79d0, Channel:11, Short Address: 0x5fd5) +esp> network open -t 200 +I (860763) cli_cmd_bdb: Network(0x79d0) is open for 200 seconds +``` + +### Start Node 2 as on_off_switch + +Create and register the HA standard on_off_switch data model: + +```bash +esp> zha add 2 on_off_switch +I (914051) cli_cmd_zha: on_off_switch created with endpoint_id 2 +esp> dm register +esp> +``` + +Joining the network: + +```bash +esp> bdb_comm start steer +I (930721) cli_cmd_bdb: ZDO signal: ZDO Config Ready (0x17), status: ESP_FAIL +I (930721) cli_cmd_bdb: Zigbee stack initialized +W (933611) cli_cmd_bdb: Network(0x79d0) closed, devices joining not allowed. +I (933981) cli_cmd_bdb: Network(0x79d0) is open for 180 seconds +I (933991) cli_cmd_bdb: Joined network successfully (Extended PAN ID: 0x744dbdfffe602dfd, PAN ID: 0x79d0, Channel:11, Short Address: 0x0935) +``` + +### Control the light using switch + +Get the light on/off status by reading the attribute from the switch: + +```bash +esp> zcl send_gen read -d 0x5fd5 --dst-ep 1 -e 2 -c 6 -a 0 +I (1347151) cli_cmd_zcl: Read response: endpoint(2), cluster(0x06) +I (1347161) cli_cmd_zcl: attribute(0x00), type(0x10), data size(1) +I (1347171) : 0x40818c02 00 |.| +``` + +Send "On" command from the switch: + +```bash +esp> zcl send_raw -d 0x5fd5 --dst-ep 1 -e 2 -c 6 --cmd 0x01 +W (1405221) ESP_ZB_CONSOLE_APP: Receive Zigbee action(0x1005) callback +``` + +Check the light status again: + +```bash +esp> zcl send_gen read -d 0x5fd5 --dst-ep 1 -e 2 -c 6 -a 0 +I (1420021) cli_cmd_zcl: Read response: endpoint(2), cluster(0x06) +I (1420021) cli_cmd_zcl: attribute(0x00), type(0x10), data size(1) +I (1420031) : 0x40818ca6 01 |.| +``` + +## Troubleshooting + +For any technical queries, please open an [issue](https://github.com/espressif/esp-zigbee-sdk/issues) on GitHub. We will get back to you soon diff --git a/examples/esp_zigbee_all_device_types_app/main/CMakeLists.txt b/examples/esp_zigbee_all_device_types_app/main/CMakeLists.txt new file mode 100644 index 0000000..3a074da --- /dev/null +++ b/examples/esp_zigbee_all_device_types_app/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRC_DIRS "." + PRIV_INCLUDE_DIRS ".") \ No newline at end of file diff --git a/examples/esp_zigbee_all_device_types_app/main/esp_zigbee_all_device_types_app.c b/examples/esp_zigbee_all_device_types_app/main/esp_zigbee_all_device_types_app.c new file mode 100644 index 0000000..714d991 --- /dev/null +++ b/examples/esp_zigbee_all_device_types_app/main/esp_zigbee_all_device_types_app.c @@ -0,0 +1,120 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "esp_system.h" +#include "esp_log.h" +#include "nvs_flash.h" + +#include "esp_zigbee_all_device_types_app.h" + +static const char *TAG = "ESP_ZB_CONSOLE_APP"; + +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; + const char *err_name = esp_err_to_name(err_status); + switch (sig_type) { + case ESP_ZB_ZDO_SIGNAL_SKIP_STARTUP: + ESP_LOGI(TAG, "Zigbee stack initialized"); + 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"); + } else { + ESP_LOGE(TAG, "Failed to initialize Zigbee stack (status: %s)", err_name); + } + 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: 0x%016" PRIx64 ", PAN ID: 0x%04hx, Channel:%d, Short Address: 0x%04hx)", + *(uint64_t *)extended_pan_id, esp_zb_get_pan_id(), esp_zb_get_current_channel(), esp_zb_get_short_address()); + } else { + ESP_LOGI(TAG, "Restart network formation (status: %s)", err_name); + } + 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: 0x%016" PRIx64 ", PAN ID: 0x%04hx, Channel:%d, Short Address: 0x%04hx)", + *(uint64_t *)extended_pan_id, esp_zb_get_pan_id(), esp_zb_get_current_channel(), esp_zb_get_short_address()); + } else { + ESP_LOGI(TAG, "Restart network steering (status: %s)", err_name); + } + 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; + default: + ESP_LOGI(TAG, "ZDO signal: %s (0x%x), status: %s", esp_zb_zdo_signal_to_string(sig_type), sig_type, + err_name); + break; + } +} + +void zb_stack_init(void) +{ + /* Initialize Zigbee stack with default configuraion */ + esp_zb_cfg_t zb_nwk_cfg = ESP_ZB_ZR_CONFIG(); + esp_zb_init(&zb_nwk_cfg); + + /* Set default allowed network channels */ + esp_zb_set_channel_mask(ESP_ZB_TRANSCEIVER_ALL_CHANNELS_MASK); + + /* Set default scan channels */ + esp_zb_set_primary_network_channel_set(ESP_ZB_TRANSCEIVER_ALL_CHANNELS_MASK); + esp_zb_set_secondary_network_channel_set(ESP_ZB_TRANSCEIVER_ALL_CHANNELS_MASK); + + /* Enable CLI managed ep_list */ + esp_zb_console_manage_ep_list(NULL); +} + +static void zb_stack_main_task(void *pvParameters) +{ + zb_stack_init(); + + /* Do not call `esp_zb_start()`. + * + * We want the timing of starting the stack to be mananged by CLI, + * so that we have a chance to do configurations on the stack. + * + */ + + esp_zb_stack_main_loop(); + + esp_zb_console_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_console_init()); + ESP_ERROR_CHECK(esp_zb_platform_config(&config)); + xTaskCreate(zb_stack_main_task, "Zigbee_main", 4096, NULL, 5, NULL); + ESP_LOGI(TAG, "Start ESP Zigbee Console"); + ESP_ERROR_CHECK(esp_zb_console_start()); +} diff --git a/examples/esp_zigbee_all_device_types_app/main/esp_zigbee_all_device_types_app.h b/examples/esp_zigbee_all_device_types_app/main/esp_zigbee_all_device_types_app.h new file mode 100644 index 0000000..7571eb0 --- /dev/null +++ b/examples/esp_zigbee_all_device_types_app/main/esp_zigbee_all_device_types_app.h @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "esp_zigbee_core.h" +#include "esp_zigbee_console.h" + +#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_ZR_CONFIG() \ + { \ + .esp_zb_role = ESP_ZB_DEVICE_TYPE_ROUTER, \ + .install_code_policy = INSTALLCODE_POLICY_ENABLE, \ + .nwk_cfg.zczr_cfg = { \ + .max_children = MAX_CHILDREN, \ + }, \ + } + +#define ESP_ZB_DEFAULT_HOST_CONFIG() \ + { \ + .host_connection_mode = ZB_HOST_CONNECTION_MODE_NONE, \ + } + +#if CONFIG_SOC_IEEE802154_SUPPORTED +#define ESP_ZB_DEFAULT_RADIO_CONFIG() \ + { \ + .radio_mode = ZB_RADIO_MODE_NATIVE, \ + } + +#else +#error "CLI example for RCP is not ready!" +#endif \ No newline at end of file diff --git a/examples/esp_zigbee_all_device_types_app/main/idf_component.yml b/examples/esp_zigbee_all_device_types_app/main/idf_component.yml new file mode 100644 index 0000000..55d0744 --- /dev/null +++ b/examples/esp_zigbee_all_device_types_app/main/idf_component.yml @@ -0,0 +1,11 @@ +## IDF Component Manager Manifest File +dependencies: + cmake_utilities: "0.*" + espressif/esp-zboss-lib: "~1.5.0" + espressif/esp-zigbee-lib: "~1.5.0" + espressif/esp-zigbee-console: + version: "1.*" + override_path: "../../../components/esp-zigbee-console" + ## Required IDF version + idf: + version: ">=5.0.0" diff --git a/examples/esp_zigbee_all_device_types_app/partitions.csv b/examples/esp_zigbee_all_device_types_app/partitions.csv new file mode 100644 index 0000000..d546195 --- /dev/null +++ b/examples/esp_zigbee_all_device_types_app/partitions.csv @@ -0,0 +1,9 @@ +# 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, , 0x6000, +otadata, data, ota, , 0x2000, +phy_init, data, phy, , 0x1000, +ota_0, app, ota_0, , 900K, +ota_1, app, ota_1, , 900K, +zb_storage, data, fat, , 16K, +zb_fct, data, fat, , 1K, diff --git a/examples/esp_zigbee_all_device_types_app/sdkconfig.defaults b/examples/esp_zigbee_all_device_types_app/sdkconfig.defaults new file mode 100644 index 0000000..810217b --- /dev/null +++ b/examples/esp_zigbee_all_device_types_app/sdkconfig.defaults @@ -0,0 +1,17 @@ +# Reduce bootloader log verbosity +CONFIG_BOOTLOADER_LOG_LEVEL_WARN=y +CONFIG_BOOTLOADER_LOG_LEVEL=2 + +# Enable filesystem +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" +CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" + +# +# Zboss +# +CONFIG_ZB_ENABLED=y +CONFIG_ZB_ZCZR=y +CONFIG_IEEE802154_RECEIVE_DONE_HANDLER=y +# end of Zboss +# end of Component config