Skip to content

feat(wlmouse): DPI stage table, high-speed, turbo, angle tune, button combinations, and the mouse behind a receiver - #52

Open
ydw1904 wants to merge 2 commits into
OpenMouse-Project:mainfrom
ydw1904:feat/wlmouse-settings
Open

feat(wlmouse): DPI stage table, high-speed, turbo, angle tune, button combinations, and the mouse behind a receiver#52
ydw1904 wants to merge 2 commits into
OpenMouse-Project:mainfrom
ydw1904:feat/wlmouse-settings

Conversation

@ydw1904

@ydw1904 ydw1904 commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Two commits that together bring the WLmouse driver up to what the vendor's own
configurator exposes: the full DPI stage table, four sensor settings the driver
could not reach, and the name of the mouse sitting behind a shared receiver.

WLmouse DPI stage editor, six stages read off a Beast Max 1K

1. The DPI stage table (873597a)

These mice hold six onboard stages and send the whole table in one packet, and
the driver has always decoded it — but only the active stage reached the app, so
OpenMouse could edit one DPI value where the vendor software edits six. The
control app already ships a generic stage editor for this exact shape (Keychron
M6 and Teevolution use it), so the driver now reports dpiStages,
activeDpiStage and the dpiStageEditor hint, plus the three setters that
editor calls.

setDpiStageCount and setDpiStageValue reuse the write setDpi was already
making: count and stages travel together in one packet, so a single-value edit
is a full table write either way. That write and its read moved into
writeStages/readStages, which setDpi now uses too.

Editing one stage keeps a Y that already differs from X — the shared editor
shows a single value per stage and must not quietly flatten a separate-axis
setup.

One inferred byte. setActiveDpiStage writes profile-page command 0x02,
which is not in any capture. It is the 0x82 read with the high bit cleared,
which is how every other command in this driver pairs up (0x80/0x00,
0x81/0x01, 0x84/0x04, 0x88/0x08, 0x89/0x09, 0x8a/0x0a). The
write is verified by reading the active stage back, and the mouse answers an
unknown command with unsupported, so a wrong byte surfaces as a plain error
rather than a silent no-op. A second pair of hands on hardware would be welcome
here.

2. Sensor settings and the mouse behind the receiver (89c8f97)

Read out of the vendor's web tool, which speaks the same page-command framing —
nothing guessed:

setting page read / write
high-speed mode 0x01 0x8b / 0x0b
turbo mode 0x01 0x93 / 0x13
angle tune (signed byte) 0x01 0x94 / 0x14
button combinations 0x03 0x81 / 0x01
paired product id 0x00, target 0x01 0x8b, arg 0x02

The four settings are optional per model: a mouse without one answers
unsupported rather than failing the exchange, so the reads catch into null and
the status reports "no such control" instead of breaking the connection. Turbo
needs high-speed mode on before the firmware will hold it, which the read-back
reports rather than silently dropping.

The paired-product read matters most for the shared 1K receiver, which
enumerates under one product id whatever it is paired with. Without it every
mouse on that dongle reports as "WLmouse 1K receiver"; with it the driver names
the model and the app can pick artwork for it. The doubled "WLmouse WLmouse"
prefix on the shared-receiver catalogue entries goes too.

Testing

  • npm run check clean, npm test 832 passing.
  • New tests in src/drivers/wlmouse/hid.test.ts cover the stage table round
    trip, the Y-preservation rule, the rejection paths, and the optional settings
    degrading to null, against a fake device that models a mutable stage table.
  • Hardware: a Beast Max on the 1K receiver (36a7:a882), reporting the same six
    stages its vendor software shows (screenshot above).

App side: OpenMouse-Project/openmouse#162.

Per-stage DPI colours are the obvious follow-up — the command carrying them is
still unknown, so they are deliberately out of scope.

🤖 Generated with Claude Code

ydw1904 and others added 2 commits September 5, 2026 10:42
The mouse has always sent its whole stage table in one packet and the
driver has always decoded it, but only the active stage reached the app,
so the panel could edit one DPI value where the vendor software edits
six. The control app already ships a generic stage editor for exactly
this shape (Keychron and Teevolution use it), so this reports
`dpiStages`, `activeDpiStage`, and the `dpiStageEditor` hint, and adds
the three setters that editor calls.

`setDpiStageCount` and `setDpiStageValue` reuse the write `setDpi` was
already making: the count and every stage travel together in one packet,
so a value edit is a full table write either way. Editing a stage keeps
a Y that already differs from X, because the shared editor shows a
single value per stage and must not quietly undo a separate-axis setup.

`setActiveDpiStage` writes a command that is not in any capture: it is
the `0x82` read with the high bit cleared, which is how every other
command in this driver pairs up. That guess is verified by reading the
active stage back, and an unknown command comes back `unsupported`, so a
wrong byte surfaces as an error rather than a silent no-op. It wants a
run on real hardware before anyone trusts it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
WLmouse's own configurator exposes four sensor settings this driver did not
reach, and names the mouse behind a receiver that the driver could not name.
All five commands are read out of the vendor's web tool, which uses the same
page-command framing this driver already speaks, so nothing here is guessed:

- high-speed mode  page 0x01, read 0x8b / write 0x0b
- turbo mode       page 0x01, read 0x93 / write 0x13
- angle tune       page 0x01, read 0x94 / write 0x14, signed byte
- button combos    page 0x03, read 0x81 / write 0x01
- paired product   target 0x01, page 0x00, read 0x8b, arg 0x02

The four settings are optional per model: a mouse without one answers
`unsupported` rather than failing the exchange, so the reads catch into null
and the status reports "no such control" instead of breaking the connection.

The paired-product read matters most for the shared 1K receiver, which
enumerates under one product id whatever mouse it is paired with. Without it
every mouse on that dongle reports as "WLmouse 1K receiver" — with it the
driver names the model, and the app can pick artwork for it. The doubled
"WLmouse WLmouse" prefix on the shared-receiver catalogue entries goes too.

Turbo needs high-speed mode on before the firmware will hold it, which the
read-back reports rather than silently dropping.

Verified against a Beast Max on the 1K receiver.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ydw1904 ydw1904 changed the title wlmouse: DPI stages, high-speed, turbo, angle tune, button combinations, and the mouse behind a receiver feat(wlmouse): DPI stage table, high-speed, turbo, angle tune, button combinations, and the mouse behind a receiver Sep 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant