Сообщение под увеличением
Как связать удобный пример с настоящим кодеком?
Здесь собран текущий проектный E0 и отмечены учебные эскизы. Проверенные байты не означают проверенный TLS, MCU или приложение.
Здесь мы опустимся до конкретных полей и байтов. Короткий JSON помогает увидеть идею; двоичный пример проверяет кодек; полная запись наблюдения нужна уже платформе. Это три разных представления, которые должны быть связаны явным преобразованием.
Не будем делать вид, что все поля окончательно заморожены. Фиксируем небольшой учебный профиль и тестовые примеры, а расширения вводим с версией и отрицательными тестами. Так ранняя сборка не запирает будущую систему в случайном формате.
Наблюдение в читаемом представлении#
Ниже приведён диагностический JSON с числовыми ключами закреплённого профиля KQIP 0.2. В JSON ключи записаны строками, а бинарные ID — шестнадцатеричным текстом; на проводе это целочисленные ключи CBOR и byte strings. Соответствие ключей типам и полям задаёт contracts/generated/SPEC_RU.md. JSON служит представлением пакета для чтения, а не альтернативным форматом передачи. UUID, идентификатор загрузки и последовательность не взаимозаменяемы.
{
"1": "11111111111111111111111111111111",
"2": 1,
"3": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"4": 1042,
"5": 120000000,
"6": 120010000,
"7": 234,
"8": -1,
"9": 1,
"10": 0,
"11": 0,
"12": 3,
"13": 2,
"14": 1,
"15": 0
}В Observation ключи 1 и 3 обозначают ID узла и загрузки: каждый показан 32 шестнадцатеричными знаками, то есть 16 байтами. Ключ 2 выбирает канал 1, который descriptor определяет как sensor_temperature. Ключи 7 и 8 задают мантиссу 234 и показатель −1; ключ 9 выбирает единицу 1 — degC. Поэтому измерение равно 23,4 °C. Десятичная пара нормализуется: лишний ноль в мантиссе удаляется, пока показатель можно увеличить в разрешённом диапазоне; ноль имеет показатель 0. На проводе float и NaN в E0 не используются. Остальные обязательные поля, включая delivery_kind, расшифрованы в сгенерированной спецификации профиля.
sample_start_us и sample_end_us задают ограничивающий интервал получения отсчёта в шкале boot_id. Для SHT45 здесь 10 мс — консервативная граница от команды до чтения, а не паспортное интегрирование 15 мс. Сам датчик реагирует на изменение температуры значительно медленнее длительности преобразования. Эти два времени хранятся и используются по-разному. [157]
Одна запись на трёх уровнях#
Возьмём температуру 23,4 °C. На плате драйвер знает режим преобразования, локальное время и статус сенсора. Кодек передаёт эти сведения вместе с идентификаторами и числом. На станции запись дополняется временем приёма, оценкой преобразования часов, зарегистрированным размещением и ссылкой на калибровку. Часть метаданных может храниться в дескрипторе с устойчивой версией, а не повторяться в каждом коротком пакете.
Это разделение экономит сеть без потери смысла. Но оно требует правила: сервер не подставляет нынешнюю ревизию размещения в старую запись, если та была измерена в другом месте. Неизвестная ревизия остаётся неизвестной или требует повторного запроса описания. Сокращение пакета не даёт права придумать отсутствующую метаинформацию.
| Уровень | Что хранится | Что не обещается |
|---|---|---|
| Драйвер и узел | Отсчёт, локальное время, режим, статус | Абсолютно точное время станции |
| Кадр KQIP | Версия профиля, ID, поля и ссылки по схеме | Полный граф комнаты в каждом пакете |
| Логическое наблюдение | Объект, единица, интервал, качество, происхождение | Что поздно полученная запись свежая |
Capability и команда#
Описание канала сообщает тип данных, единицу, операции, пределы и способ подтверждения. relay.switch принимает boolean; датчик lux не становится записываемым только потому, что имеет поле value. Ограничения в описании проверяются также на устройстве: клиентская валидация приложения не является защитой.
{
"1": "7c8b7b8e73104f21842a5d70a9b4b10a",
"2": "22222222222222222222222222222222",
"3": 1,
"4": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"5": 4,
"6": 19,
"7": 38,
"8": 7,
"9": 6969000,
"10": 1,
"11": true,
"12": 0
}В E0 предусловия обязательны: boot_id исполнителя, controller_fence, output_revision и manual_revision. Отсутствующее поле не разрешает безусловную установку. Сначала проверяются действующая авторизация и привязка к boot/fence. Для уже известного command_id в этой области кэш результата проверяется до сравнения изменяемых ревизий: совпадающее тело возвращает прежний результат без GPIO. Иначе потерянный ACK превратил бы успешный повтор в ложный конфликт. Новая команда проходит все текущие предусловия.
Срок нового исполнения фиксируется один раз. Core сохраняет своё время истечения в текущей загрузке и консервативно переводит его в not_after_device_us по интервальной оценке часов. Это поле не увеличивается при повторе. MCU проверяет собственные часы непосредственно перед изменением GPIO; он не запускает новый полный TTL с момента получения.
Исторический малый вектор 0.1: только устройство кадра#
Для объяснения длины сохранён прежний искусственный Hello 0.1: тип 0x0001 и тело {1: 1}, то есть a1 01 01. Эти 27 байт полезны как задача на декодирование; профиль E0 их отвергает по версии. Рабочие поля Hello и новые эталоны определены ниже и в комплекте protocol_r2. [48]
4b 51 42 31 00 01 00 01 00 00 00 18
00 00 00 03 00 00 00 00 00 00 00 01
a1 01 01Первые четыре байта — ASCII KQB1. Далее идут major=0, minor=1, тип=1, flags=0, header_len=24, body_len=3, session_seq=1. Итого 27 байт прикладного кадра, не считая TLS/TCP/IP. При реальной передаче заголовок может прийти частями; один read не обязан вернуть целое сообщение.
Golden vectors сохраняются как независимые файлы: входные байты, ожидаемая структура и ожидаемая ошибка. Для несовместимой major-версии, повторяющегося ключа и слишком большого body_len должны существовать отрицательные примеры. Криптографический handshake не подменяется этим тестом: он проверяется отдельно.
Реестр ошибок#
| Класс | Коды KQIP 0.2 / R3-draft | Поведение клиента |
|---|---|---|
| Формат | INVALID_FRAME (13), INVALID_VALUE (6) | Повреждённый запрос не повторяется бесконечно |
| Совместимость | PROFILE_MISMATCH (12), SYNC_REQUIRED (14) | Нужен совместимый профиль или синхронизация |
| Доступ и привязка | UNAUTHORIZED (1), BOOT_MISMATCH (2), FENCE_MISMATCH (3) | Проверить полномочия и привязку; не отключать TLS |
| Команда | ID_CONFLICT (4), STATE_CONFLICT (8), EXPIRED (9) | Не менять тело прежнего ID; новое намерение требует текущих условий |
| Ручной режим и ресурсы | MANUAL_HOLD (7), RESOURCE_LIMIT (10), STORAGE_FAILURE (15) | Показать ограничение; не обещать исполнение |
| История | RESULT_EVICTED (5), UNKNOWN_COMMAND (11) | Неизвестный результат не доказывает отсутствие действия |
| Успех | OK (0) | Успешный известный результат; не UNKNOWN |
Человекочитаемое сообщение не служит машинным кодом. Локализация русского и английского текста не должна ломать обработку ошибок. Диагностические детали не раскрывают секреты, существование чужих устройств или произвольные внутренние пути.
Планировщик узла и бюджет памяти#
При выборе ESP-IDF или no_std фиксируются реальные области памяти: сеть и криптография, стеки задач, очереди, драйверные буферы, OTA и запас на одновременные операции. Максимальный размер прикладного кадра 4096 байт не означает, что TLS-соединению нужно всего 4096 байт RAM.
Как учебный расчёт можно выделить очередь из 32 нормализованных событий по 96 байт: это 3072 байта полезных записей, ещё без служебных полей контейнера. Очередь 16 команд по 128 байт даёт 2048 байт. Размеры должны проверяться фактическим типом и сборкой. Общая RAM определяется пиковым сценарием BLE onboarding + Wi-Fi + TLS, а не спокойным измерением температуры.
При переполнении некритичной телеметрии допустимы объединение последнего значения и явный счётчик пропусков. Команда не теряется молча: RESOURCE_LIMIT возвращается до действия либо запрос помещается в надёжную очередь с заданным пределом. BUSY не является отдельным кодом исправленного реестра. События кнопки имеют защищённый бюджет очереди, чтобы поток UART радара их не вытеснил.
Временная дисциплина#
Сроки на Linux привязаны к CLOCK_BOOTTIME и host_boot_id. Rust Instant либо таймер Tokio не подменяют этот договор автоматически. После resume Core закрывает прежние рабочие сессии и пересогласует время; после reboot краткоживущий outbox не переисполняется. История фактически отправленных команд остаётся UNKNOWN до свидетельства, а не объявляется «никогда не выполненной». [145]
Clock sync полезен, но не делает разные часы одинаковыми. У записи сохраняется погрешность преобразования; при её росте уменьшается уверенность в порядке событий. Для первой проверки задержки используется полный RTT на одном клиенте. Физическое включение можно дополнительно измерить фотодатчиком и логическим анализатором с общей временной базой.
Совместимость трёх реализаций#
Общий Rust-кодек через UniFFI уменьшает риск расхождения приложения и ядра, но не заменяет независимый тест. MCU может использовать другую среду, ограничения памяти и транспортный стек. Kotlin/Swift glue должен сохранять бинарные данные, cancellation и срок жизни объектов без двойного освобождения и незавершённых подписок. [55]
Для каждого релиза собирается матрица: старое приложение — новый Core; новый Core — старая прошивка; повторное сопряжение после обновления; откат; неизвестная capability. Контракт обещает конкретный диапазон совместимости, а не абстрактное «всё со всем».
Расширенная логическая запись наблюдения включает интервал измерения, неопределённость времени, происхождение, размещение и калибровку. Краткий JSON в этой главе показывает транспортный эскиз, а не все поля такого паспорта. Оценка и прогноз имеют отдельные типы и не записываются в канал настоящего измерения.
Одинаковое намерение — одни и те же байты#
Команда неизменяема целиком, включая not_after_device_us. В повторах меняются только кадр и session_seq. execution_seq монотонно растёт на канал внутри (controller key, controller_fence, boot_id); Core допускает не больше одной новой команды в ожидании на канал. Пропуск числа допустим, повторное применение старого числа — нет. После восстановления Core сначала читает high-water исполнителя.
| Шаг MCU | Правило |
|---|---|
| 1. Действующая привязка | Проверить principal, установку, канал, boot и controller fence; история также требует действующих прав |
| 2. Известный command_id в этой области | Сравнить неизменяемое тело: совпало — вернуть историю без GPIO; иначе ID_CONFLICT |
| 3. Неизвестный ID со старым execution_seq | Номер не выше highwater не исполняется; вернуть RESULT_EVICTED |
| 4. Новая команда | Проверить тип операции, cause, ручной режим, ожидаемые ревизии, срок и локальные ограничения |
| 5. Сериализованное применение | Один владелец канала проверяет условия и меняет выход без гонки с кнопкой |
| 6. Результат | Запомнить известный APPLIED/REJECTED и highwater; текущий Snapshot хранить отдельно |
Проверки шага 5 и изменение выхода выполняет один владелец канала. Между «проверил ручной режим» и «включил GPIO» не вклинивается событие кнопки. Если кнопка уже принята, команда должна увидеть новую manual_revision. Если команда успела первой, следующая кнопка законно изменит выход; поздний ACK не отменит её.
Кэш E0 хранит 64 последних завершённых результата на канал в RAM. Живое исполнение не вытесняется. После вытеснения high-water остаётся до конца boot, поэтому исчезновение записи не превращает старое действие в новое. При переполнении ресурса новый запрос получает RESOURCE_LIMIT до действия. После reset новая boot-привязка запрещает все прежние команды; сохранность эффекта между GPIO и записью результата не обещается. Core сохраняет неизменяемую связь исходного request/intent ID с execution_seq; гарантия ID_CONFLICT по известному ID не означает бесконечного MCU-кэша.
История: A включила свет, её ответ потерян; человек нажал OFF; пришёл повтор A. Узел находит A и возвращает прежний APPLIED с прежней ревизией. Текущее состояние остаётся OFF. Если A уже вытеснена, повтор получает RESULT_EVICTED, также без действия. Это сильнее простого совета «используйте set вместо toggle».
Снять ручной режим — отдельная операция RELEASE_MANUAL (код 2, argument=null), а не скрытое следствие следующего автоматического SET. Она разрешена только для явного действия авторизованного пользователя, проходит те же проверки boot, fence, версий и срока и увеличивает manual_revision. Сам уровень выхода не меняется. Обычный SET имеет код 1 и булев аргумент; локальная кнопка обрабатывается вне сетевого протокола. Позиция длинного нажатия «вернуть автоматику» должна отличаться от сброса и сопряжения.
Время доставки нельзя начинать заново при получении#
Обозначим C — BOOTTIME станции, D — часы MCU в текущей загрузке, θ = D − C. Станция отправляет пробу в c1; MCU принимает в d2 и отвечает в d3; ответ приходит в c4. При неотрицательных задержках и ограниченном дрейфе смещение на время пробы лежит между d3 − c4 и d2 − c1. Половина RTT не используется как доказанная задержка в одну сторону. Границы расширяются на ошибки отметок и доказанный дрейф с момента пробы. [165]
θ_min = d3 − c4 − ε
θ_max = d2 − c1 + ε
not_after_device = core_deadline + θ_min − drift_marginПример в миллисекундах: c1=1000, d2=5008, d3=5009, c4=1020. До дополнительных ошибок θ лежит в [3989; 4008]. Для core_deadline=3000 и общего запаса 20 мс срок MCU выбирается 6969. Пакет, впервые дошедший после этого числа, не включает GPIO. Арифметика выполняется в широком знаковом типе; отрицательный срок, переполнение и недостаточный бюджет означают отказ, а не оборачивание uint64.
В первом опыте ширина допустимого интервала часов — не более 50 мс, возраст пробы — не более 30 с; это цели квалификации. Бюджет ошибки включает дрейф MCU, ошибку timestamp и допустимую частотную коррекцию часов станции. CLOCK_BOOTTIME учитывает suspend, но не является RAW-шкалой без коррекции частоты. При неизвестной границе срочная команда не допускается. Actuator E0 не спит; suspend/resume, смена источника часов и boot инвалидируют привязку. Непосредственно до GPIO проверяется now_device < not_after_device. [145]
Четыре вида сроков#
| Срок | Шкала и восстановление |
|---|---|
| Новая команда / ActionProposal | BOOTTIME Core → консервативный срок MCU; reboot не продлевает |
| ManualOverride E0 | До явного освобождения; при reboot BOOT_INHIBIT |
| Календарное расписание | UTC + качество; при неизвестной дате не «догонять» опасные события |
| Приглашение / обновление | Проверенный срок и отзыв; неизвестное UTC не разрешает обход проверки |
UNKNOWN описывает недостаток знания об отправленном действии. StatusQuery может уточнить его позже; это не команда повторить действие. CANCELLED гарантируется только до передачи исполнителю. После DISPATCHED запрос отмены не обещает отменить уже состоявшийся физический эффект. При переключении телефона с Wi-Fi на мобильную сеть приложение сначала спрашивает статус прежнего ID.