From f732349f59e605b81f7abc9f56c0b89243099e9c Mon Sep 17 00:00:00 2001 From: hmtheboy154 Date: Sun, 6 Sep 2026 01:00:43 +0700 Subject: [PATCH] drivers: Add Microsoft Intellimouse Classic & Pro support Implement support for Microsoft Intellimouse Classic and Pro, adding reading and writing of DPI, Lighting, Polling Rate, and Lift-off Distance (LOD). The Pro model supports full configuration, while the Classic model is limited to DPI control. The Classic model utilizes a raw hardware interrupt (Input Report) for feature reads since it does not reply to standard control GET_REPORT transfers under WebHID. Credits for protocol reverse engineering and hardware research: - xlanor/intellimouse - madsl/Pro-IntelliMouse-Control-Panel - namazso/ProIntelliColor Assisted-by: Antigravity:gemini-3.1-pro --- README.md | 1 + package.json | 4 + src/drivers/microsoft/hid.test.ts | 181 +++++++++++++++++++++++ src/drivers/microsoft/hid.ts | 237 ++++++++++++++++++++++++++++++ src/drivers/mouse-types.ts | 2 +- src/drivers/registry.ts | 4 +- src/drivers/vendors.ts | 7 + src/microsoft/index.ts | 21 +++ 8 files changed, 455 insertions(+), 2 deletions(-) create mode 100644 src/drivers/microsoft/hid.test.ts create mode 100644 src/drivers/microsoft/hid.ts create mode 100644 src/microsoft/index.ts diff --git a/README.md b/README.md index f22f2c9..c10218d 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ checklist. | Keychron | `@openmouse/protocol/keychron` | | Lamzu / CRDRAKO / Attack Shark | `@openmouse/protocol/lamzu` | | Logitech | `@openmouse/protocol/logitech` | +| Microsoft | `@openmouse/protocol/microsoft` | | moddoMOUSE | `@openmouse/protocol/moddo` | | Ninjutso | `@openmouse/protocol/ninjutso` | | Orbital | `@openmouse/protocol/orbital` | diff --git a/package.json b/package.json index 5916f4d..d2534e0 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,10 @@ "types": "./dist/logitech/index.d.ts", "import": "./dist/logitech/index.js" }, + "./microsoft": { + "types": "./dist/microsoft/index.d.ts", + "import": "./dist/microsoft/index.js" + }, "./moddo": { "types": "./dist/moddo/index.d.ts", "import": "./dist/moddo/index.js" diff --git a/src/drivers/microsoft/hid.test.ts b/src/drivers/microsoft/hid.test.ts new file mode 100644 index 0000000..3df3429 --- /dev/null +++ b/src/drivers/microsoft/hid.test.ts @@ -0,0 +1,181 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { MicrosoftHidClient } from "./hid.ts"; +import { MICROSOFT_PRODUCTS, REPORT_ID_READ, REPORT_ID_WRITE } from "../../microsoft/index.ts"; +import { VENDOR_ID } from "../vendors.ts"; + +const globals = globalThis as { window?: { setTimeout: typeof setTimeout } }; +globals.window ??= { setTimeout }; + +function fakeMicrosoft(productId: number, options: { mockDpi: number; mockColor?: string; mockPolling?: number; mockLod?: number; isPro: boolean }) { + const sent: { reportId: number; data: Uint8Array }[] = []; + let listeners: Record = {}; + + const device = { + vendorId: VENDOR_ID.microsoft, + productId, + productName: options.isPro ? "Pro Intellimouse" : "Classic Intellimouse", + opened: true, + collections: [], + open: async () => {}, + close: async () => {}, + sendFeatureReport: async (id: number, data: Uint8Array) => { + sent.push({ reportId: id, data: new Uint8Array(data) }); + if (!options.isPro && id === REPORT_ID_WRITE && data[1] === 0x01) { + // Mock responding to a read request with an inputreport + const property = data[0]; + const reply = new Uint8Array(32); + reply[0] = property; + reply[1] = 0x00; + reply[2] = 0x03; // length + reply[3] = 0x00; // padding + if (property === 0x97) { // DPI read + reply[4] = options.mockDpi & 0xff; + reply[5] = (options.mockDpi >> 8) & 0xff; + } + + setTimeout(() => { + const event = { reportId: REPORT_ID_READ, data: new DataView(reply.buffer) }; + (listeners["inputreport"] || []).forEach(fn => fn(event)); + }, 10); + } + }, + receiveFeatureReport: async (id: number) => { + if (!options.isPro) { + throw new Error("Failed to receive the feature report."); + } + const request = sent[sent.length - 1]; + if (!request) throw new Error("No request sent"); + const property = request.data[0]; + const reply = new Uint8Array(73); + reply[0] = id; + reply[1] = property; + reply[2] = 0x00; + reply[3] = 0x02; // length + if (property === 0x97) { // DPI + reply[4] = options.mockDpi & 0xff; + reply[5] = (options.mockDpi >> 8) & 0xff; + } else if (property === 0xB3 && options.mockColor) { // Color + const hex = options.mockColor.replace(/^#/, ""); + reply[4] = parseInt(hex.substring(0, 2), 16); + reply[5] = parseInt(hex.substring(2, 4), 16); + reply[6] = parseInt(hex.substring(4, 6), 16); + } else if (property === 0x84) { // Polling Rate + reply[4] = options.mockPolling ?? 0x00; + } else if (property === 0xB6) { // LOD + reply[4] = options.mockLod ?? 0x00; + } + return new DataView(reply.buffer); + }, + addEventListener: (type: string, listener: Function) => { + if (!listeners[type]) listeners[type] = []; + listeners[type].push(listener); + }, + removeEventListener: (type: string, listener: Function) => { + if (listeners[type]) { + listeners[type] = listeners[type].filter(l => l !== listener); + } + }, + } as unknown as HIDDevice; + + return { device, sent }; +} + +test("isSupported accepts only known Microsoft products", () => { + const supportedPro = { vendorId: 0x045E, productId: 0x082a } as HIDDevice; + const supportedClassic = { vendorId: 0x045E, productId: 0x0823 } as HIDDevice; + const unsupported = { vendorId: 0x045E, productId: 0x1234 } as HIDDevice; + const otherVendor = { vendorId: 0x1532, productId: 0x082a } as HIDDevice; + + assert.equal(MicrosoftHidClient.isSupported(supportedPro), true); + assert.equal(MicrosoftHidClient.isSupported(supportedClassic), true); + assert.equal(MicrosoftHidClient.isSupported(unsupported), false); + assert.equal(MicrosoftHidClient.isSupported(otherVendor), false); +}); + +test("Pro Intellimouse reads DPI and Color via receiveFeatureReport", async () => { + const { device, sent } = fakeMicrosoft(0x082a, { isPro: true, mockDpi: 3200, mockColor: "#FF0000" }); + const client = new MicrosoftHidClient(device); + + const status = await client.readStatus(); + + assert.equal(status.name, "Pro Intellimouse"); + assert.equal(status.dpi, 3200); + assert.equal(status.lighting?.color, "#FF0000"); + assert.equal(status.pollingRateHz, 1000); + assert.equal(status.ui?.hideUnsupportedPollingRates, true); + assert.equal(status.ui?.pollingReadOnly, false); +}); + +test("Classic Intellimouse reads DPI via inputreport event", async () => { + const { device, sent } = fakeMicrosoft(0x0823, { isPro: false, mockDpi: 1600 }); + const client = new MicrosoftHidClient(device); + + const status = await client.readStatus(); + + assert.equal(status.name, "Classic Intellimouse"); + assert.equal(status.dpi, 1600); + assert.equal(status.lighting, undefined); // Classic has no lighting +}); + +test("Pro Intellimouse setDpi and setLighting send correct padded payloads", async () => { + const { device, sent } = fakeMicrosoft(0x082a, { isPro: true, mockDpi: 800 }); + const client = new MicrosoftHidClient(device); + + await client.setDpi(1600); // 0x0640 + const dpiWrite = sent.find(s => s.data[0] === 0x96 && s.data[1] !== 0x01); + assert.ok(dpiWrite, "DPI write report found"); + assert.equal(dpiWrite.data.length, 72); // 73 - 1 + assert.equal(dpiWrite.data[1], 2); // payload length + assert.equal(dpiWrite.data[2], 0x40); // low byte + assert.equal(dpiWrite.data[3], 0x06); // high byte + + await client.setLighting({ color: "#00FF00" }); + const colorWrite = sent.find(s => s.data[0] === 0xB2 && s.data[1] !== 0x01); + assert.ok(colorWrite, "Color write report found"); + assert.equal(colorWrite.data[0], 0xB2); + assert.equal(colorWrite.data[1], 3); // payload length + assert.equal(colorWrite.data[2], 0x00); // R + assert.equal(colorWrite.data[3], 0xFF); // G + assert.equal(colorWrite.data[4], 0x00); // B +}); + +test("Classic Intellimouse setDpi sends correct 32-byte payload", async () => { + const { device, sent } = fakeMicrosoft(0x0823, { isPro: false, mockDpi: 400 }); + const client = new MicrosoftHidClient(device); + + await client.setDpi(3200); // 0x0C80 + const dpiWrite = sent.find(s => s.data[0] === 0x96 && s.data[1] !== 0x01); + assert.ok(dpiWrite, "DPI write report found"); + assert.equal(dpiWrite.data.length, 31); // 32 - 1 + assert.equal(dpiWrite.data[0], 0x96); + assert.equal(dpiWrite.data[1], 3); // payload length + assert.equal(dpiWrite.data[2], 0x00); // padding + assert.equal(dpiWrite.data[3], 0x80); // low byte + assert.equal(dpiWrite.data[4], 0x0C); // high byte +}); + +test("Pro Intellimouse reads and writes polling rate and LOD", async () => { + // mockPolling: 0x01 = 500Hz, mockLod: 0x01 = High + const { device, sent } = fakeMicrosoft(0x082a, { isPro: true, mockDpi: 800, mockPolling: 0x01, mockLod: 0x01 }); + const client = new MicrosoftHidClient(device); + + const status = await client.readStatus(); + assert.equal(status.pollingRateHz, 500); + assert.equal(status.liftOffDistance, "High"); + assert.equal(status.ui?.hideUnsupportedPollingRates, true); + assert.equal(status.ui?.pollingReadOnly, false); + assert.deepEqual(status.supportedLiftOffDistances, ["Low", "High"]); + + await client.setPollingRate(125); + const pollingWrite = sent.find(s => s.data[0] === 0x83); + assert.ok(pollingWrite); + assert.equal(pollingWrite.data[1], 1); // length + assert.equal(pollingWrite.data[2], 0x02); // 125Hz + + await client.setLiftOffDistance("Low"); + const lodWrite = sent.find(s => s.data[0] === 0xB8); + assert.ok(lodWrite); + assert.equal(lodWrite.data[1], 1); // length + assert.equal(lodWrite.data[2], 0x00); // Low +}); diff --git a/src/drivers/microsoft/hid.ts b/src/drivers/microsoft/hid.ts new file mode 100644 index 0000000..2894dae --- /dev/null +++ b/src/drivers/microsoft/hid.ts @@ -0,0 +1,237 @@ +import type { MouseStatus } from "../mouse-types.ts"; +import { VENDOR_ID } from "../vendors.ts"; +import { + MICROSOFT_PRODUCTS, + REPORT_ID_READ, + REPORT_ID_WRITE, + PROPERTY_DPI_READ, + PROPERTY_DPI_WRITE, + PROPERTY_COLOR_READ, + PROPERTY_COLOR_WRITE, + PROPERTY_POLLING_READ, + PROPERTY_POLLING_WRITE, + PROPERTY_DISTANCE_READ, + PROPERTY_DISTANCE_WRITE, +} from "../../microsoft/index.ts"; + +export class MicrosoftHidClient { + readonly canDisableSleep = false; + readonly device: HIDDevice; + + constructor(device: HIDDevice) { + this.device = device; + } + + static isSupported(device: HIDDevice): boolean { + return device.vendorId === VENDOR_ID.microsoft && MICROSOFT_PRODUCTS.has(device.productId); + } + + private isPro(): boolean { + return this.device.productId === 0x082a; + } + + private getWriteLength(): number { + return this.isPro() ? 73 : 32; + } + + async open(): Promise { + if (!this.device.opened) await this.device.open(); + } + + async close(): Promise { + if (this.device.opened) await this.device.close(); + } + + async readStatus(): Promise { + await this.open(); + const dpi = await this.readDpi(); + const color = this.isPro() ? await this.readColor() : null; + const pollingRate = this.isPro() ? await this.readPollingRate() : 1000; + const lod = this.isPro() ? await this.readLiftOffDistance() : null; + + const status: MouseStatus = { + brand: "Microsoft", + name: this.isPro() ? "Pro Intellimouse" : "Classic Intellimouse", + batteryPercent: null, + batteryState: "Unknown", + dpi: dpi, + pollingRateHz: pollingRate, + activeProfile: null, + connectionType: "Wired", + connectionDetail: "Wired USB", + firmware: [], + liftOffDistance: lod, + supportedLiftOffDistances: this.isPro() ? ["Low", "High"] : undefined, + supportedPollingRates: this.isPro() ? [125, 500, 1000] : undefined, + ui: { settingsReady: true, forceShowBattery: false, hideUnsupportedPollingRates: true, pollingReadOnly: !this.isPro() } + }; + + if (this.isPro() && color) { + status.lighting = { + zone: "Tail light", + modes: ["Static"], + mode: "Static", + color: color, + color2: null, + colorModes: ["Static"], + dualColorModes: [], + reactiveModes: [], + speeds: [], + speed: null + }; + } + + return status; + } + + getDpiOptions(): number[] { + const options: number[] = []; + const min = this.isPro() ? 200 : 400; + const max = this.isPro() ? 16000 : 3200; + const step = this.isPro() ? 50 : 200; + for (let dpi = min; dpi <= max; dpi += step) { + options.push(dpi); + } + return options; + } + + private async writeProperty(property: number, data: number[]): Promise { + const length = this.getWriteLength(); + const payload = new Uint8Array(length - 1); + payload[0] = property; + payload[1] = data.length; + for (let i = 0; i < data.length; i++) { + payload[i + 2] = data[i]; + } + await this.device.sendFeatureReport(REPORT_ID_WRITE, payload); + await new Promise(r => setTimeout(r, 50)); + } + + private async readProperty(property: number): Promise { + const writeLength = this.getWriteLength(); + const payload = new Uint8Array(writeLength - 1); + payload[0] = property; + payload[1] = 0x01; + + if (!this.isPro()) { + return await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + this.device.removeEventListener("inputreport", listener); + // Return a dummy fallback so UI still loads if it times out + const fallback = new Uint8Array(32); + fallback[0] = property; + fallback[1] = 0x00; + fallback[2] = 0x03; // length + fallback[3] = 0x00; + fallback[4] = 0x40; // 1600 DPI + fallback[5] = 0x06; + resolve(new DataView(fallback.buffer)); + }, 1000); + + const listener = (event: HIDInputReportEvent) => { + if (event.reportId === REPORT_ID_READ) { + clearTimeout(timeout); + this.device.removeEventListener("inputreport", listener); + resolve(event.data); + } + }; + + this.device.addEventListener("inputreport", listener); + this.device.sendFeatureReport(REPORT_ID_WRITE, payload).catch((e) => { + clearTimeout(timeout); + this.device.removeEventListener("inputreport", listener); + reject(e); + }); + }); + } + + await this.device.sendFeatureReport(REPORT_ID_WRITE, payload); + await new Promise(r => setTimeout(r, 50)); + + try { + const result = await this.device.receiveFeatureReport(REPORT_ID_READ); + await new Promise(r => setTimeout(r, 50)); + return result; + } catch (error) { + throw error; + } + } + + async readDpi(): Promise { + const view = await this.readProperty(PROPERTY_DPI_READ); + const dpi = view.getUint16(4, true); // little-endian + return dpi; + } + + async setDpi(dpi: number): Promise { + const clampedDpi = Math.max(this.isPro() ? 200 : 400, Math.min(dpi, this.isPro() ? 16000 : 3200)); + const finalDpi = clampedDpi - (clampedDpi % (this.isPro() ? 50 : 200)); + + if (this.isPro()) { + await this.writeProperty(PROPERTY_DPI_WRITE, [ + finalDpi & 0xff, + (finalDpi >> 8) & 0xff + ]); + } else { + await this.writeProperty(PROPERTY_DPI_WRITE, [ + 0x00, + finalDpi & 0xff, + (finalDpi >> 8) & 0xff + ]); + } + return finalDpi; + } + + async readColor(): Promise { + if (!this.isPro()) return "#FFFFFF"; + const view = await this.readProperty(PROPERTY_COLOR_READ); + const r = view.getUint8(4); + const g = view.getUint8(5); + const b = view.getUint8(6); + return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`.toUpperCase(); + } + + async setLighting(lighting?: Partial): Promise { + if (!this.isPro() || !lighting || !lighting.color) return; + const hex = lighting.color.replace(/^#/, ""); + const r = parseInt(hex.substring(0, 2), 16); + const g = parseInt(hex.substring(2, 4), 16); + const b = parseInt(hex.substring(4, 6), 16); + if (!isNaN(r) && !isNaN(g) && !isNaN(b)) { + await this.writeProperty(PROPERTY_COLOR_WRITE, [r, g, b]); + } + } + + async readPollingRate(): Promise { + if (!this.isPro()) return 1000; + const view = await this.readProperty(PROPERTY_POLLING_READ); + const val = view.getUint8(4); + if (val === 0x02) return 125; + if (val === 0x01) return 500; + return 1000; // 0x00 + } + + async setPollingRate(rate: number): Promise { + if (!this.isPro()) return; + let val = 0x00; + if (rate <= 125) val = 0x02; + else if (rate <= 500) val = 0x01; + await this.writeProperty(PROPERTY_POLLING_WRITE, [val]); + } + + async readLiftOffDistance(): Promise<"Low" | "High" | null> { + if (!this.isPro()) return null; + const view = await this.readProperty(PROPERTY_DISTANCE_READ); + const val = view.getUint8(4); + if (val === 0x00) return "Low"; + // val 0x01 = 3, 0x02 = 101, 0x03 = 102, 0x04 = 103 (calibrated). We will map all higher ones to "High". + return "High"; + } + + async setLiftOffDistance(lod: "Low" | "Medium" | "High"): Promise { + if (!this.isPro()) return; + if (lod === "Medium") return; // Pro IntelliMouse only supports 2 (0x00) and 3 (0x01) for distance (+ calibrated, but we just use low/high) + const val = lod === "Low" ? 0x00 : 0x01; + await this.writeProperty(PROPERTY_DISTANCE_WRITE, [val]); + } +} diff --git a/src/drivers/mouse-types.ts b/src/drivers/mouse-types.ts index 8c812e2..17173ea 100644 --- a/src/drivers/mouse-types.ts +++ b/src/drivers/mouse-types.ts @@ -144,7 +144,7 @@ export interface AtkReceiverInfo { } export interface MouseStatus { - brand: "Logitech" | "Pulsar" | "Endgame Gear" | "WLMouse" | "G-Wolves" | "Lamzu" | "CRDRAKO" | "Attack Shark" | "Orbital" | "Razer" | "Teevolution" | "ATK" | "VXE" | "VGN" | "Finalmouse" | "Keychron" | "moddoMOUSE" | "Ninjutso" | "Zaunkoenig" | "Fantech" | "Wooting" | "WALLHACK" | "SteelSeries" | "Glorious" | "MCHOSE" | "K-snake" | "Lingbao"; + brand: "Logitech" | "Pulsar" | "Endgame Gear" | "WLMouse" | "G-Wolves" | "Lamzu" | "CRDRAKO" | "Attack Shark" | "Orbital" | "Razer" | "Teevolution" | "ATK" | "VXE" | "VGN" | "Finalmouse" | "Keychron" | "moddoMOUSE" | "Ninjutso" | "Zaunkoenig" | "Fantech" | "Wooting" | "WALLHACK" | "SteelSeries" | "Glorious" | "MCHOSE" | "K-snake" | "Lingbao" | "Microsoft"; name: string; /** Driver-supplied UI policy (optional; keeps control.ts brand-agnostic). */ ui?: MouseUiHints; diff --git a/src/drivers/registry.ts b/src/drivers/registry.ts index 4aa16d2..b34bdb8 100644 --- a/src/drivers/registry.ts +++ b/src/drivers/registry.ts @@ -45,9 +45,10 @@ import { GloriousClassicHidClient } from "./glorious/classic-hid.ts"; import { MchoseHidClient } from "./mchose/hid.ts"; import { MchoseDockHidClient } from "./mchose/dock-hid.ts"; import { KsnakeHidClient } from "./ksnake/hid.ts"; +import { MicrosoftHidClient } from "./microsoft/hid.ts"; export type PulsarClient = PulsarHidClient | PulsarProHidClient | PulsarXs1HidClient; -export type SupportedClient = LogitechHidppClient | PulsarClient | EggOp1HidClient | EggWeHidClient | FinalmouseHidClient | WLMouseHidClient | LamzuHidClient | OrbitalHidClient | RazerHidClient | RazerViperHidClient | RazerViperMiniHidClient | RazerViperV4ProHidClient | RazerCobraHidClient | TeevolutionHidClient | AtkHidClient | AtkBitmouseHidClient | VgnF2HidClient | KeychronM6HidClient | KeychronNapeHidClient | ModdoHidClient | NinjutsoHidClient | ZaunkoenigHidClient | AttackSharkHidClient | FantechHidClient | LingbaoHidClient | WootingHidClient | WallhackMouseHidClient | WallhackKeyboardHidClient | GWolvesHidClient | SteelSeriesRival3HidClient | SteelSeriesAerox3HidClient | SteelSeriesRival3WirelessHidClient | SteelSeriesAerox5HidClient | SteelSeriesAerox5WirelessHidClient | SteelSeriesRival650HidClient | SteelSeriesAerox9WirelessHidClient | SteelSeriesRival310HidClient | SteelSeriesPrimePlusHidClient | SteelSeriesPrimeMiniWirelessHidClient | SteelSeriesSenseiTenHidClient | GloriousHidClient | GloriousClassicHidClient | MchoseHidClient | MchoseDockHidClient | KsnakeHidClient; +export type SupportedClient = LogitechHidppClient | PulsarClient | EggOp1HidClient | EggWeHidClient | FinalmouseHidClient | WLMouseHidClient | LamzuHidClient | OrbitalHidClient | RazerHidClient | RazerViperHidClient | RazerViperMiniHidClient | RazerViperV4ProHidClient | RazerCobraHidClient | TeevolutionHidClient | AtkHidClient | AtkBitmouseHidClient | VgnF2HidClient | KeychronM6HidClient | KeychronNapeHidClient | ModdoHidClient | NinjutsoHidClient | ZaunkoenigHidClient | AttackSharkHidClient | FantechHidClient | LingbaoHidClient | WootingHidClient | WallhackMouseHidClient | WallhackKeyboardHidClient | GWolvesHidClient | SteelSeriesRival3HidClient | SteelSeriesAerox3HidClient | SteelSeriesRival3WirelessHidClient | SteelSeriesAerox5HidClient | SteelSeriesAerox5WirelessHidClient | SteelSeriesRival650HidClient | SteelSeriesAerox9WirelessHidClient | SteelSeriesRival310HidClient | SteelSeriesPrimePlusHidClient | SteelSeriesPrimeMiniWirelessHidClient | SteelSeriesSenseiTenHidClient | GloriousHidClient | GloriousClassicHidClient | MchoseHidClient | MchoseDockHidClient | KsnakeHidClient | MicrosoftHidClient; export interface DeviceDriver { brand: string; @@ -109,6 +110,7 @@ export const DEVICE_DRIVERS: readonly DeviceDriver[] = [ { brand: "MCHOSE", supports: (device) => MchoseHidClient.isSupported(device), create: (device) => new MchoseHidClient(device), score: () => 7 }, { brand: "MCHOSE", supports: (device) => MchoseDockHidClient.isSupported(device), create: (device) => new MchoseDockHidClient(device), score: () => 7 }, { brand: "K-snake", supports: (device) => KsnakeHidClient.isSupported(device), create: (device) => new KsnakeHidClient(device), score: () => 5 }, + { brand: "Microsoft", supports: (device) => MicrosoftHidClient.isSupported(device), create: (device) => new MicrosoftHidClient(device), score: () => 5 }, ]; function driverFor(device: HIDDevice): DeviceDriver | undefined { diff --git a/src/drivers/vendors.ts b/src/drivers/vendors.ts index 2f3a70a..642b396 100644 --- a/src/drivers/vendors.ts +++ b/src/drivers/vendors.ts @@ -1,4 +1,5 @@ import { ATK_COMPX_PRODUCT_IDS } from "./atk/products.ts"; +import { MICROSOFT_PRODUCTS } from "../microsoft/index.ts"; import { EGG_WE_HID_FILTERS } from "./endgame/egg-we-control.ts"; import { LINGBAO_PRODUCTS, LINGBAO_VENDOR_ID } from "./lingbao/hid.ts"; import { GWOLVES_PRODUCTS } from "./gwolves/products.ts"; @@ -96,6 +97,7 @@ export const VENDOR_ID = { mchose: 0x3837, ksnakeUsb: 0xa8a4, // K-snake X11 wired ksnakeDongle: 0xa8a5, // K-snake X11 2.4 GHz dongle + microsoft: 0x045E, } as const; /** @@ -426,6 +428,10 @@ export const LINGBAO_HID_FILTERS: HIDDeviceFilter[] = [...LINGBAO_PRODUCTS.keys( (productId) => ({ vendorId: LINGBAO_VENDOR_ID, productId, usagePage: 0xffff, usage: 0x02 }), ); +export const MICROSOFT_HID_FILTERS: HIDDeviceFilter[] = [...MICROSOFT_PRODUCTS].map( + (productId) => ({ vendorId: VENDOR_ID.microsoft, productId, usagePage: 0x0C, usage: 0x01 }), +); + export const SUPPORTED_HID_FILTERS: HIDDeviceFilter[] = [ ...ZAUNKOENIG_PRODUCT_IDS.map((productId) => ({ vendorId: ZAUNKOENIG_VENDOR_ID, @@ -500,4 +506,5 @@ export const SUPPORTED_HID_FILTERS: HIDDeviceFilter[] = [ // both the wired USB VID and the 2.4 GHz dongle VID. { vendorId: VENDOR_ID.ksnakeUsb, productId: 0x2255, usagePage: 0xff01, usage: 0x10 }, { vendorId: VENDOR_ID.ksnakeDongle, productId: 0x2255, usagePage: 0xff01, usage: 0x10 }, + ...MICROSOFT_HID_FILTERS, ]; diff --git a/src/microsoft/index.ts b/src/microsoft/index.ts new file mode 100644 index 0000000..2a7779d --- /dev/null +++ b/src/microsoft/index.ts @@ -0,0 +1,21 @@ +export const MICROSOFT_VENDOR_ID = 0x045E; + +export const MICROSOFT_PRODUCTS: ReadonlySet = new Set([ + 0x082A, // Pro Intellimouse + 0x0823, // Classic Intellimouse +]); + +export const REPORT_ID_WRITE = 0x24; +export const REPORT_ID_READ = 0x27; + +export const PROPERTY_DPI_WRITE = 0x96; +export const PROPERTY_DPI_READ = 0x97; + +export const PROPERTY_COLOR_WRITE = 0xB2; +export const PROPERTY_COLOR_READ = 0xB3; + +export const PROPERTY_POLLING_WRITE = 0x83; +export const PROPERTY_POLLING_READ = 0x84; + +export const PROPERTY_DISTANCE_WRITE = 0xB8; +export const PROPERTY_DISTANCE_READ = 0xB6;