Linux Input/HID development
 help / color / mirror / Atom feed
* [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 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 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 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 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 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 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 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 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 13/18] HID: steelseries: Add volume limiter 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 maximum output volume cap as a writable ALSA boolean mixer
control ("Volume Limiter").

The Nova 7 family uses command 0x3a for this setting whereas the Nova 5
family uses 0x27, so a dedicated steelseries_arctis_nova_7_write_setting()
is introduced and the Nova 7, Nova 7P, and Nova 7 Gen2 entries are
updated to use it instead of the Nova 5 handler.

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

diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index 1339f965f67f..47ffec481571 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -29,11 +29,13 @@
 #define SS_CAP_EXTERNAL_CONFIG		BIT(5)
 #define SS_CAP_SIDETONE			BIT(6)
 #define SS_CAP_MIC_VOLUME		BIT(7)
+#define SS_CAP_VOLUME_LIMITER		BIT(8)
 
 #define SS_QUIRK_STATUS_SYNC_POLL	BIT(0)
 
 #define SS_SETTING_SIDETONE		0
 #define SS_SETTING_MIC_VOLUME		1
+#define SS_SETTING_VOLUME_LIMITER	2
 
 struct steelseries_device;
 
@@ -77,11 +79,13 @@ struct steelseries_device {
 	struct snd_ctl_elem_id mic_muted_id;
 	struct snd_ctl_elem_id sidetone_id;
 	struct snd_ctl_elem_id mic_volume_id;
+	struct snd_ctl_elem_id volume_limiter_id;
 	u8 chatmix_chat;
 	u8 chatmix_game;
 	bool mic_muted;
 	u8 sidetone;
 	u8 mic_volume;
+	bool volume_limiter;
 
 	bool bt_enabled;
 	bool bt_device_connected;
@@ -531,6 +535,44 @@ static int steelseries_arctis_nova_5_write_setting(struct hid_device *hdev,
 	case SS_SETTING_MIC_VOLUME:
 		cmd = 0x37;
 		break;
+	case SS_SETTING_VOLUME_LIMITER:
+		cmd = 0x27;
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	data[0] = 0x00;
+	data[1] = cmd;
+	data[2] = value;
+
+	ret = steelseries_send_output_report(hdev, data, sizeof(data));
+	if (ret)
+		return ret;
+
+	msleep(10);
+
+	return steelseries_send_output_report(hdev, save, sizeof(save));
+}
+
+static int steelseries_arctis_nova_7_write_setting(struct hid_device *hdev,
+						   u8 setting, u8 value)
+{
+	const u8 save[] = { 0x00, 0x09 };
+	u8 cmd;
+	int ret;
+	u8 data[3];
+
+	switch (setting) {
+	case SS_SETTING_SIDETONE:
+		cmd = 0x39;
+		break;
+	case SS_SETTING_MIC_VOLUME:
+		cmd = 0x37;
+		break;
+	case SS_SETTING_VOLUME_LIMITER:
+		cmd = 0x3a;
+		break;
 	default:
 		return -EINVAL;
 	}
@@ -849,13 +891,14 @@ static int steelseries_arctis_nova_7_gen2_request_settings(struct hid_device *hd
 static void steelseries_arctis_nova_7_gen2_parse_settings(
 	struct steelseries_device *sd, u8 *data, int size)
 {
-	if (size < 3)
+	if (size < 4)
 		return;
 
 	switch (data[0]) {
 	case 0x20:
 		sd->mic_volume = data[1];
 		sd->sidetone = data[2];
+		sd->volume_limiter = data[3];
 		break;
 	case 0x37:
 		sd->mic_volume = data[1];
@@ -863,6 +906,9 @@ static void steelseries_arctis_nova_7_gen2_parse_settings(
 	case 0x39:
 		sd->sidetone = data[1];
 		break;
+	case 0x3a:
+		sd->volume_limiter = data[1];
+		break;
 	}
 }
 
@@ -954,7 +1000,8 @@ static const struct steelseries_device_info arctis_nova_3p_info = {
 
 static const struct steelseries_device_info arctis_nova_5_info = {
 	.sync_interface = 3,
-	.capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME,
+	.capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME |
+			SS_CAP_VOLUME_LIMITER,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
 	.sidetone_max = 10,
 	.mic_volume_max = 15,
@@ -966,7 +1013,7 @@ static const struct steelseries_device_info arctis_nova_5_info = {
 static const struct steelseries_device_info arctis_nova_5x_info = {
 	.sync_interface = 3,
 	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
-			SS_CAP_MIC_VOLUME,
+			SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
 	.sidetone_max = 10,
 	.mic_volume_max = 15,
@@ -978,23 +1025,23 @@ static const struct steelseries_device_info arctis_nova_5x_info = {
 static const struct steelseries_device_info arctis_nova_7_info = {
 	.sync_interface = 3,
 	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
-			SS_CAP_MIC_VOLUME,
+			SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
 	.sidetone_max = 3,
 	.mic_volume_max = 7,
 	.request_status = steelseries_arctis_nova_request_status,
 	.parse_status = steelseries_arctis_nova_7_parse_status,
-	.write_setting = steelseries_arctis_nova_5_write_setting,
+	.write_setting = steelseries_arctis_nova_7_write_setting,
 };
 
 static const struct steelseries_device_info arctis_nova_7p_info = {
 	.sync_interface = 3,
-	.capabilities = SS_CAP_BATTERY | SS_CAP_MIC_VOLUME,
+	.capabilities = SS_CAP_BATTERY | SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
 	.mic_volume_max = 7,
 	.request_status = steelseries_arctis_nova_request_status,
 	.parse_status = steelseries_arctis_nova_7_parse_status,
-	.write_setting = steelseries_arctis_nova_5_write_setting,
+	.write_setting = steelseries_arctis_nova_7_write_setting,
 };
 
 static const struct steelseries_device_info arctis_nova_7_gen2_info = {
@@ -1003,14 +1050,14 @@ static const struct steelseries_device_info arctis_nova_7_gen2_info = {
 	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_MIC_MUTE |
 			SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
 			SS_CAP_EXTERNAL_CONFIG | SS_CAP_SIDETONE |
-			SS_CAP_MIC_VOLUME,
+			SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER,
 	.sidetone_max = 3,
 	.mic_volume_max = 7,
 	.request_status = steelseries_arctis_nova_request_status,
 	.parse_status = steelseries_arctis_nova_7_gen2_parse_status,
 	.request_settings = steelseries_arctis_nova_7_gen2_request_settings,
 	.parse_settings = steelseries_arctis_nova_7_gen2_parse_settings,
-	.write_setting = steelseries_arctis_nova_5_write_setting,
+	.write_setting = steelseries_arctis_nova_7_write_setting,
 };
 
 static const struct steelseries_device_info arctis_nova_pro_info = {
@@ -1450,6 +1497,66 @@ static const struct snd_kcontrol_new steelseries_mic_volume_control = {
 	.put = steelseries_mic_volume_put,
 };
 
+static int steelseries_volume_limiter_info(struct snd_kcontrol *kcontrol,
+					   struct snd_ctl_elem_info *uinfo)
+{
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
+	uinfo->count = 1;
+	uinfo->value.integer.min = 0;
+	uinfo->value.integer.max = 1;
+	uinfo->value.integer.step = 1;
+	return 0;
+}
+
+static int steelseries_volume_limiter_get(struct snd_kcontrol *kcontrol,
+					  struct snd_ctl_elem_value *ucontrol)
+{
+	struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
+	unsigned long flags;
+
+	spin_lock_irqsave(&sd->lock, flags);
+	ucontrol->value.integer.value[0] = sd->volume_limiter;
+	spin_unlock_irqrestore(&sd->lock, flags);
+	return 0;
+}
+
+static int steelseries_volume_limiter_put(struct snd_kcontrol *kcontrol,
+					  struct snd_ctl_elem_value *ucontrol)
+{
+	struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
+	unsigned long flags;
+	bool new_volume_limiter;
+	int ret;
+
+	new_volume_limiter = !!ucontrol->value.integer.value[0];
+
+	spin_lock_irqsave(&sd->lock, flags);
+	if (sd->volume_limiter == new_volume_limiter) {
+		spin_unlock_irqrestore(&sd->lock, flags);
+		return 0;
+	}
+	spin_unlock_irqrestore(&sd->lock, flags);
+
+	ret = sd->info->write_setting(sd->hdev, SS_SETTING_VOLUME_LIMITER,
+				      new_volume_limiter ? 1 : 0);
+	if (ret)
+		return ret;
+
+	spin_lock_irqsave(&sd->lock, flags);
+	sd->volume_limiter = new_volume_limiter;
+	spin_unlock_irqrestore(&sd->lock, flags);
+
+	return 1;
+}
+
+static const struct snd_kcontrol_new steelseries_volume_limiter_control = {
+	.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+	.name = "Volume Limiter",
+	.info = steelseries_volume_limiter_info,
+	.get = steelseries_volume_limiter_get,
+	.put = steelseries_volume_limiter_put,
+};
+
 static int steelseries_snd_register(struct steelseries_device *sd)
 {
 	struct hid_device *hdev = sd->hdev;
@@ -1522,6 +1629,21 @@ static int steelseries_snd_register(struct steelseries_device *sd)
 		sd->mic_volume_id = kctl->id;
 	}
 
+	if (sd->info->capabilities & SS_CAP_VOLUME_LIMITER) {
+		struct snd_kcontrol *kctl;
+		struct snd_kcontrol_new vol_lim_ctl = steelseries_volume_limiter_control;
+
+		vol_lim_ctl.access = SNDRV_CTL_ELEM_ACCESS_READWRITE;
+		if (sd->info->capabilities & SS_CAP_EXTERNAL_CONFIG)
+			vol_lim_ctl.access |= SNDRV_CTL_ELEM_ACCESS_VOLATILE;
+
+		kctl = snd_ctl_new1(&vol_lim_ctl, sd);
+		ret = snd_ctl_add(sd->card, kctl);
+		if (ret < 0)
+			goto err_free_card;
+		sd->volume_limiter_id = kctl->id;
+	}
+
 	ret = snd_card_register(sd->card);
 	if (ret < 0)
 		goto err_free_card;
@@ -1554,6 +1676,7 @@ static int steelseries_raw_event(struct hid_device *hdev,
 	bool old_mic_muted;
 	u8 old_sidetone;
 	u8 old_mic_volume;
+	bool old_volume_limiter;
 	bool is_async_interface = false;
 
 	if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1)
@@ -1570,6 +1693,7 @@ static int steelseries_raw_event(struct hid_device *hdev,
 	old_mic_muted = sd->mic_muted;
 	old_sidetone = sd->sidetone;
 	old_mic_volume = sd->mic_volume;
+	old_volume_limiter = sd->volume_limiter;
 
 	if (hid_is_usb(hdev)) {
 		struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
@@ -1636,6 +1760,9 @@ static int steelseries_raw_event(struct hid_device *hdev,
 		if (sd->mic_volume != old_mic_volume)
 			snd_ctl_notify(sd->card, SNDRV_CTL_EVENT_MASK_VALUE,
 				       &sd->mic_volume_id);
+		if (sd->volume_limiter != old_volume_limiter)
+			snd_ctl_notify(sd->card, SNDRV_CTL_EVENT_MASK_VALUE,
+				       &sd->volume_limiter_id);
 	}
 
 	return 0;
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 14/18] HID: steelseries: Add Bluetooth call audio ducking 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 Bluetooth call audio ducking behavior as a writable ALSA
enumerated mixer control ("Bluetooth Call Audio Ducking"), with three
options: off, lower game audio by 12 dB, or mute game audio entirely.

On the Arctis Nova 7 Gen2, this setting is stored alongside inactive
timeout and Bluetooth auto-enable in a dedicated device configuration
block. The settings request is expanded to also send a 0x00/0xa0 device
query in addition to the existing 0x00/0x20 audio settings query.

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

diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index 47ffec481571..bb9abbb0b6f8 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -30,12 +30,14 @@
 #define SS_CAP_SIDETONE			BIT(6)
 #define SS_CAP_MIC_VOLUME		BIT(7)
 #define SS_CAP_VOLUME_LIMITER		BIT(8)
+#define SS_CAP_BT_CALL_DUCKING		BIT(9)
 
 #define SS_QUIRK_STATUS_SYNC_POLL	BIT(0)
 
 #define SS_SETTING_SIDETONE		0
 #define SS_SETTING_MIC_VOLUME		1
 #define SS_SETTING_VOLUME_LIMITER	2
+#define SS_SETTING_BT_CALL_DUCKING	3
 
 struct steelseries_device;
 
@@ -80,12 +82,14 @@ struct steelseries_device {
 	struct snd_ctl_elem_id sidetone_id;
 	struct snd_ctl_elem_id mic_volume_id;
 	struct snd_ctl_elem_id volume_limiter_id;
+	struct snd_ctl_elem_id bt_call_ducking_id;
 	u8 chatmix_chat;
 	u8 chatmix_game;
 	bool mic_muted;
 	u8 sidetone;
 	u8 mic_volume;
 	bool volume_limiter;
+	u8 bt_call_ducking;
 
 	bool bt_enabled;
 	bool bt_device_connected;
@@ -573,6 +577,9 @@ static int steelseries_arctis_nova_7_write_setting(struct hid_device *hdev,
 	case SS_SETTING_VOLUME_LIMITER:
 		cmd = 0x3a;
 		break;
+	case SS_SETTING_BT_CALL_DUCKING:
+		cmd = 0xb3;
+		break;
 	default:
 		return -EINVAL;
 	}
@@ -883,15 +890,23 @@ static void steelseries_arctis_nova_7_gen2_parse_status(struct steelseries_devic
 
 static int steelseries_arctis_nova_7_gen2_request_settings(struct hid_device *hdev)
 {
-	const u8 data[] = { 0x00, 0x20 };
+	const u8 audio_data[] = { 0x00, 0x20 };
+	const u8 device_data[] = { 0x00, 0xa0 };
+	int ret;
 
-	return steelseries_send_output_report(hdev, data, sizeof(data));
+	ret = steelseries_send_output_report(hdev, audio_data, sizeof(audio_data));
+	if (ret)
+		return ret;
+
+	msleep(10);
+
+	return steelseries_send_output_report(hdev, device_data, sizeof(device_data));
 }
 
 static void steelseries_arctis_nova_7_gen2_parse_settings(
 	struct steelseries_device *sd, u8 *data, int size)
 {
-	if (size < 4)
+	if (size < 5)
 		return;
 
 	switch (data[0]) {
@@ -900,6 +915,9 @@ static void steelseries_arctis_nova_7_gen2_parse_settings(
 		sd->sidetone = data[2];
 		sd->volume_limiter = data[3];
 		break;
+	case 0xa0:
+		sd->bt_call_ducking = data[4];
+		break;
 	case 0x37:
 		sd->mic_volume = data[1];
 		break;
@@ -909,6 +927,9 @@ static void steelseries_arctis_nova_7_gen2_parse_settings(
 	case 0x3a:
 		sd->volume_limiter = data[1];
 		break;
+	case 0xb3:
+		sd->bt_call_ducking = data[1];
+		break;
 	}
 }
 
@@ -1025,7 +1046,8 @@ static const struct steelseries_device_info arctis_nova_5x_info = {
 static const struct steelseries_device_info arctis_nova_7_info = {
 	.sync_interface = 3,
 	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
-			SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER,
+			SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
+			SS_CAP_BT_CALL_DUCKING,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
 	.sidetone_max = 3,
 	.mic_volume_max = 7,
@@ -1036,7 +1058,8 @@ static const struct steelseries_device_info arctis_nova_7_info = {
 
 static const struct steelseries_device_info arctis_nova_7p_info = {
 	.sync_interface = 3,
-	.capabilities = SS_CAP_BATTERY | SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER,
+	.capabilities = SS_CAP_BATTERY | SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
+			SS_CAP_BT_CALL_DUCKING,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
 	.mic_volume_max = 7,
 	.request_status = steelseries_arctis_nova_request_status,
@@ -1050,7 +1073,8 @@ static const struct steelseries_device_info arctis_nova_7_gen2_info = {
 	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_MIC_MUTE |
 			SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
 			SS_CAP_EXTERNAL_CONFIG | SS_CAP_SIDETONE |
-			SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER,
+			SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
+			SS_CAP_BT_CALL_DUCKING,
 	.sidetone_max = 3,
 	.mic_volume_max = 7,
 	.request_status = steelseries_arctis_nova_request_status,
@@ -1557,6 +1581,70 @@ static const struct snd_kcontrol_new steelseries_volume_limiter_control = {
 	.put = steelseries_volume_limiter_put,
 };
 
+static const char *const bt_call_ducking_texts[] = {
+	"Off",
+	"Lower Volume (-12dB)",
+	"Mute Game",
+};
+
+static int steelseries_bt_call_ducking_info(struct snd_kcontrol *kcontrol,
+					    struct snd_ctl_elem_info *uinfo)
+{
+	return snd_ctl_enum_info(uinfo, 1, ARRAY_SIZE(bt_call_ducking_texts),
+				 bt_call_ducking_texts);
+}
+
+static int steelseries_bt_call_ducking_get(struct snd_kcontrol *kcontrol,
+					   struct snd_ctl_elem_value *ucontrol)
+{
+	struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
+	unsigned long flags;
+
+	spin_lock_irqsave(&sd->lock, flags);
+	ucontrol->value.enumerated.item[0] = sd->bt_call_ducking;
+	spin_unlock_irqrestore(&sd->lock, flags);
+	return 0;
+}
+
+static int steelseries_bt_call_ducking_put(struct snd_kcontrol *kcontrol,
+					   struct snd_ctl_elem_value *ucontrol)
+{
+	struct steelseries_device *sd = snd_kcontrol_chip(kcontrol);
+	unsigned long flags;
+	u8 new_value;
+	int ret;
+
+	new_value = ucontrol->value.enumerated.item[0];
+	if (new_value >= ARRAY_SIZE(bt_call_ducking_texts))
+		return -EINVAL;
+
+	spin_lock_irqsave(&sd->lock, flags);
+	if (sd->bt_call_ducking == new_value) {
+		spin_unlock_irqrestore(&sd->lock, flags);
+		return 0;
+	}
+	spin_unlock_irqrestore(&sd->lock, flags);
+
+	ret = sd->info->write_setting(sd->hdev, SS_SETTING_BT_CALL_DUCKING,
+				      new_value);
+	if (ret)
+		return ret;
+
+	spin_lock_irqsave(&sd->lock, flags);
+	sd->bt_call_ducking = new_value;
+	spin_unlock_irqrestore(&sd->lock, flags);
+
+	return 1;
+}
+
+static const struct snd_kcontrol_new steelseries_bt_call_ducking_control = {
+	.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+	.name = "Bluetooth Call Audio Ducking",
+	.info = steelseries_bt_call_ducking_info,
+	.get = steelseries_bt_call_ducking_get,
+	.put = steelseries_bt_call_ducking_put,
+};
+
 static int steelseries_snd_register(struct steelseries_device *sd)
 {
 	struct hid_device *hdev = sd->hdev;
@@ -1644,6 +1732,21 @@ static int steelseries_snd_register(struct steelseries_device *sd)
 		sd->volume_limiter_id = kctl->id;
 	}
 
+	if (sd->info->capabilities & SS_CAP_BT_CALL_DUCKING) {
+		struct snd_kcontrol *kctl;
+		struct snd_kcontrol_new ducking_ctl = steelseries_bt_call_ducking_control;
+
+		ducking_ctl.access = SNDRV_CTL_ELEM_ACCESS_READWRITE;
+		if (sd->info->capabilities & SS_CAP_EXTERNAL_CONFIG)
+			ducking_ctl.access |= SNDRV_CTL_ELEM_ACCESS_VOLATILE;
+
+		kctl = snd_ctl_new1(&ducking_ctl, sd);
+		ret = snd_ctl_add(sd->card, kctl);
+		if (ret < 0)
+			goto err_free_card;
+		sd->bt_call_ducking_id = kctl->id;
+	}
+
 	ret = snd_card_register(sd->card);
 	if (ret < 0)
 		goto err_free_card;
@@ -1677,6 +1780,7 @@ static int steelseries_raw_event(struct hid_device *hdev,
 	u8 old_sidetone;
 	u8 old_mic_volume;
 	bool old_volume_limiter;
+	u8 old_bt_call_ducking;
 	bool is_async_interface = false;
 
 	if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1)
@@ -1694,6 +1798,7 @@ static int steelseries_raw_event(struct hid_device *hdev,
 	old_sidetone = sd->sidetone;
 	old_mic_volume = sd->mic_volume;
 	old_volume_limiter = sd->volume_limiter;
+	old_bt_call_ducking = sd->bt_call_ducking;
 
 	if (hid_is_usb(hdev)) {
 		struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
@@ -1763,6 +1868,9 @@ static int steelseries_raw_event(struct hid_device *hdev,
 		if (sd->volume_limiter != old_volume_limiter)
 			snd_ctl_notify(sd->card, SNDRV_CTL_EVENT_MASK_VALUE,
 				       &sd->volume_limiter_id);
+		if (sd->bt_call_ducking != old_bt_call_ducking)
+			snd_ctl_notify(sd->card, SNDRV_CTL_EVENT_MASK_VALUE,
+				       &sd->bt_call_ducking_id);
 	}
 
 	return 0;
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 15/18] HID: steelseries: Add inactive time sysfs attribute
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 headset auto-shutoff timer as a read/write sysfs attribute
(inactive_time), in minutes. Writing the attribute immediately sends the
new value to the device; reading it returns the last value reported by
the firmware.

The wire encoding differs per family:
- Arctis 1: HID feature report 0x06/0x53 with the value in minutes
- Arctis 7: HID feature report 0x06/0x51; split into its own write
  function as the command byte differs from the Arctis 1
- Arctis 9: converts minutes to seconds in a big-endian u16
- Nova 3P: rounds down to the nearest value in a discrete set
  {0,1,5,10,15,30,45,60,75,90} before sending command 0xa3
- Nova 5/7: output report with command 0xa3, no rounding required
- Nova Pro: maps minutes to six firmware-defined level indices via
  command 0xc1

The inactive_time_max field is added to the device info struct to
enforce the per-device maximum at write time.

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

diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index bb9abbb0b6f8..f076a0ef8af1 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -31,6 +31,7 @@
 #define SS_CAP_MIC_VOLUME		BIT(7)
 #define SS_CAP_VOLUME_LIMITER		BIT(8)
 #define SS_CAP_BT_CALL_DUCKING		BIT(9)
+#define SS_CAP_INACTIVE_TIME		BIT(10)
 
 #define SS_QUIRK_STATUS_SYNC_POLL	BIT(0)
 
@@ -38,6 +39,7 @@
 #define SS_SETTING_MIC_VOLUME		1
 #define SS_SETTING_VOLUME_LIMITER	2
 #define SS_SETTING_BT_CALL_DUCKING	3
+#define SS_SETTING_INACTIVE_TIME	4
 
 struct steelseries_device;
 
@@ -51,6 +53,7 @@ struct steelseries_device_info {
 	u8 sidetone_max;
 	u8 mic_volume_min;
 	u8 mic_volume_max;
+	u8 inactive_time_max;
 
 	int (*request_status)(struct hid_device *hdev);
 	void (*parse_status)(struct steelseries_device *sd, u8 *data, int size);
@@ -93,6 +96,7 @@ struct steelseries_device {
 
 	bool bt_enabled;
 	bool bt_device_connected;
+	u8 inactive_timeout;
 
 	spinlock_t lock;
 	bool removed;
@@ -476,6 +480,37 @@ static int steelseries_arctis_1_write_setting(struct hid_device *hdev,
 			return steelseries_send_feature_report(hdev, data,
 							       sizeof(data));
 		}
+	case SS_SETTING_INACTIVE_TIME: {
+		const u8 data[] = { 0x06, 0x53, value };
+
+		return steelseries_send_feature_report(hdev, data, sizeof(data));
+	}
+	default:
+		return -EINVAL;
+	}
+}
+
+static int steelseries_arctis_7_write_setting(struct hid_device *hdev,
+					      u8 setting, u8 value)
+{
+	switch (setting) {
+	case SS_SETTING_SIDETONE:
+		if (value == 0) {
+			const u8 data[] = { 0x06, 0x35 };
+
+			return steelseries_send_feature_report(hdev, data,
+							       sizeof(data));
+		} else {
+			const u8 data[] = { 0x06, 0x35, 0x01, 0x00, value };
+
+			return steelseries_send_feature_report(hdev, data,
+							       sizeof(data));
+		}
+	case SS_SETTING_INACTIVE_TIME: {
+		const u8 data[] = { 0x06, 0x51, value };
+
+		return steelseries_send_feature_report(hdev, data, sizeof(data));
+	}
 	default:
 		return -EINVAL;
 	}
@@ -490,11 +525,30 @@ static int steelseries_arctis_9_write_setting(struct hid_device *hdev,
 
 		return steelseries_send_feature_report(hdev, data, sizeof(data));
 	}
+	case SS_SETTING_INACTIVE_TIME: {
+		u16 seconds = (u16)value * 60;
+		const u8 data[] = { 0x04, 0x00, seconds >> 8, seconds & 0xff };
+
+		return steelseries_send_feature_report(hdev, data, sizeof(data));
+	}
 	default:
 		return -EINVAL;
 	}
 }
 
+static u8 steelseries_arctis_nova_3p_round_inactive_time(u8 minutes)
+{
+	static const u8 supported[] = { 0, 1, 5, 10, 15, 30, 45, 60, 75, 90 };
+	int i;
+
+	for (i = ARRAY_SIZE(supported) - 1; i > 0; i--) {
+		if (minutes >= supported[i])
+			return supported[i];
+	}
+
+	return 0;
+}
+
 static int steelseries_arctis_nova_3p_write_setting(struct hid_device *hdev,
 						    u8 setting, u8 value)
 {
@@ -510,6 +564,10 @@ static int steelseries_arctis_nova_3p_write_setting(struct hid_device *hdev,
 	case SS_SETTING_MIC_VOLUME:
 		cmd = 0x37;
 		break;
+	case SS_SETTING_INACTIVE_TIME:
+		cmd = 0xa3;
+		value = steelseries_arctis_nova_3p_round_inactive_time(value);
+		break;
 	default:
 		return -EINVAL;
 	}
@@ -542,6 +600,9 @@ static int steelseries_arctis_nova_5_write_setting(struct hid_device *hdev,
 	case SS_SETTING_VOLUME_LIMITER:
 		cmd = 0x27;
 		break;
+	case SS_SETTING_INACTIVE_TIME:
+		cmd = 0xa3;
+		break;
 	default:
 		return -EINVAL;
 	}
@@ -580,6 +641,9 @@ static int steelseries_arctis_nova_7_write_setting(struct hid_device *hdev,
 	case SS_SETTING_BT_CALL_DUCKING:
 		cmd = 0xb3;
 		break;
+	case SS_SETTING_INACTIVE_TIME:
+		cmd = 0xa3;
+		break;
 	default:
 		return -EINVAL;
 	}
@@ -612,6 +676,24 @@ static int steelseries_arctis_nova_pro_write_setting(struct hid_device *hdev,
 	case SS_SETTING_MIC_VOLUME:
 		cmd = 0x37;
 		break;
+	case SS_SETTING_INACTIVE_TIME:
+		cmd = 0xc1;
+		/* Map minutes to firmware level */
+		if (value >= 45)
+			value = 6; /* 60 min */
+		else if (value >= 23)
+			value = 5; /* 30 min */
+		else if (value >= 13)
+			value = 4; /* 15 min */
+		else if (value >= 8)
+			value = 3; /* 10 min */
+		else if (value >= 3)
+			value = 2; /* 5 min */
+		else if (value > 0)
+			value = 1; /* 1 min */
+		else
+			value = 0; /* disabled */
+		break;
 	default:
 		return -EINVAL;
 	}
@@ -916,6 +998,7 @@ static void steelseries_arctis_nova_7_gen2_parse_settings(
 		sd->volume_limiter = data[3];
 		break;
 	case 0xa0:
+		sd->inactive_timeout = data[1];
 		sd->bt_call_ducking = data[4];
 		break;
 	case 0x37:
@@ -927,6 +1010,9 @@ static void steelseries_arctis_nova_7_gen2_parse_settings(
 	case 0x3a:
 		sd->volume_limiter = data[1];
 		break;
+	case 0xa3:
+		sd->inactive_timeout = data[1];
+		break;
 	case 0xb3:
 		sd->bt_call_ducking = data[1];
 		break;
@@ -936,11 +1022,13 @@ static void steelseries_arctis_nova_7_gen2_parse_settings(
 static void steelseries_arctis_nova_pro_parse_settings(
 	struct steelseries_device *sd, u8 *data, int size)
 {
-	if (size < 10)
+	if (size < 13)
 		return;
 
-	if (data[0] == 0x06 && data[1] == 0xb0)
+	if (data[0] == 0x06 && data[1] == 0xb0) {
 		sd->mic_volume = data[9];
+		sd->inactive_timeout = data[12];
+	}
 }
 
 static void steelseries_arctis_nova_pro_parse_status(struct steelseries_device *sd,
@@ -970,9 +1058,10 @@ static const struct steelseries_device_info srws1_info = { };
 
 static const struct steelseries_device_info arctis_1_info = {
 	.sync_interface = 3,
-	.capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE,
+	.capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE | SS_CAP_INACTIVE_TIME,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
 	.sidetone_max = 18,
+	.inactive_time_max = 90,
 	.request_status = steelseries_arctis_1_request_status,
 	.parse_status = steelseries_arctis_1_parse_status,
 	.write_setting = steelseries_arctis_1_write_setting,
@@ -980,19 +1069,23 @@ static const struct steelseries_device_info arctis_1_info = {
 
 static const struct steelseries_device_info arctis_7_info = {
 	.sync_interface = 5,
-	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE,
+	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
+			SS_CAP_INACTIVE_TIME,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
 	.sidetone_max = 18,
+	.inactive_time_max = 90,
 	.request_status = steelseries_arctis_7_request_status,
 	.parse_status = steelseries_arctis_7_parse_status,
-	.write_setting = steelseries_arctis_1_write_setting,
+	.write_setting = steelseries_arctis_7_write_setting,
 };
 
 static const struct steelseries_device_info arctis_7_plus_info = {
 	.sync_interface = 3,
-	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE,
+	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
+			SS_CAP_INACTIVE_TIME,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
 	.sidetone_max = 3,
+	.inactive_time_max = 90,
 	.request_status = steelseries_arctis_nova_request_status,
 	.parse_status = steelseries_arctis_7_plus_parse_status,
 	.write_setting = steelseries_arctis_nova_5_write_setting,
@@ -1000,9 +1093,11 @@ static const struct steelseries_device_info arctis_7_plus_info = {
 
 static const struct steelseries_device_info arctis_9_info = {
 	.sync_interface = 0,
-	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE,
+	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
+			SS_CAP_INACTIVE_TIME,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
 	.sidetone_max = 61,
+	.inactive_time_max = 255,
 	.request_status = steelseries_arctis_9_request_status,
 	.parse_status = steelseries_arctis_9_parse_status,
 	.write_setting = steelseries_arctis_9_write_setting,
@@ -1010,10 +1105,12 @@ static const struct steelseries_device_info arctis_9_info = {
 
 static const struct steelseries_device_info arctis_nova_3p_info = {
 	.sync_interface = 4,
-	.capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME,
+	.capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME |
+			SS_CAP_INACTIVE_TIME,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
 	.sidetone_max = 10,
 	.mic_volume_max = 14,
+	.inactive_time_max = 90,
 	.request_status = steelseries_arctis_nova_3p_request_status,
 	.parse_status = steelseries_arctis_nova_3p_parse_status,
 	.write_setting = steelseries_arctis_nova_3p_write_setting,
@@ -1022,10 +1119,11 @@ static const struct steelseries_device_info arctis_nova_3p_info = {
 static const struct steelseries_device_info arctis_nova_5_info = {
 	.sync_interface = 3,
 	.capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME |
-			SS_CAP_VOLUME_LIMITER,
+			SS_CAP_VOLUME_LIMITER | SS_CAP_INACTIVE_TIME,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
 	.sidetone_max = 10,
 	.mic_volume_max = 15,
+	.inactive_time_max = 255,
 	.request_status = steelseries_arctis_nova_request_status,
 	.parse_status = steelseries_arctis_nova_5_parse_status,
 	.write_setting = steelseries_arctis_nova_5_write_setting,
@@ -1034,10 +1132,12 @@ static const struct steelseries_device_info arctis_nova_5_info = {
 static const struct steelseries_device_info arctis_nova_5x_info = {
 	.sync_interface = 3,
 	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
-			SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER,
+			SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
+			SS_CAP_INACTIVE_TIME,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
 	.sidetone_max = 10,
 	.mic_volume_max = 15,
+	.inactive_time_max = 255,
 	.request_status = steelseries_arctis_nova_request_status,
 	.parse_status = steelseries_arctis_nova_5x_parse_status,
 	.write_setting = steelseries_arctis_nova_5_write_setting,
@@ -1047,10 +1147,11 @@ static const struct steelseries_device_info arctis_nova_7_info = {
 	.sync_interface = 3,
 	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
 			SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
-			SS_CAP_BT_CALL_DUCKING,
+			SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
 	.sidetone_max = 3,
 	.mic_volume_max = 7,
+	.inactive_time_max = 255,
 	.request_status = steelseries_arctis_nova_request_status,
 	.parse_status = steelseries_arctis_nova_7_parse_status,
 	.write_setting = steelseries_arctis_nova_7_write_setting,
@@ -1059,9 +1160,10 @@ static const struct steelseries_device_info arctis_nova_7_info = {
 static const struct steelseries_device_info arctis_nova_7p_info = {
 	.sync_interface = 3,
 	.capabilities = SS_CAP_BATTERY | SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
-			SS_CAP_BT_CALL_DUCKING,
+			SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
 	.mic_volume_max = 7,
+	.inactive_time_max = 255,
 	.request_status = steelseries_arctis_nova_request_status,
 	.parse_status = steelseries_arctis_nova_7_parse_status,
 	.write_setting = steelseries_arctis_nova_7_write_setting,
@@ -1074,9 +1176,10 @@ static const struct steelseries_device_info arctis_nova_7_gen2_info = {
 			SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
 			SS_CAP_EXTERNAL_CONFIG | SS_CAP_SIDETONE |
 			SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
-			SS_CAP_BT_CALL_DUCKING,
+			SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME,
 	.sidetone_max = 3,
 	.mic_volume_max = 7,
+	.inactive_time_max = 255,
 	.request_status = steelseries_arctis_nova_request_status,
 	.parse_status = steelseries_arctis_nova_7_gen2_parse_status,
 	.request_settings = steelseries_arctis_nova_7_gen2_request_settings,
@@ -1088,11 +1191,12 @@ static const struct steelseries_device_info arctis_nova_pro_info = {
 	.sync_interface = 4,
 	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_MIC_MUTE |
 			SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
-			SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME,
+			SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME | SS_CAP_INACTIVE_TIME,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
 	.sidetone_max = 3,
 	.mic_volume_min = 1,
 	.mic_volume_max = 10,
+	.inactive_time_max = 60,
 	.request_status = steelseries_arctis_nova_pro_request_status,
 	.parse_status = steelseries_arctis_nova_pro_parse_status,
 	.parse_settings = steelseries_arctis_nova_pro_parse_settings,
@@ -1271,12 +1375,55 @@ static ssize_t bt_device_connected_show(struct device *dev,
 	return sysfs_emit(buf, "%d\n", sd->bt_device_connected);
 }
 
+static ssize_t inactive_time_show(struct device *dev,
+				  struct device_attribute *attr, char *buf)
+{
+	struct hid_device *hdev = to_hid_device(dev);
+	struct steelseries_device *sd = hid_get_drvdata(hdev);
+
+	if (!sd->headset_connected)
+		return -ENODEV;
+
+	return sysfs_emit(buf, "%d\n", sd->inactive_timeout);
+}
+
+static ssize_t inactive_time_store(struct device *dev,
+				   struct device_attribute *attr,
+				   const char *buf, size_t count)
+{
+	struct hid_device *hdev = to_hid_device(dev);
+	struct steelseries_device *sd = hid_get_drvdata(hdev);
+	unsigned int value;
+	int ret;
+
+	if (!sd->headset_connected)
+		return -ENODEV;
+
+	ret = kstrtouint(buf, 10, &value);
+	if (ret)
+		return ret;
+
+	if (value > sd->info->inactive_time_max)
+		return -EINVAL;
+
+	ret = sd->info->write_setting(sd->hdev, SS_SETTING_INACTIVE_TIME,
+				      value);
+	if (ret)
+		return ret;
+
+	sd->inactive_timeout = value;
+
+	return count;
+}
+
 static DEVICE_ATTR_RO(bt_enabled);
 static DEVICE_ATTR_RO(bt_device_connected);
+static DEVICE_ATTR_RW(inactive_time);
 
 static struct attribute *steelseries_headset_attrs[] = {
 	&dev_attr_bt_enabled.attr,
 	&dev_attr_bt_device_connected.attr,
+	&dev_attr_inactive_time.attr,
 	NULL,
 };
 
@@ -1298,6 +1445,8 @@ static umode_t steelseries_headset_attr_is_visible(struct kobject *kobj,
 		return (caps & SS_CAP_BT_ENABLED) ? attr->mode : 0;
 	if (attr == &dev_attr_bt_device_connected.attr)
 		return (caps & SS_CAP_BT_DEVICE_CONNECTED) ? attr->mode : 0;
+	if (attr == &dev_attr_inactive_time.attr)
+		return (caps & SS_CAP_INACTIVE_TIME) ? attr->mode : 0;
 
 	return 0;
 }
@@ -1956,7 +2105,8 @@ static int steelseries_probe(struct hid_device *hdev,
 				hid_warn(hdev, "Failed to register battery: %d\n", ret);
 		}
 
-		if (info->capabilities & (SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED)) {
+		if (info->capabilities & (SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
+					  SS_CAP_INACTIVE_TIME)) {
 			ret = sysfs_create_group(&hdev->dev.kobj,
 						 &steelseries_headset_attr_group);
 			if (ret)
@@ -2038,7 +2188,8 @@ static void steelseries_remove(struct hid_device *hdev)
 	}
 
 	if (interface_num == sd->info->sync_interface) {
-		if (sd->info->capabilities & (SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED))
+		if (sd->info->capabilities & (SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
+					      SS_CAP_INACTIVE_TIME))
 			sysfs_remove_group(&hdev->dev.kobj,
 					   &steelseries_headset_attr_group);
 
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 16/18] HID: steelseries: Add Bluetooth auto-enable sysfs attribute
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 Bluetooth auto-enable setting as a read/write sysfs boolean
attribute (bt_auto_enable). When enabled, the headset activates its
Bluetooth radio automatically on power-on. Currently supported on the
Arctis Nova 7, Nova 7P, and Nova 7 Gen2 via write command 0xb2.

On the Nova 7 Gen2, the current value is recovered from the 0xa0 device
settings response alongside inactive timeout and call audio ducking.

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

diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index f076a0ef8af1..a794af01e15a 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -32,6 +32,7 @@
 #define SS_CAP_VOLUME_LIMITER		BIT(8)
 #define SS_CAP_BT_CALL_DUCKING		BIT(9)
 #define SS_CAP_INACTIVE_TIME		BIT(10)
+#define SS_CAP_BT_AUTO_ENABLE		BIT(11)
 
 #define SS_QUIRK_STATUS_SYNC_POLL	BIT(0)
 
@@ -40,6 +41,7 @@
 #define SS_SETTING_VOLUME_LIMITER	2
 #define SS_SETTING_BT_CALL_DUCKING	3
 #define SS_SETTING_INACTIVE_TIME	4
+#define SS_SETTING_BT_AUTO_ENABLE	5
 
 struct steelseries_device;
 
@@ -97,6 +99,7 @@ struct steelseries_device {
 	bool bt_enabled;
 	bool bt_device_connected;
 	u8 inactive_timeout;
+	bool bt_auto_enable;
 
 	spinlock_t lock;
 	bool removed;
@@ -644,6 +647,9 @@ static int steelseries_arctis_nova_7_write_setting(struct hid_device *hdev,
 	case SS_SETTING_INACTIVE_TIME:
 		cmd = 0xa3;
 		break;
+	case SS_SETTING_BT_AUTO_ENABLE:
+		cmd = 0xb2;
+		break;
 	default:
 		return -EINVAL;
 	}
@@ -999,6 +1005,7 @@ static void steelseries_arctis_nova_7_gen2_parse_settings(
 		break;
 	case 0xa0:
 		sd->inactive_timeout = data[1];
+		sd->bt_auto_enable = data[3];
 		sd->bt_call_ducking = data[4];
 		break;
 	case 0x37:
@@ -1013,6 +1020,9 @@ static void steelseries_arctis_nova_7_gen2_parse_settings(
 	case 0xa3:
 		sd->inactive_timeout = data[1];
 		break;
+	case 0xb2:
+		sd->bt_auto_enable = data[1];
+		break;
 	case 0xb3:
 		sd->bt_call_ducking = data[1];
 		break;
@@ -1147,7 +1157,8 @@ static const struct steelseries_device_info arctis_nova_7_info = {
 	.sync_interface = 3,
 	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
 			SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
-			SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME,
+			SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME |
+			SS_CAP_BT_AUTO_ENABLE,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
 	.sidetone_max = 3,
 	.mic_volume_max = 7,
@@ -1160,7 +1171,8 @@ static const struct steelseries_device_info arctis_nova_7_info = {
 static const struct steelseries_device_info arctis_nova_7p_info = {
 	.sync_interface = 3,
 	.capabilities = SS_CAP_BATTERY | SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
-			SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME,
+			SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME |
+			SS_CAP_BT_AUTO_ENABLE,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
 	.mic_volume_max = 7,
 	.inactive_time_max = 255,
@@ -1176,7 +1188,8 @@ static const struct steelseries_device_info arctis_nova_7_gen2_info = {
 			SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
 			SS_CAP_EXTERNAL_CONFIG | SS_CAP_SIDETONE |
 			SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
-			SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME,
+			SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME |
+			SS_CAP_BT_AUTO_ENABLE,
 	.sidetone_max = 3,
 	.mic_volume_max = 7,
 	.inactive_time_max = 255,
@@ -1416,14 +1429,53 @@ static ssize_t inactive_time_store(struct device *dev,
 	return count;
 }
 
+static ssize_t bt_auto_enable_show(struct device *dev,
+				   struct device_attribute *attr, char *buf)
+{
+	struct hid_device *hdev = to_hid_device(dev);
+	struct steelseries_device *sd = hid_get_drvdata(hdev);
+
+	if (!sd->headset_connected)
+		return -ENODEV;
+
+	return sysfs_emit(buf, "%d\n", sd->bt_auto_enable);
+}
+
+static ssize_t bt_auto_enable_store(struct device *dev,
+				    struct device_attribute *attr,
+				    const char *buf, size_t count)
+{
+	struct hid_device *hdev = to_hid_device(dev);
+	struct steelseries_device *sd = hid_get_drvdata(hdev);
+	bool value;
+	int ret;
+
+	if (!sd->headset_connected)
+		return -ENODEV;
+
+	ret = kstrtobool(buf, &value);
+	if (ret)
+		return ret;
+
+	ret = sd->info->write_setting(sd->hdev, SS_SETTING_BT_AUTO_ENABLE, value);
+	if (ret)
+		return ret;
+
+	sd->bt_auto_enable = value;
+
+	return count;
+}
+
 static DEVICE_ATTR_RO(bt_enabled);
 static DEVICE_ATTR_RO(bt_device_connected);
 static DEVICE_ATTR_RW(inactive_time);
+static DEVICE_ATTR_RW(bt_auto_enable);
 
 static struct attribute *steelseries_headset_attrs[] = {
 	&dev_attr_bt_enabled.attr,
 	&dev_attr_bt_device_connected.attr,
 	&dev_attr_inactive_time.attr,
+	&dev_attr_bt_auto_enable.attr,
 	NULL,
 };
 
@@ -1447,6 +1499,8 @@ static umode_t steelseries_headset_attr_is_visible(struct kobject *kobj,
 		return (caps & SS_CAP_BT_DEVICE_CONNECTED) ? attr->mode : 0;
 	if (attr == &dev_attr_inactive_time.attr)
 		return (caps & SS_CAP_INACTIVE_TIME) ? attr->mode : 0;
+	if (attr == &dev_attr_bt_auto_enable.attr)
+		return (caps & SS_CAP_BT_AUTO_ENABLE) ? attr->mode : 0;
 
 	return 0;
 }
@@ -2106,7 +2160,8 @@ static int steelseries_probe(struct hid_device *hdev,
 		}
 
 		if (info->capabilities & (SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
-					  SS_CAP_INACTIVE_TIME)) {
+					  SS_CAP_INACTIVE_TIME |
+					  SS_CAP_BT_AUTO_ENABLE)) {
 			ret = sysfs_create_group(&hdev->dev.kobj,
 						 &steelseries_headset_attr_group);
 			if (ret)
@@ -2189,7 +2244,7 @@ static void steelseries_remove(struct hid_device *hdev)
 
 	if (interface_num == sd->info->sync_interface) {
 		if (sd->info->capabilities & (SS_CAP_BT_ENABLED | SS_CAP_BT_DEVICE_CONNECTED |
-					      SS_CAP_INACTIVE_TIME))
+					      SS_CAP_INACTIVE_TIME | SS_CAP_BT_AUTO_ENABLE))
 			sysfs_remove_group(&hdev->dev.kobj,
 					   &steelseries_headset_attr_group);
 
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 17/18] HID: steelseries: Add mic mute LED brightness 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>

Register the microphone mute LED as an LED class device named
"<device>::micmute", following the standard LED naming convention. The
brightness range is 0-3 representing off, low, medium, and high.

On the Arctis Nova 5 family, the discrete levels map to non-linear
hardware values expected by the firmware (0, 1, 4, 10). The Nova 7
family uses a direct linear mapping. On the Nova 7 Gen2, the current
brightness is recovered from the 0xa0 settings poll response.

Registration is guarded by a LEDS_CLASS module compatibility check
analogous to the existing SND guard.

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

diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index a794af01e15a..dcd34c61cccd 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -33,6 +33,7 @@
 #define SS_CAP_BT_CALL_DUCKING		BIT(9)
 #define SS_CAP_INACTIVE_TIME		BIT(10)
 #define SS_CAP_BT_AUTO_ENABLE		BIT(11)
+#define SS_CAP_MIC_MUTE_BRIGHTNESS	BIT(12)
 
 #define SS_QUIRK_STATUS_SYNC_POLL	BIT(0)
 
@@ -42,6 +43,7 @@
 #define SS_SETTING_BT_CALL_DUCKING	3
 #define SS_SETTING_INACTIVE_TIME	4
 #define SS_SETTING_BT_AUTO_ENABLE	5
+#define SS_SETTING_MIC_MUTE_BRIGHTNESS	6
 
 struct steelseries_device;
 
@@ -100,6 +102,12 @@ struct steelseries_device {
 	bool bt_device_connected;
 	u8 inactive_timeout;
 	bool bt_auto_enable;
+	u8 mic_mute_brightness;
+
+#if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
+    (IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
+	struct led_classdev *mic_mute_led;
+#endif
 
 	spinlock_t lock;
 	bool removed;
@@ -606,6 +614,14 @@ static int steelseries_arctis_nova_5_write_setting(struct hid_device *hdev,
 	case SS_SETTING_INACTIVE_TIME:
 		cmd = 0xa3;
 		break;
+	case SS_SETTING_MIC_MUTE_BRIGHTNESS:
+		cmd = 0xae;
+		/* Hardware uses non-linear values: 0=off, 1=low, 4=medium, 10=high */
+		if (value == 2)
+			value = 0x04;
+		else if (value == 3)
+			value = 0x0a;
+		break;
 	default:
 		return -EINVAL;
 	}
@@ -650,6 +666,9 @@ static int steelseries_arctis_nova_7_write_setting(struct hid_device *hdev,
 	case SS_SETTING_BT_AUTO_ENABLE:
 		cmd = 0xb2;
 		break;
+	case SS_SETTING_MIC_MUTE_BRIGHTNESS:
+		cmd = 0xae;
+		break;
 	default:
 		return -EINVAL;
 	}
@@ -1005,6 +1024,7 @@ static void steelseries_arctis_nova_7_gen2_parse_settings(
 		break;
 	case 0xa0:
 		sd->inactive_timeout = data[1];
+		sd->mic_mute_brightness = data[2];
 		sd->bt_auto_enable = data[3];
 		sd->bt_call_ducking = data[4];
 		break;
@@ -1020,6 +1040,9 @@ static void steelseries_arctis_nova_7_gen2_parse_settings(
 	case 0xa3:
 		sd->inactive_timeout = data[1];
 		break;
+	case 0xae:
+		sd->mic_mute_brightness = data[1];
+		break;
 	case 0xb2:
 		sd->bt_auto_enable = data[1];
 		break;
@@ -1129,7 +1152,8 @@ static const struct steelseries_device_info arctis_nova_3p_info = {
 static const struct steelseries_device_info arctis_nova_5_info = {
 	.sync_interface = 3,
 	.capabilities = SS_CAP_BATTERY | SS_CAP_SIDETONE | SS_CAP_MIC_VOLUME |
-			SS_CAP_VOLUME_LIMITER | SS_CAP_INACTIVE_TIME,
+			SS_CAP_VOLUME_LIMITER | SS_CAP_INACTIVE_TIME |
+			SS_CAP_MIC_MUTE_BRIGHTNESS,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
 	.sidetone_max = 10,
 	.mic_volume_max = 15,
@@ -1143,7 +1167,7 @@ static const struct steelseries_device_info arctis_nova_5x_info = {
 	.sync_interface = 3,
 	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
 			SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
-			SS_CAP_INACTIVE_TIME,
+			SS_CAP_INACTIVE_TIME | SS_CAP_MIC_MUTE_BRIGHTNESS,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
 	.sidetone_max = 10,
 	.mic_volume_max = 15,
@@ -1158,7 +1182,7 @@ static const struct steelseries_device_info arctis_nova_7_info = {
 	.capabilities = SS_CAP_BATTERY | SS_CAP_CHATMIX | SS_CAP_SIDETONE |
 			SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
 			SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME |
-			SS_CAP_BT_AUTO_ENABLE,
+			SS_CAP_BT_AUTO_ENABLE | SS_CAP_MIC_MUTE_BRIGHTNESS,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
 	.sidetone_max = 3,
 	.mic_volume_max = 7,
@@ -1172,7 +1196,7 @@ static const struct steelseries_device_info arctis_nova_7p_info = {
 	.sync_interface = 3,
 	.capabilities = SS_CAP_BATTERY | SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
 			SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME |
-			SS_CAP_BT_AUTO_ENABLE,
+			SS_CAP_BT_AUTO_ENABLE | SS_CAP_MIC_MUTE_BRIGHTNESS,
 	.quirks = SS_QUIRK_STATUS_SYNC_POLL,
 	.mic_volume_max = 7,
 	.inactive_time_max = 255,
@@ -1189,7 +1213,7 @@ static const struct steelseries_device_info arctis_nova_7_gen2_info = {
 			SS_CAP_EXTERNAL_CONFIG | SS_CAP_SIDETONE |
 			SS_CAP_MIC_VOLUME | SS_CAP_VOLUME_LIMITER |
 			SS_CAP_BT_CALL_DUCKING | SS_CAP_INACTIVE_TIME |
-			SS_CAP_BT_AUTO_ENABLE,
+			SS_CAP_BT_AUTO_ENABLE | SS_CAP_MIC_MUTE_BRIGHTNESS,
 	.sidetone_max = 3,
 	.mic_volume_max = 7,
 	.inactive_time_max = 255,
@@ -1970,6 +1994,82 @@ static void steelseries_snd_unregister(struct steelseries_device *sd)
 
 #endif
 
+/*
+ * Mic mute LED
+ */
+
+#if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
+    (IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
+
+#define SS_MIC_MUTE_BRIGHTNESS_MAX 3
+
+static int steelseries_mic_mute_led_brightness_set(struct led_classdev *led_cdev,
+						   enum led_brightness brightness)
+{
+	struct device *dev = led_cdev->dev->parent;
+	struct hid_device *hdev = to_hid_device(dev);
+	struct steelseries_device *sd = hid_get_drvdata(hdev);
+	unsigned long flags;
+	int ret;
+
+	if (brightness > SS_MIC_MUTE_BRIGHTNESS_MAX)
+		brightness = SS_MIC_MUTE_BRIGHTNESS_MAX;
+
+	spin_lock_irqsave(&sd->lock, flags);
+	if (sd->removed) {
+		spin_unlock_irqrestore(&sd->lock, flags);
+		return 0;
+	}
+	spin_unlock_irqrestore(&sd->lock, flags);
+
+	ret = sd->info->write_setting(sd->hdev, SS_SETTING_MIC_MUTE_BRIGHTNESS,
+				      brightness);
+	if (ret)
+		return ret;
+
+	sd->mic_mute_brightness = brightness;
+
+	return 0;
+}
+
+static enum led_brightness
+steelseries_mic_mute_led_brightness_get(struct led_classdev *led_cdev)
+{
+	struct device *dev = led_cdev->dev->parent;
+	struct hid_device *hdev = to_hid_device(dev);
+	struct steelseries_device *sd = hid_get_drvdata(hdev);
+
+	return sd->mic_mute_brightness;
+}
+
+static int steelseries_mic_mute_led_register(struct steelseries_device *sd)
+{
+	struct hid_device *hdev = sd->hdev;
+	struct led_classdev *led;
+	size_t name_size;
+	char *name;
+
+	name_size = strlen(dev_name(&hdev->dev)) + 16;
+
+	led = devm_kzalloc(&hdev->dev, sizeof(*led) + name_size, GFP_KERNEL);
+	if (!led)
+		return -ENOMEM;
+
+	name = (void *)(&led[1]);
+	snprintf(name, name_size, "%s::micmute", dev_name(&hdev->dev));
+	led->name = name;
+	led->brightness = 0;
+	led->max_brightness = SS_MIC_MUTE_BRIGHTNESS_MAX;
+	led->brightness_get = steelseries_mic_mute_led_brightness_get;
+	led->brightness_set_blocking = steelseries_mic_mute_led_brightness_set;
+
+	sd->mic_mute_led = led;
+
+	return devm_led_classdev_register(&hdev->dev, led);
+}
+
+#endif
+
 static int steelseries_raw_event(struct hid_device *hdev,
 				 struct hid_report *report, u8 *data, int size)
 {
@@ -2175,6 +2275,15 @@ static int steelseries_probe(struct hid_device *hdev,
 			hid_warn(hdev, "Failed to register sound card: %d\n", ret);
 #endif
 
+#if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
+	(IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
+		if (info->capabilities & SS_CAP_MIC_MUTE_BRIGHTNESS) {
+			ret = steelseries_mic_mute_led_register(sd);
+			if (ret < 0)
+				hid_warn(hdev, "Failed to register mic mute LED: %d\n", ret);
+		}
+#endif
+
 		INIT_DELAYED_WORK(&sd->status_work, steelseries_status_timer_work_handler);
 		INIT_DELAYED_WORK(&sd->settings_work, steelseries_settings_work_handler);
 
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 18/18] HID: steelseries: Document sysfs ABI
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 Documentation/ABI/testing/sysfs-driver-hid-steelseries documenting
the sysfs attributes and LED class device exposed by the driver:

- bt_enabled, bt_device_connected: read-only Bluetooth radio state
- inactive_time: read/write auto-shutoff timer in minutes
- bt_auto_enable: read/write Bluetooth radio power-on behavior
- <dev>::micmute/brightness: mic mute LED brightness via LED class

Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
 .../ABI/testing/sysfs-driver-hid-steelseries  | 87 +++++++++++++++++++
 1 file changed, 87 insertions(+)
 create mode 100644 Documentation/ABI/testing/sysfs-driver-hid-steelseries

diff --git a/Documentation/ABI/testing/sysfs-driver-hid-steelseries b/Documentation/ABI/testing/sysfs-driver-hid-steelseries
new file mode 100644
index 000000000000..7b8d29282ed6
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-driver-hid-steelseries
@@ -0,0 +1,87 @@
+What:		/sys/bus/hid/drivers/steelseries/<dev>/bt_enabled
+Date:		February 2026
+KernelVersion:	6.20
+Contact:	Sriman Achanta <srimanachanta@gmail.com>
+Description:	(RO) Whether the Bluetooth radio on the headset is currently
+		enabled.
+
+		* 0 = Bluetooth radio off
+		* 1 = Bluetooth radio on
+
+		Returns -ENODEV if the headset is not connected to the
+		receiver.
+
+		Supported on: Arctis Nova 7 Gen2, Arctis Nova Pro Wireless
+
+What:		/sys/bus/hid/drivers/steelseries/<dev>/bt_device_connected
+Date:		February 2026
+KernelVersion:	6.20
+Contact:	Sriman Achanta <srimanachanta@gmail.com>
+Description:	(RO) Whether a Bluetooth device is currently connected to
+		the headset.
+
+		* 0 = no Bluetooth device connected
+		* 1 = Bluetooth device connected
+
+		Returns -ENODEV if the headset is not connected to the
+		receiver.
+
+		Supported on: Arctis Nova 7 Gen2, Arctis Nova Pro Wireless
+
+What:		/sys/bus/hid/drivers/steelseries/<dev>/inactive_time
+Date:		February 2026
+KernelVersion:	6.20
+Contact:	Sriman Achanta <srimanachanta@gmail.com>
+Description:	(RW) Auto-shutoff timer for the headset, in minutes. A
+		value of 0 disables the timer. The maximum accepted value
+		is device-specific.
+
+		The encoding sent to the firmware varies by device family:
+		the Arctis 9 converts the value to seconds, the Nova 3P
+		rounds down to its nearest supported discrete step, and the
+		Nova Pro maps to six firmware-defined level indices. For all
+		other devices the value is sent in minutes directly.
+
+		Reading the attribute returns the last value reported by the
+		firmware. Writing immediately sends the new timeout to the
+		device.
+
+		Returns -ENODEV if the headset is not connected to the
+		receiver.
+
+		Supported on: Arctis 1 Wireless, Arctis 7, Arctis 7+,
+		Arctis 9, Arctis Nova 3P, Arctis Nova 5, Arctis Nova 5X,
+		Arctis Nova 7, Arctis Nova 7P, Arctis Nova 7 Gen2,
+		Arctis Nova Pro Wireless
+
+What:		/sys/bus/hid/drivers/steelseries/<dev>/bt_auto_enable
+Date:		February 2026
+KernelVersion:	6.20
+Contact:	Sriman Achanta <srimanachanta@gmail.com>
+Description:	(RW) Whether the headset automatically enables its
+		Bluetooth radio on power-on.
+
+		* 0 = Bluetooth radio stays off at power-on
+		* 1 = Bluetooth radio activates automatically at power-on
+
+		Returns -ENODEV if the headset is not connected to the
+		receiver.
+
+		Supported on: Arctis Nova 7, Arctis Nova 7P,
+		Arctis Nova 7 Gen2
+
+What:		/sys/class/leds/<dev>::micmute/brightness
+Date:		February 2026
+KernelVersion:	6.20
+Contact:	Sriman Achanta <srimanachanta@gmail.com>
+Description:	(RW) Brightness of the microphone mute status LED.
+		<dev> is the HID device node name (e.g.
+		0003:1038:12AE.0001).
+
+		* 0 = off
+		* 1 = low
+		* 2 = medium
+		* 3 = high
+
+		Supported on: Arctis Nova 5, Arctis Nova 5X, Arctis Nova 7,
+		Arctis Nova 7P, Arctis Nova 7 Gen2
-- 
2.53.0


^ permalink raw reply related

* [PATCH v2 3/3] MAINTAINERS: add an entry for Goodix GTX8 Touchscreen driver
From: Aelin Reidel @ 2026-02-28  1:56 UTC (permalink / raw)
  To: Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Hans de Goede, Neil Armstrong, Henrik Rydberg
  Cc: linux-input, devicetree, linux-kernel, linux, phone-devel,
	~postmarketos/upstreaming, Aelin Reidel
In-Reply-To: <20260228-gtx8-v2-0-3a408c365f6c@mainlining.org>

Add MAINTAINERS entry for the Goodix GTX8 Touchscreen IC driver.

Signed-off-by: Aelin Reidel <aelin@mainlining.org>
---
 MAINTAINERS | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index 14899f1de77ed2e8a583cf7b0fea25725c8534cb..c76f9fbe51f929f7eded37760cb5c83dfa337d0b 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -10849,6 +10849,13 @@ M:	Maud Spierings <maudspierings@gocontroll.com>
 S:	Maintained
 F:	Documentation/devicetree/bindings/connector/gocontroll,moduline-module-slot.yaml
 
+GOODIX GTX8 TOUCHSCREEN
+M:	Aelin Reidel <aelin@mainlining.org>
+L:	linux-input@vger.kernel.org
+S:	Maintained
+F:	Documentation/devicetree/bindings/input/touchscreen/goodix,gt9886.yaml
+F:	drivers/input/touchscreen/goodix_gtx8*
+
 GOODIX TOUCHSCREEN
 M:	Hans de Goede <hansg@kernel.org>
 L:	linux-input@vger.kernel.org

-- 
2.53.0


^ permalink raw reply related

* [PATCH v2 1/3] dt-bindings: input: document Goodix GTX8 Touchscreen ICs
From: Aelin Reidel @ 2026-02-28  1:56 UTC (permalink / raw)
  To: Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Hans de Goede, Neil Armstrong, Henrik Rydberg
  Cc: linux-input, devicetree, linux-kernel, linux, phone-devel,
	~postmarketos/upstreaming, Aelin Reidel
In-Reply-To: <20260228-gtx8-v2-0-3a408c365f6c@mainlining.org>

Document the Goodix GT9886 and GT9896 which are part of the GTX8 series
of Touchscreen controller ICs from Goodix.

Reviewed-by: Rob Herring (Arm) <robh@kernel.org>
Signed-off-by: Aelin Reidel <aelin@mainlining.org>
---
 .../bindings/input/touchscreen/goodix,gt9886.yaml  | 71 ++++++++++++++++++++++
 1 file changed, 71 insertions(+)

diff --git a/Documentation/devicetree/bindings/input/touchscreen/goodix,gt9886.yaml b/Documentation/devicetree/bindings/input/touchscreen/goodix,gt9886.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..6307495c2746313cfc32cdbb701455d1596be435
--- /dev/null
+++ b/Documentation/devicetree/bindings/input/touchscreen/goodix,gt9886.yaml
@@ -0,0 +1,71 @@
+# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/input/touchscreen/goodix,gt9886.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Goodix GTX8 series touchscreen controller
+
+maintainers:
+  - Aelin Reidel <aelin@mainlining.org>
+
+allOf:
+  - $ref: touchscreen.yaml#
+
+properties:
+  compatible:
+    enum:
+      - goodix,gt9886
+      - goodix,gt9896
+
+  reg:
+    maxItems: 1
+
+  interrupts:
+    maxItems: 1
+
+  reset-gpios:
+    maxItems: 1
+
+  avdd-supply:
+    description: Analog power supply regulator on AVDD pin
+
+  vddio-supply:
+    description: power supply regulator on VDDIO pin
+
+  touchscreen-inverted-x: true
+  touchscreen-inverted-y: true
+  touchscreen-size-x: true
+  touchscreen-size-y: true
+  touchscreen-swapped-x-y: true
+
+additionalProperties: false
+
+required:
+  - compatible
+  - reg
+  - interrupts
+  - avdd-supply
+  - touchscreen-size-x
+  - touchscreen-size-y
+
+examples:
+  - |
+    #include <dt-bindings/interrupt-controller/irq.h>
+    #include <dt-bindings/gpio/gpio.h>
+    i2c {
+      #address-cells = <1>;
+      #size-cells = <0>;
+      touchscreen@5d {
+        compatible = "goodix,gt9886";
+        reg = <0x5d>;
+        interrupt-parent = <&gpio>;
+        interrupts = <9 IRQ_TYPE_LEVEL_LOW>;
+        reset-gpios = <&gpio1 8 GPIO_ACTIVE_LOW>;
+        avdd-supply = <&ts_avdd>;
+        touchscreen-size-x = <1080>;
+        touchscreen-size-y = <2340>;
+      };
+    };
+
+...

-- 
2.53.0


^ permalink raw reply related

* [PATCH v2 0/3] Input: add initial support for Goodix GTX8 touchscreen ICs
From: Aelin Reidel @ 2026-02-28  1:56 UTC (permalink / raw)
  To: Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Hans de Goede, Neil Armstrong, Henrik Rydberg
  Cc: linux-input, devicetree, linux-kernel, linux, phone-devel,
	~postmarketos/upstreaming, Aelin Reidel, Piyush Raj Chouhan,
	Alexander Koskovich

These ICs support SPI and I2C interfaces, up to 10 finger touch, stylus
and gesture events.

This driver is derived from the Goodix gtx8_driver_linux available at
[1] and only supports the GT9886 and GT9896 ICs present in the Xiaomi
Mi 9T and Xiaomi Redmi Note 10 Pro smartphones.

The current implementation only supports Normandy and Yellowstone type
ICs, aka only GT9886 and GT9896. It is also limited to I2C only, since I
don't have a device with GTX8 over SPI at hand. Adding support for SPI
should be fairly easy in the future, since the code uses a regmap.

Support for advanced features like:
- Firmware updates
- Stylus events
- Gesture events
- Nanjing IC support
is not included in current version.

The current support requires a previously flashed firmware to be
present.

As I did not have access to datasheets for these ICs, I extracted the
addresses from a couple of config files using a small tool [2]. The
addresses are identical for the same IC families in all configs I
observed, however not all of them make sense and I stubbed out firmware
request support due to this.

I've taken a lot of inspiration from the goodix_berlin driver, but the 
Berlin and GTX8 series of touchscreen ICs differ quite a bit. The driver 
architecture is the same overall, i.e. the power-up sequence and general 
concepts are the mostly same, but it is very clear that they are 
different generations when looking at it in more detail.

Some of the differences:
- There is no equivalent to the bootoption reg that I can find in the 
public GTX8 drivers
- Firmware version struct layout is different yet again
- GTX8 does not expose IC information at runtime as far as I can tell
- The checksum method differs yet again
- The vendor driver reads only 1 touch upfront rather than 2
- Register addresses are 16-bit on GTX8 and 32-bit on Berlin
- Firmware requests don't appear to really exist on GTX8

From what I can tell, the evolution seems to be:
Normandy -> Yellowstone -> Berlin
since Normandy and Yellowstone are already quite different (especially 
with the way checksums work) and Yellowstone has a couple of things 
(checksum, fw_version) that appear similar to Berlin series ICs.

I've tried to make the Berlin driver work for GTX8 ICs before, but 
they're so different (and I lack documentation for registers to perhaps 
make some parts work on GTX8) that I'd rather support these ICs in a new 
and tiny driver. I hope that makes sense. I took heavy inspiration from 
the Berlin driver, but the only parts that are really common between 
them are very trivial things like e.g. the input dev config or power on, 
which I don't think are worth putting in a separate header.

[1] https://github.com/goodix/gtx8_driver_linux
[2] https://github.com/sm7150-mainline/goodix-cfg-bin

Signed-off-by: Aelin Reidel <aelin@mainlining.org>
---
Changes in v2:
- Fix compilation issues found by Intel's kernel test robot
- Add Alexander's T-b to the driver patch
- Link to v1: https://lore.kernel.org/r/20260218-gtx8-v1-0-0d575b3dedc5@mainlining.org

Changes in v1 (post-RFC):
- Drop RFC prefix, the series has been tested enough and works well
  as-is
- Update my name and email address
- Add some reasoning for a new driver to the cover letter
- Add Rob's R-b on the dt-bindings patch
- Add Piyush's T-b to the driver patch
- Link to RFC: https://lore.kernel.org/r/20250918-gtx8-v1-0-cba879c84775@mainlining.org

---
Aelin Reidel (3):
      dt-bindings: input: document Goodix GTX8 Touchscreen ICs
      Input: add support for Goodix GTX8 Touchscreen ICs
      MAINTAINERS: add an entry for Goodix GTX8 Touchscreen driver

 .../bindings/input/touchscreen/goodix,gt9886.yaml  |  71 +++
 MAINTAINERS                                        |   7 +
 drivers/input/touchscreen/Kconfig                  |  15 +
 drivers/input/touchscreen/Makefile                 |   1 +
 drivers/input/touchscreen/goodix_gtx8.c            | 563 +++++++++++++++++++++
 drivers/input/touchscreen/goodix_gtx8.h            | 141 ++++++
 6 files changed, 798 insertions(+)
---
base-commit: 3fa5e5702a82d259897bd7e209469bc06368bf31
change-id: 20250918-gtx8-59a50ccd78a5

Best regards,
-- 
Aelin Reidel <aelin@mainlining.org>


^ permalink raw reply

* [PATCH v2 2/3] Input: add support for Goodix GTX8 Touchscreen ICs
From: Aelin Reidel @ 2026-02-28  1:56 UTC (permalink / raw)
  To: Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Hans de Goede, Neil Armstrong, Henrik Rydberg
  Cc: linux-input, devicetree, linux-kernel, linux, phone-devel,
	~postmarketos/upstreaming, Aelin Reidel, Piyush Raj Chouhan,
	Alexander Koskovich
In-Reply-To: <20260228-gtx8-v2-0-3a408c365f6c@mainlining.org>

Add initial support for the Goodix GTX8 touchscreen ICs.

These ICs support SPI and I2C interfaces, up to 10 finger touch, stylus
and gesture events.

This driver is derived from the Goodix gtx8_driver_linux available at
[1] and only supports the GT9886 and GT9896 ICs present in the Xiaomi
Mi 9T and Xiaomi Redmi Note 10 Pro smartphones.

The current implementation only supports Normandy and Yellowstone type
ICs, aka only GT9886 and GT9896. It is also limited to I2C only, since I
don't have a device with GTX8 over SPI at hand. Adding support for SPI
should be fairly easy in the future, since the code uses a regmap.

Support for advanced features like:
- Firmware updates
- Stylus events
- Gesture events
- Nanjing IC support
is not included in current version.

The current support requires a previously flashed firmware to be
present.

As I did not have access to datasheets for these ICs, I extracted the
addresses from a couple of config files using a small tool [2]. The
addresses are identical for the same IC families in all configs I
observed, however not all of them make sense and I stubbed out firmware
request support due to this.

[1] https://github.com/goodix/gtx8_driver_linux
[2] https://github.com/sm7150-mainline/goodix-cfg-bin

Tested-by: Piyush Raj Chouhan <pc1598@mainlining.org>
Tested-by: Alexander Koskovich <AKoskovich@pm.me>
Signed-off-by: Aelin Reidel <aelin@mainlining.org>
---
 drivers/input/touchscreen/Kconfig       |  15 +
 drivers/input/touchscreen/Makefile      |   1 +
 drivers/input/touchscreen/goodix_gtx8.c | 563 ++++++++++++++++++++++++++++++++
 drivers/input/touchscreen/goodix_gtx8.h | 141 ++++++++
 4 files changed, 720 insertions(+)

diff --git a/drivers/input/touchscreen/Kconfig b/drivers/input/touchscreen/Kconfig
index 7d5b72ee07fa1313da39a625b5129a0459720865..099ccd3679383dcf037bc7c6e6a3dbf0741722b4 100644
--- a/drivers/input/touchscreen/Kconfig
+++ b/drivers/input/touchscreen/Kconfig
@@ -429,6 +429,21 @@ config TOUCHSCREEN_GOODIX_BERLIN_SPI
 	  To compile this driver as a module, choose M here: the
 	  module will be called goodix_berlin_spi.
 
+config TOUCHSCREEN_GOODIX_GTX8
+	tristate "Goodix GTX8 touchscreen"
+	depends on I2C
+	select REGMAP_I2C
+	help
+	  Say Y here if you have a Goodix GTX8 IC connected to
+	  your system via I2C. This driver supports Normandy and
+	  Yellowstone ICs like the GT9886 and GT9896.
+	  They are commonly found in mobile phones.
+
+	  if unsure, say N.
+
+	  To compile this driver as a module, choose M here: the
+	  module will be called goodix_gtx8.
+
 config TOUCHSCREEN_HIDEEP
 	tristate "HiDeep Touch IC"
 	depends on I2C
diff --git a/drivers/input/touchscreen/Makefile b/drivers/input/touchscreen/Makefile
index ab9abd151078831a4b22d6998e00ef74fe01c356..9bcb8f01ea785dcbe2a22bd3293601dd4259ba1d 100644
--- a/drivers/input/touchscreen/Makefile
+++ b/drivers/input/touchscreen/Makefile
@@ -48,6 +48,7 @@ obj-$(CONFIG_TOUCHSCREEN_GOODIX)	+= goodix_ts.o
 obj-$(CONFIG_TOUCHSCREEN_GOODIX_BERLIN_CORE)	+= goodix_berlin_core.o
 obj-$(CONFIG_TOUCHSCREEN_GOODIX_BERLIN_I2C)	+= goodix_berlin_i2c.o
 obj-$(CONFIG_TOUCHSCREEN_GOODIX_BERLIN_SPI)	+= goodix_berlin_spi.o
+obj-$(CONFIG_TOUCHSCREEN_GOODIX_GTX8)	+= goodix_gtx8.o
 obj-$(CONFIG_TOUCHSCREEN_HIDEEP)	+= hideep.o
 obj-$(CONFIG_TOUCHSCREEN_HIMAX_HX852X)	+= himax_hx852x.o
 obj-$(CONFIG_TOUCHSCREEN_HYNITRON_CSTXXX)	+= hynitron_cstxxx.o
diff --git a/drivers/input/touchscreen/goodix_gtx8.c b/drivers/input/touchscreen/goodix_gtx8.c
new file mode 100644
index 0000000000000000000000000000000000000000..3fbebfa8b75672866788adf89d83be16e32abf81
--- /dev/null
+++ b/drivers/input/touchscreen/goodix_gtx8.c
@@ -0,0 +1,563 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Driver for Goodix GTX8 Touchscreens
+ *
+ * Copyright (c) 2019 - 2020 Goodix, Inc.
+ * Copyright (C) 2023 Linaro Ltd.
+ * Copyright (c) 2025 Aelin Reidel <aelin@mainlining.org>
+ *
+ * Based on gtx8_driver_linux vendor driver and goodix_berlin kernel driver.
+ *
+ * The driver currently relies on the pre-flashed firmware and only supports
+ * Normandy / Yellowstone ICs.
+ * Pen support is also missing.
+ */
+#include <linux/bitfield.h>
+#include <linux/gpio/consumer.h>
+#include <linux/i2c.h>
+#include <linux/input.h>
+#include <linux/input/mt.h>
+#include <linux/input/touchscreen.h>
+#include <linux/module.h>
+#include <linux/regmap.h>
+#include <linux/unaligned.h>
+
+#include "goodix_gtx8.h"
+
+static const struct regmap_config goodix_gtx8_regmap_conf = {
+	.reg_bits = 16,
+	.val_bits = 8,
+	.max_raw_read = I2C_MAX_TRANSFER_SIZE,
+	.max_raw_write = I2C_MAX_TRANSFER_SIZE,
+};
+
+/* vendor & product left unassigned here, should probably be updated from fw info */
+static const struct input_id goodix_gtx8_input_id = {
+	.bustype = BUS_I2C,
+};
+
+static bool goodix_gtx8_checksum_valid_normandy(const u8 *data, int size)
+{
+	u8 cal_checksum = 0;
+	int i;
+
+	if (size < GOODIX_GTX8_CHECKSUM_SIZE)
+		return false;
+
+	for (i = 0; i < size; i++)
+		cal_checksum += data[i];
+
+	return cal_checksum == 0;
+}
+
+static bool goodix_gtx8_checksum_valid_yellowstone(const u8 *data, int size)
+{
+	u16 cal_checksum = 0;
+	u16 r_checksum;
+	int i;
+
+	if (size < GOODIX_GTX8_CHECKSUM_SIZE)
+		return false;
+
+	for (i = 0; i < size - GOODIX_GTX8_CHECKSUM_SIZE; i++)
+		cal_checksum += data[i];
+
+	r_checksum = get_unaligned_be16(&data[i]);
+
+	return cal_checksum == r_checksum;
+}
+
+static int goodix_gtx8_get_remaining_contacts(struct goodix_gtx8_core *cd,
+					      int n)
+{
+	size_t offset = cd->ic_data->header_size + GOODIX_GTX8_TOUCH_SIZE +
+			GOODIX_GTX8_CHECKSUM_SIZE;
+	u32 addr = cd->ic_data->touch_data_addr + offset;
+	int error;
+
+	error = regmap_raw_read(cd->regmap, addr, &cd->event_buffer[offset],
+				(n - 1) * GOODIX_GTX8_TOUCH_SIZE);
+	if (error) {
+		dev_err_ratelimited(cd->dev, "failed to get touch data, %d\n",
+				    error);
+		return error;
+	}
+
+	return 0;
+}
+
+static void goodix_gtx8_report_state(struct goodix_gtx8_core *cd, u8 touch_num,
+				     union goodix_gtx8_touch *touch_data)
+{
+	union goodix_gtx8_touch *t;
+	int i;
+	u8 finger_id;
+
+	for (i = 0; i < touch_num; i++) {
+		t = &touch_data[i];
+
+		if (cd->ic_data->ic_type == IC_TYPE_NORMANDY) {
+			input_mt_slot(cd->input_dev, t->normandy.finger_id);
+			input_mt_report_slot_state(cd->input_dev,
+						   MT_TOOL_FINGER, true);
+
+			touchscreen_report_pos(cd->input_dev, &cd->props,
+					       __le16_to_cpu(t->normandy.x),
+					       __le16_to_cpu(t->normandy.y),
+					       true);
+			input_report_abs(cd->input_dev, ABS_MT_TOUCH_MAJOR,
+					 t->normandy.w);
+		} else {
+			finger_id = FIELD_GET(
+				GOODIX_GTX8_FINGER_ID_MASK_YELLOWSTONE,
+				t->yellowstone.finger_id);
+			input_mt_slot(cd->input_dev, finger_id);
+			input_mt_report_slot_state(cd->input_dev,
+						   MT_TOOL_FINGER, true);
+
+			touchscreen_report_pos(cd->input_dev, &cd->props,
+					       __be16_to_cpu(t->yellowstone.x),
+					       __be16_to_cpu(t->yellowstone.y),
+					       true);
+			input_report_abs(cd->input_dev, ABS_MT_TOUCH_MAJOR,
+					 t->yellowstone.w);
+		}
+	}
+
+	input_mt_sync_frame(cd->input_dev);
+	input_sync(cd->input_dev);
+}
+
+static void goodix_gtx8_touch_handler(struct goodix_gtx8_core *cd, u8 touch_num,
+				      union goodix_gtx8_touch *touch_data)
+{
+	int error;
+
+	touch_num = FIELD_GET(GOODIX_GTX8_TOUCH_COUNT_MASK, touch_num);
+
+	if (touch_num > GOODIX_GTX8_MAX_TOUCH) {
+		dev_warn(cd->dev, "invalid touch num %d\n", touch_num);
+		return;
+	}
+
+	if (touch_num > 1) {
+		/* read additional contact data if more than 1 touch event */
+		error = goodix_gtx8_get_remaining_contacts(cd, touch_num);
+		if (error)
+			return;
+	}
+
+	if (touch_num) {
+		/*
+		 * Normandy checksum is for the entire read buffer,
+		 * Yellowstone is only for the touch data (since header
+		 * has a separate checksum)
+		 */
+		if (cd->ic_data->ic_type == IC_TYPE_NORMANDY) {
+			int len = GOODIX_GTX8_HEADER_SIZE_NORMANDY +
+				  touch_num * GOODIX_GTX8_TOUCH_SIZE +
+				  GOODIX_GTX8_CHECKSUM_SIZE;
+			if (!goodix_gtx8_checksum_valid_normandy(
+				    cd->event_buffer, len)) {
+				dev_err(cd->dev,
+					"touch data checksum error: %*ph\n",
+					len, cd->event_buffer);
+				return;
+			}
+		} else {
+			int len = touch_num * GOODIX_GTX8_TOUCH_SIZE +
+				  GOODIX_GTX8_CHECKSUM_SIZE;
+			if (!goodix_gtx8_checksum_valid_yellowstone(
+				    (u8 *)touch_data, len)) {
+				dev_err(cd->dev,
+					"touch data checksum error: %*ph\n",
+					len, (u8 *)touch_data);
+				return;
+			}
+		}
+	}
+
+	goodix_gtx8_report_state(cd, touch_num, touch_data);
+}
+
+static irqreturn_t goodix_gtx8_irq(int irq, void *data)
+{
+	struct goodix_gtx8_core *cd = data;
+	struct goodix_gtx8_event_normandy *ev_normandy;
+	struct goodix_gtx8_event_yellowstone *ev_yellowstone;
+	union goodix_gtx8_touch *touch_data;
+	int error;
+	u8 status, touch_num;
+
+	error = regmap_raw_read(
+		cd->regmap, cd->ic_data->touch_data_addr, cd->event_buffer,
+		cd->ic_data->header_size + GOODIX_GTX8_TOUCH_SIZE +
+			GOODIX_GTX8_CHECKSUM_SIZE);
+	if (error) {
+		dev_warn_ratelimited(
+			cd->dev, "failed to get event head data: %d\n", error);
+		goto out;
+	}
+
+	/*
+	 * Both IC types have the same data in the header, just at different
+	 * offsets
+	 */
+	if (cd->ic_data->ic_type == IC_TYPE_NORMANDY) {
+		ev_normandy =
+			(struct goodix_gtx8_event_normandy *)cd->event_buffer;
+		status = ev_normandy->hdr.status;
+		touch_num = ev_normandy->hdr.touch_num;
+		touch_data = (union goodix_gtx8_touch *)ev_normandy->data;
+	} else {
+		ev_yellowstone = (struct goodix_gtx8_event_yellowstone *)
+					 cd->event_buffer;
+		status = ev_yellowstone->hdr.status;
+		touch_num = ev_yellowstone->hdr.touch_num;
+		touch_data = (union goodix_gtx8_touch *)ev_yellowstone->data;
+	}
+
+	if (status == 0)
+		goto out;
+
+	/* Yellowstone ICs have a checksum for the header */
+	if (cd->ic_data->ic_type == IC_TYPE_YELLOWSTONE &&
+	    !goodix_gtx8_checksum_valid_yellowstone(
+		    cd->event_buffer, GOODIX_GTX8_HEADER_SIZE_YELLOWSTONE)) {
+		dev_warn_ratelimited(cd->dev,
+				     "touch head checksum error: %*ph\n",
+				     (int)GOODIX_GTX8_HEADER_SIZE_YELLOWSTONE,
+				     cd->event_buffer);
+		goto out_clear;
+	}
+
+	if (status & GOODIX_GTX8_TOUCH_EVENT)
+		goodix_gtx8_touch_handler(cd, touch_num, touch_data);
+
+	if (status & GOODIX_GTX8_REQUEST_EVENT) {
+		/*
+		 * All configs seen so far either set the firmware request
+		 * address to 0 (Normandy) or have it equal the touch data
+		 * address (Yellowstone). Neither seems correct, and this
+		 * is not testable. Therefore it is currently omitted.
+		 */
+		dev_dbg(cd->dev, "received request event, ignoring\n");
+	}
+
+out_clear:
+	/* Clear up status field */
+	regmap_write(cd->regmap, cd->ic_data->touch_data_addr, 0);
+
+out:
+	return IRQ_HANDLED;
+}
+
+static int goodix_gtx8_input_dev_config(struct goodix_gtx8_core *cd)
+{
+	struct input_dev *input_dev;
+	int error;
+
+	input_dev = devm_input_allocate_device(cd->dev);
+	if (!input_dev)
+		return -ENOMEM;
+
+	cd->input_dev = input_dev;
+	input_set_drvdata(input_dev, cd);
+
+	input_dev->name = "Goodix GTX8 Capacitive TouchScreen";
+	input_dev->phys = "input/ts";
+
+	input_dev->id = goodix_gtx8_input_id;
+
+	input_set_abs_params(cd->input_dev, ABS_MT_POSITION_X, 0, SZ_64K - 1, 0,
+			     0);
+	input_set_abs_params(cd->input_dev, ABS_MT_POSITION_Y, 0, SZ_64K - 1, 0,
+			     0);
+	input_set_abs_params(cd->input_dev, ABS_MT_TOUCH_MAJOR, 0, 255, 0, 0);
+
+	touchscreen_parse_properties(cd->input_dev, true, &cd->props);
+
+	error = input_mt_init_slots(cd->input_dev, GOODIX_GTX8_MAX_TOUCH,
+				    INPUT_MT_DIRECT | INPUT_MT_DROP_UNUSED);
+	if (error)
+		return error;
+
+	error = input_register_device(cd->input_dev);
+	if (error)
+		return error;
+
+	return 0;
+}
+
+static int goodix_gtx8_read_version(struct goodix_gtx8_core *cd)
+{
+	int error;
+
+	/*
+	 * The vendor driver reads a whole lot more data to calculate and
+	 * verify a checksum. Without documentation, we don't know what
+	 * most of that data is, so we only read the parts we know about
+	 * and instead ensure their values are as expected
+	 */
+	error = regmap_raw_read(cd->regmap, cd->ic_data->fw_version_addr,
+				&cd->fw_version, sizeof(cd->fw_version));
+	if (error) {
+		dev_err(cd->dev, "error reading fw version, %d\n", error);
+		return error;
+	}
+
+	/*
+	 * Since we don't verify the checksum, do a basic check that the
+	 * product ID meets expectations
+	 */
+	if (memcmp(cd->fw_version.product_id, cd->ic_data->product_id,
+		   sizeof(cd->fw_version.product_id))) {
+		dev_err(cd->dev, "unexpected product ID, got: %c%c%c%c\n",
+			cd->fw_version.product_id[0],
+			cd->fw_version.product_id[1],
+			cd->fw_version.product_id[2],
+			cd->fw_version.product_id[3]);
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+static int goodix_gtx8_dev_confirm(struct goodix_gtx8_core *cd)
+{
+	u8 rx_buf[1];
+	int retry = 3;
+	int error;
+
+	while (retry--) {
+		/*
+		 * test_addr appears to always be the touch_data_addr for
+		 * Normandy, but it doesn't really matter since all we
+		 * need is a valid address
+		 */
+		error = regmap_raw_read(cd->regmap,
+					cd->ic_data->touch_data_addr, rx_buf,
+					sizeof(rx_buf));
+
+		if (!error)
+			return 0;
+
+		usleep_range(5000, 5100);
+	}
+
+	dev_err(cd->dev, "device confirm failed\n");
+
+	return -EINVAL;
+}
+
+static int goodix_gtx8_power_on(struct goodix_gtx8_core *cd)
+{
+	int error;
+
+	error = regulator_enable(cd->vddio);
+	if (error) {
+		dev_err(cd->dev, "Failed to enable VDDIO: %d\n", error);
+		return error;
+	}
+
+	error = regulator_enable(cd->avdd);
+	if (error) {
+		dev_err(cd->dev, "Failed to enable AVDD: %d\n", error);
+		goto err_vddio_disable;
+	}
+
+	/* Vendors usually configure the power on delay as 300ms */
+	msleep(GOODIX_GTX8_POWER_ON_DELAY_MS);
+
+	gpiod_set_value_cansleep(cd->reset_gpio, 0);
+
+	/* Vendor waits 5ms for firmware to initialize */
+	usleep_range(5000, 5100);
+
+	error = goodix_gtx8_dev_confirm(cd);
+	if (error)
+		goto err_dev_reset;
+
+	/* Vendor waits 100ms for firmware to fully boot */
+	msleep(GOODIX_GTX8_NORMAL_RESET_DELAY_MS);
+
+	return 0;
+
+err_dev_reset:
+	gpiod_set_value_cansleep(cd->reset_gpio, 1);
+	regulator_disable(cd->avdd);
+err_vddio_disable:
+	regulator_disable(cd->vddio);
+	return error;
+}
+
+static void goodix_gtx8_power_off(struct goodix_gtx8_core *cd)
+{
+	gpiod_set_value_cansleep(cd->reset_gpio, 1);
+	regulator_disable(cd->avdd);
+	regulator_disable(cd->vddio);
+}
+
+static int goodix_gtx8_suspend(struct device *dev)
+{
+	struct goodix_gtx8_core *cd = dev_get_drvdata(dev);
+
+	disable_irq(cd->irq);
+	goodix_gtx8_power_off(cd);
+
+	return 0;
+}
+
+static int goodix_gtx8_resume(struct device *dev)
+{
+	struct goodix_gtx8_core *cd = dev_get_drvdata(dev);
+	int error;
+
+	error = goodix_gtx8_power_on(cd);
+	if (error)
+		return error;
+
+	enable_irq(cd->irq);
+
+	return 0;
+}
+
+EXPORT_GPL_SIMPLE_DEV_PM_OPS(goodix_gtx8_pm_ops, goodix_gtx8_suspend,
+			     goodix_gtx8_resume);
+
+static void goodix_gtx8_power_off_act(void *data)
+{
+	struct goodix_gtx8_core *cd = data;
+
+	goodix_gtx8_power_off(cd);
+}
+
+static int goodix_gtx8_probe(struct i2c_client *client)
+{
+	struct goodix_gtx8_core *cd;
+	struct regmap *regmap;
+	int error;
+
+	cd = devm_kzalloc(&client->dev, sizeof(*cd), GFP_KERNEL);
+	if (!cd)
+		return -ENOMEM;
+
+	regmap = devm_regmap_init_i2c(client, &goodix_gtx8_regmap_conf);
+	if (IS_ERR(regmap))
+		return PTR_ERR(regmap);
+
+	cd->dev = &client->dev;
+	cd->irq = client->irq;
+	cd->regmap = regmap;
+	cd->ic_data = i2c_get_match_data(client);
+
+	cd->event_buffer =
+		devm_kzalloc(cd->dev, cd->ic_data->event_size, GFP_KERNEL);
+	if (!cd->event_buffer)
+		return -ENOMEM;
+
+	cd->reset_gpio =
+		devm_gpiod_get_optional(cd->dev, "reset", GPIOD_OUT_HIGH);
+	if (IS_ERR(cd->reset_gpio))
+		return dev_err_probe(cd->dev, PTR_ERR(cd->reset_gpio),
+				     "Failed to request reset GPIO\n");
+
+	cd->avdd = devm_regulator_get(cd->dev, "avdd");
+	if (IS_ERR(cd->avdd))
+		return dev_err_probe(cd->dev, PTR_ERR(cd->avdd),
+				     "Failed to request AVDD regulator\n");
+
+	cd->vddio = devm_regulator_get(cd->dev, "vddio");
+	if (IS_ERR(cd->vddio))
+		return dev_err_probe(cd->dev, PTR_ERR(cd->vddio),
+				     "Failed to request VDDIO regulator\n");
+
+	error = goodix_gtx8_power_on(cd);
+	if (error) {
+		dev_err(cd->dev, "failed power on");
+		return error;
+	}
+
+	error = devm_add_action_or_reset(cd->dev, goodix_gtx8_power_off_act,
+					 cd);
+	if (error)
+		return error;
+
+	error = goodix_gtx8_read_version(cd);
+	if (error) {
+		dev_err(cd->dev, "failed to get version info");
+		return error;
+	}
+
+	error = goodix_gtx8_input_dev_config(cd);
+	if (error) {
+		dev_err(cd->dev, "failed to set input device");
+		return error;
+	}
+
+	error = devm_request_threaded_irq(cd->dev, cd->irq, NULL,
+					  goodix_gtx8_irq, IRQF_ONESHOT,
+					  "goodix-gtx8", cd);
+	if (error) {
+		dev_err(cd->dev, "request threaded IRQ failed: %d\n", error);
+		return error;
+	}
+
+	dev_set_drvdata(cd->dev, cd);
+
+	dev_dbg(cd->dev,
+		"Goodix GT%c%c%c%c Touchscreen Controller, Version %d.%d.%d.%d\n",
+		cd->fw_version.product_id[0], cd->fw_version.product_id[1],
+		cd->fw_version.product_id[2], cd->fw_version.product_id[3],
+		cd->fw_version.fw_version[0], cd->fw_version.fw_version[1],
+		cd->fw_version.fw_version[2], cd->fw_version.fw_version[3]);
+
+	return 0;
+}
+
+static const struct goodix_gtx8_ic_data gt9886_data = {
+	.event_size = GOODIX_GTX8_EVENT_SIZE_NORMANDY,
+	.fw_version_addr = GOODIX_GTX8_FW_VERSION_ADDR_NORMANDY,
+	.header_size = GOODIX_GTX8_HEADER_SIZE_NORMANDY,
+	.ic_type = IC_TYPE_NORMANDY,
+	.product_id = { '9', '8', '8', '6' },
+	.touch_data_addr = GOODIX_GTX8_TOUCH_DATA_ADDR_NORMANDY,
+};
+
+static const struct goodix_gtx8_ic_data gt9896_data = {
+	.event_size = GOODIX_GTX8_EVENT_SIZE_YELLOWSTONE,
+	.fw_version_addr = GOODIX_GTX8_FW_VERSION_ADDR_YELLOWSTONE,
+	.header_size = GOODIX_GTX8_HEADER_SIZE_YELLOWSTONE,
+	.ic_type = IC_TYPE_YELLOWSTONE,
+	.product_id = { '9', '8', '9', '6' },
+	.touch_data_addr = GOODIX_GTX8_TOUCH_DATA_ADDR_YELLOWSTONE,
+};
+
+static const struct i2c_device_id goodix_gtx8_i2c_id[] = {
+	{ .name = "gt9886", .driver_data = (long)&gt9886_data },
+	{ .name = "gt9896", .driver_data = (long)&gt9896_data },
+	{},
+};
+MODULE_DEVICE_TABLE(i2c, goodix_gtx8_i2c_id);
+
+static const struct of_device_id goodix_gtx8_of_match[] = {
+	{ .compatible = "goodix,gt9886", .data = &gt9886_data },
+	{ .compatible = "goodix,gt9896", .data = &gt9896_data },
+	{},
+};
+MODULE_DEVICE_TABLE(of, goodix_gtx8_of_match);
+
+static struct i2c_driver goodix_gtx8_driver = {
+	.probe = goodix_gtx8_probe,
+	.id_table = goodix_gtx8_i2c_id,
+	.driver = {
+		.name = "goodix-gtx8",
+		.of_match_table = of_match_ptr(goodix_gtx8_of_match),
+		.pm = pm_sleep_ptr(&goodix_gtx8_pm_ops),
+	},
+};
+module_i2c_driver(goodix_gtx8_driver);
+
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("Goodix GTX8 Touchscreen driver");
+MODULE_AUTHOR("Aelin Reidel <aelin@mainlining.org>");
diff --git a/drivers/input/touchscreen/goodix_gtx8.h b/drivers/input/touchscreen/goodix_gtx8.h
new file mode 100644
index 0000000000000000000000000000000000000000..9b13cfce38720b35bb11f0d7f56d671b31664ade
--- /dev/null
+++ b/drivers/input/touchscreen/goodix_gtx8.h
@@ -0,0 +1,141 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef __GOODIX_GTX8_H__
+#define __GOODIX_GTX8_H__
+
+#include <linux/pm.h>
+
+#define GOODIX_GTX8_NORMAL_RESET_DELAY_MS	100
+#define GOODIX_GTX8_POWER_ON_DELAY_MS		300
+
+#define GOODIX_GTX8_TOUCH_EVENT			BIT(7)
+#define GOODIX_GTX8_REQUEST_EVENT		BIT(6)
+#define GOODIX_GTX8_TOUCH_COUNT_MASK		GENMASK(3, 0)
+#define GOODIX_GTX8_FINGER_ID_MASK_YELLOWSTONE	GENMASK(7, 4)
+
+#define GOODIX_GTX8_MAX_TOUCH			10
+#define GOODIX_GTX8_CHECKSUM_SIZE		sizeof(u16)
+
+#define GOODIX_GTX8_FW_VERSION_ADDR_NORMANDY	0x4535
+#define GOODIX_GTX8_FW_VERSION_ADDR_YELLOWSTONE	0x4022
+#define GOODIX_GTX8_TOUCH_DATA_ADDR_NORMANDY	0x4100
+#define GOODIX_GTX8_TOUCH_DATA_ADDR_YELLOWSTONE	0x4180
+
+#define I2C_MAX_TRANSFER_SIZE			256
+
+enum goodix_gtx8_ic_type {
+	IC_TYPE_NORMANDY,
+	IC_TYPE_YELLOWSTONE,
+};
+
+struct goodix_gtx8_ic_data {
+	size_t event_size;
+	/*
+	 * This is technically not the firmware version address
+	 * referenced in the vendor driver, but rather the
+	 * address of the product ID part. The meaning of the
+	 * other parts is unknown and they are therefore omitted
+	 * for now.
+	 */
+	int fw_version_addr;
+	size_t header_size;
+	enum goodix_gtx8_ic_type ic_type;
+	char product_id[4];
+	int touch_data_addr;
+};
+
+struct goodix_gtx8_header_normandy {
+	u8 status;
+	/* Only the lower 4 bits are actually used */
+	u8 touch_num;
+};
+#define GOODIX_GTX8_HEADER_SIZE_NORMANDY \
+	sizeof(struct goodix_gtx8_header_normandy)
+
+struct goodix_gtx8_header_yellowstone {
+	u8 status;
+	/* Most likely unused */
+	u8 __unknown1;
+	/* Only the lower 4 bits are actually used */
+	u8 touch_num;
+	/* Most likely unused */
+	u8 __unknown2[3];
+	__le16 checksum;
+} __packed __aligned(1);
+#define GOODIX_GTX8_HEADER_SIZE_YELLOWSTONE \
+	sizeof(struct goodix_gtx8_header_yellowstone)
+
+struct goodix_gtx8_touch_normandy {
+	u8 finger_id;
+	__le16 x;
+	__le16 y;
+	u8 w;
+	u8 __unknown[2];
+} __packed __aligned(1);
+
+struct goodix_gtx8_touch_yellowstone {
+	/*
+	 * Only the upper 4 bits are used, lower 4 bits are
+	 * probably the sensor ID.
+	 */
+	u8 finger_id;
+	u8 __unknown1;
+	__be16 x;
+	__be16 y;
+	/*
+	 * Vendor driver claims that this is a single __be16,
+	 * but testing shows that it likely isn't.
+	 */
+	u8 __unknown2;
+	u8 w;
+} __packed __aligned(1);
+
+union goodix_gtx8_touch {
+	struct goodix_gtx8_touch_normandy normandy;
+	struct goodix_gtx8_touch_yellowstone yellowstone;
+};
+#define GOODIX_GTX8_TOUCH_SIZE		sizeof(union goodix_gtx8_touch)
+
+struct goodix_gtx8_event_normandy {
+	struct goodix_gtx8_header_normandy hdr;
+	/* The data below is u16 aligned */
+	u8 data[GOODIX_GTX8_TOUCH_SIZE * GOODIX_GTX8_MAX_TOUCH +
+		GOODIX_GTX8_CHECKSUM_SIZE];
+};
+#define GOODIX_GTX8_EVENT_SIZE_NORMANDY \
+	sizeof(struct goodix_gtx8_event_normandy)
+
+struct goodix_gtx8_event_yellowstone {
+	struct goodix_gtx8_header_yellowstone hdr;
+	/* The data below is u16 aligned */
+	u8 data[GOODIX_GTX8_TOUCH_SIZE * GOODIX_GTX8_MAX_TOUCH +
+		GOODIX_GTX8_CHECKSUM_SIZE];
+};
+#define GOODIX_GTX8_EVENT_SIZE_YELLOWSTONE \
+	sizeof(struct goodix_gtx8_event_yellowstone)
+
+struct goodix_gtx8_fw_version {
+	/* 4 digits IC number */
+	char product_id[4];
+	/* Most likely unused */
+	u8 __unknown[4];
+	/* Four components version number */
+	u8 fw_version[4];
+};
+
+struct goodix_gtx8_core {
+	struct device *dev;
+	struct regmap *regmap;
+	struct regulator *avdd;
+	struct regulator *vddio;
+	struct gpio_desc *reset_gpio;
+	struct touchscreen_properties props;
+	struct goodix_gtx8_fw_version fw_version;
+	struct input_dev *input_dev;
+	int irq;
+	const struct goodix_gtx8_ic_data *ic_data;
+	u8 *event_buffer;
+};
+
+extern const struct dev_pm_ops goodix_gtx8_pm_ops;
+
+#endif

-- 
2.53.0


^ permalink raw reply related

* [PATCH] HID: hid-lenovo-go: Remove unneeded semicolon
From: Chen Ni @ 2026-02-28  3:39 UTC (permalink / raw)
  To: derekjohn.clark
  Cc: mpearson-lenovo, jikos, bentiss, linux-input, linux-kernel,
	Chen Ni

Remove unnecessary semicolons after switch statements and function
bodies. Most issues were reported by Coccinelle/coccicheck using the
semantic patch at scripts/coccinelle/misc/semicolon.cocci. Additional
instances found during manual code review were also fixed.

Signed-off-by: Chen Ni <nichen@iscas.ac.cn>
---
 drivers/hid/hid-lenovo-go.c | 52 ++++++++++++++++++-------------------
 1 file changed, 26 insertions(+), 26 deletions(-)

diff --git a/drivers/hid/hid-lenovo-go.c b/drivers/hid/hid-lenovo-go.c
index 6972d13802e2..77e3823447e5 100644
--- a/drivers/hid/hid-lenovo-go.c
+++ b/drivers/hid/hid-lenovo-go.c
@@ -455,7 +455,7 @@ static int hid_go_feature_status_event(struct command_report *cmd_rep)
 			return 0;
 		default:
 			return -EINVAL;
-		};
+		}
 	case FEATURE_IMU_BYPASS:
 		switch (cmd_rep->device_type) {
 		case LEFT_CONTROLLER:
@@ -466,7 +466,7 @@ static int hid_go_feature_status_event(struct command_report *cmd_rep)
 			return 0;
 		default:
 			return -EINVAL;
-		};
+		}
 		break;
 	case FEATURE_LIGHT_ENABLE:
 		drvdata.rgb_en = cmd_rep->data[0];
@@ -481,7 +481,7 @@ static int hid_go_feature_status_event(struct command_report *cmd_rep)
 			return 0;
 		default:
 			return -EINVAL;
-		};
+		}
 		break;
 	case FEATURE_TOUCHPAD_ENABLE:
 		drvdata.tp_en = cmd_rep->data[0];
@@ -515,7 +515,7 @@ static int hid_go_motor_event(struct command_report *cmd_rep)
 			return 0;
 		default:
 			return -EINVAL;
-		};
+		}
 		break;
 	case RUMBLE_MODE:
 		switch (cmd_rep->device_type) {
@@ -527,7 +527,7 @@ static int hid_go_motor_event(struct command_report *cmd_rep)
 			return 0;
 		default:
 			return -EINVAL;
-		};
+		}
 	case TP_VIBRATION_ENABLE:
 		drvdata.tp_vibration_en = cmd_rep->data[0];
 		return 0;
@@ -625,7 +625,7 @@ static int hid_go_os_mode_cfg_event(struct command_report *cmd_rep)
 		return 0;
 	default:
 		return -EINVAL;
-	};
+	}
 }
 
 static int hid_go_set_event_return(struct command_report *cmd_rep)
@@ -699,14 +699,14 @@ static int hid_go_raw_event(struct hid_device *hdev, struct hid_report *report,
 		default:
 			ret = -EINVAL;
 			break;
-		};
+		}
 		break;
 	case OS_MODE_DATA:
 		ret = hid_go_os_mode_cfg_event(cmd_rep);
 		break;
 	default:
 		goto passthrough;
-	};
+	}
 	dev_dbg(&hdev->dev, "Rx data as raw input report: [%*ph]\n",
 		GO_PACKET_SIZE, data);
 
@@ -925,7 +925,7 @@ static ssize_t feature_status_store(struct device *dev,
 		break;
 	default:
 		return -EINVAL;
-	};
+	}
 
 	if (ret < 0)
 		return ret;
@@ -1013,7 +1013,7 @@ static ssize_t feature_status_show(struct device *dev,
 			break;
 		default:
 			return -EINVAL;
-		};
+		}
 		count = sysfs_emit(buf, "%u\n", i);
 		break;
 	case FEATURE_FPS_SWITCH_STATUS:
@@ -1032,7 +1032,7 @@ static ssize_t feature_status_show(struct device *dev,
 		break;
 	default:
 		return -EINVAL;
-	};
+	}
 
 	return count;
 }
@@ -1070,7 +1070,7 @@ static ssize_t feature_status_options(struct device *dev,
 		break;
 	default:
 		return -EINVAL;
-	};
+	}
 
 	if (count)
 		buf[count - 1] = '\n';
@@ -1111,7 +1111,7 @@ static ssize_t motor_config_store(struct device *dev,
 		ret = sysfs_match_string(intensity_text, buf);
 		val = ret;
 		break;
-	};
+	}
 
 	if (ret < 0)
 		return ret;
@@ -1161,7 +1161,7 @@ static ssize_t motor_config_show(struct device *dev,
 			break;
 		default:
 			return -EINVAL;
-		};
+		}
 		if (i >= ARRAY_SIZE(enabled_status_text))
 			return -EINVAL;
 
@@ -1177,7 +1177,7 @@ static ssize_t motor_config_show(struct device *dev,
 			break;
 		default:
 			return -EINVAL;
-		};
+		}
 		if (i >= ARRAY_SIZE(rumble_mode_text))
 			return -EINVAL;
 
@@ -1197,7 +1197,7 @@ static ssize_t motor_config_show(struct device *dev,
 
 		count = sysfs_emit(buf, "%s\n", intensity_text[i]);
 		break;
-	};
+	}
 
 	return count;
 }
@@ -1232,7 +1232,7 @@ static ssize_t motor_config_options(struct device *dev,
 					       enabled_status_text[i]);
 		}
 		break;
-	};
+	}
 
 	if (count)
 		buf[count - 1] = '\n';
@@ -1333,7 +1333,7 @@ static ssize_t device_status_show(struct device *dev,
 		break;
 	default:
 		return -EINVAL;
-	};
+	}
 
 	if (i >= ARRAY_SIZE(cal_status_text))
 		return -EINVAL;
@@ -1459,7 +1459,7 @@ static int rgb_attr_show(void)
 	index = drvdata.rgb_profile + 3;
 
 	return rgb_cfg_call(drvdata.hdev, GET_RGB_CFG, index, 0, 0);
-};
+}
 
 static ssize_t rgb_effect_store(struct device *dev,
 				struct device_attribute *attr, const char *buf,
@@ -1489,7 +1489,7 @@ static ssize_t rgb_effect_store(struct device *dev,
 
 	drvdata.rgb_effect = effect;
 	return count;
-};
+}
 
 static ssize_t rgb_effect_show(struct device *dev,
 			       struct device_attribute *attr, char *buf)
@@ -1552,7 +1552,7 @@ static ssize_t rgb_speed_store(struct device *dev,
 	drvdata.rgb_speed = val;
 
 	return count;
-};
+}
 
 static ssize_t rgb_speed_show(struct device *dev, struct device_attribute *attr,
 			      char *buf)
@@ -1594,7 +1594,7 @@ static ssize_t rgb_mode_store(struct device *dev, struct device_attribute *attr,
 	drvdata.rgb_mode = val;
 
 	return count;
-};
+}
 
 static ssize_t rgb_mode_show(struct device *dev, struct device_attribute *attr,
 			     char *buf)
@@ -1609,7 +1609,7 @@ static ssize_t rgb_mode_show(struct device *dev, struct device_attribute *attr,
 		return -EINVAL;
 
 	return sysfs_emit(buf, "%s\n", rgb_mode_text[drvdata.rgb_mode]);
-};
+}
 
 static ssize_t rgb_mode_index_show(struct device *dev,
 				   struct device_attribute *attr, char *buf)
@@ -1649,7 +1649,7 @@ static ssize_t rgb_profile_store(struct device *dev,
 	drvdata.rgb_profile = val;
 
 	return count;
-};
+}
 
 static ssize_t rgb_profile_show(struct device *dev,
 				struct device_attribute *attr, char *buf)
@@ -1665,7 +1665,7 @@ static ssize_t rgb_profile_show(struct device *dev,
 		return -EINVAL;
 
 	return sysfs_emit(buf, "%hhu\n", drvdata.rgb_profile);
-};
+}
 
 static ssize_t rgb_profile_range_show(struct device *dev,
 				      struct device_attribute *attr, char *buf)
@@ -1704,7 +1704,7 @@ static void hid_go_brightness_set(struct led_classdev *led_cdev,
 		break;
 	default:
 		dev_err(led_cdev->dev, "Failed to write RGB profile: %i\n", ret);
-	};
+	}
 }
 
 #define LEGO_DEVICE_ATTR_RW(_name, _attrname, _dtype, _rtype, _group)         \
-- 
2.25.1


^ permalink raw reply related

* [PATCH] HID: hid-lenovo-go-s: Remove unneeded semicolon
From: Chen Ni @ 2026-02-28  3:43 UTC (permalink / raw)
  To: derekjohn.clark
  Cc: mpearson-lenovo, jikos, bentiss, linux-input, linux-kernel,
	Chen Ni

Remove unnecessary semicolons reported by Coccinelle/coccicheck and the
semantic patch at scripts/coccinelle/misc/semicolon.cocci.

Signed-off-by: Chen Ni <nichen@iscas.ac.cn>
---
 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..d1eb067509f6 100644
--- a/drivers/hid/hid-lenovo-go-s.c
+++ b/drivers/hid/hid-lenovo-go-s.c
@@ -1102,7 +1102,7 @@ static void hid_gos_brightness_set(struct led_classdev *led_cdev,
 	default:
 		dev_err(led_cdev->dev, "Failed to write RGB profile: %i\n",
 			ret);
-	};
+	}
 }
 
 #define LEGOS_DEVICE_ATTR_RW(_name, _attrname, _rtype, _group)                 \
-- 
2.25.1


^ permalink raw reply related

* [PATCH] HID: sony: add support for more instruments
From: Rosalie Wanders @ 2026-02-28  6:05 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Rosalie Wanders, Sanjay Govind, Brenton Simpson, linux-input,
	linux-kernel

This patch adds support for the following instruments:

* Rock Band 1/2/3 Wii/PS3 instruments
* Rock Band 3 PS3 Pro instruments
* DJ Hero Turntable

Wii and PS3 instruments are the same besides the vendor and product ID.

This patch also fixes the mappings for the existing Guitar Hero
instruments.

Co-developed-by: Sanjay Govind <sanjay.govind9@gmail.com>
Signed-off-by: Sanjay Govind <sanjay.govind9@gmail.com>
Co-developed-by: Brenton Simpson <appsforartists@google.com>
Signed-off-by: Brenton Simpson <appsforartists@google.com>
Signed-off-by: Rosalie Wanders <rosalie@mailbox.org>
---
 drivers/hid/hid-ids.h  |  28 ++++-
 drivers/hid/hid-sony.c | 278 ++++++++++++++++++++++++++++++++++-------
 2 files changed, 259 insertions(+), 47 deletions(-)

diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 3e299a30dcde..b0bb34fe000b 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -664,6 +664,19 @@
 #define USB_DEVICE_ID_UGCI_FLYING	0x0020
 #define USB_DEVICE_ID_UGCI_FIGHTING	0x0030
 
+#define USB_VENDOR_ID_HARMONIX		0x1bad
+#define USB_DEVICE_ID_HARMONIX_WII_RB1_GUITAR	0x0004
+#define USB_DEVICE_ID_HARMONIX_WII_RB2_GUITAR	0x3010
+#define USB_DEVICE_ID_HARMONIX_WII_RB1_DRUMS	    0x0005
+#define USB_DEVICE_ID_HARMONIX_WII_RB2_DRUMS	    0x3110
+#define USB_DEVICE_ID_HARMONIX_WII_RB3_MPA_DRUMS_MODE	0x3138
+#define USB_DEVICE_ID_HARMONIX_WII_RB3_MUSTANG_GUITAR	0x3430
+#define USB_DEVICE_ID_HARMONIX_WII_RB3_SQUIRE_GUITAR	0x3530
+#define USB_DEVICE_ID_HARMONIX_WII_RB3_MPA_MUSTANG_MODE	0x3438
+#define USB_DEVICE_ID_HARMONIX_WII_RB3_MPA_SQUIRE_MODE	0x3538
+#define USB_DEVICE_ID_HARMONIX_WII_RB3_KEYBOARD	        0x3330
+#define USB_DEVICE_ID_HARMONIX_WII_RB3_MPA_KEYBOARD_MODE	0x3338
+
 #define USB_VENDOR_ID_HP		0x03f0
 #define USB_PRODUCT_ID_HP_ELITE_PRESENTER_MOUSE_464A		0x464a
 #define USB_PRODUCT_ID_HP_LOGITECH_OEM_USB_OPTICAL_MOUSE_0A4A	0x0a4a
@@ -1298,8 +1311,19 @@
 #define USB_DEVICE_ID_SONY_WIRELESS_BUZZ_CONTROLLER	0x1000
 
 #define USB_VENDOR_ID_SONY_RHYTHM	0x12ba
-#define USB_DEVICE_ID_SONY_PS3WIIU_GHLIVE_DONGLE	0x074b
-#define USB_DEVICE_ID_SONY_PS3_GUITAR_DONGLE	0x0100
+#define USB_DEVICE_ID_SONY_PS3WIIU_GHLIVE	0x074b
+#define USB_DEVICE_ID_SONY_PS3_GH_GUITAR	0x0100
+#define USB_DEVICE_ID_SONY_PS3_GH_DRUMS		0x0120
+#define USB_DEVICE_ID_SONY_PS3_DJH_TURNTABLE	0x0140
+#define USB_DEVICE_ID_SONY_PS3_RB_GUITAR	0x0200
+#define USB_DEVICE_ID_SONY_PS3_RB_DRUMS		0x0210
+#define USB_DEVICE_ID_SONY_PS3_RB3_MPA_DRUMS_MODE	0x0218
+#define USB_DEVICE_ID_SONY_PS3_RB3_MUSTANG_GUITAR	0x2430
+#define USB_DEVICE_ID_SONY_PS3_RB3_SQUIRE_GUITAR	0x2530
+#define USB_DEVICE_ID_SONY_PS3_RB3_MPA_MUSTANG_MODE	0x2438
+#define USB_DEVICE_ID_SONY_PS3_RB3_MPA_SQUIRE_MODE	0x2538
+#define USB_DEVICE_ID_SONY_PS3_RB3_KEYBOARD	        0x2330
+#define USB_DEVICE_ID_SONY_PS3_RB3_MPA_KEYBOARD_MODE	0x2338
 
 #define USB_VENDOR_ID_SINO_LITE			0x1345
 #define USB_DEVICE_ID_SINO_LITE_CONTROLLER	0x3008
diff --git a/drivers/hid/hid-sony.c b/drivers/hid/hid-sony.c
index a89af14e4acc..4b0992cfc8a7 100644
--- a/drivers/hid/hid-sony.c
+++ b/drivers/hid/hid-sony.c
@@ -1,6 +1,6 @@
 // SPDX-License-Identifier: GPL-2.0-or-later
 /*
- *  HID driver for Sony / PS2 / PS3 / PS4 BD devices.
+ *  HID driver for Sony / PS2 / PS3 / PS4 / PS5 BD devices.
  *
  *  Copyright (c) 1999 Andreas Gal
  *  Copyright (c) 2000-2005 Vojtech Pavlik <vojtech@suse.cz>
@@ -12,9 +12,10 @@
  *  Copyright (c) 2014-2016 Frank Praznik <frank.praznik@gmail.com>
  *  Copyright (c) 2018 Todd Kelner
  *  Copyright (c) 2020-2021 Pascal Giard <pascal.giard@etsmtl.ca>
- *  Copyright (c) 2020 Sanjay Govind <sanjay.govind9@gmail.com>
+ *  Copyright (c) 2020-2026 Sanjay Govind <sanjay.govind9@gmail.com>
  *  Copyright (c) 2021 Daniel Nguyen <daniel.nguyen.1@ens.etsmtl.ca>
  *  Copyright (c) 2026 Rosalie Wanders <rosalie@mailbox.org>
+ *  Copyright (c) 2026 Brenton Simpson <appsforartists@google.com>
  */
 
 /*
@@ -59,12 +60,15 @@
 #define NSG_MR5U_REMOTE_BT        BIT(11)
 #define NSG_MR7U_REMOTE_BT        BIT(12)
 #define SHANWAN_GAMEPAD           BIT(13)
-#define GH_GUITAR_CONTROLLER      BIT(14)
-#define GHL_GUITAR_PS3WIIU        BIT(15)
-#define GHL_GUITAR_PS4            BIT(16)
-#define RB4_GUITAR_PS4_USB        BIT(17)
-#define RB4_GUITAR_PS4_BT         BIT(18)
-#define RB4_GUITAR_PS5            BIT(19)
+#define INSTRUMENT                BIT(14)
+#define GH_GUITAR_TILT            BIT(15)
+#define GHL_GUITAR_PS3WIIU        BIT(16)
+#define GHL_GUITAR_PS4            BIT(17)
+#define RB4_GUITAR_PS4_USB        BIT(18)
+#define RB4_GUITAR_PS4_BT         BIT(19)
+#define RB4_GUITAR_PS5            BIT(20)
+#define RB3_PS3_PRO_INSTRUMENT    BIT(21)
+#define PS3_DJH_TURNTABLE		  BIT(22)
 
 #define SIXAXIS_CONTROLLER (SIXAXIS_CONTROLLER_USB | SIXAXIS_CONTROLLER_BT)
 #define MOTION_CONTROLLER (MOTION_CONTROLLER_USB | MOTION_CONTROLLER_BT)
@@ -87,6 +91,10 @@
 #define GHL_GUITAR_POKE_INTERVAL 8 /* In seconds */
 #define GUITAR_TILT_USAGE 44
 
+#define TURNTABLE_EFFECTS_KNOB_USAGE 44
+#define TURNTABLE_PLATTER_BUTTONS_USAGE 45
+#define TURNTABLE_CROSS_FADER_USAGE 46
+
 /* Magic data taken from GHLtarUtility:
  * https://github.com/ghlre/GHLtarUtility/blob/master/PS3Guitar.cs
  * Note: The Wii U and PS3 dongles happen to share the same!
@@ -102,6 +110,13 @@ static const char ghl_ps4_magic_data[] = {
 	0x30, 0x02, 0x08, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00
 };
 
+/* Rock Band 3 PS3 Pro Instruments require sending a report
+ * once an instrument is connected to its dongle.
+ * We need to retry sending these reports,
+ * but to avoid doing this too often we delay the retries
+ */
+#define RB3_PRO_INSTRUMENT_POKE_RETRY_INTERVAL 8 /* In seconds */
+
 /* PS/3 Motion controller */
 static const u8 motion_rdesc[] = {
 	0x05, 0x01,         /*  Usage Page (Desktop),               */
@@ -427,20 +442,25 @@ static const unsigned int rb4_absmap[] = {
 	[0x31] = ABS_Y,
 };
 
-static const unsigned int rb4_keymap[] = {
-	[0x1] = BTN_WEST, /* Square */
-	[0x2] = BTN_SOUTH, /* Cross */
-	[0x3] = BTN_EAST, /* Circle */
-	[0x4] = BTN_NORTH, /* Triangle */
-	[0x5] = BTN_TL, /* L1 */
-	[0x6] = BTN_TR, /* R1 */
-	[0x7] = BTN_TL2, /* L2 */
-	[0x8] = BTN_TR2, /* R2 */
-	[0x9] = BTN_SELECT, /* Share */
-	[0xa] = BTN_START, /* Options */
-	[0xb] = BTN_THUMBL, /* L3 */
-	[0xc] = BTN_THUMBR, /* R3 */
-	[0xd] = BTN_MODE, /* PS */
+static const unsigned int ps3_turntable_absmap[] = {
+	[0x32] = ABS_X,
+	[0x35] = ABS_Y,
+};
+
+static const unsigned int instrument_keymap[] = {
+	[0x1] = BTN_WEST,
+	[0x2] = BTN_SOUTH,
+	[0x3] = BTN_EAST,
+	[0x4] = BTN_NORTH,
+	[0x5] = BTN_TL,
+	[0x6] = BTN_TR,
+	[0x7] = BTN_TL2,
+	[0x8] = BTN_TR2,
+	[0x9] = BTN_SELECT,
+	[0xa] = BTN_START,
+	[0xb] = BTN_THUMBL,
+	[0xc] = BTN_THUMBR,
+	[0xd] = BTN_MODE,
 };
 
 static enum power_supply_property sony_battery_props[] = {
@@ -490,6 +510,7 @@ struct motion_output_report_02 {
 #define SIXAXIS_REPORT_0xF2_SIZE 17
 #define SIXAXIS_REPORT_0xF5_SIZE 8
 #define MOTION_REPORT_0x02_SIZE 49
+#define PRO_INSTRUMENT_0x00_SIZE 8
 
 #define SENSOR_SUFFIX " Motion Sensors"
 #define TOUCHPAD_SUFFIX " Touchpad"
@@ -539,6 +560,9 @@ struct sony_sc {
 	/* GH Live */
 	struct urb *ghl_urb;
 	struct timer_list ghl_poke_timer;
+
+	/* Rock Band 3 Pro Instruments */
+	unsigned long rb3_pro_poke_jiffies;
 };
 
 static void sony_set_leds(struct sony_sc *sc);
@@ -610,35 +634,108 @@ static int ghl_init_urb(struct sony_sc *sc, struct usb_device *usbdev,
 	return 0;
 }
 
-static int gh_guitar_mapping(struct hid_device *hdev, struct hid_input *hi,
+
+
+/*
+ * Sending HID_REQ_SET_REPORT enables the full report. Without this
+ * Rock Band 3 Pro instruments only report navigation events
+ */
+static int rb3_pro_instrument_enable_full_report(struct sony_sc *sc)
+{
+	struct hid_device *hdev = sc->hdev;
+	static const u8 report[] = { 0x00, 0xE9, 0x00, 0x89, 0x1B,
+								 0x00, 0x00, 0x00, 0x02, 0x00,
+								 0x00, 0x00, 0x00, 0x00, 0x00,
+								 0x00, 0x00, 0x00, 0x00, 0x00,
+								 0x00, 0x00, 0x80, 0x00, 0x00,
+								 0x00, 0x00, 0x89, 0x00, 0x00,
+								 0x00, 0x00, 0x00, 0xE9, 0x01,
+								 0x00, 0x00, 0x00, 0x00, 0x00,
+								 0x00 };
+	u8 *buf;
+	int ret;
+
+	buf = kmemdup(report, sizeof(report), GFP_KERNEL);
+	if (!buf)
+		return -ENOMEM;
+
+	ret = hid_hw_raw_request(hdev, buf[0], buf, sizeof(report),
+				  HID_FEATURE_REPORT, HID_REQ_SET_REPORT);
+
+	kfree(buf);
+
+	return ret;
+}
+
+static int djh_turntable_mapping(struct hid_device *hdev, struct hid_input *hi,
 			  struct hid_field *field, struct hid_usage *usage,
 			  unsigned long **bit, int *max)
 {
 	if ((usage->hid & HID_USAGE_PAGE) == HID_UP_MSVENDOR) {
 		unsigned int abs = usage->hid & HID_USAGE;
 
-		if (abs == GUITAR_TILT_USAGE) {
+		if (abs == TURNTABLE_CROSS_FADER_USAGE) {
+			hid_map_usage_clear(hi, usage, bit, max, EV_ABS, ABS_RX);
+			return 1;
+		} else if (abs == TURNTABLE_EFFECTS_KNOB_USAGE) {
 			hid_map_usage_clear(hi, usage, bit, max, EV_ABS, ABS_RY);
 			return 1;
+		} else if (abs == TURNTABLE_PLATTER_BUTTONS_USAGE) {
+			hid_map_usage_clear(hi, usage, bit, max, EV_ABS, ABS_RZ);
+			return 1;
 		}
+	} else if ((usage->hid & HID_USAGE_PAGE) == HID_UP_GENDESK) {
+		unsigned int abs = usage->hid & HID_USAGE;
+
+		if (abs >= ARRAY_SIZE(ps3_turntable_absmap))
+			return -1;
+
+		abs = ps3_turntable_absmap[abs];
+
+		hid_map_usage_clear(hi, usage, bit, max, EV_ABS, abs);
+		return 1;
 	}
 	return 0;
 }
 
-static int rb4_guitar_mapping(struct hid_device *hdev, struct hid_input *hi,
+static int instrument_mapping(struct hid_device *hdev, struct hid_input *hi,
 			  struct hid_field *field, struct hid_usage *usage,
 			  unsigned long **bit, int *max)
 {
 	if ((usage->hid & HID_USAGE_PAGE) == HID_UP_BUTTON) {
 		unsigned int key = usage->hid & HID_USAGE;
 
-		if (key >= ARRAY_SIZE(rb4_keymap))
+		if (key >= ARRAY_SIZE(instrument_keymap))
 			return 0;
 
-		key = rb4_keymap[key];
+		key = instrument_keymap[key];
 		hid_map_usage_clear(hi, usage, bit, max, EV_KEY, key);
 		return 1;
-	} else if ((usage->hid & HID_USAGE_PAGE) == HID_UP_GENDESK) {
+	}
+
+	return 0;
+}
+
+static int gh_guitar_mapping(struct hid_device *hdev, struct hid_input *hi,
+			  struct hid_field *field, struct hid_usage *usage,
+			  unsigned long **bit, int *max)
+{
+	if ((usage->hid & HID_USAGE_PAGE) == HID_UP_MSVENDOR) {
+		unsigned int abs = usage->hid & HID_USAGE;
+
+		if (abs == GUITAR_TILT_USAGE) {
+			hid_map_usage_clear(hi, usage, bit, max, EV_ABS, ABS_RY);
+			return 1;
+		}
+	}
+	return 0;
+}
+
+static int rb4_guitar_mapping(struct hid_device *hdev, struct hid_input *hi,
+			  struct hid_field *field, struct hid_usage *usage,
+			  unsigned long **bit, int *max)
+{
+	if ((usage->hid & HID_USAGE_PAGE) == HID_UP_GENDESK) {
 		unsigned int abs = usage->hid & HID_USAGE;
 
 		/* Let the HID parser deal with the HAT. */
@@ -1052,6 +1149,18 @@ static int sony_raw_event(struct hid_device *hdev, struct hid_report *report,
 		return 1;
 	}
 
+	/* Rock Band 3 PS3 Pro instruments set rd[24] to 0xE0 when they're
+	 * sending full reports, and 0x02 when only sending navigation.
+	 */
+	if ((sc->quirks & RB3_PS3_PRO_INSTRUMENT) && rd[24] == 0x02) {
+		/* Only attempt to enable report every 8 seconds */
+		if (time_after(jiffies, sc->rb3_pro_poke_jiffies)) {
+			sc->rb3_pro_poke_jiffies = jiffies +
+				(RB3_PRO_INSTRUMENT_POKE_RETRY_INTERVAL * HZ);
+			rb3_pro_instrument_enable_full_report(sc);
+		}
+	}
+
 	if (sc->defer_initialization) {
 		sc->defer_initialization = 0;
 		sony_schedule_work(sc, SONY_WORKER_STATE);
@@ -1065,6 +1174,7 @@ static int sony_mapping(struct hid_device *hdev, struct hid_input *hi,
 			unsigned long **bit, int *max)
 {
 	struct sony_sc *sc = hid_get_drvdata(hdev);
+	int ret;
 
 	if (sc->quirks & BUZZ_CONTROLLER) {
 		unsigned int key = usage->hid & HID_USAGE;
@@ -1098,9 +1208,19 @@ static int sony_mapping(struct hid_device *hdev, struct hid_input *hi,
 	if (sc->quirks & SIXAXIS_CONTROLLER)
 		return sixaxis_mapping(hdev, hi, field, usage, bit, max);
 
-	if (sc->quirks & GH_GUITAR_CONTROLLER)
+	/* INSTRUMENT quirk is used as a base mapping for instruments */
+	if (sc->quirks & INSTRUMENT) {
+		ret = instrument_mapping(hdev, hi, field, usage, bit, max);
+		if (ret != 0)
+			return ret;
+	}
+
+	if (sc->quirks & GH_GUITAR_TILT)
 		return gh_guitar_mapping(hdev, hi, field, usage, bit, max);
 
+	if (sc->quirks & PS3_DJH_TURNTABLE)
+		return djh_turntable_mapping(hdev, hi, field, usage, bit, max);
+
 	if (sc->quirks & (RB4_GUITAR_PS4_USB | RB4_GUITAR_PS4_BT))
 		return rb4_guitar_mapping(hdev, hi, field, usage, bit, max);
 
@@ -2060,6 +2180,19 @@ static int sony_input_configured(struct hid_device *hdev,
 		}
 
 		sony_init_output_report(sc, sixaxis_send_output_report);
+	} else if (sc->quirks & RB3_PS3_PRO_INSTRUMENT) {
+		/*
+		 * Rock Band 3 PS3 Pro Instruments also do not handle HID Output
+		 * Reports on the interrupt EP like they should, so we need to force
+		 * HID output reports to use HID_REQ_SET_REPORT on the Control EP.
+		 *
+		 * There is also another issue about HID Output Reports via USB,
+		 * these instruments do not want the report_id as part of the data
+		 * packet, so we have to discard buf[0] when sending the actual
+		 * control message, even for numbered reports.
+		 */
+		hdev->quirks |= HID_QUIRK_NO_OUTPUT_REPORTS_ON_INTR_EP;
+		hdev->quirks |= HID_QUIRK_SKIP_OUTPUT_REPORT_ID;
 	} else if (sc->quirks & SIXAXIS_CONTROLLER_USB) {
 		/*
 		 * The Sony Sixaxis does not handle HID Output Reports on the
@@ -2227,6 +2360,10 @@ static int sony_probe(struct hid_device *hdev, const struct hid_device_id *id)
 		goto err;
 	}
 
+	if (sc->quirks & RB3_PS3_PRO_INSTRUMENT) {
+		sc->rb3_pro_poke_jiffies = 0;
+	}
+
 	if (sc->quirks & (GHL_GUITAR_PS3WIIU | GHL_GUITAR_PS4)) {
 		if (!hid_is_usb(hdev)) {
 			ret = -EINVAL;
@@ -2364,35 +2501,86 @@ static const struct hid_device_id sony_devices[] = {
 	{ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_SMK, USB_DEVICE_ID_SMK_NSG_MR7U_REMOTE),
 		.driver_data = NSG_MR7U_REMOTE_BT },
 	/* Guitar Hero Live PS3 and Wii U guitar dongles */
-	{ HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3WIIU_GHLIVE_DONGLE),
-		.driver_data = GHL_GUITAR_PS3WIIU | GH_GUITAR_CONTROLLER },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3WIIU_GHLIVE),
+		.driver_data = GHL_GUITAR_PS3WIIU | GH_GUITAR_TILT | INSTRUMENT },
 	/* Guitar Hero PC Guitar Dongle */
 	{ HID_USB_DEVICE(USB_VENDOR_ID_REDOCTANE, USB_DEVICE_ID_REDOCTANE_GUITAR_DONGLE),
-		.driver_data = GH_GUITAR_CONTROLLER },
-	/* Guitar Hero PS3 World Tour Guitar Dongle */
-	{ HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3_GUITAR_DONGLE),
-		.driver_data = GH_GUITAR_CONTROLLER },
+		.driver_data = GH_GUITAR_TILT | INSTRUMENT },
+	/* Guitar Hero PS3 Guitar Dongle */
+	{ HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3_GH_GUITAR),
+		.driver_data = GH_GUITAR_TILT | INSTRUMENT },
+	/* Guitar Hero PS3 Drum Dongle */
+	{ HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3_GH_DRUMS),
+		.driver_data = INSTRUMENT },
+	/* DJ Hero PS3 Guitar Dongle */
+	{ HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3_DJH_TURNTABLE),
+		.driver_data = PS3_DJH_TURNTABLE | INSTRUMENT },
 	/* Guitar Hero Live PS4 guitar dongles */
 	{ HID_USB_DEVICE(USB_VENDOR_ID_REDOCTANE, USB_DEVICE_ID_REDOCTANE_PS4_GHLIVE_DONGLE),
-		.driver_data = GHL_GUITAR_PS4 | GH_GUITAR_CONTROLLER },
+		.driver_data = GHL_GUITAR_PS4 | GH_GUITAR_TILT | INSTRUMENT },
+	/* Rock Band 1 Wii instruments */
+	{ HID_USB_DEVICE(USB_VENDOR_ID_HARMONIX, USB_DEVICE_ID_HARMONIX_WII_RB1_GUITAR),
+		.driver_data = INSTRUMENT },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_HARMONIX, USB_DEVICE_ID_HARMONIX_WII_RB1_DRUMS),
+		.driver_data = INSTRUMENT },
+	/* Rock Band 2 Wii instruments */
+	{ HID_USB_DEVICE(USB_VENDOR_ID_HARMONIX, USB_DEVICE_ID_HARMONIX_WII_RB2_GUITAR),
+		.driver_data = INSTRUMENT },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_HARMONIX, USB_DEVICE_ID_HARMONIX_WII_RB2_DRUMS),
+		.driver_data = INSTRUMENT },
+	/* Rock Band 3 Wii instruments */
+	{ HID_USB_DEVICE(USB_VENDOR_ID_HARMONIX, USB_DEVICE_ID_HARMONIX_WII_RB3_MPA_DRUMS_MODE),
+		.driver_data = INSTRUMENT },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_HARMONIX, USB_DEVICE_ID_HARMONIX_WII_RB3_MUSTANG_GUITAR),
+		.driver_data = INSTRUMENT },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_HARMONIX, USB_DEVICE_ID_HARMONIX_WII_RB3_SQUIRE_GUITAR),
+		.driver_data = INSTRUMENT },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_HARMONIX, USB_DEVICE_ID_HARMONIX_WII_RB3_MPA_MUSTANG_MODE),
+		.driver_data = INSTRUMENT },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_HARMONIX, USB_DEVICE_ID_HARMONIX_WII_RB3_MPA_SQUIRE_MODE),
+		.driver_data = INSTRUMENT },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_HARMONIX, USB_DEVICE_ID_HARMONIX_WII_RB3_KEYBOARD),
+		.driver_data = INSTRUMENT },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_HARMONIX, USB_DEVICE_ID_HARMONIX_WII_RB3_MPA_KEYBOARD_MODE),
+		.driver_data = INSTRUMENT },
+	/* Rock Band 3 PS3 instruments */
+	{ HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3_RB_GUITAR),
+		.driver_data = INSTRUMENT },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3_RB_DRUMS),
+		.driver_data = INSTRUMENT },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3_RB3_MPA_DRUMS_MODE),
+		.driver_data = INSTRUMENT },
+	/* Rock Band 3 PS3 Pro instruments */
+	{ HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3_RB3_MUSTANG_GUITAR),
+		.driver_data = INSTRUMENT | RB3_PS3_PRO_INSTRUMENT },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3_RB3_SQUIRE_GUITAR),
+		.driver_data = INSTRUMENT | RB3_PS3_PRO_INSTRUMENT },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3_RB3_MPA_MUSTANG_MODE),
+		.driver_data = INSTRUMENT | RB3_PS3_PRO_INSTRUMENT },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3_RB3_MPA_SQUIRE_MODE),
+		.driver_data = INSTRUMENT | RB3_PS3_PRO_INSTRUMENT },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3_RB3_KEYBOARD),
+		.driver_data = INSTRUMENT | RB3_PS3_PRO_INSTRUMENT },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3_RB3_MPA_KEYBOARD_MODE),
+		.driver_data = INSTRUMENT | RB3_PS3_PRO_INSTRUMENT },
 	/* Rock Band 4 PS4 guitars */
 	{ HID_USB_DEVICE(USB_VENDOR_ID_PDP, USB_DEVICE_ID_PDP_PS4_RIFFMASTER),
-		.driver_data = RB4_GUITAR_PS4_USB },
+		.driver_data = RB4_GUITAR_PS4_USB | INSTRUMENT },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_CRKD, USB_DEVICE_ID_CRKD_PS4_GIBSON_SG),
-		.driver_data = RB4_GUITAR_PS4_USB },
+		.driver_data = RB4_GUITAR_PS4_USB | INSTRUMENT },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_CRKD, USB_DEVICE_ID_CRKD_PS4_GIBSON_SG_DONGLE),
-		.driver_data = RB4_GUITAR_PS4_USB },
+		.driver_data = RB4_GUITAR_PS4_USB | INSTRUMENT },
 	{ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_PDP, USB_DEVICE_ID_PDP_PS4_JAGUAR),
-		.driver_data = RB4_GUITAR_PS4_BT },
+		.driver_data = RB4_GUITAR_PS4_BT | INSTRUMENT },
 	{ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_MADCATZ, USB_DEVICE_ID_MADCATZ_PS4_STRATOCASTER),
-		.driver_data = RB4_GUITAR_PS4_BT },
+		.driver_data = RB4_GUITAR_PS4_BT | INSTRUMENT },
 	/* Rock Band 4 PS5 guitars */
 	{ HID_USB_DEVICE(USB_VENDOR_ID_PDP, USB_DEVICE_ID_PDP_PS5_RIFFMASTER),
-		.driver_data = RB4_GUITAR_PS5 },
+		.driver_data = RB4_GUITAR_PS5 | INSTRUMENT },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_CRKD, USB_DEVICE_ID_CRKD_PS5_GIBSON_SG),
-		.driver_data = RB4_GUITAR_PS5 },
+		.driver_data = RB4_GUITAR_PS5 | INSTRUMENT },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_CRKD, USB_DEVICE_ID_CRKD_PS5_GIBSON_SG_DONGLE),
-		.driver_data = RB4_GUITAR_PS5 },
+		.driver_data = RB4_GUITAR_PS5 | INSTRUMENT },
 	{ }
 };
 MODULE_DEVICE_TABLE(hid, sony_devices);
@@ -2428,5 +2616,5 @@ static void __exit sony_exit(void)
 module_init(sony_init);
 module_exit(sony_exit);
 
-MODULE_DESCRIPTION("HID driver for Sony / PS2 / PS3 / PS4 BD devices");
+MODULE_DESCRIPTION("HID driver for Sony / PS2 / PS3 / PS4 / PS5 BD devices");
 MODULE_LICENSE("GPL");
-- 
2.53.0


^ permalink raw reply related

* Re: [PATCH v2 0/3] Input: add initial support for Goodix GTX8 touchscreen ICs
From: Hans de Goede @ 2026-02-28 11:23 UTC (permalink / raw)
  To: Aelin Reidel, Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Neil Armstrong, Henrik Rydberg
  Cc: linux-input, devicetree, linux-kernel, linux, phone-devel,
	~postmarketos/upstreaming, Piyush Raj Chouhan,
	Alexander Koskovich
In-Reply-To: <20260228-gtx8-v2-0-3a408c365f6c@mainlining.org>

Hi,

On 28-Feb-26 02:56, Aelin Reidel wrote:
> These ICs support SPI and I2C interfaces, up to 10 finger touch, stylus
> and gesture events.
> 
> This driver is derived from the Goodix gtx8_driver_linux available at
> [1] and only supports the GT9886 and GT9896 ICs present in the Xiaomi
> Mi 9T and Xiaomi Redmi Note 10 Pro smartphones.
> 
> The current implementation only supports Normandy and Yellowstone type
> ICs, aka only GT9886 and GT9896. It is also limited to I2C only, since I
> don't have a device with GTX8 over SPI at hand. Adding support for SPI
> should be fairly easy in the future, since the code uses a regmap.
> 
> Support for advanced features like:
> - Firmware updates
> - Stylus events
> - Gesture events
> - Nanjing IC support
> is not included in current version.
> 
> The current support requires a previously flashed firmware to be
> present.
> 
> As I did not have access to datasheets for these ICs, I extracted the
> addresses from a couple of config files using a small tool [2]. The
> addresses are identical for the same IC families in all configs I
> observed, however not all of them make sense and I stubbed out firmware
> request support due to this.
> 
> I've taken a lot of inspiration from the goodix_berlin driver, but the 
> Berlin and GTX8 series of touchscreen ICs differ quite a bit. The driver 
> architecture is the same overall, i.e. the power-up sequence and general 
> concepts are the mostly same, but it is very clear that they are 
> different generations when looking at it in more detail.

Right, this answers my main question about this driver which was:
"why another goodix driver?" (this would be the third one).

I've also compared this driver with the original goodix.c touchscreen
driver (which I know well) and the protocol is somewhat closer
to the original goodix.c driver then it is to goodix_berlin, but still
different enough that having a separate driver is the best option IMHO.

...

> From what I can tell, the evolution seems to be:
> Normandy -> Yellowstone -> Berlin
> since Normandy and Yellowstone are already quite different (especially 
> with the way checksums work) and Yellowstone has a couple of things 
> (checksum, fw_version) that appear similar to Berlin series ICs.

You forgot the original goodix.c driver, adding that it seems
the evolution is:

GTx1/GTx2/GTx6 -> Normandy -> Yellowstone -> Berlin

With GTx1/GTx2/GTx6 having no checksum at all (and 16 bit
registers) and some of the original GTx1/GTx2/GTx6 don't have
nvram for the firmware, so Linux must upload firmware every boot.

Anyways I agree that these are different enough from the existing
goodix and goodix_berlin drivers, so based on that (and only on that):

Acked-by: Hans de Goede <johannes.goede@oss.qualcomm.com>

Regards,

Hans




^ permalink raw reply

* [PATCH 0/7] HID: asus: increase robustness of the driver
From: Denis Benato @ 2026-02-28 14:46 UTC (permalink / raw)
  To: linux-kernel
  Cc: linux-input, Benjamin Tissoires, Jiri Kosina, Luke D . Jones,
	Mateusz Schyboll, Denis Benato, Denis Benato

Hi all,

Previous asus-wmi maintainer and asus-linux developer has become less
active in the project and left me in charge of advancing the support
for ASUS equipement on Linux.

I am preparing to send a patchset of his revised work to support ASUS
ROG Ally handhelds devices and since that work is also useful in
expanding support for 2025 models it is important to improve the
hid-asus driver as future patches will build on top of these changes.

Cheers,
Denis

Denis Benato (7):
  HID: asus: fix code style of comments and brackets
  HID: asus: add xg mobile 2022 external hardware support
  HID: asus: fix compiler warning about unused variables
  HID: asus: make asus_resume adhere to linux kernel coding standards
  HID: asus: simplify and improve asus_kbd_set_report()
  HID: asus: do not abort probe when not necessary
  HID: asus: do not try to initialize the backlight if the enpoint
    doesn't support it

 drivers/hid/hid-asus.c | 67 ++++++++++++++++++------------------------
 drivers/hid/hid-ids.h  |  1 +
 2 files changed, 29 insertions(+), 39 deletions(-)

-- 
2.53.0


^ permalink raw reply

* [PATCH 1/7] HID: asus: fix code style of comments and brackets
From: Denis Benato @ 2026-02-28 14:46 UTC (permalink / raw)
  To: linux-kernel
  Cc: linux-input, Benjamin Tissoires, Jiri Kosina, Luke D . Jones,
	Mateusz Schyboll, Denis Benato, Denis Benato
In-Reply-To: <20260228144626.1661530-1-denis.benato@linux.dev>

In asus_raw_event there is code that would violate checkpatch script
such as unaligned closing comments and superfluous graph parenthesis:
fix these stylistic issues.

Also remove an empty comment spanning two lines.

This commit does not change runtime behavior.

Signed-off-by: Denis Benato <denis.benato@linux.dev>
---
 drivers/hid/hid-asus.c | 16 +++++-----------
 1 file changed, 5 insertions(+), 11 deletions(-)

diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index ffbfaff9f117..5fcb06b16167 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -20,9 +20,6 @@
  *  Copyright (c) 2016 Frederik Wenigwieser <frederik.wenigwieser@gmail.com>
  */
 
-/*
- */
-
 #include <linux/acpi.h>
 #include <linux/dmi.h>
 #include <linux/hid.h>
@@ -359,7 +356,7 @@ static int asus_event(struct hid_device *hdev, struct hid_field *field,
 		      struct hid_usage *usage, __s32 value)
 {
 	struct asus_drvdata *drvdata = hid_get_drvdata(hdev);
-	
+
 	if ((usage->hid & HID_USAGE_PAGE) == HID_UP_ASUSVENDOR &&
 	    (usage->hid & HID_USAGE) != 0x00 &&
 	    (usage->hid & HID_USAGE) != 0xff && !usage->type) {
@@ -448,21 +445,18 @@ static int asus_raw_event(struct hid_device *hdev,
 		/*
 		 * G713 and G733 send these codes on some keypresses, depending on
 		 * the key pressed it can trigger a shutdown event if not caught.
-		*/
-		if (data[0] == 0x02 && data[1] == 0x30) {
+		 */
+		if (data[0] == 0x02 && data[1] == 0x30)
 			return -1;
-		}
 	}
 
 	if (drvdata->quirks & QUIRK_ROG_CLAYMORE_II_KEYBOARD) {
 		/*
 		 * CLAYMORE II keyboard sends this packet when it goes to sleep
 		 * this causes the whole system to go into suspend.
-		*/
-
-		if(size == 2 && data[0] == 0x02 && data[1] == 0x00) {
+		 */
+		if (size == 2 && data[0] == 0x02 && data[1] == 0x00)
 			return -1;
-		}
 	}
 
 	return 0;
-- 
2.53.0


^ permalink raw reply related


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