Skip to content

Commit ca0c944

Browse files
Fix Arctis Nova 7 battery level misreporting. (#507)
1 parent 9899c5b commit ca0c944

File tree

3 files changed

+29
-23
lines changed

3 files changed

+29
-23
lines changed

lib/device_registry.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ HIDDevice* DeviceRegistry::getDevice(uint16_t vendor_id, uint16_t product_id)
152152
auto product_ids = device->getProductIds();
153153
for (uint16_t pid : product_ids) {
154154
if (pid == product_id) {
155+
device->setMatchedProductId(product_id);
155156
return device.get();
156157
}
157158
}

lib/devices/hid_device.hpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ class HIDDevice {
3636
public:
3737
virtual ~HIDDevice() = default;
3838

39+
/**
40+
* @brief Get/set the matched USB product ID
41+
*
42+
* Set by DeviceRegistry when the device is matched to actual hardware.
43+
* This allows device implementations to tailor behavior (e.g., battery
44+
* protocol) based on the specific product variant.
45+
*/
46+
void setMatchedProductId(uint16_t pid) { matched_product_id_ = pid; }
47+
uint16_t getMatchedProductId() const { return matched_product_id_; }
48+
3949
/**
4050
* @brief Get USB vendor ID
4151
*/
@@ -441,6 +451,8 @@ class HIDDevice {
441451
}
442452

443453
private:
454+
uint16_t matched_product_id_ = 0;
455+
444456
// Cache for C struct conversion (allocated on first call to toCDevice)
445457
std::unique_ptr<struct device> c_device_cache;
446458
};

lib/devices/steelseries_arctis_nova_7.hpp

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include "../result_types.hpp"
44
#include "device_utils.hpp"
55
#include "protocols/steelseries_protocol.hpp"
6+
#include <algorithm>
67
#include <array>
78
#include <string_view>
89

@@ -29,7 +30,7 @@ namespace headsetcontrol {
2930
*/
3031
class SteelSeriesArctisNova7 : public protocols::SteelSeriesNovaDevice<SteelSeriesArctisNova7> {
3132
public:
32-
static constexpr std::array<uint16_t,12> SUPPORTED_PRODUCT_IDS {
33+
static constexpr std::array<uint16_t, 12> SUPPORTED_PRODUCT_IDS {
3334
0x2202, // Arctis Nova 7 (discrete battery: 0-4)
3435
0x22A1, // Arctis Nova 7 (percentage battery: 0-100, Jan. 2026 update)
3536
0x227e, // Arctis Nova 7 Wireless Gen 2 (percentage battery: 0-100)
@@ -41,7 +42,7 @@ class SteelSeriesArctisNova7 : public protocols::SteelSeriesNovaDevice<SteelSeri
4142
0x22a9, // Arctis Nova 7 Diablo IV (percentage battery: 0-100, after Jan 2026 update)
4243
0x227a, // Arctis Nova 7 WoW Edition (discrete battery: 0-4)
4344
0x22a4, // Arctis Nova 7X (discrete battery: 0-4)
44-
0x22a5 // Arctis Nova 7X (percentage battery: 0-100)
45+
0x22a5 // Arctis Nova 7X (percentage battery: 0-100)
4546
};
4647

4748
static constexpr int EQUALIZER_BANDS = 10;
@@ -126,29 +127,21 @@ class SteelSeriesArctisNova7 : public protocols::SteelSeriesNovaDevice<SteelSeri
126127
return DeviceError::deviceOffline("Headset not connected");
127128
}
128129

129-
// Auto-detect battery protocol (Gen 2 vs original models):
130+
// Determine battery protocol based on matched product ID.
130131
//
131-
// Original models (0x2202, 0x2206, 0x220a, 0x223a, 0x227a):
132-
// - Battery: data[2] in discrete levels 0-4 (0%/25%/50%/75%/100%)
133-
// - Status: data[3] = 0x01 when charging, other non-zero when on battery
132+
// Original models (discrete battery: 0-4 levels → 0%/25%/50%/75%/100%):
133+
// 0x2202, 0x2206, 0x220a, 0x223a, 0x227a, 0x22a4
134134
//
135-
// Gen 2 models (0x227e, possibly 0x2258):
136-
// - Battery: data[2] as direct percentage 0-100
137-
// - Status: data[3] = 0x01 charging, 0x02 fully charged, 0x03 on battery
138-
//
139-
// Detection heuristics (since we don't have product_id here):
140-
// 1. Status byte 0x02 or 0x03 → Gen 2 protocol
141-
// 2. Battery value > 4 → Gen 2 protocol (wouldn't be valid in discrete mode)
142-
//
143-
// TODO: Known edge case - Gen 2 at 1-4% battery while actively charging (status=0x01)
144-
// will be misdetected as original protocol and show inflated percentage
145-
// (1%→25%, 2%→50%, 3%→75%, 4%→100%). This is extremely rare because:
146-
// - Requires plugging in exactly at 1-4% battery
147-
// - At low battery, devices typically show status=0x03 (on battery)
148-
// - Self-corrects once battery charges past 4%
149-
// - Only lasts a few seconds/minutes
150-
// Proper fix would require passing product_id to getBattery() method.
151-
bool is_gen2_protocol = (data[3] == 0x02 || data[3] == 0x03) || (data[2] > 4);
135+
// Gen 2 / updated firmware models (percentage battery: 0-100 direct):
136+
// 0x22A1, 0x227e, 0x2258, 0x229e, 0x22ad, 0x22a9, 0x22a5, 0x22a7
137+
static constexpr std::array<uint16_t, 6> DISCRETE_BATTERY_PIDS {
138+
0x2202, 0x2206, 0x220a, 0x223a, 0x227a, 0x22a4
139+
};
140+
uint16_t pid = getMatchedProductId();
141+
bool is_discrete = std::find(DISCRETE_BATTERY_PIDS.begin(),
142+
DISCRETE_BATTERY_PIDS.end(), pid)
143+
!= DISCRETE_BATTERY_PIDS.end();
144+
bool is_gen2_protocol = !is_discrete;
152145

153146
enum battery_status status = BATTERY_AVAILABLE;
154147
if (data[3] == 0x01 || data[3] == 0x02) {

0 commit comments

Comments
 (0)