ГлавнаяАкадемияИсполнительные устройства: интерлоки, таймауты → Лабораторная работа: COURSE-05-M04-LAB01 'Клапан воды: команда+подтверждение+таймаут'

Лабораторная работа: COURSE-05-M04-LAB01 'Клапан воды: команда+подтверждение+таймаут'

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

COURSE-05-M04-LAB01: Введение и подготовка к лабораторной работе

Целью данной лабораторной работы является практическое закрепление навыков по созданию отказоустойчивых систем управления. Мы реализуем полный цикл управления критически важным исполнительным устройством — водяным клапаном с электроприводом — с использованием шаблона "Команда → Подтверждение (Ack) → Таймаут". Этот шаблон является обязательным стандартом для любых систем, где сбой выполнения команды может привести к серьезным последствиям, таким как протечка воды, перегрев оборудования или нарушение работы системы безопасности.

В ходе работы мы создадим поток Node-RED, который не просто отправляет команду "открыть" или "закрыть" клапан, но и контролирует ее исполнение, ожидая подтверждения от самого устройства. Если подтверждение не приходит в течение заданного времени (таймаут), система инициирует повторную отправку команды и, в случае многократной неудачи, генерирует тревожное событие для оператора и записывает инцидент в журнал аудита.

> 💡 Подсказка: Если у вас нет физического клапана с интерфейсом Modbus RTU, вы можете полностью симулировать его поведение. Для этого создайте отдельный поток в Node-RED, который будет "слушать" команды по Modbus (или MQTT) и с небольшой задержкой (1-2 секунды), используя ноду `Delay`, отправлять обратно сообщение-подтверждение о смене своего состояния. Это позволит вам отработать всю логику таймаутов и повторов без аппаратного обеспечения.

Обзор необходимого оборудования и ПО

* Node-RED с установленной палитрой `node-red-contrib-modbus`.

* Настроенный MQTT-брокер на контроллере.

Подготовка к работе

Перед началом убедитесь, что вы ознакомились с теоретическими материалами данного модуля. В частности, мы будем опираться на концепции, рассмотренные в предыдущих уроках:

Нам понадобятся следующие MQTT-топики для интеграции нашего клапана с остальной системой:

*

Секция 1: Отправка команды и формирование контракта сообщения

Первый шаг — создать механизм отправки команды, который будет инициировать весь процесс. Для отладки мы воспользуемся нодой `Inject`, которая позволит вручную запускать поток. В реальной системе триггером может служить сообщение из MQTT, нажатие кнопки в интерфейсе или логика другого сценария автоматизации.

Ключевым элементом на этом этапе является нода `Function`, в которой мы сформируем контракт сообщения. Это стандартизированный `msg`-объект, содержащий всю необходимую информацию для выполнения и контроля команды.

Пошаговая реализация

  • Создайте триггер: Разместите на поле ноду `Inject`. Настройте ее для отправки строкового payload: `"OPEN"`. Создайте вторую ноду `Inject` для отправки `"CLOSE"`. Это позволит нам тестировать обе команды.
  • Сформируйте контракт: Добавьте ноду `Function` с именем "Формирование команды клапану". Вставьте в нее следующий код:
  •     // Определяем 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;

  • Отправьте команду: Подключите выход ноды `Function` к входу ноды `Modbus-Write`. Настройте `Modbus-Write`:
  • * 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 хранится текущий статус).

  • Фильтрация и нормализация ответа: Ответ от `Modbus-Getter` нужно обработать.
  • * Добавьте ноду `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` генерирует поток сообщений, содержащих актуальное состояние клапана. Теперь нам нужно использовать эти сообщения, чтобы "сбрасывать" таймер ожидания.

  • Связывание Ack с командой: Сообщение-подтверждение должно остановить таймер, запущенный после отправки команды. Для этого мы будем использовать `transaction_id`, как это обсуждалось в уроке `COURSE-05-M04-L05`. Однако в простом сценарии можно обойтись проверкой соответствия состояния.
  • > ⚠️ Внимание: Не полагайтесь на порядок получения сообщений. В асинхронных системах, таких как MQTT или Modbus, подтверждение может прийти с задержкой, не прийти вовсе или прийти несколько раз. Логика должна быть построена так, чтобы она корректно обрабатывала все эти сценарии. Именно для этого и нужен механизм таймаута, который мы реализуем в следующей секции.

    ---

    Секция 3: Реализация таймаута и логики повторных попыток

    Это ядро нашего отказоустойчивого механизма. Мы используем ноду `Trigger` для создания "окна ожидания" после отправки команды. Если за это время не приходит правильное подтверждение, `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`.

  • Сброс таймера: Теперь нам нужно подать сигнал сброса на `Trigger`. Для этого мы возьмем поток сообщений из нашей ветки "Обработка статуса клапана".
  • * Добавьте ноду `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`:

  • После "Формирование команды" — чтобы увидеть, какой контракт сообщения мы создали.
  • После "Обработка таймаута / Retry" (на обоих выходах) — чтобы видеть срабатывания таймаута и логику повторов.
  • После "Проверка Ack" — чтобы убедиться, что подтверждения корректно распознаются.
  • Тестовые сценарии:
  • Штатный режим: Запустите поток. Нажмите `Inject "OPEN"`. В логах `Debug` вы должны увидеть:
  • * Сформированную команду.

    * Через несколько секунд — сообщение-подтверждение и сообщение с `topic="reset"`.

    * Таймер на `Trigger` не сработает. Клапан на объекте откроется.

  • Имитация сбоя (таймаут): Отключите физически шину RS-485 от клапана или остановите ваш симулятор.
  • * Нажмите `Inject "OPEN"`.

    * Через 15 секунд нода `Trigger` отправит сообщение о таймауте.

    * Нода `Function` "Обработка таймаута" увеличит `retry_count` до 1 и отправит команду повторно.

    * Это повторится 3 раза.

    * После третьей неудачи будет сформировано и отправлено тревожное сообщение в MQTT.

  • Восстановление связи: Во время выполнения сценария №2, после первой или второй попытки, подключите шину обратно. Система должна "поймать" `Ack` и прервать цикл повторов.
  • Экспорт готового потока

    Ниже представлен полный 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`, можно построить надежный механизм, который гарантирует либо выполнение команды, либо своевременное оповещение о сбое.

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

    Что дальше?

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