Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions captures/roccat-kone-pro-air/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# ROCCAT Kone Pro Air fixtures

Hardware reference material for `src/roccat/` and `src/drivers/roccat/`.
Captured from this reporter's own unit — one ROCCAT Kone Pro Air on its
Stellar Wireless dongle (USB `1e7d:2c8e`), Windows. The first session
(2026-10-14) was a driverless node-hid session with ROCCAT Swarm
uninstalled; a later session (2026-10-16, the last file in the table) ran
with Swarm installed and operating the mouse. The mouse's product string
carries no serial number, the vendor frames carry no account data, and no
capture here includes personal data.

| File | What it is |
|-|-|
| `device-enumeration.txt` | The four HID interfaces and their usage pages/uses as Windows presents them (node-hid + PnP). The config channel is interface 2, `0xFF00:0x0001`. |
| `feature-sweep.txt` | The negative result: GET for every report id 0x00–0xFF on every interface times out or answers zero. The whole protocol lives on output reports, which is why the frames are `sendReport`-shaped. |
| `session-2026-10-14.txt` | A byte-precise transcript: status probe, settings query, the vendor-replayed settings write + commit tail, a second query, the lighting write, and the echoes to each. One session earlier the query echo carried a full stage table (the mouse awake); in this session the mouse was idle and the same frame echoed an empty body — both are in the file. |
| `transcript-earlier-session.txt` | The earlier, still byte-precise outline of the same frames from the discovery session, included because it is the capture where the stage table WAS returned (`10 50 03 … 10 10 18 18 20 20 40 40 64 64 01 02 00 00` — both slot lists identical, five entries 800/1200/1600/3200/5000, which matches this unit's then-current Swarm settings rather than factory defaults). |
| `profiles-and-write-path-2026-10-16.txt` | A second session, this one with Swarm installed and driving the mouse. Identifies the five `90 00 26` reply pages as the five onboard profiles (polling at `b11`, stage table at `b14-23`, lift-off at `b24`), shows a UI polling change moving exactly that byte, aligns the read page to the plugin's write frame at +5, and records that hand-sent settings writes on interface 2 are echoed but never change the stored tables. |

## What settles

- **Transport.** Unnumbered 64-byte output reports on interface 2
(`0xFF00:0x0001`; interface 1 on the wired generation). No feature report
exists on this interface — `feature-sweep.txt` proves the negative.
- **Frame shape.** `10 <class> <sub> …` where class `0x50` is the
performance/settings group, `0x10` the lighting group, and the status
probe is the two-byte `90 0a`. Echoes come back on the same interface's
input stream and repeat byte 1 verbatim.
- **Settings write.** `10 50 14 00 01 <polling> <angle> 00 <five stage
pairs, offsets 8..17> <lod 18> 00 80 <led timeout 21> 00 <debounce 23>`
followed by the commit tail `10 50 18 04` — byte-for-byte the sequence the
vendor software replays (SignalRGB's WhirlwindFX plugin is public evidence
of that replay).
- **Status probe.** `90 0a` answers `90 0a 04 00 00 <b5> <b6> <b7> <w8> <b10>`:
byte 5 was 0x1e across one session and 0x14 across another. Bytes 8–9
(0x0aa0 = 2720, later 0x0b44 = 2884) go up as well as down, so the
"estimated minutes remaining" reading stands falsified. Whether byte 5 is
the battery percent has to be settled by watching it drain.
- **Lighting write.** `10 10 0b 00 09 64 64 64 06 <r g b> <r g b>` for the two
inner zones, echoed `10 10 03 …`. No read-back is known, so the codec ships
it write-only.

## What does not settle

- **The percent meaning of status byte 5** (see above).
- **Read-back of the settings block in the idle state.** With the mouse
idle/sleeping, the `10 50 14` query echoes with an empty body; with the
mouse awake, an earlier session returned the twenty-slot table. The decoder
treats all-zero tables as no-data instead of five zero-DPI stages.
- **Which slot list is X and which is Y.** The echo carried both lists with
identical values on the captured unit, so the assignment (even slots =
the vendor UI's stage list) is Replay-proven but sensor-axis-unproven.
- **Wired generation `2c92` parity.** The wired mode enumerates the same
interfaces one index earlier (interface 1) per the community plugin's
`Validate` guard; nothing here was captured on the cable.
24 changes: 24 additions & 0 deletions captures/roccat-kone-pro-air/device-enumeration.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# HID interface enumeration, ROCCAT Kone Pro Air Stellar Wireless dongle
# USB 1e7d:2c8e, Windows, node-hid devices() + Win32 PnP, 2026-10-14.
# The composite is "USB Composite Device"/"ROCCAT Kone Pro Air Dongle",
# four interfaces:
#
# iface 0 usage page 0x0001, usage 0x0002 -> boot mouse (pointer)
# iface 1 usage page 0x0001, usage 0x0006 -> keyboard-style interface
# (media keys? uninterpreted)
# iface 2 usage page 0xFF00, usage 0x0001 -> VENDOR CONTROL CHANNEL,
# unnumbered 64-byte output reports. No feature reports
# (every GET answer timed out or returned zero).
# iface 3 usage page 0x000C, usage 0x0001 -> consumer control,
# plus a second top-level collection on the same interface with
# usage page 0x0001, usage 0x0080 (system control).
#
# Wired generation (`1e7d:2c92`) is reported by the public SignalRGB plugin's
# Validate() guard as interface === 1 for the same collection shape; not
# captured on this unit.
#
# Win32 PnP hardware IDs (no serial suffix present):
# USB\VID_1E7D&PID_2C8E&MI_00
# USB\VID_1E7D&PID_2C8E&MI_01
# USB\VID_1E7D&PID_2C8E&MI_02
# USB\VID_1E7D&PID_2C8E&MI_03
12 changes: 12 additions & 0 deletions captures/roccat-kone-pro-air/feature-sweep.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# GET (feature report) sweep — negative result
# Interface 2 (0xFF00:0x0001), node-hid, Windows, 2026-10-14.
#
# For every report id 0x00..0xFF a 64-byte getFeatureReport was issued
# (0.4 s each). Every call threw "timed out" or returned nothing readable.
# Interfaces 0 and 1 behave the same.
#
# Conclusion: the whole protocol is output-report + input-stream based.
# The WebHID driver writes sendReport(0, 64B) and matches echoes on the
# inputreport event, echoing the frame's byte 1 (class byte).
# Sweep transcript shape (all ids identical):
# GET id 0x00 .. 0xFF -> no readable data
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Per-profile read pages and the write path (2026-10-16)

Second session on the same unit, this time with ROCCAT Swarm installed and
driving the mouse, observed from a passive node-hid listener on interface 2
while the Swarm UI was operated. It settles what the earlier captures could
not: where the settings block lives in the reply stream, and what our
user-mode writes actually do to the mouse.

## The five `90 00 26` reply pages are the five onboard profiles

Swarm exposes five profile slots ("配置文件组1"…"配置文件组5"). When the
device page attaches, the mouse streams five `90 00 26` replies, one per
profile. Fields, aligned on the page's own header `90 00 26 00 00 04 20 00
92 2c`:

```text
b10 profile index (01..05)
b11 that profile's polling code (00=1000 Hz .. 03=125 Hz, same dict as the write)
b12 00
b13 00 empty / 01 valid / 02 the profile the UI is editing
b14-23 five DPI stages, u16 little-endian, 50/unit (e.g. 08 00 = 400 DPI)
b24 lift-off distance code (06 = Low)
b25 00
b26 80 / 06 / 00 (profile-dependent; unresolved)
b27 LED timeout minutes (0f = 15)
```

Captured order (1000 Hz era, mouse awake, profiles 2..5 configured, profile 1
blank):

```text
90 00 26 00 00 04 20 00 92 2c 02 00 00 02 08 00 10 00 15 00 20 00 40 00 06 00 06 0f ...
90 00 26 00 00 04 20 00 92 2c 03 00 00 01 08 00 10 00 18 00 20 00 40 00 06 00 00 0f ...
90 00 26 00 00 04 20 00 92 2c 04 00 00 01 08 00 10 00 18 00 20 00 40 00 06 00 00 05 ...
90 00 26 00 00 04 20 00 92 2c 05 00 00 01 08 00 10 00 18 00 20 00 40 00 06 00 00 05 ...
90 00 26 00 00 04 20 00 92 2c 01 00 00 00 00 00 00 00 00 00 00 00 00 00 06 00 00 0f ...
```

The profile the UI was editing (index 2) carries the stages **400 / 800 /
1050 / 1600 / 3200** — byte-for-byte the values the UI's own "DPI 切换开关"
card displays (`400`, `800`, `1050`, `1600`, `3200`). The 1050 is the unit
owner's own value, not a side effect of any probe.

## UI edits land in these pages (one-session differential)

With the listener running, the UI was used to switch the edited profile's
polling rate 1000 → 125 Hz. Restarting Swarm re-triggered the stream, and the
same profile's page changed exactly one byte:

```text
before: 90 00 26 00 00 04 20 00 92 2c 02 00 00 02 08 00 10 00 15 00 20 00 40 00 ...
after: 90 00 26 00 00 04 20 00 92 2c 02 03 00 02 08 00 10 00 15 00 20 00 40 00 ...
```

`b11` 00 → 03 (125 Hz). That identifies the polling field and proves the
vendor's own write path reaches the mouse.

## Read page vs. write frame: a +5 byte shift

Lining the page up with the SignalRGB plugin's write frame (`Plugins/Roccat/
Roccat_Kone_Pro_Air_Mouse.js`) puts every established field at write offset
+ 5:

```text
write frame 10 50 14 00 01 <poll@6> <angle@7> 00 <stages u16 @9..18> <lod@19> 00 80 <led@22> 00 <deb@24>
read page 90 00 26 00 00 04 20 00 92 2c <prof@10> <poll@11> 00 <state@13> <stages u16 @14..23> <lod@24> 00 <b26@26> <led@27> ...
```

So the stored settings block is the plugin's layout, five u16 stage slots
(not two interleaved byte lists), and the page is that block shifted right by
five bytes behind a ten-byte read header.

## What our own writes did

- Frames sent by hand on interface 2 — the plugin layout with stage 1 set to
500 (u16 `0a 00`) and, separately, the two-interleaved-lists shape — were
**accepted and echoed** (`50 14 03 00 ec fa`), but a fresh stream after each
showed **no profile's stage 1 at 500**. The stored tables were unchanged.
- On this unit the vendor writes are not visible on any HID interface at all:
a listener on interfaces 0/1/2/3 during UI edits captured the UI's read
polling loops (`10 71 03`, `10 72 03`, `10 11 03`, `10 50 03`) but no
settings write on any of them.
- The vendor's device DLL (`data\2C8E\2C8E.dll`) is a hidapi fork whose
`hid_write` is called in exactly four places, all in the calibration
module; there is no settings-write code in it.

Read together: on this unit a settings write from a plain user-mode HID
handle is acknowledged but not applied, and the vendor applies settings
through a channel a user-mode capture cannot see. The driver's write frames
are therefore best described as **unverified on hardware**, not verified.

## Backing files

Raw listener logs from this session (not shipped here): the awake stream, the
125 Hz differential, and the status probes. The status probe while the mouse
was asleep answers `90 0a 03 00 ec fa 0d 00` with an all-zero body, versus
`90 0a 04 00 00 64 …` when awake — the same empty/no-data dichotomy the
settings echo shows, and why the Swarm device page sits on its loading
spinner and shows placeholder 0 % battery until the mouse answers.
47 changes: 47 additions & 0 deletions captures/roccat-kone-pro-air/session-2026-10-14.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Probe session transcript — byte-precise, 2026-10-14 (mouse idle this time)
# node-hid, dongle interface 2. OUT lines are the 64-byte output report payload
# as written (the leading 0x00 is the unnumbered report id node-hid consumes);
# IN lines are the interrupt-IN echo, trailing zero bytes trimmed.
#
# The mouse was NOT moved during this session (idle). Note the settings
# query echoing an EMPTY body — with the mouse awake an earlier session
# returned the full table (see transcript-earlier-session.txt).

== 1. current settings query ==
OUT 00 10 50 14
IN 10 50 03 00 ec fa 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

== 2. status probe ==
OUT 00 90 0a
IN 90 0a 04 00 00 14 01 00 44 0b 1c 00 00 00 00 00 00 00 00 00 00 00 00 00

== 3. settings write (axis-slot pair: even 16/24/32/64,100 odd 8/16/24/32,64) ==
OUT 00 10 50 14 00 01 00 00 00 10 08 18 10 20 18 40 20 64 40 06 00 80 0f 00 0a
IN 10 50 03 00 ec fa 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

== 4. commit tail ==
OUT 00 10 50 18 04
IN 10 50 03 00 ec ac 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

== 5. settings query after the round trip ==
OUT 00 10 50 14
IN 10 50 03 00 ec fa 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

== 6. lighting write + status ==
OUT 00 10 10 0b 00 09 64 64 64 06 00 00 00 00 00 00
IN 10 10 03 00 ec ac 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
OUT 00 90 0a
IN 90 0a 04 00 00 14 01 00 45 0b 1c 00 00 00 00 00 00 00 00 00 00 00 00

== 7. restore (the mouse's idle state had presented an empty table, so the
restore wrote the same empty-table shape back) ==
OUT 00 10 50 14 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 06 00 80 0f 00 0a
IN 10 50 03 00 ec ac 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
OUT 00 10 50 18 04
IN 10 50 03 00 ec ac 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
OUT 00 10 50 14
IN 10 50 03 00 ec fa 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

# Post-session note: bytes 8..9 of the status echo (0x0b44 = 2884) had INCREASED
# since the discovery session (0x0aa0 = 2720), falsifying the "estimated minutes
# remaining" reading. Byte 5 moved 0x1e -> 0x14; its meaning is unestablished.
29 changes: 29 additions & 0 deletions captures/roccat-kone-pro-air/transcript-earlier-session.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Discovery session outline — module-earlier capture (mouse awake)
# Same node-hid shape as session-2026-10-14.txt, from the session during which
# the settings query DID return the twenty-slot stage table. Frames trimmed the
# same way (trailing zeros cut). Numeric outline of the session, in order:
#
# OUT 00 90 0a
# IN 90 0a 04 00 00 1e 00 00 a0 0a 1e (first poll)
# IN 90 0a 04 00 00 1e 00 00 a0 0a 1e (second poll)
# OUT 00 10 50 14 00 01 <spinning write> ... (settings write)
# IN 10 50 03 00 ec fa 04 (write echo)
# OUT 00 10 50 18 04 (commit tail)
# IN 10 50 03 00 ec ac (commit echo)
# OUT 00 90 0a
# IN 90 0a 04 00 00 1e 00 00 9a 0a 1e (post-write poll)
# OUT 00 10 10 0b 00 09 64 64 64 06 00 00 00 00 00 00 00
# IN 10 10 03 00 ec ac (lighting echo)
#
# The discovery session's settings QUERY (a distinct, later probe) echoed:
# IN 10 50 03 00 ec fa 0d 00 10 10 18 18 20 20 40 40 64 64 01 02 00 00
#
# decoded: byte2=0x03, both slot lists identical —
# offsets 8..17 = 10 10 18 18 20 20 40 40 64 64
# = five entries of 800/1200/1600/3200/5000 (dpi/50) x 2 axes,
# matching the unit's THEN-current Swarm settings, not the factory:
# evidence that the echo table reflects live on-device state.
#
# Later idle-session query echoes only returned the empty body shape
# (`10 50 03 00 ec fa 04` then zeros), so the table's appearance is
# mouse-state-dependent.
Loading
Loading