Реализация таймаута и повтора с помощью нод Trigger и Function
Нода trigger: базовый механизм таймаута
В предыдущих уроках мы подробно рассмотрели шаблон «Команда → Подтверждение (Ack) → Таймаут» как фундаментальный принцип построения надежных систем управления. Теперь мы перейдем от теории к практике и научимся реализовывать этот шаблон с помощью стандартных средств Node-RED. Центральным элементом для реализации ожидания является нода trigger.
По своей сути, нода trigger — это усовершенствованный таймер. В отличие от простой ноды `delay`, которая лишь задерживает сообщение, `trigger` позволяет выстраивать более сложную, зависящую от времени логику.
Принцип работы ноды trigger
Основная логика ноды `trigger` следует простому, но мощному алгоритму, который идеально подходит для нашей задачи:
* Успешный сброс (Ack получен): Если в течение таймаута на вход ноды поступает сообщение со свойством `msg.reset`, таймер останавливается, и больше ничего не происходит. Это штатный сценарий, означающий, что мы получили подтверждение (`Ack`) от устройства.
* Таймаут истек: Если за отведенное время сообщение о сбросе так и не пришло, нода отправляет второе, "таймаут-сообщение". Это сигнал о том, что команда, вероятно, не была выполнена, так как подтверждение отсутствует.
Эта механика позволяет нам точно реализовать компонент "Таймаут" из нашего основного шаблона.
Конфигурация ноды
Интерфейс настройки ноды `trigger` интуитивно понятен и отражает описанный выше алгоритм.
- Отправить (Send): Здесь настраивается сообщение, которое будет отправлено немедленно. Обычно мы выбираем опцию `the original message` (оригинальное сообщение), чтобы пробросить нашу команду дальше по потоку без изменений.
- Затем ждать (then wait for): Устанавливается длительность таймаута. Это время, которое мы даем системе на доставку команды и получение подтверждения. Выбор времени зависит от типа сети и отзывчивости устройства (для локальной сети Modbus TCP это может быть 500 мс, для Zigbee — 2-3 секунды).
- Затем отправить (then send): Здесь конфигурируется сообщение, которое будет отправлено по истечении таймаута. Это может быть простое строковое значение, например `timeout`, или более сложный JSON-объект, содержащий информацию об ошибке. Крайне важно, чтобы это сообщение было уникальным и легко отличимым от штатного ответа.
> 💡 Подсказка: Для сброса `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()` для подстановки текущего времени).
* Соедините выход ноды `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.
Тестирование потока:- Сценарий 1 (Успех): Нажмите на `Inject`. В `MQTT Out` уйдет команда. Затем, в течение 3 секунд, с помощью внешнего MQTT-клиента отправьте любое сообщение в топик `hi/ack/light1`. Вы увидите, что в ноде `Debug` "ТАЙМАУТ!" ничего не появилось. Таймер был успешно сброшен.
- Сценарий 2 (Таймаут): Нажмите на `Inject`. Ничего не делайте. Через 3 секунды на втором выходе ноды `trigger` появится сообщение `timeout_error`, которое отобразится в панели `Debug`.
Этот простой, но эффективный поток является обязательным элементом для любого критически важного управления в системе.
---
Добавляем повторы (Retry) с помощью ноды function
Собранный нами поток решает проблему обнаружения сбоя, но не его исправления. Если произошел таймаут, мы просто логируем ошибку. В реальных условиях, особенно при работе с беспроводными сетями (Zigbee, LoRaWAN), единичные сбои доставки — нормальное явление. Автоматический повтор отправки команды — логичный следующий шаг для повышения надежности.
Нода `trigger` сама по себе не умеет делать повторы. Она может только один раз сообщить о таймауте. Чтобы реализовать логику повторов, нам понадобится хранить состояние — а именно, количество уже сделанных попыток. Для этого мы будем использовать контекст потока (flow context) и ноду `function`.
> ⚠️ Внимание: Всегда устанавливайте максимальное количество повторов. Отсутствие такого ограничения может привести к бесконечным циклам отправки команд, создавая высокую нагрузку на сеть и оборудование (DDoS-атака на самого себя).
Использование flow context для счетчика попыток
Контекст в Node-RED — это специальное хранилище данных, доступное внутри потока (`flow`) или глобально (`global`). Мы будем использовать `flow.context` для создания переменной-счетчика, например, `flow.retryCount`.
Логика работы будет следующей:* Получаем текущее значение счетчика: `let count = flow.get('retryCount') || 0;`.
* Проверяем, не превышен ли лимит (например, 3 попытки): `if (count < 3)`.
* Если лимит не превышен: Увеличиваем счетчик `count++`, сохраняем его обратно в контекст `flow.set('retryCount', count)` и отправляем исходное сообщение с командой обратно на вход ноды `trigger`, чтобы запустить процесс заново.
* Если лимит превышен: Попытки исчерпаны. Мы генерируем финальное сообщение об ошибке (например, для отправки уведомления администратору) и сбрасываем счетчик в `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]
Детальная реализация
Нам нужно не только сформировать команду, но и инициализировать счетчик, а также сохранить оригинальное сообщение для повторных отправок. Модифицируем самую первую ноду `Change` (или добавим новую).
* Правило 1: `Установить flow.retryCount в 0`.
* Правило 2: `Установить flow.last_command_msg в msg` (сохраняем все сообщение целиком).
* Правило 3: `Установить msg.topic` в `hi/cmd/light1`.
* Правило 4: `Установить msg.payload` в нужный JSON.
Конфигурация остается прежней. Она ждет 3 секунды.
* Первый выход (команда) идет в `MQTT Out`.
* Второй выход (таймаут) идет в нашу новую ноду `Function` с логикой повторов.
Создаем ноду `Function` с двумя выходами и вставляем код из предыдущего раздела.
* Первый выход ноды `Function` (который возвращает `original_msg` для повтора) мы соединяем обратно со входом ноды `trigger`. Это и есть цикл `Retry`.
* Второй выход ноды `Function` (который формирует `alert_msg`) мы подключаем к новой ноде `MQTT Out`, которая публикует сообщения в топик `system/alerts/critical`.
Поток, принимающий подтверждение, теперь должен выполнять две задачи: сбрасывать таймер и обнулять счетчик повторов. Модифицируем ноду `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"-потоки, которые "помнят" свои предыдущие действия и могут адаптировать свою логику в зависимости от результата.
📋 Ключевые выводы:
- Нода `trigger` является стандартным и эффективным инструментом для реализации таймаутов в Node-RED. Ее механика "отправить -> ждать -> отправить по таймауту" идеально ложится в шаблон надежной доставки команд.
- Для реализации более сложной логики, такой как повторные отправки (retry), возможностей одной ноды `trigger` недостаточно. Требуется комбинация `trigger` для тайм-аута, `flow.context` для хранения состояния (счетчика попыток) и ноды `function` для реализации самой логики.
- Правильная обработка подтверждения (`Ack`) является не менее важной частью паттерна. Поток `Ack` должен не только сбрасывать таймер с помощью `msg.reset`, но и обнулять счетчик повторов в `flow.context`, чтобы система была готова к новым командам.
- Созданный нами отказоустойчивый паттерн с повторами и финальным алертингом значительно повышает надежность системы автоматизации, особенно в условиях нестабильной среды передачи данных (беспроводные протоколы) или при работе с устройствами, которые могут быть временно недоступны.
🔗 Связанный материал:
> Данный паттерн является основой для более сложных сценариев, таких как работа с группами устройств и управление сценами, которые мы разберем в модуле `COURSE-07-M02`.
Что дальше
Мы освоили фундаментальный строительный блок надежной системы. В следующем модуле мы применим эти знания на практике, рассматривая управление более сложными исполнительными устройствами, такими как приводы штор и диммеры, где подтверждение и обработка состояния играют еще более критическую роль.