Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,11 @@ if(WIN32)
# Use static runtime to avoid VC++ Redistributable dependency
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
else()
# MinGW/GCC flags
set(CMAKE_C_STANDARD_LIBRARIES "-lsetupapi -static-libgcc -static-libstdc++ -lwsock32 -lws2_32 ${CMAKE_CXX_STANDARD_LIBRARIES}")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-Bstatic,--whole-archive -lwinpthread -Wl,--no-whole-archive")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall")
# MinGW/GCC flags - statically link runtime to avoid DLL dependencies
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static-libgcc -static-libstdc++ -Wl,-Bstatic,--whole-archive -lwinpthread -Wl,--no-whole-archive")
set(CMAKE_C_STANDARD_LIBRARIES "-lsetupapi -lwsock32 -lws2_32 ${CMAKE_C_STANDARD_LIBRARIES}")
set(CMAKE_CXX_STANDARD_LIBRARIES "-lsetupapi -lwsock32 -lws2_32 ${CMAKE_CXX_STANDARD_LIBRARIES}")
add_compile_options(-Wall)
endif()
else()
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall")
Expand Down
7 changes: 7 additions & 0 deletions cli/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
#include <algorithm>
#include <cassert>
#include <chrono>
#include <cstdio>
#include <csignal>
#include <cstdlib>
#include <format>
Expand Down Expand Up @@ -1101,6 +1102,12 @@ bool checkDeviceConnected(DiscoveredDevice& selected, const Options& opts)

int main(int argc, char* argv[])
{
#if defined(_WIN32) && defined(__GNUC__)
// Disable stdout/stderr buffering for MinGW builds
setvbuf(stdout, nullptr, _IONBF, 0);
setvbuf(stderr, nullptr, _IONBF, 0);
#endif

Options opts;
cli::ArgumentParser parser(argv[0]);
configureParser(parser, opts);
Expand Down
55 changes: 44 additions & 11 deletions lib/devices/steelseries_arctis_nova_7.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,16 @@ namespace headsetcontrol {
*/
class SteelSeriesArctisNova7 : public protocols::SteelSeriesNovaDevice<SteelSeriesArctisNova7> {
public:
static constexpr std::array<uint16_t, 8> SUPPORTED_PRODUCT_IDS {
0x2202, // Arctis Nova 7
0x227e, // Arctis Nova 7 Wireless
0x2206, // Arctis Nova 7x
0x2258, // Arctis Nova 7x v2
0x229e, // Arctis Nova 7x v2
0x223a, // Arctis Nova 7 Diablo IV (before Jan Update)
0x22a9, // Arctis Nova 7 Diablo IV (after Jan Update)
0x227a // Arctis Nova 7 WoW Edition
static constexpr std::array<uint16_t, 9> SUPPORTED_PRODUCT_IDS {
0x2202, // Arctis Nova 7 (discrete battery: 0-4)
0x22A1, // Arctis Nova 7 (percentage battery: 0-100, Jan. 2026 update)
0x227e, // Arctis Nova 7 Wireless Gen 2 (percentage battery: 0-100)
0x2206, // Arctis Nova 7x (discrete battery: 0-4)
0x2258, // Arctis Nova 7x v2 (percentage battery: 0-100)
0x229e, // Arctis Nova 7x v2 (percentage battery: 0-100)
0x223a, // Arctis Nova 7 Diablo IV (discrete battery: 0-4, before Jan 2026 update)
0x22a9, // Arctis Nova 7 Diablo IV (percentage battery: 0-100, after Jan 2026 update)
0x227a // Arctis Nova 7 WoW Edition (discrete battery: 0-4)
};

static constexpr int EQUALIZER_BANDS = 10;
Expand Down Expand Up @@ -122,12 +123,44 @@ class SteelSeriesArctisNova7 : public protocols::SteelSeriesNovaDevice<SteelSeri
return DeviceError::deviceOffline("Headset not connected");
}

// Auto-detect battery protocol (Gen 2 vs original models):
//
// Original models (0x2202, 0x2206, 0x220a, 0x223a, 0x227a):
// - Battery: data[2] in discrete levels 0-4 (0%/25%/50%/75%/100%)
// - Status: data[3] = 0x01 when charging, other non-zero when on battery
//
// Gen 2 models (0x227e, possibly 0x2258):
// - Battery: data[2] as direct percentage 0-100
// - Status: data[3] = 0x01 charging, 0x02 fully charged, 0x03 on battery
//
// Detection heuristics (since we don't have product_id here):
// 1. Status byte 0x02 or 0x03 → Gen 2 protocol
// 2. Battery value > 4 → Gen 2 protocol (wouldn't be valid in discrete mode)
//
// TODO: Known edge case - Gen 2 at 1-4% battery while actively charging (status=0x01)
// will be misdetected as original protocol and show inflated percentage
// (1%→25%, 2%→50%, 3%→75%, 4%→100%). This is extremely rare because:
// - Requires plugging in exactly at 1-4% battery
// - At low battery, devices typically show status=0x03 (on battery)
// - Self-corrects once battery charges past 4%
// - Only lasts a few seconds/minutes
// Proper fix would require passing product_id to getBattery() method.
bool is_gen2_protocol = (data[3] == 0x02 || data[3] == 0x03) || (data[2] > 4);

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

int level = map(data[2], 0, 4, 0, 100);
int level;
if (is_gen2_protocol) {
// Gen 2: Direct percentage reporting (0-100) in data[2]
level = data[2];
} else {
// Original models: Discrete levels (0-4) that need mapping
level = map(data[2], 0, 4, 0, 100);
}

if (level > 100)
level = 100;

Expand Down