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

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

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

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

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

Категории

Все категории
Loading categories
Инструменты/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, процедуру аутентификации и синхронизацию данных о здоровье для разработки альтернативного сопутствующего приложения.

РепозиторийСайт
324 дней назадЕщё не проверено

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, смещение/длина массовой передачи имеют обратный порядок байт).

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

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

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

Тестовое устройство для всех захватов: 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), координируемые управляющими кодами операций на канале команд.

✅ Целая реальная сессия прошла по одному каналу команд — во время 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` вместе образуют **opcode** (см. §6). 🔎 подтверждено с помощью фреймбилдера официального приложения (`C6117b.m30831g`).
- `chunkCount` = общее количество чанков для этой команды; `chunkIndex` **1-индексированный**.
- `chunkLen` = количество байт `chunk` в данном фрейме.
- Одна BLE-запись может быть фрагментирована по MTU канала; получатель буферизует сырые байты и повторно извлекает полные фреймы. Крупные полезные нагрузки разбиваются на несколько чанков (тот же `cmd1/cmd2`, увеличивающийся `chunkIndex`) и собираются по порядку.

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

- `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` фрейма.

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

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

✅ Все наблюдаемые значения `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‑байтовый секрет устройства (получается с часов через команду оболочки 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

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

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, полезная нагрузка 0xA5) заставляет часы ответить опкодом SET (cmd2 = 0x0001) с текущим значением. Команды SET подтверждаются с cmd2 = 0x0003 и пустым телом.


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

Кадры шифруются 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) всегда передаются открыто, поэтому последовательность команд видна в любом захвате даже без ключа — только зашифрованные полезные нагрузки требуют 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-слое. Их заголовки появляются в захватах, но семантика полезной нагрузки ⚠️ [неопределена].

Передача массовых данных (канал данных)

Циферблат/прошивка/AGPS используют цикл init → chunk‑request/chunk‑write → finish‑ack:

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


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

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

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

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₂ / Stress ✅

  • Ручной/автоматический HR, тренировочный HR, SpO₂, стресс = по 8 байт: timestamp(i32 LE) ‖ value(i32 LE) (value = bpm / SpO₂ % / индекс стресса).
  • Resting HR (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 = REM, 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, aerobic/anaerobic, recoveryTime, VO₂max, cadence, PAI, лучшие времена бега…). Набор полей известен (из Room DB приложения), но точные смещения байтов внутри этого 40-байтового блока ⚠️ [неопределены] — для их закрытия нужен один сырой захват GPS-тренировки.


9. Избранные полезные нагрузки команд

Строки в 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; целые числа полезной нагрузки LE; исключения — GOALS_SET и GPS_PUSH big-endian, а смещение/длина массовой передачи big-endian.
  • MTU: размеры чанков вычисляются так, чтобы зашифрованные чанки выравнивались по 16-байтовым блокам AES.
  • authkey можно сохранить (сохраняйте его после первой привязки); sessionKey существует для каждого соединения и вычисляется из nonce часов при каждом переподключении.

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

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

Что на самом деле работает (✅ подтверждено вживую): создание фотоциферблата из любого изображения и его установка; установка любого из 103 магазинных циферблатов офлайн; изменение обложки структурированного циферблата (замена фона или любого не фонового спрайта) и перемещение его слоёв; переупорядочивание/переключение активного лица; и создание структурированного циферблата с нуля — конверт сцены 0x20 декодирован и построитель реализован (§11.7), доказано офлайн, что он проходит все 103 магазинных циферблата байт-в-байт и выдаёт синтетические контейнеры, проходящие собственный валидатор прошивки. 🟡 единственный непроверенный шаг — наблюдение за синтетическим рендерингом с нуля на устройстве через 9075 (офлайн-доказательство структуры уже охватывает то, что раньше вызывало отказ 0a). Нет барьеров в кодеках или транспорте и нет необходимости в инструментарии вендора. Старые утверждения «структурированный рендер запакован в RES-пакет/невозможен через 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 (так официальное приложение переключает лица; нет специального опкода «установить активный»).
  • Удаление циферблата = повторно отправьте список без его идентификатора.
  • 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:~
Байт ответа: `01` = активировано и сохранено; `0a` = сохранено, но **не** активировано / отклонено. На Android каждый `DATA_CHUNK_WRITE` должен отправляться как **одно BLE-запись на фрейм** — объединение и повторная нарезка по MTU нарушает синхронизацию заголовков, и часы циклически запрашивают смещение 0.

