[Децентрализованные сети, Программирование микроконтроллеров, Умный дом, DIY или Сделай сам] Hello NXP Zigbee World

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
06-Апр-2021 16:31


И снова здравствуйте. Эта статья является продолжениемстатьи Hello NXP JN5169 World. Если в прошлый раз мы постигали основы микроконтроллера JN5169, его периферию, очереди и таймеры, то сегодня будем постигать основную фишку этого микроконтроллера - работу в ZigBee сетях. Статья является вдумчивым перевариванием документации от NXP, а также примеров кода, которые поставляются вместе с NXP ZigBee SDK.Как и в прошлый раз я буду строить приложение с нуля, понемногу добавляя в него функционал (а заодно и разбираясь в этом коде). Цель - построить работоспособное приложение с минимальным количеством кода, которое реализует одноклавишный умный выключатель с управлением по ZigBee.Готовы погрузиться в мир ZigBee?Немного теорииКогда только знакомишься с ZigBee то очень быстро получаешь следующий набор знаний
  • В сети ZigBee есть 3 типа устройств - координатор, роутеры, и конечные устройства
    • В сети ZigBee может быть только один координатор
    • Конечные устройства могут только отправлять или принимать данные к/от роутеров, не являясь транзитными узлами
    • Роутеры и координатор обеспечивают транзитную передачу данных между разными узлами сети
    • Как правило устройства с постоянным питанием являются роутерами, а батарейные устройства - конечными устройствами
    • Сеть ZigBee может быть mesh сетью, где несколько устройств могут обмениваться данными напрямую минуя координатор
  • Сеть ZigBee достаточно стандартизирована. 
    • Существует некоторое количество открытых проектов (zigbee2mqtt, SLS, sprut), которые позволяют объединять устройства разных производителей в одну сеть
    • Теоретически производители могут сделать закрытую экосистему где никто другой не сможет подключиться к сети устройств этого производителя (просто добавив ключ шифрования)
    • Устройства со схожим функционалом (выключатели, лампочки, терморегуляторы) теоретически должны работать одинаково у всех производителей
    • Разные производители по разному интерпретируют стандарт, из-за чего не все устройства могут правильно работать.
    • У ZigBee нет обязательной сертификации, как у Z-Wave. Из-за этого устройства стоят дешевле, но правильность их работы в неродных сетях производителя никто не гарантирует (в родных, впрочем, тоже)

Картинка из документа ZigBee 3.0 Stack User Guide JN-UG-3113 показывает пример ZigBee сети с разными типами устройств.В принципе этого набора знаний вполне достаточно для рядового потребителя, в т.ч. продвинутого, который сам себе строит систему умного дома. Накоплена большая база знаний о возможностях, нюансах, особенностях, и глюках тех или иных моделей устройств. Вот только информации как сделать свое устройство почему-то очень мало. Из-за этого порог вхождения в технологию достаточно высок - нужно перечитать целую кучу документации и кода примеров, прежде чем начинаешь что-то вообще понимать.Попробую простыми словами объяснить как это все работает. 
Разберем эту картинку снизу вверх. 
  • ZigBee построен на базе стандарта IEEE 802.15.4. Часть PHY описывает физический уровень сети - как передаются биты и байты через радиоканал. Микроконтроллер сам будет заниматься мониторингом радио среды, и передавать данные когда радиоканал свободен. Все это происходит без нашего участия.
  • IEEE 802.15.4 MAC уровень занимается передачей пакетов с данными между отдельно взятыми узлами сети, а также отсылает обратно подтверждение о правильной доставке данных. В принципе на этот уровень нам лезть не придется, разве что какая нибудь ошибка оттуда прилетит. Для нас это просто транспорт - отправь вот эти байты (не больше 100 за раз) вон тому устройству.
  • ZigBee Network (NWK) Layer собственно обслуживает сеть ZigBee - раздает сетевые адреса устройствам, строит маршруты как передать пакет данных между не связанными напрямую узлами, позволяет отправлять данные различными способами (unicast или broadcast). Сюда мы тоже особо лезть не будем, но нам будут интересны некоторые события, которые происходят на этом уровне.
  • Для наc самым интересным будет уровень приложения - Application Layer (APL). Этот уровень организовывает взаимодействие устройства с сетью Zigbee, описывает поведение устройства в целом, а также реализует отдельные его функции. Именно с этой частью придется иметь дело.
Попробую описать Application Layer и его компоненты своими словами.
  • Полезные функции живут в блоке Application Framework (AF), и если конкретнее, реализуются в штуках, которые называются конечными точками (End Point). Конечная точка - это логическое объединение умений, команд и параметров, которые описывают одну функцию устройства. Пока непонятно, правда?Давайте на примерах
    • Вы делаете многоканальное реле с управлением по ZigBee. Так вот каждый канал в этом устройстве будет отдельным end point’ом - каждое реле будет уметь включаться и выключаться независимо, и, вероятно, измерять какие-нибудь параметры, вроде тока или мощности. А все конечные точки все вместе уже определяют устройство целиком.
    • Или вот ночник с функцией термометра. Ночник и термометр - это совершенно отдельные сущности, каждая из которых может управляться совершенно отдельно, а значит реализуются разными конечными точками.
    • Двухклавишный умный выключатель также реализуется двумя конечными точками, каждая из которых управляется своей клавишей выключателя и управляет своим реле независимо.
    Конечные точки нужны для идентификации какая именно часть устройства была источником сообщения, или какой части целевого устройства адресуется команда. А еще конечные точки разных устройств можно связывать (bind) напрямую. Так например одна из клавиш выключателя может переключать отдельную релюшку в многоканальном реле из примера выше. И все это без участия координатора.
  • ZigBee Кластер это стандартизированный набор умений, сгруппированный по функциональному признаку. Кластер группирует несколько атрибутов и доступных команд, которые логически связаны с функциональностью кластера. Так у светильника атрибутами могут быть яркость, цветовая температура, или RGB цвет, а у климатического датчика атрибутами будут температура, влажность и давление. Командами у светильника будут включить/выключить, или задать цветовой режим, а у климатического датчика команд не будет вовсе - он же датчик, а не исполнительное устройство.Один End Point может состоять из нескольких кластеров, но эти кластеры в одной конечной точке не должны повторяются. Если нужно устройство с несколькими одинаковыми кластерами, то их нужно разнести по разным конечным точкам (именно поэтому двухклавишный выключатель реализован как минимум двумя конечными точками)И хотя устройство, в принципе, может реализовывать какой угодно функционал, разработчики ZigBee подметили, что некоторые виды функциональности повторяются в разных устройствах и реализованы схожим образом. Многие устройства реализуют что нибудь вроде выключателя, термометра, измерителя мощности, светильника, датчика движения, терморегулятора, или чего нибудь еще. Каждое из таких видов функциональности назвали кластером, стандартизировали, и детально описали в спецификации. Разработчики ZigBee стека пошли дальше и реализовали для нас целый API по работе с каждым кластером, и нарекли это ZigBee Cluster Library. Таким образом нам не придется самостоятельно реализовывать работу с низкоуровневыми пакетами, отсылающими температуру или яркость свечения лампочки. Вместо этого мы будем использовать довольно высокоуровневое API.Каждый кластер может реализовывать и клиент и сервер (или один из них). Например беспроводная кнопка будет только клиентом в кластере On/Off - у нее нет исполнительной части, и она может отправлять только сигнал по нажатию кнопки. При этом ZigBee реле также будет реализовывать On/Off кластер но будет только сервером - будет исполнять команды на включение и выключение, но никак не сможет эти команды генерировать. Разумеется бывают устройства, которые реализуют и клиент и сервер (например выключатель Xiaomi, у которого есть и клавиши, и реле)
  • ZigBee Device Object (ZDO), Base Class Behavior (BDB), и Application Support Sublayer (APS) - исходя из документации и кода я не смог для себя четко прояснить чем занимается каждая из частей по отдельности. Впрочем, это нам и не понадобится. Достаточно отметить, что все втроем они обеспечивает общую функциональность устройства как узла сети ZigBee. Эти штуки реализуют протокол присоединения устройства к сети, а также переподключения в случае потери связи. Тут же реализуется функциональность устройства как роутера, конечного устройства, или координатора. Тут также живет функциональность прямого соединения устройств (binding).Поскольку эта часть общается с другими узлами в сети (например, координатор может запросить у устройства его название, список умений и характеристик), для ZDO выделили отдельную конечную точку номер 0, и такая точка реализуется всеми устройствами в сети.
С архитектурой разобрались. Но прежде чем мы пойдем дальше, я должен упомянуть еще об одной важной вещи, которая существенным образом влияет на архитектуру прошивки - все это жутко асинхронное, и основными действующими лицами в этой системе являются события и сообщения.В школе нас учили, что алгоритм это четкая инструкция, которая описывает последовательностью действий для некоего исполнителя. И действительно большинство программ (включая даже некоторые из тех, которые занимаются сетевым взаимодействием) действительно реализованы неким потоком инструкций: сделай А, сделай Б, если верно В - повторить еще раз. В ZigBee все не так. Узлы между собой общаются короткими сообщениями. Если узел не может достучаться до получателя напрямую, сообщение будет доставлено через несколько промежуточных узлов. На каждом этапе пакет будет подтверждаться принимающей стороной на уровне IEEE 802.15.4 MAC. Конечный получатель может отправить ответ обратным путем, где на каждом этапе пакет также будет подтверждаться. Теперь добавим к этому то, что радиоэфир не очень надежная среда передачи и какие-то пакеты могут попросту не дойти. Тогда после некоторого таймаута нужно организовать повторную передачу, а возможно даже поиск нового маршрута. А пока отправитель ждет ответа может произойти еще миллион других вещей в сети - какие-то узлы будут общаться между собой, какой-то узел захочет присоединиться к сети, координатор может прийти с каким-то вопросом, или соседнее конечное устройство проснется и будет через нас передавать данные. В общем с точки зрения “линейного” программиста все это превращается в жуткий хаос. 
Пример коммуникации в ZigBee сети. Черным цветом обозначен путь некоторого запроса и соответствующего ответа. Серым цветом обозначены сообщения не относящиеся к этому запросуНо не все так плохо. NXP предоставляет нам целую кучу кода, который все это упорядочит и превратит в некий поток событий, на которые нам нужно будет написать обработчики. Пришло входящее сообщение - обработали, отвалилась сеть - переподключились, нужно отправить какие-то важные данные - выплюнули их в сеть и запустили таймер ожидания обратного ответа. Последний пункт теоретического ликбеза - как именно строятся пакетики данных. Дело вот в чем. Если нам нужно передать байты по сети, мы не можем просто взять их и отправить в радиоканал - приемники не разберутся что это за пекет, кому он адресован, нужно ли его обрабатывать, или предполагается, что его нужно по цепочке передать кому-то другому. Поэтому умные люди придумали модель OSI и каждому уровню дали свое назначение. Каждый уровень имеет свой формат пакета и добавляет к пакету свои служебные данные. Давайте взглянем на картинку.
Картинка из документа JN51xx Core Utilities User Guide JN-UG-3116Предположим приложение хочет отправить блок данных. Оно передает этот блок следующему (сетевому) уровню, который добавляет к пакету заголовок, в котором описано от какого устройства сети ZigBee и кому адресуется пакет. Конечный получатель на самом деле может не совпадать с промежуточным узлом сети, которому на самом деле сейчас будет отправлен пакет, поэтому MAC уровень добавляет еще свой заголовок. PHY слой также добавляет какие-то свои данные. Т.о. наши полезные байтики завернуты в несколько оберток, каждая со своим назначением.При приеме данных распаковка происходит обратным образом - каждый следующий слой отрезает свои заголовки и хвостики, и передает только полезную нагрузку следующему слою. К слою приложения доходят только полезные байты данных без всех оберток. К чему я это все рассказываю. А к тому, что в микроконтроллере ресурсов мало, и каждый раз копировать полезную нагрузку туда-сюда, только чтобы добавить заголовки и хвосты неразумно. Поскольку размеры всех заголовков заранее известны, то можно зарезервировать место под них, прежде чем принимать полезную нагрузку. Забегая вперед, именно этим и занимается такой компонент как Protocol Data Unit Manager (PDUM).От теории к практике. Каркас приложения. Попробуем сделать свое простое ZigBee устройство с каким-нибудь минимальным функционалом. Ну например, пусть это устройство будет умным выключателем с лампочкой. Причем лампочкой можно будет управлять удаленно, да и выключатель должен слать в сеть свое состояние.Действовать будем таким же самым образом, как и в прошлый раз - напишем минимально возможный скелет, и постепенно будем его обрастать мясцом и жирком. Ориентироваться будем на пример JN-AN-1219-Zigbee-3-0-Controller-and-Switch (если точнее, то на https://github.com/actg/JN5169-for-xiaomi-wireless-switch.git, который сделан на основе официально примера) пытаясь понять какие же части из этого наиболее важны. Тестировать будем на тех же самых соплях той же самой железке. Ну и документация, как же без нее.Я пробовал читать документацию от корки до корки. Когда мне становилось скучно - я читал код. Когда натыкался на непонятную функцию я лез читать по ней документацию. Когда в документации что-то было непонятно я приходил к мысли, что наверное это объясняется ранее по тексту, и поэтому нужно прочесть всю документацию от начала и до конца. В итоге я прочитал целиком документ ZigBee 3.0 Stack User Guide JN-UG-3113 и бОльшую часть кода примера, и только тогда у меня в голове начало укладываться как же это все работает.К коду примера от NXP у меня есть некоторые претензии:
  • ребята пытались затолкать несколько примеров в один, и выделили повторяющиеся куски в библиотеки. В итоге код стал гораздо более запутанным, чем мог бы. 
  • Более того, многие части кода включаются разными дефайнами, из-за этого очень сложно понять что мне реально нужно прям щас, а что может быть и не понадобится никогда. 
  • Имена функций и переменных сделаны в венгерской нотации и наполовину состоят из аббревиатур. Аббревиатуры частично объясняются в ZigBee 3.0 Stack User Guide JN-UG-3113 на стр 60, а вот к венгерской нотации нужно привыкать.
  • Часть фреймворка идет в предсобранном виде, а часть в исходниках. При этом исходники от NXP зависят от файлов, которые должен предоставить пользователь
  • В коде примера вызываются функции фреймворка, которые не то, что в документации не описаны - они даже в заголовочных файлах не упоминаются!!!
  • Часть кода генерируется двумя тулзами из некоторого конфигуратора с весьма специфическим UI/UX, на основе сущностей, о которых новичок и понятия не имеет. Кстати, сгенерированный код совершенно непонятен.
Короче, порог вхождения весьма высок, но при желании разобраться можно. В оправдание подхода, предложенного NXP могу предположить, что такая архитектура призвана сократить объем прошивки и перенести настройку устройства из рантайма в компайл тайм. По той же причине во фреймворке отсутствует динамическая память, а все ресурсы распределяются статически на основе параметров в конфигурационных файлах.Итак, попробуем чего-то слепить из полученных знаний. Я не буду описывать мучительный процесс как я по миллиметру в течении нескольких недель продвигался к чему-нибудь компилируемому. Вместо этого детально опишу как бы я делал теперь, когда я уже представляю как все устроено. Впрочем, общий подход даже описан в документации, просто нужно было дочитать до 471 страницы :)
Картинка из документа ZigBee 3.0 Stack User Guide JN-UG-3113Первым делом нужно сделать конфигурацию устройства. Это делается в Zigbee3ConfigEditor, который поставляется вместе с SDK. Это графическая программулина, которая позволяет задавать параметры ZigBee устройств. Причем можно сразу описать несколько устройств в сети, и даже кто с кем будет связан и как они будут между собой взаимодействовать. Вот только без глубокого понимания ZigBee в этой программе делать нечего - просто набор параметров и значений. Причем ты не можешь создать новый проект и накидать только то, что тебе нужно - по любому что-нибудь выставишь не так, или забудешь добавить какие-то параметры (по умолчанию дается пустой проект и параметры нужно добавлять вручную). В общем, я остановился на варианте творческой копи-пасты переработки файла app.zpscfg, который поставляется с примером.
Результатом работы программы Zigbee3ConfigEditor является файлик с расширением .zpscfg, который на деле является просто XML формой того, что видно на экране.Теперь нужно сгенерировать файлы, в которых будут описаны потроха ZigBee стека - различные буфера, начальные значения каких-то внутренних структур данных, дескрипторы устройства, и еще кучу всего. В общем около 1500 строк сишного и немного ассемблерного кода, читать который практически невозможно (ну хорошо, читать-то можно, только ничего не понятно). 
C:\NXP\bstudio_nxp\sdk\JN-SW-4170\Tools\PDUMConfig\bin\PDUMConfig.exe -z HelloZigbee -f HelloZigbee.zpscfg  -o .
C:\NXP\bstudio_nxp\sdk\JN-SW-4170\Tools\ZPSConfig\bin\ZPSConfig.exe -n HelloZigbee -f HelloZigBee.zpscfg -o . -t JN516x -l D:\Projects\NXP\JN5169-for-xiaomi-wireless-switch\sdk\JN-SW-4170\Components\Library\libZPSNWK_JN516x.a -a D:\Projects\NXP\JN5169-for-xiaomi-wireless-switch\sdk\JN-SW-4170\Components\Library\libZPSAPL_JN516x.a -c C:\NXP\bstudio_nxp\sdk\Tools\ba-elf-ba2-r36379
Как я уже сказал, в одном HelloZigbee.zpscfg может быть описана конфигурация нескольких устройств, а потому при вызове этих утилит мы указываем для какого устройства мы хотим генерировать наши файлики (HelloZigbee). Имя выходных файлов всегда одинаковое, т.к. на них ссылаются исходники ZigBee фреймворка.Также, пользователем должен предоставляться еще один конфигурационный файл - zcl_options.h. На него ссылается добрая половина исходников ZigBee фреймворка. В этом файле дефайнами включаются разные сервисы и кластеры ZigBee, которые будут скомпилированы в прошивку, количество энд поинтов, а также список обязательных и опциональных атрибутов кластеров, на которые будет реагировать прошивка. Я творчески переработал этот файл из примера DimmerSwitch, поубирав лишние (на мой взгляд) вещи. Самое главное было включить 1 энд поинт, кластер On/Off, его атрибуты, а также базовые атрибуты устройства из ZDO эндпоинта. Получилось как-то такzcl_options.h
/****************************************************************************/
/***        Macro Definitions                                             ***/
/****************************************************************************/
/****************************************************************************/
/*                      ZCL Specific initialization                         */
/****************************************************************************/
/* This is the NXP manufacturer code.If creating new a manufacturer         */
/* specific command apply to the Zigbee alliance for an Id for your company */
/* Also update the manufacturer code in .zpscfg: Node Descriptor->misc      */
#define ZCL_MANUFACTURER_CODE                                0x1037
/* Sets the number of endpoints that will be created by the ZCL library */
#define ZCL_NUMBER_OF_ENDPOINTS                             1
#define ZCL_ATTRIBUTE_READ_SERVER_SUPPORTED
#define ZCL_ATTRIBUTE_READ_CLIENT_SUPPORTED
#define ZCL_ATTRIBUTE_WRITE_SERVER_SUPPORTED
#define ZCL_NUMBER_OF_REPORTS     1
#define ZLO_MIN_REPORT_INTERVAL   1
#define ZLO_MAX_REPORT_INTERVAL   15
/* Enable wild card profile */
#define ZCL_ALLOW_WILD_CARD_PROFILE
/****************************************************************************/
/*                             Enable Cluster                               */
/*                                                                          */
/* Add the following #define's to your zcl_options.h file to enable         */
/* cluster and their client or server instances                             */
/****************************************************************************/
#define CLD_BASIC
#define BASIC_SERVER
#define BASIC_CLIENT
#define CLD_SCENES
#define SCENES_CLIENT
#define CLD_IDENTIFY
#define IDENTIFY_CLIENT
#define IDENTIFY_SERVER
#define CLD_ONOFF
#define ONOFF_CLIENT
#define ONOFF_SERVER
/****************************************************************************/
/*             Basic Cluster - Optional Attributes                          */
/*                                                                          */
/* Add the following #define's to your zcl_options.h file to add optional   */
/* attributes to the basic cluster.                                         */
/****************************************************************************/
#define   CLD_BAS_ATTR_APPLICATION_VERSION
#define   CLD_BAS_ATTR_STACK_VERSION
#define   CLD_BAS_ATTR_HARDWARE_VERSION
#define   CLD_BAS_ATTR_MANUFACTURER_NAME
#define   CLD_BAS_ATTR_MODEL_IDENTIFIER
#define   CLD_BAS_ATTR_DATE_CODE
#define   CLD_BAS_ATTR_SW_BUILD_ID
#define   CLD_BAS_ATTR_GENERIC_DEVICE_CLASS
#define   CLD_BAS_ATTR_GENERIC_DEVICE_TYPE
#define CLD_BAS_APP_VERSION         (1)
#define CLD_BAS_STACK_VERSION       (2)
#define CLD_BAS_HARDWARE_VERSION    (1)
#define CLD_BAS_MANUF_NAME_STR      "NXP"
#define CLD_BAS_MANUF_NAME_SIZE     3
#define CLD_BAS_MODEL_ID_STR        "Hello Zigbee Switch"
#define CLD_BAS_MODEL_ID_SIZE       19
#define CLD_BAS_DATE_STR            "20210331"
#define CLD_BAS_DATE_SIZE           8
#define CLD_BAS_POWER_SOURCE        E_CLD_BAS_PS_SINGLE_PHASE_MAINS
#define CLD_BAS_SW_BUILD_STR        "v0.1"
#define CLD_BAS_SW_BUILD_SIZE       4
#define CLD_BAS_DEVICE_CLASS        (0)
/****************************************************************************/
/*             Basic Cluster - Optional Commands                            */
/*                                                                          */
/* Add the following #define's to your zcl_options.h file to add optional   */
/* commands to the basic cluster.                                           */
/****************************************************************************/
#define   CLD_BAS_CMD_RESET_TO_FACTORY_DEFAULTS
В процессе сборки также выяснилась необходимость в файле bdb_options.h. В нем оказались какие-то очень тонкие настройки поведения устройства в сети ZigBee, в которых я мало что понимаю. Поэтому я просто взял этот файл из примера как есть.Теперь можно заняться кодом, но прежде заглянем в инструкцию. Раздел 5.1 Forming and Joining a Network нам рекомендует делать инициализацию устройства следующим образом.
Страничка из документа ZigBee 3.0 Stack User Guide JN-UG-3113Разумеется, ничего подобного в коде примера я не нашел. Вместо этого инициализация была размазана по нескольким файлам и десятку функций, и потребовало нечеловеческих усилий чтобы все эти части найти. Пришлось даже сделать дерево вызовов, чтобы понять что именно и в каком порядке вызывается. Причем часть из этих функций на самом деле реализована в ZigBee фреймворке, а пользовательский код вызывает в виде обратных вызовов.Дерево вызовов функций в примере
vAppMain
  APP_vSetUpHardware();
    TARGET_INITIALISE
  APP_vInitResources();
    ZTIMER_eOpen
    ZQ_vQueueCreate
  APP_vInitialise();
    PWRM_vInit(E_AHI_SLEEP_OSCON_RAMON);
    PDM_eInitialise(63);
    PDUM_vInit();
    APP_vInitialiseNode()
      PDM_eReadDataFromRecord(PDM_ID_APP_ZLO_SWITCH) tsDeviceDesc
      PDM_eReadDataFromRecord(PDM_ID_APP_CONVERT) tsConvertR21toR22
      APP_vConvertR21_PdmToR22_Records()
      PDM_eSaveRecordData(PDM_ID_APP_CONVERT)
      ZPS_eAplAfInit()
      APP_ZCL_vInitialise()
        eZCL_Initialise
        eApp_ZCL_RegisterEndpoint
        vAPP_ZCL_DeviceSpecific_Init
      ZPS_bAplAfSetEndDeviceTimeout(ZED_TIMEOUT_16384_MIN);
      - app_vRestartNode();
        ZPS_vSaveAllZpsRecords
      - app_vStartNodeFactoryNew();
      BDB_vInit()
      ZTIMER_eStart(u8TimerSecondStep) delay 50ms
  BDB_vStart()
    APP_vBdbCallback
      - vAppHandleAfEvent
        - APP_ZCL_vEventHandler
        - vAppHandleZdoEvents
          vHandleRunningStackEvent
            - APP_vFactoryResetRecords
              ZPS_vDefaultStack
              ZPS_vSetKeys
              ZPS_eAplAibSetApsUseExtendedPanId
              PDM_eSaveRecordData(PDM_ID_APP_ZLO_SWITCH)
            - vAHI_SwReset();
            - vHandleMatchDescriptor
      - vHandleJoinAndRejoin()
        vSetIndividualLightInformation
        ZPS_vSaveAllZpsRecords
        vStartFastPolling
      - vSetIndividualLightInformation();
      - eCLD_IdentifyCommandIdentifyRequestSend()
      - BDB_vFbExitAsInitiator
  APP_vMainLoop
    zps_taskZPS();
    bdb_taskBDB();
      APP_vBdbCallback
    ZTIMER_vTask();
    APP_taskSwitch();
      - eCLD_ScenesCommandRecallSceneRequestSend
      - BDB_eNsStartNwkSteering
    vAHI_WatchdogRestart();
    PWRM_vManagePower();
Если хорошо приглядеться, то некоторые вызовы идут не совсем в том порядке, в котором они описаны в документации. Но раз пример работает, то, видимо, этот порядок не так сильно важен. В своей реализации я буду ориентироваться на документацию, но частенько заглядывать в код примера.Попробуем изобразить что-нибудь в этом духе. Но вы думаете вот так заглянули в документацию и получили четкую инструкцию что делать? Как бы не так! Разбираясь в коде примера, в поисках ответа как все работает, я наткнулся на другой документ - ZigBee 3.0 Devices User Guide JN-UG-3114. В нем также описаны некоторые аспекты инициализации устройства, причем в форме заметок, вроде “а вот еще не забудьте вот это”.Короче говоря, включаем логику и пытаемся со всем этим багажом знаний взлететь. А логика подсказывает, что независимые подсистемы  можно инициализировать по отдельности в любом порядке. А вот сетевой стек, который опирается на базовые подсистемы, уже придется инициализировать в строгой последовательности.Для начала инициализируем микроконтроллер, UART и отладочный вывод (DBG), систему управления питанием и режимами сна (PWRM),  GPIO, систему хранения значений в EEPROM (Persistent Data Manager, PDM). Тут ничего нового - я про это все детально рассказывал в прошлой статье.
extern "C" PUBLIC void vAppMain(void)
{
   // Initialize the hardware
   TARGET_INITIALISE();
   SET_IPL(0);
   portENABLE_INTERRUPTS();
   // Initialize UART
   DBG_vUartInit(DBG_E_UART_0, DBG_E_UART_BAUD_RATE_115200);
   // Restore blink mode from EEPROM
   DBG_vPrintf(TRUE, "vAppMain(): init PDM...\n");
   PDM_eInitialise(0);
   restoreBlinkMode();
   // Initialize hardware
   DBG_vPrintf(TRUE, "vAppMain(): init GPIO...\n");
   vAHI_DioSetDirection(BOARD_BTN_PIN, BOARD_LED_PIN);
   vAHI_DioSetPullup(BOARD_BTN_PIN, 0);
   // Initialize power manager and sleep mode
   DBG_vPrintf(TRUE, "vAppMain(): init PWRM...\n");
   PWRM_vInit(E_AHI_SLEEP_DEEP);
Следующая подсистема, которую обязательно нужно проинициализировать это Protocol Data Unit Manager, PDUM. Это очень важная штука для ZigBee стека - все операции с байтами, которые нужно передать или принять происходят исключительно через PDUM. С точки зрения инициализации ничего сложного.
// PDU Manager initialization
   DBG_vPrintf(TRUE, "vAppMain(): init PDUM...\n");
   PDUM_vInit();
Код этой функции располагается в файле pdum_gen.c, который нам сгенерировали из zpscfg файла. Там статически объявляется несколько десятков PDU буферов, которые впоследствии будут использоваться стеком и приложениями. Из того же файла торчат 2 переменные apduZDP и apduZCL, которые представляют собой 2 пула буферов. Чем они отличаются я пока слабо понимаю, но эти штуки нам предстоит использовать в дальнейшем.Следующее что нам предлагают сделать это проинициализировать ZigBee Cluster Library (ZCL) с помощью вызова eZCL_Initialise(). Самое главное для нас в этом вызове это указание обратного вызова APP_ZCL_cbGeneralCallback() - подсистема ZCL будет дергать нашу функцию когда будут сообщения для кластеров, которые реализуются в нашем устройстве. 
DBG_vPrintf(TRUE, "vAppMain(): init Zigbee Class Library (ZCL)...  ");
   status = eZCL_Initialise(&APP_ZCL_cbGeneralCallback, apduZCL);
   DBG_vPrintf(TRUE, "eZCL_Initialise() status %d\n", status);
После инициализации ZCL нужно проинициализировать конечную точку устройства. Документация предлагает использовать функцию  eZCL_Register() или ее функциональные аналоги. Что тут имеется в виду под функциональными аналогами? Устройство может регистрировать свои кастомные конечные точки. Но зачем кастомные, если функционал выключателя уже давно стандартизирован ZigBee альянсом и за нас уже давно реализован NXP в виде компонента OnOffLightSwitch (это не название конкретной функции, но является частью названия целого комплекта функций и типов).
tsZLO_OnOffLightSwitchDevice sSwitch;
extern "C" PUBLIC void vAppMain(void)
{
...
   DBG_vPrintf(TRUE, "vAppMain(): register On/Off endpoint...  ");
   status = eZLO_RegisterOnOffLightSwitchEndPoint(HELLOZIGBEE_SWITCH_ENDPOINT, &APP_ZCL_cbEndpointCallback, &sSwitch);
   DBG_vPrintf(TRUE, "eApp_ZCL_RegisterEndpoint() status %d\n", status);
Вызов регистрирует конечную точку, и связывает ее с объектом sSwitch и обратным вызовом APP_ZCL_cbEndpointCallback() - в него будут приходить все события, связанные с этой конечной точкой. Если бы мы делали двухклавишный выключатель, или другое устройство с несколькими конечными точками, то этот вызов нужно было бы повторять для каждого эндпоинта.Дальше предлагается инициализировать слой приложения (Application Framework) вызовом ZPS_eAplAfInit(). Ничего примечательно в этом вызове нет.
DBG_vPrintf(TRUE, "vAppMain(): init Application Framework (AF)...\n");
   ZPS_teStatus status = ZPS_eAplAfInit();
   DBG_vPrintf(TRUE, "ZPS_eAplAfInit() status %d\n", status);
Следующий пункт программы - вызов BDB_vInit(). Тут я остановлюсь поподробнее, т.к. этот вызов не так прост. Вот что пишет документ ZigBee 3.0 Devices User Guide JN-UG-3114.
Из этого кусочка мы понимаем, что вызовы BDB_vInit() и BDB_vStart() очень важны - они инициализирует компонент Base Device Behavior (BDB), который отвечает за общее поведение устройства как узла сети. Причем если случится что-то важное, то нас оповестят через обратный вызов APP_vBdbCallback(). А важен тут Note 2, который гласит, что часть функциональности BDB работает через программные таймеры. Причем пользователь обязан выделить под них место в количестве BDB_ZTIMER_STORAGE штук в общем списке таймеров (помимо наших двух, которые мы определили в прошлой статье - они обслуживают кнопку и светодиод).
ZTIMER_tsTimer timers[2 + BDB_ZTIMER_STORAGE];
extern "C" PUBLIC void vAppMain(void)
{
...
   // Init and start timers
   DBG_vPrintf(TRUE, "vAppMain(): init software timers...\n");
   ZTIMER_eInit(timers, sizeof(timers) / sizeof(ZTIMER_tsTimer));
Но и это еще не все. Документ ZigBee 3.0 Stack User Guide JN-UG-3113 как бы невзначай в разделе 5.9.1.2 (который вообще про очереди сообщений) упоминает еще про то, что любое ZigBee устройство должно объявить еще 3 очереди для нужд ZigBee стека. Причем суть этих очередей рассказывается весьма поверхностно. Нас просто просят скопи-пастить нужный код к себе в приложение.
Ну и ладно, просят, значит надо. Вот я только не понял как именно эти очереди используются в стеке. Дело в том, что туда они попадают в виде нескольких указателей на void, завернутых в структуру в сгенерированном файле zsp_gen.c, а оттуда, опять же указателем void * попадает в стек. Из открытого кода понять как это работает нереально. Более того, в zsp_gen.c фигурируют 4 очереди, а не 3, да и код примера также инициализирует 4 очереди. Ну в данном случае больше не меньше - скопипастим из примера инициализацию четырех очередей. Более того, сама функция BDB_vInit() принимает на вход структуру, у которой одно из полей - хендл еще одной очереди для самого BDB. Итого 5 очередей.
#define BDB_QUEUE_SIZE              3
#define MLME_QUEUE_SIZE             10
#define MCPS_QUEUE_SIZE             24
#define TIMER_QUEUE_SIZE            8
#define MCPS_DCFM_QUEUE_SIZE        5
extern PUBLIC tszQueue zps_msgMlmeDcfmInd;
extern PUBLIC tszQueue zps_msgMcpsDcfmInd;
extern PUBLIC tszQueue zps_TimeEvents;
extern PUBLIC tszQueue zps_msgMcpsDcfm;
PRIVATE MAC_tsMlmeVsDcfmInd asMacMlmeVsDcfmInd[MLME_QUEUE_SIZE];
PRIVATE MAC_tsMcpsVsDcfmInd asMacMcpsDcfmInd[MCPS_QUEUE_SIZE];
PRIVATE MAC_tsMcpsVsCfmData asMacMcpsDcfm[MCPS_DCFM_QUEUE_SIZE];
PRIVATE zps_tsTimeEvent asTimeEvent[TIMER_QUEUE_SIZE];
PRIVATE BDB_tsZpsAfEvent asBdbEvent[BDB_QUEUE_SIZE];
PRIVATE tszQueue APP_msgBdbEvents;
extern "C" PUBLIC void vAppMain(void)
{
...
   // Initialize ZigBee stack queues
   ZQ_vQueueCreate(&zps_msgMlmeDcfmInd, MLME_QUEUE_SIZE, sizeof(MAC_tsMlmeVsDcfmInd), (uint8*)asMacMlmeVsDcfmInd);
   ZQ_vQueueCreate(&zps_msgMcpsDcfmInd, MCPS_QUEUE_SIZE, sizeof(MAC_tsMcpsVsDcfmInd), (uint8*)asMacMcpsDcfmInd);
   ZQ_vQueueCreate(&zps_TimeEvents, TIMER_QUEUE_SIZE, sizeof(zps_tsTimeEvent), (uint8*)asTimeEvent);
   ZQ_vQueueCreate(&zps_msgMcpsDcfm, MCPS_DCFM_QUEUE_SIZE, sizeof(MAC_tsMcpsVsCfmData), (uint8*)asMacMcpsDcfm);
   ZQ_vQueueCreate(&APP_msgBdbEvents, BDB_QUEUE_SIZE, sizeof(BDB_tsZpsAfEvent), (uint8*)asBdbEvent);
У меня нет абсолютно никакого понимания какого размера должны быть эти очереди, и что произойдет, если размеры будут другими. Буду крайне благодарен, если кто-нибудь разъяснит это в комментариях.Ну и сам вызов BDB_vInit()
// Initialize Base Class Behavior
   DBG_vPrintf(TRUE, "vAppMain(): initialize base device behavior...\n");
   BDB_tsInitArgs sInitArgs;
   sInitArgs.hBdbEventsMsgQ = &APP_msgBdbEvents;
   BDB_vInit(&sInitArgs);
Следующим (и последним, не считая инициализации всякой другой периферии) нам предлагают вызвать zps_eAplZdoStartStack(). Но тут обнаружилось существенное расхождение документации и кода примера - в коде примера нет этого вызова. Вообще! И в других примерах тоже. И во фреймворке эту функцию тоже никто не вызывает. Судя по документации эта функция должна стартовать ZigBee стек, после чего устройство сразу начнет ломиться в сеть и к кому-то подключаться. Вероятно это правильное поведение, если у нас преднастроенная сеть, но реальные устройства ведут себя по другому. Например, выключатель Xiaomi с завода будет просто выключателем, пока пользователь не нажмет кнопку на 5-10 секунд. И только тогда устройство будет подключаться к сети.Исходя из кода примеров вместо zps_eAplZdoStartStack() вызывается функция BDB_vStart(), и это согласуется с документом ZigBee 3.0 Devices User Guide JN-UG-3114, раздел 1.4 Device Initialisation. Ну пускай будет BDB_vStart().
DBG_vPrintf(TRUE, "vAppMain(): Starting base device behavior...\n");
   BDB_vStart();
Финальный штрих - прерывания. Микроконтроллер сам умеет работать с радиоприемником, и если был принят очередной пакет с данными, микроконтроллер сгенерирует прерывание. Код прерывания нам писать не придется - он реализован в глубинах ZigBee стека, и поставляется в предсобраном виде. Но его нужно прописать в таблице векторов прерываний в файле irq_JN516x.S.
.globl  PIC_SwVectTable
    .section .text,"ax"
    .extern zps_isrMAC
    .extern ISR_vTickTimer
    .extern vISR_SystemController
    .align 4
    .type   PIC_SwVectTable, @object
    .size   PIC_SwVectTable, 64
PIC_SwVectTable:
    .word vUnclaimedInterrupt               # 0
    .word vISR_SystemController             # 1
    .word vUnclaimedInterrupt               # 2
    .word vUnclaimedInterrupt               # 3
    .word vUnclaimedInterrupt               # 4
    .word vUnclaimedInterrupt               # 5
    .word vUnclaimedInterrupt               # 6
    .word zps_isrMAC                        # 7
    .word vUnclaimedInterrupt               # 8
    .word vUnclaimedInterrupt               # 9
    .word vUnclaimedInterrupt               # 10
    .word vUnclaimedInterrupt               # 11
    .word vUnclaimedInterrupt               # 12
    .word vUnclaimedInterrupt               # 13
    .word vUnclaimedInterrupt               # 14
    .word ISR_vTickTimer                    # 15
Ах да, обратные вызовы. Пока никакой логики в приложении нет, поэтому обратные вызовы практически пустые - они будут наполняться дальше по мере понимания как все это работает.
PRIVATE void APP_ZCL_cbGeneralCallback(tsZCL_CallBackEvent *psEvent)
{
   DBG_vPrintf(TRUE, "APP_ZCL_cbGeneralCallback(): Processing event %d\n", psEvent->eEventType);
   switch (psEvent->eEventType)
   {
       case E_ZCL_CBET_UNHANDLED_EVENT:
           DBG_vPrintf(TRACE_ZCL, "EVT: Unhandled Event\r\n");
           break;
       case E_ZCL_CBET_READ_ATTRIBUTES_RESPONSE:
           DBG_vPrintf(TRACE_ZCL, "EVT: Read attributes response\r\n");
           break;
       case E_ZCL_CBET_READ_REQUEST:
           DBG_vPrintf(TRACE_ZCL, "EVT: Read request\r\n");
           break;
       case E_ZCL_CBET_DEFAULT_RESPONSE:
           DBG_vPrintf(TRACE_ZCL, "EVT: Default response\r\n");
           break;
       case E_ZCL_CBET_ERROR:
           DBG_vPrintf(TRACE_ZCL, "EVT: Error\r\n");
           break;
       case E_ZCL_CBET_TIMER:
           break;
       case E_ZCL_CBET_ZIGBEE_EVENT:
           DBG_vPrintf(TRACE_ZCL, "EVT: ZigBee\r\n");
           break;
       case E_ZCL_CBET_CLUSTER_CUSTOM:
           DBG_vPrintf(TRACE_ZCL, "EP EVT: Custom\r\n");
           break;
       default:
           DBG_vPrintf(TRACE_ZCL, "Invalid event type (%d) in APP_ZCL_cbGeneralCallback\r\n", psEvent->eEventType);
           break;
   }
}
PRIVATE void APP_ZCL_cbEndpointCallback(tsZCL_CallBackEvent *psEvent)
{
   DBG_vPrintf(TRUE, "APP_ZCL_cbEndpointCallback(): Processing event %d\n", psEvent->eEventType);
   switch (psEvent->eEventType)
   {
       case E_ZCL_CBET_UNHANDLED_EVENT:
       case E_ZCL_CBET_READ_ATTRIBUTES_RESPONSE:
       case E_ZCL_CBET_READ_REQUEST:
       case E_ZCL_CBET_DEFAULT_RESPONSE:
       case E_ZCL_CBET_ERROR:
       case E_ZCL_CBET_TIMER:
       case E_ZCL_CBET_ZIGBEE_EVENT:
           DBG_vPrintf(TRACE_ZCL, "EP EVT:No action (evt type %d)\r\n", psEvent->eEventType);
           break;
       case E_ZCL_CBET_READ_INDIVIDUAL_ATTRIBUTE_RESPONSE:
           DBG_vPrintf(TRACE_ZCL, " Read Attrib Rsp %d %02x\n", psEvent->uMessage.sIndividualAttributeResponse.eAttributeStatus,
               *((uint8*)psEvent->uMessage.sIndividualAttributeResponse.pvAttributeData));
           break;
       case E_ZCL_CBET_CLUSTER_CUSTOM:
           DBG_vPrintf(TRACE_ZCL, "EP EVT: Custom %04x\r\n", psEvent->uMessage.sClusterCustomMessage.u16ClusterId);
           break;
       default:
           DBG_vPrintf(TRACE_ZCL, "EP EVT: Invalid event type (%d) in APP_ZCL_cbEndpointCallback\r\n", psEvent->eEventType);
           break;
   }
}
void vfExtendedStatusCallBack (ZPS_teExtendedStatus eExtendedStatus)
{
   DBG_vPrintf(TRUE,"ERROR: Extended status %x\n", eExtendedStatus);
}
PUBLIC void APP_vBdbCallback(BDB_tsBdbEvent *psBdbEvent)
{
   switch(psBdbEvent->eEventType)
   {
       case BDB_EVENT_INIT_SUCCESS:
           DBG_vPrintf(TRUE, "BDB event callback: : BdbInitSuccessful\n");
           break;
       default:
           DBG_vPrintf(1, "BDB event callback: evt %d\n", psBdbEvent->eEventType);
           break;
   }
}
Чтобы все это заработало нужно запустить главный цикл.
DBG_vPrintf(TRUE, "vAppMain(): Starting the main loop\n");
   while(1)
   {
       zps_taskZPS();
       bdb_taskBDB();
       ZTIMER_vTask();
       vAHI_WatchdogRestart();
   }
Кстати, функция zps_taskZPS() нигде в документации не описана, в заголовочных файлах ее нет, но она вызывается в коде примеров. Прошивка толстеет из-за этого вызова на 15 килобайт - вероятно там что-то полезное. Более того она фундаментально важна! Забегая вперед, после того как я реализовал устройство я попробовал убрать этот вызов и без него вообще ничего не работало. Вот так вот.Со сборкой пришлось немного повозиться, чтобы добавить в проект только те файлы, которые реально используются. Но в целом тут ничего интересного. Запускаем и видим
Устройство запустилось и моргает лампочкой (этот функционал я не выбрасывал с прошлой статьи). В логах видим, что инициализация прошла успешно, и устройство в целом работоспособно. Добавляемся в сетьИтак, у нас есть каркас приложения, но пока наше устройство является вещью в себе. Хоть там и крутится ZigBee стек, устройство пока еще никак не взаимодействует с сетью. Попробуем подключиться к сети. Помните функцию zps_eAplZdoStartStack(), которая рекомендуется мануалом, но не использовалась в примерах? Документация на нее гласит, что она стартует ZigBee стек и инициирует подключение устройства к сети. Лично мне пока не очень понятно как именно происходит подключение. Как мне кажется, метод тыка - это весьма неплохой способ изучить что-нибудь новое. Давайте попробуем сделать вызов zps_eAplZdoStartStack() непосредственно перед запуском основного цикла устройства.
// Start ZigBee stack
   DBG_vPrintf(TRUE, "vAppMain(): Starting ZigBee stack... ");
   status = ZPS_eAplZdoStartStack();
   DBG_vPrintf(TRUE, "ZPS_eAplZdoStartStack() status %d\n", status);
Запускаем и видим в консоли следующее.
...
vAppMain(): Starting base device behavior...
BDB event callback: BDB Init Successful
vAppMain(): Starting ZigBee stack... ZPS_eAplZdoStartStack() status 0
vAppMain(): Starting the main loop
BDB event callback: evt 1
Последняя строчка добавляется примерно через секунду после старта основного цикла. Т.е. получается устройство что-то делает, происходит какое-то событие, которое вызывает нашу функцию APP_vBdbCallback(). Смотрим в список событий BDB в файле bdb_api.h и видим, что событие под номером 1 это событие BDB_EVENT_ZPSAF. Т.е. нам хотят сказать, что что-то интересное случилось в модуле Application Framework.Из кода примеров я взял идею выносить обработчики различных событий в отдельные функции. Да, у меня тоже будет куча разных обработчиков как и в примере, но когда ты это пишешь сам, то начинаешь больше понимать структуру приложения.
PUBLIC void APP_vBdbCallback(BDB_tsBdbEvent *psBdbEvent)
{
   switch(psBdbEvent->eEventType)
   {
       case BDB_EVENT_ZPSAF:
           vAppHandleAfEvent(&psBdbEvent->uEventData.sZpsAfEvent);
           break;
...
Обработчик событий AF я взял целиком из примера, только кусочек закомментировал (но мы к этому обязательно вернемся).
PRIVATE void vAppHandleAfEvent(BDB_tsZpsAfEvent *psZpsAfEvent)
{
   DBG_vPrintf(TRUE, "AF event callback: endpoint %d, event %d\n", psZpsAfEvent->u8EndPoint, psZpsAfEvent->sStackEvent.eType);
//    if(psZpsAfEvent->u8EndPoint == app_u8GetDeviceEndpoint())
//    {
//        if(psZpsAfEvent->sStackEvent.eType == ZPS_EVENT_APS_DATA_INDICATION)
//        {
//            DBG_vPrintf(TRACE_SWITCH_NODE, "Pass to ZCL\n");
//            APP_ZCL_vEventHandler(&psZpsAfEvent->sStackEvent);
//        }
//    }
//    else
   if(psZpsAfEvent->u8EndPoint == HELLOZIGBEE_ZDO_ENDPOINT)
   {
       // events for ep 0
       vAppHandleZdoEvents(psZpsAfEvent);
   }
   // Ensure Freeing of Apdus
   if(psZpsAfEvent->sStackEvent.eType == ZPS_EVENT_APS_DATA_INDICATION)
   {
       DBG_vPrintf(TRUE, "AF event callback: freeing up data event APDU\n");
       PDUM_eAPduFreeAPduInstance(psZpsAfEvent->sStackEvent.uEvent.sApsDataIndEvent.hAPduInst);
   }
}
Давайте разбираться что тут происходит. Событие AF обязательно предназначено какой-то конечной точке. Забегая чуточку вперед, я уже подсмотрел, что это конкретное событие предназначено для Zigbee Device Object (ZDO), который сидит на нулевой конечной точке (поэтому первый if закомментирован). Более того, если бы к нам приходили какие-то полезные данные, то нужно было бы освобождать буфер с помощью PDUM_eAPduFreeAPduInstance(). Оставлю эту часть как есть.Разберемся как работает vAppHandleZdoEvents(). Я подсмотрел, что событие, которое мы сейчас рассматриваем это ZPS_EVENT_NWK_DISCOVERY_COMPLETE.
PRIVATE void vAppHandleZdoEvents(BDB_tsZpsAfEvent *psZpsAfEvent)
{
   DBG_vPrintf(TRUE, "Handle ZDO event: event type %d\n", psStackEvent->eType);
   switch(psStackEvent->eType)
   {
       case ZPS_EVENT_NWK_DISCOVERY_COMPLETE:
           vHandleDiscoveryComplete(&psStackEvent->uEvent.sNwkDiscoveryEvent);
           break;
       default:
           break;
   }
}
Теперь становится чуть понятнее что происходит. Функция zps_eAplZdoStartStack() пробует подключиться к сети, которую знает. А если она не знает никакой сети, то запускается процедура network discovery. Устройство посылает в эфир широковещательный запрос “кто здесь?” и ждет пока сети в округе ответят нашему устройству “Я здесь!”. Примерно через секунду процедура дискавери завершается, и ZigBee стек посылает нам сообщение ZPS_EVENT_NWK_DISCOVERY_COMPLETE со списком найденных сетей. Остается только распечатать эти данные.
PRIVATE void vHandleDiscoveryComplete(ZPS_tsAfNwkDiscoveryEvent * pEvent)
{
   DBG_vPrintf(TRUE, "Network Discovery Complete: status %d\n", pEvent->eStatus);
   DBG_vPrintf(TRUE, "    Network count: %d\n", pEvent->u8NetworkCount);
   DBG_vPrintf(TRUE, "    Selected network: %d\n", pEvent->u8SelectedNetwork);
   DBG_vPrintf(TRUE, "    Unscanned channels: %4x\n", pEvent->u32UnscannedChannels);
   for(uint8 i = 0; i < pEvent->u8NetworkCount; i++)
   {
       DBG_vPrintf(TRUE, "    Network %d\n", i);
       ZPS_tsNwkNetworkDescr * pNetwork = pEvent->psNwkDescriptors + i;
       DBG_vPrintf(TRUE, "        Extended PAN ID : %016llx\n", pNetwork->u64ExtPanId);
       DBG_vPrintf(TRUE, "        Logical channel : %d\n", pNetwork->u8LogicalChan);
       DBG_vPrintf(TRUE, "        Stack Profile: %d\n", pNetwork->u8StackProfile);
       DBG_vPrintf(TRUE, "        ZigBee version: %d\n", pNetwork->u8ZigBeeVersion);
       DBG_vPrintf(TRUE, "        Permit Joining: %d\n", pNetwork->u8PermitJoining);
       DBG_vPrintf(TRUE, "        Router capacity: %d\n", pNetwork->u8RouterCapacity);
       DBG_vPrintf(TRUE, "        End device capacity: %d\n", pNetwork->u8EndDeviceCapacity);
   }
}
Вкратце резюмирую суть последних нескольких кусочков кода (я знаю, с первого раза даже такие простые куски на голову натянуть сложно). К нам пришло сообщение от BDB в обработчик APP_vBdbCallback(). Мы увидели, что это сообщение относится к Application Framework и отправили его в обработчик vAppHandleAfEvent(). Там мы поняли, что это сообщение для нулевой конечной точки (т.е. ZDO) и отправили его на обработку в vAppHandleZdoEvents(). И, наконец, тут мы поняли что именно за сообщение приплыло (ZPS_EVENT_NWK_DISCOVERY_COMPLETE), которое мы обработали в функции vHandleDiscoveryComplete().Запуская это в железе видим следующую картинку
Я специально запустил помимо основной сети еще шайбу от Xiaomi, и наша железяка без труда нашла обе сети. И даже параметры (номер канала, и PAN ID) совпадают с реальными
Кусочек карты моей ZigBee сетиЧто еще интересного принесло нам сообщение DiscoveryComplete? А интересен нам параметр Selected Networks. Т.е. ZigBee стек проанализировал обе сети, и решил, что ни к одной из них подключиться не получится. Зато этот параметр чудесным образом превращается в индекс сети (номер в списке сетей), если включить режим Permit Join на координаторе. Т.о. мы уже имеем параметры сети, к которой теоретически можно подключиться. Давайте попробуем это сделать вызовом ZPS_eAplZdoJoinNetwork().
PRIVATE void vHandleDiscoveryComplete(ZPS_tsAfNwkDiscoveryEvent * pEvent)
{
...
   // Check if there is a suitable network to join
   if(pEvent->u8SelectedNetwork == 0xff)
   {
       DBG_vPrintf(TRUE, "    No good network to join\n");
       return;
   }
   // Join the network
   ZPS_tsNwkNetworkDescr * pNetwork = pEvent->psNwkDescriptors + pEvent->u8SelectedNetwork;
   DBG_vPrintf(TRUE, "Network Discovery Complete: Joining network %016llx\n", pNetwork->u64ExtPanId);
   ZPS_teStatus status = ZPS_eAplZdoJoinNetwork(pNetwork);
   DBG_vPrintf(TRUE, "Network Discovery Complete: ZPS_eAplZdoJoinNetwork() status %d", status);
}
В своей системе умного дома в качестве координатора я использую USB стик CC2538 в связке с zigbee2mqtt (Z2M). Давайте заглянем в логи умного дома.
grafalex@SmartHome:~/SmartHome/zigbee2mqtt/log/2021-03-25.23-48-31$ cat log.txt | grep 0x00158d0002b501a7
info  2021-03-28 22:00:41: Device '0x00158d0002b501a7' joined
info  2021-03-28 22:00:41: MQTT publish: topic 'zigbee2mqtt/bridge/event', payload '{"data":{"friendly_name":"0x00158d0002b501a7","ieee_address":"0x00158d0002b501a7"},"type":"device_joined"}'
info  2021-03-28 22:00:41: Starting interview of '0x00158d0002b501a7'
info  2021-03-28 22:00:41: MQTT publish: topic 'zigbee2mqtt/bridge/event', payload '{"data":{"friendly_name":"0x00158d0002b501a7","ieee_address":"0x00158d0002b501a7","status":"started"},"type":"device_interview"}'
info  2021-03-28 22:00:41: MQTT publish: topic 'zigbee2mqtt/bridge/log', payload '{"message":{"friendly_name":"0x00158d0002b501a7"},"type":"device_connected"}'
info  2021-03-28 22:00:41: MQTT publish: topic 'zigbee2mqtt/bridge/log', payload '{"message":"interview_started","meta":{"friendly_name":"0x00158d0002b501a7"},"type":"pairing"}'
info  2021-03-28 22:00:42: MQTT publish: topic 'zigbee2mqtt/bridge/event', payload '{"data":{"friendly_name":"0x00158d0002b501a7","ieee_address":"0x00158d0002b501a7"},"type":"device_announce"}'
info  2021-03-28 22:00:42: MQTT publish: topic 'zigbee2mqtt/bridge/log', payload '{"message":"announce","meta":{"friendly_name":"0x00158d0002b501a7"},"type":"device_announced"}'
info  2021-03-28 22:04:16: Successfully interviewed '0x00158d0002b501a7', device has successfully been paired
warn  2021-03-28 22:04:16: Device '0x00158d0002b501a7' with Zigbee model 'undefined' and manufacturer name 'undefined' is NOT supported, please follow https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html
info  2021-03-28 22:04:16: MQTT publish: topic 'zigbee2mqtt/bridge/event', payload '{"data":{"definition":null,"friendly_name":"0x00158d0002b501a7","ieee_address":"0x00158d0002b501a7","status":"successful","supported":false},"type":"device_interview"}'
info  2021-03-28 22:04:16: MQTT publish: topic 'zigbee2mqtt/bridge/log', payload '{"message":"interview_successful","meta":{"friendly_name":"0x00158d0002b501a7","supported":false},"type":"pairing"}'
Zigbee2mqtt увидело наше устройство и попробовало с ним поболтать. Вот только со стороны устройства практически ничего не реализовано, поэтому интервью закончилось довольно быстро. Z2M сказал, что устройство живое, только как с ним общаться оно пока не знает. На данном этапе это ожидаемо.Посмотрим что происходит со стороны устройства (прошу прощения за криво расставленые переводы строк).
От нас хотят реализации сообщений 2, 3, 5, и 13, что соответствует ZPS_EVENT_APS_DATA_CONFIRM, ZPS_EVENT_APS_DATA_ACK, ZPS_EVENT_NWK_JOINED_AS_ROUTER, и ZPS_EVENT_NWK_STATUS_INDICATION. Также нужно еще обработать сообщение BDB номер 3 (BDB_EVENT_REJOIN_SUCCESS). Поехали разбираться.Я добавил отладочный вывод на все эти события.
PRIVATE void vHandleDataConfirm(ZPS_tsAfDataConfEvent * pEvent)
{
   DBG_vPrintf(TRUE, "ZPS_EVENT_APS_DATA_CONFIRM: SrcEP=%d DstEP=%d DstAddr=%04x Status=%d\n",
           pEvent->u8SrcEndpoint,
           pEvent->u8DstEndpoint,
           pEvent->uDstAddr.u16Addr,
           pEvent->u8Status);
}
PRIVATE void vHandleDataAck(ZPS_tsAfDataAckEvent * pEvent)
{
   DBG_vPrintf(TRUE, "ZPS_EVENT_APS_DATA_ACK: SrcEP=%d DrcEP=%d DstAddr=%04x Profile=%04x Cluster=%04x\n",
               pEvent->u8SrcEndpoint,
               pEvent->u8DstEndpoint,
               pEvent->u16DstAddr,
               pEvent->u16ProfileId,
               pEvent->u16ClusterId);
}
PRIVATE void vHandleJoinedAsRouter(ZPS_tsAfNwkJoinedEvent * pEvent)
{
   DBG_vPrintf(TRUE, "ZPS_EVENT_NWK_JOINED_AS_ROUTER: Addr=%04x, rejoin=%d, secured rejoin=%d\n",
               pEvent->u16Addr,
               pEvent->bRejoin,
               pEvent->bSecuredRejoin);
}
PRIVATE void vHandleNwkStatusIndication(ZPS_tsAfNwkStatusIndEvent * pEvent)
{
   DBG_vPrintf(TRUE, "ZPS_EVENT_NWK_STATUS_INDICATION: Addr:%04x Status:%02x\n",
       pEvent->u16NwkAddr,
       pEvent->u8Status);
}
PRIVATE void vAppHandleZdoEvents(ZPS_tsAfEvent* psStackEvent)
{
   switch(psStackEvent->eType)
   {
       case ZPS_EVENT_APS_DATA_CONFIRM:
           vHandleDataConfirm(&psStackEvent->uEvent.sApsDataConfirmEvent);
           break;
   case ZPS_EVENT_APS_DATA_ACK:
       vHandleDataAck(&psStackEvent->uEvent.sApsDataAckEvent);
       break;
   case ZPS_EVENT_NWK_JOINED_AS_ROUTER:
       vHandleJoinedAsRouter(&psStackEvent->uEvent.sNwkJoinedEvent);
       break;
   case ZPS_EVENT_NWK_STATUS_INDICATION:
       vHandleNwkStatusIndication(&psStackEvent->uEvent.sNwkStatusIndicationEvent);
       break;
   case ZPS_EVENT_NWK_DISCOVERY_COMPLETE:
       vHandleDiscoveryComplete(&psStackEvent->uEvent.sNwkDiscoveryEvent);
       break;
   default:
       DBG_vPrintf(TRUE, "Handle ZDO event: event type %d\n", psStackEvent->eType);
       break;
   }
}
PUBLIC void APP_vBdbCallback(BDB_tsBdbEvent *psBdbEvent)
{
   switch(psBdbEvent->eEventType)
   {
...
   case BDB_EVENT_REJOIN_SUCCESS:
       DBG_vPrintf(TRUE, "BDB event callback: Network Join Successful\n");
       break;
...
   }
}
Перекомпилируем, пытаемся взлететь и.... ничего не происходит. А дело вот в чем. Мы уже один раз подключились к сети, и функция ZPS_eAplZdoStartStack() это запомнила в EEPROM. При следующем старте эта функция думает, что устройство уже в сети и ничего не делает. Попробуем перед запуском ZPS_eAplZdoStartStack() как-то очистить знания устройства о нашей сети. Код примера это делает так.
// Reset Zigbee stack to a very default state
   ZPS_vDefaultStack();
   ZPS_vSetKeys();
   ZPS_eAplAibSetApsUseExtendedPanId(0);
Да, устройство каждый раз будет подключаться к сети заново, но мы с этим разберемся позже. Кстати на стороне zigbee2mqtt нужно также убрать привязку, отправив сообщение в топик zigbee2mqtt/bridge/request/device/remote с содержимым {“id”:”0x00158d0002b501a7”, “force”:true} (ну или удалить устройство через дашборд Z2M)После небольшого причесывания логов процесс присоединения к сети выглядит как-то так.
Давайте разбираться что тут происходит интересного. Прежде всего нам говорят, что устройство подключилось к сети. После этого идет серия событий DATA_CONFIRM и DATA_ACK. Смотрим в документацию и понимаем, что
  • DATA_CONFIRM означает, что устройство отправило пакет данных и этот пакет был доставлен к следующему узлу в маршруте (узел подтвердил прием пакета)
  • далее пакет был передан конечному получателю (не факт, что наше устройство находилось рядом с получателем. Возможно пакету пришлось скакать между несколькими узлами согласно некоторого маршрута). 
  • А вот когда пакет дошел до конечного получателя, тот подтвердил получение событием DATA_ACK (этот пакет был доставлен обратным маршрутом через несколько промежуточных узлов)
Вот только мы видим подтверждение отсылки каких-то сообщений (наше устройство шлет какие-то данные), но не видим самих сообщений. Более того, мы не видим событий, которые обработаны самим стеком ZigBee т.к. они даже не дошли до уровня приложения - только подтверждение ответов. Давайте расчехлять сниффер. Я пользуюсь стареньким донглом на базе CC2531, программулиной whsniffдля захвата пакетов, и wireshark для анализа.
Тут мы видим, что наше устройство отправило запрос Beacon Request и ему отвечают несколько устройств-роутеров неподалеку (в основном это выключатели Xiaomi на базе такого же JN5169). Тут мы видим только то, что творится на основном канале моей ZigBee сети. На самом деле точно такой же Beacon Request был отправлен и в другие каналы, и там тоже были аналогичные ответы (например от шлюза Xiaomi). Именно так на сетевом уровне выглядит процедура network discovery, которую делает функция ZPS_eAplZdoStartStack(). В течении примерно одной секунды функция собирает ответы от разных сетей, после чего посылает нам сообщение ZPS_EVENT_NWK_DISCOVERY_COMPLETE (которое в свою очередь приводит к вызову vHandleDiscoveryComplete()).Как вы помните дальше мы вызывали функцию ZPS_eAplZdoJoinNetwork(). В сниффере это выглядит так.
Тут происходит несколько знаковых вещей
  • Устройство отправляет запрос на присоединение (Association request). 
    • К этому времени у нашего устройства еще нет логического адреса в сети, поэтому оно пользуется полным MAC адресом. 
    • Обращается наше устройство не к координатору, а к одному из роутеров сети (0x2528)
  • Роутер отвечает сообщением Association Response, и назначает нашему устройству адрес 0xcfc2
  • Роутер отправляет координатору несколько сообщений
    • о том, что появился маршрут к новому устройству 0xcfc2
    • о том, что устройство 0xcfc2 присоединилось к сети
  • К этому моменту еще не используется шифрование, поэтому координатор отправляет новому устройству транспортный ключ сети. Причем эта посылка не прямая, а через 2 прыжка - 0x0000->0x2528->0xcfc2  (сниффер видит оба сообщения)
  • Дальше начинается волна сообщений между всеми роутерами в сети о том, что появилось новое устройство и нужно обновить таблицы маршрутов (этих сообщений было очень много, я не стал делать скриншот)
  • Дальше прокатывается еще одна волна сообщений, о том что устройство заявило о себе (Device Announcement) с указанием что это за устройство и какие у него возможности (например что это Full Functional Device с постоянным питанием)

С этого момента zigbee2mqtt узнает о существовании нового устройства и начинает процесс интервью. Это серия запросов разных параметров к новому узлу сети.
А тут мы видим те самые 3 сообщения которые отправляет устройство, и на которые мы видели подтверждения в консоли - Node Descriptor, Active Endpoint, и Simple Descriptor. Разберем их чуточку позже.С этого момента устройство стало полноценным участником сети. В логах появились запросы координатора на обновление таблиц маршрутизации, и запросы на измерения качества связи. Но с точки зрения присоединения устройства к сети, вроде как, все прошло успешно.Обнаружение устройстваПодключить устройство к сети мы, конечно, подключили. Только на дашборде zigbee2mqtt пока наше устройство выглядит так.
Вкладка Exposes вообще пуста. Для примера, вот так выглядит выключатель Xiaomi. Устройство себя описывает в целом ряду полей. Также доступны показания нескольких параметров, и есть возможность включить/выключить канал выключателя.
Для начала разберемся как именно данные об устройстве попадают в сеть ZigBee, и в частности в zigbee2mqtt. Когда устройство подключается к сети, координатор (или z2m? я точно не знаю) начинает процесс интервью. Устройству посылаются различные запросы, отвечая на которые устройство себя описывает по разным критериям. Чтобы разобраться в том где, как, и что нужно сделать, чтобы в zigbee2mqtt появились какие-то данные, я решил сравнить что реально отправляют устройства на запросы Z2M. Для этого я сниффером записал протоколы подключения нашего устройства, и выключателя Xiaomi. Давайте разбираться.Первым делом Z2M посылает запрос на Node Descriptor. Это такая стандартизированная структура, которая описывает устройство как узел сети ZigBee - частоты на которых работает устройство, размеры входящих и исходящих буферов, версию ZigBee стека. Как ни странно, оба устройства выдали примерно одинаковый Node Descriptor (с точностью до MAC адресов, и сетевых счетчиков).
ZigBee Device Profile, Node Descriptor Response, Rev: 22, Nwk Addr: 0x183c, Status: Success
    Sequence Number: 52
    Status: Success (0)
    Nwk Addr of Interest: 0x183c
    Node Descriptor
        .... .... .... .001 = Type: 1 (Router)
        .... .... .... 0... = Complex Descriptor: False
        .... .... ...0 .... = User Descriptor: False
        .... 0... .... .... = 868MHz BPSK Band: False
        ..0. .... .... .... = 902MHz BPSK Band: False
        .1.. .... .... .... = 2.4GHz OQPSK Band: True
        0... .... .... .... = EU Sub-GHz FSK Band: False
        Capability Information: 0x8e
            .... ...0 = Alternate Coordinator: False
            .... ..1. = Full-Function Device: True
            .... .1.. = AC Power: True
            .... 1... = Rx On When Idle: True
            .0.. .... = Security Capability: False
            1... .... = Allocate Short Address: True
        Manufacturer Code: 0x1037
        Max Buffer Size: 127
        Max Incoming Transfer Size: 100
        Server Flags: 0x2c00
            .... .... .... ...0 = Primary Trust Center: False
            .... .... .... ..0. = Backup Trust Center: False
            .... .... .... .0.. = Primary Binding Table Cache: False
            .... .... .... 0... = Backup Binding Table Cache: False
            .... .... ...0 .... = Primary Discovery Cache: False
            .... .... ..0. .... = Backup Discovery Cache: False
            .... .... .0.. .... = Network Manager: False
            0010 110. .... .... = Stack Compliance Revision: 22
        Max Outgoing Transfer Size: 100
        Descriptor Capability Field: 0x00
            .... ...0 = Extended Active Endpoint List Available: False
            .... ..0. = Extended Simple Descriptor List Available: False
Дескрипторы у меня и у Xiaomi различались только в двух полях
  • Manufacturer Code, что в целом ожидаемо - у них Xiaomi, у меня NXP (остался из примера). Впрочем это ни на что не должно влиять
  • Stack Compliance Revision: 22 (у Xiaomi 0). Но судя по документации 22 означает более новую ревизию стека
Следующим запросом является Active Endpoint Request, в ответ на который устройство отправляет список своих конечных точек.
ZigBee Device Profile, Active Endpoint Response, Nwk Addr: 0x183c, Status: Success
    Sequence Number: 53
    Status: Success (0)
    Nwk Addr of Interest: 0x183c
    Endpoint Count: 1
    Active Endpoint List
        Endpoint: 1
Железяка от Xiaomi возвращает аж целых 7 конечных точек. У нас пока, ожидаемо, будет одна.Наконец, Simple Descriptor описывает одну конечную точку, включая список входящих и исходящих кластеров. Координатор делает по одному запросу на каждую конечную точку (т.е. в случае Xiaomi целых 7 штук). Вот так выглядит дескриптор у китайцев.
ZigBee Device Profile, Simple Descriptor Response, Nwk Addr: 0xb006, Status: Success
    Sequence Number: 224
    Status: Success (0)
    Nwk Addr of Interest: 0xb006
    Simple Descriptor Length: 30
    Simple Descriptor
        Endpoint: 1
        Profile: Home Automation (0x0104)
        Application Device: Unknown (0x0051)
        Application Version: 0x0001
        Input Cluster Count: 9
        Input Cluster List
            Input Cluster: Basic (0x0000)
            Input Cluster: Groups (0x0004)
            Input Cluster: Identify (0x0003)
            Input Cluster: On/Off (0x0006)
            Input Cluster: Binary Output (Basic) (0x0010)
            Input Cluster: Scenes (0x0005)
            Input Cluster: Time (0x000a)
            Input Cluster: Power Configuration (0x0001)
            Input Cluster: Device Temperature Configuration (0x0002)
        Output Cluster Count: 2
        Output Cluster List
            Output Cluster: OTA Upgrade (0x0019)
            Output Cluster: Time (0x000a)
А вот так у нас
ZigBee Device Profile, Simple Descriptor Response, Nwk Addr: 0x183c, Status: Success
    Sequence Number: 54
    Status: Success (0)
    Nwk Addr of Interest: 0x183c
    Simple Descriptor Length: 20
    Simple Descriptor
        Endpoint: 1
        Profile: ZigBee Device Profile (0x0000)
        Application Device: 0x0000
        Application Version: 0x0000
        Input Cluster Count: 3
        Input Cluster List
            Input Cluster: Basic (0x0000)
            Input Cluster: Identify (0x0003)
            Input Cluster: On/Off (0x0006)
        Output Cluster Count: 3
        Output Cluster List
            Output Cluster: Basic (0x0000)
            Output Cluster: Identify (0x0003)
            Output Cluster: On/Off (0x0006)
У нас, конечно, поскромнее, но вроде устройство и не обязано поддерживать все на свете. Только вот эти 3 кластера.Сравнивая отдельные поля я нашел одно важное отличие - Profile: Home Automation (0x0104) (у меня этот параметр выставлен в 0). Вероятно из-за этого Z2M не может понять что же за устройство ему подсунули. Параметры эндпоинта задаются в файле HelloZigbee.zpscfg (можно менять либо через графическое приложение, либо напрямую в XML). Нужно только не забыть перегенерировать соответствующие исходники с помощью ZPSConfig.exe. Запускаем и видим изменение - координатор теперь делает запросы к нашей конечной точке, но пока в логе видно только такое.
AF event callback: endpoint 1, event 1
AF event callback: freeing up data event APDU
Ну процесс добавления новых обработчиков уже налажен, и нужно просто написать немного нового кода. Для начала в vAppHandleAfEvent() нужно будет отделять сообщения, которые идут в ZDO (нулевая конечная точка) от сообщений для других конечных точек (в нашем случае первой). Сообщения для разных конечных точек будут направляться либо в  vAppHandleZdoEvents() либо vAppHandleZclEvents().
PRIVATE void vAppHandleAfEvent(BDB_tsZpsAfEvent *psZpsAfEvent)
{
   // Dump the event for debug purposes
   vDumpAfEvent(&psZpsAfEvent->sStackEvent);
   if(psZpsAfEvent->u8EndPoint == HELLOZIGBEE_ZDO_ENDPOINT)
   {
       // events for ep 0
       vAppHandleZdoEvents(&psZpsAfEvent->sStackEvent);
   }
   else if(psZpsAfEvent->u8EndPoint == HELLOZIGBEE_SWITCH_ENDPOINT &&
           psZpsAfEvent->sStackEvent.eType == ZPS_EVENT_APS_DATA_INDICATION)
   {
       vAppHandleZclEvents(&psZpsAfEvent->sStackEvent);
   }
   else if (psZpsAfEvent->sStackEvent.eType != ZPS_EVENT_APS_DATA_CONFIRM &&
            psZpsAfEvent->sStackEvent.eType != ZPS_EVENT_APS_DATA_ACK)
   {
       DBG_vPrintf(TRUE, "AF event callback: endpoint %d, event %d\n", psZpsAfEvent->u8EndPoint, psZpsAfEvent->sStackEvent.eType);
   }
   // Ensure Freeing of APDUs
   if(psZpsAfEvent->sStackEvent.eType == ZPS_EVENT_APS_DATA_INDICATION)
       PDUM_eAPduFreeAPduInstance(psZpsAfEvent->sStackEvent.uEvent.sApsDataIndEvent.hAPduInst);
}
Как я упоминал в теоретической части статьи, бОльшую часть кода по работе с кластерами нам предоставляет библиотека Zigbee Cluster Library (ZCL). Сообщения для конечных точек как правило являются запросами к определенным кластерам, и должны быть перенаправлены для обработки в ZCL. В общем, суть функции vAppHandleZclEvents() исключительно в передаче сообщения в обработчик ZCL (лучи кровавого поноса тому чудо программисту, который заставил завернуть событие в обертку, только ради того, чтобы оно развернулось внутри vZCL_EventHandler() и пошло дальше в обработку в исходном виде).
PRIVATE void vAppHandleZclEvents(ZPS_tsAfEvent* psStackEvent)
{
   tsZCL_CallBackEvent sCallBackEvent;
   sCallBackEvent.pZPSevent = psStackEvent;
   sCallBackEvent.eEventType = E_ZCL_CBET_ZIGBEE_EVENT;
   vZCL_EventHandler(&sCallBackEvent);
}
Запускаем и выясняем, что сообщения обрабатываются внутри ZCL и прилетают к нам обратными вызовами в APP_ZCL_cbEndpointCallback(). Нужно дописать еще немного кода и туда.
PRIVATE void APP_ZCL_cbEndpointCallback(tsZCL_CallBackEvent *psEvent)
{
   switch (psEvent->eEventType)
   {
       case E_ZCL_CBET_READ_REQUEST:
           vDumpZclReadRequest(psEvent);
           break;
...
Тут выяснился один интересный момент. ZCL будет вызывать наш обратный вызов перед обработкой запроса на чтение атрибута. Это нужно чтобы приложение обновило все данные перед тем как ZCL прочитает эти переменные и отправит их вопрошающему. Т.е. еще разок, наш обратный вызов только готовит данные, а отправляются они в недрах ZCL. В нашем конкретном случае мы только залогируем запрос, больше ничего делать нам пока не требуется. Вот только к нам в функцию не приходит информация что именно у нас запрашивают. Поэтому пришлось вычитать это из запроса вручную.
PRIVATE void vDumpZclReadRequest(tsZCL_CallBackEvent *psEvent)
{
   // Read command header
   tsZCL_HeaderParams headerParams;
   uint16 inputOffset = u16ZCL_ReadCommandHeader(psEvent->pZPSevent->uEvent.sApsDataIndEvent.hAPduInst,
                                             &headerParams);
   // read input attribute Id
   uint16 attributeId;
   inputOffset += u16ZCL_APduInstanceReadNBO(psEvent->pZPSevent->uEvent.sApsDataIndEvent.hAPduInst,
                                             inputOffset,
                                             E_ZCL_ATTRIBUTE_ID,
                                             &attributeId);
   DBG_vPrintf(TRUE, "ZCL Read Attribute: EP=%d Cluster=%04x Command=%02x Attr=%04x\n",
               psEvent->u8EndPoint,
               psEvent->pZPSevent->uEvent.sApsDataIndEvent.u16ClusterId,
               headerParams.u8CommandIdentifier,
               attributeId);
}
Пробуем со всем этим взлететь.
BDB event callback: Network Join Successful
ZPS_EVENT_NWK_JOINED_AS_ROUTER: Addr=d9fa, rejoin=0, secured rejoin=0
ZPS_EVENT_APS_DATA_CONFIRM: SrcEP=0 DstEP=0 DstAddr=0000 Status=0
ZPS_EVENT_APS_DATA_ACK: SrcEP=0 DrcEP=0 DstAddr=0000 Profile=0000 Cluster=8002
ZPS_EVENT_NWK_STATUS_INDICATION: Addr:0000 Status:11
ZPS_EVENT_APS_DATA_CONFIRM: SrcEP=0 DstEP=0 DstAddr=0000 Status=0
ZPS_EVENT_APS_DATA_ACK: SrcEP=0 DrcEP=0 DstAddr=0000 Profile=0000 Cluster=8005
ZPS_EVENT_APS_DATA_CONFIRM: SrcEP=0 DstEP=0 DstAddr=0000 Status=0
ZPS_EVENT_APS_DATA_ACK: SrcEP=0 DrcEP=0 DstAddr=0000 Profile=0000 Cluster=8004
ZPS_EVENT_NWK_STATUS_INDICATION: Addr:0000 Status:11
ZPS_EVENT_NWK_STATUS_INDICATION: Addr:0000 Status:11
ZPS_EVENT_APS_DATA_INDICATION: SrcEP=1 DstEP=1 SrcAddr=0000 Cluster=0000 Status=0
ZCL Read Attribute: EP=1 Cluster=0000 Command=00 Attr=0005
ZPS_EVENT_APS_DATA_CONFIRM: SrcEP=1 DstEP=1 DstAddr=0000 Status=0
ZPS_EVENT_APS_DATA_ACK: SrcEP=1 DrcEP=1 DstAddr=0000 Profile=0104 Cluster=0000
ZPS_EVENT_APS_DATA_INDICATION: SrcEP=1 DstEP=1 SrcAddr=0000 Cluster=0000 Status=0
ZCL Read Attribute: EP=1 Cluster=0000 Command=00 Attr=0004
ZPS_EVENT_APS_DATA_CONFIRM: SrcEP=1 DstEP=1 DstAddr=0000 Status=0
ZPS_EVENT_APS_DATA_ACK: SrcEP=1 DrcEP=1 DstAddr=0000 Profile=0104 Cluster=0000
ZPS_EVENT_APS_DATA_INDICATION: SrcEP=1 DstEP=1 SrcAddr=0000 Cluster=0000 Status=0
ZCL Read Attribute: EP=1 Cluster=0000 Command=00 Attr=0007
ZPS_EVENT_APS_DATA_CONFIRM: SrcEP=1 DstEP=1 DstAddr=0000 Status=0
ZPS_EVENT_APS_DATA_ACK: SrcEP=1 DrcEP=1 DstAddr=0000 Profile=0104 Cluster=0000
ZPS_EVENT_APS_DATA_INDICATION: SrcEP=1 DstEP=1 SrcAddr=0000 Cluster=0000 Status=0
ZCL Read Attribute: EP=1 Cluster=0000 Command=00 Attr=0000
ZPS_EVENT_APS_DATA_CONFIRM: SrcEP=1 DstEP=1 DstAddr=0000 Status=0
ZPS_EVENT_APS_DATA_ACK: SrcEP=1 DrcEP=1 DstAddr=0000 Profile=0104 Cluster=0000
ZPS_EVENT_APS_DATA_INDICATION: SrcEP=1 DstEP=1 SrcAddr=0000 Cluster=0000 Status=0
ZCL Read Attribute: EP=1 Cluster=0000 Command=00 Attr=0001
ZPS_EVENT_APS_DATA_CONFIRM: SrcEP=1 DstEP=1 DstAddr=0000 Status=0
ZPS_EVENT_APS_DATA_ACK: SrcEP=1 DrcEP=1 DstAddr=0000 Profile=0104 Cluster=0000
ZPS_EVENT_NWK_STATUS_INDICATION: Addr:0000 Status:11
ZPS_EVENT_APS_DATA_INDICATION: SrcEP=1 DstEP=1 SrcAddr=0000 Cluster=0000 Status=0
ZCL Read Attribute: EP=1 Cluster=0000 Command=00 Attr=0002
ZPS_EVENT_APS_DATA_CONFIRM: SrcEP=1 DstEP=1 DstAddr=0000 Status=0
ZPS_EVENT_APS_DATA_ACK: SrcEP=1 DrcEP=1 DstAddr=0000 Profile=0104 Cluster=0000
ZPS_EVENT_APS_DATA_INDICATION: SrcEP=1 DstEP=1 SrcAddr=0000 Cluster=0000 Status=0
ZCL Read Attribute: EP=1 Cluster=0000 Command=00 Attr=0003
ZPS_EVENT_APS_DATA_CONFIRM: SrcEP=1 DstEP=1 DstAddr=0000 Status=0
ZPS_EVENT_APS_DATA_ACK: SrcEP=1 DrcEP=1 DstAddr=0000 Profile=0104 Cluster=0000
ZPS_EVENT_APS_DATA_INDICATION: SrcEP=1 DstEP=1 SrcAddr=0000 Cluster=0000 Status=0
ZCL Read Attribute: EP=1 Cluster=0000 Command=00 Attr=0006
ZPS_EVENT_NWK_STATUS_INDICATION: Addr:0000 Status:11
ZPS_EVENT_APS_DATA_CONFIRM: SrcEP=1 DstEP=1 DstAddr=0000 Status=0
ZPS_EVENT_APS_DATA_ACK: SrcEP=1 DrcEP=1 DstAddr=0000 Profile=0104 Cluster=0000
ZPS_EVENT_APS_DATA_INDICATION: SrcEP=1 DstEP=1 SrcAddr=0000 Cluster=0000 Status=0
ZCL Read Attribute: EP=1 Cluster=0000 Command=00 Attr=4000
ZPS_EVENT_APS_DATA_CONFIRM: SrcEP=1 DstEP=1 DstAddr=0000 Status=0
ZPS_EVENT_APS_DATA_ACK: SrcEP=1 DrcEP=1 DstAddr=0000 Profile=0104 Cluster=0000
Тут я выделил входящие пакеты (DATA_INDICATION), которые были переварены ZCL в запрос ZCL Read Attribute. Как я уже сказал, ZCL сам обработает чтение атрибута и отправке ответа по сети (наше участие только в логгировании этого факта). Отправку мы видим в виде подтверждений DATA_CONFIRM и DATA_ACK.Но вернемся к данным. Мы видим запросы в кластер 0000 (Basic Cluster) с запросом на чтение атрибутов с номерами 0-7. Эти атрибуты описывают имя устройства, имя производителя, версию железа и софта, а также параметры питания. Вот только несмотря на то, что какие-то данные были отправлены, в zigbee2mqtt это доехало как минимум некорректно.
Смотрим сниффером, и действительно там передаются нулевые байты. Давайте разбираться. После непродолжительных поисков я пришел в файл zcl_options.h где сказано какие атрибуты у нас будут присутствовать (я включил все те, которые запрашивались zigbee2mqtt, и выключил другие), а также размер этих атрибутов (об этом ниже).
#define   CLD_BAS_ATTR_APPLICATION_VERSION
#define   CLD_BAS_ATTR_STACK_VERSION
#define   CLD_BAS_ATTR_HARDWARE_VERSION
#define   CLD_BAS_ATTR_MANUFACTURER_NAME
#define   CLD_BAS_ATTR_MODEL_IDENTIFIER
#define   CLD_BAS_ATTR_DATE_CODE
#define   CLD_BAS_ATTR_SW_BUILD_ID
#define   CLD_BAS_ATTR_GENERIC_DEVICE_CLASS
#define   CLD_BAS_ATTR_GENERIC_DEVICE_TYPE
#define CLD_BAS_APP_VERSION         (1)
#define CLD_BAS_STACK_VERSION       (2)
#define CLD_BAS_HARDWARE_VERSION    (1)
#define CLD_BAS_MANUF_NAME_STR      "NXP"
#define CLD_BAS_MANUF_NAME_SIZE     3
#define CLD_BAS_MODEL_ID_STR        "Hello Zigbee Switch"
#define CLD_BAS_MODEL_ID_SIZE       19
#define CLD_BAS_DATE_STR            "20210331"
#define CLD_BAS_DATE_SIZE           8
#define CLD_BAS_POWER_SOURCE        E_CLD_BAS_PS_SINGLE_PHASE_MAINS
#define CLD_BAS_SW_BUILD_STR        "v0.1"
#define CLD_BAS_SW_BUILD_SIZE       4
#define CLD_BAS_DEVICE_CLASS        (0)
Как же я “люблю” язык С, где приходится руками считать длину строк. strlen() использовать тут нельзя, т.к. этот хедер включается в файлы фреймворка, и там попросту нет необходимого #include.Также где-то в vAppMain() нужно приготовить сами данные.
//Fill Basic cluster attributes
   memcpy(sSwitch.sBasicServerCluster.au8ManufacturerName, CLD_BAS_MANUF_NAME_STR, CLD_BAS_MANUF_NAME_SIZE);
   memcpy(sSwitch.sBasicServerCluster.au8ModelIdentifier, CLD_BAS_MODEL_ID_STR, CLD_BAS_MODEL_ID_SIZE);
   memcpy(sSwitch.sBasicServerCluster.au8DateCode, CLD_BAS_DATE_STR, CLD_BAS_DATE_SIZE);
   memcpy(sSwitch.sBasicServerCluster.au8SWBuildID, CLD_BAS_SW_BUILD_STR, CLD_BAS_SW_BUILD_SIZE);
   sSwitch.sBasicServerCluster.eGenericDeviceType = E_CLD_BAS_GENERIC_DEVICE_TYPE_WALL_SWITCH;
Вуаля, теперь данные отображаются в zigbee2mqtt. Все данные подтянулись и отображаются. Статус unsupported означает, что zigbee2mqtt пока еще не очень понимает что можно делать с устройством, но во всяком случае стандартные ответы от железки оно понимает.
ВыключательИтак, устройство умеет подключаться к сети, и может себя корректно описать. Но вот выключателем оно пока еще не стало. Давайте попробуем передать какие-нибудь данные.По правде сказать, в этом месте я залип на неделю. Совершенно понятно, что устройство должно отсылать какие-то сигналы по нажатию кнопки, и наоборот реагировать на внешние сигналы и переключать состояние лампочки. Но при этом совершенно не ясно как это делать. Дело вот в чем.Как выяснилось, пример JN-AN-1219-Zigbee-3-0-Controller-and-Switch демонстрирует нам совершенно не такой режим работы как требуется. В этом примере реализованы несколько устройств, которые умеют общаться друг с другом. Т.е. они могут посылать друг другу команды вкл/выкл, находить друг друга различными способами и связывать свои конечные точки, запрашивать друг у друга какие-то данные. Но реальные выключатели (например Xiaomi) работают совершенно не так. Они общаются не между собой, а с координатором как с мозгом сети, отсылая ему отчеты о нажатии клавиш, и принимая команды на включение и выключение. При этом используются немного другие внутренние механизмы. Например, реальный выключатель не шлет команду “включись” (хотя бы потому что он не знает какой лампочке реально послать эту команду). Вместо этого он шлет отчет координатору сообщение “у меня поменялось значение атрибута ВКЛЮЧЕНО”, а уже координатор думает что с этим делать.Более того в коде, который предоставляет Zigbee Class Library есть функция “включи свет вон у того устройства”, но при этом попросту нет функции вроде “включи свет у себя” и нужно было искать альтернативные реализации. (кстати говоря функция то есть - eCLD_OnOffSetState. Только она не заявлена в публичном интерфейсе библиотеки).Ситуацию спас пример JN-AN-1220-Zigbee-3-0-Sensors я обнаружил совершенно другую реализацию ZigBee устройства. Мало того, что в нем обнаружились механизмы отсылки статусов, так и код этого примера гораздо более понятный, чем у примера с выключателем. Я смело могу рекомендовать пример с сенсорами для изучения ZigBee от NXP - кода гораздо меньше, он лучше структурирован, разбит на небольшие примерчики, и в целом лучше подходит для наших задач. И хотя из нового примера стало понятно какие действия нужно делать, все равно у меня долгое время не получалось отправить сообщения на координатор. А дело было вот в чем. Реализация ZigBee Cluster Library от NXP предоставляет заготовки для различных устройств - лампочек, термометров, сенсоров, и прочего. Поскольку я делаю выключатель, то в качестве основы для своего устройства я выбрал OnOffLightSwitchDevice. Так вот ошибка была в том, что выключатель с точки зрения ZCL это клиентское устройство. Выключатель может только отправлять команды, тогда как внутреннего состояния (во всяком случае в виде атрибута кластера OnOff) у него нет. Правильный выбор это OnOffLightDevice (не Switch) - это серверное устройство, которое имеет атрибут On/Off. Устройство может сообщать координатору значение этого атрибута, если он меняется, и принимать команды на включение/выключение. Собственно говоря, это была наибольшая проблема. Все остальное получилось достаточно быстро.В качестве краткой экскурсии в прошлую статью, я приведу код опроса кнопок и моргания лампочки. Кнопки просто опрашиваются раз в 10мс и отправляются 2 вида сигналов - короткое и длинное нажатие.
PUBLIC void buttonScanFunc(void *pvParam)
{
   static int duration = 0;
   uint32 input = u32AHI_DioReadInput();
   bool btnState = (input & BOARD_BTN_PIN) == 0;
   if(btnState)
   {
       duration++;
       DBG_vPrintf(TRUE, "Button still pressed for %d ticks\n", duration);
   }
   else
   {
       // detect long press
       if(duration > 200)
       {
           DBG_vPrintf(TRUE, "Button released. Long press detected\n");
           ButtonPressType value = BUTTON_LONG_PRESS;
           ZQ_bQueueSend(&buttonQueueHandle, (uint8*)&value);
       }
       // detect short press
       else if(duration > 5)
       {
           DBG_vPrintf(TRUE, "Button released. Short press detected\n");
           ButtonPressType value = BUTTON_SHORT_PRESS;
           ZQ_bQueueSend(&buttonQueueHandle, &value);
       }
       duration = 0;
   }
   ZTIMER_eStart(buttonScanTimerHandle, ZTIMER_TIME_MSEC(10));
}
Теперь светодиод. И хотя обычный выключатель либо включает либо выключает лампочку, мой светодиод будет моргать. Это позволит мне наблюдать не повисло ли устройство, и есть ли какие-нибудь странности (а они, кстати говоря, есть). В общем, в режиме “включено” мой светодиод будет моргать часто, а в режиме “выключено” - медленно.Самое важное: откуда брать состояние выключателя? Библиотека ZCL предлагает обращаться напрямую к соответствующим переменным в объекте кластера (в нашем случае sSwitch.sOnOffServerCluster.bOnOff)
PUBLIC void blinkFunc(void *pvParam)
{
   // Toggle LED
   uint32 currentState = u32AHI_DioReadInput();
   vAHI_DioSetOutput(currentState^BOARD_LED_PIN, currentState&BOARD_LED_PIN);
   // restart blink timer
   ZTIMER_eStart(blinkTimerHandle, sSwitch.sOnOffServerCluster.bOnOff ? ZTIMER_TIME_MSEC(200) : ZTIMER_TIME_MSEC(1000));
}
Обрабатывать кнопку будет функция APP_vTaskSwitch(), которую будем вызывать из главного цикла. В ней будет происходить инвертирование значения sSwitch.sOnOffServerCluster.bOnOff, а также отправка соответствующего сообщения на координатор.
PRIVATE void APP_vTaskSwitch()
{
   ButtonPressType value;
   if(ZQ_bQueueReceive(&buttonQueueHandle, (uint8*)&value))
   {
       DBG_vPrintf(TRUE, "Processing button message %d\n", value);
       if(value == BUTTON_SHORT_PRESS)
       {
           // Toggle the value
           sSwitch.sOnOffServerCluster.bOnOff = sSwitch.sOnOffServerCluster.bOnOff ? FALSE : TRUE;
           // Destination address - 0x0000 (coordinator)
           tsZCL_Address addr;
           addr.uAddress.u16DestinationAddress = 0x0000;
           addr.eAddressMode = E_ZCL_AM_SHORT;
           DBG_vPrintf(TRUE, "Reporing attribute... ", value);
           PDUM_thAPduInstance myPDUM_thAPduInstance = hZCL_AllocateAPduInstance();
           teZCL_Status status = eZCL_ReportAttribute(&addr,
                                                      GENERAL_CLUSTER_ID_ONOFF,
                                                      E_CLD_ONOFF_ATTR_ID_ONOFF,
                                                      HELLOZIGBEE_SWITCH_ENDPOINT,
                                                      1,
                                                      myPDUM_thAPduInstance);
           PDUM_eAPduFreeAPduInstance(myPDUM_thAPduInstance);
           DBG_vPrintf(TRUE, "status: %02x\n", status);
       }
       if(value == BUTTON_LONG_PRESS)
       {
           // TODO: add network join here
       }
   }
}
Я пока еще не дочитал документацию, но мне не ясно, почему координатору нужно слать сообщения именно в конечную точку №1. Похоже, что это просто конечная точка из профиля Home Automation (в отличии от других). Возможно это какой-то стандарт, что именно первая конечная точка это Home Automation. Буду благодарен если кто-нибудь мне это расскажет в комментариях.Поскольку координатор не реализует никакого специфичного кластера на этой конечной точке, сообщение отсылается как Profile-wide (а не предназначенное конкретному кластеру).Заливаем прошивку и видим, что сообщение координатору успешно отправляется, и даже от координатора возвращается стандартное подтверждение.
Button released. Short press detected
Processing button message 0
Reporing attribute... status: 00
ZPS_EVENT_APS_DATA_CONFIRM: SrcEP=1 DstEP=1 DstAddr=0000 Status=0
ZPS_EVENT_APS_DATA_ACK: SrcEP=1 DrcEP=1 DstAddr=0000 Profile=0104 Cluster=0006
ZPS_EVENT_APS_DATA_INDICATION: SrcEP=1 DstEP=1 SrcAddr=0000 Cluster=0006 Status=0
ZCL Endpoint Callback: DEFAULT_RESPONSE received. No action
В wireshark’е видим сообщение в сторону координатора о том, что значение атрибута изменилось
Zigbee2mqtt тоже видит это сообщение, только вот сделать с ним оно пока ничего не может. Но во всяком случае нам сказали что делать.
zigbee2mqtt      | Zigbee2MQTT:debug 2021-04-04 10:55:00: Received Zigbee message from '0x00158d0002b501a7', type 'attributeReport', cluster 'genOnOff', data '{"onOff":0}' from endpoint 1 with groupID 0
zigbee2mqtt      | Zigbee2MQTT:warn  2021-04-03 23:26:36: Received message from unsupported device with Zigbee model 'Hello Zigbee Switch' and manufacturer name 'NXP'
zigbee2mqtt      | Zigbee2MQTT:warn  2021-04-03 23:26:36: Please see: https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html.
Помимо официальной документации по подключению устройств я нашел толковую статью на русском. Особо полезным было знание, что больше не нужно лезть в код самого zigbee2mqtt (что весьма проблематично, если используется докер образ) - с недавних пор к zigbee2mqtt можно подключать внешние конвертеры. Конвертер это функция, которая преобразовывает сообщения от устройства в структуры zigbee2mqtt и наоборот.Создадим файлик myswitch.js рядом с конфигурацией Z2M.
const fz = require('zigbee-herdsman-converters/converters/fromZigbee');
const tz = require('zigbee-herdsman-converters/converters/toZigbee');
const exposes = require('zigbee-herdsman-converters/lib/exposes');
const reporting = require('zigbee-herdsman-converters/lib/reporting');
const e = exposes.presets;
const device = {
    zigbeeModel: ['Hello Zigbee Switch'],
    model: 'Hello Zigbee Switch',
    vendor: 'NXP',
    description: 'Hello Zigbee Switch',
    fromZigbee: [fz.on_off],
    toZigbee: [tz.on_off],
    exposes: [e.switch()],
};
module.exports = device;
Теперь если Z2M видит, что подключилось наше устройство, то сообщения нужно проганять через стандартные конвертеры fz.on_off и tz.on_off, которые описаны тут и тутсоответственно.Страничка устройства в Z2M больше не ругается что устройство unsupported, а закладка Exposes демонстрирует нам выключатель. Более того положение этого выключателя динамически меняется при переключении с устройства.
Закладка State также показывает актуальное состояние выключателя
В обратную сторону тоже работает. Если менять значение через Z2M, то режим мигания лампочки на устройстве также меняется. Реализация ZigBee Cluster Library самостоятельно изменит значение переменной sSwitch.sOnOffServerCluster.bOnOff, после чего лампочка замигает по новому.А что если нам нужно делать какие-то действия по приходу сообщения от координатора? Для этого нам будет выслано сигнала - E_ZCL_CBET_CLUSTER_CUSTOM и E_ZCL_CBET_CLUSTER_UPDATE. Данные в них приходят абсолютно идентичные, более того эти 2 вызова расположены буквально в соседних строках в исходниках ZCL. Можно реагировать на любой.
PRIVATE void APP_ZCL_cbEndpointCallback(tsZCL_CallBackEvent *psEvent)
{
   switch (psEvent->eEventType)
   {
...
       case E_ZCL_CBET_CLUSTER_UPDATE:
           vHandleClusterUpdateMessage(psEvent);
           break;
PRIVATE void vHandleClusterUpdateMessage(tsZCL_CallBackEvent *psEvent)
{
   uint16 u16ClusterId = psEvent->uMessage.sClusterCustomMessage.u16ClusterId;
   tsCLD_OnOffCallBackMessage * msg = (tsCLD_OnOffCallBackMessage *)psEvent->uMessage.sClusterCustomMessage.pvCustomData;
   uint8 u8CommandId = msg->u8CommandId;
   DBG_vPrintf(TRUE, "ZCL Endpoint Callback: Cluster update message EP=%d ClusterID=%04x Cmd=%02x\n", psEvent->u8EndPoint, u16ClusterId, u8CommandId);
}
По традиции я делаю только отладочный вывод, но ничего не мешает вставить сюда код переключения реле, например. Все данные у нас для этого есть - номер конечной точки, номер кластера, и номер команды.В логе это выглядит так. Я выключил/включил переключатель на дашборде Z2M.
ZPS_EVENT_APS_DATA_INDICATION: SrcEP=1 DstEP=1 SrcAddr=0000 Cluster=0006 Status=0
ZCL Endpoint Callback: Custom cluster message EP=1 ClusterID=0006 Cmd=00
ZCL Endpoint Callback: Cluster update message EP=1 ClusterID=0006 Cmd=00
ZPS_EVENT_APS_DATA_CONFIRM: SrcEP=1 DstEP=1 DstAddr=0000 Status=0
ZPS_EVENT_APS_DATA_ACK: SrcEP=1 DrcEP=1 DstAddr=0000 Profile=0104 Cluster=0006
ZPS_EVENT_APS_DATA_INDICATION: SrcEP=1 DstEP=1 SrcAddr=0000 Cluster=0006 Status=0
ZCL Endpoint Callback: Custom cluster message EP=1 ClusterID=0006 Cmd=01
ZCL Endpoint Callback: Cluster update message EP=1 ClusterID=0006 Cmd=01
На этом устройство можно считать законченным. Оно уже умеет подключаться к сети, отправлять изменения состояния, и принимать команды на включение/выключение. Далее это устройство можно интегрировать в любую систему умного дома как обычно. Можно, например, включать и выключать свет в комнате. В Home Assistant я просто сделал дополнительную автоматизацию.
- id: '1617533042952'
  alias: Test
  description: Test
  trigger:
  - platform: state
    entity_id: switch.0x00158d0002b501a7
    attribute: state
  condition: []
  action:
  - type: toggle
    device_id: 3effcfbd5022447213791dc054d043ec
    entity_id: switch.living_room_light_right
    domain: switch
  mode: single
Теперь мой умный выключатель умеет включать и выключать свет в гостинной.Заключение Итак, довольно нехитрыми действиями нам удалось создать свое собственное устройство ZigBee, которое реализует функционал умного выключателя. С первого взгляда может показаться сложно и много нужно писать кода, но если выкинуть отладочные сообщения, то там строчек 300 в сухом остатке получается.Другое дело, что смысл этих 300 строк нужно на голову натянуть - куча каких-то сообщений и обработчиков, которые как-то вызывают друг друга. Но тут тоже все не очень сложно. Это неудачный пример от NXP нас запутал, но иерархия этих обработчиков довольно простая.Давайте я еще разок приведу картинку с архитектурой ZigBee стека и на ней все расскажу.
Когда по сети передается какой-нибудь пакет с данными происходит следующее:
  • Уровень PHY слушает радиоэфир и принимает пакет с данными во внутренний буфер
  • Когда пакет полностью принят возникает прерывание zps_isrMAC и пакет обрабатывается MAC уровнем
  • Если этот пакет предназначен нашему устройству, он передается в сетевой слой ZigBee (NWK) для дальнейшего переваривания
  • Пакет попадает в ZigBee Base Device (Base Device Behavior, BDB) где обрабатывается в зависимости от текущего состояния устройства. До этого пункта включительно все происходит прозрачно для нас, и программист в этом не участвует.
  • APP_vBdbCallback() это точка входа события в наше приложение. Тут нам могут сообщить о важных событиях самого устройства в целом (инициализировалось, присоединилось к сети, отвалилось от сети), и также могут прислать пакет с полезными данными.
  • Пакет с данными обрабатывается функцией vAppHandleAfEvent(). На самом деле данные могут приехать либо части Zigbee Device Object (ZDO), которая отвечает за поведение устройства в сети, либо собственно приложению и его конечным точкам. По сути функция смотрит на номер конечной точки и распределяет сообщение либо туда либо туда.
  • Функция vAppHandleZdoEvents() обрабатывает запросы к ZDO (нулевая конечная точка). Речь идет о запросах к атрибутам устройства, так и всей сетевой коммуникации от имени устройства - всякие запросы к другим устройствам, связывание, поиск устройств, запросы всяких дескрипторов.
  • Функция vAppHandleZclEvents() обрабатывает запросы ко всем остальным конечным точкам устройства. На деле все это просто уезжает в соответствующие обработчики из Zigbee Cluster Library. 
  • Иногда оттуда прилетают обратные вызовы в функцию APP_ZCL_cbEndpointCallback() если есть какая-то информация для конечных точек устройства - запрос на запись или чтение атрибута, пришел запрос или команда.
Вот по сути и весь стек ZigBee.Если в 300 строк кода можно уместить прошивку для умного выключателя, то почему у производителя примеры занимают несколько мегабайт? А вот почему
  • В примерах много лишнего. Там задача дать программисту понять “а вот мы еще вот так умеем”
  • Многое даже из базового функционала у меня попросту не реализовано - подключение устройства по кнопке, переподключение в случае выпадания из сети, прямые соединения устройств (binding)
  • у меня абсолютно отсутствует обработка ошибок. На деле устройство ощутимо глючит: в 50% случаев не может присоединиться к сети, иногда у стека заканчивается память, какие-то сообщения не доходят. В общем нужно разбираться.
  • Сам код также оставляет желать лучшего - все свалено в кучу, без всякого деления на модули и интерфейсы.
Зачем я вам тогда в таком недоделанном виде показываю? Это учебный проект. Цель - разобраться в технологии, какие функции за что отвечают, какие события бывают и как их нужно обрабатывать. По сути получилась платформа на которой можно экспериментировать или добавлять необходимый функционал Именно в этом и ценность такого проекта.Планы на будущее
  • Реализовать подключение к сети по кнопке, а также процессы связанные с выходом из сети
  • Разобраться с обновлением прошивки по воздуху (OTA)
  • Разобраться с рандомными глюками устройства
  • Разобраться с неравномерным морганием светодиода (как будто проц кто-то сильно подгружает)
  • Разложить код по компонентам, возможно завернуть в С++ классики с удобными интерфейсами
  • Поискать удобную RTOS, т.к. работа в обработчиках получается слишком запутанной
Буду рад вашей конструктивной критике в комментариях. Возможно вы сможете прояснить моменты, которые недостаточно освещены в статье. Возможно, по мере накопления материала, будет продолжение.Код:  Использованные примеры Документация:
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_detsentralizovannye_seti (Децентрализованные сети), #_programmirovanie_mikrokontrollerov (Программирование микроконтроллеров), #_umnyj_dom (Умный дом), #_diy_ili_sdelaj_sam (DIY или Сделай сам), #_zigbee, #_zigbee2mqtt, #_nxp, #_helloworld, #_detsentralizovannye_seti (
Децентрализованные сети
)
, #_programmirovanie_mikrokontrollerov (
Программирование микроконтроллеров
)
, #_umnyj_dom (
Умный дом
)
, #_diy_ili_sdelaj_sam (
DIY или Сделай сам
)
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 15:35
Часовой пояс: UTC + 5