Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
56943db
Revert "[FROM-ML] HID: Add documentation for Lenovo Legion Go drivers"
pastaq Feb 24, 2026
966026b
Revert "[FROM-ML] HID: hid-lenovo-go-s: Add IMU and Touchpad RO Attri…
pastaq Feb 24, 2026
93588b6
Revert "[FROM-ML] HID: hid-lenovo-go-s: Add RGB LED control interface"
pastaq Feb 24, 2026
6657b50
Revert "[FROM-ML] HID: hid-lenovo-go-s: Add Touchpad Mode Attributes"
pastaq Feb 24, 2026
22a5ce4
Revert "[FROM-ML] HID: hid-lenovo-go-s: Add Feature Status Attributes"
pastaq Feb 24, 2026
f2ac19b
Revert "[FROM-ML] HID: hid-lenovo-go-s: Add MCU ID Attribute"
pastaq Feb 24, 2026
a58851a
Revert "[FROM-ML] HID: hid-lenovo-go-s: Add Lenovo Legion Go S Series…
pastaq Feb 24, 2026
e794c6e
Revert "[FROM-ML] HID: Include firmware version in the uevent"
pastaq Feb 24, 2026
564e397
Revert "[FROM-ML] HID: hid-lenovo-go: Add OS Mode Toggle"
pastaq Feb 24, 2026
d17e126
Revert "[FROM-ML] HID: hid-lenovo-go: Add Calibration Settings"
pastaq Feb 24, 2026
06db638
Revert "[FROM-ML] HID: hid-lenovo-go: Add RGB LED control interface"
pastaq Feb 24, 2026
495a430
Revert "[FROM-ML] HID: hid-lenovo-go: Add FPS Mode DPI settings"
pastaq Feb 24, 2026
36c710d
Revert "[FROM-ML] HID: hid-lenovo-go: Add Rumble and Haptic Settings"
pastaq Feb 24, 2026
2c18fef
Revert "[FROM-ML] HID: hid-lenovo-go: Add Feature Status Attributes"
pastaq Feb 24, 2026
df55679
Revert "[FROM-ML] HID: hid-lenovo-go: Add Lenovo Legion Go Series HID…
pastaq Feb 24, 2026
39d6bd3
Revert "[FROM-ML] include: device.h: Add named device attributes"
pastaq Feb 24, 2026
c544306
[FROM-ML] include: device.h: Add named device attributes
pastaq Feb 24, 2026
88dd619
[FROM-ML] HID: hid-lenovo-go: Add Lenovo Legion Go Series HID Driver
pastaq Feb 24, 2026
5102f5d
[FROM-ML] HID: hid-lenovo-go: Add Feature Status Attributes
pastaq Feb 24, 2026
6352448
[FROM-ML] HID: hid-lenovo-go: Add Rumble and Haptic Settings
pastaq Feb 24, 2026
2fb495f
[FROM-ML] HID: hid-lenovo-go: Add FPS Mode DPI settings
pastaq Feb 24, 2026
312d0b0
[FROM-ML] HID: hid-lenovo-go: Add RGB LED control interface
pastaq Feb 24, 2026
ccdd970
[FROM-ML] HID: hid-lenovo-go: Add Calibration Settings
pastaq Feb 24, 2026
efbf0ea
[FROM-ML] HID: hid-lenovo-go: Add OS Mode Toggle
pastaq Feb 24, 2026
3109d93
[FROM-ML] HID: Include firmware version in the uevent
superm1 Feb 24, 2026
f13a22b
[FROM-ML] HID: hid-lenovo-go-s: Add Lenovo Legion Go S Series HID Driver
pastaq Feb 24, 2026
3d583e2
[FROM-ML] HID: hid-lenovo-go-s: Add MCU ID Attribute
pastaq Feb 24, 2026
735dfcc
[FROM-ML] HID: hid-lenovo-go-s: Add Feature Status Attributes
pastaq Feb 24, 2026
993eb0d
[FROM-ML] HID: hid-lenovo-go-s: Add Touchpad Mode Attributes
pastaq Feb 24, 2026
bb7174b
[FROM-ML] HID: hid-lenovo-go-s: Add RGB LED control interface
pastaq Feb 24, 2026
061a493
[FROM-ML] HID: hid-lenovo-go-s: Add IMU and Touchpad RO Attributes
pastaq Feb 24, 2026
475f6d3
[FROM-ML] HID: Add documentation for Lenovo Legion Go drivers
pastaq Feb 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 42 additions & 110 deletions drivers/hid/hid-lenovo-go-s.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
/*
* HID driver for Lenovo Legion Go S devices.
*
* Copyright (c) 2025 Derek J. Clark <derekjohn.clark@gmail.com>
* Copyright (c) 2026 Derek J. Clark <derekjohn.clark@gmail.com>
* Copyright (c) 2026 Valve Corporation
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

Expand Down Expand Up @@ -32,7 +33,6 @@
#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 led_classdev *led_cdev;
Expand Down Expand Up @@ -397,12 +397,12 @@ static int hid_gos_raw_event(struct hid_device *hdev, struct hid_report *report,
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;
return 0;

if (size != GO_S_PACKET_SIZE)
return -EINVAL;

cmd_rep = (struct command_report *)data;

Expand Down Expand Up @@ -439,52 +439,49 @@ static int hid_gos_raw_event(struct hid_device *hdev, struct hid_report *report,

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)
{
unsigned char *dmabuf __free(kfree) = NULL;
u8 header[] = { command, index };
size_t header_size = ARRAY_SIZE(header);
size_t total_size = header_size + len;
int timeout = 5;
int ret;
int timeout, ret;

/* PL_TEST commands can take longer because they go out to another device */
if (command == GET_PL_TEST)
timeout = 200;
if (header_size + len > GO_S_PACKET_SIZE)
return -EINVAL;

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);
/* We can't use a devm_alloc reusable buffer without side effects during suspend */
dmabuf = kzalloc(GO_S_PACKET_SIZE, GFP_KERNEL);
if (!dmabuf)
return -ENOMEM;

