From ccd6be0ac10b53eb56ba0c38ff6334f2c2b3d62d Mon Sep 17 00:00:00 2001 From: Kunal Agrawal <66416646+kunalagra@users.noreply.github.com> Date: Sat, 31 Oct 2026 14:54:36 +0530 Subject: [PATCH] feat(devices): support the Amkette XS series Companion to the @openmouse/protocol change that adds the Amkette XS driver (UI family "amkette-xs"). - traits: give the amkette-xs family the advanced section and auto sleep, so the sleep card shows the driver's 1-15 minute options. - device images: map the "XS Flow 2 Speed" model name to amkette-xs-flow-2-speed.png; the nameless "XS series" fallback and the shared 0x04F3 ids keep the placeholder. The artwork itself is not uploaded yet. Co-Authored-By: Claude Opus 5.5 --- src/device/traits.ts | 3 +++ src/shared/device-images.test.ts | 10 ++++++++++ src/shared/device-images.ts | 5 +++++ 3 files changed, 18 insertions(+) diff --git a/src/device/traits.ts b/src/device/traits.ts index 00159e46..f79a719f 100644 --- a/src/device/traits.ts +++ b/src/device/traits.ts @@ -83,6 +83,9 @@ const BY_FAMILY: Readonly>> = { // it publishes its own sleep and debounce option lists. "mchose-v3": { advancedSection: true, sleep: true, debounce: true }, dareu: { advancedSection: true, sleep: true }, + // Amkette's XS firmware stores auto sleep as whole minutes and publishes its + // own list through getSleepOptions. + "amkette-xs": { advancedSection: true, sleep: true }, // The Model O2 Pro 4K/8K writes a debounce block and a motion sync flag; it // has no sleep command wired in, so no sleep flag. "glorious-core2": { advancedSection: true, debounce: true }, diff --git a/src/shared/device-images.test.ts b/src/shared/device-images.test.ts index 074ce838..ae8d547c 100644 --- a/src/shared/device-images.test.ts +++ b/src/shared/device-images.test.ts @@ -40,6 +40,16 @@ test("PRO X3 SUPERSTRIKE gets its own render, not the X2's", () => { assert.equal(deviceImage(null, "Attack Shark X3"), CDN + "attackshark-x3.png"); }); +test("Amkette XS Flow 2 Speed resolves by model name, never from the shared ids", () => { + assert.equal(deviceImage(dev(0x04f3, 0x026e), "XS Flow 2 Speed"), CDN + "amkette-xs-flow-2-speed.png"); + assert.equal(deviceImage(dev(0x04f3, 0x026d), "XS Flow 2 Speed"), CDN + "amkette-xs-flow-2-speed.png"); + assert.equal(deviceImage(null, "Amkette XS Flow 2 Speed"), CDN + "amkette-xs-flow-2-speed.png"); + // Without a model name the mouse could be any XS model, or share ids with a + // Ryunix Kyu Pro MX1, so it keeps the placeholder. + assert.equal(deviceImage(dev(0x04f3, 0x026e), "XS series"), CDN + "unknown-device.png"); + assert.equal(deviceImage(dev(0x04f3, 0x026e), "Kyu Pro MX1"), CDN + "unknown-device.png"); +}); + test("Razer Orochi V2 resolves by name", () => { assert.equal(deviceImage(dev(0x1532, 0x0094), "Razer Orochi V2"), CDN + "razer-orochi-v2.png"); }); diff --git a/src/shared/device-images.ts b/src/shared/device-images.ts index 413c18dd..bfdffe69 100644 --- a/src/shared/device-images.ts +++ b/src/shared/device-images.ts @@ -65,6 +65,11 @@ function computeDeviceImageFilename(displayName: string): string { } if (/\br5\s*ultra\b/i.test(displayName)) return "attackshark-r5-ultra.png"; if (/\bfloat\s*88\b/i.test(displayName)) return "ipi-float-88.png"; + // Amkette XS Flow 2 Speed: the driver reports this model name only once the + // mouse names itself ("XS Speed" over USB, "XS speed N" over Bluetooth). Its + // nameless fallback, "XS series", could be any XS model, so it keeps the + // placeholder rather than guess. + if (/\bxs\s*flow\s*2\s*speed\b/i.test(displayName)) return "amkette-xs-flow-2-speed.png"; if (/\batk\s*zero\b/i.test(displayName)) return "atk-zero.png"; // R2 shares PID 0x402D with the Lingbao M5 Pro, so it can only be told apart // by the name the gearhub driver reads back from the device id.