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 Arctis Nova 3 support #319

Merged
merged 3 commits into from
Dec 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
44 changes: 24 additions & 20 deletions src/device.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,32 @@

const char* const capabilities_str[NUM_CAPABILITIES]
= {
[CAP_SIDETONE] = "sidetone",
[CAP_BATTERY_STATUS] = "battery status",
[CAP_NOTIFICATION_SOUND] = "notification sound",
[CAP_LIGHTS] = "lights",
[CAP_INACTIVE_TIME] = "inactive time",
[CAP_CHATMIX_STATUS] = "chatmix",
[CAP_VOICE_PROMPTS] = "voice prompts",
[CAP_ROTATE_TO_MUTE] = "rotate to mute",
[CAP_EQUALIZER_PRESET] = "equalizer preset",
[CAP_EQUALIZER] = "equalizer",
[CAP_SIDETONE] = "sidetone",
[CAP_BATTERY_STATUS] = "battery status",
[CAP_NOTIFICATION_SOUND] = "notification sound",
[CAP_LIGHTS] = "lights",
[CAP_INACTIVE_TIME] = "inactive time",
[CAP_CHATMIX_STATUS] = "chatmix",
[CAP_VOICE_PROMPTS] = "voice prompts",
[CAP_ROTATE_TO_MUTE] = "rotate to mute",
[CAP_EQUALIZER_PRESET] = "equalizer preset",
[CAP_EQUALIZER] = "equalizer",
[CAP_MICROPHONE_MUTE_LED_BRIGHTNESS] = "microphone mute led brightness",
[CAP_MICROPHONE_VOLUME] = "microphone volume"
};

const char capabilities_str_short[NUM_CAPABILITIES]
= {
[CAP_SIDETONE] = 's',
[CAP_BATTERY_STATUS] = 'b',
[CAP_NOTIFICATION_SOUND] = 'n',
[CAP_LIGHTS] = 'l',
[CAP_INACTIVE_TIME] = 'i',
[CAP_CHATMIX_STATUS] = 'm',
[CAP_VOICE_PROMPTS] = 'v',
[CAP_ROTATE_TO_MUTE] = 'r',
[CAP_EQUALIZER_PRESET] = 'p',
[CAP_EQUALIZER] = 'e'
[CAP_SIDETONE] = 's',
[CAP_BATTERY_STATUS] = 'b',
[CAP_NOTIFICATION_SOUND] = 'n',
[CAP_LIGHTS] = 'l',
[CAP_INACTIVE_TIME] = 'i',
[CAP_CHATMIX_STATUS] = 'm',
[CAP_VOICE_PROMPTS] = 'v',
[CAP_ROTATE_TO_MUTE] = 'r',
[CAP_EQUALIZER_PRESET] = 'p',
[CAP_EQUALIZER] = 'e',
[CAP_MICROPHONE_MUTE_LED_BRIGHTNESS] = 't',
[CAP_MICROPHONE_VOLUME] = 'o'
};
34 changes: 33 additions & 1 deletion src/device.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ enum capabilities {
CAP_ROTATE_TO_MUTE,
CAP_EQUALIZER_PRESET,
CAP_EQUALIZER,
CAP_MICROPHONE_MUTE_LED_BRIGHTNESS,
CAP_MICROPHONE_VOLUME,
NUM_CAPABILITIES
};

Expand Down Expand Up @@ -209,7 +211,7 @@ struct device {
* @param num The preset number, between 0 - 3
*
* @returns > 0 on success
* HSC_OUT_OF_BOUNDS on preset parmeter out of range
* HSC_OUT_OF_BOUNDS on preset parameter out of range
* specific to this hardware
* -1 HIDAPI error
*/
Expand All @@ -230,4 +232,34 @@ struct device {
* -1 HIDAPI error
*/
int (*send_equalizer)(hid_device* hid_device, struct equalizer_settings* settings);

/** @brief Function pointer for setting headset microphone mute LED brightness
*
* Forwards the request to the device specific implementation
*
* @param device_handle The hidapi handle. Must be the same
* device as defined here (same ids)
* @param num The level number, between 0 - 3
*
* @returns > 0 on success
* HSC_OUT_OF_BOUNDS on level parameter out of range
* specific to this hardware
* -1 HIDAPI error
*/
int (*send_microphone_mute_led_brightness)(hid_device* hid_device, uint8_t num);

/** @brief Function pointer for setting headset microphone volume
*
* Forwards the request to the device specific implementation
*
* @param device_handle The hidapi handle. Must be the same
* device as defined here (same ids)
* @param num The volume number, between 0 - 128
*
* @returns > 0 on success
* HSC_OUT_OF_BOUNDS on volume parameter out of range
* specific to this hardware
* -1 HIDAPI error
*/
int (*send_microphone_volume)(hid_device* hid_device, uint8_t num);
};
12 changes: 7 additions & 5 deletions src/device_registry.c
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@
#include "devices/steelseries_arctis_7.h"
#include "devices/steelseries_arctis_7_plus.h"
#include "devices/steelseries_arctis_9.h"
#include "devices/steelseries_arctis_nova_3.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 21
#define NUMDEVICES 22

// array of pointers to device
static struct device*(devicelist[NUMDEVICES]);
Expand All @@ -48,10 +49,11 @@ void init_devices()
arctis_7_plus_init(&devicelist[14]);
cflight_init(&devicelist[15]);
g535_init(&devicelist[16]);
arctis_nova_7_init(&devicelist[17]);
calphaw_init(&devicelist[18]);
arctis_nova_pro_wireless_init(&devicelist[19]);
gpro_x2_init(&devicelist[20]);
arctis_nova_3_init(&devicelist[17]);
arctis_nova_7_init(&devicelist[18]);
calphaw_init(&devicelist[19]);
arctis_nova_pro_wireless_init(&devicelist[20]);
gpro_x2_init(&devicelist[21]);
}

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 @@ -23,6 +23,8 @@ set(SOURCE_FILES ${SOURCE_FILES}
${CMAKE_CURRENT_SOURCE_DIR}/steelseries_arctis_7_plus.c
${CMAKE_CURRENT_SOURCE_DIR}/steelseries_arctis_9.c
${CMAKE_CURRENT_SOURCE_DIR}/steelseries_arctis_9.h
${CMAKE_CURRENT_SOURCE_DIR}/steelseries_arctis_nova_3.h
${CMAKE_CURRENT_SOURCE_DIR}/steelseries_arctis_nova_3.c
${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
Expand Down
162 changes: 162 additions & 0 deletions src/devices/steelseries_arctis_nova_3.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
#include "../device.h"

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

#define MSG_SIZE 64

static struct device device_arctis;

#define ID_ARCTIS_NOVA_3 0x12ec

#define EQUALIZER_BANDS_SIZE 6

static const uint16_t PRODUCT_IDS[] = { ID_ARCTIS_NOVA_3 };
static const uint8_t SAVE_DATA[MSG_SIZE] = { 0x06, 0x09 }; // Command to save settings to headset

static int arctis_nova_3_send_sidetone(hid_device* device_handle, uint8_t num);
static int arctis_nova_3_send_equalizer_preset(hid_device* device_handle, uint8_t num);
static int arctis_nova_3_send_equalizer(hid_device* device_handle, struct equalizer_settings* settings);
static int arctis_nova_3_send_microphone_mute_led_brightness(hid_device* device_handle, uint8_t num);
static int arctis_nova_3_send_microphone_volume(hid_device* device_handle, uint8_t num);

void arctis_nova_3_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 3", sizeof(device_arctis.device_name));

device_arctis.capabilities = B(CAP_SIDETONE) | B(CAP_EQUALIZER_PRESET) | B(CAP_EQUALIZER) | B(CAP_MICROPHONE_MUTE_LED_BRIGHTNESS) | B(CAP_MICROPHONE_VOLUME);
// 0xc (3), 0xffc0 (4), 0xff00 (4)
device_arctis.capability_details[CAP_SIDETONE] = (struct capability_detail) { .usagepage = 0xffc0, .usageid = 0x1, .interface = 4 };
device_arctis.capability_details[CAP_EQUALIZER_PRESET] = (struct capability_detail) { .usagepage = 0xffc0, .usageid = 0x1, .interface = 4 };
device_arctis.capability_details[CAP_EQUALIZER] = (struct capability_detail) { .usagepage = 0xffc0, .usageid = 0x1, .interface = 4 };
device_arctis.capability_details[CAP_MICROPHONE_MUTE_LED_BRIGHTNESS] = (struct capability_detail) { .usagepage = 0xffc0, .usageid = 0x1, .interface = 4 };
device_arctis.capability_details[CAP_MICROPHONE_VOLUME] = (struct capability_detail) { .usagepage = 0xffc0, .usageid = 0x1, .interface = 4 };

device_arctis.send_sidetone = &arctis_nova_3_send_sidetone;
device_arctis.send_equalizer_preset = &arctis_nova_3_send_equalizer_preset;
device_arctis.send_equalizer = &arctis_nova_3_send_equalizer;
device_arctis.send_microphone_mute_led_brightness = &arctis_nova_3_send_microphone_mute_led_brightness;
device_arctis.send_microphone_volume = &arctis_nova_3_send_microphone_volume;

*device = &device_arctis;
}

static int arctis_nova_3_send_sidetone(hid_device* device_handle, uint8_t num)
{
// This headset only supports 4 levels of sidetone volume, but we allow a full range of 0-128 for it. Map the volume to the correct numbers.
if (num < 26) {
num = 0x0;
} else if (num < 51) {
num = 0x1;
} else if (num < 76) {
num = 0x2;
} else {
num = 0x3;
}

uint8_t data[MSG_SIZE] = { 0x06, 0x39, num };
hid_send_feature_report(device_handle, data, MSG_SIZE);

return hid_send_feature_report(device_handle, SAVE_DATA, MSG_SIZE);
}

static int arctis_nova_3_send_equalizer_preset(hid_device* device_handle, uint8_t num)
{
// This headset supports only 4 presets:
// Flat (default), Bass Boost, Smiley, Focus

switch (num) {
case 0: {
uint8_t flat[MSG_SIZE] = { 0x06, 0x33, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14 };
hid_send_feature_report(device_handle, flat, MSG_SIZE);

return hid_send_feature_report(device_handle, SAVE_DATA, MSG_SIZE);
}
case 1: {
uint8_t bass[MSG_SIZE] = { 0x06, 0x33, 0x1c, 0x19, 0x11, 0x14, 0x14, 0x14 };
hid_send_feature_report(device_handle, bass, MSG_SIZE);

return hid_send_feature_report(device_handle, SAVE_DATA, MSG_SIZE);
}
case 2: {
uint8_t smiley[MSG_SIZE] = { 0x06, 0x33, 0x1a, 0x17, 0x0f, 0x12, 0x17, 0x1a };
hid_send_feature_report(device_handle, smiley, MSG_SIZE);

return hid_send_feature_report(device_handle, SAVE_DATA, MSG_SIZE);
}
case 3: {
uint8_t focus[MSG_SIZE] = { 0x06, 0x33, 0x0c, 0x0d, 0x11, 0x18, 0x1c, 0x14 };
hid_send_feature_report(device_handle, focus, MSG_SIZE);

return hid_send_feature_report(device_handle, SAVE_DATA, MSG_SIZE);
}
default: {
printf("Device only supports 0-3 range for presets.\n");
return HSC_OUT_OF_BOUNDS;
}
}
}

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

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_send_feature_report(device_handle, data, MSG_SIZE);
}

static int arctis_nova_3_send_microphone_mute_led_brightness(hid_device* device_handle, uint8_t num)
{
// This headset only supports 3 levels:
// Off, low, medium (default), high

uint8_t brightness[MSG_SIZE] = { 0x06, 0xae, num };
hid_send_feature_report(device_handle, brightness, MSG_SIZE);

return hid_send_feature_report(device_handle, SAVE_DATA, MSG_SIZE);
}

static int arctis_nova_3_send_microphone_volume(hid_device* device_handle, uint8_t num)
{
// This headset only supports 10 levels of microphone volume, but we allow a full range of 0-128 for it. Map the volume to the correct numbers.
if (num < 13) {
num = 0x00;
} else if (num < 25) {
num = 0x01;
} else if (num < 37) {
num = 0x02;
} else if (num < 49) {
num = 0x03;
} else if (num < 61) {
num = 0x04;
} else if (num < 73) {
num = 0x05;
} else if (num < 85) {
num = 0x06;
} else if (num < 97) {
num = 0x07;
} else if (num < 109) {
num = 0x08;
} else if (num < 121) {
num = 0x09;
} else {
num = 0x0a;
}

uint8_t volume[MSG_SIZE] = { 0x06, 0x37, num };
hid_send_feature_report(device_handle, volume, MSG_SIZE);

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

void arctis_nova_3_init(struct device** device);
40 changes: 40 additions & 0 deletions src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,14 @@ static int handle_feature(struct device* device_found, hid_device** device_handl
PRINT_INFO("Successfully set equalizer!\n");
break;

case CAP_MICROPHONE_MUTE_LED_BRIGHTNESS:
ret = device_found->send_microphone_mute_led_brightness(*device_handle, *(int*)param);
break;

case CAP_MICROPHONE_VOLUME:
ret = device_found->send_microphone_volume(*device_handle, *(int*)param);
break;

case NUM_CAPABILITIES:
ret = -99; // silence warning

Expand Down Expand Up @@ -294,6 +302,8 @@ int main(int argc, char* argv[])
int rotate_to_mute = -1;
int print_capabilities = -1;
int equalizer_preset = -1;
int microphone_mute_led_brightness = -1;
int microphone_volume = -1;
int dev_mode = 0;
int follow = 0;
unsigned follow_sec = 2;
Expand All @@ -310,6 +320,8 @@ int main(int argc, char* argv[])
{ "help", no_argument, NULL, 'h' },
{ "equalizer", required_argument, NULL, 'e' },
{ "equalizer-preset", required_argument, NULL, 'p' },
{ "microphone-mute-led-brightness", required_argument, NULL, 0 },
{ "microphone-volume", required_argument, NULL, 0 },
{ "inactive-time", required_argument, NULL, 'i' },
{ "light", required_argument, NULL, 'l' },
{ "follow", optional_argument, NULL, 'f' },
Expand Down Expand Up @@ -444,6 +456,8 @@ int main(int argc, char* argv[])
printf(" -r, --rotate-to-mute 0|1\tTurn rotate to mute feature on or off (0 = off, 1 = on)\n");
printf(" -e, --equalizer string\tSets equalizer to specified curve, string must contain band values specific to the device (hex or decimal) delimited by spaces, or commas, or new-lines e.g \"0x18, 0x18, 0x18, 0x18, 0x18\".\n");
printf(" -p, --equalizer-preset number\tSets equalizer preset, number must be between 0 and 3, 0 sets the default\n");
printf(" --microphone-mute-led-brightness number\tSets microphone mute LED brightness, number must be between 0 and 3\n");
printf(" --microphone-volume number\tSets microphone volume, number must be between 0 and 128\n");
printf(" -f, --follow [secs timeout]\tRe-run the commands after the specified seconds timeout or 2 by default\n");
printf("\n");
printf(" --timeout 0-100000\t\tSpecifies the timeout in ms for reading data from device (default 5000)\n");
Expand All @@ -465,6 +479,22 @@ int main(int argc, char* argv[])
return 1;
}
break;
} else if (strcmp(opts[option_index].name, "microphone-mute-led-brightness") == 0) {
microphone_mute_led_brightness = strtol(optarg, &endptr, 10);

if (*endptr != '\0' || endptr == optarg || microphone_mute_led_brightness < 0 || microphone_mute_led_brightness > 3) {
printf("Usage: %s --microphone-mute-led-brightness 0-3\n", argv[0]);
return 1;
}
break;
} else if (strcmp(opts[option_index].name, "microphone-volume") == 0) {
microphone_volume = strtol(optarg, &endptr, 10);

if (*endptr != '\0' || endptr == optarg || microphone_volume < 0 || microphone_volume > 128) {
printf("Usage: %s --microphone-volume 0-128\n", argv[0]);
return 1;
}
break;
}
// fall through
default:
Expand Down Expand Up @@ -546,6 +576,16 @@ int main(int argc, char* argv[])
goto error;
}

if (microphone_mute_led_brightness != -1) {
if ((error = handle_feature(&device_found, &device_handle, &hid_path, CAP_MICROPHONE_MUTE_LED_BRIGHTNESS, &microphone_mute_led_brightness)) != 0)
goto error;
}

if (microphone_volume != -1) {
if ((error = handle_feature(&device_found, &device_handle, &hid_path, CAP_MICROPHONE_VOLUME, &microphone_volume)) != 0)
goto error;
}

if (equalizer != NULL) {
error = handle_feature(&device_found, &device_handle, &hid_path, CAP_EQUALIZER, equalizer);
free(equalizer);
Expand Down
Loading