Главная → Академия → Исполнительные устройства: интерлоки, таймауты → Лабораторная работа: COURSE-05-M03-LAB02 'Двигатель шторы: интерлоки и защита'

Лабораторная работа: COURSE-05-M03-LAB02 'Двигатель шторы: интерлоки и защита'

Цели, задачи и схема лабораторного стенда

Целью данной лабораторной работы является создание безопасного и надежного потока Node-RED для управления реверсивным двигателем шторы. Основной акцент делается на реализации механизмов защиты, которые предотвратят повреждение оборудования и обеспечат предсказуемую работу системы даже в нештатных ситуациях. По итогам выполнения вы сможете спроектировать и внедрить полный цикл управления исполнительным устройством: от приема команды до обработки обратной связи и защиты от сбоев.

Ключевая проблема, которую мы решаем, — это защита обмоток реверсивного двигателя от повреждения. Подача напряжения одновременно на обмотки "ВВЕРХ" и "ВНИЗ" приводит к короткому замыканию и мгновенному выходу двигателя из строя. Наша задача — на программном уровне исключить саму возможность возникновения такой ситуации. Этот принцип называется взаимная блокировка (interlock).

> 🔗 Связанный материал: Для лучшего понимания логики работы реверсивного двигателя, обратитесь к материалам урока COURSE-05-M03-L04 "Flow Diagram: Сценарий управления реверсивным двигателем".

Для выполнения работы мы будем использовать виртуальный лабораторный стенд, имитирующий реальное оборудование с помощью протокола MQTT. Это стандартная практика, позволяющая отладить логику без риска для физических устройств.

Компоненты нашего стенда:
  • Панель управления (виртуальная): Любой MQTT-клиент (например, MQTT Explorer), который будет публиковать команды в топик `hi/devices/curtain_1/cmd`. Сообщения будут содержать простые строковые команды: `"up"`, `"down"` и `"stop"`.
  • Контроллер HI (Node-RED): Наша основная рабочая среда, где мы будем создавать поток. Он подписывается на топик команд.
  • Исполнительное устройство (виртуальное): Управляющие сигналы контроллер будет публиковать в топики `hi/devices/curtain_1/relay/up/set` и `hi/devices/curtain_1/relay/down/set`. Сообщения `true` или `false` имитируют замыкание и размыкание реле.
  • Концевые выключатели (виртуальные): Сигналы о достижении крайних положений шторы будут приходить в топики `hi/devices/curtain_1/feedback/fully_open` и `hi/devices/curtain_1/feedback/fully_closed`.
  • Для сборки потока нам потребуются следующие узлы (ноды) из стандартной палитры Node-RED:
    • `mqtt in`: для приема команд управления и сигналов от концевых выключателей.
    • `mqtt out`: для отправки команд на виртуальные реле.
    • `switch`: ключевой узел для маршрутизации команд и для реализации логики блокировки.
    • `change`: для установки и изменения значений переменных состояния.
    • `trigger`: для создания защитного таймера (таймаута).
    • `function`: для написания более сложной логики и форматирования сообщений.

    ---

    Реализация базовой взаимной блокировки

    Первый и самый важный уровень защиты — это логическая блокировка, которая не позволит системе отправить команду на движение, если двигатель уже находится в движении. Мы реализуем это с помощью переменной состояния потока (flow context), которая будет служить флагом "занятости" двигателя.

    > 💡 Подсказка: Используйте структурированные MQTT-топики. Например, `.../cmd` для команд, `.../state` для ответа о состоянии. Это стандарт индустрии, который упрощает отладку и интеграцию.

    Пошаговая инструкция:
  • Шаг 1: Прием команд.
  • * Добавьте на холст узел `mqtt in`.

    * Настройте его на подписку на топик `hi/devices/curtain_1/cmd`.

    * Укажите ваш MQTT-брокер и установите QoS = 1 для гарантированной доставки команд.

    * Назовите узел "CMD: up/down/stop".

  • Шаг 2: Маршрутизация команд.
  • * Соедините выход узла `mqtt in` со входом узла `switch`.

    * Настройте узел `switch` для проверки `msg.payload`:

    * Правило 1: `==` "up"

    * Правило 2: `==` "down"

    * Правило 3: `==` "stop"

    * Это направит каждую команду на свою собственную ветку обработки.

  • Шаг 3: Реализация блокировки.
  • * Перед ветками "up" и "down" добавьте еще один узел `switch`. Назовите его "Check Motor Status". Этот узел будет выполнять проверку состояния перед выполнением действия.

    * Настройте его на проверку переменной `flow.motor_status`.

    * Правило 1: `==` "stopped" (или `is null`). Только если двигатель остановлен, мы можем дать новую команду на движение.

    * Соедините выход этого узла с последующей логикой обработки команд "up" и "down".

    * Команда "stop" должна обходить эту проверку, так как остановка двигателя должна быть возможна в любой момент.

  • Шаг 4: Управление состоянием.
  • * После того как команда "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".

  • Шаг 5: Отправка команд на реле.
  • * Соедините выход узла "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" по истечении таймаута.

    Интеграция в существующий поток:
  • Шаг 1: Добавление узла `trigger`.
  • * Разместите на холсте узел `trigger`. Назовите его "Safety Timeout".

    * Он должен активироваться сразу после того, как команда на движение ("up" или "down") была принята и статус двигателя был изменен на "moving_...". Соедините выходы узлов "Set State: Moving Up" и "Set State: Moving Down" со входом этого узла `trigger`.

  • Шаг 2: Конфигурация `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`.

  • Шаг 3: Маршрутизация сигнала от таймаута.
  • * Теперь выход узла `trigger` будет отправлять сообщение `msg.payload = "stop"` через 30 секунд.

    * Соедините выход узла `trigger` со входом самого первого узла `switch` ("CMD: up/down/stop"), который маршрутизирует команды. Таким образом, сгенерированный "stop" будет обработан точно так же, как и команда, пришедшая от пользователя по MQTT.

  • Шаг 4: Сброс таймера.
  • * Самое главное — таймер нужно сбрасывать, как только двигатель останавливается по штатной причине (команда "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".

    Пошаговая инструкция:
  • Шаг 1: Прием сигналов от концевых выключателей.
  • * Добавьте на холст два новых узла `mqtt in`.

    * Настройте первый на подписку на топик `hi/devices/curtain_1/feedback/fully_open`. Назовите его "FB: Fully Open".

    * Настройте второй на подписку на топик `hi/devices/curtain_1/feedback/fully_closed`. Назовите его "FB: Fully Closed".

    * Предположим, что при срабатывании выключателя в эти топики приходит сообщение со значением `true`.

  • Шаг 2: Унификация сигналов.
  • * Нам нужно, чтобы сигнал от любого из концевиков превращался в команду "stop".

    * Добавьте узел `change` после каждого из новых узлов `mqtt in`.

    * Настройте оба узла `change` так, чтобы они устанавливали `msg.payload` в строковое значение `"stop"`.

    * Назовите эти узлы "Set cmd: stop".

  • Шаг 3: Интеграция в основной поток.
  • * Теперь соедините выходы обоих узлов `change` со входом нашего главного маршрутизатора — узла `switch` "CMD: up/down/stop".

    * Когда штора достигает, например, верхнего положения, концевой выключатель срабатывает, отправляет сообщение в `.../fully_open`, узел `change` преобразует его в команду `"stop"`, и эта команда проходит через всю нашу отлаженную логику:

    1. Статус двигателя устанавливается в `flow.set("motor_status", "stopped")`.

    2. Запущенный защитный таймер в узле `trigger` сбрасывается.

    3. На оба реле отправляется команда `false`, снимая напряжение с двигателя.

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

    ---

    Итоги и полный код потока для импорта

    В этой лабораторной работе мы создали многоуровневую систему защиты для реверсивного двигателя, используя стандартные узлы Node-RED. Выстроенная архитектура потока решает три ключевые задачи:

  • Логическая взаимная блокировка: Предотвращает подачу конфликтующих команд ("вверх" и "вниз" одновременно) с помощью переменной состояния `flow.motor_status`.
  • Защита от перегрева по таймауту: Гарантирует отключение двигателя по истечении максимального времени работы, даже если штатные концевые выключатели не сработали.
  • Синхронизация с физическим миром: Обрабатывает сигналы от концевых выключателей, приводя логическое состояние системы в соответствие с реальным положением шторы и штатно завершая цикл движения.
  • Этот паттерн является универсальным и может быть легко адаптирован для управления любыми другими реверсивными механизмами: гаражными воротами, жалюзи, маркизами, подъемниками. Изменяются только 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": ""

    }

    ]