Skip to content

Commit

Permalink
Add Steelseries Arctis Nova Pro Wireless support (#306)
Browse files Browse the repository at this point in the history
  • Loading branch information
Anden authored Sep 27, 2023
1 parent 5515590 commit 1194d60
Show file tree
Hide file tree
Showing 5 changed files with 258 additions and 1 deletion.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ talking. This differs from a simple loopback via PulseAudio as you won't have an
- Sidetone, Battery, Inactive time, Chat-Mix level
- SteelSeries Arctis Pro Wireless
- Sidetone, Battery, Inactive time
- SteelSeries Arctis Nova Pro Wireless
- Sidetone, Battery, Inactive time
- Logitech G PRO
- Sidetone, Battery, Inactive time
- Logitech Zone Wired/Zone 750
Expand Down
4 changes: 3 additions & 1 deletion src/device_registry.c
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@
#include "devices/steelseries_arctis_7_plus.h"
#include "devices/steelseries_arctis_9.h"
#include "devices/steelseries_arctis_nova_7.h"
#include "devices/steelseries_arctis_nova_pro_wireless.h"
#include "devices/steelseries_arctis_pro_wireless.h"

#include <string.h>

#define NUMDEVICES 19
#define NUMDEVICES 20

// array of pointers to device
static struct device*(devicelist[NUMDEVICES]);
Expand All @@ -48,6 +49,7 @@ void init_devices()
g535_init(&devicelist[16]);
arctis_nova_7_init(&devicelist[17]);
calphaw_init(&devicelist[18]);
arctis_nova_pro_wireless_init(&devicelist[19]);
}

int get_device(struct device* device_found, uint16_t idVendor, uint16_t idProduct)
Expand Down
2 changes: 2 additions & 0 deletions src/devices/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ set(SOURCE_FILES ${SOURCE_FILES}
${CMAKE_CURRENT_SOURCE_DIR}/steelseries_arctis_9.h
${CMAKE_CURRENT_SOURCE_DIR}/steelseries_arctis_nova_7.h
${CMAKE_CURRENT_SOURCE_DIR}/steelseries_arctis_nova_7.c
${CMAKE_CURRENT_SOURCE_DIR}/steelseries_arctis_nova_pro_wireless.c
${CMAKE_CURRENT_SOURCE_DIR}/steelseries_arctis_nova_pro_wireless.h
${CMAKE_CURRENT_SOURCE_DIR}/steelseries_arctis_pro_wireless.c
${CMAKE_CURRENT_SOURCE_DIR}/steelseries_arctis_pro_wireless.h
${CMAKE_CURRENT_SOURCE_DIR}/roccat_elo_7_1_air.h
Expand Down
248 changes: 248 additions & 0 deletions src/devices/steelseries_arctis_nova_pro_wireless.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
#include "../device.h"
#include "../utility.h"

#include <stdio.h>
#include <string.h>

static struct device device_arctis;

enum { ID_ARCTIS_NOVA_PRO_WIRELESS_BASE_STATION = 0x12e0 };

enum {
MSG_SIZE = 31,
STATUS_BUF_SIZE = 128,
};

enum {
BATTERY_MIN = 0x00,
BATTERY_MAX = 0x08,
};

enum headset_status {
HEADSET_OFFLINE = 0x01,
HEADSET_CABLE_CHARGING = 0x02,
HEADSET_ONLINE = 0x08,
};

enum inactive_time {
INACTIVE_TIME_DISABLED = 0,
INACTIVE_TIME_1_MINUTES = 1,
INACTIVE_TIME_5_MINUTES = 2,
INACTIVE_TIME_10_MINUTES = 3,
INACTIVE_TIME_15_MINUTES = 4,
INACTIVE_TIME_30_MINUTES = 5,
INACTIVE_TIME_60_MINUTES = 6,
};

enum mic_mute_led_brightness {
LED_MIN = 1,
LED_MAX = 10,
};

enum {
EQUALIZER_PRESET_CUSTOM = 4,
EQUALIZER_BANDS_SIZE = 10,
};

static const uint16_t PRODUCT_IDS[] = { ID_ARCTIS_NOVA_PRO_WIRELESS_BASE_STATION };

static int set_sidetone(hid_device* device_handle, uint8_t num);
static int get_battery(hid_device* device_handle);
static int set_lights(hid_device* device_handle, uint8_t on);
static int set_inactive_time(hid_device* device_handle, uint8_t minutes);
static int set_equalizer_preset(hid_device* device_handle, uint8_t num);
static int set_equalizer(hid_device* device_handle, struct equalizer_settings* settings);

static int read_device_status(hid_device* device_handle, unsigned char* data_read);
static int save_state(hid_device* device_handle);

void arctis_nova_pro_wireless_init(struct device** device)
{
device_arctis.idVendor = VENDOR_STEELSERIES;
device_arctis.idProductsSupported = PRODUCT_IDS;
device_arctis.numIdProducts = sizeof(PRODUCT_IDS) / sizeof(PRODUCT_IDS[0]);

strncpy(device_arctis.device_name, "SteelSeries Arctis Nova Pro Wireless", sizeof(device_arctis.device_name));

device_arctis.capabilities = B(CAP_SIDETONE) | B(CAP_BATTERY_STATUS) | B(CAP_LIGHTS)
| B(CAP_INACTIVE_TIME) | B(CAP_EQUALIZER) | B(CAP_EQUALIZER_PRESET);

device_arctis.capability_details[CAP_SIDETONE] = (struct capability_detail) { .interface = 4 };
device_arctis.capability_details[CAP_BATTERY_STATUS] = (struct capability_detail) { .interface = 4 };
device_arctis.capability_details[CAP_LIGHTS] = (struct capability_detail) { .interface = 4 };
device_arctis.capability_details[CAP_INACTIVE_TIME] = (struct capability_detail) { .interface = 4 };
device_arctis.capability_details[CAP_EQUALIZER] = (struct capability_detail) { .interface = 4 };
device_arctis.capability_details[CAP_EQUALIZER_PRESET] = (struct capability_detail) { .interface = 4 };

device_arctis.send_sidetone = &set_sidetone;
device_arctis.request_battery = &get_battery;
device_arctis.switch_lights = &set_lights;
device_arctis.send_inactive_time = &set_inactive_time;
device_arctis.send_equalizer_preset = &set_equalizer_preset;
device_arctis.send_equalizer = &set_equalizer;

*device = &device_arctis;
}

static int set_sidetone(hid_device* device_handle, uint8_t num)
{
if (num > 3) {
fprintf(stderr, "Device only supports 0-3 range for sidetone (off, low, med, high).\n");
return HSC_OUT_OF_BOUNDS;
}

const unsigned char data_request[MSG_SIZE] = { 0x06, 0x39, num };

int res = hid_write(device_handle, data_request, MSG_SIZE);
if (res < 0)
return res;

return save_state(device_handle);
}

static int get_battery(hid_device* device_handle)
{
// read device info
unsigned char data_read[STATUS_BUF_SIZE];
int res = read_device_status(device_handle, data_read);

if (res < 0)
return res;

if (res == 0)
return HSC_READ_TIMEOUT;

if (res < 16)
return HSC_ERROR;

uint8_t status = data_read[15];
switch (status) {
case HEADSET_OFFLINE:
return BATTERY_UNAVAILABLE;
case HEADSET_CABLE_CHARGING:
return BATTERY_CHARGING;
case HEADSET_ONLINE:
break;
default:
fprintf(stderr, "Unknown headset status 0x%.2x.\n", status);
return HSC_ERROR;
}

int bat = data_read[6];

return map(bat, BATTERY_MIN, BATTERY_MAX, 0, 100);
}

static int set_lights(hid_device* device_handle, uint8_t on)
{
uint8_t led_strength = map(on, 0, 1, LED_MIN, LED_MAX);
uint8_t data[MSG_SIZE] = { 0x06, 0xbf, led_strength };

int res = hid_write(device_handle, data, MSG_SIZE);
if (res < 0)
return res;

return save_state(device_handle);
}

static int set_inactive_time(hid_device* device_handle, uint8_t minutes)
{
// Cannot set minutes directly, round to nearest
uint8_t num = INACTIVE_TIME_DISABLED;
if (minutes >= 45) {
num = INACTIVE_TIME_60_MINUTES;
} else if (minutes >= 23) {
num = INACTIVE_TIME_30_MINUTES;
} else if (minutes >= 13) {
num = INACTIVE_TIME_15_MINUTES;
} else if (minutes >= 8) {
num = INACTIVE_TIME_10_MINUTES;
} else if (minutes >= 3) {
num = INACTIVE_TIME_5_MINUTES;
} else if (minutes > 0) {
num = INACTIVE_TIME_1_MINUTES;
}

uint8_t data[MSG_SIZE] = { 0x06, 0xc1, num };

int res = hid_write(device_handle, data, MSG_SIZE);
if (res < 0)
return res;

return save_state(device_handle);
}

static int set_equalizer_preset(hid_device* device_handle, uint8_t num)
{
uint8_t data[MSG_SIZE] = { 0x06, 0x2e, num };

int res = hid_write(device_handle, data, MSG_SIZE);
if (res < 0)
return res;

return save_state(device_handle);
}

static int set_equalizer(hid_device* device_handle, struct equalizer_settings* settings)
{
if (settings->size != EQUALIZER_BANDS_SIZE) {
fprintf(stderr, "Device only supports %d bands.\n", EQUALIZER_BANDS_SIZE);
return HSC_OUT_OF_BOUNDS;
}

set_equalizer_preset(device_handle, EQUALIZER_PRESET_CUSTOM);

uint8_t data[MSG_SIZE] = { 0x06, 0x33 };
for (int i = 0; i < settings->size; i++) {
data[i + 2] = (uint8_t)settings->bands_values[i];
}

return hid_write(device_handle, data, MSG_SIZE);
}

/**
* Device status:
* 0-1: packet id (0x06 0xb0)
* 2: BT Default / Bluetooth powerup state: 0 (Off), 1 (On)
* 3: Bluetooth auto mute: 0 (Off), 1 (-12db), 2 (On),
* 4: Bluetooth power status: 1 (Off), 4 (On),
* 5: Bluetooth connection: 0 (Off), 1 (Connected), 2 (Not connected)
* 6: Headset battery charge: 0-8 (0%-100%)
* 7: Charge slot battery charge: 0-8 (0%-100%)
* 8: Transparent noise cancelling level: 0-10
* 9: Mic status: 0 (Unmuted), 1 (Muted)
* 10: Noise cancelling: 0 (Off), 1 (Transparent on), 2 (ANC on)
* 11: Mic led brightness: 0-10
* 12: Auto off / Inactive time: See `enum inactive_time`
* 13: 2.4ghz wireless mode: 0 (Speed), 1 (Range)
* 14: Headset pairing: 1 (Not paired), 4 (Paired but offline), 8 (Connected)
* 15: Headset power status: See `enum headset_status`
*/
static int read_device_status(hid_device* device_handle, unsigned char* data_read)
{
unsigned char data_request[MSG_SIZE] = { 0x06, 0xb0 };

int res = hid_write(device_handle, data_request, MSG_SIZE);
if (res < 0)
return res;

// read device info
res = hid_read_timeout(device_handle, data_read, STATUS_BUF_SIZE, hsc_device_timeout);

if (res < 0)
return res;

if (res < 2 || !(data_read[0] == 0x06 && data_read[1] == 0xb0)) {
fprintf(stderr, "Wrong id for device status packet\n");
return HSC_ERROR;
}

return res;
}

static int save_state(hid_device* device_handle)
{
uint8_t data[MSG_SIZE] = { 0x06, 0x09 };

return hid_write(device_handle, data, MSG_SIZE);
}
3 changes: 3 additions & 0 deletions src/devices/steelseries_arctis_nova_pro_wireless.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#pragma once

void arctis_nova_pro_wireless_init(struct device** device);

0 comments on commit 1194d60

Please sign in to comment.