Skip to content

fix(hid): classify direct-path devices via HID++ 0x0005 device type#147

Open
AprilNEA wants to merge 3 commits into
masterfrom
fix/direct-path-device-kind
Open

fix(hid): classify direct-path devices via HID++ 0x0005 device type#147
AprilNEA wants to merge 3 commits into
masterfrom
fix/direct-path-device-kind

Conversation

@AprilNEA
Copy link
Copy Markdown
Owner

@AprilNEA AprilNEA commented Jun 5, 2026

Problem

On master, a Bluetooth-direct / wired MX Anywhere 3S / MX Master 3 shows only a Lighting (color) tab — the Buttons and Pointer tabs vanish, so there's no way to remap back/forward or anything else (#127).

Root cause

Two pieces interact:

  • inventory.rs::probe_direct (the receiver-less path: Bluetooth-direct, USB-C) hard-codes kind: DeviceKind::Unknown. There's no Bolt pairing register on this path to supply a real kind, and nothing else filled it in.
  • app.rs::supports_lighting (added in feat: add wired G-series keyboard RGB control #29 for wired G-series keyboards) treats any Unknown + DeviceRoute::Direct device as a lighting-capable keyboard.

So a Bluetooth-direct mouse is indistinguishable from a wired keyboard: is_configurable_pointer(Unknown) is false (no Buttons/Pointer) while supports_lighting is true (Lighting only). A Bolt-connected unit reports Mouse(0x02) from the pairing register and is unaffected — which is why the same model works over the Bolt receiver but breaks over Bluetooth.

Fix

Read the device's marketing type from HID++ feature 0x0005 (DeviceTypeAndName) and use it for the kind:

  • Folded into the existing probe_features session, so it's one extra short round-trip on an already-open device — no new Device::new / enumerate_features handshake.
  • Direct path: uses the 0x0005 type directly → a Bluetooth-direct mouse is now Mouse, restoring the Buttons + Pointer tabs and dropping the spurious Lighting tab.
  • Bolt path: keeps the authoritative pairing-register kind, falling back to 0x0005 only when that register reads Unknown — so currently-correct devices are untouched.

map_device_type maps the 0x0005 enum to our DeviceKind; resolve_device_kind encodes the precedence (primary wins unless Unknown, then probe) and is unit-tested.

Verification

  • cargo test -p openlogi-hid — 3 new precedence tests pass.
  • cargo clippy -p openlogi-hid --all-targets clean under -D warnings.
  • Not yet verified on hardware — @AprilNEA will test on an Anywhere 3S / Master 3 over Bluetooth. The tab gating is deterministic given kind == Mouse; this just needs to confirm the device answers 0x0005 at index 0xff on the direct path.

Notes / out of scope

Fixes #127

A Bluetooth-direct / wired device probed through `probe_direct` had its
kind hard-coded to `DeviceKind::Unknown` — the receiver pairing register
that supplies a kind on the Bolt path doesn't exist there. The GUI's
wired-keyboard lighting heuristic (`supports_lighting`, added in #29)
then treats any `Unknown` + `Direct` device as a lighting-only keyboard,
so a Bluetooth-direct MX Anywhere 3S / MX Master 3 lost its Buttons and
Pointer tabs and showed only an irrelevant color panel (#127) — leaving
no way to remap back/forward.

Read the device's marketing type from HID++ feature `0x0005`
(DeviceTypeAndName), folded into the existing `probe_features` session so
it costs one extra short round-trip and no new device handshake. The
direct path now reports the real kind (Mouse, …); the Bolt path keeps
its pairing-register kind and falls back to `0x0005` only when that
register reads `Unknown`.

Fixes #127
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Jun 5, 2026

Greptile Summary

This PR fixes issue #127 where Bluetooth-direct and wired HID++ devices showed only the Lighting tab (no Buttons/Pointer) by querying HID++ feature 0x0005 (DeviceTypeAndName) inside the existing probe_features session to determine the true DeviceKind instead of hard-coding Unknown on the direct path.

  • probe_features extended: now returns (battery, model_info, kind) by querying 0x0005 as a third feature within the same Device::new + enumerate_features session.
  • map_device_type maps the 19-variant HidppDeviceType to DeviceKind, with unmapped types (Receiver, Webcam, Dock, etc.) falling back to Unknown.
  • resolve_device_kind implements 0x0005-first precedence, falling back to the Bolt register when the probe is absent or Unknown; three unit tests verify the key scenarios.

Confidence Score: 5/5

Safe to merge — self-contained change adding one HID++ round-trip within an already-open session, fully covered by unit tests.

The kind-resolution logic is internally consistent across resolve_device_kind, its doc comment, and the three new unit tests. Offline Bolt devices fall back to the register correctly. No regressions are introduced for currently-correct Bolt devices.

No files require special attention; the single changed file is well-covered by the new tests.

Important Files Changed

Filename Overview
crates/openlogi-hid/src/inventory.rs Core change: probe_features gains a third 0x0005 query; map_device_type and resolve_device_kind correctly implement kind resolution; unit tests cover the three key precedence scenarios.

Fix All in Codex Fix All in Claude Code

Reviews (2): Last reviewed commit: "fix(hid): make HID++ 0x0005 the authorit..." | Re-trigger Greptile

Comment thread crates/openlogi-hid/src/inventory.rs Outdated
Comment thread crates/openlogi-hid/src/inventory.rs Outdated
AprilNEA added 2 commits June 5, 2026 23:36
`probe_features` queried HID++ 0x0005 for every online device, but the
Bolt path only uses the result when the pairing register returned
`Unknown`. For a well-behaved Bolt device that round-trip was discarded,
eating into the shared 5s PROBE_BUDGET (worse with many slots / a
slow-waking device). Gate the read behind a `read_device_type` flag: the
direct path always asks, the Bolt path only when its register kind is
`Unknown`.
…gister

The reporter on #127 connects over a Logi Bolt receiver, not Bluetooth-
direct — so the fix has to hold on the Bolt path too. A Bolt-routed
device only shows the lighting-only UI when it is classified as a
keyboard, which means the receiver's pairing register is misreporting an
MX Anywhere 3S as `Keyboard`. The previous precedence (register wins,
0x0005 only fills an `Unknown`) could not correct that.

Flip it: the device's own `0x0005` (DeviceTypeAndName) report is the
authoritative kind for any online device; the pairing-register kind is
the fallback for offline devices (no probe) or a `0x0005` type we don't
model. This corrects a register that names the wrong concrete kind, not
just an `Unknown` one — covering both the Bolt and the Bluetooth-direct
paths in #127.

The 0x0005 read is no longer conditional, but it is no longer discarded
either: it is the primary source and runs only for online devices that
just answered two other reads, so it is one cheap short round-trip with a
real purpose (addresses the earlier review note about a wasted probe).
@AprilNEA
Copy link
Copy Markdown
Owner Author

AprilNEA commented Jun 5, 2026

Pushed a follow-up that strengthens the fix and addresses the review note.

Why the precedence flipped. #127's reporter is on a Logi Bolt receiver, not Bluetooth-direct. On the Bolt path a device only renders the lighting-only UI when it's classified as a Keyboard, which means the receiver's pairing register is misreporting the MX Anywhere 3S as a keyboard. The first revision only filled an Unknown register from 0x0005, so it would not have fixed that user.

The device's own HID++ 0x0005 (DeviceTypeAndName) report is authoritative, so resolve_device_kind now prefers it for any online device and falls back to the pairing-register kind only when the device is offline (no probe) or 0x0005 returns a type we don't model. This corrects a register that reports the wrong concrete kind — covering both the Bolt and the Bluetooth-direct cases in #127.

Re: the discarded-round-trip note. The 0x0005 read is unconditional again, but it's no longer discarded — it's now the primary kind source. It runs only for online devices that just answered the battery + device-info reads, so it's one cheap short round-trip with a real purpose.

The bolt_kind value the register reports is logged on the paired slot debug line, so a RUST_LOG=openlogi_hid=debug run on the affected device will confirm exactly what the receiver was reporting.

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.

[Bug]: master breaks support for MX Anywhere 3S

1 participant