Skip to content
KitploitKITPLOIT
StrumentiBlog
Invia
StrumentiBlog
Invia

Strumenti di Hacking, PenTest e Cybersecurity per il tuo Arsenale di Sicurezza!

Kitploit è una directory di strumenti di hacking, cybersecurity e pentesting. Scopri gli ultimi aggiornamenti dei progetti per trovare vulnerabilità, analizzare sistemi, automatizzare i test e rafforzare la tua sicurezza.

··Feed·Contatto·Privacy·© 2026 Kitploit

Directory degli strumenti

Categorie

Vedi tutte le categorie
Loading categories
CMF-Watch-Pro-2-BLE-Protocol — Protocollo BLE reverse-engineered per il CMF Watch Pro 2, con documentazione del layout GATT, frame di comando crittografati AES-128-CBC, handshake di autenticazione e sincronizzazione dei dati sulla salute per lo sviluppo di app companion alternative. | Kitploit
Strumenti/GitHubGitHub/joshuapassos/cmf-watch-pro-2-ble-protocol
Sicurezza Sistemi EmbeddedSicurezza BluetoothSicurezza IoTReverse EngineeringSicurezza WirelessCrittografiaSicurezza MobileSicurezza Hardware e IoTAnalisi del Firmware

Più Popolari

Vedi tutti →

Scopri gli strumenti più utilizzati dalla nostra community.

Esplora tutti gli strumenti

Sfoglia la nostra collezione di strumenti

Vedi tutti gli strumenti →
Condividi
GitHubjoshuapassos/cmf-watch-pro-2-ble-protocol

CMF-Watch-Pro-2-BLE-Protocol

Protocollo BLE reverse-engineered per il CMF Watch Pro 2, con documentazione del layout GATT, frame di comando crittografati AES-128-CBC, handshake di autenticazione e sincronizzazione dei dati sulla salute per lo sviluppo di app companion alternative.

Vedi RepositorySito web
3281 mese faNon ancora revisionato

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

Non ufficiale. Questo documento descrive il protocollo Bluetooth Low Energy (BLE) del CMF Watch Pro 2 (CMF by Nothing), ricostruito tramite reverse engineering per un'app companion alternativa. Non è affiliato né approvato da Nothing/CMF. Utilizzo a proprio rischio.

Tutti gli interi multi-byte nell'header del frame e negli opcode sono big-endian. Gli interi all'interno dei payload dei comandi sono little-endian salvo diversa indicazione (questo rispecchia il firmware del dispositivo) — attenzione alle eccezioni (GOALS_SET, GPS_PUSH, offset/lunghezza del trasferimento bulk sono big-endian).

Indicatori di affidabilità

Ogni affermazione non ovvia di seguito è etichettata con il metodo con cui è stata stabilita:

  • ✅ validato sul dispositivo — osservato in una cattura live decifrata o testato su un orologio reale.
  • 🔎 da firmware / RE dell'APK — estratto decompilando il firmware (1.0.0.73) o l'APK ufficiale (3.5.7); coerente con il codice ma non testato a runtime.
  • 🟡 parzialmente provato — stabilito strutturalmente (offline, su tutto il corpus, o tramite RE) ma il passaggio rimanente richiede l'orologio e non è stato eseguito.
  • ⚠️ [incerto] — dedotto, non confermato; potrebbe essere errato.

Dove una sezione successiva corregge una precedente, il testo precedente viene mantenuto con un riferimento piuttosto che eliminato — sapere quali letture sono state provate e confutate evita alla persona successiva lo stesso giro.

Dispositivo di test per tutte le catture: CMF Watch Pro 2-5485, fw 1.0.0.73, seriale CI04102520008192, MCU Actions ATS3089C (Cortex-M4), schermo 466×360.


1. Layout GATT

Il telefono è il client GATT; l'orologio è il periferico, che si pubblicizza come CMF Watch Pro 2-XXXX (4 caratteri esadecimali).

Abilita le notifiche scrivendo 01 00 su ogni CCCD (00002902-…). Il canale comandi (fff1/fff2) trasporta il protocollo incapsulato descritto di seguito. Il canale shell (77d4…) trasporta testo in stile AT semplice (es. AT GETSECRET; vedi §14). Il canale dati (02f0…) trasporta grandi blob binari (watchface, firmware, AGPS), coordinati da opcode di controllo sul canale comandi.

UUID dei servizi — l'orologio pubblicizza ~10 servizi primari. Enumerati su un'unità reale: 0xfff0 (comandi), 0x180f (batteria), 0x180a (info dispositivo), 0xefe7, 0xffd0, 02f00000-…ffe0 e 02f00000-…fe00 (dati), 77d4e67c-2fe2-2334-0d35-9ccd078f529c (shell / accoppiamento), e49a3001-f69a-11e8-8eb2-f2801f1b9fd1, f48a23c0-f69a-11e8-8eb2-f2801f1b9fd1.

⚠️ L'UUID del servizio shell è 77d4e67c-…, non 77d4ff00-…. Revisioni precedenti di questo documento assumevano che il servizio condividesse il prefisso ff00 delle sue caratteristiche (77d4ff01/77d4ff02, §14) — non è così, almeno sull'unità su cui è stato verificato (risultato da freethinkel/fmc, vedi §Fonti). Gli UUID delle caratteristiche sono invariati. Non verificato se 77d4e67c sia stabile tra le unità — enumerare piuttosto che hardcodare.

🌐 Nota su Web Bluetooth. Chromium scopre solo i servizi che la pagina ha elencato in optionalServices, anche per una chiamata getPrimaryServices() non filtrata — una pagina che elenca 3 servizi ne vede 3, mentre chrome://bluetooth-internals (il livello C++ di Chrome, non limitato) mostra tutti e 10. Se scrivi un client browser, elenca tutti gli UUID sopra in anticipo o l'accoppiamento fallirà con servizi che esistono chiaramente. Nessun Web Bluetooth in Firefox/Safari; richiede un gesto dell'utente + HTTPS/localhost.

✅ Un'intera sessione reale è stata eseguita sul solo canale comandi — durante una cattura di 160 s di uso intensivo non c'è stato traffico sui canali dati/firmware o shell tranne durante un trasferimento esplicito OTA/watchface.


2. Formato frame (0xF5)

Ogni messaggio sul canale comandi è incapsulato in uno o più frame con header di 11 byte:``` +------+-----------+--------+-------------+-------------+--------+-------------------+ | 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` insieme formano l'**opcode** (vedi §6). 🔎 confermato rispetto al frame builder
  dell'app ufficiale (`C6117b.m30831g`).
- `chunkCount` = numero totale di chunk per questo comando; `chunkIndex` è **1-based**.
- `chunkLen` = numero di byte di `chunk` in questo frame.
- Una singola scrittura BLE può essere frammentata dall'MTU del link; il ricevitore bufferizza i byte
  grezzi e ri-estrae i frame completi. I payload di grandi dimensioni vengono suddivisi in più chunk
  (stesso `cmd1/cmd2`, `chunkIndex` crescente) e riassemblati in ordine.

### Convenzione opcode (✅ confermata sul wire)

- `cmd1 = 0xFFFF`: `cmd2` in `0x80xx`/`0x90xx` = telefono→orologio (richiesta/impostazione); `0x00xx`/`0xa0xx` =
  orologio→telefono (risposta). Le coppie corrispondono tramite il byte basso (`0x9055`↔`0xa055`, `0x8051`↔`0x0051`).
- `cmd1` specifico della funzionalità: suffisso `cmd2` = `0x0001` **SET**, `0x0002` **GET**, `0x0003` **ACK**.

### Corpo del chunk

Per ogni chunk, il corpo è `payloadPiece ‖ CRC32_LE(payloadPiece)` (CRC a 4 byte, little-endian,
zlib/IEEE). Se il comando è **cifrato** (vedi §3), l'intero `payloadPiece ‖ CRC` viene poi
cifrato con AES-128-CBC/PKCS7 e quel ciphertext diventa il `chunk` del frame.

**Stranezza del plaintext:** per gli opcode in chiaro l'orologio *conta* il CRC a 4 byte in `chunkLen` ma
**non** lo trasmette. Quindi, decodificando un frame in chiaro, la lunghezza effettiva dei dati è `chunkLen − 4`.
(I frame cifrati trasportano il CRC all'interno del ciphertext come di consueto.)

Dimensionamento dei chunk (così i chunk cifrati cadono sui confini dei blocchi AES), con `maxWrite = mtu − 3`:
- cifrato: `floor((maxWrite − 11) / 16) * 16 − 4 − 1`
- in chiaro: `maxWrite − 11 − 4 − 1`

✅ Tutti i valori `chunkLen` dei frame cifrati osservati erano multipli di 16 (l'allineamento dei blocchi è rispettato).

---

## 3. Primitive crittografiche

- **AES-128-CBC** con padding **PKCS7** e un **IV fisso** (dal firmware
  `CmfCharacteristic.AES_IV`):
  `50 51 52 53 54 55 56 57 60 61 62 63 64 65 66 5A`.
- **CRC32** (zlib/IEEE), emesso come 4 byte little-endian.
- **SHA-256** sulla concatenazione delle parti.

Derivazione della chiave:```
authkey      = SHA256( rnd1 ‖ rnd2 ‖ secret )[0..16]      // persisted across sessions
sessionKey   = SHA256( nonce ‖ authkey )[0..16]           // per connection
  • secret = segreto del dispositivo a 16 byte (ottenibile dall'orologio tramite il comando shell AT GETSECRET → GETSECRET:<32-hex>,OK).
  • rnd1 = 16 byte casuali scelti dal telefono; rnd2 = 16 byte casuali dall'orologio.
  • nonce = byte dalla risposta nonce dell'orologio.

Dopo che la chiave è impostata, tutti i frame del canale di comando sono crittografati AES tranne gli opcode in chiaro elencati in §5.

✅ Entrambe le derivazioni validate: authkey recuperato dal ntwatch.db di un telefono rooted corrispondeva al valore derivato da un rnd1/rnd2/secret catturato; sessionKey riprodotto da un nonce catturato decrittografa i frame live.


4. Handshake di autenticazione / associazione

Due percorsi di ingresso condividono la stessa coda nonce/confirm.

4.1 Associazione iniziale (con il segreto del dispositivo)```

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:~
In caso di `AUTH_FAILED (0xFFFF,0xA061)` o di mancata corrispondenza della firma, l'autenticazione fallisce.

### 4.2 Riconnessione (authkey già noto)```
        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

✅ L'ordine di riconnessione (nessun traffico shell) è stato osservato intatto in una cattura reale.

4.3 Init post-autenticazione (fase 2)

⚠️→✅ TIME è obbligatorio prima delle query sui dati. Dopo Initialized, l'orologio non risponderà a BATTERY, SERIAL_NUMBER_GET o all'handshake ACTIVITY_FETCH_* finché non viene inviato un TIME (FFFF 8004) nella sessione — senza di esso, arriva solo un FIRMWARE_VERSION_RET non richiesto e tutto il resto va in timeout. ✅ confermato dal vivo (Pixel 8a): inviando i tre GET senza TIME → risponde solo il firmware; inviando prima TIME → batteria e seriale iniziano a rispondere.

Ordine consigliato per la fase 2: TIME → FIRMWARE_VERSION_GET → SERIAL_NUMBER_GET → BATTERY (0xA5) → push di configurazione → sincronizzazione salute (§8).

4.4 Pattern di eco GET → SET (✅)

Non esiste un opcode "read" separato per la maggior parte delle impostazioni. Inviare un *_GET (cmd2 = 0x0002, payload 0xA5) fa sì che l'orologio risponda con l'opcode SET (cmd2 = 0x0001) trasportando il valore corrente. I comandi SET vengono confermati con cmd2 = 0x0003 e corpo vuoto.


5. Testo chiaro vs crittografato

I frame sono crittografati AES una volta impostata una chiave, eccetto questi opcode, che sono sempre in testo chiaro:

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

Le intestazioni dei frame (cmd1/cmd2) viaggiano sempre in chiaro, quindi la sequenza dei comandi è visibile in qualsiasi cattura anche senza la chiave — solo i payload crittografati richiedono sessionKey.


6. Riferimento opcode (cmd1, cmd2)

GET/SET/REQUEST = telefono→orologio; RET/REPLY/ACK/RESPONSE/DATA = orologio→telefono.

Sessione / dispositivo

Autenticazione

Notifiche / chiamate / ricerca

Musica

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

Sveglie / contatti / promemoria

Configurazione

Meteo

Nomecmd1,cmd2
WEATHER_SET_1 (quello che funziona)FFFF 906B
WEATHER_SET_2 (ignorato su Pro 2 — vedi §9)0066 0001

Quadranti / sfere

Salute / sincronizzazione

Opcode solo-JS (FFFF 8051, FFFF 0051, FFFF 90A2, FFFF 90C5, FFFF A056, FFFF 908A/908B stato/supporto ChatGPT) sono gestiti nel bytecode Hermes dell'app, non nel layer Java. Le loro intestazioni appaiono nelle catture ma la semantica dei payload è ⚠️ [incerta].

Trasferimento dati in blocco (canale dati)

Quadranti / firmware / AGPS usano un ciclo init → richiesta/scrittura chunk → ack finale:

(tutti con cmd1 = FFFF.) L'orologio guida il ciclo emettendo DATA_CHUNK_REQUEST_*(offset, length) (offset/length = u32 big-endian); il telefono risponde con DATA_CHUNK_WRITE_* che trasporta payload[offset..offset+length] sulla caratteristica dati. Vedi §11–§12 per i dettagli.


7. Ora e fuso orario

Payload di TIME (FFFF 8004) = epochSeconds(i32, BE) ‖ utcOffsetMillis(i32, BE). Inviato subito dopo l'autenticazione così l'orologio mostra l'ora locale (e sblocca le query sui dati — vedi §4.3).

⚠️ I timestamp sanitari dell'orologio sono UTC. L'app companion deve aggiungere l'offset UTC locale prima di derivare il giorno di calendario / l'ora del giorno locale. (Raggruppare la salute per giorno UTC grezzo fa scattare il giorno all'ora locale sbagliata.)

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


8. Sincronizzazione salute

  1. Il telefono invia ACTIVITY_FETCH_1; l'orologio risponde ACTIVITY_FETCH_ACK_1 (primo byte 01 ⇒ pronto).
  2. Il telefono invia ACTIVITY_FETCH_2; l'orologio poi invia una raffica di frame dati: ACTIVITY_DATA, HEART_RATE_*, SPO2, STRESS, SLEEP_DATA, WORKOUT_SUMMARY[_V3].
  3. Ognuno viene analizzato in campioni per minuto / sessioni e aggregato per giorno locale.

La sincronizzazione è sequenziale (deve seguire TIME; l'orologio rilascia gli stream dopo ACK_2), non un'unica raffica. Una sessione pesante invia ~170–210 frame di notifica in ~160 s. ✅

8.1 Record attività — ACTIVITY_DATA (32 byte ciascuno, LE) ✅

Unità calorie: le calorie dell'attività sono riportate in cal (calorie-grammo). Dividi la somma giornaliera per 1000 per ottenere kcal. (Le calorie del riepilogo allenamento, al contrario, sono già in kcal.)

8.2 Campioni FC / SpO₂ / Stress ✅

  • FC manuale/automatica, FC allenamento, SpO₂, stress = 8 byte ciascuno: timestamp(i32 LE) ‖ valore(i32 LE) (valore = bpm / % SpO₂ / indice di stress).
  • FC a riposo (00DA 0001) è diversa — 5 byte: timestamp(i32 LE) ‖ fc(u8). ✅ esempio dal vivo 5e dc 29 6a 4e → ts, fc = 78 bpm. Intervalli punteggio stress: 1–29 / 30–59 / 60–79 / 80–99.

8.3 Sonno — SLEEP_DATA (intestazione 18 byte + N × record 8 byte) ✅

Un SLEEP_DATA = una sessione di sonno; una notte può contenerne diverse (i micro-risvegli dividono le sessioni).

Intestazione:

Ogni record di 8 byte: timestamp(u32) ‖ durata_s(u16) ‖ fase(u16). Codici fase: 1 = Profondo, 2 = Leggero/centrale, 3 = REM, 4 = Sveglio. ✅ validato su una notte intera (due sessioni, i totali D/C/R/A riconciliano).

8.4 Riepilogo allenamento — WORKOUT_SUMMARY v1 (54 byte) / _V3 (0160 0001)

v1: inizio(u32), fine(u32), durata_s(u32), poi tipo/calorie/passi/distanza/FC-media e un blocco GPS/esteso. ✅ layout v1 confermato contro il firmware. WORKOUT_SUMMARY_V3 è un layout più recente per gli stessi dati più un blocco esteso di ~40 byte (exerciseLoad, aerobico/anaerobico, recoveryTime, VO₂max, cadenza, PAI, migliori tempi di corsa…). Il set di campi è noto (dal Room DB dell'app) ma gli offset esatti dei byte dentro quel blocco di 40 byte sono ⚠️ [incerti] — chiuderli richiede una cattura grezza di un allenamento GPS.


9. Payload dei comandi selezionati

Le stringhe sono UTF-8, troncate a byte alla dimensione del campo (il troncamento può dividere un carattere multi-byte, corrispondendo al comportamento s.encode()[:max] del firmware); i campi corti sono riempiti con zeri a destra.

  • APP_NOTIFICATION (0065 0001) ✅: iconCode(1) ‖ 0x00 ‖ when(u32 BE) ‖ titleLen(1) ‖ title ‖ body. iconCode seleziona l'icona dell'app (WhatsApp=8, Telegram=12, Instagram=18, Gmail=27; sconosciuto=0xFF). Titolo ≤ 20 byte, corpo ≤ 128 byte. Inviato da un client → l'orologio lo ha visualizzato + ACK 0065 0003.
  • BATTERY (005C 0001) ✅: risposta = livello(1) ‖ inCarica(1) (es. 3b 00 = 59 %, non in carica).
  • SERIAL_NUMBER_RET (00DE 0001) ✅: len(1) ‖ ASCII (es. 10 + "CI04102520008192").
  • USER_INFO (0095 0001) ✅: altezza_cm(1) ‖ peso_kg(1) ‖ età(1) ‖ sesso(1: 1=M) (es. = 172 cm / 73 kg / 31 / maschio).

10. Note di implementazione e peculiarità

  • Nessun orologio di sistema nei codec: gli encoder prendono now/utc_offset come parametri espliciti (deterministici, testabili). Il trasporto fornisce l'ora reale.
  • TIME blocca tutto (§4.3) — invialo per primo o l'orologio resta muto sulle query dati.
  • Il conteggio CRC in testo chiaro (§2) è facile da sbagliare — i frame in testo chiaro annunciano ma omettono il CRC.
  • Endianness: intestazione + opcode BE; interi payload LE; eccezioni — GOALS_SET e GPS_PUSH sono big-endian, e offset/length del trasferimento in blocco sono big-endian.
  • MTU: le dimensioni dei chunk sono calcolate così i chunk crittografati si allineano a blocchi AES da 16 byte.
  • authkey è persistibile (salvala dopo il primo pairing); sessionKey è per-connessione e derivata dal nonce dell'orologio a ogni riconnessione.

11. Quadranti / sfere — creazione

L'orologio supporta (a) quadranti foto/personalizzati (un'immagine di sfondo + un orologio digitale disegnato dal firmware) e (b) quadranti strutturati (sfere integrate / dello store: uno sfondo più layer di sprite posizionati, lancette e widget di testo). Entrambi trasferiti sul canale dati tramite il ciclo init → chunk in §6.

Cosa funziona davvero (✅ validato dal vivo): creare un quadrante foto da qualsiasi immagine e installarlo; installare offline uno qualsiasi dei 103 quadranti dello store; reskinning di un quadrante strutturato (scambia lo sfondo o qualsiasi sprite non di sfondo) e spostare i suoi layer; riordinare / cambiare la sfera attiva; e creare un quadrante strutturato da zero — l'involucro scena 0x20 è decodificato e il builder è implementato (§11.7), provato offline a fare round-trip di tutti i 103 quadranti dello store byte per byte e a emettere contenitori sintetici che superano il validatore del firmware stesso. 🟡 l'unico passo non provato è vedere un render sintetico da zero sul dispositivo tramite 9075 (la prova strutturale offline copre già ciò che causava il rifiuto 0a). Non c'è barriera di codec o trasporto e nessuna necessità della toolchain del vendor. Le vecchie affermazioni "il render strutturato è cotto nel RES-pack / impossibile via BLE" e "codec lato server cf=0x1f" erano sbagliate (un bug di offset+byte-per-pixel) — il firmware renderizza i quadranti strutturati guidati dai dati dal file che invii.

11.1 Gestione quadranti — DIAL_COMMAND (9055 / a055) ✅

  • tipo 0 = query dell'elenco. Risposta a055 = risultato(u8) ‖ selectIndex(u8) ‖ totale(u8) ‖ max(u8) ‖ N × dialId(u32 LE) ‖ ffffffff. Esempio: 01 05 06 07 … = attivo #5, 6 quadranti, max 7.
  • tipo 1 = riordina / seleziona attivo: reinvia l'intero elenco con il quadrante target all'indice 0 (è così che l'app ufficiale cambia le sfere; non esiste un opcode dedicato "imposta attivo").
  • Eliminare un quadrante = reinvia l'elenco senza il suo id.
  • CHANGE_DIAL (009F 0001) è inerte su fw 1.0.0.73 (restituisce una costante, non cambia) — non usarlo.

11.2 Flusso di trasferimento ✅```

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` = attivato e salvato; `0a` = memorizzato ma **non** attivato / rifiutato. Su
Android ogni `DATA_CHUNK_WRITE` deve uscire come **una scrittura BLE per frame** — concatenare e
ri-tagliare in base all'MTU desincronizza gli header e l'orologio continua a richiedere l'offset 0.

- **`9063` (foto) = APPEND.** La lista dei quadranti cresce (6→7); `watchfaceId = 0xFFFFFFFF` (sentinel
  personalizzato) così non viene mai rifiutato come duplicato, e l'orologio lo attiva automaticamente.
- **`9075` (strutturato) = SOSTITUISCE** lo slot `old_id`. `old_id` **deve** essere già nella lista
  (altrimenti `0a`). Per reinstallare un id già presente, **eliminalo prima** (9055 lista-meno-id)
  poi carica "da zero" — riutilizzare un id già presente dà `0a`.

### 11.3 Quadrante foto / personalizzato — ✅ completamente validato end-to-end

**Contenitore** (round-trip verificato byte per byte; tutti i campi 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 su RGB565 little-endian, top-down (payloadLen conta dal primo byte LZ4). L'app ufficiale usa LZ4-HC e rimuove l'header/footer di 21 byte del blocco LZ4; funziona anche un encoder LZ4 solo-literals — l'orologio accetta qualsiasi LZ4 valido, l'identità dei byte non è richiesta. I pixel fuori dal cerchio inscritto (centro 233,233, raggio 233) sono impostati a 0x0000.

INIT_2 per 9063 — header esatto (✅ questo è quello che funziona):``` 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` = lunghezza esatta del `.bin`; `FFFFFFFF` = `watchfaceId` personalizzato; `styleId` 0–4 seleziona il layout
dell'orologio digitale integrato (viene sempre disegnato — non esiste uno stato "off"); `posX/posY` lo posizionano (valori noti
56 / 77); `color565` lo colora (es. `FFFF` = bianco). ⚠️ La forma più corta `A5 ‖ size ‖ watchfaceId` viene
**rifiutata** con esito `0a` — usa l'header completo sopra. (Implementazione di riferimento:
`core-rust/engine.rs::build_wf_init2`, che rispecchia `C6135t.m31104u` nell'app ufficiale.)

**Procedura:** ridimensiona l'immagine a 466×466 (e una miniatura 270×270), converti in RGB565-LE dall'alto verso il basso,
aziona facoltativamente i pixel fuori dal cerchio, comprimi ciascuna con LZ4, assembla il contenitore sopra e carica
tramite la pipeline `9063` con `watchfaceId = 0xFFFFFFFF`. (Codec di riferimento: `core-rust/watchface.rs`,
`work/codec_dfa.py`.)

### 11.4 Quadrante strutturato / da store — contenitore e codec ✅

**Struttura del file** — l'header di 36 byte viene ripetuto **byte-per-byte come footer di 36 byte** alla fine del file
(✅ verificato su 15 quadranti; un parser dovrebbe rifiutare un file in cui differiscono):```
[36-byte header][scene TLV (§11.7)][asset pool][36-byte header again]

Header (struttura identica in tutti i 103 dial dello store; tutti i campi in little-endian):``` 0x00 crc_tree u32 LE [CRC32-raw of header[0x04:0x24] ‖ scene section] ✅ see below 0x04 magic 01 00 00 XX [XX = 0x00 or 0x02; both seen, meaning of 0x02 unknown] 0x08 name char[16] [NUL-terminated, e.g. "SlopeTime", "Metaball"; may carry a non-zero tail after the NUL (@0x17) — round-trip it verbatim] 0x18 size_a u32 LE [= filesize − 36 = footer offset = header+body] ✅ 103 dials 0x1c size_b u32 LE [asset-pool length, exactly] ✅ 15 dials 0x20 crc_assets u32 LE [CRC32-raw of the asset pool] ✅ see below 0x24 … [body starts here: the 0x20 scene container, §11.7]

root@kitploit:~
> ⚠️ **Correzione (sostituisce "non esiste checksum bloccante").** Revisioni precedenti leggevano `@0x00` come
> un id/hash per-dial e `@0x20` come "3× parole u32 id/hash \[non un CRC]", e affermavano che CRC32/Adler32/
> byte-sum non corrispondono. Entrambe le parole **sono** CRC32 — i test precedenti le hanno mancate perché
> la variante non è standard, e perché la lettura "3 parole a `0x20`" confondeva l'unica parola CRC
> con i primi byte del contenitore della scena che inizia a `0x24` (allo stesso modo "nome ripetuto a
> `0x2c`" è il nodo nome `0x86` della scena, §11.11). Scoperta da
> [freethinkel/fmc](https://github.com/freethinkel/fmc); riverificata qui.

**CRC32-raw** = polinomio IEEE riflesso `0xEDB88320`, **`init = 0`**, e **nessun XOR finale** — cioè
né l'`init=0xFFFFFFFF` né il `^0xFFFFFFFF` del `crc32` standard. Questo è il motivo per cui
il CRC32 pronto all'uso non ha mai corrisposto. Nota la dipendenza dall'ordine: `crc_assets` si trova all'interno dell'intervallo
coperto da `crc_tree`, quindi **scrivi `@0x20` per primo, poi calcola `@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])

Verificato: 9/9 dial memorizzati intatti corrispondono su entrambe le parole, e 6/6 dei template di questo repo corrispondono su crc_tree.

🟡 Il firmware non sembra applicare nessuno dei due CRC. Ogni dial che questo repo ha installato su 9075 — incluse le reskin prodotte dal percorso di modifica in-place con stessa footprint (§11.6), che muta i payload degli asset e i byte X/Y senza ricalcolare l'header — è stato renderizzato correttamente sul dispositivo. Quindi un CRC obsoleto non è ciò che causa un rifiuto 0a (quello è l'invariante della finestra del contenitore, §11.7). Tratta i CRC come scrivi-corretto-comunque: economici, e l'unico campo di integrità noto nel formato. Qualsiasi cosa riscriva la scena o il pool di asset dovrebbe ricalcolare entrambe le parole.

Dial stub (~173 B, es. id 273/274/277) sono segnaposto per quadranti cotti nella ROM: header + directory, nessun asset reale.

Asset — ognuno è dimsWord(u32 LE) ‖ len(u32 LE) ‖ LZ4(payload), dove cf = dimsWord & 0x1f, w = (dimsWord >> 10) & 0x7FF, h = (dimsWord >> 21) & 0x7FF, e len conta dal primo byte LZ4 (il 1f 00 01 00 che vedi spesso lì è il primo token LZ4 — non saltarlo). Dimensione decompressa = w·h·bpp:

✅ Tutti i 4151/4151 asset sui 103 dial decodificano esattamente con un decompressore standard lz4.block a w·h·bpp. La trasparenza è il byte alpha (cf=5/24) o 0x0000 (cf=4 fuori dal cerchio) — non c'è RLE né "escape". Codifica = ri-raster → LZ4 standard → [dimsWord][len][LZ4].

INIT_2 per 9075 — corpo cifrato AES:``` kind(1) ‖ old_id(u32 LE) ‖ new_id(u32 LE) ‖ file_len(u32 LE)

root@kitploit:~
`kind` = `0x02`/`0x03`; `old_id` = dial attivo corrente (da `9055`); `file_len` = dimensione reale del `.bin`
(= `@0x18 + 36`). Installare un `.bin` dello store così com'è è il percorso garantito (Ring Data id 359 +
102 altri confermati). (Riferimento: `core-rust/engine.rs::build_dial_replace_init`.)

### 11.5 Grammatica strutturata della directory ✅ (decodificata e implementata — REVISATA 2026-07-02)

> **⚠️ Revisione (2026-07-02): lo schema piatto del record `61 01 00` qui sotto era sistematicamente
> SBAGLIATO DI UNO.** Il corpo della scena è un TLV pulito (§11.7); un **corpo di foglia** disegnabile (tag `0x30`/`0x38`
> statici, puntatore `0x70`) è:
>
> ```
> 01 xx 00 [X u16][Y u16] …attrs… 61 [count u16][base u32][count×id u16] [05 05 00 01 pivX pivY]
> ```
>
> - l'attributo `0x01` apre il corpo: **X,Y = angolo in alto a sinistra** sulla tela 466² (gli `s16 x,y` dell'SDK
>   `sty_picture_t`).
> - la **tabella dei frame `61 …` chiude il corpo** (`base` = puntatore asset; `count` 1 = immagine, 10/11 =
>   atlante delle cifre — il vecchio "tipo di record `0a/0b`" era in realtà questo count! — 7/13/2 = foglio
>   dei frame della complicazione).
> - extra del puntatore: sorgente+scala `[src] 00 3c 00` dentro l'attributo `0x01`; pivot nel
>   **trailer** `05 05 00 01 [pivX][pivY]`. **Centro di rotazione = `(X+pivX, Y+pivY)` per puntatore** —
>   non un (233,233) fisso: esistono subdiali fuori centro (es. le lancette del dial 366 ruotano attorno a 150,150).
>
> La scansione lineare per `61 01 00` cuciva la tabella dei frame+pivot dell'elemento **N** alle X/Y
> (e al byte di tag, il vecchio "f3") dell'elemento **N+1** — sembrava *giusta* solo sui dial analogici i cui
> lancette adiacenti condividono geometrie quasi identiche. Il "muro della variante compatta" (spec 24 §24.4.5) era
> questo stesso fraintendimento. Implementato come `scan_scene_drawables` in `core-rust/watchface_struct.rs`
> e `wfweb/src/codec/parse.ts` (scena = fonte primaria per immagini/puntatori; scansione piatta mantenuta per
> testo + fallback non-envelope). Validato dall'oracolo `wfweb/compare.html` (render vs PNG ufficiali dello store, 99 dial): 64→72 buoni, 8→5 cattivi, diff media 9.3→7.4%.

Lettura storica del record piatto (superata, mantenuta per contesto):

- **Immagine statica** (`61 01 00`): `asset_ptr(u32) ‖ elemId(u16) ‖ 05 05 00 01 ‖ pivotX(u16) ‖
  pivotY(u16) ‖ 3B ‖ 01 ‖ 1b 00 ‖ X(u16) ‖ Y(u16)`. Angolo in alto a sinistra sulla tela 466² = `(X−pivotX, Y−pivotY)`.
- **Puntatore/lancetta** — stesso record immagine, ruotato a runtime. **Centro di rotazione = `(X+pivotX, Y+pivotY)`**
  (≈ 233,233 sui dial analogici). La **sorgente dati è un `u8` all'offset del record `+36`**, scala `u16` a
  `+38` (=60): `0x0a`/`0x70` = ora (`h·30°+m·0.5°`), `0x0e`/`0x71` = minuti (`m·6°+s·0.1°`),
  `0x12`/`0x72` = secondi (`s·6°`). ✅ confermato disassemblando i getter (fallback RTC 10:10:30).
- **Widget testo / numero** (`61 0a 00`): `asset_ptr(u32) ‖ [10×u16 metriche font] ‖ 40 01 00 ‖ flag ‖
  3B ‖ 01 ‖ u16 ‖ X(u16) ‖ Y(u16)`. `asset_ptr` punta al glifo "0"; la cifra *d* = l'asset a
  `index("0") + d` (10 sprite cf=5 consecutivi, es. `0123456789` e punteggiatura `,°`). ✅ renderizzato.
- **Riempimento complicazione = indice del frame** (✅ confermato per complicazioni a cifre/enum/gauge, count>1): il
  valore indicizza un **foglio di frame pre-renderizzato** nel `.bin` — `frame = (count−1)·val/100` (percentuale) o
  `frame = value` (cifra flip / enum). Tabella dei frame = sotto-record `61 ‖ count(u16) ‖ base(u32) ‖
  count×id(u16)`. Es. l'ora grande di 327 Digit Max è un foglio a 13 frame (numeri 0–12), `frame = hour`.
- **Anello di avanzamento / arco = clip settore runtime** (✅ 2026-07-02, **corregge la lettura "gli anelli sono fogli di frame"
  nella spec 25 §2**): il tag dell'elemento **`0x81`** porta un **singolo** disco pieno (`61` tabella dei frame
  `count == 1`), e il cuneo parziale è quel disco **ritagliato a settore circolare** (`frac = value/max`,
  in senso orario dalle 12) — verificato pixel per pixel su 322 Glare 2 e confermato `count==1` su
  **20 dial**. Su disco: corpo `0x81` = sotto `0x01` (geometria `x@+0 y@+2 w@+4 h@+6`, `61 1 base` inline
  = disco) + sotto `0x5b` (specifica arco). Implementato in wfweb (`blendSector`).
  ⚠️ Il sotto-record `0x5b` **non** è solo "`max` u16 `@+4`" come documentato in precedenza — quello leggeva la
  metà bassa di un `max i32` e perdeva gli **angoli di inizio/fine sweep e la larghezza del tratto** che stanno
  subito dopo. Esiste anche un fratello procedurale `0x80`/`0x5a` (con raggio esplicito) che questo
  documento non ha mai coperto. Layout completo del record, e cosa l'assunzione precedente "in senso orario dalle 12"
  sbagliava: **§11.15**.
- **Id della sorgente dati** — il blocco attributi `82` dell'elemento sta a `delim+3` (dopo l'ultimo `40 01 00`),
  e **l'id della sorgente è un `u8` a `+0x14`** (anche `relX@+0x07 s16`, `relY@+0x09 s16`, `anchor@+0x0C/0E`,
  `mode@+0x15`, `frame-count@+0x1A`). Anchor < 0 = allineamento al bordo del genitore. 🔎 Il firmware risolve
  l'id tramite una tabella di getter a 142 voci a `0x101f371c` (ognuna chiama `ux2sys_get(type)`).
  ⚠️ **Preferisci leggere l'id come `meta[9]` della struct (§11.11)** — un campo fisso — rispetto a questa
  scansione in avanti dell'attributo `82`, che è la fonte dell'errore di uno descritto in §11.8/§11.9. Tabella id completa in §16; nota
  che le etichette salute/meteo elencate inline in questa sezione (`0x19` FC, `0x1b` batteria,
  `0x24` temperatura, `0x36` passi) sono **contestate e probabilmente sbagliate** — vedi il riquadro ⚠️ in §16.
  (L'esempio del gruppo `0x07:0x0b:0x0f` = HH:MM:SS qui sotto non è influenzato.)
- **Nodo gruppo** (`0x68`): annida i suoi figli dentro il proprio corpo TLV (`0x60` = valore/testo,
  `0x30` = statico); ogni `0x60` porta il suo id sorgente a `data+16`. Es. un gruppo `0x07:0x0b:0x0f` =
  orologio HH:MM:SS. Parser elementi TLV = `0x100db55c` (tabella di salto indicizzata da `tag−0x70`).

### 11.6 Matrice di authoring

| Percorso | Stato | Note |
|---|---|---|
| Dial fotografico da qualsiasi immagine | ✅ **fatto** | §11.3; validato sul dispositivo |
| Installare uno qualsiasi dei 103 dial dello store | ✅ **fatto** | §11.4; `9075`, `old_id`=attivo |
| Reskin del background cf=4 di un dial dello store | ✅ **funziona live** | scambia il payload FULL in posizione, imposta la `len` dell'asset alla **nuova** dimensione del blocco (≤ vecchia), mantieni la stessa impronta del file, installazione pulita |
| Ri-authoring tramite template (scambia i pixel di qualsiasi layer + sposta la geometria) | ✅ **renderizza via BLE** | dial 373: bg→ciano + uno sprite cf=5→rosso + X spostato 224→100, tutto renderizzato, lancette live |
| Dial strutturato 100 %-sintetico da zero | ✅ **builder fatto, validato offline** | builder envelope `0x20` in `watchface_struct.rs` (`build_container`/`serialize`/`validate_container`); round-trip byte-exact su tutti i 103 dial + sintetico supera il validator del firmware (§11.7). 🟡 render on-device su `9075` non ancora filmato |
| Font di sistema (`.font`) | ✅ **decodifica/render (tutti)** | bin LVGL (non proprietario); 32 font numerici (`num*/nm*`, non compressi) + 24 font di testo (`font*`, LVGL RLE `comp=1`) tutti decodificati — 12208 glifi, 0 overrun, ASCII completo. RLE = LVGL v8.3 `lv_font_fmt_txt.c` (SINGLE/REPEATE/COUNTER a 3 stati + prefilter XOR per riga), portato 1:1, nessun disasm |

⚠️ Insidie di reskin/ri-authoring che causano schermo nero o `0a`: lasciare la **vecchia `len` dell'asset** (l'orologio
legge oltre il blocco → overrun → nero); **ingrandire il file** (rifiutato all'installazione); riusare un id
**in posizione** invece di un'installazione pulita; **riordinare gli asset** senza correggere la catena
delle dimensioni dei blocchi della coda di riferimenti (§11.11). Non fatale oggi ma scrivilo comunque correttamente: qualsiasi modifica alla scena o al pool di asset
invalida le due parole **CRC32** dell'header — ricalcola entrambe (§11.4), `@0x20` prima di `@0x00`.

### 11.7 L'envelope di scena `0x20` — decodificato e builder implementato ✅

Un dial **reale** ri-authorizzato renderizza perché preserva l'envelope di scena del file. Un corpo puramente sintetico
di record piatti `61 …` viene **rifiutato** — il parser del firmware (`WFManager_Parser`, `0xdb35c`)
richiede che il corpo (dall'offset `0x24`) inizi con un contenitore di scena `0x20`. Il file completo è:```
[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

La scena è un TLV nidificato pulito — [tag u8][len u16 LE][body], i tag contenitore 0x20/0x21/0x22/0x68 ricorsivi, i drawable foglia 0x30 (statico) / 0x70 (elemento/puntatore) / 0x80 / 0x81 / 0x86 (nome). (I record piatti 61 01 00 / 61 0a 00 sono pattern che vivono dentro i body dei drawable; il vecchio parser li trovava in modo euristico — e cuciva insieme body adiacenti, vedi la revisione §11.5. Il layout del body dei drawable è ora completamente decodificato lì.) Ogni offset+len di un figlio deve rientrare nella finestra del genitore; primo byte del body ≠ 0x20 → errore parser −16; un figlio che sfora la sua finestra → −2; entrambi fanno scrivere al gestore 9065 (0xeb50c) il finish .

Il builder è implementato e validato offline (core-rust/watchface_struct.rs: SceneNode / serialize / parse_scene / validate_container / build_container / build_container_raw; CLI cmfwatch-wfgen reframe):

  • scene_roundtrip_identity — tutti i 103 quadranti dello store: parse_scene→serialize riproduce la scena byte per byte (i len nidificati ricalcolati corrispondono) e validate_container passa su ognuno.
  • build_reframe_identity / CLI reframe — riassemblare l'intero .bin da zero riproduce il file byte per byte tranne 1 byte di padding del nome (@0x17; non è un checksum).
  • build_container_synthetic — compone un nuovo quadrante (sfondo + drawable nidificato in 20→21) che supera l'invariante esatto del firmware (build_container emette finestre nidificate corrette).
  • validate_rejects_bad_containers — rifiuta un body piatto (→ , il bug storico ) e un figlio che sfora la sua finestra (→ ).

🟡 Ancora non dimostrato (serve l'orologio, non bloccante): caricare un sintetico da zero tramite 9075 e guardarlo renderizzare — la prova strutturale offline copre già ciò che causava il rifiuto 0a.

11.8 Rifiniture di fedeltà del rendering (2026-07-02, quadrante 275 "SlopeTime")

Ho incrociato il render di wfweb con le miniature ufficiali dello store (oracolo pixel su tutti i 103 quadranti) e ho chiuso quattro lacune:

  • X/Y di drawable/puntatore sono i16 (con segno). ✅ Le ancore possono essere negative per elementi che si estendono fuori dalla tela — es. la lancetta dei secondi rossa di 275 sta a Y = 0xFFFC = −4 (uno sprite 30×281, sorgente 0x12, ruotato dal centro oltre il bordo superiore). Leggere X/Y come u16 (65532) faceva scartare il guard. Analizza entrambi come con segno e consenti un piccolo intervallo negativo.
  • Le cifre dell'orologio digitale possono essere 0x60 img_numbers di primo livello (non solo dentro un gruppo 0x68), e la vera sorgente dati è il u8 all'offset di record −5 — la scansione in avanti dell'attributo 82 è sistematicamente fuori di uno qui e cattura l'attributo del fratello successivo (in 275 la cifra dei minuti prendeva il 0x18 del giorno della settimana). Il "10:10" di 275 = ora 0x07@X≈306 + min 0x0b@X≈369 con i come statico adiacente tra loro, ciascuno un atlante di 11 glifi (). ⚠️ Quando correggi X/Y da , anche gli , altrimenti una riesportazione corrompe quei byte (rompe la stessa impronta → ).

Inoltre: le miniature ufficiali dello store sono renderizzate alle 10:10 (classico orario di marketing), non alle 10:12 — allineare l'oracolo alle 10:10 riduce notevolmente la differenza media di pixel. Il parser di wfweb ora fa round-trip di tutti i 103 quadranti byte-esatto (la correzione degli offset di scrittura X/Y sopra ha eliminato le ultime discrepanze).

11.9 Salto del contenitore AOD + sorgente img_number standalone (2026-07-03, quadrante "Gradient")

  • Separa il contenitore AOD 0x22 nella sua propria vista. ✅ Il walker della scena salta già 0x22, ma la scansione piatta di testo/numero attraversava l'intero [0x30, firstAsset) — quindi emetteva la variante always-on (AOD) di ogni elemento come layer normale. Su "Gradient" l'atlante della data grigia AOD (offset in 0x22) si disegnava sopra quello rosso normale. Fix: tagga ogni record 0x22 con layer.aod=true (con il suo proprio set di dedup) e lascia che renderAt(…, aod) li mostri solo in modalità AOD (la modalità normale nasconde i layer aod; la modalità AOD nasconde quelli normali; lo sfondo è scambiato da setAod e si disegna sempre). Vincita netta dell'oracolo in modalità normale sull'intero corpus (284: 31%→21%, +18 altri) — le varianti AOD sovrascrivevano molti quadranti — e il toggle AOD dell'editor ora mostra il vero layout always-on invece di quelli normali. La vera AOD è uno schermo nero (nessuna scena attenuata): se il quadrante non ha un frame di sfondo AOD dedicato (dial.aod), la scena normale è nascosta in modalità AOD così renderizza nero + gli elementi al loro colore. Le AOD vengono parse anche tramite il walker della scena (ora ricorre nel contenitore taggando i drawable , invece di lasciarli alla scansione piatta dove il loro pivot non corrispondeva → "non posizionati"); le lancette AOD ruotano al centro della tela (il a volte porta una x/y di lancetta fuori centro che il firmware ignora — es. l'ora di Gradient ). L'editor espone anche questo come (§UI): ogni schermata mostra solo i propri layer e le modifiche persistono in modo indipendente. Il render in modalità normale è byte-identico ovunque; il roundtrip resta byte-esatto su tutti i 103 quadranti.

11.10 CONTEGGIO CIFRE img_number — il byte 40 01 00 XX (✅ confermato dal firmware)

Quante cifre disegna un img_number è un singolo byte nel record del campo — il byte dati XX del sotto-record attributo 40 01 00 XX dell'elemento (il sotto-record 0x40 che sta dopo la tabella frame 61 [count][base][glyph-ids]):

  • nibble basso XX & 0x0F = numero di slot cifra (0 ⇒ default firmware 7).
  • bit 7 0x80 = zero-pad (mostra zeri iniziali, es. "09" vs "9").

Confermato disassemblando il firmware (immagine XIP 0x10000000; routine di render 0x100d8e60): NDIG = ldrb[40sub+3] & 0x0F (→7 se 0); il valore è clampato value % 10^NDIG e vengono disegnati esattamente NDIG glifi MS-first, gli zeri iniziali soppressi a meno che bit7. Il u16 dopo la sorgente (60 per la data, 1000 per kcal) NON è il conteggio — alimenta solo l'inserzione del glifo separatore di migliaia/milioni (cmp #1000/#1000000), motivo per cui modificarlo non faceva nulla. L'id sorgente non limita nemmeno.

L'istogramma del corpus su tutti i 620 campi numerici corrisponde: i campi a 2 cifre (ora/min/sec/data/temp/HR) finiscono 40 01 00 02/0x82; kcal …04; passi …05; gli split di orologio a cifra singola 0x81. Quindi il campo data 40 01 00 82 = 2 cifre, zero-padded — è l'intera ragione per cui una temperatura Fahrenheit rimbalzata (≥100) veniva troncata.

Fix / editor: wfweb analizza digitCount/digitZeroPad (+digitCountOff) per i campi numerici, espone "Digits" + "Zero-pad" nell'ispettore, scrive il byte in-place (stessa impronta), e l'anteprima clampa/padda a digitCount per rispecchiare il firmware. Quindi rilegare la sorgente di un campo e impostare il suo conteggio cifre funziona per qualsiasi campo (es. data→temperatura °F → Digits 3). Oracolo in modalità normale invariato (0 regressioni, 3 piccoli miglioramenti); roundtrip byte-esatto su tutti i 103 quadranti. (L'ipotesi precedente di "larghezza Digits"/rectW era sbagliata — la larghezza è solo layout, non il conteggio.)

11.11 Inventario dei nodi, il record struct e la coda di riferimenti alle risorse ✅

La scena (§11.7) è un TLV nidificato pulito — [tag u8][len u16 LE][body]. Inventario completo dei tag come osservato sul corpus:

✅ Questo inventario è completo per il corpus. Ricorrendo solo nei tag contenitore sopra, il TLV della scena di tutti i 15 quadranti controllati cammina esattamente fino alla sua lunghezza radice dichiarata con zero tag sconosciuti — quindi un parser che gestisce questa tabella gestisce l'intero formato, e un tag sconosciuto significa una lettura disallineata, non un nuovo tipo di nodo. (Attenzione: un walker che ricorre in ogni nodo la cui lunghezza capita di essere ≥ 3 scenderà nei body struct/0x5b e allucinerà una lunga coda di "tag" una tantum — i body foglia non sono TLV.)

💡 Scorciatoia di authoring: l'auto-layout 0x48/0x68 può essere saltato del tutto — ogni widget può essere posizionato con x,y assoluti direttamente al livello superiore dello schermo, che è ciò che fa il builder da zero (§11.7). Serve solo per leggere quadranti esistenti. Una larghezza meta di 0x8000 marca uno struct come figlio auto-layout di un frame (la posizione viene dal genitore, non da x,y).

Body 0x01 struct — un prefisso fisso di 18 byte seguito da una coda opzionale di riferimenti alle risorse:``` +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:~
> ✅ Questo unifica gli "offset magici" dei §11.5/§11.8/§11.9. Quelle sezioni individuano i campi *relativi
> al byte `0x61` della tabella dei frame* — che è semplicemente `+0x12` di questa struct, quindi `−18`/`−16` = `x`/`y` e
> **`−5` = `meta[9]`, l'id della sorgente**. Stessi byte, un unico layout pulito. L'euristica di scansione in avanti
> dell'attributo `82` che era sistematicamente sfasata di uno non serve affatto: basta leggere `meta[9]` della struct.
> Il `max` di un campo numerico è anch'esso semplicemente `meta[11..13]` (ad es. i campi giorno-del-mese portano `max = 99`).

**Coda di rif** (`61`) — come un nodo punta alle sue bitmap:```
+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

⚠️ Quei u16 finali erano precedentemente documentati come "count×id(u16)" / id di glifi. Sono dimensioni di blocco: base, più la somma progressiva di essi, percorre il pool di asset voce per voce (✅ verificato esattamente per tutte le 10 voci di un atlante di cifre). Due conseguenze:

  • Gli asset referenziati da un nodo devono essere consecutivi nel pool di asset. Non c'è accesso casuale — la catena si muove solo in avanti. Pianifica il pool in modo che ogni set di cifre (10), ogni pick-list (N) e ogni foglio di frame sia un blocco contiguo. Uno scrittore che riordina gli asset senza correggere la catena produce un file che legge spazzatura (→ schermo nero).
  • Solo le prime count−1 dimensioni sono portanti; il valore dell'ultima voce non viene mai seguito, quindi i file in natura a volte contengono un valore obsoleto lì. Non trattare una discrepanza sulla voce finale come un riferimento rotto.

0x28 anteprima — la miniatura del negozio/catalogo è incorporata nel .bin stesso (27 nodi di anteprima su 15 quadranti), come una pvStruct 0x08: un prefisso di 5 byte più la stessa coda di riferimento, con nessun x/y. Utile per creare una UI a galleria senza distribuire PNG separati.

11.12 Condizioni di visibilità — tag 0x02 ✅

Il fratello 0x02 di un widget lo rende condizionale. Senza di esso, il widget viene sempre disegnato. Grammatica:``` count u8 , count × ( id u8 , op u8 , val u24 LE signed ) [5 bytes per entry]

root@kitploit:~
`id` è un id di origine dati (§16) — inclusi gli **id di slot sintetici** del §11.13. Operatori, con
conteggi di occorrenza misurati su 15 quadranti:

| op | significato | visti |
|---|---|---|
| `0x01` | disegna se `value == val` | 99 |
| `0x81` | come `0x01` (bit `0x80` impostato — appare su varianti mutuamente esclusive) | 48 |
| `0x02` | **nascondi** se `value == val` | 7 |
| `0x03` | disegna se `value == val`, dove `val` è un **marcatore di assenza dati** (es. HR `1000`) | 13 |
| `0x05` | disegna se `value >= val` | 58 |
| `0x06` | disegna se `value <= val` | 50 |
| `0x04` | ⚠️ **sconosciuto** — 15 occorrenze, nessuna semantica confermata | 15 |

Regola di combinazione (come implementata dal renderer di riferimento, maschera `op & 0x7f`): le voci di
uguaglianza vengono combinate in **OR**, poi le voci hide/`>=`/`<=` devono **tutte** essere soddisfatte.

Questo singolo meccanismo copre gran parte della variabilità runtime del formato e spiega strutture che
sembrano widget duplicati:

- **Layout 12h / 24h e metrico / imperiale** — due set di widget impilati nello stesso punto, ciascuno
  legato a `id 0x73` (il flag delle unità) con `val` 0 o 1. Il quadrante 275 ha sei di queste coppie (12 nodi).
- **Segnaposto "Nessun dato"** — op `0x03` contro un sentinella, es. `id 0x5f, val 1000` (quadrante 275, due volte):
  disegna l'arte del trattino lungo invece di una temperatura quando la metrica non è disponibile.
- **Evidenziazioni a bucket** — un intervallo accoppiato `0x05`/`0x06`, es. la catena di Metaball dove ogni anello si
  illumina per la propria finestra di 5 minuti.
- **Alternate per slot di complicazioni** — legate agli id di slot sintetici del §11.13.

### 11.13 Slot di complicazioni configurabili — `0x85` + `0x5f` ✅ (sostituisce §11.8)

Il §11.8 concludeva che la metrica attiva di una complicazione configurabile è stato RAM del dispositivo e non poteva
essere recuperata dal file. **Ciò era sbagliato** — sia il menu delle metriche dello slot sia la sua selezione predefinita
sono nel `.bin`. Ogni nodo `0x85` porta un fratello `0x5f`:```
+0x00  slotIndex u8   [0-based position among sibling 0x85 nodes]
+0x01  count     u8   [how many metrics this slot offers]
+0x02  activeIdx u8   [index into the list below = the DEFAULT SHOWN METRIC]
+0x03  count × u8     [the metric ids themselves (§16)]           … NUL padding

Le varianti che vengono effettivamente disegnate sono i normali gruppi 0x68 altrove nell'albero, ciascuno condizionato da una condizione 0x02 (§11.12) sull'id sintetico 0x79 + slotIndex — quindi le varianti dello slot 0 si legano a 0x79, quelle dello slot 1 a 0x7a, e così via. Per renderizzare uno slot: leggi activeIdx, poi disegna la variante la cui condizione corrisponde a quell'indice.

Misurato su quadranti reali:

I due slot da 6 metriche del quadrante 275 coprono 12 dei suoi 26 nodi 0x02, esattamente come previsto: 01 79 81 0X 00 00 e 01 7a 81 0X 00 00 per X = 0..5 — sei varianti chiavate su 0x79 (slot 0) e sei su 0x7a (slot 1). (I byte 0x79/0x7a che §11.8 chiamava "byte di istanza" sono questi id di legame.) I restanti 14 non sono correlati: 12 su 0x73 (il flag 24h/unità metriche, val 0 o 1 — sei coppie di widget che commutano tra layout 12h e 24h) e 2 su 0x5f con op 0x03 e val = 1000, il segnaposto di nessun dato per la temperatura.

Ancora genuinamente stato del dispositivo: qualunque cosa l'utente scelga in seguito nell'app companion sovrascrive activeIdx a runtime, quindi un'anteprima riproduce il default di file, non necessariamente ciò che mostra un dato orologio. imgs[0] di un nodo 0x85 è un segnaposto "tocca per configurare" che il firmware disegna solo nella propria modalità di modifica — saltalo quando anteprimi la normale visualizzazione dell'ora.

11.14 Colore d'accento — il flag di capacità meta[7] == 4 ✅

Alcuni quadranti consentono all'utente di scegliere un colore d'accento sul dispositivo, e il firmware lo sostituisce nelle bitmap del widget al momento del rendering. L'interruttore è un singolo byte: meta[7] della struct (§11.11) — cioè il byte +0x0B del corpo 0x01 — uguale a 4 contrassegna le risorse di quel widget come tintabili.

  • È un flag di capacità per widget, non un colore. Ricolora ogni pixel non trasparente di una risorsa contrassegnata (lascia invariato il canale alfa); non è coinvolto alcun test di colore per pixel.
  • Prevalenza: 37 su 499 struct e 8 su 15 quadranti misurati qui; il corpus fmc riporta 56 su 100 quadranti con almeno un widget contrassegnato.
  • 🛑 Non incorporare mai un colore d'accento nei byte esportati. La sostituzione è attiva sull'orologio; un .bin distribuito deve mantenere i suoi pixel originali o perdi permanentemente la scelta dell'utente. Applica la tinta solo nel percorso di anteprima/canvas.

⚠️ Non "migliorare" questo in un'euristica del colore — quel percorso è un vicolo cieco dimostrato (documentato da fmc dopo averlo fatto nel modo più difficile). La teoria intuitiva è che i pixel contrassegnati siano incorporati in qualche colore segnaposto riconoscibile che il firmware sostituisce. Non può funzionare: l'anello tintabile del quadrante 348 Tumbler e le strisce di cifre ordinarie non tintabili dei quadranti 282 Radar Sweep / 291 Vertical incorporano l'esatto stesso RGB (255,72,32) (verificato in modo esaustivo, ogni pixel); e i quadranti 305 Dots (lancetta delle ore) e 306 Large Number (cifre) sono tintabili pur essendo incorporati in bianco puro, quindi un test del colore li mancherebbe del tutto. Raffinamenti successivi (1 → 4 colori di riferimento, più una lista consentita di ruoli widget) hanno tutti fallito. Leggi il flag.

Verificato incrociato contro il dispositivo reale / app companion su 7 quadranti, scelti per stressare entrambe le direzioni — 349 Theatre, 376 Digits time, 305 Dots, 306 Large Number, 304 Elaborate 2 offrono tutti l'impostazione dell'accento e hanno tutti widget meta[7]==4; 316 Trailing (lancetta rossastra, nessuna impostazione), 312 Disc e 295 Vortex non ne offrono alcuna e hanno zero widget contrassegnati.

meta[4..6] si trova proprio accanto al flag e sembra poter codificare un colore su alcune struct (un RGB dall'aspetto reale con coda f1=1,f2=255, rispetto al segnaposto (1,0,0)/(4,0,0) delle struct contrassegnate). Non correla con la capacità di accento. ⚠️ Non risolto; ignoralo.

11.15 Anelli di avanzamento — 0x80/0x5a (procedurali) e 0x81/0x5b (ritaglio immagine) ✅

Entrambe le varianti di anello abbinano una struct breve (x, y, meta con l'id sorgente — di solito senza alcuna coda ref) a un fratello di specifica dell'arco. §11.5 documentava solo "0x5b: max u16 @+4", che è la metà bassa di un max i32 e perde la geometria di scansione. Record completo:``` +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:~
L'accoppiamento è rigido: `0x80` trasporta sempre esattamente `0x01` + un `0x5a` da **19 byte**, `0x81` sempre esattamente `0x01` + un `0x5b` da **17 byte** (✅ 26/26 anelli su 15 quadranti). ⚠️ Ma il **`0x81` ritagliato dall'immagine domina** — 25 di quei 26. La variante procedurale `0x80`/`0x5a` è apparsa **una volta** (quadrante 273), quindi il suo campo `radius` e il layout da 19 byte si basano su un singolo campione; trattala con sospetto finché non la rivedi.

`frac = clamp((valore − min) / (max − min), 0, 1)`, e l'arco riempito va da `start` verso `end`.
L'angolo zero è alle **3 in punto**, positivo in senso orario. Esempi misurati:

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

> ⚠️ **Questo corregge l'assunzione "in senso orario dalle 12 in punto"** nel §11.5. Quello è solo il caso
> speciale `start = 0, end = 3600` (una scansione completa, dove la convenzione è inosservabile). I quadranti reali usano
> **indicatori parziali** (il ventaglio ±102,8° del 273, l'anello a tre quarti da 270° del 366) e **scansioni negative**
> (60° → −120° del 276), quindi un renderer che esegue sempre una scansione completa del cerchio dall'alto li disegna in modo errato.
> 🟡 La convenzione esatta dell'angolo zero e la regola di direzione provengono dal renderer di fmc, incrociate
> con questi valori su disco — non verificate indipendentemente pixel-per-pixel sul dispositivo da questo repository.
> Nota che `frac = valore/max` e il ritaglio del settore `0x81` nel §11.5 **sono stati** validati (quadrante 322).

⚠️ **Non risolto: il trailer a 3 byte `01 00 kk`.** `kk` assume valori dall'aspetto plausibile (104, 152, 216,
232, 248) e l'ipotesi ovvia è un raggio — **testata e confutata**: il quadrante 366 usa `kk = 104`
sia per un anello 82×82 che per uno 166×166, e il quadrante 273 usa `kk = 232` sia per un anello 440×440 che per uno 284×284.
Non è un raggio, non è un diametro. Possibilmente opacità/stile. Reinstradalo così com'è.

🟡 **Insidia segnalata, non verificata qui:** nel renderer di fmc, un numero `0x60` il cui id sorgente è uguale all'id di **qualsiasi** anello sullo stesso schermo renderizza `round(frac × 100)` invece del valore grezzo — quindi un numero di frequenza cardiaca accanto a un anello di frequenza cardiaca mostra `36` invece di `71 bpm`. La loro soluzione documentata è di mettere l'anello e il numero su due id **alias** della stessa metrica (passi `0x19`/`0x26`/`0x49`, calorie `0x1c`/`0x1e`/`0x48`). Se questo sia un comportamento del firmware o specifico del loro renderer **non è stabilito** — vale un controllo dal vivo prima di progettare attorno a questo.

---

## 12. Dettagli sul trasferimento bulk e OTA

La tabella di trasferimento è nel §6. Punti aggiuntivi confermati:

- **AGPS/EPO** ✅: il primo blocco scritto inizia con l'header ASCII `000000010000…`. Il ciclo completo init → `[A05F ↔ 905F]×N` → finish è stato osservato sul filo (~892 blocchi).
- **OTA del firmware** (`9040`–`9042`, finish `9041`) 🔎: struttura mappata; payload INIT2 = byte di versione (es. `0b 00 00 39` = 11.0.0.57). **Non testato sul campo** (l'app disabilita l'aggiornamento FW qui). Le immagini del firmware sembrano essere **non firmate — l'integrità è solo CRC32** (nessuna firma asimmetrica osservata nella RE).
- ⚠️ Poiché OTA e `FACTORY_RESET (009A 0001)` condividono la sessione autenticata, una singola autenticazione BLE valida è sufficiente per cancellare o (in linea di principio) brickare l'orologio. Maneggia con cura.

---

## 13. Sensori

✅ Hardware esposto via BLE:

- **PPG ottico** — frequenza cardiaca (manuale/automatica/allenamento/riposo), SpO₂ e stress derivato da HRV.
- **Accelerometro a 3 assi** — passi, distanza, calorie, fasi del sonno, alzata del polso, cadenza.
- **GNSS/GPS** (assistito da AGPS) — traccia dell'allenamento (`WORKOUT_GPS`) e push della posizione (`GPS_PUSH`).

Non c'è **barometro/altimetro, bussola, giroscopio o sensore di temperatura cutanea/corporea**. Un termistore NTC interno (temperatura della scheda/batteria) esiste ma è leggibile **solo** tramite il canale AT (`AT GETNTCTEMP`, §14) — il flusso della cronologia della temperatura cutanea `0155` è vuoto su questo SKU.

**Dirottamenti dei widget dati** (non esiste una vera API di complicazioni/data-binding — vedi §11.5): i campi di testo esistenti dell'orologio possono essere riutilizzati per mostrare dati esterni a colpo d'occhio. Provato ✅: la stringa della **città meteo** (`WEATHER_SET_1`, es. `"BRA 2x1 ARG"` è apparsa sul widget) e i campi **traccia/artista** della musica; l'elenco dei **contatti** (20 × nome[32]+numero[25]) funziona come pannello dati scorrevole. Sono tutti push, non complicazioni persistenti.

---

## 14. Canale shell AT di fabbrica (`77d4ff01` / `77d4ff02`)

Un canale di comandi AT in testo semplice separato, indipendente dal protocollo incorniciato. ✅ testato dal vivo:

- **Lettura:** `AT GETSECRET` (segreto di accoppiamento a 16 byte), `GETVERSION`, `GETSN`, `GETNAME`, `GETPID`,
  `GETBATLV` (mV grezzi, es. `3853mv`), `GETGSENSOR` (accelerazione grezza in g, `X=… Y=… Z=…`),
  `GETNTCTEMP` (°C, NTC interno).
- **Scrittura / attuazione:** `AT SETMOTOR=1` (fai vibrare il motore), `SETHR/SETHRV/SETSPO2=…` (iniezione di test del sensore), `SETLCDSWITCH/SETGPSSWITCH/SETKEYSWITCH`.

Le risposte terminano con `,OK`. I comandi `SET*` generalmente si eseguono ma potrebbero non restituire `,OK` via BLE — conferma caso per caso.

---

## 15. Funzionalità limitate dal firmware / non disponibili (🔎 RE del firmware)

Alcune funzionalità sono presenti nel firmware ma disabilitate da SKU/regione e **non sono raggiungibili dal telefono/BLE** — richiedono una modifica del firmware, che è fuori ambito qui:

- **Voce ChatGPT** — gate = id funzionalità `ux2sys` `0x9e`, inizializzato da NVRAM/EFUSE/regione all'avvio; su questo SKU il flag di supporto `908b = 00`. Non influenzabile da telefono, account o BLE (confermato da esperimento + RE). L'app è solo un relay; l'audio va telefono → cloud Nothing.
- **Pressione sanguigna** — esiste un sottosistema completo nel firmware, disattivato da SKU/regione.
- **Alipay / pagamento NFC** — UI completa presente, solo SKU Cina.
- **Assenti in hardware/firmware:** ECG, SOS/emergenza, NFC generico.

---

## 16. Id getter per sorgenti dati / complicazioni

Non necessario per costruire un client BLE, ma **essenziale per creare o renderizzare un quadrante**: questo è il valore di `meta[9]` (§11.11) che lega un widget ai dati live, l'`id` di una condizione di visibilità (§11.12) e le voci del menu metriche di uno slot (§11.13). Il firmware lo risolve tramite una **tabella di dispatch getter a 142 voci** a `0x101f371c`, ogni voce chiama `ux2sys_get(type)` (🔎 RE del firmware).

### Ora / data — ✅ ben consolidato

| id | significato | id | significato |
|---|---|---|---|
| `0x01` | ora (12/24h secondo l'impostazione del dispositivo) | `0x0f`, `0x12` | secondo (fluido) |
| `0x04` | ora (24h) | `0x10`, `0x11` | decine / unità dei secondi |
| `0x07` | ora (24h forzato) | `0x71`, `0x72` | secondo (ticchettio / angolo lancetta) |
| `0x02`, `0x03` | decine / unità ora-12h | `0x13` | flag AM/PM (0 = AM, 1 = PM) |
| `0x05`, `0x06`, `0x08`, `0x09` | decine / unità dell'ora | `0x15`, `0x16` | mese |
| `0x0a`, `0x70` | angolo lancetta ore | `0x17` | giorno del mese |
| `0x0b` | minuto | `0x18` | giorno della settimana (0 = lunedì ⚠️) |
| `0x0c`, `0x0d` | decine / unità dei minuti | `0x0e`, `0x71` | angolo lancetta minuti |

Gli id decine/unità disegnano una **singola cifra** — un widget legato a uno di essi renderizza un glifo, non l'intero valore (quadrante 284 Square). Gli id angolo lancetta (§11.5) sono `0x0a`/`0x70` ore = `h·30° + m·0,5°`, `0x0e`/`0x71` minuti = `m·6° + s·0,1°`, `0x12`/`0x72` secondi = `s·6°` (✅ confermato disassemblando i getter, fallback RTC 10:10:30).

### Salute / sensori / meteo — ⚠️ contestato, leggi la colonna delle prove

| id | significato (migliore lettura attuale) | prove |
|---|---|---|
| `0x19` | **passi** | nel menu slot del 368 insieme a `0x1a`; corrisponde a `parse.ts` di questo repo |
| `0x1a` | **frequenza cardiaca** | nel menu slot del 368 insieme a `0x5f` e `0x19` |
| `0x1c` | calorie | menu slot, icona fiamma nell'app companion |
| `0x1e` | calorie (alias) | corpus |
| `0x22` / `0x23` | distanza km / mi (parte intera) | corpus |
| `0x74` / `0x75` | distanza km / mi (parte frazionaria) | corpus |
| `0x76` | distanza (forma slot) | menu slot, icona strada |
| `0x24` | **% batteria** | menu slot, icona fulmine |
| `0x30` | % batteria | corpus |
| `0x36` / `0x5f` | temperatura | `0x5f` = menu slot, icona nuvola-sole; 361 TempoG lega un numero semplice |
| `0x48` | ore in piedi (ore in piedi) | menu slot, icona figura in piedi |
| `0x8b` | AQI | menu slot |
| `0x73` | flag 24h / unità metriche | corpus |
| `0x25`–`0x27`, `0x49`, `0x6c`, `0x6f` | % obiettivo / alias slot di passi e calorie | corpus |
| `0x6a` | ⚠️ metrica slot non identificata | appare in 4 menu slot |
| `0x79 + slotIndex` | **sintetico** — non una metrica; l'id di selezione slot (§11.13) | ✅ §11.13 |

> ⚠️ **Tre tabelle in questo repo erano in disaccordo; questa è la riconciliazione.** Revisioni precedenti del §16
> e del §11.5 leggevano `0x19` come frequenza cardiaca, `0x1b` come batteria, `0x24` come temperatura e `0x36` come passi —
> e `wfweb/src/codec/mock.ts` codifica ancora quella lettura, mentre `wfweb/src/codec/parse.ts` ne codifica una
> diversa (`0x19` passi, `0x24` % obiettivo, `0x48` ore in piedi, `0x1a` meteo). La tabella sopra segue
> la lettura con prove migliori (etichette calibrate da fmc contro i menu icona degli slot widget dell'app companion, vedi §Fonti). **Il codice non è stato ancora modificato — `mock.ts` e `parse.ts` sono ancora incoerenti tra loro e con questa tabella.** Tratta le etichette degli id salute come ⚠️ finché qualcuno non lega un campo a ciascun id e legge l'orologio.
>
> La prova singola più forte è **il menu slot del quadrante 368 Function** (§11.13), che offre
> `0x5f 0x1c 0x19 0x48 0x24 0x76 0x1a 0x8b` come **otto metriche distinte selezionabili dall'utente** in un unico menu.
> Qualunque siano le etichette, nessuna coppia di quegli otto può essere la stessa metrica — il che esclude
> `0x19` = `0x1a` = frequenza cardiaca e `0x24` = `0x5f` = temperatura simultaneamente.

Le complicazioni ad anello/arco con un **foglio di frame** indicizzano un frame pre-renderizzato (es. 50 % = frame 50 di 100)
incorporato nel `.bin` che invii (§11.5), quindi non serve un pacchetto RES esterno; gli anelli senza immagine vengono disegnati
dalla specifica dell'arco invece (§11.15).

---

### Quick-cards (tile home) — `QUICK_CARD (906D)` ✅

Le tile home dell'orologio. Il telefono sceglie solo **quali** tile mostrare e in **che ordine** — le tile sono
renderizzate dal firmware (nessun canale di contenuto). Primo byte del payload = sottocomando: `00` = GET, `01` =
SET; **entrambi usano `0x906D`** (`0x906C` è elencato ma non usato — interrogarlo va in timeout). Risposta = `A06D`.

> 🛑 Inviare un `assemblyId` inventato **cancella gli schermi dell'orologio** (accetta l'elenco, non può abbinare
> gli id, non mostra nulla). Invia solo id che **rileggi** tramite GET; recupera tramite l'app ufficiale o un
> reset di fabbrica.

**Risposta GET** ✅: `status(1) ‖ 00 ‖ N(1) ‖ N × gruppo`, gruppo = `tag=01 ‖ K(1) ‖ K×(assemblyId, sportId)`.
Frame reale: `01 00 04  01 02 5d00 6100  01 03 1900 2e00 2300  01 03 5c00 0400 5a02  01 03 4800 5100 5300`
= 4 schermi / 11 card (`5a02` = card Sport, sportId 2).

**Slot:** ogni schermo ha **4 slot**. Il tipo di una card imposta la sua dimensione — `circolare`/`quadrata` = 1 slot,
`rettangolo` = 2 slot. La validazione è pura aritmetica degli slot (Σ ≤ 4 per schermo); nessuna card mutuamente esclusiva. `sportId` è `0` tranne sulle card Sport (87–91). Gli id `64` e `95` non esistono.

**Catalogo `assemblyId`** (ogni tipo logico = un intervallo contiguo di 6 varianti di stile `_0`..`_5`;
`0` = slot vuoto):

| dec | card | dec | card |
|----|----|----|----|
| 0 | slot vuoto | 49–53,97–98 | Meteo |
| 1–6 | Passi | 54–58 | Timer |
| 7–12 | Calorie | 59–62 | Respirazione |
| 13–18 | In piedi | 63,65–67 | Cronometro |
| 19–24 | Attività moderata | 68–71 | Batteria |
| 25–30 | Frequenza cardiaca | 72–76 | Recenti |
| 31–36 | SpO₂ | 77–81 | Contatti |
| 37–42 | Stress | 82–86 | Quadrante / telefono |
| 43–48 | Sonno | 87–91 | Sport (`sportId` ≠ 0) |
| 92 | Musica | 93/94/96 | Registro attività / PAI / Ciclo |

---

## Appendice A. Catalogo quadranti stock (id → nome)

Il §11 si riferisce ai quadranti con id numerico in tutto il documento (275 SlopeTime, 322 Glare 2, 357 Silhouette, …). Gli id
`273`–`376` sono i volti del negozio/stock; l'id è ciò che `DIAL_COMMAND (9055/a055)` riporta come attivo e
ciò che `9075` accetta come `old_id` (§11.4). 100 dei 103 id noti sono nominati sotto; i restanti sono
stub ROM (§11.4). Nomi e raggruppamenti come mostrati dall'app companion ufficiale.

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

---

## Fonti

I layout dei byte sopra sono stati ricostruiti dal firmware (1.0.0.73), dall'APK ufficiale (3.5.7) e da
catture live decriptate contro un dispositivo reale. L'implementazione di riferimento per questo progetto vive in
`core-rust/src/{commands,frame,crypto,health,session}.rs` (Rust), l'editor TypeScript `wfweb/` e
gli strumenti Python `cmftool/` (`pair.py`, `session.py`, `wf_codec.py`, `upload_custom.py`, …).

**Lavoro indipendente incorporato qui.** [freethinkel/fmc](https://github.com/freethinkel/fmc) — un
editor di watchface SvelteKit + marketplace per lo stesso orologio — ha indipendentemente fatto reverse engineering del
formato `.bin` da un corpus di ~100 volti e ha raggiunto diversi risultati che questo documento mancava o aveva
sbagliato. I loro `docs/cmf-protocol.md`, `src/lib/modules/editor/lib/{wf,render}.ts` e
`src/lib/modules/device/lib/ble.ts` valgono la lettura diretta. Risultati adottati, ciascuno ri-verificato
contro i byte dei quadranti prima di essere scritto qui:

| Risultato | Dove | Stato qui |
|---|---|---|
| Entrambe le parole header sono CRC32 (variante non standard) | §11.4 | ✅ ri-verificato 9/9 quadranti; **corregge** "nessun checksum bloccante" |
| Condizioni di visibilità, tag `0x02` | §11.12 | ✅ ri-verificato; era non documentato |
| Elenco metriche slot + `activeIdx` predefinito, `0x79 + slotIndex` | §11.13 | ✅ ri-verificato su 275/368/273/304; **sostituisce** §11.8 |
| Flag capacità tinta accento `meta[7] == 4` | §11.14 | ✅ prevalenza ri-verificata; era non documentato |
| Specifica arco completa (angoli di scansione, larghezza, raggio) | §11.15 | ✅ ri-verificato; **corregge** "`max` u16 `@+4`" |
| I `u16` della coda ref sono dimensioni dei blocchi, non id glifo | §11.11 | ✅ ri-verificato esattamente su un atlante a 10 glifi |
| Footer a 36 byte; variante magic byte `0x02`; `0x86` = 64 B; anteprima incorporata `0x28` | §11.4, §11.11 | ✅ ri-verificato |
| Etichette id salute/meteo calibrate contro il menu slot dell'app companion | §16 | ⚠️ adottato come migliore lettura; conflitti segnalati |
| L'UUID del servizio shell è `77d4e67c-…`, e l'ambito `optionalServices` di Web Bluetooth | §1 | ⚠️ report a unità singola, non ri-verificato qui |
| Numero che condivide l'id con l'anello renderizza una percentuale | §11.15 | 🟡 segnalato, **non** verificato qui |
| Catalogo id → nome quadranti stock | Appendice A | ✅ adottato così com'è |
Scarica lo strumento
ScopoServizioCaratteristicaProprietà
Scrittura comandi0000fff0-0000-1000-8000-00805f9b34fb0000fff2-…Write
Notifica comandi0000fff0-…0000fff1-…Notify
Scrittura shell (AT)—77d4ff01-2fe2-2334-0d35-9ccd078f529cWrite
Notifica shell (AT)—77d4ff02-…Notify
Scrittura dati bulk—02f00000-0000-0000-0000-00000000ffe1Write
Notifica dati bulk—02f00000-…ffe2Notify
Nomecmd1,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
Nomecmd1,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
Nomecmd1,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
Nomecmd1,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
Nomecmd1,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 🔎 (FC/cal/passi live)FFFF 9078 / FFFF A078
FEMALE_CYCLE_SET / _RETFFFF 9071 / FFFF A071
SLEEP_CONFIG_SET / _RET (minuto target)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
Nomecmd1,cmd2
DIAL_COMMAND_SET / _RET (elenco/riordino/selezione)FFFF 9055 / FFFF A055
DIAL_CONFIG_SET / _RETFFFF 9075 / FFFF A075
CHANGE_DIAL (⚠️ inerte su 1.0.0.73 — non usare)009F 0001
QUICK_CARD_SET/GET / _RET (entrambi su 906D)FFFF 906D / FFFF A06D
Nomecmd1,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 🔎 (vuoto su questo SKU)0155 0001 / 0155 0002
WORKOUT_SUMMARY / _V30057 0001 / 0160 0001
WORKOUT_GPSFFFF A05A
DominioINIT1 req/replyINIT2 req/replyCHUNK req/writeFINISH ack1/ack2
Quadrante (foto)8052/00529063/A063A064/9064A065/9065
Quadrante (strutturato/switch)8052/00529075/A075A064/9064A065/9065
Firmware9052/A0529040/A040A042/9042A041/9041
AGPS/EPO905E/A05E—A05F/905FA060/9060
OffsetDimensioneCampo
04timestamp (s epoch)
44passi
84distanza (m)
124calorie
1616riservato (osservato 0)
OffsetDimensioneCampo
04inizio_sessione (epoch, UTC)
44risveglio (epoch, UTC)
82totale_profondo_s
102totale_leggero_s
122totale_rem_s
142totale_veglia_s
162⚠️ [incerto] (id/score sessione? i valori osservati non corrispondono alle somme dei record)
ac 49 1f 01
  • CONTACTS_SET (00D5 0001) ✅: N × 57 byte = nome(32) ‖ telefono(25). L'interfaccia dell'orologio mostra fino a 20.
  • ALARMS_SET (0063 0001) ✅ — corregge Gadgetbridge (che metteva l'etichetta alla fine, riempita con 0xff — sbagliato). 40 byte per sveglia, big-endian: secondiDelGiorno(i32) ‖ indice(u8) ‖ abilitata(u8) ‖ maschera-ripetizione(u8) ‖ flag(u8) ‖ etichetta[32] UTF-8. L'etichetta è all'offset 8 e viene mostrata sull'orologio. ripetizione = maschera dei giorni della settimana (0 = una tantum); flag è ⚠️ [incerto] (marcatore una tantum?). Esempio (13:30, idx 2): 0000bdd8 02 01 15 00 "Alarm…".
  • GOALS_SET (005E 0001) ✅ — l'app ufficiale e l'implementazione di riferimento usano il DailyTargetBean v1 da 10 byte, big-endian: passi(u32 BE) ‖ distanza_m(u32 BE) ‖ calorie_kcal(u16 BE). (Questa è la forma di Gadgetbridge; i precedenti rapporti che l'orologio lo "ignorava" erano un bug di decrittazione di sessione stantia, non un problema di payload.) 🔎 La RE del firmware mostra anche una variante estesa più lunga da 29 byte (aggiunge sleep_min/exercise_min/stand_h + 6 flag di abilitazione, tutti u32 BE dopo un prefisso flag(u16 LE), con intervalli imposti: passi 2000–30000, dist 1000–99000, cal 100–5000, sonno 360–720, esercizio 30–90, in piedi 6–16) — non il percorso predefinito dell'app; preferisci la forma da 10 byte a meno che tu non abbia bisogno dei target extra.
  • STANDING_REMINDER / WATER_REMINDER (0060/0061 0001) ✅: 11 byte: abilitato(1) ‖ soglia_min(u16 LE) ‖ dndStart(u32 LE) ‖ dndEnd(u32 LE). Nota che la "finestra attiva 08:00–22:00" mostrata nell'interfaccia è un default fisso del firmware e non è trasportata nel payload.
  • SPORTS_SET (00DC 0001) ✅: count(1) = 36 slot ‖ activityTypeCode[36] (codici attivi poi padding 00). Seleziona quali sport appaiono nel menu allenamento dell'orologio.
  • HEART_MONITORING_ENABLED (009B 0001) ✅: byte kind — 01 = FC 24/7, 02 = SpO₂, 04 = stress (misurato ogni 30 min).
  • HEART_MONITORING_ALERTS (FFFF 9059) ✅: disabilitato = 00; abilitato = 01 ‖ fcBassa ‖ fcAlta ‖ fcAltaSport ‖ spo2Bassa ‖ 00 00 00 00 (un limite 0/255 = "nessun limite").
  • FEMALE_CYCLE (FFFF 9071) ✅: 01 ‖ predictionOpen ‖ notifySwitch ‖ cycleStartSwitch ‖ cycleStartNotifyBefore ‖ ovulationStartSwitch ‖ ovulationStartNotifyBefore ‖ fertileStartSwitch ‖ fertileStartNotifyBefore ‖ periodo(1) ‖ cicloPeriodo(1) ‖ dataInizioCiclo(u32) ‖ markStart(u32) ‖ markEnd(u32) (catturato: periodo=5, cicloPeriodo=0x1c=28).
  • QUICK_REPLY (FFFF 9073) ✅: TLV — count(1) ‖ total(1) ‖ [id(1) ‖ len(u16 LE) ‖ msg-UTF8]… (7 risposte predefinite catturate e decrittate).
  • WORLD_CLOCK (FFFF 906F) ✅: invia ID città numerici, non nomi (01 ‖ count ‖ cityId(2 BE)…); l'orologio mappa gli id da una tabella interna. Config DST FFFF 9083 = count ‖ [id(u16 LE) ‖ dst(u16 LE) ‖ inizio(u32 LE) ‖ fine(u32 LE)]….
  • MUSIC_INFO_SET (FFFF 905C, 131 B) ✅: stato(1: 0=nessuno/1=pausa/2=riproduzione) ‖ volume(1) ‖ volumeMax(1) ‖ traccia(64) ‖ artista(64). L'orologio invia anche MUSIC_BUTTON (A05D) indietro.
  • WEATHER_SET_1 (FFFF 906B, 199 B) ✅ — usa questo: 7×9 byte giorni + 24×2 byte ore + città(32) + 7×8 byte alba/tramonto (LE). Le temperature sono codificate come (temp_c + 100) & 0xFF. ⚠️ Lo stesso payload inviato su WEATHER_SET_2 (0066 0001) non aggiorna il widget meteo su Pro 2 — usa sempre 906B. (La stringa città è anche un vettore provato di data-hijack — vedi §13.)
  • FIND_WATCH (005D 0001) ✅: payload 0x01 → l'orologio suona/vibra (+ ACK 005D 0003).
  • GPS_PUSH (FFFF 906A) ✅ — big-endian, longitudine prima: 16 byte ts(u32 BE) ‖ lon×1e7(i32 BE) ‖ lat×1e7(i32 BE) ‖ 00 00. Validato su una posizione reale.
  • WORKOUT_GPS (FFFF A05A) ✅ — little-endian, longitudine prima: 12 byte ts(i32) ‖ lon×1e7(i32) ‖ lat×1e7(i32).
  • TIME (FFFF 8004): vedi §7.
  • cfbppraster (dopo LZ4)uso
    42RGB565-LEsfondo opaco (FULL/THUMB)
    53RGB565-LE (2 B) + alpha (1 B) per pxsprite anti-alias (glifi, lancette, icone)
    13 (0x0d)0.5maschera alpha a 4 bit; il firmware colora a runtimeatlante glifi cifre
    24 (0x18)4RGBA8888layer a colori pieni (incl. il sempre-attivo aodImage)
    1—JPEG/JFIF (ff d8 ff), estrai con qualsiasi decoderrari frame di animazione
    0a
    0x61
    NotEnvelope
    0a
    ChildOverflow
    :
    61 0a 00
    −18/−16
    offset di scrittura devono spostarsi
    0a
  • Slot di complicazioni multi-variante — la metrica attiva NON è nel .bin ⚠️ questo punto era sbagliato ed è sostituito dalla §11.13. Una complicazione configurabile è creata come N nodi gruppo 0x68 impilati alle stesse (x,y), ciascuno disegnato solo quando una condizione di visibilità corrisponde — questo era giusto. Ma la lista di metriche per slot (il 0x1c/0x6a/0x48/0x24/0x19/0x76 di 275) è esattamente la lista di metriche, non "id di opzioni/stili"; l'indice attivo predefinito è un byte nel file; e il byte 0x79/0x7a non è un "byte di istanza" ma l'id su cui sono chiavate le alternative (0x79 + slotIndex). Un'anteprima statica può riprodurre il default del file. Vedi §11.13.
  • Complicazioni inattive ancorate al bordo (es. testo bpm a (446,0), visto su 275/302/325/365/375) sono slot che il firmware non disegna nella vista predefinita — il loro valore non può nemmeno entrare prima del bordo della tela. Trattali come nascosti nell'anteprima.
  • 0x22
    lancette
    0x22
    aod
    0x22
    @69,209
    UI di modifica isolata Normal|AOD
  • 0x60 img_number standalone (cnt=10) — sorgente a −5, fuori di uno in avanti. ✅ Stesso fuori di uno della §11.8 ma per numeri non-orologio: la data di "Gradient" stava a (203,80) in alto al centro con sorgente 0x17, ma la scansione in avanti 82 catturava il getter dell'angolo del puntatore vicino (0x0a) e la posizione del puntatore → il numero renderizzava al posto del puntatore con una sorgente fasulla. Fix: per un 61 0a 00 img_number in un wrapper 0x60, fidati di −5/−18/−16 quando la sorgente in avanti è impossibile per un numero (sorgente-0 o un getter di angolo del puntatore 0x0a/0e/12/70/71/72) e la posizione −18/−16 è valida e non-zero (il guard non-zero salta le cifre figlie di gruppo con relX=0).
  • 0x17 = data (giorno del mese), 0x24 = temperatura — distinti. Il quadrante 340 usa entrambi (0x17 "Jun 09" e una 0x24 temp separata), quindi 0x17 è data, non temp. Un quadrante il cui orologio mostra una temperatura in uno slot 0x17 è una complicazione configurata dall'utente (stato del dispositivo), non il default del file.
  • tagruolocontenitore?body
    0x20radice della scena (wrapper del body, non un drawable)✅figli
    0x21schermata normale✅figli
    0x22schermata AOD (§11.9)✅figli
    0x28miniatura di anteprima catalogo incorporata✅un figlio 0x08
    0x68gruppo / contenitore auto-layout✅frame 0x48 + figli
    0x30immagine statica, o pick-by-value da N immagini✅0x01 (+0x02)
    0x60lettura numerica live (striscia di cifre)✅0x01 + 0x40 (+0x02)
    0x70lancetta rotante✅0x01 + pivot 0x05
    0x80anello di progresso, procedurale✅0x01 + 0x5a (§11.15)
    0x81anello di progresso, clippato da immagine✅0x01 + 0x5b (§11.15)
    0x85slot di complicazione assegnabile dall'utente✅0x01 + 0x5f (§11.13)
    0x01struct — geometria + attributi (sotto)—x,y,meta[14] + coda ref
    0x02condizione di visibilità (§11.12)—lista di condizioni
    0x05pivot — flag u8, pivotX u16, pivotY u16—5 B
    0x08pvStruct — prefix[5] + coda ref, senza x/y (solo anteprima)——
    0x40conteggio cifre / flag zero-pad (§11.10)—1 B
    0x48frame — x,y,w,h,gap,align riga/colonna auto-layout——
    0x5a / 0x5bspecifica arco per 0x80 / 0x81 (§11.15)—19 B / 17 B
    0x5flista di metriche slot per 0x85 (§11.13)——
    0x86nodo nome visualizzato, sempre esattamente 64 byte, terminato NUL, non disegnato—64 B
    quadranteslotconteggioactiveIdxid metriche→ attivo
    275 SlopeTime0601c 6a 48 24 19 760x1c calorie
    275 SlopeTime1641c 6a 48 24 19 760x19 passi
    368 Function0805f 1c 19 48 24 76 1a 8b0x5f temperatura
    368 Function1865f 1c 19 48 24 76 1a 8b0x1a frequenza cardiaca
    273 Activity Mood0401c 24 48 6a0x1c calorie
    304 Elaborate 20/1401c 48 6a 24 / 24 1c 6a 480x1c / 0x24