mirror of
https://github.com/torvalds/linux.git
synced 2026-05-13 00:28:54 +02:00
HID: hid-lenovo-go-s: Add Lenovo Legion Go S Series HID Driver
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>
This commit is contained in:
parent
6ca9029c82
commit
a23f3497bf
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -740,6 +740,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
|
||||
|
|
|
|||
278
drivers/hid/hid-lenovo-go-s.c
Normal file
278
drivers/hid/hid-lenovo-go-s.c
Normal file
|
|
@ -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");
|
||||
Loading…
Reference in New Issue
Block a user