From 6bf0692606530859780a94fe7301caef923b53b8 Mon Sep 17 00:00:00 2001 From: Kori Francis Date: Sun, 6 Sep 2026 15:58:32 -0400 Subject: [PATCH 1/2] corsair: wire the read-only NIGHTSWORD RGB driver into the app Pairs with OpenMouse-Project/mouse-protocol#63, which adds the Corsair codec and CorsairHidClient. The client has an explicit open(), so it joins NEEDS_OPEN in the controller. The supported-devices table gets a NIGHTSWORD RGB row marked supported with PID 0x1b5c, and the PID universe in its test learns CORSAIR_PRODUCT_IDS so the row is checked against the protocol package. Device art resolves to the generic placeholder by name until a redistributable render is uploaded. Co-Authored-By: Claude Fable 5.1 --- src/device/controller.ts | 3 ++- src/supported-mice.test.ts | 2 ++ src/supported-mice.ts | 3 +++ src/ui/device-images.test.ts | 5 +++++ src/ui/device-images.ts | 3 +++ 5 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/device/controller.ts b/src/device/controller.ts index 690f427..329c2ee 100644 --- a/src/device/controller.ts +++ b/src/device/controller.ts @@ -115,6 +115,7 @@ import { FinalmouseHidClient } from "@openmouse/protocol/drivers/finalmouse/hid" import { ModdoHidClient } from "@openmouse/protocol/drivers/moddo/hid"; import { NinjutsoHidClient } from "@openmouse/protocol/drivers/ninjutso/hid"; import { ZaunkoenigHidClient } from "@openmouse/protocol/drivers/zaunkoenig/hid"; +import { CorsairHidClient } from "@openmouse/protocol/drivers/corsair/hid"; import { TeevolutionHidClient } from "@openmouse/protocol/drivers/teevolution/hid"; import { teevolutionProfileForCid } from "@openmouse/protocol/teevolution"; import { VgnF2HidClient } from "@openmouse/protocol/drivers/vgn/hid"; @@ -193,7 +194,7 @@ function activeAs(...classes: ClientClass[]): T | null { const DM_CLASSES = [WLMouseHidClient, LamzuHidClient, AtkHidClient, NinjutsoHidClient] as const; const RAZER_CLASSES = [RazerHidClient, RazerViperMiniHidClient, RazerViperHidClient, RazerCobraHidClient] as const; -const NEEDS_OPEN = [TeevolutionHidClient, VgnF2HidClient, KeychronNapeHidClient, KeychronM6HidClient, ModdoHidClient, ZaunkoenigHidClient, FantechHidClient, WallhackMouseHidClient, WallhackKeyboardHidClient, GloriousHidClient, GloriousClassicHidClient, MchoseHidClient, MchoseDockHidClient] as const; +const NEEDS_OPEN = [TeevolutionHidClient, VgnF2HidClient, KeychronNapeHidClient, KeychronM6HidClient, ModdoHidClient, ZaunkoenigHidClient, CorsairHidClient, FantechHidClient, WallhackMouseHidClient, WallhackKeyboardHidClient, GloriousHidClient, GloriousClassicHidClient, MchoseHidClient, MchoseDockHidClient] as const; const PULSAR_CLASSES = [PulsarHidClient, PulsarProHidClient, PulsarXs1HidClient] as const; const logitechClient = (): LogitechHidppClient | null => activeAs(LogitechHidppClient); diff --git a/src/supported-mice.test.ts b/src/supported-mice.test.ts index 697ff0d..48ccae6 100644 --- a/src/supported-mice.test.ts +++ b/src/supported-mice.test.ts @@ -23,6 +23,7 @@ import { PULSAR_XS1_PRODUCT_IDS } from "@openmouse/protocol/pulsar"; import { RAZER_PRODUCTS } from "@openmouse/protocol/razer-devices"; import { TEEVOLUTION_PRODUCT_IDS } from "@openmouse/protocol/teevolution"; import { ZAUNKOENIG_PRODUCT_IDS } from "@openmouse/protocol/zaunkoenig"; +import { CORSAIR_PRODUCT_IDS } from "@openmouse/protocol/corsair"; import { MICE, STATUS, type Mouse, type Status } from "./supported-mice.ts"; @@ -94,6 +95,7 @@ const PID_UNIVERSE = new Set([ ...KEYCHRON_NAPE_PRODUCTS.keys(), ...TEEVOLUTION_PRODUCT_IDS, ...ZAUNKOENIG_PRODUCT_IDS, + ...CORSAIR_PRODUCT_IDS, ...NINJUTSO_LEGACY_MOUSE_PRODUCT_IDS, ...NINJUTSO_MOUSE_PRODUCT_IDS, ...NINJUTSO_LEGACY_RECEIVER_PRODUCT_IDS, diff --git a/src/supported-mice.ts b/src/supported-mice.ts index a2bbeb7..d2a5ff3 100644 --- a/src/supported-mice.ts +++ b/src/supported-mice.ts @@ -346,6 +346,9 @@ export const MICE: Mouse[] = [ note: "DPI presets, polling, LED color, sleep/dim timers, default lighting, and buttons are write-only; battery percentage/charging is read live (no firmware-query command in this protocol). PID 0x184a (wired mode) and 0x1848 (2.4 GHz mode) in SteelSeries Prime Mini Wireless driver" }, // CORSAIR ───────────────────────────────────────────────────────────── + { brand: "Corsair", model: "NIGHTSWORD RGB", status: "supported", req: 1, + pids: [0x1b5c], + note: "Read-only: identity, firmware, live DPI stages, polling rate, lift-off height, and angle snapping over the 0xffc2/usage-4 feature-report interface (fw 3.41). Close iCUE first — it holds the control interface. PID 0x1b5c in Corsair driver" }, { brand: "Corsair", model: "Harpoon RGB Pro", status: "driver", req: 5, note: "iCUE protocol — not implemented" }, { brand: "Corsair", model: "Katar Pro", status: "driver", req: 4, diff --git a/src/ui/device-images.test.ts b/src/ui/device-images.test.ts index 8890709..0ea7907 100644 --- a/src/ui/device-images.test.ts +++ b/src/ui/device-images.test.ts @@ -36,6 +36,11 @@ test("Razer Orochi V2 uses its own render over its Atheris receiver", () => { assert.equal(deviceImage({ vendorId: 0x1532, productId: 0x0094 } as HIDDevice), CDN + "razer-orochi-v2.png"); }); +test("Corsair NIGHTSWORD RGB falls back to the placeholder until art exists", () => { + assert.equal(deviceImage({ vendorId: 0x1b1c, productId: 0x1b5c } as HIDDevice, "Corsair NIGHTSWORD RGB"), CDN + "unknown-device.png"); + assert.equal(deviceImage(null, "CORSAIR NIGHTSWORD RGB Gaming Mouse"), CDN + "unknown-device.png"); +}); + test("fixture previews resolve product art without a HID device", () => { assert.equal(deviceImage(null, "CRDRAKO KO-ONE"), CDN + "crdrako-ko-one.png"); assert.equal(deviceImage(null, "Zaunkoenig M3K"), CDN + "zaunkoenig-m3k.png"); diff --git a/src/ui/device-images.ts b/src/ui/device-images.ts index 34eb4bd..69f975a 100644 --- a/src/ui/device-images.ts +++ b/src/ui/device-images.ts @@ -174,6 +174,9 @@ function resolveDeviceImageFilename(device: HIDDevice | null | undefined, displa if (/\bterra\s*pro\b/i.test(displayName)) return "teevolution-terra-pro.png"; if (/\bm-001\b/i.test(displayName)) return "wallhack-m-001.png"; if (/\bk-001\b/i.test(displayName)) return "wallhack-k-001.png"; + // Corsair NIGHTSWORD RGB has no product render yet; resolves to the generic + // placeholder until art is uploaded (then add ["1b1c:1b5c", ...] above). + if (/\bnightsword\b/i.test(displayName)) return "unknown-device.png"; // Newer supported-model artwork resolved from the reported product name. These // run after the shared-receiver checks above but before the Pulsar/unknown // catch-alls. Test-needed (likely) models are deliberately left out. From ebd7a7c1c5909848bd323ed40b10cbbabe35b44a Mon Sep 17 00:00:00 2001 From: snekxs <26660858+snekxs@users.noreply.github.com> Date: Sun, 6 Sep 2026 15:56:39 -0600 Subject: [PATCH 2/2] chore: bump @openmouse/protocol lockfile --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 94ab9ae..5beed48 100644 --- a/package-lock.json +++ b/package-lock.json @@ -481,7 +481,7 @@ }, "node_modules/@openmouse/protocol": { "version": "0.1.0", - "resolved": "git+ssh://git@github.com/OpenMouse-Project/mouse-protocol.git#badc8d35b00af881f6ebf9e9a812ffd903b97b76", + "resolved": "git+ssh://git@github.com/OpenMouse-Project/mouse-protocol.git#541f39eeb5b2cf5761bb1239e8d825a7052004fe", "engines": { "node": ">=20" }