Skip to content

attackshark: native X11 polling, DPI and battery over feature reports - #103

Merged
snekxs merged 3 commits into
OpenMouse-Project:mainfrom
hyusband:attack-shark-x11-native-polling
Sep 14, 2026
Merged

snekxs merged 3 commits into
OpenMouse-Project:mainfrom
hyusband:attack-shark-x11-native-polling

Conversation

@hyusband

@hyusband hyusband commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Problem

WebHID only surfaces the Attack Shark X11's Consumer collection, which declares no feature reports, so neither the 0x06 polling command nor the 0x04 DPI table is reachable in a browser (confirmed on hardware: sendFeatureReport throws NotAllowedError). Native HID adapters (the Desktop app's Tauri TauriHidDevice and the Node adapter) can reach them, but report collections: [] because they do not parse the report descriptor. detectFamily() keyed entirely on collections, so on those transports the family was null: the device appeared but settingsReady stayed false and every write threw. The Overview also showed DPI placeholder, 0 Hz and Battery N/A.

Change

Family detection. When a 0x1d57 adapter reports no collections, identify the X11 family from the vendor/product id (fa55/fa60/fa61) and treat the config channel as writable. 0x25a7 gets the same vendor-id fallback.

Polling. getSupportedPollingRates(), settingsReady and forceShowBattery follow that, and setPollingRate() sends the same 0x06 report the 0x1d57 path already builds. The firmware has no polling read-back, so the last value applied (default 1,000 Hz, matching the reference driver) is cached per product id and reported instead of 0.

DPI. Ports the X11's six-stage table from the community driver's DpiBuilder: report 0x04, stage bytes from the hardware register map, stage mask (>12000), per-stage high flags (10100-12000 / 20100-22000), active stage, angle-snap/ripple bits, and a 16-bit big-endian checksum over bytes 3..49. Wired sends 52 bytes, wireless 56. The full table is remembered per product id and re-sent whole on every edit; setDpi edits the active stage, setDpiStageValue one stage, setActiveDpiStage selects, and setAngleSnapping/setRippleControl use the same report. readStatus() exposes dpi/dpiStages/activeDpiStage/angleSnapping/rippleControl and the shared dpiStageEditor hint; readX11DpiState() best-effort GETs report 0x04 and adopts the decoded table when the signature and checksum validate. R1 (0xfa61) stays out until its separate DpiBuilder is ported.

Battery. The wireless receiver streams 03 55 40 01 <pct> on interface 2 on its own. Since the desktop app opens a short-lived client per read, readStatus() now gives the stream a bounded moment (2.5 s, skipped while a sample is younger than 30 s so the 5 s auto-refresh does not pay it every tick) and keeps the last value per product id. The wired PIDs have no documented battery endpoint and keep reporting none; the firmware exposes no charging flag on this endpoint, so the state is Discharging when a percentage is known.

Browser behavior is unchanged: the collection gate still refuses X11 entries and the setters still throw the native-only error. The file header is corrected to note the config reports are reachable through the OS HID stack on interface 2's &col04 sub-collection (node-hid with stock input.inf, no Zadig/WinUSB).

Verification

  • attackshark/dpi.test.ts: encode/clamp, exact default-table layout against the reference, wired length, full round-trip across every stage category, report-id-stripped decode, signature/checksum rejection.
  • attackshark/hid.test.ts: a collections-less adapter reports settingsReady, the X11 name, Wireless, [125,250,500,1000], the default 1,600 DPI table and editor hint; setPollingRate(500) emits 0x06 with 09 01 02 FD 00 00 00 00; setDpi(3200) emits report 0x04 with the stage byte for 3,200; a battery input packet updates the reported percentage and the applied polling rate survives into the next read.
  • tsc -p tsconfig.json --noEmit clean; 19/19 attackshark tests pass.
  • Physical 500<->1000 / DPI / battery checks pending on hardware.

WebHID only surfaces the X11's Consumer collection, so the 0x06 polling feature report is unreachable in a browser. Native adapters (Tauri/Node) report no collections at all and were therefore not identified, so the Desktop app detected the X11 but refused every write.

Detect the 1d57 X11 family from the vendor/product id when the adapter reports no collections, advertise 125/250/500/1000 Hz there, and send the same 0x06 report the 0x1d57 path already builds. Browser behavior is unchanged: the collection gate still refuses and setPollingRate still throws.

Also correct the header comment: the config reports are reachable through the OS HID stack on interface 2's &col04 sub-collection (node-hid with stock input.inf, no Zadig/WinUSB).
Ports the Attack Shark X11's six-stage DPI table from the community driver's DpiBuilder: report 0x04, stage bytes from the hardware register map, stage masks (>12000), per-stage high flags (10100-12000 / 20100-22000), active stage, angle-snap/ripple bits, and a 16-bit big-endian checksum over bytes 3..49. Wired units send the first 52 bytes, wireless the full 56.

The firmware has no cheap current-DPI command, so like the reference driver the full table is remembered per product id (module-level, because the desktop app opens a short-lived client per write) and re-sent whole on every edit: setDpi edits the active stage, setDpiStageValue edits one stage, setActiveDpiStage selects, and setAngleSnapping/setRippleControl use the same report. readStatus exposes dpi/dpiStages/activeDpiStage/angleSnapping/rippleControl plus the shared dpiStageEditor hint.

readX11DpiState() best-effort GETs report 0x04 and adopts the decoded table when the signature and checksum validate; the wired unit is documented as having no read-back, so it keeps the cached table. R1 (0xfa61) stays out until its separate DpiBuilder is ported.
@hyusband hyusband changed the title attackshark: write X11 polling over a native HID adapter attackshark: native X11 polling and DPI over feature reports Sep 14, 2026
The wireless X11 streams 03 55 40 01 <pct> on interface 2 on its own, but the desktop app opens a short-lived client, reads status and closes immediately, so the packet was never seen and the Overview showed Battery N/A. readStatus() now waits (bounded, cached for 30s so the 5s auto-refresh does not pay it every tick) for the receiver's next packet, and keeps the last value per product id. The wired PIDs have no documented battery endpoint and keep reporting none.

Polling rate has no firmware read-back either; the last value applied (default 1,000 Hz, matching the reference driver) is cached and reported instead of 0 Hz. A constructor option shortens the battery wait for tests.
@hyusband hyusband changed the title attackshark: native X11 polling and DPI over feature reports attackshark: native X11 polling, DPI and battery over feature reports Sep 14, 2026
@snekxs
snekxs merged commit eba2e83 into OpenMouse-Project:main Sep 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants