Linux Input/HID development
 help / color / mirror / Atom feed
* Re: [PATCH v2 3/4] Documentation: ABI: Document SteelSeries headset sysfs attributes
From: Bastien Nocera @ 2026-01-12 13:08 UTC (permalink / raw)
  To: Sriman Achanta, Jiri Kosina, Benjamin Tissoires, linux-input,
	linux-kernel
In-Reply-To: <20260112041941.40531-4-srimanachanta@gmail.com>

On Sun, 2026-01-11 at 23:19 -0500, Sriman Achanta wrote:
> Replace the SRW-S1 specific documentation with comprehensive
> documentation for the hid-steelseries driver covering both the legacy
> SRW-S1 wheel and the modern Arctis headset lineup.

Renaming the sysfs-driver-hid-srws1 should be done separately from
adding new contents.

This patch should also come after the feature gets added.

> 
> New sysfs attributes documented:
> - sidetone_level: Control microphone monitoring volume

This should be an ALSA mixer, not a sysfs file.

> - inactive_time: Auto-sleep timeout configuration
> - chatmix_level: Game/Chat audio balance (read-only)

Ditto for an ALSA mixer.

> - mic_mute_led_brightness: Microphone mute LED brightness control

This probably needs to be a standard LED device.

> - mic_volume: Internal microphone gain control

Ditto for an ALSA mixer.

> - volume_limiter: EU hearing protection volume limiter

Ditto for an ALSA switch.

> - bluetooth_on_power: Bluetooth auto-enable on power-on
> - bluetooth_call_vol: Bluetooth call audio attenuation settings

Ditto for an ALSA mixer.

> 
> The SRW-S1 LED documentation is preserved and moved into the new
> unified
> documentation file.
> 
> Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
> ---
>  .../ABI/testing/sysfs-driver-hid-srws1        |  21 ---
>  .../ABI/testing/sysfs-driver-hid-steelseries  | 131
> ++++++++++++++++++
>  2 files changed, 131 insertions(+), 21 deletions(-)
>  delete mode 100644 Documentation/ABI/testing/sysfs-driver-hid-srws1
>  create mode 100644 Documentation/ABI/testing/sysfs-driver-hid-
> steelseries
> 
> diff --git a/Documentation/ABI/testing/sysfs-driver-hid-srws1
> b/Documentation/ABI/testing/sysfs-driver-hid-srws1
> deleted file mode 100644
> index d0eba70c7d40..000000000000
> --- a/Documentation/ABI/testing/sysfs-driver-hid-srws1
> +++ /dev/null
> @@ -1,21 +0,0 @@
> -What:		/sys/class/leds/SRWS1::<serial>::RPM1
> -What:		/sys/class/leds/SRWS1::<serial>::RPM2
> -What:		/sys/class/leds/SRWS1::<serial>::RPM3
> -What:		/sys/class/leds/SRWS1::<serial>::RPM4
> -What:		/sys/class/leds/SRWS1::<serial>::RPM5
> -What:		/sys/class/leds/SRWS1::<serial>::RPM6
> -What:		/sys/class/leds/SRWS1::<serial>::RPM7
> -What:		/sys/class/leds/SRWS1::<serial>::RPM8
> -What:		/sys/class/leds/SRWS1::<serial>::RPM9
> -What:		/sys/class/leds/SRWS1::<serial>::RPM10
> -What:		/sys/class/leds/SRWS1::<serial>::RPM11
> -What:		/sys/class/leds/SRWS1::<serial>::RPM12
> -What:		/sys/class/leds/SRWS1::<serial>::RPM13
> -What:		/sys/class/leds/SRWS1::<serial>::RPM14
> -What:		/sys/class/leds/SRWS1::<serial>::RPM15
> -What:		/sys/class/leds/SRWS1::<serial>::RPMALL
> -Date:		Jan 2013
> -KernelVersion:	3.9
> -Contact:	Simon Wood <simon@mungewell.org>
> -Description:	Provides a control for turning on/off the LEDs which
> form
> -		an RPM meter on the front of the controller
> diff --git a/Documentation/ABI/testing/sysfs-driver-hid-steelseries
> b/Documentation/ABI/testing/sysfs-driver-hid-steelseries
> new file mode 100644
> index 000000000000..751cf01ceda3
> --- /dev/null
> +++ b/Documentation/ABI/testing/sysfs-driver-hid-steelseries
> @@ -0,0 +1,131 @@
> +What:		/sys/class/leds/SRWS1::<serial>::RPM1
> +What:		/sys/class/leds/SRWS1::<serial>::RPM2
> +What:		/sys/class/leds/SRWS1::<serial>::RPM3
> +What:		/sys/class/leds/SRWS1::<serial>::RPM4
> +What:		/sys/class/leds/SRWS1::<serial>::RPM5
> +What:		/sys/class/leds/SRWS1::<serial>::RPM6
> +What:		/sys/class/leds/SRWS1::<serial>::RPM7
> +What:		/sys/class/leds/SRWS1::<serial>::RPM8
> +What:		/sys/class/leds/SRWS1::<serial>::RPM9
> +What:		/sys/class/leds/SRWS1::<serial>::RPM10
> +What:		/sys/class/leds/SRWS1::<serial>::RPM11
> +What:		/sys/class/leds/SRWS1::<serial>::RPM12
> +What:		/sys/class/leds/SRWS1::<serial>::RPM13
> +What:		/sys/class/leds/SRWS1::<serial>::RPM14
> +What:		/sys/class/leds/SRWS1::<serial>::RPM15
> +What:		/sys/class/leds/SRWS1::<serial>::RPMALL
> +Date:		Jan 2013
> +KernelVersion:	3.9
> +Contact:	Simon Wood <simon@mungewell.org>
> +Description:	Provides a control for turning on/off the LEDs which
> form
> +		an RPM meter on the front of the controller
> +
> +What:		/sys/class/hid/drivers/steelseries/<dev>/sidetone_le
> vel
> +Date:		January 2025
> +KernelVersion:	6.19
> +Contact:	Sriman Achanta <srimanachanta@gmail.com>
> +Description:
> +		Controls the sidetone (microphone monitoring) volume
> level.
> +		This determines how much of the microphone input is
> fed back into
> +		the headset speakers.
> +
> +		Range: 0-128 (mapped internally to device-specific
> values).
> +
> +		Access: Write
> +
> +What:		/sys/class/hid/drivers/steelseries/<dev>/inactive_ti
> me
> +Date:		January 2025
> +KernelVersion:	6.19
> +Contact:	Sriman Achanta <srimanachanta@gmail.com>
> +Description:
> +		Sets the time in minutes before the headset
> automatically enters
> +		standby/sleep mode when no audio is playing.
> +
> +		Range: 0-90 (minutes).
> +		       Some devices (e.g., Arctis 1/7X) map this to
> specific presets.
> +
> +		Access: Write
> +
> +What:		/sys/class/hid/drivers/steelseries/<dev>/chatmix_lev
> el
> +Date:		January 2025
> +KernelVersion:	6.19
> +Contact:	Sriman Achanta <srimanachanta@gmail.com>
> +Description:
> +		Reports the current balance between Game and Chat
> audio channels
> +		(ChatMix). This value changes when the physical
> ChatMix dial
> +		on the headset is adjusted.
> +
> +		Range: 0-128
> +		       0   = 100% Chat / 0% Game
> +		       64  = 50% Chat / 50% Game (Balanced)
> +		       128 = 0% Chat / 100% Game
> +
> +		Access: Read
> +
> +What:		/sys/class/hid/drivers/steelseries/<dev>/mic_mute_le
> d_brightness
> +Date:		January 2025
> +KernelVersion:	6.19
> +Contact:	Sriman Achanta <srimanachanta@gmail.com>
> +Description:
> +		Controls the brightness of the LED on the microphone
> boom that
> +		indicates when the microphone is muted.
> +
> +		Range: 0-3 (off, low, medium, high) for most
> devices.
> +		       0-10 for newer Nova series devices.
> +
> +		Access: Write
> +
> +What:		/sys/class/hid/drivers/steelseries/<dev>/mic_volume
> +Date:		January 2025
> +KernelVersion:	6.19
> +Contact:	Sriman Achanta <srimanachanta@gmail.com>
> +Description:
> +		Controls the internal microphone gain/volume of the
> headset.
> +		This is distinct from the OS input volume.
> +
> +		Range: 0-128 (mapped internally to device-specific
> values).
> +
> +		Access: Write
> +
> +What:		/sys/class/hid/drivers/steelseries/<dev>/volume_limi
> ter
> +Date:		January 2025
> +KernelVersion:	6.19
> +Contact:	Sriman Achanta <srimanachanta@gmail.com>
> +Description:
> +		Enables or disables the EU volume limiter (hearing
> protection).
> +		When enabled, the maximum output volume is capped.
> +
> +		Values:
> +		0 = Disabled
> +		1 = Enabled
> +
> +		Access: Write
> +
> +What:		/sys/class/hid/drivers/steelseries/<dev>/bluetooth_o
> n_power
> +Date:		January 2025
> +KernelVersion:	6.19
> +Contact:	Sriman Achanta <srimanachanta@gmail.com>
> +Description:
> +		Configures whether the Bluetooth radio automatically
> turns on
> +		when the headset is powered on.
> +
> +		Values:
> +		0 = Bluetooth must be turned on manually
> +		1 = Bluetooth turns on automatically with headset
> +
> +		Access: Write
> +
> +What:		/sys/class/hid/drivers/steelseries/<dev>/bluetooth_c
> all_vol
> +Date:		January 2025
> +KernelVersion:	6.19
> +Contact:	Sriman Achanta <srimanachanta@gmail.com>
> +Description:
> +		Configures how the 2.4GHz Game/Chat audio is
> attenuated when
> +		a Bluetooth call is active.
> +
> +		Values:
> +		0 = No attenuation (mix both equally)
> +		1 = Attenuate Game audio by -12dB
> +		2 = Mute Game audio completely
> +
> +		Access: Write

^ permalink raw reply

* Re: [PATCH v2 2/4] HID: quirks: Add INPUT_CONFIGURED quirk for SteelSeries Arctis headsets
From: Bastien Nocera @ 2026-01-12 13:08 UTC (permalink / raw)
  To: Sriman Achanta, Jiri Kosina, Benjamin Tissoires, linux-input,
	linux-kernel
In-Reply-To: <20260112041941.40531-3-srimanachanta@gmail.com>

On Sun, 2026-01-11 at 23:19 -0500, Sriman Achanta wrote:
> Add HID_QUIRK_INPUT_CONFIGURED for all SteelSeries Arctis headsets
> that
> require the hid-steelseries driver. This quirk ensures proper device
> initialization and prevents conflicts with generic HID drivers.
> 
> The quirk is necessary because these devices expose multiple HID
> interfaces, and the hid-steelseries driver needs to bind to specific
> interfaces based on the device capabilities. Without this quirk, the
> generic HID driver may interfere with device-specific functionality
> like
> battery monitoring and feature controls.

Only devices that need it should be added to this list.
USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X (which used to be called
USB_DEVICE_ID_STEELSERIES_ARCTIS_1 before patch 1 in the list) only has
one HID interface, so doesn't need this patch.

> 
> Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
> ---
>  drivers/hid/hid-quirks.c | 25 +++++++++++++++++++++++++
>  1 file changed, 25 insertions(+)
> 
> diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c
> index c89a015686c0..8a7c3f433040 100644
> --- a/drivers/hid/hid-quirks.c
> +++ b/drivers/hid/hid-quirks.c
> @@ -699,7 +699,32 @@ static const struct hid_device_id
> hid_have_special_driver[] = {
>  #if IS_ENABLED(CONFIG_HID_STEELSERIES)
>  	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_SRWS1) },
>  	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_1) },
> +	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X) },
> +	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_7) },
> +	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_7_P) },
> +	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_7_X) },
> +	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_7_GEN2) },
>  	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_9) },
> +	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_PRO) },
> +	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS) },
> +	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_P) },
> +	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_X) },
> +	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_DESTINY) },
> +	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3) },
> +	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_P) },
> +	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_X) },
> +	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5) },
> +	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5_X) },
> +	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7) },
> +	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X) },
> +	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_P) },
> +	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_REV2) },
> +	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO) },
> +	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_WOW) },
> +	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_GEN2) },
> +	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2) },
> +	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_PRO) },
> +	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
> USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_PRO_X) },
>  #endif
>  #if IS_ENABLED(CONFIG_HID_SUNPLUS)
>  	{ HID_USB_DEVICE(USB_VENDOR_ID_SUNPLUS,
> USB_DEVICE_ID_SUNPLUS_WDESKTOP) },

^ permalink raw reply

* Re: [PATCH v2 02/16] HID: hid-lenovo-go: Add Lenovo Legion Go Series HID Driver
From: Jiri Kosina @ 2026-01-12 12:12 UTC (permalink / raw)
  To: Derek J. Clark
  Cc: Benjamin Tissoires, Mario Limonciello, Zhixin Zhang, Mia Shao,
	Mark Pearson, Pierre-Loup A . Griffais, linux-input, linux-doc,
	linux-kernel
In-Reply-To: <20251229031753.581664-3-derekjohn.clark@gmail.com>

On Mon, 29 Dec 2025, Derek J. Clark wrote:

> Adds initial framework for a new HID driver, hid-lenovo-go, along with
> attributes that report the firmware and hardware version for each
> component of the HID device, of which there are 4 parts: The MCU, the
> transmission dongle, the left "handle" controller half, and the right
> "handle" controller half. Each of these devices are provided an attribute
> group to contain its device specific attributes. Additionally, the touchpad
> device attributes are logically separated from the other components in
> another attribute group.
> 
> This driver primarily provides access to the configurable settings of the
> Lenovo Legion Go and Lenovo Legion Go 2 controllers running the latest
> firmware. As previously noted, the Legion Go controllers recently had a
> firmware update[1] which switched from the original "SepentiaUSB" protocol
> to a brand new protocol for the Go 2, primarily to ensure backwards and
> forwards compatibility between the Go and Go 2 devices. As part of that
> update the PIDs for the controllers were changed, so there is no risk of
> this driver attaching to controller firmware that it doesn't support.
> 
> Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
> ---
>  MAINTAINERS                 |   6 +
>  drivers/hid/Kconfig         |  12 +
>  drivers/hid/Makefile        |   1 +
>  drivers/hid/hid-ids.h       |   3 +
>  drivers/hid/hid-lenovo-go.c | 734 ++++++++++++++++++++++++++++++++++++
>  5 files changed, 756 insertions(+)
>  create mode 100644 drivers/hid/hid-lenovo-go.c
> 
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 9ed6d11a7746..b5ad29d24e3e 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -14135,6 +14135,12 @@ L:	platform-driver-x86@vger.kernel.org
>  S:	Maintained
>  F:	drivers/platform/x86/lenovo/wmi-hotkey-utilities.c
>  
> +LENOVO HID drivers
> +M:	Derek J. Clark <derekjohn.clark@gmail.com>
> +L:	linux-input@vger.kernel.org
> +S:	Maintained
> +F:	drivers/hid/hid-lenovo-go.c

Hi Derek,

thanks for working on this.

I am now almost finished with reviewing this pile and am planning to queue 
it in hid.git shortly, but I have a question regarding the MAINTAINERS 
entry above.

The title claims support for all of Lenovo HID, but there is much more to 
it than drivers/hid/hid-lenovo-go.c, specifically in hid-lenovo.c.

So either please make the title more specific (or claim the ownership of 
the whole Lenovo HID landscape indeed, fine by me, but the please reflect 
that in F: :) ).

Thanks,

-- 
Jiri Kosina
SUSE Labs


^ permalink raw reply

* [PATCH 05/12] HID: logitech-dj: Use pm_ptr instead of #ifdef CONFIG_PM
From: Bastien Nocera @ 2026-01-12 10:52 UTC (permalink / raw)
  To: linux-input; +Cc: linux-kernel, Jiri Kosina, Benjamin Tissoires, Bastien Nocera
In-Reply-To: <20260112105500.3664834-1-hadess@hadess.net>

This increases build coverage and allows to drop an #ifdef.

Signed-off-by: Bastien Nocera <hadess@hadess.net>
---
 drivers/hid/hid-logitech-dj.c | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/drivers/hid/hid-logitech-dj.c b/drivers/hid/hid-logitech-dj.c
index cce54dd9884a..a3ed9624ca64 100644
--- a/drivers/hid/hid-logitech-dj.c
+++ b/drivers/hid/hid-logitech-dj.c
@@ -2044,9 +2044,7 @@ static struct hid_driver logi_djreceiver_driver = {
 	.probe = logi_dj_probe,
 	.remove = logi_dj_remove,
 	.raw_event = logi_dj_raw_event,
-#ifdef CONFIG_PM
-	.reset_resume = logi_dj_reset_resume,
-#endif
+	.reset_resume = pm_ptr(logi_dj_reset_resume),
 };
 
 module_hid_driver(logi_djreceiver_driver);
-- 
2.52.0


^ permalink raw reply related

* [PATCH 09/12] HID: sony: Use pm_ptr instead of #ifdef CONFIG_PM
From: Bastien Nocera @ 2026-01-12 10:52 UTC (permalink / raw)
  To: linux-input; +Cc: linux-kernel, Jiri Kosina, Benjamin Tissoires, Bastien Nocera
In-Reply-To: <20260112105500.3664834-1-hadess@hadess.net>

This increases build coverage and allows to drop an #ifdef.

Signed-off-by: Bastien Nocera <hadess@hadess.net>
---
 drivers/hid/hid-sony.c | 9 +++------
 1 file changed, 3 insertions(+), 6 deletions(-)

diff --git a/drivers/hid/hid-sony.c b/drivers/hid/hid-sony.c
index b966e4044238..d94ece0448e9 100644
--- a/drivers/hid/hid-sony.c
+++ b/drivers/hid/hid-sony.c
@@ -2284,12 +2284,9 @@ static struct hid_driver sony_driver = {
 	.remove           = sony_remove,
 	.report_fixup     = sony_report_fixup,
 	.raw_event        = sony_raw_event,
-
-#ifdef CONFIG_PM
-	.suspend          = sony_suspend,
-	.resume	          = sony_resume,
-	.reset_resume     = sony_resume,
-#endif
+	.suspend          = pm_ptr(sony_suspend),
+	.resume	          = pm_ptr(sony_resume),
+	.reset_resume     = pm_ptr(sony_resume),
 };
 
 static int __init sony_init(void)
-- 
2.52.0


^ permalink raw reply related

* [PATCH 07/12] HID: picolcd_core: Use pm_ptr instead of #ifdef CONFIG_PM
From: Bastien Nocera @ 2026-01-12 10:52 UTC (permalink / raw)
  To: linux-input; +Cc: linux-kernel, Jiri Kosina, Benjamin Tissoires, Bastien Nocera
In-Reply-To: <20260112105500.3664834-1-hadess@hadess.net>

This increases build coverage and allows to drop an #ifdef.

Signed-off-by: Bastien Nocera <hadess@hadess.net>
---
 drivers/hid/hid-picolcd_core.c | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/drivers/hid/hid-picolcd_core.c b/drivers/hid/hid-picolcd_core.c
index 297103be3381..e579b3080ce5 100644
--- a/drivers/hid/hid-picolcd_core.c
+++ b/drivers/hid/hid-picolcd_core.c
@@ -648,11 +648,9 @@ static struct hid_driver picolcd_driver = {
 	.probe =         picolcd_probe,
 	.remove =        picolcd_remove,
 	.raw_event =     picolcd_raw_event,
-#ifdef CONFIG_PM
-	.suspend =       picolcd_suspend,
-	.resume =        picolcd_resume,
-	.reset_resume =  picolcd_reset_resume,
-#endif
+	.suspend =       pm_ptr(picolcd_suspend),
+	.resume =        pm_ptr(picolcd_resume),
+	.reset_resume =  pm_ptr(picolcd_reset_resume),
 };
 module_hid_driver(picolcd_driver);
 
-- 
2.52.0


^ permalink raw reply related

* [PATCH 08/12] HID: hid-sensor-hub: Use pm_ptr instead of #ifdef CONFIG_PM
From: Bastien Nocera @ 2026-01-12 10:52 UTC (permalink / raw)
  To: linux-input; +Cc: linux-kernel, Jiri Kosina, Benjamin Tissoires, Bastien Nocera
In-Reply-To: <20260112105500.3664834-1-hadess@hadess.net>

This increases build coverage and allows to drop an #ifdef.

Signed-off-by: Bastien Nocera <hadess@hadess.net>
---
 drivers/hid/hid-sensor-hub.c | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/drivers/hid/hid-sensor-hub.c b/drivers/hid/hid-sensor-hub.c
index 4c94c03cb573..7618dec9c31e 100644
--- a/drivers/hid/hid-sensor-hub.c
+++ b/drivers/hid/hid-sensor-hub.c
@@ -772,11 +772,9 @@ static struct hid_driver sensor_hub_driver = {
 	.remove = sensor_hub_remove,
 	.raw_event = sensor_hub_raw_event,
 	.report_fixup = sensor_hub_report_fixup,
-#ifdef CONFIG_PM
-	.suspend = sensor_hub_suspend,
-	.resume = sensor_hub_resume,
-	.reset_resume = sensor_hub_reset_resume,
-#endif
+	.suspend = pm_ptr(sensor_hub_suspend),
+	.resume = pm_ptr(sensor_hub_resume),
+	.reset_resume = pm_ptr(sensor_hub_reset_resume),
 };
 module_hid_driver(sensor_hub_driver);
 
-- 
2.52.0


^ permalink raw reply related

* [PATCH 06/12] HID: nintendo: Use pm_ptr instead of #ifdef CONFIG_PM
From: Bastien Nocera @ 2026-01-12 10:52 UTC (permalink / raw)
  To: linux-input; +Cc: linux-kernel, Jiri Kosina, Benjamin Tissoires, Bastien Nocera
In-Reply-To: <20260112105500.3664834-1-hadess@hadess.net>

This increases build coverage and allows to drop an #ifdef.

Signed-off-by: Bastien Nocera <hadess@hadess.net>
---
 drivers/hid/hid-nintendo.c | 7 ++-----
 1 file changed, 2 insertions(+), 5 deletions(-)

