Практика: Сбор событий от кнопки и датчика движения
Введение: Комплексная автоматизация на базе дискретных сигналов
В предыдущих уроках мы рассмотрели, как индивидуально работать с различными типами дискретных датчиков, таких как герконы, датчики движения и кнопки. Теперь мы переходим на следующий уровень — создание комплексных сценариев, в которых несколько источников событий работают совместно для реализации более сложной и интуитивно понятной логики автоматизации. Именно такие сценарии превращают набор разрозненных устройств в единую интеллектуальную систему.
> 🔗 Связанный материал: Физическое подключение датчиков к дискретным входам контроллера HI, а также основы работы с ними, подробно рассмотрены в уроках `COURSE-04-M02-L02` и `COURSE-04-M02-L05`. Мы не будем повторять эту информацию и сосредоточимся на логике программной обработки сигналов.
Сегодняшняя задача — создать классический, но очень востребованный сценарий управления освещением в проходной зоне (например, коридоре или прихожей), комбинируя события от настенной кнопки и пассивного инфракрасного датчика движения (PIR).
Архитектура нашего решения будет выглядеть следующим образом:Для реализации этого сценария мы будем использовать следующие ключевые узлы Node-RED:
- `hi-input`: Специализированный узел для платформы HI, позволяющий считывать состояние универсальных входов контроллера.
- `function`: Основной инструмент для написания кастомной логики на JavaScript. Мы будем использовать его для распознавания типов нажатий и для реализации конечного автомата (FSM) нашего сценария.
- `trigger`: Очень полезный узел для создания логики с задержкой, идеален для автоматического отключения света по тайм-ауту.
- `debug`: Незаменимый инструмент для отладки потоков, позволяющий в реальном времени видеть сообщения, проходящие через любую точку нашей схемы.
Наша цель — не просто заставить систему работать, а создать надежный, предсказуемый и легко модифицируемый сценарий, следуя лучшим практикам, принятым в нашей Академии.
---
Секция 1: Подготовка потока и чтение состояний входов
Первым шагом в любом проекте автоматизации является обеспечение надежного получения первичных данных от оборудования. Нам нужно убедиться, что контроллер корректно считывает сигналы с кнопки и датчика движения, прежде чем мы перейдем к построению сложной логики.
Создадим новый поток (flow) в редакторе Node-RED. Назовем его, например, "Corridor Lighting Automation".
### Чтение сигналов с помощью узла `hi-input`
* Name: `Кнопка в коридоре`
* Input Channel: Выберите номер универсального входа, к которому физически подключен выключатель (например, `UI-1`).
* Signal Type: `Digital Input`
* Name: `PIR-датчик в коридоре`
* Input Channel: Выберите соответствующий канал (например, `UI-2`).
* Signal Type: `Digital Input`
После настройки эти узлы будут автоматически генерировать сообщения каждый раз, когда состояние на их входах меняется.
### Верификация сигналов с помощью узла `debug`
Теперь нам нужно проверить, что мы получаем от датчиков. Для этого используем узел `debug`.
> 💡 Подсказка: Именуйте узлы Debug осмысленно (например, 'PIR Signal' или 'Button State'). Это значительно упрощает отладку сложных потоков с множеством параллельных ветвей логики.
Настроим узлы `debug` для более информативного вывода:
- Дважды щелкните по первому `debug` (от кнопки). В поле `Name` введите `Button State`. В поле `Output` выберите `complete msg object`. Это позволит нам видеть не только `msg.payload`, но и другие свойства сообщения, например, `msg.topic`.
- Аналогично настройте второй `debug` (от PIR-датчика), назвав его `PIR Signal`.
Разверните поток, нажав кнопку "Deploy". Теперь откройте боковую панель отладки (с иконкой жука).
Подойдите к датчику движения. Вы должны увидеть в окне отладки сообщение от узла `PIR Signal`:
{
"topic": "hi/input/ui-2",
"payload": true,
"_msgid": "..."
}
Когда датчик перестанет фиксировать движение (по истечении его внутреннего таймера), он пришлет еще одно сообщение:
{
"topic": "hi/input/ui-2",
"payload": false,
"_msgid": "..."
}
Теперь нажмите и отпустите настенную кнопку. Вы увидите два последовательных сообщения от узла `Button State`: первое с `payload: true` (нажатие) и второе с `payload: false` (отпускание).
На этом этапе мы убедились, что физическое подключение выполнено корректно и контроллер стабильно получает первичные данные. Это критически важный этап, пропуск которого часто приводит к часам мучительной отладки программной логики, в то время как проблема кроется в простом обрыве провода.
---
Секция 2: Обработка событий кнопки: генерация команд Toggle и Force
Простое включение и выключение света по нажатию кнопки — это базовый функционал. Мы же хотим большего: различать короткие и длинные нажатия для вызова разных функций. Например, короткое нажатие будет переключать свет (Toggle), а длинное — принудительно включать его на неограниченное время (Force ON).
Для реализации этого нам понадобится узел `function`. Он позволит нам написать небольшой скрипт на JavaScript, который будет анализировать временные интервалы между событиями "нажатие" и "отпускание".
### Алгоритм распознавания нажатий
Логика следующая:
### Реализация в узле `function`
// Определяем таймаут для длинного нажатия в миллисекундах
const LONG_PRESS_TIMEOUT = 700;
// Получаем ID таймера из контекста потока.
// Контекст позволяет сохранять переменные между вызовами функции.
let timer = flow.get('buttonTimer') || null;
// Если пришло сообщение о НАЖАТИИ (payload: true)
if (msg.payload === true) {
// Запускаем новый таймер. Он сработает, если кнопку не отпустят.
timer = setTimeout(() => {
// Таймер сработал - это длинное нажатие.
// Очищаем контекст, чтобы не было "фантомных" таймеров.
flow.set('buttonTimer', null);
// Формируем стандартизированное сообщение о событии.
// Использование msg.topic крайне важно для дальнейшей маршрутизации.
node.send({
topic: 'button_event',
payload: 'long_press'
});
// Отображаем статус на узле для удобства отладки
node.status({ fill: "blue", shape: "dot", text: "Long Press" });
}, LONG_PRESS_TIMEOUT);
// Сохраняем ID таймера в контекст, чтобы иметь к нему доступ при отпускании
flow.set('buttonTimer', timer);
// На этом обработка нажатия завершена, ничего не отправляем дальше
return null;
}
// Если пришло сообщение о ОТПУСКАНИИ (payload: false)
if (msg.payload === false) {
// Проверяем, был ли запущен таймер
if (timer) {
// Если таймер существует, значит, отпускание произошло раньше,
// чем сработал таймаут длинного нажатия. Это КОРОТКОЕ нажатие.
// Немедленно отменяем таймер, чтобы он не сработал.
clearTimeout(timer);
flow.set('buttonTimer', null); // Очищаем контекст
// Формируем сообщение о коротком нажатии.
node.send({
topic: 'button_event',
payload: 'short_press'
});
node.status({ fill: "green", shape: "dot", text: "Short Press" });
}
// Если таймера не было (например, пришло ложное 'false'), ничего не делаем
return null;
}
Подключите к выходу этого узла `function` новый `debug` узел ("Button Events"). Разверните поток.
Теперь, если вы коротко нажмете кнопку, в окне отладки появится:
{
"topic": "button_event",
"payload": "short_press",
"_msgid": "..."
}
А если зажмете кнопку на секунду, вы увидите:
{
"topic": "button_event",
"payload": "long_press",
"_msgid": "..."
}
Мы успешно преобразовали простые сигналы `true/false` в осмысленные события. Использование контекста потока (`flow context`) для хранения ID таймера — это ключевой паттерн для работы с событиями, разделенными во времени. Установка `msg.topic` позволит нам в дальнейшем легко отличить события от кнопки от событий PIR-датчика.
---
Секция 3: Интеграция датчика движения и управление задержкой
Теперь займемся автоматической частью нашего сценария. Логика проста: если есть движение, свет должен включиться; если движение прекратилось, свет должен выключиться, но не сразу, а спустя некоторое время (например, 5 минут). Это классическая задача для узла `trigger`.
> ⚠️ Внимание: Учитывайте "слепое время" (re-trigger time) PIR-датчиков. Если оно составляет 30 секунд, датчик не будет отправлять новые сигналы о движении в течение этого времени, даже если движение продолжается. Это означает, что таймер в Node-RED не будет перезапущен в этот период. Выбирайте время задержки на отключение значительно большим, чем re-trigger time датчика.
### Настройка узла `trigger`
* Send: `boolean` `true` (это сообщение будет отправлено немедленно при получении любого входящего сообщения).
* then wait for: `5` `minutes`.
* then send: `boolean` `false`.
* Extend delay if new message arrives: ОБЯЗАТЕЛЬНО поставьте галочку. Эта опция реализует ключевую функцию: каждый новый сигнал о движении (`payload: true` от PIR) будет сбрасывать 5-минутный таймер и запускать его заново. Свет не выключится, пока в помещении есть люди.
* Handle: `message topic` и выберите `ignore`. Мы хотим, чтобы логика работала только на `payload`.
Как это работает:- Когда PIR-датчик обнаруживает движение, он отправляет `msg` с `payload: true` на вход `trigger`.
- Узел `trigger` немедленно отправляет на свой выход сообщение с `payload: true` (команда "Включить свет").
- Одновременно он запускает таймер на 5 минут.
- Если в течение этих 5 минут от PIR-датчика приходит еще одно сообщение о движении, таймер сбрасывается и запускается заново.
- Если в течение 5 минут новых сигналов о движении не поступало, таймер срабатывает, и `trigger` отправляет на выход сообщение с `payload: false` (команда "Выключить свет").
### Формирование управляющих сообщений
Узел `trigger` на выходе дает нам простые `true` и `false`. Для унификации потока данных обернем их в стандартный формат, аналогичный тому, что мы сделали для кнопки.
* Set: `msg.topic`
* to: `pir_event`
Подключите к выходу узла `change` еще один `debug` узел ("PIR Command"). После развертывания потока, пройдя мимо датчика, вы сначала увидите сообщение с `topic: "pir_event"` и `payload: true`, а через 5 минут (если не двигаться) — такое же, но с `payload: false`.
Мы создали два независимых "поставщика" команд: один реагирует на действия пользователя с кнопкой, второй — на автоматические события от датчика движения. На следующем шаге мы объединим их в единую логику.
---
Секция 4: Пример: Объединение логики и управление состоянием
Настало время самого интересного — объединить обработчики кнопки и датчика движения в единый, умный сценарий. Просто отправлять их команды на реле напрямую нельзя, так как они будут конфликтовать. Например, вы можете войти в комнату, свет включится автоматически, а затем вы нажмете кнопку выключения, но через секунду датчик снова вас увидит и опять включит свет.
Чтобы избежать такого хаоса, нам нужна система управления состоянием (state management) или, проще говоря, конечный автомат.
### Финальный сценарий и состояния
Определим три состояния нашей системы:
Мы будем хранить текущее состояние в контексте потока (`flow context`).
| Режим | Активация | Поведение | Выход из режима |
| :----------- | :---------------------------------------- | :--------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------ |
| AUTO | По умолчанию, событие от PIR-датчика | PIR-датчик управляет светом. Короткое нажатие кнопки переводит в `MANUAL_OFF` (если горел) или `MANUAL_ON` (если не горел). | Длинное нажатие кнопки (`MANUAL_ON`) или короткое нажатие. |
| MANUAL_ON | Длинное нажатие кнопки | Свет постоянно включен. PIR-датчик игнорируется. | Короткое нажатие кнопки (переход в `MANUAL_OFF` и выключение света). |
| MANUAL_OFF | Короткое нажатие для выключения света | Свет постоянно выключен. PIR-датчик временно игнорируется. | Следующее событие `true` от PIR-датчика (возврат в `AUTO`). Короткое нажатие (в `MANUAL_ON`).|
### Финальная структура потока
Объединим наши потоки. Выходы от узла "Распознавание нажатий" (события кнопки) и узла "PIR Trigger -> Set Topic" (события датчика) должны приходить на вход одного узла `function`, который мы назовем "Главный контроллер света".
[hi-input: Кнопка]--->[function: Распознавание нажатий]--+
|
[hi-input: PIR]------>[trigger: Задержка 5 мин]--------->[change: set topic]--+
|
v
[function: Главный контроллер света]
|
v
[hi-output: Реле света]
### Код главного контроллера
Скопируйте этот код в узел `function` "Главный контроллер света".
// Инициализируем состояние системы, если оно еще не задано.
// Режим по умолчанию - AUTO.
let mode = flow.get('light_mode') || 'AUTO';
let lightState = flow.get('light_state') || false; // Текущее состояние света (true=ON, false=OFF)
// --- Логика обработки событий в зависимости от msg.topic ---
if (msg.topic === 'button_event') {
// Пришло событие от КНОПКИ
if (msg.payload === 'short_press') {
// Короткое нажатие всегда переключает свет и меняет режим
if (lightState === true) {
// Если свет горел, выключаем его и переходим в MANUAL_OFF
mode = 'MANUAL_OFF';
lightState = false;
} else {
// Если свет не горел, включаем его и переходим в MANUAL_ON
mode = 'MANUAL_ON';
lightState = true;
}
} else if (msg.payload === 'long_press') {
// Длинное нажатие всегда принудительно включает свет
mode = 'MANUAL_ON';
lightState = true;
}
} else if (msg.topic === 'pir_event') {
// Пришло событие от ДАТЧИКА ДВИЖЕНИЯ
if (msg.payload === true) {
// Движение обнаружено.
// Переводим систему в режим AUTO, неважно, в каком она была.
mode = 'AUTO';
// И если режим AUTO, то включаем свет.
if (mode === 'AUTO') {
lightState = true;
}
} else {
// Пришла команда на выключение по таймеру от датчика.
// Выполняем ее, только если мы в режиме AUTO.
if (mode === 'AUTO') {
lightState = false;
}
// Если мы в MANUAL_ON или MANUAL_OFF, мы игнорируем команду выключения от датчика.
}
}
// --- Сохранение состояния и формирование выходной команды ---
// Сохраняем обновленные состояния в контекст
flow.set('light_mode', mode);
flow.set('light_state', lightState);
// Обновляем статус узла для удобной отладки
node.status({ fill: "blue", shape: "dot", text: `Mode: ${mode} | Light: ${lightState ? 'ON' : 'OFF'}` });
// Формируем финальное сообщение для реле
// Узел hi-output ожидает boolean в payload
msg.payload = lightState;
return msg;
Подключите к выходу этого узла узел `hi-output`, настроенный на управление нужным реле. Теперь у вас есть полнофункциональная система управления светом, которая:
- Включает свет по движению и выключает по таймеру.
- Позволяет принудительно выключить свет коротким нажатием, и он не включится снова, пока вы не уйдете и не вернетесь (новое событие от PIR вернет режим `AUTO`).
- Позволяет принудительно включить свет длинным нажатием, и он будет гореть, пока вы не выключите его коротким нажатием.
---
Итоги и лучшие практики
В этом уроке мы прошли путь от чтения простых дискретных сигналов до создания сложного, но логичного и удобного для пользователя сценария автоматизации.
Давайте кратко резюмируем, что мы сделали:Этот модульный подход — разделение логики на независимые блоки (обработка кнопки, обработка PIR, главный контроллер) — является ключевым для создания поддерживаемых и масштабируемых систем. Если завтра вам понадобится изменить время задержки, вы измените всего один узел `trigger`, не трогая остальную логику.
Важнейший вывод этого урока: для создания нетривиальных сценариев, где ручное и автоматическое управление пересекаются, абсолютно необходимо внедрять механизм управления состоянием. Без него ваши потоки быстро превратятся в хаотичный набор конфликтующих правил.
Что дальше?
Созданный нами сценарий уже достаточно функционален, но его можно улучшить:
- Добавить зависимость от времени суток: Свет по движению может включаться только в темное время суток. Это можно реализовать, добавив проверку времени внутри главного контроллера.
- Интеграция с датчиком освещенности: Вместо времени суток использовать реальные показания аналогового датчика освещенности.
- Уведомления: При переходе в режим `MANUAL_ON` можно отправлять уведомление администратору в Telegram, чтобы отслеживать случаи, когда свет забыли выключить.
В следующем уроке мы перейдем к работе с аналоговыми входами и рассмотрим, как обрабатывать данные с датчиков температуры, влажности и освещенности.