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
3 changes: 3 additions & 0 deletions src/drivers/razer/devices.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,10 +338,13 @@ test("the catch-all filter does not widen a filter that was deliberately narrowe
assert.equal(broad.has(productId), false, `0x${productId.toString(16)} is filtered twice`);
}
// Every registry id still reaches the picker, through one filter or another.
// The one exception is a `nativeOnly` product, whose control channel WebHID
// can never expose and which must therefore stay out of the picker.
const offered = SUPPORTED_HID_FILTERS
.filter((filter) => filter.vendorId === VENDOR_ID.razer && filter.productId !== undefined)
.map((filter) => filter.productId);
for (const productId of RAZER_PRODUCT_IDS) {
if (RAZER_PRODUCTS.get(productId)?.nativeOnly) continue;
assert.ok(offered.includes(productId), `0x${productId.toString(16)} is not offered in the picker`);
}
});
20 changes: 20 additions & 0 deletions src/drivers/razer/hid.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,26 @@ test("the DeathAdder V2 is accepted on either interface shape", () => {
assert.equal(RazerHidClient.isSupported(vendor), true);
});

test("a nativeOnly model is never accepted, whatever the interface shape", () => {
// The DeathAdder V4 Pro 0x00be has its control channel on a collection the
// browser refuses to expose. A granted device may still enumerate through
// getDevices()/auto-reconnect, so isSupported must refuse it regardless.
// Arrange
const mouse = {
vendorId: 0x1532,
productId: 0x00be,
collections: [{ usagePage: 0x01, usage: 0x02, featureReports: [], children: [] }],
} as unknown as HIDDevice;
const vendor = {
...mouse,
collections: [{ usagePage: 0xffc0, usage: 0x01, featureReports: [], children: [] }],
} as unknown as HIDDevice;

// Act / Assert
assert.equal(RazerHidClient.isSupported(mouse), false);
assert.equal(RazerHidClient.isSupported(vendor), false);
});

test("the DeathAdder V2 caps DPI at 20000", () => {
// Arrange
const { client } = fakeMouse({ tracking: 0, liftOff: 16, landing: 11, asymmetric: false }, { productId: 0x0084 });
Expand Down
4 changes: 4 additions & 0 deletions src/drivers/razer/hid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ export class RazerHidClient {
static isSupported(device: HIDDevice): boolean {
const product = RAZER_PRODUCTS.get(device.productId);
if (device.vendorId !== VENDOR_ID.razer || !product) return false;
// A nativeOnly model's control channel sits on a collection the browser
// refuses to expose, so the WebHID driver must never claim it even when a
// previously granted device enumerates (auto-reconnect, shared grants).
if (product.nativeOnly) return false;
return isMouseControlInterface(device)
|| (product.vendorControlInterface === true && hasVendorCollection(device));
}
Expand Down
7 changes: 6 additions & 1 deletion src/drivers/vendors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
LOGITECH_BOLT_PRODUCT_IDS,
LOGITECH_DIRECT_PRODUCT_IDS,
} from "@openmouse/protocol/logitech";
import { RAZER_PRODUCT_IDS } from "@openmouse/protocol/razer-devices";
import { RAZER_PRODUCTS, RAZER_PRODUCT_IDS } from "@openmouse/protocol/razer-devices";
import {
NINJUTSO_LEGACY_MOUSE_PRODUCT_IDS,
NINJUTSO_LEGACY_RECEIVER_PRODUCT_IDS,
Expand Down Expand Up @@ -103,9 +103,14 @@ const RAZER_NARROWED_PRODUCT_IDS: ReadonlySet<number> = new Set([
* revision, so the whole device is requested and the picker offers each
* interface: the driver rejects the ones that cannot answer, and a model whose
* first entry never replies is added again on another entry.
*
* Products flagged `nativeOnly` in the registry are left out: their control
* channel is on a Chrome-protected collection, so no granted interface can
* ever answer and offering them just produces a dead picker entry.
*/
export const RAZER_REGISTRY_FILTERS: HIDDeviceFilter[] = RAZER_PRODUCT_IDS
.filter((productId) => !RAZER_NARROWED_PRODUCT_IDS.has(productId))
.filter((productId) => !RAZER_PRODUCTS.get(productId)?.nativeOnly)
.map((productId) => ({ vendorId: VENDOR_ID.razer, productId }));

// The DeathAdder V2 keeps the Essential family's split interface layout, so it
Expand Down
20 changes: 17 additions & 3 deletions src/razer/devices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,14 @@ export interface RazerProduct {
asymmetricLiftOff: boolean;
/** Confirmed against real hardware by this project. */
verified: boolean;
/**
* The control channel sits on a collection Chrome classifies as a plain mouse
* and strips feature reports from, so WebHID can never reach it no matter how
* many interfaces are granted. The registry entry stays so the native HAL
* transport still has a config, but `../vendors.ts` must not offer the id in
* the WebHID picker.
*/
nativeOnly?: boolean;
}

/** Everything a preset supplies. The transaction id is deliberately not here. */
Expand Down Expand Up @@ -415,7 +423,10 @@ const PRODUCT_DEFINITIONS: ReadonlyArray<[number, Omit<RazerProduct, "transactio

// ---- viper-receiver -------------------------------------------------------
[0x007a, { model: "Viper Ultimate (Wired)", ...VIPER_RECEIVER_WIRED }],
[0x007b, { model: "Viper Ultimate", ...VIPER_RECEIVER_WIRELESS }],
// Confirmed on hardware (PR #45): the dongle's Generic-Desktop-Mouse
// interface is Chrome-protected, so every collection is feat[none] and
// sendFeatureReport fails on any id. Native HAL only.
[0x007b, { model: "Viper Ultimate", ...VIPER_RECEIVER_WIRELESS, nativeOnly: true }],
[0x007c, { model: "DeathAdder V2 Pro (Wired)", ...VIPER_RECEIVER_WIRED }],
[0x007d, { model: "DeathAdder V2 Pro", ...VIPER_RECEIVER_WIRELESS }],
[0x009e, { model: "Viper Mini Signature Edition (Wired)", ...VIPER_RECEIVER_WIRED, maxDpi: DPI_FOCUS_PRO }],
Expand Down Expand Up @@ -469,8 +480,11 @@ const PRODUCT_DEFINITIONS: ReadonlyArray<[number, Omit<RazerProduct, "transactio
// has measured it — see TESTING.md.
[0x00b7, { model: "DeathAdder V3 Pro", ...MODERN_RECEIVER, highRatePolling: false, maxDpi: DPI_FOCUS_PRO }],
[0x00b9, { model: "Basilisk V3 X HyperSpeed", ...LEGACY_RECEIVER, maxDpi: 18_000 }],
[0x00be, { model: "DeathAdder V4 Pro (Wired)", ...MODERN_WIRED, maxDpi: DPI_FOCUS_PRO_35K }],
[0x00bf, { model: "DeathAdder V4 Pro", ...HYPERPOLLING_RECEIVER }],
// The control channel is on a Chrome-protected collection: with Synapse fully
// stopped the diagnostics harness got "no feature report channel" on every
// interface, in both the wired and wireless roles. Native HAL only.
[0x00be, { model: "DeathAdder V4 Pro (Wired)", ...MODERN_WIRED, maxDpi: DPI_FOCUS_PRO_35K, nativeOnly: true }],
[0x00bf, { model: "DeathAdder V4 Pro", ...HYPERPOLLING_RECEIVER, nativeOnly: true }],
[0x00c2, { model: "DeathAdder V3 Pro (Wired)", ...MODERN_WIRED, maxDpi: DPI_FOCUS_PRO }],
[0x00c3, { model: "DeathAdder V3 Pro", ...MODERN_RECEIVER, maxDpi: DPI_FOCUS_PRO }],
[0x00c4, { model: "DeathAdder V3 HyperSpeed (Wired)", ...MODERN_WIRED, maxDpi: DPI_FOCUS_PRO }],
Expand Down