Бинарный формат UART протокола iDryer
Все структуры упакованы с #pragma pack(1) — выравнивание отсутствует, байты идут подряд.
Все многобайтовые числа — little-endian (младший байт первым).
Кадр (Frame)
[SOF 1B][VER 1B][FLAGS 1B][KIND 1B][SEQ 1B][LEN 1B][PAYLOAD 0-200B][CRClo 1B][CRChi 1B]
| Offset |
Size |
Type |
Поле |
Значение / Описание |
| 0 |
1 |
uint8 |
sof |
Всегда 0xAA |
| 1 |
1 |
uint8 |
version |
Всегда 1 |
| 2 |
1 |
uint8 |
flags |
Битовые флаги (см. ниже) |
| 3 |
1 |
uint8 |
kind |
Тип сообщения MessageKind |
| 4 |
1 |
uint8 |
sequence |
Счётчик 0–255, инкремент при каждом кадре |
| 5 |
1 |
uint8 |
payloadLength |
Длина payload (0–200) |
| 6 |
0-200 |
bytes |
payload |
Данные (структура зависит от kind) |
| 6+LEN |
2 |
uint16 LE |
crc |
CRC16-CCITT poly=0x1021 init=0xFFFF, little-endian |
Флаги (byte 2):
| Бит |
Маска |
Константа |
Описание |
| 0 |
0x01 |
ACK_REQUIRED |
Требует подтверждения (ACK) |
| 1 |
0x02 |
IS_ACK |
Это кадр ACK |
| 2 |
0x04 |
ERROR |
Payload содержит ErrorPayload |
| 3 |
0x08 |
FRAGMENTED |
Это промежуточный фрагмент |
| 4 |
0x10 |
LAST_FRAGMENT |
Это последний фрагмент |
HelloPayload (0x01) — 86 байт
| Offset |
Size |
Type |
Поле |
Описание |
| 0 |
1 |
uint8 |
role |
0x01=MCU, 0x02=ESP, 0xFF=HelloRequest (триггер) |
| 1 |
1 |
uint8 |
deviceType |
Тип продукта (DeviceType). См. таблицу ниже. 0=Unknown/legacy |
| 2 |
2 |
— |
_pad1 |
Выравнивание |
| 4 |
4 |
uint32 LE |
firmwareVersion |
MAJOR<<16 | MINOR<<8 | PATCH |
| 8 |
4 |
uint32 LE |
workTimeCounter |
Наработка, секунды |
| 12 |
8 |
char[8] |
hardwareVersion |
ASCII строка ("v1.0\0...") |
| 20 |
1 |
uint8 |
unitsCount |
Количество юнитов (0–4) |
| 21 |
48 |
UnitConfig[4] |
units |
Конфигурация юнитов (4 × 12 байт) |
| 69 |
17 |
char[17] |
mcuSerial |
Flash ID RP2040, HEX-строка + \0 |
UnitConfig (12 байт, вложена в HelloPayload):
| Offset |
Size |
Type |
Поле |
Описание |
| 0 |
1 |
uint8 |
unitId |
0–3 |
| 1 |
1 |
— |
_pad1 |
Выравнивание |
| 2 |
2 |
uint16 LE |
capabilities |
Битовые флаги hardware |
| 4 |
4 |
uint8[4] |
scales |
Индексы датчиков весов, 0xFF=не используется |
| 8 |
4 |
uint8[4] |
rfid |
Индексы RFID-ридеров, 0xFF=не используется |
Значения deviceType:
| Значение |
Имя |
Описание |
0x00 |
Unknown |
Legacy / поле не заполнено. Портал трактует как dryer |
0x01 |
Dryer |
iDryer. Кол-во камер портал вычисляет из unitsCount |
0x02 |
Heater |
iHeater |
0x03 |
Telemetry |
Модуль телеметрии |
0x04 |
Link |
Универсальный LINK (standalone) |
0x05 |
LinkII |
Специализированный LINK для iHeater |
0x06..0xFF |
— |
Резерв |
Флаги capabilities:
| Бит |
Маска |
Описание |
| 0 |
0x0001 |
HEATER |
| 1 |
0x0002 |
FAN |
| 2 |
0x0004 |
SERVO |
| 3 |
0x0008 |
RH_AIR_SENSOR |
| 4 |
0x0010 |
TEMP_AIR_SENSOR |
| 5 |
0x0020 |
TEMP_HEATER_SENSOR |
| 6–15 |
— |
Зарезервировано |
HelloAckPayload (0x02) — 37 байт
| Offset |
Size |
Type |
Поле |
Описание |
| 0 |
4 |
uint32 LE |
ipAddress |
IP адрес (little-endian), 0=нет подключения |
| 4 |
33 |
char[33] |
ssid |
Имя WiFi сети, null-terminated, ""=нет |
ipAddress в little-endian: для IP 192.168.1.5 → байты 05 01 A8 C0.
TelemetryPayload (0x10) — 1 + N×7 байт
| Offset |
Size |
Type |
Поле |
Описание |
| 0 |
1 |
uint8 |
count |
Количество юнитов (1–4) |
| 1 |
N×7 |
TelemetryEntry[N] |
units |
Данные юнитов |
TelemetryEntry (7 байт):
| Offset |
Size |
Type |
Поле |
Описание |
| 0 |
1 |
uint8 |
unitId |
0–3 |
| 1 |
2 |
int16 LE |
temperatureC10 |
Температура × 10 (553 → 55.3°C) |
| 3 |
2 |
uint16 LE |
humidityPct10 |
Влажность × 10 (452 → 45.2%) |
| 5 |
1 |
uint8 |
heaterPowerPct |
Мощность нагревателя 0–100% |
| 6 |
1 |
uint8 |
fanOn |
0=выкл, 1=вкл |
WeightsPayload (0x12) — 1 + N×4 байт
| Offset |
Size |
Type |
Поле |
Описание |
| 0 |
1 |
uint8 |
count |
Количество датчиков (1–4) |
| 1 |
N×4 |
WeightEntry[N] |
weights |
Данные весов |
WeightEntry (4 байт):
| Offset |
Size |
Type |
Поле |
Описание |
| 0 |
1 |
uint8 |
sensorId |
0–3 (W1=0 ... W4=3) |
| 1 |
1 |
uint8 |
unitId |
0–3 |
| 2 |
2 |
uint16 LE |
weightGramsC10 |
Вес × 10 (1234 → 123.4 г) |
StatusPayload (0x13) — 133 байт
| Offset |
Size |
Type |
Поле |
Описание |
| 0 |
1 |
uint8 |
count |
Количество юнитов (1–4) |
| 1 |
N×32 |
StatusEntry[N] |
units |
Статус юнитов |
| 1+N×32 |
4 |
uint32 LE |
uptime |
Аптайм устройства, секунды |
StatusEntry (32 байт):
| Offset |
Size |
Type |
Поле |
Описание |
| 0 |
1 |
uint8 |
unitId |
0–3 |
| 1 |
1 |
uint8 |
mode |
DryerMode: 0=Idle, 1=Drying, 2=Storage, 3=Profile, 4=Fault |
| 2 |
4 |
uint32 LE |
sessionNum |
Номер сессии (0=Idle/Fault) |
| 6 |
2 |
int16 LE |
targetTempC10 |
Целевая температура × 10 |
| 8 |
2 |
uint16 LE |
targetHumidityPct |
Целевая влажность % (0=не используется) |
| 10 |
2 |
uint16 LE |
durationMinutes |
Длительность, мин (0=бесконечно) |
| 12 |
4 |
uint32 LE |
elapsedSeconds |
Секунд с начала режима |
| 16 |
4 |
uint32 LE |
stageElapsedSeconds |
Секунд на текущем этапе (PROFILE) |
| 20 |
4 |
uint32 LE |
stageRemainingSeconds |
Секунд до конца этапа (PROFILE) |
| 24 |
4 |
uint32 LE |
totalRemainingSeconds |
Секунд до конца программы |
| 28 |
1 |
uint8 |
currentStage |
Текущий этап (PROFILE, 0-based) |
| 29 |
1 |
uint8 |
totalStages |
Всего этапов (PROFILE) |
| 30 |
1 |
uint8 |
stagePhase |
StagePhase: 0=Ramp, 1=Hold |
| 31 |
1 |
— |
_pad |
Выравнивание |
RfidPayload (0x14) — 37 байт
| Offset |
Size |
Type |
Поле |
Описание |
| 0 |
1 |
uint8 |
event |
RfidEvent: 1=TagDetected, 2=TagRemoved |
| 1 |
1 |
uint8 |
readerId |
0–3 |
| 2 |
32 |
char[32] |
tag |
HEX ID метки, null-terminated (пусто для TagRemoved) |
| 34 |
1 |
uint8 |
unitId |
0–3 |
| 35 |
2 |
— |
_pad[2] |
Выравнивание |
CommandPayload (0x20) — 13 байт
| Offset |
Size |
Type |
Поле |
Описание |
| 0 |
1 |
uint8 |
command |
CommandCode (см. ниже) |
| 1 |
1 |
uint8 |
targetState |
Используется для Start (тип режима) |
| 2 |
1 |
uint8 |
unitId |
0–3 или 0xFF=все юниты |
| 3 |
2 |
— |
reserved[2] |
Зарезервировано |
| 5 |
4 |
uint32 LE |
arg0 |
Аргумент (Start: температура × 10) |
| 9 |
4 |
uint32 LE |
arg1 |
Аргумент (Start: минуты) |
CommandCode:
| Код |
Имя |
Описание |
| 0x01 |
Start |
Запуск (arg0=temp×10, arg1=minutes, targetState=DryerMode) |
| 0x02 |
Stop |
Остановка |
| 0x03 |
Find |
Поиск (мигание) |
| 0x05 |
GetConfig |
Запрос JSON конфига |
| 0x06 |
SetConfig |
Применить настройки |
| 0x07 |
ReadRfid |
Прочитать метку |
| 0x08 |
WriteRfid |
Не реализовано |
| 0x10 |
ResetFault |
Сброс ошибки |
| 0x11 |
WifiStatus |
Запрос IP (MCU → LINK) |
| 0x12 |
ClearErrors |
Очистка EEPROM лога |
ProfilePayload внутри Command (0x20) — 64 байта
Один и тот же MessageKind::Command (0x20) используется для обычной команды и для профильной сушки. Приёмник (MCU) различает формат по длине payload: 13 байт → CommandPayload, 64 байта → ProfilePayload. Логика в uart_bridge.cpp.
| Offset |
Size |
Type |
Поле |
Описание |
| 0 |
1 |
uint8 |
unitId |
0–3 (U1–U4) или согласно прошивке |
| 1 |
1 |
uint8 |
totalStages |
Число используемых этапов (1–10) |
| 2 |
1 |
uint8 |
startStage |
Индекс старта (0-based) |
| 3 |
1 |
— |
_pad |
Выравнивание |
| 4 |
60 |
ProfileStage[10] |
stages |
До 10 этапов по 6 байт |
ProfileStage (6 байт):
| Offset |
Size |
Type |
Поле |
Описание |
| 0 |
2 |
uint16 LE |
temp |
Целевая температура ×10 (60°C → 600) |
| 2 |
2 |
uint16 LE |
ramp |
Разгон до цели, секунды |
| 4 |
2 |
uint16 LE |
hold |
Удержание, секунды |
JSON-команда MQTT и семантика этапов: 07-profile-mode.md.
ConfigChunkPayload (0x30) — 200 байт
| Offset |
Size |
Type |
Поле |
Описание |
| 0 |
2 |
uint16 LE |
transferId |
ID передачи (для разделения сессий) |
| 2 |
2 |
uint16 LE |
totalSize |
Полный размер JSON (только в первом фрагменте, chunkIndex==0) |
| 4 |
2 |
uint16 LE |
chunkIndex |
Индекс фрагмента: 0, 1, 2, ... N-1 |
| 6 |
194 |
bytes |
data |
JSON-данные (до 194 байт) |
Если JSON ≤ 194 байт — отправляется одним кадром с FLAG_LAST_FRAGMENT.
Если JSON > 194 байт — фрагментируется: промежуточные с FLAG_FRAGMENTED, последний с FLAG_LAST_FRAGMENT.
Форматы JSON в data:
- MCU→LINK, полный конфиг:
{"v":8,"units":3,"active":0,"lang":"en","menu":[{"id":3,"t":"val","val":[50,65,85]},...]}
- MCU→LINK, delta:
{"d":{"3":[55,60,55],"81":3}}
- LINK→MCU, set:
{"cmd":"set","id":3,"unit":0,"val":55}
- LINK→MCU, invoke:
{"cmd":"invoke","id":5}
HeartbeatPayload (0x40) — 9 байт
| Offset |
Size |
Type |
Поле |
Описание |
| 0 |
4 |
uint32 LE |
uptimeSeconds |
Аптайм, секунды |
| 4 |
2 |
int16 LE |
wifiRssiDbm |
RSSI (ESP→MCU) или температура MCU (MCU→ESP) |
| 6 |
2 |
uint16 LE |
errorsSinceBoot |
Количество ошибок с перезапуска |
| 8 |
1 |
uint8 |
cloudState |
LinkCloudState (0–7, только ESP→MCU) |
LinkCloudState:
| Значение |
Имя |
| 0 |
Idle |
| 1 |
WifiConnecting |
| 2 |
Provisioning |
| 3 |
Registering |
| 4 |
AwaitingClaim |
| 5 |
Ready |
| 6 |
MqttConnecting |
| 7 |
Online |
errorsSinceBoot: uint16, счётчик с момента сброса питания/рестарта на стороне отправителя кадра. Семантика (ошибки приложения MCU, сбои линка UART и т.д.) задаётся прошивкой отправителя; у референсного Link значение в кадре LINK→MCU не совпадает с полем в кадре MCU→LINK — см. 01-uart.md и 03-mqtt/01-mqtt.md.
wifiRssiDbm: при LINK→MCU — RSSI WiFi в dBm (типично отрицательное число). При MCU→LINK — по соглашению прошивки часто температура MCU или иной показатель; точное значение не задаётся одним числом для всех продуктов.
LogPayload (0x60) — 164 байта
Кадр MessageKind::Log. Типичное направление: MCU→LINK (структурированные события/ошибки приложения контроллера). Строки — C-строки с завершающим \0 в пределах поля; неиспользуемый хвост заполняют нулями.
| Offset |
Size |
Type |
Поле |
Описание |
| 0 |
10 |
char[10] |
severity |
Уровень: например critical, error, warning, info |
| 10 |
20 |
char[20] |
source |
Источник: THERMISTOR, HEATER, … |
| 30 |
32 |
char[32] |
event |
Код события: SENSOR_SHORT, OVER_MAX, … |
| 62 |
100 |
char[100] |
message |
Текст для человека |
| 162 |
1 |
uint8 |
unitId |
0–3 (юнит U1–U4) |
| 163 |
1 |
uint8 |
_pad |
Выравнивание |
В uart_bridge.cpp для Log допускается payloadLength от 0 до MAX_PAYLOAD_SIZE (200); референсный Link в IdryerDevice обрабатывает пакет как LogPayload только если длина не меньше sizeof(LogPayload) (164).
Публикация в MQTT events из бинарного Log не выполняется внутри UartBridge; нужен колбэк setLogHandler и вызов MqttClient::publishEvent в прошивке продукта (в референсном Link — handleLog). См. 01-mqtt.md.
AckPayload (0x11, 0x21, 0x31) — 2 байт
| Offset |
Size |
Type |
Поле |
Описание |
| 0 |
1 |
uint8 |
ackSequence |
SEQ подтверждаемого кадра |
| 1 |
1 |
uint8 |
status |
ErrorCode: 0=OK |
ErrorPayload (0x50) — 4 байт
| Offset |
Size |
Type |
Поле |
Описание |
| 0 |
1 |
uint8 |
code |
ErrorCode (см. ниже) |
| 1 |
1 |
uint8 |
lastSequence |
SEQ кадра, вызвавшего ошибку |
| 2 |
2 |
uint16 LE |
detail |
Доп. информация (ожидаемая/фактическая длина) |
ErrorCode:
| Код |
Имя |
Описание |
| 0x00 |
None |
OK |
| 0x01 |
CrcMismatch |
CRC не совпал |
| 0x02 |
UnknownMessage |
Неизвестный MessageKind |
| 0x03 |
InvalidPayload |
Неверный размер/формат payload |
| 0x04 |
Busy |
Устройство занято |
| 0x05 |
Timeout |
ACK не получен за 700ms × 3 |
| 0x06 |
SequenceMismatch |
Неожиданный номер последовательности |
ClaimStatusPayload (0x71) — 18 байт
| Offset |
Size |
Type |
Поле |
Описание |
| 0 |
1 |
uint8 |
status |
ClaimingStatus: 0=Idle, 1=Provisioning, 2=WaitingClaim, 3=Claimed, 4=Error |
| 1 |
9 |
char[9] |
pin |
PIN-код 8 цифр + \0 (пусто если не WaitingClaim) |
| 10 |
4 |
uint32 LE |
expiresAt |
Unix timestamp истечения PIN |
| 14 |
4 |
uint32 LE |
remainingSeconds |
Секунд до истечения PIN |
ClaimCompletePayload (0x72) — 38 байт
| Offset |
Size |
Type |
Поле |
Описание |
| 0 |
1 |
uint8 |
success |
1=успех, 0=ошибка/таймаут |
| 1 |
37 |
char[37] |
deviceId |
UUID устройства (только для отображения) |
WsEnablePayload (0x73) — 4 байт
| Offset |
Size |
Type |
Поле |
Описание |
| 0 |
1 |
uint8 |
enable |
1=включить WS, 0=выключить |
| 1 |
1 |
— |
reserved |
— |
| 2 |
2 |
uint16 LE |
pin |
PIN 0–9999 (4 цифры, генерируется на MCU) |
WsStatusPayload (0x74) — 6 байт
| Offset |
Size |
Type |
Поле |
Описание |
| 0 |
1 |
uint8 |
state |
WsState: 0=Disabled, 1=Listening, 2=Connected |
| 1 |
2 |
uint16 LE |
pin |
PIN 0–9999 |
| 3 |
1 |
uint8 |
pairedCount |
Привязанных клиентов (0–5) |
| 4 |
1 |
uint8 |
maxClients |
Максимум клиентов (5) |
| 5 |
1 |
— |
reserved |
— |
Примечания
- Все структуры объявлены с
#pragma pack(push, 1) / #pragma pack(pop) — нет неявного выравнивания компилятора.
static_assert в uart_protocol.h гарантируют соответствие размеров структур.
- При добавлении новых полей — соблюдать
pack(1) и обновлять static_assert.