root/drivers/platform/x86/ayaneo-ec.c
// SPDX-License-Identifier: GPL-2.0+
/*
 * Platform driver for the Embedded Controller (EC) of Ayaneo devices. Handles
 * hwmon (fan speed, fan control), battery charge limits, and magic module
 * control (connected modules, controller disconnection).
 *
 * Copyright (C) 2025 Antheas Kapenekakis <lkml@antheas.dev>
 */

#include <linux/acpi.h>
#include <linux/bits.h>
#include <linux/dmi.h>
#include <linux/err.h>
#include <linux/hwmon.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/pm.h>
#include <linux/power_supply.h>
#include <linux/sysfs.h>
#include <acpi/battery.h>

#define AYANEO_PWM_ENABLE_REG    0x4A
#define AYANEO_PWM_REG           0x4B
#define AYANEO_PWM_MODE_AUTO     0x00
#define AYANEO_PWM_MODE_MANUAL   0x01

#define AYANEO_FAN_REG           0x76

#define EC_CHARGE_CONTROL_BEHAVIOURS                         \
        (BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO) |           \
         BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE))
#define AYANEO_CHARGE_REG               0x1e
#define AYANEO_CHARGE_VAL_AUTO          0xaa
#define AYANEO_CHARGE_VAL_INHIBIT       0x55

#define AYANEO_POWER_REG        0x2d
#define AYANEO_POWER_OFF        0xfe
#define AYANEO_POWER_ON         0xff
#define AYANEO_MODULE_REG       0x2f
#define AYANEO_MODULE_LEFT      BIT(0)
#define AYANEO_MODULE_RIGHT     BIT(1)
#define AYANEO_MODULE_MASK      (AYANEO_MODULE_LEFT | AYANEO_MODULE_RIGHT)

struct ayaneo_ec_quirk {
        bool has_fan_control;
        bool has_charge_control;
        bool has_magic_modules;
};

struct ayaneo_ec_platform_data {
        struct platform_device *pdev;
        struct ayaneo_ec_quirk *quirks;
        struct acpi_battery_hook battery_hook;

        // Protects access to restore_pwm
        struct mutex hwmon_lock;
        bool restore_charge_limit;
        bool restore_pwm;
};

static const struct ayaneo_ec_quirk quirk_fan = {
        .has_fan_control = true,
};

static const struct ayaneo_ec_quirk quirk_charge_limit = {
        .has_fan_control = true,
        .has_charge_control = true,
};

static const struct ayaneo_ec_quirk quirk_ayaneo3 = {
        .has_fan_control = true,
        .has_charge_control = true,
        .has_magic_modules = true,
};

static const struct dmi_system_id dmi_table[] = {
        {
                .matches = {
                        DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
                        DMI_MATCH(DMI_BOARD_NAME, "AYANEO 2"),
                },
                .driver_data = (void *)&quirk_fan,
        },
        {
                .matches = {
                        DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
                        DMI_MATCH(DMI_BOARD_NAME, "FLIP"),
                },
                .driver_data = (void *)&quirk_fan,
        },
        {
                .matches = {
                        DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
                        DMI_MATCH(DMI_BOARD_NAME, "GEEK"),
                },
                .driver_data = (void *)&quirk_fan,
        },
        {
                .matches = {
                        DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
                        DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR"),
                },
                .driver_data = (void *)&quirk_charge_limit,
        },
        {
                .matches = {
                        DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
                        DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR 1S"),
                },
                .driver_data = (void *)&quirk_charge_limit,
        },
        {
                .matches = {
                        DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
                        DMI_EXACT_MATCH(DMI_BOARD_NAME, "AB05-Mendocino"),
                },
                .driver_data = (void *)&quirk_charge_limit,
        },
        {
                .matches = {
                        DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
                        DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR Pro"),
                },
                .driver_data = (void *)&quirk_charge_limit,
        },
        {
                .matches = {
                        DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
                        DMI_EXACT_MATCH(DMI_BOARD_NAME, "KUN"),
                },
                .driver_data = (void *)&quirk_charge_limit,
        },
        {
                .matches = {
                        DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
                        DMI_EXACT_MATCH(DMI_BOARD_NAME, "AYANEO 3"),
                },
                .driver_data = (void *)&quirk_ayaneo3,
        },
        {},
};

/* Callbacks for hwmon interface */
static umode_t ayaneo_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;
        }
}

static int ayaneo_ec_read(struct device *dev, enum hwmon_sensor_types type,
                          u32 attr, int channel, long *val)
{
        u8 tmp;
        int ret;

        switch (type) {
        case hwmon_fan:
                switch (attr) {
                case hwmon_fan_input:
                        ret = ec_read(AYANEO_FAN_REG, &tmp);
                        if (ret)
                                return ret;
                        *val = tmp << 8;
                        ret = ec_read(AYANEO_FAN_REG + 1, &tmp);
                        if (ret)
                                return ret;
                        *val |= tmp;
                        return 0;
                default:
                        break;
                }
                break;
        case hwmon_pwm:
                switch (attr) {
                case hwmon_pwm_input:
                        ret = ec_read(AYANEO_PWM_REG, &tmp);
                        if (ret)
                                return ret;
                        if (tmp > 100)
                                return -EIO;
                        *val = (255 * tmp) / 100;
                        return 0;
                case hwmon_pwm_enable:
                        ret = ec_read(AYANEO_PWM_ENABLE_REG, &tmp);
                        if (ret)
                                return ret;
                        if (tmp == AYANEO_PWM_MODE_MANUAL)
                                *val = 1;
                        else if (tmp == AYANEO_PWM_MODE_AUTO)
                                *val = 2;
                        else
                                return -EIO;
                        return 0;
                default:
                        break;
                }
                break;
        default:
                break;
        }
        return -EOPNOTSUPP;
}

static int ayaneo_ec_write(struct device *dev, enum hwmon_sensor_types type,
                           u32 attr, int channel, long val)
{
        struct ayaneo_ec_platform_data *data = dev_get_drvdata(dev);
        int ret;

        guard(mutex)(&data->hwmon_lock);

        switch (type) {
        case hwmon_pwm:
                switch (attr) {
                case hwmon_pwm_enable:
                        data->restore_pwm = false;
                        switch (val) {
                        case 1:
                                return ec_write(AYANEO_PWM_ENABLE_REG,
                                                AYANEO_PWM_MODE_MANUAL);
                        case 2:
                                return ec_write(AYANEO_PWM_ENABLE_REG,
                                                AYANEO_PWM_MODE_AUTO);
                        default:
                                return -EINVAL;
                        }
                case hwmon_pwm_input:
                        if (val < 0 || val > 255)
                                return -EINVAL;
                        if (data->restore_pwm) {
                                /*
                                 * Defer restoring PWM control to after
                                 * userspace resumes successfully
                                 */
                                ret = ec_write(AYANEO_PWM_ENABLE_REG,
                                               AYANEO_PWM_MODE_MANUAL);
                                if (ret)
                                        return ret;
                                data->restore_pwm = false;
                        }
                        return ec_write(AYANEO_PWM_REG, (val * 100) / 255);
                default:
                        break;
                }
                break;
        default:
                break;
        }
        return -EOPNOTSUPP;
}

static const struct hwmon_ops ayaneo_ec_hwmon_ops = {
        .is_visible = ayaneo_ec_hwmon_is_visible,
        .read = ayaneo_ec_read,
        .write = ayaneo_ec_write,
};

static const struct hwmon_channel_info *const ayaneo_ec_sensors[] = {
        HWMON_CHANNEL_INFO(fan, HWMON_F_INPUT),
        HWMON_CHANNEL_INFO(pwm, HWMON_PWM_INPUT | HWMON_PWM_ENABLE),
        NULL,
};

static const struct hwmon_chip_info ayaneo_ec_chip_info = {
        .ops = &ayaneo_ec_hwmon_ops,
        .info = ayaneo_ec_sensors,
};

static int ayaneo_psy_ext_get_prop(struct power_supply *psy,
                                   const struct power_supply_ext *ext,
                                   void *data,
                                   enum power_supply_property psp,
                                   union power_supply_propval *val)
{
        int ret;
        u8 tmp;

        switch (psp) {
        case POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR:
                ret = ec_read(AYANEO_CHARGE_REG, &tmp);
                if (ret)
                        return ret;

                if (tmp == AYANEO_CHARGE_VAL_INHIBIT)
                        val->intval = POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE;
                else
                        val->intval = POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO;
                return 0;
        default:
                return -EINVAL;
        }
}

static int ayaneo_psy_ext_set_prop(struct power_supply *psy,
                                   const struct power_supply_ext *ext,
                                   void *data,
                                   enum power_supply_property psp,
                                   const union power_supply_propval *val)
{
        u8 raw_val;

        switch (psp) {
        case POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR:
                switch (val->intval) {
                case POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO:
                        raw_val = AYANEO_CHARGE_VAL_AUTO;
                        break;
                case POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE:
                        raw_val = AYANEO_CHARGE_VAL_INHIBIT;
                        break;
                default:
                        return -EINVAL;
                }
                return ec_write(AYANEO_CHARGE_REG, raw_val);
        default:
                return -EINVAL;
        }
}

static int ayaneo_psy_prop_is_writeable(struct power_supply *psy,
                                        const struct power_supply_ext *ext,
                                        void *data,
                                        enum power_supply_property psp)
{
        return true;
}

static const enum power_supply_property ayaneo_psy_ext_props[] = {
        POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR,
};

static const struct power_supply_ext ayaneo_psy_ext = {
        .name                   = "ayaneo-charge-control",
        .properties             = ayaneo_psy_ext_props,
        .num_properties         = ARRAY_SIZE(ayaneo_psy_ext_props),
        .charge_behaviours      = EC_CHARGE_CONTROL_BEHAVIOURS,
        .get_property           = ayaneo_psy_ext_get_prop,
        .set_property           = ayaneo_psy_ext_set_prop,
        .property_is_writeable  = ayaneo_psy_prop_is_writeable,
};

static int ayaneo_add_battery(struct power_supply *battery,
                              struct acpi_battery_hook *hook)
{
        struct ayaneo_ec_platform_data *data =
                container_of(hook, struct ayaneo_ec_platform_data, battery_hook);

        return power_supply_register_extension(battery, &ayaneo_psy_ext,
                                               &data->pdev->dev, NULL);
}

static int ayaneo_remove_battery(struct power_supply *battery,
                                 struct acpi_battery_hook *hook)
{
        power_supply_unregister_extension(battery, &ayaneo_psy_ext);
        return 0;
}

static ssize_t controller_power_store(struct device *dev,
                                      struct device_attribute *attr,
                                      const char *buf,
                                      size_t count)
{
        bool value;
        int ret;

        ret = kstrtobool(buf, &value);
        if (ret)
                return ret;

        ret = ec_write(AYANEO_POWER_REG, value ? AYANEO_POWER_ON : AYANEO_POWER_OFF);
        if (ret)
                return ret;

        return count;
}

static ssize_t controller_power_show(struct device *dev,
                                     struct device_attribute *attr,
                                     char *buf)
{
        int ret;
        u8 val;

        ret = ec_read(AYANEO_POWER_REG, &val);
        if (ret)
                return ret;

        return sysfs_emit(buf, "%d\n", val == AYANEO_POWER_ON);
}

static DEVICE_ATTR_RW(controller_power);

static ssize_t controller_modules_show(struct device *dev,
                                       struct device_attribute *attr, char *buf)
{
        u8 unconnected_modules;
        char *out;
        int ret;

        ret = ec_read(AYANEO_MODULE_REG, &unconnected_modules);
        if (ret)
                return ret;

        switch (~unconnected_modules & AYANEO_MODULE_MASK) {
        case AYANEO_MODULE_LEFT | AYANEO_MODULE_RIGHT:
                out = "both";
                break;
        case AYANEO_MODULE_LEFT:
                out = "left";
                break;
        case AYANEO_MODULE_RIGHT:
                out = "right";
                break;
        default:
                out = "none";
                break;
        }

        return sysfs_emit(buf, "%s\n", out);
}

static DEVICE_ATTR_RO(controller_modules);

static struct attribute *aya_mm_attrs[] = {
        &dev_attr_controller_power.attr,
        &dev_attr_controller_modules.attr,
        NULL
};

static umode_t aya_mm_is_visible(struct kobject *kobj,
                                 struct attribute *attr, int n)
{
        struct device *dev = kobj_to_dev(kobj);
        struct platform_device *pdev = to_platform_device(dev);
        struct ayaneo_ec_platform_data *data = platform_get_drvdata(pdev);

        if (data->quirks->has_magic_modules)
                return attr->mode;
        return 0;
}

static const struct attribute_group aya_mm_attribute_group = {
        .is_visible = aya_mm_is_visible,
        .attrs = aya_mm_attrs,
};

static const struct attribute_group *ayaneo_ec_groups[] = {
        &aya_mm_attribute_group,
        NULL
};

static int ayaneo_ec_probe(struct platform_device *pdev)
{
        const struct dmi_system_id *dmi_entry;
        struct ayaneo_ec_platform_data *data;
        struct device *hwdev;
        int ret;

        dmi_entry = dmi_first_match(dmi_table);
        if (!dmi_entry)
                return -ENODEV;

        data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
        if (!data)
                return -ENOMEM;

        data->pdev = pdev;
        data->quirks = dmi_entry->driver_data;
        ret = devm_mutex_init(&pdev->dev, &data->hwmon_lock);
        if (ret)
                return ret;
        platform_set_drvdata(pdev, data);

        if (data->quirks->has_fan_control) {
                hwdev = devm_hwmon_device_register_with_info(&pdev->dev,
                        "ayaneo_ec", data, &ayaneo_ec_chip_info, NULL);
                if (IS_ERR(hwdev))
                        return PTR_ERR(hwdev);
        }

        if (data->quirks->has_charge_control) {
                data->battery_hook.add_battery = ayaneo_add_battery;
                data->battery_hook.remove_battery = ayaneo_remove_battery;
                data->battery_hook.name = "Ayaneo Battery";
                ret = devm_battery_hook_register(&pdev->dev, &data->battery_hook);
                if (ret)
                        return ret;
        }

        return 0;
}

static int ayaneo_freeze(struct device *dev)
{
        struct platform_device *pdev = to_platform_device(dev);
        struct ayaneo_ec_platform_data *data = platform_get_drvdata(pdev);
        int ret;
        u8 tmp;

        if (data->quirks->has_charge_control) {
                ret = ec_read(AYANEO_CHARGE_REG, &tmp);
                if (ret)
                        return ret;

                data->restore_charge_limit = tmp == AYANEO_CHARGE_VAL_INHIBIT;
        }

        if (data->quirks->has_fan_control) {
                ret = ec_read(AYANEO_PWM_ENABLE_REG, &tmp);
                if (ret)
                        return ret;

                data->restore_pwm = tmp == AYANEO_PWM_MODE_MANUAL;

                /*
                 * Release the fan when entering hibernation to avoid
                 * overheating if hibernation fails and hangs.
                 */
                if (data->restore_pwm) {
                        ret = ec_write(AYANEO_PWM_ENABLE_REG, AYANEO_PWM_MODE_AUTO);
                        if (ret)
                                return ret;
                }
        }

        return 0;
}

static int ayaneo_restore(struct device *dev)
{
        struct platform_device *pdev = to_platform_device(dev);
        struct ayaneo_ec_platform_data *data = platform_get_drvdata(pdev);
        int ret;

        if (data->quirks->has_charge_control && data->restore_charge_limit) {
                ret = ec_write(AYANEO_CHARGE_REG, AYANEO_CHARGE_VAL_INHIBIT);
                if (ret)
                        return ret;
        }

        return 0;
}

static const struct dev_pm_ops ayaneo_pm_ops = {
        .freeze = ayaneo_freeze,
        .restore = ayaneo_restore,
};

static struct platform_driver ayaneo_platform_driver = {
        .driver = {
                .name = "ayaneo-ec",
                .dev_groups = ayaneo_ec_groups,
                .pm = pm_sleep_ptr(&ayaneo_pm_ops),
        },
        .probe = ayaneo_ec_probe,
};

static struct platform_device *ayaneo_platform_device;

static int __init ayaneo_ec_init(void)
{
        ayaneo_platform_device =
                platform_create_bundle(&ayaneo_platform_driver,
                                       ayaneo_ec_probe, NULL, 0, NULL, 0);

        return PTR_ERR_OR_ZERO(ayaneo_platform_device);
}

static void __exit ayaneo_ec_exit(void)
{
        platform_device_unregister(ayaneo_platform_device);
        platform_driver_unregister(&ayaneo_platform_driver);
}

MODULE_DEVICE_TABLE(dmi, dmi_table);

module_init(ayaneo_ec_init);
module_exit(ayaneo_ec_exit);

MODULE_AUTHOR("Antheas Kapenekakis <lkml@antheas.dev>");
MODULE_DESCRIPTION("Ayaneo Embedded Controller (EC) platform features");
MODULE_LICENSE("GPL");