You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Enumerate HID devices in the Electron main process via node-hid (which exposes per-device path + serialNumber), instead of relying solely on the renderer's WebHID. Removes the user-gesture requirement for serial discovery and provides true per-unit device identity.
Per-unit identity (identical same-transport pads). The renderer's WebHID HIDDevice has no serialNumber (Chromium strips it) and no stable per-handle key, so two identical same-vid:pid units on the same transport (e.g. two Bluetooth DualSenses) can't be told apart. Overlay #104: default to last-used controller + 'No controller selected' splash text #105 matches on vid:pid + transport (+ serial when resolvable) — covers unique models and BT-vs-USB, but not same-transport twins. node-hid gives each unit a stable path + serialNumber in main, so last-used matching can pin the exact unit.
Current state / constraints
node-hid was deliberately avoided (main.js:86) to skip the native ABI rebuild.
Serials today: main's hidControllers map is filled by select-hid-device / hid-device-added (Electron session HID events), keyed by guid || deviceId, carrying serialNumber.
The descriptor-fingerprint renderer↔main handle bridge was tried and abandoned — Electron main's HIDDevice.collections came back empty.
Add node-hid (or a maintained fork / prebuilt) in main; enumerate at launch + on hotplug.
Build the authoritative inventory in main: { path, vendorId, productId, serialNumber, transport, productName } per physical unit.
Key the pool / last-used matching on the main-provided per-unit identity (path/serial) rather than the renderer's opaque WebHID handle.
Decide the I/O boundary: (a) main owns identity only (renderer keeps WebHID for report I/O; correlate handles to main units best-effort), or (b) main owns HID report I/O too (fuller identity, bigger change — sidesteps the handle↔path correlation). Evaluate both.
Option (a)'s crux is correlating a renderer WebHID handle to a main node-hid path for the same physical device; option (b) avoids it by doing I/O in main.
Win #2 plan — per-unit identity via feature-report MAC (added 2026-07-06)
node-hid's serialNumber covers most pads (Steam FXB…, Switch 98e2…, DualSense 7c66…), but some — notably the USB DualShock 4 (054c:05c4) — leave iSerialNumber empty (serial=(none)). Those still have a unique id: the controller's Bluetooth MAC, readable over USB via a HID feature report.
Why this needs main / node-hid
Chromium blocklists these pairing/MAC feature reports in WebHID (anti-fingerprinting) — which is exactly why the renderer can't read them (same root as the stripped serialNumber). node-hid in main is not subject to the blocklist and can getFeatureReport them directly.
Per-controller reports (verify on hardware)
DS4 (Sony 054c:05c4) — feature report 0x12: returns the controller BT MAC (first ~6 payload bytes, little-endian) + the paired host MAC. Primary target — the only pad in the current set with no serial.
DualSense (054c:0ce6) — feature report 0x09 (pairing info) if ever needed; in practice node-hid already surfaces its serial.
Switch Pro / Steam — already expose a serial via serialNumber; no probe needed.
Anything else with no serial + no MAC report → unidentifiable (stays vid:pid).
Flow
In enumerateHidViaNodeHid, when a device's serialNumber is empty, run a one-shot MAC probe: open the path → getFeatureReport(reportId, len) → parse the MAC → use it as the unit's serialNumber in the inventory.
Coordinate with WebHID: the renderer also opens these devices. Probe before WebHID grabs the handle (open → read → close at enumeration time), or gate so only one owner touches feature reports. Windows generally allows shared HID opens for feature reports — verify per device.
Cache the probed MAC by path so we don't re-open on every 2s poll.
Per-unit auto-select (the handle↔identity bridge)
With a MAC/serial per physical unit in main, last-used matching keys on it. Remaining gap: the renderer's WebHID pool still can't say which of two identical handles is which unit. Close it by either (a) grant-time correlation — main hands out one deviceId per requestDevice, so map each newly-granted handle to the just-identified unit's MAC — or (b) move report I/O to main (fuller identity). Decide during implementation.
Acceptance
The USB DS4 shows a stable unique id (its MAC) in the inventory, no gesture.
Two identical same-transport pads (incl. two serial-less DS4s) are reliably distinguished for last-used auto-select.
Interim shipped in #107: gesture-free serials for pads that DO expose serialNumber (win #1).
Goal
Enumerate HID devices in the Electron main process via
node-hid(which exposes per-devicepath+serialNumber), instead of relying solely on the renderer's WebHID. Removes the user-gesture requirement for serial discovery and provides true per-unit device identity.Why — two concrete wins
select-hid-deviceevent, which only fires when the renderer callsnavigator.hid.requestDevice()— and Blink requires transient user activation for that (it's a Chromium rule the Electron renderer inherits; "local app" doesn't relax it). So the Overlay: default to the last-used controller — auto-activate + SELECT on launch, else open the AVAILABLE list #104 auto-opened controllers list shows no serials until the user interacts (interim first-gesture-scan patch shipped in Overlay #104: default to last-used controller + 'No controller selected' splash text #105).node-hidenumeration in main needs no gesture → serials available immediately at launch.HIDDevicehas noserialNumber(Chromium strips it) and no stable per-handle key, so two identical same-vid:pid units on the same transport (e.g. two Bluetooth DualSenses) can't be told apart. Overlay #104: default to last-used controller + 'No controller selected' splash text #105 matches on vid:pid + transport (+ serial when resolvable) — covers unique models and BT-vs-USB, but not same-transport twins.node-hidgives each unit a stablepath+serialNumberin main, so last-used matching can pin the exact unit.Current state / constraints
node-hidwas deliberately avoided (main.js:86) to skip the native ABI rebuild.hidControllersmap is filled byselect-hid-device/hid-device-added(Electron session HID events), keyed byguid || deviceId, carryingserialNumber.HIDDevice.collectionscame back empty.requestDevicescan to populate the auto-opened list.Proposed change
node-hid(or a maintained fork / prebuilt) in main; enumerate at launch + on hotplug.{ path, vendorId, productId, serialNumber, transport, productName }per physical unit.Tradeoffs
electron-rebuild), per-platform prebuilt binaries, CI/packaging impact.node-hidpath for the same physical device; option (b) avoids it by doing I/O in main.Acceptance
Refs #104, #105.
Win #2 plan — per-unit identity via feature-report MAC (added 2026-07-06)
node-hid'sserialNumbercovers most pads (SteamFXB…, Switch98e2…, DualSense7c66…), but some — notably the USB DualShock 4 (054c:05c4) — leaveiSerialNumberempty (serial=(none)). Those still have a unique id: the controller's Bluetooth MAC, readable over USB via a HID feature report.Why this needs main / node-hid
Chromium blocklists these pairing/MAC feature reports in WebHID (anti-fingerprinting) — which is exactly why the renderer can't read them (same root as the stripped
serialNumber).node-hidin main is not subject to the blocklist and cangetFeatureReportthem directly.Per-controller reports (verify on hardware)
054c:05c4) — feature report0x12: returns the controller BT MAC (first ~6 payload bytes, little-endian) + the paired host MAC. Primary target — the only pad in the current set with no serial.054c:0ce6) — feature report0x09(pairing info) if ever needed; in practice node-hid already surfaces its serial.serialNumber; no probe needed.Flow
enumerateHidViaNodeHid, when a device'sserialNumberis empty, run a one-shot MAC probe: open thepath→getFeatureReport(reportId, len)→ parse the MAC → use it as the unit'sserialNumberin the inventory.pathso we don't re-open on every 2s poll.Per-unit auto-select (the handle↔identity bridge)
With a MAC/serial per physical unit in main, last-used matching keys on it. Remaining gap: the renderer's WebHID pool still can't say which of two identical handles is which unit. Close it by either (a) grant-time correlation — main hands out one
deviceIdperrequestDevice, so map each newly-granted handle to the just-identified unit's MAC — or (b) move report I/O to main (fuller identity). Decide during implementation.Acceptance
Interim shipped in #107: gesture-free serials for pads that DO expose
serialNumber(win #1).