Linux Input/HID development
 help / color / mirror / Atom feed
From: "Matías Martínez" <hello@matias.me>
To: Jiri Kosina <jikos@kernel.org>, Benjamin Tissoires <bentiss@kernel.org>
Cc: linux-input@vger.kernel.org, linux-kernel@vger.kernel.org,
	"Antheas Kapenekakis" <lkml@antheas.dev>,
	"Denis Benato" <denis.benato@linux.dev>,
	"Dmitry Torokhov" <dmitry.torokhov@gmail.com>,
	"Matías Martínez" <hello@matias.me>
Subject: [PATCH v2] HID: ayaneo: Add AYANEO 3 detachable controller driver
Date: Mon, 24 Aug 2026 18:31:03 -0400	[thread overview]
Message-ID: <20260824223103.93947-1-hello@matias.me> (raw)
In-Reply-To: <20260824215041.79892-1-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 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)


  parent reply	other threads:[~2026-08-24 22:31 UTC|newest]

Thread overview: 10+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-24 21:50 [PATCH] HID: ayaneo: Add AYANEO 3 detachable controller driver Matías Martínez
2026-08-24 22:00 ` Antheas Kapenekakis
2026-08-24 22:25   ` Antheas Kapenekakis
2026-08-24 22:47     ` Matías Martínez
2026-08-24 23:23       ` Antheas Kapenekakis
2026-08-25 17:21         ` Matías Martínez
2026-08-24 22:01 ` sashiko-bot
2026-08-24 22:47   ` Matías Martínez
2026-08-24 22:31 ` Matías Martínez [this message]
2026-08-24 22:41   ` [PATCH v2] " sashiko-bot

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260824223103.93947-1-hello@matias.me \
    --to=hello@matias.me \
    --cc=bentiss@kernel.org \
    --cc=denis.benato@linux.dev \
    --cc=dmitry.torokhov@gmail.com \
    --cc=jikos@kernel.org \
    --cc=linux-input@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=lkml@antheas.dev \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox