From aad1da25ad11609ddf97702a2596f107c58be975 Mon Sep 17 00:00:00 2001 From: Simon Schindler Date: Mon, 16 Mar 2026 23:44:27 +0100 Subject: [PATCH 1/4] implementation for Logitech G522 LIGHTSPEED --- lib/device_registry.cpp | 2 + lib/devices/logitech_g522_lightspeed.hpp | 249 +++++++++++++++++++++++ 2 files changed, 251 insertions(+) create mode 100644 lib/devices/logitech_g522_lightspeed.hpp diff --git a/lib/device_registry.cpp b/lib/device_registry.cpp index f680e5f..18fdbc7 100644 --- a/lib/device_registry.cpp +++ b/lib/device_registry.cpp @@ -6,6 +6,7 @@ // Logitech devices with HIDPPDevice protocol template #include "devices/logitech_g432.hpp" +#include "devices/logitech_g522_lightspeed.hpp" #include "devices/logitech_g533.hpp" #include "devices/logitech_g535.hpp" #include "devices/logitech_g633_g933_935.hpp" @@ -85,6 +86,7 @@ void DeviceRegistry::initialize() // Each device is managed by a unique_ptr for automatic cleanup // Logitech devices (using HIDPPDevice protocol template) + registerDevice(std::make_unique()); registerDevice(std::make_unique()); registerDevice(std::make_unique()); registerDevice(std::make_unique()); diff --git a/lib/devices/logitech_g522_lightspeed.hpp b/lib/devices/logitech_g522_lightspeed.hpp new file mode 100644 index 0000000..f75982c --- /dev/null +++ b/lib/devices/logitech_g522_lightspeed.hpp @@ -0,0 +1,249 @@ +#pragma once + +#include "../utility.hpp" +#include "hid_device.hpp" +#include +#include +#include +#include + +using namespace std::string_view_literals; + +namespace headsetcontrol { + +/** + * @brief Logitech G522 LIGHTSPEED (PID 0x0b18) + * + * This variant uses a vendor-specific 64-byte protocol on usage page 0xffa0 + * for battery data instead of HID++. + * + * This implementation is modified from the working LIGHTSPEED implementation for the G PRO X 2 (PID 0x0af7) as the used protocol appears to be the same. + */ +class LogitechG522Lightspeed : public HIDDevice { +public: + static constexpr std::array SUPPORTED_PRODUCT_IDS { 0x0b18 }; + static constexpr size_t PACKET_SIZE = 64; + static constexpr std::array REPORT_PREFIX { 0x50, 0x23 }; + + constexpr uint16_t getVendorId() const override + { + return VENDOR_LOGITECH; + } + + std::vector getProductIds() const override + { + return { SUPPORTED_PRODUCT_IDS.begin(), SUPPORTED_PRODUCT_IDS.end() }; + } + + std::string_view getDeviceName() const override + { + return "Logitech G522 LIGHTSPEED"sv; + } + + constexpr int getCapabilities() const override + { + return B(CAP_SIDETONE) | B(CAP_BATTERY_STATUS) | B(CAP_INACTIVE_TIME); + } + + constexpr capability_detail getCapabilityDetail(enum capabilities cap) const override + { + switch (cap) { + case CAP_BATTERY_STATUS: + case CAP_SIDETONE: + case CAP_INACTIVE_TIME: + return { .usagepage = 0xffa0, .usageid = 0x0001, .interface_id = 3 }; + default: + return HIDDevice::getCapabilityDetail(cap); + } + } + + Result getBattery(hid_device* device_handle) override + { + auto start_time = std::chrono::steady_clock::now(); + + std::array request = buildBatteryRequest(); + if (auto write_result = writeHID(device_handle, request, PACKET_SIZE); !write_result) { + return write_result.error(); + } + + std::vector raw_packets; + raw_packets.reserve(PACKET_SIZE * 4); + + for (int attempt = 0; attempt < 4; ++attempt) { + std::array response {}; + auto read_result = readHIDTimeout(device_handle, response, hsc_device_timeout); + if (!read_result) { + return read_result.error(); + } + + raw_packets.insert(raw_packets.end(), response.begin(), response.end()); + + if (isPowerOffPacket(response)) { + return DeviceError::deviceOffline("Headset is powered off or not connected"); + } + + if (isPowerEventPacket(response)) { + continue; + } + + if (isAckPacket(response)) { + continue; + } + + if (!isBatteryResponsePacket(response)) { + continue; + } + + auto battery_result = parseBatteryResponse(response); + if (!battery_result) { + return battery_result.error(); + } + + battery_result->raw_data = std::move(raw_packets); + auto end_time = std::chrono::steady_clock::now(); + battery_result->query_duration = std::chrono::duration_cast(end_time - start_time); + return *battery_result; + } + + return DeviceError::protocolError("Battery response packet not received"); + } + + Result setSidetone(hid_device* device_handle, uint8_t level) override + { + // INFO: The original G HUB app does some strange mapping: + // 0 - 5 -> 0x00 (off) + // 6 - 16 -> 0x01 + // 17 - 27 -> 0x02 + // 28 - 38 -> 0x03 + // 39 - 49 -> 0x04 + // 50 - 61 -> 0x05 + // 62 - 72 -> 0x06 + // 73 - 83 -> 0x07 + // 84 - 94 -> 0x08 + // 95 - 100 -> 0x09 + uint8_t mapped = map(level, 0, 128, 0, 9); + + auto command = buildSidetoneCommand(mapped); + if (auto write_result = writeHID(device_handle, command, PACKET_SIZE); !write_result) { + return write_result.error(); + } + + return SidetoneResult { + .current_level = level, + .min_level = 0, + .max_level = 128, + .device_min = 0, + .device_max = 9 + }; + } + + Result setInactiveTime(hid_device* device_handle, uint8_t minutes) override + { + auto command = buildInactiveTimeCommand(minutes); + if (auto write_result = writeHID(device_handle, command, PACKET_SIZE); !write_result) { + return write_result.error(); + } + + return InactiveTimeResult { + .minutes = minutes, + .min_minutes = 0, + .max_minutes = 90 + }; + } + + static constexpr bool isAckPacket(std::span packet) + { + return packet.size() >= 3 && packet[0] == REPORT_PREFIX[0] && packet[1] == REPORT_PREFIX[1] && packet[2] == 0x03; + } + + static constexpr bool isPowerOffPacket(std::span packet) + { + return packet.size() >= 7 && packet[0] == REPORT_PREFIX[0] && packet[1] == REPORT_PREFIX[1] && packet[2] == 0x05 && packet[6] == 0x00; + } + + static constexpr bool isPowerEventPacket(std::span packet) + { + return packet.size() >= 3 && packet[0] == REPORT_PREFIX[0] && packet[1] == REPORT_PREFIX[1] && packet[2] == 0x05; + } + + static constexpr bool isBatteryResponsePacket(std::span packet) + { + return packet.size() >= 14 && packet[0] == REPORT_PREFIX[0] && packet[1] == REPORT_PREFIX[1] && packet[2] == 0x0b && packet[9] == 0x05; + } + + static Result parseBatteryResponse(std::span packet) + { + if (!isBatteryResponsePacket(packet)) { + return DeviceError::protocolError("Unexpected battery response packet"); + } + + int level = static_cast(packet[11]); + if (level > 100) { + return DeviceError::protocolError("Battery percentage out of range"); + } + + auto status = packet[13] == 0x02 ? BATTERY_CHARGING : BATTERY_AVAILABLE; + + BatteryResult result { + .level_percent = level, + .status = status, + }; + + return result; + } + +private: + static constexpr std::array buildBatteryRequest() + { + std::array request {}; + request[0] = REPORT_PREFIX[0]; + request[1] = REPORT_PREFIX[1]; + request[2] = 0x0b; + request[4] = 0x03; + request[5] = 0x1a; + request[7] = 0x03; + request[9] = 0x05; + request[10] = 0x0a; + return request; + } + + static constexpr std::array buildSidetoneCommand(uint8_t level) + { + std::array command {}; + command[0] = REPORT_PREFIX[0]; + command[1] = REPORT_PREFIX[1]; + command[2] = 0x0b; + command[4] = 0x03; + command[5] = 0x1c; + command[7] = 0x06; + command[9] = 0x0d; + command[10] = 0x1c; + command[11] = 0x01; + command[12] = 0xff; + command[13] = level; + return command; + } + + static constexpr std::array buildInactiveTimeCommand(uint8_t minutes) + { + std::array command {}; + command[0] = REPORT_PREFIX[0]; + command[1] = REPORT_PREFIX[1]; + command[2] = 0x0b; + command[4] = 0x03; + command[5] = 0x1c; + command[7] = 0x06; + command[9] = 0x14; + command[10] = 0x1c; + command[11] = minutes; + + // WARN: This has a side effect since there are multiple timers being set with the same command. + // command[12] = 0x00; // This byte sets the time until "lighting goes into inactive mode" e.g. dimmer lights, etc. (can be set in G HUB). + // command[13] = 0x00; // This byte sets the time until "lighting off because of inactivity" + // For both timers, a value of 0 is labeled "never" in G HUB + + return command; + } +}; + +} // namespace headsetcontrol From b14a740acca6a98145ce7bb905d4bc5d4ad6b774 Mon Sep 17 00:00:00 2001 From: Simon Schindler Date: Tue, 17 Mar 2026 10:34:30 +0100 Subject: [PATCH 2/4] implement mic-mute-led-brightness for Logitech G522 Lightspeed --- lib/devices/logitech_g522_lightspeed.hpp | 36 +++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/lib/devices/logitech_g522_lightspeed.hpp b/lib/devices/logitech_g522_lightspeed.hpp index f75982c..945e6f3 100644 --- a/lib/devices/logitech_g522_lightspeed.hpp +++ b/lib/devices/logitech_g522_lightspeed.hpp @@ -1,9 +1,12 @@ #pragma once #include "../utility.hpp" +#include "device.hpp" #include "hid_device.hpp" +#include "result_types.hpp" #include #include +#include #include #include @@ -42,7 +45,7 @@ class LogitechG522Lightspeed : public HIDDevice { constexpr int getCapabilities() const override { - return B(CAP_SIDETONE) | B(CAP_BATTERY_STATUS) | B(CAP_INACTIVE_TIME); + return B(CAP_SIDETONE) | B(CAP_BATTERY_STATUS) | B(CAP_INACTIVE_TIME) | B(CAP_MICROPHONE_MUTE_LED_BRIGHTNESS); } constexpr capability_detail getCapabilityDetail(enum capabilities cap) const override @@ -51,6 +54,7 @@ class LogitechG522Lightspeed : public HIDDevice { case CAP_BATTERY_STATUS: case CAP_SIDETONE: case CAP_INACTIVE_TIME: + case CAP_MICROPHONE_MUTE_LED_BRIGHTNESS: return { .usagepage = 0xffa0, .usageid = 0x0001, .interface_id = 3 }; default: return HIDDevice::getCapabilityDetail(cap); @@ -151,6 +155,21 @@ class LogitechG522Lightspeed : public HIDDevice { }; } + Result setMicMuteLedBrightness(hid_device* device_handle, uint8_t brightness) override + { + uint8_t mute_led = static_cast(static_cast(brightness)); // 0 or 1 + auto command = buildMicMuteLedCommand(mute_led); + if (auto write_result = writeHID(device_handle, command, PACKET_SIZE); !write_result) { + return write_result.error(); + } + + return MicMuteLedBrightnessResult { + .brightness = mute_led, + .min_brightness = 0, + .max_brightness = 1 + }; + } + static constexpr bool isAckPacket(std::span packet) { return packet.size() >= 3 && packet[0] == REPORT_PREFIX[0] && packet[1] == REPORT_PREFIX[1] && packet[2] == 0x03; @@ -244,6 +263,21 @@ class LogitechG522Lightspeed : public HIDDevice { return command; } + + static constexpr std::array buildMicMuteLedCommand(uint8_t mute_led) + { + std::array command {}; + command[0] = REPORT_PREFIX[0]; + command[1] = REPORT_PREFIX[1]; + command[2] = 0x09; + command[4] = 0x03; + command[5] = 0x1c; + command[7] = 0x04; + command[9] = 0x15; + command[10] = 0x2c; + command[11] = mute_led; + return command; + } }; } // namespace headsetcontrol From 5ed7a5a5221061dc23f4b8f983bc19450647f790 Mon Sep 17 00:00:00 2001 From: Simon Schindler Date: Tue, 17 Mar 2026 10:41:03 +0100 Subject: [PATCH 3/4] format changes of clang-format --- lib/devices/logitech_g522_lightspeed.hpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/devices/logitech_g522_lightspeed.hpp b/lib/devices/logitech_g522_lightspeed.hpp index 945e6f3..11136ea 100644 --- a/lib/devices/logitech_g522_lightspeed.hpp +++ b/lib/devices/logitech_g522_lightspeed.hpp @@ -74,7 +74,7 @@ class LogitechG522Lightspeed : public HIDDevice { raw_packets.reserve(PACKET_SIZE * 4); for (int attempt = 0; attempt < 4; ++attempt) { - std::array response {}; + std::array response { }; auto read_result = readHIDTimeout(device_handle, response, hsc_device_timeout); if (!read_result) { return read_result.error(); @@ -214,7 +214,7 @@ class LogitechG522Lightspeed : public HIDDevice { private: static constexpr std::array buildBatteryRequest() { - std::array request {}; + std::array request { }; request[0] = REPORT_PREFIX[0]; request[1] = REPORT_PREFIX[1]; request[2] = 0x0b; @@ -228,7 +228,7 @@ class LogitechG522Lightspeed : public HIDDevice { static constexpr std::array buildSidetoneCommand(uint8_t level) { - std::array command {}; + std::array command { }; command[0] = REPORT_PREFIX[0]; command[1] = REPORT_PREFIX[1]; command[2] = 0x0b; @@ -245,7 +245,7 @@ class LogitechG522Lightspeed : public HIDDevice { static constexpr std::array buildInactiveTimeCommand(uint8_t minutes) { - std::array command {}; + std::array command { }; command[0] = REPORT_PREFIX[0]; command[1] = REPORT_PREFIX[1]; command[2] = 0x0b; @@ -266,7 +266,7 @@ class LogitechG522Lightspeed : public HIDDevice { static constexpr std::array buildMicMuteLedCommand(uint8_t mute_led) { - std::array command {}; + std::array command { }; command[0] = REPORT_PREFIX[0]; command[1] = REPORT_PREFIX[1]; command[2] = 0x09; From 91aa11494721eabef092e9d26248d198e61ef875 Mon Sep 17 00:00:00 2001 From: Simon <74510395+iowi479@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:59:55 +0100 Subject: [PATCH 4/4] Add Logitech G522 Lightspeed as supported devices --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 81055f4..b723aca 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ A cross-platform tool to control USB gaming headsets on **Linux**, **macOS**, an | Device | Platform | sidetone | battery | notification sound | lights | inactive time | chatmix | voice prompts | rotate to mute | equalizer preset | equalizer | parametric equalizer | microphone mute led brightness | microphone volume | volume limiter | bluetooth when powered on | bluetooth call volume | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| Logitech G522 Lightspeed | All | x | x | | | x | | | | | | | x | | | | | | Logitech G533 | All | x | x | | | x | | | | | | | | | | | | | Logitech G535 | All | x | x | | | x | | | | | | | | | | | | | Logitech G633/G635/G733/G933/G935 | All | x | x | | x | | | | | | | | | | | | |