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 
37 MODULE_AUTHOR("Mario Limonciello <mario_limonciello@dell.com>");
38 MODULE_DESCRIPTION("Alienware special feature control");
39 MODULE_LICENSE("GPL");
40 MODULE_ALIAS("wmi:" LEGACY_CONTROL_GUID);
41 MODULE_ALIAS("wmi:" WMAX_CONTROL_GUID);
42 
43 enum INTERFACE_FLAGS {
44 	LEGACY,
45 	WMAX,
46 };
47 
48 enum LEGACY_CONTROL_STATES {
49 	LEGACY_RUNNING = 1,
50 	LEGACY_BOOTING = 0,
51 	LEGACY_SUSPEND = 3,
52 };
53 
54 enum WMAX_CONTROL_STATES {
55 	WMAX_RUNNING = 0xFF,
56 	WMAX_BOOTING = 0,
57 	WMAX_SUSPEND = 3,
58 };
59 
60 struct quirk_entry {
61 	u8 num_zones;
62 	u8 hdmi_mux;
63 };
64 
65 static struct quirk_entry *quirks;
66 
67 static struct quirk_entry quirk_unknown = {
68 	.num_zones = 2,
69 	.hdmi_mux = 0,
70 };
71 
72 static struct quirk_entry quirk_x51_family = {
73 	.num_zones = 3,
74 	.hdmi_mux = 0.
75 };
76 
77 static struct quirk_entry quirk_asm100 = {
78 	.num_zones = 2,
79 	.hdmi_mux = 1,
80 };
81 
dmi_matched(const struct dmi_system_id * dmi)82 static int __init dmi_matched(const struct dmi_system_id *dmi)
83 {
84 	quirks = dmi->driver_data;
85 	return 1;
86 }
87 
88 static 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 
119 struct color_platform {
120 	u8 blue;
121 	u8 green;
122 	u8 red;
123 } __packed;
124 
125 struct platform_zone {
126 	u8 location;
127 	struct device_attribute *attr;
128 	struct color_platform colors;
129 };
130 
131 struct wmax_brightness_args {
132 	u32 led_mask;
133 	u32 percentage;
134 };
135 
136 struct hdmi_args {
137 	u8 arg;
138 };
139 
140 struct legacy_led_args {
141 	struct color_platform colors;
142 	u8 brightness;
143 	u8 state;
144 } __packed;
145 
146 struct wmax_led_args {
147 	u32 led_mask;
148 	struct color_platform colors;
149 	u8 state;
150 } __packed;
151 
152 static struct platform_device *platform_device;
153 static struct device_attribute *zone_dev_attrs;
154 static struct attribute **zone_attrs;
155 static struct platform_zone *zone_data;
156 
157 static struct platform_driver platform_driver = {
158 	.driver = {
159 		   .name = "alienware-wmi",
160 		   }
161 };
162 
163 static struct attribute_group zone_attribute_group = {
164 	.name = "rgb_zones",
165 };
166 
167 static u8 interface;
168 static u8 lighting_control_state;
169 static u8 global_brightness;
170 
171 /*
172  * Helpers used for zone control
173 */
parse_rgb(const char * buf,struct platform_zone * zone)174 static 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 
match_zone(struct device_attribute * attr)198 static 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 */
alienware_update_led(struct platform_zone * zone)214 static 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 
zone_show(struct device * dev,struct device_attribute * attr,char * buf)254 static 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 
zone_set(struct device * dev,struct device_attribute * attr,const char * buf,size_t count)267 static 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 */
wmax_brightness(int brightness)287 static 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 
global_led_set(struct led_classdev * led_cdev,enum led_brightness brightness)304 static 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 
global_led_get(struct led_classdev * led_cdev)317 static enum led_brightness global_led_get(struct led_classdev *led_cdev)
318 {
319 	return global_brightness;
320 }
321 
322 static 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 */
show_control_state(struct device * dev,struct device_attribute * attr,char * buf)331 static 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 
store_control_state(struct device * dev,struct device_attribute * attr,const char * buf,size_t count)341 static 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 
360 static DEVICE_ATTR(lighting_control_state, 0644, show_control_state,
361 		   store_control_state);
362 
alienware_zone_init(struct platform_device * dev)363 static 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 
alienware_zone_exit(struct platform_device * dev)424 static 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 */
alienware_hdmi_command(struct hdmi_args * in_args,u32 command,int * out_data)442 static 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 
show_hdmi_cable(struct device * dev,struct device_attribute * attr,char * buf)470 static 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 
show_hdmi_source(struct device * dev,struct device_attribute * attr,char * buf)493 static 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 
toggle_hdmi_source(struct device * dev,struct device_attribute * attr,const char * buf,size_t count)517 static 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 
539 static DEVICE_ATTR(cable, S_IRUGO, show_hdmi_cable, NULL);
540 static DEVICE_ATTR(source, S_IRUGO | S_IWUSR, show_hdmi_source,
541 		   toggle_hdmi_source);
542 
543 static struct attribute *hdmi_attrs[] = {
544 	&dev_attr_cable.attr,
545 	&dev_attr_source.attr,
546 	NULL,
547 };
548 
549 static struct attribute_group hdmi_attribute_group = {
550 	.name = "hdmi",
551 	.attrs = hdmi_attrs,
552 };
553 
remove_hdmi(struct platform_device * dev)554 static 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 
create_hdmi(struct platform_device * dev)560 static 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 
569 error_create_hdmi:
570 	remove_hdmi(dev);
571 	return ret;
572 }
573 
alienware_wmi_init(void)574 static 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 
615 fail_prep_zones:
616 	alienware_zone_exit(platform_device);
617 fail_prep_hdmi:
618 	platform_device_del(platform_device);
619 fail_platform_device2:
620 	platform_device_put(platform_device);
621 fail_platform_device1:
622 	platform_driver_unregister(&platform_driver);
623 fail_platform_driver:
624 	return ret;
625 }
626 
627 module_init(alienware_wmi_init);
628 
alienware_wmi_exit(void)629 static 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 
639 module_exit(alienware_wmi_exit);
640