От сигнала к событию: контракт сообщения
id: COURSE-01-M02-L02
title: "От сигнала к событию: контракт сообщения"
level: foundation
course: COURSE-01
module: COURSE-01-M02
tags: [mqtt, node-red, json, data-model, events]
prerequisites:
- COURSE-01-M02-L01
version: 1.0.0
status: published
learning_outcomes:
- "Объяснять разницу между 'сигналом' и 'событием' в контексте умного дома."
- "Проектировать базовый 'контракт сообщения' для передачи данных с устройств."
- "Использовать ноду 'function' в Node-RED для преобразования сырых данных в структурированный объект события."
- "Понимать важность стандартизации формата сообщений для надежности системы."
---
# Урок COURSE-01-M02-L02: От сигнала к событию: контракт сообщения
Введение: от сигнала к событию
> 🔗 Связанный материал: Для полного понимания этой секции необходимо изучить материал урока COURSE-01-M02-L01 "Что такое сигнал: аналоговый vs дискретный".
В предыдущем уроке мы установили, что сигнал — это сырое, необработанное значение, поступающее от аппаратного входа контроллера. Например, значение `1` от датчика движения (геркона), `0` от датчика протечки, или `23.5` от датчика температуры.
Сигналы являются фундаментом автоматизации, но сами по себе они несут ограниченную информацию. Рассмотрим простой сигнал `1` от дискретного входа. Что он означает?
- Дверь открылась?
- Зафиксировано движение?
- Нажата кнопка вызова?
Без дополнительной информации (контекста) система не может принять верное решение. Сигнал отвечает на вопрос «Что?», но оставляет без ответа критически важные вопросы: «Где?», «Когда?» и «Откуда?».
В этом заключается ключевое различие между сигналом и событием.
📋 Ключевые понятия:
- Сигнал (Signal): Необработанное значение от датчика или входа (`0`, `1`, `22.5`, `450`).
- Событие (Event): Сигнал, обогащенный контекстом. Событие — это информационное сообщение, которое несет в себе не только значение, но и метаданные: время возникновения, источник, местоположение и другие атрибуты.
- Контракт сообщения (Message Contract): Формальное, стандартизированное описание структуры события. Это соглашение между всеми компонентами системы о том, в каком формате они обмениваются данными.
Использование неструктурированных сигналов напрямую в сценариях автоматизации приводит к созданию хрупких и трудноподдерживаемых систем. Каждый раз, когда вы захотите использовать данные от датчика, вам придется «вспоминать», что означает `1` для этого конкретного устройства, в каком топике оно публикуется и как его обработать.
Контракт сообщения решает эту проблему, вводя единый стандарт. Вместо того чтобы каждый раз интерпретировать сырой сигнал, мы преобразуем его в структурированное событие один раз — на входе в систему. Все последующие сценарии работают уже с предсказуемым, понятным и самодостаточным объектом данных.---
Анатомия события в MQTT: топик и полезная нагрузка (payload)
Как мы обсуждали ранее, протокол MQTT является транспортным уровнем для обмена сообщениями в нашей экосистеме. Каждое сообщение в MQTT состоит из двух основных частей: топика (topic) и полезной нагрузки (payload). При создании системы событий мы используем обе эти части для передачи контекста.
1. Топик (Topic) — Адрес СобытияТопик — это иерархическая строка, которая действует как адрес или канал для сообщения. Правильное проектирование иерархии топиков — это первый и важнейший шаг к созданию читаемой и масштабируемой системы. Мы придерживаемся следующей структуры:
`/префикс/локация/устройство/параметр`
- Префикс: Определяет тип данных или домен. Например, `/devices` для сырых данных от драйверов оборудования и `/events` для очищенных, стандартизированных событий. Это позволяет легко отделить «черновые» данные от «чистовых».
- Локация: Описывает физическое расположение устройства. Может быть многоуровневой, например: `office/floor1/room101` или `cottage/garage`.
- Устройство: Уникальный идентификатор или логическое имя устройства, например, `motion_sensor_01` или `climate_control`.
- Параметр: Конкретная характеристика, о которой сообщает устройство: `temperature`, `motion`, `state`.
- Сырой сигнал: Драйвер Modbus для датчика температуры в переговорной комнате №101 публикует значение `22.3` в топик: `/devices/wb-ms_v2_28/temperature`. Этот топик описывает физическое устройство (`wb-ms_v2` с адресом `28`) и его параметр.
- Структурированное событие: После обработки и обогащения контекстом, наша система публикует новое сообщение в топик: `/events/office/floor1/room101/climate/temperature`. Этот топик описывает логическую суть события, а не конкретную железку. Если вы завтра замените датчик на другую модель, топик события останется прежним, и ни один сценарий не сломается.
> 💡 Подсказка: Проектируйте иерархию топиков так, чтобы любой инженер, даже не знакомый с проектом, мог по названию топика понять, где находится устройство и за что оно отвечает. Это значительно упрощает отладку и поддержку.
2. Полезная нагрузка (Payload) — Содержание СобытияЕсли топик — это адрес на конверте, то полезная нагрузка — это само письмо. В `payload` мы помещаем детальную информацию о событии в стандартизированном формате. Для сырых сигналов `payload` часто содержит одно-единственное значение (`22.3`, `1`, `true`), но для структурированных событий мы всегда используем формат JSON (JavaScript Object Notation).
В Node-RED эти два компонента представлены в объекте `msg`:
- `msg.topic` - содержит строку топика.
- `msg.payload` - содержит полезную нагрузку.
Связь между ними позволяет создавать мощную логику. Мы можем использовать `msg.topic` для маршрутизации, а `msg.payload` — для выполнения условных действий.
---
Контракт сообщения: стандартная структура msg.payload
Чтобы система была предсказуемой, мы вводим строгий контракт сообщения для всех событий, циркулирующих в домене `/events`. Все полезные нагрузки в этом домене должны быть в формате JSON и соответствовать следующей структуре.
Формат Полезной Нагрузки (Payload)Мы используем JSON-объект со следующими полями:
| Поле | Тип | Описание | Обязательность |
|------|------|------------|----------------|
| `ts` | String | Временная метка события в формате ISO 8601 (например, `2023-10-27T12:35:01.123Z`). Используем UTC (Z) для однозначности. | Обязательное |
| `val`| any | Непосредственно значение сигнала (число, строка, булево). | Обязательное |
| `src`| String | Источник данных. Обычно это топик сырого сигнала (например, `/devices/wb-m1w2_34/temperature`). Критически важно для отладки. | Обязательное |
| `unit`| String | Единица измерения (`°C`, `ppm`, `%`, `V`). | Рекомендуемое |
| `type`| String | Тип данных (`float`, `integer`, `boolean`, `string`). Помогает избежать ошибок при обработке. | Рекомендуемое |
| `meta`| Object | Объект с дополнительными метаданными, которые могут быть полезны для сценариев (например, имя комнаты, ID пользователя, и т.д.). | Опциональное |
Пример 1: Событие от датчика температуры
Сырой сигнал: топик `/devices/wb-msw-v3_25/temperature`, payload `24.8`.
Стандартизированное событие (payload в топике `/events/office/floor1/room101/climate/temperature`):
{
"ts": "2023-10-27T12:40:15.589Z",
"val": 24.8,
"src": "/devices/wb-msw-v3_25/temperature",
"unit": "°C",
"type": "float",
"meta": {
"room_name": "Переговорная 'Озеро'"
}
}
Этот объект самодостаточен. Любой сценарий, получивший его, немедленно знает:
- Когда это произошло (`ts`).
- Что произошло: температура изменилась до `24.8` градусов Цельсия (`val`, `unit`).
- Откуда пришли исходные данные (`src`).
- Где это находится (`meta.room_name` и сам топик).
Пример 2: Событие от датчика движения (геркона на двери)
Сырой сигнал: топик `/devices/wb-mdm3_50/Input 1`, payload `1`.
Стандартизированное событие (payload в топике `/events/office/main_entrance/security/door_opened`):
{
"ts": "2023-10-27T12:42:01.012Z",
"val": true,
"src": "/devices/wb-mdm3_50/Input 1",
"type": "boolean",
"meta": {
"device_type": "геркон"
}
}
Обратите внимание, как мы преобразовали `1` в более понятное логическое `true`. Это упрощает написание сценариев: вместо `if (msg.payload.val == 1)` можно писать `if (msg.payload.val)`.
---
Практика в Node-RED: преобразование сигнала в событие
Теперь создадим поток в Node-RED, который реализует наш контракт. Он будет слушать сырые сигналы от оборудования, преобразовывать их в стандартизированные события и публиковать в "чистый" топик.
Сценарий:Наш контроллер получает данные от датчика температуры, подключенного через модуль расширения. Драйвер `wb-mqtt-serial` публикует температуру в виде строки в топик `/devices/wb-ms_v2_28/temperature`.
Задача:Создать поток, который:
[MQTT In]────────>[Function]────────>[MQTT Out]
│
└─────────────>[Debug: Raw]
[Debug: Event]─────┘ (подключен к выходу Function)
Шаг 1: Настройка узла `MQTT In`
- Сервер: Выберите ваш MQTT-брокер (обычно `localhost:1883`).
- Топик: `/devices/wb-ms_v2_28/temperature`
- QoS: `0`
- Вывод: `строка`
- Имя: `Raw Temp Signal`
Это самый важный узел. Он содержит JavaScript-код, который выполняет преобразование.
- Имя: `Build Event Object`
- Код: Скопируйте следующий код в редактор функции.
// Сохраняем исходный топик для поля 'src'
const sourceTopic = msg.topic;
// Извлекаем сырое значение. MQTT-драйверы часто присылают
// значения в виде строки, поэтому преобразуем его в число.
// parseFloat является безопасным способом преобразования.
const rawValue = parseFloat(msg.payload);
// Проверяем, удалось ли преобразование. Если нет - прерываем поток.
// Это базовая валидация данных.
if (isNaN(rawValue)) {
node.warn("Не удалось преобразовать payload в число: " + msg.payload);
return null; // Останавливаем дальнейшую обработку сообщения
}
// 1. Создаем новый объект msg.payload согласно нашему контракту.
const eventPayload = {
// 'ts': Временная метка в формате ISO 8601 (UTC)
ts: new Date().toISOString(),
// 'val': Числовое значение температуры
val: rawValue,
// 'src': Исходный топик для отладки
src: sourceTopic,
// 'unit' и 'type': Добавляем важные метаданные
unit: "°C",
type: "float"
};
// 2. Заменяем старый msg.payload на наш новый объект события.
msg.payload = eventPayload;
// 3. Устанавливаем новый топик для публикации события.
msg.topic = "/events/lab/room1/climate/temperature";
// Возвращаем измененный объект msg для передачи следующему узлу.
return msg;
> ⚠️ Внимание: Всегда проверяйте и приводите типы данных. Значение, пришедшее как строка (`'25.5'`), должно быть преобразовано в число (`25.5`) с помощью функций `parseFloat()` или `parseInt()`. Иначе логика сценариев, ожидающая числовые операции (например, `> 25`), может работать некорректно или вызывать ошибки.
Шаг 3: Настройка узла `MQTT Out`- Сервер: Выберите ваш MQTT-брокер.
- Топик: Оставьте пустым. Узел автоматически использует `msg.topic`, который мы задали в узле `Function`.
- QoS: Оставьте по умолчанию.
- Retain: Оставьте по умолчанию (`false`).
- Имя: `Publish Clean Event`
Разверните поток (Deploy). Теперь, когда в топик `/devices/wb-ms_v2_28/temperature` придет сообщение (например, `24.5`), вы увидите в панели отладки два вывода:
- От "Raw Temp Signal": `msg.payload` будет строкой `"24.5"`.
- От "Build Event Object": `msg.payload` будет полноценным JSON-объектом, соответствующим нашему контракту.
---
Заключение: ценность контракта и следующие шаги
В этом уроке мы совершили важный концептуальный переход от простого сигнала к информационно насыщенному событию. Мы определили, что событие — это сигнал, дополненный контекстом (временем, источником, местом), и формализовали его структуру с помощью контракта сообщения.
Ключевые выводы:
- Сигнал ограничен, событие — информативно.
- Контракт сообщения на базе JSON — это фундамент для построения надежной, масштабируемой и легко поддерживаемой системы автоматизации.
- Разделение топиков на `/devices` (сырые данные) и `/events` (очищенные данные) позволяет изолировать логику обработки от аппаратных драйверов.
- Преобразование данных в единый формат происходит один раз на входе в систему, что кардинально упрощает все последующие сценарии.
Инвестиции времени в проектирование и внедрение контрактов сообщений окупаются многократно на этапе эксплуатации и расширения системы. Вы получаете предсказуемую среду, где все компоненты "говорят" на одном языке.
Что дальше?
Теперь, когда у нас есть поток стандартизированных, понятных событий, мы готовы использовать их для создания реальной автоматизации.
🔗 Следующий урок: COURSE-01-M03-L01 "Базовые сценарии: реакция на события", где мы научимся создавать логику, которая реагирует на наши структурированные события для управления освещением, климатом и другими системами.