* [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
* Re: [PATCH v2 1/4] HID: hid-ids: Add SteelSeries Arctis headset device IDs
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-2-srimanachanta@gmail.com>
On Sun, 2026-01-11 at 23:19 -0500, Sriman Achanta wrote:
> 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).
"This also fixes" usually is a good way to tell you that this should
have been a separate patch.
It would be useful if you could change the indentation in a first
patch, "fix" the USB ID in a second patch, and add new IDs in a third
patch. Note that in your second patch, you'll need to change the source
code to use that new identifier otherwise you'll be breaking my headset
:)
Please make sure to CC: me on future patchsets you send.
>
> 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
^ permalink raw reply
* Re: [PATCH v2 4/4] HID: steelseries: Add support for Arctis headset lineup
From: Bastien Nocera @ 2026-01-12 13:09 UTC (permalink / raw)
To: Sriman Achanta, Jiri Kosina, Benjamin Tissoires, linux-input,
linux-kernel
In-Reply-To: <20260112041941.40531-5-srimanachanta@gmail.com>
On Sun, 2026-01-11 at 23:19 -0500, Sriman Achanta wrote:
> 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.
This patch needs to be split up, at the very least it needs new
features to be split up from any other refactoring that might be needed
to support features with each new feature getting its own commit.
As mentioned in the earlier patch, sidetone control, chatmix level, mic
level, volume limiter and bluetooth call volume all should be
implemented as ALSA mixers/switches so they can be toggled with stock
tools and presented in a uniform way up the stack (Pipewire/Pulseaudio
and desktop environments).
An additional comments inline.
> <snip>
> -/* 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
> */
There's really no need to reindent this array.
> 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 */
> };
^ permalink raw reply
* Subscribe Linux-input
From: Paul MORRIS @ 2026-01-12 13:49 UTC (permalink / raw)
To: linux-input, majordomo
In-Reply-To: <5EB0F81A-202E-4FBC-9018-326E896D286C.ref@aol.com>
Subscribe Linux-input
Regards
^ 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