Внедрение ключевых практик для надежных и безопасных IoT-систем
COURSE-16-M06-L04 — Внедрение ключевых практик для надежных и безопасных IoT-систем
Введение
Вы дошли до финального модуля, где теоретические знания превращаются в профессиональные навыки. Этот урок — квинтэссенция инженерной культуры в мире IoT. Мы перейдем от подхода «лишь бы работало» к созданию систем, которые являются надежными, безопасными, масштабируемыми и простыми в обслуживании.
На платформе HI вы обладаете мощным инструментарием, но именно следование лучшим практикам отличает профессиональное внедрение от любительского проекта. Мы разберем четыре столпа качественной IoT-системы и покажем, как реализовать каждый из них с помощью контроллера HI, Node-RED и стандартных протоколов.
Практика 1: Комплексная безопасность на всех уровнях
Безопасность — это не опция, а фундаментальное требование. Уязвимость в одном месте может скомпрометировать всю систему. Мы рассматриваем безопасность на трех уровнях: физическом, сетевом и прикладном.
Физическая безопасность
Задача: Защитить сам контроллер и подключенное к нему оборудование от несанкционированного доступа и внешних воздействий.- Размещение контроллера: Контроллер HI и связанные с ним блоки питания, релейные модули и клеммники должны размещаться в запираемом металлическом или пластиковом щите.
- Контроль доступа к щиту: Используйте датчик открытия двери (геркон), подключенный к универсальному входу контроллера. При несанкционированном открытии щита система может мгновенно отправить тревожное уведомление администратору через MQTT или GSM.
- Защита линий связи: Прокладывайте кабели (особенно RS-485, CAN) в кабель-каналах или гофре, чтобы минимизировать риск случайного или намеренного повреждения.
Сетевая безопасность
Задача: Защитить каналы передачи данных от перехвата и подмены.- Изоляция сети: Сеть автоматизации (в которой находятся контроллеры HI, IP-камеры, Modbus TCP устройства) должна быть физически или логически (VLAN) изолирована от гостевых и офисных сетей.
- Безопасный MQTT:
* Шифрование: Настройте TLS-шифрование для MQTT-соединений (MQTTS). Это защитит трафик от прослушивания.
* Списки контроля доступа (ACL): Настройте на брокере ACL, чтобы каждый клиент (например, контроллер) мог публиковать и подписываться только на разрешенные ему топики. Например, датчик температуры не должен иметь права отправлять команды на включение реле.
- Брандмауэр контроллера: На контроллере HI (Debian) активируйте и настройте брандмауэр `ufw` (Uncomplicated Firewall). Разрешите входящие соединения только на необходимые порты (например, 1883/8883 для MQTT, 22 для SSH) и только с доверенных IP-адресов.
Безопасность на уровне приложения (Node-RED)
Задача: Предотвратить атаки через манипуляцию данными, передаваемыми в систему.- Валидация на входе: Любые данные, поступающие в Node-RED извне (через узлы `mqtt in`, `http in`, `tcp in`), должны проходить строгую проверку.
- Применение "Контракта сообщения": Не принимайте простые значения типа `"ON"` или `25.5`. Всегда ожидайте структурированный JSON и проверяйте его.
// Узел Function после [mqtt in]
// Ожидаемый контракт: { "command": "ON" | "OFF", "source": "user-dashboard-1" }
let payload;
// 1. Проверяем, что payload - это объект
try {
payload = (typeof msg.payload === 'string') ? JSON.parse(msg.payload) : msg.payload;
if (typeof payload !== 'object' || payload === null) {
throw new Error("Payload is not an object");
}
} catch (e) {
node.error("Invalid JSON in MQTT message", msg);
node.status({fill:"red", shape:"dot", text:"Invalid JSON"});
return null; // Прерываем поток
}
// 2. Валидация полей контракта
if ( (payload.command !== "ON" && payload.command !== "OFF") || typeof payload.source !== 'string' ) {
node.error("Message does not match contract", msg);
node.status({fill:"red", shape:"dot", text:"Contract violation"});
return null; // Прерываем поток
}
// 3. Если все в порядке, передаем дальше
node.status({fill:"green", shape:"dot", text:"CMD: " + payload.command});
msg.payload = payload; // Передаем валидированный объект
return msg;
Практика 2: Обеспечение надежности сети и каналов связи
Надежность системы напрямую зависит от стабильности каналов связи с оконечными устройствами.
Проводные шины (RS-485, CAN)
⚠️ Ошибки на физическом уровне — самая частая причина сбоев в промышленных протоколах. Следуйте Стандартам схем подключения (WIRING-...) Академии HI.
- Топология и терминирование: Используйте топологию "шина". На обоих концах физической линии RS-485 или CAN установите согласующие резисторы 120 Ом.
- Качество кабеля: Используйте экранированную витую пару (FTP/STP). Экран подключайте к клемме GND только со стороны контроллера.
- Стратегия опроса: Не опрашивайте устройства слишком часто. Это создает избыточную нагрузку на шину и сами устройства.
Беспроводные каналы и TCP/IP (MQTT)
- Качество обслуживания (QoS):
* QoS 1 ("Доставлено хотя бы один раз"): Рекомендуемый стандарт для большинства команд и состояний. Гарантирует доставку, но возможны дубликаты.
* QoS 2 ("Доставлено ровно один раз"): Для критически важных операций (финансовые транзакции, команды, которые нельзя дублировать). Создает наибольшую нагрузку.
- "Последняя воля и завещание" (Last Will and Testament - LWT):
* Если клиент отключается некорректно (пропадает питание, обрывается сеть), брокер автоматически публикует это сообщение. Другие системы могут на это среагировать.
- Резервирование канала: Если объект критически важен, используйте опциональный GSM-модуль контроллера HI как резервный канал связи. В Node-RED можно реализовать логику, которая при потере Wi-Fi/Ethernet соединения переключается на отправку данных через GSM.
Практика 3: Стандартизация данных и отказоустойчивая логика
Хаос в данных и логике — прямой путь к системе, которую невозможно поддерживать. Здесь мы применяем ключевые Паттерны Node-RED из библиотеки знаний Академии.
Паттерн "Контракт сообщения"
Все данные, циркулирующие в вашей системе, должны иметь единый, предсказуемый формат.
- Стандартный контракт `msg.payload`:
{
"value": 23.5,
"source": "temp-sensor-office-1",
"ts": 1678886400000,
"unit": "°C",
"meta": {
"room": "Office 101",
"modbus_id": 15
}
}
- Преимущества:
* Отладка: Сразу видно, откуда пришли данные (`source`) и когда (`ts`).
* Масштабируемость: Легко добавлять новые типы датчиков, не переписывая всю логику.
Паттерн "Централизованная обработка ошибок"
Ни одна ошибка не должна остаться незамеченной.
- Реализация:
2. Все узлы `Catch` соедините с узлом `Link Out`, который ведет на отдельную вкладку "Error Handling".
3. На этой вкладке поток форматирует сообщение об ошибке, добавляя `msg.error.source.name` (имя узла-источника), и выполняет действия:
* Запись в таблицу `errors` в локальной базе MySQL.
* Отправка критических ошибок в Telegram или на почту администратору.
* Обновление статуса системы.
- ASCII-схема потока обработки ошибок:
// Вкладка "Управление освещением"
[Catch: All Nodes] --> [Function: Add Context] --> [Link Out: To Global Error Handler]
// Вкладка "Error Handling"
[Link In: From All Flows] --> [Function: Format Error Msg] --> [Switch: Critical/Warning] --+--> [MySQL Log]
|
+--> [Telegram Notify]
Паттерн "Конечный автомат" (FSM) для сложной логики
Для управления системами со сложными состояниями (климат-контроль, система полива) избегайте "лапши" из `if-else` в узлах `Function`.
- Принцип:
2. Определите события, вызывающие переходы между состояниями (например, `temperature_changed`, `user_set_mode`).
3. Храните текущее состояние в переменной контекста (`flow.climate_state`).
4. Используйте узел `Switch`, который маршрутизирует входящее событие в зависимости от текущего состояния.
💡 Важно: Для сохранения состояния между перезагрузками контроллера настройте `contextStorage` в файле `settings.js` вашего Node-RED на использование файловой системы. Это гарантирует, что система вернется в то же состояние после сбоя питания.
Практика 4: Мониторинг, журналирование и обновление системы
Система, за которой не наблюдают, считается вышедшей из строя.
Паттерн "Визуальный статус"
Обеспечьте мгновенную диагностику прямо в редакторе Node-RED.
- Реализация: В ключевых узлах (опрос Modbus, обработка MQTT, сложная логика) используйте функцию `node.status()` для вывода текущего состояния.
- Примеры кода в узле `Function`:
* `node.status({fill:"yellow", shape:"ring", text:"Connecting to " + ip});`
* `node.status({fill:"red", shape:"dot", text:"Error: Timeout"});`
- Узел `Status` может отслеживать эти изменения и запускать автоматические действия, например, перезапускать опрос при ошибке.
Журналирование действий (Audit Log)
Помимо ошибок, необходимо протоколировать все важные события и действия пользователя.
- Что логировать: Включение/выключение оборудования, изменение уставок, вход пользователя в систему, срабатывание систем безопасности.
- Как логировать: Создайте в локальной базе MySQL на контроллере таблицу `audit_log` со столбцами: `id`, `timestamp`, `source`, `action`, `details` (JSON).
- Реализация: Создайте переиспользуемый субпоток (subflow) `Log Action`, который принимает `msg.audit` и записывает его в базу данных.
Регулярное обновление ПО
- План обновления:
2. Обновление ОС: Регулярно (раз в квартал) выполняйте обновление пакетов Debian: `sudo apt update && sudo apt upgrade -y`.
3. Обновление Node-RED и палитр: Используйте `Manage Palette` для обновления узлов.
4. Тестирование: После обновления пройдитесь по заранее составленному тест-плану, чтобы убедиться, что вся основная функциональность работает корректно.
---
📋 Сводный чек-лист сдачи объекта заказчику
Этот чек-лист поможет вам убедиться, что система соответствует стандартам качества Академии HI.
* [ ] Контроллер в запираемом щите.
* [ ] Настроен брандмауэр `ufw`.
* [ ] MQTT-брокер использует аутентификацию и ACL.
* [ ] Весь входящий трафик в Node-RED валидируется.
* [ ] Шины RS-485/CAN терминированы.
* [ ] Используется QoS 1 для важных MQTT-сообщений.
* [ ] Настроен LWT для отслеживания статуса контроллера.
* [ ] Все потоки используют стандартизированный "Контракт сообщения".
* [ ] На каждой вкладке есть узел `Catch`, подключенный к центральному обработчику ошибок.
* [ ] Состояние сложных систем управляется через FSM и сохраняется при перезагрузке.
* [ ] Ключевые узлы отображают свой статус через `node.status()`.
* [ ] Ведется журнал аудита действий (Audit Log) в MySQL.
* [ ] Существует план резервного копирования и обновления ПО.
* [ ] Вся документация (включая ASCII-схемы `WIRING-...`) актуальна и сохранена вместе с проектом.
---
Лабораторные работы
COURSE-16-M06-LAB07: Создание отказоустойчивого потока опроса Modbus-устройства
Задача: Настроить поток, который каждые 15 секунд опрашивает датчик температуры по Modbus RTU (используйте симулятор или реальное устройство). Поток должен быть устойчив к сбоям связи. Требования к реализации:* Данные форматируются в стандартный "Контракт сообщения".
* Узел `Modbus-Getter` отображает зеленый статус с текущей температурой (`OK: 24.5°C`).
* Данные отправляются в MQTT-топик `telemetry/lab/temperature`.
* Узел `Modbus-Getter` отображает красный статус (`Error: Timeout`).
* Ошибка перехватывается узлом `Catch`.
* В системный лог (узел `Debug` с выводом в консоль) выводится форматированное сообщение об ошибке, включающее время и имя узла.
* После 3 неудачных попыток подряд в MQTT-топик `alarms/lab/temperature` отправляется сообщение `{"error": "sensor unavailable"}`.
Flow Diagram (ASCII):[Inject: 15s] --> [Modbus-Getter: Temp] --> [Function: Format Temp] --> [MQTT Out: telemetry/lab/temperature]
| |
| +----(error)---->[Catch: Modbus Error] --> [Function: Format Error] --> [Debug: Console]
| |
+------------------------------------------------------------------+--> [Function: Check Failures] --> [Switch: 3 failures?] --> [MQTT Out: alarms/lab/temperature]
Skeleton (Node-RED JSON):
[
{
"id": "lab07-inject",
"type": "inject",
"z": "YOUR_FLOW_ID",
"name": "Poll every 15s",
"props": [
{ "p": "payload" },
{ "p": "topic", "vt": "str" }
],
"repeat": "15",
"crontab": "",
"once": true,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 120,
"y": 100,
"wires": [
["lab07-modbus-getter"]
]
},
{
"id": "lab07-modbus-getter",
"type": "modbus-getter",
"z": "YOUR_FLOW_ID",
"name": "Read Temp Sensor",
"unitid": "1",
"fc": "4",
"address": "0",
"quantity": "1",
"client": "YOUR_MODBUS_CLIENT_ID",
"x": 340,
"y": 100,
"wires": [
["lab07-format-temp"],
["lab07-catch-modbus-error"]
]
},
{
"id": "lab07-format-temp",
"type": "function",
"z": "YOUR_FLOW_ID",
"name": "Format Temp Contract",
"func": "let rawValue = msg.payload.data[0];\nlet temperature = rawValue / 10.0; // Assuming value * 10\n\n// Set status\nnode.status({fill:\"green\", shape:\"dot\", text:\"OK: \" + temperature.toFixed(1) + \"°C\"});\n\n// Format message according to contract\nmsg.payload = {\n value: temperature,\n unit: \"°C\",\n source: \"modbus-temp-sensor-lab07\",\n ts: Date.now()\n};\nmsg.topic = \"telemetry/lab/temperature\";\n\n// Reset error counter on success\nflow.set('modbus_failure_count', 0);\n\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 580,
"y": 100,
"wires": [
["lab07-mqtt-out-telemetry"]
]
},
{
"id": "lab07-mqtt-out-telemetry",
"type": "mqtt out",
"z": "YOUR_FLOW_ID",
"name": "Telemetry MQTT",
"topic": "",
"qos": "1",
"retain": "false",
"broker": "YOUR_MQTT_BROKER_ID",
"x": 820,
"y": 100,
"wires": []
},
{
"id": "lab07-catch-modbus-error",
"type": "catch",
"z": "YOUR_FLOW_ID",
"name": "Catch Modbus Error",
"scope": [
"lab07-modbus-getter"
],
"uncaught": false,
"x": 340,
"y": 200,
"wires": [
["lab07-format-error"]
]
},
{
"id": "lab07-format-error",
"type": "function",
"z": "YOUR_FLOW_ID",
"name": "Format Error Message",
"func": "let errorMsg = msg.error.message || \"Unknown Modbus error\";\nlet nodeName = msg.error.source.name || \"Unknown Node\";\n\nnode.status({fill:\"red\", shape:\"dot\", text:\"Error: \" + errorMsg});\n\n// Log to console\nconsole.error(`[${new Date().toISOString()}] Modbus Error from ${nodeName}: ${errorMsg}`);\n\n// Increment failure count\nlet failureCount = flow.get('modbus_failure_count') || 0;\nfailureCount++;\nflow.set('modbus_failure_count', failureCount);\n\nmsg.payload = {\n error: errorMsg,\n sourceNode: nodeName,\n timestamp: Date.now(),\n failureCount: failureCount\n};\n\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 580,
"y": 200,
"wires": [
["lab07-debug-error", "lab07-check-failures"]
]
},
{
"id": "lab07-debug-error",
"type": "debug",
"z": "YOUR_FLOW_ID",
"name": "Error Log",
"active": true,
"toside": true,
"console": true,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 800,
"y": 200,
"wires": []
},
{
"id": "lab07-check-failures",
"type": "switch",
"z": "YOUR_FLOW_ID",
"name": "3 failures?",
"property": "payload.failureCount",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "3",
"vt": "num"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 800,
"y": 260,
"wires": [
["lab07-mqtt-out-alarm"]
]
},
{
"id": "lab07-mqtt-out-alarm",
"type": "mqtt out",
"z": "YOUR_FLOW_ID",
"name": "Alarm MQTT",
"topic": "alarms/lab/temperature",
"qos": "1",
"retain": "false",
"broker": "YOUR_MQTT_BROKER_ID",
"x": 1000,
"y": 260,
"wires": []
}
]
Checklist:
- [ ] Узел `Modbus-Getter` настроен на опрос Modbus RTU.
- [ ] Узел `Function: Format Temp Contract` корректно преобразует данные и устанавливает статус.
- [ ] Узел `MQTT Out: Telemetry MQTT` отправляет данные по контракту.
- [ ] Узел `Catch: Modbus Error` перехватывает ошибки от `Modbus-Getter`.
- [ ] Узел `Function: Format Error Message` форматирует ошибку и инкрементирует счетчик сбоев в `flow context`.
- [ ] Узел `Debug: Error Log` выводит ошибки в консоль.
- [ ] Узел `Switch: 3 failures?` проверяет количество сбоев.
- [ ] Узел `MQTT Out: Alarm MQTT` отправляет тревожное сообщение после 3 сбоев.
- 50% - Корректный опрос и форматирование данных.
- 20% - Корректная обработка ошибок и вывод статуса.
- 20% - Отправка тревожного сообщения после 3 сбоев.
- 10% - Использование `flow context` для счетчика сбоев.
COURSE-16-M06-LAB08: Реализация безопасной точки управления реле через MQTT
Задача: Создать поток для управления реле №5 контроллера HI через MQTT. Поток должен быть защищен от некорректных команд и логировать все действия. Требования к реализации:* Сформировать сообщение для управления реле №5.
* Записать действие в таблицу `audit_log` в MySQL (можно симулировать узлом `Debug`). Запись должна содержать `timestamp`, `action` (`ON`/`OFF`), `user_id`.
* После успешного выполнения команды отправить сообщение с текущим состоянием (`ON` или `OFF`) в топик `status/lab/relay5`.
Flow Diagram (ASCII):[MQTT In: commands/lab/relay5/set] --> [Function: Validate Command] --> [Switch: ON/OFF] --+--> [Function: Set ON] --> [rpi gpio out: Relay 5] --> [Function: Log & Status ON] --> [MQTT Out: status/lab/relay5]
|
+--> [Function: Set OFF] --> [rpi gpio out: Relay 5] --> [Function: Log & Status OFF] --> [MQTT Out: status/lab/relay5]
|
+----(invalid)---->[Function: Log Invalid Command] --> [Debug: Error Log]
Skeleton (Node-RED JSON):
[
{
"id": "lab08-mqtt-in-command",
"type": "mqtt in",
"z": "YOUR_FLOW_ID",
"name": "Relay 5 Command",
"topic": "commands/lab/relay5/set",
"qos": "1",
"datatype": "json",
"broker": "YOUR_MQTT_BROKER_ID",
"nl": false,
"rap": true,
"rh": 0,
"x": 140,
"y": 100,
"wires": [
["lab08-validate-command"]
]
},
{
"id": "lab08-validate-command",
"type": "function",
"z": "YOUR_FLOW_ID",
"name": "Validate Command",
"func": "let payload = msg.payload;\n\n// 1. Check if payload is an object\nif (typeof payload !== 'object' || payload === null) {\n node.error(\"Payload is not an object\", msg);\n node.status({fill:\"red\", shape:\"dot\", text:\"Invalid JSON\"});\n msg.error_reason = \"Invalid JSON\";\n return [null, msg]; // Send to error output\n}\n\n// 2. Validate contract fields\nif ( (payload.command !== \"ON\" && payload.command !== \"OFF\") || typeof payload.user_id !== 'string' || payload.user_id.length === 0 ) {\n node.error(\"Message does not match contract\", msg);\n node.status({fill:\"red\", shape:\"dot\", text:\"Contract violation\"});\n msg.error_reason = \"Contract violation\";\n return [null, msg]; // Send to error output\n}\n\n// If valid, pass through\nnode.status({fill:\"green\", shape:\"dot\", text:\"CMD: \" + payload.command});\nreturn [msg, null]; // Send to success output\n",
"outputs": 2,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 360,
"y": 100,
"wires": [
["lab08-switch-command"],
["lab08-log-invalid-command"]
]
},
{
"id": "lab08-switch-command",
"type": "switch",
"z": "YOUR_FLOW_ID",
"name": "ON/OFF?",
"property": "payload.command",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "ON",
"vt": "str"
},
{
"t": "eq",
"v": "OFF",
"vt": "str"
}
],
"checkall": "false",
"repair": false,
"outputs": 2,
"x": 560,
"y": 100,
"wires": [
["lab08-set-on"],
["lab08-set-off"]
]
},
{
"id": "lab08-set-on",
"type": "change",
"z": "YOUR_FLOW_ID",
"name": "Set ON",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "1",
"tot": "num"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 720,
"y": 60,
"wires": [
["lab08-rpi-gpio-out", "lab08-log-status-on"]
]
},
{
"id": "lab08-set-off",
"type": "change",
"z": "YOUR_FLOW_ID",
"name": "Set OFF",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "0",
"tot": "num"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 720,
"y": 140,
"wires": [
["lab08-rpi-gpio-out", "lab08-log-status-off"]
]
},
{
"id": "lab08-rpi-gpio-out",
"type": "rpi-gpio out",
"z": "YOUR_FLOW_ID",
"name": "Relay 5",
"pin": "5",
"set": true,
"level": "0",
"freq": "",
"out": "out",
"x": 900,
"y": 100,
"wires": []
},
{
"id": "lab08-log-status-on",
"type": "function",
"z": "YOUR_FLOW_ID",
"name": "Log & Status ON",
"func": "// Log to audit_log (simulated with debug)\nmsg.audit = {\n timestamp: Date.now(),\n source: msg.payload.source,\n action: \"RELAY_ON\",\n details: { relay: 5, user_id: msg.payload.user_id }\n};\nnode.status({fill:\"blue\", shape:\"dot\", text:\"Relay 5: ON\"});\nmsg.payload = \"ON\";\nmsg.topic = \"status/lab/relay5\";\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1100,
"y": 60,
"wires": [
["lab08-mqtt-out-status", "lab08-debug-audit"]
]
},
{
"id": "lab08-log-status-off",
"type": "function",
"z": "YOUR_FLOW_ID",
"name": "Log & Status OFF",
"func": "// Log to audit_log (simulated with debug)\nmsg.audit = {\n timestamp: Date.now(),\n source: msg.payload.source,\n action: \"RELAY_OFF\",\n details: { relay: 5, user_id: msg.payload.user_id }\n};\nnode.status({fill:\"grey\", shape:\"dot\", text:\"Relay 5: OFF\"});\nmsg.payload = \"OFF\";\nmsg.topic = \"status/lab/relay5\";\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1100,
"y": 140,
"wires": [
["lab08-mqtt-out-status", "lab08-debug-audit"]
]
},
{
"id": "lab08-mqtt-out-status",
"type": "mqtt out",
"z": "YOUR_FLOW_ID",
"name": "Status MQTT",
"topic": "",
"qos": "1",
"retain": "true",
"broker": "YOUR_MQTT_BROKER_ID",
"x": 1320,
"y": 100,
"wires": []
},
{
"id": "lab08-log-invalid-command",
"type": "function",
"z": "YOUR_FLOW_ID",
"name": "Log Invalid Command",
"func": "msg.audit = {\n timestamp: Date.now(),\n source: \"mqtt-command-relay5\",\n action: \"INVALID_COMMAND\",\n details: { original_payload: msg.payload, error_reason: msg.error_reason }\n};\nnode.status({fill:\"red\", shape:\"dot\", text:\"Invalid command\"});\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 580,
"y": 200,
"wires": [
["lab08-debug-audit"]
]
},
{
"id": "lab08-debug-audit",
"type": "debug",
"z": "YOUR_FLOW_ID",
"name": "Audit Log (simulated)",
"active": true,
"toside": true,
"console": false,
"tostatus": false,
"complete": "audit",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 1320,
"y": 200,
"wires": []
}
]
Checklist:
- [ ] Узел `MQTT In` подписан на `commands/lab/relay5/set`.
- [ ] Узел `Function: Validate Command` корректно проверяет `command` и `user_id`.
- [ ] Невалидные сообщения отбрасываются и логируются.
- [ ] Узел `Switch: ON/OFF?` маршрутизирует валидные команды.
- [ ] Узлы `Change: Set ON` и `Set OFF` формируют payload для `rpi gpio out`.
- [ ] Узел `rpi gpio out` управляет реле №5.
- [ ] Узлы `Function: Log & Status ON/OFF` формируют `msg.audit` и устанавливают статус.
- [ ] Узел `MQTT Out: Status MQTT` отправляет текущее состояние реле.
- [ ] Узел `Debug: Audit Log (simulated)` выводит `msg.audit` для проверки.
- 30% - Корректная валидация входящих MQTT-сообщений по контракту.
- 20% - Управление реле на основе валидных команд.
- 20% - Логирование всех действий (включения/выключения) в `msg.audit`.
- 15% - Отправка обратной связи о статусе реле в MQTT.
- 15% - Обработка и логирование невалидных команд.
---
Тест для самопроверки (COURSE-16-M06-QUIZ)
* a) Уменьшает сетевой трафик.
* b) Обеспечивает унификацию и предсказуемость структуры данных.
* c) Шифрует данные.
* d) Ускоряет выполнение потоков.
* a) QoS 2.
* b) Retained Messages.
* c) Last Will and Testament (LWT).
* d) ACL.
* a) Для отладки сообщений.
* b) Для перехвата и обработки ошибок, возникающих в узлах на той же вкладке.
* c) Для фильтрации сообщений по топику.
* d) Для запуска потоков по расписанию.
* a) Поменять местами провода A и B.
* b) Установить терминирующие резисторы 120 Ом.
* c) Подключить экран кабеля к питанию.
* d) Увеличить скорость передачи данных.
* a) `msg.status = "OK"`
* b) `return {status: "OK"}`
* c) `console.log("status: OK")`
* d) `node.status({fill:"green", text:"OK"})`
* a) Только для записи системных ошибок.
* b) Для временного хранения данных с датчиков.
* c) Для протоколирования важных бизнес-событий и действий пользователя.
* d) Для отслеживания нагрузки на CPU контроллера.
* a) QoS 0
* b) QoS 1
* c) QoS 2
* d) QoS 3
* a) Увеличивает скорость работы контекста.
* b) Обеспечивает сохранение данных контекста (например, состояний FSM) между перезагрузками контроллера.
* c) Позволяет делиться контекстом между несколькими контроллерами.
* d) Снижает использование оперативной памяти.
* a) Сразу передать их в узел, управляющий оборудованием.
* b) Сохранить их в глобальную переменную.
* c) Провести строгую валидацию структуры и содержимого сообщения.
* d) Преобразовать их в XML.
* a) Для экономии клемм.
* b) Это не имеет значения, можно подключать с обеих сторон.
* c) Чтобы избежать протекания "блуждающих токов" по экрану (земляных петель), которые могут создавать помехи.
* d) Таково требование пожарной безопасности.
---
Мини-runbook «Что делать, если...»
| Симптом | Возможная причина | Шаги диагностики
//========= WIRING-LIGHT-015: Kitchen Backlight Control =========
// AC Power Supply
Щит АВР [CTRL:HI-Core]
~L~ --------- L
~N~ --------- N
~PE~ -------- PE
// Dry Contact Input (Switch)
(SW-01) [CTRL:HI-Core]
COM --------- UI-15 (GND)
NO --------- UI-16
// Relay Output (LED Strip Power)
[PSU:12VDC] [CTRL:HI-Core]
+12V --(+)------------------------------ +V
GND --(-)---+-------------------------- GND
|
L --- ~L~ ---+-- C (RL-05)
\-- NO (RL-05) --- ~L~ --- L (PSU)
N --- ~N~ ----------------------------- N (PSU)
- Логика в Node-RED:
2. `function` узел реализует логику "триггера" (вкл/выкл по одному нажатию).
3. `rpi gpio out` (настроен на RL-05) управляет реле.
4. `status` узел отображает текущее состояние светильника в интерфейсе.
Пример 2: Подключение датчика температуры/влажности по RS-485 Modbus
Схема `WIRING-SENS-004`: подключение комнатного Modbus-сенсора. Это последнее устройство на шине.
- Легенда:
* `SENS-TH-01` — Датчик температуры/влажности WB-MSW3
- ASCII-схема:
//============ WIRING-SENS-004: Living Room Modbus Sensor ==============
// Используется кабель "витая пара" UTP Cat5e с экраном
[CTRL:HI-Core] (SENS:TH-01:WB-MSW3)
(RS485-1)
Клемма Шина Цвет Клемма
+24V --------- (Красный) ---- V+
A ---A------ (Зеленый) ---- A
B ---B------ (Белый) ----- B
GND ---GND---- (Черный) ----- GND
|
(Экран) -------- GND (только со стороны контроллера)
// Терминирование шины: WB-MSW встроенного терминатора не имеет —
// на последнем устройстве линии между клеммами A и B ставится
// внешний резистор 120 Ом (или готовый WB-T120).
- Логика в Node-RED:
2. `function` узел парсит полученные данные, приводит их к нужному формату и формирует `msg.payload`.
3. `mqtt out` узел публикует данные в топики, например, `hi/living_room/temperature` и `hi/living_room/humidity`.
4. `catch` узел ловит ошибки связи по Modbus (timeout, CRC error) и отправляет уведомление администратору.
Типовые ошибки и методы их предотвращения
* Причина: Наводки от силовых линий (230V), проложенных вплотную к сигнальным кабелям. Отсутствие общего провода (GND) между контроллером и источником питания датчика.
* Решение: Всегда физически разделяйте слаботочные и силовые трассы (минимум 10 см) или используйте экранированный кабель. Проверяйте наличие и надежность подключения провода GND.
* Причина: Перепутаны линии A/B (CAN H/L) на одном из устройств или на контроллере. Обрыв шины. Отсутствие питания на устройствах.
* Решение: Проверьте соответствие клемм A->A и B->B по всей длине шины, используя цветовую кодировку. "Прозвоните" линию на обрыв. Убедитесь, что на все устройства подается питание.
* Причина: Отсутствие терминирующих резисторов (120 Ом) на двух крайних устройствах шины. Это вызывает отражение сигнала и искажение данных.
* Решение: Установите резисторы 120 Ом между клеммами A и B на первом и последнем устройствах в линии. Многие устройства имеют встроенный переключатель для активации терминатора.
* Причина: Превышена максимальная длина шины (зависит от топологии, обычно не более 20-30 метров для "паразитного" питания) или используется некачественный кабель.
* Решение: Используйте топологию "шина", а не "звезда". Для длинных линий (>15 м) используйте схему с тремя проводами (V+, Data, GND) вместо "паразитного" питания. Используйте кабель типа "витая пара".
Критерии качества выполнения работ по подключению
Результат монтажных работ считается качественным, если он соответствует следующим критериям:
---
Мини-runbook «Что делать, если...»
| Симптом | Возможная причина | Шаги диагностики