Last active
February 24, 2017 01:37
-
-
Save iamahuman/8908e20fc6e0f540f447f5eb3107959e to your computer and use it in GitHub Desktop.
LGE HotKey driver
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt | |
#include <linux/kernel.h> | |
#include <linux/module.h> | |
#include <linux/init.h> | |
#include <linux/types.h> | |
#include <linux/err.h> | |
#include <linux/platform_device.h> | |
#include <linux/slab.h> | |
#include <linux/input.h> | |
#include <linux/input/sparse-keymap.h> | |
#include <linux/debugfs.h> | |
#include <linux/seq_file.h> | |
#include <linux/leds.h> | |
#include <linux/acpi.h> | |
#define LGE_WMI_KEY_EVENT_GUID "E4FB94F9-7F2B-4173-AD1A-CD1D95086248" | |
#define LGE_WMI_TMP_EVENT_GUID "023B133E-49D1-4E10-B313-698220140DC2" | |
#define LGE_WMI_MIS_EVENT_GUID "37BE1AC0-C3F2-4B1F-BFBE-8FDEAF2814D6" | |
#define LGE_WMI_GUID1 "C3A72B38-D3EF-42D3-8CBB-D5A57049F66D" | |
#define LGE_WMI_GUID2 "4E5C4404-3CED-4A5E-8C7A-1BA875D00A43" | |
#define LGE_WMI_GUID3 "2B4F501A-BD3C-4394-8DCF-00A7D2BC8210" | |
#define LGE_WMI_FILE "lge-wmi" | |
#define LGE_LAPTOP_NAME "LG Electronics Laptop Support" | |
#define LGE_LAPTOP_CLASS "LGE" | |
#define LGE_LAPTOP_FILE "lge-laptop" | |
#define LGE_AIRP_NAME "LGE Airplane Key driver" | |
#define LGE_AIRP_FILE "lge-airp" | |
#define LGE_WMI_GETULONG_METHODID 0x01 | |
#define LGE_WMI_SETULONG_METHODID 0x02 | |
#define LGE_WMI_FIREULONG_METHODID 0x03 /* unused */ | |
#define LGE_WMI_IOULONG_METHODID 0x04 /* unused */ | |
#define LGE_WMI_ERROR_INSTANCE_NOT_FOUND 0x80000001 | |
#define LGE_WMI_ERROR_UNSUPPORTED_OPERATION 0x80000002 | |
#define LGE_WMI_ERROR_INVALID_VALUE 0x80000003 | |
#define LGE_EVBR 0x0140 | |
#define LGE_EVWL 0x0136 | |
#define LGE_EVFN 0x013B | |
#define LGE_EVDK 0x0148 | |
#define LGE_EVPB 0x014B | |
MODULE_ALIAS("wmi:" LGE_WMI_KEY_EVENT_GUID); | |
MODULE_ALIAS("wmi:" LGE_WMI_TMP_EVENT_GUID); | |
MODULE_ALIAS("wmi:" LGE_WMI_MIS_EVENT_GUID); | |
static struct platform_device *lge_platform_device; | |
static struct input_dev *lge_wmi_inputdev; | |
static struct input_dev *lge_airp_inputdev; | |
static struct workqueue_struct *lge_touchpad_led_wq; | |
static struct work_struct lge_touchpad_led_work; | |
static int lge_touchpad_led_state; | |
static const struct key_entry lge_wmi_keymap[] = { | |
{ KE_KEY, 112, { KEY_F13 } }, | |
{ KE_KEY, 116, { KEY_F21 } }, | |
{ KE_KEY, 151, { KEY_F14 } }, | |
{ KE_END, 0}, | |
}; | |
struct wmbb_buf_header { | |
u32 req1; | |
u16 req2; | |
u16 unused; | |
u32 result; | |
}; | |
static int lge_wmab_execute(u8 instance, u32 method_id, u32 param, u32 *result) | |
{ | |
u32 tmp; | |
struct acpi_buffer input = { sizeof(u32), ¶m }; | |
struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; | |
acpi_status status; | |
union acpi_object *obj; | |
int retval = -EIO; | |
status = wmi_evaluate_method(LGE_WMI_GUID1, instance, method_id, &input, &output); | |
if (ACPI_FAILURE(status)) { | |
printk(KERN_ERR pr_fmt("wmab execute failed: %x\n"), status); | |
return retval; | |
} | |
obj = (union acpi_object *)output.pointer; | |
if (!obj || obj->type != ACPI_TYPE_INTEGER) | |
goto exit; | |
tmp = (u32) obj->integer.value; | |
if (tmp >= 0x80000000) { | |
printk(KERN_ERR pr_fmt("wmab returned %x\n"), tmp); | |
retval = -EIO; | |
if (tmp == LGE_WMI_ERROR_INSTANCE_NOT_FOUND) | |
retval = -ENXIO; | |
if (tmp == LGE_WMI_ERROR_UNSUPPORTED_OPERATION) | |
retval = -EPERM; | |
if (tmp == LGE_WMI_ERROR_INVALID_VALUE) | |
retval = -EINVAL; | |
} else { | |
retval = 0; | |
if (result) | |
*result = tmp; | |
} | |
exit: | |
kfree(obj); | |
return retval; | |
} | |
static inline int lge_wmi_get_param(u8 instance, u32 *result) { | |
return lge_wmab_execute(instance, LGE_WMI_GETULONG_METHODID, 0, result); | |
} | |
static inline int lge_wmi_set_param(u8 instance, u32 value) { | |
return lge_wmab_execute(instance, LGE_WMI_SETULONG_METHODID, value, NULL); | |
} | |
static void lge_wmi_hotkey_notify(u32 event, void *context) | |
{ | |
struct acpi_buffer response = { ACPI_ALLOCATE_BUFFER, NULL }; | |
union acpi_object *obj; | |
acpi_status status; | |
u32 code; | |
status = wmi_get_event_data(event, &response); | |
if (ACPI_FAILURE(status)) { | |
pr_err("bad event status %#x\n", status); | |
return; | |
} | |
obj = (union acpi_object *)response.pointer; | |
if (!obj || obj->type != ACPI_TYPE_INTEGER) | |
goto exit; | |
code = obj->integer.value; | |
if (code <= 0xff) { /* FN key event (EVKY) */ | |
if (!sparse_keymap_report_event(lge_wmi_inputdev, | |
code, 1, true)) | |
pr_info("Unknown key %x pressed\n", code); | |
goto exit; | |
} | |
switch (code & 0xffff) { | |
case LGE_EVBR: | |
case LGE_EVWL: | |
case LGE_EVFN: | |
case LGE_EVDK: | |
case LGE_EVPB: | |
default: | |
pr_info("event %x occured\n", code); | |
} | |
exit: | |
kfree(obj); | |
} | |
static enum led_brightness lge_wmi_touchpad_led_get(struct led_classdev *cdev) | |
{ | |
u32 res = 0xf; | |
lge_wmi_get_param(0x30, &res); | |
return (res & 0x2) ? LED_OFF : 1; | |
} | |
static void lge_wmi_touchpad_led_update(struct work_struct *work) | |
{ | |
u32 tmp; | |
acpi_status status; | |
status = lge_wmi_get_param(0x30, &tmp); | |
if (ACPI_FAILURE(status)) | |
return; | |
if (!(tmp & 0x2) != !!lge_touchpad_led_state) | |
lge_wmi_set_param(0x30, tmp ^ 0x2); | |
} | |
static void lge_wmi_touchpad_led_set(struct led_classdev *cdev, | |
enum led_brightness value) | |
{ | |
lge_touchpad_led_state = (value > 0); | |
queue_work(lge_touchpad_led_wq, &lge_touchpad_led_work); | |
} | |
static struct led_classdev touchpad_led = { | |
.name = "lge-wmi::touchpad", | |
.flags = 0, | |
.max_brightness = 1, | |
.brightness_get = lge_wmi_touchpad_led_get, | |
.brightness_set = lge_wmi_touchpad_led_set, | |
}; | |
static struct attribute *lge_attributes[] = { | |
NULL | |
}; | |
static const struct attribute_group lge_attr_group = { | |
.is_visible = NULL, | |
.attrs = lge_attributes, | |
}; | |
static int lge_wmi_input_setup(struct platform_device *device) | |
{ | |
struct input_dev *inputdev; | |
acpi_status status; | |
int err; | |
inputdev = input_allocate_device(); | |
if (!inputdev) | |
return -ENOMEM; | |
inputdev->name = "LG Electronics Laptop WMI hotkeys"; | |
inputdev->phys = LGE_WMI_FILE "/input0"; | |
inputdev->id.bustype = BUS_HOST; | |
inputdev->dev.parent = &device->dev; | |
err = sparse_keymap_setup(inputdev, lge_wmi_keymap, NULL); | |
if (err) | |
goto err_free_dev; | |
status = wmi_install_notify_handler(LGE_WMI_KEY_EVENT_GUID, | |
lge_wmi_hotkey_notify, NULL); | |
if (ACPI_FAILURE(status)) { | |
err = -EIO; | |
goto err_free_keymap; | |
} | |
err = input_register_device(inputdev); | |
if (err) | |
goto err_uninstall_notifier; | |
lge_wmi_inputdev = inputdev; | |
return 0; | |
err_uninstall_notifier: | |
wmi_remove_notify_handler(LGE_WMI_KEY_EVENT_GUID); | |
err_free_keymap: | |
sparse_keymap_free(inputdev); | |
err_free_dev: | |
input_free_device(inputdev); | |
return err; | |
} | |
static void lge_wmi_input_destroy(void) | |
{ | |
if (lge_wmi_inputdev) { | |
wmi_remove_notify_handler(LGE_WMI_KEY_EVENT_GUID); | |
sparse_keymap_free(lge_wmi_inputdev); | |
input_unregister_device(lge_wmi_inputdev); | |
} | |
lge_wmi_inputdev = NULL; | |
} | |
static int lge_airp_input_setup(struct platform_device *device) | |
{ | |
int err; | |
lge_airp_inputdev = input_allocate_device(); | |
if (!lge_airp_inputdev) | |
return -ENOMEM; | |
lge_airp_inputdev->name = LGE_AIRP_NAME; | |
lge_airp_inputdev->phys = LGE_AIRP_FILE "/input0"; | |
lge_airp_inputdev->id.bustype = BUS_HOST; | |
lge_airp_inputdev->dev.parent = &device->dev; | |
set_bit(EV_KEY, lge_airp_inputdev->evbit); | |
set_bit(KEY_RFKILL, lge_airp_inputdev->keybit); | |
err = input_register_device(lge_airp_inputdev); | |
if (err) { | |
input_free_device(lge_airp_inputdev); | |
return err; | |
} | |
return 0; | |
} | |
static int lge_airp_input_destroy(struct platform_device *device) | |
{ | |
input_unregister_device(lge_airp_inputdev); | |
return 0; | |
} | |
static void lge_airp_notify_handler(acpi_handle handle, u32 event, void *context) | |
{ | |
struct platform_device *device = context; | |
if (event != 0x80) { | |
dev_warn(&device->dev, "unknown event: %#x\n", event); | |
return; | |
} | |
input_report_key(lge_airp_inputdev, KEY_RFKILL, 1); | |
input_report_key(lge_airp_inputdev, KEY_RFKILL, 0); | |
input_sync(lge_airp_inputdev); | |
} | |
static int lge_airp_probe(struct platform_device *device) | |
{ | |
acpi_handle handle = ACPI_HANDLE(&device->dev); | |
acpi_status result; | |
u64 dev_status; | |
int err; | |
result = acpi_evaluate_integer(handle, "_STA", NULL, &dev_status); | |
if (!(result == AE_NOT_FOUND || (ACPI_SUCCESS(result) && (dev_status & 0x1f) != 0))) { | |
dev_warn(&device->dev, "Either LGE Airplane Key is not present or OSI configuration is inappropriate\n"); | |
return -ENODEV; | |
} | |
err = lge_airp_input_setup(device); | |
if (err) { | |
pr_err("Failed to set up LGE Airplane Key\n"); | |
return err; | |
} | |
result = acpi_install_notify_handler(handle, | |
ACPI_DEVICE_NOTIFY, | |
lge_airp_notify_handler, | |
device); | |
if (ACPI_FAILURE(result)) { | |
err = -EBUSY; | |
goto err_remove_input; | |
} | |
return 0; | |
err_remove_input: | |
lge_airp_input_destroy(device); | |
return err; | |
} | |
static int lge_airp_remove(struct platform_device *device) | |
{ | |
acpi_handle handle = ACPI_HANDLE(&device->dev); | |
lge_airp_input_destroy(device); | |
acpi_remove_notify_handler(handle, ACPI_DEVICE_NOTIFY, lge_airp_notify_handler); | |
return 0; | |
} | |
static const struct acpi_device_id lge_airp_ids[] = { | |
{"LGEX0815", 0}, | |
{"", 0}, | |
}; | |
static struct platform_driver lge_airp_pl_driver = { | |
.driver = { | |
.name = "lge-airp", | |
.acpi_match_table = lge_airp_ids, | |
}, | |
.probe = lge_airp_probe, | |
.remove = lge_airp_remove, | |
}; | |
MODULE_DEVICE_TABLE(acpi, lge_airp_ids); | |
static int lge_wmi_platform_probe(struct platform_device *device) | |
{ | |
int err; | |
if (wmi_has_guid(LGE_WMI_KEY_EVENT_GUID)) { | |
err = lge_wmi_input_setup(device); | |
if (err) | |
goto fail_input_setup; | |
} | |
if (wmi_has_guid(LGE_WMI_GUID1)) { | |
lge_touchpad_led_wq = | |
create_singlethread_workqueue("lge_touchpad_workqueue"); | |
if (!lge_touchpad_led_wq) { | |
err = -ENOMEM; | |
goto fail_create_touchpad_wq; | |
} | |
INIT_WORK(&lge_touchpad_led_work, lge_wmi_touchpad_led_update); | |
touchpad_led.brightness = 1; | |
err = devm_led_classdev_register(&device->dev, &touchpad_led); | |
if (err) | |
goto fail_create_touchpad_led; | |
} | |
err = sysfs_create_group(&device->dev.kobj, &lge_attr_group); | |
if (err) | |
goto fail_sysfs; | |
return 0; | |
fail_sysfs: | |
devm_led_classdev_unregister(&device->dev, &touchpad_led); | |
fail_create_touchpad_led: | |
destroy_workqueue(lge_touchpad_led_wq); | |
fail_create_touchpad_wq: | |
lge_wmi_input_destroy(); | |
fail_input_setup: | |
return err; | |
} | |
static int lge_wmi_platform_remove(struct platform_device *device) | |
{ | |
sysfs_remove_group(&device->dev.kobj, &lge_attr_group); | |
devm_led_classdev_unregister(&device->dev, &touchpad_led); | |
destroy_workqueue(lge_touchpad_led_wq); | |
lge_wmi_input_destroy(); | |
return 0; | |
} | |
static struct platform_driver lge_platform_driver = { | |
.driver = { | |
.name = LGE_LAPTOP_FILE, | |
}, | |
.remove = lge_wmi_platform_remove, | |
}; | |
static acpi_status __init | |
check_acpi_dev(acpi_handle handle, u32 lvl, void *context, void **rv) | |
{ | |
const struct acpi_device_id *ids = context; | |
struct acpi_device *dev; | |
if (acpi_bus_get_device(handle, &dev) != 0) | |
return AE_OK; | |
if (acpi_match_device_ids(dev, ids) == 0) | |
if (acpi_create_platform_device(dev, NULL)) | |
dev_info(&dev->dev, "created platform device"); | |
return AE_OK; | |
} | |
static int lge_wmi_enabled = 0, lge_airp_enabled = 0; | |
static int __init lge_wmi_init(void) | |
{ | |
if (!wmi_has_guid(LGE_WMI_GUID1)) { | |
return -ENODEV; | |
} | |
acpi_walk_namespace(ACPI_TYPE_DEVICE, ACPI_ROOT_OBJECT, | |
ACPI_UINT32_MAX, check_acpi_dev, NULL, | |
(void *)lge_airp_ids, NULL); | |
lge_platform_device = platform_create_bundle(&lge_platform_driver, | |
lge_wmi_platform_probe, | |
NULL, 0, NULL, 0); | |
lge_wmi_enabled = !IS_ERR(lge_platform_device); | |
lge_airp_enabled = !platform_driver_register(&lge_airp_pl_driver); | |
if (!lge_wmi_enabled && !lge_airp_enabled) | |
return -ENODEV; | |
return 0; | |
} | |
void lge_wmi_exit(void) | |
{ | |
if (lge_airp_enabled) | |
platform_driver_unregister(&lge_airp_pl_driver); | |
if (lge_wmi_enabled) { | |
platform_device_unregister(lge_platform_device); | |
platform_driver_unregister(&lge_platform_driver); | |
} | |
} | |
module_init(lge_wmi_init); | |
module_exit(lge_wmi_exit); | |
MODULE_LICENSE("GPL"); | |
MODULE_AUTHOR("Jin-oh Kang <[email protected]>"); | |
MODULE_DESCRIPTION("some LG laptop stuff"); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment