diff --git a/public/devices/README.md b/public/devices/README.md index 25187213..f8f6a995 100644 --- a/public/devices/README.md +++ b/public/devices/README.md @@ -32,6 +32,10 @@ before including it in a public release package. URL for the OP1 8K. Confirm redistribution terms before including it in a public release package. +`keychron-nape-pro.png` was supplied from Keychron's sysmgr cover CDN for the +Nape Pro. Confirm redistribution terms before including it in a public release +package. + `teevolution-terra-pro.png` was supplied from Teevolution's Terra PRO Shopify CDN product render. Confirm redistribution terms before including it in a public release package. diff --git a/public/devices/keychron-nape-pro.png b/public/devices/keychron-nape-pro.png new file mode 100644 index 00000000..c8a2fac7 Binary files /dev/null and b/public/devices/keychron-nape-pro.png differ diff --git a/src/control.ts b/src/control.ts index abe6ae5e..4280513e 100644 --- a/src/control.ts +++ b/src/control.ts @@ -1987,13 +1987,17 @@ async function activateClient(client: SupportedClient): Promise { dpiOptions = client.getDpiOptions(); configureDpiControl(status.dpi); showStatus(status); - } else if (client instanceof KeychronHidClient || client instanceof ModdoHidClient) { - if (client instanceof ModdoHidClient) { - activeModdoClient = client; - await client.open(); - } else { - activeKeychronClient = client; - } + } else if (client instanceof KeychronHidClient) { + activeKeychronClient = client; + await client.open(); + const status = await client.readStatus(); + deviceStatuses.set(client.device, status); + dpiOptions = client.getDpiOptions(); + configureDpiControl(status.dpi); + showStatus(status); + } else if (client instanceof ModdoHidClient) { + activeModdoClient = client; + await client.open(); const status = await client.readStatus(); deviceStatuses.set(client.device, status); dpiOptions = client.getDpiOptions(); diff --git a/src/preview-fixtures.ts b/src/preview-fixtures.ts index 56476962..c175f6be 100644 --- a/src/preview-fixtures.ts +++ b/src/preview-fixtures.ts @@ -335,7 +335,7 @@ const FINALMOUSE: MouseStatus = { batteryState: "Discharging", dpi: 1600, pollingRateHz: 8000, - supportedPollingRates: [500, 1000, 2000, 4000, 8000], + supportedPollingRates: [125, 500, 1000], activeProfile: null, connectionType: "Wireless", connectionDetail: "2.4 GHz receiver · -48 dBm", @@ -398,6 +398,30 @@ const NINJUTSO: MouseStatus = { firmware: ["Mouse 1611", "Receiver 0116"], }; +const KEYCHRON: MouseStatus = { + brand: "Keychron", + name: "Nape Pro", + ui: { + family: "keychron-nape", + defaultDisplayName: "Nape Pro", + hideUnsupportedPollingRates: true, + hideProcessingCard: true, + forceShowBattery: true, + pollingNote: "Nape Pro exposes polling through Keychron's misc HID commands when the firmware allows it.", + }, + batteryPercent: 76, + batteryState: "Discharging", + dpi: 800, + pollingRateHz: 1000, + supportedPollingRates: [500, 1000, 2000, 4000, 8000], + activeProfile: null, + connectionType: "Wired", + connectionDetail: "Wired USB · 90° orientation · DPI stage 2/5", + liftOffDistance: null, + supportedLiftOffDistances: [], + firmware: ["v1.0.4"], +}; + const LOGITECH_LEGACY: MouseStatus = { brand: "Logitech", name: "G402 Hyperion Fury", @@ -437,5 +461,6 @@ export const PREVIEW_FIXTURES: Record = { vgn: { label: "VGN F2 Master Plus", status: VGN }, finalmouse: { label: "Finalmouse UltralightX", status: FINALMOUSE }, ninjutso: { label: "Ninjutso Sora V3", status: NINJUTSO }, + keychron: { label: "Keychron Nape Pro", status: KEYCHRON }, "logitech-legacy": { label: "Logitech G402 (legacy DPI)", status: LOGITECH_LEGACY }, }; diff --git a/src/preview-modes.ts b/src/preview-modes.ts index 0b5f9d00..da65c0d8 100644 --- a/src/preview-modes.ts +++ b/src/preview-modes.ts @@ -24,6 +24,7 @@ export const PREVIEW_KEYS = [ "vgn", "finalmouse", "ninjutso", + "keychron", "logitech-legacy", ] as const; @@ -52,6 +53,7 @@ export function parsePreviewMode(value: string | null): PreviewMode | null { case "vgn": return "vgn"; case "finalmouse": return "finalmouse"; case "ninjutso": return "ninjutso"; + case "keychron": return "keychron"; case "logitech-legacy": return "logitech-legacy"; default: return null; } diff --git a/src/ui/device-images.ts b/src/ui/device-images.ts index 84de4374..02ef446e 100644 --- a/src/ui/device-images.ts +++ b/src/ui/device-images.ts @@ -34,6 +34,10 @@ const DEVICE_IMAGES: ReadonlyMap = new Map([ ["093a:e020", "/devices/ninjutso-ten.png"], ["093a:ea01", "/devices/ninjutso-ten.png"], ["093a:eb01", "/devices/ninjutso-ten.png"], + // Nape Pro wired / Link-KM receivers share the same shell artwork. + ["3434:0440", "/devices/keychron-nape-pro.png"], + ["3434:d026", "/devices/keychron-nape-pro.png"], + ["3434:d029", "/devices/keychron-nape-pro.png"], // Teevolution Terra Pro wired / receiver Compx transports. ["3554:f520", "/devices/teevolution-terra-pro.png"], ["3554:f522", "/devices/teevolution-terra-pro.png"], @@ -51,5 +55,6 @@ export function deviceImage(device: HIDDevice | null | undefined, displayName = if (mapped) return mapped; if (/superlight/i.test(displayName)) return "/devices/logitech-pro-x-superlight-2c.png"; if (/\bop1\s*8k\b/i.test(displayName)) return "/devices/endgame-gear-op1-8k.png"; + if (/\bnape\s*pro\b/i.test(displayName)) return "/devices/keychron-nape-pro.png"; return "/devices/unknown-device.png"; }