memcpy(dmabuf, header, header_size);
memcpy(dmabuf + header_size, data, len);

dev_dbg(&hdev->dev, "Send data as raw output report: [%*ph]\n",
GO_S_PACKET_SIZE, drvdata.buf);
GO_S_PACKET_SIZE, dmabuf);

ret = hid_hw_output_report(hdev, drvdata.buf, GO_S_PACKET_SIZE);
ret = hid_hw_output_report(hdev, dmabuf, GO_S_PACKET_SIZE);
if (ret < 0)
return ret;

ret = ret == GO_S_PACKET_SIZE ? 0 : -EINVAL;
if (ret)
return ret;

/* PL_TEST commands can take longer because they go out to another device */
timeout = (command == GET_PL_TEST) ? 200 : 5;
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;
return 0;
}

static ssize_t gamepad_property_store(struct device *dev,
Expand Down Expand Up @@ -540,7 +537,6 @@ static ssize_t gamepad_property_store(struct device *dev,
if (ret < 0)
return ret;
val = ret;
drvdata.os_mode = val;
break;
case FEATURE_POLL_RATE:
ret = sysfs_match_string(poll_rate_text, buf);
Expand Down Expand Up @@ -807,13 +803,8 @@ static ssize_t test_property_show(struct device *dev,
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;
Expand Down Expand Up @@ -1359,56 +1350,38 @@ static void cfg_setup(struct work_struct *work)

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;
}

/* 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);
dev_err(&drvdata.hdev->dev, "Failed to retrieve MCU Version: %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);
ret = mcu_property_out(drvdata.hdev, GET_PL_TEST, TEST_TP_MFR, 0, 0);
if (ret) {
dev_err(&drvdata.hdev->dev,
"Failed to retrieve Touchpad Manufacturer: %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);
ret = mcu_property_out(drvdata.hdev, GET_PL_TEST, TEST_TP_VER, 0, 0);
if (ret) {
dev_err(&drvdata.hdev->dev,
"Failed to retrieve Touchpad Firmware Version: %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);
ret = mcu_property_out(drvdata.hdev, GET_PL_TEST, TEST_IMU_MFR, 0, 0);
if (ret) {
dev_err(&drvdata.hdev->dev,
"Failed to retrieve IMU Manufacturer: %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);

Expand Down Expand Up @@ -1440,13 +1413,12 @@ static int hid_gos_cfg_probe(struct hid_device *hdev,
* 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));
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");
dev_err(&hdev->dev, "Failed to schedule startup delayed work\n");
return -ENODEV;
}

return 0;
}

Expand All @@ -1460,41 +1432,18 @@ 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)
{
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);
ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW);
if (ret) {
hid_err(hdev, "Failed to start HID device\n");
return ret;
Expand All @@ -1509,17 +1458,15 @@ static int hid_gos_probe(struct hid_device *hdev,

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);
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_err_probe(&hdev->dev, ret, "Failed to start configuration interface");

dev_dbg(&hdev->dev, "Started Legion Go S HID Device: %x\n", ep);
dev_dbg(&hdev->dev, "Started interface %x as Go S configuration interface\n", ep);
return ret;
}

Expand All @@ -1539,20 +1486,6 @@ 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) },
Expand All @@ -1567,7 +1500,6 @@ 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);
Expand Down
Loading