Skip to content
KitploitKITPLOIT
ツールブログ
提出
ツールブログ
提出

ハッキング、侵入テスト、サイバーセキュリティツールをあなたのセキュリティアーセナルに!

Kitploitはハッキング、サイバーセキュリティ、ペネトレーションテストのツールディレクトリです。最新のプロジェクトアップデートを見つけて、脆弱性の発見、システム分析、テストの自動化、セキュリティの強化を行いましょう。

··フィード·お問い合わせ·プライバシー·© 2026 Kitploit

ツールディレクトリ

カテゴリ

すべてのカテゴリを見る
Loading categories
CMF-Watch-Pro-2-BLE-Protocol — CMF Watch Pro 2向けのリバースエンジニアリングされたBLEプロトコル。GATTレイアウト、AES-128-CBC暗号化コマンドフレーム、認証ハンドシェイク、および代替コンパニオンアプリ開発のためのヘルスデータ同期を文書化しています。 | Kitploit
ツール/GitHubGitHub/joshuapassos/cmf-watch-pro-2-ble-protocol
組み込みシステムセキュリティBluetoothセキュリティIoTセキュリティリバースエンジニアリングワイヤレスセキュリティ暗号化モバイルセキュリティハードウェアとIoTセキュリティファームウェア解析

人気

すべて見る →

コミュニティで最も使われているツールを見つけましょう。

すべてのツールを探索

ツールコレクションを閲覧

すべてのツールを見る →
共有
GitHubjoshuapassos/cmf-watch-pro-2-ble-protocol

CMF-Watch-Pro-2-BLE-Protocol

CMF Watch Pro 2向けのリバースエンジニアリングされたBLEプロトコル。GATTレイアウト、AES-128-CBC暗号化コマンドフレーム、認証ハンドシェイク、および代替コンパニオンアプリ開発のためのヘルスデータ同期を文書化しています。

リポジトリを見るウェブサイト
324日前未レビュー

CMF Watch Pro 2 — BLEプロトコル(リバースエンジニアリング)

非公式. このドキュメントは、CMF Watch Pro 2(CMF by Nothing)のBluetooth Low Energy(BLE)プロトコルを、代替コンパニオンアプリ向けにリバースエンジニアリングによって再構築したものです。Nothing/CMFとは提携しておらず、承認も受けていません。自己責任で使用してください。

フレームヘッダとオペコード内のマルチバイト整数はビッグエンディアンです。コマンドペイロード内部の整数は、特に明記されていない限りリトルエンディアンです(これはデバイスファームウェアを反映しています)— 例外(GOALS_SET、GPS_PUSH、一括転送のオフセット/長さはビッグエンディアン)に注意してください。

確信度マーカー

以下に記載された自明でない主張にはすべて、それがどのように確立されたかを示すタグが付けられています。

  • ✅ デバイス上で検証済み — 復号化されたライブキャプチャで観測されたか、実際の時計に対して実行されました。
  • 🔎 ファームウェア/APKのリバースエンジニアリングから — ファームウェア(1.0.0.73)または公式APK(3.5.7)を逆コンパイルして抽出したもの。コードと一致しますが、ランタイムテストはされていません。
  • ⚠️ [不確か] — 推測に基づくもので、確認されていません。誤っている可能性があります。

すべてのキャプチャのテストデバイス: CMF Watch Pro 2-5485、fw 1.0.0.73、シリアル CI04102520008192、MCU Actions ATS3089C(Cortex-M4)、画面 466×360。


1. GATTレイアウト

電話がGATTクライアント、時計がペリフェラルであり、CMF Watch Pro 2-XXXX(4桁の16進数)としてアドバタイズします。

各CCCD(00002902-…)に01 00を書き込むことで通知を有効にします。コマンドチャネル(fff1/fff2)は以下のフレーム化されたプロトコルを運びます。シェルチャネル(77d4…)はプレーンなAT形式のテキスト(例: AT GETSECRET。§14参照)を運びます。データチャネル(02f0…)は大きなバイナリブロブ(ウォッチフェイス、ファームウェア、AGPS)を運び、コマンドチャネル上の制御オペコードによって調整されます。

✅ 実際のセッション全体が単一のコマンドチャネル上で実行されました — 160秒間の高負荷キャプチャ中、明示的なOTA/ウォッチフェイス転送を除いて、データ/ファームウェアチャネルやシェルチャネルにトラフィックはありませんでした。


2. フレーム形式(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 ____________________________/

root@kitploit:~
- `cmd1`/`cmd2` は合わせて **オペコード** を構成します (§6 参照)。🔎 公式アプリのフレームビルダー (`C6117b.m30831g`) で確認済み。
- `chunkCount` = このコマンドの総チャンク数、`chunkIndex` は **1 ベース**です。
- `chunkLen` = このフレーム内の `chunk` のバイト数。
- 単一の BLE 書き込みはリンク MTU によって断片化される可能性があります。受信側は生のバイトをバッファリングし、完全なフレームを再抽出します。大きなペイロードは複数のチャンク(同じ `cmd1/cmd2`、増加する `chunkIndex`)に分割され、順番に再アセンブルされます。

### オペコードの慣例 (✅ ワイヤ上で確認済み)

- `cmd1 = 0xFFFF`: `cmd2` が `0x80xx`/`0x90xx` の範囲 = スマートフォン→ウォッチ (リクエスト/設定); `0x00xx`/`0xa0xx` の範囲 = ウォッチ→スマートフォン (応答)。ペアは下位バイトで一致します (`0x9055`↔`0xa055`、`0x8051`↔`0x0051`)。
- 機能固有の `cmd1`: `cmd2` のサフィックス = `0x0001` **SET**、`0x0002` **GET**、`0x0003` **ACK**。

### チャンク本体

各チャンクについて、本体は `payloadPiece ‖ CRC32_LE(payloadPiece)` です (4 バイト CRC、リトルエンディアン、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 はライブフレームを復号します。


4. 認証 / ペアリングハンドシェイク

2つのエントリパスは同じノンス/確認テールを共有します。

4.1 初回ペアリング(デバイスシークレットを保持)```

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

root@kitploit:~
`AUTH_FAILED (0xFFFF,0xA061)` または署名ミスの場合、認証は失敗します。

### 4.2 再接続 (authkey 既知)```
        set crypto key = authkey (persisted)
phone → AUTH_PHONE_NAME      (encrypted)  payload = 0xA5 ‖ model
watch → AUTH_WATCH_MAC       (encrypted)
phone → AUTH_NONCE_REQUEST   (encrypted)  payload = 0xA5
watch → AUTH_NONCE_REPLY     (encrypted)  payload = nonce
        sessionKey = SHA256(nonce ‖ authkey)[0..16]   → set crypto key = sessionKey
phone → AUTHENTICATED_CONFIRM_REQUEST (encrypted)  payload = 0xA5
watch → AUTHENTICATED_CONFIRM_REPLY   (encrypted)  → Initialized

✅ 再接続順序(シェルトラフィックなし)は実際のキャプチャでそのまま観測されました。

4.3 認証後の初期化(フェーズ 2)

⚠️→✅ データクエリの前に TIME が必須です。 Initialized 後、ウォッチは BATTERY、SERIAL_NUMBER_GET、または ACTIVITY_FETCH_* のハンドシェイクに応答しません 。 セッション内で TIME (FFFF 8004) が送信されるまで、未承諾の FIRMWARE_VERSION_RET のみが到着し、それ以外はすべてタイムアウトします。 ✅ ライブ確認済み(Pixel 8a): TIME なしで 3 つの GET を送信 → ファームウェアのみ応答。 TIME を先に送信 → バッテリー および シリアルが応答を開始します。

推奨されるフェーズ 2 の順序:TIME → FIRMWARE_VERSION_GET → SERIAL_NUMBER_GET → BATTERY (0xA5) → 設定プッシュ → ヘルス同期(§8)。

4.4 GET → SET エコーパターン(✅)

ほとんどの設定に個別の「読み取り」オペコードはありません。 *_GET(cmd2 = 0x0002、ペイロード 0xA5)を送信すると、ウォッチは現在の値を含む SET オペコード(cmd2 = 0x0001)で応答します。 SET コマンドは cmd2 = 0x0003 と空のボディで確認応答されます。


5. 平文と暗号化

キーが設定されるとフレームは AES 暗号化されますが、以下のオペコードは常に平文です。

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

フレーム ヘッダー(cmd1/cmd2)は常にクリアで送信されるため、キーがなくてもコマンド シーケンス は キャプチャで可視です。暗号化が必要なのは ペイロード のみで、sessionKey が必要です。


6. オペコードリファレンス (cmd1, cmd2)

GET/SET/REQUEST = 電話→ウォッチ;RET/REPLY/ACK/RESPONSE/DATA = ウォッチ→電話。

セッション / デバイス

認証

通知 / 電話 / 探す

音楽

名前cmd1,cmd2
MUSIC_INFO_SET / _ACKFFFF 905C / FFFF A05C
MUSIC_BUTTONFFFF A05D

アラーム / 連絡先 / リマインダー

設定

天気

名前cmd1,cmd2
WEATHER_SET_1(動作する方)FFFF 906B
WEATHER_SET_2(Pro 2 では無視 — §9 参照)0066 0001

ウォッチフェイス / ダイヤル

ヘルス / 同期

JS のみのオペコード(FFFF 8051、FFFF 0051、FFFF 90A2、FFFF 90C5、FFFF A056、 FFFF 908A/908B ChatGPT ステータス/サポート)は、Java 層ではなくアプリの Hermes バイトコードで 処理されます。これらのヘッダーはキャプチャに表示されますが、ペイロードのセマンティクスは ⚠️ [未確定] です。

一括データ転送(データチャネル)

ウォッチフェイス / ファームウェア / AGPS は、初期化 → チャンクリクエスト/チャンク書き込みループ → 終了確認応答を使用します。

(すべて cmd1 = FFFF。)ウォッチは DATA_CHUNK_REQUEST_*(offset, length)(offset/length = u32 ビッグエンディアン)を発行してループを駆動します。 電話は、データキャラクタリスティック上で payload[offset..offset+length] を運ぶ DATA_CHUNK_WRITE_* で応答します。詳細は §11–§12 を参照してください。


7. 時刻とタイムゾーン

TIME (FFFF 8004) のペイロード = epochSeconds(i32, BE) ‖ utcOffsetMillis(i32, BE)。認証直後に送信されるため、ウォッチに現地時間が表示され(データクエリのブロックも解除 — §4.3 参照)。

⚠️ ウォッチからのヘルスタイムスタンプは UTC です。 コンパニオンアプリは、現地のカレンダー日付/時刻を導出する前に、ローカルの UTC オフセットを追加する必要があります。(生の UTC 日でヘルスをバケット化すると、間違った現地時間で日付が切り替わります。)

TIME_FORMAT (005F 0001) のペイロード = 1 バイト:00 = 24 時間制、01 = 12 時間制。


8. ヘルス同期

  1. 電話が ACTIVITY_FETCH_1 を送信。ウォッチが ACTIVITY_FETCH_ACK_1 で応答(最初のバイト 01 ⇒ 準備完了)。
  2. 電話が ACTIVITY_FETCH_2 を送信。その後、ウォッチがデータフレームのバーストをプッシュ: ACTIVITY_DATA、HEART_RATE_*、SPO2、STRESS、SLEEP_DATA、WORKOUT_SUMMARY[_V3]。
  3. 各データは分単位のサンプル/セッションに解析され、ローカルの 日付で集計されます。

同期は逐次的です(TIME に従う必要があり、ウォッチは ACK_2 後にストリームを解放します)。単一のバーストではありません。大量のセッションでは、約 160 秒で約 170~210 の通知フレームがプッシュされます。✅

8.1 アクティビティレコード — ACTIVITY_DATA(各 32 バイト、LE)✅

カロリー単位: アクティビティカロリーは cal(グラムカロリー)で報告されます。1 日の合計を 1000 で割って kcal にします。(対照的に、ワークアウトサマリーのカロリーはすでに kcal です。)

8.2 HR / SpO₂ / ストレスサンプル ✅

  • 手動/自動 HR、ワークアウト HR、SpO₂、ストレス = 各 8 バイト:timestamp(i32 LE) ‖ value(i32 LE) (値 = bpm / SpO₂ % / ストレス指数)。
  • 安静時 HR(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。

8.3 睡眠 — 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 の合計が一致)で検証済み。

8.4 ワークアウトサマリー — WORKOUT_SUMMARY v1(54 バイト)/ _V3(0160 0001)

v1:start(u32)、end(u32)、duration_s(u32)、次にタイプ/カロリー/歩数/距離/平均 HR、および GPS/拡張ブロック。✅ v1 レイアウトはファームウェアに対して確認済み。WORKOUT_SUMMARY_V3 は同じデータ用の新しいレイアウトで、約 40 バイトの拡張ブロック(exerciseLoad、有酸素/無酸素、recoveryTime、 VO₂max、ケイデンス、PAI、ベストラップタイムなど)が追加されています。フィールドセットは(アプリの Room DB から)既知ですが、その 40 バイトブロック内の 正確なバイトオフセットは ⚠️ [未確定] です。 — これらを確定するには、GPS ワークアウトの生キャプチャが 1 つ必要です。


9. 選択されたコマンドペイロード

文字列は UTF-8 で、フィールドサイズに バイト単位で切り詰められます(切り詰めによりマルチバイト文字が分割される場合があります。これはファームウェアの s.encode()[:max] の動作と一致します)。短いフィールドは右側にゼロ埋めされます。

  • APP_NOTIFICATION(0065 0001)✅:iconCode(1) ‖ 0x00 ‖ when(u32 BE) ‖ titleLen(1) ‖ title ‖ body。 iconCode はアプリアイコンを選択します(WhatsApp=8、Telegram=12、Instagram=18、Gmail=27;不明=0xFF)。 タイトル ≤ 20 バイト、body ≤ 128 バイト。クライアントから送信 → ウォッチに表示され ACK 0065 0003。
  • BATTERY(005C 0001)✅:応答 = level(1) ‖ charging(1)(例:3b 00 = 59 %、充電中ではない)。
  • SERIAL_NUMBER_RET(00DE 0001)✅:len(1) ‖ ASCII(例:10 + "CI04102520008192")。
  • USER_INFO(0095 0001)✅:height_cm(1) ‖ weight_kg(1) ‖ age(1) ‖ gender(1: 1=男性) (例: = 172 cm / 73 kg / 31 / 男性)。

10. 実装上の注意と癖

  • コーデックにシステムクロックなし: エンコーダは now/utc_offset を明示的なパラメータとして受け取ります(決定論的、テスト可能)。トランスポートが実際の時刻を提供します。
  • TIME がすべてをゲートする(§4.3) — 最初に送信しないと、ウォッチはデータクエリに応答しません。
  • 平文 CRC カウント(§2)は間違えやすい — 平文フレームは CRC を通知しますが、省略します。
  • エンディアン: ヘッダー + オペコードは BE;ペイロード整数は LE;例外 — GOALS_SET と GPS_PUSH はビッグエンディアン、一括転送のオフセット/長さはビッグエンディアンです。
  • MTU: 暗号化チャンクが 16 バイトの AES ブロックに揃うようにチャンクサイズが計算されます。
  • authkey は永続化可能(初回ペアリング後に保存);sessionKey は接続ごと で、再接続のたびにウォッチのノンスから導出されます。

11. ウォッチフェイス / ダイヤル — 制作

ウォッチは(a)写真/カスタムダイヤル(背景画像 + ファームウェア描画のデジタル時計)と(b)構造化ダイヤル(内蔵/ストアのフェイス:背景 + 位置指定のスプライトレイヤー、針、テキストウィジェット)をサポートしています。両方とも、§6 の init → チャンクループを介してデータチャネル上で転送されます。

実際に動作するもの(✅ ライブで検証済み): 任意の画像から フォトダイヤルを構築 してインストールする。103 個のストアダイヤル をオフラインでインストールする。構造化ダイヤルを リキン する(背景または非背景スプライトを交換)およびそのレイヤーを 移動 する。アクティブフェイスの 並び替え/切り替え。および スクラッチから構造化ダイヤルを構築する — 0x20 シーンエンベロープはデコードされ、ビルダーが実装されています(§11.7)。オフライン で 103 個すべてのストアダイヤルをバイト単位でラウンドトリップし、ファームウェア自身のバリデータを通過する合成コンテナを出力することが証明されています。🟡 唯一未証明のステップ は、9075 を介して デバイス上 でスクラッチからの合成レンダリングを確認することです(構造的なオフライン証明は、以前に 0a 拒否の原因となったものをすでにカバーしています)。コーデックやトランスポートの障壁はなく、ベンダーツールチェーンも必要ありません。 「構造化レンダリングは RES パックに焼き付けられている / BLE では不可能」および「cf=0x1f サーバーサイドコーデック」という古い主張は 誤りでした(オフセット+バイト/ピクセルのバグ) — ファームウェアは構造化ダイヤルを 送信したファイルからデータ駆動でレンダリングします。

11.1 ダイヤル管理 — DIAL_COMMAND (9055 / a055) ✅

  • type 0 = 一覧をクエリ。応答 a055 = result(u8) ‖ selectIndex(u8) ‖ total(u8) ‖ max(u8) ‖ N × dialId(u32 LE) ‖ ffffffff。例:01 05 06 07 … = アクティブ #5、6 ダイヤル、最大 7。
  • type 1 = 並び替え / アクティブ選択:目的のダイヤルをインデックス 0 にしてリスト全体を再送信 (これが公式アプリがフェイスを切り替える方法です。専用の「アクティブ設定」オペコードはありません)。
  • ダイヤルの削除 = その ID を 含めずに リストを再送信。
  • CHANGE_DIAL (009F 0001) は fw 1.0.0.73 では無効(定数を返し、切り替えない) — 使用しないでください。

11.2 転送フロー ✅```

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

root@kitploit:~
完了応答バイト: `01` = 有効化済み & 保存済み; `0a` = 保存されたが **有効化されていない** / 拒否された。Androidでは、各 `DATA_CHUNK_WRITE` は **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

root@kitploit:~
`size` = 正確な`.bin`の長さ; `FFFFFFFF` = カスタム`watchfaceId`; `styleId` 0~4は内蔵デジタル時計レイアウトを選択します(常に描画され、「オフ」はありません); `posX/posY`で位置指定(実績のある値 56/77); `color565`で色を付けます(例:`FFFF`=白)。⚠️ 短い`A5 ‖ size ‖ watchfaceId`形式は、完了コード`0a`で**拒否**されます — 上記の完全なヘッダーを使用してください。(参考実装:`core-rust/engine.rs::build_wf_init2`、公式アプリの`C6135t.m31104u`をミラーリング)

**手順:** 画像を466×466(および270×270のサムネイル)にリサイズし、RGB565-LEにトップダウンで変換し、必要に応じて円外のピクセルをゼロにし、それぞれをLZ4圧縮し、上記のコンテナを組み立て、`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)

root@kitploit:~
`kind` = `0x02`/`0x03`; `old_id` = 現在アクティブなダイヤル(`9055` から); `file_len` = 実際の `.bin` サイズ(= `@0x18 + 36`)。ストアの `.bin` をそのままインストールするのが保証された方法です(Ring Data id 359 + 他 102 個確認済み)。(参考: `core-rust/engine.rs::build_dial_replace_init`。)

### 11.5 構造化ディレクトリ文法 ✅(デコード済み&実装済み — 改訂 2026-07-02)

> **⚠️ 改訂(2026-07-02):以下のフラットな `61 01 00` レコードスキーマは、系統的に
> 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 拒否の原因をカバーしています。

11.8 レンダリング精度の改良 (2026-07-02, ダイアル 275 "SlopeTime")

wfweb のレンダリングを公式ストアのサムネイル (全 103 ダイアルのピクセルオラクル) と相互参照し、4 つのギャップを閉じました:

  • 描画オブジェクト/ポインタの X/Y は 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 書き込みオフセット修正により、最後の不一致が解消されました)。

11.9 AOD コンテナスキップ + スタンドアロン img_number ソース (2026-07-03, ダイアル "Gradient")

  • 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 すべてのダイアルでバイト単位で正確を維持します。

11.10 img_number 桁数 — 40 01 00 XX バイト (✅ ファームウェア確認済み)

img_number が描画する桁数は、フィールドレコード内の 1 バイト です — 要素の 40 01 00 XX 属性サブレコードのデータバイト XX (61 [count][base][glyph-ids] フレームテーブルの後に位置する 0x40 サブレコード):

  • 下位ニブル XX & 0x0F = 桁スロット数 (0 ⇒ ファームウェアデフォルト 7)。
  • ビット 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 仮説は誤りでした — 幅はレイアウトのみであり、桁数ではありません。)


12. バルク転送 & OTA の詳細

転送テーブルは §6 にあります。追加で確認された点:

  • AGPS/EPO ✅: 最初に書き込まれるチャンクは ASCII ヘッダ 000000010000… で始まります。完全な init → [A05F ↔ 905F]×N → 完了ループがワイヤー上で観測されました (~892 チャンク)。
  • ファームウェア OTA (9040–9042, finish 9041) 🔎: 構造マッピング済み; INIT2 ペイロード = バージョンバイト (例: 0b 00 00 39 = 11.0.0.57)。フィールドテスト未実施 (アプリはここで FW アップデートを無効にします)。ファームウェアイメージは 署名なし — 整合性は CRC32 のみ のように見えます (RE で非対称署名は確認されていません)。
  • ⚠️ OTA と FACTORY_RESET (009A 0001) は認証済みセッションを共有するため、単一の有効な BLE 認証で時計を消去したり (原理的には) 文鎮化したりできます。注意して扱ってください。

