Skip to content
KitploitKITPLOIT
ToolsBlog
Einreichen
ToolsBlog
Einreichen

Hacking-, PenTest- und Cybersicherheits-Tools für Ihr Sicherheitsarsenal!

Kitploit ist ein Verzeichnis von Hacking-, Cybersicherheits- und Pentesting-Tools. Entdecken Sie die neuesten Projekt-Updates, um Schwachstellen zu finden, Systeme zu analysieren, Tests zu automatisieren und Ihre Sicherheit zu stärken.

··Feeds·Kontakt·Datenschutz·© 2026 Kitploit

Tool-Verzeichnis

Kategorien

Alle Kategorien anzeigen
Loading categories
CMF-Watch-Pro-2-BLE-Protocol — Reverse-engineertes BLE-Protokoll für die CMF Watch Pro 2, das das GATT-Layout, AES-128-CBC-verschlüsselte Befehlsframes, den Authentifizierungs-Handshake und die Synchronisierung von Gesundheitsdaten für die Entwicklung alternativer Companion-Apps dokumentiert. | Kitploit
Tools/GitHubGitHub/joshuapassos/cmf-watch-pro-2-ble-protocol
Embedded-System-SicherheitBluetooth-SicherheitIoT-SicherheitReverse EngineeringDrahtlose SicherheitKryptographieMobile SicherheitHardware- & IoT-SicherheitFirmware-Analyse

Beliebteste

Alle anzeigen →

Entdecken Sie die meistgenutzten Tools unserer Community.

Alle Tools erkunden

Durchsuchen Sie unsere Tool-Sammlung

Alle Tools anzeigen →
Teilen
GitHubjoshuapassos/cmf-watch-pro-2-ble-protocol

CMF-Watch-Pro-2-BLE-Protocol

Reverse-engineertes BLE-Protokoll für die CMF Watch Pro 2, das das GATT-Layout, AES-128-CBC-verschlüsselte Befehlsframes, den Authentifizierungs-Handshake und die Synchronisierung von Gesundheitsdaten für die Entwicklung alternativer Companion-Apps dokumentiert.

Repository anzeigenWebseite
328vor 1 MonatNoch nicht geprüft

CMF Watch Pro 2 — BLE-Protokoll (reverse-engineered)

Inoffiziell. Dieses Dokument beschreibt das Bluetooth-Low-Energy-Protokoll (BLE) der CMF Watch Pro 2 (CMF by Nothing), rekonstruiert durch Reverse Engineering für eine alternative Begleit-App. Es steht in keiner Verbindung zu Nothing/CMF und wird von diesen nicht unterstützt. Nutzung auf eigene Gefahr.

Alle Mehrbyte-Ganzzahlen im Frame-Header und in den Opcodes sind Big-Endian. Ganzzahlen innerhalb von Befehls-Payloads sind Little-Endian, sofern nicht anders angegeben (dies spiegelt die Geräte-Firmware wider) — achten Sie auf die Ausnahmen (GOALS_SET, GPS_PUSH, Bulk-Transfer-Offset/-Länge sind Big-Endian).

Vertrauensmarkierungen

Jede nicht offensichtliche Behauptung unten ist mit der Art ihrer Ermittlung gekennzeichnet:

  • ✅ auf dem Gerät validiert — in einem entschlüsselten Live-Mitschnitt beobachtet oder gegen eine echte Uhr getestet.
  • 🔎 aus Firmware/APK-RE — durch Dekompilieren der Firmware (1.0.0.73) oder der offiziellen APK (3.5.7) extrahiert; konsistent mit dem Code, aber nicht zur Laufzeit getestet.
  • 🟡 teilweise belegt — strukturell etabliert (offline, korpusweit oder per RE), aber der verbleibende Schritt benötigt die Uhr und wurde noch nicht ausgeführt.
  • ⚠️ [unsicher] — abgeleitet, nicht bestätigt; kann falsch sein.

Wo ein späterer Abschnitt einen früheren korrigiert, wird der frühere Text mit einem Verweis beibehalten statt gelöscht — zu wissen, welche Messwerte versucht und widerlegt wurden, erspart der nächsten Person denselben Umweg.

Testgerät für alle Mitschnitte: CMF Watch Pro 2-5485, Firmware 1.0.0.73, Seriennummer CI04102520008192, MCU Actions ATS3089C (Cortex-M4), Bildschirm 466×360.


1. GATT-Layout

Das Telefon ist der GATT-Client; die Uhr ist das Peripheriegerät und wirbt als CMF Watch Pro 2-XXXX (4 Hex-Zeichen).

Aktivieren Sie Benachrichtigungen durch Schreiben von 01 00 in jedes CCCD (00002902-…). Der Befehlskanal (fff1/fff2) überträgt das unten beschriebene gerahmte Protokoll. Der Shell-Kanal (77d4…) überträgt einfachen AT-Stil-Text (z. B. AT GETSECRET; siehe §14). Der Datenkanal (02f0…) überträgt große Binär- Blobs (Watchface, Firmware, AGPS), koordiniert durch Steuer-Opcodes auf dem Befehlskanal.

Dienst-UUIDs — die Uhr bewirbt ~10 primäre Dienste. Auf einem echten Gerät aufgelistet: 0xfff0 (Befehl), 0x180f (Akku), 0x180a (Geräteinformationen), 0xefe7, 0xffd0, 02f00000-…ffe0 und 02f00000-…fe00 (Daten), 77d4e67c-2fe2-2334-0d35-9ccd078f529c (Shell/ Pairing), e49a3001-f69a-11e8-8eb2-f2801f1b9fd1, f48a23c0-f69a-11e8-8eb2-f2801f1b9fd1.

⚠️ Die Shell-Dienst-UUID ist 77d4e67c-…, nicht 77d4ff00-…. Frühere Revisionen dieses Dokuments gingen davon aus, dass der Dienst das ff00-Präfix seiner Charakteristiken teilt (77d4ff01/77d4ff02, §14) — das tut er nicht, zumindest nicht auf dem Gerät, auf dem dies geprüft wurde (Erkenntnis aus freethinkel/fmc, siehe §Quellen). Die Charakteristik-UUIDs sind unverändert. Nicht verifiziert, ob 77d4e67c über Geräte hinweg stabil ist — aufzählen statt fest codieren.

🌐 Web-Bluetooth-Hinweis. Chromium entdeckt nur Dienste, die die Seite in optionalServices aufgelistet hat, selbst bei einem ungefilterten getPrimaryServices()-Aufruf — eine Seite, die 3 Dienste auflistet, sieht 3, während chrome://bluetooth-internals (Chromiums eigene C++-Ebene, unbegrenzt) alle 10 zeigt. Wenn Sie einen Browser-Client schreiben, listen Sie jede oben genannte UUID im Voraus auf, sonst schlägt das Pairing fehl mit Diensten, die offensichtlich existieren. Kein Web Bluetooth in Firefox/Safari; benötigt eine Benutzergeste + HTTPS/localhost.

✅ Eine vollständige reale Sitzung lief auf dem einzigen Befehlskanal — während eines 160 s langen Mitschnitts mit starker Nutzung gab es keinen Datenverkehr auf den Daten-/Firmware- oder Shell-Kanälen, außer während einer expliziten OTA-/Watchface- Übertragung.


2. Frame-Format (0xF5)

Jede Befehlskanal-Nachricht ist in einen oder mehrere Frames mit 11-Byte-Header verpackt:``` +------+-----------+--------+-------------+-------------+--------+-------------------+ | 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` bilden zusammen den **Opcode** (siehe §6). 🔎 bestätigt gegen den Frame-Builder der offiziellen App (`C6117b.m30831g`).
- `chunkCount` = Gesamtzahl der Chunks für diesen Befehl; `chunkIndex` ist **1-basiert**.
- `chunkLen` = Anzahl der Bytes von `chunk` in diesem Frame.
- Ein einzelner BLE-Write kann durch die Link-MTU fragmentiert werden; der Empfänger puffert die Rohbytes und extrahiert die vollständigen Frames erneut. Große Payloads werden in mehrere Chunks aufgeteilt (gleiches `cmd1/cmd2`, aufsteigender `chunkIndex`) und in der Reihenfolge wieder zusammengesetzt.

### Opcode-Konvention (✅ im Datenverkehr bestätigt)

- `cmd1 = 0xFFFF`: `cmd2` in `0x80xx`/`0x90xx` = Telefon→Uhr (Anfrage/Setzen); `0x00xx`/`0xa0xx` = Uhr→Telefon (Antwort). Paare stimmen im niederwertigen Byte überein (`0x9055`↔`0xa055`, `0x8051`↔`0x0051`).
- funktionsspezifisches `cmd1`: `cmd2`-Suffix = `0x0001` **SET**, `0x0002` **GET**, `0x0003` **ACK**.

### Chunk-Body

Für jeden Chunk ist der Body `payloadPiece ‖ CRC32_LE(payloadPiece)` (4-Byte-CRC, Little-Endian, zlib/IEEE). Wenn der Befehl **verschlüsselt** ist (siehe §3), wird das gesamte `payloadPiece ‖ CRC` anschließend mit AES-128-CBC/PKCS7 verschlüsselt, und dieser Chiffretext wird zum Frame-`chunk`.

**Klartext-Eigenheit:** Bei Klartext-Opcodes *zählt* die Uhr die 4-Byte-CRC in `chunkLen`, überträgt sie aber **nicht**. Beim Dekodieren eines Klartext-Frames beträgt die tatsächliche Datenlänge also `chunkLen − 4`. (Verschlüsselte Frames tragen die CRC wie üblich innerhalb des Chiffretexts.)

Chunk-Größe (damit verschlüsselte Chunks auf AES-Blockgrenzen landen), mit `maxWrite = mtu − 3`:
- verschlüsselt: `floor((maxWrite − 11) / 16) * 16 − 4 − 1`
- Klartext: `maxWrite − 11 − 4 − 1`

✅ Alle beobachteten `chunkLen`-Werte verschlüsselter Frames waren Vielfache von 16 (Blockausrichtung gilt).

---

## 3. Kryptografische Grundbausteine

- **AES-128-CBC** mit **PKCS7**-Padding und einem **festen IV** (aus der Firmware `CmfCharacteristic.AES_IV`):
  `50 51 52 53 54 55 56 57 60 61 62 63 64 65 66 5A`.
- **CRC32** (zlib/IEEE), ausgegeben als 4 Little-Endian-Bytes.
- **SHA-256** über die Verkettung der Teile.

Schlüsselableitung:```
authkey      = SHA256( rnd1 ‖ rnd2 ‖ secret )[0..16]      // persisted across sessions
sessionKey   = SHA256( nonce ‖ authkey )[0..16]           // per connection
  • secret = 16-Byte-Gerätegeheimnis (vom Gerät über den Shell-Befehl abrufbar AT GETSECRET → GETSECRET:<32-hex>,OK).
  • rnd1 = 16 vom Telefon gewählte Zufallsbytes; rnd2 = 16 Zufallsbytes vom Gerät.
  • nonce = Bytes aus der Nonce-Antwort des Geräts.

Nachdem der Schlüssel gesetzt ist, sind alle Befehls-Kanal-Frames AES-verschlüsselt, außer den Klartext- Opcodes, die in §5 aufgeführt sind.

✅ Beide Ableitungen validiert: authkey, aus der ntwatch.db eines gerooteten Telefons wiederhergestellt, stimmte mit dem aus einem erfassten rnd1/rnd2/secret abgeleiteten Wert überein; sessionKey, aus einem erfassten Nonce reproduziert, entschlüsselt Live-Frames.


4. Authentifizierungs-/Pairing-Handshake

Zwei Einstiegspfade teilen sich denselben Nonce-/Bestätigungs-Teil.

4.1 Erstmaliges Pairing (Gerätegeheimnis vorhanden)```

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:~
Bei `AUTH_FAILED (0xFFFF,0xA061)` oder einer Signatur-Abweichung schlägt die Authentifizierung fehl.

### 4.2 Wiederverbinden (authkey bereits bekannt)```
        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

✅ Die Wiederverbindungsreihenfolge (ohne Shell-Traffic) wurde in einem echten Capture intakt beobachtet.

4.3 Post-Auth-Init (Phase 2)

⚠️→✅ TIME ist vor Datenabfragen zwingend erforderlich. Nach Initialized beantwortet die Uhr nicht BATTERY, SERIAL_NUMBER_GET oder den ACTIVITY_FETCH_*-Handshake, bis ein TIME (FFFF 8004) in der Sitzung gesendet wurde — ohne dieses kommt nur eine unaufgeforderte FIRMWARE_VERSION_RET an und alles andere läuft in einen Timeout. ✅ live bestätigt (Pixel 8a): Senden der drei GETs ohne TIME → nur Firmware antwortet; Senden von TIME zuerst → Batterie und Seriennummer beginnen zu antworten.

