Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

cm2-probe

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

Quick start

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 0000ff

Notes for macOS:

  • The vendor HID page (usage page 0xFF00, usage 0x0001) needs no special permission — all RPC and lighting commands just work.
  • Capturing boot-keyboard reports (report ID 1) with listen may require granting your terminal Input Monitoring permission.
  • hidapi is built with the macos-shared-device feature; without a shared (non-exclusive) open, macOS refuses the device with error 0xE00002E2.

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.

Commands

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 breath

Protocol

Reverse-engineered by observing the vendor channel; all of the following is live-verified on the Creator Micro 2.

Framing

  • HID report ID 6 is the vendor channel. Every packet is 64 bytes: [reportId=6][channel][len][payload ≤ 61 bytes].
  • Channel 1 carries firmware log text; channel 2 carries JSON-RPC. Messages larger than one packet are fragmented and must be reassembled (fragmented requests and responses both work, e.g. a 17-entry v.oai.thstatus call or a 1898-byte fs.read result).

JSON-RPC

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.radial notifications — joystick stream in radial mode (see Input capture).
  • v.oai.hid notifications — vendor key/encoder events on the Codex layer: {"m":"v.oai.hid","p":{"k":"AG00","act":1}}.
  • host.focused_app requests — the device periodically polls the host for the focused application (AppSense); it sends its own JSON-RPC requests with an id.

Vendor key events (Codex layer)

The Codex layer maps controls to vendor keycodes that emit v.oai.hid notifications instead of standard HID reports:

  • KV_OAI_AG00KV_OAI_AG05 — the 6 agent keys (top two rows).
  • KV_OAI_ACT06KV_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.

Lighting model

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.thstatus exclusively owns the 6 agent keys (rows 1–2, ids 0..=5).
  • The v.oai.rgbcfg keys zone drives only the 7 action keys (rows 3–4), as a single bank; ambient drives the border/underglow.
  • The keys zone 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:

  1. Firmware ≥ v0.6.0 (on v0.4.0, v.oai.thstatus returns 404 Method not found for every id).
  2. A Codex layer on the keymap (requires Input app ≥ 0.18-rc; the layer template maps keys to KV_OAI_* keycodes).
  3. 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.

Input capture

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 ID 0x02, e.g. volume up/down) — no vendor notifications. Reading these on macOS may need Input Monitoring permission.
  • Joystick (radial mode, any layer): streams kb.radial vendor 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 by d: 0, s: -1. s indexes 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.hid vendor events (KV_OAI_*, with press/release/rotate act codes) on channel 2 instead of standard HID reports.

Safety

This tool is deliberately read-only where it matters:

  • Filesystem: only fs.list and fs.read are 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.

Disclaimer

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.

License

Apache-2.0 — see LICENSE.

About

Unofficial Rust CLI for probing and controlling the Work Louder Creator Micro 2 over its vendor HID channel

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages