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

MQTT при этом занимает особое место. Среди всех протоколов, которые Node-RED умеет обслуживать, MQTT остаётся главным рабочим лошадью IoT: он появился ещё в 1999 году как протокол телеметрии нефтяных трубопроводов, поднимается за секунды, работает поверх TCP с минимальными накладными расходами и не требует от устройства ничего, кроме способности установить соединение и подписаться на топик. По данным опроса Node-RED-сообщества 2023 года, MQTT и HTTP, два наиболее часто используемых протокола в реальных проектах, причём MQTT лидирует именно там, где устройства работают в условиях нестабильной связи и ограниченного питания.

Как mqtt-in и mqtt-out превращаются в основу потока данных

Node-RED поставляется с MQTT-узлами из коробки: никаких дополнительных пакетов устанавливать не нужно. В палитре узлов они называются mqtt in и mqtt out и находятся в разделе network. Каждый из них использует общий объект конфигурации брокера, который создаётся один раз и переиспользуется всеми узлами в пределах одного проекта. Это удобно: смена адреса брокера занимает тридцать секунд и не требует обходить каждый узел вручную.

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

Host:     ${MQTT_BROKER_HOST}
Port:     ${MQTT_BROKER_PORT}
Username: ${MQTT_USER}
Password: ${MQTT_PASSWORD}

Эти переменные задаются в settings.js или через переменные окружения процесса. Поток, экспортированный как JSON и выложенный в репозиторий, не будет содержать реальных учётных данных, и это правило, которое стоит соблюдать с первого дня, а не после первого инцидента.

Для production-окружения включение TLS обязательно. Открытый MQTT на порту 1883 отправляет учётные данные и payload в открытом виде. Порт 8883 с TLS-сертификатом закрывает этот вопрос:

{
  "broker": "mqtt.example.com",
  "port": 8883,
  "tls": true,
  "usetls": true,
  "verifyservercert": true
}

Узел mqtt in подписывается на топик и передаёт каждое пришедшее сообщение дальше по потоку в виде объекта msg. Структура простая: msg.topic содержит имя топика, msg.payload содержит тело сообщения. Если устройство публикует JSON, payload придёт строкой, и это первая ловушка для новичков: строка "{"temp":23.5}" и объект {temp: 23.5} в JavaScript ведут себя совершенно по-разному. Узел json, подключённый сразу после mqtt in, конвертирует строку в объект автоматически и избавляет от необходимости делать это в каждом function node вручную.

Иерархия топиков в MQTT, не просто соглашение об именовании, это архитектурное решение. Хаотичные плоские топики вроде temp_sensor_1 и humidity_sensor_floor2 превращаются в кошмар при наличии ста устройств. Структура, рекомендованная для industrial IoT:

enterprise/site/area/line/cell/deviceType/metric
factory/moscow/assembly/line1/robot3/temperature/current
factory/moscow/assembly/line1/robot3/temperature/setpoint

Такая иерархия позволяет подписываться на всё с одного уровня с помощью wildcard-символов. Символ + заменяет один уровень, #, всё что угодно до конца:

factory/moscow/assembly/+/robot3/temperature/# , все температуры robot3 на всех линиях
factory/moscow/#                               , абсолютно все данные с московской фабрики

Узел mqtt in с топиком factory/moscow/# подпишется на всё разом. Это удобно для агрегирующих потоков, которые должны видеть весь трафик площадки.

QoS, Retain и Last Will, три механизма, которые обычно игнорируют до первого инцидента

QoS в MQTT, это гарантия доставки, и она имеет три уровня. QoS 0 означает "отправил и забыл": сообщение может потеряться при обрыве соединения, никаких подтверждений нет. QoS 1 гарантирует доставку хотя бы один раз, но допускает дубликаты. QoS 2 гарантирует доставку ровно один раз через четырёхэтапное рукопожатие. Для телеметрии, где потеря одного показания температуры некритична, QoS 0 вполне достаточен и нагружает сеть меньше всего. Для команд управления реле или актуаторами, только QoS 1 или 2.

Флаг retain на узле mqtt out указывает брокеру хранить последнее опубликованное сообщение для топика и отдавать его каждому новому подписчику немедленно при подключении. Без retain только что подключившийся клиент будет ждать следующего цикла публикации устройства, чтобы узнать текущее состояние. С retain он получает последнее известное значение мгновенно. Для топиков состояния (текущая температура, статус устройства) retain почти всегда нужен. Для событийных топиков (кнопка нажата, тревога сработала) retain может привести к тому, что новый подписчик получит устаревшее событие и ошибочно среагирует на него.

Last Will Testament (LWT), механизм, который брокер активирует автоматически при аварийном отключении клиента. Node-RED настраивает LWT через объект конфигурации брокера: задаётся топик, payload и QoS. Типичная практика: устройство при старте публикует в топик devices/robot3/status значение online, а LWT настроен на тот же топик со значением offline. Любой подписчик, следящий за статусами устройств, немедленно узнает об обрыве связи, не дожидаясь таймаута или следующего цикла опроса.

Function nodes, здесь визуальное программирование переходит в настоящий JavaScript

Function node, это точка, в которой Node-RED перестаёт быть конструктором из кубиков и становится полноценной средой выполнения JavaScript. Любая логика, которую нельзя реализовать встроенными узлами, идёт сюда. Function node получает объект msg, позволяет делать с ним что угодно и возвращает msg или массив msg для дальнейшей маршрутизации.

Базовая структура function node, обрабатывающего данные с IoT-сенсора:

// Входящий payload: {"temp": 23.5, "humidity": 67, "device": "sensor_01"}
const data = msg.payload;

// Валидация: отбрасываем невалидные показания
if (typeof data.temp !== 'number' || data.temp < -50 || data.temp > 100) {
    node.warn("Невалидное значение температуры: " + data.temp);
    return null; // null останавливает поток
}

// Обогащение данных
msg.payload = {
    ...data,
    timestamp: Date.now(),
    temp_f: (data.temp * 9/5) + 32,  // конвертация в Фаренгейт
    status: data.temp > 80 ? "ALARM" : data.temp > 60 ? "WARNING" : "OK"
};

// Метаданные потока для последующих узлов
msg.topic = `processed/${data.device}/temperature`;

return msg;

Возврат null вместо msg, один из важнейших паттернов. Он останавливает распространение сообщения по потоку без ошибки. Это фильтрация: устройства часто публикуют данные чаще, чем нужно дашборду или базе данных, и function node позволяет пропускать дальше только то, что реально изменилось.

Function node поддерживает несколько выходов, и это открывает маршрутизацию без дополнительных switch-узлов:

// Настроить узел на 3 выхода в UI
const temp = msg.payload.temp;

if (temp > 80) {
    return [null, null, msg]; // только в третий выход, аварийный
} else if (temp > 60) {
    return [null, msg, null]; // только во второй выход, предупреждение
} else {
    return [msg, null, null]; // только в первый выход, норма
}

Контекст, механизм хранения состояния между вызовами function node. Node-RED предоставляет три уровня: context (локальный для узла), flow (общий для всех узлов в потоке) и global (общий для всего приложения). Типичное использование, накопление статистики или дедупликация сообщений:

// Пропускаем сообщение, только если значение изменилось более чем на 0.5
const lastTemp = context.get('lastTemp') || 0;
const currentTemp = msg.payload.temp;

if (Math.abs(currentTemp - lastTemp) < 0.5) {
    return null; // изменение несущественное, не передаём дальше
}

context.set('lastTemp', currentTemp);
return msg;

Такой паттерн "report by exception" кардинально снижает нагрузку на брокер и базу данных в системах с сотнями датчиков, данные которых меняются плавно.

Dashboard UI превращает поток данных в живой интерфейс

Node-RED Dashboard превращает потоки в веб-интерфейс, доступный по адресу http://host:1880/ui. Оригинальный пакет node-red-dashboard в июне 2024 года объявлен устаревшим: он основан на Angular v1, который больше не поддерживается. Актуальная замена, @flowfuse/node-red-dashboard (Dashboard 2.0), построенный на Vue.js и поддерживающий современные браузеры без деградации безопасности.

Установка через Palette Manager или командой:

cd ~/.node-red
npm install @flowfuse/node-red-dashboard
# Перезапуск Node-RED для загрузки новых узлов
node-red-restart

Иерархия Dashboard 2.0 строгая и важна для понимания: Page, страница навигации, Group, секция на странице, Widget, отдельный элемент интерфейса (датчик, график, кнопка). Каждый dashboard-узел при конфигурации указывает, к какому Group он принадлежит, и Group автоматически попадает на нужную Page. Смешивать виджеты разных смысловых групп в одном Group, ошибка, которая делает интерфейс нечитаемым.

Полная схема потока от MQTT-сообщения до визуализации выглядит так. MQTT-сенсор публикует JSON в топик factory/line1/robot3/sensors. Узел mqtt in подписан на этот топик. Узел json конвертирует строку в объект. Function node валидирует, обогащает и маршрутизирует данные. Gauge-узел отображает текущую температуру, Chart-узел строит временной ряд, Text-узел показывает статус устройства.

// Function node перед dashboard: разделение данных по каналам
// Узел настроен на 3 выхода: [temperature, humidity, status]

const payload = msg.payload;

const tempMsg = { payload: payload.temperature };
const humMsg  = { payload: payload.humidity };
const statMsg = { payload: payload.device_status === 1 ? "ONLINE" : "OFFLINE" };

return [tempMsg, humMsg, statMsg];

Gauge в Dashboard 2.0 настраивается через параметры min, max и цветовые сегменты. Три сегмента покрывают типичный сценарий промышленного мониторинга:

Зелёный:  0   - 60   (норма)
Жёлтый:   60  - 80   (предупреждение)
Красный:  80  - 100  (авария)

Chart-узел принимает либо одиночное значение (msg.payload = 23.5), либо массив данных для первоначального заполнения. Для временных рядов с метками времени структура payload должна быть:

msg.payload = {
    series: ["Температура"],
    data: [[{ x: Date.now(), y: 23.5 }]],
    labels: [""]
};

При таком формате Chart автоматически сдвигает ось времени по мере поступления новых точек, и последние N значений всегда видны на экране.

Типичные ловушки, которые обнаруживаются не сразу

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

Потеря типа при прохождении через узлы, вторая частая проблема. Gauge-узел ожидает msg.payload как число. Если function node вернул {temp: 23.5} вместо 23.5, gauge не обновится и не выдаст ошибки, просто ничего не произойдёт. Привычка добавлять debug-узел после каждого function node на этапе разработки экономит часы отладки.

Глобальный контекст при работе с несколькими потоками, третья ловушка. Если два потока используют одно и то же имя ключа в global.set(), они будут молча перезаписывать данные друг друга. Именовать ключи контекста с префиксом потока, простое соглашение, которое предотвращает весь класс подобных ошибок: global.set('line1_lastTemp', value) вместо global.set('lastTemp', value).

Node-RED при всей своей визуальности устроен так, что сложность не прячется, а отражается в архитектуре потоков. Хаотичный проект выглядит хаотично на холсте, и это, честно говоря, достоинство: проблема видна до того, как она стала инцидентом. Структурированный проект с чёткой иерархией топиков, валидацией в function nodes и разделёнными Dashboard-группами читается как документация к самому себе, и новый инженер разберётся в нём за час, а не за неделю.