13. センサー

✅ BLE 経由で公開されるハードウェア:

  • 光学式 PPG — 心拍数 (手動/自動/ワークアウト/安静時)、SpO₂、HRV 由来のストレス。
  • 3 軸加速度計 — 歩数、距離、カロリー、睡眠ステージング、手首上げ、ケイデンス。
  • GNSS/GPS (AGPS 補助) — ワークアウトトラック (WORKOUT_GPS) と位置情報プッシュ (GPS_PUSH)。

気圧計/高度計、コンパス、ジャイロスコープ、皮膚/体温センサーはありません。内部 NTC サーミスタ (基板/バッテリー温度) は存在しますが、AT チャネル (AT GETNTCTEMP, §14) を介してのみ読み取り可能です — 0155 皮膚温度履歴ストリームはこの SKU では空です。

データウィジェットハイジャック (実際のコンプリケーション/データバインディング API はありません — §11.5 参照): 時計の既存のテキストフィールドを再利用して、一目でわかる外部データを表示できます。実証済み ✅: 天気の 都市文字列 (WEATHER_SET_1、例: "BRA 2x1 ARG" がウィジェットに表示) および音楽の トラック/アーティスト フィールド; 連絡先 リスト (20 × 名前[32]+番号[25]) はスクロール可能なデータパネルとして機能します。すべてプッシュであり、永続的なコンプリケーションではありません。


14. AT ファクトリー / シェルチャネル (77d4ff01 / 77d4ff02)

フレーム化プロトコルから独立した、別のプレーンテキスト AT コマンドチャネル。✅ ライブテスト済み:

  • 読み取り: AT GETSECRET (16 バイトペアリングシークレット), GETVERSION, GETSN, GETNAME, GETPID, GETBATLV (生 mV、例 3853mv), GETGSENSOR (生加速度 g 単位、X=… Y=… Z=…), GETNTCTEMP (°C、内部 NTC)。
  • 書き込み / 動作: AT SETMOTOR=1 (モーター振動), SETHR/SETHRV/SETSPO2=… (センサーテスト注入), SETLCDSWITCH/SETGPSSWITCH/SETKEYSWITCH。

応答は ,OK で終わります。SET* コマンドは通常実行されますが、BLE 経由で ,OK をエコーしない場合があります — ケースバイケースで確認してください。


15. ファームウェアゲート / 利用不可能な機能 (🔎 ファームウェア RE)

一部の機能はファームウェアに存在しますが、SKU/地域によって無効化されており、電話/BLE からはアクセスできません — ファームウェアの改造が必要ですが、ここでは範囲外です:

  • ChatGPT 音声 — ゲート = ux2sys フィーチャー ID 0x9e、起動時に NVRAM/EFUSE/地域からシード; この SKU ではサポートフラグ 908b = 00。電話、アカウント、BLE では影響不可 (実験 + RE で確認)。アプリは中継のみ; 音声は電話 → Nothing クラウドへ。
  • 血圧 — 完全なサブシステムがファームウェアに存在、SKU/地域でオフ。
  • Alipay / NFC 決済 — 完全な UI が存在、中国 SKU のみ。
  • ハードウェア/ファームウェアに不在: ECG、SOS/緊急、汎用 NFC。

16. コンプリケーションゲッターテーブル (🔎 ファームウェア内部参照)

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-00805f9b34fb0000fff2-…Write
コマンド 通知0000fff0-…0000fff1-…Notify
シェル 書き込み (AT)—77d4ff01-2fe2-2334-0d35-9ccd078f529cWrite
シェル 通知 (AT)—77d4ff02-…Notify
一括 データ書き込み—02f00000-0000-0000-0000-00000000ffe1Write
一括 データ通知—02f00000-…ffe2Notify
名前cmd1,cmd2
TIMEFFFF 8004
FIRMWARE_VERSION_GET / _RETFFFF 8006 / FFFF 0006
SERIAL_NUMBER_GET / _RET00DE 0002 / 00DE 0001
BATTERY005C 0001
TRIGGER_SYNC005C 0002
USER_INFO_SET / _RET 🔎✅0095 0001 / 0095 0003
FACTORY_RESET009A 0001
DEVICE_REBOOT 🔎FFFF 9080
RESOLUTION_GET 🔎(→ 466×360)FFFF 907F
GPS_PUSH / _RETFFFF 906A / FFFF A06A
UNBIND_SET / _RETFFFF 907A / FFFF A07A
名前cmd1,cmd2
AUTH_PHONE_NAMEFFFF 8049
AUTH_WATCH_MACFFFF 0049
AUTH_PAIR_REQUEST / _REPLYFFFF 8047 / FFFF 0048
AUTH_NONCE_REQUEST / _REPLYFFFF 804B / FFFF 004C
AUTHENTICATED_CONFIRM_REQUEST / _REPLYFFFF 804D / FFFF 0004
AUTH_FAILEDFFFF A061
名前cmd1,cmd2
APP_NOTIFICATION0065 0001
INCOMING_CALL ⚠️0064 0001
CALL_REMINDER_REQUEST / _RESPONSEFFFF 9066 / FFFF A066
FIND_PHONE005B 0001
FIND_WATCH005D 0001
FIND_WATCH_TOGGLEFFFF 9069
SMS_MESSAGE_PUSH / _RETFFFF 906E / FFFF A06E
QUICK_REPLY_SET / _RETFFFF 9073 / FFFF A073
名前cmd1,cmd2
ALARMS_SET / _GET0063 0001 / 0063 0002
CONTACTS_SET / _GET00D5 0001 / 00D5 0002
STANDING_REMINDER_SET / _GET0060 0001 / 0060 0002
WATER_REMINDER_SET / _GET0061 0001 / 0061 0002
TASK_REMINDER_SET / _RET ⚠️FFFF 9072 / FFFF A072
名前cmd1,cmd2
GOALS_SET / _ACK005E 0001 / 005E 0003
UNIT_LENGTH / _ACKFFFF 9067 / FFFF A067
UNIT_TEMPERATURE / _ACKFFFF 9068 / FFFF A068
TIME_FORMAT / _ACK005F 0001 / 005F 0003
WAKE_ON_WRIST_RAISE / _GET / _ACK0062 0001 / 0062 0002 / 0062 0003
LANGUAGE_SET / _RETFFFF 9058 / FFFF A06B
HEART_MONITORING_ENABLED_SET / _GET009B 0001 / 009B 0002
HEART_MONITORING_ALERTSFFFF 9059
DO_NOT_DISTURB / _GET0099 0001 / 0099 0002
SPORTS_SET / _GET00DC 0001 / 00DC 0002
SPORT_LINKAGE_SET / _RETFFFF 9076 / FFFF A076
SPORT_DATA_SYNC 🔎(ライブ HR/カロリー/歩数)FFFF 9078 / FFFF A078
FEMALE_CYCLE_SET / _RETFFFF 9071 / FFFF A071
SLEEP_CONFIG_SET / _RET(目標分数)FFFF 9074 / FFFF A074
WORLD_CLOCK_GETFFFF 906F
WORLD_CLOCK_DST_SET / _RETFFFF 9083 / FFFF A083
VITALITY_GET / _RETFFFF 9079 / FFFF A079
VITALITY_SW_SET / _RETFFFF 9070 / FFFF A070
名前cmd1,cmd2
DIAL_COMMAND_SET / _RET(一覧表示/並び替え/選択)FFFF 9055 / FFFF A055
DIAL_CONFIG_SET / _RETFFFF 9075 / FFFF A075
CHANGE_DIAL(⚠️ 1.0.0.73 では無効 — 使用しないでください)009F 0001
QUICK_CARD_SET/GET / _RET(両方とも 906D)FFFF 906D / FFFF A06D
名前cmd1,cmd2
ACTIVITY_FETCH_1 / _2FFFF 8005 / FFFF 9057
ACTIVITY_FETCH_ACK_1 / _2FFFF 0005 / FFFF A057
ACTIVITY_DATA0056 0001
SLEEP_DATA / _GET0058 0001 / 0058 0002
SPO20055 0001
STRESS009D 0001
HEART_RATE_MANUAL_AUTO0053 0001
HEART_RATE_RESTING00DA 0001
HEART_RATE_WORKOUT00E0 0001
SKIN_TEMP_HISTORY 🔎(この SKU では空)0155 0001 / 0155 0002
WORKOUT_SUMMARY / _V30057 0001 / 0160 0001
WORKOUT_GPSFFFF A05A
ドメインINIT1 リクエスト/応答INIT2 リクエスト/応答CHUNK リクエスト/書き込みFINISH ack1/ack2
ウォッチフェイス(写真)8052/00529063/A063A064/9064A065/9065
ウォッチフェイス(構造化/切り替え)8052/00529075/A075A064/9064A065/9065
ファームウェア9052/A0529040/A040A042/9042A041/9041
AGPS/EPO905E/A05E—A05F/905FA060/9060
オフセットサイズフィールド
04タイムスタンプ(エポック秒)
44歩数
84距離(m)
124カロリー
1616予約済み(観測値 0)
オフセットサイズフィールド
04session_start(エポック、UTC)
44wakeup(エポック、UTC)
82total_deep_s
102total_core_s
122total_rem_s
142total_awake_s
162⚠️ [未確定](セッション ID/スコア?観測値はレコードの合計と一致しません)
ac 49 1f 01
  • CONTACTS_SET(00D5 0001)✅:N × 57 バイト = name(32) ‖ phone(25)。ウォッチ UI は最大 20 件表示。
  • ALARMS_SET(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…"。
  • GOALS_SET(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 バイト形式を推奨します。
  • STANDING_REMINDER / WATER_REMINDER(0060/0061 0001)✅:11 バイト: enabled(1) ‖ threshold_min(u16 LE) ‖ dndStart(u32 LE) ‖ dndEnd(u32 LE)。UI に表示される「アクティブウィンドウ 08:00–22:00」は 固定のファームウェアデフォルト であり、ペイロードには 含まれません。
  • SPORTS_SET(00DC 0001)✅:count(1) = 36 スロット ‖ activityTypeCode[36](アクティブコードの後、00 パディング)。 ウォッチのワークアウトメニューに表示するスポーツを選択します。
  • HEART_MONITORING_ENABLED(009B 0001)✅:kind バイト — 01 = 24/7 HR、02 = SpO₂、 04 = ストレス(30 分ごとに測定)。
  • HEART_MONITORING_ALERTS(FFFF 9059)✅:無効 = 00;有効 = 01 ‖ hrLow ‖ hrHigh ‖ sportHrHigh ‖ spo2Low ‖ 00 00 00 00(0/255 の境界値 = 「制限なし」)。
  • FEMALE_CYCLE(FFFF 9071)✅:01 ‖ predictionOpen ‖ notifySwitch ‖ cycleStartSwitch ‖ cycleStartNotifyBefore ‖ ovulationStartSwitch ‖ ovulationStartNotifyBefore ‖ fertileStartSwitch ‖ fertileStartNotifyBefore ‖ period(1) ‖ cyclePeriod(1) ‖ cycleStartDate(u32) ‖ markStart(u32) ‖ markEnd(u32)(キャプチャ:period=5、cyclePeriod=0x1c=28)。
  • QUICK_REPLY(FFFF 9073)✅:TLV — count(1) ‖ total(1) ‖ [id(1) ‖ len(u16 LE) ‖ msg-UTF8]… (7 つのデフォルト返信がキャプチャおよび復号されました)。
  • WORLD_CLOCK(FFFF 906F)✅:名前ではなく数値の 市区町村 ID を送信します(01 ‖ count ‖ cityId(2 BE)…); ウォッチは内部テーブルから ID をマッピングします。DST 設定 FFFF 9083 = count ‖ [id(u16 LE) ‖ dst(u16 LE) ‖ start(u32 LE) ‖ end(u32 LE)]…。
  • MUSIC_INFO_SET(FFFF 905C、131 B)✅:state(1: 0=なし/1=一時停止/2=再生中) ‖ volume(1) ‖ volumeMax(1) ‖ track(64) ‖ artist(64)。ウォッチは MUSIC_BUTTON (A05D) も送信します。
  • WEATHER_SET_1(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 参照。)
  • FIND_WATCH(005D 0001)✅:ペイロード 0x01 → ウォッチがリンク/バイブレーション(+ ACK 005D 0003)。
  • GPS_PUSH(FFFF 906A)✅ — ビッグエンディアン、経度が先:16 バイト ts(u32 BE) ‖ lon×1e7(i32 BE) ‖ lat×1e7(i32 BE) ‖ 00 00。実際の位置で検証済み。
  • WORKOUT_GPS(FFFF A05A)✅ — リトルエンディアン、経度が先:12 バイト ts(i32) ‖ lon×1e7(i32) ‖ lat×1e7(i32)。
  • TIME(FFFF 8004):§7 を参照。
  • cfbppraster (after LZ4)use
    42RGB565-LE不透明背景 (FULL/THUMB)
    53RGB565-LE (2 B) + alpha (1 B) per pxアンチエイリアススプライト (グリフ、手、アイコン)
    13 (0x0d)0.54-bit alpha mask; firmware tints at runtime数字グリフアトラス
    24 (0x18)4RGBA8888フルカラーレイヤー (常時表示の aodImage を含む)
    1—JPEG/JFIF (ff d8 ff), extract with any decoder稀なアニメーションフレーム
    0x61
    NotEnvelope
    0a
    ChildOverflow
    61 0a 00
    −18/−16
    書き込みオフセットも移動する必要があります
    0a
  • マルチバリアントコンプリケーションスロット: アクティブなメトリックは .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 で確認) は、ファームウェアがデフォルトビューで描画しないスロットです — それらの値はキャンバスエッジにさえ収まりません。プレビューでは非表示として扱います。
  • aod
    0x22
    @69,209
    分離された Normal|AOD 編集 UI
  • スタンドアロン 0x60 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 スロットに気温を表示するダイアルは、ユーザー設定のコンプリケーション (デバイス状態) であり、ファイルのデフォルトではありません。
  • deccarddeccard
    0空スロット49–53,97–98天気
    1–6歩数54–58タイマー
    7–12カロリー59–62呼吸
    13–18スタンド63,65–67ストップウォッチ
    19–24中程度のアクティビティ68–71バッテリー
    25–30心拍数72–76最近
    31–36SpO₂77–81連絡先
    37–42ストレス82–86ダイアル/電話
    43–48睡眠87–91スポーツ (sportId ≠ 0)
    92ミュージック93/94/96アクティビティ記録 / PAI / サイクル