Skip to content
Menu

Интеграция

This page is not translated yet. The text below is the Russian source. Facts are unchanged.

Страница описывает, как внешние системы — SCADA, диспетчеризация, облако, панели, скрипты — обмениваются данными с vPLC на СА-02м. Как писать саму программу ПЛК — программирование; ключи /etc/vplc.conf — конфигурация; работа в веб-интерфейсе установки — веб и настройка установки.

Интерфейс Кто сервер Порт по умолчанию Включение Аутентификация
REST API рантайма, WebSocket, SSE служба vplc 1234 (rest_port) всегда токен Bearer для записи (api_token), см. ниже
Веб-интерфейс установки и его API служба vplc-plant-ui (server.py) 8088 отдельная служба через маршрут панели СА-02м /vplc/; напрямую — без пароля
Modbus TCP slave рантайм 502 (modbus_slave_port) modbus_slave_enabled нет
Modbus master (TCP/RTU) рантайм — клиент — таблица modbus_devices проекта —
MQTT рантайм — клиент брокера брокер 127.0.0.1:1883 mqtt_enabled mqtt_username / mqtt_password
OPC UA рантайм 4840 (opcua_port) opcua_enabled нет (Security None, анонимный доступ)

Порты на СА-02м. Порт 502 занят ведомым Modbus TCP шины, 4840 — зарезервирован, 4841 использует OPC UA-шлюз веб-панели. Для vPLC используйте "modbus_slave_port": 1502 и "opcua_port": 4842 (или оставьте их выключенными).

Базовый адрес: http://<IP-адрес СА-02м>:1234. Тела запросов и ответов — JSON (кроме загрузки файлов). Ответы содержат Access-Control-Allow-Origin: *; ошибки — {"error":"…"} с кодом 4xx/5xx.

Если в /etc/vplc.conf задан api_token, рантайм требует заголовок

Authorization: Bearer <токен>
Запрос Нужен токен
все POST (запись, загрузка, пуск/останов, конфигурация) да
GET /api/config, GET /api/logs да
остальные GET (/api/status, /api/variables, …), встроенная страница / нет
WebSocket /ws/…, SSE /api/events/… не проверяется
  • Без токена или с неверным — 401 {"error":"unauthorized"}.
  • GET /api/config возвращает api_token и mqtt_password маской ***.
  • Смена токена (POST /api/config) вступает в силу после перезапуска службы.
  • Канал /ws/variables принимает запись переменных без токена, а веб-интерфейс установки (порт 8088) обращается к рантайму без токена — при заданном api_token его запись в рантайм будет отклонена. Если порт 1234 доступен из сети объекта, ограничьте доступ к нему межсетевым экраном.
Метод Путь Назначение
GET / встроенная страница: состояние, переменные, журнал
GET /api/status состояние скана, статистика, версия
GET /api/config действующая конфигурация (секреты маскируются)
POST /api/config изменить конфигурацию (JSON, частично)
GET /api/program/info загруженная программа
POST /api/program/upload загрузить .so (двоичное тело)
POST /api/project/upload загрузить проект .vplc (двоичное тело, до 8 МиБ)
POST /api/program/reload перезагрузить программу с диска (program_path)
POST /api/start запустить скан
POST /api/stop остановить скан
POST /api/restart перезапустить скан
GET /api/variables переменные (все или по фильтру)
POST /api/variables переменные по фильтру в теле запроса
POST /api/variables/<имя> записать переменную
GET /api/logs?limit=N последние записи журнала (по умолчанию 200, максимум 2000)
POST /api/dump/save сохранить значения переменных в дамп
POST /api/dump/restore восстановить значения из дампа
GET /api/mqtt/status состояние клиента MQTT
GET /api/modbus/status состояние Modbus slave и опрашиваемых устройств
POST /api/scan/stats/reset сбросить статистику скана
WS /ws/variables поток значений переменных; запись переменных
WS /ws/logs поток записей журнала
GET /api/events/variables SSE: значения переменных
GET /api/events/status SSE: состояние (около 1 раза в секунду)

В примерах ниже:

Окно терминала
H=http://<IP-адрес СА-02м>:1234
AUTH='Authorization: Bearer <токен>' # не нужен, если api_token пуст
Окно терминала
curl "$H/api/status"
{"state":"RUNNING","program":"Fan","cycle_ms":100,"cycle_count":1520,
"last_cycle_ms":0.02,"max_cycle_ms":0.35,"overrun_count":0,"p95_cycle_ms":0.04,
"stats_window":1000,"stats_cycles":1515,"stats_age_s":151.6,
"cycle_hist":{…},"scan_profile":{…},"scan_sched":"SCHED_OTHER",
"var_count":8,"version": "<версия сборки>"}
Поле Смысл
state IDLE (нет программы), STOPPED, RUNNING, ERROR
program имя программы
cycle_ms базовый период скана, мс
cycle_count сканов с запуска
last_cycle_ms, max_cycle_ms, p95_cycle_ms длительность скана: последняя, максимум и 95-й процентиль (окно до 1000 сканов)
overrun_count сколько раз скан не уложился в период
stats_cycles, stats_age_s сканов и секунд с последнего сброса статистики
cycle_hist, scan_profile, scan_sched гистограмма, профиль самого долгого скана, политика потока скана
var_count, version число видимых переменных, версия рантайма
Окно терминала
curl "$H/api/program/info"
# {"loaded":true,"name":"Fan","path":"/var/lib/vplc/program.so","var_count":8}
Окно терминала
curl -X POST "$H/api/project/upload" -H "$AUTH" --data-binary @Fan.vplc
curl -X POST "$H/api/program/upload" -H "$AUTH" --data-binary @Fan.so # программа загружена, скан STOPPED
curl -X POST "$H/api/start" -H "$AUTH" # {"ok":true,"state":"RUNNING"}
curl -X POST "$H/api/stop" -H "$AUTH" # {"ok":true,"state":"STOPPED"}
curl -X POST "$H/api/restart" -H "$AUTH"
curl -X POST "$H/api/program/reload" -H "$AUTH" # {"ok":true,"program":"Fan","state":"RUNNING"}
Код Когда
409 {"error":"already running"} start при работающем скане
409 {"error":"not running"} stop при остановленном
409 {"error":"no program loaded"} start/restart без программы
422 .so не загрузился

Подробности загрузки — программирование.

Окно терминала
curl "$H/api/variables"
[{"name":"RoomTemp","type":"REAL","dir":"INPUT","value":21.5,"comment":""},
{"name":"FanRun","type":"BOOL","dir":"OUTPUT","value":false,"comment":""}]

dir — INPUT, OUTPUT, INOUT, MEMORY. TIME отдаётся целыми секундами.

Фильтр — чтобы не читать тысячи переменных:

Параметр Смысл
names=a,b точные имена (можно повторять параметр)
prefix=Fan начало имени
glob=Room* маска: *, ?, […]

Критерии объединяются по ИЛИ; порядок ответа — порядок переменных в программе. Ответ с фильтром содержит заголовки X-VPLC-Filter: 1 и X-VPLC-Missing: N (сколько имён из names не найдено).

Окно терминала
curl -i "$H/api/variables?names=StateCode,AlarmCount&prefix=StatusMask"
# длинные списки — телом POST (нужен токен, если он задан)
curl -X POST "$H/api/variables" -H "$AUTH" -H 'Content-Type: application/json' \
-d '{"names":["StateCode","PlantType"],"glob":["Alarm*"]}'
# или просто массив имён: -d '["StateCode","PlantType"]'

Неверное тело — 400 {"error":"bad filter body: …"}.

Окно терминала
curl -X POST "$H/api/variables/TempOn" -H "$AUTH" \
-H 'Content-Type: application/json' -d '{"value": 27.5}'
# {"ok":true}
Тип value
BOOL true / 1 — ИСТИНА; всё остальное — ЛОЖЬ
целые, REAL, LREAL число
TIME целые секунды

Нет такой переменной или значение не разбирается — 404 {"error":"variable not found or type error"}. Значение действует со следующего скана; если программа сама присваивает переменную, запись будет перезаписана.

Записанные значения сохраняются в дамп только по POST /api/dump/save (и при штатной остановке службы):

Окно терминала
curl -X POST "$H/api/dump/save" -H "$AUTH" # {"ok":true}
Окно терминала
curl -H "$AUTH" "$H/api/config"
curl -X POST "$H/api/config" -H "$AUTH" -H 'Content-Type: application/json' \
-d '{"log_level":"debug","cycle_ms":50}'
# {"ok":true,"restart_required":false}

Сразу применяются cycle_ms, log_level, log_ring_size, scan_rt_priority, opcua_publish_ms. Изменение rest_port, api_token, project_path, mqtt_enabled, mqtt_host, mqtt_port, ключей Modbus slave, opcua_enabled, opcua_port, scan_mlock записывается в /etc/vplc.conf, но требует перезапуска службы — ответ {"ok":true,"restart_required":true,"note":"…"}.

Окно терминала
curl "$H/api/mqtt/status"
# {"connected":true,"broker":"127.0.0.1:1883","topics":3,"rx":120,"tx":45}
curl "$H/api/modbus/status"
# {"slave":{"enabled":true,"listening":true,"port":1502,"mw_size":10240,"clients":1,
# "map_mode":"mp02","last_error":"","mp02_rw_ranges":"4000-4015,…"},
# "devices":[{"name":"Pump1","connected":true,"errors":0,"polls":5321,"last_error":""}]}

topics — число входных привязок MQTT. slave.listening = false при включённом slave — порт занят (причина в last_error).

Окно терминала
curl -H "$AUTH" "$H/api/logs?limit=50"
# [{"ts":"21:28:04.356","level":"INFO","module":"Main","msg":"Program uploaded and loaded: Fan"}, …]
Канал Что приходит
ws://…:1234/ws/variables после скана — JSON-массив всех переменных (формат /api/variables без comment); при медленном клиенте отправляется только последний снимок
ws://…:1234/ws/logs записи журнала уровня INFO и выше
GET /api/events/variables SSE, событие variables, тот же массив
GET /api/events/status SSE, событие status, тело как /api/status, около 1 раза в секунду

Запись через WebSocket — сообщение в /ws/variables:

{"name":"TempOn","value":27.5}
Окно терминала
curl -N "$H/api/events/status"
# event: status
# data: {"state":"RUNNING","program":"Fan", …}

Пока нет ни одного подписчика WebSocket/SSE на переменные, рантайм не тратит время скана на формирование снимка.

Служба vplc-plant-ui (web/plant_ui/server.py) отдаёт страницы установки и API, совместимое с REST прибора MP-02, поверх REST рантайма.

