attackshark: native X11 polling, DPI and battery over feature reports - #103
Merged
snekxs merged 3 commits intoSep 14, 2026
Merged
Conversation
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
sendFeatureReportthrowsNotAllowedError). Native HID adapters (the Desktop app's TauriTauriHidDeviceand the Node adapter) can reach them, but reportcollections: []because they do not parse the report descriptor.detectFamily()keyed entirely on collections, so on those transports the family wasnull: the device appeared butsettingsReadystayed false and every write threw. The Overview also showed DPI placeholder,0 HzandBattery 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.0x25a7gets the same vendor-id fallback.Polling.
getSupportedPollingRates(),settingsReadyandforceShowBatteryfollow that, andsetPollingRate()sends the same0x06report the0x1d57path 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: report0x04, 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;setDpiedits the active stage,setDpiStageValueone stage,setActiveDpiStageselects, andsetAngleSnapping/setRippleControluse the same report.readStatus()exposesdpi/dpiStages/activeDpiStage/angleSnapping/rippleControland the shareddpiStageEditorhint;readX11DpiState()best-effort GETs report0x04and adopts the decoded table when the signature and checksum validate. R1 (0xfa61) stays out until its separateDpiBuilderis 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
&col04sub-collection (node-hid with stockinput.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 reportssettingsReady, the X11 name, Wireless,[125,250,500,1000], the default 1,600 DPI table and editor hint;setPollingRate(500)emits0x06with09 01 02 FD 00 00 00 00;setDpi(3200)emits report0x04with 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 --noEmitclean; 19/19 attackshark tests pass.