ГлавнаяАкадемияИсполнительные устройства: интерлоки, таймауты → Реализация таймаута и повтора с помощью нод Trigger и Function

Реализация таймаута и повтора с помощью нод Trigger и Function

Урок 3 · Исполнительные устройства: интерлоки, таймауты · 30 мин · theory

Нода trigger: базовый механизм таймаута

В предыдущих уроках мы подробно рассмотрели шаблон «Команда → Подтверждение (Ack) → Таймаут» как фундаментальный принцип построения надежных систем управления. Теперь мы перейдем от теории к практике и научимся реализовывать этот шаблон с помощью стандартных средств Node-RED. Центральным элементом для реализации ожидания является нода trigger.

По своей сути, нода trigger — это усовершенствованный таймер. В отличие от простой ноды `delay`, которая лишь задерживает сообщение, `trigger` позволяет выстраивать более сложную, зависящую от времени логику.

Принцип работы ноды trigger

Основная логика ноды `trigger` следует простому, но мощному алгоритму, который идеально подходит для нашей задачи:

  • Прием сообщения: Как только на вход ноды поступает любое сообщение (`msg`), она немедленно активируется.
  • Отправка первого сообщения: Сразу после активации нода отправляет первое, или "начальное", сообщение. В нашем случае это будет команда исполнительному устройству (например, включить реле).
  • Запуск таймера и ожидание сброса: Одновременно с отправкой первого сообщения нода запускает внутренний таймер на заданный период (например, 2 секунды). В течение этого времени она находится в режиме ожидания специального сообщения о сбросе.
  • Два исхода:
  • * Успешный сброс (Ack получен): Если в течение таймаута на вход ноды поступает сообщение со свойством `msg.reset`, таймер останавливается, и больше ничего не происходит. Это штатный сценарий, означающий, что мы получили подтверждение (`Ack`) от устройства.

    * Таймаут истек: Если за отведенное время сообщение о сбросе так и не пришло, нода отправляет второе, "таймаут-сообщение". Это сигнал о том, что команда, вероятно, не была выполнена, так как подтверждение отсутствует.

    Эта механика позволяет нам точно реализовать компонент "Таймаут" из нашего основного шаблона.

    Конфигурация ноды

    Интерфейс настройки ноды `trigger` интуитивно понятен и отражает описанный выше алгоритм.

    > 💡 Подсказка: Для сброса `trigger` в сложных потоках удобно использовать сообщения, не несущие `payload`, а лишь специальный `topic`, например `mydevice/command/reset`, и `msg.reset=true`. Это упрощает логику и отладку.

    Роль msg.reset

    Ключевым свойством для управления нодой `trigger` является `msg.reset`. Когда нода находится в режиме ожидания, любое входящее сообщение, содержащее `msg.reset: true` (или просто `msg.reset`), немедленно деактивирует таймер.

    В нашем паттерне «Команда → Ack → Таймаут» поток, принимающий подтверждение от устройства (через MQTT, Modbus или другой механизм), должен будет сформировать такое сообщение и направить его на вход той же самой ноды `trigger`, которая инициировала команду. Таким образом, мы замыкаем цикл обратной связи: отправили команду, запустили таймер, получили `Ack`, преобразовали его в `msg.reset` и остановили таймер. Если `Ack` не пришел, таймер сработает, и мы узнаем о проблеме.

    ---

    Практика: Собираем поток «Команда → Ack → Таймаут»

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

    ASCII-схема потока:
    // Поток отправки команды и ожидания
    

    [Inject: Вкл. свет] --> [Change: Формат команды] --> [Trigger: Ждать Ack 3с] --+--> [MQTT Out: hi/cmd/light1]

    |

    +--> [Debug: ТАЙМАУТ!]

    // Поток приема подтверждения (Ack)

    [MQTT In: hi/ack/light1] --> [Change: Создать msg.reset] --> [Trigger: Ждать Ack 3с]

    Обратите внимание, что оба потока сходятся на одной и той же ноде `trigger`.

    Пошаговая сборка потока

  • Создаем поток отправки команды:
  • * Добавьте ноду `Inject`. Она будет имитировать нажатие кнопки в интерфейсе. Настройте ее на отправку `timestamp`.

    * Добавьте ноду `Change` для формирования контракта сообщения команды. Это критически важный шаг для стандартизации.

    * Правило 1: Установить `msg.topic` в `hi/cmd/light1`.

    * Правило 2: Установить `msg.payload` в JSON-объект. Используйте редактор JSON и введите:

                {

    "command": "SET_STATE",

    "value": true,

    "source": "manual-control-panel",

    "ts": 1678886400000

    }

    (Вместо `1678886400000` можно использовать JSONata выражение `$millis()` для подстановки текущего времени).

  • Настраиваем ноду `trigger`:
  • * Соедините выход ноды `Change` со входом ноды `trigger`.

    * Откройте настройки `trigger`:

    * Send: `the original message`.

    * then wait for: `3` seconds.

    * then send: Выберите тип `string` и введите `timeout_error`.

    * Установите галочку `Extend delay if new message arrives`.

    * Дайте ноде осмысленное имя, например, "Ожидание Ack: Свет Кухня".

  • Отправляем команду и обрабатываем таймаут:
  • * Первый выход ноды `trigger` подключите к ноде `MQTT Out`. Настройте ее на использование вашего MQTT-брокера. Топик можно оставить пустым, так как он уже задан в `msg.topic`.

    * Второй выход ноды `trigger` (который сработает по таймауту) подключите к ноде `Debug`. Назовите ее "ТАЙМАУТ!".

  • Создаем поток приема подтверждения:
  • * Добавьте ноду `MQTT In` и подпишите ее на топик подтверждения: `hi/ack/light1`.

    * Предположим, что в ответ на нашу команду устройство присылает сообщение со следующим `payload`:

            {

    "status": "OK",

    "currentState": true,

    "source": "relay-module-kitchen"

    }

    * Наша задача — преобразовать это сообщение в сигнал сброса для `trigger`. Добавьте ноду `Change` после `MQTT In`.

    * Настройте в ней одно правило:

    * Установить `msg.reset` в значение `true` (тип `boolean`).

    * Теперь соедините выход этой ноды `Change` со входом той же самой ноды `trigger`, которую мы настраивали в шаге 2.

    Тестирование потока:

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

    ---

    Добавляем повторы (Retry) с помощью ноды function

    Собранный нами поток решает проблему обнаружения сбоя, но не его исправления. Если произошел таймаут, мы просто логируем ошибку. В реальных условиях, особенно при работе с беспроводными сетями (Zigbee, LoRaWAN), единичные сбои доставки — нормальное явление. Автоматический повтор отправки команды — логичный следующий шаг для повышения надежности.

    Нода `trigger` сама по себе не умеет делать повторы. Она может только один раз сообщить о таймауте. Чтобы реализовать логику повторов, нам понадобится хранить состояние — а именно, количество уже сделанных попыток. Для этого мы будем использовать контекст потока (flow context) и ноду `function`.

    > ⚠️ Внимание: Всегда устанавливайте максимальное количество повторов. Отсутствие такого ограничения может привести к бесконечным циклам отправки команд, создавая высокую нагрузку на сеть и оборудование (DDoS-атака на самого себя).

    Использование flow context для счетчика попыток

    Контекст в Node-RED — это специальное хранилище данных, доступное внутри потока (`flow`) или глобально (`global`). Мы будем использовать `flow.context` для создания переменной-счетчика, например, `flow.retryCount`.

    Логика работы будет следующей:
  • Инициализация: Перед первой отправкой команды мы устанавливаем `flow.set('retryCount', 0)`.
  • Таймаут: Когда нода `trigger` сообщает о таймауте, сообщение попадает в нашу ноду `function`.
  • Логика в `function`:
  • * Получаем текущее значение счетчика: `let count = flow.get('retryCount') || 0;`.

    * Проверяем, не превышен ли лимит (например, 3 попытки): `if (count < 3)`.

    * Если лимит не превышен: Увеличиваем счетчик `count++`, сохраняем его обратно в контекст `flow.set('retryCount', count)` и отправляем исходное сообщение с командой обратно на вход ноды `trigger`, чтобы запустить процесс заново.

    * Если лимит превышен: Попытки исчерпаны. Мы генерируем финальное сообщение об ошибке (например, для отправки уведомления администратору) и сбрасываем счетчик в `0` для следующей команды.

  • Успешный Ack: Если подтверждение было получено, таймер сбрасывается. Но этого недостаточно! Нам также нужно сбросить счетчик `retryCount` в 0 в потоке обработки `Ack`. Иначе, после успешной команды, следующая команда с таймаутом начнется не с 0, а с предыдущего значения счетчика.
  • Код для ноды function

    Вот пример кода для ноды `function`, которая будет обрабатывать таймауты и управлять повторами. Предполагается, что эта нода подключена ко второму (таймаут) выходу ноды `trigger`.

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

    // Оно хранится в msg.payload, если trigger был настроен на передачу другого сообщения по таймауту.

    // Для надежности лучше сохранить его в отдельном свойстве.

    let original_msg = flow.get('last_command_msg');

    // Получаем счетчик повторов из контекста потока.

    // Если его нет, считаем, что это первая попытка (count = 0).

    let retryCount = flow.get('retryCount') || 0;

    const MAX_RETRIES = 3;

    // Увеличиваем счетчик

    retryCount++;

    node.warn(`Таймаут получения Ack. Попытка #${retryCount}`);

    if (retryCount <= MAX_RETRIES) {

    // Если лимит не превышен, сохраняем новое значение счетчика

    flow.set('retryCount', retryCount);

    // Обновляем статус ноды для визуальной диагностики

    node.status({fill:"yellow", shape:"dot", text:`Retry #${retryCount}`});

    // Возвращаем оригинальное сообщение обратно в поток для повторной отправки.

    // Этот выход должен быть соединен со входом ноды trigger.

    return original_msg;

    } else {

    // Лимит попыток исчерпан. Генерируем финальную ошибку.

    node.error(`КРИТИЧЕСКАЯ ОШИБКА: Команда не выполнена после ${MAX_RETRIES} попыток.`, original_msg);

    // Сбрасываем счетчик для будущих операций

    flow.set('retryCount', 0);

    // Формируем сообщение для системы алертинга

    let alert_msg = {

    topic: 'system/alerts/critical',

    payload: {

    error: 'Command delivery failed',

    details: `Device for command topic ${original_msg.topic} did not acknowledge.`,

    original_command: original_msg.payload,

    retries: MAX_RETRIES,

    ts: Date.now()

    }

    };

    // Обновляем статус ноды

    node.status({fill:"red", shape:"ring", text:`FAIL after ${MAX_RETRIES} retries`});

    // Отправляем сообщение на второй выход, предназначенный для алертов

    return [null, alert_msg];

    }

    Эта нода должна иметь два выхода: первый — для повторной отправки команды, второй — для критического алерта.

    ---

    Пример: отказоустойчивый поток с повторами и алертингом

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

    Обновленная ASCII-схема:
    // ========= Поток отправки команды ============
    

    [Inject] --> [Change: Set retryCount=0, store msg] --> [Trigger] --(cmd)--> [MQTT Out]

    ^ |

    // ========= Обработка таймаута и повторы ====== | +-- (timeout) --> [Function: Retry Logic] --(retry)---+

    | |

    +--------------------------------------------------+

    |

    // ========= Финальный сбой и алерт ============ +-- (fail) --> [MQTT Out: Alerts]

    // ========= Обработка Ack и сброс =============

    [MQTT In: Ack] --> [Change: msg.reset=true, reset retryCount=0] --> [Trigger]

    Детальная реализация

  • Инициализация (перед `trigger`):
  • Нам нужно не только сформировать команду, но и инициализировать счетчик, а также сохранить оригинальное сообщение для повторных отправок. Модифицируем самую первую ноду `Change` (или добавим новую).

    * Правило 1: `Установить flow.retryCount в 0`.

    * Правило 2: `Установить flow.last_command_msg в msg` (сохраняем все сообщение целиком).

    * Правило 3: `Установить msg.topic` в `hi/cmd/light1`.

    * Правило 4: `Установить msg.payload` в нужный JSON.

  • Нода `trigger`:
  • Конфигурация остается прежней. Она ждет 3 секунды.

    * Первый выход (команда) идет в `MQTT Out`.

    * Второй выход (таймаут) идет в нашу новую ноду `Function` с логикой повторов.

  • Нода `Function` (Retry Logic):
  • Создаем ноду `Function` с двумя выходами и вставляем код из предыдущего раздела.

  • Замыкаем цикл повтора:
  • * Первый выход ноды `Function` (который возвращает `original_msg` для повтора) мы соединяем обратно со входом ноды `trigger`. Это и есть цикл `Retry`.

    * Второй выход ноды `Function` (который формирует `alert_msg`) мы подключаем к новой ноде `MQTT Out`, которая публикует сообщения в топик `system/alerts/critical`.

  • Модифицируем обработку `Ack`:
  • Поток, принимающий подтверждение, теперь должен выполнять две задачи: сбрасывать таймер и обнулять счетчик повторов. Модифицируем ноду `Change` после `MQTT In` (`hi/ack/light1`).

    * Правило 1: `Установить msg.reset в true`.

    * Правило 2: `Установить flow.retryCount в 0`.

    Дополнительно можно добавить сюда `node.status`, чтобы визуально видеть, что `Ack` получен и все в порядке. Например, с помощью такой ноды `Function`:

        // Сбрасываем счетчик

    flow.set('retryCount', 0);

    // Устанавливаем статус "OK"

    node.status({ fill: "green", shape: "dot", text: "Ack OK" });

    // Готовим сообщение для сброса триггера

    msg.reset = true;

    return msg;

    Выход этой ноды также подключается ко входу `trigger`.

    Теперь наш поток полностью автономен и надежен. Он попытается доставить команду до 3 раз. Если все попытки провалятся, он отправит детальное уведомление системному администратору и прекратит дальнейшие действия, предотвращая "заспамливание" сети. При успешном выполнении счётчики и таймеры корректно сбрасываются, готовя систему к следующей команде.

    ---

    Итоги и ключевые выводы

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

    📋 Ключевые выводы:

    🔗 Связанный материал:

    > Данный паттерн является основой для более сложных сценариев, таких как работа с группами устройств и управление сценами, которые мы разберем в модуле `COURSE-07-M02`.

    Что дальше

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