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
32 changes: 14 additions & 18 deletions docs/logitech-onboard-profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -583,24 +583,20 @@ layout, so it is the Superstrike; a Superlight 2 is most likely 7, but that is a
guess until read from hardware.

**The original G Pro X Superlight (PID `0xc094`, wpid `4093` — "PRO X Wireless"
in Logitech's/Solaar's naming) reports format 7 but is not the Superlight 2 this
layout was verified on.** Live reports show it returning HID++ error `0x05`
("Logitech internal error") specifically when the format-7 per-stage lift-off
byte is written — the device rejects a lift-off offset that is valid on the
Superlight 2, most likely because this older board predates per-stage
lift-off entirely rather than storing it at a different offset. This matches
the unresolved DPI-stage-offset report below. Neither Solaar nor libratbag
implements per-stage lift-off or debounce for any Logitech mouse, onboard or
otherwise, so there is no reference layout to check this against.

OpenMouse's model is to support what a device can actually do rather than
gate features off by device, so `onboard-profiles.ts` does not refuse the
whole profile write for this PID: `isLodWritableForProduct` scopes the guard
to the one unverified field, and `encodeDpiStages`'s `writeLod` flag leaves
each stage's existing lift-off byte untouched while still writing its DPI
x/y — report rate, angle-snap, name and buttons are all unaffected and stay
writable. Lift the guard once a profile dump from a real `0xc094` device
confirms it has per-stage lift-off storage (and at what offset).
in Logitech's/Solaar's naming) reports format 4, not the format 7 this layout
was verified on.** A sector-1 dump from a real unit (main app
`MPM25.01_B0018`) answered `getOnboardProfilesInfo` with format 4, five
profiles, 16 sectors of 255 bytes, and a CRC-valid base-v1 sector 1: the
scalar table at `0x03`, 800 DPI in the first slot, report interval `0x01` at
`0x00`. Base v1 stores one scalar DPI per slot and has no per-stage lift-off
byte at all, so the format-7 `0x05` ("Logitech internal error") report that
originally motivated a product guard does not describe this layout. The
captured geometry is pinned by the "PRO X Wireless dump" test in
`onboard-profiles.test.ts`. `isLodWritableForProduct` keeps the product guard
only for a unit that ever reports a v6 format, where a per-stage lift-off byte
exists and was reported to be rejected; on the actual base-v1 device there is
nothing to refuse. Everything else a profile carries — DPI, report rate,
angle-snap, name and buttons — is writable.

**Names for format ids 7 and 8**, and the meaning of the extra
component-specific prototype fields (for example `dpi_v6` carries
Expand Down
53 changes: 48 additions & 5 deletions src/drivers/logitech/onboard-profiles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1212,13 +1212,56 @@ test("G502 keyboard shortcuts and consumer keys use direct four-byte HID binding
assert.equal(profileCrc(media), storedCrc(media));
});

/** Issue #159: a PRO X Wireless (PID 0xc094, main app MPM25.01_B0018). */
const SUPERLIGHT_C094_INFO_REPLY = bytes("01 0d 00 01 04 01 05 01 05 10 00 ff 0a 04 00 00");
/** Its sector 1, untouched from the factory, CRC `dd 75` intact. */
const SUPERLIGHT_C094_SECTOR_1 = (() => {
const sector = new Uint8Array(255).fill(0xff);
sector.set(bytes("01 00 00 20 03 00 00 00 00 00 00 00 00 ff ff ff"), 0x00);
sector.set(bytes("ff 00 ff ff ff ff ff ff ff ff ff ff ff ff ff ff"), 0x10);
sector.set(bytes("80 01 00 01 80 01 00 02 80 01 00 04 80 01 00 08"), 0x20);
sector.set(bytes("80 01 00 10 ff ff ff ff ff ff ff ff ff ff ff ff"), 0x30);
sector.set(bytes("00 00 00 00 00 00 1f 40 00 00 00 00 00 00 00 00"), 0xd0);
sector.set(bytes("00 1f 40 00 00 00 ff ff ff ff ff ff ff ff ff ff"), 0xe0);
sector.set(bytes("ff ff ff ff ff ff ff ff ff ff ff ff ff dd 75"), 0xf0);
return sector;
})();

test("the PRO X Wireless (0xc094) reports format 4, a base-v1 scalar table", () => {
// Issue #159: this exact PID was assumed to be format 7 like the Superlight
// 2, but a real dump reports format 4 — v1 storage, no per-stage lift-off.
assert.deepEqual(parseProfilesInfo(SUPERLIGHT_C094_INFO_REPLY), {
memoryModelId: 1,
profileFormatId: 4,
macroFormatId: 1,
profileCount: 5,
buttonCount: 5,
sectorCount: 16,
sectorSize: 255,
});
const profile = decodeOnboardProfile(
SUPERLIGHT_C094_SECTOR_1,
4,
{ sector: 1, enabled: true },
true,
);
assert.equal(profile.crcValid, true);
assert.equal(profile.reportRateWireless, 1000);
assert.deepEqual(profile.dpiStages, [{ x: 800, y: 800, lod: 0 }]);
assert.equal(profile.defaultDpiIndex, 0);
// There is no lift-off byte in a base-v1 layout, so the format-7 product
// guard has nothing to refuse on this device.
assert.equal(isLodWritableForProduct(4, 0xc094), true);
});

test("only the per-stage lift-off byte is refused on the original G Pro X Superlight (PID 0xc094)", () => {
// Format 7's DPI-stage triplet layout (x/y/lift-off) was only confirmed
// against a Pro X Superlight 2 dump; the original Superlight reports the
// same format id on an older board that likely predates per-stage
// lift-off, and rejects the write on hardware (HID++ error 0x05) when that
// byte is touched. Everything else format 7 carries stays writable — the
// guard is scoped to the one unverified field, not the whole profile.
// against a Pro X Superlight 2 dump. A 0xc094 was reported to reject the
// lift-off byte on hardware (HID++ error 0x05) as if it were that older
// board; the dump above shows a 0xc094 actually reports format 4, so the
// guard only stays for the case where one ever reports a v6 format.
// Everything else format 7 carries stays writable — the guard is scoped to
// the one unverified field, not the whole profile.
assert.equal(describeProfileFormat(7).writable, true);
assert.equal(isProfileWritable(7), true);
assert.equal(isLodWritableForProduct(7, 0xc094), false);
Expand Down
28 changes: 18 additions & 10 deletions src/drivers/logitech/onboard-profiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,16 +80,21 @@ export function isProfileWritable(profileFormatId: number | null | undefined): b

/**
* Format 7's per-stage lift-off byte (and the rest of its DPI-stage triplet
* layout) was recovered from a Pro X Superlight 2 dump. The original G Pro X
* Superlight (PID 0xc094, wpid 4093 — Logitech's "PRO X Wireless" in its own
* tooling) reports the same format id but is an older board that predates
* per-stage lift-off entirely; live reports show it returning HID++ error
* 0x05 ("Logitech internal error") specifically when a stage's lift-off byte
* is written, matching an unresolved report of a differently-shifted DPI
* stage layout on what is likely this device. Until a dump from this
* specific PID confirms it has lift-off storage to write to, everything else
* format 7 carries — DPI x/y, report rate, angle-snap, name, buttons — stays
* writable, and only the lift-off byte for each stage is left untouched.
* layout) was recovered from a Pro X Superlight 2 dump. A PID 0xc094 (the
* original G Pro X Superlight, wpid 4093 — Logitech's "PRO X Wireless" in its
* own tooling) was then reported to return HID++ error 0x05 ("Logitech
* internal error") when a stage's lift-off byte is written, matching an
* unresolved report of a differently-shifted DPI stage layout.
*
* That report was later pinned to format 7: a sector-1 dump from a real
* 0xc094 (main app MPM25.01_B0018) reports profile format 4, i.e. the base v1
* scalar table, which has no per-stage lift-off field at all — see the "PRO X
* Wireless dump" test. The guard therefore cannot fire against a base-v1
* layout and only matters if a unit ever reports a v6 format; it is kept for
* that case, because lifting it on a real format-7 board would bring the 0x05
* back. Everything else a format-7 profile carries — DPI x/y, report rate,
* angle-snap, name, buttons — stays writable, and only the lift-off byte for
* each stage is left untouched.
*/
const UNVERIFIED_LOD_PRODUCT_IDS = new Set([0xc094]);

Expand All @@ -99,6 +104,9 @@ export function isLodWritableForProduct(
productId: number | null | undefined,
): boolean {
if (!isProfileWritable(profileFormatId)) return false;
// Base v1 (formats 1-5) stores one scalar DPI per slot and has no lift-off
// byte, so there is nothing for the product guard to refuse.
if ((profileFormatId ?? 0) < 6) return true;
if (productId !== null && productId !== undefined && UNVERIFIED_LOD_PRODUCT_IDS.has(productId)) return false;
return true;
}
Expand Down
Loading