Skip to content

fix(hid): surface the 0xc539 dongle as a Lightspeed receiver, not Unifying - #665

Open
yuzi-co wants to merge 1 commit into
AprilNEA:masterfrom
yuzi-co:fix/lightspeed-receiver-c539-display-name
Open

fix(hid): surface the 0xc539 dongle as a Lightspeed receiver, not Unifying#665
yuzi-co wants to merge 1 commit into
AprilNEA:masterfrom
yuzi-co:fix/lightspeed-receiver-c539-display-name

Conversation

@yuzi-co

@yuzi-co yuzi-co commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

The bug

receiver_display_name(0xc539) returns "Unifying Receiver". It should return "Lightspeed Receiver".

0xc539 is the receiver bundled with the G502 LIGHTSPEED and the G Pro Wireless. It was listed in UNIFYING_PIDS, so the else branch of receiver_display_name caught it.

Three things in the repo already disagreed with that placement:

  1. UNIFYING_PIDS' own doc comment called it "the Lightspeed gaming receiver: a distinct product line".

  2. LIGHTSPEED_PIDS' doc comment states the split's entire purpose: "only the user-facing receiver name (see receiver_display_name) differs." Both lists route identically — the label is the only thing the split decides. So putting 0xc539 on the Unifying side defeated the one job the split has, for exactly the PID whose doc already identified it as Lightspeed.

  3. The device itself. Its USB product string is LIGHTSPEED Receiver:

    > Get-PnpDevice -PresentOnly | ? { $_.InstanceId -match 'VID_046D&PID_C539' }
    
    FriendlyName
    ------------
    G502 LIGHTSPEED
    LIGHTSPEED Receiver     <-- the dongle
    

    OpenLogi enumerated a dongle that names itself LIGHTSPEED Receiver and relabelled it Unifying Receiver.

Reproduced on real hardware: G502 LIGHTSPEED on a 046d:c539 receiver, Windows 11.

The fix

Move 0xc539 from UNIFYING_PIDS to LIGHTSPEED_PIDS.

Why this is behaviour-safe

Every consumer of the two lists uses their union, never one alone:

consumer how
speaks_unifying_protocol() UNIFYING_PIDS ∪ LIGHTSPEED_PIDS
is_receiver_pid() BOLT_PIDS ∪ speaks_unifying_protocol()
DeviceRoute::for_inventory via speaks_unifying_protocol()
openlogi-hid/src/transport.rs:243 .chain(UNIFYING_PIDS).chain(LIGHTSPEED_PIDS)

(Confirmed by grep -rn "UNIFYING_PIDS\|LIGHTSPEED_PIDS" --include=*.rs — every other hit is a re-export.)

So 0xc539 still routes as DeviceRoute::Unifying, still enumerates and pairs through the Unifying HID++ 1.0 register path, and is still recognised as a receiver. receiver_display_name is the only behaviour that changes.

The two routing tests both keep passing unchanged — device_route_for_unifying_pids_create_unifying_route and device_route_for_lightspeed_pids_create_unifying_route both assert a Unifying route, so 0xc539 simply moves from one loop to the other.

receiver_display_name is called from exactly one place (inventory/probe.rs:257, inside probe_unifying_receiver). Bolt never reaches it — assemble_bolt_probe hardcodes "Logi Bolt Receiver" at line 120 — so the else branch stays correct as the Unifying default.

Why it survived

The existing test enumerated 0xc53f, 0xc547 and 0xc52b, but never 0xc539 — the one PID sitting on the wrong side. Added, along with 0xc532 for symmetry on the Unifying side.

The added assertion is a real regression test. Reverting only the constant change and rerunning:

test hid::route::tests::lightspeed_receiver_has_its_own_display_name ... FAILED
assertion `left == right` failed
  left: "Unifying Receiver"
 right: "Lightspeed Receiver"

Local gate

Per AGENTS.md, on Windows 11 / x86_64-pc-windows-msvc:

command result
cargo fmt --all -- --check exit 0
cargo clippy --workspace --all-targets -- -D warnings exit 0
cargo test --workspace exit 0
RUSTDOCFLAGS="-D warnings" cargo doc -p openlogi-hid … exit 101 — pre-existing, see below

The rustdoc failure is not from this change and does not touch these files:

error: unresolved link to `AsyncHidChannel::supports_short_long_hidpp`
error: could not document `openlogi-hid`

It reproduces on a clean master on Windows and is fixed by #661. CI does not see it because the docs job runs on ubuntu, where AsyncHidChannel is not cfg-ed out.


How it happened

Traced through git log on the file. 0xc539 was not misfiled — it was stranded:

when PR what
2026-08-10 #510 "recognize Lightspeed receiver (046d:c539) as Unifying-compatible" Added 0xc539 to UNIFYING_PIDS. Correct at the time — LIGHTSPEED_PIDS and receiver_display_name did not exist yet, so there was only one list to put it in. This PR also wrote the comment that still says "the Lightspeed gaming receiver: a distinct product line".
2026-08-10 #388 "recognise Lightspeed nano receivers" Created LIGHTSPEED_PIDS = [0xc53f] and receiver_display_name, inventing the "Lightspeed receivers get their own name" concept — and wrote the test asserting 0xc53f → Lightspeed, 0xc52b → Unifying. It did not move 0xc539.
2026-08-13 #574 "recognise Lightspeed receiver 046d:c547" Added 0xc547 to the new list.

So the naming concept was introduced after 0xc539 had already been filed under Unifying, and nothing migrated it. The test hole follows from the same order: #388's test enumerated the PIDs #388 itself added, plus one Unifying control — 0xc539 was in neither group.

This PR completes the migration #388 started.

Relationship to #351

#351 (feat(hid): support LIGHTSPEED receivers and RGB effects) already does this, as part of a much larger change — it defines LIGHTSPEED_PIDS as [0xc539, 0xc53a, 0xc53d, 0xc53f, 0xc541, 0xc545, 0xc547, 0xc54d]. Flagging it explicitly so this isn't reviewed as if it were novel.

If #351 lands, close this as superseded — it is a strict subset. Offered separately because #351 is currently CONFLICTING at +1523/−267 across 42 files and has been open since 2026-07-04, while this is a one-constant fix with a regression test that can land independently and be trivially reverted. The two touch the same lines, so whichever merges second will need a trivial conflict resolution.

Related tracking issue: #512.

…fying

0xc539 is the receiver bundled with the G502 LIGHTSPEED and the G Pro
Wireless. It sat in UNIFYING_PIDS, so receiver_display_name() labelled it
"Unifying Receiver" in the inventory — even though its own doc comment
called it "the Lightspeed gaming receiver", and the device's USB product
string is literally "LIGHTSPEED Receiver".

The split between UNIFYING_PIDS and LIGHTSPEED_PIDS exists only to pick
that label: LIGHTSPEED_PIDS' doc says "only the user-facing receiver name
differs". So having 0xc539 on the Unifying side defeated the one purpose
the split has, for exactly the PID whose doc already identified it.

Move it to LIGHTSPEED_PIDS. Routing is unaffected: every consumer of the
two lists uses their union — speaks_unifying_protocol() ORs them,
is_receiver_pid() builds on that, and transport.rs chains them — so
receiver_display_name() is the only behaviour that changes. Both
device_route_for_* tests keep asserting a Unifying route; 0xc539 just
moves from one loop to the other.

The existing test covered 0xc53f, 0xc547 and 0xc52b but never 0xc539,
which is why the mismatch survived. Add it, plus 0xc532 for the other
side. Reverting the constant alone now fails the test with
left: "Unifying Receiver".
@yuzi-co
yuzi-co requested a review from AprilNEA as a code owner August 18, 2026 21:52
@greptile-apps

greptile-apps Bot commented Aug 18, 2026

Copy link
Copy Markdown

Greptile Summary

This PR correctly reclassifies receiver PID 0xc539 as Lightspeed for user-facing naming while preserving its existing Unifying-protocol routing.

  • Moves 0xc539 from UNIFYING_PIDS to LIGHTSPEED_PIDS.
  • Updates receiver documentation for the affected hardware.
  • Adds regression coverage for both Lightspeed and Unifying display names.

Confidence Score: 5/5

The PR appears safe to merge with no actionable defects identified.

The receiver remains in the union used by every routing, enumeration, and pairing consumer, while the changed list membership only corrects its user-facing display name and is covered by regression tests.

Important Files Changed

Filename Overview
crates/openlogi-core/src/hid/route.rs Reclassifies PID 0xc539 for display naming without changing protocol detection, routing, enumeration, or pairing behavior.

Reviews (1): Last reviewed commit: "fix(hid): surface the 0xc539 dongle as a..." | Re-trigger Greptile

@pyr02k1

pyr02k1 commented Aug 20, 2026

Copy link
Copy Markdown

I pulled this PR down and built it on CachyOS just to see if it would pick up my G502 X Plus, and it did. Not relevant to this PR is the limitations in that models options, but it does work. DPI changes, scroll speed, and lighting seem to work as expected. Candy is the charging pad and it's brightness and LED color options work as well. On the AUR based openlogi-bin it didn't show either of them.

Happy to test anything else if you need it, but this gets the lightspeed receiver up and working.

image

@albertorm95

Copy link
Copy Markdown

@pyr02k1 I have same mouse, I can help as well

@davidbudnick davidbudnick added type: bug Something is broken or behaves incorrectly platform: all Cross-platform issue labels Aug 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

platform: all Cross-platform issue type: bug Something is broken or behaves incorrectly

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants