summaryrefslogtreecommitdiffstats
path: root/drivers/platform/x86/asus-wireless.c
diff options
context:
space:
mode:
Diffstat (limited to 'drivers/platform/x86/asus-wireless.c')
-rw-r--r--drivers/platform/x86/asus-wireless.c202
1 files changed, 202 insertions, 0 deletions
diff --git a/drivers/platform/x86/asus-wireless.c b/drivers/platform/x86/asus-wireless.c
new file mode 100644
index 0000000000..abf01e00b7
--- /dev/null
+++ b/drivers/platform/x86/asus-wireless.c
@@ -0,0 +1,202 @@
+// 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 void 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);
+ }
+}
+
+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 <jprvita@gmail.com>");
+MODULE_LICENSE("GPL");