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 Log → handleLog |
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/.