Главная → Академия → Основы Интернета вещей и практическое применение → Внедрение ключевых практик для надежных и безопасных IoT-систем

Внедрение ключевых практик для надежных и безопасных 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:
* Аутентификация: Всегда настраивайте на 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 и проверяйте его.
Пример: Безопасная обработка MQTT-команды
// Узел 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 только со стороны контроллера.
  • Стратегия опроса: Не опрашивайте устройства слишком часто. Это создает избыточную нагрузку на шину и сами устройства.
* 💡 Совет: Для некритичных данных (температура в комнате) достаточно интервала в 30-60 секунд. Для критичных (состояние аварии) — 1-2 секунды. Используйте узел `Trigger` для создания паузы между опросами разных устройств на одной шине.

Беспроводные каналы и TCP/IP (MQTT)

  • Качество обслуживания (QoS):
* QoS 0 ("Отправил и забыл"): Для некритичных, часто отправляемых данных (например, температура раз в 5 секунд). Допускаются потери.

* QoS 1 ("Доставлено хотя бы один раз"): Рекомендуемый стандарт для большинства команд и состояний. Гарантирует доставку, но возможны дубликаты.

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

  • "Последняя воля и завещание" (Last Will and Testament - LWT):
* Это мощный механизм MQTT для определения "обрыва связи". При подключении клиент сообщает брокеру LWT-сообщение (например, `{"status": "offline"}`) и топик (например, `devices/controller-01/status`).

* Если клиент отключается некорректно (пропадает питание, обрывается сеть), брокер автоматически публикует это сообщение. Другие системы могут на это среагировать.

  • Резервирование канала: Если объект критически важен, используйте опциональный 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`).

* Масштабируемость: Легко добавлять новые типы датчиков, не переписывая всю логику.

Паттерн "Централизованная обработка ошибок"

Ни одна ошибка не должна остаться незамеченной.

  • Реализация:
1. На каждой вкладке (flow) разместите узел `Catch`, настроенный на перехват ошибок со всех узлов (`all nodes`).

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`.

  • Принцип:
