5.1 应用层对 ZCL API 的调用

一. 基本调用模式

(1)前面章节讲解到在应用层可以调用ZCL API 来进行数据通信,本节课讲解一下在应用层调用 ZCL API 的基本模式。

(2)打开zcl_samplesw.c文件中的应用层初始化函数zclSampleSw_Init,可以找到如下代码:

  1. // Register the ZCL General Cluster Library callback functions
  2. zclGeneral_RegisterCmdCallbacks(SAMPLESW_ENDPOINT,
  3. &zclSampleSw_CmdCallbacks);
  4. zclSampleSw_ResetAttributesToDefaultValues();
  5. // Register the application's attribute list
  6. zcl_registerAttrList(SAMPLESW_ENDPOINT,zclSampleSw_NumAttributes,zclSampleSw_Attrs);
  7. // Register the Application to receive the unprocessed Foundation command/response messages
  8. zcl_registerForMsg( zclSampleSw_TaskID );

(3)简单讲解一下这3、4个函数的工作内容。

  • zclGeneral_RegisterCmdCallbacks()
    注册一个命令执行回调(Call Back)。这个回调中包含一个命令处理函数列表。当设备接收到命令时,就在这个列表中找打到对应的命令处理函数,处理该命令。
  • zclSampleSw_ResetAttributesToDefaultValues()
    把一些属性设置为默认值。
  • zcl_registerAttrList()
    为设备注册一个属性列表。前面章节已讲过,函数参数zclSampleSw_Attrs是一个属性列表,可在zcl_samplesw_data.c文件找到其定义。
  • zcl_registerForMsg()
    告诉设备需要接收未处理的Foundation(基础)命令或者响应消息。调用该函数后,在接收到例如读命令、写命令或上报命令等基础命令时,就会发生系统事件ZCL_INCOMING_MSG,开发者在该事件处理函数中做相关处理即可。

    二. zclSampleSw_CmdCallbacks()

    (1)可在zcl_samplesw.c文件中找到命令执行回调的定义,代码如下:
    第5章:基于ZCL的开关命令收发 - 图1
    (2)zclSampleSw_CmdCallbacks是一个命令处理函数列表(数组),其中的每个元素均表示某个特定命令的处理函数。可以通过注释或者查看该函数列表的类型定义可以查看每个元素代表的是哪个命令的处理函数。如果元素值为NULL,表示该命令无处理函数,即不处理该命令。如果需要on/off Cluster的命令,那么在其对应的元素位置设置处理处理函数即可。

    三. ZCL_INCOMING_MSG()

    (1)在AF通信章节中,当设备接收到AF层的数据时就会产生系统事件AF_INCOMING_MSG_CMD,开发者只需在对应的事件处理函数中做相应处理即可。ZCL_INCOMING_MSG的意义与之类似,如果发生系统事件ZCL_INCOMING_MSG就表示有基础命令或消息需要处理。
    (2)打开zcl_samplesw.c文件中的zdSampleSw_event_loop()函数,可以找到ZCL_INCOMING_MSG的事件处理函数zclSamplesw_ProcessIncomingMsg():
    第5章:基于ZCL的开关命令收发 - 图2
    (3)zclSampleSw_ProcessIncomingMsg()函数的定义如下: ``` 1.static void zclSampleSw_ProcessIncomingMsg(zclIncomingMsg_t *pInMsg)
    2.{
  1. switch ( pInMsg->zclHdr.commandID )//根据不同的commandID做相应的处理
  2. {
    5.#ifdef ZCL_READ
  3. case ZCL_CMD_READ_RSP: // 读命令响应
  4. zclSampleSw_ProcessInReadRspCmd( pInMsg );
  5. break;
    9.#endif

10.#ifdef ZCL_WRITE

  1. case ZCL_CMD_WRITE_RSP: // 写命令响应
  2. zclSampleSw_ProcessInWriteRspCmd( pInMsg );
  3. break;
    14.#endif

15.#ifdef ZCL_REPORT // 上报命令

  1. case ZCL_CMD_CONFIG_REPORT:
  2. //zclSampleSw_ProcessInConfigReportCmd( pInMsg );
  3. break;
  4. case ZCL_CMD_CONFIG_REPORT_RSP:
  5. //zclSampleSw_ProcessInConfigReportRspCmd( pInMsg );
  6. break;
  7. case ZCL_CMD_READ_REPORT_CFG:
  8. //zclSampleSw_ProcessInReadReportCfgCmd( pInMsg );
  9. break;
  10. case ZCL_CMD_READ_REPORT_CFG_RSP:
  11. //zclSampleSw_ProcessInReadReportCfgRspCmd( pInMsg );
  12. break;
  13. case ZCL_CMD_REPORT:
  14. //zclSampleSw_ProcessInReportCmd( pInMsg );
  15. break;
    31.#endif

  16. case ZCL_CMD_DEFAULT_RSP: //默认响应

  17. zclSampleSw_ProcessInDefaultRspCmd( pInMsg );
  18. break;

35.#ifdef ZCL_DISCOVER // 扫描命令

  1. … … // 不展开
    37.#endif

  2. default:

  3. break;
  4. }

  5. if ( pInMsg->attrCmd )

  6. osal_mem_free( pInMsg->attrCmd );
    43.} ```

该函数的内容还是挺好理解的,大致就是根据不同的commandID做相应的处理。
提示:
读者暂时只需大致了解该函数的内容即可,无需深入了解每一行代码。

(4)需要注意的是,使用ZCL的基础命令前,需要开启相应的宏的,例如使用“读”命令需要开启ZCL_READ 。其开启方式与HAL宏的开启方式是相同的,如图所示。
第5章:基于ZCL的开关命令收发 - 图3

5.2 ZCL 开关命令收发 API

在ZCL通信过程章节中,笔者举了一个网关(协调器)和智能插座(终端或路由器)的例子,下面基于该例子介绍ZCL 命令收发API。

发送命令API

前文提及到的On/Off命令是一个Cluster限定命令,在On/Off Cluster中。On/Off Cluster中还包含其他的命令,例如Toggle(反转状态)。可以调用专门的API来使用命令。打开Profile文件夹,可以找到zcl_general.h文件,如下图所示。
第5章:基于ZCL的开关命令收发 - 图4

在zcl_general.h文件中可以找如下3个API:

  1. #ifdef ZCL_ON_OFF
  2. /*
  3. * Send an On Off Command - COMMAND_ONOFF_OFF
  4. * Use like:
  5. * ZStatus_t zclGeneral_SendOnOff_CmdOff( uint16 srcEP, afAddrType_t *dstAddr, uint8 disableDefaultRsp, uint8 seqNum );
  6. */
  7. #define zclGeneral_SendOnOff_CmdOff(a,b,c,d) zcl_SendCommand( (a), (b), ZCL_CLUSTER_ID_GEN_ON_OFF, COMMAND_OFF, TRUE, ZCL_FRAME_CLIENT_SERVER_DIR, (c), 0, (d), 0, NULL )
  8. /*
  9. * Send an On Off Command - COMMAND_ONOFF_ON
  10. * Use like:
  11. * ZStatus_t zclGeneral_SendOnOff_CmdOn( uint16 srcEP, afAddrType_t *dstAddr, uint8 disableDefaultRsp, uint8 seqNum );
  12. */
  13. #define zclGeneral_SendOnOff_CmdOn(a,b,c,d) zcl_SendCommand( (a), (b), ZCL_CLUSTER_ID_GEN_ON_OFF, COMMAND_ON, TRUE, ZCL_FRAME_CLIENT_SERVER_DIR, (c), 0, (d), 0, NULL )
  14. /*
  15. * Send an On Off Command - COMMAND_ONOFF_TOGGLE
  16. * Use like:
  17. * ZStatus_t zclGeneral_SendOnOff_CmdToggle( uint16 srcEP, afAddrType_t *dstAddr, uint8 disableDefaultRsp, uint8 seqNum );
  18. */
  19. #define zclGeneral_SendOnOff_CmdToggle(a,b,c,d) zcl_SendCommand( (a), (b), ZCL_CLUSTER_ID_GEN_ON_OFF, COMMAND_TOGGLE, TRUE, ZCL_FRAME_CLIENT_SERVER_DIR, (c), 0, (d), 0, NULL )

