Skip to content

fix: detect MX Master 4 B (0xb048) over macOS Bluetooth LE#40

Open
sanube wants to merge 1 commit into
AprilNEA:masterfrom
sanube:fix/mx-master-4b-clean
Open

fix: detect MX Master 4 B (0xb048) over macOS Bluetooth LE#40
sanube wants to merge 1 commit into
AprilNEA:masterfrom
sanube:fix/mx-master-4b-clean

Conversation

@sanube
Copy link
Copy Markdown

@sanube sanube commented Jun 1, 2026

What this fixes

Supersedes #36 (closed) — clean single commit rebased on master after PR #28 merged.

The MX Master 4 B (Bluetooth-direct, PID 0xb048) was never detected because:

  1. The original PR fix: detect MX Master 4 over macOS Bluetooth #20 used the wrong PID (0xb042) and name ("MX Master 4"). The BLE-direct variant reports ProductID = 0xb048 and Product = "MX Master 4 B" — confirmed via hidutil and ioreg on macOS 15.
  2. On macOS, the BLE HID descriptor for this device exposes only a generic-desktop mouse collection (usage_page=0x0001 / usage_id=0x0002) — no Logitech vendor HID++ collection. enumerate_hidpp_devices filtered it out entirely.
  3. The hidutil --matching literal string hardcoded 0xb042 independently of the constant, so fixing the constant alone was not sufficient.

Changes

transport.rs

inventory.rs

  • MX_MASTER_4_PID: 0xb0420xb048
  • MX_MASTER_4_NAME: "MX Master 4""MX Master 4 B"
  • probe_one: adds .or_else(fallback_direct_mouse) after probe_direct
  • hidutil_direct_mouse_fallback: wrapped in spawn_blocking, logs stderr on failure
  • Ungates the hidutil fallback: runs whenever MX Master 4 B is absent (not only when the whole list is empty — fixes Bolt-alongside-BLE topology)

Pullfrog review findings (from #36) — all addressed

Finding Resolution
TCC sqlite3 fallback non-functional (3 independent bugs) Removed entirely
info! fires on every enumerate tick Changed to debug!
is_hidpp_candidate invariant undocumented Full doc comment in transport.rs
hidutil fallback masked by non-empty inventory Ungated, runs on MX Master 4 B absence
Blocking Command::output() in async context Wrapped in spawn_blocking

Prerequisite for users

If com.logi.ghub.hidfilter (Logi G HUB HID Driver Extension) is still active it claims exclusive HID access — remove it via System Settings → General → Login Items & Extensions → Driver Extensions.

Tested on

  • macOS 15, Apple Silicon
  • MX Master 4 B, fw RBM27.00_0015, paired over BLE (PID 0xb048)
  • With and without Bolt receiver attached simultaneously

Fixes #32

The MX Master 4 B (Bluetooth-direct) has ProductID 0xb048, not 0xb042.
On macOS its BLE HID descriptor exposes only a generic-desktop mouse
collection (usage_page=0x0001 / usage_id=0x0002) — no Logitech vendor
HID++ collection — so it was invisible to OpenLogi.

transport.rs:
  - Adds is_logitech_mouse() to detect generic-desktop Logitech mouse nodes
  - Widens enumerate_hidpp_devices() to admit these nodes alongside the
    HID++ long-report collections (0xFF00 and 0xFF43) already supported
  - Documents the safety invariant: open_hidpp_channel returns None on
    non-HID++ endpoints; fallback_direct_mouse re-filters by known PID

inventory.rs:
  - MX_MASTER_4_PID: 0xb042 → 0xb048 (BLE-direct variant)
  - MX_MASTER_4_NAME: "MX Master 4" → "MX Master 4 B" (macOS-reported name)
  - probe_one: adds .or_else(fallback_direct_mouse) after probe_direct
  - hidutil_direct_mouse_fallback: shells out to hidutil list --ndjson
    (macOS 10.15+, confirmed on 13+), wrapped in spawn_blocking; logs
    status+stderr on non-zero exit
  - Ungates the hidutil fallback: runs whenever MX Master 4 B is absent
    from inventories (not only when the list is empty — fixes the
    Bolt-receiver-attached-alongside-BLE topology)
  - Adds serde_json for the ndjson line parser

Prerequisite: the Logi G HUB HID Driver Extension
(com.logi.ghub.hidfilter) must be removed — it claims exclusive HID
access and prevents IOHIDDeviceOpen() for any other process.

Verified on macOS 15, Apple Silicon, MX Master 4 B fw RBM27.00_0015,
paired over Bluetooth LE (PID 0xb048).

Fixes AprilNEA#32

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@sanube
Copy link
Copy Markdown
Author

sanube commented Jun 1, 2026

Thanks for the detailed response. Here are the diagnostics you asked for, run against current master (post-PR #28) with the G HUB HID filter already removed.

hidutil list — all Logitech nodes reported by macOS for MX Master 4 B over BLE:

usage_page=0x0001  usage_id=0x0002  pid=0xb048  type=service  product=MX Master 4 B
usage_page=0x0001  usage_id=0x0002  pid=0xb048  type=device   product=MX Master 4 B

Only 0x0001 / 0x0002 (generic-desktop mouse) — no 0xFF43 / 0x0202 node at all. macOS does not surface a Logitech vendor HID++ collection for this device over BLE, so PR #28's 0xFF43 path does not reach it regardless of the G HUB filter state.

This is consistent with what the issue author reported in #32: the device's BLE HID report descriptor simply doesn't include the vendor HID++ collection that 0xFF43 would expect.

Regarding the Accessibility/TCC workaround — that was already removed in the review fixup commit and is not present in PR #40. Happy to open a separate issue for that discussion if it's still useful.

Happy to trim this down to the minimum if you'd prefer: the two-line PID/name constant fix + the is_logitech_mouse enumeration widening + the hidutil fallback for the 0x0001 case. Let me know what scope works best.

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 critical issues — one design question about config-key routing in the body, three minor inline suggestions.

Reviewed changes — initial review of the BLE-direct MX Master 4 B detection path: enumeration filter widening in transport.rs plus the synthetic-inventory + hidutil fallback in inventory.rs.

  • Widen HID++ enumeration to admit generic-desktop Logitech mouse nodes — new is_logitech_mouse filter in transport.rs lets nodes with usage_page=0x0001 / usage_id=0x0002 through alongside the existing HID++ long-collection pairs; safety invariant documented in the function rustdoc.
  • Synthetic inventory for MX Master 4 Bfallback_direct_mouse builds a DeviceInventory keyed by PID 0xb048 when probe_direct returns None, plus synthetic_direct_mouse_inventory helper used by both the in-process and hidutil paths.
  • hidutil --ndjson last-resort fallback — shells out to /usr/bin/hidutil on macOS (wrapped in spawn_blocking), parses ndjson with serde_json, and verifies Product / VendorID / ProductID / PrimaryUsagePage / PrimaryUsage before synthesising. Ungated to run whenever MX4B is absent so the Bolt-alongside-BLE topology still works.
  • Constants correctedMX_MASTER_4_PID 0xb0420xb048, MX_MASTER_4_NAME "MX Master 4""MX Master 4 B", and MX_MASTER_4_EXT_MODEL_ID = 0x02 introduced.

ℹ️ Synthetic config_key splits MX Master 4 B from the Bolt/USB variant

The synthetic uses model_ids = [0xb048, 0, 0] with extended_model_id = 0x02, which DeviceModelInfo::config_key formats as "2b048". The same physical device family paired over Bolt or USB produces "2b042" (per device.rs:89-92 and the 2b042 keys throughout config.rs tests and docs/CONFIGURATION.md). A user with bindings saved against 2b042 who switches to BLE — or runs both transports side-by-side — gets a fresh empty config under 2b048. Asset lookup is similarly affected: the registry stores 2b042 and the suffix fallback in index.rs:90 matches on the trailing PID, so 2b048 won't hit either path.

Worth deciding intentionally rather than inheriting from "PID we observed = model_ids[0]".

Technical details
# Synthetic config_key splits MX Master 4 B from the Bolt/USB variant

## Affected sites
- `crates/openlogi-hid/src/inventory.rs:226``model_ids: [product_id, 0, 0]` where `product_id = 0xb048`.
- `crates/openlogi-core/src/device.rs:89-95``config_key()` formats `{ext:x}{model_ids[0]:04x}`.
- `crates/openlogi-assets/src/index.rs:81-96``find_by_model_id_suffix` matches by trailing PID, so `2b048` won't suffix-match a registry `2b042` entry.

## Required outcome
A deliberate choice between two policies, with the synthetic reflecting it:

1. **Unified identity** — MX Master 4 B shares config + assets with the Bolt/USB MX Master 4. Put `0xb042` in `model_ids[0]` (USB slot per the transport ordering doc on `DeviceModelInfo`) and `0xb048` in `model_ids[2]` (BTLE slot). `config_key` resolves to `"2b042"` and existing bindings + asset registry entries transfer.
2. **Separate identity** — BLE variant is its own device for config purposes. Keep `model_ids[0] = 0xb048` but ensure the asset registry has a `2b048` entry (or a fallback path), and document that bindings don't cross transports.

## Suggested approach
Option 1 is the lower-friction default and matches Logitech's per-transport `model_id` slot convention (USB=0, eQuad=1, BTLE=2). The transports flag should also stay accurate — set `usb: false, btle: true` so downstream code that branches on transport sees the truth.

## Open questions for the human
- Is the intent for MX Master 4 B BLE-direct to share bindings/assets with the Bolt/USB MX Master 4?
- Does the OpenLogi asset registry already carry a `2b048` entry, or does it rely on `2b042` covering the family?

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

d.vendor_id == LOGITECH_VID && is_hidpp_long_collection(d.usage_page, d.usage_id)
d.vendor_id == LOGITECH_VID
&& (is_hidpp_long_collection(d.usage_page, d.usage_id)
|| is_logitech_mouse(d))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This admits every Logitech mouse exposing a generic-desktop collection, not just the MX Master 4 B family. For other BLE-direct mice that pair the vendor HID++ collection with a generic mouse interface (Lift, Signature — already noted in the project's HID learnings), each enumerate now performs a redundant dev.open() + HID++ handshake on the generic interface before bailing out. Functional impact is none (the synthetic path PID-gates), but it's wasted work plus a second HID node touched on macOS.

Narrowing the admission to known fallback PIDs would keep the fix scoped to the device that needs it.

Technical details
# Narrow generic-desktop admission to known fallback PIDs

## Affected sites
- `crates/openlogi-hid/src/transport.rs:111-116``enumerate_hidpp_devices` filter chain.
- `crates/openlogi-hid/src/transport.rs:132-134``is_logitech_mouse` predicate.
- `crates/openlogi-hid/src/inventory.rs:47``MX_MASTER_4_PID` constant, currently the only known fallback PID.

## Required outcome
Generic-desktop Logitech mouse nodes are admitted only when their PID is one we know to fall back for. Other Logitech mice continue to be enumerated solely via the HID++ long-collection pages.

## Suggested approach
One option: expose the fallback PID set from `inventory.rs` and have `enumerate_hidpp_devices` check `d.product_id` against it before admitting via `is_logitech_mouse`. Another: keep the set in `transport.rs` and accept the cross-module duplication (the file already documents one such mirror for `LOGITECH_VID`). Either way, the predicate stays a one-liner.

/// MX Master 4 B (Bluetooth-direct) ProductID — distinct from 0xb042 which is
/// the USB cable / Bolt-receiver variant of the same device family.
const MX_MASTER_4_PID: u16 = 0xb048;
const MX_MASTER_4_EXT_MODEL_ID: u8 = 0x02;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

0x02 is a magic value with no attribution. The asset registry uses ext=02 while HID++ DeviceInformation reports ext=01 for the same MX Master 4 family — see crates/openlogi-assets/src/index.rs:81-88. A one-line note that this matches the registry ext (not what the device would report over HID++) would prevent the next reader from "fixing" it to 0x01.

// addresses the device's own features. Probe in case it answers.
// P2.4 — verified path; no Bolt-pairing slot indirection needed.
return Ok(probe_direct(channel, &info).await);
return Ok(probe_direct(Arc::clone(&channel), &info)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Arc::clone(&channel) is unnecessary here — channel isn't used after this call, so it can be moved directly into probe_direct. fallback_direct_mouse only borrows info.

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.

MX Master 4 over Bluetooth-direct not detected

2 participants