mirror of https://github.com/Qortal/Brooklyn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
203 lines
5.0 KiB
203 lines
5.0 KiB
// SPDX-License-Identifier: GPL-2.0-only |
|
/* |
|
* Asus Wireless Radio Control Driver |
|
* |
|
* Copyright (C) 2015-2016 Endless Mobile, Inc. |
|
*/ |
|
|
|
#include <linux/kernel.h> |
|
#include <linux/module.h> |
|
#include <linux/init.h> |
|
#include <linux/types.h> |
|
#include <linux/acpi.h> |
|
#include <linux/input.h> |
|
#include <linux/pci_ids.h> |
|
#include <linux/leds.h> |
|
|
|
struct hswc_params { |
|
u8 on; |
|
u8 off; |
|
u8 status; |
|
}; |
|
|
|
struct asus_wireless_data { |
|
struct input_dev *idev; |
|
struct acpi_device *adev; |
|
const struct hswc_params *hswc_params; |
|
struct workqueue_struct *wq; |
|
struct work_struct led_work; |
|
struct led_classdev led; |
|
int led_state; |
|
}; |
|
|
|
static const struct hswc_params atk4001_id_params = { |
|
.on = 0x0, |
|
.off = 0x1, |
|
.status = 0x2, |
|
}; |
|
|
|
static const struct hswc_params atk4002_id_params = { |
|
.on = 0x5, |
|
.off = 0x4, |
|
.status = 0x2, |
|
}; |
|
|
|
static const struct acpi_device_id device_ids[] = { |
|
{"ATK4001", (kernel_ulong_t)&atk4001_id_params}, |
|
{"ATK4002", (kernel_ulong_t)&atk4002_id_params}, |
|
{"", 0}, |
|
}; |
|
MODULE_DEVICE_TABLE(acpi, device_ids); |
|
|
|
static acpi_status asus_wireless_method(acpi_handle handle, const char *method, |
|
int param, u64 *ret) |
|
{ |
|
struct acpi_object_list p; |
|
union acpi_object obj; |
|
acpi_status s; |
|
|
|
acpi_handle_debug(handle, "Evaluating method %s, parameter %#x\n", |
|
method, param); |
|
obj.type = ACPI_TYPE_INTEGER; |
|
obj.integer.value = param; |
|
p.count = 1; |
|
p.pointer = &obj; |
|
|
|
s = acpi_evaluate_integer(handle, (acpi_string) method, &p, ret); |
|
if (ACPI_FAILURE(s)) |
|
acpi_handle_err(handle, |
|
"Failed to eval method %s, param %#x (%d)\n", |
|
method, param, s); |
|
else |
|
acpi_handle_debug(handle, "%s returned %#llx\n", method, *ret); |
|
|
|
return s; |
|
} |
|
|
|
static enum led_brightness led_state_get(struct led_classdev *led) |
|
{ |
|
struct asus_wireless_data *data; |
|
acpi_status s; |
|
u64 ret; |
|
|
|
data = container_of(led, struct asus_wireless_data, led); |
|
s = asus_wireless_method(acpi_device_handle(data->adev), "HSWC", |
|
data->hswc_params->status, &ret); |
|
if (ACPI_SUCCESS(s) && ret == data->hswc_params->on) |
|
return LED_FULL; |
|
return LED_OFF; |
|
} |
|
|
|
static void led_state_update(struct work_struct *work) |
|
{ |
|
struct asus_wireless_data *data; |
|
u64 ret; |
|
|
|
data = container_of(work, struct asus_wireless_data, led_work); |
|
asus_wireless_method(acpi_device_handle(data->adev), "HSWC", |
|
data->led_state, &ret); |
|
} |
|
|
|
static void led_state_set(struct led_classdev *led, enum led_brightness value) |
|
{ |
|
struct asus_wireless_data *data; |
|
|
|
data = container_of(led, struct asus_wireless_data, led); |
|
data->led_state = value == LED_OFF ? data->hswc_params->off : |
|
data->hswc_params->on; |
|
queue_work(data->wq, &data->led_work); |
|
} |
|
|
|
static void asus_wireless_notify(struct acpi_device *adev, u32 event) |
|
{ |
|
struct asus_wireless_data *data = acpi_driver_data(adev); |
|
|
|
dev_dbg(&adev->dev, "event=%#x\n", event); |
|
if (event != 0x88) { |
|
dev_notice(&adev->dev, "Unknown ASHS event: %#x\n", event); |
|
return; |
|
} |
|
input_report_key(data->idev, KEY_RFKILL, 1); |
|
input_sync(data->idev); |
|
input_report_key(data->idev, KEY_RFKILL, 0); |
|
input_sync(data->idev); |
|
} |
|
|
|
static int asus_wireless_add(struct acpi_device *adev) |
|
{ |
|
struct asus_wireless_data *data; |
|
const struct acpi_device_id *id; |
|
int err; |
|
|
|
data = devm_kzalloc(&adev->dev, sizeof(*data), GFP_KERNEL); |
|
if (!data) |
|
return -ENOMEM; |
|
adev->driver_data = data; |
|
data->adev = adev; |
|
|
|
data->idev = devm_input_allocate_device(&adev->dev); |
|
if (!data->idev) |
|
return -ENOMEM; |
|
data->idev->name = "Asus Wireless Radio Control"; |
|
data->idev->phys = "asus-wireless/input0"; |
|
data->idev->id.bustype = BUS_HOST; |
|
data->idev->id.vendor = PCI_VENDOR_ID_ASUSTEK; |
|
set_bit(EV_KEY, data->idev->evbit); |
|
set_bit(KEY_RFKILL, data->idev->keybit); |
|
err = input_register_device(data->idev); |
|
if (err) |
|
return err; |
|
|
|
for (id = device_ids; id->id[0]; id++) { |
|
if (!strcmp((char *) id->id, acpi_device_hid(adev))) { |
|
data->hswc_params = |
|
(const struct hswc_params *)id->driver_data; |
|
break; |
|
} |
|
} |
|
if (!data->hswc_params) |
|
return 0; |
|
|
|
data->wq = create_singlethread_workqueue("asus_wireless_workqueue"); |
|
if (!data->wq) |
|
return -ENOMEM; |
|
INIT_WORK(&data->led_work, led_state_update); |
|
data->led.name = "asus-wireless::airplane"; |
|
data->led.brightness_set = led_state_set; |
|
data->led.brightness_get = led_state_get; |
|
data->led.flags = LED_CORE_SUSPENDRESUME; |
|
data->led.max_brightness = 1; |
|
data->led.default_trigger = "rfkill-none"; |
|
err = devm_led_classdev_register(&adev->dev, &data->led); |
|
if (err) |
|
destroy_workqueue(data->wq); |
|
|
|
return err; |
|
} |
|
|
|
static int asus_wireless_remove(struct acpi_device *adev) |
|
{ |
|
struct asus_wireless_data *data = acpi_driver_data(adev); |
|
|
|
if (data->wq) { |
|
devm_led_classdev_unregister(&adev->dev, &data->led); |
|
destroy_workqueue(data->wq); |
|
} |
|
return 0; |
|
} |
|
|
|
static struct acpi_driver asus_wireless_driver = { |
|
.name = "Asus Wireless Radio Control Driver", |
|
.class = "hotkey", |
|
.ids = device_ids, |
|
.ops = { |
|
.add = asus_wireless_add, |
|
.remove = asus_wireless_remove, |
|
.notify = asus_wireless_notify, |
|
}, |
|
}; |
|
module_acpi_driver(asus_wireless_driver); |
|
|
|
MODULE_DESCRIPTION("Asus Wireless Radio Control Driver"); |
|
MODULE_AUTHOR("João Paulo Rechi Vita <[email protected]>"); |
|
MODULE_LICENSE("GPL");
|
|
|