Linux Input/HID development
 help / color / mirror / Atom feed
* [PATCH] HID: ayaneo: Add AYANEO 3 detachable controller driver
@ 2026-08-24 21:50 Matías Martínez
  2026-08-24 22:00 ` Antheas Kapenekakis
                   ` (2 more replies)
  0 siblings, 3 replies; 10+ messages in thread
From: Matías Martínez @ 2026-08-24 21:50 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Antheas Kapenekakis, Denis Benato,
	Matías Martínez

The AYANEO 3 handheld has a detachable controller with swappable
modules ("Magic Modules"). The controller exposes three USB HID
interfaces behind 1c4f:0002 (a generic SigmaMicro VID/PID, hence the
DMI gate): a gamepad, a keyboard for the extra buttons, and a vendor
interface accepting 65-byte commands.

Add a driver for the vendor interface providing module identification
(module_left/module_right sysfs attributes), software eject of the
modules (eject sysfs attribute, blocking until the firmware confirms
the release handshake), and RGB control of the joystick rings as a
multicolor LED class device ("<device name>:rgb:joystick_rings";
userspace such as InputPlumber matches the function suffix).

This complements the ayaneo-ec platform driver, which exposes module
attach state and controller power. A full physical eject is performed
by writing to eject and then cutting power through ayaneo-ec's
controller_power attribute; that orchestration is deliberately left
to userspace.

The protocol was reverse engineered in the Handheld Daemon project by
Antheas Kapenekakis. Tested on an AYANEO 3: module identification,
RGB, and a full eject/reinsert/repower cycle.

Signed-off-by: Matías Martínez <hello@matias.me>
Reviewed-by: Denis Benato <denis.benato@linux.dev>
---
 .../ABI/testing/sysfs-driver-hid-ayaneo       |  36 ++
 MAINTAINERS                                   |   7 +
 drivers/hid/Kconfig                           |  14 +
 drivers/hid/Makefile                          |   1 +
 drivers/hid/hid-ayaneo.c                      | 471 ++++++++++++++++++
 5 files changed, 529 insertions(+)
 create mode 100644 Documentation/ABI/testing/sysfs-driver-hid-ayaneo
 create mode 100644 drivers/hid/hid-ayaneo.c

diff --git a/Documentation/ABI/testing/sysfs-driver-hid-ayaneo b/Documentation/ABI/testing/sysfs-driver-hid-ayaneo
new file mode 100644
index 000000000..807c4fc9d
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-driver-hid-ayaneo
@@ -0,0 +1,36 @@
+What:		/sys/bus/hid/drivers/hid-ayaneo/<dev>/module_left
+What:		/sys/bus/hid/drivers/hid-ayaneo/<dev>/module_right
+Date:		August 2026
+KernelVersion:	7.3
+Contact:	Matías Martínez <hello@matias.me>
+Description:
+		Reports the type of the module currently inserted in the
+		left/right slot of the AYANEO 3 detachable controller, as
+		the raw identifier reported by the controller firmware in
+		hexadecimal (e.g. "0x04"). Bits 0-5 encode the module
+		type, bit 6 indicates the module is inserted rotated.
+
+		Reading these attributes queries the controller and can
+		take up to a second.
+
+What:		/sys/bus/hid/drivers/hid-ayaneo/<dev>/eject
+Date:		August 2026
+KernelVersion:	7.3
+Contact:	Matías Martínez <hello@matias.me>
+Description:
+		Write-only. Writing "left", "right" or "both" asks the
+		controller firmware to release the corresponding
+		module(s). The write blocks until the firmware confirms
+		the release handshake (typically a few seconds). The
+		module is physically released once controller power is
+		subsequently cut through the ayaneo-ec platform driver's
+		controller_power attribute; that final step is left to
+		userspace.
+
+What:		/sys/bus/hid/drivers/hid-ayaneo/<dev>/reset
+Date:		August 2026
+KernelVersion:	7.3
+Contact:	Matías Martínez <hello@matias.me>
+Description:
+		Write-only. Writing "1" asks the controller firmware to
+		perform a quick reset of the controller configuration.
diff --git a/MAINTAINERS b/MAINTAINERS
index 6ecfe6c9e..360f01d39 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -4469,6 +4469,13 @@ F:	Documentation/devicetree/bindings/spi/axiado,ax3000-spi.yaml
 F:	drivers/spi/spi-axiado.c
 F:	drivers/spi/spi-axiado.h
 
+AYANEO 3 CONTROLLER HID DRIVER
+M:	Matías Martínez <hello@matias.me>
+L:	linux-input@vger.kernel.org
+S:	Maintained
+F:	Documentation/ABI/testing/sysfs-driver-hid-ayaneo
+F:	drivers/hid/hid-ayaneo.c
+
 AYANEO PLATFORM EC DRIVER
 M:	Antheas Kapenekakis <lkml@antheas.dev>
 L:	platform-driver-x86@vger.kernel.org
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index aa7fa11a0..9693a0233 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -205,6 +205,20 @@ config HID_AUREAL
 	help
 	Support for Aureal Cy se W-01RN Remote Controller and other Aureal derived remotes.
 
+config HID_AYANEO
+	tristate "AYANEO 3 detachable controller support"
+	depends on USB_HID
+	depends on DMI
+	depends on LEDS_CLASS_MULTICOLOR
+	help
+	  Provides support for the detachable controller ("Magic Modules")
+	  of the AYANEO 3 handheld: module identification, software eject
+	  and RGB control of the joystick rings. Complements the ayaneo-ec
+	  platform driver, which handles module attach state and controller
+	  power.
+
+	  Say Y or M here if you have an AYANEO 3.
+
 config HID_BELKIN
 	tristate "Belkin Flip KVM and Wireless keyboard"
 	help
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index 48a863b24..60add9348 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -35,6 +35,7 @@ obj-$(CONFIG_HID_APPLETB_KBD)	+= hid-appletb-kbd.o
 obj-$(CONFIG_HID_CREATIVE_SB0540)	+= hid-creative-sb0540.o
 obj-$(CONFIG_HID_ASUS)		+= hid-asus.o
 obj-$(CONFIG_HID_AUREAL)	+= hid-aureal.o
+obj-$(CONFIG_HID_AYANEO)	+= hid-ayaneo.o
 obj-$(CONFIG_HID_BELKIN)	+= hid-belkin.o
 obj-$(CONFIG_HID_BETOP_FF)	+= hid-betopff.o
 obj-$(CONFIG_HID_BIGBEN_FF)	+= hid-bigbenff.o
diff --git a/drivers/hid/hid-ayaneo.c b/drivers/hid/hid-ayaneo.c
new file mode 100644
index 000000000..61c64617f
--- /dev/null
+++ b/drivers/hid/hid-ayaneo.c
@@ -0,0 +1,471 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * HID driver for the AYANEO 3 detachable controller ("Magic Modules").
+ *
+ * The AYANEO 3 controller exposes three USB HID interfaces behind
+ * VID 0x1c4f PID 0x0002 (a generic SigmaMicro ID, hence the DMI gate):
+ * a gamepad, a keyboard for the extra buttons, and a vendor interface
+ * (application usage 0xff000001) accepting 65-byte commands.
+ *
+ * This driver binds the vendor interface and provides:
+ *  - module identification (which module type is inserted on each side)
+ *  - software eject of the left/right modules
+ *  - RGB control of the joystick rings as a multicolor LED class device
+ *
+ * It complements the ayaneo-ec platform driver, which exposes module
+ * attach state and controller power. A full eject is: write to this
+ * driver's "eject" attribute, then power the controller off through
+ * ayaneo-ec's controller_power once the eject completes.
+ *
+ * The protocol was reverse engineered in the Handheld Daemon project by
+ * Antheas Kapenekakis.
+ *
+ * Command format (65 bytes, unnumbered report):
+ *   [0]   report id (0)
+ *   [1:3] little-endian sum of bytes 7..64
+ *   [3]   command
+ *   [4]   subcommand
+ *   [5:]  payload
+ * The device replies with a 64-byte report echoing the subcommand at
+ * byte 3.
+ *
+ * Copyright (C) 2026 Matías Martínez <hello@matias.me>
+ */
+
+#include <linux/delay.h>
+#include <linux/dmi.h>
+#include <linux/hid.h>
+#include <linux/led-class-multicolor.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/sysfs.h>
+#include <linux/unaligned.h>
+
+#define AYA3_REPORT_SIZE	65
+#define AYA3_RESP_SIZE		64
+#define AYA3_CMD_TIMEOUT_MS	300
+#define AYA3_CMD_ATTEMPTS	3
+
+/* Subcommands (byte 4); byte 3 is 0x00 except for the config command */
+#define AYA3_SUBCMD_CHECK	0x08
+#define AYA3_CMD_CONFIG		0x21
+#define AYA3_SUBCMD_CONFIG	0x09
+
+/* CHECK response fields */
+#define AYA3_RESP_CMD		3
+#define AYA3_RESP_EJECT_STATUS	19
+#define AYA3_RESP_MODULE_LEFT	32
+#define AYA3_RESP_MODULE_RIGHT	33
+/* Bits that stay set in the eject status byte after an eject completes */
+#define AYA3_EJECT_DONE_MASK	0x11
+
+/* Config command eject/reset field */
+#define AYA3_EJECT_LEFT		0x07
+#define AYA3_EJECT_RIGHT	0x70
+#define AYA3_RESET		0x88
+
+/* Config command RGB modes */
+#define AYA3_RGB_SOLID		0x01
+#define AYA3_RGB_OFF		0xff
+
+#define AYA3_VIBRATION_DEFAULT	0x02	/* medium */
+
+struct aya3 {
+	struct hid_device *hdev;
+	/* DMA-safe command buffer; guarded by lock */
+	u8 *xfer;
+	/* Serializes commands and cached-config access */
+	struct mutex lock;
+	struct completion resp_done;
+	u8 resp[AYA3_RESP_SIZE];
+	u8 resp_expect;
+	bool resp_pending;
+
+	u8 rgb[3];
+	u8 vibration;
+
+	struct led_classdev_mc mcled;
+	struct mc_subled subleds[3];
+};
+
+static int aya3_send(struct aya3 *aya)
+{
+	int ret;
+
+	ret = hid_hw_output_report(aya->hdev, aya->xfer, AYA3_REPORT_SIZE);
+	if (ret == -ENOSYS)
+		ret = hid_hw_raw_request(aya->hdev, aya->xfer[0], aya->xfer,
+					 AYA3_REPORT_SIZE, HID_OUTPUT_REPORT,
+					 HID_REQ_SET_REPORT);
+	if (ret < 0)
+		return ret;
+	return 0;
+}
+
+/**
+ * aya3_cmd() - send the command in aya->xfer and wait for the reply
+ * @aya: driver data; @aya->xfer holds the fully built 65-byte command
+ * @resp: destination for the AYA3_RESP_SIZE-byte reply, or NULL to
+ *        discard it
+ *
+ * The device echoes the subcommand byte of the command it is answering,
+ * which aya3_raw_event() uses to match replies. Unanswered commands are
+ * retried up to AYA3_CMD_ATTEMPTS times.
+ *
+ * Context: process context; the caller must hold @aya->lock, which
+ *          protects @aya->xfer and the reply state.
+ * Return: 0 on success, -ETIMEDOUT if every attempt went unanswered, or
+ *         a negative errno if sending failed.
+ */
+static int aya3_cmd(struct aya3 *aya, u8 *resp)
+{
+	int attempt, ret;
+
+	lockdep_assert_held(&aya->lock);
+
+	for (attempt = 0; attempt < AYA3_CMD_ATTEMPTS; attempt++) {
+		reinit_completion(&aya->resp_done);
+		aya->resp_expect = aya->xfer[4];
+		WRITE_ONCE(aya->resp_pending, true);
+
+		ret = aya3_send(aya);
+		if (ret) {
+			WRITE_ONCE(aya->resp_pending, false);
+			return ret;
+		}
+
+		if (wait_for_completion_timeout(&aya->resp_done,
+						msecs_to_jiffies(AYA3_CMD_TIMEOUT_MS))) {
+			if (resp)
+				memcpy(resp, aya->resp, AYA3_RESP_SIZE);
+			return 0;
+		}
+	}
+	WRITE_ONCE(aya->resp_pending, false);
+	return -ETIMEDOUT;
+}
+
+static void aya3_checksum(u8 *buf)
+{
+	u16 sum = 0;
+	int i;
+
+	for (i = 7; i < AYA3_REPORT_SIZE; i++)
+		sum += buf[i];
+	put_unaligned_le16(sum, buf + 1);
+}
+
+static int aya3_check(struct aya3 *aya, u8 *resp)
+{
+	memset(aya->xfer, 0, AYA3_REPORT_SIZE);
+	aya->xfer[4] = AYA3_SUBCMD_CHECK;
+	return aya3_cmd(aya, resp);
+}
+
+/*
+ * The config command sets everything at once: RGB for both rings,
+ * vibration strength, joystick sensitivity, and the eject/reset field.
+ */
+static int aya3_send_config(struct aya3 *aya, u8 eject)
+{
+	static const u8 template[AYA3_REPORT_SIZE] = {
+		[3] = AYA3_CMD_CONFIG,
+		[4] = AYA3_SUBCMD_CONFIG,
+		[22] = 0x33,
+		[23] = 0x22,	/* joystick sensitivity 100%/100% */
+		[32] = 0x01,
+		[37] = 0x64,
+		[38] = 0x64,
+	};
+	u8 *buf = aya->xfer;
+	u8 mode = (aya->rgb[0] || aya->rgb[1] || aya->rgb[2]) ?
+		  AYA3_RGB_SOLID : AYA3_RGB_OFF;
+
+	memcpy(buf, template, AYA3_REPORT_SIZE);
+	/* Right ring, then left ring: mode, R, G, B */
+	buf[8] = mode;
+	memcpy(buf + 9, aya->rgb, 3);
+	buf[12] = mode;
+	memcpy(buf + 13, aya->rgb, 3);
+	buf[20] = eject;
+	buf[24] = aya->vibration << 4;
+	aya3_checksum(buf);
+
+	return aya3_cmd(aya, NULL);
+}
+
+static int aya3_raw_event(struct hid_device *hdev, struct hid_report *report,
+			  u8 *data, int size)
+{
+	struct aya3 *aya = hid_get_drvdata(hdev);
+
+	if (!READ_ONCE(aya->resp_pending) || size < AYA3_RESP_SIZE)
+		return 0;
+	if (data[AYA3_RESP_CMD] != aya->resp_expect)
+		return 0;
+
+	memcpy(aya->resp, data, AYA3_RESP_SIZE);
+	WRITE_ONCE(aya->resp_pending, false);
+	complete(&aya->resp_done);
+	return 0;
+}
+
+static ssize_t aya3_module_show(struct device *dev, char *buf, int offset)
+{
+	struct aya3 *aya = dev_get_drvdata(dev);
+	u8 resp[AYA3_RESP_SIZE];
+	int ret;
+
+	ret = mutex_lock_interruptible(&aya->lock);
+	if (ret)
+		return ret;
+	ret = aya3_check(aya, resp);
+	mutex_unlock(&aya->lock);
+	if (ret)
+		return ret;
+
+	return sysfs_emit(buf, "0x%02x\n", resp[offset]);
+}
+
+static ssize_t module_left_show(struct device *dev,
+				struct device_attribute *attr, char *buf)
+{
+	return aya3_module_show(dev, buf, AYA3_RESP_MODULE_LEFT);
+}
+static DEVICE_ATTR_RO(module_left);
+
+static ssize_t module_right_show(struct device *dev,
+				 struct device_attribute *attr, char *buf)
+{
+	return aya3_module_show(dev, buf, AYA3_RESP_MODULE_RIGHT);
+}
+static DEVICE_ATTR_RO(module_right);
+
+static ssize_t eject_store(struct device *dev, struct device_attribute *attr,
+			   const char *buf, size_t count)
+{
+	struct aya3 *aya = dev_get_drvdata(dev);
+	u8 resp[AYA3_RESP_SIZE];
+	u8 eject;
+	int ret, i;
+
+	if (sysfs_streq(buf, "left"))
+		eject = AYA3_EJECT_LEFT;
+	else if (sysfs_streq(buf, "right"))
+		eject = AYA3_EJECT_RIGHT;
+	else if (sysfs_streq(buf, "both"))
+		eject = AYA3_EJECT_LEFT | AYA3_EJECT_RIGHT;
+	else
+		return -EINVAL;
+
+	ret = mutex_lock_interruptible(&aya->lock);
+	if (ret)
+		return ret;
+
+	ret = aya3_send_config(aya, eject);
+	if (ret)
+		goto out;
+
+	/*
+	 * Wait for the firmware to report the eject as done. Userspace
+	 * must then cut power through ayaneo-ec's controller_power for
+	 * the module to be physically released.
+	 */
+	ret = -ETIMEDOUT;
+	for (i = 0; i < 20; i++) {
+		msleep(400);
+		if (aya3_check(aya, resp))
+			continue;
+		if (!(resp[AYA3_RESP_EJECT_STATUS] & ~AYA3_EJECT_DONE_MASK)) {
+			ret = 0;
+			break;
+		}
+	}
+out:
+	mutex_unlock(&aya->lock);
+	return ret ? ret : count;
+}
+static DEVICE_ATTR_WO(eject);
+
+static ssize_t reset_store(struct device *dev, struct device_attribute *attr,
+			   const char *buf, size_t count)
+{
+	struct aya3 *aya = dev_get_drvdata(dev);
+	bool value;
+	int ret;
+
+	ret = kstrtobool(buf, &value);
+	if (ret)
+		return ret;
+	if (!value)
+		return count;
+
+	ret = mutex_lock_interruptible(&aya->lock);
+	if (ret)
+		return ret;
+	ret = aya3_send_config(aya, AYA3_RESET);
+	if (!ret) {
+		msleep(500);
+		ret = aya3_send_config(aya, 0);
+	}
+	mutex_unlock(&aya->lock);
+	return ret ? ret : count;
+}
+static DEVICE_ATTR_WO(reset);
+
+static struct attribute *aya3_attrs[] = {
+	&dev_attr_module_left.attr,
+	&dev_attr_module_right.attr,
+	&dev_attr_eject.attr,
+	&dev_attr_reset.attr,
+	NULL
+};
+ATTRIBUTE_GROUPS(aya3);
+
+static int aya3_led_set(struct led_classdev *cdev, enum led_brightness value)
+{
+	struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
+	struct aya3 *aya = container_of(mc, struct aya3, mcled);
+	int ret, i;
+
+	ret = mutex_lock_interruptible(&aya->lock);
+	if (ret)
+		return ret;
+
+	led_mc_calc_color_components(mc, value);
+	for (i = 0; i < 3; i++)
+		aya->rgb[i] = min_t(unsigned int, aya->subleds[i].brightness, 255);
+
+	ret = aya3_send_config(aya, 0);
+	if (ret)
+		hid_err(aya->hdev, "failed to update RGB config: %d\n", ret);
+	mutex_unlock(&aya->lock);
+	return ret;
+}
+
+static int aya3_register_led(struct aya3 *aya)
+{
+	struct led_classdev *cdev = &aya->mcled.led_cdev;
+
+	aya->subleds[0].color_index = LED_COLOR_ID_RED;
+	aya->subleds[1].color_index = LED_COLOR_ID_GREEN;
+	aya->subleds[2].color_index = LED_COLOR_ID_BLUE;
+	aya->mcled.subled_info = aya->subleds;
+	aya->mcled.num_colors = 3;
+
+	cdev->name = devm_kasprintf(&aya->hdev->dev, GFP_KERNEL,
+				    "%s:rgb:joystick_rings",
+				    dev_name(&aya->hdev->dev));
+	if (!cdev->name)
+		return -ENOMEM;
+	cdev->brightness = 0;
+	cdev->max_brightness = 255;
+	cdev->brightness_set_blocking = aya3_led_set;
+
+	return devm_led_classdev_multicolor_register(&aya->hdev->dev,
+						     &aya->mcled);
+}
+
+static const struct dmi_system_id aya3_dmi_table[] = {
+	{
+		.matches = {
+			DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
+			DMI_MATCH(DMI_BOARD_NAME, "AYANEO 3"),
+		},
+	},
+	{}
+};
+
+static int aya3_probe(struct hid_device *hdev, const struct hid_device_id *id)
+{
+	struct aya3 *aya;
+	int ret;
+
+	/* The VID/PID is a generic SigmaMicro ID; bind on AYANEO 3 only */
+	if (!dmi_check_system(aya3_dmi_table))
+		return -ENODEV;
+
+	if (!hid_is_usb(hdev))
+		return -ENODEV;
+
+	ret = hid_parse(hdev);
+	if (ret)
+		return ret;
+
+	/* Bind only the vendor interface, not the gamepad/keyboard ones */
+	if (hdev->collection->usage != (HID_UP_MSVENDOR | 0x0001))
+		return -ENODEV;
+
+	aya = devm_kzalloc(&hdev->dev, sizeof(*aya), GFP_KERNEL);
+	if (!aya)
+		return -ENOMEM;
+
+	aya->xfer = devm_kzalloc(&hdev->dev, AYA3_REPORT_SIZE, GFP_KERNEL);
+	if (!aya->xfer)
+		return -ENOMEM;
+
+	aya->hdev = hdev;
+	aya->vibration = AYA3_VIBRATION_DEFAULT;
+	init_completion(&aya->resp_done);
+	ret = devm_mutex_init(&hdev->dev, &aya->lock);
+	if (ret)
+		return ret;
+	hid_set_drvdata(hdev, aya);
+
+	ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW);
+	if (ret)
+		return ret;
+
+	ret = hid_hw_open(hdev);
+	if (ret)
+		goto err_stop;
+
+	/* Input reports are not delivered during probe by default */
+	hid_device_io_start(hdev);
+
+	scoped_guard(mutex, &aya->lock)
+		ret = aya3_check(aya, NULL);
+	if (ret)
+		hid_warn(hdev, "controller did not answer status check: %d\n",
+			 ret);
+
+	ret = aya3_register_led(aya);
+	if (ret)
+		goto err_close;
+
+	return 0;
+
+err_close:
+	hid_hw_close(hdev);
+err_stop:
+	hid_hw_stop(hdev);
+	return ret;
+}
+
+static void aya3_remove(struct hid_device *hdev)
+{
+	hid_hw_close(hdev);
+	hid_hw_stop(hdev);
+}
+
+static const struct hid_device_id aya3_devices[] = {
+	{ HID_USB_DEVICE(0x1c4f, 0x0002) },
+	{}
+};
+MODULE_DEVICE_TABLE(hid, aya3_devices);
+
+static struct hid_driver aya3_driver = {
+	.name = "hid-ayaneo",
+	.id_table = aya3_devices,
+	.probe = aya3_probe,
+	.remove = aya3_remove,
+	.raw_event = aya3_raw_event,
+	.driver = {
+		.dev_groups = aya3_groups,
+	},
+};
+module_hid_driver(aya3_driver);
+
+MODULE_AUTHOR("Matías Martínez <hello@matias.me>");
+MODULE_DESCRIPTION("AYANEO 3 detachable controller driver");
+MODULE_LICENSE("GPL");

base-commit: a8fcb3dbf9024da44f1614c42ea16001f4b860b0
-- 
2.54.0 (Apple Git-157)


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* Re: [PATCH] HID: ayaneo: Add AYANEO 3 detachable controller driver
  2026-08-24 21:50 [PATCH] HID: ayaneo: Add AYANEO 3 detachable controller driver Matías Martínez
@ 2026-08-24 22:00 ` Antheas Kapenekakis
  2026-08-24 22:25   ` Antheas Kapenekakis
  2026-08-24 22:01 ` sashiko-bot
  2026-08-24 22:31 ` [PATCH v2] " Matías Martínez
  2 siblings, 1 reply; 10+ messages in thread
From: Antheas Kapenekakis @ 2026-08-24 22:00 UTC (permalink / raw)
  To: Matías Martínez
  Cc: Jiri Kosina, Benjamin Tissoires, linux-input, linux-kernel,
	Denis Benato

On Mon, 24 Aug 2026 at 23:50, Matías Martínez <hello@matias.me> wrote:
>
> The AYANEO 3 handheld has a detachable controller with swappable
> modules ("Magic Modules"). The controller exposes three USB HID
> interfaces behind 1c4f:0002 (a generic SigmaMicro VID/PID, hence the
> DMI gate): a gamepad, a keyboard for the extra buttons, and a vendor
> interface accepting 65-byte commands.
>
> Add a driver for the vendor interface providing module identification
> (module_left/module_right sysfs attributes), software eject of the
> modules (eject sysfs attribute, blocking until the firmware confirms
> the release handshake), and RGB control of the joystick rings as a
> multicolor LED class device ("<device name>:rgb:joystick_rings";
> userspace such as InputPlumber matches the function suffix).
>
> This complements the ayaneo-ec platform driver, which exposes module
> attach state and controller power. A full physical eject is performed
> by writing to eject and then cutting power through ayaneo-ec's
> controller_power attribute; that orchestration is deliberately left
> to userspace.

If you need userspace coordination anyway, including the multiple
timing hacks I had to implement, the question of having to route this
through the kernel arises.

> The protocol was reverse engineered in the Handheld Daemon project by
> Antheas Kapenekakis. Tested on an AYANEO 3: module identification,
> RGB, and a full eject/reinsert/repower cycle.
>
> Signed-off-by: Matías Martínez <hello@matias.me>
> Reviewed-by: Denis Benato <denis.benato@linux.dev>
> ---
>  .../ABI/testing/sysfs-driver-hid-ayaneo       |  36 ++
>  MAINTAINERS                                   |   7 +
>  drivers/hid/Kconfig                           |  14 +
>  drivers/hid/Makefile                          |   1 +
>  drivers/hid/hid-ayaneo.c                      | 471 ++++++++++++++++++
>  5 files changed, 529 insertions(+)
>  create mode 100644 Documentation/ABI/testing/sysfs-driver-hid-ayaneo
>  create mode 100644 drivers/hid/hid-ayaneo.c
>
> diff --git a/Documentation/ABI/testing/sysfs-driver-hid-ayaneo b/Documentation/ABI/testing/sysfs-driver-hid-ayaneo
> new file mode 100644
> index 000000000..807c4fc9d
> --- /dev/null
> +++ b/Documentation/ABI/testing/sysfs-driver-hid-ayaneo
> @@ -0,0 +1,36 @@
> +What:          /sys/bus/hid/drivers/hid-ayaneo/<dev>/module_left
> +What:          /sys/bus/hid/drivers/hid-ayaneo/<dev>/module_right
> +Date:          August 2026
> +KernelVersion: 7.3
> +Contact:       Matías Martínez <hello@matias.me>
> +Description:
> +               Reports the type of the module currently inserted in the
> +               left/right slot of the AYANEO 3 detachable controller, as
> +               the raw identifier reported by the controller firmware in
> +               hexadecimal (e.g. "0x04"). Bits 0-5 encode the module
> +               type, bit 6 indicates the module is inserted rotated.
> +
> +               Reading these attributes queries the controller and can
> +               take up to a second.
> +
> +What:          /sys/bus/hid/drivers/hid-ayaneo/<dev>/eject
> +Date:          August 2026
> +KernelVersion: 7.3
> +Contact:       Matías Martínez <hello@matias.me>
> +Description:
> +               Write-only. Writing "left", "right" or "both" asks the
> +               controller firmware to release the corresponding
> +               module(s). The write blocks until the firmware confirms
> +               the release handshake (typically a few seconds). The
> +               module is physically released once controller power is
> +               subsequently cut through the ayaneo-ec platform driver's
> +               controller_power attribute; that final step is left to
> +               userspace.
> +
> +What:          /sys/bus/hid/drivers/hid-ayaneo/<dev>/reset
> +Date:          August 2026
> +KernelVersion: 7.3
> +Contact:       Matías Martínez <hello@matias.me>
> +Description:
> +               Write-only. Writing "1" asks the controller firmware to
> +               perform a quick reset of the controller configuration.
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 6ecfe6c9e..360f01d39 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -4469,6 +4469,13 @@ F:       Documentation/devicetree/bindings/spi/axiado,ax3000-spi.yaml
>  F:     drivers/spi/spi-axiado.c
>  F:     drivers/spi/spi-axiado.h
>
> +AYANEO 3 CONTROLLER HID DRIVER
> +M:     Matías Martínez <hello@matias.me>
> +L:     linux-input@vger.kernel.org
> +S:     Maintained
> +F:     Documentation/ABI/testing/sysfs-driver-hid-ayaneo
> +F:     drivers/hid/hid-ayaneo.c
> +
>  AYANEO PLATFORM EC DRIVER
>  M:     Antheas Kapenekakis <lkml@antheas.dev>
>  L:     platform-driver-x86@vger.kernel.org
> diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
> index aa7fa11a0..9693a0233 100644
> --- a/drivers/hid/Kconfig
> +++ b/drivers/hid/Kconfig
> @@ -205,6 +205,20 @@ config HID_AUREAL
>         help
>         Support for Aureal Cy se W-01RN Remote Controller and other Aureal derived remotes.
>
> +config HID_AYANEO
> +       tristate "AYANEO 3 detachable controller support"
> +       depends on USB_HID
> +       depends on DMI
> +       depends on LEDS_CLASS_MULTICOLOR
> +       help
> +         Provides support for the detachable controller ("Magic Modules")
> +         of the AYANEO 3 handheld: module identification, software eject
> +         and RGB control of the joystick rings. Complements the ayaneo-ec
> +         platform driver, which handles module attach state and controller
> +         power.
> +
> +         Say Y or M here if you have an AYANEO 3.
> +
>  config HID_BELKIN
>         tristate "Belkin Flip KVM and Wireless keyboard"
>         help
> diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
> index 48a863b24..60add9348 100644
> --- a/drivers/hid/Makefile
> +++ b/drivers/hid/Makefile
> @@ -35,6 +35,7 @@ obj-$(CONFIG_HID_APPLETB_KBD) += hid-appletb-kbd.o
>  obj-$(CONFIG_HID_CREATIVE_SB0540)      += hid-creative-sb0540.o
>  obj-$(CONFIG_HID_ASUS)         += hid-asus.o
>  obj-$(CONFIG_HID_AUREAL)       += hid-aureal.o
> +obj-$(CONFIG_HID_AYANEO)       += hid-ayaneo.o
>  obj-$(CONFIG_HID_BELKIN)       += hid-belkin.o
>  obj-$(CONFIG_HID_BETOP_FF)     += hid-betopff.o
>  obj-$(CONFIG_HID_BIGBEN_FF)    += hid-bigbenff.o
> diff --git a/drivers/hid/hid-ayaneo.c b/drivers/hid/hid-ayaneo.c
> new file mode 100644
> index 000000000..61c64617f
> --- /dev/null
> +++ b/drivers/hid/hid-ayaneo.c
> @@ -0,0 +1,471 @@
> +// SPDX-License-Identifier: GPL-2.0+
> +/*
> + * HID driver for the AYANEO 3 detachable controller ("Magic Modules").
> + *
> + * The AYANEO 3 controller exposes three USB HID interfaces behind
> + * VID 0x1c4f PID 0x0002 (a generic SigmaMicro ID, hence the DMI gate):
> + * a gamepad, a keyboard for the extra buttons, and a vendor interface
> + * (application usage 0xff000001) accepting 65-byte commands.
> + *
> + * This driver binds the vendor interface and provides:
> + *  - module identification (which module type is inserted on each side)
> + *  - software eject of the left/right modules
> + *  - RGB control of the joystick rings as a multicolor LED class device
> + *
> + * It complements the ayaneo-ec platform driver, which exposes module
> + * attach state and controller power. A full eject is: write to this
> + * driver's "eject" attribute, then power the controller off through
> + * ayaneo-ec's controller_power once the eject completes.
> + *
> + * The protocol was reverse engineered in the Handheld Daemon project by
> + * Antheas Kapenekakis.
> + *
> + * Command format (65 bytes, unnumbered report):
> + *   [0]   report id (0)
> + *   [1:3] little-endian sum of bytes 7..64
> + *   [3]   command
> + *   [4]   subcommand
> + *   [5:]  payload
> + * The device replies with a 64-byte report echoing the subcommand at
> + * byte 3.
> + *
> + * Copyright (C) 2026 Matías Martínez <hello@matias.me>
> + */
> +
> +#include <linux/delay.h>
> +#include <linux/dmi.h>
> +#include <linux/hid.h>
> +#include <linux/led-class-multicolor.h>
> +#include <linux/module.h>
> +#include <linux/mutex.h>
> +#include <linux/sysfs.h>
> +#include <linux/unaligned.h>
> +
> +#define AYA3_REPORT_SIZE       65
> +#define AYA3_RESP_SIZE         64
> +#define AYA3_CMD_TIMEOUT_MS    300
> +#define AYA3_CMD_ATTEMPTS      3
> +
> +/* Subcommands (byte 4); byte 3 is 0x00 except for the config command */
> +#define AYA3_SUBCMD_CHECK      0x08
> +#define AYA3_CMD_CONFIG                0x21
> +#define AYA3_SUBCMD_CONFIG     0x09
> +
> +/* CHECK response fields */
> +#define AYA3_RESP_CMD          3
> +#define AYA3_RESP_EJECT_STATUS 19
> +#define AYA3_RESP_MODULE_LEFT  32
> +#define AYA3_RESP_MODULE_RIGHT 33
> +/* Bits that stay set in the eject status byte after an eject completes */
> +#define AYA3_EJECT_DONE_MASK   0x11
> +
> +/* Config command eject/reset field */
> +#define AYA3_EJECT_LEFT                0x07
> +#define AYA3_EJECT_RIGHT       0x70
> +#define AYA3_RESET             0x88
> +
> +/* Config command RGB modes */
> +#define AYA3_RGB_SOLID         0x01
> +#define AYA3_RGB_OFF           0xff
> +
> +#define AYA3_VIBRATION_DEFAULT 0x02    /* medium */
> +
> +struct aya3 {
> +       struct hid_device *hdev;
> +       /* DMA-safe command buffer; guarded by lock */
> +       u8 *xfer;
> +       /* Serializes commands and cached-config access */
> +       struct mutex lock;
> +       struct completion resp_done;
> +       u8 resp[AYA3_RESP_SIZE];
> +       u8 resp_expect;
> +       bool resp_pending;
> +
> +       u8 rgb[3];
> +       u8 vibration;
> +
> +       struct led_classdev_mc mcled;
> +       struct mc_subled subleds[3];
> +};
> +
> +static int aya3_send(struct aya3 *aya)
> +{
> +       int ret;
> +
> +       ret = hid_hw_output_report(aya->hdev, aya->xfer, AYA3_REPORT_SIZE);
> +       if (ret == -ENOSYS)
> +               ret = hid_hw_raw_request(aya->hdev, aya->xfer[0], aya->xfer,
> +                                        AYA3_REPORT_SIZE, HID_OUTPUT_REPORT,
> +                                        HID_REQ_SET_REPORT);
> +       if (ret < 0)
> +               return ret;
> +       return 0;
> +}
> +
> +/**
> + * aya3_cmd() - send the command in aya->xfer and wait for the reply
> + * @aya: driver data; @aya->xfer holds the fully built 65-byte command
> + * @resp: destination for the AYA3_RESP_SIZE-byte reply, or NULL to
> + *        discard it
> + *
> + * The device echoes the subcommand byte of the command it is answering,
> + * which aya3_raw_event() uses to match replies. Unanswered commands are
> + * retried up to AYA3_CMD_ATTEMPTS times.
> + *
> + * Context: process context; the caller must hold @aya->lock, which
> + *          protects @aya->xfer and the reply state.
> + * Return: 0 on success, -ETIMEDOUT if every attempt went unanswered, or
> + *         a negative errno if sending failed.
> + */
> +static int aya3_cmd(struct aya3 *aya, u8 *resp)
> +{
> +       int attempt, ret;
> +
> +       lockdep_assert_held(&aya->lock);
> +
> +       for (attempt = 0; attempt < AYA3_CMD_ATTEMPTS; attempt++) {
> +               reinit_completion(&aya->resp_done);
> +               aya->resp_expect = aya->xfer[4];
> +               WRITE_ONCE(aya->resp_pending, true);
> +
> +               ret = aya3_send(aya);
> +               if (ret) {
> +                       WRITE_ONCE(aya->resp_pending, false);
> +                       return ret;
> +               }
> +
> +               if (wait_for_completion_timeout(&aya->resp_done,
> +                                               msecs_to_jiffies(AYA3_CMD_TIMEOUT_MS))) {
> +                       if (resp)
> +                               memcpy(resp, aya->resp, AYA3_RESP_SIZE);
> +                       return 0;
> +               }
> +       }
> +       WRITE_ONCE(aya->resp_pending, false);
> +       return -ETIMEDOUT;
> +}
> +
> +static void aya3_checksum(u8 *buf)
> +{
> +       u16 sum = 0;
> +       int i;
> +
> +       for (i = 7; i < AYA3_REPORT_SIZE; i++)
> +               sum += buf[i];
> +       put_unaligned_le16(sum, buf + 1);
> +}
> +
> +static int aya3_check(struct aya3 *aya, u8 *resp)
> +{
> +       memset(aya->xfer, 0, AYA3_REPORT_SIZE);
> +       aya->xfer[4] = AYA3_SUBCMD_CHECK;
> +       return aya3_cmd(aya, resp);
> +}
> +
> +/*
> + * The config command sets everything at once: RGB for both rings,
> + * vibration strength, joystick sensitivity, and the eject/reset field.
> + */
> +static int aya3_send_config(struct aya3 *aya, u8 eject)
> +{
> +       static const u8 template[AYA3_REPORT_SIZE] = {
> +               [3] = AYA3_CMD_CONFIG,
> +               [4] = AYA3_SUBCMD_CONFIG,
> +               [22] = 0x33,
> +               [23] = 0x22,    /* joystick sensitivity 100%/100% */
> +               [32] = 0x01,
> +               [37] = 0x64,
> +               [38] = 0x64,

Overwriting joystick sensitivity is a bit problematic. Can you see if
dropping those four bytes still allows RGB to go through? This might
be preferable. Otherwise you might have to implement those endpoints
as well, and handle the issue of multiple writes (ie setting joystick
left, right and RGB produces three writes instead of one).

> +       };
> +       u8 *buf = aya->xfer;
> +       u8 mode = (aya->rgb[0] || aya->rgb[1] || aya->rgb[2]) ?
> +                 AYA3_RGB_SOLID : AYA3_RGB_OFF;

Consider implementing the pulsing mode it offers, there should be an
accepted ABI for it somewhere...

> +
> +       memcpy(buf, template, AYA3_REPORT_SIZE);
> +       /* Right ring, then left ring: mode, R, G, B */
> +       buf[8] = mode;
> +       memcpy(buf + 9, aya->rgb, 3);
> +       buf[12] = mode;
> +       memcpy(buf + 13, aya->rgb, 3);
> +       buf[20] = eject;
> +       buf[24] = aya->vibration << 4;
> +       aya3_checksum(buf);
> +
> +       return aya3_cmd(aya, NULL);
> +}
> +
> +static int aya3_raw_event(struct hid_device *hdev, struct hid_report *report,
> +                         u8 *data, int size)
> +{
> +       struct aya3 *aya = hid_get_drvdata(hdev);
> +
> +       if (!READ_ONCE(aya->resp_pending) || size < AYA3_RESP_SIZE)
> +               return 0;
> +       if (data[AYA3_RESP_CMD] != aya->resp_expect)
> +               return 0;
> +
> +       memcpy(aya->resp, data, AYA3_RESP_SIZE);
> +       WRITE_ONCE(aya->resp_pending, false);
> +       complete(&aya->resp_done);
> +       return 0;
> +}
> +
> +static ssize_t aya3_module_show(struct device *dev, char *buf, int offset)
> +{
> +       struct aya3 *aya = dev_get_drvdata(dev);
> +       u8 resp[AYA3_RESP_SIZE];
> +       int ret;
> +
> +       ret = mutex_lock_interruptible(&aya->lock);
> +       if (ret)
> +               return ret;
> +       ret = aya3_check(aya, resp);
> +       mutex_unlock(&aya->lock);
> +       if (ret)
> +               return ret;
> +
> +       return sysfs_emit(buf, "0x%02x\n", resp[offset]);
> +}
> +
> +static ssize_t module_left_show(struct device *dev,
> +                               struct device_attribute *attr, char *buf)
> +{
> +       return aya3_module_show(dev, buf, AYA3_RESP_MODULE_LEFT);
> +}
> +static DEVICE_ATTR_RO(module_left);
> +
> +static ssize_t module_right_show(struct device *dev,
> +                                struct device_attribute *attr, char *buf)
> +{
> +       return aya3_module_show(dev, buf, AYA3_RESP_MODULE_RIGHT);
> +}
> +static DEVICE_ATTR_RO(module_right);
> +
> +static ssize_t eject_store(struct device *dev, struct device_attribute *attr,
> +                          const char *buf, size_t count)
> +{
> +       struct aya3 *aya = dev_get_drvdata(dev);
> +       u8 resp[AYA3_RESP_SIZE];
> +       u8 eject;
> +       int ret, i;
> +
> +       if (sysfs_streq(buf, "left"))
> +               eject = AYA3_EJECT_LEFT;
> +       else if (sysfs_streq(buf, "right"))
> +               eject = AYA3_EJECT_RIGHT;
> +       else if (sysfs_streq(buf, "both"))
> +               eject = AYA3_EJECT_LEFT | AYA3_EJECT_RIGHT;
> +       else
> +               return -EINVAL;
> +
> +       ret = mutex_lock_interruptible(&aya->lock);
> +       if (ret)
> +               return ret;
> +
> +       ret = aya3_send_config(aya, eject);
> +       if (ret)
> +               goto out;
> +
> +       /*
> +        * Wait for the firmware to report the eject as done. Userspace
> +        * must then cut power through ayaneo-ec's controller_power for
> +        * the module to be physically released.
> +        */
> +       ret = -ETIMEDOUT;
> +       for (i = 0; i < 20; i++) {
> +               msleep(400);
> +               if (aya3_check(aya, resp))
> +                       continue;
> +               if (!(resp[AYA3_RESP_EJECT_STATUS] & ~AYA3_EJECT_DONE_MASK)) {
> +                       ret = 0;
> +                       break;
> +               }
> +       }
> +out:
> +       mutex_unlock(&aya->lock);
> +       return ret ? ret : count;
> +}
> +static DEVICE_ATTR_WO(eject);
> +
> +static ssize_t reset_store(struct device *dev, struct device_attribute *attr,
> +                          const char *buf, size_t count)
> +{
> +       struct aya3 *aya = dev_get_drvdata(dev);
> +       bool value;
> +       int ret;
> +
> +       ret = kstrtobool(buf, &value);
> +       if (ret)
> +               return ret;
> +       if (!value)
> +               return count;
> +
> +       ret = mutex_lock_interruptible(&aya->lock);
> +       if (ret)
> +               return ret;
> +       ret = aya3_send_config(aya, AYA3_RESET);
> +       if (!ret) {
> +               msleep(500);
> +               ret = aya3_send_config(aya, 0);
> +       }
> +       mutex_unlock(&aya->lock);
> +       return ret ? ret : count;
> +}
> +static DEVICE_ATTR_WO(reset);
> +
> +static struct attribute *aya3_attrs[] = {
> +       &dev_attr_module_left.attr,
> +       &dev_attr_module_right.attr,
> +       &dev_attr_eject.attr,
> +       &dev_attr_reset.attr,
> +       NULL
> +};
> +ATTRIBUTE_GROUPS(aya3);
> +
> +static int aya3_led_set(struct led_classdev *cdev, enum led_brightness value)
> +{
> +       struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
> +       struct aya3 *aya = container_of(mc, struct aya3, mcled);
> +       int ret, i;
> +
> +       ret = mutex_lock_interruptible(&aya->lock);
> +       if (ret)
> +               return ret;
> +
> +       led_mc_calc_color_components(mc, value);
> +       for (i = 0; i < 3; i++)
> +               aya->rgb[i] = min_t(unsigned int, aya->subleds[i].brightness, 255);
> +
> +       ret = aya3_send_config(aya, 0);
> +       if (ret)
> +               hid_err(aya->hdev, "failed to update RGB config: %d\n", ret);
> +       mutex_unlock(&aya->lock);
> +       return ret;
> +}
> +
> +static int aya3_register_led(struct aya3 *aya)
> +{
> +       struct led_classdev *cdev = &aya->mcled.led_cdev;
> +
> +       aya->subleds[0].color_index = LED_COLOR_ID_RED;
> +       aya->subleds[1].color_index = LED_COLOR_ID_GREEN;
> +       aya->subleds[2].color_index = LED_COLOR_ID_BLUE;
> +       aya->mcled.subled_info = aya->subleds;
> +       aya->mcled.num_colors = 3;
> +
> +       cdev->name = devm_kasprintf(&aya->hdev->dev, GFP_KERNEL,
> +                                   "%s:rgb:joystick_rings",
> +                                   dev_name(&aya->hdev->dev));
> +       if (!cdev->name)
> +               return -ENOMEM;
> +       cdev->brightness = 0;
> +       cdev->max_brightness = 255;
> +       cdev->brightness_set_blocking = aya3_led_set;
> +
> +       return devm_led_classdev_multicolor_register(&aya->hdev->dev,
> +                                                    &aya->mcled);
> +}

The ABI for the LEDs is ok, especially if you implement pulsing mode mode.

Best,
Antheas

> +
> +static const struct dmi_system_id aya3_dmi_table[] = {
> +       {
> +               .matches = {
> +                       DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> +                       DMI_MATCH(DMI_BOARD_NAME, "AYANEO 3"),
> +               },
> +       },
> +       {}
> +};
> +
> +static int aya3_probe(struct hid_device *hdev, const struct hid_device_id *id)
> +{
> +       struct aya3 *aya;
> +       int ret;
> +
> +       /* The VID/PID is a generic SigmaMicro ID; bind on AYANEO 3 only */
> +       if (!dmi_check_system(aya3_dmi_table))
> +               return -ENODEV;
> +
> +       if (!hid_is_usb(hdev))
> +               return -ENODEV;
> +
> +       ret = hid_parse(hdev);
> +       if (ret)
> +               return ret;
> +
> +       /* Bind only the vendor interface, not the gamepad/keyboard ones */
> +       if (hdev->collection->usage != (HID_UP_MSVENDOR | 0x0001))
> +               return -ENODEV;
> +
> +       aya = devm_kzalloc(&hdev->dev, sizeof(*aya), GFP_KERNEL);
> +       if (!aya)
> +               return -ENOMEM;
> +
> +       aya->xfer = devm_kzalloc(&hdev->dev, AYA3_REPORT_SIZE, GFP_KERNEL);
> +       if (!aya->xfer)
> +               return -ENOMEM;
> +
> +       aya->hdev = hdev;
> +       aya->vibration = AYA3_VIBRATION_DEFAULT;
> +       init_completion(&aya->resp_done);
> +       ret = devm_mutex_init(&hdev->dev, &aya->lock);
> +       if (ret)
> +               return ret;
> +       hid_set_drvdata(hdev, aya);
> +
> +       ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW);
> +       if (ret)
> +               return ret;
> +
> +       ret = hid_hw_open(hdev);
> +       if (ret)
> +               goto err_stop;
> +
> +       /* Input reports are not delivered during probe by default */
> +       hid_device_io_start(hdev);
> +
> +       scoped_guard(mutex, &aya->lock)
> +               ret = aya3_check(aya, NULL);
> +       if (ret)
> +               hid_warn(hdev, "controller did not answer status check: %d\n",
> +                        ret);
> +
> +       ret = aya3_register_led(aya);
> +       if (ret)
> +               goto err_close;
> +
> +       return 0;
> +
> +err_close:
> +       hid_hw_close(hdev);
> +err_stop:
> +       hid_hw_stop(hdev);
> +       return ret;
> +}
> +
> +static void aya3_remove(struct hid_device *hdev)
> +{
> +       hid_hw_close(hdev);
> +       hid_hw_stop(hdev);
> +}
> +
> +static const struct hid_device_id aya3_devices[] = {
> +       { HID_USB_DEVICE(0x1c4f, 0x0002) },
> +       {}
> +};
> +MODULE_DEVICE_TABLE(hid, aya3_devices);
> +
> +static struct hid_driver aya3_driver = {
> +       .name = "hid-ayaneo",
> +       .id_table = aya3_devices,
> +       .probe = aya3_probe,
> +       .remove = aya3_remove,
> +       .raw_event = aya3_raw_event,
> +       .driver = {
> +               .dev_groups = aya3_groups,
> +       },
> +};
> +module_hid_driver(aya3_driver);
> +
> +MODULE_AUTHOR("Matías Martínez <hello@matias.me>");
> +MODULE_DESCRIPTION("AYANEO 3 detachable controller driver");
> +MODULE_LICENSE("GPL");
>
> base-commit: a8fcb3dbf9024da44f1614c42ea16001f4b860b0
> --
> 2.54.0 (Apple Git-157)
>
>


^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH] HID: ayaneo: Add AYANEO 3 detachable controller driver
  2026-08-24 21:50 [PATCH] HID: ayaneo: Add AYANEO 3 detachable controller driver Matías Martínez
  2026-08-24 22:00 ` Antheas Kapenekakis
@ 2026-08-24 22:01 ` sashiko-bot
  2026-08-24 22:47   ` Matías Martínez
  2026-08-24 22:31 ` [PATCH v2] " Matías Martínez
  2 siblings, 1 reply; 10+ messages in thread
From: sashiko-bot @ 2026-08-24 22:01 UTC (permalink / raw)
  To: Matías Martínez; +Cc: dmitry.torokhov, linux-input

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] Out-of-Bounds/NULL Pointer Dereference when `hdev->maxcollection == 0`
- [High] Use-After-Free due to Devres LIFO Violation on `hid_hw_stop`
- [Medium] Resource Lifecycle Stall in `eject_store` on hardware errors
- [Medium] Data Race allowing new commands to read a stale payload
--

--- Patch [1]: [PATCH] HID: ayaneo: Add AYANEO 3 detachable controller driver ---
commit fb4fc36a684a0e10b09f75f11cd5139de0102be2
Author: Matías Martínez <hello@matias.me>

HID: ayaneo: Add AYANEO 3 detachable controller driver

The AYANEO 3 handheld has a detachable controller with swappable
modules ("Magic Modules"). The controller exposes three USB HID
interfaces behind 1c4f:0002 (a generic SigmaMicro VID/PID, hence the
DMI gate): a gamepad, a keyboard for the extra buttons, and a vendor
interface accepting 65-byte commands.

> diff --git a/drivers/hid/hid-ayaneo.c b/drivers/hid/hid-ayaneo.c
> new file mode 100644
> --- /dev/null
> +++ b/drivers/hid/hid-ayaneo.c
[ ... ]
> +static int aya3_cmd(struct aya3 *aya, u8 *resp)
> +{
> +	int attempt, ret;
> +
> +	lockdep_assert_held(&aya->lock);
> +
> +	for (attempt = 0; attempt < AYA3_CMD_ATTEMPTS; attempt++) {
> +		reinit_completion(&aya->resp_done);
> +		aya->resp_expect = aya->xfer[4];
> +		WRITE_ONCE(aya->resp_pending, true);
> +
> +		ret = aya3_send(aya);
> +		if (ret) {
> +			WRITE_ONCE(aya->resp_pending, false);
> +			return ret;
> +		}
> +
> +		if (wait_for_completion_timeout(&aya->resp_done,
> +						msecs_to_jiffies(AYA3_CMD_TIMEOUT_MS))) {
> +			if (resp)
> +				memcpy(resp, aya->resp, AYA3_RESP_SIZE);
> +			return 0;
> +		}
> +	}
> +	WRITE_ONCE(aya->resp_pending, false);
> +	return -ETIMEDOUT;
> +}
[ ... ]
> +static int aya3_raw_event(struct hid_device *hdev, struct hid_report *report,
> +			  u8 *data, int size)
> +{
> +	struct aya3 *aya = hid_get_drvdata(hdev);
> +
> +	if (!READ_ONCE(aya->resp_pending) || size < AYA3_RESP_SIZE)
> +		return 0;
> +	if (data[AYA3_RESP_CMD] != aya->resp_expect)
> +		return 0;
> +
> +	memcpy(aya->resp, data, AYA3_RESP_SIZE);
> +	WRITE_ONCE(aya->resp_pending, false);
> +	complete(&aya->resp_done);
> +	return 0;
> +}

[Severity: Medium]
Is there a race condition here if a command times out just as the reply
arrives?

If aya3_cmd() times out, it clears aya->resp_pending and returns. However,
a concurrent aya3_raw_event() might have already read aya->resp_pending as
true. 

If a new command is issued immediately, could this delayed event copy stale
data and trigger complete() for the new command?

