Пишем прошивку под TI cc2530 на Z-Stack 3.0 для Zigbee реле Sonoff BASICZBR3 с датчиком ds18b20

от автора

Предполагается, что читатель уже имеет начальные знания языка C, что-то знает о Zigbee, чипе cc2530, методах его прошивания и использования, а также знаком с такими проектами, как zigbee2mqtt. Если нет — подготовьтесь или сходите почитать на https://myzigbee.ru и https://www.zigbee2mqtt.io/
Статья написана сперва подробно, но постепенно ускоряется и уже не останавливается на деталях, а описывает готовый код прошивки. Если кому-то не интересны рассуждения, то просто открывайте исходники прошивки и читайте их.

Исходный код готовой прошивки github.com/diyruz/diyruz_rt
Код и подход к разработке не претендует на идеальность. “Я не волшебник, я только учусь.”

Цель

Основная цель — разобраться, как писать прошивки под 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 шага:

  1. Добавить описание устройства в пакет zigbee-herdsman-converters. Выполнение этого шага будет достаточно для того, чтобы заставить работать устройство в проекте zigbee2mqtt.
  2. Добавить описание устройства в 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 и устанавливается вместе с ним. Но часть ссылок приведу тут:

ссылка на оригинал статьи https://habr.com/ru/company/iobroker/blog/495926/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *