[PATCH v3] HID: ayaneo: Add AYANEO 3 detachable controller driver

From: Matías Martínez

Date: Thu Sep 17 2026 - 12:28:02 EST


From: Matías Martínez <hello@xxxxxxxxx>

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@xxxxxxxxx>
Reviewed-by: Denis Benato <denis.benato@xxxxxxxxx>
---
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@xxxxxxxxx>
+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@xxxxxxxxx>
+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@xxxxxxxxx>
+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@xxxxxxxxx>
+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@xxxxxxxxx>
+L: linux-input@xxxxxxxxxxxxxxx
+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@xxxxxxxxxxx>
L: platform-driver-x86@xxxxxxxxxxxxxxx
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@xxxxxxxxx>
+ */
+
+#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@xxxxxxxxx>");
+MODULE_DESCRIPTION("AYANEO 3 detachable controller driver");
+MODULE_LICENSE("GPL");
--
2.54.0 (Apple Git-157)