Окно терминала
python3 server.py --port 8088 --vplc http://127.0.0.1:1234 \
[--host 0.0.0.0] [--plant-config /var/lib/vplc/plant_config.json] \
[--bindings bindings.json] [--contract vars_contract.json] [--mqtt 127.0.0.1:1883]
  • Через панель СА-02м (маршрут /vplc/, заголовок X-Forwarded-Prefix): вход проверяет панель. Каждый POST должен нести заголовок X-VPLC-Request: 1, иначе 403 {"error":"csrf"}. Рекомендуемый режим: служба слушает только --host 127.0.0.1.
  • Напрямую на порт 8088 локальная установка доступна без пароля (уровень «Сервис»). Не открывайте 8088 в сеть объекта.
  • Запись во внешние установки (Carel, MP-02 по TCP) при прямом доступе требует живой сессии панели СА-02м (cookie проверяется у панели), иначе 401 {"ok":false,"reason":"auth"|"login"|"panel_unreachable"}.

В примерах: P=http://127.0.0.1:8088.

Метод и путь Назначение
GET /api/plant/snapshot[?names=a,b] плоский объект «имя → значение» всех (или указанных) переменных
POST /api/plant/set {"name","value"} запись: параметры ahu_* (с проверкой диапазона и сохранением дампа); служебные PlantType (1…5), UnitType (0…2), RunReq, SimEnable, Cmd*, EcoReq, ServiceReq, ManualReq, SeasonReq — без сохранения дампа
POST /api/plant/cmd {"cmd"} импульс: start, stop, ack, reset
GET /api/params/schema[?group=N&lang=en] описание параметров (тип, пределы, единицы, группа)
GET /api/params[?group=N] значения параметров по ключам ahu.<группа>.<имя>
PUT /api/params {"values":{…}} запись параметров; ответ {ok, applied, failed, errors[], job} (200 или 207 при частичном отказе)
POST /api/params/defaults {"group":N} заводские значения группы (255 — всех)
POST /api/command {"cmd", "value"?, "arg"?} start, stop, ack, reset, mode, setpoint, season, eco, service, manual, uv_life_reset, param, param_defaults, param_save
GET /api/status, /api/info, /api/time состояние в формате MP-02, паспорт, время
GET /api/tags, /api/tags/values?ids=… словарь тегов и значения с кодом качества
GET /api/alarms[?state=active|unacked|latched|all] аварии локальной установки
POST /api/alarms/ack, /api/alarms/reset {"bit":N} или {"all":true}
GET /api/alarms/config, PUT /api/alarms/config класс, реакция, задержки аварий (задержки — записываемые параметры)
GET /api/events, POST /api/events/clear журнал событий веб-сервера
GET /api/io/bindings привязки bindings.json
GET /api/iomap/status, POST /api/iomap/apply, POST /api/iomap/autobind применение привязки I/O в ПЛК (см. Привязка I/O)
Окно терминала
curl "$P/api/plant/snapshot?names=PlantType,StateCode,rSupplyTemp"
# {"PlantType":1,"StateCode":3,"rSupplyTemp":19.8}
curl -X PUT "$P/api/params" -H 'Content-Type: application/json' \
-d '{"values":{"ahu.sp.temp":21.0}}'
# {"ok":true,"applied":1,"failed":0,"errors":[],"job":…}
curl -X POST "$P/api/command" -H 'Content-Type: application/json' -d '{"cmd":"start"}'

После записи параметров сервер сам вызывает POST /api/dump/save рантайма.

Метод и путь Назначение
GET /api/site, PUT /api/site название объекта, контакты, тема, наличие систем
GET /api/units список установок с состоянием, сводкой, энергией, расписанием
POST /api/units добавить установку (внешнюю — {kind, source:{device_id}, name?}; локальную — {kind:"local-itp"|"local-water"|"local-light"})
PUT /api/units {"order":[…]} порядок установок
GET / DELETE /api/units/<id> одна установка / удалить (кроме local)
GET / PUT /api/units/<id>/settings имя, группа, состав, энергия, расписание, подписи
PUT /api/units/<id>/params параметры локальной системы своего типа (itp.*, water.*, light.*, ahu.*)
GET /api/units/<id>/snapshot значения в именах программы + _meta
POST /api/units/<id>/command {cmd, value?, arg?, wait?} команда (см. Внешние установки)
GET /api/units/<id>/alarms аварии установки
GET / PUT /api/units/<id>/schedule недельное расписание
GET /api/units/summary сводная таблица
GET /api/alarms?all=1 аварии всех установок
GET /api/io/devices[?type=…] устройства на шине MQTT СА-02м
GET /api/energy/<id>/summary[?range=1h|6h|24h|7d|30d|mtd|month] энергия и стоимость
GET /api/energy/<id>/series?period=day|week|month ряд кВт·ч по часам/дням

Включение (/etc/vplc.conf или настройки проекта; применяется после перезапуска службы):

{"modbus_slave_enabled": true, "modbus_slave_port": 1502,
"modbus_map_mode": "packed", "modbus_mw_size": 1024}

Сервер принимает несколько клиентов одновременно. Адреса — с нуля, идентификатор устройства (unit id) не проверяется.

Область Функции Адреса Содержимое
Holding registers 3, 6, 16 0…1023 образ программы PUT_4X/GET_4X; адрес 1024 и выше — исключение 02
Input registers 4 с 0 все видимые переменные не-BOOL подряд в порядке объявления
Coils 1, 5, 15 с 0 видимые BOOL подряд; запись — только в VAR_OUTPUT/VAR_IN_OUT
Discrete inputs 2 с 0 те же BOOL, только чтение

Упаковка: переменная занимает ⌈размер/2⌉ регистров, младшее слово по меньшему адресу, младший байт — в младшем байте регистра. Пример для программы из программирование:

Input register Переменная Coil / DI Переменная
0–1 RoomTemp (REAL) 0 RoomTemp_q
2–3 FanSpeed (REAL) 1 FanRun
4–5 TempOn (REAL) 2 Request
6–7 TempOff (REAL)
8–9 Starts (UDINT)

Адреса сдвигаются при добавлении переменных — для стабильной карты выкладывайте значения в образ PUT_4X по фиксированным адресам, а уставки принимайте через GET_4X. В режиме packed оставляйте modbus_mw_size = 1024 (значение по умолчанию). [не проверено: работа режима packed при modbus_mw_size больше 1024]

Режим для SCADA, уже настроенных на контроллер MP-02: программа universal публикует данные по тем же адресам.

{"modbus_slave_enabled": true, "modbus_slave_port": 1502,
"modbus_map_mode": "mp02", "modbus_mw_size": 10240}
  • Образ — 9216…10240 регистров (modbus_mw_size прижимается к этим границам); функции 3/4 читают одни и те же адреса; запись — функции 6, 16, 22, 23.
  • Float, u32, u64 — старшее слово по меньшему адресу.
  • Запись SCADA попадает в программу в начале следующего скана; программа проверяет значение и, если оно недопустимо, возвращает регистр к прежнему значению (ответ на запись при этом успешный).
  • Защиты конфигурации (ключ в регистре 180) и авторизации источника нет: параметры может записать любой клиент.

Основные адреса (программа вентиляции, PlantType = 1):

Адрес Тип Доступ Содержимое
20 u16 R id программы = PlantType
100 u16 RW режим: 0 выкл, 1 ручной, 2 авто
101 u16 RW код команды (ниже); аргументы 102 (u16) и 103–104 (float) пишутся до кода
105 i16 R результат команды: 0, −2 диапазон, −4 нет такого, −10 не поддержано
190–193 i16 ×10 RW уставки: T притока, влажность, скорость в ручном, режим скорости
1000–1003 u64 R маска аварий «активна или защёлкнута» (1000 = биты 63…48)
1004–1067 u16 ×64 R состояние аварии: 0 нет, 1 активна, 2 защёлкнута
4000–4093 float R величины (температуры, давления, выходы)
4100–4146 i16 R те же величины ×10
4150–4156 u16 R упакованные DI (80 бит) и DO (32 бита)
4157 / 4158 / 4159 u16 R состояние / режим / зима
4160–4191 u32 R наработка узлов, ч и мин
4200 u16 R код статуса
4201–4204 u64 R маска активных аварий
4205–4206 u32 R маска статуса
4207–4209 u16 R число аварий, худший класс, худшая реакция
4210–4241 u16 ×32 R коды активных аварий (код = бит + 1)
4250–4252 — R счётчик сканов, такт скана, мс
4260–4299 — R расширение vPLC: энергоучёт, расписание, маски аварий, УФ-стерилизатор, фильтр 3
4300 + id×2 float RW параметры (id — номер параметра в контракте); время — в секундах (у параметров с шагом в целый час — в часах)

Запись в 0–99, 1000–1067 и 4000–4299 (кроме командных окон ниже) — исключение 02.

Параметры (4300 + id×2). Функция 16 на оба регистра записывает float целиком. Функция 6 в старший, затем в младший — то же. Одиночная функция 6 в младший регистр записывает целое значение (удобно для BOOL и перечислений). Номер id (gid) и адрес (modbus_addr) каждого параметра — в контракте программы (vars_contract.json), например ahu.energy.meter — id 316, адрес 4932.

Катушки:

Катушка Запись Чтение
0 / 1 1 — пуск / стоп (импульс) 0
2 / 3 1 — квитировать все / сбросить защёлки 0
4 / 5 / 6 1/0 — эко / сервис / ручной состояние
7 1 — сохранить параметры 0
8 1 — перезагрузка (не исполняется) 0
16 1 — пуск, 0 — стоп («Работа») RunReq

Discrete inputs: 0…63 — биты активных аварий, 64…95 — биты статуса.

Команды (регистр 101): 1 пуск, 2 стоп, 3 режим (arg 0…2), 4 квитировать (arg 255 — все), 5 сброс, 6 записать параметр (arg = id, значение — 103–104 в единицах хранения, время — в мс), 8 сохранить параметры, 23 уставка (arg 0…3), 24 ручной (arg 0/1), 25 сезон (arg 0 авто, 1 зима, 2 лето), 26 эко, 27 сервис; прочие — результат −10.

Командные окна внутри 4000…4299 — записываемые адреса других типов установок (ключ modbus_mp02_rw_ranges, перезапуск службы):

