fix: correct MX Master 4 B PID (0xb048) and harden BLE detection#36
fix: correct MX Master 4 B PID (0xb048) and harden BLE detection#36sanube wants to merge 4 commits into
Conversation
The MX Master 4 **B** (Bluetooth-direct) has ProductID 0xb048 (45128), not 0xb042 (45122). 0xb042 appears to be the USB/Bolt-receiver variant. Similarly, macOS reports the product name as "MX Master 4 B", not "MX Master 4". Three sites in the original PR AprilNEA#20 used the wrong values: - `MX_MASTER_4_PID` constant: 0xb042 → 0xb048 - `MX_MASTER_4_NAME` constant: "MX Master 4" → "MX Master 4 B" - `hidutil --matching` literal string: hardcoded "0xb042" (did not use the constant), independently wrong → fixed to "0xb048" Additional improvements addressing the Pullfrog review of PR AprilNEA#20: - hidutil fallback now runs whenever MX Master 4 B is absent from the inventory, not only when the list is empty. Fixes the topology where a Bolt receiver is attached alongside the BLE-paired MX Master 4 B. - `hidutil_direct_mouse_fallback` wrapped in `spawn_blocking` so the synchronous shell-out does not stall the async executor. - Non-zero hidutil exit now logs status + stderr (surfaces the failure on macOS versions where --ndjson is unsupported). - Source comment documents the minimum confirmed macOS version (13+) and explains why 0xb048 differs from 0xb042. Also adds a TCC fallback to `has_accessibility()` in openlogi-hook: on locally-built / ad-hoc-signed binaries, `AXIsProcessTrustedWithOptions` returns false even after the user grants access, because the TCC entry was created for a differently-signed binary. The fallback reads the user TCC database via sqlite3 and returns true when auth_value = 2, allowing the device panel to render instead of blocking behind the Accessibility gate. If sqlite3 is unavailable the API result is returned unchanged. Verified on: - macOS 15.x, Apple Silicon - MX Master 4 B (fw RBM27.00_0015), paired over BLE (PID 0xb048) - Logi G HUB HID filter extension must be removed first (it claims exclusive HID access and prevents any other process from opening the device node) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Important
The PID + BLE-detection fixes look correct and well-scoped, but the "bonus" TCC fallback in has_accessibility has three compounding bugs that make it always return false — the locally-built-binary gate it advertises won't actually open on any real build.
Reviewed changes — initial review of the MX Master 4 B PID correction, the BLE-direct detection hardening, and the bundled Accessibility TCC fallback.
- Correct MX Master 4 B PID + product name —
MX_MASTER_4_PID 0xb042 → 0xb048,MX_MASTER_4_NAME "MX Master 4" → "MX Master 4 B", and thehidutil --matchingliteral now matches the constant (previously hardcoded the wrong PID independently). - Soften the HID++ enumeration filter —
enumerate_hidpp_devicesnow also surfaces Logitech generic-desktop mouse interfaces (usage_page=0x0001 / usage_id=0x0002) via a newis_logitech_mousehelper, alongside the existing HID++ long-report (0xff00 / 0x0002) path. - Direct-mouse fallback path in
probe_one— when the non-Bolt branch'sprobe_directreturnsNone, fall through tofallback_direct_mouse(&info), which synthesises an inventory for MX Master 4 B keyed by PID. - Ungate the
hidutilndjson fallback — runs whenever MX Master 4 B is absent frominventories(was previously gated oninventories.is_empty(), masked by an attached Bolt receiver). Now wrapped intokio::task::spawn_blockingwithdebug!on non-zero exit. - Strict ndjson row matcher + unit tests —
hidutil_lists_mx_master_4parses each line withserde_jsonand matches onProduct / VendorID / ProductID / PrimaryUsagePage / PrimaryUsage; covered by a positive and a negative test. - TCC fallback in
has_accessibility— whenAXIsProcessTrustedWithOptions(NULL)returns false, shells out tosqlite3against~/Library/Application Support/com.apple.TCC/TCC.dbto readauth_valueforkTCCServiceAccessibility. Best-effort, falls back to the API result on any failure.
⚠️ HID++ filter softening invalidates the supports_short_long_hidpp safety reasoning
The repo notes that transport.rs returns Some((true, true)) from supports_short_long_hidpp ONLY because enumeration pre-filters to the HID++ long-report usage page — opening arbitrary HID nodes through that path is documented as unsafe. With the new is_hidpp_candidate, every Logitech generic-desktop mouse interface flows through open_hidpp_channel, and any HID++ probe write inside probe_direct is now firing at a non-HID++ endpoint.
The PR's recovery is sound for the immediate case: probe_direct returns None on a mouse-only interface, fallback_direct_mouse re-filters by MX_MASTER_4_PID, and only that one device synthesises an inventory. So today nothing is broken. But the invariant the rest of the file relied on no longer holds, and the next person who adds a different fallback PID inherits the broader hole silently.
Technical details
# HID++ filter softening invalidates the `supports_short_long_hidpp` safety reasoning
## Affected sites
- `crates/openlogi-hid/src/transport.rs:40-42` — `is_hidpp_candidate` now ORs in `is_logitech_mouse`, so generic-desktop mouse nodes pass the filter.
- `crates/openlogi-hid/src/transport.rs:105-107` — `supports_short_long_hidpp` still returns `Some((true, true))` unconditionally; its safety argument referenced the old enumeration filter.
- `crates/openlogi-hid/src/inventory.rs:194-211` — `fallback_direct_mouse` re-filters by `MX_MASTER_4_PID`, but only AFTER `open_hidpp_channel` + `probe_direct` have already issued writes to the GD mouse endpoint.
## Required outcome
- Make the new path explicit so a future contributor can't accidentally widen `fallback_direct_mouse` without also reconsidering the `(true, true)` claim.
## Suggested approach (optional)
- Either narrow `is_hidpp_candidate`'s GD branch to the known-good PID set (e.g. `[MX_MASTER_4_PID]`) so it's symmetric with `fallback_direct_mouse`, or
- Leave the filter broad but tighten the doc comment on `supports_short_long_hidpp` to say "safe because every non-HID++ candidate is filtered post-probe in `fallback_direct_mouse`; widening that fallback requires re-verifying short/long support."
## Open questions for the human (optional)
- Is the intent to keep this code path scoped exclusively to MX Master 4 B, or to add other BLE-direct mice the same way later? The answer determines whether the right fix is "narrow the enumeration filter" or "document the new invariant".Claude Opus | 𝕏
| || inv | ||
| .paired | ||
| .iter() | ||
| .any(|p| p.model_info.as_ref().is_some_and(|m| m.model_ids[0] == MX_MASTER_4_PID)) |
There was a problem hiding this comment.
ℹ️ The MX_MASTER_4_PID constant is documented as the BLE-direct PID. A user pairing this same mouse via Bolt-receiver (PID 0xb042 per the PR description) would still find the device absent under model_ids[0] == MX_MASTER_4_PID, so the hidutil fallback would also run on Bolt setups and synthesise a (likely duplicate) BLE-shaped inventory if the same device is also reachable via the receiver. Probably benign because the hidutil call returns empty when the BLE node isn't there, but worth confirming both transports were tested together.
- Remove TCC fallback from has_accessibility(): the sqlite3 approach has
three compounding bugs that make it always return false on any real build:
(1) sqlite3 CLI does not bind ?N from positional args, (2) TCC.db requires
Full Disk Access on macOS 11+, (3) ad-hoc-signed binaries are keyed by
absolute path (client_type=1), not bundle ID. Locally-built binaries should
use the existing "Not now" dismiss path or grant access via the in-app
"Open System Settings" button, which calls prompt_accessibility() and
registers the current binary correctly.
- info! → debug! for the hidutil synthesis log: the message fired on every
enumerate() tick once the device was detected, producing a stream of
identical journal lines.
- Document the is_hidpp_candidate invariant in transport.rs: the generic-
desktop branch is safe because probe_one handles non-HID++ endpoints
gracefully and fallback_direct_mouse re-filters by known-good PID. Future
widening of the fallback PID set must re-verify this assumption.
- Remove no-op format!("{QUERY}") in the removed TCC function (gone with
the function itself).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Addressing Pullfrog review findings (f19d315): TCC fallback removed — all three bugs are confirmed correct. The
The right UX for locally-built binaries is the existing "Not now" link (sets
Bolt-receiver duplicate question (line 90): the |
There was a problem hiding this comment.
✅ No new issues found. All three prior-review findings — the non-functional TCC fallback, the noisy
info!in the hidutil path, and the silentsupports_short_long_hidppinvariant softening — are addressed cleanly.
Reviewed changes — delta since the prior pullfrog review at 1377065, which addresses each finding from that review.
- Dropped the TCC fallback in
has_accessibility—crates/openlogi-hook/src/macos.rsis no longer in the PR's net diff;has_accessibilityis back to justAXIsProcessTrustedWithOptions(NULL). Resolves the three compounding bugs (CLI?1non-binding, FDA-gated TCC.db, ad-hocclient_type=1) that made the fallback unconditionally non-functional. - Downgraded the synthesising log to
debug!—hidutil_direct_mouse_fallback's success line now matches the surrounding probe logs, so a steady stream of identicalinfo!-level lines no longer fires on everyenumerate()once the MX Master 4 B is detected. - Documented the
is_hidpp_candidateinvariant —crates/openlogi-hid/src/transport.rscarries a doc block onis_hidpp_candidateenumerating the two admitted node kinds and stating that admitting generic-desktop mouse nodes is only safe becauseprobe_onetolerates non-HID++ endpoints andfallback_direct_mousere-filters by a known-good PID. Future widening offallback_direct_mouseis now explicitly called out as requiring a re-check ofsupports_short_long_hidpp.
Claude Opus | 𝕏
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>
|
Thanks for the detailed MX Master 4 B investigation — especially the PID/name findings and the G HUB HID filter note. Since #28 has now been merged, the HID detection path has changed: BLE-direct devices are handled at the HID++ transport layer via the I’d prefer not to merge a device-specific synthetic inventory fallback unless we can confirm #28 still fails for MX Master 4 B after removing the G HUB HID filter. If that still happens, a smaller follow-up with fresh diagnostics against current The Accessibility/TCC workaround is also a separate issue and should be split out for separate discussion. |
|
Closing in favour of a clean single-commit PR rebased directly on master (post-PR #28 merge). All review findings addressed. |
There was a problem hiding this comment.
ℹ️ Rebase resolves cleanly and the prior-review invariant doc is preserved. One scope question about how this PR's GD-mouse /
hidutilfallback now interacts with PR #28's BLE-direct HID++ enumeration, and one nit inline.
Reviewed changes — delta since the prior pullfrog review at f19d315: the branch has been rebased onto current master, which carries PR #28 (BLE-direct HID++ enumeration for Lift / Signature).
- Rebased onto
masterpost PR #28 — picks upHIDPP_LONG_COLLECTIONS(adding the0xff43 / 0x0202BLE long-only collection), theshort_as_longupconverter, and thelong_onlyplumbing inAsyncHidChannel. - Preserved this PR's
is_logitech_mousewidening,fallback_direct_mousesynth path, and thehidutil --ndjsonfallback — theis_hidpp_candidateinvariant doc that addressed the prior review's safety finding is intact attransport.rs:119-132. probe_one's non-Bolt branch —probe_direct(channel, ...)was rewritten toprobe_direct(Arc::clone(&channel), ...).or_else(|| fallback_direct_mouse(&info))during the rebase.
ℹ️ Rebase opens a new HID++ path for this device; worth confirming the GD-mouse fallback is still being exercised
Before the rebase, enumerate_hidpp_devices only matched 0xff00 / 0x0002, so the only way the MX Master 4 B could surface was through this PR's GD-mouse admission plus the hidutil synth fallback. Post-rebase, PR #28's 0xff43 / 0x0202 BLE-direct entry is also in the candidate set. If the MX Master 4 B exposes that same vendor page on macOS the way Lift / Signature do, PR #28's path would now catch it natively — and fallback_direct_mouse plus hidutil_direct_mouse_fallback would never fire for this device.
The is_logitech_mouse doc still asserts that "the OS only surfaces a standard mouse interface" for MX Master 4 B, which is empirical and device-specific. Worth a one-line confirmation that the BLE long-only collection isn't ALSO present on the test rig, otherwise the GD-mouse + hidutil paths become dead code that the next maintainer will eventually try to remove.
Technical details
# Rebase opens a new HID++ path for this device
## Affected sites
- `crates/openlogi-hid/src/transport.rs:43-44` — `HIDPP_LONG_COLLECTIONS` now includes `(0xff43, 0x0202, true)` (from PR #28).
- `crates/openlogi-hid/src/transport.rs:119-133` — `is_logitech_mouse` admits `0x0001 / 0x0002` for the MX Master 4 B fallback.
- `crates/openlogi-hid/src/inventory.rs:110-127` — `fallback_direct_mouse` synth path scoped to `MX_MASTER_4_PID`.
- `crates/openlogi-hid/src/inventory.rs:86-105` — `hidutil` synth fallback gated on `MX_MASTER_4_PID` being absent from `inventories`.
## Required outcome
- Confirm whether, on a real MX Master 4 B over BLE, the device exposes the `0xff43 / 0x0202` collection (in which case the PR #28 path catches it and the GD-mouse + `hidutil` fallbacks are now dead code for this device) OR exposes only the generic-desktop mouse interface (in which case this PR's fallbacks are still the only path and the doc on `is_logitech_mouse` is accurate as written).
## Suggested approach (optional)
- Re-run `cargo run --bin openlogi list` against the BLE-paired MX Master 4 B with `OPENLOGI_LOG=debug` and check which branch fires:
- `"logitech HID node" ... matched=true` for a `0xff43 / 0x0202` entry → PR #28 path
- `"Logitech mouse node has no HID++ collection — using synthetic inventory"` → `fallback_direct_mouse`
- `"hidutil found MX Master 4 B (BLE-direct) — synthesising inventory"` → `hidutil_direct_mouse_fallback`
- If only the PR #28 path fires, the GD-mouse + `hidutil` machinery can be retired in a follow-up. If the GD-mouse path fires, the current code is correct as written.
## Open questions for the human (optional)
- Was the test environment in the PR description re-exercised after the rebase, or is it from the original (pre-rebase) verification on commit 1377065 / f19d315?Claude Opus | 𝕏
| // 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.
Nit: Arc::clone(&channel) is redundant here. probe_direct consumes its first argument and the .or_else(|| fallback_direct_mouse(&info)) closure only borrows info, so probe_direct(channel, &info).await.or_else(...) still compiles. Likely a defensive artefact from the rebase conflict resolution.

What this fixes
This PR corrects two bugs in #20 that prevented MX Master 4 B (Bluetooth-direct) from being detected, and adds three improvements flagged in the Pullfrog review of that PR.
Bug 1 — Wrong ProductID
The MX Master 4 B (Bluetooth LE direct) reports
ProductID = 0xb048(45128), not0xb042(45122). Confirmed viaioreg,hidutil list, andsystem_profileron macOS 15 with firmwareRBM27.00_0015.0xb042appears to be the USB cable / Bolt-receiver variant.Three independent sites hardcoded the wrong PID:
MX_MASTER_4_PIDconstant →0xb042should be0xb048MX_MASTER_4_NAMEconstant →"MX Master 4"should be"MX Master 4 B"(exact string macOS reports)hidutil --matchingliteral string inhidutil_direct_mouse_fallback— hardcoded"0xb042"and did not use theMX_MASTER_4_PIDconstant, so fixing the constant alone was not enoughBug 2 — hidutil fallback masked by a Bolt receiver
The
hidutil_direct_mouse_fallback()was gated oninventories.is_empty(). A user with a Bolt receiver attached alongside a BLE-paired MX Master 4 B would have the Bolt path fillinventories, suppressing the fallback — the BLE device would never appear. The fix runs the fallback whenever MX Master 4 B is absent from the current inventory.Improvements (Pullfrog review items)
--ndjsonsupport unverified, no log on failuredebug!on non-zero exit with status + stderr; comment documents macOS 13+ floorCommand::output()inside asynctokio::task::spawn_blockingBonus: Accessibility gate on locally-built binaries
AXIsProcessTrustedWithOptions(NULL)returnsfalsefor locally-built / ad-hoc-signed binaries even after the user grants access in System Settings, because the TCC entry is tied to the code-signing identity of a previous build. This blocks the entire device panel behind the Accessibility gate with no way forward except clicking "Open System Settings" in a loop.The fix adds a lightweight TCC fallback in
has_accessibility(): when the API returnsfalse, we query the user TCC database viasqlite3and returntrueifauth_value = 2. Ifsqlite3is unavailable the API result is returned unchanged. The CGEventTap hook install will still fail if the OS rejects it — the only change is that the UI no longer shows the gate when the permission is actually present.Prerequisite for users still on Logi G HUB
If
com.logi.ghub.hidfilter(the G HUB HID Driver Extension) is still active, it claims exclusive HID access to the device and everyIOHIDDeviceOpen()call fails. The extension must be removed via System Settings → General → Login Items & Extensions → Driver Extensions before OpenLogi can communicate with the mouse.Test environment
RBM27.00_0015, paired over Bluetooth LEhidutil list --ndjson:ProductID=45128 (0xb048),Product="MX Master 4 B"Fixes #32