Linux Input/HID development
 help / color / mirror / Atom feed
* [PATCH v3 06/16] HID: hid-lenovo-go: Add RGB LED control interface
From: Derek J. Clark @ 2026-01-24  1:48 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Mario Limonciello, Zhixin Zhang, Mia Shao, Mark Pearson,
	Pierre-Loup A . Griffais, Derek J . Clark, linux-input, linux-doc,
	linux-kernel
In-Reply-To: <20260124014907.991265-1-derekjohn.clark@gmail.com>

Adds an LED multicolor class device and attribute group for controlling
the RGB of the Left and right handles. In addition to the standard
led_cdev attributes, additional attributes that allow for the control of
the effect (monocolor, breathe, rainbow, and chroma), speed of the
effect change, an enable toggle, and profile.

Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
 drivers/hid/hid-lenovo-go.c | 472 ++++++++++++++++++++++++++++++++++++
 1 file changed, 472 insertions(+)

diff --git a/drivers/hid/hid-lenovo-go.c b/drivers/hid/hid-lenovo-go.c
index 5d454cd2cdae..91eb61037a2c 100644
--- a/drivers/hid/hid-lenovo-go.c
+++ b/drivers/hid/hid-lenovo-go.c
@@ -17,12 +17,15 @@
 #include <linux/hid.h>
 #include <linux/jiffies.h>
 #include <linux/kstrtox.h>
+#include <linux/led-class-multicolor.h>
 #include <linux/mutex.h>
 #include <linux/printk.h>
 #include <linux/sysfs.h>
 #include <linux/types.h>
 #include <linux/unaligned.h>
 #include <linux/usb.h>
+#include <linux/workqueue.h>
+#include <linux/workqueue_types.h>
 
 #include "hid-ids.h"
 
@@ -33,7 +36,9 @@
 
 struct hid_go_cfg {
 	unsigned char *buf;
+	struct delayed_work go_cfg_setup;
 	struct completion send_cmd_complete;
+	struct led_classdev *led_cdev;
 	struct hid_device *hdev;
 	struct mutex cfg_mutex; /*ensure single synchronous output report*/
 	u8 fps_mode;
@@ -65,7 +70,11 @@ struct hid_go_cfg {
 	u32 mcu_version_product;
 	u32 mcu_version_protocol;
 	u32 mouse_dpi;
+	u8 rgb_effect;
 	u8 rgb_en;
+	u8 rgb_mode;
+	u8 rgb_profile;
+	u8 rgb_speed;
 	u8 tp_en;
 	u8 tp_vibration_en;
 	u8 tp_vibration_intensity;
@@ -220,6 +229,41 @@ static const char *const rumble_mode_text[] = {
 
 #define FPS_MODE_DPI           0x02
 
+enum rgb_config_index {
+	LIGHT_CFG_ALL = 0x01,
+	LIGHT_MODE_SEL,
+	LIGHT_PROFILE_SEL,
+	USR_LIGHT_PROFILE_1,
+	USR_LIGHT_PROFILE_2,
+	USR_LIGHT_PROFILE_3,
+};
+
+enum rgb_mode_index {
+	RGB_MODE_UNKNOWN,
+	RGB_MODE_DYNAMIC,
+	RGB_MODE_CUSTOM,
+};
+
+static const char *const rgb_mode_text[] = {
+	[RGB_MODE_UNKNOWN] = "unknown",
+	[RGB_MODE_DYNAMIC] = "dynamic",
+	[RGB_MODE_CUSTOM] = "custom",
+};
+
+enum rgb_effect_index {
+	RGB_EFFECT_MONO,
+	RGB_EFFECT_BREATHE,
+	RGB_EFFECT_CHROMA,
+	RGB_EFFECT_RAINBOW,
+};
+
+static const char *const rgb_effect_text[] = {
+	[RGB_EFFECT_MONO] = "monocolor",
+	[RGB_EFFECT_BREATHE] = "breathe",
+	[RGB_EFFECT_CHROMA] = "chroma",
+	[RGB_EFFECT_RAINBOW] = "rainbow",
+};
+
 static int hid_go_version_event(struct command_report *cmd_rep)
 {
 	switch (cmd_rep->sub_cmd) {
@@ -437,6 +481,33 @@ static int hid_go_fps_dpi_event(struct command_report *cmd_rep)
 	return 0;
 }
 
+static int hid_go_light_event(struct command_report *cmd_rep)
+{
+	struct led_classdev_mc *mc_cdev;
+
+	switch (cmd_rep->sub_cmd) {
+	case LIGHT_MODE_SEL:
+		drvdata.rgb_mode = cmd_rep->data[0];
+		return 0;
+	case LIGHT_PROFILE_SEL:
+		drvdata.rgb_profile = cmd_rep->data[0];
+		return 0;
+	case USR_LIGHT_PROFILE_1:
+	case USR_LIGHT_PROFILE_2:
+	case USR_LIGHT_PROFILE_3:
+		mc_cdev = lcdev_to_mccdev(drvdata.led_cdev);
+		drvdata.rgb_effect = cmd_rep->data[0];
+		mc_cdev->subled_info[0].intensity = cmd_rep->data[1];
+		mc_cdev->subled_info[1].intensity = cmd_rep->data[2];
+		mc_cdev->subled_info[2].intensity = cmd_rep->data[3];
+		drvdata.led_cdev->brightness = cmd_rep->data[4];
+		drvdata.rgb_speed = cmd_rep->data[5];
+		return 0;
+	default:
+		return -EINVAL;
+	}
+}
+
 static int hid_go_set_event_return(struct command_report *cmd_rep)
 {
 	if (cmd_rep->data[0] != 0)
@@ -490,9 +561,13 @@ static int hid_go_raw_event(struct hid_device *hdev, struct hid_report *report,
 		case GET_DPI_CFG:
 			ret = hid_go_fps_dpi_event(cmd_rep);
 			break;
+		case GET_RGB_CFG:
+			ret = hid_go_light_event(cmd_rep);
+			break;
 		case SET_FEATURE_STATUS:
 		case SET_MOTOR_CFG:
 		case SET_DPI_CFG:
+		case SET_RGB_CFG:
 			ret = hid_go_set_event_return(cmd_rep);
 			break;
 		default:
@@ -1077,6 +1152,274 @@ static ssize_t fps_mode_dpi_index_show(struct device *dev,
 	return sysfs_emit(buf, "500 800 1200 1800\n");
 }
 
+static int rgb_cfg_call(struct hid_device *hdev, enum mcu_command_index cmd,
+			enum rgb_config_index index, u8 *val, size_t size)
+{
+	if (cmd != SET_RGB_CFG && cmd != GET_RGB_CFG)
+		return -EINVAL;
+
+	if (index < LIGHT_CFG_ALL || index > USR_LIGHT_PROFILE_3)
+		return -EINVAL;
+
+	return mcu_property_out(hdev, MCU_CONFIG_DATA, cmd, index, UNSPECIFIED,
+				val, size);
+}
+
+static int rgb_attr_show(void)
+{
+	enum rgb_config_index index;
+
+	index = drvdata.rgb_profile + 3;
+
+	return rgb_cfg_call(drvdata.hdev, GET_RGB_CFG, index, 0, 0);
+};
+
+static ssize_t rgb_effect_store(struct device *dev,
+				struct device_attribute *attr, const char *buf,
+				size_t count)
+{
+	struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(drvdata.led_cdev);
+	enum rgb_config_index index;
+	u8 effect;
+	int ret;
+
+	ret = sysfs_match_string(rgb_effect_text, buf);
+	if (ret < 0)
+		return ret;
+
+	effect = ret;
+	index = drvdata.rgb_profile + 3;
+	u8 rgb_profile[6] = { effect,
+			      mc_cdev->subled_info[0].intensity,
+			      mc_cdev->subled_info[1].intensity,
+			      mc_cdev->subled_info[2].intensity,
+			      drvdata.led_cdev->brightness,
+			      drvdata.rgb_speed };
+
+	ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, index, rgb_profile, 6);
+	if (ret)
+		return ret;
+
+	drvdata.rgb_effect = effect;
+	return count;
+};
+
+static ssize_t rgb_effect_show(struct device *dev,
+			       struct device_attribute *attr, char *buf)
+{
+	int ret;
+
+	ret = rgb_attr_show();
+	if (ret)
+		return ret;
+
+	if (drvdata.rgb_effect >= ARRAY_SIZE(rgb_effect_text))
+		return -EINVAL;
+
+	return sysfs_emit(buf, "%s\n", rgb_effect_text[drvdata.rgb_effect]);
+}
+
+static ssize_t rgb_effect_index_show(struct device *dev,
+				     struct device_attribute *attr, char *buf)
+{
+	ssize_t count = 0;
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(rgb_effect_text); i++)
+		count += sysfs_emit_at(buf, count, "%s ", rgb_effect_text[i]);
+
+	if (count)
+		buf[count - 1] = '\n';
+
+	return count;
+}
+
+static ssize_t rgb_speed_store(struct device *dev,
+			       struct device_attribute *attr, const char *buf,
+			       size_t count)
+{
+	struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(drvdata.led_cdev);
+	enum rgb_config_index index;
+	int val = 0;
+	int ret;
+
+	ret = kstrtoint(buf, 10, &val);
+	if (ret)
+		return ret;
+
+	if (val < 0 || val > 100)
+		return -EINVAL;
+
+	index = drvdata.rgb_profile + 3;
+	u8 rgb_profile[6] = { drvdata.rgb_effect,
+			      mc_cdev->subled_info[0].intensity,
+			      mc_cdev->subled_info[1].intensity,
+			      mc_cdev->subled_info[2].intensity,
+			      drvdata.led_cdev->brightness,
+			      val };
+
+	ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, index, rgb_profile, 6);
+	if (ret)
+		return ret;
+
+	drvdata.rgb_speed = val;
+
+	return count;
+};
+
+static ssize_t rgb_speed_show(struct device *dev, struct device_attribute *attr,
+			      char *buf)
+{
+	int ret;
+
+	ret = rgb_attr_show();
+	if (ret)
+		return ret;
+
+	if (drvdata.rgb_speed > 100)
+		return -EINVAL;
+
+	return sysfs_emit(buf, "%hhu\n", drvdata.rgb_speed);
+}
+
+static ssize_t rgb_speed_range_show(struct device *dev,
+				    struct device_attribute *attr, char *buf)
+{
+	return sysfs_emit(buf, "0-100\n");
+}
+
+static ssize_t rgb_mode_store(struct device *dev, struct device_attribute *attr,
+			      const char *buf, size_t count)
+{
+	int ret;
+	u8 val;
+
+	ret = sysfs_match_string(rgb_mode_text, buf);
+	if (ret <= 0)
+		return ret;
+
+	val = ret;
+
+	ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, LIGHT_MODE_SEL, &val, 1);
+	if (ret)
+		return ret;
+
+	drvdata.rgb_mode = val;
+
+	return count;
+};
+
+static ssize_t rgb_mode_show(struct device *dev, struct device_attribute *attr,
+			     char *buf)
+{
+	int ret;
+
+	ret = rgb_cfg_call(drvdata.hdev, GET_RGB_CFG, LIGHT_MODE_SEL, 0, 0);
+	if (ret)
+		return ret;
+
+	if (drvdata.rgb_mode >= ARRAY_SIZE(rgb_mode_text))
+		return -EINVAL;
+
+	return sysfs_emit(buf, "%s\n", rgb_mode_text[drvdata.rgb_mode]);
+};
+
+static ssize_t rgb_mode_index_show(struct device *dev,
+				   struct device_attribute *attr, char *buf)
+{
+	ssize_t count = 0;
+	unsigned int i;
+
+	for (i = 1; i < ARRAY_SIZE(rgb_mode_text); i++)
+		count += sysfs_emit_at(buf, count, "%s ", rgb_mode_text[i]);
+
+	if (count)
+		buf[count - 1] = '\n';
+
+	return count;
+}
+
+static ssize_t rgb_profile_store(struct device *dev,
+				 struct device_attribute *attr, const char *buf,
+				 size_t count)
+{
+	size_t size = 1;
+	int ret;
+	u8 val;
+
+	ret = kstrtou8(buf, 10, &val);
+	if (ret < 0)
+		return ret;
+
+	if (val < 1 || val > 3)
+		return -EINVAL;
+
+	ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, LIGHT_PROFILE_SEL, &val,
+			   size);
+	if (ret)
+		return ret;
+
+	drvdata.rgb_profile = val;
+
+	return count;
+};
+
+static ssize_t rgb_profile_show(struct device *dev,
+				struct device_attribute *attr, char *buf)
+{
+	int ret;
+
+	ret = rgb_cfg_call(drvdata.hdev, GET_RGB_CFG, LIGHT_PROFILE_SEL, 0,
+			   0);
+	if (ret)
+		return ret;
+
+	if (drvdata.rgb_profile < 1 || drvdata.rgb_profile > 3)
+		return -EINVAL;
+
+	return sysfs_emit(buf, "%hhu\n", drvdata.rgb_profile);
+};
+
+static ssize_t rgb_profile_range_show(struct device *dev,
+				      struct device_attribute *attr, char *buf)
+{
+	return sysfs_emit(buf, "1-3\n");
+}
+
+static void hid_go_brightness_set(struct led_classdev *led_cdev,
+				  enum led_brightness brightness)
+{
+	struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(drvdata.led_cdev);
+	enum rgb_config_index index;
+	int ret;
+
+	if (brightness > led_cdev->max_brightness) {
+		dev_err(led_cdev->dev, "Invalid argument\n");
+		return;
+	}
+
+	index = drvdata.rgb_profile + 3;
+	u8 rgb_profile[6] = { drvdata.rgb_effect,
+			      mc_cdev->subled_info[0].intensity,
+			      mc_cdev->subled_info[1].intensity,
+			      mc_cdev->subled_info[2].intensity,
+			      brightness,
+			      drvdata.rgb_speed };
+
+	ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, index, rgb_profile, 6);
+	switch (ret) {
+	case 0:
+		led_cdev->brightness = brightness;
+		break;
+	case -ENODEV: /* during switch to IAP -ENODEV is expected */
+	case -ENOSYS: /* during rmmod -ENOSYS is expected */
+		dev_dbg(led_cdev->dev, "Failed to write RGB profile: %i\n", ret);
+		break;
+	default:
+		dev_err(led_cdev->dev, "Failed to write RGB profile: %i\n", ret);
+	};
+}
+
 #define LEGO_DEVICE_ATTR_RW(_name, _attrname, _dtype, _rtype, _group)         \
 	static ssize_t _name##_store(struct device *dev,                      \
 				     struct device_attribute *attr,           \
@@ -1387,6 +1730,109 @@ static const struct attribute_group *top_level_attr_groups[] = {
 	&touchpad_attr_group,	  NULL,
 };
 
+/* RGB */
+struct go_cfg_attr rgb_enabled = { FEATURE_LIGHT_ENABLE };
+
+LEGO_DEVICE_ATTR_RW(rgb_enabled, "enabled", UNSPECIFIED, index, feature_status);
+static DEVICE_ATTR_RO_NAMED(rgb_effect_index, "effect_index");
+static DEVICE_ATTR_RO_NAMED(rgb_enabled_index, "enabled_index");
+static DEVICE_ATTR_RO_NAMED(rgb_mode_index, "mode_index");
+static DEVICE_ATTR_RO_NAMED(rgb_profile_range, "profile_range");
+static DEVICE_ATTR_RO_NAMED(rgb_speed_range, "speed_range");
+static DEVICE_ATTR_RW_NAMED(rgb_effect, "effect");
+static DEVICE_ATTR_RW_NAMED(rgb_mode, "mode");
+static DEVICE_ATTR_RW_NAMED(rgb_profile, "profile");
+static DEVICE_ATTR_RW_NAMED(rgb_speed, "speed");
+
+static struct attribute *go_rgb_attrs[] = {
+	&dev_attr_rgb_effect.attr,
+	&dev_attr_rgb_effect_index.attr,
+	&dev_attr_rgb_enabled.attr,
+	&dev_attr_rgb_enabled_index.attr,
+	&dev_attr_rgb_mode.attr,
+	&dev_attr_rgb_mode_index.attr,
+	&dev_attr_rgb_profile.attr,
+	&dev_attr_rgb_profile_range.attr,
+	&dev_attr_rgb_speed.attr,
+	&dev_attr_rgb_speed_range.attr,
+	NULL,
+};
+
+static struct attribute_group rgb_attr_group = {
+	.attrs = go_rgb_attrs,
+};
+
+struct mc_subled go_rgb_subled_info[] = {
+	{
+		.color_index = LED_COLOR_ID_RED,
+		.brightness = 0x50,
+		.intensity = 0x24,
+		.channel = 0x1,
+	},
+	{
+		.color_index = LED_COLOR_ID_GREEN,
+		.brightness = 0x50,
+		.intensity = 0x22,
+		.channel = 0x2,
+	},
+	{
+		.color_index = LED_COLOR_ID_BLUE,
+		.brightness = 0x50,
+		.intensity = 0x99,
+		.channel = 0x3,
+	},
+};
+
+struct led_classdev_mc go_cdev_rgb = {
+	.led_cdev = {
+		.name = "go:rgb:joystick_rings",
+		.color = LED_COLOR_ID_RGB,
+		.brightness = 0x50,
+		.max_brightness = 0x64,
+		.brightness_set = hid_go_brightness_set,
+	},
+	.num_colors = ARRAY_SIZE(go_rgb_subled_info),
+	.subled_info = go_rgb_subled_info,
+};
+
+static void cfg_setup(struct work_struct *work)
+{
+	int ret;
+
+	/* RGB */
+	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA,
+			       GET_FEATURE_STATUS, FEATURE_LIGHT_ENABLE,
+			       UNSPECIFIED, 0, 0);
+	if (ret < 0) {
+		dev_err(drvdata.led_cdev->dev,
+			"Failed to retrieve RGB enabled: %i\n", ret);
+		return;
+	}
+
+	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_RGB_CFG,
+			       LIGHT_MODE_SEL, UNSPECIFIED, 0, 0);
+	if (ret < 0) {
+		dev_err(drvdata.led_cdev->dev,
+			"Failed to retrieve RGB Mode: %i\n", ret);
+		return;
+	}
+
+	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_RGB_CFG,
+			       LIGHT_PROFILE_SEL, UNSPECIFIED, 0, 0);
+	if (ret < 0) {
+		dev_err(drvdata.led_cdev->dev,
+			"Failed to retrieve RGB Profile: %i\n", ret);
+		return;
+	}
+
+	ret = rgb_attr_show();
+	if (ret < 0) {
+		dev_err(drvdata.led_cdev->dev,
+			"Failed to retrieve RGB Profile Data: %i\n", ret);
+		return;
+	}
+}
+
 static int hid_go_cfg_probe(struct hid_device *hdev,
 			    const struct hid_device_id *_id)
 {
@@ -1409,14 +1855,40 @@ static int hid_go_cfg_probe(struct hid_device *hdev,
 		return ret;
 	}
 
+	ret = devm_led_classdev_multicolor_register(&hdev->dev, &go_cdev_rgb);
+	if (ret) {
+		dev_err_probe(&hdev->dev, ret, "Failed to create RGB device\n");
+		return ret;
+	}
+
+	ret = devm_device_add_group(go_cdev_rgb.led_cdev.dev, &rgb_attr_group);
+	if (ret) {
+		dev_err_probe(&hdev->dev, ret,
+			      "Failed to create RGB configuration attributes\n");
+		return ret;
+	}
+
+	drvdata.led_cdev = &go_cdev_rgb.led_cdev;
+
 	init_completion(&drvdata.send_cmd_complete);
 
+	/* Executing calls prior to returning from probe will lock the MCU. Schedule
+	 * initial data call after probe has completed and MCU can accept calls.
+	 */
+	INIT_DELAYED_WORK(&drvdata.go_cfg_setup, &cfg_setup);
+	ret = schedule_delayed_work(&drvdata.go_cfg_setup, msecs_to_jiffies(2));
+	if (!ret) {
+		dev_err(&hdev->dev,
+			"Failed to schedule startup delayed work\n");
+		return -ENODEV;
+	}
 	return 0;
 }
 
 static void hid_go_cfg_remove(struct hid_device *hdev)
 {
 	guard(mutex)(&drvdata.cfg_mutex);
+	cancel_delayed_work_sync(&drvdata.go_cfg_setup);
 	sysfs_remove_groups(&hdev->dev.kobj, top_level_attr_groups);
 	hid_hw_close(hdev);
 	hid_hw_stop(hdev);
-- 
2.52.0


^ permalink raw reply related

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

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

Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
V3:
 - Remove erroneous renaming of enabled -> enable for some left & right
   handle attributes.
---
 drivers/hid/hid-lenovo-go.c | 312 ++++++++++++++++++++++++++++++++++++
 1 file changed, 312 insertions(+)

diff --git a/drivers/hid/hid-lenovo-go.c b/drivers/hid/hid-lenovo-go.c
index 681791f119d1..fe39a2b38f38 100644
--- a/drivers/hid/hid-lenovo-go.c
+++ b/drivers/hid/hid-lenovo-go.c
@@ -38,6 +38,8 @@ struct hid_go_cfg {
 	struct mutex cfg_mutex; /*ensure single synchronous output report*/
 	u8 fps_mode;
 	u8 gp_left_auto_sleep_time;
+	u8 gp_left_notify_en;
+	u8 gp_left_rumble_mode;
 	u32 gp_left_version_firmware;
 	u8 gp_left_version_gen;
 	u32 gp_left_version_hardware;
@@ -45,11 +47,14 @@ struct hid_go_cfg {
 	u32 gp_left_version_protocol;
 	u8 gp_mode;
 	u8 gp_right_auto_sleep_time;
+	u8 gp_right_notify_en;
+	u8 gp_right_rumble_mode;
 	u32 gp_right_version_firmware;
 	u8 gp_right_version_gen;
 	u32 gp_right_version_hardware;
 	u32 gp_right_version_product;
 	u32 gp_right_version_protocol;
+	u8 gp_rumble_intensity;
 	u8 imu_left_bypass_en;
 	u8 imu_left_sensor_en;
 	u8 imu_right_bypass_en;
@@ -61,6 +66,8 @@ struct hid_go_cfg {
 	u32 mcu_version_protocol;
 	u8 rgb_en;
 	u8 tp_en;
+	u8 tp_vibration_en;
+	u8 tp_vibration_intensity;
 	u32 tx_dongle_version_firmware;
 	u8 tx_dongle_version_gen;
 	u32 tx_dongle_version_hardware;
@@ -167,6 +174,49 @@ static const char *const gamepad_mode_text[] = {
 	[DINPUT] = "dinput",
 };
 
+enum motor_cfg_index {
+	MOTOR_CFG_ALL = 0x01,
+	MOTOR_INTENSITY,
+	VIBRATION_NOTIFY_ENABLE,
+	RUMBLE_MODE,
+	TP_VIBRATION_ENABLE,
+	TP_VIBRATION_INTENSITY,
+};
+
+enum intensity_index {
+	INTENSITY_UNKNOWN,
+	INTENSITY_OFF,
+	INTENSITY_LOW,
+	INTENSITY_MEDIUM,
+	INTENSITY_HIGH,
+};
+
+static const char *const intensity_text[] = {
+	[INTENSITY_UNKNOWN] = "unknown",
+	[INTENSITY_OFF] = "off",
+	[INTENSITY_LOW] = "low",
+	[INTENSITY_MEDIUM] = "medium",
+	[INTENSITY_HIGH] = "high",
+};
+
+enum rumble_mode_index {
+	RUMBLE_MODE_UNKNOWN,
+	RUMBLE_MODE_FPS,
+	RUMBLE_MODE_RACE,
+	RUMBLE_MODE_AVERAGE,
+	RUMBLE_MODE_SPG,
+	RUMBLE_MODE_RPG,
+};
+
+static const char *const rumble_mode_text[] = {
+	[RUMBLE_MODE_UNKNOWN] = "unknown",
+	[RUMBLE_MODE_FPS] = "fps",
+	[RUMBLE_MODE_RACE] = "racing",
+	[RUMBLE_MODE_AVERAGE] = "standard",
+	[RUMBLE_MODE_SPG] = "spg",
+	[RUMBLE_MODE_RPG] = "rpg",
+};
+
 static int hid_go_version_event(struct command_report *cmd_rep)
 {
 	switch (cmd_rep->sub_cmd) {
@@ -333,6 +383,47 @@ static int hid_go_feature_status_event(struct command_report *cmd_rep)
 	}
 }
 
+static int hid_go_motor_event(struct command_report *cmd_rep)
+{
+	switch (cmd_rep->sub_cmd) {
+	case MOTOR_CFG_ALL:
+		return -EINVAL;
+	case MOTOR_INTENSITY:
+		drvdata.gp_rumble_intensity = cmd_rep->data[0];
+		return 0;
+	case VIBRATION_NOTIFY_ENABLE:
+		switch (cmd_rep->device_type) {
+		case LEFT_CONTROLLER:
+			drvdata.gp_left_notify_en = cmd_rep->data[0];
+			return 0;
+		case RIGHT_CONTROLLER:
+			drvdata.gp_right_notify_en = cmd_rep->data[0];
+			return 0;
+		default:
+			return -EINVAL;
+		};
+		break;
+	case RUMBLE_MODE:
+		switch (cmd_rep->device_type) {
+		case LEFT_CONTROLLER:
+			drvdata.gp_left_rumble_mode = cmd_rep->data[0];
+			return 0;
+		case RIGHT_CONTROLLER:
+			drvdata.gp_right_rumble_mode = cmd_rep->data[0];
+			return 0;
+		default:
+			return -EINVAL;
+		};
+	case TP_VIBRATION_ENABLE:
+		drvdata.tp_vibration_en = cmd_rep->data[0];
+		return 0;
+	case TP_VIBRATION_INTENSITY:
+		drvdata.tp_vibration_intensity = cmd_rep->data[0];
+		return 0;
+	}
+	return -EINVAL;
+}
+
 static int hid_go_set_event_return(struct command_report *cmd_rep)
 {
 	if (cmd_rep->data[0] != 0)
@@ -380,7 +471,11 @@ static int hid_go_raw_event(struct hid_device *hdev, struct hid_report *report,
 		case GET_FEATURE_STATUS:
 			ret = hid_go_feature_status_event(cmd_rep);
 			break;
+		case GET_MOTOR_CFG:
+			ret = hid_go_motor_event(cmd_rep);
+			break;
 		case SET_FEATURE_STATUS:
+		case SET_MOTOR_CFG:
 			ret = hid_go_set_event_return(cmd_rep);
 			break;
 		default:
@@ -757,6 +852,168 @@ static ssize_t feature_status_options(struct device *dev,
 	return count;
 }
 
+static ssize_t motor_config_store(struct device *dev,
+				  struct device_attribute *attr,
+				  const char *buf, size_t count,
+				  enum motor_cfg_index index,
+				  enum dev_type device_type)
+{
+	size_t size = 1;
+	u8 val = 0;
+	int ret;
+
+	switch (index) {
+	case MOTOR_CFG_ALL:
+		return -EINVAL;
+	case MOTOR_INTENSITY:
+		ret = sysfs_match_string(intensity_text, buf);
+		val = ret;
+		break;
+	case VIBRATION_NOTIFY_ENABLE:
+		ret = sysfs_match_string(enabled_status_text, buf);
+		val = ret;
+		break;
+	case RUMBLE_MODE:
+		ret = sysfs_match_string(rumble_mode_text, buf);
+		val = ret;
+		break;
+	case TP_VIBRATION_ENABLE:
+		ret = sysfs_match_string(enabled_status_text, buf);
+		val = ret;
+		break;
+	case TP_VIBRATION_INTENSITY:
+		ret = sysfs_match_string(intensity_text, buf);
+		val = ret;
+		break;
+	};
+
+	if (ret < 0)
+		return ret;
+
+	if (!val)
+		size = 0;
+
+	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, SET_MOTOR_CFG,
+			       index, device_type, &val, size);
+	if (ret < 0)
+		return ret;
+
+	return count;
+}
+
+static ssize_t motor_config_show(struct device *dev,
+				 struct device_attribute *attr, char *buf,
+				 enum motor_cfg_index index,
+				 enum dev_type device_type)
+{
+	ssize_t count = 0;
+	int ret;
+	u8 i;
+
+	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_MOTOR_CFG,
+			       index, device_type, 0, 0);
+	if (ret)
+		return ret;
+
+	switch (index) {
+	case MOTOR_CFG_ALL:
+		return -EINVAL;
+	case MOTOR_INTENSITY:
+		i = drvdata.gp_rumble_intensity;
+		if (i >= ARRAY_SIZE(intensity_text))
+			return -EINVAL;
+
+		count = sysfs_emit(buf, "%s\n", intensity_text[i]);
+		break;
+	case VIBRATION_NOTIFY_ENABLE:
+		switch (device_type) {
+		case LEFT_CONTROLLER:
+			i = drvdata.gp_left_notify_en;
+			break;
+		case RIGHT_CONTROLLER:
+			i = drvdata.gp_right_notify_en;
+			break;
+		default:
+			return -EINVAL;
+		};
+		if (i >= ARRAY_SIZE(enabled_status_text))
+			return -EINVAL;
+
+		count = sysfs_emit(buf, "%s\n", enabled_status_text[i]);
+		break;
+	case RUMBLE_MODE:
+		switch (device_type) {
+		case LEFT_CONTROLLER:
+			i = drvdata.gp_left_rumble_mode;
+			break;
+		case RIGHT_CONTROLLER:
+			i = drvdata.gp_right_rumble_mode;
+			break;
+		default:
+			return -EINVAL;
+		};
+		if (i >= ARRAY_SIZE(rumble_mode_text))
+			return -EINVAL;
+
+		count = sysfs_emit(buf, "%s\n", rumble_mode_text[i]);
+		break;
+	case TP_VIBRATION_ENABLE:
+		i = drvdata.tp_vibration_en;
+		if (i >= ARRAY_SIZE(enabled_status_text))
+			return -EINVAL;
+
+		count = sysfs_emit(buf, "%s\n", enabled_status_text[i]);
+		break;
+	case TP_VIBRATION_INTENSITY:
+		i = drvdata.tp_vibration_intensity;
+		if (i >= ARRAY_SIZE(intensity_text))
+			return -EINVAL;
+
+		count = sysfs_emit(buf, "%s\n", intensity_text[i]);
+		break;
+	};
+
+	return count;
+}
+
+static ssize_t motor_config_options(struct device *dev,
+				    struct device_attribute *attr, char *buf,
+				    enum motor_cfg_index index)
+{
+	ssize_t count = 0;
+	unsigned int i;
+
+	switch (index) {
+	case MOTOR_CFG_ALL:
+		break;
+	case RUMBLE_MODE:
+		for (i = 1; i < ARRAY_SIZE(rumble_mode_text); i++) {
+			count += sysfs_emit_at(buf, count, "%s ",
+					       rumble_mode_text[i]);
+		}
+		break;
+	case MOTOR_INTENSITY:
+	case TP_VIBRATION_INTENSITY:
+		for (i = 1; i < ARRAY_SIZE(intensity_text); i++) {
+			count += sysfs_emit_at(buf, count, "%s ",
+					       intensity_text[i]);
+		}
+		break;
+	case VIBRATION_NOTIFY_ENABLE:
+	case TP_VIBRATION_ENABLE:
+		for (i = 1; i < ARRAY_SIZE(enabled_status_text); i++) {
+			count += sysfs_emit_at(buf, count, "%s ",
+					       enabled_status_text[i]);
+		}
+		break;
+	};
+
+	if (count)
+		buf[count - 1] = '\n';
+
+	return count;
+}
+
 #define LEGO_DEVICE_ATTR_RW(_name, _attrname, _dtype, _rtype, _group)         \
 	static ssize_t _name##_store(struct device *dev,                      \
 				     struct device_attribute *attr,           \
@@ -822,10 +1079,18 @@ static DEVICE_ATTR_RO_NAMED(gamepad_mode_index, "mode_index");
 struct go_cfg_attr reset_mcu = { FEATURE_RESET_GAMEPAD };
 LEGO_DEVICE_ATTR_WO(reset_mcu, "reset_mcu", USB_MCU, feature_status);
 
+struct go_cfg_attr gamepad_rumble_intensity = { MOTOR_INTENSITY };
+LEGO_DEVICE_ATTR_RW(gamepad_rumble_intensity, "rumble_intensity", UNSPECIFIED,
+		    index, motor_config);
+static DEVICE_ATTR_RO_NAMED(gamepad_rumble_intensity_index,
+			    "rumble_intensity_index");
+
 static struct attribute *mcu_attrs[] = {
 	&dev_attr_fps_switch_status.attr,
 	&dev_attr_gamepad_mode.attr,
 	&dev_attr_gamepad_mode_index.attr,
+	&dev_attr_gamepad_rumble_intensity.attr,
+	&dev_attr_gamepad_rumble_intensity_index.attr,
 	&dev_attr_reset_mcu.attr,
 	&dev_attr_version_firmware_mcu.attr,
 	&dev_attr_version_gen_mcu.attr,
@@ -908,6 +1173,17 @@ static DEVICE_ATTR_RO_NAMED(imu_enabled_left_index, "imu_enabled_index");
 struct go_cfg_attr reset_left = { FEATURE_RESET_GAMEPAD };
 LEGO_DEVICE_ATTR_WO(reset_left, "reset", LEFT_CONTROLLER, feature_status);
 
+struct go_cfg_attr rumble_mode_left = { RUMBLE_MODE };
+LEGO_DEVICE_ATTR_RW(rumble_mode_left, "rumble_mode", LEFT_CONTROLLER, index,
+		    motor_config);
+static DEVICE_ATTR_RO_NAMED(rumble_mode_left_index, "rumble_mode_index");
+
+struct go_cfg_attr rumble_notification_left = { VIBRATION_NOTIFY_ENABLE };
+LEGO_DEVICE_ATTR_RW(rumble_notification_left, "rumble_notification",
+		    LEFT_CONTROLLER, index, motor_config);
+static DEVICE_ATTR_RO_NAMED(rumble_notification_left_index,
+			    "rumble_notification_index");
+
 static struct attribute *left_gamepad_attrs[] = {
 	&dev_attr_auto_sleep_time_left.attr,
 	&dev_attr_auto_sleep_time_left_range.attr,
@@ -916,6 +1192,10 @@ static struct attribute *left_gamepad_attrs[] = {
 	&dev_attr_imu_enabled_left.attr,
 	&dev_attr_imu_enabled_left_index.attr,
 	&dev_attr_reset_left.attr,
+	&dev_attr_rumble_mode_left.attr,
+	&dev_attr_rumble_mode_left_index.attr,
+	&dev_attr_rumble_notification_left.attr,
+	&dev_attr_rumble_notification_left_index.attr,
 	&dev_attr_version_hardware_left.attr,
 	&dev_attr_version_firmware_left.attr,
 	&dev_attr_version_gen_left.attr,
@@ -964,6 +1244,17 @@ static DEVICE_ATTR_RO_NAMED(imu_enabled_right_index, "imu_enabled_index");
 struct go_cfg_attr reset_right = { FEATURE_RESET_GAMEPAD };
 LEGO_DEVICE_ATTR_WO(reset_right, "reset", LEFT_CONTROLLER, feature_status);
 
+struct go_cfg_attr rumble_mode_right = { RUMBLE_MODE };
+LEGO_DEVICE_ATTR_RW(rumble_mode_right, "rumble_mode", RIGHT_CONTROLLER, index,
+		    motor_config);
+static DEVICE_ATTR_RO_NAMED(rumble_mode_right_index, "rumble_mode_index");
+
+struct go_cfg_attr rumble_notification_right = { VIBRATION_NOTIFY_ENABLE };
+LEGO_DEVICE_ATTR_RW(rumble_notification_right, "rumble_notification",
+		    RIGHT_CONTROLLER, index, motor_config);
+static DEVICE_ATTR_RO_NAMED(rumble_notification_right_index,
+			    "rumble_notification_index");
+
 static struct attribute *right_gamepad_attrs[] = {
 	&dev_attr_auto_sleep_time_right.attr,
 	&dev_attr_auto_sleep_time_right_range.attr,
@@ -972,6 +1263,10 @@ static struct attribute *right_gamepad_attrs[] = {
 	&dev_attr_imu_enabled_right.attr,
 	&dev_attr_imu_enabled_right_index.attr,
 	&dev_attr_reset_right.attr,
+	&dev_attr_rumble_mode_right.attr,
+	&dev_attr_rumble_mode_right_index.attr,
+	&dev_attr_rumble_notification_right.attr,
+	&dev_attr_rumble_notification_right_index.attr,
 	&dev_attr_version_hardware_right.attr,
 	&dev_attr_version_firmware_right.attr,
 	&dev_attr_version_gen_right.attr,
@@ -991,9 +1286,26 @@ LEGO_DEVICE_ATTR_RW(touchpad_enabled, "enabled", UNSPECIFIED, index,
 		    feature_status);
 static DEVICE_ATTR_RO_NAMED(touchpad_enabled_index, "enabled_index");
 
+struct go_cfg_attr touchpad_vibration_enabled = { TP_VIBRATION_ENABLE };
+LEGO_DEVICE_ATTR_RW(touchpad_vibration_enabled, "vibration_enabled", UNSPECIFIED,
+		    index, motor_config);
+static DEVICE_ATTR_RO_NAMED(touchpad_vibration_enabled_index,
+			    "vibration_enabled_index");
+
+struct go_cfg_attr touchpad_vibration_intensity = { TP_VIBRATION_INTENSITY };
+LEGO_DEVICE_ATTR_RW(touchpad_vibration_intensity, "vibration_intensity",
+		    UNSPECIFIED, index, motor_config);
+static DEVICE_ATTR_RO_NAMED(touchpad_vibration_intensity_index,
+			    "vibration_intensity_index");
+
 static struct attribute *touchpad_attrs[] = {
 	&dev_attr_touchpad_enabled.attr,
 	&dev_attr_touchpad_enabled_index.attr,
+	&dev_attr_touchpad_vibration_enabled.attr,
+	&dev_attr_touchpad_vibration_enabled_index.attr,
+	&dev_attr_touchpad_vibration_intensity.attr,
+	&dev_attr_touchpad_vibration_intensity_index.attr,
+	NULL,
 };
 
 static const struct attribute_group touchpad_attr_group = {
-- 
2.52.0


^ permalink raw reply related

* [PATCH v3 05/16] HID: hid-lenovo-go: Add FPS Mode DPI settings
From: Derek J. Clark @ 2026-01-24  1:48 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Mario Limonciello, Zhixin Zhang, Mia Shao, Mark Pearson,
	Pierre-Loup A . Griffais, Derek J . Clark, linux-input, linux-doc,
	linux-kernel
In-Reply-To: <20260124014907.991265-1-derekjohn.clark@gmail.com>

Adds attribute that enables selection of the DPI of the optical sensor
when the right handle toggle is set to FPS mode.

Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
 drivers/hid/hid-lenovo-go.c | 68 +++++++++++++++++++++++++++++++++++++
 1 file changed, 68 insertions(+)

diff --git a/drivers/hid/hid-lenovo-go.c b/drivers/hid/hid-lenovo-go.c
index fe39a2b38f38..5d454cd2cdae 100644
--- a/drivers/hid/hid-lenovo-go.c
+++ b/drivers/hid/hid-lenovo-go.c
@@ -64,6 +64,7 @@ struct hid_go_cfg {
 	u32 mcu_version_hardware;
 	u32 mcu_version_product;
 	u32 mcu_version_protocol;
+	u32 mouse_dpi;
 	u8 rgb_en;
 	u8 tp_en;
 	u8 tp_vibration_en;
@@ -217,6 +218,8 @@ static const char *const rumble_mode_text[] = {
 	[RUMBLE_MODE_RPG] = "rpg",
 };
 
+#define FPS_MODE_DPI           0x02
+
 static int hid_go_version_event(struct command_report *cmd_rep)
 {
 	switch (cmd_rep->sub_cmd) {
@@ -424,6 +427,16 @@ static int hid_go_motor_event(struct command_report *cmd_rep)
 	return -EINVAL;
 }
 
+static int hid_go_fps_dpi_event(struct command_report *cmd_rep)
+{
+	if (cmd_rep->sub_cmd != FPS_MODE_DPI)
+		return -EINVAL;
+
+	drvdata.mouse_dpi = get_unaligned_le32(cmd_rep->data);
+
+	return 0;
+}
+
 static int hid_go_set_event_return(struct command_report *cmd_rep)
 {
 	if (cmd_rep->data[0] != 0)
@@ -474,8 +487,12 @@ static int hid_go_raw_event(struct hid_device *hdev, struct hid_report *report,
 		case GET_MOTOR_CFG:
 			ret = hid_go_motor_event(cmd_rep);
 			break;
+		case GET_DPI_CFG:
+			ret = hid_go_fps_dpi_event(cmd_rep);
+			break;
 		case SET_FEATURE_STATUS:
 		case SET_MOTOR_CFG:
+		case SET_DPI_CFG:
 			ret = hid_go_set_event_return(cmd_rep);
 			break;
 		default:
@@ -1014,6 +1031,52 @@ static ssize_t motor_config_options(struct device *dev,
 	return count;
 }
 
+static ssize_t fps_mode_dpi_store(struct device *dev,
+				  struct device_attribute *attr,
+				  const char *buf, size_t count)
+
+{
+	size_t size = 4;
+	u32 value;
+	u8 val[4];
+	int ret;
+
+	ret = kstrtou32(buf, 10, &value);
+	if (ret)
+		return ret;
+
+	if (value != 500 && value != 800 && value != 1200 && value != 1800)
+		return -EINVAL;
+
+	put_unaligned_le32(value, val);
+
+	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, SET_DPI_CFG,
+			       FPS_MODE_DPI, UNSPECIFIED, val, size);
+	if (ret < 0)
+		return ret;
+
+	return count;
+}
+
+static ssize_t fps_mode_dpi_show(struct device *dev,
+				 struct device_attribute *attr, char *buf)
+{
+	int ret;
+
+	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_DPI_CFG,
+			       FPS_MODE_DPI, UNSPECIFIED, 0, 0);
+	if (ret < 0)
+		return ret;
+
+	return sysfs_emit(buf, "%u\n", drvdata.mouse_dpi);
+}
+
+static ssize_t fps_mode_dpi_index_show(struct device *dev,
+				       struct device_attribute *attr, char *buf)
+{
+	return sysfs_emit(buf, "500 800 1200 1800\n");
+}
+
 #define LEGO_DEVICE_ATTR_RW(_name, _attrname, _dtype, _rtype, _group)         \
 	static ssize_t _name##_store(struct device *dev,                      \
 				     struct device_attribute *attr,           \
@@ -1085,7 +1148,12 @@ LEGO_DEVICE_ATTR_RW(gamepad_rumble_intensity, "rumble_intensity", UNSPECIFIED,
 static DEVICE_ATTR_RO_NAMED(gamepad_rumble_intensity_index,
 			    "rumble_intensity_index");
 
+static DEVICE_ATTR_RW(fps_mode_dpi);
+static DEVICE_ATTR_RO(fps_mode_dpi_index);
+
 static struct attribute *mcu_attrs[] = {
+	&dev_attr_fps_mode_dpi.attr,
+	&dev_attr_fps_mode_dpi_index.attr,
 	&dev_attr_fps_switch_status.attr,
 	&dev_attr_gamepad_mode.attr,
 	&dev_attr_gamepad_mode_index.attr,
-- 
2.52.0


^ permalink raw reply related

* [PATCH v3 03/16] HID: hid-lenovo-go: Add Feature Status Attributes
From: Derek J. Clark @ 2026-01-24  1:48 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Mario Limonciello, Zhixin Zhang, Mia Shao, Mark Pearson,
	Pierre-Loup A . Griffais, Derek J . Clark, linux-input, linux-doc,
	linux-kernel
In-Reply-To: <20260124014907.991265-1-derekjohn.clark@gmail.com>

Adds various feature status indicators and toggles to hid-lenovo-go,
including the FPS mode switch setting, touchpad enable toggle, handle
automatic sleep timer, etc.

Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
 drivers/hid/hid-lenovo-go.c | 396 +++++++++++++++++++++++++++++++++++-
 1 file changed, 395 insertions(+), 1 deletion(-)

diff --git a/drivers/hid/hid-lenovo-go.c b/drivers/hid/hid-lenovo-go.c
index 6380434b2d89..681791f119d1 100644
--- a/drivers/hid/hid-lenovo-go.c
+++ b/drivers/hid/hid-lenovo-go.c
@@ -36,21 +36,31 @@ struct hid_go_cfg {
 	struct completion send_cmd_complete;
 	struct hid_device *hdev;
 	struct mutex cfg_mutex; /*ensure single synchronous output report*/
+	u8 fps_mode;
+	u8 gp_left_auto_sleep_time;
 	u32 gp_left_version_firmware;
 	u8 gp_left_version_gen;
 	u32 gp_left_version_hardware;
 	u32 gp_left_version_product;
 	u32 gp_left_version_protocol;
+	u8 gp_mode;
+	u8 gp_right_auto_sleep_time;
 	u32 gp_right_version_firmware;
 	u8 gp_right_version_gen;
 	u32 gp_right_version_hardware;
 	u32 gp_right_version_product;
 	u32 gp_right_version_protocol;
+	u8 imu_left_bypass_en;
+	u8 imu_left_sensor_en;
+	u8 imu_right_bypass_en;
+	u8 imu_right_sensor_en;
 	u32 mcu_version_firmware;
 	u8 mcu_version_gen;
 	u32 mcu_version_hardware;
 	u32 mcu_version_product;
 	u32 mcu_version_protocol;
+	u8 rgb_en;
+	u8 tp_en;
 	u32 tx_dongle_version_firmware;
 	u8 tx_dongle_version_gen;
 	u32 tx_dongle_version_hardware;
@@ -102,6 +112,18 @@ enum dev_type {
 	RIGHT_CONTROLLER,
 };
 
+enum enabled_status_index {
+	FEATURE_UNKNOWN,
+	FEATURE_ENABLED,
+	FEATURE_DISABLED,
+};
+
+static const char *const enabled_status_text[] = {
+	[FEATURE_UNKNOWN] = "unknown",
+	[FEATURE_ENABLED] = "true",
+	[FEATURE_DISABLED] = "false",
+};
+
 enum version_data_index {
 	PRODUCT_VERSION = 0x02,
 	PROTOCOL_VERSION,
@@ -110,6 +132,41 @@ enum version_data_index {
 	HARDWARE_GENERATION,
 };
 
+enum feature_status_index {
+	FEATURE_RESET_GAMEPAD = 0x02,
+	FEATURE_IMU_BYPASS,
+	FEATURE_IMU_ENABLE = 0x05,
+	FEATURE_TOUCHPAD_ENABLE = 0x07,
+	FEATURE_LIGHT_ENABLE,
+	FEATURE_AUTO_SLEEP_TIME,
+	FEATURE_FPS_SWITCH_STATUS = 0x0b,
+	FEATURE_GAMEPAD_MODE = 0x0e,
+};
+
+enum fps_switch_status_index {
+	FPS_STATUS_UNKNOWN,
+	GAMEPAD,
+	FPS,
+};
+
+static const char *const fps_switch_text[] = {
+	[FPS_STATUS_UNKNOWN] = "unknown",
+	[GAMEPAD] = "gamepad",
+	[FPS] = "fps",
+};
+
+enum gamepad_mode_index {
+	GAMEPAD_MODE_UNKNOWN,
+	XINPUT,
+	DINPUT,
+};
+
+static const char *const gamepad_mode_text[] = {
+	[GAMEPAD_MODE_UNKNOWN] = "unknown",
+	[XINPUT] = "xinput",
+	[DINPUT] = "dinput",
+};
+
 static int hid_go_version_event(struct command_report *cmd_rep)
 {
 	switch (cmd_rep->sub_cmd) {
@@ -219,6 +276,71 @@ static int hid_go_version_event(struct command_report *cmd_rep)
 	}
 }
 
+static int hid_go_feature_status_event(struct command_report *cmd_rep)
+{
+	switch (cmd_rep->sub_cmd) {
+	case FEATURE_RESET_GAMEPAD:
+		return 0;
+	case FEATURE_IMU_ENABLE:
+		switch (cmd_rep->device_type) {
+		case LEFT_CONTROLLER:
+			drvdata.imu_left_sensor_en = cmd_rep->data[0];
+			return 0;
+		case RIGHT_CONTROLLER:
+			drvdata.imu_right_sensor_en = cmd_rep->data[0];
+			return 0;
+		default:
+			return -EINVAL;
+		};
+	case FEATURE_IMU_BYPASS:
+		switch (cmd_rep->device_type) {
+		case LEFT_CONTROLLER:
+			drvdata.imu_left_bypass_en = cmd_rep->data[0];
+			return 0;
+		case RIGHT_CONTROLLER:
+			drvdata.imu_right_bypass_en = cmd_rep->data[0];
+			return 0;
+		default:
+			return -EINVAL;
+		};
+		break;
+	case FEATURE_LIGHT_ENABLE:
+		drvdata.rgb_en = cmd_rep->data[0];
+		return 0;
+	case FEATURE_AUTO_SLEEP_TIME:
+		switch (cmd_rep->device_type) {
+		case LEFT_CONTROLLER:
+			drvdata.gp_left_auto_sleep_time = cmd_rep->data[0];
+			return 0;
+		case RIGHT_CONTROLLER:
+			drvdata.gp_right_auto_sleep_time = cmd_rep->data[0];
+			return 0;
+		default:
+			return -EINVAL;
+		};
+		break;
+	case FEATURE_TOUCHPAD_ENABLE:
+		drvdata.tp_en = cmd_rep->data[0];
+		return 0;
+	case FEATURE_GAMEPAD_MODE:
+		drvdata.gp_mode = cmd_rep->data[0];
+		return 0;
+	case FEATURE_FPS_SWITCH_STATUS:
+		drvdata.fps_mode = cmd_rep->data[0];
+		return 0;
+	default:
+		return -EINVAL;
+	}
+}
+
+static int hid_go_set_event_return(struct command_report *cmd_rep)
+{
+	if (cmd_rep->data[0] != 0)
+		return -EIO;
+
+	return 0;
+}
+
 static int get_endpoint_address(struct hid_device *hdev)
 {
 	struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
@@ -255,6 +377,12 @@ static int hid_go_raw_event(struct hid_device *hdev, struct hid_report *report,
 		case GET_VERSION_DATA:
 			ret = hid_go_version_event(cmd_rep);
 			break;
+		case GET_FEATURE_STATUS:
+			ret = hid_go_feature_status_event(cmd_rep);
+			break;
+		case SET_FEATURE_STATUS:
+			ret = hid_go_set_event_return(cmd_rep);
+			break;
 		default:
 			ret = -EINVAL;
 			break;
@@ -440,6 +568,195 @@ static ssize_t version_show(struct device *dev, struct device_attribute *attr,
 	return count;
 }
 
+static ssize_t feature_status_store(struct device *dev,
+				    struct device_attribute *attr,
+				    const char *buf, size_t count,
+				    enum feature_status_index index,
+				    enum dev_type device_type)
+{
+	size_t size = 1;
+	u8 val = 0;
+	int ret;
+
+	switch (index) {
+	case FEATURE_IMU_ENABLE:
+	case FEATURE_IMU_BYPASS:
+	case FEATURE_LIGHT_ENABLE:
+	case FEATURE_TOUCHPAD_ENABLE:
+		ret = sysfs_match_string(enabled_status_text, buf);
+		val = ret;
+		break;
+	case FEATURE_AUTO_SLEEP_TIME:
+		ret = kstrtou8(buf, 10, &val);
+		break;
+	case FEATURE_RESET_GAMEPAD:
+		ret = kstrtou8(buf, 10, &val);
+		if (val != GO_GP_RESET_SUCCESS)
+			return -EINVAL;
+		break;
+	case FEATURE_FPS_SWITCH_STATUS:
+		ret = sysfs_match_string(fps_switch_text, buf);
+		val = ret;
+		break;
+	case FEATURE_GAMEPAD_MODE:
+		ret = sysfs_match_string(gamepad_mode_text, buf);
+		val = ret;
+		break;
+	default:
+		return -EINVAL;
+	};
+
+	if (ret < 0)
+		return ret;
+
+	if (!val)
+		size = 0;
+
+	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA,
+			       SET_FEATURE_STATUS, index, device_type, &val,
+			       size);
+	if (ret < 0)
+		return ret;
+
+	return count;
+}
+
+static ssize_t feature_status_show(struct device *dev,
+				   struct device_attribute *attr, char *buf,
+				   enum feature_status_index index,
+				   enum dev_type device_type)
+{
+	ssize_t count = 0;
+	int ret;
+	u8 i;
+
+	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA,
+			       GET_FEATURE_STATUS, index, device_type, 0, 0);
+	if (ret)
+		return ret;
+
+	switch (index) {
+	case FEATURE_IMU_ENABLE:
+		switch (device_type) {
+		case LEFT_CONTROLLER:
+			i = drvdata.imu_left_sensor_en;
+			break;
+		case RIGHT_CONTROLLER:
+			i = drvdata.imu_right_sensor_en;
+			break;
+		default:
+			return -EINVAL;
+		}
+		if (i >= ARRAY_SIZE(enabled_status_text))
+			return -EINVAL;
+
+		count = sysfs_emit(buf, "%s\n", enabled_status_text[i]);
+		break;
+	case FEATURE_IMU_BYPASS:
+		switch (device_type) {
+		case LEFT_CONTROLLER:
+			i = drvdata.imu_left_bypass_en;
+			break;
+		case RIGHT_CONTROLLER:
+			i = drvdata.imu_right_bypass_en;
+			break;
+		default:
+			return -EINVAL;
+		}
+		if (i >= ARRAY_SIZE(enabled_status_text))
+			return -EINVAL;
+
+		count = sysfs_emit(buf, "%s\n", enabled_status_text[i]);
+		break;
+	case FEATURE_LIGHT_ENABLE:
+		i = drvdata.rgb_en;
+		if (i >= ARRAY_SIZE(enabled_status_text))
+			return -EINVAL;
+
+		count = sysfs_emit(buf, "%s\n", enabled_status_text[i]);
+		break;
+	case FEATURE_TOUCHPAD_ENABLE:
+		i = drvdata.tp_en;
+		if (i >= ARRAY_SIZE(enabled_status_text))
+			return -EINVAL;
+
+		count = sysfs_emit(buf, "%s\n", enabled_status_text[i]);
+		break;
+	case FEATURE_AUTO_SLEEP_TIME:
+		switch (device_type) {
+		case LEFT_CONTROLLER:
+			i = drvdata.gp_left_auto_sleep_time;
+			break;
+		case RIGHT_CONTROLLER:
+			i = drvdata.gp_right_auto_sleep_time;
+			break;
+		default:
+			return -EINVAL;
+		};
+		count = sysfs_emit(buf, "%u\n", i);
+		break;
+	case FEATURE_FPS_SWITCH_STATUS:
+		i = drvdata.fps_mode;
+		if (i >= ARRAY_SIZE(fps_switch_text))
+			return -EINVAL;
+
+		count = sysfs_emit(buf, "%s\n", fps_switch_text[i]);
+		break;
+	case FEATURE_GAMEPAD_MODE:
+		i = drvdata.gp_mode;
+		if (i >= ARRAY_SIZE(gamepad_mode_text))
+			return -EINVAL;
+
+		count = sysfs_emit(buf, "%s\n", gamepad_mode_text[i]);
+		break;
+	default:
+		return -EINVAL;
+	};
+
+	return count;
+}
+
+static ssize_t feature_status_options(struct device *dev,
+				      struct device_attribute *attr, char *buf,
+				      enum feature_status_index index)
+{
+	ssize_t count = 0;
+	unsigned int i;
+
+	switch (index) {
+	case FEATURE_IMU_ENABLE:
+	case FEATURE_IMU_BYPASS:
+	case FEATURE_LIGHT_ENABLE:
+	case FEATURE_TOUCHPAD_ENABLE:
+		for (i = 1; i < ARRAY_SIZE(enabled_status_text); i++) {
+			count += sysfs_emit_at(buf, count, "%s ",
+					       enabled_status_text[i]);
+		}
+		break;
+	case FEATURE_AUTO_SLEEP_TIME:
+		return sysfs_emit(buf, "0-255\n");
+	case FEATURE_FPS_SWITCH_STATUS:
+		for (i = 1; i < ARRAY_SIZE(fps_switch_text); i++) {
+			count += sysfs_emit_at(buf, count, "%s ",
+					       fps_switch_text[i]);
+		}
+		break;
+	case FEATURE_GAMEPAD_MODE:
+		for (i = 1; i < ARRAY_SIZE(gamepad_mode_text); i++) {
+			count += sysfs_emit_at(buf, count, "%s ",
+					       gamepad_mode_text[i]);
+		}
+		break;
+	default:
+		return -EINVAL;
+	};
+
+	if (count)
+		buf[count - 1] = '\n';
+
+	return count;
+}
+
 #define LEGO_DEVICE_ATTR_RW(_name, _attrname, _dtype, _rtype, _group)         \
 	static ssize_t _name##_store(struct device *dev,                      \
 				     struct device_attribute *attr,           \
@@ -494,7 +811,22 @@ LEGO_DEVICE_ATTR_RO(version_hardware_mcu, "hardware_version", USB_MCU, version);
 struct go_cfg_attr version_gen_mcu = { HARDWARE_GENERATION };
 LEGO_DEVICE_ATTR_RO(version_gen_mcu, "hardware_generation", USB_MCU, version);
 
+struct go_cfg_attr fps_switch_status = { FEATURE_FPS_SWITCH_STATUS };
+LEGO_DEVICE_ATTR_RO(fps_switch_status, "fps_switch_status", UNSPECIFIED,
+		    feature_status);
+
+struct go_cfg_attr gamepad_mode = { FEATURE_GAMEPAD_MODE };
+LEGO_DEVICE_ATTR_RW(gamepad_mode, "mode", UNSPECIFIED, index, feature_status);
+static DEVICE_ATTR_RO_NAMED(gamepad_mode_index, "mode_index");
+
+struct go_cfg_attr reset_mcu = { FEATURE_RESET_GAMEPAD };
+LEGO_DEVICE_ATTR_WO(reset_mcu, "reset_mcu", USB_MCU, feature_status);
+
 static struct attribute *mcu_attrs[] = {
+	&dev_attr_fps_switch_status.attr,
+	&dev_attr_gamepad_mode.attr,
+	&dev_attr_gamepad_mode_index.attr,
+	&dev_attr_reset_mcu.attr,
 	&dev_attr_version_firmware_mcu.attr,
 	&dev_attr_version_gen_mcu.attr,
 	&dev_attr_version_hardware_mcu.attr,
@@ -523,7 +855,11 @@ LEGO_DEVICE_ATTR_RO(version_hardware_tx_dongle, "hardware_version", TX_DONGLE, v
 struct go_cfg_attr version_gen_tx_dongle = { HARDWARE_GENERATION };
 LEGO_DEVICE_ATTR_RO(version_gen_tx_dongle, "hardware_generation", TX_DONGLE, version);
 
+struct go_cfg_attr reset_tx_dongle = { FEATURE_RESET_GAMEPAD };
+LEGO_DEVICE_ATTR_RO(reset_tx_dongle, "reset", TX_DONGLE, feature_status);
+
 static struct attribute *tx_dongle_attrs[] = {
+	&dev_attr_reset_tx_dongle.attr,
 	&dev_attr_version_hardware_tx_dongle.attr,
 	&dev_attr_version_firmware_tx_dongle.attr,
 	&dev_attr_version_gen_tx_dongle.attr,
@@ -553,7 +889,33 @@ LEGO_DEVICE_ATTR_RO(version_hardware_left, "hardware_version", LEFT_CONTROLLER,
 struct go_cfg_attr version_gen_left = { HARDWARE_GENERATION };
 LEGO_DEVICE_ATTR_RO(version_gen_left, "hardware_generation", LEFT_CONTROLLER, version);
 
+struct go_cfg_attr auto_sleep_time_left = { FEATURE_AUTO_SLEEP_TIME };
+LEGO_DEVICE_ATTR_RW(auto_sleep_time_left, "auto_sleep_time", LEFT_CONTROLLER,
+		    range, feature_status);
+static DEVICE_ATTR_RO_NAMED(auto_sleep_time_left_range,
+			    "auto_sleep_time_range");
+
+struct go_cfg_attr imu_bypass_left = { FEATURE_IMU_BYPASS };
+LEGO_DEVICE_ATTR_RW(imu_bypass_left, "imu_bypass_enabled", LEFT_CONTROLLER,
+		    index, feature_status);
+static DEVICE_ATTR_RO_NAMED(imu_bypass_left_index, "imu_bypass_enabled_index");
+
+struct go_cfg_attr imu_enabled_left = { FEATURE_IMU_ENABLE };
+LEGO_DEVICE_ATTR_RW(imu_enabled_left, "imu_enabled", LEFT_CONTROLLER, index,
+		    feature_status);
+static DEVICE_ATTR_RO_NAMED(imu_enabled_left_index, "imu_enabled_index");
+
+struct go_cfg_attr reset_left = { FEATURE_RESET_GAMEPAD };
+LEGO_DEVICE_ATTR_WO(reset_left, "reset", LEFT_CONTROLLER, feature_status);
+
 static struct attribute *left_gamepad_attrs[] = {
+	&dev_attr_auto_sleep_time_left.attr,
+	&dev_attr_auto_sleep_time_left_range.attr,
+	&dev_attr_imu_bypass_left.attr,
+	&dev_attr_imu_bypass_left_index.attr,
+	&dev_attr_imu_enabled_left.attr,
+	&dev_attr_imu_enabled_left_index.attr,
+	&dev_attr_reset_left.attr,
 	&dev_attr_version_hardware_left.attr,
 	&dev_attr_version_firmware_left.attr,
 	&dev_attr_version_gen_left.attr,
@@ -583,7 +945,33 @@ LEGO_DEVICE_ATTR_RO(version_hardware_right, "hardware_version", RIGHT_CONTROLLER
 struct go_cfg_attr version_gen_right = { HARDWARE_GENERATION };
 LEGO_DEVICE_ATTR_RO(version_gen_right, "hardware_generation", RIGHT_CONTROLLER, version);
 
+struct go_cfg_attr auto_sleep_time_right = { FEATURE_AUTO_SLEEP_TIME };
+LEGO_DEVICE_ATTR_RW(auto_sleep_time_right, "auto_sleep_time", RIGHT_CONTROLLER,
+		    range, feature_status);
+static DEVICE_ATTR_RO_NAMED(auto_sleep_time_right_range,
+			    "auto_sleep_time_range");
+
+struct go_cfg_attr imu_bypass_right = { FEATURE_IMU_BYPASS };
+LEGO_DEVICE_ATTR_RW(imu_bypass_right, "imu_bypass_enabled", RIGHT_CONTROLLER,
+		    index, feature_status);
+static DEVICE_ATTR_RO_NAMED(imu_bypass_right_index, "imu_bypass_enabled_index");
+
+struct go_cfg_attr imu_enabled_right = { FEATURE_IMU_BYPASS };
+LEGO_DEVICE_ATTR_RW(imu_enabled_right, "imu_enabled", RIGHT_CONTROLLER, index,
+		    feature_status);
+static DEVICE_ATTR_RO_NAMED(imu_enabled_right_index, "imu_enabled_index");
+
+struct go_cfg_attr reset_right = { FEATURE_RESET_GAMEPAD };
+LEGO_DEVICE_ATTR_WO(reset_right, "reset", LEFT_CONTROLLER, feature_status);
+
 static struct attribute *right_gamepad_attrs[] = {
+	&dev_attr_auto_sleep_time_right.attr,
+	&dev_attr_auto_sleep_time_right_range.attr,
+	&dev_attr_imu_bypass_right.attr,
+	&dev_attr_imu_bypass_right_index.attr,
+	&dev_attr_imu_enabled_right.attr,
+	&dev_attr_imu_enabled_right_index.attr,
+	&dev_attr_reset_right.attr,
 	&dev_attr_version_hardware_right.attr,
 	&dev_attr_version_firmware_right.attr,
 	&dev_attr_version_gen_right.attr,
@@ -598,8 +986,14 @@ static const struct attribute_group right_gamepad_attr_group = {
 };
 
 /* Touchpad */
+struct go_cfg_attr touchpad_enabled = { FEATURE_TOUCHPAD_ENABLE };
+LEGO_DEVICE_ATTR_RW(touchpad_enabled, "enabled", UNSPECIFIED, index,
+		    feature_status);
+static DEVICE_ATTR_RO_NAMED(touchpad_enabled_index, "enabled_index");
+
 static struct attribute *touchpad_attrs[] = {
-	NULL,
+	&dev_attr_touchpad_enabled.attr,
+	&dev_attr_touchpad_enabled_index.attr,
 };
 
 static const struct attribute_group touchpad_attr_group = {
-- 
2.52.0


^ permalink raw reply related

* [PATCH v3 02/16] HID: hid-lenovo-go: Add Lenovo Legion Go Series HID Driver
From: Derek J. Clark @ 2026-01-24  1:48 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Mario Limonciello, Zhixin Zhang, Mia Shao, Mark Pearson,
	Pierre-Loup A . Griffais, Derek J . Clark, linux-input, linux-doc,
	linux-kernel
In-Reply-To: <20260124014907.991265-1-derekjohn.clark@gmail.com>

Adds initial framework for a new HID driver, hid-lenovo-go, along with
attributes that report the firmware and hardware version for each
component of the HID device, of which there are 4 parts: The MCU, the
transmission dongle, the left "handle" controller half, and the right
"handle" controller half. Each of these devices are provided an attribute
group to contain its device specific attributes. Additionally, the touchpad
device attributes are logically separated from the other components in
another attribute group.

This driver primarily provides access to the configurable settings of the
Lenovo Legion Go and Lenovo Legion Go 2 controllers running the latest
firmware. As previously noted, the Legion Go controllers recently had a
firmware update[1] which switched from the original "SepentiaUSB" protocol
to a brand new protocol for the Go 2, primarily to ensure backwards and
forwards compatibility between the Go and Go 2 devices. As part of that
update the PIDs for the controllers were changed, so there is no risk of
this driver attaching to controller firmware that it doesn't support.

Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>

---
V3:
  - Add hid-lenovo.c and Mark Pearson to LENOVO HID DRIVERS entry in MAINTAINERS
---
 MAINTAINERS                 |   8 +
 drivers/hid/Kconfig         |  12 +
 drivers/hid/Makefile        |   1 +
 drivers/hid/hid-ids.h       |   3 +
 drivers/hid/hid-lenovo-go.c | 734 ++++++++++++++++++++++++++++++++++++
 5 files changed, 758 insertions(+)
 create mode 100644 drivers/hid/hid-lenovo-go.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 9ed6d11a7746..c6396be68ff1 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -14135,6 +14135,14 @@ L:	platform-driver-x86@vger.kernel.org
 S:	Maintained
 F:	drivers/platform/x86/lenovo/wmi-hotkey-utilities.c
 
+LENOVO HID drivers
+M:	Derek J. Clark <derekjohn.clark@gmail.com>
+M:	Mark Pearson <mpearson-lenovo@squebb.ca>
+L:	linux-input@vger.kernel.org
+S:	Maintained
+F:	drivers/hid/hid-lenovo-go.c
+F:	drivers/hid/hid-lenovo.c
+
 LETSKETCH HID TABLET DRIVER
 M:	Hans de Goede <hansg@kernel.org>
 L:	linux-input@vger.kernel.org
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index 04420a713be0..74ac6793b29a 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -621,6 +621,18 @@ config HID_LENOVO
 	- ThinkPad Compact Bluetooth Keyboard with TrackPoint (supports Fn keys)
 	- ThinkPad Compact USB Keyboard with TrackPoint (supports Fn keys)
 
+config HID_LENOVO_GO
+	tristate "HID Driver for Lenovo Legion Go Series Controllers"
+	depends on USB_HID
+	select LEDS_CLASS
+	select LEDS_CLASS_MULTICOLOR
+	help
+	Support for Lenovo Legion Go devices with detachable controllers.
+
+	Say Y here to include configuration interface support for the Lenovo Legion Go
+	and Legion Go 2 Handheld Console Controllers. Say M here to compile this
+	driver as a module. The module will be called hid-lenovo-go.
+
 config HID_LETSKETCH
 	tristate "Letsketch WP9620N tablets"
 	depends on USB_HID
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index 361a7daedeb8..11435bce4e47 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -76,6 +76,7 @@ obj-$(CONFIG_HID_KYE)		+= hid-kye.o
 obj-$(CONFIG_HID_KYSONA)	+= hid-kysona.o
 obj-$(CONFIG_HID_LCPOWER)	+= hid-lcpower.o
 obj-$(CONFIG_HID_LENOVO)	+= hid-lenovo.o
+obj-$(CONFIG_HID_LENOVO_GO)	+= hid-lenovo-go.o
 obj-$(CONFIG_HID_LETSKETCH)	+= hid-letsketch.o
 obj-$(CONFIG_HID_LOGITECH)	+= hid-logitech.o
 obj-$(CONFIG_HID_LOGITECH)	+= hid-lg-g15.o
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index c4589075a5ed..309e15580a38 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -841,7 +841,10 @@
 #define USB_DEVICE_ID_LENOVO_PIXART_USB_MOUSE_602E	0x602e
 #define USB_DEVICE_ID_LENOVO_PIXART_USB_MOUSE_6093	0x6093
 #define USB_DEVICE_ID_LENOVO_LEGION_GO_DUAL_DINPUT	0x6184
+#define USB_DEVICE_ID_LENOVO_LEGION_GO2_XINPUT		0x61eb
+#define USB_DEVICE_ID_LENOVO_LEGION_GO2_DINPUT		0x61ec
 #define USB_DEVICE_ID_LENOVO_LEGION_GO2_DUAL_DINPUT	0x61ed
+#define USB_DEVICE_ID_LENOVO_LEGION_GO2_FPS		0x61ee
 
 #define USB_VENDOR_ID_LETSKETCH		0x6161
 #define USB_DEVICE_ID_WP9620N		0x4d15
diff --git a/drivers/hid/hid-lenovo-go.c b/drivers/hid/hid-lenovo-go.c
new file mode 100644
index 000000000000..6380434b2d89
--- /dev/null
+++ b/drivers/hid/hid-lenovo-go.c
@@ -0,0 +1,734 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ *  HID driver for Lenovo Legion Go series gamepads.
+ *
+ *  Copyright (c) 2025 Valve Corporation
+ */
+
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/array_size.h>
+#include <linux/cleanup.h>
+#include <linux/completion.h>
+#include <linux/delay.h>
+#include <linux/dev_printk.h>
+#include <linux/device.h>
+#include <linux/device/devres.h>
+#include <linux/hid.h>
+#include <linux/jiffies.h>
+#include <linux/kstrtox.h>
+#include <linux/mutex.h>
+#include <linux/printk.h>
+#include <linux/sysfs.h>
+#include <linux/types.h>
+#include <linux/unaligned.h>
+#include <linux/usb.h>
+
+#include "hid-ids.h"
+
+#define GO_GP_INTF_IN		0x83
+#define GO_OUTPUT_REPORT_ID	0x05
+#define GO_GP_RESET_SUCCESS	0x01
+#define GO_PACKET_SIZE		64
+
+struct hid_go_cfg {
+	unsigned char *buf;
+	struct completion send_cmd_complete;
+	struct hid_device *hdev;
+	struct mutex cfg_mutex; /*ensure single synchronous output report*/
+	u32 gp_left_version_firmware;
+	u8 gp_left_version_gen;
+	u32 gp_left_version_hardware;
+	u32 gp_left_version_product;
+	u32 gp_left_version_protocol;
+	u32 gp_right_version_firmware;
+	u8 gp_right_version_gen;
+	u32 gp_right_version_hardware;
+	u32 gp_right_version_product;
+	u32 gp_right_version_protocol;
+	u32 mcu_version_firmware;
+	u8 mcu_version_gen;
+	u32 mcu_version_hardware;
+	u32 mcu_version_product;
+	u32 mcu_version_protocol;
+	u32 tx_dongle_version_firmware;
+	u8 tx_dongle_version_gen;
+	u32 tx_dongle_version_hardware;
+	u32 tx_dongle_version_product;
+	u32 tx_dongle_version_protocol;
+} drvdata;
+
+struct go_cfg_attr {
+	u8 index;
+};
+
+struct command_report {
+	u8 report_id;
+	u8 id;
+	u8 cmd;
+	u8 sub_cmd;
+	u8 device_type;
+	u8 data[59];
+} __packed;
+
+enum command_id {
+	MCU_CONFIG_DATA = 0x00,
+	OS_MODE_DATA = 0x06,
+	GAMEPAD_DATA = 0x3c,
+};
+
+enum mcu_command_index {
+	GET_VERSION_DATA = 0x02,
+	GET_FEATURE_STATUS,
+	SET_FEATURE_STATUS,
+	GET_MOTOR_CFG,
+	SET_MOTOR_CFG,
+	GET_DPI_CFG,
+	SET_DPI_CFG,
+	SET_TRIGGER_CFG = 0x0a,
+	SET_JOYSTICK_CFG = 0x0c,
+	SET_GYRO_CFG = 0x0e,
+	GET_RGB_CFG,
+	SET_RGB_CFG,
+	GET_DEVICE_STATUS = 0xa0,
+
+};
+
+enum dev_type {
+	UNSPECIFIED,
+	USB_MCU,
+	TX_DONGLE,
+	LEFT_CONTROLLER,
+	RIGHT_CONTROLLER,
+};
+
+enum version_data_index {
+	PRODUCT_VERSION = 0x02,
+	PROTOCOL_VERSION,
+	FIRMWARE_VERSION,
+	HARDWARE_VERSION,
+	HARDWARE_GENERATION,
+};
+
+static int hid_go_version_event(struct command_report *cmd_rep)
+{
+	switch (cmd_rep->sub_cmd) {
+	case PRODUCT_VERSION:
+		switch (cmd_rep->device_type) {
+		case USB_MCU:
+			drvdata.mcu_version_product =
+				get_unaligned_le32(cmd_rep->data);
+			return 0;
+		case TX_DONGLE:
+			drvdata.tx_dongle_version_product =
+				get_unaligned_le32(cmd_rep->data);
+			return 0;
+		case LEFT_CONTROLLER:
+			drvdata.gp_left_version_product =
+				get_unaligned_le32(cmd_rep->data);
+			return 0;
+		case RIGHT_CONTROLLER:
+			drvdata.gp_right_version_product =
+				get_unaligned_le32(cmd_rep->data);
+			return 0;
+		default:
+			return -EINVAL;
+		}
+	case PROTOCOL_VERSION:
+		switch (cmd_rep->device_type) {
+		case USB_MCU:
+			drvdata.mcu_version_protocol =
+				get_unaligned_le32(cmd_rep->data);
+			return 0;
+		case TX_DONGLE:
+			drvdata.tx_dongle_version_protocol =
+				get_unaligned_le32(cmd_rep->data);
+			return 0;
+		case LEFT_CONTROLLER:
+			drvdata.gp_left_version_protocol =
+				get_unaligned_le32(cmd_rep->data);
+			return 0;
+		case RIGHT_CONTROLLER:
+			drvdata.gp_right_version_protocol =
+				get_unaligned_le32(cmd_rep->data);
+			return 0;
+		default:
+			return -EINVAL;
+		}
+	case FIRMWARE_VERSION:
+		switch (cmd_rep->device_type) {
+		case USB_MCU:
+			drvdata.mcu_version_firmware =
+				get_unaligned_le32(cmd_rep->data);
+			return 0;
+		case TX_DONGLE:
+			drvdata.tx_dongle_version_firmware =
+				get_unaligned_le32(cmd_rep->data);
+			return 0;
+		case LEFT_CONTROLLER:
+			drvdata.gp_left_version_firmware =
+				get_unaligned_le32(cmd_rep->data);
+			return 0;
+		case RIGHT_CONTROLLER:
+			drvdata.gp_right_version_firmware =
+				get_unaligned_le32(cmd_rep->data);
+			return 0;
+		default:
+			return -EINVAL;
+		}
+	case HARDWARE_VERSION:
+		switch (cmd_rep->device_type) {
+		case USB_MCU:
+			drvdata.mcu_version_hardware =
+				get_unaligned_le32(cmd_rep->data);
+			return 0;
+		case TX_DONGLE:
+			drvdata.tx_dongle_version_hardware =
+				get_unaligned_le32(cmd_rep->data);
+			return 0;
+		case LEFT_CONTROLLER:
+			drvdata.gp_left_version_hardware =
+				get_unaligned_le32(cmd_rep->data);
+			return 0;
+		case RIGHT_CONTROLLER:
+			drvdata.gp_right_version_hardware =
+				get_unaligned_le32(cmd_rep->data);
+			return 0;
+		default:
+			return -EINVAL;
+		}
+	case HARDWARE_GENERATION:
+		switch (cmd_rep->device_type) {
+		case USB_MCU:
+			drvdata.mcu_version_gen = cmd_rep->data[0];
+			return 0;
+		case TX_DONGLE:
+			drvdata.tx_dongle_version_gen = cmd_rep->data[0];
+			return 0;
+		case LEFT_CONTROLLER:
+			drvdata.gp_left_version_gen = cmd_rep->data[0];
+			return 0;
+		case RIGHT_CONTROLLER:
+			drvdata.gp_right_version_gen = cmd_rep->data[0];
+			return 0;
+		default:
+			return -EINVAL;
+		}
+	default:
+		return -EINVAL;
+	}
+}
+
+static int get_endpoint_address(struct hid_device *hdev)
+{
+	struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
+	struct usb_host_endpoint *ep;
+
+	if (!intf)
+		return -ENODEV;
+
+	ep = intf->cur_altsetting->endpoint;
+	if (!ep)
+		return -ENODEV;
+
+	return ep->desc.bEndpointAddress;
+}
+
+static int hid_go_raw_event(struct hid_device *hdev, struct hid_report *report,
+			    u8 *data, int size)
+{
+	struct command_report *cmd_rep;
+	int ep, ret;
+
+	if (size != GO_PACKET_SIZE)
+		goto passthrough;
+
+	ep = get_endpoint_address(hdev);
+	if (ep != GO_GP_INTF_IN)
+		goto passthrough;
+
+	cmd_rep = (struct command_report *)data;
+
+	switch (cmd_rep->id) {
+	case MCU_CONFIG_DATA:
+		switch (cmd_rep->cmd) {
+		case GET_VERSION_DATA:
+			ret = hid_go_version_event(cmd_rep);
+			break;
+		default:
+			ret = -EINVAL;
+			break;
+		};
+		break;
+	default:
+		goto passthrough;
+	};
+	dev_dbg(&hdev->dev, "Rx data as raw input report: [%*ph]\n",
+		GO_PACKET_SIZE, data);
+
+	complete(&drvdata.send_cmd_complete);
+	return ret;
+
+passthrough:
+	/* Forward other HID reports so they generate events */
+	hid_input_report(hdev, HID_INPUT_REPORT, data, size, 1);
+	return 0;
+}
+
+static int mcu_property_out(struct hid_device *hdev, u8 id, u8 command,
+			    u8 index, enum dev_type device, u8 *data, size_t len)
+{
+	u8 header[] = { GO_OUTPUT_REPORT_ID, id, command, index, device };
+	size_t header_size = ARRAY_SIZE(header);
+	size_t total_size = header_size + len;
+	int timeout = 50;
+	int ret;
+
+	guard(mutex)(&drvdata.cfg_mutex);
+	memcpy(drvdata.buf, header, header_size);
+	memcpy(drvdata.buf + header_size, data, len);
+	memset(drvdata.buf + total_size, 0, GO_PACKET_SIZE - total_size);
+
+	dev_dbg(&hdev->dev, "Send data as raw output report: [%*ph]\n",
+		GO_PACKET_SIZE, drvdata.buf);
+
+	ret = hid_hw_output_report(hdev, drvdata.buf, GO_PACKET_SIZE);
+	if (ret < 0)
+		return ret;
+
+	ret = ret == GO_PACKET_SIZE ? 0 : -EINVAL;
+	if (ret)
+		return ret;
+
+	ret = wait_for_completion_interruptible_timeout(&drvdata.send_cmd_complete,
+							msecs_to_jiffies(timeout));
+
+	if (ret == 0) /* timeout occurred */
+		ret = -EBUSY;
+	if (ret > 0) /* timeout/interrupt didn't occur */
+		ret = 0;
+
+	reinit_completion(&drvdata.send_cmd_complete);
+	return ret;
+}
+
+static ssize_t version_show(struct device *dev, struct device_attribute *attr,
+			    char *buf, enum version_data_index index,
+			    enum dev_type device_type)
+{
+	ssize_t count = 0;
+	int ret;
+
+	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+			       index, device_type, 0, 0);
+	if (ret)
+		return ret;
+
+	switch (index) {
+	case PRODUCT_VERSION:
+		switch (device_type) {
+		case USB_MCU:
+			count = sysfs_emit(buf, "%u\n",
+					   drvdata.mcu_version_product);
+			break;
+		case TX_DONGLE:
+			count = sysfs_emit(buf, "%u\n",
+					   drvdata.tx_dongle_version_product);
+			break;
+		case LEFT_CONTROLLER:
+			count = sysfs_emit(buf, "%u\n",
+					   drvdata.gp_left_version_product);
+			break;
+		case RIGHT_CONTROLLER:
+			count = sysfs_emit(buf, "%u\n",
+					   drvdata.gp_right_version_product);
+			break;
+		default:
+			return -EINVAL;
+		}
+		break;
+	case PROTOCOL_VERSION:
+		switch (device_type) {
+		case USB_MCU:
+			count = sysfs_emit(buf, "%u\n",
+					   drvdata.mcu_version_protocol);
+			break;
+		case TX_DONGLE:
+			count = sysfs_emit(buf, "%u\n",
+					   drvdata.tx_dongle_version_protocol);
+			break;
+		case LEFT_CONTROLLER:
+			count = sysfs_emit(buf, "%u\n",
+					   drvdata.gp_left_version_protocol);
+			break;
+		case RIGHT_CONTROLLER:
+			count = sysfs_emit(buf, "%u\n",
+					   drvdata.gp_right_version_protocol);
+			break;
+		default:
+			return -EINVAL;
+		}
+		break;
+	case FIRMWARE_VERSION:
+		switch (device_type) {
+		case USB_MCU:
+			count = sysfs_emit(buf, "%u\n",
+					   drvdata.mcu_version_firmware);
+			break;
+		case TX_DONGLE:
+			count = sysfs_emit(buf, "%u\n",
+					   drvdata.tx_dongle_version_firmware);
+			break;
+		case LEFT_CONTROLLER:
+			count = sysfs_emit(buf, "%u\n",
+					   drvdata.gp_left_version_firmware);
+			break;
+		case RIGHT_CONTROLLER:
+			count = sysfs_emit(buf, "%u\n",
+					   drvdata.gp_right_version_firmware);
+			break;
+		default:
+			return -EINVAL;
+		}
+		break;
+	case HARDWARE_VERSION:
+		switch (device_type) {
+		case USB_MCU:
+			count = sysfs_emit(buf, "%u\n",
+					   drvdata.mcu_version_hardware);
+			break;
+		case TX_DONGLE:
+			count = sysfs_emit(buf, "%u\n",
+					   drvdata.tx_dongle_version_hardware);
+			break;
+		case LEFT_CONTROLLER:
+			count = sysfs_emit(buf, "%u\n",
+					   drvdata.gp_left_version_hardware);
+			break;
+		case RIGHT_CONTROLLER:
+			count = sysfs_emit(buf, "%u\n",
+					   drvdata.gp_right_version_hardware);
+			break;
+		default:
+			return -EINVAL;
+		}
+		break;
+	case HARDWARE_GENERATION:
+		switch (device_type) {
+		case USB_MCU:
+			count = sysfs_emit(buf, "%u\n",
+					   drvdata.mcu_version_gen);
+			break;
+		case TX_DONGLE:
+			count = sysfs_emit(buf, "%u\n",
+					   drvdata.tx_dongle_version_gen);
+			break;
+		case LEFT_CONTROLLER:
+			count = sysfs_emit(buf, "%u\n",
+					   drvdata.gp_left_version_gen);
+			break;
+		case RIGHT_CONTROLLER:
+			count = sysfs_emit(buf, "%u\n",
+					   drvdata.gp_right_version_gen);
+			break;
+		default:
+			return -EINVAL;
+		}
+		break;
+	}
+
+	return count;
+}
+
+#define LEGO_DEVICE_ATTR_RW(_name, _attrname, _dtype, _rtype, _group)         \
+	static ssize_t _name##_store(struct device *dev,                      \
+				     struct device_attribute *attr,           \
+				     const char *buf, size_t count)           \
+	{                                                                     \
+		return _group##_store(dev, attr, buf, count, _name.index,     \
+				      _dtype);                                \
+	}                                                                     \
+	static ssize_t _name##_show(struct device *dev,                       \
+				    struct device_attribute *attr, char *buf) \
+	{                                                                     \
+		return _group##_show(dev, attr, buf, _name.index, _dtype);    \
+	}                                                                     \
+	static ssize_t _name##_##_rtype##_show(                               \
+		struct device *dev, struct device_attribute *attr, char *buf) \
+	{                                                                     \
+		return _group##_options(dev, attr, buf, _name.index);         \
+	}                                                                     \
+	static DEVICE_ATTR_RW_NAMED(_name, _attrname)
+
+#define LEGO_DEVICE_ATTR_WO(_name, _attrname, _dtype, _group)             \
+	static ssize_t _name##_store(struct device *dev,                  \
+				     struct device_attribute *attr,       \
+				     const char *buf, size_t count)       \
+	{                                                                 \
+		return _group##_store(dev, attr, buf, count, _name.index, \
+				      _dtype);                            \
+	}                                                                 \
+	static DEVICE_ATTR_WO_NAMED(_name, _attrname)
+
+#define LEGO_DEVICE_ATTR_RO(_name, _attrname, _dtype, _group)                 \
+	static ssize_t _name##_show(struct device *dev,                       \
+				    struct device_attribute *attr, char *buf) \
+	{                                                                     \
+		return _group##_show(dev, attr, buf, _name.index, _dtype);    \
+	}                                                                     \
+	static DEVICE_ATTR_RO_NAMED(_name, _attrname)
+
+/* Gamepad - MCU */
+struct go_cfg_attr version_product_mcu = { PRODUCT_VERSION };
+LEGO_DEVICE_ATTR_RO(version_product_mcu, "product_version", USB_MCU, version);
+
+struct go_cfg_attr version_protocol_mcu = { PROTOCOL_VERSION };
+LEGO_DEVICE_ATTR_RO(version_protocol_mcu, "protocol_version", USB_MCU, version);
+
+struct go_cfg_attr version_firmware_mcu = { FIRMWARE_VERSION };
+LEGO_DEVICE_ATTR_RO(version_firmware_mcu, "firmware_version", USB_MCU, version);
+
+struct go_cfg_attr version_hardware_mcu = { HARDWARE_VERSION };
+LEGO_DEVICE_ATTR_RO(version_hardware_mcu, "hardware_version", USB_MCU, version);
+
+struct go_cfg_attr version_gen_mcu = { HARDWARE_GENERATION };
+LEGO_DEVICE_ATTR_RO(version_gen_mcu, "hardware_generation", USB_MCU, version);
+
+static struct attribute *mcu_attrs[] = {
+	&dev_attr_version_firmware_mcu.attr,
+	&dev_attr_version_gen_mcu.attr,
+	&dev_attr_version_hardware_mcu.attr,
+	&dev_attr_version_product_mcu.attr,
+	&dev_attr_version_protocol_mcu.attr,
+	NULL,
+};
+
+static const struct attribute_group mcu_attr_group = {
+	.attrs = mcu_attrs,
+};
+
+/* Gamepad - TX Dongle */
+struct go_cfg_attr version_product_tx_dongle = { PRODUCT_VERSION };
+LEGO_DEVICE_ATTR_RO(version_product_tx_dongle, "product_version", TX_DONGLE, version);
+
+struct go_cfg_attr version_protocol_tx_dongle = { PROTOCOL_VERSION };
+LEGO_DEVICE_ATTR_RO(version_protocol_tx_dongle, "protocol_version", TX_DONGLE, version);
+
+struct go_cfg_attr version_firmware_tx_dongle = { FIRMWARE_VERSION };
+LEGO_DEVICE_ATTR_RO(version_firmware_tx_dongle, "firmware_version", TX_DONGLE, version);
+
+struct go_cfg_attr version_hardware_tx_dongle = { HARDWARE_VERSION };
+LEGO_DEVICE_ATTR_RO(version_hardware_tx_dongle, "hardware_version", TX_DONGLE, version);
+
+struct go_cfg_attr version_gen_tx_dongle = { HARDWARE_GENERATION };
+LEGO_DEVICE_ATTR_RO(version_gen_tx_dongle, "hardware_generation", TX_DONGLE, version);
+
+static struct attribute *tx_dongle_attrs[] = {
+	&dev_attr_version_hardware_tx_dongle.attr,
+	&dev_attr_version_firmware_tx_dongle.attr,
+	&dev_attr_version_gen_tx_dongle.attr,
+	&dev_attr_version_product_tx_dongle.attr,
+	&dev_attr_version_protocol_tx_dongle.attr,
+	NULL,
+};
+
+static const struct attribute_group tx_dongle_attr_group = {
+	.name = "tx_dongle",
+	.attrs = tx_dongle_attrs,
+};
+
+/* Gamepad - Left */
+struct go_cfg_attr version_product_left = { PRODUCT_VERSION };
+LEGO_DEVICE_ATTR_RO(version_product_left, "product_version", LEFT_CONTROLLER, version);
+
+struct go_cfg_attr version_protocol_left = { PROTOCOL_VERSION };
+LEGO_DEVICE_ATTR_RO(version_protocol_left, "protocol_version", LEFT_CONTROLLER, version);
+
+struct go_cfg_attr version_firmware_left = { FIRMWARE_VERSION };
+LEGO_DEVICE_ATTR_RO(version_firmware_left, "firmware_version", LEFT_CONTROLLER, version);
+
+struct go_cfg_attr version_hardware_left = { HARDWARE_VERSION };
+LEGO_DEVICE_ATTR_RO(version_hardware_left, "hardware_version", LEFT_CONTROLLER, version);
+
+struct go_cfg_attr version_gen_left = { HARDWARE_GENERATION };
+LEGO_DEVICE_ATTR_RO(version_gen_left, "hardware_generation", LEFT_CONTROLLER, version);
+
+static struct attribute *left_gamepad_attrs[] = {
+	&dev_attr_version_hardware_left.attr,
+	&dev_attr_version_firmware_left.attr,
+	&dev_attr_version_gen_left.attr,
+	&dev_attr_version_product_left.attr,
+	&dev_attr_version_protocol_left.attr,
+	NULL,
+};
+
+static const struct attribute_group left_gamepad_attr_group = {
+	.name = "left_handle",
+	.attrs = left_gamepad_attrs,
+};
+
+/* Gamepad - Right */
+struct go_cfg_attr version_product_right = { PRODUCT_VERSION };
+LEGO_DEVICE_ATTR_RO(version_product_right, "product_version", RIGHT_CONTROLLER, version);
+
+struct go_cfg_attr version_protocol_right = { PROTOCOL_VERSION };
+LEGO_DEVICE_ATTR_RO(version_protocol_right, "protocol_version", RIGHT_CONTROLLER, version);
+
+struct go_cfg_attr version_firmware_right = { FIRMWARE_VERSION };
+LEGO_DEVICE_ATTR_RO(version_firmware_right, "firmware_version", RIGHT_CONTROLLER, version);
+
+struct go_cfg_attr version_hardware_right = { HARDWARE_VERSION };
+LEGO_DEVICE_ATTR_RO(version_hardware_right, "hardware_version", RIGHT_CONTROLLER, version);
+
+struct go_cfg_attr version_gen_right = { HARDWARE_GENERATION };
+LEGO_DEVICE_ATTR_RO(version_gen_right, "hardware_generation", RIGHT_CONTROLLER, version);
+
+static struct attribute *right_gamepad_attrs[] = {
+	&dev_attr_version_hardware_right.attr,
+	&dev_attr_version_firmware_right.attr,
+	&dev_attr_version_gen_right.attr,
+	&dev_attr_version_product_right.attr,
+	&dev_attr_version_protocol_right.attr,
+	NULL,
+};
+
+static const struct attribute_group right_gamepad_attr_group = {
+	.name = "right_handle",
+	.attrs = right_gamepad_attrs,
+};
+
+/* Touchpad */
+static struct attribute *touchpad_attrs[] = {
+	NULL,
+};
+
+static const struct attribute_group touchpad_attr_group = {
+	.name = "touchpad",
+	.attrs = touchpad_attrs,
+};
+
+static const struct attribute_group *top_level_attr_groups[] = {
+	&mcu_attr_group,	  &tx_dongle_attr_group,
+	&left_gamepad_attr_group, &right_gamepad_attr_group,
+	&touchpad_attr_group,	  NULL,
+};
+
+static int hid_go_cfg_probe(struct hid_device *hdev,
+			    const struct hid_device_id *_id)
+{
+	unsigned char *buf;
+	int ret;
+
+	buf = devm_kzalloc(&hdev->dev, GO_PACKET_SIZE, GFP_KERNEL);
+	if (!buf)
+		return -ENOMEM;
+
+	hid_set_drvdata(hdev, &drvdata);
+	drvdata.buf = buf;
+	drvdata.hdev = hdev;
+	mutex_init(&drvdata.cfg_mutex);
+
+	ret = sysfs_create_groups(&hdev->dev.kobj, top_level_attr_groups);
+	if (ret) {
+		dev_err_probe(&hdev->dev, ret,
+			      "Failed to create gamepad configuration attributes\n");
+		return ret;
+	}
+
+	init_completion(&drvdata.send_cmd_complete);
+
+	return 0;
+}
+
+static void hid_go_cfg_remove(struct hid_device *hdev)
+{
+	guard(mutex)(&drvdata.cfg_mutex);
+	sysfs_remove_groups(&hdev->dev.kobj, top_level_attr_groups);
+	hid_hw_close(hdev);
+	hid_hw_stop(hdev);
+	hid_set_drvdata(hdev, NULL);
+}
+
+static int hid_go_probe(struct hid_device *hdev, const struct hid_device_id *id)
+{
+	int ret, ep;
+
+	hdev->quirks |= HID_QUIRK_INPUT_PER_APP | HID_QUIRK_MULTI_INPUT;
+
+	ret = hid_parse(hdev);
+	if (ret) {
+		hid_err(hdev, "Parse failed\n");
+		return ret;
+	}
+
+	ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+	if (ret) {
+		hid_err(hdev, "Failed to start HID device\n");
+		return ret;
+	}
+
+	ret = hid_hw_open(hdev);
+	if (ret) {
+		hid_err(hdev, "Failed to open HID device\n");
+		hid_hw_stop(hdev);
+		return ret;
+	}
+
+	ep = get_endpoint_address(hdev);
+	if (ep != GO_GP_INTF_IN) {
+		dev_dbg(&hdev->dev, "Started interface %x as generic HID device\n", ep);
+		return 0;
+	}
+
+	ret = hid_go_cfg_probe(hdev, id);
+	if (ret)
+		dev_err_probe(&hdev->dev, ret, "Failed to start configuration interface\n");
+
+	dev_dbg(&hdev->dev, "Started Legion Go HID Device: %x\n", ep);
+
+	return ret;
+}
+
+static void hid_go_remove(struct hid_device *hdev)
+{
+	int ep = get_endpoint_address(hdev);
+
+	if (ep <= 0)
+		return;
+
+	switch (ep) {
+	case GO_GP_INTF_IN:
+		hid_go_cfg_remove(hdev);
+		break;
+	default:
+		hid_hw_close(hdev);
+		hid_hw_stop(hdev);
+		break;
+	}
+}
+
+static const struct hid_device_id hid_go_devices[] = {
+	{ HID_USB_DEVICE(USB_VENDOR_ID_LENOVO,
+			 USB_DEVICE_ID_LENOVO_LEGION_GO2_XINPUT) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_LENOVO,
+			 USB_DEVICE_ID_LENOVO_LEGION_GO2_DINPUT) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_LENOVO,
+			 USB_DEVICE_ID_LENOVO_LEGION_GO2_DUAL_DINPUT) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_LENOVO,
+			 USB_DEVICE_ID_LENOVO_LEGION_GO2_FPS) },
+	{}
+};
+MODULE_DEVICE_TABLE(hid, hid_go_devices);
+
+static struct hid_driver hid_lenovo_go = {
+	.name = "hid-lenovo-go",
+	.id_table = hid_go_devices,
+	.probe = hid_go_probe,
+	.remove = hid_go_remove,
+	.raw_event = hid_go_raw_event,
+};
+module_hid_driver(hid_lenovo_go);
+
+MODULE_AUTHOR("Derek J. Clark");
+MODULE_DESCRIPTION("HID Driver for Lenovo Legion Go Series Gamepads.");
+MODULE_LICENSE("GPL");
-- 
2.52.0


^ permalink raw reply related

* [PATCH v3 01/16] include: device.h: Add named device attributes
From: Derek J. Clark @ 2026-01-24  1:48 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Mario Limonciello, Zhixin Zhang, Mia Shao, Mark Pearson,
	Pierre-Loup A . Griffais, Derek J . Clark, linux-input, linux-doc,
	linux-kernel
In-Reply-To: <20260124014907.991265-1-derekjohn.clark@gmail.com>

Adds DEVICE_ATTR_[RW|RO|WO]_NAMED macros for adding attributes that
reuse the same sysfs name in a driver under separate subdirectories.

When dealing with some devices it can be useful to be able to reuse
the same name for similar attributes under a different subdirectory.
For example, a single logical HID endpoint may provide a configuration
interface for multiple physical devices. In such a case it is useful to
provide symmetrical attribute names under different subdirectories on
the configuration device. The Lenovo Legion Go is one such device,
providing configuration to a detachable left controller, detachable
right controller, the wireless transmission dongle, and the MCU. It is
therefore beneficial to treat each of these as individual devices in
the driver, providing a subdirectory for each physical device in the
sysfs. As some attributes are reused by each physical device, it
provides a much cleaner interface if the same driver can reuse the same
attribute name in sysfs while uniquely distinguishing the store/show
functions in the driver, rather than repeat string portions.

Example new WO attrs:
ATTRS{left_handle/reset}=="(not readable)"
ATTRS{right_handle/reset}=="(not readable)"
ATTRS{tx_dongle/reset}=="(not readable)"

vs old WO attrs in a subdir:
ATTRS{left_handle/left_handle_reset}=="(not readable)"
ATTRS{right_handle/right_handle_reset}=="(not readable)"
ATTRS{tx_dongle/tx_dongle_reset}=="(not readable)"

or old WO attrs with no subdir:
ATTRS{left_handle_reset}=="(not readable)"
ATTRS{right_handle_reset}=="(not readable)"
ATTRS{tx_dongle_reset}=="(not readable)"

While the third option is usable, it doesn't logically break up the
physical devices and creates a device directory with over 80 attributes
once all attrs are defined.

Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
 include/linux/device.h | 46 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 46 insertions(+)

diff --git a/include/linux/device.h b/include/linux/device.h
index b031ff71a5bd..76f485e6b24c 100644
--- a/include/linux/device.h
+++ b/include/linux/device.h
@@ -189,6 +189,22 @@ ssize_t device_show_string(struct device *dev, struct device_attribute *attr,
 #define DEVICE_ATTR_ADMIN_RW(_name) \
 	struct device_attribute dev_attr_##_name = __ATTR_RW_MODE(_name, 0600)
 
+/**
+ * DEVICE_ATTR_RW_NAMED - Define a read-write device attribute with a sysfs name
+ * that differs from the function name.
+ * @_name: Attribute function preface
+ * @_attrname: Attribute name as it wil be exposed in the sysfs.
+ *
+ * Like DEVICE_ATTR_RW(), but allows for reusing names under separate paths in
+ * the same driver.
+ */
+#define DEVICE_ATTR_RW_NAMED(_name, _attrname)                            \
+	struct device_attribute dev_attr_##_name = {                      \
+		.attr = { .name = _attrname, .mode = 0644 }, \
+		.show = _name##_show,                                     \
+		.store = _name##_store,                                   \
+	}
+
 /**
  * DEVICE_ATTR_RO - Define a readable device attribute.
  * @_name: Attribute name.
@@ -207,6 +223,21 @@ ssize_t device_show_string(struct device *dev, struct device_attribute *attr,
 #define DEVICE_ATTR_ADMIN_RO(_name) \
 	struct device_attribute dev_attr_##_name = __ATTR_RO_MODE(_name, 0400)
 
+/**
+ * DEVICE_ATTR_RO_NAMED - Define a read-only device attribute with a sysfs name
+ * that differs from the function name.
+ * @_name: Attribute function preface
+ * @_attrname: Attribute name as it wil be exposed in the sysfs.
+ *
+ * Like DEVICE_ATTR_RO(), but allows for reusing names under separate paths in
+ * the same driver.
+ */
+#define DEVICE_ATTR_RO_NAMED(_name, _attrname)                            \
+	struct device_attribute dev_attr_##_name = {                      \
+		.attr = { .name = _attrname, .mode = 0444 }, \
+		.show = _name##_show,                                     \
+	}
+
 /**
  * DEVICE_ATTR_WO - Define an admin-only writable device attribute.
  * @_name: Attribute name.
@@ -216,6 +247,21 @@ ssize_t device_show_string(struct device *dev, struct device_attribute *attr,
 #define DEVICE_ATTR_WO(_name) \
 	struct device_attribute dev_attr_##_name = __ATTR_WO(_name)
 
+/**
+ * DEVICE_ATTR_WO_NAMED - Define a read-only device attribute with a sysfs name
+ * that differs from the function name.
+ * @_name: Attribute function preface
+ * @_attrname: Attribute name as it wil be exposed in the sysfs.
+ *
+ * Like DEVICE_ATTR_WO(), but allows for reusing names under separate paths in
+ * the same driver.
+ */
+#define DEVICE_ATTR_WO_NAMED(_name, _attrname)                            \
+	struct device_attribute dev_attr_##_name = {                      \
+		.attr = { .name = _attrname, .mode = 0200 }, \
+		.store = _name##_store,                                   \
+	}
+
 /**
  * DEVICE_ULONG_ATTR - Define a device attribute backed by an unsigned long.
  * @_name: Attribute name.
-- 
2.52.0


^ permalink raw reply related

* [PATCH v3 00/16] HID: Add Legion Go and Go S Drivers
From: Derek J. Clark @ 2026-01-24  1:48 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Mario Limonciello, Zhixin Zhang, Mia Shao, Mark Pearson,
	Pierre-Loup A . Griffais, Derek J . Clark, linux-input, linux-doc,
	linux-kernel

This series adds configuration driver support for the Legion Go S,
Legion Go, and Legion Go 2 built-in controller HID interfaces. This
allows for configuring hardware specific attributes such as the auso
sleep timeout, rumble intensity, etc. non-configuration reports are
forwarded to the HID subsystem to ensure no loss of functionality in
userspace. Basic gamepad functionality is provided through xpad, while
advanced features are currently only implemented in userspace daemons
such as InputPlumber[1]. I plan to move this functionality into the
kernel in a later patch series.

Three new device.h macros are added that solve a fairly specific
problem. Many of the attributes need to have the same name as other
attributes when they are in separate attribute subdirectories. The
previous version of this series, along with the upcoming his-asus-ally
driver[2] use this macro to simplify the sysfs by removing redundancy.
An upcoming out of tree driver for the Zotac Zone [3] also found this
macro to be useful. This greatly reduces the path length and term
redundancy of file paths in the sysfs, while also allowing for cleaner
subdirectories that are grouped by functionality. Rather than carry the
same macro in four drivers, it seems beneficial to me that we include the
macro with the other device macros.

A new HID uevent property is also added, HID_FIRMWARE_VERSION, so as to
permit fwupd to read the firmware version of the Go S HID interface without
detaching the kernel driver.

Finally, there are some checkpatch warnings that will need to be supressed:
WARNING: ENOSYS means 'invalid syscall nr' and nothing else
1292: FILE: drivers/hid/lenovo-legos-hid/lenovo-legos-hid-config.c:1085:
+       case -ENOSYS: /* during rmmod -ENOSYS is expected */

This error handling case was added as it is experienced in the real world
when the driver is rmmod. The LED subsystem produces this error code in
its legacy code and this is not a new novel use of -ENOSYS, we are simply
catching the case to avoid spurious errors in dmesg when the drivers are
removed.

[1]: https://github.com/ShadowBlip/InputPlumber/tree/main/src/drivers/lego
[2]: https://lore.kernel.org/all/20240806081212.56860-1-luke@ljones.dev/
[3]: https://github.com/flukejones/linux/tree/wip/zotac-zone-6.15/drivers/hid/zotac-zone-hid

Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
Change Log
V3:
  - Fix Documentation formatting by removing extra + characters
  - Fix bugs in hid-lenovo-go-s IMU & TP RO attributes being tied to the
    wrong _show function.
  - Rename enume os_mode_index to os_mode_types_index to fix collision
    with os_mode_index attribute.
  - Remove accidental rename for enabled->enable attributes in patch 4
  - Add SOB for Mario in patch 10 as Co-Developer
V2: https://lore.kernel.org/linux-input/20251229031753.581664-1-derekjohn.clark@gmail.com/
  - Break up adding the Go S driver into feature specific patches
  - Rename Go S driver from lenovo-legos-hid to hid-lenovo-go-s
  - Drop the arbitrary uevent properties patch
  - Add Go serires driver
  - Move DEVICE_ATTR_NAMED macros to device.h
V1: https://lore.kernel.org/linux-input/20250703004943.515919-1-derekjohn.clark@gmail.com/


Derek J. Clark (15):
  include: device.h: Add named device attributes
  HID: hid-lenovo-go: Add Lenovo Legion Go Series HID Driver
  HID: hid-lenovo-go: Add Feature Status Attributes
  HID: hid-lenovo-go: Add Rumble and Haptic Settings
  HID: hid-lenovo-go: Add FPS Mode DPI settings
  HID: hid-lenovo-go: Add RGB LED control interface
  HID: hid-lenovo-go: Add Calibration Settings
  HID: hid-lenovo-go: Add OS Mode Toggle
  HID: hid-lenovo-go-s: Add Lenovo Legion Go S Series HID Driver
  HID: hid-lenovo-go-s: Add MCU ID Attribute
  HID: hid-lenovo-go-s: Add Feature Status Attributes
  HID: hid-lenovo-go-s: Add Touchpad Mode Attributes
  HID: hid-lenovo-go-s: Add RGB LED control interface
  HID: hid-lenovo-go-s: Add IMU and Touchpad RO Attributes
  HID: Add documentation for Lenovo Legion Go drivers

Mario Limonciello (1):
  HID: Include firmware version in the uevent

 .../ABI/testing/sysfs-driver-hid-lenovo-go    |  724 +++++
 .../ABI/testing/sysfs-driver-hid-lenovo-go-s  |  304 +++
 MAINTAINERS                                   |   11 +
 drivers/hid/Kconfig                           |   24 +
 drivers/hid/Makefile                          |    2 +
 drivers/hid/hid-core.c                        |    5 +
 drivers/hid/hid-ids.h                         |    7 +
 drivers/hid/hid-lenovo-go-s.c                 | 1577 +++++++++++
 drivers/hid/hid-lenovo-go.c                   | 2399 +++++++++++++++++
 include/linux/device.h                        |   46 +
 include/linux/hid.h                           |    1 +
 11 files changed, 5100 insertions(+)
 create mode 100644 Documentation/ABI/testing/sysfs-driver-hid-lenovo-go
 create mode 100644 Documentation/ABI/testing/sysfs-driver-hid-lenovo-go-s
 create mode 100644 drivers/hid/hid-lenovo-go-s.c
 create mode 100644 drivers/hid/hid-lenovo-go.c

-- 
2.52.0


^ permalink raw reply

* Re: [PATCH v2 RESEND] selftests: hid: tests: test_wacom_generic: add tests for display devices and opaque devices
From: Erin Skomra @ 2026-01-23 22:54 UTC (permalink / raw)
  To: Jiri Kosina
  Cc: Alex Tran, Benjamin Tissoires, shuah, linux-input,
	linux-kselftest, linux-kernel, Ping Cheng, Jason Gerecke
In-Reply-To: <38r99s88-7pp2-6s6n-2148-34s5rrs4s225@xreary.bet>

Hi all and thanks so much to Alex for the patch.

We tested and reviewed the patch and everything looks good with one suggestion.

Since the properties are mutually exclusive we'd like you to add one
line to each prop test asserting that the opposite property is NOT
set.

Thanks again,
Erin Skomra
Software Engineer II at Wacom


On Sat, Jan 10, 2026 at 1:20 AM Jiri Kosina <jikos@kernel.org> wrote:
>
> On Sat, 20 Dec 2025, Alex Tran wrote:
>
> > Verify Wacom devices set INPUT_PROP_DIRECT on display devices and
> > INPUT_PROP_POINTER on opaque devices. Moved test_prop_pointer into
> > TestOpaqueTablet. Created a DirectTabletTest mixin class for
> > test_prop_direct that can be inherited by display tablet test classes.
> > Used DirectTabletTest for TestDTH2452Tablet case.
> >
> > Signed-off-by: Alex Tran <alex.t.tran@gmail.com>
>
> Let me CC Benjamin and Wacom folks to take a look here as well.
>
> > ---
> > Changes in v2:
> > - Removed the tests from the BaseTest class
> > - Removed disabling tests for certain subclasses
> > - Moved test_prop_pointer under TestOpaqueTablet
> > - Created DirectTabletTest mixin class
> > - Moved test_prop_direct under TestDTH2452Tablet
> >  .../selftests/hid/tests/test_wacom_generic.py | 30 +++++++++++--------
> >  1 file changed, 17 insertions(+), 13 deletions(-)
> >
> > diff --git a/tools/testing/selftests/hid/tests/test_wacom_generic.py b/tools/testing/selftests/hid/tests/test_wacom_generic.py
> > index 2d6d04f0f..9d0b0802d 100644
> > --- a/tools/testing/selftests/hid/tests/test_wacom_generic.py
> > +++ b/tools/testing/selftests/hid/tests/test_wacom_generic.py
> > @@ -598,18 +598,6 @@ class BaseTest:
> >                  if unit_set:
> >                      assert required[usage].contains(field)
> >
> > -        def test_prop_direct(self):
> > -            """
> > -            Todo: Verify that INPUT_PROP_DIRECT is set on display devices.
> > -            """
> > -            pass
> > -
> > -        def test_prop_pointer(self):
> > -            """
> > -            Todo: Verify that INPUT_PROP_POINTER is set on opaque devices.
> > -            """
> > -            pass
> > -
> >
> >  class PenTabletTest(BaseTest.TestTablet):
> >      def assertName(self, uhdev):
> > @@ -677,6 +665,13 @@ class TestOpaqueTablet(PenTabletTest):
> >              uhdev.event(130, 240, pressure=0), [], auto_syn=False, strict=True
> >          )
> >
> > +    def test_prop_pointer(self):
> > +        """
> > +        Verify that INPUT_PROP_POINTER is set on opaque devices.
> > +        """
> > +        evdev = self.uhdev.get_evdev()
> > +        assert libevdev.INPUT_PROP_POINTER in evdev.properties
> > +
> >
> >  class TestOpaqueCTLTablet(TestOpaqueTablet):
> >      def create_device(self):
> > @@ -862,7 +857,16 @@ class TestPTHX60_Pen(TestOpaqueCTLTablet):
> >          )
> >
> >
> > -class TestDTH2452Tablet(test_multitouch.BaseTest.TestMultitouch, TouchTabletTest):
> > +class DirectTabletTest():
> > +    def test_prop_direct(self):
> > +        """
> > +        Verify that INPUT_PROP_DIRECT is set on display devices.
> > +        """
> > +        evdev = self.uhdev.get_evdev()
> > +        assert libevdev.INPUT_PROP_DIRECT in evdev.properties
> > +
> > +
> > +class TestDTH2452Tablet(test_multitouch.BaseTest.TestMultitouch, TouchTabletTest, DirectTabletTest):
> >      ContactIds = namedtuple("ContactIds", "contact_id, tracking_id, slot_num")
> >
> >      def create_device(self):
> > --
> > 2.51.0
> >
>
> --
> Jiri Kosina
> SUSE Labs
>
>

^ permalink raw reply

* [PATCH 2/2] Input: st1232 - expose firmware version via sysfs
From: Michael Tretter @ 2026-01-23 16:28 UTC (permalink / raw)
  To: Dmitry Torokhov; +Cc: linux-input, kernel, Michael Tretter, Khalid Talash
In-Reply-To: <20260123-input-st1232-firmware-version-v1-0-32df7eefdafe@pengutronix.de>

Allow user space programs to read the firmware version and revision of
the touch controller from the sysfs.

Suggested-by: Khalid Talash <ktalash@topcon.com>
Signed-off-by: Michael Tretter <m.tretter@pengutronix.de>
---
 drivers/input/touchscreen/st1232.c | 29 +++++++++++++++++++++++++++++
 1 file changed, 29 insertions(+)

diff --git a/drivers/input/touchscreen/st1232.c b/drivers/input/touchscreen/st1232.c
index 3362ee83b21d..0da7f2cec60d 100644
--- a/drivers/input/touchscreen/st1232.c
+++ b/drivers/input/touchscreen/st1232.c
@@ -69,6 +69,34 @@ struct st1232_ts_data {
 	u32 fw_revision;
 };
 
+static ssize_t fw_version_show(struct device *dev,
+			       struct device_attribute *attr, char *buf)
+{
+	struct i2c_client *client = to_i2c_client(dev);
+	struct st1232_ts_data *st1232_ts = i2c_get_clientdata(client);
+
+	return sysfs_emit(buf, "%u\n", st1232_ts->fw_version);
+}
+
+static ssize_t fw_revision_show(struct device *dev,
+				struct device_attribute *attr, char *buf)
+{
+	struct i2c_client *client = to_i2c_client(dev);
+	struct st1232_ts_data *st1232_ts = i2c_get_clientdata(client);
+
+	return sysfs_emit(buf, "%08x\n", st1232_ts->fw_revision);
+}
+
+static DEVICE_ATTR_RO(fw_version);
+static DEVICE_ATTR_RO(fw_revision);
+
+static struct attribute *st1232_attrs[] = {
+	&dev_attr_fw_version.attr,
+	&dev_attr_fw_revision.attr,
+	NULL,
+};
+ATTRIBUTE_GROUPS(st1232);
+
 static int st1232_ts_read_data(struct st1232_ts_data *ts, u8 reg,
 			       unsigned int n)
 {
@@ -445,6 +473,7 @@ static struct i2c_driver st1232_ts_driver = {
 	.driver = {
 		.name	= ST1232_TS_NAME,
 		.of_match_table = st1232_ts_dt_ids,
+		.dev_groups = st1232_groups,
 		.probe_type	= PROBE_PREFER_ASYNCHRONOUS,
 		.pm	= pm_sleep_ptr(&st1232_ts_pm_ops),
 	},

-- 
2.47.3


^ permalink raw reply related

* [PATCH 1/2] Input: st1232 - read firmware version and revision
From: Michael Tretter @ 2026-01-23 16:28 UTC (permalink / raw)
  To: Dmitry Torokhov; +Cc: linux-input, kernel, Michael Tretter, Khalid Talash
In-Reply-To: <20260123-input-st1232-firmware-version-v1-0-32df7eefdafe@pengutronix.de>

According to the data sheet, the st1332 contains a firmware, which may
be updated. The version and revision of the firmware may be read from
the registers of the device.

Read the firmware version and revision and report it to the log when
probing the device.

Suggested-by: Khalid Talash <ktalash@topcon.com>
Signed-off-by: Michael Tretter <m.tretter@pengutronix.de>
---
 drivers/input/touchscreen/st1232.c | 37 +++++++++++++++++++++++++++++++++++++
 1 file changed, 37 insertions(+)

diff --git a/drivers/input/touchscreen/st1232.c b/drivers/input/touchscreen/st1232.c
index 9b3901eec0a5..3362ee83b21d 100644
--- a/drivers/input/touchscreen/st1232.c
+++ b/drivers/input/touchscreen/st1232.c
@@ -22,11 +22,15 @@
 #include <linux/pm_qos.h>
 #include <linux/slab.h>
 #include <linux/types.h>
+#include <linux/unaligned.h>
 #include <linux/input/touch-overlay.h>
 
 #define ST1232_TS_NAME	"st1232-ts"
 #define ST1633_TS_NAME	"st1633-ts"
 
+#define REG_FIRMWARE_VERSION	0x00
+#define REG_FIRMWARE_REVISION_3	0x0C
+
 #define REG_STATUS		0x01	/* Device Status | Error Code */
 
 #define STATUS_NORMAL		0x00
@@ -61,6 +65,8 @@ struct st1232_ts_data {
 	struct list_head touch_overlay_list;
 	int read_buf_len;
 	u8 *read_buf;
+	u8 fw_version;
+	u32 fw_revision;
 };
 
 static int st1232_ts_read_data(struct st1232_ts_data *ts, u8 reg,
@@ -110,6 +116,26 @@ static int st1232_ts_wait_ready(struct st1232_ts_data *ts)
 	return -ENXIO;
 }
 
+static int st1232_ts_read_fw_version(struct st1232_ts_data *ts,
+				     u8 *fw_version, u32 *fw_revision)
+{
+	int error;
+
+	/* select firmware version register */
+	error = st1232_ts_read_data(ts, REG_FIRMWARE_VERSION, 1);
+	if (error)
+		return error;
+	*fw_version = ts->read_buf[0];
+
+	/* select firmware revision register */
+	error = st1232_ts_read_data(ts, REG_FIRMWARE_REVISION_3, 4);
+	if (error)
+		return error;
+	*fw_revision = get_unaligned_le32(ts->read_buf);
+
+	return 0;
+}
+
 static int st1232_ts_read_resolution(struct st1232_ts_data *ts, u16 *max_x,
 				     u16 *max_y)
 {
@@ -299,6 +325,17 @@ static int st1232_ts_probe(struct i2c_client *client)
 	if (error)
 		return error;
 
+	/* Read firmware version from the chip */
+	error = st1232_ts_read_fw_version(ts,
+					  &ts->fw_version, &ts->fw_revision);
+	if (error) {
+		dev_err(&client->dev,
+			"Failed to read firmware version: %d\n", error);
+		return error;
+	}
+	dev_dbg(&client->dev, "Detected firmware version %u, rev %08x\n",
+		ts->fw_version, ts->fw_revision);
+
 	if (ts->chip_info->have_z)
 		input_set_abs_params(input_dev, ABS_MT_TOUCH_MAJOR, 0,
 				     ts->chip_info->max_area, 0, 0);

-- 
2.47.3


^ permalink raw reply related

* [PATCH 0/2] Input: st1232 - add support for firmware version
From: Michael Tretter @ 2026-01-23 16:28 UTC (permalink / raw)
  To: Dmitry Torokhov; +Cc: linux-input, kernel, Michael Tretter, Khalid Talash

The st1332 runs a firmware and reports the version and revision of the
firmware via the registers of the device.

Read the firmware version and revision from the respective registers,
print it to the log and report it via sysfs.

Signed-off-by: Michael Tretter <m.tretter@pengutronix.de>
---
Michael Tretter (2):
      Input: st1232 - read firmware version and revision
      Input: st1232 - expose firmware version via sysfs

 drivers/input/touchscreen/st1232.c | 66 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 66 insertions(+)
---
base-commit: c072629f05d7bca1148ab17690d7922a31423984
change-id: 20260123-input-st1232-firmware-version-05cdf03d85c4

Best regards,
-- 
Michael Tretter <m.tretter@pengutronix.de>


^ permalink raw reply

* Re: [PATCH v1] dt-bindings: input: Add TouchNetix aXiom touchscreen driver
From: Marco Felsch @ 2026-01-23 14:24 UTC (permalink / raw)
  To: Krzysztof Kozlowski
  Cc: AThomas63, dmitry.torokhov, linux-input, linux-kernel,
	mark.satterthwaite, kamel.bouhara
In-Reply-To: <c6fabbcc-4a18-446c-b97e-944cfd23b87e@kernel.org>

Hi Krzysztof,

On 26-01-23, Krzysztof Kozlowski wrote:
> On 22/01/2026 15:40, AThomas63 wrote:
> > Add extra changes referenced in the previous patch:

...

> > +properties:
> > +  compatible:
> > +    enum:
> > +      - tnx,axiom-i2c
> > +      - tnx,axiom-spi
> 
> No, these are the same. Use only one describing the device.
> 
> No device model? Really?

That beeing said, you already provided your review for a device which is
part of this family here:
 - https://lore.kernel.org/all/20260111-v6-10-topic-touchscreen-axiom-v5-3-f94e0ae266cb@pengutronix.de/

Regards,
  Marco

> 
> > +
> > +  reg:
> > +    maxItems: 1
> > +
> > +  interrupts:
> > +    maxItems: 1
> > +
> > +  axiom,poll-enable:
> 
> There is no such company as axiom. You just said it is tnx.
> > +    type: boolean
> > +    description: Enable aXiom polling mode instead of interrupt-driven 
> > +      reporting.
> 
> Anyway, drop entire property. Not a DT suitable, not hardware.
> 
> > +
> > +  axiom,poll-period:
> > +    $ref: /schemas/types.yaml#/definitions/uint32
> > +    default: 10
> > +    description: Set the polling period in milliseconds.
> 
> Drop entire property.
> 
> > +
> > +  # Required for SPI
> > +  spi-max-frequency: true
> 
> Drop. Not needed.
> 
> > +
> > +  # Common touchscreen properties
> > +  touchscreen-size-x: true
> > +  touchscreen-size-y: true
> > +  touchscreen-inverted-x: true
> > +  touchscreen-inverted-y: true
> > +  touchscreen-swapped-x-y: true
> 
> Drop all.
> 
> > +
> > +required:
> > +  - compatible
> > +  - reg
> > +  - interrupts
> > +
> > +allOf:
> > +  - $ref: touchscreen.yaml#
> 
> Missing ref to spi-periph.
> 
> Look at other bindings instead of inventing your own.
> 
> > +
> > +unevaluatedProperties: false
> > +
> > +examples:
> > +  - |
> > +    #include <dt-bindings/interrupt-controller/irq.h>
> > +
> > +    i2c {
> > +        #address-cells = <1>;
> > +        #size-cells = <0>;
> > +
> > +        touchscreen@66 {
> > +            compatible = "touchnetix,axiom-i2c";
> > +            reg = <0x66>;
> > +            interrupt-parent = <&gpio>;
> > +            interrupts = <24 IRQ_TYPE_LEVEL_LOW>;
> > +            axiom,poll-enable;
> > +            axiom,poll-period = <15>;
> > +        };
> > +    };
> > +
> > +  - |
> > +    #include <dt-bindings/interrupt-controller/irq.h>
> > +
> > +    spi {
> > +        #address-cells = <1>;
> > +        #size-cells = <0>;
> > +
> > +        touchscreen@0 {
> > +            compatible = "touchnetix,axiom-spi";
> > +            reg = <0>;
> > +            spi-max-frequency = <4000000>;
> > +            interrupt-parent = <&gpio>;
> > +            interrupts = <24 IRQ_TYPE_LEVEL_LOW>;
> > +        };
> > +    };
> > diff --git a/Documentation/devicetree/bindings/vendor-prefixes.yaml b/Documentation/devicetree/bindings/vendor-prefixes.yaml
> > index f1d1882009ba..dadfc7036ed7 100644
> > --- a/Documentation/devicetree/bindings/vendor-prefixes.yaml
> > +++ b/Documentation/devicetree/bindings/vendor-prefixes.yaml
> > @@ -1636,6 +1636,8 @@ patternProperties:
> >      description: Trusted Logic Mobility
> >    "^tmt,.*":
> >      description: Tecon Microprocessor Technologies, LLC.
> > +  "^tnx,.*":
> 
> Domain is touchnetix, so that's your prefix.
> 
> > +    description: TouchNetix
> >    "^topeet,.*":
> >      description: Topeet
> >    "^topic,.*":
> 
> 
> Best regards,
> Krzysztof
> 

-- 
#gernperDu 
#CallMeByMyFirstName

Pengutronix e.K.                           |                             |
Steuerwalder Str. 21                       | https://www.pengutronix.de/ |
31137 Hildesheim, Germany                  | Phone: +49-5121-206917-0    |
Amtsgericht Hildesheim, HRA 2686           | Fax:   +49-5121-206917-9    |

^ permalink raw reply

* Re: [PATCH] HID: multitouch: add eGalaxTouch EXC3188 support
From: Jiri Kosina @ 2026-01-23 12:54 UTC (permalink / raw)
  To: Michael Tretter
  Cc: Benjamin Tissoires, linux-input, kernel, Thorsten Schmelzer
In-Reply-To: <20260123-egalax-exc3188-v1-1-3f62d06d554d@pengutronix.de>

On Fri, 23 Jan 2026, Michael Tretter wrote:

> From: Thorsten Schmelzer <tschmelzer@topcon.com>
> 
> Add support for the for the EXC3188 touchscreen from eGalaxy.
> 
> Signed-off-by: Thorsten Schmelzer <tschmelzer@topcon.com>
> Signed-off-by: Michael Tretter <m.tretter@pengutronix.de>

Applied, thanks.

-- 
Jiri Kosina
SUSE Labs


^ permalink raw reply

* Re: [PATCH] HID: elecom: Add support for ELECOM HUGE Plus M-HT1MRBK
From: Jiri Kosina @ 2026-01-23 12:53 UTC (permalink / raw)
  To: David Phillips; +Cc: Benjamin Tissoires, linux-input, linux-kernel
In-Reply-To: <20260123-hid-elecom-huge-plus-fix-v1-1-4bed5e52fb3d@profile.sh>

On Fri, 23 Jan 2026, David Phillips via B4 Relay wrote:

> From: David Phillips <david@profile.sh>
> 
> New model in the ELECOM HUGE trackball line that has 8 buttons but the
> report descriptor specifies only 5. The HUGE Plus supports connecting via
> Bluetooth, 2.4GHz wireless USB dongle, and directly via a USB-C cable.
> Each connection type reports a different device id, 01AA for cable,
> 01AB for USB dongle, and 01AC for Bluetooth.
> 
> This patch adds these device IDs and applies the fixups similar to the
> other ELECOM devices to get all 8 buttons working for all 3 connection
> types.
> 
> For reference, the usbhid-dump output:
> 001:013:001:DESCRIPTOR         1769085639.598405
>  05 01 09 02 A1 01 85 01 09 01 A1 00 05 09 19 01
>  29 05 15 00 25 01 75 01 95 05 81 02 75 03 95 01
>  81 01 05 01 09 30 09 31 16 01 80 26 FF 7F 75 10
>  95 02 81 06 09 38 15 81 25 7F 75 08 95 01 81 06
>  05 0C 0A 38 02 15 81 25 7F 75 08 95 01 81 06 C0
>  C0 05 0C 09 01 A1 01 85 02 15 01 26 8C 02 19 01
>  2A 8C 02 75 10 95 01 81 00 C0 05 01 09 80 A1 01
>  85 03 09 82 09 81 09 83 15 00 25 01 19 01 29 03
>  75 01 95 03 81 02 95 05 81 01 C0 06 01 FF 09 00
>  A1 01 85 08 09 00 15 00 26 FF 00 75 08 95 07 81
>  02 C0 06 02 FF 09 02 A1 01 85 06 09 02 15 00 26
>  FF 00 75 08 95 07 B1 02 C0
> 
> Signed-off-by: David Phillips <david@profile.sh>

Applied, thanks.

-- 
Jiri Kosina
SUSE Labs


^ permalink raw reply

* Re: [PATCH 1/2] linux/interrupt.h: allow "guard" notation to disable and reenable IRQ with valid IRQ check
From: Thomas Gleixner @ 2026-01-23 10:52 UTC (permalink / raw)
  To: Sebastian Andrzej Siewior, Marek Vasut
  Cc: linux-input, Peter Zijlstra (Intel), Cheng-Yang Chou,
	Dmitry Torokhov, Frank Li, Geert Uytterhoeven, Jinjie Ruan,
	Krzysztof Kozlowski, Marc Zyngier, linux-kernel,
	linux-renesas-soc
In-Reply-To: <20260122162206.9wrHkrTZ@linutronix.de>

On Thu, Jan 22 2026 at 17:22, Sebastian Andrzej Siewior wrote:

> On 2026-01-22 00:23:47 [+0100], Marek Vasut wrote:
>> @@ -242,6 +242,21 @@ extern void irq_wake_thread(unsigned int irq, void *dev_id);
>>  DEFINE_LOCK_GUARD_1(disable_irq, int,
>>  		    disable_irq(*_T->lock), enable_irq(*_T->lock))
>>  
>> +static inline void disable_valid_irq(unsigned int irq)
>> +{
>> +	if (irq > 0)
>> +		disable_irq(irq);
>> +}
>
> | $ grep " 0:" /proc/interrupts
> |    0:         43          0          IO-APIC  2-edge      timer
>
> in other words, interrupt 0 is valid.

No. It's not really.

Interrupt number zero is a historic leftover and a mistake which is only
relevant to some oddball archaic architectures like x86 and others which
tried to mimic that.

The general agreement is that interrupt 0 is a legacy oddity and only
supported in very special cases. Everything else treats 0 as invalid.

Thanks,

        tglx


^ permalink raw reply

* Re: [PATCH 1/1] Adding support for aXiom touchscreen controller
From: Krzysztof Kozlowski @ 2026-01-23  9:29 UTC (permalink / raw)
  To: AThomas63, dmitry.torokhov
  Cc: linux-input, linux-kernel, mark.satterthwaite, m.felsch,
	kamel.bouhara
In-Reply-To: <20260122124819.273188-2-andrew.thomas@touchnetix.com>

On 22/01/2026 13:48, AThomas63 wrote:
> Signed-off-by: AThomas63 <andrew.thomas@touchnetix.com>


Please run scripts/checkpatch.pl on the patches and fix reported
warnings. After that, run also 'scripts/checkpatch.pl --strict' on the
patches and (probably) fix more warnings. Some warnings can be ignored,
especially from --strict run, but the code here looks like it needs a
fix. Feel free to get in touch if the warning is not clear.

Running checkpatch is really basic and EVERY kernel guide asks you to do
that. Not doing it is not an excuse for your first patch, becasue when
writing first patch you should follow the first-patch guidelines or
kernel docs closely.


> ---
>  drivers/input/touchscreen/Kconfig      |  30 ++
>  drivers/input/touchscreen/Makefile     |   3 +
>  drivers/input/touchscreen/axiom_core.c | 482 +++++++++++++++++++++++++
>  drivers/input/touchscreen/axiom_core.h | 128 +++++++
>  drivers/input/touchscreen/axiom_i2c.c  | 152 ++++++++
>  drivers/input/touchscreen/axiom_spi.c  | 159 ++++++++
>  6 files changed, 954 insertions(+)
>  create mode 100644 drivers/input/touchscreen/axiom_core.c
>  create mode 100644 drivers/input/touchscreen/axiom_core.h
>  create mode 100644 drivers/input/touchscreen/axiom_i2c.c
>  create mode 100644 drivers/input/touchscreen/axiom_spi.c
> 
> diff --git a/drivers/input/touchscreen/Kconfig b/drivers/input/touchscreen/Kconfig
> index 7d5b72ee07fa..f2b4fb317bdd 100644
> --- a/drivers/input/touchscreen/Kconfig
> +++ b/drivers/input/touchscreen/Kconfig
> @@ -162,6 +162,36 @@ config TOUCHSCREEN_AUO_PIXCIR
>  	  To compile this driver as a module, choose M here: the
>  	  module will be called auo-pixcir-ts.
>  
> +config TOUCHSCREEN_AXIOM_CORE
> +	tristate "TouchNetix Axiom touchscreen"

This should not be user-selectable. It's useless alone. Just "tristate"
and change the depending drivers to select it.

> +	help
> +	  Say Y here if you have an Axiom touchscreen connected
> +	  to your system. You will also need to select appropriate
> +	  bus connection below.
> +
> +	  If unsure, say N.
> +
> +	  To compile this driver as a module, choose M here: the
> +	  module will be called axiom_core.
> +
> +config TOUCHSCREEN_AXIOM_I2C
> +	tristate "support I2C bus connection"
> +	depends on TOUCHSCREEN_AXIOM_CORE && I2C
> +	help
> +	  Say Y here if the touchscreen is connected via I2C bus.
> +
> +	  To compile this driver as a module, choose M here: the
> +	  module will be called axiom_spi.
> +
> +config TOUCHSCREEN_AXIOM_SPI
> +	tristate "support SPI bus connection"
> +	depends on TOUCHSCREEN_AXIOM_CORE && SPI
> +	help
> +	  Say Y here if the touchscreen is connected via SPI bus.
> +
> +	  To compile this driver as a module, choose M here: the
> +	  module will be called axiom_spi.
> +


...

> + *
> + * Author(s): Mark Satterthwaite <mark.satterthwaite@touchnetix.com>
> + *            Bart Prescott <bartp@baasheep.co.uk>
> + *            Hannah Rossiter <hannah.rossiter@touchnetix.com>
> + *            Andrew Thomas <andrew.thomas@touchnetix.com>
> + *
> + * This program is free software; you can redistribute  it and/or modify it
> + * under  the terms of  the GNU General  Public License as published by the
> + * Free Software Foundation;  either version 2 of the  License, or (at your
> + * option) any later version.

No code in the kernel has such license boilerplate. Please use recent
Linux drivers as starting code. It's really pointless to send to
upstream some old vendor code, because you will repeat all the
mistakes/issues/old style which we fixed already over last 15 years.

> + *


...

> +
> +static const struct spi_device_id axiom_spi_id_table[] = {
> +	{ "axiom-spi" },
> +	{},
> +};
> +MODULE_DEVICE_TABLE(spi, axiom_spi_id_table);
> +
> +static const struct of_device_id axiom_spi_dt_ids[] = {
> +	{
> +		.compatible = "tnx,axiom-spi",
> +		.data = "axiom",
> +	},
> +	{}
> +};
> +MODULE_DEVICE_TABLE(of, axiom_spi_dt_ids);
> +
> +static struct spi_driver axiom_spi_driver = {
> +	.id_table = axiom_spi_id_table,
> +	.driver = {
> +		.name = "axiom_spi",
> +		.of_match_table = of_match_ptr(axiom_spi_dt_ids),

Drop of_match_ptr, leads to warnings here.

> +	},
> +	.probe = axiom_spi_probe,
> +};
> +
> +module_spi_driver(axiom_spi_driver);
> +
> +MODULE_AUTHOR("TouchNetix <support@touchnetix.com>");
> +MODULE_DESCRIPTION("aXiom touchscreen SPI bus driver");
> +MODULE_LICENSE("GPL");
> +MODULE_ALIAS("axiom");

Drop alias. You should not need MODULE_ALIAS() in normal cases. If you
need it, usually it means your device ID table is wrong (e.g. misses
either entries or MODULE_DEVICE_TABLE()). MODULE_ALIAS() is not a
substitute for incomplete ID table.

> +MODULE_VERSION("1.0.0");


Best regards,
Krzysztof

^ permalink raw reply

* [PATCH] HID: multitouch: add eGalaxTouch EXC3188 support
From: Michael Tretter @ 2026-01-23  8:57 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, kernel, Thorsten Schmelzer, Michael Tretter

From: Thorsten Schmelzer <tschmelzer@topcon.com>

Add support for the for the EXC3188 touchscreen from eGalaxy.

Signed-off-by: Thorsten Schmelzer <tschmelzer@topcon.com>
Signed-off-by: Michael Tretter <m.tretter@pengutronix.de>
---
 drivers/hid/hid-ids.h        | 1 +
 drivers/hid/hid-multitouch.c | 3 +++
 2 files changed, 4 insertions(+)

diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 9c2bf584d9f6..7dc3fe079491 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -437,6 +437,7 @@
 #define USB_DEVICE_ID_DWAV_EGALAX_MULTITOUCH_7349	0x7349
 #define USB_DEVICE_ID_DWAV_EGALAX_MULTITOUCH_73F7	0x73f7
 #define USB_DEVICE_ID_DWAV_EGALAX_MULTITOUCH_A001	0xa001
+#define USB_DEVICE_ID_DWAV_EGALAX_MULTITOUCH_C000	0xc000
 #define USB_DEVICE_ID_DWAV_EGALAX_MULTITOUCH_C002	0xc002
 
 #define USB_VENDOR_ID_EDIFIER		0x2d99
diff --git a/drivers/hid/hid-multitouch.c b/drivers/hid/hid-multitouch.c
index b1c3ef129058..ea2a41d706de 100644
--- a/drivers/hid/hid-multitouch.c
+++ b/drivers/hid/hid-multitouch.c
@@ -2146,6 +2146,9 @@ static const struct hid_device_id mt_devices[] = {
 	{ .driver_data = MT_CLS_EGALAX_SERIAL,
 		MT_USB_DEVICE(USB_VENDOR_ID_DWAV,
 			USB_DEVICE_ID_DWAV_EGALAX_MULTITOUCH_A001) },
+	{ .driver_data = MT_CLS_EGALAX_SERIAL,
+		MT_USB_DEVICE(USB_VENDOR_ID_DWAV,
+			USB_DEVICE_ID_DWAV_EGALAX_MULTITOUCH_C000) },
 	{ .driver_data = MT_CLS_EGALAX,
 		MT_USB_DEVICE(USB_VENDOR_ID_DWAV,
 			USB_DEVICE_ID_DWAV_EGALAX_MULTITOUCH_C002) },

---
base-commit: c072629f05d7bca1148ab17690d7922a31423984
change-id: 20260123-egalax-exc3188-03a03e768f42

Best regards,
-- 
Michael Tretter <m.tretter@pengutronix.de>


^ permalink raw reply related

* Re: [PATCH v1] dt-bindings: input: Add TouchNetix aXiom touchscreen driver
From: Krzysztof Kozlowski @ 2026-01-23  7:27 UTC (permalink / raw)
  To: AThomas63, dmitry.torokhov
  Cc: linux-input, linux-kernel, mark.satterthwaite, m.felsch,
	kamel.bouhara
In-Reply-To: <20260122144052.358956-1-andrew.thomas@touchnetix.com>

On 22/01/2026 15:40, AThomas63 wrote:
> Add extra changes referenced in the previous patch:
>  - Add device tree documentation for axiom giving examples for SPI and I2C.
>  - Add tnx vendor prefix.

Incomplete DCO chain.

Please run scripts/checkpatch.pl on the patches and fix reported
warnings. After that, run also 'scripts/checkpatch.pl --strict' on the
patches and (probably) fix more warnings. Some warnings can be ignored,
especially from --strict run, but the code here looks like it needs a
fix. Feel free to get in touch if the warning is not clear.

Also, no anonymous contributions. It's also explicitly documented.


Best regards,
Krzysztof

^ permalink raw reply

* Re: [PATCH v1] dt-bindings: input: Add TouchNetix aXiom touchscreen driver
From: Krzysztof Kozlowski @ 2026-01-23  7:27 UTC (permalink / raw)
  To: AThomas63, dmitry.torokhov
  Cc: linux-input, linux-kernel, mark.satterthwaite, m.felsch,
	kamel.bouhara
In-Reply-To: <20260122144052.358956-1-andrew.thomas@touchnetix.com>

On 22/01/2026 15:40, AThomas63 wrote:
> Add extra changes referenced in the previous patch:

Not relevant.


>  - Add device tree documentation for axiom giving examples for SPI and I2C.
>  - Add tnx vendor prefix.

Please start using b4 or use known (described) workflows with git. Look
at the subject - why is this just one patch not 1/2? Patchsets are made
git format-patch -2 (-vX if needed).

And then:
Please use scripts/get_maintainers.pl to get a list of necessary people
and lists to CC. It might happen, that command when run on an older
kernel, gives you outdated entries. Therefore please be sure you base
your patches on recent Linux kernel.

Tools like b4 or scripts/get_maintainer.pl provide you proper list of
people, so fix your workflow. Tools might also fail if you work on some
ancient tree (don't, instead use mainline) or work on fork of kernel
(don't, instead use mainline). Just use b4 and everything should be
fine, although remember about `b4 prep --auto-to-cc` if you added new
patches to the patchset.

You missed at least devicetree list (maybe more), so this won't be
tested by automated tooling. Performing review on untested code might be
a waste of time.

Please kindly resend and include all necessary To/Cc entries.


> 
> ---
>  .../bindings/input/touchscreen/tnx,axiom.yaml | 90 +++++++++++++++++++
>  .../devicetree/bindings/vendor-prefixes.yaml  |  2 +
>  2 files changed, 92 insertions(+)
>  create mode 100644 Documentation/devicetree/bindings/input/touchscreen/tnx,axiom.yaml
> 
> diff --git a/Documentation/devicetree/bindings/input/touchscreen/tnx,axiom.yaml b/Documentation/devicetree/bindings/input/touchscreen/tnx,axiom.yaml
> new file mode 100644
> index 000000000000..dc2ea62999b8
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/input/touchscreen/tnx,axiom.yaml
> @@ -0,0 +1,90 @@
> +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
> +%YAML 1.2
> +---
> +$id: http://devicetree.org/schemas/input/touchscreen/tnx,axiom.yaml#
> +$schema: http://devicetree.org/meta-schemas/core.yaml#
> +
> +title: TouchNetix aXiom Touchscreen Controller
> +
> +maintainers:
> +  - Andrew Thomas <andrew.thomas@touchnetix.com>
> +
> +description: |


> +  The TouchNetix aXiom series are high-performance touchscreen controllers
> +  supporting various interface methods including I2C and SPI.
> +
> +properties:
> +  compatible:
> +    enum:
> +      - tnx,axiom-i2c
> +      - tnx,axiom-spi

No, these are the same. Use only one describing the device.

No device model? Really?

> +
> +  reg:
> +    maxItems: 1
> +
> +  interrupts:
> +    maxItems: 1
> +
> +  axiom,poll-enable:

There is no such company as axiom. You just said it is tnx.
> +    type: boolean
> +    description: Enable aXiom polling mode instead of interrupt-driven 
> +      reporting.

Anyway, drop entire property. Not a DT suitable, not hardware.

> +
> +  axiom,poll-period:
> +    $ref: /schemas/types.yaml#/definitions/uint32
> +    default: 10
> +    description: Set the polling period in milliseconds.

Drop entire property.

> +
> +  # Required for SPI
> +  spi-max-frequency: true

Drop. Not needed.

> +
> +  # Common touchscreen properties
> +  touchscreen-size-x: true
> +  touchscreen-size-y: true
> +  touchscreen-inverted-x: true
> +  touchscreen-inverted-y: true
> +  touchscreen-swapped-x-y: true

Drop all.

> +
> +required:
> +  - compatible
> +  - reg
> +  - interrupts
> +
> +allOf:
> +  - $ref: touchscreen.yaml#

Missing ref to spi-periph.

Look at other bindings instead of inventing your own.

> +
> +unevaluatedProperties: false
> +
> +examples:
> +  - |
> +    #include <dt-bindings/interrupt-controller/irq.h>
> +
> +    i2c {
> +        #address-cells = <1>;
> +        #size-cells = <0>;
> +
> +        touchscreen@66 {
> +            compatible = "touchnetix,axiom-i2c";
> +            reg = <0x66>;
> +            interrupt-parent = <&gpio>;
> +            interrupts = <24 IRQ_TYPE_LEVEL_LOW>;
> +            axiom,poll-enable;
> +            axiom,poll-period = <15>;
> +        };
> +    };
> +
> +  - |
> +    #include <dt-bindings/interrupt-controller/irq.h>
> +
> +    spi {
> +        #address-cells = <1>;
> +        #size-cells = <0>;
> +
> +        touchscreen@0 {
> +            compatible = "touchnetix,axiom-spi";
> +            reg = <0>;
> +            spi-max-frequency = <4000000>;
> +            interrupt-parent = <&gpio>;
> +            interrupts = <24 IRQ_TYPE_LEVEL_LOW>;
> +        };
> +    };
> diff --git a/Documentation/devicetree/bindings/vendor-prefixes.yaml b/Documentation/devicetree/bindings/vendor-prefixes.yaml
> index f1d1882009ba..dadfc7036ed7 100644
> --- a/Documentation/devicetree/bindings/vendor-prefixes.yaml
> +++ b/Documentation/devicetree/bindings/vendor-prefixes.yaml
> @@ -1636,6 +1636,8 @@ patternProperties:
>      description: Trusted Logic Mobility
>    "^tmt,.*":
>      description: Tecon Microprocessor Technologies, LLC.
> +  "^tnx,.*":

Domain is touchnetix, so that's your prefix.

> +    description: TouchNetix
>    "^topeet,.*":
>      description: Topeet
>    "^topic,.*":


Best regards,
Krzysztof

^ permalink raw reply

* [PATCH] HID: elecom: Add support for ELECOM HUGE Plus M-HT1MRBK
From: David Phillips via B4 Relay @ 2026-01-23  3:56 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires; +Cc: linux-input, linux-kernel, David Phillips

From: David Phillips <david@profile.sh>

New model in the ELECOM HUGE trackball line that has 8 buttons but the
report descriptor specifies only 5. The HUGE Plus supports connecting via
Bluetooth, 2.4GHz wireless USB dongle, and directly via a USB-C cable.
Each connection type reports a different device id, 01AA for cable,
01AB for USB dongle, and 01AC for Bluetooth.

This patch adds these device IDs and applies the fixups similar to the
other ELECOM devices to get all 8 buttons working for all 3 connection
types.

For reference, the usbhid-dump output:
001:013:001:DESCRIPTOR         1769085639.598405
 05 01 09 02 A1 01 85 01 09 01 A1 00 05 09 19 01
 29 05 15 00 25 01 75 01 95 05 81 02 75 03 95 01
 81 01 05 01 09 30 09 31 16 01 80 26 FF 7F 75 10
 95 02 81 06 09 38 15 81 25 7F 75 08 95 01 81 06
 05 0C 0A 38 02 15 81 25 7F 75 08 95 01 81 06 C0
 C0 05 0C 09 01 A1 01 85 02 15 01 26 8C 02 19 01
 2A 8C 02 75 10 95 01 81 00 C0 05 01 09 80 A1 01
 85 03 09 82 09 81 09 83 15 00 25 01 19 01 29 03
 75 01 95 03 81 02 95 05 81 01 C0 06 01 FF 09 00
 A1 01 85 08 09 00 15 00 26 FF 00 75 08 95 07 81
 02 C0 06 02 FF 09 02 A1 01 85 06 09 02 15 00 26
 FF 00 75 08 95 07 B1 02 C0

Signed-off-by: David Phillips <david@profile.sh>
---
 drivers/hid/Kconfig      |  1 +
 drivers/hid/hid-elecom.c | 16 ++++++++++++++++
 drivers/hid/hid-ids.h    |  3 +++
 drivers/hid/hid-quirks.c |  3 +++
 4 files changed, 23 insertions(+)

diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index 920a64b66b25..6ff4a3ad34cb 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -369,6 +369,7 @@ config HID_ELECOM
 	  - EX-G Trackballs (M-XT3DRBK, M-XT3URBK)
 	  - DEFT Trackballs (M-DT1DRBK, M-DT1URBK, M-DT2DRBK, M-DT2URBK)
 	  - HUGE Trackballs (M-HT1DRBK, M-HT1URBK)
+	  - HUGE Plus Trackball (M-HT1MRBK)
 
 config HID_ELO
 	tristate "ELO USB 4000/4500 touchscreen"
diff --git a/drivers/hid/hid-elecom.c b/drivers/hid/hid-elecom.c
index 2003d2dcda7c..37d88ce57f67 100644
--- a/drivers/hid/hid-elecom.c
+++ b/drivers/hid/hid-elecom.c
@@ -5,6 +5,7 @@
  *  - EX-G Trackballs (M-XT3DRBK, M-XT3URBK, M-XT4DRBK)
  *  - DEFT Trackballs (M-DT1DRBK, M-DT1URBK, M-DT2DRBK, M-DT2URBK)
  *  - HUGE Trackballs (M-HT1DRBK, M-HT1URBK)
+ *  - HUGE Plus Trackball (M-HT1MRBK)
  *
  *  Copyright (c) 2010 Richard Nauber <Richard.Nauber@gmail.com>
  *  Copyright (c) 2016 Yuxuan Shui <yshuiv7@gmail.com>
@@ -123,12 +124,25 @@ static const __u8 *elecom_report_fixup(struct hid_device *hdev, __u8 *rdesc,
 		 */
 		mouse_button_fixup(hdev, rdesc, *rsize, 22, 30, 24, 16, 8);
 		break;
+	case USB_DEVICE_ID_ELECOM_M_HT1MRBK:
+	case USB_DEVICE_ID_ELECOM_M_HT1MRBK_01AB:
+	case USB_DEVICE_ID_ELECOM_M_HT1MRBK_01AC:
+		/*
+		 * Report descriptor format:
+		 * 24: button bit count
+		 * 28: padding bit count
+		 * 22: button report size
+		 * 16: button usage maximum
+		 */
+		mouse_button_fixup(hdev, rdesc, *rsize, 24, 28, 22, 16, 8);
+		break;
 	}
 	return rdesc;
 }
 
 static const struct hid_device_id elecom_devices[] = {
 	{ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_BM084) },
+	{ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_HT1MRBK_01AC) },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_XGL20DLBK) },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_XT3URBK_00FB) },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_XT3URBK_018F) },
@@ -142,6 +156,8 @@ static const struct hid_device_id elecom_devices[] = {
 	{ HID_USB_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_HT1URBK_019B) },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_HT1DRBK_010D) },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_HT1DRBK_011C) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_HT1MRBK) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_HT1MRBK_01AB) },
 	{ }
 };
 MODULE_DEVICE_TABLE(hid, elecom_devices);
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 9c2bf584d9f6..c9b07890a808 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -465,6 +465,9 @@
 #define USB_DEVICE_ID_ELECOM_M_HT1URBK_019B	0x019b
 #define USB_DEVICE_ID_ELECOM_M_HT1DRBK_010D	0x010d
 #define USB_DEVICE_ID_ELECOM_M_HT1DRBK_011C	0x011c
