Zigbee на базе cc2530 от TI

dtvims
Site Admin
Сообщения: 141
Зарегистрирован: Пн авг 02, 2010 2:43 pm

Zigbee на базе cc2530 от TI

Сообщение dtvims » Вт сен 19, 2023 3:40 pm

Цель данного писания разобраться с данной темой максимально. Сделать полнофункциональный проект и научится делать другие. Со мной можно легко связаться многими способами, чтобы задать свой вопрос или дополнить или поругать, что я все не так понял (мой ник уникален в РУ нете).

Развитие систем умного дома, заставляет смотреть какие еще есть системы и устройства и становятся важным аспектом такие параметры как автомность и беспроводная передача данных, в чем определенно лидируют устройства zigbee.
Поиск, что сейчас из чипов доступно, привел меня к чипам от TI CC2530 и библиотеке Z-Stack 3.0, поэтому далее будем разговаривать о них. Можно было бы посмотреть еще куда-то, но пока разумных предложений от предлагателей не услышал. Найти, документацию по теме оказалось не так просто. Модули необходимые делает, например, E-BYTE, как и аналогичные модули ESP8266 или ESP32, с которыми довольно легко работать, потому я не разумно решил, что и тут будет аналогично. Но нет. Модуль модулем, а чип чипом. Документация у TI есть, но разобраться там... Не то чтобы нельзя, но не понятно как с этим работать. Описание функции "Условное действие" содержит в себе только "Условное действие делает условное действие", а зачем оно это делает, для чего совершенно и когда, совершенно не понятно. Вроде есть описание методов API, но как их использовать? Нельзя просто в произвольном месте выполнить метод инициализации, например, группы, т.к. сперва должен прийти некий запрос на ее создание, надо запомнить, что мы в группе и затем отправлять групповые данные. А тут просто сухой перечень функций. И вроде бы ну и ладно, мы что не можем их правильно использовать? Можем конечно, но дублирование нам зачем? Какое дублирование? Так ведь данное API уже все умеет делать и бОльшую часть функционала можно включить, просто добавив нужный #define. После включения нужный функционал заработает почти полностью. Почти? Да, почти. Интересно, что есть функционал полностью сам в себе, а есть тот, для которого надо добавить еще некоторые телодвижения. И вот в этих мелочах самая засада.
Обычно есть статьи, где уже кто-то разобрался и описал, но это опять не наш случай. Информация, конечно, есть, но ее очень мало.
Как в этом разобраться? Будем разбираться и я надеюсь, что у меня это получилось.

dtvims
Site Admin
Сообщения: 141
Зарегистрирован: Пн авг 02, 2010 2:43 pm

С чего начать?

Сообщение dtvims » Вт сен 19, 2023 4:03 pm

Есть проект PTVO, который позволяет сделать прошивку без навыков программирования. Это не для тру экспериментаторов, но если вы пришли сюда чтобы сделать zigbee датчик или релюшечку или кнопочку, то идите туда, а там есть все, что надо. Правда, если захочется много всего в одном, то придется добавить денюжку, чтобы активировать полную версию. Там нельзя будет докидать своей мега логики, а ведь очень хочется, потому наш путь самурая далее!

Начнем вот с этого проекта: https://habr.com/ru/companies/iobroker/articles/495926/
Автор решил переделать беспроводной выключатель/реле, добавив туда датчик температуры. Цель проекта явно не озвучена (а может и озвучина, а я не внимательно читал, но Вы прочитайте внимательно!), но у меня есть аналогичная цель. Вернее я ничего переделывать не хочу, но хочу получить аналогичное устройство, но с блэкджеком и куртизанками.
Итак: Прочитали статью по ссылке? Внимательно? НЕТ? А надо внимательно! Дело в том, что там много важных моментов. Конечно остаются вопросы, на которые я далее попробую ответить.

End Point - определяет устройство. Самое непонятное сперва, а зачем оно надо? У нас одно устройство и зачем несколько EP как несколько разных устройств? А все потому, что есть Кластеры! Кластер одного типа может быть только один на одно устройство. Т.о. если мы используем кластер температуры для одного датчика, то как добавить второй? В этот же кластер нельзя! Добавить новый такой тоже нельзя! Добавить еще одну EP можно и поместить туда второй датчик. А что такое атрибуты? это просто перечень параметров, собранных в кластер. Например, у датчика температуры есть атрибут в котором его значение будет, есть минимальное значение, максимальное и версия кластера. Зачем столько атрибутов? Надо обращаться в первоисточник стандарта zigbee, но мне лень.
Только ли датчиками и кнопками мы сыты?
Есть в стандарте куча моментов, про которые необходимо знать, чтобы понимать, какое устройство должно получить в итоге. Зачем реализовывать функционал, если он уже есть в стандарте и реализован за нас?
Touchlink - интересная тема, но как использовать не очень понятно и тем более, а надо ли вообще, но почему она есть в целом понятно. Механизм позволяет взаимодействовать двум устройств напрямую без координатора и образования сети. Устройства обмениваются сертами и живут сами между собой. Т.е. если мы не хотим заморачиваться с координатором, покупаем умную лампочку и выключатель, связываем их между собой по этой технологии и все! Выключатель теперь связан с лампочкой и будет ее вкл и выкл. Удобно? И да и нет. На мой взгляд, если так заморачиваться, то необходимо иметь более полный контроль с удаленным доступом и возможно с доп. автоматизацией, что не возможно без организации полноценной сети, а значит не вижу смысла рассматривать эту технологию. Ну, в смысле, я лично не хочу этим заморачиваться.
OTA - интересная технологи обновления прошивки по воздуху. Возможно далее мы к этому придем. Не сказать, что прям так нужно, т.к. устройства zigbee обычно довольно простые и просто обновлять там нечего, но опять же меня лично совсем простые устройства не интересуют, потому эта функция может оказаться актуальной.
Groups и scenes. Группы и сцены. Последнее, наверное как сценарии. Объединяем несколько устройств в группы и работаем со всем одновременно. Сцены что-то вроде аналогичного. Пока детально не разбирался, т.к. в текущую задачу не очень входит. Смысл сего, например, объединить несколько лампочек в группу и включать не по одной лампочке, а сразу группу. Не буду пока на этом останавливаться.
Binds - связывание (потом будет точнее). Под этим сперва мы видим связывание нашего устройства с координатором, но нет. Вернее да, но нет. Связывать с координатором необходимо устройство, чтобы оно вошло в сеть, но есть возможность связать устройство не только с координатором, но и с любым другим устройством. Что происходит обычно? Мы нажали кнопку, она оправила репорт по кластеру OnOff координатору, а он ... А он ничего. Если это устройство от какого либо производителя группы устройств, то он определяет дальнейшую логику, имеет на борту некую ОС, подключается через интернет к облаку, через которое мы можем задать правила, по которым этот координатор будет действовать. Или координатор - это наш свисток, включенный в комп, или распбери или еще в какой аналог компа, на котором есть ОС с сервисом умного дома, в котором мы настраиваем правила и согласно этим правилам даем команду координатору, что-то сделать. А что будет, если координатор отключится? А ничего не будет, от слова совсем... Не будет никакой дальнейшей логики, а иногда надо. Например, у меня на системе работает приложение Термостат. Термостат получает данные от датчика температуры и дает команду на реле включить или выключить обогреватель. Если не будет координатора, то кто будет руководить этим процессом? Никто, процесс остановится в том состоянии, которое было последним. Если вы дома, то все ок, а если нет, то кто выключит обогрев, если он остался включен? Правильно - никто. И тут нам приходит на помощь Связывание! Мы можем напрямую, поверх организованной сети, связать 2 устройства, т.о. команда на включение будет отправляться не только на адрес координатора, но и на адрес самого конечного управляемого устройства. Нам не надо чтобы ОС умного дома включала устройство, если оно может включиться само, а координатор тут нужен только чтобы сообщить ОС как там все работает, и если захотим, вмешаться в процесс. Кто-то заметит, что это все можно реализовать в одном устройстве и не париться! Правильно, что я и собираюсь сделать в итоге, но сама тема мне показалась очень интересной, потому продолжим! Связать между собой можно только однотипные кластера, т.е. OnOff только с OnOff. Получается, что включить реле просто датчик температуры не сможет, а вот устройство термостат уже сможет, потому как в нем есть и кластер термостата и кластер OnOff и кластер температуры, хотя реле он может в себе не содержать. Мы можем такой термостат повесить на стенке, а реле будет на обогревателе. Конечно, если само реле будет иметь в себе все что необходимо, то сможет работать вообще автономно, даже без сети, но ведь сама возможность... Я уже молчу, что измерять температуру надо не на обогревателе, а в комнате.

dtvims
Site Admin
Сообщения: 141
Зарегистрирован: Пн авг 02, 2010 2:43 pm

Что будем делать?

Сообщение dtvims » Вт сен 19, 2023 4:10 pm

Что лично я для себя вижу в конечном устройстве? Несколько реле. Зачем? Ну, пока это проба, я могу на одном устройстве сразу и свет включать и обогреватель. Датчик(и) температуры. Зачем их много? Ну, один может быть в одной комнате, другой в другой. Разные датчики в разных комнатах, конечно лучше делать без проводов, но если есть физическая возможность оставить провод, то можно оставить провод :).

Вообще, что-то из описанного выше можно реализовать иначе и нам в пример идет проект https://github.com/formtapez/ZigUP. И вроде бы да, но снова нет... не надо так делать, но если очень хочется, то можно :) Что не так? В проекте ZigUP реализованы несколько реле и датчиков, но для всех были заведены собственные Кластеры. А что так можно? Ну да! Стандарт не запрещает. Есть огромный диапазон значений для идентификации пользовательских кластеров, которые можно идентифицировать по аналогии с существующими в стандарте. Главный недостаток, что координатор должен уметь понимать эти кластера. Для личной автоматизации это может сгодиться или для автоматизации внутри одного производителя. А вот универсальность уже страдает, т.к. мы выходим за рамки стандарта (хотя ведь стандарт это позволяет... Ну в общем). Все сказанное не в обиду автору ZigUP, проект хороший!
Проект ZigUP тоже интересен для общего образования, но он закрывает некоторую локальную цель, и стоит его рассматривать на предмет как сделать быстро, что надо чтобы работало и сойдет. Я же не хочу отходить от стандарта. Например, в Home Assistant недавно была добавлена интеграция zigbee (не путать с плагином zigbee2mqtt), которая сразу видит стандартные кластеры устройств и определяет, их предназначения, все за рамками стандарта будет вызывать проблемы. Конкретно в интеграции HA пока не очень понятно как работать с кастомными нестандартизированными устройствами. В zigbee2mqtt есть возможность описать любое устройство в понятный вид, но эта задача может оказаться также довольно сложной. Вот мы и делаем выбор между "оно само" и "ща мы тут допилим".

Я считаю очень хорошим проектом для обучения https://github.com/grafalex82/hellozigbee. Автор проделал огромную исследовательскую работу, но для чипов NXP. Тут у всего свое начало. В проекте diyruz_rt цель была модернизировать готовое устройство на cc2530, а проект hellozigbee для модернизации устройств от xiaomi, что используют чипы NXP. Общее в этих проектах - это zigbee. Zigbee - это стандарт и значит, если что-то должно быть по стандарту, то для всех оно будет одинаково работать, разве что API отличается. Из того что я понял при изучении проекта hellozigbee, что API от TI проще (хотя многие утверждают обратное), а документации у всех не очень подробная. Доки из hellozigbee рекомендую к обязательному прочтению! Полезно. Жалко статьи на хабре удалили, но интернет их помнит, можно найти их копии, но и в Гите есть почти тоже самое (переводчик в помощь, тем более, что браузер это может сделать одной кнопкой).

Еще мне попался вот такой ресурс www.kancloud.cn/aiot/zigbee. Если получится скачать материалы, то добавлю вложение или ссылку на простое скачивание. Опять перевод в браузере очень выручает. Неплохой набор статей.

dtvims
Site Admin
Сообщения: 141
Зарегистрирован: Пн авг 02, 2010 2:43 pm

Начнем?

Сообщение dtvims » Вт сен 19, 2023 4:29 pm

Сперва возьмем проект diyruz_rt. Надо для него обзавестись всем необходимым.
Usb zigbee Dongle - свисток координатор. Донгл можно использовать и как снифер, но я его подключил к Home Assistant (далее просто HA). В HA правда оказалось есть нюансы. Есть интеграции zigbee, но там не все можно, а может и все, но мы не знаем как, а если бы знали, но не знаем. Есть плагин zigbee2mqtt собственно и плагин mqtt сервера, но чтобы плагины можно было подключать через специальный для того раздел, нужна версия HA со встроенным супервизардом, т.е. версия supervised. Нужную версию можно установить только на Debian, на распбери как самостоятельную ОС или как виртуалку, где тоже будет самостоятельная ОС (если правильно понимаю, то самостоятельная ОС - это Дебиан только купированный, чтобы там оставить только самое необходимое).
Отладочная плата с программатором, см. инструкции по diyruz_rt (https://habr.com/ru/companies/iobroker/articles/495926/), там есть картинки, купить можно алиэкспрес или уже на агрегаторах типа Озона попадаются нужные устройства, правда по цене несколько выше.
Z-Stack 3.0 - качаем данное API с сайта TI или откуда еще, если неохото мучаться :)
Устанавливаем IAR 8051. Если версия будет последней, то придется немного поплясать с бубном, ну совсем чуток, так что не страшно, все будет работать. Да система платная, но все решается.
В папке, где Z-Stack ищем папку проектов и там же в эту папку, рядом с другими проектами кладем проект diyruz_rt. Структура папок должна быть аналогичной, чтобы пути подтянулись и не пришлось много менять в настройках.
Открываем IAR и из него открываем проект из папки DIYRuZRT/CC2530DB/. Он предложит обновить версию проекта, что ни к чему особо не обязывает, потому пускай.
Если версия новая, то придется еще найти файл $PROJ_DIR$\..\..\..\Tools\CC2530DB\f8w2530.xcl (ну это все там же где Ваш проект, куда мы Z-Stack распаковали) и в конец надо добавить строчки:

Код: Выделить всё

//
//  Device specific symbol definitions
//  ==================================
-D?B=F0                       // B register location
-D?IE=A8                      // Interrupt Enable register location
-D?IP=A9                      // Interrupt Priority register location


Вот... просто если этого не сделать, то будет ошибка типа "Undefined external "?B" referred in BindingTable" и другие.

Слева у вас список файлов проекта, а над ними выпадающий список из 3-х пунктов: CoordinatorEB, RouterEB и EndDeviceEB. Возможно будет еще RouterZLight - не знаю, что он там делает :).
Так вот это сгруппированные настройки компилятора и линкера нацеленные на определенный вид устройств. Координатор у нас есть и еще один нам не нужен. Конечное устройство - это обычно устройство на батарейках, которое бОльшую часть своей жизни спит, чтобы не тратить энергию, т.е. какой-нибудь датчик. Датчик он что? Он просыпается иногда, отправляет показания и дальше спать. Что еще от него надо? А ничего. По наблюдениям за датчиком от xiaomi, да и sonoff вроде также работает, он просыпает, получает данные от датчика и если они изменились, то только тогда он их передает координатору. Разумно? Зачем тратить энергию на обновление данных, если они не менялись. Мне не очень нравится этот подход, т.к. графики становятся не красивыми и не точными, зато батарейка экономится. А для выключателей, где есть реле, обычно не требуется экономить энергию. Да и сами реле могут требовать постоянного запита, потому такие устройства могут быть постоянно в работе, т.е. в сети, а значит делать дополнительную полезную функцию как в колхозе! Наше устройство может быть роутером. Автор diyruz_rt задается вопросом, работает ли его прошивка как роутер, вроде подключения принимает, а далее все... Судя по всему, если выбрать конфигурацию RouterEB, то работает и ничего дополнительно делать не надо. У меня устройство подключенное через него вполне передавало данные координатору, делаем вывод, что уже все работает.
Выбираем пункт RouterEB, далее я буду говорить только про него, если кому нужны другие виды, то в них необходимо проделать те же действия. Выбираем свойства проекта, идем в настройки линкера, там видим предопределенные #define`ы, которые мы убираем и в поле выше, надо прописать файлик Source/preinclude.h с полным или относительным путем. Автор именно туда перенес все дефайны, ну и мы их будем откидывать туда.
Пробуем компилировать проект. Если сделать полный ребилд, то будут попытки откомпилировать все версии проекта, а если мы их не настраивали, то будет куча ошибок. Будут созданы соответствующие папки, куда будут помещены откомпилированные сущности. Нас интересует только RouterEB.
Совсем забыл!!! https://habr.com/ru/companies/iobroker/articles/495926/ - тут описаны еще несколько настроек компилятора, и если вы их не сделали, то плохо читали. В общем идите и читайте заного.
Получили мы наконец файлик с расширением .hex. Вот наша прошивочка и мы будем ее тестить.
Нам теперь нужен прошивальщик (прога): SmartRF Flash programmer v1. Именно первой версии. Можно, конечно, и взять вторую версию, но она не увидит cc2530, а потому не понимаю, зачем он вам сейчас :)
Если вы правильно соединили отладочную плату и программатор, а также подключили все это к компу, то SmartRF увидит наше устройство, хотя называться он будет не как сс2530, но увидит в общем. Выбираем нашу прошивку .hex и нажимаем кнопочку в самом низу, такую большую длинненькую. Там поскачут всякие полосочки, заморгают надписи, а в итоге получим что-то типа success. Поздравляю, мы закончили начало.
Если у Вас тоже HA или zigbee2mqtt, то открываем последнюю, разрешаем спаринги, держим кнопочку на плате долго (только не надо жать на reset и говорить, что не работает), пока не загорится светодиодик. zigbee2mqtt сообщит, что с ним кто-то спарился и определит его как проект diyruz_rt. Да его туда внесли официально. И можно понажимать кнопочки, как в HA, так и на плате и смотреть как положение выключателя меняется и там и там. Если вы еще и датчик температуры подключите, то у вас будет и температура отображаться. У меня ds18b20 под рукой не нашлось, зато есть DHT22, где температура и влажность. Далее продолжим модернизацию и заменим один датчик на другой, добавим влажность и многое другое...

З.Ы. вложения требуют переименования.
Во вложении Z-Stack 3.0.2.zip - на самом деле Z-Stack 3.0.2.exe - оригинальный файл от TI
bdb_reporting.zip - это bdb_reporting.с - скачан с сайта TI как ремонтированный, но на самом деле ремонтировать его надо иначе.
TexasInstruments.zip - тут уже zip архив, содержимое соответствует Z-Stack 3.0.2 с тестовыми проектами, которые тут везде упоминаются.
Вложения
bdb_reporting.zip
(80.51 КБ) 1111 скачиваний
TexasInstruments.zip
(25.14 МБ) 1143 скачивания
Z-Stack 3.0.2.zip
(22.64 МБ) 1148 скачиваний

dtvims
Site Admin
Сообщения: 141
Зарегистрирован: Пн авг 02, 2010 2:43 pm

Подключаем UART и printf к cc2530

Сообщение dtvims » Ср сен 20, 2023 9:25 pm

Я думаю, что в данном случае это будет простое упражнение.
Вообще в API уже все есть.
В preinclude добавим:

Код: Выделить всё

#define HAL_UART TRUE
//#define HAL_UART_ISR 1
#define HAL_UART_DMA 1


Тут просто разрешаем/активируем работу UART, для чего будут инициализированы в соответствующее состояние соответствующие регистры.
Также активируем режим DMA. На самом деле, конечно, это все делается в библиотеке hal_uart, но данные директивы это разрешают :)
DMA тут нам помогает, в том, чтобы меньше тратить время CPU на перекладку данных в UART. Это правда в теории, т.к. DMA помогает в больших объемах данных. А что там у нас? Ну что-то есть.
Есть еще вариант ISR, что работает на основе прерываний, но этот механизм уже вешается на CPU. Правда это относится только в приему данных, в то время как DMA может данные перекладывать за нас в обе стороны. Но это в теории. Опять так? Надо просто копаться как это в реальности реализовано, а мне лень. Работает: и так, и так. Мне нравится DMA. Там борту 2 UART`а, потому на оба нельзя будет использовать DMA (скорее всего, а может и можно, кто знает расскажите всем), но можно использовать для одного DMA, а для другого ISR. В общем HAL_UART_ISR я оставил, чтобы помнить, что оно там есть.

в основном файле добавим:

Код: Выделить всё


...

#include "hal_uart.h"
#include <stdio.h>

...

void initUart0(halUARTCBack_t pf);
void uart0RxCb( uint8 port, uint8 event );

...

__near_func int putchar(int c)
{
  HalUARTWrite( HAL_UART_PORT_0, (uint8 *)&c, 1);
  return(c);
}

void initUart0(halUARTCBack_t pf)
{
  halUARTCfg_t uartConfig;
  uartConfig.configured           = TRUE;
  uartConfig.baudRate             = HAL_UART_BR_115200;
  uartConfig.flowControl          = FALSE;
  uartConfig.flowControlThreshold = 48;
  uartConfig.rx.maxBufSize        = 128;
  uartConfig.tx.maxBufSize        = 128;
  uartConfig.idleTimeout          = 6;
  uartConfig.intEnable            = TRUE;           
  uartConfig.callBackFunc         = pf;
  HalUARTOpen (HAL_UART_PORT_0, &uartConfig);
}

void uart0RxCb( uint8 port, uint8 event )
{
  uint8  ch;
  while (Hal_UART_RxBufLen(port))
  {
    // Read one byte from UART to ch
    HalUARTRead (port, &ch, 1);
  }
}


"..." - надеюсь, что все понимают, что на этом месте просто какой-то другой код? Просто все ставится в разные уголки и не хочется на этом слишком заострять внимание. Все будет в готовом проекте, где-нибудь в конце, если, конечно он заработает как надо :) Да ладно, данные куски уже работают :).

Тут объявим 2 функции initUart0 и uart0RxCb - они необходимы как пользовательская инициализация порта. Вообще объявлять их правильно будет в файле ".h", но я человек не правильный и считаю, что там они не нужны никому, пусть будут в основном файле.

Если мы хотим использовать printf, то необходимо создать и вот такую функцию: putchar. Собственно она тянется в stdio.h, в которой и определяется работа printf.
Функция HalUARTWrite записывает в буфер на отправку массив данных для указанного UART порта. Тут используется отправка одного байта. Честно говоря не очень понимаю, зачем в putchar используется тип int, если речь идет только об одном байте. Возможно это тянется из 8-ми биток, но для этого есть однозначный тип uint8. В общем ...
Сделали и прием и отправку данных. Я буду только отправлять, потому про прием данных тут пока ничего не будет.

А в функцию zclDIYRuZRT_Init того же файла добавим:

Код: Выделить всё

  HalUARTInit();
  initUart0(uart0RxCb);
  printf("UART0 Init Done\r\n");


Тут инициализируем HAL, инициализируем порт на нужных нам скоростях и тестируем printf. После заливки прошивки в МК, подключаемс я к COM порту, на котором висит наша отладочная плата (не отладчик) и видим, что он нас приветствует при старте или рестарте фразой "UART0 Init Done".
Я наставил еще принтов на разные события, чтобы понимать что происходит и когда. Не рекомендую просто ставить в loop, т.к. он будет принтовать не просто все события, а даже их отсутствие, потому лучше принтовать только что-то более конкретное, т.е. именно когда есть события. Хотя это право каждого, где и зачем ставить принты.

dtvims
Site Admin
Сообщения: 141
Зарегистрирован: Пн авг 02, 2010 2:43 pm

Кластеры, Конечные точки, репорты (отчеты).

Сообщение dtvims » Пт фев 14, 2025 9:14 am

Решил менять электрический счетчик и конечно на такой, с которого можно снимать показания по RS485, оптимально некий Меркурий.
На него нашлось 2 проекта DIY одного автора, один для ESP8266, а второй для zigbee на cc2530. Наверное желание доразобраться с последним и склонило именно к выбору второго.
https://github.com/Bacchus777/Mercury - сам проект.
Проект с самого начало был в концепте Шредингера, в суперпозиции: одновременно не работал и работал.
Как заметили в комментариях к описанию проекта https://habr.com/ru/articles/803389/: как написать прошивку? Также как нарисовать сову!
К схемотехнике вопросов не нашлось, но как и бывает в таких проектах, автор больше по электронике, чем по программированию, ну или ему совсем было лень довести проект до ума и описать проблемы, которые надо решить для повторения. Пользуясь случаем, разберемся в проекте, и решим все или почти все проблемы проекта и даже zstack полечим, ну или почти. Всё далее...

Итак.

Код: Выделить всё

CONST zclAttrRec_t zclApp_Attrs_FirstEP[] = {
... Пропущено описание кластера BASIC

    {ELECTRICAL, {ATTRID_ELECTRICAL_MEASUREMENT_RMS_VOLTAGE, ZCL_UINT16, RR, (void *)&zclApp_CurrentValues.Voltage}},
    {ELECTRICAL, {ATTRID_ELECTRICAL_MEASUREMENT_RMS_CURRENT, ZCL_UINT16, RR, (void *)&zclApp_CurrentValues.Current}},
    {ELECTRICAL, {ATTRID_ELECTRICAL_MEASUREMENT_ACTIVE_POWER, ZCL_INT16, RR, (void *)&zclApp_CurrentValues.Power}},

    {ELECTRICAL, {ATTRID_ELECTRICAL_MEASUREMENT_AC_VOLTAGE_DIVISOR, ZCL_UINT16, R, (void *)&zclApp_Config.VoltageDivisor}},
    {ELECTRICAL, {ATTRID_ELECTRICAL_MEASUREMENT_AC_CURRENT_DIVISOR, ZCL_UINT16, R, (void *)&zclApp_Config.CurrentDivisor}},
    {ELECTRICAL, {ATTRID_ELECTRICAL_MEASUREMENT_AC_POWER_DIVISOR, ZCL_UINT16, R, (void *)&zclApp_Config.PowerDivisor}},
    {ELECTRICAL, {ATTRID_ELECTRICAL_MEASUREMENT_AC_VOLTAGE_MULTIPLIER, ZCL_UINT16, R, (void *)&zclApp_Config.VoltageMultiplier}},
    {ELECTRICAL, {ATTRID_ELECTRICAL_MEASUREMENT_AC_CURRENT_MULTIPLIER, ZCL_UINT16, R, (void *)&zclApp_Config.CurrentMultiplier}},
    {ELECTRICAL, {ATTRID_ELECTRICAL_MEASUREMENT_AC_POWER_MULTIPLIER, ZCL_UINT16, R, (void *)&zclApp_Config.PowerMultiplier}},
   
    {TEMP, {ATTRID_MS_TEMPERATURE_MEASURED_VALUE, ZCL_INT16, RR, (void *)&zclApp_Temperature}},
};


Вот описание первой конечной точки.
Для понимания, что мы конечной точкой объединяем несколько типов данных, т.е. кластеров, а именно Basic, Electrical и Temp. Названия кластеров тут сокращены, а в итоге использованы не все, что заготовлены. Возможно автор экспериментировал как и я далее.

Код: Выделить всё

#define BASIC         ZCL_CLUSTER_ID_GEN_BASIC
#define GEN_ON_OFF    ZCL_CLUSTER_ID_GEN_ON_OFF
#define POWER_CFG     ZCL_CLUSTER_ID_GEN_ON
#define TEMP          ZCL_CLUSTER_ID_MS_TEMPERATURE_MEASUREMENT
#define HUMIDITY      ZCL_CLUSTER_ID_MS_RELATIVE_HUMIDITY
#define PRESSURE      ZCL_CLUSTER_ID_MS_PRESSURE_MEASUREMENT
#define ELECTRICAL    ZCL_CLUSTER_ID_HA_ELECTRICAL_MEASUREMENT
#define SE_METERING   ZCL_CLUSTER_ID_SE_METERING


у каждого кластера могут быть, а могут не быть атрибуты, например ATTRID_ELECTRICAL_MEASUREMENT_AC_VOLTAGE_DIVISOR - это название атрибута или свойства, которое может менять свое значение. Вообще, если подумать, zstack не фиксирует атрибуты как константы и они могут меняться в любой момент, но есть правила, согласно которым, что-то можно менять, а что-то нет и "ZigBee Cluster Library Specification" в деталях описывает это, но не так, что у вас это не получится обойти, а просто как стандарт, который надо соблюдать, если хотите, чтобы вас поняли. Сам стандарт нацелен на устройства умного дома, т.е. как универсальный протокол по обмену состояниями между умными устройствами, где есть атрибуты и виды устройств (кластеров) на все случаи жизни, а если не на все, то есть универсальные, о чем немного позже. На основе этого, видимо, и получился такой проект как PTVO, в котором можно просто быстро построить простое устройство (интересно а там все проблемы решены?). Разработчики PTVO не делятся секретами, а мы попытаемся разобраться в том, что еще не очень понятно из ранее прочтенного.

Так вот, добавление нового свойства для нашего устройства, если совсем примитивно будет выглядеть так:
1. Придумываем код кластера для которого будем накидывать атрибуты
2. Придумываем коды атрибутов
3. Для каждого атрибута задаем тип данных
4. Для каждого атрибута задаем его свойства доступа (может это и не обязательно, если мы вот так абстрагируемся)
5. Указываем адрес переменной, откуда будут браться данные и куда они будут помещаться.

Записываем, что напридумывали в виде:

Код: Выделить всё

    {МойКластер, {Атрибут1, ZCL_UINT16, БитовоеСвойствоПравДоступа, (void *)&ПеременнаяСДанными1}},
    {МойКластер, {Атрибут2, ZCL_UINT16, БитовоеСвойствоПравДоступа, (void *)&ПеременнаяСДанными2}},


Это абстрагирование, чтобы понимать как это заполняется, т.е. просто заполняем такие комбинации, главное, что в одной конечной точке может быть только одна комбинация МойКластер-Атрибут1!
Если у меня несколько одинаковых чего-то в одном, то надо заводить 2-ю конечную точку. Тут конечная точка как одно базовое устройства и если мы несколько одинаковых устройств объединяем на одно железо, то необходимо завести столько конечных точек, сколько простейших устройств мы определяем.
Если мы делаем сложное устройство, то как нам его описать? Разбить на более простые. Если у нас метеостанция и есть два датчика температуры (на улице и дома), то один датчик мы разместим в первой конечной точке, а второй во второй. Аналогично, если у нас многоклавишный выключатель, то мы размещаем кластер OnOff на каждую кнопку в отдельной конечной точке.

Теперь взглянем на "ZigBee Cluster Library Specification"!
Мы хотим сделать передачу показаний электросчетчика. Ищем, что-то подходящее в спецификации и находим "10.4 Metering", где написано, что это как раз измерители потребления электричества, топлива, воды...
"10.4.1.3 Cluster Identifiers" - раздел описывает код кластера.
0x0702 - Metering (Smart Energy)
На основании этого кода мы и создаем константу или ищем готовую в библиотеке (почти все есть в библиотеке, но иногда надо добавить свои).

Код: Выделить всё

#define SE_METERING   ZCL_CLUSTER_ID_SE_METERING

Данная строка в нашем проект, просто задает короткое имя для константы ZCL_CLUSTER_ID_SE_METERING, определение которой есть где-то там в библиотеке и будет иметь значение 0x0702.
Далее смотрим атрибуты.
Вообще кластер интересный с точки зрения того, что там какое-то огромное количество атрибутов, для чего задействованы 2 байта кодирования, для удобства. Один байт определяет подгруппу атрибутов, а второй сам атрибут. Но это сугубо для удобства восприятия нумерования.
Первый байт "Attribute Set Identifier" он имеет 9 значений и описаны они в общей табличке (домашнее задание найти это в доке и сопоставить с написанным).
Нам интересен пункт "0x01 - TOU Information Set".
Переходим в раздел "10.4.2.2.2 Summation TOU Information Set", где табличка с атрибутами.

Код: Выделить всё

0x0000 CurrentTier1SummationDelivered uint48
0x0001 CurrentTier1SummationReceived uint48
0x0002 CurrentTier2SummationDelivered uint48


Мы будем читать их так, что они относятся к группе "0x01 - TOU Information Set", а значит их коды должны выглядеть так:

Код: Выделить всё

0x0100 CurrentTier1SummationDelivered uint48
0x0101 CurrentTier1SummationReceived uint48
0x0102 CurrentTier2SummationDelivered uint48


Для примера из самой первой группы "0x00 Reading Information Set"

Код: Выделить всё

0x0000 CurrentSummationDelivered uint48
0x0001 CurrentSummationReceived uint48


Их коды такие и останутся. В коде просто ставим значение первого байта (если читать слева на право) как значение группы.

ВНИМАНИЕ!!! Обычно такой группировки нет и код атрибута - это сразу код атрибута, тут просто их так пронумеровали по группам с "простым" понимаем (кто-то может с этим не согласиться).

Код: Выделить всё

#define ATTRID_SE_METERING_CURR_SUMM_DLVD         0x0000
#define ATTRID_SE_METERING_CURR_TIER1_SUMM_DLVD   0x0100
#define ATTRID_SE_METERING_CURR_TIER2_SUMM_DLVD   0x0102
#define ATTRID_SE_METERING_CURR_TIER3_SUMM_DLVD   0x0104
#define ATTRID_SE_METERING_CURR_TIER4_SUMM_DLVD   0x0106


Вот мы задали атрибуты в константы, которые будем использовать.
Первый атрибут для суммы показаний по всем тарифам и далее 4 разных тарифа. Мне известны 1 тарифные 2 и 3 тарифные счетчики, но там реально можно настроить и 4, а наш кластер позволяет их вообще 15 штук задать, но нам не надо, а мы и не будем.
Заметим, что там на каждый тариф по два значения "Delivered" и "Received", ну это для не только умных домов, а для очень умных, которые, например, солнечными панелями питаются и нехватку электроэнергии берут из сети, а излишек (если сами не использовали, что получили) отдают обратно в сеть и вот они 2 параметра: получил и отдал.

Вот так мы себе отобрали 5 параметров.

Также там описан тип данных uint48, т.е. 6 байт. Необходимо использовать именно такой тип, чтобы нас поняли.
Открою очевидную вещь (Спасибо КЭП): по коду атрибута мы понимаем, что его значение занимает 6 байт, значит следующие 6 байт - это данные. Именно так, наше устройство сможет понять кто-то другой. Если нет соответствия стандарта, то никто понять не сможет, ну по крайней мере интуитивно. Некоторые производители, чтобы использовали только их комплекс устройств, могут изменить стандарту, специально чтобы их не поняли. Есть, конечно, еще конфигурация, которой устройство может делиться и само рассказывать о себе, что поддерживает, но видимо это не достаточно хорошо работает.

Далее в табличке описания атрибутов значения:
Range - диапазон допустимых значений (может быть не задан)
Acc - доступ к переменной, где R только чтение, т.е. само устройство его задает, а изменить из вне его нельзя. W - запись, что можно присылать значение из вне.
Def - значение по умолчанию
M/O - Mandatory or Optional - Обязательные или опциональный соответственно.

Вот так мы добавим опции:

Код: Выделить всё

CONST zclAttrRec_t zclApp_Attrs_SecondEP[] = {

    {SE_METERING, {ATTRID_SE_METERING_CURR_SUMM_DLVD,       ZCL_UINT48, RR, (void *)&zclApp_Energies.Energy_T0}},
    {SE_METERING, {ATTRID_SE_METERING_CURR_TIER1_SUMM_DLVD, ZCL_UINT48, RR, (void *)&zclApp_Energies.Energy_T1}},
    {SE_METERING, {ATTRID_SE_METERING_CURR_TIER2_SUMM_DLVD, ZCL_UINT48, RR, (void *)&zclApp_Energies.Energy_T2}},
    {SE_METERING, {ATTRID_SE_METERING_CURR_TIER3_SUMM_DLVD, ZCL_UINT48, RR, (void *)&zclApp_Energies.Energy_T3}},
    {SE_METERING, {ATTRID_SE_METERING_CURR_TIER4_SUMM_DLVD, ZCL_UINT48, RR, (void *)&zclApp_Energies.Energy_T4}},

    {SE_METERING, {ZCL_ATTRID_CUSTOM_DEVICE_ADDRESS, ZCL_UINT32, RW, (void *)&zclApp_Config.DeviceAddress}},
    {SE_METERING, {ZCL_ATTRID_CUSTOM_MEASUREMENT_PERIOD, ZCL_UINT16, RW, (void *)&zclApp_Config.MeasurementPeriod}},
};


Тут это описание второй конечной точки.
Не забудем ее описать в конфигурации, а также перечислить все кластера которые используются в данной EP

Код: Выделить всё

const cId_t zclApp_InClusterList_SecondEP[] = {SE_METERING};


По хорошему это все что надо чтобы можно было работать с данными параметрами. Я к тому, что достаточно их описать и остальное сделает zlib за нас, не все конечно, но работать что-то уже будет. Например, после добавления устройства в сеть мы сможем увидеть что у него есть кластер SeMetering, а (если смотреть в zigbee2mqtt) в разделе для разработчика, можно выбрать 2-ю конечную точку, кластер и свойство, и, нажав Read, прочитать значение из нашего стройства, какое установлено по адресу, например (void *)&zclApp_Energies.Energy_T0.

dtvims
Site Admin
Сообщения: 141
Зарегистрирован: Пн авг 02, 2010 2:43 pm

ОТЧЕТЫ или bdb reporting!!!

Сообщение dtvims » Пт фев 14, 2025 10:49 am

Автоотправка отчетов должна включаться дефайном константы BDB_REPORTING.
Проект Bacchus777/Mercury использует репорты, но их не активирует или автор никому не сказал, что надо. По крайней мере надо добавить в линкер дефайны "BDB_REPORTING=1".
Еще момент, что репорты плохо работают или даже не хотят работать с данными размером более 32бит, а у нас 48. Надо заставить, потому добавляем еще дефайн "BDBREPORTING_MAX_ANALOG_ATTR_SIZE=8", т.е. мы разрешаем максимум 64бита значения.
В принципе, этого достаточно и если будет событие APP_REPORT_EVT, значит zstack сам найдет комбинацию конечнаяТочка-кластер в собственном списке подлежащих репортам (сам же этот список и построит, что вообще шок для данной либы) и отправит репорт. Репорт будет содержать все атрибуты кластера, если конечно они относятся к отчетным, хотя тут я не нашел логики которая бы прям как-то отделяла одно от другого, в общем будем считать, что все.

Теперь мы получаем периодически все атрибуты и можем получить каждый атрибут по запросу. Только вот есть но, или нет? Или да?!
Вообще отчеты должны отправляться только, когда атрибут поменялся. Большинство устройств, что падают в спячку так и делают. Но в компании TI как-то увидели этот процесс по своему, ну даже очень. Ну, т.е. раз в период отчет отправляется по любому, а вот если параметр изменился, то можно вызвать метод bdb_RepChangedAttrValue и он сам посмотрит, что там поменялось и сразу отправит отчет и пересмотрит время следующей отправки отчета. Но по коду, он нормально определяет изменение параметров только размером до 32бит, а 48 и более сразу считает как НЕ изменившиеся, т.е. ждите плановой отправки! А если атрибут текстовый, ведь есть такой тип String и даже long String, то он считается всегда как: верю на слово - изменился!

Справка!
String - это массив char (uint8) первый байт которого это число символов char, а тип long String - это такой же массив, но там первые 2 байта определяют число символов.


Итак, мы проделали все выше, все заработало, а теперь мы взглянули на данные, а там что-то не то!!!!
Все значения параметров задублировались первым значением.

Z-Stack 3.0.2/Components/stack/bdb/bdb_Reporting.c
Проблема тут! А именно, это баг самой либы. Предполагаю, что bdb раздел появился именно в "новых" версиях стэка "3.0.x" и делали его разные люди, среди которых были опытные, а были не очень...
Поясню!
Есть функция bdb_RepReport(), которая отправляет собственно репорт, но сперва его строит, найдя атрибуты и их значения. Находит значения с помощью функции bdb_RepFindAttrEntry, которая помещает значение найденного атрибута в переменную attrRec, а именно в ее свойство attrRec.dataPtr. attrRec.dataPtr - это указатель на значение.
Функция является своего рода аналогом функции из zlib "zclFindAttrRec()". Фнкция zclFindAttrRec возвращает аналогично в attrRec значения атрибута, а в attrRec.dataPtr будет указатель на то самое значение, что у нас в конфиге (см. выше (void *)&zclApp_Energies.Energy_T0). И разработчик, что писал bdb_RepReport видимо того же или чего-то такого же ожидал от bdb_RepFindAttrEntry, но это не так. Фукнция bdb_RepFindAttrEntry возвращает в attrRec.dataPtr всегда указатель на глобальную переменную gAttrDataValue, куда копирует значение из оригинального dataPtr. В результате этого мы получаем, что все атрибуты для отчета смотрят на один и тот же адрес gAttrDataValue, который будет содержать одно значение, которое и будет отправлено в отчете.

Я нашел 2 варианта решений это проблемы в сети и оба сводились к тому, чтобы выделить область памяти на все атрибуты сразу, вызвать bdb_RepFindAttrEntry и скопировать в подготовленную область памяти значение из ttrRec.dataPtr, и так для каждого атрибута мы собираем длинное значение из значений, записывая адрес, куда мы записали копию в итоговый отчет. Но тут есть одна огромная проблема, если не считать того, что надо выделить память под ВСЕ атрибуты кластера. Если атрибут имеет размер данных более чем BDBREPORTING_MAX_ANALOG_ATTR_SIZE, то какие данные при копировании куда попадут? А я скажу, что при копировании в область 8байт, например 15-ти, мы запишем последние 7 после отведенных нам 8ми байт, т.е. в чью-то чужую память! Результат такого копирования не предсказуем.

Я долго изучал bdb_Reporting.c и пришел к выводу, что bdb_RepFindAttrEntry используется только тут и что глобального смысла именно в копировани значения нет. Почему в attrRec.dataPtr нельзя было передать оригинальный указатель на данные? Чтобы их защитить? От кого? Ну, типа - это же отчет, он только чтение, вот только и читай, а захочешь записать, то ничего не выйдет. Огонь? А кто захочет туда писать? Сама библиотека bdb_Reporting? С учетом общей структуры проекта на Си, в этой заботе о данных нет никакого смысла, кроме как напротив все испортить (кто-то считает иначе? Расскажите!).
Я предлагаю исправить именно bdb_RepFindAttrEntry, которую писал видимо стажер! Почему? А вот! Вот этот шедевр!

Код: Выделить всё

static uint8 bdb_RepFindAttrEntry( uint8 endpoint, uint16 cluster, uint16 attrID, zclAttribute_t* attrRes )
{
  epList_t *epCur = epList;
  uint8 i;

  zcl_memset(gAttrDataValue, 0, BDBREPORTING_MAX_ANALOG_ATTR_SIZE);
  for ( epCur = epList; epCur != NULL; epCur = epCur->nextDesc )
  {
    if( epCur->epDesc->endPoint == endpoint )
    {
      zclAttrRecsList* attrItem = zclFindAttrRecsList( epCur->epDesc->endPoint );
     
      if( (attrItem != NULL) && ( (attrItem->numAttributes > 0) && (attrItem->attrs != NULL) ) )
      {
        for ( i = 0; i < attrItem->numAttributes; i++ )
        {
          if ( ( attrItem->attrs[i].clusterID == cluster ) && ( attrItem->attrs[i].attr.attrId ==  attrID ) )
          {
            uint16 dataLen;

            attrRes->attrId = attrItem->attrs[i].attr.attrId;
            attrRes->dataType = attrItem->attrs[i].attr.dataType;
            attrRes->accessControl = attrItem->attrs[i].attr.accessControl;

            dataLen = zclGetDataTypeLength(attrRes->dataType);
            zcl_ReadAttrData( endpoint, cluster, attrRes->attrId, gAttrDataValue, &dataLen );
            attrRes->dataPtr = gAttrDataValue;
            return BDBREPORTING_TRUE;
          }
        }
      }
    }
  }
  return BDBREPORTING_FALSE;
 }

Что вообще делает эта функция? Она находит сперва конечную точку и затем ищет там нужную комбинацию кластер-атрибут. Если нашла, то заполняет attrRes. Я сперва думал, что поиск идет по списку тех, что для отчета, но нет, он ищет глобально. Т.е. можно заменить функцию на zclFindAttrRec и логика не пострадает. А ведь затевалась функция явно для того чтобы не только найти атрибут, но и сказать для отчета он или нет, чтобы не включать в отчет вообще все поля, а оставить только нужные.
ИНТЕРЕСНО, что значение атрибута можно найти вот тут "attrItem->attrs[i].attr.dataPtr". Но это не наш случай! Зачем идти таким путем?
Мы сперва узнаем длину значения "dataLen = zclGetDataTypeLength(attrRes->dataType);" - весь прикол, что данная функция работает только для простых типов, а для остальных, вроде строк, надо использовать метод такой: "dataLen = zclGetAttrDataLength( pAttr->attr.dataType, (uint8*)(pAttr->attr.dataPtr);". Метод немного другой, потому как это Си. И вот странно, но zcl_ReadAttrData использует именно последнюю функцию определения длины! Что? Что значит использует? Сама? А зачем тогда туда передается dataLen? Обратите внимание, что передается не значение dataLen, а ее указатель, т.е. в эту переменную не передается длина, а наоборот, она от туда берется после выполнения. Эту строку надо читать как: функция zcl_ReadAttrData положила в gAttrDataValue данные в количестве dataLen байт.
Мы приходим к тому, что автор этой функции сам не понимал что делает. Потому исправлять надо именно тут. Ну и волей случая ее не только исправим, но и упростим.

Код: Выделить всё

static uint8 bdb_RepFindAttrEntry( uint8 endpoint, uint16 cluster, uint16 attrID, zclAttribute_t* attrRes )
{
  epList_t *epCur = epList;
  uint8 i;

  zcl_memset(gAttrDataValue, 0, BDBREPORTING_MAX_ANALOG_ATTR_SIZE);
  for ( epCur = epList; epCur != NULL; epCur = epCur->nextDesc )
  {
    if( epCur->epDesc->endPoint == endpoint )
    {
      zclAttrRecsList* attrItem = zclFindAttrRecsList( epCur->epDesc->endPoint );
     
      if( (attrItem != NULL) && ( (attrItem->numAttributes > 0) && (attrItem->attrs != NULL) ) )
      {
        for ( i = 0; i < attrItem->numAttributes; i++ )
        {
          if ( ( attrItem->attrs[i].clusterID == cluster ) && ( attrItem->attrs[i].attr.attrId ==  attrID ) )
          {
            attrRes->attrId = attrItem->attrs[i].attr.attrId;
            attrRes->dataType = attrItem->attrs[i].attr.dataType;
            attrRes->accessControl = attrItem->attrs[i].attr.accessControl;
            attrRes->dataPtr = ttrItem->attrs[i].attr.dataPtr;
            return BDBREPORTING_TRUE;
          }
        }
      }
    }
  }
  return BDBREPORTING_FALSE;
 }

Тут по сути ничего не изменилось. Все работает как и раньше, только в dataPtr адрес самих данных, а не их копии, которую делала zcl_ReadAttrData.
Предположу, что на основании значения attrRes->accessControl необходимо было принять итоговое решение об атрибуте. Например, вот так (код для примера):

Код: Выделить всё

if(attrRes->accessControl & ACCESS_REPORTABLE) {
  return BDBREPORTING_TRUE;
}else{
  return BDBREPORTING_FALSE;
}

Я скажу больше. Автор данного творения не понял, что можно было сделать именно так и использовал метод zcl_ReadAttrData для получения данных, а этому методу необходимо куда-то скопировать данные, для чего и была создана глобальная переменная gAttrDataValue. Ну, а как еще? Только вот переменная gAttrDataValue имеет размер BDBREPORTING_MAX_ANALOG_ATTR_SIZE, а если мы будем читать данные большего размера в нее? Ну в общем понятно, да, что данный метод был изначально мертворожденным? Именно по этому исправлять надо именно его.

Исправляем! Теперь все работает как надо!

dtvims
Site Admin
Сообщения: 141
Зарегистрирован: Пн авг 02, 2010 2:43 pm

Атрибут типа String!

Сообщение dtvims » Пт фев 14, 2025 11:09 am

А что, так можно?
Я взял проект Bacchus777/Mercury для дальнейших эксперментов и что получилось?

Во вторую конечную точку, т.е. в zclApp_Attrs_SecondEP я добавил еще один элемент.

Код: Выделить всё

{GEN_BINARY, {ATTRID_GEN_BINARY_ACTIVE_TEXT, ZCL_DATATYPE_CHAR_STR, RRW, (void *)zclApp_binValText}} 


также в zcl_app.h добавил пару констант:

Код: Выделить всё

#define GEN_BINARY    ZCL_CLUSTER_ID_GEN_BINARY_VALUE_BASIC 
#define ATTRID_GEN_BINARY_ACTIVE_TEXT   0x0004


Нашел я эту информацию все там же, в стандарте "ZigBee Cluster Library Specification", по алгоритму выше.

Поскольку это еще и кластер которого ранее в проекте не было, то надо модифицировать еще две переменных:

Код: Выделить всё

const cId_t zclApp_InClusterList_SecondEP[] = {SE_METERING, GEN_BINARY};
...
const cId_t zclApp_OutClusterList_SecondEP[] = {SE_METERING, GEN_BINARY};


В которые я добавил GEN_BINARY. Они подразумевают, что данный кластер теперь есть в конечной точке 2 для записи и чтения.

Еще в начале самого файлика (это конечно все в zcl_app_data.с) добавим:

Код: Выделить всё

uint8 zclApp_binValText[] = {15, 'M', 'e', 'r', 'c', 'u', 'r', 'y', '_', 'T', 'e', 's', 't', 'v', 'a', 'l'};


Всё!!!
Этого достаточно чтобы это заработало и по данному кластеру оправлялся репорт (отчет) на регулярной основе!

Где либо в коде можем менять значение zclApp_binValText и наблюдать в zigbee2mqtt как меняется данный параметр. Наблюдать, конечно, где-то в системе умного дома, например на базе zigbee2mqtt, который и прочтет нам эти данные, после настройки конвертора (к этому еще придем далее).

dtvims
Site Admin
Сообщения: 141
Зарегистрирован: Пн авг 02, 2010 2:43 pm

Особенности проекта Bacchus777/Mercury и его доработка.

Сообщение dtvims » Пт фев 14, 2025 11:20 am

В самом проекте тоже не все гладко, и очевидно, что по той же причине, что выше лечили, исправляя "bdb_Reporting.c".
Причина все в том же размере данных. Если бы проект был на Си++, то можно было бы прикрутить свой мега тип данных, но у нас Си, в котором НЕТ int64, потому надо колхозить и конкретно.

Данные кластера счетчика лежат в структуре:

Код: Выделить всё

energy_t zclApp_Energies = {
    .Energy_T0 = 0,
    .Energy_T1 = 0,
    .Energy_T2 = 0,
    .Energy_T3 = 0,
    .Energy_T4 = 0
};


где:

Код: Выделить всё

typedef struct {
    uint32 Energy_T0;
    uint32 Energy_T1;
    uint32 Energy_T2;
    uint32 Energy_T3;
    uint32 Energy_T4;
} energy_t;


К сожалению, просто заменить uint32 на uint64 не получится, но очень хотелось.
Единственное что мы можем сделать, это заменить или на массив по uint8 или по uint32. Или не на массив, а структуру.

Код: Выделить всё

typedef struct {
    uint32 lowInt;
    uint32 hiInt;
} myUint64;


ну как-то так.
Описание кластера при этом можно оставить также, пускай указатель смотрит как и ранее на нашу структуру, а вот обращаться, т.е. обновлять значения придется иначе.

Код: Выделить всё

typedef struct {
    myUint64 Energy_T0;
    myUint64 Energy_T1;
    myUint64 Energy_T2;
    myUint64 Energy_T3;
    myUint64 Energy_T4;
} energy_t;

void myUpdateEnergy_T0(energy_t myVar, uint32 newVal){
   myVar.Energy_T0.lowInt = newVal;
   myVar.Energy_T0.hiInt = 0;
}


Функция myUpdateEnergy_T0 чисто для примера, что раньше мы писали число сразу в Energy_T0, а теперь его надо писать в lowInt.
В принципе можно еще поизвращаться вот как-то так:

Код: Выделить всё

*(uint32 *)&zclApp_Energies.Energy_T0 = newVal;


Тут вообще как и было ранее, только с переопределением типа. Но что у нас во втором кусочке hiInt? Туда, в принципе можно за ранее записать 0 и радоваться, но в коде проекта Bacchus777/Mercury автор сперва создает временную переменную структуру, а затем копирует одну структуру в целевую:

Код: Выделить всё

Energies = (*mercury_dev->ReadEnergy)();
...
zclApp_Energies = Energies;


В целом это обосновано, т.к. "..." - тут я опустил проверку на ошибку чтения данных из счетчика, и если есть такая ошибка, то просто останутся старые данные. Тут можно немного порассуждать, надо ли оставлять старые данные или как-то этими же данными показать пользователю, что произошла ошибка, хотя можно для этого использовать какое-то поле статуса, куда помещать код ошибки.

Далее идет строчка:

Код: Выделить всё

bdb_RepChangedAttrValue(SECOND_ENDPOINT, SE_METERING, ATTRID_SE_METERING_CURR_TIER1_SUMM_DLVD);


Она тут не нужна! Или надо подправить метод bdb_isAttrValueChangedSurpassDelta в bdb_Reporting.c, чтобы он для типа ZCL_DATATYPE_INT48 возвращал значение BDBREPORTING_TRUE.
Также заметим, что bdb_RepReport всегда записывает последнее значение, что он отправил в т.ч. для типа ZCL_DATATYPE_INT48, те. метод bdb_isAttrValueChangedSurpassDelta можно доработать и до такого типа, что сделать можно по аналогии с uint32, только 2 раза и если есть изменение в любом, то отвечать, что значение поменялось. Считать дельту там вообще не обязательно. Зачем там это я вообще не понял.

dtvims
Site Admin
Сообщения: 141
Зарегистрирован: Пн авг 02, 2010 2:43 pm

Интересное наблюдение.

Сообщение dtvims » Пт фев 14, 2025 11:30 am

Я выше написал про эксперимент с кластером GEN_BINARY. Внес все исправления в проект и библиотеку zlib. И наблюдал как это все работает.
Опять получил "кота Шредингера".
Нет, всё работает, но есть некоторые странности, которые пока остались не понятными.
Когда вызовы для кластеров SE_METERING и GEN_BINARY метода bdb_RepChangedAttrValue отсутствуют, все работает в рамках стандартных репортов достаточно стабильно. А вот если использовать bdb_RepChangedAttrValue сразу после модификации одного из значений кластера, начинаются странности. То все данные корректно отправляются, а то нет. Я запустил проект в режиме отладки, который есть уже в данном проекте (конфигурация CHDTECH_DEV) и добавил отладочные сообщения в bdb_RepReport чтобы понять, доходит ли до него дело и корректно ли отправляется отчет. И ДА, все проходит каждый раз и доходит до zcl_SendReportCmd, который возвращает успешный результат. Проход через zcl_SendReportCmd - означает, что данные были отправлены. Тут подходит "Пули выпущены, проблема на Вашей стороне". Тут или есть бага где-то еще глубже или на стороне zigbee dongle или на стороне zigbee2mqtt, с которым тоже надо немного поразбираться, о чем будет далее.

На текущий момент я пришел к выводу, что устройство стабильно работает с короткими простыми репортами и, чем их меньше, тем лучше. Очень хочется чтобы одно устройство сразу было, и погодной станцией, и передавало показания счетчика, но стабильнее будет работать, если оно будет делать что-то одно. Есть конечно вероятность, что, добавив кластер GEN_BINARY, я там что-то еще сломал, ведь без него все работало стабильно. Или нет? Без кластера GEN_BINARY работало неверно, т.к. не было правильного исправления bdb_Reporting.c и читался uint48, а расчет памяти был как для uint32. Засада, как оно сейчас-то у меня работает? Рабочее устройство я повторил за автором уже давно, а докопался до сути только сейчас (некогда было). Возможно именно по этому у меня не работал первый UART. Тут надо пояснить, что в прошивках рабочих, первый UART используется для чтения показаний счетчика, а в режиме отладки CHDTECH_DEV на первый UART вешается либа debug.c, а общение с счетчиком переносится на второй UART. Ну вот сперва у меня ничего и не работало вообще. Я думал, что счетчик не исправен. Но подключил счетчик на прямую к компу, мне удалось в нему подключиться и даже разобраться в некоторых командах. Затем я включил режим CHDTECH_DEV, чтобы посмотреть в отладке что не так, а тут все так и все работает. Как? В общем в рабочей прошивке я тоже общение с счетчиком перевел на второй UART. Собственно, зачем так делать? Ну используешь первый для отладки ну используй или не используй только его и всегда, а второй тоже всегда используй для счетчика, тогда можно менять прошивки без необходимости всё переподключать физически. В общем сейчас я думаю, что команда запроса к счетчику портилась с помощью кривой "bdb_Reporting.c", а у автора все работало, потому что он использовал исправление, возможно даже как мое выше, но никому об этом не сказал. Осталось только задать вопрос, почему он не поменял тип с uint32 на аналог uint64. А ведь если не изменить BDBREPORTING_MAX_ANALOG_ATTR_SIZE на 8, то там еще много где будет кривое копирование с залезанием в чужую область памяти. Интересно, что автор использует атрибут CurrentSummationDelivered именно чтобы корректно отправлялся репорт, сам он этот параметр руками рассчитывает во внешнем конверторе zigbee2mqtt. А у меня тоже такой баг был, что если указать в функцию bdb_RepChangedAttrValue с параметром ATTRID_SE_METERING_CURR_TIER1_SUMM_DLVD, то в репорте были все параметры начиная с CurrentTier1SummationDelivered, но если передавать ATTRID_SE_METERING_CURR_SUMM_DLVD - в репорте есть все параметры, а должны оправляться ВСЕГДА ВСЕ (как я уже написал выше, без доработки bdb_RepChangedAttrValue для типа uint48 ничего не делает). Это все косяки неверной работы с типами данных и указателями на них.
В любом случае, используя только кластер SE_METERING, без прилипалы экспериментального GEN_BINARY, да еще и в стандартном репорте, т.е. без вызова bdb_RepChangedAttrValue - работает стабильно!

ВСЁ, написанное выше, является желанием разобраться в zstack от TI и не обязательно абсолютно верным, хотя может звучать логичным.

Справка! Если кто не понял.
bdb_RepChangedAttrValue - по описанию метода, должен отмечать, что данные изменились. На самом деле, если данные изменились, он сразу отправляет репорт, а плановый откладывается до следующего по таймеру.
Я пока не разобрался до конца как плановый репорт по таймеру работает, т.к. там замудреная логика выбора EP-Cluster для репорта.


Вернуться в «Микроконтроллеры и автоматизация»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 4 гостя