* [PATCH 4/4] HID: steelseries: Add support for Arctis headset lineup
From: Sriman Achanta @ 2025-12-28 12:20 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, linux-input, linux-kernel; +Cc: Sriman Achanta
In-Reply-To: <20251228122025.154682-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 RESEND 2/2] Input: psmouse - Replace flush_workqueue() with disable_delayed_work_sync()
From: Dmitry Torokhov @ 2025-12-28 23:15 UTC (permalink / raw)
To: Andy Shevchenko
Cc: duoming, linux-input, linux-kernel, pali, kuba, alexander.deucher,
akpm, johannes.berg, pkshih, hverkuil+cisco, tglx, mingo
In-Reply-To: <aVACDawiYQDdQkhu@smile.fi.intel.com>
On Sat, Dec 27, 2025 at 05:58:05PM +0200, Andy Shevchenko wrote:
> On Thu, Dec 18, 2025 at 10:25:27PM +0800, duoming@zju.edu.cn wrote:
> > On Wed, 17 Dec 2025 22:58:33 -0800 Dmitry Torokhov wrote:
>
> ...
>
> > > What do you mean? If you enable OLPC support the option to enable hgpk
> > > protocol driver should become available, right?
> >
> > The hgpk_init() function is never called by any other function in the
> > kernel and is therefore dead code. Since the delayed work item recalib_wq
> > is initialized within this function, it is consequently never scheduled
> > during runtime.
>
> Actually I confirm this as of v6.19-rc2. Perhaps some patch removed some
> functionality in the past and we may remove more now?
Yeah, it looks like I broke it with c378b5119eb0 ("Input: psmouse -
factor out common protocol probing code") in 2015. While it should be
easy to fix (by adding hgkp_init() to the protocol definition) I think
we should drop hgkp extended support since nobody complained in 10
years.
Thanks.
--
Dmitry
^ permalink raw reply
* Re: [PATCH 3/4] Documentation: ABI: Document SteelSeries headset sysfs attributes
From: kernel test robot @ 2025-12-29 1:42 UTC (permalink / raw)
To: Sriman Achanta, Jiri Kosina, Benjamin Tissoires, linux-input,
linux-kernel
Cc: oe-kbuild-all, Sriman Achanta
In-Reply-To: <20251228122025.154682-4-srimanachanta@gmail.com>
Hi Sriman,
kernel test robot noticed the following build warnings:
[auto build test WARNING on hid/for-next]
[also build test WARNING on linus/master v6.19-rc3 next-20251219]
[If your patch is applied to the wrong git tree, kindly drop us a note.
And when submitting patch, we suggest to use '--base' as documented in
https://git-scm.com/docs/git-format-patch#_base_tree_information]
url: https://github.com/intel-lab-lkp/linux/commits/Sriman-Achanta/HID-hid-ids-Add-SteelSeries-Arctis-headset-device-IDs/20251228-202426
base: https://git.kernel.org/pub/scm/linux/kernel/git/hid/hid.git for-next
patch link: https://lore.kernel.org/r/20251228122025.154682-4-srimanachanta%40gmail.com
patch subject: [PATCH 3/4] Documentation: ABI: Document SteelSeries headset sysfs attributes
reproduce: (https://download.01.org/0day-ci/archive/20251229/202512290229.oFaoatlF-lkp@intel.com/reproduce)
If you fix the issue in a separate patch/commit (i.e. not just a new version of
the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <lkp@intel.com>
| Closes: https://lore.kernel.org/oe-kbuild-all/202512290229.oFaoatlF-lkp@intel.com/
All warnings (new ones prefixed by >>):
Using alabaster theme
ERROR: Cannot find file ./include/linux/pci.h
WARNING: No kernel-doc for file ./include/linux/pci.h
ERROR: Cannot find file ./include/linux/mod_devicetable.h
WARNING: No kernel-doc for file ./include/linux/mod_devicetable.h
>> Documentation/ABI/testing/sysfs-driver-hid-steelseries:47: WARNING: Definition list ends without a blank line; unexpected unindent. [docutils]
Documentation/ABI/testing/sysfs-driver-hid-steelseries:35: WARNING: Definition list ends without a blank line; unexpected unindent. [docutils]
Documentation/ABI/testing/sysfs-driver-hid-steelseries:62: WARNING: Definition list ends without a blank line; unexpected unindent. [docutils]
ERROR: Cannot find file ./include/linux/bootconfig.h
WARNING: No kernel-doc for file ./include/linux/bootconfig.h
ERROR: Cannot find file ./include/linux/pstore_zone.h
vim +47 Documentation/ABI/testing/sysfs-driver-hid-steelseries
> 47 What: /sys/class/hid/drivers/steelseries/<dev>/chatmix_level
48 Date: January 2025
49 KernelVersion: 6.19
50 Contact: Sriman Achanta <srimanachanta@gmail.com>
51 Description:
52 Reports the current balance between Game and Chat audio channels
53 (ChatMix). This value changes when the physical ChatMix dial
54 on the headset is adjusted.
55
56 Range: 0-128
57 0 = 100% Chat / 0% Game
58 64 = 50% Chat / 50% Game (Balanced)
59 128 = 0% Chat / 100% Game
60 Access: Read
61
--
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki
^ permalink raw reply
* [PATCH v2 00/16] HID: Add Legion Go and Go S Drivers
From: Derek J. Clark @ 2025-12-29 3:17 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Mario Limonciello, Zhixin Zhang, Mia Shao, Mark Pearson,
Pierre-Loup A . Griffais, Derek J . Clark, linux-input, linux-doc,
linux-kernel
This series adds configuration driver support for the Legion Go S,
Legion Go, and Legion Go 2 built-in controller HID interfaces. This
allows for configuring hardware specific attributes such as the auso
sleep timeout, rumble intensity, etc. non-configuration reports are
forwarded to the HID subsystem to ensure no loss of functionality in
userspace. Basic gamepad functionality is provided through xpad, while
advanced features are currently only implemented in userspace daemons
such as InputPlumber[1]. I plan to move this functionality into the
kernel in a later patch series.
Three new device.h macros are added that solve a fairly specific
problem. Many of the attributes need to have the same name as other
attributes when they are in separate attribute subdirectories. The
previous version of this series, along with the upcoming his-asus-ally
driver[2] use this macro to simplify the sysfs by removing redundancy.
An upcoming out of tree driver for the Zotac Zone [3] also found this
macro to be useful. This greatly reduces the path length and term
redundancy of file paths in the sysfs, while also allowing for cleaner
subdirectories that are grouped by functionality. Rather than carry the
same macro in four drivers, it seems beneficial to me that we include the
macro with the other device macros.
A new HID uevent property is also added, HID_FIRMWARE_VERSION, so as to
permit fwupd to read the firmware version of the Go S HID interface without
detaching the kernel driver.
Finally, there are some checkpatch warnings that will need to be surpressed:
WARNING: ENOSYS means 'invalid syscall nr' and nothing else
1292: FILE: drivers/hid/lenovo-legos-hid/lenovo-legos-hid-config.c:1085:
+ case -ENOSYS: /* during rmmod -ENOSYS is expected */
This error handling case was added as it is experienced in the real world
when the driver is rmmod. The LED subsystem produces this error code in
its legacy code and this is not a new novel use of -ENOSYS, we are simply
catching the case to avoid spurious errors in dmesg when the drivers are
removed.
[1]: https://github.com/ShadowBlip/InputPlumber/tree/main/src/drivers/lego
[2]: https://lore.kernel.org/all/20240806081212.56860-1-luke@ljones.dev/
[3]: https://github.com/flukejones/linux/tree/wip/zotac-zone-6.15/drivers/hid/zotac-zone-hid
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
Change Log
V2:
- Break up adding the Go S driver into feature specific patches
- Drop the arbitrary uevent properties patch
- Add Go serires driver
- Move DEVICE_ATTR_NAMED macros to device.h
V1:
Derek J. Clark (15):
include: device.h: Add named device attributes
HID: hid-lenovo-go: Add Lenovo Legion Go Series HID Driver
HID: hid-lenovo-go: Add Feature Status Attributes
HID: hid-lenovo-go: Add Rumble and Haptic Settings
HID: hid-lenovo-go: Add FPS Mode DPI settings
HID: hid-lenovo-go: Add RGB LED control interface
HID: hid-lenovo-go: Add Calibration Settings
HID: hid-lenovo-go: Add OS Mode Toggle
HID: hid-lenovo-go-s: Add Lenovo Legion Go S Series HID Driver
HID: hid-lenovo-go-s: Add MCU ID Attribute
HID: hid-lenovo-go-s: Add Feature Status Attributes
HID: hid-lenovo-go-s: Add Touchpad Mode Attributes
HID: hid-lenovo-go-s: Add RGB LED control interface
HID: hid-lenovo-go-s: Add IMU and Touchpad RO Attributes
HID: Add documentation for Lenovo Legion Go drivers
Mario Limonciello (1):
HID: Include firmware version in the uevent
.../ABI/testing/sysfs-driver-hid-lenovo-go | 724 +++++
.../ABI/testing/sysfs-driver-hid-lenovo-go-s | 304 +++
MAINTAINERS | 9 +
drivers/hid/Kconfig | 24 +
drivers/hid/Makefile | 2 +
drivers/hid/hid-core.c | 5 +
drivers/hid/hid-ids.h | 7 +
drivers/hid/hid-lenovo-go-s.c | 1577 +++++++++++
drivers/hid/hid-lenovo-go.c | 2399 +++++++++++++++++
include/linux/device.h | 46 +
include/linux/hid.h | 1 +
11 files changed, 5098 insertions(+)
create mode 100644 Documentation/ABI/testing/sysfs-driver-hid-lenovo-go
create mode 100644 Documentation/ABI/testing/sysfs-driver-hid-lenovo-go-s
create mode 100644 drivers/hid/hid-lenovo-go-s.c
create mode 100644 drivers/hid/hid-lenovo-go.c
--
2.51.2
^ permalink raw reply
* [PATCH v2 01/16] include: device.h: Add named device attributes
From: Derek J. Clark @ 2025-12-29 3:17 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Mario Limonciello, Zhixin Zhang, Mia Shao, Mark Pearson,
Pierre-Loup A . Griffais, Derek J . Clark, linux-input, linux-doc,
linux-kernel
In-Reply-To: <20251229031753.581664-1-derekjohn.clark@gmail.com>
Adds DEVICE_ATTR_[RW|RO|WO]_NAMED macros for adding attributes that
reuse the same sysfs name in a driver under separate subdirectories.
When dealing with some devices it can be useful to be able to reuse
the same name for similar attributes under a different subdirectory.
For example, a single logical HID endpoint may provide a configuration
interface for multiple physical devices. In such a case it is useful to
provide symmetrical attribute names under different subdirectories on
the configuration device. The Lenovo Legion Go is one such device,
providing configuration to a detachable left controller, detachable
right controller, the wireless transmission dongle, and the MCU. It is
therefore beneficial to treat each of these as individual devices in
the driver, providing a subdirectory for each physical device in the
sysfs. As some attributes are reused by each physical device, it
provides a much cleaner interface if the same driver can reuse the same
attribute name in sysfs while uniquely distinguishing the store/show
functions in the driver, rather than repeat string portions.
Example new WO attrs:
ATTRS{left_handle/reset}=="(not readable)"
ATTRS{right_handle/reset}=="(not readable)"
ATTRS{tx_dongle/reset}=="(not readable)"
vs old WO attrs in a subdir:
ATTRS{left_handle/left_handle_reset}=="(not readable)"
ATTRS{right_handle/right_handle_reset}=="(not readable)"
ATTRS{tx_dongle/tx_dongle_reset}=="(not readable)"
or old WO attrs with no subdir:
ATTRS{left_handle_reset}=="(not readable)"
ATTRS{right_handle_reset}=="(not readable)"
ATTRS{tx_dongle_reset}=="(not readable)"
While the third option is usable, it doesn't logically break up the
physical devices and creates a device directory with over 80 attributes
once all attrs are defined.
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
include/linux/device.h | 46 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 46 insertions(+)
diff --git a/include/linux/device.h b/include/linux/device.h
index b031ff71a5bd..76f485e6b24c 100644
--- a/include/linux/device.h
+++ b/include/linux/device.h
@@ -189,6 +189,22 @@ ssize_t device_show_string(struct device *dev, struct device_attribute *attr,
#define DEVICE_ATTR_ADMIN_RW(_name) \
struct device_attribute dev_attr_##_name = __ATTR_RW_MODE(_name, 0600)
+/**
+ * DEVICE_ATTR_RW_NAMED - Define a read-write device attribute with a sysfs name
+ * that differs from the function name.
+ * @_name: Attribute function preface
+ * @_attrname: Attribute name as it wil be exposed in the sysfs.
+ *
+ * Like DEVICE_ATTR_RW(), but allows for reusing names under separate paths in
+ * the same driver.
+ */
+#define DEVICE_ATTR_RW_NAMED(_name, _attrname) \
+ struct device_attribute dev_attr_##_name = { \
+ .attr = { .name = _attrname, .mode = 0644 }, \
+ .show = _name##_show, \
+ .store = _name##_store, \
+ }
+
/**
* DEVICE_ATTR_RO - Define a readable device attribute.
* @_name: Attribute name.
@@ -207,6 +223,21 @@ ssize_t device_show_string(struct device *dev, struct device_attribute *attr,
#define DEVICE_ATTR_ADMIN_RO(_name) \
struct device_attribute dev_attr_##_name = __ATTR_RO_MODE(_name, 0400)
+/**
+ * DEVICE_ATTR_RO_NAMED - Define a read-only device attribute with a sysfs name
+ * that differs from the function name.
+ * @_name: Attribute function preface
+ * @_attrname: Attribute name as it wil be exposed in the sysfs.
+ *
+ * Like DEVICE_ATTR_RO(), but allows for reusing names under separate paths in
+ * the same driver.
+ */
+#define DEVICE_ATTR_RO_NAMED(_name, _attrname) \
+ struct device_attribute dev_attr_##_name = { \
+ .attr = { .name = _attrname, .mode = 0444 }, \
+ .show = _name##_show, \
+ }
+
/**
* DEVICE_ATTR_WO - Define an admin-only writable device attribute.
* @_name: Attribute name.
@@ -216,6 +247,21 @@ ssize_t device_show_string(struct device *dev, struct device_attribute *attr,
#define DEVICE_ATTR_WO(_name) \
struct device_attribute dev_attr_##_name = __ATTR_WO(_name)
+/**
+ * DEVICE_ATTR_WO_NAMED - Define a read-only device attribute with a sysfs name
+ * that differs from the function name.
+ * @_name: Attribute function preface
+ * @_attrname: Attribute name as it wil be exposed in the sysfs.
+ *
+ * Like DEVICE_ATTR_WO(), but allows for reusing names under separate paths in
+ * the same driver.
+ */
+#define DEVICE_ATTR_WO_NAMED(_name, _attrname) \
+ struct device_attribute dev_attr_##_name = { \
+ .attr = { .name = _attrname, .mode = 0200 }, \
+ .store = _name##_store, \
+ }
+
/**
* DEVICE_ULONG_ATTR - Define a device attribute backed by an unsigned long.
* @_name: Attribute name.
--
2.51.2
^ permalink raw reply related
* [PATCH v2 02/16] HID: hid-lenovo-go: Add Lenovo Legion Go Series HID Driver
From: Derek J. Clark @ 2025-12-29 3:17 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Mario Limonciello, Zhixin Zhang, Mia Shao, Mark Pearson,
Pierre-Loup A . Griffais, Derek J . Clark, linux-input, linux-doc,
linux-kernel
In-Reply-To: <20251229031753.581664-1-derekjohn.clark@gmail.com>
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
+
LETSKETCH HID TABLET DRIVER
M: Hans de Goede <hansg@kernel.org>
L: linux-input@vger.kernel.org
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index 04420a713be0..74ac6793b29a 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -621,6 +621,18 @@ config HID_LENOVO
- ThinkPad Compact Bluetooth Keyboard with TrackPoint (supports Fn keys)
- ThinkPad Compact USB Keyboard with TrackPoint (supports Fn keys)
+config HID_LENOVO_GO
+ tristate "HID Driver for Lenovo Legion Go Series Controllers"
+ depends on USB_HID
+ select LEDS_CLASS
+ select LEDS_CLASS_MULTICOLOR
+ help
+ Support for Lenovo Legion Go devices with detachable controllers.
+
+ Say Y here to include configuration interface support for the Lenovo Legion Go
+ and Legion Go 2 Handheld Console Controllers. Say M here to compile this
+ driver as a module. The module will be called hid-lenovo-go.
+
config HID_LETSKETCH
tristate "Letsketch WP9620N tablets"
depends on USB_HID
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index 361a7daedeb8..11435bce4e47 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -76,6 +76,7 @@ obj-$(CONFIG_HID_KYE) += hid-kye.o
obj-$(CONFIG_HID_KYSONA) += hid-kysona.o
obj-$(CONFIG_HID_LCPOWER) += hid-lcpower.o
obj-$(CONFIG_HID_LENOVO) += hid-lenovo.o
+obj-$(CONFIG_HID_LENOVO_GO) += hid-lenovo-go.o
obj-$(CONFIG_HID_LETSKETCH) += hid-letsketch.o
obj-$(CONFIG_HID_LOGITECH) += hid-logitech.o
obj-$(CONFIG_HID_LOGITECH) += hid-lg-g15.o
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index c4589075a5ed..309e15580a38 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -841,7 +841,10 @@
#define USB_DEVICE_ID_LENOVO_PIXART_USB_MOUSE_602E 0x602e
#define USB_DEVICE_ID_LENOVO_PIXART_USB_MOUSE_6093 0x6093
#define USB_DEVICE_ID_LENOVO_LEGION_GO_DUAL_DINPUT 0x6184
+#define USB_DEVICE_ID_LENOVO_LEGION_GO2_XINPUT 0x61eb
+#define USB_DEVICE_ID_LENOVO_LEGION_GO2_DINPUT 0x61ec
#define USB_DEVICE_ID_LENOVO_LEGION_GO2_DUAL_DINPUT 0x61ed
+#define USB_DEVICE_ID_LENOVO_LEGION_GO2_FPS 0x61ee
#define USB_VENDOR_ID_LETSKETCH 0x6161
#define USB_DEVICE_ID_WP9620N 0x4d15
diff --git a/drivers/hid/hid-lenovo-go.c b/drivers/hid/hid-lenovo-go.c
new file mode 100644
index 000000000000..6380434b2d89
--- /dev/null
+++ b/drivers/hid/hid-lenovo-go.c
@@ -0,0 +1,734 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * HID driver for Lenovo Legion Go series gamepads.
+ *
+ * Copyright (c) 2025 Valve Corporation
+ */
+
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/array_size.h>
+#include <linux/cleanup.h>
+#include <linux/completion.h>
+#include <linux/delay.h>
+#include <linux/dev_printk.h>
+#include <linux/device.h>
+#include <linux/device/devres.h>
+#include <linux/hid.h>
+#include <linux/jiffies.h>
+#include <linux/kstrtox.h>
+#include <linux/mutex.h>
+#include <linux/printk.h>
+#include <linux/sysfs.h>
+#include <linux/types.h>
+#include <linux/unaligned.h>
+#include <linux/usb.h>
+
+#include "hid-ids.h"
+
+#define GO_GP_INTF_IN 0x83
+#define GO_OUTPUT_REPORT_ID 0x05
+#define GO_GP_RESET_SUCCESS 0x01
+#define GO_PACKET_SIZE 64
+
+struct hid_go_cfg {
+ unsigned char *buf;
+ struct completion send_cmd_complete;
+ struct hid_device *hdev;
+ struct mutex cfg_mutex; /*ensure single synchronous output report*/
+ u32 gp_left_version_firmware;
+ u8 gp_left_version_gen;
+ u32 gp_left_version_hardware;
+ u32 gp_left_version_product;
+ u32 gp_left_version_protocol;
+ u32 gp_right_version_firmware;
+ u8 gp_right_version_gen;
+ u32 gp_right_version_hardware;
+ u32 gp_right_version_product;
+ u32 gp_right_version_protocol;
+ u32 mcu_version_firmware;
+ u8 mcu_version_gen;
+ u32 mcu_version_hardware;
+ u32 mcu_version_product;
+ u32 mcu_version_protocol;
+ u32 tx_dongle_version_firmware;
+ u8 tx_dongle_version_gen;
+ u32 tx_dongle_version_hardware;
+ u32 tx_dongle_version_product;
+ u32 tx_dongle_version_protocol;
+} drvdata;
+
+struct go_cfg_attr {
+ u8 index;
+};
+
+struct command_report {
+ u8 report_id;
+ u8 id;
+ u8 cmd;
+ u8 sub_cmd;
+ u8 device_type;
+ u8 data[59];
+} __packed;
+
+enum command_id {
+ MCU_CONFIG_DATA = 0x00,
+ OS_MODE_DATA = 0x06,
+ GAMEPAD_DATA = 0x3c,
+};
+
+enum mcu_command_index {
+ GET_VERSION_DATA = 0x02,
+ GET_FEATURE_STATUS,
+ SET_FEATURE_STATUS,
+ GET_MOTOR_CFG,
+ SET_MOTOR_CFG,
+ GET_DPI_CFG,
+ SET_DPI_CFG,
+ SET_TRIGGER_CFG = 0x0a,
+ SET_JOYSTICK_CFG = 0x0c,
+ SET_GYRO_CFG = 0x0e,
+ GET_RGB_CFG,
+ SET_RGB_CFG,
+ GET_DEVICE_STATUS = 0xa0,
+
+};
+
+enum dev_type {
+ UNSPECIFIED,
+ USB_MCU,
+ TX_DONGLE,
+ LEFT_CONTROLLER,
+ RIGHT_CONTROLLER,
+};
+
+enum version_data_index {
+ PRODUCT_VERSION = 0x02,
+ PROTOCOL_VERSION,
+ FIRMWARE_VERSION,
+ HARDWARE_VERSION,
+ HARDWARE_GENERATION,
+};
+
+static int hid_go_version_event(struct command_report *cmd_rep)
+{
+ switch (cmd_rep->sub_cmd) {
+ case PRODUCT_VERSION:
+ switch (cmd_rep->device_type) {
+ case USB_MCU:
+ drvdata.mcu_version_product =
+ get_unaligned_le32(cmd_rep->data);
+ return 0;
+ case TX_DONGLE:
+ drvdata.tx_dongle_version_product =
+ get_unaligned_le32(cmd_rep->data);
+ return 0;
+ case LEFT_CONTROLLER:
+ drvdata.gp_left_version_product =
+ get_unaligned_le32(cmd_rep->data);
+ return 0;
+ case RIGHT_CONTROLLER:
+ drvdata.gp_right_version_product =
+ get_unaligned_le32(cmd_rep->data);
+ return 0;
+ default:
+ return -EINVAL;
+ }
+ case PROTOCOL_VERSION:
+ switch (cmd_rep->device_type) {
+ case USB_MCU:
+ drvdata.mcu_version_protocol =
+ get_unaligned_le32(cmd_rep->data);
+ return 0;
+ case TX_DONGLE:
+ drvdata.tx_dongle_version_protocol =
+ get_unaligned_le32(cmd_rep->data);
+ return 0;
+ case LEFT_CONTROLLER:
+ drvdata.gp_left_version_protocol =
+ get_unaligned_le32(cmd_rep->data);
+ return 0;
+ case RIGHT_CONTROLLER:
+ drvdata.gp_right_version_protocol =
+ get_unaligned_le32(cmd_rep->data);
+ return 0;
+ default:
+ return -EINVAL;
+ }
+ case FIRMWARE_VERSION:
+ switch (cmd_rep->device_type) {
+ case USB_MCU:
+ drvdata.mcu_version_firmware =
+ get_unaligned_le32(cmd_rep->data);
+ return 0;
+ case TX_DONGLE:
+ drvdata.tx_dongle_version_firmware =
+ get_unaligned_le32(cmd_rep->data);
+ return 0;
+ case LEFT_CONTROLLER:
+ drvdata.gp_left_version_firmware =
+ get_unaligned_le32(cmd_rep->data);
+ return 0;
+ case RIGHT_CONTROLLER:
+ drvdata.gp_right_version_firmware =
+ get_unaligned_le32(cmd_rep->data);
+ return 0;
+ default:
+ return -EINVAL;
+ }
+ case HARDWARE_VERSION:
+ switch (cmd_rep->device_type) {
+ case USB_MCU:
+ drvdata.mcu_version_hardware =
+ get_unaligned_le32(cmd_rep->data);
+ return 0;
+ case TX_DONGLE:
+ drvdata.tx_dongle_version_hardware =
+ get_unaligned_le32(cmd_rep->data);
+ return 0;
+ case LEFT_CONTROLLER:
+ drvdata.gp_left_version_hardware =
+ get_unaligned_le32(cmd_rep->data);
+ return 0;
+ case RIGHT_CONTROLLER:
+ drvdata.gp_right_version_hardware =
+ get_unaligned_le32(cmd_rep->data);
+ return 0;
+ default:
+ return -EINVAL;
+ }
+ case HARDWARE_GENERATION:
+ switch (cmd_rep->device_type) {
+ case USB_MCU:
+ drvdata.mcu_version_gen = cmd_rep->data[0];
+ return 0;
+ case TX_DONGLE:
+ drvdata.tx_dongle_version_gen = cmd_rep->data[0];
+ return 0;
+ case LEFT_CONTROLLER:
+ drvdata.gp_left_version_gen = cmd_rep->data[0];
+ return 0;
+ case RIGHT_CONTROLLER:
+ drvdata.gp_right_version_gen = cmd_rep->data[0];
+ return 0;
+ default:
+ return -EINVAL;
+ }
+ default:
+ return -EINVAL;
+ }
+}
+
+static int get_endpoint_address(struct hid_device *hdev)
+{
+ struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
+ struct usb_host_endpoint *ep;
+
+ if (!intf)
+ return -ENODEV;
+
+ ep = intf->cur_altsetting->endpoint;
+ if (!ep)
+ return -ENODEV;
+
+ return ep->desc.bEndpointAddress;
+}
+
+static int hid_go_raw_event(struct hid_device *hdev, struct hid_report *report,
+ u8 *data, int size)
+{
+ struct command_report *cmd_rep;
+ int ep, ret;
+
+ if (size != GO_PACKET_SIZE)
+ goto passthrough;
+
+ ep = get_endpoint_address(hdev);
+ if (ep != GO_GP_INTF_IN)
+ goto passthrough;
+
+ cmd_rep = (struct command_report *)data;
+
+ switch (cmd_rep->id) {
+ case MCU_CONFIG_DATA:
+ switch (cmd_rep->cmd) {
+ case GET_VERSION_DATA:
+ ret = hid_go_version_event(cmd_rep);
+ break;
+ default:
+ ret = -EINVAL;
+ break;
+ };
+ break;
+ default:
+ goto passthrough;
+ };
+ dev_dbg(&hdev->dev, "Rx data as raw input report: [%*ph]\n",
+ GO_PACKET_SIZE, data);
+
+ complete(&drvdata.send_cmd_complete);
+ return ret;
+
+passthrough:
+ /* Forward other HID reports so they generate events */
+ hid_input_report(hdev, HID_INPUT_REPORT, data, size, 1);
+ return 0;
+}
+
+static int mcu_property_out(struct hid_device *hdev, u8 id, u8 command,
+ u8 index, enum dev_type device, u8 *data, size_t len)
+{
+ u8 header[] = { GO_OUTPUT_REPORT_ID, id, command, index, device };
+ size_t header_size = ARRAY_SIZE(header);
+ size_t total_size = header_size + len;
+ int timeout = 50;
+ int ret;
+
+ guard(mutex)(&drvdata.cfg_mutex);
+ memcpy(drvdata.buf, header, header_size);
+ memcpy(drvdata.buf + header_size, data, len);
+ memset(drvdata.buf + total_size, 0, GO_PACKET_SIZE - total_size);
+
+ dev_dbg(&hdev->dev, "Send data as raw output report: [%*ph]\n",
+ GO_PACKET_SIZE, drvdata.buf);
+
+ ret = hid_hw_output_report(hdev, drvdata.buf, GO_PACKET_SIZE);
+ if (ret < 0)
+ return ret;
+
+ ret = ret == GO_PACKET_SIZE ? 0 : -EINVAL;
+ if (ret)
+ return ret;
+
+ ret = wait_for_completion_interruptible_timeout(&drvdata.send_cmd_complete,
+ msecs_to_jiffies(timeout));
+
+ if (ret == 0) /* timeout occurred */
+ ret = -EBUSY;
+ if (ret > 0) /* timeout/interrupt didn't occur */
+ ret = 0;
+
+ reinit_completion(&drvdata.send_cmd_complete);
+ return ret;
+}
+
+static ssize_t version_show(struct device *dev, struct device_attribute *attr,
+ char *buf, enum version_data_index index,
+ enum dev_type device_type)
+{
+ ssize_t count = 0;
+ int ret;
+
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+ index, device_type, 0, 0);
+ if (ret)
+ return ret;
+
+ switch (index) {
+ case PRODUCT_VERSION:
+ switch (device_type) {
+ case USB_MCU:
+ count = sysfs_emit(buf, "%u\n",
+ drvdata.mcu_version_product);
+ break;
+ case TX_DONGLE:
+ count = sysfs_emit(buf, "%u\n",
+ drvdata.tx_dongle_version_product);
+ break;
+ case LEFT_CONTROLLER:
+ count = sysfs_emit(buf, "%u\n",
+ drvdata.gp_left_version_product);
+ break;
+ case RIGHT_CONTROLLER:
+ count = sysfs_emit(buf, "%u\n",
+ drvdata.gp_right_version_product);
+ break;
+ default:
+ return -EINVAL;
+ }
+ break;
+ case PROTOCOL_VERSION:
+ switch (device_type) {
+ case USB_MCU:
+ count = sysfs_emit(buf, "%u\n",
+ drvdata.mcu_version_protocol);
+ break;
+ case TX_DONGLE:
+ count = sysfs_emit(buf, "%u\n",
+ drvdata.tx_dongle_version_protocol);
+ break;
+ case LEFT_CONTROLLER:
+ count = sysfs_emit(buf, "%u\n",
+ drvdata.gp_left_version_protocol);
+ break;
+ case RIGHT_CONTROLLER:
+ count = sysfs_emit(buf, "%u\n",
+ drvdata.gp_right_version_protocol);
+ break;
+ default:
+ return -EINVAL;
+ }
+ break;
+ case FIRMWARE_VERSION:
+ switch (device_type) {
+ case USB_MCU:
+ count = sysfs_emit(buf, "%u\n",
+ drvdata.mcu_version_firmware);
+ break;
+ case TX_DONGLE:
+ count = sysfs_emit(buf, "%u\n",
+ drvdata.tx_dongle_version_firmware);
+ break;
+ case LEFT_CONTROLLER:
+ count = sysfs_emit(buf, "%u\n",
+ drvdata.gp_left_version_firmware);
+ break;
+ case RIGHT_CONTROLLER:
+ count = sysfs_emit(buf, "%u\n",
+ drvdata.gp_right_version_firmware);
+ break;
+ default:
+ return -EINVAL;
+ }
+ break;
+ case HARDWARE_VERSION:
+ switch (device_type) {
+ case USB_MCU:
+ count = sysfs_emit(buf, "%u\n",
+ drvdata.mcu_version_hardware);
+ break;
+ case TX_DONGLE:
+ count = sysfs_emit(buf, "%u\n",
+ drvdata.tx_dongle_version_hardware);
+ break;
+ case LEFT_CONTROLLER:
+ count = sysfs_emit(buf, "%u\n",
+ drvdata.gp_left_version_hardware);
+ break;
+ case RIGHT_CONTROLLER:
+ count = sysfs_emit(buf, "%u\n",
+ drvdata.gp_right_version_hardware);
+ break;
+ default:
+ return -EINVAL;
+ }
+ break;
+ case HARDWARE_GENERATION:
+ switch (device_type) {
+ case USB_MCU:
+ count = sysfs_emit(buf, "%u\n",
+ drvdata.mcu_version_gen);
+ break;
+ case TX_DONGLE:
+ count = sysfs_emit(buf, "%u\n",
+ drvdata.tx_dongle_version_gen);
+ break;
+ case LEFT_CONTROLLER:
+ count = sysfs_emit(buf, "%u\n",
+ drvdata.gp_left_version_gen);
+ break;
+ case RIGHT_CONTROLLER:
+ count = sysfs_emit(buf, "%u\n",
+ drvdata.gp_right_version_gen);
+ break;
+ default:
+ return -EINVAL;
+ }
+ break;
+ }
+
+ return count;
+}
+
+#define LEGO_DEVICE_ATTR_RW(_name, _attrname, _dtype, _rtype, _group) \
+ static ssize_t _name##_store(struct device *dev, \
+ struct device_attribute *attr, \
+ const char *buf, size_t count) \
+ { \
+ return _group##_store(dev, attr, buf, count, _name.index, \
+ _dtype); \
+ } \
+ static ssize_t _name##_show(struct device *dev, \
+ struct device_attribute *attr, char *buf) \
+ { \
+ return _group##_show(dev, attr, buf, _name.index, _dtype); \
+ } \
+ static ssize_t _name##_##_rtype##_show( \
+ struct device *dev, struct device_attribute *attr, char *buf) \
+ { \
+ return _group##_options(dev, attr, buf, _name.index); \
+ } \
+ static DEVICE_ATTR_RW_NAMED(_name, _attrname)
+
+#define LEGO_DEVICE_ATTR_WO(_name, _attrname, _dtype, _group) \
+ static ssize_t _name##_store(struct device *dev, \
+ struct device_attribute *attr, \
+ const char *buf, size_t count) \
+ { \
+ return _group##_store(dev, attr, buf, count, _name.index, \
+ _dtype); \
+ } \
+ static DEVICE_ATTR_WO_NAMED(_name, _attrname)
+
+#define LEGO_DEVICE_ATTR_RO(_name, _attrname, _dtype, _group) \
+ static ssize_t _name##_show(struct device *dev, \
+ struct device_attribute *attr, char *buf) \
+ { \
+ return _group##_show(dev, attr, buf, _name.index, _dtype); \
+ } \
+ static DEVICE_ATTR_RO_NAMED(_name, _attrname)
+
+/* Gamepad - MCU */
+struct go_cfg_attr version_product_mcu = { PRODUCT_VERSION };
+LEGO_DEVICE_ATTR_RO(version_product_mcu, "product_version", USB_MCU, version);
+
+struct go_cfg_attr version_protocol_mcu = { PROTOCOL_VERSION };
+LEGO_DEVICE_ATTR_RO(version_protocol_mcu, "protocol_version", USB_MCU, version);
+
+struct go_cfg_attr version_firmware_mcu = { FIRMWARE_VERSION };
+LEGO_DEVICE_ATTR_RO(version_firmware_mcu, "firmware_version", USB_MCU, version);
+
+struct go_cfg_attr version_hardware_mcu = { HARDWARE_VERSION };
+LEGO_DEVICE_ATTR_RO(version_hardware_mcu, "hardware_version", USB_MCU, version);
+
+struct go_cfg_attr version_gen_mcu = { HARDWARE_GENERATION };
+LEGO_DEVICE_ATTR_RO(version_gen_mcu, "hardware_generation", USB_MCU, version);
+
+static struct attribute *mcu_attrs[] = {
+ &dev_attr_version_firmware_mcu.attr,
+ &dev_attr_version_gen_mcu.attr,
+ &dev_attr_version_hardware_mcu.attr,
+ &dev_attr_version_product_mcu.attr,
+ &dev_attr_version_protocol_mcu.attr,
+ NULL,
+};
+
+static const struct attribute_group mcu_attr_group = {
+ .attrs = mcu_attrs,
+};
+
+/* Gamepad - TX Dongle */
+struct go_cfg_attr version_product_tx_dongle = { PRODUCT_VERSION };
+LEGO_DEVICE_ATTR_RO(version_product_tx_dongle, "product_version", TX_DONGLE, version);
+
+struct go_cfg_attr version_protocol_tx_dongle = { PROTOCOL_VERSION };
+LEGO_DEVICE_ATTR_RO(version_protocol_tx_dongle, "protocol_version", TX_DONGLE, version);
+
+struct go_cfg_attr version_firmware_tx_dongle = { FIRMWARE_VERSION };
+LEGO_DEVICE_ATTR_RO(version_firmware_tx_dongle, "firmware_version", TX_DONGLE, version);
+
+struct go_cfg_attr version_hardware_tx_dongle = { HARDWARE_VERSION };
+LEGO_DEVICE_ATTR_RO(version_hardware_tx_dongle, "hardware_version", TX_DONGLE, version);
+
+struct go_cfg_attr version_gen_tx_dongle = { HARDWARE_GENERATION };
+LEGO_DEVICE_ATTR_RO(version_gen_tx_dongle, "hardware_generation", TX_DONGLE, version);
+
+static struct attribute *tx_dongle_attrs[] = {
+ &dev_attr_version_hardware_tx_dongle.attr,
+ &dev_attr_version_firmware_tx_dongle.attr,
+ &dev_attr_version_gen_tx_dongle.attr,
+ &dev_attr_version_product_tx_dongle.attr,
+ &dev_attr_version_protocol_tx_dongle.attr,
+ NULL,
+};
+
+static const struct attribute_group tx_dongle_attr_group = {
+ .name = "tx_dongle",
+ .attrs = tx_dongle_attrs,
+};
+
+/* Gamepad - Left */
+struct go_cfg_attr version_product_left = { PRODUCT_VERSION };
+LEGO_DEVICE_ATTR_RO(version_product_left, "product_version", LEFT_CONTROLLER, version);
+
+struct go_cfg_attr version_protocol_left = { PROTOCOL_VERSION };
+LEGO_DEVICE_ATTR_RO(version_protocol_left, "protocol_version", LEFT_CONTROLLER, version);
+
+struct go_cfg_attr version_firmware_left = { FIRMWARE_VERSION };
+LEGO_DEVICE_ATTR_RO(version_firmware_left, "firmware_version", LEFT_CONTROLLER, version);
+
+struct go_cfg_attr version_hardware_left = { HARDWARE_VERSION };
+LEGO_DEVICE_ATTR_RO(version_hardware_left, "hardware_version", LEFT_CONTROLLER, version);
+
+struct go_cfg_attr version_gen_left = { HARDWARE_GENERATION };
+LEGO_DEVICE_ATTR_RO(version_gen_left, "hardware_generation", LEFT_CONTROLLER, version);
+
+static struct attribute *left_gamepad_attrs[] = {
+ &dev_attr_version_hardware_left.attr,
+ &dev_attr_version_firmware_left.attr,
+ &dev_attr_version_gen_left.attr,
+ &dev_attr_version_product_left.attr,
+ &dev_attr_version_protocol_left.attr,
+ NULL,
+};
+
+static const struct attribute_group left_gamepad_attr_group = {
+ .name = "left_handle",
+ .attrs = left_gamepad_attrs,
+};
+
+/* Gamepad - Right */
+struct go_cfg_attr version_product_right = { PRODUCT_VERSION };
+LEGO_DEVICE_ATTR_RO(version_product_right, "product_version", RIGHT_CONTROLLER, version);
+
+struct go_cfg_attr version_protocol_right = { PROTOCOL_VERSION };
+LEGO_DEVICE_ATTR_RO(version_protocol_right, "protocol_version", RIGHT_CONTROLLER, version);
+
+struct go_cfg_attr version_firmware_right = { FIRMWARE_VERSION };
+LEGO_DEVICE_ATTR_RO(version_firmware_right, "firmware_version", RIGHT_CONTROLLER, version);
+
+struct go_cfg_attr version_hardware_right = { HARDWARE_VERSION };
+LEGO_DEVICE_ATTR_RO(version_hardware_right, "hardware_version", RIGHT_CONTROLLER, version);
+
+struct go_cfg_attr version_gen_right = { HARDWARE_GENERATION };
+LEGO_DEVICE_ATTR_RO(version_gen_right, "hardware_generation", RIGHT_CONTROLLER, version);
+
+static struct attribute *right_gamepad_attrs[] = {
+ &dev_attr_version_hardware_right.attr,
+ &dev_attr_version_firmware_right.attr,
+ &dev_attr_version_gen_right.attr,
+ &dev_attr_version_product_right.attr,
+ &dev_attr_version_protocol_right.attr,
+ NULL,
+};
+
+static const struct attribute_group right_gamepad_attr_group = {
+ .name = "right_handle",
+ .attrs = right_gamepad_attrs,
+};
+
+/* Touchpad */
+static struct attribute *touchpad_attrs[] = {
+ NULL,
+};
+
+static const struct attribute_group touchpad_attr_group = {
+ .name = "touchpad",
+ .attrs = touchpad_attrs,
+};
+
+static const struct attribute_group *top_level_attr_groups[] = {
+ &mcu_attr_group, &tx_dongle_attr_group,
+ &left_gamepad_attr_group, &right_gamepad_attr_group,
+ &touchpad_attr_group, NULL,
+};
+
+static int hid_go_cfg_probe(struct hid_device *hdev,
+ const struct hid_device_id *_id)
+{
+ unsigned char *buf;
+ int ret;
+
+ buf = devm_kzalloc(&hdev->dev, GO_PACKET_SIZE, GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+
+ hid_set_drvdata(hdev, &drvdata);
+ drvdata.buf = buf;
+ drvdata.hdev = hdev;
+ mutex_init(&drvdata.cfg_mutex);
+
+ ret = sysfs_create_groups(&hdev->dev.kobj, top_level_attr_groups);
+ if (ret) {
+ dev_err_probe(&hdev->dev, ret,
+ "Failed to create gamepad configuration attributes\n");
+ return ret;
+ }
+
+ init_completion(&drvdata.send_cmd_complete);
+
+ return 0;
+}
+
+static void hid_go_cfg_remove(struct hid_device *hdev)
+{
+ guard(mutex)(&drvdata.cfg_mutex);
+ sysfs_remove_groups(&hdev->dev.kobj, top_level_attr_groups);
+ hid_hw_close(hdev);
+ hid_hw_stop(hdev);
+ hid_set_drvdata(hdev, NULL);
+}
+
+static int hid_go_probe(struct hid_device *hdev, const struct hid_device_id *id)
+{
+ int ret, ep;
+
+ hdev->quirks |= HID_QUIRK_INPUT_PER_APP | HID_QUIRK_MULTI_INPUT;
+
+ ret = hid_parse(hdev);
+ if (ret) {
+ hid_err(hdev, "Parse failed\n");
+ return ret;
+ }
+
+ ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+ if (ret) {
+ hid_err(hdev, "Failed to start HID device\n");
+ return ret;
+ }
+
+ ret = hid_hw_open(hdev);
+ if (ret) {
+ hid_err(hdev, "Failed to open HID device\n");
+ hid_hw_stop(hdev);
+ return ret;
+ }
+
+ ep = get_endpoint_address(hdev);
+ if (ep != GO_GP_INTF_IN) {
+ dev_dbg(&hdev->dev, "Started interface %x as generic HID device\n", ep);
+ return 0;
+ }
+
+ ret = hid_go_cfg_probe(hdev, id);
+ if (ret)
+ dev_err_probe(&hdev->dev, ret, "Failed to start configuration interface\n");
+
+ dev_dbg(&hdev->dev, "Started Legion Go HID Device: %x\n", ep);
+
+ return ret;
+}
+
+static void hid_go_remove(struct hid_device *hdev)
+{
+ int ep = get_endpoint_address(hdev);
+
+ if (ep <= 0)
+ return;
+
+ switch (ep) {
+ case GO_GP_INTF_IN:
+ hid_go_cfg_remove(hdev);
+ break;
+ default:
+ hid_hw_close(hdev);
+ hid_hw_stop(hdev);
+ break;
+ }
+}
+
+static const struct hid_device_id hid_go_devices[] = {
+ { HID_USB_DEVICE(USB_VENDOR_ID_LENOVO,
+ USB_DEVICE_ID_LENOVO_LEGION_GO2_XINPUT) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_LENOVO,
+ USB_DEVICE_ID_LENOVO_LEGION_GO2_DINPUT) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_LENOVO,
+ USB_DEVICE_ID_LENOVO_LEGION_GO2_DUAL_DINPUT) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_LENOVO,
+ USB_DEVICE_ID_LENOVO_LEGION_GO2_FPS) },
+ {}
+};
+MODULE_DEVICE_TABLE(hid, hid_go_devices);
+
+static struct hid_driver hid_lenovo_go = {
+ .name = "hid-lenovo-go",
+ .id_table = hid_go_devices,
+ .probe = hid_go_probe,
+ .remove = hid_go_remove,
+ .raw_event = hid_go_raw_event,
+};
+module_hid_driver(hid_lenovo_go);
+
+MODULE_AUTHOR("Derek J. Clark");
+MODULE_DESCRIPTION("HID Driver for Lenovo Legion Go Series Gamepads.");
+MODULE_LICENSE("GPL");
--
2.51.2
^ permalink raw reply related
* [PATCH v2 03/16] HID: hid-lenovo-go: Add Feature Status Attributes
From: Derek J. Clark @ 2025-12-29 3:17 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Mario Limonciello, Zhixin Zhang, Mia Shao, Mark Pearson,
Pierre-Loup A . Griffais, Derek J . Clark, linux-input, linux-doc,
linux-kernel
In-Reply-To: <20251229031753.581664-1-derekjohn.clark@gmail.com>
Adds various feature status indicators and toggles to hid-lenovo-go,
including the FPS mode switch setting, touchpad enable toggle, handle
automatic sleep timer, etc.
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
drivers/hid/hid-lenovo-go.c | 396 +++++++++++++++++++++++++++++++++++-
1 file changed, 395 insertions(+), 1 deletion(-)
diff --git a/drivers/hid/hid-lenovo-go.c b/drivers/hid/hid-lenovo-go.c
index 6380434b2d89..681791f119d1 100644
--- a/drivers/hid/hid-lenovo-go.c
+++ b/drivers/hid/hid-lenovo-go.c
@@ -36,21 +36,31 @@ struct hid_go_cfg {
struct completion send_cmd_complete;
struct hid_device *hdev;
struct mutex cfg_mutex; /*ensure single synchronous output report*/
+ u8 fps_mode;
+ u8 gp_left_auto_sleep_time;
u32 gp_left_version_firmware;
u8 gp_left_version_gen;
u32 gp_left_version_hardware;
u32 gp_left_version_product;
u32 gp_left_version_protocol;
+ u8 gp_mode;
+ u8 gp_right_auto_sleep_time;
u32 gp_right_version_firmware;
u8 gp_right_version_gen;
u32 gp_right_version_hardware;
u32 gp_right_version_product;
u32 gp_right_version_protocol;
+ u8 imu_left_bypass_en;
+ u8 imu_left_sensor_en;
+ u8 imu_right_bypass_en;
+ u8 imu_right_sensor_en;
u32 mcu_version_firmware;
u8 mcu_version_gen;
u32 mcu_version_hardware;
u32 mcu_version_product;
u32 mcu_version_protocol;
+ u8 rgb_en;
+ u8 tp_en;
u32 tx_dongle_version_firmware;
u8 tx_dongle_version_gen;
u32 tx_dongle_version_hardware;
@@ -102,6 +112,18 @@ enum dev_type {
RIGHT_CONTROLLER,
};
+enum enabled_status_index {
+ FEATURE_UNKNOWN,
+ FEATURE_ENABLED,
+ FEATURE_DISABLED,
+};
+
+static const char *const enabled_status_text[] = {
+ [FEATURE_UNKNOWN] = "unknown",
+ [FEATURE_ENABLED] = "true",
+ [FEATURE_DISABLED] = "false",
+};
+
enum version_data_index {
PRODUCT_VERSION = 0x02,
PROTOCOL_VERSION,
@@ -110,6 +132,41 @@ enum version_data_index {
HARDWARE_GENERATION,
};
+enum feature_status_index {
+ FEATURE_RESET_GAMEPAD = 0x02,
+ FEATURE_IMU_BYPASS,
+ FEATURE_IMU_ENABLE = 0x05,
+ FEATURE_TOUCHPAD_ENABLE = 0x07,
+ FEATURE_LIGHT_ENABLE,
+ FEATURE_AUTO_SLEEP_TIME,
+ FEATURE_FPS_SWITCH_STATUS = 0x0b,
+ FEATURE_GAMEPAD_MODE = 0x0e,
+};
+
+enum fps_switch_status_index {
+ FPS_STATUS_UNKNOWN,
+ GAMEPAD,
+ FPS,
+};
+
+static const char *const fps_switch_text[] = {
+ [FPS_STATUS_UNKNOWN] = "unknown",
+ [GAMEPAD] = "gamepad",
+ [FPS] = "fps",
+};
+
+enum gamepad_mode_index {
+ GAMEPAD_MODE_UNKNOWN,
+ XINPUT,
+ DINPUT,
+};
+
+static const char *const gamepad_mode_text[] = {
+ [GAMEPAD_MODE_UNKNOWN] = "unknown",
+ [XINPUT] = "xinput",
+ [DINPUT] = "dinput",
+};
+
static int hid_go_version_event(struct command_report *cmd_rep)
{
switch (cmd_rep->sub_cmd) {
@@ -219,6 +276,71 @@ static int hid_go_version_event(struct command_report *cmd_rep)
}
}
+static int hid_go_feature_status_event(struct command_report *cmd_rep)
+{
+ switch (cmd_rep->sub_cmd) {
+ case FEATURE_RESET_GAMEPAD:
+ return 0;
+ case FEATURE_IMU_ENABLE:
+ switch (cmd_rep->device_type) {
+ case LEFT_CONTROLLER:
+ drvdata.imu_left_sensor_en = cmd_rep->data[0];
+ return 0;
+ case RIGHT_CONTROLLER:
+ drvdata.imu_right_sensor_en = cmd_rep->data[0];
+ return 0;
+ default:
+ return -EINVAL;
+ };
+ case FEATURE_IMU_BYPASS:
+ switch (cmd_rep->device_type) {
+ case LEFT_CONTROLLER:
+ drvdata.imu_left_bypass_en = cmd_rep->data[0];
+ return 0;
+ case RIGHT_CONTROLLER:
+ drvdata.imu_right_bypass_en = cmd_rep->data[0];
+ return 0;
+ default:
+ return -EINVAL;
+ };
+ break;
+ case FEATURE_LIGHT_ENABLE:
+ drvdata.rgb_en = cmd_rep->data[0];
+ return 0;
+ case FEATURE_AUTO_SLEEP_TIME:
+ switch (cmd_rep->device_type) {
+ case LEFT_CONTROLLER:
+ drvdata.gp_left_auto_sleep_time = cmd_rep->data[0];
+ return 0;
+ case RIGHT_CONTROLLER:
+ drvdata.gp_right_auto_sleep_time = cmd_rep->data[0];
+ return 0;
+ default:
+ return -EINVAL;
+ };
+ break;
+ case FEATURE_TOUCHPAD_ENABLE:
+ drvdata.tp_en = cmd_rep->data[0];
+ return 0;
+ case FEATURE_GAMEPAD_MODE:
+ drvdata.gp_mode = cmd_rep->data[0];
+ return 0;
+ case FEATURE_FPS_SWITCH_STATUS:
+ drvdata.fps_mode = cmd_rep->data[0];
+ return 0;
+ default:
+ return -EINVAL;
+ }
+}
+
+static int hid_go_set_event_return(struct command_report *cmd_rep)
+{
+ if (cmd_rep->data[0] != 0)
+ return -EIO;
+
+ return 0;
+}
+
static int get_endpoint_address(struct hid_device *hdev)
{
struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
@@ -255,6 +377,12 @@ static int hid_go_raw_event(struct hid_device *hdev, struct hid_report *report,
case GET_VERSION_DATA:
ret = hid_go_version_event(cmd_rep);
break;
+ case GET_FEATURE_STATUS:
+ ret = hid_go_feature_status_event(cmd_rep);
+ break;
+ case SET_FEATURE_STATUS:
+ ret = hid_go_set_event_return(cmd_rep);
+ break;
default:
ret = -EINVAL;
break;
@@ -440,6 +568,195 @@ static ssize_t version_show(struct device *dev, struct device_attribute *attr,
return count;
}
+static ssize_t feature_status_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count,
+ enum feature_status_index index,
+ enum dev_type device_type)
+{
+ size_t size = 1;
+ u8 val = 0;
+ int ret;
+
+ switch (index) {
+ case FEATURE_IMU_ENABLE:
+ case FEATURE_IMU_BYPASS:
+ case FEATURE_LIGHT_ENABLE:
+ case FEATURE_TOUCHPAD_ENABLE:
+ ret = sysfs_match_string(enabled_status_text, buf);
+ val = ret;
+ break;
+ case FEATURE_AUTO_SLEEP_TIME:
+ ret = kstrtou8(buf, 10, &val);
+ break;
+ case FEATURE_RESET_GAMEPAD:
+ ret = kstrtou8(buf, 10, &val);
+ if (val != GO_GP_RESET_SUCCESS)
+ return -EINVAL;
+ break;
+ case FEATURE_FPS_SWITCH_STATUS:
+ ret = sysfs_match_string(fps_switch_text, buf);
+ val = ret;
+ break;
+ case FEATURE_GAMEPAD_MODE:
+ ret = sysfs_match_string(gamepad_mode_text, buf);
+ val = ret;
+ break;
+ default:
+ return -EINVAL;
+ };
+
+ if (ret < 0)
+ return ret;
+
+ if (!val)
+ size = 0;
+
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA,
+ SET_FEATURE_STATUS, index, device_type, &val,
+ size);
+ if (ret < 0)
+ return ret;
+
+ return count;
+}
+
+static ssize_t feature_status_show(struct device *dev,
+ struct device_attribute *attr, char *buf,
+ enum feature_status_index index,
+ enum dev_type device_type)
+{
+ ssize_t count = 0;
+ int ret;
+ u8 i;
+
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA,
+ GET_FEATURE_STATUS, index, device_type, 0, 0);
+ if (ret)
+ return ret;
+
+ switch (index) {
+ case FEATURE_IMU_ENABLE:
+ switch (device_type) {
+ case LEFT_CONTROLLER:
+ i = drvdata.imu_left_sensor_en;
+ break;
+ case RIGHT_CONTROLLER:
+ i = drvdata.imu_right_sensor_en;
+ break;
+ default:
+ return -EINVAL;
+ }
+ if (i >= ARRAY_SIZE(enabled_status_text))
+ return -EINVAL;
+
+ count = sysfs_emit(buf, "%s\n", enabled_status_text[i]);
+ break;
+ case FEATURE_IMU_BYPASS:
+ switch (device_type) {
+ case LEFT_CONTROLLER:
+ i = drvdata.imu_left_bypass_en;
+ break;
+ case RIGHT_CONTROLLER:
+ i = drvdata.imu_right_bypass_en;
+ break;
+ default:
+ return -EINVAL;
+ }
+ if (i >= ARRAY_SIZE(enabled_status_text))
+ return -EINVAL;
+
+ count = sysfs_emit(buf, "%s\n", enabled_status_text[i]);
+ break;
+ case FEATURE_LIGHT_ENABLE:
+ i = drvdata.rgb_en;
+ if (i >= ARRAY_SIZE(enabled_status_text))
+ return -EINVAL;
+
+ count = sysfs_emit(buf, "%s\n", enabled_status_text[i]);
+ break;
+ case FEATURE_TOUCHPAD_ENABLE:
+ i = drvdata.tp_en;
+ if (i >= ARRAY_SIZE(enabled_status_text))
+ return -EINVAL;
+
+ count = sysfs_emit(buf, "%s\n", enabled_status_text[i]);
+ break;
+ case FEATURE_AUTO_SLEEP_TIME:
+ switch (device_type) {
+ case LEFT_CONTROLLER:
+ i = drvdata.gp_left_auto_sleep_time;
+ break;
+ case RIGHT_CONTROLLER:
+ i = drvdata.gp_right_auto_sleep_time;
+ break;
+ default:
+ return -EINVAL;
+ };
+ count = sysfs_emit(buf, "%u\n", i);
+ break;
+ case FEATURE_FPS_SWITCH_STATUS:
+ i = drvdata.fps_mode;
+ if (i >= ARRAY_SIZE(fps_switch_text))
+ return -EINVAL;
+
+ count = sysfs_emit(buf, "%s\n", fps_switch_text[i]);
+ break;
+ case FEATURE_GAMEPAD_MODE:
+ i = drvdata.gp_mode;
+ if (i >= ARRAY_SIZE(gamepad_mode_text))
+ return -EINVAL;
+
+ count = sysfs_emit(buf, "%s\n", gamepad_mode_text[i]);
+ break;
+ default:
+ return -EINVAL;
+ };
+
+ return count;
+}
+
+static ssize_t feature_status_options(struct device *dev,
+ struct device_attribute *attr, char *buf,
+ enum feature_status_index index)
+{
+ ssize_t count = 0;
+ unsigned int i;
+
+ switch (index) {
+ case FEATURE_IMU_ENABLE:
+ case FEATURE_IMU_BYPASS:
+ case FEATURE_LIGHT_ENABLE:
+ case FEATURE_TOUCHPAD_ENABLE:
+ for (i = 1; i < ARRAY_SIZE(enabled_status_text); i++) {
+ count += sysfs_emit_at(buf, count, "%s ",
+ enabled_status_text[i]);
+ }
+ break;
+ case FEATURE_AUTO_SLEEP_TIME:
+ return sysfs_emit(buf, "0-255\n");
+ case FEATURE_FPS_SWITCH_STATUS:
+ for (i = 1; i < ARRAY_SIZE(fps_switch_text); i++) {
+ count += sysfs_emit_at(buf, count, "%s ",
+ fps_switch_text[i]);
+ }
+ break;
+ case FEATURE_GAMEPAD_MODE:
+ for (i = 1; i < ARRAY_SIZE(gamepad_mode_text); i++) {
+ count += sysfs_emit_at(buf, count, "%s ",
+ gamepad_mode_text[i]);
+ }
+ break;
+ default:
+ return -EINVAL;
+ };
+
+ if (count)
+ buf[count - 1] = '\n';
+
+ return count;
+}
+
#define LEGO_DEVICE_ATTR_RW(_name, _attrname, _dtype, _rtype, _group) \
static ssize_t _name##_store(struct device *dev, \
struct device_attribute *attr, \
@@ -494,7 +811,22 @@ LEGO_DEVICE_ATTR_RO(version_hardware_mcu, "hardware_version", USB_MCU, version);
struct go_cfg_attr version_gen_mcu = { HARDWARE_GENERATION };
LEGO_DEVICE_ATTR_RO(version_gen_mcu, "hardware_generation", USB_MCU, version);
+struct go_cfg_attr fps_switch_status = { FEATURE_FPS_SWITCH_STATUS };
+LEGO_DEVICE_ATTR_RO(fps_switch_status, "fps_switch_status", UNSPECIFIED,
+ feature_status);
+
+struct go_cfg_attr gamepad_mode = { FEATURE_GAMEPAD_MODE };
+LEGO_DEVICE_ATTR_RW(gamepad_mode, "mode", UNSPECIFIED, index, feature_status);
+static DEVICE_ATTR_RO_NAMED(gamepad_mode_index, "mode_index");
+
+struct go_cfg_attr reset_mcu = { FEATURE_RESET_GAMEPAD };
+LEGO_DEVICE_ATTR_WO(reset_mcu, "reset_mcu", USB_MCU, feature_status);
+
static struct attribute *mcu_attrs[] = {
+ &dev_attr_fps_switch_status.attr,
+ &dev_attr_gamepad_mode.attr,
+ &dev_attr_gamepad_mode_index.attr,
+ &dev_attr_reset_mcu.attr,
&dev_attr_version_firmware_mcu.attr,
&dev_attr_version_gen_mcu.attr,
&dev_attr_version_hardware_mcu.attr,
@@ -523,7 +855,11 @@ LEGO_DEVICE_ATTR_RO(version_hardware_tx_dongle, "hardware_version", TX_DONGLE, v
struct go_cfg_attr version_gen_tx_dongle = { HARDWARE_GENERATION };
LEGO_DEVICE_ATTR_RO(version_gen_tx_dongle, "hardware_generation", TX_DONGLE, version);
+struct go_cfg_attr reset_tx_dongle = { FEATURE_RESET_GAMEPAD };
+LEGO_DEVICE_ATTR_RO(reset_tx_dongle, "reset", TX_DONGLE, feature_status);
+
static struct attribute *tx_dongle_attrs[] = {
+ &dev_attr_reset_tx_dongle.attr,
&dev_attr_version_hardware_tx_dongle.attr,
&dev_attr_version_firmware_tx_dongle.attr,
&dev_attr_version_gen_tx_dongle.attr,
@@ -553,7 +889,33 @@ LEGO_DEVICE_ATTR_RO(version_hardware_left, "hardware_version", LEFT_CONTROLLER,
struct go_cfg_attr version_gen_left = { HARDWARE_GENERATION };
LEGO_DEVICE_ATTR_RO(version_gen_left, "hardware_generation", LEFT_CONTROLLER, version);
+struct go_cfg_attr auto_sleep_time_left = { FEATURE_AUTO_SLEEP_TIME };
+LEGO_DEVICE_ATTR_RW(auto_sleep_time_left, "auto_sleep_time", LEFT_CONTROLLER,
+ range, feature_status);
+static DEVICE_ATTR_RO_NAMED(auto_sleep_time_left_range,
+ "auto_sleep_time_range");
+
+struct go_cfg_attr imu_bypass_left = { FEATURE_IMU_BYPASS };
+LEGO_DEVICE_ATTR_RW(imu_bypass_left, "imu_bypass_enabled", LEFT_CONTROLLER,
+ index, feature_status);
+static DEVICE_ATTR_RO_NAMED(imu_bypass_left_index, "imu_bypass_enabled_index");
+
+struct go_cfg_attr imu_enabled_left = { FEATURE_IMU_ENABLE };
+LEGO_DEVICE_ATTR_RW(imu_enabled_left, "imu_enabled", LEFT_CONTROLLER, index,
+ feature_status);
+static DEVICE_ATTR_RO_NAMED(imu_enabled_left_index, "imu_enabled_index");
+
+struct go_cfg_attr reset_left = { FEATURE_RESET_GAMEPAD };
+LEGO_DEVICE_ATTR_WO(reset_left, "reset", LEFT_CONTROLLER, feature_status);
+
static struct attribute *left_gamepad_attrs[] = {
+ &dev_attr_auto_sleep_time_left.attr,
+ &dev_attr_auto_sleep_time_left_range.attr,
+ &dev_attr_imu_bypass_left.attr,
+ &dev_attr_imu_bypass_left_index.attr,
+ &dev_attr_imu_enabled_left.attr,
+ &dev_attr_imu_enabled_left_index.attr,
+ &dev_attr_reset_left.attr,
&dev_attr_version_hardware_left.attr,
&dev_attr_version_firmware_left.attr,
&dev_attr_version_gen_left.attr,
@@ -583,7 +945,33 @@ LEGO_DEVICE_ATTR_RO(version_hardware_right, "hardware_version", RIGHT_CONTROLLER
struct go_cfg_attr version_gen_right = { HARDWARE_GENERATION };
LEGO_DEVICE_ATTR_RO(version_gen_right, "hardware_generation", RIGHT_CONTROLLER, version);
+struct go_cfg_attr auto_sleep_time_right = { FEATURE_AUTO_SLEEP_TIME };
+LEGO_DEVICE_ATTR_RW(auto_sleep_time_right, "auto_sleep_time", RIGHT_CONTROLLER,
+ range, feature_status);
+static DEVICE_ATTR_RO_NAMED(auto_sleep_time_right_range,
+ "auto_sleep_time_range");
+
+struct go_cfg_attr imu_bypass_right = { FEATURE_IMU_BYPASS };
+LEGO_DEVICE_ATTR_RW(imu_bypass_right, "imu_bypass_enabled", RIGHT_CONTROLLER,
+ index, feature_status);
+static DEVICE_ATTR_RO_NAMED(imu_bypass_right_index, "imu_bypass_enabled_index");
+
+struct go_cfg_attr imu_enabled_right = { FEATURE_IMU_BYPASS };
+LEGO_DEVICE_ATTR_RW(imu_enabled_right, "imu_enabled", RIGHT_CONTROLLER, index,
+ feature_status);
+static DEVICE_ATTR_RO_NAMED(imu_enabled_right_index, "imu_enabled_index");
+
+struct go_cfg_attr reset_right = { FEATURE_RESET_GAMEPAD };
+LEGO_DEVICE_ATTR_WO(reset_right, "reset", LEFT_CONTROLLER, feature_status);
+
static struct attribute *right_gamepad_attrs[] = {
+ &dev_attr_auto_sleep_time_right.attr,
+ &dev_attr_auto_sleep_time_right_range.attr,
+ &dev_attr_imu_bypass_right.attr,
+ &dev_attr_imu_bypass_right_index.attr,
+ &dev_attr_imu_enabled_right.attr,
+ &dev_attr_imu_enabled_right_index.attr,
+ &dev_attr_reset_right.attr,
&dev_attr_version_hardware_right.attr,
&dev_attr_version_firmware_right.attr,
&dev_attr_version_gen_right.attr,
@@ -598,8 +986,14 @@ static const struct attribute_group right_gamepad_attr_group = {
};
/* Touchpad */
+struct go_cfg_attr touchpad_enabled = { FEATURE_TOUCHPAD_ENABLE };
+LEGO_DEVICE_ATTR_RW(touchpad_enabled, "enabled", UNSPECIFIED, index,
+ feature_status);
+static DEVICE_ATTR_RO_NAMED(touchpad_enabled_index, "enabled_index");
+
static struct attribute *touchpad_attrs[] = {
- NULL,
+ &dev_attr_touchpad_enabled.attr,
+ &dev_attr_touchpad_enabled_index.attr,
};
static const struct attribute_group touchpad_attr_group = {
--
2.51.2
^ permalink raw reply related
* [PATCH v2 04/16] HID: hid-lenovo-go: Add Rumble and Haptic Settings
From: Derek J. Clark @ 2025-12-29 3:17 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Mario Limonciello, Zhixin Zhang, Mia Shao, Mark Pearson,
Pierre-Loup A . Griffais, Derek J . Clark, linux-input, linux-doc,
linux-kernel
In-Reply-To: <20251229031753.581664-1-derekjohn.clark@gmail.com>
Adds attributes that control the handles rumble mode and intensity, as
well as touchpad haptic feedback settings.
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
drivers/hid/hid-lenovo-go.c | 340 ++++++++++++++++++++++++++++++++++--
1 file changed, 326 insertions(+), 14 deletions(-)
diff --git a/drivers/hid/hid-lenovo-go.c b/drivers/hid/hid-lenovo-go.c
index 681791f119d1..7d8bd1dba7ce 100644
--- a/drivers/hid/hid-lenovo-go.c
+++ b/drivers/hid/hid-lenovo-go.c
@@ -38,6 +38,8 @@ struct hid_go_cfg {
struct mutex cfg_mutex; /*ensure single synchronous output report*/
u8 fps_mode;
u8 gp_left_auto_sleep_time;
+ u8 gp_left_notify_en;
+ u8 gp_left_rumble_mode;
u32 gp_left_version_firmware;
u8 gp_left_version_gen;
u32 gp_left_version_hardware;
@@ -45,11 +47,14 @@ struct hid_go_cfg {
u32 gp_left_version_protocol;
u8 gp_mode;
u8 gp_right_auto_sleep_time;
+ u8 gp_right_notify_en;
+ u8 gp_right_rumble_mode;
u32 gp_right_version_firmware;
u8 gp_right_version_gen;
u32 gp_right_version_hardware;
u32 gp_right_version_product;
u32 gp_right_version_protocol;
+ u8 gp_rumble_intensity;
u8 imu_left_bypass_en;
u8 imu_left_sensor_en;
u8 imu_right_bypass_en;
@@ -61,6 +66,8 @@ struct hid_go_cfg {
u32 mcu_version_protocol;
u8 rgb_en;
u8 tp_en;
+ u8 tp_vibration_en;
+ u8 tp_vibration_intensity;
u32 tx_dongle_version_firmware;
u8 tx_dongle_version_gen;
u32 tx_dongle_version_hardware;
@@ -167,6 +174,49 @@ static const char *const gamepad_mode_text[] = {
[DINPUT] = "dinput",
};
+enum motor_cfg_index {
+ MOTOR_CFG_ALL = 0x01,
+ MOTOR_INTENSITY,
+ VIBRATION_NOTIFY_ENABLE,
+ RUMBLE_MODE,
+ TP_VIBRATION_ENABLE,
+ TP_VIBRATION_INTENSITY,
+};
+
+enum intensity_index {
+ INTENSITY_UNKNOWN,
+ INTENSITY_OFF,
+ INTENSITY_LOW,
+ INTENSITY_MEDIUM,
+ INTENSITY_HIGH,
+};
+
+static const char *const intensity_text[] = {
+ [INTENSITY_UNKNOWN] = "unknown",
+ [INTENSITY_OFF] = "off",
+ [INTENSITY_LOW] = "low",
+ [INTENSITY_MEDIUM] = "medium",
+ [INTENSITY_HIGH] = "high",
+};
+
+enum rumble_mode_index {
+ RUMBLE_MODE_UNKNOWN,
+ RUMBLE_MODE_FPS,
+ RUMBLE_MODE_RACE,
+ RUMBLE_MODE_AVERAGE,
+ RUMBLE_MODE_SPG,
+ RUMBLE_MODE_RPG,
+};
+
+static const char *const rumble_mode_text[] = {
+ [RUMBLE_MODE_UNKNOWN] = "unknown",
+ [RUMBLE_MODE_FPS] = "fps",
+ [RUMBLE_MODE_RACE] = "racing",
+ [RUMBLE_MODE_AVERAGE] = "standard",
+ [RUMBLE_MODE_SPG] = "spg",
+ [RUMBLE_MODE_RPG] = "rpg",
+};
+
static int hid_go_version_event(struct command_report *cmd_rep)
{
switch (cmd_rep->sub_cmd) {
@@ -333,6 +383,47 @@ static int hid_go_feature_status_event(struct command_report *cmd_rep)
}
}
+static int hid_go_motor_event(struct command_report *cmd_rep)
+{
+ switch (cmd_rep->sub_cmd) {
+ case MOTOR_CFG_ALL:
+ return -EINVAL;
+ case MOTOR_INTENSITY:
+ drvdata.gp_rumble_intensity = cmd_rep->data[0];
+ return 0;
+ case VIBRATION_NOTIFY_ENABLE:
+ switch (cmd_rep->device_type) {
+ case LEFT_CONTROLLER:
+ drvdata.gp_left_notify_en = cmd_rep->data[0];
+ return 0;
+ case RIGHT_CONTROLLER:
+ drvdata.gp_right_notify_en = cmd_rep->data[0];
+ return 0;
+ default:
+ return -EINVAL;
+ };
+ break;
+ case RUMBLE_MODE:
+ switch (cmd_rep->device_type) {
+ case LEFT_CONTROLLER:
+ drvdata.gp_left_rumble_mode = cmd_rep->data[0];
+ return 0;
+ case RIGHT_CONTROLLER:
+ drvdata.gp_right_rumble_mode = cmd_rep->data[0];
+ return 0;
+ default:
+ return -EINVAL;
+ };
+ case TP_VIBRATION_ENABLE:
+ drvdata.tp_vibration_en = cmd_rep->data[0];
+ return 0;
+ case TP_VIBRATION_INTENSITY:
+ drvdata.tp_vibration_intensity = cmd_rep->data[0];
+ return 0;
+ }
+ return -EINVAL;
+}
+
static int hid_go_set_event_return(struct command_report *cmd_rep)
{
if (cmd_rep->data[0] != 0)
@@ -380,7 +471,11 @@ static int hid_go_raw_event(struct hid_device *hdev, struct hid_report *report,
case GET_FEATURE_STATUS:
ret = hid_go_feature_status_event(cmd_rep);
break;
+ case GET_MOTOR_CFG:
+ ret = hid_go_motor_event(cmd_rep);
+ break;
case SET_FEATURE_STATUS:
+ case SET_MOTOR_CFG:
ret = hid_go_set_event_return(cmd_rep);
break;
default:
@@ -757,6 +852,168 @@ static ssize_t feature_status_options(struct device *dev,
return count;
}
+static ssize_t motor_config_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count,
+ enum motor_cfg_index index,
+ enum dev_type device_type)
+{
+ size_t size = 1;
+ u8 val = 0;
+ int ret;
+
+ switch (index) {
+ case MOTOR_CFG_ALL:
+ return -EINVAL;
+ case MOTOR_INTENSITY:
+ ret = sysfs_match_string(intensity_text, buf);
+ val = ret;
+ break;
+ case VIBRATION_NOTIFY_ENABLE:
+ ret = sysfs_match_string(enabled_status_text, buf);
+ val = ret;
+ break;
+ case RUMBLE_MODE:
+ ret = sysfs_match_string(rumble_mode_text, buf);
+ val = ret;
+ break;
+ case TP_VIBRATION_ENABLE:
+ ret = sysfs_match_string(enabled_status_text, buf);
+ val = ret;
+ break;
+ case TP_VIBRATION_INTENSITY:
+ ret = sysfs_match_string(intensity_text, buf);
+ val = ret;
+ break;
+ };
+
+ if (ret < 0)
+ return ret;
+
+ if (!val)
+ size = 0;
+
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, SET_MOTOR_CFG,
+ index, device_type, &val, size);
+ if (ret < 0)
+ return ret;
+
+ return count;
+}
+
+static ssize_t motor_config_show(struct device *dev,
+ struct device_attribute *attr, char *buf,
+ enum motor_cfg_index index,
+ enum dev_type device_type)
+{
+ ssize_t count = 0;
+ int ret;
+ u8 i;
+
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_MOTOR_CFG,
+ index, device_type, 0, 0);
+ if (ret)
+ return ret;
+
+ switch (index) {
+ case MOTOR_CFG_ALL:
+ return -EINVAL;
+ case MOTOR_INTENSITY:
+ i = drvdata.gp_rumble_intensity;
+ if (i >= ARRAY_SIZE(intensity_text))
+ return -EINVAL;
+
+ count = sysfs_emit(buf, "%s\n", intensity_text[i]);
+ break;
+ case VIBRATION_NOTIFY_ENABLE:
+ switch (device_type) {
+ case LEFT_CONTROLLER:
+ i = drvdata.gp_left_notify_en;
+ break;
+ case RIGHT_CONTROLLER:
+ i = drvdata.gp_right_notify_en;
+ break;
+ default:
+ return -EINVAL;
+ };
+ if (i >= ARRAY_SIZE(enabled_status_text))
+ return -EINVAL;
+
+ count = sysfs_emit(buf, "%s\n", enabled_status_text[i]);
+ break;
+ case RUMBLE_MODE:
+ switch (device_type) {
+ case LEFT_CONTROLLER:
+ i = drvdata.gp_left_rumble_mode;
+ break;
+ case RIGHT_CONTROLLER:
+ i = drvdata.gp_right_rumble_mode;
+ break;
+ default:
+ return -EINVAL;
+ };
+ if (i >= ARRAY_SIZE(rumble_mode_text))
+ return -EINVAL;
+
+ count = sysfs_emit(buf, "%s\n", rumble_mode_text[i]);
+ break;
+ case TP_VIBRATION_ENABLE:
+ i = drvdata.tp_vibration_en;
+ if (i >= ARRAY_SIZE(enabled_status_text))
+ return -EINVAL;
+
+ count = sysfs_emit(buf, "%s\n", enabled_status_text[i]);
+ break;
+ case TP_VIBRATION_INTENSITY:
+ i = drvdata.tp_vibration_intensity;
+ if (i >= ARRAY_SIZE(intensity_text))
+ return -EINVAL;
+
+ count = sysfs_emit(buf, "%s\n", intensity_text[i]);
+ break;
+ };
+
+ return count;
+}
+
+static ssize_t motor_config_options(struct device *dev,
+ struct device_attribute *attr, char *buf,
+ enum motor_cfg_index index)
+{
+ ssize_t count = 0;
+ unsigned int i;
+
+ switch (index) {
+ case MOTOR_CFG_ALL:
+ break;
+ case RUMBLE_MODE:
+ for (i = 1; i < ARRAY_SIZE(rumble_mode_text); i++) {
+ count += sysfs_emit_at(buf, count, "%s ",
+ rumble_mode_text[i]);
+ }
+ break;
+ case MOTOR_INTENSITY:
+ case TP_VIBRATION_INTENSITY:
+ for (i = 1; i < ARRAY_SIZE(intensity_text); i++) {
+ count += sysfs_emit_at(buf, count, "%s ",
+ intensity_text[i]);
+ }
+ break;
+ case VIBRATION_NOTIFY_ENABLE:
+ case TP_VIBRATION_ENABLE:
+ for (i = 1; i < ARRAY_SIZE(enabled_status_text); i++) {
+ count += sysfs_emit_at(buf, count, "%s ",
+ enabled_status_text[i]);
+ }
+ break;
+ };
+
+ if (count)
+ buf[count - 1] = '\n';
+
+ return count;
+}
+
#define LEGO_DEVICE_ATTR_RW(_name, _attrname, _dtype, _rtype, _group) \
static ssize_t _name##_store(struct device *dev, \
struct device_attribute *attr, \
@@ -822,10 +1079,18 @@ static DEVICE_ATTR_RO_NAMED(gamepad_mode_index, "mode_index");
struct go_cfg_attr reset_mcu = { FEATURE_RESET_GAMEPAD };
LEGO_DEVICE_ATTR_WO(reset_mcu, "reset_mcu", USB_MCU, feature_status);
+struct go_cfg_attr gamepad_rumble_intensity = { MOTOR_INTENSITY };
+LEGO_DEVICE_ATTR_RW(gamepad_rumble_intensity, "rumble_intensity", UNSPECIFIED,
+ index, motor_config);
+static DEVICE_ATTR_RO_NAMED(gamepad_rumble_intensity_index,
+ "rumble_intensity_index");
+
static struct attribute *mcu_attrs[] = {
&dev_attr_fps_switch_status.attr,
&dev_attr_gamepad_mode.attr,
&dev_attr_gamepad_mode_index.attr,
+ &dev_attr_gamepad_rumble_intensity.attr,
+ &dev_attr_gamepad_rumble_intensity_index.attr,
&dev_attr_reset_mcu.attr,
&dev_attr_version_firmware_mcu.attr,
&dev_attr_version_gen_mcu.attr,
@@ -896,26 +1161,41 @@ static DEVICE_ATTR_RO_NAMED(auto_sleep_time_left_range,
"auto_sleep_time_range");
struct go_cfg_attr imu_bypass_left = { FEATURE_IMU_BYPASS };
-LEGO_DEVICE_ATTR_RW(imu_bypass_left, "imu_bypass_enabled", LEFT_CONTROLLER,
+LEGO_DEVICE_ATTR_RW(imu_bypass_left, "imu_bypass_enable", LEFT_CONTROLLER,
index, feature_status);
-static DEVICE_ATTR_RO_NAMED(imu_bypass_left_index, "imu_bypass_enabled_index");
+static DEVICE_ATTR_RO_NAMED(imu_bypass_left_index, "imu_bypass_enable_index");
-struct go_cfg_attr imu_enabled_left = { FEATURE_IMU_ENABLE };
-LEGO_DEVICE_ATTR_RW(imu_enabled_left, "imu_enabled", LEFT_CONTROLLER, index,
+struct go_cfg_attr imu_enable_left = { FEATURE_IMU_ENABLE };
+LEGO_DEVICE_ATTR_RW(imu_enable_left, "imu_enable", LEFT_CONTROLLER, index,
feature_status);
-static DEVICE_ATTR_RO_NAMED(imu_enabled_left_index, "imu_enabled_index");
+static DEVICE_ATTR_RO_NAMED(imu_enable_left_index, "imu_enable_index");
struct go_cfg_attr reset_left = { FEATURE_RESET_GAMEPAD };
LEGO_DEVICE_ATTR_WO(reset_left, "reset", LEFT_CONTROLLER, feature_status);
+struct go_cfg_attr rumble_mode_left = { RUMBLE_MODE };
+LEGO_DEVICE_ATTR_RW(rumble_mode_left, "rumble_mode", LEFT_CONTROLLER, index,
+ motor_config);
+static DEVICE_ATTR_RO_NAMED(rumble_mode_left_index, "rumble_mode_index");
+
+struct go_cfg_attr rumble_notification_left = { VIBRATION_NOTIFY_ENABLE };
+LEGO_DEVICE_ATTR_RW(rumble_notification_left, "rumble_notification",
+ LEFT_CONTROLLER, index, motor_config);
+static DEVICE_ATTR_RO_NAMED(rumble_notification_left_index,
+ "rumble_notification_index");
+
static struct attribute *left_gamepad_attrs[] = {
&dev_attr_auto_sleep_time_left.attr,
&dev_attr_auto_sleep_time_left_range.attr,
&dev_attr_imu_bypass_left.attr,
&dev_attr_imu_bypass_left_index.attr,
- &dev_attr_imu_enabled_left.attr,
- &dev_attr_imu_enabled_left_index.attr,
+ &dev_attr_imu_enable_left.attr,
+ &dev_attr_imu_enable_left_index.attr,
&dev_attr_reset_left.attr,
+ &dev_attr_rumble_mode_left.attr,
+ &dev_attr_rumble_mode_left_index.attr,
+ &dev_attr_rumble_notification_left.attr,
+ &dev_attr_rumble_notification_left_index.attr,
&dev_attr_version_hardware_left.attr,
&dev_attr_version_firmware_left.attr,
&dev_attr_version_gen_left.attr,
@@ -952,26 +1232,41 @@ static DEVICE_ATTR_RO_NAMED(auto_sleep_time_right_range,
"auto_sleep_time_range");
struct go_cfg_attr imu_bypass_right = { FEATURE_IMU_BYPASS };
-LEGO_DEVICE_ATTR_RW(imu_bypass_right, "imu_bypass_enabled", RIGHT_CONTROLLER,
+LEGO_DEVICE_ATTR_RW(imu_bypass_right, "imu_bypass_enable", RIGHT_CONTROLLER,
index, feature_status);
-static DEVICE_ATTR_RO_NAMED(imu_bypass_right_index, "imu_bypass_enabled_index");
+static DEVICE_ATTR_RO_NAMED(imu_bypass_right_index, "imu_bypass_enable_index");
-struct go_cfg_attr imu_enabled_right = { FEATURE_IMU_BYPASS };
-LEGO_DEVICE_ATTR_RW(imu_enabled_right, "imu_enabled", RIGHT_CONTROLLER, index,
+struct go_cfg_attr imu_enable_right = { FEATURE_IMU_BYPASS };
+LEGO_DEVICE_ATTR_RW(imu_enable_right, "imu_enable", RIGHT_CONTROLLER, index,
feature_status);
-static DEVICE_ATTR_RO_NAMED(imu_enabled_right_index, "imu_enabled_index");
+static DEVICE_ATTR_RO_NAMED(imu_enable_right_index, "imu_enable_index");
struct go_cfg_attr reset_right = { FEATURE_RESET_GAMEPAD };
LEGO_DEVICE_ATTR_WO(reset_right, "reset", LEFT_CONTROLLER, feature_status);
+struct go_cfg_attr rumble_mode_right = { RUMBLE_MODE };
+LEGO_DEVICE_ATTR_RW(rumble_mode_right, "rumble_mode", RIGHT_CONTROLLER, index,
+ motor_config);
+static DEVICE_ATTR_RO_NAMED(rumble_mode_right_index, "rumble_mode_index");
+
+struct go_cfg_attr rumble_notification_right = { VIBRATION_NOTIFY_ENABLE };
+LEGO_DEVICE_ATTR_RW(rumble_notification_right, "rumble_notification",
+ RIGHT_CONTROLLER, index, motor_config);
+static DEVICE_ATTR_RO_NAMED(rumble_notification_right_index,
+ "rumble_notification_index");
+
static struct attribute *right_gamepad_attrs[] = {
&dev_attr_auto_sleep_time_right.attr,
&dev_attr_auto_sleep_time_right_range.attr,
&dev_attr_imu_bypass_right.attr,
&dev_attr_imu_bypass_right_index.attr,
- &dev_attr_imu_enabled_right.attr,
- &dev_attr_imu_enabled_right_index.attr,
+ &dev_attr_imu_enable_right.attr,
+ &dev_attr_imu_enable_right_index.attr,
&dev_attr_reset_right.attr,
+ &dev_attr_rumble_mode_right.attr,
+ &dev_attr_rumble_mode_right_index.attr,
+ &dev_attr_rumble_notification_right.attr,
+ &dev_attr_rumble_notification_right_index.attr,
&dev_attr_version_hardware_right.attr,
&dev_attr_version_firmware_right.attr,
&dev_attr_version_gen_right.attr,
@@ -991,9 +1286,26 @@ LEGO_DEVICE_ATTR_RW(touchpad_enabled, "enabled", UNSPECIFIED, index,
feature_status);
static DEVICE_ATTR_RO_NAMED(touchpad_enabled_index, "enabled_index");
+struct go_cfg_attr touchpad_vibration_enabled = { TP_VIBRATION_ENABLE };
+LEGO_DEVICE_ATTR_RW(touchpad_vibration_enabled, "vibration_enabled", UNSPECIFIED,
+ index, motor_config);
+static DEVICE_ATTR_RO_NAMED(touchpad_vibration_enabled_index,
+ "vibration_enabled_index");
+
+struct go_cfg_attr touchpad_vibration_intensity = { TP_VIBRATION_INTENSITY };
+LEGO_DEVICE_ATTR_RW(touchpad_vibration_intensity, "vibration_intensity",
+ UNSPECIFIED, index, motor_config);
+static DEVICE_ATTR_RO_NAMED(touchpad_vibration_intensity_index,
+ "vibration_intensity_index");
+
static struct attribute *touchpad_attrs[] = {
&dev_attr_touchpad_enabled.attr,
&dev_attr_touchpad_enabled_index.attr,
+ &dev_attr_touchpad_vibration_enabled.attr,
+ &dev_attr_touchpad_vibration_enabled_index.attr,
+ &dev_attr_touchpad_vibration_intensity.attr,
+ &dev_attr_touchpad_vibration_intensity_index.attr,
+ NULL,
};
static const struct attribute_group touchpad_attr_group = {
--
2.51.2
^ permalink raw reply related
* [PATCH v2 05/16] HID: hid-lenovo-go: Add FPS Mode DPI settings
From: Derek J. Clark @ 2025-12-29 3:17 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Mario Limonciello, Zhixin Zhang, Mia Shao, Mark Pearson,
Pierre-Loup A . Griffais, Derek J . Clark, linux-input, linux-doc,
linux-kernel
In-Reply-To: <20251229031753.581664-1-derekjohn.clark@gmail.com>
Adds attribute that enables selection of the DPI of the optical sensor
when the right handle toggle is set to FPS mode.
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
drivers/hid/hid-lenovo-go.c | 68 +++++++++++++++++++++++++++++++++++++
1 file changed, 68 insertions(+)
diff --git a/drivers/hid/hid-lenovo-go.c b/drivers/hid/hid-lenovo-go.c
index 7d8bd1dba7ce..96f16ba5526c 100644
--- a/drivers/hid/hid-lenovo-go.c
+++ b/drivers/hid/hid-lenovo-go.c
@@ -64,6 +64,7 @@ struct hid_go_cfg {
u32 mcu_version_hardware;
u32 mcu_version_product;
u32 mcu_version_protocol;
+ u32 mouse_dpi;
u8 rgb_en;
u8 tp_en;
u8 tp_vibration_en;
@@ -217,6 +218,8 @@ static const char *const rumble_mode_text[] = {
[RUMBLE_MODE_RPG] = "rpg",
};
+#define FPS_MODE_DPI 0x02
+
static int hid_go_version_event(struct command_report *cmd_rep)
{
switch (cmd_rep->sub_cmd) {
@@ -424,6 +427,16 @@ static int hid_go_motor_event(struct command_report *cmd_rep)
return -EINVAL;
}
+static int hid_go_fps_dpi_event(struct command_report *cmd_rep)
+{
+ if (cmd_rep->sub_cmd != FPS_MODE_DPI)
+ return -EINVAL;
+
+ drvdata.mouse_dpi = get_unaligned_le32(cmd_rep->data);
+
+ return 0;
+}
+
static int hid_go_set_event_return(struct command_report *cmd_rep)
{
if (cmd_rep->data[0] != 0)
@@ -474,8 +487,12 @@ static int hid_go_raw_event(struct hid_device *hdev, struct hid_report *report,
case GET_MOTOR_CFG:
ret = hid_go_motor_event(cmd_rep);
break;
+ case GET_DPI_CFG:
+ ret = hid_go_fps_dpi_event(cmd_rep);
+ break;
case SET_FEATURE_STATUS:
case SET_MOTOR_CFG:
+ case SET_DPI_CFG:
ret = hid_go_set_event_return(cmd_rep);
break;
default:
@@ -1014,6 +1031,52 @@ static ssize_t motor_config_options(struct device *dev,
return count;
}
+static ssize_t fps_mode_dpi_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+
+{
+ size_t size = 4;
+ u32 value;
+ u8 val[4];
+ int ret;
+
+ ret = kstrtou32(buf, 10, &value);
+ if (ret)
+ return ret;
+
+ if (value != 500 && value != 800 && value != 1200 && value != 1800)
+ return -EINVAL;
+
+ put_unaligned_le32(value, val);
+
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, SET_DPI_CFG,
+ FPS_MODE_DPI, UNSPECIFIED, val, size);
+ if (ret < 0)
+ return ret;
+
+ return count;
+}
+
+static ssize_t fps_mode_dpi_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ int ret;
+
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_DPI_CFG,
+ FPS_MODE_DPI, UNSPECIFIED, 0, 0);
+ if (ret < 0)
+ return ret;
+
+ return sysfs_emit(buf, "%u\n", drvdata.mouse_dpi);
+}
+
+static ssize_t fps_mode_dpi_index_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "500 800 1200 1800\n");
+}
+
#define LEGO_DEVICE_ATTR_RW(_name, _attrname, _dtype, _rtype, _group) \
static ssize_t _name##_store(struct device *dev, \
struct device_attribute *attr, \
@@ -1085,7 +1148,12 @@ LEGO_DEVICE_ATTR_RW(gamepad_rumble_intensity, "rumble_intensity", UNSPECIFIED,
static DEVICE_ATTR_RO_NAMED(gamepad_rumble_intensity_index,
"rumble_intensity_index");
+static DEVICE_ATTR_RW(fps_mode_dpi);
+static DEVICE_ATTR_RO(fps_mode_dpi_index);
+
static struct attribute *mcu_attrs[] = {
+ &dev_attr_fps_mode_dpi.attr,
+ &dev_attr_fps_mode_dpi_index.attr,
&dev_attr_fps_switch_status.attr,
&dev_attr_gamepad_mode.attr,
&dev_attr_gamepad_mode_index.attr,
--
2.51.2
^ permalink raw reply related
* [PATCH v2 06/16] HID: hid-lenovo-go: Add RGB LED control interface
From: Derek J. Clark @ 2025-12-29 3:17 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Mario Limonciello, Zhixin Zhang, Mia Shao, Mark Pearson,
Pierre-Loup A . Griffais, Derek J . Clark, linux-input, linux-doc,
linux-kernel
In-Reply-To: <20251229031753.581664-1-derekjohn.clark@gmail.com>
Adds an LED multicolor class device and attribute group for controlling
the RGB of the Left and right handles. In addition to the standard
led_cdev attributes, additional attributes that allow for the control of
the effect (monocolor, breathe, rainbow, and chroma), speed of the
effect change, an enable toggle, and profile.
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
drivers/hid/hid-lenovo-go.c | 472 ++++++++++++++++++++++++++++++++++++
1 file changed, 472 insertions(+)
diff --git a/drivers/hid/hid-lenovo-go.c b/drivers/hid/hid-lenovo-go.c
index 96f16ba5526c..c8761297f57b 100644
--- a/drivers/hid/hid-lenovo-go.c
+++ b/drivers/hid/hid-lenovo-go.c
@@ -17,12 +17,15 @@
#include <linux/hid.h>
#include <linux/jiffies.h>
#include <linux/kstrtox.h>
+#include <linux/led-class-multicolor.h>
#include <linux/mutex.h>
#include <linux/printk.h>
#include <linux/sysfs.h>
#include <linux/types.h>
#include <linux/unaligned.h>
#include <linux/usb.h>
+#include <linux/workqueue.h>
+#include <linux/workqueue_types.h>
#include "hid-ids.h"
@@ -33,7 +36,9 @@
struct hid_go_cfg {
unsigned char *buf;
+ struct delayed_work go_cfg_setup;
struct completion send_cmd_complete;
+ struct led_classdev *led_cdev;
struct hid_device *hdev;
struct mutex cfg_mutex; /*ensure single synchronous output report*/
u8 fps_mode;
@@ -65,7 +70,11 @@ struct hid_go_cfg {
u32 mcu_version_product;
u32 mcu_version_protocol;
u32 mouse_dpi;
+ u8 rgb_effect;
u8 rgb_en;
+ u8 rgb_mode;
+ u8 rgb_profile;
+ u8 rgb_speed;
u8 tp_en;
u8 tp_vibration_en;
u8 tp_vibration_intensity;
@@ -220,6 +229,41 @@ static const char *const rumble_mode_text[] = {
#define FPS_MODE_DPI 0x02
+enum rgb_config_index {
+ LIGHT_CFG_ALL = 0x01,
+ LIGHT_MODE_SEL,
+ LIGHT_PROFILE_SEL,
+ USR_LIGHT_PROFILE_1,
+ USR_LIGHT_PROFILE_2,
+ USR_LIGHT_PROFILE_3,
+};
+
+enum rgb_mode_index {
+ RGB_MODE_UNKNOWN,
+ RGB_MODE_DYNAMIC,
+ RGB_MODE_CUSTOM,
+};
+
+static const char *const rgb_mode_text[] = {
+ [RGB_MODE_UNKNOWN] = "unknown",
+ [RGB_MODE_DYNAMIC] = "dynamic",
+ [RGB_MODE_CUSTOM] = "custom",
+};
+
+enum rgb_effect_index {
+ RGB_EFFECT_MONO,
+ RGB_EFFECT_BREATHE,
+ RGB_EFFECT_CHROMA,
+ RGB_EFFECT_RAINBOW,
+};
+
+static const char *const rgb_effect_text[] = {
+ [RGB_EFFECT_MONO] = "monocolor",
+ [RGB_EFFECT_BREATHE] = "breathe",
+ [RGB_EFFECT_CHROMA] = "chroma",
+ [RGB_EFFECT_RAINBOW] = "rainbow",
+};
+
static int hid_go_version_event(struct command_report *cmd_rep)
{
switch (cmd_rep->sub_cmd) {
@@ -437,6 +481,33 @@ static int hid_go_fps_dpi_event(struct command_report *cmd_rep)
return 0;
}
+static int hid_go_light_event(struct command_report *cmd_rep)
+{
+ struct led_classdev_mc *mc_cdev;
+
+ switch (cmd_rep->sub_cmd) {
+ case LIGHT_MODE_SEL:
+ drvdata.rgb_mode = cmd_rep->data[0];
+ return 0;
+ case LIGHT_PROFILE_SEL:
+ drvdata.rgb_profile = cmd_rep->data[0];
+ return 0;
+ case USR_LIGHT_PROFILE_1:
+ case USR_LIGHT_PROFILE_2:
+ case USR_LIGHT_PROFILE_3:
+ mc_cdev = lcdev_to_mccdev(drvdata.led_cdev);
+ drvdata.rgb_effect = cmd_rep->data[0];
+ mc_cdev->subled_info[0].intensity = cmd_rep->data[1];
+ mc_cdev->subled_info[1].intensity = cmd_rep->data[2];
+ mc_cdev->subled_info[2].intensity = cmd_rep->data[3];
+ drvdata.led_cdev->brightness = cmd_rep->data[4];
+ drvdata.rgb_speed = cmd_rep->data[5];
+ return 0;
+ default:
+ return -EINVAL;
+ }
+}
+
static int hid_go_set_event_return(struct command_report *cmd_rep)
{
if (cmd_rep->data[0] != 0)
@@ -490,9 +561,13 @@ static int hid_go_raw_event(struct hid_device *hdev, struct hid_report *report,
case GET_DPI_CFG:
ret = hid_go_fps_dpi_event(cmd_rep);
break;
+ case GET_RGB_CFG:
+ ret = hid_go_light_event(cmd_rep);
+ break;
case SET_FEATURE_STATUS:
case SET_MOTOR_CFG:
case SET_DPI_CFG:
+ case SET_RGB_CFG:
ret = hid_go_set_event_return(cmd_rep);
break;
default:
@@ -1077,6 +1152,274 @@ static ssize_t fps_mode_dpi_index_show(struct device *dev,
return sysfs_emit(buf, "500 800 1200 1800\n");
}
+static int rgb_cfg_call(struct hid_device *hdev, enum mcu_command_index cmd,
+ enum rgb_config_index index, u8 *val, size_t size)
+{
+ if (cmd != SET_RGB_CFG && cmd != GET_RGB_CFG)
+ return -EINVAL;
+
+ if (index < LIGHT_CFG_ALL || index > USR_LIGHT_PROFILE_3)
+ return -EINVAL;
+
+ return mcu_property_out(hdev, MCU_CONFIG_DATA, cmd, index, UNSPECIFIED,
+ val, size);
+}
+
+static int rgb_attr_show(void)
+{
+ enum rgb_config_index index;
+
+ index = drvdata.rgb_profile + 3;
+
+ return rgb_cfg_call(drvdata.hdev, GET_RGB_CFG, index, 0, 0);
+};
+
+static ssize_t rgb_effect_store(struct device *dev,
+ struct device_attribute *attr, const char *buf,
+ size_t count)
+{
+ struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(drvdata.led_cdev);
+ enum rgb_config_index index;
+ u8 effect;
+ int ret;
+
+ ret = sysfs_match_string(rgb_effect_text, buf);
+ if (ret < 0)
+ return ret;
+
+ effect = ret;
+ index = drvdata.rgb_profile + 3;
+ u8 rgb_profile[6] = { effect,
+ mc_cdev->subled_info[0].intensity,
+ mc_cdev->subled_info[1].intensity,
+ mc_cdev->subled_info[2].intensity,
+ drvdata.led_cdev->brightness,
+ drvdata.rgb_speed };
+
+ ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, index, rgb_profile, 6);
+ if (ret)
+ return ret;
+
+ drvdata.rgb_effect = effect;
+ return count;
+};
+
+static ssize_t rgb_effect_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ int ret;
+
+ ret = rgb_attr_show();
+ if (ret)
+ return ret;
+
+ if (drvdata.rgb_effect >= ARRAY_SIZE(rgb_effect_text))
+ return -EINVAL;
+
+ return sysfs_emit(buf, "%s\n", rgb_effect_text[drvdata.rgb_effect]);
+}
+
+static ssize_t rgb_effect_index_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ ssize_t count = 0;
+ unsigned int i;
+
+ for (i = 0; i < ARRAY_SIZE(rgb_effect_text); i++)
+ count += sysfs_emit_at(buf, count, "%s ", rgb_effect_text[i]);
+
+ if (count)
+ buf[count - 1] = '\n';
+
+ return count;
+}
+
+static ssize_t rgb_speed_store(struct device *dev,
+ struct device_attribute *attr, const char *buf,
+ size_t count)
+{
+ struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(drvdata.led_cdev);
+ enum rgb_config_index index;
+ int val = 0;
+ int ret;
+
+ ret = kstrtoint(buf, 10, &val);
+ if (ret)
+ return ret;
+
+ if (val < 0 || val > 100)
+ return -EINVAL;
+
+ index = drvdata.rgb_profile + 3;
+ u8 rgb_profile[6] = { drvdata.rgb_effect,
+ mc_cdev->subled_info[0].intensity,
+ mc_cdev->subled_info[1].intensity,
+ mc_cdev->subled_info[2].intensity,
+ drvdata.led_cdev->brightness,
+ val };
+
+ ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, index, rgb_profile, 6);
+ if (ret)
+ return ret;
+
+ drvdata.rgb_speed = val;
+
+ return count;
+};
+
+static ssize_t rgb_speed_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ int ret;
+
+ ret = rgb_attr_show();
+ if (ret)
+ return ret;
+
+ if (drvdata.rgb_speed > 100)
+ return -EINVAL;
+
+ return sysfs_emit(buf, "%hhu\n", drvdata.rgb_speed);
+}
+
+static ssize_t rgb_speed_range_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "0-100\n");
+}
+
+static ssize_t rgb_mode_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ int ret;
+ u8 val;
+
+ ret = sysfs_match_string(rgb_mode_text, buf);
+ if (ret <= 0)
+ return ret;
+
+ val = ret;
+
+ ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, LIGHT_MODE_SEL, &val, 1);
+ if (ret)
+ return ret;
+
+ drvdata.rgb_mode = val;
+
+ return count;
+};
+
+static ssize_t rgb_mode_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ int ret;
+
+ ret = rgb_cfg_call(drvdata.hdev, GET_RGB_CFG, LIGHT_MODE_SEL, 0, 0);
+ if (ret)
+ return ret;
+
+ if (drvdata.rgb_mode >= ARRAY_SIZE(rgb_mode_text))
+ return -EINVAL;
+
+ return sysfs_emit(buf, "%s\n", rgb_mode_text[drvdata.rgb_mode]);
+};
+
+static ssize_t rgb_mode_index_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ ssize_t count = 0;
+ unsigned int i;
+
+ for (i = 1; i < ARRAY_SIZE(rgb_mode_text); i++)
+ count += sysfs_emit_at(buf, count, "%s ", rgb_mode_text[i]);
+
+ if (count)
+ buf[count - 1] = '\n';
+
+ return count;
+}
+
+static ssize_t rgb_profile_store(struct device *dev,
+ struct device_attribute *attr, const char *buf,
+ size_t count)
+{
+ size_t size = 1;
+ int ret;
+ u8 val;
+
+ ret = kstrtou8(buf, 10, &val);
+ if (ret < 0)
+ return ret;
+
+ if (val < 1 || val > 3)
+ return -EINVAL;
+
+ ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, LIGHT_PROFILE_SEL, &val,
+ size);
+ if (ret)
+ return ret;
+
+ drvdata.rgb_profile = val;
+
+ return count;
+};
+
+static ssize_t rgb_profile_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ int ret;
+
+ ret = rgb_cfg_call(drvdata.hdev, GET_RGB_CFG, LIGHT_PROFILE_SEL, 0,
+ 0);
+ if (ret)
+ return ret;
+
+ if (drvdata.rgb_profile < 1 || drvdata.rgb_profile > 3)
+ return -EINVAL;
+
+ return sysfs_emit(buf, "%hhu\n", drvdata.rgb_profile);
+};
+
+static ssize_t rgb_profile_range_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "1-3\n");
+}
+
+static void hid_go_brightness_set(struct led_classdev *led_cdev,
+ enum led_brightness brightness)
+{
+ struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(drvdata.led_cdev);
+ enum rgb_config_index index;
+ int ret;
+
+ if (brightness > led_cdev->max_brightness) {
+ dev_err(led_cdev->dev, "Invalid argument\n");
+ return;
+ }
+
+ index = drvdata.rgb_profile + 3;
+ u8 rgb_profile[6] = { drvdata.rgb_effect,
+ mc_cdev->subled_info[0].intensity,
+ mc_cdev->subled_info[1].intensity,
+ mc_cdev->subled_info[2].intensity,
+ brightness,
+ drvdata.rgb_speed };
+
+ ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, index, rgb_profile, 6);
+ switch (ret) {
+ case 0:
+ led_cdev->brightness = brightness;
+ break;
+ case -ENODEV: /* during switch to IAP -ENODEV is expected */
+ case -ENOSYS: /* during rmmod -ENOSYS is expected */
+ dev_dbg(led_cdev->dev, "Failed to write RGB profile: %i\n", ret);
+ break;
+ default:
+ dev_err(led_cdev->dev, "Failed to write RGB profile: %i\n", ret);
+ };
+}
+
#define LEGO_DEVICE_ATTR_RW(_name, _attrname, _dtype, _rtype, _group) \
static ssize_t _name##_store(struct device *dev, \
struct device_attribute *attr, \
@@ -1387,6 +1730,109 @@ static const struct attribute_group *top_level_attr_groups[] = {
&touchpad_attr_group, NULL,
};
+/* RGB */
+struct go_cfg_attr rgb_enabled = { FEATURE_LIGHT_ENABLE };
+
+LEGO_DEVICE_ATTR_RW(rgb_enabled, "enabled", UNSPECIFIED, index, feature_status);
+static DEVICE_ATTR_RO_NAMED(rgb_effect_index, "effect_index");
+static DEVICE_ATTR_RO_NAMED(rgb_enabled_index, "enabled_index");
+static DEVICE_ATTR_RO_NAMED(rgb_mode_index, "mode_index");
+static DEVICE_ATTR_RO_NAMED(rgb_profile_range, "profile_range");
+static DEVICE_ATTR_RO_NAMED(rgb_speed_range, "speed_range");
+static DEVICE_ATTR_RW_NAMED(rgb_effect, "effect");
+static DEVICE_ATTR_RW_NAMED(rgb_mode, "mode");
+static DEVICE_ATTR_RW_NAMED(rgb_profile, "profile");
+static DEVICE_ATTR_RW_NAMED(rgb_speed, "speed");
+
+static struct attribute *go_rgb_attrs[] = {
+ &dev_attr_rgb_effect.attr,
+ &dev_attr_rgb_effect_index.attr,
+ &dev_attr_rgb_enabled.attr,
+ &dev_attr_rgb_enabled_index.attr,
+ &dev_attr_rgb_mode.attr,
+ &dev_attr_rgb_mode_index.attr,
+ &dev_attr_rgb_profile.attr,
+ &dev_attr_rgb_profile_range.attr,
+ &dev_attr_rgb_speed.attr,
+ &dev_attr_rgb_speed_range.attr,
+ NULL,
+};
+
+static struct attribute_group rgb_attr_group = {
+ .attrs = go_rgb_attrs,
+};
+
+struct mc_subled go_rgb_subled_info[] = {
+ {
+ .color_index = LED_COLOR_ID_RED,
+ .brightness = 0x50,
+ .intensity = 0x24,
+ .channel = 0x1,
+ },
+ {
+ .color_index = LED_COLOR_ID_GREEN,
+ .brightness = 0x50,
+ .intensity = 0x22,
+ .channel = 0x2,
+ },
+ {
+ .color_index = LED_COLOR_ID_BLUE,
+ .brightness = 0x50,
+ .intensity = 0x99,
+ .channel = 0x3,
+ },
+};
+
+struct led_classdev_mc go_cdev_rgb = {
+ .led_cdev = {
+ .name = "go:rgb:joystick_rings",
+ .color = LED_COLOR_ID_RGB,
+ .brightness = 0x50,
+ .max_brightness = 0x64,
+ .brightness_set = hid_go_brightness_set,
+ },
+ .num_colors = ARRAY_SIZE(go_rgb_subled_info),
+ .subled_info = go_rgb_subled_info,
+};
+
+static void cfg_setup(struct work_struct *work)
+{
+ int ret;
+
+ /* RGB */
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA,
+ GET_FEATURE_STATUS, FEATURE_LIGHT_ENABLE,
+ UNSPECIFIED, 0, 0);
+ if (ret < 0) {
+ dev_err(drvdata.led_cdev->dev,
+ "Failed to retrieve RGB enabled: %i\n", ret);
+ return;
+ }
+
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_RGB_CFG,
+ LIGHT_MODE_SEL, UNSPECIFIED, 0, 0);
+ if (ret < 0) {
+ dev_err(drvdata.led_cdev->dev,
+ "Failed to retrieve RGB Mode: %i\n", ret);
+ return;
+ }
+
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_RGB_CFG,
+ LIGHT_PROFILE_SEL, UNSPECIFIED, 0, 0);
+ if (ret < 0) {
+ dev_err(drvdata.led_cdev->dev,
+ "Failed to retrieve RGB Profile: %i\n", ret);
+ return;
+ }
+
+ ret = rgb_attr_show();
+ if (ret < 0) {
+ dev_err(drvdata.led_cdev->dev,
+ "Failed to retrieve RGB Profile Data: %i\n", ret);
+ return;
+ }
+}
+
static int hid_go_cfg_probe(struct hid_device *hdev,
const struct hid_device_id *_id)
{
@@ -1409,14 +1855,40 @@ static int hid_go_cfg_probe(struct hid_device *hdev,
return ret;
}
+ ret = devm_led_classdev_multicolor_register(&hdev->dev, &go_cdev_rgb);
+ if (ret) {
+ dev_err_probe(&hdev->dev, ret, "Failed to create RGB device\n");
+ return ret;
+ }
+
+ ret = devm_device_add_group(go_cdev_rgb.led_cdev.dev, &rgb_attr_group);
+ if (ret) {
+ dev_err_probe(&hdev->dev, ret,
+ "Failed to create RGB configuration attributes\n");
+ return ret;
+ }
+
+ drvdata.led_cdev = &go_cdev_rgb.led_cdev;
+
init_completion(&drvdata.send_cmd_complete);
+ /* Executing calls prior to returning from probe will lock the MCU. Schedule
+ * initial data call after probe has completed and MCU can accept calls.
+ */
+ INIT_DELAYED_WORK(&drvdata.go_cfg_setup, &cfg_setup);
+ ret = schedule_delayed_work(&drvdata.go_cfg_setup, msecs_to_jiffies(2));
+ if (!ret) {
+ dev_err(&hdev->dev,
+ "Failed to schedule startup delayed work\n");
+ return -ENODEV;
+ }
return 0;
}
static void hid_go_cfg_remove(struct hid_device *hdev)
{
guard(mutex)(&drvdata.cfg_mutex);
+ cancel_delayed_work_sync(&drvdata.go_cfg_setup);
sysfs_remove_groups(&hdev->dev.kobj, top_level_attr_groups);
hid_hw_close(hdev);
hid_hw_stop(hdev);
--
2.51.2
^ permalink raw reply related
* [PATCH v2 07/16] HID: hid-lenovo-go: Add Calibration Settings
From: Derek J. Clark @ 2025-12-29 3:17 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Mario Limonciello, Zhixin Zhang, Mia Shao, Mark Pearson,
Pierre-Loup A . Griffais, Derek J . Clark, linux-input, linux-doc,
linux-kernel
In-Reply-To: <20251229031753.581664-1-derekjohn.clark@gmail.com>
Adds calibration enable and last calibration status indicators for the
triggers, joysticks, and handle gyros.
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
drivers/hid/hid-lenovo-go.c | 284 +++++++++++++++++++++++++++++++++++-
1 file changed, 283 insertions(+), 1 deletion(-)
diff --git a/drivers/hid/hid-lenovo-go.c b/drivers/hid/hid-lenovo-go.c
index c8761297f57b..470878b51e97 100644
--- a/drivers/hid/hid-lenovo-go.c
+++ b/drivers/hid/hid-lenovo-go.c
@@ -43,8 +43,11 @@ struct hid_go_cfg {
struct mutex cfg_mutex; /*ensure single synchronous output report*/
u8 fps_mode;
u8 gp_left_auto_sleep_time;
+ u8 gp_left_gyro_cal_status;
+ u8 gp_left_joy_cal_status;
u8 gp_left_notify_en;
u8 gp_left_rumble_mode;
+ u8 gp_left_trigg_cal_status;
u32 gp_left_version_firmware;
u8 gp_left_version_gen;
u32 gp_left_version_hardware;
@@ -52,8 +55,11 @@ struct hid_go_cfg {
u32 gp_left_version_protocol;
u8 gp_mode;
u8 gp_right_auto_sleep_time;
+ u8 gp_right_gyro_cal_status;
+ u8 gp_right_joy_cal_status;
u8 gp_right_notify_en;
u8 gp_right_rumble_mode;
+ u8 gp_right_trigg_cal_status;
u32 gp_right_version_firmware;
u8 gp_right_version_gen;
u32 gp_right_version_hardware;
@@ -227,7 +233,41 @@ static const char *const rumble_mode_text[] = {
[RUMBLE_MODE_RPG] = "rpg",
};
-#define FPS_MODE_DPI 0x02
+#define FPS_MODE_DPI 0x02
+#define TRIGGER_CALIBRATE 0x04
+#define JOYSTICK_CALIBRATE 0x04
+#define GYRO_CALIBRATE 0x06
+
+enum cal_device_type {
+ CALDEV_GYROSCOPE = 0x01,
+ CALDEV_JOYSTICK,
+ CALDEV_TRIGGER,
+ CALDEV_JOY_TRIGGER,
+};
+
+enum cal_enable {
+ CAL_UNKNOWN,
+ CAL_START,
+ CAL_STOP,
+};
+
+static const char *const cal_enabled_text[] = {
+ [CAL_UNKNOWN] = "unknown",
+ [CAL_START] = "start",
+ [CAL_STOP] = "stop",
+};
+
+enum cal_status_index {
+ CAL_STAT_UNKNOWN,
+ CAL_STAT_SUCCESS,
+ CAL_STAT_FAILURE,
+};
+
+static const char *const cal_status_text[] = {
+ [CAL_STAT_UNKNOWN] = "unknown",
+ [CAL_STAT_SUCCESS] = "success",
+ [CAL_STAT_FAILURE] = "failure",
+};
enum rgb_config_index {
LIGHT_CFG_ALL = 0x01,
@@ -264,6 +304,13 @@ static const char *const rgb_effect_text[] = {
[RGB_EFFECT_RAINBOW] = "rainbow",
};
+enum device_status_index {
+ GET_CAL_STATUS = 0x02,
+ GET_UPGRADE_STATUS,
+ GET_MACRO_REC_STATUS,
+ GET_HOTKEY_TRIGG_STATUS,
+};
+
static int hid_go_version_event(struct command_report *cmd_rep)
{
switch (cmd_rep->sub_cmd) {
@@ -508,6 +555,44 @@ static int hid_go_light_event(struct command_report *cmd_rep)
}
}
+static int hid_go_device_status_event(struct command_report *cmd_rep)
+{
+ switch (cmd_rep->device_type) {
+ case LEFT_CONTROLLER:
+ switch (cmd_rep->data[0]) {
+ case CALDEV_GYROSCOPE:
+ drvdata.gp_left_gyro_cal_status = cmd_rep->data[1];
+ return 0;
+ case CALDEV_JOYSTICK:
+ drvdata.gp_left_joy_cal_status = cmd_rep->data[1];
+ return 0;
+ case CALDEV_TRIGGER:
+ drvdata.gp_left_trigg_cal_status = cmd_rep->data[1];
+ return 0;
+ default:
+ return -EINVAL;
+ }
+ break;
+ case RIGHT_CONTROLLER:
+ switch (cmd_rep->data[0]) {
+ case CALDEV_GYROSCOPE:
+ drvdata.gp_right_gyro_cal_status = cmd_rep->data[1];
+ return 0;
+ case CALDEV_JOYSTICK:
+ drvdata.gp_right_joy_cal_status = cmd_rep->data[1];
+ return 0;
+ case CALDEV_TRIGGER:
+ drvdata.gp_right_trigg_cal_status = cmd_rep->data[1];
+ return 0;
+ default:
+ return -EINVAL;
+ }
+ break;
+ default:
+ return -EINVAL;
+ }
+}
+
static int hid_go_set_event_return(struct command_report *cmd_rep)
{
if (cmd_rep->data[0] != 0)
@@ -564,10 +649,16 @@ static int hid_go_raw_event(struct hid_device *hdev, struct hid_report *report,
case GET_RGB_CFG:
ret = hid_go_light_event(cmd_rep);
break;
+ case GET_DEVICE_STATUS:
+ ret = hid_go_device_status_event(cmd_rep);
+ break;
case SET_FEATURE_STATUS:
case SET_MOTOR_CFG:
case SET_DPI_CFG:
case SET_RGB_CFG:
+ case SET_TRIGGER_CFG:
+ case SET_JOYSTICK_CFG:
+ case SET_GYRO_CFG:
ret = hid_go_set_event_return(cmd_rep);
break;
default:
@@ -1152,6 +1243,101 @@ static ssize_t fps_mode_dpi_index_show(struct device *dev,
return sysfs_emit(buf, "500 800 1200 1800\n");
}
+static ssize_t device_status_show(struct device *dev,
+ struct device_attribute *attr, char *buf,
+ enum device_status_index index,
+ enum dev_type device_type,
+ enum cal_device_type cal_type)
+{
+ u8 i;
+
+ switch (index) {
+ case GET_CAL_STATUS:
+ switch (device_type) {
+ case LEFT_CONTROLLER:
+ switch (cal_type) {
+ case CALDEV_GYROSCOPE:
+ i = drvdata.gp_left_gyro_cal_status;
+ break;
+ case CALDEV_JOYSTICK:
+ i = drvdata.gp_left_joy_cal_status;
+ break;
+ case CALDEV_TRIGGER:
+ i = drvdata.gp_left_trigg_cal_status;
+ break;
+ default:
+ return -EINVAL;
+ }
+ break;
+ case RIGHT_CONTROLLER:
+ switch (cal_type) {
+ case CALDEV_GYROSCOPE:
+ i = drvdata.gp_right_gyro_cal_status;
+ break;
+ case CALDEV_JOYSTICK:
+ i = drvdata.gp_right_joy_cal_status;
+ break;
+ case CALDEV_TRIGGER:
+ i = drvdata.gp_right_trigg_cal_status;
+ break;
+ default:
+ return -EINVAL;
+ }
+ break;
+ default:
+ return -EINVAL;
+ }
+ break;
+ default:
+ return -EINVAL;
+ };
+
+ if (i >= ARRAY_SIZE(cal_status_text))
+ return -EINVAL;
+
+ return sysfs_emit(buf, "%s\n", cal_status_text[i]);
+}
+
+static ssize_t calibrate_config_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, u8 cmd, u8 sub_cmd,
+ size_t count, enum dev_type device_type)
+{
+ size_t size = 1;
+ u8 val = 0;
+ int ret;
+
+ ret = sysfs_match_string(cal_enabled_text, buf);
+ if (ret < 0)
+ return ret;
+
+ val = ret;
+ if (!val)
+ size = 0;
+
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, cmd, sub_cmd,
+ device_type, &val, size);
+ if (ret < 0)
+ return ret;
+
+ return count;
+}
+
+static ssize_t calibrate_config_options(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ ssize_t count = 0;
+ unsigned int i;
+
+ for (i = 1; i < ARRAY_SIZE(cal_enabled_text); i++)
+ count += sysfs_emit_at(buf, count, "%s ", cal_enabled_text[i]);
+
+ buf[count - 1] = '\n';
+
+ return count;
+}
+
static int rgb_cfg_call(struct hid_device *hdev, enum mcu_command_index cmd,
enum rgb_config_index index, u8 *val, size_t size)
{
@@ -1458,6 +1644,30 @@ static void hid_go_brightness_set(struct led_classdev *led_cdev,
} \
static DEVICE_ATTR_RO_NAMED(_name, _attrname)
+#define LEGO_CAL_DEVICE_ATTR(_name, _attrname, _scmd, _dtype, _rtype) \
+ static ssize_t _name##_store(struct device *dev, \
+ struct device_attribute *attr, \
+ const char *buf, size_t count) \
+ { \
+ return calibrate_config_store(dev, attr, buf, _name.index, \
+ _scmd, count, _dtype); \
+ } \
+ static ssize_t _name##_##_rtype##_show( \
+ struct device *dev, struct device_attribute *attr, char *buf) \
+ { \
+ return calibrate_config_options(dev, attr, buf); \
+ } \
+ static DEVICE_ATTR_WO_NAMED(_name, _attrname)
+
+#define LEGO_DEVICE_STATUS_ATTR(_name, _attrname, _scmd, _dtype) \
+ static ssize_t _name##_show(struct device *dev, \
+ struct device_attribute *attr, char *buf) \
+ { \
+ return device_status_show(dev, attr, buf, _name.index, _scmd, \
+ _dtype); \
+ } \
+ static DEVICE_ATTR_RO_NAMED(_name, _attrname)
+
/* Gamepad - MCU */
struct go_cfg_attr version_product_mcu = { PRODUCT_VERSION };
LEGO_DEVICE_ATTR_RO(version_product_mcu, "product_version", USB_MCU, version);
@@ -1595,9 +1805,45 @@ LEGO_DEVICE_ATTR_RW(rumble_notification_left, "rumble_notification",
static DEVICE_ATTR_RO_NAMED(rumble_notification_left_index,
"rumble_notification_index");
+struct go_cfg_attr cal_trigg_left = { TRIGGER_CALIBRATE };
+LEGO_CAL_DEVICE_ATTR(cal_trigg_left, "calibrate_trigger", SET_TRIGGER_CFG,
+ LEFT_CONTROLLER, index);
+static DEVICE_ATTR_RO_NAMED(cal_trigg_left_index, "calibrate_trigger_index");
+
+struct go_cfg_attr cal_joy_left = { JOYSTICK_CALIBRATE };
+LEGO_CAL_DEVICE_ATTR(cal_joy_left, "calibrate_joystick", SET_JOYSTICK_CFG,
+ LEFT_CONTROLLER, index);
+static DEVICE_ATTR_RO_NAMED(cal_joy_left_index, "calibrate_joystick_index");
+
+struct go_cfg_attr cal_gyro_left = { GYRO_CALIBRATE };
+LEGO_CAL_DEVICE_ATTR(cal_gyro_left, "calibrate_gyro", SET_GYRO_CFG,
+ LEFT_CONTROLLER, index);
+static DEVICE_ATTR_RO_NAMED(cal_gyro_left_index, "calibrate_gyro_index");
+
+struct go_cfg_attr cal_trigg_left_status = { GET_CAL_STATUS };
+LEGO_DEVICE_STATUS_ATTR(cal_trigg_left_status, "calibrate_trigger_status",
+ LEFT_CONTROLLER, CALDEV_TRIGGER);
+
+struct go_cfg_attr cal_joy_left_status = { GET_CAL_STATUS };
+LEGO_DEVICE_STATUS_ATTR(cal_joy_left_status, "calibrate_joystick_status",
+ LEFT_CONTROLLER, CALDEV_JOYSTICK);
+
+struct go_cfg_attr cal_gyro_left_status = { GET_CAL_STATUS };
+LEGO_DEVICE_STATUS_ATTR(cal_gyro_left_status, "calibrate_gyro_status",
+ LEFT_CONTROLLER, CALDEV_GYROSCOPE);
+
static struct attribute *left_gamepad_attrs[] = {
&dev_attr_auto_sleep_time_left.attr,
&dev_attr_auto_sleep_time_left_range.attr,
+ &dev_attr_cal_gyro_left.attr,
+ &dev_attr_cal_gyro_left_index.attr,
+ &dev_attr_cal_gyro_left_status.attr,
+ &dev_attr_cal_joy_left.attr,
+ &dev_attr_cal_joy_left_index.attr,
+ &dev_attr_cal_joy_left_status.attr,
+ &dev_attr_cal_trigg_left.attr,
+ &dev_attr_cal_trigg_left_index.attr,
+ &dev_attr_cal_trigg_left_status.attr,
&dev_attr_imu_bypass_left.attr,
&dev_attr_imu_bypass_left_index.attr,
&dev_attr_imu_enable_left.attr,
@@ -1666,9 +1912,45 @@ LEGO_DEVICE_ATTR_RW(rumble_notification_right, "rumble_notification",
static DEVICE_ATTR_RO_NAMED(rumble_notification_right_index,
"rumble_notification_index");
+struct go_cfg_attr cal_trigg_right = { TRIGGER_CALIBRATE };
+LEGO_CAL_DEVICE_ATTR(cal_trigg_right, "calibrate_trigger", SET_TRIGGER_CFG,
+ RIGHT_CONTROLLER, index);
+static DEVICE_ATTR_RO_NAMED(cal_trigg_right_index, "calibrate_trigger_index");
+
+struct go_cfg_attr cal_joy_right = { JOYSTICK_CALIBRATE };
+LEGO_CAL_DEVICE_ATTR(cal_joy_right, "calibrate_joystick", SET_JOYSTICK_CFG,
+ RIGHT_CONTROLLER, index);
+static DEVICE_ATTR_RO_NAMED(cal_joy_right_index, "calibrate_joystick_index");
+
+struct go_cfg_attr cal_gyro_right = { GYRO_CALIBRATE };
+LEGO_CAL_DEVICE_ATTR(cal_gyro_right, "calibrate_gyro", SET_GYRO_CFG,
+ RIGHT_CONTROLLER, index);
+static DEVICE_ATTR_RO_NAMED(cal_gyro_right_index, "calibrate_gyro_index");
+
+struct go_cfg_attr cal_trigg_right_status = { GET_CAL_STATUS };
+LEGO_DEVICE_STATUS_ATTR(cal_trigg_right_status, "calibrate_trigger_status",
+ RIGHT_CONTROLLER, CALDEV_TRIGGER);
+
+struct go_cfg_attr cal_joy_right_status = { GET_CAL_STATUS };
+LEGO_DEVICE_STATUS_ATTR(cal_joy_right_status, "calibrate_joystick_status",
+ RIGHT_CONTROLLER, CALDEV_JOYSTICK);
+
+struct go_cfg_attr cal_gyro_right_status = { GET_CAL_STATUS };
+LEGO_DEVICE_STATUS_ATTR(cal_gyro_right_status, "calibrate_gyro_status",
+ RIGHT_CONTROLLER, CALDEV_GYROSCOPE);
+
static struct attribute *right_gamepad_attrs[] = {
&dev_attr_auto_sleep_time_right.attr,
&dev_attr_auto_sleep_time_right_range.attr,
+ &dev_attr_cal_gyro_right.attr,
+ &dev_attr_cal_gyro_right_index.attr,
+ &dev_attr_cal_gyro_right_status.attr,
+ &dev_attr_cal_joy_right.attr,
+ &dev_attr_cal_joy_right_index.attr,
+ &dev_attr_cal_joy_right_status.attr,
+ &dev_attr_cal_trigg_right.attr,
+ &dev_attr_cal_trigg_right_index.attr,
+ &dev_attr_cal_trigg_right_status.attr,
&dev_attr_imu_bypass_right.attr,
&dev_attr_imu_bypass_right_index.attr,
&dev_attr_imu_enable_right.attr,
--
2.51.2
^ permalink raw reply related
* [PATCH v2 08/16] HID: hid-lenovo-go: Add OS Mode Toggle
From: Derek J. Clark @ 2025-12-29 3:17 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Mario Limonciello, Zhixin Zhang, Mia Shao, Mark Pearson,
Pierre-Loup A . Griffais, Derek J . Clark, linux-input, linux-doc,
linux-kernel
In-Reply-To: <20251229031753.581664-1-derekjohn.clark@gmail.com>
Adds OS Mode toggle, who's primary function is to change the built-in
functional chords to use the right handle legion button instead of the
left handle legion button as the mode shift key. This setting needs to
be restored after resume, so a reset-resume hook is added.
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
drivers/hid/hid-lenovo-go.c | 137 ++++++++++++++++++++++++++++++++++++
1 file changed, 137 insertions(+)
diff --git a/drivers/hid/hid-lenovo-go.c b/drivers/hid/hid-lenovo-go.c
index 470878b51e97..f3623871edbf 100644
--- a/drivers/hid/hid-lenovo-go.c
+++ b/drivers/hid/hid-lenovo-go.c
@@ -76,6 +76,7 @@ struct hid_go_cfg {
u32 mcu_version_product;
u32 mcu_version_protocol;
u32 mouse_dpi;
+ u8 os_mode;
u8 rgb_effect;
u8 rgb_en;
u8 rgb_mode;
@@ -166,6 +167,8 @@ enum feature_status_index {
FEATURE_GAMEPAD_MODE = 0x0e,
};
+#define FEATURE_OS_MODE 0x69
+
enum fps_switch_status_index {
FPS_STATUS_UNKNOWN,
GAMEPAD,
@@ -311,6 +314,23 @@ enum device_status_index {
GET_HOTKEY_TRIGG_STATUS,
};
+enum os_mode_cfg_index {
+ SET_OS_MODE = 0x09,
+ GET_OS_MODE,
+};
+
+enum os_mode_index {
+ OS_UNKNOWN,
+ WINDOWS,
+ LINUX,
+};
+
+static const char *const os_mode_text[] = {
+ [OS_UNKNOWN] = "unknown",
+ [WINDOWS] = "windows",
+ [LINUX] = "linux",
+};
+
static int hid_go_version_event(struct command_report *cmd_rep)
{
switch (cmd_rep->sub_cmd) {
@@ -593,6 +613,21 @@ static int hid_go_device_status_event(struct command_report *cmd_rep)
}
}
+static int hid_go_os_mode_cfg_event(struct command_report *cmd_rep)
+{
+ switch (cmd_rep->sub_cmd) {
+ case SET_OS_MODE:
+ if (cmd_rep->data[0] != 1)
+ return -EIO;
+ return 0;
+ case GET_OS_MODE:
+ drvdata.os_mode = cmd_rep->data[0];
+ return 0;
+ default:
+ return -EINVAL;
+ };
+}
+
static int hid_go_set_event_return(struct command_report *cmd_rep)
{
if (cmd_rep->data[0] != 0)
@@ -666,6 +701,9 @@ static int hid_go_raw_event(struct hid_device *hdev, struct hid_report *report,
break;
};
break;
+ case OS_MODE_DATA:
+ ret = hid_go_os_mode_cfg_event(cmd_rep);
+ break;
default:
goto passthrough;
};
@@ -1338,6 +1376,64 @@ static ssize_t calibrate_config_options(struct device *dev,
return count;
}
+static ssize_t os_mode_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ size_t size = 1;
+ int ret;
+ u8 val;
+
+ ret = sysfs_match_string(os_mode_text, buf);
+ if (ret <= 0)
+ return ret;
+
+ val = ret;
+ ret = mcu_property_out(drvdata.hdev, OS_MODE_DATA, FEATURE_OS_MODE,
+ SET_OS_MODE, USB_MCU, &val, size);
+ if (ret < 0)
+ return ret;
+
+ drvdata.os_mode = val;
+
+ return count;
+}
+
+static ssize_t os_mode_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ ssize_t count = 0;
+ int ret;
+ u8 i;
+
+ ret = mcu_property_out(drvdata.hdev, OS_MODE_DATA, FEATURE_OS_MODE,
+ GET_OS_MODE, USB_MCU, 0, 0);
+ if (ret)
+ return ret;
+
+ i = drvdata.os_mode;
+ if (i >= ARRAY_SIZE(os_mode_text))
+ return -EINVAL;
+
+ count = sysfs_emit(buf, "%s\n", os_mode_text[i]);
+
+ return count;
+}
+
+static ssize_t os_mode_index_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ ssize_t count = 0;
+ unsigned int i;
+
+ for (i = 1; i < ARRAY_SIZE(os_mode_text); i++)
+ count += sysfs_emit_at(buf, count, "%s ", os_mode_text[i]);
+
+ if (count)
+ buf[count - 1] = '\n';
+
+ return count;
+}
+
static int rgb_cfg_call(struct hid_device *hdev, enum mcu_command_index cmd,
enum rgb_config_index index, u8 *val, size_t size)
{
@@ -1704,6 +1800,9 @@ static DEVICE_ATTR_RO_NAMED(gamepad_rumble_intensity_index,
static DEVICE_ATTR_RW(fps_mode_dpi);
static DEVICE_ATTR_RO(fps_mode_dpi_index);
+static DEVICE_ATTR_RW(os_mode);
+static DEVICE_ATTR_RO(os_mode_index);
+
static struct attribute *mcu_attrs[] = {
&dev_attr_fps_mode_dpi.attr,
&dev_attr_fps_mode_dpi_index.attr,
@@ -1712,6 +1811,8 @@ static struct attribute *mcu_attrs[] = {
&dev_attr_gamepad_mode_index.attr,
&dev_attr_gamepad_rumble_intensity.attr,
&dev_attr_gamepad_rumble_intensity_index.attr,
+ &dev_attr_os_mode.attr,
+ &dev_attr_os_mode_index.attr,
&dev_attr_reset_mcu.attr,
&dev_attr_version_firmware_mcu.attr,
&dev_attr_version_gen_mcu.attr,
@@ -2177,6 +2278,27 @@ static void hid_go_cfg_remove(struct hid_device *hdev)
hid_set_drvdata(hdev, NULL);
}
+static int hid_go_cfg_reset_resume(struct hid_device *hdev)
+{
+ u8 os_mode = drvdata.os_mode;
+ int ret;
+
+ ret = mcu_property_out(drvdata.hdev, OS_MODE_DATA, FEATURE_OS_MODE,
+ SET_OS_MODE, USB_MCU, &os_mode, 1);
+ if (ret < 0)
+ return ret;
+
+ ret = mcu_property_out(drvdata.hdev, OS_MODE_DATA, FEATURE_OS_MODE,
+ GET_OS_MODE, USB_MCU, 0, 0);
+ if (ret < 0)
+ return ret;
+
+ if (drvdata.os_mode != os_mode)
+ return -ENODEV;
+
+ return 0;
+}
+
static int hid_go_probe(struct hid_device *hdev, const struct hid_device_id *id)
{
int ret, ep;
@@ -2235,6 +2357,20 @@ static void hid_go_remove(struct hid_device *hdev)
}
}
+static int hid_go_reset_resume(struct hid_device *hdev)
+{
+ int ep = get_endpoint_address(hdev);
+
+ switch (ep) {
+ case GO_GP_INTF_IN:
+ return hid_go_cfg_reset_resume(hdev);
+ default:
+ break;
+ }
+
+ return 0;
+}
+
static const struct hid_device_id hid_go_devices[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_LENOVO,
USB_DEVICE_ID_LENOVO_LEGION_GO2_XINPUT) },
@@ -2254,6 +2390,7 @@ static struct hid_driver hid_lenovo_go = {
.probe = hid_go_probe,
.remove = hid_go_remove,
.raw_event = hid_go_raw_event,
+ .reset_resume = hid_go_reset_resume,
};
module_hid_driver(hid_lenovo_go);
--
2.51.2
^ permalink raw reply related
* [PATCH v2 09/16] HID: Include firmware version in the uevent
From: Derek J. Clark @ 2025-12-29 3:17 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Mario Limonciello, Zhixin Zhang, Mia Shao, Mark Pearson,
Pierre-Loup A . Griffais, Derek J . Clark, linux-input, linux-doc,
linux-kernel, Richard Hughes
In-Reply-To: <20251229031753.581664-1-derekjohn.clark@gmail.com>
From: Mario Limonciello <mario.limonciello@amd.com>
Userspace software fwupd probes some HID devices when the daemon starts
up to determine the current firmware version in order to be able to offer
updated firmware if the manufacturer has made it available.
In order to do this fwupd will detach the existing kernel driver if one
is present, send a HID command and then reattach the kernel driver.
This can be problematic if the user is using the HID device at the time
that fwupd probes the hardware and can cause a few frames of input to be
dropped. In some cases HID drivers already have a command to look up the
firmware version, and so if that is exported to userspace fwupd can discover
it and avoid needing to detach the kernel driver until it's time to update
the device.
Introduce a new member in the struct hid_device for the version and export
a new uevent variable HID_FIRMWARE_VERSION that will display the version
that HID drivers obtained.
Cc: Richard Hughes <hughsient@gmail.com>
Signed-off-by: Mario Limonciello <mario.limonciello@amd.com>
---
drivers/hid/hid-core.c | 5 +++++
include/linux/hid.h | 1 +
2 files changed, 6 insertions(+)
diff --git a/drivers/hid/hid-core.c b/drivers/hid/hid-core.c
index a5b3a8ca2fcb..524f2b9ed512 100644
--- a/drivers/hid/hid-core.c
+++ b/drivers/hid/hid-core.c
@@ -2887,6 +2887,11 @@ static int hid_uevent(const struct device *dev, struct kobj_uevent_env *env)
if (add_uevent_var(env, "MODALIAS=hid:b%04Xg%04Xv%08Xp%08X",
hdev->bus, hdev->group, hdev->vendor, hdev->product))
return -ENOMEM;
+ if (hdev->firmware_version) {
+ if (add_uevent_var(env, "HID_FIRMWARE_VERSION=0x%04llX",
+ hdev->firmware_version))
+ return -ENOMEM;
+ }
return 0;
}
diff --git a/include/linux/hid.h b/include/linux/hid.h
index a4ddb94e3ee5..8249da55c9ba 100644
--- a/include/linux/hid.h
+++ b/include/linux/hid.h
@@ -698,6 +698,7 @@ struct hid_device {
char name[128]; /* Device name */
char phys[64]; /* Device physical location */
char uniq[64]; /* Device unique identifier (serial #) */
+ u64 firmware_version; /* Firmware version */
void *driver_data;
--
2.51.2
^ permalink raw reply related
* [PATCH v2 10/16] HID: hid-lenovo-go-s: Add Lenovo Legion Go S Series HID Driver
From: Derek J. Clark @ 2025-12-29 3:17 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Mario Limonciello, Zhixin Zhang, Mia Shao, Mark Pearson,
Pierre-Loup A . Griffais, Derek J . Clark, linux-input, linux-doc,
linux-kernel
In-Reply-To: <20251229031753.581664-1-derekjohn.clark@gmail.com>
Adds initial framework for a new HID driver, hid-lenovo-go-s, along with
a uevent to report the firmware version for the MCU.
This driver primarily provides access to the configurable settings of the
Lenovo Legion Go S controller. It will attach if the controller is in
xinput or dinput mode. Non-configuration raw reports are forwarded to
ensure the other endpoints continue to function as normal.
Co-developed-by: Mario Limonciello <mario.limonciello@amd.com>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
MAINTAINERS | 1 +
drivers/hid/Kconfig | 12 ++
drivers/hid/Makefile | 1 +
drivers/hid/hid-ids.h | 4 +
drivers/hid/hid-lenovo-go-s.c | 293 ++++++++++++++++++++++++++++++++++
5 files changed, 311 insertions(+)
create mode 100644 drivers/hid/hid-lenovo-go-s.c
diff --git a/MAINTAINERS b/MAINTAINERS
index b5ad29d24e3e..be4a0fcf23dd 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -14139,6 +14139,7 @@ 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-s.c
F: drivers/hid/hid-lenovo-go.c
LETSKETCH HID TABLET DRIVER
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index 74ac6793b29a..2b1a9fa93758 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -633,6 +633,18 @@ config HID_LENOVO_GO
and Legion Go 2 Handheld Console Controllers. Say M here to compile this
driver as a module. The module will be called hid-lenovo-go.
+config HID_LENOVO_GO_S
+ tristate "HID Driver for Lenovo Legion Go S Controller"
+ depends on USB_HID
+ select LEDS_CLASS
+ select LEDS_CLASS_MULTICOLOR
+ help
+ Support for Lenovo Legion Go S Handheld Console Controller.
+
+ Say Y here to include configuration interface support for the Lenovo Legion Go
+ S. Say M here to compile this driver as a module. The module will be called
+ hid-lenovo-go-s.
+
config HID_LETSKETCH
tristate "Letsketch WP9620N tablets"
depends on USB_HID
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index 11435bce4e47..ef9169974bf0 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -77,6 +77,7 @@ obj-$(CONFIG_HID_KYSONA) += hid-kysona.o
obj-$(CONFIG_HID_LCPOWER) += hid-lcpower.o
obj-$(CONFIG_HID_LENOVO) += hid-lenovo.o
obj-$(CONFIG_HID_LENOVO_GO) += hid-lenovo-go.o
+obj-$(CONFIG_HID_LENOVO_GO_S) += hid-lenovo-go-s.o
obj-$(CONFIG_HID_LETSKETCH) += hid-letsketch.o
obj-$(CONFIG_HID_LOGITECH) += hid-logitech.o
obj-$(CONFIG_HID_LOGITECH) += hid-lg-g15.o
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 309e15580a38..eee1091125ca 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -723,6 +723,10 @@
#define USB_DEVICE_ID_ITE8595 0x8595
#define USB_DEVICE_ID_ITE_MEDION_E1239T 0xce50
+#define USB_VENDOR_ID_QHE 0x1a86
+#define USB_DEVICE_ID_LENOVO_LEGION_GO_S_XINPUT 0xe310
+#define USB_DEVICE_ID_LENOVO_LEGION_GO_S_DINPUT 0xe311
+
#define USB_VENDOR_ID_JABRA 0x0b0e
#define USB_DEVICE_ID_JABRA_SPEAK_410 0x0412
#define USB_DEVICE_ID_JABRA_SPEAK_510 0x0420
diff --git a/drivers/hid/hid-lenovo-go-s.c b/drivers/hid/hid-lenovo-go-s.c
new file mode 100644
index 000000000000..3e9041e746df
--- /dev/null
+++ b/drivers/hid/hid-lenovo-go-s.c
@@ -0,0 +1,293 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * HID driver for Lenovo Legion Go S devices.
+ *
+ * Copyright (c) 2025 Derek J. Clark <derekjohn.clark@gmail.com>
+ */
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/array_size.h>
+#include <linux/cleanup.h>
+#include <linux/completion.h>
+#include <linux/delay.h>
+#include <linux/dev_printk.h>
+#include <linux/device.h>
+#include <linux/hid.h>
+#include <linux/jiffies.h>
+#include <linux/mutex.h>
+#include <linux/printk.h>
+#include <linux/string.h>
+#include <linux/types.h>
+#include <linux/unaligned.h>
+#include <linux/usb.h>
+#include <linux/workqueue.h>
+#include <linux/workqueue_types.h>
+
+#include "hid-ids.h"
+
+#define GO_S_CFG_INTF_IN 0x84
+#define GO_S_PACKET_SIZE 64
+
+struct hid_gos_cfg {
+ unsigned char *buf;
+ struct delayed_work gos_cfg_setup;
+ struct completion send_cmd_complete;
+ struct hid_device *hdev;
+ struct mutex cfg_mutex; /*ensure single synchronous output report*/
+} drvdata;
+
+struct command_report {
+ u8 cmd;
+ u8 sub_cmd;
+ u8 data[63];
+} __packed;
+
+struct version_report {
+ u8 cmd;
+ u32 version;
+ u8 reserved[59];
+} __packed;
+
+enum mcu_command_index {
+ GET_VERSION = 0x01,
+ GET_MCU_ID,
+ GET_GAMEPAD_CFG,
+ SET_GAMEPAD_CFG,
+ GET_TP_PARAM,
+ SET_TP_PARAM,
+ GET_RGB_CFG = 0x0f,
+ SET_RGB_CFG,
+ GET_PL_TEST = 0xdf,
+};
+
+#define FEATURE_NONE 0x00
+
+static int hid_gos_version_event(u8 *data)
+{
+ struct version_report *ver_rep = (struct version_report *)data;
+
+ drvdata.hdev->firmware_version = get_unaligned_le32(&ver_rep->version);
+ return 0;
+}
+
+static u8 get_endpoint_address(struct hid_device *hdev)
+{
+ struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
+ struct usb_host_endpoint *ep;
+
+ if (intf) {
+ ep = intf->cur_altsetting->endpoint;
+ if (ep)
+ return ep->desc.bEndpointAddress;
+ }
+
+ return -ENODEV;
+}
+
+static int hid_gos_raw_event(struct hid_device *hdev, struct hid_report *report,
+ u8 *data, int size)
+{
+ struct command_report *cmd_rep;
+ int ep, ret;
+
+ if (size != GO_S_PACKET_SIZE)
+ goto passthrough;
+
+ ep = get_endpoint_address(hdev);
+ if (ep != GO_S_CFG_INTF_IN)
+ goto passthrough;
+
+ cmd_rep = (struct command_report *)data;
+
+ switch (cmd_rep->cmd) {
+ case GET_VERSION:
+ ret = hid_gos_version_event(data);
+ break;
+ default:
+ ret = -EINVAL;
+ break;
+ }
+ dev_dbg(&hdev->dev, "Rx data as raw input report: [%*ph]\n",
+ GO_S_PACKET_SIZE, data);
+
+ complete(&drvdata.send_cmd_complete);
+ return ret;
+
+passthrough:
+ /* Forward other HID reports so they generate events */
+ hid_input_report(hdev, HID_INPUT_REPORT, data, size, 1);
+ return 0;
+}
+
+static int mcu_property_out(struct hid_device *hdev, u8 command, u8 index,
+ u8 *data, size_t len)
+{
+ u8 header[] = { command, index };
+ size_t header_size = ARRAY_SIZE(header);
+ size_t total_size = header_size + len;
+ int timeout = 5;
+ int ret;
+
+ /* PL_TEST commands can take longer because they go out to another device */
+ if (command == GET_PL_TEST)
+ timeout = 200;
+
+ guard(mutex)(&drvdata.cfg_mutex);
+ memcpy(drvdata.buf, header, header_size);
+ memcpy(drvdata.buf + header_size, data, len);
+ memset(drvdata.buf + total_size, 0, GO_S_PACKET_SIZE - total_size);
+
+ dev_dbg(&hdev->dev, "Send data as raw output report: [%*ph]\n",
+ GO_S_PACKET_SIZE, drvdata.buf);
+
+ ret = hid_hw_output_report(hdev, drvdata.buf, GO_S_PACKET_SIZE);
+ if (ret < 0)
+ return ret;
+
+ ret = ret == GO_S_PACKET_SIZE ? 0 : -EINVAL;
+ if (ret)
+ return ret;
+
+ ret = wait_for_completion_interruptible_timeout(&drvdata.send_cmd_complete,
+ msecs_to_jiffies(timeout));
+
+ if (ret == 0) /* timeout occurred */
+ ret = -EBUSY;
+ if (ret > 0) /* timeout/interrupt didn't occur */
+ ret = 0;
+
+ reinit_completion(&drvdata.send_cmd_complete);
+ return ret;
+}
+
+static void cfg_setup(struct work_struct *work)
+{
+ int ret;
+
+ ret = mcu_property_out(drvdata.hdev, GET_VERSION, FEATURE_NONE, 0, 0);
+ if (ret) {
+ dev_err(&drvdata.hdev->dev,
+ "Failed to retrieve MCU Version: %i\n", ret);
+ return;
+ }
+}
+
+static int hid_gos_cfg_probe(struct hid_device *hdev,
+ const struct hid_device_id *_id)
+{
+ unsigned char *buf;
+ int ret;
+
+ buf = devm_kzalloc(&hdev->dev, GO_S_PACKET_SIZE, GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+
+ hid_set_drvdata(hdev, &drvdata);
+ drvdata.buf = buf;
+ drvdata.hdev = hdev;
+ mutex_init(&drvdata.cfg_mutex);
+
+ init_completion(&drvdata.send_cmd_complete);
+
+ /* Executing calls prior to returning from probe will lock the MCU. Schedule
+ * initial data call after probe has completed and MCU can accept calls.
+ */
+ INIT_DELAYED_WORK(&drvdata.gos_cfg_setup, &cfg_setup);
+ ret = schedule_delayed_work(&drvdata.gos_cfg_setup,
+ msecs_to_jiffies(2));
+ if (!ret) {
+ dev_err(&hdev->dev,
+ "Failed to schedule startup delayed work\n");
+ return -ENODEV;
+ }
+ return 0;
+}
+
+static void hid_gos_cfg_remove(struct hid_device *hdev)
+{
+ guard(mutex)(&drvdata.cfg_mutex);
+ cancel_delayed_work_sync(&drvdata.gos_cfg_setup);
+ hid_hw_close(hdev);
+ hid_hw_stop(hdev);
+ hid_set_drvdata(hdev, NULL);
+}
+
+static int hid_gos_probe(struct hid_device *hdev,
+ const struct hid_device_id *id)
+{
+ int ret, ep;
+
+ hdev->quirks |= HID_QUIRK_INPUT_PER_APP | HID_QUIRK_MULTI_INPUT;
+
+ ret = hid_parse(hdev);
+ if (ret) {
+ hid_err(hdev, "Parse failed\n");
+ return ret;
+ }
+
+ ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+ if (ret) {
+ hid_err(hdev, "Failed to start HID device\n");
+ return ret;
+ }
+
+ ret = hid_hw_open(hdev);
+ if (ret) {
+ hid_err(hdev, "Failed to open HID device\n");
+ hid_hw_stop(hdev);
+ return ret;
+ }
+
+ ep = get_endpoint_address(hdev);
+ if (ep != GO_S_CFG_INTF_IN) {
+ dev_dbg(&hdev->dev,
+ "Started interface %x as generic HID device.\n", ep);
+ return 0;
+ }
+
+ ret = hid_gos_cfg_probe(hdev, id);
+ if (ret)
+ dev_err_probe(&hdev->dev, ret,
+ "Failed to start configuration interface");
+
+ dev_dbg(&hdev->dev, "Started Legion Go S HID Device: %x\n", ep);
+ return ret;
+}
+
+static void hid_gos_remove(struct hid_device *hdev)
+{
+ int ep = get_endpoint_address(hdev);
+
+ switch (ep) {
+ case GO_S_CFG_INTF_IN:
+ hid_gos_cfg_remove(hdev);
+ break;
+ default:
+ hid_hw_close(hdev);
+ hid_hw_stop(hdev);
+
+ break;
+ }
+}
+
+static const struct hid_device_id hid_gos_devices[] = {
+ { HID_USB_DEVICE(USB_VENDOR_ID_QHE,
+ USB_DEVICE_ID_LENOVO_LEGION_GO_S_XINPUT) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_QHE,
+ USB_DEVICE_ID_LENOVO_LEGION_GO_S_DINPUT) },
+ {}
+};
+
+MODULE_DEVICE_TABLE(hid, hid_gos_devices);
+static struct hid_driver hid_lenovo_go_s = {
+ .name = "hid-lenovo-go-s",
+ .id_table = hid_gos_devices,
+ .probe = hid_gos_probe,
+ .remove = hid_gos_remove,
+ .raw_event = hid_gos_raw_event,
+};
+module_hid_driver(hid_lenovo_go_s);
+
+MODULE_AUTHOR("Derek J. Clark");
+MODULE_DESCRIPTION("HID Driver for Lenovo Legion Go S Series gamepad.");
+MODULE_LICENSE("GPL");
--
2.51.2
^ permalink raw reply related
* [PATCH v2 11/16] HID: hid-lenovo-go-s: Add MCU ID Attribute
From: Derek J. Clark @ 2025-12-29 3:17 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Mario Limonciello, Zhixin Zhang, Mia Shao, Mark Pearson,
Pierre-Loup A . Griffais, Derek J . Clark, linux-input, linux-doc,
linux-kernel
In-Reply-To: <20251229031753.581664-1-derekjohn.clark@gmail.com>
Adds command to probe for the MCU ID of the Lenovo Legion Go S
Controller and assign it to a device attribute.
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
drivers/hid/hid-lenovo-go-s.c | 56 +++++++++++++++++++++++++++++++++++
1 file changed, 56 insertions(+)
diff --git a/drivers/hid/hid-lenovo-go-s.c b/drivers/hid/hid-lenovo-go-s.c
index 3e9041e746df..36cee6c3d4cc 100644
--- a/drivers/hid/hid-lenovo-go-s.c
+++ b/drivers/hid/hid-lenovo-go-s.c
@@ -17,6 +17,7 @@
#include <linux/mutex.h>
#include <linux/printk.h>
#include <linux/string.h>
+#include <linux/sysfs.h>
#include <linux/types.h>
#include <linux/unaligned.h>
#include <linux/usb.h>
@@ -34,8 +35,13 @@ struct hid_gos_cfg {
struct completion send_cmd_complete;
struct hid_device *hdev;
struct mutex cfg_mutex; /*ensure single synchronous output report*/
+ u8 mcu_id[12];
} drvdata;
+struct gos_cfg_attr {
+ u8 index;
+};
+
struct command_report {
u8 cmd;
u8 sub_cmd;
@@ -70,6 +76,14 @@ static int hid_gos_version_event(u8 *data)
return 0;
}
+static int hid_gos_mcu_id_event(struct command_report *cmd_rep)
+{
+ drvdata.mcu_id[0] = cmd_rep->sub_cmd;
+ memcpy(&drvdata.mcu_id[1], cmd_rep->data, 11);
+
+ return 0;
+}
+
static u8 get_endpoint_address(struct hid_device *hdev)
{
struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
@@ -103,6 +117,9 @@ static int hid_gos_raw_event(struct hid_device *hdev, struct hid_report *report,
case GET_VERSION:
ret = hid_gos_version_event(data);
break;
+ case GET_MCU_ID:
+ ret = hid_gos_mcu_id_event(cmd_rep);
+ break;
default:
ret = -EINVAL;
break;
@@ -160,10 +177,41 @@ static int mcu_property_out(struct hid_device *hdev, u8 command, u8 index,
return ret;
}
+static ssize_t mcu_id_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ return sysfs_emit(buf, "%*phN\n", 12, &drvdata.mcu_id);
+}
+
+/* MCU */
+static DEVICE_ATTR_RO(mcu_id);
+
+static struct attribute *legos_mcu_attrs[] = {
+ &dev_attr_mcu_id.attr,
+ NULL,
+};
+
+static const struct attribute_group mcu_attr_group = {
+ .attrs = legos_mcu_attrs,
+};
+
+static const struct attribute_group *top_level_attr_groups[] = {
+ &mcu_attr_group,
+ NULL,
+};
+
static void cfg_setup(struct work_struct *work)
{
int ret;
+ /* MCU */
+ ret = mcu_property_out(drvdata.hdev, GET_MCU_ID, FEATURE_NONE, 0, 0);
+ if (ret) {
+ dev_err(&drvdata.hdev->dev, "Failed to retrieve MCU ID: %i\n",
+ ret);
+ return;
+ }
+
ret = mcu_property_out(drvdata.hdev, GET_VERSION, FEATURE_NONE, 0, 0);
if (ret) {
dev_err(&drvdata.hdev->dev,
@@ -187,6 +235,13 @@ static int hid_gos_cfg_probe(struct hid_device *hdev,
drvdata.hdev = hdev;
mutex_init(&drvdata.cfg_mutex);
+ ret = sysfs_create_groups(&hdev->dev.kobj, top_level_attr_groups);
+ if (ret) {
+ dev_err_probe(&hdev->dev, ret,
+ "Failed to create gamepad configuration attributes\n");
+ return ret;
+ }
+
init_completion(&drvdata.send_cmd_complete);
/* Executing calls prior to returning from probe will lock the MCU. Schedule
@@ -207,6 +262,7 @@ static void hid_gos_cfg_remove(struct hid_device *hdev)
{
guard(mutex)(&drvdata.cfg_mutex);
cancel_delayed_work_sync(&drvdata.gos_cfg_setup);
+ sysfs_remove_groups(&hdev->dev.kobj, top_level_attr_groups);
hid_hw_close(hdev);
hid_hw_stop(hdev);
hid_set_drvdata(hdev, NULL);
--
2.51.2
^ permalink raw reply related
* [PATCH v2 12/16] HID: hid-lenovo-go-s: Add Feature Status Attributes
From: Derek J. Clark @ 2025-12-29 3:17 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Mario Limonciello, Zhixin Zhang, Mia Shao, Mark Pearson,
Pierre-Loup A . Griffais, Derek J . Clark, linux-input, linux-doc,
linux-kernel
In-Reply-To: <20251229031753.581664-1-derekjohn.clark@gmail.com>
Adds features status attributes for the gamepad, MCU, touchpad/mouse,
and IMU devices.
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
drivers/hid/hid-lenovo-go-s.c | 524 +++++++++++++++++++++++++++++++++-
1 file changed, 523 insertions(+), 1 deletion(-)
diff --git a/drivers/hid/hid-lenovo-go-s.c b/drivers/hid/hid-lenovo-go-s.c
index 36cee6c3d4cc..076afdaa9628 100644
--- a/drivers/hid/hid-lenovo-go-s.c
+++ b/drivers/hid/hid-lenovo-go-s.c
@@ -14,6 +14,7 @@
#include <linux/device.h>
#include <linux/hid.h>
#include <linux/jiffies.h>
+#include <linux/kstrtox.h>
#include <linux/mutex.h>
#include <linux/printk.h>
#include <linux/string.h>
@@ -35,7 +36,17 @@ struct hid_gos_cfg {
struct completion send_cmd_complete;
struct hid_device *hdev;
struct mutex cfg_mutex; /*ensure single synchronous output report*/
+ u8 gp_auto_sleep_time;
+ u8 gp_dpad_mode;
+ u8 gp_mode;
+ u8 gp_poll_rate;
+ u8 imu_bypass_en;
+ u8 imu_sensor_en;
u8 mcu_id[12];
+ u8 mouse_step;
+ u8 os_mode;
+ u8 rgb_en;
+ u8 tp_en;
} drvdata;
struct gos_cfg_attr {
@@ -66,7 +77,73 @@ enum mcu_command_index {
GET_PL_TEST = 0xdf,
};
-#define FEATURE_NONE 0x00
+enum feature_enabled_index {
+ FEATURE_DISABLED,
+ FEATURE_ENABLED,
+};
+
+static const char *const feature_enabled_text[] = {
+ [FEATURE_DISABLED] = "false",
+ [FEATURE_ENABLED] = "true",
+};
+
+enum feature_status_index {
+ FEATURE_NONE = 0x00,
+ FEATURE_GAMEPAD_MODE = 0x01,
+ FEATURE_AUTO_SLEEP_TIME = 0x04,
+ FEATURE_IMU_BYPASS,
+ FEATURE_RGB_ENABLE,
+ FEATURE_IMU_ENABLE,
+ FEATURE_TOUCHPAD_ENABLE,
+ FEATURE_OS_MODE = 0x0A,
+ FEATURE_POLL_RATE = 0x10,
+ FEATURE_DPAD_MODE,
+ FEATURE_MOUSE_WHEEL_STEP,
+};
+
+enum gamepad_mode_index {
+ XINPUT,
+ DINPUT,
+};
+
+static const char *const gamepad_mode_text[] = {
+ [XINPUT] = "xinput",
+ [DINPUT] = "dinput",
+};
+
+enum os_type_index {
+ WINDOWS,
+ LINUX,
+};
+
+static const char *const os_type_text[] = {
+ [WINDOWS] = "windows",
+ [LINUX] = "linux",
+};
+
+enum poll_rate_index {
+ HZ125,
+ HZ250,
+ HZ500,
+ HZ1000,
+};
+
+static const char *const poll_rate_text[] = {
+ [HZ125] = "125",
+ [HZ250] = "250",
+ [HZ500] = "500",
+ [HZ1000] = "1000",
+};
+
+enum dpad_mode_index {
+ DIR8,
+ DIR4,
+};
+
+static const char *const dpad_mode_text[] = {
+ [DIR8] = "8-way",
+ [DIR4] = "4-way",
+};
static int hid_gos_version_event(u8 *data)
{
@@ -84,6 +161,57 @@ static int hid_gos_mcu_id_event(struct command_report *cmd_rep)
return 0;
}
+static int hid_gos_gamepad_cfg_event(struct command_report *cmd_rep)
+{
+ int ret = 0;
+
+ switch (cmd_rep->sub_cmd) {
+ case FEATURE_GAMEPAD_MODE:
+ drvdata.gp_mode = cmd_rep->data[0];
+ break;
+ case FEATURE_AUTO_SLEEP_TIME:
+ drvdata.gp_auto_sleep_time = cmd_rep->data[0];
+ break;
+ case FEATURE_IMU_BYPASS:
+ drvdata.imu_bypass_en = cmd_rep->data[0];
+ break;
+ case FEATURE_RGB_ENABLE:
+ drvdata.rgb_en = cmd_rep->data[0];
+ break;
+ case FEATURE_IMU_ENABLE:
+ drvdata.imu_sensor_en = cmd_rep->data[0];
+ break;
+ case FEATURE_TOUCHPAD_ENABLE:
+ drvdata.tp_en = cmd_rep->data[0];
+ break;
+ case FEATURE_OS_MODE:
+ drvdata.os_mode = cmd_rep->data[0];
+ break;
+ case FEATURE_POLL_RATE:
+ drvdata.gp_poll_rate = cmd_rep->data[0];
+ break;
+ case FEATURE_DPAD_MODE:
+ drvdata.gp_dpad_mode = cmd_rep->data[0];
+ break;
+ case FEATURE_MOUSE_WHEEL_STEP:
+ drvdata.mouse_step = cmd_rep->data[0];
+ break;
+ default:
+ ret = -EINVAL;
+ break;
+ }
+
+ return ret;
+}
+
+static int hid_gos_set_event_return(struct command_report *cmd_rep)
+{
+ if (cmd_rep->data[0] != 0)
+ return -EIO;
+
+ return 0;
+}
+
static u8 get_endpoint_address(struct hid_device *hdev)
{
struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
@@ -120,6 +248,12 @@ static int hid_gos_raw_event(struct hid_device *hdev, struct hid_report *report,
case GET_MCU_ID:
ret = hid_gos_mcu_id_event(cmd_rep);
break;
+ case GET_GAMEPAD_CFG:
+ ret = hid_gos_gamepad_cfg_event(cmd_rep);
+ break;
+ case SET_GAMEPAD_CFG:
+ ret = hid_gos_set_event_return(cmd_rep);
+ break;
default:
ret = -EINVAL;
break;
@@ -177,17 +311,333 @@ static int mcu_property_out(struct hid_device *hdev, u8 command, u8 index,
return ret;
}
+static ssize_t gamepad_property_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count,
+ enum feature_status_index index)
+{
+ size_t size = 1;
+ u8 val = 0;
+ int ret;
+
+ switch (index) {
+ case FEATURE_GAMEPAD_MODE:
+ ret = sysfs_match_string(gamepad_mode_text, buf);
+ if (ret < 0)
+ return ret;
+ val = ret;
+ break;
+ case FEATURE_AUTO_SLEEP_TIME:
+ ret = kstrtou8(buf, 10, &val);
+ if (ret)
+ return ret;
+
+ if (val < 0 || val > 255)
+ return -EINVAL;
+ break;
+ case FEATURE_IMU_ENABLE:
+ ret = sysfs_match_string(feature_enabled_text, buf);
+ if (ret < 0)
+ return ret;
+ val = ret;
+ break;
+ case FEATURE_IMU_BYPASS:
+ ret = sysfs_match_string(feature_enabled_text, buf);
+ if (ret < 0)
+ return ret;
+ val = ret;
+ break;
+ case FEATURE_RGB_ENABLE:
+ ret = sysfs_match_string(feature_enabled_text, buf);
+ if (ret < 0)
+ return ret;
+ val = ret;
+ break;
+ case FEATURE_TOUCHPAD_ENABLE:
+ ret = sysfs_match_string(feature_enabled_text, buf);
+ if (ret < 0)
+ return ret;
+ val = ret;
+ break;
+ case FEATURE_OS_MODE:
+ ret = sysfs_match_string(os_type_text, buf);
+ if (ret < 0)
+ return ret;
+ val = ret;
+ drvdata.os_mode = val;
+ break;
+ case FEATURE_POLL_RATE:
+ ret = sysfs_match_string(poll_rate_text, buf);
+ if (ret < 0)
+ return ret;
+ val = ret;
+ break;
+ case FEATURE_DPAD_MODE:
+ ret = sysfs_match_string(dpad_mode_text, buf);
+ if (ret < 0)
+ return ret;
+ val = ret;
+ break;
+ case FEATURE_MOUSE_WHEEL_STEP:
+ ret = kstrtou8(buf, 10, &val);
+ if (ret)
+ return ret;
+ if (val < 1 || val > 127)
+ return -EINVAL;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ if (!val)
+ size = 0;
+
+ ret = mcu_property_out(drvdata.hdev, SET_GAMEPAD_CFG, index, &val,
+ size);
+ if (ret < 0)
+ return ret;
+
+ return count;
+}
+
+static ssize_t gamepad_property_show(struct device *dev,
+ struct device_attribute *attr, char *buf,
+ enum feature_status_index index)
+{
+ size_t count = 0;
+ u8 i;
+
+ count = mcu_property_out(drvdata.hdev, GET_GAMEPAD_CFG, index, 0, 0);
+ if (count < 0)
+ return count;
+
+ switch (index) {
+ case FEATURE_GAMEPAD_MODE:
+ i = drvdata.gp_mode;
+ if (i >= ARRAY_SIZE(gamepad_mode_text))
+ return -EINVAL;
+ count = sysfs_emit(buf, "%s\n", gamepad_mode_text[i]);
+ break;
+ case FEATURE_AUTO_SLEEP_TIME:
+ count = sysfs_emit(buf, "%u\n", drvdata.gp_auto_sleep_time);
+ break;
+ case FEATURE_IMU_ENABLE:
+ i = drvdata.imu_sensor_en;
+ if (i >= ARRAY_SIZE(feature_enabled_text))
+ return -EINVAL;
+ count = sysfs_emit(buf, "%s\n", feature_enabled_text[i]);
+ break;
+ case FEATURE_IMU_BYPASS:
+ i = drvdata.imu_bypass_en;
+ if (i >= ARRAY_SIZE(feature_enabled_text))
+ return -EINVAL;
+ count = sysfs_emit(buf, "%s\n", feature_enabled_text[i]);
+ break;
+ case FEATURE_RGB_ENABLE:
+ i = drvdata.rgb_en;
+ if (i >= ARRAY_SIZE(feature_enabled_text))
+ return -EINVAL;
+ count = sysfs_emit(buf, "%s\n", feature_enabled_text[i]);
+ break;
+ case FEATURE_TOUCHPAD_ENABLE:
+ i = drvdata.tp_en;
+ if (i >= ARRAY_SIZE(feature_enabled_text))
+ return -EINVAL;
+ count = sysfs_emit(buf, "%s\n", feature_enabled_text[i]);
+ break;
+ case FEATURE_OS_MODE:
+ i = drvdata.os_mode;
+ if (i >= ARRAY_SIZE(os_type_text))
+ return -EINVAL;
+ count = sysfs_emit(buf, "%s\n", os_type_text[i]);
+ break;
+ case FEATURE_POLL_RATE:
+ i = drvdata.gp_poll_rate;
+ if (i >= ARRAY_SIZE(poll_rate_text))
+ return -EINVAL;
+ count = sysfs_emit(buf, "%s\n", poll_rate_text[i]);
+ break;
+ case FEATURE_DPAD_MODE:
+ i = drvdata.gp_dpad_mode;
+ if (i >= ARRAY_SIZE(dpad_mode_text))
+ return -EINVAL;
+ count = sysfs_emit(buf, "%s\n", dpad_mode_text[i]);
+ break;
+ case FEATURE_MOUSE_WHEEL_STEP:
+ i = drvdata.mouse_step;
+ if (i < 1 || i > 127)
+ return -EINVAL;
+ count = sysfs_emit(buf, "%u\n", i);
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ return count;
+}
+
+static ssize_t gamepad_property_options(struct device *dev,
+ struct device_attribute *attr,
+ char *buf,
+ enum feature_status_index index)
+{
+ size_t count = 0;
+ unsigned int i;
+
+ switch (index) {
+ case FEATURE_GAMEPAD_MODE:
+ for (i = 0; i < ARRAY_SIZE(gamepad_mode_text); i++) {
+ count += sysfs_emit_at(buf, count, "%s ",
+ gamepad_mode_text[i]);
+ }
+ break;
+ case FEATURE_AUTO_SLEEP_TIME:
+ return sysfs_emit(buf, "0-255\n");
+ case FEATURE_IMU_ENABLE:
+ for (i = 0; i < ARRAY_SIZE(feature_enabled_text); i++) {
+ count += sysfs_emit_at(buf, count, "%s ",
+ feature_enabled_text[i]);
+ }
+ break;
+ case FEATURE_IMU_BYPASS:
+ case FEATURE_RGB_ENABLE:
+ case FEATURE_TOUCHPAD_ENABLE:
+ for (i = 0; i < ARRAY_SIZE(feature_enabled_text); i++) {
+ count += sysfs_emit_at(buf, count, "%s ",
+ feature_enabled_text[i]);
+ }
+ break;
+ case FEATURE_OS_MODE:
+ for (i = 0; i < ARRAY_SIZE(os_type_text); i++) {
+ count += sysfs_emit_at(buf, count, "%s ",
+ os_type_text[i]);
+ }
+ break;
+ case FEATURE_POLL_RATE:
+ for (i = 0; i < ARRAY_SIZE(poll_rate_text); i++) {
+ count += sysfs_emit_at(buf, count, "%s ",
+ poll_rate_text[i]);
+ }
+ break;
+ case FEATURE_DPAD_MODE:
+ for (i = 0; i < ARRAY_SIZE(dpad_mode_text); i++) {
+ count += sysfs_emit_at(buf, count, "%s ",
+ dpad_mode_text[i]);
+ }
+ break;
+ case FEATURE_MOUSE_WHEEL_STEP:
+ return sysfs_emit(buf, "1-127\n");
+ default:
+ return count;
+ }
+
+ if (count)
+ buf[count - 1] = '\n';
+
+ return count;
+}
+
static ssize_t mcu_id_show(struct device *dev, struct device_attribute *attr,
char *buf)
{
return sysfs_emit(buf, "%*phN\n", 12, &drvdata.mcu_id);
}
+#define LEGOS_DEVICE_ATTR_RW(_name, _attrname, _rtype, _group) \
+ static ssize_t _name##_store(struct device *dev, \
+ struct device_attribute *attr, \
+ const char *buf, size_t count) \
+ { \
+ return _group##_property_store(dev, attr, buf, count, \
+ _name.index); \
+ } \
+ static ssize_t _name##_show(struct device *dev, \
+ struct device_attribute *attr, char *buf) \
+ { \
+ return _group##_property_show(dev, attr, buf, _name.index); \
+ } \
+ static ssize_t _name##_##_rtype##_show( \
+ struct device *dev, struct device_attribute *attr, char *buf) \
+ { \
+ return _group##_property_options(dev, attr, buf, _name.index); \
+ } \
+ static DEVICE_ATTR_RW_NAMED(_name, _attrname)
+
+#define LEGOS_DEVICE_ATTR_RO(_name, _attrname, _group) \
+ static ssize_t _name##_show(struct device *dev, \
+ struct device_attribute *attr, char *buf) \
+ { \
+ return _group##_property_show(dev, attr, buf, _name.index); \
+ } \
+ static DEVICE_ATTR_RO_NAMED(_name, _attrname)
+
+/* Gamepad */
+struct gos_cfg_attr auto_sleep_time = { FEATURE_AUTO_SLEEP_TIME };
+LEGOS_DEVICE_ATTR_RW(auto_sleep_time, "auto_sleep_time", range, gamepad);
+static DEVICE_ATTR_RO(auto_sleep_time_range);
+
+struct gos_cfg_attr dpad_mode = { FEATURE_DPAD_MODE };
+LEGOS_DEVICE_ATTR_RW(dpad_mode, "dpad_mode", index, gamepad);
+static DEVICE_ATTR_RO(dpad_mode_index);
+
+struct gos_cfg_attr gamepad_mode = { FEATURE_GAMEPAD_MODE };
+LEGOS_DEVICE_ATTR_RW(gamepad_mode, "mode", index, gamepad);
+static DEVICE_ATTR_RO_NAMED(gamepad_mode_index, "mode_index");
+
+struct gos_cfg_attr gamepad_poll_rate = { FEATURE_POLL_RATE };
+LEGOS_DEVICE_ATTR_RW(gamepad_poll_rate, "poll_rate", index, gamepad);
+static DEVICE_ATTR_RO_NAMED(gamepad_poll_rate_index, "poll_rate_index");
+
+static struct attribute *legos_gamepad_attrs[] = {
+ &dev_attr_auto_sleep_time.attr,
+ &dev_attr_auto_sleep_time_range.attr,
+ &dev_attr_dpad_mode.attr,
+ &dev_attr_dpad_mode_index.attr,
+ &dev_attr_gamepad_mode.attr,
+ &dev_attr_gamepad_mode_index.attr,
+ &dev_attr_gamepad_poll_rate.attr,
+ &dev_attr_gamepad_poll_rate_index.attr,
+ NULL,
+};
+
+static const struct attribute_group gamepad_attr_group = {
+ .name = "gamepad",
+ .attrs = legos_gamepad_attrs,
+};
+
+/* IMU */
+struct gos_cfg_attr imu_bypass_enabled = { FEATURE_IMU_BYPASS };
+LEGOS_DEVICE_ATTR_RW(imu_bypass_enabled, "bypass_enabled", index, gamepad);
+static DEVICE_ATTR_RO_NAMED(imu_bypass_enabled_index, "bypass_enabled_index");
+
+struct gos_cfg_attr imu_sensor_enabled = { FEATURE_IMU_ENABLE };
+LEGOS_DEVICE_ATTR_RW(imu_sensor_enabled, "sensor_enabled", index, gamepad);
+static DEVICE_ATTR_RO_NAMED(imu_sensor_enabled_index, "sensor_enabled_index");
+
+static struct attribute *legos_imu_attrs[] = {
+ &dev_attr_imu_bypass_enabled.attr,
+ &dev_attr_imu_bypass_enabled_index.attr,
+ &dev_attr_imu_sensor_enabled.attr,
+ &dev_attr_imu_sensor_enabled_index.attr,
+ NULL,
+};
+
+static const struct attribute_group imu_attr_group = {
+ .name = "imu",
+ .attrs = legos_imu_attrs,
+};
+
/* MCU */
static DEVICE_ATTR_RO(mcu_id);
+struct gos_cfg_attr os_mode = { FEATURE_OS_MODE };
+LEGOS_DEVICE_ATTR_RW(os_mode, "os_mode", index, gamepad);
+static DEVICE_ATTR_RO(os_mode_index);
+
static struct attribute *legos_mcu_attrs[] = {
&dev_attr_mcu_id.attr,
+ &dev_attr_os_mode.attr,
+ &dev_attr_os_mode_index.attr,
NULL,
};
@@ -195,8 +645,44 @@ static const struct attribute_group mcu_attr_group = {
.attrs = legos_mcu_attrs,
};
+/* Mouse */
+struct gos_cfg_attr mouse_wheel_step = { FEATURE_MOUSE_WHEEL_STEP };
+LEGOS_DEVICE_ATTR_RW(mouse_wheel_step, "step", range, gamepad);
+static DEVICE_ATTR_RO_NAMED(mouse_wheel_step_range, "step_range");
+
+static struct attribute *legos_mouse_attrs[] = {
+ &dev_attr_mouse_wheel_step.attr,
+ &dev_attr_mouse_wheel_step_range.attr,
+ NULL,
+};
+
+static const struct attribute_group mouse_attr_group = {
+ .name = "mouse",
+ .attrs = legos_mouse_attrs,
+};
+
+/* Touchpad */
+struct gos_cfg_attr touchpad_enabled = { FEATURE_TOUCHPAD_ENABLE };
+LEGOS_DEVICE_ATTR_RW(touchpad_enabled, "enabled", index, gamepad);
+static DEVICE_ATTR_RO_NAMED(touchpad_enabled_index, "enabled_index");
+
+static struct attribute *legos_touchpad_attrs[] = {
+ &dev_attr_touchpad_enabled.attr,
+ &dev_attr_touchpad_enabled_index.attr,
+ NULL,
+};
+
+static const struct attribute_group touchpad_attr_group = {
+ .name = "touchpad",
+ .attrs = legos_touchpad_attrs,
+};
+
static const struct attribute_group *top_level_attr_groups[] = {
+ &gamepad_attr_group,
+ &imu_attr_group,
&mcu_attr_group,
+ &mouse_attr_group,
+ &touchpad_attr_group,
NULL,
};
@@ -268,6 +754,27 @@ static void hid_gos_cfg_remove(struct hid_device *hdev)
hid_set_drvdata(hdev, NULL);
}
+static int hid_gos_cfg_reset_resume(struct hid_device *hdev)
+{
+ u8 os_mode = drvdata.os_mode;
+ int ret;
+
+ ret = mcu_property_out(drvdata.hdev, SET_GAMEPAD_CFG, FEATURE_OS_MODE,
+ &os_mode, 1);
+ if (ret < 0)
+ return ret;
+
+ ret = mcu_property_out(drvdata.hdev, GET_GAMEPAD_CFG, FEATURE_OS_MODE, 0,
+ 0);
+ if (ret < 0)
+ return ret;
+
+ if (drvdata.os_mode != os_mode)
+ return -ENODEV;
+
+ return 0;
+}
+
static int hid_gos_probe(struct hid_device *hdev,
const struct hid_device_id *id)
{
@@ -326,6 +833,20 @@ static void hid_gos_remove(struct hid_device *hdev)
}
}
+static int hid_gos_reset_resume(struct hid_device *hdev)
+{
+ int ep = get_endpoint_address(hdev);
+
+ switch (ep) {
+ case GO_S_CFG_INTF_IN:
+ return hid_gos_cfg_reset_resume(hdev);
+ default:
+ break;
+ }
+
+ return 0;
+}
+
static const struct hid_device_id hid_gos_devices[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_QHE,
USB_DEVICE_ID_LENOVO_LEGION_GO_S_XINPUT) },
@@ -340,6 +861,7 @@ static struct hid_driver hid_lenovo_go_s = {
.id_table = hid_gos_devices,
.probe = hid_gos_probe,
.remove = hid_gos_remove,
+ .reset_resume = hid_gos_reset_resume,
.raw_event = hid_gos_raw_event,
};
module_hid_driver(hid_lenovo_go_s);
--
2.51.2
^ permalink raw reply related
* [PATCH v2 13/16] HID: hid-lenovo-go-s: Add Touchpad Mode Attributes
From: Derek J. Clark @ 2025-12-29 3:17 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Mario Limonciello, Zhixin Zhang, Mia Shao, Mark Pearson,
Pierre-Loup A . Griffais, Derek J . Clark, linux-input, linux-doc,
linux-kernel
In-Reply-To: <20251229031753.581664-1-derekjohn.clark@gmail.com>
Adds attributes for managing the touchpad operating modes.
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
drivers/hid/hid-lenovo-go-s.c | 142 ++++++++++++++++++++++++++++++++++
1 file changed, 142 insertions(+)
diff --git a/drivers/hid/hid-lenovo-go-s.c b/drivers/hid/hid-lenovo-go-s.c
index 076afdaa9628..38425e3d6cb2 100644
--- a/drivers/hid/hid-lenovo-go-s.c
+++ b/drivers/hid/hid-lenovo-go-s.c
@@ -47,6 +47,8 @@ struct hid_gos_cfg {
u8 os_mode;
u8 rgb_en;
u8 tp_en;
+ u8 tp_linux_mode;
+ u8 tp_windows_mode;
} drvdata;
struct gos_cfg_attr {
@@ -145,6 +147,22 @@ static const char *const dpad_mode_text[] = {
[DIR4] = "4-way",
};
+enum touchpad_mode_index {
+ TP_REL,
+ TP_ABS,
+};
+
+static const char *const touchpad_mode_text[] = {
+ [TP_REL] = "relative",
+ [TP_ABS] = "absolute",
+};
+
+enum touchpad_config_index {
+ CFG_WINDOWS_MODE = 0x03,
+ CFG_LINUX_MODE,
+
+};
+
static int hid_gos_version_event(u8 *data)
{
struct version_report *ver_rep = (struct version_report *)data;
@@ -204,6 +222,25 @@ static int hid_gos_gamepad_cfg_event(struct command_report *cmd_rep)
return ret;
}
+static int hid_gos_touchpad_event(struct command_report *cmd_rep)
+{
+ int ret = 0;
+
+ switch (cmd_rep->sub_cmd) {
+ case CFG_LINUX_MODE:
+ drvdata.tp_linux_mode = cmd_rep->data[0];
+ break;
+ case CFG_WINDOWS_MODE:
+ drvdata.tp_windows_mode = cmd_rep->data[0];
+ break;
+ default:
+ ret = -EINVAL;
+ break;
+ }
+
+ return ret;
+}
+
static int hid_gos_set_event_return(struct command_report *cmd_rep)
{
if (cmd_rep->data[0] != 0)
@@ -251,7 +288,11 @@ static int hid_gos_raw_event(struct hid_device *hdev, struct hid_report *report,
case GET_GAMEPAD_CFG:
ret = hid_gos_gamepad_cfg_event(cmd_rep);
break;
+ case GET_TP_PARAM:
+ ret = hid_gos_touchpad_event(cmd_rep);
+ break;
case SET_GAMEPAD_CFG:
+ case SET_TP_PARAM:
ret = hid_gos_set_event_return(cmd_rep);
break;
default:
@@ -537,6 +578,95 @@ static ssize_t gamepad_property_options(struct device *dev,
return count;
}
+static ssize_t touchpad_property_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count,
+ enum touchpad_config_index index)
+{
+ size_t size = 1;
+ u8 val = 0;
+ int ret;
+
+ switch (index) {
+ case CFG_WINDOWS_MODE:
+ ret = sysfs_match_string(touchpad_mode_text, buf);
+ if (ret < 0)
+ return ret;
+ val = ret;
+ break;
+ case CFG_LINUX_MODE:
+ ret = sysfs_match_string(touchpad_mode_text, buf);
+ if (ret < 0)
+ return ret;
+ val = ret;
+ break;
+ default:
+ return -EINVAL;
+ }
+ if (!val)
+ size = 0;
+
+ ret = mcu_property_out(drvdata.hdev, SET_TP_PARAM, index, &val, size);
+ if (ret < 0)
+ return ret;
+
+ return count;
+}
+
+static ssize_t touchpad_property_show(struct device *dev,
+ struct device_attribute *attr, char *buf,
+ enum touchpad_config_index index)
+{
+ int ret = 0;
+ u8 i;
+
+ ret = mcu_property_out(drvdata.hdev, GET_TP_PARAM, index, 0, 0);
+ if (ret < 0)
+ return ret;
+
+ switch (index) {
+ case CFG_WINDOWS_MODE:
+ i = drvdata.tp_windows_mode;
+ break;
+ case CFG_LINUX_MODE:
+ i = drvdata.tp_linux_mode;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ if (i >= ARRAY_SIZE(touchpad_mode_text))
+ return -EINVAL;
+
+ return sysfs_emit(buf, "%s\n", touchpad_mode_text[i]);
+}
+
+static ssize_t touchpad_property_options(struct device *dev,
+ struct device_attribute *attr,
+ char *buf,
+ enum touchpad_config_index index)
+{
+ size_t count = 0;
+ unsigned int i;
+
+ switch (index) {
+ case CFG_WINDOWS_MODE:
+ case CFG_LINUX_MODE:
+ for (i = 0; i < ARRAY_SIZE(touchpad_mode_text); i++) {
+ count += sysfs_emit_at(buf, count, "%s ",
+ touchpad_mode_text[i]);
+ }
+ break;
+ default:
+ return count;
+ }
+
+ if (count)
+ buf[count - 1] = '\n';
+
+ return count;
+}
+
static ssize_t mcu_id_show(struct device *dev, struct device_attribute *attr,
char *buf)
{
@@ -666,9 +796,21 @@ struct gos_cfg_attr touchpad_enabled = { FEATURE_TOUCHPAD_ENABLE };
LEGOS_DEVICE_ATTR_RW(touchpad_enabled, "enabled", index, gamepad);
static DEVICE_ATTR_RO_NAMED(touchpad_enabled_index, "enabled_index");
+struct gos_cfg_attr touchpad_linux_mode = { CFG_LINUX_MODE };
+LEGOS_DEVICE_ATTR_RW(touchpad_linux_mode, "linux_mode", index, touchpad);
+static DEVICE_ATTR_RO_NAMED(touchpad_linux_mode_index, "linux_mode_index");
+
+struct gos_cfg_attr touchpad_windows_mode = { CFG_WINDOWS_MODE };
+LEGOS_DEVICE_ATTR_RW(touchpad_windows_mode, "windows_mode", index, touchpad);
+static DEVICE_ATTR_RO_NAMED(touchpad_windows_mode_index, "windows_mode_index");
+
static struct attribute *legos_touchpad_attrs[] = {
&dev_attr_touchpad_enabled.attr,
&dev_attr_touchpad_enabled_index.attr,
+ &dev_attr_touchpad_linux_mode.attr,
+ &dev_attr_touchpad_linux_mode_index.attr,
+ &dev_attr_touchpad_windows_mode.attr,
+ &dev_attr_touchpad_windows_mode_index.attr,
NULL,
};
--
2.51.2
^ permalink raw reply related
* [PATCH v2 15/16] HID: hid-lenovo-go-s: Add IMU and Touchpad RO Attributes
From: Derek J. Clark @ 2025-12-29 3:17 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Mario Limonciello, Zhixin Zhang, Mia Shao, Mark Pearson,
Pierre-Loup A . Griffais, Derek J . Clark, linux-input, linux-doc,
linux-kernel
In-Reply-To: <20251229031753.581664-1-derekjohn.clark@gmail.com>
Adds attributes for reporting the touchpad manufacturer, version, and
IMU manufacturer.
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
drivers/hid/hid-lenovo-go-s.c | 108 ++++++++++++++++++++++++++++++++++
1 file changed, 108 insertions(+)
diff --git a/drivers/hid/hid-lenovo-go-s.c b/drivers/hid/hid-lenovo-go-s.c
index e422f3977517..11000cf54e56 100644
--- a/drivers/hid/hid-lenovo-go-s.c
+++ b/drivers/hid/hid-lenovo-go-s.c
@@ -43,6 +43,7 @@ struct hid_gos_cfg {
u8 gp_mode;
u8 gp_poll_rate;
u8 imu_bypass_en;
+ u8 imu_manufacturer;
u8 imu_sensor_en;
u8 mcu_id[12];
u8 mouse_step;
@@ -54,6 +55,8 @@ struct hid_gos_cfg {
u8 rgb_speed;
u8 tp_en;
u8 tp_linux_mode;
+ u8 tp_manufacturer;
+ u8 tp_version;
u8 tp_windows_mode;
} drvdata;
@@ -201,6 +204,36 @@ enum rgb_config_index {
USR_LIGHT_PROFILE_3,
};
+enum test_command_index {
+ TEST_TP_MFR = 0x02,
+ TEST_IMU_MFR,
+ TEST_TP_VER,
+};
+
+enum tp_mfr_index {
+ TP_NONE,
+ TP_BETTERLIFE,
+ TP_SIPO,
+};
+
+static const char *const touchpad_manufacturer_text[] = {
+ [TP_NONE] = "none",
+ [TP_BETTERLIFE] = "BetterLife",
+ [TP_SIPO] = "SIPO",
+};
+
+enum imu_mfr_index {
+ IMU_NONE,
+ IMU_BOSCH,
+ IMU_ST,
+};
+
+static const char *const imu_manufacturer_text[] = {
+ [IMU_NONE] = "none",
+ [IMU_BOSCH] = "Bosch",
+ [IMU_ST] = "ST",
+};
+
static int hid_gos_version_event(u8 *data)
{
struct version_report *ver_rep = (struct version_report *)data;
@@ -279,6 +312,30 @@ static int hid_gos_touchpad_event(struct command_report *cmd_rep)
return ret;
}
+static int hid_gos_pl_test_event(struct command_report *cmd_rep)
+{
+ int ret = 0;
+
+ switch (cmd_rep->sub_cmd) {
+ case TEST_TP_MFR:
+ drvdata.tp_manufacturer = cmd_rep->data[0];
+ ret = 0;
+ break;
+ case TEST_IMU_MFR:
+ drvdata.imu_manufacturer = cmd_rep->data[0];
+ ret = 0;
+ break;
+ case TEST_TP_VER:
+ drvdata.tp_version = cmd_rep->data[0];
+ ret = 0;
+ break;
+ default:
+ ret = -EINVAL;
+ break;
+ }
+ return ret;
+}
+
static int hid_gos_light_event(struct command_report *cmd_rep)
{
struct led_classdev_mc *mc_cdev;
@@ -362,6 +419,9 @@ static int hid_gos_raw_event(struct hid_device *hdev, struct hid_report *report,
case GET_TP_PARAM:
ret = hid_gos_touchpad_event(cmd_rep);
break;
+ case GET_PL_TEST:
+ ret = hid_gos_pl_test_event(cmd_rep);
+ break;
case GET_RGB_CFG:
ret = hid_gos_light_event(cmd_rep);
break;
@@ -742,6 +802,42 @@ static ssize_t touchpad_property_options(struct device *dev,
return count;
}
+static ssize_t test_property_show(struct device *dev,
+ struct device_attribute *attr, char *buf,
+ enum test_command_index index)
+{
+ size_t count = 0;
+ int ret;
+ u8 i;
+
+ ret = mcu_property_out(drvdata.hdev, GET_PL_TEST, index, 0, 0);
+ if (ret)
+ return ret;
+
+ switch (index) {
+ case TEST_TP_MFR:
+ i = drvdata.tp_manufacturer;
+ if (i >= ARRAY_SIZE(touchpad_manufacturer_text))
+ return -EINVAL;
+ count = sysfs_emit(buf, "%s\n", touchpad_manufacturer_text[i]);
+ break;
+ case TEST_IMU_MFR:
+ i = drvdata.imu_manufacturer;
+ if (i >= ARRAY_SIZE(imu_manufacturer_text))
+ return -EINVAL;
+ count = sysfs_emit(buf, "%s\n", imu_manufacturer_text[i]);
+ break;
+ case TEST_TP_VER:
+ count = sysfs_emit(buf, "%u\n", drvdata.tp_version);
+ break;
+ default:
+ count = -EINVAL;
+ break;
+ }
+
+ return count;
+}
+
static ssize_t mcu_id_show(struct device *dev, struct device_attribute *attr,
char *buf)
{
@@ -1085,6 +1181,9 @@ struct gos_cfg_attr imu_bypass_enabled = { FEATURE_IMU_BYPASS };
LEGOS_DEVICE_ATTR_RW(imu_bypass_enabled, "bypass_enabled", index, gamepad);
static DEVICE_ATTR_RO_NAMED(imu_bypass_enabled_index, "bypass_enabled_index");
+struct gos_cfg_attr imu_manufacturer = { TEST_IMU_MFR };
+LEGOS_DEVICE_ATTR_RO(imu_manufacturer, "manufacturer", test);
+
struct gos_cfg_attr imu_sensor_enabled = { FEATURE_IMU_ENABLE };
LEGOS_DEVICE_ATTR_RW(imu_sensor_enabled, "sensor_enabled", index, gamepad);
static DEVICE_ATTR_RO_NAMED(imu_sensor_enabled_index, "sensor_enabled_index");
@@ -1092,6 +1191,7 @@ static DEVICE_ATTR_RO_NAMED(imu_sensor_enabled_index, "sensor_enabled_index");
static struct attribute *legos_imu_attrs[] = {
&dev_attr_imu_bypass_enabled.attr,
&dev_attr_imu_bypass_enabled_index.attr,
+ &dev_attr_imu_manufacturer.attr,
&dev_attr_imu_sensor_enabled.attr,
&dev_attr_imu_sensor_enabled_index.attr,
NULL,
@@ -1145,6 +1245,12 @@ struct gos_cfg_attr touchpad_linux_mode = { CFG_LINUX_MODE };
LEGOS_DEVICE_ATTR_RW(touchpad_linux_mode, "linux_mode", index, touchpad);
static DEVICE_ATTR_RO_NAMED(touchpad_linux_mode_index, "linux_mode_index");
+struct gos_cfg_attr touchpad_manufacturer = { TEST_TP_MFR };
+LEGOS_DEVICE_ATTR_RO(touchpad_manufacturer, "manufacturer", touchpad);
+
+struct gos_cfg_attr touchpad_version = { TEST_TP_VER };
+LEGOS_DEVICE_ATTR_RO(touchpad_version, "version", touchpad);
+
struct gos_cfg_attr touchpad_windows_mode = { CFG_WINDOWS_MODE };
LEGOS_DEVICE_ATTR_RW(touchpad_windows_mode, "windows_mode", index, touchpad);
static DEVICE_ATTR_RO_NAMED(touchpad_windows_mode_index, "windows_mode_index");
@@ -1154,6 +1260,8 @@ static struct attribute *legos_touchpad_attrs[] = {
&dev_attr_touchpad_enabled_index.attr,
&dev_attr_touchpad_linux_mode.attr,
&dev_attr_touchpad_linux_mode_index.attr,
+ &dev_attr_touchpad_manufacturer.attr,
+ &dev_attr_touchpad_version.attr,
&dev_attr_touchpad_windows_mode.attr,
&dev_attr_touchpad_windows_mode_index.attr,
NULL,
--
2.51.2
^ permalink raw reply related
* [PATCH v2 14/16] HID: hid-lenovo-go-s: Add RGB LED control interface
From: Derek J. Clark @ 2025-12-29 3:17 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Mario Limonciello, Zhixin Zhang, Mia Shao, Mark Pearson,
Pierre-Loup A . Griffais, Derek J . Clark, linux-input, linux-doc,
linux-kernel
In-Reply-To: <20251229031753.581664-1-derekjohn.clark@gmail.com>
Adds an LED multicolor class device and attribute group for controlling
the RGB of the Left and right joystick rings. In addition to the standard
led_cdev attributes, additional attributes that allow for the control of
the effect (monocolor, breathe, rainbow, and chroma), speed of the
effect change, an enable toggle, and profile.
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
drivers/hid/hid-lenovo-go-s.c | 456 ++++++++++++++++++++++++++++++++++
1 file changed, 456 insertions(+)
diff --git a/drivers/hid/hid-lenovo-go-s.c b/drivers/hid/hid-lenovo-go-s.c
index 38425e3d6cb2..e422f3977517 100644
--- a/drivers/hid/hid-lenovo-go-s.c
+++ b/drivers/hid/hid-lenovo-go-s.c
@@ -15,6 +15,7 @@
#include <linux/hid.h>
#include <linux/jiffies.h>
#include <linux/kstrtox.h>
+#include <linux/led-class-multicolor.h>
#include <linux/mutex.h>
#include <linux/printk.h>
#include <linux/string.h>
@@ -34,6 +35,7 @@ struct hid_gos_cfg {
unsigned char *buf;
struct delayed_work gos_cfg_setup;
struct completion send_cmd_complete;
+ struct led_classdev *led_cdev;
struct hid_device *hdev;
struct mutex cfg_mutex; /*ensure single synchronous output report*/
u8 gp_auto_sleep_time;
@@ -45,7 +47,11 @@ struct hid_gos_cfg {
u8 mcu_id[12];
u8 mouse_step;
u8 os_mode;
+ u8 rgb_effect;
u8 rgb_en;
+ u8 rgb_mode;
+ u8 rgb_profile;
+ u8 rgb_speed;
u8 tp_en;
u8 tp_linux_mode;
u8 tp_windows_mode;
@@ -163,6 +169,38 @@ enum touchpad_config_index {
};
+enum rgb_mode_index {
+ RGB_MODE_DYNAMIC,
+ RGB_MODE_CUSTOM,
+};
+
+static const char *const rgb_mode_text[] = {
+ [RGB_MODE_DYNAMIC] = "dynamic",
+ [RGB_MODE_CUSTOM] = "custom",
+};
+
+enum rgb_effect_index {
+ RGB_EFFECT_MONO,
+ RGB_EFFECT_BREATHE,
+ RGB_EFFECT_CHROMA,
+ RGB_EFFECT_RAINBOW,
+};
+
+static const char *const rgb_effect_text[] = {
+ [RGB_EFFECT_MONO] = "monocolor",
+ [RGB_EFFECT_BREATHE] = "breathe",
+ [RGB_EFFECT_CHROMA] = "chroma",
+ [RGB_EFFECT_RAINBOW] = "rainbow",
+};
+
+enum rgb_config_index {
+ LIGHT_MODE_SEL = 0x01,
+ LIGHT_PROFILE_SEL,
+ USR_LIGHT_PROFILE_1,
+ USR_LIGHT_PROFILE_2,
+ USR_LIGHT_PROFILE_3,
+};
+
static int hid_gos_version_event(u8 *data)
{
struct version_report *ver_rep = (struct version_report *)data;
@@ -241,6 +279,39 @@ static int hid_gos_touchpad_event(struct command_report *cmd_rep)
return ret;
}
+static int hid_gos_light_event(struct command_report *cmd_rep)
+{
+ struct led_classdev_mc *mc_cdev;
+ int ret = 0;
+
+ switch (cmd_rep->sub_cmd) {
+ case LIGHT_MODE_SEL:
+ drvdata.rgb_mode = cmd_rep->data[0];
+ ret = 0;
+ break;
+ case LIGHT_PROFILE_SEL:
+ drvdata.rgb_profile = cmd_rep->data[0];
+ ret = 0;
+ break;
+ case USR_LIGHT_PROFILE_1:
+ case USR_LIGHT_PROFILE_2:
+ case USR_LIGHT_PROFILE_3:
+ mc_cdev = lcdev_to_mccdev(drvdata.led_cdev);
+ drvdata.rgb_effect = cmd_rep->data[0];
+ mc_cdev->subled_info[0].intensity = cmd_rep->data[1];
+ mc_cdev->subled_info[1].intensity = cmd_rep->data[2];
+ mc_cdev->subled_info[2].intensity = cmd_rep->data[3];
+ drvdata.led_cdev->brightness = cmd_rep->data[4];
+ drvdata.rgb_speed = cmd_rep->data[5];
+ ret = 0;
+ break;
+ default:
+ ret = -EINVAL;
+ break;
+ }
+ return ret;
+}
+
static int hid_gos_set_event_return(struct command_report *cmd_rep)
{
if (cmd_rep->data[0] != 0)
@@ -291,7 +362,11 @@ static int hid_gos_raw_event(struct hid_device *hdev, struct hid_report *report,
case GET_TP_PARAM:
ret = hid_gos_touchpad_event(cmd_rep);
break;
+ case GET_RGB_CFG:
+ ret = hid_gos_light_event(cmd_rep);
+ break;
case SET_GAMEPAD_CFG:
+ case SET_RGB_CFG:
case SET_TP_PARAM:
ret = hid_gos_set_event_return(cmd_rep);
break;
@@ -673,6 +748,276 @@ static ssize_t mcu_id_show(struct device *dev, struct device_attribute *attr,
return sysfs_emit(buf, "%*phN\n", 12, &drvdata.mcu_id);
}
+static int rgb_cfg_call(struct hid_device *hdev, enum mcu_command_index cmd,
+ enum rgb_config_index index, u8 *val, size_t size)
+{
+ if (cmd != SET_RGB_CFG && cmd != GET_RGB_CFG)
+ return -EINVAL;
+
+ if (index < LIGHT_MODE_SEL || index > USR_LIGHT_PROFILE_3)
+ return -EINVAL;
+
+ return mcu_property_out(hdev, cmd, index, val, size);
+}
+
+static int rgb_attr_show(void)
+{
+ enum rgb_config_index index;
+
+ index = drvdata.rgb_profile + 2;
+
+ return rgb_cfg_call(drvdata.hdev, GET_RGB_CFG, index, 0, 0);
+};
+
+static ssize_t rgb_effect_store(struct device *dev,
+ struct device_attribute *attr, const char *buf,
+ size_t count)
+{
+ struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(drvdata.led_cdev);
+ enum rgb_config_index index;
+ u8 effect;
+ int ret;
+
+ ret = sysfs_match_string(rgb_effect_text, buf);
+ if (ret < 0)
+ return ret;
+
+ effect = ret;
+ index = drvdata.rgb_profile + 2;
+ u8 rgb_profile[6] = { effect,
+ mc_cdev->subled_info[0].intensity,
+ mc_cdev->subled_info[1].intensity,
+ mc_cdev->subled_info[2].intensity,
+ drvdata.led_cdev->brightness,
+ drvdata.rgb_speed };
+
+ ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, index, rgb_profile, 6);
+ if (ret)
+ return ret;
+
+ drvdata.rgb_effect = effect;
+ return count;
+};
+
+static ssize_t rgb_effect_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ int ret;
+
+ ret = rgb_attr_show();
+ if (ret)
+ return ret;
+
+ if (drvdata.rgb_effect >= ARRAY_SIZE(rgb_effect_text))
+ return -EINVAL;
+
+ return sysfs_emit(buf, "%s\n", rgb_effect_text[drvdata.rgb_effect]);
+}
+
+static ssize_t rgb_effect_index_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ ssize_t count = 0;
+ unsigned int i;
+
+ for (i = 0; i < ARRAY_SIZE(rgb_effect_text); i++)
+ count += sysfs_emit_at(buf, count, "%s ", rgb_effect_text[i]);
+
+ if (count)
+ buf[count - 1] = '\n';
+
+ return count;
+}
+
+static ssize_t rgb_speed_store(struct device *dev,
+ struct device_attribute *attr, const char *buf,
+ size_t count)
+{
+ struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(drvdata.led_cdev);
+ enum rgb_config_index index;
+ int val = 0;
+ int ret;
+
+ ret = kstrtoint(buf, 10, &val);
+ if (ret)
+ return ret;
+
+ if (val < 0 || val > 100)
+ return -EINVAL;
+
+ index = drvdata.rgb_profile + 2;
+ u8 rgb_profile[6] = { drvdata.rgb_effect,
+ mc_cdev->subled_info[0].intensity,
+ mc_cdev->subled_info[1].intensity,
+ mc_cdev->subled_info[2].intensity,
+ drvdata.led_cdev->brightness,
+ val };
+
+ ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, index, rgb_profile, 6);
+ if (ret)
+ return ret;
+
+ drvdata.rgb_speed = val;
+
+ return count;
+};
+
+static ssize_t rgb_speed_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ int ret;
+
+ ret = rgb_attr_show();
+ if (ret)
+ return ret;
+
+ if (drvdata.rgb_speed > 100)
+ return -EINVAL;
+
+ return sysfs_emit(buf, "%hhu\n", drvdata.rgb_speed);
+}
+
+static ssize_t rgb_speed_range_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "0-100\n");
+}
+
+static ssize_t rgb_mode_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ int ret;
+ u8 val;
+
+ ret = sysfs_match_string(rgb_mode_text, buf);
+ if (ret <= 0)
+ return ret;
+
+ val = ret;
+
+ ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, LIGHT_MODE_SEL, &val,
+ 1);
+ if (ret)
+ return ret;
+
+ drvdata.rgb_mode = val;
+
+ return count;
+};
+
+static ssize_t rgb_mode_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ int ret;
+
+ ret = rgb_cfg_call(drvdata.hdev, GET_RGB_CFG, LIGHT_MODE_SEL, 0, 0);
+ if (ret)
+ return ret;
+
+ if (drvdata.rgb_mode >= ARRAY_SIZE(rgb_mode_text))
+ return -EINVAL;
+
+ return sysfs_emit(buf, "%s\n", rgb_mode_text[drvdata.rgb_mode]);
+};
+
+static ssize_t rgb_mode_index_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ ssize_t count = 0;
+ unsigned int i;
+
+ for (i = 1; i < ARRAY_SIZE(rgb_mode_text); i++)
+ count += sysfs_emit_at(buf, count, "%s ", rgb_mode_text[i]);
+
+ if (count)
+ buf[count - 1] = '\n';
+
+ return count;
+}
+
+static ssize_t rgb_profile_store(struct device *dev,
+ struct device_attribute *attr, const char *buf,
+ size_t count)
+{
+ size_t size = 1;
+ int ret;
+ u8 val;
+
+ ret = kstrtou8(buf, 10, &val);
+ if (ret < 0)
+ return ret;
+
+ if (val < 1 || val > 3)
+ return -EINVAL;
+
+ ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, LIGHT_PROFILE_SEL, &val,
+ size);
+ if (ret)
+ return ret;
+
+ drvdata.rgb_profile = val;
+
+ return count;
+};
+
+static ssize_t rgb_profile_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ int ret;
+
+ ret = rgb_cfg_call(drvdata.hdev, GET_RGB_CFG, LIGHT_PROFILE_SEL, 0,
+ 0);
+ if (ret)
+ return ret;
+
+ if (drvdata.rgb_profile < 1 || drvdata.rgb_profile > 3)
+ return -EINVAL;
+
+ return sysfs_emit(buf, "%hhu\n", drvdata.rgb_profile);
+};
+
+static ssize_t rgb_profile_range_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "1-3\n");
+}
+
+static void hid_gos_brightness_set(struct led_classdev *led_cdev,
+ enum led_brightness brightness)
+{
+ struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(drvdata.led_cdev);
+ enum rgb_config_index index;
+ int ret;
+
+ if (brightness > led_cdev->max_brightness) {
+ dev_err(led_cdev->dev, "Invalid argument\n");
+ return;
+ }
+
+ index = drvdata.rgb_profile + 2;
+ u8 rgb_profile[6] = { drvdata.rgb_effect,
+ mc_cdev->subled_info[0].intensity,
+ mc_cdev->subled_info[1].intensity,
+ mc_cdev->subled_info[2].intensity,
+ brightness,
+ drvdata.rgb_speed };
+
+ ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, index, rgb_profile, 6);
+ switch (ret) {
+ case 0:
+ led_cdev->brightness = brightness;
+ break;
+ case -ENODEV: /* during switch to IAP -ENODEV is expected */
+ case -ENOSYS: /* during rmmod -ENOSYS is expected */
+ dev_dbg(led_cdev->dev, "Failed to write RGB profile: %i\n",
+ ret);
+ break;
+ default:
+ dev_err(led_cdev->dev, "Failed to write RGB profile: %i\n",
+ ret);
+ };
+}
+
#define LEGOS_DEVICE_ATTR_RW(_name, _attrname, _rtype, _group) \
static ssize_t _name##_store(struct device *dev, \
struct device_attribute *attr, \
@@ -828,6 +1173,70 @@ static const struct attribute_group *top_level_attr_groups[] = {
NULL,
};
+/* RGB */
+struct gos_cfg_attr rgb_enabled = { FEATURE_RGB_ENABLE };
+LEGOS_DEVICE_ATTR_RW(rgb_enabled, "enabled", index, gamepad);
+static DEVICE_ATTR_RO_NAMED(rgb_enabled_index, "enabled_index");
+
+static DEVICE_ATTR_RW_NAMED(rgb_effect, "effect");
+static DEVICE_ATTR_RO_NAMED(rgb_effect_index, "effect_index");
+static DEVICE_ATTR_RW_NAMED(rgb_mode, "mode");
+static DEVICE_ATTR_RO_NAMED(rgb_mode_index, "mode_index");
+static DEVICE_ATTR_RW_NAMED(rgb_profile, "profile");
+static DEVICE_ATTR_RO_NAMED(rgb_profile_range, "profile_range");
+static DEVICE_ATTR_RW_NAMED(rgb_speed, "speed");
+static DEVICE_ATTR_RO_NAMED(rgb_speed_range, "speed_range");
+
+static struct attribute *gos_rgb_attrs[] = {
+ &dev_attr_rgb_enabled.attr,
+ &dev_attr_rgb_enabled_index.attr,
+ &dev_attr_rgb_effect.attr,
+ &dev_attr_rgb_effect_index.attr,
+ &dev_attr_rgb_mode.attr,
+ &dev_attr_rgb_mode_index.attr,
+ &dev_attr_rgb_profile.attr,
+ &dev_attr_rgb_profile_range.attr,
+ &dev_attr_rgb_speed.attr,
+ &dev_attr_rgb_speed_range.attr,
+ NULL,
+};
+
+static struct attribute_group rgb_attr_group = {
+ .attrs = gos_rgb_attrs,
+};
+
+struct mc_subled gos_rgb_subled_info[] = {
+ {
+ .color_index = LED_COLOR_ID_RED,
+ .brightness = 0x50,
+ .intensity = 0x24,
+ .channel = 0x1,
+ },
+ {
+ .color_index = LED_COLOR_ID_GREEN,
+ .brightness = 0x50,
+ .intensity = 0x22,
+ .channel = 0x2,
+ },
+ {
+ .color_index = LED_COLOR_ID_BLUE,
+ .brightness = 0x50,
+ .intensity = 0x99,
+ .channel = 0x3,
+ },
+};
+
+struct led_classdev_mc gos_cdev_rgb = {
+ .led_cdev = {
+ .name = "go_s:rgb:joystick_rings",
+ .brightness = 0x50,
+ .max_brightness = 0x64,
+ .brightness_set = hid_gos_brightness_set,
+ },
+ .num_colors = ARRAY_SIZE(gos_rgb_subled_info),
+ .subled_info = gos_rgb_subled_info,
+};
+
static void cfg_setup(struct work_struct *work)
{
int ret;
@@ -846,6 +1255,38 @@ static void cfg_setup(struct work_struct *work)
"Failed to retrieve MCU Version: %i\n", ret);
return;
}
+
+ /* RGB */
+ ret = mcu_property_out(drvdata.hdev, GET_GAMEPAD_CFG, FEATURE_RGB_ENABLE,
+ 0, 0);
+ if (ret < 0) {
+ dev_err(drvdata.led_cdev->dev,
+ "Failed to retrieve RGB enabled: %i\n", ret);
+ return;
+ }
+
+ ret = mcu_property_out(drvdata.hdev, GET_RGB_CFG, LIGHT_MODE_SEL, 0,
+ 0);
+ if (ret < 0) {
+ dev_err(drvdata.led_cdev->dev,
+ "Failed to retrieve RGB Mode: %i\n", ret);
+ return;
+ }
+
+ ret = mcu_property_out(drvdata.hdev, GET_RGB_CFG, LIGHT_PROFILE_SEL,
+ 0, 0);
+ if (ret < 0) {
+ dev_err(drvdata.led_cdev->dev,
+ "Failed to retrieve RGB Profile: %i\n", ret);
+ return;
+ }
+
+ ret = rgb_attr_show();
+ if (ret < 0) {
+ dev_err(drvdata.led_cdev->dev,
+ "Failed to retrieve RGB Profile Data: %i\n", ret);
+ return;
+ }
}
static int hid_gos_cfg_probe(struct hid_device *hdev,
@@ -870,6 +1311,21 @@ static int hid_gos_cfg_probe(struct hid_device *hdev,
return ret;
}
+ ret = devm_led_classdev_multicolor_register(&hdev->dev, &gos_cdev_rgb);
+ if (ret) {
+ dev_err_probe(&hdev->dev, ret, "Failed to create RGB device\n");
+ return ret;
+ }
+
+ ret = devm_device_add_group(gos_cdev_rgb.led_cdev.dev, &rgb_attr_group);
+ if (ret) {
+ dev_err_probe(&hdev->dev, ret,
+ "Failed to create RGB configuratiion attributes\n");
+ return ret;
+ }
+
+ drvdata.led_cdev = &gos_cdev_rgb.led_cdev;
+
init_completion(&drvdata.send_cmd_complete);
/* Executing calls prior to returning from probe will lock the MCU. Schedule
--
2.51.2
^ permalink raw reply related
* [PATCH v2 16/16] HID: Add documentation for Lenovo Legion Go drivers
From: Derek J. Clark @ 2025-12-29 3:17 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Mario Limonciello, Zhixin Zhang, Mia Shao, Mark Pearson,
Pierre-Loup A . Griffais, Derek J . Clark, linux-input, linux-doc,
linux-kernel
In-Reply-To: <20251229031753.581664-1-derekjohn.clark@gmail.com>
Adds ABI documentation for the hid-lenovo-go-s and hid-lenovo-go
drivers.
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
.../ABI/testing/sysfs-driver-hid-lenovo-go | 724 ++++++++++++++++++
.../ABI/testing/sysfs-driver-hid-lenovo-go-s | 304 ++++++++
MAINTAINERS | 2 +
3 files changed, 1030 insertions(+)
create mode 100644 Documentation/ABI/testing/sysfs-driver-hid-lenovo-go
create mode 100644 Documentation/ABI/testing/sysfs-driver-hid-lenovo-go-s
diff --git a/Documentation/ABI/testing/sysfs-driver-hid-lenovo-go b/Documentation/ABI/testing/sysfs-driver-hid-lenovo-go
new file mode 100644
index 000000000000..4e298567ac40
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-driver-hid-lenovo-go
@@ -0,0 +1,724 @@
++What: /sys/class/leds/go:rgb:joystick_rings/effect
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls the display effect of the RGB interface.
++
++ Values are monocolor, breathe, chroma, or rainbow.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/class/leds/go:rgb:joystick_rings/effect_index
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the effect attribute.
++
++ Values are monocolor, breathe, chroma, or rainbow.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/class/leds/go:rgb:joystick_rings/enabled
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls enabling or disabling the RGB interface.
++
++ Values are true or false.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/class/leds/go:rgb:joystick_rings/enabled_index
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the enabled attribute.
++
++ Values are true or false.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/class/leds/go:rgb:joystick_rings/mode
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls the operating mode of the RGB interface.
++
++ Values are dynamic or custom. Custom allows setting the RGB effect and color.
++ Dynamic is a Windows mode for syncing Lenovo RGB interfaces not currently
++ supported under Linux.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/class/leds/go:rgb:joystick_rings/mode_index
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the mode attribute.
++
++ Values are dynamic or custom.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/class/leds/go:rgb:joystick_rings/profile
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls selecting the configured RGB profile.
++
++ Values are 1-3.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/class/leds/go:rgb:joystick_rings/profile_range
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the profile attribute.
++
++ Values are 1-3.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/class/leds/go:rgb:joystick_rings/speed
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls the change rate for the breathe, chroma, and rainbow effects.
++
++ Values are 0-100.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/class/leds/go:rgb:joystick_rings/speed_range
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the speed attribute.
++
++ Values are 0-100.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/firmware_version
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the firmware version of the internal MCU.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/fps_mode_dpi
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the DPI of the right handle when the FPS mode switch is on.
++
++ Values are 500, 800, 1200, and 1800.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/fps_mode_dpi_index
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the fps_mode_dpi attribute.
++
++ Values are 500, 800, 1200, and 1800.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/hardware_generation
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the hardware generation of the internal MCU.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/hardware_version
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the hardware version of the internal MCU.
++
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/auto_sleep_time
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls the sleep timer due to inactivity for the left removable controller.
++
++ Values are 0-255.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/auto_sleep_time_range
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the left_handle/auto_sleep_time attribute.
++
++ Values are 0-255.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/calibrate_gyro
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This initiates or halts calibration of the left removable controller's IMU.
++
++ Values are start, stop.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/calibrate_gyro_index
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the left_handle/calibrate_gyro attribute.
++
++ Values are start, stop.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/calibrate_gyro_status
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the result of the last attempted calibration of the left removable controller's IMU.
++
++ Values are unknown, success, failure.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/calibrate_joystick
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This initiates or halts calibration of the left removable controller's joystick.
++
++ Values are start, stop.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/calibrate_joystick_index
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the left_handle/calibrate_jotstick attribute.
++
++ Values are start, stop.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/calibrate_joystick_status
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the result of the last attempted calibration of the left removable controller's joystick.
++
++ Values are unknown, success, failure.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/calibrate_tirgger
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This initiates or halts calibration of the left removable controller's trigger.
++
++ Values are start, stop.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/calibrate_gyro_trigger
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the left_handle/calibrate_trigger attribute.
++
++ Values are start, stop.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/calibrate_trigger_status
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the result of the last attempted calibration of the left removable controller's trigger.
++
++ Values are unknown, success, failure.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/firmware_version
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the left removable controller's firmware version.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/hardware_generation
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the hardware generation of the left removable controller.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/hardware_version
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the hardware version of the left removable controller.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/imu_bypass_enabled
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls enabling or disabling the IMU bypass function of the left removable controller.
++
++ Values are true or false.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/imu_bypass_enabled_index
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the left_handle/imu_bypass_enabled attribute.
++
++ Values are true or false.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/imu_enabled
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls enabling or disabling the IMU of the left removable controller.
++
++ Values are true or false.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/imu_enabled_index
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the left_handle/imu_enabled attribute.
++
++ Values are true or false.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/product_version
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the product version of the left removable controller.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/protocol_version
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the protocol version of the left removable controller.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/reset
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: Resets the left removable controller to factory defaults.
++
++ Writing 1 to this path initiates.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/rumble_mode
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls setting the response behavior for rumble events for the left removable controller.
++
++ Values are fps, racing, standarg, spg, rpg.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/rumble_mode_index
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the left_handle/rumble_mode attribute.
++
++ Values are fps, racing, standarg, spg, rpg.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/rumble_notification
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls enabling haptic rumble events for the left removable controller.
++
++ Values are true, false.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/rumble_notification_index
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the left_handle/rumble_notification attribute.
++
++ Values are true, false.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/mode
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls the operating mode of the built-in controller.
++
++ Values are xinput or dinput.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/mode_index
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the mode attribute.
++
++ Values are xinput or dinput.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/os_mode
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls the behavior of built in chord combinations.
++
++ Values are windows or linux.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/os_mode_index
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the os_mode attribute.
++
++ Values are windows or linux.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/product_version
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the product version of the internal MCU.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/protocol_version
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the protocol version of the internal MCU.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/reset_mcu
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: Resets the internal MCU to factory defaults.
++
++ Writing 1 to this path initiates.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/auto_sleep_time
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls the sleep timer due to inactivity for the right removable controller.
++
++ Values are 0-255.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/auto_sleep_time_range
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the right_handle/auto_sleep_time attribute.
++
++ Values are 0-255.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/calibrate_gyro
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This initiates or halts calibration of the right removable controller's IMU.
++
++ Values are start, stop.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/calibrate_gyro_index
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the right_handle/calibrate_gyro attribute.
++
++ Values are start, stop.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/calibrate_gyro_status
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the result of the last attempted calibration of the right removable controller's IMU.
++
++ Values are unknown, success, failure.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/calibrate_joystick
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This initiates or halts calibration of the right removable controller's joystick.
++
++ Values are start, stop.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/calibrate_joystick_index
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the right_handle/calibrate_jotstick attribute.
++
++ Values are start, stop.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/calibrate_joystick_status
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the result of the last attempted calibration of the right removable controller's joystick.
++
++ Values are unknown, success, failure.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/calibrate_tirgger
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This initiates or halts calibration of the right removable controller's trigger.
++
++ Values are start, stop.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/calibrate_gyro_trigger
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the right_handle/calibrate_trigger attribute.
++
++ Values are start, stop.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/calibrate_trigger_status
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the result of the last attempted calibration of the right removable controller's trigger.
++
++ Values are unknown, success, failure.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/firmware_version
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the right removable controller's firmware version.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/hardware_generation
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the hardware generation of the right removable controller.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/hardware_version
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the hardware version of the right removable controller.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/imu_bypass_enabled
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls enabling or disabling the IMU bypass function of the right removable controller.
++
++ Values are true or false.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/imu_bypass_enabled_index
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the right_handle/imu_bypass_enabled attribute.
++
++ Values are true or false.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/imu_enabled
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls enabling or disabling the IMU of the right removable controller.
++
++ Values are true or false.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/imu_enabled_index
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the right_handle/imu_enabled attribute.
++
++ Values are true or false.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/product_version
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the product version of the right removable controller.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/protocol_version
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the protocol version of the right removable controller.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/reset
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: Resets the right removable controller to factory defaults.
++
++ Writing 1 to this path initiates.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/rumble_mode
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls setting the response behavior for rumble events for the right removable controller.
++
++ Values are fps, racing, standarg, spg, rpg.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/rumble_mode_index
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the right_handle/rumble_mode attribute.
++
++ Values are fps, racing, standarg, spg, rpg.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/rumble_notification
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls enabling haptic rumble events for the right removable controller.
++
++ Values are true, false.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/rumble_notification_index
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the right_handle/rumble_notification attribute.
++
++ Values are true, false.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/rumble_intensity
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls setting the rumble intensity for both removable controllers.
++
++ Values are off, low, medium, high.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/rumble_intensity_index
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the rumble_intensity attribute.
++
++ Values are off, low, medium, high.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/touchpad/enabled
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls enabling or disabling the touchpad.
++
++ Values are true, false.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/touchpad/enabled_index
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the touchpad/enabled attribute.
++
++ Values are true, false.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/touchpad/vibration_enabled
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls enabling haptic rumble events for the touchpad.
++
++ Values are true, false.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/touchpad/vibration_enabled_index
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the touchpad/vibration_enabled attribute.
++
++ Values are true, false.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/touchpad/vibration_intensity
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls setting the intensity of the touchpad haptics.
++
++ Values are off, low, medium, high.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/touchpad/vibration_intensity_index
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the touchpad/vibration_intensity attribute.
++
++ Values are off, low, medium, high.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/tx_dongle/firmware_version
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the firmware version of the internal wireless transmission dongle.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/tx_dongle/hardware_generation
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the hardware generation of the internal wireless transmission dongle.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/tx_dongle/hardware_version
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the hardware version of the internal wireless transmission dongle.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/tx_dongle/product_version
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the product version of the internal wireless transmission dongle.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/tx_dongle/protocol_version
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the protocol version of the internal wireless transmission dongle.
++
++ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
++
diff --git a/Documentation/ABI/testing/sysfs-driver-hid-lenovo-go-s b/Documentation/ABI/testing/sysfs-driver-hid-lenovo-go-s
new file mode 100644
index 000000000000..c3c7b0918986
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-driver-hid-lenovo-go-s
@@ -0,0 +1,304 @@
++What: /sys/class/leds/go_s:rgb:joystick_rings/effect
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls the display effect of the RGB interface.
++
++ Values are monocolor, breathe, chroma, or rainbow.
++
++ Applies to Lenovo Legion Go S line of handheld devices.
++
++What: /sys/class/leds/go_s:rgb:joystick_rings/effect_index
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the effect attribute.
++
++ Values are monocolor, breathe, chroma, or rainbow.
++
++ Applies to Lenovo Legion Go S line of handheld devices.
++
++What: /sys/class/leds/go_s:rgb:joystick_rings/enabled
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls enabling or disabling the RGB interface.
++
++ Values are true or false.
++
++ Applies to Lenovo Legion Go S line of handheld devices.
++
++What: /sys/class/leds/go_s:rgb:joystick_rings/enabled_index
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the enabled attribute.
++
++ Values are true or false.
++
++ Applies to Lenovo Legion Go S line of handheld devices.
++
++What: /sys/class/leds/go_s:rgb:joystick_rings/mode
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls the operating mode of the RGB interface.
++
++ Values are dynamic or custom. Custom allows setting the RGB effect and color.
++ Dynamic is a Windows mode for syncing Lenovo RGB interfaces not currently
++ supported under Linux.
++
++ Applies to Lenovo Legion Go S line of handheld devices.
++
++What: /sys/class/leds/go_s:rgb:joystick_rings/mode_index
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the mode attribute.
++
++ Values are dynamic or custom.
++
++ Applies to Lenovo Legion Go S line of handheld devices.
++
++What: /sys/class/leds/go_s:rgb:joystick_rings/profile
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls selecting the configured RGB profile.
++
++ Values are 1-3.
++
++ Applies to Lenovo Legion Go S line of handheld devices.
++
++What: /sys/class/leds/go_s:rgb:joystick_rings/profile_range
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the profile attribute.
++
++ Values are 1-3.
++
++ Applies to Lenovo Legion Go S line of handheld devices.
++
++What: /sys/class/leds/go_s:rgb:joystick_rings/speed
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls the change rate for the breathe, chroma, and rainbow effects.
++
++ Values are 0-100.
++
++ Applies to Lenovo Legion Go S line of handheld devices.
++
++What: /sys/class/leds/go_s:rgb:joystick_rings/speed_range
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the speed attribute.
++
++ Values are 0-100.
++
++ Applies to Lenovo Legion Go S line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/gamepad/auto_sleep_time
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls the sleep timer due to inactivity for the built-in controller.
++
++ Values are 0-255.
++
++ Applies to Lenovo Legion Go S line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/gamepad/auto_sleep_time_range
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the gamepad/auto_sleep_time attribute.
++
++ Values are 0-255.
++
++ Applies to Lenovo Legion Go S line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/gamepad/dpad_mode
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls the operating mode of the built-in controllers D-pad.
++
++ Values are 4-way or 8-way.
++
++ Applies to Lenovo Legion Go S line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/gamepad/dpad_mode_index
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the gamepad/dpad_mode attribute.
++
++ Values are 4-way or 8-way.
++
++ Applies to Lenovo Legion Go S line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/gamepad/mode
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls the operating mode of the built-in controller.
++
++ Values are xinput or dinput.
++
++ Applies to Lenovo Legion Go S line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/gamepad/mode_index
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the gamepad/mode attribute.
++
++ Values are xinput or dinput.
++
++ Applies to Lenovo Legion Go S line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/gamepad/poll_rate
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls the poll rate in Hz of the built-in controller.
++
++ Values are 125, 250, 500, or 1000.
++
++ Applies to Lenovo Legion Go S line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/gamepad/poll_rate_index
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the gamepad/poll_rate attribute.
++
++ Values are 125, 250, 500, or 1000.
++
++ Applies to Lenovo Legion Go S line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/imu/bypass_enabled
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls enabling or disabling the IMU bypass function. When enabled the IMU data is directly reported to the OS through
++an HIDRAW interface.
++
++ Values are true or false.
++
++ Applies to Lenovo Legion Go S line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/imu/bypass_enabled_index
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the imu/bypass_enabled attribute.
++
++ Values are true or false.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/imu/manufacturer
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the manufacturer of the intertial measurment unit.
++
++ Values are Bosch or ST.
++
++ Applies to Lenovo Legion Go S line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/imu/sensor_enabled
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls enabling or disabling the IMU.
++
++ Values are true, false, or wake-2s.
++
++ Applies to Lenovo Legion Go S line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/imu/sensor_enabled_index
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the imu/sensor_enabled attribute.
++
++ Values are true, false, or wake-2s.
++
++ Applies to Lenovo Legion Go S line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/mcu_id
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the MCU Identification Number
++
++ Applies to Lenovo Legion Go S line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/mouse/step
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls which value is used for the mouse sensitivity.
++
++ Values are 1-127.
++
++ Applies to Lenovo Legion Go S line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/mouse/step_range
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the mouse/step attribute.
++
++ Values are 1-127.
++
++ Applies to Lenovo Legion Go S line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/os_mode
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls which value is used for the touchpads operating mode.
++
++ Values are windows or linux.
++
++ Applies to Lenovo Legion Go S line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/os_mode_index
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the os_mode attribute.
++
++ Values are windows or linux.
++
++ Applies to Lenovo Legion Go S line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/touchpad/enabled
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls enabling or disabling the built-in touchpad.
++
++ Values are true or false.
++
++ Applies to Lenovo Legion Go S line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/touchpad/enabled_index
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the touchpad/enabled attribute.
++
++ Values are true or false.
++
++ Applies to Lenovo Legion Go S line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/touchpad/linux_mode
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls behavior of the touchpad events when os_mode is set to linux.
++
++ Values are absolute or relative.
++
++ Applies to Lenovo Legion Go S line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/touchpad/linux_mode_index
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the touchpad/linux_mode attribute.
++
++ Values are absolute or relative.
++
++ Applies to Lenovo Legion Go S line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/touchpad/windows_mode
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This controls behavior of the touchpad events when os_mode is set to windows.
++
++ Values are absolute or relative.
++
++ Applies to Lenovo Legion Go S line of handheld devices.
++
++What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/touchpad/windows_mode_index
++Date: April 2026
++Contact: linux-input@vger.kernel.org
++Description: This displays the available options for the touchpad/windows_mode attribute.
++
++ Values are absolute or relative.
++
++ Applies to Lenovo Legion Go S line of handheld devices.
diff --git a/MAINTAINERS b/MAINTAINERS
index be4a0fcf23dd..9893d26d5434 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -14139,6 +14139,8 @@ LENOVO HID drivers
M: Derek J. Clark <derekjohn.clark@gmail.com>
L: linux-input@vger.kernel.org
S: Maintained
+F: Documentation/ABI/testing/sysfs-driver-hid-lenovo-go
+F: Documentation/ABI/testing/sysfs-driver-hid-lenovo-go-s
F: drivers/hid/hid-lenovo-go-s.c
F: drivers/hid/hid-lenovo-go.c
--
2.51.2
^ permalink raw reply related
* Re: [PATCH 3/4] Documentation: ABI: Document SteelSeries headset sysfs attributes
From: Bagas Sanjaya @ 2025-12-29 10:04 UTC (permalink / raw)
To: Sriman Achanta, Jiri Kosina, Benjamin Tissoires, linux-input,
linux-kernel
In-Reply-To: <20251228122025.154682-4-srimanachanta@gmail.com>
[-- Attachment #1: Type: text/plain, Size: 4103 bytes --]
On Sun, Dec 28, 2025 at 07:20:24AM -0500, Sriman Achanta wrote:
> +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
Sphinx reports htmldocs warnings:
Documentation/ABI/testing/sysfs-driver-hid-steelseries:47: WARNING: Definition list ends without a blank line; unexpected unindent. [docutils]
Documentation/ABI/testing/sysfs-driver-hid-steelseries:35: WARNING: Definition list ends without a blank line; unexpected unindent. [docutils]
Documentation/ABI/testing/sysfs-driver-hid-steelseries:62: WARNING: Definition list ends without a blank line; unexpected unindent. [docutils]
I have to fix up the bullet lists:
---- >8 ----
diff --git a/Documentation/ABI/testing/sysfs-driver-hid-steelseries b/Documentation/ABI/testing/sysfs-driver-hid-steelseries
index 3066dbb486d363..5f6c703d40892a 100644
--- a/Documentation/ABI/testing/sysfs-driver-hid-steelseries
+++ b/Documentation/ABI/testing/sysfs-driver-hid-steelseries
@@ -30,6 +30,7 @@ Description:
the headset speakers.
Range: 0-128 (mapped internally to device-specific values).
+
Access: Write
What: /sys/class/hid/drivers/steelseries/<dev>/inactive_time
@@ -42,6 +43,7 @@ Description:
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
@@ -53,10 +55,12 @@ Description:
(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
+ Range: 0-128, where:
+
+ - 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
@@ -67,8 +71,11 @@ 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.
+ 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
@@ -80,6 +87,7 @@ Description:
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
@@ -91,8 +99,10 @@ Description:
When enabled, the maximum output volume is capped.
Values:
- 0 = Disabled
- 1 = Enabled
+
+ - 0 = Disabled
+ - 1 = Enabled
+
Access: Write
What: /sys/class/hid/drivers/steelseries/<dev>/bluetooth_on_power
@@ -104,8 +114,10 @@ Description:
when the headset is powered on.
Values:
- 0 = Bluetooth must be turned on manually
- 1 = Bluetooth turns on automatically with headset
+
+ - 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
@@ -117,7 +129,9 @@ Description:
a Bluetooth call is active.
Values:
- 0 = No attenuation (mix both equally)
- 1 = Attenuate Game audio by -12dB
- 2 = Mute Game audio completely
+
+ - 0 = No attenuation (mix both equally)
+ - 1 = Attenuate Game audio by -12dB
+ - 2 = Mute Game audio completely
+
Access: Write
Thanks.
--
An old man doll... just what I always wanted! - Clara
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]
^ permalink raw reply related
* Re: [PATCH v2 2/2] dt-bindings: google,cros-ec-keyb: add use-fn-overlay prop
From: Krzysztof Kozlowski @ 2025-12-29 12:49 UTC (permalink / raw)
To: Fabio Baltieri
Cc: Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Benson Leung, Guenter Roeck, Tzung-Bi Shih, Simon Glass,
linux-input, devicetree, chrome-platform, linux-kernel
In-Reply-To: <aU__uxDmeUq20Mg3@google.com>
On 27/12/2025 16:48, Fabio Baltieri wrote:
> On Sat, Dec 27, 2025 at 01:44:26PM +0100, Krzysztof Kozlowski wrote:
>>> diff --git a/Documentation/devicetree/bindings/input/google,cros-ec-keyb.yaml b/Documentation/devicetree/bindings/input/google,cros-ec-keyb.yaml
>>> index fefaaf46a240..437575cdf352 100644
>>> --- a/Documentation/devicetree/bindings/input/google,cros-ec-keyb.yaml
>>> +++ b/Documentation/devicetree/bindings/input/google,cros-ec-keyb.yaml
>>> @@ -44,6 +44,14 @@ properties:
>>> where the lower 16 bits are reserved. This property is specified only
>>> when the keyboard has a custom design for the top row keys.
>>>
>>> + google,use-fn-overlay:
>>> + description: |
>>> + Use a function key overlay. This allows defining an extra set of codes
>>
>> What is a function key overlay? Overlays are DT term and therefore are
>> not suitable for bindings.
>
> Ok, guess I can rename it to `use-fn-key` or `use-fn-layer`, open to
> suggestions really.
Use as Linux should use? Then it's software, so not suitable for DT.
>
>>> + that are sent if a key is pressed while the KEY_FN is held pressed as
>>> + well. The function codes have to be defined in the linux,keymap property
>>> + with an offset of keypad,num-rows from the normal ones.
>>> + type: boolean
>>> +
>>> dependencies:
>>> function-row-physmap: [ 'linux,keymap' ]
>>> google,needs-ghost-filter: [ 'linux,keymap' ]
>>> @@ -132,6 +140,23 @@ examples:
>>> /* UP LEFT */
>>> 0x070b0067 0x070c0069>;
>>> };
>>> + - |
>>> + /* With function keys */
>>> + #include <dt-bindings/input/input.h>
>>> + keyboard-controller {
>>> + compatible = "google,cros-ec-keyb";
>>> + keypad,num-rows = <8>;
>>> + keypad,num-columns = <18>;
>>> + google,use-fn-overlay;
>>
>> Difference in one property does not justify new example.
>
> Sure but when the property is set then one has to specify the extra
> codes in the linux,keymap property and this examples shows how. I'll
> drop it if you want me to but I think there's value in it.
Examples are for verifying schema and you do not have schema enforcing
this, thus still pointless. Add schema for that, assuming property will
stay.
Best regards,
Krzysztof
^ permalink raw reply
* Re: [PATCH v2 2/2] dt-bindings: google,cros-ec-keyb: add use-fn-overlay prop
From: Fabio Baltieri @ 2025-12-29 13:33 UTC (permalink / raw)
To: Krzysztof Kozlowski
Cc: Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Benson Leung, Guenter Roeck, Tzung-Bi Shih, Simon Glass,
linux-input, devicetree, chrome-platform, linux-kernel
In-Reply-To: <0c01fa63-670f-4c82-aa74-dc855cb12a78@kernel.org>
On Mon, Dec 29, 2025 at 01:49:05PM +0100, Krzysztof Kozlowski wrote:
> On 27/12/2025 16:48, Fabio Baltieri wrote:
> > On Sat, Dec 27, 2025 at 01:44:26PM +0100, Krzysztof Kozlowski wrote:
> >>> diff --git a/Documentation/devicetree/bindings/input/google,cros-ec-keyb.yaml b/Documentation/devicetree/bindings/input/google,cros-ec-keyb.yaml
> >>> index fefaaf46a240..437575cdf352 100644
> >>> --- a/Documentation/devicetree/bindings/input/google,cros-ec-keyb.yaml
> >>> +++ b/Documentation/devicetree/bindings/input/google,cros-ec-keyb.yaml
> >>> @@ -44,6 +44,14 @@ properties:
> >>> where the lower 16 bits are reserved. This property is specified only
> >>> when the keyboard has a custom design for the top row keys.
> >>>
> >>> + google,use-fn-overlay:
> >>> + description: |
> >>> + Use a function key overlay. This allows defining an extra set of codes
> >>
> >> What is a function key overlay? Overlays are DT term and therefore are
> >> not suitable for bindings.
> >
> > Ok, guess I can rename it to `use-fn-key` or `use-fn-layer`, open to
> > suggestions really.
>
> Use as Linux should use? Then it's software, so not suitable for DT.
Sorry I'm not sure how I understand the comment, this describes how the
driver handles a keyboard with Fn keys, the codes are defined in the DT
linux,keymap property and the driver needs to know that there's an extra
layer to interpret the codes correctly.
> >>> + that are sent if a key is pressed while the KEY_FN is held pressed as
> >>> + well. The function codes have to be defined in the linux,keymap property
> >>> + with an offset of keypad,num-rows from the normal ones.
> >>> + type: boolean
> >>> +
> >>> dependencies:
> >>> function-row-physmap: [ 'linux,keymap' ]
> >>> google,needs-ghost-filter: [ 'linux,keymap' ]
> >>> @@ -132,6 +140,23 @@ examples:
> >>> /* UP LEFT */
> >>> 0x070b0067 0x070c0069>;
> >>> };
> >>> + - |
> >>> + /* With function keys */
> >>> + #include <dt-bindings/input/input.h>
> >>> + keyboard-controller {
> >>> + compatible = "google,cros-ec-keyb";
> >>> + keypad,num-rows = <8>;
> >>> + keypad,num-columns = <18>;
> >>> + google,use-fn-overlay;
> >>
> >> Difference in one property does not justify new example.
> >
> > Sure but when the property is set then one has to specify the extra
> > codes in the linux,keymap property and this examples shows how. I'll
> > drop it if you want me to but I think there's value in it.
>
> Examples are for verifying schema and you do not have schema enforcing
> this, thus still pointless. Add schema for that, assuming property will
> stay.
Ok got it, I'll just drop it in v3.
^ permalink raw reply
* Re: [PATCH v2 2/2] dt-bindings: google,cros-ec-keyb: add use-fn-overlay prop
From: Krzysztof Kozlowski @ 2025-12-29 14:59 UTC (permalink / raw)
To: Fabio Baltieri
Cc: Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Benson Leung, Guenter Roeck, Tzung-Bi Shih, Simon Glass,
linux-input, devicetree, chrome-platform, linux-kernel
In-Reply-To: <aVKDJhVuOz-V9tb2@google.com>
On 29/12/2025 14:33, Fabio Baltieri wrote:
> On Mon, Dec 29, 2025 at 01:49:05PM +0100, Krzysztof Kozlowski wrote:
>> On 27/12/2025 16:48, Fabio Baltieri wrote:
>>> On Sat, Dec 27, 2025 at 01:44:26PM +0100, Krzysztof Kozlowski wrote:
>>>>> diff --git a/Documentation/devicetree/bindings/input/google,cros-ec-keyb.yaml b/Documentation/devicetree/bindings/input/google,cros-ec-keyb.yaml
>>>>> index fefaaf46a240..437575cdf352 100644
>>>>> --- a/Documentation/devicetree/bindings/input/google,cros-ec-keyb.yaml
>>>>> +++ b/Documentation/devicetree/bindings/input/google,cros-ec-keyb.yaml
>>>>> @@ -44,6 +44,14 @@ properties:
>>>>> where the lower 16 bits are reserved. This property is specified only
>>>>> when the keyboard has a custom design for the top row keys.
>>>>>
>>>>> + google,use-fn-overlay:
>>>>> + description: |
>>>>> + Use a function key overlay. This allows defining an extra set of codes
>>>>
>>>> What is a function key overlay? Overlays are DT term and therefore are
>>>> not suitable for bindings.
>>>
>>> Ok, guess I can rename it to `use-fn-key` or `use-fn-layer`, open to
>>> suggestions really.
>>
>> Use as Linux should use? Then it's software, so not suitable for DT.
>
> Sorry I'm not sure how I understand the comment, this describes how the
> driver handles a keyboard with Fn keys, the codes are defined in the DT
Exactly. The purpose of DT is not to describe how driver should handle
anything.
See also DTS101 from this year's ELCE.
Best regards,
Krzysztof
^ permalink raw reply
* Re: [PATCH v2 2/2] dt-bindings: google,cros-ec-keyb: add use-fn-overlay prop
From: Fabio Baltieri @ 2025-12-29 15:39 UTC (permalink / raw)
To: Krzysztof Kozlowski
Cc: Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Benson Leung, Guenter Roeck, Tzung-Bi Shih, Simon Glass,
linux-input, devicetree, chrome-platform, linux-kernel
In-Reply-To: <c539aa2a-e9be-40e7-9b75-1723bdaf0a39@kernel.org>
On Mon, Dec 29, 2025 at 03:59:44PM +0100, Krzysztof Kozlowski wrote:
> On 29/12/2025 14:33, Fabio Baltieri wrote:
> > On Mon, Dec 29, 2025 at 01:49:05PM +0100, Krzysztof Kozlowski wrote:
> >> On 27/12/2025 16:48, Fabio Baltieri wrote:
> >>> On Sat, Dec 27, 2025 at 01:44:26PM +0100, Krzysztof Kozlowski wrote:
> >>>>> diff --git a/Documentation/devicetree/bindings/input/google,cros-ec-keyb.yaml b/Documentation/devicetree/bindings/input/google,cros-ec-keyb.yaml
> >>>>> index fefaaf46a240..437575cdf352 100644
> >>>>> --- a/Documentation/devicetree/bindings/input/google,cros-ec-keyb.yaml
> >>>>> +++ b/Documentation/devicetree/bindings/input/google,cros-ec-keyb.yaml
> >>>>> @@ -44,6 +44,14 @@ properties:
> >>>>> where the lower 16 bits are reserved. This property is specified only
> >>>>> when the keyboard has a custom design for the top row keys.
> >>>>>
> >>>>> + google,use-fn-overlay:
> >>>>> + description: |
> >>>>> + Use a function key overlay. This allows defining an extra set of codes
> >>>>
> >>>> What is a function key overlay? Overlays are DT term and therefore are
> >>>> not suitable for bindings.
> >>>
> >>> Ok, guess I can rename it to `use-fn-key` or `use-fn-layer`, open to
> >>> suggestions really.
> >>
> >> Use as Linux should use? Then it's software, so not suitable for DT.
> >
> > Sorry I'm not sure how I understand the comment, this describes how the
> > driver handles a keyboard with Fn keys, the codes are defined in the DT
>
> Exactly. The purpose of DT is not to describe how driver should handle
> anything.
>
> See also DTS101 from this year's ELCE.
Sure so I guess this falls into the "describe the hardware feature"
category, so is the suggestion to rename it to something like
"has-fn-key"? That would be the hardware feature.
^ 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