Применение IoT в 'Умном городе': Практические сценарии на платформе HI
COURSE-16-M05-L06 — Применение IoT в 'Умном городе': Практические сценарии на платформе HI
Введение в концепцию 'Умного города' на платформе HI
'Умный город' (Smart City) — это городская среда, в которой для повышения качества жизни граждан, оптимизации использования ресурсов и обеспечения безопасности применяются интегрированные информационно-коммуникационные технологии. В основе этой концепции лежит сбор, обработка и анализ данных от множества устройств, объединенных в единую экосистему.
В контексте платформы HI, 'Умный город' — это не абстрактная идея, а набор конкретных, воспроизводимых инженерных решений. Наш контроллер, работающий на Debian Linux с Node-RED, является идеальным инструментом для реализации таких решений на уровне зданий, кварталов или небольших муниципальных объектов. Он позволяет объединять различные системы — от освещения и климата до безопасности и учета ресурсов — с помощью стандартных промышленных протоколов, таких как Modbus, MQTT и CAN.
Этот урок демонстрирует, как перейти от концепции к практике, реализуя три типовых сценария 'Умного города' с использованием стандартных возможностей контроллера HI.
Сценарий 1: Адаптивное уличное освещение (SCN-LIGHT-031)
Задача: Автоматизировать управление уличным или парковым освещением для снижения энергопотребления и повышения безопасности. Освещение должно включаться при снижении естественной освещенности и может быть принудительно активировано по команде диспетчера. Архитектура решения на платформе HI:- Контроллер: HI-Core, 1 шт. (Linux Debian, Node-RED, 4 ядра / 4 ГБ RAM)
- Исполнительное устройство: Светильник 230В, подключенный к одному из релейных выходов контроллера (например, `RL-01`).
- Датчик: Аналоговый датчик освещенности (фоторезистор), подключенный к универсальному входу (UI) контроллера (например, `UI-01`).
- Протокол управления: MQTT для получения команд от системы верхнего уровня (диспетчерской).
Схема подключения: WIRING-LIGHT-018
📋 Чеклист подключения:
//========= WIRING-LIGHT-018: Adaptive Street Light Control =========
// AC Power Supply to Controller and Light
Щит АВР [CTRL:HI-Core]
~L~ --------- L (CTRL)
~N~ --------- N (CTRL)
~PE~ -------- PE (CTRL)
~L~ --- C (RL-01)
\-- NO (RL-01) --- ~L~ ----------------- L (Lamp)
~N~ ----------------------------------------- N (Lamp)
// Analog Light Sensor
(SENS:Light:ALS-01) [CTRL:HI-Core]
VCC (+5V) --------- +5V (от контроллера)
GND --------- GND
Signal --------- UI-01
Поток Node-RED: FLOW-AUTO-LIGHT-015
Логика потока://================ FLOW-AUTO-LIGHT-015 ==================
// Path 1: Automatic control based on light sensor
[Inject: 1 min] -> [Analog In: UI-01] -> |
|
// Path 2: Manual override from MQTT |
[MQTT In: cmd/light/street-01/set] ----> | -> [Function: Light FSM] -> [Relay Out: RL-01] -> [MQTT Out: state/light/street-01]
| |
// Path 3: Error and Status Handling | v
[Catch: All] ---------------------------> | -> [Function: Format Error] -> [Log to MySQL]
[Status: Relay Out] --------------------> |
Контракты сообщений и реализация
Контракт для MQTT-команд (`cmd/light/street-01/set`):Сообщение должно быть в формате JSON для унификации.
{
"command": "ON", // "ON", "OFF", "AUTO"
"source": "dispatcher-console-01",
"ts": 1678886400000
}
Код для узла `Function: Light FSM`:
// Получаем текущее состояние FSM из контекста потока
// 'file' в contextStorage обеспечит сохранение при перезагрузке
let state = flow.get("light_fsm_state") || "AUTO";
let lightLevel = flow.get("current_light_level") || 1000; // Уровень освещенности по умолчанию
// Обработка входящих сообщений
if (msg.topic && msg.topic.includes("cmd/light")) {
// Пришла команда по MQTT
try {
let cmd_payload = JSON.parse(msg.payload);
if (typeof cmd_payload.command !== 'string') {
throw new Error("Command must be a string.");
}
switch (cmd_payload.command.toUpperCase()) {
case "ON": state = "MANUAL_ON"; break;
case "OFF": state = "MANUAL_OFF"; break;
case "AUTO": state = "AUTO"; break;
default: throw new Error("Unknown command: " + cmd_payload.command);
}
// Обновляем статус узла для визуального контроля
node.status({fill:"blue", shape:"dot", text:`Command: ${cmd_payload.command}`});
} catch (e) {
node.error("Invalid JSON or command in MQTT message: " + e.message, msg);
node.status({fill:"red", shape:"dot", text:"MQTT Cmd Error"});
return null;
}
} else if (msg.payload !== undefined) {
// Пришли данные с датчика
// Предполагаем, что узел входа уже выдает число
let sensorValue = parseFloat(msg.payload);
if (isNaN(sensorValue) || sensorValue < 0 || sensorValue > 65535) { // Примерный диапазон для аналогового датчика
node.error("Invalid light level value from sensor: " + msg.payload, msg);
node.status({fill:"red", shape:"dot", text:"Sensor Error"});
return null;
}
lightLevel = sensorValue;
flow.set("current_light_level", lightLevel);
} else {
// Если сообщение не от MQTT и не от датчика, игнорируем
return null;
}
flow.set("light_fsm_state", state);
// Логика принятия решения
let turnOn = false;
const THRESHOLD = 100; // Порог освещенности в люксах
switch (state) {
case "MANUAL_ON":
turnOn = true;
break;
case "MANUAL_OFF":
turnOn = false;
break;
case "AUTO":
if (lightLevel < THRESHOLD) {
turnOn = true;
}
break;
}
// Формирование команды для реле и статуса
let outputValue = turnOn ? 1 : 0; // Для узла Relay Out (0 или 1)
let outputState = turnOn ? "ON" : "OFF";
// Обновляем статус узла Function
node.status({
fill: "green",
shape: "dot",
text: `State: ${state} | Light: ${lightLevel.toFixed(1)} lux | Output: ${outputState}`
});
// Формируем исходящее сообщение для реле
let msgToRelay = { payload: outputValue };
// Формируем исходящее сообщение для MQTT (обратная связь)
let msgToMqtt = {
topic: "state/light/street-01",
payload: JSON.stringify({
value: outputState,
state: state,
light_level: parseFloat(lightLevel.toFixed(1)), // Округляем для контракта
source: "light-fsm-01",
ts: Date.now()
})
};
return [msgToRelay, msgToMqtt]; // Отправляем на два выхода
💡 Совет: Для узла `Analog In: UI-01` убедитесь, что он настроен на выдачу числового значения. Если он выдает строку, используйте `parseFloat(msg.payload)` для преобразования. Убедитесь, что `contextStorage` в `settings.js` настроен на `file` для сохранения состояния FSM при перезагрузке контроллера.
Сценарий 2: Мониторинг качества воздуха (SCN-ENV-007)
Задача: Организовать сбор данных о концентрации CO2 или мелкодисперсных частиц PM2.5 в оживленном городском районе. Данные должны передаваться в центральную систему мониторинга и сохраняться в локальной базе данных для анализа. Архитектура решения на платформе HI:- Контроллер: HI-Core, 1 шт. (Linux Debian, Node-RED, 4 ядра / 4 ГБ RAM)
- Датчик: Промышленный датчик качества воздуха с интерфейсом RS-485 и протоколом Modbus RTU.
- Протоколы: Modbus RTU для опроса датчика, MQTT для отправки данных, MySQL для локального архивирования.
Схема подключения: WIRING-SENS-011
⚠️ Предупреждение: Шина RS-485 требует соблюдения полярности (A/B) и установки терминирующих резисторов (120 Ом) на крайних устройствах.
//============ WIRING-SENS-011: Air Quality Modbus Sensor ==============
// Используется экранированная витая пара. Экран заземляется только со стороны контроллера.
[CTRL:HI-Core] (SENS:CO2:Sensor-01)
(RS485-1)
Клемма Шина Цвет Клемма
+24V --------- (Красный) ---- V+
A ---A------ (Зеленый) ---- A
B ---B------ (Белый) ----- B
GND ---GND---- (Черный) ----- GND
|
(Экран) -------- GND
Поток Node-RED: FLOW-INTEG-MODBUS-009
Логика потока://================ FLOW-INTEG-MODBUS-009 ==================
[Inject: 5 min] -> [Modbus-Getter] -> [Function: Parse & Format] --+--> [MQTT Out: telemetry/air/co2]
|
+--> [MySQL: insert into sensor_data]
// Error Handling
[Catch: Modbus Nodes] -> [Function: Format Modbus Error] -> [Log to MySQL: audit_log]
Контракты сообщений и реализация
Контракт для телеметрии (`telemetry/air/co2`):{
"value": 450,
"unit": "ppm",
"source": "modbus-sensor-co2-01",
"ts": 1678886400000,
"meta": {
"location": "city-center-square",
"sensor_id": 15
}
}
Код для узла `Function: Parse & Format`:
// Входящее сообщение от Modbus-Getter: msg.payload.data = [450]
// Проверяем наличие данных и их формат
if (!msg.payload || !Array.isArray(msg.payload.data) || msg.payload.data.length === 0) {
node.error("Modbus-Getter returned invalid or empty data.", msg);
node.status({fill:"red", shape:"dot", text:"No Modbus data"});
return null;
}
let rawValue = msg.payload.data[0];
// 1. Валидация
// Предполагаем, что CO2 в ppm обычно находится в диапазоне 300-5000
if (rawValue < 300 || rawValue > 5000) {
node.status({fill:"red", shape:"dot", text:"Invalid value: " + rawValue});
node.error("Invalid CO2 value received: " + rawValue, msg);
// Отправляем ошибку в централизованный логгер
msg.error = {
message: "Invalid CO2 value",
value: rawValue,
source_node: node.name || "Parse & Format"
};
return [null, null, msg]; // Отправляем на третий выход для ошибок
}
// 2. Формирование по контракту для MQTT
let mqttPayload = {
value: rawValue,
unit: "ppm",
source: "modbus-sensor-co2-01",
ts: Date.now(),
meta: {
location: "city-center-square",
sensor_id: 15 // Modbus Slave ID
}
};
// 3. Подготовка SQL-запроса для узла MySQL
// Узел mysql ожидает запрос в msg.topic
let sqlQuery = `INSERT INTO sensor_data (ts, source, value, unit) VALUES (${mqttPayload.ts}, '${mqttPayload.source}', ${mqttPayload.value}, '${mqttPayload.unit}')`;
// Создаем два отдельных сообщения для разных выходов
let msgToMqtt = {
topic: "telemetry/air/co2",
payload: JSON.stringify(mqttPayload)
};
let msgToMysql = {
topic: sqlQuery,
payload: mqttPayload // Можно передать весь объект для логирования, если нужно
};
node.status({fill:"green", shape:"dot", text:`OK: ${rawValue} ppm`});
return [msgToMqtt, msgToMysql, null]; // Отправляем на два выхода, третий для ошибок пустой
💡 Совет: Для узла `MySQL` убедитесь, что он настроен на подключение к локальной базе данных на контроллере HI. Таблица `sensor_data` должна быть предварительно создана со столбцами `ts` (BIGINT), `source` (VARCHAR), `value` (FLOAT), `unit` (VARCHAR). Также создайте таблицу `audit_log` для ошибок.
Сценарий 3: Мониторинг занятости парковочных мест (SCN-TRAFFIC-004)
Задача: Предоставлять информацию о свободных/занятых парковочных местах в режиме реального времени. Архитектура решения на платформе HI:- Контроллер: HI-Core, 1 шт. (Linux Debian, Node-RED, 4 ядра / 4 ГБ RAM)
- Датчик: Ультразвуковой датчик расстояния (например, HC-SR04 или промышленный аналог), подключенный к универсальным входам/выходам.
- Протокол: MQTT для передачи статуса парковочного места.
Схема подключения: WIRING-SENS-012
💡 Совет: Для надежной работы на улице используйте промышленные ультразвуковые датчики с классом защиты IP67.
//============ WIRING-SENS-012: Ultrasonic Parking Sensor ============
(SENS:Ultrasonic:PK-01) [CTRL:HI-Core]
VCC (+5V) --------------- +5V
GND --------------------- GND
Trig -------------------- UI-05 (сконфигурирован как выход)
Echo -------------------- UI-06 (сконфигурирован как вход)
Поток Node-RED: FLOW-SENS-USONIC-002
Логика потока://================ FLOW-SENS-USONIC-002 ==================
[Inject: 10 sec] -> [Function: Trigger Pulse] -> [RPI GPIO Out: UI-05 (Trig)]
|
v
[RPI GPIO In: UI-06 (Echo)] -> [Function: Calculate Distance] -> [Function: Determine Status] -> [MQTT Out: telemetry/parking/spot-01]
|
+--> [Debug]
// Error Handling
[Catch: All] -> [Function: Format Error] -> [Log to MySQL]
Реализация
Контракт для телеметрии (`telemetry/parking/spot-01`):{
"status": "OCCUPIED", // "OCCUPIED" или "FREE"
"distance_cm": 50,
"source": "ultrasonic-pk-01",
"ts": 1678886400000,
"meta": {
"spot_id": "P1-A01",
"threshold_cm": 100
}
}
Код для узла `Function: Trigger Pulse`:
Этот узел генерирует короткий импульс на пине `Trig`.
// Конфигурация пина UI-05 как выхода
// Для HC-SR04 нужен импульс 10 мкс
// Node-RED не позволяет напрямую управлять микросекундами в setTimeout.
// Для точного управления GPIO на микросекундном уровне потребуется
// использовать внешний модуль или C-binding.
// В данном примере используется приближение, которое может работать
// для некоторых датчиков, но не гарантирует точность.
// Для промышленных датчиков часто используется Modbus или другие протоколы,
// где генерация импульса не требуется от контроллера.
// Отправляем 1, затем 0 через минимальную задержку
node.send({ payload: 1, topic: "trig_pulse" }); // Отправляем высокий уровень
setTimeout(() => {
node.send({ payload: 0, topic: "trig_pulse" }); // Отправляем низкий уровень
}, 1); // Минимальная задержка в мс, имитирующая короткий импульс
// Важно: узел RPI GPIO Out должен быть настроен на пин UI-05 и работать в режиме "Digital Output".
// Имя узла RPI GPIO Out должно быть "UI-05 (Trig)"
return null; // Не отправляем msg дальше по основному пути
⚠️ Предупреждение: Узел `RPI GPIO Out` должен быть настроен на пин `UI-05` и работать в режиме "Digital Output". Для точного управления микросекундными импульсами на GPIO в Linux/Node-RED могут потребоваться дополнительные библиотеки или использование ARM32 для критичных сценариев.
Код для узла `Function: Calculate Distance`:Этот узел измеряет длительность импульса на пине `Echo` и преобразует ее в расстояние.
Предполагается, что `RPI GPIO In: UI-06` выдает `msg.payload` как `1` (начало импульса) и `0` (конец импульса).
Для этого потребуется более сложная логика с использованием `flow context` для измерения времени.
// Сохраняем время начала импульса в контексте потока
if (msg.payload === 1) { // Начало импульса Echo
flow.set('echoStartTime', process.hrtime.bigint());
node.status({fill:"yellow", shape:"dot", text:"Echo started"});
return null; // Не отправляем сообщение дальше, ждем конца импульса
} else if (msg.payload === 0) { // Конец импульса Echo
let startTime = flow.get('echoStartTime');
if (startTime) {
let endTime = process.hrtime.bigint();
let duration_ns = endTime - startTime; // Длительность в наносекундах
let duration_us = Number(duration_ns) / 1000; // Длительность в микросекундах
// Скорость звука в воздухе ~343 м/с или 0.0343 см/мкс
// Расстояние = (длительность * скорость звука) / 2 (туда и обратно)
let distance_cm = (duration_us * 0.0343) / 2;
// Валидация расстояния (например, от 2 см до 400 см для HC-SR04)
if (distance_cm < 2 || distance_cm > 400) {
node.status({fill:"red", shape:"dot", text:"Out of range: " + distance_cm.toFixed(1) + " cm"});
node.error("Distance out of valid range: " + distance_cm.toFixed(1) + " cm", msg);
return null;
}
msg.payload = distance_cm; // Передаем расстояние в см
node.status({fill:"green", shape:"dot", text:`Distance: ${distance_cm.toFixed(1)} cm`});
return msg;
} else {
node.warn("Echo end without start time. Possible missed start pulse.", msg);
node.status({fill:"red", shape:"dot", text:"Echo error"});
return null;
}
}
return null;
⚠️ Предупреждение: Узел `RPI GPIO In` должен быть настроен на пин `UI-06` и работать в режиме "Digital Input" с прерываниями по обоим фронтам (rising and falling edge). Это критично для точного измерения длительности импульса.
Код для узла `Function: Determine Status`:Этот узел определяет статус парковочного места на основе расстояния.
const THRESHOLD_CM = 100; // Порог в сантиметрах: если ближе, то занято
let distance_cm = parseFloat(msg.payload);
if (isNaN(distance_cm)) {
node.error("Invalid distance value: " + msg.payload, msg);
node.status({fill:"red", shape:"dot", text:"Invalid input"});
return null;
}
let status = (distance_cm < THRESHOLD_CM) ? "OCCUPIED" : "FREE";
// Формируем исходящее сообщение по контракту
msg.payload = JSON.stringify({
status: status,
distance_cm: parseFloat(distance_cm.toFixed(1)), // Округляем для читаемости
source: "ultrasonic-pk-01",
ts: Date.now(),
meta: {
spot_id: "P1-A01",
threshold_cm: THRESHOLD_CM
}
});
msg.topic = "telemetry/parking/spot-01";
node.status({fill:"green", shape:"dot", text:`Status: ${status} (${distance_cm.toFixed(1)} cm)`});
return msg;
Лабораторные работы
Лабораторная работа 1: COURSE-16-M05-LAB11 — Развертывание адаптивного уличного освещения
Цель: Собрать и настроить поток Node-RED для управления реле на основе показаний симулированного датчика освещенности и MQTT-команд. Оборудование: Контроллер HI-Core, светодиод для индикации, ПК. Задачи:* Пример команды "Включить вручную": `{"command": "ON", "source": "test", "ts": 1678886400000}`
* Пример команды "Выключить вручную": `{"command": "OFF", "source": "test", "ts": 1678886400000}`
* Пример команды "Автоматический режим": `{"command": "AUTO", "source": "test", "ts": 1678886400000}`
- Светодиод включается при `msg.payload < 100` (от симулированного датчика) в режиме `AUTO`.
- Светодиод выключается при `msg.payload > 100` (от симулированного датчика) в режиме `AUTO`.
- Поток корректно обрабатывает JSON-команды `ON`, `OFF`, `AUTO` из MQTT, переключая режимы независимо от показаний датчика.
- Узел `Function` отображает корректный статус, отражающий текущий режим и состояние света.
- В MQTT-топик `state/light/street-01` отправляются корректные JSON-сообщения с текущим статусом, соответствующим контракту.
- Некорректные MQTT-сообщения вызывают ошибку, которая перехватывается узлом `Catch` и логируется.
Лабораторная работа 2: COURSE-16-M05-LAB12 — Настройка симулятора Modbus-сенсора
Цель: Создать поток для опроса Modbus-устройства, обработки и отправки данных. Оборудование: Контроллер HI-Core, ПК. Задачи:- `Modbus-Getter` успешно считывает данные из симулятора.
- Поток корректно обрабатывает валидные и невалидные (вне диапазона) значения, используя `node.error()` для некорректных.
- Исходящее сообщение `msgToMqtt.payload` полностью соответствует заданному JSON-контракту.
- `msgToMysql.topic` содержит корректный SQL-запрос для вставки данных.
- Узел `Function` отображает корректный статус (`OK` или `Invalid value`).
- Ошибки Modbus-связи (например, если выключить `Modbus-Server`) перехватываются узлом `Catch` и логируются в `audit_log`.
---
Тест для самопроверки: COURSE-16-M05-QUIZ
* a) MQTT
* b) TCP/IP
* c) Modbus
* d) DALI
* a) Шифрование данных
* b) Стандартизация формата данных для предсказуемости и совместимости
* c) Увеличение скорости передачи
* d) Сжатие сообщений
* a) `Status`
* b) `Link Out`
* c) `Function`
* d) `Catch`
* a) Усилитель сигнала
* b) Терминирующий резистор 120 Ом
* c) Фильтр питания
* d) Опторазвязку
* a) Для ускорения работы потока
* b) Для обмена данными между разными контроллерами
* c) Для сохранения состояния переменных (например, FSM) между перезагрузками контроллера
* d) Для временного хранения больших файлов
* a) Реле
* b) Датчик освещенности
* c) Узел `Function` с логикой FSM
* d) MQTT-брокер
* a) `value`
* b) `source`
* c) `ts` (timestamp)
* d) `color`
* a) Поток Node-RED аварийно завершит работу.
* b) Узел `Modbus-Getter` сгенерирует ошибку (timeout), которая будет поймана узлом `Catch`.
* c) Система будет использовать последнее успешное значение бесконечно.
* d) Контроллер автоматически перезагрузится.
* a) К клемме `GND` с обеих сторон.
* b) К клемме `GND` только со стороны контроллера.
* c) К отдельной шине заземления `PE`.
* d) Не подключать вообще.
* a) Звезда
* b) Кольцо
* c) Шина (линейная)
* d) Дерево
---
Мини-runbook "Если что-то не работает"
| Симптом | Возможная причина | Решение |
| -------------------------------------------------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Сценарий 1: Реле не включается/выключается. | 1. Ошибка в схеме подключения.
2. Неверный номер реле в узле `Relay Out`.
3. Ошибка в логике узла `Function`.
4. Некорректный формат команды MQTT. | 1. Проверить схему `WIRING-LIGHT-018` и соответствие цветов проводников стандарту Академии. Проверить напряжение на реле.
2. Убедиться, что номер реле в узле Node-RED соответствует физическому подключению на контроллере HI.
3. Подключить узел `Debug` после `Function` и проверить `msg.payload` (должно быть 0 или 1) и `node.status()`. Проверить логику FSM.
4. Проверить JSON-формат команды MQTT, включая `command`, `source`, `ts`. |
| Сценарий 2: Ошибки "Timeout" от `Modbus-Getter`. | 1. Обрыв или неверная полярность шины RS-485.
2. Несовпадение настроек (скорость, ID, четность, стоп-биты).
3. Отсутствие питания на датчике.
4. Отсутствие терминирующих резисторов. | 1. Проверить целостность кабеля и полярность A/B. Попробовать поменять местами A и B на контроллере. Использовать мультиметр для проверки напряжения на шине.
2. Убедиться, что Slave ID, скорость, четность, стоп-биты совпадают на контроллере и датчике. Проверить настройки Modbus-Client в Node-RED.
3. Проверить питание датчика (обычно 24VDC).
4. Установить терминирующие резисторы 120 Ом на крайних устройствах шины RS-485. |
| Сценарий 2: Modbus возвращает неверные данные. | 1. Ошибка "off-by-one" в адресе регистра.
2. Неправильная интерпретация формата данных (порядок байт, тип данных).
3. Неверный код функции (FC). | 1. Внимательно изучить карту регистров датчика. Если в документации указан регистр `40001`, в узле `Modbus-Getter` нужно указать адрес `0`.
2. Изучить документацию на датчик, при необходимости использовать узел `Function` для манипуляции с буфером (`msg.payload.buffer`) и правильной сборки числа (например, для 32-битных float с учетом порядка байт/слов).
3. Убедиться, что выбран правильный FC (например, FC3 для Holding Registers, FC4 для Input Registers). |
| Сценарий 3: Датчик расстояния не работает/выдает 0. | 1. Неверное подключение Trig/Echo.
2. Ошибка в логике измерения времени.
3. Недостаточное питание датчика.
4. Некорректная настройка GPIO пинов. | 1. Проверить схему `WIRING-SENS-012`. Убедиться, что `UI-05` подключен к `Trig`, а `UI-06` к `Echo`.
2. Подключить `Debug` после `Function: Calculate Distance` и проверить `msg.payload`. Убедиться, что `flow.get('echoStartTime')` корректно сохраняется и `process.hrtime.bigint()` работает.
3. Проверить стабильность питания +5V на датчике. Убедиться, что контроллер HI может обеспечить достаточный ток.
4. Убедиться, что `UI-05` настроен как "Digital Output", а `UI-06` как "Digital Input" с прерываниями по обоим фронтам (rising and falling edge) в настройках узла `RPI GPIO In`. |
| Общее: Команды по MQTT не доходят. | 1. Неверный топик.
2. Ошибка в настройках MQTT-брокера.
3. Некорректный формат JSON в сообщении.
4. Проблемы с сетевым подключением контроллера. | 1. Проверить, что топик в `MQTT In` узле и в клиенте (например, MQTT Explorer) совпадают до символа. Использовать wildcard `#` для отладки.
2. Проверить доступность брокера с контроллера (например, командой `ping
3. Валидировать JSON сообщения, убедиться, что он соответствует контракту. Использовать `JSON.parse()` с `try-catch` в узле `Function` для отладки.
4. Проверить сетевые настройки контроллера (IP-адрес, шлюз, DNS). Убедиться, что контроллер имеет доступ к MQTT-брокеру. |