Zigbee学习记录:基于Z-Stack 3.0.2的led开关控制指令和温湿度数据上报与接收

引言

本文记录了笔者基于Z-Stack 3.0.2协议栈,通过学习Zigbee通信协议,实现一个简单的开关控制指令和温湿度数据上报过程。通过这个过程,笔者对Zigbee网络的形成、设备间的通信有了更深入的理解。

在文章末尾,笔者有几个困惑点,希望各位读者能分享一下自己的经验和思路,让笔者更加深入探索Zigbee技术

工程代码链接:通过百度网盘分享的文件:Zigbee开关指令与温湿度数据上报.zip
链接:https://pan.baidu.com/s/1CfruCL2oOfzR2cFF4TbZkg?pwd=a97l 
提取码:a97l

这里推荐一个学习zigbee 3.0的网址:

其中笔者也是在这个地方学习的zigbee相关知识

Zigbee 3.0 开发指南

一、前期准备

1.1 下载与安装

首先,从官方渠道下载Z-Stack 3.0.2协议栈,并进行安装。Z-Stack是TI公司提供的Zigbee协议栈,它包含了构建Zigbee网络所需的所有基础组件和示例代码。

1.2 打开示例工程

 在Z-Stack 3.0.2中,提供了多个示例工程,本文选择GenericApp作为起点。这个示例工程展示了如何使用Zigbee协议进行基本的设备通信。

1.3移植外设驱动

这里需要移植一下 善学坊 的oled显示驱动函数,可以参考一下下面的链接:

扫描二维码关注公众号,回复: 17538043 查看本文章

注意不同板子的引脚不同,需要在对应的.h文件中更改引脚位号

善学坊 OLED显示器实验

二、实现过程

2.1 开启和加入网络

协调器是Zigbee网络的中心节点,负责创建和管理网络。在zclGenericApp_Init函数中,通过宏定义ZDO_COORDINATOR来区分协调器和其他设备(路由器或终端)。协调器的初始化过程包括注册ZDO消息、启动网络形成过程以及允许设备加入网络。

#if defined(ZDO_COORDINATOR)    //协调器
#elif defined(RTR_NWK)          //路由器
#else                           //终端节点         
#endif

1.在APP组  ->  zcl_genericapp.c  ->   void zclGenericApp_Init( byte task_id )的末尾添加下图代码:

对于协调器:开启网络

对于终端节点:加入网络并在网络中广播自己本体

还需要添加定时事件1,用来控制led闪烁展示设备连接网络的状态

对于终端节点:

还需要添加一个定时事件2,用来读取温湿度数据并且根据网络连接情况来上报数据

#if defined(ZDO_COORDINATOR)    //协调器
    ZDO_RegisterForZDOMsg ( zclGenericApp_TaskID, Device_annce );
    bdb_StartCommissioning( BDB_COMMISSIONING_MODE_NWK_FORMATION |
                            BDB_COMMISSIONING_MODE_FINDING_BINDING );
    NLME_PermitJoiningRequest(255);
#elif defined(RTR_NWK)          //路由器

#else       //终端节点
    bdb_StartCommissioning( BDB_COMMISSIONING_MODE_NWK_STEERING |
                            BDB_COMMISSIONING_MODE_FINDING_BINDING );
    zclGenericApp_DeviceAnnce();
    osal_start_timerEx( zclGenericApp_TaskID, GENERICAPP_EVT_2, 2000 );
#endif
    osal_start_timerEx( zclGenericApp_TaskID, GENERICAPP_EVT_1, 0 );

2.2 设备发现 

2.2.1 终端节点设备广播

 路由器或终端在加入网络后,会通过ZDP_DeviceAnnce函数广播自己的设备信息,包括网络地址、物理地址等。协调器接收到这些信息后,会保存设备的网络地址,以便后续通信。

    static void zclGenericapp_DeviceAnnce( void )
    {    
        ZDP_DeviceAnnce(
            NLME_GetShortAddr(),//获取本设备的网络地址(短地址)
            NLME_GetExtAddr(),//获取本设备的物理地址(通常就是MAC地址)
            ZDO_Config_Node_Descriptor.CapabilityFlags,//暂不展开简介,可忽略
            0//暂不展开讲解,可忽略
         );    
    }
2.2.2 协调器处理设备广播

协调器在接收到设备广播后,会自动调用zclGenericapp_processZDOMgs函数处理这些信息,并保存设备的网络地址。

这里的zclGenericapp_processZDOMgs函数需要在zclGenericApp_event_loop函数中调用:

    #ifdef ZDO_COORDINATOR
       case ZDO_CB_MSG:
          zclGenericapp_processZDOMgs( (zdoIncomingMsg_t *)MSGpkt );
          break; 
    #endif 
    static void zclGenericApp_ProcessZDOMsg(zdoIncomingMsg_t *pMsg)
    {
      switch ( pMsg->clusterID )
      {
        case Device_annce:
        {
          char str[20] = {0};
          LedDuty = 3;
          zcGenericApp_OnOffAddr[EndDeviceIndex++] = pMsg->srcAddr.addr.shortAddr; 
          sprintf(str, "Node:%X %X", zcGenericApp_OnOffAddr[0], zcGenericApp_OnOffAddr[1]);
          HalLcdWriteString(str, 3);
          EndDeviceIndex %= 2;
        }
        break;
        
        default:
        break;
      }
    }

2.3 连接网络状态处理

在路由器或终端中,需要处理网络加入的状态。如果加入成功,则加快LED闪烁频率;如果加入失败,则减慢LED闪烁频率,并尝试重新加入网络。

这里利用hal库中led驱动函数:

 这里定义一个占空比变量:uint8 LedDuty = 50;

HalLedBlink( HAL_LED_2, 1, LedDuty, 1000);

 这里的意义是:以周期1000ms占空比50%闪烁一次LED2

在这里初始化了一个任务:在void zclGenericApp_Init( byte task_id )

osal_start_timerEx( zclGenericApp_TaskID, GENERICAPP_EVT_1, 0 );

uint16 zclGenericApp_event_loop( uint8 task_id, uint16 events )

  if ( events & GENERICAPP_EVT_1 )
  {
    // toggle LED 2 state, start another timer for 500ms
    HalLedBlink( HAL_LED_2, 1, LedDuty, 1000);
    osal_start_timerEx( zclGenericApp_TaskID, GENERICAPP_EVT_1, 1000 );
    
    return ( events ^ GENERICAPP_EVT_1 );
  }

 在下方函数中处理关于网络状态改变时的操作:

static void zclGenericApp_ProcessCommissioningStatus(bdbCommissioningModeMsg_t *bdbCommissioningModeMsg)函数中:

 里面有几个主要的分支:

case BDB_COMMISSIONING_PARENT_LOST:代表已经连接的网络 丢失或者恢复
case BDB_COMMISSIONING_NWK_STEERING:代表网络成功连接,或者网络连接失败

 在这些分支中,我们可以执行对应的操作,例如重新连接网络,改变led状态

2.3.1 网络丢失或者恢复

2.3.2 网络连接失败或者成功

2.4 开关指令控制LED

2.4.1 绑定命令回调

路由器或终端在初始化时,会绑定一系列ZCL命令的回调函数。当接收到相应的命令时,会调用对应的回调函数进行处理。例如,接收到开关命令时,会调用zclGenericapp_OnOffCB函数控制LED的开关。

需要开启一个宏定义:ZCL_ON_OFF

zclGenericApp_Init初始化时会注册一个回调事件:

zclGeneral_RegisterCmdCallbacks( GENERICAPP_ENDPOINT, &zclGenericApp_CmdCallbacks );

static zclGeneral_AppCallbacks_t zclGenericApp_CmdCallbacks =
{
	//省略
	#if defined(ZDO_COORDINATOR)    //协调器
	    zclGenericApp_OnOffCB,
	#elif defined(RTR_NWK)          //路由器
	#else                           //终端节点
	    zclGenericApp_OnOffCB,                // On/Off cluster commands
	#endif  
    //省略
}

在zclGenericApp_Init函数初始化:

这里需要绑定函数,接收到开关命令时,会调用zclGenericapp_OnOffCB函数控制led。

这个的意思是,如果设备收到了这个控制命令,会自动触发这个函数:zclGenericApp_OnOffCB
参数是固定的,这里参数为什么是 uint8 cmd ,笔者认为是因为这里原本就是预设好的回调函数位置,功能是预设的

static void zclGenericApp_OnOffCB( uint8 cmd )    
{    
   if(cmd == COMMAND_ON) // 命令为ON时  
   {    
     HalLedSet(HAL_LED_1, HAL_LED_MODE_ON);  // 开启所有LED  
   }    
   else if(cmd == COMMAND_OFF) // 命令为OFF时  
   {    
     HalLedSet(HAL_LED_1, HAL_LED_MODE_OFF);  // 关闭所有LED  
   }
   else if(cmd == COMMAND_TOGGLE) // 命令为OFF时  
   {    
     HalLedSet(HAL_LED_1, HAL_LED_MODE_TOGGLE);  // 关闭所有LED  
   } 
}
2.4.2 开关指令发送 

有了接收那肯定有发送,这个发送的函数是协议栈中自带的,拿一个关命令举例:

zclGeneral_SendOnOff_CmdOn(GENERICAPP_ENDPOINT, &destAddr, TRUE, txID++);

 然后自己封装一个函数:

传入设备在网络中的短地址和命令,通过点对点通信实现对某一个设备的命令控制

static void zclGenericApp_OnOffCommand( uint16 shortAddr, uint8 cmd ) 
{  
      afAddrType_t destAddr;
      static uint8 txID = 0;
      
        destAddr.endPoint = GENERICAPP_ENDPOINT;
        destAddr.addrMode = afAddr16Bit;
        destAddr.addr.shortAddr = shortAddr;
      switch( cmd )
      {
          case COMMAND_ON:
            zclGeneral_SendOnOff_CmdOn(GENERICAPP_ENDPOINT, &destAddr, TRUE, txID++);
            break;
          case COMMAND_OFF:
            zclGeneral_SendOnOff_CmdOff(GENERICAPP_ENDPOINT, &destAddr, TRUE, txID++);
            break;
          case COMMAND_TOGGLE:
            zclGeneral_SendOnOff_CmdToggle(GENERICAPP_ENDPOINT, &destAddr, TRUE, txID++);
            break;
          default:
            break;
      }
}
2.4.3  开关指令发送

这里面包含后续的数据上报函数

根据预编译设置不同的网络短地址,来实现发送不同设备的功能

在按键处理函数中实现:
static void zclGenericApp_HandleKeys( byte shift, byte keys )
{
  if ( keys & HAL_KEY_SW_6 )
  {
        uint16 shortAddr;
#if defined(ZDO_COORDINATOR)    //协调器
        char *str = "Coordinator";
        shortAddr = zcGenericApp_OnOffAddr[0];
#elif defined(RTR_NWK)          //路由器
#else                           //终端节点
        char *str = "EndDevice";
        shortAddr = 0x0000;
        
#endif
        zclGenericApp_OnOffCommand( shortAddr, COMMAND_TOGGLE );
        zclGenericApp_ReportCommand( shortAddr, osal_strlen(str), (uint8 *)str); 
    
  }
}

 2.5 数据上报

2.5.1 数据上报函数

这里自己定义一个函数,通过传入设备的短地址和数据长度以及数据指针,实现上报数据到指定的设备中
reportCmd->attrList[0]里面的每一项数据,需要按照自己实际需求来设置,开辟空间,最后需要释放空间
函数中涉及的属性ID和cluster ID最好是使用协议栈中规定的内容,学习过程中,使用私有定义也没问题

函数里调用一个协议栈自带的函数:zcl_SendReportCmd,这个就是发送的功能函数,里面还会一层一层的往下调用,学习中只需要知道这个函数的使用即可

/*
 * 数据上报事件的处理函数,用于上报数据
 */
static void zclGenericApp_ReportCommand(uint16 shortAddr, uint16 len, uint8 *cmd)
{  
    static uint8 seqNum = 0;  
    zclReportCmd_t *reportCmd;  

    //目标设备的地址信息
    afAddrType_t destAddr;  
    destAddr.addrMode = afAddr16Bit;
    destAddr.endPoint = GENERICAPP_ENDPOINT;
    destAddr.addr.shortAddr = shortAddr;

    reportCmd = (zclReportCmd_t *)osal_mem_alloc(sizeof(zclReportCmd_t)+sizeof(zclReport_t));//申请内存空间

    if(reportCmd == NULL)//判断内存空间是否申请成功
         return;  


    reportCmd->numAttr = 1;//属性数量为1
    reportCmd->attrList[0].attrID = ATTRID_CLUSTER_REVISION;//属性ID
    reportCmd->attrList[0].dataType = ZCL_DATATYPE_CHAR_STR;//数据类型
    (reportCmd->attrList[0].attrData) = cmd;//属性值
    
    //上报数据
    zcl_SendReportCmd(GENERICAPP_ENDPOINT,//源端点号
        &destAddr,//地址信息
        ZCL_CLUSTER_ID_MS_TEMPERATURE_MEASUREMENT,//Cluster ID
        reportCmd,
        ZCL_FRAME_CLIENT_SERVER_DIR,//通信方向为从客户端到服务端
        TRUE,//关闭默认响应(目标设备的响应)
         seqNum++ );//数据包标号,每上报一次数据seqNum的值就会增加1

    osal_mem_free(reportCmd);
}
2.5.2 数据上报接收处理函

