Repository navigation
zfeat: add Corsair, Rapoo, AULA and Sony HID battery backends - #6
Conversation
PyFlat-JR
left a comment
There was a problem hiding this comment.
Thanks a lot for this, it's a really solid PR! The protocols are documented carefully, the frames were captured on real hardware, an off device counts as "no reading" instead of 0%, and the docs and tests were updated along with the code. 🙏
The merge of main (23ca5ff) dropped two things from #5 (macOS support), and one of them stops the build:
- The tests don't build.
HardwareTests.cslostusing DeviceBatteryInfo.Sources.SystemBattery;, soISystemPowerReader,MacSystemPowerReaderandWindowsSystemPowerReaderno longer resolve (3 errors). With that line back, the PR merged intomainbuilds and all tests pass. ExchangeReportsAsyncwent back toGetMaxOutputReportLength()and droppedOutputReportLength(device, request), which breaks report-based devices on macOS (details inline).
Also, please make GetFeatureAsync go through _openFeatureChannel (inline) so it works on macOS too.
| using DeviceBatteryInfo.Sources.Rapoo; | ||
| using DeviceBatteryInfo.Sources.Razer; | ||
| using DeviceBatteryInfo.Sources.SystemBattery; | ||
| using DeviceBatteryInfo.Sources.Sony; |
There was a problem hiding this comment.
The merge removed using DeviceBatteryInfo.Sources.SystemBattery; (it was added in #5). HardwareTests still uses ISystemPowerReader, MacSystemPowerReader and WindowsSystemPowerReader, so the test project no longer builds. Please add it back next to the other usings.
| // An empty request only listens, for a device that pushes its state on its own (Rapoo). | ||
| if (request.Length > 0) | ||
| { | ||
| var output = new byte[Math.Max(device.GetMaxOutputReportLength(), request.Length)]; |
There was a problem hiding this comment.
This reverts a fix from #5. Off Windows, one HidSharp device carries every collection, so GetMaxOutputReportLength() is the longest report on the whole device, not the length of the report being sent. The device then ignores the padded report (see the comment on OutputReportLength below). Keeping the listen-only branch and using the helper again should cover both:
| var output = new byte[Math.Max(device.GetMaxOutputReportLength(), request.Length)]; | |
| var output = new byte[Math.Max(OutputReportLength(device, request), request.Length)]; |
| public Task<byte[]> GetFeatureAsync( | ||
| string devicePath, | ||
| byte reportId, | ||
| CancellationToken cancellationToken | ||
| ) | ||
| { | ||
| if (!OperatingSystem.IsWindows()) | ||
| { | ||
| throw new PlatformNotSupportedException("HID feature reports are Windows-only in v1."); | ||
| } | ||
|
|
||
| var device = | ||
| DeviceList | ||
| .Local.GetHidDevices() | ||
| .FirstOrDefault(d => | ||
| string.Equals(d.DevicePath, devicePath, StringComparison.OrdinalIgnoreCase) | ||
| ) ?? throw new InvalidOperationException($"HID device {devicePath} is not present."); | ||
|
|
||
| cancellationToken.ThrowIfCancellationRequested(); | ||
| var report = new byte[device.GetMaxFeatureReportLength()]; | ||
| report[0] = reportId; | ||
| using var handle = NativeHid.Open(devicePath); |
There was a problem hiding this comment.
Since #5, the transport opens feature reports through _openFeatureChannel: NativeFeatureChannel on Windows, HidSharpFeatureChannel elsewhere. Going through it here instead of NativeHid.Open would drop the "Windows-only in v1" exception, so the DualSense's Bluetooth switch also works on macOS. It would also let tests inject a fake channel. Something like:
var report = new byte[device.GetMaxFeatureReportLength()];
report[0] = reportId;
using var channel = _openFeatureChannel(devicePath);
channel.Get(report);
return Task.FromResult(report);(I haven't tested the DualSense on macOS, so if HidSharp's feature path doesn't work there, a comment saying so is fine too.)
|
Thank you! 🙌🏻 |
Add four HID device backends, each read on real hardware: - Corsair VOID PRO Wireless (1B1C:0A75): output report C9 64 on FFC5:0001, level in byte 2 (mic-up bit 0x80 masked), state in byte 4. - Rapoo VT3 PRO (24AE:1215 dongle, 4415 cable): listens to the status report the mouse pushes on FF00:0002; level 0x7F while charging reads as charging with no percent, charging at 100 reads as full. - AULA F75 on the Compx receiver (3554:FA09): command 0x4A on FF02:0002 with a byte-sum checksum; on the cable the level is pinned at 100, so it reads as charging with no percent. Wired mode (258A:010C) is not read. - Sony DualSense (054C:0CE6) over USB and Bluetooth: listens to the input report; a fresh Bluetooth link only sends the short report, so feature 0x05 is read once to switch it to the full 0x31 report. Shared HID changes needed by these devices: - ExchangeReportsAsync only listens when the request is empty. - HidProtocol.ReadBudget lets a protocol extend the read and probe budgets (Rapoo pushes every ~3.2 s, longer than the 400 ms probe). - IHidTransport.GetFeatureAsync reads a feature report without writing one first, unlike the feature-report ExchangeAsync. Known limits: on the cable the Rapoo pushes irregularly and its reading may go stale; the DualSense level comes in 10% steps. The 8BitDo Ultimate (2DC8:3106) is not supported: its dongle presents as a wired XInput pad and exposes no battery. Tests: per-brand parser tests built from captured frames, a fake-transport test for the DualSense Bluetooth switch and for ReadBudget, and the new protocols added to the hardware tests. README, AGENTS.md and docs/adding-a-device.md updated. Bump version to 1.4.1.
5fd7509 to
acbc54c
Compare
|
I updated my branch with the latest version of the main branch and fixed the conflict. Could you review it again? And could a Mac user test it as well, just to be sure? xD (I confess I don't know if I did it right, but it seems to have worked out well) |
What does this PR do?
Adds four HID battery backends under "Other devices", plus three small changes to the shared HID plumbing that two of them need.
New devices
1B1C:0A75,Sources/Corsair/): output reportC9 64onFFC5:0001; level in byte 2 (the mic-up bit0x80is masked off), state in byte 4. A headset that is off reports state 0 and is no reading, not 0%.24AE:1215dongle,24AE:4415cable,Sources/Rapoo/): the mouse pushes a status report onFF00:0002by itself, so the read only listens. Byte 5 is the state (1 battery, 2 charging), byte 6 the level. While charging the firmware sends0x7F, which reads as Charging with no percent. Charging at 100 reads as Full.3554:FA09,Sources/Aula/): command0x4AonFF02:0002, 20-byte frames with a byte-sum checksum; level in byte 5, state in byte 6. With the cable in, the level is pinned at 100 (a 97% keyboard reads 100), so it reads as Charging with no percent.054C:0CE6,Sources/Sony/): listens to the input report. The status byte is byte 53 over USB and byte 54 over Bluetooth (0x31). A fresh Bluetooth link sends only the short0x01report with no battery, so feature0x05is read once to switch it to0x31until it disconnects.Shared HID changes
HidSharpTransport.ExchangeReportsAsynconly listens when the request is empty (Rapoo, DualSense).HidProtocol.ReadBudget(virtual, defaultnull) lets a protocol extend the read and probe budgets. Rapoo pushes every ~3.2 s, longer than the 400 ms probe and the 2 s read.HidFamilypasses it through.IHidTransport.GetFeatureAsync/HidChannel.GetFeatureAsyncread one feature report without writing one first.ExchangeAsyncalways doesSetFeaturebeforeGetFeature, and writing0x05to the DualSense is something no reference implementation does. The interface method has a default implementation, so the existing fakes are untouched.Known limits (documented in README / AGENTS.md)
258A:010C) with an unknown protocol and is not read.0DF2) is not included (not tested).2DC8:3106) is deliberately not supported: its dongle presents as a wired XInput pad (WIRED/FULL) and has no vendor collection.Tests
CorsairProtocolTests,RapooProtocolTests,AulaProtocolTests,SonyProtocolTests.0x05→ full report), andReadBudgetreaching both the probe and the reads (HidProtocolTests).HardwareTests.Protocols.Docs: README "Supported devices" table, AGENTS.md (orientation + one section per protocol),
docs/adding-a-device.md(listen-only reads,ReadBudget,GetFeatureAsync, per-brand test files). Version bumped to 1.4.1.Checklist
dotnet buildanddotnet testpass locally (0 warnings, 146 tests).Device/hardware tested against (if applicable)
Windows 11 Pro 10.0.26100. All four were read through the plugin with
HardwareTests, and the raw frames were captured to build the tests.0A751215/44150x7Fafter a firmware update, full on cable3554:FA090CE60x05, verified twice by reconnecting the controllerNot verified: Rapoo / DualSense low-battery states, and the DualSense's error charge states (handled as no reading).
AI assistance
A significant part of this PR was written with Claude Code: the protocol code, the tests and the docs. The protocols for Corsair and AULA come from public sources (HeadsetControl, a Node tray app, battery-hub, womier-l65-linux). Corsair Void, Rapoo's layout and the AULA cable / DualSense Bluetooth behaviour were mapped from frames captured on my own devices. Every reading above was run on the real hardware, and I reviewed the diff.