ГлавнаяАкадемияNode-RED: установка, flows, msg/JSON, отладка → Практика: Связывание двух контроллеров по MQTT

Практика: Связывание двух контроллеров по MQTT

Урок 3 · Node-RED: установка, flows, msg/JSON, отладка · 30 мин · theory

Введение: Сценарий 'Master/Slave' для двух контроллеров

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

> 💡 Подсказка: Хотя мы используем термины 'Master' и 'Slave' для простоты, архитектура MQTT является клиент-серверной, где любой клиент может быть и издателем, и подписчиком. Это обеспечивает максимальную гибкость. В нашем примере один контроллер инициирует команду (Master), а другой её исполняет (Slave), но в более сложных сценариях они могут легко поменяться ролями.

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

Постановка задачи:

Имеется два контроллера HI, назовем их Контроллер А и Контроллер B.

  • К универсальному входу (UI) Контроллера А подключен настенный выключатель.
  • К релейному выходу (RL) Контроллера B подключен светильник.
  • Необходимо сделать так, чтобы нажатие на выключатель у Контроллера А включало или выключало светильник на Контроллере B.
  • Обзор архитектуры:

    Сигнал пройдет следующий путь:

  • Физический уровень (A): Пользователь нажимает на выключатель. Контроллер A фиксирует замыкание «сухого контакта» на своем входе.
  • Логика на Контроллере A: Flow в Node-RED считывает состояние входа, формирует стандартизированное сообщение в формате JSON и публикует его в определенный MQTT-топик с помощью ноды `MQTT Out`. Контроллер А в этой схеме выступает в роли издателя (Publisher).
  • Сетевой уровень: Сообщение поступает на MQTT-брокер, который является центральным посредником.
  • Логика на Контроллере B: Контроллер В, будучи подписанным на этот же топик с помощью ноды `MQTT In`, немедленно получает сообщение от брокера. Он выступает в роли подписчика (Subscriber).
  • Исполнение на Контроллере B: Flow в Node-RED разбирает (парсит) полученное JSON-сообщение, извлекает команду и передает ее на ноду, управляющую физическим реле.
  • Физический уровень (B): Реле замыкается или размыкается, управляя светильником.
  • Эта архитектура позволяет легко масштабировать систему: мы можем добавить еще один выключатель на Контроллере А или даже на новом Контроллере С, который будет управлять тем же светильником, просто публикуя сообщения в тот же топик. Аналогично, мы можем подписать на этот топик еще один светильник, и он будет работать синхронно с первым.

    ---

    Шаг 1: Настройка 'Master' контроллера (А) на публикацию состояний

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

    Создание flow в Node-RED на Контроллере A

    Предположим, что наш выключатель подключен к универсальному входу №5 (UI-05).

  • Создаем ноду-источник. В реальном проекте это будет нода `rpi gpio in`, настроенная на чтение состояния пина, соответствующего UI-05. Для эмуляции в учебных целях мы можем использовать две ноды `Inject`, которые будут отправлять `true` и `false` для имитации включения и выключения.
  • Формируем сообщение. После ноды-источника размещаем ноду `Function`. Ее основная задача — применить "Паттерн Контракт сообщения", который мы изучали ранее. Простое значение `true` или `false` не несет достаточно информации. Нам нужно "обогатить" его метаданными.
  • Публикуем в MQTT. Выход ноды `Function` подключаем к ноде `MQTT Out`.
  • Схема потока будет выглядеть так:

    [Inject: true] --+
    

    |--> [Function: Формировать команду] --> [MQTT Out: hi/floor1/room101/light/01/set] --> [Debug: Статус отправки]

    [Inject: false]--+

    Код и конфигурация

    Нода `Function` "Формировать команду":

    Эта нода принимает на вход `msg.payload` со значением `true` или `false` и преобразует его в полноценный JSON-объект.

    // Текущее состояние выключателя приходит в msg.payload (например, true или false)
    

    const state = msg.payload;

    // ID устройства-источника. Это должно быть уникальное имя в рамках проекта.

    const sourceId = "BTN-ROOM101-MAIN";

    // Формируем payload по стандарту "Контракта сообщения"

    msg.payload = {

    "value": state, // Основное значение: true/false (вкл/выкл)

    "source": sourceId, // Источник команды

    "ts": Date.now(), // Временная метка для отладки и анализа

    "meta": {

    "description": "Команда от главного выключателя в комнате 101"

    }

    };

    // Преобразуем объект JSON в строку для передачи по MQTT.

    // Многие брокеры и клиенты делают это автоматически, но явное преобразование - хорошая практика.

    msg.payload = JSON.stringify(msg.payload);

    // Устанавливаем флаг retain. Если true, брокер сохранит последнее сообщение

    // и отправит его новым подписчикам. Полезно для состояний.

    msg.retain = true;

    // Устанавливаем QoS (Quality of Service). QoS 1 гарантирует доставку хотя бы один раз.

    msg.qos = 1;

    // Обновляем статус ноды для визуального контроля

    node.status({fill:"blue", shape:"dot", text: `Отправка: ${state}`});

    return msg;

    Этот код создает информативное и самодостаточное сообщение. Получатель будет точно знать, что за команда пришла, откуда и когда.

    Нода `MQTT Out`:

    Здесь мы настраиваем параметры подключения к брокеру и целевой топик.

    * `hi`: Префикс проекта.

    * `floor1/room101`: Местоположение.

    * `light/01`: Тип и номер устройства.

    * `set`: Намерение (установить новое состояние).

    После развертывания этого flow, каждое нажатие на `Inject` будет отправлять структурированное JSON-сообщение в указанный топик на MQTT-брокере.

    ---

    Шаг 2: Настройка 'Slave' контроллера (B) на прием команд

    Теперь перейдем к Контроллеру B. Его задача — слушать MQTT-брокер, ловить нужные сообщения и исполнять содержащиеся в них команды.

    > ⚠️ Внимание: Убедитесь, что оба контроллера имеют сетевой доступ к MQTT-брокеру. Проверьте IP-адреса, маски подсети и настройки файрволов как на контроллерах, так и на сервере с брокером. Самая частая проблема на этом этапе — банальное отсутствие сетевого подключения между компонентами.

    Предположим, светильник подключен к релейному выходу №1 (RL-01) Контроллера B.

    Создание flow в Node-RED на Контроллере B

  • Подписываемся на топик. Основным элементом здесь будет нода `MQTT In`, которая подписывается на тот же топик, в который публикует Контроллер А.
  • Обрабатываем входящее сообщение. Из ноды `MQTT In` выйдет JSON-строка. Нам нужно её распарсить и извлечь полезное значение — ключ `"value"`. Для этого используем ноду `Function`.
  • Управляем нагрузкой. Выход ноды `Function` мы подключаем к ноде, управляющей физическим реле, например `rpi gpio out`.
  • Схема потока будет выглядеть так:

    [MQTT In: hi/floor1/room101/light/01/set] --> [Function: Разобрать команду] --> [rpi gpio out: RL-01] --> [Debug: Исполнено]
    

    Код и конфигурация

    Нода `MQTT In`: Нода `Function` "Разобрать команду":

    Даже если мы выбрали автоматический парсинг, `Function` все равно полезна для валидации, логирования и извлечения нужного поля.

    // Если в ноде MQTT In выбран 'a parsed JSON object',
    

    // в msg.payload уже будет готовый объект: { value: true, source: '...', ... }

    // 1. Валидация: проверяем, что payload - это объект и содержит ключ 'value'

    if (typeof msg.payload !== 'object' || typeof msg.payload.value === 'undefined') {

    node.status({fill:"red", shape:"dot", text:"Неверный формат команды"});

    node.error("Получено сообщение с неверной структурой", msg);

    return null; // Останавливаем поток, если команда некорректна

    }

    // 2. Извлекаем полезную нагрузку

    const command = msg.payload.value;

    // 3. Подготавливаем msg для ноды управления реле.

    // Нода rpi-gpio-out ожидает 0 или 1, а не true/false.

    // Поэтому производим преобразование.

    msg.payload = command ? 1 : 0;

    // Обновляем статус для визуальной диагностики

    node.status({fill:"green", shape:"dot", text: `Команда: ${command}`});

    return msg;

    Здесь критически важен шаг 3. Разные ноды могут ожидать разные форматы данных (`true`/`false`, `0`/`1`, `"ON"`/`"OFF"`). Задача промежуточной `Function` — выступить "переводчиком" между стандартизированным форматом в MQTT и специфическими требованиями ноды исполнителя.

    Нода `rpi gpio out`:

    Теперь, если все настроено правильно, при отправке команды с Контроллера А, светильник, подключенный к Контроллеру B, будет мгновенно реагировать. Мы успешно связали два физических устройства через сеть.

    ---

    Обеспечение надежности: LWT и топики состояния

    Что произойдет, если Контроллер А внезапно выключится (сбой питания, обрыв сети)? Контроллер B не узнает об этом и будет считать, что последнее полученное состояние актуально. Для решения этой проблемы в MQTT существует мощный механизм — Last Will and Testament (LWT) или "Последняя воля и завещание".

    > 🔗 Связанный материал: Мы подробно рассматривали механизм LWT в уроке COURSE-06-M08-L01. Рекомендуем освежить знания об этой критически важной функции.

    Концепция LWT:

    При подключении клиента (нашего Контроллера А) к брокеру, он может передать "завещание":

    Если брокер обнаружит, что клиент аварийно отключился (не прислав команду `DISCONNECT`), он от имени этого клиента опубликует его "завещание" в указанный топик.

    Практическая настройка

    Для мониторинга "здоровья" устройств принято выделять отдельную ветку топиков, например `.../status`.

    На Контроллере А в ноде `MQTT Out` (или `MQTT In`) переходим на вкладку `Birth` и `Will`:
  • Birth Message (Сообщение о рождении):
  • * Topic: `hi/floor1/controllerA/status`

    * Payload: `online`

    * QoS: `1`

    * Retain: `true`

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

  • Will Message (Завещание):
  • * Topic: `hi/floor1/controllerA/status`

    * Payload: `offline`

    * QoS: `1`

    * Retain: `true`

    Это сообщение брокер опубликует, если Контроллер А "умрет".

    Реализация логики на Контроллере B

    Теперь Контроллер B должен реагировать на изменения статуса Контроллера А. Для этого создадим на нем отдельный небольшой flow:

  • `MQTT In`: Подписываемся на топик `hi/floor1/controllerA/status`.
  • `Switch`: Проверяем, что пришло в `msg.payload`: `online` или `offline`.
  • Логика реакции:
  • * Если пришло `offline`, мы можем:

    * Отправить уведомление администратору (через Telegram, Email или другой MQTT-топик).

    * Перевести управляемое оборудование в безопасное состояние (например, выключить свет).

    * Записать событие в базу данных MySQL для последующего анализа.

    * Если пришло `online`, можно сбросить флаг тревоги.

    Пример потока на Контроллере B:
                                            +--> [Function: Формировать алерт] --> [Telegram Sender]
    

    |

    [MQTT In: .../status] --> [Switch: payload] --+

    |

    +--> [Function: Сбросить алерт] --> [Debug: "Контроллер А в сети"]

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

    ---

    Отладка и мониторинг межконтроллерного взаимодействия

    Даже при тщательной настройке что-то может пойти не так. В распределенных системах отладка требует комплексного подхода.

    * На Контроллере A: после `Inject`/`rpi gpio in`, после `Function`, после `MQTT Out`. Это покажет, что именно вы отправляете.

    * На Контроллере B: после `MQTT In`, после `Function`, после `rpi gpio out`. Это покажет, что именно вы получаете и как преобразуете.

    * Увидеть, доходит ли сообщение от Контроллера А до брокера.

    * Проверить, корректен ли топик и не допустили ли вы опечатку.

    * Убедиться, что payload имеет верный формат (валидный JSON).

    * Проверить, корректно ли выставляются флаги QoS и Retain.

    * Вручную опубликовать сообщение в топик, чтобы проверить реакцию Контроллера B, не трогая Контроллер А.

    Частые ошибки и их диагностика:

    | Ошибка | Возможная причина | Метод диагностики |

    | ------------------------------------ | --------------------------------------------------------- | ------------------------------------------------------------ |

    | Контроллер B не реагирует | Опечатка в имени топика в `MQTT In` или `MQTT Out`. | Проверить имена топиков в MQTT Explorer. |

    | | Неправильный IP-адрес брокера. Firewall блокирует порт. | `ping` до брокера с контроллера. Проверка сетевых настроек. |

    | Нода `Function` на Контроллере B выдает ошибку | Контроллер А отправляет невалидный JSON. | Посмотреть payload в MQTT Explorer. Проверить код на Контроллере А. |

    | | `MQTT In` настроен на `auto-detect`, а приходит не JSON. | Установить `Output` в `a String` и парсить вручную с проверкой. |

    | Состояние не сохраняется после перезапуска | Флаг `Retain` не установлен в `true` на Контроллере А. | Проверить настройки `MQTT Out`. Посмотреть флаг в MQTT Explorer. |

    | LWT не срабатывает | Неверно настроены `Will Topic` и `Will Message`. | Проверить конфигурацию `MQTT Out` на вкладке "Will". |

    ---

    Резюме и расширение сценария

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

    Ключевые этапы, которые мы прошли:
  • Публикация (Publish): Настроили 'Master' контроллер (А) на чтение состояния входа, упаковку данных в стандартизированный JSON-формат и отправку в MQTT-топик.
  • Подписка (Subscribe): Настроили 'Slave' контроллер (B) на прослушивание этого топика, парсинг входящего JSON и исполнение команды на физическом выходе.
  • Обеспечение надежности: Внедрили механизм "Last Will and Testament" (LWT) и статусные топики для мониторинга работоспособности удаленного устройства и реакции на его отказ.
  • Этот урок закрепляет фундаментальную важность двух принципов:

    Что дальше?

    Освоенный сценарий — это базовый строительный блок. На его основе можно реализовывать гораздо более сложные взаимодействия:

    В следующем уроке мы углубимся в работу с HTTP API, что откроет еще один мощный канал для интеграции нашей системы с веб-сервисами и сторонним оборудованием.