我们正在Z-Stack 3.0上为具有ds18b20传感器的Zigbee继电器Sonoff BASICZBR3编写TI cc2530固件



假定读者已经具有C语言的初步知识,对Zigbee,cc2530芯片,闪存的使用方法以及使用它的知识有所了解,并且还熟悉诸如zigbee2mqtt之类的项目。如果不是这样,请准备或在https://myzigbee.ruhttps://www.zigbee2mqtt.io/上阅读
文章本文是首先详细编写的,但是逐步加速并没有止步于细节,而是描述了完成的固件代码。如果有人对推理不感兴趣,则只需打开固件源并阅读它们即可。

最终固件的源代码

代码和开发方法并不理想。“我不是魔术师,我只是在学习。”

目的


主要目标是长时间了解如何为Z-Stack编写固件。因此,我决定为成品设备实施替代固件(以Sonoff BASICZBR3继电器为例),并增加了连接流行的ds18b20温度传感器的功能。

此外,我想向初学者Zigbee开发人员展示在Z-Stack上为TI cc2530芯片开发固件的示例。

1.准备


要开始开发,您需要下载并安装Z-Stack 3.0.2-这是一个用于开发带有示例和文档的固件的SDK。
您还需要下载并安装适用于8051的IAR嵌入式工作台 -这是一个开发环境,能够针对TI cc2530芯片进行编译。免费使用期限为1个月(但寻求者会找到解决方案)。

对于开发和调试,我使用CCDebugger-它不仅允许刷新cc2531 / cc2530芯片,而且还可以在IAR环境中调试应用程序。



为了简化实验,原型设计和调试,我在devboard和相应的cc2530模块上进行了以下操作:



2.创建一个新的应用程序


我们基于GenericApp创建一个新项目。这是基本Z-Stack应用程序的示例。它位于Z-Stack 3.0.2 \ Projects \ zstack \ HomeAutomation \ GenericApp文件夹中。
我们将其复制到附近并将其重命名,例如,重命名为DIYRuZRT(让我们为设备调用应用程序)。

CC2530DB文件夹中包含文件:

  • GenericApp.ewd-C-SPY的项目设置
  • GenericApp.ewp-项目文件
  • GenericApp.eww-工作区

将文件重命名为DIYRuZRT.eww和DIYRuZRT.ewp。

在所有文件(包括Source文件夹)中,我们还将对GenericApp的所有引用更改为DIYRuZRT。

现在,在IAR中打开DIYRuZRT.ewp项目。我们选择RouterEB的配置并执行全部重建。



将在CC2530DB文件夹中创建RouterEB文件夹,并且DIYRuZRT.d51文件将出现在EXE文件夹中-此文件便于从IAR进行刷新和调试。

但是,如果我们需要通过SmartRF Flash编程器来刷新固件,那么我们将进行一些小的更改。为此,请在输出选项卡上链接部分的项目设置中,更改输出文件和格式的设置:



之后,将在EXE文件夹中创建DIYRuZRT.hex固件文件,方便从其他工具和其他方式进行刷新
但是上载此固件后,设备无法连接到网络。好吧,我们会明白的。

3.一点术语


Zigbee术语具有以下概念:

  • 端点 -终端设备的描述点。通常在简单设备中只有一个端点。在多功能设备中,以及在具有不同交互配置文件(一个配置文件-一个端点)的设备中,可以有多个配置文件。
  • 群集(群集) -与单个功能(开/关,调光,温度测量等)相关的一组属性和命令。集群指示端点实现的机会。在一个端点中,您可以实现几个不同的集群,但不能实现相同的集群。
  • 属性(属性) -可以读取或写入其值的集群的特征。集群可以具有许多属性。
  • 命令 -集群可以处理的控制消息。团队可能有参数。这是通过接收命令和参数时执行的功能来实现的。

Zigbee群集库中标准化了群集的类型,属性,命令。但是制造商可以使用具有自己的属性和团队的自己的集群。

一些可怜的生产者不关心标准,而对标准有所作为。然后,您必须适应它们。Z-Stack

术语也有其自己的概念,例如:

  • OSAL(操作系统抽象层) -操作系统的抽象级别。在这里,它们与任务(任务),消息(消息),事件(事件),计时器(定时器)和其他对象一起工作。
  • HAL(硬件抽象层) -设备抽象级别。在这里,它们通过按钮(按键),LED(指示灯),中断(Interrupt)等进行操作。

硬件层提供了程序代码及其控制设备的隔离。操作级别提供了用于在应用程序元素之间构建和交互的机制。

原则上,在开发固件时,所有这些都在下面等着您。

4.我们在基本应用程序中拥有什么?


应用程序代码位于“源”文件夹中:

  • OSAL_DIYRuZRT.c
  • zcl_DIYRuZRT.h
  • zcl_DIYRuZRT.c
  • zcl_DIYRuZRT_data.c — ,

OSAL_DIYRuZRT.c-主文件,在其中填充任务处理程序(任务)pTaskEventHandlerFn taskArr的数组,  并实现osalInitTasks初始化功能

需要所有其他文件来实现这些初始化程序和处理程序。

任务处理程序的列表pTaskEventHandlerFn taskAr r中填充了函数引用。一些任务通过相应的编译指令连接/断开。

您可以在“已定义符号”编译器选项中查看和配置编译指令:



