Jump to content

Как писать шаблоны для сторонних Modbus-устройств

From Wiren Board
This is the approved revision of this page, as well as being the most recent.

Введение

Рекомендуем сперва поискать ваше устройство в Таблице поддерживаемых устройств — вдруг оно уже там есть. Если устройства в списке нет, но оно поддерживает протокол Modbus, то его можно подключить к контроллеру Wiren Board.

Существует два вида протокола Modbus:

  1. Modbus RTU — устройства соединяются по шине RS-485.
  2. Modbus TCP — устройства соединяются по локальной сети через Wi-Fi или Ethernet.

Независимо от вида протокола, алгоритм добавления поддержки устройства в наш контроллер будет такой:

  1. Ищете документацию на ваше устройство с описанием его modbus-регистров.
  2. Составляете шаблон для нашего драйвера wb-mqtt-serial.
  3. Копируете созданный шаблон на контроллер в папку.
  4. Выбираете в веб-интерфейсе контроллера свой шаблон и указываете адрес.

Некоторые производители Modbus-устройств не придерживаются стандартов протокола, что может сказаться на работе всей шины. Поэтому рекомендуем проверять работу новых устройств на отдельной шине и только после того, как добьётесь стабильной работы, подключать к ней другие Modbus-устройства.

Подготовка

Устройство с Modbus RTU:

  1. Откройте документацию на устройство и найдите описание modbus-регистров и параметров подключения (Baud rate, Data bits, Parity, Stop bits, Slave ID).
  2. Подключите устройство к контроллеру по шине RS-485.
  3. Проверьте связь с устройством и правильность подключения:
    • Остановите драйвер wb-mqtt-serial или иное ПО, которое опрашивает устройство.
    • Подключитесь к устройству с помощью утилиты modbus_client и считайте данные из любого известного вам регистра.

Устройство Modbus TCP:

  1. Откройте документацию на устройство и найдите описание modbus-регистров и настроек подключения (адрес, порт).
  2. Утилита modbus_client в релизах до 2304 содержит ошибку, из-за которой она не может работать по протоколу Modbus TCP. Если используете устаревший релиз, то подключите устройство к компьютеру через Ethernet.
  3. Попробуйте считать из устройства значение одного известного вам регистра.

Если вы смогли получить содержимое регистра — вы всё делаете верно и можете продолжать.

Рекомендации к наименованию и структуре

Если вы планируете отправить шаблон к нам в репозиторий — выполнение этих правил обязательно. Для шаблонов «для себя» это рекомендации, но следовать им стоит: единообразные имена упрощают скрипты, интеграции и передачу объекта другому инженеру.

Правила именования и оформления собраны в публичные стандарты экосистемы Wiren Board — репозиторий wb-standards:

  • WB-STD-001 — идентификаторы MQTT-топиков: имена устройств, каналов и параметров;
  • WB-STD-002 — стиль текстов в интерфейсе: названия и описания на английском и русском;
  • WB-STD-003 — структура шаблона wb-mqtt-serial: поля, группы, переводы, версионирование.

Ниже — краткая выжимка. Сложные случаи (многоканальные устройства, счётчики электроэнергии, производные каналы) смотрите в самих стандартах — там же полный канон имён для типовых каналов.

Имена каналов и параметров (WB-STD-001)

Имя канала становится MQTT-топиком и попадает в конфигурацию пользователя — после публикации шаблона менять его нельзя. Поэтому сразу именуйте правильно:

  • только строчная латиница, цифры и подчёркивание: [a-z0-9_], snake_case; не более 64 символов, рекомендуем не более четырёх слов;
  • для типовых каналов используйте канонические имена (WB-STD-001 §6): sn — серийный номер, fw — версия прошивки, uptime, t_mcu — температура микроконтроллера, v_vin — напряжение питания;
  • каналы, привязанные к клеммам, именуются по этикетке на корпусе: вход «IN1» → di_in1, выход «K1» → do_k1, «Relay 2» → do_relay2;
  • единицы измерения в имя не включаются: не temperature_c, а temperature плюс поле units.
Примеры имён каналов и параметров (идут в mqtt и конфиг)
Неправильно Правильно Почему
Version of the Firmware fw канонический служебный канал (WB-STD-001 §6.1)
Supply Voltage v_vin канон: напряжение питания на клеммнике Vin; пробелы и заглавные запрещены
Status of Compressor 1 state_compressor1 префикс state_ — дискретное состояние без клеммы
VALVE_1_OUT do_v1 канал клеммы — по этикетке на корпусе (здесь «V1»)
Temperature_C temperature единственная очевидная величина — полное слово; единицы — в поле units

Тексты в интерфейсе (WB-STD-002)

Пользователь видит не имена каналов, а отображаемые тексты из секции translations — задавайте их на английском и русском:

  • английский — каждое слово с заглавной: Supply Voltage; русский — с заглавной только первое слово: Напряжение питания;
  • аббревиатуры — заглавными в обоих языках: RAM, MQTT; химические формулы — как принято: CO₂;
  • в русских текстах «е» вместо «ё», десятичный разделитель — точка;
  • единицы измерения: у каналов задаются полем units по словарю конвенции (интерфейс сам покажет °C или м³), у параметров — в названии в круглых скобках: Debounce Time (ms).
Примеры отображаемых текстов (видит пользователь в интерфейсе)
Неправильно Правильно Правило
Компрессор 1 статус Статус компрессора 1 естественный порядок слов
Использование Ram Использование RAM аббревиатуры — заглавными
Напряжение Питания Напряжение питания в русском с заглавной — только первое слово
supply voltage Supply Voltage в английском — каждое слово с заглавной
Задаёт задержку опроса. Задает задержку опроса «е» вместо «ё»; в конце описания точка не ставится

Структура шаблона (WB-STD-003)

  • Весь отображаемый текст живёт в translations, в остальных секциях — только идентификаторы-ключи. У групп и параметров ключом перевода служит обязательное поле title, равное их id.
  • device.name — маркировка устройства, как на корпусе (MY-RELAY, ОВЕН МВ110); не переводится, допустимы любые символы, включая кириллицу.
  • device.id — snake_case-идентификатор; из него драйвер строит MQTT-топики: /devices/<id>_<адрес>/....
  • Идентификаторы групп — с префиксом g_, вложенных — gg_.
  • Не используйте subdevices — для сложных шаблонов есть группы и conditions.
  • device_type должен совпадать с именем файла шаблона без префикса config- и расширения.

Создание шаблона

Если вам тяжело работать с JSON, можете попробовать использовать сервис генерации шаблонов https://tgen.wirenboard.com/ В нём можно в табличной форме перечислить регистры, выбрать как их отображать, добавить переводы и получить из этого готовый шаблон.

Шаблон можно сделать и с помощью AI-агента: подайте ему в контекст стандарты и JSON-схему — какие документы для какой задачи нужны, перечислено в таблице «Что подавать AI-агенту» в README репозитория wb-standards. Для Claude Code есть готовый скилл в маркетплейсе wb-agent-tools (плагин wb-mqtt-serial-template).

Рассказать драйверу wb-mqtt-serial, который в контроллере работает с Modbus-устройствами, можно двумя способами:

  1. Добавить регистры устройства прямо в веб-интерфейсе контроллера. Этот способ удобен для быстрой проверки работы.
  2. Создать шаблон, который описывает регистры устройства, их тип и другие параметры. Этот способ удобен для масштабирования: просто копируете шаблон на другой контроллер и в нём появляется поддержка вашего устройства.

Здесь мы рассмотрим создание простого шаблона. Упрощённо шаблон устройства выглядит так:

{
    "device_type": "my-relay",          // машинный идентификатор шаблона = имя файла без config- и расширения
    "title": "my_relay_template_title", // ключ отображаемого названия шаблона, сам текст — в translations
    "group": "g-relay",                 // группа в каталоге устройств. Список групп в документации
    "device": {
        "name": "MY-RELAY",             // маркировка устройства, как на корпусе; не переводится
        "id": "my_relay",               // из него строятся MQTT-топики: /devices/my_relay_<адрес>/...
        "groups": [ ],                  // группы параметров и каналов
        "channels": [ ],                // каналы, доступно в скриптах и на вкладке Устройства
        "parameters": [ ],              // параметры, можно менять в настройках устройства
        "translations": { }             // отображаемые тексты на английском и русском
    }
}

Полное описание смотрите в документации драйвера wb-mqtt-serial на Github.

Допустим, у нас есть одноканальное Modbus-реле: на корпусе подписаны вход «IN1» и выход «K1», а таблица регистров показана ниже.

Адрес Тип Название Назначение
0 Discrete Input Input 1 Состояние входа IN1
1 Input Register Input 1 Counter Счётчик замыканий входа IN1
3 Coil Relay 1 Состояние выхода K1 и управление им
10 Holding Input Mode Выбор режима взаимодействия входа с выходом

Каналы клемм именуем по этикеткам на корпусе (di_in1, do_k1), счётчик — по канону производных каналов (count_in1), а все тексты выносим в translations. Шаблон будет выглядеть так:

{
    "device_type": "my-relay",
    "title": "my_relay_template_title",
    "group": "g-relay",
    "device": {
        "name": "MY-RELAY",
        "id": "my_relay",
        "groups": [
            {
                "id": "g_channels",
                "title": "g_channels",
                "order": 0
            },
            {
                "id": "g_settings",
                "title": "g_settings",
                "order": 1
            }
        ],
        "channels": [
            {
                "name": "di_in1",
                "reg_type": "discrete",
                "address": 0,
                "type": "switch",
                "group": "g_channels"
            },
            {
                "name": "count_in1",
                "reg_type": "input",
                "address": 1,
                "group": "g_channels"
            },
            {
                "name": "do_k1",
                "reg_type": "coil",
                "address": 3,
                "type": "switch",
                "group": "g_channels"
            }
        ],
        "parameters": [
            {
                "id": "in1_mode",
                "title": "in1_mode",
                "reg_type": "holding",
                "address": 10,
                "format": "u16",
                "enum": [1, 2],
                "enum_titles": ["in1_mode_enum_0", "in1_mode_enum_1"],
                "default": 1,
                "group": "g_settings"
            }
        ],
        "translations": {
            "en": {
                "my_relay_template_title": "MY-RELAY (single-channel relay)",
                "g_channels": "Channels",
                "g_settings": "Settings",
                "di_in1": "Input 1",
                "count_in1": "Input 1 Counter",
                "do_k1": "K1",
                "in1_mode": "Input 1 Mode",
                "in1_mode_enum_0": "Switch Relay",
                "in1_mode_enum_1": "Not Used"
            },
            "ru": {
                "my_relay_template_title": "MY-RELAY (одноканальное реле)",
                "g_channels": "Каналы",
                "g_settings": "Настройки",
                "di_in1": "Вход 1",
                "count_in1": "Вход 1: счетчик замыканий",
                "do_k1": "K1",
                "in1_mode": "Режим входа 1",
                "in1_mode_enum_0": "Переключать реле",
                "in1_mode_enum_1": "Не используется"
            }
        }
    }
}

Обратите внимание:

  • у канала-счётчика поле type не задано — по умолчанию используется тип value;
  • настраиваемый параметр лежит в holding-регистре — в read-only регистры (input, discrete) параметр записать нельзя;
  • подписи значений enum_titles — тоже ключи переводов, по шаблону <id>_enum_<номер>;
  • обе языковые версии задаются явно: если перевода нет, пользователь увидит в интерфейсе сырой ключ.

Загрузка шаблона на контроллер

Когда шаблон готов, его надо загрузить на контроллер:

  1. Сохраните шаблон в файл. Имя файла — config- плюс device_type: например, config-my-relay.json. Загрузите его на контроллер в папку /etc/wb-mqtt-serial.conf.d/templates по инструкции.
  2. Ошибки шаблона проверяются драйвером wb-mqtt-serial при старте или при изменении шаблона на диске. Все ошибки пишутся в журнал, который можно посмотреть:
    • в веб-интерфейсе на вкладке Настройки → Системный журнал, для удобства отфильтруйте сообщения журнала по сервису wb-mqtt-serial.service и типу error. Нажмите Загрузить;
    Журнал wb-mqtt-serial
    # journalctl -u wb-mqtt-serial -p 3
    wb-mqtt-serial[31987]: ERROR: [serial] Failed to reload template: Failed to parse JSON /etc/wb-mqtt-serial.conf.d/templates/my-template-config.json:* Line 31, Column 33
    
    В примере шаблон my-template-config.json содержит ошибку в строке 31, символе 33.
  3. Если с шаблоном всё в порядке, то перейдите в настройки драйвера и выберите ваш шаблон.

Отклонения от стандарта Modbus и что с ними делать

Оптимизация запросов драйвером

Стандартом Modbus RTU предусмотрен обязательный интервал тишины в 3.5 символа между фреймами данных (под символом подразумевается посылка, состоящая из стартового бита, битов данных, бита четности и стоп-битов).

Для ускорения опроса устройств Wiren Board мы соблюдаем этот интервал только перед первым запросом к следующему в цикле опроса устройству (параметр frame_timeout_ms в шаблонах устройств).

Поэтому, чтобы соответствовать требованиям протокола Modbus-RTU, нужно для сторонних устройств задавать параметр guard_interval_us. Этот параметр задает задержку перед записью каждого запроса в порт.

Нужное значение рассчитывается по формуле:

guard_interval_us = (3.5*11*10^6)/(скорость в бит/с).

Например, для скорости 9600 бит/с guard_interval_us = (3.5*11*10^6)/9600 = 4000 мкс. При проблемах с подключением стороннего устройства для теста это значение можно увеличить (например до 100000 мкс), так как сторонние устройства иногда работают не совсем корректно.

Если при работе со сторонним устройством возникают проблемы, а настройка guard_interval_us не помогает, то можно попробовать установить параметр

force_frame_timeout = true,

Включение этого параметра приведёт к тому, что сервис будет ожидать время, равное frame_timeout_ms, даже после получения полного ответа от устройства, что уменьшает частоту опроса.

Если каналы устройства периодически мигают красным

Таймауты

Со сторонними устройствами довольно частая ситуация, когда вы сделали шаблон, данные идут, но в логах сыпятся ошибки, а каналы устройства в веб-интерфейсе контроллера окрашивают красным.

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

Чтобы починить, попробуйте увеличить параметр guard_interval_us вплоть до тысяч единиц, например, 5000. Если работа стабилизируется, потихоньку уменьшайте это значение до тех пор, пока ошибки не появятся вновь. Предыдущее значение, когда всё работало хорошо и будет вашим значением в шаблоне.

Ещё есть параметр response_timeout_ms — это максимальное время ответа устройства в миллисекундах, по умолчанию 500 мс. С ним тоже можно аккуратно поэкспериментировать.

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

Оба параметра пишутся в секцию device шаблона:

"device": {
    "name": "BAC-6000ELNW",
    "id": "bac-6000elnw",
    "response_timeout_ms": 100,
    "guard_interval_us": 5000,
...
}

Некорректное поведение на линии

Редко, но бывает, что сторонние устройства ведут себя некорректно на общей шине из-за багов или других сбоев, не зависящих от нас. В таких случаях обычно установка различных таймаутов не помогает или не убирает проблему полностью.

Wiren Board старается выявлять такие случаи и описывать индивидуальные рекомендации.

Пример:

К сожалению, подобные ошибки сложно диагностировать без осциллографа. Перед началом диагностики:

  • убедитесь, что монтаж соответствует рекомендациям по построению шины RS-485;
  • убедитесь, что рядом нет устройств, которые могут генерировать сильные наводки;
  • проверьте настройки порта контроллера и устройства, соответствие стоп-битов, скорости и чётности;
  • попробуйте рекомендации по настройке таймаутов.

Разные регистры для чтения состояния и управления

Иногда в сторонних устройствах встречаются особенности:

  1. В некоторые регистры можно только писать информацию, но нельзя считывать.
  2. Один и тот же параметр может читаться по одному адресу, а записываться по другому.

Для того, чтобы драйвер работал с такими устройствами без ошибок, мы добавили параметр write_address.

Писать можно, читать нельзя

Допустим, в нашем устройстве есть такой регистр только для записи.

Адрес Тип Название Назначение
20 Coil Relay 1 Switch Управление выходом. Только для записи.

Такой канал — команда, поэтому именуем его по форме cmd_<глагол>_<объект> (WB-STD-001 §6.3) и описываем так:

{
    "name": "cmd_switch_relay1",
    "write_address": "20",
    "reg_type": "coil",
    "type": "pushbutton",
    "format": "u16",
    "group": "g_channels",
    "on_value": 1 // определяет, что записывать в регистр при нажатии
}

Писать в один регистр, читать из другого

Допустим, в нашем устройстве команда записывается в один регистр, а читается из другого. Этот метод можно использовать только, если у обоих регистров один тип. Если типы разные — создавайте два разных канала: на чтение и на запись.

Адрес Тип Название Назначение
20 Coil Relay 1 Switch Управление выходом. Только для записи.
21 Coil Relay 1 State Состояние выхода. Только для чтения.

В этом случае канал надо описывать так: у нас будет один параметр в контроллере, но при обмене данными запись будет производиться в один регистр, а чтение — из другого. Имя каналу даём по этикетке выхода на корпусе («Relay 1» → do_relay1):

{
    "name": "do_relay1",
    "write_address": "20",   // адрес, куда мы записываем команды
    "address": "21",         // адрес, по которому мы читаем состояние
    "reg_type": "coil",
    "type": "switch",
    "format": "u16",
    "group": "g_channels"
}

Произвольные значения в регистрах с бинарной логикой

Иногда бывает так, что по смыслу регистр должен представляться в веб-интерфейсе переключателем ВКЛ/ВЫКЛ, но допустимые значения у него не 1/0.

В этом случае вы описываете обычный канал с типом switch и указываете значения on_value и off_value. Например, управление электрозамком:

{
  "name": "door_lock",
  "reg_type": "holding",
  "address": "0",
  "type": "switch",
  "format": "u16",
  "on_value": "0x00a5", // ВКЛ
  "off_value": "0x005a" // ВЫКЛ
}

Теперь драйвер будет автоматически при чтении конвертировать указанные значения в положение переключателя, а при изменении положения переключателя — записывать указанные значения.

Полезные ссылки