Практика: Связывание двух контроллеров по MQTT
Введение: Сценарий 'Master/Slave' для двух контроллеров
В современных системах автоматизации часто возникает задача, выходящая за рамки одного физического устройства. Объекты могут быть большими (несколько этажей в здании), физически разделенными (основной дом и гостевой дом) или требовать повышенной отказоустойчивости, когда критически важные функции дублируются на отдельном оборудовании. В таких случаях необходимо наладить надежное межконтроллерное взаимодействие. Протокол MQTT идеально подходит для решения этой задачи, позволяя создать гибкую и масштабируемую распределенную систему.
> 💡 Подсказка: Хотя мы используем термины 'Master' и 'Slave' для простоты, архитектура MQTT является клиент-серверной, где любой клиент может быть и издателем, и подписчиком. Это обеспечивает максимальную гибкость. В нашем примере один контроллер инициирует команду (Master), а другой её исполняет (Slave), но в более сложных сценариях они могут легко поменяться ролями.
В рамках данного урока мы реализуем классический сценарий: управление нагрузкой, подключенной к одному контроллеру, с помощью органа управления, подключенного к другому.
Постановка задачи:Имеется два контроллера HI, назовем их Контроллер А и Контроллер B.
Сигнал пройдет следующий путь:
Эта архитектура позволяет легко масштабировать систему: мы можем добавить еще один выключатель на Контроллере А или даже на новом Контроллере С, который будет управлять тем же светильником, просто публикуя сообщения в тот же топик. Аналогично, мы можем подписать на этот топик еще один светильник, и он будет работать синхронно с первым.
---
Шаг 1: Настройка 'Master' контроллера (А) на публикацию состояний
Начнем с настройки контроллера, к которому подключен источник команды — наш выключатель. Задача этого контроллера — детектировать событие (нажатие кнопки), преобразовать его в стандартизированное сообщение и отправить в общую информационную шину (MQTT).
Создание flow в Node-RED на Контроллере A
Предположим, что наш выключатель подключен к универсальному входу №5 (UI-05).
Схема потока будет выглядеть так:
[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`:Здесь мы настраиваем параметры подключения к брокеру и целевой топик.
- Server: Выбираем или создаем конфигурацию MQTT-брокера, указывая его IP-адрес (например, `192.168.1.10`) и порт (`1883`).
- Topic: Указываем топик для публикации. Следуя иерархической структуре, которую мы проектировали ранее, выберем, например, `hi/floor1/room101/light/01/set`.
* `floor1/room101`: Местоположение.
* `light/01`: Тип и номер устройства.
* `set`: Намерение (установить новое состояние).
- QoS: `1 - At least once`.
- Retain: `true`.
После развертывания этого flow, каждое нажатие на `Inject` будет отправлять структурированное JSON-сообщение в указанный топик на MQTT-брокере.
---
Шаг 2: Настройка 'Slave' контроллера (B) на прием команд
Теперь перейдем к Контроллеру B. Его задача — слушать MQTT-брокер, ловить нужные сообщения и исполнять содержащиеся в них команды.
> ⚠️ Внимание: Убедитесь, что оба контроллера имеют сетевой доступ к MQTT-брокеру. Проверьте IP-адреса, маски подсети и настройки файрволов как на контроллерах, так и на сервере с брокером. Самая частая проблема на этом этапе — банальное отсутствие сетевого подключения между компонентами.
Предположим, светильник подключен к релейному выходу №1 (RL-01) Контроллера B.
Создание flow в Node-RED на Контроллере B
Схема потока будет выглядеть так:
[MQTT In: hi/floor1/room101/light/01/set] --> [Function: Разобрать команду] --> [rpi gpio out: RL-01] --> [Debug: Исполнено]
Код и конфигурация
Нода `MQTT In`:- Server: Выбираем ту же конфигурацию MQTT-брокера, что и на Контроллере А.
- Topic: Указываем `hi/floor1/room101/light/01/set` — в точности тот же топик.
- QoS: `1 - At least once`.
- Output: `a parsed JSON object`. Эта опция очень удобна. Если Node-RED определяет, что payload является валидной JSON-строкой, он автоматически распарсит ее в объект. Это избавляет нас от необходимости делать `JSON.parse()` вручную.
Даже если мы выбрали автоматический парсинг, `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`:- Pin: Выбираем пин, соответствующий релейному выходу RL-01.
- Type: `Digital output`.
- Initialise pin state?: Можно установить начальное состояние пина при запуске.
Теперь, если все настроено правильно, при отправке команды с Контроллера А, светильник, подключенный к Контроллеру B, будет мгновенно реагировать. Мы успешно связали два физических устройства через сеть.
---
Обеспечение надежности: LWT и топики состояния
Что произойдет, если Контроллер А внезапно выключится (сбой питания, обрыв сети)? Контроллер B не узнает об этом и будет считать, что последнее полученное состояние актуально. Для решения этой проблемы в MQTT существует мощный механизм — Last Will and Testament (LWT) или "Последняя воля и завещание".
> 🔗 Связанный материал: Мы подробно рассматривали механизм LWT в уроке COURSE-06-M08-L01. Рекомендуем освежить знания об этой критически важной функции.
Концепция LWT:При подключении клиента (нашего Контроллера А) к брокеру, он может передать "завещание":
- Will Topic: Топик, в который нужно будет опубликовать сообщение.
- Will Message: Сообщение, которое нужно будет опубликовать.
- Will QoS/Retain: Параметры для этого сообщения.
Если брокер обнаружит, что клиент аварийно отключился (не прислав команду `DISCONNECT`), он от имени этого клиента опубликует его "завещание" в указанный топик.
Практическая настройка
Для мониторинга "здоровья" устройств принято выделять отдельную ветку топиков, например `.../status`.
На Контроллере А в ноде `MQTT Out` (или `MQTT In`) переходим на вкладку `Birth` и `Will`:* Topic: `hi/floor1/controllerA/status`
* Payload: `online`
* QoS: `1`
* Retain: `true`
Это сообщение будет отправлено каждый раз, когда контроллер успешно подключается к брокеру.
* Topic: `hi/floor1/controllerA/status`
* Payload: `offline`
* QoS: `1`
* Retain: `true`
Это сообщение брокер опубликует, если Контроллер А "умрет".
Реализация логики на Контроллере B
Теперь Контроллер B должен реагировать на изменения статуса Контроллера А. Для этого создадим на нем отдельный небольшой flow:
* Если пришло `offline`, мы можем:
* Отправить уведомление администратору (через Telegram, Email или другой MQTT-топик).
* Перевести управляемое оборудование в безопасное состояние (например, выключить свет).
* Записать событие в базу данных MySQL для последующего анализа.
* Если пришло `online`, можно сбросить флаг тревоги.
Пример потока на Контроллере B: +--> [Function: Формировать алерт] --> [Telegram Sender]
|
[MQTT In: .../status] --> [Switch: payload] --+
|
+--> [Function: Сбросить алерт] --> [Debug: "Контроллер А в сети"]
Использование LWT и статусных топиков превращает нашу простую связку в элемент надежной, распределенной системы, способной самостоятельно диагностировать проблемы.
---
Отладка и мониторинг межконтроллерного взаимодействия
Даже при тщательной настройке что-то может пойти не так. В распределенных системах отладка требует комплексного подхода.
- Использование ноды `Debug`: Это первый и самый главный инструмент. Размещайте ноды `Debug` после каждого ключевого узла на обоих контроллерах:
* На Контроллере B: после `MQTT In`, после `Function`, после `rpi gpio out`. Это покажет, что именно вы получаете и как преобразуете.
- Применение внешних MQTT-клиентов: Часто проблема кроется не в контроллерах, а в самом брокере или в структуре топиков. Инструменты вроде MQTT Explorer бесценны. Это десктопное приложение, которое подключается к вашему брокеру и в реальном времени показывает все топики и сообщения в них в виде удобного дерева. С его помощью можно:
* Проверить, корректен ли топик и не допустили ли вы опечатку.
* Убедиться, что payload имеет верный формат (валидный JSON).
* Проверить, корректно ли выставляются флаги QoS и Retain.
* Вручную опубликовать сообщение в топик, чтобы проверить реакцию Контроллера B, не трогая Контроллер А.
- Анализ логов брокера: Если клиенты не могут подключиться, проблема может быть в аутентификации или сетевых настройках. Логи самого MQTT-брокера (например, Mosquitto) содержат детальную информацию обо всех попытках подключения, ошибках аутентификации и срабатываниях LWT. Они являются последним рубежом диагностики.
| Ошибка | Возможная причина | Метод диагностики |
| ------------------------------------ | --------------------------------------------------------- | ------------------------------------------------------------ |
| Контроллер 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 для решения простой, но показательной задачи.
Ключевые этапы, которые мы прошли:Этот урок закрепляет фундаментальную важность двух принципов:
- Единая структура топиков: Иерархическая и осмысленная структура топиков — это скелет вашей распределенной системы. Она делает систему понятной и масштабируемой.
- Стандартизированные форматы данных (контракты сообщений): Отправка структурированных JSON-сообщений вместо простых значений позволяет передавать богатый контекст и делает систему гибкой и расширяемой.
Что дальше?
Освоенный сценарий — это базовый строительный блок. На его основе можно реализовывать гораздо более сложные взаимодействия:
- Двусторонняя связь: Дополните flow на Контроллере B так, чтобы после исполнения команды он публиковал фактическое состояние реле в "ответный" топик, например, `hi/floor1/room101/light/01/state`. Контроллер А (и любая панель визуализации) сможет подписаться на этот топик и всегда знать реальное состояние нагрузки, а не только последнюю отправленную команду.
- Управление группой устройств: Что если один выключатель должен управлять светом в трех разных комнатах, подключенных к трем разным контроллерам? Просто подпишите все три контроллера на один и тот же `set` топик.
- Создание "виртуальных" устройств: Представьте сценарий "Я ухожу из дома". Кнопка у выхода на Контроллере А публикует сообщение в топик `hi/events/leave_home`. Контроллеры B, C, D подписаны на этот топик и, получив событие, выключают весь свет и розетки в своих зонах ответственности. Таким образом, одна кнопка запускает сложную логику, распределенную по всему объекту.
В следующем уроке мы углубимся в работу с HTTP API, что откроет еще один мощный канал для интеграции нашей системы с веб-сервисами и сторонним оборудованием.