| // SPDX-License-Identifier: GPL-2.0-or-later | |
| /* | |
| * Alienware special feature control | |
| * | |
| * Copyright (C) 2014 Dell Inc <Dell.Client.Kernel@dell.com> | |
| * Copyright (C) 2025 Kurt Borja <kuurtb@gmail.com> | |
| */ | |
| #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt | |
| #include <linux/acpi.h> | |
| #include <linux/cleanup.h> | |
| #include <linux/module.h> | |
| #include <linux/platform_device.h> | |
| #include <linux/slab.h> | |
| #include <linux/types.h> | |
| #include <linux/dmi.h> | |
| #include <linux/leds.h> | |
| #include <asm/byteorder.h> | |
| #include "alienware-wmi.h" | |
| MODULE_AUTHOR("Mario Limonciello <mario.limonciello@outlook.com>"); | |
| MODULE_AUTHOR("Kurt Borja <kuurtb@gmail.com>"); | |
| MODULE_DESCRIPTION("Alienware special feature control"); | |
| MODULE_LICENSE("GPL"); | |
| struct alienfx_quirks *alienfx; | |
| static struct alienfx_quirks quirk_inspiron5675 = { | |
| .num_zones = 2, | |
| .hdmi_mux = false, | |
| .amplifier = false, | |
| .deepslp = false, | |
| }; | |
| static struct alienfx_quirks quirk_unknown = { | |
| .num_zones = 2, | |
| .hdmi_mux = false, | |
| .amplifier = false, | |
| .deepslp = false, | |
| }; | |
| static struct alienfx_quirks quirk_x51_r1_r2 = { | |
| .num_zones = 3, | |
| .hdmi_mux = false, | |
| .amplifier = false, | |
| .deepslp = false, | |
| }; | |
| static struct alienfx_quirks quirk_x51_r3 = { | |
| .num_zones = 4, | |
| .hdmi_mux = false, | |
| .amplifier = true, | |
| .deepslp = false, | |
| }; | |
| static struct alienfx_quirks quirk_asm100 = { | |
| .num_zones = 2, | |
| .hdmi_mux = true, | |
| .amplifier = false, | |
| .deepslp = false, | |
| }; | |
| static struct alienfx_quirks quirk_asm200 = { | |
| .num_zones = 2, | |
| .hdmi_mux = true, | |
| .amplifier = false, | |
| .deepslp = true, | |
| }; | |
| static struct alienfx_quirks quirk_asm201 = { | |
| .num_zones = 2, | |
| .hdmi_mux = true, | |
| .amplifier = true, | |
| .deepslp = true, | |
| }; | |
| static int __init dmi_matched(const struct dmi_system_id *dmi) | |
| { | |
| alienfx = dmi->driver_data; | |
| return 1; | |
| } | |
| static const struct dmi_system_id alienware_quirks[] __initconst = { | |
| { | |
| .callback = dmi_matched, | |
| .ident = "Alienware ASM100", | |
| .matches = { | |
| DMI_MATCH(DMI_SYS_VENDOR, "Alienware"), | |
| DMI_MATCH(DMI_PRODUCT_NAME, "ASM100"), | |
| }, | |
| .driver_data = &quirk_asm100, | |
| }, | |
| { | |
| .callback = dmi_matched, | |
| .ident = "Alienware ASM200", | |
| .matches = { | |
| DMI_MATCH(DMI_SYS_VENDOR, "Alienware"), | |
| DMI_MATCH(DMI_PRODUCT_NAME, "ASM200"), | |
| }, | |
| .driver_data = &quirk_asm200, | |
| }, | |
| { | |
| .callback = dmi_matched, | |
| .ident = "Alienware ASM201", | |
| .matches = { | |
| DMI_MATCH(DMI_SYS_VENDOR, "Alienware"), | |
| DMI_MATCH(DMI_PRODUCT_NAME, "ASM201"), | |
| }, | |
| .driver_data = &quirk_asm201, | |
| }, | |
| { | |
| .callback = dmi_matched, | |
| .ident = "Alienware X51 R1", | |
| .matches = { | |
| DMI_MATCH(DMI_SYS_VENDOR, "Alienware"), | |
| DMI_MATCH(DMI_PRODUCT_NAME, "Alienware X51"), | |
| }, | |
| .driver_data = &quirk_x51_r1_r2, | |
| }, | |
| { | |
| .callback = dmi_matched, | |
| .ident = "Alienware X51 R2", | |
| .matches = { | |
| DMI_MATCH(DMI_SYS_VENDOR, "Alienware"), | |
| DMI_MATCH(DMI_PRODUCT_NAME, "Alienware X51 R2"), | |
| }, | |
| .driver_data = &quirk_x51_r1_r2, | |
| }, | |
| { | |
| .callback = dmi_matched, | |
| .ident = "Alienware X51 R3", | |
| .matches = { | |
| DMI_MATCH(DMI_SYS_VENDOR, "Alienware"), | |
| DMI_MATCH(DMI_PRODUCT_NAME, "Alienware X51 R3"), | |
| }, | |
| .driver_data = &quirk_x51_r3, | |
| }, | |
| { | |
| .callback = dmi_matched, | |
| .ident = "Dell Inc. Inspiron 5675", | |
| .matches = { | |
| DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), | |
| DMI_MATCH(DMI_PRODUCT_NAME, "Inspiron 5675"), | |
| }, | |
| .driver_data = &quirk_inspiron5675, | |
| }, | |
| {} | |
| }; | |
| u8 alienware_interface; | |
| int alienware_wmi_command(struct wmi_device *wdev, u32 method_id, | |
| void *in_args, size_t in_size, u32 *out_data) | |
| { | |
| struct wmi_buffer out, in = { | |
| .data = in_args, | |
| .length = in_size, | |
| }; | |
| int ret; | |
| if (!out_data) | |
| return wmidev_invoke_procedure(wdev, 0, method_id, &in); | |
| ret = wmidev_invoke_method(wdev, 0, method_id, &in, &out, | |
| sizeof(*out_data)); | |
| if (ret) | |
| return ret; | |
| __le32 *data __free(kfree) = out.data; | |
| *out_data = le32_to_cpu(*data); | |
| return 0; | |
| } | |
| /* | |
| * Helpers used for zone control | |
| */ | |
| static int parse_rgb(const char *buf, struct color_platform *colors) | |
| { | |
| long unsigned int rgb; | |
| int ret; | |
| union color_union { | |
| struct color_platform cp; | |
| int package; | |
| } repackager; | |
| ret = kstrtoul(buf, 16, &rgb); | |
| if (ret) | |
| return ret; | |
| /* RGB triplet notation is 24-bit hexadecimal */ | |
| if (rgb > 0xFFFFFF) | |
| return -EINVAL; | |
| repackager.package = rgb & 0x0f0f0f0f; | |
| pr_debug("alienware-wmi: r: %d g:%d b: %d\n", | |
| repackager.cp.red, repackager.cp.green, repackager.cp.blue); | |
| *colors = repackager.cp; | |
| return 0; | |
| } | |
| /* | |
| * Individual RGB zone control | |
| */ | |
| static ssize_t zone_show(struct device *dev, struct device_attribute *attr, | |
| char *buf, u8 location) | |
| { | |
| struct alienfx_priv *priv = dev_get_drvdata(dev); | |
| struct color_platform *colors = &priv->colors[location]; | |
| return sprintf(buf, "red: %d, green: %d, blue: %d\n", | |
| colors->red, colors->green, colors->blue); | |
| } | |
| static ssize_t zone_store(struct device *dev, struct device_attribute *attr, | |
| const char *buf, size_t count, u8 location) | |
| { | |
| struct alienfx_priv *priv = dev_get_drvdata(dev); | |
| struct color_platform *colors = &priv->colors[location]; | |
| struct alienfx_platdata *pdata = dev_get_platdata(dev); | |
| int ret; | |
| ret = parse_rgb(buf, colors); | |
| if (ret) | |
| return ret; | |
| ret = pdata->ops.upd_led(priv, pdata->wdev, location); | |
| return ret ? ret : count; | |
| } | |
| static ssize_t zone00_show(struct device *dev, struct device_attribute *attr, | |
| char *buf) | |
| { | |
| return zone_show(dev, attr, buf, 0); | |
| } | |
| static ssize_t zone00_store(struct device *dev, struct device_attribute *attr, | |
| const char *buf, size_t count) | |
| { | |
| return zone_store(dev, attr, buf, count, 0); | |
| } | |
| static DEVICE_ATTR_RW(zone00); | |
| static ssize_t zone01_show(struct device *dev, struct device_attribute *attr, | |
| char *buf) | |
| { | |
| return zone_show(dev, attr, buf, 1); | |
| } | |
| static ssize_t zone01_store(struct device *dev, struct device_attribute *attr, | |
| const char *buf, size_t count) | |
| { | |
| return zone_store(dev, attr, buf, count, 1); | |
| } | |
| static DEVICE_ATTR_RW(zone01); | |
| static ssize_t zone02_show(struct device *dev, struct device_attribute *attr, | |
| char *buf) | |
| { | |
| return zone_show(dev, attr, buf, 2); | |
| } | |
| static ssize_t zone02_store(struct device *dev, struct device_attribute *attr, | |
| const char *buf, size_t count) | |
| { | |
| return zone_store(dev, attr, buf, count, 2); | |
| } | |
| static DEVICE_ATTR_RW(zone02); | |
| static ssize_t zone03_show(struct device *dev, struct device_attribute *attr, | |
| char *buf) | |
| { | |
| return zone_show(dev, attr, buf, 3); | |
| } | |
| static ssize_t zone03_store(struct device *dev, struct device_attribute *attr, | |
| const char *buf, size_t count) | |
| { | |
| return zone_store(dev, attr, buf, count, 3); | |
| } | |
| static DEVICE_ATTR_RW(zone03); | |
| /* | |
| * Lighting control state device attribute (Global) | |
| */ | |
| static ssize_t lighting_control_state_show(struct device *dev, | |
| struct device_attribute *attr, | |
| char *buf) | |
| { | |
| struct alienfx_priv *priv = dev_get_drvdata(dev); | |
| if (priv->lighting_control_state == LEGACY_BOOTING) | |
| return sysfs_emit(buf, "[booting] running suspend\n"); | |
| else if (priv->lighting_control_state == LEGACY_SUSPEND) | |
| return sysfs_emit(buf, "booting running [suspend]\n"); | |
| return sysfs_emit(buf, "booting [running] suspend\n"); | |
| } | |
| static ssize_t lighting_control_state_store(struct device *dev, | |
| struct device_attribute *attr, | |
| const char *buf, size_t count) | |
| { | |
| struct alienfx_priv *priv = dev_get_drvdata(dev); | |
| u8 val; | |
| if (strcmp(buf, "booting\n") == 0) | |
| val = LEGACY_BOOTING; | |
| else if (strcmp(buf, "suspend\n") == 0) | |
| val = LEGACY_SUSPEND; | |
| else if (alienware_interface == LEGACY) | |
| val = LEGACY_RUNNING; | |
| else | |
| val = WMAX_RUNNING; | |
| priv->lighting_control_state = val; | |
| pr_debug("alienware-wmi: updated control state to %d\n", | |
| priv->lighting_control_state); | |
| return count; | |
| } | |
| static DEVICE_ATTR_RW(lighting_control_state); | |
| static umode_t zone_attr_visible(struct kobject *kobj, | |
| struct attribute *attr, int n) | |
| { | |
| if (n < alienfx->num_zones + 1) | |
| return attr->mode; | |
| return 0; | |
| } | |
| static bool zone_group_visible(struct kobject *kobj) | |
| { | |
| return alienfx->num_zones > 0; | |
| } | |
| DEFINE_SYSFS_GROUP_VISIBLE(zone); | |
| static struct attribute *zone_attrs[] = { | |
| &dev_attr_lighting_control_state.attr, | |
| &dev_attr_zone00.attr, | |
| &dev_attr_zone01.attr, | |
| &dev_attr_zone02.attr, | |
| &dev_attr_zone03.attr, | |
| NULL | |
| }; | |
| static struct attribute_group zone_attribute_group = { | |
| .name = "rgb_zones", | |
| .is_visible = SYSFS_GROUP_VISIBLE(zone), | |
| .attrs = zone_attrs, | |
| }; | |
| /* | |
| * LED Brightness (Global) | |
| */ | |
| static void global_led_set(struct led_classdev *led_cdev, | |
| enum led_brightness brightness) | |
| { | |
| struct alienfx_priv *priv = container_of(led_cdev, struct alienfx_priv, | |
| global_led); | |
| struct alienfx_platdata *pdata = dev_get_platdata(&priv->pdev->dev); | |
| int ret; | |
| priv->global_brightness = brightness; | |
| ret = pdata->ops.upd_brightness(priv, pdata->wdev, brightness); | |
| if (ret) | |
| pr_err("LED brightness update failed\n"); | |
| } | |
| static enum led_brightness global_led_get(struct led_classdev *led_cdev) | |
| { | |
| struct alienfx_priv *priv = container_of(led_cdev, struct alienfx_priv, | |
| global_led); | |
| return priv->global_brightness; | |
| } | |
| /* | |
| * Platform Driver | |
| */ | |
| static int alienfx_probe(struct platform_device *pdev) | |
| { | |
| struct alienfx_priv *priv; | |
| priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); | |
| if (!priv) | |
| return -ENOMEM; | |
| if (alienware_interface == WMAX) | |
| priv->lighting_control_state = WMAX_RUNNING; | |
| else | |
| priv->lighting_control_state = LEGACY_RUNNING; | |
| priv->pdev = pdev; | |
| priv->global_led.name = "alienware::global_brightness"; | |
| priv->global_led.brightness_set = global_led_set; | |
| priv->global_led.brightness_get = global_led_get; | |
| priv->global_led.max_brightness = 0x0F; | |
| priv->global_brightness = priv->global_led.max_brightness; | |
| platform_set_drvdata(pdev, priv); | |
| return devm_led_classdev_register(&pdev->dev, &priv->global_led); | |
| } | |
| static const struct attribute_group *alienfx_groups[] = { | |
| &zone_attribute_group, | |
| WMAX_DEV_GROUPS | |
| NULL | |
| }; | |
| static struct platform_driver platform_driver = { | |
| .driver = { | |
| .name = "alienware-wmi", | |
| .dev_groups = alienfx_groups, | |
| }, | |
| .probe = alienfx_probe, | |
| }; | |
| static void alienware_alienfx_remove(void *data) | |
| { | |
| struct platform_device *pdev = data; | |
| platform_device_unregister(pdev); | |
| } | |
| int alienware_alienfx_setup(struct alienfx_platdata *pdata) | |
| { | |
| struct device *dev = &pdata->wdev->dev; | |
| struct platform_device *pdev; | |
| int ret; | |
| pdev = platform_device_register_data(NULL, "alienware-wmi", | |
| PLATFORM_DEVID_NONE, pdata, | |
| sizeof(*pdata)); | |
| if (IS_ERR(pdev)) | |
| return PTR_ERR(pdev); | |
| dev_set_drvdata(dev, pdev); | |
| ret = devm_add_action_or_reset(dev, alienware_alienfx_remove, pdev); | |
| if (ret) | |
| return ret; | |
| return 0; | |
| } | |
| static int __init alienware_wmi_init(void) | |
| { | |
| int ret; | |
| dmi_check_system(alienware_quirks); | |
| if (!alienfx) | |
| alienfx = &quirk_unknown; | |
| ret = platform_driver_register(&platform_driver); | |
| if (ret < 0) | |
| return ret; | |
| if (wmi_has_guid(WMAX_CONTROL_GUID)) { | |
| alienware_interface = WMAX; | |
| ret = alienware_wmax_wmi_init(); | |
| } else { | |
| alienware_interface = LEGACY; | |
| ret = alienware_legacy_wmi_init(); | |
| } | |
| if (ret < 0) | |
| platform_driver_unregister(&platform_driver); | |
| return ret; | |
| } | |
| module_init(alienware_wmi_init); | |
| static void __exit alienware_wmi_exit(void) | |
| { | |
| if (alienware_interface == WMAX) | |
| alienware_wmax_wmi_exit(); | |
| else | |
| alienware_legacy_wmi_exit(); | |
| platform_driver_unregister(&platform_driver); | |
| } | |
| module_exit(alienware_wmi_exit); |