Skip to content
Open
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
14 changes: 14 additions & 0 deletions src/drivers/razer/devices.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,20 @@ test("the asymmetric lift-off write probe is only armed where it was confirmed",
for (const id of armed) assert.equal(RAZER_PRODUCTS.get(id)?.verified, true);
});

test("button mapping is only offered on connections where class 0x02 answered", () => {
// Gated per product id and per connection, not per model. An all-zero reply
// decodes as "Disabled" on every control, so a transport that does not
// implement class 0x02 does not fail loudly — it produces a full, plausible
// set of controls that all read Disabled and silently do nothing. Hence an
// allowlist, and hence a hardware run per entry even for two connections of
// the same mouse.
const offered = RAZER_PRODUCT_IDS.filter((id) => RAZER_PRODUCTS.get(id)?.buttonMapping === true);
assert.deepEqual(offered.sort(), [0x00c0, 0x00c1]);
for (const id of offered) {
assert.equal(RAZER_PRODUCTS.get(id)?.verified, true, `0x${id.toString(16)} offers button mapping without being verified`);
}
});

test("no product is claimed by both this registry and a dedicated Razer driver", () => {
// `driverFor` returns the first match in DEVICE_DRIVERS, so an overlap would
// silently kill whichever driver is registered later.
Expand Down
25 changes: 17 additions & 8 deletions src/razer/devices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,18 @@ export interface RazerProduct {
* only ever been exercised on the Viper V3 Pro. Other Razer mice may well use
* a different class or a different control-index scheme, so nothing is
* offered to them until it has been checked on hardware. Gates both
* `RazerButtonControl` and `RazerToggleControl` — same command, same
* per-model risk, no reason to split them.
* `RazerButtonControl` and `RazerToggleControl` — same command, same risk,
* no reason to split them.
*
* Set per product id, which means per *connection*: the cable and the
* receiver are separate entries for one mouse and were verified separately.
* That is not pedantry about provenance — the failure mode is silent.
* `razerDecodeButtonMapping` reads `type=0x00, len=0x00, value=0x00` as
* "Disabled", so a transport that does not implement the class answers
* all-zero and yields a full, ordinary-looking set of controls that every
* read reports as Disabled and every write appears to accept.
* `readButtonMappings`'s null-collapse cannot catch it, because the dict
* comes back populated.
*/
buttonMapping?: boolean;
/** Also accept a vendor-defined collection as the control interface. */
Expand Down Expand Up @@ -371,6 +381,10 @@ const VIPER_V3_PRO = {
hasBattery: true,
liftOff: true,
asymmetricLiftOff: true,
// Class 0x02 is confirmed on both of this model's connections, so it belongs
// on the shared preset rather than on one product id. It sat on `0x00c1`
// alone while only the receiver had been exercised.
buttonMapping: true,
verified: true,
} as const;

Expand All @@ -386,12 +400,7 @@ const PRODUCT_DEFINITIONS: ReadonlyArray<[number, Omit<RazerProduct, "transactio
[0x00a5, { model: "Viper V2 Pro", wireless: false, highRatePolling: false, ...VIPER_V2_PRO }],
[0x00a6, { model: "Viper V2 Pro", wireless: true, highRatePolling: true, ...VIPER_V2_PRO }],
[0x00c0, { model: "Viper V3 Pro", wireless: false, pollingRates: RATES_1K, highRatePolling: false, ...VIPER_V3_PRO }],
// `buttonMapping` is set here rather than on the shared `VIPER_V3_PRO`
// preset because class 0x02 has only ever been exercised over the receiver.
// The cable (0x00c0) is the same mouse and very likely answers the same way,
// but that has not been checked, and a shared protocol family is not
// evidence for a specific product id and connection path.
[0x00c1, { model: "Viper V3 Pro", wireless: true, pollingRates: RATES_8K, highRatePolling: true, ...VIPER_V3_PRO, buttonMapping: true }],
[0x00c1, { model: "Viper V3 Pro", wireless: true, pollingRates: RATES_8K, highRatePolling: true, ...VIPER_V3_PRO }],
[0x006e, { model: "DeathAdder Essential", ...DEATHADDER_ESSENTIAL }],
[0x0071, { model: "DeathAdder Essential White Edition", ...DEATHADDER_ESSENTIAL }],
[0x0098, { model: "DeathAdder Essential (2021)", ...DEATHADDER_ESSENTIAL }],
Expand Down