Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Steelseries Arctis Nova Pro Wireless support #306

Merged
merged 1 commit into from
Sep 27, 2023
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
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);
Loading