diff --git a/drivers/hid/hid-nintendo.c b/drivers/hid/hid-nintendo.c
index c2849a541f65..95b6df7b9b13 100644
--- a/drivers/hid/hid-nintendo.c
+++ b/drivers/hid/hid-nintendo.c
@@ -2826,11 +2826,8 @@ static struct hid_driver nintendo_hid_driver = {
 	.probe		= nintendo_hid_probe,
 	.remove		= nintendo_hid_remove,
 	.raw_event	= nintendo_hid_event,
-
-#ifdef CONFIG_PM
-	.resume		= nintendo_hid_resume,
-	.suspend	= nintendo_hid_suspend,
-#endif
+	.resume		= pm_ptr(nintendo_hid_resume),
+	.suspend	= pm_ptr(nintendo_hid_suspend),
 };
 static int __init nintendo_init(void)
 {
-- 
2.52.0


^ permalink raw reply related

* [PATCH 03/12] HID: asus: Use pm_ptr instead of #ifdef CONFIG_PM
From: Bastien Nocera @ 2026-01-12 10:52 UTC (permalink / raw)
  To: linux-input; +Cc: linux-kernel, Jiri Kosina, Benjamin Tissoires, Bastien Nocera
In-Reply-To: <20260112105500.3664834-1-hadess@hadess.net>

This increases build coverage and allows to drop an #ifdef.

Signed-off-by: Bastien Nocera <hadess@hadess.net>
---
 drivers/hid/hid-asus.c | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index a444d41e53b6..1aaa40c2529a 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -1433,10 +1433,8 @@ static struct hid_driver asus_driver = {
 	.remove			= asus_remove,
 	.input_mapping          = asus_input_mapping,
 	.input_configured       = asus_input_configured,
-#ifdef CONFIG_PM
-	.reset_resume           = asus_reset_resume,
-	.resume					= asus_resume,
-#endif
+	.reset_resume           = pm_ptr(asus_reset_resume),
+	.resume			= pm_ptr(asus_resume),
 	.event			= asus_event,
 	.raw_event		= asus_raw_event
 };
-- 
2.52.0


^ permalink raw reply related

* [PATCH 00/12] HID: Use pm_*ptr instead of #ifdef CONFIG_PM*
From: Bastien Nocera @ 2026-01-12 10:52 UTC (permalink / raw)
  To: linux-input; +Cc: linux-kernel, Jiri Kosina, Benjamin Tissoires, Bastien Nocera

All those changes should be safe, as similar ones were done in other
subsystems, but I'm uncertain about the surface changes.

Bastien Nocera (12):
  HID: hid-alps: Use pm_ptr instead of #ifdef CONFIG_PM
  HID: appletb-kbd: Use pm_ptr instead of #ifdef CONFIG_PM
  HID: asus: Use pm_ptr instead of #ifdef CONFIG_PM
  HID: lenovo: Use pm_ptr instead of #ifdef CONFIG_PM
  HID: logitech-dj: Use pm_ptr instead of #ifdef CONFIG_PM
  HID: nintendo: Use pm_ptr instead of #ifdef CONFIG_PM
  HID: picolcd_core: Use pm_ptr instead of #ifdef CONFIG_PM
  HID: hid-sensor-hub: Use pm_ptr instead of #ifdef CONFIG_PM
  HID: sony: Use pm_ptr instead of #ifdef CONFIG_PM
  HID: uclogic: Use pm_ptr instead of #ifdef CONFIG_PM
  HID: wacom: Use pm_ptr instead of #ifdef CONFIG_PM
  HID: surface: Use pm_ptr_sleep instead of #ifdef CONFIG_PM_SLEEP

 drivers/hid/hid-alps.c                     | 6 ++----
 drivers/hid/hid-appletb-kbd.c              | 6 ++----
 drivers/hid/hid-asus.c                     | 6 ++----
 drivers/hid/hid-lenovo.c                   | 4 +---
 drivers/hid/hid-logitech-dj.c              | 4 +---
 drivers/hid/hid-nintendo.c                 | 7 ++-----
 drivers/hid/hid-picolcd_core.c             | 8 +++-----
 drivers/hid/hid-sensor-hub.c               | 8 +++-----
 drivers/hid/hid-sony.c                     | 9 +++------
 drivers/hid/hid-uclogic-core.c             | 6 ++----
 drivers/hid/surface-hid/surface_hid.c      | 2 +-
 drivers/hid/surface-hid/surface_hid_core.c | 5 -----
 drivers/hid/surface-hid/surface_kbd.c      | 2 +-
 drivers/hid/wacom_sys.c                    | 6 ++----
 14 files changed, 25 insertions(+), 54 deletions(-)

-- 
2.52.0


^ permalink raw reply

* [PATCH 04/12] HID: lenovo: Use pm_ptr instead of #ifdef CONFIG_PM
From: Bastien Nocera @ 2026-01-12 10:52 UTC (permalink / raw)
  To: linux-input; +Cc: linux-kernel, Jiri Kosina, Benjamin Tissoires, Bastien Nocera
In-Reply-To: <20260112105500.3664834-1-hadess@hadess.net>

This increases build coverage and allows to drop an #ifdef.

Signed-off-by: Bastien Nocera <hadess@hadess.net>
---
 drivers/hid/hid-lenovo.c | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/drivers/hid/hid-lenovo.c b/drivers/hid/hid-lenovo.c
index 9cc3e029e9f6..14652478d989 100644
--- a/drivers/hid/hid-lenovo.c
+++ b/drivers/hid/hid-lenovo.c
@@ -1570,9 +1570,7 @@ static struct hid_driver lenovo_driver = {
 	.raw_event = lenovo_raw_event,
 	.event = lenovo_event,
 	.report_fixup = lenovo_report_fixup,
-#ifdef CONFIG_PM
-	.reset_resume = lenovo_reset_resume,
-#endif
+	.reset_resume = pm_ptr(lenovo_reset_resume),
 };
 module_hid_driver(lenovo_driver);
 
-- 
2.52.0


^ permalink raw reply related

* [PATCH 01/12] HID: hid-alps: Use pm_ptr instead of #ifdef CONFIG_PM
From: Bastien Nocera @ 2026-01-12 10:52 UTC (permalink / raw)
  To: linux-input; +Cc: linux-kernel, Jiri Kosina, Benjamin Tissoires, Bastien Nocera
In-Reply-To: <20260112105500.3664834-1-hadess@hadess.net>

This increases build coverage and allows to drop an #ifdef.

Signed-off-by: Bastien Nocera <hadess@hadess.net>
---
 drivers/hid/hid-alps.c | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/drivers/hid/hid-alps.c b/drivers/hid/hid-alps.c
index ba00f6e6324b..21e55f3d0d1b 100644
--- a/drivers/hid/hid-alps.c
+++ b/drivers/hid/hid-alps.c
@@ -840,10 +840,8 @@ static struct hid_driver alps_driver = {
 	.raw_event		= alps_raw_event,
 	.input_mapping		= alps_input_mapping,
 	.input_configured	= alps_input_configured,
-#ifdef CONFIG_PM
-	.resume			= alps_post_resume,
-	.reset_resume		= alps_post_reset,
-#endif
+	.resume			= pm_ptr(alps_post_resume),
+	.reset_resume		= pm_ptr(alps_post_reset),
 };
 
 module_hid_driver(alps_driver);
-- 
2.52.0


^ permalink raw reply related

* [PATCH 02/12] HID: appletb-kbd: Use pm_ptr instead of #ifdef CONFIG_PM
From: Bastien Nocera @ 2026-01-12 10:52 UTC (permalink / raw)
  To: linux-input; +Cc: linux-kernel, Jiri Kosina, Benjamin Tissoires, Bastien Nocera
In-Reply-To: <20260112105500.3664834-1-hadess@hadess.net>

This increases build coverage and allows to drop an #ifdef.

Signed-off-by: Bastien Nocera <hadess@hadess.net>
---
 drivers/hid/hid-appletb-kbd.c | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/drivers/hid/hid-appletb-kbd.c b/drivers/hid/hid-appletb-kbd.c
index b00687e67ce8..064e136610ec 100644
--- a/drivers/hid/hid-appletb-kbd.c
+++ b/drivers/hid/hid-appletb-kbd.c
@@ -501,10 +501,8 @@ static struct hid_driver appletb_kbd_hid_driver = {
 	.remove = appletb_kbd_remove,
 	.event = appletb_kbd_hid_event,
 	.input_configured = appletb_kbd_input_configured,
-#ifdef CONFIG_PM
-	.suspend = appletb_kbd_suspend,
-	.reset_resume = appletb_kbd_reset_resume,
-#endif
+	.suspend = pm_ptr(appletb_kbd_suspend),
+	.reset_resume = pm_ptr(appletb_kbd_reset_resume),
 	.driver.dev_groups = appletb_kbd_groups,
 };
 module_hid_driver(appletb_kbd_hid_driver);
-- 
2.52.0


^ permalink raw reply related

* [PATCH 12/12] HID: surface: Use pm_ptr_sleep instead of #ifdef CONFIG_PM_SLEEP
From: Bastien Nocera @ 2026-01-12 10:53 UTC (permalink / raw)
  To: linux-input; +Cc: linux-kernel, Jiri Kosina, Benjamin Tissoires, Bastien Nocera
In-Reply-To: <20260112105500.3664834-1-hadess@hadess.net>

This increases build coverage and allows to drop an #ifdef.

Signed-off-by: Bastien Nocera <hadess@hadess.net>
---
 drivers/hid/surface-hid/surface_hid.c      | 2 +-
 drivers/hid/surface-hid/surface_hid_core.c | 5 -----
 drivers/hid/surface-hid/surface_kbd.c      | 2 +-
 3 files changed, 2 insertions(+), 7 deletions(-)

diff --git a/drivers/hid/surface-hid/surface_hid.c b/drivers/hid/surface-hid/surface_hid.c
index eae47e0d95ed..21904612b5ad 100644
--- a/drivers/hid/surface-hid/surface_hid.c
+++ b/drivers/hid/surface-hid/surface_hid.c
@@ -241,7 +241,7 @@ static struct ssam_device_driver surface_hid_driver = {
 	.match_table = surface_hid_match,
 	.driver = {
 		.name = "surface_hid",
-		.pm = &surface_hid_pm_ops,
+		.pm = pm_ptr_sleep(&surface_hid_pm_ops),
 		.probe_type = PROBE_PREFER_ASYNCHRONOUS,
 	},
 };
diff --git a/drivers/hid/surface-hid/surface_hid_core.c b/drivers/hid/surface-hid/surface_hid_core.c
index 6690c24f28f0..33fe15fbcf6c 100644
--- a/drivers/hid/surface-hid/surface_hid_core.c
+++ b/drivers/hid/surface-hid/surface_hid_core.c
@@ -281,11 +281,6 @@ const struct dev_pm_ops surface_hid_pm_ops = {
 };
 EXPORT_SYMBOL_GPL(surface_hid_pm_ops);
 
-#else /* CONFIG_PM_SLEEP */
-
-const struct dev_pm_ops surface_hid_pm_ops = { };
-EXPORT_SYMBOL_GPL(surface_hid_pm_ops);
-
 #endif /* CONFIG_PM_SLEEP */
 
 MODULE_AUTHOR("Maximilian Luz <luzmaximilian@gmail.com>");
diff --git a/drivers/hid/surface-hid/surface_kbd.c b/drivers/hid/surface-hid/surface_kbd.c
index 0be01b5e7425..6ab6870f10e8 100644
--- a/drivers/hid/surface-hid/surface_kbd.c
+++ b/drivers/hid/surface-hid/surface_kbd.c
@@ -288,7 +288,7 @@ static struct platform_driver surface_kbd_driver = {
 	.driver = {
 		.name = "surface_keyboard",
 		.acpi_match_table = surface_kbd_match,
-		.pm = &surface_hid_pm_ops,
+		.pm = pm_ptr_sleep(&surface_hid_pm_ops),
 		.probe_type = PROBE_PREFER_ASYNCHRONOUS,
 	},
 };
-- 
2.52.0


^ permalink raw reply related

* [PATCH 11/12] HID: wacom: Use pm_ptr instead of #ifdef CONFIG_PM
From: Bastien Nocera @ 2026-01-12 10:53 UTC (permalink / raw)
  To: linux-input; +Cc: linux-kernel, Jiri Kosina, Benjamin Tissoires, Bastien Nocera
In-Reply-To: <20260112105500.3664834-1-hadess@hadess.net>

This increases build coverage and allows to drop an #ifdef.

Signed-off-by: Bastien Nocera <hadess@hadess.net>
---
 drivers/hid/wacom_sys.c | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/drivers/hid/wacom_sys.c b/drivers/hid/wacom_sys.c
index 9a57504e51a1..fbb07c71f0ba 100644
--- a/drivers/hid/wacom_sys.c
+++ b/drivers/hid/wacom_sys.c
@@ -2949,10 +2949,8 @@ static struct hid_driver wacom_driver = {
 	.probe =	wacom_probe,
 	.remove =	wacom_remove,
 	.report =	wacom_wac_report,
-#ifdef CONFIG_PM
-	.resume =	wacom_resume,
-	.reset_resume =	wacom_reset_resume,
-#endif
+	.resume =	pm_ptr(wacom_resume),
+	.reset_resume =	pm_ptr(wacom_reset_resume),
 	.raw_event =	wacom_raw_event,
 };
 module_hid_driver(wacom_driver);
-- 
2.52.0


^ permalink raw reply related

* [PATCH 10/12] HID: uclogic: Use pm_ptr instead of #ifdef CONFIG_PM
From: Bastien Nocera @ 2026-01-12 10:52 UTC (permalink / raw)
  To: linux-input; +Cc: linux-kernel, Jiri Kosina, Benjamin Tissoires, Bastien Nocera
In-Reply-To: <20260112105500.3664834-1-hadess@hadess.net>

This increases build coverage and allows to drop an #ifdef.

Signed-off-by: Bastien Nocera <hadess@hadess.net>
---
 drivers/hid/hid-uclogic-core.c | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/drivers/hid/hid-uclogic-core.c b/drivers/hid/hid-uclogic-core.c
index 34fb03ae8ee2..a73a07b0b238 100644
--- a/drivers/hid/hid-uclogic-core.c
+++ b/drivers/hid/hid-uclogic-core.c
@@ -617,10 +617,8 @@ static struct hid_driver uclogic_driver = {
 	.raw_event = uclogic_raw_event,
 	.input_mapping = uclogic_input_mapping,
 	.input_configured = uclogic_input_configured,
-#ifdef CONFIG_PM
-	.resume	          = uclogic_resume,
-	.reset_resume     = uclogic_resume,
-#endif
+	.resume	          = pm_ptr(uclogic_resume),
+	.reset_resume     = pm_ptr(uclogic_resume),
 };
 module_hid_driver(uclogic_driver);
 
-- 
2.52.0


^ permalink raw reply related

* [PATCH v5 1/1] Input: cros_ec_keyb - add function key support
From: Fabio Baltieri @ 2026-01-12  9:33 UTC (permalink / raw)
  To: Dmitry Torokhov, Benson Leung, Guenter Roeck
  Cc: Fabio Baltieri, Tzung-Bi Shih, Simon Glass, linux-input,
	chrome-platform, linux-kernel
In-Reply-To: <20260112093309.240905-1-fabiobaltieri@chromium.org>

Add support for handling an Fn button and sending separate keycodes for
a subset of keys in the matrix defined in the upper half of the keymap.

Signed-off-by: Fabio Baltieri <fabiobaltieri@chromium.org>
Reviewed-by: Simon Glass <sjg@chromium.org>
---
 drivers/input/keyboard/cros_ec_keyb.c | 174 +++++++++++++++++++++++---
 1 file changed, 158 insertions(+), 16 deletions(-)

diff --git a/drivers/input/keyboard/cros_ec_keyb.c b/drivers/input/keyboard/cros_ec_keyb.c
index 1c6b0461dc35..93540f0c5a33 100644
--- a/drivers/input/keyboard/cros_ec_keyb.c
+++ b/drivers/input/keyboard/cros_ec_keyb.c
@@ -29,6 +29,11 @@
 
 #include <linux/unaligned.h>
 
+/* Maximum size of the normal key matrix, this is limited by the host command
+ * key_matrix field defined in ec_response_get_next_data_v3
+ */
+#define CROS_EC_KEYBOARD_COLS_MAX 18
+
 /**
  * struct cros_ec_keyb - Structure representing EC keyboard device
  *
@@ -44,6 +49,11 @@
  * @bs_idev: The input device for non-matrix buttons and switches (or NULL).
  * @notifier: interrupt event notifier for transport devices
  * @vdata: vivaldi function row data
+ * @has_fn_map: whether the driver uses an fn function-map layer
+ * @normal_key_status: active normal keys map
+ * @fn_key_status: active function keys map
+ * @fn_key_pressed: tracks the function key status
+ * @fn_key_triggered: tracks where any function key fired
  */
 struct cros_ec_keyb {
 	unsigned int rows;
@@ -61,6 +71,12 @@ struct cros_ec_keyb {
 	struct notifier_block notifier;
 
 	struct vivaldi_data vdata;
+
+	bool has_fn_map;
+	u8 normal_key_status[CROS_EC_KEYBOARD_COLS_MAX];
+	u8 fn_key_status[CROS_EC_KEYBOARD_COLS_MAX];
+	bool fn_key_pressed;
+	bool fn_key_triggered;
 };
 
 /**
@@ -166,16 +182,104 @@ static bool cros_ec_keyb_has_ghosting(struct cros_ec_keyb *ckdev, uint8_t *buf)
 	return false;
 }
 
+/*
+ * Process a function key state change, send an event report if appropriate.
+ */
+static void cros_ec_keyb_process_fn_key(struct cros_ec_keyb *ckdev,
+					int row, int col, bool state)
+{
+	struct input_dev *idev = ckdev->idev;
+	int pos = MATRIX_SCAN_CODE(row, col, ckdev->row_shift);
+
+	ckdev->fn_key_pressed = state;
+
+	if (state) {
+		ckdev->fn_key_triggered = false;
+	} else if (!ckdev->fn_key_triggered) {
+		/*
+		 * Send the original code if nothing else has been pressed
+		 * together with Fn.
+		 */
+		input_event(idev, EV_MSC, MSC_SCAN, pos);
+		input_report_key(idev, KEY_FN, true);
+		input_sync(idev);
+
+		input_event(idev, EV_MSC, MSC_SCAN, pos);
+		input_report_key(idev, KEY_FN, false);
+	}
+}
+
+/*
+ * Return the Fn code for a normal key row, col combination, optionally set a
+ * position code too.
+ */
+static unsigned int cros_ec_keyb_fn_code(struct cros_ec_keyb *ckdev,
+					 int row, int col, int *pos)
+{
+	struct input_dev *idev = ckdev->idev;
+	const unsigned short *keycodes = idev->keycode;
+	int fn_pos = MATRIX_SCAN_CODE(row + ckdev->rows, col, ckdev->row_shift);
+
+	if (pos)
+		*pos = fn_pos;
+
+	return keycodes[fn_pos];
+}
+
+/*
+ * Process the new state for a single key.
+ */
+static void cros_ec_keyb_process_one(struct cros_ec_keyb *ckdev,
+				     int row, int col, bool state)
+{
+	struct input_dev *idev = ckdev->idev;
+	const unsigned short *keycodes = idev->keycode;
+	int pos = MATRIX_SCAN_CODE(row, col, ckdev->row_shift);
+	unsigned int code = keycodes[pos];
+
+	dev_dbg(ckdev->dev, "changed: [r%d c%d]: byte %02x\n", row, col, state);
+
+	if (ckdev->has_fn_map) {
+		if (code == KEY_FN)
+			return cros_ec_keyb_process_fn_key(ckdev, row, col, state);
+
+		if (!state) {
+			if (ckdev->fn_key_status[col] & BIT(row)) {
+				code = cros_ec_keyb_fn_code(ckdev, row, col, &pos);
+
+				ckdev->fn_key_status[col] &= ~BIT(row);
+			} else if (ckdev->normal_key_status[col] & BIT(row)) {
+				ckdev->normal_key_status[col] &= ~BIT(row);
+			} else {
+				/* Discard, key press code was not sent */
+				return;
+			}
+		} else if (ckdev->fn_key_pressed) {
+			code = cros_ec_keyb_fn_code(ckdev, row, col, &pos);
+
+			ckdev->fn_key_triggered = true;
+
+			if (!code)
+				return;
+
+			ckdev->fn_key_status[col] |= BIT(row);
+		} else {
+			ckdev->normal_key_status[col] |= BIT(row);
+		}
+	}
+
+	input_event(idev, EV_MSC, MSC_SCAN, pos);
+	input_report_key(idev, code, state);
+}
 
 /*
  * Compares the new keyboard state to the old one and produces key
- * press/release events accordingly.  The keyboard state is 13 bytes (one byte
- * per column)
+ * press/release events accordingly.  The keyboard state is one byte
+ * per column.
  */
 static void cros_ec_keyb_process(struct cros_ec_keyb *ckdev,
 			 uint8_t *kb_state, int len)
 {
-	struct input_dev *idev = ckdev->idev;
 	int col, row;
 	int new_state;
 	int old_state;
@@ -192,20 +296,13 @@ static void cros_ec_keyb_process(struct cros_ec_keyb *ckdev,
 
 	for (col = 0; col < ckdev->cols; col++) {
 		for (row = 0; row < ckdev->rows; row++) {
-			int pos = MATRIX_SCAN_CODE(row, col, ckdev->row_shift);
-			const unsigned short *keycodes = idev->keycode;
-
 			new_state = kb_state[col] & (1 << row);
 			old_state = ckdev->old_kb_state[col] & (1 << row);
-			if (new_state != old_state) {
-				dev_dbg(ckdev->dev,
-					"changed: [r%d c%d]: byte %02x\n",
-					row, col, new_state);
-
-				input_event(idev, EV_MSC, MSC_SCAN, pos);
-				input_report_key(idev, keycodes[pos],
-						 new_state);
-			}
+
+			if (new_state == old_state)
+				continue;
+
+			cros_ec_keyb_process_one(ckdev, row, col, new_state);
 		}
 		ckdev->old_kb_state[col] = kb_state[col];
 	}
@@ -582,6 +679,43 @@ static void cros_ec_keyb_parse_vivaldi_physmap(struct cros_ec_keyb *ckdev)
 	ckdev->vdata.num_function_row_keys = n_physmap;
 }
 
+/* Returns true if there is a KEY_FN code defined in the normal keymap */
+static bool cros_ec_keyb_has_fn_key(struct cros_ec_keyb *ckdev)
+{
+	struct input_dev *idev = ckdev->idev;
+	const unsigned short *keycodes = idev->keycode;
+
+	for (int row = 0; row < ckdev->rows; row++) {
+		for (int col = 0; col < ckdev->cols; col++) {
+			int pos = MATRIX_SCAN_CODE(row, col, ckdev->row_shift);
+
+			if (keycodes[pos] == KEY_FN)
+				return true;
+		}
+	}
+
+	return false;
+}
+
+/*
+ * Returns true if there is a KEY_FN defined and at least one key in the fn
+ * layer keymap
+ */
+static bool cros_ec_keyb_has_fn_map(struct cros_ec_keyb *ckdev)
+{
+	if (!cros_ec_keyb_has_fn_key(ckdev))
+		return false;
+
+	for (int row = 0; row < ckdev->rows; row++) {
+		for (int col = 0; col < ckdev->cols; col++) {
+			if (cros_ec_keyb_fn_code(ckdev, row, col, NULL) != 0)
+				return true;
+		}
+	}
+
+	return false;
+}
+
 /**
  * cros_ec_keyb_register_matrix - Register matrix keys
  *
@@ -603,6 +737,12 @@ static int cros_ec_keyb_register_matrix(struct cros_ec_keyb *ckdev)
 	if (err)
 		return err;
 
+	if (ckdev->cols > CROS_EC_KEYBOARD_COLS_MAX) {
+		dev_err(dev, "keypad,num-columns too large: %d (max: %d)\n",
+			ckdev->cols, CROS_EC_KEYBOARD_COLS_MAX);
+		return -EINVAL;
+	}
+
 	ckdev->valid_keys = devm_kzalloc(dev, ckdev->cols, GFP_KERNEL);
 	if (!ckdev->valid_keys)
 		return -ENOMEM;
@@ -635,7 +775,7 @@ static int cros_ec_keyb_register_matrix(struct cros_ec_keyb *ckdev)
 	ckdev->ghost_filter = device_property_read_bool(dev,
 					"google,needs-ghost-filter");
 
-	err = matrix_keypad_build_keymap(NULL, NULL, ckdev->rows, ckdev->cols,
+	err = matrix_keypad_build_keymap(NULL, NULL, ckdev->rows * 2, ckdev->cols,
 					 NULL, idev);
 	if (err) {
 		dev_err(dev, "cannot build key matrix\n");
@@ -650,6 +790,8 @@ static int cros_ec_keyb_register_matrix(struct cros_ec_keyb *ckdev)
 	cros_ec_keyb_compute_valid_keys(ckdev);
 	cros_ec_keyb_parse_vivaldi_physmap(ckdev);
 
+	ckdev->has_fn_map = cros_ec_keyb_has_fn_map(ckdev);
+
 	err = input_register_device(ckdev->idev);
 	if (err) {
 		dev_err(dev, "cannot register input device\n");
-- 
2.52.0.457.g6b5491de43-goog


^ permalink raw reply related

* [PATCH v5 0/1] Input: cros_ec_keyb: add function key support
From: Fabio Baltieri @ 2026-01-12  9:33 UTC (permalink / raw)
  To: Dmitry Torokhov, Benson Leung, Guenter Roeck
  Cc: Fabio Baltieri, Tzung-Bi Shih, Simon Glass, linux-input,
	chrome-platform, linux-kernel

Changes from v4:
  - just a comment tweak

Changes from v3:
  - implemented fn layer runtime detection
  - tweaked cros_ec_keyb_fn_code to return back the correct position
    code

Changes from v2:
  - renamed the dt property to use-fn-map, dropped the example
  - added few function comments
  - added a helper for obtaining the fn code
  - reordered, dt patch first

Changes from v1:
  - change struct to short types
  - refactored the fn key handling in its own function
  - changed props to use the google, prefix
  - reworked the properties to use an overlay map rather than a
    dedicated one

Fabio Baltieri (1):
  Input: cros_ec_keyb - add function key support

 drivers/input/keyboard/cros_ec_keyb.c | 174 +++++++++++++++++++++++---
 1 file changed, 158 insertions(+), 16 deletions(-)

-- 
2.52.0.457.g6b5491de43-goog


^ permalink raw reply

* Re: [PATCH v2 3/6] dt-bindings: input: google,goldfish-events-keypad: Convert to DT schema
From: Krzysztof Kozlowski @ 2026-01-12  7:22 UTC (permalink / raw)
  To: Kuan-Wei Chiu
  Cc: airlied, simona, maarten.lankhorst, mripard, tzimmermann, robh,
	krzk+dt, conor+dt, dmitry.torokhov, sre, gregkh, jirislaby,
	lgirdwood, broonie, jserv, eleanor15x, dri-devel, devicetree,
	linux-kernel, linux-input, linux-pm, linux-serial, linux-sound
In-Reply-To: <aWPUwCbFoIItG7n3@google.com>

On 11/01/2026 17:50, Kuan-Wei Chiu wrote:
> Hi Krzysztof,
> 
> On Fri, Jan 09, 2026 at 09:23:33AM +0100, Krzysztof Kozlowski wrote:
>> On Thu, Jan 08, 2026 at 08:08:33AM +0000, Kuan-Wei Chiu wrote:
>>> +
>>> +examples:
>>> +  - |
>>> +    keypad@9040000 {
>>> +        compatible = "google,goldfish-events-keypad";
>>> +        reg = <0x9040000 0x1000>;
>>> +        interrupts = <0x5>;
>>
>> Same comment as before. It applies everywhere, btw.
> 
> Oops, I actually had a local fixup for this but forgot to apply it
> before sending. Sorry about that.
> 
> On a side note, none of your replies regarding v2 appeared in my inbox
> or spam folder. I had to retrieve this message content from the lore
> web interface to reply. However, your replies to v1 arrived without any
> issues.

I think this was issue on oss.qualcomm.com side, because email did not
reach my other mailbox either.

Well, Gmail sucks a lot and brings a lot of pain for all other people
(e.g. LF infrastructure), so if switching maybe avoid Gmail...

Best regards,
Krzysztof

^ permalink raw reply

* [PATCH v2 4/4] HID: steelseries: Add support for Arctis headset lineup
From: Sriman Achanta @ 2026-01-12  4:19 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires, linux-input, linux-kernel; +Cc: Sriman Achanta
In-Reply-To: <20260112041941.40531-1-srimanachanta@gmail.com>

Add full support for the SteelSeries Arctis wireless gaming headset
lineup, extending the driver from basic support for 2 models (Arctis 1
and 9) to comprehensive support for 25+ models across all Arctis
generations.

This is a major restructure of the hid-steelseries driver that replaces
the previous minimal implementation with a unified, capability-based
architecture.

Architecture changes:
- Introduce steelseries_device_info structure to define per-device
  capabilities, interface bindings, and metadata
- Implement capability system (SS_CAP_*) for feature detection: battery,
  sidetone, chatmix, microphone controls, volume limiting, and Bluetooth
  settings
- Add interface binding logic to correctly bind to HID control
  interfaces on multi-interface USB devices using two modes:
  * Mode 0: Bind to first enumerated interface (for Arctis 9, Pro)
  * Mode 1: Bind to specific interface via bitmask (for other models)
- Create device info tables for all supported Arctis models with their
  specific capabilities and interface requirements

Features added:
- Battery monitoring: Implement power_supply integration with periodic
  polling and device-specific battery request protocols for all model
  families. Supports battery capacity reporting, charging status, and
  wireless connection tracking.
- Sidetone control: Sysfs attribute to adjust microphone monitoring
  volume (0-128) with device-specific mapping to hardware ranges
- Auto-sleep timeout: Configure inactivity timeout (0-90 minutes) before
  headset enters standby mode
- ChatMix reporting: Read-only sysfs attribute reporting game/chat audio
  balance from physical dial on supported models
- Microphone controls:
  * Mute LED brightness (0-3 or 0-10 depending on model)
  * Internal microphone gain/volume (0-128)
- Volume limiter: Enable/disable EU hearing protection (max volume cap)
- Bluetooth controls (Nova 7 series):
  * Auto-enable Bluetooth on power-on
  * Configure game audio attenuation during BT calls

Implementation details:
- Device-specific raw_event parsing for battery updates across different
  HID report formats (8-byte, 12-byte, 64-byte, 128-byte)
- Helper functions for HID feature reports and output reports to handle
  different communication methods across device families
- Attribute visibility system to expose only relevant controls for each
  device based on capability flags
- Save-state commands after configuration changes to persist settings
  across power cycles

The legacy SRW-S1 racing wheel controller support is preserved
unchanged.

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

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

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


^ permalink raw reply related

* [PATCH v2 2/4] HID: quirks: Add INPUT_CONFIGURED quirk for SteelSeries Arctis headsets
From: Sriman Achanta @ 2026-01-12  4:19 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires, linux-input, linux-kernel; +Cc: Sriman Achanta
In-Reply-To: <20260112041941.40531-1-srimanachanta@gmail.com>

Add HID_QUIRK_INPUT_CONFIGURED for all SteelSeries Arctis headsets that
require the hid-steelseries driver. This quirk ensures proper device
initialization and prevents conflicts with generic HID drivers.

The quirk is necessary because these devices expose multiple HID
interfaces, and the hid-steelseries driver needs to bind to specific
interfaces based on the device capabilities. Without this quirk, the
generic HID driver may interfere with device-specific functionality like
battery monitoring and feature controls.

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

diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c
index c89a015686c0..8a7c3f433040 100644
--- a/drivers/hid/hid-quirks.c
+++ b/drivers/hid/hid-quirks.c
@@ -699,7 +699,32 @@ static const struct hid_device_id hid_have_special_driver[] = {
 #if IS_ENABLED(CONFIG_HID_STEELSERIES)
 	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_SRWS1) },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_1) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_7) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_7_P) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_7_X) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_7_GEN2) },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_9) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_PRO) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_P) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_X) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_DESTINY) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_P) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_X) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5_X) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_P) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_REV2) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_WOW) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_GEN2) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_PRO) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_PRO_X) },
 #endif
 #if IS_ENABLED(CONFIG_HID_SUNPLUS)
 	{ HID_USB_DEVICE(USB_VENDOR_ID_SUNPLUS, USB_DEVICE_ID_SUNPLUS_WDESKTOP) },
-- 
2.52.0


^ permalink raw reply related

* [PATCH v2 3/4] Documentation: ABI: Document SteelSeries headset sysfs attributes
From: Sriman Achanta @ 2026-01-12  4:19 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires, linux-input, linux-kernel; +Cc: Sriman Achanta
In-Reply-To: <20260112041941.40531-1-srimanachanta@gmail.com>

Replace the SRW-S1 specific documentation with comprehensive
documentation for the hid-steelseries driver covering both the legacy
SRW-S1 wheel and the modern Arctis headset lineup.

New sysfs attributes documented:
- sidetone_level: Control microphone monitoring volume
- inactive_time: Auto-sleep timeout configuration
- chatmix_level: Game/Chat audio balance (read-only)
- mic_mute_led_brightness: Microphone mute LED brightness control
- mic_volume: Internal microphone gain control
- volume_limiter: EU hearing protection volume limiter
- bluetooth_on_power: Bluetooth auto-enable on power-on
- bluetooth_call_vol: Bluetooth call audio attenuation settings

The SRW-S1 LED documentation is preserved and moved into the new unified
documentation file.

Signed-off-by: Sriman Achanta <srimanachanta@gmail.com>
---
 .../ABI/testing/sysfs-driver-hid-srws1        |  21 ---
 .../ABI/testing/sysfs-driver-hid-steelseries  | 131 ++++++++++++++++++
 2 files changed, 131 insertions(+), 21 deletions(-)
 delete mode 100644 Documentation/ABI/testing/sysfs-driver-hid-srws1
 create mode 100644 Documentation/ABI/testing/sysfs-driver-hid-steelseries

diff --git a/Documentation/ABI/testing/sysfs-driver-hid-srws1 b/Documentation/ABI/testing/sysfs-driver-hid-srws1
deleted file mode 100644
index d0eba70c7d40..000000000000
--- a/Documentation/ABI/testing/sysfs-driver-hid-srws1
+++ /dev/null
@@ -1,21 +0,0 @@
-What:		/sys/class/leds/SRWS1::<serial>::RPM1
-What:		/sys/class/leds/SRWS1::<serial>::RPM2
-What:		/sys/class/leds/SRWS1::<serial>::RPM3
-What:		/sys/class/leds/SRWS1::<serial>::RPM4
-What:		/sys/class/leds/SRWS1::<serial>::RPM5
-What:		/sys/class/leds/SRWS1::<serial>::RPM6
-What:		/sys/class/leds/SRWS1::<serial>::RPM7
-What:		/sys/class/leds/SRWS1::<serial>::RPM8
-What:		/sys/class/leds/SRWS1::<serial>::RPM9
-What:		/sys/class/leds/SRWS1::<serial>::RPM10
-What:		/sys/class/leds/SRWS1::<serial>::RPM11
-What:		/sys/class/leds/SRWS1::<serial>::RPM12
-What:		/sys/class/leds/SRWS1::<serial>::RPM13
-What:		/sys/class/leds/SRWS1::<serial>::RPM14
-What:		/sys/class/leds/SRWS1::<serial>::RPM15
-What:		/sys/class/leds/SRWS1::<serial>::RPMALL
-Date:		Jan 2013
-KernelVersion:	3.9
-Contact:	Simon Wood <simon@mungewell.org>
-Description:	Provides a control for turning on/off the LEDs which form
-		an RPM meter on the front of the controller
diff --git a/Documentation/ABI/testing/sysfs-driver-hid-steelseries b/Documentation/ABI/testing/sysfs-driver-hid-steelseries
new file mode 100644
index 000000000000..751cf01ceda3
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-driver-hid-steelseries
@@ -0,0 +1,131 @@
+What:		/sys/class/leds/SRWS1::<serial>::RPM1
+What:		/sys/class/leds/SRWS1::<serial>::RPM2
+What:		/sys/class/leds/SRWS1::<serial>::RPM3
+What:		/sys/class/leds/SRWS1::<serial>::RPM4
+What:		/sys/class/leds/SRWS1::<serial>::RPM5
+What:		/sys/class/leds/SRWS1::<serial>::RPM6
+What:		/sys/class/leds/SRWS1::<serial>::RPM7
+What:		/sys/class/leds/SRWS1::<serial>::RPM8
+What:		/sys/class/leds/SRWS1::<serial>::RPM9
+What:		/sys/class/leds/SRWS1::<serial>::RPM10
+What:		/sys/class/leds/SRWS1::<serial>::RPM11
+What:		/sys/class/leds/SRWS1::<serial>::RPM12
+What:		/sys/class/leds/SRWS1::<serial>::RPM13
+What:		/sys/class/leds/SRWS1::<serial>::RPM14
+What:		/sys/class/leds/SRWS1::<serial>::RPM15
+What:		/sys/class/leds/SRWS1::<serial>::RPMALL
+Date:		Jan 2013
+KernelVersion:	3.9
+Contact:	Simon Wood <simon@mungewell.org>
+Description:	Provides a control for turning on/off the LEDs which form
+		an RPM meter on the front of the controller
+
+What:		/sys/class/hid/drivers/steelseries/<dev>/sidetone_level
+Date:		January 2025
+KernelVersion:	6.19
+Contact:	Sriman Achanta <srimanachanta@gmail.com>
+Description:
+		Controls the sidetone (microphone monitoring) volume level.
+		This determines how much of the microphone input is fed back into
+		the headset speakers.
+
+		Range: 0-128 (mapped internally to device-specific values).
+
+		Access: Write
+
+What:		/sys/class/hid/drivers/steelseries/<dev>/inactive_time
+Date:		January 2025
+KernelVersion:	6.19
+Contact:	Sriman Achanta <srimanachanta@gmail.com>
+Description:
+		Sets the time in minutes before the headset automatically enters
+		standby/sleep mode when no audio is playing.
+
+		Range: 0-90 (minutes).
+		       Some devices (e.g., Arctis 1/7X) map this to specific presets.
+
+		Access: Write
+
+What:		/sys/class/hid/drivers/steelseries/<dev>/chatmix_level
+Date:		January 2025
+KernelVersion:	6.19
+Contact:	Sriman Achanta <srimanachanta@gmail.com>
+Description:
+		Reports the current balance between Game and Chat audio channels
+		(ChatMix). This value changes when the physical ChatMix dial
+		on the headset is adjusted.
+
+		Range: 0-128
+		       0   = 100% Chat / 0% Game
+		       64  = 50% Chat / 50% Game (Balanced)
+		       128 = 0% Chat / 100% Game
+
+		Access: Read
+
+What:		/sys/class/hid/drivers/steelseries/<dev>/mic_mute_led_brightness
+Date:		January 2025
+KernelVersion:	6.19
+Contact:	Sriman Achanta <srimanachanta@gmail.com>
+Description:
+		Controls the brightness of the LED on the microphone boom that
+		indicates when the microphone is muted.
+
+		Range: 0-3 (off, low, medium, high) for most devices.
+		       0-10 for newer Nova series devices.
+
+		Access: Write
+
+What:		/sys/class/hid/drivers/steelseries/<dev>/mic_volume
+Date:		January 2025
+KernelVersion:	6.19
+Contact:	Sriman Achanta <srimanachanta@gmail.com>
+Description:
+		Controls the internal microphone gain/volume of the headset.
+		This is distinct from the OS input volume.
+
+		Range: 0-128 (mapped internally to device-specific values).
+
+		Access: Write
+
+What:		/sys/class/hid/drivers/steelseries/<dev>/volume_limiter
+Date:		January 2025
+KernelVersion:	6.19
+Contact:	Sriman Achanta <srimanachanta@gmail.com>
+Description:
+		Enables or disables the EU volume limiter (hearing protection).
+		When enabled, the maximum output volume is capped.
+
+		Values:
+		0 = Disabled
+		1 = Enabled
+
+		Access: Write
+
+What:		/sys/class/hid/drivers/steelseries/<dev>/bluetooth_on_power
+Date:		January 2025
+KernelVersion:	6.19
+Contact:	Sriman Achanta <srimanachanta@gmail.com>
+Description:
+		Configures whether the Bluetooth radio automatically turns on
+		when the headset is powered on.
+
+		Values:
+		0 = Bluetooth must be turned on manually
+		1 = Bluetooth turns on automatically with headset
+
+		Access: Write
+
+What:		/sys/class/hid/drivers/steelseries/<dev>/bluetooth_call_vol
+Date:		January 2025
+KernelVersion:	6.19
+Contact:	Sriman Achanta <srimanachanta@gmail.com>
+Description:
+		Configures how the 2.4GHz Game/Chat audio is attenuated when
+		a Bluetooth call is active.
+
+		Values:
+		0 = No attenuation (mix both equally)
+		1 = Attenuate Game audio by -12dB
+		2 = Mute Game audio completely
+
+		Access: Write
-- 
2.52.0


^ permalink raw reply related

* [PATCH v2 1/4] HID: hid-ids: Add SteelSeries Arctis headset device IDs
From: Sriman Achanta @ 2026-01-12  4:19 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires, linux-input, linux-kernel; +Cc: Sriman Achanta
In-Reply-To: <20260112041941.40531-1-srimanachanta@gmail.com>

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

This also fixes the existing ARCTIS_1 ID to use the correct product ID
(0x12b3 instead of 0x12b6, which is actually the Arctis 1 Xbox variant).

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

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

diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index d31711f1aaec..f4f91fb4c2b9 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1303,10 +1303,35 @@
 #define USB_DEVICE_ID_STEAM_CONTROLLER_WIRELESS	0x1142
 #define USB_DEVICE_ID_STEAM_DECK	0x1205
 
-#define USB_VENDOR_ID_STEELSERIES	0x1038
-#define USB_DEVICE_ID_STEELSERIES_SRWS1	0x1410
-#define USB_DEVICE_ID_STEELSERIES_ARCTIS_1  0x12b6
-#define USB_DEVICE_ID_STEELSERIES_ARCTIS_9  0x12c2
+#define USB_VENDOR_ID_STEELSERIES			0x1038
+#define USB_DEVICE_ID_STEELSERIES_SRWS1			0x1410
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_1		0x12b3
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X		0x12b6
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_7		0x1260
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_7_P		0x12d5
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_7_X		0x12d7
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_7_GEN2		0x12ad
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS		0x220e
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_P	0x2212
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_X	0x2216
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_7_PLUS_DESTINY	0x2236
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_9		0x12c2
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_PRO		0x1290
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3		0x12ec
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_P	0x2269
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_3_X	0x226d
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5		0x2232
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5_X	0x2253
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7		0x2202
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X	0x2206
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_P	0x220a
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_REV2	0x2258
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO	0x223a
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_WOW	0x227a
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_GEN2	0x227e
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2	0x229e
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_PRO	0x12e0
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_PRO_X	0x12e5
 
 #define USB_VENDOR_ID_SUN		0x0430
 #define USB_DEVICE_ID_RARITAN_KVM_DONGLE	0xcdab
-- 
2.52.0


^ permalink raw reply related

* [PATCH v2 0/4] HID: steelseries: Add support for Arctis headset lineup
From: Sriman Achanta @ 2026-01-12  4:19 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires, linux-input, linux-kernel; +Cc: Sriman Achanta

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

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

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

Patch 1: Add 27 new device IDs to hid-ids.h
Patch 2: Add HID quirks for proper device initialization 
Patch 3: Update ABI documentation for new sysfs attributes
Patch 4: Complete driver implementation with all features

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

V2:
- Fix Documentation formatting issues

Sriman Achanta (4):
  HID: hid-ids: Add SteelSeries Arctis headset device IDs
  HID: quirks: Add INPUT_CONFIGURED quirk for SteelSeries Arctis
    headsets
  Documentation: ABI: Document SteelSeries headset sysfs attributes
  HID: steelseries: Add support for Arctis headset lineup

 .../ABI/testing/sysfs-driver-hid-srws1        |   21 -
 .../ABI/testing/sysfs-driver-hid-steelseries  |  131 ++
 drivers/hid/hid-ids.h                         |   33 +-
 drivers/hid/hid-quirks.c                      |   25 +
 drivers/hid/hid-steelseries.c                 | 2061 ++++++++++++++---
 5 files changed, 1925 insertions(+), 346 deletions(-)
 delete mode 100644 Documentation/ABI/testing/sysfs-driver-hid-srws1
 create mode 100644 Documentation/ABI/testing/sysfs-driver-hid-steelseries

-- 
2.52.0


^ permalink raw reply

* Re: [PATCH] Input: goodix - fix inverted Y coordinate on SUPI S10
From: Hans de Goede @ 2026-01-11 21:30 UTC (permalink / raw)
  To: Yajat Kumar, Dmitry Torokhov, open list:GOODIX TOUCHSCREEN,
	open list
In-Reply-To: <20251230221639.582406-1-yajatapps3@gmail.com>

[-- Attachment #1: Type: text/plain, Size: 2373 bytes --]

Hi Yajat,

On 30-Dec-25 23:16, Yajat Kumar wrote:
> The touchscreen on the SUPI S10 reports inverted Y coordinates, causing
> touch input to be mirrored vertically relative to the display.
> 
> Add a DMI-based quirk to invert the Y coordinate on this device so that
> touch input matches the display orientation.
> 
> Tested on SUPI S10 tablet with Goodix touchscreen controller.
> 
> Signed-off-by: Yajat Kumar <yajatapps3@gmail.com>

Thank you for your patch. This kind of quirks really belong
in drivers/platform/x86/touchscreen_dmi.c instead of in individual
touchscreen drivers.

The inverted_x_screen[] DMI quirk is a left-over from before we
moved these quirks to touchscreen_dmi.c and unfortunately we cannot
move this because we've not been able to find someone to test this.

I've attached a patch which should fix the issue using
touchscreen_dmi.c . Note you may need to change the GDIX1001 in
the patch to GDIX1002, see "ls /sys/bus/i2c/devices" to see which
ACPI HID your touchscreen is using.

Can you please test the attached patch ?

Regards,

Hans



> ---
>  drivers/input/touchscreen/goodix.c | 22 ++++++++++++++++++++++
>  1 file changed, 22 insertions(+)
> 
> diff --git a/drivers/input/touchscreen/goodix.c b/drivers/input/touchscreen/goodix.c
> index f8798d11ec03..d675a85a9312 100644
> --- a/drivers/input/touchscreen/goodix.c
> +++ b/drivers/input/touchscreen/goodix.c
> @@ -160,6 +160,22 @@ static const struct dmi_system_id inverted_x_screen[] = {
>  	{}
>  };
>  
> +/*
> + * Those tablets have their y coordinate inverted
> + */
> +static const struct dmi_system_id inverted_y_screen[] = {
> +#if defined(CONFIG_DMI) && defined(CONFIG_X86)
> +	{
> +		.ident = "SUPI S10",
> +		.matches = {
> +			DMI_MATCH(DMI_SYS_VENDOR, "SUPI"),
> +			DMI_MATCH(DMI_PRODUCT_NAME, "S10")
> +		},
> +	},
> +#endif
> +	{}
> +};
> +
>  /**
>   * goodix_i2c_read - read data from a register of the i2c slave device.
>   *
> @@ -1212,6 +1228,12 @@ static int goodix_configure_dev(struct goodix_ts_data *ts)
>  			"Applying 'inverted x screen' quirk\n");
>  	}
>  
> +	if (dmi_check_system(inverted_y_screen)) {
> +		ts->prop.invert_y = true;
> +		dev_dbg(&ts->client->dev,
> +			"Applying 'inverted y screen' quirk\n");
> +	}
> +
>  	error = input_mt_init_slots(ts->input_dev, ts->max_touch_num,
>  				    INPUT_MT_DIRECT | INPUT_MT_DROP_UNUSED);
>  	if (error) {

[-- Attachment #2: 0001-platform-x86-touchscreen_dmi-Add-quirk-for-y-inverte.patch --]
[-- Type: text/x-patch, Size: 2074 bytes --]

From 5df9d895db2ff3b3ec58a6b0c9c3a09d3c852b29 Mon Sep 17 00:00:00 2001
From: Hans de Goede <johannes.goede@oss.qualcomm.com>
Date: Sun, 11 Jan 2026 22:25:58 +0100
Subject: [PATCH] platform/x86: touchscreen_dmi: Add quirk for y-inverted
 Goodix touchscreen on SUPI S10

The touchscreen on the SUPI S10 tablet reports inverted Y coordinates,
causing touch input to be mirrored vertically relative to the display.

Add a quirk to set the "touchscreen-inverted-y" boolean device-property
on the touchscreen device, so that the goodix_ts driver will fixup
the coordinates.

Reported-by: Yajat Kumar <yajatapps3@gmail.com>
Closes: https://lore.kernel.org/linux-input/20251230221639.582406-1-yajatapps3@gmail.com/
Signed-off-by: Hans de Goede <johannes.goede@oss.qualcomm.com>
---
 drivers/platform/x86/touchscreen_dmi.c | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

diff --git a/drivers/platform/x86/touchscreen_dmi.c b/drivers/platform/x86/touchscreen_dmi.c
index bdc19cd8d3ed..d83c387821ea 100644
--- a/drivers/platform/x86/touchscreen_dmi.c
+++ b/drivers/platform/x86/touchscreen_dmi.c
@@ -410,6 +410,16 @@ static const struct ts_dmi_data gdix1002_upside_down_data = {
 	.properties	= gdix1001_upside_down_props,
 };
 
+static const struct property_entry gdix1001_y_inverted_props[] = {
+	PROPERTY_ENTRY_BOOL("touchscreen-inverted-y"),
+	{ }
+};
+
+static const struct ts_dmi_data gdix1001_y_inverted_data = {
+	.acpi_name	= "GDIX1001",
+	.properties	= gdix1001_y_inverted_props,
+};
+
 static const struct property_entry gp_electronic_t701_props[] = {
 	PROPERTY_ENTRY_U32("touchscreen-size-x", 960),
 	PROPERTY_ENTRY_U32("touchscreen-size-y", 640),
@@ -1658,6 +1668,14 @@ const struct dmi_system_id touchscreen_dmi_table[] = {
 			DMI_MATCH(DMI_PRODUCT_SKU, "PN20170413488"),
 		},
 	},
+	{
+		/* SUPI S10 */
+		.driver_data = (void *)&gdix1001_y_inverted_data,
+		.matches = {
+			DMI_MATCH(DMI_SYS_VENDOR, "SUPI"),
+			DMI_MATCH(DMI_PRODUCT_NAME, "S10"),
+		},
+	},
 	{
 		/* Techbite Arc 11.6 */
 		.driver_data = (void *)&techbite_arc_11_6_data,
-- 
2.52.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