
CMF Watch Pro 2向けのリバースエンジニアリングされたBLEプロトコル。GATTレイアウト、AES-128-CBC暗号化コマンドフレーム、認証ハンドシェイク、および代替コンパニオンアプリ開発のためのヘルスデータ同期を文書化しています。
非公式. このドキュメントは、CMF Watch Pro 2(CMF by Nothing)のBluetooth Low Energy(BLE)プロトコルを、代替コンパニオンアプリ向けにリバースエンジニアリングによって再構築したものです。Nothing/CMFとは提携しておらず、承認も受けていません。自己責任で使用してください。
フレームヘッダとオペコード内のマルチバイト整数はビッグエンディアンです。コマンドペイロード内部の整数は、特に明記されていない限りリトルエンディアンです(これはデバイスファームウェアを反映しています)— 例外(GOALS_SET、GPS_PUSH、一括転送のオフセット/長さはビッグエンディアン)に注意してください。
以下に記載された自明でない主張にはすべて、それがどのように確立されたかを示すタグが付けられています。
すべてのキャプチャのテストデバイス: CMF Watch Pro 2-5485、fw 1.0.0.73、シリアル CI04102520008192、MCU Actions ATS3089C(Cortex-M4)、画面 466×360。
電話がGATTクライアント、時計がペリフェラルであり、CMF Watch Pro 2-XXXX(4桁の16進数)としてアドバタイズします。
各CCCD(00002902-…)に01 00を書き込むことで通知を有効にします。コマンドチャネル(fff1/fff2)は以下のフレーム化されたプロトコルを運びます。シェルチャネル(77d4…)はプレーンなAT形式のテキスト(例: AT GETSECRET。§14参照)を運びます。データチャネル(02f0…)は大きなバイナリブロブ(ウォッチフェイス、ファームウェア、AGPS)を運び、コマンドチャネル上の制御オペコードによって調整されます。
✅ 実際のセッション全体が単一のコマンドチャネル上で実行されました — 160秒間の高負荷キャプチャ中、明示的なOTA/ウォッチフェイス転送を除いて、データ/ファームウェアチャネルやシェルチャネルにトラフィックはありませんでした。
0xF5)すべてのコマンドチャネルメッセージは、1つ以上の11バイトヘッダフレームにラップされます。``` +------+-----------+--------+-------------+-------------+--------+-------------------+ | 0xF5 | chunkLen | cmd1 | chunkCount | chunkIndex | cmd2 | chunk bytes … | | 1 B | 2 B (BE) | 2 B BE | 2 B BE | 2 B BE | 2 B BE | chunkLen bytes | +------+-----------+--------+-------------+-------------+--------+-------------------+ __________________________ 11-byte header ____________________________/
- `cmd1`/`cmd2` は合わせて **オペコード** を構成します (§6 参照)。🔎 公式アプリのフレームビルダー (`C6117b.m30831g`) で確認済み。
- `chunkCount` = このコマンドの総チャンク数、`chunkIndex` は **1 ベース**です。
- `chunkLen` = このフレーム内の `chunk` のバイト数。
- 単一の BLE 書き込みはリンク MTU によって断片化される可能性があります。受信側は生のバイトをバッファリングし、完全なフレームを再抽出します。大きなペイロードは複数のチャンク(同じ `cmd1/cmd2`、増加する `chunkIndex`)に分割され、順番に再アセンブルされます。
### オペコードの慣例 (✅ ワイヤ上で確認済み)
- `cmd1 = 0xFFFF`: `cmd2` が `0x80xx`/`0x90xx` の範囲 = スマートフォン→ウォッチ (リクエスト/設定); `0x00xx`/`0xa0xx` の範囲 = ウォッチ→スマートフォン (応答)。ペアは下位バイトで一致します (`0x9055`↔`0xa055`、`0x8051`↔`0x0051`)。
- 機能固有の `cmd1`: `cmd2` のサフィックス = `0x0001` **SET**、`0x0002` **GET**、`0x0003` **ACK**。
### チャンク本体
各チャンクについて、本体は `payloadPiece ‖ CRC32_LE(payloadPiece)` です (4 バイト CRC、リトルエンディアン、zlib/IEEE)。コマンドが **暗号化** されている場合 (§3 参照)、`payloadPiece ‖ CRC` 全体が AES-128-CBC/PKCS7 で暗号化され、その暗号文がフレーム `chunk` になります。
**平文の特異性:** 平文オペコードの場合、ウォッチは `chunkLen` に 4 バイトの CRC を *カウント* しますが、それを **送信しません**。したがって、平文フレームをデコードする場合、実際のデータ長は `chunkLen − 4` です。(暗号化フレームは通常どおり CRC を暗号文内に保持します。)
チャンクサイズ設定 (暗号化チャンクが AES ブロック境界に合うように)、`maxWrite = mtu − 3` とします:
- 暗号化: `floor((maxWrite − 11) / 16) * 16 − 4 − 1`
- 平文: `maxWrite − 11 − 4 − 1`
✅ 観測されたすべての暗号化フレームの `chunkLen` 値は 16 の倍数でした (ブロックアライメントが成立)。
---
## 3. 暗号プリミティブ
- **AES-128-CBC**、**PKCS7** パディング、**固定 IV** (ファームウェア `CmfCharacteristic.AES_IV` より):
`50 51 52 53 54 55 56 57 60 61 62 63 64 65 66 5A`。
- **CRC32** (zlib/IEEE)、4 バイトのリトルエンディアンとして出力。
- **SHA-256**、パーツの連結に対して使用。
鍵導出:```
authkey = SHA256( rnd1 ‖ rnd2 ‖ secret )[0..16] // persisted across sessions
sessionKey = SHA256( nonce ‖ authkey )[0..16] // per connection
secret = 16バイトのデバイスシークレット(ウォッチからシェルコマンド AT GETSECRET → GETSECRET:<32-hex>,OK で取得可能)。rnd1 = 電話が選択する16ランダムバイト; rnd2 = ウォッチからの16ランダムバイト。nonce = ウォッチのノンス応答からのバイト。キーが設定された後、§5にリストされた平文のオペコードを除き、すべてのコマンドチャネルフレームはAES暗号化されます。
✅ 両方の導出が検証されました: ルート化された電話の ntwatch.db から回復された authkey は、キャプチャされた rnd1/rnd2/secret から導出された値と一致しました; キャプチャされたノンスから再現された sessionKey はライブフレームを復号します。
2つのエントリパスは同じノンス/確認テールを共有します。
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
`AUTH_FAILED (0xFFFF,0xA061)` または署名ミスの場合、認証は失敗します。
### 4.2 再接続 (authkey 既知)```
set crypto key = authkey (persisted)
phone → AUTH_PHONE_NAME (encrypted) payload = 0xA5 ‖ model
watch → AUTH_WATCH_MAC (encrypted)
phone → AUTH_NONCE_REQUEST (encrypted) payload = 0xA5
watch → AUTH_NONCE_REPLY (encrypted) payload = nonce
sessionKey = SHA256(nonce ‖ authkey)[0..16] → set crypto key = sessionKey
phone → AUTHENTICATED_CONFIRM_REQUEST (encrypted) payload = 0xA5
watch → AUTHENTICATED_CONFIRM_REPLY (encrypted) → Initialized
✅ 再接続順序(シェルトラフィックなし)は実際のキャプチャでそのまま観測されました。
⚠️→✅ データクエリの前に
TIMEが必須です。Initialized後、ウォッチはBATTERY、SERIAL_NUMBER_GET、またはACTIVITY_FETCH_*のハンドシェイクに応答しません 。 セッション内でTIME (FFFF 8004)が送信されるまで、未承諾のFIRMWARE_VERSION_RETのみが到着し、それ以外はすべてタイムアウトします。 ✅ ライブ確認済み(Pixel 8a):TIMEなしで 3 つの GET を送信 → ファームウェアのみ応答。TIMEを先に送信 → バッテリー および シリアルが応答を開始します。
推奨されるフェーズ 2 の順序:TIME → FIRMWARE_VERSION_GET → SERIAL_NUMBER_GET →
BATTERY (0xA5) → 設定プッシュ → ヘルス同期(§8)。
ほとんどの設定に個別の「読み取り」オペコードはありません。 *_GET(cmd2 = 0x0002、ペイロード
0xA5)を送信すると、ウォッチは現在の値を含む SET オペコード(cmd2 = 0x0001)で応答します。
SET コマンドは cmd2 = 0x0003 と空のボディで確認応答されます。
キーが設定されるとフレームは AES 暗号化されますが、以下のオペコードは常に平文です。
AUTH_PAIR_REQUEST(FFFF 8047)、AUTH_PAIR_REPLY(FFFF 0048)DATA_CHUNK_WRITE_WATCHFACE(FFFF 9064)、DATA_CHUNK_WRITE_FIRMWARE(FFFF 9042)、
DATA_CHUNK_WRITE_AGPS(FFFF 905F)フレーム ヘッダー(cmd1/cmd2)は常にクリアで送信されるため、キーがなくてもコマンド シーケンス は
キャプチャで可視です。暗号化が必要なのは ペイロード のみで、sessionKey が必要です。
(cmd1, cmd2)GET/SET/REQUEST = 電話→ウォッチ;RET/REPLY/ACK/RESPONSE/DATA = ウォッチ→電話。
| 名前 | cmd1,cmd2 |
|---|---|
| MUSIC_INFO_SET / _ACK | FFFF 905C / FFFF A05C |
| MUSIC_BUTTON | FFFF A05D |
| 名前 | cmd1,cmd2 |
|---|---|
| WEATHER_SET_1(動作する方) | FFFF 906B |
| WEATHER_SET_2(Pro 2 では無視 — §9 参照) | 0066 0001 |
JS のみのオペコード(
FFFF 8051、FFFF 0051、FFFF 90A2、FFFF 90C5、FFFF A056、FFFF 908A/908BChatGPT ステータス/サポート)は、Java 層ではなくアプリの Hermes バイトコードで 処理されます。これらのヘッダーはキャプチャに表示されますが、ペイロードのセマンティクスは ⚠️ [未確定] です。
ウォッチフェイス / ファームウェア / AGPS は、初期化 → チャンクリクエスト/チャンク書き込みループ → 終了確認応答を使用します。
(すべて cmd1 = FFFF。)ウォッチは DATA_CHUNK_REQUEST_*(offset, length)(offset/length = u32 ビッグエンディアン)を発行してループを駆動します。
電話は、データキャラクタリスティック上で payload[offset..offset+length] を運ぶ DATA_CHUNK_WRITE_* で応答します。詳細は §11–§12 を参照してください。
TIME (FFFF 8004) のペイロード = epochSeconds(i32, BE) ‖ utcOffsetMillis(i32, BE)。認証直後に送信されるため、ウォッチに現地時間が表示され(データクエリのブロックも解除 — §4.3 参照)。
⚠️ ウォッチからのヘルスタイムスタンプは UTC です。 コンパニオンアプリは、現地のカレンダー日付/時刻を導出する前に、ローカルの UTC オフセットを追加する必要があります。(生の UTC 日でヘルスをバケット化すると、間違った現地時間で日付が切り替わります。)
TIME_FORMAT (005F 0001) のペイロード = 1 バイト:00 = 24 時間制、01 = 12 時間制。
ACTIVITY_FETCH_1 を送信。ウォッチが ACTIVITY_FETCH_ACK_1 で応答(最初のバイト 01 ⇒ 準備完了)。ACTIVITY_FETCH_2 を送信。その後、ウォッチがデータフレームのバーストをプッシュ:
ACTIVITY_DATA、HEART_RATE_*、SPO2、STRESS、SLEEP_DATA、WORKOUT_SUMMARY[_V3]。同期は逐次的です(TIME に従う必要があり、ウォッチは ACK_2 後にストリームを解放します)。単一のバーストではありません。大量のセッションでは、約 160 秒で約 170~210 の通知フレームがプッシュされます。✅
ACTIVITY_DATA(各 32 バイト、LE)✅カロリー単位: アクティビティカロリーは cal(グラムカロリー)で報告されます。1 日の合計を 1000 で割って kcal にします。(対照的に、ワークアウトサマリーのカロリーはすでに kcal です。)
timestamp(i32 LE) ‖ value(i32 LE)
(値 = bpm / SpO₂ % / ストレス指数)。00DA 0001)は異なります — 5 バイト:timestamp(i32 LE) ‖ hr(u8)。
✅ ライブ例 5e dc 29 6a 4e → ts、hr = 78 bpm。ストレススコアの範囲:1–29 / 30–59 / 60–79 / 80–99。SLEEP_DATA(18 バイトヘッダー + N × 8 バイトレコード)✅1 つの SLEEP_DATA = 1 つの睡眠セッション。夜間には複数のセッションが含まれる場合があります(微小な覚醒がセッションを分割)。
ヘッダー:
各 8 バイトレコード:timestamp(u32) ‖ duration_s(u16) ‖ stage(u16)。
ステージコード:1 = Deep、2 = Core/light、3 = REM、4 = Awake。✅ 一晩全体(2 セッション、D/C/R/A の合計が一致)で検証済み。
WORKOUT_SUMMARY v1(54 バイト)/ _V3(0160 0001)v1:start(u32)、end(u32)、duration_s(u32)、次にタイプ/カロリー/歩数/距離/平均 HR、および
GPS/拡張ブロック。✅ v1 レイアウトはファームウェアに対して確認済み。WORKOUT_SUMMARY_V3 は同じデータ用の新しいレイアウトで、約 40 バイトの拡張ブロック(exerciseLoad、有酸素/無酸素、recoveryTime、
VO₂max、ケイデンス、PAI、ベストラップタイムなど)が追加されています。フィールドセットは(アプリの Room DB から)既知ですが、その 40 バイトブロック内の 正確なバイトオフセットは ⚠️ [未確定] です。 — これらを確定するには、GPS ワークアウトの生キャプチャが 1 つ必要です。
文字列は UTF-8 で、フィールドサイズに バイト単位で切り詰められます(切り詰めによりマルチバイト文字が分割される場合があります。これはファームウェアの s.encode()[:max] の動作と一致します)。短いフィールドは右側にゼロ埋めされます。
0065 0001)✅:iconCode(1) ‖ 0x00 ‖ when(u32 BE) ‖ titleLen(1) ‖ title ‖ body。
iconCode はアプリアイコンを選択します(WhatsApp=8、Telegram=12、Instagram=18、Gmail=27;不明=0xFF)。
タイトル ≤ 20 バイト、body ≤ 128 バイト。クライアントから送信 → ウォッチに表示され ACK 0065 0003。005C 0001)✅:応答 = level(1) ‖ charging(1)(例:3b 00 = 59 %、充電中ではない)。00DE 0001)✅:len(1) ‖ ASCII(例:10 + "CI04102520008192")。0095 0001)✅:height_cm(1) ‖ weight_kg(1) ‖ age(1) ‖ gender(1: 1=男性)
(例: = 172 cm / 73 kg / 31 / 男性)。now/utc_offset を明示的なパラメータとして受け取ります(決定論的、テスト可能)。トランスポートが実際の時刻を提供します。TIME がすべてをゲートする(§4.3) — 最初に送信しないと、ウォッチはデータクエリに応答しません。GOALS_SET と
GPS_PUSH はビッグエンディアン、一括転送のオフセット/長さはビッグエンディアンです。ウォッチは(a)写真/カスタムダイヤル(背景画像 + ファームウェア描画のデジタル時計)と(b)構造化ダイヤル(内蔵/ストアのフェイス:背景 + 位置指定のスプライトレイヤー、針、テキストウィジェット)をサポートしています。両方とも、§6 の init → チャンクループを介してデータチャネル上で転送されます。
実際に動作するもの(✅ ライブで検証済み): 任意の画像から フォトダイヤルを構築 してインストールする。103 個のストアダイヤル をオフラインでインストールする。構造化ダイヤルを リキン する(背景または非背景スプライトを交換)およびそのレイヤーを 移動 する。アクティブフェイスの 並び替え/切り替え。および スクラッチから構造化ダイヤルを構築する —
0x20シーンエンベロープはデコードされ、ビルダーが実装されています(§11.7)。オフライン で 103 個すべてのストアダイヤルをバイト単位でラウンドトリップし、ファームウェア自身のバリデータを通過する合成コンテナを出力することが証明されています。🟡 唯一未証明のステップ は、9075を介して デバイス上 でスクラッチからの合成レンダリングを確認することです(構造的なオフライン証明は、以前に0a拒否の原因となったものをすでにカバーしています)。コーデックやトランスポートの障壁はなく、ベンダーツールチェーンも必要ありません。 「構造化レンダリングは RES パックに焼き付けられている / BLE では不可能」および「cf=0x1f サーバーサイドコーデック」という古い主張は 誤りでした(オフセット+バイト/ピクセルのバグ) — ファームウェアは構造化ダイヤルを 送信したファイルからデータ駆動でレンダリングします。
DIAL_COMMAND (9055 / a055) ✅a055 = result(u8) ‖ selectIndex(u8) ‖ total(u8) ‖ max(u8) ‖ N × dialId(u32 LE) ‖ ffffffff。例:01 05 06 07 … = アクティブ #5、6 ダイヤル、最大 7。CHANGE_DIAL (009F 0001) は fw 1.0.0.73 では無効(定数を返し、切り替えない) — 使用しないでください。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)
完了応答バイト: `01` = 有効化済み & 保存済み; `0a` = 保存されたが **有効化されていない** / 拒否された。Androidでは、各 `DATA_CHUNK_WRITE` は **1フレームにつき1回のBLE書き込み** として送信する必要があります。連結してMTUで再スライスするとヘッダーが同期しなくなり、ウォッチはオフセット0を要求するループになります。
- **`9063`(写真)= APPEND.** ダイアルリストが増加(6→7)。`watchfaceId = 0xFFFFFFFF`(カスタムセンチネル)であるため、重複として拒否されることはなく、ウォッチが自動的に有効化します。
- **`9075`(構造化)= REPLACE** `old_id`スロットを置き換えます。`old_id` はすでにリストに存在している **必要があります**(そうでない場合は `0a`)。すでに存在するIDを再インストールするには、**最初に削除し**(9055 list-minus-id)、「新規」としてアップロードします。IDをそのまま再利用すると `0a` になります。
### 11.3 写真 / カスタムダイアル — ✅ エンドツーエンドで完全検証済み
**コンテナ**(バイト検証済みラウンドトリップ、全フィールドはリトルエンディアン):```
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]
コーデック = RGB565 リトルエンディアン、トップダウン上の標準 LZ4 ブロック (payloadLen は最初の LZ4 バイトからカウント)。公式アプリは LZ4-HC を使用し、21バイトの LZ4 ブロックヘッダー/フッターを削除します。プレーンなリテラルのみの LZ4 エンコーダーでも動作します — ウォッチは有効な LZ4 を受け入れ、バイト同一性は必要ありません。内接円(中心 233,233、半径 233)の外側のピクセルは 0x0000 に設定されます。
9063 用の INIT_2 — 正確なヘッダー (✅ これが動作するものです):``` 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
`size` = 正確な`.bin`の長さ; `FFFFFFFF` = カスタム`watchfaceId`; `styleId` 0~4は内蔵デジタル時計レイアウトを選択します(常に描画され、「オフ」はありません); `posX/posY`で位置指定(実績のある値 56/77); `color565`で色を付けます(例:`FFFF`=白)。⚠️ 短い`A5 ‖ size ‖ watchfaceId`形式は、完了コード`0a`で**拒否**されます — 上記の完全なヘッダーを使用してください。(参考実装:`core-rust/engine.rs::build_wf_init2`、公式アプリの`C6135t.m31104u`をミラーリング)
**手順:** 画像を466×466(および270×270のサムネイル)にリサイズし、RGB565-LEにトップダウンで変換し、必要に応じて円外のピクセルをゼロにし、それぞれをLZ4圧縮し、上記のコンテナを組み立て、`watchfaceId = 0xFFFFFFFF`で`9063`パイプライン経由でアップロードします。(参考コーデック:`core-rust/watchface.rs`、`work/codec_dfa.py`)
### 11.4 構造化/ストアダイヤル — コンテナとコーデック ✅
**ヘッダー**(すべての103個のストアダイヤルで同一。すべてのフィールドはリトルエンディアン):```
0x00 perDialId u32 LE [per-dial id/hash; NOT a content checksum — 4 "Default" dials share one]
0x04 version 0x00000001 [constant]
0x08 name char[] [NUL-terminated, e.g. "SlopeTime", "Metaball"]
0x18 size_a u32 LE [= filesize − 36] ✅ 100 % confirmed across 103 dials
0x1c size_b u32 LE [data-section length, < size_a]
0x20 3× u32 LE id/hash words [not a CRC]
0x2c name (repeated on larger dials)
~0x60 directory of layer records (61 xx 00 …) then the asset pool
There is no blocking checksum (CRC32/Adler32/byte-sum all fail to match) — repacking is not gated. Stub dials (~173 B, e.g. ids 273/274/277) are placeholders for faces baked into ROM: header + directory, no real assets.
Assets — each is dimsWord(u32 LE) ‖ len(u32 LE) ‖ LZ4(payload), where
cf = dimsWord & 0x1f, w = (dimsWord >> 10) & 0x7FF, h = (dimsWord >> 21) & 0x7FF, and len
counts from the first LZ4 byte (the 1f 00 01 00 you often see there is the first LZ4 token — do
not skip it). Decompressed size = w·h·bpp:
✅ All 4151/4151 assets across the 103 dials decode exactly with a standard lz4.block
decompressor at w·h·bpp. Transparency is the alpha byte (cf=5/24) or 0x0000 (cf=4 outside the
circle) — there is no RLE and no "escape". Encode = re-raster → standard LZ4 → [dimsWord][len][LZ4].
INIT_2 for 9075 — AES-encrypted body:```
kind(1) ‖ old_id(u32 LE) ‖ new_id(u32 LE) ‖ file_len(u32 LE)
`kind` = `0x02`/`0x03`; `old_id` = 現在アクティブなダイヤル(`9055` から); `file_len` = 実際の `.bin` サイズ(= `@0x18 + 36`)。ストアの `.bin` をそのままインストールするのが保証された方法です(Ring Data id 359 + 他 102 個確認済み)。(参考: `core-rust/engine.rs::build_dial_replace_init`。)
### 11.5 構造化ディレクトリ文法 ✅(デコード済み&実装済み — 改訂 2026-07-02)
> **⚠️ 改訂(2026-07-02):以下のフラットな `61 01 00` レコードスキーマは、系統的に
> 1 バイトずれていました。** シーン本体はクリーンな TLV(§11.7)であり、描画可能な **リーフ本体**(タグ `0x30`/`0x38`
> 静的、`0x70` ポインタ)は次の通りです:
>
> ```
> 01 xx 00 [X u16][Y u16] …attrs… 61 [count u16][base u32][count×id u16] [05 05 00 01 pivX pivY]
> ```
>
> - attr `0x01` が本体を開く:**X,Y = 466² キャンバス上の左上隅**(SDK の `sty_picture_t` の `s16 x,y`)。
> - **フレームテーブル `61 …` が本体を閉じる**(`base` = アセットポインタ; `count` 1 = 画像、10/11 =
> 数字アトラス — 古い「レコードタイプ `0a/0b`」は実はこの count でした! — 7/13/2 = コンプリケーション
> フレームシート)。
> - ポインタ付加情報:`0x01` attr 内のソース+スケール `[src] 00 3c 00`; ピボットは
> `05 05 00 01 [pivX][pivY]` **トレーラー**内。**回転中心 = ポインタごとに `(X+pivX, Y+pivY)`** —
> 固定の (233,233) ではありません:中心からずれたサブダイヤルが存在します(例:ダイヤル 366 の針は 150,150 を中心に回転)。
>
> `61 01 00` の線形スキャンは、要素 **N** のフレームテーブル+ピボットを、要素 **N+1** の X/Y
> (およびタグバイト、古い "f3")に縫い付けていました — 隣接する針がほぼ同じ形状を共有するアナログダイヤルでのみ
> 正しく *見えた* のです。「コンパクトバリアントの壁」(仕様 24 §24.4.5)も、この同じ誤読によるものです。
> `core-rust/watchface_struct.rs` および `wfweb/src/codec/parse.ts` で `scan_scene_drawables` として実装済み
> (シーン = 画像/ポインタの主要ソース; テキスト+非エンベロープフォールバック用にフラットスキャンも保持)。
> `wfweb/compare.html` オラクル(レンダリング vs 公式ストア PNG、99 ダイヤル)で検証済み:64→72 good、8→5 bad、平均差分 9.3→7.4%。
歴史的なフラットレコード読み取り(置き換え済み、コンテキスト保持):
- **静的画像**(`61 01 00`):`asset_ptr(u32) ‖ elemId(u16) ‖ 05 05 00 01 ‖ pivotX(u16) ‖
pivotY(u16) ‖ 3B ‖ 01 ‖ 1b 00 ‖ X(u16) ‖ Y(u16)`。466² キャンバス上の左上隅 = `(X−pivotX, Y−pivotY)`。
- **ポインタ/針** — 同じ画像レコード、実行時に回転。**回転中心 = `(X+pivotX, Y+pivotY)`**
(アナログダイヤルでは約 233,233)。**データソースはレコードオフセット `+36` の `u8`**、スケール `u16` は
`+38`(=60):`0x0a`/`0x70` = 時(`h·30°+m·0.5°`)、`0x0e`/`0x71` = 分(`m·6°+s·0.1°`)、
`0x12`/`0x72` = 秒(`s·6°`)。✅ ゲッターの逆アセンブルにより確認済み(RTC フォールバック 10:10:30)。
- **テキスト/数値ウィジェット**(`61 0a 00`):`asset_ptr(u32) ‖ [10×u16 フォントメトリクス] ‖ 40 01 00 ‖ フラグ ‖
3B ‖ 01 ‖ u16 ‖ X(u16) ‖ Y(u16)`。`asset_ptr` はグリフ "0" を指します; 桁 *d* = `index("0") + d` のアセット
(連続する 10 個の cf=5 スプライト、例:`0123456789` および `,°` 句読点)。✅ レンダリング済み。
- **コンプリケーション塗りつぶし = フレームインデックス**(✅ 数字/列挙/ゲージコンプリケーション、count>1 で確認済み):
値は `.bin` 内の **プリレンダリング済みフレームシート** をインデックスします — `frame = (count−1)·val/100`(パーセント)または
`frame = value`(フリップ数字 / 列挙)。フレームテーブル = サブレコード `61 ‖ count(u16) ‖ base(u32) ‖
count×id(u16)`。例:327 Digit Max の大きな時針は 13 フレームシート(数字 0–12)、`frame = hour`。
- **進行リング / アーク = ランタイムセクタークリップ**(✅ 2026-07-02、**仕様 25 §2 の「リングはフレームシート」という読み取りを訂正**):
要素タグ **`0x81`** は **単一** の完全なディスク(`61` フレームテーブル `count == 1`)を持ち、部分的な扇形はそのディスクを
**パイセクターにクリップ** したもの(`frac = value/max`、12 時方向から時計回り)— 322 Glare 2 でピクセル単位で検証済み、
**20 個のダイヤル** で `count==1` を確認。ディスク上:`0x81` 本体 = サブ `0x01`(ジオメトリ `x@+0 y@+2 w@+4 h@+6`、インライン `61 1 base`
= ディスク)+ サブ `0x5b`(`max` u16 `@+4`、=100、ただし 332=60)。wfweb で実装済み(`blendSector`)。
- **データソース ID** — 要素の `82` attr ブロックは `delim+3`(最後の `40 01 00` の後)にあり、
**ソース ID は `+0x14` の `u8`**(`relX@+0x07 s16`、`relY@+0x09 s16`、`anchor@+0x0C/0E`、
`mode@+0x15`、`frame-count@+0x1A` も)。Anchor < 0 = 親の端に揃える。🔎 ファームウェアは
`0x101f371c` の 142 エントリゲッターテーブルを通じて ID を解決します(各々が `ux2sys_get(type)` を呼び出す)。
一般的な ID(§16):`0x07` 時、`0x0b` 分、`0x0f` 秒、`0x16` 月、`0x18` 曜日、`0x13` 午前/午後、
`0x19` 心拍数、`0x1b` バッテリー%、`0x24` 気温、`0x36` 歩数、`0x70/71/72` 針角度、
`0x25–27` 目標%。これは以下の `0x07:0x0b:0x0f` = HH:MM:SS グループの例と一致します。
- **グループノード**(`0x68`):自身の TLV 本体(`0x60` = 値/テキスト、`0x30` = 静的)内に子をネストします。
各 `0x60` は `data+16` に自身のソース ID を持ちます。例:グループ `0x07:0x0b:0x0f` = HH:MM:SS 時計。
TLV 要素パーサー = `0x100db55c`(`tag−0x70` でインデックスされるジャンプテーブル)。
### 11.6 オーサリングマトリックス
| パス | ステータス | 備考 |
|---|---|---|
| 任意の画像からフォトダイヤル | ✅ **完了** | §11.3; 実機で検証済み |
| 103 個のストアダイヤルをインストール | ✅ **完了** | §11.4; `9075`、`old_id`=アクティブ |
| ストアダイヤルの cf=4 背景をスキンチェンジ | ✅ **ライブで動作** | ペイロード全体をその場でスワップ、新しいブロックサイズ(≤ 旧)にアセット `len` を設定、ファイルフットプリントを維持、新規インストール |
| テンプレートによる再作成(任意のレイヤーのピクセルを入れ替え+ジオメトリ移動) | ✅ **BLE 経由でレンダリング** | ダイヤル 373: 背景→シアン、cf=5 スプライト→赤、X 224→100 に移動、すべてレンダリング、針はライブ |
| ゼロからの 100% 合成構造化ダイヤル | ✅ **ビルダー完成、オフライン検証済み** | `watchface_struct.rs` の `0x20` エンベロープビルダー(`build_container`/`serialize`/`validate_container`); 103 個すべてのダイヤルをバイト単位でラウンドトリップ+合成版はファームウェアバリデーターを通過(§11.7)。🟡 `9075` 経由の実機レンダリングは未撮影 |
| システムフォント(`.font`) | ✅ **デコード/レンダリング(すべて)** | LVGL bin(プロプライエタリではない); 32 個の数字フォント(`num*/nm*`、非圧縮)+24 個のテキストフォント(`font*`、LVGL RLE `comp=1`)をすべてデコード — 12208 グリフ、オーバーラン 0、完全な ASCII。RLE = LVGL v8.3 `lv_font_fmt_txt.c`(3 状態 SINGLE/REPEATE/COUNTER+行単位の XOR プリフィルター)、1:1 移植、逆アセンブル不要 |
⚠️ ブラックスクリーンまたは `0a` を引き起こすスキンチェンジ/再作成の落とし穴:**古いアセット `len`** を残す(ウォッチがブロックを読み過ぎる → オーバーラン → ブラック); ファイルを **大きくする**(インストールで拒否); 新規インストールではなく **その場で** ID を再利用する。
### 11.7 `0x20` シーンエンベロープ — デコード済み&ビルダー実装済み ✅
再作成された **本物の** ダイヤルがレンダリングされるのは、ファイルのシーンエンベロープを保持しているからです。純粋に合成された
フラットな `61 …` レコードの本体は **拒否されます** — ファームウェアパーサー(`WFManager_Parser`、`0xdb35c`)は
本体(オフセット `0x24` から)が `0x20` シーンコンテナで始まることを要求します。完全なファイルは:```
[0x00,0x24) header: perDialId@0 · version=1@4 · name[16]@8 · size_a@0x18 · size_b@0x1c · idWord0@0x20
[0x24, fa) scene: 20 <u16 L0> ( 21 <u16 L1> ( 86 <len>=name , 30/70/80/81… drawables ) [ 22 … AOD ] )
[fa, EOF) assets: [dimsWord u32][len u32][payload = 1f 00 01 00 + LZ4] …
size_a = filesize−36 · size_b = filesize−36−first_asset · 0x27+L0 == first_asset
シーンは クリーンなネストTLV — [tag u8][len u16 LE][body]、コンテナタグ 0x20/0x21/0x22/0x68 で再帰、リーフ描画オブジェクト 0x30 (静的) / 0x70 (要素/ポインタ) / 0x80 / 0x81 / 0x86 (名前)。
(フラットな 61 01 00 / 61 0a 00 レコードは描画オブジェクトボディの 内部 に存在するパターンです。以前のパーサーはヒューリスティックにそれらを見つけ、隣接するボディをつなぎ合わせていました。§11.5 の改訂を参照してください。描画オブジェクトボディのレイアウトは現在そこで完全にデコードされています。) すべての子の offset+len はその親のウィンドウ内に収まらなければなりません。
最初のボディバイト ≠ 0x20 → パーサーエラー −16; 子がウィンドウをオーバーラン → −2; いずれの場合も 9065 ハンドラ (0xeb50c) が完了 0a を書き込みます。
ビルダーは実装され、オフラインで検証済み (core-rust/watchface_struct.rs:
SceneNode / serialize / parse_scene / validate_container / build_container /
build_container_raw; CLI cmfwatch-wfgen reframe):
scene_roundtrip_identity — すべての103 ストアダイアル: parse_scene→serialize はシーンを バイト単位で 再現し (再計算されたネスト len が一致)、validate_container はすべてで合格。build_reframe_identity / CLI reframe — ** .bin 全体をゼロから再構築** すると、ファイルを 1 つの名前パディングバイトを除いて バイト単位で再現 (@0x17; チェックサムではない)。build_container_synthetic — ファームウェアの正確な不変条件を満たす 新しい ダイアル (背景 + 20→21 にネストされた描画オブジェクト) を構成 (build_container が正しいネストウィンドウを出力)。validate_rejects_bad_containers — フラットな ボディ (→ 、過去の バグ) と、ウィンドウをオーバーランする子 (→ ) を拒否。🟡 まだ未検証 (時計が必要、非ブロッキング): 9075 経由でゼロから作成した合成ダイアルをアップロードしてレンダリングを確認 — オフラインの構造的証明はすでに 0a 拒否の原因をカバーしています。
wfweb のレンダリングを公式ストアのサムネイル (全 103 ダイアルのピクセルオラクル) と相互参照し、4 つのギャップを閉じました:
i16 (符号付き)。 ✅ アンカーはキャンバス外に伸びる要素に対して 負の値 を取ることができます — 例: 275 の 赤い秒針 は Y = 0xFFFC = −4 にあります (30×281 スプライト、ソース 0x12、中央から上端を超えて回転)。X/Y を u16 (65532) として読み取るとガードがドロップしました。両方を符号付きとして解析し、小さな負の範囲を許可します。0x60 img_numbers になり得る (0x68 グループ内だけでなく)、そして 実際のデータソースはレコードオフセット −5 の u8 です — 前方 82 属性スキャンはここで体系的に 1 つずれており、次の 兄弟の属性を取得します (275 では分の数字が曜日 0x18 を拾いました)。275 の "10:10" = 時 0x07@X≈306 + 分 0x0b@X≈369 で : がそれらの間の隣接する静的要素としてあり、それぞれ 11 グリフのアトラス ()。⚠️ X/Y を から修正する場合、。そうしないと再エクスポートでそれらのバイトが破損します (同一フットプリントが壊れ → )。また: 公式ストアのサムネイルは 10:10 (古典的なマーケティング時間) でレンダリングされており、10:12 ではありません — オラクル時間を 10:10 に合わせると平均ピクセル差が顕著に減少します。wfweb のパーサーは現在、103 すべてのダイアルを バイト単位で正確に ラウンドトリップします (上記の X/Y 書き込みオフセット修正により、最後の不一致が解消されました)。
0x22 AOD コンテナを独自のビューに分離。 ✅ シーンウォーカーはすでに 0x22 をスキップしますが、フラットなテキスト/数字スキャンは [0x30, firstAsset) 全体を走査していたため、各要素の 常時表示 (AOD) バリアント を通常レイヤーとして出力していました。"Gradient" では、AOD の 灰色 の日付アトラス (0x22 内のオフセット) が 赤 の通常のものの上に描画されていました。修正: すべての 0x22 レコードに layer.aod=true をタグ付けし (独自の重複排除セットを持つ)、renderAt(…, aod) で AOD モードでののみ表示するようにします (通常モードは aod レイヤーを非表示、AOD モードは通常レイヤーを非表示、背景は setAod で切り替えられ常に描画されます)。コーパス全体の通常モードでの正味オラクル改善 (284: 31%→21%、+18 その他) — AOD バリアントが多くのダイアルでオーバードローイングしていました — そしてエディタの AOD トグルは、通常のレイアウトではなく実際の常時表示レイアウトを表示するようになりました。実際の AOD は 黒 画面 (薄暗いシーンなし) です: ダイアルに専用の AOD 背景フレーム (dial.aod) がない場合、通常シーンは AOD モードで非表示になり、黒 + 0x22 要素を独自の色でレンダリングします。AOD 針 もシーンウォーカーを介して解析されます (今では 0x22 コンテナを再帰して描画オブジェクトを とタグ付けし、ピボットが一致しなかったフラットスキャンに任せるのではなく → "位置なし" を防止)。AOD 針はキャンバス中心で回転します ( はファームウェアが無視する中心からずれた針の x/y を運ぶことがあります — 例: Gradient の時針 )。エディタはこれを (§UI) として公開します: 各画面は自身のレイヤーのみを表示し、編集は独立して保持されます。通常モードのレンダリングは全体でバイト同一、ラウンドトリップは 103 すべてのダイアルでバイト単位で正確を維持します。40 01 00 XX バイト (✅ ファームウェア確認済み)img_number が描画する桁数は、フィールドレコード内の 1 バイト です — 要素の 40 01 00 XX 属性サブレコードのデータバイト XX (61 [count][base][glyph-ids] フレームテーブルの後に位置する 0x40 サブレコード):
XX & 0x0F = 桁スロット数 (0 ⇒ ファームウェアデフォルト 7)。0x80 = ゼロパディング (先頭ゼロを表示、例 "09" vs "9")。ファームウェアの逆アセンブルにより確認 (XIP イメージ 0x10000000; レンダリングルーチン 0x100d8e60):
NDIG = ldrb[40sub+3] & 0x0F (0 の場合は 7); 値は value % 10^NDIG にクランプされ、正確に NDIG 個のグリフが MSB から描画され、bit7 でない限り先頭ゼロは抑制されます。ソースの後の u16 (日付は 60、kcal は 1000) は 桁数ではありません — 数千/百万の区切りグリフ挿入 (cmp #1000/#1000000) のみを提供しており、それを編集しても効果がなかった理由です。ソース ID にも上限がありません。
全 620 個の数字フィールドのコーパスヒストグラムと一致: 2 桁フィールド (時/分/秒/日付/気温/HR) は 40 01 00 02/0x82 で終了; kcal …04; steps …05; 1 桁の時計分割 0x81。したがって 日付フィールド 40 01 00 82 = 2 桁、ゼロパディング — これが、再バインドされた華氏気温 (≥100) が切り詰められた根本的な理由です。
修正/エディタ: wfweb は数値フィールドに対して digitCount/digitZeroPad (+digitCountOff) を解析し、インスペクタで "Digits" + "Zero-pad" を公開し、バイトを インプレース (同一フットプリント) で書き込み、プレビューはファームウェアを反映して digitCount にクランプ/パディングします。これにより、フィールドのソースを再バインドして桁数を設定することが 任意の フィールドで機能します (例: date→temperature °F → Digits 3)。通常モードのオラクルは変更なし (0 リグレッション、3 つの小さな改善); 103 すべてのダイアルでラウンドトリップ バイト単位で正確。
(以前の "Digits width"/rectW 仮説は誤りでした — 幅はレイアウトのみであり、桁数ではありません。)
転送テーブルは §6 にあります。追加で確認された点:
000000010000… で始まります。完全な init → [A05F ↔ 905F]×N → 完了ループがワイヤー上で観測されました (~892 チャンク)。9040–9042, finish 9041) 🔎: 構造マッピング済み; INIT2 ペイロード = バージョンバイト (例: 0b 00 00 39 = 11.0.0.57)。フィールドテスト未実施 (アプリはここで FW アップデートを無効にします)。ファームウェアイメージは 署名なし — 整合性は CRC32 のみ のように見えます (RE で非対称署名は確認されていません)。FACTORY_RESET (009A 0001) は認証済みセッションを共有するため、単一の有効な BLE 認証で時計を消去したり (原理的には) 文鎮化したりできます。注意して扱ってください。✅ BLE 経由で公開されるハードウェア:
WORKOUT_GPS) と位置情報プッシュ (GPS_PUSH)。気圧計/高度計、コンパス、ジャイロスコープ、皮膚/体温センサーはありません。内部 NTC サーミスタ (基板/バッテリー温度) は存在しますが、AT チャネル (AT GETNTCTEMP, §14) を介してのみ読み取り可能です — 0155 皮膚温度履歴ストリームはこの SKU では空です。
データウィジェットハイジャック (実際のコンプリケーション/データバインディング API はありません — §11.5 参照): 時計の既存のテキストフィールドを再利用して、一目でわかる外部データを表示できます。実証済み ✅: 天気の 都市文字列 (WEATHER_SET_1、例: "BRA 2x1 ARG" がウィジェットに表示) および音楽の トラック/アーティスト フィールド; 連絡先 リスト (20 × 名前[32]+番号[25]) はスクロール可能なデータパネルとして機能します。すべてプッシュであり、永続的なコンプリケーションではありません。
77d4ff01 / 77d4ff02)フレーム化プロトコルから独立した、別のプレーンテキスト AT コマンドチャネル。✅ ライブテスト済み:
AT GETSECRET (16 バイトペアリングシークレット), GETVERSION, GETSN, GETNAME, GETPID, GETBATLV (生 mV、例 3853mv), GETGSENSOR (生加速度 g 単位、X=… Y=… Z=…), GETNTCTEMP (°C、内部 NTC)。AT SETMOTOR=1 (モーター振動), SETHR/SETHRV/SETSPO2=… (センサーテスト注入), SETLCDSWITCH/SETGPSSWITCH/SETKEYSWITCH。応答は ,OK で終わります。SET* コマンドは通常実行されますが、BLE 経由で ,OK をエコーしない場合があります — ケースバイケースで確認してください。
一部の機能はファームウェアに存在しますが、SKU/地域によって無効化されており、電話/BLE からはアクセスできません — ファームウェアの改造が必要ですが、ここでは範囲外です:
ux2sys フィーチャー ID 0x9e、起動時に NVRAM/EFUSE/地域からシード; この SKU ではサポートフラグ 908b = 00。電話、アカウント、BLE では影響不可 (実験 + RE で確認)。アプリは中継のみ; 音声は電話 → Nothing クラウドへ。BLE クライアントを構築するために必要ではありません — 完全性のために含めています。ファームウェアのダイアルレンダラーは、各コンプリケーションスロットを数値の ゲッター ID (142 エントリのディスパッチテーブル) にバインドします。選択された ID:
0x07 時、0x0a 複合時計角度、0x0b 分、0x0f 秒、0x18 曜日、
0x19 心拍数、0x1b バッテリー %、0x24 気温、0x36 歩数、
0x70/0x71/0x72 時/分/秒の針角度、0x25–0x27 目標 %。リング/アークのコンプリケーションは、事前レンダリングされた フレームシート (例: 50% = 100 フレーム中のフレーム 50) をインデックスし、ピクセル単位のアークではありません — フレームは送信する .bin に焼き付けられているため (§11.5)、外部 RES パックは不要です。
QUICK_CARD (906D) ✅時計のホームタイル。電話はどのタイルを表示するかとどの順序で表示するかのみ選択します — タイルはファームウェアによってレンダリングされます (コンテンツチャネルなし)。最初のペイロードバイト = サブコマンド: 00 = GET, 01 = SET; 両方とも 0x906D を使用 (0x906C はリストされていますが使用されていません — クエリするとタイムアウトします)。応答 = A06D。
🛑 でっちあげの
assemblyIdを送信すると、時計の画面が消去されます (リストを受け入れますが、ID を一致できず、何も表示されません)。GET で読み戻した ID のみを送信してください; 公式アプリまたはファクトリーリセットで復元してください。
GET 応答 ✅: status(1) ‖ 00 ‖ N(1) ‖ N × group, group = tag=01 ‖ K(1) ‖ K×(assemblyId, sportId)。
実際のフレーム: 01 00 04 01 02 5d00 6100 01 03 1900 2e00 2300 01 03 5c00 0400 5a02 01 03 4800 5100 5300
= 4 画面 / 11 カード (5a02 = Sport card, sportId 2)。
スロット: 各画面には 4 スロット があります。カードのタイプによってサイズが決まります — circular/square = 1 スロット、rectangle = 2 スロット。検証は純粋なスロット演算です (画面あたり Σ ≤ 4); 相互に排他的なカードはありません。sportId は Sport カード (87–91) を除き 0 です。ID 64 と 95 は存在しません。
assemblyId カタログ (各論理タイプ = 6 つのスタイルバリアント _0.._5 の連続範囲; 0 = 空スロット):
上記のバイトレイアウトは、ファームウェア (1.0.0.73)、公式 APK (3.5.7)、および実機に対する復号化されたライブキャプチャから再構築されました。このプロジェクトのリファレンス実装は、core-rust/src/{commands,frame,crypto,health,session}.rs (Rust) および cmftool/ Python ツール (pair.py, session.py, wf_codec.py, upload_custom.py, …) にあります。
| 目的 | サービス | 特性(Characteristic) | プロパティ |
|---|
| コマンド 書き込み | 0000fff0-0000-1000-8000-00805f9b34fb | 0000fff2-… | Write |
| コマンド 通知 | 0000fff0-… | 0000fff1-… | Notify |
| シェル 書き込み (AT) | — | 77d4ff01-2fe2-2334-0d35-9ccd078f529c | Write |
| シェル 通知 (AT) | — | 77d4ff02-… | Notify |
| 一括 データ書き込み | — | 02f00000-0000-0000-0000-00000000ffe1 | Write |
| 一括 データ通知 | — | 02f00000-…ffe2 | Notify |
| 名前 | cmd1,cmd2 |
|---|
| TIME | FFFF 8004 |
| FIRMWARE_VERSION_GET / _RET | FFFF 8006 / FFFF 0006 |
| SERIAL_NUMBER_GET / _RET | 00DE 0002 / 00DE 0001 |
| BATTERY | 005C 0001 |
| TRIGGER_SYNC | 005C 0002 |
| USER_INFO_SET / _RET 🔎✅ | 0095 0001 / 0095 0003 |
| FACTORY_RESET | 009A 0001 |
| DEVICE_REBOOT 🔎 | FFFF 9080 |
| RESOLUTION_GET 🔎(→ 466×360) | FFFF 907F |
| GPS_PUSH / _RET | FFFF 906A / FFFF A06A |
| UNBIND_SET / _RET | FFFF 907A / FFFF A07A |
| 名前 | cmd1,cmd2 |
|---|
| AUTH_PHONE_NAME | FFFF 8049 |
| AUTH_WATCH_MAC | FFFF 0049 |
| AUTH_PAIR_REQUEST / _REPLY | FFFF 8047 / FFFF 0048 |
| AUTH_NONCE_REQUEST / _REPLY | FFFF 804B / FFFF 004C |
| AUTHENTICATED_CONFIRM_REQUEST / _REPLY | FFFF 804D / FFFF 0004 |
| AUTH_FAILED | FFFF A061 |
| 名前 | cmd1,cmd2 |
|---|
| APP_NOTIFICATION | 0065 0001 |
| INCOMING_CALL ⚠️ | 0064 0001 |
| CALL_REMINDER_REQUEST / _RESPONSE | FFFF 9066 / FFFF A066 |
| FIND_PHONE | 005B 0001 |
| FIND_WATCH | 005D 0001 |
| FIND_WATCH_TOGGLE | FFFF 9069 |
| SMS_MESSAGE_PUSH / _RET | FFFF 906E / FFFF A06E |
| QUICK_REPLY_SET / _RET | FFFF 9073 / FFFF A073 |
| 名前 | cmd1,cmd2 |
|---|
| ALARMS_SET / _GET | 0063 0001 / 0063 0002 |
| CONTACTS_SET / _GET | 00D5 0001 / 00D5 0002 |
| STANDING_REMINDER_SET / _GET | 0060 0001 / 0060 0002 |
| WATER_REMINDER_SET / _GET | 0061 0001 / 0061 0002 |
| TASK_REMINDER_SET / _RET ⚠️ | FFFF 9072 / FFFF A072 |
| 名前 | cmd1,cmd2 |
|---|
| GOALS_SET / _ACK | 005E 0001 / 005E 0003 |
| UNIT_LENGTH / _ACK | FFFF 9067 / FFFF A067 |
| UNIT_TEMPERATURE / _ACK | FFFF 9068 / FFFF A068 |
| TIME_FORMAT / _ACK | 005F 0001 / 005F 0003 |
| WAKE_ON_WRIST_RAISE / _GET / _ACK | 0062 0001 / 0062 0002 / 0062 0003 |
| LANGUAGE_SET / _RET | FFFF 9058 / FFFF A06B |
| HEART_MONITORING_ENABLED_SET / _GET | 009B 0001 / 009B 0002 |
| HEART_MONITORING_ALERTS | FFFF 9059 |
| DO_NOT_DISTURB / _GET | 0099 0001 / 0099 0002 |
| SPORTS_SET / _GET | 00DC 0001 / 00DC 0002 |
| SPORT_LINKAGE_SET / _RET | FFFF 9076 / FFFF A076 |
| SPORT_DATA_SYNC 🔎(ライブ HR/カロリー/歩数) | FFFF 9078 / FFFF A078 |
| FEMALE_CYCLE_SET / _RET | FFFF 9071 / FFFF A071 |
| SLEEP_CONFIG_SET / _RET(目標分数) | FFFF 9074 / FFFF A074 |
| WORLD_CLOCK_GET | FFFF 906F |
| WORLD_CLOCK_DST_SET / _RET | FFFF 9083 / FFFF A083 |
| VITALITY_GET / _RET | FFFF 9079 / FFFF A079 |
| VITALITY_SW_SET / _RET | FFFF 9070 / FFFF A070 |
| 名前 | cmd1,cmd2 |
|---|
| DIAL_COMMAND_SET / _RET(一覧表示/並び替え/選択) | FFFF 9055 / FFFF A055 |
| DIAL_CONFIG_SET / _RET | FFFF 9075 / FFFF A075 |
| CHANGE_DIAL(⚠️ 1.0.0.73 では無効 — 使用しないでください) | 009F 0001 |
| QUICK_CARD_SET/GET / _RET(両方とも 906D) | FFFF 906D / FFFF A06D |
| 名前 | cmd1,cmd2 |
|---|
| ACTIVITY_FETCH_1 / _2 | FFFF 8005 / FFFF 9057 |
| ACTIVITY_FETCH_ACK_1 / _2 | FFFF 0005 / FFFF A057 |
| ACTIVITY_DATA | 0056 0001 |
| SLEEP_DATA / _GET | 0058 0001 / 0058 0002 |
| SPO2 | 0055 0001 |
| STRESS | 009D 0001 |
| HEART_RATE_MANUAL_AUTO | 0053 0001 |
| HEART_RATE_RESTING | 00DA 0001 |
| HEART_RATE_WORKOUT | 00E0 0001 |
| SKIN_TEMP_HISTORY 🔎(この SKU では空) | 0155 0001 / 0155 0002 |
| WORKOUT_SUMMARY / _V3 | 0057 0001 / 0160 0001 |
| WORKOUT_GPS | FFFF A05A |
| ドメイン | INIT1 リクエスト/応答 | INIT2 リクエスト/応答 | CHUNK リクエスト/書き込み | FINISH ack1/ack2 |
|---|
| ウォッチフェイス(写真) | 8052/0052 | 9063/A063 | A064/9064 | A065/9065 |
| ウォッチフェイス(構造化/切り替え) | 8052/0052 | 9075/A075 | A064/9064 | A065/9065 |
| ファームウェア | 9052/A052 | 9040/A040 | A042/9042 | A041/9041 |
| AGPS/EPO | 905E/A05E | — | A05F/905F | A060/9060 |
| オフセット | サイズ | フィールド |
|---|
| 0 | 4 | タイムスタンプ(エポック秒) |
| 4 | 4 | 歩数 |
| 8 | 4 | 距離(m) |
| 12 | 4 | カロリー |
| 16 | 16 | 予約済み(観測値 0) |
| オフセット | サイズ | フィールド |
|---|
| 0 | 4 | session_start(エポック、UTC) |
| 4 | 4 | wakeup(エポック、UTC) |
| 8 | 2 | total_deep_s |
| 10 | 2 | total_core_s |
| 12 | 2 | total_rem_s |
| 14 | 2 | total_awake_s |
| 16 | 2 | ⚠️ [未確定](セッション ID/スコア?観測値はレコードの合計と一致しません) |
ac 49 1f 0100D5 0001)✅:N × 57 バイト = name(32) ‖ phone(25)。ウォッチ UI は最大 20 件表示。0063 0001)✅ — Gadgetbridge を修正(Gadgetbridge はラベルを末尾に 0xff パディングで配置 — 誤り)。1 アラームあたり 40 バイト、ビッグエンディアン:
secondsOfDay(i32) ‖ index(u8) ‖ enabled(u8) ‖ repetition-bitmask(u8) ‖ flag(u8) ‖ label[32] UTF-8。
ラベルはオフセット 8 にあり、ウォッチに表示されます。repetition = 曜日ビットマスク(0 = 1 回限り);
flag は ⚠️ [未確定](1 回限りマーカー?)。例(13:30、インデックス 2):0000bdd8 02 01 15 00 "Alarm…"。005E 0001)✅ — 公式アプリとリファレンス実装は 10 バイト、ビッグエンディアン の DailyTargetBean v1 を使用します:steps(u32 BE) ‖ distance_m(u32 BE) ‖ calories_kcal(u16 BE)。(これは Gadgetbridge の形式です。ウォッチが「無視した」という以前の報告は、古いセッションの復号バグであり、ペイロードの問題ではありません。)🔎 ファームウェア RE では、より長い 29 バイトの拡張 バリアントも示されています(sleep_min/exercise_min/stand_h + 6 つの有効フラグを追加。すべて flag(u16 LE) プレフィックスの後に u32 BE、範囲が適用されます:steps 2000–30000、dist 1000–99000、cal 100–5000、sleep 360–720、exercise 30–90、stand 6–16) — これはアプリのデフォルトパスではありません。追加のターゲットが必要ない限り、10 バイト形式を推奨します。0060/0061 0001)✅:11 バイト:
enabled(1) ‖ threshold_min(u16 LE) ‖ dndStart(u32 LE) ‖ dndEnd(u32 LE)。UI に表示される「アクティブウィンドウ
08:00–22:00」は 固定のファームウェアデフォルト であり、ペイロードには 含まれません。00DC 0001)✅:count(1) = 36 スロット ‖ activityTypeCode[36](アクティブコードの後、00 パディング)。
ウォッチのワークアウトメニューに表示するスポーツを選択します。009B 0001)✅:kind バイト — 01 = 24/7 HR、02 = SpO₂、
04 = ストレス(30 分ごとに測定)。FFFF 9059)✅:無効 = 00;有効 =
01 ‖ hrLow ‖ hrHigh ‖ sportHrHigh ‖ spo2Low ‖ 00 00 00 00(0/255 の境界値 = 「制限なし」)。FFFF 9071)✅:01 ‖ predictionOpen ‖ notifySwitch ‖ cycleStartSwitch ‖ cycleStartNotifyBefore ‖ ovulationStartSwitch ‖ ovulationStartNotifyBefore ‖ fertileStartSwitch ‖ fertileStartNotifyBefore ‖ period(1) ‖ cyclePeriod(1) ‖ cycleStartDate(u32) ‖ markStart(u32) ‖ markEnd(u32)(キャプチャ:period=5、cyclePeriod=0x1c=28)。FFFF 9073)✅:TLV — count(1) ‖ total(1) ‖ [id(1) ‖ len(u16 LE) ‖ msg-UTF8]…
(7 つのデフォルト返信がキャプチャおよび復号されました)。FFFF 906F)✅:名前ではなく数値の 市区町村 ID を送信します(01 ‖ count ‖ cityId(2 BE)…);
ウォッチは内部テーブルから ID をマッピングします。DST 設定 FFFF 9083 =
count ‖ [id(u16 LE) ‖ dst(u16 LE) ‖ start(u32 LE) ‖ end(u32 LE)]…。FFFF 905C、131 B)✅:state(1: 0=なし/1=一時停止/2=再生中) ‖ volume(1) ‖ volumeMax(1) ‖ track(64) ‖ artist(64)。ウォッチは MUSIC_BUTTON (A05D) も送信します。FFFF 906B、199 B)✅ — こちらを使用してください:7×9 バイトの日 + 24×2 バイトの時間 +
市区町村(32) + 7×8 バイトの日の出/日の入り(LE)。気温は (temp_c + 100) & 0xFF としてエンコードされます。
⚠️ 同じペイロードを WEATHER_SET_2(0066 0001)で送信しても、Pro 2 の天気ウィジェットは 更新されません — 常に 906B を使用してください。(市区町村文字列も、データハイジャックの実証済みベクターです — §13 参照。)005D 0001)✅:ペイロード 0x01 → ウォッチがリンク/バイブレーション(+ ACK 005D 0003)。FFFF 906A)✅ — ビッグエンディアン、経度が先:16 バイト
ts(u32 BE) ‖ lon×1e7(i32 BE) ‖ lat×1e7(i32 BE) ‖ 00 00。実際の位置で検証済み。FFFF A05A)✅ — リトルエンディアン、経度が先:12 バイト
ts(i32) ‖ lon×1e7(i32) ‖ lat×1e7(i32)。FFFF 8004):§7 を参照。| cf | bpp | raster (after LZ4) | use |
|---|
| 4 | 2 | RGB565-LE | 不透明背景 (FULL/THUMB) |
| 5 | 3 | RGB565-LE (2 B) + alpha (1 B) per px | アンチエイリアススプライト (グリフ、手、アイコン) |
| 13 (0x0d) | 0.5 | 4-bit alpha mask; firmware tints at runtime | 数字グリフアトラス |
| 24 (0x18) | 4 | RGBA8888 | フルカラーレイヤー (常時表示の aodImage を含む) |
| 1 | — | JPEG/JFIF (ff d8 ff), extract with any decoder | 稀なアニメーションフレーム |
0x61NotEnvelope0aChildOverflow61 0a 00−18/−160a.bin 内にありません。 ⚠️ 設定可能なコンプリケーションは、同じ (x,y) に積み重ねられた N 個の 0x68 グループノード として作成され、それぞれ異なるソースにバインドされます (275 の 2 つの円: スロットごとに 0x1e/0x6a/0x48/0x24/0x19 — 表示されるメトリックではなく オプション/スタイル ID)。2 つの円は、矩形とインスタンスバイト (0x79/0x7a) を除いてバイト単位で同一です。どのメトリックが表示されるか (STEPS vs KCAL vs …) は デバイス RAM/設定状態 であるため、静的プレビューではファイルから再現できません — ベストエフォートのみ。(446,0) の bpm テキスト、275/302/325/365/375 で確認) は、ファームウェアがデフォルトビューで描画しないスロットです — それらの値はキャンバスエッジにさえ収まりません。プレビューでは非表示として扱います。aod0x22@69,2090x60 img_number (cnt=10) — ソースは −5 で、前方に 1 つずれ。 ✅ §11.8 と同じ 1 つずれですが、時計以外の数字に対するものです: "Gradient" の日付は (203,80) の上部中央にソース 0x17 で配置されていましたが、前方 82 スキャンが隣接する ポインタの 角度ゲッター (0x0a) とポインタの位置を取得し、数字が偽のソースでポインタの場所にレンダリングされました。修正: 0x60 ラッパー内の 61 0a 00 img_number の場合、前方ソースが 数字として不可能な場合 (source-0 またはポインタ角度ゲッター 0x0a/0e/12/70/71/72) かつ −18/−16 の位置が有効で非ゼロの場合 (非ゼロガードは relX=0 のグループ子数字をスキップします)、−5/−18/−16 を信頼します。0x17 = 日付 (月の日), 0x24 = 気温 — 区別されます。 ダイアル 340 は 両方 (0x17 "Jun 09" と別の 0x24 気温) を使用しているため、0x17 は日付であり、気温ではありません。時計が 0x17 スロットに気温を表示するダイアルは、ユーザー設定のコンプリケーション (デバイス状態) であり、ファイルのデフォルトではありません。| dec | card | dec | card |
|---|
| 0 | 空スロット | 49–53,97–98 | 天気 |
| 1–6 | 歩数 | 54–58 | タイマー |
| 7–12 | カロリー | 59–62 | 呼吸 |
| 13–18 | スタンド | 63,65–67 | ストップウォッチ |
| 19–24 | 中程度のアクティビティ | 68–71 | バッテリー |
| 25–30 | 心拍数 | 72–76 | 最近 |
| 31–36 | SpO₂ | 77–81 | 連絡先 |
| 37–42 | ストレス | 82–86 | ダイアル/電話 |
| 43–48 | 睡眠 | 87–91 | スポーツ (sportId ≠ 0) |
| 92 | ミュージック | 93/94/96 | アクティビティ記録 / PAI / サイクル |