Добрый день, уважаемый читатель!
Данная статья является в первую продолжением серии “Термостат на ESP32 с удаленным управлением“, но описанное в статье в полной мере относится и к “Автоматической теплице“, и к “Автоматическому поливу“, да и к вообще к любым устройствам автоматики, собранным на ESP32 и запрограммированным с помощью ESP-IDF и моих библиотек для этой платформы. Поэтому вынес её в “общий” раздел под названием “Прошивка K12 для ESP32“. А поговорим мы в ней о командах управления.
Если вы читали статьи из перечисленных серий, то наверное поняли, что основным каналом управления устройством является протокол MQTT. С его помощью мы можем изменять различные настройки и управлять исполнительными устройствами, используя специальные топики управления и изменения параметров с обратной связью. Но иногда нужно просто отправить на устройство какую – либо команду без ожидания ответа от устройства, например:
- перезапустить устройство
- обновить прошивку через технологию OTA
- сбросить сохраненные данные о зафиксированных минимумах и максимумах для сенсоров
- выполнить какую-либо другую команду поверх логики встроенного конечного автомата
Конечно, это легко можно сделать, “прикрутив” в прошивку подписки на какие-нибудь специальные “управляющие” топики для каждой команды в отдельности. Но я поступил по другому – создал всего два “специальных” топика, в которые можно отправлять заранее определенные текстовые команды:
- %location%/%device%/system/terminal – в этот топик можно отправлять текстовые команды управления ( почему именно terminal а не commands? – на момент создания этого функционала топик commands был “занят” под другие команды, но потом я от него избавился совсем; впрочем это можно изменить )
- %location%/%device%/system/ota – а сюда вы должны отправить ссылку на BIN-файл прошивки для “обновления по воздуху” ( OTA ) в формате “https://server/…/firmware.bin”
где %location%/%device%
– параметры-макросы CONFIG_MQTTx_PUB_LOCATION
и CONFIG_MQTTx_PUB_DEVICE
из файла project_config.h. Про обновление прошивки через OTA поговорим в следующей статье, а в данной обсудим только команды управления, а также как можно добавить свои собственные команды в прошивку.
Поскольку эти топики работают только в одном направлении (на вход), их нельзя увидеть через программу MQTT Explorer.
Как это работает?
После запуска микроконтроллера и подключения устройства к MQTT-брокеру, оно “подписывается” на указанный выше топик “
/system/terminal” и ждет новых сообщений. Про поступлении нового сообщения в указанном топике устройство обрабатывает его и само стирает данные из топика, отправив туда NULL. Это является подтверждением того, что команда была получена и обработана должным образом. Дополнительно отправляется сообщение в telegram-канал управления:%location%/%device%
Вся “физическая” обработка команд реализована в функции paramsExecCmd() в модуле reParams.cpp:
Для простоты понимания здесь я выделил блоки:
- синий – отправка сообщения в telegram о получении команды
- желтый – удаление полученной команды из топика
- красный – выполнение встроенных команд (пока одна – подробнее см. ниже)
- зеленый – если команда не встроенная, то отправляем событие
RE_SYSTEM_EVENTS.RE_SYS_COMMAND
в прикладной цикл событий с текстом полученной команды. Ваше приложение должно подписаться на это событие, получить его и соответствующим образом обработать. Как это сделать – будет рассказано ниже.
Встроенные команды
На текущий момент устройства (например термостат) имеют всего одну “встроенную” команду:
restart – программный перезапуск устройства.
Получив сообщение с текстом “restart” в топик “…/system/terminal” устройство проведет все необходимые “заключительные процедуры” (например сохранит оперативные данные на флеш-память и отключит нагрузку), а затем выполнит программный сброс через системную функцию esp_restart()
.
Через какое-то время устройство выполнит указанное действие:
Вот, в общем-то и все “встроенные” команды.
Пользовательские команды
Но не все так грустно – вы можете легко и просто добавлять обработчики своих, кастомных команд по своему разумению. Более того, в перечисленных выше прошивках “Термостат“, “Автоматическая теплица” и “Автополив” уже имеются такие “пользовательские команды, даже несколько.
ClrExtr – cброс экстремумов сенсоров
Если вы читали статью про класс rSensor и для чего он нужен, то, наверное, помните, что любой драйвер сенсора или датчика, построенный на базе этого класса, умеет накапливать данные об экстремумах измеренных значений, то есть минимумах и максимумах за текущие сутки, неделю и все время работы. Эти данные сохраняются в энергонезависимой памяти ( NVS ) по при следующем включении устройства будут восстановлены. Но иногда необходимо “сбросить” эти сохраненные данные, например если были получены явно неверные данные по каким-либо причинам. Дабы иметь возможность для такого сброса, предусмотрена специальная “пользовательская” команда – clrextr (CLear EXTRemums), которая уже имеется в прошивках.
Кроме того, команда clrextr – не совсем простая команда. Она может быть выполнена в нескольких вариантах:
- clrextr – сброс всех экстремумов у всех датчиков, подключенных к данному устройству
- clrextr daily – сброс только суточных экстремумов у всех датчиков, подключенных к данному устройству
- clrextr weekly – сброс только недельных экстремумов у всех датчиков, подключенных к данному устройству
- clrextr entirely – сброс только абсолютных экстремумов у всех датчиков, подключенных к данному устройству
- clrextr датчик – сброс всех экстремумов только для конкретно указанного датчика
- clrextr датчик daily – сброс только суточных экстремумов и только для конкретно указанного датчика
- clrextr датчик weekly – сброс только недельных экстремумов и только для конкретно указанного датчика
- clrextr датчик entirely – сброс только абсолютных экстремумов и только для конкретно указанного датчика
Системное имя датчика задается программистом при написании прошивки.
Но почему эта команда не “встроенная”? Дело в том, что заранее никогда не известно сколько и каких датчиков будет подключено к устройству. А “дернуть за хвост” приходится каждый конкретный экземпляр (объект) датчика. Вот и приходится каждый раз копипастить этот код из прошивки в прошивку с необходимыми изменениями. На её примере я и расскажу, как добавлять свои команды при необходимости.
Итак, как я уже рассказал выше, нам нужно подписаться на событие RE_SYSTEM_EVENTS.RE_SYS_COMMAND и обработать его. Для этого напишем функцию: void sensorsCommandsEventHandler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data):
static void sensorsCommandsEventHandler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { if ((event_id == RE_SYS_COMMAND) && (event_data)) { char* buf = malloc_string((char*)event_data); if (buf != nullptr) { const char* seps = " "; char* cmd = nullptr; char* mode = nullptr; char* sensor = nullptr; uint8_t imode = 0; cmd = strtok(buf, seps); // Команды сброса датчиков if ((cmd != nullptr) && (strcasecmp(cmd, CONFIG_SENSOR_COMMAND_EXTR_RESET) == 0)) { rlog_i(logTAG, "Reset extremums: %s", buf); sensor = strtok(nullptr, seps); if (sensor != nullptr) { mode = strtok(nullptr, seps); }; // Определение режима сброса if (mode == nullptr) { // Возможно, вторым токеном идет режим, в этом случае сбрасываем для всех сенсоров if (sensor) { if (strcasecmp(sensor, CONFIG_SENSOR_EXTREMUMS_DAILY) == 0) { sensor = nullptr; imode = 1; } else if (strcasecmp(sensor, CONFIG_SENSOR_EXTREMUMS_WEEKLY) == 0) { sensor = nullptr; imode = 2; } else if (strcasecmp(sensor, CONFIG_SENSOR_EXTREMUMS_ENTIRELY) == 0) { sensor = nullptr; imode = 3; }; }; } else if (strcasecmp(mode, CONFIG_SENSOR_EXTREMUMS_DAILY) == 0) { imode = 1; } else if (strcasecmp(mode, CONFIG_SENSOR_EXTREMUMS_WEEKLY) == 0) { imode = 2; } else if (strcasecmp(mode, CONFIG_SENSOR_EXTREMUMS_ENTIRELY) == 0) { imode = 3; }; // Определение сенсора if ((sensor == nullptr) || (strcasecmp(sensor, CONFIG_SENSOR_COMMAND_SENSORS_PREFIX) == 0)) { sensorsResetExtremumsSensors(imode); } else { if (strcasecmp(sensor, SENSOR_SOIL_TOPIC) == 0) { sensorsResetExtremumsSensor(&sensorSoil, SENSOR_SOIL_TOPIC, imode); } else if (strcasecmp(sensor, SENSOR_INDOOR_TOPIC) == 0) { sensorsResetExtremumsSensor(&sensorIndoor, SENSOR_INDOOR_TOPIC, imode); } else if (strcasecmp(sensor, SENSOR_HEATING_TOPIC) == 0) { sensorsResetExtremumsSensor(&sensorHeating, SENSOR_HEATING_TOPIC, imode); } else { rlog_w(logTAG, "Sensor [ %s ] not found", sensor); #if CONFIG_TELEGRAM_ENABLE tgSend(CONFIG_SENSOR_COMMAND_KIND, CONFIG_SENSOR_COMMAND_PRIORITY, CONFIG_SENSOR_COMMAND_NOTIFY, CONFIG_TELEGRAM_DEVICE, CONFIG_MESSAGE_TG_SENSOR_CLREXTR_UNKNOWN, sensor); #endif // CONFIG_TELEGRAM_ENABLE }; }; }; }; if (buf != nullptr) free(buf); }; }
затем зарегистрируем её с помощью другой функции eventHandlerRegister(RE_SYSTEM_EVENTS, RE_SYS_COMMAND, &sensorsCommandsEventHandler, nullptr).
bool sensorsEventHandlersRegister() { return eventHandlerRegister(RE_MQTT_EVENTS, ESP_EVENT_ANY_ID, &sensorsMqttEventHandler, nullptr) && eventHandlerRegister(RE_TIME_EVENTS, ESP_EVENT_ANY_ID, &sensorsTimeEventHandler, nullptr) && eventHandlerRegister(RE_GPIO_EVENTS, ESP_EVENT_ANY_ID, &sensorsGpioEventHandler, nullptr) && eventHandlerRegister(RE_SYSTEM_EVENTS, RE_SYS_COMMAND, &sensorsCommandsEventHandler, nullptr) && eventHandlerRegister(RE_SYSTEM_EVENTS, RE_SYS_OTA, &sensorsOtaEventHandler, nullptr); }
Её вы можете более подробно рассмотреть в модуле sensors.cpp. На что здесь стоит обратить внимание:
- Любые прикрепленные к событию данные будут автоматически уничтожены после того как цикл событий отправит его всем подписчикам. Поэтому стоит создать локальную копию полученной команды-строки с помощью malloc_string((char*)event_data), дабы чего не вышло. Впрочем, наверное я перестраховываюсь.
- Далее разбиваем полученную строку на токены , разделенные пробелами с помощью системной функции strtok(buf, seps); чтобы получить (возможно) параметры команды – имя сенсора и тип экстремума. А затем уже вызываем соответствующую функцию: sensorsResetExtremumsSensors(mode) для сброса всех сенсоров или sensorsResetExtremumsSensor(&sensorBoiler, SENSOR_BOILER_TOPIC, imode) для конкретного сенсора. Реализации этих дополнительных функций вы можете посмотреть самостоятельно – в них нет ничего сложного, я надеюсь.
А теперь посмотрим как это работает (измените имя датчика на необходимое):
В ответ мы должны получить следующее:
Дополнительные команды для теплицы
Наверное, предыдущие пример вам показался ужасно сложным и непонятным. Не расстраивайтесь! Все не так грустно! Просто посмотрите как реализованы дополнительные команды для “полоумной теплицы“:
// Команды управления устройством else if ((cmd != nullptr) && (strcasecmp(cmd, CONFIG_COMMAND_FILLING) == 0)) { rlog_i(logTAG, "Forced filling"); xEventGroupSetBits(_sensorsFlags, BTN_CHANGED | BTN_FILLING); } else if ((cmd != nullptr) && (strcasecmp(cmd, CONFIG_COMMAND_WATERING1) == 0)) { rlog_i(logTAG, "Forced watering 1"); xEventGroupSetBits(_sensorsFlags, BTN_CHANGED | BTN_WATERING1); } else if ((cmd != nullptr) && (strcasecmp(cmd, CONFIG_COMMAND_WATERING2) == 0)) { rlog_i(logTAG, "Forced watering 2"); xEventGroupSetBits(_sensorsFlags, BTN_CHANGED | BTN_WATERING2); };
Вот видите – всё просто – сравнили с “образцом” – выполнили. Только не стоит из обработчика событий выполнять долгие и нудные действия – иначе можете словить малоприятные и н сложно обнаруживаемые глюки – тормоза. Я здесь просто “взвожу” соответствующие флаги в группе событий.
Команды ОПС
Забегая вперед, перечислю дополнительные команды для модуля “охранно-пожарная сигнализация” доступны дополнительные команды:
- alarm off – отключить режим охраны
- alarm full – включить полный режим охраны
- alarm perimiter – включить режим охраны только периметра (режим “дома”)
- alarm garage – включить режим охраны только внешних помещений (если установлены соответствующие зоны)
- alarm cancel – отменить тревогу (выключить сирену) без отключения режима охраны
- alarm reset – сбросить счетчик тревог без отключения сирены и изменения режима охраны
Переключить режим охраны можно и другими способами (с пульта 433 Мгц или через MQTT, например), а вот две последние команды можно выполнить только через топик команд system/terminal.
А баба Яга против!
А что если мне не нравится название топиков и (или) команд? Что ж, это легко исправить: открываете файл с:\PlatformIO\libs\consts\def_mqtt_commands.h
и исправляете название топика команд по своему разумению:
Ну а сами команды вы можете легко поправить, просто кликнув на соответствующий макрос с клавишей [ctrl]:
На этом, пожалуй, на сегодня закончим. Всем добра и до следующих встреч!
💠 Полный архив статей вы найдете здесь
Пожалуйста, оцените статью: