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:
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:
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!
Hi,
I would like to provide the information needed to add support for the Logitech G325 LIGHTSPEED.
Device information
lsusb:The device exposes the following usage pages on interface 3:
Existing HeadsetControl drivers tested
I first tested the existing Logitech G522 LIGHTSPEED implementation by temporarily adding PID
0x0b21to 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 0x23protocol family as the G522, but with different battery request/response identifiers.Working G325 battery request
The existing G522 battery request is:
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:
This is a 64-byte packet, zero-padded.
Response format
The G325 first replies with an ACK:
and then with the battery status packet:
Observed fields:
Charging states observed:
Verified examples
Not charging
Decoded:
Charging
Decoded:
Fully charged
Decoded:
Earlier charging samples also showed:
This appears consistent with:
Cross-check with Logitech G HUB
The battery percentage was cross-checked against Logitech G HUB.
Example:
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:
G325:
Battery response detection
The G522 implementation currently expects a battery response using:
The G325 battery response uses:
Battery parsing
For the G325:
A more defensive version could explicitly handle
0x00and0x02.Possible device class
Something along these lines should probably work:
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:
and parses:
Example output:
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!