[ ... ]
> +static ssize_t eject_store(struct device *dev, struct device_attribute *attr,
> +			   const char *buf, size_t count)
> +{
[ ... ]
> +	/*
> +	 * Wait for the firmware to report the eject as done. Userspace
> +	 * must then cut power through ayaneo-ec's controller_power for
> +	 * the module to be physically released.
> +	 */
> +	ret = -ETIMEDOUT;
> +	for (i = 0; i < 20; i++) {
> +		msleep(400);
> +		if (aya3_check(aya, resp))
> +			continue;
> +		if (!(resp[AYA3_RESP_EJECT_STATUS] & ~AYA3_EJECT_DONE_MASK)) {
> +			ret = 0;
> +			break;
> +		}
> +	}

[Severity: Medium]
Could this loop stall for up to 8 seconds if the hardware is abruptly
unplugged?

If the device is physically removed, aya3_check() will immediately return
a fatal error, such as -ENODEV.

Since this continue statement ignores the error unconditionally, won't it keep
sleeping for all 20 iterations even though the hardware is permanently gone?

[ ... ]
> +static int aya3_probe(struct hid_device *hdev, const struct hid_device_id *id)
> +{
> +	struct aya3 *aya;
> +	int ret;
> +
> +	/* The VID/PID is a generic SigmaMicro ID; bind on AYANEO 3 only */
> +	if (!dmi_check_system(aya3_dmi_table))
> +		return -ENODEV;
> +
> +	if (!hid_is_usb(hdev))
> +		return -ENODEV;
> +
> +	ret = hid_parse(hdev);
> +	if (ret)
> +		return ret;
> +
> +	/* Bind only the vendor interface, not the gamepad/keyboard ones */
> +	if (hdev->collection->usage != (HID_UP_MSVENDOR | 0x0001))
> +		return -ENODEV;

[Severity: High]
Can this dereference a NULL or uninitialized pointer if the device presents a
report descriptor with zero collections?

A malformed USB HID device could pass hid_parse() but leave
hdev->maxcollection as 0.

Should this code check that hdev->maxcollection is greater than 0 before
accessing hdev->collection->usage?

[ ... ]
> +static void aya3_remove(struct hid_device *hdev)
> +{
> +	hid_hw_close(hdev);
> +	hid_hw_stop(hdev);
> +}

[Severity: High]
Does this manual hid_hw_stop() call violate the devres release order and
create a use-after-free window?

The driver registers the LED class device earlier in aya3_probe() using
the managed devm_led_classdev_multicolor_register().

Because devres actions run after the aya3_remove() callback finishes, calling
hid_hw_stop() here tears down the HID transport while the LED sysfs attributes
are still active.

If user space writes to the LED sysfs attribute during this window,
could aya3_led_set() attempt to send commands over the stopped transport?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260824215041.79892-1-hello@matias.me?part=1

^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH] HID: ayaneo: Add AYANEO 3 detachable controller driver
  2026-08-24 22:00 ` Antheas Kapenekakis
@ 2026-08-24 22:25   ` Antheas Kapenekakis
  2026-08-24 22:47     ` Matías Martínez
  0 siblings, 1 reply; 10+ messages in thread
From: Antheas Kapenekakis @ 2026-08-24 22:25 UTC (permalink / raw)
  To: Matías Martínez
  Cc: Jiri Kosina, Benjamin Tissoires, linux-input, linux-kernel,
	Denis Benato

On Tue, 25 Aug 2026 at 00:00, Antheas Kapenekakis <lkml@antheas.dev> wrote:
>
> On Mon, 24 Aug 2026 at 23:50, Matías Martínez <hello@matias.me> wrote:
> >
> > The AYANEO 3 handheld has a detachable controller with swappable
> > modules ("Magic Modules"). The controller exposes three USB HID
> > interfaces behind 1c4f:0002 (a generic SigmaMicro VID/PID, hence the
> > DMI gate): a gamepad, a keyboard for the extra buttons, and a vendor
> > interface accepting 65-byte commands.
> >
> > Add a driver for the vendor interface providing module identification
> > (module_left/module_right sysfs attributes), software eject of the
> > modules (eject sysfs attribute, blocking until the firmware confirms
> > the release handshake), and RGB control of the joystick rings as a
> > multicolor LED class device ("<device name>:rgb:joystick_rings";
> > userspace such as InputPlumber matches the function suffix).
> >
> > This complements the ayaneo-ec platform driver, which exposes module
> > attach state and controller power. A full physical eject is performed
> > by writing to eject and then cutting power through ayaneo-ec's
> > controller_power attribute; that orchestration is deliberately left
> > to userspace.
>
> If you need userspace coordination anyway, including the multiple
> timing hacks I had to implement, the question of having to route this
> through the kernel arises.
>
> > The protocol was reverse engineered in the Handheld Daemon project by
> > Antheas Kapenekakis. Tested on an AYANEO 3: module identification,
> > RGB, and a full eject/reinsert/repower cycle.
> >
> > Signed-off-by: Matías Martínez <hello@matias.me>
> > Reviewed-by: Denis Benato <denis.benato@linux.dev>
> > ---
> >  .../ABI/testing/sysfs-driver-hid-ayaneo       |  36 ++
> >  MAINTAINERS                                   |   7 +
> >  drivers/hid/Kconfig                           |  14 +
> >  drivers/hid/Makefile                          |   1 +
> >  drivers/hid/hid-ayaneo.c                      | 471 ++++++++++++++++++
> >  5 files changed, 529 insertions(+)
> >  create mode 100644 Documentation/ABI/testing/sysfs-driver-hid-ayaneo
> >  create mode 100644 drivers/hid/hid-ayaneo.c
> >
> > diff --git a/Documentation/ABI/testing/sysfs-driver-hid-ayaneo b/Documentation/ABI/testing/sysfs-driver-hid-ayaneo
> > new file mode 100644
> > index 000000000..807c4fc9d
> > --- /dev/null
> > +++ b/Documentation/ABI/testing/sysfs-driver-hid-ayaneo
> > @@ -0,0 +1,36 @@
> > +What:          /sys/bus/hid/drivers/hid-ayaneo/<dev>/module_left
> > +What:          /sys/bus/hid/drivers/hid-ayaneo/<dev>/module_right
> > +Date:          August 2026
> > +KernelVersion: 7.3
> > +Contact:       Matías Martínez <hello@matias.me>
> > +Description:
> > +               Reports the type of the module currently inserted in the
> > +               left/right slot of the AYANEO 3 detachable controller, as
> > +               the raw identifier reported by the controller firmware in
> > +               hexadecimal (e.g. "0x04"). Bits 0-5 encode the module
> > +               type, bit 6 indicates the module is inserted rotated.
> > +
> > +               Reading these attributes queries the controller and can
> > +               take up to a second.
> > +
> > +What:          /sys/bus/hid/drivers/hid-ayaneo/<dev>/eject
> > +Date:          August 2026
> > +KernelVersion: 7.3
> > +Contact:       Matías Martínez <hello@matias.me>
> > +Description:
> > +               Write-only. Writing "left", "right" or "both" asks the
> > +               controller firmware to release the corresponding
> > +               module(s). The write blocks until the firmware confirms
> > +               the release handshake (typically a few seconds). The
> > +               module is physically released once controller power is
> > +               subsequently cut through the ayaneo-ec platform driver's
> > +               controller_power attribute; that final step is left to
> > +               userspace.
> > +
> > +What:          /sys/bus/hid/drivers/hid-ayaneo/<dev>/reset
> > +Date:          August 2026
> > +KernelVersion: 7.3
> > +Contact:       Matías Martínez <hello@matias.me>
> > +Description:
> > +               Write-only. Writing "1" asks the controller firmware to
> > +               perform a quick reset of the controller configuration.
> > diff --git a/MAINTAINERS b/MAINTAINERS
> > index 6ecfe6c9e..360f01d39 100644
> > --- a/MAINTAINERS
> > +++ b/MAINTAINERS
> > @@ -4469,6 +4469,13 @@ F:       Documentation/devicetree/bindings/spi/axiado,ax3000-spi.yaml
> >  F:     drivers/spi/spi-axiado.c
> >  F:     drivers/spi/spi-axiado.h
> >
> > +AYANEO 3 CONTROLLER HID DRIVER
> > +M:     Matías Martínez <hello@matias.me>
> > +L:     linux-input@vger.kernel.org
> > +S:     Maintained
> > +F:     Documentation/ABI/testing/sysfs-driver-hid-ayaneo
> > +F:     drivers/hid/hid-ayaneo.c
> > +
> >  AYANEO PLATFORM EC DRIVER
> >  M:     Antheas Kapenekakis <lkml@antheas.dev>
> >  L:     platform-driver-x86@vger.kernel.org
> > diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
> > index aa7fa11a0..9693a0233 100644
> > --- a/drivers/hid/Kconfig
> > +++ b/drivers/hid/Kconfig
> > @@ -205,6 +205,20 @@ config HID_AUREAL
> >         help
> >         Support for Aureal Cy se W-01RN Remote Controller and other Aureal derived remotes.
> >
> > +config HID_AYANEO
> > +       tristate "AYANEO 3 detachable controller support"
> > +       depends on USB_HID
> > +       depends on DMI
> > +       depends on LEDS_CLASS_MULTICOLOR
> > +       help
> > +         Provides support for the detachable controller ("Magic Modules")
> > +         of the AYANEO 3 handheld: module identification, software eject
> > +         and RGB control of the joystick rings. Complements the ayaneo-ec
> > +         platform driver, which handles module attach state and controller
> > +         power.
> > +
> > +         Say Y or M here if you have an AYANEO 3.
> > +
> >  config HID_BELKIN
> >         tristate "Belkin Flip KVM and Wireless keyboard"
> >         help
> > diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
> > index 48a863b24..60add9348 100644
> > --- a/drivers/hid/Makefile
> > +++ b/drivers/hid/Makefile
> > @@ -35,6 +35,7 @@ obj-$(CONFIG_HID_APPLETB_KBD) += hid-appletb-kbd.o
> >  obj-$(CONFIG_HID_CREATIVE_SB0540)      += hid-creative-sb0540.o
> >  obj-$(CONFIG_HID_ASUS)         += hid-asus.o
> >  obj-$(CONFIG_HID_AUREAL)       += hid-aureal.o
> > +obj-$(CONFIG_HID_AYANEO)       += hid-ayaneo.o
> >  obj-$(CONFIG_HID_BELKIN)       += hid-belkin.o
> >  obj-$(CONFIG_HID_BETOP_FF)     += hid-betopff.o
> >  obj-$(CONFIG_HID_BIGBEN_FF)    += hid-bigbenff.o
> > diff --git a/drivers/hid/hid-ayaneo.c b/drivers/hid/hid-ayaneo.c
> > new file mode 100644
> > index 000000000..61c64617f
> > --- /dev/null
> > +++ b/drivers/hid/hid-ayaneo.c
> > @@ -0,0 +1,471 @@
> > +// SPDX-License-Identifier: GPL-2.0+
> > +/*
> > + * HID driver for the AYANEO 3 detachable controller ("Magic Modules").
> > + *
> > + * The AYANEO 3 controller exposes three USB HID interfaces behind
> > + * VID 0x1c4f PID 0x0002 (a generic SigmaMicro ID, hence the DMI gate):
> > + * a gamepad, a keyboard for the extra buttons, and a vendor interface
> > + * (application usage 0xff000001) accepting 65-byte commands.
> > + *
> > + * This driver binds the vendor interface and provides:
> > + *  - module identification (which module type is inserted on each side)
> > + *  - software eject of the left/right modules
> > + *  - RGB control of the joystick rings as a multicolor LED class device
> > + *
> > + * It complements the ayaneo-ec platform driver, which exposes module
> > + * attach state and controller power. A full eject is: write to this
> > + * driver's "eject" attribute, then power the controller off through
> > + * ayaneo-ec's controller_power once the eject completes.
> > + *
> > + * The protocol was reverse engineered in the Handheld Daemon project by
> > + * Antheas Kapenekakis.
> > + *
> > + * Command format (65 bytes, unnumbered report):
> > + *   [0]   report id (0)
> > + *   [1:3] little-endian sum of bytes 7..64
> > + *   [3]   command
> > + *   [4]   subcommand
> > + *   [5:]  payload
> > + * The device replies with a 64-byte report echoing the subcommand at
> > + * byte 3.
> > + *
> > + * Copyright (C) 2026 Matías Martínez <hello@matias.me>
> > + */
> > +
> > +#include <linux/delay.h>
> > +#include <linux/dmi.h>
> > +#include <linux/hid.h>
> > +#include <linux/led-class-multicolor.h>
> > +#include <linux/module.h>
> > +#include <linux/mutex.h>
> > +#include <linux/sysfs.h>
> > +#include <linux/unaligned.h>
> > +
> > +#define AYA3_REPORT_SIZE       65
> > +#define AYA3_RESP_SIZE         64
> > +#define AYA3_CMD_TIMEOUT_MS    300
> > +#define AYA3_CMD_ATTEMPTS      3
> > +
> > +/* Subcommands (byte 4); byte 3 is 0x00 except for the config command */
> > +#define AYA3_SUBCMD_CHECK      0x08
> > +#define AYA3_CMD_CONFIG                0x21
> > +#define AYA3_SUBCMD_CONFIG     0x09
> > +
> > +/* CHECK response fields */
> > +#define AYA3_RESP_CMD          3
> > +#define AYA3_RESP_EJECT_STATUS 19
> > +#define AYA3_RESP_MODULE_LEFT  32
> > +#define AYA3_RESP_MODULE_RIGHT 33
> > +/* Bits that stay set in the eject status byte after an eject completes */
> > +#define AYA3_EJECT_DONE_MASK   0x11
> > +
> > +/* Config command eject/reset field */
> > +#define AYA3_EJECT_LEFT                0x07
> > +#define AYA3_EJECT_RIGHT       0x70
> > +#define AYA3_RESET             0x88
> > +
> > +/* Config command RGB modes */
> > +#define AYA3_RGB_SOLID         0x01
> > +#define AYA3_RGB_OFF           0xff
> > +
> > +#define AYA3_VIBRATION_DEFAULT 0x02    /* medium */
> > +
> > +struct aya3 {
> > +       struct hid_device *hdev;
> > +       /* DMA-safe command buffer; guarded by lock */
> > +       u8 *xfer;
> > +       /* Serializes commands and cached-config access */
> > +       struct mutex lock;
> > +       struct completion resp_done;
> > +       u8 resp[AYA3_RESP_SIZE];
> > +       u8 resp_expect;
> > +       bool resp_pending;
> > +
> > +       u8 rgb[3];
> > +       u8 vibration;
> > +
> > +       struct led_classdev_mc mcled;
> > +       struct mc_subled subleds[3];
> > +};
> > +
> > +static int aya3_send(struct aya3 *aya)
> > +{
> > +       int ret;
> > +
> > +       ret = hid_hw_output_report(aya->hdev, aya->xfer, AYA3_REPORT_SIZE);
> > +       if (ret == -ENOSYS)
> > +               ret = hid_hw_raw_request(aya->hdev, aya->xfer[0], aya->xfer,
> > +                                        AYA3_REPORT_SIZE, HID_OUTPUT_REPORT,
> > +                                        HID_REQ_SET_REPORT);
> > +       if (ret < 0)
> > +               return ret;
> > +       return 0;
> > +}
> > +
> > +/**
> > + * aya3_cmd() - send the command in aya->xfer and wait for the reply
> > + * @aya: driver data; @aya->xfer holds the fully built 65-byte command
> > + * @resp: destination for the AYA3_RESP_SIZE-byte reply, or NULL to
> > + *        discard it
> > + *
> > + * The device echoes the subcommand byte of the command it is answering,
> > + * which aya3_raw_event() uses to match replies. Unanswered commands are
> > + * retried up to AYA3_CMD_ATTEMPTS times.
> > + *
> > + * Context: process context; the caller must hold @aya->lock, which
> > + *          protects @aya->xfer and the reply state.
> > + * Return: 0 on success, -ETIMEDOUT if every attempt went unanswered, or
> > + *         a negative errno if sending failed.
> > + */
> > +static int aya3_cmd(struct aya3 *aya, u8 *resp)
> > +{
> > +       int attempt, ret;
> > +
> > +       lockdep_assert_held(&aya->lock);
> > +
> > +       for (attempt = 0; attempt < AYA3_CMD_ATTEMPTS; attempt++) {
> > +               reinit_completion(&aya->resp_done);
> > +               aya->resp_expect = aya->xfer[4];
> > +               WRITE_ONCE(aya->resp_pending, true);
> > +
> > +               ret = aya3_send(aya);
> > +               if (ret) {
> > +                       WRITE_ONCE(aya->resp_pending, false);
> > +                       return ret;
> > +               }
> > +
> > +               if (wait_for_completion_timeout(&aya->resp_done,
> > +                                               msecs_to_jiffies(AYA3_CMD_TIMEOUT_MS))) {
> > +                       if (resp)
> > +                               memcpy(resp, aya->resp, AYA3_RESP_SIZE);
> > +                       return 0;
> > +               }
> > +       }
> > +       WRITE_ONCE(aya->resp_pending, false);
> > +       return -ETIMEDOUT;
> > +}
> > +
> > +static void aya3_checksum(u8 *buf)
> > +{
> > +       u16 sum = 0;
> > +       int i;
> > +
> > +       for (i = 7; i < AYA3_REPORT_SIZE; i++)
> > +               sum += buf[i];
> > +       put_unaligned_le16(sum, buf + 1);
> > +}
> > +
> > +static int aya3_check(struct aya3 *aya, u8 *resp)
> > +{
> > +       memset(aya->xfer, 0, AYA3_REPORT_SIZE);
> > +       aya->xfer[4] = AYA3_SUBCMD_CHECK;
> > +       return aya3_cmd(aya, resp);
> > +}
> > +
> > +/*
> > + * The config command sets everything at once: RGB for both rings,
> > + * vibration strength, joystick sensitivity, and the eject/reset field.
> > + */
> > +static int aya3_send_config(struct aya3 *aya, u8 eject)
> > +{
> > +       static const u8 template[AYA3_REPORT_SIZE] = {
> > +               [3] = AYA3_CMD_CONFIG,
> > +               [4] = AYA3_SUBCMD_CONFIG,
> > +               [22] = 0x33,
> > +               [23] = 0x22,    /* joystick sensitivity 100%/100% */
> > +               [32] = 0x01,
> > +               [37] = 0x64,
> > +               [38] = 0x64,
>
> Overwriting joystick sensitivity is a bit problematic. Can you see if
> dropping those four bytes still allows RGB to go through? This might
> be preferable. Otherwise you might have to implement those endpoints
> as well, and handle the issue of multiple writes (ie setting joystick
> left, right and RGB produces three writes instead of one).
>
> > +       };
> > +       u8 *buf = aya->xfer;
> > +       u8 mode = (aya->rgb[0] || aya->rgb[1] || aya->rgb[2]) ?
> > +                 AYA3_RGB_SOLID : AYA3_RGB_OFF;
>
> Consider implementing the pulsing mode it offers, there should be an
> accepted ABI for it somewhere...
>
> > +
> > +       memcpy(buf, template, AYA3_REPORT_SIZE);
> > +       /* Right ring, then left ring: mode, R, G, B */
> > +       buf[8] = mode;
> > +       memcpy(buf + 9, aya->rgb, 3);
> > +       buf[12] = mode;
> > +       memcpy(buf + 13, aya->rgb, 3);
> > +       buf[20] = eject;
> > +       buf[24] = aya->vibration << 4;
> > +       aya3_checksum(buf);
> > +
> > +       return aya3_cmd(aya, NULL);
> > +}
> > +
> > +static int aya3_raw_event(struct hid_device *hdev, struct hid_report *report,
> > +                         u8 *data, int size)
> > +{
> > +       struct aya3 *aya = hid_get_drvdata(hdev);
> > +
> > +       if (!READ_ONCE(aya->resp_pending) || size < AYA3_RESP_SIZE)
> > +               return 0;
> > +       if (data[AYA3_RESP_CMD] != aya->resp_expect)
> > +               return 0;
> > +
> > +       memcpy(aya->resp, data, AYA3_RESP_SIZE);
> > +       WRITE_ONCE(aya->resp_pending, false);
> > +       complete(&aya->resp_done);
> > +       return 0;
> > +}
> > +
> > +static ssize_t aya3_module_show(struct device *dev, char *buf, int offset)
> > +{
> > +       struct aya3 *aya = dev_get_drvdata(dev);
> > +       u8 resp[AYA3_RESP_SIZE];
> > +       int ret;
> > +
> > +       ret = mutex_lock_interruptible(&aya->lock);
> > +       if (ret)
> > +               return ret;
> > +       ret = aya3_check(aya, resp);
> > +       mutex_unlock(&aya->lock);
> > +       if (ret)
> > +               return ret;
> > +
> > +       return sysfs_emit(buf, "0x%02x\n", resp[offset]);
> > +}
> > +
> > +static ssize_t module_left_show(struct device *dev,
> > +                               struct device_attribute *attr, char *buf)
> > +{
> > +       return aya3_module_show(dev, buf, AYA3_RESP_MODULE_LEFT);
> > +}
> > +static DEVICE_ATTR_RO(module_left);
> > +
> > +static ssize_t module_right_show(struct device *dev,
> > +                                struct device_attribute *attr, char *buf)
> > +{
> > +       return aya3_module_show(dev, buf, AYA3_RESP_MODULE_RIGHT);
> > +}
> > +static DEVICE_ATTR_RO(module_right);
> > +
> > +static ssize_t eject_store(struct device *dev, struct device_attribute *attr,
> > +                          const char *buf, size_t count)
> > +{
> > +       struct aya3 *aya = dev_get_drvdata(dev);
> > +       u8 resp[AYA3_RESP_SIZE];
> > +       u8 eject;
> > +       int ret, i;
> > +
> > +       if (sysfs_streq(buf, "left"))
> > +               eject = AYA3_EJECT_LEFT;
> > +       else if (sysfs_streq(buf, "right"))
> > +               eject = AYA3_EJECT_RIGHT;
> > +       else if (sysfs_streq(buf, "both"))
> > +               eject = AYA3_EJECT_LEFT | AYA3_EJECT_RIGHT;
> > +       else
> > +               return -EINVAL;
> > +
> > +       ret = mutex_lock_interruptible(&aya->lock);
> > +       if (ret)
> > +               return ret;
> > +
> > +       ret = aya3_send_config(aya, eject);
> > +       if (ret)
> > +               goto out;
> > +
> > +       /*
> > +        * Wait for the firmware to report the eject as done. Userspace
> > +        * must then cut power through ayaneo-ec's controller_power for
> > +        * the module to be physically released.
> > +        */
> > +       ret = -ETIMEDOUT;
> > +       for (i = 0; i < 20; i++) {
> > +               msleep(400);

Almost forgot. Magic value.

> > +               if (aya3_check(aya, resp))
> > +                       continue;
> > +               if (!(resp[AYA3_RESP_EJECT_STATUS] & ~AYA3_EJECT_DONE_MASK)) {
> > +                       ret = 0;
> > +                       break;
> > +               }
> > +       }
> > +out:
> > +       mutex_unlock(&aya->lock);
> > +       return ret ? ret : count;
> > +}
> > +static DEVICE_ATTR_WO(eject);
> > +
> > +static ssize_t reset_store(struct device *dev, struct device_attribute *attr,
> > +                          const char *buf, size_t count)
> > +{
> > +       struct aya3 *aya = dev_get_drvdata(dev);
> > +       bool value;
> > +       int ret;
> > +
> > +       ret = kstrtobool(buf, &value);
> > +       if (ret)
> > +               return ret;
> > +       if (!value)
> > +               return count;
> > +
> > +       ret = mutex_lock_interruptible(&aya->lock);
> > +       if (ret)
> > +               return ret;
> > +       ret = aya3_send_config(aya, AYA3_RESET);
> > +       if (!ret) {
> > +               msleep(500);

Magic value. You need to justify those.

> > +               ret = aya3_send_config(aya, 0);
> > +       }
> > +       mutex_unlock(&aya->lock);
> > +       return ret ? ret : count;
> > +}
> > +static DEVICE_ATTR_WO(reset);
> > +
> > +static struct attribute *aya3_attrs[] = {
> > +       &dev_attr_module_left.attr,
> > +       &dev_attr_module_right.attr,
> > +       &dev_attr_eject.attr,
> > +       &dev_attr_reset.attr,
> > +       NULL
> > +};
> > +ATTRIBUTE_GROUPS(aya3);
> > +
> > +static int aya3_led_set(struct led_classdev *cdev, enum led_brightness value)
> > +{
> > +       struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
> > +       struct aya3 *aya = container_of(mc, struct aya3, mcled);
> > +       int ret, i;
> > +
> > +       ret = mutex_lock_interruptible(&aya->lock);
> > +       if (ret)
> > +               return ret;
> > +
> > +       led_mc_calc_color_components(mc, value);
> > +       for (i = 0; i < 3; i++)
> > +               aya->rgb[i] = min_t(unsigned int, aya->subleds[i].brightness, 255);
> > +
> > +       ret = aya3_send_config(aya, 0);
> > +       if (ret)
> > +               hid_err(aya->hdev, "failed to update RGB config: %d\n", ret);
> > +       mutex_unlock(&aya->lock);
> > +       return ret;
> > +}
> > +
> > +static int aya3_register_led(struct aya3 *aya)
> > +{
> > +       struct led_classdev *cdev = &aya->mcled.led_cdev;
> > +
> > +       aya->subleds[0].color_index = LED_COLOR_ID_RED;
> > +       aya->subleds[1].color_index = LED_COLOR_ID_GREEN;
> > +       aya->subleds[2].color_index = LED_COLOR_ID_BLUE;
> > +       aya->mcled.subled_info = aya->subleds;
> > +       aya->mcled.num_colors = 3;
> > +
> > +       cdev->name = devm_kasprintf(&aya->hdev->dev, GFP_KERNEL,
> > +                                   "%s:rgb:joystick_rings",
> > +                                   dev_name(&aya->hdev->dev));
> > +       if (!cdev->name)
> > +               return -ENOMEM;
> > +       cdev->brightness = 0;
> > +       cdev->max_brightness = 255;
> > +       cdev->brightness_set_blocking = aya3_led_set;
> > +
> > +       return devm_led_classdev_multicolor_register(&aya->hdev->dev,
> > +                                                    &aya->mcled);
> > +}
>
> The ABI for the LEDs is ok, especially if you implement pulsing mode mode.
>
> Best,
> Antheas
>
> > +
> > +static const struct dmi_system_id aya3_dmi_table[] = {
> > +       {
> > +               .matches = {
> > +                       DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > +                       DMI_MATCH(DMI_BOARD_NAME, "AYANEO 3"),
> > +               },
> > +       },
> > +       {}
> > +};
> > +
> > +static int aya3_probe(struct hid_device *hdev, const struct hid_device_id *id)
> > +{
> > +       struct aya3 *aya;
> > +       int ret;
> > +
> > +       /* The VID/PID is a generic SigmaMicro ID; bind on AYANEO 3 only */
> > +       if (!dmi_check_system(aya3_dmi_table))
> > +               return -ENODEV;
> > +
> > +       if (!hid_is_usb(hdev))
> > +               return -ENODEV;
> > +
> > +       ret = hid_parse(hdev);
> > +       if (ret)
> > +               return ret;
> > +
> > +       /* Bind only the vendor interface, not the gamepad/keyboard ones */
> > +       if (hdev->collection->usage != (HID_UP_MSVENDOR | 0x0001))
> > +               return -ENODEV;
> > +
> > +       aya = devm_kzalloc(&hdev->dev, sizeof(*aya), GFP_KERNEL);
> > +       if (!aya)
> > +               return -ENOMEM;
> > +
> > +       aya->xfer = devm_kzalloc(&hdev->dev, AYA3_REPORT_SIZE, GFP_KERNEL);
> > +       if (!aya->xfer)
> > +               return -ENOMEM;
> > +
> > +       aya->hdev = hdev;
> > +       aya->vibration = AYA3_VIBRATION_DEFAULT;
> > +       init_completion(&aya->resp_done);
> > +       ret = devm_mutex_init(&hdev->dev, &aya->lock);
> > +       if (ret)
> > +               return ret;
> > +       hid_set_drvdata(hdev, aya);
> > +
> > +       ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW);
> > +       if (ret)
> > +               return ret;
> > +
> > +       ret = hid_hw_open(hdev);
> > +       if (ret)
> > +               goto err_stop;
> > +
> > +       /* Input reports are not delivered during probe by default */
> > +       hid_device_io_start(hdev);
> > +
> > +       scoped_guard(mutex, &aya->lock)
> > +               ret = aya3_check(aya, NULL);
> > +       if (ret)
> > +               hid_warn(hdev, "controller did not answer status check: %d\n",
> > +                        ret);
> > +
> > +       ret = aya3_register_led(aya);
> > +       if (ret)
> > +               goto err_close;
> > +
> > +       return 0;
> > +
> > +err_close:
> > +       hid_hw_close(hdev);
> > +err_stop:
> > +       hid_hw_stop(hdev);
> > +       return ret;
> > +}
> > +
> > +static void aya3_remove(struct hid_device *hdev)
> > +{
> > +       hid_hw_close(hdev);
> > +       hid_hw_stop(hdev);
> > +}
> > +
> > +static const struct hid_device_id aya3_devices[] = {
> > +       { HID_USB_DEVICE(0x1c4f, 0x0002) },
> > +       {}
> > +};
> > +MODULE_DEVICE_TABLE(hid, aya3_devices);
> > +
> > +static struct hid_driver aya3_driver = {
> > +       .name = "hid-ayaneo",
> > +       .id_table = aya3_devices,
> > +       .probe = aya3_probe,
> > +       .remove = aya3_remove,
> > +       .raw_event = aya3_raw_event,
> > +       .driver = {
> > +               .dev_groups = aya3_groups,
> > +       },
> > +};
> > +module_hid_driver(aya3_driver);
> > +
> > +MODULE_AUTHOR("Matías Martínez <hello@matias.me>");
> > +MODULE_DESCRIPTION("AYANEO 3 detachable controller driver");
> > +MODULE_LICENSE("GPL");
> >
> > base-commit: a8fcb3dbf9024da44f1614c42ea16001f4b860b0
> > --
> > 2.54.0 (Apple Git-157)
> >
> >


^ permalink raw reply	[flat|nested] 10+ messages in thread

* [PATCH v2] HID: ayaneo: Add AYANEO 3 detachable controller driver
  2026-08-24 21:50 [PATCH] HID: ayaneo: Add AYANEO 3 detachable controller driver Matías Martínez
  2026-08-24 22:00 ` Antheas Kapenekakis
  2026-08-24 22:01 ` sashiko-bot
@ 2026-08-24 22:31 ` Matías Martínez
  2026-08-24 22:41   ` sashiko-bot
  2 siblings, 1 reply; 10+ messages in thread
From: Matías Martínez @ 2026-08-24 22:31 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: linux-input, linux-kernel, Antheas Kapenekakis, Denis Benato,
	Dmitry Torokhov, Matías Martínez

The AYANEO 3 handheld has a detachable controller with swappable
modules ("Magic Modules"). The controller exposes three USB HID
interfaces behind 1c4f:0002 (a generic SigmaMicro VID/PID, hence the
DMI gate): a gamepad, a keyboard for the extra buttons, and a vendor
interface accepting 65-byte commands.

Add a driver for the vendor interface providing module identification
(module_left/module_right sysfs attributes), software eject of the
modules (eject sysfs attribute, blocking until the firmware confirms
the release handshake), and RGB control of the joystick rings as a
multicolor LED class device ("<device name>:rgb:joystick_rings";
userspace such as InputPlumber matches the function suffix). The
firmware's fixed breathing pattern is exposed through the hw_pattern
trigger ABI.

This complements the ayaneo-ec platform driver, which exposes module
attach state and controller power. A full physical eject is performed
by writing to eject and then cutting power through ayaneo-ec's
controller_power attribute; that orchestration is deliberately left
to userspace.

The protocol was reverse engineered in the Handheld Daemon project by
Antheas Kapenekakis. Tested on an AYANEO 3: module identification,
RGB solid and breathing, a full eject/reinsert/repower cycle, and
repeated driver unbinds under a concurrent brightness-write load.
Signed-off-by: Matías Martínez <hello@matias.me>
Reviewed-by: Denis Benato <denis.benato@linux.dev>
---
Changes in v2:
- Unregister the LED class device before tearing down the HID
  transport, and flush a late-queued brightness work item that can
  race the unregister; the work could otherwise run against freed
  memory. Found by stress-testing rmmod under a brightness-write
  loop; also reachable whenever the controller power-cycles (resume,
  module eject) while userspace writes the LED. [sashiko, Denis]
- Abort the eject wait as soon as the transport reports a fatal
  error instead of polling for up to 8 seconds. [sashiko]
- Reject report descriptors with no collections explicitly. [sashiko]
- Document why a late reply to a timed-out command is harmless.
  [sashiko]
- Stop writing the joystick-sensitivity bytes in the config command
  so RGB updates no longer clobber the firmware setting; verified on
  hardware that RGB and eject work without them. [Antheas]
- Expose the firmware's breathing mode through the hw_pattern
  trigger ABI, with an ABI document. [Antheas]
 .../testing/sysfs-class-led-driver-hid-ayaneo |  15 +
 .../ABI/testing/sysfs-driver-hid-ayaneo       |  36 ++
 MAINTAINERS                                   |   8 +
 drivers/hid/Kconfig                           |  14 +
 drivers/hid/Makefile                          |   1 +
 drivers/hid/hid-ayaneo.c                      | 546 ++++++++++++++++++
 6 files changed, 620 insertions(+)
 create mode 100644 Documentation/ABI/testing/sysfs-class-led-driver-hid-ayaneo
 create mode 100644 Documentation/ABI/testing/sysfs-driver-hid-ayaneo
 create mode 100644 drivers/hid/hid-ayaneo.c

diff --git a/Documentation/ABI/testing/sysfs-class-led-driver-hid-ayaneo b/Documentation/ABI/testing/sysfs-class-led-driver-hid-ayaneo
new file mode 100644
index 000000000..00f100dba
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-class-led-driver-hid-ayaneo
@@ -0,0 +1,15 @@
+What:		/sys/class/leds/<led>/hw_pattern
+Date:		August 2026
+KernelVersion:	7.3
+Contact:	Matías Martínez <hello@matias.me>
+Description:
+		Specify a hardware pattern for the AYANEO 3 joystick
+		rings LED. The firmware supports a single breathing
+		pattern, pulsing the current colour at a fixed,
+		firmware-controlled period:
+
+		"0 <t> <brightness> <t>"
+
+		Both delta_t values are accepted but ignored, as the
+		period is not configurable. <brightness> must be
+		non-zero. Any other pattern is rejected.
diff --git a/Documentation/ABI/testing/sysfs-driver-hid-ayaneo b/Documentation/ABI/testing/sysfs-driver-hid-ayaneo
new file mode 100644
index 000000000..807c4fc9d
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-driver-hid-ayaneo
@@ -0,0 +1,36 @@
+What:		/sys/bus/hid/drivers/hid-ayaneo/<dev>/module_left
+What:		/sys/bus/hid/drivers/hid-ayaneo/<dev>/module_right
+Date:		August 2026
+KernelVersion:	7.3
+Contact:	Matías Martínez <hello@matias.me>
+Description:
+		Reports the type of the module currently inserted in the
+		left/right slot of the AYANEO 3 detachable controller, as
+		the raw identifier reported by the controller firmware in
+		hexadecimal (e.g. "0x04"). Bits 0-5 encode the module
+		type, bit 6 indicates the module is inserted rotated.
+
+		Reading these attributes queries the controller and can
+		take up to a second.
+
+What:		/sys/bus/hid/drivers/hid-ayaneo/<dev>/eject
+Date:		August 2026
+KernelVersion:	7.3
+Contact:	Matías Martínez <hello@matias.me>
+Description:
+		Write-only. Writing "left", "right" or "both" asks the
+		controller firmware to release the corresponding
+		module(s). The write blocks until the firmware confirms
+		the release handshake (typically a few seconds). The
+		module is physically released once controller power is
+		subsequently cut through the ayaneo-ec platform driver's
+		controller_power attribute; that final step is left to
+		userspace.
+
+What:		/sys/bus/hid/drivers/hid-ayaneo/<dev>/reset
+Date:		August 2026
+KernelVersion:	7.3
+Contact:	Matías Martínez <hello@matias.me>
+Description:
+		Write-only. Writing "1" asks the controller firmware to
+		perform a quick reset of the controller configuration.
diff --git a/MAINTAINERS b/MAINTAINERS
index 6ecfe6c9e..09a660458 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -4469,6 +4469,14 @@ F:	Documentation/devicetree/bindings/spi/axiado,ax3000-spi.yaml
 F:	drivers/spi/spi-axiado.c
 F:	drivers/spi/spi-axiado.h
 
+AYANEO 3 CONTROLLER HID DRIVER
+M:	Matías Martínez <hello@matias.me>
+L:	linux-input@vger.kernel.org
+S:	Maintained
+F:	Documentation/ABI/testing/sysfs-class-led-driver-hid-ayaneo
+F:	Documentation/ABI/testing/sysfs-driver-hid-ayaneo
+F:	drivers/hid/hid-ayaneo.c
+
 AYANEO PLATFORM EC DRIVER
 M:	Antheas Kapenekakis <lkml@antheas.dev>
 L:	platform-driver-x86@vger.kernel.org
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index aa7fa11a0..9693a0233 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -205,6 +205,20 @@ config HID_AUREAL
 	help
 	Support for Aureal Cy se W-01RN Remote Controller and other Aureal derived remotes.
 
+config HID_AYANEO
+	tristate "AYANEO 3 detachable controller support"
+	depends on USB_HID
+	depends on DMI
+	depends on LEDS_CLASS_MULTICOLOR
+	help
+	  Provides support for the detachable controller ("Magic Modules")
+	  of the AYANEO 3 handheld: module identification, software eject
+	  and RGB control of the joystick rings. Complements the ayaneo-ec
+	  platform driver, which handles module attach state and controller
+	  power.
+
+	  Say Y or M here if you have an AYANEO 3.
+
 config HID_BELKIN
 	tristate "Belkin Flip KVM and Wireless keyboard"
 	help
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index 48a863b24..60add9348 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -35,6 +35,7 @@ obj-$(CONFIG_HID_APPLETB_KBD)	+= hid-appletb-kbd.o
 obj-$(CONFIG_HID_CREATIVE_SB0540)	+= hid-creative-sb0540.o
 obj-$(CONFIG_HID_ASUS)		+= hid-asus.o
 obj-$(CONFIG_HID_AUREAL)	+= hid-aureal.o
+obj-$(CONFIG_HID_AYANEO)	+= hid-ayaneo.o
 obj-$(CONFIG_HID_BELKIN)	+= hid-belkin.o
 obj-$(CONFIG_HID_BETOP_FF)	+= hid-betopff.o
 obj-$(CONFIG_HID_BIGBEN_FF)	+= hid-bigbenff.o
diff --git a/drivers/hid/hid-ayaneo.c b/drivers/hid/hid-ayaneo.c
new file mode 100644
index 000000000..fc133abf5
--- /dev/null
+++ b/drivers/hid/hid-ayaneo.c
@@ -0,0 +1,546 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * HID driver for the AYANEO 3 detachable controller ("Magic Modules").
+ *
+ * The AYANEO 3 controller exposes three USB HID interfaces behind
+ * VID 0x1c4f PID 0x0002 (a generic SigmaMicro ID, hence the DMI gate):
+ * a gamepad, a keyboard for the extra buttons, and a vendor interface
+ * (application usage 0xff000001) accepting 65-byte commands.
+ *
+ * This driver binds the vendor interface and provides:
+ *  - module identification (which module type is inserted on each side)
+ *  - software eject of the left/right modules
+ *  - RGB control of the joystick rings as a multicolor LED class device
+ *
+ * It complements the ayaneo-ec platform driver, which exposes module
+ * attach state and controller power. A full eject is: write to this
+ * driver's "eject" attribute, then power the controller off through
+ * ayaneo-ec's controller_power once the eject completes.
+ *
+ * The protocol was reverse engineered in the Handheld Daemon project by
+ * Antheas Kapenekakis.
+ *
+ * Command format (65 bytes, unnumbered report):
+ *   [0]   report id (0)
+ *   [1:3] little-endian sum of bytes 7..64
+ *   [3]   command
+ *   [4]   subcommand
+ *   [5:]  payload
+ * The device replies with a 64-byte report echoing the subcommand at
+ * byte 3.
+ *
+ * Copyright (C) 2026 Matías Martínez <hello@matias.me>
+ */
+
+#include <linux/delay.h>
+#include <linux/dmi.h>
+#include <linux/hid.h>
+#include <linux/led-class-multicolor.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/sysfs.h>
+#include <linux/unaligned.h>
+#include <linux/workqueue.h>
+
+#define AYA3_REPORT_SIZE	65
+#define AYA3_RESP_SIZE		64
+#define AYA3_CMD_TIMEOUT_MS	300
+#define AYA3_CMD_ATTEMPTS	3
+
+/* Subcommands (byte 4); byte 3 is 0x00 except for the config command */
+#define AYA3_SUBCMD_CHECK	0x08
+#define AYA3_CMD_CONFIG		0x21
+#define AYA3_SUBCMD_CONFIG	0x09
+
+/* CHECK response fields */
+#define AYA3_RESP_CMD		3
+#define AYA3_RESP_EJECT_STATUS	19
+#define AYA3_RESP_MODULE_LEFT	32
+#define AYA3_RESP_MODULE_RIGHT	33
+/* Bits that stay set in the eject status byte after an eject completes */
+#define AYA3_EJECT_DONE_MASK	0x11
+
+/* Config command eject/reset field */
+#define AYA3_EJECT_LEFT		0x07
+#define AYA3_EJECT_RIGHT	0x70
+#define AYA3_RESET		0x88
+
+/* Config command RGB modes */
+#define AYA3_RGB_SOLID		0x01
+#define AYA3_RGB_PULSE		0x02
+#define AYA3_RGB_OFF		0xff
+
+#define AYA3_VIBRATION_DEFAULT	0x02	/* medium */
+
+struct aya3 {
+	struct hid_device *hdev;
+	/* DMA-safe command buffer; guarded by lock */
+	u8 *xfer;
+	/* Serializes commands and cached-config access */
+	struct mutex lock;
+	struct completion resp_done;
+	u8 resp[AYA3_RESP_SIZE];
+	u8 resp_expect;
+	bool resp_pending;
+
+	u8 rgb[3];
+	bool pulse;
+	u8 vibration;
+
+	struct led_classdev_mc mcled;
+	struct mc_subled subleds[3];
+};
+
+static int aya3_send(struct aya3 *aya)
+{
+	int ret;
+
+	ret = hid_hw_output_report(aya->hdev, aya->xfer, AYA3_REPORT_SIZE);
+	if (ret == -ENOSYS)
+		ret = hid_hw_raw_request(aya->hdev, aya->xfer[0], aya->xfer,
+					 AYA3_REPORT_SIZE, HID_OUTPUT_REPORT,
+					 HID_REQ_SET_REPORT);
+	if (ret < 0)
+		return ret;
+	return 0;
+}
+
+/**
+ * aya3_cmd() - send the command in aya->xfer and wait for the reply
+ * @aya: driver data; @aya->xfer holds the fully built 65-byte command
+ * @resp: destination for the AYA3_RESP_SIZE-byte reply, or NULL to
+ *        discard it
+ *
+ * The device echoes the subcommand byte of the command it is answering,
+ * which aya3_raw_event() uses to match replies. Unanswered commands are
+ * retried up to AYA3_CMD_ATTEMPTS times.
+ *
+ * Context: process context; the caller must hold @aya->lock, which
+ *          protects @aya->xfer and the reply state.
+ * Return: 0 on success, -ETIMEDOUT if every attempt went unanswered, or
+ *         a negative errno if sending failed.
+ */
+static int aya3_cmd(struct aya3 *aya, u8 *resp)
+{
+	int attempt, ret;
+
+	lockdep_assert_held(&aya->lock);
+
+	for (attempt = 0; attempt < AYA3_CMD_ATTEMPTS; attempt++) {
+		reinit_completion(&aya->resp_done);
+		aya->resp_expect = aya->xfer[4];
+		WRITE_ONCE(aya->resp_pending, true);
+
+		ret = aya3_send(aya);
+		if (ret) {
+			WRITE_ONCE(aya->resp_pending, false);
+			return ret;
+		}
+
+		if (wait_for_completion_timeout(&aya->resp_done,
+						msecs_to_jiffies(AYA3_CMD_TIMEOUT_MS))) {
+			if (resp)
+				memcpy(resp, aya->resp, AYA3_RESP_SIZE);
+			return 0;
+		}
+	}
+	WRITE_ONCE(aya->resp_pending, false);
+	return -ETIMEDOUT;
+}
+
+static void aya3_checksum(u8 *buf)
+{
+	u16 sum = 0;
+	int i;
+
+	for (i = 7; i < AYA3_REPORT_SIZE; i++)
+		sum += buf[i];
+	put_unaligned_le16(sum, buf + 1);
+}
+
+static int aya3_check(struct aya3 *aya, u8 *resp)
+{
+	memset(aya->xfer, 0, AYA3_REPORT_SIZE);
+	aya->xfer[4] = AYA3_SUBCMD_CHECK;
+	return aya3_cmd(aya, resp);
+}
+
+/*
+ * The config command sets everything at once: RGB for both rings,
+ * vibration strength and the eject/reset field. The command can also
+ * carry joystick sensitivity; those bytes are left zero so the
+ * firmware setting is not clobbered on every RGB update.
+ */
+static int aya3_send_config(struct aya3 *aya, u8 eject)
+{
+	static const u8 template[AYA3_REPORT_SIZE] = {
+		[3] = AYA3_CMD_CONFIG,
+		[4] = AYA3_SUBCMD_CONFIG,
+		[32] = 0x01,
+	};
+	u8 *buf = aya->xfer;
+	u8 mode = AYA3_RGB_OFF;
+
+	if (aya->rgb[0] || aya->rgb[1] || aya->rgb[2])
+		mode = aya->pulse ? AYA3_RGB_PULSE : AYA3_RGB_SOLID;
+
+	memcpy(buf, template, AYA3_REPORT_SIZE);
+	/* Right ring, then left ring: mode, R, G, B */
+	buf[8] = mode;
+	memcpy(buf + 9, aya->rgb, 3);
+	buf[12] = mode;
+	memcpy(buf + 13, aya->rgb, 3);
+	buf[20] = eject;
+	buf[24] = aya->vibration << 4;
+	aya3_checksum(buf);
+
+	return aya3_cmd(aya, NULL);
+}
+
+static int aya3_raw_event(struct hid_device *hdev, struct hid_report *report,
+			  u8 *data, int size)
+{
+	struct aya3 *aya = hid_get_drvdata(hdev);
+
+	if (!READ_ONCE(aya->resp_pending) || size < AYA3_RESP_SIZE)
+		return 0;
+	/*
+	 * Replies carry no sequence number, only the subcommand echo. A
+	 * late reply to a timed-out command can thus complete a newer
+	 * command with the same subcommand; such replies are snapshots
+	 * of the same query milliseconds apart, so this is harmless.
+	 * Replies to a different subcommand are dropped here.
+	 */
+	if (data[AYA3_RESP_CMD] != aya->resp_expect)
+		return 0;
+
+	memcpy(aya->resp, data, AYA3_RESP_SIZE);
+	WRITE_ONCE(aya->resp_pending, false);
+	complete(&aya->resp_done);
+	return 0;
+}
+
+static ssize_t aya3_module_show(struct device *dev, char *buf, int offset)
+{
+	struct aya3 *aya = dev_get_drvdata(dev);
+	u8 resp[AYA3_RESP_SIZE];
+	int ret;
+
+	ret = mutex_lock_interruptible(&aya->lock);
+	if (ret)
+		return ret;
+	ret = aya3_check(aya, resp);
+	mutex_unlock(&aya->lock);
+	if (ret)
+		return ret;
+
+	return sysfs_emit(buf, "0x%02x\n", resp[offset]);
+}
+
+static ssize_t module_left_show(struct device *dev,
+				struct device_attribute *attr, char *buf)
+{
+	return aya3_module_show(dev, buf, AYA3_RESP_MODULE_LEFT);
+}
+static DEVICE_ATTR_RO(module_left);
+
+static ssize_t module_right_show(struct device *dev,
+				 struct device_attribute *attr, char *buf)
+{
+	return aya3_module_show(dev, buf, AYA3_RESP_MODULE_RIGHT);
+}
+static DEVICE_ATTR_RO(module_right);
+
+static ssize_t eject_store(struct device *dev, struct device_attribute *attr,
+			   const char *buf, size_t count)
+{
+	struct aya3 *aya = dev_get_drvdata(dev);
+	u8 resp[AYA3_RESP_SIZE];
+	u8 eject;
+	int ret, err, i;
+
+	if (sysfs_streq(buf, "left"))
+		eject = AYA3_EJECT_LEFT;
+	else if (sysfs_streq(buf, "right"))
+		eject = AYA3_EJECT_RIGHT;
+	else if (sysfs_streq(buf, "both"))
+		eject = AYA3_EJECT_LEFT | AYA3_EJECT_RIGHT;
+	else
+		return -EINVAL;
+
+	ret = mutex_lock_interruptible(&aya->lock);
+	if (ret)
+		return ret;
+
+	ret = aya3_send_config(aya, eject);
+	if (ret)
+		goto out;
+
+	/*
+	 * Wait for the firmware to report the eject as done. Userspace
+	 * must then cut power through ayaneo-ec's controller_power for
+	 * the module to be physically released.
+	 */
+	ret = -ETIMEDOUT;
+	for (i = 0; i < 20; i++) {
+		msleep(400);
+		err = aya3_check(aya, resp);
+		if (err == -ETIMEDOUT)
+			continue;	/* busy mid-eject, keep polling */
+		if (err) {
+			ret = err;
+			break;
+		}
+		if (!(resp[AYA3_RESP_EJECT_STATUS] & ~AYA3_EJECT_DONE_MASK)) {
+			ret = 0;
+			break;
+		}
+	}
+out:
+	mutex_unlock(&aya->lock);
+	return ret ? ret : count;
+}
+static DEVICE_ATTR_WO(eject);
+
+static ssize_t reset_store(struct device *dev, struct device_attribute *attr,
+			   const char *buf, size_t count)
+{
+	struct aya3 *aya = dev_get_drvdata(dev);
+	bool value;
+	int ret;
+
+	ret = kstrtobool(buf, &value);
+	if (ret)
+		return ret;
+	if (!value)
+		return count;
+
+	ret = mutex_lock_interruptible(&aya->lock);
+	if (ret)
+		return ret;
+	ret = aya3_send_config(aya, AYA3_RESET);
+	if (!ret) {
+		msleep(500);
+		ret = aya3_send_config(aya, 0);
+	}
+	mutex_unlock(&aya->lock);
+	return ret ? ret : count;
+}
+static DEVICE_ATTR_WO(reset);
+
+static struct attribute *aya3_attrs[] = {
+	&dev_attr_module_left.attr,
+	&dev_attr_module_right.attr,
+	&dev_attr_eject.attr,
+	&dev_attr_reset.attr,
+	NULL
+};
+ATTRIBUTE_GROUPS(aya3);
+
+static int aya3_led_set(struct led_classdev *cdev, enum led_brightness value)
+{
+	struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
+	struct aya3 *aya = container_of(mc, struct aya3, mcled);
+	int ret, i;
+
+	ret = mutex_lock_interruptible(&aya->lock);
+	if (ret)
+		return ret;
+
+	led_mc_calc_color_components(mc, value);
+	for (i = 0; i < 3; i++)
+		aya->rgb[i] = min_t(unsigned int, aya->subleds[i].brightness, 255);
+
+	ret = aya3_send_config(aya, 0);
+	if (ret)
+		hid_err(aya->hdev, "failed to update RGB config: %d\n", ret);
+	mutex_unlock(&aya->lock);
+	return ret;
+}
+
+/*
+ * The firmware offers one fixed breathing pattern, pulsing the current
+ * colour at a period it controls. Expose it through the hw_pattern
+ * trigger ABI as the two-step pattern "0 <t> <brightness> <t>"; the
+ * delta_t values and the repeat count are accepted but not tunable
+ * (the firmware always repeats indefinitely).
+ */
+static int aya3_pattern_set(struct led_classdev *cdev,
+			    struct led_pattern *pattern, u32 len, int repeat)
+{
+	struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
+	struct aya3 *aya = container_of(mc, struct aya3, mcled);
+	int ret;
+
+	if (len != 2 || pattern[0].brightness || !pattern[1].brightness)
+		return -EINVAL;
+
+	ret = mutex_lock_interruptible(&aya->lock);
+	if (ret)
+		return ret;
+	aya->pulse = true;
+	ret = aya3_send_config(aya, 0);
+	mutex_unlock(&aya->lock);
+	return ret;
+}
+
+static int aya3_pattern_clear(struct led_classdev *cdev)
+{
+	struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
+	struct aya3 *aya = container_of(mc, struct aya3, mcled);
+	int ret;
+
+	ret = mutex_lock_interruptible(&aya->lock);
+	if (ret)
+		return ret;
+	aya->pulse = false;
+	ret = aya3_send_config(aya, 0);
+	mutex_unlock(&aya->lock);
+	return ret;
+}
+
+static int aya3_register_led(struct aya3 *aya)
+{
+	struct led_classdev *cdev = &aya->mcled.led_cdev;
+
+	aya->subleds[0].color_index = LED_COLOR_ID_RED;
+	aya->subleds[1].color_index = LED_COLOR_ID_GREEN;
+	aya->subleds[2].color_index = LED_COLOR_ID_BLUE;
+	aya->mcled.subled_info = aya->subleds;
+	aya->mcled.num_colors = 3;
+
+	cdev->name = devm_kasprintf(&aya->hdev->dev, GFP_KERNEL,
+				    "%s:rgb:joystick_rings",
+				    dev_name(&aya->hdev->dev));
+	if (!cdev->name)
+		return -ENOMEM;
+	cdev->brightness = 0;
+	cdev->max_brightness = 255;
+	cdev->brightness_set_blocking = aya3_led_set;
+	cdev->pattern_set = aya3_pattern_set;
+	cdev->pattern_clear = aya3_pattern_clear;
+
+	/*
+	 * Not devm: the LED must be unregistered before hid_hw_stop() in
+	 * remove, or a concurrent brightness write could reach a torn
+	 * down transport.
+	 */
+	return led_classdev_multicolor_register(&aya->hdev->dev,
+						&aya->mcled);
+}
+
+static const struct dmi_system_id aya3_dmi_table[] = {
+	{
+		.matches = {
+			DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
+			DMI_MATCH(DMI_BOARD_NAME, "AYANEO 3"),
+		},
+	},
+	{}
+};
+
+static int aya3_probe(struct hid_device *hdev, const struct hid_device_id *id)
+{
+	struct aya3 *aya;
+	int ret;
+
+	/* The VID/PID is a generic SigmaMicro ID; bind on AYANEO 3 only */
+	if (!dmi_check_system(aya3_dmi_table))
+		return -ENODEV;
+
+	if (!hid_is_usb(hdev))
+		return -ENODEV;
+
+	ret = hid_parse(hdev);
+	if (ret)
+		return ret;
+
+	/* Bind only the vendor interface, not the gamepad/keyboard ones */
+	if (!hdev->maxcollection ||
+	    hdev->collection->usage != (HID_UP_MSVENDOR | 0x0001))
+		return -ENODEV;
+
+	aya = devm_kzalloc(&hdev->dev, sizeof(*aya), GFP_KERNEL);
+	if (!aya)
+		return -ENOMEM;
+
+	aya->xfer = devm_kzalloc(&hdev->dev, AYA3_REPORT_SIZE, GFP_KERNEL);
+	if (!aya->xfer)
+		return -ENOMEM;
+
+	aya->hdev = hdev;
+	aya->vibration = AYA3_VIBRATION_DEFAULT;
+	init_completion(&aya->resp_done);
+	ret = devm_mutex_init(&hdev->dev, &aya->lock);
+	if (ret)
+		return ret;
+	hid_set_drvdata(hdev, aya);
+
+	ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW);
+	if (ret)
+		return ret;
+
+	ret = hid_hw_open(hdev);
+	if (ret)
+		goto err_stop;
+
+	/* Input reports are not delivered during probe by default */
+	hid_device_io_start(hdev);
+
+	scoped_guard(mutex, &aya->lock)
+		ret = aya3_check(aya, NULL);
+	if (ret)
+		hid_warn(hdev, "controller did not answer status check: %d\n",
+			 ret);
+
+	ret = aya3_register_led(aya);
+	if (ret)
+		goto err_close;
+
+	return 0;
+
+err_close:
+	hid_hw_close(hdev);
+err_stop:
+	hid_hw_stop(hdev);
+	return ret;
+}
+
+static void aya3_remove(struct hid_device *hdev)
+{
+	struct aya3 *aya = hid_get_drvdata(hdev);
+
+	led_classdev_multicolor_unregister(&aya->mcled);
+	/*
+	 * A brightness store racing with the unregister can requeue
+	 * set_brightness_work after the flush inside
+	 * led_classdev_unregister() runs but before the sysfs node is
+	 * removed. Flush again now that nothing can requeue it, while
+	 * the transport is still up.
+	 */
+	flush_work(&aya->mcled.led_cdev.set_brightness_work);
+	hid_hw_close(hdev);
+	hid_hw_stop(hdev);
+}
+
+static const struct hid_device_id aya3_devices[] = {
+	{ HID_USB_DEVICE(0x1c4f, 0x0002) },
+	{}
+};
+MODULE_DEVICE_TABLE(hid, aya3_devices);
+
+static struct hid_driver aya3_driver = {
+	.name = "hid-ayaneo",
+	.id_table = aya3_devices,
+	.probe = aya3_probe,
+	.remove = aya3_remove,
+	.raw_event = aya3_raw_event,
+	.driver = {
+		.dev_groups = aya3_groups,
+	},
+};
+module_hid_driver(aya3_driver);
+
+MODULE_AUTHOR("Matías Martínez <hello@matias.me>");
+MODULE_DESCRIPTION("AYANEO 3 detachable controller driver");
+MODULE_LICENSE("GPL");

base-commit: a8fcb3dbf9024da44f1614c42ea16001f4b860b0
-- 
2.54.0 (Apple Git-157)


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* Re: [PATCH v2] HID: ayaneo: Add AYANEO 3 detachable controller driver
  2026-08-24 22:31 ` [PATCH v2] " Matías Martínez
@ 2026-08-24 22:41   ` sashiko-bot
  0 siblings, 0 replies; 10+ messages in thread
From: sashiko-bot @ 2026-08-24 22:41 UTC (permalink / raw)
  To: Matías Martínez; +Cc: linux-input, dmitry.torokhov

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] Missing initialization of the multicolor LED channel intensities results in the LED remaining off by default.
- [Medium] The `hw_pattern` implementation completely ignores the brightness requested in the pattern string.
--

--- Patch [1]: [PATCH v2] HID: ayaneo: Add AYANEO 3 detachable controller driver ---
commit e8a8ced2df93a1a83b6b165f28028f3d597aafea
Author: Matías Martínez <hello@matias.me>

HID: ayaneo: Add AYANEO 3 detachable controller driver

This commit adds a driver for the vendor interface of the AYANEO 3
detachable controller. It provides module identification, software eject,
and RGB control of the joystick rings as a multicolor LED class device.

> diff --git a/drivers/hid/hid-ayaneo.c b/drivers/hid/hid-ayaneo.c
> new file mode 100644
> --- /dev/null
> +++ b/drivers/hid/hid-ayaneo.c
[ ... ]
> @@ -359,25 +359,25 @@ static int aya3_pattern_set(struct led_classdev *cdev,
>  			    struct led_pattern *pattern, u32 len, int repeat)
>  {
>  	struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
>  	struct aya3 *aya = container_of(mc, struct aya3, mcled);
>  	int ret;
>  
>  	if (len != 2 || pattern[0].brightness || !pattern[1].brightness)
>  		return -EINVAL;
>  
>  	ret = mutex_lock_interruptible(&aya->lock);
>  	if (ret)
>  		return ret;
>  	aya->pulse = true;
>  	ret = aya3_send_config(aya, 0);

[Severity: Medium]
Will this hardware breathing pattern fail to activate if the LED is currently
turned off?

In aya3_pattern_set(), the requested brightness in pattern[1].brightness is
validated but never used to update aya->rgb. If the LED was previously off,
aya->rgb remains {0, 0, 0}.

When aya3_send_config() is called, it checks aya->rgb to determine the mode:

    if (aya->rgb[0] || aya->rgb[1] || aya->rgb[2])
        mode = aya->pulse ? AYA3_RGB_PULSE : AYA3_RGB_SOLID;

If aya->rgb is all zeros, the mode will remain AYA3_RGB_OFF instead of
AYA3_RGB_PULSE, silently ignoring the pattern request. Also, if the LED was
already on, it will pulse with the previous brightness instead of the
requested brightness.

>  	mutex_unlock(&aya->lock);
>  	return ret;
>  }
[ ... ]
> @@ -389,17 +389,17 @@ static int aya3_register_led(struct aya3 *aya)
>  {
>  	struct led_classdev *cdev = &aya->mcled.led_cdev;
>  
>  	aya->subleds[0].color_index = LED_COLOR_ID_RED;
>  	aya->subleds[1].color_index = LED_COLOR_ID_GREEN;
>  	aya->subleds[2].color_index = LED_COLOR_ID_BLUE;
>  	aya->mcled.subled_info = aya->subleds;
>  	aya->mcled.num_colors = 3;

[Severity: Medium]
Is it intentional that the subled channel intensities are left uninitialized?

Because aya is allocated with devm_kzalloc(), the intensity fields in
aya->subleds are zero-initialized. When userspace sets the brightness,
aya3_led_set() calls led_mc_calc_color_components(), which multiplies the
requested brightness by each subled's intensity to calculate the final RGB
values.

Since the intensity is 0, the resulting color components will always be 0,
causing the LED to remain off by default when a brightness is requested.
Should the channel intensity fields be initialized here?

>  
>  	cdev->name = devm_kasprintf(&aya->hdev->dev, GFP_KERNEL,
>  				    "%s:rgb:joystick_rings",
>  				    dev_name(&aya->hdev->dev));
>  	if (!cdev->name)
>  		return -ENOMEM;
[ ... ]

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260824223103.93947-1-hello@matias.me?part=1

^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH] HID: ayaneo: Add AYANEO 3 detachable controller driver
  2026-08-24 22:25   ` Antheas Kapenekakis
@ 2026-08-24 22:47     ` Matías Martínez
  2026-08-24 23:23       ` Antheas Kapenekakis
  0 siblings, 1 reply; 10+ messages in thread
From: Matías Martínez @ 2026-08-24 22:47 UTC (permalink / raw)
  To: Antheas Kapenekakis
  Cc: Jiri Kosina, Benjamin Tissoires, linux-input, linux-kernel,
	Denis Benato

> If you need userspace coordination anyway, including the multiple
> timing hacks I had to implement, the question of having to route this
> through the kernel arises.

Fair question. What tipped it for me:

- The RGB wants to be a LED class device to be usable by the existing
  stacks (InputPlumber and friends consume /sys/class/leds, and with
  hw_pattern in v2 the breathing mode fits an accepted ABI). There is
  no hidraw equivalent short of every stack reimplementing the
  checksummed vendor protocol.

- The protocol and timing part (reply matching, retries, the eject
  handshake polling) now lives in one place. What remains in userspace
  is policy: when to cut controller power and what UX to wrap around
  it. That split also lets the module/eject controls be exposed as
  narrowly-scoped sysfs attributes instead of handing out the whole
  vendor interface through hidraw permissions.

- It complements ayaneo-ec, which already exposes attach state and
  controller power on the kernel side, so both halves of the flow sit
  at the same layer.

Working on this also flushed out a teardown bug that v2 fixes: a
brightness write racing a driver unbind could queue LED work that ran
after the transport was gone and the driver data freed. Reproducible
memory corruption under a write loop, and the window is reachable in
normal use, since the controller power-cycles on resume and on module
eject while userspace may be poking the LED.

> Overwriting joystick sensitivity is a bit problematic. Can you see if
> dropping those four bytes still allows RGB to go through? This might
> be preferable.

Confirmed on hardware: with bytes 22/23/37/38 left zero the firmware
still acks the config command, and RGB (solid and breathing) and eject
all work. v2 no longer writes them.

> Consider implementing the pulsing mode it offers, there should be an
> accepted ABI for it somewhere...

Done in v2 through the hw_pattern trigger ABI (pattern_set /
pattern_clear, same two-step shape as the sc27xx breathing pattern):
"0 <t> <brightness> <t>" selects the firmware's fixed-period breathing
at the current colour. Tested on the device, with an ABI document
added.

(v2 crossed your second mail in flight, so two things are still open
there:)

> Almost forgot. Magic value.
[...]
> Magic value. You need to justify those.

Right. Both are empirical firmware timings inherited from the
Handheld Daemon implementation (its reset sequence sleeps 0.5s
between the reset and the config restore, and it polls at a similar
cadence during eject); both are validated on hardware. Queued for v3
as named constants (AYA3_RESET_SETTLE_MS, AYA3_EJECT_POLL_MS/POLLS)
with a comment stating exactly that. I'll hold v3 briefly in case
more comes out of the v2 review.

Thanks for the review!

Matías

^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH] HID: ayaneo: Add AYANEO 3 detachable controller driver
  2026-08-24 22:01 ` sashiko-bot
@ 2026-08-24 22:47   ` Matías Martínez
  0 siblings, 0 replies; 10+ messages in thread
From: Matías Martínez @ 2026-08-24 22:47 UTC (permalink / raw)
  To: linux-input
  Cc: linux-kernel, Jiri Kosina, Benjamin Tissoires, Dmitry Torokhov,
	Antheas Kapenekakis, Denis Benato

Addressing the four findings for the humans on the thread — the bot
went two for four:

- The devres ordering issue is real, and understated: any brightness
  write racing a driver unbind could queue LED work that runs after
  hid_hw_stop() and after devres frees the driver data. I reproduced
  memory corruption by unbinding under a brightness-write loop, and
  the window is reachable in normal use (the controller power-cycles
  on resume and on module eject while userspace may be writing the
  LED). v2 registers the LED without devm, unregisters it first in
  remove, and flushes set_brightness_work again once the sysfs node
  is provably gone — a store can requeue it between the flush inside
  led_classdev_unregister() and the device_unregister() that follows.

- The eject-loop stall on unplug is real too. v2 aborts on fatal
  transport errors and only keeps polling on timeouts.

- The maxcollection finding is not a crash: hid_open_report()
  zero-allocates the collection array, so a descriptor with no
  collections reads usage 0 and probe returns -ENODEV. v2 adds an
  explicit maxcollection check anyway, as it states the intent
  better.

- The stale-reply race cannot cross commands: replies are matched on
  the echoed subcommand byte, so a late reply can only complete a
  retry of the same command, and those are interchangeable snapshots
  milliseconds apart. v2 documents this in aya3_raw_event().

All of the above is in v2, along with Antheas's review items.

Matías

^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH] HID: ayaneo: Add AYANEO 3 detachable controller driver
  2026-08-24 22:47     ` Matías Martínez
@ 2026-08-24 23:23       ` Antheas Kapenekakis
  2026-08-25 17:21         ` Matías Martínez
  0 siblings, 1 reply; 10+ messages in thread
From: Antheas Kapenekakis @ 2026-08-24 23:23 UTC (permalink / raw)
  To: Matías Martínez
  Cc: Jiri Kosina, Benjamin Tissoires, linux-input, linux-kernel,
	Denis Benato

On Tue, 25 Aug 2026 at 00:47, Matías Martínez <hello@matias.me> wrote:
>
> > If you need userspace coordination anyway, including the multiple
> > timing hacks I had to implement, the question of having to route this
> > through the kernel arises.
>
> Fair question. What tipped it for me:
>
> - The RGB wants to be a LED class device to be usable by the existing
>   stacks (InputPlumber and friends consume /sys/class/leds, and with
>   hw_pattern in v2 the breathing mode fits an accepted ABI). There is
>   no hidraw equivalent short of every stack reimplementing the
>   checksummed vendor protocol.
>
> - The protocol and timing part (reply matching, retries, the eject
>   handshake polling) now lives in one place. What remains in userspace
>   is policy: when to cut controller power and what UX to wrap around
>   it. That split also lets the module/eject controls be exposed as
>   narrowly-scoped sysfs attributes instead of handing out the whole
>   vendor interface through hidraw permissions.
>
> - It complements ayaneo-ec, which already exposes attach state and
>   controller power on the kernel side, so both halves of the flow sit
>   at the same layer.
>
> Working on this also flushed out a teardown bug that v2 fixes: a
> brightness write racing a driver unbind could queue LED work that ran
> after the transport was gone and the driver data freed. Reproducible
> memory corruption under a write loop, and the window is reachable in
> normal use, since the controller power-cycles on resume and on module
> eject while userspace may be poking the LED.
>
> > Overwriting joystick sensitivity is a bit problematic. Can you see if
> > dropping those four bytes still allows RGB to go through? This might
> > be preferable.
>
> Confirmed on hardware: with bytes 22/23/37/38 left zero the firmware
> still acks the config command, and RGB (solid and breathing) and eject
> all work. v2 no longer writes them.
>
> > Consider implementing the pulsing mode it offers, there should be an
> > accepted ABI for it somewhere...
>
> Done in v2 through the hw_pattern trigger ABI (pattern_set /
> pattern_clear, same two-step shape as the sc27xx breathing pattern):
> "0 <t> <brightness> <t>" selects the firmware's fixed-period breathing
> at the current colour. Tested on the device, with an ABI document
> added.
>
> (v2 crossed your second mail in flight, so two things are still open
> there:)
>
> > Almost forgot. Magic value.
> [...]
> > Magic value. You need to justify those.
>
> Right. Both are empirical firmware timings inherited from the
> Handheld Daemon implementation (its reset sequence sleeps 0.5s
> between the reset and the config restore, and it polls at a similar
> cadence during eject); both are validated on hardware. Queued for v3
> as named constants (AYA3_RESET_SETTLE_MS, AYA3_EJECT_POLL_MS/POLLS)
> with a comment stating exactly that. I'll hold v3 briefly in case
> more comes out of the v2 review.

They are eyeballed timings that worked during my testing. Because I
operate in userspace I have the freedom to choose whatever timings I
want and change them whenever I want. Carrying them to the kernel
freezes them for the vendor device and there is a higher level of
scrutiny required before they are merged. Fiddling with timings is not
something that's favored in kernel development. You also only carry
part of the policy. Userspace still has to coordinate between the
calls to the EC so that is left to userspace. I am pretty sure I have
a lot of timing quirks there as well. So now you have a split policy
and fixing your userspace implementation requires that other
distributions backport your fixes, otherwise your software will not
work.

The full implementation would require a bridge similar to how hid-asus
talks to asus-wmi with a common header. It would also probably require
more timing quirks. The payout for asus is bigger though, because you
kind of need a working keyboard and brightness button and that should
not require userspace software. Asus devices also do not need timing
quirks. For a niche device with unstable firmware, not so much. Expect
upstreaming such a bridge to take around 4-6 months at minimum.

Those are my 2 cents.

FYI direct EC/ACPI/TDP access is a security boundary, so a kernel
driver is required for those specific subcomponents, but when it is
trivial for something like Chrome to talk directly to USB devices,
that argument does not hold much water for the controller itself.
There are already TDP driver patches for all current handhelds in the
market, so as far as I am concerned, I will slowly start upstreaming
my backlog and keep sending and reviewing dmi matches for the existing
drivers.

I did not disagree on the RGB part, that's a decent addition barring
timing quirks being needed and it should be relatively easy to
upstream. Some other downstream users find RGB control via a
standardized interface userful. I would advise some caution, because
e.g., I noticed hid-oxp got upstreamed using a global drvdata table
even though it is a HID driver and there are actually multiple
OneXPlayer models that carry both hid devices and now they will
potentially have their kernel memory corrupted. I think the cover
letter of the series said so as well [0]. I am not sure
oxp_hybrid_mcu_list is authoritative enough.

Give it a few days before sending a V3, others should leave feedback
as well. You sent V2 a bit too fast.

Best,
Antheas

[0] https://lore.kernel.org/all/20260407041354.2283201-1-derekjohn.clark@gmail.com/

> Thanks for the review!
>
> Matías
>


^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH] HID: ayaneo: Add AYANEO 3 detachable controller driver
  2026-08-24 23:23       ` Antheas Kapenekakis
