ГлавнаяАкадемияДатчики и входы: нормализация сигналов → Контракт сообщения: структура JSON для событий от датчиков

Контракт сообщения: структура JSON для событий от датчиков

Урок 5 · Датчики и входы: нормализация сигналов · 30 мин · theory

Введение: Зачем нужен 'контракт' сообщения?

В любой сложной системе автоматизации, от умного дома до промышленного объекта, десятки и сотни датчиков непрерывно генерируют данные. Датчик движения сообщает о присутствии, датчик температуры — о микроклимате, датчик протечки — об аварии. Каждый из них передает свой физический сигнал, который, как мы обсуждали в предыдущих уроках, преобразуется в логическое событие. Проблема возникает тогда, когда эти события не имеют единого, предсказуемого формата.

Представьте себе сценарий:

Каждое из этих сообщений, по сути, означает "событие произошло". Но чтобы написать единую логику, которая реагирует на любое из них (например, для системы безопасности), инженеру придется создавать сложный узел `switch`, проверяющий три разных типа данных и три разных значения. Теперь умножьте это на 50 датчиков. Система быстро превращается в хаос разрозненных форматов, где каждое новое устройство требует написания уникального "переводчика". Отладка такой системы становится кошмаром, а масштабирование — практически невозможным.

Для решения этой проблемы вводится контракт сообщения (Message Contract).

> 📋 Ключевые понятия:

> Контракт сообщения — это формальное, документированное соглашение о структуре и формате данных, которыми обмениваются все компоненты системы автоматизации. В нашей экосистеме это стандартная структура JSON-объекта, передаваемого внутри `msg`.

По своей сути, контракт сообщения — это внутренний API вашей системы автоматизации. Точно так же, как веб-сервис предоставляет внешний API для взаимодействия с ним, компоненты вашей системы (датчики, логические потоки, исполнительные устройства, панели HMI, базы данных) используют контракт для предсказуемого и стандартизированного общения друг с другом.

Преимущества стандартизации очевидны:

  • Предсказуемость: Вы всегда знаете, какую структуру имеет сообщение от любого датчика в системе. Вам не нужно гадать, придет ли `true`, `"ON"` или `1`.
  • Переиспользуемость логики: Вы можете написать один субпоток (subflow) для журналирования событий, и он будет работать для всех датчиков, потому что формат данных одинаков. Вы можете создать один блок для HMI, который отображает состояние устройства, и он будет работать с любым источником данных.
  • Упрощение интеграции: При отправке данных в сторонние системы (например, в базу данных MySQL для аналитики или на MQTT-брокер для облачной платформы) вам не нужно писать десятки кастомных преобразователей. У вас есть единый формат, который легко парсить и сохранять.
  • Ускорение разработки и отладки: Вместо того чтобы каждый раз заново "изобретать" структуру данных, вы используете готовый, проверенный шаблон. При отладке вы сразу видите всю необходимую информацию о событии в одном месте.
  • Внедрение и строгое соблюдение контракта сообщений — это не дополнительная работа, а фундаментальная инвестиция в надежность, масштабируемость и обслуживаемость вашей системы автоматизации.

    ---

    Стандартная структура события 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, чтобы описать суть сообщения.

    Такой подход позволяет одним узлом `Switch` обрабатывать целые классы событий, например, `содержит "events/inputs/"`.

    `msg.payload` (object)

    Это "полезная нагрузка" сообщения. В отличие от примитивных систем, где `payload` содержит только само значение (`true` или `25.5`), наш контракт требует, чтобы `payload` всегда был JSON-объектом, содержащим детализированную информацию о значении. Эту структуру мы разберем в следующем разделе.

    `msg.timestamp` (number)

    Это одно из самых критически важных полей. Оно должно содержать временную метку возникновения физического события, а не его обработки.

    `msg.source_type` (string)

    Это поле классифицирует источник события на высоком уровне. Оно помогает понять, где в архитектуре системы родилось это событие.

    `msg.source_id` (string)

    Уникальный идентификатор источника события. Это "паспорт" датчика или компонента, который сгенерировал сообщение. Этот ID должен быть абсолютно уникальным в рамках всего проекта.

    * Для универсального входа №1: `"UI-01"`

    * Для реле №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) корректно интерпретировать значение без дополнительных проверок.

    `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` со следующим содержанием:

    Наша задача — преобразовать это примитивное сообщение в полноценный JSON-объект.

    Схема потока (flow):
    [HI DI In] -----> [function: "Normalize DI Event"] -----> [debug: "Normalized Event"]
    

    (вход UI-03)

  • Узел `HI DI In` (гипотетический узел для работы с входами):
  • * Настроен на пин, соответствующий `UI-03`.

    * При изменении состояния генерирует `msg.payload` типа `boolean`.

  • Узел `function` (с именем "Normalize DI Event"):
  • Это сердце нашего преобразования. В него мы поместим следующий 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;

  • Узел `debug`:
  • * Настроен на отображение всего объекта `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] -----> [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 радикально упрощает разработку, отладку и поддержку системы. Любое событие от любого датчика теперь является самодостаточным, типизированным и содержит всю необходимую мета-информацию для его однозначной идентификации и обработки.

    Закрепим ключевые моменты и лучшие практики:

  • Строгость прежде всего: Всегда и везде придерживайтесь принятого контракта. Любое исключение или "временное решение" — это потенциальная точка отказа в будущем. Каждый новый датчик, каждое новое событие в системе должно быть нормализовано к стандартному виду как можно ближе к его источнику.
  • Централизуйте идентификаторы (`source_id`): Не прописывайте `source_id` в виде строковых констант ("магических строк") прямо в коде узлов `function`. Это чревато опечатками и усложняет рефакторинг. Лучшая практика — выносить ID в переменные контекста (`flow`) или окружения контроллера. Это позволяет легко менять привязки без изменения кода.
  • Уважайте `timestamp`: Всегда сохраняйте оригинальный `timestamp` события. Если сообщение проходит через несколько систем (например, Modbus-устройство сообщает время, затем контроллер его получает и отправляет в MQTT), не перезаписывайте временную метку на каждом шаге. Передавайте ее в неизменном виде или сохраняйте в дополнительном поле, например, `msg.meta.original_timestamp`.
  • Документация — ваш лучший друг: Создайте и поддерживайте в актуальном состоянии "словарь" или реестр всех `source_id`, используемых в проекте. Этот документ должен связывать логический `source_id` (например, `"UI-05"`) с его физическим воплощением ("Датчик протечки в санузле 1 этажа") и ссылкой на схему подключения (например, `WIRING-SAFETY-002`). Такая документация бесценна при сдаче объекта и его дальнейшей эксплуатации.
  • Что дальше?

    Мы научились получать сырые данные и приводить их к стандартизированному, "нормализованному" виду. Но `7.25` вольт — это еще не давление в барах, а `3.5` вольта — не уровень CO2 в ppm. В следующем модуле мы перейдем к следующему логическому этапу — масштабированию и интерпретации сигналов, где научимся преобразовывать эти нормализованные значения в реальные физические величины, понятные человеку и другим системам автоматизации.