Skip to content

atk: decode PAW3395 DPI correctly and identify VXE mice by CID/MID - #44

Open
SakethKanchi wants to merge 1 commit into
OpenMouse-Project:mainfrom
SakethKanchi:feat/vxe-r1-paw3395
Open

atk: decode PAW3395 DPI correctly and identify VXE mice by CID/MID#44
SakethKanchi wants to merge 1 commit into
OpenMouse-Project:mainfrom
SakethKanchi:feat/vxe-r1-paw3395

Conversation

@SakethKanchi

@SakethKanchi SakethKanchi commented Sep 4, 2026

Copy link
Copy Markdown

What this fixes

The ATK driver applied the A9's PAW3950Ultra DPI encoding to every device behind
vendor id 0x373b. ATK HUB Web 3.2.21 branches that decoder per sensor:

[PAW3950, PAW3950DM, PAW3395Ultra, PAW3395].includes(sensor)
  ? 50 * (1 + (x | ((12 & ex) >> 2) << 8))          // flat 50-DPI steps
  : PAW3950Ultra
    ? (2 & ex ? 50 * v + 10050 : 10 * (v + 1))      // what this repo implemented
    : /* lookup tables for PAW3395SE / PAW3315 / PAW3311 / PAW3320 */

A PAW3395 stage read with the A9 encoding is a fifth of the real value: my
1,600 DPI stage reported as 320.

Why a product-id map would have been wrong

A receiver's product id cannot tell you the sensor. 0x373b:0x1085 is a generic
"Wireless mouse 1k dongle" reused across models, and the R1 alone ships with
six different sensors
across revisions (PAW3311, PAW3395, PAW3395SE,
PAW3395Ultra, PAW3950Ultra, CORE26K) — with different encodings and ceilings.
Even the model name is not enough.

So this identifies the mouse the way the vendor's own HUB does: GetMouseCIDMID
(command 0x10, from the HUB's command enum, which also confirms this repo's
existing 0x04/0x08/0x12), looked up in a catalog keyed "<cid>,<mid>" that
carries the sensor.

Changes

  • per-sensor profiles and the step-50 codec in src/atk
  • src/drivers/atk/products.ts, keyed "<cid>,<mid>"
  • DPI encoding, ceiling and ladder come from the sensor, for reads and writes
    alike
    — a sensor-aware decode with a legacy encode would corrupt setDpi
  • an unidentified mouse keeps the A9 behaviour, matching the HUB's own fallback,
    so no existing ATK device changes behaviour
  • VXE reported as its own brand via deviceBrand(), as Lamzu/CRDRAKO already do
  • VXE's wired transport (0x3554:0xf58f, "Compx VXE R1") claimed — same
    0xff02/report-0x08 channel, previously unsupported because isSupported()
    gated on vendorId === atk. Product-id gated, since 0x3554 is shared with the
    VGN Dragonfly F2; the id also joins the Pulsar fallback's
    CLAIMED_VGN_PRODUCT_IDS (the registry overlap test caught this immediately —
    vid 0x3554 pid 0xf58f: Pulsar + ATK)
  • an unprogrammed angle register is treated as unsupported. 0x00bd reads
    ff ff ff ff and fails the (value, 0x55-value) pair, but decoded as -1
    degrees
    of angle tuning
  • identification retries instead of caching a timeout: a sleeping 2.4 GHz mouse
    answers nothing, and caching that left a woken mouse misnamed and mis-decoded
    for the rest of the session

Battery

GetBatteryLevel carries a charge flag and a cell voltage next to the
percentage, so batteryState and batteryVoltageMv are now reported instead of
a hardcoded "Unknown". Captured across a charge cycle: flag 0 with a steady
3,786 mV on battery, flag 1 with the voltage climbing 3,893 → 3,938 mV on the
cable, and flag 0 again at 3,875 mV after unplugging. A non-zero flag means
charging; the HUB tests === 2 on some families, which is also non-zero.

Note the percentage legitimately drops on unplug (70% at 3,969 mV charging vs
50% at 3,875 mV resting): the mouse derives percent from cell voltage, and a
charging cell reads high. That is the mouse's own reporting, documented so it is
not mistaken for a decode bug.

Deliberately not implemented

Sensors whose DPI mapping is a lookup table (PAW3395SE, PAW3315, PAW3311,
PAW3320). Those tables are not captured here, and a guessed step would misreport
DPI silently. Mice with those sensors keep the existing fallback.

Polling rates are left at the full 125-8,000 Hz ladder. The R1 ships with a 1K
receiver but an 8K receiver is sold separately and works with the same mouse, so
the ceiling belongs to the receiver in use, not the model. Capping by product id
would be wrong.

Hardware verification

VXE R1, CID/MID 2,12, sensor PAW3395, firmware Mouse 3.13, on Linux via
/dev/hidraw* and in Chrome via WebHID. Verified over both transports
(0x373b:0x1085 receiver and 0x3554:0xf58f wired), reporting the same identity,
DPI and firmware through each:

before after
brand ATK VXE
name ATK Wireless mouse -1k dongle VXE R1
DPI 320 1600
max DPI 42000 30000
angle tuning -1 null (unsupported)

DPI stages at EEPROM 0x000c..0x001b read 0f 0f 00 37 / 17 17 00 27 /
1f 1f 00 17 / 3f 3f 00 d7800 / 1200 / 1600 / 3200 DPI, which the mouse's
own DPI button cycles through. Cycling to stage 1 on hardware moved the UI to
1,200 DPI, matching (0x17 + 1) * 50.

Firmware, lift-off, debounce, motion sync, ripple control and sleep all read
correctly; battery is covered above. Writes were not exercised, so verified: true in the catalog
records the identity and read path only. Details in docs/atk-testing.md.

npm run check passes (818 tests). OpenMouse builds and its full suite passes
against this package locally (112 tests); the matching app-side PR is linked below.

Unknowns left documented

  • Byte 3 of the GetMouseCIDMID reply (0x47 on this unit) is unidentified.
  • 0xf58f is reused by the vendor for the R1SE/R1SE+ (PAW3395SE), which is why the
    sensor must come from CID/MID and not that id.
  • No product photo: device-images.ts matches by display-name regex and has no R1
    asset, so VXE R1 falls back to unknown-device.png. I did not point it at
    another model's render.

The ATK driver applied the A9's PAW3950Ultra DPI encoding to every device
behind vendor id 0x373b. ATK HUB Web 3.2.21 branches that decoder per
sensor: PAW3950/PAW3950DM/PAW3395/PAW3395Ultra pack a plain 10-bit count
of 50-DPI steps instead. A PAW3395 stage read with the A9 encoding is a
fifth of the real value.

A receiver's product id cannot say which sensor a mouse has: 0x373b:0x1085
is a generic "Wireless mouse 1k dongle" reused across models, and the R1
alone ships with PAW3311, PAW3395, PAW3395SE, PAW3395Ultra, PAW3950Ultra
and CORE26K. So identify the mouse the way the vendor's own HUB does, with
GetMouseCIDMID (command 0x10), and look the pair up in a catalog that
carries the sensor.

- add per-sensor profiles and the step-50 codec to src/atk
- add src/drivers/atk/products.ts, keyed "<cid>,<mid>"
- pick the DPI encoding, ceiling and DPI ladder from the sensor, for reads
  and writes alike; an unidentified mouse keeps the A9 behaviour, matching
  the HUB's own fallback
- report VXE as its own brand via deviceBrand(), as Lamzu/CRDRAKO already do
- claim VXE's wired transport (0x3554:0xf58f, "Compx VXE R1"), which speaks
  the same 0xff02/report-0x08 channel. Product-id gated: 0x3554 is shared
  with the VGN Dragonfly F2, so the id also joins the Pulsar fallback's
  CLAIMED_VGN_PRODUCT_IDS
- decode the rest of GetBatteryLevel: the reply carries a charge flag and a
  cell voltage next to the percentage, so report batteryState and
  batteryVoltageMv instead of a hardcoded "Unknown"
- treat an unprogrammed angle register as unsupported. It reads 0xff and
  fails the (value, 0x55-value) pair, but decoded as -1 degrees
- retry identification instead of caching a timeout, so a mouse that was
  asleep is not left misnamed and mis-decoded for the session

Sensors whose DPI mapping is a lookup table (PAW3395SE, PAW3315, PAW3311,
PAW3320) are deliberately not implemented; those tables are not captured
here and a guessed step would misreport DPI silently.

Verified on a VXE R1 (CID/MID 2,12, PAW3395, firmware Mouse 3.13) over both
transports: DPI now reads 1600 rather than 320, ceiling 30000 rather than
42000, and the mouse names itself "VXE R1". Battery was captured across a
charge cycle: flag 0 with a steady voltage on battery, flag 1 with the
voltage climbing on the cable. Writes were not exercised.
See docs/atk-testing.md.
@SakethKanchi

Copy link
Copy Markdown
Author

App-side counterpart: OpenMouse-Project/openmouse#156 (adds the supported-devices row). This one should land first — until it does, that row is only true against a local build.

@logm1lo

logm1lo commented Sep 5, 2026

Copy link
Copy Markdown

Continued this work in #53 against current main. #53 retains your original commit in its history, credits you as co-author, adds the verified PAW3395SE R1 SE+ path, resolves the live-settings conflicts, and includes wired read/write hardware validation.

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.

2 participants