Skip to content

Match the X2A family's hidraw node on PID too - #17

Merged
packerlschupfer merged 1 commit into
packerlschupfer:mainfrom
f-o-o-g-s:hidraw-match-pid
Oct 14, 2026
Merged

packerlschupfer merged 1 commit into
packerlschupfer:mainfrom
f-o-o-g-s:hidraw-match-pid

Conversation

@f-o-o-g-s

Copy link
Copy Markdown
Contributor

With the X3 on its cable and the 1K dongle still plugged in, the DPI button stopped reaching the app after a swap to the cable. find_hidraw() in x2a.py only checks the VID, and the Fusion2 mice and the 1K dongle all share 3710, so it returned the dongle's interface 1 (hidraw1 sorts before hidraw11).

This copies the VID+PID match nordic.py already does. It covers the X2A, Xlite v4, X3 and the 1K dongle since they all inherit it.

Tested with my X3 and 1K dongle, app running as my user:

  • cabled, dongle still in: before, the listener sat on hidraw1 and presses went nowhere. After, it's on hidraw11 and presses come through (800, 1600)
  • dongle after unplugging the cable: listener on hidraw1, all 6 presses heard
  • check-drivers.py: 7/7

feinmann8k.py's find_hidraw() has the same VID-only match. I don't have that dongle so I left it out, but it's the same change if you want it.

With the X3 cabled and the 1K dongle still plugged in, find_hidraw()
returned the dongle's interface 1, since both are VID 3710 and hidraw1
sorts before hidraw11. After a swap to the cable the DPI button went
unheard. Same VID+PID match nordic.py already uses.
@packerlschupfer
packerlschupfer merged commit 9726097 into packerlschupfer:main Oct 14, 2026
1 check passed
packerlschupfer added a commit that referenced this pull request Oct 14, 2026
#17 fixed x2a.py: matching the hidraw node on the VID alone picks whichever
node sorts first, so with an X3 cabled and the 1K dongle still plugged in the
listener sat on the dongle and the DPI button went unheard. nordic.py already
matched on VID and PID. feinmann8k.py did not - it matched the VID as a
substring of the whole uevent file - and it shares VID 0x3710 with every mouse
that triggered this, so it had the same bug waiting.

Rather than a third copy of the same loop, the matching moves to
PulsarDevice.find_hidraw() and each driver declares which interface carries
its reports:

    _HIDRAW_INTERFACE = '/input1'

find_hidraw() takes a `root`, which is what makes this testable: the new
check in tools/check-drivers.py builds a sysfs tree holding every declared
VID:PID at once - a mouse, its dongle, and someone else's Pulsar, the
situation no single desk reproduces - and asserts each driver picks its own
device's node. The node numbering keeps the lexicographic sort the bug relied
on (hidraw1 before hidraw11), and every device gets an input0 node too, so a
driver that stopped checking the interface fails as well.

Fault-injected to confirm the check is worth having: restoring the VID-only
match fails it on seven drivers and exits non-zero.

The feinmann8k change is the one nobody can test - that model is harveywuk's
and no one here has one. It is strictly narrower than what it replaces, and
the comment there says where to look if its events ever stop arriving.

Verified on the X2A: find_hidraw() still returns /dev/hidraw5, the same node
as before, and check-drivers is 8/8.
@packerlschupfer

Copy link
Copy Markdown
Owner

Merged, thanks — and it was latent on the X2A too: 3710:1404 shares the VID with the whole Fusion2 line, so the only reason it never bit here is that mine is the only Pulsar device on this machine.

I took the match up into PulsarDevice.find_hidraw() afterwards, because feinmann8k.py had the same bug — it matched the VID as a substring of the whole uevent file — and a third copy of the loop seemed the wrong direction. Each driver now declares which interface carries its reports:

_HIDRAW_INTERFACE = '/input1'

find_hidraw() takes a root, which is what made your collision testable without your hardware: check-drivers.py now builds a sysfs tree holding every declared VID:PID at once — a mouse, its dongle, and someone else's Pulsar — and asserts each driver picks its own device's node. The numbering keeps the lexicographic sort the bug relied on (hidraw1 before hidraw11), and each device gets an input0 node too, so a driver that stopped checking the interface fails as well. Restoring the VID-only match fails it on seven drivers, which is the only reason I trust it. The feinmann8k part stays untested — neither of us has that mouse — and the comment there says where to look if its events ever stop arriving.

On the stage byte: the X2A is 0-based, so the +1 is right for it and your X3 is the one that differs. Five presses here, against a 6-stage table of 400/800/1600/3200/6400/12800:

byte2=0x02  dpi=1600   -> stage 3
byte2=0x03  dpi=3200   -> stage 4
byte2=0x04  dpi=6400   -> stage 5
byte2=0x05  dpi=12800  -> stage 6
byte2=0x00  dpi=400    -> stage 1

So this wants an override in x3.py, not a change to the shared parser. Before I write that on one sample, could you run tools/hidraw-listen.py (read-only, never writes to the mouse — close the GUI first so the reports come to it) and press the DPI button once per stage through the whole loop? The 05 05 ... lines next to your stage table settle it. If it's 1-based the whole way round, the override is two lines.

@f-o-o-g-s

Copy link
Copy Markdown
Contributor Author

Ran it on both connections. It's 1-based the whole way round on both. Six presses each, same table (400/800/1600/3200/6400/12800):

dongle (3710:5402, hidraw1)        cable (3710:3409, hidraw12)
byte2=0x04  dpi=3200   stage 4     byte2=0x04  dpi=3200   stage 4
byte2=0x05  dpi=6400   stage 5     byte2=0x05  dpi=6400   stage 5
byte2=0x06  dpi=12800  stage 6     byte2=0x06  dpi=12800  stage 6
byte2=0x01  dpi=400    stage 1     byte2=0x01  dpi=400    stage 1
byte2=0x02  dpi=800    stage 2     byte2=0x02  dpi=800    stage 2
byte2=0x03  dpi=1600   stage 3     byte2=0x03  dpi=1600   stage 3

So the dongle passes the same 1-based byte through, which means xlite_v4_wireless.py needs the override too, at least with an X3 behind it. I can't tell you what an Xlite V4 sends through the same dongle. The X3 has no profile button. Full logs: https://gist.github.com/f-o-o-g-s/486cddaa5fe0f188c0ea9e748c35fbfc

A few other things showed up in the logs, in case they're useful for #7:

  • 04 20 / 04 00 on interface 1 with every DPI press and release. I bumped another button by accident twice and that came through as 04 08, plus a button bit on the pointer interface.
  • 05 07 <charging> <percent> 01 looks like an unprompted battery report. The dongle sent 05 07 00 50 01 (80%, not charging) right after the mouse reconnected, and the cable sent 05 07 01 51 01 (81%, charging). Both matched get_power() at the time.
  • For the "away" step I walked out and back within 15 s. Link quality dropped to 76, then nothing for about 7 s, and the dongle sent 05 08 02 01 and then 05 08 01 01 around that gap. Looks like link lost and back, but that's one walk, so it's a guess.

Also, a59543f stops the GUI's listeners from starting on every driver: no DPI updates in the tray and no Connection Quality on Home. Details and a fix in #19.

packerlschupfer added a commit that referenced this pull request Oct 14, 2026
The X2A's DPI button reports its six stages as bytes 0x00 to 0x05, so the
parser added one and every Sonix driver inherited that.  The X3 reports
0x01 to 0x06 for the same six stages - @f-o-o-g-s pressed it six times on
the cable and six more through the 1K dongle and got the identical table
both ways (#17), and six presses of the X2A's own button here walked
0x00..0x05, so the difference is real and not a misread.

Left as it was, the X3 showed the next stage up in the tray and in Input
Test, and its top stage showed as a seventh that does not exist.  The
dongle relays the mouse's byte untouched, so it needs the same answer as
whatever mouse is behind it; the X3 is the only one confirmed there.

The Xlite v4 keeps the X2A's numbering, as it keeps everything else about
the X2A - that driver has never been run against hardware.  Its docstring
now says this is a thing to check if its DPI button reads one stage high.
@packerlschupfer

Copy link
Copy Markdown
Owner

Both tables land on 1–6 now. parse_hidraw_event() keeps the arithmetic and takes the first-stage number from the driver: X2A 0, X3 1, 1K dongle 1 (c3667a4).

The dongle is the guess of the three. It relays the mouse's byte untouched, so the numbering is really the mouse's, and the X3 is the only mouse confirmed behind it. If a Fusion2 mouse turns up that counts from zero, that constant has to become a get_connected_mouse() lookup — the comment says so.

The Xlite v4 keeps the X2A's 0, like everything else in that driver; it has never met hardware. Its docstring now points here if its DPI button ever reads one stage high.

The other three report shapes are worth more than a comment on a merged PR — #21.

@f-o-o-g-s
f-o-o-g-s deleted the hidraw-match-pid branch October 25, 2026 15:19
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