Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/devices/logitech/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Supported identifiers:

- `046d:c54d`, `046d:c547` — Lightspeed receivers
- `046d:c539` — HERO-era Lightspeed receiver
- `046d:c53f`, `046d:c543` — Nano Lightspeed 1.1 / 1.2 receivers (G305)
- `046d:c0a8` — PRO X 2 Superstrike (USB)
- `046d:c07e` — G402 / G402 Hyperion Fury (wired)
- `046d:c08f` — G403 HERO (wired)
Expand Down Expand Up @@ -90,3 +91,17 @@ hidden.
the LightForce switch are all hidden.
3. Change the DPI and polling rate and confirm each write persists after a
reload.

## G305 LIGHTSPEED (receiver-attached, Model ID `407400000000`)

Like the G309, the G305 exposes Mode Status `0x8090` but only the power-mode
half is meaningful: the status1 byte that would carry the gaming-surface and
LightForce fields is reserved and reads 0. The G305 also has no lift-off
control. OpenMouse treats all three as absent, so those cards stay hidden.

1. Confirm the model, battery, connection type, DPI, and polling rate are read
correctly.
2. Confirm the sensor card (lift-off distance), the gaming-surface card, and
the LightForce switch are all hidden.
3. Change the DPI and polling rate and confirm each write persists after a
reload.
2 changes: 2 additions & 0 deletions src/devices/logitech/hidpp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ test("an onboard-only mouse is told how to get itself supported", () => {
test("the G309's mode status is power-only and exposes no surface or LightForce controls", () => {
// Model id captured from hardware: 0x8090 V2 with only the power-mode half.
assert.equal(isPowerOnlyModeStatus("B03C40B10000"), true);
// The G305 reports the same reserved status1 byte.
assert.equal(isPowerOnlyModeStatus("407400000000"), true);
// Every other model keeps the status1 fields, and unknown/absent ids must
// not be silently downgraded.
assert.equal(isPowerOnlyModeStatus("B03C40B10001"), false);
Expand Down
11 changes: 7 additions & 4 deletions src/devices/logitech/hidpp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export class NotAMouseError extends Error {

const LOGITECH_VENDOR_ID = 0x046d;
// HID++ control interfaces, including the PRO X 2 Superstrike USB interface.
const LOGITECH_RECEIVER_PRODUCT_IDS = new Set([0xc54d, 0xc539, 0xc0a8, 0xc547]);
const LOGITECH_RECEIVER_PRODUCT_IDS = new Set([0xc54d, 0xc539, 0xc0a8, 0xc547, 0xc53f, 0xc543]);

/**
* Models whose 0x8090 mode-status feature drives the power-mode switch only.
Expand All @@ -130,6 +130,7 @@ const LOGITECH_RECEIVER_PRODUCT_IDS = new Set([0xc54d, 0xc539, 0xc0a8, 0xc547]);
*/
const MODE_STATUS_POWER_ONLY_MODEL_IDS: ReadonlySet<string> = new Set([
"B03C40B10000", // G309 LIGHTSPEED
"407400000000", // G305 LIGHTSPEED
]);

/** Whether 0x8090 on this model is the power-mode-only variant. */
Expand Down Expand Up @@ -320,11 +321,13 @@ export class LogitechHidppClient {

for (const candidate of candidates) {
this.resolvedDeviceIndex = candidate;
// Any reply at all proves something is listening, including a HID++
// error reply. Only silence rules the index out.
// Only a successful reply proves a HID++ 2.0 mouse is on this index. A
// receiver answers the direct index with a HID++ 1.0 error reply, which
// is not a mouse and must not count — else we latch onto it and every
// later request fails with "invalid command".
const answered = await this.request(0x00, 0x00, FEATURE.firmware >> 8, FEATURE.firmware & 0xff)
.then(() => true)
.catch((error: unknown) => !(error instanceof HidppTimeoutError));
.catch(() => false);
if (answered) return;
}
this.resolvedDeviceIndex = null;
Expand Down
5 changes: 3 additions & 2 deletions src/devices/vendors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,9 @@ export const TEEVOLUTION_PRODUCT_IDS = [0xf520, 0xf523, 0xf5bb, 0xf522] as const

// Logitech HID++ control interfaces addressed through a receiver slot (HID++
// device index 0x01). 0xc54d and 0xc547 are newer Lightspeed receivers, 0xc539
// is HERO-era Lightspeed, and 0xc0a8 is the PRO X 2 Superstrike USB interface.
export const LOGITECH_RECEIVER_PRODUCT_IDS = [0xc54d, 0xc539, 0xc0a8, 0xc547] as const;
// is HERO-era Lightspeed, 0xc53f and 0xc543 are Nano Lightspeed 1.1/1.2
// receivers (G305), and 0xc0a8 is the PRO X 2 Superstrike USB interface.
export const LOGITECH_RECEIVER_PRODUCT_IDS = [0xc54d, 0xc539, 0xc0a8, 0xc547, 0xc53f, 0xc543] as const;

// Every Logitech product with an HID++ control interface, receiver-addressed or
// not. Direct-connect product IDs live in ./logitech/protocol so the driver and
Expand Down