Uh oh!
There was an error while loading. Please reload this page.
Support the Lingbao M5 Pro (PAW3395), and stop the Fantech driver inventing readings - #49
Closed
phunb3101 wants to merge 1 commit into
Closed
Support the Lingbao M5 Pro (PAW3395), and stop the Fantech driver inventing readings#49phunb3101 wants to merge 1 commit into
phunb3101 wants to merge 1 commit into
Conversation
The Lingbao M5 Pro (PixArt PAW3395) enumerates under VID 0x3151 on the very same 0xFFFF/0x02 vendor interface FantechHidClient claims, so the registry handed it to that driver — which could not read a single value from it, and reported fabricated ones rather than failing. 0x3151 belongs to MicLink/mlzn, the ODM, not to Fantech. The mouse is configured by GearHub-V5 (qmk.top); its protocol was read out of that bundle and then verified byte for byte against real hardware. LingbaoHidClient implements the three things the Fantech driver lacks: - The Bit7 checksum, byte[7] = 255 - sum(bytes 0..6). Without it the device ACKs the write and answers 64 zero bytes, silently. - The 2.4G relay. A receiver does not forward a command just because one was written to it: select the target (0xF6 0x05), poll status until ready (0xF7), send, then notice-read (0xFC) before reading back. 0xF7 also carries link state and battery. - The mouse-class command set (GET_DPI 0xD4 / SET_DPI 0x54) and its seven-entry report-rate table, in which code 5 means 250 Hz. The Fantech driver carries the six-entry keyboard table, where code 5 means 125 Hz instead. Commands are serialised through a queue. The relay is stateful, so two exchanges in flight at once interleave and corrupt each other — readStatus() ran into this on its first concurrent read. Scoped to the M5 Pro's two product ids (0x402D receiver, 0x4026 wired) so it cannot shadow Fantech's own hardware, and FantechHidClient now leaves those two ids alone; the registry's one-driver-per-device test enforces it. Verified live on a Lingbao M5 Pro receiver: DPI stages 400/800/1600/3200/ 6400/26000 with stage 2 active, 8000 Hz, battery 44%, firmware v3.03, device id 2285. Bluetooth mode is not covered — over BLE the mouse moves to a different usage page and a separate read path. The same investigation showed FantechHidClient inventing data on any device that does not answer, so this also fixes: - readStatus() throws instead of returning 1600 DPI / 8000 Hz decoded from an all-zero reply, letting the caller try the next driver - setDpiForSlot() no longer zeroes every DPI slot it is not writing - 250 Hz is no longer advertised when it cannot be encoded, so the UI cannot render a button that throws on click - the hardcoded "Wired (USB)" and the placeholder firmware string are gone rather than mislabelling wireless units Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds support for the Lingbao M5 Pro (PixArt PAW3395), and fixes the
fantechdriver, which this work showed was reporting invented values.Why the M5 Pro did not work
It enumerates under VID
0x3151on the very same vendor interfaceFantechHidClientclaims — usage page0xFFFF, usage0x02, one unnumbered64-byte feature report — so
driverFor()handed it to that driver. Every readcame back as 64 zero bytes, and the driver decoded those zeros into a
plausible-looking mouse instead of failing.
0x3151is not Fantech's vendor id. It belongs to MicLink/mlzn, an ODMwhose devices are configured by GearHub-V5. The
protocol below was read out of GearHub's own web bundle and then verified byte
for byte against real hardware.
What the Fantech driver was missing
fantechhasbyte[7] = 255 - sum(bytes 0..6)GET_DPI 144/SET_DPI 160xD4/0x54Omit any one of these and the device ACKs the
SET_FEATUREand answers 64zeros — silently, with no error. That is indistinguishable from hardware that
does not speak the protocol at all, which is why this went unnoticed.
The relay is the part that is easy to miss. A receiver does not forward a
command just because one was written to it:
F7also carries link state and battery, so battery comes free and is a realreading rather than a capability guess.
The new driver
LingbaoHidClient, scoped to the M5 Pro's two product ids —0x402D(2.4G receiver, relayed) and
0x4026(wired, direct) — so it cannot shadowFantech's own hardware.
FantechHidClientnow leaves those two ids alone, andthe registry's existing one-driver-per-device test enforces the split.
Commands are serialised through a queue: the relay is stateful, so two
exchanges in flight at once interleave and corrupt each other.
readStatus()ran into this on its first concurrent read, which is worth calling out because
it is invisible in any single-command test.
Reads: DPI stages + active stage + per-stage indicator colour, report rate,
battery, link state, firmware, device id. Writes: DPI (echoing back every
stage it is not changing) and report rate.
Verified live
Against a real M5 Pro receiver, driving the actual
LingbaoHidClientthrougha WebHID shim over
node-hid:An idle M5 Pro drops off its receiver within minutes;
readStatus()reportsthat as its own error ("no mouse is linked … move the mouse and try again")
rather than as a protocol failure, which the run above exercised before the
mouse woke.
Fantech fixes carried in the same change
The investigation showed
FantechHidClientfabricating data on any devicethat does not answer, so:
readStatus()now throws instead of returning1600 DPI / 8000 Hz / Wired (USB)decoded from an all-zero reply, letting the caller try the next driversetDpiForSlot()no longer zeroes every DPI slot it is not writing — it readthe current table and then discarded it
> 8where the layout holds 8 slots (indices 0-7), and theclamp was applied to the reported index but not to the byte offsets
button per advertised rate, so that button threw on click
"Wired (USB)"and the placeholder["Fantech mouse"]firmware string are gone rather than mislabelling wireless units
Not covered
Bluetooth. The M5 Pro's third mode enumerates on a different usage page
(
0xFF35/0xFF66, usage0x0202) and GearHub drives it through a separateread path. Not implemented, because there was no way to verify it here.
A concern worth flagging: PID
0x503d— the Fantech WG14P Yari Pro, theonly Fantech model this repo lists — appears in GearHub's device table as
type:"dongle". If that is right, it needs the same relay handshake, and thefantechdriver is likely broken on its own hardware too. I did not change itscommand set, having no Fantech device to verify against; guessing would only
trade one silent failure for another. After this change it at least fails
loudly instead of inventing numbers.
Testing
🤖 Generated with Claude Code