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

Добрый день, уважаемый читатель!

Данная статья является в первую продолжением серии “Термостат на 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-брокеру, оно “подписывается” на указанный выше топик “%location%/%device%/system/terminal” и ждет новых сообщений. Про поступлении нового сообщения в указанном топике устройство обрабатывает его и само стирает данные из топика, отправив туда NULL. Это является подтверждением того, что команда была получена и обработана должным образом. Дополнительно отправляется сообщение в telegram-канал управления:

 

Вся “физическая” обработка команд реализована в функции 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]:

 


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

💠 Полный архив статей вы найдете здесь


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

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

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