+#define USB_DEVICE_ID_ELECOM_M_HT1MRBK	0x01aa
+#define USB_DEVICE_ID_ELECOM_M_HT1MRBK_01AB	0x01ab
+#define USB_DEVICE_ID_ELECOM_M_HT1MRBK_01AC	0x01ac
 
 #define USB_VENDOR_ID_DREAM_CHEEKY	0x1d34
 #define USB_DEVICE_ID_DREAM_CHEEKY_WN	0x0004
diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c
index 11438039cdb7..3217e436c052 100644
--- a/drivers/hid/hid-quirks.c
+++ b/drivers/hid/hid-quirks.c
@@ -420,6 +420,7 @@ static const struct hid_device_id hid_have_special_driver[] = {
 #if IS_ENABLED(CONFIG_HID_ELECOM)
 	{ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_BM084) },
 	{ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_XGL20DLBK) },
+	{ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_HT1MRBK_01AC) },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_XT3URBK_00FB) },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_XT3URBK_018F) },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_XT3DRBK_00FC) },
@@ -432,6 +433,8 @@ static const struct hid_device_id hid_have_special_driver[] = {
 	{ HID_USB_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_HT1URBK_019B) },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_HT1DRBK_010D) },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_HT1DRBK_011C) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_HT1MRBK) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_ELECOM, USB_DEVICE_ID_ELECOM_M_HT1MRBK_01AB) },
 #endif
 #if IS_ENABLED(CONFIG_HID_ELO)
 	{ HID_USB_DEVICE(USB_VENDOR_ID_ELO, 0x0009) },

---
base-commit: 79b95d74470dd97d7d0908d5a3c0734a23e51aa4
change-id: 20260123-hid-elecom-huge-plus-fix-545cf029fd1a

Best regards,
-- 
David Phillips <david@profile.sh>



^ permalink raw reply related

* Re: [PATCH v12 07/11] platform/x86: asus-wmi: Add support for multiple kbd led handlers
From: Denis Benato @ 2026-01-23  0:20 UTC (permalink / raw)
  To: Antheas Kapenekakis, platform-driver-x86, linux-input
  Cc: linux-kernel, Jiri Kosina, Benjamin Tissoires, Corentin Chary,
	Luke D . Jones, Hans de Goede, Ilpo Järvinen
In-Reply-To: <20260122075044.5070-8-lkml@antheas.dev>


On 1/22/26 08:50, Antheas Kapenekakis wrote:
> Some devices, such as the Z13 have multiple Aura devices connected
> to them by USB. In addition, they might have a WMI interface for
> RGB. In Windows, Armoury Crate exposes a unified brightness slider
> for all of them, with 3 brightness levels.
>
> Therefore, to be synergistic in Linux, and support existing tooling
> such as UPower, allow adding listeners to the RGB device of the WMI
> interface. If WMI does not exist, lazy initialize the interface.
>
> Since hid-asus and asus-wmi can both interact with the led objects
> including from an atomic context, protect the brightness access with a
> spinlock and update the values from a workqueue. Use this workqueue to
> also process WMI keyboard events, so they are handled asynchronously.
Reviewed-by: Denis Benato <benato.denis96@gmail.com>
> Acked-by: Benjamin Tissoires <bentiss@kernel.org>
> Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev>
> ---
>  drivers/platform/x86/asus-wmi.c            | 183 ++++++++++++++++++---
>  include/linux/platform_data/x86/asus-wmi.h |  15 ++
>  2 files changed, 173 insertions(+), 25 deletions(-)
>
> diff --git a/drivers/platform/x86/asus-wmi.c b/drivers/platform/x86/asus-wmi.c
> index 4aec7ec69250..c45846be3f99 100644
> --- a/drivers/platform/x86/asus-wmi.c
> +++ b/drivers/platform/x86/asus-wmi.c
> @@ -31,13 +31,13 @@
>  #include <linux/pci.h>
>  #include <linux/pci_hotplug.h>
>  #include <linux/platform_data/x86/asus-wmi.h>
> -#include <linux/platform_data/x86/asus-wmi-leds-ids.h>
>  #include <linux/platform_device.h>
>  #include <linux/platform_profile.h>
>  #include <linux/power_supply.h>
>  #include <linux/rfkill.h>
>  #include <linux/seq_file.h>
>  #include <linux/slab.h>
> +#include <linux/spinlock.h>
>  #include <linux/types.h>
>  #include <linux/units.h>
>  
> @@ -256,6 +256,9 @@ struct asus_wmi {
>  	int tpd_led_wk;
>  	struct led_classdev kbd_led;
>  	int kbd_led_wk;
> +	bool kbd_led_notify;
> +	bool kbd_led_avail;
> +	bool kbd_led_registered;
>  	struct led_classdev lightbar_led;
>  	int lightbar_led_wk;
>  	struct led_classdev micmute_led;
> @@ -264,6 +267,7 @@ struct asus_wmi {
>  	struct work_struct tpd_led_work;
>  	struct work_struct wlan_led_work;
>  	struct work_struct lightbar_led_work;
> +	struct work_struct kbd_led_work;
>  
>  	struct asus_rfkill wlan;
>  	struct asus_rfkill bluetooth;
> @@ -1615,6 +1619,106 @@ static void asus_wmi_battery_exit(struct asus_wmi *asus)
>  
>  /* LEDs ***********************************************************************/
>  
> +struct asus_hid_ref {
> +	struct list_head listeners;
> +	struct asus_wmi *asus;
> +	/* Protects concurrent access from hid-asus and asus-wmi to leds */
> +	spinlock_t lock;
> +};
> +
> +static struct asus_hid_ref asus_ref = {
> +	.listeners = LIST_HEAD_INIT(asus_ref.listeners),
> +	.asus = NULL,
> +	/*
> +	 * Protects .asus, .asus.kbd_led_{wk,notify}, and .listener refs. Other
> +	 * asus variables are read-only after .asus is set.
> +	 *
> +	 * The led cdev device is not protected because it calls backlight_get
> +	 * during initialization, which would result in a nested lock attempt.
> +	 *
> +	 * The led cdev is safe to access without a lock because if
> +	 * kbd_led_avail is true it is initialized before .asus is set and never
> +	 * changed until .asus is dropped. If kbd_led_avail is false, the led
> +	 * cdev is registered by the workqueue, which is single-threaded and
> +	 * cancelled before asus-wmi would access the led cdev to unregister it.
> +	 *
> +	 * A spinlock is used, because the protected variables can be accessed
> +	 * from an IRQ context from asus-hid.
> +	 */
> +	.lock = __SPIN_LOCK_UNLOCKED(asus_ref.lock),
> +};
> +
> +/*
> + * Allows registering hid-asus listeners that want to be notified of
> + * keyboard backlight changes.
> + */
> +int asus_hid_register_listener(struct asus_hid_listener *bdev)
> +{
> +	struct asus_wmi *asus;
> +
> +	guard(spinlock_irqsave)(&asus_ref.lock);
> +	list_add_tail(&bdev->list, &asus_ref.listeners);
> +	asus = asus_ref.asus;
> +	if (asus)
> +		queue_work(asus->led_workqueue, &asus->kbd_led_work);
> +	return 0;
> +}
> +EXPORT_SYMBOL_GPL(asus_hid_register_listener);
> +
> +/*
> + * Allows unregistering hid-asus listeners that were added with
> + * asus_hid_register_listener().
> + */
> +void asus_hid_unregister_listener(struct asus_hid_listener *bdev)
> +{
> +	guard(spinlock_irqsave)(&asus_ref.lock);
> +	list_del(&bdev->list);
> +}
> +EXPORT_SYMBOL_GPL(asus_hid_unregister_listener);
> +
> +static void do_kbd_led_set(struct led_classdev *led_cdev, int value);
> +
> +static void kbd_led_update_all(struct work_struct *work)
> +{
> +	struct asus_wmi *asus;
> +	bool registered, notify;
> +	int ret, value;
> +
> +	asus = container_of(work, struct asus_wmi, kbd_led_work);
> +
> +	scoped_guard(spinlock_irqsave, &asus_ref.lock) {
> +		registered = asus->kbd_led_registered;
> +		value = asus->kbd_led_wk;
> +		notify = asus->kbd_led_notify;
> +	}
> +
> +	if (!registered) {
> +		/*
> +		 * This workqueue runs under asus-wmi, which means probe has
> +		 * completed and asus-wmi will keep running until it finishes.
> +		 * Therefore, we can safely register the LED without holding
> +		 * a spinlock.
> +		 */
> +		ret = devm_led_classdev_register(&asus->platform_device->dev,
> +						 &asus->kbd_led);
> +		if (!ret) {
> +			scoped_guard(spinlock_irqsave, &asus_ref.lock)
> +				asus->kbd_led_registered = true;
> +		} else {
> +			pr_warn("Failed to register keyboard backlight LED: %d\n", ret);
> +			return;
> +		}
> +	}
> +
> +	if (value >= 0)
> +		do_kbd_led_set(&asus->kbd_led, value);
> +	if (notify) {
> +		scoped_guard(spinlock_irqsave, &asus_ref.lock)
> +			asus->kbd_led_notify = false;
> +		led_classdev_notify_brightness_hw_changed(&asus->kbd_led, value);
> +	}
> +}
> +
>  /*
>   * These functions actually update the LED's, and are called from a
>   * workqueue. By doing this as separate work rather than when the LED
> @@ -1661,7 +1765,8 @@ static void kbd_led_update(struct asus_wmi *asus)
>  {
>  	int ctrl_param = 0;
>  
> -	ctrl_param = 0x80 | (asus->kbd_led_wk & 0x7F);
> +	scoped_guard(spinlock_irqsave, &asus_ref.lock)
> +		ctrl_param = 0x80 | (asus->kbd_led_wk & 0x7F);
>  	asus_wmi_set_devstate(ASUS_WMI_DEVID_KBD_BACKLIGHT, ctrl_param, NULL);
>  }
>  
> @@ -1694,14 +1799,23 @@ static int kbd_led_read(struct asus_wmi *asus, int *level, int *env)
>  
>  static void do_kbd_led_set(struct led_classdev *led_cdev, int value)
>  {
> +	struct asus_hid_listener *listener;
>  	struct asus_wmi *asus;
>  	int max_level;
>  
>  	asus = container_of(led_cdev, struct asus_wmi, kbd_led);
>  	max_level = asus->kbd_led.max_brightness;
>  
> -	asus->kbd_led_wk = clamp_val(value, 0, max_level);
> -	kbd_led_update(asus);
> +	scoped_guard(spinlock_irqsave, &asus_ref.lock)
> +		asus->kbd_led_wk = clamp_val(value, 0, max_level);
> +
> +	if (asus->kbd_led_avail)
> +		kbd_led_update(asus);
> +
> +	scoped_guard(spinlock_irqsave, &asus_ref.lock) {
> +		list_for_each_entry(listener, &asus_ref.listeners, list)
> +			listener->brightness_set(listener, asus->kbd_led_wk);
> +	}
>  }
>  
>  static int kbd_led_set(struct led_classdev *led_cdev, enum led_brightness value)
> @@ -1716,10 +1830,11 @@ static int kbd_led_set(struct led_classdev *led_cdev, enum led_brightness value)
>  
>  static void kbd_led_set_by_kbd(struct asus_wmi *asus, enum led_brightness value)
>  {
> -	struct led_classdev *led_cdev = &asus->kbd_led;
> -
> -	do_kbd_led_set(led_cdev, value);
> -	led_classdev_notify_brightness_hw_changed(led_cdev, asus->kbd_led_wk);
> +	scoped_guard(spinlock_irqsave, &asus_ref.lock) {
> +		asus->kbd_led_wk = value;
> +		asus->kbd_led_notify = true;
> +	}
> +	queue_work(asus->led_workqueue, &asus->kbd_led_work);
>  }
>  
>  static enum led_brightness kbd_led_get(struct led_classdev *led_cdev)
> @@ -1729,10 +1844,18 @@ static enum led_brightness kbd_led_get(struct led_classdev *led_cdev)
>  
>  	asus = container_of(led_cdev, struct asus_wmi, kbd_led);
>  
> +	scoped_guard(spinlock_irqsave, &asus_ref.lock) {
> +		if (!asus->kbd_led_avail)
> +			return asus->kbd_led_wk;
> +	}
> +
>  	retval = kbd_led_read(asus, &value, NULL);
>  	if (retval < 0)
>  		return retval;
>  
> +	scoped_guard(spinlock_irqsave, &asus_ref.lock)
> +		asus->kbd_led_wk = value;
> +
>  	return value;
>  }
>  
> @@ -1844,7 +1967,9 @@ static int camera_led_set(struct led_classdev *led_cdev,
>  
>  static void asus_wmi_led_exit(struct asus_wmi *asus)
>  {
> -	led_classdev_unregister(&asus->kbd_led);
> +	scoped_guard(spinlock_irqsave, &asus_ref.lock)
> +		asus_ref.asus = NULL;
> +
>  	led_classdev_unregister(&asus->tpd_led);
>  	led_classdev_unregister(&asus->wlan_led);
>  	led_classdev_unregister(&asus->lightbar_led);
> @@ -1882,22 +2007,26 @@ static int asus_wmi_led_init(struct asus_wmi *asus)
>  			goto error;
>  	}
>  
> -	if (!kbd_led_read(asus, &led_val, NULL) && !dmi_check_system(asus_use_hid_led_dmi_ids)) {
> -		pr_info("using asus-wmi for asus::kbd_backlight\n");
> -		asus->kbd_led_wk = led_val;
> -		asus->kbd_led.name = "asus::kbd_backlight";
> -		asus->kbd_led.flags = LED_BRIGHT_HW_CHANGED;
> -		asus->kbd_led.brightness_set_blocking = kbd_led_set;
> -		asus->kbd_led.brightness_get = kbd_led_get;
> -		asus->kbd_led.max_brightness = 3;
> +	asus->kbd_led.name = "asus::kbd_backlight";
> +	asus->kbd_led.flags = LED_BRIGHT_HW_CHANGED;
> +	asus->kbd_led.brightness_set_blocking = kbd_led_set;
> +	asus->kbd_led.brightness_get = kbd_led_get;
> +	asus->kbd_led.max_brightness = 3;
> +	asus->kbd_led_avail = !kbd_led_read(asus, &led_val, NULL);
> +	INIT_WORK(&asus->kbd_led_work, kbd_led_update_all);
>  
> +	if (asus->kbd_led_avail) {
> +		asus->kbd_led_wk = led_val;
>  		if (num_rgb_groups != 0)
>  			asus->kbd_led.groups = kbd_rgb_mode_groups;
> +	} else {
> +		asus->kbd_led_wk = -1;
> +	}
>  
> -		rv = led_classdev_register(&asus->platform_device->dev,
> -					   &asus->kbd_led);
> -		if (rv)
> -			goto error;
> +	scoped_guard(spinlock_irqsave, &asus_ref.lock) {
> +		asus_ref.asus = asus;
> +		if (asus->kbd_led_avail || !list_empty(&asus_ref.listeners))
> +			queue_work(asus->led_workqueue, &asus->kbd_led_work);
>  	}
>  
>  	if (asus_wmi_dev_is_present(asus, ASUS_WMI_DEVID_WIRELESS_LED)
> @@ -4372,6 +4501,7 @@ static int asus_wmi_get_event_code(union acpi_object *obj)
>  
>  static void asus_wmi_handle_event_code(int code, struct asus_wmi *asus)
>  {
> +	enum led_brightness led_value;
>  	unsigned int key_value = 1;
>  	bool autorelease = 1;
>  
> @@ -4388,19 +4518,22 @@ static void asus_wmi_handle_event_code(int code, struct asus_wmi *asus)
>  		return;
>  	}
>  
> +	scoped_guard(spinlock_irqsave, &asus_ref.lock)
> +		led_value = asus->kbd_led_wk;
> +
>  	if (code == NOTIFY_KBD_BRTUP) {
> -		kbd_led_set_by_kbd(asus, asus->kbd_led_wk + 1);
> +		kbd_led_set_by_kbd(asus, led_value + 1);
>  		return;
>  	}
>  	if (code == NOTIFY_KBD_BRTDWN) {
> -		kbd_led_set_by_kbd(asus, asus->kbd_led_wk - 1);
> +		kbd_led_set_by_kbd(asus, led_value - 1);
>  		return;
>  	}
>  	if (code == NOTIFY_KBD_BRTTOGGLE) {
> -		if (asus->kbd_led_wk == asus->kbd_led.max_brightness)
> +		if (led_value >= asus->kbd_led.max_brightness)
>  			kbd_led_set_by_kbd(asus, 0);
>  		else
> -			kbd_led_set_by_kbd(asus, asus->kbd_led_wk + 1);
> +			kbd_led_set_by_kbd(asus, led_value + 1);
>  		return;
>  	}
>  
> diff --git a/include/linux/platform_data/x86/asus-wmi.h b/include/linux/platform_data/x86/asus-wmi.h
> index 419491d4abca..d347cffd05d5 100644
> --- a/include/linux/platform_data/x86/asus-wmi.h
> +++ b/include/linux/platform_data/x86/asus-wmi.h
> @@ -172,12 +172,20 @@ enum asus_ally_mcu_hack {
>  	ASUS_WMI_ALLY_MCU_HACK_DISABLED,
>  };
>  
> +/* Used to notify hid-asus when asus-wmi changes keyboard backlight */
> +struct asus_hid_listener {
> +	struct list_head list;
> +	void (*brightness_set)(struct asus_hid_listener *listener, int brightness);
> +};
> +
>  #if IS_REACHABLE(CONFIG_ASUS_WMI)
>  void set_ally_mcu_hack(enum asus_ally_mcu_hack status);
>  void set_ally_mcu_powersave(bool enabled);
>  int asus_wmi_get_devstate_dsts(u32 dev_id, u32 *retval);
>  int asus_wmi_set_devstate(u32 dev_id, u32 ctrl_param, u32 *retval);
>  int asus_wmi_evaluate_method(u32 method_id, u32 arg0, u32 arg1, u32 *retval);
> +int asus_hid_register_listener(struct asus_hid_listener *cdev);
> +void asus_hid_unregister_listener(struct asus_hid_listener *cdev);
>  #else
>  static inline void set_ally_mcu_hack(enum asus_ally_mcu_hack status)
>  {
> @@ -198,6 +206,13 @@ static inline int asus_wmi_evaluate_method(u32 method_id, u32 arg0, u32 arg1,
>  {
>  	return -ENODEV;
>  }
> +static inline int asus_hid_register_listener(struct asus_hid_listener *bdev)
> +{
> +	return -ENODEV;
> +}
> +static inline void asus_hid_unregister_listener(struct asus_hid_listener *bdev)
> +{
> +}
>  #endif
>  
>  #endif	/* __PLATFORM_DATA_X86_ASUS_WMI_H */

^ permalink raw reply

* Re: [PATCH v12 02/11] HID: asus: initialize additional endpoints only for certain devices
From: Denis Benato @ 2026-01-23  0:18 UTC (permalink / raw)
  To: Antheas Kapenekakis, platform-driver-x86, linux-input
  Cc: linux-kernel, Jiri Kosina, Benjamin Tissoires, Corentin Chary,
	Luke D . Jones, Hans de Goede, Ilpo Järvinen
In-Reply-To: <20260122075044.5070-3-lkml@antheas.dev>


On 1/22/26 08:50, Antheas Kapenekakis wrote:
> Currently, ID1/ID2 initializations are performed for all NKEY devices.
> However, ID1 initializations are only required for RGB control and are
> only supported for RGB capable devices. ID2 initializations are only
> required for initializing the Anime display endpoint which is only
> supported on devices with an Anime display. Both of these
> initializations are for functionality that is not present on this driver
> and are performed for devices which might not support them.
>
> At the same time, there are older NKEY devices that have only been
> tested with these initializations in the kernel and it is not possible
> to recheck them. There is a possibility that especially with the ID1
> initialization, certain laptop models might have their shortcuts stop
> working (currently unproven).
>
> To avoid sending unnecessary commands, change to only initialize ID1/ID2
> for those NKEY devices suspected to be problematic without them by
> introducing a quirk for them and replacing the NKEY quirk in the block
> that performs the inits with that. Therefore, new devices that do not
> need (and some do not support) these initializations will not have
> them performed.
>
> In addition, as these initializations might not be supported by the
> affected devices, change the function to not bail if they fail.
Reviewed-by: Denis Benato <benato.denis96@gmail.com>
> Acked-by: Benjamin Tissoires <bentiss@kernel.org>
> Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev>
> ---
>  drivers/hid/hid-asus.c | 16 ++++++----------
>  1 file changed, 6 insertions(+), 10 deletions(-)
>
> diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
> index 323e6302bac5..92904b5a700c 100644
> --- a/drivers/hid/hid-asus.c
> +++ b/drivers/hid/hid-asus.c
> @@ -90,6 +90,7 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
>  #define QUIRK_ROG_NKEY_KEYBOARD		BIT(11)
>  #define QUIRK_ROG_CLAYMORE_II_KEYBOARD BIT(12)
>  #define QUIRK_ROG_ALLY_XPAD		BIT(13)
> +#define QUIRK_ROG_NKEY_ID1ID2_INIT		BIT(14)
>  
>  #define I2C_KEYBOARD_QUIRKS			(QUIRK_FIX_NOTEBOOK_REPORT | \
>  						 QUIRK_NO_INIT_REPORTS | \
> @@ -652,14 +653,9 @@ static int asus_kbd_register_leds(struct hid_device *hdev)
>  	if (!(kbd_func & SUPPORT_KBD_BACKLIGHT))
>  		return -ENODEV;
>  
> -	if (drvdata->quirks & QUIRK_ROG_NKEY_KEYBOARD) {
> -		ret = asus_kbd_init(hdev, FEATURE_KBD_LED_REPORT_ID1);
> -		if (ret < 0)
> -			return ret;
> -
> -		ret = asus_kbd_init(hdev, FEATURE_KBD_LED_REPORT_ID2);
> -		if (ret < 0)
> -			return ret;
> +	if (drvdata->quirks & QUIRK_ROG_NKEY_ID1ID2_INIT) {
> +		asus_kbd_init(hdev, FEATURE_KBD_LED_REPORT_ID1);
> +		asus_kbd_init(hdev, FEATURE_KBD_LED_REPORT_ID2);
>  	}
>  
>  	if (dmi_match(DMI_PRODUCT_FAMILY, "ProArt P16")) {
> @@ -1376,10 +1372,10 @@ static const struct hid_device_id asus_devices[] = {
>  	  QUIRK_USE_KBD_BACKLIGHT },
>  	{ HID_USB_DEVICE(USB_VENDOR_ID_ASUSTEK,
>  	    USB_DEVICE_ID_ASUSTEK_ROG_NKEY_KEYBOARD),
> -	  QUIRK_USE_KBD_BACKLIGHT | QUIRK_ROG_NKEY_KEYBOARD },
> +	  QUIRK_USE_KBD_BACKLIGHT | QUIRK_ROG_NKEY_KEYBOARD | QUIRK_ROG_NKEY_ID1ID2_INIT },
>  	{ HID_USB_DEVICE(USB_VENDOR_ID_ASUSTEK,
>  	    USB_DEVICE_ID_ASUSTEK_ROG_NKEY_KEYBOARD2),
> -	  QUIRK_USE_KBD_BACKLIGHT | QUIRK_ROG_NKEY_KEYBOARD },
> +	  QUIRK_USE_KBD_BACKLIGHT | QUIRK_ROG_NKEY_KEYBOARD | QUIRK_ROG_NKEY_ID1ID2_INIT },
>  	{ HID_USB_DEVICE(USB_VENDOR_ID_ASUSTEK,
>  	    USB_DEVICE_ID_ASUSTEK_ROG_Z13_LIGHTBAR),
>  	  QUIRK_USE_KBD_BACKLIGHT | QUIRK_ROG_NKEY_KEYBOARD },

^ permalink raw reply

* Re: [PATCH v1 1/1] dt-bindings: input: touchscreen: tsc2007: document '#io-channel-cells'
From: Rob Herring (Arm) @ 2026-01-22 23:34 UTC (permalink / raw)
  To: Svyatoslav Ryhel
  Cc: Fabio Estevam, Andreas Kemnade, Krzysztof Kozlowski, Frank Li,
	Dmitry Torokhov, Conor Dooley, linux-input, linux-kernel,
	devicetree
In-Reply-To: <20260122193549.29858-2-clamor95@gmail.com>


On Thu, 22 Jan 2026 21:35:49 +0200, Svyatoslav Ryhel wrote:
> The tsc2007 can be used not only as resistive touchscreen controller but
> also as a ADC IIO sensor. The second use case requires '#io-channel-cells'
> property, hence add it.
> 
> Signed-off-by: Svyatoslav Ryhel <clamor95@gmail.com>
> ---
>  .../devicetree/bindings/input/touchscreen/ti,tsc2007.yaml      | 3 +++
>  1 file changed, 3 insertions(+)
> 

Acked-by: Rob Herring (Arm) <robh@kernel.org>


^ permalink raw reply

* Re: [PATCH 1/1] Adding support for aXiom touchscreen controller
From: Marco Felsch @ 2026-01-22 22:09 UTC (permalink / raw)
  To: AThomas63
  Cc: dmitry.torokhov, linux-input, linux-kernel, mark.satterthwaite,
	kamel.bouhara, kernel
In-Reply-To: <20260122124819.273188-2-andrew.thomas@touchnetix.com>

The git-subject seems wrong, this should be:

"Input: Add support for ...."

also you miss a proper commit description.

On 26-01-22, AThomas63 wrote:
> Signed-off-by: AThomas63 <andrew.thomas@touchnetix.com>
> ---
>  drivers/input/touchscreen/Kconfig      |  30 ++
>  drivers/input/touchscreen/Makefile     |   3 +
>  drivers/input/touchscreen/axiom_core.c | 482 +++++++++++++++++++++++++
>  drivers/input/touchscreen/axiom_core.h | 128 +++++++
>  drivers/input/touchscreen/axiom_i2c.c  | 152 ++++++++
>  drivers/input/touchscreen/axiom_spi.c  | 159 ++++++++
>  6 files changed, 954 insertions(+)
>  create mode 100644 drivers/input/touchscreen/axiom_core.c
>  create mode 100644 drivers/input/touchscreen/axiom_core.h
>  create mode 100644 drivers/input/touchscreen/axiom_i2c.c
>  create mode 100644 drivers/input/touchscreen/axiom_spi.c
> 
> diff --git a/drivers/input/touchscreen/Kconfig b/drivers/input/touchscreen/Kconfig
> index 7d5b72ee07fa..f2b4fb317bdd 100644
> --- a/drivers/input/touchscreen/Kconfig
> +++ b/drivers/input/touchscreen/Kconfig
> @@ -162,6 +162,36 @@ config TOUCHSCREEN_AUO_PIXCIR
>  	  To compile this driver as a module, choose M here: the
>  	  module will be called auo-pixcir-ts.
>  
> +config TOUCHSCREEN_AXIOM_CORE
> +	tristate "TouchNetix Axiom touchscreen"

Nack, the spi or i2c should select the core.

> +	help
> +	  Say Y here if you have an Axiom touchscreen connected
> +	  to your system. You will also need to select appropriate
> +	  bus connection below.
> +
> +	  If unsure, say N.
> +
> +	  To compile this driver as a module, choose M here: the
> +	  module will be called axiom_core.
> +
> +config TOUCHSCREEN_AXIOM_I2C
> +	tristate "support I2C bus connection"
> +	depends on TOUCHSCREEN_AXIOM_CORE && I2C
> +	help
> +	  Say Y here if the touchscreen is connected via I2C bus.
> +
> +	  To compile this driver as a module, choose M here: the
> +	  module will be called axiom_spi.
> +
> +config TOUCHSCREEN_AXIOM_SPI
> +	tristate "support SPI bus connection"
> +	depends on TOUCHSCREEN_AXIOM_CORE && SPI
> +	help
> +	  Say Y here if the touchscreen is connected via SPI bus.
> +
> +	  To compile this driver as a module, choose M here: the
> +	  module will be called axiom_spi.
> +
>  config TOUCHSCREEN_BU21013
>  	tristate "BU21013 based touch panel controllers"
>  	depends on I2C
> diff --git a/drivers/input/touchscreen/Makefile b/drivers/input/touchscreen/Makefile
> index ab9abd151078..9b7d572c4589 100644
> --- a/drivers/input/touchscreen/Makefile
> +++ b/drivers/input/touchscreen/Makefile
> @@ -19,6 +19,9 @@ obj-$(CONFIG_TOUCHSCREEN_APPLE_Z2)	+= apple_z2.o
>  obj-$(CONFIG_TOUCHSCREEN_AR1021_I2C)	+= ar1021_i2c.o
>  obj-$(CONFIG_TOUCHSCREEN_ATMEL_MXT)	+= atmel_mxt_ts.o
>  obj-$(CONFIG_TOUCHSCREEN_AUO_PIXCIR)	+= auo-pixcir-ts.o
> +obj-$(CONFIG_TOUCHSCREEN_AXIOM_CORE)	+= axiom_core.o
> +obj-$(CONFIG_TOUCHSCREEN_AXIOM_I2C)	+= axiom_i2c.o
> +obj-$(CONFIG_TOUCHSCREEN_AXIOM_SPI)	+= axiom_spi.o
>  obj-$(CONFIG_TOUCHSCREEN_BU21013)	+= bu21013_ts.o
>  obj-$(CONFIG_TOUCHSCREEN_BU21029)	+= bu21029_ts.o
>  obj-$(CONFIG_TOUCHSCREEN_CHIPONE_ICN8318)	+= chipone_icn8318.o
> diff --git a/drivers/input/touchscreen/axiom_core.c b/drivers/input/touchscreen/axiom_core.c
> new file mode 100644
> index 000000000000..7983effe40f7
> --- /dev/null
> +++ b/drivers/input/touchscreen/axiom_core.c
> @@ -0,0 +1,482 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * TouchNetix aXiom Touchscreen Driver
> + *
> + * Copyright (C) 2020-2026 TouchNetix Ltd.
> + *
> + * Author(s): Mark Satterthwaite <mark.satterthwaite@touchnetix.com>
> + *            Pedro Torruella <pedro.torruella@touchnetix.com>
> + *            Bart Prescott <bartp@baasheep.co.uk>
> + *            Hannah Rossiter <hannah.rossiter@touchnetix.com>
> + *            Andrew Thomas <andrew.thomas@touchnetix.com>
> + *
> + * This program is free software; you can redistribute  it and/or modify it
> + * under  the terms of  the GNU General  Public License as published by the
> + * Free Software Foundation;  either version 2 of the  License, or (at your
> + * option) any later version.
> + *
> + */
> +
> +// #define DEBUG // Enable debug messages

remove this

> +
> +#include <linux/device.h>
> +#include <linux/input/mt.h>
> +#include <linux/crc16.h>
> +#include <linux/property.h>
> +#include <linux/interrupt.h>
> +#include <linux/unaligned.h>
> +#include "axiom_core.h"
> +
> +/* u31 device info masks */
> +#define AX_DEV_ID_MASK GENMASK(14, 0)
> +#define AX_MODE BIT(15)
> +#define AX_FW_REV_MINOR_MASK GENMASK(7, 0)
> +#define AX_FW_REV_MAJOR_MASK GENMASK(15, 8)
> +#define AX_VARIANT_MASK GENMASK(5, 0)
> +#define AX_FW_STATUS BIT(7)
> +#define AX_TCP_REV_MASK GENMASK(15, 8)
> +#define AX_BOOT_REV_MINOR_MASK GENMASK(7, 0)
> +#define AX_BOOT_REV_MAJOR_MASK GENMASK(15, 8)
> +#define AX_NUM_USAGES_MASK GENMASK(7, 0)
> +#define AX_SILICON_REV_MASK GENMASK(11, 8)
> +#define AX_RUNTIME_FW_PATCH_MASK GENMASK(15, 12)
> +
> +/* u31 usage table entry masks */
> +#define AX_U31_USAGE_NUM_MASK GENMASK(7, 0)
> +#define AX_U31_START_PAGE_MASK GENMASK(15, 8)
> +#define AX_U31_NUM_PAGES_MASK GENMASK(7, 0)
> +#define AX_U31_MAX_OFFSET_MASK GENMASK(14, 8)
> +#define AX_U31_OFFSET_TYPE_BIT BIT(15)
> +#define AX_U31_UIF_REV_MASK GENMASK(7, 0)
> +#define AX_U31_USAGE_TYPE_MASK GENMASK(15, 8)
> +
> +/* u34 report masks */
> +#define AX_U34_LEN_MASK GENMASK(6, 0)
> +#define AX_U34_OVERFLOW BIT(7)
> +#define AX_U34_USAGE_MASK GENMASK(15, 8)
> +#define AX_U34_PAYLOAD_BUFFER 2
> +
> +/* u41 report masks */
> +#define AX_U41_PRESENT_MASK GENMASK(9, 0)
> +#define U41_X_Y_OFFSET (2)
> +#define U41_COORD_SIZE (4)
> +#define U41_Z_OFFSET (42)

Align your defines.

> +
> +static const char *const fw_variants[] = { "3D", "2D", "FORCE",
> +					   "0D", "XL", "TOUCHPAD" };

Check your code against the kernel coding style, this applies to many
code parts, I won't list them all.

> +
> +static int axiom_set_capabilities(struct input_dev *input_dev)
> +{
> +	input_dev->name = "TouchNetix aXiom Touchscreen";
> +	input_dev->phys = "input/axiom_ts";
> +
> +	// Single Touch

Coding style.

> +	input_set_abs_params(input_dev, ABS_X, 0, 65535, 0, 0);
> +	input_set_abs_params(input_dev, ABS_Y, 0, 65535, 0, 0);
> +
> +	// Multi Touch
> +	// Min, Max, Fuzz (expected noise in px, try 4?) and Flat
> +	input_set_abs_params(input_dev, ABS_MT_POSITION_X, 0, 65535, 0, 0);
> +	// Min, Max, Fuzz (expected noise in px, try 4?) and Flat
> +	input_set_abs_params(input_dev, ABS_MT_POSITION_Y, 0, 65535, 0, 0);
> +	input_set_abs_params(input_dev, ABS_MT_TOOL_TYPE, 0, MT_TOOL_MAX, 0, 0);
> +	input_set_abs_params(input_dev, ABS_MT_DISTANCE, 0, 127, 0, 0);
> +	input_set_abs_params(input_dev, ABS_MT_PRESSURE, 0, 127, 0, 0);

Max values are coming from the firmware.

> +
> +	input_mt_init_slots(input_dev, U41_MAX_TARGETS, INPUT_MT_DIRECT);

num-targets can be configured via firmware config IIRC, so nack.

> +
> +	return 0;
> +}
> +
> +static struct u31_usage_entry *usage_find_entry(struct axiom *ax, u16 usage)
> +{
> +	u16 i;
> +
> +	for (i = 0; i < ax->dev_info.num_usages; i++) {
> +		if (ax->usage_table[i].usage_num == usage)
> +			return &ax->usage_table[i];
> +	}
> +
> +	pr_err("aXiom-core: Usage u%02x not found in usage table\n", usage);
> +	return ERR_PTR(-EINVAL);
> +}
> +
> +static void axiom_unpack_device_info(const u8 *buf,
> +				     struct axiom_device_info *info)
> +{
> +	u16 w;
> +
> +	w = get_unaligned_le16(buf);
> +	info->device_id = FIELD_GET(AX_DEV_ID_MASK, w);
> +	info->mode = !!(w & AX_MODE);
> +
> +	w = get_unaligned_le16(buf + 2);
> +	info->runtime_fw_rev_minor = FIELD_GET(AX_FW_REV_MINOR_MASK, w);
> +	info->runtime_fw_rev_major = FIELD_GET(AX_FW_REV_MAJOR_MASK, w);
> +
> +	w = get_unaligned_le16(buf + 4);
> +	info->device_build_variant = FIELD_GET(AX_VARIANT_MASK, w);
> +	info->runtime_fw_status = !!(w & AX_FW_STATUS);
> +	info->tcp_revision = FIELD_GET(AX_TCP_REV_MASK, w);
> +
> +	w = get_unaligned_le16(buf + 6);
> +	info->bootloader_fw_rev_minor = FIELD_GET(AX_BOOT_REV_MINOR_MASK, w);
> +	info->bootloader_fw_rev_major = FIELD_GET(AX_BOOT_REV_MAJOR_MASK, w);
> +
> +	info->jedec_id = get_unaligned_le16(buf + 8);
> +
> +	w = get_unaligned_le16(buf + 10);
> +	info->num_usages = FIELD_GET(AX_NUM_USAGES_MASK, w);
> +	info->silicon_revision = FIELD_GET(AX_SILICON_REV_MASK, w);
> +	info->runtime_fw_rev_patch = FIELD_GET(AX_RUNTIME_FW_PATCH_MASK, w);
> +}
> +
> +static void axiom_unpack_usage_table(u8 *buf, struct axiom *ax)
> +{
> +	u8 *ptr;
> +	struct u31_usage_entry *entry;
> +	int i;
> +	u16 w;
> +	u16 report_len;
> +
> +	for (i = 0; i < ax->dev_info.num_usages && i < U31_MAX_USAGES; i++) {
> +		entry = &ax->usage_table[i];
> +		/* Calculate offset for this specific entry */
> +		ptr = buf + (i * SIZE_U31_USAGE_ENTRY);
> +
> +		w = get_unaligned_le16(ptr);
> +		entry->usage_num = FIELD_GET(AX_U31_USAGE_NUM_MASK, w);
> +		entry->start_page = FIELD_GET(AX_U31_START_PAGE_MASK, w);
> +
> +		w = get_unaligned_le16(ptr + 2);
> +		entry->num_pages = FIELD_GET(AX_U31_NUM_PAGES_MASK, w);
> +		entry->max_offset = FIELD_GET(AX_U31_MAX_OFFSET_MASK, w);
> +		entry->offset_type = !!(w & AX_U31_OFFSET_TYPE_BIT);
> +
> +		w = get_unaligned_le16(ptr + 4);
> +		entry->uifrevision = FIELD_GET(AX_U31_UIF_REV_MASK, w);
> +		entry->usage_type = FIELD_GET(AX_U31_USAGE_TYPE_MASK, w);
> +
> +		// Convert words to bytes
> +		report_len = (entry->max_offset + 1) * 2;
> +		if ((entry->usage_type == REPORT) &&
> +		    (report_len > ax->max_report_len)) {
> +			ax->max_report_len = report_len;
> +		}
> +	}
> +}
> +
> +static int axiom_init_dev_info(struct axiom *ax)
> +{
> +	int i;
> +	struct u31_usage_entry *u;
> +	int err;
> +	const char *variant_str;
> +
> +	/* Read page 0 of u31 */
> +	err = ax->bus_ops->read(ax->dev, 0x0, SIZE_U31_DEVICE_INFO,
> +				ax->read_buf);

No need for custom accessors, use regmap API.

> +	if (err)
> +		return -EIO;
> +
> +	axiom_unpack_device_info(ax->read_buf, &ax->dev_info);
> +
> +	if (ax->dev_info.device_build_variant < ARRAY_SIZE(fw_variants)) {
> +		variant_str = fw_variants[ax->dev_info.device_build_variant];
> +	} else {
> +		variant_str = "UNKNOWN";
> +	}
> +	char silicon_rev = (char)(0x41 + ax->dev_info.silicon_revision);
> +
> +	dev_info(ax->dev, "Firmware Info:\n");
> +	dev_info(ax->dev, "  BL Mode     : %u\n", ax->dev_info.mode);
> +	dev_info(ax->dev, "  Device ID   : %04x\n", ax->dev_info.device_id);
> +	dev_info(ax->dev, "  FW Revision : %u.%u.%u-%s %s\n",
> +		 ax->dev_info.runtime_fw_rev_major,
> +		 ax->dev_info.runtime_fw_rev_minor,
> +		 ax->dev_info.runtime_fw_rev_patch,
> +		 (ax->dev_info.runtime_fw_status == 0) ? "eng" : "prod",
> +		 variant_str);
> +	dev_info(ax->dev, "  BL Revision : %02x.%02x\n",
> +		 ax->dev_info.bootloader_fw_rev_major,
> +		 ax->dev_info.bootloader_fw_rev_minor);
> +	dev_info(ax->dev, "  Silicon     : 0x%04X (Rev %c)\n",
> +		 ax->dev_info.jedec_id, silicon_rev);
> +	dev_info(ax->dev, "  Num Usages  : %u\n", ax->dev_info.num_usages);

Why always dev_info()?

> +
> +	if (ax->dev_info.num_usages > U31_MAX_USAGES) {
> +		dev_err(ax->dev,
> +			"Num usages (%u) exceeds maximum supported (%u)\n",
> +			ax->dev_info.num_usages, U31_MAX_USAGES);
> +		return -EINVAL;
> +	}
> +
> +	/* Read the second page of u31 to get the usage table */
> +	err = ax->bus_ops->read(ax->dev, 0x100,
> +				sizeof(ax->usage_table[0]) *
> +					ax->dev_info.num_usages,
> +				ax->read_buf);

You don't check if the device is bootloader-mode so this may fail
depending on the device state.

> +	if (err)
> +		return -EIO;
> +
> +	axiom_unpack_usage_table(ax->read_buf, ax);
> +
> +	dev_info(ax->dev, "Usage Table:\n");
> +	for (i = 0; i < ax->dev_info.num_usages; i++) {
> +		u = &ax->usage_table[i];
> +
> +		dev_info(
> +			ax->dev,
> +			"  Usage: u%02x  Rev: %3u  Page: 0x%02x00  Num Pages: %3u\n",
> +			u->usage_num, u->uifrevision, u->start_page,
> +			u->num_pages);
> +	}
> +	dev_info(ax->dev, "Max Report Length: %u\n", ax->max_report_len);
> +
> +	if (ax->max_report_len > AXIOM_MAX_READ_SIZE) {
> +		dev_err(ax->dev,
> +			"aXiom maximum report length (%u) greater than allocated buffer size (%u).",
> +			ax->max_report_len, AXIOM_MAX_READ_SIZE);
> +		return -EINVAL;
> +	}
> +
> +	/* Set u34 address to allow direct access to report reading address */
> +	u = usage_find_entry(ax, 0x34);
> +	if (IS_ERR(u))
> +		return PTR_ERR(u);
> +	ax->u34_address = u->start_page << 8;
> +
> +	return 0;
> +}
> +
> +static int axiom_process_u41_report(struct axiom *ax, u8 *report)
> +{
> +	int i;
> +	u16 target_present;
> +	bool active;
> +	u8 offset;
> +	enum u41_target_state_e state;
> +	u16 x;
> +	u16 y;
> +	s8 z;
> +
> +	target_present =
> +		FIELD_GET(AX_U41_PRESENT_MASK, get_unaligned_le16(&report[0]));
> +
> +	for (i = 0; i < U41_MAX_TARGETS; i++) {
> +		active = !!((target_present >> i) & 1);
> +
> +		offset = U41_X_Y_OFFSET + (i * U41_COORD_SIZE);
> +		x = get_unaligned_le16(&report[offset]);
> +		y = get_unaligned_le16(&report[offset + 2]);
> +		z = report[U41_Z_OFFSET + i];
> +
> +		if (!active)
> +			state = Target_State_Not_Present;
> +		else if (z >= 0)
> +			state = Target_State_Touching;
> +		else if ((z > U41_PROX_LEVEL) && (z < 0))
> +			state = Target_State_Hover;
> +		else if (z == U41_PROX_LEVEL)
> +			state = Target_State_Prox;
> +		else
> +			state = Target_State_Not_Present;
> +
> +		dev_dbg(ax->dev, "Target %d: x=%u y=%u z=%d present=%d\n", i, x,
> +			y, z, active);
> +
> +		switch (state) {
> +		case Target_State_Not_Present:
> +		case Target_State_Prox:
> +
> +			input_mt_slot(ax->input, i);
> +			input_mt_report_slot_inactive(ax->input);
> +			break;
> +
> +		case Target_State_Hover:
> +		case Target_State_Touching:
> +
> +			input_mt_slot(ax->input, i);
> +			input_report_abs(ax->input, ABS_MT_TRACKING_ID, i);
> +			input_report_abs(ax->input, ABS_MT_POSITION_X, x);
> +			input_report_abs(ax->input, ABS_MT_POSITION_Y, y);
> +
> +			if (state == Target_State_Touching) {
> +				input_report_abs(ax->input, ABS_MT_DISTANCE, 0);
> +				input_report_abs(ax->input, ABS_MT_PRESSURE, z);
> +			} else { /* Hover */
> +				input_report_abs(ax->input, ABS_MT_DISTANCE, -z);
> +				input_report_abs(ax->input, ABS_MT_PRESSURE, 0);
> +			}
> +			break;
> +
> +		default:
> +			break;
> +		}
> +	}
> +
> +	input_mt_sync_frame(ax->input);
> +	input_sync(ax->input);
> +
> +	return 0;
> +}
> +
> +static int axiom_process_report(struct axiom *ax, u8 *report)
> +{
> +	int err;
> +	struct u34_report_header hdr;
> +	u16 crc_calc;
> +	u16 crc_report;
> +	u8 len;
> +	u16 hdr_buf = get_unaligned_le16(&report[0]);
> +
> +	dev_dbg(ax->dev, "Payload Data %*ph\n", ax->max_report_len, report);
> +
> +	hdr.report_length = FIELD_GET(AX_U34_LEN_MASK, hdr_buf);
> +	hdr.overflow = !!(hdr_buf & AX_U34_OVERFLOW);
> +	hdr.report_usage = FIELD_GET(AX_U34_USAGE_MASK, hdr_buf);
> +
> +	len = hdr.report_length << 1;
> +	if (hdr.report_length == 0) {
> +		dev_err(ax->dev, "Zero length report discarded.\n");
> +		return -EIO;
> +	}
> +
> +	// Length is 16 bit words and remove the size of the CRC16 itself
> +	crc_report = (report[len - 1] << 8) | (report[len - 2]);
> +	crc_calc = crc16(0, report, (len - 2));
> +
> +	if (crc_calc != crc_report) {
> +		dev_err(ax->dev,
> +			"CRC mismatch! Expected: %04X, Calculated CRC: %04X. Report discarded.\n",
> +			crc_report, crc_calc);
> +		return -EIO;
> +	}
> +
> +	switch (hdr.report_usage) {
> +	case AX_2DCTS_REPORT_ID:
> +		err = axiom_process_u41_report(ax,
> +					       &report[AX_U34_PAYLOAD_BUFFER]);

May I ask why you guys write in your programming manual, that the host
needs to check the usage-revision and you completely ignore this?

> +		break;
> +
> +	default:
> +		break;
> +	}
> +
> +	return err;
> +}
> +
> +static void axiom_poll(struct input_dev *input_dev)
> +{
> +	struct axiom *ax = input_get_drvdata(input_dev);
> +	int err;
> +
> +	/* Read touch reports from u34 */
> +	err = ax->bus_ops->read(ax->dev, ax->u34_address, ax->max_report_len,
> +				ax->read_buf);
> +	if (err)
> +		return;
> +
> +	err = axiom_process_report(ax, ax->read_buf);
> +	if (err)
> +		dev_err(ax->dev, "Failed to process report: %d\n", err);
> +}
> +
> +static irqreturn_t axiom_irq(int irq, void *handle)
> +{
> +	struct axiom *ax = handle;
> +	int err;
> +
> +	/* Read touch reports from u34 */
> +	err = ax->bus_ops->read(ax->dev, ax->u34_address, ax->max_report_len,
> +				ax->read_buf);
> +	if (err)
> +		goto out;
> +
> +	err = axiom_process_report(ax, ax->read_buf);
> +	if (err)
> +		dev_err(ax->dev, "Failed to process report: %d\n", err);
> +
> +out:
> +	return IRQ_HANDLED;
> +}
> +
> +struct axiom *axiom_probe(const struct axiom_bus_ops *bus_ops,
> +			  struct device *dev, int irq)
> +{
> +	struct axiom *ax;
> +	struct input_dev *input_dev;
> +	int err;
> +	bool poll_enable = false;
> +	u8 poll_period = 0;

Reverse christmas tree, coding style.

> +
> +	ax = devm_kzalloc(dev, sizeof(*ax), GFP_KERNEL);
> +	if (!ax)
> +		return ERR_PTR(-ENOMEM);
> +
> +	input_dev = devm_input_allocate_device(dev);
> +	if (!input_dev) {
> +		pr_err("ERROR: aXiom-core: Failed to allocate memory for input device!\n");
> +		return ERR_PTR(-ENOMEM);
> +	}
> +
> +	poll_enable = device_property_read_bool(dev, "axiom,poll-enable");

There is common dt-property for poll.

> +
> +	device_property_read_u8(dev, "axiom,poll-period", &poll_period);
> +	if (!poll_period)
> +		poll_period = AX_POLLING_PERIOD_MS;
> +
> +	ax->dev = dev;
> +	ax->input = input_dev;
> +	ax->bus_ops = bus_ops;
> +	ax->irq = irq;

You allocate absolute no ext. resources like regulators or reset-gpios.

> +
> +	dev_info(dev, "aXiom Probe\n");
> +	if (poll_enable)
> +		dev_info(dev, "Polling Period : %u\n", poll_period);
> +	else
> +		dev_info(dev, "Device IRQ : %u\n", ax->irq);

Useless dev_info()'s

> +
> +	axiom_set_capabilities(input_dev);

Capabilities should be set after you know which firmware you're running
on, e.g. if the firmware supports 3D touchevents.

I will stop now. As said, I'm very surprised why you guys went this way
instead of just adding the SPI support to my patchset?

Your driver is missing the basics like checking the usage-revision which
is clearly written within your programming-guide. Also this driver is
missing the fw-update and cfg-update mechanism as well as basic resource
handling like ext. power-supplies.

I'd really appreciate if you guys could provide feedback to the driver
I've send so we can fix some "Downstream" comments :)

Regards,
  Marco

> +
> +	err = axiom_init_dev_info(ax);
> +	if (err) {
> +		dev_err(ax->dev, "Failed to read device info, err: %d\n", err);
> +		return ERR_PTR(err);
> +	}
> +
> +	if (poll_enable) {
> +		err = input_setup_polling(input_dev, axiom_poll);
> +		if (err) {
> +			dev_err(ax->dev, "could not set up polling mode, %d\n",
> +				err);
> +			return ERR_PTR(err);
> +		}
> +
> +		input_set_poll_interval(input_dev, poll_period);
> +	} else {
> +		err = devm_request_threaded_irq(ax->dev, ax->irq, NULL,
> +						axiom_irq,
> +						IRQF_TRIGGER_LOW | IRQF_ONESHOT,
> +						"axiom_irq", ax);
> +		if (err)
> +			return ERR_PTR(err);
> +	}
> +
> +	err = input_register_device(input_dev);
> +	if (err) {
> +		dev_err(ax->dev, "Failed to register input device: %d\n", err);
> +		return ERR_PTR(err);
> +	}
> +
> +	input_set_drvdata(input_dev, ax);
> +
> +	return ax;
> +}
> +EXPORT_SYMBOL_GPL(axiom_probe);
> +
> +MODULE_AUTHOR("TouchNetix <support@touchnetix.com>");
> +MODULE_DESCRIPTION("aXiom touchscreen core logic");
> +MODULE_LICENSE("GPL");
> +MODULE_ALIAS("axiom");
> +MODULE_VERSION("1.0.0");
> diff --git a/drivers/input/touchscreen/axiom_core.h b/drivers/input/touchscreen/axiom_core.h
> new file mode 100644
> index 000000000000..ca77f9038cb1
> --- /dev/null
> +++ b/drivers/input/touchscreen/axiom_core.h
> @@ -0,0 +1,128 @@
> +/* SPDX-License-Identifier: GPL-2.0 */
> +/*
> + * TouchNetix aXiom Touchscreen Driver
> + *
> + * Copyright (C) 2020-2026 TouchNetix Ltd.
> + *
> + * Author(s): Mark Satterthwaite <mark.satterthwaite@touchnetix.com>
> + *            Pedro Torruella <pedro.torruella@touchnetix.com>
> + *            Bart Prescott <bartp@baasheep.co.uk>
> + *            Hannah Rossiter <hannah.rossiter@touchnetix.com>
> + *            Andrew Thomas <andrew.thomas@touchnetix.com>
> + *
> + * This program is free software; you can redistribute  it and/or modify it
> + * under  the terms of  the GNU General  Public License as published by the
> + * Free Software Foundation;  either version 2 of the  License, or (at your
> + * option) any later version.
> + *
> + */
> +
> +#ifndef __AXIOM_CORE_H
> +#define __AXIOM_CORE_H
> +
> +#include <linux/input.h>
> +
> +#define AX_POLLING_PERIOD_MS (10)
> +
> +#define AXIOM_USE_TOUCHSCREEN_INTERFACE // registers the axiom device as a touch screen instead of as a mouse pointer
> +#define U46_ENABLE_RAW_FORCE_DATA // enables the raw data for up to 4 force channels to be sent to the input subsystem
> +
> +#define AXIOM_PAGE_SIZE (256)
> +// u31 has 2 pages for usage table entries. (2 * PAGE_SIZE) / U31_BYTES_PER_USAGE = 85
> +#define AXIOM_MAX_READ_SIZE (2 * AXIOM_PAGE_SIZE)
> +#define SIZE_U31_DEVICE_INFO (12)
> +#define SIZE_U31_USAGE_ENTRY (6)
> +#define U31_MAX_USAGES (85U)
> +#define U41_MAX_TARGETS (10U)
> +#define U41_PROX_LEVEL (-128)
> +#define AXIOM_HOLDOFF_DELAY_US (40)
> +
> +enum ax_comms_op_e { AX_WR_OP = 0, AX_RD_OP = 1 };
> +
> +enum report_ids_e {
> +	AX_2DCTS_REPORT_ID = 0x41,
> +};
> +
> +enum axiom_mode_e {
> +	AX_RUNTIME_STATE = 0,
> +	AX_BOOTLOADER_STATE = 1,
> +};
> +
> +enum usage_type_e {
> +	UNKNOWN = 0,
> +	OTHER = 1,
> +	REPORT = 2,
> +	REGISTER = 3,
> +	REGISTER_READ_ONLY_ = 4,
> +	CDU = 5,
> +	CDU_READ_ONLY_ = 6,
> +};
> +
> +struct axiom_device_info {
> +	u16 device_id;
> +	u8 mode;
> +	u8 runtime_fw_rev_minor;
> +	u8 runtime_fw_rev_major;
> +	u8 device_build_variant;
> +	u8 runtime_fw_status;
> +	u8 tcp_revision;
> +	u8 bootloader_fw_rev_minor;
> +	u8 bootloader_fw_rev_major;
> +	u8 jedec_id;
> +	u8 num_usages;
> +	u8 silicon_revision;
> +	u8 runtime_fw_rev_patch;
> +};
> +
> +struct u31_usage_entry {
> +	u8 usage_num;
> +	u8 start_page;
> +	u8 num_pages;
> +	u8 max_offset;
> +	u8 offset_type;
> +	u8 uifrevision;
> +	u8 usage_type;
> +};
> +
> +struct axiom_cmd_header {
> +	u16 target_address;
> +	u16 length : 15;
> +	u16 rd_wr : 1;
> +};
> +
> +struct axiom_bus_ops {
> +	u16 bustype;
> +	int (*write)(struct device *dev, u16 addr, u16 length, void *values);
> +	int (*read)(struct device *dev, u16 addr, u16 length, void *values);
> +};
> +
> +enum u41_target_state_e {
> +	Target_State_Not_Present = 0,
> +	Target_State_Prox = 1,
> +	Target_State_Hover = 2,
> +	Target_State_Touching = 3,
> +};
> +
> +struct axiom {
> +	struct device *dev;
> +	int irq;
> +	struct input_dev *input;
> +	const struct axiom_bus_ops *bus_ops;
> +	struct axiom_device_info dev_info;
> +	struct u31_usage_entry usage_table[U31_MAX_USAGES];
> +	u16 max_report_len;
> +	u16 u34_address;
> +
> +	u8 read_buf[AXIOM_MAX_READ_SIZE];
> +};
> +
> +struct u34_report_header {
> +	u8 report_length;
> +	u8 overflow;
> +	u8 report_usage;
> +};
> +
> +struct axiom *axiom_probe(const struct axiom_bus_ops *bus_ops,
> +			  struct device *dev, int irq);
> +
> +#endif /* __AXIOM_CORE_H */
> diff --git a/drivers/input/touchscreen/axiom_i2c.c b/drivers/input/touchscreen/axiom_i2c.c
> new file mode 100644
> index 000000000000..66071cc0f7b3
> --- /dev/null
> +++ b/drivers/input/touchscreen/axiom_i2c.c
> @@ -0,0 +1,152 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * TouchNetix aXiom Touchscreen Driver
> + *
> + * Copyright (C) 2020-2026 TouchNetix Ltd.
> + *
> + * Author(s): Bart Prescott <bartp@baasheep.co.uk>
> + *            Pedro Torruella <pedro.torruella@touchnetix.com>
> + *            Mark Satterthwaite <mark.satterthwaite@touchnetix.com>
> + *            Hannah Rossiter <hannah.rossiter@touchnetix.com>
> + *            Andrew Thomas <andrew.thomas@touchnetix.com>
> + *
> + * This program is free software; you can redistribute  it and/or modify it
> + * under  the terms of  the GNU General  Public License as published by the
> + * Free Software Foundation;  either version 2 of the  License, or (at your
> + * option) any later version.
> + *
> + */
> +
> +// #define DEBUG // Enable debug messages
> +
> +#include <linux/i2c.h>
> +#include <linux/kernel.h>
> +#include <linux/delay.h>
> +#include <linux/module.h>
> +#include <linux/input.h>
> +#include "axiom_core.h"
> +
> +static int axiom_i2c_read_block_data(struct device *dev, u16 addr, u16 length,
> +				     void *values)
> +{
> +	int error;
> +	struct i2c_client *client = to_i2c_client(dev);
> +	struct axiom_cmd_header cmd_header = { .target_address = addr,
> +					       .length = length,
> +					       .rd_wr = AX_RD_OP };
> +
> +	struct i2c_msg msgs[] = {
> +		{
> +			.addr = client->addr,
> +			.flags = 0,
> +			.len = sizeof(cmd_header),
> +			.buf = (u8 *)&cmd_header,
> +		},
> +		{
> +			.addr = client->addr,
> +			.flags = I2C_M_RD,
> +			.len = length,
> +			.buf = values,
> +		},
> +	};
> +
> +	error = i2c_transfer(client->adapter, msgs, ARRAY_SIZE(msgs));
> +	if (error < 0) {
> +		dev_err(dev, "I2C transfer error: %d\n", error);
> +		return error;
> +	}
> +
> +	udelay(AXIOM_HOLDOFF_DELAY_US);
> +
> +	return error != ARRAY_SIZE(msgs) ? -EIO : 0;
> +}
> +
> +static int axiom_i2c_write_block_data(struct device *dev, u16 addr, u16 length,
> +				      void *values)
> +{
> +	int error;
> +	struct i2c_client *client = to_i2c_client(dev);
> +	struct axiom_cmd_header cmd_header = { .target_address = addr,
> +					       .length = length,
> +					       .rd_wr = AX_WR_OP };
> +
> +	struct i2c_msg msgs[] = {
> +		{
> +			.addr = client->addr,
> +			.flags = 0,
> +			.len = sizeof(cmd_header),
> +			.buf = (u8 *)&cmd_header,
> +		},
> +		{
> +			.addr = client->addr,
> +			.flags = 0,
> +			.len = length,
> +			.buf = values,
> +		},
> +	};
> +
> +	error = i2c_transfer(client->adapter, msgs, ARRAY_SIZE(msgs));
> +	if (error < 0) {
> +		dev_err(dev, "I2C transfer error: %d\n", error);
> +		return error;
> +	}
> +
> +	udelay(AXIOM_HOLDOFF_DELAY_US);
> +
> +	return error != ARRAY_SIZE(msgs) ? -EIO : 0;
> +}
> +
> +static const struct axiom_bus_ops axiom_i2c_bus_ops = {
> +	.bustype = BUS_I2C,
> +	.write = axiom_i2c_write_block_data,
> +	.read = axiom_i2c_read_block_data,
> +};
> +
> +static int axiom_i2c_probe(struct i2c_client *client)
> +{
> +	struct axiom *axiom;
> +
> +	if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) {
> +		dev_err(&client->dev, "I2C functionality not Supported\n");
> +		return -EIO;
> +	}
> +
> +	axiom = axiom_probe(&axiom_i2c_bus_ops, &client->dev, client->irq);
> +	if (IS_ERR(axiom))
> +		return dev_err_probe(&client->dev, PTR_ERR(axiom),
> +				     "failed to register input device\n");
> +
> +	return 0;
> +}
> +
> +static const struct i2c_device_id axiom_i2c_id_table[] = {
> +	{ "axiom-i2c" },
> +	{},
> +};
> +MODULE_DEVICE_TABLE(i2c, axiom_i2c_id_table);
> +
> +static const struct of_device_id axiom_i2c_dt_ids[] = {
> +	{
> +		.compatible = "tnx,axiom-i2c",
> +		.data = "axiom",
> +	},
> +	{}
> +};
> +MODULE_DEVICE_TABLE(of, axiom_i2c_dt_ids);
> +
> +static struct i2c_driver axiom_i2c_driver = {
> +	.driver = {
> +		.name = "axiom_i2c",
> +		.of_match_table = of_match_ptr(axiom_i2c_dt_ids),
> +	},
> +	.id_table = axiom_i2c_id_table,
> +	.probe = axiom_i2c_probe,
> +};
> +
> +module_i2c_driver(axiom_i2c_driver);
> +
> +MODULE_AUTHOR("TouchNetix <support@touchnetix.com>");
> +MODULE_DESCRIPTION("aXiom touchscreen I2C bus driver");
> +MODULE_LICENSE("GPL");
> +MODULE_ALIAS("axiom");
> +MODULE_VERSION("1.0.0");
> diff --git a/drivers/input/touchscreen/axiom_spi.c b/drivers/input/touchscreen/axiom_spi.c
> new file mode 100644
> index 000000000000..a67ad3645689
> --- /dev/null
> +++ b/drivers/input/touchscreen/axiom_spi.c
> @@ -0,0 +1,159 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * TouchNetix aXiom Touchscreen Driver
> + *
> + * Copyright (C) 2018-2023 TouchNetix Ltd.
> + *
> + * Author(s): Mark Satterthwaite <mark.satterthwaite@touchnetix.com>
> + *            Bart Prescott <bartp@baasheep.co.uk>
> + *            Hannah Rossiter <hannah.rossiter@touchnetix.com>
> + *            Andrew Thomas <andrew.thomas@touchnetix.com>
> + *
> + * This program is free software; you can redistribute  it and/or modify it
> + * under  the terms of  the GNU General  Public License as published by the
> + * Free Software Foundation;  either version 2 of the  License, or (at your
> + * option) any later version.
> + *
> + */
> +
> +// #define DEBUG // Enable debug messages
> +
> +#include <linux/of.h>
> +#include <linux/kernel.h>
> +#include <linux/delay.h>
> +#include <linux/module.h>
> +#include <linux/spi/spi.h>
> +#include <linux/input.h>
> +#include "axiom_core.h"
> +
> +#define SPI_PADDING_LEN 32
> +
> +static int axiom_spi_transfer(struct device *dev, enum ax_comms_op_e op,
> +			      u16 addr, u16 length, void *values)
> +{
> +	int ret;
> +	struct spi_device *spi = to_spi_device(dev);
> +	struct spi_transfer xfr_header;
> +	struct spi_transfer xfr_padding;
> +	struct spi_transfer xfr_payload;
> +	struct spi_message msg;
> +	struct axiom_cmd_header cmd_header = { .target_address = addr,
> +					       .length = length,
> +					       .rd_wr = op };
> +	u8 pad_buf[SPI_PADDING_LEN] = { 0 };
> +
> +	memset(&xfr_header, 0, sizeof(xfr_header));
> +	memset(&xfr_padding, 0, sizeof(xfr_padding));
> +	memset(&xfr_payload, 0, sizeof(xfr_payload));
> +
> +	/* Setup the SPI transfer operations */
> +	xfr_header.tx_buf = &cmd_header;
> +	xfr_header.len = sizeof(cmd_header);
> +
> +	xfr_padding.tx_buf = pad_buf;
> +	xfr_padding.len = sizeof(pad_buf);
> +
> +	switch (op) {
> +	case AX_WR_OP:
> +		xfr_payload.tx_buf = values;
> +		break;
> +	case AX_RD_OP:
> +		xfr_payload.rx_buf = values;
> +		break;
> +	default:
> +		dev_err(dev, "%s: invalid operation: %d\n", __func__, op);
> +		return -EINVAL;
> +	}
> +	xfr_payload.len = length;
> +
> +	spi_message_init(&msg);
> +	spi_message_add_tail(&xfr_header, &msg);
> +	spi_message_add_tail(&xfr_padding, &msg);
> +	spi_message_add_tail(&xfr_payload, &msg);
> +
> +	ret = spi_sync(spi, &msg);
> +	if (ret < 0) {
> +		dev_err(&spi->dev, "Failed to SPI transfer, error: %d\n", ret);
> +		return ret;
> +	}
> +
> +	udelay(AXIOM_HOLDOFF_DELAY_US);
> +
> +	return 0;
> +}
> +
> +static int axiom_spi_read_block_data(struct device *dev, u16 addr, u16 length,
> +				     void *values)
> +{
> +	return axiom_spi_transfer(dev, AX_RD_OP, addr, length, values);
> +}
> +
> +static int axiom_spi_write_block_data(struct device *dev, u16 addr, u16 length,
> +				      void *values)
> +{
> +	return axiom_spi_transfer(dev, AX_WR_OP, addr, length, values);
> +}
> +
> +static const struct axiom_bus_ops axiom_spi_bus_ops = {
> +	.bustype = BUS_SPI,
> +	.write = axiom_spi_write_block_data,
> +	.read = axiom_spi_read_block_data,
> +};
> +
> +static int axiom_spi_probe(struct spi_device *spi)
> +{
> +	struct axiom *axiom;
> +	int error;
> +
> +	/* Set up SPI */
> +	spi->bits_per_word = 8;
> +	spi->mode = SPI_MODE_0;
> +	spi->max_speed_hz = 4000000;
> +
> +	if (spi->irq == 0)
> +		dev_err(&spi->dev, "No IRQ specified!\n");
> +
> +	error = spi_setup(spi);
> +	if (error < 0) {
> +		dev_err(&spi->dev, "%s: SPI setup error %d\n", __func__, error);
> +		return error;
> +	}
> +	axiom = axiom_probe(&axiom_spi_bus_ops, &spi->dev, spi->irq);
> +	if (IS_ERR(axiom))
> +		return dev_err_probe(&spi->dev, PTR_ERR(axiom),
> +				     "failed to register input device\n");
> +
> +	return 0;
> +}
> +
> +static const struct spi_device_id axiom_spi_id_table[] = {
> +	{ "axiom-spi" },
> +	{},
> +};
> +MODULE_DEVICE_TABLE(spi, axiom_spi_id_table);
> +
> +static const struct of_device_id axiom_spi_dt_ids[] = {
> +	{
> +		.compatible = "tnx,axiom-spi",
> +		.data = "axiom",
> +	},
> +	{}
> +};
> +MODULE_DEVICE_TABLE(of, axiom_spi_dt_ids);
> +
> +static struct spi_driver axiom_spi_driver = {
> +	.id_table = axiom_spi_id_table,
> +	.driver = {
> +		.name = "axiom_spi",
> +		.of_match_table = of_match_ptr(axiom_spi_dt_ids),
> +	},
> +	.probe = axiom_spi_probe,
> +};
> +
> +module_spi_driver(axiom_spi_driver);
> +
> +MODULE_AUTHOR("TouchNetix <support@touchnetix.com>");
> +MODULE_DESCRIPTION("aXiom touchscreen SPI bus driver");
> +MODULE_LICENSE("GPL");
> +MODULE_ALIAS("axiom");
> +MODULE_VERSION("1.0.0");
> -- 
> 2.43.0
> 
> 

-- 
#gernperDu 
#CallMeByMyFirstName

Pengutronix e.K.                           |                             |
Steuerwalder Str. 21                       | https://www.pengutronix.de/ |
31137 Hildesheim, Germany                  | Phone: +49-5121-206917-0    |
Amtsgericht Hildesheim, HRA 2686           | Fax:   +49-5121-206917-9    |

^ permalink raw reply

* Re: [PATCH 0/1] Input: Add TouchNetix aXiom touchscreen driver
From: Marco Felsch @ 2026-01-22 21:41 UTC (permalink / raw)
  To: AThomas63
  Cc: dmitry.torokhov, linux-input, linux-kernel, mark.satterthwaite,
	kamel.bouhara, kernel
In-Reply-To: <20260122124819.273188-1-andrew.thomas@touchnetix.com>

Hi Andrew,

thanks for your patch.

A few words prior doing a very rough review. I don't like the idea of
posting two complete different patchsets, furthermore if you could have
added the missing SPI support on-top of my patchset very easily since I
used the regmap-API.

Most of my patches already got the r-b tags and I received a very good
review from Dmitry recently which I currently integrating to send a new
version.

On 26-01-22, AThomas63 wrote:
> A Summary of the added features:
>  - Add input driver support for TouchNetix aXiom touchscreen controller 
> using either I2C or SPI.
>  - Support ABS_MT touch reports in axiom_process_u41_report().
>  - Support both polling and interrupt mode (trigger low).
>  - Add basic documentation and provide example device tree bindings.
>  - Provide the basic structure to add firmware and config download in
> the future via both I2C and SPI.
> 
> I understand the process is already under way to add a driver to support 
> aXiom by Marco at Pengutronix.
> I think it would be most useful for us to attempt to combine this patch
> in order to support both his changes for config/firmware download and 
> the patch here for SPI support along with I2C. I would be happy to help 
> with this process.
> This would allow TouchNetix to support a broader range of users of the
> aXiom device family.
> 
> I also have a device tree binding however I have not added it sincec
> from my understanding this should go in a different patch:
> Documentation/devicetree/bindings/input/touchscreen/tnx,axiom.yaml
>  
> My apologies if there are any obvious mistakes in this patch, this is the
> first patch that I have submitted.

That's not a problem at least not for me, but there are contriubtion
guidelines which you could have read first.

Also I'm a bit surprised that you have sent a driver for the same device
family which is less capable than mine. Your driver is missing the
complete fw-update mechanism and therefore needs still your downstream
python tooling. The fw-update logic is a huge part, since this was hard
to get it right.

There are a few other parts, you're doing wrong (please see the patch
review).

Regards,
  Marco


> 
> Many thanks for your time,
> Andrew Thomas
> 
> AThomas63 (1):
>   Adding support for aXiom touchscreen controller
> 
>  drivers/input/touchscreen/Kconfig      |  30 ++
>  drivers/input/touchscreen/Makefile     |   3 +
>  drivers/input/touchscreen/axiom_core.c | 482 +++++++++++++++++++++++++
>  drivers/input/touchscreen/axiom_core.h | 128 +++++++
>  drivers/input/touchscreen/axiom_i2c.c  | 152 ++++++++
>  drivers/input/touchscreen/axiom_spi.c  | 159 ++++++++
>  6 files changed, 954 insertions(+)
>  create mode 100644 drivers/input/touchscreen/axiom_core.c
>  create mode 100644 drivers/input/touchscreen/axiom_core.h
>  create mode 100644 drivers/input/touchscreen/axiom_i2c.c
>  create mode 100644 drivers/input/touchscreen/axiom_spi.c
> 
> -- 
> 2.43.0
> 
> 

-- 
#gernperDu 
#CallMeByMyFirstName

Pengutronix e.K.                           |                             |
Steuerwalder Str. 21                       | https://www.pengutronix.de/ |
31137 Hildesheim, Germany                  | Phone: +49-5121-206917-0    |
Amtsgericht Hildesheim, HRA 2686           | Fax:   +49-5121-206917-9    |

^ permalink raw reply


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