linux/drivers/platform/x86/alienware-wmi.c
<<
>>
Prefs
   1/*
   2 * Alienware AlienFX control
   3 *
   4 * Copyright (C) 2014 Dell Inc <mario_limonciello@dell.com>
   5 *
   6 *  This program is free software; you can redistribute it and/or modify
   7 *  it under the terms of the GNU General Public License as published by
   8 *  the Free Software Foundation; either version 2 of the License, or
   9 *  (at your option) any later version.
  10 *
  11 *  This program is distributed in the hope that it will be useful,
  12 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
  13 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  14 *  GNU General Public License for more details.
  15 *
  16 */
  17
  18#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
  19
  20#include <linux/acpi.h>
  21#include <linux/module.h>
  22#include <linux/platform_device.h>
  23#include <linux/dmi.h>
  24#include <linux/acpi.h>
  25#include <linux/leds.h>
  26
  27#define LEGACY_CONTROL_GUID             "A90597CE-A997-11DA-B012-B622A1EF5492"
  28#define LEGACY_POWER_CONTROL_GUID       "A80593CE-A997-11DA-B012-B622A1EF5492"
  29#define WMAX_CONTROL_GUID               "A70591CE-A997-11DA-B012-B622A1EF5492"
  30
  31#define WMAX_METHOD_HDMI_SOURCE         0x1
  32#define WMAX_METHOD_HDMI_STATUS         0x2
  33#define WMAX_METHOD_BRIGHTNESS          0x3
  34#define WMAX_METHOD_ZONE_CONTROL        0x4
  35#define WMAX_METHOD_HDMI_CABLE          0x5
  36
  37MODULE_AUTHOR("Mario Limonciello <mario_limonciello@dell.com>");
  38MODULE_DESCRIPTION("Alienware special feature control");
  39MODULE_LICENSE("GPL");
  40MODULE_ALIAS("wmi:" LEGACY_CONTROL_GUID);
  41MODULE_ALIAS("wmi:" WMAX_CONTROL_GUID);
  42
  43enum INTERFACE_FLAGS {
  44        LEGACY,
  45        WMAX,
  46};
  47
  48enum LEGACY_CONTROL_STATES {
  49        LEGACY_RUNNING = 1,
  50        LEGACY_BOOTING = 0,
  51        LEGACY_SUSPEND = 3,
  52};
  53
  54enum WMAX_CONTROL_STATES {
  55        WMAX_RUNNING = 0xFF,
  56        WMAX_BOOTING = 0,
  57        WMAX_SUSPEND = 3,
  58};
  59
  60struct quirk_entry {
  61        u8 num_zones;
  62        u8 hdmi_mux;
  63};
  64
  65static struct quirk_entry *quirks;
  66
  67static struct quirk_entry quirk_unknown = {
  68        .num_zones = 2,
  69        .hdmi_mux = 0,
  70};
  71
  72static struct quirk_entry quirk_x51_family = {
  73        .num_zones = 3,
  74        .hdmi_mux = 0.
  75};
  76
  77static struct quirk_entry quirk_asm100 = {
  78        .num_zones = 2,
  79        .hdmi_mux = 1,
  80};
  81
  82static int __init dmi_matched(const struct dmi_system_id *dmi)
  83{
  84        quirks = dmi->driver_data;
  85        return 1;
  86}
  87
  88static const struct dmi_system_id alienware_quirks[] __initconst = {
  89        {
  90         .callback = dmi_matched,
  91         .ident = "Alienware X51 R1",
  92         .matches = {
  93                     DMI_MATCH(DMI_SYS_VENDOR, "Alienware"),
  94                     DMI_MATCH(DMI_PRODUCT_NAME, "Alienware X51"),
  95                     },
  96         .driver_data = &quirk_x51_family,
  97         },
  98        {
  99         .callback = dmi_matched,
 100         .ident = "Alienware X51 R2",
 101         .matches = {
 102                     DMI_MATCH(DMI_SYS_VENDOR, "Alienware"),
 103                     DMI_MATCH(DMI_PRODUCT_NAME, "Alienware X51 R2"),
 104                     },
 105         .driver_data = &quirk_x51_family,
 106         },
 107        {
 108                .callback = dmi_matched,
 109                .ident = "Alienware ASM100",
 110                .matches = {
 111                        DMI_MATCH(DMI_SYS_VENDOR, "Alienware"),
 112                        DMI_MATCH(DMI_PRODUCT_NAME, "ASM100"),
 113                },
 114                .driver_data = &quirk_asm100,
 115        },
 116        {}
 117};
 118
 119struct color_platform {
 120        u8 blue;
 121        u8 green;
 122        u8 red;
 123} __packed;
 124
 125struct platform_zone {
 126        u8 location;
 127        struct device_attribute *attr;
 128        struct color_platform colors;
 129};
 130
 131struct wmax_brightness_args {
 132        u32 led_mask;
 133        u32 percentage;
 134};
 135
 136struct hdmi_args {
 137        u8 arg;
 138};
 139
 140struct legacy_led_args {
 141        struct color_platform colors;
 142        u8 brightness;
 143        u8 state;
 144} __packed;
 145
 146struct wmax_led_args {
 147        u32 led_mask;
 148        struct color_platform colors;
 149        u8 state;
 150} __packed;
 151
 152static struct platform_device *platform_device;
 153static struct device_attribute *zone_dev_attrs;
 154static struct attribute **zone_attrs;
 155static struct platform_zone *zone_data;
 156
 157static struct platform_driver platform_driver = {
 158        .driver = {
 159                   .name = "alienware-wmi",
 160                   }
 161};
 162
 163static struct attribute_group zone_attribute_group = {
 164        .name = "rgb_zones",
 165};
 166
 167static u8 interface;
 168static u8 lighting_control_state;
 169static u8 global_brightness;
 170
 171/*
 172 * Helpers used for zone control
 173*/
 174static int parse_rgb(const char *buf, struct platform_zone *zone)
 175{
 176        long unsigned int rgb;
 177        int ret;
 178        union color_union {
 179                struct color_platform cp;
 180                int package;
 181        } repackager;
 182
 183        ret = kstrtoul(buf, 16, &rgb);
 184        if (ret)
 185                return ret;
 186
 187        /* RGB triplet notation is 24-bit hexadecimal */
 188        if (rgb > 0xFFFFFF)
 189                return -EINVAL;
 190
 191        repackager.package = rgb & 0x0f0f0f0f;
 192        pr_debug("alienware-wmi: r: %d g:%d b: %d\n",
 193                 repackager.cp.red, repackager.cp.green, repackager.cp.blue);
 194        zone->colors = repackager.cp;
 195        return 0;
 196}
 197
 198static struct platform_zone *match_zone(struct device_attribute *attr)
 199{
 200        int i;
 201        for (i = 0; i < quirks->num_zones; i++) {
 202                if ((struct device_attribute *)zone_data[i].attr == attr) {
 203                        pr_debug("alienware-wmi: matched zone location: %d\n",
 204                                 zone_data[i].location);
 205                        return &zone_data[i];
 206                }
 207        }
 208        return NULL;
 209}
 210
 211/*
 212 * Individual RGB zone control
 213*/
 214static int alienware_update_led(struct platform_zone *zone)
 215{
 216        int method_id;
 217        acpi_status status;
 218        char *guid;
 219        struct acpi_buffer input;
 220        struct legacy_led_args legacy_args;
 221        struct wmax_led_args wmax_args;
 222        if (interface == WMAX) {
 223                wmax_args.led_mask = 1 << zone->location;
 224                wmax_args.colors = zone->colors;
 225                wmax_args.state = lighting_control_state;
 226                guid = WMAX_CONTROL_GUID;
 227                method_id = WMAX_METHOD_ZONE_CONTROL;
 228
 229                input.length = (acpi_size) sizeof(wmax_args);
 230                input.pointer = &wmax_args;
 231        } else {
 232                legacy_args.colors = zone->colors;
 233                legacy_args.brightness = global_brightness;
 234                legacy_args.state = 0;
 235                if (lighting_control_state == LEGACY_BOOTING ||
 236                    lighting_control_state == LEGACY_SUSPEND) {
 237                        guid = LEGACY_POWER_CONTROL_GUID;
 238                        legacy_args.state = lighting_control_state;
 239                } else
 240                        guid = LEGACY_CONTROL_GUID;
 241                method_id = zone->location + 1;
 242
 243                input.length = (acpi_size) sizeof(legacy_args);
 244                input.pointer = &legacy_args;
 245        }
 246        pr_debug("alienware-wmi: guid %s method %d\n", guid, method_id);
 247
 248        status = wmi_evaluate_method(guid, 1, method_id, &input, NULL);
 249        if (ACPI_FAILURE(status))
 250                pr_err("alienware-wmi: zone set failure: %u\n", status);
 251        return ACPI_FAILURE(status);
 252}
 253
 254static ssize_t zone_show(struct device *dev, struct device_attribute *attr,
 255                         char *buf)
 256{
 257        struct platform_zone *target_zone;
 258        target_zone = match_zone(attr);
 259        if (target_zone == NULL)
 260                return sprintf(buf, "red: -1, green: -1, blue: -1\n");
 261        return sprintf(buf, "red: %d, green: %d, blue: %d\n",
 262                       target_zone->colors.red,
 263                       target_zone->colors.green, target_zone->colors.blue);
 264
 265}
 266
 267static ssize_t zone_set(struct device *dev, struct device_attribute *attr,
 268                        const char *buf, size_t count)
 269{
 270        struct platform_zone *target_zone;
 271        int ret;
 272        target_zone = match_zone(attr);
 273        if (target_zone == NULL) {
 274                pr_err("alienware-wmi: invalid target zone\n");
 275                return 1;
 276        }
 277        ret = parse_rgb(buf, target_zone);
 278        if (ret)
 279                return ret;
 280        ret = alienware_update_led(target_zone);
 281        return ret ? ret : count;
 282}
 283
 284/*
 285 * LED Brightness (Global)
 286*/
 287static int wmax_brightness(int brightness)
 288{
 289        acpi_status status;
 290        struct acpi_buffer input;
 291        struct wmax_brightness_args args = {
 292                .led_mask = 0xFF,
 293                .percentage = brightness,
 294        };
 295        input.length = (acpi_size) sizeof(args);
 296        input.pointer = &args;
 297        status = wmi_evaluate_method(WMAX_CONTROL_GUID, 1,
 298                                     WMAX_METHOD_BRIGHTNESS, &input, NULL);
 299        if (ACPI_FAILURE(status))
 300                pr_err("alienware-wmi: brightness set failure: %u\n", status);
 301        return ACPI_FAILURE(status);
 302}
 303
 304static void global_led_set(struct led_classdev *led_cdev,
 305                           enum led_brightness brightness)
 306{
 307        int ret;
 308        global_brightness = brightness;
 309        if (interface == WMAX)
 310                ret = wmax_brightness(brightness);
 311        else
 312                ret = alienware_update_led(&zone_data[0]);
 313        if (ret)
 314                pr_err("LED brightness update failed\n");
 315}
 316
 317static enum led_brightness global_led_get(struct led_classdev *led_cdev)
 318{
 319        return global_brightness;
 320}
 321
 322static struct led_classdev global_led = {
 323        .brightness_set = global_led_set,
 324        .brightness_get = global_led_get,
 325        .name = "alienware::global_brightness",
 326};
 327
 328/*
 329 * Lighting control state device attribute (Global)
 330*/
 331static ssize_t show_control_state(struct device *dev,
 332                                  struct device_attribute *attr, char *buf)
 333{
 334        if (lighting_control_state == LEGACY_BOOTING)
 335                return scnprintf(buf, PAGE_SIZE, "[booting] running suspend\n");
 336        else if (lighting_control_state == LEGACY_SUSPEND)
 337                return scnprintf(buf, PAGE_SIZE, "booting running [suspend]\n");
 338        return scnprintf(buf, PAGE_SIZE, "booting [running] suspend\n");
 339}
 340
 341static ssize_t store_control_state(struct device *dev,
 342                                   struct device_attribute *attr,
 343                                   const char *buf, size_t count)
 344{
 345        long unsigned int val;
 346        if (strcmp(buf, "booting\n") == 0)
 347                val = LEGACY_BOOTING;
 348        else if (strcmp(buf, "suspend\n") == 0)
 349                val = LEGACY_SUSPEND;
 350        else if (interface == LEGACY)
 351                val = LEGACY_RUNNING;
 352        else
 353                val = WMAX_RUNNING;
 354        lighting_control_state = val;
 355        pr_debug("alienware-wmi: updated control state to %d\n",
 356                 lighting_control_state);
 357        return count;
 358}
 359
 360static DEVICE_ATTR(lighting_control_state, 0644, show_control_state,
 361                   store_control_state);
 362
 363static int alienware_zone_init(struct platform_device *dev)
 364{
 365        int i;
 366        char buffer[10];
 367        char *name;
 368
 369        if (interface == WMAX) {
 370                lighting_control_state = WMAX_RUNNING;
 371        } else if (interface == LEGACY) {
 372                lighting_control_state = LEGACY_RUNNING;
 373        }
 374        global_led.max_brightness = 0x0F;
 375        global_brightness = global_led.max_brightness;
 376
 377        /*
 378         *      - zone_dev_attrs num_zones + 1 is for individual zones and then
 379         *        null terminated
 380         *      - zone_attrs num_zones + 2 is for all attrs in zone_dev_attrs +
 381         *        the lighting control + null terminated
 382         *      - zone_data num_zones is for the distinct zones
 383         */
 384        zone_dev_attrs =
 385            kzalloc(sizeof(struct device_attribute) * (quirks->num_zones + 1),
 386                    GFP_KERNEL);
 387        if (!zone_dev_attrs)
 388                return -ENOMEM;
 389
 390        zone_attrs =
 391            kzalloc(sizeof(struct attribute *) * (quirks->num_zones + 2),
 392                    GFP_KERNEL);
 393        if (!zone_attrs)
 394                return -ENOMEM;
 395
 396        zone_data =
 397            kzalloc(sizeof(struct platform_zone) * (quirks->num_zones),
 398                    GFP_KERNEL);
 399        if (!zone_data)
 400                return -ENOMEM;
 401
 402        for (i = 0; i < quirks->num_zones; i++) {
 403                sprintf(buffer, "zone%02X", i);
 404                name = kstrdup(buffer, GFP_KERNEL);
 405                if (name == NULL)
 406                        return 1;
 407                sysfs_attr_init(&zone_dev_attrs[i].attr);
 408                zone_dev_attrs[i].attr.name = name;
 409                zone_dev_attrs[i].attr.mode = 0644;
 410                zone_dev_attrs[i].show = zone_show;
 411                zone_dev_attrs[i].store = zone_set;
 412                zone_data[i].location = i;
 413                zone_attrs[i] = &zone_dev_attrs[i].attr;
 414                zone_data[i].attr = &zone_dev_attrs[i];
 415        }
 416        zone_attrs[quirks->num_zones] = &dev_attr_lighting_control_state.attr;
 417        zone_attribute_group.attrs = zone_attrs;
 418
 419        led_classdev_register(&dev->dev, &global_led);
 420
 421        return sysfs_create_group(&dev->dev.kobj, &zone_attribute_group);
 422}
 423
 424static void alienware_zone_exit(struct platform_device *dev)
 425{
 426        sysfs_remove_group(&dev->dev.kobj, &zone_attribute_group);
 427        led_classdev_unregister(&global_led);
 428        if (zone_dev_attrs) {
 429                int i;
 430                for (i = 0; i < quirks->num_zones; i++)
 431                        kfree(zone_dev_attrs[i].attr.name);
 432        }
 433        kfree(zone_dev_attrs);
 434        kfree(zone_data);
 435        kfree(zone_attrs);
 436}
 437
 438/*
 439        The HDMI mux sysfs node indicates the status of the HDMI input mux.
 440        It can toggle between standard system GPU output and HDMI input.
 441*/
 442static acpi_status alienware_hdmi_command(struct hdmi_args *in_args,
 443                                          u32 command, int *out_data)
 444{
 445        acpi_status status;
 446        union acpi_object *obj;
 447        struct acpi_buffer input;
 448        struct acpi_buffer output;
 449
 450        input.length = (acpi_size) sizeof(*in_args);
 451        input.pointer = in_args;
 452        if (out_data != NULL) {
 453                output.length = ACPI_ALLOCATE_BUFFER;
 454                output.pointer = NULL;
 455                status = wmi_evaluate_method(WMAX_CONTROL_GUID, 1,
 456                                             command, &input, &output);
 457        } else
 458                status = wmi_evaluate_method(WMAX_CONTROL_GUID, 1,
 459                                             command, &input, NULL);
 460
 461        if (ACPI_SUCCESS(status) && out_data != NULL) {
 462                obj = (union acpi_object *)output.pointer;
 463                if (obj && obj->type == ACPI_TYPE_INTEGER)
 464                        *out_data = (u32) obj->integer.value;
 465        }
 466        return status;
 467
 468}
 469
 470static ssize_t show_hdmi_cable(struct device *dev,
 471                               struct device_attribute *attr, char *buf)
 472{
 473        acpi_status status;
 474        u32 out_data;
 475        struct hdmi_args in_args = {
 476                .arg = 0,
 477        };
 478        status =
 479            alienware_hdmi_command(&in_args, WMAX_METHOD_HDMI_CABLE,
 480                                   (u32 *) &out_data);
 481        if (ACPI_SUCCESS(status)) {
 482                if (out_data == 0)
 483                        return scnprintf(buf, PAGE_SIZE,
 484                                         "[unconnected] connected unknown\n");
 485                else if (out_data == 1)
 486                        return scnprintf(buf, PAGE_SIZE,
 487                                         "unconnected [connected] unknown\n");
 488        }
 489        pr_err("alienware-wmi: unknown HDMI cable status: %d\n", status);
 490        return scnprintf(buf, PAGE_SIZE, "unconnected connected [unknown]\n");
 491}
 492
 493static ssize_t show_hdmi_source(struct device *dev,
 494                                struct device_attribute *attr, char *buf)
 495{
 496        acpi_status status;
 497        u32 out_data;
 498        struct hdmi_args in_args = {
 499                .arg = 0,
 500        };
 501        status =
 502            alienware_hdmi_command(&in_args, WMAX_METHOD_HDMI_STATUS,
 503                                   (u32 *) &out_data);
 504
 505        if (ACPI_SUCCESS(status)) {
 506                if (out_data == 1)
 507                        return scnprintf(buf, PAGE_SIZE,
 508                                         "[input] gpu unknown\n");
 509                else if (out_data == 2)
 510                        return scnprintf(buf, PAGE_SIZE,
 511                                         "input [gpu] unknown\n");
 512        }
 513        pr_err("alienware-wmi: unknown HDMI source status: %d\n", out_data);
 514        return scnprintf(buf, PAGE_SIZE, "input gpu [unknown]\n");
 515}
 516
 517static ssize_t toggle_hdmi_source(struct device *dev,
 518                                  struct device_attribute *attr,
 519                                  const char *buf, size_t count)
 520{
 521        acpi_status status;
 522        struct hdmi_args args;
 523        if (strcmp(buf, "gpu\n") == 0)
 524                args.arg = 1;
 525        else if (strcmp(buf, "input\n") == 0)
 526                args.arg = 2;
 527        else
 528                args.arg = 3;
 529        pr_debug("alienware-wmi: setting hdmi to %d : %s", args.arg, buf);
 530
 531        status = alienware_hdmi_command(&args, WMAX_METHOD_HDMI_SOURCE, NULL);
 532
 533        if (ACPI_FAILURE(status))
 534                pr_err("alienware-wmi: HDMI toggle failed: results: %u\n",
 535                       status);
 536        return count;
 537}
 538
 539static DEVICE_ATTR(cable, S_IRUGO, show_hdmi_cable, NULL);
 540static DEVICE_ATTR(source, S_IRUGO | S_IWUSR, show_hdmi_source,
 541                   toggle_hdmi_source);
 542
 543static struct attribute *hdmi_attrs[] = {
 544        &dev_attr_cable.attr,
 545        &dev_attr_source.attr,
 546        NULL,
 547};
 548
 549static struct attribute_group hdmi_attribute_group = {
 550        .name = "hdmi",
 551        .attrs = hdmi_attrs,
 552};
 553
 554static void remove_hdmi(struct platform_device *dev)
 555{
 556        if (quirks->hdmi_mux > 0)
 557                sysfs_remove_group(&dev->dev.kobj, &hdmi_attribute_group);
 558}
 559
 560static int create_hdmi(struct platform_device *dev)
 561{
 562        int ret;
 563
 564        ret = sysfs_create_group(&dev->dev.kobj, &hdmi_attribute_group);
 565        if (ret)
 566                goto error_create_hdmi;
 567        return 0;
 568
 569error_create_hdmi:
 570        remove_hdmi(dev);
 571        return ret;
 572}
 573
 574static int __init alienware_wmi_init(void)
 575{
 576        int ret;
 577
 578        if (wmi_has_guid(LEGACY_CONTROL_GUID))
 579                interface = LEGACY;
 580        else if (wmi_has_guid(WMAX_CONTROL_GUID))
 581                interface = WMAX;
 582        else {
 583                pr_warn("alienware-wmi: No known WMI GUID found\n");
 584                return -ENODEV;
 585        }
 586
 587        dmi_check_system(alienware_quirks);
 588        if (quirks == NULL)
 589                quirks = &quirk_unknown;
 590
 591        ret = platform_driver_register(&platform_driver);
 592        if (ret)
 593                goto fail_platform_driver;
 594        platform_device = platform_device_alloc("alienware-wmi", -1);
 595        if (!platform_device) {
 596                ret = -ENOMEM;
 597                goto fail_platform_device1;
 598        }
 599        ret = platform_device_add(platform_device);
 600        if (ret)
 601                goto fail_platform_device2;
 602
 603        if (quirks->hdmi_mux > 0) {
 604                ret = create_hdmi(platform_device);
 605                if (ret)
 606                        goto fail_prep_hdmi;
 607        }
 608
 609        ret = alienware_zone_init(platform_device);
 610        if (ret)
 611                goto fail_prep_zones;
 612
 613        return 0;
 614
 615fail_prep_zones:
 616        alienware_zone_exit(platform_device);
 617fail_prep_hdmi:
 618        platform_device_del(platform_device);
 619fail_platform_device2:
 620        platform_device_put(platform_device);
 621fail_platform_device1:
 622        platform_driver_unregister(&platform_driver);
 623fail_platform_driver:
 624        return ret;
 625}
 626
 627module_init(alienware_wmi_init);
 628
 629static void __exit alienware_wmi_exit(void)
 630{
 631        if (platform_device) {
 632                alienware_zone_exit(platform_device);
 633                remove_hdmi(platform_device);
 634                platform_device_unregister(platform_device);
 635                platform_driver_unregister(&platform_driver);
 636        }
 637}
 638
 639module_exit(alienware_wmi_exit);
 640