* [PATCH v3 11/16] HID: hid-lenovo-go-s: Add MCU ID Attribute
From: Derek J. Clark @ 2026-01-24 1:49 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: <20260124014907.991265-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.52.0
^ permalink raw reply related
* [PATCH v3 10/16] HID: hid-lenovo-go-s: Add Lenovo Legion Go S Series HID Driver
From: Derek J. Clark @ 2026-01-24 1:49 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: <20260124014907.991265-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: Mario Limonciello <mario.limonciello@amd.com>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
V3:
- Include Mario's SOB tag
---
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 c6396be68ff1..723598010189 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -14140,6 +14140,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
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.52.0
^ permalink raw reply related
* [PATCH v3 09/16] HID: Include firmware version in the uevent
From: Derek J. Clark @ 2026-01-24 1:49 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: <20260124014907.991265-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.52.0
^ permalink raw reply related
* [PATCH v3 08/16] HID: hid-lenovo-go: Add OS Mode Toggle
From: Derek J. Clark @ 2026-01-24 1:48 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: <20260124014907.991265-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>
---
V3:
- Fix collision with os_mode_index attribute and os_mode_index enum.
---
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 6326a559fa73..a09950874add 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_type_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.52.0
^ permalink raw reply related
* [PATCH v3 07/16] HID: hid-lenovo-go: Add Calibration Settings
From: Derek J. Clark @ 2026-01-24 1:48 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: <20260124014907.991265-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 91eb61037a2c..6326a559fa73 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_enabled_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_enabled_right.attr,
--
2.52.0
^ permalink raw reply related
* [PATCH v3 06/16] HID: hid-lenovo-go: Add RGB LED control interface
From: Derek J. Clark @ 2026-01-24 1:48 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: <20260124014907.991265-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 5d454cd2cdae..91eb61037a2c 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.52.0
^ permalink raw reply related
* [PATCH v3 04/16] HID: hid-lenovo-go: Add Rumble and Haptic Settings
From: Derek J. Clark @ 2026-01-24 1:48 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: <20260124014907.991265-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>
---
V3:
- Remove erroneous renaming of enabled -> enable for some left & right
handle attributes.
---
drivers/hid/hid-lenovo-go.c | 312 ++++++++++++++++++++++++++++++++++++
1 file changed, 312 insertions(+)
diff --git a/drivers/hid/hid-lenovo-go.c b/drivers/hid/hid-lenovo-go.c
index 681791f119d1..fe39a2b38f38 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,
@@ -908,6 +1173,17 @@ 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);
+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,
@@ -916,6 +1192,10 @@ static struct attribute *left_gamepad_attrs[] = {
&dev_attr_imu_enabled_left.attr,
&dev_attr_imu_enabled_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,
@@ -964,6 +1244,17 @@ 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);
+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,
@@ -972,6 +1263,10 @@ static struct attribute *right_gamepad_attrs[] = {
&dev_attr_imu_enabled_right.attr,
&dev_attr_imu_enabled_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.52.0
^ permalink raw reply related
* [PATCH v3 05/16] HID: hid-lenovo-go: Add FPS Mode DPI settings
From: Derek J. Clark @ 2026-01-24 1:48 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: <20260124014907.991265-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 fe39a2b38f38..5d454cd2cdae 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.52.0
^ permalink raw reply related
* [PATCH v3 03/16] HID: hid-lenovo-go: Add Feature Status Attributes
From: Derek J. Clark @ 2026-01-24 1:48 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: <20260124014907.991265-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.52.0
^ permalink raw reply related
* [PATCH v3 02/16] HID: hid-lenovo-go: Add Lenovo Legion Go Series HID Driver
From: Derek J. Clark @ 2026-01-24 1:48 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: <20260124014907.991265-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>
---
V3:
- Add hid-lenovo.c and Mark Pearson to LENOVO HID DRIVERS entry in MAINTAINERS
---
MAINTAINERS | 8 +
drivers/hid/Kconfig | 12 +
drivers/hid/Makefile | 1 +
drivers/hid/hid-ids.h | 3 +
drivers/hid/hid-lenovo-go.c | 734 ++++++++++++++++++++++++++++++++++++
5 files changed, 758 insertions(+)
create mode 100644 drivers/hid/hid-lenovo-go.c
diff --git a/MAINTAINERS b/MAINTAINERS
index 9ed6d11a7746..c6396be68ff1 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -14135,6 +14135,14 @@ 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>
+M: Mark Pearson <mpearson-lenovo@squebb.ca>
+L: linux-input@vger.kernel.org
+S: Maintained
+F: drivers/hid/hid-lenovo-go.c
+F: drivers/hid/hid-lenovo.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.52.0
^ permalink raw reply related
* [PATCH v3 01/16] include: device.h: Add named device attributes
From: Derek J. Clark @ 2026-01-24 1:48 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: <20260124014907.991265-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.52.0
^ permalink raw reply related
* [PATCH v3 00/16] HID: Add Legion Go and Go S Drivers
From: Derek J. Clark @ 2026-01-24 1:48 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 supressed:
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
V3:
- Fix Documentation formatting by removing extra + characters
- Fix bugs in hid-lenovo-go-s IMU & TP RO attributes being tied to the
wrong _show function.
- Rename enume os_mode_index to os_mode_types_index to fix collision
with os_mode_index attribute.
- Remove accidental rename for enabled->enable attributes in patch 4
- Add SOB for Mario in patch 10 as Co-Developer
V2: https://lore.kernel.org/linux-input/20251229031753.581664-1-derekjohn.clark@gmail.com/
- Break up adding the Go S driver into feature specific patches
- Rename Go S driver from lenovo-legos-hid to hid-lenovo-go-s
- Drop the arbitrary uevent properties patch
- Add Go serires driver
- Move DEVICE_ATTR_NAMED macros to device.h
V1: https://lore.kernel.org/linux-input/20250703004943.515919-1-derekjohn.clark@gmail.com/
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 | 11 +
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, 5100 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.52.0
^ permalink raw reply
* Re: [PATCH v2 RESEND] selftests: hid: tests: test_wacom_generic: add tests for display devices and opaque devices
From: Erin Skomra @ 2026-01-23 22:54 UTC (permalink / raw)
To: Jiri Kosina
Cc: Alex Tran, Benjamin Tissoires, shuah, linux-input,
linux-kselftest, linux-kernel, Ping Cheng, Jason Gerecke
In-Reply-To: <38r99s88-7pp2-6s6n-2148-34s5rrs4s225@xreary.bet>
Hi all and thanks so much to Alex for the patch.
We tested and reviewed the patch and everything looks good with one suggestion.
Since the properties are mutually exclusive we'd like you to add one
line to each prop test asserting that the opposite property is NOT
set.
Thanks again,
Erin Skomra
Software Engineer II at Wacom
On Sat, Jan 10, 2026 at 1:20 AM Jiri Kosina <jikos@kernel.org> wrote:
>
> On Sat, 20 Dec 2025, Alex Tran wrote:
>
> > Verify Wacom devices set INPUT_PROP_DIRECT on display devices and
> > INPUT_PROP_POINTER on opaque devices. Moved test_prop_pointer into
> > TestOpaqueTablet. Created a DirectTabletTest mixin class for
> > test_prop_direct that can be inherited by display tablet test classes.
> > Used DirectTabletTest for TestDTH2452Tablet case.
> >
> > Signed-off-by: Alex Tran <alex.t.tran@gmail.com>
>
> Let me CC Benjamin and Wacom folks to take a look here as well.
>
> > ---
> > Changes in v2:
> > - Removed the tests from the BaseTest class
> > - Removed disabling tests for certain subclasses
> > - Moved test_prop_pointer under TestOpaqueTablet
> > - Created DirectTabletTest mixin class
> > - Moved test_prop_direct under TestDTH2452Tablet
> > .../selftests/hid/tests/test_wacom_generic.py | 30 +++++++++++--------
> > 1 file changed, 17 insertions(+), 13 deletions(-)
> >
> > diff --git a/tools/testing/selftests/hid/tests/test_wacom_generic.py b/tools/testing/selftests/hid/tests/test_wacom_generic.py
> > index 2d6d04f0f..9d0b0802d 100644
> > --- a/tools/testing/selftests/hid/tests/test_wacom_generic.py
> > +++ b/tools/testing/selftests/hid/tests/test_wacom_generic.py
> > @@ -598,18 +598,6 @@ class BaseTest:
> > if unit_set:
> > assert required[usage].contains(field)
> >
> > - def test_prop_direct(self):
> > - """
> > - Todo: Verify that INPUT_PROP_DIRECT is set on display devices.
> > - """
> > - pass
> > -
> > - def test_prop_pointer(self):
> > - """
> > - Todo: Verify that INPUT_PROP_POINTER is set on opaque devices.
> > - """
> > - pass
> > -
> >
> > class PenTabletTest(BaseTest.TestTablet):
> > def assertName(self, uhdev):
> > @@ -677,6 +665,13 @@ class TestOpaqueTablet(PenTabletTest):
> > uhdev.event(130, 240, pressure=0), [], auto_syn=False, strict=True
> > )
> >
> > + def test_prop_pointer(self):
> > + """
> > + Verify that INPUT_PROP_POINTER is set on opaque devices.
> > + """
> > + evdev = self.uhdev.get_evdev()
> > + assert libevdev.INPUT_PROP_POINTER in evdev.properties
> > +
> >
> > class TestOpaqueCTLTablet(TestOpaqueTablet):
> > def create_device(self):
> > @@ -862,7 +857,16 @@ class TestPTHX60_Pen(TestOpaqueCTLTablet):
> > )
> >
> >
> > -class TestDTH2452Tablet(test_multitouch.BaseTest.TestMultitouch, TouchTabletTest):
> > +class DirectTabletTest():
> > + def test_prop_direct(self):
> > + """
> > + Verify that INPUT_PROP_DIRECT is set on display devices.
> > + """
> > + evdev = self.uhdev.get_evdev()
> > + assert libevdev.INPUT_PROP_DIRECT in evdev.properties
> > +
> > +
> > +class TestDTH2452Tablet(test_multitouch.BaseTest.TestMultitouch, TouchTabletTest, DirectTabletTest):
> > ContactIds = namedtuple("ContactIds", "contact_id, tracking_id, slot_num")
> >
> > def create_device(self):
> > --
> > 2.51.0
> >
>
> --
> Jiri Kosina
> SUSE Labs
>
>
^ permalink raw reply
* [PATCH 2/2] Input: st1232 - expose firmware version via sysfs
From: Michael Tretter @ 2026-01-23 16:28 UTC (permalink / raw)
To: Dmitry Torokhov; +Cc: linux-input, kernel, Michael Tretter, Khalid Talash
In-Reply-To: <20260123-input-st1232-firmware-version-v1-0-32df7eefdafe@pengutronix.de>
Allow user space programs to read the firmware version and revision of
the touch controller from the sysfs.
Suggested-by: Khalid Talash <ktalash@topcon.com>
Signed-off-by: Michael Tretter <m.tretter@pengutronix.de>
---
drivers/input/touchscreen/st1232.c | 29 +++++++++++++++++++++++++++++
1 file changed, 29 insertions(+)
diff --git a/drivers/input/touchscreen/st1232.c b/drivers/input/touchscreen/st1232.c
index 3362ee83b21d..0da7f2cec60d 100644
--- a/drivers/input/touchscreen/st1232.c
+++ b/drivers/input/touchscreen/st1232.c
@@ -69,6 +69,34 @@ struct st1232_ts_data {
u32 fw_revision;
};
+static ssize_t fw_version_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct i2c_client *client = to_i2c_client(dev);
+ struct st1232_ts_data *st1232_ts = i2c_get_clientdata(client);
+
+ return sysfs_emit(buf, "%u\n", st1232_ts->fw_version);
+}
+
+static ssize_t fw_revision_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct i2c_client *client = to_i2c_client(dev);
+ struct st1232_ts_data *st1232_ts = i2c_get_clientdata(client);
+
+ return sysfs_emit(buf, "%08x\n", st1232_ts->fw_revision);
+}
+
+static DEVICE_ATTR_RO(fw_version);
+static DEVICE_ATTR_RO(fw_revision);
+
+static struct attribute *st1232_attrs[] = {
+ &dev_attr_fw_version.attr,
+ &dev_attr_fw_revision.attr,
+ NULL,
+};
+ATTRIBUTE_GROUPS(st1232);
+
static int st1232_ts_read_data(struct st1232_ts_data *ts, u8 reg,
unsigned int n)
{
@@ -445,6 +473,7 @@ static struct i2c_driver st1232_ts_driver = {
.driver = {
.name = ST1232_TS_NAME,
.of_match_table = st1232_ts_dt_ids,
+ .dev_groups = st1232_groups,
.probe_type = PROBE_PREFER_ASYNCHRONOUS,
.pm = pm_sleep_ptr(&st1232_ts_pm_ops),
},
--
2.47.3
^ permalink raw reply related
* [PATCH 1/2] Input: st1232 - read firmware version and revision
From: Michael Tretter @ 2026-01-23 16:28 UTC (permalink / raw)
To: Dmitry Torokhov; +Cc: linux-input, kernel, Michael Tretter, Khalid Talash
In-Reply-To: <20260123-input-st1232-firmware-version-v1-0-32df7eefdafe@pengutronix.de>
According to the data sheet, the st1332 contains a firmware, which may
be updated. The version and revision of the firmware may be read from
the registers of the device.
Read the firmware version and revision and report it to the log when
probing the device.
Suggested-by: Khalid Talash <ktalash@topcon.com>
Signed-off-by: Michael Tretter <m.tretter@pengutronix.de>
---
drivers/input/touchscreen/st1232.c | 37 +++++++++++++++++++++++++++++++++++++
1 file changed, 37 insertions(+)
diff --git a/drivers/input/touchscreen/st1232.c b/drivers/input/touchscreen/st1232.c
index 9b3901eec0a5..3362ee83b21d 100644
--- a/drivers/input/touchscreen/st1232.c
+++ b/drivers/input/touchscreen/st1232.c
@@ -22,11 +22,15 @@
#include <linux/pm_qos.h>
#include <linux/slab.h>
#include <linux/types.h>
+#include <linux/unaligned.h>
#include <linux/input/touch-overlay.h>
#define ST1232_TS_NAME "st1232-ts"
#define ST1633_TS_NAME "st1633-ts"
+#define REG_FIRMWARE_VERSION 0x00
+#define REG_FIRMWARE_REVISION_3 0x0C
+
#define REG_STATUS 0x01 /* Device Status | Error Code */
#define STATUS_NORMAL 0x00
@@ -61,6 +65,8 @@ struct st1232_ts_data {
struct list_head touch_overlay_list;
int read_buf_len;
u8 *read_buf;
+ u8 fw_version;
+ u32 fw_revision;
};
static int st1232_ts_read_data(struct st1232_ts_data *ts, u8 reg,
@@ -110,6 +116,26 @@ static int st1232_ts_wait_ready(struct st1232_ts_data *ts)
return -ENXIO;
}
+static int st1232_ts_read_fw_version(struct st1232_ts_data *ts,
+ u8 *fw_version, u32 *fw_revision)
+{
+ int error;
+
+ /* select firmware version register */
+ error = st1232_ts_read_data(ts, REG_FIRMWARE_VERSION, 1);
+ if (error)
+ return error;
+ *fw_version = ts->read_buf[0];
+
+ /* select firmware revision register */
+ error = st1232_ts_read_data(ts, REG_FIRMWARE_REVISION_3, 4);
+ if (error)
+ return error;
+ *fw_revision = get_unaligned_le32(ts->read_buf);
+
+ return 0;
+}
+
static int st1232_ts_read_resolution(struct st1232_ts_data *ts, u16 *max_x,
u16 *max_y)
{
@@ -299,6 +325,17 @@ static int st1232_ts_probe(struct i2c_client *client)
if (error)
return error;
+ /* Read firmware version from the chip */
+ error = st1232_ts_read_fw_version(ts,
+ &ts->fw_version, &ts->fw_revision);
+ if (error) {
+ dev_err(&client->dev,
+ "Failed to read firmware version: %d\n", error);
+ return error;
+ }
+ dev_dbg(&client->dev, "Detected firmware version %u, rev %08x\n",
+ ts->fw_version, ts->fw_revision);
+
if (ts->chip_info->have_z)
input_set_abs_params(input_dev, ABS_MT_TOUCH_MAJOR, 0,
ts->chip_info->max_area, 0, 0);
--
2.47.3
^ permalink raw reply related
* [PATCH 0/2] Input: st1232 - add support for firmware version
From: Michael Tretter @ 2026-01-23 16:28 UTC (permalink / raw)
To: Dmitry Torokhov; +Cc: linux-input, kernel, Michael Tretter, Khalid Talash
The st1332 runs a firmware and reports the version and revision of the
firmware via the registers of the device.
Read the firmware version and revision from the respective registers,
print it to the log and report it via sysfs.
Signed-off-by: Michael Tretter <m.tretter@pengutronix.de>
---
Michael Tretter (2):
Input: st1232 - read firmware version and revision
Input: st1232 - expose firmware version via sysfs
drivers/input/touchscreen/st1232.c | 66 ++++++++++++++++++++++++++++++++++++++
1 file changed, 66 insertions(+)
---
base-commit: c072629f05d7bca1148ab17690d7922a31423984
change-id: 20260123-input-st1232-firmware-version-05cdf03d85c4
Best regards,
--
Michael Tretter <m.tretter@pengutronix.de>
^ permalink raw reply
* Re: [PATCH v1] dt-bindings: input: Add TouchNetix aXiom touchscreen driver
From: Marco Felsch @ 2026-01-23 14:24 UTC (permalink / raw)
To: Krzysztof Kozlowski
Cc: AThomas63, dmitry.torokhov, linux-input, linux-kernel,
mark.satterthwaite, kamel.bouhara
In-Reply-To: <c6fabbcc-4a18-446c-b97e-944cfd23b87e@kernel.org>
Hi Krzysztof,
On 26-01-23, Krzysztof Kozlowski wrote:
> On 22/01/2026 15:40, AThomas63 wrote:
> > Add extra changes referenced in the previous patch:
...
> > +properties:
> > + compatible:
> > + enum:
> > + - tnx,axiom-i2c
> > + - tnx,axiom-spi
>
> No, these are the same. Use only one describing the device.
>
> No device model? Really?
That beeing said, you already provided your review for a device which is
part of this family here:
- https://lore.kernel.org/all/20260111-v6-10-topic-touchscreen-axiom-v5-3-f94e0ae266cb@pengutronix.de/
Regards,
Marco
>
> > +
> > + reg:
> > + maxItems: 1
> > +
> > + interrupts:
> > + maxItems: 1
> > +
> > + axiom,poll-enable:
>
> There is no such company as axiom. You just said it is tnx.
> > + type: boolean
> > + description: Enable aXiom polling mode instead of interrupt-driven
> > + reporting.
>
> Anyway, drop entire property. Not a DT suitable, not hardware.
>
> > +
> > + axiom,poll-period:
> > + $ref: /schemas/types.yaml#/definitions/uint32
> > + default: 10
> > + description: Set the polling period in milliseconds.
>
> Drop entire property.
>
> > +
> > + # Required for SPI
> > + spi-max-frequency: true
>
> Drop. Not needed.
>
> > +
> > + # Common touchscreen properties
> > + touchscreen-size-x: true
> > + touchscreen-size-y: true
> > + touchscreen-inverted-x: true
> > + touchscreen-inverted-y: true
> > + touchscreen-swapped-x-y: true
>
> Drop all.
>
> > +
> > +required:
> > + - compatible
> > + - reg
> > + - interrupts
> > +
> > +allOf:
> > + - $ref: touchscreen.yaml#
>
> Missing ref to spi-periph.
>
> Look at other bindings instead of inventing your own.
>
> > +
> > +unevaluatedProperties: false
> > +
> > +examples:
> > + - |
> > + #include <dt-bindings/interrupt-controller/irq.h>
> > +
> > + i2c {
> > + #address-cells = <1>;
> > + #size-cells = <0>;
> > +
> > + touchscreen@66 {
> > + compatible = "touchnetix,axiom-i2c";
> > + reg = <0x66>;
> > + interrupt-parent = <&gpio>;
> > + interrupts = <24 IRQ_TYPE_LEVEL_LOW>;
> > + axiom,poll-enable;
> > + axiom,poll-period = <15>;
> > + };
> > + };
> > +
> > + - |
> > + #include <dt-bindings/interrupt-controller/irq.h>
> > +
> > + spi {
> > + #address-cells = <1>;
> > + #size-cells = <0>;
> > +
> > + touchscreen@0 {
> > + compatible = "touchnetix,axiom-spi";
> > + reg = <0>;
> > + spi-max-frequency = <4000000>;
> > + interrupt-parent = <&gpio>;
> > + interrupts = <24 IRQ_TYPE_LEVEL_LOW>;
> > + };
> > + };
> > diff --git a/Documentation/devicetree/bindings/vendor-prefixes.yaml b/Documentation/devicetree/bindings/vendor-prefixes.yaml
> > index f1d1882009ba..dadfc7036ed7 100644
> > --- a/Documentation/devicetree/bindings/vendor-prefixes.yaml
> > +++ b/Documentation/devicetree/bindings/vendor-prefixes.yaml
> > @@ -1636,6 +1636,8 @@ patternProperties:
> > description: Trusted Logic Mobility
> > "^tmt,.*":
> > description: Tecon Microprocessor Technologies, LLC.
> > + "^tnx,.*":
>
> Domain is touchnetix, so that's your prefix.
>
> > + description: TouchNetix
> > "^topeet,.*":
> > description: Topeet
> > "^topic,.*":
>
>
> Best regards,
> Krzysztof
>
--
#gernperDu
#CallMeByMyFirstName
Pengutronix e.K. | |
Steuerwalder Str. 21 | https://www.pengutronix.de/ |
31137 Hildesheim, Germany | Phone: +49-5121-206917-0 |
Amtsgericht Hildesheim, HRA 2686 | Fax: +49-5121-206917-9 |
^ permalink raw reply
* Re: [PATCH] HID: multitouch: add eGalaxTouch EXC3188 support
From: Jiri Kosina @ 2026-01-23 12:54 UTC (permalink / raw)
To: Michael Tretter
Cc: Benjamin Tissoires, linux-input, kernel, Thorsten Schmelzer
In-Reply-To: <20260123-egalax-exc3188-v1-1-3f62d06d554d@pengutronix.de>
On Fri, 23 Jan 2026, Michael Tretter wrote:
> From: Thorsten Schmelzer <tschmelzer@topcon.com>
>
> Add support for the for the EXC3188 touchscreen from eGalaxy.
>
> Signed-off-by: Thorsten Schmelzer <tschmelzer@topcon.com>
> Signed-off-by: Michael Tretter <m.tretter@pengutronix.de>
Applied, thanks.
--
Jiri Kosina
SUSE Labs
^ permalink raw reply
* Re: [PATCH] HID: elecom: Add support for ELECOM HUGE Plus M-HT1MRBK
From: Jiri Kosina @ 2026-01-23 12:53 UTC (permalink / raw)
To: David Phillips; +Cc: Benjamin Tissoires, linux-input, linux-kernel
In-Reply-To: <20260123-hid-elecom-huge-plus-fix-v1-1-4bed5e52fb3d@profile.sh>
On Fri, 23 Jan 2026, David Phillips via B4 Relay wrote:
> From: David Phillips <david@profile.sh>
>
> New model in the ELECOM HUGE trackball line that has 8 buttons but the
> report descriptor specifies only 5. The HUGE Plus supports connecting via
> Bluetooth, 2.4GHz wireless USB dongle, and directly via a USB-C cable.
> Each connection type reports a different device id, 01AA for cable,
> 01AB for USB dongle, and 01AC for Bluetooth.
>
> This patch adds these device IDs and applies the fixups similar to the
> other ELECOM devices to get all 8 buttons working for all 3 connection
> types.
>
> For reference, the usbhid-dump output:
> 001:013:001:DESCRIPTOR 1769085639.598405
> 05 01 09 02 A1 01 85 01 09 01 A1 00 05 09 19 01
> 29 05 15 00 25 01 75 01 95 05 81 02 75 03 95 01
> 81 01 05 01 09 30 09 31 16 01 80 26 FF 7F 75 10
> 95 02 81 06 09 38 15 81 25 7F 75 08 95 01 81 06
> 05 0C 0A 38 02 15 81 25 7F 75 08 95 01 81 06 C0
> C0 05 0C 09 01 A1 01 85 02 15 01 26 8C 02 19 01
> 2A 8C 02 75 10 95 01 81 00 C0 05 01 09 80 A1 01
> 85 03 09 82 09 81 09 83 15 00 25 01 19 01 29 03
> 75 01 95 03 81 02 95 05 81 01 C0 06 01 FF 09 00
> A1 01 85 08 09 00 15 00 26 FF 00 75 08 95 07 81
> 02 C0 06 02 FF 09 02 A1 01 85 06 09 02 15 00 26
> FF 00 75 08 95 07 B1 02 C0
>
> Signed-off-by: David Phillips <david@profile.sh>
Applied, thanks.
--
Jiri Kosina
SUSE Labs
^ permalink raw reply
* Re: [PATCH 1/2] linux/interrupt.h: allow "guard" notation to disable and reenable IRQ with valid IRQ check
From: Thomas Gleixner @ 2026-01-23 10:52 UTC (permalink / raw)
To: Sebastian Andrzej Siewior, Marek Vasut
Cc: linux-input, Peter Zijlstra (Intel), Cheng-Yang Chou,
Dmitry Torokhov, Frank Li, Geert Uytterhoeven, Jinjie Ruan,
Krzysztof Kozlowski, Marc Zyngier, linux-kernel,
linux-renesas-soc
In-Reply-To: <20260122162206.9wrHkrTZ@linutronix.de>
On Thu, Jan 22 2026 at 17:22, Sebastian Andrzej Siewior wrote:
> On 2026-01-22 00:23:47 [+0100], Marek Vasut wrote:
>> @@ -242,6 +242,21 @@ extern void irq_wake_thread(unsigned int irq, void *dev_id);
>> DEFINE_LOCK_GUARD_1(disable_irq, int,
>> disable_irq(*_T->lock), enable_irq(*_T->lock))
>>
>> +static inline void disable_valid_irq(unsigned int irq)
>> +{
>> + if (irq > 0)
>> + disable_irq(irq);
>> +}
>
> | $ grep " 0:" /proc/interrupts
> | 0: 43 0 IO-APIC 2-edge timer
>
> in other words, interrupt 0 is valid.
No. It's not really.
Interrupt number zero is a historic leftover and a mistake which is only
relevant to some oddball archaic architectures like x86 and others which
tried to mimic that.
The general agreement is that interrupt 0 is a legacy oddity and only
supported in very special cases. Everything else treats 0 as invalid.
Thanks,
tglx
^ permalink raw reply
* Re: [PATCH 1/1] Adding support for aXiom touchscreen controller
From: Krzysztof Kozlowski @ 2026-01-23 9:29 UTC (permalink / raw)
To: AThomas63, dmitry.torokhov
Cc: linux-input, linux-kernel, mark.satterthwaite, m.felsch,
kamel.bouhara
In-Reply-To: <20260122124819.273188-2-andrew.thomas@touchnetix.com>
On 22/01/2026 13:48, AThomas63 wrote:
> Signed-off-by: AThomas63 <andrew.thomas@touchnetix.com>
Please run scripts/checkpatch.pl on the patches and fix reported
warnings. After that, run also 'scripts/checkpatch.pl --strict' on the
patches and (probably) fix more warnings. Some warnings can be ignored,
especially from --strict run, but the code here looks like it needs a
fix. Feel free to get in touch if the warning is not clear.
Running checkpatch is really basic and EVERY kernel guide asks you to do
that. Not doing it is not an excuse for your first patch, becasue when
writing first patch you should follow the first-patch guidelines or
kernel docs closely.
> ---
> drivers/input/touchscreen/Kconfig | 30 ++
> drivers/input/touchscreen/Makefile | 3 +
> drivers/input/touchscreen/axiom_core.c | 482 +++++++++++++++++++++++++
> drivers/input/touchscreen/axiom_core.h | 128 +++++++
> drivers/input/touchscreen/axiom_i2c.c | 152 ++++++++
> drivers/input/touchscreen/axiom_spi.c | 159 ++++++++
> 6 files changed, 954 insertions(+)
> create mode 100644 drivers/input/touchscreen/axiom_core.c
> create mode 100644 drivers/input/touchscreen/axiom_core.h
> create mode 100644 drivers/input/touchscreen/axiom_i2c.c
> create mode 100644 drivers/input/touchscreen/axiom_spi.c
>
> diff --git a/drivers/input/touchscreen/Kconfig b/drivers/input/touchscreen/Kconfig
> index 7d5b72ee07fa..f2b4fb317bdd 100644
> --- a/drivers/input/touchscreen/Kconfig
> +++ b/drivers/input/touchscreen/Kconfig
> @@ -162,6 +162,36 @@ config TOUCHSCREEN_AUO_PIXCIR
> To compile this driver as a module, choose M here: the
> module will be called auo-pixcir-ts.
>
> +config TOUCHSCREEN_AXIOM_CORE
> + tristate "TouchNetix Axiom touchscreen"
This should not be user-selectable. It's useless alone. Just "tristate"
and change the depending drivers to select it.
> + help
> + Say Y here if you have an Axiom touchscreen connected
> + to your system. You will also need to select appropriate
> + bus connection below.
> +
> + If unsure, say N.
> +
> + To compile this driver as a module, choose M here: the
> + module will be called axiom_core.
> +
> +config TOUCHSCREEN_AXIOM_I2C
> + tristate "support I2C bus connection"
> + depends on TOUCHSCREEN_AXIOM_CORE && I2C
> + help
> + Say Y here if the touchscreen is connected via I2C bus.
> +
> + To compile this driver as a module, choose M here: the
> + module will be called axiom_spi.
> +
> +config TOUCHSCREEN_AXIOM_SPI
> + tristate "support SPI bus connection"
> + depends on TOUCHSCREEN_AXIOM_CORE && SPI
> + help
> + Say Y here if the touchscreen is connected via SPI bus.
> +
> + To compile this driver as a module, choose M here: the
> + module will be called axiom_spi.
> +
...
> + *
> + * Author(s): Mark Satterthwaite <mark.satterthwaite@touchnetix.com>
> + * Bart Prescott <bartp@baasheep.co.uk>
> + * Hannah Rossiter <hannah.rossiter@touchnetix.com>
> + * Andrew Thomas <andrew.thomas@touchnetix.com>
> + *
> + * This program is free software; you can redistribute it and/or modify it
> + * under the terms of the GNU General Public License as published by the
> + * Free Software Foundation; either version 2 of the License, or (at your
> + * option) any later version.
No code in the kernel has such license boilerplate. Please use recent
Linux drivers as starting code. It's really pointless to send to
upstream some old vendor code, because you will repeat all the
mistakes/issues/old style which we fixed already over last 15 years.
> + *
...
> +
> +static const struct spi_device_id axiom_spi_id_table[] = {
> + { "axiom-spi" },
> + {},
> +};
> +MODULE_DEVICE_TABLE(spi, axiom_spi_id_table);
> +
> +static const struct of_device_id axiom_spi_dt_ids[] = {
> + {
> + .compatible = "tnx,axiom-spi",
> + .data = "axiom",
> + },
> + {}
> +};
> +MODULE_DEVICE_TABLE(of, axiom_spi_dt_ids);
> +
> +static struct spi_driver axiom_spi_driver = {
> + .id_table = axiom_spi_id_table,
> + .driver = {
> + .name = "axiom_spi",
> + .of_match_table = of_match_ptr(axiom_spi_dt_ids),
Drop of_match_ptr, leads to warnings here.
> + },
> + .probe = axiom_spi_probe,
> +};
> +
> +module_spi_driver(axiom_spi_driver);
> +
> +MODULE_AUTHOR("TouchNetix <support@touchnetix.com>");
> +MODULE_DESCRIPTION("aXiom touchscreen SPI bus driver");
> +MODULE_LICENSE("GPL");
> +MODULE_ALIAS("axiom");
Drop alias. You should not need MODULE_ALIAS() in normal cases. If you
need it, usually it means your device ID table is wrong (e.g. misses
either entries or MODULE_DEVICE_TABLE()). MODULE_ALIAS() is not a
substitute for incomplete ID table.
> +MODULE_VERSION("1.0.0");
Best regards,
Krzysztof
^ permalink raw reply
* [PATCH] HID: multitouch: add eGalaxTouch EXC3188 support
From: Michael Tretter @ 2026-01-23 8:57 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, kernel, Thorsten Schmelzer, Michael Tretter
From: Thorsten Schmelzer <tschmelzer@topcon.com>
Add support for the for the EXC3188 touchscreen from eGalaxy.
Signed-off-by: Thorsten Schmelzer <tschmelzer@topcon.com>
Signed-off-by: Michael Tretter <m.tretter@pengutronix.de>
---
drivers/hid/hid-ids.h | 1 +
drivers/hid/hid-multitouch.c | 3 +++
2 files changed, 4 insertions(+)
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 9c2bf584d9f6..7dc3fe079491 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -437,6 +437,7 @@
#define USB_DEVICE_ID_DWAV_EGALAX_MULTITOUCH_7349 0x7349
#define USB_DEVICE_ID_DWAV_EGALAX_MULTITOUCH_73F7 0x73f7
#define USB_DEVICE_ID_DWAV_EGALAX_MULTITOUCH_A001 0xa001
+#define USB_DEVICE_ID_DWAV_EGALAX_MULTITOUCH_C000 0xc000
#define USB_DEVICE_ID_DWAV_EGALAX_MULTITOUCH_C002 0xc002
#define USB_VENDOR_ID_EDIFIER 0x2d99
diff --git a/drivers/hid/hid-multitouch.c b/drivers/hid/hid-multitouch.c
index b1c3ef129058..ea2a41d706de 100644
--- a/drivers/hid/hid-multitouch.c
+++ b/drivers/hid/hid-multitouch.c
@@ -2146,6 +2146,9 @@ static const struct hid_device_id mt_devices[] = {
{ .driver_data = MT_CLS_EGALAX_SERIAL,
MT_USB_DEVICE(USB_VENDOR_ID_DWAV,
USB_DEVICE_ID_DWAV_EGALAX_MULTITOUCH_A001) },
+ { .driver_data = MT_CLS_EGALAX_SERIAL,
+ MT_USB_DEVICE(USB_VENDOR_ID_DWAV,
+ USB_DEVICE_ID_DWAV_EGALAX_MULTITOUCH_C000) },
{ .driver_data = MT_CLS_EGALAX,
MT_USB_DEVICE(USB_VENDOR_ID_DWAV,
USB_DEVICE_ID_DWAV_EGALAX_MULTITOUCH_C002) },
---
base-commit: c072629f05d7bca1148ab17690d7922a31423984
change-id: 20260123-egalax-exc3188-03a03e768f42
Best regards,
--
Michael Tretter <m.tretter@pengutronix.de>
^ permalink raw reply related
* Re: [PATCH v1] dt-bindings: input: Add TouchNetix aXiom touchscreen driver
From: Krzysztof Kozlowski @ 2026-01-23 7:27 UTC (permalink / raw)
To: AThomas63, dmitry.torokhov
Cc: linux-input, linux-kernel, mark.satterthwaite, m.felsch,
kamel.bouhara
In-Reply-To: <20260122144052.358956-1-andrew.thomas@touchnetix.com>
On 22/01/2026 15:40, AThomas63 wrote:
> Add extra changes referenced in the previous patch:
> - Add device tree documentation for axiom giving examples for SPI and I2C.
> - Add tnx vendor prefix.
Incomplete DCO chain.
Please run scripts/checkpatch.pl on the patches and fix reported
warnings. After that, run also 'scripts/checkpatch.pl --strict' on the
patches and (probably) fix more warnings. Some warnings can be ignored,
especially from --strict run, but the code here looks like it needs a
fix. Feel free to get in touch if the warning is not clear.
Also, no anonymous contributions. It's also explicitly documented.
Best regards,
Krzysztof
^ permalink raw reply
* Re: [PATCH v1] dt-bindings: input: Add TouchNetix aXiom touchscreen driver
From: Krzysztof Kozlowski @ 2026-01-23 7:27 UTC (permalink / raw)
To: AThomas63, dmitry.torokhov
Cc: linux-input, linux-kernel, mark.satterthwaite, m.felsch,
kamel.bouhara
In-Reply-To: <20260122144052.358956-1-andrew.thomas@touchnetix.com>
On 22/01/2026 15:40, AThomas63 wrote:
> Add extra changes referenced in the previous patch:
Not relevant.
> - Add device tree documentation for axiom giving examples for SPI and I2C.
> - Add tnx vendor prefix.
Please start using b4 or use known (described) workflows with git. Look
at the subject - why is this just one patch not 1/2? Patchsets are made
git format-patch -2 (-vX if needed).
And then:
Please use scripts/get_maintainers.pl to get a list of necessary people
and lists to CC. It might happen, that command when run on an older
kernel, gives you outdated entries. Therefore please be sure you base
your patches on recent Linux kernel.
Tools like b4 or scripts/get_maintainer.pl provide you proper list of
people, so fix your workflow. Tools might also fail if you work on some
ancient tree (don't, instead use mainline) or work on fork of kernel
(don't, instead use mainline). Just use b4 and everything should be
fine, although remember about `b4 prep --auto-to-cc` if you added new
patches to the patchset.
You missed at least devicetree list (maybe more), so this won't be
tested by automated tooling. Performing review on untested code might be
a waste of time.
Please kindly resend and include all necessary To/Cc entries.
>
> ---
> .../bindings/input/touchscreen/tnx,axiom.yaml | 90 +++++++++++++++++++
> .../devicetree/bindings/vendor-prefixes.yaml | 2 +
> 2 files changed, 92 insertions(+)
> create mode 100644 Documentation/devicetree/bindings/input/touchscreen/tnx,axiom.yaml
>
> diff --git a/Documentation/devicetree/bindings/input/touchscreen/tnx,axiom.yaml b/Documentation/devicetree/bindings/input/touchscreen/tnx,axiom.yaml
> new file mode 100644
> index 000000000000..dc2ea62999b8
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/input/touchscreen/tnx,axiom.yaml
> @@ -0,0 +1,90 @@
> +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
> +%YAML 1.2
> +---
> +$id: http://devicetree.org/schemas/input/touchscreen/tnx,axiom.yaml#
> +$schema: http://devicetree.org/meta-schemas/core.yaml#
> +
> +title: TouchNetix aXiom Touchscreen Controller
> +
> +maintainers:
> + - Andrew Thomas <andrew.thomas@touchnetix.com>
> +
> +description: |
> + The TouchNetix aXiom series are high-performance touchscreen controllers
> + supporting various interface methods including I2C and SPI.
> +
> +properties:
> + compatible:
> + enum:
> + - tnx,axiom-i2c
> + - tnx,axiom-spi
No, these are the same. Use only one describing the device.
No device model? Really?
> +
> + reg:
> + maxItems: 1
> +
> + interrupts:
> + maxItems: 1
> +
> + axiom,poll-enable:
There is no such company as axiom. You just said it is tnx.
> + type: boolean
> + description: Enable aXiom polling mode instead of interrupt-driven
> + reporting.
Anyway, drop entire property. Not a DT suitable, not hardware.
> +
> + axiom,poll-period:
> + $ref: /schemas/types.yaml#/definitions/uint32
> + default: 10
> + description: Set the polling period in milliseconds.
Drop entire property.
> +
> + # Required for SPI
> + spi-max-frequency: true
Drop. Not needed.
> +
> + # Common touchscreen properties
> + touchscreen-size-x: true
> + touchscreen-size-y: true
> + touchscreen-inverted-x: true
> + touchscreen-inverted-y: true
> + touchscreen-swapped-x-y: true
Drop all.
> +
> +required:
> + - compatible
> + - reg
> + - interrupts
> +
> +allOf:
> + - $ref: touchscreen.yaml#
Missing ref to spi-periph.
Look at other bindings instead of inventing your own.
> +
> +unevaluatedProperties: false
> +
> +examples:
> + - |
> + #include <dt-bindings/interrupt-controller/irq.h>
> +
> + i2c {
> + #address-cells = <1>;
> + #size-cells = <0>;
> +
> + touchscreen@66 {
> + compatible = "touchnetix,axiom-i2c";
> + reg = <0x66>;
> + interrupt-parent = <&gpio>;
> + interrupts = <24 IRQ_TYPE_LEVEL_LOW>;
> + axiom,poll-enable;
> + axiom,poll-period = <15>;
> + };
> + };
> +
> + - |
> + #include <dt-bindings/interrupt-controller/irq.h>
> +
> + spi {
> + #address-cells = <1>;
> + #size-cells = <0>;
> +
> + touchscreen@0 {
> + compatible = "touchnetix,axiom-spi";
> + reg = <0>;
> + spi-max-frequency = <4000000>;
> + interrupt-parent = <&gpio>;
> + interrupts = <24 IRQ_TYPE_LEVEL_LOW>;
> + };
> + };
> diff --git a/Documentation/devicetree/bindings/vendor-prefixes.yaml b/Documentation/devicetree/bindings/vendor-prefixes.yaml
> index f1d1882009ba..dadfc7036ed7 100644
> --- a/Documentation/devicetree/bindings/vendor-prefixes.yaml
> +++ b/Documentation/devicetree/bindings/vendor-prefixes.yaml
> @@ -1636,6 +1636,8 @@ patternProperties:
> description: Trusted Logic Mobility
> "^tmt,.*":
> description: Tecon Microprocessor Technologies, LLC.
> + "^tnx,.*":
Domain is touchnetix, so that's your prefix.
> + description: TouchNetix
> "^topeet,.*":
> description: Topeet
> "^topic,.*":
Best regards,
Krzysztof
^ permalink raw reply
* [PATCH] HID: elecom: Add support for ELECOM HUGE Plus M-HT1MRBK
From: David Phillips via B4 Relay @ 2026-01-23 3:56 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires; +Cc: linux-input, linux-kernel, David Phillips
From: David Phillips <david@profile.sh>
New model in the ELECOM HUGE trackball line that has 8 buttons but the
report descriptor specifies only 5. The HUGE Plus supports connecting via
Bluetooth, 2.4GHz wireless USB dongle, and directly via a USB-C cable.
Each connection type reports a different device id, 01AA for cable,
01AB for USB dongle, and 01AC for Bluetooth.
This patch adds these device IDs and applies the fixups similar to the
other ELECOM devices to get all 8 buttons working for all 3 connection
types.
For reference, the usbhid-dump output:
001:013:001:DESCRIPTOR 1769085639.598405
05 01 09 02 A1 01 85 01 09 01 A1 00 05 09 19 01
29 05 15 00 25 01 75 01 95 05 81 02 75 03 95 01
81 01 05 01 09 30 09 31 16 01 80 26 FF 7F 75 10
95 02 81 06 09 38 15 81 25 7F 75 08 95 01 81 06
05 0C 0A 38 02 15 81 25 7F 75 08 95 01 81 06 C0
C0 05 0C 09 01 A1 01 85 02 15 01 26 8C 02 19 01
2A 8C 02 75 10 95 01 81 00 C0 05 01 09 80 A1 01
85 03 09 82 09 81 09 83 15 00 25 01 19 01 29 03
75 01 95 03 81 02 95 05 81 01 C0 06 01 FF 09 00
A1 01 85 08 09 00 15 00 26 FF 00 75 08 95 07 81
02 C0 06 02 FF 09 02 A1 01 85 06 09 02 15 00 26
FF 00 75 08 95 07 B1 02 C0
Signed-off-by: David Phillips <david@profile.sh>
---
drivers/hid/Kconfig | 1 +
drivers/hid/hid-elecom.c | 16 ++++++++++++++++
drivers/hid/hid-ids.h | 3 +++
drivers/hid/hid-quirks.c | 3 +++
4 files changed, 23 insertions(+)
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index 920a64b66b25..6ff4a3ad34cb 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -369,6 +369,7 @@ config HID_ELECOM
- EX-G Trackballs (M-XT3DRBK, M-XT3URBK)
- DEFT Trackballs (M-DT1DRBK, M-DT1URBK, M-DT2DRBK, M-DT2URBK)
- HUGE Trackballs (M-HT1DRBK, M-HT1URBK)
+ - HUGE Plus Trackball (M-HT1MRBK)
config HID_ELO
tristate "ELO USB 4000/4500 touchscreen"
diff --git a/drivers/hid/hid-elecom.c b/drivers/hid/hid-elecom.c
index 2003d2dcda7c..37d88ce57f67 100644
--- a/drivers/hid/hid-elecom.c
+++ b/drivers/hid/hid-elecom.c
@@ -5,6 +5,7 @@
* - EX-G Trackballs (M-XT3DRBK, M-XT3URBK, M-XT4DRBK)
* - DEFT Trackballs (M-DT1DRBK, M-DT1URBK, M-DT2DRBK, M-DT2URBK)
* - HUGE Trackballs (M-HT1DRBK, M-HT1URBK)
+ * - HUGE Plus Trackball (M-HT1MRBK)
*
* Copyright (c) 2010 Richard Nauber <Richard.Nauber@gmail.com>
* Copyright (c) 2016 Yuxuan Shui <yshuiv7@gmail.com>
@@ -123,12 +124,25 @@ static const __u8 *elecom_report_fixup(struct hid_device *hdev, __u8 *rdesc,
*/
mouse_button_fixup(hdev, rdesc, *rsize, 22, 30, 24, 16, 8);
break;
+ case USB_DEVICE_ID_ELECOM_M_HT1MRBK:
+ case USB_DEVICE_ID_ELECOM_M_HT1MRBK_01AB:
+ case USB_DEVICE_ID_ELECOM_M_HT1MRBK_01AC:
+ /*
+ * Report descriptor format:
+ * 24: button bit count
+ * 28: padding bit count
+ * 22: button report size
+ * 16: button usage maximum
+ */
+ mouse_button_fixup(hdev, rdesc, *rsize, 24, 28, 22, 16, 8);
+ break;
}
return rdesc;
}
static const struct hid_device_id elecom_devices[] = {
{ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_BM084) },
+ { HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_HT1MRBK_01AC) },
{ HID_USB_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_XGL20DLBK) },
{ HID_USB_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_XT3URBK_00FB) },
{ HID_USB_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_XT3URBK_018F) },
@@ -142,6 +156,8 @@ static const struct hid_device_id elecom_devices[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_HT1URBK_019B) },
{ HID_USB_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_HT1DRBK_010D) },
{ HID_USB_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_HT1DRBK_011C) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_HT1MRBK) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_HT1MRBK_01AB) },
{ }
};
MODULE_DEVICE_TABLE(hid, elecom_devices);
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 9c2bf584d9f6..c9b07890a808 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -465,6 +465,9 @@
#define USB_DEVICE_ID_ELECOM_M_HT1URBK_019B 0x019b
#define USB_DEVICE_ID_ELECOM_M_HT1DRBK_010D 0x010d
#define USB_DEVICE_ID_ELECOM_M_HT1DRBK_011C 0x011c
+#define USB_DEVICE_ID_ELECOM_M_HT1MRBK 0x01aa
+#define USB_DEVICE_ID_ELECOM_M_HT1MRBK_01AB 0x01ab
+#define USB_DEVICE_ID_ELECOM_M_HT1MRBK_01AC 0x01ac
#define USB_VENDOR_ID_DREAM_CHEEKY 0x1d34
#define USB_DEVICE_ID_DREAM_CHEEKY_WN 0x0004
diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c
index 11438039cdb7..3217e436c052 100644
--- a/drivers/hid/hid-quirks.c
+++ b/drivers/hid/hid-quirks.c
@@ -420,6 +420,7 @@ static const struct hid_device_id hid_have_special_driver[] = {
#if IS_ENABLED(CONFIG_HID_ELECOM)
{ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_BM084) },
{ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_XGL20DLBK) },
+ { HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_HT1MRBK_01AC) },
{ HID_USB_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_XT3URBK_00FB) },
{ HID_USB_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_XT3URBK_018F) },
{ HID_USB_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_XT3DRBK_00FC) },
@@ -432,6 +433,8 @@ static const struct hid_device_id hid_have_special_driver[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_HT1URBK_019B) },
{ HID_USB_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_HT1DRBK_010D) },
{ HID_USB_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_HT1DRBK_011C) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_HT1MRBK) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_HT1MRBK_01AB) },
#endif
#if IS_ENABLED(CONFIG_HID_ELO)
{ HID_USB_DEVICE(USB_VENDOR_ID_ELO, 0x0009) },
---
base-commit: 79b95d74470dd97d7d0908d5a3c0734a23e51aa4
change-id: 20260123-hid-elecom-huge-plus-fix-545cf029fd1a
Best regards,
--
David Phillips <david@profile.sh>
^ permalink raw reply related
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