ПротоколГлава 21 / 51

KQIP: свой язык устройств

Чем собственный протокол отличается от произвольного JSON?

Протокол описывает смысл, пределы и восстановление. Двоичная упаковка — только один из его слоёв.

Протокол — это договор не только о байтах, но и о поведении. Что означает подтверждение? Можно ли повторить команду? Когда сообщение считается старым? Что делать с новой версией поля?

KQIP задаёт эти правила для наших устройств. TCP доставляет поток байтов, TLS защищает соединение, а собственный слой определяет границы сообщений и их смысл. Такое разделение позволяет сосредоточиться на поведении экосистемы, не изобретая криптографию.

KQIP — Koobique Interface Protocol, собственный прикладной язык Koobique. Текущий проектный профиль — E0 поверх кадра 0.2; совместимых реализаций пока нет. Для первого узла выбираются TCP, TLS 1.3 и ограниченный CBOR. Прежний 27-байтовый Hello 0.1 оставлен в главе 22 только как учебный разбор, не как рабочий handshake.

Почему не собственный UDP сразу#

TCP снимает с первой реализации часть задач доставки и контроля потока. Это не означает его оптимальность для каждого будущего носимого датчика. BLE, QUIC или UDP-профиль могут появиться позднее при конкретной необходимости. Прикладные идентификаторы, команды, ошибки и синхронизация должны переноситься между транспортами без изменения смысла.

TLS обеспечивает защищённый канал; права доступа и допустимость команды проверяются отдельно. CBOR удобен компактным представлением и возможностью ограниченной реализации. minicbor поддерживает сценарии без стандартной библиотеки. Использование этих механизмов не отменяет собственную спецификацию. [47][48] [49][50]

Кадр 0.2 и профиль E0#

A26Предлагаемая структура сетевого кадра. Версия формата фиксируется независимо от памяти Rust-структур.
ПолеРазмерE0-R2
magic4 байтаKQB1 — постоянная сигнатура семейства
major / minor1 + 10 / 2; другой профиль до согласования запрещён
message_type2Реестр типов E0-R2
flags20; неизвестные биты запрещены
header_len224, без расширения в E0
body_len4Не более 4096 байт, затем проверка лимита типа
session_seq8С 1, отдельно в каждом направлении нового TLS-соединения

Многобайтовые числа заголовка передаются в network byte order. Тело не является memcpy Rust-структуры: выравнивание, endian и enum representation не входят в переносимый контракт. Ключи CBOR имеют закреплённые номера и смысл.

В исправленном проектном профиле E0 / KQIP 0.2, revision R3-draft тело ограничено 4096 байтами, вложенность — 8 уровнями, строка — 128 байтами UTF-8, byte string — 1024 байтами, массив — 256 элементами, карта — 32 полями; конкретное поле может иметь меньший предел. Запрещены повторные ключи, indefinite-length, теги, floats, NaN и Infinity. Измерения используют нормализованную целую мантиссу и десятичный показатель. Канонические значения находятся в contracts/registry.json; таблицы и схемы генерируются из него. Bulk/OTA при необходимости использует отдельный ограниченный канал: приоритет очереди не обгоняет байты, уже переданные TCP. [48]

Сообщения и совместимость#

Канонический набор KQIP 0.2: Hello, Ready, Describe, Snapshot, Observation, Event, Command, CommandResult, StatusQuery, Resync, Ping, Pong, TelemetryAck и Error. Имена HelloAck, CommandStatusQuery, SyncRequest и SyncReply не являются дополнительными типами этого профиля. Успешный TLS ещё не разрешает команды: проверяются связанная с ключом идентичность, роль и установка, профиль, дескриптор, snapshot и пригодная оценка часов. Полная машина соединения и её интероперабельность остаются отдельной приёмкой.

В спецификации нужны примеры корректных байтов, заведомо неправильные пакеты и ожидаемые ошибки. Один набор golden vectors проверяет реализации KQIP на Core и MCU и любую дополнительную сторону, если она явно получает роль участника этого протокола. Мобильный клиент по умолчанию обращается к отдельному Client API. Версия устройства, версии capability и версия кадра — отдельные величины.

Идентификаторы и повторы#

Узел имеет постоянный ID, каждая загрузка — boot_id, каждое наблюдение — номер последовательности. Команда имеет 128-битный command_id, выбранный инициатором и сохраняемый при повторе. Session_seq не заменяет command_id: после переподключения прикладной смысл команды остаётся прежним.

set(on) удобнее toggle, но одной идемпотентности значения недостаточно. Между двумя доставками пользователь мог нажать OFF. Повтор прежнего ID тогда возвращает сохранённый результат без нового действия; при отсутствии записи срабатывает защита от старого execution_seq. Для дозирования, шага мотора и других необратимых операций E0 не выдаёт допуска. Повтор toggle инвертировал бы уже изменённый выход, тогда как SET задаёт требуемое значение явно. Сбой между физическим эффектом и записью результата всё равно оставляет неопределённость: гарантия exactly-once для физического мира не заявляется.

Когда команда считается принятой#

A27Пять раздельных записей: намерение, попытка, исполнение, текущее состояние и свидетельство эффекта. События попадают в приложение через Core; стрелки показывают логическую последовательность. Поздний ACK не возвращает старый уровень выхода.

ACCEPTED означает транзакционную запись намерения в Core. DISPATCHED — попытку передачи, не доставку. RECEIVED относится к получению MCU, APPLIED — к историческому результату операции. Текущее состояние хранится отдельным Snapshot. VERIFIED допустимо лишь как состояние отдельного EffectEvidence; оно не делает исторический ON текущим после более нового OFF. UNKNOWN означает недостаток знания и может уточняться позднее.

Первое получение после not_after_device_us не разрешает действие. Повтор уже выполненной команды возвращает исторический результат, даже если срок нового исполнения истёк: таймер не стирает прошлое. StatusQuery только читает историю и не может снова включить выход. По окончании срока новая попытка — новый ID, новое решение и подтверждённый пользовательский смысл, а не скрытое продление старого намерения.

Порядок, snapshots и большие потоки#

Snapshot содержит boot_id, controller_fence, output_revision, manual_revision, выход и ручной режим. Старый ACK не обновляет плитку. История с меткой backfill не становится живым триггером. OTA использует отдельный bulk-канал; его пауза при управляющей нагрузке не заменяет испытания конкуренции за радио и CPU. Для профиля с OTA образ передаётся ограниченными фрагментами с проверяемыми offset, длиной и принадлежностью одному подписанному образу. Размер фрагмента ограничивается до выделения буфера; бесконечный CBOR-массив или всё содержимое образа в одном сообщении не допускаются. Чанки и восстановление передачи остаются отдельной приёмкой E1.

TCP гарантирует порядок байтов в одном соединении, но не создаёт общий порядок между разными устройствами. Станция не должна делать вывод, что датчик A измерил раньше B только потому, что его пакет первым попал в сокет.

Безопасная обработка#

Длины проверяются до выделения буфера. Handshake ограничен по времени и числу параллельных попыток. Неизвестный peer не получает бесконечный ресурс парсера. CBOR-парсер проверяется fuzz-тестами; журнал не печатает неограниченную строку из пакета.

TLS 0-RTT для управляющих действий в первом профиле отключается из-за риска повторного воспроизведения. Дополнительный CRC не является заменой аутентификации. Перед изменением версии сохраняются тесты старого поведения и правила отказа от несовместимого соединения. [50]

Реестр E0 и жизненный цикл соединения#

ТипНазваниеНазначение
0x0001 / 0x0002Hello / ReadyВерсия, роль, ID, boot, лимиты; затем допуск
0x0010 / 0x0011Describe / SnapshotОписание и состояние, включая ручной режим
0x0020 / 0x0021Observation / EventОтсчёт и дискретное событие; live отделён от backfill
0x0030 / 0x0031Command / CommandResultОграниченная команда и исторический результат
0x0032 / 0x0033StatusQuery / ResyncЧтение результата и запрос согласованного состояния
0x0040 / 0x0041Ping / PongЖизнь канала и четыре метки времени
0x0042TelemetryAckПодтверждённая граница хранения и кредит
0x00FFErrorКод, ссылка на запрос; без секретов

TLS → Hello в обе стороны → проверка роли/версии/лимитов → Ready → запрос Snapshot/Describe → синхронизация → ACTIVE. Command до ACTIVE отклоняется. После смены boot или descriptor Core снимает допуск автоматизации до повторной проверки. Несовместимость 0.1 и 0.2 не маскируется minor-обновлением: обе стороны E0 обязаны явно поддерживать 0.2.

session_seq — порядковый номер кадра в одном направлении одной TLS-сессии. На reconnect начинается с 1; он обнаруживает ошибку потока, а не служит защитой от повторного физического действия. Для связи ответов используется command_id либо поле reply_to. Magic KQB1 оставлен как историческая сигнатура; цифра не объявляется номером текущего профиля.

Строгий CBOR без ловушек сравнения#

E0 принимает только детерминированное кодирование: кратчайшие целые, упорядоченные числовые ключи, конечные длины контейнеров. Повтор ключа, float любого размера, NaN/Infinity, теги, indefinite-length и неизвестный ключ 1…127 отклоняются. Поля 128…255 могут быть только документированными некритичными расширениями; права, сроки, версии и запреты нельзя поместить туда. Новое обязательное поле требует нового профиля. Подписанные билеты COSE имеют отдельную схему контейнера и не отменяют ограничения обычного сообщения. [48][149]

Общие пределы: тело 4096 байт, вложенность 8, map до 32 элементов, array до 256 элементов, строка до 128 байт, bstr до 1024 байт, если тип не задаёт меньший предел. ID — ровно 16 байт; counters — uint64 с проверкой переполнения. Длины и типы проверяются до резервирования памяти. Декодер не должен принимать true как целое 1 или выбирать «последнее» значение повторного ключа.

Два канала и ограниченные очереди#

В E0 управление и небольшая текущая телеметрия используют основное TLS/TCP-соединение. OTA и длинная история — отдельное TLS-соединение с той же проверенной идентичностью и ограничением скорости. Приоритет в очереди не позволяет обогнать байты, уже переданные одному TCP-потоку. Разделение соединений также не отменяет конкуренции за Wi-Fi, flash и CPU; обновление не проходит допуск без измерений задержки кнопки и команд под нагрузкой.

Начальные сетевые лимиты: handshake до 5 с, до 4 незавершённых handshake на Hub суммарно для E0, один управляющий и один bulk-сеанс на узел, Ping раз в 10 с и разрыв после трёх отсутствующих ответов. Это проектные значения с возможностью измеренного изменения. TCP_USER_TIMEOUT применяется только там, где его поддерживает стек. Потеря heartbeat сообщает о канале, но не доказывает пустоту комнаты.

TelemetryAck содержит boot_id, channel_id, durable_through_seq и credit. Курсор продвигается только по непрерывно сохранённому диапазону либо после явно записанного Gap. «Получено в RAM» и «сохранено» не смешиваются. Узел не освобождает надёжный буфер по подтверждению только сетевого приёма. Backfill остаётся историей даже при только что установленном соединении.

Почему собственный протокол — осознанная стоимость#

CoAP + OSCORE/EDHOC, Matter и MQTT-профили остаются альтернативами транспорта и интеграции. SenML подходит для обмена простыми сенсорными величинами. KQIP выбран ради собственного учебного проекта и единого договора исполнения, не потому, что доказан быстрее готовых протоколов. При экспорте в SenML нельзя терять boot, интервал, калибровку и размещение: невыразимые поля идут в сопроводительные метаданные. [169][170][168]

Найти в книге

Поиск по всем главам, схемам и техническим разделам.