From 89d618d01c68b4e407ef68301a17a09cdaa58815 Mon Sep 17 00:00:00 2001 From: nekoinemo Date: Sun, 28 Jun 2026 03:49:07 +0300 Subject: [PATCH] Add Steelseries Arctis Buds (battery-only) --- README.md | 1 + lib/device_registry.cpp | 2 + lib/devices/steelseries_arctis_gamebuds.hpp | 102 ++++++++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 lib/devices/steelseries_arctis_gamebuds.hpp diff --git a/README.md b/README.md index 2a371d1..1d9609a 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ A cross-platform tool to control USB gaming headsets on **Linux**, **macOS**, an | 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 Buds | All | | x | | | | | | | | | | | | | | | | | HyperX Cloud Alpha Wireless | All | x | x | | | x | | x | | | | | | | | | | | | HyperX Cloud Flight Wireless | All | | x | | | | | | | | | | | | | | | | | HyperX Cloud II Wireless | All | | x | | | x | | | | | | | | | | | | | diff --git a/lib/device_registry.cpp b/lib/device_registry.cpp index f6af570..c2c29c2 100644 --- a/lib/device_registry.cpp +++ b/lib/device_registry.cpp @@ -23,6 +23,7 @@ #include "devices/steelseries_arctis_7.hpp" #include "devices/steelseries_arctis_7_plus.hpp" #include "devices/steelseries_arctis_9.hpp" +#include "devices/steelseries_arctis_gamebuds.hpp" #include "devices/steelseries_arctis_nova_3.hpp" #include "devices/steelseries_arctis_nova_3p_wireless.hpp" #include "devices/steelseries_arctis_nova_5.hpp" @@ -123,6 +124,7 @@ void DeviceRegistry::initialize() registerDevice(std::make_unique()); registerDevice(std::make_unique()); registerDevice(std::make_unique()); + registerDevice(std::make_unique()); // HyperX devices registerDevice(std::make_unique()); diff --git a/lib/devices/steelseries_arctis_gamebuds.hpp b/lib/devices/steelseries_arctis_gamebuds.hpp new file mode 100644 index 0000000..cf4122e --- /dev/null +++ b/lib/devices/steelseries_arctis_gamebuds.hpp @@ -0,0 +1,102 @@ +#pragma once + +#include "../result_types.hpp" +#include "protocols/steelseries_protocol.hpp" +#include +#include +#include +#include + +using namespace std::string_view_literals; + +namespace headsetcontrol { + +/** + * @brief SteelSeries Arctis GameBuds (wireless earbuds + USB-C dongle) + * + * Features: + * - Battery status + * + * Battery protocol: + * byte[3] left bud status (0x03 = out of case/active, 0x02 = docked/off) + * byte[4] right bud status + * byte[5] left bud battery % (direct 0-100 percentage) + * byte[6] right bud battery % + * The earbuds report no charging state (docking a bud powers it off) and the charging + * case has no wireless connection, so only active buds are reported. The lower of the two + * active buds is surfaced, since that bud tends to be the control/passthrough one and drains + * faster. + */ +class SteelSeriesArctisGamebuds : public protocols::SteelSeriesNovaDevice { +public: + static constexpr std::array SUPPORTED_PRODUCT_IDS { + 0x230a // Arctis GameBuds + }; + + static constexpr size_t LEFT_BUD_STATUS = 3; + static constexpr size_t RIGHT_BUD_STATUS = 4; + static constexpr size_t LEFT_BUD_BATTERY = 5; + static constexpr size_t RIGHT_BUD_BATTERY = 6; + + static constexpr uint8_t BUD_STATUS_ACTIVE = 0x03; // out of case; 0x02 = docked/off + + std::vector getProductIds() const override + { + return { SUPPORTED_PRODUCT_IDS.begin(), SUPPORTED_PRODUCT_IDS.end() }; + } + + std::string_view getDeviceName() const override + { + return "SteelSeries Arctis GameBuds"sv; + } + + constexpr int getCapabilities() const override + { + return B(CAP_BATTERY_STATUS); + } + + constexpr capability_detail getCapabilityDetail(enum capabilities cap) const override + { + switch (cap) { + case CAP_BATTERY_STATUS: + return { .usagepage = 0xffc0, .usageid = 0x1, .interface_id = 3 }; + default: + return HIDDevice::getCapabilityDetail(cap); + } + } + + Result getBattery(hid_device* device_handle) override + { + auto status_result = readDeviceStatus(device_handle); // sends 0x00 0xb0 + if (!status_result) { + return status_result.error(); + } + auto& data = *status_result; + + if (data.size() <= RIGHT_BUD_BATTERY) { + return DeviceError::protocolError("Battery status response too short"); + } + + // Consider only buds that are out of the case as docked bud reports 0% + std::optional level; + if (data[LEFT_BUD_STATUS] == BUD_STATUS_ACTIVE) { + level = data[LEFT_BUD_BATTERY]; + } + if (data[RIGHT_BUD_STATUS] == BUD_STATUS_ACTIVE) { + int right = data[RIGHT_BUD_BATTERY]; + level = level ? std::min(*level, right) : right; + } + + if (!level) { + return DeviceError::deviceOffline("Earbuds are docked in the charging case"); + } + + return BatteryResult { + .level_percent = std::min(*level, 100), + .status = BATTERY_AVAILABLE, + .raw_data = data + }; + } +}; + +} // namespace headsetcontrol