Linux Input/HID development
 help / color / mirror / Atom feed
* [PATCH v3 12/18] HID: steelseries: Add mic volume ALSA mixer control
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
	Christian Mayer, Sriman Achanta
In-Reply-To: <20260227235042.410062-1-srimanachanta@gmail.com>

Expose microphone gain as a writable ALSA integer mixer control
("Mic Volume"). The valid range is per-device; mic_volume_min and
mic_volume_max are added to the device info struct to accommodate models
with a non-zero minimum (e.g. the Arctis Nova Pro, which has a range
of 1-10).

The write command (0x37) is added to the Nova 3P, Nova 5, and Nova Pro
write handlers. On the Nova Pro, the current mic volume is recovered
from the 0x06/0xb0 settings response via a new parse_settings hook.

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

diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index 2bdf772432d0..1339f965f67f 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -28,10 +28,12 @@
 #define SS_CAP_BT_DEVICE_CONNECTED	BIT(4)
 #define SS_CAP_EXTERNAL_CONFIG		BIT(5)
 #define SS_CAP_SIDETONE			BIT(6)
+#define SS_CAP_MIC_VOLUME		BIT(7)
 
 #define SS_QUIRK_STATUS_SYNC_POLL	BIT(0)
 
 #define SS_SETTING_SIDETONE		0
+#define SS_SETTING_MIC_VOLUME		1
 
 struct steelseries_device;
 
@@ -43,6 +45,8 @@ struct steelseries_device_info {
 	u8 async_interface;
 
 	u8 sidetone_max;
+	u8 mic_volume_min;
+	u8 mic_volume_max;
 
 	int (*request_status)(struct hid_device *hdev);
 	void (*parse_status)(struct steelseries_device *sd, u8 *data, int size);
@@ -72,10 +76,12 @@ struct steelseries_device {
 	struct snd_ctl_elem_id chatmix_game_id;
 	struct snd_ctl_elem_id mic_muted_id;
 	struct snd_ctl_elem_id sidetone_id;
+	struct snd_ctl_elem_id mic_volume_id;
 	u8 chatmix_chat;
 	u8 chatmix_game;
 	bool mic_muted;
 	u8 sidetone;
+	u8 mic_volume;
 
 	bool bt_enabled;
 	bool bt_device_connected;
@@ -493,6 +499,9 @@ static int steelseries_arctis_nova_3p_write_setting(struct hid_device *hdev,
 	case SS_SETTING_SIDETONE:
 		cmd = 0x39;
 		break;
+	case SS_SETTING_MIC_VOLUME:
+		cmd = 0x37;
+		break;
 	default:
 		return -EINVAL;
 	}
@@ -519,6 +528,9 @@ static int steelseries_arctis_nova_5_write_setting(struct hid_device *hdev,
 	case SS_SETTING_SIDETONE:
 		cmd = 0x39;
 		break;
+	case SS_SETTING_MIC_VOLUME:
+		cmd = 0x37;
+		break;
 	default:
 		return -EINVAL;
 	}
@@ -548,6 +560,9 @@ static int steelseries_arctis_nova_pro_write_setting(struct hid_device *hdev,
 	case SS_SETTING_SIDETONE:
 		cmd = 0x39;
 		break;
+	case SS_SETTING_MIC_VOLUME:
+		cmd = 0x37;
+		break;
 	default:
 		return -EINVAL;
 	}
@@ -839,14 +854,28 @@ static void steelseries_arctis_nova_7_gen2_parse_settings(
 
 	switch (data[0]) {
 	case 0x20:
+		sd->mic_volume = data[1];
 		sd->sidetone = data[2];
 		break;
+	case 0x37:
+		sd->mic_volume = data[1];
+		break;
 	case 0x39:
 		sd->sidetone = data[1];
 		break;
 	}
 }
 
+static void steelseries_arctis_nova_pro_parse_settings(
+	struct steelseries_device *sd, u8 *data, int size)
+{
+	if (size < 10)
+		return;
+
+	if (data[0] == 0x06 && data[1] == 0xb0)
+		sd->mic_volume = data[9];
+}
+
 static void steelseries_arctis_nova_pro_parse_status(struct steelseries_device *sd,
 						     u8 *data, int size)
 {
@@ -914,9 +943,10 @@ static const struct steelseries_device_info arctis_9_info = {
 
 static const struct steelseries_device_info arctis_nova_3p_info = {
 	.sync_interface = 4,
-	.capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE,
+	.capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
 	.sidetone_max = 10,
+	.mic_volume_max = 14,
 	.request_status = steelseries_arctis_nova_3p_request_status,
 	.parse_status = steelseries_arctis_nova_3p_parse_status,
 	.write_setting = steelseries_arctis_nova_3p_write_setting,
@@ -924,9 +954,10 @@ static const struct steelseries_device_info arctis_nova_3p_info = {
 
 static const struct steelseries_device_info arctis_nova_5_info = {
 	.sync_interface = 3,
-	.capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE,
+	.capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
 	.sidetone_max = 10,
+	.mic_volume_max = 15,
 	.request_status = steelseries_arctis_nova_request_status,
 	.parse_status = steelseries_arctis_nova_5_parse_status,
 	.write_setting = steelseries_arctis_nova_5_write_setting,
@@ -934,9 +965,11 @@ static const struct steelseries_device_info arctis_nova_5_info = {
 
 static const struct steelseries_device_info arctis_nova_5x_info = {
 	.sync_interface = 3,
-	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE,
+	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
+			SS_CAP_MIC_VOLUME,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
 	.sidetone_max = 10,
+	.mic_volume_max = 15,
 	.request_status = steelseries_arctis_nova_request_status,
 	.parse_status = steelseries_arctis_nova_5x_parse_status,
 	.write_setting = steelseries_arctis_nova_5_write_setting,
@@ -944,9 +977,11 @@ static const struct steelseries_device_info arctis_nova_5x_info = {
 
 static const struct steelseries_device_info arctis_nova_7_info = {
 	.sync_interface = 3,
-	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE,
+	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
+			SS_CAP_MIC_VOLUME,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
 	.sidetone_max = 3,
+	.mic_volume_max = 7,
 	.request_status = steelseries_arctis_nova_request_status,
 	.parse_status = steelseries_arctis_nova_7_parse_status,
 	.write_setting = steelseries_arctis_nova_5_write_setting,
@@ -954,10 +989,12 @@ static const struct steelseries_device_info arctis_nova_7_info = {
 
 static const struct steelseries_device_info arctis_nova_7p_info = {
 	.sync_interface = 3,
-	.capabilities = SS_CAP_BATTERY,
+	.capabilities = SS_CAP_BATTERY | SS_CAP_MIC_VOLUME,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
+	.mic_volume_max = 7,
 	.request_status = steelseries_arctis_nova_request_status,
 	.parse_status = steelseries_arctis_nova_7_parse_status,
+	.write_setting = steelseries_arctis_nova_5_write_setting,
 };
 
 static const struct steelseries_device_info arctis_nova_7_gen2_info = {
@@ -965,8 +1002,10 @@ static const struct steelseries_device_info arctis_nova_7_gen2_info = {
 	.async_interface = 5,
 	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_MIC_MUTE |
 			SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
-			SS_CAP_EXTERNAL_CONFIG | SS_CAP_SIDETONE,
+			SS_CAP_EXTERNAL_CONFIG | SS_CAP_SIDETONE |
+			SS_CAP_MIC_VOLUME,
 	.sidetone_max = 3,
+	.mic_volume_max = 7,
 	.request_status = steelseries_arctis_nova_request_status,
 	.parse_status = steelseries_arctis_nova_7_gen2_parse_status,
 	.request_settings = steelseries_arctis_nova_7_gen2_request_settings,
@@ -978,11 +1017,14 @@ static const struct steelseries_device_info arctis_nova_pro_info = {
 	.sync_interface = 4,
 	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_MIC_MUTE |
 			SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
-			SS_CAP_SIDETONE,
+			SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
 	.sidetone_max = 3,
+	.mic_volume_min = 1,
+	.mic_volume_max = 10,
 	.request_status = steelseries_arctis_nova_pro_request_status,
 	.parse_status = steelseries_arctis_nova_pro_parse_status,
+	.parse_settings = steelseries_arctis_nova_pro_parse_settings,
 	.write_setting = steelseries_arctis_nova_pro_write_setting,
 };
 
@@ -1343,6 +1385,71 @@ static const struct snd_kcontrol_new steelseries_sidetone_control = {
 	.put = steelseries_sidetone_put,
 };
 
+static int steelseries_mic_volume_info(struct snd_kcontrol *kcontrol,
+				       struct snd_ctl_elem_info *uinfo)
+{
+	struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
+
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+	uinfo->count = 1;
+	uinfo->value.integer.min = sd->info->mic_volume_min;
+	uinfo->value.integer.max = sd->info->mic_volume_max;
+	uinfo->value.integer.step = 1;
+	return 0;
+}
+
+static int steelseries_mic_volume_get(struct snd_kcontrol *kcontrol,
+				      struct snd_ctl_elem_value *ucontrol)
+{
+	struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
+	unsigned long flags;
+
+	spin_lock_irqsave(&sd->lock, flags);
+	ucontrol->value.integer.value[0] = sd->mic_volume;
+	spin_unlock_irqrestore(&sd->lock, flags);
+	return 0;
+}
+
+static int steelseries_mic_volume_put(struct snd_kcontrol *kcontrol,
+				      struct snd_ctl_elem_value *ucontrol)
+{
+	struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
+	unsigned long flags;
+	u8 new_mic_volume;
+	int ret;
+
+	new_mic_volume = ucontrol->value.integer.value[0];
+	if (new_mic_volume < sd->info->mic_volume_min ||
+	    new_mic_volume > sd->info->mic_volume_max)
+		return -EINVAL;
+
+	spin_lock_irqsave(&sd->lock, flags);
+	if (sd->mic_volume == new_mic_volume) {
+		spin_unlock_irqrestore(&sd->lock, flags);
+		return 0;
+	}
+	spin_unlock_irqrestore(&sd->lock, flags);
+
+	ret = sd->info->write_setting(sd->hdev, SS_SETTING_MIC_VOLUME,
+				      new_mic_volume);
+	if (ret)
+		return ret;
+
+	spin_lock_irqsave(&sd->lock, flags);
+	sd->mic_volume = new_mic_volume;
+	spin_unlock_irqrestore(&sd->lock, flags);
+
+	return 1;
+}
+
+static const struct snd_kcontrol_new steelseries_mic_volume_control = {
+	.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+	.name = "Mic Volume",
+	.info = steelseries_mic_volume_info,
+	.get = steelseries_mic_volume_get,
+	.put = steelseries_mic_volume_put,
+};
+
 static int steelseries_snd_register(struct steelseries_device *sd)
 {
 	struct hid_device *hdev = sd->hdev;
@@ -1400,6 +1507,21 @@ static int steelseries_snd_register(struct steelseries_device *sd)
 		sd->sidetone_id = kctl->id;
 	}
 
+	if (sd->info->capabilities & SS_CAP_MIC_VOLUME) {
+		struct snd_kcontrol *kctl;
+		struct snd_kcontrol_new mic_vol_ctl = steelseries_mic_volume_control;
+
+		mic_vol_ctl.access = SNDRV_CTL_ELEM_ACCESS_READWRITE;
+		if (sd->info->capabilities & SS_CAP_EXTERNAL_CONFIG)
+			mic_vol_ctl.access |= SNDRV_CTL_ELEM_ACCESS_VOLATILE;
+
+		kctl = snd_ctl_new1(&mic_vol_ctl, sd);
+		ret = snd_ctl_add(sd->card, kctl);
+		if (ret < 0)
+			goto err_free_card;
+		sd->mic_volume_id = kctl->id;
+	}
+
 	ret = snd_card_register(sd->card);
 	if (ret < 0)
 		goto err_free_card;
@@ -1431,6 +1553,7 @@ static int steelseries_raw_event(struct hid_device *hdev,
 	u8 old_chatmix_game;
 	bool old_mic_muted;
 	u8 old_sidetone;
+	u8 old_mic_volume;
 	bool is_async_interface = false;
 
 	if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1)
@@ -1446,6 +1569,7 @@ static int steelseries_raw_event(struct hid_device *hdev,
 	old_chatmix_game = sd->chatmix_game;
 	old_mic_muted = sd->mic_muted;
 	old_sidetone = sd->sidetone;
+	old_mic_volume = sd->mic_volume;
 
 	if (hid_is_usb(hdev)) {
 		struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
@@ -1509,6 +1633,9 @@ static int steelseries_raw_event(struct hid_device *hdev,
 		if (sd->sidetone != old_sidetone)
 			snd_ctl_notify(sd->card, SNDRV_CTL_EVENT_MASK_VALUE,
 				       &sd->sidetone_id);
+		if (sd->mic_volume != old_mic_volume)
+			snd_ctl_notify(sd->card, SNDRV_CTL_EVENT_MASK_VALUE,
+				       &sd->mic_volume_id);
 	}
 
 	return 0;
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 11/18] HID: steelseries: Add sidetone ALSA mixer control
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
	Christian Mayer, Sriman Achanta
In-Reply-To: <20260227235042.410062-1-srimanachanta@gmail.com>

Expose sidetone level as a writable ALSA integer mixer control
("Sidetone Volume"). The valid range is device-specific and stored in
the sidetone_max field of the device info struct.

The write protocol differs per family:
- Arctis 1/7: HID feature report with a separate two-byte command when
  disabling (value == 0) versus a five-byte enable command otherwise
- Arctis 9: single feature report with the value offset by 0xc0
- Nova 3P: output report followed by a save-to-flash command (0x09)
- Nova 5/7/Pro: output report followed by model-specific save commands

For devices with SS_CAP_EXTERNAL_CONFIG, the control is marked volatile
as the headset can modify the value independently and the current level
is recovered via the settings poll.

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

diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index f2423c350154..2bdf772432d0 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -27,9 +27,12 @@
 #define SS_CAP_BT_ENABLED		BIT(3)
 #define SS_CAP_BT_DEVICE_CONNECTED	BIT(4)
 #define SS_CAP_EXTERNAL_CONFIG		BIT(5)
+#define SS_CAP_SIDETONE			BIT(6)
 
 #define SS_QUIRK_STATUS_SYNC_POLL	BIT(0)
 
+#define SS_SETTING_SIDETONE		0
+
 struct steelseries_device;
 
 struct steelseries_device_info {
@@ -39,11 +42,14 @@ struct steelseries_device_info {
 	u8 sync_interface;
 	u8 async_interface;
 
+	u8 sidetone_max;
+
 	int (*request_status)(struct hid_device *hdev);
 	void (*parse_status)(struct steelseries_device *sd, u8 *data, int size);
 
 	int (*request_settings)(struct hid_device *hdev);
 	void (*parse_settings)(struct steelseries_device *sd, u8 *data, int size);
+	int (*write_setting)(struct hid_device *hdev, u8 setting, u8 value);
 };
 
 struct steelseries_device {
@@ -65,9 +71,11 @@ struct steelseries_device {
 	struct snd_ctl_elem_id chatmix_chat_id;
 	struct snd_ctl_elem_id chatmix_game_id;
 	struct snd_ctl_elem_id mic_muted_id;
+	struct snd_ctl_elem_id sidetone_id;
 	u8 chatmix_chat;
 	u8 chatmix_game;
 	bool mic_muted;
+	u8 sidetone;
 
 	bool bt_enabled;
 	bool bt_device_connected;
@@ -434,6 +442,127 @@ static inline int steelseries_send_output_report(struct hid_device *hdev,
 	return steelseries_send_report(hdev, data, len, HID_OUTPUT_REPORT);
 }
 
+/*
+ * Headset settings write functions
+ */
+
+static int steelseries_arctis_1_write_setting(struct hid_device *hdev,
+					      u8 setting, u8 value)
+{
+	switch (setting) {
+	case SS_SETTING_SIDETONE:
+		if (value == 0) {
+			const u8 data[] = { 0x06, 0x35 };
+
+			return steelseries_send_feature_report(hdev, data,
+							       sizeof(data));
+		} else {
+			const u8 data[] = { 0x06, 0x35, 0x01, 0x00, value };
+
+			return steelseries_send_feature_report(hdev, data,
+							       sizeof(data));
+		}
+	default:
+		return -EINVAL;
+	}
+}
+
+static int steelseries_arctis_9_write_setting(struct hid_device *hdev,
+					     u8 setting, u8 value)
+{
+	switch (setting) {
+	case SS_SETTING_SIDETONE: {
+		const u8 data[] = { 0x06, 0x00, value + 0xc0 };
+
+		return steelseries_send_feature_report(hdev, data, sizeof(data));
+	}
+	default:
+		return -EINVAL;
+	}
+}
+
+static int steelseries_arctis_nova_3p_write_setting(struct hid_device *hdev,
+						    u8 setting, u8 value)
+{
+	const u8 save[] = { 0x09 };
+	u8 cmd;
+	int ret;
+	u8 data[2];
+
+	switch (setting) {
+	case SS_SETTING_SIDETONE:
+		cmd = 0x39;
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	data[0] = cmd;
+	data[1] = value;
+
+	ret = steelseries_send_feature_report(hdev, data, sizeof(data));
+	if (ret)
+		return ret;
+
+	return steelseries_send_feature_report(hdev, save, sizeof(save));
+}
+
+static int steelseries_arctis_nova_5_write_setting(struct hid_device *hdev,
+						   u8 setting, u8 value)
+{
+	const u8 save[] = { 0x00, 0x09 };
+	u8 cmd;
+	int ret;
+	u8 data[3];
+
+	switch (setting) {
+	case SS_SETTING_SIDETONE:
+		cmd = 0x39;
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	data[0] = 0x00;
+	data[1] = cmd;
+	data[2] = value;
+
+	ret = steelseries_send_output_report(hdev, data, sizeof(data));
+	if (ret)
+		return ret;
+
+	msleep(10);
+
+	return steelseries_send_output_report(hdev, save, sizeof(save));
+}
+
+static int steelseries_arctis_nova_pro_write_setting(struct hid_device *hdev,
+						     u8 setting, u8 value)
+{
+	const u8 save[] = { 0x06, 0x09 };
+	u8 cmd;
+	int ret;
+	u8 data[3];
+
+	switch (setting) {
+	case SS_SETTING_SIDETONE:
+		cmd = 0x39;
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	data[0] = 0x06;
+	data[1] = cmd;
+	data[2] = value;
+
+	ret = steelseries_send_output_report(hdev, data, sizeof(data));
+	if (ret)
+		return ret;
+
+	return steelseries_send_output_report(hdev, save, sizeof(save));
+}
+
 /*
  * Headset status request functions
  */
@@ -702,6 +831,21 @@ static int steelseries_arctis_nova_7_gen2_request_settings(struct hid_device *hd
 	return steelseries_send_output_report(hdev, data, sizeof(data));
 }
 
+static void steelseries_arctis_nova_7_gen2_parse_settings(
+	struct steelseries_device *sd, u8 *data, int size)
+{
+	if (size < 3)
+		return;
+
+	switch (data[0]) {
+	case 0x20:
+		sd->sidetone = data[2];
+		break;
+	case 0x39:
+		sd->sidetone = data[1];
+		break;
+	}
+}
 
 static void steelseries_arctis_nova_pro_parse_status(struct steelseries_device *sd,
 						     u8 *data, int size)
@@ -730,66 +874,82 @@ static const struct steelseries_device_info srws1_info = { };
 
 static const struct steelseries_device_info arctis_1_info = {
 	.sync_interface = 3,
-	.capabilities = SS_CAP_BATTERY,
+	.capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
+	.sidetone_max = 18,
 	.request_status = steelseries_arctis_1_request_status,
 	.parse_status = steelseries_arctis_1_parse_status,
+	.write_setting = steelseries_arctis_1_write_setting,
 };
 
 static const struct steelseries_device_info arctis_7_info = {
 	.sync_interface = 5,
-	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX,
+	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
+	.sidetone_max = 18,
 	.request_status = steelseries_arctis_7_request_status,
 	.parse_status = steelseries_arctis_7_parse_status,
+	.write_setting = steelseries_arctis_1_write_setting,
 };
 
 static const struct steelseries_device_info arctis_7_plus_info = {
 	.sync_interface = 3,
-	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX,
+	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
+	.sidetone_max = 3,
 	.request_status = steelseries_arctis_nova_request_status,
 	.parse_status = steelseries_arctis_7_plus_parse_status,
+	.write_setting = steelseries_arctis_nova_5_write_setting,
 };
 
 static const struct steelseries_device_info arctis_9_info = {
 	.sync_interface = 0,
-	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX,
+	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
+	.sidetone_max = 61,
 	.request_status = steelseries_arctis_9_request_status,
 	.parse_status = steelseries_arctis_9_parse_status,
+	.write_setting = steelseries_arctis_9_write_setting,
 };
 
 static const struct steelseries_device_info arctis_nova_3p_info = {
 	.sync_interface = 4,
-	.capabilities = SS_CAP_BATTERY,
+	.capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
+	.sidetone_max = 10,
 	.request_status = steelseries_arctis_nova_3p_request_status,
 	.parse_status = steelseries_arctis_nova_3p_parse_status,
+	.write_setting = steelseries_arctis_nova_3p_write_setting,
 };
 
 static const struct steelseries_device_info arctis_nova_5_info = {
 	.sync_interface = 3,
-	.capabilities = SS_CAP_BATTERY,
+	.capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
+	.sidetone_max = 10,
 	.request_status = steelseries_arctis_nova_request_status,
 	.parse_status = steelseries_arctis_nova_5_parse_status,
+	.write_setting = steelseries_arctis_nova_5_write_setting,
 };
 
 static const struct steelseries_device_info arctis_nova_5x_info = {
 	.sync_interface = 3,
-	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX,
+	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
+	.sidetone_max = 10,
 	.request_status = steelseries_arctis_nova_request_status,
 	.parse_status = steelseries_arctis_nova_5x_parse_status,
+	.write_setting = steelseries_arctis_nova_5_write_setting,
 };
 
 static const struct steelseries_device_info arctis_nova_7_info = {
 	.sync_interface = 3,
-	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX,
+	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
+	.sidetone_max = 3,
 	.request_status = steelseries_arctis_nova_request_status,
 	.parse_status = steelseries_arctis_nova_7_parse_status,
+	.write_setting = steelseries_arctis_nova_5_write_setting,
 };
 
 static const struct steelseries_device_info arctis_nova_7p_info = {
@@ -805,19 +965,25 @@ static const struct steelseries_device_info arctis_nova_7_gen2_info = {
 	.async_interface = 5,
 	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_MIC_MUTE |
 			SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
-			SS_CAP_EXTERNAL_CONFIG,
+			SS_CAP_EXTERNAL_CONFIG | SS_CAP_SIDETONE,
+	.sidetone_max = 3,
 	.request_status = steelseries_arctis_nova_request_status,
 	.parse_status = steelseries_arctis_nova_7_gen2_parse_status,
 	.request_settings = steelseries_arctis_nova_7_gen2_request_settings,
+	.parse_settings = steelseries_arctis_nova_7_gen2_parse_settings,
+	.write_setting = steelseries_arctis_nova_5_write_setting,
 };
 
 static const struct steelseries_device_info arctis_nova_pro_info = {
 	.sync_interface = 4,
 	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_MIC_MUTE |
-			SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED,
+			SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
+			SS_CAP_SIDETONE,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
+	.sidetone_max = 3,
 	.request_status = steelseries_arctis_nova_pro_request_status,
 	.parse_status = steelseries_arctis_nova_pro_parse_status,
+	.write_setting = steelseries_arctis_nova_pro_write_setting,
 };
 
 /*
@@ -1113,6 +1279,70 @@ static const struct snd_kcontrol_new steelseries_mic_muted_control = {
 	.get = steelseries_mic_muted_get,
 };
 
+static int steelseries_sidetone_info(struct snd_kcontrol *kcontrol,
+				     struct snd_ctl_elem_info *uinfo)
+{
+	struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
+
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+	uinfo->count = 1;
+	uinfo->value.integer.min = 0;
+	uinfo->value.integer.max = sd->info->sidetone_max;
+	uinfo->value.integer.step = 1;
+	return 0;
+}
+
+static int steelseries_sidetone_get(struct snd_kcontrol *kcontrol,
+				    struct snd_ctl_elem_value *ucontrol)
+{
+	struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
+	unsigned long flags;
+
+	spin_lock_irqsave(&sd->lock, flags);
+	ucontrol->value.integer.value[0] = sd->sidetone;
+	spin_unlock_irqrestore(&sd->lock, flags);
+	return 0;
+}
+
+static int steelseries_sidetone_put(struct snd_kcontrol *kcontrol,
+				    struct snd_ctl_elem_value *ucontrol)
+{
+	struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
+	unsigned long flags;
+	u8 new_sidetone;
+	int ret;
+
+	new_sidetone = ucontrol->value.integer.value[0];
+	if (new_sidetone > sd->info->sidetone_max)
+		return -EINVAL;
+
+	spin_lock_irqsave(&sd->lock, flags);
+	if (sd->sidetone == new_sidetone) {
+		spin_unlock_irqrestore(&sd->lock, flags);
+		return 0;
+	}
+	spin_unlock_irqrestore(&sd->lock, flags);
+
+	ret = sd->info->write_setting(sd->hdev, SS_SETTING_SIDETONE,
+				      new_sidetone);
+	if (ret)
+		return ret;
+
+	spin_lock_irqsave(&sd->lock, flags);
+	sd->sidetone = new_sidetone;
+	spin_unlock_irqrestore(&sd->lock, flags);
+
+	return 1;
+}
+
+static const struct snd_kcontrol_new steelseries_sidetone_control = {
+	.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+	.name = "Sidetone Volume",
+	.info = steelseries_sidetone_info,
+	.get = steelseries_sidetone_get,
+	.put = steelseries_sidetone_put,
+};
+
 static int steelseries_snd_register(struct steelseries_device *sd)
 {
 	struct hid_device *hdev = sd->hdev;
@@ -1155,6 +1385,21 @@ static int steelseries_snd_register(struct steelseries_device *sd)
 		sd->mic_muted_id = kctl->id;
 	}
 
+	if (sd->info->capabilities & SS_CAP_SIDETONE) {
+		struct snd_kcontrol *kctl;
+		struct snd_kcontrol_new sidetone_ctl = steelseries_sidetone_control;
+
+		sidetone_ctl.access = SNDRV_CTL_ELEM_ACCESS_READWRITE;
+		if (sd->info->capabilities & SS_CAP_EXTERNAL_CONFIG)
+			sidetone_ctl.access |= SNDRV_CTL_ELEM_ACCESS_VOLATILE;
+
+		kctl = snd_ctl_new1(&sidetone_ctl, sd);
+		ret = snd_ctl_add(sd->card, kctl);
+		if (ret < 0)
+			goto err_free_card;
+		sd->sidetone_id = kctl->id;
+	}
+
 	ret = snd_card_register(sd->card);
 	if (ret < 0)
 		goto err_free_card;
@@ -1185,6 +1430,7 @@ static int steelseries_raw_event(struct hid_device *hdev,
 	u8 old_chatmix_chat;
 	u8 old_chatmix_game;
 	bool old_mic_muted;
+	u8 old_sidetone;
 	bool is_async_interface = false;
 
 	if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1)
@@ -1199,6 +1445,7 @@ static int steelseries_raw_event(struct hid_device *hdev,
 	old_chatmix_chat = sd->chatmix_chat;
 	old_chatmix_game = sd->chatmix_game;
 	old_mic_muted = sd->mic_muted;
+	old_sidetone = sd->sidetone;
 
 	if (hid_is_usb(hdev)) {
 		struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
@@ -1259,6 +1506,9 @@ static int steelseries_raw_event(struct hid_device *hdev,
 		if (sd->mic_muted != old_mic_muted)
 			snd_ctl_notify(sd->card, SNDRV_CTL_EVENT_MASK_VALUE,
 				       &sd->mic_muted_id);
+		if (sd->sidetone != old_sidetone)
+			snd_ctl_notify(sd->card, SNDRV_CTL_EVENT_MASK_VALUE,
+				       &sd->sidetone_id);
 	}
 
 	return 0;
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 10/18] HID: steelseries: Add settings poll infrastructure
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
	Christian Mayer, Sriman Achanta
In-Reply-To: <20260227235042.410062-1-srimanachanta@gmail.com>

Some headset settings (sidetone level, mic volume, etc.) are not
reported spontaneously but must be explicitly requested from the device.
Introduce a separate delayed work item (settings_work) for fetching
these persistent settings, independent of the existing status work.

Settings are requested once at probe time and again whenever the headset
reconnects after being disconnected. Device info structs gain
request_settings and parse_settings hooks for model-specific
implementations. The SS_CAP_EXTERNAL_CONFIG capability flag marks
devices whose writable controls can also be changed from the headset
hardware directly; writable ALSA controls on such devices will be marked
volatile.

The initial implementation adds the Arctis Nova 7 Gen2 audio settings
request (0x00, 0x20).

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

diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index 8c6116d02f19..f2423c350154 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -26,6 +26,7 @@
 #define SS_CAP_MIC_MUTE			BIT(2)
 #define SS_CAP_BT_ENABLED		BIT(3)
 #define SS_CAP_BT_DEVICE_CONNECTED	BIT(4)
+#define SS_CAP_EXTERNAL_CONFIG		BIT(5)
 
 #define SS_QUIRK_STATUS_SYNC_POLL	BIT(0)
 
@@ -40,6 +41,9 @@ struct steelseries_device_info {
 
 	int (*request_status)(struct hid_device *hdev);
 	void (*parse_status)(struct steelseries_device *sd, u8 *data, int size);
+
+	int (*request_settings)(struct hid_device *hdev);
+	void (*parse_settings)(struct steelseries_device *sd, u8 *data, int size);
 };
 
 struct steelseries_device {
@@ -49,6 +53,7 @@ struct steelseries_device {
 	bool use_async_protocol;
 
 	struct delayed_work status_work;
+	struct delayed_work settings_work;
 
 	struct power_supply_desc battery_desc;
 	struct power_supply *battery;
@@ -690,6 +695,14 @@ static void steelseries_arctis_nova_7_gen2_parse_status(struct steelseries_devic
 	}
 }
 
+static int steelseries_arctis_nova_7_gen2_request_settings(struct hid_device *hdev)
+{
+	const u8 data[] = { 0x00, 0x20 };
+
+	return steelseries_send_output_report(hdev, data, sizeof(data));
+}
+
+
 static void steelseries_arctis_nova_pro_parse_status(struct steelseries_device *sd,
 						     u8 *data, int size)
 {
@@ -791,9 +804,11 @@ static const struct steelseries_device_info arctis_nova_7_gen2_info = {
 	.sync_interface = 3,
 	.async_interface = 5,
 	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_MIC_MUTE |
-			SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED,
+			SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
+			SS_CAP_EXTERNAL_CONFIG,
 	.request_status = steelseries_arctis_nova_request_status,
 	.parse_status = steelseries_arctis_nova_7_gen2_parse_status,
+	.request_settings = steelseries_arctis_nova_7_gen2_request_settings,
 };
 
 static const struct steelseries_device_info arctis_nova_pro_info = {
@@ -901,6 +916,15 @@ static void steelseries_status_timer_work_handler(struct work_struct *work)
 	spin_unlock_irqrestore(&sd->lock, flags);
 }
 
+static void steelseries_settings_work_handler(struct work_struct *work)
+{
+	struct steelseries_device *sd = container_of(
+		work, struct steelseries_device, settings_work.work);
+
+	if (sd->info->request_settings)
+		sd->info->request_settings(sd->hdev);
+}
+
 static int steelseries_battery_register(struct steelseries_device *sd)
 {
 	static atomic_t battery_no = ATOMIC_INIT(0);
@@ -1185,6 +1209,9 @@ static int steelseries_raw_event(struct hid_device *hdev,
 
 	sd->info->parse_status(sd, data, size);
 
+	if (sd->info->parse_settings)
+		sd->info->parse_settings(sd, data, size);
+
 	if (sd->headset_connected != old_connected) {
 		hid_dbg(hdev,
 			"Connected status changed from %sconnected to %sconnected\n",
@@ -1194,6 +1221,9 @@ static int steelseries_raw_event(struct hid_device *hdev,
 		if (sd->headset_connected && !old_connected &&
 		    sd->use_async_protocol && is_async_interface) {
 			schedule_delayed_work(&sd->status_work, 0);
+			if (sd->info->request_settings)
+				schedule_delayed_work(&sd->settings_work,
+						      msecs_to_jiffies(10));
 		}
 
 		if (sd->battery) {
@@ -1329,7 +1359,11 @@ static int steelseries_probe(struct hid_device *hdev,
 #endif
 
 		INIT_DELAYED_WORK(&sd->status_work, steelseries_status_timer_work_handler);
+		INIT_DELAYED_WORK(&sd->settings_work, steelseries_settings_work_handler);
+
 		schedule_delayed_work(&sd->status_work, msecs_to_jiffies(100));
+		if (info->request_settings)
+			schedule_delayed_work(&sd->settings_work, msecs_to_jiffies(200));
 
 		return 0;
 	}
@@ -1415,6 +1449,7 @@ static void steelseries_remove(struct hid_device *hdev)
 		spin_unlock_irqrestore(&sd->lock, flags);
 
 		cancel_delayed_work_sync(&sd->status_work);
+		cancel_delayed_work_sync(&sd->settings_work);
 	}
 
 	hid_hw_close(hdev);
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 09/18] HID: steelseries: Add Bluetooth state sysfs attributes
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
	Christian Mayer, Sriman Achanta
In-Reply-To: <20260227235042.410062-1-srimanachanta@gmail.com>

Add read-only sysfs attributes bt_enabled and bt_device_connected that
reflect the current Bluetooth radio state for headsets that support it.
Attributes are registered via an attribute group with an is_visible
callback so they only appear on capable devices.

Bluetooth state is decoded from the following HID reports:
- Arctis Nova 7 Gen2: 0xb0 initial status packet and 0xb5 async events
- Arctis Nova Pro: initial 0x06/0x14 status packet

Returns -ENODEV if the headset is not currently connected.

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

diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index 3de8e1555263..8c6116d02f19 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -24,6 +24,8 @@
 #define SS_CAP_BATTERY			BIT(0)
 #define SS_CAP_CHATMIX			BIT(1)
 #define SS_CAP_MIC_MUTE			BIT(2)
+#define SS_CAP_BT_ENABLED		BIT(3)
+#define SS_CAP_BT_DEVICE_CONNECTED	BIT(4)
 
 #define SS_QUIRK_STATUS_SYNC_POLL	BIT(0)
 
@@ -62,6 +64,9 @@ struct steelseries_device {
 	u8 chatmix_game;
 	bool mic_muted;
 
+	bool bt_enabled;
+	bool bt_device_connected;
+
 	spinlock_t lock;
 	bool removed;
 };
@@ -641,6 +646,20 @@ static void steelseries_arctis_nova_7_gen2_parse_status(struct steelseries_devic
 		sd->battery_charging = (data[3] == 0x01);
 		sd->chatmix_game = data[4];
 		sd->chatmix_chat = data[5];
+		switch (data[6]) {
+		case 0x00:
+			sd->bt_enabled = false;
+			sd->bt_device_connected = false;
+			break;
+		case 0x03:
+			sd->bt_enabled = true;
+			sd->bt_device_connected = false;
+			break;
+		case 0x02:
+			sd->bt_enabled = true;
+			sd->bt_device_connected = true;
+			break;
+		}
 		sd->mic_muted = (data[9] == 0x01);
 		break;
 	case 0xb7:
@@ -659,6 +678,15 @@ static void steelseries_arctis_nova_7_gen2_parse_status(struct steelseries_devic
 	case 0x52:
 		sd->mic_muted = (data[2] == 0x01);
 		break;
+	case 0xb5:
+		if (data[1] == 0x01) {
+			sd->bt_enabled = false;
+			sd->bt_device_connected = false;
+		} else if (data[1] == 0x04) {
+			sd->bt_enabled = true;
+			sd->bt_device_connected = (data[2] == 0x01);
+		}
+		break;
 	}
 }
 
@@ -673,6 +701,8 @@ static void steelseries_arctis_nova_pro_parse_status(struct steelseries_device *
 		sd->battery_capacity = steelseries_map_capacity(data[6], 0x00, 0x08);
 		sd->battery_charging = (data[15] == 0x02);
 		sd->mic_muted = (data[9] == 0x01);
+		sd->bt_enabled = (data[4] == 0x00);
+		sd->bt_device_connected = (data[5] == 0x01);
 	} else if (data[0] == 0x07 && data[1] == 0x45) {
 		sd->chatmix_game = data[2];
 		sd->chatmix_chat = data[3];
@@ -760,14 +790,16 @@ static const struct steelseries_device_info arctis_nova_7p_info = {
 static const struct steelseries_device_info arctis_nova_7_gen2_info = {
 	.sync_interface = 3,
 	.async_interface = 5,
-	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_MIC_MUTE,
+	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_MIC_MUTE |
+			SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED,
 	.request_status = steelseries_arctis_nova_request_status,
 	.parse_status = steelseries_arctis_nova_7_gen2_parse_status,
 };
 
 static const struct steelseries_device_info arctis_nova_pro_info = {
 	.sync_interface = 4,
-	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_MIC_MUTE,
+	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_MIC_MUTE |
+			SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
 	.request_status = steelseries_arctis_nova_pro_request_status,
 	.parse_status = steelseries_arctis_nova_pro_parse_status,
@@ -908,6 +940,70 @@ static int steelseries_battery_register(struct steelseries_device *sd)
 	return 0;
 }
 
+/*
+ * Sysfs attributes for device state
+ */
+
+static ssize_t bt_enabled_show(struct device *dev,
+			       struct device_attribute *attr, char *buf)
+{
+	struct hid_device *hdev = to_hid_device(dev);
+	struct steelseries_device *sd = hid_get_drvdata(hdev);
+
+	if (!sd->headset_connected)
+		return -ENODEV;
+
+	return sysfs_emit(buf, "%d\n", sd->bt_enabled);
+}
+
+static ssize_t bt_device_connected_show(struct device *dev,
+					struct device_attribute *attr, char *buf)
+{
+	struct hid_device *hdev = to_hid_device(dev);
+	struct steelseries_device *sd = hid_get_drvdata(hdev);
+
+	if (!sd->headset_connected)
+		return -ENODEV;
+
+	return sysfs_emit(buf, "%d\n", sd->bt_device_connected);
+}
+
+static DEVICE_ATTR_RO(bt_enabled);
+static DEVICE_ATTR_RO(bt_device_connected);
+
+static struct attribute *steelseries_headset_attrs[] = {
+	&dev_attr_bt_enabled.attr,
+	&dev_attr_bt_device_connected.attr,
+	NULL,
+};
+
+static umode_t steelseries_headset_attr_is_visible(struct kobject *kobj,
+						   struct attribute *attr,
+						   int index)
+{
+	struct device *dev = kobj_to_dev(kobj);
+	struct hid_device *hdev = to_hid_device(dev);
+	struct steelseries_device *sd = hid_get_drvdata(hdev);
+	unsigned long caps;
+
+	if (!sd)
+		return 0;
+
+	caps = sd->info->capabilities;
+
+	if (attr == &dev_attr_bt_enabled.attr)
+		return (caps & SS_CAP_BT_ENABLED) ? attr->mode : 0;
+	if (attr == &dev_attr_bt_device_connected.attr)
+		return (caps & SS_CAP_BT_DEVICE_CONNECTED) ? attr->mode : 0;
+
+	return 0;
+}
+
+static const struct attribute_group steelseries_headset_attr_group = {
+	.attrs = steelseries_headset_attrs,
+	.is_visible = steelseries_headset_attr_is_visible,
+};
+
 #if IS_BUILTIN(CONFIG_SND) || \
 	(IS_MODULE(CONFIG_SND) && IS_MODULE(CONFIG_HID_STEELSERIES))
 
@@ -1218,6 +1314,13 @@ static int steelseries_probe(struct hid_device *hdev,
 				hid_warn(hdev, "Failed to register battery: %d\n", ret);
 		}
 
+		if (info->capabilities & (SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED)) {
+			ret = sysfs_create_group(&hdev->dev.kobj,
+						 &steelseries_headset_attr_group);
+			if (ret)
+				hid_warn(hdev, "Failed to create sysfs group: %d\n", ret);
+		}
+
 #if IS_BUILTIN(CONFIG_SND) || \
 	(IS_MODULE(CONFIG_SND) && IS_MODULE(CONFIG_HID_STEELSERIES))
 		ret = steelseries_snd_register(sd);
@@ -1289,6 +1392,10 @@ static void steelseries_remove(struct hid_device *hdev)
 	}
 
 	if (interface_num == sd->info->sync_interface) {
+		if (sd->info->capabilities & (SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED))
+			sysfs_remove_group(&hdev->dev.kobj,
+					   &steelseries_headset_attr_group);
+
 		if (sd->info->async_interface) {
 			struct hid_device *sibling;
 
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 07/18] HID: steelseries: Add ChatMix ALSA mixer controls
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
	Christian Mayer, Sriman Achanta
In-Reply-To: <20260227235042.410062-1-srimanachanta@gmail.com>

ChatMix is a hardware feature on Arctis headsets that independently
mixes game and chat audio streams. Expose the per-channel levels as
read-only volatile ALSA integer mixer controls ("ChatMix Chat" and
"ChatMix Game"), scaled to a 0-100 range. Controls are updated via
snd_ctl_notify() whenever the underlying values change in a HID event.

Each device family encodes the levels differently in HID reports;
parsing is added for the Arctis 7, 7+, 9, Nova 5X, Nova 7, Nova 7 Gen2,
and Nova Pro. The Nova 7 request path is extended to also fetch ChatMix
data (0x06, 0x24).

The Nova 5X gets its own device info entry as it uses a different status
packet layout, and the Nova 7P is split into a separate entry since it
does not expose ChatMix.

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

diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index b7f932cde98d..30ee9da1deac 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -16,11 +16,13 @@
 #include <linux/power_supply.h>
 #include <linux/workqueue.h>
 #include <linux/spinlock.h>
+#include <sound/control.h>
 #include <sound/core.h>
 
 #include "hid-ids.h"
 
 #define SS_CAP_BATTERY			BIT(0)
+#define SS_CAP_CHATMIX			BIT(1)
 
 #define SS_QUIRK_STATUS_SYNC_POLL	BIT(0)
 
@@ -52,6 +54,10 @@ struct steelseries_device {
 	bool battery_charging;
 
 	struct snd_card *card;
+	struct snd_ctl_elem_id chatmix_chat_id;
+	struct snd_ctl_elem_id chatmix_game_id;
+	u8 chatmix_chat;
+	u8 chatmix_game;
 
 	spinlock_t lock;
 	bool removed;
@@ -431,6 +437,7 @@ static int steelseries_arctis_7_request_status(struct hid_device *hdev)
 	int ret;
 	const u8 connection_data[] = { 0x06, 0x14 };
 	const u8 battery_data[] = { 0x06, 0x18 };
+	const u8 chatmix_data[] = { 0x06, 0x24 };
 
 	ret = steelseries_send_feature_report(hdev, connection_data, sizeof(connection_data));
 	if (ret)
@@ -438,7 +445,13 @@ static int steelseries_arctis_7_request_status(struct hid_device *hdev)
 
 	msleep(10);
 
-	return steelseries_send_feature_report(hdev, battery_data, sizeof(battery_data));
+	ret = steelseries_send_feature_report(hdev, battery_data, sizeof(battery_data));
+	if (ret)
+		return ret;
+
+	msleep(10);
+
+	return steelseries_send_feature_report(hdev, chatmix_data, sizeof(chatmix_data));
 }
 
 static int steelseries_arctis_9_request_status(struct hid_device *hdev)
@@ -508,40 +521,52 @@ static void steelseries_arctis_1_parse_status(struct steelseries_device *sd,
 static void steelseries_arctis_7_parse_status(struct steelseries_device *sd,
 					      u8 *data, int size)
 {
-	if (size < 3)
+	if (size < 4)
 		return;
 
 	if (data[0] == 0x06) {
-		if (data[1] == 0x14)
+		switch (data[1]) {
+		case 0x14:
 			sd->headset_connected = (data[2] == 0x03);
-		else if (data[1] == 0x18)
+			break;
+		case 0x18:
 			sd->battery_capacity = data[2];
+			break;
+		case 0x24:
+			sd->chatmix_game = steelseries_map_capacity(data[2], 0xbf, 0xff);
+			sd->chatmix_chat = steelseries_map_capacity(data[3], 0xbf, 0xff);
+			break;
+		}
 	}
 }
 
 static void steelseries_arctis_7_plus_parse_status(struct steelseries_device *sd,
 						   u8 *data, int size)
 {
-	if (size < 4)
+	if (size < 6)
 		return;
 
 	if (data[0] == 0xb0) {
 		sd->headset_connected = !(data[1] == 0x01);
 		sd->battery_capacity = steelseries_map_capacity(data[2], 0x00, 0x04);
 		sd->battery_charging = (data[3] == 0x01);
+		sd->chatmix_game = steelseries_map_capacity(data[4], 0x00, 0x64);
+		sd->chatmix_chat = steelseries_map_capacity(data[5], 0x00, 0x64);
 	}
 }
 
 static void steelseries_arctis_9_parse_status(struct steelseries_device *sd,
 					      u8 *data, int size)
 {
-	if (size < 5)
+	if (size < 11)
 		return;
 
 	if (data[0] == 0xaa) {
 		sd->headset_connected = (data[1] == 0x01);
 		sd->battery_charging = (data[4] == 0x01);
 		sd->battery_capacity = steelseries_map_capacity(data[3], 0x64, 0x9A);
+		sd->chatmix_game = steelseries_map_capacity(data[9], 0x00, 0x13);
+		sd->chatmix_chat = steelseries_map_capacity(data[10], 0x00, 0x13);
 	}
 }
 
@@ -570,23 +595,40 @@ static void steelseries_arctis_nova_5_parse_status(struct steelseries_device *sd
 	}
 }
 
+static void steelseries_arctis_nova_5x_parse_status(struct steelseries_device *sd,
+						   u8 *data, int size)
+{
+	if (size < 7)
+		return;
+
+	if (data[0] == 0xb0) {
+		sd->headset_connected = !(data[1] == 0x02);
+		sd->battery_capacity = data[3];
+		sd->battery_charging = (data[4] == 0x01);
+		sd->chatmix_chat = data[5];
+		sd->chatmix_game = data[6];
+	}
+}
+
 static void steelseries_arctis_nova_7_parse_status(struct steelseries_device *sd,
 						   u8 *data, int size)
 {
-	if (size < 4)
+	if (size < 6)
 		return;
 
 	if (data[0] == 0xb0) {
 		sd->headset_connected = (data[1] == 0x03);
 		sd->battery_capacity = steelseries_map_capacity(data[2], 0x00, 0x04);
 		sd->battery_charging = (data[3] == 0x01);
+		sd->chatmix_game = data[4];
+		sd->chatmix_chat = data[5];
 	}
 }
 
 static void steelseries_arctis_nova_7_gen2_parse_status(struct steelseries_device *sd,
 							u8 *data, int size)
 {
-	if (size < 4)
+	if (size < 6)
 		return;
 
 	switch (data[0]) {
@@ -594,6 +636,8 @@ static void steelseries_arctis_nova_7_gen2_parse_status(struct steelseries_devic
 		sd->headset_connected = (data[1] == 0x03);
 		sd->battery_capacity = data[2];
 		sd->battery_charging = (data[3] == 0x01);
+		sd->chatmix_game = data[4];
+		sd->chatmix_chat = data[5];
 		break;
 	case 0xb7:
 		sd->battery_capacity = data[1];
@@ -604,6 +648,10 @@ static void steelseries_arctis_nova_7_gen2_parse_status(struct steelseries_devic
 	case 0xbb:
 		sd->battery_charging = (data[1] == 0x01);
 		break;
+	case 0x45:
+		sd->chatmix_game = data[1];
+		sd->chatmix_chat = data[2];
+		break;
 	}
 }
 
@@ -617,6 +665,9 @@ static void steelseries_arctis_nova_pro_parse_status(struct steelseries_device *
 		sd->headset_connected = (data[15] == 0x08 || data[15] == 0x02);
 		sd->battery_capacity = steelseries_map_capacity(data[6], 0x00, 0x08);
 		sd->battery_charging = (data[15] == 0x02);
+	} else if (data[0] == 0x07 && data[1] == 0x45) {
+		sd->chatmix_game = data[2];
+		sd->chatmix_chat = data[3];
 	}
 }
 
@@ -636,7 +687,7 @@ static const struct steelseries_device_info arctis_1_info = {
 
 static const struct steelseries_device_info arctis_7_info = {
 	.sync_interface = 5,
-	.capabilities = SS_CAP_BATTERY,
+	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
 	.request_status = steelseries_arctis_7_request_status,
 	.parse_status = steelseries_arctis_7_parse_status,
@@ -644,7 +695,7 @@ static const struct steelseries_device_info arctis_7_info = {
 
 static const struct steelseries_device_info arctis_7_plus_info = {
 	.sync_interface = 3,
-	.capabilities = SS_CAP_BATTERY,
+	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
 	.request_status = steelseries_arctis_nova_request_status,
 	.parse_status = steelseries_arctis_7_plus_parse_status,
@@ -652,7 +703,7 @@ static const struct steelseries_device_info arctis_7_plus_info = {
 
 static const struct steelseries_device_info arctis_9_info = {
 	.sync_interface = 0,
-	.capabilities = SS_CAP_BATTERY,
+	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
 	.request_status = steelseries_arctis_9_request_status,
 	.parse_status = steelseries_arctis_9_parse_status,
@@ -674,7 +725,23 @@ static const struct steelseries_device_info arctis_nova_5_info = {
 	.parse_status = steelseries_arctis_nova_5_parse_status,
 };
 
+static const struct steelseries_device_info arctis_nova_5x_info = {
+	.sync_interface = 3,
+	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX,
+	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
+	.request_status = steelseries_arctis_nova_request_status,
+	.parse_status = steelseries_arctis_nova_5x_parse_status,
+};
+
 static const struct steelseries_device_info arctis_nova_7_info = {
+	.sync_interface = 3,
+	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX,
+	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
+	.request_status = steelseries_arctis_nova_request_status,
+	.parse_status = steelseries_arctis_nova_7_parse_status,
+};
+
+static const struct steelseries_device_info arctis_nova_7p_info = {
 	.sync_interface = 3,
 	.capabilities = SS_CAP_BATTERY,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
@@ -685,14 +752,14 @@ static const struct steelseries_device_info arctis_nova_7_info = {
 static const struct steelseries_device_info arctis_nova_7_gen2_info = {
 	.sync_interface = 3,
 	.async_interface = 5,
-	.capabilities = SS_CAP_BATTERY,
+	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX,
 	.request_status = steelseries_arctis_nova_request_status,
 	.parse_status = steelseries_arctis_nova_7_gen2_parse_status,
 };
 
 static const struct steelseries_device_info arctis_nova_pro_info = {
 	.sync_interface = 4,
-	.capabilities = SS_CAP_BATTERY,
+	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
 	.request_status = steelseries_arctis_nova_pro_request_status,
 	.parse_status = steelseries_arctis_nova_pro_parse_status,
@@ -836,6 +903,57 @@ static int steelseries_battery_register(struct steelseries_device *sd)
 #if IS_BUILTIN(CONFIG_SND) || \
 	(IS_MODULE(CONFIG_SND) && IS_MODULE(CONFIG_HID_STEELSERIES))
 
+static int steelseries_chatmix_info(struct snd_kcontrol *kcontrol,
+				    struct snd_ctl_elem_info *uinfo)
+{
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+	uinfo->count = 1;
+	uinfo->value.integer.min = 0;
+	uinfo->value.integer.max = 100;
+	uinfo->value.integer.step = 1;
+	return 0;
+}
+
+static int steelseries_chatmix_chat_get(struct snd_kcontrol *kcontrol,
+					struct snd_ctl_elem_value *ucontrol)
+{
+	struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
+	unsigned long flags;
+
+	spin_lock_irqsave(&sd->lock, flags);
+	ucontrol->value.integer.value[0] = sd->chatmix_chat;
+	spin_unlock_irqrestore(&sd->lock, flags);
+	return 0;
+}
+
+static int steelseries_chatmix_game_get(struct snd_kcontrol *kcontrol,
+					struct snd_ctl_elem_value *ucontrol)
+{
+	struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
+	unsigned long flags;
+
+	spin_lock_irqsave(&sd->lock, flags);
+	ucontrol->value.integer.value[0] = sd->chatmix_game;
+	spin_unlock_irqrestore(&sd->lock, flags);
+	return 0;
+}
+
+static const struct snd_kcontrol_new steelseries_chatmix_chat_control = {
+	.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+	.name = "ChatMix Chat",
+	.access = SNDRV_CTL_ELEM_ACCESS_READ | SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+	.info = steelseries_chatmix_info,
+	.get = steelseries_chatmix_chat_get,
+};
+
+static const struct snd_kcontrol_new steelseries_chatmix_game_control = {
+	.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+	.name = "ChatMix Game",
+	.access = SNDRV_CTL_ELEM_ACCESS_READ | SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+	.info = steelseries_chatmix_info,
+	.get = steelseries_chatmix_game_get,
+};
+
 static int steelseries_snd_register(struct steelseries_device *sd)
 {
 	struct hid_device *hdev = sd->hdev;
@@ -852,14 +970,32 @@ static int steelseries_snd_register(struct steelseries_device *sd)
 	snprintf(sd->card->longname, sizeof(sd->card->longname),
 		"%s at USB %s", hdev->name, dev_name(&hdev->dev));
 
-	ret = snd_card_register(sd->card);
-	if (ret < 0) {
-		snd_card_free(sd->card);
-		sd->card = NULL;
-		return ret;
+	if (sd->info->capabilities & SS_CAP_CHATMIX) {
+		struct snd_kcontrol *kctl;
+
+		kctl = snd_ctl_new1(&steelseries_chatmix_chat_control, sd);
+		ret = snd_ctl_add(sd->card, kctl);
+		if (ret < 0)
+			goto err_free_card;
+		sd->chatmix_chat_id = kctl->id;
+
+		kctl = snd_ctl_new1(&steelseries_chatmix_game_control, sd);
+		ret = snd_ctl_add(sd->card, kctl);
+		if (ret < 0)
+			goto err_free_card;
+		sd->chatmix_game_id = kctl->id;
 	}
 
+	ret = snd_card_register(sd->card);
+	if (ret < 0)
+		goto err_free_card;
+
 	return 0;
+
+err_free_card:
+	snd_card_free(sd->card);
+	sd->card = NULL;
+	return ret;
 }
 
 static void steelseries_snd_unregister(struct steelseries_device *sd)
@@ -877,6 +1013,8 @@ static int steelseries_raw_event(struct hid_device *hdev,
 	u8 old_capacity;
 	bool old_connected;
 	bool old_charging;
+	u8 old_chatmix_chat;
+	u8 old_chatmix_game;
 	bool is_async_interface = false;
 
 	if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1)
@@ -888,6 +1026,8 @@ static int steelseries_raw_event(struct hid_device *hdev,
 	old_capacity = sd->battery_capacity;
 	old_connected = sd->headset_connected;
 	old_charging = sd->battery_charging;
+	old_chatmix_chat = sd->chatmix_chat;
+	old_chatmix_game = sd->chatmix_game;
 
 	if (hid_is_usb(hdev)) {
 		struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
@@ -932,6 +1072,15 @@ static int steelseries_raw_event(struct hid_device *hdev,
 			power_supply_changed(sd->battery);
 	}
 
+	if (sd->card) {
+		if (sd->chatmix_chat != old_chatmix_chat)
+			snd_ctl_notify(sd->card, SNDRV_CTL_EVENT_MASK_VALUE,
+				       &sd->chatmix_chat_id);
+		if (sd->chatmix_game != old_chatmix_game)
+			snd_ctl_notify(sd->card, SNDRV_CTL_EVENT_MASK_VALUE,
+				       &sd->chatmix_game_id);
+	}
+
 	return 0;
 }
 
@@ -1159,7 +1308,7 @@ static const struct hid_device_id steelseries_devices[] = {
 	  .driver_data = (unsigned long)&arctis_nova_5_info },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
 			 USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5_X),
-	  .driver_data = (unsigned long)&arctis_nova_5_info },
+	  .driver_data = (unsigned long)&arctis_nova_5x_info },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
 			 USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7),
 	  .driver_data = (unsigned long)&arctis_nova_7_info },
@@ -1168,7 +1317,7 @@ static const struct hid_device_id steelseries_devices[] = {
 	  .driver_data = (unsigned long)&arctis_nova_7_gen2_info },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
 			 USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_P),
-	  .driver_data = (unsigned long)&arctis_nova_7_info },
+	  .driver_data = (unsigned long)&arctis_nova_7p_info },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
 			 USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X),
 	  .driver_data = (unsigned long)&arctis_nova_7_info },
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 08/18] HID: steelseries: Add mic mute ALSA mixer control
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
	Christian Mayer, Sriman Achanta
In-Reply-To: <20260227235042.410062-1-srimanachanta@gmail.com>

Expose the hardware microphone mute button state as a read-only volatile
ALSA boolean mixer control ("Mic Muted"). State is decoded from HID
events reported by the Arctis Nova 7 Gen2 (0xb0 status packet and 0x52
async event) and the Nova Pro (initial status packet). Changes are
propagated to userspace via snd_ctl_notify().

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

diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index 30ee9da1deac..3de8e1555263 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -23,6 +23,7 @@
 
 #define SS_CAP_BATTERY			BIT(0)
 #define SS_CAP_CHATMIX			BIT(1)
+#define SS_CAP_MIC_MUTE			BIT(2)
 
 #define SS_QUIRK_STATUS_SYNC_POLL	BIT(0)
 
@@ -56,8 +57,10 @@ struct steelseries_device {
 	struct snd_card *card;
 	struct snd_ctl_elem_id chatmix_chat_id;
 	struct snd_ctl_elem_id chatmix_game_id;
+	struct snd_ctl_elem_id mic_muted_id;
 	u8 chatmix_chat;
 	u8 chatmix_game;
+	bool mic_muted;
 
 	spinlock_t lock;
 	bool removed;
@@ -628,7 +631,7 @@ static void steelseries_arctis_nova_7_parse_status(struct steelseries_device *sd
 static void steelseries_arctis_nova_7_gen2_parse_status(struct steelseries_device *sd,
 							u8 *data, int size)
 {
-	if (size < 6)
+	if (size < 10)
 		return;
 
 	switch (data[0]) {
@@ -638,6 +641,7 @@ static void steelseries_arctis_nova_7_gen2_parse_status(struct steelseries_devic
 		sd->battery_charging = (data[3] == 0x01);
 		sd->chatmix_game = data[4];
 		sd->chatmix_chat = data[5];
+		sd->mic_muted = (data[9] == 0x01);
 		break;
 	case 0xb7:
 		sd->battery_capacity = data[1];
@@ -652,6 +656,9 @@ static void steelseries_arctis_nova_7_gen2_parse_status(struct steelseries_devic
 		sd->chatmix_game = data[1];
 		sd->chatmix_chat = data[2];
 		break;
+	case 0x52:
+		sd->mic_muted = (data[2] == 0x01);
+		break;
 	}
 }
 
@@ -665,6 +672,7 @@ static void steelseries_arctis_nova_pro_parse_status(struct steelseries_device *
 		sd->headset_connected = (data[15] == 0x08 || data[15] == 0x02);
 		sd->battery_capacity = steelseries_map_capacity(data[6], 0x00, 0x08);
 		sd->battery_charging = (data[15] == 0x02);
+		sd->mic_muted = (data[9] == 0x01);
 	} else if (data[0] == 0x07 && data[1] == 0x45) {
 		sd->chatmix_game = data[2];
 		sd->chatmix_chat = data[3];
@@ -752,14 +760,14 @@ static const struct steelseries_device_info arctis_nova_7p_info = {
 static const struct steelseries_device_info arctis_nova_7_gen2_info = {
 	.sync_interface = 3,
 	.async_interface = 5,
-	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX,
+	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_MIC_MUTE,
 	.request_status = steelseries_arctis_nova_request_status,
 	.parse_status = steelseries_arctis_nova_7_gen2_parse_status,
 };
 
 static const struct steelseries_device_info arctis_nova_pro_info = {
 	.sync_interface = 4,
-	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX,
+	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_MIC_MUTE,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
 	.request_status = steelseries_arctis_nova_pro_request_status,
 	.parse_status = steelseries_arctis_nova_pro_parse_status,
@@ -938,6 +946,29 @@ static int steelseries_chatmix_game_get(struct snd_kcontrol *kcontrol,
 	return 0;
 }
 
+static int steelseries_mic_muted_info(struct snd_kcontrol *kcontrol,
+				      struct snd_ctl_elem_info *uinfo)
+{
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
+	uinfo->count = 1;
+	uinfo->value.integer.min = 0;
+	uinfo->value.integer.max = 1;
+	uinfo->value.integer.step = 1;
+	return 0;
+}
+
+static int steelseries_mic_muted_get(struct snd_kcontrol *kcontrol,
+				     struct snd_ctl_elem_value *ucontrol)
+{
+	struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
+	unsigned long flags;
+
+	spin_lock_irqsave(&sd->lock, flags);
+	ucontrol->value.integer.value[0] = sd->mic_muted;
+	spin_unlock_irqrestore(&sd->lock, flags);
+	return 0;
+}
+
 static const struct snd_kcontrol_new steelseries_chatmix_chat_control = {
 	.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
 	.name = "ChatMix Chat",
@@ -954,6 +985,14 @@ static const struct snd_kcontrol_new steelseries_chatmix_game_control = {
 	.get = steelseries_chatmix_game_get,
 };
 
+static const struct snd_kcontrol_new steelseries_mic_muted_control = {
+	.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+	.name = "Mic Muted",
+	.access = SNDRV_CTL_ELEM_ACCESS_READ | SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+	.info = steelseries_mic_muted_info,
+	.get = steelseries_mic_muted_get,
+};
+
 static int steelseries_snd_register(struct steelseries_device *sd)
 {
 	struct hid_device *hdev = sd->hdev;
@@ -986,6 +1025,16 @@ static int steelseries_snd_register(struct steelseries_device *sd)
 		sd->chatmix_game_id = kctl->id;
 	}
 
+	if (sd->info->capabilities & SS_CAP_MIC_MUTE) {
+		struct snd_kcontrol *kctl;
+
+		kctl = snd_ctl_new1(&steelseries_mic_muted_control, sd);
+		ret = snd_ctl_add(sd->card, kctl);
+		if (ret < 0)
+			goto err_free_card;
+		sd->mic_muted_id = kctl->id;
+	}
+
 	ret = snd_card_register(sd->card);
 	if (ret < 0)
 		goto err_free_card;
@@ -1015,6 +1064,7 @@ static int steelseries_raw_event(struct hid_device *hdev,
 	bool old_charging;
 	u8 old_chatmix_chat;
 	u8 old_chatmix_game;
+	bool old_mic_muted;
 	bool is_async_interface = false;
 
 	if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1)
@@ -1028,6 +1078,7 @@ static int steelseries_raw_event(struct hid_device *hdev,
 	old_charging = sd->battery_charging;
 	old_chatmix_chat = sd->chatmix_chat;
 	old_chatmix_game = sd->chatmix_game;
+	old_mic_muted = sd->mic_muted;
 
 	if (hid_is_usb(hdev)) {
 		struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
@@ -1079,6 +1130,9 @@ static int steelseries_raw_event(struct hid_device *hdev,
 		if (sd->chatmix_game != old_chatmix_game)
 			snd_ctl_notify(sd->card, SNDRV_CTL_EVENT_MASK_VALUE,
 				       &sd->chatmix_game_id);
+		if (sd->mic_muted != old_mic_muted)
+			snd_ctl_notify(sd->card, SNDRV_CTL_EVENT_MASK_VALUE,
+				       &sd->mic_muted_id);
 	}
 
 	return 0;
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 06/18] HID: steelseries: Add ALSA sound card infrastructure
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
	Christian Mayer, Sriman Achanta
In-Reply-To: <20260227235042.410062-1-srimanachanta@gmail.com>

Register an ALSA sound card for each supported Arctis headset to expose
headset-specific audio controls to userspace. The card is created in
steelseries_snd_register() and freed in steelseries_snd_unregister(),
both guarded by a module compatibility check so that registration only
occurs when both SND and HID_STEELSERIES are built-in or are both
modules, avoiding a dependency mismatch.

The Kconfig entry is updated to add SND as a dependency. Subsequent
commits build on this infrastructure to register mixer controls.

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

diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index 29f05d4b7e30..fcdb5406159a 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -1139,7 +1139,7 @@ config STEAM_FF
 
 config HID_STEELSERIES
 	tristate "Steelseries devices support"
-	depends on USB_HID
+	depends on USB_HID && SND
 	help
 	Support for Steelseries SRW-S1 steering wheel, and the Steelseries
 	Arctis headset family (Arctis 1, Arctis 7, Arctis 7+, Arctis 9,
diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index d8ece8449255..b7f932cde98d 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -16,6 +16,7 @@
 #include <linux/power_supply.h>
 #include <linux/workqueue.h>
 #include <linux/spinlock.h>
+#include <sound/core.h>
 
 #include "hid-ids.h"
 
@@ -50,6 +51,8 @@ struct steelseries_device {
 	u8 battery_capacity;
 	bool battery_charging;
 
+	struct snd_card *card;
+
 	spinlock_t lock;
 	bool removed;
 };
@@ -830,6 +833,43 @@ static int steelseries_battery_register(struct steelseries_device *sd)
 	return 0;
 }
 
+#if IS_BUILTIN(CONFIG_SND) || \
+	(IS_MODULE(CONFIG_SND) && IS_MODULE(CONFIG_HID_STEELSERIES))
+
+static int steelseries_snd_register(struct steelseries_device *sd)
+{
+	struct hid_device *hdev = sd->hdev;
+	int ret;
+
+	ret = snd_card_new(&hdev->dev, -1, "SteelSeries", THIS_MODULE,
+			   0, &sd->card);
+	if (ret < 0)
+		return ret;
+
+	sd->card->private_data = sd;
+	strscpy(sd->card->driver, "SteelSeries");
+	strscpy(sd->card->shortname, hdev->name);
+	snprintf(sd->card->longname, sizeof(sd->card->longname),
+		"%s at USB %s", hdev->name, dev_name(&hdev->dev));
+
+	ret = snd_card_register(sd->card);
+	if (ret < 0) {
+		snd_card_free(sd->card);
+		sd->card = NULL;
+		return ret;
+	}
+
+	return 0;
+}
+
+static void steelseries_snd_unregister(struct steelseries_device *sd)
+{
+	if (sd->card)
+		snd_card_free(sd->card);
+}
+
+#endif
+
 static int steelseries_raw_event(struct hid_device *hdev,
 				 struct hid_report *report, u8 *data, int size)
 {
@@ -975,6 +1015,13 @@ static int steelseries_probe(struct hid_device *hdev,
 				hid_warn(hdev, "Failed to register battery: %d\n", ret);
 		}
 
+#if IS_BUILTIN(CONFIG_SND) || \
+	(IS_MODULE(CONFIG_SND) && IS_MODULE(CONFIG_HID_STEELSERIES))
+		ret = steelseries_snd_register(sd);
+		if (ret < 0)
+			hid_warn(hdev, "Failed to register sound card: %d\n", ret);
+#endif
+
 		INIT_DELAYED_WORK(&sd->status_work, steelseries_status_timer_work_handler);
 		schedule_delayed_work(&sd->status_work, msecs_to_jiffies(100));
 
@@ -1048,6 +1095,11 @@ static void steelseries_remove(struct hid_device *hdev)
 				hid_set_drvdata(sibling, NULL);
 		}
 
+#if IS_BUILTIN(CONFIG_SND) || \
+	(IS_MODULE(CONFIG_SND) && IS_MODULE(CONFIG_HID_STEELSERIES))
+		steelseries_snd_unregister(sd);
+#endif
+
 		spin_lock_irqsave(&sd->lock, flags);
 		sd->removed = true;
 		spin_unlock_irqrestore(&sd->lock, flags);
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 05/18] HID: steelseries: Update Kconfig help text for expanded headset support
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
	Christian Mayer, Sriman Achanta
In-Reply-To: <20260227235042.410062-1-srimanachanta@gmail.com>

The previous help text only mentioned the SRW-S1 steering wheel and Arctis 1 Wireless for Xbox. Update it to reflect the full Arctis headset family now supported.

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

diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index c1d9f7c6a5f2..29f05d4b7e30 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -1142,7 +1142,8 @@ config HID_STEELSERIES
 	depends on USB_HID
 	help
 	Support for Steelseries SRW-S1 steering wheel, and the Steelseries
-	Arctis 1 Wireless for XBox headset.
+	Arctis headset family (Arctis 1, Arctis 7, Arctis 7+, Arctis 9,
+	Arctis Nova 3, Arctis Nova 5, Arctis Nova 7, and Arctis Nova Pro).
 
 config HID_SUNPLUS
 	tristate "Sunplus wireless desktop"
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 04/18] HID: steelseries: Add async support and unify device definitions
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
	Christian Mayer, Sriman Achanta
In-Reply-To: <20260227235042.410062-1-srimanachanta@gmail.com>

Refactor the SteelSeries driver to improve scalability and support the
modern Arctis Nova headset lineup along with legacy models.

- Replace the bitmap-based quirk system with `struct
  steelseries_device_info` to encapsulate device-specific traits
  (product ID, name, capabilities, interfaces).
- Implement asynchronous battery monitoring. Devices that support async
  updates (like the Nova series) now rely on interrupt events rather
  than periodic polling, reducing overhead.
- Add support for complex multi-interface devices (e.g., Nova 7) where
  battery events arrive on a separate asynchronous interface.
- Consolidate battery request and report parsing logic. New helpers
  `steelseries_send_feature_report` and `steelseries_send_output_report`
  simplify command dispatch.
- Add support for over 20 new devices including the entire Arctis Nova
  series (3, 5, 7, Pro) and various Arctis 7/9/Pro variants.
- Clean up naming conventions (e.g., removing `_headset_` prefix from
  general functions) and improve locking in the battery timer.

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

diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index d3711022bf86..d8ece8449255 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -4,37 +4,54 @@
  *
  *  Copyright (c) 2013 Simon Wood
  *  Copyright (c) 2023 Bastien Nocera
+ *  Copyright (c) 2025 Sriman Achanta
  */
 
-/*
- */
-
+#include <linux/delay.h>
 #include <linux/device.h>
 #include <linux/hid.h>
 #include <linux/module.h>
 #include <linux/usb.h>
 #include <linux/leds.h>
+#include <linux/power_supply.h>
+#include <linux/workqueue.h>
+#include <linux/spinlock.h>
 
 #include "hid-ids.h"
 
-#define STEELSERIES_SRWS1		BIT(0)
-#define STEELSERIES_ARCTIS_1		BIT(1)
-#define STEELSERIES_ARCTIS_1_X		BIT(2)
-#define STEELSERIES_ARCTIS_9		BIT(3)
+#define SS_CAP_BATTERY			BIT(0)
+
+#define SS_QUIRK_STATUS_SYNC_POLL	BIT(0)
+
+struct steelseries_device;
+
+struct steelseries_device_info {
+	unsigned long capabilities;
+	unsigned long quirks;
+
+	u8 sync_interface;
+	u8 async_interface;
+
+	int (*request_status)(struct hid_device *hdev);
+	void (*parse_status)(struct steelseries_device *sd, u8 *data, int size);
+};
 
 struct steelseries_device {
 	struct hid_device *hdev;
-	unsigned long quirks;
+	const struct steelseries_device_info *info;
 
-	struct delayed_work battery_work;
-	spinlock_t lock;
-	bool removed;
+	bool use_async_protocol;
+
+	struct delayed_work status_work;
 
 	struct power_supply_desc battery_desc;
 	struct power_supply *battery;
-	uint8_t battery_capacity;
 	bool headset_connected;
+	u8 battery_capacity;
 	bool battery_charging;
+
+	spinlock_t lock;
+	bool removed;
 };
 
 #if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
@@ -341,53 +358,118 @@ static int steelseries_srws1_probe(struct hid_device *hdev,
 }
 #endif
 
-#define STEELSERIES_HEADSET_BATTERY_TIMEOUT_MS	3000
+static const __u8 *steelseries_srws1_report_fixup(struct hid_device *hdev,
+		__u8 *rdesc, unsigned int *rsize)
+{
+	if (hdev->vendor != USB_VENDOR_ID_STEELSERIES ||
+	    hdev->product != USB_DEVICE_ID_STEELSERIES_SRWS1)
+		return rdesc;
 
-#define ARCTIS_1_BATTERY_RESPONSE_LEN		8
-#define ARCTIS_9_BATTERY_RESPONSE_LEN		64
-static const char arctis_1_battery_request[] = { 0x06, 0x12 };
-static const char arctis_9_battery_request[] = { 0x00, 0x20 };
+	if (*rsize >= 115 && rdesc[11] == 0x02 && rdesc[13] == 0xc8
+			&& rdesc[29] == 0xbb && rdesc[40] == 0xc5) {
+		hid_info(hdev, "Fixing up Steelseries SRW-S1 report descriptor\n");
+		*rsize = sizeof(steelseries_srws1_rdesc_fixed);
+		return steelseries_srws1_rdesc_fixed;
+	}
+	return rdesc;
+}
 
-static int steelseries_headset_request_battery(struct hid_device *hdev,
-	const char *request, size_t len)
+/*
+ * Headset report helpers
+ */
+
+static int steelseries_send_report(struct hid_device *hdev, const u8 *data,
+				    int len, enum hid_report_type type)
 {
-	u8 *write_buf;
+	u8 *buf;
 	int ret;
 
-	/* Request battery information */
-	write_buf = kmemdup(request, len, GFP_KERNEL);
-	if (!write_buf)
+	buf = kmemdup(data, len, GFP_KERNEL);
+	if (!buf)
 		return -ENOMEM;
 
-	hid_dbg(hdev, "Sending battery request report");
-	ret = hid_hw_raw_request(hdev, request[0], write_buf, len,
-				 HID_OUTPUT_REPORT, HID_REQ_SET_REPORT);
-	if (ret < (int)len) {
-		hid_err(hdev, "hid_hw_raw_request() failed with %d\n", ret);
-		ret = -ENODATA;
-	}
+	ret = hid_hw_raw_request(hdev, data[0], buf, len, type,
+				 HID_REQ_SET_REPORT);
+	kfree(buf);
 
-	kfree(write_buf);
-	return ret;
+	if (ret < 0)
+		return ret;
+	if (ret < len)
+		return -EIO;
+
+	return 0;
 }
 
-static void steelseries_headset_fetch_battery(struct hid_device *hdev)
+static inline int steelseries_send_feature_report(struct hid_device *hdev,
+						   const u8 *data, int len)
 {
-	int ret = 0;
+	return steelseries_send_report(hdev, data, len, HID_FEATURE_REPORT);
+}
 
-	if (hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1 ||
-	    hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X)
-		ret = steelseries_headset_request_battery(hdev,
-			arctis_1_battery_request, sizeof(arctis_1_battery_request));
-	else if (hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_9)
-		ret = steelseries_headset_request_battery(hdev,
-			arctis_9_battery_request, sizeof(arctis_9_battery_request));
+static inline int steelseries_send_output_report(struct hid_device *hdev,
+						  const u8 *data, int len)
+{
+	return steelseries_send_report(hdev, data, len, HID_OUTPUT_REPORT);
+}
 
-	if (ret < 0)
-		hid_dbg(hdev,
-			"Battery query failed (err: %d)\n", ret);
+/*
+ * Headset status request functions
+ */
+
+static int steelseries_arctis_1_request_status(struct hid_device *hdev)
+{
+	const u8 data[] = { 0x06, 0x12 };
+
+	return steelseries_send_feature_report(hdev, data, sizeof(data));
+}
+
+static int steelseries_arctis_7_request_status(struct hid_device *hdev)
+{
+	int ret;
+	const u8 connection_data[] = { 0x06, 0x14 };
+	const u8 battery_data[] = { 0x06, 0x18 };
+
+	ret = steelseries_send_feature_report(hdev, connection_data, sizeof(connection_data));
+	if (ret)
+		return ret;
+
+	msleep(10);
+
+	return steelseries_send_feature_report(hdev, battery_data, sizeof(battery_data));
+}
+
+static int steelseries_arctis_9_request_status(struct hid_device *hdev)
+{
+	const u8 data[] = { 0x00, 0x20 };
+
+	return steelseries_send_feature_report(hdev, data, sizeof(data));
 }
 
+static int steelseries_arctis_nova_request_status(struct hid_device *hdev)
+{
+	const u8 data[] = { 0x00, 0xb0 };
+
+	return steelseries_send_output_report(hdev, data, sizeof(data));
+}
+
+static int steelseries_arctis_nova_3p_request_status(struct hid_device *hdev)
+{
+	const u8 data[] = { 0xb0 };
+
+	return steelseries_send_output_report(hdev, data, sizeof(data));
+}
+
+static int steelseries_arctis_nova_pro_request_status(struct hid_device *hdev)
+{
+	const u8 data[] = { 0x06, 0xb0 };
+
+	return steelseries_send_output_report(hdev, data, sizeof(data));
+}
+
+/*
+ * Headset battery helpers
+ */
+
 static int battery_capacity_to_level(int capacity)
 {
 	if (capacity >= 50)
@@ -397,19 +479,247 @@ static int battery_capacity_to_level(int capacity)
 	return POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL;
 }
 
-static void steelseries_headset_battery_timer_tick(struct work_struct *work)
+static u8 steelseries_map_capacity(u8 capacity, u8 min_in, u8 max_in)
+{
+	if (capacity >= max_in)
+		return 100;
+	if (capacity <= min_in)
+		return 0;
+	return (capacity - min_in) * 100 / (max_in - min_in);
+}
+
+/*
+ * Headset status parse functions
+ */
+
+static void steelseries_arctis_1_parse_status(struct steelseries_device *sd,
+					      u8 *data, int size)
+{
+	if (size < 4)
+		return;
+
+	sd->headset_connected = (data[2] != 0x01);
+	sd->battery_capacity = data[3];
+}
+
+static void steelseries_arctis_7_parse_status(struct steelseries_device *sd,
+					      u8 *data, int size)
+{
+	if (size < 3)
+		return;
+
+	if (data[0] == 0x06) {
+		if (data[1] == 0x14)
+			sd->headset_connected = (data[2] == 0x03);
+		else if (data[1] == 0x18)
+			sd->battery_capacity = data[2];
+	}
+}
+
+static void steelseries_arctis_7_plus_parse_status(struct steelseries_device *sd,
+						   u8 *data, int size)
+{
+	if (size < 4)
+		return;
+
+	if (data[0] == 0xb0) {
+		sd->headset_connected = !(data[1] == 0x01);
+		sd->battery_capacity = steelseries_map_capacity(data[2], 0x00, 0x04);
+		sd->battery_charging = (data[3] == 0x01);
+	}
+}
+
+static void steelseries_arctis_9_parse_status(struct steelseries_device *sd,
+					      u8 *data, int size)
+{
+	if (size < 5)
+		return;
+
+	if (data[0] == 0xaa) {
+		sd->headset_connected = (data[1] == 0x01);
+		sd->battery_charging = (data[4] == 0x01);
+		sd->battery_capacity = steelseries_map_capacity(data[3], 0x64, 0x9A);
+	}
+}
+
+static void steelseries_arctis_nova_3p_parse_status(struct steelseries_device *sd,
+						   u8 *data, int size)
+{
+	if (size < 4)
+		return;
+
+	if (data[0] == 0xb0) {
+		sd->headset_connected = !(data[1] == 0x02);
+		sd->battery_capacity = steelseries_map_capacity(data[3], 0x00, 0x64);
+	}
+}
+
+static void steelseries_arctis_nova_5_parse_status(struct steelseries_device *sd,
+						   u8 *data, int size)
+{
+	if (size < 5)
+		return;
+
+	if (data[0] == 0xb0) {
+		sd->headset_connected = !(data[1] == 0x02);
+		sd->battery_capacity = data[3];
+		sd->battery_charging = (data[4] == 0x01);
+	}
+}
+
+static void steelseries_arctis_nova_7_parse_status(struct steelseries_device *sd,
+						   u8 *data, int size)
+{
+	if (size < 4)
+		return;
+
+	if (data[0] == 0xb0) {
+		sd->headset_connected = (data[1] == 0x03);
+		sd->battery_capacity = steelseries_map_capacity(data[2], 0x00, 0x04);
+		sd->battery_charging = (data[3] == 0x01);
+	}
+}
+
+static void steelseries_arctis_nova_7_gen2_parse_status(struct steelseries_device *sd,
+							u8 *data, int size)
+{
+	if (size < 4)
+		return;
+
+	switch (data[0]) {
+	case 0xb0:
+		sd->headset_connected = (data[1] == 0x03);
+		sd->battery_capacity = data[2];
+		sd->battery_charging = (data[3] == 0x01);
+		break;
+	case 0xb7:
+		sd->battery_capacity = data[1];
+		break;
+	case 0xb9:
+		sd->headset_connected = (data[1] == 0x03);
+		break;
+	case 0xbb:
+		sd->battery_charging = (data[1] == 0x01);
+		break;
+	}
+}
+
+static void steelseries_arctis_nova_pro_parse_status(struct steelseries_device *sd,
+						     u8 *data, int size)
+{
+	if (size < 16)
+		return;
+
+	if (data[0] == 0x06 && data[1] == 0xb0) {
+		sd->headset_connected = (data[15] == 0x08 || data[15] == 0x02);
+		sd->battery_capacity = steelseries_map_capacity(data[6], 0x00, 0x08);
+		sd->battery_charging = (data[15] == 0x02);
+	}
+}
+
+/*
+ * Device info definitions
+ */
+
+static const struct steelseries_device_info srws1_info = { };
+
+static const struct steelseries_device_info arctis_1_info = {
+	.sync_interface = 3,
+	.capabilities = SS_CAP_BATTERY,
+	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
+	.request_status = steelseries_arctis_1_request_status,
+	.parse_status = steelseries_arctis_1_parse_status,
+};
+
+static const struct steelseries_device_info arctis_7_info = {
+	.sync_interface = 5,
+	.capabilities = SS_CAP_BATTERY,
+	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
+	.request_status = steelseries_arctis_7_request_status,
+	.parse_status = steelseries_arctis_7_parse_status,
+};
+
+static const struct steelseries_device_info arctis_7_plus_info = {
+	.sync_interface = 3,
+	.capabilities = SS_CAP_BATTERY,
+	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
+	.request_status = steelseries_arctis_nova_request_status,
+	.parse_status = steelseries_arctis_7_plus_parse_status,
+};
+
+static const struct steelseries_device_info arctis_9_info = {
+	.sync_interface = 0,
+	.capabilities = SS_CAP_BATTERY,
+	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
+	.request_status = steelseries_arctis_9_request_status,
+	.parse_status = steelseries_arctis_9_parse_status,
+};
+
+static const struct steelseries_device_info arctis_nova_3p_info = {
+	.sync_interface = 4,
+	.capabilities = SS_CAP_BATTERY,
+	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
+	.request_status = steelseries_arctis_nova_3p_request_status,
+	.parse_status = steelseries_arctis_nova_3p_parse_status,
+};
+
+static const struct steelseries_device_info arctis_nova_5_info = {
+	.sync_interface = 3,
+	.capabilities = SS_CAP_BATTERY,
+	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
+	.request_status = steelseries_arctis_nova_request_status,
+	.parse_status = steelseries_arctis_nova_5_parse_status,
+};
+
+static const struct steelseries_device_info arctis_nova_7_info = {
+	.sync_interface = 3,
+	.capabilities = SS_CAP_BATTERY,
+	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
+	.request_status = steelseries_arctis_nova_request_status,
+	.parse_status = steelseries_arctis_nova_7_parse_status,
+};
+
+static const struct steelseries_device_info arctis_nova_7_gen2_info = {
+	.sync_interface = 3,
+	.async_interface = 5,
+	.capabilities = SS_CAP_BATTERY,
+	.request_status = steelseries_arctis_nova_request_status,
+	.parse_status = steelseries_arctis_nova_7_gen2_parse_status,
+};
+
+static const struct steelseries_device_info arctis_nova_pro_info = {
+	.sync_interface = 4,
+	.capabilities = SS_CAP_BATTERY,
+	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
+	.request_status = steelseries_arctis_nova_pro_request_status,
+	.parse_status = steelseries_arctis_nova_pro_parse_status,
+};
+
+/*
+ * Headset wireless status and battery infrastructure
+ */
+
+#define STEELSERIES_HEADSET_STATUS_TIMEOUT_MS	3000
+
+static void
+steelseries_headset_set_wireless_status(struct hid_device *hdev,
+					bool connected)
 {
-	struct steelseries_device *sd = container_of(work,
-		struct steelseries_device, battery_work.work);
-	struct hid_device *hdev = sd->hdev;
+	struct usb_interface *intf;
+
+	if (!hid_is_usb(hdev))
+		return;
 
-	steelseries_headset_fetch_battery(hdev);
+	intf = to_usb_interface(hdev->dev.parent);
+	usb_set_wireless_status(intf, connected ?
+				USB_WIRELESS_STATUS_CONNECTED :
+				USB_WIRELESS_STATUS_DISCONNECTED);
 }
 
 #define STEELSERIES_PREFIX "SteelSeries "
 #define STEELSERIES_PREFIX_LEN strlen(STEELSERIES_PREFIX)
 
-static int steelseries_headset_battery_get_property(struct power_supply *psy,
+static int steelseries_battery_get_property(struct power_supply *psy,
 				enum power_supply_property psp,
 				union power_supply_propval *val)
 {
@@ -452,22 +762,7 @@ static int steelseries_headset_battery_get_property(struct power_supply *psy,
 	return ret;
 }
 
-static void
-steelseries_headset_set_wireless_status(struct hid_device *hdev,
-					bool connected)
-{
-	struct usb_interface *intf;
-
-	if (!hid_is_usb(hdev))
-		return;
-
-	intf = to_usb_interface(hdev->dev.parent);
-	usb_set_wireless_status(intf, connected ?
-				USB_WIRELESS_STATUS_CONNECTED :
-				USB_WIRELESS_STATUS_DISCONNECTED);
-}
-
-static enum power_supply_property steelseries_headset_battery_props[] = {
+static enum power_supply_property steelseries_battery_props[] = {
 	POWER_SUPPLY_PROP_MODEL_NAME,
 	POWER_SUPPLY_PROP_MANUFACTURER,
 	POWER_SUPPLY_PROP_PRESENT,
@@ -477,7 +772,26 @@ static enum power_supply_property steelseries_headset_battery_props[] = {
 	POWER_SUPPLY_PROP_CAPACITY_LEVEL,
 };
 
-static int steelseries_headset_battery_register(struct steelseries_device *sd)
+/*
+ * Delayed work handlers for status polling and settings requests
+ */
+
+static void steelseries_status_timer_work_handler(struct work_struct *work)
+{
+	struct steelseries_device *sd = container_of(
+		work, struct steelseries_device, status_work.work);
+	unsigned long flags;
+
+	sd->info->request_status(sd->hdev);
+
+	spin_lock_irqsave(&sd->lock, flags);
+	if (!sd->removed && !sd->use_async_protocol)
+		schedule_delayed_work(&sd->status_work,
+				msecs_to_jiffies(STEELSERIES_HEADSET_STATUS_TIMEOUT_MS));
+	spin_unlock_irqrestore(&sd->lock, flags);
+}
+
+static int steelseries_battery_register(struct steelseries_device *sd)
 {
 	static atomic_t battery_no = ATOMIC_INIT(0);
 	struct power_supply_config battery_cfg = { .drv_data = sd, };
@@ -485,9 +799,9 @@ static int steelseries_headset_battery_register(struct steelseries_device *sd)
 	int ret;
 
 	sd->battery_desc.type = POWER_SUPPLY_TYPE_BATTERY;
-	sd->battery_desc.properties = steelseries_headset_battery_props;
-	sd->battery_desc.num_properties = ARRAY_SIZE(steelseries_headset_battery_props);
-	sd->battery_desc.get_property = steelseries_headset_battery_get_property;
+	sd->battery_desc.properties = steelseries_battery_props;
+	sd->battery_desc.num_properties = ARRAY_SIZE(steelseries_battery_props);
+	sd->battery_desc.get_property = steelseries_battery_get_property;
 	sd->battery_desc.use_for_apm = 0;
 	n = atomic_inc_return(&battery_no) - 1;
 	sd->battery_desc.name = devm_kasprintf(&sd->hdev->dev, GFP_KERNEL,
@@ -496,14 +810,16 @@ static int steelseries_headset_battery_register(struct steelseries_device *sd)
 		return -ENOMEM;
 
 	/* avoid the warning of 0% battery while waiting for the first info */
-	steelseries_headset_set_wireless_status(sd->hdev, false);
 	sd->battery_capacity = 100;
 	sd->battery_charging = false;
+	sd->headset_connected = false;
+	steelseries_headset_set_wireless_status(sd->hdev, false);
 
 	sd->battery = devm_power_supply_register(&sd->hdev->dev,
 			&sd->battery_desc, &battery_cfg);
 	if (IS_ERR(sd->battery)) {
 		ret = PTR_ERR(sd->battery);
+		sd->battery = NULL;
 		hid_err(sd->hdev,
 				"%s:power_supply_register failed with error %d\n",
 				__func__, ret);
@@ -511,68 +827,185 @@ static int steelseries_headset_battery_register(struct steelseries_device *sd)
 	}
 	power_supply_powers(sd->battery, &sd->hdev->dev);
 
-	INIT_DELAYED_WORK(&sd->battery_work, steelseries_headset_battery_timer_tick);
-	steelseries_headset_fetch_battery(sd->hdev);
+	return 0;
+}
+
+static int steelseries_raw_event(struct hid_device *hdev,
+				 struct hid_report *report, u8 *data, int size)
+{
+	struct steelseries_device *sd = hid_get_drvdata(hdev);
+	u8 old_capacity;
+	bool old_connected;
+	bool old_charging;
+	bool is_async_interface = false;
+
+	if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1)
+		return 0;
+
+	if (!sd)
+		return 0;
+
+	old_capacity = sd->battery_capacity;
+	old_connected = sd->headset_connected;
+	old_charging = sd->battery_charging;
+
+	if (hid_is_usb(hdev)) {
+		struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
+
+		is_async_interface = (intf->cur_altsetting->desc.bInterfaceNumber ==
+				      sd->info->async_interface);
+	}
+
+	sd->info->parse_status(sd, data, size);
+
+	if (sd->headset_connected != old_connected) {
+		hid_dbg(hdev,
+			"Connected status changed from %sconnected to %sconnected\n",
+			old_connected ? "" : "not ",
+			sd->headset_connected ? "" : "not ");
+
+		if (sd->headset_connected && !old_connected &&
+		    sd->use_async_protocol && is_async_interface) {
+			schedule_delayed_work(&sd->status_work, 0);
+		}
 
-	if (sd->quirks & STEELSERIES_ARCTIS_9) {
-		/* The first fetch_battery request can remain unanswered in some cases */
-		schedule_delayed_work(&sd->battery_work,
-				msecs_to_jiffies(STEELSERIES_HEADSET_BATTERY_TIMEOUT_MS));
+		if (sd->battery) {
+			steelseries_headset_set_wireless_status(sd->hdev,
+							       sd->headset_connected);
+			power_supply_changed(sd->battery);
+		}
+	}
+
+	if (sd->battery_capacity != old_capacity) {
+		hid_dbg(hdev, "Battery capacity changed from %d%% to %d%%\n",
+			old_capacity, sd->battery_capacity);
+		if (sd->battery)
+			power_supply_changed(sd->battery);
+	}
+
+	if (sd->battery_charging != old_charging) {
+		hid_dbg(hdev,
+			"Battery charging status changed from %scharging to %scharging\n",
+			old_charging ? "" : "not ",
+			sd->battery_charging ? "" : "not ");
+		if (sd->battery)
+			power_supply_changed(sd->battery);
 	}
 
 	return 0;
 }
 
-static bool steelseries_is_vendor_usage_page(struct hid_device *hdev, uint8_t usage_page)
+static struct hid_device *steelseries_get_sibling_hdev(struct hid_device *hdev,
+						       int interface_num)
 {
-	return hdev->rdesc[0] == 0x06 &&
-		hdev->rdesc[1] == usage_page &&
-		hdev->rdesc[2] == 0xff;
+	struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
+	struct usb_device *usb_dev = interface_to_usbdev(intf);
+	struct usb_interface *sibling_intf;
+	struct hid_device *sibling_hdev;
+
+	sibling_intf = usb_ifnum_to_if(usb_dev, interface_num);
+	if (!sibling_intf)
+		return NULL;
+
+	sibling_hdev = usb_get_intfdata(sibling_intf);
+
+	return sibling_hdev;
 }
 
-static int steelseries_probe(struct hid_device *hdev, const struct hid_device_id *id)
+static int steelseries_probe(struct hid_device *hdev,
+			     const struct hid_device_id *id)
 {
+	const struct steelseries_device_info *info =
+		(const struct steelseries_device_info *)id->driver_data;
 	struct steelseries_device *sd;
+	struct usb_interface *intf;
+	struct hid_device *master_hdev;
+	u8 interface_num;
 	int ret;
 
 	if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1) {
 #if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
-    (IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
+	(IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
 		return steelseries_srws1_probe(hdev, id);
 #else
 		return -ENODEV;
 #endif
 	}
 
-	sd = devm_kzalloc(&hdev->dev, sizeof(*sd), GFP_KERNEL);
-	if (!sd)
-		return -ENOMEM;
-	hid_set_drvdata(hdev, sd);
-	sd->hdev = hdev;
-	sd->quirks = id->driver_data;
+	if (hid_is_usb(hdev)) {
+		intf = to_usb_interface(hdev->dev.parent);
+		interface_num = intf->cur_altsetting->desc.bInterfaceNumber;
+	} else {
+		return -ENODEV;
+	}
 
 	ret = hid_parse(hdev);
 	if (ret)
 		return ret;
 
-	if (sd->quirks & STEELSERIES_ARCTIS_9 &&
-			!steelseries_is_vendor_usage_page(hdev, 0xc0))
-		return -ENODEV;
+	/* Let hid-generic handle non-vendor or unknown interfaces */
+	if (interface_num != info->sync_interface &&
+	    (!info->async_interface || interface_num != info->async_interface))
+		return hid_hw_start(hdev, HID_CONNECT_DEFAULT);
 
-	spin_lock_init(&sd->lock);
+	if (interface_num == info->sync_interface) {
+		sd = devm_kzalloc(&hdev->dev, sizeof(*sd), GFP_KERNEL);
+		if (!sd)
+			return -ENOMEM;
 
-	ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
-	if (ret)
-		return ret;
+		sd->hdev = hdev;
+		sd->info = info;
+		spin_lock_init(&sd->lock);
 
-	ret = hid_hw_open(hdev);
-	if (ret)
-		return ret;
+		hid_set_drvdata(hdev, sd);
 
-	if (steelseries_headset_battery_register(sd) < 0)
-		hid_err(sd->hdev,
-			"Failed to register battery for headset\n");
+		ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+		if (ret)
+			return ret;
+
+		ret = hid_hw_open(hdev);
+		if (ret)
+			goto err_stop;
+
+		sd->use_async_protocol = !(info->quirks & SS_QUIRK_STATUS_SYNC_POLL);
+
+		if (info->capabilities & SS_CAP_BATTERY) {
+			ret = steelseries_battery_register(sd);
+			if (ret < 0)
+				hid_warn(hdev, "Failed to register battery: %d\n", ret);
+		}
+
+		INIT_DELAYED_WORK(&sd->status_work, steelseries_status_timer_work_handler);
+		schedule_delayed_work(&sd->status_work, msecs_to_jiffies(100));
+
+		return 0;
+	}
+
+	if (info->async_interface && interface_num == info->async_interface) {
+		master_hdev = steelseries_get_sibling_hdev(hdev, info->sync_interface);
 
+		if (!master_hdev || !hid_get_drvdata(master_hdev))
+			return -EPROBE_DEFER;
+
+		sd = hid_get_drvdata(master_hdev);
+		hid_set_drvdata(hdev, sd);
+
+		ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+		if (ret)
+			return ret;
+
+		ret = hid_hw_open(hdev);
+		if (ret) {
+			hid_hw_stop(hdev);
+			return ret;
+		}
+		return 0;
+	}
+
+	return -ENODEV;
+
+err_stop:
+	hid_hw_stop(hdev);
 	return ret;
 }
 
@@ -580,166 +1013,144 @@ static void steelseries_remove(struct hid_device *hdev)
 {
 	struct steelseries_device *sd;
 	unsigned long flags;
+	struct usb_interface *intf;
+	u8 interface_num;
 
 	if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1) {
 #if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
-    (IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
+	(IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
 		hid_hw_stop(hdev);
 #endif
 		return;
 	}
 
-	sd = hid_get_drvdata(hdev);
-
-	spin_lock_irqsave(&sd->lock, flags);
-	sd->removed = true;
-	spin_unlock_irqrestore(&sd->lock, flags);
-
-	cancel_delayed_work_sync(&sd->battery_work);
-
-	hid_hw_close(hdev);
-	hid_hw_stop(hdev);
-}
-
-static const __u8 *steelseries_srws1_report_fixup(struct hid_device *hdev,
-		__u8 *rdesc, unsigned int *rsize)
-{
-	if (hdev->vendor != USB_VENDOR_ID_STEELSERIES ||
-	    hdev->product != USB_DEVICE_ID_STEELSERIES_SRWS1)
-		return rdesc;
-
-	if (*rsize >= 115 && rdesc[11] == 0x02 && rdesc[13] == 0xc8
-			&& rdesc[29] == 0xbb && rdesc[40] == 0xc5) {
-		hid_info(hdev, "Fixing up Steelseries SRW-S1 report descriptor\n");
-		*rsize = sizeof(steelseries_srws1_rdesc_fixed);
-		return steelseries_srws1_rdesc_fixed;
+	if (hid_is_usb(hdev)) {
+		intf = to_usb_interface(hdev->dev.parent);
+		interface_num = intf->cur_altsetting->desc.bInterfaceNumber;
+	} else {
+		return;
 	}
-	return rdesc;
-}
 
-static uint8_t steelseries_headset_map_capacity(uint8_t capacity, uint8_t min_in, uint8_t max_in)
-{
-	if (capacity >= max_in)
-		return 100;
-	if (capacity <= min_in)
-		return 0;
-	return (capacity - min_in) * 100 / (max_in - min_in);
-}
-
-static int steelseries_headset_raw_event(struct hid_device *hdev,
-					struct hid_report *report, u8 *read_buf,
-					int size)
-{
-	struct steelseries_device *sd = hid_get_drvdata(hdev);
-	int capacity = sd->battery_capacity;
-	bool connected = sd->headset_connected;
-	bool charging = sd->battery_charging;
-	unsigned long flags;
-
-	/* Not a headset */
-	if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1)
-		return 0;
+	sd = hid_get_drvdata(hdev);
 
-	if (hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1 ||
-	    hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X) {
-		hid_dbg(sd->hdev,
-			"Parsing raw event for Arctis 1 headset (%*ph)\n", size, read_buf);
-		if (size < ARCTIS_1_BATTERY_RESPONSE_LEN ||
-		    memcmp(read_buf, arctis_1_battery_request, sizeof(arctis_1_battery_request))) {
-			if (!delayed_work_pending(&sd->battery_work))
-				goto request_battery;
-			return 0;
-		}
-		if (read_buf[2] == 0x01) {
-			connected = false;
-			capacity = 100;
-		} else {
-			connected = true;
-			capacity = read_buf[3];
-		}
+	if (!sd) {
+		hid_hw_stop(hdev);
+		return;
 	}
 
-	if (hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_9) {
-		hid_dbg(sd->hdev,
-			"Parsing raw event for Arctis 9 headset (%*ph)\n", size, read_buf);
-		if (size < ARCTIS_9_BATTERY_RESPONSE_LEN) {
-			if (!delayed_work_pending(&sd->battery_work))
-				goto request_battery;
-			return 0;
-		}
+	if (interface_num == sd->info->sync_interface) {
+		if (sd->info->async_interface) {
+			struct hid_device *sibling;
 
-		if (read_buf[0] == 0xaa && read_buf[1] == 0x01) {
-			connected = true;
-			charging = read_buf[4] == 0x01;
-
-			/*
-			 * Found no official documentation about min and max.
-			 * Values defined by testing.
-			 */
-			capacity = steelseries_headset_map_capacity(read_buf[3], 0x68, 0x9d);
-		} else {
-			/*
-			 * Device is off and sends the last known status read_buf[1] == 0x03 or
-			 * there is no known status of the device read_buf[0] == 0x55
-			 */
-			connected = false;
-			charging = false;
+			sibling = steelseries_get_sibling_hdev(hdev,
+							       sd->info->async_interface);
+			if (sibling)
+				hid_set_drvdata(sibling, NULL);
 		}
-	}
 
-	if (connected != sd->headset_connected) {
-		hid_dbg(sd->hdev,
-			"Connected status changed from %sconnected to %sconnected\n",
-			sd->headset_connected ? "" : "not ",
-			connected ? "" : "not ");
-		sd->headset_connected = connected;
-		steelseries_headset_set_wireless_status(hdev, connected);
-	}
+		spin_lock_irqsave(&sd->lock, flags);
+		sd->removed = true;
+		spin_unlock_irqrestore(&sd->lock, flags);
 
-	if (capacity != sd->battery_capacity) {
-		hid_dbg(sd->hdev,
-			"Battery capacity changed from %d%% to %d%%\n",
-			sd->battery_capacity, capacity);
-		sd->battery_capacity = capacity;
-		power_supply_changed(sd->battery);
-	}
-
-	if (charging != sd->battery_charging) {
-		hid_dbg(sd->hdev,
-			"Battery charging status changed from %scharging to %scharging\n",
-			sd->battery_charging ? "" : "not ",
-			charging ? "" : "not ");
-		sd->battery_charging = charging;
-		power_supply_changed(sd->battery);
+		cancel_delayed_work_sync(&sd->status_work);
 	}
 
-request_battery:
-	spin_lock_irqsave(&sd->lock, flags);
-	if (!sd->removed)
-		schedule_delayed_work(&sd->battery_work,
-				msecs_to_jiffies(STEELSERIES_HEADSET_BATTERY_TIMEOUT_MS));
-	spin_unlock_irqrestore(&sd->lock, flags);
-
-	return 0;
+	hid_hw_close(hdev);
+	hid_hw_stop(hdev);
 }
 
 static const struct hid_device_id steelseries_devices[] = {
-	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_SRWS1),
-	  .driver_data = STEELSERIES_SRWS1 },
-
-	{ /* SteelSeries Arctis 1 Wireless */
-	  HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_1),
-	  .driver_data = STEELSERIES_ARCTIS_1 },
-
-	{ /* SteelSeries Arctis 1 Wireless for XBox */
-	  HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X),
-	  .driver_data = STEELSERIES_ARCTIS_1_X },
-
-	{ /* SteelSeries Arctis 9 Wireless for XBox */
-	  HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_9),
-	  .driver_data = STEELSERIES_ARCTIS_9 },
-
-	{ }
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_SRWS1),
+	  .driver_data = (unsigned long)&srws1_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_1),
+	  .driver_data = (unsigned long)&arctis_1_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X),
+	  .driver_data = (unsigned long)&arctis_1_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_7),
+	  .driver_data = (unsigned long)&arctis_7_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_7_P),
+	  .driver_data = (unsigned long)&arctis_1_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_7_X),
+	  .driver_data = (unsigned long)&arctis_1_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_7_GEN2),
+	  .driver_data = (unsigned long)&arctis_7_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS),
+	  .driver_data = (unsigned long)&arctis_7_plus_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_P),
+	  .driver_data = (unsigned long)&arctis_7_plus_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_X),
+	  .driver_data = (unsigned long)&arctis_7_plus_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_DESTINY),
+	  .driver_data = (unsigned long)&arctis_7_plus_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_9),
+	  .driver_data = (unsigned long)&arctis_9_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_P),
+	  .driver_data = (unsigned long)&arctis_nova_3p_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_X),
+	  .driver_data = (unsigned long)&arctis_nova_3p_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5),
+	  .driver_data = (unsigned long)&arctis_nova_5_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5_X),
+	  .driver_data = (unsigned long)&arctis_nova_5_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7),
+	  .driver_data = (unsigned long)&arctis_nova_7_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_2),
+	  .driver_data = (unsigned long)&arctis_nova_7_gen2_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_P),
+	  .driver_data = (unsigned long)&arctis_nova_7_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X),
+	  .driver_data = (unsigned long)&arctis_nova_7_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_2),
+	  .driver_data = (unsigned long)&arctis_nova_7_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_3),
+	  .driver_data = (unsigned long)&arctis_nova_7_gen2_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO),
+	  .driver_data = (unsigned long)&arctis_nova_7_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO_2),
+	  .driver_data = (unsigned long)&arctis_nova_7_gen2_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_WOW),
+	  .driver_data = (unsigned long)&arctis_nova_7_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_GEN2),
+	  .driver_data = (unsigned long)&arctis_nova_7_gen2_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2),
+	  .driver_data = (unsigned long)&arctis_nova_7_gen2_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2_2),
+	  .driver_data = (unsigned long)&arctis_nova_7_gen2_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_PRO),
+	  .driver_data = (unsigned long)&arctis_nova_pro_info },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+			 USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_PRO_X),
+	  .driver_data = (unsigned long)&arctis_nova_pro_info },
+	{}
 };
 MODULE_DEVICE_TABLE(hid, steelseries_devices);
 
@@ -749,7 +1160,7 @@ static struct hid_driver steelseries_driver = {
 	.probe = steelseries_probe,
 	.remove = steelseries_remove,
 	.report_fixup = steelseries_srws1_report_fixup,
-	.raw_event = steelseries_headset_raw_event,
+	.raw_event = steelseries_raw_event,
 };
 
 module_hid_driver(steelseries_driver);
@@ -758,3 +1169,4 @@ MODULE_LICENSE("GPL");
 MODULE_AUTHOR("Bastien Nocera <hadess@hadess.net>");
 MODULE_AUTHOR("Simon Wood <simon@mungewell.org>");
 MODULE_AUTHOR("Christian Mayer <git@mayer-bgk.de>");
+MODULE_AUTHOR("Sriman Achanta <srimanachanta@gmail.com>");
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 03/18] HID: quirks: Add additional Arctis headset device IDs
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
	Christian Mayer, Sriman Achanta
In-Reply-To: <20260227235042.410062-1-srimanachanta@gmail.com>

Add support for additional SteelSeries Arctis headset models to the HID
quirks table. This enables proper device recognition and handling for:
- Arctis 7 series (7, 7P, 7X, 7 Gen2)
- Arctis 7 Plus series (7 Plus, 7 Plus P, 7 Plus X, 7 Plus Destiny)
- Arctis Pro
- Arctis Nova 3 series (3, 3P, 3X)
- Arctis Nova 5 series (5, 5X)
- Arctis Nova 7 series (7, 7X, 7P, 7X Rev2, 7 Diablo, 7 WoW, 7 Gen2, 7X
  Gen2)
- Arctis Nova Pro series (Pro, Pro X)

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

diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c
index 17349eac5c3e..65a6f6ab30b9 100644
--- a/drivers/hid/hid-quirks.c
+++ b/drivers/hid/hid-quirks.c
@@ -714,7 +714,33 @@ static const struct hid_device_id hid_have_special_driver[] = {
 	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_SRWS1) },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_1) },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_7) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_7_P) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_7_X) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_7_GEN2) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_P) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_X) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_DESTINY) },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_9) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_P) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_X) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5_X) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_2) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_P) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_2) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_3) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO_2) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_WOW) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_GEN2) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2_2) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_PRO) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_PRO_X) },
 #endif
 #if IS_ENABLED(CONFIG_HID_SUNPLUS)
 	{ HID_USB_DEVICE(USB_VENDOR_ID_SUNPLUS, USB_DEVICE_ID_SUNPLUS_WDESKTOP) },
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 02/18] HID: hid-ids: Add SteelSeries Arctis headset device IDs
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
	Christian Mayer, Sriman Achanta
In-Reply-To: <20260227235042.410062-1-srimanachanta@gmail.com>

Add USB device IDs for the complete SteelSeries Arctis headset lineup,
including:
- Arctis 1, 1 Wireless, 7, 7P, 7X variants
- Arctis 7+ series (PS5, Xbox, Destiny editions)
- Arctis 9 Wireless
- Arctis Pro Wireless
- Arctis Nova 3, 3P, 3X
- Arctis Nova 5, 5X
- Arctis Nova 7 series (multiple variants and special editions)
- Arctis Nova Pro Wireless and Pro X

These IDs will be used by the updated hid-steelseries driver to provide
battery monitoring, sidetone control, and other device-specific features
for these wireless gaming headsets.

Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
 drivers/hid/hid-ids.h | 36 +++++++++++++++++++++++++++++++-----
 1 file changed, 31 insertions(+), 5 deletions(-)

diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index b01704f37142..e16b8dc5edd0 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1327,11 +1327,37 @@
 #define USB_DEVICE_ID_STEAM_CONTROLLER_WIRELESS	0x1142
 #define USB_DEVICE_ID_STEAM_DECK	0x1205
 
-#define USB_VENDOR_ID_STEELSERIES	0x1038
-#define USB_DEVICE_ID_STEELSERIES_SRWS1	0x1410
-#define USB_DEVICE_ID_STEELSERIES_ARCTIS_1	0x12b3
-#define USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X	0x12b6
-#define USB_DEVICE_ID_STEELSERIES_ARCTIS_9	0x12c2
+#define USB_VENDOR_ID_STEELSERIES				0x1038
+#define USB_DEVICE_ID_STEELSERIES_SRWS1				0x1410
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_1			0x12b3
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X			0x12b6
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_7			0x1260
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_7_P			0x12d5
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_7_X			0x12d7
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_7_GEN2			0x12ad
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS			0x220e
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_P		0x2212
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_X		0x2216
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_DESTINY		0x2236
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_9			0x12c2
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_P		0x2269
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_X		0x226d
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5			0x2232
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5_X		0x2253
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7			0x2202
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_2		0x22a1
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_P		0x220a
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X		0x2206
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_2		0x22a4
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_3		0x22a5
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO		0x223a
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO_2	0x22a9
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_WOW		0x227a
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_GEN2		0x227e
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2		0x229e
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2_2	0x2258
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_PRO		0x12e0
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_PRO_X		0x12e5
 
 #define USB_VENDOR_ID_SUN		0x0430
 #define USB_DEVICE_ID_RARITAN_KVM_DONGLE	0xcdab
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 01/18] HID: steelseries: Fix ARCTIS_1_X device mislabeling
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
	Christian Mayer, Sriman Achanta
In-Reply-To: <20260227235042.410062-1-srimanachanta@gmail.com>

The SteelSeries Arctis 1 Wireless for Xbox (device ID 0x12b6) was
previously mislabeled as the regular Arctis 1 Wireless (device ID
0x12b3). This commit corrects the labeling by introducing a new
STEELSERIES_ARCTIS_1_X quirk flag and device table entry.

Both the Arctis 1 and Arctis 1 Wireless for Xbox share the same battery
reporting protocol, so they are handled identically in the battery fetch
and raw event processing functions.

Changes:
- Add STEELSERIES_ARCTIS_1_X quirk flag definition
- Shift STEELSERIES_ARCTIS_9 quirk flag bit accordingly
- Add device table entry for USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X
- Update steelseries_headset_fetch_battery() to handle both Arctis 1
  variants
- Update steelseries_headset_raw_event() to handle both Arctis 1
  variants
- Update device comment to clarify Arctis 1 vs Arctis 1 Wireless for
  Xbox

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

diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 3e299a30dcde..b01704f37142 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1329,8 +1329,9 @@
 
 #define USB_VENDOR_ID_STEELSERIES	0x1038
 #define USB_DEVICE_ID_STEELSERIES_SRWS1	0x1410
-#define USB_DEVICE_ID_STEELSERIES_ARCTIS_1  0x12b6
-#define USB_DEVICE_ID_STEELSERIES_ARCTIS_9  0x12c2
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_1	0x12b3
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X	0x12b6
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_9	0x12c2
 
 #define USB_VENDOR_ID_SUN		0x0430
 #define USB_DEVICE_ID_RARITAN_KVM_DONGLE	0xcdab
diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c
index edc4339adb50..17349eac5c3e 100644
--- a/drivers/hid/hid-quirks.c
+++ b/drivers/hid/hid-quirks.c
@@ -713,6 +713,7 @@ static const struct hid_device_id hid_have_special_driver[] = {
 #if IS_ENABLED(CONFIG_HID_STEELSERIES)
 	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_SRWS1) },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_1) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X) },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_9) },
 #endif
 #if IS_ENABLED(CONFIG_HID_SUNPLUS)
diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index f98435631aa1..d3711022bf86 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -19,7 +19,8 @@
 
 #define STEELSERIES_SRWS1		BIT(0)
 #define STEELSERIES_ARCTIS_1		BIT(1)
-#define STEELSERIES_ARCTIS_9		BIT(2)
+#define STEELSERIES_ARCTIS_1_X		BIT(2)
+#define STEELSERIES_ARCTIS_9		BIT(3)
 
 struct steelseries_device {
 	struct hid_device *hdev;
@@ -97,7 +98,7 @@ static const __u8 steelseries_srws1_rdesc_fixed[] = {
 0x29, 0x11,         /*          Usage Maximum (11h),        */
 0x95, 0x11,         /*          Report Count (17),          */
 0x81, 0x02,         /*          Input (Variable),           */
-                    /*   ---- Dial patch starts here ----   */
+		    /*   ---- Dial patch starts here ----   */
 0x05, 0x01,         /*          Usage Page (Desktop),       */
 0x09, 0x33,         /*          Usage (RX),                 */
 0x75, 0x04,         /*          Report Size (4),            */
@@ -110,7 +111,7 @@ static const __u8 steelseries_srws1_rdesc_fixed[] = {
 0x95, 0x01,         /*          Report Count (1),           */
 0x25, 0x03,         /*          Logical Maximum (3),        */
 0x81, 0x02,         /*          Input (Variable),           */
-                    /*    ---- Dial patch ends here ----    */
+		    /*    ---- Dial patch ends here ----    */
 0x06, 0x00, 0xFF,   /*          Usage Page (FF00h),         */
 0x09, 0x01,         /*          Usage (01h),                */
 0x75, 0x04,         /* Changed  Report Size (4),            */
@@ -374,7 +375,8 @@ static void steelseries_headset_fetch_battery(struct hid_device *hdev)
 {
 	int ret = 0;
 
-	if (hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1)
+	if (hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1 ||
+	    hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X)
 		ret = steelseries_headset_request_battery(hdev,
 			arctis_1_battery_request, sizeof(arctis_1_battery_request));
 	else if (hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_9)
@@ -638,7 +640,8 @@ static int steelseries_headset_raw_event(struct hid_device *hdev,
 	if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1)
 		return 0;
 
-	if (hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1) {
+	if (hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1 ||
+	    hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X) {
 		hid_dbg(sd->hdev,
 			"Parsing raw event for Arctis 1 headset (%*ph)\n", size, read_buf);
 		if (size < ARCTIS_1_BATTERY_RESPONSE_LEN ||
@@ -724,10 +727,14 @@ static const struct hid_device_id steelseries_devices[] = {
 	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_SRWS1),
 	  .driver_data = STEELSERIES_SRWS1 },
 
-	{ /* SteelSeries Arctis 1 Wireless for XBox */
+	{ /* SteelSeries Arctis 1 Wireless */
 	  HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_1),
 	  .driver_data = STEELSERIES_ARCTIS_1 },
 
+	{ /* SteelSeries Arctis 1 Wireless for XBox */
+	  HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X),
+	  .driver_data = STEELSERIES_ARCTIS_1_X },
+
 	{ /* SteelSeries Arctis 9 Wireless for XBox */
 	  HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_9),
 	  .driver_data = STEELSERIES_ARCTIS_9 },
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 00/18] HID: steelseries: Add support for Arctis headset lineup
From: Sriman Achanta @ 2026-02-27 23:50 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Bastien Nocera, Simon Wood,
	Christian Mayer, Sriman Achanta

This patch series adds comprehensive support for the SteelSeries Arctis
wireless gaming headset lineup to the hid-steelseries driver.

The current driver provides only basic battery monitoring for Arctis 1
and Arctis 9. This series extends support to 25+ Arctis models with
full feature control including sidetone, auto-sleep, microphone
controls, volume limiting, and Bluetooth settings.

The driver restructure uses a capability-based device info system to
cleanly handle the varying feature sets across different Arctis
generations while maintaining support for the legacy SRW-S1 racing
wheel.

The driver also sets up future support for async device control which
is currently implemented for the Arctis Nova 7 Gen 2 and Post-January update
Gen 1 devices as implemented.

Tested on Arctis Nova 7 (0x2202) and Arctis Nova 7 (0x22a1). All other
implementation details are based on the reverse engineering done in the
HeadsetControl library (902e9bc).

Changes since v2:
* Expose audio related controls via ALSA mixers
* Implement async inputs from supported devices with known protocols
* Overall code cleanup and improvements to initalization logic
* Fixed several logical and protocol issues for Arctis 7 and 9

Sriman Achanta (18):
  HID: steelseries: Fix ARCTIS_1_X device mislabeling
  HID: hid-ids: Add SteelSeries Arctis headset device IDs
  HID: quirks: Add additional Arctis headset device IDs
  HID: steelseries: Add async support and unify device definitions
  HID: steelseries: Update Kconfig help text for expanded headset
    support
  HID: steelseries: Add ALSA sound card infrastructure
  HID: steelseries: Add ChatMix ALSA mixer controls
  HID: steelseries: Add mic mute ALSA mixer control
  HID: steelseries: Add Bluetooth state sysfs attributes
  HID: steelseries: Add settings poll infrastructure
  HID: steelseries: Add sidetone ALSA mixer control
  HID: steelseries: Add mic volume ALSA mixer control
  HID: steelseries: Add volume limiter ALSA mixer control
  HID: steelseries: Add Bluetooth call audio ducking control
  HID: steelseries: Add inactive time sysfs attribute
  HID: steelseries: Add Bluetooth auto-enable sysfs attribute
  HID: steelseries: Add mic mute LED brightness control
  HID: steelseries: Document sysfs ABI

 .../ABI/testing/sysfs-driver-hid-steelseries  |   87 +
 drivers/hid/Kconfig                           |    5 +-
 drivers/hid/hid-ids.h                         |   35 +-
 drivers/hid/hid-quirks.c                      |   27 +
 drivers/hid/hid-steelseries.c                 | 2329 ++++++++++++++---
 5 files changed, 2184 insertions(+), 299 deletions(-)
 create mode 100644 Documentation/ABI/testing/sysfs-driver-hid-steelseries

-- 
2.53.0


^ permalink raw reply

* [PATCH][next] HID: hid-lenovo-go-s: Fix spelling mistake "configuratiion" -> "configuration"
From: Colin Ian King @ 2026-02-27 23:16 UTC (permalink / raw)
  To: Derek J . Clark, Mark Pearson, Jiri Kosina, Benjamin Tissoires,
	linux-input
  Cc: kernel-janitors, linux-kernel

There is a spelling mistake in a dev_err_probe message. Fix it.

Signed-off-by: Colin Ian King <colin.i.king@gmail.com>
---
 drivers/hid/hid-lenovo-go-s.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/drivers/hid/hid-lenovo-go-s.c b/drivers/hid/hid-lenovo-go-s.c
index cacc5bd5ed2b..dbb88492fbba 100644
--- a/drivers/hid/hid-lenovo-go-s.c
+++ b/drivers/hid/hid-lenovo-go-s.c
@@ -1401,7 +1401,7 @@ static int hid_gos_cfg_probe(struct hid_device *hdev,
 	ret = devm_device_add_group(gos_cdev_rgb.led_cdev.dev, &rgb_attr_group);
 	if (ret) {
 		dev_err_probe(&hdev->dev, ret,
-			      "Failed to create RGB configuratiion attributes\n");
+			      "Failed to create RGB configuration attributes\n");
 		return ret;
 	}
 
-- 
2.51.0


^ permalink raw reply related

* Re: [PATCH RESEND v2] HID: winwing: Enable rumble effects
From: Ivan Gorinov @ 2026-02-27 23:10 UTC (permalink / raw)
  To: Jiri Kosina; +Cc: linux-input, linux-kernel
In-Reply-To: <s44p8pqp-6934-0r6q-n712-o5p5n2qs0429@xreary.bet>

On 2026-02-26 06:58, Jiri Kosina wrote:

> On Thu, 26 Feb 2026, Ivan Gorinov wrote:
> 
>> Enable rumble motor control on TGRIP-15E and TGRIP-15EX throttle grips
>> by sending haptic feedback commands (EV_FF events) to the input 
>> device.
>> 
>> Signed-off-by: Ivan Gorinov <linux-kernel@altimeter.info>
> [ ... snip ... ]
> 
>> +
>> +        buf[0] = 0x02;
>> +        buf[1] = 0x03;
>> +        buf[2] = 0xbf;
>> +        buf[3] = 0x00;
>> +        buf[4] = 0x00;
>> +        buf[5] = 0x03;
>> +        buf[6] = 0x49;
>> +        buf[7] = 0x00;
>> +        buf[8] = m;
>> +        buf[9] = 0x00;
>> +        buf[10] = 0;
>> +        buf[11] = 0;
>> +        buf[12] = 0;
>> +        buf[13] = 0;
> 
> Do these magic numbers have any real meaning, or is it just mimicking
> observed binary stream?
> It'd be nice to have at least short comment explaining it.

Mimicking USB requests captured by usbmon when the vendor's app is 
running in a Win10 VM (Qemu).
I will add some comments.

^ permalink raw reply

* Re: [PATCH v5 1/4] firmware_loader: expand firmware error codes with up-to-date error
From: Russ Weight @ 2026-02-27 22:05 UTC (permalink / raw)
  To: Marco Felsch
  Cc: Luis Chamberlain, Greg Kroah-Hartman, Rafael J. Wysocki,
	Andrew Morton, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Dmitry Torokhov, Kamel Bouhara, Marco Felsch, Henrik Rydberg,
	Danilo Krummrich, linux-kernel, devicetree, linux-input
In-Reply-To: <3ohould4vufzfqau4e7vg2ztks3gflmfosyaizggwzufbwvx2f@yqsgim7ash3x>

On Mon, Feb 23, 2026 at 11:39:02AM +0100, Marco Felsch wrote:
> Hi Russ,
> 
> On 26-02-19, Russ Weight wrote:
> > On Sun, Jan 11, 2026 at 04:05:44PM +0100, Marco Felsch wrote:
> > > Add FW_UPLOAD_ERR_DUPLICATE to allow drivers to inform the firmware_loader
> > > framework that the update is not required. This can be the case if the
> > > user provided firmware matches the current running firmware.
> > > 
> > > Sync lib/test_firmware.c accordingly.
> > > 
> > > Reviewed-by: Russ Weight <russ.weight@linux.dev>
> > > Reviewed-by: Luis Chamberlain <mcgrof@kernel.org>
> > > Signed-off-by: Marco Felsch <m.felsch@pengutronix.de>
> > > ---
> > >  drivers/base/firmware_loader/sysfs_upload.c | 1 +
> > >  include/linux/firmware.h                    | 2 ++
> > >  lib/test_firmware.c                         | 1 +
> > >  3 files changed, 4 insertions(+)
> > > 
> > > diff --git a/drivers/base/firmware_loader/sysfs_upload.c b/drivers/base/firmware_loader/sysfs_upload.c
> > > index c3797b93c5f5a2ecf2ae34707893c89eb7773154..9e93070b2c24179986b868a24b09cf051776c644 100644
> > > --- a/drivers/base/firmware_loader/sysfs_upload.c
> > > +++ b/drivers/base/firmware_loader/sysfs_upload.c
> > > @@ -28,6 +28,7 @@ static const char * const fw_upload_err_str[] = {
> > >  	[FW_UPLOAD_ERR_RW_ERROR]     = "read-write-error",
> > >  	[FW_UPLOAD_ERR_WEAROUT]	     = "flash-wearout",
> > >  	[FW_UPLOAD_ERR_FW_INVALID]   = "firmware-invalid",
> > > +	[FW_UPLOAD_ERR_DUPLICATE]    = "firmware-duplicate",
> > >  };
> > 
> > Hi Marco,
> > 
> > There is a corresponding change that should be made to
> > lib/test_firmware.c. You can look at the recent change for
> > FW_UPLOAD_ERR_FW_INVALID as an example.
> 
> Can you elaborate a bit more please? I've added the
> FW_UPLOAD_ERR_DUPLICATE to lib/test_firmware.c with this patchset and I
> don't know what you want me todo.

Hi Marco,

Please disregard. I didn't remember that test_firmware.c was updated
in the same patch as sysfs_upload.c. It looks good as is.

- Russ

> 
> Regards,
>   Marco
> 
> 
> > 
> > - Russ
> > 
> > >  
> > >  static const char *fw_upload_progress(struct device *dev,
> > > diff --git a/include/linux/firmware.h b/include/linux/firmware.h
> > > index aae1b85ffc10e20e9c3c9b6009d26b83efd8cb24..fe7797be4c08cd62cdad9617b8f70095d5e0af2f 100644
> > > --- a/include/linux/firmware.h
> > > +++ b/include/linux/firmware.h
> > > @@ -29,6 +29,7 @@ struct firmware {
> > >   * @FW_UPLOAD_ERR_RW_ERROR: read or write to HW failed, see kernel log
> > >   * @FW_UPLOAD_ERR_WEAROUT: FLASH device is approaching wear-out, wait & retry
> > >   * @FW_UPLOAD_ERR_FW_INVALID: invalid firmware file
> > > + * @FW_UPLOAD_ERR_DUPLICATE: firmware is already up to date (duplicate)
> > >   * @FW_UPLOAD_ERR_MAX: Maximum error code marker
> > >   */
> > >  enum fw_upload_err {
> > > @@ -41,6 +42,7 @@ enum fw_upload_err {
> > >  	FW_UPLOAD_ERR_RW_ERROR,
> > >  	FW_UPLOAD_ERR_WEAROUT,
> > >  	FW_UPLOAD_ERR_FW_INVALID,
> > > +	FW_UPLOAD_ERR_DUPLICATE,
> > >  	FW_UPLOAD_ERR_MAX
> > >  };
> > >  
> > > diff --git a/lib/test_firmware.c b/lib/test_firmware.c
> > > index be4f93124901e5faac41f48a66dabe6da56be0ca..952ec1cb03102911dbea9abd648ab9d9e0112a46 100644
> > > --- a/lib/test_firmware.c
> > > +++ b/lib/test_firmware.c
> > > @@ -1134,6 +1134,7 @@ static const char * const fw_upload_err_str[] = {
> > >  	[FW_UPLOAD_ERR_RW_ERROR]     = "read-write-error",
> > >  	[FW_UPLOAD_ERR_WEAROUT]	     = "flash-wearout",
> > >  	[FW_UPLOAD_ERR_FW_INVALID]   = "firmware-invalid",
> > > +	[FW_UPLOAD_ERR_DUPLICATE]    = "firmware-duplicate",
> > >  };
> > >  
> > >  static void upload_err_inject_error(struct test_firmware_upload *tst,
> > > 
> > > -- 
> > > 2.47.3
> > > 
> > 
> 
> -- 
> #gernperDu 
> #CallMeByMyFirstName
> 
> Pengutronix e.K.                           |                             |
> Steuerwalder Str. 21                       | https://www.pengutronix.de/ |
> 31137 Hildesheim, Germany                  | Phone: +49-5121-206917-0    |
> Amtsgericht Hildesheim, HRA 2686           | Fax:   +49-5121-206917-9    |

^ permalink raw reply

* [PATCH 3/3] HID: hid-lenovo-go-s: Fix positive promotion bug
From: Ethan Tidmore @ 2026-02-27 20:54 UTC (permalink / raw)
  To: Derek J . Clark, Mark Pearson, Jiri Kosina
  Cc: Benjamin Tissoires, Mario Limonciello, linux-input, linux-kernel,
	Ethan Tidmore
In-Reply-To: <20260227205444.1083103-1-ethantidmore06@gmail.com>

The function mcu_property_out() returns type int and returns negative
error codes. The variable count is assigned from it and checked with
(count < 0) but this check would always be false because count can
never be less than zero as it is size_t.

Change count to ssize_t.

Detected by Smatch:
drivers/hid/hid-lenovo-go-s.c:583 gamepad_property_show() warn:
unsigned 'count' is never less than zero.

drivers/hid/hid-lenovo-go-s.c:583 gamepad_property_show() warn:
error code type promoted to positive: 'count'

Fixes: 14651777fd675 ("HID: hid-lenovo-go-s: Add Feature Status Attributes")
Signed-off-by: Ethan Tidmore <ethantidmore06@gmail.com>
---
 drivers/hid/hid-lenovo-go-s.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/drivers/hid/hid-lenovo-go-s.c b/drivers/hid/hid-lenovo-go-s.c
index a24737170f83..4596c18037a9 100644
--- a/drivers/hid/hid-lenovo-go-s.c
+++ b/drivers/hid/hid-lenovo-go-s.c
@@ -573,7 +573,7 @@ static ssize_t gamepad_property_show(struct device *dev,
 				     struct device_attribute *attr, char *buf,
 				     enum feature_status_index index)
 {
-	size_t count = 0;
+	ssize_t count = 0;
 	u8 i;
 
 	count = mcu_property_out(drvdata.hdev, GET_GAMEPAD_CFG, index, 0, 0);
-- 
2.53.0


^ permalink raw reply related

* [PATCH 2/3] HID: hid-lenovo-go-s: Remove impossible condition
From: Ethan Tidmore @ 2026-02-27 20:54 UTC (permalink / raw)
  To: Derek J . Clark, Mark Pearson, Jiri Kosina
  Cc: Benjamin Tissoires, Mario Limonciello, linux-input, linux-kernel,
	Ethan Tidmore
In-Reply-To: <20260227205444.1083103-1-ethantidmore06@gmail.com>

The variable val is of type u8, so it can only be 0-255.

Remove this condition.

Detected by Smatch:
drivers/hid/hid-lenovo-go-s.c:508 gamepad_property_store() warn:
impossible condition '(val > 255) => (0-255 > 255)'

Signed-off-by: Ethan Tidmore <ethantidmore06@gmail.com>
---
 drivers/hid/hid-lenovo-go-s.c | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/drivers/hid/hid-lenovo-go-s.c b/drivers/hid/hid-lenovo-go-s.c
index 0ef98ba68d86..a24737170f83 100644
--- a/drivers/hid/hid-lenovo-go-s.c
+++ b/drivers/hid/hid-lenovo-go-s.c
@@ -504,9 +504,6 @@ static ssize_t gamepad_property_store(struct device *dev,
 		ret = kstrtou8(buf, 10, &val);
 		if (ret)
 			return ret;
-
-		if (val < 0 || val > 255)
-			return -EINVAL;
 		break;
 	case FEATURE_IMU_ENABLE:
 		ret = sysfs_match_string(feature_enabled_text, buf);
-- 
2.53.0


^ permalink raw reply related

* [PATCH 1/3] HID: hid-lenovo-go-s: Fix signedness bug
From: Ethan Tidmore @ 2026-02-27 20:54 UTC (permalink / raw)
  To: Derek J . Clark, Mark Pearson, Jiri Kosina
  Cc: Benjamin Tissoires, Mario Limonciello, linux-input, linux-kernel,
	Ethan Tidmore
In-Reply-To: <20260227205444.1083103-1-ethantidmore06@gmail.com>

The function get_endpoint_address() returns type u8 but in its error
path returns -ENODEV. Also, every variable that is assigned from this
function is type int.

Change return type to int from u8.

Detected by Smatch:
drivers/hid/hid-lenovo-go-s.c:391 get_endpoint_address() warn:
signedness bug returning '(-19)'

Fixes: 4325fdab5dbbf ("HID: hid-lenovo-go-s: Add Lenovo Legion Go S Series HID Driver")
Signed-off-by: Ethan Tidmore <ethantidmore06@gmail.com>
---
 drivers/hid/hid-lenovo-go-s.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/drivers/hid/hid-lenovo-go-s.c b/drivers/hid/hid-lenovo-go-s.c
index cacc5bd5ed2b..0ef98ba68d86 100644
--- a/drivers/hid/hid-lenovo-go-s.c
+++ b/drivers/hid/hid-lenovo-go-s.c
@@ -377,7 +377,7 @@ static int hid_gos_set_event_return(struct command_report *cmd_rep)
 	return 0;
 }
 
-static u8 get_endpoint_address(struct hid_device *hdev)
+static int get_endpoint_address(struct hid_device *hdev)
 {
 	struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
 	struct usb_host_endpoint *ep;
-- 
2.53.0


^ permalink raw reply related

* [PATCH 0/3] HID: hid-lenovo-go-s: Multiple bug fixes
From: Ethan Tidmore @ 2026-02-27 20:54 UTC (permalink / raw)
  To: Derek J . Clark, Mark Pearson, Jiri Kosina
  Cc: Benjamin Tissoires, Mario Limonciello, linux-input, linux-kernel,
	Ethan Tidmore

Here are three simple bug fixes found with Smatch.

Ethan Tidmore (3):
  HID: hid-lenovo-go-s: Fix signedness bug
  HID: hid-lenovo-go-s: Remove impossible condition
  HID: hid-lenovo-go-s: Fix positive promotion bug

 drivers/hid/hid-lenovo-go-s.c | 7 ++-----
 1 file changed, 2 insertions(+), 5 deletions(-)

-- 
2.53.0


^ permalink raw reply

* [PATCH 2/2] HID: multitouch: Check to ensure report responses match the request
From: Lee Jones @ 2026-02-27 16:30 UTC (permalink / raw)
  To: lee, Jiri Kosina, Benjamin Tissoires, linux-input, linux-kernel
In-Reply-To: <20260227163031.1166560-1-lee@kernel.org>

It is possible for a malicious (or clumsy) device to respond to a
specific report's feature request using a completely different report
ID.  This can cause confusion in the HID core resulting in nasty
side-effects such as OOB writes.

Add a check to ensure that the report ID in the response, matches the
one that was requested.  If it doesn't, omit reporting the raw event and
return early.

Signed-off-by: Lee Jones <lee@kernel.org>
---
 drivers/hid/hid-multitouch.c | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/drivers/hid/hid-multitouch.c b/drivers/hid/hid-multitouch.c
index b1c3ef129058..834c1a334887 100644
--- a/drivers/hid/hid-multitouch.c
+++ b/drivers/hid/hid-multitouch.c
@@ -499,12 +499,19 @@ static void mt_get_feature(struct hid_device *hdev, struct hid_report *report)
 		dev_warn(&hdev->dev, "failed to fetch feature %d\n",
 			 report->id);
 	} else {
+		/* The report ID in the request and the response should match */
+		if (report->id != buf[0]) {
+			hid_err(hdev, "Returned feature report did not match the request\n");
+			goto free;
+		}
+
 		ret = hid_report_raw_event(hdev, HID_FEATURE_REPORT, buf,
 					   size, 0);
 		if (ret)
 			dev_warn(&hdev->dev, "failed to report feature\n");
 	}
 
+free:
 	kfree(buf);
 }
 
-- 
2.53.0.473.g4a7958ca14-goog


^ permalink raw reply related

* [PATCH 1/2] HID: core: Mitigate potential OOB by removing bogus memset()
From: Lee Jones @ 2026-02-27 16:30 UTC (permalink / raw)
  To: lee, Jiri Kosina, Benjamin Tissoires, linux-input, linux-kernel

The memset() in hid_report_raw_event() has the good intention of
clearing out bogus data by zeroing the area from the end of the incoming
data string to the assumed end of the buffer.  However, as we have
recently seen, the size of the report buffer isn't always correct which
can culminate in OOB writes.

The current suggestion from one of the HID maintainers is to remove the
attempt completely.  The subsequent handling should be able to simply
use the data size provided to prevent any potential overruns.

Suggested-by Benjamin Tissoires <bentiss@kernel.org>
Signed-off-by: Lee Jones <lee@kernel.org>
---
 drivers/hid/hid-core.c | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/drivers/hid/hid-core.c b/drivers/hid/hid-core.c
index a5b3a8ca2fcb..1d51042e4b1f 100644
--- a/drivers/hid/hid-core.c
+++ b/drivers/hid/hid-core.c
@@ -2056,12 +2056,6 @@ int hid_report_raw_event(struct hid_device *hid, enum hid_report_type type, u8 *
 	else if (rsize > max_buffer_size)
 		rsize = max_buffer_size;
 
-	if (csize < rsize) {
-		dbg_hid("report %d is too short, (%d < %d)\n", report->id,
-				csize, rsize);
-		memset(cdata + csize, 0, rsize - csize);
-	}
-
 	if ((hid->claimed & HID_CLAIMED_HIDDEV) && hid->hiddev_report_event)
 		hid->hiddev_report_event(hid, report);
 	if (hid->claimed & HID_CLAIMED_HIDRAW) {
-- 
2.53.0.473.g4a7958ca14-goog


^ permalink raw reply related

* Re: [PATCH v6 1/2] rust: core abstractions for HID drivers
From: Gary Guo @ 2026-02-27 15:27 UTC (permalink / raw)
  To: Rahul Rameshbabu, linux-input, linux-kernel, rust-for-linux
  Cc: a.hindborg, alex.gaynor, aliceryhl, benjamin.tissoires,
	benno.lossin, bjorn3_gh, boqun.feng, dakr, db48x, gary, jikos,
	ojeda, peter.hutterer, tmgross
In-Reply-To: <20260222215611.79760-2-sergeantsagara@protonmail.com>

On Sun Feb 22, 2026 at 9:56 PM GMT, Rahul Rameshbabu wrote:
> These abstractions enable the development of HID drivers in Rust by binding
> with the HID core C API. They provide Rust types that map to the
> equivalents in C. In this initial draft, only hid_device and hid_device_id
> are provided direct Rust type equivalents. hid_driver is specially wrapped
> with a custom Driver type. The module_hid_driver! macro provides analogous
> functionality to its C equivalent. Only the .report_fixup callback is
> binded to Rust so far.
>
> Future work for these abstractions would include more bindings for common
> HID-related types, such as hid_field, hid_report_enum, and hid_report as
> well as more bus callbacks. Providing Rust equivalents to useful core HID
> functions will also be necessary for HID driver development in Rust.
>
> Signed-off-by: Rahul Rameshbabu <sergeantsagara@protonmail.com>
> ---
>
> Notes:
>     Changelog:
>     
>         v5->v6:
>           * Converted From<u16> for Group to TryFrom<u16> to properly handle
>             error case
>           * Renamed into method for Group to into_u16 to not conflate with the
>             From trait
>           * Refactored new upstream changes to RegistrationOps
>           * Implemented DriverLayout trait for hid::Adapter<T>
>         v4->v5:
>           * Add rust/ to drivers/hid/Makefile
>           * Implement RawDeviceIdIndex trait
>         v3->v4:
>           * Removed specifying tree in MAINTAINERS file since that is up for
>             debate
>           * Minor rebase cleanup
>           * Moved driver logic under drivers/hid/rust
>         v2->v3:
>           * Implemented AlwaysRefCounted trait using embedded struct device's
>             reference counts instead of the separate reference counter in struct
>             hid_device
>           * Used &raw mut as appropriate
>           * Binded include/linux/device.h for get_device and put_device
>           * Cleaned up various comment related formatting
>           * Minified dev_err! format string
>           * Updated Group enum to be repr(u16)
>           * Implemented From<u16> trait for Group
>           * Added TODO comment when const_trait_impl stabilizes
>           * Made group getter functions return a Group variant instead of a raw
>             number
>           * Made sure example code builds
>         v1->v2:
>           * Binded drivers/hid/hid-ids.h for use in Rust drivers
>           * Remove pre-emptive referencing of a C HID driver instance before
>             it is fully initialized in the driver registration path
>           * Moved static getters to generic Device trait implementation, so
>             they can be used by all device::DeviceContext
>           * Use core macros for supporting DeviceContext transitions
>           * Implemented the AlwaysRefCounted and AsRef traits
>           * Make use for dev_err! as appropriate
>         RFC->v1:
>           * Use Danilo's core infrastructure
>           * Account for HID device groups
>           * Remove probe and remove callbacks
>           * Implement report_fixup support
>           * Properly comment code including SAFETY comments
>
>  MAINTAINERS                     |   8 +
>  drivers/hid/Kconfig             |   2 +
>  drivers/hid/Makefile            |   2 +
>  drivers/hid/rust/Kconfig        |  12 +
>  rust/bindings/bindings_helper.h |   3 +
>  rust/kernel/hid.rs              | 530 ++++++++++++++++++++++++++++++++
>  rust/kernel/lib.rs              |   2 +
>  7 files changed, 559 insertions(+)
>  create mode 100644 drivers/hid/rust/Kconfig
>  create mode 100644 rust/kernel/hid.rs
>
> diff --git a/MAINTAINERS b/MAINTAINERS
> index b8d8a5c41597..1fee14024fa2 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -11319,6 +11319,14 @@ F:	include/uapi/linux/hid*
>  F:	samples/hid/
>  F:	tools/testing/selftests/hid/
>  
> +HID CORE LAYER [RUST]
> +M:	Rahul Rameshbabu <sergeantsagara@protonmail.com>
> +R:	Benjamin Tissoires <bentiss@kernel.org>
> +L:	linux-input@vger.kernel.org
> +S:	Maintained
> +F:	drivers/hid/rust/*.rs
> +F:	rust/kernel/hid.rs
> +
>  HID LOGITECH DRIVERS
>  R:	Filipe Laíns <lains@riseup.net>
>  L:	linux-input@vger.kernel.org
> diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
> index c1d9f7c6a5f2..750c2d49a806 100644
> --- a/drivers/hid/Kconfig
> +++ b/drivers/hid/Kconfig
> @@ -1439,6 +1439,8 @@ endmenu
>  
>  source "drivers/hid/bpf/Kconfig"
>  
> +source "drivers/hid/rust/Kconfig"
> +
>  source "drivers/hid/i2c-hid/Kconfig"
>  
>  source "drivers/hid/intel-ish-hid/Kconfig"
> diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
> index e01838239ae6..b78ab84c47b4 100644
> --- a/drivers/hid/Makefile
> +++ b/drivers/hid/Makefile
> @@ -8,6 +8,8 @@ hid-$(CONFIG_HID_HAPTIC)	+= hid-haptic.o
>  
>  obj-$(CONFIG_HID_BPF)		+= bpf/
>  
> +obj-$(CONFIG_RUST_HID_ABSTRACTIONS)		+= rust/
> +
>  obj-$(CONFIG_HID)		+= hid.o
>  obj-$(CONFIG_UHID)		+= uhid.o
>  
> diff --git a/drivers/hid/rust/Kconfig b/drivers/hid/rust/Kconfig
> new file mode 100644
> index 000000000000..d3247651829e
> --- /dev/null
> +++ b/drivers/hid/rust/Kconfig
> @@ -0,0 +1,12 @@
> +# SPDX-License-Identifier: GPL-2.0-only
> +menu "Rust HID support"
> +
> +config RUST_HID_ABSTRACTIONS
> +	bool "Rust HID abstractions support"
> +	depends on RUST
> +	depends on HID=y
> +	help
> +	  Adds support needed for HID drivers written in Rust. It provides a
> +	  wrapper around the C hid core.
> +
> +endmenu # Rust HID support
> diff --git a/rust/bindings/bindings_helper.h b/rust/bindings/bindings_helper.h
> index 083cc44aa952..200e58af27a3 100644
> --- a/rust/bindings/bindings_helper.h
> +++ b/rust/bindings/bindings_helper.h
> @@ -48,6 +48,7 @@
>  #include <linux/cpumask.h>
>  #include <linux/cred.h>
>  #include <linux/debugfs.h>
> +#include <linux/device.h>
>  #include <linux/device/faux.h>
>  #include <linux/dma-direction.h>
>  #include <linux/dma-mapping.h>
> @@ -60,6 +61,8 @@
>  #include <linux/i2c.h>
>  #include <linux/interrupt.h>
>  #include <linux/io-pgtable.h>
> +#include <linux/hid.h>
> +#include "../../drivers/hid/hid-ids.h"
>  #include <linux/ioport.h>
>  #include <linux/jiffies.h>
>  #include <linux/jump_label.h>
> diff --git a/rust/kernel/hid.rs b/rust/kernel/hid.rs
> new file mode 100644
> index 000000000000..b9db542d923a
> --- /dev/null
> +++ b/rust/kernel/hid.rs
> @@ -0,0 +1,530 @@
> +// SPDX-License-Identifier: GPL-2.0
> +
> +// Copyright (C) 2025 Rahul Rameshbabu <sergeantsagara@protonmail.com>
> +
> +//! Abstractions for the HID interface.
> +//!
> +//! C header: [`include/linux/hid.h`](srctree/include/linux/hid.h)
> +
> +use crate::{
> +    device,
> +    device_id::{
> +        RawDeviceId,
> +        RawDeviceIdIndex, //
> +    },
> +    driver,
> +    error::*,
> +    prelude::*,
> +    types::Opaque, //
> +};
> +use core::{
> +    marker::PhantomData,
> +    ptr::{
> +        addr_of_mut,
> +        NonNull, //
> +    } //
> +};
> +
> +/// Indicates the item is static read-only.
> +///
> +/// Refer to [Device Class Definition for HID 1.11]
> +/// Section 6.2.2.5 Input, Output, and Feature Items.
> +///
> +/// [Device Class Definition for HID 1.11]: https://www.usb.org/sites/default/files/hid1_11.pdf
> +pub const MAIN_ITEM_CONSTANT: u8 = bindings::HID_MAIN_ITEM_CONSTANT as u8;

These can be bitflags with `impl_flags!`?

> +
> +/// Indicates the item represents data from a physical control.
> +///
> +/// Refer to [Device Class Definition for HID 1.11]
> +/// Section 6.2.2.5 Input, Output, and Feature Items.
> +///
> +/// [Device Class Definition for HID 1.11]: https://www.usb.org/sites/default/files/hid1_11.pdf
> +pub const MAIN_ITEM_VARIABLE: u8 = bindings::HID_MAIN_ITEM_VARIABLE as u8;
> +
> +/// Indicates the item should be treated as a relative change from the previous
> +/// report.
> +///
> +/// Refer to [Device Class Definition for HID 1.11]
> +/// Section 6.2.2.5 Input, Output, and Feature Items.
> +///
> +/// [Device Class Definition for HID 1.11]: https://www.usb.org/sites/default/files/hid1_11.pdf
> +pub const MAIN_ITEM_RELATIVE: u8 = bindings::HID_MAIN_ITEM_RELATIVE as u8;
> +
> +/// Indicates the item should wrap around when reaching the extreme high or
> +/// extreme low values.
> +///
> +/// Refer to [Device Class Definition for HID 1.11]
> +/// Section 6.2.2.5 Input, Output, and Feature Items.
> +///
> +/// [Device Class Definition for HID 1.11]: https://www.usb.org/sites/default/files/hid1_11.pdf
> +pub const MAIN_ITEM_WRAP: u8 = bindings::HID_MAIN_ITEM_WRAP as u8;
> +
> +/// Indicates the item should wrap around when reaching the extreme high or
> +/// extreme low values.
> +///
> +/// Refer to [Device Class Definition for HID 1.11]
> +/// Section 6.2.2.5 Input, Output, and Feature Items.
> +///
> +/// [Device Class Definition for HID 1.11]: https://www.usb.org/sites/default/files/hid1_11.pdf
> +pub const MAIN_ITEM_NONLINEAR: u8 = bindings::HID_MAIN_ITEM_NONLINEAR as u8;
> +
> +/// Indicates whether the control has a preferred state it will physically
> +/// return to without user intervention.
> +///
> +/// Refer to [Device Class Definition for HID 1.11]
> +/// Section 6.2.2.5 Input, Output, and Feature Items.
> +///
> +/// [Device Class Definition for HID 1.11]: https://www.usb.org/sites/default/files/hid1_11.pdf
> +pub const MAIN_ITEM_NO_PREFERRED: u8 = bindings::HID_MAIN_ITEM_NO_PREFERRED as u8;
> +
> +/// Indicates whether the control has a physical state where it will not send
> +/// any reports.
> +///
> +/// Refer to [Device Class Definition for HID 1.11]
> +/// Section 6.2.2.5 Input, Output, and Feature Items.
> +///
> +/// [Device Class Definition for HID 1.11]: https://www.usb.org/sites/default/files/hid1_11.pdf
> +pub const MAIN_ITEM_NULL_STATE: u8 = bindings::HID_MAIN_ITEM_NULL_STATE as u8;
> +
> +/// Indicates whether the control requires host system logic to change state.
> +///
> +/// Refer to [Device Class Definition for HID 1.11]
> +/// Section 6.2.2.5 Input, Output, and Feature Items.
> +///
> +/// [Device Class Definition for HID 1.11]: https://www.usb.org/sites/default/files/hid1_11.pdf
> +pub const MAIN_ITEM_VOLATILE: u8 = bindings::HID_MAIN_ITEM_VOLATILE as u8;
> +
> +/// Indicates whether the item is fixed size or a variable buffer of bytes.
> +///
> +/// Refer to [Device Class Definition for HID 1.11]
> +/// Section 6.2.2.5 Input, Output, and Feature Items.
> +///
> +/// [Device Class Definition for HID 1.11]: https://www.usb.org/sites/default/files/hid1_11.pdf
> +pub const MAIN_ITEM_BUFFERED_BYTE: u8 = bindings::HID_MAIN_ITEM_BUFFERED_BYTE as u8;
> +
> +/// HID device groups are intended to help categories HID devices based on a set
> +/// of common quirks and logic that they will require to function correctly.
> +#[repr(u16)]
> +pub enum Group {
> +    /// Used to match a device against any group when probing.
> +    Any = bindings::HID_GROUP_ANY as u16,
> +
> +    /// Indicates a generic device that should need no custom logic from the
> +    /// core HID stack.
> +    Generic = bindings::HID_GROUP_GENERIC as u16,
> +
> +    /// Maps multitouch devices to hid-multitouch instead of hid-generic.
> +    Multitouch = bindings::HID_GROUP_MULTITOUCH as u16,
> +
> +    /// Used for autodetecing and mapping of HID sensor hubs to
> +    /// hid-sensor-hub.
> +    SensorHub = bindings::HID_GROUP_SENSOR_HUB as u16,
> +
> +    /// Used for autodetecing and mapping Win 8 multitouch devices to set the
> +    /// needed quirks.
> +    MultitouchWin8 = bindings::HID_GROUP_MULTITOUCH_WIN_8 as u16,
> +
> +    // Vendor-specific device groups.
> +    /// Used to distinguish Synpatics touchscreens from other products. The
> +    /// touchscreens will be handled by hid-multitouch instead, while everything
> +    /// else will be managed by hid-rmi.
> +    RMI = bindings::HID_GROUP_RMI as u16,
> +
> +    /// Used for hid-core handling to automatically identify Wacom devices and
> +    /// have them probed by hid-wacom.
> +    Wacom = bindings::HID_GROUP_WACOM as u16,
> +
> +    /// Used by logitech-djreceiver and logitech-djdevice to autodetect if
> +    /// devices paied to the DJ receivers are DJ devices and handle them with
> +    /// the device driver.
> +    LogitechDJDevice = bindings::HID_GROUP_LOGITECH_DJ_DEVICE as u16,
> +
> +    /// Since the Valve Steam Controller only has vendor-specific usages,
> +    /// prevent hid-generic from parsing its reports since there would be
> +    /// nothing hid-generic could do for the device.
> +    Steam = bindings::HID_GROUP_STEAM as u16,
> +
> +    /// Used to differentiate 27 Mhz frequency Logitech DJ devices from other
> +    /// Logitech DJ devices.
> +    Logitech27MHzDevice = bindings::HID_GROUP_LOGITECH_27MHZ_DEVICE as u16,
> +
> +    /// Used for autodetecting and mapping Vivaldi devices to hid-vivaldi.
> +    Vivaldi = bindings::HID_GROUP_VIVALDI as u16,
> +}
> +
> +// TODO: use `const_trait_impl` once stabilized:
> +//
> +// ```
> +// impl const From<Group> for u16 {
> +//     /// [`Group`] variants are represented by [`u16`] values.
> +//     fn from(value: Group) -> Self {
> +//         value as Self
> +//     }
> +// }
> +// ```
> +impl Group {
> +    /// Internal function used to convert [`Group`] variants into [`u16`].
> +    const fn into_u16(self) -> u16 {

This and many functions in the abstraction can be `#[inline]`.

> +        self as u16
> +    }
> +}
> +
> +impl TryFrom<u16> for Group {
> +    type Error = &'static str;
> +
> +    /// [`u16`] values can be safely converted to [`Group`] variants.
> +    fn try_from(value: u16) -> Result<Group, Self::Error> {
> +        match value.into() {
> +            bindings::HID_GROUP_GENERIC => Ok(Group::Generic),
> +            bindings::HID_GROUP_MULTITOUCH => Ok(Group::Multitouch),
> +            bindings::HID_GROUP_SENSOR_HUB => Ok(Group::SensorHub),
> +            bindings::HID_GROUP_MULTITOUCH_WIN_8 => Ok(Group::MultitouchWin8),
> +            bindings::HID_GROUP_RMI => Ok(Group::RMI),
> +            bindings::HID_GROUP_WACOM => Ok(Group::Wacom),
> +            bindings::HID_GROUP_LOGITECH_DJ_DEVICE => Ok(Group::LogitechDJDevice),
> +            bindings::HID_GROUP_STEAM => Ok(Group::Steam),
> +            bindings::HID_GROUP_LOGITECH_27MHZ_DEVICE => Ok(Group::Logitech27MHzDevice),
> +            bindings::HID_GROUP_VIVALDI => Ok(Group::Vivaldi),
> +            _ => Err("Unknown HID group encountered!"),

Please define a custom error type or just use a kernel error code.

> +        }
> +    }
> +}
> +
> +/// The HID device representation.
> +///
> +/// This structure represents the Rust abstraction for a C `struct hid_device`.
> +/// The implementation abstracts the usage of an already existing C `struct
> +/// hid_device` within Rust code that we get passed from the C side.
> +///
> +/// # Invariants
> +///
> +/// A [`Device`] instance represents a valid `struct hid_device` created by the
> +/// C portion of the kernel.
> +#[repr(transparent)]
> +pub struct Device<Ctx: device::DeviceContext = device::Normal>(
> +    Opaque<bindings::hid_device>,
> +    PhantomData<Ctx>,
> +);
> +
> +impl<Ctx: device::DeviceContext> Device<Ctx> {
> +    fn as_raw(&self) -> *mut bindings::hid_device {
> +        self.0.get()
> +    }
> +
> +    /// Returns the HID transport bus ID.
> +    pub fn bus(&self) -> u16 {
> +        // SAFETY: `self.as_raw` is a valid pointer to a `struct hid_device`
> +        unsafe { *self.as_raw() }.bus
> +    }
> +
> +    /// Returns the HID report group.
> +    pub fn group(&self) -> Result<Group, &'static str> {
> +        // SAFETY: `self.as_raw` is a valid pointer to a `struct hid_device`
> +        unsafe { *self.as_raw() }.group.try_into()
> +    }
> +
> +    /// Returns the HID vendor ID.
> +    pub fn vendor(&self) -> u32 {
> +        // SAFETY: `self.as_raw` is a valid pointer to a `struct hid_device`
> +        unsafe { *self.as_raw() }.vendor
> +    }
> +
> +    /// Returns the HID product ID.
> +    pub fn product(&self) -> u32 {
> +        // SAFETY: `self.as_raw` is a valid pointer to a `struct hid_device`
> +        unsafe { *self.as_raw() }.product
> +    }
> +}
> +
> +// SAFETY: `Device` is a transparent wrapper of a type that doesn't depend on `Device`'s generic
> +// argument.
> +kernel::impl_device_context_deref!(unsafe { Device });
> +kernel::impl_device_context_into_aref!(Device);
> +
> +// SAFETY: Instances of `Device` are always reference-counted.
> +unsafe impl crate::types::AlwaysRefCounted for Device {
> +    fn inc_ref(&self) {
> +        // SAFETY: The existence of a shared reference guarantees that the refcount is non-zero.
> +        unsafe { bindings::get_device(&raw mut (*self.as_raw()).dev) };
> +    }
> +
> +    unsafe fn dec_ref(obj: NonNull<Self>) {
> +        // SAFETY: The safety requirements guarantee that the refcount is non-zero.
> +        unsafe { bindings::put_device(&raw mut (*obj.cast::<bindings::hid_device>().as_ptr()).dev) }
> +    }
> +}
> +
> +impl<Ctx: device::DeviceContext> AsRef<device::Device<Ctx>> for Device<Ctx> {
> +    fn as_ref(&self) -> &device::Device<Ctx> {
> +        // SAFETY: By the type invariant of `Self`, `self.as_raw()` is a pointer to a valid
> +        // `struct hid_device`.
> +        let dev = unsafe { addr_of_mut!((*self.as_raw()).dev) };
> +
> +        // SAFETY: `dev` points to a valid `struct device`.
> +        unsafe { device::Device::from_raw(dev) }
> +    }
> +}
> +
> +/// Abstraction for the HID device ID structure `struct hid_device_id`.
> +#[repr(transparent)]
> +#[derive(Clone, Copy)]
> +pub struct DeviceId(bindings::hid_device_id);
> +
> +impl DeviceId {
> +    /// Equivalent to C's `HID_USB_DEVICE` macro.
> +    ///
> +    /// Create a new `hid::DeviceId` from a group, vendor ID, and device ID
> +    /// number.

The main description should be the first sentence, and C equivalent sentence
follows.

> +    pub const fn new_usb(group: Group, vendor: u32, product: u32) -> Self {
> +        Self(bindings::hid_device_id {
> +            bus: 0x3, // BUS_USB
> +            group: group.into_u16(),
> +            vendor,
> +            product,
> +            driver_data: 0,
> +        })
> +    }
> +
> +    /// Returns the HID transport bus ID.
> +    pub fn bus(&self) -> u16 {
> +        self.0.bus
> +    }
> +
> +    /// Returns the HID report group.
> +    pub fn group(&self) -> Result<Group, &'static str> {
> +        self.0.group.try_into()
> +    }
> +
> +    /// Returns the HID vendor ID.
> +    pub fn vendor(&self) -> u32 {
> +        self.0.vendor
> +    }
> +
> +    /// Returns the HID product ID.
> +    pub fn product(&self) -> u32 {
> +        self.0.product
> +    }
> +}
> +
> +// SAFETY:
> +// * `DeviceId` is a `#[repr(transparent)` wrapper of `hid_device_id` and does not add
> +//   additional invariants, so it's safe to transmute to `RawType`.
> +// * `DRIVER_DATA_OFFSET` is the offset to the `driver_data` field.
> +unsafe impl RawDeviceId for DeviceId {
> +    type RawType = bindings::hid_device_id;
> +}
> +
> +// SAFETY: `DRIVER_DATA_OFFSET` is the offset to the `driver_data` field.
> +unsafe impl RawDeviceIdIndex for DeviceId {
> +    const DRIVER_DATA_OFFSET: usize = core::mem::offset_of!(bindings::hid_device_id, driver_data);
> +
> +    fn index(&self) -> usize {
> +        self.0.driver_data
> +    }
> +}
> +
> +/// [`IdTable`] type for HID.
> +pub type IdTable<T> = &'static dyn kernel::device_id::IdTable<DeviceId, T>;
> +
> +/// Create a HID [`IdTable`] with its alias for modpost.
> +#[macro_export]
> +macro_rules! hid_device_table {
> +    ($table_name:ident, $module_table_name:ident, $id_info_type: ty, $table_data: expr) => {
> +        const $table_name: $crate::device_id::IdArray<
> +            $crate::hid::DeviceId,
> +            $id_info_type,
> +            { $table_data.len() },
> +        > = $crate::device_id::IdArray::new($table_data);
> +
> +        $crate::module_device_table!("hid", $module_table_name, $table_name);
> +    };
> +}
> +
> +/// The HID driver trait.
> +///
> +/// # Examples
> +///
> +/// ```
> +/// use kernel::{bindings, device, hid};
> +///
> +/// struct MyDriver;
> +///
> +/// kernel::hid_device_table!(
> +///     HID_TABLE,
> +///     MODULE_HID_TABLE,
> +///     <MyDriver as hid::Driver>::IdInfo,
> +///     [(
> +///         hid::DeviceId::new_usb(
> +///             hid::Group::Steam,
> +///             bindings::USB_VENDOR_ID_VALVE,
> +///             bindings::USB_DEVICE_ID_STEAM_DECK,
> +///         ),
> +///         (),
> +///     )]
> +/// );
> +///
> +/// #[vtable]
> +/// impl hid::Driver for MyDriver {
> +///     type IdInfo = ();
> +///     const ID_TABLE: hid::IdTable<Self::IdInfo> = &HID_TABLE;
> +///
> +///     /// This function is optional to implement.
> +///     fn report_fixup<'a, 'b: 'a>(_hdev: &hid::Device<device::Core>, rdesc: &'b mut [u8]) -> &'a [u8] {
> +///         // Perform some report descriptor fixup.
> +///         rdesc
> +///     }
> +/// }
> +/// ```
> +/// Drivers must implement this trait in order to get a HID driver registered.
> +/// Please refer to the `Adapter` documentation for an example.
> +#[vtable]
> +pub trait Driver: Send {
> +    /// The type holding information about each device id supported by the driver.
> +    // TODO: Use `associated_type_defaults` once stabilized:
> +    //
> +    // ```
> +    // type IdInfo: 'static = ();
> +    // ```
> +    type IdInfo: 'static;
> +
> +    /// The table of device ids supported by the driver.
> +    const ID_TABLE: IdTable<Self::IdInfo>;
> +
> +    /// Called before report descriptor parsing. Can be used to mutate the
> +    /// report descriptor before the core HID logic processes the descriptor.
> +    /// Useful for problematic report descriptors that prevent HID devices from
> +    /// functioning correctly.
> +    ///
> +    /// Optional to implement.
> +    fn report_fixup<'a, 'b: 'a>(_hdev: &Device<device::Core>, _rdesc: &'b mut [u8]) -> &'a [u8] {

I think this can just use a single lifetime?

> +        build_error!(VTABLE_DEFAULT_ERROR)
> +    }
> +}
> +
> +/// An adapter for the registration of HID drivers.
> +pub struct Adapter<T: Driver>(T);
> +
> +// SAFETY:
> +// - `bindings::hid_driver` is a C type declared as `repr(C)`.
> +// - `T` is the type of the driver's device private data.
> +// - `struct hid_driver` embeds a `struct device_driver`.
> +// - `DEVICE_DRIVER_OFFSET` is the correct byte offset to the embedded `struct device_driver`.
> +unsafe impl<T: Driver + 'static> driver::DriverLayout for Adapter<T> {
> +    type DriverType = bindings::hid_driver;
> +    type DriverData = T;
> +    const DEVICE_DRIVER_OFFSET: usize = core::mem::offset_of!(Self::DriverType, driver);
> +}
> +
> +// SAFETY: A call to `unregister` for a given instance of `DriverType` is guaranteed to be valid if
> +// a preceding call to `register` has been successful.
> +unsafe impl<T: Driver + 'static> driver::RegistrationOps for Adapter<T> {
> +    unsafe fn register(
> +        hdrv: &Opaque<Self::DriverType>,
> +        name: &'static CStr,
> +        module: &'static ThisModule,
> +    ) -> Result {
> +        // SAFETY: It's safe to set the fields of `struct hid_driver` on initialization.
> +        unsafe {
> +            (*hdrv.get()).name = name.as_char_ptr();
> +            (*hdrv.get()).id_table = T::ID_TABLE.as_ptr();
> +            (*hdrv.get()).report_fixup = if T::HAS_REPORT_FIXUP {
> +                Some(Self::report_fixup_callback)
> +            } else {
> +                None
> +            };
> +        }
> +
> +        // SAFETY: `hdrv` is guaranteed to be a valid `DriverType`
> +        to_result(unsafe {
> +            bindings::__hid_register_driver(hdrv.get(), module.0, name.as_char_ptr())
> +        })
> +    }
> +
> +    unsafe fn unregister(hdrv: &Opaque<Self::DriverType>) {
> +        // SAFETY: `hdrv` is guaranteed to be a valid `DriverType`
> +        unsafe { bindings::hid_unregister_driver(hdrv.get()) }
> +    }
> +}
> +
> +impl<T: Driver + 'static> Adapter<T> {
> +    extern "C" fn report_fixup_callback(
> +        hdev: *mut bindings::hid_device,
> +        buf: *mut u8,
> +        size: *mut kernel::ffi::c_uint,
> +    ) -> *const u8 {
> +        // SAFETY: The HID subsystem only ever calls the report_fixup callback
> +        // with a valid pointer to a `struct hid_device`.
> +        //
> +        // INVARIANT: `hdev` is valid for the duration of
> +        // `report_fixup_callback()`.
> +        let hdev = unsafe { &*hdev.cast::<Device<device::Core>>() };
> +
> +        // SAFETY: The HID subsystem only ever calls the report_fixup callback
> +        // with a valid pointer to a `kernel::ffi::c_uint`.
> +        //
> +        // INVARIANT: `size` is valid for the duration of
> +        // `report_fixup_callback()`.
> +        let buf_len: usize = match unsafe { *size }.try_into() {

This can just be a cast. In all kernel envs `usize` is at least as large as `u32`.

> +            Ok(len) => len,
> +            Err(e) => {
> +                dev_err!(
> +                    hdev.as_ref(),
> +                    "Cannot fix report description due to {:?}!\n",
> +                    e
> +                );
> +
> +                return buf;
> +            }
> +        };
> +
> +        // Build a mutable Rust slice from `buf` and `size`.
> +        //
> +        // SAFETY: The HID subsystem only ever calls the `report_fixup callback`
> +        // with a valid pointer to a `u8` buffer.
> +        //
> +        // INVARIANT: `buf` is valid for the duration of
> +        // `report_fixup_callback()`.
> +        let rdesc_slice = unsafe { core::slice::from_raw_parts_mut(buf, buf_len) };
> +        let rdesc_slice = T::report_fixup(hdev, rdesc_slice);
> +
> +        match rdesc_slice.len().try_into() {

I'm somewhat inclined to just `BUG()` (i.e. `.expect()`) in this case. A driver
that hits this error condition would need to leak >= 4G of memory to satisfy the
lifetime bound.

> +            // SAFETY: The HID subsystem only ever calls the report_fixup
> +            // callback with a valid pointer to a `kernel::ffi::c_uint`.
> +            //
> +            // INVARIANT: `size` is valid for the duration of
> +            // `report_fixup_callback()`.
> +            Ok(len) => unsafe { *size = len },
> +            Err(e) => {
> +                dev_err!(
> +                    hdev.as_ref(),

as_ref() shouldn't be needed now.

Best,
Gary

> +                    "Fixed report description will not be used due to {:?}!\n",
> +                    e
> +                );
> +
> +                return buf;
> +            }
> +        }
> +
> +        rdesc_slice.as_ptr()
> +    }
> +}
> +
> +/// Declares a kernel module that exposes a single HID driver.
> +///
> +/// # Examples
> +///
> +/// ```ignore
> +/// kernel::module_hid_driver! {
> +///     type: MyDriver,
> +///     name: "Module name",
> +///     authors: ["Author name"],
> +///     description: "Description",
> +///     license: "GPL",
> +/// }
> +/// ```
> +#[macro_export]
> +macro_rules! module_hid_driver {
> +    ($($f:tt)*) => {
> +        $crate::module_driver!(<T>, $crate::hid::Adapter<T>, { $($f)* });
> +    };
> +}
> diff --git a/rust/kernel/lib.rs b/rust/kernel/lib.rs
> index 3da92f18f4ee..e2dcacd9369e 100644
> --- a/rust/kernel/lib.rs
> +++ b/rust/kernel/lib.rs
> @@ -102,6 +102,8 @@
>  pub mod id_pool;
>  #[doc(hidden)]
>  pub mod impl_flags;
> +#[cfg(CONFIG_RUST_HID_ABSTRACTIONS)]
> +pub mod hid;
>  pub mod init;
>  pub mod io;
>  pub mod ioctl;


^ permalink raw reply

* Re: [PATCH 2/2] hid: asus: always fully initialize devices
From: Jiri Kosina @ 2026-02-27 13:11 UTC (permalink / raw)
  To: Denis Benato
  Cc: linux-kernel, linux-input, Benjamin Tissoires, Luke D . Jones,
	Mateusz Schyboll, Denis Benato, Ilpo Järvinen
In-Reply-To: <20260216175539.12415-2-denis.benato@linux.dev>

On Mon, 16 Feb 2026, Denis Benato wrote:

> ASUS has more devices other than laptop keyboards which needs
> to be initializated: the init sequence is the same as keyboards.
> 
> ID1 and ID2 are USB report IDs that are correctly enumerated:
> send init commands to all devices that supports those reports IDs.
> 
> Signed-off-by: Denis Benato <denis.benato@linux.dev>

Applied to hid.git#for-7.1/asus.

-- 
Jiri Kosina
SUSE Labs


^ permalink raw reply

* Re: [PATCH 1/2] hid: asus: add xg mobile 2023 external hardware support
From: Jiri Kosina @ 2026-02-27 13:11 UTC (permalink / raw)
  To: Denis Benato
  Cc: linux-kernel, linux-input, Benjamin Tissoires, Luke D . Jones,
	Mateusz Schyboll, Denis Benato, Ilpo Järvinen
In-Reply-To: <20260216175539.12415-1-denis.benato@linux.dev>

On Mon, 16 Feb 2026, Denis Benato wrote:

> XG mobile stations have the 0x5a endpoint and has to be initialized:
> add them to hid-asus.

Applied to hid.git#for-7.0/upstream-fixes.

-- 
Jiri Kosina
SUSE Labs


^ permalink raw reply


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