Skip to content

feat(openlogi-hid): route HID++ writes to directly-attached devices#5

Merged
AprilNEA merged 3 commits into
masterfrom
feat/direct-device-route
May 31, 2026
Merged

feat(openlogi-hid): route HID++ writes to directly-attached devices#5
AprilNEA merged 3 commits into
masterfrom
feat/direct-device-route

Conversation

@AprilNEA
Copy link
Copy Markdown
Owner

What & why

Today every control path assumes a device hangs off a Logi Bolt receiver at a pairing slot. Directly-attached devices (USB cable or Bluetooth) are second-class: Bluetooth ones list but are read-only, and wired ones (no battery — e.g. a corded G502) are dropped entirely. This makes them first-class.

Three blockers removed:

Before After
probe_direct required a battery → wired mice dropped Hybrid discriminator: keep a direct device if it reports a battery OR exposes a control feature (adjustable DPI 0x2201/0x2202 or ReprogControls 0x1b04). A receiver's secondary 0xff interface exposes neither, so the phantom-entry guard still holds.
with_device / open_target_channel bailed unless Receiver::Bolt New DeviceRoute { Bolt { receiver_uid, slot } | Direct { vendor_id, product_id } }; open_route_channel resolves both in one place. Direct devices are addressed at HID++ self-index 0xff and re-found by vid/pid.
DpiTarget required a receiver_uid → direct devices got dpi_target = None, so DPI/SmartShift/capture were never wired up The GUI builds a Bolt or Direct route per device; DPI, SmartShift, and gesture/button capture all route through it.

DpiTarget/GestureTarget collapse into DeviceRoute. The CLI diag commands and SharedChannel reuse follow the same route, so openlogi diag {features,dpi,smartshift} work on direct devices too.

Test plan

Run in the project devenv (real-Xcode Metal env for the GUI):

  • cargo clippy --workspace --all-targets -- -D warnings → clean
  • cargo fmt --check → clean
  • cargo test → non-GUI 14 passed, GUI 6 passed, 0 failed
  • Cargo.lock untouched; gpui pin (eb2223c0) unchanged

Still needs real-hardware verification (no G502 on hand)

  • Enumeration filter (transport.rs): only Logitech VID + usage page 0xff00 / usage 0x0002 are enumerated — unchanged here. If a wired G502 exposes HID++ on a different vendor usage page (some G-series use 0xff43), it won't appear at all. This is the first thing to confirm with openlogi diag features.
  • Feature discriminator: assumes HID++ 2.0 with 0x2201/0x2202/0x1b04. A variant speaking only legacy HID++ 1.0 registers would be skipped.
  • Two identical direct mice on one host are indistinguishable by vid/pid (first match wins) — documented as a v0 limitation.

Introduce DeviceRoute { Bolt | Direct } so DPI, SmartShift, and control
capture work for devices attached over a USB cable or Bluetooth, not just
ones paired to a Bolt receiver. open_route_channel resolves either kind in
one place, replacing the Bolt-only assumption baked into the write and
capture paths.

Relax the inventory's direct-device discriminator from "must report a
battery" to "battery OR a control feature (adjustable DPI / reprogrammable
buttons)", so wired mice — which have no battery, e.g. a corded G502 — are
listed instead of dropped as a receiver's secondary interface.

The GUI builds a Bolt or Direct route per device (direct ones keyed by
vendor/product id at HID++ self-index 0xff), and the CLI diag commands plus
SharedChannel reuse follow the same route. DpiTarget/GestureTarget collapse
into DeviceRoute.
Copy link
Copy Markdown

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ Minor suggestion only — design and refactor look solid.

Reviewed changes — initial review of the DpiTarget/GestureTargetDeviceRoute consolidation, the new open_route_channel single-source resolver, and the hybrid peripheral discriminator that admits wired devices.

  • Introduce DeviceRoute + open_route_channel — single Bolt-vs-direct resolver shared by write::with_route and gesture::run_capture_session; DpiTarget/GestureTarget removed; WriteError/GestureError collapse ReceiverNotFound into DeviceNotFound and re-key DeviceUnreachable on index rather than slot.
  • Hybrid probe_direct discriminator — keep a slot-0xff device if it reports a battery OR exposes 0x2201/0x2202/0x1b04 via root feature lookup; phantom-receiver guard still holds because a Bolt secondary interface exposes neither.
  • GUI plumbingDeviceRecord::route replaces dpi_target; state/devices.rs::device_route builds Bolt or Direct from inventory (Bolt with no UID → None so writes are skipped, not mis-routed); DPI cycle / SmartShift / gesture capture all flow through the route.
  • CLI diagfirst_online_device returns (DeviceRoute, String); dpi / features / smartshift take &DeviceRoute.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread crates/openlogi-hid/src/route.rs Outdated
Address PR review: a direct route's vendor/product id lives on the
unopened async_hid DeviceInfo, so filter candidates by vid/pid before
open_hidpp_channel rather than opening every node (~100ms each) first.
On a host with a Bolt receiver plus a direct mouse, this stops every
non-reused direct write from opening the receiver's channel first.
Copy link
Copy Markdown

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes — incremental review of the perf follow-up to the prior open_route_channel thread.

  • Pre-filter Direct candidates by vid/pid before open_hidpp_channel — in crates/openlogi-hid/src/route.rs, the candidate loop now continues on a vid/pid miss for DeviceRoute::Direct before paying the ~100ms channel-open cost; the inner DeviceRoute::Direct { .. } match arm collapses to return Ok(Some(channel)) because the pre-filter already guarantees the match. Bolt branch unchanged — still opens the channel for receiver::detect and bolt.get_unique_id().

The prior review's only thread (route.rs:80-102) is addressed exactly as suggested and has been resolved.

Pullfrog  | View workflow run | Using Claude Opus𝕏

…ice-routing

# Conflicts:
#	crates/openlogi-hid/src/gesture.rs
@AprilNEA AprilNEA merged commit e6322ea into master May 31, 2026
6 checks passed
@AprilNEA AprilNEA deleted the feat/direct-device-route branch May 31, 2026 13:11
This was referenced May 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant