1fd13c862SJeremy Soller // SPDX-License-Identifier: GPL-2.0+
2fd13c862SJeremy Soller /*
3fd13c862SJeremy Soller  * System76 ACPI Driver
4fd13c862SJeremy Soller  *
5fd13c862SJeremy Soller  * Copyright (C) 2019 System76
6fd13c862SJeremy Soller  *
7fd13c862SJeremy Soller  * This program is free software; you can redistribute it and/or modify
8fd13c862SJeremy Soller  * it under the terms of the GNU General Public License version 2 as
9fd13c862SJeremy Soller  * published by the Free Software Foundation.
10fd13c862SJeremy Soller  */
11fd13c862SJeremy Soller 
12fd13c862SJeremy Soller #include <linux/acpi.h>
13fd13c862SJeremy Soller #include <linux/init.h>
14fd13c862SJeremy Soller #include <linux/kernel.h>
15fd13c862SJeremy Soller #include <linux/leds.h>
16fd13c862SJeremy Soller #include <linux/module.h>
17fd13c862SJeremy Soller #include <linux/pci_ids.h>
18fd13c862SJeremy Soller #include <linux/types.h>
19fd13c862SJeremy Soller 
20fd13c862SJeremy Soller struct system76_data {
21fd13c862SJeremy Soller 	struct acpi_device *acpi_dev;
22fd13c862SJeremy Soller 	struct led_classdev ap_led;
23fd13c862SJeremy Soller 	struct led_classdev kb_led;
24fd13c862SJeremy Soller 	enum led_brightness kb_brightness;
25fd13c862SJeremy Soller 	enum led_brightness kb_toggle_brightness;
26fd13c862SJeremy Soller 	int kb_color;
27fd13c862SJeremy Soller };
28fd13c862SJeremy Soller 
29fd13c862SJeremy Soller static const struct acpi_device_id device_ids[] = {
30fd13c862SJeremy Soller 	{"17761776", 0},
31fd13c862SJeremy Soller 	{"", 0},
32fd13c862SJeremy Soller };
33fd13c862SJeremy Soller MODULE_DEVICE_TABLE(acpi, device_ids);
34fd13c862SJeremy Soller 
35fd13c862SJeremy Soller // Array of keyboard LED brightness levels
36fd13c862SJeremy Soller static const enum led_brightness kb_levels[] = {
37fd13c862SJeremy Soller 	48,
38fd13c862SJeremy Soller 	72,
39fd13c862SJeremy Soller 	96,
40fd13c862SJeremy Soller 	144,
41fd13c862SJeremy Soller 	192,
42fd13c862SJeremy Soller 	255
43fd13c862SJeremy Soller };
44fd13c862SJeremy Soller 
45fd13c862SJeremy Soller // Array of keyboard LED colors in 24-bit RGB format
46fd13c862SJeremy Soller static const int kb_colors[] = {
47fd13c862SJeremy Soller 	0xFFFFFF,
48fd13c862SJeremy Soller 	0x0000FF,
49fd13c862SJeremy Soller 	0xFF0000,
50fd13c862SJeremy Soller 	0xFF00FF,
51fd13c862SJeremy Soller 	0x00FF00,
52fd13c862SJeremy Soller 	0x00FFFF,
53fd13c862SJeremy Soller 	0xFFFF00
54fd13c862SJeremy Soller };
55fd13c862SJeremy Soller 
56fd13c862SJeremy Soller // Get a System76 ACPI device value by name
57fd13c862SJeremy Soller static int system76_get(struct system76_data *data, char *method)
58fd13c862SJeremy Soller {
59fd13c862SJeremy Soller 	acpi_handle handle;
60fd13c862SJeremy Soller 	acpi_status status;
61fd13c862SJeremy Soller 	unsigned long long ret = 0;
62fd13c862SJeremy Soller 
63fd13c862SJeremy Soller 	handle = acpi_device_handle(data->acpi_dev);
64fd13c862SJeremy Soller 	status = acpi_evaluate_integer(handle, method, NULL, &ret);
65fd13c862SJeremy Soller 	if (ACPI_SUCCESS(status))
66fd13c862SJeremy Soller 		return (int)ret;
67fd13c862SJeremy Soller 	else
68fd13c862SJeremy Soller 		return -1;
69fd13c862SJeremy Soller }
70fd13c862SJeremy Soller 
71fd13c862SJeremy Soller // Set a System76 ACPI device value by name
72fd13c862SJeremy Soller static int system76_set(struct system76_data *data, char *method, int value)
73fd13c862SJeremy Soller {
74fd13c862SJeremy Soller 	union acpi_object obj;
75fd13c862SJeremy Soller 	struct acpi_object_list obj_list;
76fd13c862SJeremy Soller 	acpi_handle handle;
77fd13c862SJeremy Soller 	acpi_status status;
78fd13c862SJeremy Soller 
79fd13c862SJeremy Soller 	obj.type = ACPI_TYPE_INTEGER;
80fd13c862SJeremy Soller 	obj.integer.value = value;
81fd13c862SJeremy Soller 	obj_list.count = 1;
82fd13c862SJeremy Soller 	obj_list.pointer = &obj;
83fd13c862SJeremy Soller 	handle = acpi_device_handle(data->acpi_dev);
84fd13c862SJeremy Soller 	status = acpi_evaluate_object(handle, method, &obj_list, NULL);
85fd13c862SJeremy Soller 	if (ACPI_SUCCESS(status))
86fd13c862SJeremy Soller 		return 0;
87fd13c862SJeremy Soller 	else
88fd13c862SJeremy Soller 		return -1;
89fd13c862SJeremy Soller }
90fd13c862SJeremy Soller 
91fd13c862SJeremy Soller // Get the airplane mode LED brightness
92fd13c862SJeremy Soller static enum led_brightness ap_led_get(struct led_classdev *led)
93fd13c862SJeremy Soller {
94fd13c862SJeremy Soller 	struct system76_data *data;
95fd13c862SJeremy Soller 	int value;
96fd13c862SJeremy Soller 
97fd13c862SJeremy Soller 	data = container_of(led, struct system76_data, ap_led);
98fd13c862SJeremy Soller 	value = system76_get(data, "GAPL");
99fd13c862SJeremy Soller 	if (value > 0)
100fd13c862SJeremy Soller 		return (enum led_brightness)value;
101fd13c862SJeremy Soller 	else
102fd13c862SJeremy Soller 		return LED_OFF;
103fd13c862SJeremy Soller }
104fd13c862SJeremy Soller 
105fd13c862SJeremy Soller // Set the airplane mode LED brightness
1065b36398dSNick Shipp static int ap_led_set(struct led_classdev *led, enum led_brightness value)
107fd13c862SJeremy Soller {
108fd13c862SJeremy Soller 	struct system76_data *data;
109fd13c862SJeremy Soller 
110fd13c862SJeremy Soller 	data = container_of(led, struct system76_data, ap_led);
1115b36398dSNick Shipp 	return system76_set(data, "SAPL", value == LED_OFF ? 0 : 1);
112fd13c862SJeremy Soller }
113fd13c862SJeremy Soller 
114fd13c862SJeremy Soller // Get the last set keyboard LED brightness
115fd13c862SJeremy Soller static enum led_brightness kb_led_get(struct led_classdev *led)
116fd13c862SJeremy Soller {
117fd13c862SJeremy Soller 	struct system76_data *data;
118fd13c862SJeremy Soller 
119fd13c862SJeremy Soller 	data = container_of(led, struct system76_data, kb_led);
120fd13c862SJeremy Soller 	return data->kb_brightness;
121fd13c862SJeremy Soller }
122fd13c862SJeremy Soller 
123fd13c862SJeremy Soller // Set the keyboard LED brightness
1245b36398dSNick Shipp static int kb_led_set(struct led_classdev *led, enum led_brightness value)
125fd13c862SJeremy Soller {
126fd13c862SJeremy Soller 	struct system76_data *data;
127fd13c862SJeremy Soller 
128fd13c862SJeremy Soller 	data = container_of(led, struct system76_data, kb_led);
129fd13c862SJeremy Soller 	data->kb_brightness = value;
1305b36398dSNick Shipp 	return system76_set(data, "SKBL", (int)data->kb_brightness);
131fd13c862SJeremy Soller }
132fd13c862SJeremy Soller 
133fd13c862SJeremy Soller // Get the last set keyboard LED color
134fd13c862SJeremy Soller static ssize_t kb_led_color_show(
135fd13c862SJeremy Soller 	struct device *dev,
136fd13c862SJeremy Soller 	struct device_attribute *dev_attr,
137fd13c862SJeremy Soller 	char *buf)
138fd13c862SJeremy Soller {
139fd13c862SJeremy Soller 	struct led_classdev *led;
140fd13c862SJeremy Soller 	struct system76_data *data;
141fd13c862SJeremy Soller 
142fd13c862SJeremy Soller 	led = (struct led_classdev *)dev->driver_data;
143fd13c862SJeremy Soller 	data = container_of(led, struct system76_data, kb_led);
144fd13c862SJeremy Soller 	return sprintf(buf, "%06X\n", data->kb_color);
145fd13c862SJeremy Soller }
146fd13c862SJeremy Soller 
147fd13c862SJeremy Soller // Set the keyboard LED color
148fd13c862SJeremy Soller static ssize_t kb_led_color_store(
149fd13c862SJeremy Soller 	struct device *dev,
150fd13c862SJeremy Soller 	struct device_attribute *dev_attr,
151fd13c862SJeremy Soller 	const char *buf,
152fd13c862SJeremy Soller 	size_t size)
153fd13c862SJeremy Soller {
154fd13c862SJeremy Soller 	struct led_classdev *led;
155fd13c862SJeremy Soller 	struct system76_data *data;
156fd13c862SJeremy Soller 	unsigned int val;
157fd13c862SJeremy Soller 	int ret;
158fd13c862SJeremy Soller 
159fd13c862SJeremy Soller 	led = (struct led_classdev *)dev->driver_data;
160fd13c862SJeremy Soller 	data = container_of(led, struct system76_data, kb_led);
161fd13c862SJeremy Soller 	ret = kstrtouint(buf, 16, &val);
162fd13c862SJeremy Soller 	if (ret)
163fd13c862SJeremy Soller 		return ret;
164fd13c862SJeremy Soller 	if (val > 0xFFFFFF)
165fd13c862SJeremy Soller 		return -EINVAL;
166fd13c862SJeremy Soller 	data->kb_color = (int)val;
167fd13c862SJeremy Soller 	system76_set(data, "SKBC", data->kb_color);
168fd13c862SJeremy Soller 
169fd13c862SJeremy Soller 	return size;
170fd13c862SJeremy Soller }
171fd13c862SJeremy Soller 
172fd13c862SJeremy Soller static const struct device_attribute kb_led_color_dev_attr = {
173fd13c862SJeremy Soller 	.attr = {
174fd13c862SJeremy Soller 		.name = "color",
175fd13c862SJeremy Soller 		.mode = 0644,
176fd13c862SJeremy Soller 	},
177fd13c862SJeremy Soller 	.show = kb_led_color_show,
178fd13c862SJeremy Soller 	.store = kb_led_color_store,
179fd13c862SJeremy Soller };
180fd13c862SJeremy Soller 
181fd13c862SJeremy Soller // Notify that the keyboard LED was changed by hardware
182fd13c862SJeremy Soller static void kb_led_notify(struct system76_data *data)
183fd13c862SJeremy Soller {
184fd13c862SJeremy Soller 	led_classdev_notify_brightness_hw_changed(
185fd13c862SJeremy Soller 		&data->kb_led,
186fd13c862SJeremy Soller 		data->kb_brightness
187fd13c862SJeremy Soller 	);
188fd13c862SJeremy Soller }
189fd13c862SJeremy Soller 
190fd13c862SJeremy Soller // Read keyboard LED brightness as set by hardware
191fd13c862SJeremy Soller static void kb_led_hotkey_hardware(struct system76_data *data)
192fd13c862SJeremy Soller {
193fd13c862SJeremy Soller 	int value;
194fd13c862SJeremy Soller 
195fd13c862SJeremy Soller 	value = system76_get(data, "GKBL");
196fd13c862SJeremy Soller 	if (value < 0)
197fd13c862SJeremy Soller 		return;
198fd13c862SJeremy Soller 	data->kb_brightness = value;
199fd13c862SJeremy Soller 	kb_led_notify(data);
200fd13c862SJeremy Soller }
201fd13c862SJeremy Soller 
202fd13c862SJeremy Soller // Toggle the keyboard LED
203fd13c862SJeremy Soller static void kb_led_hotkey_toggle(struct system76_data *data)
204fd13c862SJeremy Soller {
205fd13c862SJeremy Soller 	if (data->kb_brightness > 0) {
206fd13c862SJeremy Soller 		data->kb_toggle_brightness = data->kb_brightness;
207fd13c862SJeremy Soller 		kb_led_set(&data->kb_led, 0);
208fd13c862SJeremy Soller 	} else {
209fd13c862SJeremy Soller 		kb_led_set(&data->kb_led, data->kb_toggle_brightness);
210fd13c862SJeremy Soller 	}
211fd13c862SJeremy Soller 	kb_led_notify(data);
212fd13c862SJeremy Soller }
213fd13c862SJeremy Soller 
214fd13c862SJeremy Soller // Decrease the keyboard LED brightness
215fd13c862SJeremy Soller static void kb_led_hotkey_down(struct system76_data *data)
216fd13c862SJeremy Soller {
217fd13c862SJeremy Soller 	int i;
218fd13c862SJeremy Soller 
219fd13c862SJeremy Soller 	if (data->kb_brightness > 0) {
220fd13c862SJeremy Soller 		for (i = ARRAY_SIZE(kb_levels); i > 0; i--) {
221fd13c862SJeremy Soller 			if (kb_levels[i - 1] < data->kb_brightness) {
222fd13c862SJeremy Soller 				kb_led_set(&data->kb_led, kb_levels[i - 1]);
223fd13c862SJeremy Soller 				break;
224fd13c862SJeremy Soller 			}
225fd13c862SJeremy Soller 		}
226fd13c862SJeremy Soller 	} else {
227fd13c862SJeremy Soller 		kb_led_set(&data->kb_led, data->kb_toggle_brightness);
228fd13c862SJeremy Soller 	}
229fd13c862SJeremy Soller 	kb_led_notify(data);
230fd13c862SJeremy Soller }
231fd13c862SJeremy Soller 
232fd13c862SJeremy Soller // Increase the keyboard LED brightness
233fd13c862SJeremy Soller static void kb_led_hotkey_up(struct system76_data *data)
234fd13c862SJeremy Soller {
235fd13c862SJeremy Soller 	int i;
236fd13c862SJeremy Soller 
237fd13c862SJeremy Soller 	if (data->kb_brightness > 0) {
238fd13c862SJeremy Soller 		for (i = 0; i < ARRAY_SIZE(kb_levels); i++) {
239fd13c862SJeremy Soller 			if (kb_levels[i] > data->kb_brightness) {
240fd13c862SJeremy Soller 				kb_led_set(&data->kb_led, kb_levels[i]);
241fd13c862SJeremy Soller 				break;
242fd13c862SJeremy Soller 			}
243fd13c862SJeremy Soller 		}
244fd13c862SJeremy Soller 	} else {
245fd13c862SJeremy Soller 		kb_led_set(&data->kb_led, data->kb_toggle_brightness);
246fd13c862SJeremy Soller 	}
247fd13c862SJeremy Soller 	kb_led_notify(data);
248fd13c862SJeremy Soller }
249fd13c862SJeremy Soller 
250fd13c862SJeremy Soller // Cycle the keyboard LED color
251fd13c862SJeremy Soller static void kb_led_hotkey_color(struct system76_data *data)
252fd13c862SJeremy Soller {
253fd13c862SJeremy Soller 	int i;
254fd13c862SJeremy Soller 
255fd13c862SJeremy Soller 	if (data->kb_color < 0)
256fd13c862SJeremy Soller 		return;
257fd13c862SJeremy Soller 	if (data->kb_brightness > 0) {
258fd13c862SJeremy Soller 		for (i = 0; i < ARRAY_SIZE(kb_colors); i++) {
259fd13c862SJeremy Soller 			if (kb_colors[i] == data->kb_color)
260fd13c862SJeremy Soller 				break;
261fd13c862SJeremy Soller 		}
262fd13c862SJeremy Soller 		i += 1;
263fd13c862SJeremy Soller 		if (i >= ARRAY_SIZE(kb_colors))
264fd13c862SJeremy Soller 			i = 0;
265fd13c862SJeremy Soller 		data->kb_color = kb_colors[i];
266fd13c862SJeremy Soller 		system76_set(data, "SKBC", data->kb_color);
267fd13c862SJeremy Soller 	} else {
268fd13c862SJeremy Soller 		kb_led_set(&data->kb_led, data->kb_toggle_brightness);
269fd13c862SJeremy Soller 	}
270fd13c862SJeremy Soller 	kb_led_notify(data);
271fd13c862SJeremy Soller }
272fd13c862SJeremy Soller 
273fd13c862SJeremy Soller // Handle ACPI notification
274fd13c862SJeremy Soller static void system76_notify(struct acpi_device *acpi_dev, u32 event)
275fd13c862SJeremy Soller {
276fd13c862SJeremy Soller 	struct system76_data *data;
277fd13c862SJeremy Soller 
278fd13c862SJeremy Soller 	data = acpi_driver_data(acpi_dev);
279fd13c862SJeremy Soller 	switch (event) {
280fd13c862SJeremy Soller 	case 0x80:
281fd13c862SJeremy Soller 		kb_led_hotkey_hardware(data);
282fd13c862SJeremy Soller 		break;
283fd13c862SJeremy Soller 	case 0x81:
284fd13c862SJeremy Soller 		kb_led_hotkey_toggle(data);
285fd13c862SJeremy Soller 		break;
286fd13c862SJeremy Soller 	case 0x82:
287fd13c862SJeremy Soller 		kb_led_hotkey_down(data);
288fd13c862SJeremy Soller 		break;
289fd13c862SJeremy Soller 	case 0x83:
290fd13c862SJeremy Soller 		kb_led_hotkey_up(data);
291fd13c862SJeremy Soller 		break;
292fd13c862SJeremy Soller 	case 0x84:
293fd13c862SJeremy Soller 		kb_led_hotkey_color(data);
294fd13c862SJeremy Soller 		break;
295fd13c862SJeremy Soller 	}
296fd13c862SJeremy Soller }
297fd13c862SJeremy Soller 
298fd13c862SJeremy Soller // Add a System76 ACPI device
299fd13c862SJeremy Soller static int system76_add(struct acpi_device *acpi_dev)
300fd13c862SJeremy Soller {
301fd13c862SJeremy Soller 	struct system76_data *data;
302fd13c862SJeremy Soller 	int err;
303fd13c862SJeremy Soller 
304fd13c862SJeremy Soller 	data = devm_kzalloc(&acpi_dev->dev, sizeof(*data), GFP_KERNEL);
305fd13c862SJeremy Soller 	if (!data)
306fd13c862SJeremy Soller 		return -ENOMEM;
307fd13c862SJeremy Soller 	acpi_dev->driver_data = data;
308fd13c862SJeremy Soller 	data->acpi_dev = acpi_dev;
309fd13c862SJeremy Soller 
310fd13c862SJeremy Soller 	err = system76_get(data, "INIT");
311fd13c862SJeremy Soller 	if (err)
312fd13c862SJeremy Soller 		return err;
313fd13c862SJeremy Soller 	data->ap_led.name = "system76_acpi::airplane";
314fd13c862SJeremy Soller 	data->ap_led.flags = LED_CORE_SUSPENDRESUME;
315fd13c862SJeremy Soller 	data->ap_led.brightness_get = ap_led_get;
3165b36398dSNick Shipp 	data->ap_led.brightness_set_blocking = ap_led_set;
317fd13c862SJeremy Soller 	data->ap_led.max_brightness = 1;
318fd13c862SJeremy Soller 	data->ap_led.default_trigger = "rfkill-none";
319fd13c862SJeremy Soller 	err = devm_led_classdev_register(&acpi_dev->dev, &data->ap_led);
320fd13c862SJeremy Soller 	if (err)
321fd13c862SJeremy Soller 		return err;
322fd13c862SJeremy Soller 
323fd13c862SJeremy Soller 	data->kb_led.name = "system76_acpi::kbd_backlight";
324fd13c862SJeremy Soller 	data->kb_led.flags = LED_BRIGHT_HW_CHANGED | LED_CORE_SUSPENDRESUME;
325fd13c862SJeremy Soller 	data->kb_led.brightness_get = kb_led_get;
3265b36398dSNick Shipp 	data->kb_led.brightness_set_blocking = kb_led_set;
327fd13c862SJeremy Soller 	if (acpi_has_method(acpi_device_handle(data->acpi_dev), "SKBC")) {
328fd13c862SJeremy Soller 		data->kb_led.max_brightness = 255;
329fd13c862SJeremy Soller 		data->kb_toggle_brightness = 72;
330fd13c862SJeremy Soller 		data->kb_color = 0xffffff;
331fd13c862SJeremy Soller 		system76_set(data, "SKBC", data->kb_color);
332fd13c862SJeremy Soller 	} else {
333fd13c862SJeremy Soller 		data->kb_led.max_brightness = 5;
334fd13c862SJeremy Soller 		data->kb_color = -1;
335fd13c862SJeremy Soller 	}
336fd13c862SJeremy Soller 	err = devm_led_classdev_register(&acpi_dev->dev, &data->kb_led);
337fd13c862SJeremy Soller 	if (err)
338fd13c862SJeremy Soller 		return err;
339fd13c862SJeremy Soller 
340fd13c862SJeremy Soller 	if (data->kb_color >= 0) {
341fd13c862SJeremy Soller 		err = device_create_file(
342fd13c862SJeremy Soller 			data->kb_led.dev,
343fd13c862SJeremy Soller 			&kb_led_color_dev_attr
344fd13c862SJeremy Soller 		);
345fd13c862SJeremy Soller 		if (err)
346fd13c862SJeremy Soller 			return err;
347fd13c862SJeremy Soller 	}
348fd13c862SJeremy Soller 
349fd13c862SJeremy Soller 	return 0;
350fd13c862SJeremy Soller }
351fd13c862SJeremy Soller 
352fd13c862SJeremy Soller // Remove a System76 ACPI device
353fd13c862SJeremy Soller static int system76_remove(struct acpi_device *acpi_dev)
354fd13c862SJeremy Soller {
355fd13c862SJeremy Soller 	struct system76_data *data;
356fd13c862SJeremy Soller 
357fd13c862SJeremy Soller 	data = acpi_driver_data(acpi_dev);
358fd13c862SJeremy Soller 	if (data->kb_color >= 0)
359fd13c862SJeremy Soller 		device_remove_file(data->kb_led.dev, &kb_led_color_dev_attr);
360fd13c862SJeremy Soller 
361fd13c862SJeremy Soller 	devm_led_classdev_unregister(&acpi_dev->dev, &data->ap_led);
362fd13c862SJeremy Soller 
363fd13c862SJeremy Soller 	devm_led_classdev_unregister(&acpi_dev->dev, &data->kb_led);
364fd13c862SJeremy Soller 
365fd13c862SJeremy Soller 	system76_get(data, "FINI");
366fd13c862SJeremy Soller 
367fd13c862SJeremy Soller 	return 0;
368fd13c862SJeremy Soller }
369fd13c862SJeremy Soller 
370fd13c862SJeremy Soller static struct acpi_driver system76_driver = {
371fd13c862SJeremy Soller 	.name = "System76 ACPI Driver",
372fd13c862SJeremy Soller 	.class = "hotkey",
373fd13c862SJeremy Soller 	.ids = device_ids,
374fd13c862SJeremy Soller 	.ops = {
375fd13c862SJeremy Soller 		.add = system76_add,
376fd13c862SJeremy Soller 		.remove = system76_remove,
377fd13c862SJeremy Soller 		.notify = system76_notify,
378fd13c862SJeremy Soller 	},
379fd13c862SJeremy Soller };
380fd13c862SJeremy Soller module_acpi_driver(system76_driver);
381fd13c862SJeremy Soller 
382fd13c862SJeremy Soller MODULE_DESCRIPTION("System76 ACPI Driver");
383fd13c862SJeremy Soller MODULE_AUTHOR("Jeremy Soller <jeremy@system76.com>");
384fd13c862SJeremy Soller MODULE_LICENSE("GPL");
385