Skip to content

feat(rapoo): add the VT9 Pro driver for the 0x12xx/0x44xx generation - #147

Merged
snekxs merged 3 commits into
OpenMouse-Project:mainfrom
LinkPass888:feat/rapoo-vt9-pro
Sep 30, 2026
Merged

snekxs merged 3 commits into
OpenMouse-Project:mainfrom
LinkPass888:feat/rapoo-vt9-pro

Conversation

@LinkPass888

@LinkPass888 LinkPass888 commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Device

Model Rapoo VT9 Pro (1st gen)
USB IDs 0x24AE:0x1205 (2.4 GHz receiver) and 0x24AE:0x4405 (cable)
Connection both. Every register below was read over both paths and the two agree byte for byte
Config collection usagePage 0xFF00, usage 0x000E
Request output report id 0xBA, 31 bytes of report data
Answer fetched with GET_REPORT(Input), not delivered as an interrupt IN report

Why this driver is read-only in a browser

The answer to a read never arrives on its own: it has to be fetched with
GET_REPORT(Input), which is HidD_GetInputReport on Windows, HIDIOCGINPUT
on Linux hidraw and receiveInputReport in OpenMouse Bridge. WebHID has no such
call, so with WebHID alone the register channel is unreachable - not broken, just
unreadable. The client therefore reads its registers when the transport exposes
receiveInputReport and, when it does not, reports only the battery the mouse
pushes on its own (0xBB, FF00:0002, about every 3 s:
B0 51 20 03 <link> <percent>). It does not pretend the registers are readable.

What this adds

  • src/rapoo/index.ts - the codec: frame layout, commands, the busy/ok answer,
    the battery marker, and decoders for the performance, sensor, DPI, stage and
    timing blocks plus the 0xBB notification.
  • src/drivers/rapoo/hid.ts - RapooHidClient: discovery of the config
    collection (including inside a child collection), the busy -> ok handshake,
    per-read resend, the register walk, and the fallback to the notification.
  • Registry, vendor filter, package export, src/index.ts, README and tsconfig
    wiring.
  • 35 tests over both files: captured packets, truncated and busy answers, a
    dropped frame that is sent again, a register the first pass lost that is read
    again, every decoder, and the browser-only path that sends nothing.
  • docs/rapoo-vt9-pro-testing.md and the sanitized capture in
    captures/rapoo-vt9-pro/.

Evidence

  • a capture of this mouse on 2026-09-29, over the receiver and over the cable
    (captures/rapoo-vt9-pro/sweep-2026-09-29.txt);
  • mousectl's rapoo_vt3pro driver, reverse engineered from Rapoo's own
    RapooGameDevDriver 1.6.29 - the same installer build the capture was
    taken with. Same frame, same offsets, for the VT3 PRO;
  • rapoo-software-linux's PROTOCOL.md (from A HUB 1.0.19) describes a third
    Rapoo generation whose addresses line up after subtracting the profile-0 base
    of 0x600.

The three agree on the channel; everything the driver decodes was then measured
on the mouse.

Verified on hardware

readStatus() on this mouse, over the cable and over the receiver, 14 runs with
the same result:

Polling rate 4000 Hz
DPI 800 (stage 2 of 7: 400/800/1200/1600/3200/6400/26000)
Motion sync on
Debounce 16 ms
Sleep 7200 s
Angle snapping / ripple control off
Battery 100 %
Read-back verification valuesVerified: true

Two details a reviewer should know

  • The frame is 31 bytes of report data. HidP_GetCaps reports 32 because it
    counts the report id; the browser rejects a 32-byte payload as oversized and
    answers the 31-byte one.
  • Feature report 0x2B is never read. Reading it once drops the link until
    the cable is pulled (0xC00000E5), so it is refused in the client and excluded
    from the probes. It is a distinct report id from the 0x2A status block.

Safety

The client has no write path at all - it encodes reads, the battery query and
nothing else - and the register walk is idempotent.

Companion PR

OpenMouse-Project/openmouse#422 wires RapooHidClient into the controller
so the device is opened on connect. It needs @openmouse/protocol 0.20.0 or
newer; the OpenMouse repository bumps that dependency from the published release,
so it should land after this one is released.

The VT9 Pro (0x1205 receiver, 0x4405 cable) answers on its own vendor
collection FF00:000E: a 0xBA output frame carries the register address,
and the answer to it only ever shows up in an input report. WebHID has
no GET_REPORT(Input), so this driver reads its registers through the
transport when it exposes receiveInputReport (OpenMouse Bridge) and, when
it does not, reports the battery the mouse pushes on 0xBB by itself
rather than pretending the registers are readable.

Every byte in src/rapoo and src/drivers/rapoo was read off one mouse with
HidD_GetInputReport - wired and through the receiver, both links
answering byte-for-byte the same - and cross-checked against the vendor
driver (RapooGameDevDriver 1.6.29) and pedro3z0/rapoo-software-linux.
The frame length is 31 bytes of report data behind the report id; Windows
counts the id in the 32-byte buffer, and the browser rejects 32 as
oversized.

Reads only: the driver has no write path, and it never asks for feature
report 0x2B, which drops the link until the cable is pulled.

Verified on hardware: polling 4000 Hz, DPI 800 of 7 stages (stage 2),
motion sync on, debounce 16 ms, sleep 7200 s, battery 100 %, over both
USB paths.
@snekxs
snekxs merged commit ecfe788 into OpenMouse-Project:main Sep 30, 2026
5 checks passed
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 0.21.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants