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 0000000000000..00f100dba74e1 --- /dev/null +++ b/Documentation/ABI/testing/sysfs-class-led-driver-hid-ayaneo @@ -0,0 +1,15 @@ +What: /sys/class/leds//hw_pattern +Date: August 2026 +KernelVersion: 7.3 +Contact: Matías Martínez +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 " + + Both delta_t values are accepted but ignored, as the + period is not configurable. 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 0000000000000..807c4fc9d768b --- /dev/null +++ b/Documentation/ABI/testing/sysfs-driver-hid-ayaneo @@ -0,0 +1,36 @@ +What: /sys/bus/hid/drivers/hid-ayaneo//module_left +What: /sys/bus/hid/drivers/hid-ayaneo//module_right +Date: August 2026 +KernelVersion: 7.3 +Contact: Matías Martínez +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//eject +Date: August 2026 +KernelVersion: 7.3 +Contact: Matías Martínez +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//reset +Date: August 2026 +KernelVersion: 7.3 +Contact: Matías Martínez +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 4dae728bffcf5..77de7c0ceed54 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -4466,6 +4466,14 @@ S: Maintained F: Documentation/ABI/testing/sysfs-platform-ayn-ec F: drivers/platform/x86/ayn-ec.c +AYANEO 3 CONTROLLER HID DRIVER +M: Matías Martínez +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 L: platform-driver-x86@vger.kernel.org diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig index 48934c4f3c45c..a0dcace8e005a 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 23e6e3dd0c56a..6dced58a2b6fc 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 0000000000000..3a7af5918ee1e --- /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 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 "; 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 "); +MODULE_DESCRIPTION("AYANEO 3 detachable controller driver"); +MODULE_LICENSE("GPL");