Skip to content

MQTT API iDryer (LINK ↔ Backend)

Разработчик продукта: контекст и порядок чтения — 00-for-product-developers.md.

LINK — мост между MCU (UART) и Backend (MQTT). Получает данные от MCU, публикует в MQTT. Для config собирает полный JSON из vals + menu_meta.h.

Константы топиков, QoS и retained: src/mqtt/idryer_topics.h. Разбор команд: src/cloud/command_handler.cpp.

Подключение

  • Client ID: строка serialNumber моста (та же, что в POST /devices/provision и в префиксе топиков). На шине UART в Hello приходит и mcuSerial (RP2040) — для облака портала важен согласованный с backend один идентификатор серийного номера LINK; см. HTTP API портала.
  • Auth (iDryer Portal / EMQX): username = serialNumber, password = deviceToken из ответа POST /devices/provision (секрет в NVS, строка из таблицы Link.token). Брокер проверяет пару (serialNumber, token) через HTTP auth hook backend (MqttAuthService). Legacy: совпадение с полем Device.token до полной миграции на Link.
  • Не путать с JWT пользователя портала: JWT нужен приложению/вебу для POST /devices/claim, а не для MQTT с устройства.
  • Служебно: пользователь backend + MQTT_BACKEND_SECRET — суперпользователь брокера (инфраструктура), не для прошивки LINK.
  • TLS: в продакшене устройства ожидаются на TLS (типично порт 8883); plaintext 1883 может быть закрыт снаружи. На стороне прошивки — MQTT_USE_TLS, корневой CA (например Let’s Encrypt) в root_ca.h. Точный хост/порт задаются конфигурацией окружения, не этим репозиторием.
  • Подписка: idryer/{serial}/commands/# (QoS1).
  • LWT (Last Will): при обрыве TCP брокер публикует в idryer/{serial}/offline сообщение {} (QoS1, retain false). Реализация: src/mqtt/mqtt_client.cpp (connect с аргументами LWT).

Топики

Префикс: idryer/{serial}/

Device → Backend

Суффикс QoS Retained Описание
info 1 да Версии, capabilities, units, mcuSerial, deviceType
telemetry 0 нет Температура, влажность, мощность, вентилятор
status 1 да Режимы, таймеры, сессии
weights 1 нет Веса филамента
rfid 1 да События tag_detected / tag_removed
events 1 нет События и алерты (JSON); источники: UART Log (0x60) и ошибки протокола UART в референсном Link — см. раздел «Топик events» ниже
config 1 нет Полный JSON меню
config/delta 1 нет Изменения настроек

В idryer_topics.h для телеметрии задан ориентир интервала IDRYER_INTERVAL_TELEMETRY_MS (5 с); фактическая частота на шине UART от MCU — 1 с в активном режиме и 15 с в idle (uart_protocol.h: TELEMETRY_ACTIVE_INTERVAL_MS / TELEMETRY_IDLE_INTERVAL_MS). Период публикации в MQTT задаёт логика приложения (cloud state machine), не сам заголовок топиков.

Backend → Device

Топик: idryer/{serial}/commands/<имя>. Имя команды — последний сегмент пути (см. разбор в mqtt_client.cpp).

Поддерживаемые команды (обработчик CommandHandler::handleMqttCommand):

Команда UART / действие
ping Только лог на стороне LINK, без UART
drying CommandPayload: Start, режим Drying
storage CommandPayload: Start, режим Storage
profile Отдельный бинарный кадр Command с телом ProfilePayload (см. 07-profile-mode, 02-binary-format)
stop CommandPayload: Stop
find CommandPayload: Find
get_config CommandPayload: GetConfig
set JSON уходит в MCU через ConfigPush ({"cmd":"set","id",...,"val"}), не через CommandPayload
invoke JSON уходит в MCU через ConfigPush ({"cmd":"invoke","id":…})
read_rfid CommandPayload: ReadRfid
write_rfid Обработчик вызывается, но не реализован (TODO в коде)
clear_errors CommandPayload: ClearErrors (0x12)

Опционально в корне JSON любой команды: timestamp (строка) — если задан callback синхронизации времени, он будет вызван.

Payloadы от устройства

info: версии, топология, capabilities юнитов, mcuSerial, deviceType (опционально). Пример: {"hardwareVersion":"v1.0","firmwareVersion":"1.2.3","workTimeCounter":360000,"unitsCount":2,"units":[...],"mcuSerial":"...","deviceType":"dryer"}. Поле deviceType — одно из 'dryer' | 'heater' | 'telemetry' | 'link' | 'link_ii'. Для legacy-прошивок (не заполняют поле) оно отсутствует в JSON — портал трактует как dryer. См. таблицу DeviceType в 02-binary-format.

telemetry: {"units": [{"unitId": "U1", "temperature": 55.3, "humidity": 45.2, "heaterPower": 80, "fanStatus": true}]} — влажность в процентах с одним знаком после запятой (из UART humidityPct10).
status: {"units": [{"unitId": "U1", "mode": "DRYING", "sessionNum": 42, "target": {...}, "timers": {...}}]}
weights: {"weights": [{"sensorId": "W1", "value": 123.0, "unitId": "U1"}]}
rfid: {"unitId": "U1", "event": "tag_detected", "tag": "DEADBEEF", "readerId": 0}

config: полный JSON меню (~3KB) с названиями из menu_meta.h
config/delta: изменения {"d":{"3":[55,60,55],"81":3}}

Топик events

MqttClient::publishEvent() публикует в events (QoS1, не retained).

Библиотека: UartBridge при кадре Log (0x60) вызывает только logHandler_(payload, len)без MQTT. Ошибки кадра Error (0x50) идут в errorHandler_, тоже без MQTT.

Референсная прошивка iDryer Link (репозиторий idryer-link, src/IdryerDevice.cpp):

Источник JSON (характерные поля)
UART LoghandleLog severity, source, event, message, unitId ("U1"…), timestamp
UART протокол → handleUartError severity: error, source: UART, event: PROTOCOL_ERROR_LOCAL / PROTOCOL_ERROR_REMOTE, message, timestamp

Публикация выполняется при cloud_.isOnline().

Своя прошивка: без setLogHandler (и без вызова publishEvent из обработчиков ошибок) в MQTT в events ничего не появится — API остаётся в MqttClient.

Счётчик errorsSinceBoot в UART Heartbeat: в MQTT референсный Link не публикует значение из входящего heartbeat MCU. В исходящем heartbeat LINK→MCU в это поле записывается внутренний счётчик сбоев линка (ACK с ошибкой, кадр Error), а не обязательно тот же счётчик, что ведёт MCU в своём кадре MCU→LINK. Семантика поля — у отправителя кадра; см. 02-binary-format.md (раздел Heartbeat).

Команды из Backend (примеры JSON)

drying: {"unitId": "U1", "params": {"temperature": 55, "duration": 240}}
Legacy drying (без params): {"unitId": "U1", "targetTemperature": 55.0, "durationMinutes": 240}

storage: {"unitId": "U1", "params": {"temperature": 40, "humidity": 15}} — при отсутствии params используются значения по умолчанию (40°C, 15% RH).

stop: {"unitId": "U1"} — отсутствие или некорректный unitId может трактоваться как «все юниты» (0xFF).

find: {"unitId": "U1"}

get_config: тело может быть пустым; опционально unitId.

set: {"id": 3, "unit": 0, "val": 55}val может быть числом или массивом (копируется в JSON для ConfigPush).

invoke: {"id": 5}

read_rfid: {"unitId": "U1"}

clear_errors: {"unitId": "U1"} или без unitId — поведение парсера см. код (0xFF = все юниты при отсутствии строки).

profile: см. 07-profile-mode.

write_rfid: не реализовано (заглушка в лог).

Для команд, идущих в UART как Command/ProfilePayload, MCU отвечает CommandAck. Для set/invoke используется цепочка ConfigPush + ACK на стороне UART.

Ограничения

  • write_rfid не реализовано (нужна цепочка с фрагментированными кадрами для данных метки).
  • Публикация в events не встроена в UartBridge/облачный слой библиотеки; референс Link вызывает publishEvent из IdryerDevice (см. выше).
  • Home Assistant: отдельный слой (ha_mqtt_client, ha_publisher) и префикс homeassistant/... — не описываются в этом файле; см. исходники в src/mqtt/ и src/cloud/.

См. также