Embedded HandbookПриложения
Все разделы › XIII. Другие языки и их сборщики
STM32F407 · уровень средний · 25 мин чтения
Открыть в интерактивном справочнике

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 мВ.

Одно измерение номер 1, 25,00 °C, 3300 мВ побайтно: двоичный пакет 115в 8 байт, TLV 11 байт, protobuf 8 байт, CBOR словарь с числовыми ключами 11 байт, CBOR массив 8 байт, JSON 25 байт; цветом выделены номер, температура, напряжение и служебные байты

Формат Байт Что знает разборщик без схемы Новое поле
двоичный пакет 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; свои разборщики — код ошибки

Отсюда правила, общие для всех трёх форматов:


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 Итоги

Полные исходники

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 «Типовые подходы и алгоритмы: от порога до маленькой нейросети».

Открыть в интерактивном справочнике