* 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
* [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
* [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 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 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 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
* 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 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
* [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 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 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 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 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 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 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 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 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 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 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 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 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 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
* 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
* 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 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
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox