Предполагается, что читатель уже имеет начальные знания языка C, что-то знает о Zigbee, чипе cc2530, методах его прошивания и использования, а также знаком с такими проектами, как zigbee2mqtt. Если нет — подготовьтесь или сходите почитать на https://myzigbee.ru и https://www.zigbee2mqtt.io/
Статья написана сперва подробно, но постепенно ускоряется и уже не останавливается на деталях, а описывает готовый код прошивки. Если кому-то не интересны рассуждения, то просто открывайте исходники прошивки и читайте их.
Исходный код готовой прошивки
Код и подход к разработке не претендует на идеальность. “Я не волшебник, я только учусь.”
Цель
Основная цель — разобраться, как писать прошивки под Z-Stack, давно хотел. Поэтому решил реализовать альтернативную прошивку под готовое оборудование (в качестве примера выбрано реле Sonoff BASICZBR3) и добавить возможность подключения популярного датчика температуры ds18b20.
Дополнительно хотел показать начинающим Zigbee-разработчикам пример разработки прошивки под чип TI cc2530 на Z-Stack.
1. Подготовка
Для начала разработки нужно скачать и установить Z-Stack 3.0.2 — это SDK для разработки прошивок с примерами и документацией.
Также нужно скачать и установить IAR Embeded Workbench for 8051 — это среда разработки с возможностью компиляции под чипы 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 — рабочая область Workspace
Переименовываем файлы в DIYRuZRT.eww и DIYRuZRT.ewp.
Внутри всех файлов (в том числе и в папке Source) также меняем все упоминания GenericApp на DIYRuZRT.
Теперь открываем проект DIYRuZRT.ewp в IAR. Выбираем конфигурацию RouterEB и выполняем Rebuild All.
В папке CC2530DB создастся папка RouterEB, а внутри, в папке EXE, появится файл DIYRuZRT.d51 — этот файл удобен для прошивки и отладки из IAR.
Но если нам надо прошить прошивку через SmartRF Flash Programmer, то сделаем небольшие изменения. Для этого в настройках проекта в разделе Link на вкладке Output поменяем настройки Output file и Format:
После этого в папке EXE будет создаваться файл прошивки DIYRuZRT.hex удобный для прошивания из других инструментов и другими способами.
Но после заливки этой прошивки устройство не подключается к сети. Что ж, будем разбираться.
3. Немного терминологии
В терминологии Zigbee есть следующие понятия:
- Endpoint (эндпоинт) — точка описания конечного устройства. Обычно в простых устройствах один эндпоинт. В многофункциональных устройствах их может быть несколько, также как в устройствах с разными профилями взаимодействия (один профиль — один эндпоинт).
- Cluster (кластер) — набор атрибутов и команд, относящихся к единому функционалу (вкл/выкл, регулирование освещения, температурные измерения и т.п.). Кластер указывает на возможности, реализуемые эндпоинтом. В одном эндпоинте можно реализовать несколько разных кластеров, но не одинаковых.
- Attribute (атрибут) — характеристика кластера, значение которого можно прочитать или записать. В кластере может быть множество атрибутов.
- Command (команда) — управляющее сообщение, которое может обработать кластер. У команды могут быть параметры. Это реализуется функцией, которая выполняется при при получении команды и параметров.
Виды кластеров, атрибутов, команд стандартизованы в Zigbee Cluster Library. Но производители могут применять собственные кластеры, со своими атрибутами и командами.
Некоторые горе-производители наплевательски относятся к стандартам и делают что-то около стандарта. Потом под них приходится подстраиваться.
В терминологии Z-Stack тоже есть свои понятия, например:
- OSAL (Operating System Abstraction Layer) — уровень абстракции Операционной системы. Здесь оперируют задачами (tasks), сообщениями (messages), событиями (events), таймерами (timers) и другими объектами.
- HAL (Hardware Abstraction Layer) — уровень абстракции оборудования. Здесь оперируют кнопками (keys), светодиодами (leds), прерываниями (Interrupt) и т.п.
Аппаратный уровень обеспечивает изоляцию программного кода и оборудования, которым он управляет. Операционный уровень предоставляет механизмы построения и взаимодействия между элементами приложения.
Использование этого всего вас ждет ниже и в принципе при разработке прошивок.
4. Что же у нас внутри базового приложения?
Код приложения расположен в папке Source:
- OSAL_DIYRuZRT.c — основной файл инициализирующий приложение
- zcl_DIYRuZRT.h — заголовочный файл
- zcl_DIYRuZRT.c — файл реализации функций
- zcl_DIYRuZRT_data.c — файл констант, переменных и структур
OSAL_DIYRuZRT.c — основной файл, в котором заполняется массив обработчиков задач (task) pTaskEventHandlerFn tasksArr и реализуется функция их инициализации osalInitTasks.
Все остальные файлы нужны для реализации этих инициализаторов и обработчиков.
Список обработчиков задач pTaskEventHandlerFn tasksArr заполняется ссылками на функции. Часть задач подключаются/отключаются соответствующими директивами компиляции.
Посмотреть и настроить директивы компиляции можно в опциях компилятора Defined symbols:
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 — стартовая функция приложения, которая регистрирует задачи, реализуемые приложением.
Регистрация задач выполняется по порядку, и каждая задача получает свой собственный номер. Важно соблюсти тот же порядок, что и в массиве tasksArr, т.к. обработчики вызываются в соответствие с номером задачи.
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_RegisterCmdCallbacks — регистрация таблицы обработчиков команд эндпоинта zclGeneral_AppCallbacks_t zclDIYRuZRT_CmdCallbacks — это структура, где для каждой команды надо указать обработчик.
- zcl_registerAttrList — регистрация атрибутов эндпоинта zclAttrRec_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, которое переключает состояние светодиода HAL_LED_2 и запускает таймер на 500м с таким же событием. Тем самым запускается мигание светодиода 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, то мигание остановится (у меня светодиод остается включенным). Повторное нажатие снова запустит мигание.
5. HAL: светодиоды и кнопки
«Погодите, какой светодиод и кнопки?», — спросите вы. Изначально, все примеры в Z-stack ориентированы на различного рода отладочные платы серии SmartRF05 EB:
У меня немного другая плата для отладки и модуль с чипом.
На плате есть 2 кнопки (+ ресет) и 3 светодиода (+ индикатор питания). Вот один из них (D2) мигает при корректной работе прошивки.
Прозвонив контакты, определяем соответствие пинов, диодов и кнопок:
- D1 — P10
- D2 — P11
- D3 — P14
- S2 — P20
- S1 — P01
Так вот, HAL — это Hardware Abstraction Layer, способ абстрагироваться от реализации оборудования. В коде приложения используются макросы и функции, которые работают с абстракциями типа Кнопка 1 или Светодиод 2, а конкретное соответствие абстракций и оборудования задается отдельно.
Разберемся что за HAL_LED_2 и как понять, на какой пин он подвешен.
Поиском находим файл hal_led.h, где описаны эти константы и функция HalLedSet, куда передается номер светодиода и режим. Внутри вызывается функция HalLedOnOff для включения и выключения светодиода, которая в свою очередь выполняет либо 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:
/* 2 - Red */
#define LED2_BV BV(1)
#define LED2_SBIT P1_1
#define LED2_DDR P1DIR
#define LED2_POLARITY ACTIVE_HIGH
Это означает, что светодиод 2 у нас располагается на пине P1_1 и его уровень включения — высокий. Но, судя по коду, светодиод должен был погаснуть при нажатии на кнопку, а у нас он остается гореть. Если в этом файле hal_board_cfg.h поменяем:
#define LED2_POLARITY ACTIVE_HIGH
на
#define LED2_POLARITY ACTIVE_LOW
то теперь светодиод гаснет при нажатии на кнопку S2, как и должно быть по логике.
Чтобы не менять общие файлы, не относящиеся к нашему приложению, лучше сделать иначе:
- создадим копию файла hal_board_cfg.h (из папки Z-Stack 3.0.2\Components\hal\target\CC2530EB\) в нашу папку Source и назовём его например 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 я перенес директивы компилятора и удалил их в Options компилятора:
#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 и Joystick Center Press:
/* 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 и других будет подставлен P1DIR — это регистр порта P1 , отвечающий за режим работы пинов (вход или выход). Соответственно LED2_BV — это установка в значения 1 в бит соответствующего пина (в нашем случае в 1й бит, что соответствует пину P1_1):
Регистры и режимы процессора описаны в документации
«cc253x User's Guide»
Но нигде не видно, как настраиваются кнопки. Кнопки обрабатываются аналогично, но в другом файле — hal_key.c. В нем определены параметры работы кнопок и функции HalKeyInit, HalKeyConfig, HalKeyRead, HalKeyPoll. Эти функции отвечают за инициализацию подсистемы работы с кнопками и считывания значений.
По-умолчанию обработка кнопок выполняется по таймеру, каждые 100мс. Пин P2_0 для текущей конфигурации назначен на джойстик и его текущее состояние считывается как нажатие — поэтому запускается таймер мигания светодиодом.
6. Настраиваем устройство под себя
Поменяем в файле zcl_DIYRuZRT.h:
- DIYRuZRT_ENDPOINT на 1
в файле OSAL_DIYRuZRT_data.c:
- DIYRuZRT_DEVICE_VERSION на 1
- zclDIYRuZRT_ManufacturerName на { 6, 'D','I','Y','R','u','Z' }
- zclDIYRuZRT_ModelId на { 9, 'D','I','Y','R','u','Z','_','R','T' }
- zclDIYRuZRT_DateCode на { 8, '2','0','2','0','0','4','0','5' }
Для того, чтобы устройство могло подключаться к сети на любом канале (по умолчанию только на 11, указан в директиве DEFAULT_CHANLIST в файле Tools\f8wConfig.cfg), надо указать эту возможность в файле 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, заливаем прошивку в чип и запускаем спаривание на координаторе. Я проверяю работу Zigbee-сети в ioBroker.zigbee, вот так выглядит новое подключенное устройство:
Отлично, получилось подключить устройство!
7. Усложняем работу устройства
Теперь попробуем немного адаптировать функционал:
- Процесс подключения устройства к сети сделаем по долгому нажатию на кнопку.
- Если устройство уже было в сети, то долгое нажатие выводит его из сети.
- Короткое нажатие — переключает состояние светодиода.
- Состояние светодиода должно сохраняться при запуске устройства после пропадания питания.
Для настройки собственной обработки кнопок я создал функцию 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_key в том же файле hal_board_cfg_DIYRuZRT.h:
#define HAL_KEY FALSE
Т.к. отключен стандартный обработчик считывания кнопок, то будем это делать сами.
В функции инициализации zclDIYRuZRT_Init запустим таймер на считывание состояний кнопок. Таймер будет генерировать наше событие HAL_KEY_EVENT.
osal_start_reload_timer( zclDIYRuZRT_TaskID, HAL_KEY_EVENT, 100);
А в цикле обработки событий обработаем событие HAL_KEY_EVENT вызвав функцию DIYRuZRT_HalKeyPoll:
// Считывание кнопок
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. Если кнопку отпускают, то таймер сбрасывается. В любом случае, если нажимают кнопку — переключаем состояние светодиода.
// Обработчик нажатий клавиш
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 );
}
Идем далее. Состояние светодиода сохраним в переменной, значение которой будем сохранять в 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
С аппаратной частью пока разобрались — кнопкой управляем светодиодом. Теперь реализуем это же через Zigbee.
Для управления реле нам достаточно использовать наш единственный эндпоинт и реализовать кластер GenOnOff. Прочитаем спецификацию Zigbee Cluster Library для кластера GenOnOff:
Достаточно реализовать атрибут 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);
}
}
Отлично, теперь реле можно переключать командами.
Но этого мало. Теперь мы должны также информировать координатор о текущем состоянии светодиода, если мы переключаем его кнопкой.
Опять же, добавим директиву:
#define ZCL_REPORTING_DEVICE
Теперь создадим функцию zclDIYRuZRT_ReportOnOff , отправляющую сообщение о состоянии. Будем вызывать ее при переключении светодиода и при старте устройства.
// Информирование о состоянии реле
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);
}
Теперь в логах видим сообщения об изменении состояния светодиода.
9. Подключаем датчик температуры ds18b20
Подключается датчик на любой свободный пин (в моем случае поставил P2_1).
Добавляем в приложение код опроса датчика. Опрашивать будем регулярно — раз в минуту.
Сразу при опросе будет оповещать координатор сети о текущем значении.
Прочитаем спецификации ZCL по отправке данных с датчиков температуры. Нам нужен кластер
Temperature Measurement
Видим что нужно реализовать 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. Заливаем прошивку в устройство
Для смены devboard на Sonoff BASICZBR3 необходимо скорректировать соответствие пинов светодиодов и кнопки.
Переделаем светодиод 1 на пин P0_7, чтобы управлять реле. Включение осуществляется высоким уровнем ACTIVE_HIGH. Кнопку S1 перевешиваем на пин P1_3, а информационный светодиод 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, нужно выполнить 2 шага:
- Добавить описание устройства в пакет zigbee-herdsman-converters. Выполнение этого шага будет достаточно для того, чтобы заставить работать устройство в проекте zigbee2mqtt.
- Добавить описание устройства в ioBroker.Zigbee
Все изменения можно сперва выполнить в локальных файлах, а затем сделать PR в соответствующие репозитории.
Находим расположение пакета zigbee-herdsman-converters в установленном ioBroker (или zigbee2mqtt). Внутри пакета находим файл 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 — данные о температуре. В коде этих конвертеров (располагаются в файле converters/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,
],
},
Здесь достаточно указать точно такую же модель, файл с картинкой и перечень состояний. В нашем случае состояния также стандартные: state для состояния реле, temperature для отображения значений температуры.
12. Что дальше?
К сожалению, я не смог разобраться со всеми аспектами и возможностями, которые предоставляет Z-Stack 3.0. Скорее всего я даже не корректно реализовал какой-то функционал или для его реализации можно было использовать какие-то встроенные механизмы.
Поэтому, приведенное решение можно улучшать и развивать. Вот некоторые направления:
- Я не смог быстро найти решения по возможности подключения дочерних устройств через реле. Другие устройства-роутеры могут выполнять команду “permit_join” и подключать новые устройства через себя, без необходимости подносить новое устройство к координатору. Устройство представляется роутером, корректно отображается на карте сети, но выполнять команду “permit_join”, отказывается. Точнее, команду выполняет, но устройства не подключаются через него.
- Также, не реализовал корректный reporting. Это способ настройки уведомления о состоянии, когда можно командой configReport указать перечень атрибутов для отправки и частоту уведомления.
- Работа с группами.
- Разобраться с прерываниями и реализовать опрос кнопки через прерывания.
Ну а для следующих устройств нужно разбираться с режимами питания и созданием “спящих” устройств на батарейках.
Как всегда, помимо комментариев, приглашаю обсуждать это и другие устройства в Телеграм-чатике по Zigbee.
Хочу выразить благодарность за поддержку и помощь в разработке своим коллегам по Телеграм-чату и Zigbee сообществу:
Ссылки
Основная документация включена в SDK Z-Stack 3.0.2 и устанавливается вместе с ним. Но часть ссылок приведу тут:
- OSAL и HAL
- TI ZIgbee Wiki
- Create New Application For SmartRF05 + CC2530 SWRA231 Version 1.1
- Z-Stack Home Sample Application User's Guide SWRU354 Version 1.3
- Z-Stack Sample Applications SWRA201 Version 1.6
- Z-Stack Developer's Guide SWRA176 Version 1.12
- Z-Stack 3.0 Developer's Guide Version 1.14
- Z-Stack API SWRA195 Version 1.10
- TI CC253x User's Guide
- https://github.com/formtapez/ZigUP
- Zigbee Cluster Library rev 6
- Исходные тексты прошивки DIYRuZ_RT