ГлавнаяАкадемияДатчики и входы: нормализация сигналов → Практика: Сбор событий от кнопки и датчика движения

Практика: Сбор событий от кнопки и датчика движения

Урок 6 · Датчики и входы: нормализация сигналов · 30 мин · theory

Введение: Комплексная автоматизация на базе дискретных сигналов

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

> 🔗 Связанный материал: Физическое подключение датчиков к дискретным входам контроллера HI, а также основы работы с ними, подробно рассмотрены в уроках `COURSE-04-M02-L02` и `COURSE-04-M02-L05`. Мы не будем повторять эту информацию и сосредоточимся на логике программной обработки сигналов.

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

Архитектура нашего решения будет выглядеть следующим образом:
  • Уровень входов: Два дискретных входа (DI) контроллера HI подключены к выходам типа "сухой контакт" от настенного выключателя и PIR-датчика. Они генерируют простые логические сигналы `true` (контакт замкнут) и `false` (контакт разомкнут).
  • Уровень логики: Среда Node-RED на контроллере будет принимать эти сигналы. С помощью набора узлов мы реализуем сложную логику: распознаем короткие и длинные нажатия кнопки, управляем таймером автоматического отключения света и менеджим общее состояние системы (например, автоматический или ручной режим).
  • Уровень выходов: На основе принятого логического решения, Node-RED будет отправлять команду на один из релейных выходов контроллера, физически включая или выключая группу освещения.
  • Для реализации этого сценария мы будем использовать следующие ключевые узлы Node-RED:

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

    ---

    Секция 1: Подготовка потока и чтение состояний входов

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

    Создадим новый поток (flow) в редакторе Node-RED. Назовем его, например, "Corridor Lighting Automation".

    ### Чтение сигналов с помощью узла `hi-input`

  • Перетащите на холст два узла `hi-input` из палитры HI. Один будет для кнопки, другой — для PIR-датчика.
  • Настройте первый узел для кнопки. Дважды щелкните по нему, чтобы открыть окно конфигурации:
  • * Name: `Кнопка в коридоре`

    * Input Channel: Выберите номер универсального входа, к которому физически подключен выключатель (например, `UI-1`).

    * Signal Type: `Digital Input`

  • Аналогично настройте второй узел для датчика движения:
  • * Name: `PIR-датчик в коридоре`

    * Input Channel: Выберите соответствующий канал (например, `UI-2`).

    * Signal Type: `Digital Input`

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

    ### Верификация сигналов с помощью узла `debug`

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

  • Добавьте два узла `debug` на холст.
  • Соедините выход узла "Кнопка в коридоре" с входом первого `debug`.
  • Соедините выход узла "PIR-датчик в коридоре" с входом второго `debug`.
  • > 💡 Подсказка: Именуйте узлы Debug осмысленно (например, 'PIR Signal' или 'Button State'). Это значительно упрощает отладку сложных потоков с множеством параллельных ветвей логики.

    Настроим узлы `debug` для более информативного вывода:

    Разверните поток, нажав кнопку "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, который будет анализировать временные интервалы между событиями "нажатие" и "отпускание".

    ### Алгоритм распознавания нажатий

    Логика следующая:

  • Когда от кнопки приходит сообщение `msg.payload: true` (кнопка нажата), мы не реагируем немедленно, а запускаем таймер, например, на 700 миллисекунд.
  • Сценарий 1 (короткое нажатие): Если в течение этих 700 мс приходит сообщение `msg.payload: false` (кнопку отпустили), мы останавливаем таймер и генерируем событие "короткое нажатие".
  • Сценарий 2 (длинное нажатие): Если сообщение `msg.payload: false` не приходит, таймер срабатывает по истечении 700 мс. В этот момент мы генерируем событие "длинное нажатие".
  • ### Реализация в узле `function`

  • Добавьте узел `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`

  • Перетащите на холст узел `trigger`.
  • Подключите к его входу выход узла "PIR-датчик в коридоре".
  • Дважды щелкните по узлу `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`.

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

    ### Формирование управляющих сообщений

    Узел `trigger` на выходе дает нам простые `true` и `false`. Для унификации потока данных обернем их в стандартный формат, аналогичный тому, что мы сделали для кнопки.

  • Добавьте узел `change` после `trigger`.
  • Настройте его так, чтобы он устанавливал `msg.topic` в значение `pir_event`. Это позволит нашему главному обработчику отличить команду от датчика движения от команды с кнопки.
  • * Set: `msg.topic`

    * to: `pir_event`

    Подключите к выходу узла `change` еще один `debug` узел ("PIR Command"). После развертывания потока, пройдя мимо датчика, вы сначала увидите сообщение с `topic: "pir_event"` и `payload: true`, а через 5 минут (если не двигаться) — такое же, но с `payload: false`.

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

    ---

    Секция 4: Пример: Объединение логики и управление состоянием

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

    Чтобы избежать такого хаоса, нам нужна система управления состоянием (state management) или, проще говоря, конечный автомат.

    ### Финальный сценарий и состояния

    Определим три состояния нашей системы:

  • `AUTO` (Автоматический режим): Состояние по умолчанию. Свет управляется датчиком движения (включается по движению, выключается по таймеру). Кнопка в этом режиме работает как переключатель (Toggle).
  • `MANUAL_ON` (Принудительно включено): Активируется длинным нажатием кнопки. Свет горит постоянно, датчик движения игнорируется.
  • `MANUAL_OFF` (Принудительно выключено): Активируется коротким нажатием кнопки для выключения света, когда он горел в режиме `AUTO`. В этом режиме датчик движения временно игнорируется, чтобы свет не включился сразу после выключения.
  • Мы будем хранить текущее состояние в контексте потока (`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-датчика с помощью узлов `hi-input`.
  • Реализовали событийную модель для кнопки, преобразовав физические нажатия в логические события `short_press` и `long_press` с помощью узла `function` и контекста потока.
  • Настроили узел `trigger` для реализации автоматического выключения света с перезапуском таймера при повторном движении.
  • Объединили все источники событий в одном узле-контроллере, который управляет состоянием системы (`AUTO`, `MANUAL_ON`, `MANUAL_OFF`) с помощью `flow context`.
  • Этот модульный подход — разделение логики на независимые блоки (обработка кнопки, обработка PIR, главный контроллер) — является ключевым для создания поддерживаемых и масштабируемых систем. Если завтра вам понадобится изменить время задержки, вы измените всего один узел `trigger`, не трогая остальную логику.

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

    Что дальше?

    Созданный нами сценарий уже достаточно функционален, но его можно улучшить:

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