Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ac17fce
feat: add SteelSeries Sensei Pro / Rival Pro protocol family
snekxs Oct 12, 2026
f028803
feat: register plain Prime and Prime Wireless product IDs
snekxs Oct 12, 2026
e0bc509
feat: add SteelSeries Sensei 310 protocol family
snekxs Oct 12, 2026
1f6850b
feat: add SteelSeries Prime Mini protocol family
snekxs Oct 12, 2026
3f8fdb7
feat: add SteelSeries Rival 5 protocol family
snekxs Oct 12, 2026
c06ee80
feat: add SteelSeries Rival 3 Gen 2 protocol family
snekxs Oct 13, 2026
13556fa
feat: add SteelSeries Rival 3 Wireless Gen 2 protocol family
snekxs Oct 13, 2026
4b0075a
feat: add SteelSeries Aerox 3 Wireless Gen 2 protocol family
snekxs Oct 13, 2026
78b50c0
feat: add SteelSeries Rival 600 protocol family
snekxs Oct 13, 2026
e22eabc
feat: add SteelSeries Rival 100 family protocol
snekxs Oct 13, 2026
947b467
feat: add SteelSeries Rival 110 family protocol
snekxs Oct 13, 2026
df5b845
feat: add SteelSeries Rival 300 family protocol
snekxs Oct 13, 2026
9ae9e48
fix(rival-110): write the active DPI preset, not always preset 1
snekxs Oct 13, 2026
55b9004
fix(rival-300): write the active DPI preset and track both presets
snekxs Oct 13, 2026
a1a2e3c
feat: add SteelSeries Sensei [RAW] protocol family
snekxs Oct 13, 2026
790cc86
Merge remote-tracking branch 'origin/pr/206' into integration/steelse…
snekxs Oct 16, 2026
d55ab3a
Merge remote-tracking branch 'origin/pr/207' into integration/steelse…
snekxs Oct 16, 2026
52cf070
Merge remote-tracking branch 'origin/pr/208' into integration/steelse…
snekxs Oct 16, 2026
7f19ae8
Merge remote-tracking branch 'origin/pr/209' into integration/steelse…
snekxs Oct 16, 2026
d4f35ba
Merge remote-tracking branch 'origin/pr/210' into integration/steelse…
snekxs Oct 16, 2026
0edde51
Merge remote-tracking branch 'origin/pr/211' into integration/steelse…
snekxs Oct 16, 2026
b007f2d
Merge remote-tracking branch 'origin/pr/212' into integration/steelse…
snekxs Oct 16, 2026
f9d79a6
Merge remote-tracking branch 'origin/pr/214' into integration/steelse…
snekxs Oct 16, 2026
21bd7f5
Merge remote-tracking branch 'origin/pr/215' into integration/steelse…
snekxs Oct 16, 2026
a03536a
Merge remote-tracking branch 'origin/pr/217' into integration/steelse…
snekxs Oct 16, 2026
90b5712
Merge remote-tracking branch 'origin/pr/218' into integration/steelse…
snekxs Oct 16, 2026
be03195
Merge remote-tracking branch 'origin/pr/219' into integration/steelse…
snekxs Oct 16, 2026
cf93825
Merge remote-tracking branch 'origin/pr/220' into integration/steelse…
snekxs Oct 16, 2026
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
27 changes: 25 additions & 2 deletions src/drivers/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,29 @@ 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 { SteelSeriesRival100HidClient } from "./steelseries/rival-100-hid.ts";
import { SteelSeriesRival110HidClient } from "./steelseries/rival-110-hid.ts";
import { SteelSeriesRival300HidClient } from "./steelseries/rival-300-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";
import { SteelSeriesAerox5HidClient } from "./steelseries/aerox5-hid.ts";
import { SteelSeriesAerox5WirelessHidClient } from "./steelseries/aerox5-wireless-hid.ts";
import { SteelSeriesRival650HidClient } from "./steelseries/rival650-hid.ts";
import { SteelSeriesRival600HidClient } from "./steelseries/rival-600-hid.ts";
import { SteelSeriesAerox9WirelessHidClient } from "./steelseries/aerox9-wireless-hid.ts";
import { SteelSeriesRival310HidClient } from "./steelseries/rival310-hid.ts";
import { SteelSeriesPrimePlusHidClient } from "./steelseries/prime-plus-hid.ts";
import { SteelSeriesPrimeMiniWirelessHidClient } from "./steelseries/prime-mini-wireless-hid.ts";
import { SteelSeriesPrimeMiniHidClient } from "./steelseries/prime-mini-hid.ts";
import { SteelSeriesSenseiTenHidClient } from "./steelseries/sensei-ten-hid.ts";
import { SteelSeriesSenseiRivalProHidClient } from "./steelseries/sensei-rival-pro-hid.ts";
import { SteelSeriesSensei310HidClient } from "./steelseries/sensei-310-hid.ts";
import { SteelSeriesRival5HidClient } from "./steelseries/rival-5-hid.ts";
import { SteelSeriesRival3Gen2HidClient } from "./steelseries/rival-3-v2-hid.ts";
import { SteelSeriesRival3WirelessV2HidClient } from "./steelseries/rival-3-wireless-v2-hid.ts";
import { SteelSeriesAerox3WirelessGen2HidClient } from "./steelseries/aerox-3-wireless-gen2-hid.ts";
import { SteelSeriesSenseiRawHidClient } from "./steelseries/sensei-raw-hid.ts";
import { GloriousHidClient } from "./glorious/hid.ts";
import { GloriousClassicHidClient } from "./glorious/classic-hid.ts";
import { GloriousCore2HidClient } from "./glorious/core2-hid.ts";
Expand All @@ -83,8 +95,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 | SteelSeriesAerox3HidClient | SteelSeriesAerox3WirelessHidClient | SteelSeriesRival3WirelessHidClient | SteelSeriesAerox5HidClient | SteelSeriesAerox5WirelessHidClient | SteelSeriesRival650HidClient | SteelSeriesAerox9WirelessHidClient | SteelSeriesRival310HidClient | SteelSeriesPrimePlusHidClient | SteelSeriesPrimeMiniWirelessHidClient | SteelSeriesSenseiTenHidClient | SteelSeriesSenseiRivalProHidClient | SteelSeriesSensei310HidClient | SteelSeriesPrimeMiniHidClient | SteelSeriesRival5HidClient | SteelSeriesRival3Gen2HidClient | SteelSeriesRival3WirelessV2HidClient | SteelSeriesAerox3WirelessGen2HidClient | SteelSeriesRival600HidClient | SteelSeriesRival100HidClient | SteelSeriesRival110HidClient | SteelSeriesRival300HidClient | SteelSeriesSenseiRawHidClient | 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;
supports(device: HIDDevice): boolean;
Expand Down Expand Up @@ -165,17 +176,29 @@ 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) => SteelSeriesRival100HidClient.isSupported(device), create: (device) => new SteelSeriesRival100HidClient(device), score: () => 6 },
{ brand: "SteelSeries", supports: (device) => SteelSeriesRival110HidClient.isSupported(device), create: (device) => new SteelSeriesRival110HidClient(device), score: () => 6 },
{ brand: "SteelSeries", supports: (device) => SteelSeriesRival300HidClient.isSupported(device), create: (device) => new SteelSeriesRival300HidClient(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 },
{ brand: "SteelSeries", supports: (device) => SteelSeriesAerox5HidClient.isSupported(device), create: (device) => new SteelSeriesAerox5HidClient(device), score: () => 6 },
{ brand: "SteelSeries", supports: (device) => SteelSeriesAerox5WirelessHidClient.isSupported(device), create: (device) => new SteelSeriesAerox5WirelessHidClient(device), score: () => 6 },
{ brand: "SteelSeries", supports: (device) => SteelSeriesRival650HidClient.isSupported(device), create: (device) => new SteelSeriesRival650HidClient(device), score: () => 6 },
{ brand: "SteelSeries", supports: (device) => SteelSeriesRival600HidClient.isSupported(device), create: (device) => new SteelSeriesRival600HidClient(device), score: () => 6 },
{ brand: "SteelSeries", supports: (device) => SteelSeriesAerox9WirelessHidClient.isSupported(device), create: (device) => new SteelSeriesAerox9WirelessHidClient(device), score: () => 6 },
{ brand: "SteelSeries", supports: (device) => SteelSeriesRival310HidClient.isSupported(device), create: (device) => new SteelSeriesRival310HidClient(device), score: () => 6 },
{ brand: "SteelSeries", supports: (device) => SteelSeriesPrimePlusHidClient.isSupported(device), create: (device) => new SteelSeriesPrimePlusHidClient(device), score: () => 6 },
{ brand: "SteelSeries", supports: (device) => SteelSeriesPrimeMiniWirelessHidClient.isSupported(device), create: (device) => new SteelSeriesPrimeMiniWirelessHidClient(device), score: () => 6 },
{ brand: "SteelSeries", supports: (device) => SteelSeriesPrimeMiniHidClient.isSupported(device), create: (device) => new SteelSeriesPrimeMiniHidClient(device), score: () => 6 },
{ brand: "SteelSeries", supports: (device) => SteelSeriesSenseiTenHidClient.isSupported(device), create: (device) => new SteelSeriesSenseiTenHidClient(device), score: () => 6 },
{ brand: "SteelSeries", supports: (device) => SteelSeriesSenseiRivalProHidClient.isSupported(device), create: (device) => new SteelSeriesSenseiRivalProHidClient(device), score: () => 6 },
{ brand: "SteelSeries", supports: (device) => SteelSeriesSensei310HidClient.isSupported(device), create: (device) => new SteelSeriesSensei310HidClient(device), score: () => 6 },
{ brand: "SteelSeries", supports: (device) => SteelSeriesRival5HidClient.isSupported(device), create: (device) => new SteelSeriesRival5HidClient(device), score: () => 6 },
{ brand: "SteelSeries", supports: (device) => SteelSeriesRival3Gen2HidClient.isSupported(device), create: (device) => new SteelSeriesRival3Gen2HidClient(device), score: () => 6 },
{ brand: "SteelSeries", supports: (device) => SteelSeriesRival3WirelessV2HidClient.isSupported(device), create: (device) => new SteelSeriesRival3WirelessV2HidClient(device), score: () => 6 },
{ brand: "SteelSeries", supports: (device) => SteelSeriesAerox3WirelessGen2HidClient.isSupported(device), create: (device) => new SteelSeriesAerox3WirelessGen2HidClient(device), score: () => 6 },
{ brand: "SteelSeries", supports: (device) => SteelSeriesSenseiRawHidClient.isSupported(device), create: (device) => new SteelSeriesSenseiRawHidClient(device), score: () => 6 },
{ brand: "Glorious", supports: (device) => GloriousHidClient.isSupported(device), create: (device) => new GloriousHidClient(device), score: () => 5 },
{ brand: "Glorious", supports: (device) => GloriousClassicHidClient.isSupported(device), create: (device) => new GloriousClassicHidClient(device), score: () => 5 },
// Same vendor id as the classic family; disjoint by product id.
Expand Down
119 changes: 119 additions & 0 deletions src/drivers/steelseries/aerox-3-wireless-gen2-hid.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import assert from "node:assert/strict";
import test from "node:test";

import { SteelSeriesAerox3WirelessGen2HidClient } from "./aerox-3-wireless-gen2-hid.ts";

function fakeDevice(options: { productId?: number; battery?: number[]; firmware?: number[]; answer?: boolean } = {}) {
const sent: Array<{ reportId: number; payload: number[] }> = [];
let listener: ((event: HIDInputReportEvent) => void) | null = null;
const device = {
vendorId: 0x1038,
productId: options.productId ?? 0x1892,
productName: "SteelSeries Aerox 3 Wireless Gen 2",
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] === 0x92 || payload[0] === 0xd2) && payload.length === 1) {
const response = new Uint8Array(options.battery ?? [0x01, 0x64]);
queueMicrotask(() =>
listener?.({ reportId: 0, data: new DataView(response.buffer), device } as unknown as HIDInputReportEvent));
}
if ((payload[0] === 0x90 || payload[0] === 0xd0) && payload[1] === 0x00) {
const response = new Uint8Array(options.firmware ?? [0x31, 0x2e, 0x31, 0x2e, 0x35, ...new Array(7).fill(0x00)]);
queueMicrotask(() =>
listener?.({ reportId: 0, data: new DataView(response.buffer), device } as unknown as HIDInputReportEvent));
}
},
sendFeatureReport: async () => { throw new Error("the Aerox 3 Wireless Gen 2 protocol does not use feature reports"); },
receiveFeatureReport: async () => { throw new Error("the Aerox 3 Wireless Gen 2 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 wired and dongle product ids, and selects transport by PID", () => {
const { device } = fakeDevice();
assert.equal(SteelSeriesAerox3WirelessGen2HidClient.isSupported(device), true);
assert.equal(
SteelSeriesAerox3WirelessGen2HidClient.isSupported(fakeDevice({ productId: 0x1890 }).device),
true,
);
for (const productId of [0x183a, 0x1838, 0x1854, 0x1852, 0x1830, 0x1836]) {
assert.equal(
SteelSeriesAerox3WirelessGen2HidClient.isSupported({ ...device, productId } as HIDDevice),
false,
`must not claim product id 0x${productId.toString(16)}`,
);
}
assert.equal(SteelSeriesAerox3WirelessGen2HidClient.isSupported({ ...device, vendorId: 0x1532 } as HIDDevice), false);
});

test("readStatus probes battery and firmware without claiming charging state", async () => {
const { device, sent } = fakeDevice();
const status = await new SteelSeriesAerox3WirelessGen2HidClient(device).readStatus();
assert.deepEqual(sent, [
{ reportId: 0, payload: [0x92] },
{ reportId: 0, payload: [0x90, 0x00] },
]);
assert.equal(status.brand, "SteelSeries");
assert.equal(status.name, "SteelSeries Aerox 3 Wireless Gen 2");
assert.deepEqual(status.firmware, ["1.1.5"]);
assert.equal(status.connectionType, "Wireless");
assert.equal(status.batteryPercent, 100);
assert.equal(status.batteryState, "Unknown");
assert.equal(status.dpi, 400);
assert.equal(status.pollingRateHz, 1000);
assert.equal(status.ui?.valuesVerified, false);
assert.ok(status.ui?.pollingNote);
assert.deepEqual(status.supportedPollingRates, [125, 250, 500, 1000, 2000, 4000]);
});

test("a silent interface fails the battery probe loudly and names SteelSeries GG", async () => {
const { device } = fakeDevice({ answer: false });
const client = new SteelSeriesAerox3WirelessGen2HidClient(device);
await assert.rejects(() => client.readStatus(), /Close SteelSeries GG/);
});

test("dongle mode ORs the wireless flag into every command", async () => {
const { device, sent } = fakeDevice({ productId: 0x1890 });
const client = new SteelSeriesAerox3WirelessGen2HidClient(device);
await client.readStatus();
sent.length = 0;
await client.setPollingRate(1000);
assert.deepEqual(sent, [
{ reportId: 0, payload: [0x6b, 0x02, 0x02] },
{ reportId: 0, payload: [0x51, 0x00] },
]);
});

test("setDpi edits the active stage plus save", async () => {
const { device, sent } = fakeDevice();
const client = new SteelSeriesAerox3WirelessGen2HidClient(device);
await client.readStatus();
sent.length = 0;
const dpi = await client.setDpi(1600);
assert.equal(dpi, 1600);
assert.deepEqual(sent, [
{
reportId: 0,
payload: [
0x2d, 0x05, 0x00,
0x40, 0x06, 0x40, 0x06, 0x00,
0x20, 0x03, 0x20, 0x03, 0x00,
0x40, 0x06, 0x40, 0x06, 0x00,
0x60, 0x09, 0x60, 0x09, 0x00,
0x80, 0x0c, 0x80, 0x0c, 0x00,
],
},
{ reportId: 0, payload: [0x11, 0x00] },
]);
});
Loading
Loading