这3个API分别实现了发送关闭、打开和反转状态命令。它们均是使用#define来定义,并且最终是调用zcl_SendCommand函数来发送命令的。zcl_SendCommand函数的定义如下:

  1. extern ZStatus_t zcl_SendCommand(
  2. uint8 srcEP,//源端点,发送者的端点号
  3. afAddrType_t *dstAddr,//目标设备地址
  4. uint16 clusterID, uint8 cmd,//Cluster ID和命令
  5. uint8 specific,//是否为属性关联命令
  6. uint8 direction,//通信方向
  7. uint8 disableDefaultRsp,//是否关闭默认响应(目标设备的响应)
  8. uint16 manuCode,//manu code
  9. uint8 seqNum,//数据包标识号,由开发者自定义
  10. uint16 cmdFormatLen,//命令格式长度
  11. uint8 *cmdFormat//命令格式
  12. );

以zclGeneral_SendOnOff_CmdOff为例子展开简介一下其对zcl_SendCommand的调用,代码如下:

  1. zcl_SendCommand(
  2. (a),
  3. (b),
  4. ZCL_CLUSTER_ID_GEN_ON_OFF,//Cluster ID
  5. COMMAND_OFF,//待发送命令为关闭命令
  6. TRUE,//TRUE表示属性关联命令
  7. ZCL_FRAME_CLIENT_SERVER_DIR,//表示通信方向为从Client到Server
  8. (c),
  9. 0,//manu code为0
  10. (d),
  11. 0,//命令格式长度为0
  12. NULL)//命令格式为空

通常,开发者调用基于zcl_SendCommand封装出来的各种命令发送API即可,例如上述的3个命令发送API。

接收命令

接收On/Off命令的方法比较简单,只需要在ZCL命令处理函数列表zclSampleSw_CmdCallbacks的对应位置添加命令处理函数即可实现命令的接收和处理了。下节课将结合具体案例进行讲解。

5.3 ZCL 开关命令收发实验

本节课将以实验的方式讲解如何使用ZCL通信API发送On/Off命令,实验设备包含一个网关(协调器)和一个智能插座(终端或路由器),内容是当智能插座加入到网络后,网关自动定期地向这个智能插座发送On和Off指令来控制智能插座的开关。

智能插座开发

打开zcl_samplesw.c文件在应用初始化函数zdSampleSw_Init中可以找到如下代码:
第5章:基于ZCL的开关命令收发 - 图5

else与#endif中的代码是当设备处于终端(或路由器)角色时执行,其中的bdb_StartCommissioning函数是用于组建网络的的,在BDB章节中已经讲解过。

zclSampleSw_DeviceAnnce函数是由笔者自定义的,用于向整个网络广播一个数据包,该数据包中包含本设备的地址。当协调器收到这个数据包时,就知道这个设备的地址信息了。其定义代码如下:

  1. static void zclSampleSw_DeviceAnnce( void )
  2. {
  3. ZDP_DeviceAnnce(
  4. NLME_GetShortAddr(),//获取本设备的网络地址(短地址)
  5. NLME_GetExtAddr(),//获取本设备的物理地址(通常就是MAC地址)
  6. ZDO_Config_Node_Descriptor.CapabilityFlags,//暂不展开简介,可忽略
  7. 0//暂不展开讲解,可忽略
  8. );
  9. }

该函数调用了ZDP_DeviceAnnce函数,这是一个协议栈的API。可以套用上述代码来实现向网络中广播本设备的地址信息。

处理指令

在处理On/Off命令前,需要先在ZCL命令回调函数列表中注册一个回调函数,即在zcl_samplesw.c文件中添加zclSampleSw_OnOffCB函数,如下图所示。
第5章:基于ZCL的开关命令收发 - 图6

zclSampleSw_OnOffCB函数的定义代码如下:

  1. 1.static void zclSampleSw_OnOffCB( uint8 cmd )
  2. 2.{
  3. 3. if(cmd == COMMAND_ON) // 命令为ON时
  4. 4. {
  5. 5. HalLcdWriteString("Set ON", 4); // 打印信息到屏幕
  6. 6.
  7. 7. HalLedSet(HAL_LED_ALL, HAL_LED_MODE_ON); // 开启所有LED
  8. 8. }
  9. 9. else if(cmd == COMMAND_OFF) // 命令为OFF时
  10. 10. {
  11. 11. HalLcdWriteString("Set OFF", 4); // 打印信息到屏幕
  12. 12.
  13. 13. HalLedSet(HAL_LED_ALL, HAL_LED_MODE_OFF); // 关闭所有LED
  14. 14. }
  15. 15.}

