Skip to content

feat(devices): add MX Ergo, LIFT, LIFT for Business and MX Anywhere 2S (beta, untested) - #180

Merged
mmaher88 merged 4 commits into
masterfrom
feat/devices-mx-ergo-lift-anywhere-2s
Nov 10, 2026
Merged

mmaher88 merged 4 commits into
masterfrom
feat/devices-mx-ergo-lift-anywhere-2s

Conversation

@mmaher88

@mmaher88 mmaher88 commented Nov 9, 2026 •

Copy link
Copy Markdown
Owner

Adds beta descriptors for three requested devices.

Refs #119 (MX Ergo), #144 (LIFT), #78 (MX Anywhere 2S). The issues aren't auto-closed, so they can stay open until someone confirms on hardware.

⚠️ None of these devices have been tested on hardware. All four descriptors are "status": "beta". Facts come from the Logi Options+ device catalogue (devices_initial.json): PIDs, programmable CIDs, and capabilities. CID names were cross-checked against Solaar's special_keys.py. Only facts were taken from Options+, no code or assets; the product images come from logitech.com (see Images below).

Devices

Descriptor PIDs Configurable controls (Options+ specialKeys.programmable) Hosts
mx-ergo 0xb01d (BT), 0x406f (Unifying) 0x52 middle, 0x53 back, 0x56 forward, 0x5B tilt left, 0x5D tilt right, 0xED precision mode 2
lift 0xb031 0x52, 0x53, 0x56, 0xFD DPI button 3
lift-for-business 0xb033 0x52, 0x53, 0x56, 0xFD DPI button 3
mx-anywhere-2s 0xb01a (BT), 0x406a (Unifying) 0x52, 0x53, 0x56, 0x5B tilt left, 0x5D tilt right 3

Controls are ordered by CID ascending from index 2, the same rule as optionsplus_extractor/canonicalize.sort_buttons. Every control defaults to "default", which means not diverted, so the firmware's own behaviour stays in place until the user remaps something:

  • Tilt keeps native horizontal scroll.
  • 0xED on MX Ergo keeps native precision-mode toggling.
  • 0xFD on LIFT keeps native pointer-speed switching. Options+'s default for this button is "change pointer speed". I didn't pick dpi-cycle, because that would divert the button by default on hardware nobody has tested.

Feature flags

The mapping is the same as scripts/optionsplus_extractor/capabilities.py:

  • battery ✅: hasBatteryStatus. LIFT also has unified_battery, so it should resolve to 0x1004; MX Ergo and MX Anywhere 2S have no unified-battery flag, so they should resolve to 0x1000. Either way the variant is picked at runtime by the capability table.
  • adjustableDpi ✅: Options+ pointerSpeed. pointerSpeed (0x2205) is left false, as the importer does.
  • hiResWheel ✅: scroll_wheel_capabilities.high_resolution.
  • reprogControls ✅: specialKeys.programmable.
  • smoothScroll ✅
  • smartShift ❌: none of the three has it.
  • thumbWheel ❌: no mouseThumbWheelOverride. LIFT's Options+ "virtual thumbwheel" (hold back/forward and scroll) is a software feature Logitune doesn't have.

DPI

Options+ gives no sensor range for these three, and Solaar has no static entries for them; it discovers them at runtime. DPI is therefore left to runtime discovery: DeviceSession reads min/max/step from AdjustableDPI (0x2201) at connect. The dpi block (200–4000, step 50) is the importer's own fallback (dpi_from_capabilities). It only matters for --simulate-all and for validating cycleRing, and none of these descriptors sets a ring.

Images, hotspots, Easy-Switch

Images are Logitech's own product gallery shots with transparent backgrounds, taken from logitech.com product pages. MX Anywhere 2S is end-of-life and its page is gone, so its shots come from Logitech's asset server, using the URLs on its old product page. Each image is cropped to the product and capped at 1200px tall. Hotspot and Easy-Switch coordinates are measured on those crops.

Descriptor front (device card) side (Buttons, Point & Scroll) back (Easy-Switch)
mx-ergo three-quarter view showing the ball top-down top-down: the channel lights are the "1"/"2" under the Easy-Switch button on top
lift top three-quarter same as front underside, slots on the three channel lights
lift-for-business top three-quarter same as front underside, from the Business gallery ("Lift for Business" on the battery door)
mx-anywhere-2s top-down three-quarter from the thumb side none: no underside shot exists
  • MX Anywhere 2S, middle button: the hotspot for 0x52 is the button behind the wheel. On this mouse, clicking the wheel switches scroll mode instead.
  • MX Anywhere 2S, Easy-Switch: there's no back image, so SideNav keeps the Easy-Switch tab hidden, which is existing behaviour. The three easySwitchSlots stay in the descriptor, so the tab appears if an underside image is added later.
  • Slot counts: match Options+ flow.hostCount, so MX Ergo has 2 channels.

