Zigbee ZCL: Keepalive/Poll Control Cluster and Groups/Scenes#
Introduction#
The intention of this lab is to help developers understand how keepalives can be implemented in a Zigbee network and the use of Groups and Scenes clusters. Topics that we will cover in this lab include:
Keepalives and the Poll Control Cluster
Groups and Scenes Clusters
Z-Stack is a component of the SimpleLink CC13xx / CC26xx Software Development Kit and is a complete Zigbee 3.0 software solution.
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.
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
Knowledge of packet sniffing (optional)
Software#
Code Composer Studio (latest version) with support for CC13xx/CC26xx devices
SimpleLink CC13xx / CC26xx SDK (latest)
Ubiqua Protocol Analyzer (optional for packet sniffing)
Wireshark (optional for packet sniffing)
TI Packet Sniffer (optional for packet sniffing with CC2531EMK)
TI Packet Sniffer 2 (optional for packet sniffing with LaunchPads)
Hardware#
3 x Zigbee-capable SimpleLink LaunchPads for running sample applications, review the SDK Release Notes for more information.
1 x LaunchPad or CC2531EMK for packet sniffing (optional)
Micro USB Cables
Recommended Reading#
Other Zigbee SimpleLink Academy Labs#
Zigbee Documentation#
Zigbee PRO 2017 Specification (Zigbee Spec R22) - Zigbee Document number 05-3474-22
Zigbee Cluster Library v7 Specification (ZCL spec) - Zigbee Document number 07-5123-07
Zigbee Lighting and Occupancy Device Specification (ZLO spec) - Zigbee Document number 15-0014-05
Zigbee Home Automation Public Application Profile (ZHA spec) - Zigbee Document number 05-3520-29
Note
Zigbee documentation can be obtained from the Zigbee Alliance website
Part 1: Keepalive and Poll Control Cluster#
In this part of the lab, we are going to discuss keepalives and the Poll Control cluster. Zigbee 3.0 introduces a child aging feature, which allows parent devices to know which of its child devices are still in the network. If a parent detects prolonged absence of activity from a child end device, it will queue a leave request for that child and free the corresponding entry in its association table. The length of time for the prolonged absense may be specified by an End Device Timeout request from an end device (if no request is sent, then the parent simply uses a default value for aging out that child). This is discussed in Z-Stack Overview > Miscellaneous > Child Management section of the TI Z-Stack User’s Guide.
According to the Zigbee spec (r22), parent devices may support two types of messages as a keepalives: MAC data poll and End Device timeout request (see Table 3-55 in the spec). Z-Stack currently supports MAC data poll for keepalives (as indicated by zgNwkParentInformation
in Stack/sys/zglobals.h
).
Zigbee PRO 2017 Spec: Table 3-55
The Z-Stack examples by default define POLL_RATE
as 3000 (determined inside the Z-Stack > Power Management > Poll Period parameter of the .syscfg
project file). That means child devices will poll (send a MAC data request) every 3000 ms. Note that other mechanisms, such as commissioning, may also cause the end device to send MAC data requests.
Note
In addition to serving as keepalives, the MAC data request also allows the ZED to check whether the parent has a message for it.
Parent devices keep a queue of messages for its sleepy children (eg. ZEDs). Since the queue has finite size, if the ZED does not poll frequently enough, messages for it may be dropped. A parent device’s queue size is defined by MAC_CFG_TX_MAX
(found in Stack/Config/f8wcoord.opts
or Stack/Config/f8wrouter.opts
).
Messages in the queue may also timeout. The timeout value is defined by NWK_INDIRECT_MSG_TIMEOUT
(controlled by the Z-Stack > Advanced > Routing > Network Indirect Message Timeout parameter of the .syscfg
project file).
Values related to keepalives are described in the following table.
Macro Name | Description | Default Value | Location |
END_DEV_TIMEOUT_VALUE | For a parent device, this is the default time that parent expects a child device to send a keepalive. If no keepalive is received from a child, then this parent proceeds to age that child out. For a child device, this is the time that the child device requests the parent device to wait before aging it out. If successful, the child device should wait no more than this amount of time before polling. | 8 (256 minutes) | default/syscfg/ti_zstack_config.h (from .syscfg: Z-Stack > Network > End Device Timeout) |
NWK_END_DEVICE_LEAVE_TIMEOUT | Used only by the parent device. When a parent proceeds to age out a child, the parent queues a leave request for that child. This leave request remains queued for that child for this amount of time. | 9 (512 minutes) | Stack/sys/zglobals.h |
Now, let’s use the LaunchPads and SimpleLink SDK to evaluate these statements.
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:
Task 1: Parent Aging Out a Child#
For this task, we will use the zed_light and zc_sw examples. In CCS, go to
File > Import > C/C++ > CCS Projects
and underSelect search-directory:
select the following search path:C:\ti\simplelink_cc13xx_cc26xx_sdk_<version>\examples\rtos\<LaunchPad variant>\zstack\
Import zc_sw and zed_light
Choose zc_sw and zed_light from the Discovered projects: to import.
Note
Typically the light would be ZC and the switch would be ZED, since the light would likely be mains powered while the switch has no need to stay on for an extended time. However, we use the light as ZED in this example because it clearly shows evidence of the differing poll rates (via latency between switch press and light toggling). This will be relevant in Task 2.
Given the above discussion, in order to see the ZC send a leave request to the child after aging out that child, the child must poll at a rate slower than the “keepalive” time (
END_DEV_TIMEOUT_VALUE
) but at a faster rate than theNWK_END_DEVICE_LEAVE_TIMEOUT
. To do this on the ZED, setEND_DEV_TIMEOUT_VALUE
to 0 (10 secs) and poll rate to 15 secs. No need to changeNWK_END_DEVICE_LEAVE_TIMEOUT
, since its default value of 9 is sufficiently large (512 minutes).Change poll rate
Change the polling rate after the end device has successfully completed the commissioning process. The application layer is notified of this completion through a commissioning status message, which the example applications handle in the
*_ProcessCommissioningStatus
function.For the zed_light, locate the
zclSampleLight_ProcessCommissioningStatus
function insideApplication/zcl_samplelight.c
. Once the ZED has joined a network, the BDB commissioning process will be in theBDB_COMMISSIONING_NWK_STEERING
state with a status ofBDB_COMMISSIONING_SUCCESS
. Add code to change the polling rate here.// ... case BDB_COMMISSIONING_NWK_STEERING: if(bdbCommissioningModeMsg->bdbCommissioningStatus == BDB_COMMISSIONING_SUCCESS) { //YOUR JOB: //We are on the nwk, what now? // ... // for SLA, added to change the default poll rate zstack_sysConfigWriteReq_t writeReq = { 0 }; writeReq.has_pollRate = true; writeReq.pollRate = 15000; // poll every 15 secs writeReq.pollRateType = POLL_RATE_TYPE_DEFAULT | POLL_RATE_KEEP_ALIVE; Zstackapi_sysConfigWriteReq(appServiceTaskId, &writeReq); // end added } // ...
After flashing both devices, you should now see the leave request that the ZC sends the ZED, as in the following sniffer log:
ZC ages out ZED
Notice that the ZED sends a Rejoin Request. Although the parent has already removed its association with the ZED, the ZED does not know this and polls its last parent. In this case, the Zigbee Spec defines that the parent should send this ZED a leave request with rejoin set to true. The ZED is then able to perform the rejoin sequence to any nearby routing devices.
Task 2: Poll Control Cluster#
For the purposes of this task, we will use the same zed_light and zc_sw examples.
Undo the lines of code from the zed_light that changed the polling rate.
Also, revert the changes we made to the zglobal.h
macros (ie, change END_DEV_TIMEOUT_VALUE
to the default value of 8).
In the previous task, the polling rate remained constant after the ZED joins a network. A longer poll rate saves more power on the ZED. However, in use cases where responsiveness is a high priority sometimes, a shorter poll rate is required. Since a shorter poll rate reduces battery life, a balance between the two scenarios is desirable and can be achieved using the Poll Control cluster of the Zigbee Cluster Library specification (ZCL spec).
The ZCL spec defines a Poll Control cluster that may be used to dynamically change the polling rate of a ZED. The ZED will implement the Poll Control cluster server, and its parent (in this case the ZC) will implement the Poll Control cluster client. In this lab, we will go over the following:
Checkin interval is the period at which the server will send a checkin command to each of its clients. This allows clients to request that the server enter fast poll mode.
Long poll interval is the period at which the server will poll when it is not in fast poll mode.
Short poll interval is the period at which the server will poll when it is in fast poll mode.
Section 3.16 of the ZCL spec describes these in more detail.
First, we need to add the Poll Control cluster into these examples.
Integrate the Poll Control cluster to the projects by performing the following for both projects:
Include
zcl_poll_control.c
in buildRight click
Common/zcl/zcl_poll_control.c
and de-select “Exclude from Build”.In
zcl_*.c
andzcl_*_data.c
, make sure you have#include "zcl_poll_control.h"
. These files contain commands and definitions needed to implement the Poll Control cluster.Add Poll Control cluster into the cluster list (in
zcl_*_data.c
).For client (sw): add to zclSampleSw_OutClusterList
const cId_t zclSampleSw_OutClusterList[] = { ZCL_CLUSTER_ID_GENERAL_IDENTIFY, ZCL_CLUSTER_ID_GENERAL_ON_OFF, ZCL_CLUSTER_ID_GENERAL_GROUPS, #if defined (OTA_CLIENT_INTEGRATED) ZCL_CLUSTER_ID_OTA #endif #if defined(ZCL_POLL_CONTROL) ZCL_CLUSTER_ID_GENERAL_POLL_CONTROL, #endif };
For server (light): add to zclSampleLight_InClusterList
const cId_t zclSampleLight_InClusterList[] = { ZCL_CLUSTER_ID_GENERAL_BASIC, ZCL_CLUSTER_ID_GENERAL_IDENTIFY, ZCL_CLUSTER_ID_GENERAL_GROUPS, ZCL_CLUSTER_ID_GENERAL_SCENES, ZCL_CLUSTER_ID_GENERAL_ON_OFF #ifdef ZCL_LEVEL_CTRL , ZCL_CLUSTER_ID_GENERAL_LEVEL_CONTROL #endif #ifdef ZCL_POLL_CONTROL , ZCL_CLUSTER_ID_GENERAL_POLL_CONTROL #endif };
Note
Make sure to add ZCL_POLL_CONTROL to the predefined symbols for both light and switch.
Add Poll Control cluster attributes to the server’s attribute list.
Declare attributes and assign default values
// Poll Control Cluster #ifdef ZCL_POLL_CONTROL uint32_t zclSampleLight_PollControlCheckInInterval = 120; // 120 qs == 30 s uint32_t zclSampleLight_PollControlLongPollInterval = 20; // 20 qs == 5 s uint16_t zclSampleLight_PollControlShortPollInterval = 2; // 2 qs == 0.5 s uint16_t zclSampleLight_PollControlFastPollTimeout = 32; // 32 qs == 8 s #endif
Add server attributes
#ifdef ZCL_POLL_CONTROL { ZCL_CLUSTER_ID_GENERAL_POLL_CONTROL, { ATTRID_POLL_CONTROL_CHECK_IN_INTERVAL, ZCL_DATATYPE_UINT32, ACCESS_CONTROL_READ | ACCESS_CONTROL_WRITE, (void *)&zclSampleLight_PollControlCheckInInterval } }, { ZCL_CLUSTER_ID_GENERAL_POLL_CONTROL, { ATTRID_POLL_CONTROL_LONG_POLL_INTERVAL, ZCL_DATATYPE_UINT32, ACCESS_CONTROL_READ, (void *)&zclSampleLight_PollControlLongPollInterval } }, { ZCL_CLUSTER_ID_GENERAL_POLL_CONTROL, { ATTRID_POLL_CONTROL_SHORT_POLL_INTERVAL, ZCL_DATATYPE_UINT16, ACCESS_CONTROL_READ, (void *)&zclSampleLight_PollControlShortPollInterval } }, { ZCL_CLUSTER_ID_GENERAL_POLL_CONTROL, { ATTRID_POLL_CONTROL_FAST_POLL_TIMEOUT, ZCL_DATATYPE_UINT16, ACCESS_CONTROL_READ | ACCESS_CONTROL_WRITE, (void *)&zclSampleLight_PollControlFastPollTimeout } }, #endif
Declare and define the Poll Control callbacks struct, the callbacks themselves, and other relevant code:
Poll Control server side:
// Application Events #define SAMPLELIGHT_POLL_CONTROL_TIMEOUT_EVT 0x0001 #define SAMPLELIGHT_LEVEL_CTRL_EVT 0x0002 #define SAMPLEAPP_END_DEVICE_REJOIN_EVT 0x0004 #define SAMPLEAPP_DISCOVERY_TIMEOUT_EVT 0x0008 #define SAMPLEAPP_PROV_CONNECT_EVT 0x0080 #define SAMPLEAPP_PROV_DISCONNECT_EVT 0x0010 #define SAMPLEAPP_POLICY_UPDATE_EVT 0x0020 #define SAMPLEAPP_SYNC_ATTR_EVT 0x0040 #define SAMPLELIGHT_NEW_POLL_RATE_EVT 0x00010000 // added for SLA :::{code-block} c :caption: "declare external variables for poll control attributes in zcl_samplelight.h" // Poll Control Attributes #ifdef ZCL_POLL_CONTROL extern uint32_t zclSampleLight_PollControlCheckInInterval; extern uint32_t zclSampleLight_PollControlLongPollInterval; extern uint16_t zclSampleLight_PollControlShortPollInterval; extern uint16_t zclSampleLight_PollControlFastPollTimeout; #endif :::{code-block} c :caption: "add to zcl_samplelight.c" #ifdef ZCL_POLL_CONTROL static ZStatus_t zclSampleLight_PollControlCheckInRspCB( zclPollControlCheckInRsp_t *pCmd ); static ZStatus_t zclSampleLight_PollControlFastPollStopCB(); static ZStatus_t zclSampleLight_PollControlSetLongPollIntervalCB( zclPollControlSetLongPollInterval_t *pCmd ); static ZStatus_t zclSampleLight_PollControlSetShortPollIntervalCB( zclPollControlSetShortPollInterval_t *pCmd ); #endif #ifdef ZCL_POLL_CONTROL static uint32_t newPollRate; static void changePollRate(uint32_t newPollRateMs) { newPollRate = newPollRateMs; appServiceTaskEvents |= SAMPLELIGHT_NEW_POLL_RATE_EVT; Semaphore_post(appSemHandle); } #endif /********************************************************************* * ZCL Poll Control Callback table */ #ifdef ZCL_POLL_CONTROL static zclPollControl_AppCallbacks_t zclSampleLight_PollControlCallbacks = { NULL, zclSampleLight_PollControlCheckInRspCB, zclSampleLight_PollControlFastPollStopCB, zclSampleLight_PollControlSetLongPollIntervalCB, zclSampleLight_PollControlSetShortPollIntervalCB }; // Poll Control cluster callback implementation(s) static Clock_Struct pollControlFastTimeoutClkStruct; static Clock_Handle pollControlFastTimeoutClkHandle; // pollControlFastTimeoutClkHandle; static void pollControl_FastPollTimeoutCB(UArg a0) { zclSampleLight_PollControlFastPollStopCB(); } static ZStatus_t zclSampleLight_PollControlCheckInRspCB( zclPollControlCheckInRsp_t *pCmd ) { if (pCmd->startFastPolling) { // start timer for fast polling timeout uint32_t RequestedTimeoutInMs; if (UtilTimer_isActive(&pollControlFastTimeoutClkStruct)) UtilTimer_stop(&pollControlFastTimeoutClkStruct); if (pCmd->fastPollTimeOut == 0) // use default fast poll timeout RequestedTimeoutInMs = zclSampleLight_PollControlFastPollTimeout * 250; else // use requested fast poll timeout RequestedTimeoutInMs = pCmd->fastPollTimeOut * 250; UtilTimer_setTimeout(pollControlFastTimeoutClkHandle, RequestedTimeoutInMs); UtilTimer_start(&pollControlFastTimeoutClkStruct); // start fast polling uint32_t IntervalInMs = zclSampleLight_PollControlShortPollInterval * 250; changePollRate(IntervalInMs); } return ZSuccess; } static ZStatus_t zclSampleLight_PollControlFastPollStopCB() { // stop fast polling timer, if running if (UtilTimer_isActive(&pollControlFastTimeoutClkStruct)) UtilTimer_stop(&pollControlFastTimeoutClkStruct); // stop fast polling (ie, resume long polling) uint32_t IntervalInMs = zclSampleLight_PollControlLongPollInterval * 250; changePollRate(IntervalInMs); return ZSuccess; } static ZStatus_t zclSampleLight_PollControlSetLongPollIntervalCB( zclPollControlSetLongPollInterval_t *pCmd ) { uint32_t IntervalInMs = pCmd->newLongPollInterval * 250; zclSampleLight_PollControlLongPollInterval = IntervalInMs; return ZSuccess; } static ZStatus_t zclSampleLight_PollControlSetShortPollIntervalCB( zclPollControlSetShortPollInterval_t *pCmd ) { uint32_t IntervalInMs = pCmd->newShortPollInterval * 250; zclSampleLight_PollControlShortPollInterval = IntervalInMs; return ZSuccess; } #endif :::{code-block} c :caption: "register callbacks for the Poll Control cluster zclSampleLight_Init in zcl_samplelight.c" // ... // Register the ZCL General Cluster Library callback functions zclGeneral_RegisterCmdCallbacks( SAMPLELIGHT_ENDPOINT, &zclSampleLight_CmdCallbacks ); #ifdef ZCL_POLL_CONTROL zclPollControl_RegisterCmdCallbacks( SAMPLELIGHT_ENDPOINT, &zclSampleLight_PollControlCallbacks ); #endif // ...
Poll Control client side:
/********************************************************************* * ZCL Poll Control Callback table */ #ifdef ZCL_POLL_CONTROL // Functions to process Poll Control cluster command/response messages static ZStatus_t zclSampleSw_PollControlCheckInCB( zclPollControlCheckIn_t *pCmd ); static zclPollControl_AppCallbacks_t zclSampleSw_PollControlCallbacks = { zclSampleSw_PollControlCheckInCB, NULL, NULL, NULL, NULL }; // Poll Control cluster callback implementation(s) static uint8_t startFastPolling = TRUE; static uint16_t fastPollTimeout = 0; static uint8_t disableDefaultRsp = FALSE; static ZStatus_t zclSampleSw_PollControlCheckInCB( zclPollControlCheckIn_t *pCmd ) { afAddrType_t dstAddr; dstAddr.addr.shortAddr = pCmd->srcAddr->addr.shortAddr; dstAddr.addrMode = (afAddrMode_t)Addr16Bit; dstAddr.endPoint = pCmd->srcAddr->endPoint; zstack_getZCLFrameCounterRsp_t Rsp; Zstackapi_getZCLFrameCounterReq(appServiceTaskId, &Rsp); return zclPollControl_Send_CheckInRsp( SAMPLESW_ENDPOINT, &dstAddr, startFastPolling, fastPollTimeout, disableDefaultRsp, Rsp.zclFrameCounter); } #endif :::{code-block} c :caption: "register callbacks for the Poll Control cluster zclSampleSw_Init in zcl_samplesw.c" // ... // Register the ZCL General Cluster Library callback functions zclGeneral_RegisterCmdCallbacks( SAMPLESW_ENDPOINT, &zclSampleSw_CmdCallbacks ); #ifdef ZCL_POLL_CONTROL zclPollControl_RegisterCmdCallbacks(SAMPLESW_ENDPOINT, &zclSampleSw_PollControlCallbacks); #endif // ...
On the server side, add in code to handle events triggered by the Poll Control cluster.
Handle the events related to Poll Control
#ifdef ZCL_POLL_CONTROL if ( appServiceTaskEvents & SAMPLELIGHT_POLL_CONTROL_TIMEOUT_EVT ) { // send the periodic Poll Control checkin zstack_getZCLFrameCounterRsp_t Rsp; Zstackapi_getZCLFrameCounterReq(appServiceTaskId, &Rsp); zclPollControl_Send_CheckIn(SAMPLELIGHT_ENDPOINT, &zclSampleLight_DstAddr, FALSE, Rsp.zclFrameCounter); appServiceTaskEvents &= ~SAMPLELIGHT_POLL_CONTROL_TIMEOUT_EVT; } if ( appServiceTaskEvents & SAMPLELIGHT_NEW_POLL_RATE_EVT ) { // need to set a new poll rate zstack_sysConfigWriteReq_t writeReq; memset(&writeReq, 0x00, sizeof(zstack_sysConfigWriteReq_t)); writeReq.has_pollRate = true; writeReq.pollRate = newPollRate; writeReq.pollRateType = POLL_RATE_TYPE_DEFAULT | POLL_RATE_KEEP_ALIVE; Zstackapi_sysConfigWriteReq(appServiceTaskId, &writeReq); appServiceTaskEvents &= ~SAMPLELIGHT_NEW_POLL_RATE_EVT; } #endif
On the server side, add in code to initialize the Poll Control cluster clocks:
Initialization of Poll Control cluster clocks
#ifdef ZCL_POLL_CONTROL // initialization for Poll Control static Clock_Struct pollControlCheckInClkStruct; static Clock_Handle pollControlCheckInClkHandle; static void pollControl_CheckInClkCB(UArg a0) { appServiceTaskEvents |= SAMPLELIGHT_POLL_CONTROL_TIMEOUT_EVT; Semaphore_post(appSemHandle); } static void zclPollControl_initClocks() { // initialize timer for fast poll timeout uint32_t DefaultDurationInMs = zclSampleLight_PollControlFastPollTimeout * 250; pollControlFastTimeoutClkHandle = UtilTimer_construct(&pollControlFastTimeoutClkStruct, pollControl_FastPollTimeoutCB, /* callback when timer expires */ DefaultDurationInMs, /* offset duration before timer starts */ 0, /* period (in ms) after which the timer will expire. 0 means one-shot */ 0, /* don't immediately start timer*/ 0 /* don't care*/ ); // start timer for periodic checkin. Clock_Handle not needed because checkin interval typically doesn't change. DefaultDurationInMs = zclSampleLight_PollControlCheckInInterval * 250; UtilTimer_construct(&pollControlCheckInClkStruct, pollControl_CheckInClkCB, DefaultDurationInMs, DefaultDurationInMs, 1, /* start timer immediately */ 0 ); } #endif
Add in a call to the initialization function
static void zclSampleLight_initializeClocks(void) { // ... #ifdef ZCL_POLL_CONTROL zclPollControl_initClocks(); #endif }
For Type1 or Type2 clusters, Finding & Binding could be used for automatic binding. However, the Poll Control cluster is neither a Type1 nor Type2 cluster (ZCL documents it as a utility cluster), we need to manually create a bind for it. (If you recall from the Zigbee Fundamentals SLA, we discussed how to manually create a bind.) The client (sw) will send a bind request for itself to the server (light). The direction of this bind is from server to client.
In ZC, let’s add the following pieces of code.
Declare a file local variable to store the address of the Poll Control cluster client
#ifdef ZCL_POLL_CONTROL static zstack_LongAddr_t PollControl_serverLongAddr; #endif
Set up notifications of Device Announce and Simple Descriptor Response messages
/******************************************************************************* * @fn SetupZStackCallbacks * * @brief Setup the Zstack Callbacks wanted * * @param none * * @return none */ static void SetupZStackCallbacks(void) { zstack_devZDOCBReq_t zdoCBReq = {0}; // ... // for SLA. let's also utilize the following callbacks zdoCBReq.has_deviceAnnounce = true; zdoCBReq.deviceAnnounce = true; zdoCBReq.has_simpleDescRsp = true; zdoCBReq.simpleDescRsp = true; (void)Zstackapi_DevZDOCBReq(appServiceTaskId, &zdoCBReq); }
Handle Device Announce message: extract extended address of ZED
// ... case zstackmsg_CmdIDs_ZDO_DEVICE_ANNOUNCE: #if defined(DMM_ZCSWITCH) && defined(NWK_TOPOLOGY_DISCOVERY) { NwkDiscovery_start(); } break; #else { // for this SLA, we are only expecting one device (light with Poll Control cluster server) zstackmsg_zdoDeviceAnnounceInd_t *pInd; pInd = (zstackmsg_zdoDeviceAnnounceInd_t *) pMsg; memcpy(PollControl_serverLongAddr, pInd->req.devExtAddr, sizeof(zstack_LongAddr_t)); } break; #endif // ...
Handle Simple Descriptor response: send a Bind Request to Poll Control cluster server
case zstackmsg_CmdIDs_ZDO_SIMPLE_DESC_RSP: //... { zstackmsg_zdoSimpleDescRspInd_t *pInd; pInd = (zstackmsg_zdoSimpleDescRspInd_t *) pMsg; zstack_sysNwkInfoReadRsp_t myNwkInfo; Zstackapi_sysNwkInfoReadReq(appServiceTaskId, &myNwkInfo); zstack_zdoBindReq_t bindReq; // send this bind request to the Poll Control server bindReq.nwkAddr = pInd->rsp.srcAddr; // the bind source is the Poll Control server OsalPort_memcpy(bindReq.bindInfo.srcAddr, PollControl_serverLongAddr, sizeof (zstack_LongAddr_t)); bindReq.bindInfo.srcEndpoint = pInd->rsp.simpleDesc.endpoint; // this bind is for the Poll Control cluster bindReq.bindInfo.clusterID = ZCL_CLUSTER_ID_GENERAL_POLL_CONTROL; // destination of the bind is us, the Poll Control client bindReq.bindInfo.dstAddr.addrMode = (zstack_AFAddrMode) zstack_AFAddrMode_EXT; OsalPort_memcpy(bindReq.bindInfo.dstAddr.addr.extAddr, myNwkInfo.ieeeAddr, sizeof(zstack_LongAddr_t)); bindReq.bindInfo.dstAddr.endpoint = SAMPLESW_ENDPOINT; // check status as necessary zstack_ZStatusValues status = Zstackapi_ZdoBindReq(appServiceTaskId, &bindReq); } break;
Now we can use the Poll Control cluster. Let’s evaluate the following to make sure our setup works:
The ZED will send a check in command (every 30 sec).
Sniffer log of checkin interval
After the ZED sends a check in command, the ZC will send a check in response, indicating that the ZED should enter fast poll mode. Since the check in response contains a fast poll timeout of 0, the Poll Control cluster server will use the default timeout (
zclSampleLight_PollControlFastPollTimeout
).Sniffer log of ZC response
When the ZED receives a check in response indicating “yes” for fast polling, the ZED will begin polling at a rate of 500 ms for up to 8 sec.
Sniffer log of fast polling mode
While the ZED is in fast poll mode, it will be more responsive to toggle commands (since it polls every 0.5 sec). It is left as an exercise for the developer to evaluate the other Poll Control cluster commands and callbacks.
Part 2: Groups and Scenes Clusters#
In this part of the lab, we are going to discuss/use the Groups and Scenes clusters of the ZCL.
The Groups cluster provides over-the-air management of groups. This is in addition to the group support handled inherently by the Application Support Layer (APS). In other words, while APS completely takes care of core group functionality (such as adding, removing, and filtering), the Groups cluster gives devices the ability to manage groups using over-the-air Groups cluster commands.
The Scenes cluster provides a way to set cluster attributes on a device(s) to specific values, as defined by a scene entry. This is useful if a set of (usually home automation related) devices need to enter a defined state. For example, consider a “night” scene. Recalling that scene may consist of turning off lights, locking doors, lowering shades, and setting the temperature. The corresponding scene entry would include an On/Off cluster with the OnOff attribute set to off, a Door Lock cluster with the LockState set to locked, etc.
Although Groups and Scenes clusters may be used individually, they may be used together for efficiently setting up common environmental settings among related devices.
Task 1: Groups Cluster#
In Zigbee, a group is a collection of endpoints. Since group messages are broadcasted to non-sleepy devices (ie with network address 0xFFFD), endpoints of sleepy end devices cannot receive group messages (they can still send group messages).
Groups are differentiated by unique group IDs, similar to how devices are differentiated by unique network addresses. Commands sent to a group are received by all endpoints within that group.
For that reason, it’s possible to evaluate the Groups cluster and group addressing among slightly differing setups. As an example, consider two endpoints EP1 and EP2 that are in the same group.
EP1 and EP2 may reside on one device (eg. both on one LaunchPad)
EP1 and EP2 may reside on different devices (eg. one on LaunchPad 1 and the other on LaunchPad 2)
Because the behaviors of setup 1 and setup 2 are practically indistinguishable for this task, we will aribitrarily use setup 2.
For this task, we will use the zc_sw and zr_light examples. The zc_sw will request On/Off cluster server(s) to add themselves to the same group. In this way, one group addressed toggle command will be received by both lights.
Import the zc_sw and zr_light examples. Use the same method as in Part 1 Task 1. (If you are using the same workspace as in Part 1, please rename the zc_sw project used in Part 1.)
zc_sw is already configured as a Groups cluster client
zr_light is already configured as a Groups cluster server
Add code to the zc_sw (Groups cluster client).
Define the group that the zc_sw will add lights to
// the first byte of GROUP_NAME is the length of this group's name, "my_group" static uint8_t GROUP_NAME[] = { 0x08, 'm', 'y', '_', 'g', 'r', 'o', 'u', 'p' }; #define GROUP_ID 0x0001
Since group addressed commands will be used in this task, Finding & Binding is unnecessary. Instead, we can utilize the Match Descriptor request to find endpoints with specific clusters. For our case, the ZED needs to find endpoints containing both the On/Off and Groups cluster servers.
Register to callbacks for Device Announce indication and Match Descriptor response
zdoCBReq.has_deviceAnnounce = true; zdoCBReq.deviceAnnounce = true; zdoCBReq.has_matchDescRsp = true; zdoCBReq.matchDescRsp = true;
Send Match Descriptor request after a device has joined
case zstackmsg_CmdIDs_ZDO_DEVICE_ANNOUNCE: // ... { // added for SLA zstackmsg_zdoDeviceAnnounceInd_t *pInd; pInd = (zstackmsg_zdoDeviceAnnounceInd_t *) pMsg; // send a Match Descriptor request zstack_zdoMatchDescReq_t req; memset(&req, 0x00, sizeof(req)); uint16_t inClusterIDs[] = {ZCL_CLUSTER_ID_GENERAL_ON_OFF, ZCL_CLUSTER_ID_GENERAL_GROUPS}; req.dstAddr = pInd->req.srcAddr; req.nwkAddrOfInterest = pInd->req.devAddr; req.n_inputClusters = 2; req.pInputClusters = inClusterIDs; req.n_outputClusters = 0; req.profileID = ZCL_HA_PROFILE_ID; Zstackapi_ZdoMatchDescReq(appServiceTaskId, &req); // end added } break;
Handle the Match Descriptor response
case zstackmsg_CmdIDs_ZDO_MATCH_DESC_RSP: // ... { zstackmsg_zdoMatchDescRspInd_t *pInd = (zstackmsg_zdoMatchDescRspInd_t *) pMsg; afAddrType_t dstAddr; memset(&dstAddr, 0x00, sizeof(afAddrType_t)); dstAddr.addrMode = afAddr16Bit; dstAddr.addr.shortAddr = pInd->rsp.nwkAddrOfInterest; uint8_t i; for (i = 0; i < pInd->rsp.n_matchList; i++) { dstAddr.endPoint = pInd->rsp.pMatchList[i]; zstack_getZCLFrameCounterRsp_t Rsp; Zstackapi_getZCLFrameCounterReq(appServiceTaskId, &Rsp); zclGeneral_SendGroupAdd(SAMPLESW_ENDPOINT, &dstAddr, GROUP_ID, GROUP_NAME, false, Rsp.zclFrameCounter); } } break;
Note
For the previous two code additions, you may see code in various
#if defined
blocks. That code is provided to enable other Zigbee features and will not be used for this SLA.Finding & Binding unnecessary
// set up default application BDB commissioning modes based on build type #if ZG_BUILD_COORDINATOR_TYPE //By default, Coordiantor has Formation selected in the CUI menu //#define DEFAULT_COMISSIONING_MODE (BDB_COMMISSIONING_MODE_NWK_STEERING | BDB_COMMISSIONING_MODE_NWK_FORMATION | BDB_COMMISSIONING_MODE_FINDING_BINDING) #define DEFAULT_COMISSIONING_MODE (BDB_COMMISSIONING_MODE_NWK_STEERING | BDB_COMMISSIONING_MODE_NWK_FORMATION) // changed for SLA #else //By default, joining devices such as Router and ZED do not have formation selected. It can be enabled in the Common User Interface (CUI) if needed. #define DEFAULT_COMISSIONING_MODE (BDB_COMMISSIONING_MODE_NWK_STEERING | BDB_COMMISSIONING_MODE_FINDING_BINDING) #endif
Send a group addressed toggle command on button press
if(key == CONFIG_BTN_RIGHT) { zstack_getZCLFrameCounterRsp_t rsp; Zstackapi_getZCLFrameCounterReq(appServiceTaskId, &rsp); // zclGeneral_SendOnOff_CmdToggle( SAMPLESW_ENDPOINT, &zclSampleSw_DstAddr, FALSE, rsp.zclFrameCounter ); // added for SLA // send a toggle afAddrType_t lightGroupAddr; lightGroupAddr.addrMode = (afAddrMode_t)AddrGroup; lightGroupAddr.addr.shortAddr = GROUP_ID; zclGeneral_SendOnOff_CmdToggle( SAMPLESW_ENDPOINT, &lightGroupAddr, FALSE, rsp.zclFrameCounter ); // end added }
Build both projects, then flash the LaunchPads. One LaunchPad will run the zc_sw, and the other two will run the zr_light.
From the sniffer logs, you should see the group commands and the group addressed toggle commands, similar to the following:
Match Descriptor and Group Add
Group addressed toggle command
Task 2: Using the Scenes Cluster with the Groups Cluster#
In Zigbee, a scene is a collection of stored values for attributes, as defined by a scene table entry. The Scenes cluster offers commands for adding and recalling scenes (among others).
Since the Scenes cluster is typically used alongside the Groups cluster, we will continue development from Task 1.
Modify the zc_sw.
Add the Scenes cluster client to the zc_sw endpoint
const cId_t zclSampleSw_OutClusterList[] = { ZCL_CLUSTER_ID_GENERAL_IDENTIFY, ZCL_CLUSTER_ID_GENERAL_ON_OFF, ZCL_CLUSTER_ID_GENERAL_GROUPS, ZCL_CLUSTER_ID_GENERAL_SCENES, // added for SLA #if defined (OTA_CLIENT_INTEGRATED) ZCL_CLUSTER_ID_OTA #endif };
Also, confirm that
ZCL_SCENES
is in the predefined symbols for the zc_sw.Define the scene
#define SCENE_ID 0x0007 static zclGeneral_Scene_t nightScene = { .groupID = GROUP_ID, .ID = SCENE_ID, .name = { 0x08, 'm', 'y', '_', 's', 'c', 'e', 'n', 'e' }, .extLen = 4, .extField = { // extension field set 1 // cluster ID (ZCL_CLUSTER_ID_GENERAL_ON_OFF & 0xFF), ((ZCL_CLUSTER_ID_GENERAL_ON_OFF >> 8) & 0xFF), // length of extension field 1, // attribute value (eg. for OnOff attribute of On/Off cluster) 0, }, };
Send an add scene command
case zstackmsg_CmdIDs_ZDO_MATCH_DESC_RSP: // ... { zstackmsg_zdoMatchDescRspInd_t *pInd = (zstackmsg_zdoMatchDescRspInd_t *) pMsg; afAddrType_t dstAddr; memset(&dstAddr, 0x00, sizeof(afAddrType_t)); dstAddr.addrMode = afAddr16Bit; dstAddr.addr.shortAddr = pInd->rsp.nwkAddrOfInterest; uint8_t i; for (i = 0; i < pInd->rsp.n_matchList; i++) { dstAddr.endPoint = pInd->rsp.pMatchList[i]; zstack_getZCLFrameCounterRsp_t Rsp; Zstackapi_getZCLFrameCounterReq(appServiceTaskId, &Rsp); zclGeneral_SendGroupAdd(SAMPLESW_ENDPOINT, &dstAddr, GROUP_ID, GROUP_NAME, false, Rsp.zclFrameCounter); Zstackapi_getZCLFrameCounterReq(appServiceTaskId, &Rsp); zclGeneral_SendAddScene(SAMPLESW_ENDPOINT, &dstAddr, &nightScene, FALSE, Rsp.zclFrameCounter); } } break;
Send a scene recall command on button press
if(key == CONFIG_BTN_RIGHT) { zstack_getZCLFrameCounterRsp_t rsp; Zstackapi_getZCLFrameCounterReq(appServiceTaskId, &rsp); // send a group addressed scene recall command afAddrType_t lightGroupAddr; lightGroupAddr.addrMode = (afAddrMode_t)AddrGroup; lightGroupAddr.addr.shortAddr = GROUP_ID; zclGeneral_SendSceneRecall(SAMPLESW_ENDPOINT, &lightGroupAddr, GROUP_ID, SCENE_ID, TRUE, rsp.zclFrameCounter); }
In the zed_light:
Check the Scenes cluster callbacks
The Scenes Recall callback is used to notify the application that a Scene Recall command has been received. The application can then set the attributes to the values contained in that Scene. Additionally, Scene attributes indicating the recently recalled scene are updated.
Scenes cluster callbacks:
#ifdef ZCL_SCENES NULL, // Scene Store Request command zclSampleLight_SceneRecallCB, // Scene Recall Request command NULL, // Scene Response command #endif Handles the Scene Recall callback: :::{code-block} c :caption: "in zcl_samplelight.c" static void zclSampleLight_SceneRecallCB( zclSceneReq_t *pReq ) { zclGeneral_Scene_extField_t extField; uint8_t *pBuf; uint8_t extLen = 0; pBuf = pReq->scene->extField; extField.AttrLen = pBuf[2]; while(extLen < ZCL_GENERAL_SCENE_EXT_LEN) { //Parse ExtField extField.ClusterID = BUILD_UINT16(pBuf[0],pBuf[1]); extField.AttrLen = pBuf[2]; extField.AttrBuf = &pBuf[3]; if(extField.AttrLen == 0xFF || extField.ClusterID == 0xFFFF) { break; } //If On/Off then retrieve the attribute if(extField.ClusterID == ZCL_CLUSTER_ID_GENERAL_ON_OFF) { uint8_t tempState = *extField.AttrBuf; zclSampleLight_updateOnOffAttribute(tempState); } #ifdef ZCL_LVL_CTRL //If Level Control then retrieve the attribute else if(extField.ClusterID == ZCL_CLUSTER_ID_GENERAL_LEVEL_CONTROL) { uint8_t tempState = *extField.AttrBuf; zclSampleLight_updateCurrentLevelAttribute(tempState); } #endif //Move to the next extension field (increase pointer by clusterId, Attribute len field, and attribute) pBuf += sizeof(uint16_t) + sizeof(uint8_t) + extField.AttrLen; extLen += sizeof(uint16_t) + sizeof(uint8_t) + extField.AttrLen; //increase ExtField } //Update scene attributes zclSampleLight_ScenesValid = TRUE; zclSampleLight_ScenesCurrentGroup = pReq->scene->groupID; zclSampleLight_ScenesCurrentScene = pReq->scene->ID; zclSampleLight_UpdateLedState(); }
Check Scenes cluster attributes
// Scenes Cluster uint8_t zclSampleLight_ScenesCurrentScene = 0; uint16_t zclSampleLight_ScenesCurrentGroup = 0; uint8_t zclSampleLight_ScenesValid = 0; uint8_t zclSampleLight_ScenesNameSupport = 1; :::{code-block} c :caption: "in zclSampleLight_Attrs of zcl_samplelight_data.c" // *** Scene Cluster Attributes *** { ZCL_CLUSTER_ID_GENERAL_SCENES, { // Attribute record ATTRID_SCENES_COUNT, ZCL_DATATYPE_UINT8, ACCESS_CONTROL_READ, NULL // Use application's callback to Read this attribute } }, { ZCL_CLUSTER_ID_GENERAL_SCENES, { // Attribute record ATTRID_SCENES_CURRENT_SCENE, ZCL_DATATYPE_UINT8, ACCESS_CONTROL_READ, (void *)&zclSampleLight_ScenesCurrentScene } }, { ZCL_CLUSTER_ID_GENERAL_SCENES, { // Attribute record ATTRID_SCENES_CURRENT_GROUP, ZCL_DATATYPE_UINT16, ACCESS_CONTROL_READ, (void *)&zclSampleLight_ScenesCurrentGroup } }, { ZCL_CLUSTER_ID_GENERAL_SCENES, { // Attribute record ATTRID_SCENES_SCENE_VALID, ZCL_DATATYPE_BOOLEAN, ACCESS_CONTROL_READ, (void *)&zclSampleLight_ScenesValid } }, { ZCL_CLUSTER_ID_GENERAL_SCENES, { // Attribute record ATTRID_SCENES_NAME_SUPPORT, ZCL_DATATYPE_BITMAP8, ACCESS_CONTROL_READ, (void *)&zclSampleLight_ScenesNameSupport } }, { ZCL_CLUSTER_ID_GENERAL_SCENES, { // Attribute record ATTRID_CLUSTER_REVISION, ZCL_DATATYPE_UINT16, ACCESS_CONTROL_READ, (void *)&zclSampleLight_scenes_clusterRevision } }, :::{code-block} c :caption: "in zcl_samplight.h" // Scenes attributes extern uint8_t zclSampleLight_ScenesCurrentScene; extern uint16_t zclSampleLight_ScenesCurrentGroup; extern uint8_t zclSampleLight_ScenesValid; extern uint8_t zclSampleLight_ScenesNameSupport;
To visually differentiate between Scene recalls, add in code to set a default LED state
if(key == CONFIG_BTN_RIGHT) { zclSampleLight_updateOnOffAttribute(LIGHT_ON); zclSampleLight_UpdateLedState(); }
Flash these projects on the respective LaunchPads.
Press BTN-1 (the left button) on the zc_sw to form/open the network.
Press BTN-1 on each zr_sw to join the network.
Press BTN-2 (the right button) on the zc_sw to send a group addressed Scene Recall command.
Press BTN-2 on each zr_light to turn on their red LED. From the sniffer logs, you should see the Scene commands, similar to the following:
Add Scenes command/response
Recall Scene
Please refer to the Groups and Scenes clusters in the Zigbee Cluster Library documentation for further information.