An unofficial, read-only-safe Rust CLI for probing and controlling the Work Louder Creator Micro 2 (VID 0x303A, PID 0x8298) over its vendor HID channel — a 64-byte JSON-RPC transport on HID report ID 6. It can enumerate the device, query firmware/status, read (never write) the on-device filesystem, stream input events and firmware logs, and drive the RGB lighting, over both USB and Bluetooth.
Everything documented here was discovered by clean-room observation of the vendor channel (building on the sibling Codex Micro protocol docs) and live-verified against firmware v0.6.0 with the Work Louder Input app 0.18.0-rc.8 (as of 2026-08-01).
cargo build
cargo run -- enumerate # find the device
cargo run -- version # query firmware version
cargo run -- status # battery, charging, profile, layer
cargo run -- led zones --backlight ff0000 --underglow 0000ffNotes for macOS:
- The vendor HID page (usage page
0xFF00, usage0x0001) needs no special permission — all RPC and lighting commands just work. - Capturing boot-keyboard reports (report ID
1) withlistenmay require granting your terminal Input Monitoring permission. - hidapi is built with the
macos-shared-devicefeature; without a shared (non-exclusive) open, macOS refuses the device with error0xE00002E2.
Works over USB and Bluetooth: the vendor channel is exposed on both transports (6 HID usage pairs on USB, 4 on BT, vendor pair present on both), with comparable latency and correct multi-fragment reassembly over BT.
| Command | Description |
|---|---|
| enumerate [--all] | List HID devices (defaults to VID 0x303A only; --all lists everything). Shows usage pairs and transport (bus=) per device. |
| version | Firmware version via sys.version. |
| status | Battery, charging, profile, layer via device.status. |
| fs-list | List files on the device filesystem via fs.list. |
| fs-read [NAME] | Read a file via fs.read (default: keymap.json). Read-only. |
| listen [--raw] [--duration ] | Stream firmware logs and decoded input notifications. --raw hex-dumps every report; --duration bounds the capture (default: until Ctrl-C). |
| led zones --backlight --underglow | Color the backlight + underglow zones via lights.preview. |
| led set | Color one LED by id via v.oai.thstatus. |
| led multi = [...] | Color multiple LEDs in a single v.oai.thstatus call, e.g. led multi 0=ff0000 1=00ff00. |
| led sweep [--delay-ms ] | Walk LED ids 0..=16 with distinct colors to show which are addressable (default delay: 500 ms). Restores a rainbow preview when done. |
| led rgbcfg --keys --ambient | Color the grouped keys + ambient zones via v.oai.rgbcfg. |
Flags shared by zones, set, multi, and rgbcfg: --effect <off|solid|snake|rainbow|breath|gradient> (default solid) and --brightness <0.0-1.0> (default 0.5); zones and rgbcfg also take --speed <0.0-1.0> (default 0.5). Colors accept #ff0000 or ff0000.
Examples:
cargo run -- fs-read keymap.json
cargo run -- listen --duration 10
cargo run -- led multi 0=ff0000 1=ff6600 2=ffcc00 3=ccff00 4=66ff00 5=00ff00
cargo run -- led rgbcfg --keys ff0000 --ambient 00ffff --effect breathReverse-engineered by observing the vendor channel; all of the following is live-verified on the Creator Micro 2.
- HID report ID
6is the vendor channel. Every packet is 64 bytes:[reportId=6][channel][len][payload ≤ 61 bytes]. - Channel
1carries firmware log text; channel2carries JSON-RPC. Messages larger than one packet are fragmented and must be reassembled (fragmented requests and responses both work, e.g. a 17-entryv.oai.thstatuscall or a 1898-bytefs.readresult).
Requests look like {"method":"sys.version","params":null,"id":1}; responses like {"id":1,"result":{...}}.
Known host→device methods:
| Method | Params | Notes |
|---|---|---|
| sys.version | null | Firmware version. |
| device.status | null | Battery, charging, profile, layer index. |
| fs.list | null | Lists device files (e.g. keymap.json, and smart_actions.json since fw v0.6.0). |
| fs.read | {"file": ""} | Returns {"data": ""}. Read-only. |
| lights.preview | backlight/underglow zone objects (effect, brightness, speed, magic, color) | Zone lighting on plain layers. Acks with result: null. |
| v.oai.thstatus | Array of {id, c: <24-bit RGB>, b: <0-1>, e: , s: } | Per-LED status lighting (Codex layer). Acks {ok:1}. |
| v.oai.rgbcfg | keys/ambient zone objects (e, b, s, m, c) | Grouped zone lighting on the Codex layer. Acks {ok:1}. |
Device→host traffic on channel 2:
kb.radialnotifications — joystick stream in radial mode (see Input capture).v.oai.hidnotifications — vendor key/encoder events on the Codex layer:{"m":"v.oai.hid","p":{"k":"AG00","act":1}}.host.focused_apprequests — the device periodically polls the host for the focused application (AppSense); it sends its own JSON-RPC requests with anid.
The Codex layer maps controls to vendor keycodes that emit v.oai.hid notifications instead of standard HID reports:
KV_OAI_AG00–KV_OAI_AG05— the 6 agent keys (top two rows).KV_OAI_ACT06–KV_OAI_ACT12— the 7 action keys (bottom two rows).KV_OAI_ENC_CC/KV_OAI_ENC_CW/KV_OAI_ENC_CLK— encoder counter-clockwise / clockwise / click.
act values: 1 = press, 0 = release, 2 = rotate (encoder). All live-verified on the CM2: ENC_CC/ENC_CW emit act=2 with one event per detent; ENC_CLK emits act=1/0 press/release like a key.
Which lighting surface renders depends on the type of the active layer:
| Active layer | Renders | Doesn't render |
|---|---|---|
| Plain (KC_*) layer | lights.preview (backlight + underglow zones) | v.oai.thstatus frames (acked but invisible) |
| Codex layer | v.oai.thstatus (per-LED, agent keys) + v.oai.rgbcfg (keys = action keys, ambient = underglow/border) | lights.preview (acks result: null but does not render) |
On the Codex layer the two surfaces are disjoint (live-verified on the CM2):
v.oai.thstatusexclusively owns the 6 agent keys (rows 1–2, ids 0..=5).- The
v.oai.rgbcfgkeyszone drives only the 7 action keys (rows 3–4), as a single bank;ambientdrives the border/underglow. - The
keyszone does not affect the agent keys — there is no blending or priority between the surfaces, so they can be painted independently. - There is no per-LED addressing for the action keys: thstatus ids 6..=16 are acked but unmapped (see below), and no other per-LED method exists in the enumerated protocol surface.
Per-LED control via v.oai.thstatus:
- Only ids 0..=5 map to physical LEDs — the six agent keys: id 0 = top-left (AG00), id 1 = top-right (AG01), ids 2..=5 = second row left→right (AG02..AG05).
- Ids 6..=16 are acked (
{ok:1}) but unmapped — an ids-6..16-only frame at full brightness lights nothing (no action keys, indicators, or underglow). Same surface as the Codex Micro. - Semantics are full-frame repaint: each call replaces the whole status display; ids omitted from the array are not preserved. To animate individual LEDs, maintain the full frame host-side and resend it whole (multi-entry frames fit fine; fragmented requests work).
Requirements chain for per-LED control:
- Firmware ≥ v0.6.0 (on v0.4.0,
v.oai.thstatusreturns404 Method not foundfor every id). - A Codex layer on the keymap (requires Input app ≥ 0.18-rc; the layer template maps keys to
KV_OAI_*keycodes). - The Codex layer must be active on the device.
All lighting is volatile — nothing is written to flash, and unplugging/replugging restores the configured behavior.
What listen sees depends on the active layer:
- Plain layers: keys emit standard boot-keyboard reports (report ID
0x01) and the encoder emits consumer-control reports (report ID0x02, e.g. volume up/down) — no vendor notifications. Reading these on macOS may need Input Monitoring permission. - Joystick (radial mode, any layer): streams
kb.radialvendor notifications at ~40 Hz while deflected:{"m":"kb.radial","p":{"a":<angle 0-1>,"d":<deflection 0-1>,"s":<sector 0-7>,...}}. Release is signaled byd: 0, s: -1.sindexes the layer's radial-menu sectors; selecting a sector emits that sector's keycode as a normal HID report. - Joystick (VENDOR mode, Codex layer): streams vendor notifications at ~45 Hz while deflected with raw polar coordinates:
{"a": <angle 0-1>, "d": <deflection 0-1>}. Unlike radial mode there is no sector field — the firmware doesn't compute a sector, the host does its own sector math. Release ends the stream with a final centered event (a: 0.0, d: 0.0). - Codex layer: keys and encoder emit
v.oai.hidvendor events (KV_OAI_*, with press/release/rotateactcodes) on channel 2 instead of standard HID reports.
This tool is deliberately read-only where it matters:
- Filesystem: only
fs.listandfs.readare implemented.fs.write,fs.delete,sys.bootloader, and other persistent/destructive methods are not exposed and never called. - Lighting: all lighting methods (
lights.preview,v.oai.thstatus,v.oai.rgbcfg) are volatile — they never touch flash, and a reboot or unplug/replug resets them. - Nothing this tool does can brick or persistently modify the device.
This is an unofficial community project, not affiliated with or endorsed by Work Louder or OpenAI. The protocol details come from clean-room observation of the device's vendor HID channel. Behavior was verified against firmware v0.6.0 and Input 0.18.0-rc.8 as of 2026-08-01; future firmware may change any of it.
Apache-2.0 — see LICENSE.