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:

  1. Keepalives and the Poll Control Cluster

  2. 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#

Hardware#

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
../../../_images/zigbee_spec_table_3_55.PNG

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:

../../../_images/uniflash_erase_memory4.png

Uniflash Erase Memory#

Task 1: Parent Aging Out a Child#

  1. For this task, we will use the zed_light and zc_sw examples. In CCS, go to File > Import > C/C++ > CCS Projects and under Select 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.

    ../../../_images/ccs_import.png

    Import zed_light#

    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.

  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 the NWK_END_DEVICE_LEAVE_TIMEOUT. To do this on the ZED, set END_DEV_TIMEOUT_VALUE to 0 (10 secs) and poll rate to 15 secs. No need to change NWK_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 inside Application/zcl_samplelight.c. Once the ZED has joined a network, the BDB commissioning process will be in the BDB_COMMISSIONING_NWK_STEERING state with a status of BDB_COMMISSIONING_SUCCESS. Add code to change the polling rate here.

    zclSampleLight_ProcessCommissioningStatus in zcl_samplelight.c#
      // ...
      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
    ../../../_images/aging_out_child.PNG

    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.

  1. Integrate the Poll Control cluster to the projects by performing the following for both projects:

    Include zcl_poll_control.c in build

    Right click Common/zcl/zcl_poll_control.c and de-select “Exclude from Build”.

    ../../../_images/include_in_build_poll_control.png

    Include zcl_poll_control.c#

    In zcl_*.c and zcl_*_data.c, make sure you have #include "zcl_poll_control.h". These files contain commands and definitions needed to implement the Poll Control cluster.

  2. Add Poll Control cluster into the cluster list (in zcl_*_data.c).

    For client (sw): add to zclSampleSw_OutClusterList
    zcl_samplesw_data.c#
    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
    zcl_samplelight_data.c#
    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.

  3. Add Poll Control cluster attributes to the server’s attribute list.

    Declare attributes and assign default values
    in zcl_samplelight_data.c#
      // 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
    at the end of zclSampleLight_Attrs[] in zcl_samplelight_data.c#
    #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
    
  4. Declare and define the Poll Control callbacks struct, the callbacks themselves, and other relevant code:

    Poll Control server side:
    define new event in zcl_samplelight.h#
    // 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:
    add to zcl_samplesw.c#
    /*********************************************************************
    * 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
      // ...
    
  5. On the server side, add in code to handle events triggered by the Poll Control cluster.

    Handle the events related to Poll Control
    in zclSampleLight_process_loop in zcl_samplelight.c#
      #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
    
  6. On the server side, add in code to initialize the Poll Control cluster clocks:

    Initialization of Poll Control cluster clocks
    add to zcl_samplelight.c#
      #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
    in zcl_samplelight.c#
      static void zclSampleLight_initializeClocks(void)
      {
          // ...
    
          #ifdef ZCL_POLL_CONTROL
              zclPollControl_initClocks();
          #endif   
      }
    
  7. 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
    in zcl_samplesw.c#
    #ifdef ZCL_POLL_CONTROL
      static zstack_LongAddr_t PollControl_serverLongAddr;
    #endif
    
    Set up notifications of Device Announce and Simple Descriptor Response messages
    in zcl_samplesw.c#
    /*******************************************************************************
    * @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
    in function zclSampleSw_processZStackMsgs of zcl_samplesw.c#
    // ...
    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
    in function zclSampleSw_processZStackMsgs of zcl_samplesw.c#
          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;
    
  8. 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
      ../../../_images/check_in_interval.PNG

      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
      ../../../_images/check_in.png

      Checkin#

    • 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
      ../../../_images/short_poll_interval.png

      Short poll interval#

    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.

  1. EP1 and EP2 may reside on one device (eg. both on one LaunchPad)

  2. 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.

  1. 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

  2. Add code to the zc_sw (Groups cluster client).

    Define the group that the zc_sw will add lights to
    in zcl_samplesw.c#
    // 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
    in SetupZStackCallbacks of zcl_samplesw.c#
    zdoCBReq.has_deviceAnnounce = true;
    zdoCBReq.deviceAnnounce = true;
    zdoCBReq.has_matchDescRsp = true;
    zdoCBReq.matchDescRsp = true;
    
    Send Match Descriptor request after a device has joined
    in zclSampleSw_ProcessCommissioningStatus of zcl_samplesw.c#
    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
    in zclSampleSw_processZStackMsgs of zcl_samplesw.c#
    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
    in zstack.h and zcl_sampleapps_ui.c#
    // 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
    in zclSampleSw_processKey of zcl_samplesw.c#
    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
    }
    
  3. 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
    ../../../_images/groups_add_success.PNG

    groups_add_success#

    Group addressed toggle command
    ../../../_images/group_addressed_toggle_command.PNG

    groups_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.

  1. Modify the zc_sw.

    Add the Scenes cluster client to the zc_sw endpoint
    in zcl_samplesw_data.c#
      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
    in zcl_samplesw.c#
      #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
    in zclSampleSw_processZStackMsgs of zcl_samplesw.c#
      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
    in zclSampleSw_processKey of zcl_samplesw.c#
    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);
    }
    
  2. 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:

    in zclSampleLight_CmdCallbacks zcl_samplelight.c#
    #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
    in zcl_samplelight_data.c#
    // 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
    in zclSampleLight_processKey of zcl_samplelight.c#
    if(key == CONFIG_BTN_RIGHT)
    {
      zclSampleLight_updateOnOffAttribute(LIGHT_ON);
      zclSampleLight_UpdateLedState();
    }
    
  3. 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
    ../../../_images/scene_cmd_rsp.PNG

    scene_cmd_rsp.PNG#

    ../../../_images/scene_cmd_rsp_packets.PNG

    scene_cmd_rsp_packets.PNG#

    Recall Scene
    ../../../_images/scene_recall.PNG

    scene_recall.PNG#

Please refer to the Groups and Scenes clusters in the Zigbee Cluster Library documentation for further information.