Zigbee Fundamental Project Development#
Introduction#
The intention of this lab is to help developers use Zigbee features and ZBOSS API to implement commonly used functionality. Topics that we will cover in this lab include:
Task 1: Altering the OnOff light coordinator for a manual bind
Task 2: Configuring the Switch end device for receiving attribute reports
Task 3: Programming and viewing results
Task 4: Send raw data packets over-the-air
Note: This lab is intended to be an informative guide for starting development and as such may not offer the optimal solution for any given application.
Zigbee is a component of the SimpleLink LowPower F3 Software Development Kit and is a complete Zigbee 3.0 software solution.
Note
Technical support: For any questions you may have, please refer to the TI Zigbee & Thread E2E Forum.
Prerequisites#
Background#
Familiarity with the Zigbee 3.0 specification
More information about the changes in Zigbee 3.0 compared to older specifications can be found in SWRA615
Basic CCS knowledge
Some basic familiarity with embedded programming.
Software#
Code Composer Studio (latest version) with support for CC23XX devices
SimpleLink Lowpower F3 SDK (latest)
Ubiqua Protocol Analyzer (optional for packet sniffing with CC26XX LaunchPads)
Wireshark (optional for packet sniffing)
TI Packet Sniffer 2 (optional for packet sniffing with CC26XX LaunchPads)
Hardware#
2 x CC23XX Zigbee-capable SimpleLink LaunchPads for running sample applications, review the SDK Release Notes for more information.
2 x XDS110ET LaunchPads for programming and powering each CC23XX LaunchPad
1 x CC26XX LaunchPad for packet sniffing (optional)
Micro USB and USB-C Cables
Recommended Reading#
Other Zigbee SimpleLink Academy Labs#
Task 0: Erase the Flash Memory on your LaunchPads#
Make sure that the flash memory has been completely erased on each of your LaunchPads before flashing any Zigbee projects. This can be done in Uniflash, here:

Uniflash Erase Memory#
Otherwise, ensure that the CCS Project Properties > Debug > Flash Settings
has Chip Erase
selected.
Task 1: Altering the Light Coordinator for a Manual Bind#
In this part of the lab, we are going to create a manual bind on the On/Off Cluster between a Light and Switch nodes, in the Server-to-Client direction, where the Light implements the Server side of the cluster and the Switch implements the Client side of the cluster. By the ZCL Specification, the On/Off cluster Server device has a mandatorily-reportable attribute (Attribute ID 0x0000, OnOff, of the ZCL v8 Spec). Zigbee 3.0 mandates that, if a bind exists on this cluster in the correct direction (Server-to-Client in this case), this device must support automatic ZCL attribute reporting. In other words, if we create the aforementioned bind, the Light will be enabled to start reporting its OnOff attribute’s state to the Switch.
We will start by loading the out-of-box onoff_light project. In CCS, go to
File > Import Project(s)
and underSearch directory:
browse to the following path:C:\ti\simplelink_lowpower_f3_sdk_<version>\examples\rtos\<LaunchPad variant>\zigbee\onoff_light
Import OnOff Light#
There are a few global variables to define in
on_off_light.c
that will help later on in the lab:on_off_light.c##define LIGHT_REPORTING_MIN_INTERVAL 10 #define LIGHT_REPORTING_MAX_INTERVAL 60 //... zb_uint16_t short_addr; zb_uint8_t ep_id[8]; zb_uint8_t ep_count; zb_ieee_addr_t ieee_addr; zb_uint8_t cluster_id[16];
In order to create a bind in a Zigbee network, we need the NWK (short) address of the destination device, the IEEE (long) address of the destination device, and the destination Zigbee endpoint where the relevant cluster lives on the destination device. One simple way to start this process is to parse incoming Device Announce messages that are broadcasted as new devices join our Zigbee network.
Once we process the Device Announce inside of the
ZB_ZDO_SIGNAL_DEVICE_ANNCE
case ofzboss_signal_handler
, we can useZB_SCHEDULE_APP_ALARM
to schedule a callback which will then prepare an Active Endpoint Request to find which of the target device’s endpoints have active Zigbee applications running on them. We can use thezb_zdo_active_ep_req()
API to request this endpoint information.on_off_light.c#/* Send active_ep_req */ void send_active_ep_req(zb_bufid_t param, zb_uint16_t dev_idx) { if (!param) { zb_buf_get_out_delayed_ext(send_active_ep_req, dev_idx, 0); } else { zb_zdo_active_ep_req_t *req; req = zb_buf_initial_alloc(param, sizeof(zb_zdo_active_ep_req_t)); req->nwk_addr = short_addr; zb_zdo_active_ep_req(param, active_ep_callback); } } void schedule_send_active_ep_req (zb_uint8_t dev_idx) { zb_buf_get_out_delayed_ext(send_active_ep_req, dev_idx, 0); } /* Callback to handle the stack events */ void zboss_signal_handler(zb_uint8_t param) { //... case ZB_ZDO_SIGNAL_DEVICE_ANNCE: { zb_zdo_signal_device_annce_params_t *dev_annce_params = ZB_ZDO_SIGNAL_GET_PARAMS(sg_p, zb_zdo_signal_device_annce_params_t); short_addr = dev_annce_params->device_short_addr; zb_ret_t ret = zb_address_ieee_by_short(short_addr, ieee_addr); ZB_SCHEDULE_APP_ALARM(schedule_send_active_ep_req, 0, 1 * ZB_TIME_ONE_SECOND); } break; //... }
After receiving the destination device’s Active Endpoint Response from the
active_ep_callback
callback, we can use the received endpoint to send a Simple Descriptor Requests to each endpoint to find the one which contains the desired application clusters. We can use thezb_zdo_simple_desc_req
API to verify the application clusters of this endpoint.on_off_light.c#/* Send_simple_desc_req */ static void send_simple_desc_req(zb_bufid_t param, zb_uint16_t dev_idx) { zb_zdo_simple_desc_req_t *req; if (!param) { zb_buf_get_out_delayed_ext(send_simple_desc_req, dev_idx, 0); } else { req = zb_buf_initial_alloc(param, sizeof(zb_zdo_simple_desc_req_t)); req->nwk_addr = short_addr; for (int i = 0; i < ep_count; i++) { if(ep_id[i] != 0xF2) { req->endpoint = ep_id[i]; zb_zdo_simple_desc_req(param, simple_desc_callback); } } } } void schedule_send_simple_desc_req (zb_uint8_t dev_idx) { zb_buf_get_out_delayed_ext(send_simple_desc_req, dev_idx, 0); } /* active_ep_req callback */ void active_ep_callback(zb_bufid_t param) { zb_uint8_t *zdp_cmd = zb_buf_begin(param); zb_zdo_ep_resp_t *resp = (zb_zdo_ep_resp_t*)zdp_cmd; zb_uint8_t *ep_list = zdp_cmd + sizeof(zb_zdo_ep_resp_t); if (resp->status == ZB_ZDP_STATUS_SUCCESS) { for (int i = 0; i < resp->ep_count; i++) { ep_id[i] = *(ep_list + i); /*Send a simple_dec_req per active EP received*/ ZB_SCHEDULE_APP_ALARM(schedule_send_simple_desc_req, 0, 2 * ZB_TIME_ONE_SECOND); ep_count++; } } zb_buf_free(param); }
The
simple_desc_callback
callback handles the Simple Descriptor Response from the target device and will confirm which destination endpoint inquired contains the On/Off output cluster. Since we have already received the bind destination IEEE address from the Device Announce, we can usezb_get_long_address
to obtain the bind source IEEE address andzb_get_short_address
to get the request source short address, since these parameters are both pertaining to the local device. Thezb_zdo_bind_req
API can now be called to create the bind.on_off_light.c#void send_bind_req(zb_uint8_t param, zb_uint16_t endpoint) { zb_bufid_t buf = param; zb_zdo_bind_req_param_t *req; req = ZB_BUF_GET_PARAM(buf, zb_zdo_bind_req_param_t); ZB_MEMCPY(&req->dst_address.addr_long, ieee_addr, sizeof(zb_ieee_addr_t)); req->src_endp = ZB_OUTPUT_ENDPOINT; req->cluster_id = ZB_ZCL_CLUSTER_ID_ON_OFF; req->dst_addr_mode = ZB_APS_ADDR_MODE_64_ENDP_PRESENT; zb_get_long_address(req->src_address); req->dst_endp = endpoint; req->req_dst_addr = zb_get_short_address(); zb_zdo_bind_req(param, bind_device_cb); } void schedule_send_bind_req (zb_uint8_t endpoint) { zb_buf_get_out_delayed_ext(send_bind_req, endpoint, 0); } static void simple_desc_callback(zb_bufid_t param) { zb_uint8_t *zdp_cmd = zb_buf_begin(param); zb_zdo_simple_desc_resp_t *resp = (zb_zdo_simple_desc_resp_t*)(zdp_cmd); zb_uint_t i; if (resp->hdr.status == ZB_ZDP_STATUS_SUCCESS) { /* Add the received clusters to cluster array*/ for(i = 0; i < (resp->simple_desc.app_input_cluster_count + resp->simple_desc.app_output_cluster_count); i++) { cluster_id[i] = *(resp->simple_desc.app_cluster_list + i); if((i >= resp->simple_desc.app_input_cluster_count) && (cluster_id[i] == ZB_ZCL_CLUSTER_ID_ON_OFF)) ZB_SCHEDULE_APP_ALARM(schedule_send_bind_req, resp->simple_desc.endpoint, 1 * ZB_TIME_ONE_SECOND); } } zb_buf_free(param); }
Once the bind is successfully confirmed using
bind_device_cb
, we can useZB_ZCL_GENERAL
APIs to configure reporting on the light.LIGHT_REPORTING_MIN_INTERVAL
represents the minimum time delay before the on/off attribute is reported, whereasLIGHT_REPORTING_MAX_INTERVAL
is the maximum allowed time.on_off_light.c#void configure_reporting_cb(zb_uint8_t param) { zb_bufid_t buf = param; /* Uncomment to use APSDE-DATA.confirm parameters. */ /* zb_apsde_data_confirm_t *aps_data_conf = ZB_BUF_GET_PARAM(buf, zb_apsde_data_confirm_t); */ if (zb_buf_get_status(param) == (zb_uint8_t)RET_OK) { /* Success, what now?*/ } zb_buf_free(buf); } void configure_reporting(zb_uint8_t param) { zb_bufid_t buf = param; zb_uint8_t *cmd_ptr; zb_uint16_t local_addr = zb_get_short_address(); /* [zcl_general_fill_configure_report] */ ZB_ZCL_GENERAL_INIT_CONFIGURE_REPORTING_SRV_REQ(buf, cmd_ptr, ZB_ZCL_ENABLE_DEFAULT_RESPONSE); ZB_ZCL_GENERAL_ADD_SEND_REPORT_CONFIGURE_REPORTING_REQ( cmd_ptr, ZB_ZCL_ATTR_ON_OFF_ON_OFF_ID, ZB_ZCL_ATTR_TYPE_BOOL, LIGHT_REPORTING_MIN_INTERVAL, LIGHT_REPORTING_MAX_INTERVAL, 0); ZB_ZCL_GENERAL_SEND_CONFIGURE_REPORTING_REQ(buf, cmd_ptr, local_addr, ZB_APS_ADDR_MODE_16_ENDP_PRESENT, ZB_OUTPUT_ENDPOINT, ZB_OUTPUT_ENDPOINT, ZB_AF_HA_PROFILE_ID, ZB_ZCL_CLUSTER_ID_ON_OFF, configure_reporting_cb); }
Task 2: Configuring the Switch End Device for Receiving Attribute Reports#
Repeat Step 1 of Task 1, except with the onoff_switch project
(Optional) open
on_off_light.syscfg
in the SysConfig Editor, navigate to theZigbee
module, and select theDevice Type
andRadio
primary channels. Note that the channel must be the same between both Light and Switch devices.In order to process the reports received from the ZC add a
ZB_ZCL_CMD_REPORT_ATTRIB
statement inside ofzcl_specific_cluster_cmd_handler
fromon_off_switch.c
:zclSampleSw_ProcessIncomingMsg#zb_uint8_t zcl_specific_cluster_cmd_handler(zb_uint8_t param) { // ... else if (cmd_info->cmd_id == ZB_ZCL_CMD_REPORT_ATTRIB) { // Received attribute report from the light, what now? unknown_cmd_received = ZB_FALSE; cmd_in_progress = ZB_FALSE; zb_buf_free(param); } // ... }
Task 3: Programming and Viewing Results#
Choose similar network configurations for both projects (make sure that at least a common channel has been selected). Refer to the Network Configurations section of the TI Zigbee User’s Guide for more information.
For the ZED Switch project, go to
Run > Debug Project
to flash the program on the first LaunchPad. If you have multiple LaunchPads connected to your PC, CCS may prompt you to select one of the connected LaunchPads via the box below:Select LaunchPad#
The selected LaunchPad’s serial number will be assigned to this project workspace so future program launches will automatically attempt to select this LaunchPad first if it is plugged in. More information about debugging with multiple probes can be found here.
Run the program from inside the CCS debugger, then exit the debugger and reset the LaunchPad using the pushbutton on top.
Now change active projects to the ZC Light and once more use
Run > Debug Project
to flash the program on your second LaunchPad. After running the program from the debugger you should be able to commission the network and join the ZED device by either resetting or powering on the ZED switch node. If successful, then an active sniffer should be able to observe the ZED Device Announce, ZC Active Endpoint Request/ZED Active Endpoint Response, and ZC Simple Descriptor Request/ZED Simple Descriptor Response.Binding Sniffer Log#
You can set a breakpoint within multiple callbacks, the last being
configure_reporting_cb
and observe the status each to determine whether they
return successfully.
Warning
Pausing the ZC Light in the debugger will disrupt any processes for connected devices which are expecting radio responses from this node.
You should also now be able to notice the ZC Light reporting to the ZED Switch at an interval between
LIGHT_REPORTING_MIN_INTERVAL
andLIGHT_REPORTING_MAX_INTERVAL
seconds (pending the next Data Request packet from the sleepy ZED). AF acknowledgments are requested by using theZB_ZCL_ENABLE_DEFAULT_RESPONSE
parameter inZB_ZCL_GENERAL_INIT_CONFIGURE_REPORTING_SRV_REQ
Reporting Sniffer Log#
Task 4: Send raw data packets over-the-air#
Sometimes it is desired to send a custom, non-ZCL data packet to a target device.
In these instances it is important to use a private clusterID
(defined outside
of the ZCL specification e.g. 0xF00D) to send the raw data. These messages are
commonly achieved by using the zb_aps_send_user_payload
API, as shown with the
ZED switch sample application:
void send_aps_payload(zb_uint8_t param)
{
zb_addr_u addr = { 0 };
uint8_t theMessageData[] = {"\0\0\30Hello World"};
ZB_ASSERT(param);
if (ZB_JOINED() && !cmd_in_progress)
{
cmd_in_progress = ZB_TRUE;
Log_printf(LogModule_Zigbee_App, Log_INFO, "send_af_data_req %d - send af data req", param);
zb_aps_send_user_payload(
param,
addr, /* dst_addr */
0x0104, /* profile id */
0xf00d, /* cluster id */
5, /* destination endpoint */
ZB_SWITCH_ENDPOINT, /* source endpoint */
ZB_APS_ADDR_MODE_16_ENDP_PRESENT, /* address mode */
ZB_TRUE, /* APS ACK enabled */
theMessageData, /* payload pointer */
sizeof(theMessageData)); /* size of payload */
}
else
{
Log_printf(LogModule_Zigbee_App, Log_INFO, "send_af_data_req %d - not joined", param);
zb_buf_free(param);
}
}
This function can be triggered by the user, for example replacing
send_toggle_req
inside the button_press_handler
. A callback, in this
instance named send_aps_payload_cb
, can also be established using
zb_aps_set_user_data_tx_cb
during device initialization to check on the status.
void send_aps_payload_cb(zb_uint8_t param);
// ...
zb_aps_set_user_data_tx_cb(send_aps_payload_cb); // IN MAIN BEFORE zboss_start_no_autostart
// ...
void send_aps_payload_cb(zb_uint8_t param)
{
zb_ret_t buf_status;
zb_uint8_t i;
zb_uint8_t aps_payload_size = 0U;
zb_uint8_t *aps_payload_ptr;
Log_printf(LogModule_Zigbee_App, Log_INFO, ">> send_aps_payload_cb, param: %hd",
param);
ZB_ASSERT(param != ZB_BUF_INVALID);
buf_status = zb_buf_get_status(param);
aps_payload_ptr = zb_aps_get_aps_payload(param, &aps_payload_size);
Log_printf(LogModule_Zigbee_App, Log_INFO, "buf_status %d, buf_len: %hd, aps_payload_size: %hd",
buf_status, zb_buf_len(param), aps_payload_size);
switch ((zb_aps_user_payload_cb_status_t)buf_status)
{
case ZB_APS_USER_PAYLOAD_CB_STATUS_SUCCESS:
Log_printf(LogModule_Zigbee_App, Log_INFO, "Transmission status: SUCCESS");
break;
case ZB_APS_USER_PAYLOAD_CB_STATUS_NO_APS_ACK:
Log_printf(LogModule_Zigbee_App, Log_INFO, "Transmission status: NO_APS_ACK");
break;
default:
Log_printf(LogModule_Zigbee_App, Log_INFO, "Transmission status: INVALID");
break;
}
for (i = 0U; i < aps_payload_size; ++i)
{
Log_printf(LogModule_Zigbee_App, Log_INFO, "aps_payload[%hd] == 0x%hx",
aps_payload_ptr[i]);
}
zb_buf_free(param);
Log_printf(LogModule_Zigbee_App, Log_INFO, "<< send_aps_payload_cb");
}
In this example:
uint8_t theMessageData[]
is defined globally as{"\0\0\30Hello World"}
, where the first three characters fill in the ZCL header: 0x00 Frame Counter, incrementing Transaction Sequence Number, and 0x18 General Command Frame (reserved/unspecified by ZCL).The destination address and endpoint are assumed to be known, but could also be discovered using the techniques covered earlier in this lab.

AF Data Request Sniffer Log#
On the target device (ZC Light) side, the incoming message can be processed
inside of zcl_specific_cluster_cmd_handler
as before