feat(hid): serve native HID to the browser over the loopback socket - #3
Open
ydw1904 wants to merge 1 commit into
Open
feat(hid): serve native HID to the browser over the loopback socket#3ydw1904 wants to merge 1 commit into
ydw1904 wants to merge 1 commit into
Conversation
Firefox and Safari have no WebHID, so the OpenMouse control panel cannot reach a mouse there at all. This adds `GET /v1/hid`, a WebSocket carrying the same primitives WebHID exposes — enumerate, open, send/receive reports, stream input reports — so the web app can implement `navigator.hid` on top of Bridge and run its existing driver classes unchanged. No vendor protocol is reimplemented here. `@openmouse/protocol`'s drivers stay the single source of truth; this is transport only. The report descriptor is parsed (src/hid/descriptor.rs) rather than left empty. Every driver's `isSupported()` reads `device.collections`, including the report ids inside them, so an adapter that reports none fails the whole registry — which is why `native-hid/` and Desktop both had to bypass auto-detection and hand-maintain a brand table. Parsing here means the web app's own registry works over this socket exactly as it does over WebHID, with no second device list to keep in sync. Security, both deliberate and load-bearing: - The handshake's Origin is checked against `allowedOrigins` by hand. CORS does not apply to a WebSocket handshake, so without this any page the user visits could enumerate and write to their mouse. - Generic Desktop mouse and keyboard collections are never listed or opened, matching what Chrome withholds from WebHID. Opening one natively freezes the device's own input on macOS. - Enumeration is scoped to the vendor ids the client asks for, so a page never learns about HID devices OpenMouse has no driver for. Two hardware findings from this project's other native adapters are carried over rather than rediscovered: writes try every enumerated split of an interface and remember which one answered a given report id, and the reader thread uses try_lock plus an unconditional sleep outside the lock, without which a writer can be starved indefinitely. hidapi gains the `macos-shared-device` feature: Darwin opens exclusively by default, which freezes the mouse for as long as a handle is held. Not yet validated against real hardware — the descriptor parser and the socket protocol are unit tested, but no supported mouse was reachable from the machine this was written on.
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.
Why
Firefox and Safari have no WebHID, and Mozilla's standards position on it is negative. In those browsers the OpenMouse control panel cannot reach a mouse at all — the app can only tell the user to switch browsers.
Bridge is already installed on the user's machine, already speaks to the hardware, and already has a vetted origin allowlist. Giving it a HID endpoint turns "unsupported browser" into "install Bridge", and the web app keeps running its existing driver classes unchanged.
What this adds
GET /v1/hidupgrades to a WebSocket carrying the same primitives WebHID exposes: enumerate, open, close, send output/feature reports, receive feature reports, and a stream of input reports. Frames are JSON with a client-chosenidechoed in each reply; byte payloads are number arrays, matching whatnative-hid/already exchanges with these drivers.No vendor protocol is implemented here.
@openmouse/protocolstays the single source of truth; this is transport.The part worth reviewing: report descriptors
src/hid/descriptor.rsparses each device's HID report descriptor into realcollections.This is not decoration. Every driver's
isSupported()readsdevice.collections, and not just the top-level usage — Pulsar matches "one input and one output report, both id 0x08", WLMouse recurses intochildrenlooking for a feature report id. An adapter that reports no collections fails the entire registry, which is exactly whynative-hid/src/hid-device-adapter.mjsand Desktop'sTauriHidDeviceboth had to bypass auto-detection and hand-maintain a brand table. Parsing here means the web app's own registry auto-detects over this socket exactly as it does over WebHID, with no second device list drifting out of sync.Only the items that shape
collectionsare interpreted; logical ranges, units, and string indices are skipped. Malformed input truncates rather than failing, because a device with a slightly wrong descriptor is common in this hardware class and hiding it would be worse than describing the part that parsed.Security
Three rules, all deliberate:
origin_allowed()rejects any handshake whoseOriginis not inallowedOrigins, and one with noOriginat all. Without this, any page the user visits could enumerate and write to their mouse.Every device a socket opened is closed when that socket disconnects.
Carried over rather than rediscovered
Two findings from this project's other native adapters, both load-bearing:
try_each). macOS enumerates one entry per top-level collection, and a report id may only be answerable on one of them.try_lockplus an unconditional sleep outside the lock. Desktop'ssrc-tauri/src/hid.rsdocuments whytry_lockalone is not enough:read_timeoutholds the guard for its full duration and the loop re-acquires immediately, starving a writer parked onlock()indefinitely.hidapigains themacos-shared-devicefeature. Darwin opens exclusively by default, which freezes the mouse for as long as a handle is held — this also affects the existing Pulsar driver.Verification
cargo test,cargo fmt --check, andcargo clippy --all-targets -- -D warningsare clean. Seven new unit tests cover the descriptor parser (vendor control collection, nested children, push/pop, truncated input, the protected-usage set) and the socket (frame parsing, error replies, the origin gate).Validated end to end against a Keychron M6 on macOS:
Out of scope
Companion change
The web-side
navigator.hidshim that consumes this: OpenMouse-Project/openmouse#127🤖 Generated with Claude Code