Skip to content

Add support for Logitech G325 LIGHTSPEED (046d:0b21) #581

Description

@Platerios

Hi,

I would like to provide the information needed to add support for the Logitech G325 LIGHTSPEED.

Device information

Product: Logitech G325 LIGHTSPEED - Wireless Mode
USB VID:PID: 046d:0b21
Interface: 3
Usage Page: 0xffa0
Usage ID: 0x0001
Packet size: 64 bytes

lsusb:

Bus 001 Device 007: ID 046d:0b21 Logitech, Inc. G325 LIGHTSPEED - Wireless Mode

The device exposes the following usage pages on interface 3:

Usage-Page: 0x000c  Usage-ID: 0x0001
Usage-Page: 0xff13  Usage-ID: 0x0001
Usage-Page: 0xffa0  Usage-ID: 0x0001

Existing HeadsetControl drivers tested

I first tested the existing Logitech G522 LIGHTSPEED implementation by temporarily adding PID 0x0b21 to its supported product IDs.

The G325 was detected successfully, but battery reading timed out.

I also tested the Logitech G PRO X 2 LIGHTSPEED / Centurion implementation by adding PID 0x0b21.

The Centurion request timed out as well.

This suggests that the G325 uses the same general 0x50 0x23 protocol family as the G522, but with different battery request/response identifiers.

Working G325 battery request

The existing G522 battery request is:

50 23 0b 00 03 1a 00 03 00 05 0a 00 00 ...

The G325 acknowledges this request, but does not return the expected G522 battery response.

After testing a small set of closely related read/status requests, the following request reliably returns the G325 battery state:

50 23 0b 00 03 1a 00 03 00 07 0a 00 00 ...

This is a 64-byte packet, zero-padded.

Response format

The G325 first replies with an ACK:

50 23 03 00 03 1a 00 00 ...

and then with the battery status packet:

50 23 0b 00 03 10 00 06 00 07 0a XX YY ZZ ...

Observed fields:

byte 11 = internal/raw battery value
byte 12 = displayed battery percentage
byte 13 = charging state

Charging states observed:

0x00 = not charging
0x02 = charging

Verified examples

Not charging

50 23 0b 00 03 10 00 06 00 07 0a 5a 5a 00 ...

Decoded:

internal/raw: 90
battery:      90%
charging:     no

Charging

50 23 0b 00 03 10 00 06 00 07 0a 59 5a 02 ...

Decoded:

internal/raw: 89
battery:      90%
charging:     yes

Fully charged

50 23 0b 00 03 10 00 06 00 07 0a 64 64 02 ...

Decoded:

internal/raw: 100
battery:      100%
charging:     yes

Earlier charging samples also showed:

... 46 46 02 ...
... 47 46 02 ...
... 5e 5a 02 ...
... 5f 5a 02 ...

This appears consistent with:

  • byte 11 = more fine-grained/internal value
  • byte 12 = displayed battery percentage, apparently rounded/stepped

Cross-check with Logitech G HUB

The battery percentage was cross-checked against Logitech G HUB.

Example:

G325 packet:
byte 12 = 0x5a = 90%

Logitech G HUB:
90%

So byte 12 appears to be the correct user-facing battery percentage.

Suggested implementation

The G325 appears close enough to the G522 implementation that a dedicated variant could likely reuse most of the existing logic.

Relevant differences:

Battery request

G522:

request[9]  = 0x05;
request[10] = 0x0a;

G325:

request[9]  = 0x07;
request[10] = 0x0a;

Battery response detection

The G522 implementation currently expects a battery response using:

packet[9] == 0x05

The G325 battery response uses:

packet[9]  == 0x07;
packet[10] == 0x0a;

Battery parsing

For the G325:

int raw_value = packet[11];
int battery_percent = packet[12];

auto status =
    packet[13] == 0x02
        ? BATTERY_CHARGING
        : BATTERY_AVAILABLE;

A more defensive version could explicitly handle 0x00 and 0x02.

Possible device class

Something along these lines should probably work:

class LogitechG325Lightspeed : public HIDDevice {
public:
    static constexpr std::array<uint16_t, 1> SUPPORTED_PRODUCT_IDS {
        0x0b21
    };

    static constexpr size_t PACKET_SIZE = 64;
    static constexpr std::array<uint8_t, 2> REPORT_PREFIX {
        0x50, 0x23
    };

    constexpr uint16_t getVendorId() const override
    {
        return VENDOR_LOGITECH;
    }

    std::vector<uint16_t> getProductIds() const override
    {
        return {
            SUPPORTED_PRODUCT_IDS.begin(),
            SUPPORTED_PRODUCT_IDS.end()
        };
    }

    std::string_view getDeviceName() const override
    {
        return "Logitech G325 LIGHTSPEED"sv;
    }

    constexpr int getCapabilities() const override
    {
        return B(CAP_BATTERY_STATUS);
    }

    DeviceConnectionInfo getDeviceConnectionInfo() const override
    {
        return {
            .usagepage = 0xffa0,
            .usageid = 0x0001,
            .interface_id = 3
        };
    }

    Result<BatteryResult> getBattery(hid_device* device_handle) override
    {
        std::array<uint8_t, PACKET_SIZE> request {};

        request[0]  = 0x50;
        request[1]  = 0x23;
        request[2]  = 0x0b;
        request[4]  = 0x03;
        request[5]  = 0x1a;
        request[7]  = 0x03;
        request[9]  = 0x07;
        request[10] = 0x0a;

        // write request
        // wait for ACK / battery response
        // battery response:
        //
        // 50 23 0b 00 03 10 00 06 00 07 0a RAW PERCENT STATUS ...
        //
        // packet[12] = displayed percentage
        // packet[13] = 0x02 charging, 0x00 not charging
    }
};

This is only a rough suggestion; reusing/refactoring the existing G522 implementation may be cleaner.

Linux userspace proof of concept

I currently have a small Linux userspace script that successfully reads the G325 battery percentage and charging state directly through /dev/hidraw.

It performs only this request:

50 23 0b 00 03 1a 00 03 00 07 0a ...

and parses:

packet[12] = battery percentage
packet[13] = charging state

Example output:

100% (charging)

JSON output:

{
  "device": "Logitech G325 LIGHTSPEED",
  "percent": 100,
  "charging": true,
  "status_raw": 2,
  "internal_value": 100
}

I can provide the script, additional raw packet logs, or test patches if useful.

Thanks!

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions