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

Термостат на ESP32 с удаленным управлением. Часть 10. Охранно-пожарная и аварийная сигнализация

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

В данной статье я расскажу как подключить к вашему проекту на ESP32 и ESP-IDF модуль reAlarm, и тем самым добавить в него функции охранно-пожарной и аварийной сигнализации. Данная статья является логичным продолжением и завершением серии статей “Термостат + ОПС”, но никто не запрещает вам применить его и в других ваших проектах. Краткое содержание предыдущих серий:

 


Концепция библиотеки reAlarm

Для начала определимся с терминами и понятиями, которые будут использоваться в дальнейшем в данной статье. А заодно разберемся с принципами функционирования библиотеки на текущий момент времени.

Дисклеймер.

  1. Все, что описано в данной статье – применимо в первую очередь для частных домовладений и квартир, где пока что не требуется обязательная установка только сертифицированных устройств охранно-пожарных сигнализаций, лицензирование и контроль со стороны соответствующих “органов”.
  2. Все что вы делаете – исключительно на ваш страх и риск, я не могу дать никаких предварительных гарантий в ваших самодельных конструкциях и прошивках.
  3. Я не являюсь профессиональным монтажником или инженером ОПС, поэтому некоторые фразы или термины могут показаться профессионалам из данной области некорректными или неуместными. Что ж, в этом случае можете поправить меня в комментариях.

 

Используемые датчики или сенсоры

Основной частью любого устройства сигнализации являются сенсоры. В данной библиотеке (и в данном устройстве, соответственно) вы можете использовать как проводные, так и  беспроводные сенсоры, работающие на частоте 433 MHz. Количество подключаемых датчиков ограничено только физическими возможностями микроконтроллера (по наличию свободных GPIO) и памятью устройства. Для подключения проводных датчиков можно так же легко использовать I2C-расширители GPIO

Это могут быть:

  • Герконы или датчики дверей и окон, ворот и т.д. которые размыкаются при открытии окна или двери
  • PIR-датчики движения для обнаружения движения теплых объектов внутри охраняемых помещений
  • Датчики протечки, уровня воды, угарного и горючих газов – то есть датчики для контроля технического состояния инженерных коммуникаций дома
  • Датчики дыма и открытого пламени – позволяют своевременно обнаружить пожар и принять меры к его ликвидации (или хотя бы к эвакуации)
  • Датчики контроля сетевого напряжения – позволяют обнаружить пропадание основного сетевого питания дома или квартиры
  • Кнопки от дверных 433-MHz звонков – можно использовать как тревожные кнопки, либо просто прислать уведомление на телефон

Данный список далеко не полный, вы можете дополнить его своими вариантами. Библиотека может обрабатывать сигналы и сообщения с датчиков трех типов: проводных, беспроводных и виртуальных.

🔶 Проводные датчики подключаются к входам устройства путем двухпроводного или трехпроводного шлейфа. Количество проводных датчиков ограничено только наличием свободных GPIO на вашем микроконтроллере, а также можно задействовать расширители различные GPIO. Поддерживаются как нормально замкнутые датчики, так и датчики с нормально разомкнутым выходом. Питание датчиков не зависит от напряжения питания микроконтроллера – например это могут быть стандартные промышленные 12-вольтовые PIR. Если рассматривать устройство “Термостат + ОПС”, то схема подключения проводных датчиков выглядит следующим образом:

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

🔶 Беспроводные датчики 433 Мгц вы можете использовать, если по каким-либо причинам невозможно использовать проводные. Про принципы их работы я уже рассказывал в другой статье, сами датчики планирую подробнее рассмотреть позже. 

Использование беспроводных датчиков для охраны, в принципе, не является безопасной практикой, так как их сигналы легко подавить внешней помехой на той же частоте. Однако использование беспроводных инженерных датчиков вполне оправдано, так как подавлять их нет особого смысла, а тянуть провод под мойку или стиральную машину для датчиков протечки – ну так себе удовольствие. Можно также легко принимать и обрабатывать сигналы от кнопок беспроводных звонков – таким образом вы получите уведомление на телефон, что кто-то ломится к вам в двери в ваше отсутствие.

В качестве датчиков так же могут выступать различные беспроводные пульты управления для переключения режима охраны – но учтите, что их довольно легко перехватить и имитировать. Впрочем, в подавляющем большинстве китайских WiFi и GSM-сигнализаций беспроводные 433 МГц датчики и пульты используются вообще без зазрения совести. Да и от деревенских синяков, ищущих чем-бы поживиться на вашей даче, уж точно защитят.

Также вы можете использовать в качестве датчиков множество DIY-модулей с AliExpress, как пяти-вольтовых, так и трех-вольтовых, при условии соответствующего подключения к микроконтроллеру. Здесь всё зависит только от вашей фантазии и бюджета.

🔶 Виртуальные датчики. Кроме датчиков, которые подключены тем или иным способом к вашему устройству, библиотека позволяет использовать данные с других “умных” устройств, созданных по той же самой технологии. Для этого можно использовать разные способы обмена данными между ESP, но проще всего – использовать локальный MQTT-брокер со специальными скрытыми  от внешних систем топиками. Это позволяет задействовать в работе “центрального пульта” сенсоры, которые физически подключены к другому контроллеру. А так же передавать данные о сигналов с датчиков на другие устройства. Локальный MQTT брокер позволяет сделать это с минимальными задержками, даже есть отсутствует доступ к “глобальному” интернету. 

Например, у меня имеется несколько относительно автономных устройств: в доме, в гараже, в бане, в теплице. Все они подключены к моей локальной сети и локальному брокеру, который запущен на домашнем роутере. Да, использование wifi-это не 100% надежно, но зато очень просто и очень легко. Но потребуется предпринять дополнительные меры по поддержанию бесперебойной работы роутера.

В гараже установлены несколько проводных датчиков движения и датчики двери и подъемных ворот. Контроллер гаража использует данные с них для управления освещением. Но вместе с этим, сигналы с этих датчиков одновременно передаются на центральное устройство, которые выполняет функции ОПС (кроме прочих). Получается, что одни и те же датчики одновременно используются для двух совершенно разных целей – автоматики освещения и охраны. Там же есть датчики дыма, сигналы с которых так же передаются на центральный “пульт” дома.

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

 

Сигналы (сообщения) с датчиков

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

  • alarm (тревога) – это сигнал на пульт о любом событии, на которое рассчитан ваш датчик: открытие двери, движение в зоне охраны, нажатие кнопки, дым, протечка воды и т.д.
  • alarm cancel – отмена (сброс) тревоги – сигнал о восстановлении спокойствия: закрытие двери, устранение протечки, восстановление нормального процесса и т.д. 
  • tamper – сигнал о попытке вскрытия или повреждения датчика или шлейфа охраны
  • low power – низкий уровень заряда батареи – для устройств с автономным питанием
  • и  т.д.

Как правило, “отмена тревоги” генерируется только проводными датчиками (например самыми обычными герконами или концевиками), поэтому в беспроводных сенсорах сброс тревоги происходит автоматически спустя какое-то время по таймеру. Иногда необходимо сбросить сам датчик после сигнала тревоги путем кратковременного снятия питания – например для пожарных датчиков, что также может делаться автоматически.

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

 

Зоны охраны

Но сами по себе сигналы и сообщения с датчиков ничего в системе не генерируют – они просто подаются “на вход” той или иной зоны охраны. А уже настройки зоны определяют, каким образом необходимо реагировать на поступающие сигналы. Таким образом зона охраны – это виртуальная группа, которая позволяет логически объединять сигналы с датчиков и определять (настраивать) реакции на них в разных режимах работы системы.

Зоны могут быть организованы как по физическому расположению, так и по логическим функциям, например:

  • Зона “периметр дома” – датчики дверей и окон, срабатывание которых говорит о том, что в помещение пытаются проникнуть извне. В эту же зону можно включить датчики вибрации, которые сработают при разбитии окна, например.
  • Зона “внутри дома” – как правило в эту зону стоит “завести” сигналы с датчиков движения внутри охраняемого помещения.
  • Зона “вне дома”датчики движения во дворе используются для управления освещением, а в режиме охраны – выдадут сигнал на запись для камеры и пришлют тихое уведомление на телефон, не поднимая паники и не включая сирены.
  • Зона “tamper”это может быть виртуальная зона с круглосуточной охраной, куда стекаются данные со всех датчиков контроля целостности. Таким образом любой сигнал в этой зоне вызовет выдачу уведомлений вне зависимости от того, включена охрана или нет.
  • Зона “пожар”– зона с безусловной круглосуточной тревогой в любом режиме охраны.
  • Инженерные системы – при обнаружении сигнала тревоги в этой зоне присылается уведомление на телефон и предпринимаются меры по устранению аварии – перекрывается вода, газ и т.д.

Список зон вы можете расширить или изменить по своим вкусам и понятиям. Разные сигналы одного и того же датчика можно направлять в разные зоны с разными типами реакции.

 

Режимы работы

На текущий момент библиотека поддерживает четыре режима работы:

  • ASM_DISABLED  – Режим охраны отключен. Реакции на датчики движения внутри охраняемой зоны и по её периметры (двери, окна) – нет. Инженерные и пожарные датчики обрабатываются в полном объеме.
  • ASM_ARMEDПолный режим охраны. Система реагирует на сигналы любых датчиков, подключенных к устройству тем или иным способом. При нарушении включается сирена и световой маячок.
  • ASM_PERIMETERРежим охраны периметра. Система не реагирует на датчики, установленные внутри охраняемого помещения, однако двери и окна охраняются как при полном режиме охраны. Инженерные и пожарные датчики обрабатываются в полном объеме.
  • ASM_OUTBUILDINGSРежим охраны внешних помещений. Для постановки на охрану только внешних строений, например гаража. Инженерные и пожарные датчики обрабатываются в полном объеме. Вот честно говоря, сам не пользовался ни разу. 

Переключать режимы охраны можно:

  • С помощью MQTT панели управления со смартфона или компьютера
  • С 433 МГц радиопульта
  • С помощью кнопочной станции на панели управления устройством (опционально)

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

 

Оповещения о событиях

Предусмотрены несколько типов оповещений о событиях:

  • Сирена 12в
  • Световой маячок 12в
  • Уведомления в telegram

Сирена включается только в режиме тревоги на заданное время (например 3 минуты). При включении и отключении режима охраны выдается несколько коротких звуковых сигналов – для индикации переключения режима. Сирену можно отключить удаленно, если не вы хотите беспокоить соседей, например.

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

Уведомления в telegram отправляются в группу, в которую вы можете включить всех заинтересованных лиц. Например так:

При наличии шлюза “MQTT – telegram” с обратной связью, вы можете управлять системой охраны непосредственно из telegram.

 

Типы реакции на события

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

  • ASR_ALARM_INC – Увеличить счетчик тревог
  • ASR_ALARM_DEC  – Уменьшить счетчик тревог
  • ASR_MQTT_EVENT  – Публикация события на MQTT
  • ASR_MQTT_STATUS – Публикация состояния охраны на MQTT
  • ASR_TELEGRAM – Уведомление в Telegram
  • ASR_SIREN – Включить сирену
  • ASR_FLASHER – Включить маячок
  • ASR_BUZZER – Звуковой сигнал на пульте
  • ASR_RELAY_ON – Включить реле (нагрузку)
  • ASR_RELAY_OFF – Выключить реле (нагрузку)
  • ASR_RELAY_SWITCH – Переключить реле (нагрузку)

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

// "Стандартные" наборы реакций
ASRS_NONE         = 0x0000; // Никакой реакции (по умолчанию)
ASRS_CONTROL      = ASR_MQTT_EVENT | ASR_MQTT_STATUS;
ASRS_REGISTER     = ASR_MQTT_EVENT | ASR_MQTT_STATUS;
ASRS_ONLY_NOTIFY  = ASR_MQTT_EVENT | ASR_MQTT_STATUS | ASR_TELEGRAM;
ASRS_FLASH_NOTIFY = ASR_MQTT_EVENT | ASR_MQTT_STATUS | ASR_TELEGRAM | ASR_FLASHER;
ASRS_ALARM_NOTIFY = ASR_ALARM_INC | ASR_MQTT_EVENT | ASR_MQTT_STATUS | ASR_TELEGRAM | ASR_BUZZER;
ASRS_ALARM_SILENT = ASR_ALARM_INC | ASR_MQTT_EVENT | ASR_MQTT_STATUS | ASR_TELEGRAM | ASR_BUZZER | ASR_FLASHER;
ASRS_ALARM_SIREN  = ASR_ALARM_INC | ASR_MQTT_EVENT | ASR_MQTT_STATUS | ASR_TELEGRAM | ASR_BUZZER | ASR_SIREN | ASR_FLASHER;
ASRS_POWER_ON     = ASR_MQTT_EVENT | ASR_MQTT_STATUS | ASR_TELEGRAM | ASR_FLASHER;
ASRS_POWER_OFF    = ASR_ALARM_INC | ASR_MQTT_EVENT | ASR_MQTT_STATUS | ASR_TELEGRAM | ASR_BUZZER | ASR_FLASHER;

Таким образом ASRS_NONE означает полный игнор, ASRS_ONLY_NOTIFY – только уведомления в telegram,  ASRS_ALARM_SILENT – тихую тревогу без сирены, а ASRS_ALARM_SIREN – тревогу по полной программе, с сиреной, блекджеком и …

 

С концепцией разобрались, переходим к практической части – добавим в наш проект соответствующие функции.

 


Подключаем библиотеку к проекту

Все настройки датчиков, зон охраны и реакций на них осуществляются только программистом на этапе создания прошивки. Никаких экранных меню, web-интерфейсов и прочих способов настройки системы охраны в run-time не предусмотрено (и не будет предусмотрено в моем варианте исполнения). Я не вижу никакой необходимости усложнять прошивку, добавлять сохранение всех этих параметров на flash, и кроме того, выполнять эти настройки не очень удобными способами. При необходимости внесения изменений мне гораздо проще внести изменения сразу в программный код и обновить устройство посредством OTA-технологии из любой точки, где есть интернет. Это и проще, и экономичнее, и быстрее, и зачастую удобнее.

Итак, приступим.

Первым делом нужно добавить ещё одну “локальную библиотеку” (подкаталог) в наш проект. В ней мы создадим файл, в котором и будет производиться вся настройка системы охраны и технического контроля помещений. Назовем эту “библиотеку” – security.cpp, а хедер к ней – security.h. Каталог, соответственно, можно назвать security:

В них объявим одну единственную функцию (пока пустую), которая и будут производить всю работу (один раз при запуске устройства), например так:

/* 
   Модуль настроек охранно-пожарной сигнализации с управлением через MQTT и Telegram
   --------------------------
   (с) 2021-2024 Разживин Александр | Razzhivin Alexander
   kotyara12@yandex.ru | https://kotyara12.ru | tg: @kotyara1971
*/

#ifndef __SECURITY_H__
#define __SECURITY_H__

#include "reRx433.h"
#include "reAlarm.h"

#ifdef __cplusplus
extern "C" {
#endif

void alarmStart();

#ifdef __cplusplus
}
#endif

#endif // __SECURITY_H__

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

Кроме того, нам потребуются некоторые новые константы, которые необходимо добавить в project_config.h:

// -----------------------------------------------------------------------------------------------------------------------
// -------------------------------------------------- EN - Security ------------------------------------------------------
// ------------------------------------------------ RU - Сигнализация ----------------------------------------------------
// -----------------------------------------------------------------------------------------------------------------------

// EN: Use static memory allocation for the fire alarm task
// RU: Использовать статическое выделение памяти для задачи охранно-пожарной сигнализации
#define CONFIG_ALARM_STATIC_ALLOCATION 1
// EN: Stack size for the fire alarm task
// RU: Размер стека для задачи охранно-пожарной сигнализации
#define CONFIG_ALARM_STACK_SIZE 4098
// EN: Queue size for the fire alarm task
// RU: Размер очереди для задачи охранно-пожарной сигнализации
#define CONFIG_ALARM_QUEUE_SIZE 32
// EN: Device topic for OPS
// RU: Топик устройства для ОПС
// #define CONFIG_ALARM_MQTT_DEVICE_TOPIC "home"
// EN: Publish the status of OPS sensors in local topics for transmission to other devices
// RU: Публиковать состояние сенсоров ОПС в локальных топиках для передачи на другие устройства
#define CONFIG_ALARM_LOCAL_PUBLISH true
// EN: Scheme of OPS topics: 0 - %location%/config/security/mode; 1 - %location%/%device%/config/security/mode
// RU: Схема топиков ОПС: 0 - %location%/config/security/mode; 1 - %location%/%device%/config/security/mode
#define CONFIG_ALARM_MQTT_DEVICE_MODE 0
// EN: Scheme of OPS topics: 0 - %location%/security/events/%zone%; 1 - %location%/%device%/security/events/%zone%
// RU: Схема топиков ОПС: 0 - %location%/security/events/%zone%; 1 - %location%/%device%/security/events/%zone%
#define CONFIG_ALARM_MQTT_DEVICE_EVENTS 0
// EN: Scheme of OPS topics: 0 - %location%/security/status/%device%; 1 - %location%/%device%/security/status
// RU: Схема топиков ОПС: 0 - %location%/security/status/%device%; 1 - %location%/%device%/security/status
#define CONFIG_ALARM_MQTT_DEVICE_STATUS 0
// EN: When disabling the alarm from the remote control, immediately disarm; otherwise disable the alarm without disarming
// RU: При отключении тревоги с пульта сразу же снять с охраны; иначе отключить тревогу без снятия с охраны
#define CONFIG_ALARM_TOGETHER_DISABLE_SIREN_AND_ALARM 1

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

Далее нам необходимо запустить задачу (её код уже написан в недрах библиотеки) и настроить “оборудование”, которое будет работать в нашей системе – сирену, маячок, приемник 433 Mhz. Для этого напишем такую функцию:

void alarmInitDevices()
{
  rlog_i(logTAG, "Initialization of AFS devices");

  // Создаем светодиоды, сирену и флешер
  ledQueue_t ledAlarm = nullptr;
  #if defined(CONFIG_GPIO_ALARM_LED) && (CONFIG_GPIO_ALARM_LED > -1)
    ledAlarm = ledTaskCreate(CONFIG_GPIO_ALARM_LED, true, true, "led_alarm", CONFIG_LED_TASK_STACK_SIZE, nullptr);
    ledTaskSend(ledAlarm, lmOff, 0, 0, 0);
  #endif // CONFIG_GPIO_ALARM_LED
  ledQueue_t siren = nullptr;
  #if defined(CONFIG_GPIO_ALARM_SIREN) && (CONFIG_GPIO_ALARM_SIREN > -1)
    siren = ledTaskCreate(CONFIG_GPIO_ALARM_SIREN, true, false, "siren", CONFIG_LED_TASK_STACK_SIZE, nullptr);
    ledTaskSend(siren, lmOff, 0, 0, 0);
  #endif // CONFIG_GPIO_ALARM_SIREN
  ledQueue_t flasher = nullptr;
  #if defined(CONFIG_GPIO_ALARM_FLASH) && (CONFIG_GPIO_ALARM_FLASH > -1)
    flasher = ledTaskCreate(CONFIG_GPIO_ALARM_FLASH, true, true, "flasher", CONFIG_LED_TASK_STACK_SIZE, nullptr);
    ledTaskSend(flasher, lmBlinkOn, 1, 100, 5000);
  #endif // CONFIG_GPIO_ALARM_FLASH
  
  // Запускаем задачу
  alarmTaskCreate(siren, flasher, buzzer, ledAlarm, ledAlarm, nullptr);

  // Запускаем приемник RX 433 MHz
  #ifdef CONFIG_GPIO_RX433
    rx433_Init(CONFIG_GPIO_RX433, alarmTaskQueue());
    rx433_Enable();
  #endif // CONFIG_GPIO_RX433
}

Вызов этой функции добавим в ранее созданную void alarmStart().

Пробуем компилировать проект – если всё удачно, можно переходить к следующему этапу – настройке зон и датчиков.

 


Настраиваем зоны охраны и контроля

Вначале нам необходимо определить зоны охраны и настроить реакции в различных режимах работы. Делается это с помощью функций alarmZoneAdd() и alarmResponsesSet().

Добавить зону в список не просто, а очень просто: alarmZoneHandle_t zoneVariable = alarmZoneAdd("Понятное имя зоны", "topic", callback);

Например, это может выглядеть так:

// Двери (периметр) :: создаем зону охраны
alarmZoneHandle_t azDoors = alarmZoneAdd(
  "Двери",           // Понятное название зоны
  "doors",           // MQTT-топик зоны
  nullptr            // Функция управления реле, при необходимости
);

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

Далее нам необходимо настроить реакции для данной зоны для всех четырех режимов работы:

Пример использования:

// Настраиваем реакции для данной зоны в разных режимах
alarmResponsesSet(
  azDoors,           // Ссылка на зону охраны
  ASM_DISABLED,      // Настраиваем реакции для режима ASM_DISABLED - "охрана отключена"
  ASRS_REGISTER,     // Реакция на события тревоги: только регистрация (фактически это приводит к публикации его на MQTT)
  ASRS_REGISTER      // Реакция на отмену тревоги: только регистрация (фактически это приводит к публикации его на MQTT)
);
alarmResponsesSet(
  azDoors,           // Ссылка на зону охраны
  ASM_ARMED,         // Настраиваем реакции для режима ASM_ARMED - "полная охрана"
  ASRS_ALARM_SIREN,  // Реакция на события тревоги: включить сирену и отправить уведомления
  ASRS_REGISTER      // Реакция на отмену тревоги: только регистрация (фактически это приводит к публикации его на MQTT)
);
alarmResponsesSet(
  azDoors,           // Ссылка на зону охраны
  ASM_PERIMETER,     // Настраиваем реакции для режима ASM_PERIMETER - "только периметр (дома)" 
  ASRS_ALARM_SIREN,  // Реакция на события тревоги: включить сирену и отправить уведомления
  ASRS_REGISTER      // Реакция на отмену тревоги: только регистрация (фактически это приводит к публикации его на MQTT)
);
alarmResponsesSet(
  azDoors,           // Ссылка на зону охраны
  ASM_OUTBUILDINGS,  // Настраиваем реакции для режима ASM_OUTBUILDINGS - "внешние помещения" 
  ASRS_ALARM_NOTIFY, // Реакция на события тревоги: тихая тревога - отправить уведомления, но сирену не включать
  ASRS_REGISTER      // Реакция на отмену тревоги: только регистрация (фактически это приводит к публикации его на MQTT)
);

То есть для данной зоны:

  • в режиме отключенной охраны происходит только публикация данных на MQTT брокере
  • в режиме полной охраны и охраны периметра – включается сирена, маячок, отправляется уведомление на смартфон, ну и публикация данных на MQTT брокере, само собой
  • в режиме охраны гаража только отправим уведомление на смартфон (на всякий случай), но сирену включать не будем

Точно также настраиваем другие зоны, список придумайте сами. Для примера я создал такие зоны:

// Окна (периметр)
alarmZoneHandle_t azWindows = alarmZoneAdd("Окна", "windows", nullptr);
alarmResponsesSet(azWindows, ASM_DISABLED, ASRS_REGISTER, ASRS_REGISTER);
alarmResponsesSet(azWindows, ASM_ARMED, ASRS_ALARM_SIREN, ASRS_REGISTER);
alarmResponsesSet(azWindows, ASM_PERIMETER, ASRS_ALARM_NOTIFY, ASRS_REGISTER);
alarmResponsesSet(azWindows, ASM_OUTBUILDINGS, ASRS_ALARM_NOTIFY, ASRS_REGISTER);

// Дом (внутренние помещения)
alarmZoneHandle_t azIndoor = alarmZoneAdd("Дом", "indoor", nullptr);
alarmResponsesSet(azIndoor, ASM_DISABLED, ASRS_REGISTER, ASRS_REGISTER);
alarmResponsesSet(azIndoor, ASM_ARMED, ASRS_ALARM_SIREN, ASRS_REGISTER);
alarmResponsesSet(azIndoor, ASM_PERIMETER, ASRS_REGISTER, ASRS_REGISTER);
alarmResponsesSet(azIndoor, ASM_OUTBUILDINGS, ASRS_REGISTER, ASRS_REGISTER);

// Двор (внешние датчики - только уведомления, без сирен и тревоги)
alarmZoneHandle_t azOutdoor = alarmZoneAdd("Двор", "outdoor", nullptr);
alarmResponsesSet(azOutdoor, ASM_DISABLED, ASRS_REGISTER, ASRS_REGISTER);
alarmResponsesSet(azOutdoor, ASM_ARMED, ASRS_ALARM_NOTIFY, ASRS_REGISTER);
alarmResponsesSet(azOutdoor, ASM_PERIMETER, ASRS_ALARM_NOTIFY, ASRS_REGISTER);
alarmResponsesSet(azOutdoor, ASM_OUTBUILDINGS, ASRS_ALARM_NOTIFY, ASRS_REGISTER);

// Датчики дыма и пламени - тревога 24*7
alarmZoneHandle_t azFire = alarmZoneAdd("Пожар", "fire", nullptr);
alarmResponsesSet(azFire, ASM_DISABLED, ASRS_ALARM_SILENT, ASRS_ALARM_NOTIFY);
alarmResponsesSet(azFire, ASM_ARMED, ASRS_ALARM_SIREN, ASRS_ALARM_NOTIFY);
alarmResponsesSet(azFire, ASM_PERIMETER, ASRS_ALARM_SIREN, ASRS_ALARM_NOTIFY);
alarmResponsesSet(azFire, ASM_OUTBUILDINGS, ASRS_ALARM_SIREN, ASRS_ALARM_NOTIFY);

// Tamper (попытки вскрытия системы)
alarmZoneHandle_t azTamper = alarmZoneAdd("Tamper", "tamper", nullptr);
alarmResponsesSet(azTamper, ASM_DISABLED, ASRS_ALARM_SIREN, ASRS_ALARM_NOTIFY);
alarmResponsesSet(azTamper, ASM_ARMED, ASRS_ALARM_SIREN, ASRS_ALARM_NOTIFY);
alarmResponsesSet(azTamper, ASM_PERIMETER, ASRS_ALARM_SIREN, ASRS_ALARM_NOTIFY);
alarmResponsesSet(azTamper, ASM_OUTBUILDINGS, ASRS_ALARM_SIREN, ASRS_ALARM_NOTIFY);

// Контроль сетевого напряжения
alarmZoneHandle_t azPower = alarmZoneAdd("Контроль питания", "power", nullptr);
alarmResponsesSet(azPower, ASM_DISABLED, ASRS_REGISTER, ASRS_REGISTER);
alarmResponsesSet(azPower, ASM_ARMED, ASRS_REGISTER, ASRS_REGISTER);
alarmResponsesSet(azPower, ASM_PERIMETER, ASRS_REGISTER, ASRS_REGISTER);
alarmResponsesSet(azPower, ASM_OUTBUILDINGS, ASRS_REGISTER, ASRS_REGISTER);

// Инженерные системы: протечка воды, утечка газа, датчик(и) угарного газа и т.д.
alarmZoneHandle_t azTech = alarmZoneAdd("Инженерные системы", "tech", nullptr);
alarmResponsesSet(azTech, ASM_DISABLED, ASRS_ALARM_SILENT, ASRS_ALARM_NOTIFY);
alarmResponsesSet(azTech, ASM_ARMED, ASRS_ALARM_SILENT, ASRS_ALARM_NOTIFY);
alarmResponsesSet(azTech, ASM_PERIMETER, ASRS_ALARM_SILENT, ASRS_ALARM_NOTIFY);
alarmResponsesSet(azTech, ASM_OUTBUILDINGS, ASRS_ALARM_SILENT, ASRS_ALARM_NOTIFY);

// Тревожные кнопки
alarmZoneHandle_t azButtons = alarmZoneAdd("Тревожные кнопки", "buttons", nullptr);
alarmResponsesSet(azButtons, ASM_DISABLED, ASRS_ALARM_SIREN, ASRS_ALARM_NOTIFY);
alarmResponsesSet(azButtons, ASM_ARMED, ASRS_ALARM_SIREN, ASRS_ALARM_NOTIFY);
alarmResponsesSet(azButtons, ASM_PERIMETER, ASRS_ALARM_SIREN, ASRS_ALARM_NOTIFY);
alarmResponsesSet(azButtons, ASM_OUTBUILDINGS, ASRS_ALARM_SIREN, ASRS_ALARM_NOTIFY);

// 433 MHz пульты управления
alarmZoneHandle_t azRemoteControls = alarmZoneAdd("Пульты управления", "controls", nullptr);
alarmResponsesSet(azRemoteControls, ASM_DISABLED, ASRS_CONTROL, ASRS_CONTROL);
alarmResponsesSet(azRemoteControls, ASM_ARMED, ASRS_CONTROL, ASRS_CONTROL);
alarmResponsesSet(azRemoteControls, ASM_PERIMETER, ASRS_CONTROL, ASRS_CONTROL);
alarmResponsesSet(azRemoteControls, ASM_OUTBUILDINGS, ASRS_CONTROL, ASRS_CONTROL);

Как видите – ничего особо сложного, но тем не менее вы можете настроить реакции для каждой зоны с максимально возможной гибкостью. Например вы можете определить свои наборы бит-реакций и использовать их. Теперь осталось добавить датчики в только что созданные зоны.

 


Добавляем датчики охраны и контроля инженерных систем

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

После настройки зон уже можно добавлять в систему сами датчики с помощью функции alarmSensorAdd(alarm_sensor_type_t type, const char* name, const char* topic, bool local_publish, uint32_t address):

Тип датчика должен быть может принимать значения:

  • AST_WIRED – проводная зона (любого типа)
  • AST_RX433_GENERIC – беспроводной сенсор, без выделения команд
  • AST_RX433_20A4C – беспроводной сенсор, общая длина кода 24 бит: 20 бит – адрес, последние 4 бита – команда
  • AST_MQTT – виртуальный сенсор, получение данных с других устройств через локальный MQTT брокер

После этого необходимо настроить события (команды) для этого датчика. Всего можно добавить до 4 событий включительно, но для проводных датчиков он всегда один (даже если ваш датчик имеет tamper – в этом случае его следует настроить как отдельный gpio датчик). Делается это с помощью следующей другой функции:

Некоторые параметры, возможно, требуют особого пояснения:

  • type определяет, какого типа сигнал мы получили: тревога, tamper, нажатие кнопки на пульте и т.д.
  • value_set и value_clear – определяют логические уровни для установки и снятия тревоги, а для беспроводных датчиков – это коды сообщений (команд).
  • message_set и message_clr – сообщения для отправки уведомлений в telegram при получении данного события (в режиме охраны, разумеется), например
    #define CONFIG_ALARM_EVENT_MESSAGE_MOTION "? Обнаружено движение".
  • timeout_clr – если ваш датчик не может уведомлять об сбросе тревоги (например для беспроводных датчиков), то можно добавить таймер для автоматической отмены тревоги спустя заданное время в миллисекундах
  • alarm_confirm – если вы хотите настроить датчик так, чтобы он вызывал тревогу только после подтверждающих сигналов с этого же или других датчиков в течение заданного времени – это позволит минимизировать ложные тревоги.

А теперь давайте рассмотрим разные типы датчиков отдельно.

 


Подключение проводных датчиков ко встроенным GPIO микроконтроллера

Рассмотрим подключение проводных датчиков к встроенным GPIO микроконтроллера. Можно использовать датчики и устройства как с нормально-замкнутыми контактами, так и с нормально-разомкнутыми (но на разных GPIO, разумеется). В данном случае вы можете использовать:

  • дверные и оконные датчики на основе геркона и магнита, либо любые концевые выключатели
  • промышленные PIR-датчики движения, которые используются в промышленных ОПС, например Paradox
  • DIY-модули с AliExpress, например те же самые датчики движения, датчики ИК-излучения (пламени), лазерные датчики и т.д. и т.п. – то есть любые DIY-модули с цифровым выходом
  • можно использовать выходы “сухой контакт” промышленных сенсоров, например газовых сигнализаторов.

В простейшем случае GPIO-входы можно защитить только RC-фильтром; для промышленных 12-вольтовых ОПС-датчиков я рекомендую такую схему, она позволяет согласовать уровни и не спалить микроконтроллер при случайном замыкании сигнального провода шлейфа на +12в:

 

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

// -----------------------------------------------------------------------------------------------------------------------
// --------------------------------------------------- Проводные входы ---------------------------------------------------
// -----------------------------------------------------------------------------------------------------------------------

// Объекты reGPIO для обработки прерываний по проводным входым ОПС
static reGPIO gpioAlarm1(CONFIG_GPIO_ALARM_ZONE_1, CONFIG_GPIO_ALARM_LEVEL, false, true, CONFIG_BUTTON_DEBOUNCE_TIME_US, nullptr);
static reGPIO gpioAlarm2(CONFIG_GPIO_ALARM_ZONE_2, CONFIG_GPIO_ALARM_LEVEL, false, true, CONFIG_BUTTON_DEBOUNCE_TIME_US, nullptr);
static reGPIO gpioAlarm3(CONFIG_GPIO_ALARM_ZONE_3, CONFIG_GPIO_ALARM_LEVEL, false, true, CONFIG_BUTTON_DEBOUNCE_TIME_US, nullptr);
static reGPIO gpioAlarm4(CONFIG_GPIO_ALARM_ZONE_4, CONFIG_GPIO_ALARM_LEVEL, false, true, CONFIG_BUTTON_DEBOUNCE_TIME_US, nullptr);
static reGPIO gpioAlarm5(CONFIG_GPIO_ALARM_ZONE_5, CONFIG_GPIO_ALARM_LEVEL, false, true, CONFIG_BUTTON_DEBOUNCE_TIME_US, nullptr);

Здесь мы указали номера выводов, к которым будут подключены проводные датчики, и их активный уровень – в данном примере он одинаков для всех выводов сразу #define CONFIG_GPIO_ALARM_LEVEL 0x01. Затем необходимо настроить эти GPIO с помощью метода initGPIO().

После этого можно добавлять в систему сами датчики с помощью функции alarmSensorAdd() и настраивать их. Например так:

// Проводная зона 1: входная дверь
gpioAlarm1.initGPIO();
alarmSensorHandle_t asWired1 = alarmSensorAdd(
  AST_WIRED,                                      // Тип датчика: проводные датчики
  "Входная дверь",                                // Понятное имя датчика
  "door",                                         // Топик датчика
  CONFIG_ALARM_LOCAL_PUBLISH,                     // Использовать локальные топики для передачи сигналов на другие устройства, в примере = TRUE (0x01)
  CONFIG_GPIO_ALARM_ZONE_1                        // Номер вывода или адрес датчика
);
if (asWired1) {
  alarmEventSet(asWired1, azDoors, 0, ASE_ALARM, 
    1, CONFIG_ALARM_EVENT_MESSAGE_DOOROPEN,       // Сообщение при сигнале тревоги: "🚨 Открыта дверь"
    0, NULL,                                      // Сообщение при отмене тревоги: отсутствует
    1,                                            // Порог срабатывания (нужен только для беспроводных датчиков, для остальных = 1)
    0,                                            // Время автосброса тревоги по таймеру, 0 = отключено
    60,                                           // Период публикации на MQTT брокере
    false);                                       // Тревога без подтверждения с других датчиков
};

Для примера я настроил проводные входы как (вы это сможете найти в примере прошивки с комментариями): 

  1. Датчик двери
  2. PIR-датчик
  3. Подключение к выходу промышленного газосигнализатора
  4. Контроль наличия питания 220В
  5. Контроль уровня напряжения на аккумуляторе (только низкий уровень)
// -----------------------------------------------------------------------------------
// Проводные входы для встроенных GPIO
// -----------------------------------------------------------------------------------

// Проводная зона 1: входная дверь
gpioAlarm1.initGPIO();
alarmSensorHandle_t asWired1 = alarmSensorAdd(
  AST_WIRED,                                      // Тип датчика: проводные датчики
  "Входная дверь",                                // Понятное имя датчика
  "door",                                         // Топик датчика
  CONFIG_ALARM_LOCAL_PUBLISH,                     // Использовать локальные топики для передачи сигналов на другие устройства, в примере = TRUE (0x01)
  CONFIG_GPIO_ALARM_ZONE_1                        // Номер вывода или адрес датчика
);
if (asWired1) {
  alarmEventSet(asWired1, azDoors, 0, ASE_ALARM, 
    1, CONFIG_ALARM_EVENT_MESSAGE_DOOROPEN,       // Сообщение при сигнале тревоги: "🚨 Открыта дверь"
    0, NULL,                                      // Сообщение при отмене тревоги: отсутствует
    1,                                            // Порог срабатывания (нужен только для беспроводных датчиков, для остальных = 1)
    0,                                            // Время автосброса тревоги по таймеру, 0 = отключено
    60,                                           // Период публикации на MQTT брокере
    false);                                       // Тревога без подтверждения с других датчиков
};
 
// Проводная зона 2: PIR сенсор в прихожей
gpioAlarm2.initGPIO();
alarmSensorHandle_t asWired2 = alarmSensorAdd(AST_WIRED, "Прихожая", "hallway", CONFIG_ALARM_LOCAL_PUBLISH, CONFIG_GPIO_ALARM_ZONE_2);
if (asWired2) {
  alarmEventSet(asWired2, azIndoor, 0, ASE_ALARM, 
    1, CONFIG_ALARM_EVENT_MESSAGE_MOTION,         // Сообщение при сигнале тревоги: "🚨 Обнаружено движение"
    0, NULL,                                      // Сообщение при отмене тревоги: отсутствует
    1,                                            // Порог срабатывания (нужен только для беспроводных датчиков, для остальных = 1)
    0,                                            // Время автосброса тревоги по таймеру, 0 = отключено (это проводной PIR, он сам все умеет)
    60,                                           // Период публикации на MQTT брокере
    true);                                        // Тревогу поднимать только при подтверждении с других иди этого же датчика - PIR иногда могут выдавать ложные тревоги 
};

// Проводная зона 3: 
gpioAlarm3.initGPIO();
alarmSensorHandle_t asGasLeak = alarmSensorAdd(AST_WIRED, "Газ", "gas", CONFIG_ALARM_LOCAL_PUBLISH, CONFIG_GPIO_ALARM_ZONE_3);
if (asGasLeak) {
  alarmEventSet(asGasLeak, azTech, 0, ASE_ALARM, 
    1, CONFIG_ALARM_EVENT_MESSAGE_GAS,            // Сообщение при сигнале тревоги: "🚨 Обнаружена утечка газа"
    0, CONFIG_ALARM_EVENT_MESSAGE_CLEAR,          // Сообщение при отмене тревоги: "🟢 Авария устранена"
    1,                                            // Порог срабатывания (нужен только для беспроводных датчиков, для остальных = 1)
    0,                                            // Время автосброса тревоги по таймеру, 0 = отключено
    60,                                           // Период публикации на MQTT брокере
    false);                                       // Тревога без подтверждения с других датчиков
};

// Проводная зона 4: контроль питания 220В
gpioAlarm4.initGPIO();
alarmSensorHandle_t asPowerMain = alarmSensorAdd(AST_WIRED, "Питание 220В", "main_power", CONFIG_ALARM_LOCAL_PUBLISH, CONFIG_GPIO_ALARM_ZONE_4);
if (asPowerMain) {
  alarmEventSet(asPowerMain, azPower, 0, ASE_POWER, 
    1, CONFIG_ALARM_EVENT_MESSAGE_POWER_MAIN_OFF, // Сообщение при сигнале тревоги: "🔴 Основное питание отключено"
    0, CONFIG_ALARM_EVENT_MESSAGE_POWER_MAIN_ON,  // Сообщение при отмене тревоги: "💡 Основное питание восстановлено"
    1,                                            // Порог срабатывания (нужен только для беспроводных датчиков, для остальных = 1)
    0,                                            // Время автосброса тревоги по таймеру, 0 = отключено
    0,                                            // Без повторной публикации состояния
    false);                                       // Тревога без подтверждения с других датчиков
};

// Проводная зона 5: контроль заряда аккумулятора
gpioAlarm5.initGPIO();
alarmSensorHandle_t asBattery = alarmSensorAdd(AST_WIRED, "Аккумулятор", "battery", false, CONFIG_GPIO_ALARM_ZONE_5);
if (asBattery) {
  alarmEventSet(asBattery, azPower, 0, ASE_POWER, 
    0, CONFIG_ALARM_EVENT_MESSAGE_BATTERY_LOW,    // Сообщение при сигнале тревоги: "🔋 Низкий уровень заряда батареи"
    1, CONFIG_ALARM_EVENT_MESSAGE_BATTERY_NRM,    // Сообщение при отмене тревоги: "🔋 Аккумулятор заряжен"
    1,                                            // Порог срабатывания (нужен только для беспроводных датчиков, для остальных = 1)
    0,                                            // Время автосброса тревоги по таймеру, 0 = отключено
    0,                                            // Без повторной публикации состояния
    false);                                       // Тревога без подтверждения с других датчиков
};

Так как проводные датчики “умеют” выдавать сигнал “отмены” по умолчанию, здесь я не стал использовать таймеры для автоматической отмены тревоги.

 


Подключение проводных датчиков через I2C расширитель входов

Однако встроенных GPIO иногда не хватает для всех хотелок. На помощь придут I2C-расширители входов, например MCP23017. Про них я уже писал ранее на данном сайте.

Вначале необходимо объявить экземпляры драйверов для MCP23017 и GPIO, которые будут использоваться для обработки прерываний от этих расширителей. Например у меня их сразу два:

// Объекты для работы с MCP23017
static reMCP23017 ioexpZones1(CONFIG_IOEXP_ZONES1_BUS, CONFIG_IOEXP_ZONES1_ADDRESS, nullptr);
static reMCP23017 ioexpZones2(CONFIG_IOEXP_ZONES2_BUS, CONFIG_IOEXP_ZONES2_ADDRESS, nullptr);
// GPIO, необходимые для обслуживания прерываний MCP23017
static reGPIO gpioIsrZones1(CONFIG_GPIO_ZONES1_ISR, 0, false, 0, nullptr);
static reGPIO gpioIsrZones2(CONFIG_GPIO_ZONES2_ISR, 0, false, 0, nullptr);

Затем напишем обработчик прерываний для gpioIsrZones1 и gpioIsrZones2, он общий для всех микросхем. Этот обработчик получает сигнал о том, что что-то произвошло на одном из входов прерываний и запускает чтение данных с микросхем расширителей:

static void alarmIoExpIsrEventHandler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data)
{
  if ((event_id == RE_GPIO_CHANGE) && (event_data)) {
    gpio_data_t* data = (gpio_data_t*)event_data;
    if (data->bus == 0) {
      if (data->pin == CONFIG_GPIO_ZONES1_ISR) {
        ioexpZones1.update();
      }
      else if (data->pin == CONFIG_GPIO_ZONES2_ISR) {
        ioexpZones2.update();
      };
    };
  };
}

Теперь осталось добавить превоначальную настройку расширителей в alarmInitDevices():

// Инициализация расширителей портов
rlog_i(logTAG, "Port expanders initialization...");
alarmIoExpIsrRegisterHandlers();
alarmIoExpTimerFireResetInit();

ioexpZones1.configSet(MCP23017_ACTIVE_LOW, true);
ioexpZones1.portSetMode(CONFIG_IOEXP_ZONES1_INPUTS);
ioexpZones1.portSetInterrupt(CONFIG_IOEXP_ZONES1_IN_PIR, MCP23017_INT_ANY_EDGE);
ioexpZones1.portSetInterrupt(CONFIG_IOEXP_ZONES1_IN_FIRE, MCP23017_INT_ANY_EDGE);
ioexpZones1.update();
gpioIsrZones1.initGPIO();

ioexpZones2.configSet(MCP23017_ACTIVE_LOW, true);
ioexpZones2.portSetMode(CONFIG_IOEXP_ZONES2_INPUTS);
ioexpZones2.portSetPullup(CONFIG_IOEXP_ZONES2_IN_FIRE);
ioexpZones2.portSetInterrupt(CONFIG_IOEXP_ZONES2_IN_PIR, MCP23017_INT_ANY_EDGE);
ioexpZones2.portSetInterrupt(CONFIG_IOEXP_ZONES2_IN_FIRE, MCP23017_INT_ANY_EDGE);
ioexpZones2.update();
gpioIsrZones2.initGPIO();

Теперь прерывания с этих микросхем будут перенаправляться в общий поток ввода, как и для обычных GPIO, и мы сможем их использовать почти также, как и в предыдущем случае: 

// IOEXT1 0.0x24 pin 1: Входная дверь
alarmSensorHandle_t asZones1Pin01 = alarmSensorAdd(AST_WIRED, "Входная дверь", "hall", 
  CONFIG_ALARM_IOEXP_SENSOR(CONFIG_IOEXP_ZONES1_BUS, CONFIG_IOEXP_ZONES1_ADDRESS, CONFIG_IOEXP_ZONES1_PIN_1));
if (asZones1Pin01) {
  alarmEventSet(asZones1Pin01, azDoors, 0, ASE_ALARM, 0, CONFIG_ALARM_EVENT_MESSAGE_DOOROPEN, 1, NULL, 1, 0, 60, false);
};

// IOEXT1 0.0x24 pin 2: Прихожая PIR, ALARM
alarmSensorHandle_t asZones1Pin02 = alarmSensorAdd(AST_WIRED, "Прихожая", "hall/pir", 
  CONFIG_ALARM_IOEXP_SENSOR(CONFIG_IOEXP_ZONES1_BUS, CONFIG_IOEXP_ZONES1_ADDRESS, CONFIG_IOEXP_ZONES1_PIN_2));
if (asZones1Pin02) {
  alarmEventSet(asZones1Pin02, azHome, 0, ASE_ALARM, 0, CONFIG_ALARM_EVENT_MESSAGE_MOTION, 1, NULL, 1, 0, 60, true);
};

// IOEXT1 0.0x24 pin 3: Прихожая PIR, TAMPER
alarmSensorHandle_t asZones1Pin03 = alarmSensorAdd(AST_WIRED, "Прихожая", "hall/pir", 
  CONFIG_ALARM_IOEXP_SENSOR(CONFIG_IOEXP_ZONES1_BUS, CONFIG_IOEXP_ZONES1_ADDRESS, CONFIG_IOEXP_ZONES1_PIN_3));
if (asZones1Pin03) {
  alarmEventSet(asZones1Pin03, azTamper, 0, ASE_ALARM, 0, CONFIG_ALARM_EVENT_MESSAGE_TAMPER, 1, CONFIG_ALARM_EVENT_MESSAGE_CLOSED, 1, 0, 0, false);
};

Основное отличие – адрес сенсора “кодируется” с помощью макроса #define CONFIG_ALARM_IOEXP_SENSOR(bus, address, pin) ((((bus)+1) << 16) | ((address) << 8) | (pin)),  то есть в нём зашит номер шины и адрес микросхемы и собственно номер её вывода. Это позволяет однозначно определить, с какого именно вывода поступил сигнал.

 


Подключение проводных дымовых датчиков со сбросом по питанию

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

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

static esp_timer_handle_t timerZones1FireDelay = nullptr;
static esp_timer_handle_t timerZones1FireReset = nullptr;

static void alarmIoExpTimerFire1ResetEnd(void* arg)
{
  rlog_i(logTAG, "Power on fire detectors #1...");
  ioexpZones1.pinWrite(CONFIG_IOEXP_ZONES1_RELAY_RESET, false);
  vTaskDelay(1);
  ioexpZones1.portSetInterrupt(CONFIG_IOEXP_ZONES1_IN_FIRE, MCP23017_INT_ANY_EDGE);
}

static void alarmIoExpTimerFire1ResetStart(void* arg)
{
  rlog_i(logTAG, "Power off fire detectors #1...");
  ioexpZones1.portSetInterrupt(CONFIG_IOEXP_ZONES1_IN_FIRE, MCP23017_INT_DISABLED);
  vTaskDelay(1);
  ioexpZones1.pinWrite(CONFIG_IOEXP_ZONES1_RELAY_RESET, true);
  if (timerZones1FireReset) {
    if (esp_timer_start_once(timerZones1FireReset, CONFIG_ALARM_IOEXP_FIRE_RESET_RESET_US) != ESP_OK) {
      rlog_e(logTAG, "Failed to start fire #1 reset timer");
      vTaskDelay(pdMS_TO_TICKS(CONFIG_ALARM_IOEXP_FIRE_RESET_RESET_US / 1000));
      alarmIoExpTimerFire1ResetEnd(nullptr);
    };
  } else {
    vTaskDelay(pdMS_TO_TICKS(CONFIG_ALARM_IOEXP_FIRE_RESET_RESET_US / 1000));
    alarmIoExpTimerFire1ResetEnd(nullptr);
  };
}

static void alarmIoExpTimerFireResetInit()
{
  if (timerZones1FireDelay == nullptr) {
    esp_timer_create_args_t cfg;  
    memset(&cfg, 0, sizeof(cfg));
    cfg.callback = alarmIoExpTimerFire1ResetStart;
    cfg.name = "fire1_rst_delay"; 
    esp_timer_create(&cfg, &timerZones1FireDelay);
  };
  if (timerZones1FireDelay) {
    if (timerZones1FireReset == nullptr) {
      esp_timer_create_args_t cfg;  
      memset(&cfg, 0, sizeof(cfg));
      cfg.callback = alarmIoExpTimerFire1ResetEnd;
      cfg.name = "fire1_rst_reset"; 
      esp_timer_create(&cfg, &timerZones1FireReset);
    };
  };
}

static bool alarmIoExpZines1FireReset(bool relay_state)
{
  rlog_i(logTAG, "Start fire detectors #1 reset timer...");
  if (timerZones1FireDelay) {
    if (esp_timer_is_active(timerZones1FireDelay)) {
      esp_timer_stop(timerZones1FireDelay);
    };
    if (esp_timer_start_once(timerZones1FireDelay, CONFIG_ALARM_IOEXP_FIRE_RESET_DELAY_US) != ESP_OK) {
      rlog_e(logTAG, "Failed to start fire detectors #1 reset timer");
      alarmIoExpTimerFire1ResetStart(nullptr);  
    };
  } else {
    alarmIoExpTimerFire1ResetStart(nullptr);
  };
  return true;
}

В заключении в настройках зоны указываем callback-функцию для запуска функции сброса:

 

// Пожарные датчики со сбросом, плата 1. Режим охраны: 24/7, полная тревога
alarmZoneHandle_t azFireZ1 = alarmZoneAdd("Пожар (3 WIRE)", "fire", alarmIoExpZines1FireReset);
if (azFireZ1) {
  alarmResponsesSet(azFireZ1, ASM_DISABLED, ASRS_ALARM_SILENT | ASR_RELAY_ON, ASRS_ALARM_NOTIFY);
  alarmResponsesSet(azFireZ1, ASM_ARMED, ASRS_ALARM_SIREN | ASR_RELAY_ON, ASRS_ALARM_NOTIFY);
  alarmResponsesSet(azFireZ1, ASM_PERIMETER, ASRS_ALARM_SIREN | ASR_RELAY_ON, ASRS_ALARM_NOTIFY);
  alarmResponsesSet(azFireZ1, ASM_OUTBUILDINGS, ASRS_ALARM_SIREN | ASR_RELAY_ON, ASRS_ALARM_NOTIFY);
};

 


Подключение беспроводных датчиков 433 МГц

Теперь рассмотрим настройку беспроводных датчиков. Как подключить приемник – я уже писал здесь. На плате рассматриваемого “термостата + ОПС” он уже предусмотрен. Здесь мы рассмотрим только программную настройку.

Как правило, кодовая посылка с такого датчика приходит в виде сообщения длиной 24 бит, причем 20 бит – это адрес собственно датчика, а последние 4 бита – это собственно полезный сигнал или команда. Сами датчики в данной статье я рассматривать не собираюсь – так как их много разных, и у разных датчиков разные команды и возможности.

При получении кодовой посылки она также, как и сигналы с GPIO, автоматически направляется в общий входной поток данных ОПС и там обрабатывается. Как это происходит – в рамках данной статьи я описывать также не буду, если интересно – загляните в недра библиотеки. Рассмотрим только их настройку.

Прежде чем вы добавить датчик в систему, вы должны узнать адрес данного датчика и какие команды он может выдать. Узнать адрес датчика можно и из отладочного вывода c com-порта устройства: 09:12:04 [W] ALARM :: Failed to identify RX433 signal [0x004D1D09]!, но вот со списком поддерживаемых команд сложнее. Некоторые датчики умеют передавать только сигнал тревоги, некоторые – дополнительно сигнал tamper и (или) предупреждение о низком уровне заряда батареи питания. Их определить можно только либо из данных производителя, либо опытным путем – вскрывая датчик и (или) изменяя его напряжение питания и следя за кодами. В следующих статьях я постараюсь рассмотреть некоторые из популярных датчиков, которые попадали мне в лапы.

А пока рассмотрим только процесс добавления оных в прошивку:

// PIR комната 1 (maxkin PIR-600)
alarmSensorHandle_t asPirCorridor = alarmSensorAdd(
  AST_RX433_20A4C,                                // Тип датчика: беспроводной
  "Комната 1",                                    // Понятное имя датчика
  "room1/pir",                                    // Топик датчика
  CONFIG_ALARM_LOCAL_PUBLISH,                     // Использовать локальные топики для передачи сигналов на другие устройства, в примере = TRUE (0x01) 
  0x0004D1D0                                      // Адрес датчика
);
if (asPirCorridor) {
  // Первая команда
  alarmEventSet(asPirCorridor, azIndoor, 0, 
    ASE_ALARM,                                    // Основная команда - движение, её код 0x09
    0x09, CONFIG_ALARM_EVENT_MESSAGE_MOTION,      // Сообщение при сигнале тревоги: "🚨 Обнаружено движение"
    ALARM_VALUE_NONE, NULL,                       // Сообщение при отмене тревоги: отсутствует
    1,                                            // Порог срабатывания: при первом же сигнале
    30*1000,                                      // Время автосброса: 30 секунд
    600,                                          // Период публикации на MQTT брокере
    true);                                        // Тревогу поднимать только при подтверждении с других иди этого же датчика - PIR иногда могут выдавать ложные тревоги 
  // Вторая команда
  alarmEventSet(asPirCorridor, azTamper, 1,   
    ASE_TAMPER,                                   // Команда tamper, её код 0x0D
    0x0D, CONFIG_ALARM_EVENT_MESSAGE_TAMPER,      // Сообщение при сигнале тревоги: "⚠️ Попытка взлома датчика"
    ALARM_VALUE_NONE, NULL,                       // Сообщение при отмене тревоги: отсутствует
    1,                                            // Порог срабатывания: при первом же сигнале
    5*60*1000,                                    // Время автосброса: 5 минут
    0,                                            // Без повторной публикации состояния
    false);                                       // Тревога без подтверждения с других датчиков
};

Основные отличия от рассмотренных ранее проводных датчиков здесь следующие:

  • Тип датчика теперь другой, и вместо номера вывода мы теперь должны указать его адрес
  • Датчик теперь поддерживает сразу две команды – 0x09 и 0x0D
  • Но “отменять” команды он уже не умеет, и вы должны сделать это с помощью таймера (при повторных сигналах с датчика таймер “сдвигается”)
  • Разные команды с одного и того же датчика могут (но не обязаны) быть включены в разные зоны (для разных реакций).

 


Подключение беспроводных пультов управления 433 МГц

Настройка беспроводных пультов управления 433 МГц осуществляется очень похожим образом, так как принцип передачи команд собственно тот же самый.

alarmSensorHandle_t asRC_R2 = alarmSensorAdd(     
  AST_RX433_20A4C,                                // Тип датчика: беспроводной
  "Пульт",                                        // Название пульта
  "rc",                                           // Топик пульта
  false,                                          // Локальные топики не используются
  0x0004F9CB                                      // Адрес пульта
);
if (asRC_R2) {
  alarmEventSet(asRC_R2, azRemoteControls, 0,     // Зона "пультов"
    ASE_CTRL_OFF,                                 // Команда отключения режима охраны
    0x01, NULL,                                   // Код команды 0x01, без сообщений
    ALARM_VALUE_NONE, NULL,                       // Кода отмены нет, без сообщений
    2,                                            // Должно прийти как минимум 2 кодовых посылки для переключения
    3*1000,                                       // Время автосброса: 3 секунды
    0,                                            // Без повторной публикации состояния
    false);                                       // Не требуется подтверждение с других датчиков
  alarmEventSet(asRC_R2, azRemoteControls, 1,     // Зона "пультов"
    ASE_CTRL_ON,                                  // Команда включения режима охраны
    0x08, NULL,                                   // Код команды 0x08, без сообщений
    ALARM_VALUE_NONE, NULL,                       // Кода отмены нет, без сообщений
    2,                                            // Должно прийти как минимум 2 кодовых посылки для переключения
    3*1000,                                       // Время автосброса: 3 секунды
    0,                                            // Без повторной публикации состояния
    false);                                       // Не требуется подтверждение с других датчиков
  alarmEventSet(asRC_R2, azRemoteControls, 2,     // Зона "пультов"
    ASE_CTRL_PERIMETER,                           // Команда включения режима "периметр"
    0x04, NULL,                                   // Код команды 0x04, без сообщений
    ALARM_VALUE_NONE, NULL,                       // Кода отмены нет, без сообщений
    2,                                            // Должно прийти как минимум 2 кодовых посылки для переключения
    3*1000,                                       // Время автосброса: 3 секунды
    0,                                            // Без повторной публикации состояния
    false);                                       // Не требуется подтверждение с других датчиков
  alarmEventSet(asRC_R2, azButtons, 3,            // Зона "тревожные кнопки"
    ASE_ALARM,                                    // Команда "тревога"
    0x02, NULL,                                   // Код команды 0x02, сообщение "🔴 Нажата тревожная кнопка"
    ALARM_VALUE_NONE, NULL,                       // Кода отмены нет, без сообщений
    2,                                            // Должно прийти как минимум 2 кодовых посылки для переключения
    3*1000,                                       // Время автосброса: 3 секунды
    0,                                            // Без повторной публикации состояния
    false);                                       // Не требуется подтверждение с других датчиков
};

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

 


Подключение виртуальных датчиков

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

  • Добавляю в систему фиктивные параметры без сохранения значений в памяти – это позволит MQTT-клиенту подписаться на нужные локальные топики
  • Добавляю подписчика на события изменения данных параметров
  • При получении события отправляем его в недра библиотеки с помощью alarmPostQueueExtId

Например: у меня имеется два “внешних” датчика движения, которые изначально не были связаны с ОПС:

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

Приспособим их для функций сигнализации:

static bool _extToiletPir  = false;
static bool _extKitchenPir = false;

#define EXT_DATA_QOS                  2
#define EXT_DATA_FRIENDLY             "Состояние"

#define EXT_DATA_TOILET_PIR_ID         0xFF000001
#define EXT_DATA_TOILET_PIR_KEY       "toilet_pir"
#define EXT_DATA_TOILET_PIR_TOPIC     "security/home/toilet/pir"     // local/security/home/toilet/pir/status
#define EXT_DATA_TOILET_PIR_FRIENDLY  "Санузел"

#define EXT_DATA_KITCHEN_PIR_ID        0xFF000002
#define EXT_DATA_KITCHEN_PIR_KEY      "kitchen_pir"
#define EXT_DATA_KITCHEN_PIR_TOPIC    "security/home/kitchen/pir"     // local/security/home/kitchen/pir/status
#define EXT_DATA_KITCHEN_PIR_FRIENDLY "Кухня"

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

Далее пишем функцию подписчика:

static void alarmExternalSensorsEventHandler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data)
{
  if (*(uint32_t*)event_data == (uint32_t)&_extToiletPir) {
    if ((event_id == RE_PARAMS_CHANGED) || (event_id == RE_PARAMS_EQUALS)) {
      vTaskDelay(1);
      alarmPostQueueExtId(IDS_MQTT, EXT_DATA_TOILET_PIR_ID, _extToiletPir);
    };
  } else if (*(uint32_t*)event_data == (uint32_t)&_extKitchenPir) {
    if ((event_id == RE_PARAMS_CHANGED) || (event_id == RE_PARAMS_EQUALS)) {
      vTaskDelay(1);
      alarmPostQueueExtId(IDS_MQTT, EXT_DATA_KITCHEN_PIR_ID, _extKitchenPir);
    };
  };
}

И в конце регистрируем все это:

static void alarmExternalSensorsInit()
{
  paramsGroupHandle_t extDataToilet = paramsRegisterGroup(nullptr, 
    EXT_DATA_TOILET_PIR_KEY, EXT_DATA_TOILET_PIR_TOPIC, EXT_DATA_TOILET_PIR_FRIENDLY);
  if (extDataToilet) {
    paramsRegisterValue(OPT_KIND_LOCDATA_ONLINE, OPT_TYPE_U8, nullptr, extDataToilet, 
      CONFIG_ALARM_MQTT_EVENTS_STATUS, EXT_DATA_FRIENDLY, EXT_DATA_QOS, &_extToiletPir);
  };

  paramsGroupHandle_t extDataKitchen = paramsRegisterGroup(nullptr, 
    EXT_DATA_KITCHEN_PIR_KEY, EXT_DATA_KITCHEN_PIR_TOPIC, EXT_DATA_KITCHEN_PIR_FRIENDLY);
  if (extDataKitchen) {
    paramsRegisterValue(OPT_KIND_LOCDATA_ONLINE, OPT_TYPE_U8, nullptr, extDataKitchen, 
      CONFIG_ALARM_MQTT_EVENTS_STATUS, EXT_DATA_FRIENDLY, EXT_DATA_QOS, &_extKitchenPir);
  };

  eventHandlerRegister(RE_PARAMS_EVENTS, RE_PARAMS_CHANGED, alarmExternalSensorsEventHandler, nullptr);
  eventHandlerRegister(RE_PARAMS_EVENTS, RE_PARAMS_EQUALS, alarmExternalSensorsEventHandler, nullptr);
}

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

 


MQTT-топики

Теперь можно скомпилировать код и залить его в наше устройство. Если все прошилось успешно, то вы получите кучку новых топиков, например так:

Основные топики системы ОПС “вынесены за пределы” устройства, то есть начинаются они не с location/device/, а просто location/. Сделано это так потому, чтобы иметь единую систему управления, если по каким либо причинам придется разделить ОПС на несколько разных ESP32. Но это поведение можно изменить с помощью макроса CONFIG_ALARM_MQTT_DEVICE_MODE в project_config.h, который мы и добавили ранее.

Итак, для локации dzen топики будут такими:

  • dzen/config/security/mode – этот топик отвечает за текущий режим работы, здесь его можно посмотреть и изменить при необходимости (без топика подтверждения)
  • dzen/security/events/… – сюда публикуются данные с сенсоров ОПС
  • dzen/local/security/… – сюда публикуются данные с сенсоров ОПС для передачи на другие устройства в пределах локальной сети

В примерах дублирование данных в  dzen/local и dzen/security кажется излишним, но это только потому, что в примере настроен только один публичный сервер MQTT. Для локального брокера картина другая, топики dzen/local не покидают пределов локальной сети (мост на них не реагирует).

Данные в топики сенсоров попадают в “открытом” виде – только статус, и в виде JSON-пакетов с дополнительными атрибутами, например:

  • dzen/local/security/indoor/room1/pir/alarm/json – {“status”: 0,“time”: “03.03.2024 10:55:15”,“time_short”: “03.03.24 10:55”,“unixtime”: 1709452515,“count”: 1}
  • dzen/local/security/indoor/room1/pir/alarm/status – 0 или 1 

Соответственно, топики /status – это машиночитаемые топики, а /json – для настройки на клиенте.

 

Топики дополнительных настроек (как обычно, с подтверждением /config/… + …/confirm/…):

  • dzen/thermostat/config/security/buzzer – разрешить звуковые сигналы на встроенном зуммере
  • dzen/thermostat/config/security/confirmation – время в миллисекундах для подтверждения тревоги, например 60000 – 60 секунд
  • dzen/thermostat/config/security/exit_time – время выхода из помещения в секундах до постановки на охрану, по умолчанию 60 секунд
  • dzen/thermostat/config/security/fix_433_codes – сохранять новые коды RX433 в специальных топиках для “удаленного” поиска новых датчиков, но все “ошибки” тоже будут попадать в этот список
  • dzen/thermostat/config/security/flash_duration – длительность мигания светового маячка в секундах
  • dzen/thermostat/config/security/siren_duration – длительность звуковой сирены в секундах
  • dzen/thermostat/config/security/silent_enabled – разрешить тихий режим для сирены (не включать сирену в указанный ниже период времени)
  • dzen/thermostat/config/security/silent_period – временной интервал для тихого режима сирены

 


Настройка MQTT-клиента

На текущий момент я использую для управления своими устройствами mqtt dash. Про него я писал уже не раз, я думаю вам не составит труда настроить клиент под описываемое устройство.

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

var sJSON = event.getLastPayload();
if (sJSON != '') {
  var data = JSON.parse(sJSON);

  // Формируем текст состояния из трех строк: статус, 
  // последний активный сенсор (или пульт) и время его сработки
  event.text = data['status'] + '\n'
             + data['event']['sensor'] + '\n'
             + data['event']['time_short'];

  var iMode = data['mode'];
  if (iMode == 0) {
    // Охрана отключена
    if (data['annunciator']['summary'] > 0) {
      // Тревога по зонам 24 в настоящий момент
      event.textColor = '#FF0000';
      event.blink = true;
    } else {
      // Тревоги нет, всё тихо
      event.textColor = '#9ACD32';
      event.blink = false;
    };
  } else {
    // Мигание, если были зафиксированы тревоги с момента последнего включения
    if ((data['alarms'] > 0) || (data['annunciator']['summary'] > 0)) {
      event.textColor = '#FF0000';
      event.blink = true;
    } else {
      event.textColor = '#FFFF00';
      event.blink = false;
    };
  };
} else {
  // Нет данных
  event.text = 'Устройство выключено или не доступно';
  event.textColor = '#FF0000';
  event.blink = true;
};

Есть еще небольшая “хитрость” – я использую ещё один простой JavaScript-ик для более “красивого” отображения состояния всех датчиков на отдельной вкладке:

 


Как заменить пассивный зуммер на активный

Изначально схема была рассчитана на пассивный зуммер, то есть такой, который сам по себе при подаче напряжения могут только щелкать. Чтобы заставить такой зуммер звучать – необходимо подавать на него импульсы с заданной частотой и скважностью. Этим занимается библиотечка reBeep посредством LEDC API ESP32 (по сути это просто PWM).

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

Во первых – нужно закомментировать макрос CONFIG_GPIO_BUZZER 13 и заменить его на #define CONFIG_GPIO_BUZZER_ACTIVE 13.

Во вторых, дабы пищалка могла звучать не непрерывно, а “импульсами”, добавим для её управления тот же объект, что и для управления светодиодами. Я это сделал в той же функции настройки устройств ОПС, но можно и main.cpp это сделать, например.

// Замена пассивной пищалки на активную
ledQueue_t buzzer = nullptr;
#if defined(CONFIG_GPIO_BUZZER_ACTIVE) && (CONFIG_GPIO_BUZZER_ACTIVE > -1)
  buzzer = ledTaskCreate(CONFIG_GPIO_BUZZER_ACTIVE, true, false, "buzzer", CONFIG_LED_TASK_STACK_SIZE, nullptr);
  ledTaskSend(buzzer, lmOff, 0, 0, 0);
#endif // CONFIG_GPIO_ALARM_FLASH

А затем не забыть передать указатель на входящую очередь светодиода в функцию запуска задачи:

Вот в общем-то и всё, этого достаточно.

На этом я завершаю цикл “Термостат+ОПС”, однако если я чего-то забыл – материал, возможно, будет дополнен.

 


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

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


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

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

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