From fb188a97a8ce6f362a23333c1c48ee3f2c666e4d Mon Sep 17 00:00:00 2001 From: Phelete Date: Mon, 11 May 2026 11:01:48 +0200 Subject: [PATCH 1/5] feat(device): add HyperX Cloud II Wireless (Kingston) --- README.md | 1 + lib/device_registry.cpp | 2 + .../hyperx_cloud_2_wireless_kingston.hpp | 174 ++++++++++++++++++ 3 files changed, 177 insertions(+) create mode 100644 lib/devices/hyperx_cloud_2_wireless_kingston.hpp diff --git a/README.md b/README.md index 7114c30..967886d 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ A cross-platform tool to control USB gaming headsets on **Linux**, **macOS**, an | HyperX Cloud Flight Wireless | All | | x | | | | | | | | | | | | | | | | | HyperX Cloud II Wireless | All | | x | | | x | | | | | | | | | | | | | | HyperX Cloud 3 | All | x | | | | | | | | | | | | | | | | | +| HyperX Cloud II Wireless (Kingston) | All | x | x | | | x | | | | | | | | | | | | | | ROCCAT Elo 7.1 Air | All | | | | x | x | | | | | | | | | | | | | | ROCCAT Elo 7.1 USB | All | | | | x | | | | | | | | | | | | | | | Audeze Maxwell | All | x | x | | | x | x | x | | x | | | | | x | | | | diff --git a/lib/device_registry.cpp b/lib/device_registry.cpp index 94a9b43..c49a4d7 100644 --- a/lib/device_registry.cpp +++ b/lib/device_registry.cpp @@ -30,6 +30,7 @@ // HyperX devices #include "devices/hyperx_cloud_2_wireless.hpp" +#include "devices/hyperx_cloud_2_wireless_kingston.hpp" #include "devices/hyperx_cloud_3.hpp" #include "devices/hyperx_cloud_alpha_wireless.hpp" #include "devices/hyperx_cloud_flight.hpp" @@ -121,6 +122,7 @@ void DeviceRegistry::initialize() registerDevice(std::make_unique()); registerDevice(std::make_unique()); registerDevice(std::make_unique()); + registerDevice(std::make_unique()); registerDevice(std::make_unique()); // Roccat devices diff --git a/lib/devices/hyperx_cloud_2_wireless_kingston.hpp b/lib/devices/hyperx_cloud_2_wireless_kingston.hpp new file mode 100644 index 0000000..23132af --- /dev/null +++ b/lib/devices/hyperx_cloud_2_wireless_kingston.hpp @@ -0,0 +1,174 @@ +#pragma once + +#include "../result_types.hpp" +#include "hid_device.hpp" +#include +#include +#include +#include + +using namespace std::string_view_literals; + +namespace headsetcontrol { + +/** + * @brief HyperX Cloud II Wireless Gaming Headset (Kingston-branded revision) + * + * Uses a different protocol than the HP-branded version (0x03f0:0x0696). + * Protocol reverse-engineered from HyperHeadset project: + * https://github.com/LennardKittner/HyperHeadset + * + * Key differences: + * - 62-byte packets instead of 52 + * - Different base packet structure + * - Response uses Report ID 11 (0x0B) instead of 6 + * - Battery level at response[7] + */ +class HyperXCloud2WirelessKingston : public HIDDevice { +public: + static constexpr uint16_t VENDOR_KINGSTON = 0x0951; + static constexpr std::array SUPPORTED_PRODUCT_IDS_KINGSTON { + 0x1718 // Cloud II Wireless (Kingston) + }; + + static constexpr int WRITE_PACKET_SIZE = 62; + static constexpr int WRITE_TIMEOUT = 100; + static constexpr int READ_PACKET_SIZE = 64; + static constexpr int READ_TIMEOUT = 1000; + + static constexpr uint8_t CMD_GET_BATTERY_LEVEL = 0x02; + static constexpr uint8_t CMD_GET_BATTERY_CHARGING = 0x03; + static constexpr uint8_t CMD_SET_AUTO_SHUTDOWN = 0x18; + static constexpr uint8_t CMD_SET_SIDETONE = 0x19; + + static constexpr int BATTERY_LEVEL_INDEX = 7; + static constexpr int CHARGING_STATUS_INDEX = 4; + + constexpr uint16_t getVendorId() const override + { + return VENDOR_KINGSTON; + } + + std::vector getProductIds() const override + { + return { SUPPORTED_PRODUCT_IDS_KINGSTON.begin(), SUPPORTED_PRODUCT_IDS_KINGSTON.end() }; + } + + std::string_view getDeviceName() const override + { + return "HyperX Cloud II Wireless (Kingston)"sv; + } + + constexpr int getCapabilities() const override + { + return B(CAP_BATTERY_STATUS) | B(CAP_SIDETONE) | B(CAP_INACTIVE_TIME); + } + + Result> sendCommand(hid_device* device_handle, uint8_t command, uint8_t payload = 0, bool check_response = true) + { + // Base packet structure from HyperHeadset + std::array request { }; + request[0] = 0x06; + request[1] = 0x00; + request[2] = 0x02; + request[3] = 0x00; + request[4] = 0x9A; + request[5] = 0x00; + request[6] = 0x00; + request[7] = 0x68; + request[8] = 0x4A; + request[9] = 0x8E; + request[10] = 0x0A; + request[11] = 0x00; + request[12] = 0x00; + request[13] = 0x00; + request[14] = 0xBB; + request[15] = command; + request[16] = payload; + + // Prepare write: attempt to read input report first (may fail, ignore error) + std::array input_report { }; + input_report[0] = 0x06; + hid_get_input_report(device_handle, input_report.data(), input_report.size()); + + auto wr = writeHID(device_handle, request); + std::this_thread::sleep_for(std::chrono::milliseconds(WRITE_TIMEOUT)); + if (!wr) { + return wr.error(); + } + + std::array response { }; + auto rd = readHIDTimeout(device_handle, response, READ_TIMEOUT); + + if (!check_response) { + return response; + } + + if (!rd) { + return rd.error(); + } + + // Response format: [11, 0, 187, cmd_id, ...] + if (response[0] != 0x0B || response[2] != 0xBB || response[3] != command) { + return DeviceError::protocolError("Invalid response header"); + } + + return response; + } + + Result getBattery(hid_device* device_handle) override + { + auto level_res = sendCommand(device_handle, CMD_GET_BATTERY_LEVEL); + if (!level_res) { + return level_res.error(); + } + + auto charging_res = sendCommand(device_handle, CMD_GET_BATTERY_CHARGING); + if (!charging_res) { + return charging_res.error(); + } + + return BatteryResult { + .level_percent = (*level_res)[BATTERY_LEVEL_INDEX], + .status = ((*charging_res)[CHARGING_STATUS_INDEX] == 1) ? BATTERY_CHARGING : BATTERY_AVAILABLE, + .voltage_mv = -1, + .raw_data = std::vector(level_res->begin(), level_res->end()) + }; + } + + Result setSidetone(hid_device* device_handle, uint8_t level) override + { + // Protocol only supports binary on/off (1 or 0) + uint8_t hardware_level = (level > 0) ? 1 : 0; + auto res = sendCommand(device_handle, CMD_SET_SIDETONE, hardware_level, false); + if (!res) { + return res.error(); + } + + return SidetoneResult { + .current_level = level, + .min_level = 0, + .max_level = 128, + .device_min = 0, + .device_max = 1 + }; + } + + Result setInactiveTime(hid_device* device_handle, uint8_t minutes) override + { + // Hardware limit: 30 minutes maximum + uint8_t hardware_mins = (minutes > 30) ? 30 : minutes; + auto res = sendCommand(device_handle, CMD_SET_AUTO_SHUTDOWN, hardware_mins); + if (!res) { + return res.error(); + } + + return InactiveTimeResult { + .minutes = hardware_mins, + .min_minutes = 0, + .max_minutes = 30 + }; + } +}; + +} // namespace headsetcontrol From f50f2ca8cbb86b95a01f8751e172a94196335fc0 Mon Sep 17 00:00:00 2001 From: Phelete Date: Mon, 11 May 2026 22:21:59 +0200 Subject: [PATCH 2/5] refactor(hyperx): split protocol logic into helper methods --- .../hyperx_cloud_2_wireless_kingston.hpp | 52 ++++++++++++------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/lib/devices/hyperx_cloud_2_wireless_kingston.hpp b/lib/devices/hyperx_cloud_2_wireless_kingston.hpp index 23132af..ee424af 100644 --- a/lib/devices/hyperx_cloud_2_wireless_kingston.hpp +++ b/lib/devices/hyperx_cloud_2_wireless_kingston.hpp @@ -64,9 +64,9 @@ class HyperXCloud2WirelessKingston : public HIDDevice { return B(CAP_BATTERY_STATUS) | B(CAP_SIDETONE) | B(CAP_INACTIVE_TIME); } - Result> sendCommand(hid_device* device_handle, uint8_t command, uint8_t payload = 0, bool check_response = true) +private: + std::array buildRequest(uint8_t command, uint8_t payload = 0) const { - // Base packet structure from HyperHeadset std::array request { }; request[0] = 0x06; request[1] = 0x00; @@ -85,11 +85,22 @@ class HyperXCloud2WirelessKingston : public HIDDevice { request[14] = 0xBB; request[15] = command; request[16] = payload; + return request; + } - // Prepare write: attempt to read input report first (may fail, ignore error) + void prepareDevice(hid_device* device_handle) const + { std::array input_report { }; input_report[0] = 0x06; - hid_get_input_report(device_handle, input_report.data(), input_report.size()); + // Attempt to read input report before writing (may fail, ignore error) + [[maybe_unused]] auto _ = getInputReport(device_handle, input_report); + } + +public: + Result> sendCommand(hid_device* device_handle, uint8_t command, uint8_t payload = 0) + { + auto request = buildRequest(command, payload); + prepareDevice(device_handle); auto wr = writeHID(device_handle, request); std::this_thread::sleep_for(std::chrono::milliseconds(WRITE_TIMEOUT)); @@ -100,10 +111,6 @@ class HyperXCloud2WirelessKingston : public HIDDevice { std::array response { }; auto rd = readHIDTimeout(device_handle, response, READ_TIMEOUT); - if (!check_response) { - return response; - } - if (!rd) { return rd.error(); } @@ -116,6 +123,13 @@ class HyperXCloud2WirelessKingston : public HIDDevice { return response; } + Result sendCommandFireAndForget(hid_device* device_handle, uint8_t command, uint8_t payload = 0) + { + auto request = buildRequest(command, payload); + prepareDevice(device_handle); + return writeHID(device_handle, request); + } + Result getBattery(hid_device* device_handle) override { auto level_res = sendCommand(device_handle, CMD_GET_BATTERY_LEVEL); @@ -130,9 +144,9 @@ class HyperXCloud2WirelessKingston : public HIDDevice { return BatteryResult { .level_percent = (*level_res)[BATTERY_LEVEL_INDEX], - .status = ((*charging_res)[CHARGING_STATUS_INDEX] == 1) ? BATTERY_CHARGING : BATTERY_AVAILABLE, - .voltage_mv = -1, - .raw_data = std::vector(level_res->begin(), level_res->end()) + .status = ((*charging_res)[CHARGING_STATUS_INDEX] == 1) ? BATTERY_CHARGING : BATTERY_AVAILABLE, + .voltage_mv = std::nullopt, + .raw_data = std::vector(level_res->begin(), level_res->end()) }; } @@ -140,17 +154,17 @@ class HyperXCloud2WirelessKingston : public HIDDevice { { // Protocol only supports binary on/off (1 or 0) uint8_t hardware_level = (level > 0) ? 1 : 0; - auto res = sendCommand(device_handle, CMD_SET_SIDETONE, hardware_level, false); + auto res = sendCommandFireAndForget(device_handle, CMD_SET_SIDETONE, hardware_level); if (!res) { return res.error(); } return SidetoneResult { - .current_level = level, - .min_level = 0, - .max_level = 128, - .device_min = 0, - .device_max = 1 + .current_level = hardware_level, + .min_level = 0, + .max_level = 128, + .device_min = 0, + .device_max = 1 }; } @@ -158,13 +172,13 @@ class HyperXCloud2WirelessKingston : public HIDDevice { { // Hardware limit: 30 minutes maximum uint8_t hardware_mins = (minutes > 30) ? 30 : minutes; - auto res = sendCommand(device_handle, CMD_SET_AUTO_SHUTDOWN, hardware_mins); + auto res = sendCommand(device_handle, CMD_SET_AUTO_SHUTDOWN, hardware_mins); if (!res) { return res.error(); } return InactiveTimeResult { - .minutes = hardware_mins, + .minutes = hardware_mins, .min_minutes = 0, .max_minutes = 30 }; From 0a8a058b3cf9362080af725f147f36ca8cead289 Mon Sep 17 00:00:00 2001 From: Youssef Behari <71698934+Phelete@users.noreply.github.com> Date: Sat, 16 May 2026 14:33:12 +0200 Subject: [PATCH 3/5] Update lib/devices/hyperx_cloud_2_wireless_kingston.hpp Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --- lib/devices/hyperx_cloud_2_wireless_kingston.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/devices/hyperx_cloud_2_wireless_kingston.hpp b/lib/devices/hyperx_cloud_2_wireless_kingston.hpp index ee424af..19665c5 100644 --- a/lib/devices/hyperx_cloud_2_wireless_kingston.hpp +++ b/lib/devices/hyperx_cloud_2_wireless_kingston.hpp @@ -99,7 +99,7 @@ class HyperXCloud2WirelessKingston : public HIDDevice { public: Result> sendCommand(hid_device* device_handle, uint8_t command, uint8_t payload = 0) { - auto request = buildRequest(command, payload); + prepareDevice(device_handle); prepareDevice(device_handle); auto wr = writeHID(device_handle, request); From 1e3d7d4c37e939a24d1fdddc72c579bba3d238d8 Mon Sep 17 00:00:00 2001 From: Youssef Behari <71698934+Phelete@users.noreply.github.com> Date: Sat, 16 May 2026 14:33:26 +0200 Subject: [PATCH 4/5] Update lib/devices/hyperx_cloud_2_wireless_kingston.hpp Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --- lib/devices/hyperx_cloud_2_wireless_kingston.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/devices/hyperx_cloud_2_wireless_kingston.hpp b/lib/devices/hyperx_cloud_2_wireless_kingston.hpp index 19665c5..7352b5a 100644 --- a/lib/devices/hyperx_cloud_2_wireless_kingston.hpp +++ b/lib/devices/hyperx_cloud_2_wireless_kingston.hpp @@ -125,7 +125,7 @@ class HyperXCloud2WirelessKingston : public HIDDevice { Result sendCommandFireAndForget(hid_device* device_handle, uint8_t command, uint8_t payload = 0) { - auto request = buildRequest(command, payload); + prepareDevice(device_handle); prepareDevice(device_handle); return writeHID(device_handle, request); } From 110c6c4b27f62aa4fd333e6b1a1f182b27985a4c Mon Sep 17 00:00:00 2001 From: Phelete Date: Sat, 16 May 2026 14:57:30 +0200 Subject: [PATCH 5/5] fix(hyperx): Restore missing request declaration --- lib/devices/hyperx_cloud_2_wireless_kingston.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/devices/hyperx_cloud_2_wireless_kingston.hpp b/lib/devices/hyperx_cloud_2_wireless_kingston.hpp index 7352b5a..94f6d73 100644 --- a/lib/devices/hyperx_cloud_2_wireless_kingston.hpp +++ b/lib/devices/hyperx_cloud_2_wireless_kingston.hpp @@ -100,7 +100,7 @@ class HyperXCloud2WirelessKingston : public HIDDevice { Result> sendCommand(hid_device* device_handle, uint8_t command, uint8_t payload = 0) { prepareDevice(device_handle); - prepareDevice(device_handle); + auto request = buildRequest(command, payload); auto wr = writeHID(device_handle, request); std::this_thread::sleep_for(std::chrono::milliseconds(WRITE_TIMEOUT)); @@ -126,7 +126,7 @@ class HyperXCloud2WirelessKingston : public HIDDevice { Result sendCommandFireAndForget(hid_device* device_handle, uint8_t command, uint8_t payload = 0) { prepareDevice(device_handle); - prepareDevice(device_handle); + auto request = buildRequest(command, payload); return writeHID(device_handle, request); }