Практика: Сценарий 'Открыть/стоп/закрыть' для шарового крана
Введение: От двухпозиционного к трехпозиционному управлению
> 🔗 Связанный материал: Данный урок является развитием идей, заложенных в уроке COURSE-05-M06-L02 "Схема управления двумя реле: Открыть/Закрыть". Рекомендуется ознакомиться с ним перед продолжением.
В предыдущих занятиях мы подробно разобрали классическую схему управления исполнительным устройством с реверсивным двигателем, таким как шаровой кран или привод заслонки. Схема на базе двух реле контроллера HI позволяет реализовать базовую логику: подать напряжение на обмотку «Открыть» для полного открытия или на обмотку «Закрыть» для полного закрытия. Это эффективное и надежное решение для задач, где требуется только два крайних положения.
Однако во многих сценариях автоматизации такая двухпозиционная логика оказывается недостаточной. Представьте систему водоснабжения, где необходимо не просто перекрыть поток, а отрегулировать его интенсивность, приоткрыв кран на 30% или 50%. Или систему вентиляции, где требуется плавно изменять положение заслонки для поддержания заданного качества воздуха. Во всех этих случаях возникает острая необходимость в третьей команде — «Стоп». Эта команда должна мгновенно прекращать движение привода, фиксируя его в текущем промежуточном положении.
Цель этого урока — спроектировать и реализовать в среде Node-RED отказоустойчивый и безопасный сценарий для управления шаровым краном по логике «Открыть/Стоп/Закрыть». Мы отойдем от простых схем и построим полноценный цифровой двойник устройства, который будет отслеживать его состояние, корректно обрабатывать команды и защищать оборудование от неправильной эксплуатации.
Для выполнения практической части урока нам понадобится следующее оборудование:
- Контроллер HI с доступом к среде Node-RED.
- Релейный модуль, подключенный к выходам контроллера (как минимум 2 свободных реле).
- Привод шарового крана с тремя проводами управления (Общий, Открыть, Закрыть). Идеально, если привод также оснащен концевыми выключателями.
- Источник питания для привода (согласно его спецификации, обычно AC 230V или DC 24V).
По завершении урока вы сможете создавать сложные, но при этом надежные и предсказуемые сценарии управления моторизированными устройствами, что является ключевым навыком для инсталлятора систем автоматизации.
---
Логика состояний: Управление через конечный автомат (State Machine)
> 💡 Подсказка: Использование `flow.context` для хранения состояния привода делает поток более читаемым и легким в отладке, изолируя состояние в рамках одного экрана Node-RED. Это предпочтительнее `global.context` для задач, логика которых не выходит за пределы одной вкладки.
При переходе к трехпозиционному управлению возникает логическая сложность: как система должна реагировать на команду в зависимости от текущего состояния привода? Например, команда «Открыть» должна игнорироваться, если кран уже полностью открыт или находится в процессе открытия. Простое переключение (toggle) здесь не работает, так как у нас более двух состояний.
Наиболее правильным и масштабируемым подходом для решения этой задачи является реализация конечного автомата (Finite State Machine, FSM). Конечный автомат — это математическая модель, описывающая систему с конечным числом состояний. В любой момент времени система может находиться только в одном из них. Переход из одного состояния в другое осуществляется под воздействием входящих событий (в нашем случае — команд).
Для нашего шарового крана определим следующий набор состояний:
- `closed`: Кран полностью закрыт (исходное или конечное положение).
- `opening`: Кран находится в процессе движения на открытие.
- `stopped`: Кран остановлен в промежуточном положении.
- `closing`: Кран находится в процессе движения на закрытие.
- `open`: Кран полностью открыт (конечное положение).
Теперь определим входящие команды, которые могут получать наш поток (например, по 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` будет выглядеть примерно так:
// Пример кода внутри узла 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 и программный интерлок. Он будет иметь три выхода:
Код внутри узла будет расширенной версией того, что мы обсуждали ранее. Главное дополнение — программный интерлок. Перед тем, как отдать команду на включение одного реле, мы всегда в первую очередь отдаем команду на выключение другого.
// --- Полный код для узла "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`, который будет управлять реле.
- Настройка узла `trigger` (для реле «Открыть»):
2. then wait for: Значение задержки берется из `msg.delay`.
3. then send: `payload` = `0`.
4. Handling: `Extend delay if new message arrives`.
5. Установите флажок "By topic", если управляете несколькими устройствами.
Такая связка `Function` + `Trigger` решает сразу две задачи:
- Включает реле на заданное время.
- Автоматически выключает его по истечении Времени полного хода (ВПХ). Это таймаут безопасности, который предотвращает перегрев двигателя, если по какой-то причине не сработал концевой выключатель. Команда `stop` или команда реверса, отправляя сообщение `msgStopAll`, которое далее преобразуется в сообщение с `msg.reset = true`, мгновенно сбросит этот таймер, останавливая движение.
Итоговая схема соединений для управления одним реле:
[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: Создание обработчиков для входов- Для LSO: `msg.topic = "limit_switch"` и `msg.payload = "open"`
- Для LSC: `msg.topic = "limit_switch"` и `msg.payload = "closed"`
Теперь нам нужно модифицировать код, чтобы он реагировал на эти новые сообщения.
// --- Модифицированный код узла "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):
Применение такого подхода выгодно отличает профессиональную инсталляцию от любительской. Он гарантирует, что система будет работать стабильно, не повредит дорогостоящее оборудование и будет проста в диагностике и обслуживании.
В следующем уроке мы разовьем эту идею дальше. Мы научимся управлять не просто направлениям, а точным положением привода в процентах. Это позволит реализовать такие сценарии, как установка жалюзи на 50% или регулировка теплого пола с точностью до градуса, что открывает еще более широкие возможности для автоматизации.