Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
CMF-Watch-Pro-2-BLE-Protocol — Обратно спроектированный протокол BLE для CMF Watch Pro 2, документирующий структуру GATT, зашифрованные командные фреймы AES-128-CBC, процедуру аутентификации и синхронизацию данных о здоровье для разработки альтернативного сопутствующего приложения. | Kitploit
Инструменты/GitHubGitHub/joshuapassos/cmf-watch-pro-2-ble-protocol
Безопасность встроенных системБезопасность BluetoothБезопасность IoTОбратная инженерияБезопасность беспроводных сетейКриптографияМобильная безопасностьБезопасность оборудования и IoTАнализ Прошивок

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться
GitHubjoshuapassos/cmf-watch-pro-2-ble-protocol

CMF-Watch-Pro-2-BLE-Protocol

Обратно спроектированный протокол BLE для CMF Watch Pro 2, документирующий структуру GATT, зашифрованные командные фреймы AES-128-CBC, процедуру аутентификации и синхронизацию данных о здоровье для разработки альтернативного сопутствующего приложения.

РепозиторийСайт
3281 месяц назадЕщё не проверено

CMF Watch Pro 2 — протокол BLE (реконструированный)

Неофициально. Этот документ описывает протокол Bluetooth Low Energy (BLE) часов CMF Watch Pro 2 (CMF by Nothing), восстановленный методом обратной разработки для альтернативного приложения-компаньона. Он не связан с Nothing/CMF и не одобрен ими. Используйте на свой страх и риск.

Все многобайтовые целые числа в заголовке кадра и кодах операций — big-endian. Целые числа внутри полезной нагрузки команд — little-endian, если не указано иное (это отражает прошивку устройства) — остерегайтесь исключений (GOALS_SET, GPS_PUSH, смещение/длина массовой передачи — big-endian).

Маркеры достоверности

Каждое неочевидное утверждение ниже помечено способом его установления:

  • ✅ проверено на устройстве — наблюдалось в расшифрованном живом захвате или проверено на реальных часах.
  • 🔎 из прошивки / RE APK — извлечено декомпиляцией прошивки (1.0.0.73) или официального APK (3.5.7); согласуется с кодом, но не протестировано в рантайме.
  • 🟡 частично доказано — установлено структурно (офлайн, по корпусу или через RE), но оставшийся шаг требует часов и не был выполнен.
  • ⚠️ [неопределённо] — предположительно, не подтверждено; может быть неверно.

Если более поздний раздел исправляет более ранний, ранний текст сохраняется с указателем, а не удаляется — знание того, какие чтения были опробованы и опровергнуты, избавит следующего человека от того же обходного пути.

Тестовое устройство для всех захватов: CMF Watch Pro 2-5485, прошивка 1.0.0.73, серийный номер CI04102520008192, MCU Actions ATS3089C (Cortex-M4), экран 466×360.


1. Структура GATT

Телефон является GATT-клиентом; часы — периферийным устройством, рекламирующимся как CMF Watch Pro 2-XXXX (4 шестнадцатеричных символа).

Включите уведомления, записав 01 00 в каждый CCCD (00002902-…). Канал команд (fff1/fff2) передаёт кадрированный протокол ниже. Канал оболочки (77d4…) передаёт обычный текст в стиле AT (например, AT GETSECRET; см. §14). Канал данных (02f0…) передаёт большие бинарные блоки (циферблат, прошивка, AGPS), координируемые управляющими кодами операций на канале команд.

UUID сервисов — часы рекламируют ~10 основных сервисов. Перечислены на реальном устройстве: 0xfff0 (команды), 0x180f (батарея), 0x180a (информация об устройстве), 0xefe7, 0xffd0, 02f00000-…ffe0 и 02f00000-…fe00 (данные), 77d4e67c-2fe2-2334-0d35-9ccd078f529c (оболочка / сопряжение), e49a3001-f69a-11e8-8eb2-f2801f1b9fd1, f48a23c0-f69a-11e8-8eb2-f2801f1b9fd1.

⚠️ UUID сервиса оболочки — 77d4e67c-…, а не 77d4ff00-…. В более ранних версиях этого документа предполагалось, что сервис разделяет префикс ff00 своих характеристик (77d4ff01/77d4ff02, §14) — это не так, по крайней мере на устройстве, на котором это проверялось (находка из freethinkel/fmc, см. §Источники). UUID характеристик не изменились. Не проверено, стабилен ли 77d4e67c между устройствами — перечисляйте, а не зашивайте жёстко.

🌐 Примечание о Web Bluetooth. Chromium обнаруживает только те сервисы, которые страница перечислила в optionalServices, даже для нефильтрованного вызова getPrimaryServices() — страница, перечисляющая 3 сервиса, видит 3, тогда как chrome://bluetooth-internals (собственный C++-слой Chrome, без ограничений) показывает все 10. Если вы пишете браузерный клиент, перечислите все UUID выше заранее, иначе сопряжение завершится ошибкой с сервисами, которые явно существуют. Web Bluetooth нет в Firefox/Safari; требуется жест пользователя + HTTPS/localhost.

✅ Целая реальная сессия прошла на единственном канале команд — во время 160-секундного захвата при интенсивном использовании не было трафика на каналах данных/прошивки или оболочки, кроме явной передачи OTA/циферблата.


2. Формат кадра (0xF5)

Каждое сообщение канала команд обёрнуто в один или несколько кадров с 11-байтовым заголовком:``` +------+-----------+--------+-------------+-------------+--------+-------------------+ | 0xF5 | chunkLen | cmd1 | chunkCount | chunkIndex | cmd2 | chunk bytes … | | 1 B | 2 B (BE) | 2 B BE | 2 B BE | 2 B BE | 2 B BE | chunkLen bytes | +------+-----------+--------+-------------+-------------+--------+-------------------+ __________________________ 11-byte header ____________________________/

root@kitploit:~
- `cmd1`/`cmd2` вместе образуют **опкод** (см. §6). 🔎 подтверждено по фрейм-билдеру официального приложения (`C6117b.m30831g`).
- `chunkCount` = общее количество чанков для этой команды; `chunkIndex` — **с 1**.
- `chunkLen` = количество байт `chunk` в этом фрейме.
- Одна BLE-запись может фрагментироваться по MTU канала; приёмник буферизует сырые байты и заново извлекает полные фреймы. Большие полезные нагрузки разбиваются на несколько чанков (одинаковые `cmd1/cmd2`, возрастающий `chunkIndex`) и собираются по порядку.

### Соглашение об опкодах (✅ подтверждено на проводе)

- `cmd1 = 0xFFFF`: `cmd2` в диапазоне `0x80xx`/`0x90xx` = телефон→часы (запрос/установка); `0x00xx`/`0xa0xx` = часы→телефон (ответ). Пары сопоставляются по младшему байту (`0x9055`↔`0xa055`, `0x8051`↔`0x0051`).
- специфичный для функции `cmd1`: суффикс `cmd2` = `0x0001` **SET**, `0x0002` **GET**, `0x0003` **ACK**.

### Тело чанка

Для каждого чанка тело — это `payloadPiece ‖ CRC32_LE(payloadPiece)` (4-байтный CRC, little-endian, zlib/IEEE). Если команда **зашифрована** (см. §3), весь `payloadPiece ‖ CRC` затем шифруется AES-128-CBC/PKCS7, и этот шифротекст становится `chunk` фрейма.

**Особенность открытого текста:** для опкодов без шифрования часы *учитывают* 4-байтный CRC в `chunkLen`, но **не** передают его. Поэтому при декодировании фрейма с открытым текстом фактическая длина данных равна `chunkLen − 4`. (Зашифрованные фреймы несут CRC внутри шифротекста как обычно.)

Размер чанка (чтобы зашифрованные чанки ложились на границы AES-блоков), при `maxWrite = mtu − 3`:
- зашифрованный: `floor((maxWrite − 11) / 16) * 16 − 4 − 1`
- открытый текст: `maxWrite − 11 − 4 − 1`

✅ Все наблюдаемые значения `chunkLen` зашифрованных фреймов были кратны 16 (выравнивание по блокам соблюдается).

---

## 3. Криптографические примитивы

- **AES-128-CBC** с дополнением **PKCS7** и **фиксированным IV** (из прошивки `CmfCharacteristic.AES_IV`):
  `50 51 52 53 54 55 56 57 60 61 62 63 64 65 66 5A`.
- **CRC32** (zlib/IEEE), выдаётся как 4 байта little-endian.
- **SHA-256** по конкатенации частей.

Вывод ключа:```
authkey      = SHA256( rnd1 ‖ rnd2 ‖ secret )[0..16]      // persisted across sessions
sessionKey   = SHA256( nonce ‖ authkey )[0..16]           // per connection
  • secret = 16-байтовый секрет устройства (получается с часов через shell-команду AT GETSECRET → GETSECRET:<32-hex>,OK).
  • rnd1 = 16 случайных байт, выбранных телефоном; rnd2 = 16 случайных байт с часов.
  • nonce = байты из ответа nonce часов.

После установки ключа все кадры командного канала шифруются AES, кроме открытых опкодов, перечисленных в §5.

✅ Обе производные проверены: authkey, восстановленный с рутованного телефона из ntwatch.db, совпал со значением, полученным из захваченных rnd1/rnd2/secret; sessionKey, воспроизведённый из захваченного nonce, расшифровывает живые кадры.


4. Аутентификация / процедура сопряжения

Два пути входа используют один и тот же хвост nonce/confirm.

4.1 Первое сопряжение (при наличии секрета устройства)```

phone → (shell) AT GETSECRET watch → (shell) GETSECRET:<32hex>,OK phone: rnd1 = random16 ; signed1 = SHA256(rnd1 ‖ secret) phone → AUTH_PAIR_REQUEST (plaintext) payload = rnd1(16) ‖ signed1(32) // 48 B watch → AUTH_PAIR_REPLY (plaintext) payload = rnd2(16) ‖ signed2(32) // 48 B phone verifies signed2 == SHA256(rnd2 ‖ secret) phone: authkey = SHA256(rnd1 ‖ rnd2 ‖ secret)[0..16] → set crypto key = authkey phone → AUTH_PHONE_NAME (encrypted) payload = 0xA5 ‖ model(UTF-8) // e.g. "CMF Watch Pro 2" watch → AUTH_WATCH_MAC (encrypted) phone → AUTH_NONCE_REQUEST (encrypted) payload = 0xA5 watch → AUTH_NONCE_REPLY (encrypted) payload = nonce phone: sessionKey = SHA256(nonce ‖ authkey)[0..16] → set crypto key = sessionKey phone → AUTHENTICATED_CONFIRM_REQUEST (encrypted) payload = 0xA5 watch → AUTHENTICATED_CONFIRM_REPLY (encrypted) → state = Initialized

root@kitploit:~
При `AUTH_FAILED (0xFFFF,0xA061)` или несовпадении подписи аутентификация завершается неудачей.

### 4.2 Повторное подключение (authkey уже известен)```
        set crypto key = authkey (persisted)
phone → AUTH_PHONE_NAME      (encrypted)  payload = 0xA5 ‖ model
watch → AUTH_WATCH_MAC       (encrypted)
phone → AUTH_NONCE_REQUEST   (encrypted)  payload = 0xA5
watch → AUTH_NONCE_REPLY     (encrypted)  payload = nonce
        sessionKey = SHA256(nonce ‖ authkey)[0..16]   → set crypto key = sessionKey
phone → AUTHENTICATED_CONFIRM_REQUEST (encrypted)  payload = 0xA5
watch → AUTHENTICATED_CONFIRM_REPLY   (encrypted)  → Initialized

✅ Порядок переподключения (без shell-трафика) был подтверждён на реальном захвате.

4.3 Пост-аутентификационная инициализация (фаза 2)

⚠️→✅ TIME обязателен перед запросами данных. После Initialized часы не будут отвечать на BATTERY, SERIAL_NUMBER_GET или рукопожатие ACTIVITY_FETCH_*, пока в сессии не будет отправлен TIME (FFFF 8004) — без него приходит только незапрошенный FIRMWARE_VERSION_RET, а всё остальное истекает по таймауту. ✅ подтверждено вживую (Pixel 8a): отправка трёх GET-запросов без TIME → отвечает только прошивка; отправка TIME первым → батарея и серийный номер начинают отвечать.

Рекомендуемый порядок фазы 2: TIME → FIRMWARE_VERSION_GET → SERIAL_NUMBER_GET → BATTERY (0xA5) → отправка конфигурации → синхронизация здоровья (§8).

4.4 Паттерн эха GET → SET (✅)

