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

Лабораторная работа: Создание flow управления светом по датчику освещенности с гистерезисом

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

Цели и подготовка к лабораторной работе

Добро пожаловать в практическую лабораторную работу, где мы объединим теоретические знания, полученные в предыдущих уроках, для создания полноценного и надежного сценария автоматизации. Основная проблема, которую мы решаем сегодня, — нестабильное поведение системы управления светом на основе датчика освещенности, известное как флаттеринг (flapping). Это происходит, когда уровень освещенности колеблется вблизи одного порогового значения, заставляя реле многократно и бессмысленно включаться и выключаться.

Цель настоящей лабораторной работы: создать отказоустойчивый flow в Node-RED для автоматического управления группой освещения. Этот flow должен игнорировать кратковременные колебания и "шум" от датчика освещенности, принимая решения о включении и выключении света на основе устойчивых, усредненных данных и с использованием логики гистерезиса.

Архитектура решения

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

  • Получение данных: Узел `mqtt in` подписывается на топик датчика освещенности и получает сырые значения в люксах.
  • Сглаживание сигнала: Узел `smooth` принимает сырые значения и вычисляет по ним простое скользящее среднее (SMA). Это позволяет избавиться от резких, но кратковременных всплесков или провалов в данных (например, от промелькнувшей тени).
  • Принятие решения: Узел `function` содержит основную бизнес-логику. Он принимает сглаженное значение, сравнивает его с двумя порогами (гистерезис) и, учитывая текущее состояние света (включен или выключен), решает, нужно ли его изменить.
  • Маршрутизация команды: Узел `switch` направляет поток в зависимости от решения, принятого в узле `function` (включить или выключить).
  • Формирование команды: Узлы `change` преобразуют логическое решение (`true`/`false`) в конкретную команду, понятную исполнительному устройству (например, в строку `"1"` или `"0"`).
  • Отправка команды: Узел `mqtt out` публикует сформированную команду в MQTT-топик реле, управляющего освещением.
  • > 💡 Подсказка: Перед началом работы убедитесь, что ваш MQTT-брокер активен, и вы можете получать данные с датчика и управлять реле. Используйте сторонний MQTT-клиент (например, MQTT Explorer) для проверки топиков. Это базовый навык диагностики, который сэкономит вам массу времени.

    Подготовка рабочего пространства

  • Откройте редактор Node-RED на вашем контроллере.
  • Создайте новую вкладку (flow), нажав на `+` в верхней панели. Дайте ей осмысленное имя, например, "Auto Light - Office".
  • Определите и запишите MQTT-топики, которые вы будете использовать:
  • * Топик датчика освещенности (вход): Например, `/devices/wb-msw-v3_25/controls/Illuminance`.

    * Топик управления реле (выход): Например, `/devices/wb-mr6c_12/controls/K1/on`.

  • Убедитесь, что в вашей палитре Node-RED установлены все необходимые узлы:
  • * `mqtt in`, `mqtt out`

    * `smooth`

    * `function`

    * `switch`

    * `change`

    * `debug`

    Теперь, когда цели ясны и рабочее место готово, приступим к созданию нашего flow шаг за шагом.

    ---

    Шаг 1: Получение и сглаживание аналогового сигнала

    Первый и самый важный этап — получение стабильных данных. Как мы обсуждали в уроке `COURSE-04-M05-L06`, сырые данные с аналоговых датчиков часто бывают "зашумленными". Наша задача — отфильтровать этот шум, прежде чем передавать данные в узел принятия решений.

    Добавление и настройка `mqtt in`

  • Перетащите узел `mqtt in` на холст.
  • Дважды щелкните по нему, чтобы открыть окно настроек.
  • Server: Выберите ваш предварительно настроенный MQTT-брокер.
  • Topic: Укажите топик, из которого датчик освещенности публикует свои показания. Например, `/devices/wb-msw-v3_25/controls/Illuminance`.
  • QoS: Установите `0` или `1`. Для некритичных данных телеметрии QoS 0 является достаточным.
  • Output: Выберите `a parsed JSON object`, если датчик отправляет данные в формате JSON, или `auto-detect`, что в большинстве случаев работает корректно для простых числовых значений.
  • Name: Дайте узлу понятное имя, например, "Датчик освещенности (Lux)".
  • После настройки подключите к выходу узла `mqtt in` узел `debug`, чтобы убедиться, что данные поступают корректно. Вы должны видеть в панели отладки сообщения, `msg.payload` которых содержит числовое значение освещенности.

    Интеграция узла `smooth`

    Теперь добавим сглаживание. Узел `smooth` реализует несколько алгоритмов усреднения, но для нашей задачи идеально подходит простое скользящее среднее (SMA).

  • Перетащите узел `smooth` на холст и разместите его между `mqtt in` и `debug`.
  • Откройте его настройки.
  • | Параметр | Описание | Рекомендуемое значение |

    | :---------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------- |

    | Action | Алгоритм сглаживания. Выберите `simple moving average`. | `simple moving average` |

    | Count | Ключевой параметр. Это количество последних значений, которые будут использоваться для расчета среднего. Чем выше это число, тем более гладким будет результат, но и более инертной станет реакция системы. | `10` |

    | Round to| Округление результата до указанного количества знаков после запятой. Помогает избежать излишней точности. | `0` (целое число) |

    | Name | Имя узла. | "SMA (10)" |

    Анализ эффекта сглаживания

    Чтобы наглядно увидеть результат работы узла `smooth`, используйте два узла `debug`:

  • Подключите первый узел `debug` (назовите его "Сырые данные") напрямую к выходу `mqtt in`.
  • Подключите второй узел `debug` (назовите его "Сглаженные данные") к выходу `smooth`.
  • Теперь симулируйте изменение освещенности (например, медленно поднося и убирая фонарик телефона к датчику). В панели отладки вы увидите две последовательности чисел.

    Пример данных в панели отладки:
    // Сообщение от "Сырые данные"
    

    msg.payload: 152

    // Сообщение от "Сглаженные данные"

    msg.payload: 125

    // Сообщение от "Сырые данные"

    msg.payload: 98

    // Сообщение от "Сглаженные данные"

    msg.payload: 120

    // Сообщение от "Сырые данные"

    msg.payload: 155

    // Сообщение от "Сглаженные данные"

    msg.payload: 126

    Выбор значения `count` — это компромисс. Для управления светом значение от 5 до 15 обычно является оптимальным. Оно достаточно велико, чтобы отсечь шум (например, от промелькнувшей тени человека), но достаточно мало, чтобы система не реагировала на реальное изменение освещенности (например, на закат) с большой задержкой.

    ---

    Шаг 2: Реализация логики гистерезиса в ноде Function

    Теперь, когда у нас есть стабильный, сглаженный поток данных, мы можем реализовать основную логику управления с помощью узла `function`. Этот узел позволит нам написать небольшой JavaScript-код, реализующий алгоритм "двойного порога", который мы детально рассмотрели в уроке `LESSON-04-M05-L03`.

    > ⚠️ Внимание: Всегда инициализируйте переменные контекста в начале! Если при первом запуске `flow.get('isLightOn')` вернет `undefined`, это может привести к непредсказуемому поведению или ошибке. Добавьте проверку `if (current_state === undefined) { ... }` для установки начального значения.

    Перетащите узел `function` на холст и соедините его с выходом узла `smooth`. Вставьте в него следующий код:

    /*
    
    • ЛОГИКА УПРАВЛЕНИЯ СВЕТОМ С ГИСТЕРЕЗИСОМ
    • Этот узел принимает сглаженное значение освещенности (lux) и решает,
    • нужно ли изменить состояние света.
    */

    // --- 1. Определение порогов ---

    // Порог для ВКЛЮЧЕНИЯ света. Свет включится, когда станет ТЕМНЕЕ этого значения.

    const thresholdOn = 100; // lux

    // Порог для ВЫКЛЮЧЕНИЯ света. Свет выключится, когда станет СВЕТЛЕЕ этого значения.

    const thresholdOff = 150; // lux

    // --- 2. Получение текущего состояния ---

    // Считываем текущее состояние света из контекста уровня Flow.

    // Это позволяет "помнить" состояние между вызовами функции.

    let isLightOn = flow.get('isLightOn');

    // --- 3. Инициализация состояния (критически важный шаг!) ---

    // Если узел запускается впервые, переменная isLightOn будет `undefined`.

    // Устанавливаем ей начальное значение (например, `false` - свет выключен).

    if (isLightOn === undefined) {

    isLightOn = false;

    flow.set('isLightOn', isLightOn);

    node.status({fill:"yellow", shape:"dot", text:"Init: OFF"});

    // Возвращаем null, чтобы не отправлять команду при инициализации

    return null;

    }

    // --- 4. Получение входных данных ---

    // `msg.payload` содержит сглаженное значение освещенности от узла `smooth`.

    const currentLux = msg.payload;

    // --- 5. Основная логика гистерезиса ---

    // Переменная для хранения новой команды. null означает "ничего не делать".

    let command = null;

    // Проверяем, нужно ли ВКЛЮЧИТЬ свет

    // Условия: сейчас темно (lux < thresholdOn) И свет сейчас выключен (!isLightOn)

    if (currentLux < thresholdOn && !isLightOn) {

    command = true; // Команда на включение

    flow.set('isLightOn', true); // Запоминаем новое состояние

    node.status({fill:"blue", shape:"dot", text:"ON (lux: " + currentLux + ")"});

    }

    // Проверяем, нужно ли ВЫКЛЮЧИТЬ свет

    // Условия: сейчас светло (lux > thresholdOff) И свет сейчас включен (isLightOn)

    else if (currentLux > thresholdOff && isLightOn) {

    command = false; // Команда на выключение

    flow.set('isLightOn', false); // Запоминаем новое состояние

    node.status({fill:"green", shape:"dot", text:"OFF (lux: " + currentLux + ")"});

    }

    // Если ни одно из условий не выполнено, значит мы находимся в "мертвой зоне"

    // гистерезиса (между thresholdOn и thresholdOff). Ничего не меняем.

    else {

    // Обновляем статус для наглядности, но не отправляем команду

    let statusText = isLightOn ? "State: ON" : "State: OFF";

    statusText += " (lux: " + currentLux + ")";

    node.status({fill:"grey", shape:"dot", text:statusText});

    // Очень важно вернуть null, чтобы остановить дальнейшее движение сообщения

    // и не отправлять дублирующие команды.

    return null;

    }

    // --- 6. Формирование исходящего сообщения ---

    // Отправляем в следующий узел только если было принято решение об изменении состояния.

    msg.payload = command;

    return msg;

    Разбор кода:
  • Пороги: `thresholdOn` и `thresholdOff` определяют чувствительность системы. `thresholdOff` должен быть больше `thresholdOn`, чтобы создать "мертвую зону".
  • Контекст: `flow.get('isLightOn')` и `flow.set('isLightOn', ...)` — это механизм хранения состояния (state) между отдельными сообщениями. Без него функция не сможет "помнить", включен свет или выключен.
  • Инициализация: Блок `if (isLightOn === undefined)` выполняется только один раз при развертывании flow и задает начальное, безопасное состояние.
  • Логика `if/else if`: Это сердце гистерезиса. Обратите внимание, что каждое условие проверяет как уровень освещенности, так и текущее состояние света. Это предотвращает отправку команды "Включить" каждую секунду, если свет уже включен.
  • Возврат `null`: Это ключевой механизм оптимизации. Если состояние света менять не нужно, функция возвращает `null`, и сообщение не идет дальше по цепочке. Это предотвращает отправку лишних команд в MQTT.
  • Статус узла: `node.status()` очень полезен для визуальной отладки прямо в редакторе, без необходимости постоянно смотреть в панель `debug`.
  • ---

    Шаг 3: Формирование и отправка управляющих команд

    Наш узел `function` теперь выдает на выход `msg` с `payload`, равным `true` (включить), `false` (выключить), или не выдает ничего (`null`). Следующий шаг — преобразовать эти логические значения в команды, которые понимает наше исполнительное устройство (реле), и отправить их.

    Такой подход, разделяющий логику, маршрутизацию и формирование данных, является примером хорошей практики проектирования в Node-RED. Он делает flow модульным и легко читаемым.

    Маршрутизация с помощью `switch`

  • Добавьте на холст узел `switch` и подключите его к выходу узла `function`.
  • Откройте настройки узла `switch`.
  • Property: Оставьте `msg.payload`.
  • Настройте два правила маршрутизации:
  • * Правило 1: `is true` (тип `boolean`). Это будет выход 1.

    * Правило 2: `is false` (тип `boolean`). Это будет выход 2.

  • Name: "Команда ON/OFF?".
  • Теперь узел `switch` будет направлять сообщения с `msg.payload: true` на свой первый выход, а сообщения с `msg.payload: false` — на второй.

    Формирование команд с помощью `change`

    Многие MQTT-устройства ожидают в качестве команды не булевы значения `true`/`false`, а строки `"1"`/`"0"` или `"ON"`/`"OFF"`. Узлы `change` идеально подходят для этого простого преобразования.

  • Для команды "Включить":
  • * Добавьте узел `change` и подключите его к первому выходу узла `switch`.

    * В настройках узла `change`, установите `msg.payload` в значение (string) `1`.

    * Назовите узел "Set Payload: 1".

  • Для команды "Выключить":
  • * Добавьте второй узел `change` и подключите его к второму выходу узла `switch`.

    * В настройках установите `msg.payload` в значение (string) `0`.

    * Назовите узел "Set Payload: 0".

    Теперь, когда `function` вернет `true`, сообщение пойдет по верхней ветке, и `msg.payload` станет равным `"1"`. Если `function` вернет `false`, `msg.payload` станет равным `"0"`.

    Отправка команды в MQTT

    Последний шаг — отправить сформированную команду на реле.

  • Добавьте узел `mqtt out` на холст.
  • Подключите к его входу оба выхода от узлов `change`.
  • Откройте настройки узла `mqtt out`:
  • * Server: Выберите тот же MQTT-брокер.

    * Topic: Укажите топик управления вашим реле. Например, `/devices/wb-mr6c_12/controls/K1/on`.

    * QoS: `1` или `2` для гарантированной доставки управляющих команд.

    * Retain: Оставьте `false`. Как правило, не нужно сохранять команду на брокере.

    * Name: "Реле света (K1)".

    Итоговый flow на этом шаге должен выглядеть так: выход из `function` идет на вход `switch`, а два выхода из `switch` идут в два соответствующих узла `change`, выходы которых объединяются на входе `mqtt out`.

    ---

    Итоги, отладка и проверка результата

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

    Итоговая схема flow

    Ваш итоговый flow реализует следующую логическую цепочку:

    [mqtt in] -> [smooth] -> [function] -> [switch] --+--> [change: "1"] --+--> [mqtt out]
    

    (Датчик) (SMA) (Логика) (ON/OFF) | |

    +--> [change: "0"] --+

    Ключевые методы отладки

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

  • Проверка каждого этапа: Разместите узлы `debug` после каждого узла в цепочке (`mqtt in`, `smooth`, `function`, `change`). Это позволит вам точно увидеть, на каком этапе данные искажаются или поток прерывается.
  • Анализ контекста: В правой боковой панели Node-RED откройте вкладку `Context Data`. Выберите `Flow` и найдите вашу переменную `isLightOn`. Вы сможете в реальном времени видеть, как "думает" ваш алгоритм и какое состояние он хранит.
  • Мониторинг MQTT: Используйте внешний MQTT-клиент (MQTT Explorer) для двух вещей:
  • * Убедитесь, что данные с датчика приходят в ожидаемом формате.

    * Посмотрите, какие именно команды (`"1"`, `"0"`, `true`, `false`?) ваш flow отправляет в топик реле. Возможно, реле ожидает другой формат.

    Практическое тестирование

    Теперь самый интересный этап — проверка в реальных условиях.

    Возможные улучшения

    Данный flow является отличной базой для дальнейших усложнений:

    > 🔗 Связанный материал: Для более сложных сценариев, зависящих от времени, изучите наш модуль по работе с таймерами и расписаниями: `COURSE-06-M02: Сценарии автоматизации на основе времени и событий`.

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