Skip to content

[Device Request]: Logitech G502 Lightspeed #21

Closed

Description

@Luckylucasv

Device

Logitech G502 Lightspeed

How does the plugin process reach it?

USB HID (wired or dongle)

Battery protocol details, if you know any

Hey, first thanks for the nice project!
I just had some tries to make the G502LS works since I thought it was using the same protocol as either the G Pro X Wireless or the G Pro X Superlight 2.

The G502 LS relies also on Voltages instead of direct %, so I took a similar approach as the Headset.
From there 2 options are possible, we do the interpolation math for each device in their own class or we only implement them in the base class to avoid duplication (that's what I did in the files shared here, logic stays in LogitechHidppProtocol.cs). Your choice here ^^

I didn't want to make a pull request directly since it was not all straigt foward + we might need some tweaking for the ramping in the % curve as I only used few points as reference (Compared the results from the plugin with Logi GHub).

G502-Lightspeed-Changes.zip


To make it work, I had to had 2 files and modify 6 existing ones, here are all the changes :

New:
src/DeviceBatteryInfo/Sources/Logitech/LogitechVoltageBatteryProtocol.cs >> the G502 protocol
tests/DeviceBatteryInfo.Tests/LogitechVoltageBatteryProtocolTests.cs >> its unit tests

Modifications :
src/DeviceBatteryInfo/Sources/Logitech/LogitechHidppProtocol.cs - add PercentFromMillivolts at the end of the class (before the closing }):

    // after ThrowOnError, add:
    internal static int PercentFromMillivolts(
        int millivolts,
        ReadOnlySpan<(int Millivolts, int Percent)> curve
    )
    { /* the interpolation loop */ }

src/DeviceBatteryInfo/Sources/Logitech/LogitechHeadsetProtocol.cs - replace the 20-line PercentFromMillivolts method with a one-liner:

    internal static int PercentFromMillivolts(int millivolts) =>
        LogitechHidppProtocol.PercentFromMillivolts(millivolts, Curve);

tests/DeviceBatteryInfo.Tests/HardwareTests.cs - add one line to the Protocols array:
new LogitechVoltageBatteryProtocol(),

README.md - add one row to the "Specific devices" table:
| Logitech | G502 Lightspeed | Mouse | Receiver or cable | approx. | yes | Reports a voltage, so the percentage is an estimate.

packaging/linux/70-device-battery-info.rules - add two lines after the Superlight 2 entries:

# Logitech G502 Lightspeed
SUBSYSTEM=="hidraw", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c539", TAG+="uaccess"
SUBSYSTEM=="hidraw", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c08d", TAG+="uaccess"

docs/linux-setup.md - add the same three lines to the inline printf block, after the Superlight 2 lines.

Contribution

  • I own this device and am willing to help test a backend for it.
  • I'd like to implement this myself and am opening this issue to track it.

Activity

  1. PyFlat-JR commented on Oct 9, 2026

    @PyFlat-JR
    Member

    Hey, thanks a lot for this, and for the recorded frames! They made it much easier.

    I've put your changes on the branch feature/g502-lightspeed. Most of it is as you wrote it, including moving PercentFromMillivolts into LogitechHidppProtocol. I changed two things after comparing with the Linux kernel driver (hid-logitech-hidpp.c), Solaar and LGSTrayBattery:

    1. Flag bits: bit 5 of the 0x1001 flags byte means critical level, not "charge complete". Bits 0–2 only mean something while bit 7 (external power) is set: 0 charging, 1 full, 2 not charging, anything else is an error. The status now follows that.
    2. Voltage curve: I replaced your fitted curve with the Linux kernel's 0x1001 table, at the points Solaar samples. Your reference point (4133 mV ≈ 97% in G Hub) falls exactly on that table, but in the middle range your curve and the kernel's differ by up to ~45% (3900 mV: ~22% vs ~67%). G Hub uses its own per-device table, so a gap of a few percent is still expected.

    Could you test the branch thoroughly on your G502? These are the things I'm most interested in:

    • Receiver and cable: does it read correctly both through the receiver (C539) and on the cable (C08D)? dotnet test --filter Category=Hardware in tests/DeviceBatteryInfo.Tests shows the raw reads.
    • Compared with G Hub: how close are the percentages across the range? A few readings at different levels (full, ~50%, low) would be great, ideally with the mV value.
    • Charging: does it switch to charging when you plug in the cable, and to full when it's done? Does the percentage jump up while charging? The voltage reads higher under charge, so that would be expected.
    • Sleep and off: when the mouse sleeps or is switched off, does the plugin keep the last value, rather than dropping to 0% or showing an error?
    • Both connected: with the receiver plugged in and the mouse on the cable, does it appear once or twice?
    • Inside Macro Deck: do the variables, the low-battery and charging events, and the widget behave over a longer session?

    If anything looks wrong, a log or the raw frames would help a lot. Thanks again!

  2. Luckylucasv commented on Oct 10, 2026

    @Luckylucasv
    Author

    Hey,
    Sure no worries, I'm a bit rusty so I figured adding some details might help :)

    Receiver and Cable: Both are working,
    Receiver (C539) - wireless - C539 - 96% Discharging - Pass
    Cable (C08D) -wired - C08D - 100% Charging - Pass

    GHub comparaison: I am currently trying to test at multiple ranges but it takes ages to drop down, for now on really higher percentages (90-100 in Ghub), I have around 2-4% difference (GHub shows lower values as the plugin). The weird this is that after playing a bit, the battery dropped to 72% while the plugin remains at 97%. I need to let my battery drain and test further to find out exactly how its behaving and maybe find out a better discharge curve.

    Charging : When connecting the mouse via cable, it does show charging in the plugin and the % almost instantly jumps to 100, when switching back to dongle, the value goes back to normal. As we are reading voltages, this is Ok for now. I'm still trying to find out how GHub is available to estimate the charge when charging, my guess would be that they are using a charging curve in addition to the discharge one.
    When I charge the mouse as it's turned off, "No signals" shows up (looks like a normal behavior as GHub looks also not able to communicate with it at all).

    Sleep and Off : When unplugged completly >> The plugin shows No Signal and --- (greyed out). When the mouse is turned off, we see the old percentage greyed out with no signal after few seconds.

    Both Connected: The mouse appears once and reads correctly. With both connected, the plugin probes both interfaces, the receiver doesn't answer (because the mouse is on the cable), so it falls through to the cable path (C08D) and reads from there 100% Charging.

    In MD3: The variables and widget seems to work fine for now after 1-2hrs, no big gaps or jumps. I will give feedback when I'll reach the Low-Battery phase to see what shows up.


    I think the only thing left to verify are the values for discharge with your table and compare with GHub.
    I will try once fully discharged, to get multiple reference points to maybe build a better curve while also building in parralel a charging curve, seems to be the most efficient her :).
    Then we will have the flexibility after either to keep your table and accept the small gaps or completly use an other one that will also have some gaps ^^

  3. Luckylucasv commented on Oct 10, 2026

    @Luckylucasv
    Author

    Also instead of extrapolating from the Kernel values each 10%, I have made some tries with a complete 100value table to reduce the gap that I had in the mid range since it doesn't look completly linear. Again this is not really a clean solution as I had to extrapolate myself but it might help to solve some inconsistencies.

    Before:
    private static readonly (int Millivolts, int Percent)[] Curve = [ (4186, 100), (4067, 90), (3989, 80), (3922, 70), (3859, 60), (3811, 50), (3778, 40), (3751, 30), (3717, 20), (3671, 10), (3646, 5), (3579, 2), (3500, 0), ];

    After:
    private static readonly int[] VoltageTable = [ 4186, 4156, 4143, 4133, 4122, 4113, 4103, 4094, 4086, 4075, 4067, 4059, 4051, 4043, 4035, 4027, 4019, 4011, 4003, 3997, 3989, 3983, 3976, 3969, 3961, 3955, 3949, 3942, 3935, 3929, 3922, 3916, 3909, 3902, 3896, 3890, 3883, 3877, 3870, 3865, 3859, 3853, 3848, 3842, 3837, 3833, 3828, 3824, 3819, 3815, 3811, 3808, 3804, 3800, 3797, 3793, 3790, 3787, 3784, 3781, 3778, 3775, 3772, 3770, 3767, 3764, 3762, 3759, 3757, 3754, 3751, 3748, 3744, 3741, 3737, 3734, 3730, 3726, 3724, 3720, 3717, 3714, 3710, 3706, 3702, 3697, 3693, 3688, 3683, 3677, 3671, 3666, 3662, 3658, 3654, 3646, 3633, 3612, 3579, 3537, ];

  4. PyFlat-JR commented on Oct 10, 2026

    @PyFlat-JR
    Member

    Thanks for the detailed test run, that's really helpful!

    Full table: good call. I've switched to the kernel's full 100-entry table (your "After" table matches the kernel's hidpp20_map_battery_capacity exactly), still interpolating linearly between entries. It now hits your G Hub reference point exactly (4133 mV → 97%). One caveat: the full table and the old 13 points differ by at most 2% anywhere between 3500 and 4200 mV, so it can't explain the 72% (G Hub) vs 97% (plugin) gap you saw. Both curves give about the same number there. So either G Hub isn't going by voltage alone, or the plugin was showing an old value at that moment.

    To find out, I added a hardware test that prints the raw voltage:

    dotnet test --filter "Category=Hardware&Name=Reads_the_raw_logitech_battery_voltage" --logger "console;verbosity=detailed"
    

    from tests/DeviceBatteryInfo.Tests. It prints a line like HH:mm:ss 4133 mV flags 0x00 = 97% Discharging. Close G Hub before running it, in case it holds the interface.

    What would help most while the battery drains:

    • A few pairs of plugin mV and G Hub % at different levels, especially when the two disagree a lot (like the 72 vs 97 case).
    • Whether the plugin's percentage moves at all while G Hub drops. If the mV stays high while G Hub falls, G Hub is using something other than voltage. If the plugin's value is stuck while the mV moves, that's a bug on our side.

    Charging: the jump to ~100% while charging is because the voltage is higher on the cable. We'll leave it like this for now and decide once we have your data.

  5. PyFlat-JR commented on Oct 15, 2026

    @PyFlat-JR
    Member

    Quick note: I rebased feature/g502-lightspeed onto the current main, so a plain git pull will fail. To update, run git fetch and then git reset --hard origin/feature/g502-lightspeed.

  6. PyFlat-JR commented on Oct 16, 2026

    @PyFlat-JR
    Member

    Thanks again for the code and for all the testing! The G502 Lightspeed is merged in #30.

    Two things changed since you last tested: the curve is now the kernel's full 100-entry table (your "After" table), and while the mouse charges the plugin now shows only "charging" without a percentage, because the voltage reads too high on the cable (the jump to ~100% you saw).

    It will be in the next release, which should come soon. If you notice anything off until then, just tell me.

  7. Luckylucasv commented on Oct 16, 2026

    @Luckylucasv
    Author

    Hey man,
    Just received the Merge infos and wanted to give you a small update.

    Btw I had a local build runing all that time with the complete 100 Table on my end so it's corresponding exactly to the merge that wou did.

    I have been testing a bit in the last days and so far I could only notice a maximal gap between GHub and the plugin of 1-3%. So overall I would say that this is quite an acceptable range for me. (Even by playing all week I only dropped to 72% battery, so still far from the low battery status). Also I litteraly cannot explain the massive gap from last time and to be honnest, I would even say here that GHub was in the wrong here as I'm only reaching the 72% mark today on both the plugin and GHub!

    Let's say this closes the whole integration of it, if I find anything else in the meantime, I'll just write you up so that we can figure it out.
    Thanks again, was a pleasure, and have a good one !

  8. PyFlat-JR commented on Oct 16, 2026

    @PyFlat-JR
    Member

    Great, thanks for testing it over the whole week and for the code in the first place! If anything comes up, just write here or open a new issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions