1 | // SPDX-License-Identifier: GPL-2.0-only |
2 | /* |
3 | * Asus Wireless Radio Control Driver |
4 | * |
5 | * Copyright (C) 2015-2016 Endless Mobile, Inc. |
6 | */ |
7 | |
8 | #include <linux/kernel.h> |
9 | #include <linux/module.h> |
10 | #include <linux/init.h> |
11 | #include <linux/types.h> |
12 | #include <linux/acpi.h> |
13 | #include <linux/input.h> |
14 | #include <linux/pci_ids.h> |
15 | #include <linux/leds.h> |
16 | |
17 | struct hswc_params { |
18 | u8 on; |
19 | u8 off; |
20 | u8 status; |
21 | }; |
22 | |
23 | struct asus_wireless_data { |
24 | struct input_dev *idev; |
25 | struct acpi_device *adev; |
26 | const struct hswc_params *hswc_params; |
27 | struct workqueue_struct *wq; |
28 | struct work_struct led_work; |
29 | struct led_classdev led; |
30 | int led_state; |
31 | }; |
32 | |
33 | static const struct hswc_params atk4001_id_params = { |
34 | .on = 0x0, |
35 | .off = 0x1, |
36 | .status = 0x2, |
37 | }; |
38 | |
39 | static const struct hswc_params atk4002_id_params = { |
40 | .on = 0x5, |
41 | .off = 0x4, |
42 | .status = 0x2, |
43 | }; |
44 | |
45 | static const struct acpi_device_id device_ids[] = { |
46 | {"ATK4001" , (kernel_ulong_t)&atk4001_id_params}, |
47 | {"ATK4002" , (kernel_ulong_t)&atk4002_id_params}, |
48 | {"" , 0}, |
49 | }; |
50 | MODULE_DEVICE_TABLE(acpi, device_ids); |
51 | |
52 | static acpi_status asus_wireless_method(acpi_handle handle, const char *method, |
53 | int param, u64 *ret) |
54 | { |
55 | struct acpi_object_list p; |
56 | union acpi_object obj; |
57 | acpi_status s; |
58 | |
59 | acpi_handle_debug(handle, "Evaluating method %s, parameter %#x\n" , |
60 | method, param); |
61 | obj.type = ACPI_TYPE_INTEGER; |
62 | obj.integer.value = param; |
63 | p.count = 1; |
64 | p.pointer = &obj; |
65 | |
66 | s = acpi_evaluate_integer(handle, pathname: (acpi_string) method, arguments: &p, data: ret); |
67 | if (ACPI_FAILURE(s)) |
68 | acpi_handle_err(handle, |
69 | "Failed to eval method %s, param %#x (%d)\n" , |
70 | method, param, s); |
71 | else |
72 | acpi_handle_debug(handle, "%s returned %#llx\n" , method, *ret); |
73 | |
74 | return s; |
75 | } |
76 | |
77 | static enum led_brightness led_state_get(struct led_classdev *led) |
78 | { |
79 | struct asus_wireless_data *data; |
80 | acpi_status s; |
81 | u64 ret; |
82 | |
83 | data = container_of(led, struct asus_wireless_data, led); |
84 | s = asus_wireless_method(handle: acpi_device_handle(adev: data->adev), method: "HSWC" , |
85 | param: data->hswc_params->status, ret: &ret); |
86 | if (ACPI_SUCCESS(s) && ret == data->hswc_params->on) |
87 | return LED_FULL; |
88 | return LED_OFF; |
89 | } |
90 | |
91 | static void led_state_update(struct work_struct *work) |
92 | { |
93 | struct asus_wireless_data *data; |
94 | u64 ret; |
95 | |
96 | data = container_of(work, struct asus_wireless_data, led_work); |
97 | asus_wireless_method(handle: acpi_device_handle(adev: data->adev), method: "HSWC" , |
98 | param: data->led_state, ret: &ret); |
99 | } |
100 | |
101 | static void led_state_set(struct led_classdev *led, enum led_brightness value) |
102 | { |
103 | struct asus_wireless_data *data; |
104 | |
105 | data = container_of(led, struct asus_wireless_data, led); |
106 | data->led_state = value == LED_OFF ? data->hswc_params->off : |
107 | data->hswc_params->on; |
108 | queue_work(wq: data->wq, work: &data->led_work); |
109 | } |
110 | |
111 | static void asus_wireless_notify(struct acpi_device *adev, u32 event) |
112 | { |
113 | struct asus_wireless_data *data = acpi_driver_data(d: adev); |
114 | |
115 | dev_dbg(&adev->dev, "event=%#x\n" , event); |
116 | if (event != 0x88) { |
117 | dev_notice(&adev->dev, "Unknown ASHS event: %#x\n" , event); |
118 | return; |
119 | } |
120 | input_report_key(dev: data->idev, KEY_RFKILL, value: 1); |
121 | input_sync(dev: data->idev); |
122 | input_report_key(dev: data->idev, KEY_RFKILL, value: 0); |
123 | input_sync(dev: data->idev); |
124 | } |
125 | |
126 | static int asus_wireless_add(struct acpi_device *adev) |
127 | { |
128 | struct asus_wireless_data *data; |
129 | const struct acpi_device_id *id; |
130 | int err; |
131 | |
132 | data = devm_kzalloc(dev: &adev->dev, size: sizeof(*data), GFP_KERNEL); |
133 | if (!data) |
134 | return -ENOMEM; |
135 | adev->driver_data = data; |
136 | data->adev = adev; |
137 | |
138 | data->idev = devm_input_allocate_device(&adev->dev); |
139 | if (!data->idev) |
140 | return -ENOMEM; |
141 | data->idev->name = "Asus Wireless Radio Control" ; |
142 | data->idev->phys = "asus-wireless/input0" ; |
143 | data->idev->id.bustype = BUS_HOST; |
144 | data->idev->id.vendor = PCI_VENDOR_ID_ASUSTEK; |
145 | set_bit(EV_KEY, addr: data->idev->evbit); |
146 | set_bit(KEY_RFKILL, addr: data->idev->keybit); |
147 | err = input_register_device(data->idev); |
148 | if (err) |
149 | return err; |
150 | |
151 | id = acpi_match_acpi_device(ids: device_ids, adev); |
152 | if (!id) |
153 | return 0; |
154 | |
155 | data->hswc_params = (const struct hswc_params *)id->driver_data; |
156 | |
157 | data->wq = create_singlethread_workqueue("asus_wireless_workqueue" ); |
158 | if (!data->wq) |
159 | return -ENOMEM; |
160 | INIT_WORK(&data->led_work, led_state_update); |
161 | data->led.name = "asus-wireless::airplane" ; |
162 | data->led.brightness_set = led_state_set; |
163 | data->led.brightness_get = led_state_get; |
164 | data->led.flags = LED_CORE_SUSPENDRESUME; |
165 | data->led.max_brightness = 1; |
166 | data->led.default_trigger = "rfkill-none" ; |
167 | err = devm_led_classdev_register(parent: &adev->dev, led_cdev: &data->led); |
168 | if (err) |
169 | destroy_workqueue(wq: data->wq); |
170 | |
171 | return err; |
172 | } |
173 | |
174 | static void asus_wireless_remove(struct acpi_device *adev) |
175 | { |
176 | struct asus_wireless_data *data = acpi_driver_data(d: adev); |
177 | |
178 | if (data->wq) { |
179 | devm_led_classdev_unregister(parent: &adev->dev, led_cdev: &data->led); |
180 | destroy_workqueue(wq: data->wq); |
181 | } |
182 | } |
183 | |
184 | static struct acpi_driver asus_wireless_driver = { |
185 | .name = "Asus Wireless Radio Control Driver" , |
186 | .class = "hotkey" , |
187 | .ids = device_ids, |
188 | .ops = { |
189 | .add = asus_wireless_add, |
190 | .remove = asus_wireless_remove, |
191 | .notify = asus_wireless_notify, |
192 | }, |
193 | }; |
194 | module_acpi_driver(asus_wireless_driver); |
195 | |
196 | MODULE_DESCRIPTION("Asus Wireless Radio Control Driver" ); |
197 | MODULE_AUTHOR("João Paulo Rechi Vita <jprvita@gmail.com>" ); |
198 | MODULE_LICENSE("GPL" ); |
199 | |