Для большинства настроек нет отдельного опкода «чтения». Отправка *_GET (cmd2 = 0x0002, payload 0xA5) заставляет часы ответить опкодом SET (cmd2 = 0x0001) с текущим значением. SET-команды подтверждаются через cmd2 = 0x0003 с пустым телом.


5. Открытый текст vs шифрование

Кадры шифруются AES после установки ключа, кроме следующих опкодов, которые всегда передаются открытым текстом:

  • AUTH_PAIR_REQUEST (FFFF 8047), AUTH_PAIR_REPLY (FFFF 0048)
  • DATA_CHUNK_WRITE_WATCHFACE (FFFF 9064), DATA_CHUNK_WRITE_FIRMWARE (FFFF 9042), DATA_CHUNK_WRITE_AGPS (FFFF 905F)

Заголовки кадров (cmd1/cmd2) всегда передаются в открытом виде, поэтому последовательность команд видна в любом захвате даже без ключа — только зашифрованные payload требуют sessionKey.


6. Справочник опкодов (cmd1, cmd2)

GET/SET/REQUEST = телефон→часы; RET/REPLY/ACK/RESPONSE/DATA = часы→телефон.

Сессия / устройство

Аутентификация

Уведомления / звонки / поиск

Музыка

Имяcmd1,cmd2
MUSIC_INFO_SET / _ACKFFFF 905C / FFFF A05C
MUSIC_BUTTONFFFF A05D

Будильники / контакты / напоминания

Конфигурация

Погода

Имяcmd1,cmd2
WEATHER_SET_1 (тот, что работает)FFFF 906B
WEATHER_SET_2 (игнорируется на Pro 2 — см. §9)0066 0001

Циферблаты / диски

Здоровье / синхронизация

Опкоды только для JS (FFFF 8051, FFFF 0051, FFFF 90A2, FFFF 90C5, FFFF A056, FFFF 908A/908B статус/поддержка ChatGPT) обрабатываются в байткоде Hermes приложения, а не в Java- слое. Их заголовки появляются в захватах, но семантика payload ⚠️ [неопределённа].

Массовая передача данных (канал данных)

Циферблаты / прошивка / AGPS используют цикл init → запрос/запись чанков → finish-ack:

(все cmd1 = FFFF.) Часы управляют циклом, отправляя DATA_CHUNK_REQUEST_*(offset, length) (offset/length = u32 big-endian); телефон отвечает DATA_CHUNK_WRITE_* с payload[offset..offset+length] на характеристике данных. Подробности см. в §11–§12.


7. Время и часовой пояс

Payload TIME (FFFF 8004) = epochSeconds(i32, BE) ‖ utcOffsetMillis(i32, BE). Отправляется сразу после аутентификации, чтобы часы показывали локальное время (и разблокировали запросы данных — см. §4.3).

⚠️ Метки времени здоровья от часов — UTC. Приложение-компаньон должно добавлять локальное смещение UTC перед вычислением локального календарного дня / времени суток. (Группировка здоровья по сырому дню UTC переносит смену дня на неправильное локальное время.)

Payload TIME_FORMAT (005F 0001) = 1 байт: 00 = 24 ч, 01 = 12 ч.


8. Синхронизация здоровья

  1. Телефон отправляет ACTIVITY_FETCH_1; часы отвечают ACTIVITY_FETCH_ACK_1 (первый байт 01 ⇒ готово).
  2. Телефон отправляет ACTIVITY_FETCH_2; затем часы отправляют поток кадров данных: ACTIVITY_DATA, HEART_RATE_*, SPO2, STRESS, SLEEP_DATA, WORKOUT_SUMMARY[_V3].
  3. Каждый разбирается в поминутные сэмплы / сессии и агрегируется по локальному дню.

Синхронизация последовательная (должна следовать за TIME; часы освобождают потоки после ACK_2), а не одиночный пакет. Тяжёлая сессия отправляет ~170–210 кадров уведомлений за ~160 с. ✅

8.1 Запись активности — ACTIVITY_DATA (по 32 байта, LE) ✅

Единица калорий: калории активности сообщаются в кал (грамм-калориях). Разделите дневную сумму на 1000, чтобы получить ккал. (Калории в сводке тренировки, напротив, уже в ккал.)

8.2 Сэмплы HR / SpO₂ / стресса ✅

  • Ручной/авто HR, HR тренировки, SpO₂, стресс = по 8 байт: timestamp(i32 LE) ‖ value(i32 LE) (value = уд/мин / % SpO₂ / индекс стресса).
  • Пульс в покое (00DA 0001) отличается — 5 байт: timestamp(i32 LE) ‖ hr(u8). ✅ живой пример 5e dc 29 6a 4e → ts, hr = 78 уд/мин. Диапазоны оценки стресса: 1–29 / 30–59 / 60–79 / 80–99.

8.3 Сон — SLEEP_DATA (заголовок 18 байт + N × 8-байтовых записей) ✅

Один SLEEP_DATA = одна сессия сна; ночь может содержать несколько (микропробуждения разделяют сессии).

Заголовок:

Каждая 8-байтовая запись: timestamp(u32) ‖ duration_s(u16) ‖ stage(u16). Коды стадий: 1 = Глубокий, 2 = Основной/лёгкий, 3 = БДГ, 4 = Бодрствование. ✅ проверено на полной ночи (две сессии, суммы D/C/R/A сходятся).

8.4 Сводка тренировки — WORKOUT_SUMMARY v1 (54 байта) / _V3 (0160 0001)

v1: start(u32), end(u32), duration_s(u32), затем тип/калории/шаги/дистанция/средний HR и блок GPS/расширенный. ✅ раскладка v1 подтверждена по прошивке. WORKOUT_SUMMARY_V3 — более новая раскладка для тех же данных плюс расширенный блок ~40 байт (exerciseLoad, аэробная/анаэробная, recoveryTime, VO₂max, каденс, PAI, лучшие времена бега…). Набор полей известен (из Room DB приложения), но точные байтовые смещения внутри этого 40-байтового блока ⚠️ [неопределённы] — для их закрытия нужен один сырой захват GPS-тренировки.


9. Избранные payload команд

Строки — UTF-8, усечённые по байтам до размера поля (усечение может разбить многобайтовый символ, соответствуя поведению s.encode()[:max] в прошивке); короткие поля дополняются нулями справа.

  • APP_NOTIFICATION (0065 0001) ✅: iconCode(1) ‖ 0x00 ‖ when(u32 BE) ‖ titleLen(1) ‖ title ‖ body. iconCode выбирает иконку приложения (WhatsApp=8, Telegram=12, Instagram=18, Gmail=27; неизвестно=0xFF). Заголовок ≤ 20 байт, тело ≤ 128 байт. Отправлено с клиента → часы отобразили его + ACK 0065 0003.
  • BATTERY (005C 0001) ✅: ответ = level(1) ‖ charging(1) (например, 3b 00 = 59 %, не заряжается).
  • SERIAL_NUMBER_RET (00DE 0001) ✅: len(1) ‖ ASCII (например, 10 + "CI04102520008192").
  • USER_INFO (0095 0001) ✅: height_cm(1) ‖ weight_kg(1) ‖ age(1) ‖ gender(1: 1=М) (например, = 172 см / 73 кг / 31 / мужской).

10. Заметки по реализации и особенности

  • Нет системных часов в кодеках: кодировщики принимают now/utc_offset как явные параметры (детерминированно, тестируемо). Транспорт предоставляет реальное время.
  • TIME управляет всем (§4.3) — отправьте его первым, иначе часы останутся немыми на запросы данных.
  • Подсчёт CRC открытого текста (§2) легко испортить — кадры открытого текста объявляют, но опускают CRC.
  • Порядок байтов: заголовок + опкоды BE; целые числа payload LE; исключения — GOALS_SET и GPS_PUSH — big-endian, а смещение/длина массовой передачи — big-endian.
  • MTU: размеры чанков вычисляются так, чтобы зашифрованные чанки выравнивались по 16-байтовым блокам AES.
  • authkey можно сохранять (храните его после первой привязки); sessionKey — на каждое соединение и выводится из nonce часов при каждом переподключении.

11. Циферблаты / диски — создание

Часы поддерживают (a) фото/пользовательские диски (фоновое изображение + цифровые часы, рисуемые прошивкой) и (b) структурированные диски (встроенные / магазинные циферблаты: фон плюс позиционированные слои спрайтов, стрелки и текстовые виджеты). Оба передаются по каналу данных через цикл init → чанк в §6.

Что реально работает (✅ проверено вживую): создание фото-диска из любого изображения и его установка; установка любого из 103 магазинных циферблатов офлайн; перекраска структурированного диска (замена фона или любого не-фонового спрайта) и перемещение его слоёв; переупорядочивание / переключение активного циферблата; и создание структурированного диска с нуля — конверт сцены 0x20 декодирован, и построитель реализован (§11.7), доказано офлайн, что все 103 магазинных циферблата проходят round-trip байт-в-байт и что синтетические контейнеры проходят собственный валидатор прошивки. 🟡 единственный непроверенный шаг — наблюдение синтетического рендера с нуля на устройстве через 9075 (структурное офлайн-доказательство уже покрывает то, что раньше вызывало отклонение 0a). Нет барьера кодека или транспорта и нет необходимости в вендорском тулчейне. Старые утверждения «структурированный рендер запечён в RES-pack / невозможен по BLE» и «серверный кодек cf=0x1f» были неверны (баг смещения+байт-на-пиксель) — прошивка рендерит структурированные циферблаты управляемо данными из файла, который вы отправляете.

11.1 Управление дисками — DIAL_COMMAND (9055 / a055) ✅

  • type 0 = запрос списка. Ответ a055 = result(u8) ‖ selectIndex(u8) ‖ total(u8) ‖ max(u8) ‖ N × dialId(u32 LE) ‖ ffffffff. Пример: 01 05 06 07 … = активен #5, 6 циферблатов, максимум 7.
  • type 1 = переупорядочивание / выбор активного: повторно отправьте весь список с целевым циферблатом на индексе 0 (так официальное приложение переключает циферблаты; отдельного опкода «установить активный» нет).
  • Удаление циферблата = повторная отправка списка без его id.
  • CHANGE_DIAL (009F 0001) неактивен на прошивке 1.0.0.73 (возвращает константу, не переключает) — не используйте его.

11.2 Поток передачи ✅```

INIT1 8052 (payload A5) → 0052 [0]=01 INIT2 9063 (photo, APPEND) | 9075 (structured, REPLACE) → A063 / A075 [0]=01 [ watch → DATA_CHUNK_REQUEST A064 (offset, length; u32 BE, +progress u8) phone → DATA_CHUNK_WRITE 9064 (bytes[offset..offset+length], plaintext) ] × N FINISH A065 → 9065 (payload A5)

root@kitploit:~
Finish reply byte: `01` = активировано и сохранено; `0a` = сохранено, но **не** активировано / отклонено. На
Android каждая `DATA_CHUNK_WRITE` должна отправляться как **одна BLE-запись на кадр** — объединение и
повторная нарезка по MTU рассинхронизирует заголовки, и часы зацикливаются, запрашивая offset 0.

- **`9063` (фото) = APPEND.** Список циферблатов растёт (6→7); `watchfaceId = 0xFFFFFFFF` (кастомный
  sentinel), поэтому он никогда не отклоняется как дубликат, и часы автоматически активируют его.
- **`9075` (структурированный) = REPLACE** слот `old_id`. `old_id` **обязан** уже быть в списке
  (иначе `0a`). Чтобы переустановить уже присутствующий id, **сначала удалите его** (9055 list-minus-id),
  затем загружайте «свежий» — повторное использование id на месте даёт `0a`.

### 11.3 Фото / кастомный циферблат — ✅ полностью проверен end-to-end

**Контейнер** (побайтово проверенный round-trip; все поля little-endian):```
0x00  magic     6c 8d c4 a5
0x04  count     12 00 00 00   (=18)  [constant, NOT an element count]
0x08  00 × 8
0x10  lenFull   u32 LE        (length of the whole FULL block: tag+len+payload)
0x14  FULL  tag 04 48 47 3a ‖ payloadLen(u32 LE) ‖ LZ4(RGB565-LE)   → 466×466  [raw 434312 B]
      THUMB tag 04 38 c4 21 ‖ payloadLen(u32 LE) ‖ LZ4(RGB565-LE)   → 270×270  [raw 145800 B]
EOF-4 magic     6c 8d c4 a5   [trailer = magic repeated]

Codec = стандартный блок LZ4 поверх RGB565 little-endian, сверху вниз (payloadLen отсчитывается от первого байта LZ4). Официальное приложение использует LZ4-HC и удаляет 21-байтовый заголовок/хвост блока LZ4; обычный энкодер LZ4 только с литералами также работает — часы принимают любой корректный LZ4, побайтовая идентичность не требуется. Пиксели за пределами вписанной окружности (центр 233,233, радиус 233) устанавливаются в 0x0000.

INIT_2 для 9063 — точный заголовок (✅ именно этот работает):``` 01 ‖ size(u32 BE) ‖ FF FF FF FF ‖ 01 01 01 ‖ styleId(u16 BE) ‖ posX(u16 BE) ‖ posY(u16 BE) ‖ color565(u16 BE) ‖ FF × 8

root@kitploit:~
`size` = точная длина `.bin`; `FFFFFFFF` = пользовательский `watchfaceId`; `styleId` 0–4 выбирает встроенный
макет цифровых часов (он всегда отрисовывается — «выключить» нельзя); `posX/posY` задают его позицию (проверенные
56 / 77); `color565` задаёт его оттенок (например, `FFFF` = белый). ⚠️ Более короткая форма `A5 ‖ size ‖ watchfaceId`
**отклоняется** с завершением `0a` — используйте полный заголовок выше. (Эталонная реализация:
`core-rust/engine.rs::build_wf_init2`, повторяющая `C6135t.m31104u` в официальном приложении.)

**Рецепт:** измените размер изображения до 466×466 (и миниатюры 270×270), преобразуйте в RGB565-LE сверху вниз,
при необходимости обнулите пиксели за пределами круга, сожмите каждое LZ4, соберите контейнер выше и загрузите
через конвейер `9063` с `watchfaceId = 0xFFFFFFFF`. (Эталонный кодек: `core-rust/watchface.rs`,
`work/codec_dfa.py`.)

### 11.4 Структурированный / магазинный циферблат — контейнер и кодеки ✅

**Расположение файла** — 36-байтовый заголовок повторяется **побайтно идентично как 36-байтовый нижний колонтитул** в конце файла
(✅ проверено на 15 циферблатах; парсер должен отклонять файл, если они различаются):```
[36-byte header][scene TLV (§11.7)][asset pool][36-byte header again]

Заголовок (идентичная структура для всех 103 дисков хранилища; все поля в формате little-endian):``` 0x00 crc_tree u32 LE [CRC32-raw of header[0x04:0x24] ‖ scene section] ✅ see below 0x04 magic 01 00 00 XX [XX = 0x00 or 0x02; both seen, meaning of 0x02 unknown] 0x08 name char[16] [NUL-terminated, e.g. "SlopeTime", "Metaball"; may carry a non-zero tail after the NUL (@0x17) — round-trip it verbatim] 0x18 size_a u32 LE [= filesize − 36 = footer offset = header+body] ✅ 103 dials 0x1c size_b u32 LE [asset-pool length, exactly] ✅ 15 dials 0x20 crc_assets u32 LE [CRC32-raw of the asset pool] ✅ see below 0x24 … [body starts here: the 0x20 scene container, §11.7]

root@kitploit:~
> ⚠️ **Исправление (заменяет «нет блокирующей контрольной суммы»).** В более ранних ревизиях `@0x00` читался как
> per-dial идентификатор/хеш, а `@0x20` — как «3× u32 слова идентификатора/хеша \[не CRC]», и утверждалось, что CRC32/Adler32/
> байтовая сумма — все не совпадают. Оба слова **являются** CRC32 — более ранние тесты их пропустили, потому что
> вариант нестандартный, и потому что чтение «3 слова по адресу `0x20`» смешивало единственное слово CRC
> с первыми байтами контейнера сцены, который начинается по адресу `0x24` (аналогично «имя, повторённое по адресу
> `0x2c`» — это узел имени `0x86` сцены, §11.11). Находка из
> [freethinkel/fmc](https://github.com/freethinkel/fmc); повторно проверено здесь.

**CRC32-raw** = отражённый IEEE-полином `0xEDB88320`, **`init = 0`**, и **без финального XOR** — т.е.
ни `init=0xFFFFFFFF`, ни `^0xFFFFFFFF` стандартного `crc32`. Именно поэтому
готовый CRC32 никогда не совпадал. Обратите внимание на зависимость порядка: `crc_assets` находится внутри диапазона,
покрываемого `crc_tree`, поэтому **сначала записывайте `@0x20`, затем вычисляйте `@0x00`**.```python
def crc32_raw(data: bytes) -> int:          # tab = standard 0xEDB88320 reflected table
    c = 0                                    # init 0, no final inversion
    for b in data: c = tab[(c ^ b) & 0xFF] ^ (c >> 8)
    return c & 0xFFFFFFFF

crc_tree   = crc32_raw(f[0x04:0x24] + f[0x24:first_asset])
crc_assets = crc32_raw(f[first_asset:len(f)-36])

Verified: 9/9 pristine store dials match on both words, and 6/6 of this repo's own templates match on crc_tree.

🟡 The firmware does not appear to enforce either CRC. Every dial this repo has installed over 9075 — including reskins produced by the same-footprint in-place edit path (§11.6), which mutates asset payloads and X/Y bytes without recomputing the header — rendered fine on-device. So a stale CRC is not what causes a 0a reject (that is the container-window invariant, §11.7). Treat the CRCs as write-correct-anyway: cheap, and the only known integrity field in the format. Anything that rewrites the scene or the asset pool should recompute both words.

Stub dials (~173 B, e.g. ids 273/274/277) are placeholders for faces baked into ROM: header + directory, no real assets.

Assets — each is dimsWord(u32 LE) ‖ len(u32 LE) ‖ LZ4(payload), where cf = dimsWord & 0x1f, w = (dimsWord >> 10) & 0x7FF, h = (dimsWord >> 21) & 0x7FF, and len counts from the first LZ4 byte (the 1f 00 01 00 you often see there is the first LZ4 token — do not skip it). Decompressed size = w·h·bpp:

✅ All 4151/4151 assets across the 103 dials decode exactly with a standard lz4.block decompressor at w·h·bpp. Transparency is the alpha byte (cf=5/24) or 0x0000 (cf=4 outside the circle) — there is no RLE and no "escape". Encode = re-raster → standard LZ4 → [dimsWord][len][LZ4].

INIT_2 for 9075 — AES-encrypted body:``` kind(1) ‖ old_id(u32 LE) ‖ new_id(u32 LE) ‖ file_len(u32 LE)

root@kitploit:~
`kind` = `0x02`/`0x03`; `old_id` = текущий активный циферблат (из `9055`); `file_len` = реальный размер `.bin`
(= `@0x18 + 36`). Установка магазинного `.bin` как есть — гарантированный путь (Ring Data id 359 +
102 других подтверждены). (Ссылка: `core-rust/engine.rs::build_dial_replace_init`.)

### 11.5 Структурированная грамматика директории ✅ (декодирована и реализована — ПЕРЕСМОТРЕНО 2026-07-02)

> **⚠️ Пересмотр (2026-07-02): плоская схема записи `61 01 00` ниже была систематически
> СО СДВИГОМ НА ОДИН БАЙТ.** Тело сцены — это чистый TLV (§11.7); тело **листового** drawable (теги `0x30`/`0x38`
> статические, `0x70` указатель) выглядит так:
>
> ```
> 01 xx 00 [X u16][Y u16] …attrs… 61 [count u16][base u32][count×id u16] [05 05 00 01 pivX pivY]
> ```
>
> - атрибут `0x01` открывает тело: **X,Y = верхний левый угол** на холсте 466² (это `s16 x,y` из SDK
>   `sty_picture_t`).
> - **таблица кадров `61 …` закрывает тело** (`base` = указатель на ассет; `count` 1 = изображение, 10/11 =
>   атлас цифр — старый «тип записи `0a/0b`» на самом деле был этим count! — 7/13/2 = лист кадров усложнения).
> - дополнительные указатели: источник+масштаб `[src] 00 3c 00` внутри атрибута `0x01`; точка поворота в
>   **хвосте** `05 05 00 01 [pivX][pivY]`. **Центр вращения = `(X+pivX, Y+pivY)` для каждого указателя** —
>   не фиксированный (233,233): существуют смещённые от центра субциферблаты (например, стрелки циферблата 366 вращаются вокруг 150,150).
>
> Линейное сканирование `61 01 00` сшивало таблицу кадров+точку поворота элемента **N** с X/Y
> (и байтом тега, старым «f3») элемента **N+1** — это лишь *выглядело* правильным на аналоговых циферблатах, где
> соседние стрелки имеют почти идентичную геометрию. «Стена компактных вариантов» (спецификация 24 §24.4.5) была
> тем же неверным прочтением. Реализовано как `scan_scene_drawables` в `core-rust/watchface_struct.rs`
> и `wfweb/src/codec/parse.ts` (сцена = основной источник для изображений/указателей; плоское сканирование сохранено для
> текста + запасной вариант без конверта). Проверено оракулом `wfweb/compare.html` (рендер против официальных
> PNG из магазина, 99 циферблатов): 64→72 хороших, 8→5 плохих, средняя разница 9.3→7.4%.

Историческое чтение плоских записей (заменено, сохранено для контекста):

- **Статическое изображение** (`61 01 00`): `asset_ptr(u32) ‖ elemId(u16) ‖ 05 05 00 01 ‖ pivotX(u16) ‖
  pivotY(u16) ‖ 3B ‖ 01 ‖ 1b 00 ‖ X(u16) ‖ Y(u16)`. Верхний левый угол на холсте 466² = `(X−pivotX, Y−pivotY)`.
- **Указатель/стрелка** — та же запись изображения, поворачивается во время выполнения. **Центр вращения = `(X+pivotX, Y+pivotY)`**
  (≈ 233,233 на аналоговых циферблатах). **Источник данных — это `u8` по смещению записи `+36`**, масштаб `u16` по
  `+38` (=60): `0x0a`/`0x70` = часы (`h·30°+m·0.5°`), `0x0e`/`0x71` = минуты (`m·6°+s·0.1°`),
  `0x12`/`0x72` = секунды (`s·6°`). ✅ подтверждено дизассемблированием геттеров (запасной вариант RTC 10:10:30).
- **Виджет текста / числа** (`61 0a 00`): `asset_ptr(u32) ‖ [10×u16 метрики шрифта] ‖ 40 01 00 ‖ флаг ‖
  3B ‖ 01 ‖ u16 ‖ X(u16) ‖ Y(u16)`. `asset_ptr` указывает на глиф «0»; цифра *d* = ассет по
  `index("0") + d` (10 последовательных спрайтов cf=5, например `0123456789` и пунктуация `,°`). ✅ отрендерено.
- **Заполнение усложнения = индекс кадра** (✅ подтверждено для цифровых/перечисляемых/шкальных усложнений, count>1):
  значение индексирует **предварительно отрендеренный лист кадров** в `.bin` — `frame = (count−1)·val/100` (проценты) или
  `frame = value` (перекидная цифра / перечисление). Таблица кадров = подзапись `61 ‖ count(u16) ‖ base(u32) ‖
  count×id(u16)`. Например, большие часы 327 Digit Max — это лист из 13 кадров (числа 0–12), `frame = hour`.
- **Кольцо прогресса / дуга = секторный клип во время выполнения** (✅ 2026-07-02, **исправляет чтение «кольца — это листы кадров»
  в спецификации 25 §2**): тег элемента **`0x81`** несёт **один** полный диск (`61` таблица кадров
  `count == 1`), а частичный клин — это тот диск, **обрезанный до сектора круга** (`frac = value/max`,
  по часовой стрелке от 12 часов) — проверено попиксельно на 322 Glare 2 и подтверждено `count==1` на
  **20 циферблатах**. На диске: тело `0x81` = под `0x01` (геометрия `x@+0 y@+2 w@+4 h@+6`, встроенный `61 1 base`
  = диск) + под `0x5b` (спецификация дуги). Реализовано в wfweb (`blendSector`).
  ⚠️ Подзапись `0x5b` — это **не** просто «`max` u16 `@+4`», как документировалось ранее — это читало
  младшую половину `max i32` и пропускало **углы начала/конца развёртки и ширину штриха**, которые находятся сразу
  после неё. Также существует процедурный собрат `0x80`/`0x5a` (с явным радиусом), который этот документ
  никогда не охватывал. Полная структура записи и то, что не так с прежним предположением «по часовой стрелке от 12 часов»:
  **§11.15**.
- **Идентификатор источника данных** — блок атрибутов `82` элемента находится на `delim+3` (после последнего `40 01 00`),
  а **идентификатор источника — это `u8` по `+0x14`** (также `relX@+0x07 s16`, `relY@+0x09 s16`, `anchor@+0x0C/0E`,
  `mode@+0x15`, `frame-count@+0x1A`). Anchor < 0 = выравнивание по краю родителя. 🔎 Прошивка разрешает
  идентификатор через таблицу геттеров из 142 записей по адресу `0x101f371c` (каждая вызывает `ux2sys_get(type)`).
  ⚠️ **Предпочтительно читать идентификатор как `meta[9]` структуры (§11.11)** — фиксированное поле — вместо этого прямого
  сканирования атрибута `82`, которое является источником сдвига на один байт, описанного в §11.8/§11.9. Полная таблица идентификаторов в §16; обратите
  внимание, что метки здоровья/погоды, ранее перечисленные в этом разделе (`0x19` ЧСС, `0x1b` батарея,
  `0x24` температура, `0x36` шаги), **оспариваются и, вероятно, неверны** — см. блок ⚠️ в §16.
  (Пример группы `0x07:0x0b:0x0f` = ЧЧ:ММ:СС ниже не затронут.)
- **Узел группы** (`0x68`): вкладывает свои дочерние элементы в собственное тело TLV (`0x60` = значение/текст,
  `0x30` = статический); каждый `0x60` несёт свой идентификатор источника на `data+16`. Например, группа `0x07:0x0b:0x0f` =
  часы ЧЧ:ММ:СС. Парсер элементов TLV = `0x100db55c` (таблица переходов, индексируемая по `tag−0x70`).

### 11.6 Матрица авторства

| Путь | Статус | Примечания |
|---|---|---|
| Фотоциферблат из любого изображения | ✅ **готово** | §11.3; проверено на устройстве |
| Установка любого из 103 магазинных циферблатов | ✅ **готово** | §11.4; `9075`, `old_id`=активный |
| Перекраска фона cf=4 магазинного циферблата | ✅ **работает вживую** | заменить полную полезную нагрузку на месте, задать ассету `len` **новый** размер блока (≤ старого), сохранить тот же размер файла, чистая установка |
| Повторное авторство через шаблонирование (замена пикселей любого слоя + перемещение геометрии) | ✅ **рендерится через BLE** | циферблат 373: фон→голубой + спрайт cf=5→красный + перемещён X 224→100, всё отрендерено, стрелки живые |
| 100 %-синтетический структурированный циферблат с нуля | ✅ **билдер готов, проверен офлайн** | билдер конверта `0x20` в `watchface_struct.rs` (`build_container`/`serialize`/`validate_container`); round-trip всех 103 циферблатов байт-в-байт + синтетика проходит валидатор прошивки (§11.7). 🟡 рендер на устройстве через `9075` ещё не снят на видео |
| Системные шрифты (`.font`) | ✅ **декодирование/рендер (все)** | бинарник LVGL (не проприетарный); 32 числовых шрифта (`num*/nm*`, без сжатия) + 24 текстовых шрифта (`font*`, LVGL RLE `comp=1`) — все декодируются: 12208 глифов, 0 переполнений, полный ASCII. RLE = LVGL v8.3 `lv_font_fmt_txt.c` (3-состояния SINGLE/REPEATE/COUNTER + построчный XOR-префильтр), портировано 1:1, без дизассемблирования |

⚠️ Ловушки перекраски/повторного авторства, вызывающие чёрный экран или `0a`: оставление **старого `len` ассета** (часы
читают за пределы блока → переполнение → чёрный экран); **увеличение файла** (отклоняется при установке); повторное использование
идентификатора **на месте** вместо чистой установки; **переупорядочивание ассетов** без исправления цепочки размеров блоков в хвосте ссылок
(§11.11). Сегодня это не фатально, но всё равно пишите правильно: любое изменение сцены или пула ассетов
аннулирует два слова **CRC32** в заголовке — пересчитайте оба (§11.4), `@0x20` перед `@0x00`.

### 11.7 Конверт сцены `0x20` — декодирован и билдер реализован ✅

Повторно авторизованный **настоящий** циферблат рендерится, потому что сохраняет конверт сцены файла. Чисто синтетическое
тело из плоских записей `61 …` **отклоняется** — парсер прошивки (`WFManager_Parser`, `0xdb35c`)
требует, чтобы тело (со смещения `0x24`) начиналось с контейнера сцены `0x20`. Полный файл:```
[0x00,0x24)  header:  perDialId@0 · version=1@4 · name[16]@8 · size_a@0x18 · size_b@0x1c · idWord0@0x20
[0x24, fa)   scene:   20 <u16 L0> ( 21 <u16 L1> ( 86 <len>=name , 30/70/80/81… drawables ) [ 22 … AOD ] )
[fa, EOF)    assets:  [dimsWord u32][len u32][payload = 1f 00 01 00 + LZ4] …
   size_a = filesize−36 · size_b = filesize−36−first_asset · 0x27+L0 == first_asset

Сцена представляет собой чистый вложенный TLV — [tag u8][len u16 LE][body], контейнерные теги 0x20/0x21/0x22/0x68 рекурсивно, листовые отрисовываемые элементы 0x30 (статический) / 0x70 (элемент/указатель) / 0x80 / 0x81 / 0x86 (имя). (Плоские записи 61 01 00 / 61 0a 00 — это паттерны, которые живут внутри тел отрисовываемых элементов; старый парсер находил их эвристически — и сшивал соседние тела вместе, см. правку §11.5. Макет тела отрисовываемого элемента теперь полностью декодирован там.) Каждый дочерний offset+len должен помещаться внутри окна своего родителя; первый байт тела ≠ 0x20 → ошибка парсера −16; дочерний элемент, выходящий за пределы своего окна → −2; любое из этих условий заставляет обработчик 9065 (0xeb50c) записать завершение .

Сборщик реализован и проверен офлайн (core-rust/watchface_struct.rs: SceneNode / serialize / parse_scene / validate_container / build_container / build_container_raw; CLI cmfwatch-wfgen reframe):

  • scene_roundtrip_identity — все 103 магазинных циферблата: parse_scene→serialize воспроизводит сцену байт-в-байт (пересчитанные вложенные len совпадают), и validate_container проходит на каждом из них.
  • build_reframe_identity / CLI reframe — пересборка всего .bin с нуля воспроизводит файл байт-в-байт кроме 1 байта выравнивания имени (@0x17; не контрольная сумма).
  • build_container_synthetic — составляет новый циферблат (фон + отрисовываемый элемент, вложенный в 20→21), который проходит точный инвариант прошивки (build_container генерирует корректные вложенные окна).
  • — отклоняет плоское тело (→ , историческая ошибка ) и дочерний элемент, выходящий за пределы своего окна (→ ).

🟡 Всё ещё не доказано (нужны часы, не блокирует): загрузка синтетического циферблата с нуля через 9075 и наблюдение за его отрисовкой — офлайн-структурное доказательство уже покрывает то, что вызывало отклонение 0a.

11.8 Уточнения точности отрисовки (2026-07-02, циферблат 275 "SlopeTime")

Сверил отрисовку wfweb с официальными магазинными миниатюрами (пиксельный оракул по всем 103 циферблатам) и закрыл четыре пробела:

  • X/Y отрисовываемых элементов/указателей — это i16 (со знаком). ✅ Якоря могут быть отрицательными для элементов, выходящих за пределы холста — например, красная секундная стрелка 275-го находится на Y = 0xFFFC = −4 (спрайт 30×281, источник 0x12, повёрнут от центра за верхний край). Чтение X/Y как u16 (65532) заставляло защиту отбрасывать его. Разбирайте оба как знаковые и допускайте небольшой отрицательный диапазон.
  • Цифры цифровых часов могут быть img_numbers верхнего уровня 0x60 (не только внутри группы 0x68), и реальный источник данных — это u8 по смещению записи −5 — прямое сканирование атрибута 82 здесь систематически со сдвигом на один и захватывает атрибут следующего соседа (в 275-м цифра минут подхватывала 0x18 дня недели). "10:10" 275-го = час 0x07@X≈306 + минуты 0x0b@X≈369 с как соседним статическим элементом между ними, каждый — атлас из 11 глифов (). ⚠️ При исправлении X/Y с , иначе повторный экспорт повредит эти байты (ломает сохранение размера → ).

Также: официальные магазинные миниатюры отрисованы в 10:10 (классическое маркетинговое время), а не 10:12 — совпадение времени оракула с 10:10 заметно снижает среднюю пиксельную разницу. Парсер wfweb теперь выполняет полный цикл по всем 103 циферблатам байт-точно (исправление смещения записи X/Y выше устранило последние несоответствия).

11.9 Пропуск AOD-контейнера + автономный источник img_number (2026-07-03, циферблат "Gradient")

  • Отделите контейнер AOD 0x22 в собственное представление. ✅ Обходчик сцены уже пропускает 0x22, но плоское сканирование текста/чисел проходило по всему [0x30, firstAsset) — поэтому оно выводило вариант always-on (AOD) каждого элемента как обычный слой. На "Gradient" AOD серый атлас даты (смещение в 0x22) рисовался поверх красного обычного. Исправление: пометить каждую запись 0x22 как layer.aod=true (с собственным набором дедупликации) и позволить renderAt(…, aod) показывать их только в режиме AOD (обычный режим скрывает слои aod; режим AOD скрывает обычные; фон заменяется setAod и рисуется всегда). Чистый выигрыш оракула в обычном режиме по корпусу (284: 31%→21%, +18 других) — варианты AOD перерисовывали многие циферблаты — и переключатель AOD в редакторе теперь показывает реальный макет always-on вместо обычного. Настоящий AOD — это чёрный экран (без затемнённой сцены): если у циферблата нет выделенного фонового кадра AOD (dial.aod), обычная сцена скрывается в режиме AOD, поэтому отображается чёрный + элементы в их собственном цвете. AOD разбираются обходчиком сцены тоже (теперь он рекурсивно проходит контейнер , помечая отрисовываемые элементы , вместо того чтобы оставлять их плоскому сканированию, где их точка поворота не совпадала → "без позиции"); стрелки AOD вращаются вокруг центра холста ( иногда несёт смещённую от центра x/y стрелки, которую прошивка игнорирует — например, час Gradient ). Редактор также предоставляет это как (§UI): каждый экран показывает только свои слои, и правки сохраняются независимо. Отрисовка в обычном режиме байт-идентична во всём; полный цикл остаётся байт-точным на всех 103 циферблатах.

11.10 КОЛИЧЕСТВО ЦИФР img_number — байт 40 01 00 XX (✅ подтверждено прошивкой)

Сколько цифр рисует img_number — это один байт в записи поля — байт данных XX подзаписи атрибута 40 01 00 XX элемента (подзапись 0x40, которая находится после таблицы кадров 61 [count][base][glyph-ids]):

  • младшая полубайт XX & 0x0F = количество слотов цифр (0 ⇒ по умолчанию прошивки 7).
  • бит 7 0x80 = дополнение нулями (показывать ведущие нули, например "09" вместо "9").

