Skip to content

Overlay: move HID enumeration/identity to main via node-hid (gesture-free serials + per-unit identity) #106

Description

@petegordon

Goal

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.

Why — two concrete wins

  1. Gesture-free serials at boot. Serials today come from Electron's select-hid-device event, which only fires when the renderer calls navigator.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-hid enumeration in main needs no gesture → serials available immediately at launch.
  2. 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.
  • Interim (Overlay #104: default to last-used controller + 'No controller selected' splash text #105): renderer transport+serial matching + a first-gesture requestDevice scan to populate the auto-opened list.

Proposed change

  • 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.

Tradeoffs

  • Native dependency: per-Electron/Node ABI rebuild (electron-rebuild), per-platform prebuilt binaries, CI/packaging impact.
  • 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.

Acceptance

Refs #104, #105.


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

  1. In enumerateHidViaNodeHid, when a device's serialNumber is empty, run a one-shot MAC probe: open the pathgetFeatureReport(reportId, len) → parse the MAC → use it as the unit's serialNumber in the inventory.
  2. 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.
  3. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions