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