[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Message-ID: <20251228122025.154682-5-srimanachanta@gmail.com>
Date: Sun, 28 Dec 2025 07:20:25 -0500
From: Sriman Achanta <srimanachanta@...il.com>
To: Jiri Kosina <jikos@...nel.org>,
Benjamin Tissoires <bentiss@...nel.org>,
linux-input@...r.kernel.org,
linux-kernel@...r.kernel.org
Cc: Sriman Achanta <srimanachanta@...il.com>
Subject: [PATCH 4/4] HID: steelseries: Add support for Arctis headset lineup
Add full support for the SteelSeries Arctis wireless gaming headset
lineup, extending the driver from basic support for 2 models (Arctis 1
and 9) to comprehensive support for 25+ models across all Arctis
generations.
This is a major restructure of the hid-steelseries driver that replaces
the previous minimal implementation with a unified, capability-based
architecture.
Architecture changes:
- Introduce steelseries_device_info structure to define per-device
capabilities, interface bindings, and metadata
- Implement capability system (SS_CAP_*) for feature detection: battery,
sidetone, chatmix, microphone controls, volume limiting, and Bluetooth
settings
- Add interface binding logic to correctly bind to HID control
interfaces on multi-interface USB devices using two modes:
* Mode 0: Bind to first enumerated interface (for Arctis 9, Pro)
* Mode 1: Bind to specific interface via bitmask (for other models)
- Create device info tables for all supported Arctis models with their
specific capabilities and interface requirements
Features added:
- Battery monitoring: Implement power_supply integration with periodic
polling and device-specific battery request protocols for all model
families. Supports battery capacity reporting, charging status, and
wireless connection tracking.
- Sidetone control: Sysfs attribute to adjust microphone monitoring
volume (0-128) with device-specific mapping to hardware ranges
- Auto-sleep timeout: Configure inactivity timeout (0-90 minutes) before
headset enters standby mode
- ChatMix reporting: Read-only sysfs attribute reporting game/chat audio
balance from physical dial on supported models
- Microphone controls:
* Mute LED brightness (0-3 or 0-10 depending on model)
* Internal microphone gain/volume (0-128)
- Volume limiter: Enable/disable EU hearing protection (max volume cap)
- Bluetooth controls (Nova 7 series):
* Auto-enable Bluetooth on power-on
* Configure game audio attenuation during BT calls
Implementation details:
- Device-specific raw_event parsing for battery updates across different
HID report formats (8-byte, 12-byte, 64-byte, 128-byte)
- Helper functions for HID feature reports and output reports to handle
different communication methods across device families
- Attribute visibility system to expose only relevant controls for each
device based on capability flags
- Save-state commands after configuration changes to persist settings
across power cycles
The legacy SRW-S1 racing wheel controller support is preserved
unchanged.
Tested on Arctis Nova 7 (0x2202). All other implementation details are
based on the reverse engineering done in the HeadsetControl library
(abe3ac8).
Signed-off-by: Sriman Achanta <srimanachanta@...il.com>
---
drivers/hid/hid-steelseries.c | 2061 ++++++++++++++++++++++++++++-----
1 file changed, 1740 insertions(+), 321 deletions(-)
diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index f98435631aa1..a0046fbc830b 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -4,9 +4,7 @@
*
* Copyright (c) 2013 Simon Wood
* Copyright (c) 2023 Bastien Nocera
- */
-
-/*
+ * Copyright (c) 2025 Sriman Achanta
*/
#include <linux/device.h>
@@ -14,124 +12,144 @@
#include <linux/module.h>
#include <linux/usb.h>
#include <linux/leds.h>
+#include <linux/power_supply.h>
+#include <linux/workqueue.h>
+#include <linux/spinlock.h>
#include "hid-ids.h"
-#define STEELSERIES_SRWS1 BIT(0)
-#define STEELSERIES_ARCTIS_1 BIT(1)
-#define STEELSERIES_ARCTIS_9 BIT(2)
+#define SS_CAP_SIDETONE BIT(0)
+#define SS_CAP_BATTERY BIT(1)
+#define SS_CAP_INACTIVE_TIME BIT(2)
+#define SS_CAP_CHATMIX BIT(3)
+#define SS_CAP_MIC_MUTE_LED BIT(4)
+#define SS_CAP_MIC_VOLUME BIT(5)
+#define SS_CAP_VOLUME_LIMITER BIT(6)
+#define SS_CAP_BT_POWER_ON BIT(7)
+#define SS_CAP_BT_CALL_VOL BIT(8)
+
+/* Legacy quirk flag for SRW-S1 */
+#define STEELSERIES_SRWS1 BIT(0)
+
+struct steelseries_device_info {
+ u16 product_id;
+ const char *name;
+ u8 interface_binding_mode; /* 0 = first enumerated, 1 = specific interface(s) */
+ u16 valid_interfaces; /* Bitmask when mode = 1, ignored when mode = 0 */
+ unsigned long capabilities;
+};
struct steelseries_device {
struct hid_device *hdev;
- unsigned long quirks;
-
- struct delayed_work battery_work;
- spinlock_t lock;
- bool removed;
+ const struct steelseries_device_info *info;
+ /* Battery subsystem */
struct power_supply_desc battery_desc;
struct power_supply *battery;
- uint8_t battery_capacity;
+ struct delayed_work battery_work;
+ u8 battery_capacity;
bool headset_connected;
bool battery_charging;
+
+ /* Synchronization */
+ spinlock_t lock;
+ bool removed;
+
+ /* Cached chatmix value (read-only from status) */
+ int chatmix_level;
};
#if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
- (IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
+ (IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
#define SRWS1_NUMBER_LEDS 15
struct steelseries_srws1_data {
__u16 led_state;
- /* the last element is used for setting all leds simultaneously */
struct led_classdev *led[SRWS1_NUMBER_LEDS + 1];
};
#endif
-/* Fixed report descriptor for Steelseries SRW-S1 wheel controller
- *
- * The original descriptor hides the sensitivity and assists dials
- * a custom vendor usage page. This inserts a patch to make them
- * appear in the 'Generic Desktop' usage.
- */
-
+/* Fixed report descriptor for Steelseries SRW-S1 wheel controller */
static const __u8 steelseries_srws1_rdesc_fixed[] = {
-0x05, 0x01, /* Usage Page (Desktop) */
-0x09, 0x08, /* Usage (MultiAxis), Changed */
-0xA1, 0x01, /* Collection (Application), */
-0xA1, 0x02, /* Collection (Logical), */
-0x95, 0x01, /* Report Count (1), */
-0x05, 0x01, /* Changed Usage Page (Desktop), */
-0x09, 0x30, /* Changed Usage (X), */
-0x16, 0xF8, 0xF8, /* Logical Minimum (-1800), */
-0x26, 0x08, 0x07, /* Logical Maximum (1800), */
-0x65, 0x14, /* Unit (Degrees), */
-0x55, 0x0F, /* Unit Exponent (15), */
-0x75, 0x10, /* Report Size (16), */
-0x81, 0x02, /* Input (Variable), */
-0x09, 0x31, /* Changed Usage (Y), */
-0x15, 0x00, /* Logical Minimum (0), */
-0x26, 0xFF, 0x03, /* Logical Maximum (1023), */
-0x75, 0x0C, /* Report Size (12), */
-0x81, 0x02, /* Input (Variable), */
-0x09, 0x32, /* Changed Usage (Z), */
-0x15, 0x00, /* Logical Minimum (0), */
-0x26, 0xFF, 0x03, /* Logical Maximum (1023), */
-0x75, 0x0C, /* Report Size (12), */
-0x81, 0x02, /* Input (Variable), */
-0x05, 0x01, /* Usage Page (Desktop), */
-0x09, 0x39, /* Usage (Hat Switch), */
-0x25, 0x07, /* Logical Maximum (7), */
-0x35, 0x00, /* Physical Minimum (0), */
-0x46, 0x3B, 0x01, /* Physical Maximum (315), */
-0x65, 0x14, /* Unit (Degrees), */
-0x75, 0x04, /* Report Size (4), */
-0x95, 0x01, /* Report Count (1), */
-0x81, 0x02, /* Input (Variable), */
-0x25, 0x01, /* Logical Maximum (1), */
-0x45, 0x01, /* Physical Maximum (1), */
-0x65, 0x00, /* Unit, */
-0x75, 0x01, /* Report Size (1), */
-0x95, 0x03, /* Report Count (3), */
-0x81, 0x01, /* Input (Constant), */
-0x05, 0x09, /* Usage Page (Button), */
-0x19, 0x01, /* Usage Minimum (01h), */
-0x29, 0x11, /* Usage Maximum (11h), */
-0x95, 0x11, /* Report Count (17), */
-0x81, 0x02, /* Input (Variable), */
- /* ---- Dial patch starts here ---- */
-0x05, 0x01, /* Usage Page (Desktop), */
-0x09, 0x33, /* Usage (RX), */
-0x75, 0x04, /* Report Size (4), */
-0x95, 0x02, /* Report Count (2), */
-0x15, 0x00, /* Logical Minimum (0), */
-0x25, 0x0b, /* Logical Maximum (b), */
-0x81, 0x02, /* Input (Variable), */
-0x09, 0x35, /* Usage (RZ), */
-0x75, 0x04, /* Report Size (4), */
-0x95, 0x01, /* Report Count (1), */
-0x25, 0x03, /* Logical Maximum (3), */
-0x81, 0x02, /* Input (Variable), */
- /* ---- Dial patch ends here ---- */
-0x06, 0x00, 0xFF, /* Usage Page (FF00h), */
-0x09, 0x01, /* Usage (01h), */
-0x75, 0x04, /* Changed Report Size (4), */
-0x95, 0x0D, /* Changed Report Count (13), */
-0x81, 0x02, /* Input (Variable), */
-0xC0, /* End Collection, */
-0xA1, 0x02, /* Collection (Logical), */
-0x09, 0x02, /* Usage (02h), */
-0x75, 0x08, /* Report Size (8), */
-0x95, 0x10, /* Report Count (16), */
-0x91, 0x02, /* Output (Variable), */
-0xC0, /* End Collection, */
-0xC0 /* End Collection */
+ 0x05, 0x01, /* Usage Page (Desktop) */
+ 0x09, 0x08, /* Usage (MultiAxis), Changed */
+ 0xA1, 0x01, /* Collection (Application), */
+ 0xA1, 0x02, /* Collection (Logical), */
+ 0x95, 0x01, /* Report Count (1), */
+ 0x05, 0x01, /* Changed Usage Page (Desktop), */
+ 0x09, 0x30, /* Changed Usage (X), */
+ 0x16, 0xF8, 0xF8, /* Logical Minimum (-1800), */
+ 0x26, 0x08, 0x07, /* Logical Maximum (1800), */
+ 0x65, 0x14, /* Unit (Degrees), */
+ 0x55, 0x0F, /* Unit Exponent (15), */
+ 0x75, 0x10, /* Report Size (16), */
+ 0x81, 0x02, /* Input (Variable), */
+ 0x09, 0x31, /* Changed Usage (Y), */
+ 0x15, 0x00, /* Logical Minimum (0), */
+ 0x26, 0xFF, 0x03, /* Logical Maximum (1023), */
+ 0x75, 0x0C, /* Report Size (12), */
+ 0x81, 0x02, /* Input (Variable), */
+ 0x09, 0x32, /* Changed Usage (Z), */
+ 0x15, 0x00, /* Logical Minimum (0), */
+ 0x26, 0xFF, 0x03, /* Logical Maximum (1023), */
+ 0x75, 0x0C, /* Report Size (12), */
+ 0x81, 0x02, /* Input (Variable), */
+ 0x05, 0x01, /* Usage Page (Desktop), */
+ 0x09, 0x39, /* Usage (Hat Switch), */
+ 0x25, 0x07, /* Logical Maximum (7), */
+ 0x35, 0x00, /* Physical Minimum (0), */
+ 0x46, 0x3B, 0x01, /* Physical Maximum (315), */
+ 0x65, 0x14, /* Unit (Degrees), */
+ 0x75, 0x04, /* Report Size (4), */
+ 0x95, 0x01, /* Report Count (1), */
+ 0x81, 0x02, /* Input (Variable), */
+ 0x25, 0x01, /* Logical Maximum (1), */
+ 0x45, 0x01, /* Physical Maximum (1), */
+ 0x65, 0x00, /* Unit, */
+ 0x75, 0x01, /* Report Size (1), */
+ 0x95, 0x03, /* Report Count (3), */
+ 0x81, 0x01, /* Input (Constant), */
+ 0x05, 0x09, /* Usage Page (Button), */
+ 0x19, 0x01, /* Usage Minimum (01h), */
+ 0x29, 0x11, /* Usage Maximum (11h), */
+ 0x95, 0x11, /* Report Count (17), */
+ 0x81, 0x02, /* Input (Variable), */
+ /* ---- Dial patch starts here ---- */
+ 0x05, 0x01, /* Usage Page (Desktop), */
+ 0x09, 0x33, /* Usage (RX), */
+ 0x75, 0x04, /* Report Size (4), */
+ 0x95, 0x02, /* Report Count (2), */
+ 0x15, 0x00, /* Logical Minimum (0), */
+ 0x25, 0x0b, /* Logical Maximum (b), */
+ 0x81, 0x02, /* Input (Variable), */
+ 0x09, 0x35, /* Usage (RZ), */
+ 0x75, 0x04, /* Report Size (4), */
+ 0x95, 0x01, /* Report Count (1), */
+ 0x25, 0x03, /* Logical Maximum (3), */
+ 0x81, 0x02, /* Input (Variable), */
+ /* ---- Dial patch ends here ---- */
+ 0x06, 0x00, 0xFF, /* Usage Page (FF00h), */
+ 0x09, 0x01, /* Usage (01h), */
+ 0x75, 0x04, /* Changed Report Size (4), */
+ 0x95, 0x0D, /* Changed Report Count (13), */
+ 0x81, 0x02, /* Input (Variable), */
+ 0xC0, /* End Collection, */
+ 0xA1, 0x02, /* Collection (Logical), */
+ 0x09, 0x02, /* Usage (02h), */
+ 0x75, 0x08, /* Report Size (8), */
+ 0x95, 0x10, /* Report Count (16), */
+ 0x91, 0x02, /* Output (Variable), */
+ 0xC0, /* End Collection, */
+ 0xC0 /* End Collection */
};
#if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
- (IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
+ (IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
static void steelseries_srws1_set_leds(struct hid_device *hdev, __u16 leds)
{
- struct list_head *report_list = &hdev->report_enum[HID_OUTPUT_REPORT].report_list;
- struct hid_report *report = list_entry(report_list->next, struct hid_report, list);
+ struct list_head *report_list =
+ &hdev->report_enum[HID_OUTPUT_REPORT].report_list;
+ struct hid_report *report =
+ list_entry(report_list->next, struct hid_report, list);
__s32 *value = report->field[0]->value;
value[0] = 0x40;
@@ -152,12 +170,11 @@ static void steelseries_srws1_set_leds(struct hid_device *hdev, __u16 leds)
value[15] = 0x00;
hid_hw_request(hdev, report, HID_REQ_SET_REPORT);
-
- /* Note: LED change does not show on device until the device is read/polled */
}
-static void steelseries_srws1_led_all_set_brightness(struct led_classdev *led_cdev,
- enum led_brightness value)
+static void
+steelseries_srws1_led_all_set_brightness(struct led_classdev *led_cdev,
+ enum led_brightness value)
{
struct device *dev = led_cdev->dev->parent;
struct hid_device *hid = to_hid_device(dev);
@@ -176,7 +193,8 @@ static void steelseries_srws1_led_all_set_brightness(struct led_classdev *led_cd
steelseries_srws1_set_leds(hid, drv_data->led_state);
}
-static enum led_brightness steelseries_srws1_led_all_get_brightness(struct led_classdev *led_cdev)
+static enum led_brightness
+steelseries_srws1_led_all_get_brightness(struct led_classdev *led_cdev)
{
struct device *dev = led_cdev->dev->parent;
struct hid_device *hid = to_hid_device(dev);
@@ -193,7 +211,7 @@ static enum led_brightness steelseries_srws1_led_all_get_brightness(struct led_c
}
static void steelseries_srws1_led_set_brightness(struct led_classdev *led_cdev,
- enum led_brightness value)
+ enum led_brightness value)
{
struct device *dev = led_cdev->dev->parent;
struct hid_device *hid = to_hid_device(dev);
@@ -221,7 +239,8 @@ static void steelseries_srws1_led_set_brightness(struct led_classdev *led_cdev,
}
}
-static enum led_brightness steelseries_srws1_led_get_brightness(struct led_classdev *led_cdev)
+static enum led_brightness
+steelseries_srws1_led_get_brightness(struct led_classdev *led_cdev)
{
struct device *dev = led_cdev->dev->parent;
struct hid_device *hid = to_hid_device(dev);
@@ -245,7 +264,7 @@ static enum led_brightness steelseries_srws1_led_get_brightness(struct led_class
}
static int steelseries_srws1_probe(struct hid_device *hdev,
- const struct hid_device_id *id)
+ const struct hid_device_id *id)
{
int ret, i;
struct led_classdev *led;
@@ -288,7 +307,8 @@ static int steelseries_srws1_probe(struct hid_device *hdev,
name_sz = strlen(hdev->uniq) + 16;
/* 'ALL', for setting all LEDs simultaneously */
- led = devm_kzalloc(&hdev->dev, sizeof(struct led_classdev)+name_sz, GFP_KERNEL);
+ led = devm_kzalloc(&hdev->dev, sizeof(struct led_classdev) + name_sz,
+ GFP_KERNEL);
if (!led) {
hid_err(hdev, "can't allocate memory for LED ALL\n");
goto out;
@@ -305,20 +325,23 @@ static int steelseries_srws1_probe(struct hid_device *hdev,
drv_data->led[SRWS1_NUMBER_LEDS] = led;
ret = devm_led_classdev_register(&hdev->dev, led);
if (ret) {
- hid_err(hdev, "failed to register LED %d. Aborting.\n", SRWS1_NUMBER_LEDS);
- goto out; /* let the driver continue without LEDs */
+ hid_err(hdev, "failed to register LED %d. Aborting.\n",
+ SRWS1_NUMBER_LEDS);
+ goto out;
}
/* Each individual LED */
for (i = 0; i < SRWS1_NUMBER_LEDS; i++) {
- led = devm_kzalloc(&hdev->dev, sizeof(struct led_classdev)+name_sz, GFP_KERNEL);
+ led = devm_kzalloc(&hdev->dev,
+ sizeof(struct led_classdev) + name_sz,
+ GFP_KERNEL);
if (!led) {
hid_err(hdev, "can't allocate memory for LED %d\n", i);
break;
}
name = (void *)(&led[1]);
- snprintf(name, name_sz, "SRWS1::%s::RPM%d", hdev->uniq, i+1);
+ snprintf(name, name_sz, "SRWS1::%s::RPM%d", hdev->uniq, i + 1);
led->name = name;
led->brightness = 0;
led->max_brightness = 1;
@@ -329,8 +352,9 @@ static int steelseries_srws1_probe(struct hid_device *hdev,
ret = devm_led_classdev_register(&hdev->dev, led);
if (ret) {
- hid_err(hdev, "failed to register LED %d. Aborting.\n", i);
- break; /* but let the driver continue without LEDs */
+ hid_err(hdev, "failed to register LED %d. Aborting.\n",
+ i);
+ break;
}
}
out:
@@ -340,51 +364,277 @@ static int steelseries_srws1_probe(struct hid_device *hdev,
}
#endif
-#define STEELSERIES_HEADSET_BATTERY_TIMEOUT_MS 3000
+static const __u8 *steelseries_srws1_report_fixup(struct hid_device *hdev,
+ __u8 *rdesc,
+ unsigned int *rsize)
+{
+ if (hdev->vendor != USB_VENDOR_ID_STEELSERIES ||
+ hdev->product != USB_DEVICE_ID_STEELSERIES_SRWS1)
+ return rdesc;
+
+ if (*rsize >= 115 && rdesc[11] == 0x02 && rdesc[13] == 0xc8 &&
+ rdesc[29] == 0xbb && rdesc[40] == 0xc5) {
+ hid_info(hdev,
+ "Fixing up Steelseries SRW-S1 report descriptor\n");
+ *rsize = sizeof(steelseries_srws1_rdesc_fixed);
+ return steelseries_srws1_rdesc_fixed;
+ }
+ return rdesc;
+}
+
+static const struct steelseries_device_info arctis_1_info = {
+ .product_id = USB_DEVICE_ID_STEELSERIES_ARCTIS_1,
+ .name = "Arctis 1 Wireless",
+ .interface_binding_mode = 1,
+ .valid_interfaces = BIT(3),
+ .capabilities = SS_CAP_SIDETONE | SS_CAP_BATTERY | SS_CAP_INACTIVE_TIME,
+};
-#define ARCTIS_1_BATTERY_RESPONSE_LEN 8
-#define ARCTIS_9_BATTERY_RESPONSE_LEN 64
-static const char arctis_1_battery_request[] = { 0x06, 0x12 };
-static const char arctis_9_battery_request[] = { 0x00, 0x20 };
+static const struct steelseries_device_info arctis_1_x_info = {
+ .product_id = USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X,
+ .name = "Arctis 1 Wireless for Xbox",
+ .interface_binding_mode = 1,
+ .valid_interfaces = BIT(3),
+ .capabilities = SS_CAP_SIDETONE | SS_CAP_BATTERY | SS_CAP_INACTIVE_TIME,
+};
-static int steelseries_headset_request_battery(struct hid_device *hdev,
- const char *request, size_t len)
-{
- u8 *write_buf;
- int ret;
+static const struct steelseries_device_info arctis_7_info = {
+ .product_id = USB_DEVICE_ID_STEELSERIES_ARCTIS_7,
+ .name = "Arctis 7",
+ .interface_binding_mode = 1,
+ .valid_interfaces = BIT(5),
+ .capabilities = SS_CAP_SIDETONE | SS_CAP_BATTERY | SS_CAP_INACTIVE_TIME,
+};
- /* Request battery information */
- write_buf = kmemdup(request, len, GFP_KERNEL);
- if (!write_buf)
- return -ENOMEM;
+static const struct steelseries_device_info arctis_7_p_info = {
+ .product_id = USB_DEVICE_ID_STEELSERIES_ARCTIS_7_P,
+ .name = "Arctis 7P",
+ .interface_binding_mode = 1,
+ .valid_interfaces = BIT(3),
+ .capabilities = SS_CAP_SIDETONE | SS_CAP_BATTERY | SS_CAP_INACTIVE_TIME,
+};
- hid_dbg(hdev, "Sending battery request report");
- ret = hid_hw_raw_request(hdev, request[0], write_buf, len,
- HID_OUTPUT_REPORT, HID_REQ_SET_REPORT);
- if (ret < (int)len) {
- hid_err(hdev, "hid_hw_raw_request() failed with %d\n", ret);
- ret = -ENODATA;
- }
+static const struct steelseries_device_info arctis_7_x_info = {
+ .product_id = USB_DEVICE_ID_STEELSERIES_ARCTIS_7_X,
+ .name = "Arctis 7X",
+ .interface_binding_mode = 1,
+ .valid_interfaces = BIT(3),
+ .capabilities = SS_CAP_SIDETONE | SS_CAP_BATTERY | SS_CAP_INACTIVE_TIME,
+};
- kfree(write_buf);
- return ret;
-}
+static const struct steelseries_device_info arctis_7_gen2_info = {
+ .product_id = USB_DEVICE_ID_STEELSERIES_ARCTIS_7_GEN2,
+ .name = "Arctis 7 (2019 Edition)",
+ .interface_binding_mode = 1,
+ .valid_interfaces = BIT(5),
+ .capabilities = SS_CAP_SIDETONE | SS_CAP_BATTERY | SS_CAP_INACTIVE_TIME,
+};
-static void steelseries_headset_fetch_battery(struct hid_device *hdev)
-{
- int ret = 0;
+static const struct steelseries_device_info arctis_7_plus_info = {
+ .product_id = USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS,
+ .name = "Arctis 7+",
+ .interface_binding_mode = 1,
+ .valid_interfaces = BIT(3),
+ .capabilities = SS_CAP_SIDETONE | SS_CAP_BATTERY |
+ SS_CAP_INACTIVE_TIME | SS_CAP_CHATMIX,
+};
- if (hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1)
- ret = steelseries_headset_request_battery(hdev,
- arctis_1_battery_request, sizeof(arctis_1_battery_request));
- else if (hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_9)
- ret = steelseries_headset_request_battery(hdev,
- arctis_9_battery_request, sizeof(arctis_9_battery_request));
+static const struct steelseries_device_info arctis_7_plus_p_info = {
+ .product_id = USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_P,
+ .name = "Arctis 7+ (PlayStation)",
+ .interface_binding_mode = 1,
+ .valid_interfaces = BIT(3),
+ .capabilities = SS_CAP_SIDETONE | SS_CAP_BATTERY |
+ SS_CAP_INACTIVE_TIME | SS_CAP_CHATMIX,
+};
- if (ret < 0)
- hid_dbg(hdev,
- "Battery query failed (err: %d)\n", ret);
-}
+static const struct steelseries_device_info arctis_7_plus_x_info = {
+ .product_id = USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_X,
+ .name = "Arctis 7+ (Xbox)",
+ .interface_binding_mode = 1,
+ .valid_interfaces = BIT(3),
+ .capabilities = SS_CAP_SIDETONE | SS_CAP_BATTERY |
+ SS_CAP_INACTIVE_TIME | SS_CAP_CHATMIX,
+};
+
+static const struct steelseries_device_info arctis_7_plus_destiny_info = {
+ .product_id = USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_DESTINY,
+ .name = "Arctis 7+ (Destiny Edition)",
+ .interface_binding_mode = 1,
+ .valid_interfaces = BIT(3),
+ .capabilities = SS_CAP_SIDETONE | SS_CAP_BATTERY |
+ SS_CAP_INACTIVE_TIME | SS_CAP_CHATMIX,
+};
+
+static const struct steelseries_device_info arctis_9_info = {
+ .product_id = USB_DEVICE_ID_STEELSERIES_ARCTIS_9,
+ .name = "Arctis 9",
+ .interface_binding_mode = 0,
+ .valid_interfaces = 0,
+ .capabilities = SS_CAP_SIDETONE | SS_CAP_BATTERY |
+ SS_CAP_INACTIVE_TIME | SS_CAP_CHATMIX,
+};
+
+static const struct steelseries_device_info arctis_pro_info = {
+ .product_id = USB_DEVICE_ID_STEELSERIES_ARCTIS_PRO,
+ .name = "Arctis Pro Wireless",
+ .interface_binding_mode = 0,
+ .valid_interfaces = 0,
+ .capabilities = SS_CAP_SIDETONE | SS_CAP_BATTERY | SS_CAP_INACTIVE_TIME,
+};
+
+static const struct steelseries_device_info arctis_nova_3_info = {
+ .product_id = USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3,
+ .name = "Arctis Nova 3",
+ .interface_binding_mode = 1,
+ .valid_interfaces = BIT(4),
+ .capabilities = SS_CAP_SIDETONE | SS_CAP_MIC_MUTE_LED |
+ SS_CAP_MIC_VOLUME,
+};
+
+static const struct steelseries_device_info arctis_nova_3_p_info = {
+ .product_id = USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_P,
+ .name = "Arctis Nova 3 (PlayStation)",
+ .interface_binding_mode = 1,
+ .valid_interfaces = BIT(0),
+ .capabilities = SS_CAP_SIDETONE | SS_CAP_BATTERY |
+ SS_CAP_INACTIVE_TIME | SS_CAP_MIC_VOLUME,
+};
+
+static const struct steelseries_device_info arctis_nova_3_x_info = {
+ .product_id = USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_X,
+ .name = "Arctis Nova 3 (Xbox)",
+ .interface_binding_mode = 1,
+ .valid_interfaces = BIT(0),
+ .capabilities = SS_CAP_SIDETONE | SS_CAP_BATTERY |
+ SS_CAP_INACTIVE_TIME | SS_CAP_MIC_VOLUME,
+};
+
+static const struct steelseries_device_info arctis_nova_5_info = {
+ .product_id = USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5,
+ .name = "Arctis Nova 5",
+ .interface_binding_mode = 1,
+ .valid_interfaces = BIT(3),
+ .capabilities = SS_CAP_SIDETONE | SS_CAP_BATTERY | SS_CAP_CHATMIX |
+ SS_CAP_INACTIVE_TIME | SS_CAP_MIC_MUTE_LED |
+ SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER,
+};
+
+static const struct steelseries_device_info arctis_nova_5_x_info = {
+ .product_id = USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5_X,
+ .name = "Arctis Nova 5X",
+ .interface_binding_mode = 1,
+ .valid_interfaces = BIT(3),
+ .capabilities = SS_CAP_SIDETONE | SS_CAP_BATTERY | SS_CAP_CHATMIX |
+ SS_CAP_INACTIVE_TIME | SS_CAP_MIC_MUTE_LED |
+ SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER,
+};
+
+static const struct steelseries_device_info arctis_nova_7_info = {
+ .product_id = USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7,
+ .name = "Arctis Nova 7",
+ .interface_binding_mode = 1,
+ .valid_interfaces = BIT(3),
+ .capabilities = SS_CAP_SIDETONE | SS_CAP_BATTERY | SS_CAP_CHATMIX |
+ SS_CAP_INACTIVE_TIME | SS_CAP_MIC_MUTE_LED |
+ SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
+ SS_CAP_BT_POWER_ON | SS_CAP_BT_CALL_VOL,
+};
+
+static const struct steelseries_device_info arctis_nova_7_x_info = {
+ .product_id = USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X,
+ .name = "Arctis Nova 7X",
+ .interface_binding_mode = 1,
+ .valid_interfaces = BIT(3),
+ .capabilities = SS_CAP_SIDETONE | SS_CAP_BATTERY | SS_CAP_CHATMIX |
+ SS_CAP_INACTIVE_TIME | SS_CAP_MIC_MUTE_LED |
+ SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
+ SS_CAP_BT_POWER_ON | SS_CAP_BT_CALL_VOL,
+};
+
+static const struct steelseries_device_info arctis_nova_7_p_info = {
+ .product_id = USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_P,
+ .name = "Arctis Nova 7P",
+ .interface_binding_mode = 1,
+ .valid_interfaces = BIT(3),
+ .capabilities = SS_CAP_SIDETONE | SS_CAP_BATTERY | SS_CAP_CHATMIX |
+ SS_CAP_INACTIVE_TIME | SS_CAP_MIC_MUTE_LED |
+ SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
+ SS_CAP_BT_POWER_ON | SS_CAP_BT_CALL_VOL,
+};
+
+static const struct steelseries_device_info arctis_nova_7_x_rev2_info = {
+ .product_id = USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_REV2,
+ .name = "Arctis Nova 7X (Rev 2)",
+ .interface_binding_mode = 1,
+ .valid_interfaces = BIT(3),
+ .capabilities = SS_CAP_SIDETONE | SS_CAP_BATTERY | SS_CAP_CHATMIX |
+ SS_CAP_INACTIVE_TIME | SS_CAP_MIC_MUTE_LED |
+ SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
+ SS_CAP_BT_POWER_ON | SS_CAP_BT_CALL_VOL,
+};
+
+static const struct steelseries_device_info arctis_nova_7_diablo_info = {
+ .product_id = USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO,
+ .name = "Arctis Nova 7 (Diablo IV Edition)",
+ .interface_binding_mode = 1,
+ .valid_interfaces = BIT(3),
+ .capabilities = SS_CAP_SIDETONE | SS_CAP_BATTERY | SS_CAP_CHATMIX |
+ SS_CAP_INACTIVE_TIME | SS_CAP_MIC_MUTE_LED |
+ SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
+ SS_CAP_BT_POWER_ON | SS_CAP_BT_CALL_VOL,
+};
+
+static const struct steelseries_device_info arctis_nova_7_wow_info = {
+ .product_id = USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_WOW,
+ .name = "Arctis Nova 7 (World of Warcraft Edition)",
+ .interface_binding_mode = 1,
+ .valid_interfaces = BIT(3),
+ .capabilities = SS_CAP_SIDETONE | SS_CAP_BATTERY | SS_CAP_CHATMIX |
+ SS_CAP_INACTIVE_TIME | SS_CAP_MIC_MUTE_LED |
+ SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
+ SS_CAP_BT_POWER_ON | SS_CAP_BT_CALL_VOL,
+};
+
+static const struct steelseries_device_info arctis_nova_7_gen2_info = {
+ .product_id = USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_GEN2,
+ .name = "Arctis Nova 7 (Gen 2)",
+ .interface_binding_mode = 1,
+ .valid_interfaces = BIT(3),
+ .capabilities = SS_CAP_SIDETONE | SS_CAP_BATTERY | SS_CAP_CHATMIX |
+ SS_CAP_INACTIVE_TIME | SS_CAP_MIC_MUTE_LED |
+ SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
+ SS_CAP_BT_POWER_ON | SS_CAP_BT_CALL_VOL,
+};
+
+static const struct steelseries_device_info arctis_nova_7_x_gen2_info = {
+ .product_id = USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2,
+ .name = "Arctis Nova 7X (Gen 2)",
+ .interface_binding_mode = 1,
+ .valid_interfaces = BIT(3),
+ .capabilities = SS_CAP_SIDETONE | SS_CAP_BATTERY | SS_CAP_CHATMIX |
+ SS_CAP_INACTIVE_TIME | SS_CAP_MIC_MUTE_LED |
+ SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
+ SS_CAP_BT_POWER_ON | SS_CAP_BT_CALL_VOL,
+};
+
+static const struct steelseries_device_info arctis_nova_pro_info = {
+ .product_id = USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_PRO,
+ .name = "Arctis Nova Pro Wireless",
+ .interface_binding_mode = 1,
+ .valid_interfaces = BIT(4),
+ .capabilities = SS_CAP_SIDETONE | SS_CAP_BATTERY | SS_CAP_INACTIVE_TIME,
+};
+
+static const struct steelseries_device_info arctis_nova_pro_x_info = {
+ .product_id = USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_PRO_X,
+ .name = "Arctis Nova Pro Wireless (Xbox)",
+ .interface_binding_mode = 1,
+ .valid_interfaces = BIT(4),
+ .capabilities = SS_CAP_SIDETONE | SS_CAP_BATTERY | SS_CAP_INACTIVE_TIME,
+};
+
+#define STEELSERIES_HEADSET_BATTERY_TIMEOUT_MS 3000
static int battery_capacity_to_level(int capacity)
{
@@ -395,29 +645,45 @@ static int battery_capacity_to_level(int capacity)
return POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL;
}
-static void steelseries_headset_battery_timer_tick(struct work_struct *work)
+static u8 steelseries_map_battery(u8 capacity, u8 min_in, u8 max_in)
+{
+ if (capacity >= max_in)
+ return 100;
+ if (capacity <= min_in)
+ return 0;
+ return (capacity - min_in) * 100 / (max_in - min_in);
+}
+
+static void steelseries_headset_set_wireless_status(struct hid_device *hdev,
+ bool connected)
{
- struct steelseries_device *sd = container_of(work,
- struct steelseries_device, battery_work.work);
- struct hid_device *hdev = sd->hdev;
+ struct usb_interface *intf;
- steelseries_headset_fetch_battery(hdev);
+ if (!hid_is_usb(hdev))
+ return;
+
+ intf = to_usb_interface(hdev->dev.parent);
+ usb_set_wireless_status(intf, connected ?
+ USB_WIRELESS_STATUS_CONNECTED :
+ USB_WIRELESS_STATUS_DISCONNECTED);
}
#define STEELSERIES_PREFIX "SteelSeries "
#define STEELSERIES_PREFIX_LEN strlen(STEELSERIES_PREFIX)
-static int steelseries_headset_battery_get_property(struct power_supply *psy,
- enum power_supply_property psp,
- union power_supply_propval *val)
+static int steelseries_battery_get_property(struct power_supply *psy,
+ enum power_supply_property psp,
+ union power_supply_propval *val)
{
struct steelseries_device *sd = power_supply_get_drvdata(psy);
+ unsigned long flags;
int ret = 0;
switch (psp) {
case POWER_SUPPLY_PROP_MODEL_NAME:
val->strval = sd->hdev->name;
- while (!strncmp(val->strval, STEELSERIES_PREFIX, STEELSERIES_PREFIX_LEN))
+ while (!strncmp(val->strval, STEELSERIES_PREFIX,
+ STEELSERIES_PREFIX_LEN))
val->strval += STEELSERIES_PREFIX_LEN;
break;
case POWER_SUPPLY_PROP_MANUFACTURER:
@@ -427,21 +693,28 @@ static int steelseries_headset_battery_get_property(struct power_supply *psy,
val->intval = 1;
break;
case POWER_SUPPLY_PROP_STATUS:
+ spin_lock_irqsave(&sd->lock, flags);
if (sd->headset_connected) {
val->intval = sd->battery_charging ?
- POWER_SUPPLY_STATUS_CHARGING :
- POWER_SUPPLY_STATUS_DISCHARGING;
- } else
+ POWER_SUPPLY_STATUS_CHARGING :
+ POWER_SUPPLY_STATUS_DISCHARGING;
+ } else {
val->intval = POWER_SUPPLY_STATUS_UNKNOWN;
+ }
+ spin_unlock_irqrestore(&sd->lock, flags);
break;
case POWER_SUPPLY_PROP_SCOPE:
val->intval = POWER_SUPPLY_SCOPE_DEVICE;
break;
case POWER_SUPPLY_PROP_CAPACITY:
+ spin_lock_irqsave(&sd->lock, flags);
val->intval = sd->battery_capacity;
+ spin_unlock_irqrestore(&sd->lock, flags);
break;
case POWER_SUPPLY_PROP_CAPACITY_LEVEL:
+ spin_lock_irqsave(&sd->lock, flags);
val->intval = battery_capacity_to_level(sd->battery_capacity);
+ spin_unlock_irqrestore(&sd->lock, flags);
break;
default:
ret = -EINVAL;
@@ -450,289 +723,1434 @@ static int steelseries_headset_battery_get_property(struct power_supply *psy,
return ret;
}
-static void
-steelseries_headset_set_wireless_status(struct hid_device *hdev,
- bool connected)
+static enum power_supply_property steelseries_battery_props[] = {
+ POWER_SUPPLY_PROP_MODEL_NAME, POWER_SUPPLY_PROP_MANUFACTURER,
+ POWER_SUPPLY_PROP_PRESENT, POWER_SUPPLY_PROP_STATUS,
+ POWER_SUPPLY_PROP_SCOPE, POWER_SUPPLY_PROP_CAPACITY,
+ POWER_SUPPLY_PROP_CAPACITY_LEVEL,
+};
+
+/* Forward declarations for battery request functions */
+static int steelseries_arctis_1_request_battery(struct hid_device *hdev);
+static int steelseries_arctis_7_plus_request_battery(struct hid_device *hdev);
+static int steelseries_arctis_9_request_battery(struct hid_device *hdev);
+static int steelseries_arctis_nova_request_battery(struct hid_device *hdev);
+static int steelseries_arctis_nova_3p_request_battery(struct hid_device *hdev);
+static int
+steelseries_arctis_pro_wireless_request_battery(struct hid_device *hdev);
+
+static int steelseries_request_battery(struct hid_device *hdev)
{
- struct usb_interface *intf;
+ u16 product = hdev->product;
- if (!hid_is_usb(hdev))
- return;
+ /* Route to device-specific battery request handler */
+ if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1 ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_7_P ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_7_X)
+ return steelseries_arctis_1_request_battery(hdev);
- intf = to_usb_interface(hdev->dev.parent);
- usb_set_wireless_status(intf, connected ?
- USB_WIRELESS_STATUS_CONNECTED :
- USB_WIRELESS_STATUS_DISCONNECTED);
+ if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_P ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_X ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_DESTINY)
+ return steelseries_arctis_7_plus_request_battery(hdev);
+
+ if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_9)
+ return steelseries_arctis_9_request_battery(hdev);
+
+ if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_PRO)
+ return steelseries_arctis_pro_wireless_request_battery(hdev);
+
+ if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_P ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_X)
+ return steelseries_arctis_nova_3p_request_battery(hdev);
+
+ /* All other Nova series use the same battery request */
+ return steelseries_arctis_nova_request_battery(hdev);
}
-static enum power_supply_property steelseries_headset_battery_props[] = {
- POWER_SUPPLY_PROP_MODEL_NAME,
- POWER_SUPPLY_PROP_MANUFACTURER,
- POWER_SUPPLY_PROP_PRESENT,
- POWER_SUPPLY_PROP_STATUS,
- POWER_SUPPLY_PROP_SCOPE,
- POWER_SUPPLY_PROP_CAPACITY,
- POWER_SUPPLY_PROP_CAPACITY_LEVEL,
-};
+static void steelseries_battery_timer_tick(struct work_struct *work)
+{
+ struct steelseries_device *sd = container_of(
+ work, struct steelseries_device, battery_work.work);
+
+ steelseries_request_battery(sd->hdev);
+}
-static int steelseries_headset_battery_register(struct steelseries_device *sd)
+static int steelseries_battery_register(struct steelseries_device *sd)
{
static atomic_t battery_no = ATOMIC_INIT(0);
- struct power_supply_config battery_cfg = { .drv_data = sd, };
+ struct power_supply_config battery_cfg = {
+ .drv_data = sd,
+ };
unsigned long n;
int ret;
sd->battery_desc.type = POWER_SUPPLY_TYPE_BATTERY;
- sd->battery_desc.properties = steelseries_headset_battery_props;
- sd->battery_desc.num_properties = ARRAY_SIZE(steelseries_headset_battery_props);
- sd->battery_desc.get_property = steelseries_headset_battery_get_property;
+ sd->battery_desc.properties = steelseries_battery_props;
+ sd->battery_desc.num_properties = ARRAY_SIZE(steelseries_battery_props);
+ sd->battery_desc.get_property = steelseries_battery_get_property;
sd->battery_desc.use_for_apm = 0;
n = atomic_inc_return(&battery_no) - 1;
- sd->battery_desc.name = devm_kasprintf(&sd->hdev->dev, GFP_KERNEL,
- "steelseries_headset_battery_%ld", n);
+ sd->battery_desc.name =
+ devm_kasprintf(&sd->hdev->dev, GFP_KERNEL,
+ "steelseries_headset_battery_%ld", n);
if (!sd->battery_desc.name)
return -ENOMEM;
- /* avoid the warning of 0% battery while waiting for the first info */
steelseries_headset_set_wireless_status(sd->hdev, false);
- sd->battery_capacity = 100;
+ sd->battery_capacity =
+ 100; /* Start with full to avoid low battery warnings */
sd->battery_charging = false;
+ sd->headset_connected = false;
+ sd->chatmix_level = 64;
- sd->battery = devm_power_supply_register(&sd->hdev->dev,
- &sd->battery_desc, &battery_cfg);
+ sd->battery = devm_power_supply_register(
+ &sd->hdev->dev, &sd->battery_desc, &battery_cfg);
if (IS_ERR(sd->battery)) {
ret = PTR_ERR(sd->battery);
- hid_err(sd->hdev,
- "%s:power_supply_register failed with error %d\n",
- __func__, ret);
+ hid_err(sd->hdev, "Failed to register battery: %d\n", ret);
return ret;
}
power_supply_powers(sd->battery, &sd->hdev->dev);
- INIT_DELAYED_WORK(&sd->battery_work, steelseries_headset_battery_timer_tick);
- steelseries_headset_fetch_battery(sd->hdev);
+ INIT_DELAYED_WORK(&sd->battery_work, steelseries_battery_timer_tick);
+ steelseries_request_battery(sd->hdev);
- if (sd->quirks & STEELSERIES_ARCTIS_9) {
- /* The first fetch_battery request can remain unanswered in some cases */
- schedule_delayed_work(&sd->battery_work,
- msecs_to_jiffies(STEELSERIES_HEADSET_BATTERY_TIMEOUT_MS));
+ /* Arctis 9 may need a retry */
+ if (sd->hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_9) {
+ schedule_delayed_work(
+ &sd->battery_work,
+ msecs_to_jiffies(
+ STEELSERIES_HEADSET_BATTERY_TIMEOUT_MS));
}
return 0;
}
-static bool steelseries_is_vendor_usage_page(struct hid_device *hdev, uint8_t usage_page)
+/* Helper function to send feature reports */
+static int steelseries_send_feature_report(struct hid_device *hdev,
+ const u8 *data, size_t len)
{
- return hdev->rdesc[0] == 0x06 &&
- hdev->rdesc[1] == usage_page &&
- hdev->rdesc[2] == 0xff;
+ u8 *buf;
+ int ret;
+
+ buf = kmemdup(data, len, GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+
+ ret = hid_hw_raw_request(hdev, data[0], buf, len, HID_FEATURE_REPORT,
+ HID_REQ_SET_REPORT);
+ kfree(buf);
+
+ if (ret < 0)
+ return ret;
+ if (ret < len)
+ return -EIO;
+
+ return 0;
}
-static int steelseries_probe(struct hid_device *hdev, const struct hid_device_id *id)
+/* Helper function to send output reports */
+static int steelseries_send_output_report(struct hid_device *hdev,
+ const u8 *data, size_t len)
{
- struct steelseries_device *sd;
+ u8 *buf;
int ret;
- if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1) {
-#if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
- (IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
- return steelseries_srws1_probe(hdev, id);
-#else
- return -ENODEV;
-#endif
- }
-
- sd = devm_kzalloc(&hdev->dev, sizeof(*sd), GFP_KERNEL);
- if (!sd)
+ buf = kmemdup(data, len, GFP_KERNEL);
+ if (!buf)
return -ENOMEM;
- hid_set_drvdata(hdev, sd);
- sd->hdev = hdev;
- sd->quirks = id->driver_data;
- ret = hid_parse(hdev);
- if (ret)
+ /* Use raw_request with OUTPUT_REPORT type for devices without Interrupt OUT */
+ ret = hid_hw_raw_request(hdev, data[0], buf, len, HID_OUTPUT_REPORT,
+ HID_REQ_SET_REPORT);
+ kfree(buf);
+
+ if (ret < 0)
return ret;
+ if (ret < len)
+ return -EIO;
- if (sd->quirks & STEELSERIES_ARCTIS_9 &&
- !steelseries_is_vendor_usage_page(hdev, 0xc0))
- return -ENODEV;
+ return 0;
+}
- spin_lock_init(&sd->lock);
+/* Sidetone level attribute */
+static ssize_t sidetone_level_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ /* Sidetone is write-only, no way to read current value */
+ return sysfs_emit(buf, "Write-only attribute (0-128)\n");
+}
- ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
- if (ret)
- return ret;
+static ssize_t sidetone_level_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ u16 product = hdev->product;
+ unsigned int value;
+ u8 data[64] = { 0 };
+ int ret;
- ret = hid_hw_open(hdev);
- if (ret)
- return ret;
+ if (kstrtouint(buf, 10, &value))
+ return -EINVAL;
+ if (value > 128)
+ return -EINVAL;
+
+ /* Device-specific sidetone mappings */
+ if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1 ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_7_P ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_7_X ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_7 ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_7_GEN2) {
+ /* Map 0-128 to 0x00-0x12 (18) */
+ u8 level = (value * 0x12) / 128;
+
+ if (level == 0) {
+ data[0] = 0x06;
+ data[1] = 0x35;
+ data[2] = 0x00;
+ ret = steelseries_send_feature_report(hdev, data, 31);
+ } else {
+ data[0] = 0x06;
+ data[1] = 0x35;
+ data[2] = 0x01;
+ data[3] = 0x00;
+ data[4] = level;
+ ret = steelseries_send_feature_report(hdev, data, 31);
+ }
+ if (ret >= 0) {
+ /* Save state */
+ data[0] = 0x06;
+ data[1] = 0x09;
+ steelseries_send_feature_report(hdev, data, 31);
+ }
+ } else if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_P ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_X ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_DESTINY) {
+ /* Map to 0-3 levels */
+ u8 level;
+
+ if (value < 26)
+ level = 0x0;
+ else if (value < 51)
+ level = 0x1;
+ else if (value < 76)
+ level = 0x2;
+ else
+ level = 0x3;
+
+ data[0] = 0x00;
+ data[1] = 0x39;
+ data[2] = level;
+ ret = steelseries_send_feature_report(hdev, data, 64);
+ } else if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_9) {
+ /* Arctis 9: exponential mapping to 0xc0-0xfd */
+ u8 level;
+
+ if (value == 0)
+ level = 0xc0;
+ else
+ level = 0xc0 + ((value * (0xfd - 0xc0)) / 128);
+
+ data[0] = 0x06;
+ data[1] = 0x00;
+ data[2] = level;
+ ret = steelseries_send_feature_report(hdev, data, 31);
+ if (ret >= 0) {
+ data[0] = 0x90;
+ data[1] = 0x00;
+ steelseries_send_feature_report(hdev, data, 31);
+ }
+ } else if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_PRO) {
+ /* Arctis Pro Wireless: 0x00-0x09 */
+ u8 level = (value * 0x09) / 128;
+
+ data[0] = 0x39;
+ data[1] = 0xAA;
+ data[2] = level;
+ ret = steelseries_send_feature_report(hdev, data, 31);
+ if (ret >= 0) {
+ data[0] = 0x90;
+ data[1] = 0xAA;
+ steelseries_send_feature_report(hdev, data, 31);
+ }
+ } else if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3) {
+ /* Nova 3: 0-3 levels */
+ u8 level;
+
+ if (value < 26)
+ level = 0x0;
+ else if (value < 51)
+ level = 0x1;
+ else if (value < 76)
+ level = 0x2;
+ else
+ level = 0x3;
+
+ data[0] = 0x06;
+ data[1] = 0x39;
+ data[2] = level;
+ ret = steelseries_send_output_report(hdev, data, 64);
+ if (ret >= 0) {
+ data[0] = 0x06;
+ data[1] = 0x09;
+ steelseries_send_output_report(hdev, data, 64);
+ }
+ } else if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_P ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_X) {
+ /* Nova 3P/3X: Map to 0-10 */
+ u8 level = (value * 0x0a) / 128;
+
+ data[0] = 0x39;
+ data[1] = level;
+ ret = steelseries_send_output_report(hdev, data, 64);
+ if (ret >= 0) {
+ data[0] = 0x09;
+ steelseries_send_output_report(hdev, data, 64);
+ }
+ } else if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5 ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5_X) {
+ /* Nova 5: Map to 0-10 */
+ u8 level = (value * 0x0a) / 128;
+
+ data[0] = 0x00;
+ data[1] = 0x39;
+ data[2] = level;
+ ret = steelseries_send_output_report(hdev, data, 64);
+ if (ret >= 0) {
+ data[0] = 0x00;
+ data[1] = 0x09;
+ steelseries_send_output_report(hdev, data, 64);
+ data[0] = 0x00;
+ data[1] = 0x35;
+ data[2] = 0x01;
+ steelseries_send_output_report(hdev, data, 64);
+ }
+ } else if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_PRO ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_PRO_X) {
+ /* Nova Pro: 0-3 only */
+ if (value > 3)
+ return -EINVAL;
+ data[0] = 0x06;
+ data[1] = 0x39;
+ data[2] = value;
+ ret = steelseries_send_output_report(hdev, data, 31);
+ if (ret >= 0) {
+ data[0] = 0x06;
+ data[1] = 0x09;
+ steelseries_send_output_report(hdev, data, 31);
+ }
+ } else {
+ /* Nova 7 series: 0-3 levels */
+ u8 level;
+
+ if (value < 26)
+ level = 0x0;
+ else if (value < 51)
+ level = 0x1;
+ else if (value < 76)
+ level = 0x2;
+ else
+ level = 0x3;
+
+ data[0] = 0x00;
+ data[1] = 0x39;
+ data[2] = level;
+ ret = steelseries_send_output_report(hdev, data, 64);
+ if (ret >= 0) {
+ memset(data, 0, sizeof(data));
+ data[0] = 0x00;
+ data[1] = 0x09;
+ steelseries_send_output_report(hdev, data, 64);
+ }
+ }
- if (steelseries_headset_battery_register(sd) < 0)
- hid_err(sd->hdev,
- "Failed to register battery for headset\n");
+ return (ret < 0) ? ret : count;
+}
+static DEVICE_ATTR_RW(sidetone_level);
- return ret;
+/* Inactive time attribute */
+static ssize_t inactive_time_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "Write-only attribute (0-90 minutes)\n");
}
-static void steelseries_remove(struct hid_device *hdev)
+static ssize_t inactive_time_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
{
- struct steelseries_device *sd;
- unsigned long flags;
+ struct hid_device *hdev = to_hid_device(dev);
+ u16 product = hdev->product;
+ unsigned int value;
+ u8 data[64] = { 0 };
+ int ret;
- if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1) {
-#if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
- (IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
- hid_hw_stop(hdev);
-#endif
- return;
+ if (kstrtouint(buf, 10, &value))
+ return -EINVAL;
+ if (value > 90)
+ return -EINVAL;
+
+ /* Device-specific mappings */
+ if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1 ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_7_P ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_7_X) {
+ data[0] = 0x06;
+ data[1] = 0x53;
+ data[2] = value;
+ ret = steelseries_send_feature_report(hdev, data, 31);
+ if (ret >= 0) {
+ data[0] = 0x06;
+ data[1] = 0x09;
+ steelseries_send_feature_report(hdev, data, 31);
+ }
+ } else if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_7 ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_7_GEN2) {
+ data[0] = 0x06;
+ data[1] = 0x51;
+ data[2] = value;
+ ret = steelseries_send_feature_report(hdev, data, 31);
+ if (ret >= 0) {
+ data[0] = 0x06;
+ data[1] = 0x09;
+ steelseries_send_feature_report(hdev, data, 31);
+ }
+ } else if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_P ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_X ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_DESTINY) {
+ data[0] = 0x00;
+ data[1] = 0xa3;
+ data[2] = value;
+ ret = steelseries_send_feature_report(hdev, data, 64);
+ } else if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_9) {
+ /* Arctis 9 uses seconds */
+ u32 seconds = value * 60;
+
+ data[0] = 0x04;
+ data[1] = 0x00;
+ data[2] = (seconds >> 8) & 0xff;
+ data[3] = seconds & 0xff;
+ ret = steelseries_send_feature_report(hdev, data, 31);
+ if (ret >= 0) {
+ data[0] = 0x90;
+ data[1] = 0x00;
+ steelseries_send_feature_report(hdev, data, 31);
+ }
+ } else if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_PRO) {
+ /* Pro Wireless uses 10-minute increments */
+ u8 increments = value / 10;
+
+ data[0] = 0x3c;
+ data[1] = 0xAA;
+ data[2] = increments;
+ ret = steelseries_send_feature_report(hdev, data, 31);
+ if (ret >= 0) {
+ data[0] = 0x90;
+ data[1] = 0xAA;
+ steelseries_send_feature_report(hdev, data, 31);
+ }
+ } else if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_P ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_X) {
+ /* Map to specific values */
+ u8 mapped;
+
+ if (value >= 90)
+ mapped = 90;
+ else if (value >= 75)
+ mapped = 75;
+ else if (value >= 60)
+ mapped = 60;
+ else if (value >= 45)
+ mapped = 45;
+ else if (value >= 30)
+ mapped = 30;
+ else if (value >= 15)
+ mapped = 15;
+ else if (value >= 10)
+ mapped = 10;
+ else if (value >= 5)
+ mapped = 5;
+ else if (value >= 1)
+ mapped = 1;
+ else
+ mapped = 0;
+
+ data[0] = 0xa3;
+ data[1] = mapped;
+ ret = steelseries_send_output_report(hdev, data, 64);
+ if (ret >= 0) {
+ data[0] = 0x09;
+ steelseries_send_output_report(hdev, data, 64);
+ }
+ } else if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_PRO ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_PRO_X) {
+ /* Map to enum values */
+ u8 mapped;
+
+ if (value >= 45)
+ mapped = 6;
+ else if (value >= 23)
+ mapped = 5;
+ else if (value >= 13)
+ mapped = 4;
+ else if (value >= 8)
+ mapped = 3;
+ else if (value >= 3)
+ mapped = 2;
+ else if (value > 0)
+ mapped = 1;
+ else
+ mapped = 0;
+
+ data[0] = 0x06;
+ data[1] = 0xc1;
+ data[2] = mapped;
+ ret = steelseries_send_output_report(hdev, data, 31);
+ if (ret >= 0) {
+ data[0] = 0x06;
+ data[1] = 0x09;
+ steelseries_send_output_report(hdev, data, 31);
+ }
+ } else {
+ /* Nova 5/7 series */
+ data[0] = 0x00;
+ data[1] = 0xa3;
+ data[2] = value;
+ ret = steelseries_send_output_report(hdev, data, 64);
+ if (ret >= 0) {
+ memset(data, 0, sizeof(data));
+ data[0] = 0x00;
+ data[1] = 0x09;
+ steelseries_send_output_report(hdev, data, 64);
+ }
}
- sd = hid_get_drvdata(hdev);
+ return (ret < 0) ? ret : count;
+}
+static DEVICE_ATTR_RW(inactive_time);
- spin_lock_irqsave(&sd->lock, flags);
- sd->removed = true;
- spin_unlock_irqrestore(&sd->lock, flags);
+/* ChatMix level attribute (read-only) */
+static ssize_t chatmix_level_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct steelseries_device *sd = hid_get_drvdata(hdev);
- cancel_delayed_work_sync(&sd->battery_work);
+ return sysfs_emit(buf, "%d\n", sd->chatmix_level);
+}
+static DEVICE_ATTR_RO(chatmix_level);
- hid_hw_close(hdev);
- hid_hw_stop(hdev);
+/* Microphone mute LED brightness */
+static ssize_t mic_mute_led_brightness_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ return sysfs_emit(buf,
+ "Write-only (0-3 or 0-10 depending on device)\n");
}
-static const __u8 *steelseries_srws1_report_fixup(struct hid_device *hdev,
- __u8 *rdesc, unsigned int *rsize)
+static ssize_t mic_mute_led_brightness_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
{
- if (hdev->vendor != USB_VENDOR_ID_STEELSERIES ||
- hdev->product != USB_DEVICE_ID_STEELSERIES_SRWS1)
- return rdesc;
+ struct hid_device *hdev = to_hid_device(dev);
+ u16 product = hdev->product;
+ unsigned int value;
+ u8 data[64] = { 0 };
+ int ret;
- if (*rsize >= 115 && rdesc[11] == 0x02 && rdesc[13] == 0xc8
- && rdesc[29] == 0xbb && rdesc[40] == 0xc5) {
- hid_info(hdev, "Fixing up Steelseries SRW-S1 report descriptor\n");
- *rsize = sizeof(steelseries_srws1_rdesc_fixed);
- return steelseries_srws1_rdesc_fixed;
+ if (kstrtouint(buf, 10, &value))
+ return -EINVAL;
+
+ /* Device-specific validation */
+ if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3) {
+ if (value > 3)
+ return -EINVAL;
+ } else if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5 ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5_X) {
+ if (value > 10)
+ return -EINVAL;
+ /* Map special values */
+ if (value == 2)
+ value = 0x04;
+ else if (value == 3)
+ value = 0x0a;
+ } else {
+ if (value > 3)
+ return -EINVAL;
}
- return rdesc;
+
+ if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3) {
+ data[0] = 0x06;
+ data[1] = 0xae;
+ data[2] = value;
+ ret = steelseries_send_output_report(hdev, data, 64);
+ if (ret >= 0) {
+ data[0] = 0x06;
+ data[1] = 0x09;
+ steelseries_send_output_report(hdev, data, 64);
+ }
+ } else if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5 ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5_X) {
+ data[0] = 0x00;
+ data[1] = 0xae;
+ data[2] = value;
+ ret = steelseries_send_output_report(hdev, data, 64);
+ if (ret >= 0) {
+ data[0] = 0x00;
+ data[1] = 0x09;
+ steelseries_send_output_report(hdev, data, 64);
+ data[0] = 0x00;
+ data[1] = 0x35;
+ data[2] = 0x01;
+ steelseries_send_output_report(hdev, data, 64);
+ }
+ } else {
+ /* Nova 7 series */
+ data[0] = 0x00;
+ data[1] = 0xae;
+ data[2] = value;
+ ret = steelseries_send_output_report(hdev, data, 64);
+ if (ret >= 0) {
+ memset(data, 0, sizeof(data));
+ data[0] = 0x00;
+ data[1] = 0x09;
+ steelseries_send_output_report(hdev, data, 64);
+ }
+ }
+
+ return (ret < 0) ? ret : count;
}
+static DEVICE_ATTR_RW(mic_mute_led_brightness);
-static uint8_t steelseries_headset_map_capacity(uint8_t capacity, uint8_t min_in, uint8_t max_in)
+/* Microphone volume */
+static ssize_t mic_volume_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
{
- if (capacity >= max_in)
- return 100;
- if (capacity <= min_in)
- return 0;
- return (capacity - min_in) * 100 / (max_in - min_in);
+ return sysfs_emit(buf, "Write-only (0-128)\n");
+}
+
+static ssize_t mic_volume_store(struct device *dev,
+ struct device_attribute *attr, const char *buf,
+ size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ u16 product = hdev->product;
+ unsigned int value;
+ u8 data[64] = { 0 };
+ u8 mapped;
+ int ret;
+
+ if (kstrtouint(buf, 10, &value))
+ return -EINVAL;
+ if (value > 128)
+ return -EINVAL;
+
+ /* Map 0-128 to device-specific range */
+ if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3) {
+ /* Map to 0-10 */
+ if (value < 13)
+ mapped = 0x00;
+ else if (value < 25)
+ mapped = 0x01;
+ else if (value < 37)
+ mapped = 0x02;
+ else if (value < 49)
+ mapped = 0x03;
+ else if (value < 61)
+ mapped = 0x04;
+ else if (value < 73)
+ mapped = 0x05;
+ else if (value < 85)
+ mapped = 0x06;
+ else if (value < 97)
+ mapped = 0x07;
+ else if (value < 109)
+ mapped = 0x08;
+ else if (value < 121)
+ mapped = 0x09;
+ else
+ mapped = 0x0a;
+
+ data[0] = 0x06;
+ data[1] = 0x37;
+ data[2] = mapped;
+ ret = steelseries_send_output_report(hdev, data, 64);
+ if (ret >= 0) {
+ data[0] = 0x06;
+ data[1] = 0x09;
+ steelseries_send_output_report(hdev, data, 64);
+ }
+ } else if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_P ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_X) {
+ /* Map to 0-14 */
+ mapped = (value * 0x0e) / 128;
+ data[0] = 0x37;
+ data[1] = mapped;
+ ret = steelseries_send_output_report(hdev, data, 64);
+ if (ret >= 0) {
+ data[0] = 0x09;
+ steelseries_send_output_report(hdev, data, 64);
+ }
+ } else if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5 ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5_X) {
+ /* Map to 0-15 */
+ mapped = value / 8;
+ if (mapped == 16)
+ mapped = 15;
+
+ data[0] = 0x00;
+ data[1] = 0x37;
+ data[2] = mapped;
+ ret = steelseries_send_output_report(hdev, data, 64);
+ if (ret >= 0) {
+ data[0] = 0x00;
+ data[1] = 0x09;
+ steelseries_send_output_report(hdev, data, 64);
+ data[0] = 0x00;
+ data[1] = 0x35;
+ data[2] = 0x01;
+ steelseries_send_output_report(hdev, data, 64);
+ }
+ } else {
+ /* Nova 7: map to 0-7 */
+ mapped = value / 16;
+ if (mapped == 8)
+ mapped = 7;
+
+ data[0] = 0x00;
+ data[1] = 0x37;
+ data[2] = mapped;
+ ret = steelseries_send_output_report(hdev, data, 64);
+ if (ret >= 0) {
+ memset(data, 0, sizeof(data));
+ data[0] = 0x00;
+ data[1] = 0x09;
+ steelseries_send_output_report(hdev, data, 64);
+ }
+ }
+
+ return (ret < 0) ? ret : count;
+}
+static DEVICE_ATTR_RW(mic_volume);
+
+/* Volume limiter */
+static ssize_t volume_limiter_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "Write-only (0=off, 1=on)\n");
+}
+
+static ssize_t volume_limiter_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ u16 product = hdev->product;
+ unsigned int value;
+ u8 data[64] = { 0 };
+ int ret;
+
+ if (kstrtouint(buf, 10, &value))
+ return -EINVAL;
+ if (value > 1)
+ return -EINVAL;
+
+ if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5 ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5_X) {
+ data[0] = 0x00;
+ data[1] = 0x27;
+ data[2] = value;
+ ret = steelseries_send_output_report(hdev, data, 64);
+ if (ret >= 0) {
+ data[0] = 0x00;
+ data[1] = 0x09;
+ steelseries_send_output_report(hdev, data, 64);
+ data[0] = 0x00;
+ data[1] = 0x35;
+ data[2] = 0x01;
+ steelseries_send_output_report(hdev, data, 64);
+ }
+ } else {
+ /* Nova 7 series */
+ data[0] = 0x00;
+ data[1] = 0x3a;
+ data[2] = value;
+ ret = steelseries_send_output_report(hdev, data, 64);
+ if (ret >= 0) {
+ memset(data, 0, sizeof(data));
+ data[0] = 0x00;
+ data[1] = 0x09;
+ steelseries_send_output_report(hdev, data, 64);
+ }
+ }
+
+ return (ret < 0) ? ret : count;
}
+static DEVICE_ATTR_RW(volume_limiter);
-static int steelseries_headset_raw_event(struct hid_device *hdev,
- struct hid_report *report, u8 *read_buf,
- int size)
+/* Bluetooth when powered on */
+static ssize_t bluetooth_on_power_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
{
+ return sysfs_emit(buf, "Write-only (0=off, 1=on)\n");
+}
+
+static ssize_t bluetooth_on_power_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ unsigned int value;
+ u8 data[64] = { 0 };
+ int ret;
+
+ if (kstrtouint(buf, 10, &value))
+ return -EINVAL;
+ if (value > 1)
+ return -EINVAL;
+
+ data[0] = 0x00;
+ data[1] = 0xb2;
+ data[2] = value;
+ ret = steelseries_send_output_report(hdev, data, 64);
+ if (ret >= 0) {
+ /* Send save state command as output report */
+ memset(data, 0, sizeof(data));
+ data[0] = 0x00;
+ data[1] = 0x09;
+ steelseries_send_output_report(hdev, data, 64);
+ }
+
+ return (ret < 0) ? ret : count;
+}
+static DEVICE_ATTR_RW(bluetooth_on_power);
+
+/* Bluetooth call volume */
+static ssize_t bluetooth_call_vol_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf,
+ "Write-only (0=nothing, 1=-12dB, 2=mute game)\n");
+}
+
+static ssize_t bluetooth_call_vol_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ unsigned int value;
+ u8 data[64] = { 0 };
+ int ret;
+
+ if (kstrtouint(buf, 10, &value))
+ return -EINVAL;
+ if (value > 2)
+ return -EINVAL;
+
+ data[0] = 0x00;
+ data[1] = 0xb3;
+ data[2] = value;
+ ret = steelseries_send_output_report(hdev, data, 64);
+
+ return (ret < 0) ? ret : count;
+}
+static DEVICE_ATTR_RW(bluetooth_call_vol);
+
+/* Attribute group setup based on capabilities */
+static struct attribute *steelseries_attrs[] = {
+ &dev_attr_sidetone_level.attr,
+ &dev_attr_inactive_time.attr,
+ &dev_attr_chatmix_level.attr,
+ &dev_attr_mic_mute_led_brightness.attr,
+ &dev_attr_mic_volume.attr,
+ &dev_attr_volume_limiter.attr,
+ &dev_attr_bluetooth_on_power.attr,
+ &dev_attr_bluetooth_call_vol.attr,
+ NULL
+};
+
+static umode_t steelseries_attr_is_visible(struct kobject *kobj,
+ struct attribute *attr, int n)
+{
+ struct device *dev = kobj_to_dev(kobj);
+ struct hid_device *hdev = to_hid_device(dev);
struct steelseries_device *sd = hid_get_drvdata(hdev);
+ unsigned long caps = sd->info->capabilities;
+
+ if (attr == &dev_attr_sidetone_level.attr)
+ return (caps & SS_CAP_SIDETONE) ? attr->mode : 0;
+ if (attr == &dev_attr_inactive_time.attr)
+ return (caps & SS_CAP_INACTIVE_TIME) ? attr->mode : 0;
+ if (attr == &dev_attr_chatmix_level.attr)
+ return (caps & SS_CAP_CHATMIX) ? attr->mode : 0;
+ if (attr == &dev_attr_mic_mute_led_brightness.attr)
+ return (caps & SS_CAP_MIC_MUTE_LED) ? attr->mode : 0;
+ if (attr == &dev_attr_mic_volume.attr)
+ return (caps & SS_CAP_MIC_VOLUME) ? attr->mode : 0;
+ if (attr == &dev_attr_volume_limiter.attr)
+ return (caps & SS_CAP_VOLUME_LIMITER) ? attr->mode : 0;
+ if (attr == &dev_attr_bluetooth_on_power.attr)
+ return (caps & SS_CAP_BT_POWER_ON) ? attr->mode : 0;
+ if (attr == &dev_attr_bluetooth_call_vol.attr)
+ return (caps & SS_CAP_BT_CALL_VOL) ? attr->mode : 0;
+
+ return 0;
+}
+
+static const struct attribute_group steelseries_attr_group = {
+ .attrs = steelseries_attrs,
+ .is_visible = steelseries_attr_is_visible,
+};
+
+static int steelseries_arctis_1_request_battery(struct hid_device *hdev)
+{
+ const u8 data[] = { 0x06, 0x12 };
+
+ return steelseries_send_feature_report(hdev, data, sizeof(data));
+}
+
+static int steelseries_arctis_7_plus_request_battery(struct hid_device *hdev)
+{
+ const u8 data[] = { 0x00, 0xb0 };
+
+ return steelseries_send_output_report(hdev, data, sizeof(data));
+}
+
+static int steelseries_arctis_9_request_battery(struct hid_device *hdev)
+{
+ const u8 data[] = { 0x00, 0x20 };
+
+ return steelseries_send_feature_report(hdev, data, sizeof(data));
+}
+
+static int steelseries_arctis_nova_request_battery(struct hid_device *hdev)
+{
+ const u8 data[] = { 0x00, 0xb0 };
+
+ return steelseries_send_output_report(hdev, data, sizeof(data));
+}
+
+static int steelseries_arctis_nova_3p_request_battery(struct hid_device *hdev)
+{
+ const u8 data[] = { 0xb0 };
+
+ return steelseries_send_output_report(hdev, data, sizeof(data));
+}
+
+static int
+steelseries_arctis_pro_wireless_request_battery(struct hid_device *hdev)
+{
+ /* Request battery - response will arrive asynchronously via raw_event */
+ const u8 data[] = { 0x40, 0xAA };
+
+ return steelseries_send_output_report(hdev, data, sizeof(data));
+}
+
+static int steelseries_raw_event(struct hid_device *hdev,
+ struct hid_report *report, u8 *data, int size)
+{
+ struct steelseries_device *sd = hid_get_drvdata(hdev);
+ u16 product = hdev->product;
int capacity = sd->battery_capacity;
bool connected = sd->headset_connected;
bool charging = sd->battery_charging;
- unsigned long flags;
+ int chatmix = sd->chatmix_level;
+ unsigned long flags = 0;
- /* Not a headset */
- if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1)
+ /* Skip SRW-S1 */
+ if (product == USB_DEVICE_ID_STEELSERIES_SRWS1)
return 0;
- if (hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1) {
- hid_dbg(sd->hdev,
- "Parsing raw event for Arctis 1 headset (%*ph)\n", size, read_buf);
- if (size < ARCTIS_1_BATTERY_RESPONSE_LEN ||
- memcmp(read_buf, arctis_1_battery_request, sizeof(arctis_1_battery_request))) {
- if (!delayed_work_pending(&sd->battery_work))
- goto request_battery;
- return 0;
+ /* Arctis 1 family (Arctis 1, 1X, 7P, 7X) */
+ if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1 ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_7_P ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_7_X) {
+ if (size < 8)
+ goto schedule_work;
+
+ if (data[2] == 0x01) {
+ connected = false;
+ capacity = 100;
+ } else {
+ connected = true;
+ capacity = data[3];
+ if (capacity > 100)
+ capacity = 100;
}
- if (read_buf[2] == 0x01) {
+ }
+
+ /* Arctis 7 (original and 2019) */
+ else if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_7 ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_7_GEN2) {
+ /* Battery response is 8 bytes for Arctis 7 */
+ if (size < 8)
+ goto schedule_work;
+
+ connected = true;
+ charging = false;
+
+ /* Battery level is in data[2] */
+ capacity = data[2];
+ if (capacity > 100)
+ capacity = 100;
+ }
+
+ /* Arctis 7+ family */
+ else if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_P ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_X ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_DESTINY) {
+ if (size < 6)
+ goto schedule_work;
+
+ /* data[1] == 0x01 means HEADSET_OFFLINE */
+ if (data[1] == 0x01) {
connected = false;
capacity = 100;
} else {
connected = true;
- capacity = read_buf[3];
+ /* data[3] == 0x01 means charging */
+ charging = (data[3] == 0x01);
+ /* data[2] contains battery level (0x00-0x04 range) */
+ capacity = steelseries_map_battery(data[2], 0x00, 0x04);
+
+ /* ChatMix available */
+ if (size >= 6 &&
+ (sd->info->capabilities & SS_CAP_CHATMIX)) {
+ /* data[4] is game (0-100), data[5] is chat (0-100) */
+ int game = (data[4] * 64) / 100;
+ int chat = (data[5] * -64) / 100;
+
+ chatmix = 64 - (chat + game);
+ }
}
}
- if (hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_9) {
- hid_dbg(sd->hdev,
- "Parsing raw event for Arctis 9 headset (%*ph)\n", size, read_buf);
- if (size < ARCTIS_9_BATTERY_RESPONSE_LEN) {
- if (!delayed_work_pending(&sd->battery_work))
- goto request_battery;
- return 0;
+ /* Arctis 9 */
+ else if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_9) {
+ if (size < 12)
+ goto schedule_work;
+
+ connected = true;
+
+ charging = (data[4] == 0x01);
+
+ capacity = steelseries_map_battery(data[3], 0x64, 0x9A);
+
+ /* ChatMix: data[9] is game (0-19), data[10] is chat (0-19) */
+ if (size >= 11 && (sd->info->capabilities & SS_CAP_CHATMIX)) {
+ int game = (data[9] * 64) / 19;
+ int chat = (data[10] * -64) / 19;
+
+ chatmix = 64 - (chat + game);
}
+ }
- if (read_buf[0] == 0xaa && read_buf[1] == 0x01) {
+ /* Arctis Pro Wireless */
+ else if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_PRO) {
+ if (size >= 2 && (data[0] == 0x02 || data[0] == 0x04)) {
+ /* This is a connection status response */
+ /* HEADSET_OFFLINE */
+ if (data[0] == 0x02) {
+ connected = false;
+ capacity = 100;
+ charging = false;
+ }
+ /* HEADSET_ONLINE (0x04) */
+ else {
+ connected = true;
+ charging = false;
+ }
+ } else if (size >= 1 && sd->headset_connected) {
+ /* This is a battery level response (only valid if headset connected) */
+ /* Battery range is 0x00-0x04 */
+ capacity = steelseries_map_battery(data[0], 0x00, 0x04);
+ }
+ }
+
+ /* Arctis Nova 3 */
+ else if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3) {
+ /* No battery monitoring for wired headset */
+ goto schedule_work;
+ }
+
+ /* Arctis Nova 3P/3X Wireless */
+ else if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_P ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_X) {
+ if (size < 4)
+ goto schedule_work;
+
+ /* data[1] == 0x02 means HEADSET_OFFLINE */
+ if (data[1] == 0x02) {
+ connected = false;
+ capacity = 100;
+ } else {
connected = true;
- charging = read_buf[4] == 0x01;
+ charging = false;
+ /* data[3] contains battery level (0x00-0x64 range, 0-100) */
+ capacity = steelseries_map_battery(data[3], 0x00, 0x64);
+ }
+ }
+
+ /* Arctis Nova 5/5X */
+ else if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5 ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5_X) {
+ if (size < 16)
+ goto schedule_work;
+
+ /* data[1] == 0x02 means HEADSET_OFFLINE */
+ if (data[1] == 0x02) {
+ connected = false;
+ capacity = 100;
+ } else {
+ connected = true;
+ /* data[4] == 0x01 means charging */
+ charging = (data[4] == 0x01);
+ /* data[3] contains battery level (0-100) */
+ capacity = data[3];
+ if (capacity > 100)
+ capacity = 100;
+
+ /* ChatMix available */
+ if (size >= 7 &&
+ (sd->info->capabilities & SS_CAP_CHATMIX)) {
+ /* data[5] is game (0-100), data[6] is chat (0-100) */
+ int game = (data[5] * 64) / 100;
+ int chat = (data[6] * -64) / 100;
+
+ chatmix = 64 - (chat + game);
+ }
+ }
+ }
- /*
- * Found no official documentation about min and max.
- * Values defined by testing.
- */
- capacity = steelseries_headset_map_capacity(read_buf[3], 0x68, 0x9d);
+ /* Arctis Nova 7 family */
+ else if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7 ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_P ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_REV2 ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_WOW ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_GEN2 ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2) {
+ if (size < 8)
+ goto schedule_work;
+
+ /* data[3] == 0x00 means HEADSET_OFFLINE */
+ if (data[3] == 0x00) {
+ connected = false;
+ capacity = 100;
} else {
- /*
- * Device is off and sends the last known status read_buf[1] == 0x03 or
- * there is no known status of the device read_buf[0] == 0x55
- */
+ connected = true;
+ /* data[3] == 0x01 means charging */
+ charging = (data[3] == 0x01);
+ /* data[2] contains battery level (0x00-0x04 range) */
+ capacity = steelseries_map_battery(data[2], 0x00, 0x04);
+
+ /* ChatMix available */
+ if (size >= 6 &&
+ (sd->info->capabilities & SS_CAP_CHATMIX)) {
+ /* data[4] is game (0-100), data[5] is chat (0-100) */
+ int game = (data[4] * 64) / 100;
+ int chat = (data[5] * -64) / 100;
+
+ chatmix = 64 - (chat + game);
+ }
+ }
+ }
+
+ /* Arctis Nova Pro Wireless */
+ else if (product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_PRO ||
+ product == USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_PRO_X) {
+ if (size < 16)
+ goto schedule_work;
+
+ /* data[15] contains headset status */
+ if (data[15] == 0x01) { /* HEADSET_OFFLINE */
connected = false;
+ capacity = 100;
+ } else if (data[15] == 0x02) { /* HEADSET_CABLE_CHARGING */
+ connected = true;
+ charging = true;
+ /* data[6] contains battery level (0x00-0x08 range) */
+ capacity = steelseries_map_battery(data[6], 0x00, 0x08);
+ } else if (data[15] == 0x08) { /* HEADSET_ONLINE */
+ connected = true;
charging = false;
+ /* data[6] contains battery level (0x00-0x08 range) */
+ capacity = steelseries_map_battery(data[6], 0x00, 0x08);
+ } else {
+ /* Unknown status */
+ goto schedule_work;
}
}
+ /* Update state if changed */
+ spin_lock_irqsave(&sd->lock, flags);
+
if (connected != sd->headset_connected) {
- hid_dbg(sd->hdev,
+ hid_dbg(hdev,
"Connected status changed from %sconnected to %sconnected\n",
sd->headset_connected ? "" : "not ",
connected ? "" : "not ");
sd->headset_connected = connected;
+ spin_unlock_irqrestore(&sd->lock, flags);
steelseries_headset_set_wireless_status(hdev, connected);
+ spin_lock_irqsave(&sd->lock, flags);
}
if (capacity != sd->battery_capacity) {
- hid_dbg(sd->hdev,
- "Battery capacity changed from %d%% to %d%%\n",
+ hid_dbg(hdev, "Battery capacity changed from %d%% to %d%%\n",
sd->battery_capacity, capacity);
sd->battery_capacity = capacity;
+ spin_unlock_irqrestore(&sd->lock, flags);
power_supply_changed(sd->battery);
+ spin_lock_irqsave(&sd->lock, flags);
}
if (charging != sd->battery_charging) {
- hid_dbg(sd->hdev,
+ hid_dbg(hdev,
"Battery charging status changed from %scharging to %scharging\n",
sd->battery_charging ? "" : "not ",
charging ? "" : "not ");
sd->battery_charging = charging;
+ spin_unlock_irqrestore(&sd->lock, flags);
power_supply_changed(sd->battery);
+ spin_lock_irqsave(&sd->lock, flags);
}
-request_battery:
- spin_lock_irqsave(&sd->lock, flags);
+ if (chatmix != sd->chatmix_level)
+ sd->chatmix_level = chatmix;
+
+schedule_work:
if (!sd->removed)
- schedule_delayed_work(&sd->battery_work,
- msecs_to_jiffies(STEELSERIES_HEADSET_BATTERY_TIMEOUT_MS));
+ schedule_delayed_work(
+ &sd->battery_work,
+ msecs_to_jiffies(
+ STEELSERIES_HEADSET_BATTERY_TIMEOUT_MS));
spin_unlock_irqrestore(&sd->lock, flags);
return 0;
}
-static const struct hid_device_id steelseries_devices[] = {
- { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_SRWS1),
- .driver_data = STEELSERIES_SRWS1 },
+static bool steelseries_is_vendor_usage_page(struct hid_device *hdev,
+ u8 usage_page)
+{
+ return hdev->rdesc[0] == 0x06 && hdev->rdesc[1] == usage_page &&
+ hdev->rdesc[2] == 0xff;
+}
+
+static int steelseries_probe(struct hid_device *hdev,
+ const struct hid_device_id *id)
+{
+ struct steelseries_device_info *info =
+ (struct steelseries_device_info *)id->driver_data;
+ struct steelseries_device *sd;
+ struct usb_interface *intf;
+ u8 interface_num;
+ int ret;
+
+ /* Legacy SRW-S1 handling */
+ if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1) {
+#if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
+ (IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
+ return steelseries_srws1_probe(hdev, id);
+#else
+ return -ENODEV;
+#endif
+ }
+
+ /* Get interface number for binding check */
+ if (hid_is_usb(hdev)) {
+ intf = to_usb_interface(hdev->dev.parent);
+ interface_num = intf->cur_altsetting->desc.bInterfaceNumber;
+ } else {
+ /* Non-USB devices not supported for modern Arctis */
+ return -ENODEV;
+ }
+
+ /* Interface binding logic */
+ if (info->interface_binding_mode == 0) {
+ /* Mode 0: First enumerated (interface 0) */
+ if (interface_num != 0)
+ return -ENODEV;
+ } else {
+ /* Mode 1: Check bitmask */
+ if (!(info->valid_interfaces & BIT(interface_num)))
+ return -ENODEV;
+ }
+
+ sd = devm_kzalloc(&hdev->dev, sizeof(*sd), GFP_KERNEL);
+ if (!sd)
+ return -ENOMEM;
+
+ sd->hdev = hdev;
+ sd->info = info;
+ hid_set_drvdata(hdev, sd);
+
+ ret = hid_parse(hdev);
+ if (ret)
+ return ret;
+
+ /* Arctis 9 requires vendor usage page check */
+ if (hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_9 &&
+ !steelseries_is_vendor_usage_page(hdev, 0xc0))
+ return -ENODEV;
+
+ spin_lock_init(&sd->lock);
+
+ ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+ if (ret)
+ return ret;
+
+ ret = hid_hw_open(hdev);
+ if (ret)
+ goto err_stop;
+
+ /* Register battery if supported */
+ if (info->capabilities & SS_CAP_BATTERY) {
+ ret = steelseries_battery_register(sd);
+ if (ret < 0)
+ hid_warn(hdev, "Failed to register battery: %d\n", ret);
+ }
+
+ /* Create sysfs attributes */
+ ret = sysfs_create_group(&hdev->dev.kobj, &steelseries_attr_group);
+ if (ret)
+ hid_warn(hdev, "Failed to create sysfs attributes: %d\n", ret);
+
+ hid_info(hdev, "SteelSeries %s initialized\n", info->name);
- { /* SteelSeries Arctis 1 Wireless for XBox */
- HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_1),
- .driver_data = STEELSERIES_ARCTIS_1 },
+ return 0;
- { /* SteelSeries Arctis 9 Wireless for XBox */
- HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_9),
- .driver_data = STEELSERIES_ARCTIS_9 },
+err_stop:
+ hid_hw_stop(hdev);
+ return ret;
+}
- { }
+static void steelseries_remove(struct hid_device *hdev)
+{
+ struct steelseries_device *sd;
+ unsigned long flags;
+
+ /* Legacy SRW-S1 */
+ if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1) {
+#if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
+ (IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
+ hid_hw_stop(hdev);
+#endif
+ return;
+ }
+
+ sd = hid_get_drvdata(hdev);
+
+ sysfs_remove_group(&hdev->dev.kobj, &steelseries_attr_group);
+
+ spin_lock_irqsave(&sd->lock, flags);
+ sd->removed = true;
+ spin_unlock_irqrestore(&sd->lock, flags);
+
+ cancel_delayed_work_sync(&sd->battery_work);
+
+ hid_hw_close(hdev);
+ hid_hw_stop(hdev);
+}
+
+static const struct hid_device_id steelseries_devices[] = {
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_SRWS1),
+ .driver_data = STEELSERIES_SRWS1 },
+
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_1),
+ .driver_data = (unsigned long)&arctis_1_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X),
+ .driver_data = (unsigned long)&arctis_1_x_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_7),
+ .driver_data = (unsigned long)&arctis_7_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_7_P),
+ .driver_data = (unsigned long)&arctis_7_p_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_7_X),
+ .driver_data = (unsigned long)&arctis_7_x_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_7_GEN2),
+ .driver_data = (unsigned long)&arctis_7_gen2_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS),
+ .driver_data = (unsigned long)&arctis_7_plus_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_P),
+ .driver_data = (unsigned long)&arctis_7_plus_p_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_X),
+ .driver_data = (unsigned long)&arctis_7_plus_x_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_DESTINY),
+ .driver_data = (unsigned long)&arctis_7_plus_destiny_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_9),
+ .driver_data = (unsigned long)&arctis_9_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_PRO),
+ .driver_data = (unsigned long)&arctis_pro_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3),
+ .driver_data = (unsigned long)&arctis_nova_3_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_P),
+ .driver_data = (unsigned long)&arctis_nova_3_p_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_X),
+ .driver_data = (unsigned long)&arctis_nova_3_x_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5),
+ .driver_data = (unsigned long)&arctis_nova_5_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5_X),
+ .driver_data = (unsigned long)&arctis_nova_5_x_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7),
+ .driver_data = (unsigned long)&arctis_nova_7_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X),
+ .driver_data = (unsigned long)&arctis_nova_7_x_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_P),
+ .driver_data = (unsigned long)&arctis_nova_7_p_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_REV2),
+ .driver_data = (unsigned long)&arctis_nova_7_x_rev2_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO),
+ .driver_data = (unsigned long)&arctis_nova_7_diablo_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_WOW),
+ .driver_data = (unsigned long)&arctis_nova_7_wow_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_GEN2),
+ .driver_data = (unsigned long)&arctis_nova_7_gen2_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2),
+ .driver_data = (unsigned long)&arctis_nova_7_x_gen2_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_PRO),
+ .driver_data = (unsigned long)&arctis_nova_pro_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_PRO_X),
+ .driver_data = (unsigned long)&arctis_nova_pro_x_info },
+ {}
};
MODULE_DEVICE_TABLE(hid, steelseries_devices);
@@ -742,12 +2160,13 @@ static struct hid_driver steelseries_driver = {
.probe = steelseries_probe,
.remove = steelseries_remove,
.report_fixup = steelseries_srws1_report_fixup,
- .raw_event = steelseries_headset_raw_event,
+ .raw_event = steelseries_raw_event,
};
-
module_hid_driver(steelseries_driver);
-MODULE_DESCRIPTION("HID driver for Steelseries devices");
+
+MODULE_DESCRIPTION("HID driver for SteelSeries devices");
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Bastien Nocera <hadess@...ess.net>");
MODULE_AUTHOR("Simon Wood <simon@...gewell.org>");
MODULE_AUTHOR("Christian Mayer <git@...er-bgk.de>");
+MODULE_AUTHOR("Sriman Achanta <srimanachanta@...il.com>");
--
2.52.0
Powered by blists - more mailing lists