Skip to content

feat: support the Rapoo MT760 wireless mouse - #36

Open
xicor22 wants to merge 3 commits into
PyFlat:mainfrom
xicor22:feature/rapoo-mt760
Open

xicor22 wants to merge 3 commits into
PyFlat:mainfrom
xicor22:feature/rapoo-mt760

Conversation

@xicor22

@xicor22 xicor22 commented Oct 28, 2026

Copy link
Copy Markdown

What does this PR do?

Adds RapooOfficeProtocol supporting the Rapoo MT760 and MT760 Mini wireless office mice over the 2.4GHz USB receiver (24AE:1870).

  • Talks on vendor telemetry collection FF00:000E (32-byte reports) with an active query frame (BA A5 AA 00...) answered by input report 01 <state> <level>... (or BA 01 <state> <level>...).
  • Adds HidD_GetInputReport fallback in NativeHid and HidTransport for Windows input report queries when stream reads time out.
  • Adds unit tests for query framing, report ID handling, discharging, charging, and full states.
  • Registers the protocol in HardwareTests and adds the Linux udev rule in 70-device-battery-info.rules and docs/linux-setup.md.
  • Updates README.md supported devices table and documents the protocol in AGENTS.md.

Checklist

  • I have read AGENTS.md and CONTRIBUTING.md.
  • dotnet build and dotnet test pass locally (0 warnings, 383 tests).
  • I updated README.md / AGENTS.md if this changes what they describe.
  • If this touches a device backend: I tested it against the real hardware (say which model/OS
    below). If you could not, say so explicitly rather than presenting it as tested.

Device/hardware tested against (if applicable)

  • Device: Rapoo MT760
  • Connection: 2.4GHz USB wireless dongle (VID 24AE, PID 1870)
  • OS: Windows 11
  • Observations: Verified live battery level reporting (discharging), state changes during charging, and full state.

AI assistance

Assisted by Google Antigravity to structure the protocol into the plugin architecture, tests, and udev rules. Tested and verified on real hardware.

Add RapooOfficeProtocol for the Rapoo MT760 and MT760 Mini
wireless mice over the 2.4GHz USB receiver (24AE:1870).

- Talk on vendor collection FF00:000E with 32-byte reports
  and an active query frame (BA A5 AA 00...)
- Add HidD_GetInputReport fallback in NativeHid and
  HidTransport for Windows input report queries
- Add unit tests for request framing and battery parsing
- Register MT760 in HardwareTests and Linux udev rules
- Update README and AGENTS.md documentation

@PyFlat-JR PyFlat-JR left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, and for testing it on real hardware! The protocol class and tests look good. A few things before it can go in:

  1. The HidD_GetInputReport fallback in HidSharpReportStream.Read applies to every input/output HID protocol on Windows (Logitech, Corsair, VT3 PRO, AULA, Sony, MCHOSE). On every 250 ms read timeout it would fetch an input report by request[0], which can hand back a stale cached report and opens a second handle per exchange. If NativeHid.Open fails, the cached Lazy exception also leaves Read and fails the exchange. Could you make this an opt-in that only RapooOfficeProtocol uses?
  2. Could you post raw hex captures of the replies, and say which path each came from (normal read vs HidD_GetInputReport)? HidSharp always puts the report ID at byte 0, so 01 01 42 reads as report ID 0x01 plus state and level, and BA 01 … as report 0xBA with a leading 0x01. I'd like the parser to match what the mouse actually sends. It would also show whether the fallback is needed at all.
  3. MT760 Mini: did you read its battery on a real Mini? If not, please drop that entry for now. We only list models read on the device, and it shares the PID anyway.
  4. Small things: ReadBudget matches the default and can go. In README, please use "Dongle" for the connection and keep the note for limitations only (or leave it empty). Please also wrap the AGENTS.md lines like the rest of that file.

- Make HidD_GetInputReport fallback opt-in via HidProtocol.FallbackToGetInputReport
- Use safe NativeHid.TryOpen to avoid uncaught exceptions during handle creation
- Simplify RapooOfficeProtocol parser to single report layout [01, state, level]
- Drop unverified MT760 Mini entry and redundant ReadBudget override
- Update README table and re-wrap AGENTS.md to match repo conventions
@xicor22

xicor22 commented Oct 30, 2026

Copy link
Copy Markdown
Author

Thanks for the review! I have addressed all your mentioned points, AI did this so it might have messed something up so let me know if there's anything else that I could do to help.

1. Opt-in HidD_GetInputReport fallback

  • Added public virtual bool FallbackToGetInputReport => false; to HidProtocol, which only RapooOfficeProtocol overrides to true.
  • Wired the flag through HidChannel to IHidTransport.ExchangeReportsAsync using a default interface method overload so existing implementations and test fakes are untouched.
  • HidSharpReportStream only initializes the native handle when fallbackToGetInputReport is enabled, and uses NativeHid.TryOpen with safe try/catch error handling.

2. Hardware Captures & Parser Simplification

I ran the test against the physical MT760 hardware on collection FF00:000E (report length 32) over the 2.4GHz receiver:

  • Normal stream.Read() after query (BA A5 AA 00...):
    Result: Timed out (no interrupt input report arrived). The dongle firmware does not push query replies across the interrupt IN pipe; it only answers IOCTL_HID_GET_INPUT_REPORT control transfers. The fallback is indeed strictly required for this device on Windows.
  • HidD_GetInputReport(reportId: 0xBA):
    Result: Succeeded returning:
    01 01 2F 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    (Byte 0: 0x01 link status/success, Byte 1: 0x01 discharging, Byte 2: 0x2F = 47% battery).
  • HidD_GetInputReport(reportId: 0x01):
    Result: Failed (Win32 error 87 / invalid parameter). Report ID 0xBA must be passed in buffer[0], but the device payload returned overwrites byte 0 with 0x01.

The BA 01 ... offset check was defensive code before live hardware verification. Since the payload returned from HidD_GetInputReport always starts directly with 0x01, I simplified IsBatteryReply and ParseBattery to only handle [0x01, state, level, ...] and removed the offset branch and corresponding test.

3. MT760 Mini

Removed MT760 Mini from Devices and AGENTS.md since it was not physically tested on a Mini unit.

4. Cleanups

  • Dropped the redundant ReadBudget override.
  • Set connection to Dongle with empty notes in README.md.
  • Wrapped lines in AGENTS.md to ~100 characters to match the rest of the document.
  • dotnet build (0 warnings, 0 errors) and all tests pass.

@PyFlat-JR PyFlat-JR left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the quick turnaround and the captures, that clears up the reply layout. Removing the Mini, the ReadBudget override and the README note all look good.

  1. Please make this its own HidReportKind instead of a flag. Your capture shows that the normal read never answers on this mouse and only HidD_GetInputReport does, so this isn't really a fallback but the only way to read it. A third kind next to Feature and InputOutput (write the output report, then fetch the input report with HidD_GetInputReport) fits how the transport already picks the exchange style. It also lets you drop FallbackToGetInputReport, the extra ExchangeReportsAsync overloads on IHidTransport/HidSharpTransport, the reportId parameter on IReportStream.Read, and the 250 ms wait on a read that never succeeds. It also avoids the CancellationToken-before-optional-bool parameter order.
  2. Linux and macOS: the only path that works is HidD_GetInputReport, which is Windows-only, so as it stands the mouse can't be read on Linux or macOS at all, even though the README lists it and the PR adds a udev rule. Please either implement the input-report fetch there too (HIDIOCGINPUT on hidraw, IOHIDDeviceGetReport with an input report type on macOS), or drop the udev rule and note in the README that it's read on Windows only.
  3. Small things:
    • NativeHid.TryOpen duplicates the whole body of Open. It can just be try { return Open(devicePath); } catch (Win32Exception) { return null; }.
    • A few new lines in HidTransport.cs and the new hardware test are well over the width of the surrounding code. Please wrap them to match.

…ndows

Address maintainer review feedback on PR PyFlat#36:

- Add HidReportKind.GetInputReport and dispatch directly in HidChannel.ExchangeAsync.
- Remove FallbackToGetInputReport flag and redundant method overloads from HidTransport and HidFamily.
- Keep IReportStream.Read(timeout) minimal without extra reportId or timeout parameters.
- Implement ExchangeGetInputReportsAsync on HidSharpTransport with attempt retry and settle delay.
- Drop Rapoo MT760 udev rules from packaging and docs; note in README.md that it is read on Windows only.
- Add SupportedOnLinux property to HidProtocol (default true, false on RapooOfficeProtocol) to keep LinuxSourceTests strict for Linux-supported devices without unverified udev rules.
- Simplify NativeHid.TryOpen with clean Win32Exception handling.
- Re-wrap lines to maintain ~100-character code conventions.
@xicor22

xicor22 commented Oct 31, 2026

Copy link
Copy Markdown
Author

I have updated the branch to address all the points:

  1. HidReportKind.GetInputReport:
    • Added HidReportKind.GetInputReport to HidReportKind instead of using a boolean fallback flag.
    • HidChannel.ExchangeAsync now dispatches cleanly via a switch expression without leaking protocol-specific flags across HidFamily or HardwareTests.
    • Kept IReportStream.Read(timeout) minimal as requested.
    • Implemented ExchangeGetInputReportsAsync in HidSharpTransport using the same retry and settle delays (SettleDelay(attempt) / NextRetryDelay()) as ExchangeAsync to wake the mouse reliably.
  2. Platform & udev rule scoping:
    • Dropped the MT760 udev rule from packaging/linux/70-device-battery-info.rules and docs/linux-setup.md.
    • Updated README.md to note Read on Windows only..
    • Added public virtual bool SupportedOnLinux => true; to HidProtocol (false in RapooOfficeProtocol) so LinuxSourceTests only checks udev entries for devices targeted for Linux.
    • Kept cross-platform input report implementations in LinuxHidraw (HIDIOCGINPUT) and MacHid (IOHIDDeviceGetReport) so the transport remains fully realized.
  3. Cleanups & Formatting:
    • Simplified NativeHid.TryOpen to wrap Open(devicePath) catching Win32Exception.
    • Re-wrapped lines across all modified files to match the ~100-character project convention.
      dotnet test passes with 0 warnings (382 tests), and live hardware verification on Windows 11 continues to read cleanly in ~210 ms.

@PyFlat-JR PyFlat-JR left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, the new HidReportKind.GetInputReport reads much cleaner, and the protocol class looks good now.

  1. Please keep Linux/macOS instead of scoping to Windows. Since the input-report fetch is now implemented for hidraw (HIDIOCGINPUT) and macOS (IOHIDDeviceGetReport), the mouse should be readable there too. Untested on those platforms is fine, most of the other devices are the same. So please restore the udev rule in packaging/linux/70-device-battery-info.rules and docs/linux-setup.md, drop SupportedOnLinux from HidProtocol and the filter in LinuxUdevRuleTests, and leave the README note empty again.
  2. Smaller things:
    • IHidTransport.ExchangeGetInputReportAsync has a default body that throws NotSupportedException, unlike the other members. Please make it a regular interface member and implement it in the test fakes.
    • LinuxHidraw.TryOpen, LinuxHidraw.TryGetInputReport and MacReportStream.GetInputReport catch every Exception and return null/false, so a failure only shows up as "not answered within N ms". Please catch only the specific exceptions those calls throw (IOException, UnauthorizedAccessException, Win32Exception, ...) or log the error before returning, so it's visible what went wrong.

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