Linux Input/HID development
 help / color / mirror / Atom feed
* [PATCH v4 00/10] HID: steelseries: Refactor Arctis driver and add Arctis Nova 7 Gen2 support
@ 2026-06-23 17:23 Sriman Achanta
  2026-06-23 17:23 ` [PATCH v4 01/10] HID: steelseries: Fix ARCTIS_1_X device mislabeling Sriman Achanta
                   ` (9 more replies)
  0 siblings, 10 replies; 15+ messages in thread
From: Sriman Achanta @ 2026-06-23 17:23 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer,
	Bastien Nocera, Sriman Achanta

This series reworks the Arctis headset support in hid-steelseries and
adds battery reporting for the Arctis Nova 7 Gen2 family.

The work splits the Arctis code out of hid-steelseries.c into its own
module, moves it onto a device_info framework so each model declares its
own capabilities and status callbacks, fixes a few battery reporting
issues, and adds an async status interface so headsets that push their
own updates do not need polling. The Arctis Nova 7 Gen2 family is the
first user of that interface.

This is a large scope cut from v3. v3 tried to add the full control
surface (sidetone, ChatMix, microphone, volume limiting, Bluetooth
settings) across 25+ models, with the audio controls exposed through
ALSA mixers. There is no clear precedent for an ALSA control surface
living in a HID driver, and I am not confident about where that code
belongs. Rather than hold up the rest behind that question, this version
keeps the refactor and the battery work and limits new hardware support
to the one headset I own and can test. The control surface can come back
later once its home is settled.

Tested on the Arctis Nova 7 (0x2202) and the Arctis Nova 7 2026 (0x22a1).
The Arctis 9 calibration values come from the HeadsetControl project and
public reverse engineering, not from direct measurement, as noted in
that patch.

Changes since v3:
- Drop the ALSA sound card infrastructure and all ALSA mixer controls
  (sidetone, ChatMix, mic mute, mic volume, volume limiter, Bluetooth
  call audio ducking). The right location for audio control in a HID
  driver needs more discussion first.
- Drop the sysfs control attributes (Bluetooth state, inactive time,
  Bluetooth auto-enable, mic mute LED) and the settings poll
  infrastructure that backed them.
- Drop the sysfs ABI documentation patch, since those attributes are
  gone.
- Limit new device support to the Arctis Nova 7 Gen2 family, the only
  hardware I can test.
- Keep the module split, the device_info refactor, the battery fixes,
  and the async status interface as the base for future work.

Changes since v2:
- Expand support to 25+ Arctis models with a capability based
  device_info system.
- Expose audio controls through ALSA mixers.
- Add async input handling for devices with known protocols.
- Fix several logical and protocol issues for the Arctis 7 and 9.
- General code cleanup and initialization logic improvements.

Changes since v1:
- Fix Documentation formatting issues.

Sriman Achanta (10):
  HID: steelseries: Fix ARCTIS_1_X device mislabeling
  HID: steelseries: Fix whitespace in srws1 report descriptor
  HID: steelseries: Split Arctis headset driver into separate module
  HID: steelseries: Inline and simplify SRWS1 wheel driver
  HID: steelseries: Refactor Arctis driver to use device_info framework
  HID: steelseries: Report POWER_SUPPLY_STATUS_FULL when full
  HID: steelseries: Correct Arctis 9 battery calibration range
  HID: steelseries: Manage battery lifetime with refcounting
  HID: steelseries: Add async status interface support
  HID: steelseries: Add support for Arctis Nova 7 Gen2 family

 drivers/hid/Makefile                 |   2 +-
 drivers/hid/hid-ids.h                |  12 +-
 drivers/hid/hid-quirks.c             |  10 +-
 drivers/hid/hid-steelseries-arctis.c | 631 +++++++++++++++++++++++++++
 drivers/hid/hid-steelseries.c        | 550 ++++-------------------
 5 files changed, 732 insertions(+), 473 deletions(-)
 create mode 100644 drivers/hid/hid-steelseries-arctis.c


base-commit: 502d801f0ab03e4f32f9a33d203154ce84887921
-- 
2.54.0


^ permalink raw reply	[flat|nested] 15+ messages in thread

* [PATCH v4 01/10] HID: steelseries: Fix ARCTIS_1_X device mislabeling
  2026-06-23 17:23 [PATCH v4 00/10] HID: steelseries: Refactor Arctis driver and add Arctis Nova 7 Gen2 support Sriman Achanta
@ 2026-06-23 17:23 ` Sriman Achanta
  2026-06-23 17:23 ` [PATCH v4 02/10] HID: steelseries: Fix whitespace in srws1 report descriptor Sriman Achanta
                   ` (8 subsequent siblings)
  9 siblings, 0 replies; 15+ messages in thread
From: Sriman Achanta @ 2026-06-23 17:23 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer,
	Bastien Nocera, Sriman Achanta

The SteelSeries Arctis 1 Wireless for Xbox (0x12b6) was labelled as the
plain Arctis 1 Wireless. Rename USB_DEVICE_ID_STEELSERIES_ARCTIS_1 to
USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X, along with the matching quirk flag
and device table entry. The device ID value is unchanged.

Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
 drivers/hid/hid-ids.h         |  4 ++--
 drivers/hid/hid-quirks.c      |  2 +-
 drivers/hid/hid-steelseries.c | 10 +++++-----
 3 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 1059922baaac..915e936cbf8b 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1377,8 +1377,8 @@
 
 #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_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 57d8efdd9b89..f546179858c2 100644
--- a/drivers/hid/hid-quirks.c
+++ b/drivers/hid/hid-quirks.c
@@ -747,7 +747,7 @@ static const struct hid_device_id hid_have_special_driver[] = {
 #endif
 #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..fd38ee3ea6fc 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -18,7 +18,7 @@
 #include "hid-ids.h"
 
 #define STEELSERIES_SRWS1		BIT(0)
-#define STEELSERIES_ARCTIS_1		BIT(1)
+#define STEELSERIES_ARCTIS_1_X		BIT(1)
 #define STEELSERIES_ARCTIS_9		BIT(2)
 
 struct steelseries_device {
@@ -374,7 +374,7 @@ 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_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 +638,7 @@ 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_X) {
 		hid_dbg(sd->hdev,
 			"Parsing raw event for Arctis 1 headset (%*ph)\n", size, read_buf);
 		if (size < ARCTIS_1_BATTERY_RESPONSE_LEN ||
@@ -725,8 +725,8 @@ static const struct hid_device_id steelseries_devices[] = {
 	  .driver_data = STEELSERIES_SRWS1 },
 
 	{ /* SteelSeries Arctis 1 Wireless for XBox */
-	  HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_1),
-	  .driver_data = STEELSERIES_ARCTIS_1 },
+	  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),
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [PATCH v4 02/10] HID: steelseries: Fix whitespace in srws1 report descriptor
  2026-06-23 17:23 [PATCH v4 00/10] HID: steelseries: Refactor Arctis driver and add Arctis Nova 7 Gen2 support Sriman Achanta
  2026-06-23 17:23 ` [PATCH v4 01/10] HID: steelseries: Fix ARCTIS_1_X device mislabeling Sriman Achanta
@ 2026-06-23 17:23 ` Sriman Achanta
  2026-06-23 17:23 ` [PATCH v4 03/10] HID: steelseries: Split Arctis headset driver into separate module Sriman Achanta
                   ` (7 subsequent siblings)
  9 siblings, 0 replies; 15+ messages in thread
From: Sriman Achanta @ 2026-06-23 17:23 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer,
	Bastien Nocera, Sriman Achanta

Align the SRW-S1 report descriptor so the comment on each line sits in a
single column. No functional change.

Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
 drivers/hid/hid-steelseries.c | 164 +++++++++++++++++-----------------
 1 file changed, 82 insertions(+), 82 deletions(-)

diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index fd38ee3ea6fc..4c97126a4342 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -37,7 +37,7 @@ struct steelseries_device {
 };
 
 #if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
-    (IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
+	(IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
 #define SRWS1_NUMBER_LEDS 15
 struct steelseries_srws1_data {
 	__u16 led_state;
@@ -54,80 +54,80 @@ struct steelseries_srws1_data {
  */
 
 static const __u8 steelseries_srws1_rdesc_fixed[] = {
-0x05, 0x01,         /*  Usage Page (Desktop)                */
-0x09, 0x08,         /*  Usage (MultiAxis), Changed          */
-0xA1, 0x01,         /*  Collection (Application),           */
-0xA1, 0x02,         /*      Collection (Logical),           */
-0x95, 0x01,         /*          Report Count (1),           */
-0x05, 0x01,         /* Changed  Usage Page (Desktop),       */
-0x09, 0x30,         /* Changed  Usage (X),                  */
-0x16, 0xF8, 0xF8,   /*          Logical Minimum (-1800),    */
-0x26, 0x08, 0x07,   /*          Logical Maximum (1800),     */
-0x65, 0x14,         /*          Unit (Degrees),             */
-0x55, 0x0F,         /*          Unit Exponent (15),         */
-0x75, 0x10,         /*          Report Size (16),           */
-0x81, 0x02,         /*          Input (Variable),           */
-0x09, 0x31,         /* Changed  Usage (Y),                  */
-0x15, 0x00,         /*          Logical Minimum (0),        */
-0x26, 0xFF, 0x03,   /*          Logical Maximum (1023),     */
-0x75, 0x0C,         /*          Report Size (12),           */
-0x81, 0x02,         /*          Input (Variable),           */
-0x09, 0x32,         /* Changed  Usage (Z),                  */
-0x15, 0x00,         /*          Logical Minimum (0),        */
-0x26, 0xFF, 0x03,   /*          Logical Maximum (1023),     */
-0x75, 0x0C,         /*          Report Size (12),           */
-0x81, 0x02,         /*          Input (Variable),           */
-0x05, 0x01,         /*          Usage Page (Desktop),       */
-0x09, 0x39,         /*          Usage (Hat Switch),         */
-0x25, 0x07,         /*          Logical Maximum (7),        */
-0x35, 0x00,         /*          Physical Minimum (0),       */
-0x46, 0x3B, 0x01,   /*          Physical Maximum (315),     */
-0x65, 0x14,         /*          Unit (Degrees),             */
-0x75, 0x04,         /*          Report Size (4),            */
-0x95, 0x01,         /*          Report Count (1),           */
-0x81, 0x02,         /*          Input (Variable),           */
-0x25, 0x01,         /*          Logical Maximum (1),        */
-0x45, 0x01,         /*          Physical Maximum (1),       */
-0x65, 0x00,         /*          Unit,                       */
-0x75, 0x01,         /*          Report Size (1),            */
-0x95, 0x03,         /*          Report Count (3),           */
-0x81, 0x01,         /*          Input (Constant),           */
-0x05, 0x09,         /*          Usage Page (Button),        */
-0x19, 0x01,         /*          Usage Minimum (01h),        */
-0x29, 0x11,         /*          Usage Maximum (11h),        */
-0x95, 0x11,         /*          Report Count (17),          */
-0x81, 0x02,         /*          Input (Variable),           */
-                    /*   ---- Dial patch starts here ----   */
-0x05, 0x01,         /*          Usage Page (Desktop),       */
-0x09, 0x33,         /*          Usage (RX),                 */
-0x75, 0x04,         /*          Report Size (4),            */
-0x95, 0x02,         /*          Report Count (2),           */
-0x15, 0x00,         /*          Logical Minimum (0),        */
-0x25, 0x0b,         /*          Logical Maximum (b),        */
-0x81, 0x02,         /*          Input (Variable),           */
-0x09, 0x35,         /*          Usage (RZ),                 */
-0x75, 0x04,         /*          Report Size (4),            */
-0x95, 0x01,         /*          Report Count (1),           */
-0x25, 0x03,         /*          Logical Maximum (3),        */
-0x81, 0x02,         /*          Input (Variable),           */
-                    /*    ---- Dial patch ends here ----    */
-0x06, 0x00, 0xFF,   /*          Usage Page (FF00h),         */
-0x09, 0x01,         /*          Usage (01h),                */
-0x75, 0x04,         /* Changed  Report Size (4),            */
-0x95, 0x0D,         /* Changed  Report Count (13),          */
-0x81, 0x02,         /*          Input (Variable),           */
-0xC0,               /*      End Collection,                 */
-0xA1, 0x02,         /*      Collection (Logical),           */
-0x09, 0x02,         /*          Usage (02h),                */
-0x75, 0x08,         /*          Report Size (8),            */
-0x95, 0x10,         /*          Report Count (16),          */
-0x91, 0x02,         /*          Output (Variable),          */
-0xC0,               /*      End Collection,                 */
-0xC0                /*  End Collection                      */
+0x05, 0x01,					/*  Usage Page (Desktop)                */
+0x09, 0x08,					/*  Usage (MultiAxis), Changed          */
+0xA1, 0x01,					/*  Collection (Application),           */
+0xA1, 0x02,					/*      Collection (Logical),           */
+0x95, 0x01,					/*          Report Count (1),           */
+0x05, 0x01,				/* Changed  Usage Page (Desktop),       */
+0x09, 0x30,				/* Changed  Usage (X),                  */
+0x16, 0xF8, 0xF8,	/*          Logical Minimum (-1800),    */
+0x26, 0x08, 0x07,	/*          Logical Maximum (1800),     */
+0x65, 0x14,				/*          Unit (Degrees),             */
+0x55, 0x0F,				/*          Unit Exponent (15),         */
+0x75, 0x10,				/*          Report Size (16),           */
+0x81, 0x02,				/*          Input (Variable),           */
+0x09, 0x31,				/* Changed  Usage (Y),                  */
+0x15, 0x00,				/*          Logical Minimum (0),        */
+0x26, 0xFF, 0x03,	/*          Logical Maximum (1023),     */
+0x75, 0x0C,				/*          Report Size (12),           */
+0x81, 0x02,				/*          Input (Variable),           */
+0x09, 0x32,				/* Changed  Usage (Z),                  */
+0x15, 0x00,				/*          Logical Minimum (0),        */
+0x26, 0xFF, 0x03,	/*          Logical Maximum (1023),     */
+0x75, 0x0C,				/*          Report Size (12),           */
+0x81, 0x02,				/*          Input (Variable),           */
+0x05, 0x01,				/*          Usage Page (Desktop),       */
+0x09, 0x39,				/*          Usage (Hat Switch),         */
+0x25, 0x07,				/*          Logical Maximum (7),        */
+0x35, 0x00,				/*          Physical Minimum (0),       */
+0x46, 0x3B, 0x01,	/*          Physical Maximum (315),     */
+0x65, 0x14,				/*          Unit (Degrees),             */
+0x75, 0x04,				/*          Report Size (4),            */
+0x95, 0x01,				/*          Report Count (1),           */
+0x81, 0x02,				/*          Input (Variable),           */
+0x25, 0x01,				/*          Logical Maximum (1),        */
+0x45, 0x01,				/*          Physical Maximum (1),       */
+0x65, 0x00,				/*          Unit,                       */
+0x75, 0x01,				/*          Report Size (1),            */
+0x95, 0x03,				/*          Report Count (3),           */
+0x81, 0x01,				/*          Input (Constant),           */
+0x05, 0x09,				/*          Usage Page (Button),        */
+0x19, 0x01,				/*          Usage Minimum (01h),        */
+0x29, 0x11,				/*          Usage Maximum (11h),        */
+0x95, 0x11,				/*          Report Count (17),          */
+0x81, 0x02,				/*          Input (Variable),           */
+									/*   ---- Dial patch starts here ----   */
+0x05, 0x01,				/*          Usage Page (Desktop),       */
+0x09, 0x33,				/*          Usage (RX),                 */
+0x75, 0x04,				/*          Report Size (4),            */
+0x95, 0x02,				/*          Report Count (2),           */
+0x15, 0x00,				/*          Logical Minimum (0),        */
+0x25, 0x0b,				/*          Logical Maximum (b),        */
+0x81, 0x02,				/*          Input (Variable),           */
+0x09, 0x35,				/*          Usage (RZ),                 */
+0x75, 0x04,				/*          Report Size (4),            */
+0x95, 0x01,				/*          Report Count (1),           */
+0x25, 0x03,				/*          Logical Maximum (3),        */
+0x81, 0x02,				/*          Input (Variable),           */
+									/*    ---- Dial patch ends here ----    */
+0x06, 0x00, 0xFF,	/*          Usage Page (FF00h),         */
+0x09, 0x01,				/*          Usage (01h),                */
+0x75, 0x04,				/* Changed  Report Size (4),            */
+0x95, 0x0D,				/* Changed  Report Count (13),          */
+0x81, 0x02,				/*          Input (Variable),           */
+0xC0,							/*      End Collection,                 */
+0xA1, 0x02,				/*      Collection (Logical),           */
+0x09, 0x02,				/*          Usage (02h),                */
+0x75, 0x08,				/*          Report Size (8),            */
+0x95, 0x10,				/*          Report Count (16),          */
+0x91, 0x02,				/*          Output (Variable),          */
+0xC0,							/*      End Collection,                 */
+0xC0							/*  End Collection                      */
 };
 
 #if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
-    (IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
+	(IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
 static void steelseries_srws1_set_leds(struct hid_device *hdev, __u16 leds)
 {
 	struct list_head *report_list = &hdev->report_enum[HID_OUTPUT_REPORT].report_list;
@@ -489,7 +489,7 @@ static int steelseries_headset_battery_register(struct steelseries_device *sd)
 	sd->battery_desc.use_for_apm = 0;
 	n = atomic_inc_return(&battery_no) - 1;
 	sd->battery_desc.name = devm_kasprintf(&sd->hdev->dev, GFP_KERNEL,
-						    "steelseries_headset_battery_%ld", n);
+							"steelseries_headset_battery_%ld", n);
 	if (!sd->battery_desc.name)
 		return -ENOMEM;
 
@@ -535,7 +535,7 @@ static int steelseries_probe(struct hid_device *hdev, const struct hid_device_id
 
 	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;
@@ -581,7 +581,7 @@ static void steelseries_remove(struct hid_device *hdev)
 
 	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;
@@ -603,7 +603,7 @@ 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)
+		hdev->product != USB_DEVICE_ID_STEELSERIES_SRWS1)
 		return rdesc;
 
 	if (*rsize >= 115 && rdesc[11] == 0x02 && rdesc[13] == 0xc8
@@ -642,7 +642,7 @@ static int steelseries_headset_raw_event(struct hid_device *hdev,
 		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))) {
+			memcmp(read_buf, arctis_1_battery_request, sizeof(arctis_1_battery_request))) {
 			if (!delayed_work_pending(&sd->battery_work))
 				goto request_battery;
 			return 0;
@@ -722,15 +722,15 @@ static int steelseries_headset_raw_event(struct hid_device *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 },
+		.driver_data = STEELSERIES_SRWS1 },
 
 	{ /* 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 },
+		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_ARCTIS_9),
+		.driver_data = STEELSERIES_ARCTIS_9 },
 
 	{ }
 };
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [PATCH v4 03/10] HID: steelseries: Split Arctis headset driver into separate module
  2026-06-23 17:23 [PATCH v4 00/10] HID: steelseries: Refactor Arctis driver and add Arctis Nova 7 Gen2 support Sriman Achanta
  2026-06-23 17:23 ` [PATCH v4 01/10] HID: steelseries: Fix ARCTIS_1_X device mislabeling Sriman Achanta
  2026-06-23 17:23 ` [PATCH v4 02/10] HID: steelseries: Fix whitespace in srws1 report descriptor Sriman Achanta
@ 2026-06-23 17:23 ` Sriman Achanta
  2026-06-23 17:23 ` [PATCH v4 04/10] HID: steelseries: Inline and simplify SRWS1 wheel driver Sriman Achanta
                   ` (6 subsequent siblings)
  9 siblings, 0 replies; 15+ messages in thread
From: Sriman Achanta @ 2026-06-23 17:23 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer,
	Bastien Nocera, Sriman Achanta

Move all Arctis headset code (battery monitoring, wireless status,
power supply registration, raw event handling) from hid-steelseries.c
into the new hid-steelseries-arctis.c driver module.

hid-steelseries.c now handles only the SRWS1 racing wheel, while
hid-steelseries-arctis.c handles the Arctis 1 (Xbox) and Arctis 9
wireless headsets with their own device table, probe, remove, and
raw_event implementations.

Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
 drivers/hid/Makefile                 |   2 +-
 drivers/hid/hid-steelseries-arctis.c | 404 +++++++++++++++++++++++++++
 drivers/hid/hid-steelseries.c        | 386 +------------------------
 3 files changed, 410 insertions(+), 382 deletions(-)
 create mode 100644 drivers/hid/hid-steelseries-arctis.c

diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index 23e6e3dd0c56..4a172bd27b11 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -134,7 +134,7 @@ obj-$(CONFIG_HID_SMARTJOYPLUS)	+= hid-sjoy.o
 obj-$(CONFIG_HID_SONY)		+= hid-sony.o
 obj-$(CONFIG_HID_SPEEDLINK)	+= hid-speedlink.o
 obj-$(CONFIG_HID_STEAM)		+= hid-steam.o
-obj-$(CONFIG_HID_STEELSERIES)	+= hid-steelseries.o
+obj-$(CONFIG_HID_STEELSERIES)	+= hid-steelseries.o hid-steelseries-arctis.o
 obj-$(CONFIG_HID_SUNPLUS)	+= hid-sunplus.o
 obj-$(CONFIG_HID_GREENASIA)	+= hid-gaff.o
 obj-$(CONFIG_HID_THRUSTMASTER)	+= hid-tmff.o hid-thrustmaster.o
diff --git a/drivers/hid/hid-steelseries-arctis.c b/drivers/hid/hid-steelseries-arctis.c
new file mode 100644
index 000000000000..079504e6932a
--- /dev/null
+++ b/drivers/hid/hid-steelseries-arctis.c
@@ -0,0 +1,404 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ *  HID driver for Steelseries arctis headsets
+ *
+ *  Copyright (c) 2023 Bastien Nocera
+ */
+
+#include <linux/device.h>
+#include <linux/hid.h>
+#include <linux/module.h>
+#include <linux/power_supply.h>
+#include <linux/spinlock.h>
+#include <linux/usb.h>
+#include <linux/workqueue.h>
+
+#include "hid-ids.h"
+
+#define STEELSERIES_ARCTIS_1_X		BIT(0)
+#define STEELSERIES_ARCTIS_9		BIT(1)
+
+struct steelseries_device {
+	struct hid_device *hdev;
+	unsigned long quirks;
+
+	struct delayed_work battery_work;
+	spinlock_t lock;
+	bool removed;
+
+	struct power_supply_desc battery_desc;
+	struct power_supply *battery;
+	uint8_t battery_capacity;
+	bool headset_connected;
+	bool battery_charging;
+};
+
+#define STEELSERIES_HEADSET_BATTERY_TIMEOUT_MS	3000
+
+#define ARCTIS_1_BATTERY_RESPONSE_LEN		8
+#define ARCTIS_9_BATTERY_RESPONSE_LEN		64
+static const char arctis_1_battery_request[] = { 0x06, 0x12 };
+static const char arctis_9_battery_request[] = { 0x00, 0x20 };
+
+static int steelseries_headset_request_battery(struct hid_device *hdev,
+	const char *request, size_t len)
+{
+	u8 *write_buf;
+	int ret;
+
+	/* Request battery information */
+	write_buf = kmemdup(request, len, GFP_KERNEL);
+	if (!write_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;
+	}
+
+	kfree(write_buf);
+	return ret;
+}
+
+static void steelseries_headset_fetch_battery(struct hid_device *hdev)
+{
+	int ret = 0;
+
+	if (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));
+
+	if (ret < 0)
+		hid_dbg(hdev,
+			"Battery query failed (err: %d)\n", ret);
+}
+
+static int battery_capacity_to_level(int capacity)
+{
+	if (capacity >= 50)
+		return POWER_SUPPLY_CAPACITY_LEVEL_NORMAL;
+	if (capacity >= 20)
+		return POWER_SUPPLY_CAPACITY_LEVEL_LOW;
+	return POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL;
+}
+
+static void steelseries_headset_battery_timer_tick(struct work_struct *work)
+{
+	struct steelseries_device *sd = container_of(work,
+		struct steelseries_device, battery_work.work);
+	struct hid_device *hdev = sd->hdev;
+
+	steelseries_headset_fetch_battery(hdev);
+}
+
+#define STEELSERIES_PREFIX "SteelSeries "
+#define STEELSERIES_PREFIX_LEN strlen(STEELSERIES_PREFIX)
+
+static int steelseries_headset_battery_get_property(struct power_supply *psy,
+				enum power_supply_property psp,
+				union power_supply_propval *val)
+{
+	struct steelseries_device *sd = power_supply_get_drvdata(psy);
+	int ret = 0;
+
+	switch (psp) {
+	case POWER_SUPPLY_PROP_MODEL_NAME:
+		val->strval = sd->hdev->name;
+		while (!strncmp(val->strval, STEELSERIES_PREFIX, STEELSERIES_PREFIX_LEN))
+			val->strval += STEELSERIES_PREFIX_LEN;
+		break;
+	case POWER_SUPPLY_PROP_MANUFACTURER:
+		val->strval = "SteelSeries";
+		break;
+	case POWER_SUPPLY_PROP_PRESENT:
+		val->intval = 1;
+		break;
+	case POWER_SUPPLY_PROP_STATUS:
+		if (sd->headset_connected) {
+			val->intval = sd->battery_charging ?
+				POWER_SUPPLY_STATUS_CHARGING :
+				POWER_SUPPLY_STATUS_DISCHARGING;
+		} else
+			val->intval = POWER_SUPPLY_STATUS_UNKNOWN;
+		break;
+	case POWER_SUPPLY_PROP_SCOPE:
+		val->intval = POWER_SUPPLY_SCOPE_DEVICE;
+		break;
+	case POWER_SUPPLY_PROP_CAPACITY:
+		val->intval = sd->battery_capacity;
+		break;
+	case POWER_SUPPLY_PROP_CAPACITY_LEVEL:
+		val->intval = battery_capacity_to_level(sd->battery_capacity);
+		break;
+	default:
+		ret = -EINVAL;
+		break;
+	}
+	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[] = {
+	POWER_SUPPLY_PROP_MODEL_NAME,
+	POWER_SUPPLY_PROP_MANUFACTURER,
+	POWER_SUPPLY_PROP_PRESENT,
+	POWER_SUPPLY_PROP_STATUS,
+	POWER_SUPPLY_PROP_SCOPE,
+	POWER_SUPPLY_PROP_CAPACITY,
+	POWER_SUPPLY_PROP_CAPACITY_LEVEL,
+};
+
+static int steelseries_headset_battery_register(struct steelseries_device *sd)
+{
+	static atomic_t battery_no = ATOMIC_INIT(0);
+	struct power_supply_config battery_cfg = { .drv_data = sd, };
+	unsigned long n;
+	int ret;
+
+	sd->battery_desc.type = POWER_SUPPLY_TYPE_BATTERY;
+	sd->battery_desc.properties = steelseries_headset_battery_props;
+	sd->battery_desc.num_properties = ARRAY_SIZE(steelseries_headset_battery_props);
+	sd->battery_desc.get_property = steelseries_headset_battery_get_property;
+	sd->battery_desc.use_for_apm = 0;
+	n = atomic_inc_return(&battery_no) - 1;
+	sd->battery_desc.name = devm_kasprintf(&sd->hdev->dev, GFP_KERNEL,
+							"steelseries_headset_battery_%ld", n);
+	if (!sd->battery_desc.name)
+		return -ENOMEM;
+
+	/* avoid the warning of 0% battery while waiting for the first info */
+	steelseries_headset_set_wireless_status(sd->hdev, false);
+	sd->battery_capacity = 100;
+	sd->battery_charging = 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);
+		hid_err(sd->hdev,
+				"%s:power_supply_register failed with error %d\n",
+				__func__, ret);
+		return ret;
+	}
+	power_supply_powers(sd->battery, &sd->hdev->dev);
+
+	INIT_DELAYED_WORK(&sd->battery_work, steelseries_headset_battery_timer_tick);
+	steelseries_headset_fetch_battery(sd->hdev);
+
+	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));
+	}
+
+	return 0;
+}
+
+static bool steelseries_is_vendor_usage_page(struct hid_device *hdev, uint8_t usage_page)
+{
+	return hdev->rdesc[0] == 0x06 &&
+		hdev->rdesc[1] == usage_page &&
+		hdev->rdesc[2] == 0xff;
+}
+
+static int steelseries_arctis_probe(struct hid_device *hdev, const struct hid_device_id *id)
+{
+	struct steelseries_device *sd;
+	int ret;
+
+	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;
+
+	ret = hid_parse(hdev);
+	if (ret)
+		return ret;
+
+	if (sd->quirks & STEELSERIES_ARCTIS_9 &&
+			!steelseries_is_vendor_usage_page(hdev, 0xc0))
+		return -ENODEV;
+
+	spin_lock_init(&sd->lock);
+
+	ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+	if (ret)
+		return ret;
+
+	ret = hid_hw_open(hdev);
+	if (ret)
+		return ret;
+
+	if (steelseries_headset_battery_register(sd) < 0)
+		hid_err(sd->hdev,
+			"Failed to register battery for headset\n");
+
+	return ret;
+}
+
+static void steelseries_arctis_remove(struct hid_device *hdev)
+{
+	struct steelseries_device *sd;
+	unsigned long flags;
+
+	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 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_arctis_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;
+
+	if (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 (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 (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;
+		}
+	}
+
+	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);
+	}
+
+	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);
+	}
+
+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;
+}
+
+static const struct hid_device_id steelseries_arctis_devices[] = {
+	{ /* 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 },
+
+	{ }
+};
+MODULE_DEVICE_TABLE(hid, steelseries_arctis_devices);
+
+static struct hid_driver steelseries_arctis_driver = {
+	.name = "hid-steelseries-arctis",
+	.id_table = steelseries_arctis_devices,
+	.probe = steelseries_arctis_probe,
+	.remove = steelseries_arctis_remove,
+	.raw_event = steelseries_arctis_raw_event,
+};
+
+module_hid_driver(steelseries_arctis_driver);
+MODULE_DESCRIPTION("HID driver for Steelseries arctis headsets");
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Christian Mayer <git@mayer-bgk.de>");
+MODULE_AUTHOR("Bastien Nocera <hadess@hadess.net>");
diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index 4c97126a4342..984b13999d28 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -3,7 +3,6 @@
  *  HID driver for Steelseries devices
  *
  *  Copyright (c) 2013 Simon Wood
- *  Copyright (c) 2023 Bastien Nocera
  */
 
 /*
@@ -12,29 +11,11 @@
 #include <linux/device.h>
 #include <linux/hid.h>
 #include <linux/module.h>
-#include <linux/usb.h>
 #include <linux/leds.h>
 
 #include "hid-ids.h"
 
 #define STEELSERIES_SRWS1		BIT(0)
-#define STEELSERIES_ARCTIS_1_X		BIT(1)
-#define STEELSERIES_ARCTIS_9		BIT(2)
-
-struct steelseries_device {
-	struct hid_device *hdev;
-	unsigned long quirks;
-
-	struct delayed_work battery_work;
-	spinlock_t lock;
-	bool removed;
-
-	struct power_supply_desc battery_desc;
-	struct power_supply *battery;
-	uint8_t battery_capacity;
-	bool headset_connected;
-	bool battery_charging;
-};
 
 #if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
 	(IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
@@ -97,7 +78,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 +91,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),            */
@@ -340,263 +321,22 @@ static int steelseries_srws1_probe(struct hid_device *hdev,
 }
 #endif
 
-#define STEELSERIES_HEADSET_BATTERY_TIMEOUT_MS	3000
-
-#define ARCTIS_1_BATTERY_RESPONSE_LEN		8
-#define ARCTIS_9_BATTERY_RESPONSE_LEN		64
-static const char arctis_1_battery_request[] = { 0x06, 0x12 };
-static const char arctis_9_battery_request[] = { 0x00, 0x20 };
-
-static int steelseries_headset_request_battery(struct hid_device *hdev,
-	const char *request, size_t len)
-{
-	u8 *write_buf;
-	int ret;
-
-	/* Request battery information */
-	write_buf = kmemdup(request, len, GFP_KERNEL);
-	if (!write_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;
-	}
-
-	kfree(write_buf);
-	return ret;
-}
-
-static void steelseries_headset_fetch_battery(struct hid_device *hdev)
-{
-	int ret = 0;
-
-	if (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));
-
-	if (ret < 0)
-		hid_dbg(hdev,
-			"Battery query failed (err: %d)\n", ret);
-}
-
-static int battery_capacity_to_level(int capacity)
-{
-	if (capacity >= 50)
-		return POWER_SUPPLY_CAPACITY_LEVEL_NORMAL;
-	if (capacity >= 20)
-		return POWER_SUPPLY_CAPACITY_LEVEL_LOW;
-	return POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL;
-}
-
-static void steelseries_headset_battery_timer_tick(struct work_struct *work)
-{
-	struct steelseries_device *sd = container_of(work,
-		struct steelseries_device, battery_work.work);
-	struct hid_device *hdev = sd->hdev;
-
-	steelseries_headset_fetch_battery(hdev);
-}
-
-#define STEELSERIES_PREFIX "SteelSeries "
-#define STEELSERIES_PREFIX_LEN strlen(STEELSERIES_PREFIX)
-
-static int steelseries_headset_battery_get_property(struct power_supply *psy,
-				enum power_supply_property psp,
-				union power_supply_propval *val)
-{
-	struct steelseries_device *sd = power_supply_get_drvdata(psy);
-	int ret = 0;
-
-	switch (psp) {
-	case POWER_SUPPLY_PROP_MODEL_NAME:
-		val->strval = sd->hdev->name;
-		while (!strncmp(val->strval, STEELSERIES_PREFIX, STEELSERIES_PREFIX_LEN))
-			val->strval += STEELSERIES_PREFIX_LEN;
-		break;
-	case POWER_SUPPLY_PROP_MANUFACTURER:
-		val->strval = "SteelSeries";
-		break;
-	case POWER_SUPPLY_PROP_PRESENT:
-		val->intval = 1;
-		break;
-	case POWER_SUPPLY_PROP_STATUS:
-		if (sd->headset_connected) {
-			val->intval = sd->battery_charging ?
-				POWER_SUPPLY_STATUS_CHARGING :
-				POWER_SUPPLY_STATUS_DISCHARGING;
-		} else
-			val->intval = POWER_SUPPLY_STATUS_UNKNOWN;
-		break;
-	case POWER_SUPPLY_PROP_SCOPE:
-		val->intval = POWER_SUPPLY_SCOPE_DEVICE;
-		break;
-	case POWER_SUPPLY_PROP_CAPACITY:
-		val->intval = sd->battery_capacity;
-		break;
-	case POWER_SUPPLY_PROP_CAPACITY_LEVEL:
-		val->intval = battery_capacity_to_level(sd->battery_capacity);
-		break;
-	default:
-		ret = -EINVAL;
-		break;
-	}
-	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[] = {
-	POWER_SUPPLY_PROP_MODEL_NAME,
-	POWER_SUPPLY_PROP_MANUFACTURER,
-	POWER_SUPPLY_PROP_PRESENT,
-	POWER_SUPPLY_PROP_STATUS,
-	POWER_SUPPLY_PROP_SCOPE,
-	POWER_SUPPLY_PROP_CAPACITY,
-	POWER_SUPPLY_PROP_CAPACITY_LEVEL,
-};
-
-static int steelseries_headset_battery_register(struct steelseries_device *sd)
-{
-	static atomic_t battery_no = ATOMIC_INIT(0);
-	struct power_supply_config battery_cfg = { .drv_data = sd, };
-	unsigned long n;
-	int ret;
-
-	sd->battery_desc.type = POWER_SUPPLY_TYPE_BATTERY;
-	sd->battery_desc.properties = steelseries_headset_battery_props;
-	sd->battery_desc.num_properties = ARRAY_SIZE(steelseries_headset_battery_props);
-	sd->battery_desc.get_property = steelseries_headset_battery_get_property;
-	sd->battery_desc.use_for_apm = 0;
-	n = atomic_inc_return(&battery_no) - 1;
-	sd->battery_desc.name = devm_kasprintf(&sd->hdev->dev, GFP_KERNEL,
-							"steelseries_headset_battery_%ld", n);
-	if (!sd->battery_desc.name)
-		return -ENOMEM;
-
-	/* avoid the warning of 0% battery while waiting for the first info */
-	steelseries_headset_set_wireless_status(sd->hdev, false);
-	sd->battery_capacity = 100;
-	sd->battery_charging = 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);
-		hid_err(sd->hdev,
-				"%s:power_supply_register failed with error %d\n",
-				__func__, ret);
-		return ret;
-	}
-	power_supply_powers(sd->battery, &sd->hdev->dev);
-
-	INIT_DELAYED_WORK(&sd->battery_work, steelseries_headset_battery_timer_tick);
-	steelseries_headset_fetch_battery(sd->hdev);
-
-	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));
-	}
-
-	return 0;
-}
-
-static bool steelseries_is_vendor_usage_page(struct hid_device *hdev, uint8_t usage_page)
-{
-	return hdev->rdesc[0] == 0x06 &&
-		hdev->rdesc[1] == usage_page &&
-		hdev->rdesc[2] == 0xff;
-}
-
 static int steelseries_probe(struct hid_device *hdev, const struct hid_device_id *id)
 {
-	struct steelseries_device *sd;
-	int ret;
-
-	if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1) {
 #if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
 	(IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
-		return steelseries_srws1_probe(hdev, id);
+	return steelseries_srws1_probe(hdev, id);
 #else
-		return -ENODEV;
+	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;
-
-	ret = hid_parse(hdev);
-	if (ret)
-		return ret;
-
-	if (sd->quirks & STEELSERIES_ARCTIS_9 &&
-			!steelseries_is_vendor_usage_page(hdev, 0xc0))
-		return -ENODEV;
-
-	spin_lock_init(&sd->lock);
-
-	ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
-	if (ret)
-		return ret;
-
-	ret = hid_hw_open(hdev);
-	if (ret)
-		return ret;
-
-	if (steelseries_headset_battery_register(sd) < 0)
-		hid_err(sd->hdev,
-			"Failed to register battery for headset\n");
-
-	return ret;
 }
 
 static void steelseries_remove(struct hid_device *hdev)
 {
-	struct steelseries_device *sd;
-	unsigned long flags;
-
-	if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1) {
 #if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
 	(IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
-		hid_hw_stop(hdev);
-#endif
-		return;
-	}
-
-	sd = hid_get_drvdata(hdev);
-
-	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);
+#endif
 }
 
 static const __u8 *steelseries_srws1_report_fixup(struct hid_device *hdev,
@@ -615,123 +355,10 @@ static const __u8 *steelseries_srws1_report_fixup(struct hid_device *hdev,
 	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;
-
-	if (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 (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 (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;
-		}
-	}
-
-	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);
-	}
-
-	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);
-	}
-
-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;
-}
-
 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 */
-		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 },
-
 	{ }
 };
 MODULE_DEVICE_TABLE(hid, steelseries_devices);
@@ -742,12 +369,9 @@ static struct hid_driver steelseries_driver = {
 	.probe = steelseries_probe,
 	.remove = steelseries_remove,
 	.report_fixup = steelseries_srws1_report_fixup,
-	.raw_event = steelseries_headset_raw_event,
 };
 
 module_hid_driver(steelseries_driver);
 MODULE_DESCRIPTION("HID driver for Steelseries devices");
 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>");
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [PATCH v4 04/10] HID: steelseries: Inline and simplify SRWS1 wheel driver
  2026-06-23 17:23 [PATCH v4 00/10] HID: steelseries: Refactor Arctis driver and add Arctis Nova 7 Gen2 support Sriman Achanta
                   ` (2 preceding siblings ...)
  2026-06-23 17:23 ` [PATCH v4 03/10] HID: steelseries: Split Arctis headset driver into separate module Sriman Achanta
@ 2026-06-23 17:23 ` Sriman Achanta
  2026-06-23 17:23 ` [PATCH v4 05/10] HID: steelseries: Refactor Arctis driver to use device_info framework Sriman Achanta
                   ` (5 subsequent siblings)
  9 siblings, 0 replies; 15+ messages in thread
From: Sriman Achanta @ 2026-06-23 17:23 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer,
	Bastien Nocera, Sriman Achanta

Arctis headsets have their own driver now, so hid-steelseries.c only
handles the SRWS1 racing wheel. Remove the dispatch layer:

- Inline steelseries_srws1_probe() as steelseries_probe()
- Add a steelseries_remove() that calls hid_hw_stop()
- Drop the STEELSERIES_SRWS1 quirk bit, no longer needed
- Remove the vendor/product check in steelseries_srws1_report_fixup(),
  since this driver only binds the SRWS1

Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
 drivers/hid/hid-steelseries.c | 30 +++++++++---------------------
 1 file changed, 9 insertions(+), 21 deletions(-)

diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index 984b13999d28..9a7047fbd6dd 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -15,8 +15,6 @@
 
 #include "hid-ids.h"
 
-#define STEELSERIES_SRWS1		BIT(0)
-
 #if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
 	(IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
 #define SRWS1_NUMBER_LEDS 15
@@ -225,7 +223,7 @@ static enum led_brightness steelseries_srws1_led_get_brightness(struct led_class
 	return value ? LED_FULL : LED_OFF;
 }
 
-static int steelseries_srws1_probe(struct hid_device *hdev,
+static int steelseries_probe(struct hid_device *hdev,
 		const struct hid_device_id *id)
 {
 	int ret, i;
@@ -319,33 +317,24 @@ static int steelseries_srws1_probe(struct hid_device *hdev,
 err:
 	return ret;
 }
-#endif
 
-static int steelseries_probe(struct hid_device *hdev, const struct hid_device_id *id)
+static void steelseries_remove(struct hid_device *hdev)
 {
-#if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
-	(IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
-	return steelseries_srws1_probe(hdev, id);
+	hid_hw_stop(hdev);
+}
 #else
+static int steelseries_probe(struct hid_device *hdev,
+		const struct hid_device_id *id)
+{
 	return -ENODEV;
-#endif
 }
 
-static void steelseries_remove(struct hid_device *hdev)
-{
-#if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
-	(IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
-	hid_hw_stop(hdev);
+static void steelseries_remove(struct hid_device *hdev) {}
 #endif
-}
 
 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");
@@ -356,8 +345,7 @@ static const __u8 *steelseries_srws1_report_fixup(struct hid_device *hdev,
 }
 
 static const struct hid_device_id steelseries_devices[] = {
-	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_SRWS1),
-		.driver_data = STEELSERIES_SRWS1 },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_SRWS1) },
 
 	{ }
 };
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [PATCH v4 05/10] HID: steelseries: Refactor Arctis driver to use device_info framework
  2026-06-23 17:23 [PATCH v4 00/10] HID: steelseries: Refactor Arctis driver and add Arctis Nova 7 Gen2 support Sriman Achanta
                   ` (3 preceding siblings ...)
  2026-06-23 17:23 ` [PATCH v4 04/10] HID: steelseries: Inline and simplify SRWS1 wheel driver Sriman Achanta
@ 2026-06-23 17:23 ` Sriman Achanta
  2026-06-23 17:33   ` sashiko-bot
  2026-06-23 17:23 ` [PATCH v4 06/10] HID: steelseries: Report POWER_SUPPLY_STATUS_FULL when full Sriman Achanta
                   ` (4 subsequent siblings)
  9 siblings, 1 reply; 15+ messages in thread
From: Sriman Achanta @ 2026-06-23 17:23 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer,
	Bastien Nocera, Sriman Achanta

Replace the per-product if/else quirk bitmap with a
steelseries_device_info struct. Each model provides its capabilities,
sync_interface, and request_status/parse_status callbacks. Report
sending is folded into steelseries_send_report() and the feature and
output wrappers, and the battery identifiers lose their per-model names.

This is mostly a refactor, but it changes two things:

  - Battery status is polled from a periodic delayed work (status_work)
    instead of being requested from raw_event(). The interval stays at
    3s.
  - Arctis 1 no longer clamps the reported capacity to 100% while
    disconnected. The connection state already controls how this is
    shown to userspace.

ARCTIS_1_X and ARCTIS_9 keep working. No new devices are added.

Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
 drivers/hid/hid-steelseries-arctis.c | 469 +++++++++++++++------------
 1 file changed, 268 insertions(+), 201 deletions(-)

diff --git a/drivers/hid/hid-steelseries-arctis.c b/drivers/hid/hid-steelseries-arctis.c
index 079504e6932a..f00f4c5e6d9e 100644
--- a/drivers/hid/hid-steelseries-arctis.c
+++ b/drivers/hid/hid-steelseries-arctis.c
@@ -3,6 +3,7 @@
  *  HID driver for Steelseries arctis headsets
  *
  *  Copyright (c) 2023 Bastien Nocera
+ *  Copyright (c) 2026 Sriman Achanta
  */
 
 #include <linux/device.h>
@@ -15,70 +16,95 @@
 
 #include "hid-ids.h"
 
-#define STEELSERIES_ARCTIS_1_X		BIT(0)
-#define STEELSERIES_ARCTIS_9		BIT(1)
+#define SS_CAP_BATTERY			BIT(0)
+
+struct steelseries_device;
+
+struct steelseries_device_info {
+	unsigned long capabilities;
+
+	u8 sync_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;
+	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;
-};
 
-#define STEELSERIES_HEADSET_BATTERY_TIMEOUT_MS	3000
+	spinlock_t lock;
+	bool removed;
+};
 
-#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 };
+/*
+ * Headset report helpers
+ */
 
-static int steelseries_headset_request_battery(struct hid_device *hdev,
-	const char *request, size_t len)
+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);
+}
+
+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 (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));
+/*
+ * Headset status request functions
+ */
 
-	if (ret < 0)
-		hid_dbg(hdev,
-			"Battery query failed (err: %d)\n", ret);
+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_9_request_status(struct hid_device *hdev)
+{
+	const u8 data[] = { 0x00, 0x20 };
+
+	return steelseries_send_feature_report(hdev, data, sizeof(data));
 }
 
+/*
+ * Headset battery helpers
+ */
+
 static int battery_capacity_to_level(int capacity)
 {
 	if (capacity >= 50)
@@ -88,30 +114,96 @@ 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_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], 0x68, 0x9d);
+	}
+}
+
+/*
+ * Device info definitions
+ */
+
+static const struct steelseries_device_info arctis_1_info = {
+	.sync_interface = 3,
+	.capabilities = SS_CAP_BATTERY,
+	.request_status = steelseries_arctis_1_request_status,
+	.parse_status = steelseries_arctis_1_parse_status,
+};
+
+static const struct steelseries_device_info arctis_9_info = {
+	.sync_interface = 0,
+	.capabilities = SS_CAP_BATTERY,
+	.request_status = steelseries_arctis_9_request_status,
+	.parse_status = steelseries_arctis_9_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)
 {
 	struct steelseries_device *sd = power_supply_get_drvdata(psy);
+	size_t prefix_len;
 	int ret = 0;
 
 	switch (psp) {
 	case POWER_SUPPLY_PROP_MODEL_NAME:
 		val->strval = sd->hdev->name;
-		while (!strncmp(val->strval, STEELSERIES_PREFIX, STEELSERIES_PREFIX_LEN))
-			val->strval += STEELSERIES_PREFIX_LEN;
+		while ((prefix_len = str_has_prefix(val->strval, STEELSERIES_PREFIX)))
+			val->strval += prefix_len;
 		break;
 	case POWER_SUPPLY_PROP_MANUFACTURER:
 		val->strval = "SteelSeries";
@@ -120,12 +212,12 @@ static int steelseries_headset_battery_get_property(struct power_supply *psy,
 		val->intval = 1;
 		break;
 	case POWER_SUPPLY_PROP_STATUS:
-		if (sd->headset_connected) {
-			val->intval = sd->battery_charging ?
-				POWER_SUPPLY_STATUS_CHARGING :
-				POWER_SUPPLY_STATUS_DISCHARGING;
-		} else
+		if (!sd->headset_connected)
 			val->intval = POWER_SUPPLY_STATUS_UNKNOWN;
+		else if (sd->battery_charging)
+			val->intval = POWER_SUPPLY_STATUS_CHARGING;
+		else
+			val->intval = POWER_SUPPLY_STATUS_DISCHARGING;
 		break;
 	case POWER_SUPPLY_PROP_SCOPE:
 		val->intval = POWER_SUPPLY_SCOPE_DEVICE;
@@ -143,22 +235,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,
@@ -168,7 +245,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
+ */
+
+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)
+		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, };
@@ -176,25 +272,27 @@ 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,
-							"steelseries_headset_battery_%ld", n);
+						"steelseries_headset_battery_%ld", n);
 	if (!sd->battery_desc.name)
 		return -ENOMEM;
 
 	/* avoid the warning of 0% battery while waiting for the first info */
-	steelseries_headset_set_wireless_status(sd->hdev, false);
 	sd->battery_capacity = 100;
 	sd->battery_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);
@@ -202,59 +300,65 @@ 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);
-
-	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));
-	}
-
 	return 0;
 }
 
-static bool steelseries_is_vendor_usage_page(struct hid_device *hdev, uint8_t usage_page)
-{
-	return hdev->rdesc[0] == 0x06 &&
-		hdev->rdesc[1] == usage_page &&
-		hdev->rdesc[2] == 0xff;
-}
-
-static int steelseries_arctis_probe(struct hid_device *hdev, const struct hid_device_id *id)
+static int steelseries_arctis_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;
+	u8 interface_num;
 	int ret;
 
-	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-sync interfaces */
+	if (interface_num != info->sync_interface)
+		return hid_hw_start(hdev, HID_CONNECT_DEFAULT);
 
+	sd = devm_kzalloc(&hdev->dev, sizeof(*sd), GFP_KERNEL);
+	if (!sd)
+		return -ENOMEM;
+
+	sd->hdev = hdev;
+	sd->info = info;
 	spin_lock_init(&sd->lock);
 
+	hid_set_drvdata(hdev, sd);
+
 	ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
 	if (ret)
 		return ret;
 
 	ret = hid_hw_open(hdev);
 	if (ret)
-		return ret;
+		goto err_stop;
 
-	if (steelseries_headset_battery_register(sd) < 0)
-		hid_err(sd->hdev,
-			"Failed to register battery for headset\n");
+	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;
 
+err_stop:
+	hid_hw_stop(hdev);
 	return ret;
 }
 
@@ -262,130 +366,92 @@ static void steelseries_arctis_remove(struct hid_device *hdev)
 {
 	struct steelseries_device *sd;
 	unsigned long flags;
+	struct usb_interface *intf;
+	u8 interface_num;
+
+	if (hid_is_usb(hdev)) {
+		intf = to_usb_interface(hdev->dev.parent);
+		interface_num = intf->cur_altsetting->desc.bInterfaceNumber;
+	} else {
+		return;
+	}
 
 	sd = hid_get_drvdata(hdev);
 
-	spin_lock_irqsave(&sd->lock, flags);
-	sd->removed = true;
-	spin_unlock_irqrestore(&sd->lock, flags);
+	if (!sd) {
+		hid_hw_stop(hdev);
+		return;
+	}
+
+	if (interface_num == sd->info->sync_interface) {
+		spin_lock_irqsave(&sd->lock, flags);
+		sd->removed = true;
+		spin_unlock_irqrestore(&sd->lock, flags);
 
-	cancel_delayed_work_sync(&sd->battery_work);
+		cancel_delayed_work_sync(&sd->status_work);
+	}
 
 	hid_hw_close(hdev);
 	hid_hw_stop(hdev);
 }
 
-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_arctis_raw_event(struct hid_device *hdev,
-					struct hid_report *report, u8 *read_buf,
-					int size)
+				 struct hid_report *report, u8 *data, 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;
+	u8 old_capacity;
+	bool old_connected;
+	bool old_charging;
 
-	if (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)
+		return 0;
 
-	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;
-		}
+	old_capacity = sd->battery_capacity;
+	old_connected = sd->headset_connected;
+	old_charging = sd->battery_charging;
 
-		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;
-		}
-	}
+	sd->info->parse_status(sd, data, size);
 
-	if (connected != sd->headset_connected) {
-		hid_dbg(sd->hdev,
+	if (sd->headset_connected != old_connected) {
+		hid_dbg(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);
+			old_connected ? "" : "not ",
+			sd->headset_connected ? "" : "not ");
+
+		if (sd->battery) {
+			steelseries_headset_set_wireless_status(sd->hdev,
+							       sd->headset_connected);
+			power_supply_changed(sd->battery);
+		}
 	}
 
-	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 (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 (charging != sd->battery_charging) {
-		hid_dbg(sd->hdev,
+	if (sd->battery_charging != old_charging) {
+		hid_dbg(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);
+			old_charging ? "" : "not ",
+			sd->battery_charging ? "" : "not ");
+		if (sd->battery)
+			power_supply_changed(sd->battery);
 	}
 
-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;
 }
 
 static const struct hid_device_id steelseries_arctis_devices[] = {
-	{ /* 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_ARCTIS_1_X),
+	  .driver_data = (unsigned long)&arctis_1_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_9),
+	  .driver_data = (unsigned long)&arctis_9_info },
+	{}
 };
 MODULE_DEVICE_TABLE(hid, steelseries_arctis_devices);
 
@@ -402,3 +468,4 @@ MODULE_DESCRIPTION("HID driver for Steelseries arctis headsets");
 MODULE_LICENSE("GPL");
 MODULE_AUTHOR("Christian Mayer <git@mayer-bgk.de>");
 MODULE_AUTHOR("Bastien Nocera <hadess@hadess.net>");
+MODULE_AUTHOR("Sriman Achanta <srimanachanta@gmail.com>");
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [PATCH v4 06/10] HID: steelseries: Report POWER_SUPPLY_STATUS_FULL when full
  2026-06-23 17:23 [PATCH v4 00/10] HID: steelseries: Refactor Arctis driver and add Arctis Nova 7 Gen2 support Sriman Achanta
                   ` (4 preceding siblings ...)
  2026-06-23 17:23 ` [PATCH v4 05/10] HID: steelseries: Refactor Arctis driver to use device_info framework Sriman Achanta
@ 2026-06-23 17:23 ` Sriman Achanta
  2026-06-23 17:23 ` [PATCH v4 07/10] HID: steelseries: Correct Arctis 9 battery calibration range Sriman Achanta
                   ` (3 subsequent siblings)
  9 siblings, 0 replies; 15+ messages in thread
From: Sriman Achanta @ 2026-06-23 17:23 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer,
	Bastien Nocera, Sriman Achanta

Report POWER_SUPPLY_STATUS_FULL when the headset is connected, not
charging, and at 100% capacity. It reported DISCHARGING in that case
before.

Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
 drivers/hid/hid-steelseries-arctis.c | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/drivers/hid/hid-steelseries-arctis.c b/drivers/hid/hid-steelseries-arctis.c
index f00f4c5e6d9e..4be586db0004 100644
--- a/drivers/hid/hid-steelseries-arctis.c
+++ b/drivers/hid/hid-steelseries-arctis.c
@@ -216,6 +216,8 @@ static int steelseries_battery_get_property(struct power_supply *psy,
 			val->intval = POWER_SUPPLY_STATUS_UNKNOWN;
 		else if (sd->battery_charging)
 			val->intval = POWER_SUPPLY_STATUS_CHARGING;
+		else if (sd->battery_capacity >= 100)
+			val->intval = POWER_SUPPLY_STATUS_FULL;
 		else
 			val->intval = POWER_SUPPLY_STATUS_DISCHARGING;
 		break;
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [PATCH v4 07/10] HID: steelseries: Correct Arctis 9 battery calibration range
  2026-06-23 17:23 [PATCH v4 00/10] HID: steelseries: Refactor Arctis driver and add Arctis Nova 7 Gen2 support Sriman Achanta
                   ` (5 preceding siblings ...)
  2026-06-23 17:23 ` [PATCH v4 06/10] HID: steelseries: Report POWER_SUPPLY_STATUS_FULL when full Sriman Achanta
@ 2026-06-23 17:23 ` Sriman Achanta
  2026-06-23 17:23 ` [PATCH v4 08/10] HID: steelseries: Manage battery lifetime with refcounting Sriman Achanta
                   ` (2 subsequent siblings)
  9 siblings, 0 replies; 15+ messages in thread
From: Sriman Achanta @ 2026-06-23 17:23 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer,
	Bastien Nocera, Sriman Achanta

Map the Arctis 9 raw battery value over 0x64 (empty) to 0x9a (full)
instead of 0x68 to 0x9d. These values match the HeadsetControl project
[1] and fit the calibration points from an independent reverse
engineering of the battery tray (about 25% at raw 112, 50% at raw 125)
[2].

I do not have this headset. The values come from those references and
were not measured directly.

[1] https://github.com/Sapd/HeadsetControl/blob/master/lib/devices/steelseries_arctis_9.hpp
[2] https://magnier.io/reverse-engineering-arctis-9-battery-tray/

Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
 drivers/hid/hid-steelseries-arctis.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/drivers/hid/hid-steelseries-arctis.c b/drivers/hid/hid-steelseries-arctis.c
index 4be586db0004..1f0e9cb5138f 100644
--- a/drivers/hid/hid-steelseries-arctis.c
+++ b/drivers/hid/hid-steelseries-arctis.c
@@ -146,7 +146,7 @@ static void steelseries_arctis_9_parse_status(struct steelseries_device *sd,
 	if (data[0] == 0xaa) {
 		sd->headset_connected = (data[1] == 0x01);
 		sd->battery_charging = (data[4] == 0x01);
-		sd->battery_capacity = steelseries_map_capacity(data[3], 0x68, 0x9d);
+		sd->battery_capacity = steelseries_map_capacity(data[3], 0x64, 0x9a);
 	}
 }
 
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [PATCH v4 08/10] HID: steelseries: Manage battery lifetime with refcounting
  2026-06-23 17:23 [PATCH v4 00/10] HID: steelseries: Refactor Arctis driver and add Arctis Nova 7 Gen2 support Sriman Achanta
                   ` (6 preceding siblings ...)
  2026-06-23 17:23 ` [PATCH v4 07/10] HID: steelseries: Correct Arctis 9 battery calibration range Sriman Achanta
@ 2026-06-23 17:23 ` Sriman Achanta
  2026-06-23 17:36   ` sashiko-bot
  2026-06-23 17:23 ` [PATCH v4 09/10] HID: steelseries: Add async status interface support Sriman Achanta
  2026-06-23 17:23 ` [PATCH v4 10/10] HID: steelseries: Add support for Arctis Nova 7 Gen2 family Sriman Achanta
  9 siblings, 1 reply; 15+ messages in thread
From: Sriman Achanta @ 2026-06-23 17:23 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer,
	Bastien Nocera, Sriman Achanta

The next change shares one steelseries_device between two HID
interfaces, so the state can outlive either interface. Stop using devm
for it. Reference count the struct with a kref and free it from
steelseries_device_release(). Register and unregister the power supply
explicitly, and clear sd->battery under sd->lock in remove() so it is
not touched after it is unregistered.

Drop the global atomic battery counter and name the power supply after
the device (hdev->uniq, or dev_name() when empty), as hid-input and the
other HID battery drivers do.

No functional change for the current single-interface devices.

Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
 drivers/hid/hid-steelseries-arctis.c | 36 ++++++++++++++++++++++------
 1 file changed, 29 insertions(+), 7 deletions(-)

diff --git a/drivers/hid/hid-steelseries-arctis.c b/drivers/hid/hid-steelseries-arctis.c
index 1f0e9cb5138f..734cf1eb8789 100644
--- a/drivers/hid/hid-steelseries-arctis.c
+++ b/drivers/hid/hid-steelseries-arctis.c
@@ -8,6 +8,8 @@
 
 #include <linux/device.h>
 #include <linux/hid.h>
+#include <linux/kref.h>
+#include <linux/slab.h>
 #include <linux/module.h>
 #include <linux/power_supply.h>
 #include <linux/spinlock.h>
@@ -30,6 +32,8 @@ struct steelseries_device_info {
 };
 
 struct steelseries_device {
+	struct kref refcnt;
+
 	struct hid_device *hdev;
 	const struct steelseries_device_info *info;
 
@@ -45,6 +49,14 @@ struct steelseries_device {
 	bool removed;
 };
 
+static void steelseries_device_release(struct kref *ref)
+{
+	struct steelseries_device *sd =
+		container_of(ref, struct steelseries_device, refcnt);
+
+	kfree(sd);
+}
+
 /*
  * Headset report helpers
  */
@@ -268,9 +280,7 @@ static void steelseries_status_timer_work_handler(struct work_struct *work)
 
 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, };
-	unsigned long n;
 	int ret;
 
 	sd->battery_desc.type = POWER_SUPPLY_TYPE_BATTERY;
@@ -278,9 +288,10 @@ static int steelseries_battery_register(struct steelseries_device *sd)
 	sd->battery_desc.num_properties = ARRAY_SIZE(steelseries_battery_props);
 	sd->battery_desc.get_property = steelseries_battery_get_property;
 	sd->battery_desc.use_for_apm = 0;
-	n = atomic_inc_return(&battery_no) - 1;
 	sd->battery_desc.name = devm_kasprintf(&sd->hdev->dev, GFP_KERNEL,
-						"steelseries_headset_battery_%ld", n);
+					       "steelseries_headset_battery_%s",
+					       sd->hdev->uniq[0] ? sd->hdev->uniq :
+					       dev_name(&sd->hdev->dev));
 	if (!sd->battery_desc.name)
 		return -ENOMEM;
 
@@ -290,7 +301,7 @@ static int steelseries_battery_register(struct steelseries_device *sd)
 	sd->headset_connected = false;
 	steelseries_headset_set_wireless_status(sd->hdev, false);
 
-	sd->battery = devm_power_supply_register(&sd->hdev->dev,
+	sd->battery = power_supply_register(&sd->hdev->dev,
 			&sd->battery_desc, &battery_cfg);
 	if (IS_ERR(sd->battery)) {
 		ret = PTR_ERR(sd->battery);
@@ -330,10 +341,11 @@ static int steelseries_arctis_probe(struct hid_device *hdev,
 	if (interface_num != info->sync_interface)
 		return hid_hw_start(hdev, HID_CONNECT_DEFAULT);
 
-	sd = devm_kzalloc(&hdev->dev, sizeof(*sd), GFP_KERNEL);
+	sd = kzalloc_obj(*sd, GFP_KERNEL);
 	if (!sd)
 		return -ENOMEM;
 
+	kref_init(&sd->refcnt);
 	sd->hdev = hdev;
 	sd->info = info;
 	spin_lock_init(&sd->lock);
@@ -342,7 +354,7 @@ static int steelseries_arctis_probe(struct hid_device *hdev,
 
 	ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
 	if (ret)
-		return ret;
+		goto err_put;
 
 	ret = hid_hw_open(hdev);
 	if (ret)
@@ -361,12 +373,15 @@ static int steelseries_arctis_probe(struct hid_device *hdev,
 
 err_stop:
 	hid_hw_stop(hdev);
+err_put:
+	kref_put(&sd->refcnt, steelseries_device_release);
 	return ret;
 }
 
 static void steelseries_arctis_remove(struct hid_device *hdev)
 {
 	struct steelseries_device *sd;
+	struct power_supply *battery;
 	unsigned long flags;
 	struct usb_interface *intf;
 	u8 interface_num;
@@ -388,13 +403,20 @@ static void steelseries_arctis_remove(struct hid_device *hdev)
 	if (interface_num == sd->info->sync_interface) {
 		spin_lock_irqsave(&sd->lock, flags);
 		sd->removed = true;
+		battery = sd->battery;
+		sd->battery = NULL;
 		spin_unlock_irqrestore(&sd->lock, flags);
 
 		cancel_delayed_work_sync(&sd->status_work);
+
+		if (battery)
+			power_supply_unregister(battery);
 	}
 
 	hid_hw_close(hdev);
 	hid_hw_stop(hdev);
+
+	kref_put(&sd->refcnt, steelseries_device_release);
 }
 
 static int steelseries_arctis_raw_event(struct hid_device *hdev,
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [PATCH v4 09/10] HID: steelseries: Add async status interface support
  2026-06-23 17:23 [PATCH v4 00/10] HID: steelseries: Refactor Arctis driver and add Arctis Nova 7 Gen2 support Sriman Achanta
                   ` (7 preceding siblings ...)
  2026-06-23 17:23 ` [PATCH v4 08/10] HID: steelseries: Manage battery lifetime with refcounting Sriman Achanta
@ 2026-06-23 17:23 ` Sriman Achanta
  2026-06-23 17:36   ` sashiko-bot
  2026-06-23 17:23 ` [PATCH v4 10/10] HID: steelseries: Add support for Arctis Nova 7 Gen2 family Sriman Achanta
  9 siblings, 1 reply; 15+ messages in thread
From: Sriman Achanta @ 2026-06-23 17:23 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer,
	Bastien Nocera, Sriman Achanta

Some headsets expose a second HID interface that sends battery and
connection updates on its own. Watching that interface lets the driver
stop polling the sync interface. Add a
steelseries_device_info::async_interface field and the code to handle
it:

  - The driver binds both the sync and async interfaces. The async
    interface shares the steelseries_device created by the sync
    interface. It finds the sibling with usb_ifnum_to_if(), takes a
    reference, and returns -EPROBE_DEFER until the sync interface has
    probed. If the sync interface never binds, the async interface
    defers forever, which is fine here.
  - raw_event() now holds sd->lock and re-checks sd->removed so events
    on either interface are serialised against removal.
  - status_work runs once for async devices instead of rearming. A
    single status request is sent when the headset connects to get the
    initial battery level.

No device sets async_interface yet. This is the infrastructure for the
next commit.

Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
 drivers/hid/hid-steelseries-arctis.c | 121 +++++++++++++++++++++------
 1 file changed, 97 insertions(+), 24 deletions(-)

diff --git a/drivers/hid/hid-steelseries-arctis.c b/drivers/hid/hid-steelseries-arctis.c
index 734cf1eb8789..2208d0e4cd2a 100644
--- a/drivers/hid/hid-steelseries-arctis.c
+++ b/drivers/hid/hid-steelseries-arctis.c
@@ -26,6 +26,7 @@ struct steelseries_device_info {
 	unsigned long capabilities;
 
 	u8 sync_interface;
+	u8 async_interface;
 
 	int (*request_status)(struct hid_device *hdev);
 	void (*parse_status)(struct steelseries_device *sd, u8 *data, int size);
@@ -272,7 +273,8 @@ static void steelseries_status_timer_work_handler(struct work_struct *work)
 	sd->info->request_status(sd->hdev);
 
 	spin_lock_irqsave(&sd->lock, flags);
-	if (!sd->removed)
+	/* Async devices push status events themselves; only poll once. */
+	if (!sd->removed && !sd->info->async_interface)
 		schedule_delayed_work(&sd->status_work,
 				msecs_to_jiffies(STEELSERIES_HEADSET_STATUS_TIMEOUT_MS));
 	spin_unlock_irqrestore(&sd->lock, flags);
@@ -316,6 +318,23 @@ static int steelseries_battery_register(struct steelseries_device *sd)
 	return 0;
 }
 
+static struct hid_device *steelseries_get_sibling_hdev(struct hid_device *hdev,
+						       int interface_num)
+{
+	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_arctis_probe(struct hid_device *hdev,
 				    const struct hid_device_id *id)
 {
@@ -337,39 +356,76 @@ static int steelseries_arctis_probe(struct hid_device *hdev,
 	if (ret)
 		return ret;
 
-	/* Let hid-generic handle non-sync interfaces */
-	if (interface_num != info->sync_interface)
+	/* 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);
 
-	sd = kzalloc_obj(*sd, GFP_KERNEL);
-	if (!sd)
-		return -ENOMEM;
+	if (interface_num == info->sync_interface) {
+		sd = kzalloc_obj(*sd, GFP_KERNEL);
+		if (!sd)
+			return -ENOMEM;
 
-	kref_init(&sd->refcnt);
-	sd->hdev = hdev;
-	sd->info = info;
-	spin_lock_init(&sd->lock);
+		kref_init(&sd->refcnt);
+		sd->hdev = hdev;
+		sd->info = info;
+		spin_lock_init(&sd->lock);
 
-	hid_set_drvdata(hdev, sd);
+		hid_set_drvdata(hdev, sd);
 
-	ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
-	if (ret)
-		goto err_put;
+		ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+		if (ret)
+			goto err_put;
 
-	ret = hid_hw_open(hdev);
-	if (ret)
-		goto err_stop;
+		ret = hid_hw_open(hdev);
+		if (ret)
+			goto err_stop;
+
+		if (info->capabilities & SS_CAP_BATTERY) {
+			ret = steelseries_battery_register(sd);
+			if (ret < 0)
+				hid_warn(hdev, "Failed to register battery: %d\n", ret);
+		}
 
-	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;
 	}
 
-	INIT_DELAYED_WORK(&sd->status_work, steelseries_status_timer_work_handler);
-	schedule_delayed_work(&sd->status_work, msecs_to_jiffies(100));
+	/*
+	 * The async interface shares the steelseries_device created by the
+	 * sync interface. Defer until the sync interface has probed and
+	 * published its drvdata.
+	 */
+	if (info->async_interface && interface_num == info->async_interface) {
+		struct hid_device *master_hdev;
 
-	return 0;
+		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);
+		kref_get(&sd->refcnt);
+		hid_set_drvdata(hdev, sd);
+
+		ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+		if (ret) {
+			kref_put(&sd->refcnt, steelseries_device_release);
+			return ret;
+		}
+
+		ret = hid_hw_open(hdev);
+		if (ret) {
+			hid_hw_stop(hdev);
+			kref_put(&sd->refcnt, steelseries_device_release);
+			return ret;
+		}
+		return 0;
+	}
+
+	return -ENODEV;
 
 err_stop:
 	hid_hw_stop(hdev);
@@ -426,10 +482,21 @@ static int steelseries_arctis_raw_event(struct hid_device *hdev,
 	u8 old_capacity;
 	bool old_connected;
 	bool old_charging;
+	bool is_async_interface;
+	unsigned long flags;
 
 	if (!sd)
 		return 0;
 
+	is_async_interface = (hdev != sd->hdev);
+
+	spin_lock_irqsave(&sd->lock, flags);
+
+	if (sd->removed) {
+		spin_unlock_irqrestore(&sd->lock, flags);
+		return 0;
+	}
+
 	old_capacity = sd->battery_capacity;
 	old_connected = sd->headset_connected;
 	old_charging = sd->battery_charging;
@@ -442,6 +509,10 @@ static int steelseries_arctis_raw_event(struct hid_device *hdev,
 			old_connected ? "" : "not ",
 			sd->headset_connected ? "" : "not ");
 
+		if (sd->headset_connected && !old_connected &&
+		    sd->info->async_interface && is_async_interface)
+			schedule_delayed_work(&sd->status_work, 0);
+
 		if (sd->battery) {
 			steelseries_headset_set_wireless_status(sd->hdev,
 							       sd->headset_connected);
@@ -465,6 +536,8 @@ static int steelseries_arctis_raw_event(struct hid_device *hdev,
 			power_supply_changed(sd->battery);
 	}
 
+	spin_unlock_irqrestore(&sd->lock, flags);
+
 	return 0;
 }
 
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [PATCH v4 10/10] HID: steelseries: Add support for Arctis Nova 7 Gen2 family
  2026-06-23 17:23 [PATCH v4 00/10] HID: steelseries: Refactor Arctis driver and add Arctis Nova 7 Gen2 support Sriman Achanta
                   ` (8 preceding siblings ...)
  2026-06-23 17:23 ` [PATCH v4 09/10] HID: steelseries: Add async status interface support Sriman Achanta
@ 2026-06-23 17:23 ` Sriman Achanta
  2026-06-23 17:35   ` sashiko-bot
  9 siblings, 1 reply; 15+ messages in thread
From: Sriman Achanta @ 2026-06-23 17:23 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Simon Wood, Christian Mayer,
	Bastien Nocera, Sriman Achanta

The Arctis Nova 7 Gen2 headsets answer status polls, but they also send
unsolicited battery and connection updates on a second HID interface
(interface 5). Use that interface through async_interface so the driver
does not have to poll. Add request and parse helpers for the Gen2 status
format (0xb0/0xb7/0xb9/0xbb opcodes).

Add the eight USB product IDs that share this protocol:

  0x22a1  Arctis Nova 7 2026
  0x22a7  Arctis Nova 7P 2026
  0x22a5  Arctis Nova 7X 2026
  0x22a9  Arctis Nova 7 Diablo 2026
  0x227e  Arctis Nova 7 Gen 2
  0x2258  Arctis Nova 7X Gen 2
  0x229e  Arctis Nova 7X Gen 2 (alternate PID)
  0x22ad  Arctis Nova 7X Gen 2 (alternate PID)

Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
 drivers/hid/hid-ids.h                |  8 ++++
 drivers/hid/hid-quirks.c             |  8 ++++
 drivers/hid/hid-steelseries-arctis.c | 63 ++++++++++++++++++++++++++++
 3 files changed, 79 insertions(+)

diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 915e936cbf8b..6b5be86b1bdf 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1379,6 +1379,14 @@
 #define USB_DEVICE_ID_STEELSERIES_SRWS1	0x1410
 #define USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X	0x12b6
 #define USB_DEVICE_ID_STEELSERIES_ARCTIS_9	0x12c2
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_2026	0x22a1
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_P_2026	0x22a7
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_2026	0x22a5
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO_2026	0x22a9
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_GEN2	0x227e
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2	0x2258
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2_2	0x229e
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2_3	0x22ad
 
 #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 f546179858c2..40a761b5df91 100644
--- a/drivers/hid/hid-quirks.c
+++ b/drivers/hid/hid-quirks.c
@@ -749,6 +749,14 @@ 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_X) },
 	{ 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_7_2026) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_P_2026) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_2026) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO_2026) },
+	{ 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_7_X_GEN2_3) },
 #endif
 #if IS_ENABLED(CONFIG_HID_SUNPLUS)
 	{ HID_USB_DEVICE(USB_VENDOR_ID_SUNPLUS, USB_DEVICE_ID_SUNPLUS_WDESKTOP) },
diff --git a/drivers/hid/hid-steelseries-arctis.c b/drivers/hid/hid-steelseries-arctis.c
index 2208d0e4cd2a..eecd384b1a33 100644
--- a/drivers/hid/hid-steelseries-arctis.c
+++ b/drivers/hid/hid-steelseries-arctis.c
@@ -114,6 +114,13 @@ static int steelseries_arctis_9_request_status(struct hid_device *hdev)
 	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));
+}
+
 /*
  * Headset battery helpers
  */
@@ -163,6 +170,30 @@ static void steelseries_arctis_9_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 < 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;
+	}
+}
+
 /*
  * Device info definitions
  */
@@ -181,6 +212,14 @@ static const struct steelseries_device_info arctis_9_info = {
 	.parse_status = steelseries_arctis_9_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,
+};
+
 /*
  * Headset wireless status and battery infrastructure
  */
@@ -548,6 +587,30 @@ static const struct hid_device_id steelseries_arctis_devices[] = {
 	{ 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_7_2026),
+	  .driver_data = (unsigned long)&arctis_nova_7_gen2_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_P_2026),
+	  .driver_data = (unsigned long)&arctis_nova_7_gen2_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_2026),
+	  .driver_data = (unsigned long)&arctis_nova_7_gen2_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO_2026),
+	  .driver_data = (unsigned long)&arctis_nova_7_gen2_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_7_X_GEN2_3),
+	  .driver_data = (unsigned long)&arctis_nova_7_gen2_info },
 	{}
 };
 MODULE_DEVICE_TABLE(hid, steelseries_arctis_devices);
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* Re: [PATCH v4 05/10] HID: steelseries: Refactor Arctis driver to use device_info framework
  2026-06-23 17:23 ` [PATCH v4 05/10] HID: steelseries: Refactor Arctis driver to use device_info framework Sriman Achanta
@ 2026-06-23 17:33   ` sashiko-bot
  0 siblings, 0 replies; 15+ messages in thread
From: sashiko-bot @ 2026-06-23 17:33 UTC (permalink / raw)
  To: Sriman Achanta; +Cc: linux-input, dmitry.torokhov

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:

New issues:
- [High] Missing report ID signature validation for Arctis 1 causes arbitrary raw events to corrupt headset state.
- [High] Battery status requests are incorrectly sent as Feature reports instead of Output reports, breaking battery polling.
- [Medium] Dropped logic for marking Arctis 9 headset as disconnected when it reports an unknown status.

Pre-existing issues:
- [High] The raw_event interrupt handler can dereference an ERR_PTR due to a TOCTOU race window when battery registration fails.
--

commit 0576cf59ec529dc31ed68f59a8d598bdb4ca4a82
Author: Sriman Achanta <srimanachanta@gmail.com>

HID: steelseries: Refactor Arctis driver to use device_info framework

Replace the per-product if/else quirk bitmap with a steelseries_device_info
struct. Each model provides its capabilities, sync_interface, and
request_status/parse_status callbacks.

> diff --git a/drivers/hid/hid-steelseries-arctis.c b/drivers/hid/hid-steelseries-arctis.c
> index 079504e6932a1..f00f4c5e6d9e4 100644
> --- a/drivers/hid/hid-steelseries-arctis.c
> +++ b/drivers/hid/hid-steelseries-arctis.c
[ ... ]
> @@ -15,70 +16,95 @@
[ ... ]
> +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_9_request_status(struct hid_device *hdev)
> +{
> +	const u8 data[] = { 0x00, 0x20 };
> +
> +	return steelseries_send_feature_report(hdev, data, sizeof(data));
> +}

[Severity: High]
Do these devices expect feature reports instead of output reports for status
requests?

The original code sent the periodic battery status request to the device
using an output report via hid_hw_raw_request(..., HID_OUTPUT_REPORT, ...).
This refactoring switches it to use steelseries_send_feature_report()
instead.

Sending a feature report to a device endpoint expecting an output report
might result in a USB stall, which would break battery polling.

[ ... ]
> @@ -88,30 +114,96 @@ static int battery_capacity_to_level(int capacity)
[ ... ]
> +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];
> +}

[Severity: High]
Will this corrupt the battery state if other reports are received?

The original raw event handler verified that the incoming Arctis 1 report
matched the expected battery status signature using memcmp().

Since steelseries_arctis_raw_event() now invokes parse_status()
unconditionally for all incoming raw events, could a different report
(such as volume control) with a payload of at least 4 bytes be erroneously
parsed as battery data?

> +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], 0x68, 0x9d);
> +	}
> +}

[Severity: Medium]
What happens if the device is turned off and sends the "no known status"
packet?

The original code explicitly handled the 0x55 response and used it to mark
the headset as disconnected.

By only processing 0xaa and dropping other responses without taking any
action, will we fail to update the connection state and leave the device
incorrectly reported as connected?

[ ... ]
> @@ -168,7 +245,26 @@ static enum power_supply_property steelseries_headset_battery_props[] = {
[ ... ]
> +static int steelseries_battery_register(struct steelseries_device *sd)
>  {
[ ... ]
>  	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;

[Severity: High]
This isn't a bug introduced by this patch, but does assigning the return
value directly to sd->battery before checking for an error create a race
condition?

Since hid_hw_start() and hid_hw_open() are already called in the probe
function, hardware interrupts are active. If power_supply_register() fails,
sd->battery temporarily holds an error pointer.

If steelseries_arctis_raw_event() triggers during this small window, the
following check would evaluate to true because an error pointer is not NULL:

steelseries_arctis_raw_event() {
    ...
    if (sd->battery)
        power_supply_changed(sd->battery);
}

Could this lead to an invalid pointer dereference and a kernel panic?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260623172310.272708-1-srimanachanta@gmail.com?part=5

^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [PATCH v4 10/10] HID: steelseries: Add support for Arctis Nova 7 Gen2 family
  2026-06-23 17:23 ` [PATCH v4 10/10] HID: steelseries: Add support for Arctis Nova 7 Gen2 family Sriman Achanta
@ 2026-06-23 17:35   ` sashiko-bot
  0 siblings, 0 replies; 15+ messages in thread
From: sashiko-bot @ 2026-06-23 17:35 UTC (permalink / raw)
  To: Sriman Achanta; +Cc: linux-input, dmitry.torokhov

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:

New issues:
- [Medium] The unconditional minimum size check in the Gen2 parsing function may silently drop valid unsolicited status updates.

Pre-existing issues:
- [High] Unsafe fetching of the sibling interface's private data lacks driver validation and synchronization, leading to Type Confusion and Use-After-Free.
--

commit ca415cb624472143e783db3389a867c4e203e6c8
Author: Sriman Achanta <srimanachanta@gmail.com>

HID: steelseries: Add support for Arctis Nova 7 Gen2 family

This commit adds support for the Arctis Nova 7 Gen2 headsets.
It introduces a new parsing helper for the Gen2 status format and uses an
async interface to handle unsolicited battery and connection updates without
polling.

> diff --git a/drivers/hid/hid-steelseries-arctis.c b/drivers/hid/hid-steelseries-arctis.c
> --- a/drivers/hid/hid-steelseries-arctis.c
> +++ b/drivers/hid/hid-steelseries-arctis.c
[ ... ]
> @@ -163,6 +170,30 @@ static void steelseries_arctis_9_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 < 4)
> +		return;

[Severity: Medium]
Will this strict minimum size check silently drop valid short packets?

Since opcodes like 0xb7, 0xb9, and 0xbb only appear to require 2 bytes of
payload (e.g., data[0] and data[1]), if the hardware sends an unsolicited
report as a 2 or 3 byte packet, it seems it would be dropped before
processing.

> +
> +	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;
> +	}
> +}
> +
>  /*
>   * Device info definitions
>   */
> @@ -181,6 +212,14 @@ static const struct steelseries_device_info arctis_9_info = {
>  	.parse_status = steelseries_arctis_9_parse_status,
>  };
>  
> +static const struct steelseries_device_info arctis_nova_7_gen2_info = {
> +	.sync_interface = 3,
> +	.async_interface = 5,

[Severity: High]
This isn't a bug introduced by this patch, but since these devices use an
async interface, does the sibling interface lookup in 
steelseries_get_sibling_hdev() have adequate locking and validation?

When a device probes the async interface, it appears
steelseries_get_sibling_hdev() fetches intfdata from the sibling interface
via usb_get_intfdata() locklessly.

If the sibling interface (interface 3) is unbound via sysfs and bound to
another driver (like usbfs), or physically unplugged concurrently, could
this return a foreign private context or a soon-to-be-freed pointer?

Then in steelseries_arctis_probe(), this unvalidated pointer is cast and
dereferenced to get the driver data:

    sd = hid_get_drvdata(master_hdev);
    kref_get(&sd->refcnt);

Could this lead to type confusion or a use-after-free if the sibling driver
is no longer the expected usbhid driver?

> +	.capabilities = SS_CAP_BATTERY,
> +	.request_status = steelseries_arctis_nova_request_status,
> +	.parse_status = steelseries_arctis_nova_7_gen2_parse_status,
> +};
> +

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260623172310.272708-1-srimanachanta@gmail.com?part=10

^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [PATCH v4 09/10] HID: steelseries: Add async status interface support
  2026-06-23 17:23 ` [PATCH v4 09/10] HID: steelseries: Add async status interface support Sriman Achanta
@ 2026-06-23 17:36   ` sashiko-bot
  0 siblings, 0 replies; 15+ messages in thread
From: sashiko-bot @ 2026-06-23 17:36 UTC (permalink / raw)
  To: Sriman Achanta; +Cc: dmitry.torokhov, linux-input

Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:

New issues:
- [Critical] The driver blindly casts the sibling interface's driver data to a `struct hid_device` without verifying the bound driver, leading to type confusion and memory corruption.
- [Critical] The sync interface publishes `sd` to `drvdata` before initialization is complete, allowing the async interface to crash the kernel by scheduling an uninitialized work struct.
- [High] Lockless retrieval and access of the sibling interface's driver data leads to a Use-After-Free if the sync interface is unbound concurrently.
- [High] If the sync interface probe fails after the async interface binds to it, the async interface will access a freed `sd->hdev` pointer.

Pre-existing issues:
- [High] An ERR_PTR temporarily assigned to `sd->battery` during initialization failure can be concurrently dereferenced by the raw event handler.
--

commit 002dd61306f7255151ab901886b17a9e612226ad
Author: Sriman Achanta <srimanachanta@gmail.com>
Subject: HID: steelseries: Add async status interface support

This commit adds support for a second HID interface that sends battery
and connection updates asynchronously. This allows the driver to stop
polling the sync interface by watching the async interface instead.

> diff --git a/drivers/hid/hid-steelseries-arctis.c b/drivers/hid/hid-steelseries-arctis.c
> index 734cf1eb87894..2208d0e4cd2ad 100644
> --- a/drivers/hid/hid-steelseries-arctis.c
> +++ b/drivers/hid/hid-steelseries-arctis.c
[ ... ]
> @@ -316,6 +318,23 @@ static int steelseries_battery_register(struct steelseries_device *sd)
>  	return 0;
>  }
>  
> +static struct hid_device *steelseries_get_sibling_hdev(struct hid_device *hdev,
> +						       int interface_num)
> +{
> +	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);

[Severity: High]
Does this code safely retrieve the sibling interface's driver data? Because
no reference is taken on the USB interface and usb_driver_claim_interface()
is not used, could a concurrent sysfs unbind destroy the interface and the
retrieved data while we are accessing it?

[Severity: Critical]
Is it safe to assume usb_get_intfdata() returns a struct hid_device pointer
here? If the sync interface is bound to a different driver (such as
snd-usb-audio), its driver data would be a completely different structure.
Could this lead to type confusion and memory corruption when the pointer is
dereferenced later in the probe function?

> +
> +	return sibling_hdev;
> +}
> +
>  static int steelseries_arctis_probe(struct hid_device *hdev,
>  				    const struct hid_device_id *id)
>  {
> @@ -337,39 +356,76 @@ static int steelseries_arctis_probe(struct hid_device *hdev,
>  	if (ret)
>  		return ret;
>  
> -	/* Let hid-generic handle non-sync interfaces */
> -	if (interface_num != info->sync_interface)
> +	/* 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);
>  
> -	sd = kzalloc_obj(*sd, GFP_KERNEL);
> -	if (!sd)
> -		return -ENOMEM;
> +	if (interface_num == info->sync_interface) {
> +		sd = kzalloc_obj(*sd, GFP_KERNEL);
> +		if (!sd)
> +			return -ENOMEM;
>  
> -	kref_init(&sd->refcnt);
> -	sd->hdev = hdev;
> -	sd->info = info;
> -	spin_lock_init(&sd->lock);
> +		kref_init(&sd->refcnt);
> +		sd->hdev = hdev;
> +		sd->info = info;
> +		spin_lock_init(&sd->lock);
>  
> -	hid_set_drvdata(hdev, sd);
> +		hid_set_drvdata(hdev, sd);

[Severity: Critical]
By calling hid_set_drvdata() here, sd is globally published before
INIT_DELAYED_WORK() is called below. If the async interface probes concurrently
and immediately receives an event, could it call schedule_delayed_work() on
an uninitialized work struct?

>  
> -	ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
> -	if (ret)
> -		goto err_put;
> +		ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
> +		if (ret)
> +			goto err_put;
>  
> -	ret = hid_hw_open(hdev);
> -	if (ret)
> -		goto err_stop;
> +		ret = hid_hw_open(hdev);
> +		if (ret)
> +			goto err_stop;

[Severity: High]
If the sync interface probe fails after this point (for example, if hid_hw_open()
or steelseries_battery_register() fails) and returns an error, but the async
interface has already bound and incremented sd->refcnt:

Since steelseries_arctis_remove() is never called for the failed sync probe,
sd->removed remains false. If the driver core clears the sync interface's driver
data and the sibling interface is unbound, sd->hdev is freed.

Could the still-active async interface then process raw events, bypass the
sd->removed check, and access the freed sd->hdev pointer via
steelseries_headset_set_wireless_status()?

> +
> +		if (info->capabilities & SS_CAP_BATTERY) {
> +			ret = steelseries_battery_register(sd);
> +			if (ret < 0)
> +				hid_warn(hdev, "Failed to register battery: %d\n", ret);
> +		}
>  
> -	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;
>  	}
[ ... ]
> @@ -442,6 +509,10 @@ static int steelseries_arctis_raw_event(struct hid_device *hdev,
>  			old_connected ? "" : "not ",
>  			sd->headset_connected ? "" : "not ");
>  
> +		if (sd->headset_connected && !old_connected &&
> +		    sd->info->async_interface && is_async_interface)
> +			schedule_delayed_work(&sd->status_work, 0);
> +
>  		if (sd->battery) {
>  			steelseries_headset_set_wireless_status(sd->hdev,
>  							       sd->headset_connected);
>  			power_supply_changed(sd->battery);

[Severity: High]
This is a pre-existing issue, but is there a data race with sd->battery here?

During initialization in steelseries_battery_register():

	sd->battery = power_supply_register(&sd->hdev->dev,
			&sd->battery_desc, &battery_cfg);
	if (IS_ERR(sd->battery)) {
		ret = PTR_ERR(sd->battery);
		sd->battery = NULL;

Since hardware has already been enabled via hid_hw_start(), interrupts can
arrive concurrently. If an event arrives during the window where sd->battery
holds an ERR_PTR, if (sd->battery) will evaluate to true.

Could this cause power_supply_changed(sd->battery) to dereference the error
pointer?

>  		}
>  
> @@ -465,6 +536,8 @@ static int steelseries_arctis_raw_event(struct hid_device *hdev,
>  			power_supply_changed(sd->battery);
>  	}
>  
> +	spin_unlock_irqrestore(&sd->lock, flags);
> +
>  	return 0;
>  }

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260623172310.272708-1-srimanachanta@gmail.com?part=9

^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [PATCH v4 08/10] HID: steelseries: Manage battery lifetime with refcounting
  2026-06-23 17:23 ` [PATCH v4 08/10] HID: steelseries: Manage battery lifetime with refcounting Sriman Achanta
@ 2026-06-23 17:36   ` sashiko-bot
  0 siblings, 0 replies; 15+ messages in thread
From: sashiko-bot @ 2026-06-23 17:36 UTC (permalink / raw)
  To: Sriman Achanta; +Cc: linux-input, dmitry.torokhov

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] Dangling pointer left in device driver data on probe error path leads to Use-After-Free.
- [High] Race condition in sibling interface lookup during probe leading to Use-After-Free of `hid_device`.
--

commit 5a206f2162dc642386285174c7af698f767dad15
Author: Sriman Achanta <srimanachanta@gmail.com>

HID: steelseries: Manage battery lifetime with refcounting

The next change shares one steelseries_device between two HID interfaces,
so the state can outlive either interface. Stop using devm for it. Reference
count the struct with a kref and free it from steelseries_device_release().

> diff --git a/drivers/hid/hid-steelseries-arctis.c b/drivers/hid/hid-steelseries-arctis.c
> index 1f0e9cb5138f6..734cf1eb87894 100644
> --- a/drivers/hid/hid-steelseries-arctis.c
> +++ b/drivers/hid/hid-steelseries-arctis.c
[ ... ]
> @@ -361,12 +373,15 @@ static int steelseries_arctis_probe(struct hid_device *hdev,
>  
>  err_stop:
>  	hid_hw_stop(hdev);
> +err_put:
> +	kref_put(&sd->refcnt, steelseries_device_release);

[Severity: High]
Does this error path leave a dangling pointer in the device's driver data?

If hid_hw_start() or hid_hw_open() fails during the sync interface probe,
the driver explicitly frees the sd struct via kref_put(). However, it leaves
a dangling pointer because hid_set_drvdata(hdev, sd) was already called
earlier and is not reverted here.

If the async interface is probed concurrently (e.g., via sysfs bind),
it calls steelseries_get_sibling_hdev() to find the sync interface's HID
device and reads its driver data. It might receive this dangling pointer
and call kref_get() on freed memory, resulting in a use-after-free.

>  	return ret;
>  }

[Severity: High]
Is there a race condition when looking up the sibling interface during probe
that could lead to a use-after-free of the hid_device?

When probing the async interface, steelseries_get_sibling_hdev() retrieves
the sibling interface's hid_device pointer locklessly:

drivers/hid/hid-steelseries-arctis.c:steelseries_get_sibling_hdev() {
    ...
    sibling_hdev = usb_get_intfdata(sibling_intf);
    return sibling_hdev;
}

Then in steelseries_arctis_probe():

    master_hdev = steelseries_get_sibling_hdev(hdev, info->sync_interface);
    if (!master_hdev || !hid_get_drvdata(master_hdev))
        return -EPROBE_DEFER;

If the sync interface is unbound concurrently (e.g., via sysfs unbind),
hid_destroy_device() can free the hid_device. If the async probe reads
the intfdata pointer just before it is cleared and dereferences it via
hid_get_drvdata(master_hdev), it accesses freed memory. The HID device
probe path does not seem to hold the parent USB device lock to prevent
this concurrent execution.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260623172310.272708-1-srimanachanta@gmail.com?part=8

^ permalink raw reply	[flat|nested] 15+ messages in thread

end of thread, other threads:[~2026-06-23 17:36 UTC | newest]

Thread overview: 15+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-23 17:23 [PATCH v4 00/10] HID: steelseries: Refactor Arctis driver and add Arctis Nova 7 Gen2 support Sriman Achanta
2026-06-23 17:23 ` [PATCH v4 01/10] HID: steelseries: Fix ARCTIS_1_X device mislabeling Sriman Achanta
2026-06-23 17:23 ` [PATCH v4 02/10] HID: steelseries: Fix whitespace in srws1 report descriptor Sriman Achanta
2026-06-23 17:23 ` [PATCH v4 03/10] HID: steelseries: Split Arctis headset driver into separate module Sriman Achanta
2026-06-23 17:23 ` [PATCH v4 04/10] HID: steelseries: Inline and simplify SRWS1 wheel driver Sriman Achanta
2026-06-23 17:23 ` [PATCH v4 05/10] HID: steelseries: Refactor Arctis driver to use device_info framework Sriman Achanta
2026-06-23 17:33   ` sashiko-bot
2026-06-23 17:23 ` [PATCH v4 06/10] HID: steelseries: Report POWER_SUPPLY_STATUS_FULL when full Sriman Achanta
2026-06-23 17:23 ` [PATCH v4 07/10] HID: steelseries: Correct Arctis 9 battery calibration range Sriman Achanta
2026-06-23 17:23 ` [PATCH v4 08/10] HID: steelseries: Manage battery lifetime with refcounting Sriman Achanta
2026-06-23 17:36   ` sashiko-bot
2026-06-23 17:23 ` [PATCH v4 09/10] HID: steelseries: Add async status interface support Sriman Achanta
2026-06-23 17:36   ` sashiko-bot
2026-06-23 17:23 ` [PATCH v4 10/10] HID: steelseries: Add support for Arctis Nova 7 Gen2 family Sriman Achanta
2026-06-23 17:35   ` sashiko-bot

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox