From 4e8b171f3f9a37559a0ec4588871fc1107919cdd Mon Sep 17 00:00:00 2001 From: snekxs <26660858+snekxs@users.noreply.github.com> Date: Mon, 5 Oct 2026 18:51:51 -0600 Subject: [PATCH] fix(attackshark): enable the X11 on the rebuilt Bridge config path A genuine X11 (0xfa60, receiver model id 0x55) over OpenMouse Bridge was refused by X11_REBUILT_CONFIG_MODELS, which only listed the Delux M600 Pro, so x11ConfigReachable stayed false and the mouse was status-only even though Bridge can reach the vendor feature reports. This is the failure reported in issue #161. Add 0x55: same receiver, protocol and rebuilt transport as the verified M600 Pro, with the receiver's documented 56-byte DPI frame. Models whose frame shape is not established (R1, X3, X6) stay read-only. The Bridge diagnostic is recorded under captures/attackshark-x11/. The write still wants confirmation on X11 hardware before release. Refs #161 --- captures/attackshark-x11/README.md | 36 +++++++++++++++++++++++++++++ src/drivers/attackshark/hid.test.ts | 33 ++++++++++++++++++++++++-- src/drivers/attackshark/hid.ts | 11 ++++++--- 3 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 captures/attackshark-x11/README.md diff --git a/captures/attackshark-x11/README.md b/captures/attackshark-x11/README.md new file mode 100644 index 0000000..b5b1df6 --- /dev/null +++ b/captures/attackshark-x11/README.md @@ -0,0 +1,36 @@ +# Attack Shark X11 fixtures + +Reference material for the X11 path in `src/drivers/attackshark/hid.ts`. + +Polled from one user's Windows machine (Firefox 157, OpenMouse webapp +`BETA · v2.0.c81` through OpenMouse Bridge, `ws://127.0.0.1:17846`) and posted +in [issue #161](https://github.com/OpenMouse-Project/mouse-protocol/issues/161). +The receiver is `0x1d57:0xfa60`; no serial string and no personal data are +included. + +## What Bridge lists for the receiver + +Seven collections are merged into one device, and every one of them reports no +feature report — the same rebuilt shape `captures/delux-m600-pro/windows-bridge.txt` +shows: + +``` +usage 0x1:6 feat[none] usage 0x1:80 feat[none] +usage 0xc:1 feat[none] usage 0xa:0 feat[none] +usage 0x1:2 feat[none] usage 0xb:0 feat[none] +usage 0x1:6 feat[none] +``` + +The `0x0b` config collection therefore declares nothing, which is exactly the +`x11RebuiltConfigCollection` shape. No receiver message (`03 40 01 +`) was captured in this session, so the status read returned +`settingsReady: false` and no battery. + +## Why the X11 is enabled on this path + +`X11_REBUILT_CONFIG_MODELS` accepts model id `0x55` on top of the +hardware-verified M600 Pro (`0x20`): the same receiver, protocol and rebuilt +transport, with the receiver's documented 56-byte DPI frame for `0xfa60` +(`x11UsesShortDpiReport` returns the long form). The write itself has not been +observed on X11 hardware yet — the reporter was asked to confirm DPI, polling +and battery on a build, per issue #161. diff --git a/src/drivers/attackshark/hid.test.ts b/src/drivers/attackshark/hid.test.ts index a448e26..2fa8bc1 100644 --- a/src/drivers/attackshark/hid.test.ts +++ b/src/drivers/attackshark/hid.test.ts @@ -477,16 +477,45 @@ test("Bridge on Windows: the M600 Pro is named by its unnumbered receiver messag assert.deepEqual(reads, []); }); -test("Bridge on Windows: other models are named but stay read-only", async () => { +test("Bridge on Windows: the X11 is named by its receiver message and becomes writable", async () => { resetAttackSharkX11DpiState(); resetAttackSharkX11RuntimeState(); - const { unit, sent, push } = windowsBridgeX11Receiver(); + const { unit, sent, reads, push } = windowsBridgeX11Receiver(); const client = new AttackSharkHidClient(unit, { batteryWaitMs: 0, receiverWriteGapMs: 0 }); await client.open(); + + // Issue #161: a genuine X11 (model id 0x55) over the same rebuilt collection. push([0x03, 0x55, 0x40, 0x01, 0x50]); const status = await client.readStatus(); assert.equal(status.name, "Attack Shark X11"); assert.equal(status.batteryPercent, 80); + assert.equal(status.ui?.settingsReady, true); + assert.equal(status.ui?.statusNote, undefined); + assert.deepEqual(status.supportedPollingRates, [125, 250, 500, 1000]); + assert.deepEqual(reads, []); + + // Same 0x06 packet as the verified M600 Pro write. + assert.equal(await client.setPollingRate(500), 500); + assert.deepEqual(sent, [{ reportId: 0x06, data: [0x09, 0x01, 0x02, 0xfd, 0, 0, 0, 0] }]); + sent.length = 0; + assert.equal(await client.setDpi(800), 800); + assert.equal(sent[0].reportId, 0x04); + // The receiver's 56-byte DPI frame the reference driver documents, minus the + // report id — not the M600 Pro's 52-byte one. + assert.equal(sent[0].data.length, 55); + assert.deepEqual(reads, []); +}); + +test("Bridge on Windows: models with no established frame shape still stay read-only", async () => { + resetAttackSharkX11DpiState(); + resetAttackSharkX11RuntimeState(); + const { unit, sent, push } = windowsBridgeX11Receiver(); + const client = new AttackSharkHidClient(unit, { batteryWaitMs: 0, receiverWriteGapMs: 0 }); + await client.open(); + // The R1 (0x10) uses a different DPI map per the reference driver. + push([0x03, 0x10, 0x40, 0x01, 0x05]); + const status = await client.readStatus(); + assert.equal(status.name, "Attack Shark R1"); assert.equal(status.ui?.settingsReady, false); await assert.rejects(() => client.setDpi(800), /not reachable from a browser/); assert.deepEqual(sent, []); diff --git a/src/drivers/attackshark/hid.ts b/src/drivers/attackshark/hid.ts index a08d8ee..2f75b98 100644 --- a/src/drivers/attackshark/hid.ts +++ b/src/drivers/attackshark/hid.ts @@ -110,8 +110,9 @@ const attackShark = (model: string, battery?: ReceiverModel["battery"]): Receive * Models identified by the model id their receiver messages carry. Attack * Shark ids: HarukaYamamoto0/attack-shark-x11-driver src/core/devices.ts. */ +const X11_MODEL_ID = 0x55; const X11_RECEIVER_MODELS: ReadonlyMap = new Map([ - [0x55, attackShark("X11", "percent")], + [X11_MODEL_ID, attackShark("X11", "percent")], // xb-bx/attack-shark-r1-driver reports charge as p2 * 10. [0x10, attackShark("R1", "tenths")], [0x85, attackShark("X6", "tenths")], @@ -253,8 +254,12 @@ const X11_DPI_PIDS: ReadonlySet = new Set([0xfa55, 0xfa60]); // A rebuilt config collection (see x11RebuiltConfigCollection) carries no // report sizes and no hint of which firmware is behind it, so writes through // it are only enabled once a receiver message names a model checked on that -// path (docs/delux-m600-pro-testing.md). -const X11_REBUILT_CONFIG_MODELS: ReadonlySet = new Set([DELUX_M600_PRO_MODEL_ID]); +// path (docs/delux-m600-pro-testing.md). The M600 Pro was captured through +// Bridge on Windows; the genuine X11 (model id 0x55 — issue #161) rides the +// same receiver and protocol and resolves to the receiver's 56-byte DPI +// frame, so it is enabled too. Models whose frame shape is not established +// (R1 0x10, X3 0x4d/0x4e, X6 0x85, …) stay read-only. +const X11_REBUILT_CONFIG_MODELS: ReadonlySet = new Set([DELUX_M600_PRO_MODEL_ID, X11_MODEL_ID]); // The firmware has no cheap "current DPI" command, so — exactly like the // reference driver — the last full six-stage table this process wrote (or