ГлавнаяАкадемияNode-RED: установка, flows, msg/JSON, отладка → `msg.topic`: ключ к маршрутизации и фильтрации

`msg.topic`: ключ к маршрутизации и фильтрации

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

Введение в msg.topic: Больше, чем просто свойство

В предыдущих уроках мы установили, что объект `msg` является фундаментальной единицей передачи информации в Node-RED, а `msg.payload` — его основным контейнером данных. Теперь мы углубимся в другое критически важное свойство — `msg.topic`. Если `payload` — это что передается, то `topic` — это о чём эти данные, откуда они пришли или куда они направляются.

Для инженера автоматизации `msg.topic` — это прежде всего мощный инструмент для организации и управления потоками данных. Его можно рассматривать как семантический адрес или идентификатор сообщения, который путешествует вместе с данными, но не является их частью.

Самая простая и наглядная аналогия — это электронное письмо:

Вы можете получить десять писем с одинаковым содержимым (например, "Встреча перенесена"), но именно тема ("Проект Альфа", "Отчет за Q3", "Срочно: Техническое обслуживание") позволяет вам мгновенно понять контекст и принять решение, что делать с этим письмом, не вчитываясь в него. Точно так же `msg.topic` позволяет узлам в Node-RED эффективно маршрутизировать и фильтровать сообщения, не анализируя сложную структуру `msg.payload`.

> 📋 Ключевые понятия:

> - `msg.topic`: Строковое свойство объекта `msg`, содержащее метаданные о сообщении: его источник, категорию или назначение.

> - Маршрутизация (Routing): Процесс направления сообщения по разным веткам потока в зависимости от его свойств.

> - Фильтрация (Filtering): Процесс отсеивания или пропуска сообщений, соответствующих определенным критериям.

В рамках этого урока мы увидим, как `msg.topic` становится главным инструментом в следующих узлах:

Правильное использование `msg.topic` отделяет логику управления потоком от логики обработки данных, что делает ваши сценарии более чистыми, масштабируемыми и легкими для отладки — ключевые качества для профессионального инженера-интегратора.

---

Практика: Базовая маршрутизация с помощью узла switch

Узел `switch` — это основной инструмент для реализации условной логики в Node-RED. Хотя он может проверять любое свойство объекта `msg`, его наиболее эффективное и частое применение — это маршрутизация на основе `msg.topic`.

Представим типовую задачу: на контроллер HI поступают данные от двух разных датчиков, подключенных через единый шлюз (например, LoRaWAN или универсальный преобразователь). Шлюз отправляет все сообщения на один и тот же вход в Node-RED, но помечает их разными топиками.

Наша задача — направить каждое сообщение на свою собственную ветку обработки.

> 💡 Подсказка: Используйте `topic` для маршрутизации, когда структура `payload` одинакова, а источник или тип данных разный. Это делает flow более читаемым и масштабируемым по сравнению с проверкой внутренних полей `msg.payload`.

Построение потока маршрутизации

  • Настройте источник данных. Для эмуляции шлюза мы будем использовать два узла `inject`, каждый со своим `topic`.
  • * Inject 1 (Температура):

    * `msg.payload`: `{"value": 23.5, "unit": "°C", "ts": 1678886400000}` (JSON)

    * `msg.topic`: `"sensors/room/temperature"` (String)

    * Inject 2 (Влажность):

    * `msg.payload`: `{"value": 45, "unit": "%", "ts": 1678886500000}` (JSON)

    * `msg.topic`: `"sensors/room/humidity"` (String)

  • Добавьте узел `switch`. Соедините оба узла `inject` с одним входом узла `switch`.
  • Сконфигурируйте `switch`.
  • * Property: Убедитесь, что в верхнем поле выбрано `msg.topic`.

    * Правила маршрутизации: Добавьте два правила:

    1. `==` (равно) `sensors/room/temperature`

    2. `==` (равно) `sensors/room/humidity`

    Узел `switch` теперь будет иметь два выхода. Первый сработает, если `msg.topic` точно совпадет со строкой `"sensors/room/temperature"`, второй — для влажности.

  • Добавьте обработчики. К каждому выходу узла `switch` подключите по узлу `debug`. Это позволит нам увидеть, как сообщения разделяются.
  • Схема потока (ASCII):
    Inject 1 (Temp) --+
    

    |

    +--> [switch on msg.topic] --+-- (выход 1) --> [debug: "Обработка температуры"]

    | |

    Inject 2 (Hum) ---+ +-- (выход 2) --> [debug: "Обработка влажности"]

    При нажатии на первый `inject`, сообщение появится только у первого `debug`. При нажатии на второй — только у второго. Мы успешно разделили поток данных, не заглядывая в `msg.payload`.

    Сравнение с маршрутизацией по `msg.payload`

    А что, если бы мы маршрутизировали по `msg.payload.unit`?

    Это тоже будет работать. Однако такой подход менее надежен и масштабируем:

    Использование `msg.topic` для маршрутизации является одним из ключевых паттернов проектирования надежных потоков в Node-RED.

    ---

    Динамическое управление topic в узле change

    Если `switch` читает `msg.topic` для принятия решений, то узел `change` позволяет его модифицировать или создавать "на лету". Это необходимо, когда сообщения приходят из источников, которые не задают `topic`, или когда нам нужно подготовить сообщение для отправки в систему, требующую строгой структуры топиков, например, MQTT.

    Узел `change` предоставляет несколько мощных правил для работы с `msg.topic`.

    1. Установка статического топика

    Самый простой случай: у нас есть сообщение без `topic`, и ему нужно присвоить фиксированное значение. Например, сигнал от физической кнопки, подключенной к универсальному входу `UI-05` контроллера HI, должен получить топик `"events/button/office_main_light"`.

  • Создайте узел `inject`, который отправляет простой `payload`, например, `true`. Убедитесь, что поле `topic` пустое.
  • Добавьте узел `change` и соедините `inject` с ним.
  • Настройте узел `change`:
  • * Создайте правило: Set `msg.topic` to `events/button/office_main_light` (выберите тип `string`).

    Теперь любое сообщение, проходящее через этот узел `change`, получит указанный `topic`, сохраняя при этом свой оригинальный `payload`.

    Пример `msg` до и после узла `change`:
    // ДО узла 'change'
    

    {

    "payload": true,

    "_msgid": "a1b2c3d4.e5f6g7"

    }

    // ПОСЛЕ узла 'change'

    {

    "payload": true,

    "topic": "events/button/office_main_light",

    "_msgid": "a1b2c3d4.e5f6g7"

    }

    2. Формирование иерархических topic

    Намного более интересный сценарий — это создание `topic` на основе другой информации в сообщении. Это позволяет создавать гибкие и структурированные пространства имен, что особенно важно для MQTT.

    Предположим, узел-драйвер Modbus считывает данные с климатического модуля и формирует `msg` со следующей структурой:

    {
    

    "payload": {

    "value": 24.1

    },

    "device_info": {

    "location": "room101",

    "floor": "floor2",

    "type": "temperature"

    }

    }

    Мы хотим отправить эти данные в MQTT с топиком, имеющим вид `hi_platform/floor2/room101/temperature/value`. Это можно сделать с помощью правила `Set` в узле `change` и языка выражений JSONata.

    Настройка узла `change` для динамического топика:
        "hi_platform/" & device_info.floor & "/" & device_info.location & "/" & device_info.type & "/value"
    

    Это выражение конкатенирует (склеивает) статические строки (`"hi_platform/"`) со значениями из объекта `msg.device_info`. Результатом будет сгенерированный `topic`, идеально подходящий для структурированной MQTT-иерархии.

    > ℹ️ Информация: JSONata — это мощный язык запросов и преобразований для JSON, встроенный в Node-RED. Он позволяет выполнять сложные манипуляции с данными прямо в узлах `change` и `switch` без написания кода в узле `function`.

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

    ---

    msg.topic и протокол MQTT: Основа взаимодействия

    Взаимосвязь `msg.topic` и протокола MQTT настолько тесна, что можно сказать — они созданы друг для друга. В MQTT топик — это не просто свойство, а фундаментальная концепция, адрес, по которому сообщения публикуются (`publish`) и на который клиенты подписываются (`subscribe`). Node-RED элегантно интегрирует эту концепцию через `msg.topic`.

    Узел `mqtt in`: От MQTT к Node-RED

    Когда вы настраиваете узел `mqtt in` для подписки на топик (например, `devices/+/status`), он делает две вещи при получении сообщения:

  • Содержимое (payload) MQTT-сообщения помещается в `msg.payload`.
  • Топик, по которому пришло MQTT-сообщение, помещается в `msg.topic`.
  • Это позволяет не только получать данные, но и сразу знать их источник. Если `mqtt in` подписан на `devices/+/status`, то при получении сообщения по топику `devices/thermostat-01/status`, `msg.topic` в Node-RED будет равен именно `devices/thermostat-01/status`. Это дает нам готовый ключ для маршрутизации с помощью узла `switch`, как мы рассматривали ранее.

    Узел `mqtt out`: От Node-RED к MQTT

    Узел `mqtt out` работает в обратном направлении. При публикации сообщения он ищет топик в двух местах, в следующем порядке приоритета:

  • Поле "Topic" в настройках самого узла `mqtt out`. Если оно заполнено, будет использовано это статическое значение, а `msg.topic` будет проигнорирован.
  • Свойство `msg.topic` входящего сообщения. Если поле "Topic" в узле пусто, узел возьмет значение из `msg.topic`.
  • > ⚠️ Внимание: Если `msg.topic` не задан и поле "Topic" в узле `mqtt-out` пусто, сообщение НЕ будет отправлено. Узел `mqtt-out` просто проигнорирует его, не выдав видимой ошибки (если не использовать узел `Status`). Это частая ошибка начинающих инженеров, приводящая к часам отладки.

    Поэтому перед каждым узлом `mqtt out` (если его собственное поле "Topic" не заполнено) должна стоять логика, гарантирующая наличие `msg.topic`, например, с помощью узла `change`.

    Стратегия именования топиков

    Для надежной и масштабируемой системы автоматизации (офис, гостиница, большой дом) необходима согласованная стратегия именования топиков.

    | Хорошие топики | Плохие топики |

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

    | `hi/building-A/floor-3/room-305/light/main/set` | `light_on` |

    | `hi/building-A/floor-3/room-305/temp/value` | `temp_sensor` |

    | `hi/system/controller-01/status/online` | `status` |

    Принципы хорошей структуры топиков:

    Такая структура позволяет использовать мощные механизмы фильтрации, которые мы рассмотрим далее.

    ---

    Пример: Продвинутая фильтрация с Wildcards (+ и #)

    Одно из главных преимуществ иерархической структуры топиков MQTT — возможность использовать специальные символы подстановки (wildcards) при подписке. Это позволяет одному узлу `mqtt in` получать сообщения из целой группы топиков.

    > 📋 Ключевые понятия:

    > - `+` (Плюс, одноуровневый wildcard): Заменяет ровно один уровень в иерархии топиков. Например, `hi/floor-1/+/light/status` подпишется на `.../room-101/light/status` и `.../corridor/light/status`, но не на `.../room-101/main-light/status` или `.../light/status`.

    > - `#` (Решетка, многоуровневый wildcard): Заменяет ноль или более уровней в иерархии. Должен быть последним символом в топике. Например, `hi/floor-1/#` подпишется на абсолютно все сообщения, относящиеся к первому этажу.

    Практический пример: единый обработчик статуса освещения

    Задача: Создать поток, который логирует в базе данных MySQL любое изменение состояния светильников на всем первом этаже.

    Вместо того чтобы создавать десятки узлов `mqtt in` для каждого светильника (`hi/floor-1/room-101/light/status`, `hi/floor-1/room-102/light/status` и т.д.), мы можем использовать один узел с wildcard.

  • Добавьте узел `mqtt in`.
  • Сконфигурируйте его:
  • * Broker: Выберите ваш MQTT-брокер.

    * Topic: `hi/floor-1/+/light/status`.

    * QoS: `1`.

    Теперь этот узел будет получать все сообщения об изменении статуса света на первом этаже. Важно, что `msg.topic` в Node-RED будет содержать полный топик, по которому пришло сообщение. Это позволяет нам в дальнейшем потоке определить, какой именно светильник изменил состояние.

    Схема потока (ASCII):
    [mqtt in: hi/floor-1/+/light/status] -> [function: "Подготовить данные для SQL"] -> [mysql: "Записать в лог"]
    
    Код для узла `function`:
    // Входящий msg.topic может быть "hi/floor-1/room-101/light/status"
    

    // Входящий msg.payload может быть "ON" или "OFF"

    // Извлекаем название помещения из топика

    const topicParts = msg.topic.split('/'); // ["hi", "floor-1", "room-101", "light", "status"]

    const room = topicParts[2]; // "room-101"

    const state = msg.payload;

    // Формируем SQL-запрос

    msg.query = `INSERT INTO light_log (room, state, timestamp) VALUES (?, ?, NOW())`;

    msg.payload = [room, state]; // Параметры для защиты от SQL-инъекций

    return msg;

    Этот пример демонстрирует всю мощь связки `msg.topic` и wildcards: один компактный поток может обрабатывать события от сотен устройств, оставаясь при этом читаемым и эффективным.

    Имитация Wildcards внутри Node-RED с помощью Regex

    Иногда нужно выполнить подобную сложную фильтрацию не в `mqtt in`, а прямо внутри потока с помощью узла `switch`. Стандартные правила `==`, `contains` тут не помогут. На помощь приходят регулярные выражения (regex).

    Предположим, все сообщения уже в Node-RED, и мы хотим отфильтровать те же `hi/floor-1/+/light/status`.

    В узле `switch` можно настроить правило:

    Это регулярное выражение делает то же самое, что и MQTT-wildcard `+`.

    ---

    Итоги и лучшие практики

    В этом уроке мы глубоко погрузились в `msg.topic` и убедились, что это не просто вспомогательное свойство, а один из краеугольных камней проектирования эффективных и надежных потоков в Node-RED.

    Ключевые выводы:

    Освоив `msg.topic`, вы переходите от простого соединения узлов к осознанному проектированию архитектуры потоков данных. Это навык, который отличает новичка от профессионального инженера автоматизации.

    Что дальше

    В следующем уроке мы расширим наши знания об управлении состоянием и контекстом, рассмотрев узел `trigger`. Он позволяет создавать сложные временные последовательности и управлять потоками, которые должны реагировать не только на приходящие сообщения, но и на отсутствие таковых. Мы научимся строить сторожевые таймеры (watchdogs), детекторы "зависания" датчиков и реализовывать логику, требующую выдержки времени.