Skip to content

Commit 2718895

Browse files
committed
docs: MX Master 4 firmware-first + HID++ wheel native-invert
Update the docs to match the firmware-first implementation that landed with the MX Master 4 catalog work and the HID++ wheel native-invert plumbing. README.md's scroll-inversion bullet now mentions that MX Master devices apply inversion at the source via HID++ (0x2121 + 0x2150) and that inversion therefore survives Synergy / DeskFlow / KVM forwarding. The Limitations section documents the `wheel_divert: "off"` kill switch. DEVELOPMENT.md replaces the older "divert + inject" description (which described a flow Mouser doesn't implement) with the actual native-invert flow: the firmware flips the wheel sign in the device, the OS still receives native HID scroll, and Mouser stays out of the scroll path entirely. The config-version reference is bumped to 11 to match the recent migrations. CONTRIBUTING_DEVICES.md gains a compact note explaining the MX Master 4 thumb-area role assignment. The Sense Panel (CID 0x01A0, Solaar's `Haptic` feature, marketed by Logitech as "Haptic Sense" and exposed by Logi Options+ under the "Action Ring" software overlay) drives directional gestures because it's far more comfortable for swipes than the small side button. The small Thumb button (CID 0x00C3, Solaar's `Mouse_Gesture_Button` with the `Thumb_Button` alias on MX Master 4) is the single-press mapping target. The note includes the spec wiring snippet (`gesture_cids`, `thumb_button_cid`, `gesture_via_sense_panel`) and lists the runtime fields the platform mouse hooks read to pick the active path. The new-device template adds has_hires_wheel / has_thumbwheel so contributors know which fields matter for wheel inversion on future devices.
1 parent c4d3086 commit 2718895

3 files changed

Lines changed: 77 additions & 5 deletions

File tree

CONTRIBUTING_DEVICES.md

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,41 @@ Look at the `reprog_controls` array. Each entry has a `cid` (Control ID) and
4242
| `0x0052` | Middle click |
4343
| `0x0053` | Back (side button) |
4444
| `0x0056` | Forward (side button) |
45-
| `0x00C3` | Gesture button (physical) |
45+
| `0x00C3` | Gesture button (physical, "Thumb button" on MX Master 4) |
4646
| `0x00C4` | Smart Shift / Mode Shift |
4747
| `0x00D7` | Virtual gesture button |
48+
| `0x01A0` | MX Master 4 Sense Panel (see role-swap notes below) |
49+
50+
MX Master 4 has two thumb-area buttons that both surface as divertable
51+
HID++ controls and need an explicit role swap. The Sense Panel
52+
(`0x01A0`, Solaar's `Haptic` feature -- the touch surface Logitech
53+
markets as "Haptic Sense" and Logi Options+ exposes under the "Action
54+
Ring" overlay) drives directional gestures because it's far more
55+
comfortable for swipes than the small side button. The small Thumb
56+
button (`0x00C3`, the legacy gesture CID on older MX Master variants;
57+
Solaar's `Mouse_Gesture_Button` with the `Thumb_Button` alias on MX
58+
Master 4) is the single-press trigger. Wire it like this:
59+
60+
```python
61+
"gesture_cids": (0x01A0, 0x00C3, 0x00D7), # Sense Panel CID first
62+
"thumb_button_cid": 0x00C3, # small button as button-only extra
63+
"gesture_via_sense_panel": True, # enables OS-level fallback swap
64+
```
65+
66+
`0x01A0` lives in `gesture_cids` so the listener prefers diverting it
67+
with rawXY. `thumb_button_cid` is diverted as button-only (no rawXY),
68+
so the firmware doesn't suppress normal OS mouse motion while the
69+
small button is held -- that was the root cause of the cursor-freeze
70+
on stock MX Master 4. `gesture_via_sense_panel` enables an OS-level
71+
`btn=6` / `BTN_TASK` swap fallback for cases where firmware rejects
72+
the `0x01A0` divert; the platform mouse hooks consult
73+
`active_gesture_cid` (set to `0x01A0` on success, anything else on
74+
fallback) and `thumb_button_via_hid` (true when the extra divert is
75+
installed) on `ConnectedDeviceInfo` to pick the right path.
76+
77+
Older MX Master mice (3S, 3, 2S, classic) keep `gesture_via_sense_panel
78+
= False` (the default) so their HID++ gesture button continues to
79+
drive swipes and the global `Gesture button` label is shown.
4880

4981
Not all CIDs are divertable. Check the `flags` field -- if bit `0x0020` is
5082
set, the control can be intercepted by Mouser.
@@ -75,12 +107,37 @@ Add a new dict to `LOGI_DEVICE_SPECS`:
75107
"gesture_cids": (0x00C3,), # from gesture_candidates in your dump
76108
"dpi_min": 200,
77109
"dpi_max": 4000, # from discovered DPI range, or vendor specs
110+
"has_hires_wheel": False, # set True if device exposes 0x2121
111+
"has_thumbwheel": False, # set True if device exposes 0x2150
78112
},
79113
```
80114

115+
#### `has_hires_wheel` and `has_thumbwheel`
116+
117+
These flags tell Mouser the device exposes the corresponding HID++ feature so it
118+
can divert the wheel and apply scroll inversion at the source (matching Logitech
119+
Options+ behavior). They're catalog hints only — runtime feature discovery in
120+
`HidGestureListener` always overrides them, so a wrong catalog flag won't cause
121+
a divert attempt against a non-existent feature.
122+
123+
Set them on every device you can confirm exposes the feature. Quick way to find
124+
out: connect the device, run Mouser with debug logs enabled, and look for
125+
`[HidGesture] Found wheel feature 0x2121` (HiResWheel) or `Found wheel feature
126+
0x2150` (Thumbwheel) in the output.
127+
128+
| Flag | True when device has |
129+
|---|---|
130+
| `has_hires_wheel` | A vertical scroll wheel that supports HID++ feature `0x2121` (HiResWheel). Most modern Logitech mice. |
131+
| `has_thumbwheel` | A horizontal thumbwheel that supports HID++ feature `0x2150` (Thumbwheel). MX Master family only. |
132+
81133
Pick the right button tuple for `supported_buttons`:
82134

83135
- `MX_MASTER_BUTTONS` -- middle, gesture (with swipes), back, forward, hscroll, mode_shift
136+
- `MX_MASTER_4_BUTTONS` -- everything in `MX_MASTER_BUTTONS` plus
137+
`thumb_button`, the slot fed by the small Thumb button (CID `0x00C3`
138+
via HID++) and -- on fallback paths where the Sense Panel divert was
139+
rejected -- by the Sense Panel itself (button 6 / `BTN_TASK` at the
140+
OS layer)
84141
- `MX_ANYWHERE_BUTTONS` -- middle, gesture (with swipes), back, forward
85142
- `MX_VERTICAL_BUTTONS` -- middle, back, forward
86143
- `GENERIC_BUTTONS` -- middle, back, forward (safe default)

DEVELOPMENT.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,19 @@ Logitech gesture / thumb buttons do not always appear as standard mouse events.
147147

148148
The same module owns the SmartShift integration. It prefers the enhanced feature `0x2111` (`FEAT_SMART_SHIFT_ENHANCED`) when available and falls back to `0x2110`, exposing both an enable flag and a sensitivity threshold; pending settings are re-applied on every reconnect (including wake-from-sleep).
149149

150+
#### HID++ wheel native-invert
151+
152+
On MX Master devices `HidGestureListener` discovers two HID++ wheel features and asks the firmware to invert the scroll sign at the source. The OS receives native HID scroll reports with the direction already flipped, so inversion survives Synergy / DeskFlow / KVM forwarding without Mouser becoming the scroll producer.
153+
154+
| Feature | ID | Functions Mouser uses | Wire format |
155+
|---|---|---|---|
156+
| **HiResWheel** (vertical) | `0x2121` | fn 0 `getWheelCapability` (multiplier discovery), fn 1 `getWheelMode` (read-before-write), fn 2 `setWheelMode` | `setWheelMode(mode)` byte: bit0 = target (Mouser writes 0 = OS), bit1 = hi-res (Mouser writes 0 = low-res so the firmware-default cadence survives), bit2 = invert (Mouser flips per `invert_vscroll`). The device continues emitting native HID scroll; only the sign is reversed. |
157+
| **Thumbwheel** (horizontal) | `0x2150` | fn 0 `getThumbwheelInfo`, fn 2 `setThumbwheelReporting` | `setThumbwheelReporting(reporting, invertDirection)`: Mouser writes `[0x00, invertByte]` to keep the wheel non-diverted but flip the firmware's horizontal sign for `invert_hscroll`. |
158+
159+
The engine drives this via `_apply_wheel_invert_setting` on every profile / device change. When the device acknowledges, `wheel_native_invert_active` is set on both the engine and the platform mouse hook, and the hook's OS-layer inversion path is suppressed so a second flip doesn't net out to no inversion. On unsupported devices, devices that reject the request, or when the kill-switch is off, the hook's existing OS-layer inversion handles `invert_vscroll` / `invert_hscroll` via the fallback path (in-place CGEvent negation on macOS; uinput delta sign-flip on Linux; LL hook delta sign-flip on Windows).
160+
161+
The kill-switch is `settings.wheel_divert` in `config.json`: `"auto"` (default) enables divert on capable devices; `"off"` forces the OS-layer fallback even on MX Master.
162+
150163
### App detector
151164

152165
[`core/app_detector.py`](core/app_detector.py) polls the foreground window every 300ms.
@@ -173,9 +186,9 @@ All settings live in `config.json` under the platform config dir (`%APPDATA%\Mou
173186

174187
- Multiple named profiles with per-profile button mappings, including gesture tap + swipe actions
175188
- Per-profile app associations (list of `.exe` / bundle / process names)
176-
- Global settings: DPI, scroll inversion, macOS trackpad filtering, gesture tuning, appearance, debug flags, Smart Shift mode + sensitivity, language, and startup preferences (`start_at_login`, `start_minimized`)
189+
- Global settings: DPI, scroll inversion (`invert_vscroll`, `invert_hscroll`), HID++ wheel-divert kill-switch (`wheel_divert: "auto" | "off"`), macOS trackpad filtering, gesture tuning, appearance, debug flags, Smart Shift mode + sensitivity, language, and startup preferences (`start_at_login`, `start_minimized`)
177190
- Per-device layout override selections for unsupported devices
178-
- Automatic migration from older config versions (current version `9`)
191+
- Automatic migration from older config versions (current version `11`)
179192

180193
Logs are written via [`core/log_setup.py`](core/log_setup.py) to a 5 × 5 MB rotating file in `~/Library/Logs/Mouser`, `%APPDATA%\Mouser\logs`, or `$XDG_STATE_HOME/Mouser/logs`. The setup is idempotent and safe to call multiple times — `main_qml.py` invokes it before any Qt or core import so startup output is captured from the very first line.
181194

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,12 @@ That's it. The app opens, drops a tray / menu-bar icon, and starts remapping imm
7777
- **Logitech Options+ must not be running.** Both apps fight over HID++ access; quit Options+ before launching Mouser.
7878
- **macOS** asks for **Accessibility** permission so the event tap can intercept mouse events. See [readme_mac_osx.md](readme_mac_osx.md) for the full setup walkthrough.
7979
- **Linux** needs read access to `/dev/hidraw*`, `/dev/input/event*`, and write access to `/dev/uinput`. Run the bundled helper once after extracting:
80+
8081
```bash
8182
cd /path/to/extracted/Mouser
8283
./install-linux-permissions.sh
8384
```
85+
8486
Reconnect the mouse, then relaunch.
8587
- Config is saved automatically to:
8688
- `%APPDATA%\Mouser\config.json` (Windows)
@@ -113,7 +115,7 @@ That's it. The app opens, drops a tray / menu-bar icon, and starts remapping imm
113115
- **DPI / pointer speed** — slider from 200 to the device max (8000 on MX Master) with quick presets, plus a `Cycle DPI Presets` action you can map to a button.
114116
- **Smart Shift** — toggle Logitech's ratchet ↔ free-spin scroll mode (HID++ `0x2111`), with a sensitivity threshold and a mappable `Toggle SmartShift` action.
115117
- **Switch scroll mode** — bind a button to flip ratchet / free-spin without opening the UI; defaults to mode-shift.
116-
- **Scroll direction inversion** — independent toggles for vertical and horizontal scroll.
118+
- **Scroll direction inversion** — independent toggles for vertical and horizontal scroll. On MX Master devices, inversion is applied at the device via HID++ (`0x2121` HiResWheel + `0x2150` Thumbwheel) and survives Synergy / DeskFlow / KVM forwarding.
117119
- **Gesture button + swipe actions** — tap for one action, swipe up/down/left/right for four others.
118120

119121
### Cross-platform
@@ -314,7 +316,7 @@ For project layout, the architecture diagram, the HID++ gesture detector, the En
314316

315317
- **Per-device mappings aren't fully separated yet** — layout overrides are stored per detected device, but profile mappings are still global.
316318
- **Conflicts with Logitech Options+** — both apps fight over HID++ access. Quit Options+ before running Mouser.
317-
- **Scroll inversion** uses coalesced post-injection on Windows to avoid LL-hook deadlocks; it's stable in mainstream apps but may misbehave in some games or low-level drivers.
319+
- **Scroll inversion** — on MX Master devices, Mouser asks the firmware to flip the wheel and thumbwheel sign at the source (HID++ `0x2121` / `0x2150`); the device keeps emitting native HID scroll, so inversion survives Synergy / DeskFlow / KVM forwarding. On other devices it falls back to OS-layer event-tap / LL-hook injection (stable in mainstream apps but may misbehave in some games or low-level drivers). Set `"wheel_divert": "off"` in `config.json` `settings` to force the OS-layer fallback even on capable devices.
318320
- **Admin not required** — but injected keystrokes may not reach elevated windows or some games. Run Mouser elevated if you need that path.
319321
- **Linux app detection is partial** — X11 works via `xdotool`, KDE Wayland works via `kdotool`, GNOME / other Wayland compositors still fall back to the default profile.
320322
- **Linux device permissions** — Mouser needs access to `/dev/hidraw*`, `/dev/input/event*`, and `/dev/uinput`. Use [`install-linux-permissions.sh`](packaging/linux/install-linux-permissions.sh) once instead of running as root.

0 commit comments

Comments
 (0)