]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
HID: hid-lenovo-go-s: Add Lenovo Legion Go S Series HID Driver
authorDerek J. Clark <derekjohn.clark@gmail.com>
Tue, 10 Mar 2026 07:29:28 +0000 (07:29 +0000)
committerJiri Kosina <jkosina@suse.com>
Tue, 10 Mar 2026 16:53:17 +0000 (17:53 +0100)
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.

Reviewed-by: Mark Pearson <mpearson-lenovo@squebb.ca>
Co-developed-by: Mario Limonciello <mario.limonciello@amd.com>
Signed-off-by: Mario Limonciello <mario.limonciello@amd.com>
Co-developed-by: Ethan Tidmore <ethantidmore06@gmail.com>
Signed-off-by: Ethan Tidmore <ethantidmore06@gmail.com>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
Signed-off-by: Jiri Kosina <jkosina@suse.com>
MAINTAINERS
drivers/hid/Kconfig
drivers/hid/Makefile
drivers/hid/hid-ids.h
drivers/hid/hid-lenovo-go-s.c [new file with mode: 0644]

index c4987c39d9daa9822e7abe58c43fe591811735ff..3602aadaf1e89a7bb85a5409cf2991231d528e4d 100644 (file)
@@ -14472,6 +14472,7 @@ M:      Derek J. Clark <derekjohn.clark@gmail.com>
 M:     Mark Pearson <mpearson-lenovo@squebb.ca>
 L:     linux-input@vger.kernel.org
 S:     Maintained
+F:     drivers/hid/hid-lenovo-go-s.c
 F:     drivers/hid/hid-lenovo-go.c
 F:     drivers/hid/hid-lenovo.c
 
index 2925dba429f525b21c9a79717617e70e3fc46b31..10c12d8e65579b4728da854ebfbe0f874f7c4b1c 100644 (file)
@@ -635,6 +635,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
index 79fbe4e3e2f474eb063827e6fef9bcf6930eed2a..07dfdb6a49c592426398b496a161c6cc9f8e95f6 100644 (file)
@@ -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
index 04bbfc408a99adbebfca4d5526af999c1f28d657..64a88f2c55f258ceda7bc507e74f5b092257e27b 100644 (file)
 #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 (file)
index 0000000..c9f57df
--- /dev/null
@@ -0,0 +1,278 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ *  HID driver for Lenovo Legion Go S devices.
+ *
+ *  Copyright (c) 2026 Derek J. Clark <derekjohn.clark@gmail.com>
+ *  Copyright (c) 2026 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/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
+
+static struct hid_gos_cfg {
+       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 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) {
+               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;
+
+       ep = get_endpoint_address(hdev);
+       if (ep != GO_S_CFG_INTF_IN)
+               return 0;
+
+       if (size != GO_S_PACKET_SIZE)
+               return -EINVAL;
+
+       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;
+}
+
+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);
+       int timeout, ret;
+
+       if (header_size + len > GO_S_PACKET_SIZE)
+               return -EINVAL;
+
+       guard(mutex)(&drvdata.cfg_mutex);
+       /* 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, dmabuf);
+
+       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;
+
+       reinit_completion(&drvdata.send_cmd_complete);
+       return 0;
+}
+
+static void cfg_setup(struct work_struct *work)
+{
+       int ret;
+
+       ret = mcu_property_out(drvdata.hdev, GET_VERSION, FEATURE_NONE, NULL, 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)
+{
+       int ret;
+
+       hid_set_drvdata(hdev, &drvdata);
+       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;
+
+       ret = hid_parse(hdev);
+       if (ret) {
+               hid_err(hdev, "Parse failed\n");
+               return ret;
+       }
+
+       ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW);
+       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 interface %x as Go S configuration interface\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");