* [PATCH v3 01/18] HID: steelseries: Fix ARCTIS_1_X device mislabeling
2026-02-27 23:50 [PATCH v3 00/18] HID: steelseries: Add support for Arctis headset lineup Sriman Achanta
@ 2026-02-27 23:50 ` Sriman Achanta
2026-03-03 10:58 ` Bastien Nocera
2026-02-27 23:50 ` [PATCH v3 02/18] HID: hid-ids: Add SteelSeries Arctis headset device IDs Sriman Achanta
` (17 subsequent siblings)
18 siblings, 1 reply; 29+ messages in thread
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
Christian Mayer, Sriman Achanta
The SteelSeries Arctis 1 Wireless for Xbox (device ID 0x12b6) was
previously mislabeled as the regular Arctis 1 Wireless (device ID
0x12b3). This commit corrects the labeling by introducing a new
STEELSERIES_ARCTIS_1_X quirk flag and device table entry.
Both the Arctis 1 and Arctis 1 Wireless for Xbox share the same battery
reporting protocol, so they are handled identically in the battery fetch
and raw event processing functions.
Changes:
- Add STEELSERIES_ARCTIS_1_X quirk flag definition
- Shift STEELSERIES_ARCTIS_9 quirk flag bit accordingly
- Add device table entry for USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X
- Update steelseries_headset_fetch_battery() to handle both Arctis 1
variants
- Update steelseries_headset_raw_event() to handle both Arctis 1
variants
- Update device comment to clarify Arctis 1 vs Arctis 1 Wireless for
Xbox
Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
drivers/hid/hid-ids.h | 5 +++--
drivers/hid/hid-quirks.c | 1 +
drivers/hid/hid-steelseries.c | 19 +++++++++++++------
3 files changed, 17 insertions(+), 8 deletions(-)
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 3e299a30dcde..b01704f37142 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1329,8 +1329,9 @@
#define USB_VENDOR_ID_STEELSERIES 0x1038
#define USB_DEVICE_ID_STEELSERIES_SRWS1 0x1410
-#define USB_DEVICE_ID_STEELSERIES_ARCTIS_1 0x12b6
-#define USB_DEVICE_ID_STEELSERIES_ARCTIS_9 0x12c2
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_1 0x12b3
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X 0x12b6
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_9 0x12c2
#define USB_VENDOR_ID_SUN 0x0430
#define USB_DEVICE_ID_RARITAN_KVM_DONGLE 0xcdab
diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c
index edc4339adb50..17349eac5c3e 100644
--- a/drivers/hid/hid-quirks.c
+++ b/drivers/hid/hid-quirks.c
@@ -713,6 +713,7 @@ static const struct hid_device_id hid_have_special_driver[] = {
#if IS_ENABLED(CONFIG_HID_STEELSERIES)
{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_SRWS1) },
{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_1) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X) },
{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_9) },
#endif
#if IS_ENABLED(CONFIG_HID_SUNPLUS)
diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index f98435631aa1..d3711022bf86 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -19,7 +19,8 @@
#define STEELSERIES_SRWS1 BIT(0)
#define STEELSERIES_ARCTIS_1 BIT(1)
-#define STEELSERIES_ARCTIS_9 BIT(2)
+#define STEELSERIES_ARCTIS_1_X BIT(2)
+#define STEELSERIES_ARCTIS_9 BIT(3)
struct steelseries_device {
struct hid_device *hdev;
@@ -97,7 +98,7 @@ static const __u8 steelseries_srws1_rdesc_fixed[] = {
0x29, 0x11, /* Usage Maximum (11h), */
0x95, 0x11, /* Report Count (17), */
0x81, 0x02, /* Input (Variable), */
- /* ---- Dial patch starts here ---- */
+ /* ---- Dial patch starts here ---- */
0x05, 0x01, /* Usage Page (Desktop), */
0x09, 0x33, /* Usage (RX), */
0x75, 0x04, /* Report Size (4), */
@@ -110,7 +111,7 @@ static const __u8 steelseries_srws1_rdesc_fixed[] = {
0x95, 0x01, /* Report Count (1), */
0x25, 0x03, /* Logical Maximum (3), */
0x81, 0x02, /* Input (Variable), */
- /* ---- Dial patch ends here ---- */
+ /* ---- Dial patch ends here ---- */
0x06, 0x00, 0xFF, /* Usage Page (FF00h), */
0x09, 0x01, /* Usage (01h), */
0x75, 0x04, /* Changed Report Size (4), */
@@ -374,7 +375,8 @@ static void steelseries_headset_fetch_battery(struct hid_device *hdev)
{
int ret = 0;
- if (hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1)
+ if (hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1 ||
+ hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X)
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)
@@ -638,7 +640,8 @@ static int steelseries_headset_raw_event(struct hid_device *hdev,
if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1)
return 0;
- if (hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1) {
+ if (hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1 ||
+ hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X) {
hid_dbg(sd->hdev,
"Parsing raw event for Arctis 1 headset (%*ph)\n", size, read_buf);
if (size < ARCTIS_1_BATTERY_RESPONSE_LEN ||
@@ -724,10 +727,14 @@ static const struct hid_device_id steelseries_devices[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_SRWS1),
.driver_data = STEELSERIES_SRWS1 },
- { /* SteelSeries Arctis 1 Wireless for XBox */
+ { /* SteelSeries Arctis 1 Wireless */
HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_1),
.driver_data = STEELSERIES_ARCTIS_1 },
+ { /* SteelSeries Arctis 1 Wireless for XBox */
+ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X),
+ .driver_data = STEELSERIES_ARCTIS_1_X },
+
{ /* SteelSeries Arctis 9 Wireless for XBox */
HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_9),
.driver_data = STEELSERIES_ARCTIS_9 },
--
2.53.0
^ permalink raw reply related [flat|nested] 29+ messages in thread* Re: [PATCH v3 01/18] HID: steelseries: Fix ARCTIS_1_X device mislabeling
2026-02-27 23:50 ` [PATCH v3 01/18] HID: steelseries: Fix ARCTIS_1_X device mislabeling Sriman Achanta
@ 2026-03-03 10:58 ` Bastien Nocera
0 siblings, 0 replies; 29+ messages in thread
From: Bastien Nocera @ 2026-03-03 10:58 UTC (permalink / raw)
To: Sriman Achanta, Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer
On Fri, 2026-02-27 at 18:50 -0500, Sriman Achanta wrote:
> @@ -97,7 +98,7 @@ static const __u8 steelseries_srws1_rdesc_fixed[] =
> {
> 0x29, 0x11, /* Usage Maximum (11h), */
> 0x95, 0x11, /* Report Count (17), */
> 0x81, 0x02, /* Input (Variable), */
> - /* ---- Dial patch starts here ---- */
> + /* ---- Dial patch starts here ---- */
> 0x05, 0x01, /* Usage Page (Desktop), */
> 0x09, 0x33, /* Usage (RX), */
> 0x75, 0x04, /* Report Size (4), */
> @@ -110,7 +111,7 @@ static const __u8 steelseries_srws1_rdesc_fixed[]
> = {
> 0x95, 0x01, /* Report Count (1), */
> 0x25, 0x03, /* Logical Maximum (3), */
> 0x81, 0x02, /* Input (Variable), */
> - /* ---- Dial patch ends here ---- */
> + /* ---- Dial patch ends here ---- */
> 0x06, 0x00, 0xFF, /* Usage Page (FF00h), */
> 0x09, 0x01, /* Usage (01h), */
> 0x75, 0x04, /* Changed Report Size (4), */
Unrelated whitespace changes
^ permalink raw reply [flat|nested] 29+ messages in thread
* [PATCH v3 02/18] HID: hid-ids: Add SteelSeries Arctis headset device IDs
2026-02-27 23:50 [PATCH v3 00/18] HID: steelseries: Add support for Arctis headset lineup Sriman Achanta
2026-02-27 23:50 ` [PATCH v3 01/18] HID: steelseries: Fix ARCTIS_1_X device mislabeling Sriman Achanta
@ 2026-02-27 23:50 ` Sriman Achanta
2026-02-27 23:50 ` [PATCH v3 03/18] HID: quirks: Add additional " Sriman Achanta
` (16 subsequent siblings)
18 siblings, 0 replies; 29+ messages in thread
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
Christian Mayer, Sriman Achanta
Add USB device IDs for the complete SteelSeries Arctis headset lineup,
including:
- Arctis 1, 1 Wireless, 7, 7P, 7X variants
- Arctis 7+ series (PS5, Xbox, Destiny editions)
- Arctis 9 Wireless
- Arctis Pro Wireless
- Arctis Nova 3, 3P, 3X
- Arctis Nova 5, 5X
- Arctis Nova 7 series (multiple variants and special editions)
- Arctis Nova Pro Wireless and Pro X
These IDs will be used by the updated hid-steelseries driver to provide
battery monitoring, sidetone control, and other device-specific features
for these wireless gaming headsets.
Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
drivers/hid/hid-ids.h | 36 +++++++++++++++++++++++++++++++-----
1 file changed, 31 insertions(+), 5 deletions(-)
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index b01704f37142..e16b8dc5edd0 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1327,11 +1327,37 @@
#define USB_DEVICE_ID_STEAM_CONTROLLER_WIRELESS 0x1142
#define USB_DEVICE_ID_STEAM_DECK 0x1205
-#define USB_VENDOR_ID_STEELSERIES 0x1038
-#define USB_DEVICE_ID_STEELSERIES_SRWS1 0x1410
-#define USB_DEVICE_ID_STEELSERIES_ARCTIS_1 0x12b3
-#define USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X 0x12b6
-#define USB_DEVICE_ID_STEELSERIES_ARCTIS_9 0x12c2
+#define USB_VENDOR_ID_STEELSERIES 0x1038
+#define USB_DEVICE_ID_STEELSERIES_SRWS1 0x1410
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_1 0x12b3
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X 0x12b6
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_7 0x1260
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_7_P 0x12d5
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_7_X 0x12d7
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_7_GEN2 0x12ad
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS 0x220e
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_P 0x2212
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_X 0x2216
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_DESTINY 0x2236
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_9 0x12c2
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_P 0x2269
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_X 0x226d
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5 0x2232
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5_X 0x2253
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7 0x2202
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_2 0x22a1
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_P 0x220a
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X 0x2206
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_2 0x22a4
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_3 0x22a5
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO 0x223a
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO_2 0x22a9
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_WOW 0x227a
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_GEN2 0x227e
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2 0x229e
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2_2 0x2258
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_PRO 0x12e0
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_PRO_X 0x12e5
#define USB_VENDOR_ID_SUN 0x0430
#define USB_DEVICE_ID_RARITAN_KVM_DONGLE 0xcdab
--
2.53.0
^ permalink raw reply related [flat|nested] 29+ messages in thread* [PATCH v3 03/18] HID: quirks: Add additional Arctis headset device IDs
2026-02-27 23:50 [PATCH v3 00/18] HID: steelseries: Add support for Arctis headset lineup Sriman Achanta
2026-02-27 23:50 ` [PATCH v3 01/18] HID: steelseries: Fix ARCTIS_1_X device mislabeling Sriman Achanta
2026-02-27 23:50 ` [PATCH v3 02/18] HID: hid-ids: Add SteelSeries Arctis headset device IDs Sriman Achanta
@ 2026-02-27 23:50 ` Sriman Achanta
2026-03-03 10:58 ` Bastien Nocera
2026-02-27 23:50 ` [PATCH v3 04/18] HID: steelseries: Add async support and unify device definitions Sriman Achanta
` (15 subsequent siblings)
18 siblings, 1 reply; 29+ messages in thread
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
Christian Mayer, Sriman Achanta
Add support for additional SteelSeries Arctis headset models to the HID
quirks table. This enables proper device recognition and handling for:
- Arctis 7 series (7, 7P, 7X, 7 Gen2)
- Arctis 7 Plus series (7 Plus, 7 Plus P, 7 Plus X, 7 Plus Destiny)
- Arctis Pro
- Arctis Nova 3 series (3, 3P, 3X)
- Arctis Nova 5 series (5, 5X)
- Arctis Nova 7 series (7, 7X, 7P, 7X Rev2, 7 Diablo, 7 WoW, 7 Gen2, 7X
Gen2)
- Arctis Nova Pro series (Pro, Pro X)
Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
drivers/hid/hid-quirks.c | 26 ++++++++++++++++++++++++++
1 file changed, 26 insertions(+)
diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c
index 17349eac5c3e..65a6f6ab30b9 100644
--- a/drivers/hid/hid-quirks.c
+++ b/drivers/hid/hid-quirks.c
@@ -714,7 +714,33 @@ static const struct hid_device_id hid_have_special_driver[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_SRWS1) },
{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_1) },
{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_7) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_7_P) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_7_X) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_7_GEN2) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_P) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_X) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_DESTINY) },
{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_9) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_P) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_X) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5_X) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_2) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_P) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_2) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_3) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO_2) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_WOW) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_GEN2) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2_2) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_PRO) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_PRO_X) },
#endif
#if IS_ENABLED(CONFIG_HID_SUNPLUS)
{ HID_USB_DEVICE(USB_VENDOR_ID_SUNPLUS, USB_DEVICE_ID_SUNPLUS_WDESKTOP) },
--
2.53.0
^ permalink raw reply related [flat|nested] 29+ messages in thread* Re: [PATCH v3 03/18] HID: quirks: Add additional Arctis headset device IDs
2026-02-27 23:50 ` [PATCH v3 03/18] HID: quirks: Add additional " Sriman Achanta
@ 2026-03-03 10:58 ` Bastien Nocera
0 siblings, 0 replies; 29+ messages in thread
From: Bastien Nocera @ 2026-03-03 10:58 UTC (permalink / raw)
To: Sriman Achanta, Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer
On Fri, 2026-02-27 at 18:50 -0500, Sriman Achanta wrote:
> Add support for additional SteelSeries Arctis headset models to the
> HID
> quirks table. This enables proper device recognition and handling
> for:
> - Arctis 7 series (7, 7P, 7X, 7 Gen2)
> - Arctis 7 Plus series (7 Plus, 7 Plus P, 7 Plus X, 7 Plus Destiny)
> - Arctis Pro
> - Arctis Nova 3 series (3, 3P, 3X)
> - Arctis Nova 5 series (5, 5X)
> - Arctis Nova 7 series (7, 7X, 7P, 7X Rev2, 7 Diablo, 7 WoW, 7 Gen2,
> 7X
> Gen2)
> - Arctis Nova Pro series (Pro, Pro X)
This seems premature, when that code is introduced, the headsets are
not supported yet.
Those definitions should be added as support for the devices is
introduced as one would expect intermediate commits in a patchset to
still be functional.
>
> Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
> ---
> drivers/hid/hid-quirks.c | 26 ++++++++++++++++++++++++++
> 1 file changed, 26 insertions(+)
>
> diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c
> index 17349eac5c3e..65a6f6ab30b9 100644
> --- a/drivers/hid/hid-quirks.c
> +++ b/drivers/hid/hid-quirks.c
> @@ -714,7 +714,33 @@ static const struct hid_device_id
> hid_have_special_driver[] = {
> { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_SRWS1) },
> { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_1) },
> { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_7) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_7_P) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_7_X) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_7_GEN2) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_P) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_X) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_DESTINY) },
> { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_9) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_P) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_X) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5_X) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_2) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_P) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_2) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_3) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO_2) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_WOW) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_GEN2) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2_2) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_PRO) },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_PRO_X) },
> #endif
> #if IS_ENABLED(CONFIG_HID_SUNPLUS)
> { HID_USB_DEVICE(USB_VENDOR_ID_SUNPLUS,
> USB_DEVICE_ID_SUNPLUS_WDESKTOP) },
^ permalink raw reply [flat|nested] 29+ messages in thread
* [PATCH v3 04/18] HID: steelseries: Add async support and unify device definitions
2026-02-27 23:50 [PATCH v3 00/18] HID: steelseries: Add support for Arctis headset lineup Sriman Achanta
` (2 preceding siblings ...)
2026-02-27 23:50 ` [PATCH v3 03/18] HID: quirks: Add additional " Sriman Achanta
@ 2026-02-27 23:50 ` Sriman Achanta
2026-03-03 10:58 ` Bastien Nocera
2026-02-27 23:50 ` [PATCH v3 05/18] HID: steelseries: Update Kconfig help text for expanded headset support Sriman Achanta
` (14 subsequent siblings)
18 siblings, 1 reply; 29+ messages in thread
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
Christian Mayer, Sriman Achanta
Refactor the SteelSeries driver to improve scalability and support the
modern Arctis Nova headset lineup along with legacy models.
- Replace the bitmap-based quirk system with `struct
steelseries_device_info` to encapsulate device-specific traits
(product ID, name, capabilities, interfaces).
- Implement asynchronous battery monitoring. Devices that support async
updates (like the Nova series) now rely on interrupt events rather
than periodic polling, reducing overhead.
- Add support for complex multi-interface devices (e.g., Nova 7) where
battery events arrive on a separate asynchronous interface.
- Consolidate battery request and report parsing logic. New helpers
`steelseries_send_feature_report` and `steelseries_send_output_report`
simplify command dispatch.
- Add support for over 20 new devices including the entire Arctis Nova
series (3, 5, 7, Pro) and various Arctis 7/9/Pro variants.
- Clean up naming conventions (e.g., removing `_headset_` prefix from
general functions) and improve locking in the battery timer.
Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
drivers/hid/hid-steelseries.c | 894 +++++++++++++++++++++++++---------
1 file changed, 653 insertions(+), 241 deletions(-)
diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index d3711022bf86..d8ece8449255 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -4,37 +4,54 @@
*
* Copyright (c) 2013 Simon Wood
* Copyright (c) 2023 Bastien Nocera
+ * Copyright (c) 2025 Sriman Achanta
*/
-/*
- */
-
+#include <linux/delay.h>
#include <linux/device.h>
#include <linux/hid.h>
#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_1_X BIT(2)
-#define STEELSERIES_ARCTIS_9 BIT(3)
+#define SS_CAP_BATTERY BIT(0)
+
+#define SS_QUIRK_STATUS_SYNC_POLL BIT(0)
+
+struct steelseries_device;
+
+struct steelseries_device_info {
+ unsigned long capabilities;
+ unsigned long quirks;
+
+ u8 sync_interface;
+ u8 async_interface;
+
+ int (*request_status)(struct hid_device *hdev);
+ void (*parse_status)(struct steelseries_device *sd, u8 *data, int size);
+};
struct steelseries_device {
struct hid_device *hdev;
- unsigned long quirks;
+ const struct steelseries_device_info *info;
- struct delayed_work battery_work;
- spinlock_t lock;
- bool removed;
+ bool use_async_protocol;
+
+ struct delayed_work status_work;
struct power_supply_desc battery_desc;
struct power_supply *battery;
- uint8_t battery_capacity;
bool headset_connected;
+ u8 battery_capacity;
bool battery_charging;
+
+ spinlock_t lock;
+ bool removed;
};
#if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
@@ -341,53 +358,118 @@ 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;
-#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 };
+ 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 int steelseries_headset_request_battery(struct hid_device *hdev,
- const char *request, size_t len)
+/*
+ * Headset report helpers
+ */
+
+static int steelseries_send_report(struct hid_device *hdev, const u8 *data,
+ int len, enum hid_report_type type)
{
- u8 *write_buf;
+ u8 *buf;
int ret;
- /* Request battery information */
- write_buf = kmemdup(request, len, GFP_KERNEL);
- if (!write_buf)
+ buf = kmemdup(data, len, GFP_KERNEL);
+ if (!buf)
return -ENOMEM;
- 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;
- }
+ ret = hid_hw_raw_request(hdev, data[0], buf, len, type,
+ HID_REQ_SET_REPORT);
+ kfree(buf);
- kfree(write_buf);
- return ret;
+ if (ret < 0)
+ return ret;
+ if (ret < len)
+ return -EIO;
+
+ return 0;
}
-static void steelseries_headset_fetch_battery(struct hid_device *hdev)
+static inline int steelseries_send_feature_report(struct hid_device *hdev,
+ const u8 *data, int len)
{
- int ret = 0;
+ return steelseries_send_report(hdev, data, len, HID_FEATURE_REPORT);
+}
- if (hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1 ||
- hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X)
- 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 inline int steelseries_send_output_report(struct hid_device *hdev,
+ const u8 *data, int len)
+{
+ return steelseries_send_report(hdev, data, len, HID_OUTPUT_REPORT);
+}
- if (ret < 0)
- hid_dbg(hdev,
- "Battery query failed (err: %d)\n", ret);
+/*
+ * Headset status request functions
+ */
+
+static int steelseries_arctis_1_request_status(struct hid_device *hdev)
+{
+ const u8 data[] = { 0x06, 0x12 };
+
+ return steelseries_send_feature_report(hdev, data, sizeof(data));
+}
+
+static int steelseries_arctis_7_request_status(struct hid_device *hdev)
+{
+ int ret;
+ const u8 connection_data[] = { 0x06, 0x14 };
+ const u8 battery_data[] = { 0x06, 0x18 };
+
+ ret = steelseries_send_feature_report(hdev, connection_data, sizeof(connection_data));
+ if (ret)
+ return ret;
+
+ msleep(10);
+
+ return steelseries_send_feature_report(hdev, battery_data, sizeof(battery_data));
+}
+
+static int steelseries_arctis_9_request_status(struct hid_device *hdev)
+{
+ const u8 data[] = { 0x00, 0x20 };
+
+ return steelseries_send_feature_report(hdev, data, sizeof(data));
}
+static int steelseries_arctis_nova_request_status(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_status(struct hid_device *hdev)
+{
+ const u8 data[] = { 0xb0 };
+
+ return steelseries_send_output_report(hdev, data, sizeof(data));
+}
+
+static int steelseries_arctis_nova_pro_request_status(struct hid_device *hdev)
+{
+ const u8 data[] = { 0x06, 0xb0 };
+
+ return steelseries_send_output_report(hdev, data, sizeof(data));
+}
+
+/*
+ * Headset battery helpers
+ */
+
static int battery_capacity_to_level(int capacity)
{
if (capacity >= 50)
@@ -397,19 +479,247 @@ 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_capacity(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);
+}
+
+/*
+ * Headset status parse functions
+ */
+
+static void steelseries_arctis_1_parse_status(struct steelseries_device *sd,
+ u8 *data, int size)
+{
+ if (size < 4)
+ return;
+
+ sd->headset_connected = (data[2] != 0x01);
+ sd->battery_capacity = data[3];
+}
+
+static void steelseries_arctis_7_parse_status(struct steelseries_device *sd,
+ u8 *data, int size)
+{
+ if (size < 3)
+ return;
+
+ if (data[0] == 0x06) {
+ if (data[1] == 0x14)
+ sd->headset_connected = (data[2] == 0x03);
+ else if (data[1] == 0x18)
+ sd->battery_capacity = data[2];
+ }
+}
+
+static void steelseries_arctis_7_plus_parse_status(struct steelseries_device *sd,
+ u8 *data, int size)
+{
+ if (size < 4)
+ return;
+
+ if (data[0] == 0xb0) {
+ sd->headset_connected = !(data[1] == 0x01);
+ sd->battery_capacity = steelseries_map_capacity(data[2], 0x00, 0x04);
+ sd->battery_charging = (data[3] == 0x01);
+ }
+}
+
+static void steelseries_arctis_9_parse_status(struct steelseries_device *sd,
+ u8 *data, int size)
+{
+ if (size < 5)
+ return;
+
+ if (data[0] == 0xaa) {
+ sd->headset_connected = (data[1] == 0x01);
+ sd->battery_charging = (data[4] == 0x01);
+ sd->battery_capacity = steelseries_map_capacity(data[3], 0x64, 0x9A);
+ }
+}
+
+static void steelseries_arctis_nova_3p_parse_status(struct steelseries_device *sd,
+ u8 *data, int size)
+{
+ if (size < 4)
+ return;
+
+ if (data[0] == 0xb0) {
+ sd->headset_connected = !(data[1] == 0x02);
+ sd->battery_capacity = steelseries_map_capacity(data[3], 0x00, 0x64);
+ }
+}
+
+static void steelseries_arctis_nova_5_parse_status(struct steelseries_device *sd,
+ u8 *data, int size)
+{
+ if (size < 5)
+ return;
+
+ if (data[0] == 0xb0) {
+ sd->headset_connected = !(data[1] == 0x02);
+ sd->battery_capacity = data[3];
+ sd->battery_charging = (data[4] == 0x01);
+ }
+}
+
+static void steelseries_arctis_nova_7_parse_status(struct steelseries_device *sd,
+ u8 *data, int size)
+{
+ if (size < 4)
+ return;
+
+ if (data[0] == 0xb0) {
+ sd->headset_connected = (data[1] == 0x03);
+ sd->battery_capacity = steelseries_map_capacity(data[2], 0x00, 0x04);
+ sd->battery_charging = (data[3] == 0x01);
+ }
+}
+
+static void steelseries_arctis_nova_7_gen2_parse_status(struct steelseries_device *sd,
+ u8 *data, int size)
+{
+ if (size < 4)
+ return;
+
+ switch (data[0]) {
+ case 0xb0:
+ sd->headset_connected = (data[1] == 0x03);
+ sd->battery_capacity = data[2];
+ sd->battery_charging = (data[3] == 0x01);
+ break;
+ case 0xb7:
+ sd->battery_capacity = data[1];
+ break;
+ case 0xb9:
+ sd->headset_connected = (data[1] == 0x03);
+ break;
+ case 0xbb:
+ sd->battery_charging = (data[1] == 0x01);
+ break;
+ }
+}
+
+static void steelseries_arctis_nova_pro_parse_status(struct steelseries_device *sd,
+ u8 *data, int size)
+{
+ if (size < 16)
+ return;
+
+ if (data[0] == 0x06 && data[1] == 0xb0) {
+ sd->headset_connected = (data[15] == 0x08 || data[15] == 0x02);
+ sd->battery_capacity = steelseries_map_capacity(data[6], 0x00, 0x08);
+ sd->battery_charging = (data[15] == 0x02);
+ }
+}
+
+/*
+ * Device info definitions
+ */
+
+static const struct steelseries_device_info srws1_info = { };
+
+static const struct steelseries_device_info arctis_1_info = {
+ .sync_interface = 3,
+ .capabilities = SS_CAP_BATTERY,
+ .quirks = SS_QUIRK_STATUS_SYNC_POLL,
+ .request_status = steelseries_arctis_1_request_status,
+ .parse_status = steelseries_arctis_1_parse_status,
+};
+
+static const struct steelseries_device_info arctis_7_info = {
+ .sync_interface = 5,
+ .capabilities = SS_CAP_BATTERY,
+ .quirks = SS_QUIRK_STATUS_SYNC_POLL,
+ .request_status = steelseries_arctis_7_request_status,
+ .parse_status = steelseries_arctis_7_parse_status,
+};
+
+static const struct steelseries_device_info arctis_7_plus_info = {
+ .sync_interface = 3,
+ .capabilities = SS_CAP_BATTERY,
+ .quirks = SS_QUIRK_STATUS_SYNC_POLL,
+ .request_status = steelseries_arctis_nova_request_status,
+ .parse_status = steelseries_arctis_7_plus_parse_status,
+};
+
+static const struct steelseries_device_info arctis_9_info = {
+ .sync_interface = 0,
+ .capabilities = SS_CAP_BATTERY,
+ .quirks = SS_QUIRK_STATUS_SYNC_POLL,
+ .request_status = steelseries_arctis_9_request_status,
+ .parse_status = steelseries_arctis_9_parse_status,
+};
+
+static const struct steelseries_device_info arctis_nova_3p_info = {
+ .sync_interface = 4,
+ .capabilities = SS_CAP_BATTERY,
+ .quirks = SS_QUIRK_STATUS_SYNC_POLL,
+ .request_status = steelseries_arctis_nova_3p_request_status,
+ .parse_status = steelseries_arctis_nova_3p_parse_status,
+};
+
+static const struct steelseries_device_info arctis_nova_5_info = {
+ .sync_interface = 3,
+ .capabilities = SS_CAP_BATTERY,
+ .quirks = SS_QUIRK_STATUS_SYNC_POLL,
+ .request_status = steelseries_arctis_nova_request_status,
+ .parse_status = steelseries_arctis_nova_5_parse_status,
+};
+
+static const struct steelseries_device_info arctis_nova_7_info = {
+ .sync_interface = 3,
+ .capabilities = SS_CAP_BATTERY,
+ .quirks = SS_QUIRK_STATUS_SYNC_POLL,
+ .request_status = steelseries_arctis_nova_request_status,
+ .parse_status = steelseries_arctis_nova_7_parse_status,
+};
+
+static const struct steelseries_device_info arctis_nova_7_gen2_info = {
+ .sync_interface = 3,
+ .async_interface = 5,
+ .capabilities = SS_CAP_BATTERY,
+ .request_status = steelseries_arctis_nova_request_status,
+ .parse_status = steelseries_arctis_nova_7_gen2_parse_status,
+};
+
+static const struct steelseries_device_info arctis_nova_pro_info = {
+ .sync_interface = 4,
+ .capabilities = SS_CAP_BATTERY,
+ .quirks = SS_QUIRK_STATUS_SYNC_POLL,
+ .request_status = steelseries_arctis_nova_pro_request_status,
+ .parse_status = steelseries_arctis_nova_pro_parse_status,
+};
+
+/*
+ * Headset wireless status and battery infrastructure
+ */
+
+#define STEELSERIES_HEADSET_STATUS_TIMEOUT_MS 3000
+
+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;
+
+ if (!hid_is_usb(hdev))
+ return;
- steelseries_headset_fetch_battery(hdev);
+ 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,
+static int steelseries_battery_get_property(struct power_supply *psy,
enum power_supply_property psp,
union power_supply_propval *val)
{
@@ -452,22 +762,7 @@ 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)
-{
- struct usb_interface *intf;
-
- 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);
-}
-
-static enum power_supply_property steelseries_headset_battery_props[] = {
+static enum power_supply_property steelseries_battery_props[] = {
POWER_SUPPLY_PROP_MODEL_NAME,
POWER_SUPPLY_PROP_MANUFACTURER,
POWER_SUPPLY_PROP_PRESENT,
@@ -477,7 +772,26 @@ static enum power_supply_property steelseries_headset_battery_props[] = {
POWER_SUPPLY_PROP_CAPACITY_LEVEL,
};
-static int steelseries_headset_battery_register(struct steelseries_device *sd)
+/*
+ * Delayed work handlers for status polling and settings requests
+ */
+
+static void steelseries_status_timer_work_handler(struct work_struct *work)
+{
+ struct steelseries_device *sd = container_of(
+ work, struct steelseries_device, status_work.work);
+ unsigned long flags;
+
+ sd->info->request_status(sd->hdev);
+
+ spin_lock_irqsave(&sd->lock, flags);
+ if (!sd->removed && !sd->use_async_protocol)
+ schedule_delayed_work(&sd->status_work,
+ msecs_to_jiffies(STEELSERIES_HEADSET_STATUS_TIMEOUT_MS));
+ spin_unlock_irqrestore(&sd->lock, flags);
+}
+
+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, };
@@ -485,9 +799,9 @@ static int steelseries_headset_battery_register(struct steelseries_device *sd)
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,
@@ -496,14 +810,16 @@ static int steelseries_headset_battery_register(struct steelseries_device *sd)
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_charging = false;
+ sd->headset_connected = false;
+ steelseries_headset_set_wireless_status(sd->hdev, false);
sd->battery = devm_power_supply_register(&sd->hdev->dev,
&sd->battery_desc, &battery_cfg);
if (IS_ERR(sd->battery)) {
ret = PTR_ERR(sd->battery);
+ sd->battery = NULL;
hid_err(sd->hdev,
"%s:power_supply_register failed with error %d\n",
__func__, ret);
@@ -511,68 +827,185 @@ static int steelseries_headset_battery_register(struct steelseries_device *sd)
}
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);
+ return 0;
+}
+
+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);
+ u8 old_capacity;
+ bool old_connected;
+ bool old_charging;
+ bool is_async_interface = false;
+
+ if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1)
+ return 0;
+
+ if (!sd)
+ return 0;
+
+ old_capacity = sd->battery_capacity;
+ old_connected = sd->headset_connected;
+ old_charging = sd->battery_charging;
+
+ if (hid_is_usb(hdev)) {
+ struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
+
+ is_async_interface = (intf->cur_altsetting->desc.bInterfaceNumber ==
+ sd->info->async_interface);
+ }
+
+ sd->info->parse_status(sd, data, size);
+
+ if (sd->headset_connected != old_connected) {
+ hid_dbg(hdev,
+ "Connected status changed from %sconnected to %sconnected\n",
+ old_connected ? "" : "not ",
+ sd->headset_connected ? "" : "not ");
+
+ if (sd->headset_connected && !old_connected &&
+ sd->use_async_protocol && is_async_interface) {
+ schedule_delayed_work(&sd->status_work, 0);
+ }
- 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));
+ if (sd->battery) {
+ steelseries_headset_set_wireless_status(sd->hdev,
+ sd->headset_connected);
+ power_supply_changed(sd->battery);
+ }
+ }
+
+ if (sd->battery_capacity != old_capacity) {
+ hid_dbg(hdev, "Battery capacity changed from %d%% to %d%%\n",
+ old_capacity, sd->battery_capacity);
+ if (sd->battery)
+ power_supply_changed(sd->battery);
+ }
+
+ if (sd->battery_charging != old_charging) {
+ hid_dbg(hdev,
+ "Battery charging status changed from %scharging to %scharging\n",
+ old_charging ? "" : "not ",
+ sd->battery_charging ? "" : "not ");
+ if (sd->battery)
+ power_supply_changed(sd->battery);
}
return 0;
}
-static bool steelseries_is_vendor_usage_page(struct hid_device *hdev, uint8_t usage_page)
+static struct hid_device *steelseries_get_sibling_hdev(struct hid_device *hdev,
+ int interface_num)
{
- return hdev->rdesc[0] == 0x06 &&
- hdev->rdesc[1] == usage_page &&
- hdev->rdesc[2] == 0xff;
+ struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
+ struct usb_device *usb_dev = interface_to_usbdev(intf);
+ struct usb_interface *sibling_intf;
+ struct hid_device *sibling_hdev;
+
+ sibling_intf = usb_ifnum_to_if(usb_dev, interface_num);
+ if (!sibling_intf)
+ return NULL;
+
+ sibling_hdev = usb_get_intfdata(sibling_intf);
+
+ return sibling_hdev;
}
-static int steelseries_probe(struct hid_device *hdev, const struct hid_device_id *id)
+static int steelseries_probe(struct hid_device *hdev,
+ const struct hid_device_id *id)
{
+ const struct steelseries_device_info *info =
+ (const struct steelseries_device_info *)id->driver_data;
struct steelseries_device *sd;
+ struct usb_interface *intf;
+ struct hid_device *master_hdev;
+ u8 interface_num;
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))
+ (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)
- return -ENOMEM;
- hid_set_drvdata(hdev, sd);
- sd->hdev = hdev;
- sd->quirks = id->driver_data;
+ if (hid_is_usb(hdev)) {
+ intf = to_usb_interface(hdev->dev.parent);
+ interface_num = intf->cur_altsetting->desc.bInterfaceNumber;
+ } else {
+ return -ENODEV;
+ }
ret = hid_parse(hdev);
if (ret)
return ret;
- if (sd->quirks & STEELSERIES_ARCTIS_9 &&
- !steelseries_is_vendor_usage_page(hdev, 0xc0))
- return -ENODEV;
+ /* Let hid-generic handle non-vendor or unknown interfaces */
+ if (interface_num != info->sync_interface &&
+ (!info->async_interface || interface_num != info->async_interface))
+ return hid_hw_start(hdev, HID_CONNECT_DEFAULT);
- spin_lock_init(&sd->lock);
+ if (interface_num == info->sync_interface) {
+ sd = devm_kzalloc(&hdev->dev, sizeof(*sd), GFP_KERNEL);
+ if (!sd)
+ return -ENOMEM;
- ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
- if (ret)
- return ret;
+ sd->hdev = hdev;
+ sd->info = info;
+ spin_lock_init(&sd->lock);
- ret = hid_hw_open(hdev);
- if (ret)
- return ret;
+ hid_set_drvdata(hdev, sd);
- if (steelseries_headset_battery_register(sd) < 0)
- hid_err(sd->hdev,
- "Failed to register battery for headset\n");
+ ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+ if (ret)
+ return ret;
+
+ ret = hid_hw_open(hdev);
+ if (ret)
+ goto err_stop;
+
+ sd->use_async_protocol = !(info->quirks & SS_QUIRK_STATUS_SYNC_POLL);
+
+ if (info->capabilities & SS_CAP_BATTERY) {
+ ret = steelseries_battery_register(sd);
+ if (ret < 0)
+ hid_warn(hdev, "Failed to register battery: %d\n", ret);
+ }
+
+ INIT_DELAYED_WORK(&sd->status_work, steelseries_status_timer_work_handler);
+ schedule_delayed_work(&sd->status_work, msecs_to_jiffies(100));
+
+ return 0;
+ }
+
+ if (info->async_interface && interface_num == info->async_interface) {
+ master_hdev = steelseries_get_sibling_hdev(hdev, info->sync_interface);
+ if (!master_hdev || !hid_get_drvdata(master_hdev))
+ return -EPROBE_DEFER;
+
+ sd = hid_get_drvdata(master_hdev);
+ hid_set_drvdata(hdev, sd);
+
+ ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+ if (ret)
+ return ret;
+
+ ret = hid_hw_open(hdev);
+ if (ret) {
+ hid_hw_stop(hdev);
+ return ret;
+ }
+ return 0;
+ }
+
+ return -ENODEV;
+
+err_stop:
+ hid_hw_stop(hdev);
return ret;
}
@@ -580,166 +1013,144 @@ static void steelseries_remove(struct hid_device *hdev)
{
struct steelseries_device *sd;
unsigned long flags;
+ struct usb_interface *intf;
+ u8 interface_num;
if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1) {
#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))
hid_hw_stop(hdev);
#endif
return;
}
- sd = hid_get_drvdata(hdev);
-
- 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 __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;
+ if (hid_is_usb(hdev)) {
+ intf = to_usb_interface(hdev->dev.parent);
+ interface_num = intf->cur_altsetting->desc.bInterfaceNumber;
+ } else {
+ return;
}
- return rdesc;
-}
-static uint8_t steelseries_headset_map_capacity(uint8_t capacity, uint8_t min_in, uint8_t max_in)
-{
- if (capacity >= max_in)
- return 100;
- if (capacity <= min_in)
- return 0;
- return (capacity - min_in) * 100 / (max_in - min_in);
-}
-
-static int steelseries_headset_raw_event(struct hid_device *hdev,
- struct hid_report *report, u8 *read_buf,
- int size)
-{
- struct steelseries_device *sd = hid_get_drvdata(hdev);
- int capacity = sd->battery_capacity;
- bool connected = sd->headset_connected;
- bool charging = sd->battery_charging;
- unsigned long flags;
-
- /* Not a headset */
- if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1)
- return 0;
+ sd = hid_get_drvdata(hdev);
- if (hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1 ||
- hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X) {
- 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;
- }
- if (read_buf[2] == 0x01) {
- connected = false;
- capacity = 100;
- } else {
- connected = true;
- capacity = read_buf[3];
- }
+ if (!sd) {
+ hid_hw_stop(hdev);
+ return;
}
- 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;
- }
+ if (interface_num == sd->info->sync_interface) {
+ if (sd->info->async_interface) {
+ struct hid_device *sibling;
- if (read_buf[0] == 0xaa && read_buf[1] == 0x01) {
- connected = true;
- charging = read_buf[4] == 0x01;
-
- /*
- * Found no official documentation about min and max.
- * Values defined by testing.
- */
- capacity = steelseries_headset_map_capacity(read_buf[3], 0x68, 0x9d);
- } 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 = false;
- charging = false;
+ sibling = steelseries_get_sibling_hdev(hdev,
+ sd->info->async_interface);
+ if (sibling)
+ hid_set_drvdata(sibling, NULL);
}
- }
- if (connected != sd->headset_connected) {
- hid_dbg(sd->hdev,
- "Connected status changed from %sconnected to %sconnected\n",
- sd->headset_connected ? "" : "not ",
- connected ? "" : "not ");
- sd->headset_connected = connected;
- steelseries_headset_set_wireless_status(hdev, connected);
- }
+ spin_lock_irqsave(&sd->lock, flags);
+ sd->removed = true;
+ spin_unlock_irqrestore(&sd->lock, flags);
- if (capacity != sd->battery_capacity) {
- hid_dbg(sd->hdev,
- "Battery capacity changed from %d%% to %d%%\n",
- sd->battery_capacity, capacity);
- sd->battery_capacity = capacity;
- power_supply_changed(sd->battery);
- }
-
- if (charging != sd->battery_charging) {
- hid_dbg(sd->hdev,
- "Battery charging status changed from %scharging to %scharging\n",
- sd->battery_charging ? "" : "not ",
- charging ? "" : "not ");
- sd->battery_charging = charging;
- power_supply_changed(sd->battery);
+ cancel_delayed_work_sync(&sd->status_work);
}
-request_battery:
- spin_lock_irqsave(&sd->lock, flags);
- if (!sd->removed)
- schedule_delayed_work(&sd->battery_work,
- msecs_to_jiffies(STEELSERIES_HEADSET_BATTERY_TIMEOUT_MS));
- spin_unlock_irqrestore(&sd->lock, flags);
-
- return 0;
+ 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 },
-
- { /* SteelSeries Arctis 1 Wireless */
- HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_1),
- .driver_data = STEELSERIES_ARCTIS_1 },
-
- { /* SteelSeries Arctis 1 Wireless for XBox */
- HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X),
- .driver_data = STEELSERIES_ARCTIS_1_X },
-
- { /* SteelSeries Arctis 9 Wireless for XBox */
- HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_9),
- .driver_data = STEELSERIES_ARCTIS_9 },
-
- { }
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_SRWS1),
+ .driver_data = (unsigned long)&srws1_info },
+ { 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_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_1_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_7_X),
+ .driver_data = (unsigned long)&arctis_1_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_7_GEN2),
+ .driver_data = (unsigned long)&arctis_7_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_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_X),
+ .driver_data = (unsigned long)&arctis_7_plus_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_DESTINY),
+ .driver_data = (unsigned long)&arctis_7_plus_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_NOVA_3_P),
+ .driver_data = (unsigned long)&arctis_nova_3p_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_X),
+ .driver_data = (unsigned long)&arctis_nova_3p_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_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_2),
+ .driver_data = (unsigned long)&arctis_nova_7_gen2_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_P),
+ .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_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_2),
+ .driver_data = (unsigned long)&arctis_nova_7_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_3),
+ .driver_data = (unsigned long)&arctis_nova_7_gen2_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO),
+ .driver_data = (unsigned long)&arctis_nova_7_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO_2),
+ .driver_data = (unsigned long)&arctis_nova_7_gen2_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_WOW),
+ .driver_data = (unsigned long)&arctis_nova_7_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_gen2_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2_2),
+ .driver_data = (unsigned long)&arctis_nova_7_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_info },
+ {}
};
MODULE_DEVICE_TABLE(hid, steelseries_devices);
@@ -749,7 +1160,7 @@ 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);
@@ -758,3 +1169,4 @@ MODULE_LICENSE("GPL");
MODULE_AUTHOR("Bastien Nocera <hadess@hadess.net>");
MODULE_AUTHOR("Simon Wood <simon@mungewell.org>");
MODULE_AUTHOR("Christian Mayer <git@mayer-bgk.de>");
+MODULE_AUTHOR("Sriman Achanta <srimanachanta@gmail.com>");
--
2.53.0
^ permalink raw reply related [flat|nested] 29+ messages in thread* Re: [PATCH v3 04/18] HID: steelseries: Add async support and unify device definitions
2026-02-27 23:50 ` [PATCH v3 04/18] HID: steelseries: Add async support and unify device definitions Sriman Achanta
@ 2026-03-03 10:58 ` Bastien Nocera
0 siblings, 0 replies; 29+ messages in thread
From: Bastien Nocera @ 2026-03-03 10:58 UTC (permalink / raw)
To: Sriman Achanta, Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer
On Fri, 2026-02-27 at 18:50 -0500, Sriman Achanta wrote:
> Refactor the SteelSeries driver to improve scalability and support
> the
> modern Arctis Nova headset lineup along with legacy models.
This patch is the biggest in the patchset, and could really do with
being split up.
>
> - Replace the bitmap-based quirk system with `struct
> steelseries_device_info` to encapsulate device-specific traits
> (product ID, name, capabilities, interfaces).
> - Implement asynchronous battery monitoring. Devices that support
> async
> updates (like the Nova series) now rely on interrupt events rather
> than periodic polling, reducing overhead.
> - Add support for complex multi-interface devices (e.g., Nova 7)
> where
> battery events arrive on a separate asynchronous interface.
> - Consolidate battery request and report parsing logic. New helpers
> `steelseries_send_feature_report` and
> `steelseries_send_output_report`
> simplify command dispatch.
> - Add support for over 20 new devices including the entire Arctis
> Nova
> series (3, 5, 7, Pro) and various Arctis 7/9/Pro variants.
You can't mix refactoring and new code to support new devices, it just
makes the code unreviewable.
> - Clean up naming conventions (e.g., removing `_headset_` prefix from
> general functions) and improve locking in the battery timer.
No, there are many other Steelseries devices (keyboards, mice, and the
already support steering wheel!) so removing the headset descriptor
seems like something that could cause problems in the future. It might
be a good idea to split it off into hid-steelseries-headset.c instead,
but that's a lot of work...
>
> Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
> ---
> drivers/hid/hid-steelseries.c | 894 +++++++++++++++++++++++++-------
> --
> 1 file changed, 653 insertions(+), 241 deletions(-)
>
> diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-
> steelseries.c
> index d3711022bf86..d8ece8449255 100644
> --- a/drivers/hid/hid-steelseries.c
> +++ b/drivers/hid/hid-steelseries.c
> @@ -4,37 +4,54 @@
> *
> * Copyright (c) 2013 Simon Wood
> * Copyright (c) 2023 Bastien Nocera
> + * Copyright (c) 2025 Sriman Achanta
> */
>
> -/*
> - */
> -
> +#include <linux/delay.h>
> #include <linux/device.h>
> #include <linux/hid.h>
> #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_1_X BIT(2)
> -#define STEELSERIES_ARCTIS_9 BIT(3)
> +#define SS_CAP_BATTERY BIT(0)
> +
> +#define SS_QUIRK_STATUS_SYNC_POLL BIT(0)
> +
> +struct steelseries_device;
> +
> +struct steelseries_device_info {
> + unsigned long capabilities;
> + unsigned long quirks;
> +
> + u8 sync_interface;
> + u8 async_interface;
> +
> + int (*request_status)(struct hid_device *hdev);
> + void (*parse_status)(struct steelseries_device *sd, u8
> *data, int size);
> +};
>
> struct steelseries_device {
> struct hid_device *hdev;
> - unsigned long quirks;
> + const struct steelseries_device_info *info;
>
> - struct delayed_work battery_work;
> - spinlock_t lock;
> - bool removed;
> + bool use_async_protocol;
> +
> + struct delayed_work status_work;
>
> struct power_supply_desc battery_desc;
> struct power_supply *battery;
> - uint8_t battery_capacity;
> bool headset_connected;
> + u8 battery_capacity;
> bool battery_charging;
> +
> + spinlock_t lock;
> + bool removed;
> };
>
> #if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
> @@ -341,53 +358,118 @@ 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;
>
> -#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 };
> + 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 int steelseries_headset_request_battery(struct hid_device
> *hdev,
> - const char *request, size_t len)
> +/*
> + * Headset report helpers
> + */
> +
> +static int steelseries_send_report(struct hid_device *hdev, const u8
> *data,
> + int len, enum hid_report_type
> type)
> {
> - u8 *write_buf;
> + u8 *buf;
> int ret;
>
> - /* Request battery information */
> - write_buf = kmemdup(request, len, GFP_KERNEL);
> - if (!write_buf)
> + buf = kmemdup(data, len, GFP_KERNEL);
> + if (!buf)
> return -ENOMEM;
>
> - 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;
> - }
> + ret = hid_hw_raw_request(hdev, data[0], buf, len, type,
> + HID_REQ_SET_REPORT);
> + kfree(buf);
>
> - kfree(write_buf);
> - return ret;
> + if (ret < 0)
> + return ret;
> + if (ret < len)
> + return -EIO;
> +
> + return 0;
> }
>
> -static void steelseries_headset_fetch_battery(struct hid_device
> *hdev)
> +static inline int steelseries_send_feature_report(struct hid_device
> *hdev,
> + const u8 *data,
> int len)
> {
> - int ret = 0;
> + return steelseries_send_report(hdev, data, len,
> HID_FEATURE_REPORT);
> +}
>
> - if (hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1 ||
> - hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X)
> - 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 inline int steelseries_send_output_report(struct hid_device
> *hdev,
> + const u8 *data,
> int len)
> +{
> + return steelseries_send_report(hdev, data, len,
> HID_OUTPUT_REPORT);
> +}
>
> - if (ret < 0)
> - hid_dbg(hdev,
> - "Battery query failed (err: %d)\n", ret);
> +/*
> + * Headset status request functions
> + */
> +
> +static int steelseries_arctis_1_request_status(struct hid_device
> *hdev)
> +{
> + const u8 data[] = { 0x06, 0x12 };
> +
> + return steelseries_send_feature_report(hdev, data,
> sizeof(data));
> +}
> +
> +static int steelseries_arctis_7_request_status(struct hid_device
> *hdev)
> +{
> + int ret;
> + const u8 connection_data[] = { 0x06, 0x14 };
> + const u8 battery_data[] = { 0x06, 0x18 };
> +
> + ret = steelseries_send_feature_report(hdev, connection_data,
> sizeof(connection_data));
> + if (ret)
> + return ret;
> +
> + msleep(10);
> +
> + return steelseries_send_feature_report(hdev, battery_data,
> sizeof(battery_data));
> +}
> +
> +static int steelseries_arctis_9_request_status(struct hid_device
> *hdev)
> +{
> + const u8 data[] = { 0x00, 0x20 };
> +
> + return steelseries_send_feature_report(hdev, data,
> sizeof(data));
> }
>
> +static int steelseries_arctis_nova_request_status(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_status(struct
> hid_device *hdev)
> +{
> + const u8 data[] = { 0xb0 };
> +
> + return steelseries_send_output_report(hdev, data,
> sizeof(data));
> +}
> +
> +static int steelseries_arctis_nova_pro_request_status(struct
> hid_device *hdev)
> +{
> + const u8 data[] = { 0x06, 0xb0 };
> +
> + return steelseries_send_output_report(hdev, data,
> sizeof(data));
> +}
> +
> +/*
> + * Headset battery helpers
> + */
> +
> static int battery_capacity_to_level(int capacity)
> {
> if (capacity >= 50)
> @@ -397,19 +479,247 @@ 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_capacity(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);
> +}
> +
> +/*
> + * Headset status parse functions
> + */
> +
> +static void steelseries_arctis_1_parse_status(struct
> steelseries_device *sd,
> + u8 *data, int size)
> +{
> + if (size < 4)
> + return;
> +
> + sd->headset_connected = (data[2] != 0x01);
> + sd->battery_capacity = data[3];
> +}
> +
> +static void steelseries_arctis_7_parse_status(struct
> steelseries_device *sd,
> + u8 *data, int size)
> +{
> + if (size < 3)
> + return;
> +
> + if (data[0] == 0x06) {
> + if (data[1] == 0x14)
> + sd->headset_connected = (data[2] == 0x03);
> + else if (data[1] == 0x18)
> + sd->battery_capacity = data[2];
> + }
> +}
> +
> +static void steelseries_arctis_7_plus_parse_status(struct
> steelseries_device *sd,
> + u8 *data, int
> size)
> +{
> + if (size < 4)
> + return;
> +
> + if (data[0] == 0xb0) {
> + sd->headset_connected = !(data[1] == 0x01);
> + sd->battery_capacity =
> steelseries_map_capacity(data[2], 0x00, 0x04);
> + sd->battery_charging = (data[3] == 0x01);
> + }
> +}
> +
> +static void steelseries_arctis_9_parse_status(struct
> steelseries_device *sd,
> + u8 *data, int size)
> +{
> + if (size < 5)
> + return;
> +
> + if (data[0] == 0xaa) {
> + sd->headset_connected = (data[1] == 0x01);
> + sd->battery_charging = (data[4] == 0x01);
> + sd->battery_capacity =
> steelseries_map_capacity(data[3], 0x64, 0x9A);
> + }
> +}
> +
> +static void steelseries_arctis_nova_3p_parse_status(struct
> steelseries_device *sd,
> + u8 *data, int
> size)
> +{
> + if (size < 4)
> + return;
> +
> + if (data[0] == 0xb0) {
> + sd->headset_connected = !(data[1] == 0x02);
> + sd->battery_capacity =
> steelseries_map_capacity(data[3], 0x00, 0x64);
> + }
> +}
> +
> +static void steelseries_arctis_nova_5_parse_status(struct
> steelseries_device *sd,
> + u8 *data, int
> size)
> +{
> + if (size < 5)
> + return;
> +
> + if (data[0] == 0xb0) {
> + sd->headset_connected = !(data[1] == 0x02);
> + sd->battery_capacity = data[3];
> + sd->battery_charging = (data[4] == 0x01);
> + }
> +}
> +
> +static void steelseries_arctis_nova_7_parse_status(struct
> steelseries_device *sd,
> + u8 *data, int
> size)
> +{
> + if (size < 4)
> + return;
> +
> + if (data[0] == 0xb0) {
> + sd->headset_connected = (data[1] == 0x03);
> + sd->battery_capacity =
> steelseries_map_capacity(data[2], 0x00, 0x04);
> + sd->battery_charging = (data[3] == 0x01);
> + }
> +}
> +
> +static void steelseries_arctis_nova_7_gen2_parse_status(struct
> steelseries_device *sd,
> + u8 *data,
> int size)
> +{
> + if (size < 4)
> + return;
> +
> + switch (data[0]) {
> + case 0xb0:
> + sd->headset_connected = (data[1] == 0x03);
> + sd->battery_capacity = data[2];
> + sd->battery_charging = (data[3] == 0x01);
> + break;
> + case 0xb7:
> + sd->battery_capacity = data[1];
> + break;
> + case 0xb9:
> + sd->headset_connected = (data[1] == 0x03);
> + break;
> + case 0xbb:
> + sd->battery_charging = (data[1] == 0x01);
> + break;
> + }
> +}
> +
> +static void steelseries_arctis_nova_pro_parse_status(struct
> steelseries_device *sd,
> + u8 *data, int
> size)
> +{
> + if (size < 16)
> + return;
> +
> + if (data[0] == 0x06 && data[1] == 0xb0) {
> + sd->headset_connected = (data[15] == 0x08 ||
> data[15] == 0x02);
> + sd->battery_capacity =
> steelseries_map_capacity(data[6], 0x00, 0x08);
> + sd->battery_charging = (data[15] == 0x02);
> + }
> +}
> +
> +/*
> + * Device info definitions
> + */
> +
> +static const struct steelseries_device_info srws1_info = { };
> +
> +static const struct steelseries_device_info arctis_1_info = {
> + .sync_interface = 3,
> + .capabilities = SS_CAP_BATTERY,
> + .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> + .request_status = steelseries_arctis_1_request_status,
> + .parse_status = steelseries_arctis_1_parse_status,
> +};
> +
> +static const struct steelseries_device_info arctis_7_info = {
> + .sync_interface = 5,
> + .capabilities = SS_CAP_BATTERY,
> + .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> + .request_status = steelseries_arctis_7_request_status,
> + .parse_status = steelseries_arctis_7_parse_status,
> +};
> +
> +static const struct steelseries_device_info arctis_7_plus_info = {
> + .sync_interface = 3,
> + .capabilities = SS_CAP_BATTERY,
> + .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> + .request_status = steelseries_arctis_nova_request_status,
> + .parse_status = steelseries_arctis_7_plus_parse_status,
> +};
> +
> +static const struct steelseries_device_info arctis_9_info = {
> + .sync_interface = 0,
> + .capabilities = SS_CAP_BATTERY,
> + .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> + .request_status = steelseries_arctis_9_request_status,
> + .parse_status = steelseries_arctis_9_parse_status,
> +};
> +
> +static const struct steelseries_device_info arctis_nova_3p_info = {
> + .sync_interface = 4,
> + .capabilities = SS_CAP_BATTERY,
> + .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> + .request_status = steelseries_arctis_nova_3p_request_status,
> + .parse_status = steelseries_arctis_nova_3p_parse_status,
> +};
> +
> +static const struct steelseries_device_info arctis_nova_5_info = {
> + .sync_interface = 3,
> + .capabilities = SS_CAP_BATTERY,
> + .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> + .request_status = steelseries_arctis_nova_request_status,
> + .parse_status = steelseries_arctis_nova_5_parse_status,
> +};
> +
> +static const struct steelseries_device_info arctis_nova_7_info = {
> + .sync_interface = 3,
> + .capabilities = SS_CAP_BATTERY,
> + .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> + .request_status = steelseries_arctis_nova_request_status,
> + .parse_status = steelseries_arctis_nova_7_parse_status,
> +};
> +
> +static const struct steelseries_device_info arctis_nova_7_gen2_info
> = {
> + .sync_interface = 3,
> + .async_interface = 5,
> + .capabilities = SS_CAP_BATTERY,
> + .request_status = steelseries_arctis_nova_request_status,
> + .parse_status = steelseries_arctis_nova_7_gen2_parse_status,
> +};
> +
> +static const struct steelseries_device_info arctis_nova_pro_info = {
> + .sync_interface = 4,
> + .capabilities = SS_CAP_BATTERY,
> + .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> + .request_status =
> steelseries_arctis_nova_pro_request_status,
> + .parse_status = steelseries_arctis_nova_pro_parse_status,
> +};
> +
> +/*
> + * Headset wireless status and battery infrastructure
> + */
> +
> +#define STEELSERIES_HEADSET_STATUS_TIMEOUT_MS 3000
> +
> +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;
> +
> + if (!hid_is_usb(hdev))
> + return;
>
> - steelseries_headset_fetch_battery(hdev);
> + 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,
> +static int steelseries_battery_get_property(struct power_supply
> *psy,
> enum power_supply_property psp,
> union power_supply_propval *val)
> {
> @@ -452,22 +762,7 @@ 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)
> -{
> - struct usb_interface *intf;
> -
> - 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);
> -}
> -
> -static enum power_supply_property
> steelseries_headset_battery_props[] = {
> +static enum power_supply_property steelseries_battery_props[] = {
> POWER_SUPPLY_PROP_MODEL_NAME,
> POWER_SUPPLY_PROP_MANUFACTURER,
> POWER_SUPPLY_PROP_PRESENT,
> @@ -477,7 +772,26 @@ static enum power_supply_property
> steelseries_headset_battery_props[] = {
> POWER_SUPPLY_PROP_CAPACITY_LEVEL,
> };
>
> -static int steelseries_headset_battery_register(struct
> steelseries_device *sd)
> +/*
> + * Delayed work handlers for status polling and settings requests
> + */
> +
> +static void steelseries_status_timer_work_handler(struct work_struct
> *work)
> +{
> + struct steelseries_device *sd = container_of(
> + work, struct steelseries_device, status_work.work);
> + unsigned long flags;
> +
> + sd->info->request_status(sd->hdev);
> +
> + spin_lock_irqsave(&sd->lock, flags);
> + if (!sd->removed && !sd->use_async_protocol)
> + schedule_delayed_work(&sd->status_work,
> + msecs_to_jiffies(STEELSERIES_HEADSET
> _STATUS_TIMEOUT_MS));
> + spin_unlock_irqrestore(&sd->lock, flags);
> +}
> +
> +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,
> };
> @@ -485,9 +799,9 @@ static int
> steelseries_headset_battery_register(struct steelseries_device *sd)
> 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,
> @@ -496,14 +810,16 @@ static int
> steelseries_headset_battery_register(struct steelseries_device *sd)
> 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_charging = false;
> + sd->headset_connected = false;
> + steelseries_headset_set_wireless_status(sd->hdev, false);
>
> sd->battery = devm_power_supply_register(&sd->hdev->dev,
> &sd->battery_desc, &battery_cfg);
> if (IS_ERR(sd->battery)) {
> ret = PTR_ERR(sd->battery);
> + sd->battery = NULL;
> hid_err(sd->hdev,
> "%s:power_supply_register failed
> with error %d\n",
> __func__, ret);
> @@ -511,68 +827,185 @@ static int
> steelseries_headset_battery_register(struct steelseries_device *sd)
> }
> 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);
> + return 0;
> +}
> +
> +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);
> + u8 old_capacity;
> + bool old_connected;
> + bool old_charging;
> + bool is_async_interface = false;
> +
> + if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1)
> + return 0;
> +
> + if (!sd)
> + return 0;
> +
> + old_capacity = sd->battery_capacity;
> + old_connected = sd->headset_connected;
> + old_charging = sd->battery_charging;
> +
> + if (hid_is_usb(hdev)) {
> + struct usb_interface *intf = to_usb_interface(hdev-
> >dev.parent);
> +
> + is_async_interface = (intf->cur_altsetting-
> >desc.bInterfaceNumber ==
> + sd->info->async_interface);
> + }
> +
> + sd->info->parse_status(sd, data, size);
> +
> + if (sd->headset_connected != old_connected) {
> + hid_dbg(hdev,
> + "Connected status changed from %sconnected
> to %sconnected\n",
> + old_connected ? "" : "not ",
> + sd->headset_connected ? "" : "not ");
> +
> + if (sd->headset_connected && !old_connected &&
> + sd->use_async_protocol && is_async_interface) {
> + schedule_delayed_work(&sd->status_work, 0);
> + }
>
> - 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));
> + if (sd->battery) {
> + steelseries_headset_set_wireless_status(sd-
> >hdev,
> + sd-
> >headset_connected);
> + power_supply_changed(sd->battery);
> + }
> + }
> +
> + if (sd->battery_capacity != old_capacity) {
> + hid_dbg(hdev, "Battery capacity changed from %d%% to
> %d%%\n",
> + old_capacity, sd->battery_capacity);
> + if (sd->battery)
> + power_supply_changed(sd->battery);
> + }
> +
> + if (sd->battery_charging != old_charging) {
> + hid_dbg(hdev,
> + "Battery charging status changed from
> %scharging to %scharging\n",
> + old_charging ? "" : "not ",
> + sd->battery_charging ? "" : "not ");
> + if (sd->battery)
> + power_supply_changed(sd->battery);
> }
>
> return 0;
> }
>
> -static bool steelseries_is_vendor_usage_page(struct hid_device
> *hdev, uint8_t usage_page)
> +static struct hid_device *steelseries_get_sibling_hdev(struct
> hid_device *hdev,
> + int
> interface_num)
> {
> - return hdev->rdesc[0] == 0x06 &&
> - hdev->rdesc[1] == usage_page &&
> - hdev->rdesc[2] == 0xff;
> + struct usb_interface *intf = to_usb_interface(hdev-
> >dev.parent);
> + struct usb_device *usb_dev = interface_to_usbdev(intf);
> + struct usb_interface *sibling_intf;
> + struct hid_device *sibling_hdev;
> +
> + sibling_intf = usb_ifnum_to_if(usb_dev, interface_num);
> + if (!sibling_intf)
> + return NULL;
> +
> + sibling_hdev = usb_get_intfdata(sibling_intf);
> +
> + return sibling_hdev;
> }
>
> -static int steelseries_probe(struct hid_device *hdev, const struct
> hid_device_id *id)
> +static int steelseries_probe(struct hid_device *hdev,
> + const struct hid_device_id *id)
> {
> + const struct steelseries_device_info *info =
> + (const struct steelseries_device_info *)id-
> >driver_data;
> struct steelseries_device *sd;
> + struct usb_interface *intf;
> + struct hid_device *master_hdev;
> + u8 interface_num;
> 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))
> + (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)
> - return -ENOMEM;
> - hid_set_drvdata(hdev, sd);
> - sd->hdev = hdev;
> - sd->quirks = id->driver_data;
> + if (hid_is_usb(hdev)) {
> + intf = to_usb_interface(hdev->dev.parent);
> + interface_num = intf->cur_altsetting-
> >desc.bInterfaceNumber;
> + } else {
> + return -ENODEV;
> + }
>
> ret = hid_parse(hdev);
> if (ret)
> return ret;
>
> - if (sd->quirks & STEELSERIES_ARCTIS_9 &&
> - !steelseries_is_vendor_usage_page(hdev,
> 0xc0))
> - return -ENODEV;
> + /* Let hid-generic handle non-vendor or unknown interfaces
> */
> + if (interface_num != info->sync_interface &&
> + (!info->async_interface || interface_num != info-
> >async_interface))
> + return hid_hw_start(hdev, HID_CONNECT_DEFAULT);
>
> - spin_lock_init(&sd->lock);
> + if (interface_num == info->sync_interface) {
> + sd = devm_kzalloc(&hdev->dev, sizeof(*sd),
> GFP_KERNEL);
> + if (!sd)
> + return -ENOMEM;
>
> - ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
> - if (ret)
> - return ret;
> + sd->hdev = hdev;
> + sd->info = info;
> + spin_lock_init(&sd->lock);
>
> - ret = hid_hw_open(hdev);
> - if (ret)
> - return ret;
> + hid_set_drvdata(hdev, sd);
>
> - if (steelseries_headset_battery_register(sd) < 0)
> - hid_err(sd->hdev,
> - "Failed to register battery for headset\n");
> + ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
> + if (ret)
> + return ret;
> +
> + ret = hid_hw_open(hdev);
> + if (ret)
> + goto err_stop;
> +
> + sd->use_async_protocol = !(info->quirks &
> SS_QUIRK_STATUS_SYNC_POLL);
> +
> + if (info->capabilities & SS_CAP_BATTERY) {
> + ret = steelseries_battery_register(sd);
> + if (ret < 0)
> + hid_warn(hdev, "Failed to register
> battery: %d\n", ret);
> + }
> +
> + INIT_DELAYED_WORK(&sd->status_work,
> steelseries_status_timer_work_handler);
> + schedule_delayed_work(&sd->status_work,
> msecs_to_jiffies(100));
> +
> + return 0;
> + }
> +
> + if (info->async_interface && interface_num == info-
> >async_interface) {
> + master_hdev = steelseries_get_sibling_hdev(hdev,
> info->sync_interface);
>
> + if (!master_hdev || !hid_get_drvdata(master_hdev))
> + return -EPROBE_DEFER;
> +
> + sd = hid_get_drvdata(master_hdev);
> + hid_set_drvdata(hdev, sd);
> +
> + ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
> + if (ret)
> + return ret;
> +
> + ret = hid_hw_open(hdev);
> + if (ret) {
> + hid_hw_stop(hdev);
> + return ret;
> + }
> + return 0;
> + }
> +
> + return -ENODEV;
> +
> +err_stop:
> + hid_hw_stop(hdev);
> return ret;
> }
>
> @@ -580,166 +1013,144 @@ static void steelseries_remove(struct
> hid_device *hdev)
> {
> struct steelseries_device *sd;
> unsigned long flags;
> + struct usb_interface *intf;
> + u8 interface_num;
>
> if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1) {
> #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))
> hid_hw_stop(hdev);
> #endif
> return;
> }
>
> - sd = hid_get_drvdata(hdev);
> -
> - 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 __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;
> + if (hid_is_usb(hdev)) {
> + intf = to_usb_interface(hdev->dev.parent);
> + interface_num = intf->cur_altsetting-
> >desc.bInterfaceNumber;
> + } else {
> + return;
> }
> - return rdesc;
> -}
>
> -static uint8_t steelseries_headset_map_capacity(uint8_t capacity,
> uint8_t min_in, uint8_t max_in)
> -{
> - if (capacity >= max_in)
> - return 100;
> - if (capacity <= min_in)
> - return 0;
> - return (capacity - min_in) * 100 / (max_in - min_in);
> -}
> -
> -static int steelseries_headset_raw_event(struct hid_device *hdev,
> - struct hid_report *report,
> u8 *read_buf,
> - int size)
> -{
> - struct steelseries_device *sd = hid_get_drvdata(hdev);
> - int capacity = sd->battery_capacity;
> - bool connected = sd->headset_connected;
> - bool charging = sd->battery_charging;
> - unsigned long flags;
> -
> - /* Not a headset */
> - if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1)
> - return 0;
> + sd = hid_get_drvdata(hdev);
>
> - if (hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1 ||
> - hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X) {
> - 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;
> - }
> - if (read_buf[2] == 0x01) {
> - connected = false;
> - capacity = 100;
> - } else {
> - connected = true;
> - capacity = read_buf[3];
> - }
> + if (!sd) {
> + hid_hw_stop(hdev);
> + return;
> }
>
> - 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;
> - }
> + if (interface_num == sd->info->sync_interface) {
> + if (sd->info->async_interface) {
> + struct hid_device *sibling;
>
> - if (read_buf[0] == 0xaa && read_buf[1] == 0x01) {
> - connected = true;
> - charging = read_buf[4] == 0x01;
> -
> - /*
> - * Found no official documentation about min
> and max.
> - * Values defined by testing.
> - */
> - capacity =
> steelseries_headset_map_capacity(read_buf[3], 0x68, 0x9d);
> - } 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 = false;
> - charging = false;
> + sibling = steelseries_get_sibling_hdev(hdev,
> + sd-
> >info->async_interface);
> + if (sibling)
> + hid_set_drvdata(sibling, NULL);
> }
> - }
>
> - if (connected != sd->headset_connected) {
> - hid_dbg(sd->hdev,
> - "Connected status changed from %sconnected
> to %sconnected\n",
> - sd->headset_connected ? "" : "not ",
> - connected ? "" : "not ");
> - sd->headset_connected = connected;
> - steelseries_headset_set_wireless_status(hdev,
> connected);
> - }
> + spin_lock_irqsave(&sd->lock, flags);
> + sd->removed = true;
> + spin_unlock_irqrestore(&sd->lock, flags);
>
> - if (capacity != sd->battery_capacity) {
> - hid_dbg(sd->hdev,
> - "Battery capacity changed from %d%% to
> %d%%\n",
> - sd->battery_capacity, capacity);
> - sd->battery_capacity = capacity;
> - power_supply_changed(sd->battery);
> - }
> -
> - if (charging != sd->battery_charging) {
> - hid_dbg(sd->hdev,
> - "Battery charging status changed from
> %scharging to %scharging\n",
> - sd->battery_charging ? "" : "not ",
> - charging ? "" : "not ");
> - sd->battery_charging = charging;
> - power_supply_changed(sd->battery);
> + cancel_delayed_work_sync(&sd->status_work);
> }
>
> -request_battery:
> - spin_lock_irqsave(&sd->lock, flags);
> - if (!sd->removed)
> - schedule_delayed_work(&sd->battery_work,
> -
> msecs_to_jiffies(STEELSERIES_HEADSET_BATTERY_TIMEOUT_MS));
> - spin_unlock_irqrestore(&sd->lock, flags);
> -
> - return 0;
> + 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 },
> -
> - { /* SteelSeries Arctis 1 Wireless */
> - HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_1),
> - .driver_data = STEELSERIES_ARCTIS_1 },
> -
> - { /* SteelSeries Arctis 1 Wireless for XBox */
> - HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X),
> - .driver_data = STEELSERIES_ARCTIS_1_X },
> -
> - { /* SteelSeries Arctis 9 Wireless for XBox */
> - HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_9),
> - .driver_data = STEELSERIES_ARCTIS_9 },
> -
> - { }
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> + USB_DEVICE_ID_STEELSERIES_SRWS1),
> + .driver_data = (unsigned long)&srws1_info },
> + { 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_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_1_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> + USB_DEVICE_ID_STEELSERIES_ARCTIS_7_X),
> + .driver_data = (unsigned long)&arctis_1_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> + USB_DEVICE_ID_STEELSERIES_ARCTIS_7_GEN2),
> + .driver_data = (unsigned long)&arctis_7_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_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> + USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_X),
> + .driver_data = (unsigned long)&arctis_7_plus_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> +
> USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_DESTINY),
> + .driver_data = (unsigned long)&arctis_7_plus_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_NOVA_3_P),
> + .driver_data = (unsigned long)&arctis_nova_3p_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> + USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_X),
> + .driver_data = (unsigned long)&arctis_nova_3p_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_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_2),
> + .driver_data = (unsigned long)&arctis_nova_7_gen2_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> + USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_P),
> + .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_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> +
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_2),
> + .driver_data = (unsigned long)&arctis_nova_7_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> +
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_3),
> + .driver_data = (unsigned long)&arctis_nova_7_gen2_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> +
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO),
> + .driver_data = (unsigned long)&arctis_nova_7_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> +
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO_2),
> + .driver_data = (unsigned long)&arctis_nova_7_gen2_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> +
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_WOW),
> + .driver_data = (unsigned long)&arctis_nova_7_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_gen2_info },
> + { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> +
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2_2),
> + .driver_data = (unsigned long)&arctis_nova_7_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_info },
> + {}
> };
> MODULE_DEVICE_TABLE(hid, steelseries_devices);
>
> @@ -749,7 +1160,7 @@ 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);
> @@ -758,3 +1169,4 @@ MODULE_LICENSE("GPL");
> MODULE_AUTHOR("Bastien Nocera <hadess@hadess.net>");
> MODULE_AUTHOR("Simon Wood <simon@mungewell.org>");
> MODULE_AUTHOR("Christian Mayer <git@mayer-bgk.de>");
> +MODULE_AUTHOR("Sriman Achanta <srimanachanta@gmail.com>");
^ permalink raw reply [flat|nested] 29+ messages in thread
* [PATCH v3 05/18] HID: steelseries: Update Kconfig help text for expanded headset support
2026-02-27 23:50 [PATCH v3 00/18] HID: steelseries: Add support for Arctis headset lineup Sriman Achanta
` (3 preceding siblings ...)
2026-02-27 23:50 ` [PATCH v3 04/18] HID: steelseries: Add async support and unify device definitions Sriman Achanta
@ 2026-02-27 23:50 ` Sriman Achanta
2026-02-27 23:50 ` [PATCH v3 06/18] HID: steelseries: Add ALSA sound card infrastructure Sriman Achanta
` (13 subsequent siblings)
18 siblings, 0 replies; 29+ messages in thread
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
Christian Mayer, Sriman Achanta
The previous help text only mentioned the SRW-S1 steering wheel and Arctis 1 Wireless for Xbox. Update it to reflect the full Arctis headset family now supported.
Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
drivers/hid/Kconfig | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index c1d9f7c6a5f2..29f05d4b7e30 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -1142,7 +1142,8 @@ config HID_STEELSERIES
depends on USB_HID
help
Support for Steelseries SRW-S1 steering wheel, and the Steelseries
- Arctis 1 Wireless for XBox headset.
+ Arctis headset family (Arctis 1, Arctis 7, Arctis 7+, Arctis 9,
+ Arctis Nova 3, Arctis Nova 5, Arctis Nova 7, and Arctis Nova Pro).
config HID_SUNPLUS
tristate "Sunplus wireless desktop"
--
2.53.0
^ permalink raw reply related [flat|nested] 29+ messages in thread* [PATCH v3 06/18] HID: steelseries: Add ALSA sound card infrastructure
2026-02-27 23:50 [PATCH v3 00/18] HID: steelseries: Add support for Arctis headset lineup Sriman Achanta
` (4 preceding siblings ...)
2026-02-27 23:50 ` [PATCH v3 05/18] HID: steelseries: Update Kconfig help text for expanded headset support Sriman Achanta
@ 2026-02-27 23:50 ` Sriman Achanta
2026-03-03 10:58 ` Bastien Nocera
2026-02-27 23:50 ` [PATCH v3 07/18] HID: steelseries: Add ChatMix ALSA mixer controls Sriman Achanta
` (12 subsequent siblings)
18 siblings, 1 reply; 29+ messages in thread
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
Christian Mayer, Sriman Achanta
Register an ALSA sound card for each supported Arctis headset to expose
headset-specific audio controls to userspace. The card is created in
steelseries_snd_register() and freed in steelseries_snd_unregister(),
both guarded by a module compatibility check so that registration only
occurs when both SND and HID_STEELSERIES are built-in or are both
modules, avoiding a dependency mismatch.
The Kconfig entry is updated to add SND as a dependency. Subsequent
commits build on this infrastructure to register mixer controls.
Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
drivers/hid/Kconfig | 2 +-
drivers/hid/hid-steelseries.c | 52 +++++++++++++++++++++++++++++++++++
2 files changed, 53 insertions(+), 1 deletion(-)
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index 29f05d4b7e30..fcdb5406159a 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -1139,7 +1139,7 @@ config STEAM_FF
config HID_STEELSERIES
tristate "Steelseries devices support"
- depends on USB_HID
+ depends on USB_HID && SND
help
Support for Steelseries SRW-S1 steering wheel, and the Steelseries
Arctis headset family (Arctis 1, Arctis 7, Arctis 7+, Arctis 9,
diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index d8ece8449255..b7f932cde98d 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -16,6 +16,7 @@
#include <linux/power_supply.h>
#include <linux/workqueue.h>
#include <linux/spinlock.h>
+#include <sound/core.h>
#include "hid-ids.h"
@@ -50,6 +51,8 @@ struct steelseries_device {
u8 battery_capacity;
bool battery_charging;
+ struct snd_card *card;
+
spinlock_t lock;
bool removed;
};
@@ -830,6 +833,43 @@ static int steelseries_battery_register(struct steelseries_device *sd)
return 0;
}
+#if IS_BUILTIN(CONFIG_SND) || \
+ (IS_MODULE(CONFIG_SND) && IS_MODULE(CONFIG_HID_STEELSERIES))
+
+static int steelseries_snd_register(struct steelseries_device *sd)
+{
+ struct hid_device *hdev = sd->hdev;
+ int ret;
+
+ ret = snd_card_new(&hdev->dev, -1, "SteelSeries", THIS_MODULE,
+ 0, &sd->card);
+ if (ret < 0)
+ return ret;
+
+ sd->card->private_data = sd;
+ strscpy(sd->card->driver, "SteelSeries");
+ strscpy(sd->card->shortname, hdev->name);
+ snprintf(sd->card->longname, sizeof(sd->card->longname),
+ "%s at USB %s", hdev->name, dev_name(&hdev->dev));
+
+ ret = snd_card_register(sd->card);
+ if (ret < 0) {
+ snd_card_free(sd->card);
+ sd->card = NULL;
+ return ret;
+ }
+
+ return 0;
+}
+
+static void steelseries_snd_unregister(struct steelseries_device *sd)
+{
+ if (sd->card)
+ snd_card_free(sd->card);
+}
+
+#endif
+
static int steelseries_raw_event(struct hid_device *hdev,
struct hid_report *report, u8 *data, int size)
{
@@ -975,6 +1015,13 @@ static int steelseries_probe(struct hid_device *hdev,
hid_warn(hdev, "Failed to register battery: %d\n", ret);
}
+#if IS_BUILTIN(CONFIG_SND) || \
+ (IS_MODULE(CONFIG_SND) && IS_MODULE(CONFIG_HID_STEELSERIES))
+ ret = steelseries_snd_register(sd);
+ if (ret < 0)
+ hid_warn(hdev, "Failed to register sound card: %d\n", ret);
+#endif
+
INIT_DELAYED_WORK(&sd->status_work, steelseries_status_timer_work_handler);
schedule_delayed_work(&sd->status_work, msecs_to_jiffies(100));
@@ -1048,6 +1095,11 @@ static void steelseries_remove(struct hid_device *hdev)
hid_set_drvdata(sibling, NULL);
}
+#if IS_BUILTIN(CONFIG_SND) || \
+ (IS_MODULE(CONFIG_SND) && IS_MODULE(CONFIG_HID_STEELSERIES))
+ steelseries_snd_unregister(sd);
+#endif
+
spin_lock_irqsave(&sd->lock, flags);
sd->removed = true;
spin_unlock_irqrestore(&sd->lock, flags);
--
2.53.0
^ permalink raw reply related [flat|nested] 29+ messages in thread* Re: [PATCH v3 06/18] HID: steelseries: Add ALSA sound card infrastructure
2026-02-27 23:50 ` [PATCH v3 06/18] HID: steelseries: Add ALSA sound card infrastructure Sriman Achanta
@ 2026-03-03 10:58 ` Bastien Nocera
0 siblings, 0 replies; 29+ messages in thread
From: Bastien Nocera @ 2026-03-03 10:58 UTC (permalink / raw)
To: Sriman Achanta, Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer
On Fri, 2026-02-27 at 18:50 -0500, Sriman Achanta wrote:
> Register an ALSA sound card for each supported Arctis headset to
> expose
> headset-specific audio controls to userspace. The card is created in
> steelseries_snd_register() and freed in steelseries_snd_unregister(),
> both guarded by a module compatibility check so that registration
> only
> occurs when both SND and HID_STEELSERIES are built-in or are both
> modules, avoiding a dependency mismatch.
This seems pretty weird, are there examples of that pattern somewhere
else in the kernel source tree?
As we're adding new sound devices, it would probably be nice to CC: the
linux-sound@ mailing-list for their opinion.
>
> The Kconfig entry is updated to add SND as a dependency. Subsequent
> commits build on this infrastructure to register mixer controls.
>
> Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
> ---
> drivers/hid/Kconfig | 2 +-
> drivers/hid/hid-steelseries.c | 52
> +++++++++++++++++++++++++++++++++++
> 2 files changed, 53 insertions(+), 1 deletion(-)
>
> diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
> index 29f05d4b7e30..fcdb5406159a 100644
> --- a/drivers/hid/Kconfig
> +++ b/drivers/hid/Kconfig
> @@ -1139,7 +1139,7 @@ config STEAM_FF
>
> config HID_STEELSERIES
> tristate "Steelseries devices support"
> - depends on USB_HID
> + depends on USB_HID && SND
> help
> Support for Steelseries SRW-S1 steering wheel, and the
> Steelseries
> Arctis headset family (Arctis 1, Arctis 7, Arctis 7+, Arctis
> 9,
> diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-
> steelseries.c
> index d8ece8449255..b7f932cde98d 100644
> --- a/drivers/hid/hid-steelseries.c
> +++ b/drivers/hid/hid-steelseries.c
> @@ -16,6 +16,7 @@
> #include <linux/power_supply.h>
> #include <linux/workqueue.h>
> #include <linux/spinlock.h>
> +#include <sound/core.h>
>
> #include "hid-ids.h"
>
> @@ -50,6 +51,8 @@ struct steelseries_device {
> u8 battery_capacity;
> bool battery_charging;
>
> + struct snd_card *card;
> +
> spinlock_t lock;
> bool removed;
> };
> @@ -830,6 +833,43 @@ static int steelseries_battery_register(struct
> steelseries_device *sd)
> return 0;
> }
>
> +#if IS_BUILTIN(CONFIG_SND) || \
> + (IS_MODULE(CONFIG_SND) && IS_MODULE(CONFIG_HID_STEELSERIES))
> +
> +static int steelseries_snd_register(struct steelseries_device *sd)
> +{
> + struct hid_device *hdev = sd->hdev;
> + int ret;
> +
> + ret = snd_card_new(&hdev->dev, -1, "SteelSeries",
> THIS_MODULE,
> + 0, &sd->card);
> + if (ret < 0)
> + return ret;
> +
> + sd->card->private_data = sd;
> + strscpy(sd->card->driver, "SteelSeries");
> + strscpy(sd->card->shortname, hdev->name);
> + snprintf(sd->card->longname, sizeof(sd->card->longname),
> + "%s at USB %s", hdev->name, dev_name(&hdev->dev));
> +
> + ret = snd_card_register(sd->card);
> + if (ret < 0) {
> + snd_card_free(sd->card);
> + sd->card = NULL;
> + return ret;
> + }
> +
> + return 0;
> +}
> +
> +static void steelseries_snd_unregister(struct steelseries_device
> *sd)
> +{
> + if (sd->card)
> + snd_card_free(sd->card);
> +}
> +
> +#endif
> +
> static int steelseries_raw_event(struct hid_device *hdev,
> struct hid_report *report, u8
> *data, int size)
> {
> @@ -975,6 +1015,13 @@ static int steelseries_probe(struct hid_device
> *hdev,
> hid_warn(hdev, "Failed to register
> battery: %d\n", ret);
> }
>
> +#if IS_BUILTIN(CONFIG_SND) || \
> + (IS_MODULE(CONFIG_SND) && IS_MODULE(CONFIG_HID_STEELSERIES))
> + ret = steelseries_snd_register(sd);
> + if (ret < 0)
> + hid_warn(hdev, "Failed to register sound
> card: %d\n", ret);
> +#endif
> +
> INIT_DELAYED_WORK(&sd->status_work,
> steelseries_status_timer_work_handler);
> schedule_delayed_work(&sd->status_work,
> msecs_to_jiffies(100));
>
> @@ -1048,6 +1095,11 @@ static void steelseries_remove(struct
> hid_device *hdev)
> hid_set_drvdata(sibling, NULL);
> }
>
> +#if IS_BUILTIN(CONFIG_SND) || \
> + (IS_MODULE(CONFIG_SND) && IS_MODULE(CONFIG_HID_STEELSERIES))
> + steelseries_snd_unregister(sd);
> +#endif
> +
> spin_lock_irqsave(&sd->lock, flags);
> sd->removed = true;
> spin_unlock_irqrestore(&sd->lock, flags);
^ permalink raw reply [flat|nested] 29+ messages in thread
* [PATCH v3 07/18] HID: steelseries: Add ChatMix ALSA mixer controls
2026-02-27 23:50 [PATCH v3 00/18] HID: steelseries: Add support for Arctis headset lineup Sriman Achanta
` (5 preceding siblings ...)
2026-02-27 23:50 ` [PATCH v3 06/18] HID: steelseries: Add ALSA sound card infrastructure Sriman Achanta
@ 2026-02-27 23:50 ` Sriman Achanta
2026-02-27 23:50 ` [PATCH v3 08/18] HID: steelseries: Add mic mute ALSA mixer control Sriman Achanta
` (11 subsequent siblings)
18 siblings, 0 replies; 29+ messages in thread
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
Christian Mayer, Sriman Achanta
ChatMix is a hardware feature on Arctis headsets that independently
mixes game and chat audio streams. Expose the per-channel levels as
read-only volatile ALSA integer mixer controls ("ChatMix Chat" and
"ChatMix Game"), scaled to a 0-100 range. Controls are updated via
snd_ctl_notify() whenever the underlying values change in a HID event.
Each device family encodes the levels differently in HID reports;
parsing is added for the Arctis 7, 7+, 9, Nova 5X, Nova 7, Nova 7 Gen2,
and Nova Pro. The Nova 7 request path is extended to also fetch ChatMix
data (0x06, 0x24).
The Nova 5X gets its own device info entry as it uses a different status
packet layout, and the Nova 7P is split into a separate entry since it
does not expose ChatMix.
Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
drivers/hid/hid-steelseries.c | 189 ++++++++++++++++++++++++++++++----
1 file changed, 169 insertions(+), 20 deletions(-)
diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index b7f932cde98d..30ee9da1deac 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -16,11 +16,13 @@
#include <linux/power_supply.h>
#include <linux/workqueue.h>
#include <linux/spinlock.h>
+#include <sound/control.h>
#include <sound/core.h>
#include "hid-ids.h"
#define SS_CAP_BATTERY BIT(0)
+#define SS_CAP_CHATMIX BIT(1)
#define SS_QUIRK_STATUS_SYNC_POLL BIT(0)
@@ -52,6 +54,10 @@ struct steelseries_device {
bool battery_charging;
struct snd_card *card;
+ struct snd_ctl_elem_id chatmix_chat_id;
+ struct snd_ctl_elem_id chatmix_game_id;
+ u8 chatmix_chat;
+ u8 chatmix_game;
spinlock_t lock;
bool removed;
@@ -431,6 +437,7 @@ static int steelseries_arctis_7_request_status(struct hid_device *hdev)
int ret;
const u8 connection_data[] = { 0x06, 0x14 };
const u8 battery_data[] = { 0x06, 0x18 };
+ const u8 chatmix_data[] = { 0x06, 0x24 };
ret = steelseries_send_feature_report(hdev, connection_data, sizeof(connection_data));
if (ret)
@@ -438,7 +445,13 @@ static int steelseries_arctis_7_request_status(struct hid_device *hdev)
msleep(10);
- return steelseries_send_feature_report(hdev, battery_data, sizeof(battery_data));
+ ret = steelseries_send_feature_report(hdev, battery_data, sizeof(battery_data));
+ if (ret)
+ return ret;
+
+ msleep(10);
+
+ return steelseries_send_feature_report(hdev, chatmix_data, sizeof(chatmix_data));
}
static int steelseries_arctis_9_request_status(struct hid_device *hdev)
@@ -508,40 +521,52 @@ static void steelseries_arctis_1_parse_status(struct steelseries_device *sd,
static void steelseries_arctis_7_parse_status(struct steelseries_device *sd,
u8 *data, int size)
{
- if (size < 3)
+ if (size < 4)
return;
if (data[0] == 0x06) {
- if (data[1] == 0x14)
+ switch (data[1]) {
+ case 0x14:
sd->headset_connected = (data[2] == 0x03);
- else if (data[1] == 0x18)
+ break;
+ case 0x18:
sd->battery_capacity = data[2];
+ break;
+ case 0x24:
+ sd->chatmix_game = steelseries_map_capacity(data[2], 0xbf, 0xff);
+ sd->chatmix_chat = steelseries_map_capacity(data[3], 0xbf, 0xff);
+ break;
+ }
}
}
static void steelseries_arctis_7_plus_parse_status(struct steelseries_device *sd,
u8 *data, int size)
{
- if (size < 4)
+ if (size < 6)
return;
if (data[0] == 0xb0) {
sd->headset_connected = !(data[1] == 0x01);
sd->battery_capacity = steelseries_map_capacity(data[2], 0x00, 0x04);
sd->battery_charging = (data[3] == 0x01);
+ sd->chatmix_game = steelseries_map_capacity(data[4], 0x00, 0x64);
+ sd->chatmix_chat = steelseries_map_capacity(data[5], 0x00, 0x64);
}
}
static void steelseries_arctis_9_parse_status(struct steelseries_device *sd,
u8 *data, int size)
{
- if (size < 5)
+ if (size < 11)
return;
if (data[0] == 0xaa) {
sd->headset_connected = (data[1] == 0x01);
sd->battery_charging = (data[4] == 0x01);
sd->battery_capacity = steelseries_map_capacity(data[3], 0x64, 0x9A);
+ sd->chatmix_game = steelseries_map_capacity(data[9], 0x00, 0x13);
+ sd->chatmix_chat = steelseries_map_capacity(data[10], 0x00, 0x13);
}
}
@@ -570,23 +595,40 @@ static void steelseries_arctis_nova_5_parse_status(struct steelseries_device *sd
}
}
+static void steelseries_arctis_nova_5x_parse_status(struct steelseries_device *sd,
+ u8 *data, int size)
+{
+ if (size < 7)
+ return;
+
+ if (data[0] == 0xb0) {
+ sd->headset_connected = !(data[1] == 0x02);
+ sd->battery_capacity = data[3];
+ sd->battery_charging = (data[4] == 0x01);
+ sd->chatmix_chat = data[5];
+ sd->chatmix_game = data[6];
+ }
+}
+
static void steelseries_arctis_nova_7_parse_status(struct steelseries_device *sd,
u8 *data, int size)
{
- if (size < 4)
+ if (size < 6)
return;
if (data[0] == 0xb0) {
sd->headset_connected = (data[1] == 0x03);
sd->battery_capacity = steelseries_map_capacity(data[2], 0x00, 0x04);
sd->battery_charging = (data[3] == 0x01);
+ sd->chatmix_game = data[4];
+ sd->chatmix_chat = data[5];
}
}
static void steelseries_arctis_nova_7_gen2_parse_status(struct steelseries_device *sd,
u8 *data, int size)
{
- if (size < 4)
+ if (size < 6)
return;
switch (data[0]) {
@@ -594,6 +636,8 @@ static void steelseries_arctis_nova_7_gen2_parse_status(struct steelseries_devic
sd->headset_connected = (data[1] == 0x03);
sd->battery_capacity = data[2];
sd->battery_charging = (data[3] == 0x01);
+ sd->chatmix_game = data[4];
+ sd->chatmix_chat = data[5];
break;
case 0xb7:
sd->battery_capacity = data[1];
@@ -604,6 +648,10 @@ static void steelseries_arctis_nova_7_gen2_parse_status(struct steelseries_devic
case 0xbb:
sd->battery_charging = (data[1] == 0x01);
break;
+ case 0x45:
+ sd->chatmix_game = data[1];
+ sd->chatmix_chat = data[2];
+ break;
}
}
@@ -617,6 +665,9 @@ static void steelseries_arctis_nova_pro_parse_status(struct steelseries_device *
sd->headset_connected = (data[15] == 0x08 || data[15] == 0x02);
sd->battery_capacity = steelseries_map_capacity(data[6], 0x00, 0x08);
sd->battery_charging = (data[15] == 0x02);
+ } else if (data[0] == 0x07 && data[1] == 0x45) {
+ sd->chatmix_game = data[2];
+ sd->chatmix_chat = data[3];
}
}
@@ -636,7 +687,7 @@ static const struct steelseries_device_info arctis_1_info = {
static const struct steelseries_device_info arctis_7_info = {
.sync_interface = 5,
- .capabilities = SS_CAP_BATTERY,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.request_status = steelseries_arctis_7_request_status,
.parse_status = steelseries_arctis_7_parse_status,
@@ -644,7 +695,7 @@ static const struct steelseries_device_info arctis_7_info = {
static const struct steelseries_device_info arctis_7_plus_info = {
.sync_interface = 3,
- .capabilities = SS_CAP_BATTERY,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_7_plus_parse_status,
@@ -652,7 +703,7 @@ static const struct steelseries_device_info arctis_7_plus_info = {
static const struct steelseries_device_info arctis_9_info = {
.sync_interface = 0,
- .capabilities = SS_CAP_BATTERY,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.request_status = steelseries_arctis_9_request_status,
.parse_status = steelseries_arctis_9_parse_status,
@@ -674,7 +725,23 @@ static const struct steelseries_device_info arctis_nova_5_info = {
.parse_status = steelseries_arctis_nova_5_parse_status,
};
+static const struct steelseries_device_info arctis_nova_5x_info = {
+ .sync_interface = 3,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX,
+ .quirks = SS_QUIRK_STATUS_SYNC_POLL,
+ .request_status = steelseries_arctis_nova_request_status,
+ .parse_status = steelseries_arctis_nova_5x_parse_status,
+};
+
static const struct steelseries_device_info arctis_nova_7_info = {
+ .sync_interface = 3,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX,
+ .quirks = SS_QUIRK_STATUS_SYNC_POLL,
+ .request_status = steelseries_arctis_nova_request_status,
+ .parse_status = steelseries_arctis_nova_7_parse_status,
+};
+
+static const struct steelseries_device_info arctis_nova_7p_info = {
.sync_interface = 3,
.capabilities = SS_CAP_BATTERY,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
@@ -685,14 +752,14 @@ static const struct steelseries_device_info arctis_nova_7_info = {
static const struct steelseries_device_info arctis_nova_7_gen2_info = {
.sync_interface = 3,
.async_interface = 5,
- .capabilities = SS_CAP_BATTERY,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_nova_7_gen2_parse_status,
};
static const struct steelseries_device_info arctis_nova_pro_info = {
.sync_interface = 4,
- .capabilities = SS_CAP_BATTERY,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.request_status = steelseries_arctis_nova_pro_request_status,
.parse_status = steelseries_arctis_nova_pro_parse_status,
@@ -836,6 +903,57 @@ static int steelseries_battery_register(struct steelseries_device *sd)
#if IS_BUILTIN(CONFIG_SND) || \
(IS_MODULE(CONFIG_SND) && IS_MODULE(CONFIG_HID_STEELSERIES))
+static int steelseries_chatmix_info(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_info *uinfo)
+{
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 1;
+ uinfo->value.integer.min = 0;
+ uinfo->value.integer.max = 100;
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int steelseries_chatmix_chat_get(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
+ unsigned long flags;
+
+ spin_lock_irqsave(&sd->lock, flags);
+ ucontrol->value.integer.value[0] = sd->chatmix_chat;
+ spin_unlock_irqrestore(&sd->lock, flags);
+ return 0;
+}
+
+static int steelseries_chatmix_game_get(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
+ unsigned long flags;
+
+ spin_lock_irqsave(&sd->lock, flags);
+ ucontrol->value.integer.value[0] = sd->chatmix_game;
+ spin_unlock_irqrestore(&sd->lock, flags);
+ return 0;
+}
+
+static const struct snd_kcontrol_new steelseries_chatmix_chat_control = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "ChatMix Chat",
+ .access = SNDRV_CTL_ELEM_ACCESS_READ | SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+ .info = steelseries_chatmix_info,
+ .get = steelseries_chatmix_chat_get,
+};
+
+static const struct snd_kcontrol_new steelseries_chatmix_game_control = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "ChatMix Game",
+ .access = SNDRV_CTL_ELEM_ACCESS_READ | SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+ .info = steelseries_chatmix_info,
+ .get = steelseries_chatmix_game_get,
+};
+
static int steelseries_snd_register(struct steelseries_device *sd)
{
struct hid_device *hdev = sd->hdev;
@@ -852,14 +970,32 @@ static int steelseries_snd_register(struct steelseries_device *sd)
snprintf(sd->card->longname, sizeof(sd->card->longname),
"%s at USB %s", hdev->name, dev_name(&hdev->dev));
- ret = snd_card_register(sd->card);
- if (ret < 0) {
- snd_card_free(sd->card);
- sd->card = NULL;
- return ret;
+ if (sd->info->capabilities & SS_CAP_CHATMIX) {
+ struct snd_kcontrol *kctl;
+
+ kctl = snd_ctl_new1(&steelseries_chatmix_chat_control, sd);
+ ret = snd_ctl_add(sd->card, kctl);
+ if (ret < 0)
+ goto err_free_card;
+ sd->chatmix_chat_id = kctl->id;
+
+ kctl = snd_ctl_new1(&steelseries_chatmix_game_control, sd);
+ ret = snd_ctl_add(sd->card, kctl);
+ if (ret < 0)
+ goto err_free_card;
+ sd->chatmix_game_id = kctl->id;
}
+ ret = snd_card_register(sd->card);
+ if (ret < 0)
+ goto err_free_card;
+
return 0;
+
+err_free_card:
+ snd_card_free(sd->card);
+ sd->card = NULL;
+ return ret;
}
static void steelseries_snd_unregister(struct steelseries_device *sd)
@@ -877,6 +1013,8 @@ static int steelseries_raw_event(struct hid_device *hdev,
u8 old_capacity;
bool old_connected;
bool old_charging;
+ u8 old_chatmix_chat;
+ u8 old_chatmix_game;
bool is_async_interface = false;
if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1)
@@ -888,6 +1026,8 @@ static int steelseries_raw_event(struct hid_device *hdev,
old_capacity = sd->battery_capacity;
old_connected = sd->headset_connected;
old_charging = sd->battery_charging;
+ old_chatmix_chat = sd->chatmix_chat;
+ old_chatmix_game = sd->chatmix_game;
if (hid_is_usb(hdev)) {
struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
@@ -932,6 +1072,15 @@ static int steelseries_raw_event(struct hid_device *hdev,
power_supply_changed(sd->battery);
}
+ if (sd->card) {
+ if (sd->chatmix_chat != old_chatmix_chat)
+ snd_ctl_notify(sd->card, SNDRV_CTL_EVENT_MASK_VALUE,
+ &sd->chatmix_chat_id);
+ if (sd->chatmix_game != old_chatmix_game)
+ snd_ctl_notify(sd->card, SNDRV_CTL_EVENT_MASK_VALUE,
+ &sd->chatmix_game_id);
+ }
+
return 0;
}
@@ -1159,7 +1308,7 @@ static const struct hid_device_id steelseries_devices[] = {
.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_info },
+ .driver_data = (unsigned long)&arctis_nova_5x_info },
{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7),
.driver_data = (unsigned long)&arctis_nova_7_info },
@@ -1168,7 +1317,7 @@ static const struct hid_device_id steelseries_devices[] = {
.driver_data = (unsigned long)&arctis_nova_7_gen2_info },
{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_P),
- .driver_data = (unsigned long)&arctis_nova_7_info },
+ .driver_data = (unsigned long)&arctis_nova_7p_info },
{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X),
.driver_data = (unsigned long)&arctis_nova_7_info },
--
2.53.0
^ permalink raw reply related [flat|nested] 29+ messages in thread* [PATCH v3 08/18] HID: steelseries: Add mic mute ALSA mixer control
2026-02-27 23:50 [PATCH v3 00/18] HID: steelseries: Add support for Arctis headset lineup Sriman Achanta
` (6 preceding siblings ...)
2026-02-27 23:50 ` [PATCH v3 07/18] HID: steelseries: Add ChatMix ALSA mixer controls Sriman Achanta
@ 2026-02-27 23:50 ` Sriman Achanta
2026-02-27 23:50 ` [PATCH v3 09/18] HID: steelseries: Add Bluetooth state sysfs attributes Sriman Achanta
` (10 subsequent siblings)
18 siblings, 0 replies; 29+ messages in thread
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
Christian Mayer, Sriman Achanta
Expose the hardware microphone mute button state as a read-only volatile
ALSA boolean mixer control ("Mic Muted"). State is decoded from HID
events reported by the Arctis Nova 7 Gen2 (0xb0 status packet and 0x52
async event) and the Nova Pro (initial status packet). Changes are
propagated to userspace via snd_ctl_notify().
Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
drivers/hid/hid-steelseries.c | 60 +++++++++++++++++++++++++++++++++--
1 file changed, 57 insertions(+), 3 deletions(-)
diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index 30ee9da1deac..3de8e1555263 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -23,6 +23,7 @@
#define SS_CAP_BATTERY BIT(0)
#define SS_CAP_CHATMIX BIT(1)
+#define SS_CAP_MIC_MUTE BIT(2)
#define SS_QUIRK_STATUS_SYNC_POLL BIT(0)
@@ -56,8 +57,10 @@ struct steelseries_device {
struct snd_card *card;
struct snd_ctl_elem_id chatmix_chat_id;
struct snd_ctl_elem_id chatmix_game_id;
+ struct snd_ctl_elem_id mic_muted_id;
u8 chatmix_chat;
u8 chatmix_game;
+ bool mic_muted;
spinlock_t lock;
bool removed;
@@ -628,7 +631,7 @@ static void steelseries_arctis_nova_7_parse_status(struct steelseries_device *sd
static void steelseries_arctis_nova_7_gen2_parse_status(struct steelseries_device *sd,
u8 *data, int size)
{
- if (size < 6)
+ if (size < 10)
return;
switch (data[0]) {
@@ -638,6 +641,7 @@ static void steelseries_arctis_nova_7_gen2_parse_status(struct steelseries_devic
sd->battery_charging = (data[3] == 0x01);
sd->chatmix_game = data[4];
sd->chatmix_chat = data[5];
+ sd->mic_muted = (data[9] == 0x01);
break;
case 0xb7:
sd->battery_capacity = data[1];
@@ -652,6 +656,9 @@ static void steelseries_arctis_nova_7_gen2_parse_status(struct steelseries_devic
sd->chatmix_game = data[1];
sd->chatmix_chat = data[2];
break;
+ case 0x52:
+ sd->mic_muted = (data[2] == 0x01);
+ break;
}
}
@@ -665,6 +672,7 @@ static void steelseries_arctis_nova_pro_parse_status(struct steelseries_device *
sd->headset_connected = (data[15] == 0x08 || data[15] == 0x02);
sd->battery_capacity = steelseries_map_capacity(data[6], 0x00, 0x08);
sd->battery_charging = (data[15] == 0x02);
+ sd->mic_muted = (data[9] == 0x01);
} else if (data[0] == 0x07 && data[1] == 0x45) {
sd->chatmix_game = data[2];
sd->chatmix_chat = data[3];
@@ -752,14 +760,14 @@ static const struct steelseries_device_info arctis_nova_7p_info = {
static const struct steelseries_device_info arctis_nova_7_gen2_info = {
.sync_interface = 3,
.async_interface = 5,
- .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_MIC_MUTE,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_nova_7_gen2_parse_status,
};
static const struct steelseries_device_info arctis_nova_pro_info = {
.sync_interface = 4,
- .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_MIC_MUTE,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.request_status = steelseries_arctis_nova_pro_request_status,
.parse_status = steelseries_arctis_nova_pro_parse_status,
@@ -938,6 +946,29 @@ static int steelseries_chatmix_game_get(struct snd_kcontrol *kcontrol,
return 0;
}
+static int steelseries_mic_muted_info(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_info *uinfo)
+{
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
+ uinfo->count = 1;
+ uinfo->value.integer.min = 0;
+ uinfo->value.integer.max = 1;
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int steelseries_mic_muted_get(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
+ unsigned long flags;
+
+ spin_lock_irqsave(&sd->lock, flags);
+ ucontrol->value.integer.value[0] = sd->mic_muted;
+ spin_unlock_irqrestore(&sd->lock, flags);
+ return 0;
+}
+
static const struct snd_kcontrol_new steelseries_chatmix_chat_control = {
.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
.name = "ChatMix Chat",
@@ -954,6 +985,14 @@ static const struct snd_kcontrol_new steelseries_chatmix_game_control = {
.get = steelseries_chatmix_game_get,
};
+static const struct snd_kcontrol_new steelseries_mic_muted_control = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "Mic Muted",
+ .access = SNDRV_CTL_ELEM_ACCESS_READ | SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+ .info = steelseries_mic_muted_info,
+ .get = steelseries_mic_muted_get,
+};
+
static int steelseries_snd_register(struct steelseries_device *sd)
{
struct hid_device *hdev = sd->hdev;
@@ -986,6 +1025,16 @@ static int steelseries_snd_register(struct steelseries_device *sd)
sd->chatmix_game_id = kctl->id;
}
+ if (sd->info->capabilities & SS_CAP_MIC_MUTE) {
+ struct snd_kcontrol *kctl;
+
+ kctl = snd_ctl_new1(&steelseries_mic_muted_control, sd);
+ ret = snd_ctl_add(sd->card, kctl);
+ if (ret < 0)
+ goto err_free_card;
+ sd->mic_muted_id = kctl->id;
+ }
+
ret = snd_card_register(sd->card);
if (ret < 0)
goto err_free_card;
@@ -1015,6 +1064,7 @@ static int steelseries_raw_event(struct hid_device *hdev,
bool old_charging;
u8 old_chatmix_chat;
u8 old_chatmix_game;
+ bool old_mic_muted;
bool is_async_interface = false;
if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1)
@@ -1028,6 +1078,7 @@ static int steelseries_raw_event(struct hid_device *hdev,
old_charging = sd->battery_charging;
old_chatmix_chat = sd->chatmix_chat;
old_chatmix_game = sd->chatmix_game;
+ old_mic_muted = sd->mic_muted;
if (hid_is_usb(hdev)) {
struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
@@ -1079,6 +1130,9 @@ static int steelseries_raw_event(struct hid_device *hdev,
if (sd->chatmix_game != old_chatmix_game)
snd_ctl_notify(sd->card, SNDRV_CTL_EVENT_MASK_VALUE,
&sd->chatmix_game_id);
+ if (sd->mic_muted != old_mic_muted)
+ snd_ctl_notify(sd->card, SNDRV_CTL_EVENT_MASK_VALUE,
+ &sd->mic_muted_id);
}
return 0;
--
2.53.0
^ permalink raw reply related [flat|nested] 29+ messages in thread* [PATCH v3 09/18] HID: steelseries: Add Bluetooth state sysfs attributes
2026-02-27 23:50 [PATCH v3 00/18] HID: steelseries: Add support for Arctis headset lineup Sriman Achanta
` (7 preceding siblings ...)
2026-02-27 23:50 ` [PATCH v3 08/18] HID: steelseries: Add mic mute ALSA mixer control Sriman Achanta
@ 2026-02-27 23:50 ` Sriman Achanta
2026-03-03 10:58 ` Bastien Nocera
2026-02-27 23:50 ` [PATCH v3 10/18] HID: steelseries: Add settings poll infrastructure Sriman Achanta
` (9 subsequent siblings)
18 siblings, 1 reply; 29+ messages in thread
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
Christian Mayer, Sriman Achanta
Add read-only sysfs attributes bt_enabled and bt_device_connected that
reflect the current Bluetooth radio state for headsets that support it.
Attributes are registered via an attribute group with an is_visible
callback so they only appear on capable devices.
Bluetooth state is decoded from the following HID reports:
- Arctis Nova 7 Gen2: 0xb0 initial status packet and 0xb5 async events
- Arctis Nova Pro: initial 0x06/0x14 status packet
Returns -ENODEV if the headset is not currently connected.
Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
drivers/hid/hid-steelseries.c | 111 +++++++++++++++++++++++++++++++++-
1 file changed, 109 insertions(+), 2 deletions(-)
diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index 3de8e1555263..8c6116d02f19 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -24,6 +24,8 @@
#define SS_CAP_BATTERY BIT(0)
#define SS_CAP_CHATMIX BIT(1)
#define SS_CAP_MIC_MUTE BIT(2)
+#define SS_CAP_BT_ENABLED BIT(3)
+#define SS_CAP_BT_DEVICE_CONNECTED BIT(4)
#define SS_QUIRK_STATUS_SYNC_POLL BIT(0)
@@ -62,6 +64,9 @@ struct steelseries_device {
u8 chatmix_game;
bool mic_muted;
+ bool bt_enabled;
+ bool bt_device_connected;
+
spinlock_t lock;
bool removed;
};
@@ -641,6 +646,20 @@ static void steelseries_arctis_nova_7_gen2_parse_status(struct steelseries_devic
sd->battery_charging = (data[3] == 0x01);
sd->chatmix_game = data[4];
sd->chatmix_chat = data[5];
+ switch (data[6]) {
+ case 0x00:
+ sd->bt_enabled = false;
+ sd->bt_device_connected = false;
+ break;
+ case 0x03:
+ sd->bt_enabled = true;
+ sd->bt_device_connected = false;
+ break;
+ case 0x02:
+ sd->bt_enabled = true;
+ sd->bt_device_connected = true;
+ break;
+ }
sd->mic_muted = (data[9] == 0x01);
break;
case 0xb7:
@@ -659,6 +678,15 @@ static void steelseries_arctis_nova_7_gen2_parse_status(struct steelseries_devic
case 0x52:
sd->mic_muted = (data[2] == 0x01);
break;
+ case 0xb5:
+ if (data[1] == 0x01) {
+ sd->bt_enabled = false;
+ sd->bt_device_connected = false;
+ } else if (data[1] == 0x04) {
+ sd->bt_enabled = true;
+ sd->bt_device_connected = (data[2] == 0x01);
+ }
+ break;
}
}
@@ -673,6 +701,8 @@ static void steelseries_arctis_nova_pro_parse_status(struct steelseries_device *
sd->battery_capacity = steelseries_map_capacity(data[6], 0x00, 0x08);
sd->battery_charging = (data[15] == 0x02);
sd->mic_muted = (data[9] == 0x01);
+ sd->bt_enabled = (data[4] == 0x00);
+ sd->bt_device_connected = (data[5] == 0x01);
} else if (data[0] == 0x07 && data[1] == 0x45) {
sd->chatmix_game = data[2];
sd->chatmix_chat = data[3];
@@ -760,14 +790,16 @@ static const struct steelseries_device_info arctis_nova_7p_info = {
static const struct steelseries_device_info arctis_nova_7_gen2_info = {
.sync_interface = 3,
.async_interface = 5,
- .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_MIC_MUTE,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_MIC_MUTE |
+ SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_nova_7_gen2_parse_status,
};
static const struct steelseries_device_info arctis_nova_pro_info = {
.sync_interface = 4,
- .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_MIC_MUTE,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_MIC_MUTE |
+ SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.request_status = steelseries_arctis_nova_pro_request_status,
.parse_status = steelseries_arctis_nova_pro_parse_status,
@@ -908,6 +940,70 @@ static int steelseries_battery_register(struct steelseries_device *sd)
return 0;
}
+/*
+ * Sysfs attributes for device state
+ */
+
+static ssize_t bt_enabled_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);
+
+ if (!sd->headset_connected)
+ return -ENODEV;
+
+ return sysfs_emit(buf, "%d\n", sd->bt_enabled);
+}
+
+static ssize_t bt_device_connected_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);
+
+ if (!sd->headset_connected)
+ return -ENODEV;
+
+ return sysfs_emit(buf, "%d\n", sd->bt_device_connected);
+}
+
+static DEVICE_ATTR_RO(bt_enabled);
+static DEVICE_ATTR_RO(bt_device_connected);
+
+static struct attribute *steelseries_headset_attrs[] = {
+ &dev_attr_bt_enabled.attr,
+ &dev_attr_bt_device_connected.attr,
+ NULL,
+};
+
+static umode_t steelseries_headset_attr_is_visible(struct kobject *kobj,
+ struct attribute *attr,
+ int index)
+{
+ 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;
+
+ if (!sd)
+ return 0;
+
+ caps = sd->info->capabilities;
+
+ if (attr == &dev_attr_bt_enabled.attr)
+ return (caps & SS_CAP_BT_ENABLED) ? attr->mode : 0;
+ if (attr == &dev_attr_bt_device_connected.attr)
+ return (caps & SS_CAP_BT_DEVICE_CONNECTED) ? attr->mode : 0;
+
+ return 0;
+}
+
+static const struct attribute_group steelseries_headset_attr_group = {
+ .attrs = steelseries_headset_attrs,
+ .is_visible = steelseries_headset_attr_is_visible,
+};
+
#if IS_BUILTIN(CONFIG_SND) || \
(IS_MODULE(CONFIG_SND) && IS_MODULE(CONFIG_HID_STEELSERIES))
@@ -1218,6 +1314,13 @@ static int steelseries_probe(struct hid_device *hdev,
hid_warn(hdev, "Failed to register battery: %d\n", ret);
}
+ if (info->capabilities & (SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED)) {
+ ret = sysfs_create_group(&hdev->dev.kobj,
+ &steelseries_headset_attr_group);
+ if (ret)
+ hid_warn(hdev, "Failed to create sysfs group: %d\n", ret);
+ }
+
#if IS_BUILTIN(CONFIG_SND) || \
(IS_MODULE(CONFIG_SND) && IS_MODULE(CONFIG_HID_STEELSERIES))
ret = steelseries_snd_register(sd);
@@ -1289,6 +1392,10 @@ static void steelseries_remove(struct hid_device *hdev)
}
if (interface_num == sd->info->sync_interface) {
+ if (sd->info->capabilities & (SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED))
+ sysfs_remove_group(&hdev->dev.kobj,
+ &steelseries_headset_attr_group);
+
if (sd->info->async_interface) {
struct hid_device *sibling;
--
2.53.0
^ permalink raw reply related [flat|nested] 29+ messages in thread* Re: [PATCH v3 09/18] HID: steelseries: Add Bluetooth state sysfs attributes
2026-02-27 23:50 ` [PATCH v3 09/18] HID: steelseries: Add Bluetooth state sysfs attributes Sriman Achanta
@ 2026-03-03 10:58 ` Bastien Nocera
0 siblings, 0 replies; 29+ messages in thread
From: Bastien Nocera @ 2026-03-03 10:58 UTC (permalink / raw)
To: Sriman Achanta, Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer
On Fri, 2026-02-27 at 18:50 -0500, Sriman Achanta wrote:
> Add read-only sysfs attributes bt_enabled and bt_device_connected
> that
> reflect the current Bluetooth radio state for headsets that support
> it.
> Attributes are registered via an attribute group with an is_visible
> callback so they only appear on capable devices.
This needs a longer explanation as to what that "Bluetooth state" is,
so folks that aren't familiar with the Steelseries products'
capabilities.
> Bluetooth state is decoded from the following HID reports:
> - Arctis Nova 7 Gen2: 0xb0 initial status packet and 0xb5 async
> events
> - Arctis Nova Pro: initial 0x06/0x14 status packet
>
> Returns -ENODEV if the headset is not currently connected.
Do you have any ideas on how it might be used? Maybe a link to a
product page that shows the Windows or macOS software using the
feature, or another manufacturer using something similar?
Adding sysfs files like this is pretty frowned upon, so you probably
want to make sure that this is the last or close to last patch in your
patchset so maintainers can easily defer it without blocking the rest.
>
> Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
> ---
> drivers/hid/hid-steelseries.c | 111
> +++++++++++++++++++++++++++++++++-
> 1 file changed, 109 insertions(+), 2 deletions(-)
>
> diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-
> steelseries.c
> index 3de8e1555263..8c6116d02f19 100644
> --- a/drivers/hid/hid-steelseries.c
> +++ b/drivers/hid/hid-steelseries.c
> @@ -24,6 +24,8 @@
> #define SS_CAP_BATTERY BIT(0)
> #define SS_CAP_CHATMIX BIT(1)
> #define SS_CAP_MIC_MUTE BIT(2)
> +#define SS_CAP_BT_ENABLED BIT(3)
> +#define SS_CAP_BT_DEVICE_CONNECTED BIT(4)
>
> #define SS_QUIRK_STATUS_SYNC_POLL BIT(0)
>
> @@ -62,6 +64,9 @@ struct steelseries_device {
> u8 chatmix_game;
> bool mic_muted;
>
> + bool bt_enabled;
> + bool bt_device_connected;
> +
> spinlock_t lock;
> bool removed;
> };
> @@ -641,6 +646,20 @@ static void
> steelseries_arctis_nova_7_gen2_parse_status(struct steelseries_devic
> sd->battery_charging = (data[3] == 0x01);
> sd->chatmix_game = data[4];
> sd->chatmix_chat = data[5];
> + switch (data[6]) {
> + case 0x00:
> + sd->bt_enabled = false;
> + sd->bt_device_connected = false;
> + break;
> + case 0x03:
> + sd->bt_enabled = true;
> + sd->bt_device_connected = false;
> + break;
> + case 0x02:
> + sd->bt_enabled = true;
> + sd->bt_device_connected = true;
> + break;
> + }
> sd->mic_muted = (data[9] == 0x01);
> break;
> case 0xb7:
> @@ -659,6 +678,15 @@ static void
> steelseries_arctis_nova_7_gen2_parse_status(struct steelseries_devic
> case 0x52:
> sd->mic_muted = (data[2] == 0x01);
> break;
> + case 0xb5:
> + if (data[1] == 0x01) {
> + sd->bt_enabled = false;
> + sd->bt_device_connected = false;
> + } else if (data[1] == 0x04) {
> + sd->bt_enabled = true;
> + sd->bt_device_connected = (data[2] == 0x01);
> + }
> + break;
> }
> }
>
> @@ -673,6 +701,8 @@ static void
> steelseries_arctis_nova_pro_parse_status(struct steelseries_device *
> sd->battery_capacity =
> steelseries_map_capacity(data[6], 0x00, 0x08);
> sd->battery_charging = (data[15] == 0x02);
> sd->mic_muted = (data[9] == 0x01);
> + sd->bt_enabled = (data[4] == 0x00);
> + sd->bt_device_connected = (data[5] == 0x01);
> } else if (data[0] == 0x07 && data[1] == 0x45) {
> sd->chatmix_game = data[2];
> sd->chatmix_chat = data[3];
> @@ -760,14 +790,16 @@ static const struct steelseries_device_info
> arctis_nova_7p_info = {
> static const struct steelseries_device_info arctis_nova_7_gen2_info
> = {
> .sync_interface = 3,
> .async_interface = 5,
> - .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX |
> SS_CAP_MIC_MUTE,
> + .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX |
> SS_CAP_MIC_MUTE |
> + SS_CAP_BT_ENABLED |
> SS_CAP_BT_DEVICE_CONNECTED,
> .request_status = steelseries_arctis_nova_request_status,
> .parse_status = steelseries_arctis_nova_7_gen2_parse_status,
> };
>
> static const struct steelseries_device_info arctis_nova_pro_info = {
> .sync_interface = 4,
> - .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX |
> SS_CAP_MIC_MUTE,
> + .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX |
> SS_CAP_MIC_MUTE |
> + SS_CAP_BT_ENABLED |
> SS_CAP_BT_DEVICE_CONNECTED,
> .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> .request_status =
> steelseries_arctis_nova_pro_request_status,
> .parse_status = steelseries_arctis_nova_pro_parse_status,
> @@ -908,6 +940,70 @@ static int steelseries_battery_register(struct
> steelseries_device *sd)
> return 0;
> }
>
> +/*
> + * Sysfs attributes for device state
> + */
> +
> +static ssize_t bt_enabled_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);
> +
> + if (!sd->headset_connected)
> + return -ENODEV;
> +
> + return sysfs_emit(buf, "%d\n", sd->bt_enabled);
> +}
> +
> +static ssize_t bt_device_connected_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);
> +
> + if (!sd->headset_connected)
> + return -ENODEV;
> +
> + return sysfs_emit(buf, "%d\n", sd->bt_device_connected);
> +}
> +
> +static DEVICE_ATTR_RO(bt_enabled);
> +static DEVICE_ATTR_RO(bt_device_connected);
> +
> +static struct attribute *steelseries_headset_attrs[] = {
> + &dev_attr_bt_enabled.attr,
> + &dev_attr_bt_device_connected.attr,
> + NULL,
> +};
> +
> +static umode_t steelseries_headset_attr_is_visible(struct kobject
> *kobj,
> + struct attribute
> *attr,
> + int index)
> +{
> + 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;
> +
> + if (!sd)
> + return 0;
> +
> + caps = sd->info->capabilities;
> +
> + if (attr == &dev_attr_bt_enabled.attr)
> + return (caps & SS_CAP_BT_ENABLED) ? attr->mode : 0;
> + if (attr == &dev_attr_bt_device_connected.attr)
> + return (caps & SS_CAP_BT_DEVICE_CONNECTED) ? attr-
> >mode : 0;
> +
> + return 0;
> +}
> +
> +static const struct attribute_group steelseries_headset_attr_group =
> {
> + .attrs = steelseries_headset_attrs,
> + .is_visible = steelseries_headset_attr_is_visible,
> +};
> +
> #if IS_BUILTIN(CONFIG_SND) || \
> (IS_MODULE(CONFIG_SND) && IS_MODULE(CONFIG_HID_STEELSERIES))
>
> @@ -1218,6 +1314,13 @@ static int steelseries_probe(struct hid_device
> *hdev,
> hid_warn(hdev, "Failed to register
> battery: %d\n", ret);
> }
>
> + if (info->capabilities & (SS_CAP_BT_ENABLED |
> SS_CAP_BT_DEVICE_CONNECTED)) {
> + ret = sysfs_create_group(&hdev->dev.kobj,
> +
> &steelseries_headset_attr_group);
> + if (ret)
> + hid_warn(hdev, "Failed to create
> sysfs group: %d\n", ret);
> + }
> +
> #if IS_BUILTIN(CONFIG_SND) || \
> (IS_MODULE(CONFIG_SND) && IS_MODULE(CONFIG_HID_STEELSERIES))
> ret = steelseries_snd_register(sd);
> @@ -1289,6 +1392,10 @@ static void steelseries_remove(struct
> hid_device *hdev)
> }
>
> if (interface_num == sd->info->sync_interface) {
> + if (sd->info->capabilities & (SS_CAP_BT_ENABLED |
> SS_CAP_BT_DEVICE_CONNECTED))
> + sysfs_remove_group(&hdev->dev.kobj,
> +
> &steelseries_headset_attr_group);
> +
> if (sd->info->async_interface) {
> struct hid_device *sibling;
>
^ permalink raw reply [flat|nested] 29+ messages in thread
* [PATCH v3 10/18] HID: steelseries: Add settings poll infrastructure
2026-02-27 23:50 [PATCH v3 00/18] HID: steelseries: Add support for Arctis headset lineup Sriman Achanta
` (8 preceding siblings ...)
2026-02-27 23:50 ` [PATCH v3 09/18] HID: steelseries: Add Bluetooth state sysfs attributes Sriman Achanta
@ 2026-02-27 23:50 ` Sriman Achanta
2026-03-03 10:58 ` Bastien Nocera
2026-02-27 23:50 ` [PATCH v3 11/18] HID: steelseries: Add sidetone ALSA mixer control Sriman Achanta
` (8 subsequent siblings)
18 siblings, 1 reply; 29+ messages in thread
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
Christian Mayer, Sriman Achanta
Some headset settings (sidetone level, mic volume, etc.) are not
reported spontaneously but must be explicitly requested from the device.
Introduce a separate delayed work item (settings_work) for fetching
these persistent settings, independent of the existing status work.
Settings are requested once at probe time and again whenever the headset
reconnects after being disconnected. Device info structs gain
request_settings and parse_settings hooks for model-specific
implementations. The SS_CAP_EXTERNAL_CONFIG capability flag marks
devices whose writable controls can also be changed from the headset
hardware directly; writable ALSA controls on such devices will be marked
volatile.
The initial implementation adds the Arctis Nova 7 Gen2 audio settings
request (0x00, 0x20).
Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
drivers/hid/hid-steelseries.c | 37 ++++++++++++++++++++++++++++++++++-
1 file changed, 36 insertions(+), 1 deletion(-)
diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index 8c6116d02f19..f2423c350154 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -26,6 +26,7 @@
#define SS_CAP_MIC_MUTE BIT(2)
#define SS_CAP_BT_ENABLED BIT(3)
#define SS_CAP_BT_DEVICE_CONNECTED BIT(4)
+#define SS_CAP_EXTERNAL_CONFIG BIT(5)
#define SS_QUIRK_STATUS_SYNC_POLL BIT(0)
@@ -40,6 +41,9 @@ struct steelseries_device_info {
int (*request_status)(struct hid_device *hdev);
void (*parse_status)(struct steelseries_device *sd, u8 *data, int size);
+
+ int (*request_settings)(struct hid_device *hdev);
+ void (*parse_settings)(struct steelseries_device *sd, u8 *data, int size);
};
struct steelseries_device {
@@ -49,6 +53,7 @@ struct steelseries_device {
bool use_async_protocol;
struct delayed_work status_work;
+ struct delayed_work settings_work;
struct power_supply_desc battery_desc;
struct power_supply *battery;
@@ -690,6 +695,14 @@ static void steelseries_arctis_nova_7_gen2_parse_status(struct steelseries_devic
}
}
+static int steelseries_arctis_nova_7_gen2_request_settings(struct hid_device *hdev)
+{
+ const u8 data[] = { 0x00, 0x20 };
+
+ return steelseries_send_output_report(hdev, data, sizeof(data));
+}
+
+
static void steelseries_arctis_nova_pro_parse_status(struct steelseries_device *sd,
u8 *data, int size)
{
@@ -791,9 +804,11 @@ static const struct steelseries_device_info arctis_nova_7_gen2_info = {
.sync_interface = 3,
.async_interface = 5,
.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_MIC_MUTE |
- SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED,
+ SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
+ SS_CAP_EXTERNAL_CONFIG,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_nova_7_gen2_parse_status,
+ .request_settings = steelseries_arctis_nova_7_gen2_request_settings,
};
static const struct steelseries_device_info arctis_nova_pro_info = {
@@ -901,6 +916,15 @@ static void steelseries_status_timer_work_handler(struct work_struct *work)
spin_unlock_irqrestore(&sd->lock, flags);
}
+static void steelseries_settings_work_handler(struct work_struct *work)
+{
+ struct steelseries_device *sd = container_of(
+ work, struct steelseries_device, settings_work.work);
+
+ if (sd->info->request_settings)
+ sd->info->request_settings(sd->hdev);
+}
+
static int steelseries_battery_register(struct steelseries_device *sd)
{
static atomic_t battery_no = ATOMIC_INIT(0);
@@ -1185,6 +1209,9 @@ static int steelseries_raw_event(struct hid_device *hdev,
sd->info->parse_status(sd, data, size);
+ if (sd->info->parse_settings)
+ sd->info->parse_settings(sd, data, size);
+
if (sd->headset_connected != old_connected) {
hid_dbg(hdev,
"Connected status changed from %sconnected to %sconnected\n",
@@ -1194,6 +1221,9 @@ static int steelseries_raw_event(struct hid_device *hdev,
if (sd->headset_connected && !old_connected &&
sd->use_async_protocol && is_async_interface) {
schedule_delayed_work(&sd->status_work, 0);
+ if (sd->info->request_settings)
+ schedule_delayed_work(&sd->settings_work,
+ msecs_to_jiffies(10));
}
if (sd->battery) {
@@ -1329,7 +1359,11 @@ static int steelseries_probe(struct hid_device *hdev,
#endif
INIT_DELAYED_WORK(&sd->status_work, steelseries_status_timer_work_handler);
+ INIT_DELAYED_WORK(&sd->settings_work, steelseries_settings_work_handler);
+
schedule_delayed_work(&sd->status_work, msecs_to_jiffies(100));
+ if (info->request_settings)
+ schedule_delayed_work(&sd->settings_work, msecs_to_jiffies(200));
return 0;
}
@@ -1415,6 +1449,7 @@ static void steelseries_remove(struct hid_device *hdev)
spin_unlock_irqrestore(&sd->lock, flags);
cancel_delayed_work_sync(&sd->status_work);
+ cancel_delayed_work_sync(&sd->settings_work);
}
hid_hw_close(hdev);
--
2.53.0
^ permalink raw reply related [flat|nested] 29+ messages in thread* Re: [PATCH v3 10/18] HID: steelseries: Add settings poll infrastructure
2026-02-27 23:50 ` [PATCH v3 10/18] HID: steelseries: Add settings poll infrastructure Sriman Achanta
@ 2026-03-03 10:58 ` Bastien Nocera
0 siblings, 0 replies; 29+ messages in thread
From: Bastien Nocera @ 2026-03-03 10:58 UTC (permalink / raw)
To: Sriman Achanta, Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer
On Fri, 2026-02-27 at 18:50 -0500, Sriman Achanta wrote:
> Some headset settings (sidetone level, mic volume, etc.) are not
> reported spontaneously but must be explicitly requested from the
> device.
> Introduce a separate delayed work item (settings_work) for fetching
> these persistent settings, independent of the existing status work.
>
> Settings are requested once at probe time and again whenever the
> headset
> reconnects after being disconnected. Device info structs gain
> request_settings and parse_settings hooks for model-specific
> implementations. The SS_CAP_EXTERNAL_CONFIG capability flag marks
> devices whose writable controls can also be changed from the headset
> hardware directly; writable ALSA controls on such devices will be
> marked
> volatile.
The settings polling work and the addition of SS_CAP_EXTERNAL_CONFIG
for some headsets should probably be made in separate commits.
>
> The initial implementation adds the Arctis Nova 7 Gen2 audio settings
> request (0x00, 0x20).
>
> Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
> ---
> drivers/hid/hid-steelseries.c | 37
> ++++++++++++++++++++++++++++++++++-
> 1 file changed, 36 insertions(+), 1 deletion(-)
>
> diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-
> steelseries.c
> index 8c6116d02f19..f2423c350154 100644
> --- a/drivers/hid/hid-steelseries.c
> +++ b/drivers/hid/hid-steelseries.c
> @@ -26,6 +26,7 @@
> #define SS_CAP_MIC_MUTE BIT(2)
> #define SS_CAP_BT_ENABLED BIT(3)
> #define SS_CAP_BT_DEVICE_CONNECTED BIT(4)
> +#define SS_CAP_EXTERNAL_CONFIG BIT(5)
>
> #define SS_QUIRK_STATUS_SYNC_POLL BIT(0)
>
> @@ -40,6 +41,9 @@ struct steelseries_device_info {
>
> int (*request_status)(struct hid_device *hdev);
> void (*parse_status)(struct steelseries_device *sd, u8
> *data, int size);
> +
> + int (*request_settings)(struct hid_device *hdev);
> + void (*parse_settings)(struct steelseries_device *sd, u8
> *data, int size);
> };
>
> struct steelseries_device {
> @@ -49,6 +53,7 @@ struct steelseries_device {
> bool use_async_protocol;
>
> struct delayed_work status_work;
> + struct delayed_work settings_work;
>
> struct power_supply_desc battery_desc;
> struct power_supply *battery;
> @@ -690,6 +695,14 @@ static void
> steelseries_arctis_nova_7_gen2_parse_status(struct steelseries_devic
> }
> }
>
> +static int steelseries_arctis_nova_7_gen2_request_settings(struct
> hid_device *hdev)
> +{
> + const u8 data[] = { 0x00, 0x20 };
> +
> + return steelseries_send_output_report(hdev, data,
> sizeof(data));
> +}
> +
> +
> static void steelseries_arctis_nova_pro_parse_status(struct
> steelseries_device *sd,
> u8 *data, int
> size)
> {
> @@ -791,9 +804,11 @@ static const struct steelseries_device_info
> arctis_nova_7_gen2_info = {
> .sync_interface = 3,
> .async_interface = 5,
> .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX |
> SS_CAP_MIC_MUTE |
> - SS_CAP_BT_ENABLED |
> SS_CAP_BT_DEVICE_CONNECTED,
> + SS_CAP_BT_ENABLED |
> SS_CAP_BT_DEVICE_CONNECTED |
> + SS_CAP_EXTERNAL_CONFIG,
> .request_status = steelseries_arctis_nova_request_status,
> .parse_status = steelseries_arctis_nova_7_gen2_parse_status,
> + .request_settings =
> steelseries_arctis_nova_7_gen2_request_settings,
> };
>
> static const struct steelseries_device_info arctis_nova_pro_info = {
> @@ -901,6 +916,15 @@ static void
> steelseries_status_timer_work_handler(struct work_struct *work)
> spin_unlock_irqrestore(&sd->lock, flags);
> }
>
> +static void steelseries_settings_work_handler(struct work_struct
> *work)
> +{
> + struct steelseries_device *sd = container_of(
> + work, struct steelseries_device,
> settings_work.work);
> +
> + if (sd->info->request_settings)
> + sd->info->request_settings(sd->hdev);
> +}
> +
> static int steelseries_battery_register(struct steelseries_device
> *sd)
> {
> static atomic_t battery_no = ATOMIC_INIT(0);
> @@ -1185,6 +1209,9 @@ static int steelseries_raw_event(struct
> hid_device *hdev,
>
> sd->info->parse_status(sd, data, size);
>
> + if (sd->info->parse_settings)
> + sd->info->parse_settings(sd, data, size);
> +
> if (sd->headset_connected != old_connected) {
> hid_dbg(hdev,
> "Connected status changed from %sconnected
> to %sconnected\n",
> @@ -1194,6 +1221,9 @@ static int steelseries_raw_event(struct
> hid_device *hdev,
> if (sd->headset_connected && !old_connected &&
> sd->use_async_protocol && is_async_interface) {
> schedule_delayed_work(&sd->status_work, 0);
> + if (sd->info->request_settings)
> + schedule_delayed_work(&sd-
> >settings_work,
> +
> msecs_to_jiffies(10));
> }
>
> if (sd->battery) {
> @@ -1329,7 +1359,11 @@ static int steelseries_probe(struct hid_device
> *hdev,
> #endif
>
> INIT_DELAYED_WORK(&sd->status_work,
> steelseries_status_timer_work_handler);
> + INIT_DELAYED_WORK(&sd->settings_work,
> steelseries_settings_work_handler);
> +
> schedule_delayed_work(&sd->status_work,
> msecs_to_jiffies(100));
> + if (info->request_settings)
> + schedule_delayed_work(&sd->settings_work,
> msecs_to_jiffies(200));
>
> return 0;
> }
> @@ -1415,6 +1449,7 @@ static void steelseries_remove(struct
> hid_device *hdev)
> spin_unlock_irqrestore(&sd->lock, flags);
>
> cancel_delayed_work_sync(&sd->status_work);
> + cancel_delayed_work_sync(&sd->settings_work);
> }
>
> hid_hw_close(hdev);
^ permalink raw reply [flat|nested] 29+ messages in thread
* [PATCH v3 11/18] HID: steelseries: Add sidetone ALSA mixer control
2026-02-27 23:50 [PATCH v3 00/18] HID: steelseries: Add support for Arctis headset lineup Sriman Achanta
` (9 preceding siblings ...)
2026-02-27 23:50 ` [PATCH v3 10/18] HID: steelseries: Add settings poll infrastructure Sriman Achanta
@ 2026-02-27 23:50 ` Sriman Achanta
2026-02-27 23:50 ` [PATCH v3 12/18] HID: steelseries: Add mic volume " Sriman Achanta
` (7 subsequent siblings)
18 siblings, 0 replies; 29+ messages in thread
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
Christian Mayer, Sriman Achanta
Expose sidetone level as a writable ALSA integer mixer control
("Sidetone Volume"). The valid range is device-specific and stored in
the sidetone_max field of the device info struct.
The write protocol differs per family:
- Arctis 1/7: HID feature report with a separate two-byte command when
disabling (value == 0) versus a five-byte enable command otherwise
- Arctis 9: single feature report with the value offset by 0xc0
- Nova 3P: output report followed by a save-to-flash command (0x09)
- Nova 5/7/Pro: output report followed by model-specific save commands
For devices with SS_CAP_EXTERNAL_CONFIG, the control is marked volatile
as the headset can modify the value independently and the current level
is recovered via the settings poll.
Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
drivers/hid/hid-steelseries.c | 270 ++++++++++++++++++++++++++++++++--
1 file changed, 260 insertions(+), 10 deletions(-)
diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index f2423c350154..2bdf772432d0 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -27,9 +27,12 @@
#define SS_CAP_BT_ENABLED BIT(3)
#define SS_CAP_BT_DEVICE_CONNECTED BIT(4)
#define SS_CAP_EXTERNAL_CONFIG BIT(5)
+#define SS_CAP_SIDETONE BIT(6)
#define SS_QUIRK_STATUS_SYNC_POLL BIT(0)
+#define SS_SETTING_SIDETONE 0
+
struct steelseries_device;
struct steelseries_device_info {
@@ -39,11 +42,14 @@ struct steelseries_device_info {
u8 sync_interface;
u8 async_interface;
+ u8 sidetone_max;
+
int (*request_status)(struct hid_device *hdev);
void (*parse_status)(struct steelseries_device *sd, u8 *data, int size);
int (*request_settings)(struct hid_device *hdev);
void (*parse_settings)(struct steelseries_device *sd, u8 *data, int size);
+ int (*write_setting)(struct hid_device *hdev, u8 setting, u8 value);
};
struct steelseries_device {
@@ -65,9 +71,11 @@ struct steelseries_device {
struct snd_ctl_elem_id chatmix_chat_id;
struct snd_ctl_elem_id chatmix_game_id;
struct snd_ctl_elem_id mic_muted_id;
+ struct snd_ctl_elem_id sidetone_id;
u8 chatmix_chat;
u8 chatmix_game;
bool mic_muted;
+ u8 sidetone;
bool bt_enabled;
bool bt_device_connected;
@@ -434,6 +442,127 @@ static inline int steelseries_send_output_report(struct hid_device *hdev,
return steelseries_send_report(hdev, data, len, HID_OUTPUT_REPORT);
}
+/*
+ * Headset settings write functions
+ */
+
+static int steelseries_arctis_1_write_setting(struct hid_device *hdev,
+ u8 setting, u8 value)
+{
+ switch (setting) {
+ case SS_SETTING_SIDETONE:
+ if (value == 0) {
+ const u8 data[] = { 0x06, 0x35 };
+
+ return steelseries_send_feature_report(hdev, data,
+ sizeof(data));
+ } else {
+ const u8 data[] = { 0x06, 0x35, 0x01, 0x00, value };
+
+ return steelseries_send_feature_report(hdev, data,
+ sizeof(data));
+ }
+ default:
+ return -EINVAL;
+ }
+}
+
+static int steelseries_arctis_9_write_setting(struct hid_device *hdev,
+ u8 setting, u8 value)
+{
+ switch (setting) {
+ case SS_SETTING_SIDETONE: {
+ const u8 data[] = { 0x06, 0x00, value + 0xc0 };
+
+ return steelseries_send_feature_report(hdev, data, sizeof(data));
+ }
+ default:
+ return -EINVAL;
+ }
+}
+
+static int steelseries_arctis_nova_3p_write_setting(struct hid_device *hdev,
+ u8 setting, u8 value)
+{
+ const u8 save[] = { 0x09 };
+ u8 cmd;
+ int ret;
+ u8 data[2];
+
+ switch (setting) {
+ case SS_SETTING_SIDETONE:
+ cmd = 0x39;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ data[0] = cmd;
+ data[1] = value;
+
+ ret = steelseries_send_feature_report(hdev, data, sizeof(data));
+ if (ret)
+ return ret;
+
+ return steelseries_send_feature_report(hdev, save, sizeof(save));
+}
+
+static int steelseries_arctis_nova_5_write_setting(struct hid_device *hdev,
+ u8 setting, u8 value)
+{
+ const u8 save[] = { 0x00, 0x09 };
+ u8 cmd;
+ int ret;
+ u8 data[3];
+
+ switch (setting) {
+ case SS_SETTING_SIDETONE:
+ cmd = 0x39;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ data[0] = 0x00;
+ data[1] = cmd;
+ data[2] = value;
+
+ ret = steelseries_send_output_report(hdev, data, sizeof(data));
+ if (ret)
+ return ret;
+
+ msleep(10);
+
+ return steelseries_send_output_report(hdev, save, sizeof(save));
+}
+
+static int steelseries_arctis_nova_pro_write_setting(struct hid_device *hdev,
+ u8 setting, u8 value)
+{
+ const u8 save[] = { 0x06, 0x09 };
+ u8 cmd;
+ int ret;
+ u8 data[3];
+
+ switch (setting) {
+ case SS_SETTING_SIDETONE:
+ cmd = 0x39;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ data[0] = 0x06;
+ data[1] = cmd;
+ data[2] = value;
+
+ ret = steelseries_send_output_report(hdev, data, sizeof(data));
+ if (ret)
+ return ret;
+
+ return steelseries_send_output_report(hdev, save, sizeof(save));
+}
+
/*
* Headset status request functions
*/
@@ -702,6 +831,21 @@ static int steelseries_arctis_nova_7_gen2_request_settings(struct hid_device *hd
return steelseries_send_output_report(hdev, data, sizeof(data));
}
+static void steelseries_arctis_nova_7_gen2_parse_settings(
+ struct steelseries_device *sd, u8 *data, int size)
+{
+ if (size < 3)
+ return;
+
+ switch (data[0]) {
+ case 0x20:
+ sd->sidetone = data[2];
+ break;
+ case 0x39:
+ sd->sidetone = data[1];
+ break;
+ }
+}
static void steelseries_arctis_nova_pro_parse_status(struct steelseries_device *sd,
u8 *data, int size)
@@ -730,66 +874,82 @@ static const struct steelseries_device_info srws1_info = { };
static const struct steelseries_device_info arctis_1_info = {
.sync_interface = 3,
- .capabilities = SS_CAP_BATTERY,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
+ .sidetone_max = 18,
.request_status = steelseries_arctis_1_request_status,
.parse_status = steelseries_arctis_1_parse_status,
+ .write_setting = steelseries_arctis_1_write_setting,
};
static const struct steelseries_device_info arctis_7_info = {
.sync_interface = 5,
- .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
+ .sidetone_max = 18,
.request_status = steelseries_arctis_7_request_status,
.parse_status = steelseries_arctis_7_parse_status,
+ .write_setting = steelseries_arctis_1_write_setting,
};
static const struct steelseries_device_info arctis_7_plus_info = {
.sync_interface = 3,
- .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
+ .sidetone_max = 3,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_7_plus_parse_status,
+ .write_setting = steelseries_arctis_nova_5_write_setting,
};
static const struct steelseries_device_info arctis_9_info = {
.sync_interface = 0,
- .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
+ .sidetone_max = 61,
.request_status = steelseries_arctis_9_request_status,
.parse_status = steelseries_arctis_9_parse_status,
+ .write_setting = steelseries_arctis_9_write_setting,
};
static const struct steelseries_device_info arctis_nova_3p_info = {
.sync_interface = 4,
- .capabilities = SS_CAP_BATTERY,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
+ .sidetone_max = 10,
.request_status = steelseries_arctis_nova_3p_request_status,
.parse_status = steelseries_arctis_nova_3p_parse_status,
+ .write_setting = steelseries_arctis_nova_3p_write_setting,
};
static const struct steelseries_device_info arctis_nova_5_info = {
.sync_interface = 3,
- .capabilities = SS_CAP_BATTERY,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
+ .sidetone_max = 10,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_nova_5_parse_status,
+ .write_setting = steelseries_arctis_nova_5_write_setting,
};
static const struct steelseries_device_info arctis_nova_5x_info = {
.sync_interface = 3,
- .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
+ .sidetone_max = 10,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_nova_5x_parse_status,
+ .write_setting = steelseries_arctis_nova_5_write_setting,
};
static const struct steelseries_device_info arctis_nova_7_info = {
.sync_interface = 3,
- .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
+ .sidetone_max = 3,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_nova_7_parse_status,
+ .write_setting = steelseries_arctis_nova_5_write_setting,
};
static const struct steelseries_device_info arctis_nova_7p_info = {
@@ -805,19 +965,25 @@ static const struct steelseries_device_info arctis_nova_7_gen2_info = {
.async_interface = 5,
.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_MIC_MUTE |
SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
- SS_CAP_EXTERNAL_CONFIG,
+ SS_CAP_EXTERNAL_CONFIG | SS_CAP_SIDETONE,
+ .sidetone_max = 3,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_nova_7_gen2_parse_status,
.request_settings = steelseries_arctis_nova_7_gen2_request_settings,
+ .parse_settings = steelseries_arctis_nova_7_gen2_parse_settings,
+ .write_setting = steelseries_arctis_nova_5_write_setting,
};
static const struct steelseries_device_info arctis_nova_pro_info = {
.sync_interface = 4,
.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_MIC_MUTE |
- SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED,
+ SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
+ SS_CAP_SIDETONE,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
+ .sidetone_max = 3,
.request_status = steelseries_arctis_nova_pro_request_status,
.parse_status = steelseries_arctis_nova_pro_parse_status,
+ .write_setting = steelseries_arctis_nova_pro_write_setting,
};
/*
@@ -1113,6 +1279,70 @@ static const struct snd_kcontrol_new steelseries_mic_muted_control = {
.get = steelseries_mic_muted_get,
};
+static int steelseries_sidetone_info(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_info *uinfo)
+{
+ struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
+
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 1;
+ uinfo->value.integer.min = 0;
+ uinfo->value.integer.max = sd->info->sidetone_max;
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int steelseries_sidetone_get(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
+ unsigned long flags;
+
+ spin_lock_irqsave(&sd->lock, flags);
+ ucontrol->value.integer.value[0] = sd->sidetone;
+ spin_unlock_irqrestore(&sd->lock, flags);
+ return 0;
+}
+
+static int steelseries_sidetone_put(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
+ unsigned long flags;
+ u8 new_sidetone;
+ int ret;
+
+ new_sidetone = ucontrol->value.integer.value[0];
+ if (new_sidetone > sd->info->sidetone_max)
+ return -EINVAL;
+
+ spin_lock_irqsave(&sd->lock, flags);
+ if (sd->sidetone == new_sidetone) {
+ spin_unlock_irqrestore(&sd->lock, flags);
+ return 0;
+ }
+ spin_unlock_irqrestore(&sd->lock, flags);
+
+ ret = sd->info->write_setting(sd->hdev, SS_SETTING_SIDETONE,
+ new_sidetone);
+ if (ret)
+ return ret;
+
+ spin_lock_irqsave(&sd->lock, flags);
+ sd->sidetone = new_sidetone;
+ spin_unlock_irqrestore(&sd->lock, flags);
+
+ return 1;
+}
+
+static const struct snd_kcontrol_new steelseries_sidetone_control = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "Sidetone Volume",
+ .info = steelseries_sidetone_info,
+ .get = steelseries_sidetone_get,
+ .put = steelseries_sidetone_put,
+};
+
static int steelseries_snd_register(struct steelseries_device *sd)
{
struct hid_device *hdev = sd->hdev;
@@ -1155,6 +1385,21 @@ static int steelseries_snd_register(struct steelseries_device *sd)
sd->mic_muted_id = kctl->id;
}
+ if (sd->info->capabilities & SS_CAP_SIDETONE) {
+ struct snd_kcontrol *kctl;
+ struct snd_kcontrol_new sidetone_ctl = steelseries_sidetone_control;
+
+ sidetone_ctl.access = SNDRV_CTL_ELEM_ACCESS_READWRITE;
+ if (sd->info->capabilities & SS_CAP_EXTERNAL_CONFIG)
+ sidetone_ctl.access |= SNDRV_CTL_ELEM_ACCESS_VOLATILE;
+
+ kctl = snd_ctl_new1(&sidetone_ctl, sd);
+ ret = snd_ctl_add(sd->card, kctl);
+ if (ret < 0)
+ goto err_free_card;
+ sd->sidetone_id = kctl->id;
+ }
+
ret = snd_card_register(sd->card);
if (ret < 0)
goto err_free_card;
@@ -1185,6 +1430,7 @@ static int steelseries_raw_event(struct hid_device *hdev,
u8 old_chatmix_chat;
u8 old_chatmix_game;
bool old_mic_muted;
+ u8 old_sidetone;
bool is_async_interface = false;
if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1)
@@ -1199,6 +1445,7 @@ static int steelseries_raw_event(struct hid_device *hdev,
old_chatmix_chat = sd->chatmix_chat;
old_chatmix_game = sd->chatmix_game;
old_mic_muted = sd->mic_muted;
+ old_sidetone = sd->sidetone;
if (hid_is_usb(hdev)) {
struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
@@ -1259,6 +1506,9 @@ static int steelseries_raw_event(struct hid_device *hdev,
if (sd->mic_muted != old_mic_muted)
snd_ctl_notify(sd->card, SNDRV_CTL_EVENT_MASK_VALUE,
&sd->mic_muted_id);
+ if (sd->sidetone != old_sidetone)
+ snd_ctl_notify(sd->card, SNDRV_CTL_EVENT_MASK_VALUE,
+ &sd->sidetone_id);
}
return 0;
--
2.53.0
^ permalink raw reply related [flat|nested] 29+ messages in thread* [PATCH v3 12/18] HID: steelseries: Add mic volume ALSA mixer control
2026-02-27 23:50 [PATCH v3 00/18] HID: steelseries: Add support for Arctis headset lineup Sriman Achanta
` (10 preceding siblings ...)
2026-02-27 23:50 ` [PATCH v3 11/18] HID: steelseries: Add sidetone ALSA mixer control Sriman Achanta
@ 2026-02-27 23:50 ` Sriman Achanta
2026-02-27 23:50 ` [PATCH v3 13/18] HID: steelseries: Add volume limiter " Sriman Achanta
` (6 subsequent siblings)
18 siblings, 0 replies; 29+ messages in thread
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
Christian Mayer, Sriman Achanta
Expose microphone gain as a writable ALSA integer mixer control
("Mic Volume"). The valid range is per-device; mic_volume_min and
mic_volume_max are added to the device info struct to accommodate models
with a non-zero minimum (e.g. the Arctis Nova Pro, which has a range
of 1-10).
The write command (0x37) is added to the Nova 3P, Nova 5, and Nova Pro
write handlers. On the Nova Pro, the current mic volume is recovered
from the 0x06/0xb0 settings response via a new parse_settings hook.
Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
drivers/hid/hid-steelseries.c | 141 ++++++++++++++++++++++++++++++++--
1 file changed, 134 insertions(+), 7 deletions(-)
diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index 2bdf772432d0..1339f965f67f 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -28,10 +28,12 @@
#define SS_CAP_BT_DEVICE_CONNECTED BIT(4)
#define SS_CAP_EXTERNAL_CONFIG BIT(5)
#define SS_CAP_SIDETONE BIT(6)
+#define SS_CAP_MIC_VOLUME BIT(7)
#define SS_QUIRK_STATUS_SYNC_POLL BIT(0)
#define SS_SETTING_SIDETONE 0
+#define SS_SETTING_MIC_VOLUME 1
struct steelseries_device;
@@ -43,6 +45,8 @@ struct steelseries_device_info {
u8 async_interface;
u8 sidetone_max;
+ u8 mic_volume_min;
+ u8 mic_volume_max;
int (*request_status)(struct hid_device *hdev);
void (*parse_status)(struct steelseries_device *sd, u8 *data, int size);
@@ -72,10 +76,12 @@ struct steelseries_device {
struct snd_ctl_elem_id chatmix_game_id;
struct snd_ctl_elem_id mic_muted_id;
struct snd_ctl_elem_id sidetone_id;
+ struct snd_ctl_elem_id mic_volume_id;
u8 chatmix_chat;
u8 chatmix_game;
bool mic_muted;
u8 sidetone;
+ u8 mic_volume;
bool bt_enabled;
bool bt_device_connected;
@@ -493,6 +499,9 @@ static int steelseries_arctis_nova_3p_write_setting(struct hid_device *hdev,
case SS_SETTING_SIDETONE:
cmd = 0x39;
break;
+ case SS_SETTING_MIC_VOLUME:
+ cmd = 0x37;
+ break;
default:
return -EINVAL;
}
@@ -519,6 +528,9 @@ static int steelseries_arctis_nova_5_write_setting(struct hid_device *hdev,
case SS_SETTING_SIDETONE:
cmd = 0x39;
break;
+ case SS_SETTING_MIC_VOLUME:
+ cmd = 0x37;
+ break;
default:
return -EINVAL;
}
@@ -548,6 +560,9 @@ static int steelseries_arctis_nova_pro_write_setting(struct hid_device *hdev,
case SS_SETTING_SIDETONE:
cmd = 0x39;
break;
+ case SS_SETTING_MIC_VOLUME:
+ cmd = 0x37;
+ break;
default:
return -EINVAL;
}
@@ -839,14 +854,28 @@ static void steelseries_arctis_nova_7_gen2_parse_settings(
switch (data[0]) {
case 0x20:
+ sd->mic_volume = data[1];
sd->sidetone = data[2];
break;
+ case 0x37:
+ sd->mic_volume = data[1];
+ break;
case 0x39:
sd->sidetone = data[1];
break;
}
}
+static void steelseries_arctis_nova_pro_parse_settings(
+ struct steelseries_device *sd, u8 *data, int size)
+{
+ if (size < 10)
+ return;
+
+ if (data[0] == 0x06 && data[1] == 0xb0)
+ sd->mic_volume = data[9];
+}
+
static void steelseries_arctis_nova_pro_parse_status(struct steelseries_device *sd,
u8 *data, int size)
{
@@ -914,9 +943,10 @@ static const struct steelseries_device_info arctis_9_info = {
static const struct steelseries_device_info arctis_nova_3p_info = {
.sync_interface = 4,
- .capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 10,
+ .mic_volume_max = 14,
.request_status = steelseries_arctis_nova_3p_request_status,
.parse_status = steelseries_arctis_nova_3p_parse_status,
.write_setting = steelseries_arctis_nova_3p_write_setting,
@@ -924,9 +954,10 @@ static const struct steelseries_device_info arctis_nova_3p_info = {
static const struct steelseries_device_info arctis_nova_5_info = {
.sync_interface = 3,
- .capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 10,
+ .mic_volume_max = 15,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_nova_5_parse_status,
.write_setting = steelseries_arctis_nova_5_write_setting,
@@ -934,9 +965,11 @@ static const struct steelseries_device_info arctis_nova_5_info = {
static const struct steelseries_device_info arctis_nova_5x_info = {
.sync_interface = 3,
- .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
+ SS_CAP_MIC_VOLUME,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 10,
+ .mic_volume_max = 15,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_nova_5x_parse_status,
.write_setting = steelseries_arctis_nova_5_write_setting,
@@ -944,9 +977,11 @@ static const struct steelseries_device_info arctis_nova_5x_info = {
static const struct steelseries_device_info arctis_nova_7_info = {
.sync_interface = 3,
- .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
+ SS_CAP_MIC_VOLUME,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 3,
+ .mic_volume_max = 7,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_nova_7_parse_status,
.write_setting = steelseries_arctis_nova_5_write_setting,
@@ -954,10 +989,12 @@ static const struct steelseries_device_info arctis_nova_7_info = {
static const struct steelseries_device_info arctis_nova_7p_info = {
.sync_interface = 3,
- .capabilities = SS_CAP_BATTERY,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_MIC_VOLUME,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
+ .mic_volume_max = 7,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_nova_7_parse_status,
+ .write_setting = steelseries_arctis_nova_5_write_setting,
};
static const struct steelseries_device_info arctis_nova_7_gen2_info = {
@@ -965,8 +1002,10 @@ static const struct steelseries_device_info arctis_nova_7_gen2_info = {
.async_interface = 5,
.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_MIC_MUTE |
SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
- SS_CAP_EXTERNAL_CONFIG | SS_CAP_SIDETONE,
+ SS_CAP_EXTERNAL_CONFIG | SS_CAP_SIDETONE |
+ SS_CAP_MIC_VOLUME,
.sidetone_max = 3,
+ .mic_volume_max = 7,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_nova_7_gen2_parse_status,
.request_settings = steelseries_arctis_nova_7_gen2_request_settings,
@@ -978,11 +1017,14 @@ static const struct steelseries_device_info arctis_nova_pro_info = {
.sync_interface = 4,
.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_MIC_MUTE |
SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
- SS_CAP_SIDETONE,
+ SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 3,
+ .mic_volume_min = 1,
+ .mic_volume_max = 10,
.request_status = steelseries_arctis_nova_pro_request_status,
.parse_status = steelseries_arctis_nova_pro_parse_status,
+ .parse_settings = steelseries_arctis_nova_pro_parse_settings,
.write_setting = steelseries_arctis_nova_pro_write_setting,
};
@@ -1343,6 +1385,71 @@ static const struct snd_kcontrol_new steelseries_sidetone_control = {
.put = steelseries_sidetone_put,
};
+static int steelseries_mic_volume_info(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_info *uinfo)
+{
+ struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
+
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 1;
+ uinfo->value.integer.min = sd->info->mic_volume_min;
+ uinfo->value.integer.max = sd->info->mic_volume_max;
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int steelseries_mic_volume_get(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
+ unsigned long flags;
+
+ spin_lock_irqsave(&sd->lock, flags);
+ ucontrol->value.integer.value[0] = sd->mic_volume;
+ spin_unlock_irqrestore(&sd->lock, flags);
+ return 0;
+}
+
+static int steelseries_mic_volume_put(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
+ unsigned long flags;
+ u8 new_mic_volume;
+ int ret;
+
+ new_mic_volume = ucontrol->value.integer.value[0];
+ if (new_mic_volume < sd->info->mic_volume_min ||
+ new_mic_volume > sd->info->mic_volume_max)
+ return -EINVAL;
+
+ spin_lock_irqsave(&sd->lock, flags);
+ if (sd->mic_volume == new_mic_volume) {
+ spin_unlock_irqrestore(&sd->lock, flags);
+ return 0;
+ }
+ spin_unlock_irqrestore(&sd->lock, flags);
+
+ ret = sd->info->write_setting(sd->hdev, SS_SETTING_MIC_VOLUME,
+ new_mic_volume);
+ if (ret)
+ return ret;
+
+ spin_lock_irqsave(&sd->lock, flags);
+ sd->mic_volume = new_mic_volume;
+ spin_unlock_irqrestore(&sd->lock, flags);
+
+ return 1;
+}
+
+static const struct snd_kcontrol_new steelseries_mic_volume_control = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "Mic Volume",
+ .info = steelseries_mic_volume_info,
+ .get = steelseries_mic_volume_get,
+ .put = steelseries_mic_volume_put,
+};
+
static int steelseries_snd_register(struct steelseries_device *sd)
{
struct hid_device *hdev = sd->hdev;
@@ -1400,6 +1507,21 @@ static int steelseries_snd_register(struct steelseries_device *sd)
sd->sidetone_id = kctl->id;
}
+ if (sd->info->capabilities & SS_CAP_MIC_VOLUME) {
+ struct snd_kcontrol *kctl;
+ struct snd_kcontrol_new mic_vol_ctl = steelseries_mic_volume_control;
+
+ mic_vol_ctl.access = SNDRV_CTL_ELEM_ACCESS_READWRITE;
+ if (sd->info->capabilities & SS_CAP_EXTERNAL_CONFIG)
+ mic_vol_ctl.access |= SNDRV_CTL_ELEM_ACCESS_VOLATILE;
+
+ kctl = snd_ctl_new1(&mic_vol_ctl, sd);
+ ret = snd_ctl_add(sd->card, kctl);
+ if (ret < 0)
+ goto err_free_card;
+ sd->mic_volume_id = kctl->id;
+ }
+
ret = snd_card_register(sd->card);
if (ret < 0)
goto err_free_card;
@@ -1431,6 +1553,7 @@ static int steelseries_raw_event(struct hid_device *hdev,
u8 old_chatmix_game;
bool old_mic_muted;
u8 old_sidetone;
+ u8 old_mic_volume;
bool is_async_interface = false;
if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1)
@@ -1446,6 +1569,7 @@ static int steelseries_raw_event(struct hid_device *hdev,
old_chatmix_game = sd->chatmix_game;
old_mic_muted = sd->mic_muted;
old_sidetone = sd->sidetone;
+ old_mic_volume = sd->mic_volume;
if (hid_is_usb(hdev)) {
struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
@@ -1509,6 +1633,9 @@ static int steelseries_raw_event(struct hid_device *hdev,
if (sd->sidetone != old_sidetone)
snd_ctl_notify(sd->card, SNDRV_CTL_EVENT_MASK_VALUE,
&sd->sidetone_id);
+ if (sd->mic_volume != old_mic_volume)
+ snd_ctl_notify(sd->card, SNDRV_CTL_EVENT_MASK_VALUE,
+ &sd->mic_volume_id);
}
return 0;
--
2.53.0
^ permalink raw reply related [flat|nested] 29+ messages in thread* [PATCH v3 13/18] HID: steelseries: Add volume limiter ALSA mixer control
2026-02-27 23:50 [PATCH v3 00/18] HID: steelseries: Add support for Arctis headset lineup Sriman Achanta
` (11 preceding siblings ...)
2026-02-27 23:50 ` [PATCH v3 12/18] HID: steelseries: Add mic volume " Sriman Achanta
@ 2026-02-27 23:50 ` Sriman Achanta
2026-02-27 23:50 ` [PATCH v3 14/18] HID: steelseries: Add Bluetooth call audio ducking control Sriman Achanta
` (5 subsequent siblings)
18 siblings, 0 replies; 29+ messages in thread
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
Christian Mayer, Sriman Achanta
Expose the maximum output volume cap as a writable ALSA boolean mixer
control ("Volume Limiter").
The Nova 7 family uses command 0x3a for this setting whereas the Nova 5
family uses 0x27, so a dedicated steelseries_arctis_nova_7_write_setting()
is introduced and the Nova 7, Nova 7P, and Nova 7 Gen2 entries are
updated to use it instead of the Nova 5 handler.
Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
drivers/hid/hid-steelseries.c | 145 +++++++++++++++++++++++++++++++---
1 file changed, 136 insertions(+), 9 deletions(-)
diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index 1339f965f67f..47ffec481571 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -29,11 +29,13 @@
#define SS_CAP_EXTERNAL_CONFIG BIT(5)
#define SS_CAP_SIDETONE BIT(6)
#define SS_CAP_MIC_VOLUME BIT(7)
+#define SS_CAP_VOLUME_LIMITER BIT(8)
#define SS_QUIRK_STATUS_SYNC_POLL BIT(0)
#define SS_SETTING_SIDETONE 0
#define SS_SETTING_MIC_VOLUME 1
+#define SS_SETTING_VOLUME_LIMITER 2
struct steelseries_device;
@@ -77,11 +79,13 @@ struct steelseries_device {
struct snd_ctl_elem_id mic_muted_id;
struct snd_ctl_elem_id sidetone_id;
struct snd_ctl_elem_id mic_volume_id;
+ struct snd_ctl_elem_id volume_limiter_id;
u8 chatmix_chat;
u8 chatmix_game;
bool mic_muted;
u8 sidetone;
u8 mic_volume;
+ bool volume_limiter;
bool bt_enabled;
bool bt_device_connected;
@@ -531,6 +535,44 @@ static int steelseries_arctis_nova_5_write_setting(struct hid_device *hdev,
case SS_SETTING_MIC_VOLUME:
cmd = 0x37;
break;
+ case SS_SETTING_VOLUME_LIMITER:
+ cmd = 0x27;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ data[0] = 0x00;
+ data[1] = cmd;
+ data[2] = value;
+
+ ret = steelseries_send_output_report(hdev, data, sizeof(data));
+ if (ret)
+ return ret;
+
+ msleep(10);
+
+ return steelseries_send_output_report(hdev, save, sizeof(save));
+}
+
+static int steelseries_arctis_nova_7_write_setting(struct hid_device *hdev,
+ u8 setting, u8 value)
+{
+ const u8 save[] = { 0x00, 0x09 };
+ u8 cmd;
+ int ret;
+ u8 data[3];
+
+ switch (setting) {
+ case SS_SETTING_SIDETONE:
+ cmd = 0x39;
+ break;
+ case SS_SETTING_MIC_VOLUME:
+ cmd = 0x37;
+ break;
+ case SS_SETTING_VOLUME_LIMITER:
+ cmd = 0x3a;
+ break;
default:
return -EINVAL;
}
@@ -849,13 +891,14 @@ static int steelseries_arctis_nova_7_gen2_request_settings(struct hid_device *hd
static void steelseries_arctis_nova_7_gen2_parse_settings(
struct steelseries_device *sd, u8 *data, int size)
{
- if (size < 3)
+ if (size < 4)
return;
switch (data[0]) {
case 0x20:
sd->mic_volume = data[1];
sd->sidetone = data[2];
+ sd->volume_limiter = data[3];
break;
case 0x37:
sd->mic_volume = data[1];
@@ -863,6 +906,9 @@ static void steelseries_arctis_nova_7_gen2_parse_settings(
case 0x39:
sd->sidetone = data[1];
break;
+ case 0x3a:
+ sd->volume_limiter = data[1];
+ break;
}
}
@@ -954,7 +1000,8 @@ static const struct steelseries_device_info arctis_nova_3p_info = {
static const struct steelseries_device_info arctis_nova_5_info = {
.sync_interface = 3,
- .capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME |
+ SS_CAP_VOLUME_LIMITER,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 10,
.mic_volume_max = 15,
@@ -966,7 +1013,7 @@ static const struct steelseries_device_info arctis_nova_5_info = {
static const struct steelseries_device_info arctis_nova_5x_info = {
.sync_interface = 3,
.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
- SS_CAP_MIC_VOLUME,
+ SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 10,
.mic_volume_max = 15,
@@ -978,23 +1025,23 @@ static const struct steelseries_device_info arctis_nova_5x_info = {
static const struct steelseries_device_info arctis_nova_7_info = {
.sync_interface = 3,
.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
- SS_CAP_MIC_VOLUME,
+ SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 3,
.mic_volume_max = 7,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_nova_7_parse_status,
- .write_setting = steelseries_arctis_nova_5_write_setting,
+ .write_setting = steelseries_arctis_nova_7_write_setting,
};
static const struct steelseries_device_info arctis_nova_7p_info = {
.sync_interface = 3,
- .capabilities = SS_CAP_BATTERY | SS_CAP_MIC_VOLUME,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.mic_volume_max = 7,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_nova_7_parse_status,
- .write_setting = steelseries_arctis_nova_5_write_setting,
+ .write_setting = steelseries_arctis_nova_7_write_setting,
};
static const struct steelseries_device_info arctis_nova_7_gen2_info = {
@@ -1003,14 +1050,14 @@ static const struct steelseries_device_info arctis_nova_7_gen2_info = {
.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_MIC_MUTE |
SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
SS_CAP_EXTERNAL_CONFIG | SS_CAP_SIDETONE |
- SS_CAP_MIC_VOLUME,
+ SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER,
.sidetone_max = 3,
.mic_volume_max = 7,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_nova_7_gen2_parse_status,
.request_settings = steelseries_arctis_nova_7_gen2_request_settings,
.parse_settings = steelseries_arctis_nova_7_gen2_parse_settings,
- .write_setting = steelseries_arctis_nova_5_write_setting,
+ .write_setting = steelseries_arctis_nova_7_write_setting,
};
static const struct steelseries_device_info arctis_nova_pro_info = {
@@ -1450,6 +1497,66 @@ static const struct snd_kcontrol_new steelseries_mic_volume_control = {
.put = steelseries_mic_volume_put,
};
+static int steelseries_volume_limiter_info(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_info *uinfo)
+{
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
+ uinfo->count = 1;
+ uinfo->value.integer.min = 0;
+ uinfo->value.integer.max = 1;
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int steelseries_volume_limiter_get(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
+ unsigned long flags;
+
+ spin_lock_irqsave(&sd->lock, flags);
+ ucontrol->value.integer.value[0] = sd->volume_limiter;
+ spin_unlock_irqrestore(&sd->lock, flags);
+ return 0;
+}
+
+static int steelseries_volume_limiter_put(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
+ unsigned long flags;
+ bool new_volume_limiter;
+ int ret;
+
+ new_volume_limiter = !!ucontrol->value.integer.value[0];
+
+ spin_lock_irqsave(&sd->lock, flags);
+ if (sd->volume_limiter == new_volume_limiter) {
+ spin_unlock_irqrestore(&sd->lock, flags);
+ return 0;
+ }
+ spin_unlock_irqrestore(&sd->lock, flags);
+
+ ret = sd->info->write_setting(sd->hdev, SS_SETTING_VOLUME_LIMITER,
+ new_volume_limiter ? 1 : 0);
+ if (ret)
+ return ret;
+
+ spin_lock_irqsave(&sd->lock, flags);
+ sd->volume_limiter = new_volume_limiter;
+ spin_unlock_irqrestore(&sd->lock, flags);
+
+ return 1;
+}
+
+static const struct snd_kcontrol_new steelseries_volume_limiter_control = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "Volume Limiter",
+ .info = steelseries_volume_limiter_info,
+ .get = steelseries_volume_limiter_get,
+ .put = steelseries_volume_limiter_put,
+};
+
static int steelseries_snd_register(struct steelseries_device *sd)
{
struct hid_device *hdev = sd->hdev;
@@ -1522,6 +1629,21 @@ static int steelseries_snd_register(struct steelseries_device *sd)
sd->mic_volume_id = kctl->id;
}
+ if (sd->info->capabilities & SS_CAP_VOLUME_LIMITER) {
+ struct snd_kcontrol *kctl;
+ struct snd_kcontrol_new vol_lim_ctl = steelseries_volume_limiter_control;
+
+ vol_lim_ctl.access = SNDRV_CTL_ELEM_ACCESS_READWRITE;
+ if (sd->info->capabilities & SS_CAP_EXTERNAL_CONFIG)
+ vol_lim_ctl.access |= SNDRV_CTL_ELEM_ACCESS_VOLATILE;
+
+ kctl = snd_ctl_new1(&vol_lim_ctl, sd);
+ ret = snd_ctl_add(sd->card, kctl);
+ if (ret < 0)
+ goto err_free_card;
+ sd->volume_limiter_id = kctl->id;
+ }
+
ret = snd_card_register(sd->card);
if (ret < 0)
goto err_free_card;
@@ -1554,6 +1676,7 @@ static int steelseries_raw_event(struct hid_device *hdev,
bool old_mic_muted;
u8 old_sidetone;
u8 old_mic_volume;
+ bool old_volume_limiter;
bool is_async_interface = false;
if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1)
@@ -1570,6 +1693,7 @@ static int steelseries_raw_event(struct hid_device *hdev,
old_mic_muted = sd->mic_muted;
old_sidetone = sd->sidetone;
old_mic_volume = sd->mic_volume;
+ old_volume_limiter = sd->volume_limiter;
if (hid_is_usb(hdev)) {
struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
@@ -1636,6 +1760,9 @@ static int steelseries_raw_event(struct hid_device *hdev,
if (sd->mic_volume != old_mic_volume)
snd_ctl_notify(sd->card, SNDRV_CTL_EVENT_MASK_VALUE,
&sd->mic_volume_id);
+ if (sd->volume_limiter != old_volume_limiter)
+ snd_ctl_notify(sd->card, SNDRV_CTL_EVENT_MASK_VALUE,
+ &sd->volume_limiter_id);
}
return 0;
--
2.53.0
^ permalink raw reply related [flat|nested] 29+ messages in thread* [PATCH v3 14/18] HID: steelseries: Add Bluetooth call audio ducking control
2026-02-27 23:50 [PATCH v3 00/18] HID: steelseries: Add support for Arctis headset lineup Sriman Achanta
` (12 preceding siblings ...)
2026-02-27 23:50 ` [PATCH v3 13/18] HID: steelseries: Add volume limiter " Sriman Achanta
@ 2026-02-27 23:50 ` Sriman Achanta
2026-03-03 10:59 ` Bastien Nocera
2026-02-27 23:50 ` [PATCH v3 15/18] HID: steelseries: Add inactive time sysfs attribute Sriman Achanta
` (4 subsequent siblings)
18 siblings, 1 reply; 29+ messages in thread
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
Christian Mayer, Sriman Achanta
Expose Bluetooth call audio ducking behavior as a writable ALSA
enumerated mixer control ("Bluetooth Call Audio Ducking"), with three
options: off, lower game audio by 12 dB, or mute game audio entirely.
On the Arctis Nova 7 Gen2, this setting is stored alongside inactive
timeout and Bluetooth auto-enable in a dedicated device configuration
block. The settings request is expanded to also send a 0x00/0xa0 device
query in addition to the existing 0x00/0x20 audio settings query.
Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
drivers/hid/hid-steelseries.c | 120 ++++++++++++++++++++++++++++++++--
1 file changed, 114 insertions(+), 6 deletions(-)
diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index 47ffec481571..bb9abbb0b6f8 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -30,12 +30,14 @@
#define SS_CAP_SIDETONE BIT(6)
#define SS_CAP_MIC_VOLUME BIT(7)
#define SS_CAP_VOLUME_LIMITER BIT(8)
+#define SS_CAP_BT_CALL_DUCKING BIT(9)
#define SS_QUIRK_STATUS_SYNC_POLL BIT(0)
#define SS_SETTING_SIDETONE 0
#define SS_SETTING_MIC_VOLUME 1
#define SS_SETTING_VOLUME_LIMITER 2
+#define SS_SETTING_BT_CALL_DUCKING 3
struct steelseries_device;
@@ -80,12 +82,14 @@ struct steelseries_device {
struct snd_ctl_elem_id sidetone_id;
struct snd_ctl_elem_id mic_volume_id;
struct snd_ctl_elem_id volume_limiter_id;
+ struct snd_ctl_elem_id bt_call_ducking_id;
u8 chatmix_chat;
u8 chatmix_game;
bool mic_muted;
u8 sidetone;
u8 mic_volume;
bool volume_limiter;
+ u8 bt_call_ducking;
bool bt_enabled;
bool bt_device_connected;
@@ -573,6 +577,9 @@ static int steelseries_arctis_nova_7_write_setting(struct hid_device *hdev,
case SS_SETTING_VOLUME_LIMITER:
cmd = 0x3a;
break;
+ case SS_SETTING_BT_CALL_DUCKING:
+ cmd = 0xb3;
+ break;
default:
return -EINVAL;
}
@@ -883,15 +890,23 @@ static void steelseries_arctis_nova_7_gen2_parse_status(struct steelseries_devic
static int steelseries_arctis_nova_7_gen2_request_settings(struct hid_device *hdev)
{
- const u8 data[] = { 0x00, 0x20 };
+ const u8 audio_data[] = { 0x00, 0x20 };
+ const u8 device_data[] = { 0x00, 0xa0 };
+ int ret;
- return steelseries_send_output_report(hdev, data, sizeof(data));
+ ret = steelseries_send_output_report(hdev, audio_data, sizeof(audio_data));
+ if (ret)
+ return ret;
+
+ msleep(10);
+
+ return steelseries_send_output_report(hdev, device_data, sizeof(device_data));
}
static void steelseries_arctis_nova_7_gen2_parse_settings(
struct steelseries_device *sd, u8 *data, int size)
{
- if (size < 4)
+ if (size < 5)
return;
switch (data[0]) {
@@ -900,6 +915,9 @@ static void steelseries_arctis_nova_7_gen2_parse_settings(
sd->sidetone = data[2];
sd->volume_limiter = data[3];
break;
+ case 0xa0:
+ sd->bt_call_ducking = data[4];
+ break;
case 0x37:
sd->mic_volume = data[1];
break;
@@ -909,6 +927,9 @@ static void steelseries_arctis_nova_7_gen2_parse_settings(
case 0x3a:
sd->volume_limiter = data[1];
break;
+ case 0xb3:
+ sd->bt_call_ducking = data[1];
+ break;
}
}
@@ -1025,7 +1046,8 @@ static const struct steelseries_device_info arctis_nova_5x_info = {
static const struct steelseries_device_info arctis_nova_7_info = {
.sync_interface = 3,
.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
- SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER,
+ SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
+ SS_CAP_BT_CALL_DUCKING,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 3,
.mic_volume_max = 7,
@@ -1036,7 +1058,8 @@ static const struct steelseries_device_info arctis_nova_7_info = {
static const struct steelseries_device_info arctis_nova_7p_info = {
.sync_interface = 3,
- .capabilities = SS_CAP_BATTERY | SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
+ SS_CAP_BT_CALL_DUCKING,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.mic_volume_max = 7,
.request_status = steelseries_arctis_nova_request_status,
@@ -1050,7 +1073,8 @@ static const struct steelseries_device_info arctis_nova_7_gen2_info = {
.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_MIC_MUTE |
SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
SS_CAP_EXTERNAL_CONFIG | SS_CAP_SIDETONE |
- SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER,
+ SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
+ SS_CAP_BT_CALL_DUCKING,
.sidetone_max = 3,
.mic_volume_max = 7,
.request_status = steelseries_arctis_nova_request_status,
@@ -1557,6 +1581,70 @@ static const struct snd_kcontrol_new steelseries_volume_limiter_control = {
.put = steelseries_volume_limiter_put,
};
+static const char *const bt_call_ducking_texts[] = {
+ "Off",
+ "Lower Volume (-12dB)",
+ "Mute Game",
+};
+
+static int steelseries_bt_call_ducking_info(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_info *uinfo)
+{
+ return snd_ctl_enum_info(uinfo, 1, ARRAY_SIZE(bt_call_ducking_texts),
+ bt_call_ducking_texts);
+}
+
+static int steelseries_bt_call_ducking_get(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
+ unsigned long flags;
+
+ spin_lock_irqsave(&sd->lock, flags);
+ ucontrol->value.enumerated.item[0] = sd->bt_call_ducking;
+ spin_unlock_irqrestore(&sd->lock, flags);
+ return 0;
+}
+
+static int steelseries_bt_call_ducking_put(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
+ unsigned long flags;
+ u8 new_value;
+ int ret;
+
+ new_value = ucontrol->value.enumerated.item[0];
+ if (new_value >= ARRAY_SIZE(bt_call_ducking_texts))
+ return -EINVAL;
+
+ spin_lock_irqsave(&sd->lock, flags);
+ if (sd->bt_call_ducking == new_value) {
+ spin_unlock_irqrestore(&sd->lock, flags);
+ return 0;
+ }
+ spin_unlock_irqrestore(&sd->lock, flags);
+
+ ret = sd->info->write_setting(sd->hdev, SS_SETTING_BT_CALL_DUCKING,
+ new_value);
+ if (ret)
+ return ret;
+
+ spin_lock_irqsave(&sd->lock, flags);
+ sd->bt_call_ducking = new_value;
+ spin_unlock_irqrestore(&sd->lock, flags);
+
+ return 1;
+}
+
+static const struct snd_kcontrol_new steelseries_bt_call_ducking_control = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "Bluetooth Call Audio Ducking",
+ .info = steelseries_bt_call_ducking_info,
+ .get = steelseries_bt_call_ducking_get,
+ .put = steelseries_bt_call_ducking_put,
+};
+
static int steelseries_snd_register(struct steelseries_device *sd)
{
struct hid_device *hdev = sd->hdev;
@@ -1644,6 +1732,21 @@ static int steelseries_snd_register(struct steelseries_device *sd)
sd->volume_limiter_id = kctl->id;
}
+ if (sd->info->capabilities & SS_CAP_BT_CALL_DUCKING) {
+ struct snd_kcontrol *kctl;
+ struct snd_kcontrol_new ducking_ctl = steelseries_bt_call_ducking_control;
+
+ ducking_ctl.access = SNDRV_CTL_ELEM_ACCESS_READWRITE;
+ if (sd->info->capabilities & SS_CAP_EXTERNAL_CONFIG)
+ ducking_ctl.access |= SNDRV_CTL_ELEM_ACCESS_VOLATILE;
+
+ kctl = snd_ctl_new1(&ducking_ctl, sd);
+ ret = snd_ctl_add(sd->card, kctl);
+ if (ret < 0)
+ goto err_free_card;
+ sd->bt_call_ducking_id = kctl->id;
+ }
+
ret = snd_card_register(sd->card);
if (ret < 0)
goto err_free_card;
@@ -1677,6 +1780,7 @@ static int steelseries_raw_event(struct hid_device *hdev,
u8 old_sidetone;
u8 old_mic_volume;
bool old_volume_limiter;
+ u8 old_bt_call_ducking;
bool is_async_interface = false;
if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1)
@@ -1694,6 +1798,7 @@ static int steelseries_raw_event(struct hid_device *hdev,
old_sidetone = sd->sidetone;
old_mic_volume = sd->mic_volume;
old_volume_limiter = sd->volume_limiter;
+ old_bt_call_ducking = sd->bt_call_ducking;
if (hid_is_usb(hdev)) {
struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
@@ -1763,6 +1868,9 @@ static int steelseries_raw_event(struct hid_device *hdev,
if (sd->volume_limiter != old_volume_limiter)
snd_ctl_notify(sd->card, SNDRV_CTL_EVENT_MASK_VALUE,
&sd->volume_limiter_id);
+ if (sd->bt_call_ducking != old_bt_call_ducking)
+ snd_ctl_notify(sd->card, SNDRV_CTL_EVENT_MASK_VALUE,
+ &sd->bt_call_ducking_id);
}
return 0;
--
2.53.0
^ permalink raw reply related [flat|nested] 29+ messages in thread* Re: [PATCH v3 14/18] HID: steelseries: Add Bluetooth call audio ducking control
2026-02-27 23:50 ` [PATCH v3 14/18] HID: steelseries: Add Bluetooth call audio ducking control Sriman Achanta
@ 2026-03-03 10:59 ` Bastien Nocera
0 siblings, 0 replies; 29+ messages in thread
From: Bastien Nocera @ 2026-03-03 10:59 UTC (permalink / raw)
To: Sriman Achanta, Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer
On Fri, 2026-02-27 at 18:50 -0500, Sriman Achanta wrote:
> Expose Bluetooth call audio ducking behavior as a writable ALSA
> enumerated mixer control ("Bluetooth Call Audio Ducking"), with three
> options: off, lower game audio by 12 dB, or mute game audio entirely.
You probably want to explain what "call ducking" or even "Bluetooth
call ducking" is.
>
> On the Arctis Nova 7 Gen2, this setting is stored alongside inactive
> timeout and Bluetooth auto-enable in a dedicated device configuration
> block. The settings request is expanded to also send a 0x00/0xa0
> device
> query in addition to the existing 0x00/0x20 audio settings query.
>
> Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
> ---
> drivers/hid/hid-steelseries.c | 120
> ++++++++++++++++++++++++++++++++--
> 1 file changed, 114 insertions(+), 6 deletions(-)
>
> diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-
> steelseries.c
> index 47ffec481571..bb9abbb0b6f8 100644
> --- a/drivers/hid/hid-steelseries.c
> +++ b/drivers/hid/hid-steelseries.c
> @@ -30,12 +30,14 @@
> #define SS_CAP_SIDETONE BIT(6)
> #define SS_CAP_MIC_VOLUME BIT(7)
> #define SS_CAP_VOLUME_LIMITER BIT(8)
> +#define SS_CAP_BT_CALL_DUCKING BIT(9)
>
> #define SS_QUIRK_STATUS_SYNC_POLL BIT(0)
>
> #define SS_SETTING_SIDETONE 0
> #define SS_SETTING_MIC_VOLUME 1
> #define SS_SETTING_VOLUME_LIMITER 2
> +#define SS_SETTING_BT_CALL_DUCKING 3
>
> struct steelseries_device;
>
> @@ -80,12 +82,14 @@ struct steelseries_device {
> struct snd_ctl_elem_id sidetone_id;
> struct snd_ctl_elem_id mic_volume_id;
> struct snd_ctl_elem_id volume_limiter_id;
> + struct snd_ctl_elem_id bt_call_ducking_id;
> u8 chatmix_chat;
> u8 chatmix_game;
> bool mic_muted;
> u8 sidetone;
> u8 mic_volume;
> bool volume_limiter;
> + u8 bt_call_ducking;
>
> bool bt_enabled;
> bool bt_device_connected;
> @@ -573,6 +577,9 @@ static int
> steelseries_arctis_nova_7_write_setting(struct hid_device *hdev,
> case SS_SETTING_VOLUME_LIMITER:
> cmd = 0x3a;
> break;
> + case SS_SETTING_BT_CALL_DUCKING:
> + cmd = 0xb3;
> + break;
> default:
> return -EINVAL;
> }
> @@ -883,15 +890,23 @@ static void
> steelseries_arctis_nova_7_gen2_parse_status(struct steelseries_devic
>
> static int steelseries_arctis_nova_7_gen2_request_settings(struct
> hid_device *hdev)
> {
> - const u8 data[] = { 0x00, 0x20 };
> + const u8 audio_data[] = { 0x00, 0x20 };
> + const u8 device_data[] = { 0x00, 0xa0 };
> + int ret;
>
> - return steelseries_send_output_report(hdev, data,
> sizeof(data));
> + ret = steelseries_send_output_report(hdev, audio_data,
> sizeof(audio_data));
> + if (ret)
> + return ret;
> +
> + msleep(10);
> +
> + return steelseries_send_output_report(hdev, device_data,
> sizeof(device_data));
> }
>
> static void steelseries_arctis_nova_7_gen2_parse_settings(
> struct steelseries_device *sd, u8 *data, int size)
> {
> - if (size < 4)
> + if (size < 5)
> return;
>
> switch (data[0]) {
> @@ -900,6 +915,9 @@ static void
> steelseries_arctis_nova_7_gen2_parse_settings(
> sd->sidetone = data[2];
> sd->volume_limiter = data[3];
> break;
> + case 0xa0:
> + sd->bt_call_ducking = data[4];
> + break;
> case 0x37:
> sd->mic_volume = data[1];
> break;
> @@ -909,6 +927,9 @@ static void
> steelseries_arctis_nova_7_gen2_parse_settings(
> case 0x3a:
> sd->volume_limiter = data[1];
> break;
> + case 0xb3:
> + sd->bt_call_ducking = data[1];
> + break;
> }
> }
>
> @@ -1025,7 +1046,8 @@ static const struct steelseries_device_info
> arctis_nova_5x_info = {
> static const struct steelseries_device_info arctis_nova_7_info = {
> .sync_interface = 3,
> .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX |
> SS_CAP_SIDETONE |
> - SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER,
> + SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
> + SS_CAP_BT_CALL_DUCKING,
> .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> .sidetone_max = 3,
> .mic_volume_max = 7,
> @@ -1036,7 +1058,8 @@ static const struct steelseries_device_info
> arctis_nova_7_info = {
>
> static const struct steelseries_device_info arctis_nova_7p_info = {
> .sync_interface = 3,
> - .capabilities = SS_CAP_BATTERY | SS_CAP_MIC_VOLUME |
> SS_CAP_VOLUME_LIMITER,
> + .capabilities = SS_CAP_BATTERY | SS_CAP_MIC_VOLUME |
> SS_CAP_VOLUME_LIMITER |
> + SS_CAP_BT_CALL_DUCKING,
> .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> .mic_volume_max = 7,
> .request_status = steelseries_arctis_nova_request_status,
> @@ -1050,7 +1073,8 @@ static const struct steelseries_device_info
> arctis_nova_7_gen2_info = {
> .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX |
> SS_CAP_MIC_MUTE |
> SS_CAP_BT_ENABLED |
> SS_CAP_BT_DEVICE_CONNECTED |
> SS_CAP_EXTERNAL_CONFIG | SS_CAP_SIDETONE |
> - SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER,
> + SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
> + SS_CAP_BT_CALL_DUCKING,
> .sidetone_max = 3,
> .mic_volume_max = 7,
> .request_status = steelseries_arctis_nova_request_status,
> @@ -1557,6 +1581,70 @@ static const struct snd_kcontrol_new
> steelseries_volume_limiter_control = {
> .put = steelseries_volume_limiter_put,
> };
>
> +static const char *const bt_call_ducking_texts[] = {
> + "Off",
> + "Lower Volume (-12dB)",
> + "Mute Game",
> +};
> +
> +static int steelseries_bt_call_ducking_info(struct snd_kcontrol
> *kcontrol,
> + struct snd_ctl_elem_info
> *uinfo)
> +{
> + return snd_ctl_enum_info(uinfo, 1,
> ARRAY_SIZE(bt_call_ducking_texts),
> + bt_call_ducking_texts);
> +}
> +
> +static int steelseries_bt_call_ducking_get(struct snd_kcontrol
> *kcontrol,
> + struct snd_ctl_elem_value
> *ucontrol)
> +{
> + struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
> + unsigned long flags;
> +
> + spin_lock_irqsave(&sd->lock, flags);
> + ucontrol->value.enumerated.item[0] = sd->bt_call_ducking;
> + spin_unlock_irqrestore(&sd->lock, flags);
> + return 0;
> +}
> +
> +static int steelseries_bt_call_ducking_put(struct snd_kcontrol
> *kcontrol,
> + struct snd_ctl_elem_value
> *ucontrol)
> +{
> + struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
> + unsigned long flags;
> + u8 new_value;
> + int ret;
> +
> + new_value = ucontrol->value.enumerated.item[0];
> + if (new_value >= ARRAY_SIZE(bt_call_ducking_texts))
> + return -EINVAL;
> +
> + spin_lock_irqsave(&sd->lock, flags);
> + if (sd->bt_call_ducking == new_value) {
> + spin_unlock_irqrestore(&sd->lock, flags);
> + return 0;
> + }
> + spin_unlock_irqrestore(&sd->lock, flags);
> +
> + ret = sd->info->write_setting(sd->hdev,
> SS_SETTING_BT_CALL_DUCKING,
> + new_value);
> + if (ret)
> + return ret;
> +
> + spin_lock_irqsave(&sd->lock, flags);
> + sd->bt_call_ducking = new_value;
> + spin_unlock_irqrestore(&sd->lock, flags);
> +
> + return 1;
> +}
> +
> +static const struct snd_kcontrol_new
> steelseries_bt_call_ducking_control = {
> + .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
> + .name = "Bluetooth Call Audio Ducking",
> + .info = steelseries_bt_call_ducking_info,
> + .get = steelseries_bt_call_ducking_get,
> + .put = steelseries_bt_call_ducking_put,
> +};
> +
> static int steelseries_snd_register(struct steelseries_device *sd)
> {
> struct hid_device *hdev = sd->hdev;
> @@ -1644,6 +1732,21 @@ static int steelseries_snd_register(struct
> steelseries_device *sd)
> sd->volume_limiter_id = kctl->id;
> }
>
> + if (sd->info->capabilities & SS_CAP_BT_CALL_DUCKING) {
> + struct snd_kcontrol *kctl;
> + struct snd_kcontrol_new ducking_ctl =
> steelseries_bt_call_ducking_control;
> +
> + ducking_ctl.access =
> SNDRV_CTL_ELEM_ACCESS_READWRITE;
> + if (sd->info->capabilities & SS_CAP_EXTERNAL_CONFIG)
> + ducking_ctl.access |=
> SNDRV_CTL_ELEM_ACCESS_VOLATILE;
> +
> + kctl = snd_ctl_new1(&ducking_ctl, sd);
> + ret = snd_ctl_add(sd->card, kctl);
> + if (ret < 0)
> + goto err_free_card;
> + sd->bt_call_ducking_id = kctl->id;
> + }
> +
> ret = snd_card_register(sd->card);
> if (ret < 0)
> goto err_free_card;
> @@ -1677,6 +1780,7 @@ static int steelseries_raw_event(struct
> hid_device *hdev,
> u8 old_sidetone;
> u8 old_mic_volume;
> bool old_volume_limiter;
> + u8 old_bt_call_ducking;
> bool is_async_interface = false;
>
> if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1)
> @@ -1694,6 +1798,7 @@ static int steelseries_raw_event(struct
> hid_device *hdev,
> old_sidetone = sd->sidetone;
> old_mic_volume = sd->mic_volume;
> old_volume_limiter = sd->volume_limiter;
> + old_bt_call_ducking = sd->bt_call_ducking;
>
> if (hid_is_usb(hdev)) {
> struct usb_interface *intf = to_usb_interface(hdev-
> >dev.parent);
> @@ -1763,6 +1868,9 @@ static int steelseries_raw_event(struct
> hid_device *hdev,
> if (sd->volume_limiter != old_volume_limiter)
> snd_ctl_notify(sd->card,
> SNDRV_CTL_EVENT_MASK_VALUE,
> &sd->volume_limiter_id);
> + if (sd->bt_call_ducking != old_bt_call_ducking)
> + snd_ctl_notify(sd->card,
> SNDRV_CTL_EVENT_MASK_VALUE,
> + &sd->bt_call_ducking_id);
> }
>
> return 0;
^ permalink raw reply [flat|nested] 29+ messages in thread
* [PATCH v3 15/18] HID: steelseries: Add inactive time sysfs attribute
2026-02-27 23:50 [PATCH v3 00/18] HID: steelseries: Add support for Arctis headset lineup Sriman Achanta
` (13 preceding siblings ...)
2026-02-27 23:50 ` [PATCH v3 14/18] HID: steelseries: Add Bluetooth call audio ducking control Sriman Achanta
@ 2026-02-27 23:50 ` Sriman Achanta
2026-03-03 10:59 ` Bastien Nocera
2026-02-27 23:50 ` [PATCH v3 16/18] HID: steelseries: Add Bluetooth auto-enable " Sriman Achanta
` (3 subsequent siblings)
18 siblings, 1 reply; 29+ messages in thread
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
Christian Mayer, Sriman Achanta
Expose the headset auto-shutoff timer as a read/write sysfs attribute
(inactive_time), in minutes. Writing the attribute immediately sends the
new value to the device; reading it returns the last value reported by
the firmware.
The wire encoding differs per family:
- Arctis 1: HID feature report 0x06/0x53 with the value in minutes
- Arctis 7: HID feature report 0x06/0x51; split into its own write
function as the command byte differs from the Arctis 1
- Arctis 9: converts minutes to seconds in a big-endian u16
- Nova 3P: rounds down to the nearest value in a discrete set
{0,1,5,10,15,30,45,60,75,90} before sending command 0xa3
- Nova 5/7: output report with command 0xa3, no rounding required
- Nova Pro: maps minutes to six firmware-defined level indices via
command 0xc1
The inactive_time_max field is added to the device info struct to
enforce the per-device maximum at write time.
Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
drivers/hid/hid-steelseries.c | 183 +++++++++++++++++++++++++++++++---
1 file changed, 167 insertions(+), 16 deletions(-)
diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index bb9abbb0b6f8..f076a0ef8af1 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -31,6 +31,7 @@
#define SS_CAP_MIC_VOLUME BIT(7)
#define SS_CAP_VOLUME_LIMITER BIT(8)
#define SS_CAP_BT_CALL_DUCKING BIT(9)
+#define SS_CAP_INACTIVE_TIME BIT(10)
#define SS_QUIRK_STATUS_SYNC_POLL BIT(0)
@@ -38,6 +39,7 @@
#define SS_SETTING_MIC_VOLUME 1
#define SS_SETTING_VOLUME_LIMITER 2
#define SS_SETTING_BT_CALL_DUCKING 3
+#define SS_SETTING_INACTIVE_TIME 4
struct steelseries_device;
@@ -51,6 +53,7 @@ struct steelseries_device_info {
u8 sidetone_max;
u8 mic_volume_min;
u8 mic_volume_max;
+ u8 inactive_time_max;
int (*request_status)(struct hid_device *hdev);
void (*parse_status)(struct steelseries_device *sd, u8 *data, int size);
@@ -93,6 +96,7 @@ struct steelseries_device {
bool bt_enabled;
bool bt_device_connected;
+ u8 inactive_timeout;
spinlock_t lock;
bool removed;
@@ -476,6 +480,37 @@ static int steelseries_arctis_1_write_setting(struct hid_device *hdev,
return steelseries_send_feature_report(hdev, data,
sizeof(data));
}
+ case SS_SETTING_INACTIVE_TIME: {
+ const u8 data[] = { 0x06, 0x53, value };
+
+ return steelseries_send_feature_report(hdev, data, sizeof(data));
+ }
+ default:
+ return -EINVAL;
+ }
+}
+
+static int steelseries_arctis_7_write_setting(struct hid_device *hdev,
+ u8 setting, u8 value)
+{
+ switch (setting) {
+ case SS_SETTING_SIDETONE:
+ if (value == 0) {
+ const u8 data[] = { 0x06, 0x35 };
+
+ return steelseries_send_feature_report(hdev, data,
+ sizeof(data));
+ } else {
+ const u8 data[] = { 0x06, 0x35, 0x01, 0x00, value };
+
+ return steelseries_send_feature_report(hdev, data,
+ sizeof(data));
+ }
+ case SS_SETTING_INACTIVE_TIME: {
+ const u8 data[] = { 0x06, 0x51, value };
+
+ return steelseries_send_feature_report(hdev, data, sizeof(data));
+ }
default:
return -EINVAL;
}
@@ -490,11 +525,30 @@ static int steelseries_arctis_9_write_setting(struct hid_device *hdev,
return steelseries_send_feature_report(hdev, data, sizeof(data));
}
+ case SS_SETTING_INACTIVE_TIME: {
+ u16 seconds = (u16)value * 60;
+ const u8 data[] = { 0x04, 0x00, seconds >> 8, seconds & 0xff };
+
+ return steelseries_send_feature_report(hdev, data, sizeof(data));
+ }
default:
return -EINVAL;
}
}
+static u8 steelseries_arctis_nova_3p_round_inactive_time(u8 minutes)
+{
+ static const u8 supported[] = { 0, 1, 5, 10, 15, 30, 45, 60, 75, 90 };
+ int i;
+
+ for (i = ARRAY_SIZE(supported) - 1; i > 0; i--) {
+ if (minutes >= supported[i])
+ return supported[i];
+ }
+
+ return 0;
+}
+
static int steelseries_arctis_nova_3p_write_setting(struct hid_device *hdev,
u8 setting, u8 value)
{
@@ -510,6 +564,10 @@ static int steelseries_arctis_nova_3p_write_setting(struct hid_device *hdev,
case SS_SETTING_MIC_VOLUME:
cmd = 0x37;
break;
+ case SS_SETTING_INACTIVE_TIME:
+ cmd = 0xa3;
+ value = steelseries_arctis_nova_3p_round_inactive_time(value);
+ break;
default:
return -EINVAL;
}
@@ -542,6 +600,9 @@ static int steelseries_arctis_nova_5_write_setting(struct hid_device *hdev,
case SS_SETTING_VOLUME_LIMITER:
cmd = 0x27;
break;
+ case SS_SETTING_INACTIVE_TIME:
+ cmd = 0xa3;
+ break;
default:
return -EINVAL;
}
@@ -580,6 +641,9 @@ static int steelseries_arctis_nova_7_write_setting(struct hid_device *hdev,
case SS_SETTING_BT_CALL_DUCKING:
cmd = 0xb3;
break;
+ case SS_SETTING_INACTIVE_TIME:
+ cmd = 0xa3;
+ break;
default:
return -EINVAL;
}
@@ -612,6 +676,24 @@ static int steelseries_arctis_nova_pro_write_setting(struct hid_device *hdev,
case SS_SETTING_MIC_VOLUME:
cmd = 0x37;
break;
+ case SS_SETTING_INACTIVE_TIME:
+ cmd = 0xc1;
+ /* Map minutes to firmware level */
+ if (value >= 45)
+ value = 6; /* 60 min */
+ else if (value >= 23)
+ value = 5; /* 30 min */
+ else if (value >= 13)
+ value = 4; /* 15 min */
+ else if (value >= 8)
+ value = 3; /* 10 min */
+ else if (value >= 3)
+ value = 2; /* 5 min */
+ else if (value > 0)
+ value = 1; /* 1 min */
+ else
+ value = 0; /* disabled */
+ break;
default:
return -EINVAL;
}
@@ -916,6 +998,7 @@ static void steelseries_arctis_nova_7_gen2_parse_settings(
sd->volume_limiter = data[3];
break;
case 0xa0:
+ sd->inactive_timeout = data[1];
sd->bt_call_ducking = data[4];
break;
case 0x37:
@@ -927,6 +1010,9 @@ static void steelseries_arctis_nova_7_gen2_parse_settings(
case 0x3a:
sd->volume_limiter = data[1];
break;
+ case 0xa3:
+ sd->inactive_timeout = data[1];
+ break;
case 0xb3:
sd->bt_call_ducking = data[1];
break;
@@ -936,11 +1022,13 @@ static void steelseries_arctis_nova_7_gen2_parse_settings(
static void steelseries_arctis_nova_pro_parse_settings(
struct steelseries_device *sd, u8 *data, int size)
{
- if (size < 10)
+ if (size < 13)
return;
- if (data[0] == 0x06 && data[1] == 0xb0)
+ if (data[0] == 0x06 && data[1] == 0xb0) {
sd->mic_volume = data[9];
+ sd->inactive_timeout = data[12];
+ }
}
static void steelseries_arctis_nova_pro_parse_status(struct steelseries_device *sd,
@@ -970,9 +1058,10 @@ static const struct steelseries_device_info srws1_info = { };
static const struct steelseries_device_info arctis_1_info = {
.sync_interface = 3,
- .capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE | SS_CAP_INACTIVE_TIME,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 18,
+ .inactive_time_max = 90,
.request_status = steelseries_arctis_1_request_status,
.parse_status = steelseries_arctis_1_parse_status,
.write_setting = steelseries_arctis_1_write_setting,
@@ -980,19 +1069,23 @@ static const struct steelseries_device_info arctis_1_info = {
static const struct steelseries_device_info arctis_7_info = {
.sync_interface = 5,
- .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
+ SS_CAP_INACTIVE_TIME,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 18,
+ .inactive_time_max = 90,
.request_status = steelseries_arctis_7_request_status,
.parse_status = steelseries_arctis_7_parse_status,
- .write_setting = steelseries_arctis_1_write_setting,
+ .write_setting = steelseries_arctis_7_write_setting,
};
static const struct steelseries_device_info arctis_7_plus_info = {
.sync_interface = 3,
- .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
+ SS_CAP_INACTIVE_TIME,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 3,
+ .inactive_time_max = 90,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_7_plus_parse_status,
.write_setting = steelseries_arctis_nova_5_write_setting,
@@ -1000,9 +1093,11 @@ static const struct steelseries_device_info arctis_7_plus_info = {
static const struct steelseries_device_info arctis_9_info = {
.sync_interface = 0,
- .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
+ SS_CAP_INACTIVE_TIME,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 61,
+ .inactive_time_max = 255,
.request_status = steelseries_arctis_9_request_status,
.parse_status = steelseries_arctis_9_parse_status,
.write_setting = steelseries_arctis_9_write_setting,
@@ -1010,10 +1105,12 @@ static const struct steelseries_device_info arctis_9_info = {
static const struct steelseries_device_info arctis_nova_3p_info = {
.sync_interface = 4,
- .capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME,
+ .capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME |
+ SS_CAP_INACTIVE_TIME,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 10,
.mic_volume_max = 14,
+ .inactive_time_max = 90,
.request_status = steelseries_arctis_nova_3p_request_status,
.parse_status = steelseries_arctis_nova_3p_parse_status,
.write_setting = steelseries_arctis_nova_3p_write_setting,
@@ -1022,10 +1119,11 @@ static const struct steelseries_device_info arctis_nova_3p_info = {
static const struct steelseries_device_info arctis_nova_5_info = {
.sync_interface = 3,
.capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME |
- SS_CAP_VOLUME_LIMITER,
+ SS_CAP_VOLUME_LIMITER | SS_CAP_INACTIVE_TIME,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 10,
.mic_volume_max = 15,
+ .inactive_time_max = 255,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_nova_5_parse_status,
.write_setting = steelseries_arctis_nova_5_write_setting,
@@ -1034,10 +1132,12 @@ static const struct steelseries_device_info arctis_nova_5_info = {
static const struct steelseries_device_info arctis_nova_5x_info = {
.sync_interface = 3,
.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
- SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER,
+ SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
+ SS_CAP_INACTIVE_TIME,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 10,
.mic_volume_max = 15,
+ .inactive_time_max = 255,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_nova_5x_parse_status,
.write_setting = steelseries_arctis_nova_5_write_setting,
@@ -1047,10 +1147,11 @@ static const struct steelseries_device_info arctis_nova_7_info = {
.sync_interface = 3,
.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
- SS_CAP_BT_CALL_DUCKING,
+ SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 3,
.mic_volume_max = 7,
+ .inactive_time_max = 255,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_nova_7_parse_status,
.write_setting = steelseries_arctis_nova_7_write_setting,
@@ -1059,9 +1160,10 @@ static const struct steelseries_device_info arctis_nova_7_info = {
static const struct steelseries_device_info arctis_nova_7p_info = {
.sync_interface = 3,
.capabilities = SS_CAP_BATTERY | SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
- SS_CAP_BT_CALL_DUCKING,
+ SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.mic_volume_max = 7,
+ .inactive_time_max = 255,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_nova_7_parse_status,
.write_setting = steelseries_arctis_nova_7_write_setting,
@@ -1074,9 +1176,10 @@ static const struct steelseries_device_info arctis_nova_7_gen2_info = {
SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
SS_CAP_EXTERNAL_CONFIG | SS_CAP_SIDETONE |
SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
- SS_CAP_BT_CALL_DUCKING,
+ SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME,
.sidetone_max = 3,
.mic_volume_max = 7,
+ .inactive_time_max = 255,
.request_status = steelseries_arctis_nova_request_status,
.parse_status = steelseries_arctis_nova_7_gen2_parse_status,
.request_settings = steelseries_arctis_nova_7_gen2_request_settings,
@@ -1088,11 +1191,12 @@ static const struct steelseries_device_info arctis_nova_pro_info = {
.sync_interface = 4,
.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_MIC_MUTE |
SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
- SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME,
+ SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME | SS_CAP_INACTIVE_TIME,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 3,
.mic_volume_min = 1,
.mic_volume_max = 10,
+ .inactive_time_max = 60,
.request_status = steelseries_arctis_nova_pro_request_status,
.parse_status = steelseries_arctis_nova_pro_parse_status,
.parse_settings = steelseries_arctis_nova_pro_parse_settings,
@@ -1271,12 +1375,55 @@ static ssize_t bt_device_connected_show(struct device *dev,
return sysfs_emit(buf, "%d\n", sd->bt_device_connected);
}
+static ssize_t inactive_time_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);
+
+ if (!sd->headset_connected)
+ return -ENODEV;
+
+ return sysfs_emit(buf, "%d\n", sd->inactive_timeout);
+}
+
+static ssize_t inactive_time_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct steelseries_device *sd = hid_get_drvdata(hdev);
+ unsigned int value;
+ int ret;
+
+ if (!sd->headset_connected)
+ return -ENODEV;
+
+ ret = kstrtouint(buf, 10, &value);
+ if (ret)
+ return ret;
+
+ if (value > sd->info->inactive_time_max)
+ return -EINVAL;
+
+ ret = sd->info->write_setting(sd->hdev, SS_SETTING_INACTIVE_TIME,
+ value);
+ if (ret)
+ return ret;
+
+ sd->inactive_timeout = value;
+
+ return count;
+}
+
static DEVICE_ATTR_RO(bt_enabled);
static DEVICE_ATTR_RO(bt_device_connected);
+static DEVICE_ATTR_RW(inactive_time);
static struct attribute *steelseries_headset_attrs[] = {
&dev_attr_bt_enabled.attr,
&dev_attr_bt_device_connected.attr,
+ &dev_attr_inactive_time.attr,
NULL,
};
@@ -1298,6 +1445,8 @@ static umode_t steelseries_headset_attr_is_visible(struct kobject *kobj,
return (caps & SS_CAP_BT_ENABLED) ? attr->mode : 0;
if (attr == &dev_attr_bt_device_connected.attr)
return (caps & SS_CAP_BT_DEVICE_CONNECTED) ? attr->mode : 0;
+ if (attr == &dev_attr_inactive_time.attr)
+ return (caps & SS_CAP_INACTIVE_TIME) ? attr->mode : 0;
return 0;
}
@@ -1956,7 +2105,8 @@ static int steelseries_probe(struct hid_device *hdev,
hid_warn(hdev, "Failed to register battery: %d\n", ret);
}
- if (info->capabilities & (SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED)) {
+ if (info->capabilities & (SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
+ SS_CAP_INACTIVE_TIME)) {
ret = sysfs_create_group(&hdev->dev.kobj,
&steelseries_headset_attr_group);
if (ret)
@@ -2038,7 +2188,8 @@ static void steelseries_remove(struct hid_device *hdev)
}
if (interface_num == sd->info->sync_interface) {
- if (sd->info->capabilities & (SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED))
+ if (sd->info->capabilities & (SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
+ SS_CAP_INACTIVE_TIME))
sysfs_remove_group(&hdev->dev.kobj,
&steelseries_headset_attr_group);
--
2.53.0
^ permalink raw reply related [flat|nested] 29+ messages in thread* Re: [PATCH v3 15/18] HID: steelseries: Add inactive time sysfs attribute
2026-02-27 23:50 ` [PATCH v3 15/18] HID: steelseries: Add inactive time sysfs attribute Sriman Achanta
@ 2026-03-03 10:59 ` Bastien Nocera
0 siblings, 0 replies; 29+ messages in thread
From: Bastien Nocera @ 2026-03-03 10:59 UTC (permalink / raw)
To: Sriman Achanta, Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer
On Fri, 2026-02-27 at 18:50 -0500, Sriman Achanta wrote:
> Expose the headset auto-shutoff timer as a read/write sysfs attribute
> (inactive_time), in minutes. Writing the attribute immediately sends
> the
> new value to the device; reading it returns the last value reported
> by
> the firmware.
Question to you and the HID maintainers:
Is there no existing sysfs definition that could match this description
and could be reused instead of needing a new one?
>
> The wire encoding differs per family:
> - Arctis 1: HID feature report 0x06/0x53 with the value in minutes
> - Arctis 7: HID feature report 0x06/0x51; split into its own write
> function as the command byte differs from the Arctis 1
> - Arctis 9: converts minutes to seconds in a big-endian u16
> - Nova 3P: rounds down to the nearest value in a discrete set
> {0,1,5,10,15,30,45,60,75,90} before sending command 0xa3
> - Nova 5/7: output report with command 0xa3, no rounding required
> - Nova Pro: maps minutes to six firmware-defined level indices via
> command 0xc1
>
> The inactive_time_max field is added to the device info struct to
> enforce the per-device maximum at write time.
>
> Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
> ---
> drivers/hid/hid-steelseries.c | 183 +++++++++++++++++++++++++++++++-
> --
> 1 file changed, 167 insertions(+), 16 deletions(-)
>
> diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-
> steelseries.c
> index bb9abbb0b6f8..f076a0ef8af1 100644
> --- a/drivers/hid/hid-steelseries.c
> +++ b/drivers/hid/hid-steelseries.c
> @@ -31,6 +31,7 @@
> #define SS_CAP_MIC_VOLUME BIT(7)
> #define SS_CAP_VOLUME_LIMITER BIT(8)
> #define SS_CAP_BT_CALL_DUCKING BIT(9)
> +#define SS_CAP_INACTIVE_TIME BIT(10)
>
> #define SS_QUIRK_STATUS_SYNC_POLL BIT(0)
>
> @@ -38,6 +39,7 @@
> #define SS_SETTING_MIC_VOLUME 1
> #define SS_SETTING_VOLUME_LIMITER 2
> #define SS_SETTING_BT_CALL_DUCKING 3
> +#define SS_SETTING_INACTIVE_TIME 4
>
> struct steelseries_device;
>
> @@ -51,6 +53,7 @@ struct steelseries_device_info {
> u8 sidetone_max;
> u8 mic_volume_min;
> u8 mic_volume_max;
> + u8 inactive_time_max;
>
> int (*request_status)(struct hid_device *hdev);
> void (*parse_status)(struct steelseries_device *sd, u8
> *data, int size);
> @@ -93,6 +96,7 @@ struct steelseries_device {
>
> bool bt_enabled;
> bool bt_device_connected;
> + u8 inactive_timeout;
>
> spinlock_t lock;
> bool removed;
> @@ -476,6 +480,37 @@ static int
> steelseries_arctis_1_write_setting(struct hid_device *hdev,
> return steelseries_send_feature_report(hdev,
> data,
>
> sizeof(data));
> }
> + case SS_SETTING_INACTIVE_TIME: {
> + const u8 data[] = { 0x06, 0x53, value };
> +
> + return steelseries_send_feature_report(hdev, data,
> sizeof(data));
> + }
> + default:
> + return -EINVAL;
> + }
> +}
> +
> +static int steelseries_arctis_7_write_setting(struct hid_device
> *hdev,
> + u8 setting, u8 value)
> +{
> + switch (setting) {
> + case SS_SETTING_SIDETONE:
> + if (value == 0) {
> + const u8 data[] = { 0x06, 0x35 };
> +
> + return steelseries_send_feature_report(hdev,
> data,
> +
> sizeof(data));
> + } else {
> + const u8 data[] = { 0x06, 0x35, 0x01, 0x00,
> value };
> +
> + return steelseries_send_feature_report(hdev,
> data,
> +
> sizeof(data));
> + }
> + case SS_SETTING_INACTIVE_TIME: {
> + const u8 data[] = { 0x06, 0x51, value };
> +
> + return steelseries_send_feature_report(hdev, data,
> sizeof(data));
> + }
> default:
> return -EINVAL;
> }
> @@ -490,11 +525,30 @@ static int
> steelseries_arctis_9_write_setting(struct hid_device *hdev,
>
> return steelseries_send_feature_report(hdev, data,
> sizeof(data));
> }
> + case SS_SETTING_INACTIVE_TIME: {
> + u16 seconds = (u16)value * 60;
> + const u8 data[] = { 0x04, 0x00, seconds >> 8,
> seconds & 0xff };
> +
> + return steelseries_send_feature_report(hdev, data,
> sizeof(data));
> + }
> default:
> return -EINVAL;
> }
> }
>
> +static u8 steelseries_arctis_nova_3p_round_inactive_time(u8 minutes)
> +{
> + static const u8 supported[] = { 0, 1, 5, 10, 15, 30, 45, 60,
> 75, 90 };
> + int i;
> +
> + for (i = ARRAY_SIZE(supported) - 1; i > 0; i--) {
> + if (minutes >= supported[i])
> + return supported[i];
> + }
> +
> + return 0;
> +}
> +
> static int steelseries_arctis_nova_3p_write_setting(struct
> hid_device *hdev,
> u8 setting, u8
> value)
> {
> @@ -510,6 +564,10 @@ static int
> steelseries_arctis_nova_3p_write_setting(struct hid_device *hdev,
> case SS_SETTING_MIC_VOLUME:
> cmd = 0x37;
> break;
> + case SS_SETTING_INACTIVE_TIME:
> + cmd = 0xa3;
> + value =
> steelseries_arctis_nova_3p_round_inactive_time(value);
> + break;
> default:
> return -EINVAL;
> }
> @@ -542,6 +600,9 @@ static int
> steelseries_arctis_nova_5_write_setting(struct hid_device *hdev,
> case SS_SETTING_VOLUME_LIMITER:
> cmd = 0x27;
> break;
> + case SS_SETTING_INACTIVE_TIME:
> + cmd = 0xa3;
> + break;
> default:
> return -EINVAL;
> }
> @@ -580,6 +641,9 @@ static int
> steelseries_arctis_nova_7_write_setting(struct hid_device *hdev,
> case SS_SETTING_BT_CALL_DUCKING:
> cmd = 0xb3;
> break;
> + case SS_SETTING_INACTIVE_TIME:
> + cmd = 0xa3;
> + break;
> default:
> return -EINVAL;
> }
> @@ -612,6 +676,24 @@ static int
> steelseries_arctis_nova_pro_write_setting(struct hid_device *hdev,
> case SS_SETTING_MIC_VOLUME:
> cmd = 0x37;
> break;
> + case SS_SETTING_INACTIVE_TIME:
> + cmd = 0xc1;
> + /* Map minutes to firmware level */
> + if (value >= 45)
> + value = 6; /* 60 min */
> + else if (value >= 23)
> + value = 5; /* 30 min */
> + else if (value >= 13)
> + value = 4; /* 15 min */
> + else if (value >= 8)
> + value = 3; /* 10 min */
> + else if (value >= 3)
> + value = 2; /* 5 min */
> + else if (value > 0)
> + value = 1; /* 1 min */
> + else
> + value = 0; /* disabled */
> + break;
> default:
> return -EINVAL;
> }
> @@ -916,6 +998,7 @@ static void
> steelseries_arctis_nova_7_gen2_parse_settings(
> sd->volume_limiter = data[3];
> break;
> case 0xa0:
> + sd->inactive_timeout = data[1];
> sd->bt_call_ducking = data[4];
> break;
> case 0x37:
> @@ -927,6 +1010,9 @@ static void
> steelseries_arctis_nova_7_gen2_parse_settings(
> case 0x3a:
> sd->volume_limiter = data[1];
> break;
> + case 0xa3:
> + sd->inactive_timeout = data[1];
> + break;
> case 0xb3:
> sd->bt_call_ducking = data[1];
> break;
> @@ -936,11 +1022,13 @@ static void
> steelseries_arctis_nova_7_gen2_parse_settings(
> static void steelseries_arctis_nova_pro_parse_settings(
> struct steelseries_device *sd, u8 *data, int size)
> {
> - if (size < 10)
> + if (size < 13)
> return;
>
> - if (data[0] == 0x06 && data[1] == 0xb0)
> + if (data[0] == 0x06 && data[1] == 0xb0) {
> sd->mic_volume = data[9];
> + sd->inactive_timeout = data[12];
> + }
> }
>
> static void steelseries_arctis_nova_pro_parse_status(struct
> steelseries_device *sd,
> @@ -970,9 +1058,10 @@ static const struct steelseries_device_info
> srws1_info = { };
>
> static const struct steelseries_device_info arctis_1_info = {
> .sync_interface = 3,
> - .capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE,
> + .capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE |
> SS_CAP_INACTIVE_TIME,
> .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> .sidetone_max = 18,
> + .inactive_time_max = 90,
> .request_status = steelseries_arctis_1_request_status,
> .parse_status = steelseries_arctis_1_parse_status,
> .write_setting = steelseries_arctis_1_write_setting,
> @@ -980,19 +1069,23 @@ static const struct steelseries_device_info
> arctis_1_info = {
>
> static const struct steelseries_device_info arctis_7_info = {
> .sync_interface = 5,
> - .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX |
> SS_CAP_SIDETONE,
> + .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX |
> SS_CAP_SIDETONE |
> + SS_CAP_INACTIVE_TIME,
> .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> .sidetone_max = 18,
> + .inactive_time_max = 90,
> .request_status = steelseries_arctis_7_request_status,
> .parse_status = steelseries_arctis_7_parse_status,
> - .write_setting = steelseries_arctis_1_write_setting,
> + .write_setting = steelseries_arctis_7_write_setting,
> };
>
> static const struct steelseries_device_info arctis_7_plus_info = {
> .sync_interface = 3,
> - .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX |
> SS_CAP_SIDETONE,
> + .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX |
> SS_CAP_SIDETONE |
> + SS_CAP_INACTIVE_TIME,
> .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> .sidetone_max = 3,
> + .inactive_time_max = 90,
> .request_status = steelseries_arctis_nova_request_status,
> .parse_status = steelseries_arctis_7_plus_parse_status,
> .write_setting = steelseries_arctis_nova_5_write_setting,
> @@ -1000,9 +1093,11 @@ static const struct steelseries_device_info
> arctis_7_plus_info = {
>
> static const struct steelseries_device_info arctis_9_info = {
> .sync_interface = 0,
> - .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX |
> SS_CAP_SIDETONE,
> + .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX |
> SS_CAP_SIDETONE |
> + SS_CAP_INACTIVE_TIME,
> .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> .sidetone_max = 61,
> + .inactive_time_max = 255,
> .request_status = steelseries_arctis_9_request_status,
> .parse_status = steelseries_arctis_9_parse_status,
> .write_setting = steelseries_arctis_9_write_setting,
> @@ -1010,10 +1105,12 @@ static const struct steelseries_device_info
> arctis_9_info = {
>
> static const struct steelseries_device_info arctis_nova_3p_info = {
> .sync_interface = 4,
> - .capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE |
> SS_CAP_MIC_VOLUME,
> + .capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE |
> SS_CAP_MIC_VOLUME |
> + SS_CAP_INACTIVE_TIME,
> .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> .sidetone_max = 10,
> .mic_volume_max = 14,
> + .inactive_time_max = 90,
> .request_status = steelseries_arctis_nova_3p_request_status,
> .parse_status = steelseries_arctis_nova_3p_parse_status,
> .write_setting = steelseries_arctis_nova_3p_write_setting,
> @@ -1022,10 +1119,11 @@ static const struct steelseries_device_info
> arctis_nova_3p_info = {
> static const struct steelseries_device_info arctis_nova_5_info = {
> .sync_interface = 3,
> .capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE |
> SS_CAP_MIC_VOLUME |
> - SS_CAP_VOLUME_LIMITER,
> + SS_CAP_VOLUME_LIMITER |
> SS_CAP_INACTIVE_TIME,
> .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> .sidetone_max = 10,
> .mic_volume_max = 15,
> + .inactive_time_max = 255,
> .request_status = steelseries_arctis_nova_request_status,
> .parse_status = steelseries_arctis_nova_5_parse_status,
> .write_setting = steelseries_arctis_nova_5_write_setting,
> @@ -1034,10 +1132,12 @@ static const struct steelseries_device_info
> arctis_nova_5_info = {
> static const struct steelseries_device_info arctis_nova_5x_info = {
> .sync_interface = 3,
> .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX |
> SS_CAP_SIDETONE |
> - SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER,
> + SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
> + SS_CAP_INACTIVE_TIME,
> .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> .sidetone_max = 10,
> .mic_volume_max = 15,
> + .inactive_time_max = 255,
> .request_status = steelseries_arctis_nova_request_status,
> .parse_status = steelseries_arctis_nova_5x_parse_status,
> .write_setting = steelseries_arctis_nova_5_write_setting,
> @@ -1047,10 +1147,11 @@ static const struct steelseries_device_info
> arctis_nova_7_info = {
> .sync_interface = 3,
> .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX |
> SS_CAP_SIDETONE |
> SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
> - SS_CAP_BT_CALL_DUCKING,
> + SS_CAP_BT_CALL_DUCKING |
> SS_CAP_INACTIVE_TIME,
> .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> .sidetone_max = 3,
> .mic_volume_max = 7,
> + .inactive_time_max = 255,
> .request_status = steelseries_arctis_nova_request_status,
> .parse_status = steelseries_arctis_nova_7_parse_status,
> .write_setting = steelseries_arctis_nova_7_write_setting,
> @@ -1059,9 +1160,10 @@ static const struct steelseries_device_info
> arctis_nova_7_info = {
> static const struct steelseries_device_info arctis_nova_7p_info = {
> .sync_interface = 3,
> .capabilities = SS_CAP_BATTERY | SS_CAP_MIC_VOLUME |
> SS_CAP_VOLUME_LIMITER |
> - SS_CAP_BT_CALL_DUCKING,
> + SS_CAP_BT_CALL_DUCKING |
> SS_CAP_INACTIVE_TIME,
> .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> .mic_volume_max = 7,
> + .inactive_time_max = 255,
> .request_status = steelseries_arctis_nova_request_status,
> .parse_status = steelseries_arctis_nova_7_parse_status,
> .write_setting = steelseries_arctis_nova_7_write_setting,
> @@ -1074,9 +1176,10 @@ static const struct steelseries_device_info
> arctis_nova_7_gen2_info = {
> SS_CAP_BT_ENABLED |
> SS_CAP_BT_DEVICE_CONNECTED |
> SS_CAP_EXTERNAL_CONFIG | SS_CAP_SIDETONE |
> SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
> - SS_CAP_BT_CALL_DUCKING,
> + SS_CAP_BT_CALL_DUCKING |
> SS_CAP_INACTIVE_TIME,
> .sidetone_max = 3,
> .mic_volume_max = 7,
> + .inactive_time_max = 255,
> .request_status = steelseries_arctis_nova_request_status,
> .parse_status = steelseries_arctis_nova_7_gen2_parse_status,
> .request_settings =
> steelseries_arctis_nova_7_gen2_request_settings,
> @@ -1088,11 +1191,12 @@ static const struct steelseries_device_info
> arctis_nova_pro_info = {
> .sync_interface = 4,
> .capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX |
> SS_CAP_MIC_MUTE |
> SS_CAP_BT_ENABLED |
> SS_CAP_BT_DEVICE_CONNECTED |
> - SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME,
> + SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME |
> SS_CAP_INACTIVE_TIME,
> .quirks = SS_QUIRK_STATUS_SYNC_POLL,
> .sidetone_max = 3,
> .mic_volume_min = 1,
> .mic_volume_max = 10,
> + .inactive_time_max = 60,
> .request_status =
> steelseries_arctis_nova_pro_request_status,
> .parse_status = steelseries_arctis_nova_pro_parse_status,
> .parse_settings =
> steelseries_arctis_nova_pro_parse_settings,
> @@ -1271,12 +1375,55 @@ static ssize_t
> bt_device_connected_show(struct device *dev,
> return sysfs_emit(buf, "%d\n", sd->bt_device_connected);
> }
>
> +static ssize_t inactive_time_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);
> +
> + if (!sd->headset_connected)
> + return -ENODEV;
> +
> + return sysfs_emit(buf, "%d\n", sd->inactive_timeout);
> +}
> +
> +static ssize_t inactive_time_store(struct device *dev,
> + struct device_attribute *attr,
> + const char *buf, size_t count)
> +{
> + struct hid_device *hdev = to_hid_device(dev);
> + struct steelseries_device *sd = hid_get_drvdata(hdev);
> + unsigned int value;
> + int ret;
> +
> + if (!sd->headset_connected)
> + return -ENODEV;
> +
> + ret = kstrtouint(buf, 10, &value);
> + if (ret)
> + return ret;
> +
> + if (value > sd->info->inactive_time_max)
> + return -EINVAL;
> +
> + ret = sd->info->write_setting(sd->hdev,
> SS_SETTING_INACTIVE_TIME,
> + value);
> + if (ret)
> + return ret;
> +
> + sd->inactive_timeout = value;
> +
> + return count;
> +}
> +
> static DEVICE_ATTR_RO(bt_enabled);
> static DEVICE_ATTR_RO(bt_device_connected);
> +static DEVICE_ATTR_RW(inactive_time);
>
> static struct attribute *steelseries_headset_attrs[] = {
> &dev_attr_bt_enabled.attr,
> &dev_attr_bt_device_connected.attr,
> + &dev_attr_inactive_time.attr,
> NULL,
> };
>
> @@ -1298,6 +1445,8 @@ static umode_t
> steelseries_headset_attr_is_visible(struct kobject *kobj,
> return (caps & SS_CAP_BT_ENABLED) ? attr->mode : 0;
> if (attr == &dev_attr_bt_device_connected.attr)
> return (caps & SS_CAP_BT_DEVICE_CONNECTED) ? attr-
> >mode : 0;
> + if (attr == &dev_attr_inactive_time.attr)
> + return (caps & SS_CAP_INACTIVE_TIME) ? attr->mode :
> 0;
>
> return 0;
> }
> @@ -1956,7 +2105,8 @@ static int steelseries_probe(struct hid_device
> *hdev,
> hid_warn(hdev, "Failed to register
> battery: %d\n", ret);
> }
>
> - if (info->capabilities & (SS_CAP_BT_ENABLED |
> SS_CAP_BT_DEVICE_CONNECTED)) {
> + if (info->capabilities & (SS_CAP_BT_ENABLED |
> SS_CAP_BT_DEVICE_CONNECTED |
> + SS_CAP_INACTIVE_TIME)) {
> ret = sysfs_create_group(&hdev->dev.kobj,
>
> &steelseries_headset_attr_group);
> if (ret)
> @@ -2038,7 +2188,8 @@ static void steelseries_remove(struct
> hid_device *hdev)
> }
>
> if (interface_num == sd->info->sync_interface) {
> - if (sd->info->capabilities & (SS_CAP_BT_ENABLED |
> SS_CAP_BT_DEVICE_CONNECTED))
> + if (sd->info->capabilities & (SS_CAP_BT_ENABLED |
> SS_CAP_BT_DEVICE_CONNECTED |
> + SS_CAP_INACTIVE_TIME))
> sysfs_remove_group(&hdev->dev.kobj,
>
> &steelseries_headset_attr_group);
>
^ permalink raw reply [flat|nested] 29+ messages in thread
* [PATCH v3 16/18] HID: steelseries: Add Bluetooth auto-enable sysfs attribute
2026-02-27 23:50 [PATCH v3 00/18] HID: steelseries: Add support for Arctis headset lineup Sriman Achanta
` (14 preceding siblings ...)
2026-02-27 23:50 ` [PATCH v3 15/18] HID: steelseries: Add inactive time sysfs attribute Sriman Achanta
@ 2026-02-27 23:50 ` Sriman Achanta
2026-02-27 23:50 ` [PATCH v3 17/18] HID: steelseries: Add mic mute LED brightness control Sriman Achanta
` (2 subsequent siblings)
18 siblings, 0 replies; 29+ messages in thread
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
Christian Mayer, Sriman Achanta
Expose the Bluetooth auto-enable setting as a read/write sysfs boolean
attribute (bt_auto_enable). When enabled, the headset activates its
Bluetooth radio automatically on power-on. Currently supported on the
Arctis Nova 7, Nova 7P, and Nova 7 Gen2 via write command 0xb2.
On the Nova 7 Gen2, the current value is recovered from the 0xa0 device
settings response alongside inactive timeout and call audio ducking.
Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
drivers/hid/hid-steelseries.c | 65 ++++++++++++++++++++++++++++++++---
1 file changed, 60 insertions(+), 5 deletions(-)
diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index f076a0ef8af1..a794af01e15a 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -32,6 +32,7 @@
#define SS_CAP_VOLUME_LIMITER BIT(8)
#define SS_CAP_BT_CALL_DUCKING BIT(9)
#define SS_CAP_INACTIVE_TIME BIT(10)
+#define SS_CAP_BT_AUTO_ENABLE BIT(11)
#define SS_QUIRK_STATUS_SYNC_POLL BIT(0)
@@ -40,6 +41,7 @@
#define SS_SETTING_VOLUME_LIMITER 2
#define SS_SETTING_BT_CALL_DUCKING 3
#define SS_SETTING_INACTIVE_TIME 4
+#define SS_SETTING_BT_AUTO_ENABLE 5
struct steelseries_device;
@@ -97,6 +99,7 @@ struct steelseries_device {
bool bt_enabled;
bool bt_device_connected;
u8 inactive_timeout;
+ bool bt_auto_enable;
spinlock_t lock;
bool removed;
@@ -644,6 +647,9 @@ static int steelseries_arctis_nova_7_write_setting(struct hid_device *hdev,
case SS_SETTING_INACTIVE_TIME:
cmd = 0xa3;
break;
+ case SS_SETTING_BT_AUTO_ENABLE:
+ cmd = 0xb2;
+ break;
default:
return -EINVAL;
}
@@ -999,6 +1005,7 @@ static void steelseries_arctis_nova_7_gen2_parse_settings(
break;
case 0xa0:
sd->inactive_timeout = data[1];
+ sd->bt_auto_enable = data[3];
sd->bt_call_ducking = data[4];
break;
case 0x37:
@@ -1013,6 +1020,9 @@ static void steelseries_arctis_nova_7_gen2_parse_settings(
case 0xa3:
sd->inactive_timeout = data[1];
break;
+ case 0xb2:
+ sd->bt_auto_enable = data[1];
+ break;
case 0xb3:
sd->bt_call_ducking = data[1];
break;
@@ -1147,7 +1157,8 @@ static const struct steelseries_device_info arctis_nova_7_info = {
.sync_interface = 3,
.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
- SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME,
+ SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME |
+ SS_CAP_BT_AUTO_ENABLE,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 3,
.mic_volume_max = 7,
@@ -1160,7 +1171,8 @@ static const struct steelseries_device_info arctis_nova_7_info = {
static const struct steelseries_device_info arctis_nova_7p_info = {
.sync_interface = 3,
.capabilities = SS_CAP_BATTERY | SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
- SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME,
+ SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME |
+ SS_CAP_BT_AUTO_ENABLE,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.mic_volume_max = 7,
.inactive_time_max = 255,
@@ -1176,7 +1188,8 @@ static const struct steelseries_device_info arctis_nova_7_gen2_info = {
SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
SS_CAP_EXTERNAL_CONFIG | SS_CAP_SIDETONE |
SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
- SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME,
+ SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME |
+ SS_CAP_BT_AUTO_ENABLE,
.sidetone_max = 3,
.mic_volume_max = 7,
.inactive_time_max = 255,
@@ -1416,14 +1429,53 @@ static ssize_t inactive_time_store(struct device *dev,
return count;
}
+static ssize_t bt_auto_enable_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);
+
+ if (!sd->headset_connected)
+ return -ENODEV;
+
+ return sysfs_emit(buf, "%d\n", sd->bt_auto_enable);
+}
+
+static ssize_t bt_auto_enable_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct steelseries_device *sd = hid_get_drvdata(hdev);
+ bool value;
+ int ret;
+
+ if (!sd->headset_connected)
+ return -ENODEV;
+
+ ret = kstrtobool(buf, &value);
+ if (ret)
+ return ret;
+
+ ret = sd->info->write_setting(sd->hdev, SS_SETTING_BT_AUTO_ENABLE, value);
+ if (ret)
+ return ret;
+
+ sd->bt_auto_enable = value;
+
+ return count;
+}
+
static DEVICE_ATTR_RO(bt_enabled);
static DEVICE_ATTR_RO(bt_device_connected);
static DEVICE_ATTR_RW(inactive_time);
+static DEVICE_ATTR_RW(bt_auto_enable);
static struct attribute *steelseries_headset_attrs[] = {
&dev_attr_bt_enabled.attr,
&dev_attr_bt_device_connected.attr,
&dev_attr_inactive_time.attr,
+ &dev_attr_bt_auto_enable.attr,
NULL,
};
@@ -1447,6 +1499,8 @@ static umode_t steelseries_headset_attr_is_visible(struct kobject *kobj,
return (caps & SS_CAP_BT_DEVICE_CONNECTED) ? attr->mode : 0;
if (attr == &dev_attr_inactive_time.attr)
return (caps & SS_CAP_INACTIVE_TIME) ? attr->mode : 0;
+ if (attr == &dev_attr_bt_auto_enable.attr)
+ return (caps & SS_CAP_BT_AUTO_ENABLE) ? attr->mode : 0;
return 0;
}
@@ -2106,7 +2160,8 @@ static int steelseries_probe(struct hid_device *hdev,
}
if (info->capabilities & (SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
- SS_CAP_INACTIVE_TIME)) {
+ SS_CAP_INACTIVE_TIME |
+ SS_CAP_BT_AUTO_ENABLE)) {
ret = sysfs_create_group(&hdev->dev.kobj,
&steelseries_headset_attr_group);
if (ret)
@@ -2189,7 +2244,7 @@ static void steelseries_remove(struct hid_device *hdev)
if (interface_num == sd->info->sync_interface) {
if (sd->info->capabilities & (SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
- SS_CAP_INACTIVE_TIME))
+ SS_CAP_INACTIVE_TIME | SS_CAP_BT_AUTO_ENABLE))
sysfs_remove_group(&hdev->dev.kobj,
&steelseries_headset_attr_group);
--
2.53.0
^ permalink raw reply related [flat|nested] 29+ messages in thread* [PATCH v3 17/18] HID: steelseries: Add mic mute LED brightness control
2026-02-27 23:50 [PATCH v3 00/18] HID: steelseries: Add support for Arctis headset lineup Sriman Achanta
` (15 preceding siblings ...)
2026-02-27 23:50 ` [PATCH v3 16/18] HID: steelseries: Add Bluetooth auto-enable " Sriman Achanta
@ 2026-02-27 23:50 ` Sriman Achanta
2026-02-27 23:50 ` [PATCH v3 18/18] HID: steelseries: Document sysfs ABI Sriman Achanta
2026-03-03 10:59 ` [PATCH v3 00/18] HID: steelseries: Add support for Arctis headset lineup Bastien Nocera
18 siblings, 0 replies; 29+ messages in thread
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
Christian Mayer, Sriman Achanta
Register the microphone mute LED as an LED class device named
"<device>::micmute", following the standard LED naming convention. The
brightness range is 0-3 representing off, low, medium, and high.
On the Arctis Nova 5 family, the discrete levels map to non-linear
hardware values expected by the firmware (0, 1, 4, 10). The Nova 7
family uses a direct linear mapping. On the Nova 7 Gen2, the current
brightness is recovered from the 0xa0 settings poll response.
Registration is guarded by a LEDS_CLASS module compatibility check
analogous to the existing SND guard.
Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
drivers/hid/hid-steelseries.c | 119 ++++++++++++++++++++++++++++++++--
1 file changed, 114 insertions(+), 5 deletions(-)
diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index a794af01e15a..dcd34c61cccd 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -33,6 +33,7 @@
#define SS_CAP_BT_CALL_DUCKING BIT(9)
#define SS_CAP_INACTIVE_TIME BIT(10)
#define SS_CAP_BT_AUTO_ENABLE BIT(11)
+#define SS_CAP_MIC_MUTE_BRIGHTNESS BIT(12)
#define SS_QUIRK_STATUS_SYNC_POLL BIT(0)
@@ -42,6 +43,7 @@
#define SS_SETTING_BT_CALL_DUCKING 3
#define SS_SETTING_INACTIVE_TIME 4
#define SS_SETTING_BT_AUTO_ENABLE 5
+#define SS_SETTING_MIC_MUTE_BRIGHTNESS 6
struct steelseries_device;
@@ -100,6 +102,12 @@ struct steelseries_device {
bool bt_device_connected;
u8 inactive_timeout;
bool bt_auto_enable;
+ u8 mic_mute_brightness;
+
+#if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
+ (IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
+ struct led_classdev *mic_mute_led;
+#endif
spinlock_t lock;
bool removed;
@@ -606,6 +614,14 @@ static int steelseries_arctis_nova_5_write_setting(struct hid_device *hdev,
case SS_SETTING_INACTIVE_TIME:
cmd = 0xa3;
break;
+ case SS_SETTING_MIC_MUTE_BRIGHTNESS:
+ cmd = 0xae;
+ /* Hardware uses non-linear values: 0=off, 1=low, 4=medium, 10=high */
+ if (value == 2)
+ value = 0x04;
+ else if (value == 3)
+ value = 0x0a;
+ break;
default:
return -EINVAL;
}
@@ -650,6 +666,9 @@ static int steelseries_arctis_nova_7_write_setting(struct hid_device *hdev,
case SS_SETTING_BT_AUTO_ENABLE:
cmd = 0xb2;
break;
+ case SS_SETTING_MIC_MUTE_BRIGHTNESS:
+ cmd = 0xae;
+ break;
default:
return -EINVAL;
}
@@ -1005,6 +1024,7 @@ static void steelseries_arctis_nova_7_gen2_parse_settings(
break;
case 0xa0:
sd->inactive_timeout = data[1];
+ sd->mic_mute_brightness = data[2];
sd->bt_auto_enable = data[3];
sd->bt_call_ducking = data[4];
break;
@@ -1020,6 +1040,9 @@ static void steelseries_arctis_nova_7_gen2_parse_settings(
case 0xa3:
sd->inactive_timeout = data[1];
break;
+ case 0xae:
+ sd->mic_mute_brightness = data[1];
+ break;
case 0xb2:
sd->bt_auto_enable = data[1];
break;
@@ -1129,7 +1152,8 @@ static const struct steelseries_device_info arctis_nova_3p_info = {
static const struct steelseries_device_info arctis_nova_5_info = {
.sync_interface = 3,
.capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME |
- SS_CAP_VOLUME_LIMITER | SS_CAP_INACTIVE_TIME,
+ SS_CAP_VOLUME_LIMITER | SS_CAP_INACTIVE_TIME |
+ SS_CAP_MIC_MUTE_BRIGHTNESS,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 10,
.mic_volume_max = 15,
@@ -1143,7 +1167,7 @@ static const struct steelseries_device_info arctis_nova_5x_info = {
.sync_interface = 3,
.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
- SS_CAP_INACTIVE_TIME,
+ SS_CAP_INACTIVE_TIME | SS_CAP_MIC_MUTE_BRIGHTNESS,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 10,
.mic_volume_max = 15,
@@ -1158,7 +1182,7 @@ static const struct steelseries_device_info arctis_nova_7_info = {
.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME |
- SS_CAP_BT_AUTO_ENABLE,
+ SS_CAP_BT_AUTO_ENABLE | SS_CAP_MIC_MUTE_BRIGHTNESS,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.sidetone_max = 3,
.mic_volume_max = 7,
@@ -1172,7 +1196,7 @@ static const struct steelseries_device_info arctis_nova_7p_info = {
.sync_interface = 3,
.capabilities = SS_CAP_BATTERY | SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME |
- SS_CAP_BT_AUTO_ENABLE,
+ SS_CAP_BT_AUTO_ENABLE | SS_CAP_MIC_MUTE_BRIGHTNESS,
.quirks = SS_QUIRK_STATUS_SYNC_POLL,
.mic_volume_max = 7,
.inactive_time_max = 255,
@@ -1189,7 +1213,7 @@ static const struct steelseries_device_info arctis_nova_7_gen2_info = {
SS_CAP_EXTERNAL_CONFIG | SS_CAP_SIDETONE |
SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME |
- SS_CAP_BT_AUTO_ENABLE,
+ SS_CAP_BT_AUTO_ENABLE | SS_CAP_MIC_MUTE_BRIGHTNESS,
.sidetone_max = 3,
.mic_volume_max = 7,
.inactive_time_max = 255,
@@ -1970,6 +1994,82 @@ static void steelseries_snd_unregister(struct steelseries_device *sd)
#endif
+/*
+ * Mic mute LED
+ */
+
+#if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
+ (IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
+
+#define SS_MIC_MUTE_BRIGHTNESS_MAX 3
+
+static int steelseries_mic_mute_led_brightness_set(struct led_classdev *led_cdev,
+ enum led_brightness brightness)
+{
+ struct device *dev = led_cdev->dev->parent;
+ struct hid_device *hdev = to_hid_device(dev);
+ struct steelseries_device *sd = hid_get_drvdata(hdev);
+ unsigned long flags;
+ int ret;
+
+ if (brightness > SS_MIC_MUTE_BRIGHTNESS_MAX)
+ brightness = SS_MIC_MUTE_BRIGHTNESS_MAX;
+
+ spin_lock_irqsave(&sd->lock, flags);
+ if (sd->removed) {
+ spin_unlock_irqrestore(&sd->lock, flags);
+ return 0;
+ }
+ spin_unlock_irqrestore(&sd->lock, flags);
+
+ ret = sd->info->write_setting(sd->hdev, SS_SETTING_MIC_MUTE_BRIGHTNESS,
+ brightness);
+ if (ret)
+ return ret;
+
+ sd->mic_mute_brightness = brightness;
+
+ return 0;
+}
+
+static enum led_brightness
+steelseries_mic_mute_led_brightness_get(struct led_classdev *led_cdev)
+{
+ struct device *dev = led_cdev->dev->parent;
+ struct hid_device *hdev = to_hid_device(dev);
+ struct steelseries_device *sd = hid_get_drvdata(hdev);
+
+ return sd->mic_mute_brightness;
+}
+
+static int steelseries_mic_mute_led_register(struct steelseries_device *sd)
+{
+ struct hid_device *hdev = sd->hdev;
+ struct led_classdev *led;
+ size_t name_size;
+ char *name;
+
+ name_size = strlen(dev_name(&hdev->dev)) + 16;
+
+ led = devm_kzalloc(&hdev->dev, sizeof(*led) + name_size, GFP_KERNEL);
+ if (!led)
+ return -ENOMEM;
+
+ name = (void *)(&led[1]);
+ snprintf(name, name_size, "%s::micmute", dev_name(&hdev->dev));
+ led->name = name;
+ led->brightness = 0;
+ led->max_brightness = SS_MIC_MUTE_BRIGHTNESS_MAX;
+ led->brightness_get = steelseries_mic_mute_led_brightness_get;
+ led->brightness_set_blocking = steelseries_mic_mute_led_brightness_set;
+
+ sd->mic_mute_led = led;
+
+ return devm_led_classdev_register(&hdev->dev, led);
+}
+
+#endif
+
static int steelseries_raw_event(struct hid_device *hdev,
struct hid_report *report, u8 *data, int size)
{
@@ -2175,6 +2275,15 @@ static int steelseries_probe(struct hid_device *hdev,
hid_warn(hdev, "Failed to register sound card: %d\n", ret);
#endif
+#if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
+ (IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
+ if (info->capabilities & SS_CAP_MIC_MUTE_BRIGHTNESS) {
+ ret = steelseries_mic_mute_led_register(sd);
+ if (ret < 0)
+ hid_warn(hdev, "Failed to register mic mute LED: %d\n", ret);
+ }
+#endif
+
INIT_DELAYED_WORK(&sd->status_work, steelseries_status_timer_work_handler);
INIT_DELAYED_WORK(&sd->settings_work, steelseries_settings_work_handler);
--
2.53.0
^ permalink raw reply related [flat|nested] 29+ messages in thread* [PATCH v3 18/18] HID: steelseries: Document sysfs ABI
2026-02-27 23:50 [PATCH v3 00/18] HID: steelseries: Add support for Arctis headset lineup Sriman Achanta
` (16 preceding siblings ...)
2026-02-27 23:50 ` [PATCH v3 17/18] HID: steelseries: Add mic mute LED brightness control Sriman Achanta
@ 2026-02-27 23:50 ` Sriman Achanta
2026-03-03 10:58 ` Bastien Nocera
2026-03-03 10:59 ` [PATCH v3 00/18] HID: steelseries: Add support for Arctis headset lineup Bastien Nocera
18 siblings, 1 reply; 29+ messages in thread
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
Christian Mayer, Sriman Achanta
Add Documentation/ABI/testing/sysfs-driver-hid-steelseries documenting
the sysfs attributes and LED class device exposed by the driver:
- bt_enabled, bt_device_connected: read-only Bluetooth radio state
- inactive_time: read/write auto-shutoff timer in minutes
- bt_auto_enable: read/write Bluetooth radio power-on behavior
- <dev>::micmute/brightness: mic mute LED brightness via LED class
Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
.../ABI/testing/sysfs-driver-hid-steelseries | 87 +++++++++++++++++++
1 file changed, 87 insertions(+)
create mode 100644 Documentation/ABI/testing/sysfs-driver-hid-steelseries
diff --git a/Documentation/ABI/testing/sysfs-driver-hid-steelseries b/Documentation/ABI/testing/sysfs-driver-hid-steelseries
new file mode 100644
index 000000000000..7b8d29282ed6
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-driver-hid-steelseries
@@ -0,0 +1,87 @@
+What: /sys/bus/hid/drivers/steelseries/<dev>/bt_enabled
+Date: February 2026
+KernelVersion: 6.20
+Contact: Sriman Achanta <srimanachanta@gmail.com>
+Description: (RO) Whether the Bluetooth radio on the headset is currently
+ enabled.
+
+ * 0 = Bluetooth radio off
+ * 1 = Bluetooth radio on
+
+ Returns -ENODEV if the headset is not connected to the
+ receiver.
+
+ Supported on: Arctis Nova 7 Gen2, Arctis Nova Pro Wireless
+
+What: /sys/bus/hid/drivers/steelseries/<dev>/bt_device_connected
+Date: February 2026
+KernelVersion: 6.20
+Contact: Sriman Achanta <srimanachanta@gmail.com>
+Description: (RO) Whether a Bluetooth device is currently connected to
+ the headset.
+
+ * 0 = no Bluetooth device connected
+ * 1 = Bluetooth device connected
+
+ Returns -ENODEV if the headset is not connected to the
+ receiver.
+
+ Supported on: Arctis Nova 7 Gen2, Arctis Nova Pro Wireless
+
+What: /sys/bus/hid/drivers/steelseries/<dev>/inactive_time
+Date: February 2026
+KernelVersion: 6.20
+Contact: Sriman Achanta <srimanachanta@gmail.com>
+Description: (RW) Auto-shutoff timer for the headset, in minutes. A
+ value of 0 disables the timer. The maximum accepted value
+ is device-specific.
+
+ The encoding sent to the firmware varies by device family:
+ the Arctis 9 converts the value to seconds, the Nova 3P
+ rounds down to its nearest supported discrete step, and the
+ Nova Pro maps to six firmware-defined level indices. For all
+ other devices the value is sent in minutes directly.
+
+ Reading the attribute returns the last value reported by the
+ firmware. Writing immediately sends the new timeout to the
+ device.
+
+ Returns -ENODEV if the headset is not connected to the
+ receiver.
+
+ Supported on: Arctis 1 Wireless, Arctis 7, Arctis 7+,
+ Arctis 9, Arctis Nova 3P, Arctis Nova 5, Arctis Nova 5X,
+ Arctis Nova 7, Arctis Nova 7P, Arctis Nova 7 Gen2,
+ Arctis Nova Pro Wireless
+
+What: /sys/bus/hid/drivers/steelseries/<dev>/bt_auto_enable
+Date: February 2026
+KernelVersion: 6.20
+Contact: Sriman Achanta <srimanachanta@gmail.com>
+Description: (RW) Whether the headset automatically enables its
+ Bluetooth radio on power-on.
+
+ * 0 = Bluetooth radio stays off at power-on
+ * 1 = Bluetooth radio activates automatically at power-on
+
+ Returns -ENODEV if the headset is not connected to the
+ receiver.
+
+ Supported on: Arctis Nova 7, Arctis Nova 7P,
+ Arctis Nova 7 Gen2
+
+What: /sys/class/leds/<dev>::micmute/brightness
+Date: February 2026
+KernelVersion: 6.20
+Contact: Sriman Achanta <srimanachanta@gmail.com>
+Description: (RW) Brightness of the microphone mute status LED.
+ <dev> is the HID device node name (e.g.
+ 0003:1038:12AE.0001).
+
+ * 0 = off
+ * 1 = low
+ * 2 = medium
+ * 3 = high
+
+ Supported on: Arctis Nova 5, Arctis Nova 5X, Arctis Nova 7,
+ Arctis Nova 7P, Arctis Nova 7 Gen2
--
2.53.0
^ permalink raw reply related [flat|nested] 29+ messages in thread* Re: [PATCH v3 18/18] HID: steelseries: Document sysfs ABI
2026-02-27 23:50 ` [PATCH v3 18/18] HID: steelseries: Document sysfs ABI Sriman Achanta
@ 2026-03-03 10:58 ` Bastien Nocera
0 siblings, 0 replies; 29+ messages in thread
From: Bastien Nocera @ 2026-03-03 10:58 UTC (permalink / raw)
To: Sriman Achanta, Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer
On Fri, 2026-02-27 at 18:50 -0500, Sriman Achanta wrote:
> Add Documentation/ABI/testing/sysfs-driver-hid-steelseries
> documenting
> the sysfs attributes and LED class device exposed by the driver:
>
> - bt_enabled, bt_device_connected: read-only Bluetooth radio state
> - inactive_time: read/write auto-shutoff timer in minutes
> - bt_auto_enable: read/write Bluetooth radio power-on behavior
> - <dev>::micmute/brightness: mic mute LED brightness via LED class
This should probably be documented in the commit that introduces the
new attribute, rather than all of them being updated at once.
>
> Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
> ---
> .../ABI/testing/sysfs-driver-hid-steelseries | 87
> +++++++++++++++++++
> 1 file changed, 87 insertions(+)
> create mode 100644 Documentation/ABI/testing/sysfs-driver-hid-
> steelseries
>
> diff --git a/Documentation/ABI/testing/sysfs-driver-hid-steelseries
> b/Documentation/ABI/testing/sysfs-driver-hid-steelseries
> new file mode 100644
> index 000000000000..7b8d29282ed6
> --- /dev/null
> +++ b/Documentation/ABI/testing/sysfs-driver-hid-steelseries
> @@ -0,0 +1,87 @@
> +What: /sys/bus/hid/drivers/steelseries/<dev>/bt_enabled
> +Date: February 2026
> +KernelVersion: 6.20
> +Contact: Sriman Achanta <srimanachanta@gmail.com>
> +Description: (RO) Whether the Bluetooth radio on the headset is
> currently
> + enabled.
> +
> + * 0 = Bluetooth radio off
> + * 1 = Bluetooth radio on
> +
> + Returns -ENODEV if the headset is not connected to
> the
> + receiver.
> +
> + Supported on: Arctis Nova 7 Gen2, Arctis Nova Pro
> Wireless
> +
> +What: /sys/bus/hid/drivers/steelseries/<dev>/bt_device_con
> nected
> +Date: February 2026
> +KernelVersion: 6.20
> +Contact: Sriman Achanta <srimanachanta@gmail.com>
> +Description: (RO) Whether a Bluetooth device is currently
> connected to
> + the headset.
> +
> + * 0 = no Bluetooth device connected
> + * 1 = Bluetooth device connected
> +
> + Returns -ENODEV if the headset is not connected to
> the
> + receiver.
> +
> + Supported on: Arctis Nova 7 Gen2, Arctis Nova Pro
> Wireless
> +
> +What: /sys/bus/hid/drivers/steelseries/<dev>/inactive_time
> +Date: February 2026
> +KernelVersion: 6.20
> +Contact: Sriman Achanta <srimanachanta@gmail.com>
> +Description: (RW) Auto-shutoff timer for the headset, in minutes.
> A
> + value of 0 disables the timer. The maximum accepted
> value
> + is device-specific.
> +
> + The encoding sent to the firmware varies by device
> family:
> + the Arctis 9 converts the value to seconds, the Nova
> 3P
> + rounds down to its nearest supported discrete step,
> and the
> + Nova Pro maps to six firmware-defined level indices.
> For all
> + other devices the value is sent in minutes directly.
> +
> + Reading the attribute returns the last value
> reported by the
> + firmware. Writing immediately sends the new timeout
> to the
> + device.
> +
> + Returns -ENODEV if the headset is not connected to
> the
> + receiver.
> +
> + Supported on: Arctis 1 Wireless, Arctis 7, Arctis
> 7+,
> + Arctis 9, Arctis Nova 3P, Arctis Nova 5, Arctis Nova
> 5X,
> + Arctis Nova 7, Arctis Nova 7P, Arctis Nova 7 Gen2,
> + Arctis Nova Pro Wireless
> +
> +What: /sys/bus/hid/drivers/steelseries/<dev>/bt_auto_enabl
> e
> +Date: February 2026
> +KernelVersion: 6.20
> +Contact: Sriman Achanta <srimanachanta@gmail.com>
> +Description: (RW) Whether the headset automatically enables its
> + Bluetooth radio on power-on.
> +
> + * 0 = Bluetooth radio stays off at power-on
> + * 1 = Bluetooth radio activates automatically at
> power-on
> +
> + Returns -ENODEV if the headset is not connected to
> the
> + receiver.
> +
> + Supported on: Arctis Nova 7, Arctis Nova 7P,
> + Arctis Nova 7 Gen2
> +
> +What: /sys/class/leds/<dev>::micmute/brightness
> +Date: February 2026
> +KernelVersion: 6.20
> +Contact: Sriman Achanta <srimanachanta@gmail.com>
> +Description: (RW) Brightness of the microphone mute status LED.
> + <dev> is the HID device node name (e.g.
> + 0003:1038:12AE.0001).
> +
> + * 0 = off
> + * 1 = low
> + * 2 = medium
> + * 3 = high
> +
> + Supported on: Arctis Nova 5, Arctis Nova 5X, Arctis
> Nova 7,
> + Arctis Nova 7P, Arctis Nova 7 Gen2
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH v3 00/18] HID: steelseries: Add support for Arctis headset lineup
2026-02-27 23:50 [PATCH v3 00/18] HID: steelseries: Add support for Arctis headset lineup Sriman Achanta
` (17 preceding siblings ...)
2026-02-27 23:50 ` [PATCH v3 18/18] HID: steelseries: Document sysfs ABI Sriman Achanta
@ 2026-03-03 10:59 ` Bastien Nocera
18 siblings, 0 replies; 29+ messages in thread
From: Bastien Nocera @ 2026-03-03 10:59 UTC (permalink / raw)
To: Sriman Achanta, Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer
Hey Sriman,
Great work splitting up your original code. I'm afraid that I see some
of the patches as still needing more splitting up to be easily
reviewable, especially "HID: steelseries: Add async support and unify
device definitions".
The code looks good to me from a cursory glance, though we probably
want to get more eyeballs on the sound code.
I won't have time to test this patchset on real hardware for a little
while, but I'll test this version or any updates if there are any when
I have time.
I also don't know what out subsystem maintainers think, but, if you
have the patience, this might be the opportunity to split off the
headset support from the completely unrelated force feedback steering
wheel driver. I'm not going to block your patchset on this but
something to consider for the future.
Cheers
On Fri, 2026-02-27 at 18:50 -0500, Sriman Achanta wrote:
> This patch series adds comprehensive support for the SteelSeries
> Arctis
> wireless gaming headset lineup to the hid-steelseries driver.
>
> The current driver provides only basic battery monitoring for Arctis
> 1
> and Arctis 9. This series extends support to 25+ Arctis models with
> full feature control including sidetone, auto-sleep, microphone
> controls, volume limiting, and Bluetooth settings.
>
> The driver restructure uses a capability-based device info system to
> cleanly handle the varying feature sets across different Arctis
> generations while maintaining support for the legacy SRW-S1 racing
> wheel.
>
> The driver also sets up future support for async device control which
> is currently implemented for the Arctis Nova 7 Gen 2 and Post-January
> update
> Gen 1 devices as implemented.
>
> Tested on Arctis Nova 7 (0x2202) and Arctis Nova 7 (0x22a1). All
> other
> implementation details are based on the reverse engineering done in
> the
> HeadsetControl library (902e9bc).
>
> Changes since v2:
> * Expose audio related controls via ALSA mixers
> * Implement async inputs from supported devices with known protocols
> * Overall code cleanup and improvements to initalization logic
> * Fixed several logical and protocol issues for Arctis 7 and 9
>
> Sriman Achanta (18):
> HID: steelseries: Fix ARCTIS_1_X device mislabeling
> HID: hid-ids: Add SteelSeries Arctis headset device IDs
> HID: quirks: Add additional Arctis headset device IDs
> HID: steelseries: Add async support and unify device definitions
> HID: steelseries: Update Kconfig help text for expanded headset
> support
> HID: steelseries: Add ALSA sound card infrastructure
> HID: steelseries: Add ChatMix ALSA mixer controls
> HID: steelseries: Add mic mute ALSA mixer control
> HID: steelseries: Add Bluetooth state sysfs attributes
> HID: steelseries: Add settings poll infrastructure
> HID: steelseries: Add sidetone ALSA mixer control
> HID: steelseries: Add mic volume ALSA mixer control
> HID: steelseries: Add volume limiter ALSA mixer control
> HID: steelseries: Add Bluetooth call audio ducking control
> HID: steelseries: Add inactive time sysfs attribute
> HID: steelseries: Add Bluetooth auto-enable sysfs attribute
> HID: steelseries: Add mic mute LED brightness control
> HID: steelseries: Document sysfs ABI
>
> .../ABI/testing/sysfs-driver-hid-steelseries | 87 +
> drivers/hid/Kconfig | 5 +-
> drivers/hid/hid-ids.h | 35 +-
> drivers/hid/hid-quirks.c | 27 +
> drivers/hid/hid-steelseries.c | 2329 ++++++++++++++-
> --
> 5 files changed, 2184 insertions(+), 299 deletions(-)
> create mode 100644 Documentation/ABI/testing/sysfs-driver-hid-
> steelseries
^ permalink raw reply [flat|nested] 29+ messages in thread