Лабораторная работа: COURSE-05-M03-LAB02 'Двигатель шторы: интерлоки и защита'
Цели, задачи и схема лабораторного стенда
Целью данной лабораторной работы является создание безопасного и надежного потока Node-RED для управления реверсивным двигателем шторы. Основной акцент делается на реализации механизмов защиты, которые предотвратят повреждение оборудования и обеспечат предсказуемую работу системы даже в нештатных ситуациях. По итогам выполнения вы сможете спроектировать и внедрить полный цикл управления исполнительным устройством: от приема команды до обработки обратной связи и защиты от сбоев.
Ключевая проблема, которую мы решаем, — это защита обмоток реверсивного двигателя от повреждения. Подача напряжения одновременно на обмотки "ВВЕРХ" и "ВНИЗ" приводит к короткому замыканию и мгновенному выходу двигателя из строя. Наша задача — на программном уровне исключить саму возможность возникновения такой ситуации. Этот принцип называется взаимная блокировка (interlock).
> 🔗 Связанный материал: Для лучшего понимания логики работы реверсивного двигателя, обратитесь к материалам урока COURSE-05-M03-L04 "Flow Diagram: Сценарий управления реверсивным двигателем".
Для выполнения работы мы будем использовать виртуальный лабораторный стенд, имитирующий реальное оборудование с помощью протокола MQTT. Это стандартная практика, позволяющая отладить логику без риска для физических устройств.
Компоненты нашего стенда:- `mqtt in`: для приема команд управления и сигналов от концевых выключателей.
- `mqtt out`: для отправки команд на виртуальные реле.
- `switch`: ключевой узел для маршрутизации команд и для реализации логики блокировки.
- `change`: для установки и изменения значений переменных состояния.
- `trigger`: для создания защитного таймера (таймаута).
- `function`: для написания более сложной логики и форматирования сообщений.
---
Реализация базовой взаимной блокировки
Первый и самый важный уровень защиты — это логическая блокировка, которая не позволит системе отправить команду на движение, если двигатель уже находится в движении. Мы реализуем это с помощью переменной состояния потока (flow context), которая будет служить флагом "занятости" двигателя.
> 💡 Подсказка: Используйте структурированные MQTT-топики. Например, `.../cmd` для команд, `.../state` для ответа о состоянии. Это стандарт индустрии, который упрощает отладку и интеграцию.
Пошаговая инструкция:* Добавьте на холст узел `mqtt in`.
* Настройте его на подписку на топик `hi/devices/curtain_1/cmd`.
* Укажите ваш MQTT-брокер и установите QoS = 1 для гарантированной доставки команд.
* Назовите узел "CMD: up/down/stop".
* Соедините выход узла `mqtt in` со входом узла `switch`.
* Настройте узел `switch` для проверки `msg.payload`:
* Правило 1: `==` "up"
* Правило 2: `==` "down"
* Правило 3: `==` "stop"
* Это направит каждую команду на свою собственную ветку обработки.
* Перед ветками "up" и "down" добавьте еще один узел `switch`. Назовите его "Check Motor Status". Этот узел будет выполнять проверку состояния перед выполнением действия.
* Настройте его на проверку переменной `flow.motor_status`.
* Правило 1: `==` "stopped" (или `is null`). Только если двигатель остановлен, мы можем дать новую команду на движение.
* Соедините выход этого узла с последующей логикой обработки команд "up" и "down".
* Команда "stop" должна обходить эту проверку, так как остановка двигателя должна быть возможна в любой момент.
* После того как команда "up" или "down" успешно прошла проверку статуса, нам нужно немедленно обновить состояние двигателя.
* Используйте узел `change` или `function`. Узел `function` предпочтительнее, так как позволяет добавить логирование.
* Для ветки "up": добавьте узел `function` с именем "Set State: Moving Up".
// Устанавливаем статус "движение вверх"
flow.set("motor_status", "moving_up");
// Логируем действие для последующего анализа
node.log({
message: "Motor command received and accepted",
command: "up",
newState: "moving_up"
});
// Формируем payload для реле
msg.payload = true;
return msg;
* Для ветки "down": аналогичный узел `function` "Set State: Moving Down", который устанавливает `flow.set("motor_status", "moving_down")`.
* Для ветки "stop": добавьте узел `function` "Set State: Stopped", который должен устанавливать `flow.set("motor_status", "stopped")`. Эта ветка — единственное место, где статус сбрасывается в "stopped".
* Соедините выход узла "Set State: Moving Up" со входом узла `mqtt out`, настроенного на топик `hi/devices/curtain_1/relay/up/set`.
* Аналогично для команды "down" и топика `hi/devices/curtain_1/relay/down/set`.
* Для команды "stop" нужно отправить `false` на ОБА реле, чтобы гарантированно снять напряжение с обеих обмоток.
Теперь, если двигатель уже движется (например, `flow.motor_status` равен `moving_up`), и приходит новая команда `down`, она будет заблокирована узлом "Check Motor Status", предотвращая одновременную подачу питания на обе обмотки.
---
Защита двигателя по таймауту
Второй уровень защиты — это таймаут на максимальное время работы. Он необходим для сценария, когда механический концевой выключатель не сработал. Без этого таймаута двигатель будет продолжать работать "в упор", что приведет к его перегреву и возможному выходу из строя.
> ⚠️ Внимание: Критически важно! Время таймаута должно быть на 10-15% больше максимального времени полного цикла открытия/закрытия шторы. Слишком короткий таймаут приведет к ложным срабатываниям и некорректной работе автоматики.
Мы реализуем этот механизм с помощью узла Node-RED: нода Trigger. Этот узел может отправлять одно сообщение, ждать заданное время, а затем отправлять другое. Мы будем использовать его для отправки команды "stop" по истечении таймаута.
Интеграция в существующий поток:* Разместите на холсте узел `trigger`. Назовите его "Safety Timeout".
* Он должен активироваться сразу после того, как команда на движение ("up" или "down") была принята и статус двигателя был изменен на "moving_...". Соедините выходы узлов "Set State: Moving Up" и "Set State: Moving Down" со входом этого узла `trigger`.
* Откройте настройки узла "Safety Timeout".
* В поле `Send` установите `nothing`. Нам не нужно отправлять сообщение сразу.
* В разделе `then wait for` установите ваше время таймаута, например, `30` секунд. Это значение должно быть подобрано под конкретный механизм шторы.
* В разделе `then send` выберите `the following` и установите `payload` (string) в значение `"stop"`.
* Флажок `Extend delay if new message arrives` должен быть снят.
* В выпадающем списке `Handle` выберите `all messages`.
* Теперь выход узла `trigger` будет отправлять сообщение `msg.payload = "stop"` через 30 секунд.
* Соедините выход узла `trigger` со входом самого первого узла `switch` ("CMD: up/down/stop"), который маршрутизирует команды. Таким образом, сгенерированный "stop" будет обработан точно так же, как и команда, пришедшая от пользователя по MQTT.
* Самое главное — таймер нужно сбрасывать, как только двигатель останавливается по штатной причине (команда "stop" от пользователя или сигнал от концевого выключателя).
* В настройках узла `trigger` есть поле `If `msg.payload` arrives...`. Установите его в значение `reset the timer`.
* Теперь нам нужно подать на вход узла `trigger` сообщение, которое его сбросит.
* В узле `function` "Set State: Stopped", который обрабатывает команду "stop", добавьте вторую строчку:
// Устанавливаем статус "остановлен"
flow.set("motor_status", "stopped");
// Формируем сообщение для сброса таймера
msg.payload = "reset"; // Содержимое не важно, главное - сам факт сообщения
return msg;
* Соедините выход узла "Set State: Stopped" со входом узла `trigger`. Теперь каждая команда "stop" будет не только менять логический статус, но и отменять запущенный ранее защитный таймаут.
Этот механизм гарантирует, что двигатель никогда не будет работать дольше положенного времени, обеспечивая надежную защиту от перегрева.
---
Синхронизация состояния: обработка концевых выключателей
Третий уровень надежности — это синхронизация логического состояния в Node-RED с реальным, физическим состоянием устройства. Для этого используются сигналы от концевых выключателей, которые сообщают о достижении шторой крайних положений (полностью открыта или полностью закрыта).
> 💡 Подсказка: Публикуйте текущий статус мотора (moving_up, moving_down, stopped) в отдельный топик `.../state`. Это упростит интеграцию с панелями визуализации и голосовыми ассистентами, предоставляя им актуальную обратную связь.
Обработка этих сигналов критически важна. Получение сигнала от концевого выключателя функционально эквивалентно штатной команде "stop".
Пошаговая инструкция:* Добавьте на холст два новых узла `mqtt in`.
* Настройте первый на подписку на топик `hi/devices/curtain_1/feedback/fully_open`. Назовите его "FB: Fully Open".
* Настройте второй на подписку на топик `hi/devices/curtain_1/feedback/fully_closed`. Назовите его "FB: Fully Closed".
* Предположим, что при срабатывании выключателя в эти топики приходит сообщение со значением `true`.
* Нам нужно, чтобы сигнал от любого из концевиков превращался в команду "stop".
* Добавьте узел `change` после каждого из новых узлов `mqtt in`.
* Настройте оба узла `change` так, чтобы они устанавливали `msg.payload` в строковое значение `"stop"`.
* Назовите эти узлы "Set cmd: stop".
* Теперь соедините выходы обоих узлов `change` со входом нашего главного маршрутизатора — узла `switch` "CMD: up/down/stop".
* Когда штора достигает, например, верхнего положения, концевой выключатель срабатывает, отправляет сообщение в `.../fully_open`, узел `change` преобразует его в команду `"stop"`, и эта команда проходит через всю нашу отлаженную логику:
1. Статус двигателя устанавливается в `flow.set("motor_status", "stopped")`.
2. Запущенный защитный таймер в узле `trigger` сбрасывается.
3. На оба реле отправляется команда `false`, снимая напряжение с двигателя.
Эта простая, но мощная связка обеспечивает полную синхронизацию. Логика в Node-RED всегда знает, когда двигатель физически остановился, что исключает рассинхронизацию состояний и делает автоматизацию по-настоящему надежной. Если система думает, что штора еще едет, хотя она уже уперлась в край, любая последующая логика (например, сценарий "Закрыть все шторы") будет работать некорректно. Синхронизация решает эту проблему.
---
Итоги и полный код потока для импорта
В этой лабораторной работе мы создали многоуровневую систему защиты для реверсивного двигателя, используя стандартные узлы Node-RED. Выстроенная архитектура потока решает три ключевые задачи:
Этот паттерн является универсальным и может быть легко адаптирован для управления любыми другими реверсивными механизмами: гаражными воротами, жалюзи, маркизами, подъемниками. Изменяются только MQTT-топики и время таймаута, но фундаментальная логика безопасности остается той же.
Ниже приведен полный JSON-код потока, который вы можете импортировать в свой редактор Node-RED (`Меню -> Импорт`) для изучения и дальнейшей модификации.
```json
[
{
"id": "cc317b9b.e2a858",
"type": "tab",
"label": "COURSE-05-M03-LAB02: Curtain Interlock",
"disabled": false,
"info": "Поток для управления реверсивным двигателем шторы с взаимной блокировкой и защитой по таймауту."
},
{
"id": "a1b2c3d4.123456",
"type": "mqtt in",
"z": "cc317b9b.e2a858",
"name": "CMD: up/down/stop",
"topic": "hi/devices/curtain_1/cmd",
"qos": "1",
"datatype": "utf8",
"broker": "YOUR_MQTT_BROKER_ID",
"x": 170,
"y": 200,
"wires": [
[
"b5c6d7e8.234567"
]
]
},
{
"id": "b5c6d7e8.234567",
"type": "switch",
"z": "cc317b9b.e2a858",
"name": "Route Command",
"property": "payload",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "up",
"vt": "str"
},
{
"t": "eq",
"v": "down",
"vt": "str"
},
{
"t": "eq",
"v": "stop",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 3,
"x": 370,
"y": 200,
"wires": [
[
"c9d0e1f2.345678"
],
[
"c9d0e1f2.345678"
],
[
"d3e4f5a6.456789"
]
]
},
{
"id": "c9d0e1f2.345678",
"type": "switch",
"z": "cc317b9b.e2a858",
"name": "Check Motor Status (Interlock)",
"property": "motor_status",
"propertyType": "flow",
"rules": [
{
"t": "eq",
"v": "stopped",
"vt": "str"
},
{
"t": "isnull"
}
],
"checkall": "false",
"repair": false,
"outputs": 2,
"x": 580,
"y": 160,
"wires": [
[
"e7f8a9b0.567890"
],
[
"e7f8a9b0.567890"
]
]
},
{
"id": "d3e4f5a6.456789",
"type": "function",
"z": "cc317b9b.e2a858",
"name": "Set State: Stopped & Reset Timer",
"func": "flow.set(\"motor_status\", \"stopped\");\n\nnode.log({\n message: \"Motor stopped by command or feedback\",\n newState: \"stopped\"\n});\n\n// Отправляем false на ОБА реле для гарантированной остановки\nreturn [\n { payload: false }, // для реле UP\n { payload: false }, // для реле DOWN\n { payload: \"reset\"} // для сброса trigger\n];",
"outputs": 3,
"noerr": 0,
"initialize": "",
"finalize": "",
"x": 590,
"y": 260,
"wires": [
[
"f1g2h3i4.678901"
],
[
"a2b3c4d5.789012"
],
[
"b6c7d8e9.890123"
]
]
},
{
"id": "e7f8a9b0.567890",
"type": "function",
"z": "cc317b9b.e2a858",
"name": "Set State: Moving & Prep Relay",
"func": "let direction = msg.payload; // up or down\n\nif (direction === 'up') {\n flow.set('motor_status', 'moving_up');\n msg.payload = true;\n return [msg, null];\n} else if (direction === 'down') {\n flow.set('motor_status', 'moving_down');\n msg.payload = true;\n return [null, msg];\n}\n",
"outputs": 2,
"noerr": 0,
"initialize": "",
"finalize": "",
"x": 820,
"y": 160,
"wires": [
[
"f1g2h3i4.678901",
"b6c7d8e9.890123"
],
[
"a2b3c4d5.789012",
"b6c7d8e9.890123"
]
]
},
{
"id": "f1g2h3i4.678901",
"type": "mqtt out",
"z": "cc317b9b.e2a858",
"name": "RELAY: UP",
"topic": "hi/devices/curtain_1/relay/up/set",
"qos": "1",
"retain": "false",
"broker": "YOUR_MQTT_BROKER_ID",
"x": 1050,
"y": 140,
"wires": []
},
{
"id": "a2b3c4d5.789012",
"type": "mqtt out",
"z": "cc317b9b.e2a858",
"name": "RELAY: DOWN",
"topic": "hi/devices/curtain_1/relay/down/set",
"qos": "1",
"retain": "false",
"broker": "YOUR_MQTT_BROKER_ID",
"x": 1060,
"y": 200,
"wires": []
},
{
"id": "b6c7d8e9.890123",
"type": "trigger",
"z": "cc317b9b.e2a858",
"name": "Safety Timeout (30s)",
"op1": "",
"op2": "stop",
"op1type": "nul",
"op2type": "str",
"duration": "30",
"extend": false,
"overrideDelay": false,
"units": "s",
"reset": "reset",
"bytopic": "all",
"topic": "topic",
"outputs": 1,
"x": 800,
"y": 340,
"wires": [
[
"b5c6d7e8.234567"
]
]
},
{
"id": "c0d1e2f3.901234",
"type": "mqtt in",
"z": "cc317b9b.e2a858",
"name": "FB: Fully Open",
"topic": "hi/devices/curtain_1/feedback/fully_open",
"qos": "1",
"datatype": "utf8",
"broker": "YOUR_MQTT_BROKER_ID",
"x": 150,
"y": 380,
"wires": [
[
"d4e5f6a7.012345"
]
]
},
{
"id": "d4e5f6a7.012345",
"type": "change",
"z": "cc317b9b.e2a858",
"name": "Set cmd: stop",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "stop",
"tot": "str"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 360,
"y": 410,
"wires": [
[
"b5c6d7e8.234567"
]
]
},
{
"id": "e8f9a0b1.123456",
"type": "mqtt in",
"z": "cc317b9b.e2a858",
"name": "FB: Fully Closed",
"topic": "hi/devices/curtain_1/feedback/fully_closed",
"qos": "1",
"datatype": "utf8",
"broker": "YOUR_MQTT_BROKER_ID",
"x": 150,
"y": 440,
"wires": [
[
"d4e5f6a7.012345"
]
]
},
{
"id": "f2g3h4i5.234567",
"type": "comment",
"z": "cc317b9b.e2a858",
"name": "Управление и Команды",
"scope": "local",
"x": 160,
"y": 140,
"wires": []
},
{
"id": "a6b7c8d9.345678",
"type": "comment",
"z": "cc317b9b.e2a858",
"name": "Обратная связь от оборудования",
"scope": "local",
"x": 190,
"y": 320,
"wires": []
},
{
"id": "YOUR_MQTT_BROKER_ID",
"type": "mqtt-broker",
"name": "Local Broker",
"broker": "localhost",
"port": "1883",
"clientid": "",
"usetls": false,
"compatmode": false,
"protocolVersion": "4",
"keepalive": "60",
"cleansession": true,
"birthTopic": "",
"birthQos": "0",
"birthPayload": "",
"closeTopic": "",
"closeQos": "0",
"closePayload": "",
"willTopic": "",
"willQos": "0",
"willPayload": ""
}
]