Контракт сообщения: структура JSON для событий от датчиков
Введение: Зачем нужен 'контракт' сообщения?
В любой сложной системе автоматизации, от умного дома до промышленного объекта, десятки и сотни датчиков непрерывно генерируют данные. Датчик движения сообщает о присутствии, датчик температуры — о микроклимате, датчик протечки — об аварии. Каждый из них передает свой физический сигнал, который, как мы обсуждали в предыдущих уроках, преобразуется в логическое событие. Проблема возникает тогда, когда эти события не имеют единого, предсказуемого формата.
Представьте себе сценарий:
- Датчик движения при срабатывании отправляет сообщение с `msg.payload`, равным строке `"MOTION_DETECTED"`.
- Кнопка настенного выключателя при нажатии отправляет `msg.payload`, равный булеву значению `true`.
- Датчик открытия двери отправляет `msg.payload`, равный числу `1`.
Каждое из этих сообщений, по сути, означает "событие произошло". Но чтобы написать единую логику, которая реагирует на любое из них (например, для системы безопасности), инженеру придется создавать сложный узел `switch`, проверяющий три разных типа данных и три разных значения. Теперь умножьте это на 50 датчиков. Система быстро превращается в хаос разрозненных форматов, где каждое новое устройство требует написания уникального "переводчика". Отладка такой системы становится кошмаром, а масштабирование — практически невозможным.
Для решения этой проблемы вводится контракт сообщения (Message Contract).
> 📋 Ключевые понятия:
> Контракт сообщения — это формальное, документированное соглашение о структуре и формате данных, которыми обмениваются все компоненты системы автоматизации. В нашей экосистеме это стандартная структура JSON-объекта, передаваемого внутри `msg`.
По своей сути, контракт сообщения — это внутренний API вашей системы автоматизации. Точно так же, как веб-сервис предоставляет внешний API для взаимодействия с ним, компоненты вашей системы (датчики, логические потоки, исполнительные устройства, панели HMI, базы данных) используют контракт для предсказуемого и стандартизированного общения друг с другом.
Преимущества стандартизации очевидны:
Внедрение и строгое соблюдение контракта сообщений — это не дополнительная работа, а фундаментальная инвестиция в надежность, масштабируемость и обслуживаемость вашей системы автоматизации.
---
Стандартная структура события HI: базовые поля
Каждое событие, циркулирующее внутри контроллера HI, должно быть представлено в виде объекта `msg`, который соответствует стандартному контракту. Контракт определяет как верхнеуровневые свойства самого объекта `msg`, так и вложенную структуру `msg.payload`.
Давайте рассмотрим базовую структуру сообщения, которое генерируется при срабатывании любого датчика.
{
"topic": "events/inputs/digital/ui-01",
"payload": {
"...": "детали события, см. следующий раздел"
},
"_msgid": "a1b2c3d4.e5f6g7",
"timestamp": 1678886400123,
"source_type": "hardware_input",
"source_id": "UI-01"
}
Разберем каждое поле подробно:
`msg.topic` (string)
Это свойство играет ключевую роль в маршрутизации событий. Хотя оно исторически связано с протоколом MQTT, в Node-RED его семантика гораздо шире. `topic` используется для логической фильтрации потоков с помощью узла `Switch`. Рекомендуется использовать иерархическую структуру, похожую на топики MQTT, чтобы описать суть сообщения.
- Структура: `class/category/subcategory/item`
- Пример для датчика движения на входе UI-01: `events/inputs/digital/ui-01`
- Пример для датчика температуры на шине 1-Wire: `events/sensors/1-wire/28-01234567abcd`
Такой подход позволяет одним узлом `Switch` обрабатывать целые классы событий, например, `содержит "events/inputs/"`.
`msg.payload` (object)
Это "полезная нагрузка" сообщения. В отличие от примитивных систем, где `payload` содержит только само значение (`true` или `25.5`), наш контракт требует, чтобы `payload` всегда был JSON-объектом, содержащим детализированную информацию о значении. Эту структуру мы разберем в следующем разделе.
`msg.timestamp` (number)
Это одно из самых критически важных полей. Оно должно содержать временную метку возникновения физического события, а не его обработки.
- Формат: Unix Epoch timestamp в миллисекундах (его генерирует `Date.now()`).
- Назначение: Точная хронология событий для систем журналирования, аналитики и отладки. Позволяет ответить на вопрос "что произошло раньше?" с точностью до миллисекунды, что незаменимо при разборе сложных инцидентов.
`msg.source_type` (string)
Это поле классифицирует источник события на высоком уровне. Оно помогает понять, где в архитектуре системы родилось это событие.
- `hardware_input`: Событие сгенерировано физическим входом контроллера (UI, DI, AI).
- `modbus`: Событие получено от устройства на шине Modbus.
- `1-wire`: Событие получено от датчика на шине 1-Wire.
- `virtual_device`: Событие сгенерировано программной логикой (например, виртуальный выключатель в HMI).
- `scenario`: Событие создано в рамках выполнения сценария автоматизации.
`msg.source_id` (string)
Уникальный идентификатор источника события. Это "паспорт" датчика или компонента, который сгенерировал сообщение. Этот ID должен быть абсолютно уникальным в рамках всего проекта.
- Примеры:
* Для реле №5: `"RELAY-05"`
* Для датчика температуры DS18B20: его уникальный 64-битный адрес `"28-01234567abcd"`
* Для Modbus-счетчика электроэнергии с адресом 15, параметр "Активная мощность": `"modbus-meter-15/ActivePower"`
Сочетание `source_type` и `source_id` дает полное представление об источнике данных, что неоценимо при отладке и автоматическом построении карт системы.
---
Детализация payload: типизация и единицы измерения
Как мы установили, `msg.payload` в нашем контракте — это не просто значение, а структурированный JSON-объект. Такая структура позволяет передавать не только "сырые" данные, но и мета-информацию о них: тип данных и единицы измерения. Это устраняет любую двусмысленность при их дальнейшей обработке.
Стандартная структура `msg.payload` выглядит следующим образом:
{
"value_type": "string|boolean|integer|float",
"value": "...",
"units": "..."
}
Рассмотрим каждое поле.
`payload.value_type` (string)
Это поле явно указывает, какой тип данных содержится в поле `value`. Это позволяет принимающей стороне (например, базе данных или HMI) корректно интерпретировать значение без дополнительных проверок.
- `'boolean'`: Для бинарных состояний (включено/выключено, открыто/закрыто, есть движение/нет движения). Значение будет `true` или `false`.
- `'integer'`: Для целочисленных значений (например, количество импульсов со счетчика).
- `'float'`: Для чисел с плавающей запятой (температура, влажность, напряжение, давление).
- `'string'`: Для текстовых значений (например, состояние ошибки от устройства или серийный номер).
`payload.value` (any)
Собственно, само значение, полученное от датчика. Тип этого поля должен строго соответствовать тому, что указано в `value_type`.
`payload.units` (string, опционально)
Единица измерения физической величины. Это поле обязательно для всех числовых типов (`integer`, `float`), представляющих физические величины.
> 💡 Подсказка: Используйте единицы измерения из Международной системы единиц (СИ) (например, 'V' для вольт, 'A' для ампер, '°C' для градусов Цельсия, 'Pa' для паскалей), чтобы избежать путаницы при дальнейшей обработке данных в HMI или системах аналитики. Для относительных величин используйте `'%'`. Для безразмерных — не указывайте это поле.
Примеры полных сообщений
Давайте посмотрим, как выглядят полные, нормализованные сообщения для разных типов датчиков.
Пример 1: Датчик движения ("сухой контакт") на входе UI-03Датчик обнаружил движение. Его реле замыкает цепь на универсальном входе контроллера.
{
"topic": "events/inputs/digital/ui-03",
"payload": {
"value_type": "boolean",
"value": true
},
"_msgid": "b2c3d4e5.f6g7h8",
"timestamp": 1678887100543,
"source_type": "hardware_input",
"source_id": "UI-03"
}
Пример 2: Датчик освещенности (выход 0-10В) на аналоговом входе AI-01
Датчик измеряет освещенность и выдает напряжение 4.53В, которое считывает контроллер.
{
"topic": "events/inputs/analog/ai-01",
"payload": {
"value_type": "float",
"value": 4.53,
"units": "V"
},
"_msgid": "c3d4e5f6.g7h8i9",
"timestamp": 1678887250111,
"source_type": "hardware_input",
"source_id": "AI-01"
}
Пример 3: Датчик температуры DS18B20 на шине 1-Wire
Контроллер опросил датчик и получил значение температуры 24.125 градуса.
{
"topic": "events/sensors/1-wire/28-01234567abcd",
"payload": {
"value_type": "float",
"value": 24.125,
"units": "°C"
},
"_msgid": "d4e5f6g7.h8i9j0",
"timestamp": 1678887320890,
"source_type": "1-wire",
"source_id": "28-01234567abcd"
}
Эти три примера наглядно демонстрируют мощь контракта: несмотря на совершенно разную физическую природу, все три события имеют единую, понятную и самодостаточную структуру.
---
Практика: Нормализация сигнала от 'сухого контакта' в Node-RED
Теперь перейдем от теории к практике. Наша задача — взять "сырое" сообщение от физического входа контроллера и преобразовать его в стандартный формат, соответствующий контракту. Мы будем использовать для этого самый мощный инструмент в арсенале Node-RED — узел `function`.
Как мы рассмотрели в уроке `COURSE-04-M01-L04`, подключение датчика с выходом типа "сухой контакт" (например, датчик движения или геркон на двери) к универсальному входу (UI) контроллера приводит к генерации простого сообщения. Узел, отвечающий за опрос входа (назовем его `HI DI In`), при срабатывании датчика выдаст `msg` со следующим содержанием:
- `msg.payload`: `true` (при замыкании) или `false` (при размыкании)
- `msg.topic`: `"ui-03"` (имя, заданное в настройках узла)
Наша задача — преобразовать это примитивное сообщение в полноценный JSON-объект.
Схема потока (flow):[HI DI In] -----> [function: "Normalize DI Event"] -----> [debug: "Normalized Event"]
(вход UI-03)
* Настроен на пин, соответствующий `UI-03`.
* При изменении состояния генерирует `msg.payload` типа `boolean`.
Это сердце нашего преобразования. В него мы поместим следующий JavaScript-код:
// Сохраняем оригинальное значение, так как msg.payload будет перезаписан
const originalValue = msg.payload;
const inputId = 'UI-03'; // Уникальный ID источника
// 1. Формируем стандартизированный payload
const newPayload = {
value_type: 'boolean',
value: originalValue
// 'units' для boolean не требуется
};
// 2. Перезаписываем msg.payload новым объектом
msg.payload = newPayload;
// 3. Добавляем/обновляем мета-данные в самом объекте msg
msg.timestamp = Date.now(); // Фиксируем время события
msg.source_type = 'hardware_input';
msg.source_id = inputId;
// 4. Формируем семантический topic для маршрутизации
msg.topic = `events/inputs/digital/${inputId}`;
// Возвращаем полностью сформированное сообщение для дальнейшей обработки
return msg;
* Настроен на отображение всего объекта `msg object`.
Результат:Когда датчик движения на входе `UI-03` сработает, в окне отладки `debug` мы увидим уже не просто `true`, а полноценное, обогащенное информацией сообщение, полностью соответствующее нашему контракту:
{
"topic": "events/inputs/digital/UI-03",
"payload": {
"value_type": "boolean",
"value": true
},
"timestamp": 1678888000123,
"source_type": "hardware_input",
"source_id": "UI-03",
"_msgid": "e5f6g7h8.i9j0k1"
}
Теперь это сообщение готово для отправки в любые другие подсистемы — будь то система логирования, HMI или сценарий включения света, — и все они "поймут" его без дополнительных преобразований.
---
Практика: Нормализация данных от аналогового входа
Следующий шаг — нормализация данных от аналогового датчика, подключенного к аналоговому входу (AI) контроллера. Например, датчик давления с выходом 0-10В.
> ⚠️ Внимание: На этом этапе мы только формируем сообщение. Логика преобразования сырого значения (например, вольт) в физическую величину (например, бары или паскали) будет рассмотрена в следующем модуле `COURSE-04-M02`, посвященном масштабированию и калибровке сигналов. Наша текущая задача — корректно оформить исходные данные от АЦП.
Предположим, узел `HI AI In`, настроенный на вход `AI-01`, периодически считывает напряжение и отправляет его в `msg.payload` в виде числа с плавающей запятой.
- Входящее сообщение от `HI AI In`: `msg.payload = 7.25`
[HI AI In] -----> [function: "Normalize AI Event"] -----> [debug: "Normalized Event"]
(вход AI-01)
Мы можем немного модифицировать код из предыдущего примера для работы с аналоговыми данными.
Код для узла `function` ("Normalize AI Event"):// Сохраняем оригинальное значение
const originalValue = msg.payload;
const inputId = 'AI-01';
// 1. Проверяем, что на входе действительно число.
// Это базовая валидация для защиты от ошибок.
if (typeof originalValue !== 'number') {
node.warn(`Получено нечисловое значение от ${inputId}: ${originalValue}`);
return null; // Прерываем поток, чтобы не распространять ошибку
}
// 2. Формируем стандартизированный payload
const newPayload = {
value_type: 'float',
value: originalValue,
units: 'V' // Явно указываем, что это вольты
};
// 3. Перезаписываем msg.payload новым объектом
msg.payload = newPayload;
// 4. Добавляем/обновляем мета-данные
msg.timestamp = Date.now();
msg.source_type = 'hardware_input';
msg.source_id = inputId;
// 5. Формируем семантический topic
msg.topic = `events/inputs/analog/${inputId}`;
return msg;
Результат:
Теперь, когда `HI AI In` пришлет значение `7.25`, узел `function` преобразует его в следующее сообщение:
{
"topic": "events/inputs/analog/AI-01",
"payload": {
"value_type": "float",
"value": 7.25,
"units": "V"
},
"timestamp": 1678888300456,
"source_type": "hardware_input",
"source_id": "AI-01",
"_msgid": "f6g7h8i9.j0k1l2"
}
Мы получили стандартизированное сообщение, которое четко говорит: "С аппаратного входа `AI-01` в такое-то время пришло значение `7.25` Вольт". Это сообщение является идеальной "заготовкой" для следующего этапа обработки — масштабирования, где эти вольты будут преобразованы в паскали, проценты или любую другую физическую величину в соответствии с характеристиками датчика. Разделение нормализации и масштабирования — ключевой принцип построения гибкой и обслуживаемой системы.
---
Итоги и лучшие практики
В этом уроке мы заложили один из самых важных камней в фундамент надежной системы автоматизации — контракт сообщения. Мы увидели, как переход от хаоса разрозненных форматов к единому стандарту на базе JSON радикально упрощает разработку, отладку и поддержку системы. Любое событие от любого датчика теперь является самодостаточным, типизированным и содержит всю необходимую мета-информацию для его однозначной идентификации и обработки.
Закрепим ключевые моменты и лучшие практики:
Что дальше?
Мы научились получать сырые данные и приводить их к стандартизированному, "нормализованному" виду. Но `7.25` вольт — это еще не давление в барах, а `3.5` вольта — не уровень CO2 в ppm. В следующем модуле мы перейдем к следующему логическому этапу — масштабированию и интерпретации сигналов, где научимся преобразовывать эти нормализованные значения в реальные физические величины, понятные человеку и другим системам автоматизации.