I checked every Buttons, Point & Scroll and Easy-Switch page, plus the home carousel, in --simulate-all.

Schema and UI support

  • Trackball: the schema has no device-type field, and nothing in the app is mouse-specific in a way that matters here. The MX Ergo is a normal descriptor: DPI applies to the ball sensor, and the pointer scroll hotspot stands for the ball.
  • Tilt CIDs (0x5B/0x5D): these go through ReprogControlsV4 like any other button, so no app changes were needed.
  • Left-handed LIFT: Options+ lists it as derivative 2b031_ext1 on the same PID 0xb031, so it uses the same descriptor. Hotspot mirroring only becomes relevant once there are images.

Tests

  • DeviceSpec entries for all four descriptors in tests/test_device_registry.cpp.
  • UnifyingWpidsResolveToTheirDescriptor: 0x406f and 0x406a resolve. FindsByPid only covers the first PID.
  • EasySwitchSlotCountMatchesHostCount: 2 for MX Ergo, 3 for the others.
  • ProductImagesExistOnDisk: every referenced image file exists, and MX Anywhere 2S has no back image.
  • README devices table regenerated with scripts/generate-readme-devices.py.
  • All four descriptors validate against devices/schema.json.
  • Local runs: logitune-tests 838/838, and the pre-push hook ran the full suite. All descriptors load (strict) in --simulate-all with no JsonDevice warnings.

Needs hardware

Anyone with one of these devices (see the issues): run logitune and check the following.

  • Battery reads.
  • The DPI range in the log (DPI range:) matches the device.
  • Each button diverts when remapped and goes back to native when reset.
  • The callouts point at the right buttons, and the Easy-Switch highlight sits on the active channel light.

The tilt and precision/DPI buttons are the least certain.

🤖 Generated with Claude Code

Beta descriptors for three requested devices (#119, #144, #78), built from
the Logi Options+ device catalogue (PIDs, programmable CIDs, capabilities)
and cross-checked against Solaar's CID names. Untested on hardware.

- MX Ergo (0xb01d BT, 0x406f Unifying): middle, back, forward, tilt
  left/right (0x5B/0x5D), precision-mode button (0xED). 2 Easy-Switch hosts.
- LIFT (0xb031) and LIFT for Business (0xb033): middle, back, forward,
  DPI button (0xFD). 3 hosts.
- MX Anywhere 2S (0xb01a BT, 0x406a Unifying): middle, back, forward,
  tilt left/right. 3 hosts.

Feature flags follow scripts/optionsplus_extractor/capabilities.py:
battery, adjustableDpi (Options+ pointerSpeed), hiResWheel, reprogControls
and smoothScroll on; no SmartShift or thumb wheel. Every control keeps the
firmware's native action by default and is remappable.

No product images are available, so hotspots and Easy-Switch slots use
placeholder coordinates; Editor Mode can position them once images are
added. The dpi block is the importer fallback (200-4000/50) and only
affects --simulate-all; on hardware the range is read from 0x2201.

Tests: DeviceSpec entries for all four, plus checks that the Unifying
WPIDs resolve and that Easy-Switch slot counts match the host counts.
README devices table regenerated.
…s and MX Anywhere 2S

Use Logitech's product gallery shots, cropped to their opaque bounds and
capped at 1200px tall, and place hotspots and Easy-Switch slots on them
instead of the placeholder grid.

- MX Ergo: top-down shot for buttons and Easy-Switch (the channel lights
  sit under the Easy-Switch button on top), three-quarter shot for the
  device card.
- LIFT / LIFT for Business: top three-quarter shot for buttons, underside
  shot for Easy-Switch.
- MX Anywhere 2S: top-down and three-quarter shots. No underside shot is
  available, so there is no back image and the Easy-Switch tab stays
  hidden for it.

Adds a test that the referenced image files exist.
# Conflicts:
#	tests/test_device_registry.cpp
@mmaher88
mmaher88 merged commit c09ee7b into master Nov 10, 2026
6 checks passed
@mmaher88
mmaher88 deleted the feat/devices-mx-ergo-lift-anywhere-2s branch November 10, 2026 13:19
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