diff --git a/src/drivers/atk/hid.test.ts b/src/drivers/atk/hid.test.ts index 6aef35f0..12d4c9ca 100644 --- a/src/drivers/atk/hid.test.ts +++ b/src/drivers/atk/hid.test.ts @@ -4,6 +4,7 @@ import test from "node:test"; import { atkDpiOptionsForSensor, atkPackDpiStage, atkPackDpiStageForSensor, atkUnpackDpiStageForSensor } from "@openmouse/protocol/atk"; import { wePackScalarPair } from "@openmouse/protocol/endgame-gear-we"; import { AtkHidClient } from "./hid.ts"; +import { atkCatalogDevice, atkCatalogSensorForCidMid } from "./products.ts"; import { PulsarHidClient } from "../pulsar/pulsar-hid.ts"; import { createSupportedClient, deviceBrand } from "../registry.ts"; import { SUPPORTED_HID_FILTERS } from "../vendors.ts"; @@ -1278,3 +1279,32 @@ test("an unlisted mouse falls back to its USB product string", () => { assert.equal(client.displayName(), "ATK Wireless mouse -1k dongle"); assert.equal(client.deviceBrand(), "ATK"); }); + +test("a mouse behind the generic 8K dongle-L is decoded with the sensor its CID/MID names, not the generic codec", async () => { + const fake = device(0x1278, "Wireless mouse 8k dongle-L"); + (fake as unknown as FakeAtkDevice).replies = [ + reply(0x10, 0x0000, [2, 77]), + reply(0x04, 0x0000, [0x5f, 0x01]), + reply(0x08, 0x0000, [0x01, 0x54, 0x01, 0x54, 0x00, 0x55]), + reply(0x08, 0x000c, atkPackDpiStageForSensor("PAW3395SE", 900, 900)!), + reply(0x12, 0x0000, [0x05, 0x0a]), + reply(0x08, 0x000a, [0x04, 0x51]), + reply(0x08, 0x00a9, [0x08, 0x4d, 0x00, 0x55, 0x1e, 0x37, 0x00, 0x55, 0x00, 0x55]), + reply(0x08, 0x00bd, [0x00, 0x55, 0x00, 0x55]), + ]; + + const status = await new AtkHidClient(fake).readStatus(); + assert.equal(status.dpi, 900); + assert.equal(status.dpiY, 900); + assert.deepEqual(status.dpiStages, [900]); +}); + +test("a CID/MID shared by models with different sensors stays on the generic codec behind a receiver", () => { + assert.equal(atkCatalogSensorForCidMid("2,77"), "PAW3395SE"); + assert.equal(atkCatalogSensorForCidMid("1,2"), "PAW3950Ultra"); + assert.equal(atkCatalogSensorForCidMid("2,5"), null); + assert.equal(atkCatalogSensorForCidMid("250,250"), null); + assert.equal(atkCatalogSensorForCidMid(null), null); + assert.equal(atkCatalogDevice(0x373b, 0x1278, "2,77")?.model, "ATK X1 SE NK"); + assert.equal(atkCatalogDevice(0x373b, 0x1278, "2,5"), undefined); +}); diff --git a/src/drivers/atk/hid.ts b/src/drivers/atk/hid.ts index 27002b2d..e3e88157 100644 --- a/src/drivers/atk/hid.ts +++ b/src/drivers/atk/hid.ts @@ -45,7 +45,7 @@ import { atkUnpackDpiStage, atkUnpackDpiStageForSensor, } from "@openmouse/protocol/atk"; -import { type AtkProduct, ATK_COMPX_PRODUCT_IDS, ATK_PRODUCTS, atkCatalogDevice, atkCatalogSensorToSensor } from "./products.ts"; +import { type AtkProduct, ATK_COMPX_PRODUCT_IDS, ATK_PRODUCTS, atkCatalogDevice, atkCatalogSensorForCidMid, atkCatalogSensorToSensor } from "./products.ts"; import type { AtkDeviceDescriptor, AtkDeviceFeatures } from "./device-catalog.generated.ts"; import type { AtkSensor } from "@openmouse/protocol/atk"; @@ -1615,7 +1615,7 @@ export class AtkHidClient { */ private sensorKey(): AtkSensor | null { if (this.product) return this.product.sensor; - return atkCatalogSensorToSensor(this.catalogEntry()?.sensor ?? null); + return atkCatalogSensorToSensor(this.catalogEntry()?.sensor ?? null) ?? atkCatalogSensorForCidMid(this.cidMid); } private usesSharedR1Transport(): boolean { diff --git a/src/drivers/atk/products.ts b/src/drivers/atk/products.ts index 31f4b205..9a6d6992 100644 --- a/src/drivers/atk/products.ts +++ b/src/drivers/atk/products.ts @@ -1,6 +1,8 @@ import type { AtkSensor } from "@openmouse/protocol/atk"; import { atkDeviceForHid, + atkDevicesForCidMid, + atkDevicesForHid, type AtkCatalogSensor, type AtkDeviceDescriptor, } from "./device-catalog.generated.ts"; @@ -49,7 +51,23 @@ export function atkCatalogDevice( productId: number, cidMid?: string | null, ): AtkDeviceDescriptor | undefined { - return atkDeviceForHid(vendorId, productId, cidMid); + const byUsbId = atkDeviceForHid(vendorId, productId, cidMid); + if (byUsbId || !cidMid || atkDevicesForHid(vendorId, productId).length > 0) return byUsbId; + const byCidMid = atkDevicesForCidMid(cidMid); + return byCidMid.length === 1 ? byCidMid[0] : undefined; +} + +/** + * Sensor shared by every catalog model with this CID/MID. Receivers enumerate + * under their own USB id, so the paired mouse is known only by CID/MID; the + * pair is not unique, but when every model sharing it has the same sensor the + * DPI codec is still unambiguous. + */ +export function atkCatalogSensorForCidMid(cidMid: string | null): AtkSensor | null { + if (!cidMid) return null; + const sensors = new Set(atkDevicesForCidMid(cidMid).map((device) => atkCatalogSensorToSensor(device.sensor))); + const [only] = sensors; + return sensors.size === 1 && only ? only : null; }