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#

Hardware#

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_memory1.png

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.

  1. We will start by loading the out-of-box onoff_light project. In CCS, go to File > Import Project(s) and under Search directory: browse to the following path:

    C:\ti\simplelink_lowpower_f3_sdk_<version>\examples\rtos\<LaunchPad variant>\zigbee\onoff_light

    ../../_images/ccs_import_onoff_light1.png

    Import OnOff Light#

  2. 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];
    
  3. 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 of zboss_signal_handler, we can use ZB_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 the zb_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;
    //...
    }
    
  4. 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 the zb_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);
    }
    
  5. 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 use zb_get_long_address to obtain the bind source IEEE address and zb_get_short_address to get the request source short address, since these parameters are both pertaining to the local device. The zb_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);
    }
    
  6. Once the bind is successfully confirmed using bind_device_cb, we can use ZB_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, whereas LIGHT_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#

  1. Repeat Step 1 of Task 1, except with the onoff_switch project

  2. (Optional) open on_off_light.syscfg in the SysConfig Editor, navigate to the Zigbee module, and select the Device Type and Radio primary channels. Note that the channel must be the same between both Light and Switch devices.

  3. In order to process the reports received from the ZC add a ZB_ZCL_CMD_REPORT_ATTRIB statement inside of zcl_specific_cluster_cmd_handler from on_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#

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

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

    ../../_images/ccs_debug_board_selection1.png

    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.

  3. Run the program from inside the CCS debugger, then exit the debugger and reset the LaunchPad using the pushbutton on top.

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

    ../../_images/binding_sniffer_log.png

    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.

  1. You should also now be able to notice the ZC Light reporting to the ZED Switch at an interval between LIGHT_REPORTING_MIN_INTERVAL and LIGHT_REPORTING_MAX_INTERVAL seconds (pending the next Data Request packet from the sleepy ZED). AF acknowledgments are requested by using the ZB_ZCL_ENABLE_DEFAULT_RESPONSE parameter in ZB_ZCL_GENERAL_INIT_CONFIGURE_REPORTING_SRV_REQ

    ../../_images/reporting_sniffer_log.png

    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:

on_off_switch.c#
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.

on_off_switch.c#
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.

../../_images/af_data_request_sniffer_log.png

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