假定读者已经具有C语言的初步知识,对Zigbee,cc2530芯片,闪存的使用方法以及使用它的知识有所了解,并且还熟悉诸如zigbee2mqtt之类的项目。如果不是这样,请准备或在https://myzigbee.ru和https://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
#if defined ( INTER_PAN )
StubAPS_ProcessEvent,
#endif
#if defined ( BDB_TL_INITIATOR )
touchLinkInitiator_event_loop,
#endif
#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
#if defined ( INTER_PAN )
StubAPS_Init( taskID++ );
#endif
#if defined( BDB_TL_INITIATOR )
touchLinkInitiator_Init( taskID++ );
#endif
#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_RegisterCmdCallbacks — zclGeneral_AppCallbacks_t zclDIYRuZRT_CmdCallbacks — , .
- zcl_registerAttrList — zclAttrRec_t zclDIYRuZRT_Attrs — , .
- zcl_registerForMsg-注册接收控制消息。
- RegisterForKeys-我们签署了接收按钮单击事件的任务。
const cId_t zclDIYRuZRT_InClusterList[] =
{
ZCL_CLUSTER_ID_GEN_BASIC,
ZCL_CLUSTER_ID_GEN_IDENTIFY,
};
#define ZCLDIYRuZRT_MAX_INCLUSTERS (sizeof(zclDIYRuZRT_InClusterList) / sizeof(zclDIYRuZRT_InClusterList[0]))
const cId_t zclDIYRuZRT_OutClusterList[] =
{
ZCL_CLUSTER_ID_GEN_BASIC,
};
#define ZCLDIYRuZRT_MAX_OUTCLUSTERS (sizeof(zclDIYRuZRT_OutClusterList) / sizeof(zclDIYRuZRT_OutClusterList[0]))
SimpleDescriptionFormat_t zclDIYRuZRT_SimpleDesc =
{
DIYRuZRT_ENDPOINT,
ZCL_HA_PROFILE_ID,
ZCL_HA_DEVICEID_ON_OFF_LIGHT,
DIYRuZRT_DEVICE_VERSION,
DIYRuZRT_FLAGS,
ZCLDIYRuZRT_MAX_INCLUSTERS,
(cId_t *)zclDIYRuZRT_InClusterList,
ZCLDIYRuZRT_MAX_OUTCLUSTERS,
(cId_t *)zclDIYRuZRT_OutClusterList
};
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:
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);
if ( (zclDIYRuZRT_NwkState == DEV_ZB_COORD) ||
(zclDIYRuZRT_NwkState == DEV_ROUTER) ||
(zclDIYRuZRT_NwkState == DEV_END_DEVICE) )
{
giGenAppScreenMode = GENERIC_MAINMODE;
zclDIYRuZRT_LcdDisplayUpdate();
}
break;
default:
break;
}
osal_msg_deallocate( (uint8 *)MSGpkt );
}
接下来是特殊事件DIYRuZRT_EVT_1的处理,该事件将切换LED HAL_LED_2的状态,并为同一事件启动500m的计时器。这将开始闪烁LED HAL_LED_2。 if ( events & DIYRuZRT_EVT_1 )
{
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 1或LED 2)配合使用的宏和函数,并且抽象和设备的特定对应关系是单独设置的。让我们弄清楚什么是HAL_LED_2,以及如何了解它挂在哪个引脚上。通过搜索,我们找到了hal_led.h文件,其中描述了这些常量和HalLedSet函数,并在其中传输了LED编号和模式。在内部,调用HalLedOnOff函数来打开和关闭LED,后者依次执行HAL_TURN_ON_LED2或HAL_TURN_OFF_LED2。HAL_TURN_ON_LED2和HAL_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_SBIT和LED2_POLARITY的对应关系:
#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按钮和操纵杆中心按的说明:
#define PUSH1_BV BV(1)
#define PUSH1_SBIT P0_1
#define PUSH2_BV BV(0)
#define PUSH2_SBIT P2_0
#define PUSH2_POLARITY ACTIVE_HIGH
这对应于板上按钮的引脚。让我们看一下硬件初始化- 同一文件中的HAL_BOARD_INIT宏。默认情况下,HAL_BOARD_CC2530EB_REV17指令处于打开状态,因此我们看一下相应的宏变体。
#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; \
while (!(SLEEPSTA & XOSC_STB)); \
asm("NOP"); \
for (i=0; i<504; i++) asm("NOP"); \
CLKCONCMD = (CLKCONCMD_32MHZ | OSC_32KHZ); \
while (CLKCONSTA != (CLKCONCMD_32MHZ | OSC_32KHZ)); \
SLEEPCMD |= OSC_PD; \
\
\
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(); \
\
\
P0INP |= PUSH2_BV; \
}
在此宏中,处理器的模式和寄存器被初始化。取而代之的是将LED2_DDR和其他LED2_DDR替换为P1DIR-将该端口P1注册,充电操作模式引脚(输入或输出)。相应地,LED2_BV设置为相应引脚的每位1(在我们的情况下为1位,对应于P1_1引脚):
寄存器和处理器模式在“ cc253x用户指南”的文档中进行了描述,但是在何处看不到按钮的配置方式。按钮的处理方式类似,但是在另一个文件hal_key.c中。它定义按钮的参数和功能HalKeyInit,HalKeyConfig,HalKeyRead,HalKeyPoll。这些功能负责初始化使用按钮和读取值的子系统。缺省情况下,定时器每100ms进行一次按钮处理。当前配置的引脚P2_0被分配到操纵杆,并且单击即可读取其当前状态-因此,LED闪烁计时器启动。6.我们为自己配置设备
在文件zcl_DIYRuZRT.h中进行更改:在文件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 DISABLE_GREENPOWER_BASIC_PROXY
#define DEFAULT_CHANLIST 0x07FFF800
为了使我们的设备自动尝试连接到网络,我们将在功能代码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 )
{
halKeySavedKeys = 0;
PUSH1_SEL &= ~(PUSH1_BV);
PUSH1_DIR &= ~(PUSH1_BV);
PUSH1_ICTL &= ~(PUSH1_ICTLBIT);
PUSH1_IEN &= ~(PUSH1_IENBIT);
PUSH2_SEL &= ~(PUSH2_BV);
PUSH2_DIR &= ~(PUSH2_BV);
PUSH2_ICTL &= ~(PUSH2_ICTLBIT);
PUSH2_IEN &= ~(PUSH2_IENBIT);
}
调用此函数将添加到宏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;
if (HAL_PUSH_BUTTON1())
{
keys |= HAL_KEY_SW_1;
}
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 )
{
osal_start_timerEx(zclDIYRuZRT_TaskID, DIYRuZRT_EVT_LONG, 5000);
updateRelay(RELAY_STATE == 0);
}
else
{
osal_stop_timerEx(zclDIYRuZRT_TaskID, DIYRuZRT_EVT_LONG);
}
}
现在,在处理长按事件时,我们通过结构属性bdbAttributes.bdbNodeIsOnANetwork注意网络的当前状态。
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存储器中。设备启动时,我们将从内存中读取该值到一个变量中。
if ( SUCCESS == osal_nv_item_init( NV_DIYRuZRT_RELAY_STATE_ID, 1, &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) {
HalLedSet ( HAL_LED_1, HAL_LED_MODE_OFF );
} else {
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的描述中,我们添加了新的集群属性:
{
ZCL_CLUSTER_ID_GEN_ON_OFF,
{
ATTRID_ON_OFF,
ZCL_DATATYPE_BOOLEAN,
ACCESS_CONTROL_READ,
(void *)&RELAY_STATE
}
},
{
ZCL_CLUSTER_ID_GEN_ON_OFF,
{
ATTRID_CLUSTER_REVISION,
ZCL_DATATYPE_UINT16,
ACCESS_CONTROL_READ | ACCESS_CLIENT,
(void *)&zclDIYRuZRT_clusterRevision_all
}
},
我们还将集群添加到支持的传入端点集群zclDIYRuZRT_InClusterList的列表中。要实现控制命令,请将处理程序添加到zclDIYRuZRT_CmdCallbacks表。
static zclGeneral_AppCallbacks_t zclDIYRuZRT_CmdCallbacks =
{
zclDIYRuZRT_BasicResetCB,
NULL,
zclDIYRuZRT_OnOffCB,
NULL,
NULL,
NULL,
#ifdef ZCL_LEVEL_CTRL
NULL,
NULL,
NULL,
NULL,
#endif
我们实现它:
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
#define LED1_BV BV(7)
#define LED1_SBIT P0_7
#define LED1_DDR P0DIR
#define LED1_POLARITY ACTIVE_HIGH
#define LED2_BV BV(0)
#define LED2_SBIT P1_0
#define LED2_DDR P1DIR
#define LED2_POLARITY ACTIVE_LOW
#else
#define LED1_BV BV(0)
#define LED1_SBIT P1_0
#define LED1_DDR P1DIR
#define LED1_POLARITY ACTIVE_LOW
#define LED2_BV BV(1)
#define LED2_SBIT P1_1
#define LED2_DDR P1DIR
#define LED2_POLARITY ACTIVE_LOW
#endif
必须纠正另一个重要参数-“手表”石英的存在,因为 在Sonoff BASICZBR3板上未焊接:
#define OSC32K_CRYSTAL_INSTALLED FALSE
没有这些选项,固件将无法启动(或者并非总是启动)。之后,我们收集固件并连接到固件。注意!!!在进行任何连接和固件之前,请先将Sonoff BASICZBR3继电器与交流电源断开!
我们将Sonoff BASICZBR3布线与CCDebugger连接,擦除芯片并刷新固件。
11.我们在zigbee2mqtt和ioBroker.Zigbee中启动设备
尽管该设备已经出现在已连接设备的列表中,并且可以通过发送命令进行管理,但是我们需要更正确地执行此操作-使用图片和状态。要在ioBroker.Zigbee中获得新设备,您需要执行两个步骤:- zigbee-herdsman-converters. , zigbee2mqtt.
- ioBroker.Zigbee
可以首先在本地文件中进行所有更改,然后在相应的存储库中进行PR。我们在已安装的ioBroker(或zigbee2mqtt)中找到zigbee-herdsman-converters软件包的位置。在包内,我们可以找到devices.js 文件https://github.com/Koenkk/zigbee-herdsman-converters/blob/master/devices.js。此文件包含ioBroker.zigbee和zigbee2mqtt可以使用的所有设备的描述。我们在其中找到了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中,并随其一起安装。但我将在此处提供部分链接: