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/Documentation/ABI/testing/sysfs-platform-ayn-ec b/Documentation/ABI/testing/sysfs-platform-ayn-ec new file mode 100644 index 0000000000000..32cb6f7ca2fc5 --- /dev/null +++ b/Documentation/ABI/testing/sysfs-platform-ayn-ec @@ -0,0 +1,59 @@ +What: /sys/class/hwmon/hwmon[0-9]/pwm1_enable +Date: July 2025 +KernelVersion: 6.17 +Contact: "Derek J. Clark" +Description: + This sets the PWM fan mode of operation. Valid values are [0-3]. + Values [0-2] conform with standard hwmon operating modes. Value 3 + enables user defined fan curve settings. + + Applies to AYN Loki and Tectoy Zeenix lines of handheld devices. + +What: /sys/class/hwmon/hwmon[0-9]/pwm1_auto_point[1-5]_pwm +Date: July 2025 +KernelVersion: 6.17 +Contact: "Derek J. Clark" +Description: + This sets the PWM fan duty cycle for the given index of the fan curve. + When the temperature reaches the corresponding pwm1_auto_point[1-5]_temp, + the EC will automatically increase the fan duty cycle to the given value. + + Values are [0-255] + + Applies to AYN Loki and Tectoy Zeenix lines of handheld devices. + +What: /sys/class/hwmon/hwmon[0-9]/pwm1_auto_point[1-5]_temp +Date: July 2025 +KernelVersion: 6.17 +Contact: "Derek J. Clark" +Description: + This sets the activation temperature for the given index of the fan curve. + When the temperature reaches the given value, the EC will automatically + increase the fan duty cycle to the corresponding pwm1_auto_point[1-5]_pwm + value. + + Values are [0-100] + + Applies to AYN Loki and Tectoy Zeenix lines of handheld devices. + +What: /sys/class/leds/ayn:rgb:joystick_rings/effect +Date: July 2025 +KernelVersion: 6.17 +Contact: "Derek J. Clark" +Description: + This controls the display effect of the RGB interface. + + Values are monocolor or breathe. + + Applies to AYN Loki and Tectoy Zeenix lines of handheld devices. + +What: /sys/class/leds/ayn:rgb:joystick_rings/effect_index +Date: July 2025 +KernelVersion: 6.17 +Contact: "Derek J. Clark" +Description: + This displays the available options for the effect attribute. + + Values are monocolor or breathe. + + Applies to AYN Loki and Tectoy Zeenix lines of handheld devices. diff --git a/MAINTAINERS b/MAINTAINERS index 8014b9f8253ed..b81987f25387b 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -4459,6 +4459,20 @@ F: Documentation/devicetree/bindings/spi/axiado,ax3000-spi.yaml F: drivers/spi/spi-axiado.c F: drivers/spi/spi-axiado.h +AYN PLATFORM EC DRIVER +M: Derek J. Clark +L: platform-driver-x86@vger.kernel.org +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-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 f9bcaeb66385b..4077c77bfa44d 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..a44b9a52a8429 --- /dev/null +++ b/drivers/hid/hid-ayaneo.c @@ -0,0 +1,455 @@ +// 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 + +#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; +} + +/* Send the command in aya->xfer and wait for the echoing reply. */ +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) +{ + unsigned int 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) +{ + u8 *buf = aya->xfer; + u8 mode = AYA3_RGB_SOLID; + + if (!aya->rgb[0] && !aya->rgb[1] && !aya->rgb[2]) + mode = AYA3_RGB_OFF; + + memset(buf, 0, AYA3_REPORT_SIZE); + buf[3] = AYA3_CMD_CONFIG; + buf[4] = AYA3_SUBCMD_CONFIG; + /* 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[22] = 0x33; + buf[23] = 0x22; /* joystick sensitivity 100%/100% */ + buf[24] = aya->vibration << 4; + buf[32] = 0x01; + buf[37] = 0x64; + buf[38] = 0x64; + 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); + 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 = "ayaneo:rgb:joystick_rings"; + 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; + u8 resp[AYA3_RESP_SIZE]; + int ret; + + /* The VID/PID is a generic SigmaMicro ID; bind on AYANEO 3 only */ + if (!dmi_check_system(aya3_dmi_table)) + return -ENODEV; + + ret = hid_parse(hdev); + if (ret) + return ret; + + /* Bind only the vendor interface, not the gamepad/keyboard ones */ + if (!hid_is_usb(hdev) || + 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); + + mutex_lock(&aya->lock); + ret = aya3_check(aya, resp); + mutex_unlock(&aya->lock); + if (ret) + hid_warn(hdev, "controller did not answer status check: %d\n", + ret); + else + hid_info(hdev, "modules: left 0x%02x right 0x%02x\n", + resp[AYA3_RESP_MODULE_LEFT], + resp[AYA3_RESP_MODULE_RIGHT]); + + 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 "); +MODULE_DESCRIPTION("AYANEO 3 detachable controller driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig index b54b5212b2047..96f3b7ad497aa 100644 --- a/drivers/platform/x86/Kconfig +++ b/drivers/platform/x86/Kconfig @@ -330,6 +330,21 @@ config ASUS_TF103C_DOCK If you have an Asus TF103C tablet say Y or M here, for a generic x86 distro config say M here. +config AYN_EC + tristate "AYN x86 devices EC platform control" + depends on ACPI + depends on HWMON + depends on NEW_LEDS + select LEDS_CLASS + select LEDS_CLASS_MULTICOLOR + help + This is a driver for AYN and Tectoy x86 handheld devices. It provides + temperature monitoring, manual fan speed control, fan curve control, + and chassis RGB settings. + + If you have an x86 AYN or Tectoy handheld device say M here. The module + will be called ayn-platform. + config AYANEO_EC tristate "Ayaneo EC platform control" depends on DMI diff --git a/drivers/platform/x86/Makefile b/drivers/platform/x86/Makefile index 872ac3842391f..31b4fd9083948 100644 --- a/drivers/platform/x86/Makefile +++ b/drivers/platform/x86/Makefile @@ -40,6 +40,9 @@ obj-$(CONFIG_ASUS_TF103C_DOCK) += asus-tf103c-dock.o obj-$(CONFIG_EEEPC_LAPTOP) += eeepc-laptop.o obj-$(CONFIG_EEEPC_WMI) += eeepc-wmi.o +# Ayn +obj-$(CONFIG_AYN_EC) += ayn-ec.o + # Ayaneo obj-$(CONFIG_AYANEO_EC) += ayaneo-ec.o diff --git a/drivers/platform/x86/ayn-ec.c b/drivers/platform/x86/ayn-ec.c new file mode 100644 index 0000000000000..25f748d7db18e --- /dev/null +++ b/drivers/platform/x86/ayn-ec.c @@ -0,0 +1,889 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Platform driver for AYN x86 Handhelds. + * + * Implements multiple attributes provided by the EC. Fan reading and control, + * as well as temperature sensor readings are exposed via hwmon sysfs. EC RGB + * control is exposed via an led-class-multicolor interface. + * + * Fan control is provided via a pwm interface in the range [0-255]. AYN use + * [0-128] as the range in the EC, the written value is scaled to accommodate. + * The EC also provides a configurable fan curve with five set points that + * associate a temperature in Celcius [0-100] with a fan speed [0-128]. The + * auto_point fan speeds are also scaled from the range [0-255]. Temperature + * readings are scaled from degrees to millidegrees when read. + * + * RGB control is provided using 4 registers. One each for the colors red, + * green, and blue are [0-255]. There is also a effect register that takes + * switches between an EC controlled breathing that cycles through all colors + * and fades in/out, and manual, which enables setting a user defined color. + * + * Copyright (C) 2025 Derek J. Clark + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Fan speed and PWM registers */ +#define AYN_SENSOR_PWM_FAN_ENABLE_REG 0x10 /* PWM operating mode */ +#define AYN_SENSOR_PWM_FAN_SET_REG 0x11 /* PWM duty cycle */ +#define AYN_SENSOR_PWM_FAN_SPEED_REG 0x20 /* Fan speed */ + +/* EC controlled fan curve registers */ +#define AYN_SENSOR_PWM_FAN_SPEED_1_REG 0x12 +#define AYN_SENSOR_PWM_FAN_SPEED_2_REG 0x14 +#define AYN_SENSOR_PWM_FAN_SPEED_3_REG 0x16 +#define AYN_SENSOR_PWM_FAN_SPEED_4_REG 0x18 +#define AYN_SENSOR_PWM_FAN_SPEED_5_REG 0x1A +#define AYN_SENSOR_PWM_FAN_TEMP_1_REG 0x13 +#define AYN_SENSOR_PWM_FAN_TEMP_2_REG 0x15 +#define AYN_SENSOR_PWM_FAN_TEMP_3_REG 0x17 +#define AYN_SENSOR_PWM_FAN_TEMP_4_REG 0x19 +#define AYN_SENSOR_PWM_FAN_TEMP_5_REG 0x1B + +/* AYN EC PWM Fan modes */ +#define AYN_PWM_FAN_MODE_MANUAL 0x00 +#define AYN_PWM_FAN_MODE_AUTO 0x01 +#define AYN_PWM_FAN_MODE_EC_CURVE 0x02 + +/* hwmon fan modes */ +#define HWMON_PWM_FAN_MODE_FULL 0x00 +#define HWMON_PWM_FAN_MODE_MANUAL 0x01 +#define HWMON_PWM_FAN_MODE_AUTO 0x02 +#define HWMON_PWM_FAN_MODE_EC_CURVE 0x03 + +/* EC Temperature Sensors */ +#define AYN_SENSOR_BAT_TEMP_REG 0x04 /* Battery */ +#define AYN_SENSOR_CHARGE_TEMP_REG 0x07 /* Charger IC */ +#define AYN_SENSOR_MB_TEMP_REG 0x05 /* Motherboard */ +#define AYN_SENSOR_PROC_TEMP_REG 0x09 /* CPU Core */ +#define AYN_SENSOR_VCORE_TEMP_REG 0x08 /* vCore */ + +/* EC Controlled RGB registers */ +#define AYN_LED_MC_RED_REG 0xB0 /* Range 0x00-0xFF */ +#define AYN_LED_MC_GREEN_REG 0xB1 /* Range 0x00-0xFF */ +#define AYN_LED_MC_BLUE_REG 0xB2 /* Range 0x00-0xFF */ +#define AYN_RGB_EFFECT_REG 0xB3 + +/* RGB effect modes */ +#define AYN_RGB_EFFECT_BREATHE 0x00 +#define AYN_RGB_EFFECT_MONOCOLOR 0x55 +#define AYN_RGB_EFFECT_WRITE 0xAA + +/* Handle ACPI lock mechanism */ +#define ACPI_LOCK_DELAY_MS 500 + +int ayn_pwm_curve_registers[10] = { + AYN_SENSOR_PWM_FAN_SPEED_1_REG, + AYN_SENSOR_PWM_FAN_SPEED_2_REG, + AYN_SENSOR_PWM_FAN_SPEED_3_REG, + AYN_SENSOR_PWM_FAN_SPEED_4_REG, + AYN_SENSOR_PWM_FAN_SPEED_5_REG, + AYN_SENSOR_PWM_FAN_TEMP_1_REG, + AYN_SENSOR_PWM_FAN_TEMP_2_REG, + AYN_SENSOR_PWM_FAN_TEMP_3_REG, + AYN_SENSOR_PWM_FAN_TEMP_4_REG, + AYN_SENSOR_PWM_FAN_TEMP_5_REG, +}; + +struct ayn_device { + struct led_classdev *led_cdev; + u32 ayn_lock; /* ACPI EC Lock */ + u8 rgb_effect; +} drvdata; + +struct thermal_sensor { + char *name; + int reg; +}; + +static struct thermal_sensor thermal_sensors[] = { + { "Battery", AYN_SENSOR_BAT_TEMP_REG }, + { "Motherboard", AYN_SENSOR_MB_TEMP_REG }, + { "Charger IC", AYN_SENSOR_CHARGE_TEMP_REG }, + { "vCore", AYN_SENSOR_VCORE_TEMP_REG }, + { "CPU Core", AYN_SENSOR_PROC_TEMP_REG }, + {} +}; + +#define DEVICE_ATTR_RW_NAMED(_name, _attrname) \ + struct device_attribute dev_attr_##_name = { \ + .attr = { .name = _attrname, .mode = 0644 }, \ + .show = _name##_show, \ + .store = _name##_store, \ + } + +#define DEVICE_ATTR_RO_NAMED(_name, _attrname) \ + struct device_attribute dev_attr_##_name = { \ + .attr = { .name = _attrname, .mode = 0444 }, \ + .show = _name##_show, \ + } + +/* Handle ACPI lock mechanism */ +#define ACPI_LOCK_DELAY_MS 500 + +/* RGB effect values */ +enum RGB_EFFECT_OPTION { + BREATHE, + MONOCOLOR, +}; + +static const char *const RGB_EFFECT_TEXT[] = { + [BREATHE] = "breathe", + [MONOCOLOR] = "monocolor", +}; + +static bool lock_global_acpi_lock(void) +{ + return ACPI_SUCCESS(acpi_acquire_global_lock(ACPI_LOCK_DELAY_MS, + &drvdata.ayn_lock)); +} + +static bool unlock_global_acpi_lock(void) +{ + return ACPI_SUCCESS(acpi_release_global_lock(drvdata.ayn_lock)); +} + +/** + * read_from_ec() - Reads a value from the embedded controller. + * + * @reg: The register to start the read from. + * @size: The number of sequential registers the data is contained in. + * @val: Pointer to return the data with. + * + * Return: 0, or an error. + */ +static int read_from_ec(u8 reg, int size, long *val) +{ + int ret, i; + u8 buf; + + if (!lock_global_acpi_lock()) + return -EBUSY; + + *val = 0; + for (i = 0; i < size; i++) { + ret = ec_read(reg + i, &buf); + if (ret) + return ret; + *val <<= i * 8; + *val += buf; + } + + if (!unlock_global_acpi_lock()) + return -EBUSY; + + return 0; +} + +/** + * write_to_ec() - Writes a value to the embedded controller. + * + * @reg: The register to write to. + * @val: Value to write + * + * Return: 0, or an error. + */ +static int write_to_ec(u8 reg, u8 val) +{ + int ret; + + if (!lock_global_acpi_lock()) + return -EBUSY; + + pr_info("Writing EC value %d to register %u\n", val, reg); + ret = ec_write(reg, val); + + if (!unlock_global_acpi_lock()) + return -EBUSY; + + return ret; +} + +/** + * ayn_pwm_manual() - Enable manual control of the fan. + */ +static int ayn_pwm_manual(void) +{ + return write_to_ec(AYN_SENSOR_PWM_FAN_ENABLE_REG, 0x00); +} + +/** + * ayn_pwm_full() - Set fan to 100% speed. + */ +static int ayn_pwm_full(void) +{ + int ret; + + ret = write_to_ec(AYN_SENSOR_PWM_FAN_ENABLE_REG, 0x00); + if (ret) + return ret; + + return write_to_ec(AYN_SENSOR_PWM_FAN_SET_REG, 128); +} + +/** + * ayn_pwm_auto() - Enable automatic EC control of the fan. + */ +static int ayn_pwm_auto(void) +{ + return write_to_ec(AYN_SENSOR_PWM_FAN_ENABLE_REG, 0x01); +} + +/** + * ayn_pwm_ec_curve() - Enable manually setting the fan curve for automatic + * EC control of the fan. + */ +static int ayn_pwm_ec_curve(void) +{ + return write_to_ec(AYN_SENSOR_PWM_FAN_ENABLE_REG, 0x02); +} + +/** + * ayn_ec_hwmon_is_visible() - Determines RO or RW for hwmon attribute sysfs. + * + * @drvdata: Unused void pointer to context data. + * @type: The hwmon_sensor_types type. + * @attr: The attribute to set RO/RW on. + * @channel: HWMON subsystem usage flags for the attribute. + * + * Return: Permission level. + */ +static umode_t ayn_ec_hwmon_is_visible(const void *drvdata, + enum hwmon_sensor_types type, u32 attr, + int channel) +{ + switch (type) { + case hwmon_fan: + return 0444; + case hwmon_pwm: + return 0644; + default: + return 0; + } +} + +/** + * ayn_pwm_fan_read() - Read from a hwmon pwm or fan attribute. + * + * @dev: parent device of the given attribute. + * @type: The hwmon_sensor_types type. + * @attr: The attribute to read from. + * @channel: HWMON subsystem usage flags for the attribute. + * @val: Pointer to return the read value from. + * + * Return: 0, or an error. + */ +static int ayn_pwm_fan_read(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long *val) +{ + int ret; + + switch (type) { + case hwmon_fan: + switch (attr) { + case hwmon_fan_input: + return read_from_ec(AYN_SENSOR_PWM_FAN_SPEED_REG, 2, + val); + default: + break; + } + break; + case hwmon_pwm: + switch (attr) { + case hwmon_pwm_enable: + ret = read_from_ec(AYN_SENSOR_PWM_FAN_ENABLE_REG, 1, + val); + if (ret) + return ret; + + /* EC uses 0 for manual, 1 for automatic, 2 for user + * fan curve. Reflect hwmon usage instead. + */ + if (*val == 1) { + *val = 2; + return 0; + } + + if (*val == 2) { + *val = 3; + return 0; + } + + /* Return 0 when fan at max, otherwise 1 for manual. */ + ret = read_from_ec(AYN_SENSOR_PWM_FAN_SET_REG, 1, val); + if (ret) + return ret; + + if (*val == 128) + *val = 0; + else + *val = 1; + + return ret; + case hwmon_pwm_input: + ret = read_from_ec(AYN_SENSOR_PWM_FAN_SET_REG, 1, val); + if (ret) + return ret; + + *val = *val << 1; /* Max value is 128, scale to 255 */ + + return 0; + default: + break; + } + break; + default: + break; + } + return -EOPNOTSUPP; +} + +/** + * ayn_pwm_fan_write() - Write to a hwmon pwm attribute. + * + * @dev: parent device of the given attribute. + * @type: The hwmon_sensor_types type. + * @attr: The attribute to write to. + * @channel: HWMON subsystem usage flags for the attribute. + * @val: Value to write. + * + * Return: 0, or an error. + */ +static int ayn_pwm_fan_write(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long val) +{ + if (type != hwmon_pwm) + return -EOPNOTSUPP; + switch (attr) { + case hwmon_pwm_enable: + switch (val) { + case HWMON_PWM_FAN_MODE_FULL: + return ayn_pwm_full(); + case HWMON_PWM_FAN_MODE_MANUAL: + return ayn_pwm_manual(); + case HWMON_PWM_FAN_MODE_AUTO: + return ayn_pwm_auto(); + case HWMON_PWM_FAN_MODE_EC_CURVE: + return ayn_pwm_ec_curve(); + default: + return -EINVAL; + } + case hwmon_pwm_input: + if (val < 0 || val > 255) + return -EINVAL; + + val = val >> 1; /* Max value is 128, scale from 255 */ + + return write_to_ec(AYN_SENSOR_PWM_FAN_SET_REG, val); + default: + return -EOPNOTSUPP; + } +} + +static const struct hwmon_channel_info *ayn_ec_sensors[] = { + HWMON_CHANNEL_INFO(fan, HWMON_F_INPUT), + HWMON_CHANNEL_INFO(pwm, HWMON_PWM_INPUT | HWMON_PWM_ENABLE), + NULL, +}; + +static const struct hwmon_ops ayn_ec_hwmon_ops = { + .is_visible = ayn_ec_hwmon_is_visible, + .read = ayn_pwm_fan_read, + .write = ayn_pwm_fan_write, +}; + +static const struct hwmon_chip_info ayn_ec_chip_info = { + .ops = &ayn_ec_hwmon_ops, + .info = ayn_ec_sensors, +}; + +/** + * pwm_curve_store() - Write a fan curve speed or temperature value. + * + * @dev: The attribute's parent device. + * @attr: The attribute to read. + * @buf: Input value string from sysfs write. + * + * Return: Number of bytes read, or an error. + */ +static ssize_t pwm_curve_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + int i = to_sensor_dev_attr(attr)->index; + int ret, val; + u8 reg; + + ret = kstrtoint(buf, 0, &val); + if (ret) + return ret; + + if (i < 5) { + if (val < 0 || val > 255) + return -EINVAL; + val = val >> 1; /* Max EC value is 128, scale from 255 */ + } else + if (val < 0 || val > 100) + return -EINVAL; + + reg = ayn_pwm_curve_registers[i]; + + ret = write_to_ec(reg, val); + if (ret) + return ret; + + return count; +} + +/** + * pwm_curve_show() - Read a fan curve speed or temperature value. + * + * @dev: The attribute's parent device. + * @attr: The attribute to read. + * @buf: Output buffer. + * + * Return: Number of bytes read, or an error. + */ +static ssize_t pwm_curve_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + int i = to_sensor_dev_attr(attr)->index; + long val; + int ret; + u8 reg; + + reg = ayn_pwm_curve_registers[i]; + + ret = read_from_ec(reg, 1, &val); + if (ret) + return ret; + + if (i < 5) + val = val << 1; /* Max EC value is 128, scale to 255 */ + + return sysfs_emit(buf, "%ld\n", val); +} + +/* Fan curve attributes */ +static SENSOR_DEVICE_ATTR_RW(pwm1_auto_point1_pwm, pwm_curve, 0); +static SENSOR_DEVICE_ATTR_RW(pwm1_auto_point2_pwm, pwm_curve, 1); +static SENSOR_DEVICE_ATTR_RW(pwm1_auto_point3_pwm, pwm_curve, 2); +static SENSOR_DEVICE_ATTR_RW(pwm1_auto_point4_pwm, pwm_curve, 3); +static SENSOR_DEVICE_ATTR_RW(pwm1_auto_point5_pwm, pwm_curve, 4); +static SENSOR_DEVICE_ATTR_RW(pwm1_auto_point1_temp, pwm_curve, 5); +static SENSOR_DEVICE_ATTR_RW(pwm1_auto_point2_temp, pwm_curve, 6); +static SENSOR_DEVICE_ATTR_RW(pwm1_auto_point3_temp, pwm_curve, 7); +static SENSOR_DEVICE_ATTR_RW(pwm1_auto_point4_temp, pwm_curve, 8); +static SENSOR_DEVICE_ATTR_RW(pwm1_auto_point5_temp, pwm_curve, 9); + +/** + * thermal_sensor_show() - Read a thermal sensor attribute value. + * + * @dev: The attribute's parent device. + * @attr: The attribute to read. + * @buf: Buffer to write the result into. + * + * Return: Number of bytes read, or an error. + */ +static ssize_t thermal_sensor_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + long ret, val; + int i; + + i = to_sensor_dev_attr(attr)->index; + + ret = read_from_ec(thermal_sensors[i].reg, 1, &val); + if (ret) + return ret; + + val = val * 1000L; + + return sysfs_emit(buf, "%ld\n", val); +} + +/** + * thermal_sensor_label_show() - Read a thermal sensor attribute label. + * + * @dev: The attribute's parent device. + * @attr: The attribute to read. + * @buf: Buffer to read to. + * + * Return: Number of bytes read, or an error. + */ +static ssize_t thermal_sensor_label_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int i = to_sensor_dev_attr(attr)->index; + + return sysfs_emit(buf, "%s\n", thermal_sensors[i].name); +} + +static SENSOR_DEVICE_ATTR_RO(temp1_input, thermal_sensor, 0); +static SENSOR_DEVICE_ATTR_RO(temp2_input, thermal_sensor, 1); +static SENSOR_DEVICE_ATTR_RO(temp3_input, thermal_sensor, 2); +static SENSOR_DEVICE_ATTR_RO(temp4_input, thermal_sensor, 3); +static SENSOR_DEVICE_ATTR_RO(temp5_input, thermal_sensor, 4); +static SENSOR_DEVICE_ATTR_RO(temp1_label, thermal_sensor_label, 0); +static SENSOR_DEVICE_ATTR_RO(temp2_label, thermal_sensor_label, 1); +static SENSOR_DEVICE_ATTR_RO(temp3_label, thermal_sensor_label, 2); +static SENSOR_DEVICE_ATTR_RO(temp4_label, thermal_sensor_label, 3); +static SENSOR_DEVICE_ATTR_RO(temp5_label, thermal_sensor_label, 4); + +static struct attribute *ayn_sensors_attrs[] = { + &sensor_dev_attr_pwm1_auto_point1_pwm.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point1_temp.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point2_pwm.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point2_temp.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point3_pwm.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point3_temp.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point4_pwm.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point4_temp.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point5_pwm.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point5_temp.dev_attr.attr, + &sensor_dev_attr_temp1_input.dev_attr.attr, + &sensor_dev_attr_temp1_label.dev_attr.attr, + &sensor_dev_attr_temp2_input.dev_attr.attr, + &sensor_dev_attr_temp2_label.dev_attr.attr, + &sensor_dev_attr_temp3_input.dev_attr.attr, + &sensor_dev_attr_temp3_label.dev_attr.attr, + &sensor_dev_attr_temp4_input.dev_attr.attr, + &sensor_dev_attr_temp4_label.dev_attr.attr, + &sensor_dev_attr_temp5_input.dev_attr.attr, + &sensor_dev_attr_temp5_label.dev_attr.attr, + NULL, +}; + +ATTRIBUTE_GROUPS(ayn_sensors); + +/** + * rgb_effect_write() - Set the RGB effect stored in drvdata.rgb_effect. + */ +static int rgb_effect_write(void) +{ + return write_to_ec(AYN_RGB_EFFECT_REG, drvdata.rgb_effect); +}; + +/** + * rgb_effect_read() - Read the RGB effect and store it in drvdata.rgb_effect. + */ +static int rgb_effect_read(void) +{ + int ret; + long effect; + + ret = read_from_ec(AYN_RGB_EFFECT_REG, 1, &effect); + if (ret) + return ret; + + switch (effect) { + case AYN_RGB_EFFECT_WRITE: + case AYN_RGB_EFFECT_MONOCOLOR: + drvdata.rgb_effect = AYN_RGB_EFFECT_WRITE; + break; + default: + drvdata.rgb_effect = AYN_RGB_EFFECT_BREATHE; + } + + return 0; +} + +/** + * rgb_effect_store() - Store the given RGB effect and set it. + * + * @dev: parent device of the given attribute. + * @attr: The attribute to write to. + * @buf: Input value string from sysfs write. + * @count: The number of bytes written. + * + * Return: The number of bytes written, or an error. + */ +static ssize_t rgb_effect_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + int ret; + + ret = sysfs_match_string(RGB_EFFECT_TEXT, buf); + if (ret < 0) + return ret; + + if (ret) + drvdata.rgb_effect = AYN_RGB_EFFECT_WRITE; + else + drvdata.rgb_effect = AYN_RGB_EFFECT_BREATHE; + + ret = rgb_effect_write(); + if (ret) + return ret; + + return count; +}; + +/** + * rgb_effect_show() - Read the current RGB effect. + * + * @dev: parent device of the given attribute. + * @attr: The attribute to read. + * @buf: Buffer to read to. + * + * Return: The number of bytes read, or an error. + */ +static ssize_t rgb_effect_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + int ret, i; + + ret = rgb_effect_read(); + if (ret) + return ret; + + switch (drvdata.rgb_effect) { + case AYN_RGB_EFFECT_WRITE: + case AYN_RGB_EFFECT_MONOCOLOR: + i = MONOCOLOR; + break; + default: + i = BREATHE; + break; + } + + return sysfs_emit(buf, "%s\n", RGB_EFFECT_TEXT[i]); +}; + +static DEVICE_ATTR_RW_NAMED(rgb_effect, "effect"); + +/** + * rgb_effect_show() - Display the RGB effects available. + * + * @dev: parent device of the given attribute. + * @attr: The attribute to read. + * @buf: Buffer to read to. + * + * Return: The number of bytes read, or an error. + */ +static ssize_t rgb_effect_index_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + size_t count = 0; + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(RGB_EFFECT_TEXT); i++) + count += sysfs_emit_at(buf, count, "%s ", RGB_EFFECT_TEXT[i]); + + buf[count - 1] = '\n'; + + return count; +} + +static DEVICE_ATTR_RO_NAMED(rgb_effect_index, "effect_index"); + +/** + * ayn_led_mc_brightness_set() - Write the brightness for the RGB LED. + * + * @led_cdev: Parent LED device for the led_classdev_mc. + * @brightness: Brightness value to write [0-255]. + */ +static void ayn_led_mc_brightness_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct led_classdev_mc *led_cdev_mc = lcdev_to_mccdev(led_cdev); + struct mc_subled s_led; + int i, ret, val; + + switch (drvdata.rgb_effect) { + case AYN_RGB_EFFECT_WRITE: + case AYN_RGB_EFFECT_MONOCOLOR: + break; + case AYN_RGB_EFFECT_BREATHE: + return; + } + + led_cdev->brightness = brightness; + for (i = 0; i < led_cdev_mc->num_colors; i++) { + s_led = led_cdev_mc->subled_info[i]; + val = brightness * s_led.intensity / led_cdev->max_brightness; + ret = write_to_ec(s_led.channel, val); + if (ret) { + dev_err(led_cdev->dev, + "Error setting brightness: %d\n", ret); + return; + } + } + + /* Must write mode again to change to set color */ + write_to_ec(AYN_RGB_EFFECT_REG, AYN_RGB_EFFECT_WRITE); +}; + +/** + * ayn_led_mc_brightness_get() - Get the brightness for the RGB LED. + * + * @led_cdev: Parent LED device for the led_classdev_mc. + * + * Return: Current brightness. + */ +static enum led_brightness ayn_led_mc_brightness_get(struct led_classdev *led_cdev) +{ + return led_cdev->brightness; +}; + +static struct attribute *ayn_led_mc_attrs[] = { + &dev_attr_rgb_effect.attr, + &dev_attr_rgb_effect_index.attr, + NULL, +}; + +static struct attribute_group ayn_led_mc_group = { + .attrs = ayn_led_mc_attrs, +}; + +struct mc_subled ayn_led_mc_subled_info[] = { + { + .color_index = LED_COLOR_ID_RED, + .brightness = 0, + .intensity = 0, + .channel = AYN_LED_MC_RED_REG, + }, + { + .color_index = LED_COLOR_ID_GREEN, + .brightness = 0, + .intensity = 0, + .channel = AYN_LED_MC_GREEN_REG, + }, + { + .color_index = LED_COLOR_ID_BLUE, + .brightness = 0, + .intensity = 0, + .channel = AYN_LED_MC_BLUE_REG, + }, +}; + +struct led_classdev_mc ayn_led_mc = { + .led_cdev = { + .name = "ayn:rgb:joystick_rings", + .brightness = 0, + .max_brightness = 255, + .brightness_set = ayn_led_mc_brightness_set, + .brightness_get = ayn_led_mc_brightness_get, + .color = LED_COLOR_ID_RGB, + }, + .num_colors = ARRAY_SIZE(ayn_led_mc_subled_info), + .subled_info = ayn_led_mc_subled_info, +}; + +static int ayn_ec_resume(struct platform_device *pdev) +{ + struct led_classdev *led_cdev = drvdata.led_cdev; + int ret; + + ret = rgb_effect_write(); + if (ret) + return ret; + + ayn_led_mc_brightness_set(led_cdev, led_cdev->brightness); + + return 0; +} + +static int ayn_ec_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct device *hwdev; + int ret; + + ret = devm_led_classdev_multicolor_register(dev, &ayn_led_mc); + if (ret) + return ret; + + ret = devm_device_add_group(ayn_led_mc.led_cdev.dev, &ayn_led_mc_group); + if (ret) + return ret; + + drvdata.led_cdev = &ayn_led_mc.led_cdev; + ret = rgb_effect_read(); + if (ret) + return ret; + + hwdev = devm_hwmon_device_register_with_info(dev, "aynec", NULL, + &ayn_ec_chip_info, + ayn_sensors_groups); + return PTR_ERR_OR_ZERO(hwdev); +} + +static struct platform_driver ayn_ec_driver = { + .driver = { + .name = "ayn-ec", + }, + .probe = ayn_ec_probe, + .resume = ayn_ec_resume, +}; + +static struct platform_device *ayn_ec_device; + +static int __init ayn_ec_init(void) +{ + ayn_ec_device = platform_create_bundle(&ayn_ec_driver, ayn_ec_probe, + NULL, 0, NULL, 0); + + return PTR_ERR_OR_ZERO(ayn_ec_device); +} + +static void __exit ayn_ec_exit(void) +{ + platform_device_unregister(ayn_ec_device); + platform_driver_unregister(&ayn_ec_driver); +} + +static const struct dmi_system_id ayn_dmi_table[] = { + { + .ident = "AYN Loki Max", + .matches = { + DMI_EXACT_MATCH(DMI_BOARD_VENDOR, "ayn"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "Loki Max"), + }, + }, + { + .ident = "AYN Loki MiniPro", + .matches = { + DMI_EXACT_MATCH(DMI_BOARD_VENDOR, "ayn"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "Loki MiniPro"), + }, + }, + { + .ident = "AYN Loki Zero", + .matches = { + DMI_EXACT_MATCH(DMI_BOARD_VENDOR, "ayn"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "Loki Zero"), + }, + }, + { + .ident = "Tectoy Zeenix Lite", + .matches = { + DMI_EXACT_MATCH(DMI_BOARD_VENDOR, "Tectoy"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "Zeenix Lite"), + }, + }, + {}, +}; + +MODULE_DEVICE_TABLE(dmi, ayn_dmi_table); + +module_init(ayn_ec_init); +module_exit(ayn_ec_exit); + +MODULE_AUTHOR("Derek J. Clark "); +MODULE_DESCRIPTION("Platform driver that handles EC sensors of AYN x86 devices"); +MODULE_LICENSE("GPL");