- **`9063` (фото) = ДОБАВИТЬ.** Список циферблатов увеличивается (6→7); `watchfaceId = 0xFFFFFFFF` (пользовательский сторожевой маркер), поэтому он никогда не отклоняется как дубликат, и часы автоматически его активируют.
- **`9075` (структурированный) = ЗАМЕНИТЬ** слот `old_id`. `old_id` **должен** уже быть в списке (иначе `0a`). Чтобы переустановить уже присутствующий id, **сначала удалите его** (9055 list-minus-id), затем загрузите "новый" — повторное использование id на месте даёт `0a`.

### 11.3 Фото / пользовательский циферблат — ✅ полностью проверено от начала до конца

**Контейнер** (проверка байтов в прямом и обратном порядке; все поля 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 = standard LZ4 block over RGB565 little-endian, top-down (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 Структурированный / магазинный циферблат — контейнер и кодеки ✅

**Заголовок** (идентичен для всех 103 магазинных циферблатов; все поля little-endian):```
0x00  perDialId  u32 LE     [per-dial id/hash; NOT a content checksum — 4 "Default" dials share one]
0x04  version    0x00000001 [constant]
0x08  name       char[]     [NUL-terminated, e.g. "SlopeTime", "Metaball"]
0x18  size_a     u32 LE     [= filesize − 36]   ✅ 100 % confirmed across 103 dials
0x1c  size_b     u32 LE     [data-section length, < size_a]
0x20  3× u32 LE  id/hash words [not a CRC]
0x2c  name       (repeated on larger dials)
~0x60 directory of layer records (61 xx 00 …) then the asset pool

There is no blocking checksum (CRC32/Adler32/byte-sum all fail to match) — repacking is not gated. 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); рисуемое **тело листа** (теги `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 font metrics] ‖ 40 01 00 ‖ flag ‖
  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)`. Например, большая часовая цифра Digit Max 327 — это таблица из 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`, inline `61 1 base`
  = диск) + под `0x5b` (`max` u16 `@+4`, =100 кроме 332=60). Реализовано в wfweb (`blendSector`).
- **Идентификатор источника данных** — блок атрибутов `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)`).
  Обычные идентификаторы (§16): `0x07` час, `0x0b` минута, `0x0f` секунда, `0x16` месяц, `0x18` день недели,
  `0x13` AM/PM, `0x19` ЧСС, `0x1b` заряд батареи %, `0x24` температура, `0x36` шаги, `0x70/71/72` углы стрелок,
  `0x25–27` процент цели. (Это соответствует примеру группы `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`); проходит все 103 циферблата побайтово + синтетический проходит верификатор прошивки (§11.7). 🟡 Рендер на устройстве через `9075` ещё не заснят |
| Системные шрифты (`.font`) | ✅ **декодирование/рендер (все)** | LVGL bin (не проприетарный); 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.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 генерирует правильные вложенные окна).
  • validate_rejects_bad_containers — отвергает плоское тело (→ , историческая ошибка ) и дочерний элемент, выходящий за своё окно (→ ).

🟡 Всё ещё не проверено (нужны часы, не блокирующе): загрузка синтезированного с нуля через 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) заставляло защиту отбрасывать его. Оба парсите как знаковые и допускайте небольшой отрицательный диапазон.
  • Цифры цифровых часов могут быть 0x60 img_numbers верхнего уровня (не только внутри группы 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) — поэтому он выводил вариант "всегда включён" (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 была неверна — ширина относится только к расположению, а не к количеству.)


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

Таблица передачи находится в §6. Дополнительно подтверждённые моменты:

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

13. Датчики

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

  • Оптический ФПГ — частота сердцебиения (ручной/автоматический/тренировка/покой), 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]) работает как прокручиваемая панель данных. Всё это отправляемые данные, а не постоянные усложнения.


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

16. Таблица геттеров усложнений (🔎 внутренний справочник прошивки)

Не требуется для построения BLE-клиента — включено для полноты. Рендерер циферблатов прошивки привязывает каждый слот усложнения к числовому идентификатору геттера (таблица диспетчеризации на 142 записи). Избранные идентификаторы: 0x07 час, 0x0a комбинированный угол часов, 0x0b минута, 0x0f секунда, 0x18 день недели, 0x19 частота сердцебиения, 0x1b уровень заряда батареи, 0x24 температура, 0x36 шаги, 0x70/0x71/0x72 угол часовой/минутной/секундной стрелки, 0x25–0x27 процент цели. Кольцевые/дуговые усложнения индексируют предварительно отрендеренный лист кадров (например, 50 % = кадр 50 из 100), а не попиксельную дугу — кадры встроены в отправляемый .bin (§11.5), поэтому внешний набор RES не требуется.


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

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

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

Ответ GET ✅: status(1) ‖ 00 ‖ N(1) ‖ N × group, group = 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 = Спортивная карточка, sportId 2).

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

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


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

Скачать инструмент
НазначениеСервисХарактеристикаСвойства
Запись команд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 🔎 (live HR/cal/steps)FFFF 9078 / FFFF A078
FEMALE_CYCLE_SET / _RETFFFF 9071 / FFFF A071
SLEEP_CONFIG_SET / _RET (target min)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 (list/reorder/select)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 запрос/ответINIT2 запрос/ответCHUNK запрос/записьFINISH подтверждение1/подтверждение2
Циферблат (фото)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)
82total_deep_s
102total_core_s
122total_rem_s
142total_awake_s
162⚠️ [неопределено] (идентификатор сессии/оценка? наблюдаемые значения не совпадают с суммами записей)
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; более ранние сообщения, что часы «игнорируют» его, были ошибкой расшифровки устаревшей сессии, а не проблемой полезной нагрузки.) 🔎 RE прошивки также показывает более длинный 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», отображаемое в интерфейсе, является фиксированным значением по умолчанию прошивки и не передаётся в полезной нагрузке.
  • SPORTS_SET (00DC 0001) ✅: count(1) = 36 слотов ‖ activityTypeCode[36] (активные коды, затем дополнение 00). Выбирает, какие виды спорта отображаются в меню тренировок часов.
  • HEART_MONITORING_ENABLED (009B 0001) ✅: байт kind — 01 = 24/7 HR, 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) ✅: отправляет числовые идентификаторы городов, а не названия (01 ‖ count ‖ cityId(2 BE)…); часы сопоставляют идентификаторы из внутренней таблицы. Конфигурация 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-байтовых часов + city(32) + 7×8-байтовых восход/заход (LE). Температуры кодируются как (temp_c + 100) & 0xFF. ⚠️ Та же полезная нагрузка, отправленная на WEATHER_SET_2 (0066 0001), не обновляет виджет погоды на Pro 2 — всегда используйте 906B. (Строка города также является проверенным вектором перехвата данных — см. §13.)
  • FIND_WATCH (005D 0001) ✅: полезная нагрузка 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
    0x61
    NotEnvelope
    0a
    ChildOverflow
    :
    61 0a 00
    −18/−16
    смещения записи также должны переместиться
    0a
  • Мульти-вариантные слоты усложнений: активная метрика НЕ в .bin. ⚠️ Настраиваемое усложнение создаётся как N узлов групп 0x68, расположенных в одних и тех же (x,y), каждый привязан к разному источнику (два круга 275-го: 0x1e/0x6a/0x48/0x24/0x19 для каждого слота — идентификаторы опций/стиля, не отображаемая метрика); два круга побайтово идентичны, за исключением их прямоугольника + байта экземпляра (0x79/0x7a). Какая метрика показывается (ШАГИ vs ККАЛ vs …) — это состояние ОЗУ/конфигурации устройства, поэтому статический предпросмотр не может воспроизвести её из файла — только по возможности.
  • Неактивные усложнения, привязанные к краю (например, текст 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, когда прямой источник невозможен для числа (source-0 или получатель угла указателя 0x0a/0e/12/70/71/72) и позиция −18/−16 действительна и не равна нулю (защита от нуля пропускает цифры-дочерние элементы группы с relX=0).
  • 0x17 = дата (день месяца), 0x24 = температура — различаются. Циферблат 340 использует оба (0x17 "Jun 09" и отдельный 0x24 температура), поэтому 0x17 — это дата, а не температура. Циферблат, на котором часы показывают температуру в слоте 0x17, — это пользовательское усложнение (состояние устройства), а не значение по умолчанию из файла.
  • decкартаdecкарта
    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–36SpO₂77–81Контакты
    37–42Стресс82–86Циферблат / телефон
    43–48Сон87–91Спорт (sportId ≠ 0)
    92Музыка93/94/96Запись активности / PAI / Цикл