Skip to content

MX Master 4: button remapping (0x1B04) - #11

Merged
snekxs merged 1 commit into
OpenMouse-Project:mainfrom
weltern:feat/logitech-button-remapping
Aug 14, 2026
Merged

snekxs merged 1 commit into
OpenMouse-Project:mainfrom
weltern:feat/logitech-button-remapping

Conversation

@weltern

@weltern weltern commented Aug 12, 2026 •

Copy link
Copy Markdown
Contributor

Adds feature 0x1B04 (REPROG CONTROLS V4): read the control table, point one
control at another, and hand diverted controls back to the hardware. Codec in
src/logitech/controls.ts, driver methods on LogitechHidppClient.

Follow-up to #9 (merged), whose scope deliberately held button remapping
back so that change stayed reviewable. Companion OpenMouse PR: OpenMouse-Project/openmouse#83
(the buttons-tab card).

Verified on hardware: MX Master 4, WPID B042, firmware LD 04.00, over a Logi
Bolt receiver 046d:c548, pairing slot 2.

The limitation, up front

This remaps control → control only: a button can be made to act as another
button the device already knows (middle click, back, forward, etc.). It does
not implement Options+-style actions like "launch an app" or "switch
desktop". Options+ does those by diverting the button — routing its events to
HID++ — and handling them in its own software, which a driver with nothing
listening cannot do. So diversion is exposed only to be cleared, never set.

Evidence

As with #9, almost all of it came from watching Logi Options+ rather than
guessing: the Bolt receiver broadcasts replies to every open handle, and the
low nibble of the function byte carries the requester's software id (Options+
uses 0x0F). Changing a mapping in the vendor app and reading the reply gives
the function id and byte layout directly. No packet capture.

What the device decides, not this code

Every control reports a group and a bitmask of the groups it accepts as remap
targets. Left and right click report a mask of zero — the firmware itself
refuses to move the primary buttons; there is no rule here that excludes them.
They still appear as targets for other controls, because any control in an
accepted group is a legal destination even when it is not a legal source.

Two decode details worth flagging

  • The mapping flags are one 16-bit field split across two non-adjacent
    bytes
    — low at payload[2], high at payload[5], with the remap target at
    [3..4] between them. Read as a contiguous pair, the target's high byte lands
    in the flags, and any button remapped to a high control id (e.g. the Actions
    Ring, 0x01A0) reads as diverted. Decoded from the split field it does not.
  • Each mapping flag occupies two bits — a value and a "valid" bit one position
    higher
    — and the device ignores any flag whose valid bit is clear. A pure
    remap therefore writes a zero flags byte, which changes no flag at all and
    leaves diversion exactly as it was. Clearing diversion sets only the valid
    bits, with the value bits clear, so it can only ever turn diversion off.

A control merges getControlIdInfo (key/capability flags) with
getControlIdReporting (the 16-bit mapping field). Those were named apart
(flags vs mappingFlags) so the merge keeps both; virtual and
reprogrammable are exposed as their own booleans so consumers never read raw
bits.

Deliberately not here

  • Persistent diversion (0x04). Options+ uses the temporary flag (0x01),
    which the device clears itself when the owner stops — so quitting Options+
    does not leave buttons dead. The persistent flag would, so it is neither set
    nor offered.
  • Rich software actions (launch app, macros): see the limitation above.

Testing

  • npm run check green: 384 tests, including codec tests for every byte
    layout above and fake-HID driver tests for read-back, refusal of illegal or
    absent controls, the acknowledge-but-ignore path, and restoring diversion
    without disturbing an existing mapping.
  • Mutation-tested: 13/13 on the driver, 8/9 on the codec (the one survivor is a
    group > 0 guard no 8-bit mask can distinguish, kept as documentation).
  • On hardware (MX Master 4 over Bolt): remap applied and confirmed by the
    physical button, persisted across a reload, restored to default, and the
    diverted/restore path exercised against Logi Options+.

Its predecessor PR #46 (MX Master 3S) closes by listing button remapping as
not yet implemented; this is that piece.

Adds feature 0x1B04 (REPROG CONTROLS V4): read the control table, point one
control at another, and hand diverted controls back to the hardware. Codec in
src/logitech/controls.ts, driver methods on LogitechHidppClient. Follow-up to
OpenMouse-Project#9, whose scope held button remapping back to stay reviewable.

This remaps control to control only. It does not implement Options+-style
actions like launching an app; Options+ does those by diverting the button and
handling it in its own software, which nothing here receives. So diversion is
exposed only to be cleared, never set.

The device decides what is legal: every control reports a group and a mask of
the groups it accepts, and the primary buttons report a mask of zero, so the
firmware refuses to move them rather than any rule here. They still appear as
targets for other controls, because any control in an accepted group is a legal
destination even when it is not a legal source.

Two decode details. The mapping flags are one 16-bit field split across two
non-adjacent bytes (low at payload[2], high at payload[5], with the remap
target between), so reading them as a contiguous pair mistakes a button
remapped to a high control id for a diverted one. And each mapping flag is a
value plus a valid bit one position higher, so a pure remap writes a zero flags
byte and leaves diversion untouched; clearing diversion sets only the valid
bits, so it can only ever turn diversion off.

A control merges getControlIdInfo (key flags) with getControlIdReporting (the
16-bit mapping field); those are named apart (flags vs mappingFlags) so neither
overwrites the other, and virtual/reprogrammable are exposed as booleans.

Verified on an MX Master 4 over a Logi Bolt receiver. Codec tests cover every
byte layout above; fake-HID driver tests cover read-back, refusal of illegal or
absent controls, the acknowledge-but-ignore path, and restoring diversion
without disturbing an existing mapping.
@snekxs
snekxs merged commit 89a130b into OpenMouse-Project:main Aug 14, 2026
1 check passed
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.

2 participants