fix: detect MX Master 4 B (0xb048) over macOS Bluetooth LE#40
Conversation
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>
|
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.
Only 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 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 |
There was a problem hiding this comment.
ℹ️ 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_mousefilter intransport.rslets nodes withusage_page=0x0001 / usage_id=0x0002through alongside the existing HID++ long-collection pairs; safety invariant documented in the function rustdoc. - Synthetic inventory for MX Master 4 B —
fallback_direct_mousebuilds aDeviceInventorykeyed by PID0xb048whenprobe_directreturnsNone, plussynthetic_direct_mouse_inventoryhelper used by both the in-process andhidutilpaths. hidutil --ndjsonlast-resort fallback — shells out to/usr/bin/hidutilon macOS (wrapped inspawn_blocking), parses ndjson withserde_json, and verifiesProduct/VendorID/ProductID/PrimaryUsagePage/PrimaryUsagebefore synthesising. Ungated to run whenever MX4B is absent so the Bolt-alongside-BLE topology still works.- Constants corrected —
MX_MASTER_4_PID0xb042→0xb048,MX_MASTER_4_NAME"MX Master 4"→"MX Master 4 B", andMX_MASTER_4_EXT_MODEL_ID = 0x02introduced.
ℹ️ 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?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)) |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.

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:0xb042) and name ("MX Master 4"). The BLE-direct variant reportsProductID = 0xb048andProduct = "MX Master 4 B"— confirmed viahidutilandioregon macOS 15.usage_page=0x0001 / usage_id=0x0002) — no Logitech vendor HID++ collection.enumerate_hidpp_devicesfiltered it out entirely.hidutil --matchingliteral string hardcoded0xb042independently of the constant, so fixing the constant alone was not sufficient.Changes
transport.rsis_logitech_mouse()for generic-desktop Logitech mouse nodesenumerate_hidpp_devices()to admit these nodes alongside the HID++ collections (0xFF00and0xFF43) handled by PR fix(openlogi-hid): enumerate Bluetooth-LE-direct HID++ mice (Lift, Signature) #28inventory.rsMX_MASTER_4_PID:0xb042→0xb048MX_MASTER_4_NAME:"MX Master 4"→"MX Master 4 B"probe_one: adds.or_else(fallback_direct_mouse)afterprobe_directhidutil_direct_mouse_fallback: wrapped inspawn_blocking, logs stderr on failurePullfrog review findings (from #36) — all addressed
info!fires on every enumerate tickdebug!is_hidpp_candidateinvariant undocumentedtransport.rsCommand::output()in async contextspawn_blockingPrerequisite 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
RBM27.00_0015, paired over BLE (PID 0xb048)Fixes #32