@ 2026-08-25 17:21         ` Matías Martínez
  0 siblings, 0 replies; 10+ messages in thread
From: Matías Martínez @ 2026-08-25 17:21 UTC (permalink / raw)
  To: Antheas Kapenekakis
  Cc: Jiri Kosina, Benjamin Tissoires, linux-input, linux-kernel,
	Denis Benato

> They are eyeballed timings that worked during my testing. Because I
> operate in userspace I have the freedom to choose whatever timings I
> want and change them whenever I want. Carrying them to the kernel
> freezes them for the vendor device and there is a higher level of
> scrutiny required before they are merged.

To be precise about what would actually be frozen: the ABI is
"writing 'left' to eject returns once the firmware confirms the
release". The poll cadence and the timeout behind that are
implementation details, so they can be retuned in-kernel later
without breaking userspace. Your larger point stands though: they are
eyeballed numbers, they now sit under kernel scrutiny, and I am the
one signing up to maintain them.

> You also only carry part of the policy. Userspace still has to
> coordinate between the calls to the EC so that is left to userspace.
> I am pretty sure I have a lot of timing quirks there as well. So now
> you have a split policy

The split I ended up with is a bit cleaner than that: the kernel
absorbed the timed parts of the protocol (reply matching, retries,
the eject handshake polling), and what remains in userspace is
ordered rather than timed -- wait for the blocking eject write to
return, then cut controller_power. The UI I tested against has no
timing loops left in its eject path. Whether that split carries its
weight for a niche device is exactly the scope question, and I am
happy to follow the HID maintainers' call on it -- including trimming
the driver to the LED plus module identification and leaving
eject/reset to userspace over hidraw, if that is where they land.

> I did not disagree on the RGB part, that's a decent addition barring
> timing quirks being needed and it should be relatively easy to
> upstream.

Good to hear. No timing quirks on that path: the config command is a
single write with a reply echo, and solid/breathing/off all worked
first try on hardware without settle delays.

> I would advise some caution, because e.g., I noticed hid-oxp got
> upstreamed using a global drvdata table even though it is a HID
> driver and there are actually multiple OneXPlayer models that carry
> both hid devices and now they will potentially have their kernel
> memory corrupted.

Thanks for the pointer -- I went and checked. hid-ayaneo keeps all
state in a per-device struct (devm-allocated, reached through
hid_get_drvdata); the only file-scope objects are const tables, so
multiple bound instances each get their own state.

> Give it a few days before sending a V3, others should leave feedback
> as well. You sent V2 a bit too fast.

That is fair -- v3 will wait until the thread has settled and the
maintainers have had a chance to weigh in.

Thanks, this was a useful mail.

Matías

^ permalink raw reply	[flat|nested] 10+ messages in thread

end of thread, other threads:[~2026-08-25 17:21 UTC | newest]

Thread overview: 10+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-24 21:50 [PATCH] HID: ayaneo: Add AYANEO 3 detachable controller driver Matías Martínez
2026-08-24 22:00 ` Antheas Kapenekakis
2026-08-24 22:25   ` Antheas Kapenekakis
2026-08-24 22:47     ` Matías Martínez
2026-08-24 23:23       ` Antheas Kapenekakis
2026-08-25 17:21         ` Matías Martínez
2026-08-24 22:01 ` sashiko-bot
2026-08-24 22:47   ` Matías Martínez
2026-08-24 22:31 ` [PATCH v2] " Matías Martínez
2026-08-24 22:41   ` sashiko-bot

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