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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | | | | | | | | | | | | |
Expand Down
2 changes: 2 additions & 0 deletions lib/device_registry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -123,6 +124,7 @@ void DeviceRegistry::initialize()
registerDevice(std::make_unique<SteelSeriesArctis7Plus>());
registerDevice(std::make_unique<SteelSeriesArctisNovaProWireless>());
registerDevice(std::make_unique<SteelSeriesArctisNova3PWireless>());
registerDevice(std::make_unique<SteelSeriesArctisGamebuds>());

// HyperX devices
registerDevice(std::make_unique<HyperXCloudAlphaWireless>());
Expand Down
102 changes: 102 additions & 0 deletions lib/devices/steelseries_arctis_gamebuds.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#pragma once

#include "../result_types.hpp"
#include "protocols/steelseries_protocol.hpp"
#include <algorithm>
#include <array>
#include <optional>
#include <string_view>

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<SteelSeriesArctisGamebuds> {
public:
static constexpr std::array<uint16_t, 1> 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<uint16_t> 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<BatteryResult> 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<int> 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
Loading