ГлавнаяАкадемияИсполнительные устройства: интерлоки, таймауты → Практика: Сценарий 'Открыть/стоп/закрыть' для шарового крана

Практика: Сценарий 'Открыть/стоп/закрыть' для шарового крана

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

Введение: От двухпозиционного к трехпозиционному управлению

> 🔗 Связанный материал: Данный урок является развитием идей, заложенных в уроке COURSE-05-M06-L02 "Схема управления двумя реле: Открыть/Закрыть". Рекомендуется ознакомиться с ним перед продолжением.

В предыдущих занятиях мы подробно разобрали классическую схему управления исполнительным устройством с реверсивным двигателем, таким как шаровой кран или привод заслонки. Схема на базе двух реле контроллера HI позволяет реализовать базовую логику: подать напряжение на обмотку «Открыть» для полного открытия или на обмотку «Закрыть» для полного закрытия. Это эффективное и надежное решение для задач, где требуется только два крайних положения.

Однако во многих сценариях автоматизации такая двухпозиционная логика оказывается недостаточной. Представьте систему водоснабжения, где необходимо не просто перекрыть поток, а отрегулировать его интенсивность, приоткрыв кран на 30% или 50%. Или систему вентиляции, где требуется плавно изменять положение заслонки для поддержания заданного качества воздуха. Во всех этих случаях возникает острая необходимость в третьей команде — «Стоп». Эта команда должна мгновенно прекращать движение привода, фиксируя его в текущем промежуточном положении.

Цель этого урока — спроектировать и реализовать в среде Node-RED отказоустойчивый и безопасный сценарий для управления шаровым краном по логике «Открыть/Стоп/Закрыть». Мы отойдем от простых схем и построим полноценный цифровой двойник устройства, который будет отслеживать его состояние, корректно обрабатывать команды и защищать оборудование от неправильной эксплуатации.

Для выполнения практической части урока нам понадобится следующее оборудование:

По завершении урока вы сможете создавать сложные, но при этом надежные и предсказуемые сценарии управления моторизированными устройствами, что является ключевым навыком для инсталлятора систем автоматизации.

---

Логика состояний: Управление через конечный автомат (State Machine)

> 💡 Подсказка: Использование `flow.context` для хранения состояния привода делает поток более читаемым и легким в отладке, изолируя состояние в рамках одного экрана Node-RED. Это предпочтительнее `global.context` для задач, логика которых не выходит за пределы одной вкладки.

При переходе к трехпозиционному управлению возникает логическая сложность: как система должна реагировать на команду в зависимости от текущего состояния привода? Например, команда «Открыть» должна игнорироваться, если кран уже полностью открыт или находится в процессе открытия. Простое переключение (toggle) здесь не работает, так как у нас более двух состояний.

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

Для нашего шарового крана определим следующий набор состояний:

Теперь определим входящие команды, которые могут получать наш поток (например, по MQTT): `open`, `close`, `stop`. Построим таблицу переходов, которая станет ядром логики нашего сценария.

| Текущее состояние (`flow.state`) | Команда (`msg.payload.command`) | Новое состояние | Действие |

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

| `closed` | `open` | `opening` | Включить реле "Открыть" |

| `closed` | `close` / `stop` | `closed` | Ничего не делать |

| `opening` | `stop` | `stopped` | Выключить все реле |

| `opening` | `close` | `closing` | Выключить реле "Открыть", включить реле "Закрыть" |

| `opening` | `open` | `opening` | Ничего не делать |

| `stopped` | `open` | `opening` | Включить реле "Открыть" |

| `stopped` | `close` | `closing` | Включить реле "Закрыть" |

| `stopped` | `stop` | `stopped` | Ничего не делать |

| `closing` | `stop` | `stopped` | Выключить все реле |

| `closing` | `open` | `opening` | Выключить реле "Закрыть", включить реле "Открыть" |

| `closing` | `close` | `closing` | Ничего не делать |

| `open` | `close` | `closing` | Включить реле "Закрыть" |

| `open` | `open` / `stop` | `open` | Ничего не делать |

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

Логика внутри узла `Function` будет выглядеть примерно так:

  • При получении сообщения, сначала считываем текущее состояние из контекста.
  • Если состояние не определено (первый запуск), инициализируем его как `closed`.
  • На основе текущего состояния и полученной команды, используя логику из таблицы выше, определяем новое состояние и необходимые действия.
  • Сохраняем новое состояние в контекст для следующего вызова.
  • Формируем и отправляем на выход управляющие сообщения для реле.
  • // Пример кода внутри узла Function для работы с состоянием
    
    

    // 1. Получаем текущее состояние или инициализируем его

    let state = flow.get('valve_state') || 'closed';

    // 2. Получаем команду из входящего сообщения

    let command = msg.payload.command;

    // 3. Логика переходов (упрощенный пример)

    switch (state) {

    case 'closed':

    if (command === 'open') {

    state = 'opening';

    // ... здесь код для отправки команды на реле

    }

    break;

    case 'opening':

    if (command === 'stop') {

    state = 'stopped';

    // ... здесь код для отправки команды на реле

    } else if (command === 'close') {

    state = 'closing';

    // ... здесь код для реверса

    }

    break;

    // ... и так далее для всех состояний

    }

    // 4. Сохраняем новое состояние

    flow.set('valve_state', state);

    // 5. Обновляем статус узла для наглядности

    node.status({ fill: "blue", shape: "dot", text: "State: " + state });

    return msg; // Передаем сообщение дальше

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

    ---

    Практика: Реализация интерлоков и таймаутов в Node-RED

    > ⚠️ Внимание: Аппаратная и программная блокировки — обязательное требование безопасности. Одновременная подача напряжения на обмотки «Открыть» и «Закрыть» приведет к короткому замыканию через двигатель привода, его выходу из строя и может создать аварийную ситуацию в силовой цепи.

    Теперь перейдем к сборке основного потока в Node-RED. Наша задача — не только реализовать логику конечного автомата, но и встроить в нее два критически важных механизма безопасности: программную блокировку (interlock) и таймаут безопасности (safety timeout).

    Сборка потока Node-RED

    Базовая структура потока будет выглядеть следующим образом:

    [MQTT In] (Команды) --► [Function: State Machine] --┐
    

    ├--► [Function: Relay OFF] --► [GPIO Out: Relay OPEN]

    └--► [Function: Relay ON] --► [GPIO Out: Relay CLOSE]

    На самом деле, поток будет несколько сложнее, поскольку нам нужно управлять двумя реле и таймером.

    Шаг 1: Прием команд по MQTT

    Создадим узел `mqtt in`, подписанный на топик `hi/devices/valve-01/command`. Он будет принимать JSON-сообщения вида:

    { "command": "open" }
    

    Или `"close"`, или `"stop"`.

    Шаг 2: Ядро логики — узел Function "State Machine"

    Это главный узел, реализующий FSM и программный интерлок. Он будет иметь три выхода:

  • Выход 1: Команда на реле «Открыть».
  • Выход 2: Команда на реле «Закрыть».
  • Выход 3: Команда на сброс (остановку) обоих реле.
  • Код внутри узла будет расширенной версией того, что мы обсуждали ранее. Главное дополнение — программный интерлок. Перед тем, как отдать команду на включение одного реле, мы всегда в первую очередь отдаем команду на выключение другого.

    // --- Полный код для узла "State Machine" ---
    
    

    // Получаем команду и текущее состояние

    const command = msg.payload.command;

    let state = flow.get('valve_state') || 'closed';

    // Получаем из контекста время полного хода (ВПХ), например 90 секунд

    // Это значение должно быть установлено на этапе калибровки

    const travelTimeMs = (flow.get('valve_travel_time_sec') || 90) * 1000;

    let msgOpen = null; // Сообщение для реле "Открыть"

    let msgClose = null; // Сообщение для реле "Закрыть"

    let msgStopAll = { "payload": 0, "topic": "stop" }; // Сообщение для останова

    // Логика конечного автомата

    switch (state) {

    case 'closed':

    if (command === 'open') {

    state = 'opening';

    // Сначала выключить "Закрыть", потом включить "Открыть"

    msgOpen = { payload: 1, reset: true, delay: travelTimeMs };

    }

    break;

    case 'opening':

    if (command === 'stop') {

    state = 'stopped';

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

    return [null, null, msgStopAll];

    } else if (command === 'close') {

    state = 'closing';

    // Интерлок: сначала стоп, потом реверс

    msgClose = { payload: 1, reset: true, delay: travelTimeMs };

    return [null, msgClose, msgStopAll];

    }

    break;

    case 'stopped':

    if (command === 'open') {

    state = 'opening';

    msgOpen = { payload: 1, reset: true, delay: travelTimeMs };

    } else if (command === 'close') {

    state = 'closing';

    msgClose = { payload: 1, reset: true, delay: travelTimeMs };

    }

    break;

    // ... логика для состояний 'closing' и 'open' аналогична ...

    }

    // Сохраняем новое состояние и обновляем статус

    flow.set('valve_state', state);

    node.status({ fill: "blue", shape: "dot", text: "State: " + state });

    // Возвращаем сообщения на соответствующие выходы

    return [msgOpen, msgClose, null];

    Шаг 3: Таймаут безопасности с помощью узла `trigger`

    Как видно из кода выше, мы формируем специальное сообщение для узла `trigger`. Например: `{ payload: 1, reset: true, delay: 90000 }`. Это сообщение пойдет на узел `trigger`, который будет управлять реле.

    1. Send: `payload` (берется из входящего `msg.payload`, т.е. `1`).

    2. then wait for: Значение задержки берется из `msg.delay`.

    3. then send: `payload` = `0`.

    4. Handling: `Extend delay if new message arrives`.

    5. Установите флажок "By topic", если управляете несколькими устройствами.

    Такая связка `Function` + `Trigger` решает сразу две задачи:

    Итоговая схема соединений для управления одним реле:

    [Function: State Machine] --(Выход 1)-► [trigger: Open Timer] --► [GPIO Out: Relay OPEN]
    

    |

    --(Выход 3)-► [Function: Create Reset] --► [trigger: Open Timer] (для сброса)

    Аналогичная схема собирается и для реле «Закрыть». Это и есть надежное ядро нашего сценария.

    ---

    Обратная связь и интеграция концевых выключателей

    Созданный нами поток уже умеет управлять приводом, но он работает "вслепую". Для полноценной интеграции в систему умного дома или диспетчеризации зданий нам необходима обратная связь: система должна не только отправлять команды, но и знать фактическое состояние устройства. Также необходимо интегрировать сигналы от аппаратных концевых выключателей (Limit Switches), которые являются самым надежным источником информации о крайних положениях привода.

    Отправка статуса по MQTT

    Доработаем наш основной узел `Function` "State Machine". После каждого изменения состояния мы будем формировать и отправлять на отдельный выход сообщение о новом статусе.

    Шаг 1: Добавляем четвертый выход

    В узле `Function` "State Machine" добавим 4-й выход, который будет использоваться исключительно для отправки сообщений о состоянии.

    Шаг 2: Формирование статусного сообщения

    В конце кода узла, после `flow.set(...)`, добавим следующий фрагмент:

    // ... предыдущий код ...
    
    

    // Сохраняем новое состояние и обновляем статус

    flow.set('valve_state', state);

    node.status({ fill: "blue", shape: "dot", text: "State: " + state });

    // Формируем сообщение о статусе

    const statusMsg = {

    payload: {

    state: state,

    // Для 'open' и 'closed' позиция 100% и 0% соответственно.

    // Для 'stopped' можно вычислять позицию по времени, но пока оставим null.

    position: (state === 'open') ? 100 : (state === 'closed') ? 0 : null,

    timestamp: Date.now()

    }

    };

    // Возвращаем сообщения на соответствующие выходы

    // На 4-й выход отправляем статус

    return [msgOpen, msgClose, msgStopAll, statusMsg];

    Этот выход мы подключаем к узлу `mqtt out` с топиком `hi/devices/valve-01/status`. Теперь любая система верхнего уровня (например, мобильное приложение или SCADA) может подписаться на этот топик и в реальном времени видеть, что происходит с краном.

    Интеграция концевых выключателей

    Концевые выключатели, как мы рассматривали в уроке COURSE-05-M06-L03, предоставляют самый достоверный сигнал о достижении крайних положений. Они подключаются к универсальным входам контроллера HI.

    Логика обработки: Сигнал с концевого выключателя должен иметь наивысший приоритет. Он принудительно завершает текущую операцию (открытие/закрытие) и устанавливает финальное состояние. Шаг 3: Создание обработчиков для входов
  • Создадим два узла `rpi gpio in`, настроенные на входы, к которым подключены выключатели "Полностью открыт" (Limit Switch Open, LSO) и "Полностью закрыт" (Limit Switch Closed, LSC).
  • Эти узлы будут генерировать сообщение при замыкании/размыкании контактов. Объединим их выходы и направим на вход нашего `Function` "State Machine". Чтобы отличить их от MQTT-команд, зададим им разные `msg.topic`. Например:
  • Шаг 4: Доработка `Function` "State Machine"

    Теперь нам нужно модифицировать код, чтобы он реагировал на эти новые сообщения.

    // --- Модифицированный код узла "State Machine" ---
    
    

    // Определяем, пришла команда или сигнал с концевика

    if (msg.topic === 'limit_switch') {

    // Получен сигнал от концевого выключателя

    const limit = msg.payload; // "open" или "closed"

    // Принудительно останавливаем движение

    const stopMsg = { "payload": 0, "topic": "stop" };

    if (limit === 'open') {

    flow.set('valve_state', 'open');

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

    } else if (limit === 'closed') {

    flow.set('valve_state', 'closed');

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

    }

    // Формируем статусное сообщение

    const statusMsg = { payload: { state: flow.get('valve_state'), position: (limit === 'open' ? 100 : 0) }};

    // Отправляем команду "Стоп" на 3-й выход и статус на 4-й

    return [null, null, stopMsg, statusMsg];

    }

    // ... здесь остается вся предыдущая логика для обработки MQTT-команд ...

    // const command = msg.payload.command;

    // let state = flow.get('valve_state') || 'closed';

    // ... и так далее

    Теперь наш поток стал еще надежнее. Даже если мы установили неверное время хода привода, концевой выключатель сработает и остановит двигатель, предотвратив его повреждение. Сообщение, отправленное им, сбросит `trigger`-таймер и установит каноническое состояние (`open` или `closed`), которое тут же будет отправлено по MQTT.

    Итоговый JSON-объект в топике статуса:
    {
    

    "state": "open",

    "position": 100,

    "timestamp": 1678886400000

    }

    Такая структура сообщения является исчерпывающей и легко парсится на стороне клиента.

    ---

    Итоги и следующие шаги

    > 🔗 Связанный материал: Знания, полученные в этом уроке, являются фундаментом для более сложных сценариев позиционирования, которые будут рассмотрены в уроке COURSE-05-M07-L01 "Практика: Процентное управление положением штор".

    В этом уроке мы совершили важный шаг от простого двухпозиционного управления к созданию сложного и надежного сценария «Открыть/Стоп/Закрыть». Мы спроектировали и реализовали многокомпонентную систему, которая не только выполняет команды, но и обеспечивает безопасность и предоставляет исчерпывающую обратную связь.

    Давайте резюмируем ключевые элементы, которые делают наш сценарий по-настоящему готовым к эксплуатации (production-ready):

  • Конечный автомат (State Machine): Мы использовали эту концепцию для строгого контроля состояний привода, что сделало логику потока предсказуемой и легко расширяемой. Система всегда знает, в каком состоянии находится кран, и корректно реагирует на новые команды.
  • Программный интерлок: В ядро логики встроена защита от одновременного включения реле «Открыть» и «Закрыть», что является критически важным для предотвращения выхода оборудования из строя.
  • Сторожевой таймер (Watchdog): Использование узла `trigger` в паре со значением времени полного хода выступает в роли таймера безопасности. Он гарантирует отключение питания двигателя даже в случае отказа механических концевых выключателей.
  • Система обратной связи: Интеграция с MQTT для отправки статуса и прием сигналов от аппаратных концевых выключателей превращают наш поток из "черного ящика" в полноценного участника системы автоматизации, состояние которого всегда можно проконтролировать.
  • Применение такого подхода выгодно отличает профессиональную инсталляцию от любительской. Он гарантирует, что система будет работать стабильно, не повредит дорогостоящее оборудование и будет проста в диагностике и обслуживании.

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