Linux Input/HID development
 help / color / mirror / Atom feed
* [PATCH v6 12/19] HID: hid-lenovo-go-s: Add Feature Status Attributes
From: Derek J. Clark @ 2026-03-10  7:29 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Richard Hughes, Mario Limonciello, Zhixin Zhang, Mia Shao,
	Mark Pearson, Pierre-Loup A . Griffais, Derek J . Clark,
	linux-input, linux-doc, linux-kernel, Ethan Tidmore
In-Reply-To: <20260310072937.3295875-1-derekjohn.clark@gmail.com>

Adds features status attributes for the gamepad, MCU, touchpad/mouse,
and IMU devices.

Reviewed-by: Mark Pearson <mpearson-lenovo@squebb.ca>
Co-developed-by: Ethan Tidmore <ethantidmore06@gmail.com>
Signed-off-by: Ethan Tidmore <ethantidmore06@gmail.com>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v6:
  - Include positive promotion bug fix from Ethan Tidmore.
  - Include impossible condition bug fix from Ethan Tidmore.
  - Make local attributes static.
  - Use NULL instead of 0 in mcu_propery_out when there is no data.
v4:
  - Cleaner formatting on debug message.
---
 drivers/hid/hid-lenovo-go-s.c | 484 +++++++++++++++++++++++++++++++++-
 1 file changed, 483 insertions(+), 1 deletion(-)

diff --git a/drivers/hid/hid-lenovo-go-s.c b/drivers/hid/hid-lenovo-go-s.c
index 8ee75f724b5b..97c572cfe66c 100644
--- a/drivers/hid/hid-lenovo-go-s.c
+++ b/drivers/hid/hid-lenovo-go-s.c
@@ -15,6 +15,7 @@
 #include <linux/device.h>
 #include <linux/hid.h>
 #include <linux/jiffies.h>
+#include <linux/kstrtox.h>
 #include <linux/mutex.h>
 #include <linux/printk.h>
 #include <linux/string.h>
@@ -35,7 +36,17 @@ static struct hid_gos_cfg {
 	struct completion send_cmd_complete;
 	struct hid_device *hdev;
 	struct mutex cfg_mutex; /*ensure single synchronous output report*/
+	u8 gp_auto_sleep_time;
+	u8 gp_dpad_mode;
+	u8 gp_mode;
+	u8 gp_poll_rate;
+	u8 imu_bypass_en;
+	u8 imu_sensor_en;
 	u8 mcu_id[12];
+	u8 mouse_step;
+	u8 os_mode;
+	u8 rgb_en;
+	u8 tp_en;
 } drvdata;
 
 struct gos_cfg_attr {
@@ -66,7 +77,73 @@ enum mcu_command_index {
 	GET_PL_TEST = 0xdf,
 };
 
-#define FEATURE_NONE 0x00
+enum feature_enabled_index {
+	FEATURE_DISABLED,
+	FEATURE_ENABLED,
+};
+
+static const char *const feature_enabled_text[] = {
+	[FEATURE_DISABLED] = "false",
+	[FEATURE_ENABLED] = "true",
+};
+
+enum feature_status_index {
+	FEATURE_NONE = 0x00,
+	FEATURE_GAMEPAD_MODE = 0x01,
+	FEATURE_AUTO_SLEEP_TIME = 0x04,
+	FEATURE_IMU_BYPASS,
+	FEATURE_RGB_ENABLE,
+	FEATURE_IMU_ENABLE,
+	FEATURE_TOUCHPAD_ENABLE,
+	FEATURE_OS_MODE = 0x0A,
+	FEATURE_POLL_RATE = 0x10,
+	FEATURE_DPAD_MODE,
+	FEATURE_MOUSE_WHEEL_STEP,
+};
+
+enum gamepad_mode_index {
+	XINPUT,
+	DINPUT,
+};
+
+static const char *const gamepad_mode_text[] = {
+	[XINPUT] = "xinput",
+	[DINPUT] = "dinput",
+};
+
+enum os_type_index {
+	WINDOWS,
+	LINUX,
+};
+
+static const char *const os_type_text[] = {
+	[WINDOWS] = "windows",
+	[LINUX] = "linux",
+};
+
+enum poll_rate_index {
+	HZ125,
+	HZ250,
+	HZ500,
+	HZ1000,
+};
+
+static const char *const poll_rate_text[] = {
+	[HZ125] = "125",
+	[HZ250] = "250",
+	[HZ500] = "500",
+	[HZ1000] = "1000",
+};
+
+enum dpad_mode_index {
+	DIR8,
+	DIR4,
+};
+
+static const char *const dpad_mode_text[] = {
+	[DIR8] = "8-way",
+	[DIR4] = "4-way",
+};
 
 static int hid_gos_version_event(u8 *data)
 {
@@ -84,6 +161,57 @@ static int hid_gos_mcu_id_event(struct command_report *cmd_rep)
 	return 0;
 }
 
+static int hid_gos_gamepad_cfg_event(struct command_report *cmd_rep)
+{
+	int ret = 0;
+
+	switch (cmd_rep->sub_cmd) {
+	case FEATURE_GAMEPAD_MODE:
+		drvdata.gp_mode = cmd_rep->data[0];
+		break;
+	case FEATURE_AUTO_SLEEP_TIME:
+		drvdata.gp_auto_sleep_time = cmd_rep->data[0];
+		break;
+	case FEATURE_IMU_BYPASS:
+		drvdata.imu_bypass_en = cmd_rep->data[0];
+		break;
+	case FEATURE_RGB_ENABLE:
+		drvdata.rgb_en = cmd_rep->data[0];
+		break;
+	case FEATURE_IMU_ENABLE:
+		drvdata.imu_sensor_en = cmd_rep->data[0];
+		break;
+	case FEATURE_TOUCHPAD_ENABLE:
+		drvdata.tp_en = cmd_rep->data[0];
+		break;
+	case FEATURE_OS_MODE:
+		drvdata.os_mode = cmd_rep->data[0];
+		break;
+	case FEATURE_POLL_RATE:
+		drvdata.gp_poll_rate = cmd_rep->data[0];
+		break;
+	case FEATURE_DPAD_MODE:
+		drvdata.gp_dpad_mode = cmd_rep->data[0];
+		break;
+	case FEATURE_MOUSE_WHEEL_STEP:
+		drvdata.mouse_step = cmd_rep->data[0];
+		break;
+	default:
+		ret = -EINVAL;
+		break;
+	}
+
+	return ret;
+}
+
+static int hid_gos_set_event_return(struct command_report *cmd_rep)
+{
+	if (cmd_rep->data[0] != 0)
+		return -EIO;
+
+	return 0;
+}
+
 static int get_endpoint_address(struct hid_device *hdev)
 {
 	struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
@@ -120,6 +248,12 @@ static int hid_gos_raw_event(struct hid_device *hdev, struct hid_report *report,
 	case GET_MCU_ID:
 		ret = hid_gos_mcu_id_event(cmd_rep);
 		break;
+	case GET_GAMEPAD_CFG:
+		ret = hid_gos_gamepad_cfg_event(cmd_rep);
+		break;
+	case SET_GAMEPAD_CFG:
+		ret = hid_gos_set_event_return(cmd_rep);
+		break;
 	default:
 		ret = -EINVAL;
 		break;
@@ -174,17 +308,329 @@ static int mcu_property_out(struct hid_device *hdev, u8 command, u8 index,
 	return 0;
 }
 
+static ssize_t gamepad_property_store(struct device *dev,
+				      struct device_attribute *attr,
+				      const char *buf, size_t count,
+				      enum feature_status_index index)
+{
+	size_t size = 1;
+	u8 val = 0;
+	int ret;
+
+	switch (index) {
+	case FEATURE_GAMEPAD_MODE:
+		ret = sysfs_match_string(gamepad_mode_text, buf);
+		if (ret < 0)
+			return ret;
+		val = ret;
+		break;
+	case FEATURE_AUTO_SLEEP_TIME:
+		ret = kstrtou8(buf, 10, &val);
+		if (ret)
+			return ret;
+		break;
+	case FEATURE_IMU_ENABLE:
+		ret = sysfs_match_string(feature_enabled_text, buf);
+		if (ret < 0)
+			return ret;
+		val = ret;
+		break;
+	case FEATURE_IMU_BYPASS:
+		ret = sysfs_match_string(feature_enabled_text, buf);
+		if (ret < 0)
+			return ret;
+		val = ret;
+		break;
+	case FEATURE_RGB_ENABLE:
+		ret = sysfs_match_string(feature_enabled_text, buf);
+		if (ret < 0)
+			return ret;
+		val = ret;
+		break;
+	case FEATURE_TOUCHPAD_ENABLE:
+		ret = sysfs_match_string(feature_enabled_text, buf);
+		if (ret < 0)
+			return ret;
+		val = ret;
+		break;
+	case FEATURE_OS_MODE:
+		ret = sysfs_match_string(os_type_text, buf);
+		if (ret < 0)
+			return ret;
+		val = ret;
+		break;
+	case FEATURE_POLL_RATE:
+		ret = sysfs_match_string(poll_rate_text, buf);
+		if (ret < 0)
+			return ret;
+		val = ret;
+		break;
+	case FEATURE_DPAD_MODE:
+		ret = sysfs_match_string(dpad_mode_text, buf);
+		if (ret < 0)
+			return ret;
+		val = ret;
+		break;
+	case FEATURE_MOUSE_WHEEL_STEP:
+		ret = kstrtou8(buf, 10, &val);
+		if (ret)
+			return ret;
+		if (val < 1 || val > 127)
+			return -EINVAL;
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	if (!val)
+		size = 0;
+
+	ret = mcu_property_out(drvdata.hdev, SET_GAMEPAD_CFG, index, &val,
+			       size);
+	if (ret < 0)
+		return ret;
+
+	return count;
+}
+
+static ssize_t gamepad_property_show(struct device *dev,
+				     struct device_attribute *attr, char *buf,
+				     enum feature_status_index index)
+{
+	ssize_t count = 0;
+	u8 i;
+
+	count = mcu_property_out(drvdata.hdev, GET_GAMEPAD_CFG, index, NULL, 0);
+	if (count < 0)
+		return count;
+
+	switch (index) {
+	case FEATURE_GAMEPAD_MODE:
+		i = drvdata.gp_mode;
+		if (i >= ARRAY_SIZE(gamepad_mode_text))
+			return -EINVAL;
+		count = sysfs_emit(buf, "%s\n", gamepad_mode_text[i]);
+		break;
+	case FEATURE_AUTO_SLEEP_TIME:
+		count = sysfs_emit(buf, "%u\n", drvdata.gp_auto_sleep_time);
+		break;
+	case FEATURE_IMU_ENABLE:
+		i = drvdata.imu_sensor_en;
+		if (i >= ARRAY_SIZE(feature_enabled_text))
+			return -EINVAL;
+		count = sysfs_emit(buf, "%s\n", feature_enabled_text[i]);
+		break;
+	case FEATURE_IMU_BYPASS:
+		i = drvdata.imu_bypass_en;
+		if (i >= ARRAY_SIZE(feature_enabled_text))
+			return -EINVAL;
+		count = sysfs_emit(buf, "%s\n", feature_enabled_text[i]);
+		break;
+	case FEATURE_RGB_ENABLE:
+		i = drvdata.rgb_en;
+		if (i >= ARRAY_SIZE(feature_enabled_text))
+			return -EINVAL;
+		count = sysfs_emit(buf, "%s\n", feature_enabled_text[i]);
+		break;
+	case FEATURE_TOUCHPAD_ENABLE:
+		i = drvdata.tp_en;
+		if (i >= ARRAY_SIZE(feature_enabled_text))
+			return -EINVAL;
+		count = sysfs_emit(buf, "%s\n", feature_enabled_text[i]);
+		break;
+	case FEATURE_OS_MODE:
+		i = drvdata.os_mode;
+		if (i >= ARRAY_SIZE(os_type_text))
+			return -EINVAL;
+		count = sysfs_emit(buf, "%s\n", os_type_text[i]);
+		break;
+	case FEATURE_POLL_RATE:
+		i = drvdata.gp_poll_rate;
+		if (i >= ARRAY_SIZE(poll_rate_text))
+			return -EINVAL;
+		count = sysfs_emit(buf, "%s\n", poll_rate_text[i]);
+		break;
+	case FEATURE_DPAD_MODE:
+		i = drvdata.gp_dpad_mode;
+		if (i >= ARRAY_SIZE(dpad_mode_text))
+			return -EINVAL;
+		count = sysfs_emit(buf, "%s\n", dpad_mode_text[i]);
+		break;
+	case FEATURE_MOUSE_WHEEL_STEP:
+		i = drvdata.mouse_step;
+		if (i < 1 || i > 127)
+			return -EINVAL;
+		count = sysfs_emit(buf, "%u\n", i);
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	return count;
+}
+
+static ssize_t gamepad_property_options(struct device *dev,
+					struct device_attribute *attr,
+					char *buf,
+					enum feature_status_index index)
+{
+	size_t count = 0;
+	unsigned int i;
+
+	switch (index) {
+	case FEATURE_GAMEPAD_MODE:
+		for (i = 0; i < ARRAY_SIZE(gamepad_mode_text); i++) {
+			count += sysfs_emit_at(buf, count, "%s ",
+					       gamepad_mode_text[i]);
+		}
+		break;
+	case FEATURE_AUTO_SLEEP_TIME:
+		return sysfs_emit(buf, "0-255\n");
+	case FEATURE_IMU_ENABLE:
+		for (i = 0; i < ARRAY_SIZE(feature_enabled_text); i++) {
+			count += sysfs_emit_at(buf, count, "%s ",
+					       feature_enabled_text[i]);
+		}
+		break;
+	case FEATURE_IMU_BYPASS:
+	case FEATURE_RGB_ENABLE:
+	case FEATURE_TOUCHPAD_ENABLE:
+		for (i = 0; i < ARRAY_SIZE(feature_enabled_text); i++) {
+			count += sysfs_emit_at(buf, count, "%s ",
+					       feature_enabled_text[i]);
+		}
+		break;
+	case FEATURE_OS_MODE:
+		for (i = 0; i < ARRAY_SIZE(os_type_text); i++) {
+			count += sysfs_emit_at(buf, count, "%s ",
+					       os_type_text[i]);
+		}
+		break;
+	case FEATURE_POLL_RATE:
+		for (i = 0; i < ARRAY_SIZE(poll_rate_text); i++) {
+			count += sysfs_emit_at(buf, count, "%s ",
+					       poll_rate_text[i]);
+		}
+		break;
+	case FEATURE_DPAD_MODE:
+		for (i = 0; i < ARRAY_SIZE(dpad_mode_text); i++) {
+			count += sysfs_emit_at(buf, count, "%s ",
+					       dpad_mode_text[i]);
+		}
+		break;
+	case FEATURE_MOUSE_WHEEL_STEP:
+		return sysfs_emit(buf, "1-127\n");
+	default:
+		return count;
+	}
+
+	if (count)
+		buf[count - 1] = '\n';
+
+	return count;
+}
+
 static ssize_t mcu_id_show(struct device *dev, struct device_attribute *attr,
 			   char *buf)
 {
 	return sysfs_emit(buf, "%*phN\n", 12, &drvdata.mcu_id);
 }
 
+#define LEGOS_DEVICE_ATTR_RW(_name, _attrname, _rtype, _group)                 \
+	static ssize_t _name##_store(struct device *dev,                       \
+				     struct device_attribute *attr,            \
+				     const char *buf, size_t count)            \
+	{                                                                      \
+		return _group##_property_store(dev, attr, buf, count,          \
+					       _name.index);                   \
+	}                                                                      \
+	static ssize_t _name##_show(struct device *dev,                        \
+				    struct device_attribute *attr, char *buf)  \
+	{                                                                      \
+		return _group##_property_show(dev, attr, buf, _name.index);    \
+	}                                                                      \
+	static ssize_t _name##_##_rtype##_show(                                \
+		struct device *dev, struct device_attribute *attr, char *buf)  \
+	{                                                                      \
+		return _group##_property_options(dev, attr, buf, _name.index); \
+	}                                                                      \
+	static DEVICE_ATTR_RW_NAMED(_name, _attrname)
+
+#define LEGOS_DEVICE_ATTR_RO(_name, _attrname, _group)                        \
+	static ssize_t _name##_show(struct device *dev,                       \
+				    struct device_attribute *attr, char *buf) \
+	{                                                                     \
+		return _group##_property_show(dev, attr, buf, _name.index);   \
+	}                                                                     \
+	static DEVICE_ATTR_RO_NAMED(_name, _attrname)
+
+/* Gamepad */
+static struct gos_cfg_attr auto_sleep_time = { FEATURE_AUTO_SLEEP_TIME };
+LEGOS_DEVICE_ATTR_RW(auto_sleep_time, "auto_sleep_time", range, gamepad);
+static DEVICE_ATTR_RO(auto_sleep_time_range);
+
+static struct gos_cfg_attr dpad_mode = { FEATURE_DPAD_MODE };
+LEGOS_DEVICE_ATTR_RW(dpad_mode, "dpad_mode", index, gamepad);
+static DEVICE_ATTR_RO(dpad_mode_index);
+
+static struct gos_cfg_attr gamepad_mode = { FEATURE_GAMEPAD_MODE };
+LEGOS_DEVICE_ATTR_RW(gamepad_mode, "mode", index, gamepad);
+static DEVICE_ATTR_RO_NAMED(gamepad_mode_index, "mode_index");
+
+static struct gos_cfg_attr gamepad_poll_rate = { FEATURE_POLL_RATE };
+LEGOS_DEVICE_ATTR_RW(gamepad_poll_rate, "poll_rate", index, gamepad);
+static DEVICE_ATTR_RO_NAMED(gamepad_poll_rate_index, "poll_rate_index");
+
+static struct attribute *legos_gamepad_attrs[] = {
+	&dev_attr_auto_sleep_time.attr,
+	&dev_attr_auto_sleep_time_range.attr,
+	&dev_attr_dpad_mode.attr,
+	&dev_attr_dpad_mode_index.attr,
+	&dev_attr_gamepad_mode.attr,
+	&dev_attr_gamepad_mode_index.attr,
+	&dev_attr_gamepad_poll_rate.attr,
+	&dev_attr_gamepad_poll_rate_index.attr,
+	NULL,
+};
+
+static const struct attribute_group gamepad_attr_group = {
+	.name = "gamepad",
+	.attrs = legos_gamepad_attrs,
+};
+
+/* IMU */
+static struct gos_cfg_attr imu_bypass_enabled = { FEATURE_IMU_BYPASS };
+LEGOS_DEVICE_ATTR_RW(imu_bypass_enabled, "bypass_enabled", index, gamepad);
+static DEVICE_ATTR_RO_NAMED(imu_bypass_enabled_index, "bypass_enabled_index");
+
+static struct gos_cfg_attr imu_sensor_enabled = { FEATURE_IMU_ENABLE };
+LEGOS_DEVICE_ATTR_RW(imu_sensor_enabled, "sensor_enabled", index, gamepad);
+static DEVICE_ATTR_RO_NAMED(imu_sensor_enabled_index, "sensor_enabled_index");
+
+static struct attribute *legos_imu_attrs[] = {
+	&dev_attr_imu_bypass_enabled.attr,
+	&dev_attr_imu_bypass_enabled_index.attr,
+	&dev_attr_imu_sensor_enabled.attr,
+	&dev_attr_imu_sensor_enabled_index.attr,
+	NULL,
+};
+
+static const struct attribute_group imu_attr_group = {
+	.name = "imu",
+	.attrs = legos_imu_attrs,
+};
+
 /* MCU */
 static DEVICE_ATTR_RO(mcu_id);
 
+static struct gos_cfg_attr os_mode = { FEATURE_OS_MODE };
+LEGOS_DEVICE_ATTR_RW(os_mode, "os_mode", index, gamepad);
+static DEVICE_ATTR_RO(os_mode_index);
+
 static struct attribute *legos_mcu_attrs[] = {
 	&dev_attr_mcu_id.attr,
+	&dev_attr_os_mode.attr,
+	&dev_attr_os_mode_index.attr,
 	NULL,
 };
 
@@ -192,8 +638,44 @@ static const struct attribute_group mcu_attr_group = {
 	.attrs = legos_mcu_attrs,
 };
 
+/* Mouse */
+static struct gos_cfg_attr mouse_wheel_step = { FEATURE_MOUSE_WHEEL_STEP };
+LEGOS_DEVICE_ATTR_RW(mouse_wheel_step, "step", range, gamepad);
+static DEVICE_ATTR_RO_NAMED(mouse_wheel_step_range, "step_range");
+
+static struct attribute *legos_mouse_attrs[] = {
+	&dev_attr_mouse_wheel_step.attr,
+	&dev_attr_mouse_wheel_step_range.attr,
+	NULL,
+};
+
+static const struct attribute_group mouse_attr_group = {
+	.name = "mouse",
+	.attrs = legos_mouse_attrs,
+};
+
+/* Touchpad */
+static struct gos_cfg_attr touchpad_enabled = { FEATURE_TOUCHPAD_ENABLE };
+LEGOS_DEVICE_ATTR_RW(touchpad_enabled, "enabled", index, gamepad);
+static DEVICE_ATTR_RO_NAMED(touchpad_enabled_index, "enabled_index");
+
+static struct attribute *legos_touchpad_attrs[] = {
+	&dev_attr_touchpad_enabled.attr,
+	&dev_attr_touchpad_enabled_index.attr,
+	NULL,
+};
+
+static const struct attribute_group touchpad_attr_group = {
+	.name = "touchpad",
+	.attrs = legos_touchpad_attrs,
+};
+
 static const struct attribute_group *top_level_attr_groups[] = {
+	&gamepad_attr_group,
+	&imu_attr_group,
 	&mcu_attr_group,
+	&mouse_attr_group,
+	&touchpad_attr_group,
 	NULL,
 };
 
-- 
2.53.0


^ permalink raw reply related

* [PATCH v6 13/19] HID: hid-lenovo-go-s: Add Touchpad Mode Attributes
From: Derek J. Clark @ 2026-03-10  7:29 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Richard Hughes, Mario Limonciello, Zhixin Zhang, Mia Shao,
	Mark Pearson, Pierre-Loup A . Griffais, Derek J . Clark,
	linux-input, linux-doc, linux-kernel
In-Reply-To: <20260310072937.3295875-1-derekjohn.clark@gmail.com>

Adds attributes for managing the touchpad operating modes.

Reviewed-by: Mark Pearson <mpearson-lenovo@squebb.ca>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v6:
  - Make local attributes static.
  - Use NULL instead of 0 in mcu_propery_out when there is no data.
---
 drivers/hid/hid-lenovo-go-s.c | 142 ++++++++++++++++++++++++++++++++++
 1 file changed, 142 insertions(+)

diff --git a/drivers/hid/hid-lenovo-go-s.c b/drivers/hid/hid-lenovo-go-s.c
index 97c572cfe66c..5899cabe950f 100644
--- a/drivers/hid/hid-lenovo-go-s.c
+++ b/drivers/hid/hid-lenovo-go-s.c
@@ -47,6 +47,8 @@ static struct hid_gos_cfg {
 	u8 os_mode;
 	u8 rgb_en;
 	u8 tp_en;
+	u8 tp_linux_mode;
+	u8 tp_windows_mode;
 } drvdata;
 
 struct gos_cfg_attr {
@@ -145,6 +147,22 @@ static const char *const dpad_mode_text[] = {
 	[DIR4] = "4-way",
 };
 
+enum touchpad_mode_index {
+	TP_REL,
+	TP_ABS,
+};
+
+static const char *const touchpad_mode_text[] = {
+	[TP_REL] = "relative",
+	[TP_ABS] = "absolute",
+};
+
+enum touchpad_config_index {
+	CFG_WINDOWS_MODE = 0x03,
+	CFG_LINUX_MODE,
+
+};
+
 static int hid_gos_version_event(u8 *data)
 {
 	struct version_report *ver_rep = (struct version_report *)data;
@@ -204,6 +222,25 @@ static int hid_gos_gamepad_cfg_event(struct command_report *cmd_rep)
 	return ret;
 }
 
+static int hid_gos_touchpad_event(struct command_report *cmd_rep)
+{
+	int ret = 0;
+
+	switch (cmd_rep->sub_cmd) {
+	case CFG_LINUX_MODE:
+		drvdata.tp_linux_mode = cmd_rep->data[0];
+		break;
+	case CFG_WINDOWS_MODE:
+		drvdata.tp_windows_mode = cmd_rep->data[0];
+		break;
+	default:
+		ret = -EINVAL;
+		break;
+	}
+
+	return ret;
+}
+
 static int hid_gos_set_event_return(struct command_report *cmd_rep)
 {
 	if (cmd_rep->data[0] != 0)
@@ -251,7 +288,11 @@ static int hid_gos_raw_event(struct hid_device *hdev, struct hid_report *report,
 	case GET_GAMEPAD_CFG:
 		ret = hid_gos_gamepad_cfg_event(cmd_rep);
 		break;
+	case GET_TP_PARAM:
+		ret = hid_gos_touchpad_event(cmd_rep);
+		break;
 	case SET_GAMEPAD_CFG:
+	case SET_TP_PARAM:
 		ret = hid_gos_set_event_return(cmd_rep);
 		break;
 	default:
@@ -530,6 +571,95 @@ static ssize_t gamepad_property_options(struct device *dev,
 	return count;
 }
 
+static ssize_t touchpad_property_store(struct device *dev,
+				       struct device_attribute *attr,
+				       const char *buf, size_t count,
+				       enum touchpad_config_index index)
+{
+	size_t size = 1;
+	u8 val = 0;
+	int ret;
+
+	switch (index) {
+	case CFG_WINDOWS_MODE:
+		ret = sysfs_match_string(touchpad_mode_text, buf);
+		if (ret < 0)
+			return ret;
+		val = ret;
+		break;
+	case CFG_LINUX_MODE:
+		ret = sysfs_match_string(touchpad_mode_text, buf);
+		if (ret < 0)
+			return ret;
+		val = ret;
+		break;
+	default:
+		return -EINVAL;
+	}
+	if (!val)
+		size = 0;
+
+	ret = mcu_property_out(drvdata.hdev, SET_TP_PARAM, index, &val, size);
+	if (ret < 0)
+		return ret;
+
+	return count;
+}
+
+static ssize_t touchpad_property_show(struct device *dev,
+				      struct device_attribute *attr, char *buf,
+				      enum touchpad_config_index index)
+{
+	int ret = 0;
+	u8 i;
+
+	ret = mcu_property_out(drvdata.hdev, GET_TP_PARAM, index, NULL, 0);
+	if (ret < 0)
+		return ret;
+
+	switch (index) {
+	case CFG_WINDOWS_MODE:
+		i = drvdata.tp_windows_mode;
+		break;
+	case CFG_LINUX_MODE:
+		i = drvdata.tp_linux_mode;
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	if (i >= ARRAY_SIZE(touchpad_mode_text))
+		return -EINVAL;
+
+	return sysfs_emit(buf, "%s\n", touchpad_mode_text[i]);
+}
+
+static ssize_t touchpad_property_options(struct device *dev,
+					 struct device_attribute *attr,
+					 char *buf,
+					 enum touchpad_config_index index)
+{
+	size_t count = 0;
+	unsigned int i;
+
+	switch (index) {
+	case CFG_WINDOWS_MODE:
+	case CFG_LINUX_MODE:
+		for (i = 0; i < ARRAY_SIZE(touchpad_mode_text); i++) {
+			count += sysfs_emit_at(buf, count, "%s ",
+					       touchpad_mode_text[i]);
+		}
+		break;
+	default:
+		return count;
+	}
+
+	if (count)
+		buf[count - 1] = '\n';
+
+	return count;
+}
+
 static ssize_t mcu_id_show(struct device *dev, struct device_attribute *attr,
 			   char *buf)
 {
@@ -659,9 +789,21 @@ static struct gos_cfg_attr touchpad_enabled = { FEATURE_TOUCHPAD_ENABLE };
 LEGOS_DEVICE_ATTR_RW(touchpad_enabled, "enabled", index, gamepad);
 static DEVICE_ATTR_RO_NAMED(touchpad_enabled_index, "enabled_index");
 
+static struct gos_cfg_attr touchpad_linux_mode = { CFG_LINUX_MODE };
+LEGOS_DEVICE_ATTR_RW(touchpad_linux_mode, "linux_mode", index, touchpad);
+static DEVICE_ATTR_RO_NAMED(touchpad_linux_mode_index, "linux_mode_index");
+
+static struct gos_cfg_attr touchpad_windows_mode = { CFG_WINDOWS_MODE };
+LEGOS_DEVICE_ATTR_RW(touchpad_windows_mode, "windows_mode", index, touchpad);
+static DEVICE_ATTR_RO_NAMED(touchpad_windows_mode_index, "windows_mode_index");
+
 static struct attribute *legos_touchpad_attrs[] = {
 	&dev_attr_touchpad_enabled.attr,
 	&dev_attr_touchpad_enabled_index.attr,
+	&dev_attr_touchpad_linux_mode.attr,
+	&dev_attr_touchpad_linux_mode_index.attr,
+	&dev_attr_touchpad_windows_mode.attr,
+	&dev_attr_touchpad_windows_mode_index.attr,
 	NULL,
 };
 
-- 
2.53.0


^ permalink raw reply related

* [PATCH v6 11/19] HID: hid-lenovo-go-s: Add MCU ID Attribute
From: Derek J. Clark @ 2026-03-10  7:29 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Richard Hughes, Mario Limonciello, Zhixin Zhang, Mia Shao,
	Mark Pearson, Pierre-Loup A . Griffais, Derek J . Clark,
	linux-input, linux-doc, linux-kernel
In-Reply-To: <20260310072937.3295875-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.

Reviewed-by: Mark Pearson <mpearson-lenovo@squebb.ca>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v6:
  - Use NULL instead of 0 in mcu_propery_out when there is no data.
---
 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 c9f57dfa145a..8ee75f724b5b 100644
--- a/drivers/hid/hid-lenovo-go-s.c
+++ b/drivers/hid/hid-lenovo-go-s.c
@@ -18,6 +18,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 @@ static 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 int 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;
@@ -157,10 +174,41 @@ static int mcu_property_out(struct hid_device *hdev, u8 command, u8 index,
 	return 0;
 }
 
+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, NULL, 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, NULL, 0);
 	if (ret) {
 		dev_err(&drvdata.hdev->dev, "Failed to retrieve MCU Version: %i\n", ret);
@@ -177,6 +225,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
@@ -196,6 +251,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.53.0


^ permalink raw reply related

* [PATCH v6 10/19] HID: hid-lenovo-go-s: Add Lenovo Legion Go S Series HID Driver
From: Derek J. Clark @ 2026-03-10  7:29 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Richard Hughes, Mario Limonciello, Zhixin Zhang, Mia Shao,
	Mark Pearson, Pierre-Loup A . Griffais, Derek J . Clark,
	linux-input, linux-doc, linux-kernel, Ethan Tidmore
In-Reply-To: <20260310072937.3295875-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.

Reviewed-by: Mark Pearson <mpearson-lenovo@squebb.ca>
Co-developed-by: Mario Limonciello <mario.limonciello@amd.com>
Signed-off-by: Mario Limonciello <mario.limonciello@amd.com>
Co-developed-by: Ethan Tidmore <ethantidmore06@gmail.com>
Signed-off-by: Ethan Tidmore <ethantidmore06@gmail.com>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v6:
  - Include signdedness bug fix by Ethan Tidmore.
  - Make local attributes static.
  - Use NULL instead of 0 in mcu_propery_out when there is no data.
v4:
  - Use dmabuf per request instead of devm allocated static buffer.
    Resolves bug with side effects during suspend.
  - Remove unnecessary HID quirks and return to HID_CONNECT_HIDRAW.
  - Adjust delayed work time to 5ms to fix some side effects during
    resume when the MCU disconnects in some circumstances.
  - Cleaner formatting on multiple debug messages.
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 | 278 ++++++++++++++++++++++++++++++++++
 5 files changed, 296 insertions(+)
 create mode 100644 drivers/hid/hid-lenovo-go-s.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 75d89590f3d2..c81f10292ff7 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -14420,6 +14420,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 2925dba429f5..10c12d8e6557 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -635,6 +635,18 @@ config HID_LENOVO_GO
 	and Legion Go 2 Handheld Console Controllers. Say M here to compile this
 	driver as a module. The module will be called hid-lenovo-go.
 
+config HID_LENOVO_GO_S
+	tristate "HID Driver for Lenovo Legion Go S Controller"
+	depends on USB_HID
+	select LEDS_CLASS
+	select LEDS_CLASS_MULTICOLOR
+	help
+	Support for Lenovo Legion Go S Handheld Console Controller.
+
+	Say Y here to include configuration interface support for the Lenovo Legion Go
+	S. Say M here to compile this driver as a module. The module will be called
+	hid-lenovo-go-s.
+
 config HID_LETSKETCH
 	tristate "Letsketch WP9620N tablets"
 	depends on USB_HID
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index 79fbe4e3e2f4..07dfdb6a49c5 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 093ee86ebf90..145eb9921fee 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -739,6 +739,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..c9f57dfa145a
--- /dev/null
+++ b/drivers/hid/hid-lenovo-go-s.c
@@ -0,0 +1,278 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ *  HID driver for Lenovo Legion Go S devices.
+ *
+ *  Copyright (c) 2026 Derek J. Clark <derekjohn.clark@gmail.com>
+ *  Copyright (c) 2026 Valve Corporation
+ */
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/array_size.h>
+#include <linux/cleanup.h>
+#include <linux/completion.h>
+#include <linux/delay.h>
+#include <linux/dev_printk.h>
+#include <linux/device.h>
+#include <linux/hid.h>
+#include <linux/jiffies.h>
+#include <linux/mutex.h>
+#include <linux/printk.h>
+#include <linux/string.h>
+#include <linux/types.h>
+#include <linux/unaligned.h>
+#include <linux/usb.h>
+#include <linux/workqueue.h>
+#include <linux/workqueue_types.h>
+
+#include "hid-ids.h"
+
+#define GO_S_CFG_INTF_IN	0x84
+#define GO_S_PACKET_SIZE	64
+
+static struct hid_gos_cfg {
+	struct delayed_work gos_cfg_setup;
+	struct completion send_cmd_complete;
+	struct hid_device *hdev;
+	struct mutex cfg_mutex; /*ensure single synchronous output report*/
+} drvdata;
+
+struct command_report {
+	u8 cmd;
+	u8 sub_cmd;
+	u8 data[63];
+} __packed;
+
+struct version_report {
+	u8 cmd;
+	u32 version;
+	u8 reserved[59];
+} __packed;
+
+enum mcu_command_index {
+	GET_VERSION = 0x01,
+	GET_MCU_ID,
+	GET_GAMEPAD_CFG,
+	SET_GAMEPAD_CFG,
+	GET_TP_PARAM,
+	SET_TP_PARAM,
+	GET_RGB_CFG = 0x0f,
+	SET_RGB_CFG,
+	GET_PL_TEST = 0xdf,
+};
+
+#define FEATURE_NONE 0x00
+
+static int hid_gos_version_event(u8 *data)
+{
+	struct version_report *ver_rep = (struct version_report *)data;
+
+	drvdata.hdev->firmware_version = get_unaligned_le32(&ver_rep->version);
+	return 0;
+}
+
+static int get_endpoint_address(struct hid_device *hdev)
+{
+	struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
+	struct usb_host_endpoint *ep;
+
+	if (intf) {
+		ep = intf->cur_altsetting->endpoint;
+		if (ep)
+			return ep->desc.bEndpointAddress;
+	}
+
+	return -ENODEV;
+}
+
+static int hid_gos_raw_event(struct hid_device *hdev, struct hid_report *report,
+			     u8 *data, int size)
+{
+	struct command_report *cmd_rep;
+	int ep, ret;
+
+	ep = get_endpoint_address(hdev);
+	if (ep != GO_S_CFG_INTF_IN)
+		return 0;
+
+	if (size != GO_S_PACKET_SIZE)
+		return -EINVAL;
+
+	cmd_rep = (struct command_report *)data;
+
+	switch (cmd_rep->cmd) {
+	case GET_VERSION:
+		ret = hid_gos_version_event(data);
+		break;
+	default:
+		ret = -EINVAL;
+		break;
+	}
+	dev_dbg(&hdev->dev, "Rx data as raw input report: [%*ph]\n",
+		GO_S_PACKET_SIZE, data);
+
+	complete(&drvdata.send_cmd_complete);
+	return ret;
+}
+
+static int mcu_property_out(struct hid_device *hdev, u8 command, u8 index,
+			    u8 *data, size_t len)
+{
+	unsigned char *dmabuf __free(kfree) = NULL;
+	u8 header[] = { command, index };
+	size_t header_size = ARRAY_SIZE(header);
+	int timeout, ret;
+
+	if (header_size + len > GO_S_PACKET_SIZE)
+		return -EINVAL;
+
+	guard(mutex)(&drvdata.cfg_mutex);
+	/* We can't use a devm_alloc reusable buffer without side effects during suspend */
+	dmabuf = kzalloc(GO_S_PACKET_SIZE, GFP_KERNEL);
+	if (!dmabuf)
+		return -ENOMEM;
+
+	memcpy(dmabuf, header, header_size);
+	memcpy(dmabuf + header_size, data, len);
+
+	dev_dbg(&hdev->dev, "Send data as raw output report: [%*ph]\n",
+		GO_S_PACKET_SIZE, dmabuf);
+
+	ret = hid_hw_output_report(hdev, dmabuf, GO_S_PACKET_SIZE);
+	if (ret < 0)
+		return ret;
+
+	ret = ret == GO_S_PACKET_SIZE ? 0 : -EINVAL;
+	if (ret)
+		return ret;
+
+	/* PL_TEST commands can take longer because they go out to another device */
+	timeout = (command == GET_PL_TEST) ? 200 : 5;
+	ret = wait_for_completion_interruptible_timeout(&drvdata.send_cmd_complete,
+							msecs_to_jiffies(timeout));
+
+	if (ret == 0) /* timeout occurred */
+		ret = -EBUSY;
+
+	reinit_completion(&drvdata.send_cmd_complete);
+	return 0;
+}
+
+static void cfg_setup(struct work_struct *work)
+{
+	int ret;
+
+	ret = mcu_property_out(drvdata.hdev, GET_VERSION, FEATURE_NONE, NULL, 0);
+	if (ret) {
+		dev_err(&drvdata.hdev->dev, "Failed to retrieve MCU Version: %i\n", ret);
+		return;
+	}
+}
+
+static int hid_gos_cfg_probe(struct hid_device *hdev,
+			     const struct hid_device_id *_id)
+{
+	int ret;
+
+	hid_set_drvdata(hdev, &drvdata);
+	drvdata.hdev = hdev;
+	mutex_init(&drvdata.cfg_mutex);
+
+	init_completion(&drvdata.send_cmd_complete);
+
+	/* Executing calls prior to returning from probe will lock the MCU. Schedule
+	 * initial data call after probe has completed and MCU can accept calls.
+	 */
+	INIT_DELAYED_WORK(&drvdata.gos_cfg_setup, &cfg_setup);
+	ret = schedule_delayed_work(&drvdata.gos_cfg_setup, msecs_to_jiffies(2));
+	if (!ret) {
+		dev_err(&hdev->dev, "Failed to schedule startup delayed work\n");
+		return -ENODEV;
+	}
+
+	return 0;
+}
+
+static void hid_gos_cfg_remove(struct hid_device *hdev)
+{
+	guard(mutex)(&drvdata.cfg_mutex);
+	cancel_delayed_work_sync(&drvdata.gos_cfg_setup);
+	hid_hw_close(hdev);
+	hid_hw_stop(hdev);
+	hid_set_drvdata(hdev, NULL);
+}
+
+static int hid_gos_probe(struct hid_device *hdev,
+			 const struct hid_device_id *id)
+{
+	int ret, ep;
+
+	ret = hid_parse(hdev);
+	if (ret) {
+		hid_err(hdev, "Parse failed\n");
+		return ret;
+	}
+
+	ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW);
+	if (ret) {
+		hid_err(hdev, "Failed to start HID device\n");
+		return ret;
+	}
+
+	ret = hid_hw_open(hdev);
+	if (ret) {
+		hid_err(hdev, "Failed to open HID device\n");
+		hid_hw_stop(hdev);
+		return ret;
+	}
+
+	ep = get_endpoint_address(hdev);
+	if (ep != GO_S_CFG_INTF_IN) {
+		dev_dbg(&hdev->dev, "Started interface %x as generic HID device.\n", ep);
+		return 0;
+	}
+
+	ret = hid_gos_cfg_probe(hdev, id);
+	if (ret)
+		dev_err_probe(&hdev->dev, ret, "Failed to start configuration interface");
+
+	dev_dbg(&hdev->dev, "Started interface %x as Go S configuration interface\n", ep);
+	return ret;
+}
+
+static void hid_gos_remove(struct hid_device *hdev)
+{
+	int ep = get_endpoint_address(hdev);
+
+	switch (ep) {
+	case GO_S_CFG_INTF_IN:
+		hid_gos_cfg_remove(hdev);
+		break;
+	default:
+		hid_hw_close(hdev);
+		hid_hw_stop(hdev);
+
+		break;
+	}
+}
+
+static const struct hid_device_id hid_gos_devices[] = {
+	{ HID_USB_DEVICE(USB_VENDOR_ID_QHE,
+			 USB_DEVICE_ID_LENOVO_LEGION_GO_S_XINPUT) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_QHE,
+			 USB_DEVICE_ID_LENOVO_LEGION_GO_S_DINPUT) },
+	{}
+};
+
+MODULE_DEVICE_TABLE(hid, hid_gos_devices);
+static struct hid_driver hid_lenovo_go_s = {
+	.name = "hid-lenovo-go-s",
+	.id_table = hid_gos_devices,
+	.probe = hid_gos_probe,
+	.remove = hid_gos_remove,
+	.raw_event = hid_gos_raw_event,
+};
+module_hid_driver(hid_lenovo_go_s);
+
+MODULE_AUTHOR("Derek J. Clark");
+MODULE_DESCRIPTION("HID Driver for Lenovo Legion Go S Series gamepad.");
+MODULE_LICENSE("GPL");
-- 
2.53.0


^ permalink raw reply related

* [PATCH v6 09/19] HID: Include firmware version in the uevent
From: Derek J. Clark @ 2026-03-10  7:29 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Richard Hughes, Mario Limonciello, Zhixin Zhang, Mia Shao,
	Mark Pearson, Pierre-Loup A . Griffais, Derek J . Clark,
	linux-input, linux-doc, linux-kernel
In-Reply-To: <20260310072937.3295875-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.

Reviewed-by: Derek J. Clark <derekjohn.clark@gmail.com>
Reviewed-by: Mark Pearson <mpearson-lenovo@squebb.ca>
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 dce862cafbbd..ce728c8d5bdc 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.53.0


^ permalink raw reply related

* [PATCH v6 08/19] HID: hid-lenovo-go: Add OS Mode Toggle
From: Derek J. Clark @ 2026-03-10  7:29 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Richard Hughes, Mario Limonciello, Zhixin Zhang, Mia Shao,
	Mark Pearson, Pierre-Loup A . Griffais, Derek J . Clark,
	linux-input, linux-doc, linux-kernel
In-Reply-To: <20260310072937.3295875-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.

Reviewed-by: Mark Pearson <mpearson-lenovo@squebb.ca>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v6:
  - Use NULL instead of 0 in mcu_propery_out when there is no data.
v5:
  - Remove reset_resume as it doesn't run, the device disconnects are
    reconnects during suspend. Udev or userspace will reset os_mode
    after resume.
v3:
  - Fix collision with os_mode_index attribute and os_mode_index enum.
---
 drivers/hid/hid-lenovo-go.c | 101 ++++++++++++++++++++++++++++++++++++
 1 file changed, 101 insertions(+)

diff --git a/drivers/hid/hid-lenovo-go.c b/drivers/hid/hid-lenovo-go.c
index 082d1b85d679..54861f2e04fc 100644
--- a/drivers/hid/hid-lenovo-go.c
+++ b/drivers/hid/hid-lenovo-go.c
@@ -76,6 +76,7 @@ static 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;
 	};
@@ -1343,6 +1381,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, NULL, 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)
 {
@@ -1712,6 +1808,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,
@@ -1720,6 +1819,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,
-- 
2.53.0


^ permalink raw reply related

* [PATCH v6 07/19] HID: hid-lenovo-go: Add Calibration Settings
From: Derek J. Clark @ 2026-03-10  7:29 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Richard Hughes, Mario Limonciello, Zhixin Zhang, Mia Shao,
	Mark Pearson, Pierre-Loup A . Griffais, Derek J . Clark,
	linux-input, linux-doc, linux-kernel
In-Reply-To: <20260310072937.3295875-1-derekjohn.clark@gmail.com>

Adds calibration enable and last calibration status indicators for the
triggers, joysticks, and handle gyros.

Reviewed-by: Mark Pearson <mpearson-lenovo@squebb.ca>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v6:
  - Make local attributes static.
---
 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 ee7d4bf23a17..082d1b85d679 100644
--- a/drivers/hid/hid-lenovo-go.c
+++ b/drivers/hid/hid-lenovo-go.c
@@ -43,8 +43,11 @@ static 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 @@ static 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:
@@ -1157,6 +1248,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)
 {
@@ -1466,6 +1652,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 */
 static struct go_cfg_attr version_product_mcu = { PRODUCT_VERSION };
 LEGO_DEVICE_ATTR_RO(version_product_mcu, "product_version", USB_MCU, version);
@@ -1603,9 +1813,45 @@ LEGO_DEVICE_ATTR_RW(rumble_notification_left, "rumble_notification",
 static DEVICE_ATTR_RO_NAMED(rumble_notification_left_index,
 			    "rumble_notification_index");
 
+static 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");
+
+static 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");
+
+static 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");
+
+static 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);
+
+static 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);
+
+static 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,
@@ -1674,9 +1920,45 @@ LEGO_DEVICE_ATTR_RW(rumble_notification_right, "rumble_notification",
 static DEVICE_ATTR_RO_NAMED(rumble_notification_right_index,
 			    "rumble_notification_index");
 
+static 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");
+
+static 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");
+
+static 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");
+
+static 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);
+
+static 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);
+
+static 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.53.0


^ permalink raw reply related

* [PATCH v6 06/19] HID: hid-lenovo-go: Add RGB LED control interface
From: Derek J. Clark @ 2026-03-10  7:29 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Richard Hughes, Mario Limonciello, Zhixin Zhang, Mia Shao,
	Mark Pearson, Pierre-Loup A . Griffais, Derek J . Clark,
	linux-input, linux-doc, linux-kernel
In-Reply-To: <20260310072937.3295875-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.

Reviewed-by: Mark Pearson <mpearson-lenovo@squebb.ca>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v6:
  - Invert the rgb_speed logic so larger number is faster.
  - Make local attributes static.
  - Use NULL instead of 0 in mcu_propery_out when there is no data.
v5:
  - Don't retrieve RGB state during delayed work.
---
 drivers/hid/hid-lenovo-go.c | 429 ++++++++++++++++++++++++++++++++++++
 1 file changed, 429 insertions(+)

diff --git a/drivers/hid/hid-lenovo-go.c b/drivers/hid/hid-lenovo-go.c
index 24f9444c93cd..ee7d4bf23a17 100644
--- a/drivers/hid/hid-lenovo-go.c
+++ b/drivers/hid/hid-lenovo-go.c
@@ -18,6 +18,7 @@
 #include <linux/hid.h>
 #include <linux/jiffies.h>
 #include <linux/kstrtox.h>
+#include <linux/led-class-multicolor.h>
 #include <linux/mutex.h>
 #include <linux/printk.h>
 #include <linux/sysfs.h>
@@ -37,6 +38,7 @@
 static struct hid_go_cfg {
 	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;
@@ -68,7 +70,11 @@ static 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;
@@ -223,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) {
@@ -440,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 = 100 - 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)
@@ -493,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:
@@ -565,6 +637,12 @@ static ssize_t version_show(struct device *dev, struct device_attribute *attr,
 			    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, NULL, 0);
+	if (ret)
+		return ret;
 
 	switch (index) {
 	case PRODUCT_VERSION:
@@ -1079,6 +1157,277 @@ 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, NULL, 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;
+
+	/* This is a delay setting, invert logic for consistency with other drivers */
+	val = 100 - val;
+
+	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, val;
+
+	ret = rgb_attr_show();
+	if (ret)
+		return ret;
+
+	if (drvdata.rgb_speed > 100)
+		return -EINVAL;
+
+	val = drvdata.rgb_speed;
+
+	return sysfs_emit(buf, "%hhu\n", val);
+}
+
+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, NULL, 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, NULL, 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,           \
@@ -1389,6 +1738,71 @@ static const struct attribute_group *top_level_attr_groups[] = {
 	&touchpad_attr_group,	  NULL,
 };
 
+/* RGB */
+static 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,
+};
+
+static 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,
+	},
+};
+
+static 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;
@@ -1579,6 +1993,21 @@ 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
-- 
2.53.0


^ permalink raw reply related

* [PATCH v6 05/19] HID: hid-lenovo-go: Add FPS Mode DPI settings
From: Derek J. Clark @ 2026-03-10  7:29 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Richard Hughes, Mario Limonciello, Zhixin Zhang, Mia Shao,
	Mark Pearson, Pierre-Loup A . Griffais, Derek J . Clark,
	linux-input, linux-doc, linux-kernel
In-Reply-To: <20260310072937.3295875-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.

Reviewed-by: Mark Pearson <mpearson-lenovo@squebb.ca>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v6:
  - Use NULL instead of 0 in mcu_propery_out when there is no data.
---
 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 f2a54865cfbb..24f9444c93cd 100644
--- a/drivers/hid/hid-lenovo-go.c
+++ b/drivers/hid/hid-lenovo-go.c
@@ -67,6 +67,7 @@ static 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;
@@ -220,6 +221,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) {
@@ -427,6 +430,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)
@@ -477,8 +490,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:
@@ -1016,6 +1033,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, NULL, 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,           \
@@ -1087,7 +1150,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.53.0


^ permalink raw reply related

* [PATCH v6 04/19] HID: hid-lenovo-go: Add Rumble and Haptic Settings
From: Derek J. Clark @ 2026-03-10  7:29 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Richard Hughes, Mario Limonciello, Zhixin Zhang, Mia Shao,
	Mark Pearson, Pierre-Loup A . Griffais, Derek J . Clark,
	linux-input, linux-doc, linux-kernel
In-Reply-To: <20260310072937.3295875-1-derekjohn.clark@gmail.com>

Adds attributes that control the handles rumble mode and intensity, as
well as touchpad haptic feedback settings.

Reviewed-by: Mark Pearson <mpearson-lenovo@squebb.ca>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v6:
  - Make local attributes static.
  - Use NULL instead of 0 in mcu_propery_out when there is no data.
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 d7d47db8362c..f2a54865cfbb 100644
--- a/drivers/hid/hid-lenovo-go.c
+++ b/drivers/hid/hid-lenovo-go.c
@@ -41,6 +41,8 @@ static 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;
@@ -48,11 +50,14 @@ static 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;
@@ -64,6 +69,8 @@ static 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;
@@ -170,6 +177,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) {
@@ -336,6 +386,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)
@@ -383,7 +474,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:
@@ -759,6 +854,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, NULL, 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,           \
@@ -824,10 +1081,18 @@ static DEVICE_ATTR_RO_NAMED(gamepad_mode_index, "mode_index");
 static struct go_cfg_attr reset_mcu = { FEATURE_RESET_GAMEPAD };
 LEGO_DEVICE_ATTR_WO(reset_mcu, "reset_mcu", USB_MCU, feature_status);
 
+static 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,
@@ -910,6 +1175,17 @@ static DEVICE_ATTR_RO_NAMED(imu_enabled_left_index, "imu_enabled_index");
 static struct go_cfg_attr reset_left = { FEATURE_RESET_GAMEPAD };
 LEGO_DEVICE_ATTR_WO(reset_left, "reset", LEFT_CONTROLLER, feature_status);
 
+static 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");
+
+static 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,
@@ -918,6 +1194,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,
@@ -966,6 +1246,17 @@ static DEVICE_ATTR_RO_NAMED(imu_enabled_right_index, "imu_enabled_index");
 static struct go_cfg_attr reset_right = { FEATURE_RESET_GAMEPAD };
 LEGO_DEVICE_ATTR_WO(reset_right, "reset", LEFT_CONTROLLER, feature_status);
 
+static 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");
+
+static 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,
@@ -974,6 +1265,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,
@@ -993,9 +1288,26 @@ LEGO_DEVICE_ATTR_RW(touchpad_enabled, "enabled", UNSPECIFIED, index,
 		    feature_status);
 static DEVICE_ATTR_RO_NAMED(touchpad_enabled_index, "enabled_index");
 
+static 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");
+
+static 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.53.0


^ permalink raw reply related

* [PATCH v6 03/19] HID: hid-lenovo-go: Add Feature Status Attributes
From: Derek J. Clark @ 2026-03-10  7:29 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Richard Hughes, Mario Limonciello, Zhixin Zhang, Mia Shao,
	Mark Pearson, Pierre-Loup A . Griffais, Derek J . Clark,
	linux-input, linux-doc, linux-kernel
In-Reply-To: <20260310072937.3295875-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.

Reviewed-by: Mark Pearson <mpearson-lenovo@squebb.ca>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v6:
  - Make local attributes static.
  - Use NULL instead of 0 in mcu_propery_out when there is no data.
---
 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 a13ddbe28c7d..d7d47db8362c 100644
--- a/drivers/hid/hid-lenovo-go.c
+++ b/drivers/hid/hid-lenovo-go.c
@@ -39,21 +39,31 @@ static 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;
@@ -105,6 +115,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,
@@ -113,6 +135,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) {
@@ -222,6 +279,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);
@@ -258,6 +380,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;
@@ -442,6 +570,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, NULL, 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,           \
@@ -496,7 +813,22 @@ LEGO_DEVICE_ATTR_RO(version_hardware_mcu, "hardware_version", USB_MCU, version);
 static struct go_cfg_attr version_gen_mcu = { HARDWARE_GENERATION };
 LEGO_DEVICE_ATTR_RO(version_gen_mcu, "hardware_generation", USB_MCU, version);
 
+static struct go_cfg_attr fps_switch_status = { FEATURE_FPS_SWITCH_STATUS };
+LEGO_DEVICE_ATTR_RO(fps_switch_status, "fps_switch_status", UNSPECIFIED,
+		    feature_status);
+
+static 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");
+
+static 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,
@@ -525,7 +857,11 @@ LEGO_DEVICE_ATTR_RO(version_hardware_tx_dongle, "hardware_version", TX_DONGLE, v
 static 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 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,
@@ -555,7 +891,33 @@ LEGO_DEVICE_ATTR_RO(version_hardware_left, "hardware_version", LEFT_CONTROLLER,
 static struct go_cfg_attr version_gen_left = { HARDWARE_GENERATION };
 LEGO_DEVICE_ATTR_RO(version_gen_left, "hardware_generation", LEFT_CONTROLLER, version);
 
+static 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");
+
+static 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");
+
+static 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");
+
+static 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,
@@ -585,7 +947,33 @@ LEGO_DEVICE_ATTR_RO(version_hardware_right, "hardware_version", RIGHT_CONTROLLER
 static struct go_cfg_attr version_gen_right = { HARDWARE_GENERATION };
 LEGO_DEVICE_ATTR_RO(version_gen_right, "hardware_generation", RIGHT_CONTROLLER, version);
 
+static 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");
+
+static 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");
+
+static 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");
+
+static 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,
@@ -600,8 +988,14 @@ static const struct attribute_group right_gamepad_attr_group = {
 };
 
 /* Touchpad */
+static 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.53.0


^ permalink raw reply related

* [PATCH v6 02/19] HID: hid-lenovo-go: Add Lenovo Legion Go Series HID Driver
From: Derek J. Clark @ 2026-03-10  7:29 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Richard Hughes, Mario Limonciello, Zhixin Zhang, Mia Shao,
	Mark Pearson, Pierre-Loup A . Griffais, Derek J . Clark,
	linux-input, linux-doc, linux-kernel
In-Reply-To: <20260310072937.3295875-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.

Reviewed-by: Mark Pearson <mpearson-lenovo@squebb.ca>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
--
v6:
  - Make attributes static.
  - Use NULL instead of 0 in mcu_propery_out when there is no data.
v5:
  - Make version attributes static, retrieve them using delayed work
    during probe.
  - Fix endianness of version strings and print as hex.
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 | 914 ++++++++++++++++++++++++++++++++++++
 5 files changed, 938 insertions(+)
 create mode 100644 drivers/hid/hid-lenovo-go.c

diff --git a/MAINTAINERS b/MAINTAINERS
index bacaec38aaf1..75d89590f3d2 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -14415,6 +14415,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 c1d9f7c6a5f2..2925dba429f5 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -623,6 +623,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 e01838239ae6..79fbe4e3e2f4 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 3e299a30dcde..093ee86ebf90 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -858,7 +858,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..a13ddbe28c7d
--- /dev/null
+++ b/drivers/hid/hid-lenovo-go.c
@@ -0,0 +1,914 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ *  HID driver for Lenovo Legion Go series gamepads.
+ *
+ *  Copyright (c) 2026 Derek J. Clark <derekjohn.clark@gmail.com>
+ *  Copyright (c) 2026 Valve Corporation
+ */
+
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/array_size.h>
+#include <linux/cleanup.h>
+#include <linux/completion.h>
+#include <linux/delay.h>
+#include <linux/dev_printk.h>
+#include <linux/device.h>
+#include <linux/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 <linux/workqueue.h>
+#include <linux/workqueue_types.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
+
+static struct hid_go_cfg {
+	struct delayed_work go_cfg_setup;
+	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_be32(cmd_rep->data);
+			return 0;
+		case TX_DONGLE:
+			drvdata.tx_dongle_version_product =
+				get_unaligned_be32(cmd_rep->data);
+			return 0;
+		case LEFT_CONTROLLER:
+			drvdata.gp_left_version_product =
+				get_unaligned_be32(cmd_rep->data);
+			return 0;
+		case RIGHT_CONTROLLER:
+			drvdata.gp_right_version_product =
+				get_unaligned_be32(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_be32(cmd_rep->data);
+			return 0;
+		case TX_DONGLE:
+			drvdata.tx_dongle_version_protocol =
+				get_unaligned_be32(cmd_rep->data);
+			return 0;
+		case LEFT_CONTROLLER:
+			drvdata.gp_left_version_protocol =
+				get_unaligned_be32(cmd_rep->data);
+			return 0;
+		case RIGHT_CONTROLLER:
+			drvdata.gp_right_version_protocol =
+				get_unaligned_be32(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_be32(cmd_rep->data);
+			return 0;
+		case TX_DONGLE:
+			drvdata.tx_dongle_version_firmware =
+				get_unaligned_be32(cmd_rep->data);
+			return 0;
+		case LEFT_CONTROLLER:
+			drvdata.gp_left_version_firmware =
+				get_unaligned_be32(cmd_rep->data);
+			return 0;
+		case RIGHT_CONTROLLER:
+			drvdata.gp_right_version_firmware =
+				get_unaligned_be32(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_be32(cmd_rep->data);
+			return 0;
+		case TX_DONGLE:
+			drvdata.tx_dongle_version_hardware =
+				get_unaligned_be32(cmd_rep->data);
+			return 0;
+		case LEFT_CONTROLLER:
+			drvdata.gp_left_version_hardware =
+				get_unaligned_be32(cmd_rep->data);
+			return 0;
+		case RIGHT_CONTROLLER:
+			drvdata.gp_right_version_hardware =
+				get_unaligned_be32(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)
+{
+	unsigned char *dmabuf __free(kfree) = NULL;
+	u8 header[] = { GO_OUTPUT_REPORT_ID, id, command, index, device };
+	size_t header_size = ARRAY_SIZE(header);
+	int timeout = 50;
+	int ret;
+
+	if (header_size + len > GO_PACKET_SIZE)
+		return -EINVAL;
+
+	guard(mutex)(&drvdata.cfg_mutex);
+	/* We can't use a devm_alloc reusable buffer without side effects during suspend */
+	dmabuf = kzalloc(GO_PACKET_SIZE, GFP_KERNEL);
+	if (!dmabuf)
+		return -ENOMEM;
+
+	memcpy(dmabuf, header, header_size);
+	memcpy(dmabuf + header_size, data, len);
+
+	dev_dbg(&hdev->dev, "Send data as raw output report: [%*ph]\n",
+		GO_PACKET_SIZE, dmabuf);
+
+	ret = hid_hw_output_report(hdev, dmabuf, 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;
+
+	reinit_completion(&drvdata.send_cmd_complete);
+	return 0;
+}
+
+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;
+
+	switch (index) {
+	case PRODUCT_VERSION:
+		switch (device_type) {
+		case USB_MCU:
+			count = sysfs_emit(buf, "%x\n",
+					   drvdata.mcu_version_product);
+			break;
+		case TX_DONGLE:
+			count = sysfs_emit(buf, "%x\n",
+					   drvdata.tx_dongle_version_product);
+			break;
+		case LEFT_CONTROLLER:
+			count = sysfs_emit(buf, "%x\n",
+					   drvdata.gp_left_version_product);
+			break;
+		case RIGHT_CONTROLLER:
+			count = sysfs_emit(buf, "%x\n",
+					   drvdata.gp_right_version_product);
+			break;
+		default:
+			return -EINVAL;
+		}
+		break;
+	case PROTOCOL_VERSION:
+		switch (device_type) {
+		case USB_MCU:
+			count = sysfs_emit(buf, "%x\n",
+					   drvdata.mcu_version_protocol);
+			break;
+		case TX_DONGLE:
+			count = sysfs_emit(buf, "%x\n",
+					   drvdata.tx_dongle_version_protocol);
+			break;
+		case LEFT_CONTROLLER:
+			count = sysfs_emit(buf, "%x\n",
+					   drvdata.gp_left_version_protocol);
+			break;
+		case RIGHT_CONTROLLER:
+			count = sysfs_emit(buf, "%x\n",
+					   drvdata.gp_right_version_protocol);
+			break;
+		default:
+			return -EINVAL;
+		}
+		break;
+	case FIRMWARE_VERSION:
+		switch (device_type) {
+		case USB_MCU:
+			count = sysfs_emit(buf, "%x\n",
+					   drvdata.mcu_version_firmware);
+			break;
+		case TX_DONGLE:
+			count = sysfs_emit(buf, "%x\n",
+					   drvdata.tx_dongle_version_firmware);
+			break;
+		case LEFT_CONTROLLER:
+			count = sysfs_emit(buf, "%x\n",
+					   drvdata.gp_left_version_firmware);
+			break;
+		case RIGHT_CONTROLLER:
+			count = sysfs_emit(buf, "%x\n",
+					   drvdata.gp_right_version_firmware);
+			break;
+		default:
+			return -EINVAL;
+		}
+		break;
+	case HARDWARE_VERSION:
+		switch (device_type) {
+		case USB_MCU:
+			count = sysfs_emit(buf, "%x\n",
+					   drvdata.mcu_version_hardware);
+			break;
+		case TX_DONGLE:
+			count = sysfs_emit(buf, "%x\n",
+					   drvdata.tx_dongle_version_hardware);
+			break;
+		case LEFT_CONTROLLER:
+			count = sysfs_emit(buf, "%x\n",
+					   drvdata.gp_left_version_hardware);
+			break;
+		case RIGHT_CONTROLLER:
+			count = sysfs_emit(buf, "%x\n",
+					   drvdata.gp_right_version_hardware);
+			break;
+		default:
+			return -EINVAL;
+		}
+		break;
+	case HARDWARE_GENERATION:
+		switch (device_type) {
+		case USB_MCU:
+			count = sysfs_emit(buf, "%x\n",
+					   drvdata.mcu_version_gen);
+			break;
+		case TX_DONGLE:
+			count = sysfs_emit(buf, "%x\n",
+					   drvdata.tx_dongle_version_gen);
+			break;
+		case LEFT_CONTROLLER:
+			count = sysfs_emit(buf, "%x\n",
+					   drvdata.gp_left_version_gen);
+			break;
+		case RIGHT_CONTROLLER:
+			count = sysfs_emit(buf, "%x\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 */
+static struct go_cfg_attr version_product_mcu = { PRODUCT_VERSION };
+LEGO_DEVICE_ATTR_RO(version_product_mcu, "product_version", USB_MCU, version);
+
+static struct go_cfg_attr version_protocol_mcu = { PROTOCOL_VERSION };
+LEGO_DEVICE_ATTR_RO(version_protocol_mcu, "protocol_version", USB_MCU, version);
+
+static struct go_cfg_attr version_firmware_mcu = { FIRMWARE_VERSION };
+LEGO_DEVICE_ATTR_RO(version_firmware_mcu, "firmware_version", USB_MCU, version);
+
+static struct go_cfg_attr version_hardware_mcu = { HARDWARE_VERSION };
+LEGO_DEVICE_ATTR_RO(version_hardware_mcu, "hardware_version", USB_MCU, version);
+
+static 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 */
+static struct go_cfg_attr version_product_tx_dongle = { PRODUCT_VERSION };
+LEGO_DEVICE_ATTR_RO(version_product_tx_dongle, "product_version", TX_DONGLE, version);
+
+static struct go_cfg_attr version_protocol_tx_dongle = { PROTOCOL_VERSION };
+LEGO_DEVICE_ATTR_RO(version_protocol_tx_dongle, "protocol_version", TX_DONGLE, version);
+
+static struct go_cfg_attr version_firmware_tx_dongle = { FIRMWARE_VERSION };
+LEGO_DEVICE_ATTR_RO(version_firmware_tx_dongle, "firmware_version", TX_DONGLE, version);
+
+static struct go_cfg_attr version_hardware_tx_dongle = { HARDWARE_VERSION };
+LEGO_DEVICE_ATTR_RO(version_hardware_tx_dongle, "hardware_version", TX_DONGLE, version);
+
+static 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 */
+static struct go_cfg_attr version_product_left = { PRODUCT_VERSION };
+LEGO_DEVICE_ATTR_RO(version_product_left, "product_version", LEFT_CONTROLLER, version);
+
+static struct go_cfg_attr version_protocol_left = { PROTOCOL_VERSION };
+LEGO_DEVICE_ATTR_RO(version_protocol_left, "protocol_version", LEFT_CONTROLLER, version);
+
+static struct go_cfg_attr version_firmware_left = { FIRMWARE_VERSION };
+LEGO_DEVICE_ATTR_RO(version_firmware_left, "firmware_version", LEFT_CONTROLLER, version);
+
+static struct go_cfg_attr version_hardware_left = { HARDWARE_VERSION };
+LEGO_DEVICE_ATTR_RO(version_hardware_left, "hardware_version", LEFT_CONTROLLER, version);
+
+static 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 */
+static struct go_cfg_attr version_product_right = { PRODUCT_VERSION };
+LEGO_DEVICE_ATTR_RO(version_product_right, "product_version", RIGHT_CONTROLLER, version);
+
+static struct go_cfg_attr version_protocol_right = { PROTOCOL_VERSION };
+LEGO_DEVICE_ATTR_RO(version_protocol_right, "protocol_version", RIGHT_CONTROLLER, version);
+
+static struct go_cfg_attr version_firmware_right = { FIRMWARE_VERSION };
+LEGO_DEVICE_ATTR_RO(version_firmware_right, "firmware_version", RIGHT_CONTROLLER, version);
+
+static struct go_cfg_attr version_hardware_right = { HARDWARE_VERSION };
+LEGO_DEVICE_ATTR_RO(version_hardware_right, "hardware_version", RIGHT_CONTROLLER, version);
+
+static 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 void cfg_setup(struct work_struct *work)
+{
+	int ret;
+
+	/* MCU Version Attrs */
+	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+			       PRODUCT_VERSION, USB_MCU, NULL, 0);
+	if (ret < 0) {
+		dev_err(&drvdata.hdev->dev,
+			"Failed to retrieve USB_MCU Product Version: %i\n", ret);
+		return;
+	}
+
+	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+			       PROTOCOL_VERSION, USB_MCU, NULL, 0);
+	if (ret < 0) {
+		dev_err(&drvdata.hdev->dev,
+			"Failed to retrieve USB_MCU Protocol Version: %i\n", ret);
+		return;
+	}
+
+	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+			       FIRMWARE_VERSION, USB_MCU, NULL, 0);
+	if (ret < 0) {
+		dev_err(&drvdata.hdev->dev,
+			"Failed to retrieve USB_MCU Firmware Version: %i\n", ret);
+		return;
+	}
+
+	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+			       HARDWARE_VERSION, USB_MCU, NULL, 0);
+	if (ret < 0) {
+		dev_err(&drvdata.hdev->dev,
+			"Failed to retrieve USB_MCU Hardware Version: %i\n", ret);
+		return;
+	}
+
+	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+			       HARDWARE_GENERATION, USB_MCU, NULL, 0);
+	if (ret < 0) {
+		dev_err(&drvdata.hdev->dev,
+			"Failed to retrieve USB_MCU Hardware Generation: %i\n", ret);
+		return;
+	}
+
+	/* TX Dongle Version Attrs */
+	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+			       PRODUCT_VERSION, TX_DONGLE, NULL, 0);
+	if (ret < 0) {
+		dev_err(&drvdata.hdev->dev,
+			"Failed to retrieve TX_DONGLE Product Version: %i\n", ret);
+		return;
+	}
+
+	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+			       PROTOCOL_VERSION, TX_DONGLE, NULL, 0);
+	if (ret < 0) {
+		dev_err(&drvdata.hdev->dev,
+			"Failed to retrieve TX_DONGLE Protocol Version: %i\n", ret);
+		return;
+	}
+
+	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+			       FIRMWARE_VERSION, TX_DONGLE, NULL, 0);
+	if (ret < 0) {
+		dev_err(&drvdata.hdev->dev,
+			"Failed to retrieve TX_DONGLE Firmware Version: %i\n", ret);
+		return;
+	}
+
+	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+			       HARDWARE_VERSION, TX_DONGLE, NULL, 0);
+	if (ret < 0) {
+		dev_err(&drvdata.hdev->dev,
+			"Failed to retrieve TX_DONGLE Hardware Version: %i\n", ret);
+		return;
+	}
+
+	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+			       HARDWARE_GENERATION, TX_DONGLE, NULL, 0);
+	if (ret < 0) {
+		dev_err(&drvdata.hdev->dev,
+			"Failed to retrieve TX_DONGLE Hardware Generation: %i\n", ret);
+		return;
+	}
+
+	/* Left Handle Version Attrs */
+	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+			       PRODUCT_VERSION, LEFT_CONTROLLER, NULL, 0);
+	if (ret < 0) {
+		dev_err(&drvdata.hdev->dev,
+			"Failed to retrieve LEFT_CONTROLLER Product Version: %i\n", ret);
+		return;
+	}
+
+	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+			       PROTOCOL_VERSION, LEFT_CONTROLLER, NULL, 0);
+	if (ret < 0) {
+		dev_err(&drvdata.hdev->dev,
+			"Failed to retrieve LEFT_CONTROLLER Protocol Version: %i\n", ret);
+		return;
+	}
+
+	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+			       FIRMWARE_VERSION, LEFT_CONTROLLER, NULL, 0);
+	if (ret < 0) {
+		dev_err(&drvdata.hdev->dev,
+			"Failed to retrieve LEFT_CONTROLLER Firmware Version: %i\n", ret);
+		return;
+	}
+
+	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+			       HARDWARE_VERSION, LEFT_CONTROLLER, NULL, 0);
+	if (ret < 0) {
+		dev_err(&drvdata.hdev->dev,
+			"Failed to retrieve LEFT_CONTROLLER Hardware Version: %i\n", ret);
+		return;
+	}
+
+	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+			       HARDWARE_GENERATION, LEFT_CONTROLLER, NULL, 0);
+	if (ret < 0) {
+		dev_err(&drvdata.hdev->dev,
+			"Failed to retrieve LEFT_CONTROLLER Hardware Generation: %i\n", ret);
+		return;
+	}
+
+	/* Right Handle Version Attrs */
+	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+			       PRODUCT_VERSION, RIGHT_CONTROLLER, NULL, 0);
+	if (ret < 0) {
+		dev_err(&drvdata.hdev->dev,
+			"Failed to retrieve RIGHT_CONTROLLER Product Version: %i\n", ret);
+		return;
+	}
+
+	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+			       PROTOCOL_VERSION, RIGHT_CONTROLLER, NULL, 0);
+	if (ret < 0) {
+		dev_err(&drvdata.hdev->dev,
+			"Failed to retrieve RIGHT_CONTROLLER Protocol Version: %i\n", ret);
+		return;
+	}
+
+	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+			       FIRMWARE_VERSION, RIGHT_CONTROLLER, NULL, 0);
+	if (ret < 0) {
+		dev_err(&drvdata.hdev->dev,
+			"Failed to retrieve RIGHT_CONTROLLER Firmware Version: %i\n", ret);
+		return;
+	}
+
+	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+			       HARDWARE_VERSION, RIGHT_CONTROLLER, NULL, 0);
+	if (ret < 0) {
+		dev_err(&drvdata.hdev->dev,
+			"Failed to retrieve RIGHT_CONTROLLER Hardware Version: %i\n", ret);
+		return;
+	}
+
+	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+			       HARDWARE_GENERATION, RIGHT_CONTROLLER, NULL, 0);
+	if (ret < 0) {
+		dev_err(&drvdata.hdev->dev,
+			"Failed to retrieve RIGHT_CONTROLLER Hardware Generation: %i\n", ret);
+		return;
+	}
+}
+
+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.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
+	 * 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);
+	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.53.0


^ permalink raw reply related

* [PATCH v6 01/19] include: device.h: Add named device attributes
From: Derek J. Clark @ 2026-03-10  7:29 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Richard Hughes, Mario Limonciello, Zhixin Zhang, Mia Shao,
	Mark Pearson, Pierre-Loup A . Griffais, Derek J . Clark,
	linux-input, linux-doc, linux-kernel, Greg Kroah-Hartman
In-Reply-To: <20260310072937.3295875-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.

Reviewed-by: Mark Pearson <mpearson-lenovo@squebb.ca>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
Acked-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
---
 include/linux/device.h | 46 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 46 insertions(+)

diff --git a/include/linux/device.h b/include/linux/device.h
index 0be95294b6e6..381463baed6d 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.53.0


^ permalink raw reply related

* [PATCH v6 00/19] HID: Add Legion Go and Go S Drivers
From: Derek J. Clark @ 2026-03-10  7:29 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Richard Hughes, 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

Reviewed-by: Mark Pearson <mpearson-lenovo@squebb.ca>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
Change Log
v6:
  - Include multiple bug fixes from Ethan Tidmore.
  - Make all local attributes static.
  - Invert the rgb_speed logic for the go driver. On the Go this
    attribute sets a delay, so flip the logic to match Go S where the
    larger number means faster so userspace can target this consistently.
  - Include 3 new patches that fix formatting issues with v5 authored by
    additional developers.
v5: https://lore.kernel.org/linux-input/20260224013217.1363996-1-derekjohn.clark@gmail.com/
  - Make all RO attributes cache the data during probe using delayed
    work for both drivers. All RW attributes are read in realtime to
    ensure they match the device current state in the event of firmware
    reset or a userspace application.
  - Fix endianness of version strings and print as hex for Go driver.
  - Remove reset__esume function. It was not being hit as the MCU of
    both devices disconnects of suspend, forcing a reinit of the driver.
    Udev or userpsace will need to set the OS Mode upon resume.
v4: https://lore.kernel.org/linux-input/20260220070533.4083667-1-derekjohn.clark@gmail.com/
  - Use dmabuf allocated per request for both drivers instead of a devm
    preallocated buffer that is reused. This solves a bug where some
    attributes couldn't be restored without manual writing after resume.
  - Reduce the number of quirks and flags in the Go S init to only those
    necessary. Previously they were duplicated from the Go driver but
    everything except HID_CONNECT_HIDRAW was found to be unnessary
    during operational testing.
  - Clean up formatting for debug prints in Go S driver.
  - Fix bugs in RGB driver for Go that caused the effect to switch to
    solid when the speed or brightness was changed.
  - Remove extraneous setting of os_mode member of drvdata when setting
    os_mode. It will be read from the hardware in _show.
v3: https://lore.kernel.org/linux-input/20260124014907.991265-1-derekjohn.clark@gmail.com/
  - 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 and move
    it out of a subdirectory.
  - Drop the arbitrary uevent properties patch.
  - Add Go series driver.
  - Move DEVICE_ATTR_NAMED macros to device.h.
v1: https://lore.kernel.org/linux-input/20250703004943.515919-1-derekjohn.clark@gmail.com/

Chen Ni (2):
  HID: hid-lenovo-go-s: Remove unneeded semicolon
  HID: hid-lenovo-go: Remove unneeded semicolon

Colin Ian King (1):
  HID: hid-lenovo-go-s: Fix spelling mistake "configuratiion" ->
    "configuration"

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                 | 1506 ++++++++++
 drivers/hid/hid-lenovo-go.c                   | 2502 +++++++++++++++++
 include/linux/device.h                        |   46 +
 include/linux/hid.h                           |    1 +
 11 files changed, 5132 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.53.0


^ permalink raw reply

* Re: [PATCH v3 10/10] Input: xbox_gip - Add flight stick support
From: Vicki Pfau @ 2026-03-10  5:23 UTC (permalink / raw)
  To: Dmitry Torokhov, linux-input
In-Reply-To: <20260310052017.1289494-11-vi@endrift.com>

On 3/9/26 10:20 PM, Vicki Pfau wrote:
> This adds preliminary flight stick support, with a few caveats:
> 
> - Flight sticks support up to 64 extra buttons. This only exposes the first
>   50, as there isn't any good place to map the remainder.
> - Flight sticks support up to 12 extra axes. This picks a fairly abritrary
>   mapping for them, as there's again no good place to map them.
> 
> Flight sticks also have addressible LEDs, but I don't have a device that
> supports them so I can't test them yet.
> 
> Signed-off-by: Vicki Pfau <vi@endrift.com>
> ---
>  drivers/input/joystick/gip/Makefile           |   2 +-
>  drivers/input/joystick/gip/gip-core.c         |   4 +-
>  drivers/input/joystick/gip/gip-flight-stick.c | 179 ++++++++++++++++++
>  drivers/input/joystick/gip/gip.h              |   1 +
>  4 files changed, 184 insertions(+), 2 deletions(-)
>  create mode 100644 drivers/input/joystick/gip/gip-flight-stick.c
> 
> diff --git a/drivers/input/joystick/gip/Makefile b/drivers/input/joystick/gip/Makefile
> index db6c9079c7e18..4de873f77a020 100644
> --- a/drivers/input/joystick/gip/Makefile
> +++ b/drivers/input/joystick/gip/Makefile
> @@ -1,3 +1,3 @@
>  # SPDX-License-Identifier: GPL-2.0-or-later
>  obj-$(CONFIG_JOYSTICK_XBOX_GIP)	+= xbox-gip.o
> -xbox-gip-y := gip-arcade-stick.o gip-core.o gip-drivers.o gip-wheel.o
> +xbox-gip-y := gip-arcade-stick.o gip-core.o gip-drivers.o gip-flight-stick.o gip-wheel.o
> diff --git a/drivers/input/joystick/gip/gip-core.c b/drivers/input/joystick/gip/gip-core.c
> index 773d7705b7be8..17e2be5cd2444 100644
> --- a/drivers/input/joystick/gip/gip-core.c
> +++ b/drivers/input/joystick/gip/gip-core.c
> @@ -11,9 +11,10 @@
>   * - Sending fragmented messages
>   * - Raw character device
>   * - Wheel force feedback
> - * - Flight stick support
>   * - More arcade stick testing
>   * - Arcade stick extra buttons
> + * - More flight stick testing
> + * - Flight stick LEDs
>   * - Split into driver-per-attachment GIP-as-a-bus approach drivers
>   *
>   * This driver is based on the Microsoft GIP spec at:
> @@ -319,6 +320,7 @@ static const struct gip_driver* base_drivers[] = {
>  	&gip_driver_arcade_stick,
>  	&gip_driver_trueforce_wheel,
>  	&gip_driver_wheel,
> +	&gip_driver_flight_stick,
>  	NULL /* Sentinel */
>  };
>  
> diff --git a/drivers/input/joystick/gip/gip-flight-stick.c b/drivers/input/joystick/gip/gip-flight-stick.c
> new file mode 100644
> index 0000000000000..c2b913a012d0e
> --- /dev/null
> +++ b/drivers/input/joystick/gip/gip-flight-stick.c
> @@ -0,0 +1,179 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * Drivers for GIP flight stick devices
> + *
> + * Copyright (c) 2025 Valve Software
> + *
> + * This driver is based on the Microsoft GIP spec at:
> + * https://aka.ms/gipdocs
> + * https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gipusb/e7c90904-5e21-426e-b9ad-d82adeee0dbc
> + */
> +#include "gip.h"
> +
> +/* FlightStick vendor messages */
> +#define GIP_CMD_DEVICE_CAPABILITIES	0x00
> +#define GIP_CMD_LED_CAPABILITIES	0x01
> +#define GIP_CMD_SET_LED_STATE		0x02
> +
> +/*
> + * The spec defines up to 64 extra buttons and 12 extra axes, but there's
> + * currently no good way to map all of them. For now, let's leave it as a much
> + * smaller number.and add more when we get a better way.
> + */
> +#define MAX_GIP_FLIGHT_STICK_BUTTONS 10
> +#define MAX_GIP_FLIGHT_STICK_AXES 1
> +
> +struct gip_flight_stick_capabilities_response {
> +	uint8_t extra_button_count;
> +	uint8_t extra_axis_count;
> +	uint8_t led_count;
> +	uint8_t max_global_led_gain;
> +};
> +
> +static const unsigned int gip_flight_stick_extra_buttons[] = {
> +	BTN_0,
> +	BTN_1,
> +	BTN_2,
> +	BTN_3,
> +	BTN_4,
> +	BTN_5,
> +	BTN_6,
> +	BTN_7,
> +	BTN_8,
> +	BTN_9,
> +};
> +
> +static const unsigned int gip_flight_stick_extra_axes[] = {
> +	ABS_RUDDER,
> +};
> +
> +static int gip_flight_stick_init(struct gip_attachment *attachment)
> +{
> +	int rc = gip_send_vendor_message(attachment, GIP_CMD_DEVICE_CAPABILITIES, 0, NULL, 0);
> +
> +	if (rc < 0)
> +		return rc;
> +
> +	return GIP_INIT_NO_INPUT;
> +}
> +
> +static int gip_setup_flight_stick_input(struct gip_attachment *attachment, struct input_dev* input)
> +{
> +	int i;
> +	int rc = gip_driver_navigation.setup_input(attachment, input);
> +
> +	if (rc)
> +		return rc;
> +
> +	input_set_capability(input, EV_KEY, BTN_TOP);
> +	input_set_capability(input, EV_KEY, BTN_TOP2);
> +	for (i = 0; i < attachment->extra_buttons && i < MAX_GIP_FLIGHT_STICK_BUTTONS; i++)
> +		input_set_capability(input, EV_KEY, BTN_0 + i);
> +	if (attachment->extra_buttons > MAX_GIP_FLIGHT_STICK_BUTTONS)
> +		dev_info(GIP_DEV(attachment),
> +			"Device has too many extra buttons, %i through %i ignored\n",
> +			MAX_GIP_FLIGHT_STICK_BUTTONS,
> +			attachment->extra_buttons + 1);
> +	input_set_abs_params(input, ABS_X, -32768, 32767, 0, 0);
> +	input_set_abs_params(input, ABS_Y, -32768, 32767, 0, 0);
> +	input_set_abs_params(input, ABS_Z, -32768, 32767, 0, 0);
> +	input_set_abs_params(input, ABS_THROTTLE, 0, 65535, 0, 0);
> +	for (i = 0; i < attachment->extra_axes && i < MAX_GIP_FLIGHT_STICK_AXES; i++)
> +		input_set_abs_params(input, gip_flight_stick_extra_axes[i], 0, 65535, 0, 0);
> +
> +	return 0;
> +}
> +
> +static int gip_handle_flight_stick_report(struct gip_attachment *attachment,
> +	struct input_dev *input, const uint8_t *bytes, int num_bytes)
> +{
> +	int32_t axis;
> +	int rc = gip_driver_navigation.handle_input_report(attachment, input, bytes, num_bytes);
> +	int i;
> +
> +	if (rc)
> +		return rc;
> +
> +	if (num_bytes < 19)
> +		return -EINVAL;
> +
> +	/* Fire 1 and 2 */
> +	input_report_key(input, BTN_TOP, bytes[2] & BIT(0));
> +	input_report_key(input, BTN_TOP2, bytes[2] & BIT(1));
> +
> +	for (i = 0; i < attachment->extra_buttons && i < MAX_GIP_FLIGHT_STICK_BUTTONS; i++) {
> +		input_report_key(input, gip_flight_stick_extra_buttons[i],
> +			bytes[i / 8 + 3] & BIT(i));
> +	}
> +
> +	/*
> +	 * Roll, pitch and yaw are signed. Throttle and any
> +	 * extra axes are unsigned. All values are full-range.
> +	 */
> +	axis = bytes[11];
> +	axis |= bytes[12] << 8;
> +	input_report_abs(input, ABS_X, (int16_t) axis);
> +
> +	axis = bytes[13];
> +	axis |= bytes[14] << 8;
> +	input_report_abs(input, ABS_Y, (int16_t) axis);
> +
> +	axis = bytes[15];
> +	axis |= bytes[16] << 8;
> +	input_report_abs(input, ABS_Z, (int16_t) axis);
> +
> +	axis = bytes[17];
> +	axis |= bytes[18] << 8;
> +	input_report_abs(input, ABS_THROTTLE, axis);
> +
> +	for (i = 0; i < attachment->extra_axes && i < MAX_GIP_FLIGHT_STICK_AXES; i++) {
> +		if (20 + i * 2 >= num_bytes)
> +			break;
> +
> +		axis = bytes[19 + i * 2];
> +		axis |= bytes[20 + i * 2] << 8;
> +		input_report_abs(input, gip_flight_stick_extra_axes[i], axis);
> +	}
> +
> +	return 0;
> +}
> +
> +static int gip_handle_flight_stick_cmd_device_capabilities(struct gip_attachment *attachment,
> +	const struct gip_header *header, const uint8_t *bytes, int num_bytes)
> +{
> +	const struct gip_flight_stick_capabilities_response *response =
> +		(const struct gip_flight_stick_capabilities_response*)bytes;
> +	struct input_dev *input;
> +
> +	rcu_read_lock();
> +	input = rcu_dereference(attachment->input);
> +	rcu_read_unlock();
> +	if (input)
> +		return 0;
> +
> +	if (num_bytes < 4)
> +		return -EINVAL;
> +
> +	attachment->extra_axes = min(response->extra_axis_count, MAX_GIP_FLIGHT_STICK_AXES);
> +	attachment->extra_buttons = min(response->extra_button_count, MAX_GIP_FLIGHT_STICK_BUTTONS);
> +	return gip_setup_input_device(attachment);
> +}
> +
> +const struct gip_driver gip_driver_flight_stick = {
> +	.types = (const char* const[]) {
> +		"Windows.Xbox.Input.FlightStick",
> +		"Microsoft.Xbox.Input.FlightStick",
> +		NULL
> +	},
> +	.guid = GUID_INIT(0x03f1a011, 0xefe9, 0x4cc1, 0x96, 0x9c,
> +		0x38, 0xdc, 0x55, 0xf4, 0x04, 0xd0),
> +
> +	.probe = NULL,
> +	.remove = NULL,
> +	.init = gip_flight_stick_init,
> +	.setup_input = gip_setup_flight_stick_input,
> +	.handle_input_report = gip_handle_flight_stick_report,
> +	.vendor_handlers = {
> +		[GIP_CMD_DEVICE_CAPABILITIES] = gip_handle_flight_stick_cmd_device_capabilities,
> +	},
> +};
> diff --git a/drivers/input/joystick/gip/gip.h b/drivers/input/joystick/gip/gip.h
> index 3b5cab96dc0b7..6d70a69d99a39 100644
> --- a/drivers/input/joystick/gip/gip.h
> +++ b/drivers/input/joystick/gip/gip.h
> @@ -323,4 +323,5 @@ extern const struct gip_driver gip_driver_gamepad;
>  extern const struct gip_driver gip_driver_arcade_stick;
>  extern const struct gip_driver gip_driver_wheel;
>  extern const struct gip_driver gip_driver_trueforce_wheel;
> +extern const struct gip_driver gip_driver_flight_stick;
>  #endif

Shoot, I didn't mean to send this tenth patch. I don't consider it ready due to issues with evdev mapping of purely-numeric, unnamed inputs, but the other 9 should be.

Vicki

^ permalink raw reply

* [PATCH v3 10/10] Input: xbox_gip - Add flight stick support
From: Vicki Pfau @ 2026-03-10  5:20 UTC (permalink / raw)
  To: Dmitry Torokhov, linux-input; +Cc: Vicki Pfau
In-Reply-To: <20260310052017.1289494-1-vi@endrift.com>

This adds preliminary flight stick support, with a few caveats:

- Flight sticks support up to 64 extra buttons. This only exposes the first
  50, as there isn't any good place to map the remainder.
- Flight sticks support up to 12 extra axes. This picks a fairly abritrary
  mapping for them, as there's again no good place to map them.

Flight sticks also have addressible LEDs, but I don't have a device that
supports them so I can't test them yet.

Signed-off-by: Vicki Pfau <vi@endrift.com>
---
 drivers/input/joystick/gip/Makefile           |   2 +-
 drivers/input/joystick/gip/gip-core.c         |   4 +-
 drivers/input/joystick/gip/gip-flight-stick.c | 179 ++++++++++++++++++
 drivers/input/joystick/gip/gip.h              |   1 +
 4 files changed, 184 insertions(+), 2 deletions(-)
 create mode 100644 drivers/input/joystick/gip/gip-flight-stick.c

diff --git a/drivers/input/joystick/gip/Makefile b/drivers/input/joystick/gip/Makefile
index db6c9079c7e18..4de873f77a020 100644
--- a/drivers/input/joystick/gip/Makefile
+++ b/drivers/input/joystick/gip/Makefile
@@ -1,3 +1,3 @@
 # SPDX-License-Identifier: GPL-2.0-or-later
 obj-$(CONFIG_JOYSTICK_XBOX_GIP)	+= xbox-gip.o
-xbox-gip-y := gip-arcade-stick.o gip-core.o gip-drivers.o gip-wheel.o
+xbox-gip-y := gip-arcade-stick.o gip-core.o gip-drivers.o gip-flight-stick.o gip-wheel.o
diff --git a/drivers/input/joystick/gip/gip-core.c b/drivers/input/joystick/gip/gip-core.c
index 773d7705b7be8..17e2be5cd2444 100644
--- a/drivers/input/joystick/gip/gip-core.c
+++ b/drivers/input/joystick/gip/gip-core.c
@@ -11,9 +11,10 @@
  * - Sending fragmented messages
  * - Raw character device
  * - Wheel force feedback
- * - Flight stick support
  * - More arcade stick testing
  * - Arcade stick extra buttons
+ * - More flight stick testing
+ * - Flight stick LEDs
  * - Split into driver-per-attachment GIP-as-a-bus approach drivers
  *
  * This driver is based on the Microsoft GIP spec at:
@@ -319,6 +320,7 @@ static const struct gip_driver* base_drivers[] = {
 	&gip_driver_arcade_stick,
 	&gip_driver_trueforce_wheel,
 	&gip_driver_wheel,
+	&gip_driver_flight_stick,
 	NULL /* Sentinel */
 };
 
diff --git a/drivers/input/joystick/gip/gip-flight-stick.c b/drivers/input/joystick/gip/gip-flight-stick.c
new file mode 100644
index 0000000000000..c2b913a012d0e
--- /dev/null
+++ b/drivers/input/joystick/gip/gip-flight-stick.c
@@ -0,0 +1,179 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Drivers for GIP flight stick devices
+ *
+ * Copyright (c) 2025 Valve Software
+ *
+ * This driver is based on the Microsoft GIP spec at:
+ * https://aka.ms/gipdocs
+ * https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gipusb/e7c90904-5e21-426e-b9ad-d82adeee0dbc
+ */
+#include "gip.h"
+
+/* FlightStick vendor messages */
+#define GIP_CMD_DEVICE_CAPABILITIES	0x00
+#define GIP_CMD_LED_CAPABILITIES	0x01
+#define GIP_CMD_SET_LED_STATE		0x02
+
+/*
+ * The spec defines up to 64 extra buttons and 12 extra axes, but there's
+ * currently no good way to map all of them. For now, let's leave it as a much
+ * smaller number.and add more when we get a better way.
+ */
+#define MAX_GIP_FLIGHT_STICK_BUTTONS 10
+#define MAX_GIP_FLIGHT_STICK_AXES 1
+
+struct gip_flight_stick_capabilities_response {
+	uint8_t extra_button_count;
+	uint8_t extra_axis_count;
+	uint8_t led_count;
+	uint8_t max_global_led_gain;
+};
+
+static const unsigned int gip_flight_stick_extra_buttons[] = {
+	BTN_0,
+	BTN_1,
+	BTN_2,
+	BTN_3,
+	BTN_4,
+	BTN_5,
+	BTN_6,
+	BTN_7,
+	BTN_8,
+	BTN_9,
+};
+
+static const unsigned int gip_flight_stick_extra_axes[] = {
+	ABS_RUDDER,
+};
+
+static int gip_flight_stick_init(struct gip_attachment *attachment)
+{
+	int rc = gip_send_vendor_message(attachment, GIP_CMD_DEVICE_CAPABILITIES, 0, NULL, 0);
+
+	if (rc < 0)
+		return rc;
+
+	return GIP_INIT_NO_INPUT;
+}
+
+static int gip_setup_flight_stick_input(struct gip_attachment *attachment, struct input_dev* input)
+{
+	int i;
+	int rc = gip_driver_navigation.setup_input(attachment, input);
+
+	if (rc)
+		return rc;
+
+	input_set_capability(input, EV_KEY, BTN_TOP);
+	input_set_capability(input, EV_KEY, BTN_TOP2);
+	for (i = 0; i < attachment->extra_buttons && i < MAX_GIP_FLIGHT_STICK_BUTTONS; i++)
+		input_set_capability(input, EV_KEY, BTN_0 + i);
+	if (attachment->extra_buttons > MAX_GIP_FLIGHT_STICK_BUTTONS)
+		dev_info(GIP_DEV(attachment),
+			"Device has too many extra buttons, %i through %i ignored\n",
+			MAX_GIP_FLIGHT_STICK_BUTTONS,
+			attachment->extra_buttons + 1);
+	input_set_abs_params(input, ABS_X, -32768, 32767, 0, 0);
+	input_set_abs_params(input, ABS_Y, -32768, 32767, 0, 0);
+	input_set_abs_params(input, ABS_Z, -32768, 32767, 0, 0);
+	input_set_abs_params(input, ABS_THROTTLE, 0, 65535, 0, 0);
+	for (i = 0; i < attachment->extra_axes && i < MAX_GIP_FLIGHT_STICK_AXES; i++)
+		input_set_abs_params(input, gip_flight_stick_extra_axes[i], 0, 65535, 0, 0);
+
+	return 0;
+}
+
+static int gip_handle_flight_stick_report(struct gip_attachment *attachment,
+	struct input_dev *input, const uint8_t *bytes, int num_bytes)
+{
+	int32_t axis;
+	int rc = gip_driver_navigation.handle_input_report(attachment, input, bytes, num_bytes);
+	int i;
+
+	if (rc)
+		return rc;
+
+	if (num_bytes < 19)
+		return -EINVAL;
+
+	/* Fire 1 and 2 */
+	input_report_key(input, BTN_TOP, bytes[2] & BIT(0));
+	input_report_key(input, BTN_TOP2, bytes[2] & BIT(1));
+
+	for (i = 0; i < attachment->extra_buttons && i < MAX_GIP_FLIGHT_STICK_BUTTONS; i++) {
+		input_report_key(input, gip_flight_stick_extra_buttons[i],
+			bytes[i / 8 + 3] & BIT(i));
+	}
+
+	/*
+	 * Roll, pitch and yaw are signed. Throttle and any
+	 * extra axes are unsigned. All values are full-range.
+	 */
+	axis = bytes[11];
+	axis |= bytes[12] << 8;
+	input_report_abs(input, ABS_X, (int16_t) axis);
+
+	axis = bytes[13];
+	axis |= bytes[14] << 8;
+	input_report_abs(input, ABS_Y, (int16_t) axis);
+
+	axis = bytes[15];
+	axis |= bytes[16] << 8;
+	input_report_abs(input, ABS_Z, (int16_t) axis);
+
+	axis = bytes[17];
+	axis |= bytes[18] << 8;
+	input_report_abs(input, ABS_THROTTLE, axis);
+
+	for (i = 0; i < attachment->extra_axes && i < MAX_GIP_FLIGHT_STICK_AXES; i++) {
+		if (20 + i * 2 >= num_bytes)
+			break;
+
+		axis = bytes[19 + i * 2];
+		axis |= bytes[20 + i * 2] << 8;
+		input_report_abs(input, gip_flight_stick_extra_axes[i], axis);
+	}
+
+	return 0;
+}
+
+static int gip_handle_flight_stick_cmd_device_capabilities(struct gip_attachment *attachment,
+	const struct gip_header *header, const uint8_t *bytes, int num_bytes)
+{
+	const struct gip_flight_stick_capabilities_response *response =
+		(const struct gip_flight_stick_capabilities_response*)bytes;
+	struct input_dev *input;
+
+	rcu_read_lock();
+	input = rcu_dereference(attachment->input);
+	rcu_read_unlock();
+	if (input)
+		return 0;
+
+	if (num_bytes < 4)
+		return -EINVAL;
+
+	attachment->extra_axes = min(response->extra_axis_count, MAX_GIP_FLIGHT_STICK_AXES);
+	attachment->extra_buttons = min(response->extra_button_count, MAX_GIP_FLIGHT_STICK_BUTTONS);
+	return gip_setup_input_device(attachment);
+}
+
+const struct gip_driver gip_driver_flight_stick = {
+	.types = (const char* const[]) {
+		"Windows.Xbox.Input.FlightStick",
+		"Microsoft.Xbox.Input.FlightStick",
+		NULL
+	},
+	.guid = GUID_INIT(0x03f1a011, 0xefe9, 0x4cc1, 0x96, 0x9c,
+		0x38, 0xdc, 0x55, 0xf4, 0x04, 0xd0),
+
+	.probe = NULL,
+	.remove = NULL,
+	.init = gip_flight_stick_init,
+	.setup_input = gip_setup_flight_stick_input,
+	.handle_input_report = gip_handle_flight_stick_report,
+	.vendor_handlers = {
+		[GIP_CMD_DEVICE_CAPABILITIES] = gip_handle_flight_stick_cmd_device_capabilities,
+	},
+};
diff --git a/drivers/input/joystick/gip/gip.h b/drivers/input/joystick/gip/gip.h
index 3b5cab96dc0b7..6d70a69d99a39 100644
--- a/drivers/input/joystick/gip/gip.h
+++ b/drivers/input/joystick/gip/gip.h
@@ -323,4 +323,5 @@ extern const struct gip_driver gip_driver_gamepad;
 extern const struct gip_driver gip_driver_arcade_stick;
 extern const struct gip_driver gip_driver_wheel;
 extern const struct gip_driver gip_driver_trueforce_wheel;
+extern const struct gip_driver gip_driver_flight_stick;
 #endif
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 08/10] HID: Map more automobile simulation inputs
From: Vicki Pfau @ 2026-03-10  5:20 UTC (permalink / raw)
  To: Dmitry Torokhov, linux-input; +Cc: Vicki Pfau, Jiri Kosina
In-Reply-To: <20260310052017.1289494-1-vi@endrift.com>

The HID usage tables section 5.3 specify clutch and shifter values that had
previously been ignored. As the ABS_CLUTCH and ABS_SHIFTER bits now exist,
we should use them appropriately.

Signed-off-by: Vicki Pfau <vi@endrift.com>
Acked-by: Jiri Kosina <jkosina@suse.com>
---
 drivers/hid/hid-input.c | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/drivers/hid/hid-input.c b/drivers/hid/hid-input.c
index 2bbb645c2ff41..f14d9dd2e4fbb 100644
--- a/drivers/hid/hid-input.c
+++ b/drivers/hid/hid-input.c
@@ -801,6 +801,8 @@ static void hidinput_configure_usage(struct hid_input *hidinput, struct hid_fiel
 		case 0xbb: map_abs(ABS_THROTTLE); break;
 		case 0xc4: map_abs(ABS_GAS);      break;
 		case 0xc5: map_abs(ABS_BRAKE);    break;
+		case 0xc6: map_abs(ABS_CLUTCH);   break;
+		case 0xc7: map_abs(ABS_SHIFTER);  break;
 		case 0xc8: map_abs(ABS_WHEEL);    break;
 		default:   goto ignore;
 		}
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 09/10] Input: xbox_gip - Add wheel support
From: Vicki Pfau @ 2026-03-10  5:20 UTC (permalink / raw)
  To: Dmitry Torokhov, linux-input; +Cc: Vicki Pfau
In-Reply-To: <20260310052017.1289494-1-vi@endrift.com>

This adds preliminary support for racing wheel support in xbox_gip,
exposing them mapped to the newly added axes.

Signed-off-by: Vicki Pfau <vi@endrift.com>
---
 drivers/input/joystick/gip/Makefile      |   2 +-
 drivers/input/joystick/gip/gip-core.c    |   4 +-
 drivers/input/joystick/gip/gip-drivers.c |  10 +
 drivers/input/joystick/gip/gip-wheel.c   | 348 +++++++++++++++++++++++
 drivers/input/joystick/gip/gip.h         |   4 +
 5 files changed, 366 insertions(+), 2 deletions(-)
 create mode 100644 drivers/input/joystick/gip/gip-wheel.c

diff --git a/drivers/input/joystick/gip/Makefile b/drivers/input/joystick/gip/Makefile
index 94ce6462d7ab0..db6c9079c7e18 100644
--- a/drivers/input/joystick/gip/Makefile
+++ b/drivers/input/joystick/gip/Makefile
@@ -1,3 +1,3 @@
 # SPDX-License-Identifier: GPL-2.0-or-later
 obj-$(CONFIG_JOYSTICK_XBOX_GIP)	+= xbox-gip.o
-xbox-gip-y := gip-arcade-stick.o gip-core.o gip-drivers.o
+xbox-gip-y := gip-arcade-stick.o gip-core.o gip-drivers.o gip-wheel.o
diff --git a/drivers/input/joystick/gip/gip-core.c b/drivers/input/joystick/gip/gip-core.c
index 1164b689856e7..773d7705b7be8 100644
--- a/drivers/input/joystick/gip/gip-core.c
+++ b/drivers/input/joystick/gip/gip-core.c
@@ -10,7 +10,7 @@
  * - Event logging
  * - Sending fragmented messages
  * - Raw character device
- * - Wheel support
+ * - Wheel force feedback
  * - Flight stick support
  * - More arcade stick testing
  * - Arcade stick extra buttons
@@ -317,6 +317,8 @@ static const struct gip_driver* base_drivers[] = {
 	&gip_driver_navigation,
 	&gip_driver_gamepad,
 	&gip_driver_arcade_stick,
+	&gip_driver_trueforce_wheel,
+	&gip_driver_wheel,
 	NULL /* Sentinel */
 };
 
diff --git a/drivers/input/joystick/gip/gip-drivers.c b/drivers/input/joystick/gip/gip-drivers.c
index f5507e6215a94..e15f580d371d8 100644
--- a/drivers/input/joystick/gip/gip-drivers.c
+++ b/drivers/input/joystick/gip/gip-drivers.c
@@ -140,6 +140,11 @@ static int gip_setup_navigation_input(struct gip_attachment *attachment, struct
 	input_set_capability(input, EV_KEY, BTN_TR);
 	input_set_capability(input, EV_KEY, BTN_TL);
 
+	if (attachment->quirks & GIP_QUIRK_FORCE_GAMEPAD_SB) {
+		input_set_capability(input, EV_KEY, BTN_THUMBR);
+		input_set_capability(input, EV_KEY, BTN_THUMBL);
+	}
+
 	attachment->dpad_as_buttons = dpad_as_buttons;
 	if (attachment->dpad_as_buttons) {
 		input_set_capability(input, EV_KEY, BTN_DPAD_UP);
@@ -191,6 +196,11 @@ static int gip_handle_navigation_report(struct gip_attachment *attachment,
 		input_report_key(input, BTN_TR, bytes[1] & BIT(5));
 	}
 
+	if (attachment->quirks & GIP_QUIRK_FORCE_GAMEPAD_SB) {
+		input_report_key(input, BTN_THUMBL, bytes[1] & BIT(6));
+		input_report_key(input, BTN_THUMBR, bytes[1] & BIT(7));
+	}
+
 	return 0;
 }
 
diff --git a/drivers/input/joystick/gip/gip-wheel.c b/drivers/input/joystick/gip/gip-wheel.c
new file mode 100644
index 0000000000000..34e7795f16534
--- /dev/null
+++ b/drivers/input/joystick/gip/gip-wheel.c
@@ -0,0 +1,348 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Drivers for GIP racing wheel devices
+ *
+ * Copyright (c) 2025 Valve Software
+ *
+ * This driver is based on the Microsoft GIP spec at:
+ * https://aka.ms/gipdocs
+ * https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gipusb/e7c90904-5e21-426e-b9ad-d82adeee0dbc
+ */
+#include "gip.h"
+
+/* Wheel vendor messages */
+#define GIP_CMD_SET_APPLICATION_MEMORY	0x0b
+#define GIP_CMD_SET_EQUATIONS_STATES	0x0c
+#define GIP_CMD_SET_EQUATION 0x0d
+
+/* Wheel-specific flags */
+#define GIP_WHEEL_HAS_POWER		BIT(3)
+#define GIP_WHEEL_HANDBRAKE_CONN	BIT(4)
+#define GIP_WHEEL_CLUTCH_CONN		BIT(5)
+#define GIP_WHEEL_BRAKE_CONN		BIT(6)
+#define GIP_WHEEL_THROTTLE_CONN		BIT(7)
+
+#define GIP_HSHIFTER_NONE	0
+#define GIP_HSHIFTER_2POS	1 /* 2 position, no neutral */
+#define GIP_HSHIFTER_2POS_N	2 /* 2 position, neutral */
+#define GIP_HSHIFTER_RTL_1TL	3 /* Reverse top left, first top left */
+#define GIP_HSHIFTER_RTL_1BL	4 /* Reverse top left, first bottom left */
+#define GIP_HSHIFTER_RBL	5 /* Reverse bottom left */
+#define GIP_HSHIFTER_RTR	6 /* Reverse top right */
+#define GIP_HSHIFTER_RBR	7 /* Reverse bottom right */
+
+struct gip_wheel_info {
+	uint8_t shifter_type: 3;
+	uint8_t max_gear: 5;
+	uint16_t angle_setting;
+	uint16_t max_angle;
+	uint16_t max_throttle;
+	uint16_t max_brake;
+	uint16_t max_clutch;
+	uint8_t max_handbrake;
+	int8_t value_retries;
+};
+
+struct gip_initial_reports_request {
+	uint8_t type;
+	uint8_t data[2];
+};
+
+static int gip_wheel_probe(struct gip_attachment *attachment)
+{
+	struct gip_wheel_info *info = kzalloc(sizeof(*info), GFP_KERNEL);
+
+	if (!info)
+		return -ENOMEM;
+	attachment->driver_data = info;
+
+	return 0;
+}
+
+static void gip_wheel_remove(struct gip_attachment *attachment)
+{
+	kfree(attachment->driver_data);
+	attachment->driver_data = NULL;
+}
+
+static int gip_wheel_init(struct gip_attachment *attachment)
+{
+	struct gip_initial_reports_request request = { 0 };
+	int rc = gip_send_vendor_message(attachment,
+		GIP_CMD_INITIAL_REPORTS_REQUEST, 0, &request,
+		sizeof(request));
+
+	if (rc < 0)
+		return rc;
+
+	return GIP_INIT_NO_INPUT;
+}
+
+static int gip_setup_wheel_input(struct gip_attachment *attachment, struct input_dev* input)
+{
+	int rc = gip_driver_navigation.setup_input(attachment, input);
+	struct gip_wheel_info *info = attachment->driver_data;
+
+	if (rc < 0)
+		return rc;
+
+	if (!info)
+		return -ENODEV;
+
+	input_set_abs_params(input, ABS_WHEEL, -info->max_angle - 1, info->max_angle, 0, 0);
+	input_abs_set_res(input, ABS_WHEEL, info->angle_setting);
+	if (info->max_throttle)
+		input_set_abs_params(input, ABS_GAS, 0, info->max_throttle, 0, 0);
+
+	if (info->max_brake)
+		input_set_abs_params(input, ABS_BRAKE, 0, info->max_brake, 0, 0);
+
+	if (info->max_clutch)
+		input_set_abs_params(input, ABS_CLUTCH, 0, info->max_clutch, 0, 0);
+
+	if (info->max_handbrake)
+		input_set_abs_params(input, ABS_HANDBRAKE, 0, info->max_handbrake, 0, 0);
+
+	if (info->shifter_type)
+		input_set_abs_params(input, ABS_SHIFTER, -1, info->max_gear, 0, 0);
+
+	return 0;
+}
+
+static int gip_handle_wheel_ll_input_report(struct gip_attachment *attachment,
+	const struct gip_header *header, const uint8_t *bytes, int num_bytes)
+{
+	int rc = 0;
+	struct gip_wheel_info *info = attachment->driver_data;
+
+	if (num_bytes < 17)
+		return -EINVAL;
+
+	if (!info)
+		return -ENODEV;
+
+	info->max_gear = bytes[11] & 0x1F;
+	info->shifter_type = bytes[11] >> 5;
+	info->angle_setting = bytes[13];
+	info->angle_setting |= bytes[14] << 8;
+
+	if (info->angle_setting && info->max_angle) {
+		if (info->value_retries-- > 0)
+			return 0;
+
+		rc = gip_setup_input_device(attachment);
+	} else {
+		return 0;
+	}
+
+	if (rc < 0)
+		return rc;
+
+	/* Now that we're done configuring, fall back to default handler */
+	attachment->vendor_handlers[GIP_LL_INPUT_REPORT] = NULL;
+
+	return 0;
+}
+
+static int gip_handle_wheel_report(struct gip_attachment *attachment,
+	struct input_dev *input, const uint8_t *bytes, int num_bytes)
+{
+	int32_t axis;
+	uint8_t connections;
+	struct gip_wheel_info *info = attachment->driver_data;
+	int rc = gip_driver_navigation.handle_input_report(attachment, input, bytes, num_bytes);
+
+	if (rc < 0)
+		return rc;
+
+	if (!info)
+		return -ENODEV;
+
+	if (num_bytes < 17)
+		return -EINVAL;
+
+	axis = bytes[2];
+	axis |= bytes[3] << 8;
+	input_report_abs(input, ABS_WHEEL, axis - 0x8000);
+
+	connections = bytes[16];
+
+	if (attachment->quirks & GIP_QUIRK_WHEEL_FORCE_HANDBRAKE)
+		connections |= GIP_WHEEL_HANDBRAKE_CONN;
+
+	if (connections & GIP_WHEEL_THROTTLE_CONN) {
+		axis = bytes[4];
+		axis |= bytes[5] << 8;
+		input_report_abs(input, ABS_GAS, axis);
+	} else {
+		input_report_abs(input, ABS_GAS, 0);
+	}
+
+	if (connections & GIP_WHEEL_BRAKE_CONN) {
+		axis = bytes[6];
+		axis |= bytes[7] << 8;
+		input_report_abs(input, ABS_BRAKE, axis);
+	} else {
+		input_report_abs(input, ABS_BRAKE, 0);
+	}
+
+	if (connections & GIP_WHEEL_CLUTCH_CONN) {
+		axis = bytes[8];
+		axis |= bytes[9] << 8;
+		input_report_abs(input, ABS_CLUTCH, axis);
+	} else {
+		input_report_abs(input, ABS_CLUTCH, 0);
+	}
+
+	if (connections & GIP_WHEEL_HANDBRAKE_CONN)
+		input_report_abs(input, ABS_HANDBRAKE, bytes[10]);
+	else
+		input_report_abs(input, ABS_HANDBRAKE, 0);
+
+	if (info->shifter_type)
+		input_report_abs(input, ABS_SHIFTER, (int8_t)bytes[12]);
+
+	return 0;
+}
+
+static int gip_handle_wheel_ll_static_configuration(struct gip_attachment *attachment,
+	const struct gip_header *header, const uint8_t *bytes, int num_bytes)
+{
+	struct gip_wheel_info *info = attachment->driver_data;
+
+	if (!info)
+		return -ENODEV;
+
+	if (num_bytes < 11)
+		return -EINVAL;
+
+	info->max_angle = BIT(bytes[0]) - 1;
+	info->max_throttle = BIT(bytes[1]) - 1;
+	info->max_brake = BIT(bytes[2]) - 1;
+	info->max_clutch = BIT(bytes[3]) - 1;
+	info->max_handbrake = BIT(bytes[4]) - 1;
+
+	if (info->angle_setting && info->max_angle && info->value_retries <= 0)
+		return gip_setup_input_device(attachment);
+
+	return 0;
+}
+
+const struct gip_driver gip_driver_wheel = {
+	.types = (const char* const[]) {
+		"Windows.Xbox.Input.Wheel",
+		"Microsoft.Xbox.Input.Wheel",
+		NULL
+	},
+	.guid = GUID_INIT(0x646979cf, 0x6b71, 0x4e96, 0x8d, 0xf9,
+		0x59, 0xe3, 0x98, 0xd7, 0x42, 0x0c),
+
+	.quirks = (const struct gip_quirks[]) {
+		/* Thrustmaster T128X GIP Racing Wheel */
+		{ 0x044f, 0xb69c, 0,
+			.quirks = GIP_QUIRK_FORCE_GAMEPAD_SB | GIP_QUIRK_WHEEL_FORCE_HANDBRAKE, },
+
+		{0},
+	},
+
+	.probe = gip_wheel_probe,
+	.remove = gip_wheel_remove,
+	.init = gip_wheel_init,
+	.setup_input = gip_setup_wheel_input,
+	.handle_input_report = gip_handle_wheel_report,
+	.vendor_handlers = {
+		[GIP_LL_INPUT_REPORT] = gip_handle_wheel_ll_input_report,
+		[GIP_LL_STATIC_CONFIGURATION] = gip_handle_wheel_ll_static_configuration,
+	},
+};
+
+struct gip_trueforce_wheel_state {
+	struct gip_wheel_info wheel_info; /* This field must be first for type punning */
+	int8_t dial;
+};
+
+static int gip_trueforce_wheel_probe(struct gip_attachment *attachment)
+{
+	struct gip_trueforce_wheel_state *state = kzalloc(sizeof(*state), GFP_KERNEL);
+
+	if (!state)
+		return -ENOMEM;
+	attachment->driver_data = state;
+	/* The shifter won't show up until the second input report */
+	state->wheel_info.value_retries = 1;
+
+	return 0;
+}
+
+static void gip_trueforce_wheel_remove(struct gip_attachment *attachment)
+{
+	kfree(attachment->driver_data);
+	attachment->driver_data = NULL;
+}
+
+static int gip_setup_trueforce_wheel_input(struct gip_attachment *attachment,
+	struct input_dev* input)
+{
+	int rc = gip_driver_wheel.setup_input(attachment, input);
+
+	if (rc < 0)
+		return rc;
+
+	input_set_capability(input, EV_KEY, BTN_THUMBL);
+	input_set_capability(input, EV_KEY, BTN_THUMBR);
+	input_set_capability(input, EV_KEY, KEY_KPPLUS);
+	input_set_capability(input, EV_KEY, KEY_KPMINUS);
+	input_set_capability(input, EV_KEY, KEY_KPENTER);
+	input_set_capability(input, EV_REL, REL_DIAL);
+
+	return 0;
+}
+
+static int gip_handle_trueforce_wheel_report(struct gip_attachment *attachment,
+	struct input_dev *input, const uint8_t *bytes, int num_bytes)
+{
+	int rc = gip_driver_wheel.handle_input_report(attachment, input, bytes, num_bytes);
+	struct gip_trueforce_wheel_state *state = attachment->driver_data;
+	int dial;
+
+	if (rc < 0)
+		return rc;
+
+	if (num_bytes < 18)
+		return -EINVAL;
+
+	dial = bytes[17] >> 5;
+
+	input_report_key(input, BTN_THUMBL, bytes[17] & BIT(0));
+	input_report_key(input, BTN_THUMBR, bytes[17] & BIT(1));
+	input_report_key(input, KEY_KPPLUS, bytes[17] & BIT(2));
+	input_report_key(input, KEY_KPMINUS, bytes[17] & BIT(3));
+	input_report_key(input, KEY_KPENTER, bytes[17] & BIT(4));
+	if (dial == 0 && state->dial == 7)
+		input_report_rel(input, REL_DIAL, -1);
+	else if (dial == 7 && state->dial == 0)
+		input_report_rel(input, REL_DIAL, 1);
+	else
+		input_report_rel(input, REL_DIAL,
+			state->dial - dial);
+	state->dial = dial;
+
+	return 0;
+}
+
+const struct gip_driver gip_driver_trueforce_wheel = {
+	.types = (const char *const[]) { "Logi.Xbox.Input.TrueForceWheel", NULL },
+	.guid = GUID_INIT(0x6ca319e5, 0x0bc0, 0x41be, 0x83, 0x19,
+		0x6b, 0xb7, 0x10, 0x81, 0xec, 0x55),
+
+	.probe = gip_trueforce_wheel_probe,
+	.remove = gip_trueforce_wheel_remove,
+	.init = gip_wheel_init,
+	.setup_input = gip_setup_trueforce_wheel_input,
+	.handle_input_report = gip_handle_trueforce_wheel_report,
+	.vendor_handlers = {
+		[GIP_LL_INPUT_REPORT] = gip_handle_wheel_ll_input_report,
+		[GIP_LL_STATIC_CONFIGURATION] = gip_handle_wheel_ll_static_configuration,
+	},
+};
+
diff --git a/drivers/input/joystick/gip/gip.h b/drivers/input/joystick/gip/gip.h
index 6e5ab8f357aaa..3b5cab96dc0b7 100644
--- a/drivers/input/joystick/gip/gip.h
+++ b/drivers/input/joystick/gip/gip.h
@@ -31,6 +31,8 @@
 #define GIP_QUIRK_NO_HELLO		BIT(0)
 #define GIP_QUIRK_NO_IMPULSE_VIBRATION	BIT(1)
 #define GIP_QUIRK_SWAP_LB_RB		BIT(2)
+#define GIP_QUIRK_FORCE_GAMEPAD_SB	BIT(3)
+#define GIP_QUIRK_WHEEL_FORCE_HANDBRAKE	BIT(31)
 
 #define GIP_FEATURE_CONTROLLER				BIT(0)
 #define GIP_FEATURE_CONSOLE_FUNCTION_MAP		BIT(1)
@@ -319,4 +321,6 @@ int gip_send_vendor_message(struct gip_attachment *attachment,
 extern const struct gip_driver gip_driver_navigation;
 extern const struct gip_driver gip_driver_gamepad;
 extern const struct gip_driver gip_driver_arcade_stick;
+extern const struct gip_driver gip_driver_wheel;
+extern const struct gip_driver gip_driver_trueforce_wheel;
 #endif
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 06/10] Input: xbox_gip - Add arcade stick support
From: Vicki Pfau @ 2026-03-10  5:20 UTC (permalink / raw)
  To: Dmitry Torokhov, linux-input; +Cc: Vicki Pfau
In-Reply-To: <20260310052017.1289494-1-vi@endrift.com>

This adds support for the arcade stick device type. Currently I'm only
aware of one Xbox controller that uses this device type, the Razer Atrox,
so testing coverage is limited.

Signed-off-by: Vicki Pfau <vi@endrift.com>
---
 drivers/input/joystick/gip/Makefile           |   2 +-
 drivers/input/joystick/gip/gip-arcade-stick.c | 169 ++++++++++++++++++
 drivers/input/joystick/gip/gip-core.c         |   6 +-
 drivers/input/joystick/gip/gip.h              |   4 +-
 4 files changed, 176 insertions(+), 5 deletions(-)
 create mode 100644 drivers/input/joystick/gip/gip-arcade-stick.c

diff --git a/drivers/input/joystick/gip/Makefile b/drivers/input/joystick/gip/Makefile
index a75e0cace0f92..94ce6462d7ab0 100644
--- a/drivers/input/joystick/gip/Makefile
+++ b/drivers/input/joystick/gip/Makefile
@@ -1,3 +1,3 @@
 # SPDX-License-Identifier: GPL-2.0-or-later
 obj-$(CONFIG_JOYSTICK_XBOX_GIP)	+= xbox-gip.o
-xbox-gip-y := gip-core.o gip-drivers.o
+xbox-gip-y := gip-arcade-stick.o gip-core.o gip-drivers.o
diff --git a/drivers/input/joystick/gip/gip-arcade-stick.c b/drivers/input/joystick/gip/gip-arcade-stick.c
new file mode 100644
index 0000000000000..c9ee3483e02d8
--- /dev/null
+++ b/drivers/input/joystick/gip/gip-arcade-stick.c
@@ -0,0 +1,169 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Drivers for GIP arcade sticks
+ *
+ * Copyright (c) 2025 Valve Software
+ *
+ * This driver is based on the Microsoft GIP spec at:
+ * https://aka.ms/gipdocs
+ * https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gipusb/e7c90904-5e21-426e-b9ad-d82adeee0dbc
+ */
+#include "gip.h"
+
+enum gip_arcade_stick_vibration_motor_support {
+	GIP_VIBRATION_NO_MOTOR = 0,
+	GIP_VIBRATION_SINGLE_MOTOR = 1,
+	GIP_VIBRATION_DUAL_MOTOR = 2,
+};
+
+struct gip_arcade_stick_info {
+	uint8_t vibration_motor;
+	uint8_t actuator_bits;
+};
+
+struct gip_arcade_stick_static_coniguration {
+	uint8_t button_count;
+	uint8_t report_version;
+	uint8_t vibration_motor_support;
+	uint8_t actuator_bits;
+};
+
+static int gip_arcade_stick_probe(struct gip_attachment *attachment)
+{
+	struct gip_arcade_stick_info *info = kzalloc(sizeof(*info), GFP_KERNEL);
+
+	if (!info)
+		return -ENOMEM;
+
+	attachment->quirks |= GIP_QUIRK_SWAP_LB_RB;
+	attachment->driver_data = info;
+
+	return 0;
+}
+
+static void gip_arcade_stick_remove(struct gip_attachment *attachment)
+{
+	kfree(attachment->driver_data);
+	attachment->driver_data = NULL;
+}
+
+static int gip_init_arcade_stick(struct gip_attachment *attachment)
+{
+	if (gip_supports_vendor_message(attachment, GIP_CMD_INITIAL_REPORTS_REQUEST, false)) {
+		uint8_t request = GIP_LL_STATIC_CONFIGURATION;
+		int rc = gip_send_vendor_message(attachment, GIP_CMD_INITIAL_REPORTS_REQUEST, 0,
+			&request, sizeof(request));
+
+		if (rc < 0)
+			return rc;
+
+		return GIP_INIT_NO_INPUT;
+	}
+
+	return 0;
+}
+
+static int gip_setup_arcade_stick_input(struct gip_attachment *attachment, struct input_dev* input)
+{
+	struct gip_arcade_stick_info *info = attachment->driver_data;
+	int rc;
+
+	if (!info)
+		return -ENODEV;
+
+	rc = gip_driver_navigation.setup_input(attachment, input);
+	if (rc < 0)
+		return rc;
+
+	if (info->actuator_bits > 0) {
+		input_set_abs_params(input, ABS_X, 0, (1 << info->actuator_bits) - 1, 0, 0);
+		input_set_abs_params(input, ABS_Y, 0, (1 << info->actuator_bits) - 1, 0, 0);
+	}
+
+	if (attachment->extra_buttons >= 1)
+		input_set_capability(input, EV_KEY, BTN_TR2);
+
+	if (attachment->extra_buttons >= 2)
+		input_set_capability(input, EV_KEY, BTN_TL2);
+	return 0;
+}
+
+static int gip_handle_arcade_stick_report(struct gip_attachment *attachment,
+	struct input_dev* input, const uint8_t *bytes, int num_bytes)
+{
+	struct gip_arcade_stick_info *info = attachment->driver_data;
+	int16_t axis;
+	int rc;
+
+	if (!info)
+		return -ENODEV;
+
+	rc = gip_driver_navigation.handle_input_report(attachment, input, bytes, num_bytes);
+	if (rc < 0)
+		return rc;
+
+	if (num_bytes < 6) {
+		dev_dbg(GIP_DEV(attachment), "Discarding too-short input report\n");
+		return -EINVAL;
+	}
+
+	if (info->actuator_bits > 0) {
+		axis = bytes[2];
+		axis |= bytes[3] << 8;
+		input_report_abs(input, ABS_X, axis);
+
+		axis = bytes[4];
+		axis |= bytes[5] << 8;
+		input_report_abs(input, ABS_Y, axis);
+	}
+
+	if (num_bytes >= 19) {
+		/* Extra button 6 */
+		input_report_key(input, BTN_TR2, bytes[18] & BIT(6));
+		/* Extra button 7 */
+		input_report_key(input, BTN_TL2, bytes[18] & BIT(7));
+	}
+
+	return 0;
+}
+
+static int gip_handle_arcade_stick_ll_static_configuration(struct gip_attachment *attachment,
+	const struct gip_header *header, const uint8_t *bytes, int num_bytes)
+{
+	const struct gip_arcade_stick_static_coniguration *config =
+		(const struct gip_arcade_stick_static_coniguration*)bytes;
+	struct gip_arcade_stick_info *info = attachment->driver_data;
+
+	if (!info)
+		return -ENODEV;
+
+	if (num_bytes < 4)
+		return -EINVAL;
+
+	attachment->extra_buttons = clamp(config->button_count, 6, 38) - 6;
+	info->actuator_bits = min(config->actuator_bits, 8);
+
+	if (config->vibration_motor_support == GIP_VIBRATION_NO_MOTOR)
+		attachment->features &= ~GIP_FEATURE_MOTOR_CONTROL;
+
+	return gip_setup_input_device(attachment);
+}
+
+const struct gip_driver gip_driver_arcade_stick = {
+	.types = (const char* const[]) {
+		"Windows.Xbox.Input.ArcadeStick",
+		"Microsoft.Xbox.Input.ArcadeStick",
+		NULL
+	},
+	.guid = GUID_INIT(0x332054cc, 0xa34b, 0x41d5, 0xa3, 0x4a,
+		0xa6, 0xa6, 0x71, 0x1e, 0xc4, 0xb3),
+
+	.probe = gip_arcade_stick_probe,
+	.remove = gip_arcade_stick_remove,
+	.init = gip_init_arcade_stick,
+	.setup_input = gip_setup_arcade_stick_input,
+	.handle_input_report = gip_handle_arcade_stick_report,
+	.vendor_handlers = {
+		[GIP_LL_STATIC_CONFIGURATION] = gip_handle_arcade_stick_ll_static_configuration,
+	},
+};
diff --git a/drivers/input/joystick/gip/gip-core.c b/drivers/input/joystick/gip/gip-core.c
index 89f8e0ed47344..1164b689856e7 100644
--- a/drivers/input/joystick/gip/gip-core.c
+++ b/drivers/input/joystick/gip/gip-core.c
@@ -12,7 +12,8 @@
  * - Raw character device
  * - Wheel support
  * - Flight stick support
- * - Arcade stick support
+ * - More arcade stick testing
+ * - Arcade stick extra buttons
  * - Split into driver-per-attachment GIP-as-a-bus approach drivers
  *
  * This driver is based on the Microsoft GIP spec at:
@@ -315,6 +316,7 @@ struct gip_direct_motor {
 static const struct gip_driver* base_drivers[] = {
 	&gip_driver_navigation,
 	&gip_driver_gamepad,
+	&gip_driver_arcade_stick,
 	NULL /* Sentinel */
 };
 
@@ -1344,7 +1346,7 @@ static int gip_handle_command_raw_report(struct gip_attachment *attachment,
 	return 0;
 }
 
-static int gip_setup_input_device(struct gip_attachment *attachment)
+int gip_setup_input_device(struct gip_attachment *attachment)
 {
 	struct input_dev *input;
 	int rc;
diff --git a/drivers/input/joystick/gip/gip.h b/drivers/input/joystick/gip/gip.h
index dd77f16b00fd8..6e5ab8f357aaa 100644
--- a/drivers/input/joystick/gip/gip.h
+++ b/drivers/input/joystick/gip/gip.h
@@ -307,6 +307,8 @@ static inline struct device *gip_device_dev(struct gip_device *device)
 	return &device->udev->dev;
 }
 
+int gip_setup_input_device(struct gip_attachment *attachment);
+
 bool gip_supports_vendor_message(struct gip_attachment *attachment, uint8_t command, bool upstream);
 
 int gip_send_system_message(struct gip_attachment *attachment,
@@ -317,6 +319,4 @@ int gip_send_vendor_message(struct gip_attachment *attachment,
 extern const struct gip_driver gip_driver_navigation;
 extern const struct gip_driver gip_driver_gamepad;
 extern const struct gip_driver gip_driver_arcade_stick;
-extern const struct gip_driver gip_driver_wheel;
-extern const struct gip_driver gip_driver_flight_stick;
 #endif
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 07/10] Input: Add ABS_CLUTCH, HANDBRAKE, and SHIFTER
From: Vicki Pfau @ 2026-03-10  5:20 UTC (permalink / raw)
  To: Dmitry Torokhov, linux-input; +Cc: Vicki Pfau
In-Reply-To: <20260310052017.1289494-1-vi@endrift.com>

Add new absolute axes for racing game controllers

Signed-off-by: Vicki Pfau <vi@endrift.com>
---
 drivers/hid/hid-debug.c                | 16 +++++++++-------
 include/uapi/linux/input-event-codes.h |  3 +++
 2 files changed, 12 insertions(+), 7 deletions(-)

diff --git a/drivers/hid/hid-debug.c b/drivers/hid/hid-debug.c
index 337d2dc81b4ca..23aa2787e55b1 100644
--- a/drivers/hid/hid-debug.c
+++ b/drivers/hid/hid-debug.c
@@ -3505,13 +3505,15 @@ static const char *absolutes[ABS_CNT] = {
 	[ABS_RY] = "Ry",		[ABS_RZ] = "Rz",
 	[ABS_THROTTLE] = "Throttle",	[ABS_RUDDER] = "Rudder",
 	[ABS_WHEEL] = "Wheel",		[ABS_GAS] = "Gas",
-	[ABS_BRAKE] = "Brake",		[ABS_HAT0X] = "Hat0X",
-	[ABS_HAT0Y] = "Hat0Y",		[ABS_HAT1X] = "Hat1X",
-	[ABS_HAT1Y] = "Hat1Y",		[ABS_HAT2X] = "Hat2X",
-	[ABS_HAT2Y] = "Hat2Y",		[ABS_HAT3X] = "Hat3X",
-	[ABS_HAT3Y] = "Hat 3Y",		[ABS_PRESSURE] = "Pressure",
-	[ABS_DISTANCE] = "Distance",	[ABS_TILT_X] = "XTilt",
-	[ABS_TILT_Y] = "YTilt",		[ABS_TOOL_WIDTH] = "ToolWidth",
+	[ABS_BRAKE] = "Brake",		[ABS_CLUTCH] = "Clutch",
+	[ABS_HANDBRAKE] = "Handbrake",	[ABS_SHIFTER] = "Shifter",
+	[ABS_HAT0X] = "Hat0X",		[ABS_HAT0Y] = "Hat0Y",
+	[ABS_HAT1X] = "Hat1X",		[ABS_HAT1Y] = "Hat1Y",
+	[ABS_HAT2X] = "Hat2X",		[ABS_HAT2Y] = "Hat2Y",
+	[ABS_HAT3X] = "Hat3X",		[ABS_HAT3Y] = "Hat3Y",
+	[ABS_PRESSURE] = "Pressure",	[ABS_DISTANCE] = "Distance",
+	[ABS_TILT_X] = "XTilt",		[ABS_TILT_Y] = "YTilt",
+	[ABS_TOOL_WIDTH] = "ToolWidth",
 	[ABS_VOLUME] = "Volume",	[ABS_PROFILE] = "Profile",
 	[ABS_MISC] = "Misc",
 	[ABS_MT_SLOT] = "MTSlot",
diff --git a/include/uapi/linux/input-event-codes.h b/include/uapi/linux/input-event-codes.h
index 30f3c9eaafaad..34245cd6f22cf 100644
--- a/include/uapi/linux/input-event-codes.h
+++ b/include/uapi/linux/input-event-codes.h
@@ -875,6 +875,9 @@
 #define ABS_WHEEL		0x08
 #define ABS_GAS			0x09
 #define ABS_BRAKE		0x0a
+#define ABS_CLUTCH		0x0b
+#define ABS_HANDBRAKE		0x0c
+#define ABS_SHIFTER		0x0d
 #define ABS_HAT0X		0x10
 #define ABS_HAT0Y		0x11
 #define ABS_HAT1X		0x12
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 05/10] Input: xbox_gip - Add battery support
From: Vicki Pfau @ 2026-03-10  5:19 UTC (permalink / raw)
  To: Dmitry Torokhov, linux-input; +Cc: Vicki Pfau
In-Reply-To: <20260310052017.1289494-1-vi@endrift.com>

Controllers are required to give information about the battery in the
controller, if present. This patch exposes that information as a
power_supply device.

Signed-off-by: Vicki Pfau <vi@endrift.com>
---
 drivers/input/joystick/gip/gip-core.c | 97 +++++++++++++++++++++++++++
 drivers/input/joystick/gip/gip.h      |  3 +
 2 files changed, 100 insertions(+)

diff --git a/drivers/input/joystick/gip/gip-core.c b/drivers/input/joystick/gip/gip-core.c
index 7355737b29d19..89f8e0ed47344 100644
--- a/drivers/input/joystick/gip/gip-core.c
+++ b/drivers/input/joystick/gip/gip-core.c
@@ -262,6 +262,13 @@ static const struct gip_audio_format gip_audio_format_table[MAX_GIP_AUDIO_FORMAT
 };
 
 
+static enum power_supply_property gip_battery_props[] = {
+	POWER_SUPPLY_PROP_PRESENT,
+	POWER_SUPPLY_PROP_CAPACITY_LEVEL,
+	POWER_SUPPLY_PROP_SCOPE,
+	POWER_SUPPLY_PROP_STATUS,
+};
+
 static const struct gip_quirks base_quirks[] = {
 	/* PDP Rock Candy */
 	{ 0x0e6f, 0x0246, 0, .quirks = GIP_QUIRK_NO_HELLO },
@@ -1205,6 +1212,92 @@ static int gip_guide_led_probe(struct gip_attachment *attachment, struct device
 }
 #endif
 
+static int gip_battery_get_property(struct power_supply *psy,
+	enum power_supply_property psp, union power_supply_propval *val)
+{
+	struct gip_attachment *attachment = power_supply_get_drvdata(psy);
+
+	guard(mutex)(&attachment->lock);
+	switch (psp) {
+	case POWER_SUPPLY_PROP_PRESENT:
+		val->intval = attachment->status.base.battery_type != GIP_BATTERY_ABSENT;
+		break;
+	case POWER_SUPPLY_PROP_SCOPE:
+		val->intval = POWER_SUPPLY_SCOPE_DEVICE;
+		break;
+	case POWER_SUPPLY_PROP_STATUS:
+		if (attachment->status.base.battery_type == GIP_BATTERY_ABSENT) {
+			val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING;
+		} else {
+			switch (attachment->status.base.charge) {
+			case GIP_CHARGING:
+				if (attachment->status.base.battery_level == GIP_BATTERY_FULL)
+					val->intval = POWER_SUPPLY_STATUS_FULL;
+				else
+					val->intval = POWER_SUPPLY_STATUS_CHARGING;
+				break;
+			case GIP_NOT_CHARGING:
+				val->intval = POWER_SUPPLY_STATUS_DISCHARGING;
+				break;
+			case GIP_CHARGE_ERROR:
+			default:
+				val->intval = POWER_SUPPLY_STATUS_UNKNOWN;
+				break;
+			}
+		}
+		break;
+	case POWER_SUPPLY_PROP_CAPACITY_LEVEL:
+		if (attachment->status.base.battery_type == GIP_BATTERY_ABSENT) {
+			val->intval = POWER_SUPPLY_CAPACITY_LEVEL_FULL;
+		} else {
+			switch (attachment->status.base.battery_level) {
+			case GIP_BATTERY_CRITICAL:
+				val->intval = POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL;
+				break;
+			case GIP_BATTERY_LOW:
+				val->intval = POWER_SUPPLY_CAPACITY_LEVEL_LOW;
+				break;
+			case GIP_BATTERY_MEDIUM:
+				val->intval = POWER_SUPPLY_CAPACITY_LEVEL_NORMAL;
+				break;
+			case GIP_BATTERY_FULL:
+				val->intval = POWER_SUPPLY_CAPACITY_LEVEL_FULL;
+				break;
+			default:
+				val->intval = POWER_SUPPLY_CAPACITY_LEVEL_UNKNOWN;
+				break;
+			}
+			break;
+		}
+		break;
+	default:
+		return -EINVAL;
+	}
+	return 0;
+}
+
+static int gip_battery_create(struct gip_attachment *attachment, struct device *dev)
+{
+	struct power_supply_config supply_config = { .drv_data = attachment, };
+
+	attachment->battery_desc.properties = gip_battery_props;
+	attachment->battery_desc.num_properties = ARRAY_SIZE(gip_battery_props);
+	attachment->battery_desc.get_property = gip_battery_get_property;
+	attachment->battery_desc.type = POWER_SUPPLY_TYPE_BATTERY;
+	attachment->battery_desc.name = devm_kasprintf(dev, GFP_KERNEL,
+		"gip-battery-%s", dev_name(dev));
+
+	if (!attachment->battery_desc.name)
+		return -ENOMEM;
+
+	attachment->battery = devm_power_supply_register(dev,
+		&attachment->battery_desc, &supply_config);
+	if (IS_ERR(attachment->battery))
+		return PTR_ERR(attachment->battery);
+
+	return power_supply_powers(attachment->battery, dev);
+}
+
 static bool gip_send_set_device_state(struct gip_attachment *attachment, uint8_t state)
 {
 	uint8_t buffer[] = { state };
@@ -1316,6 +1409,9 @@ static int gip_setup_input_device(struct gip_attachment *attachment)
 	if (rc)
 		dev_err(GIP_DEV(attachment), "Failed to register LEDs: %d\n", rc);
 #endif
+	rc = gip_battery_create(attachment, &input->dev);
+	if (rc)
+		dev_err(GIP_DEV(attachment), "Failed to register battery: %d\n", rc);
 
 	return 0;
 
@@ -2598,6 +2694,7 @@ static int gip_shutdown(struct gip_device *device)
 			hdev = rcu_dereference(attachment->hdev);
 			rcu_read_unlock();
 
+			attachment->battery = NULL;
 			rcu_assign_pointer(attachment->input, NULL);
 			rcu_assign_pointer(attachment->hdev, NULL);
 			synchronize_rcu();
diff --git a/drivers/input/joystick/gip/gip.h b/drivers/input/joystick/gip/gip.h
index c9d1c4f16c760..dd77f16b00fd8 100644
--- a/drivers/input/joystick/gip/gip.h
+++ b/drivers/input/joystick/gip/gip.h
@@ -17,6 +17,7 @@
 #include <linux/led-class-multicolor.h>
 #endif
 #include <linux/rcupdate.h>
+#include <linux/power_supply.h>
 #include <linux/usb/input.h>
 
 #define BASE_GIP_MTU 64
@@ -212,6 +213,8 @@ struct gip_attachment {
 	} guide_led;
 #endif
 
+	struct power_supply *battery;
+	struct power_supply_desc battery_desc;
 	struct gip_extended_status status;
 
 	enum gip_elite_button_format xbe_format;
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 02/10] Input: xpad - Remove Xbox One support
From: Vicki Pfau @ 2026-03-10  5:19 UTC (permalink / raw)
  To: Dmitry Torokhov, linux-input; +Cc: Vicki Pfau
In-Reply-To: <20260310052017.1289494-1-vi@endrift.com>

It has been superseded by xbox_gip. As the new driver is already at feature
parity, removing the fairly rough Xbox One support from xpad is safe and
will prevent any potential conflicts.

Signed-off-by: Vicki Pfau <vi@endrift.com>
---
 Documentation/input/devices/xpad.rst |  17 +-
 drivers/input/joystick/xpad.c        | 634 +--------------------------
 2 files changed, 11 insertions(+), 640 deletions(-)

diff --git a/Documentation/input/devices/xpad.rst b/Documentation/input/devices/xpad.rst
index a480bc781565e..fd2afdd7b4059 100644
--- a/Documentation/input/devices/xpad.rst
+++ b/Documentation/input/devices/xpad.rst
@@ -2,19 +2,19 @@
 xpad - Linux USB driver for Xbox compatible controllers
 =======================================================
 
-This driver exposes all first-party and third-party Xbox compatible
-controllers. It has a long history and has enjoyed considerable usage
-as Windows' xinput library caused most PC games to focus on Xbox
-controller compatibility.
+This driver exposes all first-party and third-party Xbox and Xbox 360
+compatible controllers. It has a long history and has enjoyed considerable
+usage as Windows' xinput library caused most PC games to focus on Xbox
+controller compatibility. Xbox One/Series controller support has been
+superseded by the xbox_gip driver, which specializes in the Gaming Input
+Protocl that is introduced on the Xbox One.
 
 Due to backwards compatibility all buttons are reported as digital.
 This only affects Original Xbox controllers. All later controller models
 have only digital face buttons.
 
 Rumble is supported on some models of Xbox 360 controllers but not of
-Original Xbox controllers nor on Xbox One controllers. As of writing
-the Xbox One's rumble protocol has not been reverse-engineered but in
-the future could be supported.
+Original Xbox controllers.
 
 
 Notes
@@ -98,9 +98,6 @@ All generations of Xbox controllers speak USB over the wire.
 - Wireless Xbox 360 controllers require a 'Xbox 360 Wireless Gaming Receiver
   for Windows'
 - Wired Xbox 360 controllers use standard USB connectors.
-- Xbox One controllers can be wireless but speak Wi-Fi Direct and are not
-  yet supported.
-- Xbox One controllers can be wired and use standard Micro-USB connectors.
 
 
 
diff --git a/drivers/input/joystick/xpad.c b/drivers/input/joystick/xpad.c
index d72e89c25e503..d5957f8ac4f77 100644
--- a/drivers/input/joystick/xpad.c
+++ b/drivers/input/joystick/xpad.c
@@ -17,7 +17,6 @@
  *  - the iForce driver    drivers/char/joystick/iforce.c
  *  - the skeleton-driver  drivers/usb/usb-skeleton.c
  *  - Xbox 360 information http://www.free60.org/wiki/Gamepad
- *  - Xbox One information https://github.com/quantus/xbox-one-controller-protocol
  *
  * Thanks to:
  *  - ITO Takayuki for providing essential xpad information on his website
@@ -80,10 +79,6 @@
 #define MAP_DPAD_TO_BUTTONS		BIT(0)
 #define MAP_TRIGGERS_TO_BUTTONS		BIT(1)
 #define MAP_STICKS_TO_NULL		BIT(2)
-#define MAP_SHARE_BUTTON		BIT(3)
-#define MAP_PADDLES			BIT(4)
-#define MAP_PROFILE_BUTTON		BIT(5)
-#define MAP_SHARE_OFFSET		BIT(6)
 
 #define DANCEPAD_MAP_CONFIG	(MAP_DPAD_TO_BUTTONS |			\
 				MAP_TRIGGERS_TO_BUTTONS | MAP_STICKS_TO_NULL)
@@ -91,8 +86,7 @@
 #define XTYPE_XBOX        0
 #define XTYPE_XBOX360     1
 #define XTYPE_XBOX360W    2
-#define XTYPE_XBOXONE     3
-#define XTYPE_UNKNOWN     4
+#define XTYPE_UNKNOWN     3
 
 /* Send power-off packet to xpad360w after holding the mode button for this many
  * seconds
@@ -137,16 +131,11 @@ static const struct xpad_device {
 	{ 0x03eb, 0xff02, "Wooting Two (Legacy)", 0, XTYPE_XBOX360 },
 	{ 0x03f0, 0x038D, "HyperX Clutch", 0, XTYPE_XBOX360 },			/* wired */
 	{ 0x03f0, 0x048D, "HyperX Clutch", 0, XTYPE_XBOX360 },			/* wireless */
-	{ 0x03f0, 0x0495, "HyperX Clutch Gladiate", 0, XTYPE_XBOXONE },
-	{ 0x03f0, 0x07A0, "HyperX Clutch Gladiate RGB", 0, XTYPE_XBOXONE },
-	{ 0x03f0, 0x08B6, "HyperX Clutch Gladiate", MAP_SHARE_BUTTON, XTYPE_XBOXONE },		/* v2 */
-	{ 0x03f0, 0x09B4, "HyperX Clutch Tanto", 0, XTYPE_XBOXONE },
 	{ 0x044f, 0x0f00, "Thrustmaster Wheel", 0, XTYPE_XBOX },
 	{ 0x044f, 0x0f03, "Thrustmaster Wheel", 0, XTYPE_XBOX },
 	{ 0x044f, 0x0f07, "Thrustmaster, Inc. Controller", 0, XTYPE_XBOX },
 	{ 0x044f, 0x0f10, "Thrustmaster Modena GT Wheel", 0, XTYPE_XBOX },
 	{ 0x044f, 0xb326, "Thrustmaster Gamepad GP XID", 0, XTYPE_XBOX360 },
-	{ 0x044f, 0xd01e, "ThrustMaster, Inc. ESWAP X 2 ELDEN RING EDITION", 0, XTYPE_XBOXONE },
 	{ 0x045e, 0x0202, "Microsoft X-Box pad v1 (US)", 0, XTYPE_XBOX },
 	{ 0x045e, 0x0285, "Microsoft X-Box pad (Japan)", 0, XTYPE_XBOX },
 	{ 0x045e, 0x0287, "Microsoft Xbox Controller S", 0, XTYPE_XBOX },
@@ -156,14 +145,7 @@ static const struct xpad_device {
 	{ 0x045e, 0x028f, "Microsoft X-Box 360 pad v2", 0, XTYPE_XBOX360 },
 	{ 0x045e, 0x0291, "Xbox 360 Wireless Receiver (XBOX)", MAP_DPAD_TO_BUTTONS, XTYPE_XBOX360W },
 	{ 0x045e, 0x02a9, "Xbox 360 Wireless Receiver (Unofficial)", MAP_DPAD_TO_BUTTONS, XTYPE_XBOX360W },
-	{ 0x045e, 0x02d1, "Microsoft X-Box One pad", 0, XTYPE_XBOXONE },
-	{ 0x045e, 0x02dd, "Microsoft X-Box One pad (Firmware 2015)", 0, XTYPE_XBOXONE },
-	{ 0x045e, 0x02e3, "Microsoft X-Box One Elite pad", MAP_PADDLES, XTYPE_XBOXONE },
-	{ 0x045e, 0x02ea, "Microsoft X-Box One S pad", 0, XTYPE_XBOXONE },
 	{ 0x045e, 0x0719, "Xbox 360 Wireless Receiver", MAP_DPAD_TO_BUTTONS, XTYPE_XBOX360W },
-	{ 0x045e, 0x0b00, "Microsoft X-Box One Elite 2 pad", MAP_PADDLES, XTYPE_XBOXONE },
-	{ 0x045e, 0x0b0a, "Microsoft X-Box Adaptive Controller", MAP_PROFILE_BUTTON, XTYPE_XBOXONE },
-	{ 0x045e, 0x0b12, "Microsoft Xbox Series S|X Controller", MAP_SHARE_BUTTON | MAP_SHARE_OFFSET, XTYPE_XBOXONE },
 	{ 0x046d, 0xc21d, "Logitech Gamepad F310", 0, XTYPE_XBOX360 },
 	{ 0x046d, 0xc21e, "Logitech Gamepad F510", 0, XTYPE_XBOX360 },
 	{ 0x046d, 0xc21f, "Logitech Gamepad F710", 0, XTYPE_XBOX360 },
@@ -183,7 +165,6 @@ static const struct xpad_device {
 	{ 0x06a3, 0x0200, "Saitek Racing Wheel", 0, XTYPE_XBOX },
 	{ 0x06a3, 0x0201, "Saitek Adrenalin", 0, XTYPE_XBOX },
 	{ 0x06a3, 0xf51a, "Saitek P3600", 0, XTYPE_XBOX360 },
-	{ 0x0738, 0x4503, "Mad Catz Racing Wheel", 0, XTYPE_XBOXONE },
 	{ 0x0738, 0x4506, "Mad Catz 4506 Wireless Controller", 0, XTYPE_XBOX },
 	{ 0x0738, 0x4516, "Mad Catz Control Pad", 0, XTYPE_XBOX },
 	{ 0x0738, 0x4520, "Mad Catz Control Pad Pro", 0, XTYPE_XBOX },
@@ -205,7 +186,6 @@ static const struct xpad_device {
 	{ 0x0738, 0x4740, "Mad Catz Beat Pad", 0, XTYPE_XBOX360 },
 	{ 0x0738, 0x4743, "Mad Catz Beat Pad Pro", MAP_DPAD_TO_BUTTONS, XTYPE_XBOX },
 	{ 0x0738, 0x4758, "Mad Catz Arcade Game Stick", MAP_TRIGGERS_TO_BUTTONS, XTYPE_XBOX360 },
-	{ 0x0738, 0x4a01, "Mad Catz FightStick TE 2", MAP_TRIGGERS_TO_BUTTONS, XTYPE_XBOXONE },
 	{ 0x0738, 0x6040, "Mad Catz Beat Pad Pro", MAP_DPAD_TO_BUTTONS, XTYPE_XBOX },
 	{ 0x0738, 0x9871, "Mad Catz Portable Drum", 0, XTYPE_XBOX360 },
 	{ 0x0738, 0xb726, "Mad Catz Xbox controller - MW2", 0, XTYPE_XBOX360 },
@@ -216,8 +196,6 @@ static const struct xpad_device {
 	{ 0x0738, 0xcb29, "Saitek Aviator Stick AV8R02", 0, XTYPE_XBOX360 },
 	{ 0x0738, 0xf738, "Super SFIV FightStick TE S", 0, XTYPE_XBOX360 },
 	{ 0x07ff, 0xffff, "Mad Catz GamePad", 0, XTYPE_XBOX360 },
-	{ 0x0b05, 0x1a38, "ASUS ROG RAIKIRI", MAP_SHARE_BUTTON, XTYPE_XBOXONE },
-	{ 0x0b05, 0x1abb, "ASUS ROG RAIKIRI PRO", 0, XTYPE_XBOXONE },
 	{ 0x0c12, 0x0005, "Intec wireless", 0, XTYPE_XBOX },
 	{ 0x0c12, 0x8801, "Nyko Xbox Controller", 0, XTYPE_XBOX },
 	{ 0x0c12, 0x8802, "Zeroplus Xbox Controller", 0, XTYPE_XBOX },
@@ -240,34 +218,10 @@ static const struct xpad_device {
 	{ 0x0e6f, 0x011f, "Rock Candy Gamepad Wired Controller", 0, XTYPE_XBOX360 },
 	{ 0x0e6f, 0x0131, "PDP EA Sports Controller", 0, XTYPE_XBOX360 },
 	{ 0x0e6f, 0x0133, "Xbox 360 Wired Controller", 0, XTYPE_XBOX360 },
-	{ 0x0e6f, 0x0139, "Afterglow Prismatic Wired Controller", 0, XTYPE_XBOXONE },
-	{ 0x0e6f, 0x013a, "PDP Xbox One Controller", 0, XTYPE_XBOXONE },
-	{ 0x0e6f, 0x0146, "Rock Candy Wired Controller for Xbox One", 0, XTYPE_XBOXONE },
-	{ 0x0e6f, 0x0147, "PDP Marvel Xbox One Controller", 0, XTYPE_XBOXONE },
-	{ 0x0e6f, 0x015c, "PDP Xbox One Arcade Stick", MAP_TRIGGERS_TO_BUTTONS, XTYPE_XBOXONE },
-	{ 0x0e6f, 0x015d, "PDP Mirror's Edge Official Wired Controller for Xbox One", 0, XTYPE_XBOXONE },
-	{ 0x0e6f, 0x0161, "PDP Xbox One Controller", 0, XTYPE_XBOXONE },
-	{ 0x0e6f, 0x0162, "PDP Xbox One Controller", 0, XTYPE_XBOXONE },
-	{ 0x0e6f, 0x0163, "PDP Xbox One Controller", 0, XTYPE_XBOXONE },
-	{ 0x0e6f, 0x0164, "PDP Battlefield One", 0, XTYPE_XBOXONE },
-	{ 0x0e6f, 0x0165, "PDP Titanfall 2", 0, XTYPE_XBOXONE },
 	{ 0x0e6f, 0x0201, "Pelican PL-3601 'TSZ' Wired Xbox 360 Controller", 0, XTYPE_XBOX360 },
 	{ 0x0e6f, 0x0213, "Afterglow Gamepad for Xbox 360", 0, XTYPE_XBOX360 },
 	{ 0x0e6f, 0x021f, "Rock Candy Gamepad for Xbox 360", 0, XTYPE_XBOX360 },
-	{ 0x0e6f, 0x0246, "Rock Candy Gamepad for Xbox One 2015", 0, XTYPE_XBOXONE },
-	{ 0x0e6f, 0x02a0, "PDP Xbox One Controller", 0, XTYPE_XBOXONE },
-	{ 0x0e6f, 0x02a1, "PDP Xbox One Controller", 0, XTYPE_XBOXONE },
-	{ 0x0e6f, 0x02a2, "PDP Wired Controller for Xbox One - Crimson Red", 0, XTYPE_XBOXONE },
-	{ 0x0e6f, 0x02a4, "PDP Wired Controller for Xbox One - Stealth Series", 0, XTYPE_XBOXONE },
-	{ 0x0e6f, 0x02a6, "PDP Wired Controller for Xbox One - Camo Series", 0, XTYPE_XBOXONE },
-	{ 0x0e6f, 0x02a7, "PDP Xbox One Controller", 0, XTYPE_XBOXONE },
-	{ 0x0e6f, 0x02a8, "PDP Xbox One Controller", 0, XTYPE_XBOXONE },
-	{ 0x0e6f, 0x02ab, "PDP Controller for Xbox One", 0, XTYPE_XBOXONE },
-	{ 0x0e6f, 0x02ad, "PDP Wired Controller for Xbox One - Stealth Series", 0, XTYPE_XBOXONE },
-	{ 0x0e6f, 0x02b3, "Afterglow Prismatic Wired Controller", 0, XTYPE_XBOXONE },
-	{ 0x0e6f, 0x02b8, "Afterglow Prismatic Wired Controller", 0, XTYPE_XBOXONE },
 	{ 0x0e6f, 0x0301, "Logic3 Controller", 0, XTYPE_XBOX360 },
-	{ 0x0e6f, 0x0346, "Rock Candy Gamepad for Xbox One 2016", 0, XTYPE_XBOXONE },
 	{ 0x0e6f, 0x0401, "Logic3 Controller", 0, XTYPE_XBOX360 },
 	{ 0x0e6f, 0x0413, "Afterglow AX.1 Gamepad for Xbox 360", 0, XTYPE_XBOX360 },
 	{ 0x0e6f, 0x0501, "PDP Xbox 360 Controller", 0, XTYPE_XBOX360 },
@@ -279,23 +233,13 @@ static const struct xpad_device {
 	{ 0x0f0d, 0x000d, "Hori Fighting Stick EX2", MAP_TRIGGERS_TO_BUTTONS, XTYPE_XBOX360 },
 	{ 0x0f0d, 0x0016, "Hori Real Arcade Pro.EX", MAP_TRIGGERS_TO_BUTTONS, XTYPE_XBOX360 },
 	{ 0x0f0d, 0x001b, "Hori Real Arcade Pro VX", MAP_TRIGGERS_TO_BUTTONS, XTYPE_XBOX360 },
-	{ 0x0f0d, 0x0063, "Hori Real Arcade Pro Hayabusa (USA) Xbox One", MAP_TRIGGERS_TO_BUTTONS, XTYPE_XBOXONE },
-	{ 0x0f0d, 0x0067, "HORIPAD ONE", 0, XTYPE_XBOXONE },
-	{ 0x0f0d, 0x0078, "Hori Real Arcade Pro V Kai Xbox One", MAP_TRIGGERS_TO_BUTTONS, XTYPE_XBOXONE },
-	{ 0x0f0d, 0x00c5, "Hori Fighting Commander ONE", MAP_TRIGGERS_TO_BUTTONS, XTYPE_XBOXONE },
 	{ 0x0f0d, 0x00dc, "HORIPAD FPS for Nintendo Switch", MAP_TRIGGERS_TO_BUTTONS, XTYPE_XBOX360 },
-	{ 0x0f0d, 0x0151, "Hori Racing Wheel Overdrive for Xbox Series X", 0, XTYPE_XBOXONE },
-	{ 0x0f0d, 0x0152, "Hori Racing Wheel Overdrive for Xbox Series X", 0, XTYPE_XBOXONE },
-	{ 0x0f0d, 0x01b2, "HORI Taiko No Tatsujin Drum Controller", MAP_SHARE_BUTTON, XTYPE_XBOXONE },
 	{ 0x0f30, 0x010b, "Philips Recoil", 0, XTYPE_XBOX },
 	{ 0x0f30, 0x0202, "Joytech Advanced Controller", 0, XTYPE_XBOX },
 	{ 0x0f30, 0x8888, "BigBen XBMiniPad Controller", 0, XTYPE_XBOX },
 	{ 0x102c, 0xff0c, "Joytech Wireless Advanced Controller", 0, XTYPE_XBOX },
 	{ 0x1038, 0x1430, "SteelSeries Stratus Duo", 0, XTYPE_XBOX360 },
 	{ 0x1038, 0x1431, "SteelSeries Stratus Duo", 0, XTYPE_XBOX360 },
-	{ 0x10f5, 0x7005, "Turtle Beach Recon Controller", 0, XTYPE_XBOXONE },
-	{ 0x10f5, 0x7008, "Turtle Beach Recon Controller", MAP_SHARE_BUTTON, XTYPE_XBOXONE },
-	{ 0x10f5, 0x7073, "Turtle Beach Stealth Ultra Controller", MAP_SHARE_BUTTON, XTYPE_XBOXONE },
 	{ 0x11c9, 0x55f0, "Nacon GC-100XF", 0, XTYPE_XBOX360 },
 	{ 0x11ff, 0x0511, "PXN V900", 0, XTYPE_XBOX360 },
 	{ 0x1209, 0x2882, "Ardwiino Controller", 0, XTYPE_XBOX360 },
@@ -308,9 +252,6 @@ static const struct xpad_device {
 	{ 0x1430, 0xf801, "RedOctane Controller", 0, XTYPE_XBOX360 },
 	{ 0x146b, 0x0601, "BigBen Interactive XBOX 360 Controller", 0, XTYPE_XBOX360 },
 	{ 0x146b, 0x0604, "Bigben Interactive DAIJA Arcade Stick", MAP_TRIGGERS_TO_BUTTONS, XTYPE_XBOX360 },
-	{ 0x1532, 0x0a00, "Razer Atrox Arcade Stick", MAP_TRIGGERS_TO_BUTTONS, XTYPE_XBOXONE },
-	{ 0x1532, 0x0a03, "Razer Wildcat", 0, XTYPE_XBOXONE },
-	{ 0x1532, 0x0a29, "Razer Wolverine V2", 0, XTYPE_XBOXONE },
 	{ 0x15e4, 0x3f00, "Power A Mini Pro Elite", 0, XTYPE_XBOX360 },
 	{ 0x15e4, 0x3f0a, "Xbox Airflo wired controller", 0, XTYPE_XBOX360 },
 	{ 0x15e4, 0x3f10, "Batarang Xbox 360 controller", 0, XTYPE_XBOX360 },
@@ -358,12 +299,7 @@ static const struct xpad_device {
 	{ 0x1bad, 0xfd00, "Razer Onza TE", 0, XTYPE_XBOX360 },
 	{ 0x1bad, 0xfd01, "Razer Onza", 0, XTYPE_XBOX360 },
 	{ 0x1ee9, 0x1590, "ZOTAC Gaming Zone", 0, XTYPE_XBOX360 },
-	{ 0x20d6, 0x2001, "BDA Xbox Series X Wired Controller", 0, XTYPE_XBOXONE },
-	{ 0x20d6, 0x2009, "PowerA Enhanced Wired Controller for Xbox Series X|S", 0, XTYPE_XBOXONE },
-	{ 0x20d6, 0x2064, "PowerA Wired Controller for Xbox", MAP_SHARE_BUTTON, XTYPE_XBOXONE },
 	{ 0x20d6, 0x281f, "PowerA Wired Controller For Xbox 360", 0, XTYPE_XBOX360 },
-	{ 0x20d6, 0x400b, "PowerA FUSION Pro 4 Wired Controller", MAP_SHARE_BUTTON, XTYPE_XBOXONE },
-	{ 0x20d6, 0x890b, "PowerA MOGA XP-Ultra Controller", MAP_SHARE_BUTTON, XTYPE_XBOXONE },
 	{ 0x2345, 0xe00b, "Machenike G5 Pro Controller", 0, XTYPE_XBOX360 },
 	{ 0x24c6, 0x5000, "Razer Atrox Arcade Stick", MAP_TRIGGERS_TO_BUTTONS, XTYPE_XBOX360 },
 	{ 0x24c6, 0x5300, "PowerA MINI PROEX Controller", 0, XTYPE_XBOX360 },
@@ -371,9 +307,6 @@ static const struct xpad_device {
 	{ 0x24c6, 0x530a, "Xbox 360 Pro EX Controller", 0, XTYPE_XBOX360 },
 	{ 0x24c6, 0x531a, "PowerA Pro Ex", 0, XTYPE_XBOX360 },
 	{ 0x24c6, 0x5397, "FUS1ON Tournament Controller", 0, XTYPE_XBOX360 },
-	{ 0x24c6, 0x541a, "PowerA Xbox One Mini Wired Controller", 0, XTYPE_XBOXONE },
-	{ 0x24c6, 0x542a, "Xbox ONE spectra", 0, XTYPE_XBOXONE },
-	{ 0x24c6, 0x543a, "PowerA Xbox One wired controller", 0, XTYPE_XBOXONE },
 	{ 0x24c6, 0x5500, "Hori XBOX 360 EX 2 with Turbo", 0, XTYPE_XBOX360 },
 	{ 0x24c6, 0x5501, "Hori Real Arcade Pro VX-SA", 0, XTYPE_XBOX360 },
 	{ 0x24c6, 0x5502, "Hori Fighting Stick VX Alt", MAP_TRIGGERS_TO_BUTTONS, XTYPE_XBOX360 },
@@ -382,29 +315,18 @@ static const struct xpad_device {
 	{ 0x24c6, 0x550d, "Hori GEM Xbox controller", 0, XTYPE_XBOX360 },
 	{ 0x24c6, 0x550e, "Hori Real Arcade Pro V Kai 360", MAP_TRIGGERS_TO_BUTTONS, XTYPE_XBOX360 },
 	{ 0x24c6, 0x5510, "Hori Fighting Commander ONE (Xbox 360/PC Mode)", MAP_TRIGGERS_TO_BUTTONS, XTYPE_XBOX360 },
-	{ 0x24c6, 0x551a, "PowerA FUSION Pro Controller", 0, XTYPE_XBOXONE },
-	{ 0x24c6, 0x561a, "PowerA FUSION Controller", 0, XTYPE_XBOXONE },
-	{ 0x24c6, 0x581a, "ThrustMaster XB1 Classic Controller", 0, XTYPE_XBOXONE },
 	{ 0x24c6, 0x5b00, "ThrustMaster Ferrari 458 Racing Wheel", 0, XTYPE_XBOX360 },
 	{ 0x24c6, 0x5b02, "Thrustmaster, Inc. GPX Controller", 0, XTYPE_XBOX360 },
 	{ 0x24c6, 0x5b03, "Thrustmaster Ferrari 458 Racing Wheel", 0, XTYPE_XBOX360 },
 	{ 0x24c6, 0x5d04, "Razer Sabertooth", 0, XTYPE_XBOX360 },
 	{ 0x24c6, 0xfafe, "Rock Candy Gamepad for Xbox 360", 0, XTYPE_XBOX360 },
 	{ 0x2563, 0x058d, "OneXPlayer Gamepad", 0, XTYPE_XBOX360 },
-	{ 0x294b, 0x3303, "Snakebyte GAMEPAD BASE X", 0, XTYPE_XBOXONE },
-	{ 0x294b, 0x3404, "Snakebyte GAMEPAD RGB X", 0, XTYPE_XBOXONE },
 	{ 0x2993, 0x2001, "TECNO Pocket Go", 0, XTYPE_XBOX360 },
-	{ 0x2dc8, 0x2000, "8BitDo Pro 2 Wired Controller fox Xbox", 0, XTYPE_XBOXONE },
-	{ 0x2dc8, 0x200f, "8BitDo Ultimate 3-mode Controller for Xbox", MAP_SHARE_BUTTON, XTYPE_XBOXONE },
 	{ 0x2dc8, 0x3106, "8BitDo Ultimate Wireless / Pro 2 Wired Controller", 0, XTYPE_XBOX360 },
 	{ 0x2dc8, 0x3109, "8BitDo Ultimate Wireless Bluetooth", 0, XTYPE_XBOX360 },
 	{ 0x2dc8, 0x310a, "8BitDo Ultimate 2C Wireless Controller", 0, XTYPE_XBOX360 },
 	{ 0x2dc8, 0x310b, "8BitDo Ultimate 2 Wireless Controller", 0, XTYPE_XBOX360 },
 	{ 0x2dc8, 0x6001, "8BitDo SN30 Pro", 0, XTYPE_XBOX360 },
-	{ 0x2e24, 0x0423, "Hyperkin DuchesS Xbox One pad", MAP_SHARE_BUTTON, XTYPE_XBOXONE },
-	{ 0x2e24, 0x0652, "Hyperkin Duke X-Box One pad", 0, XTYPE_XBOXONE },
-	{ 0x2e24, 0x1688, "Hyperkin X91 X-Box One pad", 0, XTYPE_XBOXONE },
-	{ 0x2e95, 0x0504, "SCUF Gaming Controller", MAP_SHARE_BUTTON, XTYPE_XBOXONE },
 	{ 0x31e3, 0x1100, "Wooting One", 0, XTYPE_XBOX360 },
 	{ 0x31e3, 0x1200, "Wooting Two", 0, XTYPE_XBOX360 },
 	{ 0x31e3, 0x1210, "Wooting Lekker", 0, XTYPE_XBOX360 },
@@ -412,15 +334,9 @@ static const struct xpad_device {
 	{ 0x31e3, 0x1230, "Wooting Two HE (ARM)", 0, XTYPE_XBOX360 },
 	{ 0x31e3, 0x1300, "Wooting 60HE (AVR)", 0, XTYPE_XBOX360 },
 	{ 0x31e3, 0x1310, "Wooting 60HE (ARM)", 0, XTYPE_XBOX360 },
-	{ 0x3285, 0x0603, "Nacon Pro Compact controller for Xbox", 0, XTYPE_XBOXONE },
 	{ 0x3285, 0x0607, "Nacon GC-100", 0, XTYPE_XBOX360 },
-	{ 0x3285, 0x0614, "Nacon Pro Compact", 0, XTYPE_XBOXONE },
-	{ 0x3285, 0x0646, "Nacon Pro Compact", 0, XTYPE_XBOXONE },
 	{ 0x3285, 0x0662, "Nacon Revolution5 Pro", 0, XTYPE_XBOX360 },
-	{ 0x3285, 0x0663, "Nacon Evol-X", 0, XTYPE_XBOXONE },
 	{ 0x3537, 0x1004, "GameSir T4 Kaleid", 0, XTYPE_XBOX360 },
-	{ 0x3537, 0x1010, "GameSir G7 SE", 0, XTYPE_XBOXONE },
-	{ 0x366c, 0x0005, "ByoWave Proteus Controller", MAP_SHARE_BUTTON, XTYPE_XBOXONE, FLAG_DELAY_INIT },
 	{ 0x3767, 0x0101, "Fanatec Speedster 3 Forceshock Wheel", 0, XTYPE_XBOX },
 	{ 0x37d7, 0x2501, "Flydigi Apex 5", 0, XTYPE_XBOX360 },
 	{ 0x413d, 0x2104, "Black Shark Green Ghost Gamepad", 0, XTYPE_XBOX360 },
@@ -478,13 +394,6 @@ static const signed short xpad_abs_triggers[] = {
 	-1
 };
 
-/* used when the controller has extra paddle buttons */
-static const signed short xpad_btn_paddles[] = {
-	BTN_GRIPR, BTN_GRIPR2, /* paddle upper right, lower right */
-	BTN_GRIPL, BTN_GRIPL2, /* paddle upper left, lower left */
-	-1						/* terminating entry */
-};
-
 /*
  * Xbox 360 has a vendor-specific class, so we cannot match it with only
  * USB_INTERFACE_INFO (also specifically refused by USB subsystem), so we
@@ -501,47 +410,28 @@ static const signed short xpad_btn_paddles[] = {
 	{ XPAD_XBOX360_VENDOR_PROTOCOL((vend), 1) }, \
 	{ XPAD_XBOX360_VENDOR_PROTOCOL((vend), 129) }
 
-/* The Xbox One controller uses subclass 71 and protocol 208. */
-#define XPAD_XBOXONE_VENDOR_PROTOCOL(vend, pr) \
-	.match_flags = USB_DEVICE_ID_MATCH_VENDOR | USB_DEVICE_ID_MATCH_INT_INFO, \
-	.idVendor = (vend), \
-	.bInterfaceClass = USB_CLASS_VENDOR_SPEC, \
-	.bInterfaceSubClass = 71, \
-	.bInterfaceProtocol = (pr)
-#define XPAD_XBOXONE_VENDOR(vend) \
-	{ XPAD_XBOXONE_VENDOR_PROTOCOL((vend), 208) }
-
 static const struct usb_device_id xpad_table[] = {
 	/*
-	 * Please keep this list sorted by vendor ID. Note that there are 2
-	 * macros - XPAD_XBOX360_VENDOR and XPAD_XBOXONE_VENDOR.
+	 * Please keep this list sorted by vendor ID
 	 */
 	{ USB_INTERFACE_INFO('X', 'B', 0) },	/* Xbox USB-IF not-approved class */
 	XPAD_XBOX360_VENDOR(0x0079),		/* GPD Win 2 controller */
 	XPAD_XBOX360_VENDOR(0x03eb),		/* Wooting Keyboards (Legacy) */
 	XPAD_XBOX360_VENDOR(0x03f0),		/* HP HyperX Xbox 360 controllers */
-	XPAD_XBOXONE_VENDOR(0x03f0),		/* HP HyperX Xbox One controllers */
 	XPAD_XBOX360_VENDOR(0x044f),		/* Thrustmaster Xbox 360 controllers */
-	XPAD_XBOXONE_VENDOR(0x044f),		/* Thrustmaster Xbox One controllers */
 	XPAD_XBOX360_VENDOR(0x045e),		/* Microsoft Xbox 360 controllers */
-	XPAD_XBOXONE_VENDOR(0x045e),		/* Microsoft Xbox One controllers */
 	XPAD_XBOX360_VENDOR(0x046d),		/* Logitech Xbox 360-style controllers */
 	XPAD_XBOX360_VENDOR(0x0502),		/* Acer Inc. Xbox 360 style controllers */
 	XPAD_XBOX360_VENDOR(0x056e),		/* Elecom JC-U3613M */
 	XPAD_XBOX360_VENDOR(0x06a3),		/* Saitek P3600 */
 	XPAD_XBOX360_VENDOR(0x0738),		/* Mad Catz Xbox 360 controllers */
 	{ USB_DEVICE(0x0738, 0x4540) },		/* Mad Catz Beat Pad */
-	XPAD_XBOXONE_VENDOR(0x0738),		/* Mad Catz FightStick TE 2 */
 	XPAD_XBOX360_VENDOR(0x07ff),		/* Mad Catz Gamepad */
-	XPAD_XBOXONE_VENDOR(0x0b05),		/* ASUS controllers */
 	XPAD_XBOX360_VENDOR(0x0c12),		/* Zeroplus X-Box 360 controllers */
 	XPAD_XBOX360_VENDOR(0x0db0),		/* Micro Star International X-Box 360 controllers */
 	XPAD_XBOX360_VENDOR(0x0e6f),		/* 0x0e6f Xbox 360 controllers */
-	XPAD_XBOXONE_VENDOR(0x0e6f),		/* 0x0e6f Xbox One controllers */
 	XPAD_XBOX360_VENDOR(0x0f0d),		/* Hori controllers */
-	XPAD_XBOXONE_VENDOR(0x0f0d),		/* Hori controllers */
 	XPAD_XBOX360_VENDOR(0x1038),		/* SteelSeries controllers */
-	XPAD_XBOXONE_VENDOR(0x10f5),		/* Turtle Beach Controllers */
 	XPAD_XBOX360_VENDOR(0x11c9),		/* Nacon GC100XF */
 	XPAD_XBOX360_VENDOR(0x11ff),		/* PXN V900 */
 	XPAD_XBOX360_VENDOR(0x1209),		/* Ardwiino Controllers */
@@ -549,7 +439,6 @@ static const struct usb_device_id xpad_table[] = {
 	XPAD_XBOX360_VENDOR(0x1430),		/* RedOctane Xbox 360 controllers */
 	XPAD_XBOX360_VENDOR(0x146b),		/* Bigben Interactive controllers */
 	XPAD_XBOX360_VENDOR(0x1532),		/* Razer Sabertooth */
-	XPAD_XBOXONE_VENDOR(0x1532),		/* Razer Wildcat */
 	XPAD_XBOX360_VENDOR(0x15e4),		/* Numark Xbox 360 controllers */
 	XPAD_XBOX360_VENDOR(0x162e),		/* Joytech Xbox 360 controllers */
 	XPAD_XBOX360_VENDOR(0x1689),		/* Razer Onza */
@@ -559,26 +448,17 @@ static const struct usb_device_id xpad_table[] = {
 	XPAD_XBOX360_VENDOR(0x1bad),		/* Harmonix Rock Band guitar and drums */
 	XPAD_XBOX360_VENDOR(0x1ee9),		/* ZOTAC Technology Limited */
 	XPAD_XBOX360_VENDOR(0x20d6),		/* PowerA controllers */
-	XPAD_XBOXONE_VENDOR(0x20d6),		/* PowerA controllers */
 	XPAD_XBOX360_VENDOR(0x2345),		/* Machenike Controllers */
 	XPAD_XBOX360_VENDOR(0x24c6),		/* PowerA controllers */
-	XPAD_XBOXONE_VENDOR(0x24c6),		/* PowerA controllers */
 	XPAD_XBOX360_VENDOR(0x2563),		/* OneXPlayer Gamepad */
 	XPAD_XBOX360_VENDOR(0x260d),		/* Dareu H101 */
-	XPAD_XBOXONE_VENDOR(0x294b),		/* Snakebyte */
 	XPAD_XBOX360_VENDOR(0x2993),		/* TECNO Mobile */
 	XPAD_XBOX360_VENDOR(0x2c22),		/* Qanba Controllers */
 	XPAD_XBOX360_VENDOR(0x2dc8),		/* 8BitDo Controllers */
-	XPAD_XBOXONE_VENDOR(0x2dc8),		/* 8BitDo Controllers */
-	XPAD_XBOXONE_VENDOR(0x2e24),		/* Hyperkin Controllers */
 	XPAD_XBOX360_VENDOR(0x2f24),		/* GameSir Controllers */
-	XPAD_XBOXONE_VENDOR(0x2e95),		/* SCUF Gaming Controller */
 	XPAD_XBOX360_VENDOR(0x31e3),		/* Wooting Keyboards */
 	XPAD_XBOX360_VENDOR(0x3285),		/* Nacon GC-100 */
-	XPAD_XBOXONE_VENDOR(0x3285),		/* Nacon Evol-X */
 	XPAD_XBOX360_VENDOR(0x3537),		/* GameSir Controllers */
-	XPAD_XBOXONE_VENDOR(0x3537),		/* GameSir Controllers */
-	XPAD_XBOXONE_VENDOR(0x366c),		/* ByoWave controllers */
 	XPAD_XBOX360_VENDOR(0x37d7),		/* Flydigi Controllers */
 	XPAD_XBOX360_VENDOR(0x413d),		/* Black Shark Green Ghost Controller */
 	{ }
@@ -586,162 +466,6 @@ static const struct usb_device_id xpad_table[] = {
 
 MODULE_DEVICE_TABLE(usb, xpad_table);
 
-struct xboxone_init_packet {
-	u16 idVendor;
-	u16 idProduct;
-	const u8 *data;
-	u8 len;
-};
-
-#define XBOXONE_INIT_PKT(_vid, _pid, _data)		\
-	{						\
-		.idVendor	= (_vid),		\
-		.idProduct	= (_pid),		\
-		.data		= (_data),		\
-		.len		= ARRAY_SIZE(_data),	\
-	}
-
-/*
- * starting with xbox one, the game input protocol is used
- * magic numbers are taken from
- * - https://github.com/xpadneo/gip-dissector/blob/main/src/gip-dissector.lua
- * - https://github.com/medusalix/xone/blob/master/bus/protocol.c
- */
-#define GIP_CMD_ACK      0x01
-#define GIP_CMD_ANNOUNCE 0x02
-#define GIP_CMD_IDENTIFY 0x04
-#define GIP_CMD_POWER    0x05
-#define GIP_CMD_AUTHENTICATE 0x06
-#define GIP_CMD_VIRTUAL_KEY  0x07
-#define GIP_CMD_RUMBLE   0x09
-#define GIP_CMD_LED      0x0a
-#define GIP_CMD_FIRMWARE 0x0c
-#define GIP_CMD_INPUT    0x20
-
-#define GIP_SEQ0 0x00
-
-#define GIP_OPT_ACK      0x10
-#define GIP_OPT_INTERNAL 0x20
-
-/*
- * length of the command payload encoded with
- * https://en.wikipedia.org/wiki/LEB128
- * which is a no-op for N < 128
- */
-#define GIP_PL_LEN(N) (N)
-
-/*
- * payload specific defines
- */
-#define GIP_PWR_ON 0x00
-#define GIP_LED_ON 0x01
-
-#define GIP_MOTOR_R  BIT(0)
-#define GIP_MOTOR_L  BIT(1)
-#define GIP_MOTOR_RT BIT(2)
-#define GIP_MOTOR_LT BIT(3)
-#define GIP_MOTOR_ALL (GIP_MOTOR_R | GIP_MOTOR_L | GIP_MOTOR_RT | GIP_MOTOR_LT)
-
-#define GIP_WIRED_INTF_DATA 0
-#define GIP_WIRED_INTF_AUDIO 1
-
-/*
- * This packet is required for all Xbox One pads with 2015
- * or later firmware installed (or present from the factory).
- */
-static const u8 xboxone_power_on[] = {
-	GIP_CMD_POWER, GIP_OPT_INTERNAL, GIP_SEQ0, GIP_PL_LEN(1), GIP_PWR_ON
-};
-
-/*
- * This packet is required for Xbox One S (0x045e:0x02ea)
- * and Xbox One Elite Series 2 (0x045e:0x0b00) pads to
- * initialize the controller that was previously used in
- * Bluetooth mode.
- */
-static const u8 xboxone_s_init[] = {
-	GIP_CMD_POWER, GIP_OPT_INTERNAL, GIP_SEQ0, 0x0f, 0x06
-};
-
-/*
- * This packet is required to get additional input data
- * from Xbox One Elite Series 2 (0x045e:0x0b00) pads.
- * We mostly do this right now to get paddle data
- */
-static const u8 extra_input_packet_init[] = {
-	0x4d, 0x10, 0x01, 0x02, 0x07, 0x00
-};
-
-/*
- * This packet is required for the Titanfall 2 Xbox One pads
- * (0x0e6f:0x0165) to finish initialization and for Hori pads
- * (0x0f0d:0x0067) to make the analog sticks work.
- */
-static const u8 xboxone_hori_ack_id[] = {
-	GIP_CMD_ACK, GIP_OPT_INTERNAL, GIP_SEQ0, GIP_PL_LEN(9),
-	0x00, GIP_CMD_IDENTIFY, GIP_OPT_INTERNAL, 0x3a, 0x00, 0x00, 0x00, 0x80, 0x00
-};
-
-/*
- * This packet is sent by default on Windows, and is required for some pads to
- * start sending input reports, including most (all?) of the PDP. These pads
- * include: (0x0e6f:0x02ab), (0x0e6f:0x02a4), (0x0e6f:0x02a6).
- */
-static const u8 xboxone_led_on[] = { GIP_CMD_LED, GIP_OPT_INTERNAL, GIP_SEQ0,
-GIP_PL_LEN(3), 0x00, GIP_LED_ON, 0x14 };
-
-/*
- * This packet is required for most (all?) of the PDP pads to start
- * sending input reports. These pads include: (0x0e6f:0x02ab),
- * (0x0e6f:0x02a4), (0x0e6f:0x02a6).
- */
-static const u8 xboxone_auth_done[] = {
-	GIP_CMD_AUTHENTICATE, GIP_OPT_INTERNAL, GIP_SEQ0, GIP_PL_LEN(2), 0x01, 0x00
-};
-
-/*
- * A specific rumble packet is required for some PowerA pads to start
- * sending input reports. One of those pads is (0x24c6:0x543a).
- */
-static const u8 xboxone_rumblebegin_init[] = {
-	GIP_CMD_RUMBLE, 0x00, GIP_SEQ0, GIP_PL_LEN(9),
-	0x00, GIP_MOTOR_ALL, 0x00, 0x00, 0x1D, 0x1D, 0xFF, 0x00, 0x00
-};
-
-/*
- * A rumble packet with zero FF intensity will immediately
- * terminate the rumbling required to init PowerA pads.
- * This should happen fast enough that the motors don't
- * spin up to enough speed to actually vibrate the gamepad.
- */
-static const u8 xboxone_rumbleend_init[] = {
-	GIP_CMD_RUMBLE, 0x00, GIP_SEQ0, GIP_PL_LEN(9),
-	0x00, GIP_MOTOR_ALL, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
-};
-
-/*
- * This specifies the selection of init packets that a gamepad
- * will be sent on init *and* the order in which they will be
- * sent. The correct sequence number will be added when the
- * packet is going to be sent.
- */
-static const struct xboxone_init_packet xboxone_init_packets[] = {
-	XBOXONE_INIT_PKT(0x0e6f, 0x0165, xboxone_hori_ack_id),
-	XBOXONE_INIT_PKT(0x0f0d, 0x0067, xboxone_hori_ack_id),
-	XBOXONE_INIT_PKT(0x0000, 0x0000, xboxone_power_on),
-	XBOXONE_INIT_PKT(0x045e, 0x02ea, xboxone_s_init),
-	XBOXONE_INIT_PKT(0x045e, 0x0b00, xboxone_s_init),
-	XBOXONE_INIT_PKT(0x045e, 0x0b00, extra_input_packet_init),
-	XBOXONE_INIT_PKT(0x0000, 0x0000, xboxone_led_on),
-	XBOXONE_INIT_PKT(0x0000, 0x0000, xboxone_auth_done),
-	XBOXONE_INIT_PKT(0x24c6, 0x541a, xboxone_rumblebegin_init),
-	XBOXONE_INIT_PKT(0x24c6, 0x542a, xboxone_rumblebegin_init),
-	XBOXONE_INIT_PKT(0x24c6, 0x543a, xboxone_rumblebegin_init),
-	XBOXONE_INIT_PKT(0x24c6, 0x541a, xboxone_rumbleend_init),
-	XBOXONE_INIT_PKT(0x24c6, 0x542a, xboxone_rumbleend_init),
-	XBOXONE_INIT_PKT(0x24c6, 0x543a, xboxone_rumbleend_init),
-};
-
 struct xpad_output_packet {
 	u8 data[XPAD_PKT_LEN];
 	u8 len;
@@ -771,7 +495,6 @@ struct usb_xpad {
 	struct urb *irq_out;		/* urb for interrupt out report */
 	struct usb_anchor irq_out_anchor;
 	bool irq_out_active;		/* we must not use an active URB */
-	u8 odata_serial;		/* serial number for xbox one protocol */
 	unsigned char *odata;		/* output data */
 	dma_addr_t odata_dma;
 	spinlock_t odata_lock;
@@ -799,8 +522,6 @@ struct usb_xpad {
 
 static int xpad_init_input(struct usb_xpad *xpad);
 static void xpad_deinit_input(struct usb_xpad *xpad);
-static int xpad_start_input(struct usb_xpad *xpad);
-static void xpadone_ack_mode_report(struct usb_xpad *xpad, u8 seq_num);
 static void xpad360w_poweroff_controller(struct usb_xpad *xpad);
 
 /*
@@ -1038,187 +759,6 @@ static void xpad360w_process_packet(struct usb_xpad *xpad, u16 cmd, unsigned cha
 	rcu_read_unlock();
 }
 
-/*
- *	xpadone_process_packet
- *
- *	Completes a request by converting the data into events for the
- *	input subsystem. This version is for the Xbox One controller.
- *
- *	The report format was gleaned from
- *	https://github.com/kylelemons/xbox/blob/master/xbox.go
- */
-static void xpadone_process_packet(struct usb_xpad *xpad, u16 cmd, unsigned char *data, u32 len)
-{
-	struct input_dev *dev = xpad->dev;
-	bool do_sync = false;
-
-	/* the xbox button has its own special report */
-	if (data[0] == GIP_CMD_VIRTUAL_KEY) {
-		/*
-		 * The Xbox One S controller requires these reports to be
-		 * acked otherwise it continues sending them forever and
-		 * won't report further mode button events.
-		 */
-		if (data[1] == (GIP_OPT_ACK | GIP_OPT_INTERNAL))
-			xpadone_ack_mode_report(xpad, data[2]);
-
-		input_report_key(dev, BTN_MODE, data[4] & GENMASK(1, 0));
-		input_sync(dev);
-
-		do_sync = true;
-	} else if (data[0] == GIP_CMD_FIRMWARE) {
-		/* Some packet formats force us to use this separate to poll paddle inputs */
-		if (xpad->packet_type == PKT_XBE2_FW_5_11) {
-			/* Mute paddles if controller is in a custom profile slot
-			 * Checked by looking at the active profile slot to
-			 * verify it's the default slot
-			 */
-			if (data[19] != 0)
-				data[18] = 0;
-
-			/* Elite Series 2 split packet paddle bits */
-			input_report_key(dev, BTN_GRIPR, data[18] & BIT(0));
-			input_report_key(dev, BTN_GRIPR2, data[18] & BIT(1));
-			input_report_key(dev, BTN_GRIPL, data[18] & BIT(2));
-			input_report_key(dev, BTN_GRIPL2, data[18] & BIT(3));
-
-			do_sync = true;
-		}
-	} else if (data[0] == GIP_CMD_ANNOUNCE) {
-		int error;
-
-		if (xpad->delay_init && !xpad->delayed_init_done) {
-			xpad->delayed_init_done = true;
-			error = xpad_start_input(xpad);
-			if (error)
-				dev_warn(&xpad->dev->dev,
-					 "unable to start delayed input: %d\n",
-					 error);
-		}
-	} else if (data[0] == GIP_CMD_INPUT) { /* The main valid packet type for inputs */
-		/* menu/view buttons */
-		input_report_key(dev, BTN_START,  data[4] & BIT(2));
-		input_report_key(dev, BTN_SELECT, data[4] & BIT(3));
-		if (xpad->mapping & MAP_SHARE_BUTTON) {
-			if (xpad->mapping & MAP_SHARE_OFFSET)
-				input_report_key(dev, KEY_RECORD, data[len - 26] & BIT(0));
-			else
-				input_report_key(dev, KEY_RECORD, data[len - 18] & BIT(0));
-		}
-
-		/* buttons A,B,X,Y */
-		input_report_key(dev, BTN_A,	data[4] & BIT(4));
-		input_report_key(dev, BTN_B,	data[4] & BIT(5));
-		input_report_key(dev, BTN_X,	data[4] & BIT(6));
-		input_report_key(dev, BTN_Y,	data[4] & BIT(7));
-
-		/* digital pad */
-		if (xpad->mapping & MAP_DPAD_TO_BUTTONS) {
-			/* dpad as buttons (left, right, up, down) */
-			input_report_key(dev, BTN_DPAD_LEFT, data[5] & BIT(2));
-			input_report_key(dev, BTN_DPAD_RIGHT, data[5] & BIT(3));
-			input_report_key(dev, BTN_DPAD_UP, data[5] & BIT(0));
-			input_report_key(dev, BTN_DPAD_DOWN, data[5] & BIT(1));
-		} else {
-			input_report_abs(dev, ABS_HAT0X,
-					!!(data[5] & 0x08) - !!(data[5] & 0x04));
-			input_report_abs(dev, ABS_HAT0Y,
-					!!(data[5] & 0x02) - !!(data[5] & 0x01));
-		}
-
-		/* TL/TR */
-		input_report_key(dev, BTN_TL,	data[5] & BIT(4));
-		input_report_key(dev, BTN_TR,	data[5] & BIT(5));
-
-		/* stick press left/right */
-		input_report_key(dev, BTN_THUMBL, data[5] & BIT(6));
-		input_report_key(dev, BTN_THUMBR, data[5] & BIT(7));
-
-		if (!(xpad->mapping & MAP_STICKS_TO_NULL)) {
-			/* left stick */
-			input_report_abs(dev, ABS_X,
-					(__s16) le16_to_cpup((__le16 *)(data + 10)));
-			input_report_abs(dev, ABS_Y,
-					~(__s16) le16_to_cpup((__le16 *)(data + 12)));
-
-			/* right stick */
-			input_report_abs(dev, ABS_RX,
-					(__s16) le16_to_cpup((__le16 *)(data + 14)));
-			input_report_abs(dev, ABS_RY,
-					~(__s16) le16_to_cpup((__le16 *)(data + 16)));
-		}
-
-		/* triggers left/right */
-		if (xpad->mapping & MAP_TRIGGERS_TO_BUTTONS) {
-			input_report_key(dev, BTN_TL2,
-					(__u16) le16_to_cpup((__le16 *)(data + 6)));
-			input_report_key(dev, BTN_TR2,
-					(__u16) le16_to_cpup((__le16 *)(data + 8)));
-		} else {
-			input_report_abs(dev, ABS_Z,
-					(__u16) le16_to_cpup((__le16 *)(data + 6)));
-			input_report_abs(dev, ABS_RZ,
-					(__u16) le16_to_cpup((__le16 *)(data + 8)));
-		}
-
-		/* Profile button has a value of 0-3, so it is reported as an axis */
-		if (xpad->mapping & MAP_PROFILE_BUTTON)
-			input_report_abs(dev, ABS_PROFILE, data[34]);
-
-		/* paddle handling */
-		/* based on SDL's SDL_hidapi_xboxone.c */
-		if (xpad->mapping & MAP_PADDLES) {
-			if (xpad->packet_type == PKT_XBE1) {
-				/* Mute paddles if controller has a custom mapping applied.
-				 * Checked by comparing the current mapping
-				 * config against the factory mapping config
-				 */
-				if (memcmp(&data[4], &data[18], 2) != 0)
-					data[32] = 0;
-
-				/* OG Elite Series Controller paddle bits */
-				input_report_key(dev, BTN_GRIPR, data[32] & BIT(1));
-				input_report_key(dev, BTN_GRIPR2, data[32] & BIT(3));
-				input_report_key(dev, BTN_GRIPL, data[32] & BIT(0));
-				input_report_key(dev, BTN_GRIPL2, data[32] & BIT(2));
-			} else if (xpad->packet_type == PKT_XBE2_FW_OLD) {
-				/* Mute paddles if controller has a custom mapping applied.
-				 * Checked by comparing the current mapping
-				 * config against the factory mapping config
-				 */
-				if (data[19] != 0)
-					data[18] = 0;
-
-				/* Elite Series 2 4.x firmware paddle bits */
-				input_report_key(dev, BTN_GRIPR, data[18] & BIT(0));
-				input_report_key(dev, BTN_GRIPR2, data[18] & BIT(1));
-				input_report_key(dev, BTN_GRIPL, data[18] & BIT(2));
-				input_report_key(dev, BTN_GRIPL2, data[18] & BIT(3));
-			} else if (xpad->packet_type == PKT_XBE2_FW_5_EARLY) {
-				/* Mute paddles if controller has a custom mapping applied.
-				 * Checked by comparing the current mapping
-				 * config against the factory mapping config
-				 */
-				if (data[23] != 0)
-					data[22] = 0;
-
-				/* Elite Series 2 5.x firmware paddle bits
-				 * (before the packet was split)
-				 */
-				input_report_key(dev, BTN_GRIPR, data[22] & BIT(0));
-				input_report_key(dev, BTN_GRIPR2, data[22] & BIT(1));
-				input_report_key(dev, BTN_GRIPL, data[22] & BIT(2));
-				input_report_key(dev, BTN_GRIPL2, data[22] & BIT(3));
-			}
-		}
-
-		do_sync = true;
-	}
-
-	if (do_sync)
-		input_sync(dev);
-}
-
 static void xpad_irq_in(struct urb *urb)
 {
 	struct usb_xpad *xpad = urb->context;
@@ -1251,9 +791,6 @@ static void xpad_irq_in(struct urb *urb)
 	case XTYPE_XBOX360W:
 		xpad360w_process_packet(xpad, 0, xpad->idata);
 		break;
-	case XTYPE_XBOXONE:
-		xpadone_process_packet(xpad, 0, xpad->idata, urb->actual_length);
-		break;
 	default:
 		xpad_process_packet(xpad, 0, xpad->idata);
 	}
@@ -1265,56 +802,12 @@ static void xpad_irq_in(struct urb *urb)
 			__func__, retval);
 }
 
-/* Callers must hold xpad->odata_lock spinlock */
-static bool xpad_prepare_next_init_packet(struct usb_xpad *xpad)
-{
-	const struct xboxone_init_packet *init_packet;
-
-	if (xpad->xtype != XTYPE_XBOXONE)
-		return false;
-
-	/*
-	 * Some dongles will discard init packets if they're sent before the
-	 * controller connects. In these cases, we need to wait until we get
-	 * an announce packet from them to send the init packet sequence.
-	 */
-	if (xpad->delay_init && !xpad->delayed_init_done)
-		return false;
-
-	/* Perform initialization sequence for Xbox One pads that require it */
-	while (xpad->init_seq < ARRAY_SIZE(xboxone_init_packets)) {
-		init_packet = &xboxone_init_packets[xpad->init_seq++];
-
-		if (init_packet->idVendor != 0 &&
-		    init_packet->idVendor != xpad->dev->id.vendor)
-			continue;
-
-		if (init_packet->idProduct != 0 &&
-		    init_packet->idProduct != xpad->dev->id.product)
-			continue;
-
-		/* This packet applies to our device, so prepare to send it */
-		memcpy(xpad->odata, init_packet->data, init_packet->len);
-		xpad->irq_out->transfer_buffer_length = init_packet->len;
-
-		/* Update packet with current sequence number */
-		xpad->odata[2] = xpad->odata_serial++;
-		return true;
-	}
-
-	return false;
-}
-
 /* Callers must hold xpad->odata_lock spinlock */
 static bool xpad_prepare_next_out_packet(struct usb_xpad *xpad)
 {
 	struct xpad_output_packet *pkt, *packet = NULL;
 	int i;
 
-	/* We may have init packets to send before we can send user commands */
-	if (xpad_prepare_next_init_packet(xpad))
-		return true;
-
 	for (i = 0; i < XPAD_NUM_OUT_PACKETS; i++) {
 		if (++xpad->last_out_packet >= XPAD_NUM_OUT_PACKETS)
 			xpad->last_out_packet = 0;
@@ -1490,57 +983,6 @@ static int xpad_inquiry_pad_presence(struct usb_xpad *xpad)
 	return xpad_try_sending_next_out_packet(xpad);
 }
 
-static int xpad_start_xbox_one(struct usb_xpad *xpad)
-{
-	int error;
-
-	if (usb_ifnum_to_if(xpad->udev, GIP_WIRED_INTF_AUDIO)) {
-		/*
-		 * Explicitly disable the audio interface. This is needed
-		 * for some controllers, such as the PowerA Enhanced Wired
-		 * Controller for Series X|S (0x20d6:0x200e) to report the
-		 * guide button.
-		 */
-		error = usb_set_interface(xpad->udev,
-					  GIP_WIRED_INTF_AUDIO, 0);
-		if (error)
-			dev_warn(&xpad->dev->dev,
-				 "unable to disable audio interface: %d\n",
-				 error);
-	}
-
-	guard(spinlock_irqsave)(&xpad->odata_lock);
-
-	/*
-	 * Begin the init sequence by attempting to send a packet.
-	 * We will cycle through the init packet sequence before
-	 * sending any packets from the output ring.
-	 */
-	xpad->init_seq = 0;
-	return xpad_try_sending_next_out_packet(xpad);
-}
-
-static void xpadone_ack_mode_report(struct usb_xpad *xpad, u8 seq_num)
-{
-	struct xpad_output_packet *packet =
-			&xpad->out_packets[XPAD_OUT_CMD_IDX];
-	static const u8 mode_report_ack[] = {
-		GIP_CMD_ACK, GIP_OPT_INTERNAL, GIP_SEQ0, GIP_PL_LEN(9),
-		0x00, GIP_CMD_VIRTUAL_KEY, GIP_OPT_INTERNAL, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00
-	};
-
-	guard(spinlock_irqsave)(&xpad->odata_lock);
-
-	packet->len = sizeof(mode_report_ack);
-	memcpy(packet->data, mode_report_ack, packet->len);
-	packet->data[2] = seq_num;
-	packet->pending = true;
-
-	/* Reset the sequence so we send out the ack now */
-	xpad->last_out_packet = -1;
-	xpad_try_sending_next_out_packet(xpad);
-}
-
 #ifdef CONFIG_JOYSTICK_XPAD_FF
 static int xpad_play_effect(struct input_dev *dev, void *data, struct ff_effect *effect)
 {
@@ -1599,24 +1041,6 @@ static int xpad_play_effect(struct input_dev *dev, void *data, struct ff_effect
 		packet->pending = true;
 		break;
 
-	case XTYPE_XBOXONE:
-		packet->data[0] = GIP_CMD_RUMBLE; /* activate rumble */
-		packet->data[1] = 0x00;
-		packet->data[2] = xpad->odata_serial++;
-		packet->data[3] = GIP_PL_LEN(9);
-		packet->data[4] = 0x00;
-		packet->data[5] = GIP_MOTOR_ALL;
-		packet->data[6] = 0x00; /* left trigger */
-		packet->data[7] = 0x00; /* right trigger */
-		packet->data[8] = strong / 512;	/* left actuator */
-		packet->data[9] = weak / 512;	/* right actuator */
-		packet->data[10] = 0xFF; /* on period */
-		packet->data[11] = 0x00; /* off period */
-		packet->data[12] = 0xFF; /* repeat count */
-		packet->len = 13;
-		packet->pending = true;
-		break;
-
 	default:
 		dev_dbg(&xpad->dev->dev,
 			"%s - rumble command sent to unsupported xpad type: %d\n",
@@ -1795,13 +1219,6 @@ static int xpad_start_input(struct usb_xpad *xpad)
 	if (usb_submit_urb(xpad->irq_in, GFP_KERNEL))
 		return -EIO;
 
-	if (xpad->xtype == XTYPE_XBOXONE) {
-		error = xpad_start_xbox_one(xpad);
-		if (error) {
-			usb_kill_urb(xpad->irq_in);
-			return error;
-		}
-	}
 	if (xpad->xtype == XTYPE_XBOX360) {
 		/*
 		 * Some third-party controllers Xbox 360-style controllers
@@ -1907,8 +1324,6 @@ static void xpad_close(struct input_dev *dev)
 
 static void xpad_set_up_abs(struct input_dev *input_dev, signed short abs)
 {
-	struct usb_xpad *xpad = input_get_drvdata(input_dev);
-
 	switch (abs) {
 	case ABS_X:
 	case ABS_Y:
@@ -1918,18 +1333,12 @@ static void xpad_set_up_abs(struct input_dev *input_dev, signed short abs)
 		break;
 	case ABS_Z:
 	case ABS_RZ:	/* the triggers (if mapped to axes) */
-		if (xpad->xtype == XTYPE_XBOXONE)
-			input_set_abs_params(input_dev, abs, 0, 1023, 0, 0);
-		else
-			input_set_abs_params(input_dev, abs, 0, 255, 0, 0);
+		input_set_abs_params(input_dev, abs, 0, 255, 0, 0);
 		break;
 	case ABS_HAT0X:
 	case ABS_HAT0Y:	/* the d-pad (only if dpad is mapped to axes */
 		input_set_abs_params(input_dev, abs, -1, 1, 0, 0);
 		break;
-	case ABS_PROFILE: /* 4 value profile button (such as on XAC) */
-		input_set_abs_params(input_dev, abs, 0, 4, 0, 0);
-		break;
 	default:
 		input_set_abs_params(input_dev, abs, 0, 0, 0, 0);
 		break;
@@ -1984,12 +1393,9 @@ static int xpad_init_input(struct usb_xpad *xpad)
 		input_set_capability(input_dev, EV_KEY, xpad_common_btn[i]);
 
 	/* set up model-specific ones */
-	if (xpad->xtype == XTYPE_XBOX360 || xpad->xtype == XTYPE_XBOX360W ||
-	    xpad->xtype == XTYPE_XBOXONE) {
+	if (xpad->xtype == XTYPE_XBOX360 || xpad->xtype == XTYPE_XBOX360W) {
 		for (i = 0; xpad360_btn[i] >= 0; i++)
 			input_set_capability(input_dev, EV_KEY, xpad360_btn[i]);
-		if (xpad->mapping & MAP_SHARE_BUTTON)
-			input_set_capability(input_dev, EV_KEY, KEY_RECORD);
 	} else {
 		for (i = 0; xpad_btn[i] >= 0; i++)
 			input_set_capability(input_dev, EV_KEY, xpad_btn[i]);
@@ -2001,12 +1407,6 @@ static int xpad_init_input(struct usb_xpad *xpad)
 					     xpad_btn_pad[i]);
 	}
 
-	/* set up paddles if the controller has them */
-	if (xpad->mapping & MAP_PADDLES) {
-		for (i = 0; xpad_btn_paddles[i] >= 0; i++)
-			input_set_capability(input_dev, EV_KEY, xpad_btn_paddles[i]);
-	}
-
 	/*
 	 * This should be a simple else block. However historically
 	 * xbox360w has mapped DPAD to buttons while xbox360 did not. This
@@ -2028,10 +1428,6 @@ static int xpad_init_input(struct usb_xpad *xpad)
 			xpad_set_up_abs(input_dev, xpad_abs_triggers[i]);
 	}
 
-	/* setup profile button as an axis with 4 possible values */
-	if (xpad->mapping & MAP_PROFILE_BUTTON)
-		xpad_set_up_abs(input_dev, ABS_PROFILE);
-
 	error = xpad_init_ff(xpad);
 	if (error)
 		goto err_free_input;
@@ -2107,8 +1503,6 @@ static int xpad_probe(struct usb_interface *intf, const struct usb_device_id *id
 		if (intf->cur_altsetting->desc.bInterfaceClass == USB_CLASS_VENDOR_SPEC) {
 			if (intf->cur_altsetting->desc.bInterfaceProtocol == 129)
 				xpad->xtype = XTYPE_XBOX360W;
-			else if (intf->cur_altsetting->desc.bInterfaceProtocol == 208)
-				xpad->xtype = XTYPE_XBOXONE;
 			else
 				xpad->xtype = XTYPE_XBOX360;
 		} else {
@@ -2123,17 +1517,6 @@ static int xpad_probe(struct usb_interface *intf, const struct usb_device_id *id
 			xpad->mapping |= MAP_STICKS_TO_NULL;
 	}
 
-	if (xpad->xtype == XTYPE_XBOXONE &&
-	    intf->cur_altsetting->desc.bInterfaceNumber != GIP_WIRED_INTF_DATA) {
-		/*
-		 * The Xbox One controller lists three interfaces all with the
-		 * same interface class, subclass and protocol. Differentiate by
-		 * interface number.
-		 */
-		error = -ENODEV;
-		goto err_free_in_urb;
-	}
-
 	ep_irq_in = ep_irq_out = NULL;
 
 	for (i = 0; i < 2; i++) {
@@ -2308,15 +1691,6 @@ static int xpad_resume(struct usb_interface *intf)
 	if (input_device_enabled(input))
 		return xpad_start_input(xpad);
 
-	if (xpad->xtype == XTYPE_XBOXONE) {
-		/*
-		 * Even if there are no users, we'll send Xbox One pads
-		 * the startup sequence so they don't sit there and
-		 * blink until somebody opens the input device again.
-		 */
-		return xpad_start_xbox_one(xpad);
-	}
-
 	return 0;
 }
 
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 04/10] Input: xbox_gip - Add HID relaying
From: Vicki Pfau @ 2026-03-10  5:19 UTC (permalink / raw)
  To: Dmitry Torokhov, linux-input; +Cc: Vicki Pfau
In-Reply-To: <20260310052017.1289494-1-vi@endrift.com>

GIP allows tunneling of HID packets, with the HID descriptor embedded in
the GIP metadata exchanged during the initial handshake. This patch creates
a hid_device for this HID descriptor if found, as well as relaying the HID
packets.

Signed-off-by: Vicki Pfau <vi@endrift.com>
---
 drivers/input/joystick/gip/gip-core.c | 95 ++++++++++++++++++++++++++-
 drivers/input/joystick/gip/gip.h      |  2 +
 2 files changed, 94 insertions(+), 3 deletions(-)

diff --git a/drivers/input/joystick/gip/gip-core.c b/drivers/input/joystick/gip/gip-core.c
index 223668ca2b2a9..7355737b29d19 100644
--- a/drivers/input/joystick/gip/gip-core.c
+++ b/drivers/input/joystick/gip/gip-core.c
@@ -547,6 +547,54 @@ int gip_send_vendor_message(struct gip_attachment *attachment,
 		bytes, num_bytes);
 }
 
+static int gip_hid_ll_parse(struct hid_device *hdev)
+{
+	struct gip_attachment *attachment = hdev->driver_data;
+
+	return hid_parse_report(hdev,
+		attachment->metadata.device.hid_descriptor,
+		attachment->metadata.device.hid_descriptor_size);
+}
+
+static int gip_hid_ll_start(struct hid_device *hdev)
+{
+	return 0;
+}
+
+static void gip_hid_ll_stop(struct hid_device *hdev)
+{
+}
+
+static int gip_hid_ll_open(struct hid_device *hdev)
+{
+	return 0;
+}
+
+static void gip_hid_ll_close(struct hid_device *hdev)
+{
+}
+
+static int gip_hid_ll_raw_request(struct hid_device *hdev,
+	unsigned char reportnum, uint8_t *buf, size_t count,
+	unsigned char report_type, int reqtype)
+{
+	/*
+	 * TODO: Based on the metadata, output reports appear to be possible,
+	 * but the chatpad doesn't have the LEDs it claims to support, so
+	 * it's not clear how to test we're sending them properly.
+	 */
+	return 0;
+}
+
+static const struct hid_ll_driver gip_hid_ll_driver = {
+	.parse = gip_hid_ll_parse,
+	.start = gip_hid_ll_start,
+	.stop = gip_hid_ll_stop,
+	.open = gip_hid_ll_open,
+	.close = gip_hid_ll_close,
+	.raw_request = gip_hid_ll_raw_request,
+};
+
 static void gip_metadata_free(struct device *dev, struct gip_metadata *metadata)
 {
 	devm_kfree(dev, metadata->device.audio_formats);
@@ -1350,8 +1398,36 @@ static int gip_send_init_sequence(struct gip_attachment *attachment)
 		if (rc)
 			return rc;
 	}
+	rc = 0;
 
-	return 0;
+	if (attachment->metadata.device.hid_descriptor) {
+		struct hid_device *hdev = hid_allocate_device();
+
+		if (IS_ERR(hdev))
+			return PTR_ERR(hdev);
+
+		hdev->ll_driver = &gip_hid_ll_driver;
+		hdev->bus = BUS_USB;
+		hdev->vendor = attachment->vendor_id;
+		hdev->product = attachment->product_id;
+		hdev->dev.parent = GIP_DEV(attachment);
+		hdev->driver_data = attachment;
+		if (attachment->name)
+			strscpy(hdev->name, attachment->name);
+		else
+			strscpy(hdev->name, "Xbox Chatpad");
+		strscpy(hdev->phys, attachment->phys);
+		rc = hid_add_device(hdev);
+		if (rc) {
+			dev_err(GIP_DEV(attachment), "HID device add failed: %d\n", rc);
+			hid_destroy_device(hdev);
+		} else {
+			rcu_assign_pointer(attachment->hdev, hdev);
+			synchronize_rcu();
+		}
+	}
+
+	return rc;
 }
 
 static void gip_fragment_timeout(struct work_struct *work)
@@ -1784,9 +1860,16 @@ static int gip_handle_command_firmware(struct gip_attachment *attachment,
 static int gip_handle_command_hid_report(struct gip_attachment *attachment,
 	const struct gip_header *header, uint8_t *bytes, int num_bytes)
 {
-	dev_warn(GIP_DEV(attachment), "Unimplemented HID report message\n");
+	struct hid_device *hdev;
 
-	return -ENOTSUPP;
+	guard(rcu)();
+	hdev = rcu_dereference(attachment->hdev);
+	if (hdev)
+		return hid_input_report(hdev, HID_INPUT_REPORT, bytes, num_bytes, true);
+
+	dev_warn(GIP_DEV(attachment), "Got HID report with no HID descriptor\n");
+
+	return -EINVAL;
 }
 
 static int gip_handle_command_extended(struct gip_attachment *attachment,
@@ -2501,6 +2584,7 @@ static int gip_shutdown(struct gip_device *device)
 	for (i = 0; i < MAX_ATTACHMENTS; i++) {
 		struct gip_attachment *attachment = device->attachments[i];
 		struct input_dev *input;
+		struct hid_device *hdev;
 
 		if (!attachment)
 			continue;
@@ -2511,14 +2595,19 @@ static int gip_shutdown(struct gip_device *device)
 
 			rcu_read_lock();
 			input = rcu_dereference(attachment->input);
+			hdev = rcu_dereference(attachment->hdev);
 			rcu_read_unlock();
 
 			rcu_assign_pointer(attachment->input, NULL);
+			rcu_assign_pointer(attachment->hdev, NULL);
 			synchronize_rcu();
 		}
 
 		if (input)
 			input_unregister_device(input);
+
+		if (hdev)
+			hid_destroy_device(hdev);
 	}
 
 	return 0;
diff --git a/drivers/input/joystick/gip/gip.h b/drivers/input/joystick/gip/gip.h
index 63b4929b14e7f..c9d1c4f16c760 100644
--- a/drivers/input/joystick/gip/gip.h
+++ b/drivers/input/joystick/gip/gip.h
@@ -12,6 +12,7 @@
 #ifndef _GIP_H
 #define _GIP_H
 
+#include <linux/hid.h>
 #ifdef CONFIG_JOYSTICK_XBOX_GIP_LEDS
 #include <linux/led-class-multicolor.h>
 #endif
@@ -221,6 +222,7 @@ struct gip_attachment {
 	int extra_axes;
 
 	bool dpad_as_buttons;
+	struct hid_device __rcu *hdev;
 };
 
 struct gip_urb {
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 03/10] Input: xbox_gip - Add controllable LED support
From: Vicki Pfau @ 2026-03-10  5:19 UTC (permalink / raw)
  To: Dmitry Torokhov, linux-input; +Cc: Vicki Pfau
In-Reply-To: <20260310052017.1289494-1-vi@endrift.com>

Xbox One controllers have two different types of controllable LED support:

- Monochrome white, which most controllers have.
- RGBW addressible, which the Elite 2 controllers have.

This exposes both types as led cdevs.

Signed-off-by: Vicki Pfau <vi@endrift.com>
---
 drivers/input/joystick/gip/gip-core.c | 115 ++++++++++++++++++++++++++
 drivers/input/joystick/gip/gip.h      |  10 ++-
 2 files changed, 124 insertions(+), 1 deletion(-)

diff --git a/drivers/input/joystick/gip/gip-core.c b/drivers/input/joystick/gip/gip-core.c
index 0881797592fea..223668ca2b2a9 100644
--- a/drivers/input/joystick/gip/gip-core.c
+++ b/drivers/input/joystick/gip/gip-core.c
@@ -1045,9 +1045,118 @@ static int gip_send_guide_button_led(struct gip_attachment *attachment,
 	if (!gip_supports_system_message(attachment, GIP_CMD_LED, false))
 		return 0;
 
+#ifdef CONFIG_JOYSTICK_XBOX_GIP_LEDS
+	if (!(attachment->features & GIP_FEATURE_GUIDE_COLOR))
+		attachment->guide_led.standard.brightness = intensity;
+#endif
+
 	return gip_send_system_message(attachment, GIP_CMD_LED, 0, buffer, sizeof(buffer));
 }
 
+#ifdef CONFIG_JOYSTICK_XBOX_GIP_LEDS
+static int gip_send_guide_button_color_led(struct gip_attachment *attachment,
+	uint8_t r, uint8_t g, uint8_t b, uint8_t w)
+{
+	uint8_t buffer[] = { 0x00, w, r, g, b };
+
+	if (!(attachment->features & GIP_FEATURE_GUIDE_COLOR))
+		return -EINVAL;
+
+	attachment->guide_led.color.subled_info[0].brightness = r;
+	attachment->guide_led.color.subled_info[1].brightness = g;
+	attachment->guide_led.color.subled_info[2].brightness = b;
+	attachment->guide_led.color.subled_info[3].brightness = w;
+
+	return gip_send_vendor_message(attachment, GIP_CMD_GUIDE_COLOR, 0, buffer, sizeof(buffer));
+}
+
+static int gip_guide_led_set(struct led_classdev *led,
+	enum led_brightness value)
+{
+	struct gip_attachment *attachment = container_of(led,
+		struct gip_attachment, guide_led.standard);
+
+	guard(mutex)(&attachment->lock);
+	return gip_send_guide_button_led(attachment, GIP_LED_GUIDE_ON, value);
+}
+
+static int gip_guide_color_led_set(struct led_classdev *led,
+	enum led_brightness value)
+{
+	struct led_classdev_mc *mc_cdev = container_of(led,
+		struct led_classdev_mc, led_cdev);
+	struct gip_attachment *attachment = container_of(mc_cdev,
+		struct gip_attachment, guide_led.color);
+
+	led_mc_calc_color_components(mc_cdev, value);
+	guard(mutex)(&attachment->lock);
+	return gip_send_guide_button_color_led(attachment,
+		mc_cdev->subled_info[0].brightness,
+		mc_cdev->subled_info[1].brightness,
+		mc_cdev->subled_info[2].brightness,
+		mc_cdev->subled_info[3].brightness);
+}
+
+static int gip_guide_led_probe(struct gip_attachment *attachment, struct device *dev)
+{
+	int rc = 0;
+
+	if (!gip_supports_system_message(attachment, GIP_CMD_LED, false))
+		return 0;
+
+	if (attachment->features & GIP_FEATURE_GUIDE_COLOR) {
+		struct mc_subled *mc_led_info;
+		struct led_classdev_mc *mc_cdev = &attachment->guide_led.color;
+		struct led_classdev *cdev = &mc_cdev->led_cdev;
+
+		mc_led_info = devm_kcalloc(dev, 4,
+			sizeof(*mc_led_info), GFP_KERNEL);
+		if (!mc_led_info)
+			return -ENOMEM;
+
+		mc_led_info[0].color_index = LED_COLOR_ID_RED;
+		mc_led_info[1].color_index = LED_COLOR_ID_GREEN;
+		mc_led_info[2].color_index = LED_COLOR_ID_BLUE;
+		mc_led_info[3].color_index = LED_COLOR_ID_WHITE;
+
+		mc_cdev->subled_info = mc_led_info;
+		mc_cdev->num_colors = 4;
+
+		cdev->brightness = 51;
+		cdev->max_brightness = 255;
+		cdev->flags = LED_CORE_SUSPENDRESUME | LED_RETAIN_AT_SHUTDOWN;
+		cdev->brightness_set_blocking = gip_guide_color_led_set;
+		cdev->name = devm_kasprintf(dev, GFP_KERNEL,
+			"%s:rgb:power", dev_name(dev));
+		if (!cdev->name)
+			rc = -ENOMEM;
+
+		if (!rc)
+			rc = devm_led_classdev_multicolor_register(dev,
+				&attachment->guide_led.color);
+
+		if (rc)
+			devm_kfree(dev, mc_led_info);
+	} else {
+		struct led_classdev *cdev = &attachment->guide_led.standard;
+
+		cdev->max_brightness = GIP_LED_GUIDE_MAX_BRIGHTNESS;
+		cdev->brightness = GIP_LED_GUIDE_INIT_BRIGHTNESS;
+		cdev->flags = LED_CORE_SUSPENDRESUME | LED_RETAIN_AT_SHUTDOWN;
+		cdev->brightness_set_blocking = gip_guide_led_set;
+		cdev->name = devm_kasprintf(dev, GFP_KERNEL,
+			"%s:white:power", dev_name(dev));
+		if (!cdev->name)
+			return -ENOMEM;
+
+		rc = devm_led_classdev_register(dev,
+			&attachment->guide_led.standard);
+	}
+
+	return rc;
+}
+#endif
+
 static bool gip_send_set_device_state(struct gip_attachment *attachment, uint8_t state)
 {
 	uint8_t buffer[] = { state };
@@ -1154,6 +1263,12 @@ static int gip_setup_input_device(struct gip_attachment *attachment)
 	if (rc)
 		goto err_free_device;
 
+#ifdef CONFIG_JOYSTICK_XBOX_GIP_LEDS
+	rc = gip_guide_led_probe(attachment, &input->dev);
+	if (rc)
+		dev_err(GIP_DEV(attachment), "Failed to register LEDs: %d\n", rc);
+#endif
+
 	return 0;
 
 err_free_device:
diff --git a/drivers/input/joystick/gip/gip.h b/drivers/input/joystick/gip/gip.h
index 2c60430c81590..63b4929b14e7f 100644
--- a/drivers/input/joystick/gip/gip.h
+++ b/drivers/input/joystick/gip/gip.h
@@ -12,6 +12,9 @@
 #ifndef _GIP_H
 #define _GIP_H
 
+#ifdef CONFIG_JOYSTICK_XBOX_GIP_LEDS
+#include <linux/led-class-multicolor.h>
+#endif
 #include <linux/rcupdate.h>
 #include <linux/usb/input.h>
 
@@ -201,6 +204,12 @@ struct gip_attachment {
 	uint8_t seq_vendor;
 
 	int device_state;
+#ifdef CONFIG_JOYSTICK_XBOX_GIP_LEDS
+	union {
+		struct led_classdev standard;
+		struct led_classdev_mc color;
+	} guide_led;
+#endif
 
 	struct gip_extended_status status;
 
@@ -212,7 +221,6 @@ struct gip_attachment {
 	int extra_axes;
 
 	bool dpad_as_buttons;
-	struct hid_device __rcu *hdev;
 };
 
 struct gip_urb {
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 01/10] Input: xbox_gip - Add new driver for Xbox GIP
From: Vicki Pfau @ 2026-03-10  5:19 UTC (permalink / raw)
  To: Dmitry Torokhov, linux-input; +Cc: Vicki Pfau
In-Reply-To: <20260310052017.1289494-1-vi@endrift.com>

This introduces a new driver for the Xbox One/Series controller protocol,
officially known as the Gaming Input Protocol, or GIP for short.

Microsoft released documentation on (some of) GIP in late 2024, upon which
this driver is based. Though the documentation was incomplete, it still
provided enough information to warrant a clean start over the previous,
incomplete implementation.

This driver is already at feature parity with the GIP support in xpad,
along with several more enhancements:

- Proper support for parsing message length and fragmented messages
- Metadata parsing, allowing for auto-detection on various parameters,
  including the presence and location in the message of the share button,
  as well as detection of specific device types

The framework set out in this driver also allows future expansion for
specialized device types and additional features more cleanly than xpad.

Future plans include:

- Adding support for more device types, such as arcade sticks, racing
  wheels and flight sticks.
- Support for the security handshake, which is required for devices that
  use wireless dongles.
- Exposing a raw character device to enable sending vendor-specific
  commands from userspace.
- Event logging to either sysfs or dmesg.
- Support for the headphone jack.

Signed-off-by: Vicki Pfau <vi@endrift.com>
---
 MAINTAINERS                              |    6 +
 drivers/input/joystick/Kconfig           |    2 +
 drivers/input/joystick/Makefile          |    1 +
 drivers/input/joystick/gip/Kconfig       |   30 +
 drivers/input/joystick/gip/Makefile      |    3 +
 drivers/input/joystick/gip/gip-core.c    | 2536 ++++++++++++++++++++++
 drivers/input/joystick/gip/gip-drivers.c |  210 ++
 drivers/input/joystick/gip/gip.h         |  309 +++
 8 files changed, 3097 insertions(+)
 create mode 100644 drivers/input/joystick/gip/Kconfig
 create mode 100644 drivers/input/joystick/gip/Makefile
 create mode 100644 drivers/input/joystick/gip/gip-core.c
 create mode 100644 drivers/input/joystick/gip/gip-drivers.c
 create mode 100644 drivers/input/joystick/gip/gip.h

diff --git a/MAINTAINERS b/MAINTAINERS
index 9ed6d11a77466..6c744d0af359d 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -27927,6 +27927,12 @@ S:	Maintained
 F:	drivers/media/rc/keymaps/rc-xbox-dvd.c
 F:	drivers/media/rc/xbox_remote.c
 
+XBOX GIP
+M:	Vicki Pfau <vi@endrift.com>
+L:	linux-input@vger.kernel.org
+S:	Maintained
+F:	drivers/input/joystick/gip/
+
 XC2028/3028 TUNER DRIVER
 M:	Mauro Carvalho Chehab <mchehab@kernel.org>
 L:	linux-media@vger.kernel.org
diff --git a/drivers/input/joystick/Kconfig b/drivers/input/joystick/Kconfig
index 7755e5b454d2c..d4665c80a3713 100644
--- a/drivers/input/joystick/Kconfig
+++ b/drivers/input/joystick/Kconfig
@@ -291,6 +291,8 @@ config JOYSTICK_JOYDUMP
 	  To compile this driver as a module, choose M here: the
 	  module will be called joydump.
 
+source "drivers/input/joystick/gip/Kconfig"
+
 config JOYSTICK_XPAD
 	tristate "Xbox gamepad support"
 	depends on USB_ARCH_HAS_HCD
diff --git a/drivers/input/joystick/Makefile b/drivers/input/joystick/Makefile
index 9976f596a9208..323392921b7dc 100644
--- a/drivers/input/joystick/Makefile
+++ b/drivers/input/joystick/Makefile
@@ -39,5 +39,6 @@ obj-$(CONFIG_JOYSTICK_TURBOGRAFX)	+= turbografx.o
 obj-$(CONFIG_JOYSTICK_TWIDJOY)		+= twidjoy.o
 obj-$(CONFIG_JOYSTICK_WARRIOR)		+= warrior.o
 obj-$(CONFIG_JOYSTICK_WALKERA0701)	+= walkera0701.o
+obj-$(CONFIG_JOYSTICK_XBOX_GIP)		+= gip/
 obj-$(CONFIG_JOYSTICK_XPAD)		+= xpad.o
 obj-$(CONFIG_JOYSTICK_ZHENHUA)		+= zhenhua.o
diff --git a/drivers/input/joystick/gip/Kconfig b/drivers/input/joystick/gip/Kconfig
new file mode 100644
index 0000000000000..83293df3b0410
--- /dev/null
+++ b/drivers/input/joystick/gip/Kconfig
@@ -0,0 +1,30 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# Gaming Input Protocol  driver configuration
+#
+config JOYSTICK_XBOX_GIP
+	tristate "Xbox One/Series controller support"
+	depends on USB_ARCH_HAS_HCD
+	select USB
+	help
+	  Say Y here if you want to use Xbox One and Series controllers with your
+	  computer. Make sure to say Y to "Joystick support" (CONFIG_INPUT_JOYDEV)
+	  and/or "Event interface support" (CONFIG_INPUT_EVDEV) as well.
+
+	  To compile this driver as a module, choose M here: the
+	  module will be called xbox_gip.
+
+config JOYSTICK_XBOX_GIP_FF
+	bool "Xbox One/Series controller rumble support"
+	depends on JOYSTICK_XBOX_GIP && INPUT
+	select INPUT_FF_MEMLESS
+	help
+	  Say Y here if you want to take advantage of Xbox One/Series rumble.
+
+config JOYSTICK_XBOX_GIP_LEDS
+	bool "LED Support for the Xbox One/Series controller Guide button"
+	depends on JOYSTICK_XBOX_GIP && LEDS_CLASS_MULTICOLOR
+	help
+	  This option enables support for the LED which surrounds the Big X on
+	  Xbox One/Series controllers.
+
diff --git a/drivers/input/joystick/gip/Makefile b/drivers/input/joystick/gip/Makefile
new file mode 100644
index 0000000000000..a75e0cace0f92
--- /dev/null
+++ b/drivers/input/joystick/gip/Makefile
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+obj-$(CONFIG_JOYSTICK_XBOX_GIP)	+= xbox-gip.o
+xbox-gip-y := gip-core.o gip-drivers.o
diff --git a/drivers/input/joystick/gip/gip-core.c b/drivers/input/joystick/gip/gip-core.c
new file mode 100644
index 0000000000000..0881797592fea
--- /dev/null
+++ b/drivers/input/joystick/gip/gip-core.c
@@ -0,0 +1,2536 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Gaming Input Protocol driver for Xbox One/Series controllers
+ *
+ * Copyright (c) 2025 Valve Software
+ *
+ * TODO:
+ * - Audio device support
+ * - Security packet handshake
+ * - Event logging
+ * - Sending fragmented messages
+ * - Raw character device
+ * - Wheel support
+ * - Flight stick support
+ * - Arcade stick support
+ * - Split into driver-per-attachment GIP-as-a-bus approach drivers
+ *
+ * This driver is based on the Microsoft GIP spec at:
+ * https://aka.ms/gipdocs
+ * https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gipusb/e7c90904-5e21-426e-b9ad-d82adeee0dbc
+ */
+
+#include <linux/module.h>
+#include <linux/uuid.h>
+#include "gip.h"
+
+#define GIP_WIRED_INTF_DATA 0
+#define GIP_WIRED_INTF_AUDIO 1
+
+#define MAX_MESSAGE_LENGTH 0x4000
+
+#define MAX_AUDIO_MESSAGES 9
+
+#define GIP_DATA_CLASS_COMMAND		(0u << 5)
+#define GIP_DATA_CLASS_LOW_LATENCY	(1u << 5)
+#define GIP_DATA_CLASS_STANDARD_LATENCY	(2u << 5)
+#define GIP_DATA_CLASS_AUDIO		(3u << 5)
+
+#define GIP_DATA_CLASS_SHIFT 5
+#define GIP_DATA_CLASS_MASK (7u << 5)
+
+/* Undocumented Elite 2 vendor messages */
+#define GIP_CMD_RAW_REPORT		0x0c
+#define GIP_CMD_GUIDE_COLOR		0x0e
+#define GIP_SL_ELITE_CONFIG		0x4d
+
+#define GIP_BTN_OFFSET_XBE1 28
+#define GIP_BTN_OFFSET_XBE2 14
+
+#define GIP_FLAG_FRAGMENT	BIT(7)
+#define GIP_FLAG_INIT_FRAG	BIT(6)
+#define GIP_FLAG_SYSTEM		BIT(5)
+#define GIP_FLAG_ACME		BIT(4)
+#define GIP_FLAG_ATTACHMENT_MASK 0x7
+
+#define GIP_AUDIO_FORMAT_NULL		0
+#define GIP_AUDIO_FORMAT_8000HZ_1CH	1
+#define GIP_AUDIO_FORMAT_8000HZ_2CH	2
+#define GIP_AUDIO_FORMAT_12000HZ_1CH	3
+#define GIP_AUDIO_FORMAT_12000HZ_2CH	4
+#define GIP_AUDIO_FORMAT_16000HZ_1CH	5
+#define GIP_AUDIO_FORMAT_16000HZ_2CH	6
+#define GIP_AUDIO_FORMAT_20000HZ_1CH	7
+#define GIP_AUDIO_FORMAT_20000HZ_2CH	8
+#define GIP_AUDIO_FORMAT_24000HZ_1CH	9
+#define GIP_AUDIO_FORMAT_24000HZ_2CH	10
+#define GIP_AUDIO_FORMAT_32000HZ_1CH	11
+#define GIP_AUDIO_FORMAT_32000HZ_2CH	12
+#define GIP_AUDIO_FORMAT_40000HZ_1CH	13
+#define GIP_AUDIO_FORMAT_40000HZ_2CH	14
+#define GIP_AUDIO_FORMAT_48000HZ_1CH	15
+#define GIP_AUDIO_FORMAT_48000HZ_2CH	16
+#define GIP_AUDIO_FORMAT_48000HZ_6CH	32
+#define GIP_AUDIO_FORMAT_48000HZ_8CH	33
+#define MAX_GIP_AUDIO_FORMAT GIP_AUDIO_FORMAT_48000HZ_8CH
+
+/* Protocol Control constants */
+#define GIP_CONTROL_CODE_ACK	0
+#define GIP_CONTROL_CODE_NACK	1 /* obsolete */
+#define GIP_CONTROL_CODE_UNK	2 /* obsolete */
+#define GIP_CONTROL_CODE_AB	3 /* obsolete */
+#define GIP_CONTROL_CODE_MPER	4 /* obsolete */
+#define GIP_CONTROL_CODE_STOP	5 /* obsolete */
+#define GIP_CONTROL_CODE_START	6 /* obsolete */
+#define GIP_CONTROL_CODE_ERR	7 /* obsolete */
+
+/* Status Device constants */
+#define GIP_POWER_LEVEL_OFF	0
+#define GIP_POWER_LEVEL_STANDBY	1 /* obsolete */
+#define GIP_POWER_LEVEL_FULL	2
+
+#define GIP_NOT_CHARGING	0
+#define GIP_CHARGING		1
+#define GIP_CHARGE_ERROR	2
+
+#define GIP_BATTERY_ABSENT		0
+#define GIP_BATTERY_STANDARD		1
+#define GIP_BATTERY_RECHARGEABLE	2
+
+#define GIP_BATTERY_CRITICAL	0
+#define GIP_BATTERY_LOW		1
+#define GIP_BATTERY_MEDIUM	2
+#define GIP_BATTERY_FULL	3
+
+#define GIP_EVENT_FAULT 0x0002
+
+#define GIP_FAULT_UNKNOWN	0
+#define GIP_FAULT_HARD		1
+#define GIP_FAULT_NMI		2
+#define GIP_FAULT_SVC		3
+#define GIP_FAULT_PEND_SV	4
+#define GIP_FAULT_SMART_PTR	5
+#define GIP_FAULT_MCU		6
+#define GIP_FAULT_BUS		7
+#define GIP_FAULT_USAGE		8
+#define GIP_FAULT_RADIO_HANG	9
+#define GIP_FAULT_WATCHDOG	10
+#define GIP_FAULT_LINK_STALL	11
+#define GIP_FAULT_ASSERTION	12
+
+/* Metadata constants */
+#define GIP_MESSAGE_FLAG_BIG_ENDIAN		BIT(0)
+#define GIP_MESSAGE_FLAG_RELIABLE		BIT(1)
+#define GIP_MESSAGE_FLAG_SEQUENCED		BIT(2)
+#define GIP_MESSAGE_FLAG_DOWNSTREAM		BIT(3)
+#define GIP_MESSAGE_FLAG_UPSTREAM		BIT(4)
+#define GIP_MESSAGE_FLAG_DS_REQUEST_RESPONSE	BIT(5)
+
+#define GIP_DATA_TYPE_CUSTOM	1
+#define GIP_DATA_TYPE_AUDIO	2
+#define GIP_DATA_TYPE_SECURITY	3
+#define GIP_DATA_TYPE_GIP	4
+
+/* Set Device State constants */
+#define GIP_STATE_START		0
+#define GIP_STATE_STOP		1
+#define GIP_STATE_STANDBY	2 /* obsolete */
+#define GIP_STATE_FULL_POWER	3
+#define GIP_STATE_OFF		4
+#define GIP_STATE_QUIESCE	5
+#define GIP_STATE_UNK6		6
+#define GIP_STATE_RESET		7
+
+/* Guide Button Status constants */
+#define GIP_LED_GUIDE	0
+#define GIP_LED_IR	1 /* deprecated, for Kinect */
+
+#define GIP_LED_GUIDE_OFF		0
+#define GIP_LED_GUIDE_ON		1
+#define GIP_LED_GUIDE_FAST_BLINK	2
+#define GIP_LED_GUIDE_SLOW_BLINK	3
+#define GIP_LED_GUIDE_CHARGING_BLINK	4
+#define GIP_LED_GUIDE_RAMP_TO_LEVEL	0xd
+
+#define GIP_LED_IR_OFF		0
+#define GIP_LED_IR_ON_100MS	1
+#define GIP_LED_IR_PATTERN	4
+
+/* Direct Motor Command constants */
+#define GIP_MOTOR_RIGHT_VIBRATION	BIT(0)
+#define GIP_MOTOR_LEFT_VIBRATION	BIT(1)
+#define GIP_MOTOR_RIGHT_IMPULSE		BIT(2)
+#define GIP_MOTOR_LEFT_IMPULSE		BIT(3)
+#define GIP_MOTOR_ALL 0xf
+
+/* Extended Command constants */
+#define GIP_EXTCMD_GET_CAPABILITIES	0x00
+#define GIP_EXTCMD_GET_TELEMETRY_DATA	0x01
+#define GIP_EXTCMD_GET_SERIAL_NUMBER	0x04
+
+#define GIP_EXTENDED_STATUS_OK			0
+#define GIP_EXTENDED_STATUS_NOT_SUPPORTED	1
+#define GIP_EXTENDED_STATUS_NOT_READY		2
+#define GIP_EXTENDED_STATUS_ACCESS_DENIED	3
+#define GIP_EXTENDED_STATUS_FAILED		4
+
+/* Internal constants, not part of protocol */
+#define GIP_DEFAULT_IN_SYSTEM_MESSAGES 0x5e
+#define GIP_DEFAULT_OUT_SYSTEM_MESSAGES 0x472
+
+#define GIP_FEATURE_CONTROLLER				BIT(0)
+#define GIP_FEATURE_CONSOLE_FUNCTION_MAP		BIT(1)
+#define GIP_FEATURE_CONSOLE_FUNCTION_MAP_OVERFLOW	BIT(2)
+#define GIP_FEATURE_ELITE_BUTTONS			BIT(3)
+#define GIP_FEATURE_DYNAMIC_LATENCY_INPUT		BIT(4)
+#define GIP_FEATURE_SECURITY_OPT_OUT			BIT(5)
+#define GIP_FEATURE_MOTOR_CONTROL			BIT(6)
+#define GIP_FEATURE_GUIDE_COLOR				BIT(7)
+#define GIP_FEATURE_EXTENDED_SET_DEVICE_STATE		BIT(8)
+
+#define GIP_LED_GUIDE_MAX_BRIGHTNESS	100 /* Spec says 47, but larger values work */
+#define GIP_LED_GUIDE_INIT_BRIGHTNESS	20
+
+#ifndef VK_LWIN
+#define VK_LWIN 0x5b
+#endif
+
+static const guid_t guid_console_function_map =
+	GUID_INIT(0xecddd2fe, 0xd387, 0x4294, 0xbd, 0x96, 0x1a, 0x71, 0x2e, 0x3d, 0xc7, 0x7d);
+static const guid_t guid_console_function_map_overflow =
+	GUID_INIT(0x137d4bd0, 0x9347, 0x4472, 0xaa, 0x26, 0x8c, 0x34, 0xa0, 0x8f, 0xf9, 0xbd);
+static const guid_t guid_controller =
+	GUID_INIT(0x9776ff56, 0x9bfd, 0x4581, 0xad, 0x45, 0xb6, 0x45, 0xbb, 0xa5, 0x26, 0xd6);
+static const guid_t guid_dev_auth_pc_opt_out =
+	GUID_INIT(0x7a34ce77, 0x7de2, 0x45c6, 0x8c, 0xa4, 0x00, 0x42, 0xc0, 0x8b, 0xd9, 0x4a);
+static const guid_t guid_dynamic_latency_input =
+	GUID_INIT(0x87f2e56b, 0xc3bb, 0x49b1, 0x82, 0x65, 0xff, 0xff, 0xf3, 0x77, 0x99, 0xee);
+static const guid_t guid_elite_buttons =
+	GUID_INIT(0x37d19ff7, 0xb5c6, 0x49d1, 0xa7, 0x5e, 0x03, 0xb2, 0x4b, 0xef, 0x8c, 0x89);
+static const guid_t guid_headset =
+	GUID_INIT(0xbc25d1a3, 0xc24e, 0x4992, 0x9d, 0xda, 0xef, 0x4f, 0x12, 0x3e, 0xf5, 0xdc);
+
+/*
+ * The following GUIDs are observed, but the exact meanings aren't known, so
+ * for now we document them but don't use them anywhere.
+ *
+ * GamepadEmu: GUID_INIT(0xe2e5f1bc, 0xa6e6, 0x41a2, 0x8f, 0x43, 0x33, 0xcf, 0xa2, 0x51, 0x09, 0x81)
+ * IAudioOnly: GUID_INIT(0x92844cd1, 0xf7c8, 0x49ef, 0x97, 0x77, 0x46, 0x7d, 0xa7, 0x08, 0xad, 0x10)
+ * IControllerProfileModeState: GUID_INIT(0xf758dc66, 0x022c, 0x48b8, 0xa4, 0xf6, 0x45, 0x7b, 0xa8, 0x0e, 0x2a, 0x5b)
+ * ICustomAudio: GUID_INIT(0x63fd9cc9, 0x94ee, 0x4b5d, 0x9c, 0x4d, 0x8b, 0x86, 0x4c, 0x14, 0x9c, 0xac)
+ * IExtendedDeviceFlags: GUID_INIT(0x34ad9b1e, 0x36ad, 0x4fb5, 0x8a, 0xc7, 0x17, 0x23, 0x4c, 0x9f, 0x54, 0x6f)
+ * IProgrammableGamepad: GUID_INIT(0x31c1034d, 0xb5b7, 0x4551, 0x98, 0x13, 0x87, 0x69, 0xd4, 0xa0, 0xe4, 0xf9)
+ * IVirtualDevice: GUID_INIT(0xdfd26825, 0x110a, 0x4e94, 0xb9, 0x37, 0xb2, 0x7c, 0xe4, 0x7b, 0x25, 0x40)
+ * OnlineDevAuth: GUID_INIT(0x632b1fd1, 0xa3e9, 0x44f9, 0x84, 0x20, 0x5c, 0xe3, 0x44, 0xa0, 0x64, 0x04)
+ *
+ * Seen on Elite Controller, Adaptive Controller: 9ebd00a3-b5e6-4c08-a33b-673126459ec4
+ * Seen on Adaptive Controller: ce1e58c5-221c-4bdb-9c24-bf3941601320
+ * Seen on Adaptive Joystick: db02f681-5038-4219-8668-c3459c5c3293
+ * Seen on Elite 2 Controller: f758dc66-022c-48b8-a4f6-457ba80e2a5b (IControllerProfileModeState)
+ * Seen on Elite 2 Controller: 31c1034d-b5b7-4551-9813-8769d4a0e4f9 (IProgrammableGamepad)
+ * Seen on Elite 2 Controller: 34ad9b1e-36ad-4fb5-8ac7-17234c9f546f (IExtendedDeviceFlags)
+ * Seen on Elite 2 Controller: 88e0b694-6bd9-4416-a560-e7fafdfa528f
+ * Seen on Elite 2 Controller: ea96c8c0-b216-448b-be80-7e5deb0698e2
+ */
+
+static const int gip_data_class_mtu[8] = { 64, 64, 64, 2048, 0, 0, 0, 0 };
+
+struct gip_audio_format {
+	uint16_t rate;
+	uint8_t channels;
+};
+
+static const struct gip_audio_format gip_audio_format_table[MAX_GIP_AUDIO_FORMAT + 1] = {
+	[GIP_AUDIO_FORMAT_8000HZ_1CH] = { .rate = 8000, .channels = 1 },
+	[GIP_AUDIO_FORMAT_8000HZ_2CH] = { .rate = 8000, .channels = 2 },
+	[GIP_AUDIO_FORMAT_12000HZ_1CH] = { .rate = 12000, .channels = 1 },
+	[GIP_AUDIO_FORMAT_12000HZ_2CH] = { .rate = 12000, .channels = 2 },
+	[GIP_AUDIO_FORMAT_16000HZ_1CH] = { .rate = 16000, .channels = 1 },
+	[GIP_AUDIO_FORMAT_16000HZ_2CH] = { .rate = 16000, .channels = 2 },
+	[GIP_AUDIO_FORMAT_20000HZ_1CH] = { .rate = 20000, .channels = 1 },
+	[GIP_AUDIO_FORMAT_20000HZ_2CH] = { .rate = 20000, .channels = 2 },
+	[GIP_AUDIO_FORMAT_24000HZ_1CH] = { .rate = 24000, .channels = 1 },
+	[GIP_AUDIO_FORMAT_24000HZ_2CH] = { .rate = 24000, .channels = 2 },
+	[GIP_AUDIO_FORMAT_32000HZ_1CH] = { .rate = 32000, .channels = 1 },
+	[GIP_AUDIO_FORMAT_32000HZ_2CH] = { .rate = 32000, .channels = 2 },
+	[GIP_AUDIO_FORMAT_40000HZ_1CH] = { .rate = 40000, .channels = 1 },
+	[GIP_AUDIO_FORMAT_40000HZ_2CH] = { .rate = 40000, .channels = 2 },
+	[GIP_AUDIO_FORMAT_48000HZ_1CH] = { .rate = 48000, .channels = 1 },
+	[GIP_AUDIO_FORMAT_48000HZ_2CH] = { .rate = 48000, .channels = 2 },
+	[GIP_AUDIO_FORMAT_48000HZ_6CH] = { .rate = 48000, .channels = 6 },
+	[GIP_AUDIO_FORMAT_48000HZ_8CH] = { .rate = 48000, .channels = 8 },
+};
+
+
+static const struct gip_quirks base_quirks[] = {
+	/* PDP Rock Candy */
+	{ 0x0e6f, 0x0246, 0, .quirks = GIP_QUIRK_NO_HELLO },
+
+	{0},
+};
+
+struct gip_audio_format_pair {
+	uint8_t inbound;
+	uint8_t outbound;
+};
+static_assert(sizeof(struct gip_audio_format_pair) == 2);
+
+struct gip_hello_device {
+	uint64_t device_id;
+	uint16_t vendor_id;
+	uint16_t product_id;
+	uint16_t firmware_major_version;
+	uint16_t firmware_minor_version;
+	uint16_t firmware_build_version;
+	uint16_t firmware_revision;
+	uint8_t hardware_major_version;
+	uint8_t hardware_minor_version;
+	uint8_t rf_proto_major_version;
+	uint8_t rf_proto_minor_version;
+	uint8_t security_major_version;
+	uint8_t security_minor_version;
+	uint8_t gip_major_version;
+	uint8_t gip_minor_version;
+};
+
+struct gip_direct_motor {
+	uint8_t command;
+	uint8_t motor_bitmap;
+	uint8_t left_impulse_level;
+	uint8_t right_impulse_level;
+	uint8_t left_vibration_level;
+	uint8_t right_vibration_level;
+	uint8_t duration;
+	uint8_t delay;
+	uint8_t repeat;
+};
+
+static const struct gip_driver* base_drivers[] = {
+	&gip_driver_navigation,
+	&gip_driver_gamepad,
+	NULL /* Sentinel */
+};
+
+static int gip_decode_length(uint64_t *length, const uint8_t *bytes, int num_bytes)
+{
+	*length = 0;
+	int offset;
+
+	for (offset = 0; offset < num_bytes; offset++) {
+		uint8_t byte = bytes[offset];
+
+		*length |= (byte & 0x7full) << (offset * 7);
+		if (!(byte & 0x80)) {
+			offset++;
+			break;
+		}
+	}
+	return offset;
+}
+
+static int gip_encode_length(uint64_t length, uint8_t *bytes, int num_bytes)
+{
+	int offset;
+
+	for (offset = 0; offset < num_bytes; offset++) {
+		uint8_t byte = length & 0x7f;
+
+		length >>= 7;
+		if (length)
+			byte |= 0x80;
+		bytes[offset] = byte;
+		if (!length) {
+			offset++;
+			break;
+		}
+	}
+	return offset;
+}
+
+static bool gip_supports_system_message(struct gip_attachment *attachment,
+	uint8_t command, bool upstream)
+{
+	if (upstream)
+		return attachment->metadata.device
+			.in_system_messages[command >> 5] & (1u << command);
+	else
+		return attachment->metadata.device
+			.out_system_messages[command >> 5] & (1u << command);
+}
+
+bool gip_supports_vendor_message(struct gip_attachment *attachment,
+	uint8_t command, bool upstream)
+{
+	size_t i;
+
+	for (i = 0; i < attachment->metadata.num_messages; i++) {
+		struct gip_message_metadata *metadata =
+			&attachment->metadata.message_metadata[i];
+
+		if (metadata->type != command)
+			continue;
+		if (metadata->flags & GIP_MESSAGE_FLAG_DS_REQUEST_RESPONSE)
+			return true;
+
+		if (upstream)
+			return metadata->flags & GIP_MESSAGE_FLAG_UPSTREAM;
+		else
+			return metadata->flags & GIP_MESSAGE_FLAG_DOWNSTREAM;
+	}
+	return false;
+}
+
+static uint8_t gip_sequence_next(struct gip_attachment *attachment,
+	uint8_t command, bool system)
+{
+	uint8_t seq;
+
+	if (system) {
+		switch (command) {
+		case GIP_CMD_SECURITY:
+			seq = attachment->seq_security++;
+			if (!seq)
+				seq = attachment->seq_security++;
+			break;
+		case GIP_CMD_EXTENDED:
+			seq = attachment->seq_extended++;
+			if (!seq)
+				seq = attachment->seq_extended++;
+			break;
+		case GIP_AUDIO_DATA:
+			seq = attachment->seq_audio++;
+			if (!seq)
+				seq = attachment->seq_audio++;
+			break;
+		default:
+			seq = attachment->seq_system++;
+			if (!seq)
+				seq = attachment->seq_system++;
+			break;
+		}
+	} else {
+		seq = attachment->seq_vendor++;
+		if (!seq)
+			seq = attachment->seq_vendor++;
+	}
+	return seq;
+}
+
+static void gip_handle_quirks_array(struct gip_attachment *attachment,
+	const struct gip_quirks *quirks)
+{
+	size_t i, j;
+
+	for (i = 0; quirks[i].vendor_id; i++) {
+		if (quirks[i].vendor_id != attachment->vendor_id)
+			continue;
+		if (quirks[i].product_id != attachment->product_id)
+			continue;
+		if (quirks[i].attachment_index != attachment->attachment_index)
+			continue;
+
+		attachment->features |= quirks[i].added_features;
+		attachment->features &= ~quirks[i].filtered_features;
+		attachment->quirks |= quirks[i].quirks;
+
+		if (quirks[i].override_name)
+			attachment->name = quirks[i].override_name;
+
+		for (j = 0; j < 8; ++j) {
+			struct gip_device_metadata *metadata = &attachment->metadata.device;
+
+			metadata->in_system_messages[j] |= quirks[i].extra_in_system[j];
+			metadata->out_system_messages[j] |= quirks[i].extra_out_system[j];
+		}
+
+		attachment->extra_buttons = quirks[i].extra_buttons;
+		attachment->extra_axes = quirks[i].extra_axes;
+		break;
+	}
+
+}
+
+static void gip_handle_quirks(struct gip_attachment *attachment)
+{
+	gip_handle_quirks_array(attachment, base_quirks);
+
+	if (attachment->driver && attachment->driver->quirks)
+		gip_handle_quirks_array(attachment, attachment->driver->quirks);
+}
+
+static int gip_send_raw_message(struct gip_device *device,
+	uint8_t message_type, uint8_t flags, uint8_t seq, const uint8_t *bytes,
+	int num_bytes)
+{
+	struct gip_interface *intf;
+	int offset = 3;
+	struct gip_urb *urb = NULL;
+	int i;
+	int rc = 0;
+
+	if (num_bytes < 0) {
+		dev_warn(GIP_DEV(device), "Invalid message length %d\n", num_bytes);
+		return -EINVAL;
+	}
+
+	if (num_bytes > gip_data_class_mtu[message_type >> GIP_DATA_CLASS_SHIFT]) {
+		dev_err(GIP_DEV(device),
+			"Attempted to send a message that requires fragmenting, which is not yet supported.\n");
+		return -ENOTSUPP;
+	}
+
+	if ((message_type & GIP_DATA_CLASS_MASK) == GIP_DATA_CLASS_AUDIO)
+		intf = &device->audio;
+	else
+		intf = &device->data;
+
+	if (intf->isoc_messages) {
+		/* TODO: Needed for audio support */
+		dev_warn(GIP_DEV(intf), "Unimplemented isochronous message output\n");
+		return -ENOTSUPP;
+	}
+
+	guard(spinlock_irqsave)(&device->message_lock);
+	for (i = 0; i < MAX_OUT_MESSAGES && !urb; i++) {
+		if (!intf->out_queue[i].urb)
+			continue;
+		if (!intf->out_queue[i].urb->anchor)
+			urb = &intf->out_queue[i];
+	}
+	if (!urb) {
+		dev_err(GIP_DEV(device), "Output queue is full; dropping message\n");
+		return -ENOSPC;
+	}
+	urb->data[0] = message_type;
+	urb->data[1] = flags;
+	urb->data[2] = seq;
+	offset += gip_encode_length(num_bytes, &urb->data[offset],
+		sizeof(urb->data) - offset);
+
+	if (num_bytes > 0)
+		memcpy(&urb->data[offset], bytes, num_bytes);
+
+	num_bytes += offset;
+	urb->urb->transfer_buffer_length = num_bytes;
+
+	print_hex_dump_debug(KBUILD_MODNAME ": Sending message: ",
+		DUMP_PREFIX_OFFSET, 16, 1, urb->data, num_bytes,
+		false);
+
+	usb_anchor_urb(urb->urb, &intf->out_anchor);
+	rc = usb_submit_urb(urb->urb, GFP_ATOMIC);
+	if (rc) {
+		dev_err(&intf->intf->dev,
+			"%s - usb_submit_urb failed with result %d\n",
+			__func__, rc);
+		usb_unanchor_urb(urb->urb);
+		rc = -EIO;
+	}
+
+	return rc;
+}
+
+int gip_send_system_message(struct gip_attachment *attachment,
+	uint8_t message_type, uint8_t flags, const void *bytes, int num_bytes)
+{
+	return gip_send_raw_message(attachment->device, message_type,
+		GIP_FLAG_SYSTEM | attachment->attachment_index | flags,
+		gip_sequence_next(attachment, message_type, true),
+		bytes, num_bytes);
+}
+
+int gip_send_vendor_message(struct gip_attachment *attachment,
+	uint8_t message_type, uint8_t flags, const void *bytes, int num_bytes)
+{
+	return gip_send_raw_message(attachment->device, message_type, flags,
+		gip_sequence_next(attachment, message_type, false),
+		bytes, num_bytes);
+}
+
+static void gip_metadata_free(struct device *dev, struct gip_metadata *metadata)
+{
+	devm_kfree(dev, metadata->device.audio_formats);
+
+	if (metadata->device.preferred_types) {
+		int i;
+
+		for (i = 0; i < metadata->device.num_preferred_types; i++)
+			devm_kfree(dev, metadata->device.preferred_types[i]);
+		devm_kfree(dev, metadata->device.preferred_types);
+	}
+	devm_kfree(dev, metadata->device.supported_interfaces);
+	devm_kfree(dev, metadata->device.hid_descriptor);
+	devm_kfree(dev, metadata->message_metadata);
+
+	memset(metadata, 0, sizeof(*metadata));
+}
+
+static int gip_parse_audio_format_metadata(struct device *dev,
+	struct gip_device_metadata *dev_metadata, const uint8_t *bytes,
+	int length, int buffer_offset)
+{
+	unsigned int i;
+
+	dev_metadata->num_audio_formats = bytes[buffer_offset];
+	if (buffer_offset + dev_metadata->num_audio_formats * 2 + 1 > length)
+		return -EINVAL;
+	dev_metadata->audio_formats = devm_kmalloc_array(dev,
+		dev_metadata->num_audio_formats, 2, GFP_KERNEL);
+	if (!dev_metadata->audio_formats)
+		return -ENOMEM;
+	memcpy(dev_metadata->audio_formats, &bytes[buffer_offset + 1],
+		dev_metadata->num_audio_formats * 2);
+
+	for (i = 0; i < dev_metadata->num_audio_formats; i++) {
+		const struct gip_audio_format_pair *pair = &dev_metadata->audio_formats[i];
+		const struct gip_audio_format *inbound = NULL;
+		const struct gip_audio_format *outbound = NULL;
+
+		if (pair->inbound <= MAX_GIP_AUDIO_FORMAT) {
+			inbound = &gip_audio_format_table[pair->inbound];
+			if (pair->inbound != GIP_AUDIO_FORMAT_NULL && inbound->rate == 0)
+				inbound = NULL;
+		}
+		if (!inbound)
+			dev_warn(dev, "Unknown audio format %u\n", pair->inbound);
+
+		if (pair->outbound <= MAX_GIP_AUDIO_FORMAT) {
+			outbound = &gip_audio_format_table[pair->outbound];
+			if (pair->outbound != GIP_AUDIO_FORMAT_NULL && outbound->rate == 0)
+				outbound = NULL;
+		}
+		if (!outbound)
+			dev_warn(dev, "Unknown audio format %u\n", pair->outbound);
+
+		if (inbound && outbound)
+			dev_dbg(dev,
+				"Supported audio format: %uHz %uch inbound, %uHz %uch outbound\n",
+				inbound->rate,
+				inbound->channels,
+				outbound->rate,
+				outbound->channels);
+	}
+	return 0;
+}
+
+static int gip_parse_preferred_types_metadata(struct device *dev,
+	struct gip_device_metadata *dev_metadata, const uint8_t *bytes,
+	int length, int buffer_offset)
+{
+	int i;
+	int count;
+
+	dev_metadata->num_preferred_types = bytes[buffer_offset];
+	dev_metadata->preferred_types = devm_kcalloc(dev,
+		dev_metadata->num_preferred_types, sizeof(char *), GFP_KERNEL);
+	if (!dev_metadata->preferred_types)
+		return -ENOMEM;
+
+	buffer_offset++;
+	for (i = 0; i < dev_metadata->num_preferred_types; i++) {
+		if (buffer_offset + 2 >= length)
+			return -EINVAL;
+
+		count = bytes[buffer_offset];
+		count |= bytes[buffer_offset];
+		buffer_offset += 2;
+		if (buffer_offset + count > length)
+			return -EINVAL;
+
+		dev_metadata->preferred_types[i] = devm_kcalloc(dev, count + 1,
+			sizeof(char), GFP_KERNEL);
+		if (!dev_metadata->preferred_types[i])
+			return -ENOMEM;
+		memcpy(dev_metadata->preferred_types[i], &bytes[buffer_offset], count);
+		buffer_offset += count;
+	}
+
+	return 0;
+}
+
+static int gip_parse_supported_interfaces_metadata(struct device *dev,
+	struct gip_device_metadata *dev_metadata, const uint8_t *bytes,
+	int length, int buffer_offset)
+{
+	dev_metadata->num_supported_interfaces = bytes[buffer_offset];
+	if (buffer_offset + 1 +
+		(int32_t) (dev_metadata->num_supported_interfaces * sizeof(guid_t)) > length)
+		return -EINVAL;
+
+	dev_metadata->supported_interfaces = devm_kmalloc_array(dev,
+		dev_metadata->num_supported_interfaces, sizeof(guid_t), GFP_KERNEL);
+	if (!dev_metadata->supported_interfaces)
+		return -ENOMEM;
+
+	memcpy(dev_metadata->supported_interfaces, &bytes[buffer_offset + 1],
+		sizeof(guid_t) * dev_metadata->num_supported_interfaces);
+
+	return 0;
+}
+
+static int gip_parse_hid_descriptor_metadata(struct device *dev,
+	struct gip_device_metadata *dev_metadata, const uint8_t *bytes,
+	int length, int buffer_offset)
+{
+	dev_metadata->hid_descriptor_size = bytes[buffer_offset];
+	if (buffer_offset + 1 + dev_metadata->hid_descriptor_size > length)
+		return -EINVAL;
+
+	dev_metadata->hid_descriptor = devm_kmalloc(dev,
+		dev_metadata->hid_descriptor_size, GFP_KERNEL);
+	if (!dev_metadata->hid_descriptor)
+		return -ENOMEM;
+
+	memcpy(dev_metadata->hid_descriptor, &bytes[buffer_offset + 1],
+		dev_metadata->hid_descriptor_size);
+	print_hex_dump_debug(KBUILD_MODNAME ": Received HID descriptor: ",
+		DUMP_PREFIX_OFFSET, 16, 1, dev_metadata->hid_descriptor,
+		dev_metadata->hid_descriptor_size, false);
+
+	return 0;
+}
+
+static int gip_parse_device_metadata(struct device *dev,
+	struct gip_metadata *metadata, const uint8_t *bytes, int num_bytes,
+	int *offset)
+{
+	struct gip_device_metadata *dev_metadata = &metadata->device;
+	int buffer_offset;
+	int count;
+	int length;
+	int i;
+	int rc;
+
+	bytes = &bytes[*offset];
+	num_bytes -= *offset;
+	if (num_bytes < 16)
+		return -EINVAL;
+
+	length = bytes[0];
+	length |= bytes[1] << 8;
+	if (num_bytes < length)
+		return -EINVAL;
+
+	/* Skip supported firmware versions for now */
+
+	buffer_offset = bytes[4];
+	buffer_offset |= bytes[5] << 8;
+	if (buffer_offset >= length)
+		return -EINVAL;
+
+	if (buffer_offset > 0) {
+		rc = gip_parse_audio_format_metadata(dev, dev_metadata,
+			bytes, length, buffer_offset);
+		if (rc)
+			return rc;
+	}
+
+	buffer_offset = bytes[6];
+	buffer_offset |= bytes[7] << 8;
+	if (buffer_offset >= length)
+		return -EINVAL;
+
+	if (buffer_offset > 0) {
+		count = bytes[buffer_offset];
+		if (buffer_offset + count + 1 > length)
+			return -EINVAL;
+
+		for (i = 0; i < count; i++) {
+			uint8_t message = bytes[buffer_offset + 1 + i];
+
+			dev_dbg(dev,
+				"Supported upstream system message %02x\n",
+				message);
+			dev_metadata->in_system_messages[message >> 5] |=
+				BIT(message & 0x1F);
+		}
+	}
+
+	buffer_offset = bytes[8];
+	buffer_offset |= bytes[9] << 8;
+	if (buffer_offset >= length)
+		return -EINVAL;
+
+	if (buffer_offset > 0) {
+		count = bytes[buffer_offset];
+		if (buffer_offset + count + 1 > length)
+			return -EINVAL;
+
+		for (i = 0; i < count; i++) {
+			uint8_t message = bytes[buffer_offset + 1 + i];
+
+			dev_dbg(dev,
+				"Supported downstream system message %02x\n",
+				message);
+			dev_metadata->out_system_messages[message >> 5] |=
+				BIT(message & 0x1F);
+		}
+	}
+
+	buffer_offset = bytes[10];
+	buffer_offset |= bytes[11] << 8;
+	if (buffer_offset >= length)
+		return -EINVAL;
+
+	if (buffer_offset > 0) {
+		rc = gip_parse_preferred_types_metadata(dev, dev_metadata,
+			bytes, length, buffer_offset);
+		if (rc)
+			return rc;
+	}
+
+	buffer_offset = bytes[12];
+	buffer_offset |= bytes[13] << 8;
+	if (buffer_offset >= length)
+		return -EINVAL;
+
+	if (buffer_offset > 0) {
+		rc = gip_parse_supported_interfaces_metadata(dev,
+			dev_metadata, bytes, length, buffer_offset);
+		if (rc)
+			return rc;
+	}
+
+	if (metadata->version_major > 1 || metadata->version_minor >= 1) {
+		/* HID descriptor support added in metadata version 1.1 */
+		buffer_offset = bytes[14];
+		buffer_offset |= bytes[15] << 8;
+		if (buffer_offset >= length)
+			return -EINVAL;
+
+		if (buffer_offset > 0) {
+			rc = gip_parse_hid_descriptor_metadata(dev,
+				dev_metadata, bytes, length, buffer_offset);
+			if (rc)
+				return rc;
+		}
+	}
+
+	*offset += length;
+	return 0;
+}
+
+static int gip_parse_message_metadata(struct device *dev,
+	struct gip_message_metadata *metadata, const uint8_t *bytes,
+	int num_bytes, int *offset)
+{
+	uint16_t length;
+
+	bytes = &bytes[*offset];
+	num_bytes -= *offset;
+
+	if (num_bytes < 2)
+		return -EINVAL;
+
+	length = bytes[0];
+	length |= bytes[1] << 8;
+	if (num_bytes < length)
+		return -EINVAL;
+
+	if (length < 15)
+		return -EINVAL;
+
+	metadata->type = bytes[2];
+	metadata->length = bytes[3];
+	metadata->length |= bytes[4] << 8;
+	metadata->data_type = bytes[5];
+	metadata->data_type |= bytes[6] << 8;
+	metadata->flags = bytes[7];
+	metadata->flags |= bytes[8] << 8;
+	metadata->flags |= bytes[9] << 16;
+	metadata->flags |= bytes[10] << 24;
+	metadata->period = bytes[11];
+	metadata->period |= bytes[12] << 8;
+	metadata->persistence_timeout = bytes[13];
+	metadata->persistence_timeout |= bytes[14] << 8;
+
+	dev_dbg(dev,
+		"Supported vendor message type %02x of length %d, %s, %s, %s\n",
+		metadata->type, metadata->length,
+		metadata->flags & GIP_MESSAGE_FLAG_UPSTREAM ?
+			(metadata->flags & GIP_MESSAGE_FLAG_DOWNSTREAM ? "bidirectional" : "upstream") :
+			metadata->flags & GIP_MESSAGE_FLAG_DOWNSTREAM ? "downstream" :
+			metadata->flags & GIP_MESSAGE_FLAG_DS_REQUEST_RESPONSE ? "downstream request response" :
+			"unknown direction",
+		metadata->flags & GIP_MESSAGE_FLAG_SEQUENCED ? "sequenced" : "not sequenced",
+		metadata->flags & GIP_MESSAGE_FLAG_RELIABLE ? "reliable" : "unreliable");
+
+	*offset += length;
+	return 0;
+}
+
+static bool gip_parse_metadata(struct device *dev,
+	struct gip_metadata *metadata, const uint8_t *bytes, int num_bytes)
+{
+	int header_size;
+	int metadata_size;
+	int offset = 0;
+	int i;
+	int rc;
+
+	if (num_bytes < 16)
+		return -EINVAL;
+
+	print_hex_dump_debug(KBUILD_MODNAME ": Received metadata: ", DUMP_PREFIX_OFFSET,
+		16, 1, bytes, num_bytes, false);
+
+	header_size = bytes[0];
+	header_size |= bytes[1] << 8;
+	if (num_bytes < header_size || header_size < 16)
+		return -EINVAL;
+
+	metadata->version_major = bytes[2];
+	metadata->version_major |= bytes[3] << 8;
+	metadata->version_minor = bytes[4];
+	metadata->version_minor |= bytes[5] << 8;
+	/* Middle bytes are reserved */
+	metadata_size = bytes[14];
+	metadata_size |= bytes[15] << 8;
+
+	if (num_bytes < metadata_size || metadata_size < header_size)
+		return -EINVAL;
+
+	offset = header_size;
+
+	rc = gip_parse_device_metadata(dev, metadata, bytes, num_bytes, &offset);
+	if (rc)
+		goto parse_err;
+
+	if (offset >= num_bytes)
+		goto parse_err;
+
+	metadata->num_messages = bytes[offset];
+	offset++;
+	if (metadata->num_messages > 0) {
+		metadata->message_metadata = devm_kcalloc(dev,
+			metadata->num_messages,
+			sizeof(*metadata->message_metadata), GFP_KERNEL);
+		if (!metadata->message_metadata)
+			return -ENOMEM;
+
+		for (i = 0; i < metadata->num_messages; i++) {
+			rc = gip_parse_message_metadata(dev,
+				&metadata->message_metadata[i], bytes,
+				num_bytes, &offset);
+			if (rc)
+				goto parse_err;
+		}
+	}
+
+	return 0;
+
+parse_err:
+	gip_metadata_free(dev, metadata);
+	return rc;
+}
+
+static int gip_acknowledge(struct gip_device *device,
+	const struct gip_header *header, uint32_t fragment_offset,
+	uint16_t bytes_remaining)
+{
+	uint8_t buffer[] = {
+		GIP_CONTROL_CODE_ACK,
+		header->message_type,
+		header->flags & GIP_FLAG_SYSTEM,
+		fragment_offset,
+		fragment_offset >> 8,
+		fragment_offset >> 16,
+		fragment_offset >> 24,
+		bytes_remaining,
+		bytes_remaining >> 8,
+	};
+
+	return gip_send_raw_message(device, GIP_CMD_PROTO_CONTROL,
+		GIP_FLAG_SYSTEM | (header->flags & GIP_FLAG_ATTACHMENT_MASK),
+		header->sequence_id, buffer, sizeof(buffer));
+}
+
+static int gip_fragment_failed(struct gip_attachment *attachment,
+	const struct gip_header *header)
+{
+	attachment->fragment_retries++;
+	if (attachment->fragment_retries > 8) {
+		devm_kfree(GIP_DEV(attachment), attachment->fragment_data);
+		attachment->fragment_data = NULL;
+		attachment->fragment_message = 0;
+	}
+	return gip_acknowledge(attachment->device, header,
+		attachment->fragment_offset,
+		attachment->total_length - attachment->fragment_offset);
+}
+
+static int gip_bind_driver(struct gip_attachment *attachment, const struct gip_driver *driver)
+{
+	if (driver->probe) {
+		int rc = driver->probe(attachment);
+
+		if (rc)
+			return rc;
+	}
+
+	attachment->driver = driver;
+	memcpy(attachment->vendor_handlers, driver->vendor_handlers,
+		sizeof(attachment->vendor_handlers));
+	return 0;
+}
+
+static int gip_enable_elite_buttons(struct gip_attachment *attachment)
+{
+	if (attachment->vendor_id == 0x045e) {
+		if (attachment->product_id == 0x02e3) {
+			attachment->xbe_format = GIP_BTN_FMT_XBE1;
+		} else if (attachment->product_id == 0x0b00) {
+			if (attachment->firmware_major_version == 4) {
+				attachment->xbe_format = GIP_BTN_FMT_XBE2_4;
+			} else if (attachment->firmware_major_version == 5) {
+				/*
+				 * The exact range for this being necessary is
+				 * unknown, but it starts at 5.11 and at either
+				 * 5.16 or 5.17. This approach still works on
+				 * 5.21, even if it's not necessary, so having
+				 * a loose upper limit is fine.
+				 */
+				if (attachment->firmware_minor_version >= 11 &&
+					attachment->firmware_minor_version < 17)
+					attachment->xbe_format = GIP_BTN_FMT_XBE2_RAW;
+				else
+					attachment->xbe_format = GIP_BTN_FMT_XBE2_5;
+			}
+		}
+	}
+
+	if (attachment->xbe_format == GIP_BTN_FMT_XBE2_RAW) {
+		/*
+		 * The meaning of this packet is unknown and not documented, but
+		 * it's needed for the Elite 2 controller to send raw reports
+		 */
+		static const uint8_t enable_raw_report[] = { 7, 0 };
+
+		return gip_send_vendor_message(attachment, GIP_SL_ELITE_CONFIG,
+			0, enable_raw_report, sizeof(enable_raw_report));
+	}
+
+	return 0;
+}
+
+#ifdef CONFIG_JOYSTICK_XBOX_GIP_FF
+static int gip_play_effect(struct input_dev *dev, void *data, struct ff_effect *effect)
+{
+	struct gip_attachment *attachment = input_get_drvdata(dev);
+	struct gip_direct_motor control = {
+		.motor_bitmap = GIP_MOTOR_LEFT_VIBRATION | GIP_MOTOR_RIGHT_VIBRATION
+	};
+
+	if (effect->type != FF_RUMBLE)
+		return 0;
+
+	control.left_vibration_level = effect->u.rumble.strong_magnitude * 100 / 0xFFFF;
+	control.right_vibration_level = effect->u.rumble.weak_magnitude * 100 / 0xFFFF;
+	control.duration = 255;
+
+	return gip_send_vendor_message(attachment, GIP_CMD_DIRECT_MOTOR,
+		0, &control, sizeof(control));
+}
+#endif
+
+static int gip_send_guide_button_led(struct gip_attachment *attachment,
+	uint8_t pattern, uint8_t intensity)
+{
+	uint8_t buffer[] = {
+		GIP_LED_GUIDE,
+		pattern,
+		intensity,
+	};
+
+	if (!gip_supports_system_message(attachment, GIP_CMD_LED, false))
+		return 0;
+
+	return gip_send_system_message(attachment, GIP_CMD_LED, 0, buffer, sizeof(buffer));
+}
+
+static bool gip_send_set_device_state(struct gip_attachment *attachment, uint8_t state)
+{
+	uint8_t buffer[] = { state };
+
+	return gip_send_system_message(attachment, GIP_CMD_SET_DEVICE_STATE,
+		attachment->attachment_index, buffer, sizeof(buffer));
+}
+
+static int gip_handle_command_raw_report(struct gip_attachment *attachment,
+	const struct gip_header *header, const uint8_t *bytes, int num_bytes)
+{
+	struct input_dev *input;
+
+	if (num_bytes < 17) {
+		dev_dbg(GIP_DEV(attachment), "Discarding too-short raw report\n");
+		return -EINVAL;
+	}
+	guard(rcu)();
+	input = rcu_dereference(attachment->input);
+	if (!input)
+		return -ENODEV;
+
+	if ((attachment->features & GIP_FEATURE_ELITE_BUTTONS)
+		&& attachment->xbe_format == GIP_BTN_FMT_XBE2_RAW) {
+		input_report_abs(input, ABS_PROFILE, bytes[15] & 3);
+		if (bytes[15] & 3) {
+			input_report_key(input, BTN_GRIPL, 0);
+			input_report_key(input, BTN_GRIPR, 0);
+			input_report_key(input, BTN_GRIPL2, 0);
+			input_report_key(input, BTN_GRIPR2, 0);
+		} else {
+			input_report_key(input, BTN_GRIPL,
+				bytes[GIP_BTN_OFFSET_XBE2] & BIT(2));
+			input_report_key(input, BTN_GRIPR,
+				bytes[GIP_BTN_OFFSET_XBE2] & BIT(0));
+			input_report_key(input, BTN_GRIPL2,
+				bytes[GIP_BTN_OFFSET_XBE2] & BIT(3));
+			input_report_key(input, BTN_GRIPR2,
+				bytes[GIP_BTN_OFFSET_XBE2] & BIT(1));
+		}
+
+		input_sync(input);
+	}
+	return 0;
+}
+
+static int gip_setup_input_device(struct gip_attachment *attachment)
+{
+	struct input_dev *input;
+	int rc;
+
+	if (!attachment->driver || !attachment->driver->setup_input)
+		return -ENODEV;
+
+	rcu_read_lock();
+	input = rcu_dereference(attachment->input);
+	rcu_read_unlock();
+	if (input)
+		return 0;
+
+	input = input_allocate_device();
+	if (!input)
+		return -ENOMEM;
+	input->id.bustype = BUS_USB;
+	input->id.vendor = attachment->vendor_id;
+	input->id.product = attachment->product_id;
+	input->uniq = attachment->uniq;
+	if (attachment->name)
+		input->name = attachment->name;
+	else if (attachment->attachment_index == 0)
+		input->name = attachment->device->udev->product;
+	input->phys = attachment->phys;
+
+	rc = attachment->driver->setup_input(attachment, input);
+	if (rc < 0)
+		goto err_free_device;
+
+	if (attachment->features & GIP_FEATURE_CONSOLE_FUNCTION_MAP)
+		input_set_capability(input, EV_KEY, KEY_RECORD);
+
+	if (attachment->features & GIP_FEATURE_ELITE_BUTTONS) {
+		input_set_capability(input, EV_KEY, BTN_GRIPL);
+		input_set_capability(input, EV_KEY, BTN_GRIPR);
+		input_set_capability(input, EV_KEY, BTN_GRIPL2);
+		input_set_capability(input, EV_KEY, BTN_GRIPR2);
+		if (attachment->xbe_format == GIP_BTN_FMT_XBE1)
+			input_set_abs_params(input, ABS_PROFILE, 0, 1, 0, 0);
+		else
+			input_set_abs_params(input, ABS_PROFILE, 0, 3, 0, 0);
+
+		attachment->vendor_handlers[GIP_CMD_RAW_REPORT] = gip_handle_command_raw_report;
+	}
+
+#ifdef CONFIG_JOYSTICK_XBOX_GIP_FF
+	if (attachment->features & GIP_FEATURE_MOTOR_CONTROL) {
+		input_set_capability(input, EV_FF, FF_RUMBLE);
+		input_ff_create_memless(input, NULL, gip_play_effect);
+	}
+#endif
+
+	input_set_drvdata(input, attachment);
+	rcu_assign_pointer(attachment->input, input);
+	rc = input_register_device(input);
+	if (rc)
+		goto err_free_device;
+
+	return 0;
+
+err_free_device:
+	input_free_device(input);
+	return rc;
+}
+
+static int gip_send_init_sequence(struct gip_attachment *attachment)
+{
+	int rc = 0;
+	size_t len;
+
+	if (attachment->features & GIP_FEATURE_EXTENDED_SET_DEVICE_STATE) {
+		/*
+		 * The meaning of this packet is unknown and not documented, but it's
+		 * needed for the Elite 2 controller to start up on older firmwares
+		 */
+		static const uint8_t set_device_state[] = {
+			GIP_STATE_UNK6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+			0x55, 0x53, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0
+		};
+
+		rc = gip_send_system_message(attachment,
+			GIP_CMD_SET_DEVICE_STATE, 0, set_device_state,
+			sizeof(set_device_state));
+		if (rc)
+			return rc;
+	}
+	rc = gip_enable_elite_buttons(attachment);
+	if (rc)
+		return rc;
+	if (!gip_supports_system_message(attachment, GIP_CMD_AUDIO_CONTROL, false)) {
+		rc = gip_send_set_device_state(attachment, GIP_STATE_START);
+		if (rc)
+			return rc;
+		attachment->device_state = GIP_STATE_START;
+	} else {
+		rc = gip_send_set_device_state(attachment, GIP_STATE_STOP);
+		if (rc)
+			return rc;
+		attachment->device_state = GIP_STATE_STOP;
+	}
+
+	rc = gip_send_guide_button_led(attachment,
+		GIP_LED_GUIDE_ON,
+		GIP_LED_GUIDE_INIT_BRIGHTNESS);
+	if (rc)
+		return rc;
+
+	if (gip_supports_system_message(attachment, GIP_CMD_SECURITY, false)
+		&& !(attachment->features & GIP_FEATURE_SECURITY_OPT_OUT)) {
+		/* TODO: Implement Security command property */
+		uint8_t buffer[] = { 0x1, 0x0 };
+
+		rc = gip_send_system_message(attachment, GIP_CMD_SECURITY, 0,
+			buffer, sizeof(buffer));
+		if (rc)
+			return rc;
+	}
+
+	usb_make_path(attachment->device->udev, attachment->phys,
+		sizeof(attachment->phys));
+	len = strlen(attachment->phys);
+	if (len < sizeof(attachment->phys) - 1)
+		snprintf(attachment->phys + len,
+			sizeof(attachment->phys) - len, "/input%d",
+			attachment->attachment_index);
+
+	if (attachment->driver && attachment->driver->init) {
+		rc = attachment->driver->init(attachment);
+		if (rc < 0)
+			return rc;
+	}
+
+	if (rc != GIP_INIT_NO_INPUT && (attachment->features & GIP_FEATURE_CONTROLLER)) {
+		rc = gip_setup_input_device(attachment);
+		if (rc == -ENODEV)
+			return 0;
+		if (rc)
+			return rc;
+	}
+
+	return 0;
+}
+
+static void gip_fragment_timeout(struct work_struct *work)
+{
+	struct gip_attachment *attachment = container_of(to_delayed_work(work),
+		struct gip_attachment, fragment_timeout);
+
+	guard(mutex)(&attachment->lock);
+	devm_kfree(GIP_DEV(attachment), attachment->fragment_data);
+	attachment->fragment_data = NULL;
+	attachment->fragment_message = 0;
+}
+
+static void gip_retry_metadata(struct work_struct *work)
+{
+	struct gip_attachment *attachment = container_of(to_delayed_work(work),
+		struct gip_attachment, metadata_next);
+
+	guard(mutex)(&attachment->lock);
+	if (attachment->metadata_retries < 4) {
+		attachment->metadata_retries++;
+		schedule_delayed_work(&attachment->metadata_next, HZ / 2);
+		gip_send_system_message(attachment, GIP_CMD_METADATA, 0, NULL, 0);
+	} else {
+		dev_info(GIP_DEV(attachment),
+			"Unable to obtain metadata, attempting to reset device\n");
+		gip_send_set_device_state(attachment, GIP_STATE_RESET);
+	}
+}
+
+static int gip_ensure_metadata(struct gip_attachment *attachment)
+{
+	switch (attachment->got_metadata) {
+	case GIP_METADATA_GOT:
+	case GIP_METADATA_FAKED:
+		return 0;
+	case GIP_METADATA_NONE:
+		attachment->got_metadata = GIP_METADATA_PENDING;
+		cancel_delayed_work_sync(&attachment->metadata_next);
+		schedule_delayed_work(&attachment->metadata_next, HZ / 2);
+		attachment->metadata_retries = 0;
+		return gip_send_system_message(attachment, GIP_CMD_METADATA, 0, NULL, 0);
+	default:
+		return 0;
+	}
+}
+
+static void gip_set_metadata_defaults(struct gip_attachment *attachment)
+{
+	if (attachment->got_metadata != GIP_METADATA_NONE)
+		return;
+
+	attachment->metadata.device.in_system_messages[0] =
+		GIP_DEFAULT_IN_SYSTEM_MESSAGES;
+	attachment->metadata.device.out_system_messages[0] =
+		GIP_DEFAULT_OUT_SYSTEM_MESSAGES;
+	if (attachment->attachment_index == 0) {
+		/* Some decent default settings */
+		attachment->features |= GIP_FEATURE_CONTROLLER;
+		attachment->metadata.device.in_system_messages[0] |= (1u << GIP_CMD_GUIDE_BUTTON);
+	}
+
+	gip_handle_quirks(attachment);
+	if (attachment->quirks & GIP_QUIRK_NO_HELLO)
+		gip_ensure_metadata(attachment);
+
+	attachment->got_metadata = GIP_METADATA_FAKED;
+}
+
+static bool gip_handle_command_protocol_control(struct gip_attachment *attachment,
+	const struct gip_header *header, const uint8_t *bytes, int num_bytes)
+{
+	/* TODO */
+	dev_warn(GIP_DEV(attachment), "Unimplemented Protocol Control message\n");
+	return -ENOTSUPP;
+}
+
+static bool gip_handle_command_hello_device(struct gip_attachment *attachment,
+	const struct gip_header *header, const uint8_t *bytes, int num_bytes)
+{
+	struct gip_hello_device message = {0};
+
+	if (num_bytes != 28)
+		return -EINVAL;
+
+	message.device_id = (uint64_t) bytes[0];
+	message.device_id |= (uint64_t) bytes[1] << 8;
+	message.device_id |= (uint64_t) bytes[2] << 16;
+	message.device_id |= (uint64_t) bytes[3] << 24;
+	message.device_id |= (uint64_t) bytes[4] << 32;
+	message.device_id |= (uint64_t) bytes[5] << 40;
+	message.device_id |= (uint64_t) bytes[6] << 48;
+	message.device_id |= (uint64_t) bytes[7] << 56;
+
+	message.vendor_id = bytes[8];
+	message.vendor_id |= bytes[9] << 8;
+
+	message.product_id = bytes[10];
+	message.product_id |= bytes[11] << 8;
+
+	message.firmware_major_version = bytes[12];
+	message.firmware_major_version |= bytes[13] << 8;
+
+	message.firmware_minor_version = bytes[14];
+	message.firmware_minor_version |= bytes[15] << 8;
+
+	message.firmware_build_version = bytes[16];
+	message.firmware_build_version |= bytes[17] << 8;
+
+	message.firmware_revision = bytes[18];
+	message.firmware_revision |= bytes[19] << 8;
+
+	message.hardware_major_version = bytes[20];
+	message.hardware_minor_version = bytes[21];
+
+	message.rf_proto_major_version = bytes[22];
+	message.rf_proto_minor_version = bytes[23];
+
+	message.security_major_version = bytes[24];
+	message.security_minor_version = bytes[25];
+
+	message.gip_major_version = bytes[26];
+	message.gip_minor_version = bytes[27];
+
+	dev_dbg(GIP_DEV(attachment), "Device hello from %llx (%04x:%04x)\n",
+		message.device_id, message.vendor_id, message.product_id);
+	dev_dbg(GIP_DEV(attachment), "Firmware version %d.%d.%d rev %d\n",
+		message.firmware_major_version, message.firmware_minor_version,
+		message.firmware_build_version, message.firmware_revision);
+
+	/*
+	 * The GIP spec specifies that the host should reject the device if any of these are wrong.
+	 * I don't know if Windows or an Xbox do, however, so let's just log warnings instead.
+	 */
+	if (message.rf_proto_major_version != 1 && message.rf_proto_minor_version != 0)
+		dev_warn(GIP_DEV(attachment),
+			"Invalid RF protocol version %d.%d, expected 1.0\n",
+			message.rf_proto_major_version, message.rf_proto_minor_version);
+
+	if (message.security_major_version != 1 && message.security_minor_version != 0)
+		dev_warn(GIP_DEV(attachment),
+			"Invalid security protocol version %d.%d, expected 1.0\n",
+			message.security_major_version, message.security_minor_version);
+
+	if (message.gip_major_version != 1 && message.gip_minor_version != 0)
+		dev_warn(GIP_DEV(attachment),
+			"Invalid GIP version %d.%d, expected 1.0\n",
+			message.gip_major_version, message.gip_minor_version);
+
+	attachment->firmware_major_version = message.firmware_major_version;
+	attachment->firmware_minor_version = message.firmware_minor_version;
+	attachment->vendor_id = message.vendor_id;
+	attachment->product_id = message.product_id;
+	attachment->uniq = devm_kasprintf(GIP_DEV(attachment),
+		GFP_KERNEL, "%llx", message.device_id);
+
+	if (header->flags & GIP_FLAG_ATTACHMENT_MASK)
+		return gip_send_system_message(attachment, GIP_CMD_METADATA, 0, NULL, 0);
+	if (attachment->got_metadata == GIP_METADATA_FAKED)
+		attachment->got_metadata = GIP_METADATA_NONE;
+	return gip_ensure_metadata(attachment);
+}
+
+static int gip_handle_command_status_device(struct gip_attachment *attachment,
+	const struct gip_header *header, const uint8_t *bytes, int num_bytes)
+{
+	int i;
+
+	if (num_bytes < 1)
+		return -EINVAL;
+
+	attachment->status.base.battery_level = bytes[0] & 3;
+	attachment->status.base.battery_type = (bytes[0] >> 2) & 3;
+	attachment->status.base.charge = (bytes[0] >> 4) & 3;
+	attachment->status.base.power_level = (bytes[0] >> 6) & 3;
+
+	if (num_bytes >= 4) {
+		attachment->status.device_active = bytes[1] & 1;
+		if (bytes[1] & 2) {
+			/* Events present */
+			if (num_bytes < 5)
+				return -EINVAL;
+
+			attachment->status.num_events = bytes[4];
+			if (attachment->status.num_events > 5) {
+				dev_info(GIP_DEV(attachment),
+					"Device reported too many events, %d > 5\n",
+					attachment->status.num_events);
+				return -EINVAL;
+			}
+			if (5 + attachment->status.num_events * 10 > num_bytes)
+				return -EINVAL;
+
+			for (i = 0; i < attachment->status.num_events; i++) {
+				struct gip_status_event *event = &attachment->status.events[i];
+
+				event->event_type = bytes[i * 10 + 5];
+				event->event_type |= bytes[i * 10 + 6] << 8;
+				event->fault_tag = bytes[i * 10 + 7];
+				event->fault_tag |= bytes[i * 10 + 8] << 8;
+				event->fault_tag |= bytes[i * 10 + 9] << 16;
+				event->fault_tag |= bytes[i * 10 + 10] << 24;
+				event->fault_address = bytes[i * 10 + 11];
+				event->fault_address |= bytes[i * 10 + 12] << 8;
+				event->fault_address |= bytes[i * 10 + 13] << 16;
+				event->fault_address |= bytes[i * 10 + 14] << 24;
+
+				dev_info(GIP_DEV(attachment),
+					"Attachment %i event type %i, tag %i address %x\n",
+					attachment->attachment_index,
+					event->event_type,
+					event->fault_tag,
+					event->fault_address);
+			}
+		}
+	}
+
+	return gip_ensure_metadata(attachment);
+}
+
+static int gip_handle_command_metadata_respose(struct gip_attachment *attachment,
+	const struct gip_header *header, const uint8_t *bytes, int num_bytes)
+{
+	struct gip_metadata metadata = {0};
+	const guid_t *expected_guid = NULL;
+	bool found_expected_guid;
+	bool found_controller_guid = false;
+	int i, j, k;
+	int rc;
+
+	rc = gip_parse_metadata(GIP_DEV(attachment), &metadata, bytes, num_bytes);
+	if (rc)
+		return rc;
+
+	if (attachment->got_metadata == GIP_METADATA_GOT) {
+		struct input_dev *input;
+
+		gip_metadata_free(GIP_DEV(attachment), &attachment->metadata);
+		rcu_read_lock();
+		input = rcu_dereference(attachment->input);
+		rcu_read_unlock();
+		if (input) {
+			rcu_assign_pointer(attachment->input, NULL);
+			synchronize_rcu();
+			input_unregister_device(input);
+		}
+	}
+
+	attachment->metadata = metadata;
+	attachment->got_metadata = GIP_METADATA_GOT;
+	attachment->features = 0;
+	cancel_delayed_work_sync(&attachment->metadata_next);
+
+	for (i = 0; i < metadata.device.num_preferred_types; i++) {
+		const char *type = metadata.device.preferred_types[i];
+
+		dev_dbg(GIP_DEV(attachment), "Device preferred type: %s\n",
+			type);
+	}
+	for (i = 0; i < metadata.device.num_preferred_types; i++) {
+		const char *type = metadata.device.preferred_types[i];
+
+		for (j = 0; base_drivers[j] && !expected_guid; j++) {
+			for (k = 0; base_drivers[j]->types[k] && !expected_guid; k++) {
+				if (strcmp(type, base_drivers[j]->types[k]) == 0) {
+					rc = gip_bind_driver(attachment, base_drivers[j]);
+					if (rc == 0)
+						expected_guid = &base_drivers[j]->guid;
+					else if (rc != -ENODEV)
+						return rc;
+				}
+			}
+		}
+		if (expected_guid)
+			break;
+
+		if (strcmp(type, "Windows.Xbox.Input.Chatpad") == 0) {
+			break;
+		}
+		if (strcmp(type, "Windows.Xbox.Input.Headset") == 0) {
+			expected_guid = &guid_headset;
+			break;
+		}
+	}
+
+	found_expected_guid = !expected_guid;
+	for (i = 0; i < metadata.device.num_supported_interfaces; i++) {
+		const guid_t *guid = &metadata.device.supported_interfaces[i];
+
+		dev_dbg(GIP_DEV(attachment), "Supported interface: %pUl\n", guid);
+		if (expected_guid && guid_equal(expected_guid, guid))
+			found_expected_guid = true;
+
+		if (guid_equal(&guid_controller, guid)) {
+			found_controller_guid = true;
+			continue;
+		}
+		if (guid_equal(&gip_driver_navigation.guid, guid)) {
+			attachment->features |= GIP_FEATURE_CONTROLLER;
+			continue;
+		}
+		if (guid_equal(&guid_dev_auth_pc_opt_out, guid)) {
+			attachment->features |= GIP_FEATURE_SECURITY_OPT_OUT;
+			continue;
+		}
+		if (guid_equal(&guid_console_function_map, guid)) {
+			attachment->features |= GIP_FEATURE_CONSOLE_FUNCTION_MAP;
+			continue;
+		}
+		if (guid_equal(&guid_console_function_map_overflow, guid)) {
+			attachment->features |= GIP_FEATURE_CONSOLE_FUNCTION_MAP_OVERFLOW;
+			continue;
+		}
+		if (guid_equal(&guid_elite_buttons, guid)) {
+			attachment->features |= GIP_FEATURE_ELITE_BUTTONS;
+			continue;
+		}
+		if (guid_equal(&guid_dynamic_latency_input, guid)) {
+			attachment->features |= GIP_FEATURE_DYNAMIC_LATENCY_INPUT;
+			continue;
+		}
+	}
+
+	for (i = 0; i < metadata.num_messages; i++) {
+		struct gip_message_metadata *message = &metadata.message_metadata[i];
+
+		if (message->type == GIP_CMD_DIRECT_MOTOR && message->length >= 9
+			&& (message->flags & GIP_MESSAGE_FLAG_DOWNSTREAM))
+			attachment->features |= GIP_FEATURE_MOTOR_CONTROL;
+	}
+
+	if (!found_expected_guid || !found_controller_guid)
+		dev_dbg(GIP_DEV(attachment),
+			"Controller was missing expected GUID. "
+			"This controller probably won't work on an actual Xbox.\n");
+
+	gip_handle_quirks(attachment);
+
+	if ((attachment->features & GIP_FEATURE_GUIDE_COLOR)
+		&& !gip_supports_vendor_message(attachment,
+			GIP_CMD_GUIDE_COLOR, false))
+		attachment->features &= ~GIP_FEATURE_GUIDE_COLOR;
+
+	dev_dbg(GIP_DEV(attachment),
+		"Attachment %i has features: %02x\n",
+		attachment->attachment_index, attachment->features);
+
+	return gip_send_init_sequence(attachment);
+}
+
+static int gip_handle_command_security(struct gip_attachment *attachment,
+	const struct gip_header *header, const uint8_t *bytes, int num_bytes)
+{
+	/* TODO: Needed for controllers that connect via dongles */
+	dev_warn(GIP_DEV(attachment), "Unimplemented Security message\n");
+	return -ENOTSUPP;
+}
+
+static int gip_handle_command_guide_button_status(struct gip_attachment *attachment,
+	const struct gip_header *header, const uint8_t *bytes, int num_bytes)
+{
+	struct input_dev *input;
+
+	if (num_bytes < 2)
+		return -EINVAL;
+
+	guard(rcu)();
+	input = rcu_dereference(attachment->input);
+	if (!input)
+		return -ENODEV;
+
+	if (bytes[1] == VK_LWIN) {
+		input_report_key(input, BTN_MODE, bytes[0] & 3);
+		input_sync(input);
+	}
+
+	return 0;
+}
+
+static int gip_handle_command_audio_control(struct gip_attachment *attachment,
+	const struct gip_header *header, const uint8_t *bytes, int num_bytes)
+{
+	/* TODO: Needed for audio */
+	dev_warn(GIP_DEV(attachment), "Unimplemented Audio Control message\n");
+	return -ENOTSUPP;
+}
+
+static int gip_handle_command_firmware(struct gip_attachment *attachment,
+	const struct gip_header *header, const uint8_t *bytes, int num_bytes)
+{
+	if (num_bytes < 1)
+		return -EINVAL;
+
+	if (bytes[0] == 1) {
+		uint16_t major, minor, build, rev;
+
+		if (num_bytes < 14) {
+			dev_dbg(GIP_DEV(attachment),
+				"Discarding too-short firmware message\n");
+
+			return -EINVAL;
+		}
+		major = bytes[6];
+		major |= bytes[7] << 8;
+		minor = bytes[8];
+		minor |= bytes[9] << 8;
+		build = bytes[10];
+		build |= bytes[11] << 8;
+		rev = bytes[12];
+		rev |= bytes[13] << 8;
+
+		dev_dbg(GIP_DEV(attachment),
+			"Firmware version: %d.%d.%d rev %d\n", major, minor, build, rev);
+
+		attachment->firmware_major_version = major;
+		attachment->firmware_minor_version = minor;
+
+		if (attachment->vendor_id == 0x045e
+			&& attachment->product_id == 0x0b00)
+			return gip_enable_elite_buttons(attachment);
+
+		return 0;
+	}
+
+	dev_warn(GIP_DEV(attachment), "Unimplemented Firmware message\n");
+
+	return -ENOTSUPP;
+}
+
+static int gip_handle_command_hid_report(struct gip_attachment *attachment,
+	const struct gip_header *header, uint8_t *bytes, int num_bytes)
+{
+	dev_warn(GIP_DEV(attachment), "Unimplemented HID report message\n");
+
+	return -ENOTSUPP;
+}
+
+static int gip_handle_command_extended(struct gip_attachment *attachment,
+	const struct gip_header *header, const uint8_t *bytes, int num_bytes)
+{
+	if (num_bytes < 2)
+		return -EINVAL;
+
+	if (bytes[1] != GIP_EXTENDED_STATUS_OK) {
+		dev_dbg(GIP_DEV(attachment),
+			"Extended message type %02x failed with status %i\n",
+			bytes[0], bytes[1]);
+		return -EPROTO;
+	}
+
+	switch (bytes[0]) {
+	case GIP_EXTCMD_GET_SERIAL_NUMBER:
+		memcpy(attachment->serial, &bytes[2],
+			min(sizeof(attachment->serial), (size_t)(num_bytes - 2)));
+		break;
+	default:
+		/* TODO */
+		dev_dbg(GIP_DEV(attachment), "Unimplemented extended message type %02x\n",
+			bytes[0]);
+		return -ENOTSUPP;
+	}
+
+	return 0;
+}
+
+static int gip_handle_elite_buttons(struct gip_attachment *attachment,
+	struct input_dev *input, const uint8_t *bytes, int num_bytes)
+{
+	bool grip[4] = { 0, 0, 0, 0 };
+	int profile = -1;
+
+	if (attachment->xbe_format == GIP_BTN_FMT_XBE1
+		&& num_bytes > GIP_BTN_OFFSET_XBE1) {
+		profile = bytes[GIP_BTN_OFFSET_XBE1] >> 4;
+		if (profile) {
+			grip[0] = bytes[GIP_BTN_OFFSET_XBE1] & BIT(0);
+			grip[1] = bytes[GIP_BTN_OFFSET_XBE1] & BIT(1);
+			grip[2] = bytes[GIP_BTN_OFFSET_XBE1] & BIT(2);
+			grip[3] = bytes[GIP_BTN_OFFSET_XBE1] & BIT(3);
+		}
+	} else if ((attachment->xbe_format == GIP_BTN_FMT_XBE2_4
+		|| attachment->xbe_format == GIP_BTN_FMT_XBE2_5)
+		&& num_bytes > GIP_BTN_OFFSET_XBE2) {
+		int profile_offset;
+
+		if (attachment->xbe_format == GIP_BTN_FMT_XBE2_4)
+			profile_offset = 15;
+		else
+			profile_offset = 20;
+		profile = bytes[profile_offset] & 3;
+
+		if (!profile) {
+			grip[0] = bytes[GIP_BTN_OFFSET_XBE2] & BIT(2);
+			grip[1] = bytes[GIP_BTN_OFFSET_XBE2] & BIT(0);
+			grip[2] = bytes[GIP_BTN_OFFSET_XBE2] & BIT(3);
+			grip[3] = bytes[GIP_BTN_OFFSET_XBE2] & BIT(1);
+		}
+	}
+	if (profile >= 0) {
+		input_report_key(input, BTN_GRIPL, grip[0]);
+		input_report_key(input, BTN_GRIPR, grip[1]);
+		input_report_key(input, BTN_GRIPL2, grip[2]);
+		input_report_key(input, BTN_GRIPR2, grip[3]);
+		input_report_abs(input, ABS_PROFILE, profile);
+	}
+	return 0;
+}
+
+static int gip_handle_console_map(struct gip_attachment *attachment,
+	struct input_dev *input, const uint8_t *bytes, int num_bytes)
+{
+	int function_map_offset = -1;
+	if (num_bytes < 32)
+		return 0;
+
+	if (attachment->features & GIP_FEATURE_DYNAMIC_LATENCY_INPUT) {
+		/* The dynamic latency input bytes are after the console function map */
+		if (num_bytes >= 40)
+			function_map_offset = num_bytes - 26;
+	} else {
+		function_map_offset = num_bytes - 18;
+	}
+	if (function_map_offset >= 14) {
+		input_report_key(input, KEY_RECORD,
+			bytes[function_map_offset] & BIT(0));
+	}
+	return 0;
+}
+
+static int gip_handle_ll_input_report(struct gip_attachment *attachment,
+	const struct gip_header *header, const uint8_t *bytes, int num_bytes)
+{
+	struct input_dev *input;
+	int rc = 0;
+
+	guard(rcu)();
+	input = rcu_dereference(attachment->input);
+	if (!input)
+		return -ENODEV;
+
+	if (attachment->device_state != GIP_STATE_START) {
+		dev_dbg(GIP_DEV(attachment), "Discarding early input report\n");
+		attachment->device_state = GIP_STATE_START;
+		return 0;
+	}
+
+	if (attachment->driver && attachment->driver->handle_input_report) {
+		rc = attachment->driver->handle_input_report(attachment, input, bytes, num_bytes);
+		if (rc < 0)
+			return rc;
+	}
+
+	if (attachment->features & GIP_FEATURE_ELITE_BUTTONS) {
+		rc = gip_handle_elite_buttons(attachment, input, bytes, num_bytes);
+		if (rc < 0)
+			goto exit;
+	}
+
+	rc = gip_handle_console_map(attachment, input, bytes, num_bytes);
+
+exit:
+	input_sync(input);
+
+	return rc;
+}
+
+static int gip_handle_ll_overflow_input_report(struct gip_attachment *attachment,
+	const struct gip_header *header, const uint8_t *bytes, int num_bytes)
+{
+	/* TODO: Unknown if any devices actually use this */
+	dev_dbg(GIP_DEV(attachment), "Unimplemented Overflow Input Report message\n");
+	return -ENOTSUPP;
+}
+
+static int gip_handle_audio_data(struct gip_attachment *attachment,
+	const struct gip_header *header, const uint8_t *bytes, int num_bytes)
+{
+	/* TODO: Needed for audio support */
+	dev_dbg(GIP_DEV(attachment), "Unimplemented Audio Data message\n");
+	return -ENOTSUPP;
+}
+
+static int gip_handle_system_message(struct gip_attachment *attachment,
+	const struct gip_header *header, uint8_t *bytes, int num_bytes)
+{
+	if (!gip_supports_system_message(attachment, header->message_type, true)) {
+		dev_warn(GIP_DEV(attachment),
+			"Received claimed-unsupported system message type %02x\n",
+			header->message_type);
+		return -EINVAL;
+	}
+	switch (header->message_type) {
+	case GIP_CMD_PROTO_CONTROL:
+		return gip_handle_command_protocol_control(attachment, header,
+			bytes, num_bytes);
+	case GIP_CMD_HELLO_DEVICE:
+		return gip_handle_command_hello_device(attachment, header,
+			bytes, num_bytes);
+	case GIP_CMD_STATUS_DEVICE:
+		return gip_handle_command_status_device(attachment, header,
+			bytes, num_bytes);
+	case GIP_CMD_METADATA:
+		return gip_handle_command_metadata_respose(attachment, header,
+			bytes, num_bytes);
+	case GIP_CMD_SECURITY:
+		return gip_handle_command_security(attachment, header, bytes,
+			num_bytes);
+	case GIP_CMD_GUIDE_BUTTON:
+		return gip_handle_command_guide_button_status(attachment,
+			header, bytes, num_bytes);
+	case GIP_CMD_AUDIO_CONTROL:
+		return gip_handle_command_audio_control(attachment, header,
+			bytes, num_bytes);
+	case GIP_CMD_FIRMWARE:
+		return gip_handle_command_firmware(attachment, header, bytes,
+			num_bytes);
+	case GIP_CMD_HID_REPORT:
+		return gip_handle_command_hid_report(attachment, header,
+			bytes, num_bytes);
+	case GIP_CMD_EXTENDED:
+		return gip_handle_command_extended(attachment, header, bytes,
+			num_bytes);
+	case GIP_AUDIO_DATA:
+		return gip_handle_audio_data(attachment, header, bytes,
+			num_bytes);
+	default:
+		dev_warn(GIP_DEV(attachment),
+			"Received unknown system message type %02x\n",
+			header->message_type);
+		return -EINVAL;
+	}
+}
+
+static struct gip_attachment *gip_ensure_attachment(struct gip_device *device,
+	uint8_t attachment_index)
+{
+	struct gip_attachment *attachment = device->attachments[attachment_index];
+
+	if (!attachment) {
+		attachment = devm_kzalloc(GIP_DEV(device),
+			sizeof(*attachment), GFP_KERNEL);
+		if (!attachment)
+			return ERR_PTR(-ENOMEM);
+
+		attachment->attachment_index = attachment_index;
+		attachment->device = device;
+
+		if (attachment_index == 0) {
+			attachment->vendor_id = device->udev->descriptor.idVendor;
+			attachment->product_id = device->udev->descriptor.idProduct;
+		}
+
+		device->attachments[attachment_index] = attachment;
+
+		mutex_init(&attachment->lock);
+		INIT_DELAYED_WORK(&attachment->fragment_timeout, gip_fragment_timeout);
+		INIT_DELAYED_WORK(&attachment->metadata_next, gip_retry_metadata);
+
+		gip_set_metadata_defaults(attachment);
+	}
+	return attachment;
+}
+
+static int gip_handle_message(struct gip_attachment *attachment,
+	const struct gip_header *header, uint8_t *bytes, int num_bytes)
+{
+	if (header->flags & GIP_FLAG_SYSTEM)
+		return gip_handle_system_message(attachment, header, bytes,
+			num_bytes);
+
+	if (header->message_type < MAX_GIP_CMD && attachment->vendor_handlers[header->message_type])
+		return attachment->vendor_handlers[header->message_type](attachment,
+			header, bytes, num_bytes);
+
+	switch (header->message_type) {
+	case GIP_LL_INPUT_REPORT:
+		return gip_handle_ll_input_report(attachment, header, bytes,
+			num_bytes);
+	case GIP_LL_OVERFLOW_INPUT_REPORT:
+		return gip_handle_ll_overflow_input_report(attachment, header,
+			bytes, num_bytes);
+	}
+	dev_warn(GIP_DEV(attachment),
+		"Received unknown vendor message type %02x\n",
+		header->message_type);
+	return -ENOTSUPP;
+}
+
+static int gip_receive_fragment(struct gip_attachment *attachment,
+	const struct gip_header *header, int offset,
+	uint64_t *fragment_offset, uint16_t *bytes_remaining, uint8_t *bytes,
+	int num_bytes)
+{
+	int rc = 0;
+
+	if (header->flags & GIP_FLAG_INIT_FRAG) {
+		uint64_t total_length;
+
+		if (attachment->fragment_message) {
+			/*
+			 * Reset fragment buffer if we get a new initial
+			 * fragment before finishing the last message.
+			 * TODO: Is this the correct behavior?
+			 */
+			devm_kfree(GIP_DEV(attachment), attachment->fragment_data);
+			attachment->fragment_data = NULL;
+		}
+		offset += gip_decode_length(&total_length, &bytes[offset],
+			num_bytes - offset);
+		if (total_length > MAX_MESSAGE_LENGTH)
+			return -EINVAL;
+
+		attachment->total_length = total_length;
+		attachment->fragment_message = header->message_type;
+		if (header->length > num_bytes - offset) {
+			dev_warn(GIP_DEV(attachment),
+				"Received fragment that claims to be %llu bytes, expected %i\n",
+				header->length, num_bytes - offset);
+			return -EINVAL;
+		}
+		if (header->length > total_length) {
+			dev_warn(GIP_DEV(attachment),
+				"Received too long fragment, %llu bytes, exceeds %d\n",
+				header->length, attachment->total_length);
+			return -EINVAL;
+		}
+		attachment->fragment_data = devm_kmalloc(GIP_DEV(attachment),
+			attachment->total_length, GFP_KERNEL);
+		if (!attachment->fragment_data)
+			return -ENOMEM;
+		memcpy(attachment->fragment_data, &bytes[offset],
+			header->length);
+		*fragment_offset = header->length;
+		attachment->fragment_offset = header->length;
+		*bytes_remaining = attachment->total_length - header->length;
+	} else {
+		if (header->message_type != attachment->fragment_message) {
+			dev_warn(GIP_DEV(attachment),
+				"Received out of sequence message type %02x, expected %02x\n",
+				header->message_type, attachment->fragment_message);
+			gip_fragment_failed(attachment, header);
+			return -EINVAL;
+		}
+
+		offset += gip_decode_length(fragment_offset, &bytes[offset],
+			num_bytes - offset);
+		if (*fragment_offset != attachment->fragment_offset) {
+			dev_warn(GIP_DEV(attachment),
+				"Received out of sequence fragment, (claimed %llu, expected %d)\n",
+				*fragment_offset, attachment->fragment_offset);
+			gip_acknowledge(attachment->device, header,
+				attachment->fragment_offset,
+				attachment->total_length - attachment->fragment_offset);
+			return -EINVAL;
+		} else if (*fragment_offset + header->length > attachment->total_length) {
+			dev_warn(GIP_DEV(attachment),
+				"Received too long fragment, %llu exceeds %d\n",
+				*fragment_offset + header->length, attachment->total_length);
+			gip_fragment_failed(attachment, header);
+			return -EINVAL;
+		}
+
+		*bytes_remaining = attachment->total_length -
+			(*fragment_offset + header->length);
+		if (header->length != 0) {
+			memcpy(&attachment->fragment_data[*fragment_offset],
+				&bytes[offset], header->length);
+		} else {
+			rc = gip_handle_message(attachment, header,
+				attachment->fragment_data,
+				attachment->total_length);
+			devm_kfree(GIP_DEV(attachment), attachment->fragment_data);
+			attachment->fragment_data = NULL;
+			attachment->fragment_message = 0;
+		}
+		*fragment_offset += header->length;
+		attachment->fragment_offset = *fragment_offset;
+	}
+	cancel_delayed_work_sync(&attachment->fragment_timeout);
+	schedule_delayed_work(&attachment->fragment_timeout, HZ);
+
+	return rc;
+}
+
+static int gip_receive_message(struct gip_device *device, uint8_t *bytes,
+	int num_bytes)
+{
+	struct gip_header header;
+	int offset = 3;
+	int rc = 0;
+	uint64_t fragment_offset = 0;
+	uint16_t bytes_remaining = 0;
+	bool is_fragment;
+	uint8_t attachment_index;
+	struct gip_attachment *attachment;
+
+	if (num_bytes < 5)
+		return -EINVAL;
+
+	header.message_type = bytes[0];
+	header.flags = bytes[1];
+	header.sequence_id = bytes[2];
+	offset += gip_decode_length(&header.length, &bytes[offset], num_bytes - offset);
+
+	is_fragment = header.flags & GIP_FLAG_FRAGMENT;
+	attachment_index = header.flags & GIP_FLAG_ATTACHMENT_MASK;
+	attachment = gip_ensure_attachment(device, attachment_index);
+
+	print_hex_dump_debug(KBUILD_MODNAME ": Received message: ", DUMP_PREFIX_OFFSET,
+		16, 1, bytes, num_bytes, false);
+
+	guard(mutex)(&attachment->lock);
+	/* Handle coalescing fragmented messages */
+	if (is_fragment) {
+		rc = gip_receive_fragment(attachment, &header, offset,
+			&fragment_offset, &bytes_remaining, bytes, num_bytes);
+	} else if (header.length + offset > num_bytes) {
+		dev_warn(GIP_DEV(device),
+			"Received message with erroneous length (claimed %llu, actual %d), discarding\n",
+			header.length + offset, num_bytes);
+		rc = -EINVAL;
+	} else {
+		num_bytes -= offset;
+		bytes += offset;
+		fragment_offset = header.length;
+		rc = gip_handle_message(attachment, &header, bytes, num_bytes);
+	}
+
+	if (!rc && (header.flags & GIP_FLAG_ACME))
+		gip_acknowledge(device, &header, fragment_offset, bytes_remaining);
+
+	return rc;
+}
+
+static void gip_receive_work(struct work_struct *work)
+{
+	struct gip_device *device = container_of(work, struct gip_device,
+		receive_message);
+	unsigned long flags;
+
+	spin_lock_irqsave(&device->message_lock, flags);
+	while (device->pending_in_messages) {
+		struct gip_raw_message *message = &device->in_queue[device->next_in_message];
+
+		spin_unlock_irqrestore(&device->message_lock, flags);
+
+		gip_receive_message(device, message->bytes, message->num_bytes);
+
+		spin_lock_irqsave(&device->message_lock, flags);
+		device->next_in_message = (device->next_in_message + 1) % MAX_IN_MESSAGES;
+		device->pending_in_messages--;
+	}
+	spin_unlock_irqrestore(&device->message_lock, flags);
+}
+
+static void gip_urb_in(struct urb *urb)
+{
+	struct gip_interface *intf = urb->context;
+	struct gip_device *gip = intf->device;
+	struct device *dev = &intf->intf->dev;
+	int status = urb->status;
+	int message_id;
+	struct gip_raw_message *message;
+	unsigned long flags;
+
+	switch (status) {
+	case 0:
+		/* success */
+		break;
+	case -ECONNRESET:
+	case -ENOENT:
+	case -ESHUTDOWN:
+		/* this urb is terminated, clean up */
+		dev_dbg(dev, "%s - urb shutting down with status: %d\n",
+			__func__, status);
+		return;
+	default:
+		dev_dbg(dev, "%s - urb has status of: %d\n",
+			__func__, status);
+		goto exit;
+	}
+	if (intf->isoc_messages) {
+		/* TODO: Needed for audio support */
+		dev_warn(GIP_DEV(gip), "Unimplemented isochronous message input\n");
+		goto exit;
+	}
+
+	spin_lock_irqsave(&gip->message_lock, flags);
+	if (gip->pending_in_messages >= MAX_IN_MESSAGES) {
+		dev_err(GIP_DEV(gip), "Input queue is full; dropping message\n");
+	} else {
+		message_id = (gip->next_in_message + gip->pending_in_messages) % MAX_IN_MESSAGES;
+		message = &gip->in_queue[message_id];
+		gip->pending_in_messages++;
+		memcpy(message->bytes, intf->in_data, urb->actual_length);
+		message->num_bytes = urb->actual_length;
+	}
+	spin_unlock_irqrestore(&gip->message_lock, flags);
+	schedule_work(&gip->receive_message);
+
+exit:
+	status = usb_submit_urb(urb, GFP_ATOMIC);
+	if (status)
+		dev_err(dev, "%s - usb_submit_urb failed with result %d\n",
+			__func__, status);
+}
+
+static void gip_urb_out(struct urb *urb)
+{
+	struct gip_interface *intf = urb->context;
+	struct device *dev = &intf->intf->dev;
+	int status = urb->status;
+
+	guard(spinlock_irqsave)(&intf->device->message_lock);
+
+	switch (status) {
+	case 0:
+		/* success */
+		break;
+
+	case -ECONNRESET:
+	case -ENOENT:
+	case -ESHUTDOWN:
+		/* this urb is terminated, clean up */
+		dev_dbg(dev, "%s - urb shutting down with status: %d\n",
+			__func__, status);
+		break;
+
+	default:
+		dev_dbg(dev, "%s - nonzero urb status received: %d\n",
+			__func__, status);
+		break;
+	}
+}
+
+static int gip_init_input(struct gip_interface *intf,
+	struct usb_endpoint_descriptor *ep_in)
+{
+	int error;
+	struct usb_device *udev = interface_to_usbdev(intf->intf);
+
+	intf->urb_in = usb_alloc_urb(intf->isoc_messages, GFP_KERNEL);
+	if (!intf->urb_in)
+		return -ENOMEM;
+
+	intf->in_data = usb_alloc_coherent(udev, intf->mtu, GFP_KERNEL,
+		&intf->urb_in->transfer_dma);
+
+	if (!intf->in_data) {
+		return -ENOMEM;
+		goto err_free_urb;
+	}
+
+	usb_fill_int_urb(intf->urb_in, udev,
+		usb_rcvintpipe(udev, ep_in->bEndpointAddress),
+		intf->in_data, intf->mtu, gip_urb_in, intf,
+		ep_in->bInterval);
+	intf->urb_in->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
+
+	if (intf->isoc_messages)
+		intf->urb_in->transfer_flags |= URB_ISO_ASAP;
+
+	return 0;
+
+err_free_urb:
+	usb_free_urb(intf->urb_in);
+	intf->urb_in = NULL;
+
+	return error;
+}
+
+static int gip_init_output(struct gip_interface *intf,
+	struct usb_endpoint_descriptor *ep_out)
+{
+	int error;
+	struct usb_device *udev = interface_to_usbdev(intf->intf);
+	int i;
+
+	if (usb_ifnum_to_if(udev, GIP_WIRED_INTF_AUDIO)) {
+		/*
+		 * Explicitly disable the audio interface. This is needed
+		 * for some controllers, such as the PowerA Enhanced Wired
+		 * Controller for Series X|S (0x20d6:0x200e) to report the
+		 * guide button.
+		 */
+		error = usb_set_interface(udev, GIP_WIRED_INTF_AUDIO, 0);
+		if (error)
+			dev_warn(GIP_DEV(intf),
+				"unable to disable audio interface: %d\n",
+				error);
+	}
+
+	init_usb_anchor(&intf->out_anchor);
+
+	for (i = 0; i < MAX_OUT_MESSAGES; i++) {
+		intf->out_queue[i].urb = usb_alloc_urb(intf->isoc_messages, GFP_KERNEL);
+		if (!intf->out_queue[i].urb) {
+			error = -ENOMEM;
+			goto err_free_urbs;
+		}
+
+		intf->out_queue[i].data = usb_alloc_coherent(udev, intf->mtu, GFP_KERNEL,
+			&intf->out_queue[i].urb->transfer_dma);
+
+		if (!intf->out_queue[i].data) {
+			return -ENOMEM;
+			goto err_free_urbs;
+		}
+
+		usb_fill_int_urb(intf->out_queue[i].urb, udev,
+			usb_sndintpipe(udev, ep_out->bEndpointAddress),
+			intf->out_queue[i].data, intf->mtu, gip_urb_out, intf,
+			ep_out->bInterval);
+		intf->out_queue[i].urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
+
+		if (intf->isoc_messages)
+			intf->out_queue[i].urb->transfer_flags |= URB_ISO_ASAP;
+	}
+
+	return 0;
+
+err_free_urbs:
+	for (i = 0; i < MAX_OUT_MESSAGES; i++) {
+		if (intf->out_queue[i].data)
+			usb_free_coherent(udev, intf->mtu, intf->out_queue[i].data,
+				intf->out_queue[i].urb->transfer_dma);
+		if (intf->out_queue[i].urb) {
+			usb_free_urb(intf->out_queue[i].urb);
+			intf->out_queue[i].urb = NULL;
+		}
+	}
+	return error;
+}
+
+static void gip_deinit_output(struct gip_interface *intf)
+{
+	int i;
+
+	for (i = 0; i < MAX_OUT_MESSAGES; i++) {
+		if (!intf->out_queue[i].urb)
+			continue;
+		usb_free_coherent(interface_to_usbdev(intf->intf), intf->mtu,
+			intf->out_queue[i].data, intf->out_queue[i].urb->transfer_dma);
+		usb_free_urb(intf->out_queue[i].urb);
+		intf->out_queue[i].data = NULL;
+		intf->out_queue[i].urb = NULL;
+	}
+}
+
+static void gip_deinit_input(struct gip_interface *intf)
+{
+	usb_free_coherent(interface_to_usbdev(intf->intf), intf->mtu,
+		intf->in_data, intf->urb_in->transfer_dma);
+	usb_free_urb(intf->urb_in);
+	intf->urb_in = NULL;
+}
+
+static int gip_interface_init(struct gip_interface *intf)
+{
+	struct usb_endpoint_descriptor *ep_in = NULL;
+	struct usb_endpoint_descriptor *ep_out = NULL;
+	int error = usb_find_common_endpoints(intf->intf->cur_altsetting,
+		NULL, NULL, &ep_in, &ep_out);
+
+	if (error)
+		return error;
+
+	if (!ep_in || !ep_out)
+		return -ENODEV;
+
+	error = gip_init_input(intf, ep_in);
+	if (error)
+		return error;
+
+	error = gip_init_output(intf, ep_out);
+	if (error)
+		goto err_free_input;
+
+	if (usb_submit_urb(intf->urb_in, GFP_KERNEL)) {
+		error = -EIO;
+		goto err_free_output;
+	}
+
+	return 0;
+
+err_free_output:
+	gip_deinit_output(intf);
+err_free_input:
+	gip_deinit_input(intf);
+	return error;
+}
+
+static int gip_probe(struct usb_interface *intf, const struct usb_device_id *id)
+{
+	struct usb_device *udev = interface_to_usbdev(intf);
+	struct gip_device *gip = NULL;
+	struct gip_attachment *attachment;
+	int rc;
+
+	if (intf->cur_altsetting->desc.bInterfaceNumber != GIP_WIRED_INTF_DATA) {
+		/*
+		 * The Xbox One controller lists three interfaces all with the
+		 * same interface class, subclass and protocol. Differentiate by
+		 * interface number.
+		 */
+		return 0;
+	}
+
+	gip = devm_kzalloc(&udev->dev, sizeof(*gip), GFP_KERNEL);
+	if (!gip)
+		return -ENOMEM;
+
+	gip->udev = udev;
+	gip->data.device = gip;
+	gip->data.intf = intf;
+	gip->data.mtu = BASE_GIP_MTU;
+	gip->audio.device = gip;
+	gip->audio.mtu = MAX_GIP_MTU;
+	gip->audio.isoc_messages = MAX_AUDIO_MESSAGES;
+
+	INIT_WORK(&gip->receive_message, gip_receive_work);
+	spin_lock_init(&gip->message_lock);
+
+	rc = gip_interface_init(&gip->data);
+	if (rc) {
+		devm_kfree(GIP_DEV(gip), gip);
+		return rc;
+	}
+	/* Don't init audio interface -- we aren't using it yet */
+
+	usb_set_intfdata(intf, gip);
+
+	/* Pre-create the first attachment, as it should always exist */
+	attachment = gip_ensure_attachment(gip, 0);
+	if (IS_ERR(attachment))
+		return PTR_ERR(attachment);
+
+	return 0;
+}
+
+static int gip_shutdown(struct gip_device *device)
+{
+	int i;
+
+	cancel_work_sync(&device->receive_message);
+
+	for (i = 0; i < MAX_ATTACHMENTS; i++) {
+		struct gip_attachment *attachment = device->attachments[i];
+		struct input_dev *input;
+
+		if (!attachment)
+			continue;
+
+		scoped_guard (mutex, &attachment->lock) {
+			cancel_delayed_work_sync(&attachment->metadata_next);
+			cancel_delayed_work_sync(&attachment->fragment_timeout);
+
+			rcu_read_lock();
+			input = rcu_dereference(attachment->input);
+			rcu_read_unlock();
+
+			rcu_assign_pointer(attachment->input, NULL);
+			synchronize_rcu();
+		}
+
+		if (input)
+			input_unregister_device(input);
+	}
+
+	return 0;
+}
+
+static void gip_disconnect(struct usb_interface *intf)
+{
+	struct gip_device *gip = usb_get_intfdata(intf);
+	unsigned long flags;
+	int i;
+
+	if (!gip)
+		return;
+
+	usb_kill_urb(gip->data.urb_in);
+	if (gip->audio.intf)
+		usb_kill_urb(gip->audio.urb_in);
+
+	gip_shutdown(gip);
+
+	spin_lock_irqsave(&gip->message_lock, flags);
+	gip_deinit_input(&gip->data);
+	gip_deinit_output(&gip->data);
+	if (gip->audio.intf) {
+		gip_deinit_input(&gip->audio);
+		gip_deinit_output(&gip->audio);
+	}
+	spin_unlock_irqrestore(&gip->message_lock, flags);
+
+	usb_set_intfdata(intf, NULL);
+
+	for (i = 0; i < MAX_ATTACHMENTS; i++) {
+		struct gip_attachment *attachment = gip->attachments[i];
+
+		if (!attachment)
+			continue;
+		devm_kfree(GIP_DEV(attachment), attachment->uniq);
+		devm_kfree(GIP_DEV(attachment), attachment);
+	}
+
+	devm_kfree(GIP_DEV(gip), gip);
+}
+
+static int gip_suspend(struct usb_interface *intf, pm_message_t message)
+{
+	struct gip_device *gip = usb_get_intfdata(intf);
+
+	if (!gip)
+		return 0;
+
+	usb_kill_urb(gip->data.urb_in);
+	if (gip->audio.intf)
+		usb_kill_urb(gip->audio.urb_in);
+
+	if (gip->attachments[0]) {
+		struct gip_attachment *attachment = gip->attachments[0];
+
+		guard(mutex)(&attachment->lock);
+		gip_send_set_device_state(attachment, GIP_STATE_OFF);
+		attachment->device_state = GIP_STATE_OFF;
+	}
+
+	return gip_shutdown(gip);
+}
+
+static int gip_resume(struct usb_interface *intf)
+{
+	struct gip_device *gip = usb_get_intfdata(intf);
+
+	if (!gip)
+		return 0;
+
+	if (usb_submit_urb(gip->data.urb_in, GFP_KERNEL))
+		return -EIO;
+
+	return 0;
+}
+
+/* The Xbox One controller uses subclass 71 and protocol 208. */
+#define GIP_VENDOR(vend) \
+	{ \
+		.match_flags = USB_DEVICE_ID_MATCH_VENDOR | USB_DEVICE_ID_MATCH_INT_INFO, \
+		.idVendor = (vend), \
+		.bInterfaceClass = USB_CLASS_VENDOR_SPEC, \
+		.bInterfaceSubClass = 71, \
+		.bInterfaceProtocol = 208 \
+	}
+
+static const struct usb_device_id gip_table[] = {
+	/*
+	 * Please keep this list sorted by vendor ID.
+	 */
+	GIP_VENDOR(0x03f0),		/* HP/HyperX */
+	GIP_VENDOR(0x044f),		/* ThrustMaster */
+	GIP_VENDOR(0x045e),		/* Microsoft */
+	GIP_VENDOR(0x046d),		/* Logitech */
+	GIP_VENDOR(0x0738),		/* Mad Catz */
+	GIP_VENDOR(0x0b05),		/* ASUS */
+	GIP_VENDOR(0x0e6f),		/* PDP */
+	GIP_VENDOR(0x0f0d),		/* Hori */
+	GIP_VENDOR(0x10f5),		/* Turtle Beach */
+	GIP_VENDOR(0x1532),		/* Razer */
+	GIP_VENDOR(0x20d6),		/* PowerA/BDA */
+	GIP_VENDOR(0x24c6),		/* PowerA/BDA/ThrustMaster */
+	GIP_VENDOR(0x294b),		/* Snakebyte */
+	GIP_VENDOR(0x2dc8),		/* 8BitDo */
+	GIP_VENDOR(0x2e24),		/* Hyperkin */
+	GIP_VENDOR(0x2e95),		/* SCUF Gaming */
+	GIP_VENDOR(0x3285),		/* Nacon */
+	GIP_VENDOR(0x3537),		/* GameSir */
+	GIP_VENDOR(0x366c),		/* ByoWave */
+	{ }
+};
+
+MODULE_DEVICE_TABLE(usb, gip_table);
+
+static struct usb_driver gip_driver = {
+	.name		= "xbox-gip",
+	.probe		= gip_probe,
+	.disconnect	= gip_disconnect,
+	.suspend	= gip_suspend,
+	.resume		= gip_resume,
+	.id_table	= gip_table,
+};
+
+module_usb_driver(gip_driver);
+
+MODULE_AUTHOR("Vicki Pfau <vi@endrift.com>");
+MODULE_DESCRIPTION("Xbox Gaming Input Protocol driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/input/joystick/gip/gip-drivers.c b/drivers/input/joystick/gip/gip-drivers.c
new file mode 100644
index 0000000000000..f5507e6215a94
--- /dev/null
+++ b/drivers/input/joystick/gip/gip-drivers.c
@@ -0,0 +1,210 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Base drivers for common GIP devices
+ *
+ * Copyright (c) 2025 Valve Software
+ *
+ * This driver is based on the Microsoft GIP spec at:
+ * https://aka.ms/gipdocs
+ * https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gipusb/e7c90904-5e21-426e-b9ad-d82adeee0dbc
+ */
+#include "gip.h"
+
+struct gip_device_capabilities_response {
+	uint8_t extra_button_count;
+	uint8_t extra_axis_count;
+	uint8_t led_count;
+	uint8_t max_global_led_gain;
+};
+
+static bool dpad_as_buttons;
+
+static int gip_setup_gamepad_input(struct gip_attachment *attachment, struct input_dev* input)
+{
+	int ret = gip_driver_navigation.setup_input(attachment, input);
+
+	if (ret < 0)
+		return ret;
+	input_set_capability(input, EV_KEY, BTN_THUMBR);
+	input_set_capability(input, EV_KEY, BTN_THUMBL);
+	input_set_abs_params(input, ABS_X, -32768, 32767, 16, 128);
+	input_set_abs_params(input, ABS_Y, -32768, 32767, 16, 128);
+	input_set_abs_params(input, ABS_RX, -32768, 32767, 16, 128);
+	input_set_abs_params(input, ABS_RY, -32768, 32767, 16, 128);
+	input_set_abs_params(input, ABS_Z, 0, 1023, 0, 0);
+	input_set_abs_params(input, ABS_RZ, 0, 1023, 0, 0);
+
+	/* Xbox Adaptive Controller */
+	if (attachment->vendor_id == 0x045e && attachment->product_id == 0x0b0a)
+		input_set_abs_params(input, ABS_PROFILE, 0, 3, 0, 0);
+	return 0;
+}
+
+static int gip_handle_gamepad_report(struct gip_attachment *attachment,
+	struct input_dev *input, const uint8_t *bytes, int num_bytes)
+{
+	int16_t axis;
+	int ret = gip_driver_navigation.handle_input_report(attachment, input, bytes, num_bytes);
+
+	if (ret < 0)
+		return ret;
+
+	if (num_bytes < 14) {
+		dev_dbg(GIP_DEV(attachment), "Discarding too-short input report\n");
+		return -EINVAL;
+	}
+
+	input_report_key(input, BTN_THUMBL, bytes[1] & BIT(6));
+	input_report_key(input, BTN_THUMBR, bytes[1] & BIT(7));
+
+	axis = bytes[2];
+	axis |= bytes[3] << 8;
+	input_report_abs(input, ABS_Z, axis);
+
+	axis = bytes[4];
+	axis |= bytes[5] << 8;
+	input_report_abs(input, ABS_RZ, axis);
+
+	axis = bytes[6];
+	axis |= bytes[7] << 8;
+	input_report_abs(input, ABS_X, axis);
+	axis = bytes[8];
+	axis |= bytes[9] << 8;
+	input_report_abs(input, ABS_Y, ~axis);
+	axis = bytes[10];
+	axis |= bytes[11] << 8;
+	input_report_abs(input, ABS_RX, axis);
+	axis = bytes[12];
+	axis |= bytes[13] << 8;
+	input_report_abs(input, ABS_RY, ~axis);
+
+	/* Xbox Adaptive Controller */
+	if (attachment->vendor_id == 0x045e && attachment->product_id == 0x0b0a && num_bytes >= 31)
+		input_report_abs(input, ABS_PROFILE, bytes[30] & 3);
+
+	return 0;
+}
+
+const struct gip_driver gip_driver_gamepad = {
+	.types = (const char* const[]) { "Windows.Xbox.Input.Gamepad", NULL },
+	.guid = GUID_INIT(0x082e402c, 0x07df, 0x45e1, 0xa5, 0xab,
+		0xa3, 0x12, 0x7a, 0xf1, 0x97, 0xb5),
+
+	.quirks = (const struct gip_quirks[]) {
+		/* Xbox One Controller (model 1573) */
+		{ 0x045e, 0x02d1, 0, .override_name = "Xbox One Controller" },
+
+		/* Xbox One Controller (model 1697) */
+		{ 0x045e, 0x02dd, 0, .override_name = "Xbox One Controller" },
+
+		/* Xbox Elite */
+		{ 0x045e, 0x02e3, 0,
+			.override_name = "Xbox Elite Controller",
+			.added_features = GIP_FEATURE_ELITE_BUTTONS,
+			.filtered_features = GIP_FEATURE_CONSOLE_FUNCTION_MAP },
+
+		/* Xbox One Controller (model 1708) */
+		{ 0x045e, 0x02ea, 0, .override_name = "Xbox One Controller" },
+
+		/* Xbox Elite 2 */
+		{ 0x045e, 0x0b00, 0,
+			.override_name = "Xbox Elite Series 2 Controller",
+			.added_features = GIP_FEATURE_GUIDE_COLOR |
+				GIP_FEATURE_EXTENDED_SET_DEVICE_STATE },
+
+		/* Xbox Adaptive Controller */
+		{ 0x045e, 0x0b0a, 0, .override_name = "Xbox Adaptive Controller" },
+
+		/* Xbox Wireless Controller */
+		{ 0x045e, 0x0b12, 0, .override_name = "Xbox Wireless Controller" },
+
+		{0},
+	},
+
+	.probe = NULL,
+	.remove = NULL,
+	.init = NULL,
+	.setup_input = gip_setup_gamepad_input,
+	.handle_input_report = gip_handle_gamepad_report,
+};
+
+static int gip_setup_navigation_input(struct gip_attachment *attachment, struct input_dev *input)
+{
+	input_set_capability(input, EV_KEY, BTN_Y);
+	input_set_capability(input, EV_KEY, BTN_B);
+	input_set_capability(input, EV_KEY, BTN_X);
+	input_set_capability(input, EV_KEY, BTN_A);
+	input_set_capability(input, EV_KEY, BTN_SELECT);
+	input_set_capability(input, EV_KEY, BTN_MODE);
+	input_set_capability(input, EV_KEY, BTN_START);
+	input_set_capability(input, EV_KEY, BTN_TR);
+	input_set_capability(input, EV_KEY, BTN_TL);
+
+	attachment->dpad_as_buttons = dpad_as_buttons;
+	if (attachment->dpad_as_buttons) {
+		input_set_capability(input, EV_KEY, BTN_DPAD_UP);
+		input_set_capability(input, EV_KEY, BTN_DPAD_RIGHT);
+		input_set_capability(input, EV_KEY, BTN_DPAD_LEFT);
+		input_set_capability(input, EV_KEY, BTN_DPAD_DOWN);
+	} else {
+		input_set_abs_params(input, ABS_HAT0X, -1, 1, 0, 0);
+		input_set_abs_params(input, ABS_HAT0Y, -1, 1, 0, 0);
+	}
+
+	return 0;
+}
+
+static int gip_handle_navigation_report(struct gip_attachment *attachment,
+	struct input_dev *input, const uint8_t *bytes, int num_bytes)
+{
+	if (num_bytes < 2) {
+		dev_dbg(GIP_DEV(attachment), "Discarding too-short input report\n");
+		return -EINVAL;
+	}
+
+	input_report_key(input, BTN_START, bytes[0] & BIT(2));
+	input_report_key(input, BTN_SELECT, bytes[0] & BIT(3));
+	input_report_key(input, BTN_A, bytes[0] & BIT(4));
+	input_report_key(input, BTN_B, bytes[0] & BIT(5));
+	input_report_key(input, BTN_X, bytes[0] & BIT(6));
+	input_report_key(input, BTN_Y, bytes[0] & BIT(7));
+
+	if (attachment->dpad_as_buttons) {
+		input_report_key(input, BTN_DPAD_UP, bytes[1] & BIT(0));
+		input_report_key(input, BTN_DPAD_DOWN, bytes[1] & BIT(1));
+		input_report_key(input, BTN_DPAD_LEFT, bytes[1] & BIT(2));
+		input_report_key(input, BTN_DPAD_RIGHT, bytes[1] & BIT(3));
+	} else {
+		input_report_abs(input, ABS_HAT0X,
+			!!(bytes[1] & BIT(3)) - !!(bytes[1] & BIT(2)));
+		input_report_abs(input, ABS_HAT0Y,
+			!!(bytes[1] & BIT(1)) - !!(bytes[1] & BIT(0)));
+	}
+
+	if (attachment->quirks & GIP_QUIRK_SWAP_LB_RB) {
+		/* Previous */
+		input_report_key(input, BTN_TR, bytes[1] & BIT(4));
+		/* Next */
+		input_report_key(input, BTN_TL, bytes[1] & BIT(5));
+	} else {
+		input_report_key(input, BTN_TL, bytes[1] & BIT(4));
+		input_report_key(input, BTN_TR, bytes[1] & BIT(5));
+	}
+
+	return 0;
+}
+
+const struct gip_driver gip_driver_navigation = {
+	.types = (const char* const[]) { "Windows.Xbox.Input.NavigationController", NULL },
+	.guid = GUID_INIT(0xb8f31fe7, 0x7386, 0x40e9, 0xa9, 0xf8,
+		0x2f, 0x21, 0x26, 0x3a, 0xcf, 0xb7),
+
+	.probe = NULL,
+	.remove = NULL,
+	.init = NULL,
+	.setup_input = gip_setup_navigation_input,
+	.handle_input_report = gip_handle_navigation_report,
+};
+
+module_param(dpad_as_buttons, bool, 0444);
+MODULE_PARM_DESC(dpad_as_buttons, "Map the D-Pad as buttons instead of axes");
diff --git a/drivers/input/joystick/gip/gip.h b/drivers/input/joystick/gip/gip.h
new file mode 100644
index 0000000000000..2c60430c81590
--- /dev/null
+++ b/drivers/input/joystick/gip/gip.h
@@ -0,0 +1,309 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Gaming Input Protocol driver for Xbox One/Series controllers
+ *
+ * Copyright (c) 2025 Valve Software
+ *
+ * This driver is based on the Microsoft GIP spec at:
+ * https://aka.ms/gipdocs
+ * https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gipusb/e7c90904-5e21-426e-b9ad-d82adeee0dbc
+ */
+
+#ifndef _GIP_H
+#define _GIP_H
+
+#include <linux/rcupdate.h>
+#include <linux/usb/input.h>
+
+#define BASE_GIP_MTU 64
+#define MAX_GIP_MTU 2048
+
+#define MAX_ATTACHMENTS 8
+
+#define MAX_IN_MESSAGES 8
+#define MAX_OUT_MESSAGES 8
+
+#define GIP_QUIRK_NO_HELLO		BIT(0)
+#define GIP_QUIRK_NO_IMPULSE_VIBRATION	BIT(1)
+#define GIP_QUIRK_SWAP_LB_RB		BIT(2)
+
+#define GIP_FEATURE_CONTROLLER				BIT(0)
+#define GIP_FEATURE_CONSOLE_FUNCTION_MAP		BIT(1)
+#define GIP_FEATURE_CONSOLE_FUNCTION_MAP_OVERFLOW	BIT(2)
+#define GIP_FEATURE_ELITE_BUTTONS			BIT(3)
+#define GIP_FEATURE_DYNAMIC_LATENCY_INPUT		BIT(4)
+#define GIP_FEATURE_SECURITY_OPT_OUT			BIT(5)
+#define GIP_FEATURE_MOTOR_CONTROL			BIT(6)
+#define GIP_FEATURE_GUIDE_COLOR				BIT(7)
+#define GIP_FEATURE_EXTENDED_SET_DEVICE_STATE		BIT(8)
+
+/* System messages */
+#define GIP_CMD_PROTO_CONTROL		0x01
+#define GIP_CMD_HELLO_DEVICE		0x02
+#define GIP_CMD_STATUS_DEVICE		0x03
+#define GIP_CMD_METADATA		0x04
+#define GIP_CMD_SET_DEVICE_STATE	0x05
+#define GIP_CMD_SECURITY		0x06
+#define GIP_CMD_GUIDE_BUTTON		0x07
+#define GIP_CMD_AUDIO_CONTROL		0x08
+#define GIP_CMD_LED			0x0a
+#define GIP_CMD_HID_REPORT		0x0b
+#define GIP_CMD_FIRMWARE		0x0c
+#define GIP_CMD_EXTENDED		0x1e
+#define GIP_CMD_DEBUG			0x1f
+#define GIP_AUDIO_DATA			0x60
+
+/* Navigation vendor messages */
+#define GIP_CMD_DIRECT_MOTOR		0x09
+#define GIP_LL_INPUT_REPORT		0x20
+#define GIP_LL_OVERFLOW_INPUT_REPORT	0x26
+
+/* Wheel and ArcadeStick vendor messages */
+#define GIP_CMD_INITIAL_REPORTS_REQUEST	0x0a
+#define GIP_LL_STATIC_CONFIGURATION	0x21
+#define GIP_LL_BUTTON_INFO_REPORT	0x22
+
+#define MAX_GIP_CMD 0x80
+
+#define GIP_DEV(p) \
+	_Generic((p), \
+		struct gip_attachment * : gip_attachment_dev, \
+		struct gip_interface * : gip_interface_dev, \
+		struct gip_device * : gip_device_dev)(p)
+
+enum gip_init_status {
+	GIP_INIT_OK = 0,
+	GIP_INIT_NO_INPUT = 1,
+};
+
+enum gip_metadata_status {
+	GIP_METADATA_NONE = 0,
+	GIP_METADATA_GOT = 1,
+	GIP_METADATA_FAKED = 2,
+	GIP_METADATA_PENDING = 3,
+};
+
+enum gip_elite_button_format {
+	GIP_BTN_FMT_UNKNOWN,
+	GIP_BTN_FMT_XBE1,
+	GIP_BTN_FMT_XBE2_RAW,
+	GIP_BTN_FMT_XBE2_4,
+	GIP_BTN_FMT_XBE2_5,
+};
+
+struct gip_header {
+	uint8_t message_type;
+	uint8_t flags;
+	uint8_t sequence_id;
+	uint64_t length;
+};
+
+struct gip_raw_message {
+	uint16_t num_bytes;
+	uint8_t bytes[BASE_GIP_MTU];
+};
+
+struct gip_device_metadata {
+	uint8_t num_audio_formats;
+	uint8_t num_preferred_types;
+	uint8_t num_supported_interfaces;
+	uint8_t hid_descriptor_size;
+
+	uint32_t in_system_messages[8];
+	uint32_t out_system_messages[8];
+
+	struct gip_audio_format_pair *audio_formats;
+	char **preferred_types;
+	guid_t *supported_interfaces;
+	uint8_t *hid_descriptor;
+};
+
+struct gip_message_metadata {
+	uint8_t type;
+	uint16_t length;
+	uint16_t data_type;
+	uint32_t flags;
+	uint16_t period;
+	uint16_t persistence_timeout;
+};
+
+struct gip_metadata {
+	uint16_t version_major;
+	uint16_t version_minor;
+
+	struct gip_device_metadata device;
+
+	uint8_t num_messages;
+	struct gip_message_metadata *message_metadata;
+};
+
+struct gip_status {
+	int power_level;
+	int charge;
+	int battery_type;
+	int battery_level;
+};
+
+struct gip_status_event {
+	uint16_t event_type;
+	uint32_t fault_tag;
+	uint32_t fault_address;
+};
+
+struct gip_extended_status {
+	struct gip_status base;
+	bool device_active;
+
+	int num_events;
+	struct gip_status_event events[5];
+};
+
+struct gip_attachment;
+typedef int (*gip_command_handler)(struct gip_attachment *a, const struct gip_header *header,
+		const uint8_t *bytes, int num_bytes);
+
+struct gip_device;
+struct gip_attachment {
+	const struct gip_driver *driver;
+	struct gip_device *device;
+	void *driver_data;
+	gip_command_handler vendor_handlers[MAX_GIP_CMD];
+
+	uint8_t attachment_index;
+	struct input_dev __rcu *input;
+	uint16_t vendor_id;
+	uint16_t product_id;
+	char *uniq;
+	const char *name;
+	char phys[32];
+	char serial[32];
+	struct mutex lock;
+
+	uint8_t fragment_message;
+	uint16_t total_length;
+	uint8_t *fragment_data;
+	uint32_t fragment_offset;
+	struct delayed_work fragment_timeout;
+	int fragment_retries;
+
+	uint16_t firmware_major_version;
+	uint16_t firmware_minor_version;
+
+	enum gip_metadata_status got_metadata;
+	struct delayed_work metadata_next;
+	int metadata_retries;
+	struct gip_metadata metadata;
+
+	uint8_t seq_system;
+	uint8_t seq_security;
+	uint8_t seq_extended;
+	uint8_t seq_audio;
+	uint8_t seq_vendor;
+
+	int device_state;
+
+	struct gip_extended_status status;
+
+	enum gip_elite_button_format xbe_format;
+	uint32_t features;
+	uint32_t quirks;
+
+	int extra_buttons;
+	int extra_axes;
+
+	bool dpad_as_buttons;
+	struct hid_device __rcu *hdev;
+};
+
+struct gip_urb {
+	struct urb *urb;
+	uint8_t *data;
+	unsigned int offset;
+};
+
+struct gip_interface {
+	struct gip_device *device;
+	struct usb_interface *intf;
+	uint32_t mtu;
+	int isoc_messages;
+
+	struct urb *urb_in;
+	uint8_t *in_data;
+
+	struct usb_anchor out_anchor;
+	struct gip_urb out_queue[MAX_OUT_MESSAGES];
+};
+
+struct gip_device {
+	struct usb_device *udev;
+
+	struct gip_interface data;
+	struct gip_interface audio;
+
+	struct gip_raw_message in_queue[MAX_IN_MESSAGES];
+	int pending_in_messages;
+	int next_in_message;
+
+	struct work_struct receive_message;
+	spinlock_t message_lock;
+
+	struct gip_attachment *attachments[MAX_ATTACHMENTS];
+};
+
+struct gip_quirks {
+	uint16_t vendor_id;
+	uint16_t product_id;
+	uint8_t attachment_index;
+	const char *override_name;
+	uint32_t added_features;
+	uint32_t filtered_features;
+	uint32_t quirks;
+	uint32_t extra_in_system[8];
+	uint32_t extra_out_system[8];
+	uint8_t extra_buttons;
+	uint8_t extra_axes;
+};
+
+struct gip_driver {
+	const char *const *types;
+	guid_t guid;
+
+	const struct gip_quirks *quirks;
+
+	int (*probe)(struct gip_attachment *a);
+	void (*remove)(struct gip_attachment *a);
+	int (*init)(struct gip_attachment *a);
+	int (*setup_input)(struct gip_attachment *a, struct input_dev* input);
+	int (*handle_input_report)(struct gip_attachment *a,
+		struct input_dev* input, const uint8_t *bytes, int num_bytes);
+	gip_command_handler vendor_handlers[MAX_GIP_CMD];
+};
+
+static inline struct device *gip_attachment_dev(struct gip_attachment *attachment)
+{
+	return &attachment->device->udev->dev;
+}
+
+static inline struct device *gip_interface_dev(struct gip_interface *intf)
+{
+	return &intf->device->udev->dev;
+}
+
+static inline struct device *gip_device_dev(struct gip_device *device)
+{
+	return &device->udev->dev;
+}
+
+bool gip_supports_vendor_message(struct gip_attachment *attachment, uint8_t command, bool upstream);
+
+int gip_send_system_message(struct gip_attachment *attachment,
+	uint8_t message_type, uint8_t flags, const void *bytes, int num_bytes);
+int gip_send_vendor_message(struct gip_attachment *attachment,
+	uint8_t message_type, uint8_t flags, const void *bytes, int num_bytes);
+
+extern const struct gip_driver gip_driver_navigation;
+extern const struct gip_driver gip_driver_gamepad;
+extern const struct gip_driver gip_driver_arcade_stick;
+extern const struct gip_driver gip_driver_wheel;
+extern const struct gip_driver gip_driver_flight_stick;
+#endif
-- 
2.53.0


^ permalink raw reply related


This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox