feat(hid): dual-handle HID++ channel for Windows short/long reports#73
Open
josiah-nelson wants to merge 2 commits into
Open
feat(hid): dual-handle HID++ channel for Windows short/long reports#73josiah-nelson wants to merge 2 commits into
josiah-nelson wants to merge 2 commits into
Conversation
On Windows the Bolt receiver exposes HID++ short (report 0x10) and long (report 0x11) reports as two separate top-level collections, and therefore two separate device handles (0xff00/0x0001 = short, 0xff00/0x0002 = long). A handle rejects any foreign report id with ERROR_INVALID_FUNCTION before the device sees it, so the short-report receiver-register access used for pairing and device enumeration failed against the long-only handle the transport opened — every HID++ command returned 0x80070001. macOS (IOHIDManager) and Linux (hidraw) accept both report ids on a single handle, which is why the single-handle AsyncHidChannel sufficed there. On Windows, pair the two collections of one physical interface (matched by a device-path grouping key that strips the &ColNN token and trailing collection index) into a new WinDualChannel that routes each write by its leading report id and merges reads from both handles. A node with no short companion (long-only BLE-direct) degrades to the previous single-handle behaviour and reports short-unsupported, so the hidpp channel up-converts shorts to long as before. Non-Windows targets are unchanged (cfg-gated); enumerate/open now yield an opaque HidppNode that derefs to DeviceInfo. Validated on a Logi Bolt receiver (Options+ running, no contention): - `openlogi list` reads the receiver UID, pairing count, and all paired device codenames/kinds (MX KEYS S, MX Master 3S, MX Anywhere 3). - DPI round-trip on MX Anywhere 3: 1000 -> 1200 -> 1000 (read-back OK). - SmartShift round-trip: Ratchet -> Free -> Ratchet (read-back OK). Adds Windows grouping-key unit tests built from real Bolt device paths.
Contributor
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — adds Windows dual-handle HID++ transport via WinDualChannel, pairing short and long collections by device path grouping key, with a HidppNode opaque type to abstract the platform difference from callers. All existing call sites (inventory.rs, pairing.rs, route.rs) compile transparently via Deref<Target = DeviceInfo>. Everything new is #[cfg(windows)] — macOS/Linux paths are unchanged.
HidppNodeopaque type — wraps single-handle (non-Windows) or dual-handle (Windows) device ownership; Derefs toDeviceInfofor pre-filter filteringWinDualChannel— routes writes by leading report ID (0x10→ short,0x11→ long); merges reads viafutures_lite::future::orwith documented cancel-safety analysisgrouping_key/normalize_collection_path— strips&ColNNand trailing instance index from Windows HID device paths to pair short/long siblings on the same physical interface- Windows enumeration — indexes short HID++ collections (
0xff00/0x0001) by grouping key, attaches each to its long sibling; degrades gracefully to single-handle long-only when no short companion found
Big Pickle (free) (credentials for Anthropic not configured) | 𝕏
This was referenced Jun 2, 2026
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.

Summary
Adds Windows support for the HID++ transport by handling a Windows-specific quirk: the receiver splits HID++ short (
0x10) and long (0x11) reports into two separate top-level collections — and therefore two separate device handles — whereas macOS/Linux expose both report IDs on one handle.Before this change, every HID++ command on Windows failed with
ERROR_INVALID_FUNCTION (0x80070001):openlogi listshowed the Bolt receiver but "no paired devices", anddiagcould not read or write anything.Root cause
On Windows the Bolt receiver enumerates as:
A handle rejects any report ID that isn't its own with
ERROR_INVALID_FUNCTIONbefore the device sees it (confirmed with a per-collection write probe). The transport matched only0xff00/0x0002(long), so the short-report receiver-register access used for pairing/device enumeration (HID++ 1.0) was sent to the long-only handle and failed. macOS (IOHIDManager) and Linux (hidraw) accept both IDs on a single handle, which is why the single-handleAsyncHidChannelwas sufficient there.async-hid's write path is not at fault — it correctly zero-pads toOutputReportByteLength.Change
&ColNNhardware-ID token and the trailing instance-ID collection index.WinDualChannel(aRawHidChannel) holds both handles, routes each write by its leading report ID (0x10→ short handle,0x11→ long handle), and merges reads from both handles (whichever yields a report first; the other read stays armed, so nothing is dropped).hidppchannel up-converts shorts to long exactly as before.enumerate_hidpp_devices/open_hidpp_channelnow yield an opaqueHidppNodethat derefs toDeviceInfo(so callers can still pre-filter on vendor/product ID before opening).Scope & safety
Everything new is
#[cfg(windows)]. macOS and Linux code paths are unchanged — the existing single-handleAsyncHidChannelis retained verbatim on non-Windows targets. This cannot regress the currently-supported platform.Validation
On a Logi Bolt receiver (with Logi Options+ running — no exclusive-access conflict):
openlogi listreads the receiver UID, pairing count, and all paired device codenames/kinds:1000 → 1200 → 1000(read-back OK).Ratchet → Free → Ratchet(read-back OK).Tests
Adds Windows grouping-key unit tests built from real Bolt device paths (short/long collections of one interface collapse to the same key; distinct interfaces do not).
cargo fmt,cargo clippy -p openlogi-hid(clean), andcargo test -p openlogi-hid(21 passing) all green onx86_64-pc-windows-msvc.Notes
openlogi-gui/ GPUI), the OS event hook, and packaging are separate follow-ups.clippy::result_large_errfires inopenlogi-core/src/config.rsunder newer stable toolchains (not touched here) — worth a separate look when the pinned toolchain is bumped.