115г. Форматы обмена: TLV, protobuf (nanopb) и CBOR на микроконтроллере
Раздел 115в повторил на Python кадры раздела 194: двоичный пакет из восьми
байт 01 00 09 C4 0C E4 00 00 — номер, температура в сотых долях градуса и
напряжение в милливольтах — в кадре COBS с CRC, всего 12 байт. Пакет короткий, но у него есть слабое место: смысл байтов знает
только тот, кто знает раскладку. Добавили поле — старый разборщик на
компьютере читает чужие байты как свои. Здесь то же измерение записано
тремя самоописывающими способами: TLV, protobuf и CBOR — и для сравнения
текстом JSON.
Каждый формат закодирован своими руками на C, без библиотек, и сверен с
чужими реализациями: protobuf — с библиотекой nanopb на C и с python
protobuf, CBOR — с библиотекой zcbor на C и с пакетом cbor для Python.
Одни и те же байты получены на ПК (gcc в образе stm-sys:1) и на Cortex-M4
в qemu-system-arm -M netduinoplus2 (образ stm-arm:1, вывод через
semihosting, раздел 35а). Размер кода посчитан armclang для F407.
Опоры: nanopb 0.4.9 и zcbor 0.9.1 — модули дерева Zephyr на диске
(modules/lib/nanopb, modules/lib/zcbor); python protobuf 7.35.1 и его
internal/wire_format.py — типы провода и zigzag; пакет cbor 1.0.0 из
окружения Zephyr; README zcbor — про канонический CBOR по RFC 8949. Файлы
раздела — _verify/fmt115e/, сверяет _verify/fmt115e_check.py.
Как это называется в документации
Оригинал Перевод TLV (type, length, value) тег, длина, значение varint число переменной длины: по 7 бит в байте zigzag encoding зигзаг: −1 → 1, 1 → 2, −2 → 3 — малые по модулю числа короткие wire type тип провода: как перешагнуть поле, не зная его field number, tag номер поля; ключ = номер и тип провода schema evolution развитие схемы: новые поля без поломки старых приборов unknown fields чужие (неизвестные) поля major type, additional information старший тип CBOR, добавочные 5 бит первого байта definite / indefinite length длина указана заранее / до байта-ограничителя deterministic (canonical) encoding однозначная запись: одно значение — одни байты preferred serialization предпочтительная запись: самая короткая
115г.1 Одно измерение и пять способов его записать
Сообщение одно и то же во всём разделе: номер 1, 25,00 °C (2500 в сотых), 3300 мВ, как в пакете 194. Для отрицательных и больших чисел — второе: номер 300, −20,00 °C, 3300 мВ.
| Формат | Байт | Что знает разборщик без схемы | Новое поле |
|---|---|---|---|
| двоичный пакет 115в | 8 (в кадре 12) | ничего | ломает раскладку |
| TLV | 11 | где кончается каждое поле | перешагивается по длине |
| protobuf | 8 | номер поля и как его перешагнуть | перешагивается по типу провода |
| CBOR, словарь с числовыми ключами | 11 | всё: числа, строки, словари | перешагивается |
| CBOR, массив | 8 | всё, но поле узнаётся по месту | только в конец |
CBOR, словарь с именами "n", "t", "u" |
14 | всё, и имена | перешагивается |
| JSON | 25 | всё, это текст | перешагивается |
Главное различие — не размер, а самоописание. Двоичный пакет без схемы не прочесть вовсе. TLV и protobuf говорят, где поле кончается, но не что оно значит: смысл номера поля — в схеме. CBOR и JSON несут и тип значения: число, строку, словарь.
115г.2 TLV, varint и zigzag
TLV — самый простой самоописывающий формат: тег (1 байт), длина (1 байт), значение. В опыте значение — целое со знаком, старшим байтом вперёд, в наименьшем числе байт:
size_t tlv_put_int(uint8_t *p, uint8_t tag, int32_t v)
{
uint8_t n = 4; /* убрать лишние старшие байты, не меняя знака */
while (n > 1) {
int32_t top = v >> (8 * (n - 1) - 1); /* старший байт и знаковый бит следующего */
if (top != 0 && top != -1)
break;
n--;
}
p[0] = tag;
p[1] = n;
for (uint8_t i = 0; i < n; i++)
p[2 + i] = (uint8_t)((uint32_t)v >> (8 * (n - 1 - i)));
return 2u + n;
}
Первое измерение — 01 01 01 02 02 09 C4 03 02 0C E4, 11 байт; второе —
01 02 01 2C 02 02 F8 30 03 02 0C E4, 12 байт: номер 300 и −2000 заняли
по два байта. Разборщик читает тег и длину; незнакомый тег перешагивает на
длину, не вникая. Цена — два служебных байта на каждое поле и потолок
длины 255 байт.
varint экономит иначе: в каждом байте 7 бит числа, старший бит — «дальше ещё байт», младшие 7 бит идут первыми. Это основа protobuf:
| Число | varint |
|---|---|
| 0, 1, 127 | 00, 01, 7F — один байт |
| 128 | 80 01 |
| 300 | AC 02 |
| 3300 | E4 19 |
| 16383 / 16384 | FF 7F / 80 80 01 |
| 0xFFFFFFFF | FF FF FF FF 0F — пять байт |
| −2000 как 64-битное | B0 F0 FF FF FF FF FF FF FF 01 — десять байт |
Последняя строка — беда отрицательных: в дополнительном коде у них все
старшие биты единицы, и varint тащит их все. Лечение — zigzag:
(v << 1) ^ (v >> 31), знак уходит в младший бит.
| v | 0 | −1 | 1 | −2 | 2 | 2500 | −2000 | 2147483647 | −2147483648 |
|---|---|---|---|---|---|---|---|---|---|
| zigzag | 0 | 1 | 2 | 3 | 4 | 5000 | 3999 | 4294967294 | 4294967295 |
Те же числа даёт ZigZagEncode из python protobuf; в опыте сверены все.
115г.3 protobuf по спецификации провода и nanopb
Схема — файл meas.proto:
message Measurement { // версия 1
uint32 n = 1; // номер измерения
sint32 t = 2; // температура, сотые доли °C (sint32: отрицательные — zigzag)
uint32 u = 3; // напряжение, мВ
}
На проводе каждое поле — ключ, а за ним значение. Ключ — varint
(номер поля << 3) | тип провода. Типов провода, которыми пользуются,
четыре (так же в wire_format.py: WIRETYPE_VARINT = 0 … WIRETYPE_FIXED32 = 5):
| Тип провода | Что за ним | Поля |
|---|---|---|
| 0 | varint | uint32, sint32, int32, bool, enum |
| 1 | 8 байт | fixed64, double |
| 2 | длина-varint и столько байт | строки, байты, вложенные сообщения |
| 5 | 4 байта | fixed32, float |
Типы 3 и 4 — группы (WIRETYPE_START_GROUP, WIRETYPE_END_GROUP), в схеме
раздела их нет. Свой кодировщик — десяток строк:
size_t meas_pb_encode(const meas_t *m, int version, uint8_t *out)
{
size_t k = 0;
k += pb_uint(out + k, 1, m->n);
k += pb_uint(out + k, 2, zigzag32(m->t)); /* sint32: −1 → 1, 1 → 2, −2000 → 3999 */
k += pb_uint(out + k, 3, m->u);
if (version >= 2)
k += pb_uint(out + k, 4, m->rh);
return k;
}
pb_uint пишет ключ и varint, а нуль не пишет вовсе: в proto3 значение
по умолчанию не передаётся. Результат — 08 01 10 88 27 18 E4 19, 8 байт:
08 — поле 1, тип 0; 10 — поле 2; 88 27 — 5000, zigzag от 2500; 18 —
поле 3; E4 19 — 3300. Второе измерение — 9 байт.
Тип поля решает размер. Та же схема с int32 t вместо sint32 t: для
+25,00 °C разница только в байтах (C4 13 вместо 88 27), а −20,00 °C
занимает 10 байт одним полем, и сообщение растёт с 9 до 17 байт.
float t (градусы) — тип провода 5 и 4 байта младшим вперёд:
15 00 00 C8 41, сообщение 10 байт.
nanopb — библиотека protobuf на C для микроконтроллеров: сообщение —
обычная структура, описание полей — таблица в флеш-памяти. Описание обычно
делает генератор nanopb_generator.py, но ему нужен компилятор схем
protoc, а на машине его нет. Файлы meas.pb.h и meas.pb.c набраны
вручную в том же виде, что пишет генератор:
#define Measurement_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, n, 1) \
X(a, STATIC, SINGULAR, SINT32, t, 2) \
X(a, STATIC, SINGULAR, UINT32, u, 3)
Ошибиться в них трудно: PB_BIND при компиляции проверяет, что поля
структуры помещаются в описание. Самая большая длина сообщения известна
заранее — Measurement_size равно 18 (три поля по ключу и пять байт
varint), и опыт с наибольшими значениями дал ровно 18 байт; для схемы с
int32 — 23.
Сверка трёх реализаций:
| Что | Свой код | nanopb | python protobuf 7.35.1 |
|---|---|---|---|
| измерение 1 | 08 01 10 88 27 18 E4 19 |
то же | то же |
| измерение 2 | 08 AC 02 10 9F 1F 18 E4 19 |
то же | то же |
с int32 и с float |
8 и 10 байт / 17 и 11 байт | то же | то же |
| 1000 случайных измерений | 10003 байта | байт в байт в 1000 из 1000 | та же CRC-32 всех байтов |
Схему для python protobuf protoc тоже не собирал: проверка читает
meas.proto сама и строит описание сообщений в памяти.
115г.4 CBOR: первый байт говорит всё
CBOR (RFC 8949) устроен как «двоичный JSON». Первый байт каждого элемента — старший тип в трёх старших битах и добавочная часть в пяти младших:
| Старший тип | Что | Пример из опыта |
|---|---|---|
| 0 | целое ≥ 0 | 01 — 1; 19 09 C4 — 2500 |
| 1 | целое < 0, записано −1 − v | 39 07 CF — −2000 (1999 = 0x07CF) |
| 3 | текст UTF-8 | 61 6E — строка «n» |
| 4 | массив | 83 — три элемента |
| 5 | словарь | A3 — три пары |
| 7 | float и простые значения | F9 4E 40 — 25,0 в половинной точности |
Добавочная часть 0…23 — само число; 24, 25, 26, 27 — за ним 1, 2, 4 или 8
байт числа старшим вперёд; 31 — длина не указана, элементы идут до байта
FF. Свой кодировщик заголовка:
size_t cbor_head(uint8_t *p, uint8_t major, uint64_t v)
{
uint8_t mt = (uint8_t)(major << 5);
if (v < 24) {
p[0] = mt | (uint8_t)v; /* число прямо в первом байте */
return 1;
}
int n = v <= 0xFF ? 1 : v <= 0xFFFF ? 2 : v <= 0xFFFFFFFFu ? 4 : 8;
p[0] = mt | (uint8_t)(n == 1 ? 24 : n == 2 ? 25 : n == 4 ? 26 : 27);
for (int i = 0; i < n; i++)
p[1 + i] = (uint8_t)(v >> (8 * (n - 1 - i))); /* старшим вперёд */
return 1u + (size_t)n;
}
Измерение словарём {1: 1, 2: 2500, 3: 3300} — A3 01 01 02 19 09 C4 03 19 0C E4,
11 байт; массивом [1, 2500, 3300] — 8 байт, как protobuf; с именами
"n", "t", "u" — 14 байт.
Сверка: пакет cbor дал те же байты для обоих измерений во всех трёх видах,
zcbor с ZCBOR_CANONICAL — для словаря, а на 1000 случайных измерениях
своё, zcbor и cbor совпали в 1000 случаях из 1000. Без ZCBOR_CANONICAL zcbor пишет словарь
неопределённой длины: BF … FF, 12 байт вместо 11. Это тоже правильный
CBOR, и пакет cbor читает его в тот же словарь, но у одного значения
стало две записи — сравнивать сообщения побайтно или считать от них
подпись уже нельзя.
Числа с дробью. Свой кодировщик пишет float самым коротким способом без
потери: 25,0 → F9 4E 40, половинная точность, 3 байта; −20,0 → F9 CD 00.
Пакет cbor на ПК тот же словарь с 25.0 записал через FB — 8 байт
double, сообщение выросло с 11 до 17 байт. Оба варианта правильные, и
разборщику нужно понимать все три размера float: пакет cbor прочитал
половинную точность своего кодировщика как 25.0 и −20.0.
115г.5 Версия схемы и обратная совместимость
Прибор в поле обновляют не сразу, компьютер — тоже. Значит, новое
сообщение попадёт к старому разборщику, и наоборот. Версия 2 схемы
добавляет влажность новым номером поля: uint32 rh = 4; (45 %).
| Опыт | protobuf | TLV | CBOR |
|---|---|---|---|
| сообщение v2 | … 20 2D, 10 байт |
… 04 01 2D, 14 байт |
A4 … 04 18 2D, 14 байт |
| старый разборщик читает v2 | nanopb: успех, n, t, u верные; свой: чужих полей 1 | чужих тегов 1 | чужих ключей 1 |
| новый разборщик читает v1 | nanopb и python: rh = 0 |
rh = 0 |
rh = 0 |
| в v2 пришёл тег 9 из будущей v3 | — | v2 пропустил 1 тег, v1 — 2 | — |
Python protobuf ещё и хранит чужое поле: сообщение v2, прочитанное старой схемой и записанное обратно, совпало с исходным байт в байт. nanopb чужое поле просто выбрасывает.
Что нельзя делать со схемой — тоже проверено:
| Ошибка | Что вышло |
|---|---|
поле 2 было sint32, стало uint32 |
−20,00 °C прочитано как 3999 — «39,99 °C», без единой ошибки (nanopb и python) |
поле 2 было sint32, стало float |
nanopb: отказ wrong wire type; python: t = 0.0 без ошибки, байты поля ушли в чужие |
| сообщение оборвано на байт | nanopb: end-of-stream; python: DecodeError; свои разборщики — код ошибки |
Отсюда правила, общие для всех трёх форматов:
- номер поля — навсегда: не менять его тип и не отдавать другому полю, даже когда старое удалили;
- новое поле — только с новым номером, и старый код должен жить без него;
- в proto3 нуль и «поля нет» неразличимы: если 0 — осмысленное значение,
поле объявляют
optional(по CHANGELOG nanopb — с protoc 3.12), и у него появляется признакhas_<поле>, либо договариваются, что 0 значит «нет данных»; - двоичному пакету вроде 115в нужен байт версии в начале, иначе старый разборщик не узнает, что раскладка другая.
115г.6 Сколько это стоит: байты сообщения и байты кода
Размер сообщения.
| Формат | Измерение 1 | Измерение 2 | 1000 случайных, в среднем | Наибольшее |
|---|---|---|---|---|
| двоичный пакет 115в | 8 | 8 | — | 8 |
protobuf (sint32) |
8 | 9 | 10,0 | 18 |
| TLV | 11 | 12 | 12,6 | 18 |
| CBOR, словарь | 11 | 13 | 13,7 | 19 |
| CBOR, массив | 8 | 10 | — | — |
| JSON | 25 | 28 | 29,1 | — |
Случайные измерения — номер до 100 000, температура от −100,00 до +100,00 °C, напряжение до 5000 мВ. protobuf и CBOR-массив короче TLV, потому что не тратят байт длины; JSON втрое длиннее — цифры текстом и имена полей в каждом сообщении.
Размер кода — armclang 6.24, -Os, Cortex-M4 с FPU, полный образ с
библиотекой C; в таблице — насколько образ вырос против программы без
формата:
| Что | Флеш, байт |
|---|---|
| свой TLV, запись | 176 |
| свой protobuf, запись | 304 |
| свой protobuf, разбор | 644 |
| nanopb, запись | 3204 |
| nanopb, разбор | 4992 |
| свой CBOR, запись словаря | 316 |
zcbor, запись словаря, ZCBOR_CANONICAL |
1548 |
zcbor без ZCBOR_CANONICAL |
868 |
JSON через snprintf, только целые |
692 |
JSON через snprintf с %.2f |
5724 |
Свой кодировщик одного сообщения — сотни байт, библиотека — килобайты:
nanopb и zcbor умеют все типы полей, вложенные сообщения и проверки.
Библиотека окупается, когда сообщений десятки и схема меняется. Один %.2f в
строке формата стоит больше, чем nanopb целиком: библиотечного кода в
образе стало 5824 байта вместо 996. README zcbor предупреждает, что
ZCBOR_CANONICAL увеличивает код, — в опыте на 680 байт.
115г.7 Грабли
Отрицательная температура заняла 10 байт. Поле объявлено int32: varint
тащит все единичные старшие биты. Для чисел со знаком — sint32.
После обновления схемы «39,99 °C» вместо −20,00. Под старым номером поля сменили тип. Ошибки нет ни в nanopb, ни в python — байты честно прочитаны по новой схеме. Номер поля — навсегда.
Python читает, прибор отказывает. Тип провода поля сменился (sint32 →
float): nanopb отвечает wrong wire type, а python тихо кладёт поле в
чужие и отдаёт t = 0.0. Молчаливый нуль хуже отказа.
«Влажность 0 %» у старых приборов. Новое поле proto3 без optional:
«нет поля» и 0 выглядят одинаково.
CBOR от двух библиотек не совпадает побайтно. Одна пишет словарь с
длиной, другая — до FF; одна пишет 25,0 тремя байтами, другая — девятью.
Для сравнения и подписи — однозначная запись (ZCBOR_CANONICAL, самые
короткие числа).
Буфер под сообщение мал. Длина protobuf зависит от значений: 8 байт для
первого измерения, 18 — для наибольших. Буфер — по Measurement_size, а
не по примеру.
Разбор вышел за конец буфера. Длина в TLV или CBOR пришла от чужого —
её сверяют с остатком сообщения до чтения. Свои разборщики возвращают
ошибку на оборванном сообщении, nanopb — end-of-stream.
JSON стоил 5,7 КБ флеш-памяти. В snprintf оказался %.2f. Сотые доли
градуса — целым числом, и печать остаётся 692 байта.
115г.8 Итоги
- Одно измерение: двоичный пакет 8 байт, protobuf 8, CBOR-массив 8, TLV 11, CBOR-словарь 11, JSON 25; на 1000 случайных в среднем protobuf 10,0, TLV 12,6, CBOR 13,7, JSON 29,1 байта.
- varint — 7 бит в байте; отрицательное
int32— 10 байт, zigzag (sint32) превращает −2000 в 3999 и два байта. - Свой protobuf совпал с nanopb и python protobuf на всех векторах и на
1000 случайных измерениях; свой CBOR — с zcbor (
ZCBOR_CANONICAL) и пакетомcbor; байты одинаковы на ПК и на Cortex-M4 в QEMU. - Совместимость: новое поле с новым номером перешагивают все три формата; смена типа под старым номером даёт тихое «39,99 °C»; в proto3 «нет поля» = 0.
- Флеш F407: свои кодировщики — 176…316 байт, nanopb — 3204 на запись и
4992 на разбор, zcbor — 1548; JSON — 692 байта с целыми и 5724 с
%.2f.
Полные исходники
fmt.c — свои кодировщики varint, TLV, protobuf и CBOR.
/* Раздел 115г: свои кодировщики TLV, protobuf и CBOR — см. fmt.h. */
#include "fmt.h"
/* ════ varint и zigzag ════ */
size_t put_varint(uint8_t *p, uint64_t v)
{
size_t n = 0;
while (v >= 0x80) {
p[n++] = (uint8_t)(v | 0x80); /* младшие 7 бит и флаг «дальше ещё» */
v >>= 7;
}
p[n++] = (uint8_t)v;
return n;
}
size_t get_varint(const uint8_t *p, size_t len, uint64_t *v)
{
uint64_t r = 0;
for (size_t i = 0; i < len && i < 10; i++) {
r |= (uint64_t)(p[i] & 0x7F) << (7 * i);
if (!(p[i] & 0x80)) {
*v = r;
return i + 1;
}
}
return 0; /* обрыв посреди числа или больше 10 байт */
}
/* ════ TLV ════ */
size_t tlv_put_int(uint8_t *p, uint8_t tag, int32_t v)
{
uint8_t n = 4; /* убрать лишние старшие байты, не меняя знака */
while (n > 1) {
int32_t top = v >> (8 * (n - 1) - 1); /* старший байт и знаковый бит следующего */
if (top != 0 && top != -1)
break;
n--;
}
p[0] = tag;
p[1] = n;
for (uint8_t i = 0; i < n; i++)
p[2 + i] = (uint8_t)((uint32_t)v >> (8 * (n - 1 - i)));
return 2u + n;
}
size_t meas_tlv_encode(const meas_t *m, int version, uint8_t *out)
{
size_t k = 0;
k += tlv_put_int(out + k, 1, (int32_t)m->n);
k += tlv_put_int(out + k, 2, m->t);
k += tlv_put_int(out + k, 3, (int32_t)m->u);
if (version >= 2)
k += tlv_put_int(out + k, 4, (int32_t)m->rh);
return k;
}
int meas_tlv_decode(const uint8_t *p, size_t len, int version, meas_t *m)
{
int skipped = 0, last = version >= 2 ? 4 : 3; /* сколько тегов знает эта версия */
size_t i = 0;
*m = (meas_t){0};
while (i < len) {
if (len - i < 2 || p[i + 1] > len - i - 2)
return -1; /* длина выходит за конец сообщения */
uint8_t tag = p[i], l = p[i + 1];
const uint8_t *v = p + i + 2;
i += 2u + l;
if (tag < 1 || tag > last) { /* чужой тег: длина известна — просто перешагнуть */
skipped++;
continue;
}
if (l < 1 || l > 4)
return -2;
uint32_t x = (v[0] & 0x80) ? 0xFFFFFFFFu : 0; /* расширить знак */
for (uint8_t j = 0; j < l; j++)
x = (x << 8) | v[j];
if (tag == 1) m->n = x;
if (tag == 2) m->t = (int32_t)x;
if (tag == 3) m->u = x;
if (tag == 4) m->rh = x;
}
return skipped;
}
/* ════ protobuf ════ */
size_t pb_key(uint8_t *p, uint32_t field, uint32_t wt)
{
return put_varint(p, ((uint64_t)field << 3) | wt);
}
static size_t pb_uint(uint8_t *p, uint32_t field, uint64_t v)
{
if (v == 0)
return 0; /* proto3: значение по умолчанию не передаётся */
size_t k = pb_key(p, field, WT_VARINT);
return k + put_varint(p + k, v);
}
size_t meas_pb_encode(const meas_t *m, int version, uint8_t *out)
{
size_t k = 0;
k += pb_uint(out + k, 1, m->n);
k += pb_uint(out + k, 2, zigzag32(m->t)); /* sint32: −1 → 1, 1 → 2, −2000 → 3999 */
k += pb_uint(out + k, 3, m->u);
if (version >= 2)
k += pb_uint(out + k, 4, m->rh);
return k;
}
size_t meas_pb_encode_int32(const meas_t *m, uint8_t *out)
{
size_t k = 0;
k += pb_uint(out + k, 1, m->n);
k += pb_uint(out + k, 2, (uint64_t)(int64_t)m->t); /* int32: отрицательное расширяется до 64 бит */
k += pb_uint(out + k, 3, m->u);
return k;
}
static uint32_t float_bits(float f)
{
union { float f; uint32_t u; } c = {f};
return c.u;
}
size_t meas_pb_encode_float(const meas_t *m, uint8_t *out)
{
size_t k = pb_uint(out, 1, m->n);
float deg = (float)m->t / 100.0f;
if (deg != 0.0f) {
k += pb_key(out + k, 2, WT_I32);
uint32_t b = float_bits(deg);
for (int i = 0; i < 4; i++)
out[k++] = (uint8_t)(b >> (8 * i)); /* fixed32 и float — младшим байтом вперёд */
}
k += pb_uint(out + k, 3, m->u);
return k;
}
int meas_pb_decode(const uint8_t *p, size_t len, int version, meas_t *m)
{
uint32_t last = version >= 2 ? 4 : 3;
int skipped = 0;
size_t i = 0;
*m = (meas_t){0};
while (i < len) {
uint64_t key, v = 0;
size_t k = get_varint(p + i, len - i, &key);
if (!k)
return -1;
i += k;
uint32_t field = (uint32_t)(key >> 3), wt = (uint32_t)(key & 7);
if (wt == WT_VARINT) {
k = get_varint(p + i, len - i, &v);
if (!k)
return -1;
i += k;
} else if (wt == WT_I64 || wt == WT_I32) {
size_t w = wt == WT_I64 ? 8 : 4;
if (len - i < w)
return -1;
i += w;
} else if (wt == WT_LEN) {
k = get_varint(p + i, len - i, &v);
if (!k || v > len - i - k)
return -1;
i += k + (size_t)v;
} else {
return -3; /* типы 3, 4, 6, 7 — группы и мусор */
}
if (field >= 1 && field <= last && wt != WT_VARINT)
return -2; /* знакомое поле пришло с другим типом провода */
if (field == 1) m->n = (uint32_t)v;
else if (field == 2) m->t = unzigzag32((uint32_t)v);
else if (field == 3) m->u = (uint32_t)v;
else if (field == 4 && last >= 4) m->rh = (uint32_t)v;
else skipped++; /* чужое поле: тип провода говорит, сколько перешагнуть */
}
return skipped;
}
/* ════ CBOR ════ */
size_t cbor_head(uint8_t *p, uint8_t major, uint64_t v)
{
uint8_t mt = (uint8_t)(major << 5);
if (v < 24) {
p[0] = mt | (uint8_t)v; /* число прямо в первом байте */
return 1;
}
int n = v <= 0xFF ? 1 : v <= 0xFFFF ? 2 : v <= 0xFFFFFFFFu ? 4 : 8;
p[0] = mt | (uint8_t)(n == 1 ? 24 : n == 2 ? 25 : n == 4 ? 26 : 27);
for (int i = 0; i < n; i++)
p[1 + i] = (uint8_t)(v >> (8 * (n - 1 - i))); /* старшим вперёд */
return 1u + (size_t)n;
}
size_t cbor_int(uint8_t *p, int64_t v)
{
return v >= 0 ? cbor_head(p, 0, (uint64_t)v) : cbor_head(p, 1, (uint64_t)(-1 - v)); /* −1 → 0x20 */
}
size_t cbor_tstr(uint8_t *p, const char *s)
{
size_t n = 0;
while (s[n])
n++;
size_t k = cbor_head(p, 3, n);
for (size_t i = 0; i < n; i++)
p[k + i] = (uint8_t)s[i];
return k + n;
}
size_t cbor_float(uint8_t *p, float f)
{
uint32_t b = float_bits(f);
uint32_t s = b >> 31, e = (b >> 23) & 0xFF, m = b & 0x7FFFFF;
int he = (int)e - 127 + 15;
if ((b & 0x7FFFFFFF) == 0 || (e != 0xFF && he >= 1 && he <= 30 && (m & 0x1FFF) == 0)) {
uint16_t h = (uint16_t)(s << 15);
if (b & 0x7FFFFFFF)
h |= (uint16_t)((uint32_t)he << 10 | m >> 13);
p[0] = 0xF9; /* половинная точность: 2 байта без потерь */
p[1] = (uint8_t)(h >> 8);
p[2] = (uint8_t)h;
return 3;
}
p[0] = 0xFA; /* иначе одинарная: 4 байта */
for (int i = 0; i < 4; i++)
p[1 + i] = (uint8_t)(b >> (8 * (3 - i)));
return 5;
}
size_t meas_cbor_map(const meas_t *m, int version, uint8_t *out)
{
size_t k = cbor_head(out, 5, version >= 2 ? 4 : 3);
k += cbor_int(out + k, 1); k += cbor_int(out + k, m->n);
k += cbor_int(out + k, 2); k += cbor_int(out + k, m->t);
k += cbor_int(out + k, 3); k += cbor_int(out + k, m->u);
if (version >= 2) {
k += cbor_int(out + k, 4); k += cbor_int(out + k, m->rh);
}
return k;
}
size_t meas_cbor_array(const meas_t *m, uint8_t *out)
{
size_t k = cbor_head(out, 4, 3);
k += cbor_int(out + k, m->n);
k += cbor_int(out + k, m->t);
k += cbor_int(out + k, m->u);
return k;
}
size_t meas_cbor_smap(const meas_t *m, uint8_t *out)
{
size_t k = cbor_head(out, 5, 3);
k += cbor_tstr(out + k, "n"); k += cbor_int(out + k, m->n);
k += cbor_tstr(out + k, "t"); k += cbor_int(out + k, m->t);
k += cbor_tstr(out + k, "u"); k += cbor_int(out + k, m->u);
return k;
}
size_t meas_cbor_map_float(const meas_t *m, uint8_t *out)
{
size_t k = cbor_head(out, 5, 3);
k += cbor_int(out + k, 1); k += cbor_int(out + k, m->n);
k += cbor_int(out + k, 2); k += cbor_float(out + k, (float)m->t / 100.0f);
k += cbor_int(out + k, 3); k += cbor_int(out + k, m->u);
return k;
}
/* разбор: заголовок элемента */
static int rd_head(const uint8_t *p, size_t len, size_t *i, uint8_t *major, uint64_t *v, int *indef)
{
if (*i >= len)
return 0;
uint8_t b = p[(*i)++], ai = b & 31;
*major = b >> 5;
*indef = 0;
*v = 0;
if (ai < 24) {
*v = ai;
return 1;
}
if (ai == 31) { /* неопределённая длина: до байта 0xFF */
*indef = 1;
return 1;
}
if (ai > 27)
return 0;
size_t n = (size_t)1 << (ai - 24);
if (len - *i < n)
return 0;
for (size_t j = 0; j < n; j++)
*v = (*v << 8) | p[(*i)++];
return 1;
}
static int skip_item(const uint8_t *p, size_t len, size_t *i, int depth)
{
uint8_t mt;
uint64_t v;
int indef;
if (depth > 8 || !rd_head(p, len, i, &mt, &v, &indef))
return 0;
if (mt <= 1 || (mt == 7 && !indef))
return 1; /* числа, простые значения и float — только заголовок */
if (mt == 2 || mt == 3) {
if (indef || v > len - *i)
return 0;
*i += (size_t)v;
return 1;
}
if (mt == 6)
return skip_item(p, len, i, depth + 1); /* тег + одно значение */
uint64_t items = mt == 5 ? 2 * v : v;
if (indef) {
while (*i < len && p[*i] != 0xFF)
if (!skip_item(p, len, i, depth + 1))
return 0;
return *i < len ? (++*i, 1) : 0;
}
for (uint64_t j = 0; j < items; j++)
if (!skip_item(p, len, i, depth + 1))
return 0;
return 1;
}
int meas_cbor_decode(const uint8_t *p, size_t len, int version, meas_t *m)
{
uint64_t last = version >= 2 ? 4 : 3;
size_t i = 0;
uint8_t mt;
uint64_t cnt, key, v;
int indef, skipped = 0;
*m = (meas_t){0};
if (!rd_head(p, len, &i, &mt, &cnt, &indef) || mt != 5)
return -1;
for (uint64_t j = 0; indef ? (i < len && p[i] != 0xFF) : j < cnt; j++) {
int ki;
if (!rd_head(p, len, &i, &mt, &key, &ki))
return -1; /* обрыв */
if (mt != 0)
return -2; /* ключи в этой схеме — только неотрицательные числа */
if (key < 1 || key > last) {
if (!skip_item(p, len, &i, 0))
return -1;
skipped++;
continue;
}
if (!rd_head(p, len, &i, &mt, &v, &ki))
return -1;
if (mt > 1)
return -2;
int64_t x = mt == 0 ? (int64_t)v : -1 - (int64_t)v;
if (key == 1) m->n = (uint32_t)x;
if (key == 2) m->t = (int32_t)x;
if (key == 3) m->u = (uint32_t)x;
if (key == 4) m->rh = (uint32_t)x;
}
if (indef) {
if (i >= len)
return -1;
i++; /* байт 0xFF — конец словаря */
}
return i == len ? skipped : -1;
}
meas.pb.h — описание сообщений для nanopb в том виде, в каком его пишет генератор.
/* Раздел 115г: описание сообщений meas.proto для nanopb 0.4.9 — в том виде, в каком его пишет
* генератор nanopb_generator.py. Генератору нужен protoc, а его на машине нет, поэтому файл
* набран вручную; правильность сверяет сама nanopb (PB_BIND проверяет размеры и смещения полей
* при компиляции) и опыт: байты nanopb = байты своего кодировщика = байты python protobuf. */
#ifndef PB_MEAS_PB_H_INCLUDED
#define PB_MEAS_PB_H_INCLUDED
#include <pb.h>
#if PB_PROTO_HEADER_VERSION != 40
#error Regenerate this file with the current version of nanopb generator.
#endif
/* Struct definitions */
typedef struct _Measurement {
uint32_t n;
int32_t t;
uint32_t u;
} Measurement;
typedef struct _Measurement2 {
uint32_t n;
int32_t t;
uint32_t u;
uint32_t rh;
} Measurement2;
typedef struct _MeasurementI {
uint32_t n;
int32_t t;
uint32_t u;
} MeasurementI;
typedef struct _MeasurementU {
uint32_t n;
uint32_t t;
uint32_t u;
} MeasurementU;
typedef struct _MeasurementF {
uint32_t n;
float t;
uint32_t u;
} MeasurementF;
/* Initializer values for message structs */
#define Measurement_init_zero {0, 0, 0}
#define Measurement2_init_zero {0, 0, 0, 0}
#define MeasurementI_init_zero {0, 0, 0}
#define MeasurementU_init_zero {0, 0, 0}
#define MeasurementF_init_zero {0, 0, 0}
/* Struct field encoding specification for nanopb */
#define Measurement_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, n, 1) \
X(a, STATIC, SINGULAR, SINT32, t, 2) \
X(a, STATIC, SINGULAR, UINT32, u, 3)
#define Measurement_CALLBACK NULL
#define Measurement_DEFAULT NULL
#define Measurement2_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, n, 1) \
X(a, STATIC, SINGULAR, SINT32, t, 2) \
X(a, STATIC, SINGULAR, UINT32, u, 3) \
X(a, STATIC, SINGULAR, UINT32, rh, 4)
#define Measurement2_CALLBACK NULL
#define Measurement2_DEFAULT NULL
#define MeasurementI_FIELDLIST(X, a) X(a, STATIC, SINGULAR, UINT32, n, 1) X(a, STATIC, SINGULAR, INT32, t, 2) X(a, STATIC, SINGULAR, UINT32, u, 3)
#define MeasurementI_CALLBACK NULL
#define MeasurementI_DEFAULT NULL
#define MeasurementU_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, n, 1) \
X(a, STATIC, SINGULAR, UINT32, t, 2) \
X(a, STATIC, SINGULAR, UINT32, u, 3)
#define MeasurementU_CALLBACK NULL
#define MeasurementU_DEFAULT NULL
#define MeasurementF_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, n, 1) \
X(a, STATIC, SINGULAR, FLOAT, t, 2) \
X(a, STATIC, SINGULAR, UINT32, u, 3)
#define MeasurementF_CALLBACK NULL
#define MeasurementF_DEFAULT NULL
extern const pb_msgdesc_t Measurement_msg;
extern const pb_msgdesc_t Measurement2_msg;
extern const pb_msgdesc_t MeasurementI_msg;
extern const pb_msgdesc_t MeasurementU_msg;
extern const pb_msgdesc_t MeasurementF_msg;
#define Measurement_fields &Measurement_msg
#define Measurement2_fields &Measurement2_msg
#define MeasurementI_fields &MeasurementI_msg
#define MeasurementU_fields &MeasurementU_msg
#define MeasurementF_fields &MeasurementF_msg
/* Maximum encoded size of messages (where known) */
#define Measurement_size 18
#define Measurement2_size 24
#define MeasurementI_size 23
#define MeasurementU_size 18
#define MeasurementF_size 17
#endif
Дальше: раздел 116 «Типовые подходы и алгоритмы: от порога до маленькой нейросети».