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
4 changes: 3 additions & 1 deletion src/drivers/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { GWolvesHidClient } from "./gwolves/hid.ts";
import { GWolvesXviHidClient } from "./gwolves/xvi-hid.ts";
import { GWolvesXviNewHidClient } from "./gwolves/xvi-new-hid.ts";
import { SteelSeriesRival3HidClient } from "./steelseries/hid.ts";
import { SteelSeriesRival110HidClient } from "./steelseries/rival-110-hid.ts";
import { SteelSeriesAerox3HidClient } from "./steelseries/aerox3-hid.ts";
import { SteelSeriesAerox3WirelessHidClient } from "./steelseries/aerox3-wireless-hid.ts";
import { SteelSeriesRival3WirelessHidClient } from "./steelseries/rival3-wireless-hid.ts";
Expand Down Expand Up @@ -83,7 +84,7 @@ import { AjazzHidClient } from "./ajazz/hid.ts";
import { AjazzAk820HidClient } from "./ajazz/ak820-hid.ts";

export type PulsarClient = PulsarHidClient | PulsarProHidClient | PulsarXs1HidClient;
export type SupportedClient = RawmHidClient | MotospeedHidClient | LogitechHidppClient | PulsarClient | EggOp1HidClient | EggOtHidClient | EggWeHidClient | FinalmouseHidClient | WLMouseHidClient | WLMouseBeastX4kHidClient | LamzuHidClient | LamzuAtlantisHidClient | OrbitalHidClient | RazerHidClient | RazerViperHidClient | RazerViperMiniHidClient | RazerViperV4ProHidClient | RazerCobraHidClient | TeevolutionHidClient | AtkHidClient | AtkBitmouseHidClient | VgnF2HidClient | VaxeeHidClient | Keychron8kHidClient | Keychron1kHidClient | Keychron4kHidClient | Keychron8kNordicHidClient | KeychronNapeHidClient | ModdoHidClient | NinjutsoHidClient | ZaunkoenigHidClient | CorsairHidClient | CorsairBragiHidClient | AttackSharkHidClient | FantechHidClient | GearHubHidClient | WootingHidClient | WallhackMouseHidClient | WallhackKeyboardHidClient | GWolvesHidClient | GWolvesXviHidClient | GWolvesXviNewHidClient | SteelSeriesRival3HidClient | SteelSeriesAerox3HidClient | SteelSeriesAerox3WirelessHidClient | SteelSeriesRival3WirelessHidClient | SteelSeriesAerox5HidClient | SteelSeriesAerox5WirelessHidClient | SteelSeriesRival650HidClient | SteelSeriesAerox9WirelessHidClient | SteelSeriesRival310HidClient | SteelSeriesPrimePlusHidClient | SteelSeriesPrimeMiniWirelessHidClient | SteelSeriesSenseiTenHidClient | GloriousHidClient | GloriousClassicHidClient | GloriousCore2HidClient | MchoseHidClient | MchoseDockHidClient | MchoseA5ProMaxHidClient | KsnakeHidClient | MicrosoftHidClient | DareuHidClient | RedragonHidClient | RedragonM690ProHidClient | IncottHidClient | HyperXHidClient | MchoseV3HidClient | AsusHidClient | KyuProMx1Client | DeluxHidClient | BytechHidClient | RapooHidClient | FaterHidClient | CoolerMasterHidClient | AjazzHidClient | AjazzAk820HidClient;
export type SupportedClient = RawmHidClient | MotospeedHidClient | LogitechHidppClient | PulsarClient | EggOp1HidClient | EggOtHidClient | EggWeHidClient | FinalmouseHidClient | WLMouseHidClient | WLMouseBeastX4kHidClient | LamzuHidClient | LamzuAtlantisHidClient | OrbitalHidClient | RazerHidClient | RazerViperHidClient | RazerViperMiniHidClient | RazerViperV4ProHidClient | RazerCobraHidClient | TeevolutionHidClient | AtkHidClient | AtkBitmouseHidClient | VgnF2HidClient | VaxeeHidClient | Keychron8kHidClient | Keychron1kHidClient | Keychron4kHidClient | Keychron8kNordicHidClient | KeychronNapeHidClient | ModdoHidClient | NinjutsoHidClient | ZaunkoenigHidClient | CorsairHidClient | CorsairBragiHidClient | AttackSharkHidClient | FantechHidClient | GearHubHidClient | WootingHidClient | WallhackMouseHidClient | WallhackKeyboardHidClient | GWolvesHidClient | GWolvesXviHidClient | GWolvesXviNewHidClient | SteelSeriesRival3HidClient | SteelSeriesRival110HidClient | SteelSeriesAerox3HidClient | SteelSeriesAerox3WirelessHidClient | SteelSeriesRival3WirelessHidClient | SteelSeriesAerox5HidClient | SteelSeriesAerox5WirelessHidClient | SteelSeriesRival650HidClient | SteelSeriesAerox9WirelessHidClient | SteelSeriesRival310HidClient | SteelSeriesPrimePlusHidClient | SteelSeriesPrimeMiniWirelessHidClient | SteelSeriesSenseiTenHidClient | GloriousHidClient | GloriousClassicHidClient | GloriousCore2HidClient | MchoseHidClient | MchoseDockHidClient | MchoseA5ProMaxHidClient | KsnakeHidClient | MicrosoftHidClient | DareuHidClient | RedragonHidClient | RedragonM690ProHidClient | IncottHidClient | HyperXHidClient | MchoseV3HidClient | AsusHidClient | KyuProMx1Client | DeluxHidClient | BytechHidClient | RapooHidClient | FaterHidClient | CoolerMasterHidClient | AjazzHidClient | AjazzAk820HidClient;

