Skip to content

Commit

Permalink
Add Arctis Nova 3 support (#319)
Browse files Browse the repository at this point in the history
* Add Arctis Nova 3 support

Signed-off-by: AnErrupTion <[email protected]>

* Fix code style

Signed-off-by: AnErrupTion <[email protected]>

* Don't use short letters for new capabilities

Signed-off-by: AnErrupTion <[email protected]>

---------

Signed-off-by: AnErrupTion <[email protected]>
  • Loading branch information
AnErrupTion authored Dec 27, 2023
1 parent e08c0b7 commit 5b9ed65
Show file tree
Hide file tree
Showing 7 changed files with 271 additions and 26 deletions.
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

0 comments on commit 5b9ed65

Please sign in to comment.