Zigbee на базе cc2530 от TI

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

Reporting, его проблемы и особенности.

Сообщение dtvims »

Пост выше - не разобрался до конца, хотя казалось, что все понятно, но почему тогда остались вопросы?

Итак я читал доки, смотрел код, делал свою версию прошивки для счетчика. Пришел в очередной раз к тому, что в целом проект Bacchus777/Mercury сделан достаточно хорошо и я лишь незначительно делаю по своему и многое переношу как есть из оригинального проекта, т.е. хотелось бы переписать лучше, но не могу :(. Однако расследование работы z-lib от TI приводит к тому, что все работает не совсем так.

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

bdb_RepChangedAttrValue(FIRST_ENDPOINT, ELECTRICAL, ATTRID_ELECTRICAL_MEASUREMENT_RMS_VOLTAGE);
Вызов данной функции проверяет есть ли изменение параметра в рамках заданного критерия и если есть, то, нет, не запускает, а сбрасывает время очередной отправки отчета на "пора"!
Если посмотреть в том же zigbee2mqtt на закладку "отчеты" в нашем устройстве, то там будет конфигурация этих отчетов. Минимальное время отправки (нельзя отправлять отчет чаще), максимальное время отправки (если это время превысится, то надо отправить отчет обязательно) и критерий (дельта, т.е. значение должно изменится более чем на эту величину).
Так же в документации от TI написано, что параметры репорта, для оптимизации, усредняются до одного значения для всех параметром на кластер конечной точки. Т.о. для всех параметров репорты будут отправляться по одному критерию, но если параметр не менялся, то репорт будет отправлен общему максимальному времени репорта.
Когда функция "bdb_RepChangedAttrValue" вызывается один раз только для параметра "RMS_VOLTAGE", если этот параметр не менялся, то репорта не будет, даже если изменились другие параметры. А чтобы репорт отправлялся наверняка, необходимо bdb_RepChangedAttrValue вызвать для каждого параметра из 3-х (в данном случае), чтобы репорт точно отправился, если изменился любой из параметров.

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

bdb_RepChangedAttrValue(SECOND_ENDPOINT, SE_METERING, ATTRID_SE_METERING_CURR_TIER1_SUMM_DLVD);
В случае же с параметром "CURR_TIER1_SUMM_DLVD" все несколько хуже, т.к. параметр типа ZCL_UINT48. Дело в том, что cc2530 и аналогичные контроллеры являются 8-ми битными и компилятор 8051 не умеет в 64 бита. Нет типа int64, а значит нет ничего более чем int32 (странно что хоть это есть). Для того чтобы сравнить разницу, нужна арифметика на 64 бита, а ее нет. Можно конечно ее прикрутить искусственно, но никто не захотел и просто исключили типы размером более чем 32 из проверки и они считаются как без изменений! И тут опять необходимо или переписывать bdb_reporting или решать проблему обходными путями. Т.е. вызов выше НЕ РАБОТАЕТ вообще. Но сообщения приходят по максимальному времени репорта.
Если посмотрим в предложенный внешний конвертер для zigbee2mqtt:

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

await second_endpoint.configureReporting('seMetering', [{attribute: 'currentSummDelivered', minimumReportInterval: 0, maximumReportInterval: 30, reportableChange: 0}]);
await second_endpoint.configureReporting('seMetering', [{attribute: 'currentTier1SummDelivered', minimumReportInterval: 0, maximumReportInterval: 30, reportableChange: 0}]);
await second_endpoint.configureReporting('seMetering', [{attribute: 'currentTier2SummDelivered', minimumReportInterval: 0, maximumReportInterval: 30, reportableChange: 0}]);
await second_endpoint.configureReporting('seMetering', [{attribute: 'currentTier3SummDelivered', minimumReportInterval: 0, maximumReportInterval: 30, reportableChange: 0}]);
await second_endpoint.configureReporting('seMetering', [{attribute: 'currentTier4SummDelivered', minimumReportInterval: 0, maximumReportInterval: 30, reportableChange: 0}]);
то мы видим, что maximumReportInterval устанавливается в значение 30, т.е. это соответствует желаемому времени отправки репорта по умолчанию "measurement_period" и создается впечатление, что все работает корректно.

Но и это еще не все.

В функции bdb_RepReport, собирается сам репорт вот так:

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

pReportCmd->attrList[i].dataType = attrRec.dataType;
pReportCmd->attrList[i].attrData = attrRec.dataPtr;
//Update last value reported
if( zclAnalogDataType( attrRec.dataType ) ) {
	//Only if the datatype is analog
	osal_memset( attrListItem->data->lastValueReported,0x00, BDBREPORTING_MAX_ANALOG_ATTR_SIZE );
	osal_memcpy( attrListItem->data->lastValueReported, attrRec.dataPtr, zclGetDataTypeLength( attrRec.dataType ) );
}
Где attrRec - данные по параметру, а dataPtr в нем - это указатель на значение.
Если тип данных "аналоговый", то мы его сохраняем, для фиксирования изменений (lastValueReported). Вот только для типов размерностью более 32 это не используется. А патч предлагается народом сводящийся к:

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

MAP_osal_memcpy( dataPtr, attrRec.dataPtr, BDBREPORTING_MAX_ANALOG_ATTR_SIZE );
pReportCmd->attrList[i].attrData = dataPtr;
причем всегда, а ведь типы данных - это не не только эти пресловутые "Аналоговые". Эксперимент выше я делал на строках, а строка имеет размер больше чем BDBREPORTING_MAX_ANALOG_ATTR_SIZE. Как результат получались ошибки - вот он тот конь в вакууме.
Я не помню, на какой версии "bdb_RepReporting.c" я проводил эксперименты, патченной как предложено или по своему варианту, где исправлял только "bdb_RepFindAttrEntry", но вероятнее всего это тоже был кривой патченный файл и именно он был причиной глюков.

А еще, чтобы в репортах получать обновления поля, необходимо этот репорт завести, т.е. просто завести кластер с параметром недостаточно. Надо завести репорт именн для поля, ибо в репорт будут включены только те поля которые заявлены в репорте для данного кластера и конечной точки.

Итого, для нестандартных типов, необходимо или НЕ использовать репорты, а именно отправлять данные принудительно, или, настраивать репорты, но заводить дополнительный параметр в кластере с разрядом 32 или меньше, его так же репортить и делать в нем регулярные изменения, что будет искусственно провоцировать репорт всего кластера. Еще можно самостоятельно вызывать метод "bdb_RepStartReporting( );", чтобы провоцировать репепорт вручную.

Из более сложных решений: реализовать сравнение типов разрядностью более 32.
dtvims
Site Admin
Сообщения: 145
Зарегистрирован: Пн авг 02, 2010 2:43 pm

Вызов reports zigbee

Сообщение dtvims »

Сделал чистую библиотеку, с предложенным мной же исправлением bdb_RepReporting.c - все работает как и задумано.
Надо доработать для типа ZCL_DATATYPE_UINT48 расчет изменений параметра, для полноты стандарта
Это надо дорабатывать метод bdb_isAttrValueChangedSurpassDelta, в котором расчет делается для разных типов размерностью менее 32 включительно, например:

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

case ZCL_DATATYPE_UINT8:
    {
      uint8 L = *((uint8*)lastValue);
      uint8 D = *((uint8*)delta);
      uint8 C = *((uint8*)curValue);
      if( L >= C )
      {
        res = ( L-C >= D) ? BDBREPORTING_TRUE:BDBREPORTING_FALSE;
      }
      else
      {
        res = ( C-L >= D) ? BDBREPORTING_TRUE:BDBREPORTING_FALSE;
      }
      break;
    }
lastValue - последнее сохраненное значение, что было отправлено репортом.
curValue - текущее значение
delta - значение из настроек репорта, управляется внешней конфигурацией, например в zigbee2mqtt в настройках устройства закладка "Отчетность" (перевод там конечно такой себе) и свойство "Мин интервал отчетов при изменении". Если интервал установлен 5, а предыдущее значение параметра было 10, а новое 13, то репорта не будет, а, когда новое значение станет 15 и более, то будет репорт. Аналогично в меньшую сторону.

Собственно, задача сделать тоже самое, но для типов большей размерности.
С учетом того, что типа int64 у нас нет, придется сделать самостоятельно, но внедрить в чистый Си его красиво не выйдет, потому надо сделать пару методов, которые будут выполнять нужные операции.
Алгоритм давно известен, т.к. числа даже 32-х разрядные мы считаем, а МК у нас всего 8-ми разрядный, значит есть готовые решения, но их надо искать. Потому я решил попросить помочь с этим Искусственный интеллект. За меня он еще прямо вот супер код не напишет, но 80% необходимого сделать точно может и он смог. Я попросил написать в итоге 3 функции: Вычитание, сравнение без знака и сравнение со знаком. Последние 2 функции он сделал идеально (ну легкая коррекция синтаксиса понадобилась), а вот метод для расчета разницы получился недоделанный. Пришлось чутка все же подумать, доделать самому и на выходе получил нужные мне три метода. Мы скармливаем им массивы байт нужного нам размера (я ограничился только 64-ю разрядами, т.е. 8 байт) и получаем нужный результат в том же виде. Операция сравнения тоже сразу дает нужное нам условие. Для вычитания знак значения не имеет, оно автоматически получается как надо, а вод при сравнении знак имеет большое значение.
На основе этих функций я написал нужное мне сравнение и добавил его в bdb_isAttrValueChangedSurpassDelta

Сперва вроде все заработало, потом что-то пошло не так, я добавил отладку и все оказалось так :)

А что не так?
Я ранее писал, что надо создавать расчет репорта для каждого кто может меняться, ну и добавил:

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

bdb_RepChangedAttrValue(SECOND_ENDPOINT, SE_METERING, ATTRID_SE_METERING_CURR_SUMM_DLVD);
bdb_RepChangedAttrValue(SECOND_ENDPOINT, SE_METERING, ATTRID_SE_METERING_CURR_TIER1_SUMM_DLVD);
bdb_RepChangedAttrValue(SECOND_ENDPOINT, SE_METERING, ATTRID_SE_METERING_CURR_TIER2_SUMM_DLVD);
bdb_RepChangedAttrValue(SECOND_ENDPOINT, SE_METERING, ATTRID_SE_METERING_CURR_TIER2_SUMM_DLVD);
bdb_RepChangedAttrValue(SECOND_ENDPOINT, SE_METERING, ATTRID_SE_METERING_CURR_TIER4_SUMM_DLVD);
Где CURR_SUMM_DLVD - это параметр, который в оригинальном проекте фактически не использовался. Автор добавил его, думая что это поможет с ошибками bdb_RepReporting.c и, наверное, как-то помогло, но неконкретно это. А параметр для суммы учетных данных, собственно я и сделал сумму в этот параметр.

Очевидно, что если CURR_TIER1_SUMM_DLVD увеличился на 1, CURR_TIER2_SUMM_DLVD, тоже увеличился на 1 и т.п., то CURR_SUMM_DLVD увеличится на 4. Неожиданно? Ну вот я делал разницу 3, а репорт все-равно приходил. И только с отладкой я увидел, про что забыл.

Также, если первый вызов bdb_RepChangedAttrValue запускает репорт, т.е. проверка о необходимости делать репорт TRUE, то последующие вызовы bdb_RepChangedAttrValue уже фактически ничего не делают, т.к. первое, что там проверяется: а не установлен ли уже признак, что надо делать репорт и, если установлен, то дальнейшие проверки не нужны.
Таким образом я ожидал, что увижу 5 проверок, а была только одна первая. Поняв эти казалось бы очевидные вещи, я провел еще ряд экспериментов и они все были успешны, устройство работало как и ожидалось.

Как мне показалось, перенастройка репортов может не очень корректно обрабатываться, потому лучше один раз сразу выставить корректные настройки и более к ним не прикасаться, тогда будет работать все стабильно и прозрачно.

Конечно режим отладки очень помогает, я от него долго еще не откажусь, пока не отработаю модификацию bdb_RepReporting.c до приемлемого состояния. Далее, конечно выложу, что получилось.
dtvims
Site Admin
Сообщения: 145
Зарегистрирован: Пн авг 02, 2010 2:43 pm

Еще баги bdb_RepReporting.c

Сообщение dtvims »

Как работает bdb_RepReporting на сс2500 с z-lib 3.0.2.
На редкость забагованая библиотека z-lib 3.0.2. А ведь фирма TI серьезная, продает свои отладочные платы за дорого...
Пришлось поразбираться как работает bdb_RepReporting от TI ввиду того, что, все сделал круто, а не работает.

Все в z-lib работает на событиях и эти события часто таймеры. За это отвечает библиотека OSAL. Есть также CallBack функция, которую часто называют zclSomething_event_loop( uint8 task_id, uint16 events ), которая регулярно вызывается. Там проверяем случившееся событие и на него реагируем, возможно вызывая новые события.
bdb_RepReporting - не исключение. Тут обрабатываются события реконфигурации и основное событие по отправке регулярного репорта.
Там заведен один таймер на все, который генерирует событие BDB_REPORT_TIMEOUT. Таймер этот не регулярный.
В OSAL_Timers два вида таймера:
osal_start_timerEx - создает таймер, который сработает один раз через заданное время.
osal_start_reload_timer - создает таймер который будет регулярно срабатывать через заданный промежуток времени.
.
BDB_REPORT_TIMEOUT срабатывает всегда один раз, а потом запускается снова, через новый промежуток времени. Если необходимо, чтобы он сработал вот прямо сейчас, то его отменяют, создают новый с временем срабатывания 0, что его вызывает сейчас.
Например, bdb_RepStartReporting - только если таймер остановлен, запускает его с задержкой 0. Как правило перед этим вызывается bdb_RepStopEventTimer.
Зачем? Если мы все будем делать подряд, т.е. в одном потоке, то мы будем выполнять только одну задачу, вот прямо сейчас срочно и только ее. Сложное устройство, работающее в некой сети, должно одновременно обрабатывать кучу заданий, которые не терпят откладывания. Вот b]bdb_RepStartReporting[/b] позволяет отложить нашу задачу, но выполнить ее сразу, как другие будут обработаны. Таким образом наше устройство "не зависнет" и будет иметь хороший отклик.
По событию BDB_REPORT_TIMEOUT вызывается bdb_RepProcessEvent, которая проверяет проверяет план отправки обязательных репортов.
Что касается плана, то при конфигурации репортов, собирается весь список комбинаций КонечнаяТочка-Кластер и список Параметров, что подлежат Отчетам. Каждая комбинация отмечается, есть ли Отчет и его параметры (минВремя, МаксВремя, МинКритерий). Если отчетных нет, то и вообще не будет создаваться событие. Также там ведется учет, сколько времени прошло с последнего Отчета (Репорта).
При обработке Эвента сперва ко всем репортам (комбинациям КонечнаяТочка-Кластер) прибавляется время, прошедшее с последнего срабатывания, ко времени, когда последний раз сработало. Выполняется расчет, когда надо отправить репорт, берется минимальное время - это и будет время следующего события для репорта.
Если следующий репорт через какое-то время, то на это время запускается новый таймер. Если Время = 0, т.е. сейчас, то отправляется репорт.
Выбор репорта для отправки выбирается также. За раз выбирается один репорт, вернее один кластер (КонечнаяТочка-Кластер). Берется первый, который должен отправится сейчас. Затем цикл события повторяется с запуском СЕЙЧАС, снова находится, что есть репорты на сейчас, снова находится первый и т.д. пока не отправятся все, что должны отправится СЕЙЧАС. И только потом найдется минимальное время НЕ СЕЙЧАС и Будет запуск нового Таймера, но уже отсрочкой до следующего репорта.

И вот мы подошли к bdb_RepChangedAttrValue.
Функция сперва проверяет, что за репорт проверяется, а репорт ли он и т.п.. Проверяется минимальное время репорта, отправлять чаще которого нельзя, и, если мы отправили репорт недавно, то пропускаем. Заметим, что не откладываем до минимального времени, а именно пропускаем. Далее проверяется, было ли изменение параметра достаточным для репорта, согласно критерию (из настроек репорта Минимальное изменение) и тоже пропускается, если изменилось меньше чем задано. Если принято решение ОТПРАВЛЯТЬ, то отключается BDB_REPORT_TIMEOUT, оправляется репорт и запускается BDB_REPORT_TIMEOUT через bdb_RepStartReporting (запускает с задержкой 0, далее будет пресчет нового времени старта и новый старт через новое время).

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

БАГА
Я выше описал, что сделал для типа UINT48 расчет его дельты для учета минимального критерия срабатывания (см алгоритм bdb_RepChangedAttrValue). Теперь максимальное время отправки репорта нет смысла ставить на 30 или 60 секунд, чтобы именно с этой задержкой отправлялся репорт и чтобы заработал метод bdb_RepChangedAttrValue (А зачем он нужен, если не работает, так вот теперь-то работает). Я установил максимальное время 3600 секунд. И репорты ПЕРЕСТАЛИ отправляться. Ну как перестали, через час бы репорт отправился, наверное, но ждать час - это такое. bdb_RepChangedAttrValue - вызывается каждые 30 секунд, но ничего не происходит. Путем перебора разных значений, я выяснил, что на 65 секунд (в максимальном значении репорта) репорт по bdb_RepChangedAttrValue отправляется каждые 30 секунд (время вызова bdb_RepChangedAttrValue), а при установке 66 секунд и более, уже нет.
Вот я и начал полностью разбирать как работает алгоритм bdb_RepReporting, чтобы понять от куда такое странное ограничение.
Собственно, алгоритм выглядел на 100% рабочим и проблем я не нашел.
Пошел по пути понимания, а что такого в числе 65 секунд?
В настройках используются секунды, а таймер работает в миллисекундах, т.е. чтобы из секунд сделать миллисекунды, надо их умножить на 1000.
Если 65 умножить на 1000, то получим число укладывающееся в uint16, а вот число 66 * 1000 уже в uint16 не помещается и уже нужно uint32. Собственно, таймер запускается в миллисекундах и использует для этого uint32. А в настройках просто секунды хранятся в uint16. Вот где-то тут видимо и наша проблема.
Я предполагал также, что кто-то просто отключает таймер, т.к. при запуске таймера в ручную по нажатию кнопки (правда я время указывал в секундах, вот тут мой косяк), один репорт отправлялся и далее снова тишина.
Я наставил кучу принтов, почти везде, где запускаются и останавливаются таймеры, но там все было норм, кроме того, что библиотека debug.h использует для форматирования vsprintf, который у IAR 8051 похоже тоже проблемный. Во-первых, он принтовал числа не более чем 16-ти разрядные, т.е. от 32-х разрядного числа он печатал только LOW часть, а во-вторых, он не поддерживает unsigned числа, у него формат "%u" менялся просто на "u". Я просто не понимал, что за шлак он мне печатает. Когда понял, то стал печать в HEX и переводить uint32 в uint8[4] и просто печатать их в HEX побайтно. И вот тут я наконец получил нормальные значения и нашел место где все ломалось.

РЕШЕНИЕ
Проблема нашлась все в той же bdb_RepChangedAttrValue, а вернее в расчете времени до следующего отчета, не осталось ли до следующего отчета времени менее, чем минимальное время отчета.
Знакомьтесь:

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

 /*********************************************************************
 * @fn          bdb_RepCalculateEventElapsedTime
 *
 * @brief       Calculate the elapsed time of the currently running timer, 
 *              the remaining time is roundup.
 *
 * @param       remainingTimeoutTimer - timeout value from the osal_get_timeoutEx method, 
 *              its in milliseconds units
 * @param       nextEventTimeout - the timeout given to the timer when it started
 *
 * @return      the elapsed time in seconds
 */
static uint16 bdb_RepCalculateEventElapsedTime( uint32 remainingTimeoutTimer, uint16 nextEventTimeout )
{
  uint32 passTimeOfEvent = 0;
  passTimeOfEvent = nextEventTimeout*1000 >= remainingTimeoutTimer? nextEventTimeout*1000 - remainingTimeoutTimer: 0;
  uint16 elapsedTime = passTimeOfEvent / 1000;
  elapsedTime = elapsedTime + ((passTimeOfEvent % 1000) >0 ? 1:0); //roundup
  return elapsedTime;  
}
На первый взгляд, тут все хорошо. Я более того скажу, что на 32-х разрядном компиляторе, а тем более на 64-х разрядном, все будет работать. А вот на 8-ми разрядном есть проблемка. Увидели?
Я повторил туже проблему и на AVR.
просто выполнил вот такой код:

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

uint16 = 3600;
uint32 val= 3500000;
uint32 res = (num * 1000>= val?  1 : 0);
Чему будет равен res? На 8-ми битном ЦПУ будет 0, хотя мы ожидаем 1.

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

uint16 = 3600;
uint32 val= 3500000;
uint32 res = (1000 * num>= val?  1 : 0);
А тут? Тоже самое, 0!

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

uint16 = 3600;
uint32 val= 3500000;
uint32 res = (1000L * num>= val?  1 : 0);
А если так?
Пояснительная бригада!
Компилятор старается сделать все как можно проще и компактнее. У 32-х разрядного, каждый регистр 32 разряда, и он каждое число по умолчанию будет вычислять как 32-х разрядное, а потом уже попытается его запихнуть в то что есть.
8-ми битному сперва надо увидеть, что 1000 в 8 бит не влезает, а в 16 влезает, и использует 16-ти разрядную арифметику, тем более, что умножается число на тоже 16-ти разрядное.
1000L - 32-х разрядное, типа long. Компилятор видит, что тут явно тип 32 бита и уже отталкивается от первого числа использует уже 32-х разрядную арифметику. Конечно это эмуляция 32-х разрядной арифметики, но все же.
Обычно при автоматическом преобразовании типов, если это возможно, за основу берется именно первый операнд, т.е. используется его тип и второй операнд приводится к его типу. В более жесткой семантике компилятор бы потребовал от разработчика сперва всё привести к одному типу, а потом уже делать вычисления (тут обобщения и для др. языков программирования, но везде есть свои нюансы).
Интересно, что в файле bdb_RepReporting.c есть вот такая функция:

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

static void bdb_RepRestartNextEventTimer( void )
{
  uint32 timeMs;
  // convert from seconds to milliseconds
  timeMs = 1000L * (bdb_reportingNextEventTimeout); 
  osal_start_timerEx( bdb_TaskID, BDB_REPORT_TIMEOUT, timeMs );
}
Они знали! ЗНАЛИ!!! Переменная bdb_reportingNextEventTimeout тоже uint16.
Это еще раз подтверждает мою теорию, что писали разные куски разные люди с разной квалификацией. Нет, я за молодых, но они должны учится и получать опыт. Тут опыт получен не был, т.к. не было произведено полноценное тестирование. Не было получено знание, что там есть ошибки. Ошибки не были исправлены. Это последняя версия аж 16-го года. Ей уже 10 лет. Все-таки от коммерческого продукта ожидаешь как-то больше (да библиотека бесплатная, но прилагается к вполне платному железу, как бы в комплекте, т.е. это не OpenSource).
Ответить