* [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
2026-08-24 22:31 ` [PATCH v2] " Matías Martínez
0 siblings, 2 replies; 15+ 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 [flat|nested] 15+ 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:31 ` [PATCH v2] " Matías Martínez
1 sibling, 1 reply; 15+ 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] 15+ 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; 15+ 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] 15+ 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:31 ` Matías Martínez
2026-09-17 16:07 ` [PATCH v3] " Matías Martínez
1 sibling, 1 reply; 15+ 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 [flat|nested] 15+ 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; 15+ 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] 15+ 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; 15+ 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] 15+ 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; 15+ 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] 15+ messages in thread
* [PATCH v3] HID: ayaneo: Add AYANEO 3 detachable controller driver
2026-08-24 22:31 ` [PATCH v2] " Matías Martínez
@ 2026-09-17 16:07 ` Matías Martínez
2026-09-17 21:48 ` Antheas Kapenekakis
0 siblings, 1 reply; 15+ messages in thread
From: Matías Martínez @ 2026-09-17 16:07 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Antheas Kapenekakis, Denis Benato,
Dmitry Torokhov, Derek J . Clark, Ilpo Järvinen, Armin Wolf
From: Matías Martínez <hello@matias.me>
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 v3:
- Use a generic ayaneo_ prefix for entry points and driver structure
so a future device or protocol revision slots in without churn;
wire-protocol constants stay AYA3_* since they are specific to
this firmware generation. [Derek J. Clark]
- Describe the wire format with packed aya3_config/aya3_resp structs,
static_assert their sizes against the report sizes, and name every
firmware vibration level in an enum instead of a lone default
define. [Derek J. Clark]
- Take the command lock with scoped_cond_guard(mutex_intr, ...) at
every interruptible lock site so the unlock cannot be dropped in a
future edit. [Derek J. Clark]
- Mark the LED class device LED_COLOR_ID_RGB so userspace can detect
the RGB interface generically. [Derek J. Clark]
- Name the firmware timing constants and record where the timings
come from and what was validated on hardware.
No functional change relative to v2. The rework was prompted by
Derek J. Clark's review of the driver in the OpenGamingCollective
tree and retested on an AYANEO 3: module identification, RGB solid
and breathing via hw_pattern, rejection of malformed hw_pattern
writes, and repeated bind/unbind cycles, all with a clean dmesg.
One question from that review was whether RGB sysfs writes need
debouncing, since Steam emits one write per slider increment during
a color drag. They do not: the driver registers only
brightness_set_blocking, so the LED core defers stores to its
set_brightness_work and coalesces bursts to the latest state.
Measured on hardware, a command+ACK round trip averages 5.3 ms
(3.8-8 ms over 100 samples) and 1000 back-to-back multi_intensity
stores return in 12 ms total, reaching the device as two to three
commands.
Also suggested in that review, but held out of the patch while the
driver's scope is under discussion: a rumble_intensity attribute
(the firmware takes three vibration levels), an eject_index
attribute, and a notification path from hid-ayaneo to ayaneo-ec so
the EC driver could react to ejects. The last one concerns the
ayaneo-ec/pdx86 side, hence the added Cc.
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 | 594 ++++++++++++++++++
6 files changed, 668 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 8b14f290c..3290d9957 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -4508,6 +4508,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 0e3a0ccd6..319eda887 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 79384d905..2f74f2867 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..3a7af5918
--- /dev/null
+++ b/drivers/hid/hid-ayaneo.c
@@ -0,0 +1,594 @@
+// 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/build_bug.h>
+#include <linux/cleanup.h>
+#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
+
+/*
+ * Empirical timings, inherited from the Handheld Daemon
+ * implementation of this protocol and validated on hardware: the
+ * device answers well within 300ms or not at all, needs about half
+ * a second to settle after a reset before it accepts a new
+ * configuration, and completes an eject handshake within a few
+ * seconds (polled below at a rate that keeps the sysfs write
+ * responsive).
+ */
+#define AYA3_CMD_TIMEOUT_MS 300
+#define AYA3_CMD_ATTEMPTS 3
+#define AYA3_RESET_SETTLE_MS 500
+#define AYA3_EJECT_POLL_MS 400
+#define AYA3_EJECT_POLLS 20
+
+/* 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
+
+/* 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
+
+/* Config command vibration levels, stored in the high nibble */
+enum aya3_vibration {
+ AYA3_VIBRATION_LOW = 0x1,
+ AYA3_VIBRATION_MEDIUM = 0x2,
+ AYA3_VIBRATION_HIGH = 0x3,
+ AYA3_VIBRATION_OFF = 0x4,
+};
+
+struct aya3_rgb {
+ u8 mode;
+ u8 r;
+ u8 g;
+ u8 b;
+} __packed;
+
+/*
+ * The 65-byte config command. The checksum is the little-endian sum of
+ * bytes 7..64; unk* fields are sent as zero.
+ */
+struct aya3_config {
+ u8 report_id;
+ __le16 csum;
+ u8 cmd;
+ u8 subcmd;
+ u8 unk5[3];
+ struct aya3_rgb right;
+ struct aya3_rgb left;
+ u8 unk16[4];
+ u8 eject;
+ u8 unk21;
+ u8 sensitivity[2];
+ u8 vibration;
+ u8 unk25[7];
+ u8 magic;
+ u8 unk33[32];
+} __packed;
+static_assert(sizeof(struct aya3_config) == AYA3_REPORT_SIZE);
+
+/* Replies echo the subcommand they answer at byte 3 */
+struct aya3_resp {
+ u8 unk0[3];
+ u8 subcmd;
+ u8 unk4[15];
+ u8 eject_status;
+ u8 unk20[12];
+ u8 module_left;
+ u8 module_right;
+ u8 unk34[30];
+} __packed;
+static_assert(sizeof(struct aya3_resp) == AYA3_RESP_SIZE);
+
+struct ayaneo {
+ 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;
+ struct aya3_resp resp;
+ 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 ayaneo_send(struct ayaneo *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;
+}
+
+/**
+ * ayaneo_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 reply, or NULL to discard it
+ *
+ * The device echoes the subcommand byte of the command it is answering,
+ * which ayaneo_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 ayaneo_cmd(struct ayaneo *aya, struct aya3_resp *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 = ayaneo_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, sizeof(*resp));
+ return 0;
+ }
+ }
+ WRITE_ONCE(aya->resp_pending, false);
+ return -ETIMEDOUT;
+}
+
+static void ayaneo_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 ayaneo_check(struct ayaneo *aya, struct aya3_resp *resp)
+{
+ memset(aya->xfer, 0, AYA3_REPORT_SIZE);
+ aya->xfer[4] = AYA3_SUBCMD_CHECK;
+ return ayaneo_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 ayaneo_send_config(struct ayaneo *aya, u8 eject)
+{
+ static const struct aya3_config template = {
+ .cmd = AYA3_CMD_CONFIG,
+ .subcmd = AYA3_SUBCMD_CONFIG,
+ .magic = 0x01,
+ };
+ struct aya3_config *cfg = (struct aya3_config *)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;
+
+ *cfg = template;
+ cfg->right.mode = mode;
+ cfg->right.r = aya->rgb[0];
+ cfg->right.g = aya->rgb[1];
+ cfg->right.b = aya->rgb[2];
+ cfg->left = cfg->right;
+ cfg->eject = eject;
+ cfg->vibration = aya->vibration << 4;
+ ayaneo_checksum(aya->xfer);
+
+ return ayaneo_cmd(aya, NULL);
+}
+
+static int ayaneo_raw_event(struct hid_device *hdev, struct hid_report *report,
+ u8 *data, int size)
+{
+ struct ayaneo *aya = hid_get_drvdata(hdev);
+ const struct aya3_resp *resp = (const struct aya3_resp *)data;
+
+ 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 (resp->subcmd != aya->resp_expect)
+ return 0;
+
+ memcpy(&aya->resp, data, sizeof(aya->resp));
+ WRITE_ONCE(aya->resp_pending, false);
+ complete(&aya->resp_done);
+ return 0;
+}
+
+static ssize_t ayaneo_module_show(struct device *dev, char *buf, bool right)
+{
+ struct ayaneo *aya = dev_get_drvdata(dev);
+ struct aya3_resp resp;
+ int ret = 0;
+
+ scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock)
+ ret = ayaneo_check(aya, &resp);
+ if (ret)
+ return ret;
+
+ return sysfs_emit(buf, "0x%02x\n",
+ right ? resp.module_right : resp.module_left);
+}
+
+static ssize_t module_left_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return ayaneo_module_show(dev, buf, false);
+}
+static DEVICE_ATTR_RO(module_left);
+
+static ssize_t module_right_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return ayaneo_module_show(dev, buf, true);
+}
+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 ayaneo *aya = dev_get_drvdata(dev);
+ struct aya3_resp resp;
+ u8 eject;
+ int ret = 0, 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;
+
+ scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock) {
+ ret = ayaneo_send_config(aya, eject);
+ if (ret)
+ break;
+
+ /*
+ * 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 < AYA3_EJECT_POLLS; i++) {
+ msleep(AYA3_EJECT_POLL_MS);
+ err = ayaneo_check(aya, &resp);
+ if (err == -ETIMEDOUT)
+ continue; /* busy mid-eject, keep polling */
+ if (err) {
+ ret = err;
+ break;
+ }
+ if (!(resp.eject_status & ~AYA3_EJECT_DONE_MASK)) {
+ ret = 0;
+ break;
+ }
+ }
+ }
+ 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 ayaneo *aya = dev_get_drvdata(dev);
+ bool value;
+ int ret;
+
+ ret = kstrtobool(buf, &value);
+ if (ret)
+ return ret;
+ if (!value)
+ return count;
+
+ scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock) {
+ ret = ayaneo_send_config(aya, AYA3_RESET);
+ if (!ret) {
+ msleep(AYA3_RESET_SETTLE_MS);
+ ret = ayaneo_send_config(aya, 0);
+ }
+ }
+ return ret ? ret : count;
+}
+static DEVICE_ATTR_WO(reset);
+
+static struct attribute *ayaneo_attrs[] = {
+ &dev_attr_module_left.attr,
+ &dev_attr_module_right.attr,
+ &dev_attr_eject.attr,
+ &dev_attr_reset.attr,
+ NULL
+};
+ATTRIBUTE_GROUPS(ayaneo);
+
+static int ayaneo_led_set(struct led_classdev *cdev, enum led_brightness value)
+{
+ struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
+ struct ayaneo *aya = container_of(mc, struct ayaneo, mcled);
+ int ret = 0, i;
+
+ scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock) {
+ 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 = ayaneo_send_config(aya, 0);
+ if (ret)
+ hid_err(aya->hdev,
+ "failed to update RGB config: %d\n", ret);
+ }
+ 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 ayaneo_pattern_set(struct led_classdev *cdev,
+ struct led_pattern *pattern, u32 len, int repeat)
+{
+ struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
+ struct ayaneo *aya = container_of(mc, struct ayaneo, mcled);
+ int ret = 0;
+
+ if (len != 2 || pattern[0].brightness || !pattern[1].brightness)
+ return -EINVAL;
+
+ scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock) {
+ aya->pulse = true;
+ ret = ayaneo_send_config(aya, 0);
+ }
+ return ret;
+}
+
+static int ayaneo_pattern_clear(struct led_classdev *cdev)
+{
+ struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
+ struct ayaneo *aya = container_of(mc, struct ayaneo, mcled);
+ int ret = 0;
+
+ scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock) {
+ aya->pulse = false;
+ ret = ayaneo_send_config(aya, 0);
+ }
+ return ret;
+}
+
+static int ayaneo_register_led(struct ayaneo *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->color = LED_COLOR_ID_RGB;
+ cdev->brightness = 0;
+ cdev->max_brightness = 255;
+ cdev->brightness_set_blocking = ayaneo_led_set;
+ cdev->pattern_set = ayaneo_pattern_set;
+ cdev->pattern_clear = ayaneo_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 ayaneo_dmi_table[] = {
+ {
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
+ DMI_MATCH(DMI_BOARD_NAME, "AYANEO 3"),
+ },
+ },
+ {}
+};
+
+static int ayaneo_probe(struct hid_device *hdev, const struct hid_device_id *id)
+{
+ struct ayaneo *aya;
+ int ret;
+
+ /* The VID/PID is a generic SigmaMicro ID; bind on AYANEO 3 only */
+ if (!dmi_check_system(ayaneo_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_MEDIUM;
+ 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 = ayaneo_check(aya, NULL);
+ if (ret)
+ hid_warn(hdev, "controller did not answer status check: %d\n",
+ ret);
+
+ ret = ayaneo_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 ayaneo_remove(struct hid_device *hdev)
+{
+ struct ayaneo *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 ayaneo_devices[] = {
+ { HID_USB_DEVICE(0x1c4f, 0x0002) },
+ {}
+};
+MODULE_DEVICE_TABLE(hid, ayaneo_devices);
+
+static struct hid_driver ayaneo_driver = {
+ .name = "hid-ayaneo",
+ .id_table = ayaneo_devices,
+ .probe = ayaneo_probe,
+ .remove = ayaneo_remove,
+ .raw_event = ayaneo_raw_event,
+ .driver = {
+ .dev_groups = ayaneo_groups,
+ },
+};
+module_hid_driver(ayaneo_driver);
+
+MODULE_AUTHOR("Matías Martínez <hello@matias.me>");
+MODULE_DESCRIPTION("AYANEO 3 detachable controller driver");
+MODULE_LICENSE("GPL");
--
2.54.0 (Apple Git-157)
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v3] HID: ayaneo: Add AYANEO 3 detachable controller driver
2026-09-17 16:07 ` [PATCH v3] " Matías Martínez
@ 2026-09-17 21:48 ` Antheas Kapenekakis
2026-09-17 22:23 ` Matías Martínez
2026-09-18 0:42 ` Derek J. Clark
0 siblings, 2 replies; 15+ messages in thread
From: Antheas Kapenekakis @ 2026-09-17 21:48 UTC (permalink / raw)
To: Matías Martínez
Cc: Jiri Kosina, Benjamin Tissoires, linux-input, linux-kernel,
Denis Benato, Dmitry Torokhov, Derek J . Clark,
Ilpo Järvinen, Armin Wolf
On Thu, 17 Sept 2026 at 18:07, Matías Martínez <hello@matias.me> wrote:
>
> From: Matías Martínez <hello@matias.me>
Hi Matias,
> 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 v3:
> - Use a generic ayaneo_ prefix for entry points and driver structure
> so a future device or protocol revision slots in without churn;
> wire-protocol constants stay AYA3_* since they are specific to
> this firmware generation. [Derek J. Clark]
> - Describe the wire format with packed aya3_config/aya3_resp structs,
> static_assert their sizes against the report sizes, and name every
> firmware vibration level in an enum instead of a lone default
> define. [Derek J. Clark]
> - Take the command lock with scoped_cond_guard(mutex_intr, ...) at
> every interruptible lock site so the unlock cannot be dropped in a
> future edit. [Derek J. Clark]
> - Mark the LED class device LED_COLOR_ID_RGB so userspace can detect
> the RGB interface generically. [Derek J. Clark]
> - Name the firmware timing constants and record where the timings
> come from and what was validated on hardware.
>
> No functional change relative to v2. The rework was prompted by
> Derek J. Clark's review of the driver in the OpenGamingCollective
> tree and retested on an AYANEO 3: module identification, RGB solid
> and breathing via hw_pattern, rejection of malformed hw_pattern
> writes, and repeated bind/unbind cycles, all with a clean dmesg.
>
> One question from that review was whether RGB sysfs writes need
> debouncing, since Steam emits one write per slider increment during
> a color drag. They do not: the driver registers only
> brightness_set_blocking, so the LED core defers stores to its
> set_brightness_work and coalesces bursts to the latest state.
> Measured on hardware, a command+ACK round trip averages 5.3 ms
> (3.8-8 ms over 100 samples) and 1000 back-to-back multi_intensity
> stores return in 12 ms total, reaching the device as two to three
> commands.
After you submitted your patch series to the lore, why didn't the
review take place here and take place downstream? I understand that
prior to submitting your first kernel patch, it is natural to get some
informal feedback, but it seems that all of the reviews of this driver
happened outside the submission and after the submission? The rby
Denis should not be added by you. It should be added by denis by
replying to the mailing list and then you carry it forward on future
revisions (for provenance).
If you are developing a downstream kernel patch for
OGC/Bazzite/whoever, it is perfectly fine to do downstream work and
reviews and only submit the driver after it is ready. In fact, it
would be very preferable for all of us for you to test your driver
downstream, resolve all feedback and then submit it when it's ready.
But mixing this is peculiar. This is not a comment on you, you were
not the one reviewing your patch out of band.
> Also suggested in that review, but held out of the patch while the
> driver's scope is under discussion: a rumble_intensity attribute
> (the firmware takes three vibration levels), an eject_index
> attribute, and a notification path from hid-ayaneo to ayaneo-ec so
> the EC driver could react to ejects. The last one concerns the
> ayaneo-ec/pdx86 side, hence the added Cc.
>
> 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 | 594 ++++++++++++++++++
> 6 files changed, 668 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
Consider exploring similar ABIs in e.g., Legion Go S hid and mirroring
their ABI. Then remove this file. This is not the only breathing
device. Moreover, delta_t values are ignored and you are introducing
an ABI for them? Prefer a mode setting that can be standard/breathing
mirroring a different driver.
> +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 8b14f290c..3290d9957 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -4508,6 +4508,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 0e3a0ccd6..319eda887 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 79384d905..2f74f2867 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..3a7af5918
> --- /dev/null
> +++ b/drivers/hid/hid-ayaneo.c
> @@ -0,0 +1,594 @@
> +// 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/build_bug.h>
> +#include <linux/cleanup.h>
> +#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
> +
> +/*
> + * Empirical timings, inherited from the Handheld Daemon
> + * implementation of this protocol and validated on hardware: the
> + * device answers well within 300ms or not at all, needs about half
> + * a second to settle after a reset before it accepts a new
> + * configuration, and completes an eject handshake within a few
> + * seconds (polled below at a rate that keeps the sysfs write
> + * responsive).
> + */
> +#define AYA3_CMD_TIMEOUT_MS 300
> +#define AYA3_CMD_ATTEMPTS 3
> +#define AYA3_RESET_SETTLE_MS 500
> +#define AYA3_EJECT_POLL_MS 400
> +#define AYA3_EJECT_POLLS 20
> +
> +/* 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
> +
> +/* 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
> +
> +/* Config command vibration levels, stored in the high nibble */
> +enum aya3_vibration {
> + AYA3_VIBRATION_LOW = 0x1,
> + AYA3_VIBRATION_MEDIUM = 0x2,
> + AYA3_VIBRATION_HIGH = 0x3,
> + AYA3_VIBRATION_OFF = 0x4,
> +};
> +
> +struct aya3_rgb {
> + u8 mode;
> + u8 r;
> + u8 g;
> + u8 b;
> +} __packed;
> +
> +/*
> + * The 65-byte config command. The checksum is the little-endian sum of
> + * bytes 7..64; unk* fields are sent as zero.
> + */
> +struct aya3_config {
> + u8 report_id;
> + __le16 csum;
> + u8 cmd;
> + u8 subcmd;
> + u8 unk5[3];
> + struct aya3_rgb right;
> + struct aya3_rgb left;
> + u8 unk16[4];
> + u8 eject;
> + u8 unk21;
> + u8 sensitivity[2];
> + u8 vibration;
> + u8 unk25[7];
> + u8 magic;
> + u8 unk33[32];
I am not sure of the struct naming or whether a struct is needed in
this case. if you do not use most of the report, consider documenting
it somewhere else and doing direct accesses to the appropriate bytes.
> +} __packed;
> +static_assert(sizeof(struct aya3_config) == AYA3_REPORT_SIZE);
> +
> +/* Replies echo the subcommand they answer at byte 3 */
> +struct aya3_resp {
> + u8 unk0[3];
> + u8 subcmd;
> + u8 unk4[15];
> + u8 eject_status;
> + u8 unk20[12];
> + u8 module_left;
> + u8 module_right;
> + u8 unk34[30];
> +} __packed;
> +static_assert(sizeof(struct aya3_resp) == AYA3_RESP_SIZE);
> +
> +struct ayaneo {
> + 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;
> + struct aya3_resp resp;
> + 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 ayaneo_send(struct ayaneo *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;
> +}
> +
> +/**
> + * ayaneo_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 reply, or NULL to discard it
> + *
> + * The device echoes the subcommand byte of the command it is answering,
> + * which ayaneo_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 ayaneo_cmd(struct ayaneo *aya, struct aya3_resp *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 = ayaneo_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, sizeof(*resp));
> + return 0;
> + }
> + }
> + WRITE_ONCE(aya->resp_pending, false);
> + return -ETIMEDOUT;
> +}
> +
> +static void ayaneo_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 ayaneo_check(struct ayaneo *aya, struct aya3_resp *resp)
> +{
> + memset(aya->xfer, 0, AYA3_REPORT_SIZE);
> + aya->xfer[4] = AYA3_SUBCMD_CHECK;
> + return ayaneo_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 ayaneo_send_config(struct ayaneo *aya, u8 eject)
> +{
> + static const struct aya3_config template = {
> + .cmd = AYA3_CMD_CONFIG,
> + .subcmd = AYA3_SUBCMD_CONFIG,
> + .magic = 0x01,
> + };
> + struct aya3_config *cfg = (struct aya3_config *)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;
> +
> + *cfg = template;
> + cfg->right.mode = mode;
> + cfg->right.r = aya->rgb[0];
> + cfg->right.g = aya->rgb[1];
> + cfg->right.b = aya->rgb[2];
> + cfg->left = cfg->right;
> + cfg->eject = eject;
> + cfg->vibration = aya->vibration << 4;
If you set vibration, you need to expose it to userspace. Otherwise
this driver degrades functionality over userspace implementations.
> + ayaneo_checksum(aya->xfer);
> +
> + return ayaneo_cmd(aya, NULL);
> +}
> +
> +static int ayaneo_raw_event(struct hid_device *hdev, struct hid_report *report,
> + u8 *data, int size)
> +{
> + struct ayaneo *aya = hid_get_drvdata(hdev);
> + const struct aya3_resp *resp = (const struct aya3_resp *)data;
> +
> + 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 (resp->subcmd != aya->resp_expect)
> + return 0;
> +
> + memcpy(&aya->resp, data, sizeof(aya->resp));
> + WRITE_ONCE(aya->resp_pending, false);
> + complete(&aya->resp_done);
> + return 0;
> +}
> +
> +static ssize_t ayaneo_module_show(struct device *dev, char *buf, bool right)
> +{
> + struct ayaneo *aya = dev_get_drvdata(dev);
> + struct aya3_resp resp;
> + int ret = 0;
> +
> + scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock)
> + ret = ayaneo_check(aya, &resp);
> + if (ret)
> + return ret;
> +
> + return sysfs_emit(buf, "0x%02x\n",
> + right ? resp.module_right : resp.module_left);
> +}
> +
> +static ssize_t module_left_show(struct device *dev,
> + struct device_attribute *attr, char *buf)
> +{
> + return ayaneo_module_show(dev, buf, false);
> +}
> +static DEVICE_ATTR_RO(module_left);
> +
> +static ssize_t module_right_show(struct device *dev,
> + struct device_attribute *attr, char *buf)
> +{
> + return ayaneo_module_show(dev, buf, true);
> +}
> +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 ayaneo *aya = dev_get_drvdata(dev);
> + struct aya3_resp resp;
> + u8 eject;
> + int ret = 0, 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;
> +
> + scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock) {
> + ret = ayaneo_send_config(aya, eject);
> + if (ret)
> + break;
> +
> + /*
> + * 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 < AYA3_EJECT_POLLS; i++) {
> + msleep(AYA3_EJECT_POLL_MS);
> + err = ayaneo_check(aya, &resp);
> + if (err == -ETIMEDOUT)
> + continue; /* busy mid-eject, keep polling */
> + if (err) {
> + ret = err;
> + break;
> + }
> + if (!(resp.eject_status & ~AYA3_EJECT_DONE_MASK)) {
> + ret = 0;
> + break;
> + }
> + }
> + }
> + 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 ayaneo *aya = dev_get_drvdata(dev);
> + bool value;
> + int ret;
> +
> + ret = kstrtobool(buf, &value);
> + if (ret)
> + return ret;
> + if (!value)
> + return count;
> +
> + scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock) {
> + ret = ayaneo_send_config(aya, AYA3_RESET);
> + if (!ret) {
> + msleep(AYA3_RESET_SETTLE_MS);
> + ret = ayaneo_send_config(aya, 0);
> + }
> + }
> + return ret ? ret : count;
> +}
> +static DEVICE_ATTR_WO(reset);
> +
> +static struct attribute *ayaneo_attrs[] = {
> + &dev_attr_module_left.attr,
> + &dev_attr_module_right.attr,
> + &dev_attr_eject.attr,
> + &dev_attr_reset.attr,
> + NULL
> +};
> +ATTRIBUTE_GROUPS(ayaneo);
> +
> +static int ayaneo_led_set(struct led_classdev *cdev, enum led_brightness value)
> +{
> + struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
> + struct ayaneo *aya = container_of(mc, struct ayaneo, mcled);
> + int ret = 0, i;
> +
> + scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock) {
> + 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 = ayaneo_send_config(aya, 0);
> + if (ret)
> + hid_err(aya->hdev,
> + "failed to update RGB config: %d\n", ret);
> + }
> + 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).
> + */
Prefer removing semicolons; they have a particular smell ;)
> +static int ayaneo_pattern_set(struct led_classdev *cdev,
> + struct led_pattern *pattern, u32 len, int repeat)
> +{
> + struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
> + struct ayaneo *aya = container_of(mc, struct ayaneo, mcled);
> + int ret = 0;
> +
> + if (len != 2 || pattern[0].brightness || !pattern[1].brightness)
> + return -EINVAL;
> +
> + scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock) {
> + aya->pulse = true;
> + ret = ayaneo_send_config(aya, 0);
> + }
> + return ret;
> +}
> +
> +static int ayaneo_pattern_clear(struct led_classdev *cdev)
> +{
> + struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
> + struct ayaneo *aya = container_of(mc, struct ayaneo, mcled);
> + int ret = 0;
> +
> + scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock) {
> + aya->pulse = false;
> + ret = ayaneo_send_config(aya, 0);
> + }
> + return ret;
> +}
> +
> +static int ayaneo_register_led(struct ayaneo *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->color = LED_COLOR_ID_RGB;
> + cdev->brightness = 0;
> + cdev->max_brightness = 255;
> + cdev->brightness_set_blocking = ayaneo_led_set;
> + cdev->pattern_set = ayaneo_pattern_set;
> + cdev->pattern_clear = ayaneo_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 ayaneo_dmi_table[] = {
> + {
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_MATCH(DMI_BOARD_NAME, "AYANEO 3"),
> + },
> + },
> + {}
> +};
> +
> +static int ayaneo_probe(struct hid_device *hdev, const struct hid_device_id *id)
> +{
> + struct ayaneo *aya;
> + int ret;
> +
> + /* The VID/PID is a generic SigmaMicro ID; bind on AYANEO 3 only */
> + if (!dmi_check_system(ayaneo_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_MEDIUM;
> + 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 = ayaneo_check(aya, NULL);
> + if (ret)
> + hid_warn(hdev, "controller did not answer status check: %d\n",
> + ret);
Consider dropping the hid_device ... check block unless it is
necessary. it seems like a premature test that can go wrong and you
touch the device. Particularly, hid_device_io_start is a bit
unconventional.
With this check removed, this driver does not touch the device without
userspace involvement, which is good for userspace implementations
such as mine.
I think these are all the comments I have. I'd suggest waiting a week
before the next revision and up to two weeks for jiri/Benjamin to
reply with some comments as I think I was the only one that reviewed
the previous revision.
Best,
Antheas
> +
> + ret = ayaneo_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 ayaneo_remove(struct hid_device *hdev)
> +{
> + struct ayaneo *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 ayaneo_devices[] = {
> + { HID_USB_DEVICE(0x1c4f, 0x0002) },
> + {}
> +};
> +MODULE_DEVICE_TABLE(hid, ayaneo_devices);
> +
> +static struct hid_driver ayaneo_driver = {
> + .name = "hid-ayaneo",
> + .id_table = ayaneo_devices,
> + .probe = ayaneo_probe,
> + .remove = ayaneo_remove,
> + .raw_event = ayaneo_raw_event,
> + .driver = {
> + .dev_groups = ayaneo_groups,
> + },
> +};
> +module_hid_driver(ayaneo_driver);
> +
> +MODULE_AUTHOR("Matías Martínez <hello@matias.me>");
> +MODULE_DESCRIPTION("AYANEO 3 detachable controller driver");
> +MODULE_LICENSE("GPL");
> --
> 2.54.0 (Apple Git-157)
>
>
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v3] HID: ayaneo: Add AYANEO 3 detachable controller driver
2026-09-17 21:48 ` Antheas Kapenekakis
@ 2026-09-17 22:23 ` Matías Martínez
2026-09-17 22:54 ` Antheas Kapenekakis
2026-09-18 0:42 ` Derek J. Clark
1 sibling, 1 reply; 15+ messages in thread
From: Matías Martínez @ 2026-09-17 22:23 UTC (permalink / raw)
To: Antheas Kapenekakis
Cc: Jiri Kosina, Benjamin Tissoires, linux-input, linux-kernel,
Denis Benato, Dmitry Torokhov, Derek J . Clark,
Ilpo Järvinen, Armin Wolf
On Thu, 17 Sept 2026 at 23:48, Antheas Kapenekakis <lkml@antheas.dev> wrote:
Hi Antheas,
Thanks for going through v3, and for all the help along the way. This
driver exists because of your protocol work in Handheld Daemon and
your reviews of every revision here.
> After you submitted your patch series to the lore, why didn't the
> review take place here and take place downstream?
Sequencing rather than intent. The OGC kernel folks wanted the driver
submitted and tested in their tree first, and their maintainer's
review landed on the already-merged downstream PR after v1 and v2
were on the list. I folded it into v3 rather than ignoring it. Agreed
that from here on review belongs on the list.
> The rby Denis should not be added by you. It should be added by
> denis by replying to the mailing list and then you carry it forward
> on future revisions (for provenance).
Understood, I carried it over from his downstream review round.
Denis, if you are still fine with the driver as of v3, could you
confirm your Reviewed-by here? Otherwise I will drop the tag in v4.
> Consider exploring similar ABIs in e.g., Legion Go S hid and
> mirroring their ABI. Then remove this file. This is not the only
> breathing device. Moreover, delta_t values are ignored and you are
> introducing an ABI for them?
The delta_t point is fair: hw_pattern accepts timing values this
firmware cannot honor, and the sc27xx precedent does use them. For
context, hw_pattern was my reading of your v1 suggestion to find an
accepted ABI for the pulsing mode, and the downstream review
preferred the generic trigger ABI over adding a custom effect
attribute, so the two reviews point in opposite directions here.
hid-lenovo-go-s documents its effect/effect_index attributes in its
own ABI file, so a doc file exists on that path too, though shared
with a second driver it could become a common one. I am happy to
switch to effect/effect_index mirroring hid-lenovo-go-s (values
monocolor and breathe) if that is the preferred direction. Jiri,
Benjamin, a preference either way would settle this.
> I am not sure of the struct naming or whether a struct is needed in
> this case. if you do not use most of the report, consider
> documenting it somewhere else and doing direct accesses to the
> appropriate bytes.
The packed structs came from the downstream review, mirroring
hid-lenovo-go; they replaced a set of byte-offset defines and the
static_asserts pin the wire sizes. I would mildly prefer to keep
them, but I am fine either way if the maintainers have a preference.
> If you set vibration, you need to expose it to userspace. Otherwise
> this driver degrades functionality over userspace implementations.
Agreed. v4 adds an attribute for the vibration level (the firmware
takes three) plus an index listing the options, instead of writing a
hardcoded default.
> Prefer removing semicolons; they have a particular smell ;)
Will reword the comment.
> Consider dropping the hid_device ... check block unless it is
> necessary. it seems like a premature test that can go wrong and you
> touch the device. Particularly, hid_device_io_start is a bit
> unconventional.
Agreed, v4 drops the probe-time status check and with it the
hid_device_io_start call. Every sysfs read already talks to the
device live, so the check adds nothing, and without it the driver
generates no traffic unless userspace asks, which also keeps it inert
next to userspace implementations like yours.
> I'd suggest waiting a week before the next revision and up to two
> weeks for jiri/Benjamin to reply with some comments as I think I
> was the only one that reviewed the previous revision.
Will do. v4 waits for Jiri and Benjamin, and goes out as a fresh
thread.
Thanks again,
Matías
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v3] HID: ayaneo: Add AYANEO 3 detachable controller driver
2026-09-17 22:23 ` Matías Martínez
@ 2026-09-17 22:54 ` Antheas Kapenekakis
0 siblings, 0 replies; 15+ messages in thread
From: Antheas Kapenekakis @ 2026-09-17 22:54 UTC (permalink / raw)
To: Matías Martínez
Cc: Jiri Kosina, Benjamin Tissoires, linux-input, linux-kernel,
Denis Benato, Dmitry Torokhov, Derek J . Clark,
Ilpo Järvinen, Armin Wolf
On Fri, 18 Sept 2026 at 00:23, Matías Martínez <hello@matias.me> wrote:
>
> On Thu, 17 Sept 2026 at 23:48, Antheas Kapenekakis <lkml@antheas.dev> wrote:
>
> Hi Antheas,
>
> Thanks for going through v3, and for all the help along the way. This
> driver exists because of your protocol work in Handheld Daemon and
> your reviews of every revision here.
>
> > After you submitted your patch series to the lore, why didn't the
> > review take place here and take place downstream?
>
> Sequencing rather than intent. The OGC kernel folks wanted the driver
> submitted and tested in their tree first, and their maintainer's
> review landed on the already-merged downstream PR after v1 and v2
> were on the list. I folded it into v3 rather than ignoring it. Agreed
> that from here on review belongs on the list.
>
> > The rby Denis should not be added by you. It should be added by
> > denis by replying to the mailing list and then you carry it forward
> > on future revisions (for provenance).
>
> Understood, I carried it over from his downstream review round.
> Denis, if you are still fine with the driver as of v3, could you
> confirm your Reviewed-by here? Otherwise I will drop the tag in v4.
As I said, none of this is your responsibility.
> > Consider exploring similar ABIs in e.g., Legion Go S hid and
> > mirroring their ABI. Then remove this file. This is not the only
> > breathing device. Moreover, delta_t values are ignored and you are
> > introducing an ABI for them?
>
> The delta_t point is fair: hw_pattern accepts timing values this
> firmware cannot honor, and the sc27xx precedent does use them. For
> context, hw_pattern was my reading of your v1 suggestion to find an
> accepted ABI for the pulsing mode, and the downstream review
> preferred the generic trigger ABI over adding a custom effect
> attribute, so the two reviews point in opposite directions here.
> hid-lenovo-go-s documents its effect/effect_index attributes in its
> own ABI file, so a doc file exists on that path too, though shared
> with a second driver it could become a common one. I am happy to
> switch to effect/effect_index mirroring hid-lenovo-go-s (values
> monocolor and breathe) if that is the preferred direction. Jiri,
> Benjamin, a preference either way would settle this.
If there is an existing ABI, you shouldnt add a new sysfs doc file.
https://github.com/torvalds/linux/blob/b5a051f6b840d48f159166ef073d3021989bfb50/Documentation/ABI/testing/sysfs-driver-hid-lenovo-go#L1-L13
There is an existing class of device in the same space with a new ABI
that does exactly what this device needs. I am really not sure why you
were guided on a LED PWM interface by the author of that ABI.
You need to modify that sysfs file in a separate patch to make it
generic by removing go.
> > I am not sure of the struct naming or whether a struct is needed in
> > this case. if you do not use most of the report, consider
> > documenting it somewhere else and doing direct accesses to the
> > appropriate bytes.
>
> The packed structs came from the downstream review, mirroring
> hid-lenovo-go; they replaced a set of byte-offset defines and the
> static_asserts pin the wire sizes. I would mildly prefer to keep
> them, but I am fine either way if the maintainers have a preference.
I am also fine either way. I'd prefer what is simpler.
> > If you set vibration, you need to expose it to userspace. Otherwise
> > this driver degrades functionality over userspace implementations.
>
> Agreed. v4 adds an attribute for the vibration level (the firmware
> takes three) plus an index listing the options, instead of writing a
> hardcoded default.
>
> > Prefer removing semicolons; they have a particular smell ;)
>
> Will reword the comment.
All occurances.
Thanks,
Antheas
> > Consider dropping the hid_device ... check block unless it is
> > necessary. it seems like a premature test that can go wrong and you
> > touch the device. Particularly, hid_device_io_start is a bit
> > unconventional.
>
> Agreed, v4 drops the probe-time status check and with it the
> hid_device_io_start call. Every sysfs read already talks to the
> device live, so the check adds nothing, and without it the driver
> generates no traffic unless userspace asks, which also keeps it inert
> next to userspace implementations like yours.
>
> > I'd suggest waiting a week before the next revision and up to two
> > weeks for jiri/Benjamin to reply with some comments as I think I
> > was the only one that reviewed the previous revision.
>
> Will do. v4 waits for Jiri and Benjamin, and goes out as a fresh
> thread.
>
> Thanks again,
> Matías
>
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v3] HID: ayaneo: Add AYANEO 3 detachable controller driver
2026-09-17 21:48 ` Antheas Kapenekakis
2026-09-17 22:23 ` Matías Martínez
@ 2026-09-18 0:42 ` Derek J. Clark
2026-09-18 1:38 ` Denis Benato
2026-09-18 7:52 ` Antheas Kapenekakis
1 sibling, 2 replies; 15+ messages in thread
From: Derek J. Clark @ 2026-09-18 0:42 UTC (permalink / raw)
To: Antheas Kapenekakis, Matías Martínez
Cc: Jiri Kosina, Benjamin Tissoires, linux-input, linux-kernel,
Denis Benato, Dmitry Torokhov, Ilpo Järvinen, Armin Wolf
On September 17, 2026 2:48:11 PM PDT, Antheas Kapenekakis <lkml@antheas.dev> wrote:
>On Thu, 17 Sept 2026 at 18:07, Matías Martínez <hello@matias.me> wrote:
>>
>> From: Matías Martínez <hello@matias.me>
>
>Hi Matias,
>
>> 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 v3:
>> - Use a generic ayaneo_ prefix for entry points and driver structure
>> so a future device or protocol revision slots in without churn;
>> wire-protocol constants stay AYA3_* since they are specific to
>> this firmware generation. [Derek J. Clark]
>> - Describe the wire format with packed aya3_config/aya3_resp structs,
>> static_assert their sizes against the report sizes, and name every
>> firmware vibration level in an enum instead of a lone default
>> define. [Derek J. Clark]
>> - Take the command lock with scoped_cond_guard(mutex_intr, ...) at
>> every interruptible lock site so the unlock cannot be dropped in a
>> future edit. [Derek J. Clark]
>> - Mark the LED class device LED_COLOR_ID_RGB so userspace can detect
>> the RGB interface generically. [Derek J. Clark]
>> - Name the firmware timing constants and record where the timings
>> come from and what was validated on hardware.
>>
>> No functional change relative to v2. The rework was prompted by
>> Derek J. Clark's review of the driver in the OpenGamingCollective
>> tree and retested on an AYANEO 3: module identification, RGB solid
>> and breathing via hw_pattern, rejection of malformed hw_pattern
>> writes, and repeated bind/unbind cycles, all with a clean dmesg.
>>
>> One question from that review was whether RGB sysfs writes need
>> debouncing, since Steam emits one write per slider increment during
>> a color drag. They do not: the driver registers only
>> brightness_set_blocking, so the LED core defers stores to its
>> set_brightness_work and coalesces bursts to the latest state.
>> Measured on hardware, a command+ACK round trip averages 5.3 ms
>> (3.8-8 ms over 100 samples) and 1000 back-to-back multi_intensity
>> stores return in 12 ms total, reaching the device as two to three
>> commands.
>
>After you submitted your patch series to the lore, why didn't the
>review take place here and take place downstream? I understand that
>prior to submitting your first kernel patch, it is natural to get some
>informal feedback, but it seems that all of the reviews of this driver
>happened outside the submission and after the submission? The rby
>Denis should not be added by you. It should be added by denis by
>replying to the mailing list and then you carry it forward on future
>revisions (for provenance).
>
>If you are developing a downstream kernel patch for
>OGC/Bazzite/whoever, it is perfectly fine to do downstream work and
>reviews and only submit the driver after it is ready. In fact, it
>would be very preferable for all of us for you to test your driver
>downstream, resolve all feedback and then submit it when it's ready.
>But mixing this is peculiar. This is not a comment on you, you were
>not the one reviewing your patch out of band.
Antheas,
There is no reason to throw shade about the process. This was submitted to OGC unstable prior to v1 where Denis reviewed and tagged it prior to it being submitted. I wasn't Cc'd or informed that it was already on a v2 less than 48h later, so I submitted my feedback directly on the PR a couple days later. Had I been aware I would have obviously submitted feedback here. Without your feedback necessary we've already implemented better control policies to avoid this in the future.
Thanks,
Derek
>> Also suggested in that review, but held out of the patch while the
>> driver's scope is under discussion: a rumble_intensity attribute
>> (the firmware takes three vibration levels), an eject_index
>> attribute, and a notification path from hid-ayaneo to ayaneo-ec so
>> the EC driver could react to ejects. The last one concerns the
>> ayaneo-ec/pdx86 side, hence the added Cc.
>>
>> 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 | 594 ++++++++++++++++++
>> 6 files changed, 668 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
>
>Consider exploring similar ABIs in e.g., Legion Go S hid and mirroring
>their ABI. Then remove this file. This is not the only breathing
>device. Moreover, delta_t values are ignored and you are introducing
>an ABI for them? Prefer a mode setting that can be standard/breathing
>mirroring a different driver.
>
>> +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 8b14f290c..3290d9957 100644
>> --- a/MAINTAINERS
>> +++ b/MAINTAINERS
>> @@ -4508,6 +4508,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 0e3a0ccd6..319eda887 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 79384d905..2f74f2867 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..3a7af5918
>> --- /dev/null
>> +++ b/drivers/hid/hid-ayaneo.c
>> @@ -0,0 +1,594 @@
>> +// 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/build_bug.h>
>> +#include <linux/cleanup.h>
>> +#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
>> +
>> +/*
>> + * Empirical timings, inherited from the Handheld Daemon
>> + * implementation of this protocol and validated on hardware: the
>> + * device answers well within 300ms or not at all, needs about half
>> + * a second to settle after a reset before it accepts a new
>> + * configuration, and completes an eject handshake within a few
>> + * seconds (polled below at a rate that keeps the sysfs write
>> + * responsive).
>> + */
>> +#define AYA3_CMD_TIMEOUT_MS 300
>> +#define AYA3_CMD_ATTEMPTS 3
>> +#define AYA3_RESET_SETTLE_MS 500
>> +#define AYA3_EJECT_POLL_MS 400
>> +#define AYA3_EJECT_POLLS 20
>> +
>> +/* 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
>> +
>> +/* 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
>> +
>> +/* Config command vibration levels, stored in the high nibble */
>> +enum aya3_vibration {
>> + AYA3_VIBRATION_LOW = 0x1,
>> + AYA3_VIBRATION_MEDIUM = 0x2,
>> + AYA3_VIBRATION_HIGH = 0x3,
>> + AYA3_VIBRATION_OFF = 0x4,
>> +};
>> +
>> +struct aya3_rgb {
>> + u8 mode;
>> + u8 r;
>> + u8 g;
>> + u8 b;
>> +} __packed;
>> +
>> +/*
>> + * The 65-byte config command. The checksum is the little-endian sum of
>> + * bytes 7..64; unk* fields are sent as zero.
>> + */
>> +struct aya3_config {
>> + u8 report_id;
>> + __le16 csum;
>> + u8 cmd;
>> + u8 subcmd;
>> + u8 unk5[3];
>> + struct aya3_rgb right;
>> + struct aya3_rgb left;
>> + u8 unk16[4];
>> + u8 eject;
>> + u8 unk21;
>> + u8 sensitivity[2];
>> + u8 vibration;
>> + u8 unk25[7];
>> + u8 magic;
>> + u8 unk33[32];
>
>I am not sure of the struct naming or whether a struct is needed in
>this case. if you do not use most of the report, consider documenting
>it somewhere else and doing direct accesses to the appropriate bytes.
>
>> +} __packed;
>> +static_assert(sizeof(struct aya3_config) == AYA3_REPORT_SIZE);
>> +
>> +/* Replies echo the subcommand they answer at byte 3 */
>> +struct aya3_resp {
>> + u8 unk0[3];
>> + u8 subcmd;
>> + u8 unk4[15];
>> + u8 eject_status;
>> + u8 unk20[12];
>> + u8 module_left;
>> + u8 module_right;
>> + u8 unk34[30];
>> +} __packed;
>> +static_assert(sizeof(struct aya3_resp) == AYA3_RESP_SIZE);
>> +
>> +struct ayaneo {
>> + 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;
>> + struct aya3_resp resp;
>> + 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 ayaneo_send(struct ayaneo *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;
>> +}
>> +
>> +/**
>> + * ayaneo_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 reply, or NULL to discard it
>> + *
>> + * The device echoes the subcommand byte of the command it is answering,
>> + * which ayaneo_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 ayaneo_cmd(struct ayaneo *aya, struct aya3_resp *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 = ayaneo_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, sizeof(*resp));
>> + return 0;
>> + }
>> + }
>> + WRITE_ONCE(aya->resp_pending, false);
>> + return -ETIMEDOUT;
>> +}
>> +
>> +static void ayaneo_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 ayaneo_check(struct ayaneo *aya, struct aya3_resp *resp)
>> +{
>> + memset(aya->xfer, 0, AYA3_REPORT_SIZE);
>> + aya->xfer[4] = AYA3_SUBCMD_CHECK;
>> + return ayaneo_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 ayaneo_send_config(struct ayaneo *aya, u8 eject)
>> +{
>> + static const struct aya3_config template = {
>> + .cmd = AYA3_CMD_CONFIG,
>> + .subcmd = AYA3_SUBCMD_CONFIG,
>> + .magic = 0x01,
>> + };
>> + struct aya3_config *cfg = (struct aya3_config *)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;
>> +
>> + *cfg = template;
>> + cfg->right.mode = mode;
>> + cfg->right.r = aya->rgb[0];
>> + cfg->right.g = aya->rgb[1];
>> + cfg->right.b = aya->rgb[2];
>> + cfg->left = cfg->right;
>> + cfg->eject = eject;
>> + cfg->vibration = aya->vibration << 4;
>
>If you set vibration, you need to expose it to userspace. Otherwise
>this driver degrades functionality over userspace implementations.
>
>> + ayaneo_checksum(aya->xfer);
>> +
>> + return ayaneo_cmd(aya, NULL);
>> +}
>> +
>> +static int ayaneo_raw_event(struct hid_device *hdev, struct hid_report *report,
>> + u8 *data, int size)
>> +{
>> + struct ayaneo *aya = hid_get_drvdata(hdev);
>> + const struct aya3_resp *resp = (const struct aya3_resp *)data;
>> +
>> + 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 (resp->subcmd != aya->resp_expect)
>> + return 0;
>> +
>> + memcpy(&aya->resp, data, sizeof(aya->resp));
>> + WRITE_ONCE(aya->resp_pending, false);
>> + complete(&aya->resp_done);
>> + return 0;
>> +}
>> +
>> +static ssize_t ayaneo_module_show(struct device *dev, char *buf, bool right)
>> +{
>> + struct ayaneo *aya = dev_get_drvdata(dev);
>> + struct aya3_resp resp;
>> + int ret = 0;
>> +
>> + scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock)
>> + ret = ayaneo_check(aya, &resp);
>> + if (ret)
>> + return ret;
>> +
>> + return sysfs_emit(buf, "0x%02x\n",
>> + right ? resp.module_right : resp.module_left);
>> +}
>> +
>> +static ssize_t module_left_show(struct device *dev,
>> + struct device_attribute *attr, char *buf)
>> +{
>> + return ayaneo_module_show(dev, buf, false);
>> +}
>> +static DEVICE_ATTR_RO(module_left);
>> +
>> +static ssize_t module_right_show(struct device *dev,
>> + struct device_attribute *attr, char *buf)
>> +{
>> + return ayaneo_module_show(dev, buf, true);
>> +}
>> +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 ayaneo *aya = dev_get_drvdata(dev);
>> + struct aya3_resp resp;
>> + u8 eject;
>> + int ret = 0, 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;
>> +
>> + scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock) {
>> + ret = ayaneo_send_config(aya, eject);
>> + if (ret)
>> + break;
>> +
>> + /*
>> + * 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 < AYA3_EJECT_POLLS; i++) {
>> + msleep(AYA3_EJECT_POLL_MS);
>> + err = ayaneo_check(aya, &resp);
>> + if (err == -ETIMEDOUT)
>> + continue; /* busy mid-eject, keep polling */
>> + if (err) {
>> + ret = err;
>> + break;
>> + }
>> + if (!(resp.eject_status & ~AYA3_EJECT_DONE_MASK)) {
>> + ret = 0;
>> + break;
>> + }
>> + }
>> + }
>> + 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 ayaneo *aya = dev_get_drvdata(dev);
>> + bool value;
>> + int ret;
>> +
>> + ret = kstrtobool(buf, &value);
>> + if (ret)
>> + return ret;
>> + if (!value)
>> + return count;
>> +
>> + scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock) {
>> + ret = ayaneo_send_config(aya, AYA3_RESET);
>> + if (!ret) {
>> + msleep(AYA3_RESET_SETTLE_MS);
>> + ret = ayaneo_send_config(aya, 0);
>> + }
>> + }
>> + return ret ? ret : count;
>> +}
>> +static DEVICE_ATTR_WO(reset);
>> +
>> +static struct attribute *ayaneo_attrs[] = {
>> + &dev_attr_module_left.attr,
>> + &dev_attr_module_right.attr,
>> + &dev_attr_eject.attr,
>> + &dev_attr_reset.attr,
>> + NULL
>> +};
>> +ATTRIBUTE_GROUPS(ayaneo);
>> +
>> +static int ayaneo_led_set(struct led_classdev *cdev, enum led_brightness value)
>> +{
>> + struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
>> + struct ayaneo *aya = container_of(mc, struct ayaneo, mcled);
>> + int ret = 0, i;
>> +
>> + scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock) {
>> + 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 = ayaneo_send_config(aya, 0);
>> + if (ret)
>> + hid_err(aya->hdev,
>> + "failed to update RGB config: %d\n", ret);
>> + }
>> + 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).
>> + */
>
>Prefer removing semicolons; they have a particular smell ;)
>
>> +static int ayaneo_pattern_set(struct led_classdev *cdev,
>> + struct led_pattern *pattern, u32 len, int repeat)
>> +{
>> + struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
>> + struct ayaneo *aya = container_of(mc, struct ayaneo, mcled);
>> + int ret = 0;
>> +
>> + if (len != 2 || pattern[0].brightness || !pattern[1].brightness)
>> + return -EINVAL;
>> +
>> + scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock) {
>> + aya->pulse = true;
>> + ret = ayaneo_send_config(aya, 0);
>> + }
>> + return ret;
>> +}
>> +
>> +static int ayaneo_pattern_clear(struct led_classdev *cdev)
>> +{
>> + struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
>> + struct ayaneo *aya = container_of(mc, struct ayaneo, mcled);
>> + int ret = 0;
>> +
>> + scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock) {
>> + aya->pulse = false;
>> + ret = ayaneo_send_config(aya, 0);
>> + }
>> + return ret;
>> +}
>> +
>> +static int ayaneo_register_led(struct ayaneo *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->color = LED_COLOR_ID_RGB;
>> + cdev->brightness = 0;
>> + cdev->max_brightness = 255;
>> + cdev->brightness_set_blocking = ayaneo_led_set;
>> + cdev->pattern_set = ayaneo_pattern_set;
>> + cdev->pattern_clear = ayaneo_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 ayaneo_dmi_table[] = {
>> + {
>> + .matches = {
>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
>> + DMI_MATCH(DMI_BOARD_NAME, "AYANEO 3"),
>> + },
>> + },
>> + {}
>> +};
>> +
>> +static int ayaneo_probe(struct hid_device *hdev, const struct hid_device_id *id)
>> +{
>> + struct ayaneo *aya;
>> + int ret;
>> +
>> + /* The VID/PID is a generic SigmaMicro ID; bind on AYANEO 3 only */
>> + if (!dmi_check_system(ayaneo_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_MEDIUM;
>> + 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 = ayaneo_check(aya, NULL);
>> + if (ret)
>> + hid_warn(hdev, "controller did not answer status check: %d\n",
>> + ret);
>
>Consider dropping the hid_device ... check block unless it is
>necessary. it seems like a premature test that can go wrong and you
>touch the device. Particularly, hid_device_io_start is a bit
>unconventional.
>
>With this check removed, this driver does not touch the device without
>userspace involvement, which is good for userspace implementations
>such as mine.
>
>I think these are all the comments I have. I'd suggest waiting a week
>before the next revision and up to two weeks for jiri/Benjamin to
>reply with some comments as I think I was the only one that reviewed
>the previous revision.
>
>Best,
>Antheas
>
>> +
>> + ret = ayaneo_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 ayaneo_remove(struct hid_device *hdev)
>> +{
>> + struct ayaneo *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 ayaneo_devices[] = {
>> + { HID_USB_DEVICE(0x1c4f, 0x0002) },
>> + {}
>> +};
>> +MODULE_DEVICE_TABLE(hid, ayaneo_devices);
>> +
>> +static struct hid_driver ayaneo_driver = {
>> + .name = "hid-ayaneo",
>> + .id_table = ayaneo_devices,
>> + .probe = ayaneo_probe,
>> + .remove = ayaneo_remove,
>> + .raw_event = ayaneo_raw_event,
>> + .driver = {
>> + .dev_groups = ayaneo_groups,
>> + },
>> +};
>> +module_hid_driver(ayaneo_driver);
>> +
>> +MODULE_AUTHOR("Matías Martínez <hello@matias.me>");
>> +MODULE_DESCRIPTION("AYANEO 3 detachable controller driver");
>> +MODULE_LICENSE("GPL");
>> --
>> 2.54.0 (Apple Git-157)
>>
>>
>
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v3] HID: ayaneo: Add AYANEO 3 detachable controller driver
2026-09-18 0:42 ` Derek J. Clark
@ 2026-09-18 1:38 ` Denis Benato
2026-09-18 7:52 ` Antheas Kapenekakis
1 sibling, 0 replies; 15+ messages in thread
From: Denis Benato @ 2026-09-18 1:38 UTC (permalink / raw)
To: Derek J. Clark, Antheas Kapenekakis, Matías Martínez
Cc: Jiri Kosina, Benjamin Tissoires, linux-input, linux-kernel,
Dmitry Torokhov, Ilpo Järvinen, Armin Wolf
On 9/18/26 02:42, Derek J. Clark wrote:
> On September 17, 2026 2:48:11 PM PDT, Antheas Kapenekakis <lkml@antheas.dev> wrote:
>> On Thu, 17 Sept 2026 at 18:07, Matías Martínez <hello@matias.me> wrote:
>>> From: Matías Martínez <hello@matias.me>
>> Hi Matias,
>>
>>> 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 v3:
>>> - Use a generic ayaneo_ prefix for entry points and driver structure
>>> so a future device or protocol revision slots in without churn;
>>> wire-protocol constants stay AYA3_* since they are specific to
>>> this firmware generation. [Derek J. Clark]
>>> - Describe the wire format with packed aya3_config/aya3_resp structs,
>>> static_assert their sizes against the report sizes, and name every
>>> firmware vibration level in an enum instead of a lone default
>>> define. [Derek J. Clark]
>>> - Take the command lock with scoped_cond_guard(mutex_intr, ...) at
>>> every interruptible lock site so the unlock cannot be dropped in a
>>> future edit. [Derek J. Clark]
>>> - Mark the LED class device LED_COLOR_ID_RGB so userspace can detect
>>> the RGB interface generically. [Derek J. Clark]
>>> - Name the firmware timing constants and record where the timings
>>> come from and what was validated on hardware.
>>>
>>> No functional change relative to v2. The rework was prompted by
>>> Derek J. Clark's review of the driver in the OpenGamingCollective
>>> tree and retested on an AYANEO 3: module identification, RGB solid
>>> and breathing via hw_pattern, rejection of malformed hw_pattern
>>> writes, and repeated bind/unbind cycles, all with a clean dmesg.
>>>
>>> One question from that review was whether RGB sysfs writes need
>>> debouncing, since Steam emits one write per slider increment during
>>> a color drag. They do not: the driver registers only
>>> brightness_set_blocking, so the LED core defers stores to its
>>> set_brightness_work and coalesces bursts to the latest state.
>>> Measured on hardware, a command+ACK round trip averages 5.3 ms
>>> (3.8-8 ms over 100 samples) and 1000 back-to-back multi_intensity
>>> stores return in 12 ms total, reaching the device as two to three
>>> commands.
>> After you submitted your patch series to the lore, why didn't the
>> review take place here and take place downstream? I understand that
>> prior to submitting your first kernel patch, it is natural to get some
>> informal feedback, but it seems that all of the reviews of this driver
>> happened outside the submission and after the submission? The rby
>> Denis should not be added by you. It should be added by denis by
>> replying to the mailing list and then you carry it forward on future
>> revisions (for provenance).
Hello Antheas,
thanks for the interest but don't worry: he added my rvb because
I asked him to do so.
The driver was developed and tested against the at-the-time version
of linux-next, as all new drivers should be as stated in the linux-next
landing page.
Best regards,
Denis
>> If you are developing a downstream kernel patch for
>> OGC/Bazzite/whoever, it is perfectly fine to do downstream work and
>> reviews and only submit the driver after it is ready. In fact, it
>> would be very preferable for all of us for you to test your driver
>> downstream, resolve all feedback and then submit it when it's ready.
>> But mixing this is peculiar. This is not a comment on you, you were
>> not the one reviewing your patch out of band.
> Antheas,
>
> There is no reason to throw shade about the process. This was submitted to OGC unstable prior to v1 where Denis reviewed and tagged it prior to it being submitted. I wasn't Cc'd or informed that it was already on a v2 less than 48h later, so I submitted my feedback directly on the PR a couple days later. Had I been aware I would have obviously submitted feedback here. Without your feedback necessary we've already implemented better control policies to avoid this in the future.
>
> Thanks,
> Derek
>
>>> Also suggested in that review, but held out of the patch while the
>>> driver's scope is under discussion: a rumble_intensity attribute
>>> (the firmware takes three vibration levels), an eject_index
>>> attribute, and a notification path from hid-ayaneo to ayaneo-ec so
>>> the EC driver could react to ejects. The last one concerns the
>>> ayaneo-ec/pdx86 side, hence the added Cc.
>>>
>>> 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 | 594 ++++++++++++++++++
>>> 6 files changed, 668 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
>> Consider exploring similar ABIs in e.g., Legion Go S hid and mirroring
>> their ABI. Then remove this file. This is not the only breathing
>> device. Moreover, delta_t values are ignored and you are introducing
>> an ABI for them? Prefer a mode setting that can be standard/breathing
>> mirroring a different driver.
>>
>>> +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 8b14f290c..3290d9957 100644
>>> --- a/MAINTAINERS
>>> +++ b/MAINTAINERS
>>> @@ -4508,6 +4508,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 0e3a0ccd6..319eda887 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 79384d905..2f74f2867 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..3a7af5918
>>> --- /dev/null
>>> +++ b/drivers/hid/hid-ayaneo.c
>>> @@ -0,0 +1,594 @@
>>> +// 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/build_bug.h>
>>> +#include <linux/cleanup.h>
>>> +#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
>>> +
>>> +/*
>>> + * Empirical timings, inherited from the Handheld Daemon
>>> + * implementation of this protocol and validated on hardware: the
>>> + * device answers well within 300ms or not at all, needs about half
>>> + * a second to settle after a reset before it accepts a new
>>> + * configuration, and completes an eject handshake within a few
>>> + * seconds (polled below at a rate that keeps the sysfs write
>>> + * responsive).
>>> + */
>>> +#define AYA3_CMD_TIMEOUT_MS 300
>>> +#define AYA3_CMD_ATTEMPTS 3
>>> +#define AYA3_RESET_SETTLE_MS 500
>>> +#define AYA3_EJECT_POLL_MS 400
>>> +#define AYA3_EJECT_POLLS 20
>>> +
>>> +/* 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
>>> +
>>> +/* 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
>>> +
>>> +/* Config command vibration levels, stored in the high nibble */
>>> +enum aya3_vibration {
>>> + AYA3_VIBRATION_LOW = 0x1,
>>> + AYA3_VIBRATION_MEDIUM = 0x2,
>>> + AYA3_VIBRATION_HIGH = 0x3,
>>> + AYA3_VIBRATION_OFF = 0x4,
>>> +};
>>> +
>>> +struct aya3_rgb {
>>> + u8 mode;
>>> + u8 r;
>>> + u8 g;
>>> + u8 b;
>>> +} __packed;
>>> +
>>> +/*
>>> + * The 65-byte config command. The checksum is the little-endian sum of
>>> + * bytes 7..64; unk* fields are sent as zero.
>>> + */
>>> +struct aya3_config {
>>> + u8 report_id;
>>> + __le16 csum;
>>> + u8 cmd;
>>> + u8 subcmd;
>>> + u8 unk5[3];
>>> + struct aya3_rgb right;
>>> + struct aya3_rgb left;
>>> + u8 unk16[4];
>>> + u8 eject;
>>> + u8 unk21;
>>> + u8 sensitivity[2];
>>> + u8 vibration;
>>> + u8 unk25[7];
>>> + u8 magic;
>>> + u8 unk33[32];
>> I am not sure of the struct naming or whether a struct is needed in
>> this case. if you do not use most of the report, consider documenting
>> it somewhere else and doing direct accesses to the appropriate bytes.
>>
>>> +} __packed;
>>> +static_assert(sizeof(struct aya3_config) == AYA3_REPORT_SIZE);
>>> +
>>> +/* Replies echo the subcommand they answer at byte 3 */
>>> +struct aya3_resp {
>>> + u8 unk0[3];
>>> + u8 subcmd;
>>> + u8 unk4[15];
>>> + u8 eject_status;
>>> + u8 unk20[12];
>>> + u8 module_left;
>>> + u8 module_right;
>>> + u8 unk34[30];
>>> +} __packed;
>>> +static_assert(sizeof(struct aya3_resp) == AYA3_RESP_SIZE);
>>> +
>>> +struct ayaneo {
>>> + 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;
>>> + struct aya3_resp resp;
>>> + 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 ayaneo_send(struct ayaneo *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;
>>> +}
>>> +
>>> +/**
>>> + * ayaneo_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 reply, or NULL to discard it
>>> + *
>>> + * The device echoes the subcommand byte of the command it is answering,
>>> + * which ayaneo_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 ayaneo_cmd(struct ayaneo *aya, struct aya3_resp *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 = ayaneo_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, sizeof(*resp));
>>> + return 0;
>>> + }
>>> + }
>>> + WRITE_ONCE(aya->resp_pending, false);
>>> + return -ETIMEDOUT;
>>> +}
>>> +
>>> +static void ayaneo_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 ayaneo_check(struct ayaneo *aya, struct aya3_resp *resp)
>>> +{
>>> + memset(aya->xfer, 0, AYA3_REPORT_SIZE);
>>> + aya->xfer[4] = AYA3_SUBCMD_CHECK;
>>> + return ayaneo_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 ayaneo_send_config(struct ayaneo *aya, u8 eject)
>>> +{
>>> + static const struct aya3_config template = {
>>> + .cmd = AYA3_CMD_CONFIG,
>>> + .subcmd = AYA3_SUBCMD_CONFIG,
>>> + .magic = 0x01,
>>> + };
>>> + struct aya3_config *cfg = (struct aya3_config *)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;
>>> +
>>> + *cfg = template;
>>> + cfg->right.mode = mode;
>>> + cfg->right.r = aya->rgb[0];
>>> + cfg->right.g = aya->rgb[1];
>>> + cfg->right.b = aya->rgb[2];
>>> + cfg->left = cfg->right;
>>> + cfg->eject = eject;
>>> + cfg->vibration = aya->vibration << 4;
>> If you set vibration, you need to expose it to userspace. Otherwise
>> this driver degrades functionality over userspace implementations.
>>
>>> + ayaneo_checksum(aya->xfer);
>>> +
>>> + return ayaneo_cmd(aya, NULL);
>>> +}
>>> +
>>> +static int ayaneo_raw_event(struct hid_device *hdev, struct hid_report *report,
>>> + u8 *data, int size)
>>> +{
>>> + struct ayaneo *aya = hid_get_drvdata(hdev);
>>> + const struct aya3_resp *resp = (const struct aya3_resp *)data;
>>> +
>>> + 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 (resp->subcmd != aya->resp_expect)
>>> + return 0;
>>> +
>>> + memcpy(&aya->resp, data, sizeof(aya->resp));
>>> + WRITE_ONCE(aya->resp_pending, false);
>>> + complete(&aya->resp_done);
>>> + return 0;
>>> +}
>>> +
>>> +static ssize_t ayaneo_module_show(struct device *dev, char *buf, bool right)
>>> +{
>>> + struct ayaneo *aya = dev_get_drvdata(dev);
>>> + struct aya3_resp resp;
>>> + int ret = 0;
>>> +
>>> + scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock)
>>> + ret = ayaneo_check(aya, &resp);
>>> + if (ret)
>>> + return ret;
>>> +
>>> + return sysfs_emit(buf, "0x%02x\n",
>>> + right ? resp.module_right : resp.module_left);
>>> +}
>>> +
>>> +static ssize_t module_left_show(struct device *dev,
>>> + struct device_attribute *attr, char *buf)
>>> +{
>>> + return ayaneo_module_show(dev, buf, false);
>>> +}
>>> +static DEVICE_ATTR_RO(module_left);
>>> +
>>> +static ssize_t module_right_show(struct device *dev,
>>> + struct device_attribute *attr, char *buf)
>>> +{
>>> + return ayaneo_module_show(dev, buf, true);
>>> +}
>>> +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 ayaneo *aya = dev_get_drvdata(dev);
>>> + struct aya3_resp resp;
>>> + u8 eject;
>>> + int ret = 0, 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;
>>> +
>>> + scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock) {
>>> + ret = ayaneo_send_config(aya, eject);
>>> + if (ret)
>>> + break;
>>> +
>>> + /*
>>> + * 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 < AYA3_EJECT_POLLS; i++) {
>>> + msleep(AYA3_EJECT_POLL_MS);
>>> + err = ayaneo_check(aya, &resp);
>>> + if (err == -ETIMEDOUT)
>>> + continue; /* busy mid-eject, keep polling */
>>> + if (err) {
>>> + ret = err;
>>> + break;
>>> + }
>>> + if (!(resp.eject_status & ~AYA3_EJECT_DONE_MASK)) {
>>> + ret = 0;
>>> + break;
>>> + }
>>> + }
>>> + }
>>> + 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 ayaneo *aya = dev_get_drvdata(dev);
>>> + bool value;
>>> + int ret;
>>> +
>>> + ret = kstrtobool(buf, &value);
>>> + if (ret)
>>> + return ret;
>>> + if (!value)
>>> + return count;
>>> +
>>> + scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock) {
>>> + ret = ayaneo_send_config(aya, AYA3_RESET);
>>> + if (!ret) {
>>> + msleep(AYA3_RESET_SETTLE_MS);
>>> + ret = ayaneo_send_config(aya, 0);
>>> + }
>>> + }
>>> + return ret ? ret : count;
>>> +}
>>> +static DEVICE_ATTR_WO(reset);
>>> +
>>> +static struct attribute *ayaneo_attrs[] = {
>>> + &dev_attr_module_left.attr,
>>> + &dev_attr_module_right.attr,
>>> + &dev_attr_eject.attr,
>>> + &dev_attr_reset.attr,
>>> + NULL
>>> +};
>>> +ATTRIBUTE_GROUPS(ayaneo);
>>> +
>>> +static int ayaneo_led_set(struct led_classdev *cdev, enum led_brightness value)
>>> +{
>>> + struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
>>> + struct ayaneo *aya = container_of(mc, struct ayaneo, mcled);
>>> + int ret = 0, i;
>>> +
>>> + scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock) {
>>> + 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 = ayaneo_send_config(aya, 0);
>>> + if (ret)
>>> + hid_err(aya->hdev,
>>> + "failed to update RGB config: %d\n", ret);
>>> + }
>>> + 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).
>>> + */
>> Prefer removing semicolons; they have a particular smell ;)
>>
>>> +static int ayaneo_pattern_set(struct led_classdev *cdev,
>>> + struct led_pattern *pattern, u32 len, int repeat)
>>> +{
>>> + struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
>>> + struct ayaneo *aya = container_of(mc, struct ayaneo, mcled);
>>> + int ret = 0;
>>> +
>>> + if (len != 2 || pattern[0].brightness || !pattern[1].brightness)
>>> + return -EINVAL;
>>> +
>>> + scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock) {
>>> + aya->pulse = true;
>>> + ret = ayaneo_send_config(aya, 0);
>>> + }
>>> + return ret;
>>> +}
>>> +
>>> +static int ayaneo_pattern_clear(struct led_classdev *cdev)
>>> +{
>>> + struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
>>> + struct ayaneo *aya = container_of(mc, struct ayaneo, mcled);
>>> + int ret = 0;
>>> +
>>> + scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock) {
>>> + aya->pulse = false;
>>> + ret = ayaneo_send_config(aya, 0);
>>> + }
>>> + return ret;
>>> +}
>>> +
>>> +static int ayaneo_register_led(struct ayaneo *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->color = LED_COLOR_ID_RGB;
>>> + cdev->brightness = 0;
>>> + cdev->max_brightness = 255;
>>> + cdev->brightness_set_blocking = ayaneo_led_set;
>>> + cdev->pattern_set = ayaneo_pattern_set;
>>> + cdev->pattern_clear = ayaneo_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 ayaneo_dmi_table[] = {
>>> + {
>>> + .matches = {
>>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
>>> + DMI_MATCH(DMI_BOARD_NAME, "AYANEO 3"),
>>> + },
>>> + },
>>> + {}
>>> +};
>>> +
>>> +static int ayaneo_probe(struct hid_device *hdev, const struct hid_device_id *id)
>>> +{
>>> + struct ayaneo *aya;
>>> + int ret;
>>> +
>>> + /* The VID/PID is a generic SigmaMicro ID; bind on AYANEO 3 only */
>>> + if (!dmi_check_system(ayaneo_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_MEDIUM;
>>> + 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 = ayaneo_check(aya, NULL);
>>> + if (ret)
>>> + hid_warn(hdev, "controller did not answer status check: %d\n",
>>> + ret);
>> Consider dropping the hid_device ... check block unless it is
>> necessary. it seems like a premature test that can go wrong and you
>> touch the device. Particularly, hid_device_io_start is a bit
>> unconventional.
>>
>> With this check removed, this driver does not touch the device without
>> userspace involvement, which is good for userspace implementations
>> such as mine.
>>
>> I think these are all the comments I have. I'd suggest waiting a week
>> before the next revision and up to two weeks for jiri/Benjamin to
>> reply with some comments as I think I was the only one that reviewed
>> the previous revision.
>>
>> Best,
>> Antheas
>>
>>> +
>>> + ret = ayaneo_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 ayaneo_remove(struct hid_device *hdev)
>>> +{
>>> + struct ayaneo *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 ayaneo_devices[] = {
>>> + { HID_USB_DEVICE(0x1c4f, 0x0002) },
>>> + {}
>>> +};
>>> +MODULE_DEVICE_TABLE(hid, ayaneo_devices);
>>> +
>>> +static struct hid_driver ayaneo_driver = {
>>> + .name = "hid-ayaneo",
>>> + .id_table = ayaneo_devices,
>>> + .probe = ayaneo_probe,
>>> + .remove = ayaneo_remove,
>>> + .raw_event = ayaneo_raw_event,
>>> + .driver = {
>>> + .dev_groups = ayaneo_groups,
>>> + },
>>> +};
>>> +module_hid_driver(ayaneo_driver);
>>> +
>>> +MODULE_AUTHOR("Matías Martínez <hello@matias.me>");
>>> +MODULE_DESCRIPTION("AYANEO 3 detachable controller driver");
>>> +MODULE_LICENSE("GPL");
>>> --
>>> 2.54.0 (Apple Git-157)
>>>
>>>
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v3] HID: ayaneo: Add AYANEO 3 detachable controller driver
2026-09-18 0:42 ` Derek J. Clark
2026-09-18 1:38 ` Denis Benato
@ 2026-09-18 7:52 ` Antheas Kapenekakis
2026-09-18 13:44 ` Derek J. Clark
1 sibling, 1 reply; 15+ messages in thread
From: Antheas Kapenekakis @ 2026-09-18 7:52 UTC (permalink / raw)
To: Derek J. Clark
Cc: Matías Martínez, Jiri Kosina, Benjamin Tissoires,
linux-input, linux-kernel, Denis Benato, Dmitry Torokhov,
Ilpo Järvinen, Armin Wolf
On Fri, 18 Sept 2026 at 02:42, Derek J. Clark <derekjohn.clark@gmail.com> wrote:
>
> On September 17, 2026 2:48:11 PM PDT, Antheas Kapenekakis <lkml@antheas.dev> wrote:
> >On Thu, 17 Sept 2026 at 18:07, Matías Martínez <hello@matias.me> wrote:
> >>
> >> From: Matías Martínez <hello@matias.me>
> >
> >Hi Matias,
> >
> >> 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 v3:
> >> - Use a generic ayaneo_ prefix for entry points and driver structure
> >> so a future device or protocol revision slots in without churn;
> >> wire-protocol constants stay AYA3_* since they are specific to
> >> this firmware generation. [Derek J. Clark]
> >> - Describe the wire format with packed aya3_config/aya3_resp structs,
> >> static_assert their sizes against the report sizes, and name every
> >> firmware vibration level in an enum instead of a lone default
> >> define. [Derek J. Clark]
> >> - Take the command lock with scoped_cond_guard(mutex_intr, ...) at
> >> every interruptible lock site so the unlock cannot be dropped in a
> >> future edit. [Derek J. Clark]
> >> - Mark the LED class device LED_COLOR_ID_RGB so userspace can detect
> >> the RGB interface generically. [Derek J. Clark]
> >> - Name the firmware timing constants and record where the timings
> >> come from and what was validated on hardware.
> >>
> >> No functional change relative to v2. The rework was prompted by
> >> Derek J. Clark's review of the driver in the OpenGamingCollective
> >> tree and retested on an AYANEO 3: module identification, RGB solid
> >> and breathing via hw_pattern, rejection of malformed hw_pattern
> >> writes, and repeated bind/unbind cycles, all with a clean dmesg.
> >>
> >> One question from that review was whether RGB sysfs writes need
> >> debouncing, since Steam emits one write per slider increment during
> >> a color drag. They do not: the driver registers only
> >> brightness_set_blocking, so the LED core defers stores to its
> >> set_brightness_work and coalesces bursts to the latest state.
> >> Measured on hardware, a command+ACK round trip averages 5.3 ms
> >> (3.8-8 ms over 100 samples) and 1000 back-to-back multi_intensity
> >> stores return in 12 ms total, reaching the device as two to three
> >> commands.
> >
> >After you submitted your patch series to the lore, why didn't the
> >review take place here and take place downstream? I understand that
> >prior to submitting your first kernel patch, it is natural to get some
> >informal feedback, but it seems that all of the reviews of this driver
> >happened outside the submission and after the submission? The rby
> >Denis should not be added by you. It should be added by denis by
> >replying to the mailing list and then you carry it forward on future
> >revisions (for provenance).
> >
> >If you are developing a downstream kernel patch for
> >OGC/Bazzite/whoever, it is perfectly fine to do downstream work and
> >reviews and only submit the driver after it is ready. In fact, it
> >would be very preferable for all of us for you to test your driver
> >downstream, resolve all feedback and then submit it when it's ready.
> >But mixing this is peculiar. This is not a comment on you, you were
> >not the one reviewing your patch out of band.
>
> Antheas,
>
> There is no reason to throw shade about the process. This was submitted to OGC unstable prior to v1 where Denis reviewed and tagged it prior to it being submitted. I wasn't Cc'd or informed that it was already on a v2 less than 48h later, so I submitted my feedback directly on the PR a couple days later. Had I been aware I would have obviously submitted feedback here.
If Denis reviewed it prior to V1 this is ok. It still does not explain
why you reviewed the PR after V1. I checked. Both V1 and V2 were
posted on Aug 24. Your feedback was on the 28th
What you did is a disservice to Matias and to us. Matias wants to see
his device work under Bazzite. You set the arbitrary requirement of
kernel drivers being needed for hid writes and that those drivers
_need_ to be on the mailing list before you merge them. So, his device
broke in a recent update. Then, Matias spends his personal time to
develop such a kernel driver and posts it to the mailing list.
Instead of getting thorough reviews here and support to show this
driver has traction, you conduct your review on a misc repo after
getting your arbitrary ML requirement met. This is a disservice to
Matias. And we see a driver without any traction and conduct
duplicative reviews. This is a disservice to us.
> Without your feedback necessary we've already implemented better control policies to avoid this in the future.
Explain how you will avoid this situation in the future
Antheas
> Thanks,
> Derek
>
> >> Also suggested in that review, but held out of the patch while the
> >> driver's scope is under discussion: a rumble_intensity attribute
> >> (the firmware takes three vibration levels), an eject_index
> >> attribute, and a notification path from hid-ayaneo to ayaneo-ec so
> >> the EC driver could react to ejects. The last one concerns the
> >> ayaneo-ec/pdx86 side, hence the added Cc.
> >>
> >> 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 | 594 ++++++++++++++++++
> >> 6 files changed, 668 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
> >
> >Consider exploring similar ABIs in e.g., Legion Go S hid and mirroring
> >their ABI. Then remove this file. This is not the only breathing
> >device. Moreover, delta_t values are ignored and you are introducing
> >an ABI for them? Prefer a mode setting that can be standard/breathing
> >mirroring a different driver.
> >
> >> +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 8b14f290c..3290d9957 100644
> >> --- a/MAINTAINERS
> >> +++ b/MAINTAINERS
> >> @@ -4508,6 +4508,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 0e3a0ccd6..319eda887 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 79384d905..2f74f2867 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..3a7af5918
> >> --- /dev/null
> >> +++ b/drivers/hid/hid-ayaneo.c
> >> @@ -0,0 +1,594 @@
> >> +// 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/build_bug.h>
> >> +#include <linux/cleanup.h>
> >> +#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
> >> +
> >> +/*
> >> + * Empirical timings, inherited from the Handheld Daemon
> >> + * implementation of this protocol and validated on hardware: the
> >> + * device answers well within 300ms or not at all, needs about half
> >> + * a second to settle after a reset before it accepts a new
> >> + * configuration, and completes an eject handshake within a few
> >> + * seconds (polled below at a rate that keeps the sysfs write
> >> + * responsive).
> >> + */
> >> +#define AYA3_CMD_TIMEOUT_MS 300
> >> +#define AYA3_CMD_ATTEMPTS 3
> >> +#define AYA3_RESET_SETTLE_MS 500
> >> +#define AYA3_EJECT_POLL_MS 400
> >> +#define AYA3_EJECT_POLLS 20
> >> +
> >> +/* 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
> >> +
> >> +/* 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
> >> +
> >> +/* Config command vibration levels, stored in the high nibble */
> >> +enum aya3_vibration {
> >> + AYA3_VIBRATION_LOW = 0x1,
> >> + AYA3_VIBRATION_MEDIUM = 0x2,
> >> + AYA3_VIBRATION_HIGH = 0x3,
> >> + AYA3_VIBRATION_OFF = 0x4,
> >> +};
> >> +
> >> +struct aya3_rgb {
> >> + u8 mode;
> >> + u8 r;
> >> + u8 g;
> >> + u8 b;
> >> +} __packed;
> >> +
> >> +/*
> >> + * The 65-byte config command. The checksum is the little-endian sum of
> >> + * bytes 7..64; unk* fields are sent as zero.
> >> + */
> >> +struct aya3_config {
> >> + u8 report_id;
> >> + __le16 csum;
> >> + u8 cmd;
> >> + u8 subcmd;
> >> + u8 unk5[3];
> >> + struct aya3_rgb right;
> >> + struct aya3_rgb left;
> >> + u8 unk16[4];
> >> + u8 eject;
> >> + u8 unk21;
> >> + u8 sensitivity[2];
> >> + u8 vibration;
> >> + u8 unk25[7];
> >> + u8 magic;
> >> + u8 unk33[32];
> >
> >I am not sure of the struct naming or whether a struct is needed in
> >this case. if you do not use most of the report, consider documenting
> >it somewhere else and doing direct accesses to the appropriate bytes.
> >
> >> +} __packed;
> >> +static_assert(sizeof(struct aya3_config) == AYA3_REPORT_SIZE);
> >> +
> >> +/* Replies echo the subcommand they answer at byte 3 */
> >> +struct aya3_resp {
> >> + u8 unk0[3];
> >> + u8 subcmd;
> >> + u8 unk4[15];
> >> + u8 eject_status;
> >> + u8 unk20[12];
> >> + u8 module_left;
> >> + u8 module_right;
> >> + u8 unk34[30];
> >> +} __packed;
> >> +static_assert(sizeof(struct aya3_resp) == AYA3_RESP_SIZE);
> >> +
> >> +struct ayaneo {
> >> + 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;
> >> + struct aya3_resp resp;
> >> + 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 ayaneo_send(struct ayaneo *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;
> >> +}
> >> +
> >> +/**
> >> + * ayaneo_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 reply, or NULL to discard it
> >> + *
> >> + * The device echoes the subcommand byte of the command it is answering,
> >> + * which ayaneo_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 ayaneo_cmd(struct ayaneo *aya, struct aya3_resp *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 = ayaneo_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, sizeof(*resp));
> >> + return 0;
> >> + }
> >> + }
> >> + WRITE_ONCE(aya->resp_pending, false);
> >> + return -ETIMEDOUT;
> >> +}
> >> +
> >> +static void ayaneo_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 ayaneo_check(struct ayaneo *aya, struct aya3_resp *resp)
> >> +{
> >> + memset(aya->xfer, 0, AYA3_REPORT_SIZE);
> >> + aya->xfer[4] = AYA3_SUBCMD_CHECK;
> >> + return ayaneo_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 ayaneo_send_config(struct ayaneo *aya, u8 eject)
> >> +{
> >> + static const struct aya3_config template = {
> >> + .cmd = AYA3_CMD_CONFIG,
> >> + .subcmd = AYA3_SUBCMD_CONFIG,
> >> + .magic = 0x01,
> >> + };
> >> + struct aya3_config *cfg = (struct aya3_config *)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;
> >> +
> >> + *cfg = template;
> >> + cfg->right.mode = mode;
> >> + cfg->right.r = aya->rgb[0];
> >> + cfg->right.g = aya->rgb[1];
> >> + cfg->right.b = aya->rgb[2];
> >> + cfg->left = cfg->right;
> >> + cfg->eject = eject;
> >> + cfg->vibration = aya->vibration << 4;
> >
> >If you set vibration, you need to expose it to userspace. Otherwise
> >this driver degrades functionality over userspace implementations.
> >
> >> + ayaneo_checksum(aya->xfer);
> >> +
> >> + return ayaneo_cmd(aya, NULL);
> >> +}
> >> +
> >> +static int ayaneo_raw_event(struct hid_device *hdev, struct hid_report *report,
> >> + u8 *data, int size)
> >> +{
> >> + struct ayaneo *aya = hid_get_drvdata(hdev);
> >> + const struct aya3_resp *resp = (const struct aya3_resp *)data;
> >> +
> >> + 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 (resp->subcmd != aya->resp_expect)
> >> + return 0;
> >> +
> >> + memcpy(&aya->resp, data, sizeof(aya->resp));
> >> + WRITE_ONCE(aya->resp_pending, false);
> >> + complete(&aya->resp_done);
> >> + return 0;
> >> +}
> >> +
> >> +static ssize_t ayaneo_module_show(struct device *dev, char *buf, bool right)
> >> +{
> >> + struct ayaneo *aya = dev_get_drvdata(dev);
> >> + struct aya3_resp resp;
> >> + int ret = 0;
> >> +
> >> + scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock)
> >> + ret = ayaneo_check(aya, &resp);
> >> + if (ret)
> >> + return ret;
> >> +
> >> + return sysfs_emit(buf, "0x%02x\n",
> >> + right ? resp.module_right : resp.module_left);
> >> +}
> >> +
> >> +static ssize_t module_left_show(struct device *dev,
> >> + struct device_attribute *attr, char *buf)
> >> +{
> >> + return ayaneo_module_show(dev, buf, false);
> >> +}
> >> +static DEVICE_ATTR_RO(module_left);
> >> +
> >> +static ssize_t module_right_show(struct device *dev,
> >> + struct device_attribute *attr, char *buf)
> >> +{
> >> + return ayaneo_module_show(dev, buf, true);
> >> +}
> >> +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 ayaneo *aya = dev_get_drvdata(dev);
> >> + struct aya3_resp resp;
> >> + u8 eject;
> >> + int ret = 0, 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;
> >> +
> >> + scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock) {
> >> + ret = ayaneo_send_config(aya, eject);
> >> + if (ret)
> >> + break;
> >> +
> >> + /*
> >> + * 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 < AYA3_EJECT_POLLS; i++) {
> >> + msleep(AYA3_EJECT_POLL_MS);
> >> + err = ayaneo_check(aya, &resp);
> >> + if (err == -ETIMEDOUT)
> >> + continue; /* busy mid-eject, keep polling */
> >> + if (err) {
> >> + ret = err;
> >> + break;
> >> + }
> >> + if (!(resp.eject_status & ~AYA3_EJECT_DONE_MASK)) {
> >> + ret = 0;
> >> + break;
> >> + }
> >> + }
> >> + }
> >> + 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 ayaneo *aya = dev_get_drvdata(dev);
> >> + bool value;
> >> + int ret;
> >> +
> >> + ret = kstrtobool(buf, &value);
> >> + if (ret)
> >> + return ret;
> >> + if (!value)
> >> + return count;
> >> +
> >> + scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock) {
> >> + ret = ayaneo_send_config(aya, AYA3_RESET);
> >> + if (!ret) {
> >> + msleep(AYA3_RESET_SETTLE_MS);
> >> + ret = ayaneo_send_config(aya, 0);
> >> + }
> >> + }
> >> + return ret ? ret : count;
> >> +}
> >> +static DEVICE_ATTR_WO(reset);
> >> +
> >> +static struct attribute *ayaneo_attrs[] = {
> >> + &dev_attr_module_left.attr,
> >> + &dev_attr_module_right.attr,
> >> + &dev_attr_eject.attr,
> >> + &dev_attr_reset.attr,
> >> + NULL
> >> +};
> >> +ATTRIBUTE_GROUPS(ayaneo);
> >> +
> >> +static int ayaneo_led_set(struct led_classdev *cdev, enum led_brightness value)
> >> +{
> >> + struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
> >> + struct ayaneo *aya = container_of(mc, struct ayaneo, mcled);
> >> + int ret = 0, i;
> >> +
> >> + scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock) {
> >> + 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 = ayaneo_send_config(aya, 0);
> >> + if (ret)
> >> + hid_err(aya->hdev,
> >> + "failed to update RGB config: %d\n", ret);
> >> + }
> >> + 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).
> >> + */
> >
> >Prefer removing semicolons; they have a particular smell ;)
> >
> >> +static int ayaneo_pattern_set(struct led_classdev *cdev,
> >> + struct led_pattern *pattern, u32 len, int repeat)
> >> +{
> >> + struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
> >> + struct ayaneo *aya = container_of(mc, struct ayaneo, mcled);
> >> + int ret = 0;
> >> +
> >> + if (len != 2 || pattern[0].brightness || !pattern[1].brightness)
> >> + return -EINVAL;
> >> +
> >> + scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock) {
> >> + aya->pulse = true;
> >> + ret = ayaneo_send_config(aya, 0);
> >> + }
> >> + return ret;
> >> +}
> >> +
> >> +static int ayaneo_pattern_clear(struct led_classdev *cdev)
> >> +{
> >> + struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
> >> + struct ayaneo *aya = container_of(mc, struct ayaneo, mcled);
> >> + int ret = 0;
> >> +
> >> + scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock) {
> >> + aya->pulse = false;
> >> + ret = ayaneo_send_config(aya, 0);
> >> + }
> >> + return ret;
> >> +}
> >> +
> >> +static int ayaneo_register_led(struct ayaneo *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->color = LED_COLOR_ID_RGB;
> >> + cdev->brightness = 0;
> >> + cdev->max_brightness = 255;
> >> + cdev->brightness_set_blocking = ayaneo_led_set;
> >> + cdev->pattern_set = ayaneo_pattern_set;
> >> + cdev->pattern_clear = ayaneo_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 ayaneo_dmi_table[] = {
> >> + {
> >> + .matches = {
> >> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> >> + DMI_MATCH(DMI_BOARD_NAME, "AYANEO 3"),
> >> + },
> >> + },
> >> + {}
> >> +};
> >> +
> >> +static int ayaneo_probe(struct hid_device *hdev, const struct hid_device_id *id)
> >> +{
> >> + struct ayaneo *aya;
> >> + int ret;
> >> +
> >> + /* The VID/PID is a generic SigmaMicro ID; bind on AYANEO 3 only */
> >> + if (!dmi_check_system(ayaneo_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_MEDIUM;
> >> + 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 = ayaneo_check(aya, NULL);
> >> + if (ret)
> >> + hid_warn(hdev, "controller did not answer status check: %d\n",
> >> + ret);
> >
> >Consider dropping the hid_device ... check block unless it is
> >necessary. it seems like a premature test that can go wrong and you
> >touch the device. Particularly, hid_device_io_start is a bit
> >unconventional.
> >
> >With this check removed, this driver does not touch the device without
> >userspace involvement, which is good for userspace implementations
> >such as mine.
> >
> >I think these are all the comments I have. I'd suggest waiting a week
> >before the next revision and up to two weeks for jiri/Benjamin to
> >reply with some comments as I think I was the only one that reviewed
> >the previous revision.
> >
> >Best,
> >Antheas
> >
> >> +
> >> + ret = ayaneo_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 ayaneo_remove(struct hid_device *hdev)
> >> +{
> >> + struct ayaneo *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 ayaneo_devices[] = {
> >> + { HID_USB_DEVICE(0x1c4f, 0x0002) },
> >> + {}
> >> +};
> >> +MODULE_DEVICE_TABLE(hid, ayaneo_devices);
> >> +
> >> +static struct hid_driver ayaneo_driver = {
> >> + .name = "hid-ayaneo",
> >> + .id_table = ayaneo_devices,
> >> + .probe = ayaneo_probe,
> >> + .remove = ayaneo_remove,
> >> + .raw_event = ayaneo_raw_event,
> >> + .driver = {
> >> + .dev_groups = ayaneo_groups,
> >> + },
> >> +};
> >> +module_hid_driver(ayaneo_driver);
> >> +
> >> +MODULE_AUTHOR("Matías Martínez <hello@matias.me>");
> >> +MODULE_DESCRIPTION("AYANEO 3 detachable controller driver");
> >> +MODULE_LICENSE("GPL");
> >> --
> >> 2.54.0 (Apple Git-157)
> >>
> >>
> >
>
>
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v3] HID: ayaneo: Add AYANEO 3 detachable controller driver
2026-09-18 7:52 ` Antheas Kapenekakis
@ 2026-09-18 13:44 ` Derek J. Clark
0 siblings, 0 replies; 15+ messages in thread
From: Derek J. Clark @ 2026-09-18 13:44 UTC (permalink / raw)
To: Antheas Kapenekakis
Cc: Matías Martínez, Jiri Kosina, Benjamin Tissoires,
linux-input, linux-kernel, Denis Benato, Dmitry Torokhov,
Ilpo Järvinen, Armin Wolf
On September 18, 2026 12:52:38 AM PDT, Antheas Kapenekakis <lkml@antheas.dev> wrote:
>On Fri, 18 Sept 2026 at 02:42, Derek J. Clark <derekjohn.clark@gmail.com> wrote:
>>
>> On September 17, 2026 2:48:11 PM PDT, Antheas Kapenekakis <lkml@antheas.dev> wrote:
>> >On Thu, 17 Sept 2026 at 18:07, Matías Martínez <hello@matias.me> wrote:
>> >>
>> >> From: Matías Martínez <hello@matias.me>
>> >
>> >Hi Matias,
>> >
>> >> 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 v3:
>> >> - Use a generic ayaneo_ prefix for entry points and driver structure
>> >> so a future device or protocol revision slots in without churn;
>> >> wire-protocol constants stay AYA3_* since they are specific to
>> >> this firmware generation. [Derek J. Clark]
>> >> - Describe the wire format with packed aya3_config/aya3_resp structs,
>> >> static_assert their sizes against the report sizes, and name every
>> >> firmware vibration level in an enum instead of a lone default
>> >> define. [Derek J. Clark]
>> >> - Take the command lock with scoped_cond_guard(mutex_intr, ...) at
>> >> every interruptible lock site so the unlock cannot be dropped in a
>> >> future edit. [Derek J. Clark]
>> >> - Mark the LED class device LED_COLOR_ID_RGB so userspace can detect
>> >> the RGB interface generically. [Derek J. Clark]
>> >> - Name the firmware timing constants and record where the timings
>> >> come from and what was validated on hardware.
>> >>
>> >> No functional change relative to v2. The rework was prompted by
>> >> Derek J. Clark's review of the driver in the OpenGamingCollective
>> >> tree and retested on an AYANEO 3: module identification, RGB solid
>> >> and breathing via hw_pattern, rejection of malformed hw_pattern
>> >> writes, and repeated bind/unbind cycles, all with a clean dmesg.
>> >>
>> >> One question from that review was whether RGB sysfs writes need
>> >> debouncing, since Steam emits one write per slider increment during
>> >> a color drag. They do not: the driver registers only
>> >> brightness_set_blocking, so the LED core defers stores to its
>> >> set_brightness_work and coalesces bursts to the latest state.
>> >> Measured on hardware, a command+ACK round trip averages 5.3 ms
>> >> (3.8-8 ms over 100 samples) and 1000 back-to-back multi_intensity
>> >> stores return in 12 ms total, reaching the device as two to three
>> >> commands.
>> >
>> >After you submitted your patch series to the lore, why didn't the
>> >review take place here and take place downstream? I understand that
>> >prior to submitting your first kernel patch, it is natural to get some
>> >informal feedback, but it seems that all of the reviews of this driver
>> >happened outside the submission and after the submission? The rby
>> >Denis should not be added by you. It should be added by denis by
>> >replying to the mailing list and then you carry it forward on future
>> >revisions (for provenance).
>> >
>> >If you are developing a downstream kernel patch for
>> >OGC/Bazzite/whoever, it is perfectly fine to do downstream work and
>> >reviews and only submit the driver after it is ready. In fact, it
>> >would be very preferable for all of us for you to test your driver
>> >downstream, resolve all feedback and then submit it when it's ready.
>> >But mixing this is peculiar. This is not a comment on you, you were
>> >not the one reviewing your patch out of band.
>>
>> Antheas,
>>
>> There is no reason to throw shade about the process. This was submitted to OGC unstable prior to v1 where Denis reviewed and tagged it prior to it being submitted. I wasn't Cc'd or informed that it was already on a v2 less than 48h later, so I submitted my feedback directly on the PR a couple days later. Had I been aware I would have obviously submitted feedback here.
>
>If Denis reviewed it prior to V1 this is ok. It still does not explain
>why you reviewed the PR after V1. I checked. Both V1 and V2 were
>posted on Aug 24. Your feedback was on the 28th
Antheas,
I already told you I wasn't aware that it had been posted the LKML when I reviewed it on GitHub. I'm not going to engage with this line of conversation further.
>What you did is a disservice to Matias and to us. Matias wants to see
>his device work under Bazzite. You set the arbitrary requirement of
>kernel drivers being needed for hid writes and that those drivers
>_need_ to be on the mailing list before you merge them. So, his device
>broke in a recent update. Then, Matias spends his personal time to
>develop such a kernel driver and posts it to the mailing list.
>
>Instead of getting thorough reviews here and support to show this
>driver has traction, you conduct your review on a misc repo after
>getting your arbitrary ML requirement met. This is a disservice to
>Matias. And we see a driver without any traction and conduct
>duplicative reviews. This is a disservice to us.
>> Without your feedback necessary we've already implemented better control policies to avoid this in the future.
>
>Explain how you will avoid this situation in the future
No.
Derek
>Antheas
>
>> Thanks,
>> Derek
>>
>> >> Also suggested in that review, but held out of the patch while the
>> >> driver's scope is under discussion: a rumble_intensity attribute
>> >> (the firmware takes three vibration levels), an eject_index
>> >> attribute, and a notification path from hid-ayaneo to ayaneo-ec so
>> >> the EC driver could react to ejects. The last one concerns the
>> >> ayaneo-ec/pdx86 side, hence the added Cc.
>> >>
>> >> 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 | 594 ++++++++++++++++++
>> >> 6 files changed, 668 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
>> >
>> >Consider exploring similar ABIs in e.g., Legion Go S hid and mirroring
>> >their ABI. Then remove this file. This is not the only breathing
>> >device. Moreover, delta_t values are ignored and you are introducing
>> >an ABI for them? Prefer a mode setting that can be standard/breathing
>> >mirroring a different driver.
>> >
>> >> +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 8b14f290c..3290d9957 100644
>> >> --- a/MAINTAINERS
>> >> +++ b/MAINTAINERS
>> >> @@ -4508,6 +4508,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 0e3a0ccd6..319eda887 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 79384d905..2f74f2867 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..3a7af5918
>> >> --- /dev/null
>> >> +++ b/drivers/hid/hid-ayaneo.c
>> >> @@ -0,0 +1,594 @@
>> >> +// 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/build_bug.h>
>> >> +#include <linux/cleanup.h>
>> >> +#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
>> >> +
>> >> +/*
>> >> + * Empirical timings, inherited from the Handheld Daemon
>> >> + * implementation of this protocol and validated on hardware: the
>> >> + * device answers well within 300ms or not at all, needs about half
>> >> + * a second to settle after a reset before it accepts a new
>> >> + * configuration, and completes an eject handshake within a few
>> >> + * seconds (polled below at a rate that keeps the sysfs write
>> >> + * responsive).
>> >> + */
>> >> +#define AYA3_CMD_TIMEOUT_MS 300
>> >> +#define AYA3_CMD_ATTEMPTS 3
>> >> +#define AYA3_RESET_SETTLE_MS 500
>> >> +#define AYA3_EJECT_POLL_MS 400
>> >> +#define AYA3_EJECT_POLLS 20
>> >> +
>> >> +/* 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
>> >> +
>> >> +/* 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
>> >> +
>> >> +/* Config command vibration levels, stored in the high nibble */
>> >> +enum aya3_vibration {
>> >> + AYA3_VIBRATION_LOW = 0x1,
>> >> + AYA3_VIBRATION_MEDIUM = 0x2,
>> >> + AYA3_VIBRATION_HIGH = 0x3,
>> >> + AYA3_VIBRATION_OFF = 0x4,
>> >> +};
>> >> +
>> >> +struct aya3_rgb {
>> >> + u8 mode;
>> >> + u8 r;
>> >> + u8 g;
>> >> + u8 b;
>> >> +} __packed;
>> >> +
>> >> +/*
>> >> + * The 65-byte config command. The checksum is the little-endian sum of
>> >> + * bytes 7..64; unk* fields are sent as zero.
>> >> + */
>> >> +struct aya3_config {
>> >> + u8 report_id;
>> >> + __le16 csum;
>> >> + u8 cmd;
>> >> + u8 subcmd;
>> >> + u8 unk5[3];
>> >> + struct aya3_rgb right;
>> >> + struct aya3_rgb left;
>> >> + u8 unk16[4];
>> >> + u8 eject;
>> >> + u8 unk21;
>> >> + u8 sensitivity[2];
>> >> + u8 vibration;
>> >> + u8 unk25[7];
>> >> + u8 magic;
>> >> + u8 unk33[32];
>> >
>> >I am not sure of the struct naming or whether a struct is needed in
>> >this case. if you do not use most of the report, consider documenting
>> >it somewhere else and doing direct accesses to the appropriate bytes.
>> >
>> >> +} __packed;
>> >> +static_assert(sizeof(struct aya3_config) == AYA3_REPORT_SIZE);
>> >> +
>> >> +/* Replies echo the subcommand they answer at byte 3 */
>> >> +struct aya3_resp {
>> >> + u8 unk0[3];
>> >> + u8 subcmd;
>> >> + u8 unk4[15];
>> >> + u8 eject_status;
>> >> + u8 unk20[12];
>> >> + u8 module_left;
>> >> + u8 module_right;
>> >> + u8 unk34[30];
>> >> +} __packed;
>> >> +static_assert(sizeof(struct aya3_resp) == AYA3_RESP_SIZE);
>> >> +
>> >> +struct ayaneo {
>> >> + 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;
>> >> + struct aya3_resp resp;
>> >> + 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 ayaneo_send(struct ayaneo *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;
>> >> +}
>> >> +
>> >> +/**
>> >> + * ayaneo_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 reply, or NULL to discard it
>> >> + *
>> >> + * The device echoes the subcommand byte of the command it is answering,
>> >> + * which ayaneo_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 ayaneo_cmd(struct ayaneo *aya, struct aya3_resp *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 = ayaneo_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, sizeof(*resp));
>> >> + return 0;
>> >> + }
>> >> + }
>> >> + WRITE_ONCE(aya->resp_pending, false);
>> >> + return -ETIMEDOUT;
>> >> +}
>> >> +
>> >> +static void ayaneo_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 ayaneo_check(struct ayaneo *aya, struct aya3_resp *resp)
>> >> +{
>> >> + memset(aya->xfer, 0, AYA3_REPORT_SIZE);
>> >> + aya->xfer[4] = AYA3_SUBCMD_CHECK;
>> >> + return ayaneo_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 ayaneo_send_config(struct ayaneo *aya, u8 eject)
>> >> +{
>> >> + static const struct aya3_config template = {
>> >> + .cmd = AYA3_CMD_CONFIG,
>> >> + .subcmd = AYA3_SUBCMD_CONFIG,
>> >> + .magic = 0x01,
>> >> + };
>> >> + struct aya3_config *cfg = (struct aya3_config *)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;
>> >> +
>> >> + *cfg = template;
>> >> + cfg->right.mode = mode;
>> >> + cfg->right.r = aya->rgb[0];
>> >> + cfg->right.g = aya->rgb[1];
>> >> + cfg->right.b = aya->rgb[2];
>> >> + cfg->left = cfg->right;
>> >> + cfg->eject = eject;
>> >> + cfg->vibration = aya->vibration << 4;
>> >
>> >If you set vibration, you need to expose it to userspace. Otherwise
>> >this driver degrades functionality over userspace implementations.
>> >
>> >> + ayaneo_checksum(aya->xfer);
>> >> +
>> >> + return ayaneo_cmd(aya, NULL);
>> >> +}
>> >> +
>> >> +static int ayaneo_raw_event(struct hid_device *hdev, struct hid_report *report,
>> >> + u8 *data, int size)
>> >> +{
>> >> + struct ayaneo *aya = hid_get_drvdata(hdev);
>> >> + const struct aya3_resp *resp = (const struct aya3_resp *)data;
>> >> +
>> >> + 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 (resp->subcmd != aya->resp_expect)
>> >> + return 0;
>> >> +
>> >> + memcpy(&aya->resp, data, sizeof(aya->resp));
>> >> + WRITE_ONCE(aya->resp_pending, false);
>> >> + complete(&aya->resp_done);
>> >> + return 0;
>> >> +}
>> >> +
>> >> +static ssize_t ayaneo_module_show(struct device *dev, char *buf, bool right)
>> >> +{
>> >> + struct ayaneo *aya = dev_get_drvdata(dev);
>> >> + struct aya3_resp resp;
>> >> + int ret = 0;
>> >> +
>> >> + scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock)
>> >> + ret = ayaneo_check(aya, &resp);
>> >> + if (ret)
>> >> + return ret;
>> >> +
>> >> + return sysfs_emit(buf, "0x%02x\n",
>> >> + right ? resp.module_right : resp.module_left);
>> >> +}
>> >> +
>> >> +static ssize_t module_left_show(struct device *dev,
>> >> + struct device_attribute *attr, char *buf)
>> >> +{
>> >> + return ayaneo_module_show(dev, buf, false);
>> >> +}
>> >> +static DEVICE_ATTR_RO(module_left);
>> >> +
>> >> +static ssize_t module_right_show(struct device *dev,
>> >> + struct device_attribute *attr, char *buf)
>> >> +{
>> >> + return ayaneo_module_show(dev, buf, true);
>> >> +}
>> >> +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 ayaneo *aya = dev_get_drvdata(dev);
>> >> + struct aya3_resp resp;
>> >> + u8 eject;
>> >> + int ret = 0, 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;
>> >> +
>> >> + scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock) {
>> >> + ret = ayaneo_send_config(aya, eject);
>> >> + if (ret)
>> >> + break;
>> >> +
>> >> + /*
>> >> + * 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 < AYA3_EJECT_POLLS; i++) {
>> >> + msleep(AYA3_EJECT_POLL_MS);
>> >> + err = ayaneo_check(aya, &resp);
>> >> + if (err == -ETIMEDOUT)
>> >> + continue; /* busy mid-eject, keep polling */
>> >> + if (err) {
>> >> + ret = err;
>> >> + break;
>> >> + }
>> >> + if (!(resp.eject_status & ~AYA3_EJECT_DONE_MASK)) {
>> >> + ret = 0;
>> >> + break;
>> >> + }
>> >> + }
>> >> + }
>> >> + 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 ayaneo *aya = dev_get_drvdata(dev);
>> >> + bool value;
>> >> + int ret;
>> >> +
>> >> + ret = kstrtobool(buf, &value);
>> >> + if (ret)
>> >> + return ret;
>> >> + if (!value)
>> >> + return count;
>> >> +
>> >> + scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock) {
>> >> + ret = ayaneo_send_config(aya, AYA3_RESET);
>> >> + if (!ret) {
>> >> + msleep(AYA3_RESET_SETTLE_MS);
>> >> + ret = ayaneo_send_config(aya, 0);
>> >> + }
>> >> + }
>> >> + return ret ? ret : count;
>> >> +}
>> >> +static DEVICE_ATTR_WO(reset);
>> >> +
>> >> +static struct attribute *ayaneo_attrs[] = {
>> >> + &dev_attr_module_left.attr,
>> >> + &dev_attr_module_right.attr,
>> >> + &dev_attr_eject.attr,
>> >> + &dev_attr_reset.attr,
>> >> + NULL
>> >> +};
>> >> +ATTRIBUTE_GROUPS(ayaneo);
>> >> +
>> >> +static int ayaneo_led_set(struct led_classdev *cdev, enum led_brightness value)
>> >> +{
>> >> + struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
>> >> + struct ayaneo *aya = container_of(mc, struct ayaneo, mcled);
>> >> + int ret = 0, i;
>> >> +
>> >> + scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock) {
>> >> + 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 = ayaneo_send_config(aya, 0);
>> >> + if (ret)
>> >> + hid_err(aya->hdev,
>> >> + "failed to update RGB config: %d\n", ret);
>> >> + }
>> >> + 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).
>> >> + */
>> >
>> >Prefer removing semicolons; they have a particular smell ;)
>> >
>> >> +static int ayaneo_pattern_set(struct led_classdev *cdev,
>> >> + struct led_pattern *pattern, u32 len, int repeat)
>> >> +{
>> >> + struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
>> >> + struct ayaneo *aya = container_of(mc, struct ayaneo, mcled);
>> >> + int ret = 0;
>> >> +
>> >> + if (len != 2 || pattern[0].brightness || !pattern[1].brightness)
>> >> + return -EINVAL;
>> >> +
>> >> + scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock) {
>> >> + aya->pulse = true;
>> >> + ret = ayaneo_send_config(aya, 0);
>> >> + }
>> >> + return ret;
>> >> +}
>> >> +
>> >> +static int ayaneo_pattern_clear(struct led_classdev *cdev)
>> >> +{
>> >> + struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
>> >> + struct ayaneo *aya = container_of(mc, struct ayaneo, mcled);
>> >> + int ret = 0;
>> >> +
>> >> + scoped_cond_guard(mutex_intr, return -EINTR, &aya->lock) {
>> >> + aya->pulse = false;
>> >> + ret = ayaneo_send_config(aya, 0);
>> >> + }
>> >> + return ret;
>> >> +}
>> >> +
>> >> +static int ayaneo_register_led(struct ayaneo *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->color = LED_COLOR_ID_RGB;
>> >> + cdev->brightness = 0;
>> >> + cdev->max_brightness = 255;
>> >> + cdev->brightness_set_blocking = ayaneo_led_set;
>> >> + cdev->pattern_set = ayaneo_pattern_set;
>> >> + cdev->pattern_clear = ayaneo_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 ayaneo_dmi_table[] = {
>> >> + {
>> >> + .matches = {
>> >> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
>> >> + DMI_MATCH(DMI_BOARD_NAME, "AYANEO 3"),
>> >> + },
>> >> + },
>> >> + {}
>> >> +};
>> >> +
>> >> +static int ayaneo_probe(struct hid_device *hdev, const struct hid_device_id *id)
>> >> +{
>> >> + struct ayaneo *aya;
>> >> + int ret;
>> >> +
>> >> + /* The VID/PID is a generic SigmaMicro ID; bind on AYANEO 3 only */
>> >> + if (!dmi_check_system(ayaneo_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_MEDIUM;
>> >> + 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 = ayaneo_check(aya, NULL);
>> >> + if (ret)
>> >> + hid_warn(hdev, "controller did not answer status check: %d\n",
>> >> + ret);
>> >
>> >Consider dropping the hid_device ... check block unless it is
>> >necessary. it seems like a premature test that can go wrong and you
>> >touch the device. Particularly, hid_device_io_start is a bit
>> >unconventional.
>> >
>> >With this check removed, this driver does not touch the device without
>> >userspace involvement, which is good for userspace implementations
>> >such as mine.
>> >
>> >I think these are all the comments I have. I'd suggest waiting a week
>> >before the next revision and up to two weeks for jiri/Benjamin to
>> >reply with some comments as I think I was the only one that reviewed
>> >the previous revision.
>> >
>> >Best,
>> >Antheas
>> >
>> >> +
>> >> + ret = ayaneo_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 ayaneo_remove(struct hid_device *hdev)
>> >> +{
>> >> + struct ayaneo *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 ayaneo_devices[] = {
>> >> + { HID_USB_DEVICE(0x1c4f, 0x0002) },
>> >> + {}
>> >> +};
>> >> +MODULE_DEVICE_TABLE(hid, ayaneo_devices);
>> >> +
>> >> +static struct hid_driver ayaneo_driver = {
>> >> + .name = "hid-ayaneo",
>> >> + .id_table = ayaneo_devices,
>> >> + .probe = ayaneo_probe,
>> >> + .remove = ayaneo_remove,
>> >> + .raw_event = ayaneo_raw_event,
>> >> + .driver = {
>> >> + .dev_groups = ayaneo_groups,
>> >> + },
>> >> +};
>> >> +module_hid_driver(ayaneo_driver);
>> >> +
>> >> +MODULE_AUTHOR("Matías Martínez <hello@matias.me>");
>> >> +MODULE_DESCRIPTION("AYANEO 3 detachable controller driver");
>> >> +MODULE_LICENSE("GPL");
>> >> --
>> >> 2.54.0 (Apple Git-157)
>> >>
>> >>
>> >
>>
>>
>
^ permalink raw reply [flat|nested] 15+ messages in thread
end of thread, other threads:[~2026-09-18 13:44 UTC | newest]
Thread overview: 15+ 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:31 ` [PATCH v2] " Matías Martínez
2026-09-17 16:07 ` [PATCH v3] " Matías Martínez
2026-09-17 21:48 ` Antheas Kapenekakis
2026-09-17 22:23 ` Matías Martínez
2026-09-17 22:54 ` Antheas Kapenekakis
2026-09-18 0:42 ` Derek J. Clark
2026-09-18 1:38 ` Denis Benato
2026-09-18 7:52 ` Antheas Kapenekakis
2026-09-18 13:44 ` Derek J. Clark
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®