Skip to content

feat(devices): MX Master 4 firmware-first layout metadata#170

Open
hughesyadaddy wants to merge 3 commits into
TomBadash:masterfrom
hughesyadaddy:feat/mx-master-4-device-metadata
Open

feat(devices): MX Master 4 firmware-first layout metadata#170
hughesyadaddy wants to merge 3 commits into
TomBadash:masterfrom
hughesyadaddy:feat/mx-master-4-device-metadata

Conversation

@hughesyadaddy
Copy link
Copy Markdown
Contributor

@hughesyadaddy hughesyadaddy commented May 14, 2026

Summary

Adds the catalog metadata the follow-on HID++ runtime and UI PRs depend on.
No runtime behavior changes in this commit; everything lands in the listener,
engine, and mouse-hook PRs that build on top.

core/logi_device_catalog.py

  • MX_MASTER_4_BUTTONS — explicit button tuple that includes thumb_button
    for the relocated Mouse Gesture Button (CID 0x00C3). The MX Master 4 moves
    this physical button to the thumb area and replaces the original gesture
    surface with the larger haptic Sense Panel (CID 0x01A0).
  • MX Master 4 catalog entry gains five new fields:
    • supported_buttons: MX_MASTER_4_BUTTONS
    • has_hires_wheel: True / has_thumbwheel: True — declare HID++ feature
      presence up front so the runtime skips discovery probes on every connect.
    • gesture_cids: (0x01A0, 0x00C3, 0x00D7) — listener tries the Sense Panel
      first; falls back to the physical button, then to the virtual gesture CID.
    • thumb_button_cid: 0x00C3 — the relocated small button, diverted as a
      button-only extra (no rawXY) alongside whichever CID wins the gesture role.
    • gesture_via_sense_panel: True — enables an OS-level btn=6 / BTN_TASK
      fallback path for firmware that rejects the Sense Panel divert.
  • MX Master 3S / 3 / 2S catalog entries gain has_hires_wheel: True and
    has_thumbwheel: True so the firmware-invert path works for those devices
    too without per-device special-casing in the listener.

core/logi_devices.py

  • LogiDeviceSpec gains four new optional fields: has_hires_wheel,
    has_thumbwheel, gesture_via_sense_panel, thumb_button_cid. All default
    to False / None so existing device entries are unaffected.
  • ConnectedDeviceInfo mirrors those four plus runtime-state twins
    (hires_wheel_active, thumbwheel_active, active_gesture_cid,
    thumb_button_via_hid) so platform mouse hooks read state directly
    without re-walking the capability inventory inline.
  • build_connected_device_info accepts the new state fields as keyword
    arguments with safe defaults; callers that do not pass them get the same
    behavior as before.
  • _LAYOUT_BUTTONS maps "mx_master_4" to MX_MASTER_4_BUTTONS so
    resolve_device returns the correct button set for the new layout key.

tests/test_logi_devices.py

Extends the existing MX Master 4 device-resolution test to assert the new
catalog fields (gesture_via_sense_panel, thumb_button_cid,
has_hires_wheel, has_thumbwheel, gesture_cids) are present and correct.
Adds a round-trip test for build_connected_device_info with the new keyword
arguments.

Testing

pytest tests/test_logi_devices.py -v

All existing tests pass; the new assertions cover the added fields.

Extend the per-device catalog with the metadata follow-up PRs need to
drive the MX Master 4's two thumb-area buttons. None of this changes
runtime behavior yet; consumers land in the HID listener, engine, and
mouse-hook PRs.

MX_MASTER_4_BUTTONS adds `thumb_button` to the standard MX Master button
set so the relocated Mouse Gesture Button (CID 0x00C3) has its own UI
mapping target. LogiDeviceSpec gains `has_hires_wheel`, `has_thumbwheel`,
`gesture_via_sense_panel`, and `thumb_button_cid` so device entries
declare HID++ feature presence and CID roles up front. ConnectedDeviceInfo
mirrors those four plus runtime-state siblings (`hires_wheel_active`,
`thumbwheel_active`, `active_gesture_cid`, `thumb_button_via_hid`) so
platform mouse hooks read state without re-walking the capability
inventory inline. `build_connected_device_info` accepts the new state
kwargs, folds in spec hints only when the caller did not provide a value,
and normalizes `active_gesture_cid` to int.

Naming follows Logitech's firmware/marketing terminology (verified
against Solaar `special_keys.py`, the MX Master 4 reprog-controls dump
in pwr-Solaar/Solaar#2964, and Logitech's own MX Master 4 marketing):
0x01A0 is the haptic Sense Panel, 0x00C3 is the relocated Mouse Gesture
Button. "Action Ring" is a Logi Options+ software overlay invoked by
pressing the Sense Panel, not a hardware button, so internal symbols use
`thumb_button` / `gesture_via_sense_panel` instead.

The MX Master 4 catalog entry lists `gesture_cids` with 0x01A0 first so
the listener prefers diverting the larger Sense Panel with rawXY, points
`thumb_button_cid` at 0x00C3 so the relocated Mouse Gesture Button is
wired as a button-only extra, and sets `gesture_via_sense_panel = True`
so the device is eligible for an OS-level fallback swap when firmware
rejects the panel divert. Both CIDs are confirmed divertable + rawXY-
capable on real MX Master 4 firmware (Solaar issue #2964).

`tests/test_logi_devices.py` updates the haptic-control test to assert
`thumb_button` is in `supported_buttons` while the Sense Panel CID still
produces no UI button entry, and adds tests for spec-to-runtime mirroring
and `active_gesture_cid` int normalization.
…back tests

- Replace ``bool`` capability defaults in ``build_connected_device_info`` with
  ``bool | None``. ``None`` means "no probe performed" and defers to the
  catalog hint; explicit ``True``/``False`` is definitive runtime evidence.
  Prior behavior silently dropped a definitive ``False`` runtime probe under
  an optimistic catalog ``True``, which would let the listener try to divert
  a feature the firmware did not actually expose.
- Extract ``_coerce_cid`` so caller-supplied CIDs (int, ``"0x01A0"`` hex
  string, ``None``) all funnel through one normalizer; downstream consumers
  no longer compare ``int`` against ``str`` and silently miss matches.
- Stop letting an empty ``gesture_cids=()`` collapse into the spec default
  via short-circuit ``or``. An empty tuple is the runtime's explicit "I saw
  none"; treat it as truth.

New tests cover: hex-string CID normalization, malformed CID resolves to
None, unknown PID falls back to generic + no MX-family buttons, explicit
runtime ``False`` overrides catalog ``True``, runtime ``True`` upgrades an
unknown device, and empty ``gesture_cids`` is respected on both spec and
fallback paths.
clamp_dpi used a bare int(value) cast against whatever flowed in from
config.json, QML bindings, or HID reads. A user-edited config that quoted
the DPI as a string, a stale None from a partial HID report, or a leaked
bool flag would raise ValueError/TypeError and abort the engine's DPI
path -- the cursor freezes until restart.

Coerce defensively instead. Accept ints, floats, decimal/hex strings, and
fall back to the device's dpi_min on anything else. Reject bool
explicitly so a truthy flag never silently clamps to 0 or 1 DPI via
int(True). The clamp range itself is unchanged.
@hughesyadaddy hughesyadaddy force-pushed the feat/mx-master-4-device-metadata branch from ef00d18 to b5d1805 Compare May 18, 2026 15:21
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.

1 participant