设备接受数据上报的信息:

uint16 zclGenericApp_event_loop( uint8 task_id, uint16 events ) 的 下方函数


在这个函数中:

static void zclGenericApp_ProcessIncomingMsg( zclIncomingMsg_t *pInMsg )
{
  switch ( pInMsg->zclHdr.commandID )
  {
    //省略
    case ZCL_CMD_REPORT:
      bdb_ProcessIncomingReportingMsg( pInMsg );
      break;
     //省略
    default:
      break;
  }

  if ( pInMsg->attrCmd )
    osal_mem_free( pInMsg->attrCmd );
}

 如果接受到数据上报会触发一个分支:case ZCL_CMD_REPORT:

这里放自己的数据处理函数,例如下方的接受节点发送的字符串数据

static uint8 bdb_ProcessIncomingReportingMsg( zclIncomingMsg_t *pInMsg )
{
    zclReportCmd_t *reportCmd;  
    uint8 i;  

    reportCmd = (zclReportCmd_t *)pInMsg->attrCmd;  

    for ( i = 0; i < reportCmd->numAttr; i++ )//reportCmd->numAttr为属性数量
    {  
         if( pInMsg -> clusterId == ZCL_CLUSTER_ID_MS_TEMPERATURE_MEASUREMENT//Cluster ID
            && reportCmd->attrList[i].attrID == ATTRID_CLUSTER_REVISION)//属性ID
        {  
            char str[20];
            uint8* attrDat = (reportCmd->attrList[i].attrData);//读取属性值
            sprintf(str, "Rx:%s", attrDat);
            HalLcdWriteString( str, 4 );//显示属性值
        }  
    }  
    return ( TRUE );   
}

其中这里的:

if( pInMsg -> clusterId == ZCL_CLUSTER_ID_MS_TEMPERATURE_MEASUREMENT//Cluster ID
            && reportCmd->attrList[i].attrID == ATTRID_CLUSTER_REVISION)//属性ID


            就是用来判断哪个设备发送来的数据,实际使用中可能需要更多的判断 

三、问题与思考,请求各位读者解答

3.1 多个终端设备如何实现控制:

例如我有三个设备,一个协调器,一个温湿度终端设备,一个风扇终端设备,想通过温度去控制风扇
未确认思路:是需要把温湿度数据上报给协调器,然后协调器再根据代码逻辑去控制风扇吗

3.2 当终端节点重新烧录固件后,需要复位节点和协调器,该终端节点才可以重新加入网络中或者重新烧录固件后,需要重新烧录所有的节点代码

未确认思路:是不是里面会有一个掉电不丢失存储着上次连接的状态,每次烧录是擦除了所以才可以重新连接到网络中,那么要怎么解决这样的问题呢

3.3 看网络上一些zigbee设备可以实现多设备或者不同厂家的设备互联,这是如何实现的

未确认的思路:是不是因为协议栈开发规范,需要准者每个设备的实际用途,需要按照相关规定编写代码
例如cluster,属性id,设备id,产品类型等等,网关协调器的代码也是提前写好大量的功能处理函数吗(预判设备),例如我接入一个温湿度节点设备,网关就可以获取我的温湿度
但是不清楚一个具体的实现过程

3.4 低功耗设计

Zigbee设备的一个重要特点是低功耗。目前,本实现还没有进行低功耗设计。未来可以通过优化代码、使用低功耗硬件、设置休眠模式等方式来降低设备功耗。

3.5 工程裁剪与优化

Z-Stack 3.0.1协议栈包含了大量的功能和示例代码,对于特定的应用来说,可能有很多不需要的部分。未来可以根据实际需求对工程进行裁剪和优化,去除不需要的功能和代码,减少资源占用和功耗。

四、总结与展望

通过本次学习实践,笔者成功实现了基于Z-Stack 3.0.2的Zigbee成功实现一个简单的开关控制指令和温湿度数据上报过程。这个过程不仅加深了对Zigbee协议的理解,还提高了编程实践和问题解决的能力。未来,笔者将继续深入探索Zigbee技术的更多应用场景和优化方法,为物联网技术的发展贡献自己的力量。

同时也希望读者阅读本文章末尾的问题后,能提供一些解决思路,让笔者更加深入探索Zigbee技术