网关开发

网关主要完成这两件事情:

  • 接收并处理智能插座广播的地址信息
  • 启动一个周期性事件来周期地向智能插座发送On/Off指令

接收地址信息

当协调器接收到由智能插座广播(Annce)的地址消息时,会产生系统事件ZDO_CB_MSG,可以在应用层事件处理函数中处理事件,代码如下图所示。
第5章:基于ZCL的开关命令收发 - 图7

事件处理函数zclSampleSw_ProcessZDOMgs的代码定义如下:

  1. 1.static void zclSampleSw_processZDOMgs(zdoIncomingMsg_t *pMsg)
  2. 2.{
  3. 3. switch ( pMsg->clusterID )//判断消息中的Cluster ID
  4. 4. {
  5. 5. case Device_annce://如果是Device_annce
  6. 6. {
  7. 7. // 把目标设备的网络地址保存到全局变量中
  8. 8. zclSampleSw_OnOffTestAddr = pMsg->srcAddr.addr.shortAddr;
  9. 9. // 在屏幕上显示目标设备网络地址和提示信息
  10. 10. HalLcdWriteStringValue("Node:", pMsg->srcAddr.addr.shortAddr, 16, 3);
  11. 11. HalLcdWriteString("On/Off Test...", 4);
  12. 12.
  13. 13. //周期地产生SAMPLEAPP_ONOFF_TEST_EVT事件,即发送On/Off指令
  14. 14. osal_start_timerEx(zclSampleSw_TaskID,
  15. 15. SAMPLEAPP_ONOFF_TEST_EVT,//事件类型,在zcl\_samplesw.h文件中定义
  16. 16. SAMPLEAPP_ONOFF_TEST_PERIOD);//时间间隔,在zcl\_samplesw.h文件中定义
  17. 17. }
  18. 18. break;
  19. 19. default:
  20. 20. break;
  21. 21. }
  22. 22.}

处理SAMPLEAPP_ONOFF_TEST_EVT事件

在zcl_samplesw.c文件中找到zclSampelSw_eventloop函数,添加事件处理代码,如下图所示。
第5章:基于ZCL的开关命令收发 - 图8

发送On/Off指令

在SAMPLEAPP_ONOFF_TEST_EVT事件处理代码中调用了zclSampleSw_OnOffTest来发送指令,该函数定义如下:

  1. static void zclSampleSw_OnOffTest(void)
  2. {
  3. afAddrType_t destAddr;//用于保存目标设备的地址信息
  4. static uint8 txID = 0;
  5. static bool on = true;//静态变量,指示智能插座的开关状态
  6. destAddr.endPoint = SAMPLESW_ENDPOINT;//端点号
  7. destAddr.addrMode = Addr16Bit;//地址模式(类型)为16为的地址,使用P2P的通信方式
  8. destAddr.addr.shortAddr = zclSampleSw_OnOffTestAddr;//网络地址
  9. if(on) {//如果智能插座正在开启
  10. HalLcdWriteString("Command: ON", 4); // 屏幕打印提示信息
  11. zclGeneral_SendOnOff_CmdOn(//发送打开命令
  12. SAMPLESW_ENDPOINT,//端点号
  13. &destAddr,//地址信息
  14. TRUE,//TRUE表示属性关联命令
  15. txID++);
  16. }
  17. else//如果智能插座已关闭
  18. {
  19. HalLcdWriteString("Command: OFF", 4); // 屏幕打印提示信息
  20. zclGeneral_SendOnOff_CmdOff(//发送关闭命令
  21. SAMPLESW_ENDPOINT,//端点号
  22. &destAddr,//地址信息
  23. TRUE,//TRUE表示属性关联命令
  24. txID++);
  25. }
  26. on = !on;//反转开关状态
  27. }

仿真调试

  • 编译协调器工程,然后把固件烧录到其中一块开发板中,该开发板充当网关;
  • 编译终端(或路由器)工程,然后烧录到另外一块开发板中,该开发板充当智能插座;
  • 先后分别给网关和智能插座供电;
  • 智能插座会自动加入到网关创建的网络中,接着可以在智能插座的显示器中看到有网关发送过来的命令,如图所示。
    第5章:基于ZCL的开关命令收发 - 图9
    第5章:基于ZCL的开关命令收发 - 图10