Skip to content

fix(openlogi-hid): enumerate Bluetooth-LE-direct HID++ mice (Lift, Signature)#28

Merged
AprilNEA merged 2 commits into
AprilNEA:masterfrom
7a6163:fix/ble-direct-hidpp-enumeration
Jun 1, 2026
Merged

fix(openlogi-hid): enumerate Bluetooth-LE-direct HID++ mice (Lift, Signature)#28
AprilNEA merged 2 commits into
AprilNEA:masterfrom
7a6163:fix/ble-direct-hidpp-enumeration

Conversation

@7a6163
Copy link
Copy Markdown
Contributor

@7a6163 7a6163 commented Jun 1, 2026

Summary

Bluetooth-LE-direct Logitech mice (e.g. Logitech Lift, Signature series)
were never enumerated — list reported "no devices" even with the mouse
connected and working. This makes them fully controllable (inventory,
battery, DPI) without a Bolt receiver.

Root cause

Three layered issues, found by inspecting what macOS actually reports for a
BLE-connected Lift:

  1. Enumeration filter too narrow. Over USB / receivers / BT-classic,
    Logitech exposes its HID++ long-report collection under usage page
    0xFF00 / 0x0002. Over BLE it uses 0xFF43 / 0x0202 instead, so the
    device was filtered out before it was ever probed.
  2. Short report doesn't exist on BLE. On macOS the BLE device exposes
    only the long report (0x11) — there is no short-report (0x10)
    collection. hidpp 0.2's determine_version() hardcodes a short-report
    ping with no long fallback, so IOHIDDeviceSetReport returns
    kIOReturnNotFound.
  3. (Environmental) BLE-direct mice also expose Generic-Desktop collections, so
    macOS requires Input Monitoring permission to open them — unlike a Bolt
    receiver, which is a vendor device.

Changes

  • transport.rs — broaden the enumeration filter to accept both the
    USB/receiver (0xFF00/0x0002) and BLE (0xFF43/0x0202) HID++ vendor
    collections. Add a long_only channel flag (set when usage_page == 0xFF43)
    that up-converts outgoing short HID++ reports to long (0x11, zero-padded,
    same header bytes). The device answers with a long report, which hidpp
    parses and matches by header regardless of width — the same long-only
    approach Solaar uses over BLE. The receiver/USB path is byte-for-byte
    unchanged. Adds unit tests for the filter and the up-conversion.
  • list.rs — refresh the "no devices" hint: BLE-direct mice need Input
    Monitoring permission; drop the stale "Bluetooth not enumerated" note.

Testing

Verified on a real Logitech Lift (BLE-direct, 046d:b031, config key 2b031):

  • list — device, kind, online, battery 45%
  • diag features — 32 HID++ features dumped (incl. 0x2201 DPI, 0x1b04
    reprog controls; no 0x2111, so no SmartShift — expected for the Lift) ✓
  • diag dpi — read 1000 → write 1200 → read-back 1200 → restore 1000 ✓
  • GUI enumerates and renders the device (hotspots, DPI, battery) ✓

cargo fmt --all --check, cargo clippy --workspace -- -D warnings, and
cargo test all pass.

Follow-up (not in this PR)

This fixes HID++ control for BLE-direct mice. The device render for the
Lift (2b031) isn't in the asset registry on assets.openlogi.org yet, so
the GUI falls back to the synthetic silhouette — functional, just no photo.
Adding 2b031 to the asset host is a separate follow-up.

…gnature)

BLE-direct Logitech mice expose their HID++ long-report collection under
HID usage page 0xff43/0x0202 — not the 0xff00/0x0002 used over USB and
receivers — and on macOS carry only the long report (0x11), no short (0x10).
Two changes make them controllable:

- Broaden the enumeration filter to accept both the USB/receiver and the
  BLE vendor collections.
- Up-convert outgoing short HID++ reports to long for BLE-only channels.
  hidpp 0.2 hardcodes a short-report ping, which macOS rejects with
  kIOReturnNotFound when the device exposes no short report; reframing it
  as a zero-padded long report (same header bytes) lets the device answer.
  Gated on usage_page == 0xff43, so the receiver/USB path is unchanged.

Also refresh the `list` no-device hint: BLE-direct mice need Input
Monitoring permission, and drop the stale "Bluetooth not enumerated" note.

Verified on a real Logitech Lift: list, diag features (32 features), and a
DPI write round-trip all succeed.
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 suggestions only — the fix is well-scoped and the unit tests cover the new behavior.

Reviewed changes — initial review of the BLE-direct HID++ enumeration fix: a widened usage-page filter plus a long-only channel mode that up-converts short HID++ requests to long when the transport (BLE-direct on macOS) doesn't expose the short report.

  • Widen the enumeration filter to two (usage_page, usage_id) pairs0xFF00 / 0x0002 (USB / Bolt / Unifying / BT-classic) plus 0xFF43 / 0x0202 (BLE-direct, e.g. Lift / Signature), via a constant array and an is_hidpp_long_collection helper.
  • Add a long_only flag to AsyncHidChannel — set when info.usage_page == HIDPP_BLE_USAGE_PAGE. write_report re-frames short HID++ reports as zero-padded 0x11 long reports for these channels; the receiver / USB path is unchanged.
  • Debug-log every Logitech HID node at enumeration timeOPENLOGI_LOG=debug surfaces vendor pages from new BLE devices without a rebuild.
  • Refresh the list "no devices" hint — point at macOS Input Monitoring permission for BLE-direct mice; drop the stale "Bluetooth not enumerated" note; keep the Logi Options+ caveat and the Bolt-only hidpp 0.2 note.
  • Unit tests — filter membership across USB / BLE / generic-desktop pages, plus short→long up-conversion (header preserved, payload zero-padded, non-short reports passed through).

ℹ️ Nitpicks

  • The PR body has a copy/paste duplication in the transport.rs bullet — the sentence starting with "Add a long_only channel flag…" and the "long-only approach Solaar uses over BLE" sentence appear twice. Worth tidying before merge so the description matches the diff.

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

Comment thread crates/openlogi-hid/src/transport.rs Outdated
@7a6163
Copy link
Copy Markdown
Contributor Author

7a6163 commented Jun 1, 2026

Thanks for the review! Good catch — the transport.rs bullet had a copy/paste duplication in the description, now tidied. No code change; the diff is identical.

To reiterate the key safety point: the short→long up-conversion is gated strictly on the BLE vendor page (usage_page == 0xFF43), so the receiver / USB path is byte-for-byte unchanged. It's verified end-to-end on a real Logitech Lift — list, diag features (32 features), and a DPI write round-trip all succeed. Happy to address anything else.

… table

Addresses PR review: `long_only` was derived from a separate
`HIDPP_BLE_USAGE_PAGE` constant while collection membership used the
data-driven `HIDPP_LONG_COLLECTIONS` table — so a future long-only
transport would have to update both sites in lockstep, or silently
regress to short-report writes. Encode `long_only` as a third column in
the table and look it up via `is_long_only_collection()`, making a new
collection a single-line addition. Adds a unit test for the flag.
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 — re-review of the single follow-up commit since the prior pullfrog review. The new commit retires the extensibility risk I flagged in the snapshot: long_only is now derived from the HIDPP_LONG_COLLECTIONS table itself, not from a separate HIDPP_BLE_USAGE_PAGE constant.

  • Folded long_only into the HIDPP_LONG_COLLECTIONS table — table is now [(u16, u16, bool); 2] with (0xff00, 0x0002, false) and (0xff43, 0x0202, true). Adding a new long-only transport is a single-row edit; the HIDPP_BLE_USAGE_PAGE constant is gone.
  • Added is_long_only_collection(usage_page, usage_id) helper — used in open_hidpp_channel to derive the channel's long_only flag from the table. Returns false for pages not in HIDPP_LONG_COLLECTIONS (consistent with the filter gating opens).
  • Added only_ble_collection_is_long_only unit test — asserts the BLE row is long-only, the USB / receiver row is not, and a non-matching page returns false.

Pullfrog  | View workflow run | Using Claude Opus𝕏

@AprilNEA
Copy link
Copy Markdown
Owner

AprilNEA commented Jun 1, 2026

I did local testing, Master 3S can now connect via Bluetooth, thank you for your contribution. Of course, there are still issues with the image display, I see that #26 has resolved this problem.

@AprilNEA AprilNEA merged commit 769044b into AprilNEA:master Jun 1, 2026
7 checks passed
@aprilnea aprilnea Bot mentioned this pull request Jun 1, 2026
sanube pushed a commit to sanube/OpenLogi that referenced this pull request Jun 1, 2026
PR AprilNEA#28 (merged) rewrote transport.rs to support BLE-direct HID++ mice
(Lift/Signature) via the 0xFF43/0x0202 collection. This commit rebases
the MX Master 4 B detection on top of that new structure:

transport.rs:
  - Adds is_logitech_mouse() for generic-desktop mouse nodes (0x0001/0x0002)
  - Widens enumerate_hidpp_devices() to also admit these nodes alongside
    the HID++ long-report collections already handled by PR AprilNEA#28
  - Documents the safety invariant: generic-desktop nodes are safe because
    open_hidpp_channel returns None on non-HID++ endpoints and
    fallback_direct_mouse re-filters by known-good PID

inventory.rs:
  - Corrects MX_MASTER_4_PID: 0xb042 → 0xb048 (BLE-direct variant)
  - Corrects MX_MASTER_4_NAME: "MX Master 4" → "MX Master 4 B"
  - Adds fallback_direct_mouse() (per-device, keyed by PID)
  - Adds hidutil_direct_mouse_fallback() wrapped in spawn_blocking,
    with debug! logging on failure
  - Ungates the hidutil fallback: runs whenever MX Master 4 B is absent
    from inventories (not only when the list is empty)
  - Adds probe_direct().or_else(fallback_direct_mouse) in probe_one

Cargo.toml: adds serde_json dependency for the ndjson parser

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.

2 participants