Перейти к содержимому

Библиотека настроек и “внешних” данных для ESP32 – reParams

Здравствуйте, уважаемые читатели!

Ранее я уже писал о том, как хранить различные параметры и настройки на flash-памяти ESP32: Настройка таблицы разделов FLASH-памяти для ESP32 и Настройка таблицы разделов FLASH-памяти для ESP32. Используя этот механизм, уже можно сохранять значения некоторых глобальных переменных для повторного использования. При этом для каждой такой переменной необходимо обеспечить не только её хранение, но и изменение.

Например, если я применяю механизм управления устройствами через WiFi (Ethernet) и MQTT протокол, то я должен выполнить следующие действия для каждого используемого в проекте параметра:

  • При запуске устройства записать в переменную-параметр значение “по умолчанию”.
  • Попытаться считать последнее сохраненное значение из NVS-раздела, если оно существует.
  • После подключения к сети и MQTT-серверу необходимо опубликовать значение параметра (при необходимости) и подписаться на сопоставленный данному параметру “входящий” топик. 
  • При получении нового значения во “входящем” топике преобразовать строку значения в необходимый формат и заменить значение переменной
  • После изменения значения переменной-параметра записать новое значение в NVS-разделе для дальнейшего использования.

Если вы используете другой способ управления, например изменение значений с помощью дисплея, кнопок и “экранного” меню, то суть процесса, в общем-то не меняется – просто замените третий и, возможно, четвертый пункт на свои варианты.

Таких параметров в проекте может быть далеко не один. При этом никогда не забывайте о совместном доступе к каждой из этих параметров-переменных из контекста разных задач. 

Так мне и пришла идея сделать универсальную библиотеку, которая бы взяла на себе всю рутинную работу по “обслуживанию” каждой такой переменной-параметра, дабы не “отвлекаться” на это и не копипастить один и тот же повторяющийся код. Кроме того, в дополнение к стандартному NVS API я добавил поддержку чисел с плавающей запятой и некоторых других типов данных. Найти и скачать её можно по ссылке: https://github.com/kotyara12/reParams

Предупреждение! Данная версия библиотеки рассчитана на работу в составе других моих библиотек – отдельно запустить её сходу вряд ли получится, хотя бы потому, что она рассчитана она строго на MQTT-брокер и на получение системных уведомлений строго определенного типа. Статья будет полезна в первую очередь тем, кто повторил или хочет повторить какой-либо из моих проектов на данном сайте.
Но вы можете использовать её в качестве основы для ваших идей. И конечно же, она используется во всех моих прошивках на текущий момент. Потихоньку-полегоньку я создаю вторую, сильно переработанную версию, в которой постараюсь учесть некоторые недостатки и недочеты, но когда это будет – пока неизвестно.

 


Функциональность

Библиотека берет на себя все основные обязанности по:

  • сохранению значений регистрируемых переменных-параметров в NVS-разделе,
  • подписке и публикации значений на MQTT-брокере в определенных топиках,
  • оповещению пользователей об внесенных изменениях в настройки устройства. 

Параметры могут быть сгруппированы по одной или нескольким группам, причем в отличие от стандартного NVS API, группы могут быть вложены друг в друга, но не увлекайтесь – длина составного “ключа” группы не может превышать 15 символов.

Для работы с “обычными” параметрами по умолчанию используется схема “с подтверждением”. Это означает, что вы отправляете новое значение в один – входящий топик; а устройство, получив и обработав его, отвечает вам тем же самым (или другим, если не удалось его обработать) значением в другой – топик подтверждения. Этот механизм позволяет гарантировать доставку отправляемых значений до устройства и поддерживается многими MQTT-клиентами.

При изменении параметра имеется возможность настроить отправку уведомлений в telegram о внесенных изменениях. Таким образом, например, если кто-то из вашей семьи или коллег изменит какие-либо настройки вашего устройства, вы тоже узнаете об этом.

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

Также можно изменять значения параметров не только “снаружи”, через MQTT, но и “изнутри”, например с использованием клавиш и экрана. В этом случае измененное значение будет немедленно опубликовано на MQTT-брокере в “подтверждяющем” топике.

Поддерживаются переменные-параметры следующих типов:

  • INT8 / UINT8 – однобайтовое целое со знаком или без
  • INT16 / UINT16 – двухбайтовое целое со знаком или без
  • INT32 / UINT32 – 32-битное целое со знаком или без
  • INT64 / UINT64 – 64-битное целое со знаком или без
  • FLOAT – число с запятой обычной точности
  • DOUBLE – число с запятой двойной точности
  • STRING – динамическая строка (с размещением в heap/куче) – и это не класс String!
  • TIMEVAL – суточная отметка времени без секунд в виде целого 32-битного числа: например 1830, что соответствует времени 18:30:00 
  • TIMESPAN – суточный интервал в виде целого 32-битного числа: например 23000700, что соответствует ночному интервалу с 23:00 до 07:00

 

Типы параметров и переменных:

  • OPT_KIND_PARAMETER – параметр, относящийся только к данному конкретному устройству. Следует использовать в большинстве случаев. Значение параметра сохраняется в NVS-разделе. Топики MQTT генерируется по правилу:
    • %LOCATION%/%DEVICE%/CONFIG/%GROUPS_TOPIC%/%PARAM_TOPIC% – “входящий” топик, в этот топик вы должны отправить новое значение параметра, которое хотели бы отправить на ваше устройство
    • %LOCATION%/%DEVICE%/CONFIRM/%GROUPS_TOPIC%/%PARAM_TOPIC% – топик “подтверждения”, в этом топике вы увидите реальное значение параметра после его изменения или перезапуска устройства
  • OPT_KIND_PARAMETER_LOCATION – общий параметр для всех устройств данной локации. Используется, чтобы не плодить одинаковые параметры для всех умных устройств. Очень похож на предыдущий случай, но топики немного отличаются:
    • %LOCATION%/CONFIG/%GROUPS_TOPIC%/%PARAM_TOPIC% – “входящий” топик, в этот топик вы должны отправить новое значение параметра, которое хотели бы отправить на все ваши устройства
    • %LOCATION%/CONFIRM/%GROUPS_TOPIC%/%PARAM_TOPIC% – топик “подтверждения”, в этом топике вы увидите реальное значение параметра после его изменения или перезапуска устройства
  • OPT_KIND_PARAMETER_ONLINE – параметр, значение которого не будет сохранено в NVS-разделе. То есть обычный параметр, но каждый новый перезапуск устройства начинается со значения по умолчанию. Топики формируется точно так же, как и для OPT_KIND_PARAMETER:
    • %LOCATION%/%DEVICE%/CONFIG/%GROUPS_TOPIC%/%PARAM_TOPIC% – “входящий” топик, в этот топик вы должны отправить новое значение параметра, которое хотели бы отправить на ваше устройство
    • %LOCATION%/%DEVICE%/CONFIRM/%GROUPS_TOPIC%/%PARAM_TOPIC% – топик “подтверждения”, в этом топике вы увидите реальное значение параметра после его изменения или перезапуска устройства
  •  OPT_KIND_LOCDATA_ONLINE – используется для получения данных в открытом виде с других устройств через локальный MQTT-брокер. Значения в NVS-разделе не сохраняется. Топика подтверждения нет и не может быть, а “входящий” топик формируется по правилу:
    • %LOCAL%/%GROUPS_TOPIC%/%PARAM_TOPIC% – “входящий” топик для внешних данных
  • OPT_KIND_LOCDATA_STORED – тоже самое, но каждое полученное значение сохраняется в ТVS-разделе. С одной стороны это удобно, но может привести к повышенной нагрузке на flash-память.
    • %LOCAL%/%GROUPS_TOPIC%/%PARAM_TOPIC% – “входящий” топик для внешних данных
  • OPT_KIND_EXTDATA_ONLINE – может быть использован для обмена данными между устройствами с использованием любого произвольного заранее определенного топика. Значение не сохраняется в NVS-разделе.
  • OPT_KIND_EXTDATA_STORED – тоже самое, но , но каждое полученное значение сохраняется в ТVS-разделе. С одной стороны это удобно, но может привести к повышенной нагрузке на flash-память.
  • OPT_KIND_SIGNAL – внешний управляющий сигнал, отправляемый на устройство в один и тот же топик. Полученное значение не сохраняется в NVS-разделе. Топика подтверждения нет, “входящий” топик генерируется по правилу:
    • %LOCATION%/%DEVICE%/%GROUPS_TOPIC%/%PARAM_TOPIC% – сюда, например, можно отправить номер GPIO, на котором нужно выставить высокий уровень.
  • OPT_KIND_SIGNAL_AUTOCLR – тот же самый сигнал, но с самоочисткой “входящего” топика после получения.
  • OPT_KIND_COMMAND – “системный” топик, который используется для отправки устройству текстовая команд, например RESET. Не следует использовать этот параметр в ваших проектах, он уже встроен в базовый вариант прошивки! Само собой, отправленные команды не сохраняются в NVS-разделе. Входящий топик строго фиксированный: 
    • %LOCATION%/%DEVICE%/SYSTEM/TERMINAL
  • OPT_KIND_OTA – еще один “служебный” топик для отправки устройствам ссылки на OTA-обновление прошивки. Не следует использовать этот параметр в ваших проектах, он уже встроен в базовый вариант прошивки! Входящий топик строго фиксированный: 
    • %LOCATION%/%DEVICE%/SYSTEM/OTA

Примечания:

  • %LOCATION% – субтопик локации (например village), определенный в макросах CONFIG_MQTTx_PUB_LOCATION файла конфигурации.
  • %DEVICE% – субтопик, содержащий название текущего устройства (например greenhouse), определенный в макросах CONFIG_MQTTx_LOC_DEVICE
     или CONFIG_MQTTx_PUB_DEVICE файла конфигурации. 
  • %LOCAL% – префикс локальных топиков, то есть используется только для топиков локального сервера. Может быть задан в макросах CONFIG_MQTTx_PUB_LOCATION файла конфигурации.
  • %GROUPS_TOPIC% – составной топик группы. Если группа одна, то он будет соответствовать заданному топику группы; если группы вложены друг в друга, то и топики будут вложены друг в друга.
  • %PARAM_TOPIC% – топик собственно параметра. Это значение также используется в качестве ключа NVS-раздела, поэтому длина топика не может превышать 15-символов.

Выглядит эта система сложновато, но на деле все довольно просто.

На скриншоте видны только топики подтверждения в виде иерархии

Пример настройки типичного параметра в программе MQTT Dash для Andriod:

Еще можно почитать об этом в одной из прежних статей: Термостат на ESP32 с удаленным управлением. Часть 4. MQTT-топики

 


Как использовать

1. Инициализировать NVS-раздел

Прежде всего необходимо инициализировать NVS-раздел. Как это сделать – я рассказывал в другой статье NVS: энергонезависимая библиотека хранения параметров, поэтому специально здесь останавливаться не буду, это как бы само собой разумеющееся действие. Замечу только, что поскольку драйвер WiFi обычно требует NVS раздел для хранения внутренних физических параметров, я обычно делаю это в самом начале выполнения программы.

 

2. Регистрируем обработчики событий подключения и отключения от сервера

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

Сделать это можно с помощью функции paramsEventHandlerRegister(), вызывав её, например, в app_main().

bool paramsEventHandlerRegister();

Разумеется, сиё справедливо только для моих проектов, где используется цикл событий и другая моя библиотека reEvents. Однако это не строго обязательно,  поскольку по событиям вызываются внутренние функции:

void paramsMqttSubscribesOpen(bool mqttPrimary, bool forcedResubscribe);
void paramsMqttSubscribesClose();
void paramsMqttIncomingMessage(char *topic, char *payload, size_t len);

Поэтому можно вырезать из кода проекта все, что связано с событиями RE_MQTT_CONNECTED и RE_MQTT_CONN_LOST и забыть про них как про страшный сон.

  • Вызовите paramsMqttSubscribesOpen(), когда вы подключились или переподключились к MQTT-брокеру, указав, к какому серверу вы подключились – основному или резервному.
  • Соответственно, paramsMqttSubscribesClose() должна быть вызвана перед отключением от MQTT-сервера или сразу после него (увы, не всегда отключение происходит по нашей инициативе).
  • Функция paramsMqttIncomingMessage() используется для обработки входящих сообщений от брокера.

 

3. Создаем группу(ы) параметров

Переходим к непосредственным действиям. Далее нам необходимо создать как минимум одну группу параметров. Группа – это моя обёртка для namespace-ов NVS.

Зарегистрировать группу параметров можно с помощью функции paramsRegisterGroup():

paramsGroupHandle_t paramsRegisterGroup(paramsGroup_t* parent_group, const char* name_key, const char* name_topic, const char* name_friendly);

где:

  • parent_group – указатель на группу верхнего уровня, если есть
  • name_key – “ключ” группы, который будет использован при открытии NVS-раздела; длина ключа не может превышать 15 символов
  • name_topic – MQTT-топик группы, используется в формировании полного топика каждого вложенного в группу параметров
  • name_friendly – понятное название группы, которое может быть использовано в уведомлениях об изменениях параметров

Повторюсь, группы можно “вкладывать” друг в друга. В этом случае параметры name_key и name_topic “склеиваются” друг с другом с использованием разделителей “.” и “/” соответственно. Поэтому, если вы планируете вложение групп – заранее укорачивайте name_key так, чтобы результирующая длина “общего” ключа не превысила 15 символов.

Например:

#define CONFIG_SENSOR_PGROUP_ROOT_KEY "sens"
#define CONFIG_SENSOR_PGROUP_ROOT_TOPIC "sensors"
#define CONFIG_SENSOR_PGROUP_ROOT_FRIENDLY "Сенсоры"

#define CONFIG_SENSOR_PGROUP_INTERVALS_KEY "intv"
#define CONFIG_SENSOR_PGROUP_INTERVALS_TOPIC "intervals"
#define CONFIG_SENSOR_PGROUP_INTERVALS_FRIENDLY "Интервалы отправки данных"

pgSensors = paramsRegisterGroup(NULL, 
  CONFIG_SENSOR_PGROUP_ROOT_KEY, CONFIG_SENSOR_PGROUP_ROOT_TOPIC, CONFIG_SENSOR_PGROUP_ROOT_FRIENDLY);
pgIntervals = paramsRegisterGroup(pgSensors, 
  CONFIG_SENSOR_PGROUP_INTERVALS_KEY, CONFIG_SENSOR_PGROUP_INTERVALS_TOPIC, CONFIG_SENSOR_PGROUP_INTERVALS_FRIENDLY);

В этом случае для параметров, прикрепленных к группе pgIntervals, ключ (namespace) будет равен sens.intv, а топик sensors/intervals.

 

4. Регистрируем параметр

Затем создаем простую глобальную или статическую переменную нужного типа и регистрируем её в списке параметров с помощью другой функции paramsRegisterValueEx():

paramsEntryHandle_t paramsRegisterValueEx(const param_kind_t type_param, const param_type_t type_value, 
  param_handler_type_t handler_type, void* change_handler,
  paramsGroupHandle_t parent_group, 
  const char* name_key, const char* name_friendly, const int qos, 
  void * value);

где:

  • type_param – тип параметра: OPT_KIND_PARAMETER / OPT_KIND_PARAMETER_LOCATION и т.д.
  • type_value – тип значения переменной: OPT_TYPE_I8 / OPT_TYPE_U8 и т.д.
  • handler_type – тип обработчика при получении нового значения: 
    • PARAM_HANDLER_NONE – без уведомлений,
    • PARAM_HANDLER_EVENT – отправить уведомление через системный цикл событий,
    • PARAM_HANDLER_CALLBACK – вызвать функцию обратного вызова,
    • PARAM_HANDLER_CLASS – использовать специальный класс-обработчик param_handler_t
  • change_handler – указатель на функцию обратного вызова или класс-обработчик param_handler_t
  • parent_group – указатель на группу, к которой относится данный параметр
  • name_key – имя ключа параметра, по совместительству являющийся и топиком, не должно превышать 15 символов
  • name_friendly -понятное имя, которое может быть использовано в уведомлениях об изменениях параметров
  • qos – режим отправки значений на MQTT-сервер, используется при публикации подтверждений
  • value – указатель на вашу переменную

Можно так же использовать макрос paramsRegisterValue(), который просто вызывает эту самую функцию с обработчиком PARAM_HANDLER_EVENT:

#define paramsRegisterValue(type_param, type_value, change_handler, parent_group, name_key, name_friendly, qos, value) \
  paramsRegisterValueEx(type_param, type_value, PARAM_HANDLER_EVENT, change_handler, parent_group, name_key, name_friendly, qos, value)

Например:

// Период публикации данных с сенсоров на MQTT 
static uint32_t iMqttPubInterval = 60;

...

if (pgIntervals) {
  paramsRegisterValue(OPT_KIND_PARAMETER, OPT_TYPE_U32, nullptr, pgIntervals,
    CONFIG_SENSOR_PARAM_INTERVAL_MQTT_KEY, CONFIG_SENSOR_PARAM_INTERVAL_MQTT_FRIENDLY,
    CONFIG_MQTT_PARAMS_QOS, (void*)&iMqttPubInterval);
};

 

В качестве альтернативы можно использовать функцию paramsRegisterCommonValueEx(), которая регистрирует параметр в заранее определенной группе common:

paramsEntryHandle_t paramsRegisterCommonValueEx(const param_kind_t type_param, const param_type_t type_value, 
  param_handler_type_t handler_type, void* change_handler,
  const char* name_key, const char* name_friendly, const int qos, 
  void * value);

и соответствующий ей макрос paramsRegisterCommonValue():

#define paramsRegisterCommonValue(type_param, type_value, change_handler, name_key, name_friendly, qos, value) \
  paramsRegisterCommonValueEx(type_param, type_value, PARAM_HANDLER_EVENT, change_handler, name_key, name_friendly, qos, value)

 

5. Дополнительно можно установить ограничения

Лимиты – это минимальное и максимальное значение параметра. Если попытаться установить значение вне заданного диапазона, попытка будет отклонена. Для задания диапазона используйте любую из нижеперечисленных функций, в зависимости от типа переменной:

void paramsSetLimitsI8(paramsEntryHandle_t entry, int8_t min_value, int8_t max_value);
void paramsSetLimitsU8(paramsEntryHandle_t entry, uint8_t min_value, uint8_t max_value);
void paramsSetLimitsI16(paramsEntryHandle_t entry, int16_t min_value, int16_t max_value);
void paramsSetLimitsU16(paramsEntryHandle_t entry, uint16_t min_value, uint16_t max_value);
void paramsSetLimitsI32(paramsEntryHandle_t entry, int32_t min_value, int32_t max_value);
void paramsSetLimitsU32(paramsEntryHandle_t entry, uint32_t min_value, uint32_t max_value);
void paramsSetLimitsI64(paramsEntryHandle_t entry, int64_t min_value, int64_t max_value);
void paramsSetLimitsU64(paramsEntryHandle_t entry, uint64_t min_value, uint64_t max_value);
void paramsSetLimitsFloat(paramsEntryHandle_t entry, float min_value, float max_value);
void paramsSetLimitsDouble(paramsEntryHandle_t entry, double min_value, double max_value);

Например:

void notifyInitParameters()
{
  rlog_i(logLAMPS, "Register notification parameters...");

  paramsGroupHandle_t pgNoticeGates = paramsRegisterGroup(nullptr, NOTICE_GATES_GROUP_KEY, NOTICE_GATES_GROUP_TOPIC, NOTICE_GATES_GROUP_FRIENDLY);
  if (pgNoticeGates) {
    paramsSetLimitsU8(
      paramsRegisterValue(OPT_KIND_PARAMETER, OPT_TYPE_U8, nullptr, pgNoticeGates, 
        NOTICE_GATES_TYPE_TOPIC, NOTICE_GATES_TYPE_FRIENDLY, 
        CONFIG_MQTT_PARAMS_QOS, (void*)&noticeGatesState),
      0, 2);
  };
}

 

Если вам просто необходимо сохранить значение переменной прямо из кода прошивки, можно воспользоваться следующей функцией:

void paramsValueStore(paramsEntryHandle_t entry, const bool callHandler);

При этом если callHandler == true, то будет сгенерировано событие-уведомление или произведен вызов функции обратного вызова. А также будет сгенерировано уведомление в telegram и опубликовано новое значение в в confirm топике. Иначе (при callHandler != true) все произойдет тихо и незаметно под покровом ночи.

Важно! Перед вызовом paramsValueStore() новое значение уже должно быть записано в соответствующую переменную!

 

Если же вам необходимо записать новое значение в параметр, без непосредственного доступа к переменной, которая его хранит в памяти, воспользуйтесь другой функцией:

void paramsValueSet(paramsEntryHandle_t entry, char *new_value, bool publish_in_mqtt);

Эта функция принимает на входе значение в виде строки (char*) и сама преобразует его в нужный формат данных. Именно эта функция, в конечном итоге, обрабатывает все входящие сообщения MQTT-брокера. 

Важно! По окончании работы переданное “строковое” значение будет удалено из кучи автоматически! Дополнительно удалять его не нужно. Это несколько не логично, зато удобно.

 


Как это работает

Библиотека хранит список параметров и групп в виде двух связных списков.

При подключении к MQTT-брокеру через системный цикл событий поступает соответствующее событие-уведомление, вызывается функция paramsMqttSubscribesOpen(), которая делает следующее:

  • Генерируется пары “полных” топиков (config + confirm) по заранее определенным правилам (см. выше), для каждого из параметров. То есть те самые %LOCATION%/%DEVICE%/CONFIG/%GROUPS_TOPIC%/%PARAM_TOPIC% и %LOCATION%/%DEVICE%/CONFIRM/%GROUPS_TOPIC%/%PARAM_TOPIC%, про которые я писал выше. Топики могут быть разными в зависимости от того, к какому серверу мы подключились в данный момент – основному или резервному. Сгенерированные строки топиков хранятся в куче до момента отключения от сервера.
  • Подписываемся на все топики сразу “одной командой”: %LOCATION%/%DEVICE%/CONFIG/# (и %LOCATION%/CONFIG/#, если таковые параметры есть в прошивке), с помощью paramsMqttSubscribe(). Для параметров, отличающихся от шаблонов, указанных выше, подписываемся отдельно на конкретный топик.
  • Публикуем последнее сохраненное значение каждого параметра в %LOCATION%/%DEVICE%/CONFIRM/%GROUPS_TOPIC%/%PARAM_TOPIC% с помощью paramsMqttPublish(). Таким образом мы сразу же после запуска устройства автоматически увидим последние сохраненные значения на MQTT.

При поступлении нового входящего сообщения от MQTT-брокера (%LOCATION%/%DEVICE%/CONFIG/%GROUPS_TOPIC%/%PARAM_TOPIC% или аналогичных по смыслу) выполняется следующее:

  • Строковое сообщение преобразуется к заданному типу данных (число, целое и т.д.)
  • Новое значение проверяется на допустимые границы, если они заданы. Если обнаружен выход за заданные лимиты – выдаем предупреждение и отменяем изменение
  • Новое значение сравнивается с текущим. Если оно не отличается от текущего, прерываем процесс и выдаем оповещение
  • Приостанавливаем планировщик (дабы он не мог переключить контекст на другую задачу), записываем новое значение в переменную по ссылке, а затем восстанавливаем нормальную работу планировщика.
  • Сохраняем новое значение в NVS-разделе, если тип параметра это предусматривает
  • Отправляем оповещение другим задачам через цикл событий или вызываем callback
  • Публикуем новое значение в %LOCATION%/%DEVICE%/CONFIRM/%GROUPS_TOPIC%/%PARAM_TOPIC% 
  • Отправляем уведомление в telegram об изменении параметра

Если по каким-то причинам необходимо прервать связь с MQTT-брокером или она была прервана неожиданно, библиотека “отпишется” от входящих топиков  с помощью paramsMqttUnsubscribe() и удалит сгенерированные строки топиков из памяти.

 


Обработка оповещений об изменении параметров

В самом простейшем случае никакие оповещения и их обработки обычно не нужны. Пример был уже приведен выше – “интервал публикации данных с сенсоров на MQTT”:

// Период публикации данных с сенсоров на MQTT 
static uint32_t iMqttPubInterval = 60;

...

if (pgIntervals) {
  paramsRegisterValue(OPT_KIND_PARAMETER, OPT_TYPE_U32, nullptr, pgIntervals,
    CONFIG_SENSOR_PARAM_INTERVAL_MQTT_KEY, CONFIG_SENSOR_PARAM_INTERVAL_MQTT_FRIENDLY,
    CONFIG_MQTT_PARAMS_QOS, (void*)&iMqttPubInterval);
};

Если вы дадите команду на изменение этого параметра с телефона, оно будет записано в переменную iMqttPubInterval, и начнет действовать в следующем цикле задачи, которая отправляет данные с сенсоров на сервер. Если вас это устраивает – выберите режим handler_type = PARAM_HANDLER_NONE при регистрации такой переменной.

Но иногда нужно, чтобы внесенные изменения были применены в работе немедленно после их получения. Пример: вы изменяете уставку температуры на термостате, а чтение и обработка датчиков происходит, скажем один раз в 10 минут. Если ничего дополнительно не предпринимать, то новые настройки будут применены через 0 ~ 10 минут в зависимости от того, в какой момент вам повезло отправить команду на изменение.

Для этого предусмотрены три способа, которые можно задать при помощи атрибута handler_type. Рассмотрим их поподробнее.

PARAM_HANDLER_EVENT

В этом случае при получении нового значения библиотека отправит в системный цикл событий оповещение RE_PARAMS_EVENTS со следующими идентификаторами:

  • RE_PARAMS_RESTORED – значение переменной было восстановлено (считано из NVS-раздела)
  • RE_PARAMS_INTERNAL – значение переменной было изменено “изнутри” прошивки
  • RE_PARAMS_CHANGED – значение переменной было изменено по команду извне (на данный момент это может быть только MQTT-брокер)
  • RE_PARAMS_EQUALS – поступило новое значение, но оно совпадает с текущим и не было заменено

Дабы было понятно, от какой именно переменной поступило уведомление, в качестве “полезной нагрузки” события прикрепляется адрес переменной в памяти. Сравнив его с заранее известными адресами, можно однозначно понять, чем было вызвано это событие. А можно и не определять переменную – источник события, а просто досрочно выполнить новый цикл рабочей задачи. В приведенном ниже примере использованы оба способа:

void evhdlParamsEventHandler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data)
{
  if (*(uint32_t*)event_data == (uint32_t)&extOutdoorTemp) {
    extOutdoorTempTime = time(nullptr);
  } else if (*(uint32_t*)event_data == (uint32_t)&extOutdoorHumd) {
    extOutdoorHumdTime = time(nullptr);
  } else if (*(uint32_t*)event_data == (uint32_t)&extOutdoorLight) {
    extOutdoorLightTime = time(nullptr);
  } else if (*(uint32_t*)event_data == (uint32_t)&extOutdoorWind) {
    extOutdoorWindTime = time(nullptr);
    filterOutdoorWindPut();
  } else if (event_id == RE_PARAMS_CHANGED) {
    xEventGroupSetBits(_sensorsFlags, FLG_PARAMS_CHANGED);
  };
}

Дабы это заработало, необходимо зарегистрировать этот обработчик:

eventHandlerRegister(RE_PARAMS_EVENTS, ESP_EVENT_ANY_ID, &evhdlParamsEventHandler, nullptr);

 

PARAM_HANDLER_CALLBACK

Допустим, по каким-то причинам вам не удобно применять циклы событий. Например вы решили изменить библиотеку так, чтобы она работала вне пакета моих библиотек. Тогла вы можете использовать функцию обратного вызова. Её прототип выглядит так:

typedef void (*params_callback_t) (paramsEntryHandle_t item, param_change_mode_t mode, void* value);

Вы должны будете написать функцию с указанными аргументами самостоятельно и передать её при регистрации параметра.

 

PARAM_HANDLER_CLASS

Иногда не удобно использовать простую функцию обратного вызова, потому что изменяемые параметры находятся “внутри” класса. Конечно, можно передать указатель на конкретный экземпляр класса при регистрации параметра, но как потом оповестить этот конкретный экземпляр класса об изменении? Чтобы изолировать механизм библиотеки от произвольных классов, я придумал еще один класс-прокладку:

class param_handler_t {
  public:
    virtual ~param_handler_t() {};
    virtual void onChange(param_change_mode_t mode) = 0;
};

Что нужно сделать?

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

В примере ниже класс-обработчик связан с классом rSensorItem и при изменении параметров перенастраивает режим фильтрации измеряемых значений, “дергая” _item->doChangeFilterMode():

class rSensorFilterHandler: public param_handler_t {
  private:
    rSensorItem *_item;
  public:
    explicit rSensorFilterHandler(rSensorItem *item);
    void onChange(param_change_mode_t mode) override;
};

// ============================================================

rSensorFilterHandler::rSensorFilterHandler(rSensorItem *item)
{
  _item = item;
}

void rSensorFilterHandler::onChange(param_change_mode_t mode)
{
  if (_item) _item->doChangeFilterMode();
}

Указатель на этот класс-обработчик вы должны будете передать при регистрации переменной, не забыв создать экземпляр обработчика в конструкторе основного класса:

_filterHandler = new rSensorFilterHandler(this);

 


Использование библиотеки для обмена данными между устройствами

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

Например:

  • Погоду снаружи “собирает” метеостанция. Полученные данные публикуются на локальном (то есть внутри локальной сети дома) MQTT-брокера в определенных топиках.
  • На эти топики “подписаны” термостат отопления, контроллер теплицы, контроллер гаража, контроллер вентиляции в душевой и т.д.

В качестве иллюстрации

 

Допустим, метеостанция “выдает” такие локальные данные (то есть предназначенные для M2M-обмена):

  • local/sensors/meteo/air1/temperature – температура
  • local/sensors/meteo/air1/humidity – влажность
  • local/sensors/meteo/wind/speed – скорость ветра
  • local/sensors/meteo/illumination – освещенность

Данные должны публиковаться в “простом” виде, то есть без упаковки в JSON, XML и прочие структуры.

Наша задача – считать их оттуда, не особо напрягаясь в плане кодирования.

Для начала объявим необходимые переменные:

static float  extOutdoorTemp      = NAN;
static float  extOutdoorHumd      = NAN;
static float  extOutdoorWind      = NAN;
static float  extOutdoorLight     = NAN;

А затем просто зарегистрируем их как OPT_KIND_LOCDATA_ONLINE:

paramsGroupHandle_t extDataMeteo = paramsRegisterGroup(nullptr, "meteo", "sensors/meteo", "Метеостанция");
if (extDataMeteo) {
  // Климат
  paramsGroupHandle_t extDataAir1 = paramsRegisterGroup(extDataMeteo, "s1", "air1", "Климат");
  if (extDataAir1) {
    paramsRegisterValue(OPT_KIND_LOCDATA_ONLINE, OPT_TYPE_FLOAT, nullptr, extDataAir1, "temperature", "Температура", 1, &extOutdoorTemp);
    paramsRegisterValue(OPT_KIND_LOCDATA_ONLINE, OPT_TYPE_FLOAT, nullptr, extDataAir1, "humidity", "Влажность", 1, &extOutdoorHumd);
  };
  // Ветер
  paramsGroupHandle_t extDataWind = paramsRegisterGroup(extDataMeteo, "s2", "wind", "Ветер");
  if (extDataWind) {
    paramsRegisterValue(OPT_KIND_LOCDATA_ONLINE, OPT_TYPE_FLOAT, nullptr, extDataWind, "speed", "Скорость", 1, &extOutdoorWind);
  };
  // Освещенность
  paramsRegisterValue(OPT_KIND_LOCDATA_ONLINE, OPT_TYPE_FLOAT, nullptr, extDataMeteo, "illumination", "Освещенность", 1, &extOutdoorLight);
};

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

Можно усложнить задачу и добавить “время жизни” полученных внешних данных – тогда при длительном отсутствии новых данных можно будет использовать альтернативные способы получения необходимых сведений. Но это вы можете подсмотреть в моих публичных проектах на GitHub.


Ну а на этом разрешите откланяться, с вами был Александр aka kotyara12. Благодарю за внимание.


Пожалуйста, оцените статью:
[ 5 из 5, всего 1 оценок ]

-= Каталог статей (по разделам) =-   -= Архив статей (подряд) =-

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

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