const pTaskEventHandlerFn tasksArr[] = {
  macEventLoop,
  nwk_event_loop,
#if !defined (DISABLE_GREENPOWER_BASIC_PROXY) && (ZG_BUILD_RTR_TYPE)
  gp_event_loop,
#endif  
  Hal_ProcessEvent,
#if defined( MT_TASK )
  MT_ProcessEvent,
#endif
  APS_event_loop,
#if defined ( ZIGBEE_FRAGMENTATION )
  APSF_ProcessEvent,
#endif
  ZDApp_event_loop,
#if defined ( ZIGBEE_FREQ_AGILITY ) || defined ( ZIGBEE_PANID_CONFLICT )
  ZDNwkMgr_event_loop,
#endif
  //Added to include TouchLink functionality
  #if defined ( INTER_PAN )
    StubAPS_ProcessEvent,
  #endif
  // Added to include TouchLink initiator functionality
  #if defined ( BDB_TL_INITIATOR )
    touchLinkInitiator_event_loop,
  #endif
  // Added to include TouchLink target functionality
  #if defined ( BDB_TL_TARGET )
    touchLinkTarget_event_loop,
  #endif
  zcl_event_loop,
  bdb_event_loop,
  zclDIYRuZRT_event_loop
};

osalInitTasks是应用程序启动功能,用于注册应用程序执行的任务。

任务注册按顺序执行,每个任务都有自己的编号。重要的是要遵循与taskArr数组相同的顺序,因为根据任务编号调用处理程序。

void osalInitTasks( void )
{
  uint8 taskID = 0;

  tasksEvents = (uint16 *)osal_mem_alloc( sizeof( uint16 ) * tasksCnt);
  osal_memset( tasksEvents, 0, (sizeof( uint16 ) * tasksCnt));

  macTaskInit( taskID++ );
  nwk_init( taskID++ );
#if !defined (DISABLE_GREENPOWER_BASIC_PROXY) && (ZG_BUILD_RTR_TYPE)
  gp_Init( taskID++ );
#endif
  Hal_Init( taskID++ );
#if defined( MT_TASK )
  MT_TaskInit( taskID++ );
#endif
  APS_Init( taskID++ );
#if defined ( ZIGBEE_FRAGMENTATION )
  APSF_Init( taskID++ );
#endif
  ZDApp_Init( taskID++ );
#if defined ( ZIGBEE_FREQ_AGILITY ) || defined ( ZIGBEE_PANID_CONFLICT )
  ZDNwkMgr_Init( taskID++ );
#endif
  // Added to include TouchLink functionality
#if defined ( INTER_PAN )
  StubAPS_Init( taskID++ );
#endif
// Added to include TouchLink initiator functionality
#if defined( BDB_TL_INITIATOR )
  touchLinkInitiator_Init( taskID++ );
#endif
// Added to include TouchLink target functionality
#if defined ( BDB_TL_TARGET )
  touchLinkTarget_Init( taskID++ );
#endif
  zcl_Init( taskID++ );
  bdb_Init( taskID++ );
  zclDIYRuZRT_Init( taskID );
}

我们的应用程序注册了函数处理程序zclDIYRuZRT_event_loop和初始化函数zclDIYRuZRT_Init。它们被添加到列表的最后。
这是我们应用程序的两个主要功能。这些功能的实现在zcl_DIYRuZRT.c文件中

zclDIYRuZRT_Init-任务注册功能。
DIYRuZRT_ENDPOINT-我们的应用程序实现的端点号。

描述我们的应用程序的注册步骤是按顺序执行的:

  • bdb_RegisterSimpleDescriptor — . SimpleDescriptionFormat_t zclDIYRuZRT_SimpleDesc — , , , . OSAL_DIYRuZRT_data.c
  • zclGeneral_RegisterCmdCallbackszclGeneral_AppCallbacks_t zclDIYRuZRT_CmdCallbacks — , .
  • zcl_registerAttrListzclAttrRec_t zclDIYRuZRT_Attrs — , .
  • zcl_registerForMsg-注册接收控制消息。
  • RegisterForKeys-我们签署了接收按钮单击事件的任务。

/*********************************************************************
 * SIMPLE DESCRIPTOR
 */
// This is the Cluster ID List and should be filled with Application
// specific cluster IDs.
const cId_t zclDIYRuZRT_InClusterList[] =
{
  ZCL_CLUSTER_ID_GEN_BASIC,
  ZCL_CLUSTER_ID_GEN_IDENTIFY,
  
  // DIYRuZRT_TODO: Add application specific Input Clusters Here. 
  //       See zcl.h for Cluster ID definitions
  
};
#define ZCLDIYRuZRT_MAX_INCLUSTERS   (sizeof(zclDIYRuZRT_InClusterList) / sizeof(zclDIYRuZRT_InClusterList[0]))


const cId_t zclDIYRuZRT_OutClusterList[] =
{
  ZCL_CLUSTER_ID_GEN_BASIC,
  
  // DIYRuZRT_TODO: Add application specific Output Clusters Here. 
  //       See zcl.h for Cluster ID definitions
};
#define ZCLDIYRuZRT_MAX_OUTCLUSTERS  (sizeof(zclDIYRuZRT_OutClusterList) / sizeof(zclDIYRuZRT_OutClusterList[0]))


SimpleDescriptionFormat_t zclDIYRuZRT_SimpleDesc =
{
  DIYRuZRT_ENDPOINT,                  //  int Endpoint;
  ZCL_HA_PROFILE_ID,                     //  uint16 AppProfId;
  // DIYRuZRT_TODO: Replace ZCL_HA_DEVICEID_ON_OFF_LIGHT with application specific device ID
  ZCL_HA_DEVICEID_ON_OFF_LIGHT,          //  uint16 AppDeviceId; 
  DIYRuZRT_DEVICE_VERSION,            //  int   AppDevVer:4;
  DIYRuZRT_FLAGS,                     //  int   AppFlags:4;
  ZCLDIYRuZRT_MAX_INCLUSTERS,         //  byte  AppNumInClusters;
  (cId_t *)zclDIYRuZRT_InClusterList, //  byte *pAppInClusterList;
  ZCLDIYRuZRT_MAX_OUTCLUSTERS,        //  byte  AppNumInClusters;
  (cId_t *)zclDIYRuZRT_OutClusterList //  byte *pAppInClusterList;
};


zclDIYRuZRT_event_loop-我们的应用程序的事件处理程序的功能。

首先,系统事件在循环中处理:

  • ZCL_INCOMING_MSG-设备控制命令在zclDIYRuZRT_ProcessIncomingMsg中处理。
  • KEY_CHANGE-按钮单击事件在zclDIYRuZRT_HandleKeys中处理
  • ZDO_STATE_CHANGE-网络状态更改事件。

  if ( events & SYS_EVENT_MSG )
  {
    while ( (MSGpkt = (afIncomingMSGPacket_t *)osal_msg_receive( zclDIYRuZRT_TaskID )) )
    {
      switch ( MSGpkt->hdr.event )
      {
        case ZCL_INCOMING_MSG:
          // Incoming ZCL Foundation command/response messages
          zclDIYRuZRT_ProcessIncomingMsg( (zclIncomingMsg_t *)MSGpkt );
          break;

        case KEY_CHANGE:
          zclDIYRuZRT_HandleKeys( ((keyChange_t *)MSGpkt)->state, ((keyChange_t *)MSGpkt)->keys );
          break;

        case ZDO_STATE_CHANGE:
          zclDIYRuZRT_NwkState = (devStates_t)(MSGpkt->hdr.status);

          // now on the network
          if ( (zclDIYRuZRT_NwkState == DEV_ZB_COORD) ||
               (zclDIYRuZRT_NwkState == DEV_ROUTER)   ||
               (zclDIYRuZRT_NwkState == DEV_END_DEVICE) )
          {
            giGenAppScreenMode = GENERIC_MAINMODE;
            zclDIYRuZRT_LcdDisplayUpdate();
          }
          break;

        default:
          break;
      }

      // Release the memory
      osal_msg_deallocate( (uint8 *)MSGpkt );
    }

接下来是特殊事件DIYRuZRT_EVT_1的处理,该事件将切换LED HAL_LED_2的状态,并为同一事件启动500m的计时器。这将开始闪烁LED HAL_LED_2

  if ( events & DIYRuZRT_EVT_1 )
  {
    // toggle LED 2 state, start another timer for 500ms
    HalLedSet ( HAL_LED_2, HAL_LED_MODE_TOGGLE );
    osal_start_timerEx( zclDIYRuZRT_TaskID, DIYRuZRT_EVT_1, 500 );
    
    return ( events ^ DIYRuZRT_EVT_1 );
  }

事实是,当固件启动时,会发生HAL_KEY_SW_1事件并且正是在其中初始化了计时器和DIYRuZRT_EVT_1事件如果您按S2按钮,则闪烁将停止(我的LED保持点亮)。再按一次将开始闪烁。

5. HAL:LED和按钮


您问:“等等,哪个LED和按钮?”。最初,Z-stack中的所有示例都集中于SmartRF05 EB系列的各种调试板:



我有一个略有不同的调试板和一个带有芯片的模块。

板上有2个按钮(+重置)和3个LED(+电源指示灯)。当固件正常工作时,这是其中之一(D2)闪烁。

调用触点后,我们确定引脚,二极管和按钮的对应关系:

  • D1-P10
  • D2-P11
  • D3-P14
  • S2-P20
  • S1-P01

因此,HAL是硬件抽象层,一种从设备的实现中抽象的方法。应用程序代码使用与抽象(例如Button 1LED 2)配合使用的宏和函数,并且抽象和设备的特定对应关系是单独设置的。

让我们弄清楚什么是HAL_LED_2,以及如何了解它挂在哪个引脚上。通过

搜索,我们找到了hal_led.h文件,其中描述了这些常量和HalLedSet函数,并在其中传输了LED编号和模式。在内部,调用HalLedOnOff函数来打开和关闭LED,后者依次执行HAL_TURN_ON_LED2HAL_TURN_OFF_LED2

HAL_TURN_ON_LED2HAL_TURN_OFF_LED2是中描述的宏hal_board_cfg.h宏会根据硬件配置而变化。
就我而言:

#define HAL_TURN_OFF_LED2()       st( LED2_SBIT = LED2_POLARITY (0); )
#define HAL_TURN_ON_LED2()        st( LED2_SBIT = LED2_POLARITY (1); )

文件中稍高一点的是LED2_SBITLED2_POLARITY的对应关系

  /* 2 - Red */
  #define LED2_BV           BV(1)
  #define LED2_SBIT         P1_1
  #define LED2_DDR          P1DIR
  #define LED2_POLARITY     ACTIVE_HIGH

这意味着LED 2位于引脚P1_1上,并且其开关电平为高。但是,根据代码判断,当按下按钮时,LED应该熄灭,但随着我们的出现,LED会一直亮着。如果在此文件hal_board_cfg.h中,我们进行了更改:

#define LED2_POLARITY     ACTIVE_HIGH



#define LED2_POLARITY     ACTIVE_LOW

然后现在按S2按钮时LED熄灭,这应该是逻辑上的。

为了不更改与我们的应用程序不相关的通用文件,最好这样做:

  • 在我们的Source文件夹中创建hal_board_cfg.h文件的副本(来自Z-Stack 3.0.2 \ Components \ hal \ target \ CC2530EB \文件夹),并将其命名为hal_board_cfg_DIYRuZRT.h
  • 让我们将文件的副本制作为第一个副本(从而排除共享文件的连接)。在我们的Source文件夹中创建一个preinclude.h文件,并在其中写入以下行:

#include "hal_board_cfg_DIYRuZRT.h"

  • 表示此文件的连接是第一个-在项目设置中:

$PROJ_DIR$\..\Source\preinclude.h



现在,我们可以在我们的设备参数hal_board_cfg_DIYRuZRT.h文件,并在preinclude.h文件,而无需修改共享文件。

我将编译器指令转移到相同的preinclude.h文件,并在编译器选项中将其删除:

#define SECURE 1
#define TC_LINKKEY_JOIN
#define NV_INIT
#define NV_RESTORE
#define xZTOOL_P1
#define xMT_TASK
#define xMT_APP_FUNC
#define xMT_SYS_FUNC
#define xMT_ZDO_FUNC
#define xMT_ZDO_MGMT
#define xMT_APP_CNF_FUNC
#define LEGACY_LCD_DEBUG
#define LCD_SUPPORTED DEBUG
#define MULTICAST_ENABLED FALSE
#define ZCL_READ
#define ZCL_WRITE
#define ZCL_BASIC
#define ZCL_IDENTIFY
#define ZCL_SCENES
#define ZCL_GROUPS

在同一个文件hal_board_cfg_DIYRuZRT.h中,我们找到S1按钮和操纵杆中心按的说明:

/* S1 */
#define PUSH1_BV          BV(1)
#define PUSH1_SBIT        P0_1

/* Joystick Center Press */
#define PUSH2_BV          BV(0)
#define PUSH2_SBIT        P2_0
#define PUSH2_POLARITY    ACTIVE_HIGH

这对应于板上按钮的引脚。

让我们看一下硬件初始化- 同一文件中HAL_BOARD_INIT默认情况下,HAL_BOARD_CC2530EB_REV17指令处于打开状态,因此我们看一下相应的宏变体。

/* ----------- Board Initialization ---------- */
#if defined (HAL_BOARD_CC2530EB_REV17) && !defined (HAL_PA_LNA) && \
    !defined (HAL_PA_LNA_CC2590) && !defined (HAL_PA_LNA_SE2431L) && \
    !defined (HAL_PA_LNA_CC2592)

#define HAL_BOARD_INIT()                                         \
{                                                                \
  uint16 i;                                                      \
                                                                 \
  SLEEPCMD &= ~OSC_PD;                       /* turn on 16MHz RC and 32MHz XOSC */                \
  while (!(SLEEPSTA & XOSC_STB));            /* wait for 32MHz XOSC stable */                     \
  asm("NOP");                                /* chip bug workaround */                            \
  for (i=0; i<504; i++) asm("NOP");          /* Require 63us delay for all revs */                \
  CLKCONCMD = (CLKCONCMD_32MHZ | OSC_32KHZ); /* Select 32MHz XOSC and the source for 32K clock */ \
  while (CLKCONSTA != (CLKCONCMD_32MHZ | OSC_32KHZ)); /* Wait for the change to be effective */   \
  SLEEPCMD |= OSC_PD;                        /* turn off 16MHz RC */                              \
                                                                 \
  /* Turn on cache prefetch mode */                              \
  PREFETCH_ENABLE();                                             \
                                                                 \
  HAL_TURN_OFF_LED1();                                           \
  LED1_DDR |= LED1_BV;                                           \
  HAL_TURN_OFF_LED2();                                           \
  LED2_DDR |= LED2_BV;                                           \
  HAL_TURN_OFF_LED3();                                           \
  LED3_DDR |= LED3_BV;                                           \
  HAL_TURN_OFF_LED4();                                           \
  LED4_SET_DIR();                                                \
                                                                 \
  /* configure tristates */                                      \
  P0INP |= PUSH2_BV;                                             \
}

在此宏中,处理器的模式和寄存器被初始化。
取而代之的是将LED2_DDR和其他LED2_DDR替换为P1DIR-将该端口P1注册,充电操作模式引脚(输入或输出)。相应地,LED2_BV设置为相应引脚的每位1(在我们的情况下为1位,对应于P1_1引脚):



寄存器和处理器模式在

“ cc253x用户指南”的文档中进行了描述,但是在何处看不到

按钮的配置方式。按钮的处理方式类似,但是在另一个文件hal_key.c中。它定义按钮的参数和功能HalKeyInitHalKeyConfigHalKeyReadHalKeyPoll这些功能负责初始化使用按钮和读取值的子系统。

缺省情况下,定时器每100ms进行一次按钮处理。当前配置的引脚P2_0被分配到操纵杆,并且单击即可读取其当前状态-因此,LED闪烁计时器启动。

6.我们为自己配置设备


在文件zcl_DIYRuZRT.h中进行更改

  • DIYRuZRT_ENDPOINT在1

在文件OSAL_DIYRuZRT_data.c中

  • DIYRuZRT_DEVICE_VERSION为1
  • {6,'D','I','Y','R','u','Z'}上的zclDIYRuZRT_ManufacturerName
  • {9,'D','I','Y','R','u','Z','_','R','T'}上的zclDIYRuZRT_ModelId
  • {8,'2','0','2','0','0','4','0','5'}上的zclDIYRuZRT_DateCode

为了使设备能够在任何通道上连接网络(默认情况下,Tools \ f8wConfig.cfg文件DEFAULT_CHANLIST伪指令仅指定11个),必须通过更改伪指令的值preinclude.h文件中指定此功能 我们还添加了编译指令DISABLE_GREENPOWER_BASIC_PROXY,以便未为我们的设备创建GREENPOWER端点。 同时关闭对LCD屏幕的不必要的支持。




//#define LCD_SUPPORTED DEBUG
#define DISABLE_GREENPOWER_BASIC_PROXY
#define DEFAULT_CHANLIST 0x07FFF800  // ALL Channels

为了使我们的设备自动尝试连接到网络,我们将在功能代码zclDIYRuZRT_Init中添加到网络的连接

bdb_StartCommissioning(BDB_COMMISSIONING_MODE_NWK_STEERING |
                         BDB_COMMISSIONING_MODE_FINDING_BINDING);

之后,执行Build,将固件填充到芯片中,然后开始在协调器上进行配对。我在ioBroker.zigbee中检查了Zigbee网络的运行情况,这是新连接的设备的外观:



太好了,原来可以连接该设备!

7.我们使设备的操作复杂化


现在,让我们尝试对功能进行一些调整:

  • 长按按钮可将设备连接到网络。
  • 如果设备已经在网络上,则长按可从网络上显示它。
  • 短按-切换LED的状态。
  • 设备停电后启动时,LED的状态应保持不变。

为了设置自己的按钮处理,我创建了DIYRuZRT_HalKeyInit函数,类似于hal_key.c模块中的函数,但仅用于我的一组按钮。

//    ()
void DIYRuZRT_HalKeyInit( void )
{
  /*      0 */
  halKeySavedKeys = 0;

  PUSH1_SEL &= ~(PUSH1_BV); /*    - GPIO */
  PUSH1_DIR &= ~(PUSH1_BV); /*    -  */
  
  PUSH1_ICTL &= ~(PUSH1_ICTLBIT); /*      */
  PUSH1_IEN &= ~(PUSH1_IENBIT);   /*     */
  
  PUSH2_SEL &= ~(PUSH2_BV); /* Set pin function to GPIO */
  PUSH2_DIR &= ~(PUSH2_BV); /* Set pin direction to Input */
  
  PUSH2_ICTL &= ~(PUSH2_ICTLBIT); /* don't generate interrupt */
  PUSH2_IEN &= ~(PUSH2_IENBIT);   /* Clear interrupt enable bit */
}

调用此函数将添加到宏HAL_BOARD_INIT文件hal_board_cfg_DIYRuZRT.h中为避免冲突,请禁用同一文件hal_board_cfg_DIYRuZRT.h中的内置hal_key

#define HAL_KEY FALSE

因为 标准按钮阅读器已禁用,我们会自己做。
在初始化函数zclDIYRuZRT_Init中我们启动计时器以读取按钮的状态。计时器将生成我们的HAL_KEY_EVENT事件

osal_start_reload_timer( zclDIYRuZRT_TaskID, HAL_KEY_EVENT, 100);

在事件循环中,我们通过调用DIYRuZRT_HalKeyPoll函数来处理HAL_KEY_EVENT事件

//  
void DIYRuZRT_HalKeyPoll (void)
{
  uint8 keys = 0;

  //   1 ?
  if (HAL_PUSH_BUTTON1())
  {
    keys |= HAL_KEY_SW_1;
  }
  
  //   2 ?
  if (HAL_PUSH_BUTTON2())
  {
    keys |= HAL_KEY_SW_2;
  }
  
  if (keys == halKeySavedKeys)
  {
    //  -  
    return;
  }
  //        . 
  halKeySavedKeys = keys;

  //     
  OnBoard_SendKeys(keys, HAL_KEY_STATE_NORMAL);
}

将按钮的状态保存在halKeySavedKeys变量中,使我们能够确定更改的时刻-按下并释放按钮。

当您单击按钮时,启动计时器5秒钟。如果触发此计时器,则将生成DIYRuZRT_EVT_LONG事件如果释放按钮,计时器将重置。无论如何,如果您按下按钮,我们将切换LED的状态。

//   
static void zclDIYRuZRT_HandleKeys( byte shift, byte keys )
{
  if ( keys & HAL_KEY_SW_1 )
  {
    //       - 5 
    osal_start_timerEx(zclDIYRuZRT_TaskID, DIYRuZRT_EVT_LONG, 5000);
    //  
    updateRelay(RELAY_STATE == 0);
  }
  else
  {
    //     
    osal_stop_timerEx(zclDIYRuZRT_TaskID, DIYRuZRT_EVT_LONG);
  }
}

现在,在处理长按事件时,我们通过结构属性bdbAttributes.bdbNodeIsOnANetwork注意网络的当前状态。

  //  DIYRuZRT_EVT_LONG
  if ( events & DIYRuZRT_EVT_LONG )
  {
    //    
    //      ?
    if ( bdbAttributes.bdbNodeIsOnANetwork )
    {
      //  
      zclDIYRuZRT_LeaveNetwork();
    }
    else 
    {
      //    
      bdb_StartCommissioning(
        BDB_COMMISSIONING_MODE_NWK_FORMATION | 
        BDB_COMMISSIONING_MODE_NWK_STEERING | 
        BDB_COMMISSIONING_MODE_FINDING_BINDING | 
        BDB_COMMISSIONING_MODE_INITIATOR_TL
      );
      //  ,   
      osal_start_timerEx(zclDIYRuZRT_TaskID, DIYRuZRT_EVT_BLINK, 500);
    }
    
    return ( events ^ DIYRuZRT_EVT_LONG );
  }

我们走得更远。LED的状态将保存在变量中,我们将其值保存在NV存储器中。设备启动时,我们将从内存中读取该值到一个变量中。

  //  NVM   RELAY STATE
  if ( SUCCESS == osal_nv_item_init( NV_DIYRuZRT_RELAY_STATE_ID, 1, &RELAY_STATE ) ) {
    //   RELAY STATE  
    osal_nv_read( NV_DIYRuZRT_RELAY_STATE_ID, 0, 1, &RELAY_STATE );
  }
  //   
  applyRelay();

//   
void updateRelay ( bool value )
{
  if (value) {
    RELAY_STATE = 1;
  } else {
    RELAY_STATE = 0;
  }
  //   
  osal_nv_write(NV_DIYRuZRT_RELAY_STATE_ID, 0, 1, &RELAY_STATE);
  //   
  applyRelay();
}
  
//   
void applyRelay ( void )
{
  //  
  if (RELAY_STATE == 0) {
    //    1
    HalLedSet ( HAL_LED_1, HAL_LED_MODE_OFF );
  } else {
    //    1
    HalLedSet ( HAL_LED_1, HAL_LED_MODE_ON );
  }
}

8.现在我们将处理Zigbee


到目前为止,我们已经对硬件进行了分类-通过按钮,我们可以控制LED。现在我们通过Zigbee实现相同的功能。

要控制中继,对于我们来说,使用我们唯一的端点并实现GenOnOff集群就足够了我们已经阅读了GenOnOff集群的Zigbee集群库规范





足以实现OnOff属性和On,Off,Toggle命令。
首先,将指令添加到preinclude.h中

#define ZCL_ON_OFF

在属性zclDIYRuZRT_Attrs的描述中,我们添加了新的集群属性:

  // ***  On/Off  ***
  {
    ZCL_CLUSTER_ID_GEN_ON_OFF,
    { // 
      ATTRID_ON_OFF,
      ZCL_DATATYPE_BOOLEAN,
      ACCESS_CONTROL_READ,
      (void *)&RELAY_STATE
    }
  },
  {
    ZCL_CLUSTER_ID_GEN_ON_OFF,
    {  //  On/Off 
      ATTRID_CLUSTER_REVISION,
      ZCL_DATATYPE_UINT16,
      ACCESS_CONTROL_READ | ACCESS_CLIENT,
      (void *)&zclDIYRuZRT_clusterRevision_all
    }
  },

我们还将集群添加到支持的传入端点集群zclDIYRuZRT_InClusterList的列表中

要实现控制命令,请将处理程序添加到zclDIYRuZRT_CmdCallbacks

/*********************************************************************
 *    ZCL 
 */
static zclGeneral_AppCallbacks_t zclDIYRuZRT_CmdCallbacks =
{
  zclDIYRuZRT_BasicResetCB,               // Basic Cluster Reset command
  NULL,                                   // Identify Trigger Effect command
  zclDIYRuZRT_OnOffCB,                    // On/Off cluster commands
  NULL,                                   // On/Off cluster enhanced command Off with Effect
  NULL,                                   // On/Off cluster enhanced command On with Recall Global Scene
  NULL,                                   // On/Off cluster enhanced command On with Timed Off
#ifdef ZCL_LEVEL_CTRL
  NULL,                                   // Level Control Move to Level command
  NULL,                                   // Level Control Move command
  NULL,                                   // Level Control Step command
  NULL,                                   // Level Control Stop command
#endif

我们实现它:
//    OnOff
static void zclDIYRuZRT_OnOffCB(uint8 cmd)
{
  //  ,   
  //    
  afIncomingMSGPacket_t *pPtr = zcl_getRawAFMsg();
  zclDIYRuZRT_DstAddr.addr.shortAddr = pPtr->srcAddr.addr.shortAddr;
  
  // 
  if (cmd == COMMAND_ON) {
    updateRelay(TRUE);
  }
  // 
  else if (cmd == COMMAND_OFF) {
    updateRelay(FALSE);
  }
  // 
  else if (cmd == COMMAND_TOGGLE) {
    updateRelay(RELAY_STATE == 0);
  }
}

很好,现在可以通过命令切换继电器。





但这还不够。现在,如果我们使用按钮进行切换,我们还必须告知协调器LED的当前状态。

再次,添加指令:

#define ZCL_REPORTING_DEVICE

现在,创建一个发送状态消息的函数zclDIYRuZRT_ReportOnOff我们将在切换LED和设备启动时称呼它。

//    
void zclDIYRuZRT_ReportOnOff(void) {
  const uint8 NUM_ATTRIBUTES = 1;

  zclReportCmd_t *pReportCmd;

  pReportCmd = osal_mem_alloc(sizeof(zclReportCmd_t) +
                              (NUM_ATTRIBUTES * sizeof(zclReport_t)));
  if (pReportCmd != NULL) {
    pReportCmd->numAttr = NUM_ATTRIBUTES;

    pReportCmd->attrList[0].attrID = ATTRID_ON_OFF;
    pReportCmd->attrList[0].dataType = ZCL_DATATYPE_BOOLEAN;
    pReportCmd->attrList[0].attrData = (void *)(&RELAY_STATE);

    zclDIYRuZRT_DstAddr.addrMode = (afAddrMode_t)Addr16Bit;
    zclDIYRuZRT_DstAddr.addr.shortAddr = 0;
    zclDIYRuZRT_DstAddr.endPoint = 1;

    zcl_SendReportCmd(DIYRuZRT_ENDPOINT, &zclDIYRuZRT_DstAddr,
                      ZCL_CLUSTER_ID_GEN_ON_OFF, pReportCmd,
                      ZCL_FRAME_CLIENT_SERVER_DIR, false, SeqNum++);
  }

  osal_mem_free(pReportCmd);
}

现在,在日志中,我们看到有关LED状态更改的消息。

9.连接ds18b20温度传感器


传感器连接到任何空闲引脚(在我的情况下,设置为P2_1)。

将传感器轮询代码添加到应用程序。我们将定期进行面试-每分钟一次。
轮询后,网络协调员会立即将当前值通知您。

阅读ZCL规范以从温度传感器发送数据。我们需要一个
温度测量集群



,我们需要实现3个属性,其中一个代表温度值乘以100,

这里我们通过与GenOnOff集群类似的方式添加属性。我们将在开始时每分钟计划一次DIYRuZRT_REPORTING_EVT事件通知协调员。在事件处理程序中,我们将调用zclDIYRuZRT_ReportTemp,它将读取传感器的温度并发送一条消息。

//   
void zclDIYRuZRT_ReportTemp( void )
{
  //  
  zclDIYRuZRT_MeasuredValue = readTemperature();
  
  const uint8 NUM_ATTRIBUTES = 1;

  zclReportCmd_t *pReportCmd;

  pReportCmd = osal_mem_alloc(sizeof(zclReportCmd_t) +
                              (NUM_ATTRIBUTES * sizeof(zclReport_t)));
  if (pReportCmd != NULL) {
    pReportCmd->numAttr = NUM_ATTRIBUTES;

    pReportCmd->attrList[0].attrID = ATTRID_MS_TEMPERATURE_MEASURED_VALUE;
    pReportCmd->attrList[0].dataType = ZCL_DATATYPE_INT16;
    pReportCmd->attrList[0].attrData = (void *)(&zclDIYRuZRT_MeasuredValue);

    zclDIYRuZRT_DstAddr.addrMode = (afAddrMode_t)Addr16Bit;
    zclDIYRuZRT_DstAddr.addr.shortAddr = 0;
    zclDIYRuZRT_DstAddr.endPoint = 1;

    zcl_SendReportCmd(DIYRuZRT_ENDPOINT, &zclDIYRuZRT_DstAddr,
                      ZCL_CLUSTER_ID_MS_TEMPERATURE_MEASUREMENT, pReportCmd,
                      ZCL_FRAME_CLIENT_SERVER_DIR, false, SeqNum++);
  }

  osal_mem_free(pReportCmd);
}

10.将固件倒入设备


要将开发板更改为Sonoff BASICZBR3,您需要调整LED引脚和按钮的匹配。



重做引脚P0_7上的LED 1 以控制继电器。包含由高级别的ACTIVE_HIGH执行我们将S1按钮重新挂在引脚P1_3上,并将信息LED 2 P1_0上。我们将温度传感器留在引脚P2_1上。我们在hal_board_cfg_DIYRuZRT.h文件中进行所有这些更改。要选择一个配置,我们将创建一个单独的指令HAL_SONOFF。如果已设置,则将使用Sonoff BASICZBR3的设置,否则将使用devboard。

#ifdef HAL_SONOFF
  /* 1 - P0_7  */
  #define LED1_BV           BV(7)
  #define LED1_SBIT         P0_7
  #define LED1_DDR          P0DIR
  #define LED1_POLARITY     ACTIVE_HIGH

  /* 2 - P1_0  */
  #define LED2_BV           BV(0)
  #define LED2_SBIT         P1_0
  #define LED2_DDR          P1DIR
  #define LED2_POLARITY     ACTIVE_LOW
#else
  /* 1 - P1_0  */
  #define LED1_BV           BV(0)
  #define LED1_SBIT         P1_0
  #define LED1_DDR          P1DIR
  #define LED1_POLARITY     ACTIVE_LOW

  /* 2 - P1_1  */
  #define LED2_BV           BV(1)
  #define LED2_SBIT         P1_1
  #define LED2_DDR          P1DIR
  #define LED2_POLARITY     ACTIVE_LOW
#endif

必须纠正另一个重要参数-“手表”石英的存在,因为 在Sonoff BASICZBR3板上未焊接:

//#define HAL_CLOCK_CRYSTAL
  #define OSC32K_CRYSTAL_INSTALLED FALSE

没有这些选项,固件将无法启动(或者并非总是启动)。

之后,我们收集固件并连接到固件。
注意!!!在进行任何连接和固件之前,请先将Sonoff BASICZBR3继电器与交流电源断开!

我们将Sonoff BASICZBR3布线与CCDebugger连接,擦除芯片并刷新固件。



11.我们在zigbee2mqtt和ioBroker.Zigbee中启动设备


尽管该设备已经出现在已连接设备的列表中,并且可以通过发送命令进行管理,但是我们需要更正确地执行此操作-使用图片和状态。

要在ioBroker.Zigbee中获得新设备,您需要执行两个步骤:

  1. zigbee-herdsman-converters. , zigbee2mqtt.
  2. ioBroker.Zigbee

可以首先在本地文件中进行所有更改,然后在相应的存储库中进行PR。

我们在已安装的ioBroker(或zigbee2mqtt)中找到zigbee-herdsman-converters软件包的位置。在包内,我们可以找到devices.js 文件https://github.com/Koenkk/zigbee-herdsman-converters/blob/master/devices.js。

此文件包含ioBroker.zigbeezigbee2mqtt可以使用的所有设备的描述。我们在其中找到了DIYRuZ设备的描述块(在2300行之后)。在此块中添加新设备的描述:

    {
        zigbeeModel: ['DIYRuZ_RT'],
        model: 'DIYRuZ_RT',
        vendor: 'DIYRuZ',
        description: '',
        supports: 'on/off, temperature',
        fromZigbee: [fz.on_off, fz.temperature],
        toZigbee: [tz.on_off],
    },

fromZigbee属性中我们指定将处理来自设备的消息的转换器。我们的两个职位是标准化的。fz.on_off转换器处理开/关消息,而fz.temperature处理温度数据。这些转换器的代码(位于文件转换器/fromZigbee.js中)显示了如何处理传入消息以及温度除以100。

   on_off: {
        cluster: 'genOnOff',
        type: ['attributeReport', 'readResponse'],
        convert: (model, msg, publish, options, meta) => {
            if (msg.data.hasOwnProperty('onOff')) {
                const property = getProperty('state', msg, model);
                return {[property]: msg.data['onOff'] === 1 ? 'ON' : 'OFF'};
            }
        },
    },

 temperature: {
        cluster: 'msTemperatureMeasurement',
        type: ['attributeReport', 'readResponse'],
        convert: (model, msg, publish, options, meta) => {
            const temperature = parseFloat(msg.data['measuredValue']) / 100.0;
            return {temperature: calibrateAndPrecisionRoundOptions(temperature, options, 'temperature')};
        },
    },

toZigbee属性中,我们指定将处理命令到设备的转换器。在我们的例子中,它是用于切换继电器tz.on_off转换器

一切都添加到了“转换器”中。谁在使用zigbee2mqtt-您已经可以使用它。

并且ioBroker用户仍将设备的描述添加到ioBroker.zigbee \ lib \ devices.js文件中

    {
        vendor: 'DIYRuZ',
        models: ['DIYRuZ_RT'],
        icon: 'img/DIYRuZ.png',
        states: [
            states.state,
            states.temperature,
        ],
    },

在这里,足以表明完全相同的模型,带有图片的文件和状态列表。在我们的情况下,状态也是标准的:继电器状态的状态,温度显示的温度值。



12.接下来呢?


不幸的是,我无法弄清楚Z-Stack 3.0提供的所有方面和功能。我很可能甚至没有正确实现某种功能,或者可能使用一些内置机制来实现它。
因此,可以改进和开发上述解决方案。以下是一些指示:

  • 我无法快速找到通过继电器连接子设备的解决方案。其他路由器设备可以执行allow_join命令并通过自身连接新设备,而不必将新设备带到协调器。该设备由路由器代表,可以在网络图上正确显示,但是拒绝执行“ permit_join”命令。更准确地说,该命令会执行,但设备不会通过该命令进行连接。
  • 此外,未实施正确的报告。当您可以使用configReport命令指定要发送的属性列表和通知频率时,这是一种配置状态通知的方法。
  • 与小组合作。
  • 处理中断并通过中断实现轮询按钮。

好吧,对于以下设备,您需要处理电源模式并在电池上创建“休眠”设备。
与往常一样,除评论外,我邀请您在Zigbee上的Telegram聊天中讨论此设备和其他设备

我想对Telegram聊天和Zigbee社区的同事们的发展给予的支持和帮助表示感谢:


参考文献


主要文档包含在Z-Stack 3.0.2 SDK中,并随其一起安装。但我将在此处提供部分链接:


All Articles