export interface DeviceDriver {
brand: string;
Expand Down Expand Up @@ -165,6 +166,7 @@ export const DEVICE_DRIVERS: readonly DeviceDriver[] = [
{ brand: "G-Wolves", supports: (device) => GWolvesXviHidClient.isSupported(device), create: (device) => new GWolvesXviHidClient(device), score: () => 7 },
{ brand: "G-Wolves", supports: (device) => GWolvesXviNewHidClient.isSupported(device), create: (device) => new GWolvesXviNewHidClient(device), score: () => 7 },
{ brand: "SteelSeries", supports: (device) => SteelSeriesRival3HidClient.isSupported(device), create: (device) => new SteelSeriesRival3HidClient(device), score: () => 6 },
{ brand: "SteelSeries", supports: (device) => SteelSeriesRival110HidClient.isSupported(device), create: (device) => new SteelSeriesRival110HidClient(device), score: () => 6 },
{ brand: "SteelSeries", supports: (device) => SteelSeriesAerox3HidClient.isSupported(device), create: (device) => new SteelSeriesAerox3HidClient(device), score: () => 6 },
{ brand: "SteelSeries", supports: (device) => SteelSeriesAerox3WirelessHidClient.isSupported(device), create: (device) => new SteelSeriesAerox3WirelessHidClient(device), score: () => 6 },
{ brand: "SteelSeries", supports: (device) => SteelSeriesRival3WirelessHidClient.isSupported(device), create: (device) => new SteelSeriesRival3WirelessHidClient(device), score: () => 6 },
Expand Down
154 changes: 154 additions & 0 deletions src/drivers/steelseries/rival-110-hid.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import assert from "node:assert/strict";
import test from "node:test";

import { SteelSeriesRival110HidClient } from "./rival-110-hid.ts";

function fakeDevice(options: { productId?: number; firmware?: number[]; activePreset?: number; answer?: boolean } = {}) {
const sent: Array<{ reportId: number; payload: number[] }> = [];
let listener: ((event: HIDInputReportEvent) => void) | null = null;
const reply = (bytes: number[]) => {
const response = new Uint8Array(bytes);
queueMicrotask(() =>
listener?.({ reportId: 0, data: new DataView(response.buffer), device } as unknown as HIDInputReportEvent));
};
const device = {
vendorId: 0x1038,
productId: options.productId ?? 0x1729,
productName: "SteelSeries Rival 110",
opened: true,
collections: [],
open: async () => {},
close: async () => {},
sendReport: async (reportId: number, data: BufferSource) => {
const view = ArrayBuffer.isView(data)
? new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
: new Uint8Array(data as ArrayBuffer);
const payload = [...view];
sent.push({ reportId, payload });
if (options.answer === false) return;
if (payload[0] === 0x10 && payload[1] === 0x00) reply(options.firmware ?? [0x05, 0x02]);
if (payload[0] === 0x16 && payload[1] === 0x00) reply([options.activePreset ?? 0x01]);
},
sendFeatureReport: async () => { throw new Error("the Rival 110 protocol does not use feature reports"); },
receiveFeatureReport: async () => { throw new Error("the Rival 110 protocol does not use feature reports"); },
addEventListener: (_type: string, attached: (event: HIDInputReportEvent) => void) => { listener = attached; },
removeEventListener: () => { listener = null; },
};
return { device: device as unknown as HIDDevice, sent };
}

test("claims the Rival 110 family product ids and nothing else", () => {
const { device } = fakeDevice();
for (const productId of [0x1729, 0x1816, 0x1810]) {
assert.equal(
SteelSeriesRival110HidClient.isSupported({ ...device, productId } as HIDDevice),
true,
`expected 0x${productId.toString(16)} to be claimed`,
);
}
for (const productId of [0x1702, 0x1814, 0x1824, 0x1836]) {
assert.equal(
SteelSeriesRival110HidClient.isSupported({ ...device, productId } as HIDDevice),
false,
`must not claim product id 0x${productId.toString(16)}`,
);
}
assert.equal(SteelSeriesRival110HidClient.isSupported({ ...device, vendorId: 0x1532 } as HIDDevice), false);
});

test("readStatus probes firmware and the active preset, reporting that preset's default", async () => {
const { device, sent } = fakeDevice({ activePreset: 0x02 });
const status = await new SteelSeriesRival110HidClient(device).readStatus();
assert.deepEqual(sent, [
{ reportId: 0, payload: [0x10, 0x00] },
{ reportId: 0, payload: [0x16, 0x00] },
]);
assert.equal(status.brand, "SteelSeries");
assert.equal(status.name, "SteelSeries Rival 110");
assert.deepEqual(status.firmware, ["2.5"]);
assert.equal(status.connectionType, "Wired");
// Active preset is 2, which this session has not written, so its default is shown.
assert.equal(status.dpi, 1600);
assert.equal(status.pollingRateHz, 1000);
assert.equal(status.ui?.valuesVerified, false);
assert.ok(status.ui?.pollingNote?.includes("active preset (2)"));
});

test("a silent interface fails the firmware probe loudly", async () => {
const { device } = fakeDevice({ answer: false });
const client = new SteelSeriesRival110HidClient(device);
await assert.rejects(() => client.readStatus(), /did not answer the firmware query/);
});

test("setDpi writes the active preset (1 by default) plus save", async () => {
const { device, sent } = fakeDevice({ activePreset: 0x01 });
const client = new SteelSeriesRival110HidClient(device);
await client.readStatus();
sent.length = 0;
const dpi = await client.setDpi(800);
assert.equal(dpi, 800);
assert.deepEqual(sent, [
{ reportId: 0, payload: [0x16, 0x00] },
{ reportId: 0, payload: [0x03, 0x01, 0x12] },
{ reportId: 0, payload: [0x09, 0x00] },
]);
});

test("setDpi writes preset 2 when the CPI button has selected it", async () => {
const { device, sent } = fakeDevice({ activePreset: 0x02 });
const client = new SteelSeriesRival110HidClient(device);
await client.readStatus();
sent.length = 0;
await client.setDpi(3200);
assert.deepEqual(sent, [
{ reportId: 0, payload: [0x16, 0x00] },
{ reportId: 0, payload: [0x03, 0x02, 0x49] },
{ reportId: 0, payload: [0x09, 0x00] },
]);
// readStatus now reports the value written to the active preset.
sent.length = 0;
const status = await client.readStatus();
assert.equal(status.dpi, 3200);
});

test("setDpiPreset2 writes and records preset 2", async () => {
const { device, sent } = fakeDevice({ activePreset: 0x01 });
const client = new SteelSeriesRival110HidClient(device);
await client.readStatus();
sent.length = 0;
await client.setDpiPreset2(1600);
assert.deepEqual(sent, [
{ reportId: 0, payload: [0x03, 0x02, 0x24] },
{ reportId: 0, payload: [0x09, 0x00] },
]);
});

test("setPollingRate, setLedColor, setLightEffect, and setButton6Mode write plus save", async () => {
const { device, sent } = fakeDevice();
const client = new SteelSeriesRival110HidClient(device);
await client.readStatus();
sent.length = 0;
await client.setPollingRate(500);
assert.deepEqual(sent, [
{ reportId: 0, payload: [0x04, 0x00, 0x02] },
{ reportId: 0, payload: [0x09, 0x00] },
]);
sent.length = 0;
await client.setLedColor(0xff, 0x18, 0x00);
assert.deepEqual(sent, [
{ reportId: 0, payload: [0x05, 0x00, 0xff, 0x18, 0x00] },
{ reportId: 0, payload: [0x09, 0x00] },
]);
sent.length = 0;
await client.setLightEffect("breath");
assert.deepEqual(sent, [
{ reportId: 0, payload: [0x07, 0x00, 0x03] },
{ reportId: 0, payload: [0x09, 0x00] },
]);
sent.length = 0;
await client.setButton6Mode("os");
assert.deepEqual(sent, [
{ reportId: 0, payload: [0x0b, 0x01] },
{ reportId: 0, payload: [0x09, 0x00] },
]);
});
Loading
Loading