1. Определите все состояния системы (`Off`, `Heating`, `Cooling`, `FanOnly`).

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:"green", shape:"dot", text:"OK: " + temp + " °C"});`

* `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` и записывает его в базу данных.

Регулярное обновление ПО

  • План обновления:
1. Резервное копирование: Перед любыми действиями сделайте полную резервную копию проекта Node-RED (файл `flows.json`) и настроек.

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` для чтения регистра.
  • При успешном чтении:
  • * Данные форматируются в стандартный "Контракт сообщения".

    * Узел `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 сбоев.
    Rubric:
    • 50% - Корректный опрос и форматирование данных.
    • 20% - Корректная обработка ошибок и вывод статуса.
    • 20% - Отправка тревожного сообщения после 3 сбоев.
    • 10% - Использование `flow context` для счетчика сбоев.

    COURSE-16-M06-LAB08: Реализация безопасной точки управления реле через MQTT

    Задача: Создать поток для управления реле №5 контроллера HI через MQTT. Поток должен быть защищен от некорректных команд и логировать все действия. Требования к реализации:
  • Подписаться на MQTT-топик `commands/lab/relay5/set`.
  • Реализовать узел `Function` для валидации входящих сообщений. Ожидаемый контракт: `{"command": "ON" | "OFF", "user_id": "some_user"}`. Любые другие сообщения должны отбрасываться с записью ошибки в лог.
  • При получении валидной команды:
  • * Сформировать сообщение для управления реле №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` для проверки.
    Rubric:
    • 30% - Корректная валидация входящих MQTT-сообщений по контракту.
    • 20% - Управление реле на основе валидных команд.
    • 20% - Логирование всех действий (включения/выключения) в `msg.audit`.
    • 15% - Отправка обратной связи о статусе реле в MQTT.
    • 15% - Обработка и логирование невалидных команд.

    ---

    Тест для самопроверки (COURSE-16-M06-QUIZ)

  • Какую основную проблему решает паттерн "Контракт сообщения"?
  • * a) Уменьшает сетевой трафик.

    * b) Обеспечивает унификацию и предсказуемость структуры данных.

    * c) Шифрует данные.

    * d) Ускоряет выполнение потоков.

  • Какой механизм MQTT используется для определения некорректного отключения клиента?
  • * a) QoS 2.

    * b) Retained Messages.

    * c) Last Will and Testament (LWT).

    * d) ACL.

  • Для чего в первую очередь предназначен узел `Catch` в Node-RED?
  • * a) Для отладки сообщений.

    * b) Для перехвата и обработки ошибок, возникающих в узлах на той же вкладке.

    * c) Для фильтрации сообщений по топику.

    * d) Для запуска потоков по расписанию.

  • Что необходимо сделать на физических концах длинной шины RS-485 для стабильной работы?
  • * a) Поменять местами провода A и B.

    * b) Установить терминирующие резисторы 120 Ом.

    * c) Подключить экран кабеля к питанию.

    * d) Увеличить скорость передачи данных.

  • Какая команда в узле `Function` позволяет динамически изменять текст статуса под узлом?
  • * a) `msg.status = "OK"`

    * b) `return {status: "OK"}`

    * c) `console.log("status: OK")`

    * d) `node.status({fill:"green", text:"OK"})`

  • Для чего предназначен "Audit Log" (журнал аудита)?
  • * a) Только для записи системных ошибок.

    * b) Для временного хранения данных с датчиков.

    * c) Для протоколирования важных бизнес-событий и действий пользователя.

    * d) Для отслеживания нагрузки на CPU контроллера.

  • Какой уровень QoS в MQTT гарантирует доставку сообщения, допуская дубликаты, и является рекомендуемым для большинства команд?
  • * a) QoS 0

    * b) QoS 1

    * c) QoS 2

    * d) QoS 3

  • При настройке `contextStorage` на использование файловой системы, какую проблему это решает?
  • * a) Увеличивает скорость работы контекста.

    * b) Обеспечивает сохранение данных контекста (например, состояний FSM) между перезагрузками контроллера.

    * c) Позволяет делиться контекстом между несколькими контроллерами.

    * d) Снижает использование оперативной памяти.

  • Что является лучшей практикой при обработке данных, пришедших из `http in` узла?
  • * a) Сразу передать их в узел, управляющий оборудованием.

    * b) Сохранить их в глобальную переменную.

    * c) Провести строгую валидацию структуры и содержимого сообщения.

    * d) Преобразовать их в XML.

  • Зачем экран экранированной витой пары (FTP/STP) подключается к GND только с одной стороны (со стороны контроллера)?
  • * 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:
    1. `rpi gpio in` (настроен на UI-16) отлавливает замыкание "сухого контакта".

    2. `function` узел реализует логику "триггера" (вкл/выкл по одному нажатию).

    3. `rpi gpio out` (настроен на RL-05) управляет реле.

    4. `status` узел отображает текущее состояние светильника в интерфейсе.

    Пример 2: Подключение датчика температуры/влажности по RS-485 Modbus

    Схема `WIRING-SENS-004`: подключение комнатного Modbus-сенсора. Это последнее устройство на шине.

    • Легенда:
    * `CTRL:HI-Core` — Контроллер

    * `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:
    1. `modbus-read` узел настраивается на COM-порт контроллера, ID устройства `SENS-TH-01` и адреса регистров для температуры и влажности.

    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.

  • Ошибка: Ни одно устройство на шине RS-485/CAN не отвечает.
  • * Причина: Перепутаны линии A/B (CAN H/L) на одном из устройств или на контроллере. Обрыв шины. Отсутствие питания на устройствах.

    * Решение: Проверьте соответствие клемм A->A и B->B по всей длине шины, используя цветовую кодировку. "Прозвоните" линию на обрыв. Убедитесь, что на все устройства подается питание.

  • Ошибка: Периодические сбои в работе шины RS-485, особенно на длинных линиях.
  • * Причина: Отсутствие терминирующих резисторов (120 Ом) на двух крайних устройствах шины. Это вызывает отражение сигнала и искажение данных.

    * Решение: Установите резисторы 120 Ом между клеммами A и B на первом и последнем устройствах в линии. Многие устройства имеют встроенный переключатель для активации терминатора.

  • Ошибка: Датчик 1-Wire не определяется или периодически "отваливается".
  • * Причина: Превышена максимальная длина шины (зависит от топологии, обычно не более 20-30 метров для "паразитного" питания) или используется некачественный кабель.

    * Решение: Используйте топологию "шина", а не "звезда". Для длинных линий (>15 м) используйте схему с тремя проводами (V+, Data, GND) вместо "паразитного" питания. Используйте кабель типа "витая пара".

    Критерии качества выполнения работ по подключению

    Результат монтажных работ считается качественным, если он соответствует следующим критериям:

  • Соответствие стандарту: Все подключения выполнены в строгом соответствии с правилами маркировки и цветового кодирования, изложенными в данном документе.
  • Безопасность: Все соединения, находящиеся под высоким напряжением, надежно изолированы. Используются защитные крышки для клемм. Выполнено заземление металлических корпусов.
  • Надежность: Все контакты затянуты, используются кабельные наконечники. Кабели зафиксированы стяжками и не испытывают механического натяжения.
  • Обслуживаемость: Маркировка легко читается. Любой инженер, не знакомый с объектом, может быстро идентифицировать любой кабель и устройство по схеме. Оставлен запас длины кабелей для перекоммутации.
  • Документированность: Актуальная и подробная ASCII-схема физических подключений сохранена в составе проектной документации и соответствует реальному положению дел.
  • ---

    Мини-runbook «Что делать, если...»

    | Симптом | Возможная причина | Шаги диагностики