Тип (PlantType) Записываемые регистры
вентиляция (1) нет (команды — 100…105, 190…193, катушки)
освещение (2) 4000+z слово зоны, 4140+z ручной уровень, 4160+z команда зоне (z = 0…15); 4190+g мастер группы (g = 0…3); 4256 сцена, 4257 паника, 4258 «погасить всё», 4273 общий мастер
ИТП (3) 4260 команда, 4261 сброс, 4262/4263 уставка ГВС (float), 4264 сезон, 4265 дезинфекция, 4266 сброс аварий, 4270 подпитка, 4271 ночное снижение
вода (4) 4150 режим, 4151 пуск/останов, 4190 квитирование, 4191 промывка, 4192 контрольный пуск пожарных, 4193 останов пожарной станции, 4194…4199 ручные команды насосам 1…6
холод (5) 4150 режим, 4151 пуск, 4155 нагрев, 4158 эко, 4159 вторая уставка, 4169 разрешение принудительной производительности, 4190…4195 квитирование, уставка, производительность, оттайка, задание и разрешение BMS

По умолчанию открыто объединение всех окон (4000-4015,4140-4155,4158-4175,4190-4199,4256-4258,4260-4266,4270-4271,4273). Для объекта с одним типом сузьте набор, например для ИТП: "modbus_mp02_rw_ranges": "4260-4266,4270-4271"; "none" — всё окно только чтение. Действующий набор — GET /api/modbus/status → slave.mp02_rw_ranges.

Полная карта (все 94 величины, 270 параметров вентиляции и расширение vPLC) — документ MODBUS_MP02.md и контракты apps/universal/contract/*.json в дереве репозитория vPLC. Отдельной подписанной карты регистров в этой вики нет.

Рантайм сам опрашивает устройства из таблицы modbus_devices проекта .vplc (как объявлять переменные и шаблоны устройств — программирование).

Поле Смысл
name имя устройства — префикс переменных <name>_IW<адрес>, <name>_MW<адрес>
mode tcp или rtu
host, port адрес и порт (TCP)
serial, baud порт и скорость (RTU; формат кадра 8N1)
unit_id адрес устройства
read_start, read_count блок чтения (функция 3, holding)
write_start, write_count блок записи (функция 16)
template шаблон устройства (необязательно)

Поведение:

  • Опрос идёт в отдельном потоке и не задерживает скан. Проход по всем устройствам — каждые 50 мс плюс время обмена.
  • За проход: чтение блока read_* (запросами до 125 регистров), затем запись всего блока write_* текущими значениями переменных _MW (каждый проход, не только при изменении).
  • Таймаут ответа — 500 мс. Любая ошибка обмена разрывает соединение; повторное подключение — через 1 с, затем 2, 4 … до 30 с.
  • Состояние — GET /api/modbus/status → devices[] (connected, errors, polls, last_error).
  • Список устройств читается при старте службы: после изменения modbus_devices перезапустите службу vplc.

На СА-02м порты RS-485, которые опрашивает мост Modbus → MQTT СА-02м, занимать нельзя: модули на этих портах читайте через MQTT (ниже). Мастер vPLC можно использовать для устройств Modbus TCP и для свободных последовательных портов.

Ключ (/etc/vplc.conf) По умолчанию Смысл
mqtt_enabled false включить клиента
mqtt_host, mqtt_port 127.0.0.1, 1883 брокер (на СА-02м — локальный)
mqtt_client_id vplc идентификатор и имя собственного дерева топиков (проект из VS Code — vplc-sa02m)
mqtt_username, mqtt_password пусто <логин> / <пароль>, если брокер требует
mqtt_retain_outputs true retain для публикаций в собственное дерево
mqtt_stale_ms 125000 время устаревания входа, мс (не меньше 5000)
mqtt_bridge_id sa02m-bridge устройство моста для признака BRIDGE_ok

Таблица mqtt_brokers проекта перекрывает адрес брокера, client_id и учётные данные. Клиент переподключается сам (интервал 5…30 с).

vPLC работает с деревом устройств моста Modbus → MQTT СА-02м:

/devices/<id>/controls/<канал> значение (публикует мост, retain)
/devices/<id>/controls/<канал>/on команда записи (без retain)
/devices/<id>/meta/error "" или "r" — устройство не отвечает
Столбец Смысл
topic топик канала /devices/<id>/controls/<канал> (для выхода — без /on)
var_name переменная программы
is_input 1 — вход (топик → переменная), 0 — выход (переменная → топик)
scale, offset вход: перем = сырое × scale + offset; выход: сырое = (перем − offset) / scale
publish_suffix выход: суффикс топика записи; NULL — автоматически (см. ниже), '' — ровно topic
safe_value выход: значение при останове скана
on_stop выход: safe (по умолчанию при заданном safe_value) или hold
stale_ms вход: собственное время устаревания (NULL — mqtt_stale_ms)
quality_var вход: имя BOOL-переменной достоверности (NULL — <var_name>_q)

Карта загружается вместе с проектом: POST /api/project/upload, затем загрузка или POST /api/program/reload (перезапуск службы не нужен).

INSERT INTO mqtt_io_map (topic, var_name, is_input, scale, offset, safe_value, stale_ms, quality_var) VALUES
('/devices/mr02m-COM2-5/controls/di_1', 'xFilterClog', 1, 1.0, 0.0, NULL, NULL, NULL),
('/devices/mr02m-COM2-5/controls/ai_1', 'rSupplyTemp', 1, 1.0, 0.0, NULL, 30000, 'q_t_supply'),
('/devices/mr02m-COM2-6/controls/do_1', 'yFanRun', 0, 1.0, 0.0, 'FALSE', NULL, NULL),
('/devices/mr02m-COM2-7/controls/ao_1', 'yValvePct', 0, 0.1, 0.0, '0', NULL, NULL);

Аналоговый выход МР-02м принимает 0…1000 (0…10,00 В): при переменной в процентах scale = 0.1 (100 % → 1000), в вольтах — scale = 0.01.

  • Значение разбирается по типу переменной: BOOL — true/false/on/off или число (≠ 0 — ИСТИНА); целые — с округлением и насыщением; REAL — число с точкой. Невалидная строка не меняет переменную и снимает признак качества.
  • Значение пишется в переменную только при новом сообщении.
  • Качество. Если в программе объявлена BOOL-переменная <вход>_q (или имя из quality_var), рантайм держит её ИСТИНОЙ, пока: сообщение валидно и моложе stale_ms, у устройства и канала нет meta/error = "r", брокер на связи. При плохом качестве значение входа держится — программа должна проверять _q.
  • Дополнительные признаки (если объявлены в программе как BOOL):
Переменная ИСТИНА, когда
<устройство>_ok брокер на связи и у устройства нет meta/error = "r"; символы кроме [A-Za-z0-9_] заменяются на _: mr02m-COM2-5 → mr02m_COM2_5_ok
MQTT_ok рантайм подключён к брокеру
BRIDGE_ok мост СА-02м на связи (/devices/<mqtt_bridge_id>/controls/connection = 1)
  • Публикация при изменении значения (и заново всех выходов после переподключения), QoS 1.
  • Формат: BOOL — 1/0; REAL — до 6 значащих цифр; LREAL — до 10; целые — как есть. NaN и бесконечность не публикуются.
  • Топик записи: для чужого устройства /devices/<id>/controls/<канал> рантайм сам добавляет /on; в собственное дерево /devices/<mqtt_client_id>/… — без суффикса.
  • Retain: в …/on — никогда; в собственное дерево — по mqtt_retain_outputs.
  • Безопасное состояние. При POST /api/stop, ошибке скана и штатной остановке службы каждый выход с safe_value (и on_stop ≠ hold) публикуется этим значением. Загрузка программы безопасные значения не публикует. При аварийном завершении процесса срабатывает только LWT (meta/error = "r") — безопасное состояние поля обеспечивают правила СА-02м или сторожевой таймер модуля.
/devices/<mqtt_client_id>/meta/name "vPLC (…)"
/devices/<mqtt_client_id>/meta/error ""; при обрыве (LWT) — "r"
/devices/<mqtt_client_id>/controls/connection "1"; "0" при штатной остановке
/devices/<mqtt_client_id>/controls/runtime_state RUNNING | STOPPED | IDLE | ERROR
/devices/<mqtt_client_id>/controls/<переменная>/on ← запись любой видимой переменной
Окно терминала
# состояние рантайма
mosquitto_sub -h 127.0.0.1 -v -t '/devices/vplc-sa02m/#'
# записать уставку
mosquitto_pub -h 127.0.0.1 -t '/devices/vplc-sa02m/controls/TempOn/on' -m '27.5'

Запись через …/<переменная>/on разбирается как в REST: для BOOL — 1 или true. Публиковать переменную в собственное дерево — строка карты с is_input = 0 и топиком /devices/<mqtt_client_id>/controls/<имя>. Автоматически все переменные не публикуются.

{"opcua_enabled": true, "opcua_port": 4842, "opcua_name": "vPLC OPC UA Server",
"opcua_publish_ms": 500}
Свойство Значение
Конечная точка opc.tcp://<IP-адрес СА-02м>:<opcua_port>/ (только IPv4)
Безопасность Security None, анонимный доступ
Узлы каждая видимая переменная — ns=2;s=<Имя> в папке Objects (пространство имён urn:vplc:variables)
Доступ чтение и запись
Обновление значений собственным потоком сервера раз в opcua_publish_ms (по умолчанию 500 мс, 0 — как можно чаще); меняется без перезапуска
Перезагрузка программы дерево узлов перестраивается автоматически

Типы узлов:

ST OPC UA ST OPC UA
BOOL Boolean DINT Int32
BYTE, USINT Byte LINT Int64
SINT SByte UDINT, DWORD, TIME UInt32
INT Int16 ULINT, LWORD UInt64
UINT, WORD UInt16 REAL Float
LREAL Double

Записывать нужно значением точно этого типа: запись Double в узел REAL отклоняется (в журнале OPC UA write ignored … unsupported variant).

Сервер работает, если рантайм собран с библиотекой open62541; иначе при opcua_enabled: true в журнале будет OPC UA support not compiled in.

from asyncua.sync import Client # pip install asyncua
with Client("opc.tcp://<IP-адрес СА-02м>:4842/") as c:
node = c.get_node("ns=2;s=TempOn")
print(node.read_value())
from asyncua import ua
node.write_value(ua.DataValue(ua.Variant(27.5, ua.VariantType.Float)))

universal — готовая программа vPLC, повторяющая программы прибора MP-02 для пяти типов установок. В одном program.so активен один тип — его задаёт переменная PlantType; переключение — без пересборки.

PlantType Установка Вид в plant_config.json
1 вентиляция (ПВУ) — по умолчанию local-ahu
2 освещение local-light
3 ИТП / отопление local-itp
4 водоснабжение и насосы local-water
5 холодоснабжение —

Внутри вентиляции тип установки — параметр ahu.unit.unit_type (переменная UnitType): 0 приточная, 1 вытяжная, 2 приточно-вытяжная.

Выбор типа (любой способ), затем сохранение дампа:

Окно терминала
# через веб установки: тип берётся из kind установки, дамп сохраняется
curl -X POST "$P/api/units/<id>/command" -H 'Content-Type: application/json' \
-d '{"cmd":"set_plant_type"}'
# или запись переменной (через веб установки или напрямую) и сохранение дампа
curl -X POST "$P/api/plant/set" -H 'Content-Type: application/json' \
-d '{"name":"PlantType","value":3}'
curl -X POST "$H/api/dump/save" -H "$AUTH"

POST /api/plant/set сохраняет дамп только при записи параметров ahu_*; после записи служебных переменных (PlantType, SimEnable, RunReq …) вызовите POST /api/dump/save рантайма, иначе после перезапуска вернётся прежнее значение.

Смена типа снимает «Работу» (RunReq) и обнуляет выходы прежнего типа. Окно Modbus 4000…4299 и параметры 4300+ занимает активный тип.

По умолчанию SimEnable = TRUE: входы программы ведёт встроенная модель объекта, аварии неполной карты I/O и потери связи не формируются. На объекте установите SimEnable = FALSE и сохраните дамп:

Окно терминала
curl -X POST "$P/api/plant/set" -H 'Content-Type: application/json' \
-d '{"name":"SimEnable","value":false}'
curl -X POST "$H/api/dump/save" -H "$AUTH"

Выходы, привязанные в карте MQTT, публикуются и при SimEnable = TRUE.

Где Как называется
контракт, веб, /api/params <тип>.<группа>.<имя>: ahu.sp.temp, itp.common.season_mode, water.station.pump_count, light.gen.zone_count, chl.unit.machine_type
переменная рантайма (REST, OPC UA, дамп) <тип>_<группа>_<имя>: ahu_sp_temp
Modbus mp02 4300 + id×2 (float)

Описания, пределы и заводские значения — GET /api/params/schema и контракты apps/universal/contract/{vars,itp,water,light,chiller}_contract.json. Время в параметрах хранится в миллисекундах, на шине Modbus — в секундах. Параметры вентиляции пишет PUT /api/params; параметры ИТП, воды и освещения — PUT /api/units/<id>/params; холодоснабжения — напрямую POST /api/variables/chl_… рантайма. После записи напрямую в рантайм вызовите POST /api/dump/save.

На СА-02м модули МР-02м опрашивает мост Modbus → MQTT СА-02м; vPLC читает и пишет их каналы через MQTT. Привязка тегов программы вентиляции к каналам хранится в bindings.json (рядом с server.py веб-интерфейса установки):

"rSupplyTemp": {"topic": "/devices/mr02m-COM2-5/controls/ai_1", "var": "rSupplyTemp",
"is_input": true, "kind": "AI", "class": "ai", "iec": "REAL", "unit": "°C",
"required": true, "scale": 1.0, "offset": 0.0,
"label": "T приточного воздуха", "quality": "q_rSupplyTemp"}

На объекте правят только topic, scale, offset. topic — путь канала без /on; пустой — точка не привязана. Аналоговые входы мост отдаёт физической величиной (scale = 1); аналоговые выходы (y* в процентах) — заводское scale = 0.1.

Порядок привязки:

  1. Найдите имена устройств и каналов: mosquitto_sub -h 127.0.0.1 -v -t '/devices/+/controls/+'.

  2. Впишите topic у нужных тегов — в первую очередь у обязательных при составе установки (окно «Установка», таблица точек ввода-вывода, или POST /api/iomap/autobind — автопривязка по найденным модулям).

  3. Проверьте, что получится:

    Окно терминала
    curl "$P/api/iomap/status"

    Ответ: строки будущей карты mqtt_io_map, маски привязки, обязательные непривязанные точки, pending — ПЛК работает по старой карте.

  4. Примените в ПЛК:

    Окно терминала
    curl -X POST "$P/api/iomap/apply" -H 'Content-Type: application/json' \
    -d '{"force": false, "reload": true, "dry_run": false}'

    Сервер записывает маски привязки AhuIoBnd* в программу, сохраняет дамп, собирает проект .vplc с картой mqtt_io_map, загружает его (POST /api/project/upload) и перезагружает программу (POST /api/program/reload). Перезагрузка останавливает скан на время загрузки: при работающей установке без "force": true — 409 plant_running. Программа старой сборки без масок — 409 program_without_masks.

Как программа использует привязку:

  • маски AhuIoBnd* (бит = тег привязан) хранятся в дампе; раз в 30 с сервер сверяет их с применённой привязкой и восстанавливает, если дамп потерян;
  • q_<тег> = качество MQTT-входа И бит привязки; непривязанный вход недостоверен;
  • обязательная при текущем составе точка без привязки поднимает аварию 61 «неполная карта ввода-вывода» (реакция — останов);
  • при привязанных тегах потеря брокера или моста (MQTT_ok AND BRIDGE_ok = ЛОЖЬ) поднимает аварию 54;
  • при останове скана выходы получают безопасные значения (DO — FALSE, AO — 0).

Частотный преобразователь, опрашиваемый Modbus master vPLC, в bindings.json не привязывают (topic пустой), иначе команда уйдёт и в модуль, и в регистр привода.

Для ИТП, воды, освещения и холодоснабжения привязка задаётся при сборке программы universal и через bindings.json не меняется. [не проверено: порядок привязки этих типов на объекте]

Кроме локальной программы веб установки показывает и управляет установками других контроллеров, которые мост СА-02м публикует в MQTT:

kind Устройство Что даёт
carel (carel-crst, carel-uaria) контроллеры Carel ПВУ значения, состав, аварии с текстами, пуск/стоп, уставки, скорость вентилятора, сброс аварий
mp02-tcp, vplc-tcp прибор MP-02 или второй vPLC по Modbus TCP (шаблон моста mp02-ahu) значения, состояние, аварии, пуск/стоп, уставки, режим
wilo-sk712 шкаф дренажных насосов Wilo SK712 (шаблон) уровень, перелив, насосы, режим, аварии

Добавление:

Окно терминала
curl "$P/api/io/devices" # найти device_id на шине
curl -X POST "$P/api/units" -H 'Content-Type: application/json' \
-d '{"kind":"carel","source":{"device_id":"<id устройства>"},"name":"ПВУ-2"}'
# {"ok":true,"id":"…"}

Снимок внешней установки (GET /api/units/<id>/snapshot) отдаёт значения в тех же именах, что и локальная программа (rSupplyTemp, StateCode, ahu_sp_temp …) плюс _meta (online, stale_s, status_text, pending …). Устройство без публикаций дольше 125 с или с meta/error = "r" — online:false, «НЕТ СВЯЗИ».

Команды (POST /api/units/<id>/command, {cmd, value?, wait?}):

cmd Carel MP-02 по TCP Локальная
start / stop unit_on/on = 1/0 run/on = 1/0 CmdStart / CmdStop
setpoint setpoint/on temp_setpoint/on (5…40) параметр ahu.sp.temp
fan скорость или ступень вентилятора fan_speed_manual/on (0…100) ahu.sp.fan_manual
reset alarm_reset/on alarm_reset/on CmdReset
ack отметка vPLC alarm_ack/on CmdAck

Запись — только в …/on, QoS 1, без retain, по белому списку адаптера. С "wait": true ответ приходит после подтверждения устройством (до 10 с): result — ok, refused, unconfirmed или timeout. Команды ИТП, воды и освещения (season, disinfect, zone_on, pump_start …) — в таблице маршрута POST /api/units/<id>/command документа UNITS_API.md.

Недельное расписание внешних установок — PUT /api/units/<id>/schedule {"enable":true,"days":[[8,0,20,0], …×7]} (days[0] — понедельник; [вкл_ч, вкл_м, выкл_ч, выкл_м]); проверка — раз в 30 с, команда уходит только при смене требуемого состояния.

Счётчик (анализатор сети) СЭ-02м-3 опрашивает мост СА-02м; программа получает его показания по MQTT.

  1. Укажите устройство счётчика у локальной установки:

    Окно терминала
    curl -X PUT "$P/api/units/local/settings" -H 'Content-Type: application/json' \
    -d '{"energy":{"meter_id":"ce02m3-COM2-14","tariff":7.5}}'

    meter_id — id устройства на шине; tariff — ₽/кВт·ч (0…1000), сервер передаёт его в параметр <тип>.energy.tariff.

  2. Включите учёт в программе: параметр <тип>.energy.meter = 1 («СЭ-02м-3»), например ahu.energy.meter.

  3. Примените привязку (POST /api/iomap/apply): в карту добавятся две строки:

Топик Переменная Качество
/devices/<meter_id>/controls/power_total (Вт) MeterPowerW (REAL) MeterPowerQ
/devices/<meter_id>/controls/energy_active_import MeterEnergyWh (LREAL) MeterEnergyQ
  • Коэффициент <тип>.energy.meter_scale (Вт·ч на единицу показания) сервер выставляет сам: 1.0, если мост публикует meta/energy_unit = "Wh", иначе 0.003125 (1/320 Вт·ч); число в plant_config.json (energy.scale) его перекрывает.
  • Достоверность: MeterOk = (meter = 1) AND MeterPowerQ AND MeterEnergyQ; при MeterOk = FALSE суммы не растут.
  • Суммы программы: EnergyRunWh (за пуск), EnergyTodayWh, EnergyYesterdayWh, EnergyTotalWh (UDINT, Вт·ч), PowerPeakTodayW, PowerAvgRunW; граница суток — по часам контроллера.
  • Те же величины — в Modbus mp02 4260…4277 (кВт·ч, float) и в GET /api/energy/local/summary:
Окно терминала
curl "$P/api/energy/local/summary?range=24h"
# пример ответа (значения условные):
# {"available":true,"meter_id":"ce02m3-COM2-14","power_w":4120.0,
# "energy_today_kwh":38.2,"energy_yesterday_kwh":51.7,"cost_rub":286.5,"tariff":7.5,
# "source":"plc","reliable":true, …}

reliable:false и meter_stalled означают, что данные архива недостоверны (разрыв, смена единиц, счётчик стоит при ненулевой мощности).

  • Не открывайте порты 1234, 8088, Modbus и OPC UA в сети за пределами щита/объекта.
  • Задайте api_token, если веб установки не используется; иначе ограничьте доступ к порту 1234 межсетевым экраном.
  • Modbus TCP slave и OPC UA не имеют аутентификации: любой клиент может писать переменные и параметры.
  • Веб установки открывайте через маршрут панели СА-02м /vplc/ (--host 127.0.0.1).
  • Пароли брокера (mqtt_password) и токен в ответах API маскируются, в plant_config.json не хранятся.