Подтверждено дизассемблированием прошивки (XIP-образ 0x10000000; процедура отрисовки 0x100d8e60): NDIG = ldrb[40sub+3] & 0x0F (→7 если 0); значение ограничивается value % 10^NDIG, и рисуется ровно NDIG глифов, начиная со старшего, ведущие нули подавляются, если не установлен бит 7. u16 после источника (60 для даты, 1000 для ккал) — НЕ количество — он только питает вставку глифа-разделителя тысяч/миллионов (cmp #1000/#1000000), поэтому его редактирование ничего не давало. Идентификатор источника тоже не ограничивает.

Гистограмма по корпусу по всем 620 числовым полям совпадает: 2-значные поля (час/мин/сек/дата/темп/ЧСС) заканчиваются 40 01 00 02/0x82; ккал …04; шаги …05; однозначные разделения часов 0x81. Таким образом, поле даты 40 01 00 82 = 2 цифры, с дополнением нулями — это и есть вся причина, по которой перепривязанная температура по Фаренгейту (≥100) обрезалась.

Исправление / редактор: wfweb разбирает digitCount/digitZeroPad (+digitCountOff) для числовых полей, предоставляет "Цифры" + "Дополнение нулями" в инспекторе, записывает байт на месте (с сохранением размера), и предпросмотр ограничивает/дополняет до digitCount, чтобы зеркально отражать прошивку. Таким образом, перепривязка источника поля и установка количества его цифр работает для любого поля (например, дата→температура °F → Цифры 3). Оракул обычного режима без изменений (0 регрессий, 3 небольших улучшения); полный цикл байт-точен на всех 103 циферблатах. (Более ранняя гипотеза "ширины цифр"/rectW была неверна — ширина только для макета, а не количество.)

11.11 Инвентаризация узлов, запись struct и хвост ссылок на ресурсы ✅

Сцена (§11.7) — это чистый вложенный TLV — [tag u8][len u16 LE][body]. Полная инвентаризация тегов, наблюдаемая по корпусу:

✅ Эта инвентаризация полна для корпуса. Рекурсивно проходя только по указанным выше тегам контейнеров, TLV сцены всех 15 проверенных циферблатов проходит точно до объявленной длины корня с нулевыми неизвестными тегами — поэтому парсер, обрабатывающий эту таблицу, обрабатывает весь формат, а неизвестный тег означает смещённое чтение, а не новый тип узла. (Осторожно: обходчик, который рекурсивно спускается в каждый узел, чья длина случайно ≥ 3, спустится в тела struct/0x5b и выдумает длинный хвост одноразовых "тегов" — тела листьев не являются TLV.)

💡 Ярлык для авторов: автопозиционирование 0x48/0x68 можно полностью пропустить — каждый виджет можно разместить с абсолютными x,y прямо на верхнем уровне экрана, что и делает сборщик с нуля (§11.7). Нужно только для чтения существующих циферблатов. Ширина meta 0x8000 помечает struct как дочерний элемент автопозиционирования кадра (позиция берётся от родителя, а не из x,y).

Тело 0x01 struct — фиксированный 18-байтовый префикс, за которым следует необязательный хвост ссылок на ресурсы:``` +0x00 x i16 [signed — can be negative, see §11.8] +0x02 y i16 +0x04 meta[14] ──────────────────────────────────────────────────── meta[0..1] w u16 [0x8000 = auto-layout child of a frame] meta[2..3] h u16 meta[4..6] unknown [placeholder-looking (1,0,0)/(4,0,0); see §11.14] meta[7] accent-tint capability flag — 4 = tintable (§11.14) meta[9] DATA SOURCE ID (§16) meta[10] sub / variant meta[11..13] max u24 LE [the metric's nominal full-scale value] +0x12 ref tail [61 …] — absent on imageless rings (0x80/0x81, §11.15)

root@kitploit:~
> ✅ Это объединяет «магические смещения» из §11.5/§11.8/§11.9. Эти разделы находят поля *относительно
> байта `0x61` таблицы кадров* — что просто `+0x12` этой структуры, так что `−18`/`−16` = `x`/`y` и
> **`−5` = `meta[9]`, идентификатор источника**. Те же байты, одна чистая компоновка. Эвристика
> прямого сканирования атрибута `82`, которая систематически ошибалась на единицу, вообще не нужна: читайте `meta[9]` структуры.
> `max` числового поля — это также просто `meta[11..13]` (например, поля дня месяца несут `max = 99`).

**Хвост ссылки** (`61`) — как узел указывает на свои битовые карты:```
+0x00  0x61      [tail type]
+0x01  count u16 [1 = single image · 10/11 = digit atlas · N = pick-list / frame sheet]
+0x03  base  u32 [ABSOLUTE FILE OFFSET of the first asset block]
+0x07  count × u16  = the BLOCK SIZE (8 + payload len) of each referenced asset, in order

⚠️ Эти завершающие u16 ранее документировались как «count×id(u16)» / идентификаторы глифов. На самом деле это размеры блоков: base плюс их нарастающая сумма проходит по записям пула ассетов по одной (✅ проверено точно для всех 10 записей цифрового атласа). Два следствия:

  • Ассеты, на которые ссылается узел, должны быть последовательными в пуле ассетов. Произвольного доступа нет — цепочка движется только вперёд. Планируйте пул так, чтобы каждый набор цифр (10), каждый список выбора (N) и каждый лист кадров были одним непрерывным блоком. Писатель, который переупорядочивает ассеты без исправления цепочки, создаёт файл, который читается как мусор (→ чёрный экран).
  • Только первые count−1 размеров являются значимыми; значение последней записи никогда не используется, поэтому в файлах, встречающихся в дикой природе, там иногда хранится устаревшее значение. Не считайте несоответствие в последней записи битой ссылкой.

0x28 превью — миниатюра магазина/каталога встроена в сам .bin (27 узлов превью в 15 циферблатах) как 0x08 pvStruct: 5-байтовый префикс плюс тот же хвост ссылки, без x/y. Полезно для создания галерейного интерфейса без поставки отдельных PNG.

11.12 Условия видимости — тег 0x02 ✅

Соседний элемент 0x02 виджета делает его условным. Без него виджет всегда отрисовывается. Грамматика:``` count u8 , count × ( id u8 , op u8 , val u24 LE signed ) [5 bytes per entry]

root@kitploit:~
`id` — это идентификатор источника данных (§16), включая **синтетические идентификаторы слотов** из §11.13. Операторы, с
подсчётом частоты встречаемости по 15 циферблатам:

| op | значение | встречаемость |
|---|---|---|
| `0x01` | рисовать, если `value == val` | 99 |
| `0x81` | то же, что `0x01` (бит `0x80` установлен — встречается на взаимоисключающих вариантах) | 48 |
| `0x02` | **скрыть**, если `value == val` | 7 |
| `0x03` | рисовать, если `value == val`, где `val` — это **маркер отсутствия данных** (например, HR `1000`) | 13 |
| `0x05` | рисовать, если `value >= val` | 58 |
| `0x06` | рисовать, если `value <= val` | 50 |
| `0x04` | ⚠️ **неизвестно** — 15 вхождений, подтверждённой семантики нет | 15 |

Правило комбинирования (как реализовано в эталонном рендерере, маска `op & 0x7f`): записи с равенством
объединяются через **ИЛИ**, затем записи hide/`>=`/`<=` должны **все** выполняться.

Этот единственный механизм покрывает большую часть изменчивости формата во время выполнения и объясняет структуры, которые выглядят
как дублированные виджеты:

- **Макеты 12ч / 24ч и метрические / имперские** — два набора виджетов, наложенных в одном месте, каждый привязан к
  `id 0x73` (флаг единиц измерения) со значением `val` 0 или 1. На циферблате 275 таких пар шесть (12 узлов).
- **Заглушки «нет данных»** — операция `0x03` против контрольного значения, например `id 0x5f, val 1000` (циферблат 275, дважды):
  рисуется тире-арт вместо температуры, когда метрика недоступна.
- **Сегментированные подсветки** — парный диапазон `0x05`/`0x06`, например цепочка Metaball, где каждое звено загорается
  в своё собственное 5-минутное окно.
- **Альтернативы для слотов усложнений** — привязаны к синтетическим идентификаторам слотов из §11.13.

### 11.13 Настраиваемые слоты усложнений — `0x85` + `0x5f` ✅ (заменяет §11.8)

В §11.8 был сделан вывод, что активная метрика настраиваемого усложнения — это состояние оперативной памяти устройства и не может быть
восстановлена из файла. **Это было неверно** — и меню метрик слота, и его выбор по умолчанию
находятся в `.bin`. Каждый узел `0x85` несёт родственный узел `0x5f`:```
+0x00  slotIndex u8   [0-based position among sibling 0x85 nodes]
+0x01  count     u8   [how many metrics this slot offers]
+0x02  activeIdx u8   [index into the list below = the DEFAULT SHOWN METRIC]
+0x03  count × u8     [the metric ids themselves (§16)]           … NUL padding

Реальные альтернативы, которые фактически отрисовываются, — это обычные группы 0x68 в других местах дерева, каждая из которых управляется условием 0x02 (§11.12) на синтетическом id 0x79 + slotIndex — так, варианты слота 0 привязаны к 0x79, слота 1 — к 0x7a и так далее. Чтобы отрисовать слот: прочитайте activeIdx, затем отрисуйте вариант, чьё условие совпадает с этим индексом.

Измерено на реальных циферблатах:

Два слота по 6 метрик у циферблата 275 дают 12 из его 26 узлов 0x02, ровно как и предсказывалось: 01 79 81 0X 00 00 и 01 7a 81 0X 00 00 для X = 0..5 — шесть альтернатив, привязанных к 0x79 (слот 0), и шесть к 0x7a (слот 1). (Байты 0x79/0x7a, которые §11.8 назвал «байтом экземпляра», — это и есть эти id привязки.) Остальные 14 не связаны с этим: 12 на 0x73 (флаг 24ч/единицы метрик, val 0 или 1 — шесть пар виджетов, переключающихся между 12-часовой и 24-часовой раскладками) и 2 на 0x5f с операцией 0x03 и val = 1000 — заглушка «нет данных» для температуры.

Всё ещё реальное состояние устройства: что бы пользователь ни выбрал позже в сопутствующем приложении, это переопределяет activeIdx во время выполнения, поэтому предпросмотр воспроизводит значение по умолчанию из файла, а не обязательно то, что показывают конкретные часы. imgs[0] узла 0x85 — это заглушка «нажмите, чтобы настроить», которую прошивка рисует только в собственном режиме редактирования — пропускайте её при предпросмотре обычного отображения времени.

11.14 Акцентный цвет — флаг возможности meta[7] == 4 ✅

Некоторые циферблаты позволяют пользователю выбрать акцентный цвет прямо на устройстве, и прошивка подставляет его в растровые изображения виджета во время отрисовки. Переключатель — это один байт: meta[7] структуры (§11.11) — то есть байт +0x0B тела 0x01 — равный 4 — помечает ресурс(ы) этого виджета как подлежащие тонированию.

  • Это флаг возможности для каждого виджета, а не цвет. Перекрашивайте каждый непрозрачный пиксель помеченного ресурса (альфа-канал не трогайте); никакого попиксельного теста на цвет здесь нет.
  • Распространённость: 37 из 499 структур и 8 из 15 циферблатов, измеренных здесь; корпус fmc сообщает 56 из 100 циферблатов хотя бы с одним помеченным виджетом.
  • 🛑 Никогда не встраивайте акцентный цвет в экспортируемые байты. Подстановка выполняется в реальном времени на часах; поставляемый .bin должен сохранять исходные пиксели, иначе вы навсегда потеряете выбор пользователя. Применяйте тонирование только в пути предпросмотра/канваса.

⚠️ Не «улучшайте» это до эвристики по цвету — этот путь доказанно тупиковый (задокументировано fmc после того, как они прошли его трудным путём). Интуитивная теория гласит, что помеченные пиксели запечены в каком-то узнаваемом цвете-заглушке, который прошивка заменяет. Это не может работать: тонируемое кольцо циферблата 348 Tumbler и обычные нетонируемые полосы цифр циферблатов 282 Radar Sweep / 291 Vertical запекают ровно тот же самый RGB (255,72,32) (проверено исчерпывающе, каждый пиксель); а циферблаты 305 Dots (часовая стрелка) и 306 Large Number (цифры) тонируемые, но запечены чисто белым, так что тест по цвету их полностью пропустит. Последовательные уточнения (1 → 4 эталонных цвета плюс белый список ролей виджетов) — все провалились. Читайте флаг.

Перекрёстно проверено на реальном устройстве / сопутствующем приложении на 7 циферблатах, выбранных для нагрузки в обе стороны — 349 Theatre, 376 Digits time, 305 Dots, 306 Large Number, 304 Elaborate 2 — все предлагают настройку акцента и все имеют виджеты с meta[7]==4; 316 Trailing (красноватая стрелка, без настройки), 312 Disc и 295 Vortex не предлагают её и не имеют ни одного помеченного виджета.

meta[4..6] находится прямо рядом с флагом и выглядит так, будто может кодировать цвет в некоторых структурах (похожий на настоящий RGB с хвостом f1=1,f2=255, против заглушки (1,0,0)/(4,0,0) у помеченных структур). Он не коррелирует с возможностью акцента. ⚠️ Не разрешено; игнорируйте его.

11.15 Кольца прогресса — 0x80/0x5a (процедурные) и 0x81/0x5b (с обрезкой по изображению) ✅

Обе разновидности колец объединяют короткую структуру (x, y, meta с id источника — обычно вообще без хвоста ref) с родственным узлом описания дуги. §11.5 задокументировал только «0x5b: max u16 @+4», что является младшей половиной max i32 и упускает геометрию дуги. Полная запись:``` +0x00 min i32 LE [always 0 in the corpus] +0x04 max i32 LE [100 in the corpus, except dial 332 = 60] +0x08 start i16 LE [sweep start, units of 0.1° — SIGNED] +0x0a end i16 LE [sweep end, units of 0.1° — SIGNED] +0x0c width u16 LE [stroke width in px] +0x0e radius u16 LE [0x5a ONLY — 0x81 takes its radius from the clipped image] +0x0e / +0x10 trailer 01 00 kk ⚠️ unresolved (see below)

root@kitploit:~
Сопряжение жёсткое: `0x80` всегда несёт ровно `0x01` + **19-байтный** `0x5a`, `0x81` всегда ровно
`0x01` + **17-байтный** `0x5b` (✅ 26/26 колец на 15 циферблатах). ⚠️ Но **обрезанный по изображению `0x81`
доминирует** — 25 из этих 26. Процедурный вариант `0x80`/`0x5a` появился **один раз** (циферблат 273), так что его
поле `radius` и 19-байтная раскладка опираются на единственный образец; относитесь с подозрением, пока не увидите снова.

`frac = clamp((value − min) / (max − min), 0, 1)`, и заполненная дуга идёт от `start` к `end`.
Нулевой угол — на **3 часах**, положительное направление — по часовой стрелке. Измеренные примеры:

| циферблат | тег | min..max | start → end | ширина | radius |
|---|---|---|---|---|---|
| 273 Activity Mood | `0x5a` | 0..100 | **−102,8° → 102,8°** | 42 | 222 |
| 273 Activity Mood | `0x5b` | 0..100 | 270,0° → 90,0° | 80 | (изображение) |
| 276 Dichotomy | `0x5b` | 0..100 | 60,0° → −120,0° | 23 | (изображение) |
| 304 Elaborate 2 | `0x5b` | 0..100 | −2,0° → 358,0° | 24 / 80 | (изображение) |
| 366 Combo | `0x5b` | 0..100 | 0,0° → 270,0° | 18 | (изображение) |
| 368 Function | `0x5b` | 0..100 | 0,0° → 360,0° | 20 | (изображение) |

> ⚠️ **Это исправляет предположение «по часовой стрелке от 12 часов»** из §11.5. Это лишь частный
> случай `start = 0, end = 3600` (полный оборот, где соглашение ненаблюдаемо). Реальные циферблаты используют
> **частичные шкалы** (веер ±102,8° у 273, трёхчетвертное кольцо 270° у 366) и **отрицательные развёртки**
> (60° → −120° у 276), поэтому рендерер, который всегда рисует полный круг сверху, отображает их неверно.
> 🟡 Точное соглашение о нулевом угле и правило направления взяты из рендерера fmc, перепроверены
> по этим значениям на диске — не подтверждены попиксельно на устройстве этим репозиторием.
> Обратите внимание: `frac = value/max` и результат обрезки сектора `0x81` из §11.5 **были** проверены (циферблат 322).

⚠️ **Нерешено: 3-байтный хвост `01 00 kk`.** `kk` принимает правдоподобные значения (104, 152, 216,
232, 248), и очевидная гипотеза — радиус; **проверено и опровергнуто**: циферблат 366 использует `kk = 104`
и для кольца 82×82, и для кольца 166×166, а циферблат 273 использует `kk = 232` и для кольца 440×440, и для кольца 284×284.
Не радиус, не диаметр. Возможно, непрозрачность/стиль. Передавайте как есть.

🟡 **Сообщённая ловушка, здесь не проверена:** в рендерере fmc число `0x60`, чей исходный id совпадает с id
**любого** кольца на том же экране, отображает `round(frac × 100)` вместо сырого значения — так,
число пульса рядом с кольцом пульса показывает `36` вместо `71 уд/мин`. Их документированный
обходной путь — разместить кольцо и число на двух **псевдонимных** id одной метрики (шаги
`0x19`/`0x26`/`0x49`, калории `0x1c`/`0x1e`/`0x48`). Является ли это поведением прошивки или особенностью
их рендерера — **не установлено**; стоит проверить вживую, прежде чем проектировать под это.

---

## 12. Детали массовой передачи и OTA

Таблица передачи — в §6. Дополнительные подтверждённые пункты:

- **AGPS/EPO** ✅: первый записанный чанк начинается с ASCII-заголовка `000000010000…`. Полный
  цикл init → `[A05F ↔ 905F]×N` → finish наблюдался в эфире (~892 чанка).
- **OTA прошивки** (`9040`–`9042`, finish `9041`) 🔎: структура сопоставлена; полезная нагрузка INIT2 = байты версии
  (например, `0b 00 00 39` = 11.0.0.57). **Не проверено в полевых условиях** (приложение отключает обновление прошивки здесь). Образы
  прошивки, по-видимому, **без подписи — целостность только CRC32** (асимметричной подписи в RE не наблюдалось).
- ⚠️ Поскольку OTA и `FACTORY_RESET (009A 0001)` используют аутентифицированную сессию, одной валидной BLE-
  аутентификации достаточно, чтобы стереть или (в принципе) «окирпичить» часы. Обращайтесь осторожно.

---

## 13. Датчики

✅ Аппаратное обеспечение, доступное через BLE:

- **Оптический PPG** — пульс (ручной/авто/тренировка/покой), SpO₂ и стресс на основе ВСР.
- **3-осевой акселерометр** — шаги, дистанция, калории, стадии сна, подъём запястья, каденс.
- **GNSS/GPS** (с поддержкой AGPS) — трек тренировки (`WORKOUT_GPS`) и отправка местоположения (`GPS_PUSH`).

**Нет** барометра/альтиметра, компаса, гироскопа или датчика температуры кожи/тела. Внутренний
термистор NTC (температура платы/батареи) существует, но читается **только** через AT-канал
(`AT GETNTCTEMP`, §14) — поток истории температуры кожи `0155` на этом SKU пуст.

**Перехват виджетов данных** (настоящего API усложнений/привязки данных нет — см. §11.5): существующие
текстовые поля часов можно перепрофилировать для отображения внешних данных с одного взгляда. Проверено ✅: строка
**города** погоды (`WEATHER_SET_1`, например, `"BRA 2x1 ARG"` появилась на виджете) и поля музыки
**трек/исполнитель**; список **контактов** (20 × имя[32]+номер[25]) работает как прокручиваемая панель
данных. Всё это push-уведомления, а не постоянные усложнения.

---

## 14. AT-канал завода / оболочки (`77d4ff01` / `77d4ff02`)

Отдельный текстовый AT-канал, независимый от фреймового протокола. ✅ проверено вживую:

- **Чтение:** `AT GETSECRET` (16-байтный секрет сопряжения), `GETVERSION`, `GETSN`, `GETNAME`, `GETPID`,
  `GETBATLV` (сырые мВ, например, `3853mv`), `GETGSENSOR` (сырой акселерометр в g, `X=… Y=… Z=…`),
  `GETNTCTEMP` (°C, внутренний NTC).
- **Запись / действие:** `AT SETMOTOR=1` (вибрация мотора), `SETHR/SETHRV/SETSPO2=…` (тестовая
  инъекция датчиков), `SETLCDSWITCH/SETGPSSWITCH/SETKEYSWITCH`.

Ответы заканчиваются на `,OK`. Команды `SET*` обычно выполняются, но могут не возвращать `,OK` по BLE — проверяйте
в каждом конкретном случае.

---

## 15. Функции, ограниченные прошивкой / недоступные (🔎 RE прошивки)

Некоторые функции присутствуют в прошивке, но отключены SKU/регионом и **недоступны с телефона/BLE** —
для них нужна модификация прошивки, что выходит за рамки здесь:

- **ChatGPT голос** — шлюз = id функции `ux2sys` `0x9e`, заполняется из NVRAM/EFUSE/региона при загрузке; на
  этом SKU флаг поддержки `908b = 00`. Не влияет ни телефон, ни аккаунт, ни BLE (подтверждено
  экспериментом + RE). Приложение — лишь ретранслятор; аудио идёт телефон → облако Nothing.
- **Кровяное давление** — полная подсистема существует в прошивке, отключена SKU/регионом.
- **Alipay / NFC-платежи** — полный UI присутствует, только для китайского SKU.
- **Отсутствует в аппаратном обеспечении/прошивке:** ЭКГ, SOS/экстренная помощь, универсальный NFC.

---

## 16. Id источников данных / геттеров усложнений

Не нужны для создания BLE-клиента, но **критичны для создания или рендеринга циферблата**: это значение
`meta[9]` (§11.11), которое привязывает виджет к живым данным, `id` условия видимости (§11.12) и
записи меню метрик слота (§11.13). Прошивка разрешает его через **таблицу диспетчеризации геттеров из 142 записей**
по адресу `0x101f371c`, каждая запись вызывает `ux2sys_get(type)` (🔎 RE прошивки).

### Время / дата — ✅ хорошо установлено

| id | значение | id | значение |
|---|---|---|---|
| `0x01` | час (12/24ч по настройке устройства) | `0x0f`, `0x12` | секунда (плавная) |
| `0x04` | час (24ч) | `0x10`, `0x11` | десятки / единицы секунд |
| `0x07` | час (принудительно 24ч) | `0x71`, `0x72` | секунда (тикающая / угол стрелки) |
| `0x02`, `0x03` | десятки / единицы часа-12ч | `0x13` | флаг AM/PM (0 = AM, 1 = PM) |
| `0x05`, `0x06`, `0x08`, `0x09` | десятки / единицы часа | `0x15`, `0x16` | месяц |
| `0x0a`, `0x70` | угол часовой стрелки | `0x17` | день месяца |
| `0x0b` | минута | `0x18` | день недели (0 = понедельник ⚠️) |
| `0x0c`, `0x0d` | десятки / единицы минут | `0x0e`, `0x71` | угол минутной стрелки |

Id десятков/единиц рисуют **одну цифру** — виджет, привязанный к одному из них, отображает один глиф, а не всё
значение (циферблат 284 Square). Id углов стрелок (§11.5): `0x0a`/`0x70` час = `ч·30° + м·0,5°`,
`0x0e`/`0x71` минута = `м·6° + с·0,1°`, `0x12`/`0x72` секунда = `с·6°` (✅ подтверждено дизассемблированием
геттеров, запасной вариант RTC 10:10:30).

### Здоровье / датчики / погода — ⚠️ спорно, читайте колонку доказательств

| id | значение (лучшее текущее прочтение) | доказательства |
|---|---|---|
| `0x19` | **шаги** | в меню слотов 368 рядом с `0x1a`; совпадает с `parse.ts` этого репозитория |
| `0x1a` | **пульс** | в меню слотов 368 рядом с `0x5f` и `0x19` |
| `0x1c` | калории | меню слотов, иконка пламени в сопутствующем приложении |
| `0x1e` | калории (псевдоним) | корпус |
| `0x22` / `0x23` | дистанция км / мили (целая часть) | корпус |
| `0x74` / `0x75` | дистанция км / мили (дробная часть) | корпус |
| `0x76` | дистанция (форма слота) | меню слотов, иконка дороги |
| `0x24` | **заряд батареи %** | меню слотов, иконка молнии |
| `0x30` | заряд батареи % | корпус |
| `0x36` / `0x5f` | температура | `0x5f` = меню слотов, иконка облако-солнце; 361 TempoG привязывает обычное число |
| `0x48` | периоды активности (часы стояния) | меню слотов, иконка стоящей фигуры |
| `0x8b` | ИКВ | меню слотов |
| `0x73` | флаг 24ч / метрических единиц | корпус |
| `0x25`–`0x27`, `0x49`, `0x6c`, `0x6f` | % цели / псевдонимы слотов шагов и калорий | корпус |
| `0x6a` | ⚠️ неопознанная метрика слота | появляется в 4 меню слотов |
| `0x79 + slotIndex` | **синтетический** — не метрика; id выбора слота (§11.13) | ✅ §11.13 |

> ⚠️ **Три таблицы в этом репозитории расходились; эта — согласование.** Более ранние редакции §16
> и §11.5 читали `0x19` как пульс, `0x1b` как заряд батареи, `0x24` как температуру и `0x36` как шаги —
> и `wfweb/src/codec/mock.ts` до сих пор кодирует это прочтение, тогда как `wfweb/src/codec/parse.ts` кодирует
> другое (`0x19` шаги, `0x24` % цели, `0x48` периоды активности, `0x1a` погода). Таблица выше следует
> лучше обоснованному прочтению (метки откалиброваны fmc по иконкам меню слотов виджетов самого сопутствующего
> приложения, см. §Источники). **Код ещё не изменён — `mock.ts` и `parse.ts` по-прежнему не согласованы
> друг с другом и с этой таблицей.** Относитесь к меткам id здоровья как к ⚠️, пока кто-то не привяжет
> поле к каждому id и не прочитает часы.
>
> Самое сильное единичное доказательство — **меню слотов циферблата 368 Function** (§11.13), которое предлагает
> `0x5f 0x1c 0x19 0x48 0x24 0x76 0x1a 0x8b` как **восемь различных выбираемых пользователем метрик** в одном меню.
> Какими бы ни были метки, никакие две из этих восьми не могут быть одной метрикой — что исключает
> одновременное `0x19` = `0x1a` = пульс и `0x24` = `0x5f` = температура.

Кольцевые/дуговые усложнения с **листом кадров** индексируют предварительно отрендеренный кадр (например, 50 % = кадр 50 из 100),
встроенный в `.bin`, который вы отправляете (§11.5), так что внешний RES-пакет не нужен; кольца без изображений
рисуются из спецификации дуги (§11.15).

---

### Быстрые карточки (домашние плитки) — `QUICK_CARD (906D)` ✅

Домашние плитки часов. Телефон выбирает только **какие** плитки показывать и в **каком порядке** — плитки
рендерит прошивка (канала контента нет). Первый байт полезной нагрузки = подкоманда: `00` = GET, `01` =
SET; **обе используют `0x906D`** (`0x906C` указан, но не используется — запрос к нему истекает по таймауту). Ответ = `A06D`.

> 🛑 Отправка выдуманного `assemblyId` **стирает экраны часов** (они принимают список, не могут сопоставить
> id, ничего не показывают). Отправляйте только те id, которые вы **считали** через GET; восстановление — через официальное приложение или
> сброс к заводским настройкам.

**Ответ GET** ✅: `status(1) ‖ 00 ‖ N(1) ‖ N × группа`, группа = `tag=01 ‖ K(1) ‖ K×(assemblyId, sportId)`.
Реальный кадр: `01 00 04  01 02 5d00 6100  01 03 1900 2e00 2300  01 03 5c00 0400 5a02  01 03 4800 5100 5300`
= 4 экрана / 11 карточек (`5a02` = карточка Sport, sportId 2).

**Слоты:** на каждом экране **4 слота**. Тип карточки задаёт её размер — `circular`/`square` = 1 слот,
`rectangle` = 2 слота. Проверка — чистая арифметика слотов (Σ ≤ 4 на экран); взаимоисключающих
карточек нет. `sportId` равен `0`, кроме карточек Sport (87–91). Id `64` и `95` не существуют.

**Каталог `assemblyId`** (каждый логический тип = непрерывный диапазон из 6 вариантов стиля `_0`..`_5`;
`0` = пустой слот):

| дес. | карточка | дес. | карточка |
|----|----|----|----|
| 0 | пустой слот | 49–53,97–98 | Погода |
| 1–6 | Шаги | 54–58 | Таймер |
| 7–12 | Калории | 59–62 | Дыхание |
| 13–18 | Периоды активности | 63,65–67 | Секундомер |
| 19–24 | Умеренная активность | 68–71 | Батарея |
| 25–30 | Пульс | 72–76 | Недавние |
| 31–36 | SpO₂ | 77–81 | Контакты |
| 37–42 | Стресс | 82–86 | Циферблат / телефон |
| 43–48 | Сон | 87–91 | Спорт (`sportId` ≠ 0) |
| 92 | Музыка | 93/94/96 | Запись активности / PAI / Цикл |

---

## Приложение A. Каталог стоковых циферблатов (id → имя)

§11 ссылается на циферблаты по числовому id (275 SlopeTime, 322 Glare 2, 357 Silhouette, …). Id
`273`–`376` — это магазинные/стоковые лица; id — это то, что `DIAL_COMMAND (9055/a055)` сообщает как активный и
что `9075` принимает как `old_id` (§11.4). 100 из 103 известных id названы ниже; остальные —
заглушки ROM (§11.4). Имена и группировка — как показывает официальное сопутствующее приложение.

- **По умолчанию** (6) — `273` Activity Mood · `274` Sun Circle · `275` SlopeTime · `276` Dichotomy · `277` Prismatic Time · `280` Multifunction
- **Аналоговые** (34) — `286` Sundial · `287` Simple Dial · `292` City · `294` Sudoku · `305` Dots · `306` Large Number · `309` Gradient · `310` Glare · `311` Bold · `313` Classical · `314` Fragment · `315` Infinite · `316` Trailing · `322` Glare 2 · `326` Chrono Master · `327` Digit Max · `328` Coherent · `329` Wheel · `330` Zenith · `331` Intersection · `335` Time Phase · `336` Energetic · `338` Chronos · `341` Dual View · `346` Time Windmill · `347` Large Panel · `349` Theatre · `352` Elegant Sweep · `360` Explorer · `364` SportPulse · `370` Hemisphere · `371` ActiveTrio · `372` Time Wheel · `373` Traditional Pointer
- **Цифровые** (41) — `281` Metaball · `282` Radar Sweep · `283` Radio · `285` Widgets · `288` Type · `289` Rotate · `290` Gradual · `291` Vertical · `293` Stairs · `296` Ladder · `297` Ray · `298` Eclectic · `299` Echo · `300` Mono Dial · `301` Orbit · `302` Calendar · `303` Space · `307` Sprung · `308` Sundial 2 · `319` One Line · `320` Orienteer · `321` Revolution · `323` Dash · `324` Finesse · `325` Metric · `333` Circularity · `334` Globe of Time · `337` Time Finder · `339` Suprematism · `340` Sport Mode · `345` Time Dot · `350` Timeline · `351` Cyclopes · `353` Dual phase · `357` Silhouette · `359` Ring data · `361` TempoG · `362` Steady · `365` Elegance · `369` Solar System · `376` Digits time
- **Многофункциональные** (10) — `304` Elaborate 2 · `344` InfoMeter · `348` Tumbler · `354` Dual · `363` Vintage · `366` Combo · `367` Complex Figure · `368` Function · `374` Cirquary · `375` InfoHub
- **Креативные** (8) — `284` Square · `312` Disc · `317` Disc 2 · `318` Dominos · `332` Flux · `342` Perfect Match · `343` Progress Day · `358` Asteroid
- **Дивали** (1) — `295` Vortex

---

## Источники

Байтовые раскладки выше реконструированы из прошивки (1.0.0.73), официального APK (3.5.7) и
расшифрованных живых захватов с реального устройства. Эталонная реализация этого проекта находится в
`core-rust/src/{commands,frame,crypto,health,session}.rs` (Rust), редакторе TypeScript `wfweb/` и
Python-инструментах `cmftool/` (`pair.py`, `session.py`, `wf_codec.py`, `upload_custom.py`, …).

**Включённые независимые работы.** [freethinkel/fmc](https://github.com/freethinkel/fmc) — это
редактор циферблатов SvelteKit + маркетплейс для тех же часов — независимо провёл обратную разработку
формата `.bin` на корпусе из ~100 лиц и достиг нескольких результатов, которых в этом документе не было или которые были неверны.
Их `docs/cmf-protocol.md`, `src/lib/modules/editor/lib/{wf,render}.ts` и
`src/lib/modules/device/lib/ble.ts` стоит прочитать напрямую. Принятые находки, каждая повторно проверена
по байтам циферблатов перед включением сюда:

| Находка | Где | Статус здесь |
|---|---|---|
| Оба заголовочных слова — CRC32 (нестандартный вариант) | §11.4 | ✅ повторно проверено 9/9 циферблатов; **исправляет** «нет блокирующей контрольной суммы» |
| Условия видимости, тег `0x02` | §11.12 | ✅ повторно проверено; было недокументировано |
| Список метрик слота + активный `activeIdx` по умолчанию, `0x79 + slotIndex` | §11.13 | ✅ повторно проверено на 275/368/273/304; **заменяет** §11.8 |
| Флаг возможности акцентного оттенка `meta[7] == 4` | §11.14 | ✅ распространённость повторно проверена; было недокументировано |
| Полная спецификация дуги (углы развёртки, ширина, радиус) | §11.15 | ✅ повторно проверено; **исправляет** «`max` u16 `@+4`» |
| Хвостовые `u16` ref — размеры блоков, а не id глифов | §11.11 | ✅ повторно проверено точно на атласе из 10 глифов |
| 36-байтный футер; вариант магического байта `0x02`; `0x86` = 64 Б; встроенный предпросмотр `0x28` | §11.4, §11.11 | ✅ повторно проверено |
| Метки id здоровья/погоды откалиброваны по меню слотов сопутствующего приложения | §16 | ⚠️ принято как лучшее прочтение; конфликты отмечены |
| UUID сервиса оболочки — `77d4e67c-…`, и область видимости `optionalServices` Web Bluetooth | §1 | ⚠️ отчёт с одного устройства, здесь не повторно проверено |
| Число с id, совпадающим с кольцом, отображает процент | §11.15 | 🟡 сообщено, **не** проверено здесь |
| Каталог стоковых циферблатов id → имя | Приложение A | ✅ принято как есть |
Скачать инструмент
НазначениеСервисХарактеристикаСвойства
Запись команд0000fff0-0000-1000-8000-00805f9b34fb0000fff2-…Write
Уведомления команд0000fff0-…0000fff1-…Notify
Запись оболочки (AT)—77d4ff01-2fe2-2334-0d35-9ccd078f529cWrite
Уведомления оболочки (AT)—77d4ff02-…Notify
Запись массовых данных—02f00000-0000-0000-0000-00000000ffe1Write
Уведомления массовых данных—02f00000-…ffe2Notify
Имяcmd1,cmd2
TIMEFFFF 8004
FIRMWARE_VERSION_GET / _RETFFFF 8006 / FFFF 0006
SERIAL_NUMBER_GET / _RET00DE 0002 / 00DE 0001
BATTERY005C 0001
TRIGGER_SYNC005C 0002
USER_INFO_SET / _RET 🔎✅0095 0001 / 0095 0003
FACTORY_RESET009A 0001
DEVICE_REBOOT 🔎FFFF 9080
RESOLUTION_GET 🔎 (→ 466×360)FFFF 907F
GPS_PUSH / _RETFFFF 906A / FFFF A06A
UNBIND_SET / _RETFFFF 907A / FFFF A07A
Имяcmd1,cmd2
AUTH_PHONE_NAMEFFFF 8049
AUTH_WATCH_MACFFFF 0049
AUTH_PAIR_REQUEST / _REPLYFFFF 8047 / FFFF 0048
AUTH_NONCE_REQUEST / _REPLYFFFF 804B / FFFF 004C
AUTHENTICATED_CONFIRM_REQUEST / _REPLYFFFF 804D / FFFF 0004
AUTH_FAILEDFFFF A061
Имяcmd1,cmd2
APP_NOTIFICATION0065 0001
INCOMING_CALL ⚠️0064 0001
CALL_REMINDER_REQUEST / _RESPONSEFFFF 9066 / FFFF A066
FIND_PHONE005B 0001
FIND_WATCH005D 0001
FIND_WATCH_TOGGLEFFFF 9069
SMS_MESSAGE_PUSH / _RETFFFF 906E / FFFF A06E
QUICK_REPLY_SET / _RETFFFF 9073 / FFFF A073
Имяcmd1,cmd2
ALARMS_SET / _GET0063 0001 / 0063 0002
CONTACTS_SET / _GET00D5 0001 / 00D5 0002
STANDING_REMINDER_SET / _GET0060 0001 / 0060 0002
WATER_REMINDER_SET / _GET0061 0001 / 0061 0002
TASK_REMINDER_SET / _RET ⚠️FFFF 9072 / FFFF A072
Имяcmd1,cmd2
GOALS_SET / _ACK005E 0001 / 005E 0003
UNIT_LENGTH / _ACKFFFF 9067 / FFFF A067
UNIT_TEMPERATURE / _ACKFFFF 9068 / FFFF A068
TIME_FORMAT / _ACK005F 0001 / 005F 0003
WAKE_ON_WRIST_RAISE / _GET / _ACK0062 0001 / 0062 0002 / 0062 0003
LANGUAGE_SET / _RETFFFF 9058 / FFFF A06B
HEART_MONITORING_ENABLED_SET / _GET009B 0001 / 009B 0002
HEART_MONITORING_ALERTSFFFF 9059
DO_NOT_DISTURB / _GET0099 0001 / 0099 0002
SPORTS_SET / _GET00DC 0001 / 00DC 0002
SPORT_LINKAGE_SET / _RETFFFF 9076 / FFFF A076
SPORT_DATA_SYNC 🔎 (живой HR/калории/шаги)FFFF 9078 / FFFF A078
FEMALE_CYCLE_SET / _RETFFFF 9071 / FFFF A071
SLEEP_CONFIG_SET / _RET (целевой минимум)FFFF 9074 / FFFF A074
WORLD_CLOCK_GETFFFF 906F
WORLD_CLOCK_DST_SET / _RETFFFF 9083 / FFFF A083
VITALITY_GET / _RETFFFF 9079 / FFFF A079
VITALITY_SW_SET / _RETFFFF 9070 / FFFF A070
Имяcmd1,cmd2
DIAL_COMMAND_SET / _RET (список/переупорядочивание/выбор)FFFF 9055 / FFFF A055
DIAL_CONFIG_SET / _RETFFFF 9075 / FFFF A075
CHANGE_DIAL (⚠️ неактивен на 1.0.0.73 — не использовать)009F 0001
QUICK_CARD_SET/GET / _RET (оба на 906D)FFFF 906D / FFFF A06D
Имяcmd1,cmd2
ACTIVITY_FETCH_1 / _2FFFF 8005 / FFFF 9057
ACTIVITY_FETCH_ACK_1 / _2FFFF 0005 / FFFF A057
ACTIVITY_DATA0056 0001
SLEEP_DATA / _GET0058 0001 / 0058 0002
SPO20055 0001
STRESS009D 0001
HEART_RATE_MANUAL_AUTO0053 0001
HEART_RATE_RESTING00DA 0001
HEART_RATE_WORKOUT00E0 0001
SKIN_TEMP_HISTORY 🔎 (пусто на этом SKU)0155 0001 / 0155 0002
WORKOUT_SUMMARY / _V30057 0001 / 0160 0001
WORKOUT_GPSFFFF A05A
ДоменINIT1 req/replyINIT2 req/replyCHUNK req/writeFINISH ack1/ack2
Циферблат (фото)8052/00529063/A063A064/9064A065/9065
Циферблат (структурированный/переключение)8052/00529075/A075A064/9064A065/9065
Прошивка9052/A0529040/A040A042/9042A041/9041
AGPS/EPO905E/A05E—A05F/905FA060/9060
СмещениеРазмерПоле
04метка времени (эпоха, с)
44шаги
84дистанция (м)
124калории
1616зарезервировано (наблюдалось 0)
СмещениеРазмерПоле
04начало_сессии (эпоха, UTC)
44пробуждение (эпоха, UTC)
82всего_глубокий_с
102всего_основной_с
122всего_БДГ_с
142всего_бодрств_с
162⚠️ [неопределённо] (id/оценка сессии? наблюдаемые значения не совпадают с суммами записей)
ac 49 1f 01
  • CONTACTS_SET (00D5 0001) ✅: N × 57 байт = name(32) ‖ phone(25). Интерфейс часов показывает до 20.
  • ALARMS_SET (0063 0001) ✅ — исправляет Gadgetbridge (который помещал метку в конец, с заполнением 0xff — неверно). 40 байт на будильник, big-endian: secondsOfDay(i32) ‖ index(u8) ‖ enabled(u8) ‖ repetition-bitmask(u8) ‖ flag(u8) ‖ label[32] UTF-8. Метка находится на смещении 8 и отображается на часах. repetition = битовая маска дней недели (0 = одноразовый); flag ⚠️ [неопределёнен] (маркер одноразового?). Пример (13:30, idx 2): 0000bdd8 02 01 15 00 "Alarm…".
  • GOALS_SET (005E 0001) ✅ — официальное приложение и эталонная реализация используют 10-байтовый, big-endian DailyTargetBean v1: steps(u32 BE) ‖ distance_m(u32 BE) ‖ calories_kcal(u16 BE). (Это форма Gadgetbridge; более ранние сообщения, что часы «игнорировали» его, были ошибкой расшифровки устаревшей сессии, а не проблемой payload.) 🔎 РЕ прошивки также показывает более длинный 29-байтовый расширенный вариант (добавляет sleep_min/exercise_min/stand_h + 6 флагов включения, все u32 BE после префикса flag(u16 LE), с принудительными диапазонами: шаги 2000–30000, дист 1000–99000, кал 100–5000, сон 360–720, упражнения 30–90, стояние 6–16) — не путь по умолчанию приложения; предпочитайте 10-байтовую форму, если не нужны дополнительные цели.
  • STANDING_REMINDER / WATER_REMINDER (0060/0061 0001) ✅: 11 байт: enabled(1) ‖ threshold_min(u16 LE) ‖ dndStart(u32 LE) ‖ dndEnd(u32 LE). Обратите внимание, что «активное окно 08:00–22:00», показанное в интерфейсе, — это фиксированный дефолт прошивки и не передаётся в payload.
  • SPORTS_SET (00DC 0001) ✅: count(1) = 36 слотов ‖ activityTypeCode[36] (активные коды, затем заполнение 00). Выбирает, какие виды спорта появляются в меню тренировок часов.
  • HEART_MONITORING_ENABLED (009B 0001) ✅: байт kind — 01 = HR 24/7, 02 = SpO₂, 04 = стресс (измеряется каждые 30 мин).
  • HEART_MONITORING_ALERTS (FFFF 9059) ✅: отключено = 00; включено = 01 ‖ hrLow ‖ hrHigh ‖ sportHrHigh ‖ spo2Low ‖ 00 00 00 00 (граница 0/255 = «без лимита»).
  • FEMALE_CYCLE (FFFF 9071) ✅: 01 ‖ predictionOpen ‖ notifySwitch ‖ cycleStartSwitch ‖ cycleStartNotifyBefore ‖ ovulationStartSwitch ‖ ovulationStartNotifyBefore ‖ fertileStartSwitch ‖ fertileStartNotifyBefore ‖ period(1) ‖ cyclePeriod(1) ‖ cycleStartDate(u32) ‖ markStart(u32) ‖ markEnd(u32) (захвачено: period=5, cyclePeriod=0x1c=28).
  • QUICK_REPLY (FFFF 9073) ✅: TLV — count(1) ‖ total(1) ‖ [id(1) ‖ len(u16 LE) ‖ msg-UTF8]… (захвачено и расшифровано 7 ответов по умолчанию).
  • WORLD_CLOCK (FFFF 906F) ✅: отправляет числовые ID городов, а не названия (01 ‖ count ‖ cityId(2 BE)…); часы сопоставляют id из внутренней таблицы. Конфигурация DST FFFF 9083 = count ‖ [id(u16 LE) ‖ dst(u16 LE) ‖ start(u32 LE) ‖ end(u32 LE)]….
  • MUSIC_INFO_SET (FFFF 905C, 131 Б) ✅: state(1: 0=нет/1=пауза/2=играет) ‖ volume(1) ‖ volumeMax(1) ‖ track(64) ‖ artist(64). Часы также отправляют обратно MUSIC_BUTTON (A05D).
  • WEATHER_SET_1 (FFFF 906B, 199 Б) ✅ — используйте этот: 7×9-байтовых дней + 24×2-байтовых часов + город(32) + 7×8-байтовых восход/закат (LE). Температуры кодируются как (temp_c + 100) & 0xFF. ⚠️ Тот же payload, отправленный на WEATHER_SET_2 (0066 0001), не обновляет виджет погоды на Pro 2 — всегда используйте 906B. (Строка города также является доказанным вектором перехвата данных — см. §13.)
  • FIND_WATCH (005D 0001) ✅: payload 0x01 → часы звонят/вибрируют (+ ACK 005D 0003).
  • GPS_PUSH (FFFF 906A) ✅ — big-endian, сначала долгота: 16 байт ts(u32 BE) ‖ lon×1e7(i32 BE) ‖ lat×1e7(i32 BE) ‖ 00 00. Проверено на реальном местоположении.
  • WORKOUT_GPS (FFFF A05A) ✅ — little-endian, сначала долгота: 12 байт ts(i32) ‖ lon×1e7(i32) ‖ lat×1e7(i32).
  • TIME (FFFF 8004): см. §7.
  • cfbppraster (after LZ4)use
    42RGB565-LEopaque background (FULL/THUMB)
    53RGB565-LE (2 B) + alpha (1 B) per pxanti-aliased sprites (glyphs, hands, icons)
    13 (0x0d)0.54-bit alpha mask; firmware tints at runtimedigit-glyph atlas
    24 (0x18)4RGBA8888full-colour layers (incl. the always-on aodImage)
    1—JPEG/JFIF (ff d8 ff), extract with any decoderrare animation frames
    0a
    validate_rejects_bad_containers
    0x61
    NotEnvelope
    0a
    ChildOverflow
    :
    61 0a 00
    −18/−16
    смещения записи тоже должны сдвигаться
    0a
  • Слоты усложнений с несколькими вариантами — активная метрика НЕ в .bin ⚠️ этот пункт был неверен и заменён §11.13. Настраиваемое усложнение создаётся как N узлов групп 0x68 , сложенных в одной точке (x,y), каждый рисуется только при совпадении условия видимости — это было верно. Но список метрик для слота (275-й 0x1c/0x6a/0x48/0x24/0x19/0x76) — это именно список метрик, а не "идентификаторы опций/стилей"; индекс активного по умолчанию — это байт в файле; а байт 0x79/0x7a — не "байт экземпляра", а идентификатор, по которому ключуются альтернативы (0x79 + slotIndex). Статический предпросмотр может воспроизвести значение по умолчанию из файла. См. §11.13.
  • Неактивные усложнения, привязанные к краю (например, текст bpm в (446,0), виден на 275/302/325/365/375) — это слоты, которые прошивка не рисует в представлении по умолчанию — их значение даже не помещается перед краем холста. Считайте их скрытыми в предпросмотре.
  • 0x22
    Стрелки
    0x22
    aod
    0x22
    @69,209
    изолированный UI редактирования Normal|AOD
  • Автономный 0x60 img_number (cnt=10) — источник на −5, прямое смещение на один. ✅ Та же ошибка смещения на один, что и в §11.8, но для нечасовых чисел: дата "Gradient" находилась в (203,80) по центру сверху с источником 0x17, но прямое сканирование 82 захватывало геттер угла соседнего указателя (0x0a) и позицию указателя → число отображалось в месте указателя с ложным источником. Исправление: для 61 0a 00 img_number в обёртке 0x60 доверяйте −5/−18/−16, когда прямой источник невозможен для числа (источник-0 или геттер угла указателя 0x0a/0e/12/70/71/72) и позиция −18/−16 допустима и ненулевая (защита от нуля пропускает цифры-дети групп с relX=0).
  • 0x17 = дата (день месяца), 0x24 = температура — различны. Циферблат 340 использует оба (0x17 "Jun 09" и отдельную 0x24 температуру), поэтому 0x17 — это дата, а не температура. Циферблат, на котором часы показывают температуру в слоте 0x17, — это пользовательское усложнение (состояние устройства), а не значение по умолчанию из файла.
  • тегрольконтейнер?тело
    0x20корень сцены (обёртка тела, не отрисовываемый элемент)✅дети
    0x21обычный экран✅дети
    0x22экран AOD (§11.9)✅дети
    0x28встроенная миниатюра предпросмотра каталога✅один дочерний 0x08
    0x68группа / контейнер автопозиционирования✅кадр 0x48 + дети
    0x30статическое изображение или выбор по значению из N изображений✅0x01 (+0x02)
    0x60живое числовое считывание (полоса цифр)✅0x01 + 0x40 (+0x02)
    0x70вращающаяся стрелка✅0x01 + точка поворота 0x05
    0x80кольцо прогресса, процедурное✅0x01 + 0x5a (§11.15)
    0x81кольцо прогресса, с обрезкой по изображению✅0x01 + 0x5b (§11.15)
    0x85назначаемый пользователем слот усложнения✅0x01 + 0x5f (§11.13)
    0x01struct — геометрия + атрибуты (ниже)—x,y,meta[14] + хвост ссылок
    0x02условие видимости (§11.12)—список условий
    0x05точка поворота — flag u8, pivotX u16, pivotY u16—5 Б
    0x08pvStruct — prefix[5] + хвост ссылок, без x/y (только предпросмотр)——
    0x40количество цифр / флаг дополнения нулями (§11.10)—1 Б
    0x48кадр — x,y,w,h,gap,align строка/столбец автопозиционирования——
    0x5a / 0x5bспецификация дуги для 0x80 / 0x81 (§11.15)—19 Б / 17 Б
    0x5fсписок метрик слота для 0x85 (§11.13)——
    0x86узел отображаемого имени, всегда ровно 64 байта, завершается NUL, не рисуется—64 Б
    циферблатслоткол-воactiveIdxid метрик→ активный
    275 SlopeTime0601c 6a 48 24 19 760x1c калории
    275 SlopeTime1641c 6a 48 24 19 760x19 шаги
    368 Function0805f 1c 19 48 24 76 1a 8b0x5f температура
    368 Function1865f 1c 19 48 24 76 1a 8b0x1a пульс
    273 Activity Mood0401c 24 48 6a0x1c калории
    304 Elaborate 20/1401c 48 6a 24 / 24 1c 6a 480x1c / 0x24