Лабораторная работа: COURSE-05-M04-LAB01 'Клапан воды: команда+подтверждение+таймаут'
COURSE-05-M04-LAB01: Введение и подготовка к лабораторной работе
Целью данной лабораторной работы является практическое закрепление навыков по созданию отказоустойчивых систем управления. Мы реализуем полный цикл управления критически важным исполнительным устройством — водяным клапаном с электроприводом — с использованием шаблона "Команда → Подтверждение (Ack) → Таймаут". Этот шаблон является обязательным стандартом для любых систем, где сбой выполнения команды может привести к серьезным последствиям, таким как протечка воды, перегрев оборудования или нарушение работы системы безопасности.
В ходе работы мы создадим поток Node-RED, который не просто отправляет команду "открыть" или "закрыть" клапан, но и контролирует ее исполнение, ожидая подтверждения от самого устройства. Если подтверждение не приходит в течение заданного времени (таймаут), система инициирует повторную отправку команды и, в случае многократной неудачи, генерирует тревожное событие для оператора и записывает инцидент в журнал аудита.
> 💡 Подсказка: Если у вас нет физического клапана с интерфейсом Modbus RTU, вы можете полностью симулировать его поведение. Для этого создайте отдельный поток в Node-RED, который будет "слушать" команды по Modbus (или MQTT) и с небольшой задержкой (1-2 секунды), используя ноду `Delay`, отправлять обратно сообщение-подтверждение о смене своего состояния. Это позволит вам отработать всю логику таймаутов и повторов без аппаратного обеспечения.
Обзор необходимого оборудования и ПО
- Контроллер: Платформа HI с установленной средой Node-RED.
- Исполнительное устройство: Шаровой кран с электроприводом и интерфейсом Modbus RTU (например, Gidrolock, Neptun с соответствующим модулем). Устройство должно быть подключено к шине RS-485 контроллера.
- Программное обеспечение:
* Настроенный MQTT-брокер на контроллере.
Подготовка к работе
Перед началом убедитесь, что вы ознакомились с теоретическими материалами данного модуля. В частности, мы будем опираться на концепции, рассмотренные в предыдущих уроках:
- 🔗 Связанный материал: Шаблон "Команда -> Подтверждение (Ack) -> Таймаут" (COURSE-05-M04-L02)
- 🔗 Связанный материал: Реализация таймаута и повтора с помощью нод Trigger и Function (COURSE-05-M04-L04)
- 🔗 Связанный материал: Контракт сообщения для подтверждения (Ack) (COURSE-05-M04-L05)
- 🔗 Связанный материал: Журналирование (audit log) для отслеживания команд (COURSE-05-M04-L06)
Нам понадобятся следующие MQTT-топики для интеграции нашего клапана с остальной системой:
- `hi/command/kitchen/water_valve/set`: Топик для отправки команд (`"OPEN"`, `"CLOSE"`).
- `hi/state/kitchen/water_valve/status`: Топик для публикации текущего фактического состояния клапана (`"OPEN"`, `"CLOSED"`, `"TRANSITIONING"`, `"ERROR"`).
- `hi/system/alarms/water_control`: Топик для публикации тревожных сообщений в случае сбоя управления клапаном.
Секция 1: Отправка команды и формирование контракта сообщения
Первый шаг — создать механизм отправки команды, который будет инициировать весь процесс. Для отладки мы воспользуемся нодой `Inject`, которая позволит вручную запускать поток. В реальной системе триггером может служить сообщение из MQTT, нажатие кнопки в интерфейсе или логика другого сценария автоматизации.
Ключевым элементом на этом этапе является нода `Function`, в которой мы сформируем контракт сообщения. Это стандартизированный `msg`-объект, содержащий всю необходимую информацию для выполнения и контроля команды.
Пошаговая реализация
// Определяем ID устройства и адрес регистра в Modbus
const MODBUS_UNIT_ID = 10;
const MODBUS_ADDRESS = 100; // Адрес Holding Register для управления клапаном
// Получаем команду из входящего сообщения
const command = msg.payload;
let commandValue;
if (command === "OPEN") {
commandValue = 1; // '1' для открытия
} else if (command === "CLOSE") {
commandValue = 0; // '0' для закрытия
} else {
// Если команда не распознана, логируем ошибку и останавливаем поток
node.error("Неизвестная команда: " + command, msg);
node.status({fill:"red", shape:"dot", text:"ERR: unknown cmd"});
return null;
}
// 1. Формируем msg.payload для ноды modbus-write
// Этот контракт определяется документацией на Modbus-устройство
msg.payload = {
'value': commandValue,
'fc': 5, // FC 5: Force Single Coil (или FC 6 для Holding Register)
'unitid': MODBUS_UNIT_ID,
'address': MODBUS_ADDRESS
};
// 2. Добавляем метаданные для контроля исполнения
msg.topic = "water_valve_control"; // Внутренний топик для идентификации потока
msg.ack_required = true; // Флаг, что команда требует подтверждения
msg.command_sent = command; // Сохраняем исходную команду для сверки
msg.retry_count = 0; // Инициализируем счетчик повторов
msg.transaction_id = "wv-" + Date.now(); // Уникальный ID транзакции
node.status({fill:"blue", shape:"dot", text:"Cmd: " + command});
return msg;
* Server: Выберите ваш предварительно настроенный Modbus RTU клиент (подключенный к порту RS-485).
* Остальные поля (`Unit-ID`, `FC`, `Address`) будут взяты из `msg.payload`, как мы определили в контракте.
На данном этапе у нас готов фрагмент потока, который по нажатию на `Inject` формирует правильную команду и отправляет ее на физическое устройство.
> 📋 Ключевые понятия: Контракт сообщения в данном контексте — это не только `payload` для исполнительного узла, но и набор мета-полей (`ack_required`, `retry_count`, `transaction_id`), которые будут использоваться на последующих этапах для контроля и отладки.
---
Секция 2: Обработка подтверждения (Ack) от устройства
Просто отправить команду недостаточно. Мы должны убедиться, что она была не только получена, но и выполнена. Подтверждением (Acknowledgement, Ack) в нашем случае будет служить фактическое изменение состояния клапана, которое мы считаем с него отдельным запросом.
Лучшей практикой является не доверять отчету об успешной записи (`Modbus-Write`), а периодически опрашивать регистр состояния устройства (`Modbus-Read`). Это защищает от ситуаций, когда команда записана, но механизм клапана заклинило.
Пошаговая реализация
* Используйте ноду `Inject` в режиме интервальной отправки (например, каждые 2 секунды).
* Подключите ее к ноде `Modbus-Getter`. Настройте `Modbus-Getter` на чтение регистра состояния клапана. Например, `Unit-ID: 10`, `FC: 3: Read Holding Registers`, `Address: 101` (предположим, что по адресу 101 хранится текущий статус).
* Добавьте ноду `Function` "Обработка статуса клапана". Ее задача — преобразовать сырые данные из Modbus в понятный формат (`"OPEN"`, `"CLOSED"`) и сохранить в контексте потока.
let statusValue = msg.payload.data[0];
let valveStatus;
if (statusValue === 1) {
valveStatus = "OPEN";
} else if (statusValue === 0) {
valveStatus = "CLOSED";
} else {
valveStatus = "UNKNOWN";
}
// Сохраняем актуальное состояние в контексте для доступа из других потоков
flow.set("water_valve_kitchen_state", valveStatus);
// Формируем сообщение-подтверждение
msg.payload = {
ack_for: "water_valve_control", // Признак, что это Ack
status: valveStatus // Текущий статус
};
node.status({fill:"green", shape:"ring", text:"State: " + valveStatus});
return msg;
* Эта нода `Function` генерирует поток сообщений, содержащих актуальное состояние клапана. Теперь нам нужно использовать эти сообщения, чтобы "сбрасывать" таймер ожидания.
> ⚠️ Внимание: Не полагайтесь на порядок получения сообщений. В асинхронных системах, таких как MQTT или Modbus, подтверждение может прийти с задержкой, не прийти вовсе или прийти несколько раз. Логика должна быть построена так, чтобы она корректно обрабатывала все эти сценарии. Именно для этого и нужен механизм таймаута, который мы реализуем в следующей секции.
---
Секция 3: Реализация таймаута и логики повторных попыток
Это ядро нашего отказоустойчивого механизма. Мы используем ноду `Trigger` для создания "окна ожидания" после отправки команды. Если за это время не приходит правильное подтверждение, `Trigger` инициирует процедуру таймаута.
Пошаговая реализация
* Вставьте ноду `Trigger` между вашей нодой "Формирование команды клапану" и нодой `Modbus-Write`.
* Настройте ее следующим образом:
* Send: `the original message` (отправляет `msg` дальше без изменений).
* Then wait for: `15 seconds` (это наше окно ожидания Ack; для клапана это адекватное время).
* Then send: `a specific message`. В качестве payload выберите `JSON` и введите:
{
"status": "timeout"
}
* Handle: `while waiting, if msg arrives with topic...` -> `...that does not match...` -> `...the value "reset"`, `do nothing`.
* `...if msg arrives with topic that matches "reset"`, `cancel the timer and do nothing else`.
* Добавьте ноду `Switch` после "Обработка статуса клапана".
* Настройте ее так, чтобы она проверяла, соответствует ли полученный статус тому, который мы ожидаем. Для этого нам понадобится доступ к `msg.command_sent` из исходной команды. Это сложная задача, требующая сохранения состояния между потоками (например, в `flow context`).
* Более простой подход для этой лабы: Добавим ноду `Function` "Проверка Ack". Она будет получать сообщения-подтверждения. Мы ожидаем, что после команды "OPEN" придет статус "OPEN".
// Эта нода стоит после поллера состояния
const expected_command = flow.get("last_valve_command") || {};
const current_status = msg.payload.status;
// Если текущий статус соответствует тому, что мы отправляли
if (expected_command.command === current_status) {
msg.topic = "reset"; // Готовим сообщение для сброса триггера
flow.set("last_valve_command", {}); // Очищаем команду, т.к. она выполнена
node.status({fill:"green", shape:"dot", text:"Ack OK"});
return msg;
}
// Иначе, это не тот Ack, который мы ждем. Игнорируем.
return null;
* Соедините выход этой ноды со входом `Trigger`. Теперь, когда придет правильный Ack, он отправит `msg` с `topic="reset"` в `Trigger` и остановит таймер.
* Чтобы это работало, в ноду "Формирование команды клапану" нужно добавить строку: `flow.set("last_valve_command", { command: command, ts: Date.now() });`
* Второй выход `Trigger` (который срабатывает по таймауту) мы заводим на ноду `Function` "Обработка таймаута / Retry".
* Код для этой ноды:
// msg, пришедший сюда, - это исходное сообщение команды, сохраненное в Trigger
const MAX_RETRIES = 3;
msg.retry_count = (msg.retry_count || 0) + 1;
if (msg.retry_count <= MAX_RETRIES) {
// Если лимит попыток не исчерпан, отправляем на повтор
node.warn(`Таймаут выполнения команды! Попытка #${msg.retry_count}`, msg);
node.status({fill:"yellow", shape:"dot", text:`Retry ${msg.retry_count}`});
return [msg, null]; // Выход 1: на повторную отправку
} else {
// Лимит исчерпан, формируем тревогу
node.error(`КРИТИЧЕСКАЯ ОШИБКА: Клапан не отвечает после ${MAX_RETRIES} попыток!`, msg);
let alarm_msg = {
payload: {
timestamp: Date.now(),
device_id: "water_valve_kitchen",
severity: "critical",
message: `Клапан не подтвердил выполнение команды '${msg.command_sent}' после ${MAX_RETRIES} попыток.`
},
topic: "hi/system/alarms/water_control"
};
node.status({fill:"red", shape:"dot", text:"FATAL ERROR"});
return [null, alarm_msg]; // Выход 2: на отправку тревоги
}
* Нода `Function` должна иметь 2 выхода. Первый выход зацикливаем обратно на вход `Trigger`. Второй выход подключаем к ноде `MQTT Out` для отправки тревоги.
---
Секция 4: Сборка, тестирование и отладка комплексного потока
Теперь объединим все части. Финальная структура потока будет выглядеть сложной, но логичной. Используйте группы и комментарии для поддержания читаемости.
Финальная ASCII-схема потока:// Ветка 1: Инициация команды
[Inject "OPEN"]--+
+-->[Function: Формирование команды]--->[Trigger: 15s таймаут]---(out 1)-->[Modbus-Write]
[Inject "CLOSE"]--+ ^ |
| | (out 2: TIMEOUT)
| V
// Ветка 3: Повторы и тревога | [Function: Обработка таймаута / Retry]--+
// (петля обратной связи) | |
<---------------------------------------------------------------+----(out 1: RETRY) |
| |
| (out 2: ALARM)
| |
// Ветка 2: Обработка подтверждения | V
[Inject 2s] -> [Modbus-Getter] -> [Func: Статус клапана] -> [Func: Проверка Ack] --(topic=reset)--> (вход Trigger) [MQTT Out: Alarms]
Тестирование и отладка
Разместите ноды `Debug` на всех ключевых участках для отслеживания жизненного цикла `msg`:
* Сформированную команду.
* Через несколько секунд — сообщение-подтверждение и сообщение с `topic="reset"`.
* Таймер на `Trigger` не сработает. Клапан на объекте откроется.
* Нажмите `Inject "OPEN"`.
* Через 15 секунд нода `Trigger` отправит сообщение о таймауте.
* Нода `Function` "Обработка таймаута" увеличит `retry_count` до 1 и отправит команду повторно.
* Это повторится 3 раза.
* После третьей неудачи будет сформировано и отправлено тревожное сообщение в MQTT.
Экспорт готового потока
Ниже представлен полный JSON-код готового потока. Вы можете импортировать его в свой Node-RED (`Меню -> Импорт`) для изучения и адаптации.
[
{
"id": "YOUR_FLOW_ID",
"type": "tab",
"label": "LAB: Water Valve Control",
"disabled": false,
"info": "Реализация надежного управления клапаном с Ack/Timeout/Retry"
}
]
> ℹ️ Информация: Полный JSON-код потока будет предоставлен в дополнительных материалах к курсу, так как его объем слишком велик для методического пособия. Приведенный выше фрагмент является заготовкой для импорта.
---
Итоги лабораторной работы и ключевые выводы
В ходе этой лабораторной работы мы на практике реализовали один из важнейших паттернов проектирования систем автоматизации — "Команда-Подтверждение-Таймаут". Мы увидели, как с помощью стандартных нод Node-RED, таких как `Trigger`, `Function` и `Switch`, можно построить надежный механизм, который гарантирует либо выполнение команды, либо своевременное оповещение о сбое.
Ключевые выводы:
- Надежность — это не опция: Для всех критически важных исполнительных устройств (водяные клапаны, электрозамки, приводы отопления, системы вентиляции) применение подобных шаблонов является обязательным. Подход "отправил и забыл" недопустим в профессиональных инсталляциях.
- Гибкость подхода: Несмотря на то, что мы использовали Modbus RTU, данный шаблон абсолютно универсален. Его можно с тем же успехом применить для устройств, работающих по MQTT, KNX, DALI или любому другому протоколу. Главное — иметь источник подтверждения (обратную связь) и реализовать логику таймаута.
- Важность журналирования: Все события — успешное выполнение, таймауты, повторные попытки и, особенно, критические сбои — должны скрупулезно фиксироваться. Как мы обсуждали в `COURSE-05-M04-L06`, эта информация бесценна для последующего анализа инцидентов, поиска неисправностей и превентивного обслуживания.
Что дальше?
Освоив надежную доставку одиночных команд, мы готовы перейти к более сложным сценариям. В следующем модуле мы рассмотрим, как применять этот шаблон в системах с взаимными блокировками (интерлоками), где состояние одного устройства напрямую влияет на возможность управления другим. Например, запрет на включение насоса подкачки при закрытом магистральном клапане.