解析日: 2026-08-07
対象: Codexデスクトップアプリ + Work Louder Codex Micro(BLE HID)
目的: 6個のエージェントキーと、キー照明・外周照明を互換デバイスから制御する
Codex MicroのLED制御は、独自UUIDのBLEサービスではない。標準のBLE HID Service(HOGP)内にある Report Characteristic を使い、Vendor HID Report ID 6 の中へJSON-RPCを分割して送っている。
6個のエージェントキーはスロット 0...5 に対応し、個別の色・明るさ・エフェクトは次のRPCで更新される。
v.oai.thstatus: 6個のエージェントキーの個別LEDv.oai.rgbcfg: 通常キーのバックライトと外周アンビエントLED
重要なのはBLE Characteristic UUIDだけでなく、0x2908 Report Reference Descriptorで Report ID 6 / Output Report を選ぶことである。
| 項目 | 値 |
|---|---|
| BLE Service | HID Service 0x1812 |
| Characteristic | Report 0x2A4D |
| Descriptor | Report Reference 0x2908 |
| LED制御のReport Reference値 | 06 02 |
| Report ID | 6 |
| Report Type | 2 = Output |
| Reportデータ長 | 63 bytes(Report IDを除く) |
| Usage Page | 0xFF00(Vendor Defined) |
| Output Usage | 0x03 |
0x2A4Dは同じHID Service内に複数存在するため、UUIDだけでは特定できない。各Characteristicの0x2908を読み、次の2バイトで識別する。
0x2908値 |
用途 |
|---|---|
06 01 |
Report ID 6 / Input。デバイスからCodexへの通知・RPC応答 |
06 02 |
Report ID 6 / Output。CodexからデバイスへのRPC・LED制御 |
06 03 |
Report ID 6 / Feature |
Output Characteristicは通常、Read、Write、Write Without Responseを持つ。実際のAttribute Handleは接続・GATT実装ごとに変わるため固定してはいけない。
Vendor部分のHID Report Mapは、概念的には次の構成になる。
Usage Page (Vendor 0xFF00)
Usage 0x01
Collection (Application)
Report ID 6
Input Usage 0x02, 63 bytes
Output Usage 0x03, 63 bytes
Feature Usage 0x04, 63 bytes
End Collection
macOSではHID ServiceがOSに管理されるため、CoreBluetoothから直接アクセスしにくい場合がある。その場合はIOHIDManagerまたはnode-hidでReport ID 6を開く方が確実である。
1回のHID Reportは64 bytesである。node-hidの書き込みバッファでは先頭にReport IDが付く。
byte 0 Report ID = 0x06
byte 1 Channel = 0x02 (JSON-RPC)
byte 2 このパケットに入っているJSON断片の長さ = 0...61
byte 3...63 UTF-8 JSON断片(最大61 bytes)
BLE GATTの0x2A4Dへ直接書く場合、Report IDは0x2908で決まるため、Characteristic Valueは通常次の63 bytesになる。
byte 0 Channel = 0x02
byte 1 JSON断片長
byte 2...62 UTF-8 JSON断片
JSONが61 bytesを超える場合は複数Reportへ連続分割する。JSON自体に分割番号や終端記号はなく、受信側は断片を連結し、完全なJSONとしてparseできた時点で1リクエストと判定する。
RPCリクエストの基本形は次の通り。
{
"method": "v.oai.thstatus",
"params": [],
"id": 123
}物理キーのイベント名はAG00〜AG05、LED更新時のスロットIDは0〜5である。
| キー | LEDスロット |
|---|---|
AG00 |
id: 0 |
AG01 |
id: 1 |
AG02 |
id: 2 |
AG03 |
id: 3 |
AG04 |
id: 4 |
AG05 |
id: 5 |
Codexアプリは6スロットの状態を短縮フィールド名で送る。
| フィールド | 意味 | 値 |
|---|---|---|
id |
スロット番号 | 0...5 |
c |
Packed RGB | 0xRRGGBBを整数化 |
b |
明るさ | 0.0...1.0 |
e |
LEDエフェクト | 下表参照 |
s |
アニメーション速度 | 0.0...1.0 |
sk |
通常キー照明と同期 | 0 / 1 |
sa |
アンビエント照明と同期 | 0 / 1 |
Codexアプリが使用する状態色は次の通り。
| Codex状態 | 色 | 10進数 | RGB |
|---|---|---|---|
working |
0x304FFE |
3166206 |
48, 79, 254 |
unread |
0x00FF4C |
65356 |
0, 255, 76 |
idle |
0xFFFFFF |
16777215 |
255, 255, 255 |
awaiting-approval |
0xFF6D00 |
16739584 |
255, 109, 0 |
awaiting-response |
0xFF6D00 |
16739584 |
255, 109, 0 |
error |
0xFF0033 |
16711731 |
255, 0, 51 |
off |
0x000000 |
0 |
0, 0, 0 |
awaiting-approvalとawaiting-responseは同じオレンジ色なので、LED表示としては6色(青・緑・白・オレンジ・赤・消灯)になる。
off:e=0,b=0,s=0- 通常表示:
e=1(solid),s=0 - 選択中またはpulse中:
e=4(breath),s=0.4 bにはCodex設定画面の明るさが0.0...1.0で入る- 現在のCodexアプリは
sk=0,sa=0を送る
6スロットを一括更新する例:
{
"method": "v.oai.thstatus",
"params": [
{ "id": 0, "c": 3166206, "b": 1, "e": 4, "s": 0.4, "sk": 0, "sa": 0 },
{ "id": 1, "c": 65356, "b": 1, "e": 1, "s": 0, "sk": 0, "sa": 0 },
{ "id": 2, "c": 16777215, "b": 1, "e": 1, "s": 0, "sk": 0, "sa": 0 },
{ "id": 3, "c": 16739584, "b": 1, "e": 1, "s": 0, "sk": 0, "sa": 0 },
{ "id": 4, "c": 16711731, "b": 1, "e": 1, "s": 0, "sk": 0, "sa": 0 },
{ "id": 5, "c": 0, "b": 0, "e": 0, "s": 0, "sk": 0, "sa": 0 }
],
"id": 123
}このRPCは6個のエージェントキーとは別に、通常キー照明と外周アンビエント照明を設定する。
{
"method": "v.oai.rgbcfg",
"params": {
"ambient": { "e": 2, "b": 1, "s": 0.4, "m": 0, "c": 3166206 },
"keys": { "e": 0, "b": 0, "s": 0, "m": 0, "c": 0 }
},
"id": 124
}| フィールド | 意味 |
|---|---|
ambient |
外周LED |
keys |
通常キーのバックライト |
e |
エフェクト番号 |
b |
明るさ 0.0...1.0 |
s |
速度 0.0...1.0 |
m |
エフェクト固有パラメータ |
c |
Packed RGB整数 |
| 値 | 名前 | 動作 |
|---|---|---|
0 |
off |
消灯 |
1 |
solid |
単色点灯 |
2 |
snake |
色のセグメントが移動 |
3 |
rainbow |
虹色循環 |
4 |
breath |
明滅 |
5 |
gradient |
グラデーション |
6 |
shallowBreath |
50%〜100%付近で浅く明滅 |
- 選択中スロットが
workingなら、外周はそのスロット色のsnake、速度0.4 - 選択直後の約4秒間は、通常キーも選択スロットまたは外周と同じ色で
solid - 音声録音中は外周が
0x2E8B57(SeaGreen)のsnake - 音声処理中は外周が白の
snake - 音声完了時は外周が白の
solid - 非アクティブタイムアウト時は
rgbcfgとthstatusの両方を消灯設定にする
Codexアプリは照明RPCへの応答を待ってから次のコマンドを送る。互換デバイス側が応答しないとタイムアウトし、未検出・再接続・照明停止の原因になる。
応答はReport ID 6のInput Report(0x2908 = 06 01)で返し、JSON末尾にCRLFを付ける。
{"result":{"ok":1},"id":123,"method":"v.oai.thstatus"}\r\n{"result":{"ok":1},"id":124,"method":"v.oai.rgbcfg"}\r\n応答側の63-byte payloadも同じ形式である。
byte 0 Channel = 0x02
byte 1 UTF-8断片長(最大61)
byte 2...62 JSON応答断片
複数パケットになる場合は、前のBLE notificationが送信キューを抜ける時間を確保して順番にnotifyする。
0x2A4Dは複数ある。0x2908 = 06 02でOutput Reportを選ぶ。- GATT Handleは固定しない。Service Discovery後に毎回解決する。
- Report IDをCharacteristic Valueへ含めるかどうかはAPI層で異なる。
node-hid.write: 先頭に0x06を含む64 bytes- BLE GATT直接write:
0x06を除く63 bytes
- 61-byte境界でUTF-8を分割する。現在のフィールド値はASCII中心だが、一般化する場合はバイト列で分割する。
- 受信JSONは複数Reportを連結してからparseする。
v.oai.thstatusとv.oai.rgbcfgの両方へ必ず同じidを付けて応答する。- HID Report ID 6のInput notificationを有効にしてからRPCを開始する。
以下はCodexデスクトップアプリに同梱されたWork LouderデバイスSDKの通信形式、Codex側の照明状態生成ロジック、実機互換HID Report Map、およびM5Stack Core2互換実装での送受信確認を突き合わせた結果である。
- Report ID 6、63-byte Vendor Input/Output/Feature: 確認済み
- JSON-RPCフレーミングと61-byte分割: 確認済み
v.oai.thstatus/v.oai.rgbcfgのフィールド: 確認済み- 6スロットの状態色とエフェクト選択: Codexアプリ実装から確認済み
- BLE側のCharacteristic識別: HOGP Report Characteristic + Report Reference仕様、および互換GATT実装から確認