From c560caaa33edffd8b4396f80ea5aaabd63090d00 Mon Sep 17 00:00:00 2001 From: RpYT <137761902+RpYTMc@users.noreply.github.com> Date: Sun, 2 Aug 2026 12:59:27 +0300 Subject: [PATCH] fix: enable Windows support for Arctis Nova 3P/3X via output reports Windows hidapi pads HID feature reports to the interface's declared report length (~1060 bytes on this device), and the firmware silently ignores oversized reports - so every feature-report command was a no-op on Windows, which is why the device was limited to Linux/macOS (#417). Switch the battery query to the shared readDeviceStatus() write path and all commands to output reports, matching the Nova 5/7 implementations. The status response layout is identical to the Nova 5: offline = data[1] == 0x02, charging = data[4] == 0x01, level = data[3]. This also adds charging detection, which the feature-report implementation never had. Tested on a real Arctis Nova 3X (0x1038:0x226d) on Windows 11: battery level, charging flag, sidetone, equalizer presets and inactive time all work. Co-Authored-By: Claude Fable 5 --- README.md | 2 +- .../steelseries_arctis_nova_3p_wireless.hpp | 101 +++++++++--------- 2 files changed, 51 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 50fff6e3..59407922 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,7 @@ sudo udevadm control --reload-rules && sudo udevadm trigger | SteelSeries Arctis Nova 7P | All | | x | | | x | | | | x | x | | x | x | x | x | x | | | SteelSeries Arctis 7+ | All | x | x | | | x | x | | | x | x | | | | | | | | | SteelSeries Arctis Nova Pro Wireless | All | x | x | | x | x | | | | x | x | | | | | | | | -| SteelSeries Arctis Nova 3P Wireless | L/M | x | x | | | x | | | | x | x | x | | x | | | | | +| SteelSeries Arctis Nova 3P Wireless | All | x | x | | | x | | | | x | x | x | | x | | | | | | SteelSeries Arctis Buds | All | | x | | | | | | | | | | | | | | | | | HyperX Cloud Alpha Wireless | All | x | x | | | x | | x | | | | | | | | | | | | HyperX Cloud Flight Wireless | All | | x | | | | | | | | | | | | | | | | diff --git a/lib/devices/steelseries_arctis_nova_3p_wireless.hpp b/lib/devices/steelseries_arctis_nova_3p_wireless.hpp index 8ca321e8..1535bc96 100644 --- a/lib/devices/steelseries_arctis_nova_3p_wireless.hpp +++ b/lib/devices/steelseries_arctis_nova_3p_wireless.hpp @@ -19,8 +19,6 @@ namespace headsetcontrol { * - Battery status * - Equalizer (10 bands) * - Parametric equalizer (ADVANCED!) - * - * NOTE: Windows support has HID packet length issues */ class SteelSeriesArctisNova3PWireless : public protocols::SteelSeriesNovaDevice { public: @@ -106,12 +104,6 @@ class SteelSeriesArctisNova3PWireless : public protocols::SteelSeriesNovaDevice< return { .usagepage = 0xffc0, .usageid = 0x1, .interface_id = 3 }; } - constexpr uint8_t getSupportedPlatforms() const override - { - // Windows has HID packet length issues - return PLATFORM_LINUX | PLATFORM_MACOS; - } - std::optional getEqualizerInfo() const override { return EqualizerInfo { @@ -167,36 +159,41 @@ class SteelSeriesArctisNova3PWireless : public protocols::SteelSeriesNovaDevice< // Helper: save state Result saveStateNova3P(hid_device* device_handle) const { - std::array cmd { 0x09 }; - return sendFeatureReport(device_handle, cmd, MSG_SIZE); + std::array cmd { 0x00, 0x09 }; + return writeHID(device_handle, cmd, MSG_SIZE); } // Rich Results V2 API Result getBattery(hid_device* device_handle) override { - std::array request { 0xb0 }; - if (auto result = sendFeatureReport(device_handle, request, MSG_SIZE); !result) { - return result.error(); + auto status_result = readDeviceStatus(device_handle); + if (!status_result) { + return status_result.error(); } - std::array response {}; - auto read_result = readHIDTimeout(device_handle, response, hsc_device_timeout); - - if (!read_result) { - return read_result.error(); + auto& data = *status_result; + if (data.size() < 5) { + return DeviceError::protocolError("Response too short"); } - constexpr uint8_t HEADSET_OFFLINE = 0x02; - if (response[1] == HEADSET_OFFLINE) { + constexpr uint8_t HEADSET_OFFLINE = 0x02; + constexpr uint8_t HEADSET_CHARGING = 0x01; + + if (data[1] == HEADSET_OFFLINE) { return DeviceError::deviceOffline("Headset not connected"); } - int level = map(response[3], 0, 100, 0, 100); + enum battery_status status = BATTERY_AVAILABLE; + if (data[4] == HEADSET_CHARGING) { + status = BATTERY_CHARGING; + } + + int level = data[3]; // Already in percentage return BatteryResult { .level_percent = level, - .status = BATTERY_AVAILABLE, - .raw_data = std::vector { response.begin(), response.end() } + .status = status, + .raw_data = data }; } @@ -204,8 +201,8 @@ class SteelSeriesArctisNova3PWireless : public protocols::SteelSeriesNovaDevice< { uint8_t mapped = map(level, 0, 128, 0, 0xa); - std::array cmd { 0x39, mapped }; - if (auto result = sendFeatureReport(device_handle, cmd, MSG_SIZE); !result) { + std::array cmd { 0x00, 0x39, mapped }; + if (auto result = writeHID(device_handle, cmd, MSG_SIZE); !result) { return result.error(); } @@ -246,8 +243,8 @@ class SteelSeriesArctisNova3PWireless : public protocols::SteelSeriesNovaDevice< else minutes = 0; // Never turn off - std::array cmd { 0xa3, minutes }; - if (auto result = sendFeatureReport(device_handle, cmd, MSG_SIZE); !result) { + std::array cmd { 0x00, 0xa3, minutes }; + if (auto result = writeHID(device_handle, cmd, MSG_SIZE); !result) { return result.error(); } @@ -266,8 +263,8 @@ class SteelSeriesArctisNova3PWireless : public protocols::SteelSeriesNovaDevice< { uint8_t mapped = map(volume, 0, 128, 0, 0x0e); - std::array cmd { 0x37, mapped }; - if (auto result = sendFeatureReport(device_handle, cmd, MSG_SIZE); !result) { + std::array cmd { 0x00, 0x37, mapped }; + if (auto result = writeHID(device_handle, cmd, MSG_SIZE); !result) { return result.error(); } @@ -321,7 +318,8 @@ class SteelSeriesArctisNova3PWireless : public protocols::SteelSeriesNovaDevice< } std::array data {}; - data[0] = 0x33; + data[0] = 0x00; + data[1] = 0x33; for (int i = 0; i < EQUALIZER_BANDS; i++) { float gain_value = settings.bands[i]; @@ -331,16 +329,16 @@ class SteelSeriesArctisNova3PWireless : public protocols::SteelSeriesNovaDevice< // Each band: 2 bytes frequency (LE), 1 byte filter type, 1 byte gain, 2 bytes Q-factor (LE) uint16_t freq = BAND_FREQUENCIES[i]; - data[1 + 6 * i] = freq & 0xFF; - data[1 + 6 * i + 1] = (freq >> 8) & 0xFF; - data[1 + 6 * i + 2] = 0x01; // Peaking filter - data[1 + 6 * i + 3] = encodeGain(gain_value); + data[2 + 6 * i] = freq & 0xFF; + data[2 + 6 * i + 1] = (freq >> 8) & 0xFF; + data[2 + 6 * i + 2] = 0x01; // Peaking filter + data[2 + 6 * i + 3] = encodeGain(gain_value); // Default Q-factor: 1.414 * 1000 = 1414 = 0x0586 - data[1 + 6 * i + 4] = 0x86; - data[1 + 6 * i + 5] = 0x05; + data[2 + 6 * i + 4] = 0x86; + data[2 + 6 * i + 5] = 0x05; } - if (auto result = sendFeatureReport(device_handle, data, MSG_SIZE); !result) { + if (auto result = writeHID(device_handle, data, MSG_SIZE); !result) { return result.error(); } @@ -358,7 +356,8 @@ class SteelSeriesArctisNova3PWireless : public protocols::SteelSeriesNovaDevice< } std::array data {}; - data[0] = 0x33; + data[0] = 0x00; + data[1] = 0x33; // Write provided bands for (int i = 0; i < settings.size(); i++) { @@ -383,40 +382,40 @@ class SteelSeriesArctisNova3PWireless : public protocols::SteelSeriesNovaDevice< // Write frequency (LE) uint16_t freq = static_cast(band.frequency); - data[1 + 6 * i] = freq & 0xFF; - data[1 + 6 * i + 1] = (freq >> 8) & 0xFF; + data[2 + 6 * i] = freq & 0xFF; + data[2 + 6 * i + 1] = (freq >> 8) & 0xFF; // Write filter type - data[1 + 6 * i + 2] = getFilterByte(band.type); + data[2 + 6 * i + 2] = getFilterByte(band.type); // Write gain (two's complement for negative) - data[1 + 6 * i + 3] = encodeGain(band.gain); + data[2 + 6 * i + 3] = encodeGain(band.gain); // Write Q-factor (LE) uint16_t q = static_cast(band.q_factor * 1000); - data[1 + 6 * i + 4] = q & 0xFF; - data[1 + 6 * i + 5] = (q >> 8) & 0xFF; + data[2 + 6 * i + 4] = q & 0xFF; + data[2 + 6 * i + 5] = (q >> 8) & 0xFF; } // Fill remaining bands with disabled band for (int i = settings.size(); i < EQUALIZER_BANDS; i++) { // Disabled frequency uint16_t freq = EQUALIZER_FREQ_DISABLED; - data[1 + 6 * i] = freq & 0xFF; - data[1 + 6 * i + 1] = (freq >> 8) & 0xFF; + data[2 + 6 * i] = freq & 0xFF; + data[2 + 6 * i + 1] = (freq >> 8) & 0xFF; // Peaking filter type - data[1 + 6 * i + 2] = getFilterByte(EqualizerFilterType::Peaking); + data[2 + 6 * i + 2] = getFilterByte(EqualizerFilterType::Peaking); // Gain = 0 - data[1 + 6 * i + 3] = 0; + data[2 + 6 * i + 3] = 0; // Default Q-factor: 1.414 * 1000 = 1414 = 0x0586 - data[1 + 6 * i + 4] = 0x86; - data[1 + 6 * i + 5] = 0x05; + data[2 + 6 * i + 4] = 0x86; + data[2 + 6 * i + 5] = 0x05; } - if (auto result = sendFeatureReport(device_handle, data, MSG_SIZE); !result) { + if (auto result = writeHID(device_handle, data, MSG_SIZE); !result) { return result.error(); }