Empfohlene Phase-2-Reihenfolge: TIME → FIRMWARE_VERSION_GET → SERIAL_NUMBER_GET → BATTERY (0xA5) → Konfig-Pushes → Health-Sync (§8).

4.4 GET → SET-Echo-Muster (✅)

Es gibt keinen separaten „Lese“-Opcode für die meisten Einstellungen. Das Senden eines *_GET (cmd2 = 0x0002, Payload 0xA5) bewirkt, dass die Uhr mit dem SET-Opcode antwortet (cmd2 = 0x0001), der den aktuellen Wert trägt. SET-Befehle werden mit cmd2 = 0x0003 und leerem Body bestätigt.


5. Klartext vs. verschlüsselt

Frames sind AES-verschlüsselt, sobald ein Schlüssel gesetzt ist, außer bei diesen Opcodes, die immer im Klartext sind:

  • 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)

Frame-Header (cmd1/cmd2) reisen immer im Klartext, sodass die Befehlssequenz in jedem Capture auch ohne Schlüssel sichtbar ist — nur verschlüsselte Payloads benötigen sessionKey.


6. Opcode-Referenz (cmd1, cmd2)

GET/SET/REQUEST = Telefon→Uhr; RET/REPLY/ACK/RESPONSE/DATA = Uhr→Telefon.

Sitzung / Gerät

Auth

Benachrichtigungen / Anruf / Suchen

Musik

Namecmd1,cmd2
MUSIC_INFO_SET / _ACKFFFF 905C / FFFF A05C
MUSIC_BUTTONFFFF A05D

Alarme / Kontakte / Erinnerungen

Konfiguration

Wetter

Namecmd1,cmd2
WEATHER_SET_1 (der funktionierende)FFFF 906B
WEATHER_SET_2 (auf Pro 2 ignoriert — siehe §9)0066 0001

Watchfaces / Zifferblätter

Health / Sync

Nur-JS-Opcodes (FFFF 8051, FFFF 0051, FFFF 90A2, FFFF 90C5, FFFF A056, FFFF 908A/908B ChatGPT-Status/Support) werden im Hermes-Bytecode der App verarbeitet, nicht in der Java- Schicht. Ihre Header erscheinen in Captures, aber die Payload-Semantik ist ⚠️ [unsicher].

Massendatenübertragung (Datenkanal)

Watchface / Firmware / AGPS verwenden eine Init → Chunk-Request/Chunk-Write-Schleife → Finish-Ack:

(alle cmd1 = FFFF.) Die Uhr steuert die Schleife, indem sie DATA_CHUNK_REQUEST_*(offset, length) ausgibt (offset/length = u32 Big-Endian); das Telefon antwortet mit DATA_CHUNK_WRITE_*, das payload[offset..offset+length] auf der Daten-Charakteristik trägt. Details siehe §11–§12.


7. Zeit & Zeitzone

TIME (FFFF 8004)-Payload = epochSeconds(i32, BE) ‖ utcOffsetMillis(i32, BE). Wird direkt nach Auth gesendet, damit die Uhr die lokale Zeit anzeigt (und Datenabfragen freischaltet — siehe §4.3).

⚠️ Health-Zeitstempel von der Uhr sind UTC. Die Companion-App muss den lokalen UTC-Offset hinzufügen, bevor sie den lokalen Kalendertag / die Tageszeit ableitet. (Das Bucketing von Health-Daten nach rohem UTC-Tag verschiebt den Tageswechsel auf die falsche lokale Zeit.)

TIME_FORMAT (005F 0001)-Payload = 1 Byte: 00 = 24h, 01 = 12h.


8. Health-Sync

  1. Telefon sendet ACTIVITY_FETCH_1; Uhr antwortet ACTIVITY_FETCH_ACK_1 (erstes Byte 01 ⇒ bereit).
  2. Telefon sendet ACTIVITY_FETCH_2; die Uhr pusht dann einen Burst von Datenframes: ACTIVITY_DATA, HEART_RATE_*, SPO2, STRESS, SLEEP_DATA, WORKOUT_SUMMARY[_V3].
  3. Jedes wird in Pro-Minuten-Samples / Sitzungen geparst und nach lokalem Tag aggregiert.

Der Sync ist sequenziell (muss TIME folgen; die Uhr gibt die Streams nach ACK_2 frei), kein einzelner Burst. Eine schwere Sitzung pusht ~170–210 Benachrichtigungsframes in ~160 s. ✅

8.1 Aktivitätsdatensatz — ACTIVITY_DATA (je 32 Bytes, LE) ✅

Kalorieneinheit: Aktivitätskalorien werden in cal (Gramm-Kalorien) gemeldet. Teilen Sie die Tages- summe durch 1000, um kcal zu erhalten. (Workout-Summary-Kalorien sind dagegen bereits in kcal.)

8.2 HR / SpO₂ / Stress-Samples ✅

  • Manuelle/automatische HR, Workout-HR, SpO₂, Stress = je 8 Bytes: timestamp(i32 LE) ‖ value(i32 LE) (value = bpm / SpO₂ % / Stressindex).
  • Ruhe-HR (00DA 0001) ist anders — 5 Bytes: timestamp(i32 LE) ‖ hr(u8). ✅ Live-Beispiel 5e dc 29 6a 4e → ts, hr = 78 bpm. Stress-Score-Bereiche: 1–29 / 30–59 / 60–79 / 80–99.

8.3 Schlaf — SLEEP_DATA (18-Byte-Header + N × 8-Byte-Datensätze) ✅

Ein SLEEP_DATA = eine Schlafsitzung; eine Nacht kann mehrere enthalten (Mikro-Aufwachen teilen Sitzungen).

Header:

Jeder 8-Byte-Datensatz: timestamp(u32) ‖ duration_s(u16) ‖ stage(u16). Stadiencodes: 1 = Tiefschlaf, 2 = Kern/leichter Schlaf, 3 = REM, 4 = Wach. ✅ validiert gegen eine ganze Nacht (zwei Sitzungen, D/C/R/A-Summen stimmen überein).

8.4 Workout-Summary — WORKOUT_SUMMARY v1 (54 Bytes) / _V3 (0160 0001)

v1: start(u32), end(u32), duration_s(u32), dann Typ/Kalorien/Schritte/Distanz/Ø-HR und ein GPS/erweiterter Block. ✅ v1-Layout gegen Firmware bestätigt. WORKOUT_SUMMARY_V3 ist ein neueres Layout für dieselben Daten plus einen ~40-Byte-erweiterten Block (exerciseLoad, aerob/anaerob, recoveryTime, VO₂max, Kadenz, PAI, Bestzeiten…). Der Feldsatz ist bekannt (aus der Room-DB der App), aber die exakten Byte-Offsets innerhalb dieses 40-Byte-Blocks sind ⚠️ [unsicher] — zum Schließen braucht es ein Roh- Capture eines GPS-Workouts.


9. Ausgewählte Befehls-Payloads

Strings sind UTF-8, byte-truncated auf die Feldgröße (Truncation kann ein Multi-Byte-Zeichen teilen, passend zum s.encode()[:max]-Verhalten der Firmware); kurze Felder werden rechts mit Nullen aufgefüllt.

  • APP_NOTIFICATION (0065 0001) ✅: iconCode(1) ‖ 0x00 ‖ when(u32 BE) ‖ titleLen(1) ‖ title ‖ body. iconCode wählt das App-Symbol (WhatsApp=8, Telegram=12, Instagram=18, Gmail=27; unbekannt=0xFF). Titel ≤ 20 Bytes, Body ≤ 128 Bytes. Von einem Client gesendet → Uhr zeigte es an + ACK 0065 0003.
  • BATTERY (005C 0001) ✅: Antwort = level(1) ‖ charging(1) (z. B. 3b 00 = 59 %, nicht ladend).
  • SERIAL_NUMBER_RET (00DE 0001) ✅: len(1) ‖ ASCII (z. B. 10 + "CI04102520008192").
  • USER_INFO (0095 0001) ✅: height_cm(1) ‖ weight_kg(1) ‖ age(1) ‖ gender(1: 1=M) (z. B. = 172 cm / 73 kg / 31 / männlich).

10. Implementierungshinweise & Eigenheiten

  • Keine Systemuhr in Codecs: Encoder nehmen now/utc_offset als explizite Parameter (deterministisch, testbar). Der Transport liefert die echte Zeit.
  • TIME sperrt alles (§4.3) — zuerst senden, sonst bleibt die Uhr bei Datenabfragen stumm.
  • Klartext-CRC-Zählung (§2) ist leicht falsch zu machen — Klartext-Frames kündigen die CRC an, lassen sie aber weg.
  • Endianness: Header + Opcodes BE; Payload-Ganzzahlen LE; Ausnahmen — GOALS_SET und GPS_PUSH sind Big-Endian, und Bulk-Transfer-Offset/Länge sind Big-Endian.
  • MTU: Chunk-Größen werden so berechnet, dass verschlüsselte Chunks auf 16-Byte-AES-Blöcke ausgerichtet sind.
  • authkey ist persistierbar (nach dem ersten Pairing speichern); sessionKey ist pro Verbindung und wird bei jeder Wiederverbindung aus der Watch-Nonce abgeleitet.

11. Watchfaces / Zifferblätter — Erstellung

Die Uhr unterstützt (a) Foto/Benutzerdefinierte Zifferblätter (ein Hintergrundbild + eine firmwaregezeichnete Digitaluhr) und (b) strukturierte Zifferblätter (eingebaute/Store-Faces: ein Hintergrund plus positionierte Sprite-Ebenen, Zeiger und Text-Widgets). Beide werden über den Datenkanal via der Init → Chunk-Schleife in §6 übertragen.

Was tatsächlich funktioniert (✅ live validiert): Erstellen eines Foto-Zifferblatts aus jedem Bild und Installieren desselben; Offline-Installieren eines der 103 Store-Zifferblätter; Reskinning eines strukturierten Zifferblatts (Hintergrund oder ein beliebiges Nicht-Hintergrund-Sprite tauschen) und Verschieben seiner Ebenen; Neuordnen / Wechseln des aktiven Faces; und Erstellen eines strukturierten Zifferblatts von Grund auf — die 0x20-Szenen-Hülle ist dekodiert und der Builder implementiert (§11.7), offline bewiesen, um alle 103 Store- Zifferblätter byte-für-byte zu round-trippen und synthetische Container zu erzeugen, die den eigenen Validator der Firmware bestehen. 🟡 der einzige unbewiesene Schritt ist das Beobachten eines von Grund auf synthetischen Renders auf dem Gerät über 9075 (der strukturelle Offline-Beweis deckt bereits ab, was früher die 0a-Ablehnung verursachte). Es gibt keine Codec- oder Transportbarriere und keine Notwendigkeit für die Vendor-Toolchain. Die alten Behauptungen „strukturiertes Rendern ist RES-pack-gebacken / über BLE unmöglich“ und „cf=0x1f serverseitiger Codec“ waren falsch (ein Offset+Bytes-pro-Pixel-Bug) — die Firmware rendert strukturierte Zifferblätter datengetrieben aus der Datei, die Sie senden.

11.1 Zifferblatt-Verwaltung — DIAL_COMMAND (9055 / a055) ✅

  • Typ 0 = Liste abfragen. Antwort a055 = result(u8) ‖ selectIndex(u8) ‖ total(u8) ‖ max(u8) ‖ N × dialId(u32 LE) ‖ ffffffff. Beispiel: 01 05 06 07 … = aktiv #5, 6 Zifferblätter, max 7.
  • Typ 1 = Neuordnen / aktives auswählen: die gesamte Liste mit dem Ziel-Zifferblatt an Index 0 erneut senden (so wechselt die offizielle App Faces; es gibt keinen dedizierten „Set Active“-Opcode).
  • Ein Zifferblatt löschen = Liste ohne seine ID erneut senden.
  • CHANGE_DIAL (009F 0001) ist inert auf FW 1.0.0.73 (gibt eine Konstante zurück, wechselt nicht) — nicht verwenden.

11.2 Übertragungsablauf ✅```

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` = aktiviert & gespeichert; `0a` = gespeichert, aber **nicht** aktiviert / abgelehnt. Unter
Android muss jedes `DATA_CHUNK_WRITE` als **ein BLE-Write pro Frame** rausgehen — das Verketten und
erneute Aufteilen nach MTU desynchronisiert die Header und die Uhr fragt in einer Schleife nach Offset 0.

- **`9063` (Foto) = APPEND.** Die Zifferblattliste wächst (6→7); `watchfaceId = 0xFFFFFFFF` (eigener
  Sentinel), sodass es nie als Duplikat abgelehnt wird, und die Uhr aktiviert es automatisch.
- **`9075` (strukturiert) = REPLACE** des `old_id`-Slots. `old_id` **muss** bereits in der Liste sein
  (sonst `0a`). Um eine bereits vorhandene ID erneut zu installieren, **zuerst löschen** (9055 Liste-minus-ID)
  und dann „frisch" hochladen — die Wiederverwendung einer ID an Ort und Stelle ergibt `0a`.

### 11.3 Foto / benutzerdefiniertes Zifferblatt — ✅ vollständig Ende-zu-Ende validiert

**Container** (byte-verifizierter Round-Trip; alle Felder 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 über RGB565 Little-Endian, Top-Down (payloadLen zählt ab dem ersten LZ4-Byte). Die offizielle App verwendet LZ4-HC und entfernt den 21-Byte-LZ4-Block-Header/Footer; ein reiner Literals-only-LZ4-Encoder funktioniert ebenfalls — die Uhr akzeptiert jedes gültige LZ4, Byte-Identität ist nicht erforderlich. Pixel außerhalb des eingeschriebenen Kreises (Mittelpunkt 233,233, Radius 233) werden auf 0x0000 gesetzt.

INIT_2 für 9063 — exakter Header (✅ dieser funktioniert):``` 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` = exakte `.bin`-Länge; `FFFFFFFF` = benutzerdefinierte `watchfaceId`; `styleId` 0–4 wählt das eingebaute
Digitaluhr-Layout (es wird immer gezeichnet — es gibt kein „aus"); `posX/posY` positionieren es (bekannt gut
56 / 77); `color565` färbt es (z. B. `FFFF` = Weiß). ⚠️ Die kürzere Form `A5 ‖ size ‖ watchfaceId` wird
mit Finish `0a` **abgelehnt** — verwenden Sie den vollständigen Header oben. (Referenzimplementierung:
`core-rust/engine.rs::build_wf_init2`, spiegelt `C6135t.m31104u` in der offiziellen App wider.)

**Rezept:** Bild auf 466×466 skalieren (und ein 270×270-Thumbnail), in RGB565-LE von oben nach unten konvertieren,
optional die Pixel außerhalb des Kreises auf Null setzen, jeweils LZ4-komprimieren, den Container oben zusammenbauen und
über die `9063`-Pipeline mit `watchfaceId = 0xFFFFFFFF` hochladen. (Referenz-Codec: `core-rust/watchface.rs`,
`work/codec_dfa.py`.)

### 11.4 Strukturiertes / Store-Zifferblatt — Container & Codecs ✅

**Dateilayout** — der 36-Byte-Header wird **byte-identisch als 36-Byte-Footer** am Dateiende wiederholt
(✅ an 15 Zifferblättern verifiziert; ein Parser sollte eine Datei ablehnen, bei der sie sich unterscheiden):```
[36-byte header][scene TLV (§11.7)][asset pool][36-byte header again]

Header (identische Struktur über alle 103 Store-Dials; alle Felder im Little-Endian-Format):``` 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:~
> ⚠️ **Korrektur (ersetzt „es gibt keine blockierende Prüfsumme“).** Frühere Revisionen lasen `@0x00` als
> eine Per-Dial-ID/Per-Dial-Hash und `@0x20` als „3× u32-ID/Hash-Wörter \[kein CRC]“ und stellten fest, dass CRC32/Adler32/
> Byte-Summe alle nicht übereinstimmen. Beide Wörter **sind** CRC32 — die früheren Tests übersahen sie, weil die
> Variante nicht standardkonform ist und weil die Lesart „3 Wörter bei `0x20`“ das einzelne CRC-Wort
> mit den ersten Bytes des Szenen-Containers vermischte, der bei `0x24` beginnt (ebenso ist „Name bei
> `0x2c` wiederholt“ der `0x86`-Namensknoten der Szene, §11.11). Erkenntnis aus
> [freethinkel/fmc](https://github.com/freethinkel/fmc); hier erneut verifiziert.

**CRC32-raw** = reflektiertes IEEE-Polynom `0xEDB88320`, **`init = 0`** und **kein finales XOR** — d. h.
weder das `init=0xFFFFFFFF` noch das `^0xFFFFFFFF` des Standard-`crc32`. Das ist der einzige Grund,
warum handelsübliches CRC32 nie übereinstimmte. Beachte die Reihenfolge-Abhängigkeit: `crc_assets` liegt innerhalb des Bereichs,
der von `crc_tree` abgedeckt wird, also **schreibe zuerst `@0x20`, dann berechne `@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])

Verifiziert: 9/9 unberührte Store-Zifferblätter stimmen bei beiden Wörtern überein, und 6/6 der eigenen Vorlagen dieses Repos stimmen bei crc_tree überein.

🟡 Die Firmware scheint keine der beiden CRCs durchzusetzen. Jedes Zifferblatt, das dieses Repo über 9075 installiert hat — einschließlich Reskins, die über den In-Place-Edit-Pfad mit gleicher Fußabdruckgröße (§11.6) erzeugt wurden, der Asset-Payloads und X/Y-Bytes ohne Neuberechnung des Headers mutiert — wurde auf dem Gerät problemlos gerendert. Eine veraltete CRC ist also nicht die Ursache für eine 0a-Ablehnung (das ist die Container-Fenster-Invariante, §11.7). Behandle die CRCs als trotzdem korrekt schreiben: günstig und das einzige bekannte Integritätsfeld im Format. Alles, was die Szene oder den Asset-Pool neu schreibt, sollte beide Wörter neu berechnen.

Stub-Zifferblätter (~173 B, z. B. IDs 273/274/277) sind Platzhalter für in ROM eingebrannte Zifferblätter: Header + Verzeichnis, keine echten Assets.

Assets — jedes ist dimsWord(u32 LE) ‖ len(u32 LE) ‖ LZ4(payload), wobei cf = dimsWord & 0x1f, w = (dimsWord >> 10) & 0x7FF, h = (dimsWord >> 21) & 0x7FF, und len zählt ab dem ersten LZ4-Byte (das 1f 00 01 00, das man dort oft sieht, ist das erste LZ4-Token — nicht überspringen). Dekomprimierte Größe = w·h·bpp:

✅ Alle 4151/4151 Assets über die 103 Zifferblätter dekodieren exakt mit einem Standard-lz4.block-Dekompressor bei w·h·bpp. Transparenz ist das Alpha-Byte (cf=5/24) oder 0x0000 (cf=4 außerhalb des Kreises) — es gibt kein RLE und kein „Escape". Kodieren = neu rastern → Standard-LZ4 → [dimsWord][len][LZ4].

INIT_2 für 9075 — AES-verschlüsselter Body:``` kind(1) ‖ old_id(u32 LE) ‖ new_id(u32 LE) ‖ file_len(u32 LE)

root@kitploit:~
`kind` = `0x02`/`0x03`; `old_id` = aktive aktuelle Dial (von `9055`); `file_len` = echte `.bin`-Größe (= `@0x18 + 36`). Das Installieren eines Store-`.bin`-Files unverändert ist der garantierte Weg (Ring Data id 359 + 102 weitere bestätigt). (Referenz: `core-rust/engine.rs::build_dial_replace_init`.)

### 11.5 Strukturierte Verzeichnis-Grammatik ✅ (dekodiert & implementiert — ÜBERARBEITET 2026-07-02)

> **⚠️ Revision (2026-07-02): das flache `61 01 00`-Recordschema unten war systematisch
> OFF-BY-ONE.** Der Szenen-Body ist ein sauberes TLV (§11.7); ein zeichenbarer **Leaf-Body** (Tags `0x30`/`0x38`
> statisch, `0x70` Zeiger) ist:
>
> ```
> 01 xx 00 [X u16][Y u16] …attrs… 61 [count u16][base u32][count×id u16] [05 05 00 01 pivX pivY]
> ```
>
> - attr `0x01` öffnet den Body: **X,Y = oben links** auf der 466²-Leinwand (das `s16 x,y` des SDK-
>   `sty_picture_t`).
> - die **Frame-Tabelle `61 …` schließt den Body** (`base` = Asset-Ptr; `count` 1 = Bild, 10/11 =
>   Ziffern-Atlas — der alte „Record-Typ `0a/0b`“ war eigentlich dieser Count! — 7/13/2 = Complication-
>   Frame-Sheet).
> - Zeiger-Extras: Quelle+Skalierung `[src] 00 3c 00` innerhalb des `0x01`-attrs; Pivot im
>   `05 05 00 01 [pivX][pivY]` **Trailer**. **Rotationszentrum = `(X+pivX, Y+pivY)` pro Zeiger** —
>   nicht ein festes (233,233): es existieren außermittige Subdials (z. B. rotieren die Zeiger von Dial 366 um 150,150).
>
> Der lineare Scan nach `61 01 00` nähte die Frame-Tabelle+Pivot von Element **N** an die X/Y
> (und das Tag-Byte, das alte „f3“) von Element **N+1** — es sah nur auf analogen Dials *richtig* aus,
> deren benachbarte Zeiger nahezu identische Geometrie teilen. Die „Compact-Variant-Wand“ (Spec 24 §24.4.5)
> war dieselbe Fehllesung. Implementiert als `scan_scene_drawables` in `core-rust/watchface_struct.rs`
> und `wfweb/src/codec/parse.ts` (Szene = primäre Quelle für Bilder/Zeiger; flacher Scan beibehalten für
> Text + Nicht-Envelope-Fallback). Validiert durch das `wfweb/compare.html`-Orakel (Render vs. offizielle
> Store-PNGs, 99 Dials): 64→72 gut, 8→5 schlecht, mittlere Differenz 9.3→7.4%.

Historische Flach-Record-Lesart (ersetzt, zur Kontextwahrung beibehalten):

- **Statisches Bild** (`61 01 00`): `asset_ptr(u32) ‖ elemId(u16) ‖ 05 05 00 01 ‖ pivotX(u16) ‖
  pivotY(u16) ‖ 3B ‖ 01 ‖ 1b 00 ‖ X(u16) ‖ Y(u16)`. Oben links auf der 466²-Leinwand = `(X−pivotX, Y−pivotY)`.
- **Zeiger/Hand** — gleicher Bild-Record, zur Laufzeit rotiert. **Rotationszentrum = `(X+pivotX, Y+pivotY)`**
  (≈ 233,233 auf analogen Dials). Die **Datenquelle ist ein `u8` bei Record-Offset `+36`**, Skalierung `u16` bei
  `+38` (=60): `0x0a`/`0x70` = Stunde (`h·30°+m·0.5°`), `0x0e`/`0x71` = Minute (`m·6°+s·0.1°`),
  `0x12`/`0x72` = Sekunde (`s·6°`). ✅ bestätigt durch Disassemblieren der Getter (RTC-Fallback 10:10:30).
- **Text-/Zahlen-Widget** (`61 0a 00`): `asset_ptr(u32) ‖ [10×u16 Font-Metriken] ‖ 40 01 00 ‖ Flag ‖
  3B ‖ 01 ‖ u16 ‖ X(u16) ‖ Y(u16)`. `asset_ptr` zeigt auf das Glyph „0“; Ziffer *d* = das Asset bei
  `index("0") + d` (10 aufeinanderfolgende cf=5-Sprites, z. B. `0123456789` und `,°`-Interpunktion). ✅ gerendert.
- **Complication-Füllung = Frame-Index** (✅ bestätigt für Ziffern-/Enum-/Gauge-Complications, count>1): der
  Wert indiziert ein **vorgerendertes Frame-Sheet** im `.bin` — `frame = (count−1)·val/100` (Prozent) oder
  `frame = value` (Flip-Ziffer / Enum). Frame-Tabelle = Sub-Record `61 ‖ count(u16) ‖ base(u32) ‖
  count×id(u16)`. Z. B. ist die große Stunde von 327 Digit Max ein 13-Frame-Sheet (Zahlen 0–12), `frame = hour`.
- **Fortschrittsring / Bogen = Laufzeit-Sektor-Clip** (✅ 2026-07-02, **korrigiert die „Ringe sind Frame-Sheets“-Lesart
  in Spec 25 §2**): Element-Tag **`0x81`** trägt eine **einzelne** volle Scheibe (`61`-Frame-Tabelle
  `count == 1`), und der Teilkeil ist diese Scheibe **auf einen Torten-Sektor geclippt** (`frac = value/max`,
  im Uhrzeigersinn ab 12 Uhr) — pixelgenau auf 322 Glare 2 verifiziert und `count==1` über
  **20 Dials** bestätigt. Auf der Platte: `0x81`-Body = Sub `0x01` (Geometrie `x@+0 y@+2 w@+4 h@+6`, inline `61 1 base`
  = Scheibe) + Sub `0x5b` (Bogen-Spec). Implementiert in wfweb (`blendSector`).
  ⚠️ Das `0x5b`-Sub-Record ist **nicht** nur „`max` u16 `@+4`“ wie zuvor dokumentiert — das las die
  untere Hälfte eines `max i32` und übersah die **Start-/End-Sweep-Winkel und Strichbreite**, die direkt
  danach liegen. Es gibt auch ein prozedurales Geschwister `0x80`/`0x5a` (mit explizitem Radius), das dieses
  Dokument nie abdeckte. Vollständiges Record-Layout und was die bisherige „im Uhrzeigersinn ab 12 Uhr“-Annahme
  falsch machte: **§11.15**.
- **Datenquellen-ID** — der `82`-Attr-Block des Elements sitzt bei `delim+3` (nach dem letzten `40 01 00`),
  und die **Quellen-ID ist ein `u8` bei `+0x14`** (auch `relX@+0x07 s16`, `relY@+0x09 s16`, `anchor@+0x0C/0E`,
  `mode@+0x15`, `frame-count@+0x1A`). Anchor < 0 = Ausrichtung an der Kante des Elternteils. 🔎 Die Firmware löst
  die ID über eine 142-Einträge-Getter-Tabelle bei `0x101f371c` auf (jeder ruft `ux2sys_get(type)`).
  ⚠️ **Bevorzuge das Lesen der ID als `meta[9]` der Struktur (§11.11)** — ein festes Feld — gegenüber diesem
  vorwärts gerichteten `82`-Attr-Scan, der die in §11.8/§11.9 beschriebene Off-by-One-Quelle ist. Vollständige ID-Tabelle in §16; beachte,
  dass die Health-/Weather-Labels, die dieser Abschnitt zuvor inline auflistete (`0x19` HR, `0x1b` Akku,
  `0x24` Temperatur, `0x36` Schritte), **umstritten und wahrscheinlich falsch sind** — siehe das ⚠️-Kästchen in §16.
  (Das `0x07:0x0b:0x0f` = HH:MM:SS-Gruppenbeispiel unten ist davon unberührt.)
- **Gruppenknoten** (`0x68`): verschachtelt seine Kinder in seinem eigenen TLV-Body (`0x60` = Wert/Text,
  `0x30` = statisch); jedes `0x60` trägt seine Quellen-ID bei `data+16`. Z. B. eine Gruppe `0x07:0x0b:0x0f` =
  HH:MM:SS-Uhr. TLV-Element-Parser = `0x100db55c` (Sprungtabelle, indiziert durch `tag−0x70`).

### 11.6 Authoring-Matrix

| Pfad | Status | Notizen |
|---|---|---|
| Foto-Dial aus beliebigem Bild | ✅ **fertig** | §11.3; auf dem Gerät validiert |
| Installieren eines beliebigen von 103 Store-Dials | ✅ **fertig** | §11.4; `9075`, `old_id`=aktiv |
| Reskin des cf=4-Hintergrunds eines Store-Dials | ✅ **funktioniert live** | FULL-Payload an Ort und Stelle tauschen, die Asset-`len` auf die **neue** Blockgröße (≤ alt) setzen, denselben Datei-Fußabdruck behalten, frisch installieren |
| Neu-Authoring per Templating (Pixel jeder Ebene tauschen + Geometrie verschieben) | ✅ **rendert über BLE** | Dial 373: bg→cyan + ein cf=5-Sprite→rot + X 224→100 verschoben, alles gerendert, Zeiger live |
| 100 %-synthetischer strukturierter Dial von Grund auf | ✅ **Builder fertig, offline validiert** | `0x20`-Envelope-Builder in `watchface_struct.rs` (`build_container`/`serialize`/`validate_container`); round-tript alle 103 Dials byte-exakt + synthetisch besteht Firmware-Validator (§11.7). 🟡 On-Device-Render über `9075` noch nicht gefilmt |
| System-Fonts (`.font`) | ✅ **Dekodieren/Rendern (alle)** | LVGL-Bin (nicht proprietär); 32 Zahlen-Fonts (`num*/nm*`, unkomprimiert) + 24 Text-Fonts (`font*`, LVGL-RLE `comp=1`) alle dekodiert — 12208 Glyphen, 0 Überläufe, volles ASCII. RLE = LVGL v8.3 `lv_font_fmt_txt.c` (3-Zustand SINGLE/REPEATE/COUNTER + zeilenweiser XOR-Prefilter), 1:1 portiert, kein Disasm |

⚠️ Reskin-/Re-Author-Fallstricke, die einen schwarzen Bildschirm oder `0a` verursachen: die **alte Asset-`len`** belassen (die
Uhr liest über den Block hinaus → Überlauf → schwarz); **die Datei vergrößern** (bei der Installation abgelehnt); eine ID
**an Ort und Stelle** wiederverwenden statt einer frischen Installation; **Assets neu anordnen**, ohne die Ref-Tail-Blockgrößen-
Kette zu fixieren (§11.11). Heute nicht fatal, aber schreibe es trotzdem korrekt: jede Änderung an der Szene oder dem Asset-Pool
macht die beiden Header-**CRC32**-Wörter ungültig — beide neu berechnen (§11.4), `@0x20` vor `@0x00`.

### 11.7 Die `0x20`-Szenen-Envelope — dekodiert & Builder implementiert ✅

Ein neu authorter **echter** Dial rendert, weil er die Szenen-Envelope der Datei bewahrt. Ein rein synthetischer
Body aus flachen `61 …`-Records wird **abgelehnt** — der Firmware-Parser (`WFManager_Parser`, `0xdb35c`)
verlangt, dass der Body (ab Offset `0x24`) mit einem `0x20`-Szenen-Container beginnt. Die vollständige Datei ist:```
[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

Die Szene ist ein sauberes verschachteltes TLV — [tag u8][len u16 LE][body], Container-Tags 0x20/0x21/0x22/0x68 rekursiv, Blatt-Drawables 0x30 (statisch) / 0x70 (Element/Zeiger) / 0x80 / 0x81 / 0x86 (Name). (Die flachen 61 01 00 / 61 0a 00-Datensätze sind Muster, die innerhalb der Drawable-Bodies leben; der alte Parser fand sie heuristisch — und nähte benachbarte Bodies zusammen, siehe die §11.5-Überarbeitung. Das Drawable-Body-Layout ist dort jetzt vollständig dekodiert.) Jedes offset+len eines Kindes muss in das Fenster seines Elternteils passen; erstes Body-Byte ≠ 0x20 → Parser-Fehler −16; ein Kind, das sein Fenster überschreitet → −2; beides lässt den 9065-Handler (0xeb50c) das Finish schreiben.

Builder ist implementiert und offline validiert (core-rust/watchface_struct.rs: SceneNode / serialize / parse_scene / validate_container / build_container / build_container_raw; CLI cmfwatch-wfgen reframe):

  • scene_roundtrip_identity — alle 103 Store-Zifferblätter: parse_scene→serialize reproduziert die Szene byte-für-byte (neu berechnete verschachtelte lens stimmen überein) und validate_container besteht bei jedem einzelnen.
  • build_reframe_identity / CLI reframe — das Zusammenbauen der gesamten .bin von Grund auf reproduziert die Datei byte-für-byte außer 1 Namens-Padding-Byte (@0x17; keine Prüfsumme).
  • build_container_synthetic — komponiert ein neues Zifferblatt (Hintergrund + Drawable verschachtelt in 20→21), das die exakte Invariante der Firmware besteht (build_container erzeugt korrekte verschachtelte Fenster).

🟡 Noch unbewiesen (braucht die Uhr, nicht blockierend): Hochladen eines synthetischen Zifferblatts von Grund auf über 9075 und beobachten, wie es gerendert wird — der Offline-Strukturbeweis deckt bereits ab, was die 0a-Ablehnung verursachte.

11.8 Render-Treue-Verfeinerungen (2026-07-02, Zifferblatt 275 „SlopeTime")

Querverweis des wfweb-Renders mit den offiziellen Store-Thumbnails (Pixel-Orakel über alle 103 Zifferblätter) und Schließen von vier Lücken:

  • Drawable/Zeiger-X/Y sind i16 (vorzeichenbehaftet). ✅ Anker können negativ sein für Elemente, die über die Leinwand hinausragen — z. B. sitzt der rote Sekundenzeiger von 275 bei Y = 0xFFFC = −4 (ein 30×281-Sprite, Quelle 0x12, von der Mitte über die Oberkante rotiert). Das Lesen von X/Y als u16 (65532) ließ die Wächterprüfung es verwerfen. Beide als vorzeichenbehaftet parsen und einen kleinen negativen Bereich zulassen.
  • Ziffern der Digitaluhr können Top-Level-0x60-img_numbers sein (nicht nur innerhalb einer 0x68-Gruppe), und die echte Datenquelle ist das u8 bei Datensatz-Offset −5 — der Vorwärts-82-Attributscan ist hier systematisch um eins daneben und greift das Attribut des nächsten Geschwisters (bei 275 nahm die Minutenziffer den Wochentag 0x18 auf). 275s „10:10" = Stunde 0x07@X≈306 + Minute 0x0b@X≈369 mit dem als benachbarter Statik dazwischen, jeweils ein 11-Glyphen-Atlas (). ⚠️ Beim Korrigieren von X/Y von müssen , sonst beschädigt ein Re-Export diese Bytes (bricht gleiche Fußabdruckgröße → ).

Außerdem: Die offiziellen Store-Thumbnails werden bei 10:10 gerendert (klassische Marketing-Zeit), nicht 10:12 — das Anpassen der Orakel-Zeit auf 10:10 reduziert die mittlere Pixel-Differenz merklich. wfwebs Parser rundet jetzt alle 103 Zifferblätter byte-exakt (die X/Y-Schreib-Offset-Korrektur oben beseitigte die letzten Abweichungen).

11.9 AOD-Container-Überspringen + eigenständige img_number-Quelle (2026-07-03, Zifferblatt „Gradient")

  • Den 0x22-AOD-Container in eine eigene Ansicht trennen. ✅ Der Szenen-Walker überspringt 0x22 bereits, aber der flache Text-/Zahlenscan durchlief das gesamte [0x30, firstAsset) — er gab also die Always-on (AOD)-Variante jedes Elements als normale Ebene aus. Bei „Gradient" zeichnete der AOD-graue Datums- Atlas (Offset in 0x22) über dem roten normalen. Fix: Jeden 0x22-Datensatz mit layer.aod=true taggen (mit eigenem Dedup-Set) und renderAt(…, aod) sie nur im AOD-Modus zeigen lassen (Normalmodus blendet aod-Ebenen aus; AOD-Modus blendet normale aus; der Hintergrund wird von setAod getauscht und zeichnet immer). Netto-Orakel-Gewinn im Normalmodus über das Korpus (284: 31%→21%, +18 andere) — die AOD-Varianten überzeichneten viele Zifferblätter — und der AOD-Umschalter des Editors zeigt jetzt das echte Always-on-Layout statt der normalen. Das echte AOD ist ein schwarzer Bildschirm (keine gedimmte Szene): Wenn das Zifferblatt keinen dedizierten AOD-Hintergrundrahmen hat (dial.aod), wird die normale Szene im AOD- Modus ausgeblendet, sodass sie schwarz rendert + die -Elemente in ihrer eigenen Farbe. AOD- parsen ebenfalls über den Szenen-Walker (er rekursiert jetzt in den -Container und taggt Drawables , statt sie dem flachen Scan zu überlassen, wo ihr Drehpunkt nicht passte → „unpositioniert"); AOD-Zeiger rotieren um die Leinwandmitte (die trägt manchmal ein außermittiges Zeiger-x/y, das die Firmware ignoriert — z. B. Gradients Stunde ). Der Editor stellt dies auch als (§UI) bereit: Jeder Bildschirm zeigt nur seine eigenen Ebenen und Bearbeitungen bleiben unabhängig erhalten. Der Normalmodus-Render ist durchgehend byte-identisch; der Roundtrip bleibt byte-exakt auf allen 103 Zifferblättern.

11.10 img_number-ZIFFERANZAHL — das 40 01 00 XX-Byte (✅ firmware-bestätigt)

Wie viele Ziffern eine img_number zeichnet, ist ein einzelnes Byte im Felddatensatz — das Datenbyte XX des 40 01 00 XX-Attribut-Unterdatensatzes des Elements (der 0x40-Unterdatensatz, der nach der 61 [count][base][glyph-ids]-Frametabelle sitzt):

  • niedriges Nibble XX & 0x0F = Anzahl der Ziffernslots (0 ⇒ Firmware-Standard 7).
  • Bit 7 0x80 = Null-Padding (führende Nullen zeigen, z. B. „09" vs. „9").

Bestätigt durch Disassemblieren der Firmware (XIP-Image 0x10000000; Render-Routine 0x100d8e60): NDIG = ldrb[40sub+3] & 0x0F (→7 wenn 0); der Wert wird geklemmt value % 10^NDIG und genau NDIG Glyphen werden MS-zuerst gezeichnet, führende Nullen unterdrückt außer bei Bit7. Das u16 nach der Quelle (60 für Datum, 1000 für kcal) ist NICHT die Anzahl — es speist nur die Tausender-/Millionen-Trennzeichen-Glyphen- Einfügung (cmp #1000/#1000000), weshalb das Bearbeiten nichts bewirkte. Die Quellen-ID begrenzt auch nicht.

Korpus-Histogramm über alle 620 Zahlenfelder stimmt überein: 2-stellige Felder (Stunde/Minute/Sekunde/Datum/Temp/HR) enden 40 01 00 02/0x82; kcal …04; Schritte …05; einstellige Uhr-Splits 0x81. Also das Datumsfeld 40 01 00 82 = 2 Ziffern, null-gepolstert — das ist der gesamte Grund, warum eine umgebundene Fahrenheit-Temperatur (≥100) abgeschnitten wurde.

Fix / Editor: wfweb parst digitCount/digitZeroPad (+digitCountOff) für Zahlenfelder, stellt „Ziffern" + „Null-Padding" im Inspektor bereit, schreibt das Byte an Ort und Stelle (gleicher Fußabdruck), und die Vorschau klemmt/polstert auf digitCount, um die Firmware zu spiegeln. Das Umbinden der Quelle eines Felds und das Setzen seiner Ziffernanzahl funktioniert also für jedes Feld (z. B. Datum→Temperatur °F → Ziffern 3). Normalmodus- Orakel unverändert (0 Regressionen, 3 winzige Verbesserungen); Roundtrip byte-exakt auf allen 103 Zifferblättern. (Die frühere „Ziffernbreite"/rectW-Hypothese war falsch — Breite ist nur Layout, nicht die Anzahl.)

11.11 Knoteninventar, der struct-Datensatz und der Ressourcenreferenz-Schwanz ✅

Die Szene (§11.7) ist ein sauberes verschachteltes TLV — [tag u8][len u16 LE][body]. Vollständiges Tag-Inventar wie über das Korpus beobachtet:

✅ Dieses Inventar ist für das Korpus vollständig. Beim Rekursieren nur in die obigen Container-Tags läuft das Szenen-TLV aller 15 geprüften Zifferblätter exakt auf seine deklarierte Wurzellänge mit null unbekannten Tags — ein Parser, der diese Tabelle verarbeitet, verarbeitet also das gesamte Format, und ein unbekannter Tag bedeutet eine fehlausgerichtete Leseoperation, keinen neuen Knotentyp. (Vorsicht: Ein Walker, der in jeden Knoten rekursiert, dessen Länge zufällig ≥ 3 ist, steigt in struct/0x5b-Bodies hinab und halluziniert einen langen Schwanz von einmaligen „Tags" — die Blatt-Bodies sind kein TLV.)

💡 Erstellungs-Abkürzung: 0x48/0x68-Auto-Layout kann komplett übersprungen werden — jedes Widget kann mit absolutem x,y direkt auf Bildschirm-Top-Level platziert werden, was der Von-Grund-auf-Builder (§11.7) tut. Nur zum Lesen vorhandener Zifferblätter nötig. Eine meta-Breite von 0x8000 markiert einen struct als Auto-Layout-Kind eines Rahmens (Position kommt vom Elternteil, nicht von x,y).

0x01-struct-Body — ein festes 18-Byte-Präfix gefolgt von einem optionalen Ressourcenreferenz-Schwanz:``` +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:~
> ✅ Dies vereinheitlicht die "magischen Offsets" von §11.5/§11.8/§11.9. Diese Abschnitte lokalisieren Felder *relativ
> zum `0x61`-Frame-Tabellen-Byte* — das ist einfach `+0x12` dieser Struktur, also `−18`/`−16` = `x`/`y` und
> **`−5` = `meta[9]`, die Quell-ID**. Gleiche Bytes, ein sauberes Layout. Die vorwärts scannende `82`-Attr-
> Heuristik, die systematisch um eins daneben lag, wird überhaupt nicht benötigt: lies `meta[9]` der Struktur.
> Das `max` eines Zahlenfelds ist ebenso einfach `meta[11..13]` (z. B. tragen Tagesfelder `max = 99`).

**Ref-Tail** (`61`) — wie ein Knoten auf seine Bitmaps zeigt:```
+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

⚠️ Diese nachgestellten u16s wurden zuvor als "count×id(u16)" / Glyphen-IDs dokumentiert. Es handelt sich um Blockgrößen: base, plus die laufende Summe davon, durchläuft den Asset-Pool Eintrag für Eintrag (✅ verifiziert exakt für alle 10 Einträge eines Ziffern-Atlasses). Zwei Konsequenzen:

  • Die referenzierten Assets eines Knotens müssen im Asset-Pool aufeinanderfolgend sein. Es gibt keinen wahlfreien Zugriff — die Kette bewegt sich nur vorwärts. Plane den Pool so, dass jeder Ziffernsatz (10), jede Auswahlliste (N) und jedes Frame-Blatt ein zusammenhängender Block ist. Ein Writer, der Assets neu anordnet, ohne die Kette zu korrigieren, erzeugt eine Datei, die Müll liest (→ schwarzer Bildschirm).
  • Nur die ersten count−1 Größen sind tragend; der Wert des letzten Eintrags wird nie verfolgt, daher enthalten Dateien in freier Wildbahn dort manchmal einen veralteten Wert. Behandle eine Abweichung beim letzten Eintrag nicht als defekte Referenz.

0x28 Vorschau — das Store-/Katalog-Thumbnail ist in der .bin selbst eingebettet (27 Vorschau- Knoten über 15 Zifferblätter), als 0x08 pvStruct: ein 5-Byte-Präfix plus denselben Referenz-Endteil, mit keinem x/y. Nützlich zum Erstellen einer Galerie-UI, ohne separate PNGs auszuliefern.

11.12 Sichtbarkeitsbedingungen — Tag 0x02 ✅

Ein 0x02-Geschwister eines Widgets macht es bedingt. Ohne eines zeichnet das Widget immer. Grammatik:``` count u8 , count × ( id u8 , op u8 , val u24 LE signed ) [5 bytes per entry]

root@kitploit:~
`id` ist eine Datenquellen-ID (§16) — einschließlich der **synthetischen Slot-IDs** aus §11.13. Operatoren, mit
Häufigkeiten gemessen über 15 Zifferblätter:

| op | Bedeutung | gesehen |
|---|---|---|
| `0x01` | zeichnen, wenn `value == val` | 99 |
| `0x81` | wie `0x01` (Bit `0x80` gesetzt — erscheint bei sich gegenseitig ausschließenden Varianten) | 48 |
| `0x02` | **ausblenden**, wenn `value == val` | 7 |
| `0x03` | zeichnen, wenn `value == val`, wobei `val` ein **Keine-Daten-Marker** ist (z. B. HR `1000`) | 13 |
| `0x05` | zeichnen, wenn `value >= val` | 58 |
| `0x06` | zeichnen, wenn `value <= val` | 50 |
| `0x04` | ⚠️ **unbekannt** — 15 Vorkommen, keine bestätigte Semantik | 15 |

Kombinationsregel (wie vom Referenz-Renderer implementiert, Maske `op & 0x7f`): die Gleichheits-Einträge
werden **ODER**-verknüpft, dann müssen die Ausblenden-/`>=`/`<=`-Einträge **alle** gelten.

Dieser eine Mechanismus deckt den Großteil der Laufzeit-Variabilität des Formats ab und erklärt Strukturen, die
wie duplizierte Widgets aussehen:

- **12h / 24h- und metrische / imperiale Layouts** — zwei Widget-Sätze am selben Punkt gestapelt, jeweils gebunden an
  `id 0x73` (das Einheiten-Flag) mit `val` 0 oder 1. Zifferblatt 275 hat sechs solcher Paare (12 Knoten).
- **„Keine Daten"-Platzhalter** — op `0x03` gegen einen Sentinel, z. B. `id 0x5f, val 1000` (Zifferblatt 275, zweimal):
  zeichnet die Gedankenstrich-Grafik statt einer Temperatur, wenn die Metrik nicht verfügbar ist.
- **Bucket-Hervorhebungen** — ein gepaarter `0x05`/`0x06`-Bereich, z. B. Metaballs Kette, bei der jedes Glied
  für sein eigenes 5-Minuten-Fenster aufleuchtet.
- **Alternativen für Komplikations-Slots** — gebunden an die synthetischen Slot-IDs aus §11.13.

### 11.13 Konfigurierbare Komplikations-Slots — `0x85` + `0x5f` ✅ (ersetzt §11.8)

§11.8 kam zu dem Schluss, dass die aktive Metrik einer konfigurierbaren Komplikation Geräte-RAM-Zustand ist und nicht
aus der Datei wiederhergestellt werden kann. **Das war falsch** — sowohl das Metrik-Menü des Slots als auch seine Standardauswahl
befinden sich in der `.bin`. Jeder `0x85`-Knoten trägt ein `0x5f`-Geschwister:```
+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

Die tatsächlich gezeichneten Alternativen sind gewöhnliche 0x68-Gruppen an anderer Stelle im Baum, jede gesteuert durch eine 0x02-Bedingung (§11.12) auf der synthetischen ID 0x79 + slotIndex — die Varianten von Slot 0 binden also an 0x79, die von Slot 1 an 0x7a und so weiter. Um einen Slot zu rendern: activeIdx lesen, dann die Variante zeichnen, deren Bedingung diesem Index entspricht.

Gemessen an echten Zifferblättern:

Die beiden 6-Metrik-Slots von Zifferblatt 275 machen 12 seiner 26 0x02-Knoten aus, genau wie vorhergesagt: 01 79 81 0X 00 00 und 01 7a 81 0X 00 00 für X = 0..5 — sechs Alternativen, die auf 0x79 (Slot 0) schlüsseln und sechs auf 0x7a (Slot 1). (Die 0x79/0x7a-Bytes, die §11.8 als „Instanz-Byte" bezeichnete, sind diese Bindungs- IDs.) Seine restlichen 14 sind unabhängig: 12 auf 0x73 (das 24h/Metrik-Einheiten-Flag, val 0 oder 1 — sechs Widget-Paare, die zwischen 12h- und 24h-Layouts wechseln) und 2 auf 0x5f mit Op 0x03 und val = 1000, der Temperatur-No-Data-Platzhalter.

Weiterhin echter Gerätezustand: Was auch immer der Benutzer später in der Companion-App wählt, überschreibt activeIdx zur Laufzeit, sodass eine Vorschau den Datei-Standard reproduziert, nicht unbedingt das, was ein bestimmtes Zifferblatt anzeigt. imgs[0] eines 0x85-Knotens ist ein „Tippen zum Konfigurieren"-Platzhalter, den die Firmware nur in ihrem eigenen Bearbeitungsmodus zeichnet — überspringen Sie ihn bei der Vorschau des normalen Zeitmodus.

11.14 Akzentfarbe — das Fähigkeits-Flag meta[7] == 4 ✅

Einige Zifferblätter lassen den Benutzer am Gerät eine Akzentfarbe wählen, und die Firmware setzt sie beim Rendern in die Bitmaps des Widgets ein. Der Schalter ist ein einzelnes Byte: meta[7] der Struktur (§11.11) — d. h. Byte +0x0B des 0x01-Körpers — gleich 4 markiert die Ressource(n) dieses Widgets als einfärbbar.

  • Es ist ein Fähigkeits-Flag pro Widget, keine Farbe. Färben Sie jedes nicht-transparente Pixel einer markierten Ressource neu ein (Alpha unverändert lassen); es gibt keinen Pixel-für-Pixel-Farbtest.
  • Verbreitung: 37 von 499 Strukturen und 8 von 15 hier gemessenen Zifferblättern; das fmc-Korpus berichtet 56 von 100 Zifferblättern mit mindestens einem markierten Widget.
  • 🛑 Baken Sie niemals eine Akzentfarbe in die exportierten Bytes ein. Die Substitution erfolgt live auf der Uhr; eine ausgelieferte .bin muss ihre ursprünglichen Pixel behalten, sonst verlieren Sie dauerhaft die Wahl des Benutzers. Wenden Sie die Tönung nur im Vorschau-/Canvas-Pfad an.

⚠️ „Verbessern" Sie dies nicht zu einer Farb-Heuristik — dieser Weg ist eine nachweisliche Sackgasse (dokumentiert von fmc, nachdem es auf die harte Tour gemacht wurde). Die intuitive Theorie ist, dass markierte Pixel in einer erkennbaren Platzhalterfarbe eingebacken sind, die die Firmware austauscht. Das kann nicht funktionieren: Der einfärbbare Ring von Zifferblatt 348 Tumbler und die gewöhnlichen nicht-einfärbbaren Ziffernstreifen der Zifferblätter 282 Radar Sweep / 291 Vertical backen das exakt gleiche (255,72,32)-RGB ein (erschöpfend geprüft, jedes Pixel); und die Zifferblätter 305 Dots (Stundenzeiger) und 306 Large Number (Ziffern) sind einfärbbar, während sie schlicht weiß eingebacken sind, sodass ein Farbtest sie vollständig übersehen würde. Aufeinanderfolgende Verfeinerungen (1 → 4 Referenzfarben, plus eine Widget-Rollen-Allowlist) schlugen alle fehl. Lesen Sie das Flag.

Gegen das echte Gerät / die Companion-App auf 7 Zifferblättern kreuzgeprüft, gewählt, um beide Richtungen zu belasten — 349 Theatre, 376 Digits time, 305 Dots, 306 Large Number, 304 Elaborate 2 bieten alle die Akzent- Einstellung an und haben alle meta[7]==4-Widgets; 316 Trailing (rötlicher Zeiger, keine Einstellung), 312 Disc und 295 Vortex bieten keine an und haben null markierte Widgets.

meta[4..6] liegt direkt neben dem Flag und sieht so aus, als könnte es bei einigen Strukturen eine Farbe kodieren (ein echt aussehendes RGB mit Schwanz f1=1,f2=255, gegenüber dem Platzhalter (1,0,0)/(4,0,0) der markierten Strukturen). Es korreliert nicht mit der Akzentfähigkeit. ⚠️ Ungeklärt; ignorieren.

11.15 Fortschrittsringe — 0x80/0x5a (prozedural) und 0x81/0x5b (bildbeschnitten) ✅

Beide Ringvarianten paaren eine kurze Struktur (x, y, meta mit der Quell-ID — normalerweise ohne Ref-Schwanz überhaupt) mit einem Bogenspezifikations-Geschwister. §11.5 dokumentierte nur „0x5b: max u16 @+4", was die untere Hälfte eines max i32 ist und die Bogengeometrie übersieht. Vollständiger Datensatz:``` +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:~
Pairing ist starr: `0x80` trägt immer genau `0x01` + ein **19-Byte** `0x5a`, `0x81` immer genau
`0x01` + ein **17-Byte** `0x5b` (✅ 26/26 Ringe über 15 Zifferblätter). ⚠️ Aber das **bildbeschnittene `0x81`
dominiert** — 25 dieser 26. Die prozedurale `0x80`/`0x5a`-Variante trat **einmal** auf (Zifferblatt 273), daher
beruhen ihr `radius`-Feld und das 19-Byte-Layout auf einer einzigen Stichprobe; mit Vorsicht behandeln, bis sie erneut gesehen wird.

`frac = clamp((value − min) / (max − min), 0, 1)`, und der gefüllte Bogen verläuft von `start` in Richtung `end`.
Winkel null ist bei **3 Uhr**, positiv im Uhrzeigersinn. Gemessene Beispiele:

| Zifferblatt | Tag | min..max | start → end | Breite | radius |
|---|---|---|---|---|---|
| 273 Activity Mood | `0x5a` | 0..100 | **−102,8° → 102,8°** | 42 | 222 |
| 273 Activity Mood | `0x5b` | 0..100 | 270,0° → 90,0° | 80 | (Bild) |
| 276 Dichotomy | `0x5b` | 0..100 | 60,0° → −120,0° | 23 | (Bild) |
| 304 Elaborate 2 | `0x5b` | 0..100 | −2,0° → 358,0° | 24 / 80 | (Bild) |
| 366 Combo | `0x5b` | 0..100 | 0,0° → 270,0° | 18 | (Bild) |
| 368 Function | `0x5b` | 0..100 | 0,0° → 360,0° | 20 | (Bild) |

> ⚠️ **Dies korrigiert die Annahme „im Uhrzeigersinn ab 12 Uhr"** in §11.5. Das ist nur der Sonderfall
> `start = 0, end = 3600` (ein voller Umlauf, bei dem die Konvention nicht beobachtbar ist). Echte Zifferblätter verwenden
> **Teilanzeigen** (273er ±102,8°-Fächer, 366er 270°-Dreiviertelring) und **negative Bögen**
> (276er 60° → −120°), daher zeichnet ein Renderer, der immer einen vollen Kreis von oben überstreicht, diese falsch.
> 🟡 Die genaue Winkel-null-Konvention und die Richtungsregel stammen aus fmcs Renderer, kreuzgeprüft
> gegen diese On-Disk-Werte — nicht unabhängig Pixel-für-Pixel auf dem Gerät von diesem Repo verifiziert.
> Beachte, dass `frac = value/max` und das `0x81`-Sektor-Clipping in §11.5 **validiert** wurden (Zifferblatt 322).

⚠️ **Ungelöst: der 3-Byte-Trailer `01 00 kk`.** `kk` nimmt plausibel aussehende Werte an (104, 152, 216,
232, 248), und die naheliegende Hypothese ist ein Radius — **getestet und widerlegt**: Zifferblatt 366 verwendet `kk = 104`
sowohl für einen 82×82- als auch einen 166×166-Ring, und Zifferblatt 273 verwendet `kk = 232` sowohl für einen 440×440- als auch einen 284×284-
Ring. Kein Radius, kein Durchmesser. Möglicherweise Deckkraft/Stil. Unverändert hin- und zurückschreiben.

🟡 **Gemeldete Stolperfalle, hier nicht verifiziert:** in fmcs Renderer rendert eine `0x60`-Zahl, deren Quell-ID der ID
**irgendeines** Rings auf demselben Bildschirm entspricht, `round(frac × 100)` statt des Rohwerts — daher
zeigt eine Herzfrequenzzahl neben einem Herzfrequenzring `36` statt `71 bpm`. Ihr dokumentierter
Workaround besteht darin, Ring und Zahl auf zwei **Alias**-IDs derselben Metrik zu platzieren (Schritte
`0x19`/`0x26`/`0x49`, Kalorien `0x1c`/`0x1e`/`0x48`). Ob dies Firmware-Verhalten ist oder spezifisch
für ihren Renderer, ist **nicht geklärt** — eine Live-Prüfung wert, bevor man das Design darauf aufbaut.

---

## 12. Massenübertragung & OTA-Details

Die Übertragungstabelle befindet sich in §6. Zusätzliche bestätigte Punkte:

- **AGPS/EPO** ✅: Der erste geschriebene Block beginnt mit dem ASCII-Header `000000010000…`. Die vollständige
  Init- → `[A05F ↔ 905F]×N`- → Finish-Schleife wurde auf der Leitung beobachtet (~892 Blöcke).
- **Firmware-OTA** (`9040`–`9042`, Finish `9041`) 🔎: Struktur kartiert; INIT2-Payload = Versionsbytes
  (z. B. `0b 00 00 39` = 11.0.0.57). **Nicht im Feld getestet** (die App deaktiviert hier das FW-Update). Firmware-
  Images scheinen **unsigniert zu sein — Integrität ist nur CRC32** (keine asymmetrische Signatur im RE beobachtet).
- ⚠️ Da OTA und `FACTORY_RESET (009A 0001)` die authentifizierte Sitzung teilen, reicht eine einzige gültige BLE-
  Authentifizierung aus, um die Uhr zu löschen oder (im Prinzip) zu zerstören. Vorsichtig behandeln.

---

## 13. Sensoren

✅ Hardware, die über BLE verfügbar ist:

- **Optischer PPG** — Herzfrequenz (manuell/automatisch/Training/Ruhe), SpO₂ und HRV-basierter Stress.
- **3-Achsen-Beschleunigungsmesser** — Schritte, Distanz, Kalorien, Schlafphasen, Handgelenkheben, Kadenz.
- **GNSS/GPS** (AGPS-unterstützt) — Trainingsstrecke (`WORKOUT_GPS`) und Standort-Push (`GPS_PUSH`).

Es gibt **keinen Barometer/Höhenmesser, Kompass, Gyroskop oder Haut-/Körpertemperatursensor**. Ein interner
NTC-Thermistor (Platinen-/Akkutemperatur) existiert, ist aber **nur** über den AT-Kanal lesbar
(`AT GETNTCTEMP`, §14) — der `0155`-Hauttemperatur-Verlaufsstream ist bei dieser SKU leer.

**Daten-Widget-Übernahmen** (es gibt keine echte Complication-/Datenbindungs-API — siehe §11.5): die
vorhandenen Textfelder der Uhr können zweckentfremdet werden, um auf einen Blick externe Daten anzuzeigen. Bewährt ✅: die Wetter-
**Stadtzeichenkette** (`WEATHER_SET_1`, z. B. `"BRA 2x1 ARG"` erschien auf dem Widget) und die Musik-
**Titel-/Künstlerfelder**; die **Kontaktliste** (20 × Name[32]+Nummer[25]) funktioniert als scrollbares Daten-
panel. Alles sind Push-Daten, keine persistenten Complications.

---

## 14. AT-Factory-/Shell-Kanal (`77d4ff01` / `77d4ff02`)

Ein separater Klartext-AT-Befehls kanal, unabhängig vom gerahmten Protokoll. ✅ live getestet:

- **Lesen:** `AT GETSECRET` (16-Byte-Pairing-Geheimnis), `GETVERSION`, `GETSN`, `GETNAME`, `GETPID`,
  `GETBATLV` (rohe mV, z. B. `3853mv`), `GETGSENSOR` (rohe Beschleunigung in g, `X=… Y=… Z=…`),
  `GETNTCTEMP` (°C, interner NTC).
- **Schreiben / Auslösen:** `AT SETMOTOR=1` (Motor vibrieren lassen), `SETHR/SETHRV/SETSPO2=…` (Sensor-Test-
  Injektion), `SETLCDSWITCH/SETGPSSWITCH/SETKEYSWITCH`.

Antworten enden mit `,OK`. `SET*`-Befehle werden im Allgemeinen ausgeführt, geben aber möglicherweise kein `,OK` über BLE zurück — Fall
für Fall bestätigen.

---

## 15. Firmware-gesteuerte / nicht verfügbare Funktionen (🔎 Firmware-RE)

Einige Funktionen sind in der Firmware vorhanden, aber durch SKU/Region deaktiviert und **vom Telefon/BLE aus nicht erreichbar** —
sie benötigen einen Firmware-Mod, der hier außerhalb des Rahmens liegt:

- **ChatGPT-Sprache** — Gate = `ux2sys`-Feature-ID `0x9e`, beim Boot aus NVRAM/EFUSE/Region gespeist; bei
  dieser SKU Unterstützungsflag `908b = 00`. Nicht durch Telefon, Konto oder BLE beeinflussbar (durch
  Experiment + RE bestätigt). Die App ist nur ein Relais; Audio geht Telefon → Nothing-Cloud.
- **Blutdruck** — ein vollständiges Subsystem existiert in der Firmware, durch SKU/Region abgeschaltet.
- **Alipay / NFC-Zahlung** — vollständige UI vorhanden, nur China-SKU.
- **In Hardware/Firmware nicht vorhanden:** EKG, SOS/Notfall, generisches NFC.

---

## 16. Datenquellen-/Complication-Getter-IDs

Nicht nötig, um einen BLE-Client zu bauen, aber **unerlässlich, um ein Zifferblatt zu erstellen oder zu rendern**: Dies ist der Wert von
`meta[9]` (§11.11), der ein Widget an Live-Daten bindet, die `id` einer Sichtbarkeitsbedingung (§11.12) und
die Einträge des Metrikmenüs eines Slots (§11.13). Die Firmware löst es über eine **142-Einträge-Getter-
Dispatch-Tabelle** bei `0x101f371c` auf, wobei jeder Eintrag `ux2sys_get(type)` aufruft (🔎 Firmware-RE).

### Zeit / Datum — ✅ gut etabliert

| id | Bedeutung | id | Bedeutung |
|---|---|---|---|
| `0x01` | Stunde (12/24h je nach Geräteeinstellung) | `0x0f`, `0x12` | Sekunde (weich) |
| `0x04` | Stunde (24h) | `0x10`, `0x11` | Sekunden Zehner / Einer |
| `0x07` | Stunde (erzwungen 24h) | `0x71`, `0x72` | Sekunde (tickend / Zeigerwinkel) |
| `0x02`, `0x03` | Stunde-12h Zehner / Einer | `0x13` | AM/PM-Flag (0 = AM, 1 = PM) |
| `0x05`, `0x06`, `0x08`, `0x09` | Stunde Zehner / Einer | `0x15`, `0x16` | Monat |
| `0x0a`, `0x70` | Stundenzeigerwinkel | `0x17` | Tag des Monats |
| `0x0b` | Minute | `0x18` | Wochentag (0 = Montag ⚠️) |
| `0x0c`, `0x0d` | Minute Zehner / Einer | `0x0e`, `0x71` | Minutenzeigerwinkel |

Zehner-/Einer-IDs zeichnen eine **einzelne Ziffer** — ein Widget, das an eine davon gebunden ist, rendert ein Glyph, nicht den gesamten
Wert (Zifferblatt 284 Square). Zeigerwinkel-IDs (§11.5) sind `0x0a`/`0x70` Stunde = `h·30° + m·0,5°`,
`0x0e`/`0x71` Minute = `m·6° + s·0,1°`, `0x12`/`0x72` Sekunde = `s·6°` (✅ durch Disassemblieren bestätigt
der Getter, RTC-Fallback 10:10:30).

### Gesundheit / Sensoren / Wetter — ⚠️ umstritten, Evidenzspalte lesen

| id | Bedeutung (beste aktuelle Lesart) | Evidenz |
|---|---|---|
| `0x19` | **Schritte** | in 368er-Slotmenü neben `0x1a`; stimmt mit `parse.ts` dieses Repos überein |
| `0x1a` | **Herzfrequenz** | in 368er-Slotmenü neben `0x5f` und `0x19` |
| `0x1c` | Kalorien | Slotmenü, Flammen-Symbol in der Begleit-App |
| `0x1e` | Kalorien (Alias) | Korpus |
| `0x22` / `0x23` | Distanz km / mi (Ganzzahlteil) | Korpus |
| `0x74` / `0x75` | Distanz km / mi (Nachkommateil) | Korpus |
| `0x76` | Distanz (Slot-Form) | Slotmenü, Straßen-Symbol |
| `0x24` | **Akku %** | Slotmenü, Blitz-Symbol |
| `0x30` | Akku % | Korpus |
| `0x36` / `0x5f` | Temperatur | `0x5f` = Slotmenü, Wolke-Sonne-Symbol; 361 TempoG bindet eine einfache Zahl |
| `0x48` | Stehen (gestandene Stunden) | Slotmenü, stehende-Figur-Symbol |
| `0x8b` | AQI | Slotmenü |
| `0x73` | 24h-/metrische-Einheiten-Flag | Korpus |
| `0x25`–`0x27`, `0x49`, `0x6c`, `0x6f` | Ziel % / Slot-Aliase von Schritten & Kalorien | Korpus |
| `0x6a` | ⚠️ nicht identifizierte Slot-Metrik | erscheint in 4 Slotmenüs |
| `0x79 + slotIndex` | **synthetisch** — keine Metrik; die Slot-Auswahl-ID (§11.13) | ✅ §11.13 |

> ⚠️ **Drei Tabellen in diesem Repo widersprachen sich; diese hier ist die Abstimmung.** Frühere Revisionen von §16
> und §11.5 lasen `0x19` als Herzfrequenz, `0x1b` als Akku, `0x24` als Temperatur und `0x36` als Schritte —
> und `wfweb/src/codec/mock.ts` kodiert diese Lesart weiterhin, während `wfweb/src/codec/parse.ts` eine
> andere kodiert (`0x19` Schritte, `0x24` Ziel %, `0x48` Stehen, `0x1a` Wetter). Die obige Tabelle folgt
> der besser belegten Lesart (Beschriftungen von fmc gegen die eigenen Widget-Slot-
> Menüsymbole der Begleit-App kalibriert, siehe §Quellen). **Der Code wurde noch nicht geändert — `mock.ts` und `parse.ts` sind weiterhin
> inkonsistent miteinander und mit dieser Tabelle.** Gesundheits-ID-Beschriftungen als ⚠️ behandeln, bis jemand
> ein Feld an jede ID bindet und die Uhr ausliest.
>
> Das stärkste einzelne Beweisstück ist **das Slotmenü von Zifferblatt 368 Function** (§11.13), das
> `0x5f 0x1c 0x19 0x48 0x24 0x76 0x1a 0x8b` als **acht verschiedene vom Benutzer wählbare Metriken** in einem Menü anbietet.
> Was auch immer die Beschriftungen sind, keine zwei dieser acht können dieselbe Metrik sein — was
> `0x19` = `0x1a` = Herzfrequenz und `0x24` = `0x5f` = Temperatur gleichzeitig ausschließt.

Ring-/Bogen-Complications mit einem **Frame-Sheet** indizieren einen vorgerenderten Frame (z. B. 50 % = Frame 50 von 100),
der in die `.bin` eingebacken ist, die du sendest (§11.5), daher ist kein externes RES-Paket nötig; bildlose Ringe werden
aus der Bogenspezifikation gezeichnet (§11.15).

---

### Schnellkarten (Startseiten-Kacheln) — `QUICK_CARD (906D)` ✅

Die Startseiten-Kacheln der Uhr. Das Telefon wählt nur **welche** Kacheln angezeigt werden und in **welcher Reihenfolge** — Kacheln werden
von der Firmware gerendert (kein Inhaltskanal). Erstes Payload-Byte = Unterbefehl: `00` = GET, `01` =
SET; **beide verwenden `0x906D`** (`0x906C` ist aufgeführt, wird aber nicht verwendet — eine Abfrage läuft in einen Timeout). Antwort = `A06D`.

> 🛑 Das Senden einer erfundenen `assemblyId` **löscht die Bildschirme der Uhr** (sie akzeptiert die Liste, kann die
> IDs nicht zuordnen, zeigt nichts). Nur IDs senden, die du über GET **zurückgelesen** hast; Wiederherstellung über die offizielle App oder einen
> Werksreset.

**GET-Antwort** ✅: `status(1) ‖ 00 ‖ N(1) ‖ N × Gruppe`, Gruppe = `tag=01 ‖ K(1) ‖ K×(assemblyId, sportId)`.
Echter Frame: `01 00 04  01 02 5d00 6100  01 03 1900 2e00 2300  01 03 5c00 0400 5a02  01 03 4800 5100 5300`
= 4 Bildschirme / 11 Karten (`5a02` = Sportkarte, sportId 2).

**Slots:** Jeder Bildschirm hat **4 Slots**. Der Kartentyp bestimmt seine Größe — `circular`/`square` = 1 Slot,
`rectangle` = 2 Slots. Validierung ist reine Slot-Arithmetik (Σ ≤ 4 pro Bildschirm); keine sich gegenseitig ausschließenden
Karten. `sportId` ist `0`, außer bei Sportkarten (87–91). IDs `64` und `95` existieren nicht.

**`assemblyId`-Katalog** (jeder logische Typ = ein zusammenhängender Bereich von 6 Stilvarianten `_0`..`_5`;
`0` = leerer Slot):

| dez | Karte | dez | Karte |
|----|----|----|----|
| 0 | leerer Slot | 49–53,97–98 | Wetter |
| 1–6 | Schritte | 54–58 | Timer |
| 7–12 | Kalorien | 59–62 | Atmung |
| 13–18 | Stehen | 63,65–67 | Stoppuhr |
| 19–24 | Moderate Aktivität | 68–71 | Akku |
| 25–30 | Herzfrequenz | 72–76 | Zuletzt |
| 31–36 | SpO₂ | 77–81 | Kontakte |
| 37–42 | Stress | 82–86 | Zifferblatt / Telefon |
| 43–48 | Schlaf | 87–91 | Sport (`sportId` ≠ 0) |
| 92 | Musik | 93/94/96 | Aktivitätsaufzeichnung / PAI / Zyklus |

---

## Anhang A. Werks-Zifferblattkatalog (id → Name)

§11 bezieht sich durchgehend auf Zifferblätter mit numerischer ID (275 SlopeTime, 322 Glare 2, 357 Silhouette, …). IDs
`273`–`376` sind die Store-/Werksgesichter; die ID ist das, was `DIAL_COMMAND (9055/a055)` als aktiv meldet und
was `9075` als `old_id` akzeptiert (§11.4). 100 der 103 bekannten IDs sind unten benannt; die restlichen sind
ROM-Stubs (§11.4). Namen und Gruppierung wie von der offiziellen Begleit-App angezeigt.

- **Standard** (6) — `273` Activity Mood · `274` Sun Circle · `275` SlopeTime · `276` Dichotomy · `277` Prismatic Time · `280` Multifunction
- **Analog** (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
- **Digital** (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
- **Multifunktion** (10) — `304` Elaborate 2 · `344` InfoMeter · `348` Tumbler · `354` Dual · `363` Vintage · `366` Combo · `367` Complex Figure · `368` Function · `374` Cirquary · `375` InfoHub
- **Kreativ** (8) — `284` Square · `312` Disc · `317` Disc 2 · `318` Dominos · `332` Flux · `342` Perfect Match · `343` Progress Day · `358` Asteroid
- **Diwali** (1) — `295` Vortex

---

## Quellen

Die obigen Byte-Layouts wurden aus der Firmware (1.0.0.73), der offiziellen APK (3.5.7) und
entschlüsselten Live-Mitschnitten gegen ein echtes Gerät rekonstruiert. Die Referenzimplementierung für dieses Projekt befindet sich in
`core-rust/src/{commands,frame,crypto,health,session}.rs` (Rust), dem `wfweb/`-TypeScript-Editor und
den `cmftool/`-Python-Tools (`pair.py`, `session.py`, `wf_codec.py`, `upload_custom.py`, …).

**Hier eingearbeitete unabhängige Arbeit.** [freethinkel/fmc](https://github.com/freethinkel/fmc) — ein
SvelteKit-Zifferblatt-Editor + Marktplatz für dieselbe Uhr — hat unabhängig das
`.bin`-Format aus einem ~100-Gesichter-Korpus reverse-engineert und mehrere Ergebnisse erzielt, die dieses Dokument übersehen oder
falsch hatte. Ihre `docs/cmf-protocol.md`, `src/lib/modules/editor/lib/{wf,render}.ts` und
`src/lib/modules/device/lib/ble.ts` sind eine direkte Lektüre wert. Übernommene Erkenntnisse, jeweils vor der Niederschrift hier gegen Zifferblatt-Bytes erneut verifiziert:

| Erkenntnis | Wo | Status hier |
|---|---|---|
| Beide Header-Wörter sind CRC32 (nicht standardmäßige Variante) | §11.4 | ✅ 9/9 Zifferblätter erneut verifiziert; **korrigiert** „keine blockierende Prüfsumme" |
| Sichtbarkeitsbedingungen, Tag `0x02` | §11.12 | ✅ erneut verifiziert; war undokumentiert |
| Slot-Metrikliste + Standard-`activeIdx`, `0x79 + slotIndex` | §11.13 | ✅ auf 275/368/273/304 erneut verifiziert; **ersetzt** §11.8 |
| Akzent-Farbfähigkeitsflag `meta[7] == 4` | §11.14 | ✅ Prävalenz erneut verifiziert; war undokumentiert |
| Vollständige Bogenspezifikation (Schwenkwinkel, Breite, Radius) | §11.15 | ✅ erneut verifiziert; **korrigiert** „`max` u16 `@+4`" |
| Ref-Tail-`u16`s sind Blockgrößen, keine Glyph-IDs | §11.11 | ✅ exakt auf einem 10-Glyph-Atlas erneut verifiziert |
| 36-Byte-Fußzeile; Magic-Byte-Variante `0x02`; `0x86` = 64 B; `0x28` eingebettete Vorschau | §11.4, §11.11 | ✅ erneut verifiziert |
| Gesundheits-/Wetter-ID-Beschriftungen gegen das Slotmenü der Begleit-App kalibriert | §16 | ⚠️ als beste Lesart übernommen; Konflikte gekennzeichnet |
| Shell-Dienst-UUID ist `77d4e67c-…`, und Web-Bluetooth-`optionalServices`-Scoping | §1 | ⚠️ Einzelgeräte-Bericht, hier nicht erneut verifiziert |
| Zahl, die ID mit Ring teilt, rendert einen Prozentsatz | §11.15 | 🟡 gemeldet, hier **nicht** verifiziert |
| Werks-Zifferblatt-ID → Name-Katalog | Anhang A | ✅ unverändert übernommen |
Tool herunterladen
ZweckDienstCharakteristikEigenschaften
Befehls-Schreiben0000fff0-0000-1000-8000-00805f9b34fb0000fff2-…Write
Befehls-Benachrichtigung0000fff0-…0000fff1-…Notify
Shell-Schreiben (AT)—77d4ff01-2fe2-2334-0d35-9ccd078f529cWrite
Shell-Benachrichtigung (AT)—77d4ff02-…Notify
Bulk-Datenschreiben—02f00000-0000-0000-0000-00000000ffe1Write
Bulk-Datenbenachrichtigung—02f00000-…ffe2Notify
Namecmd1,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
Namecmd1,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
Namecmd1,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
Namecmd1,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
Namecmd1,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/Schritte)FFFF 9078 / FFFF A078
FEMALE_CYCLE_SET / _RETFFFF 9071 / FFFF A071
SLEEP_CONFIG_SET / _RET (Zielminuten)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
Namecmd1,cmd2
DIAL_COMMAND_SET / _RET (Liste/Neuordnen/Auswählen)FFFF 9055 / FFFF A055
DIAL_CONFIG_SET / _RETFFFF 9075 / FFFF A075
CHANGE_DIAL (⚠️ inert auf 1.0.0.73 — nicht verwenden)009F 0001
QUICK_CARD_SET/GET / _RET (beide auf 906D)FFFF 906D / FFFF A06D
Namecmd1,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 🔎 (leer bei dieser SKU)0155 0001 / 0155 0002
WORKOUT_SUMMARY / _V30057 0001 / 0160 0001
WORKOUT_GPSFFFF A05A
DomäneINIT1 req/replyINIT2 req/replyCHUNK req/writeFINISH ack1/ack2
Watchface (Foto)8052/00529063/A063A064/9064A065/9065
Watchface (strukturiert/Wechsel)8052/00529075/A075A064/9064A065/9065
Firmware9052/A0529040/A040A042/9042A041/9041
AGPS/EPO905E/A05E—A05F/905FA060/9060
OffsetGrößeFeld
04Zeitstempel (Epoche s)
44Schritte
84Distanz (m)
124Kalorien
1616reserviert (beobachtet 0)
OffsetGrößeFeld
04Sitzungsstart (Epoche, UTC)
44Aufwachen (Epoche, UTC)
82total_deep_s
102total_core_s
122total_rem_s
142total_awake_s
162⚠️ [unsicher] (Sitzungs-ID/Score? beobachtete Werte stimmen nicht mit Datensatzsummen überein)
ac 49 1f 01
  • CONTACTS_SET (00D5 0001) ✅: N × 57 Bytes = name(32) ‖ phone(25). Die Watch-UI zeigt bis zu 20.
  • ALARMS_SET (0063 0001) ✅ — korrigiert Gadgetbridge (das das Label am Ende platzierte, 0xff-gepolstert — falsch). 40 Bytes pro Alarm, Big-Endian: secondsOfDay(i32) ‖ index(u8) ‖ enabled(u8) ‖ repetition-Bitmaske(u8) ‖ flag(u8) ‖ label[32] UTF-8. Das Label liegt bei Offset 8 und wird auf der Uhr angezeigt. repetition = Wochentags-Bitmaske (0 = einmalig); flag ist ⚠️ [unsicher] (Einmalig-Marker?). Beispiel (13:30, idx 2): 0000bdd8 02 01 15 00 "Alarm…".
  • GOALS_SET (005E 0001) ✅ — die offizielle App und die Referenzimplementierung verwenden das 10-Byte, Big-Endian DailyTargetBean v1: steps(u32 BE) ‖ distance_m(u32 BE) ‖ calories_kcal(u16 BE). (Das ist die Gadgetbridge-Form; frühere Berichte, dass die Uhr es „ignorierte“, waren ein Stale-Session-Entschlüsselungs-Bug, kein Payload-Problem.) 🔎 Firmware-RE zeigt auch eine längere 29-Byte-erweiterte Variante (fügt sleep_min/exercise_min/stand_h + 6 Enable-Flags hinzu, alle u32 BE nach einem flag(u16 LE)-Präfix, mit erzwungenen Bereichen: Schritte 2000–30000, Dist 1000–99000, cal 100–5000, Schlaf 360–720, Training 30–90, Stehen 6–16) — nicht der Standardpfad der App; bevorzugen Sie die 10-Byte-Form, es sei denn, Sie benötigen die zusätzlichen Ziele.
  • STANDING_REMINDER / WATER_REMINDER (0060/0061 0001) ✅: 11 Bytes: enabled(1) ‖ threshold_min(u16 LE) ‖ dndStart(u32 LE) ‖ dndEnd(u32 LE). Beachten Sie, dass das in der UI angezeigte „aktive Fenster 08:00–22:00“ ein fester Firmware-Standard ist und nicht im Payload enthalten ist.
  • SPORTS_SET (00DC 0001) ✅: count(1) = 36 Slots ‖ activityTypeCode[36] (aktive Codes, dann 00- Padding). Wählt aus, welche Sportarten im Workout-Menü der Uhr erscheinen.
  • HEART_MONITORING_ENABLED (009B 0001) ✅: kind-Byte — 01 = 24/7-HR, 02 = SpO₂, 04 = Stress (alle 30 min gemessen).
  • HEART_MONITORING_ALERTS (FFFF 9059) ✅: deaktiviert = 00; aktiviert = 01 ‖ hrLow ‖ hrHigh ‖ sportHrHigh ‖ spo2Low ‖ 00 00 00 00 (eine 0/255-Grenze = „kein Limit“).
  • FEMALE_CYCLE (FFFF 9071) ✅: 01 ‖ predictionOpen ‖ notifySwitch ‖ cycleStartSwitch ‖ cycleStartNotifyBefore ‖ ovulationStartSwitch ‖ ovulationStartNotifyBefore ‖ fertileStartSwitch ‖ fertileStartNotifyBefore ‖ period(1) ‖ cyclePeriod(1) ‖ cycleStartDate(u32) ‖ markStart(u32) ‖ markEnd(u32) (erfasst: period=5, cyclePeriod=0x1c=28).
  • QUICK_REPLY (FFFF 9073) ✅: TLV — count(1) ‖ total(1) ‖ [id(1) ‖ len(u16 LE) ‖ msg-UTF8]… (7 Standardantworten erfasst & entschlüsselt).
  • WORLD_CLOCK (FFFF 906F) ✅: sendet numerische Stadt-IDs, keine Namen (01 ‖ count ‖ cityId(2 BE)…); die Uhr mappt IDs aus einer internen Tabelle. DST-Konfig FFFF 9083 = count ‖ [id(u16 LE) ‖ dst(u16 LE) ‖ start(u32 LE) ‖ end(u32 LE)]….
  • MUSIC_INFO_SET (FFFF 905C, 131 B) ✅: state(1: 0=none/1=paused/2=playing) ‖ volume(1) ‖ volumeMax(1) ‖ track(64) ‖ artist(64). Die Uhr sendet auch MUSIC_BUTTON (A05D) zurück.
  • WEATHER_SET_1 (FFFF 906B, 199 B) ✅ — diesen verwenden: 7×9-Byte-Tage + 24×2-Byte-Stunden + Stadt(32) + 7×8-Byte-Sonnenaufgang/-untergang (LE). Temperaturen kodiert als (temp_c + 100) & 0xFF. ⚠️ Derselbe Payload, gesendet auf WEATHER_SET_2 (0066 0001), aktualisiert das Wetter-Widget auf Pro 2 nicht — immer 906B verwenden. (Die Stadtzeichenkette ist auch ein bewiesener Daten-Hijack-Vektor — siehe §13.)
  • FIND_WATCH (005D 0001) ✅: Payload 0x01 → Uhr klingelt/vibriert (+ ACK 005D 0003).
  • GPS_PUSH (FFFF 906A) ✅ — Big-Endian, Längengrad zuerst: 16 Bytes ts(u32 BE) ‖ lon×1e7(i32 BE) ‖ lat×1e7(i32 BE) ‖ 00 00. Gegen einen echten Standort validiert.
  • WORKOUT_GPS (FFFF A05A) ✅ — Little-Endian, Längengrad zuerst: 12 Bytes ts(i32) ‖ lon×1e7(i32) ‖ lat×1e7(i32).
  • TIME (FFFF 8004): siehe §7.
  • cfbppRaster (nach LZ4)Verwendung
    42RGB565-LEopaker Hintergrund (FULL/THUMB)
    53RGB565-LE (2 B) + Alpha (1 B) pro pxkantengeglättete Sprites (Glyphen, Zeiger, Symbole)
    13 (0x0d)0,54-Bit-Alpha-Maske; Firmware färbt zur LaufzeitZiffern-Glyphen-Atlas
    24 (0x18)4RGBA8888Vollfarb-Ebenen (inkl. des immer aktiven aodImage)
    1—JPEG/JFIF (ff d8 ff), mit jedem Decoder extrahierenseltene Animationsframes
    0a
  • validate_rejects_bad_containers — lehnt einen flachen 0x61-Body ab (→ NotEnvelope, der historische 0a Bug) und ein Kind, das sein Fenster überschreitet (→ ChildOverflow).
  • :
    61 0a 00
    −18/−16
    die Schreib-Offsets ebenfalls mitwandern
    0a
  • Multi-Varianten-Komplikations-Slots — die aktive Metrik ist NICHT in der .bin ⚠️ dieser Aufzählungspunkt war falsch und wird durch §11.13 ersetzt. Eine konfigurierbare Komplikation wird als N 0x68-Gruppenknoten gestapelt an derselben (x,y) erstellt, jeder nur gezeichnet, wenn eine Sichtbarkeitsbedingung zutrifft — das stimmte. Aber die Metrikliste pro Slot (275s 0x1c/0x6a/0x48/0x24/0x19/0x76) ist exakt die Metrikliste, nicht „Options-/Stil-IDs"; der Standard-Aktivindex ist ein Byte in der Datei; und das 0x79/0x7a-Byte ist kein „Instanz-Byte", sondern die ID, auf die die Alternativen schlüsseln (0x79 + SlotIndex). Eine statische Vorschau kann den Dateistandard reproduzieren. Siehe §11.13.
  • Kantenverankerte inaktive Komplikationen (z. B. BPM-Text bei (446,0), gesehen auf 275/302/325/365/375) sind Slots, die die Firmware in der Standardansicht nicht zeichnet — ihr Wert passt nicht einmal vor die Leinwandkante. In der Vorschau als verborgen behandeln.
  • 0x22
    Zeiger
    0x22
    aod
    0x22
    @69,209
    isolierte Normal|AOD-Bearbeitungs-UI
  • Eigenständige 0x60-img_number (cnt=10) — Quelle bei −5, Vorwärts-Off-by-one. ✅ Gleicher Off-by-one wie §11.8, aber für Nicht-Uhr-Zahlen: „Gradients" Datum saß bei (203,80) oben mittig mit Quelle 0x17, aber der Vorwärts-82-Scan griff den Winkel-Getter des benachbarten Zeigers (0x0a) und die Zeigerposition → die Zahl rendert an der Zeigerstelle mit einer falschen Quelle. Fix: Für eine 61 0a 00-img_number in einem 0x60-Wrapper −5/−18/−16 vertrauen, wenn die Vorwärtsquelle für eine Zahl unmöglich ist (Quelle-0 oder ein Zeigerwinkel-Getter 0x0a/0e/12/70/71/72) und die −18/−16-Position gültig & ungleich null ist (die Ungleich-null-Wächterprüfung überspringt Gruppen-Kind-Ziffern mit relX=0).
  • 0x17 = Datum (Tag des Monats), 0x24 = Temperatur — verschieden. Zifferblatt 340 verwendet beide (0x17 „Jun 09" und eine separate 0x24-Temp), also ist 0x17 Datum, nicht Temp. Ein Zifferblatt, dessen Uhr eine Temperatur in einem 0x17-Slot zeigt, ist eine benutzerkonfigurierte Komplikation (Gerätezustand), nicht der Dateistandard.
  • TagRolleContainer?Body
    0x20Szenenwurzel (Body-Wrapper, kein Drawable)✅Kinder
    0x21normaler Bildschirm✅Kinder
    0x22AOD-Bildschirm (§11.9)✅Kinder
    0x28eingebettetes Katalog-Vorschau-Thumbnail✅ein 0x08-Kind
    0x68Gruppe / Auto-Layout-Container✅0x48-Rahmen + Kinder
    0x30statisches Bild, oder Auswahl nach Wert aus N Bildern✅0x01 (+0x02)
    0x60Live-Zahlenanzeige (Ziffernstreifen)✅0x01 + 0x40 (+0x02)
    0x70rotierender Zeiger✅0x01 + 0x05 Drehpunkt
    0x80Fortschrittsring, prozedural✅0x01 + 0x5a (§11.15)
    0x81Fortschrittsring, bildbeschnitten✅0x01 + 0x5b (§11.15)
    0x85benutzerzuweisbarer Komplikations-Slot✅0x01 + 0x5f (§11.13)
    0x01struct — Geometrie + Attribute (unten)—x,y,meta[14] + Referenz-Schwanz
    0x02Sichtbarkeitsbedingung (§11.12)—Bedingungsliste
    0x05Drehpunkt — flag u8, pivotX u16, pivotY u16—5 B
    0x08pvStruct — prefix[5] + Referenz-Schwanz, kein x/y (nur Vorschau)——
    0x40Ziffernanzahl / Null-Padding-Flag (§11.10)—1 B
    0x48Rahmen — x,y,w,h,gap,align Auto-Layout-Zeile/-Spalte——
    0x5a / 0x5bBogenspezifikation für 0x80 / 0x81 (§11.15)—19 B / 17 B
    0x5fSlot-Metrikliste für 0x85 (§11.13)——
    0x86Anzeigename-Knoten, immer exakt 64 Bytes, NUL-terminiert, nicht gezeichnet—64 B
    ZifferblattSlotAnzahlactiveIdxMetrik-IDs→ aktiv
    275 SlopeTime0601c 6a 48 24 19 760x1c Kalorien
    275 SlopeTime1641c 6a 48 24 19 760x19 Schritte
    368 Function0805f 1c 19 48 24 76 1a 8b0x5f Temperatur
    368 Function1865f 1c 19 48 24 76 1a 8b0x1a Herzfrequenz
    273 Activity Mood0401c 24 48 6a0x1c Kalorien
    304 Elaborate 20/1401c 48 6a 24 / 24 1c 6a 480x1c / 0x24