Skip to content

Add Logitech Pebble M350s support - #39

Closed
CharlieNR wants to merge 1 commit into
OpenMouse-Project:devfrom
CharlieNR:pebble-m350s
Closed

Add Logitech Pebble M350s support#39
CharlieNR wants to merge 1 commit into
OpenMouse-Project:devfrom
CharlieNR:pebble-m350s

Conversation

@CharlieNR

Copy link
Copy Markdown

Adds the Logitech Pebble M350s (046d:b036), which pairs over Bluetooth rather
than through a receiver.

Why it did not work

The Logitech driver assumed a USB vendor interface throughout. Over Bluetooth
the same HID++ protocol behaves differently in three ways:

  • the control interface moves from usagePage 0xff00 / usage 0x0001 to
    usagePage 0xff43, so the mouse was never offered by the WebHID picker;
  • only the long report (0x11) exists, so request() sending a 0x10 short
    report would have been rejected even if the mouse had been reachable;
  • it answers on device index 0xFF rather than a receiver slot.

On top of that, readStatus() and getDpiOptions() threw when a mouse exposed
no DPI or report-rate feature. An office mouse exposes neither, so the read
failed on its behalf rather than reporting what the mouse could actually do.

Changes

  • protocol.ts: LOGITECH_BLUETOOTH_PRODUCT_IDS and isBluetoothProduct(),
    kept separate from LOGITECH_DIRECT_PRODUCT_IDS so a Bluetooth mouse routes
    to index 0xFF without inheriting the G402's read-only polling rate or its
    "close Logitech Gaming Software" timeout message. Adds
    decodeBatteryLevelStatus() for HID++ 0x1000.
  • hidpp.ts: recognises the Bluetooth control interface, sends every request as
    a long report on that transport, reads Battery Level Status (0x1000) when
    the rechargeable-pack features are absent, and treats a missing DPI or
    report-rate feature as "not present" instead of an error.
  • vendors.ts: LOGITECH_BLUETOOTH_FILTERS, so the picker offers the mouse.
  • control.ts: hides the polling card when a driver reports an explicitly empty
    rate list, mirroring how an empty supportedLiftOffDistances already hides
    the sensor card. Without this a mouse with DPI but no report rate showed an
    empty card and a 0 Hz live-status line.

Battery Level Status is the feature Logitech's AA/AAA-powered mice expose. It
returns a coarse level rather than a continuous reading, and 0 means
"cannot measure" rather than "empty" — hence the explicit null.

Testing

npm run check and npm run size pass (98 tests). New unit tests cover the
Bluetooth device-index routing, the separation from the direct-connect path, and
the 0x1000 decoding.

Behaviour was exercised against a scripted WebHID device standing in for the
mouse, in three configurations:

  • name + firmware + 0x1000 only — connects, shows battery, firmware and
    Wireless / Bluetooth, hides the whole settings grid, and sends every
    request as a 19-byte 0x11 report;
  • the same plus 0x2201 — DPI card appears on its own with the advertised
    values, polling and sensor cards stay hidden;
  • a c54d receiver with 0x2202 / 0x8061 / 0x1004 — unchanged: DPI
    presets, all seven rates, lift-off buttons, and short 0x10 reports.

Not yet confirmed on hardware

I have the mouse but cannot complete a hardware pass yet: macOS gates HID access
behind Input Monitoring, which I have not been able to grant on this machine.
The transport details above are decoded from the mouse's own HID report
descriptor, and the expectation that it exposes no DPI or report-rate feature is
inference from it being an office mouse, not a capture.

src/devices/logitech/TESTING.md records this and lists what to check, so the
feature indexes it really answers with can be recorded once someone runs it.
Happy to hold this open until I can post that capture.

The Logitech driver only reached mice through a USB vendor interface. Over
Bluetooth the same HID++ protocol moves to usage page 0xff43 and drops the
0x10 short report, so a Bluetooth mouse was never offered by the picker and
would not have answered if it had been.
- Detect the Bluetooth control interface and address it on device index 0xFF,
kept separate from the direct-connect USB list so the Pebble does not inherit
the G402's read-only polling rate.
- Send every request as a long report when the transport has no short report.
- Read Battery Level Status (0x1000), which is what mice on a replaceable cell
expose instead of the rechargeable-pack features.
- Treat a missing DPI or report-rate feature as "not present" rather than an
error. An office mouse has neither, and the read used to fail on its behalf.
The shell now hides the polling card when a driver reports an explicitly empty
rate list, matching how an empty lift-off list already hides the sensor card.
@snekxs

Copy link
Copy Markdown
Member

Closing because this branch conflicts with the current Logitech implementation on dev. Please rebase and resubmit the Pebble M350s support as a focused change with the current HID++ tests passing.

@snekxssnekxs closed this Aug 8, 2026
dwei30 pushed a commit to dwei30/openmouse that referenced this pull request Aug 30, 2026
Picks up Keychron M6 (OpenMouse-Project#33), Nape Pro (OpenMouse-Project#37), Glorious Model O 2/I 2 (OpenMouse-Project#38),
and SteelSeries Rival 3 (OpenMouse-Project#39) support from mouse-protocol main.
The bump retires the generic KeychronHidClient in favor of
KeychronNapeHidClient (a strict superset), which this app's controller.ts
still imported by its old path. Also widens the Glorious lighting call site
since SupportedClient now includes GloriousHidClient's stricter setLighting
signature.
Verified with npm ci, npm run build, and the full test suite (84/84).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for freeto 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

@CharlieNR@snekxs