Controller inventory: core data model + HID fingerprint probes (#241 foundation)#34
Merged
Conversation
…undation)
The data model for the "all controllers seen" table (#241): a headless,
dependency-free registry of every controller observed, with identity,
capabilities, and connection lifecycle. No `three`/DOM, so it stays out
of the three import graph and runs under CI's dependency-free node:test.
packages/core/src/controller-inventory.js — ControllerInventory:
- Identity, best available wins: serialNumber (per-unit) > vid:pid (merges
identicals) > productName. Folds WebHID + Gamepad-API sightings of one
physical pad into a single record (re-keys a vid:pid record to per-unit
when a serial later arrives); keeps two identical-but-distinct serials
separate; leaves an un-correlatable serial-less sighting honest.
- Lifecycle: firstSeen / lastConnected / lastDisconnected / connected /
connectCount, via an injectable clock.
- capabilitiesFor(): gyro/accel/touchpad + trackpadCount + haptics +
lightbar from the dictionary.
- toJSON()/loadJSON() persistence; restored records start disconnected.
devices.js — added trackpadCount + haptics{count,type} to every entry
(per-entry, since DualSense's adaptive triggers differ from a DS4 sharing
PS_FEATURES). index.js/package.json export the module + subpath.
tools/hid-probe/ (WebHID feature-report reader) and tools/hid-serial-dump.cjs
(node-hid serial dumper) — the diagnostics that established the identity
model: Chromium blocklists the MAC reports (DS4 0x12 / DualSense 0x09 →
NotAllowedError) so the browser is VID:PID-only, but the OS HID layer
(node-hid, and Electron's main-process HID events) exposes a real per-unit
serial/MAC — even for a GameSir Super Nova. So per-unit identity is a
desktop(Electron) capability; the web overlay merges by vid:pid.
+11 tests. Core suite: 67 pass. UI (overlay table) is the next step.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A one-file page that drives the real ControllerInventory off the browser's WebHID + Gamepad-API events and renders the table, so the core module can be exercised before the production overlay UI exists. History persists to localStorage. Browser limit (VID:PID, no serial) is stated in the UI. Run: npm run inventory-preview → open /tools/inventory-preview/. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The product code is ESM (packages/core etc. set "type":"module"); the .cjs outlier was only chosen because node-hid is CommonJS. Use a dynamic import + default interop in an .mjs instead (still fails gracefully when node-hid isn't installed). Update the hid-dump npm script. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
macOui() extracts the OUI (vendor block) from a 12-hex Bluetooth MAC serial; formatSerial() pretty-prints it. Strict MAC matching (exactly 12 hex, or colon/hyphen separated) so a product serial like the Steam Controller's "FXB9960202571" isn't mistaken for a MAC. Foundation for flagging a pad that claims one vendor's USB VID but carries another vendor's MAC OUI (GameSir-Super-Nova-as-DualShock). +1 test; core 68. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Confirmed from hardware (both DS4s over BT): a real Sony DualShock 4 has MAC OUI 90:fb:a6 (Sony), while a GameSir Super Nova spoofing the same Sony VID 054c has OUI a0:5a:5e (GameSir). analyzeIdentity() cross-checks the claimed USB vendor against the MAC's OUI and returns genuine / clone / unverified / no-mac / no-serial. Seeded OUI table with confirmed entries. Honestly handles non-MAC serials (Steam "FXB99…" product serial, Xbox 360 XInput slot "01"/"02") and USB DS4 (no serial). +2 tests; core 70. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…D events) The desktop inventory table (#241). Main process captures serialNumber from its HID device events (select-hid-device deviceList + hid-device-added/ removed) — which expose the serial the renderer's blocklisted WebHID can't — and pushes the controller map to a new inventory window. No native module (avoids the node-hid/Electron-ABI rebuild); inventory degrades to Gamepad API if serials don't come through. Logs each device's serial so we can confirm Electron surfaces it. - electron/main.js: hidControllers map + upsert/broadcast from session HID events; openInventoryWindow(); ipc open-inventory-window / list-hid- controllers; tray "Controller Inventory…" item. - electron/preload.js: openInventoryWindow / listHidControllers / onHidControllersSnapshot bridge. - src/inventory.html + src/js/inventory.js: table (Genuine?/Serial/OUI/ Transport/lifecycle/caps) driven by ControllerInventory; Scan button triggers enumeration; localStorage persistence; Gamepad-API merge. Open via the tray menu after `npm start`. analyzeIdentity flags the Super Nova as a clone vs a real DS4 by MAC OUI. Core suite still 70. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Confirmed from hardware: a GameSir Cyclone 2 in DS4 v2 mode (advertises Sony 054c:09cc) has Bluetooth MAC OUI d0:56:80 — a different GameSir block than the Super Nova's a0:5a:5e. Added so it's flagged as a clone instead of "unverified". GameSir uses multiple OUIs; catalogue each as captured. +test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Confirmed from hardware: a genuine DualSense (054c:0ce6) has MAC OUI 50:ee:32 → now labelled genuine instead of unverified. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Evaluation showed OUI→vendor is unreliable for this: genuine PlayStation controllers carry a Foxconn (Hon Hai) OUI — NOT Sony's (90:fb:a6, 50:ee:32 both resolve to Hon Hai in the 88k IEEE/Wireshark/nmap master DB) — and the GameSir clone OUIs (a0:5a:5e, d0:56:80) aren't in any registry at all. So an allowlist would false-flag real pads and a denylist is a treadmill. Removed analyzeIdentity + the OUI/VID vendor tables and the Genuine?/OUI columns. Kept the solid part: the full MAC is the per-unit identity (distinguishes identical pads over Bluetooth). macOui/formatSerial retained; added isMacSerial (used for BT-vs-USB transport inference). If a vendor lookup is ever wanted it can be injected — design + the parked "OUI web API" idea are in the controller-identity memo. Core suite 69. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Scan's select-hid-device deviceList is the set of currently-present HID devices, so on each Scan we now also mark any known controller NOT in that list as disconnected. This gives an accurate on-demand refresh of connected/offline + Last disconnected without a native module — chosen over node-hid polling to keep the no-dependency, browser/web-compatible path (node-hid auto-polling remains a possible future upgrade). Button relabeled "Scan / Refresh". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
First half of the controller-inventory feature (#241) — core module only; the overlay table UI is the next step (we agreed: build & review the data model first).
What the probes established
Using the two diagnostics in
tools/against real hardware:0x12, DualSense0x09→NotAllowedError) for real Sony pads and clones alike. The other readable reports are stale input buffers / uninitialized memory, not identity.node-hid(and, in production, Electron's main-process HID events) returned a unique MAC even for a GameSir Super Nova (a05a5ef610cb) and a Valve serial for the Steam Controller (FXB9960202571, identical across all its interfaces).→ Per-unit identity is a desktop (Electron) capability; the web overlay merges by vid:pid. No
node-hidneeded in the shipped app — Electron'sselect-hid-device/hid-device-addedevents carryserialNumber.packages/core/src/controller-inventory.jsHeadless, dependency-free (no
three/DOM):serialNumber→vid:pid(merge identicals) →productName. Folds WebHID + Gamepad-API sightings of one pad into a single record; upgrades a vid:pid record to per-unit when a serial later arrives; keeps two distinct serials separate; leaves an un-correlatable serial-less sighting as its own row (honest).toJSON()/loadJSON()(localStorage on web, userData JSON in Electron); restored records start disconnected.devices.jsAdded
trackpadCount+haptics {count,type}to every entry (per-entry — DualSense's adaptive triggers differ from a DS4 that sharesPS_FEATURES).Tests
controller-inventory.test.js— 11 tests (identity precedence, gamepad-id parsing, capability enrichment, connect/reconnect/disconnect lifecycle, both cross-source merge directions, identical-pads-stay-separate, sort order, persistence round-trip, Xbox limited-caps). Core suite: 67 pass.Next (after review)
Overlay table view in
apps/overlay(WebHID +navigator.getGamepads()wiring + Electron serial bridge via IPC).🤖 Generated with Claude Code