[Resend] [PATCH] ACPI: New driver for Lenovo SL laptops

Ike Panhc
2009-10-23T19:12:12Z

lenovo-sl-laptop is a new driver that provides support for hotkeys, bluetooth,
LenovoCare LEDs and fan speed on the Lenovo ThinkPad SL series laptops. The
original author is Alexandre Rostovtsev. [1] In February 2009 Alexandre has
posted the driver on the linux-acpi mailing list and and there was some
feedback suggesting further modifications. [2] I would like to see Linux
working properly on these laptops. I was encouraged to push this driver again
with the modifications that where suggested in the responses to the initial
post in order to allow me and others interested in that driver to improve it
and hopefully get it included upstream.

[1] homepage : http://github.com/tetromino/lenovo-sl-laptop/tree/master
[2] http://patchwork.kernel.org/patch/7427/

Following the suggestions when last time the origin author has posted on the
linux-acpi mailing list. The major modification of this driver is listed below.
- Remove backlight control
- Remove procfs EC debug
- Remove fan control function
- Using generic debugging infrastructure
- Support for lastest rfkill infrastructure (by Alexandre)
- Register query function into EC for detecting hotkey event

Patch against current checkout of linux-acpi 2.6.31 is below.

The major modification of this driver since last time I posted this driver on
linux-acpi mailing list [3] is listed below.
- Dont free input device when exit
- Not to register rfkill on a device which not exist
- Add the poll function on rfkill, I found a way to register a notify function
on H/W radio switch (on EC event with query bit 0x81), but after register,
the H/W radio switch is no longer function to turn off the radio device. I
will keep finding a way to use this event.
- Remove auto_enable parameters of rfkill since we have a hardware switch.
- Using hotkey deivce ids for module alias
- Let the driver simpler for reading

[3] http://patchwork.kernel.org/patch/49912/

=== 8< ===

lenovo-sl-laptop: Extra driver for Lenovo SL series laptop

This driver provides support for the following functions.
- Hotkeys: LenovoCare, Volumn up/down/mute, Battery, Suspend, WLAN switch,
Video switch, Pointer switch (as KEY_PROG1), Dock eject (as
KEY_PROG2), Hibernate, Lock screen, Screen Zoom and LCD brightness
up/down.
- Radio RFKILL: switching on/off UWB, bluetooth and wifi.
- LenovoCare LEDs: On, off, Dimmed blinking and standard blinking.
(Blinking supported with ledtrig_timer)
- Fan speed: Reading current fan speed

The original author of this driver is Alexandre Rostovtsev

The Lenovo ThinkPad SL series laptops are not supported by the normal
thinkpad_acpi driver because their firmware is quite different from the
T-series/R-series/X-series ThinkPads. [3]

[3] http://mailman.linux-thinkpad.org/pipermail/linux-thinkpad/2009-January/046122.html

Signed-off-by: Ike Panhc <ike.pan@canonical.com>

---
drivers/platform/x86/Kconfig | 12 +
drivers/platform/x86/Makefile | 1 +
drivers/platform/x86/lenovo-sl-laptop.c | 721 +++++++++++++++++++++++++++++++
3 files changed, 734 insertions(+), 0 deletions(-)
create mode 100644 drivers/platform/x86/lenovo-sl-laptop.c

diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig
index 55ca39d..1ae72e3 100644
--- a/drivers/platform/x86/Kconfig
+++ b/drivers/platform/x86/Kconfig
@@ -143,6 +143,18 @@ config HP_WMI
To compile this driver as a module, choose M here: the module will
be called hp-wmi.

+config LENOVO_SL_LAPTOP
+ tristate "Lenovo ThinkPad SL Series Laptop Extras"
+ depends on ACPI
+ select HWMON
+ select INPUT
+ select RFKILL
+ ---help---
+ This is a driver for the Lenovo ThinkPad SL series laptops
+ (SL300/400/500), which are not supported by the thinkpad_acpi
+ driver. This driver adds support for hotkeys, rfkill control,
+ the Lenovo Care LED, fan speed.
+
config MSI_LAPTOP
tristate "MSI Laptop Extras"
depends on ACPI
diff --git a/drivers/platform/x86/Makefile b/drivers/platform/x86/Makefile
index d1c1621..1037739 100644
--- a/drivers/platform/x86/Makefile
+++ b/drivers/platform/x86/Makefile
@@ -11,6 +11,7 @@ obj-$(CONFIG_DELL_WMI) += dell-wmi.o
obj-$(CONFIG_ACER_WMI) += acer-wmi.o
obj-$(CONFIG_ACERHDF) += acerhdf.o
obj-$(CONFIG_HP_WMI) += hp-wmi.o
+obj-$(CONFIG_LENOVO_SL_LAPTOP) += lenovo-sl-laptop.o
obj-$(CONFIG_TC1100_WMI) += tc1100-wmi.o
obj-$(CONFIG_SONY_LAPTOP) += sony-laptop.o
obj-$(CONFIG_THINKPAD_ACPI) += thinkpad_acpi.o
diff --git a/drivers/platform/x86/lenovo-sl-laptop.c b/drivers/platform/x86/lenovo-sl-laptop.c
new file mode 100644
index 0000000..d8fc093
--- /dev/null
+++ b/drivers/platform/x86/lenovo-sl-laptop.c
@@ -0,0 +1,721 @@
+/*
+ * lenovo-sl-laptop.c - Lenovo ThinkPad SL Series Extras Driver
+ *
+ *
+ * Copyright (C) 2008-2009 Alexandre Rostovtsev <tetromino@gmail.com>
+ * 2009 Ike Panhc <ike.pan@canonical.com>
+ *
+ * Largely based on thinkpad_acpi.c, eeepc-laptop.c, and video.c which
+ * are copyright their respective authors.
+ *
+ * The original website of this driver is at
+ * http://github.com/tetromino/lenovo-sl-laptop/tree/master
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ *
+ */
+#include <linux/module.h>
+#include <linux/kernel.h>
+#include <linux/version.h>
+#include <linux/init.h>
+#include <linux/acpi.h>
+#include <linux/pci_ids.h>
+#include <linux/rfkill.h>
+#include <linux/hwmon.h>
+#include <linux/hwmon-sysfs.h>
+#include <linux/platform_device.h>
+#include <linux/input.h>
+#include <linux/uaccess.h>
+
+#define LENOVO_SL_MODULE_DESC "Lenovo ThinkPad SL Series Extras driver"
+#define LENOVO_SL_MODULE_NAME "lenovo-sl-laptop"
+#define ACPI_EC0_PATH "\\_SB.PCI0.SBRG.EC0"
+#define ACPI_HKEY_PATH ACPI_EC0_PATH ".HKEY"
+#define LENOVO_SL_MAX_ACPI_ARGS 3
+
+MODULE_AUTHOR("Alexandre Rostovtsev");
+MODULE_AUTHOR("Ike Panhc");
+MODULE_DESCRIPTION(LENOVO_SL_MODULE_DESC);
+MODULE_LICENSE("GPL");
+
+/* general */
+
+static acpi_handle lenovo_sl_laptop_hkey_handle;
+static acpi_handle lenovo_sl_laptop_ec0_handle;
+static struct platform_device *lenovo_sl_laptop_pdev;
+
+static int lensl_acpi_int_func(acpi_handle handle, char *pathname,
+ int *ret, int n_arg, ...)
+{
+ acpi_status status;
+ struct acpi_object_list params;
+ union acpi_object in_obj[LENOVO_SL_MAX_ACPI_ARGS], out_obj;
+ struct acpi_buffer result, *resultp;
+ int i;
+ va_list ap;
+
+ if (!handle)
+ return -EINVAL;
+ if (n_arg < 0 || n_arg > LENOVO_SL_MAX_ACPI_ARGS)
+ return -EINVAL;
+ va_start(ap, n_arg);
+ for (i = 0; i < n_arg; i++) {
+ in_obj[i].integer.value = va_arg(ap, int);
+ in_obj[i].type = ACPI_TYPE_INTEGER;
+ }
+ va_end(ap);
+ params.count = n_arg;
+ params.pointer = in_obj;
+
+ if (ret) {
+ result.length = sizeof(out_obj);
+ result.pointer = &out_obj;
+ resultp = &result;
+ } else
+ resultp = NULL;
+
+ status = acpi_evaluate_object(handle, pathname, &params, resultp);
+ if (ACPI_FAILURE(status))
+ return -EIO;
+ if (ret)
+ *ret = out_obj.integer.value;
+
+ return 0;
+}
+
+/*************************************************************************
+ Bluetooth, WWAN, UWB
+ *************************************************************************/
+
+/* ACPI GBDC/SBDC, GWAN/SWAN, GUWB/SUWB bits */
+#define LENOVO_SL_RADIO_HWPRESENT (0x01) /* hardware is available */
+#define LENOVO_SL_RADIO_RADIOSSW (0x02) /* radio is enabled */
+#define LENOVO_SL_RADIO_RESUMECTRL (0x04) /* state at resume: off/last state */
+
+struct lensl_radio {
+ int type;
+ enum rfkill_type rfktype;
+ char *rfkname;
+ struct rfkill *rfk;
+ char *get_pathname;
+ char *set_pathname;
+};
+
+static int radio_get_acpi(char *pathname, int *value)
+{
+ return lensl_acpi_int_func(lenovo_sl_laptop_hkey_handle, pathname,
+ value, 0);
+}
+
+static int radio_set_acpi(char *pathname, int value)
+{
+ return lensl_acpi_int_func(lenovo_sl_laptop_hkey_handle, pathname,
+ NULL, 1, value);
+}
+
+static int radio_get(struct lensl_radio *radio, bool *sw_blocked,
+ bool *hw_blocked)
+{
+ int wlsw;
+ int value;
+
+ if (!radio)
+ return -EINVAL;
+ if (!radio_get_acpi("WLSW", &wlsw) && wlsw)
+ *hw_blocked = 0;
+ else
+ *hw_blocked = 1;
+ if (radio_get_acpi(radio->get_pathname, &value))
+ return -ENODEV;
+ if (!(value & LENOVO_SL_RADIO_HWPRESENT))
+ return -ENODEV;
+ if (value & LENOVO_SL_RADIO_RADIOSSW)
+ *sw_blocked = 0;
+ else
+ *sw_blocked = 1;
+ return 0;
+}
+
+static int radio_set(struct lensl_radio *radio, bool blocked)
+{
+ int res, value;
+
+ res = radio_get_acpi(radio->get_pathname, &value);
+ if (res)
+ return res;
+
+ if (blocked)
+ value &= ~LENOVO_SL_RADIO_RADIOSSW;
+ else
+ value |= LENOVO_SL_RADIO_RADIOSSW;
+ if (radio_set_acpi(radio->set_pathname, value))
+ return -EIO;
+
+ return 0;
+}
+
+/* Bluetooth/WWAN/UWB rfkill interface */
+
+static void radio_rfkill_query(struct rfkill *rfk, void *data)
+{
+ struct lensl_radio *radio = data;
+ int res;
+ bool sw_blocked, hw_blocked;
+
+ if (!radio)
+ return;
+
+ res = radio_get(radio, &sw_blocked, &hw_blocked);
+ if (res)
+ return;
+
+ rfkill_set_states(rfk, sw_blocked, hw_blocked);
+}
+
+static int radio_rfkill_set_block(void *data, bool blocked)
+{
+ struct lensl_radio *radio = data;
+ int res;
+ bool sw_blocked, hw_blocked;
+
+ if (!radio)
+ return -EINVAL;
+
+ res = radio_get(radio, &sw_blocked, &hw_blocked);
+ if (res)
+ return res;
+
+ if (hw_blocked)
+ return 0;
+ if (sw_blocked == blocked)
+ return 0;
+
+ return radio_set(radio, sw_blocked);
+}
+
+static struct rfkill_ops radio_rfkops = {
+ .poll = radio_rfkill_query,
+ .query = radio_rfkill_query,
+ .set_block = radio_rfkill_set_block,
+};
+
+/* Bluetooth/WWAN/UWB init and exit and HW switch notification */
+
+static struct lensl_radio radio_radios[3] = {
+#define RADIO_BLUETOOTH (0)
+ {
+ .type = RADIO_BLUETOOTH,
+ .rfktype = RFKILL_TYPE_BLUETOOTH,
+ .rfkname = "lenovo-sl-bluetooth",
+ .get_pathname = "GBDC",
+ .set_pathname = "SBDC",
+ },
+#define RADIO_WWAN (1)
+ {
+ .type = RADIO_WWAN,
+ .rfktype = RFKILL_TYPE_WWAN,
+ .rfkname = "lenovo-sl-wwan",
+ .get_pathname = "GWAN",
+ .set_pathname = "SWAN",
+ },
+#define RADIO_UWB (2)
+ {
+ .type = RADIO_UWB,
+ .rfktype = RFKILL_TYPE_UWB,
+ .rfkname = "lenovo-sl-uwb",
+ .get_pathname = "GUWB",
+ .set_pathname = "SUWB",
+ },
+};
+
+static void radio_exit(int type)
+{
+ if (radio_radios[type].rfk) {
+ rfkill_unregister(radio_radios[type].rfk);
+ rfkill_destroy(radio_radios[type].rfk);
+ radio_radios[type].rfk = NULL;
+ }
+}
+
+static int radio_init(int type)
+{
+ int res;
+ bool sw_blocked, hw_blocked;
+
+ if (!lenovo_sl_laptop_hkey_handle)
+ return -ENODEV;
+
+ /* 1st: Get the sw/hw status */
+ res = radio_get(&radio_radios[type], &sw_blocked, &hw_blocked);
+ if (res)
+ return res;
+
+ /* 2nd: allocate rfkill */
+ radio_radios[type].rfk = rfkill_alloc(radio_radios[type].rfkname,
+ &lenovo_sl_laptop_pdev->dev,
+ radio_radios[type].rfktype,
+ &radio_rfkops,
+ &radio_radios[type]);
+ if (!(radio_radios[type].rfk)) {
+ pr_err("Failed to allocate memory for rfkill class\n");
+ return -ENOMEM;
+ }
+
+ /* 3rd: Set status */
+ rfkill_init_sw_state(radio_radios[type].rfk, sw_blocked);
+ rfkill_set_hw_state(radio_radios[type].rfk, hw_blocked);
+
+ /* 4th: Register rfkill */
+ res = rfkill_register(radio_radios[type].rfk);
+ if (res < 0) {
+ pr_err("Failed to register %s rfkill switch: %d\n",
+ radio_radios[type].rfkname, res);
+ rfkill_destroy(radio_radios[type].rfk);
+ radio_radios[type].rfk = NULL;
+ }
+
+ return res;
+}
+
+/*************************************************************************
+ LEDs
+ *************************************************************************/
+#ifdef CONFIG_NEW_LEDS
+
+#define LED_OFF 0
+#define LED_ON 0x02
+#define LED_BLINK 0x01
+#define LED_DIM 0x100
+
+/* equivalent to the ThinkVantage LED on other ThinkPads */
+#define LED_NAME "lensl::lenovocare"
+#define LED_WQ_NAME "lenovo-sl-led-wq"
+
+static struct workqueue_struct *led_wq;
+
+struct {
+ struct led_classdev cdev;
+ enum led_brightness brightness;
+ int supported, new_code;
+ struct work_struct work;
+} led_tv;
+
+static inline int led_set_tvls(int code)
+{
+ return lensl_acpi_int_func(lenovo_sl_laptop_hkey_handle, "TVLS", NULL,
+ 1, code);
+}
+
+static void led_tv_worker(struct work_struct *work)
+{
+ if (!led_tv.supported)
+ return;
+ led_set_tvls(led_tv.new_code);
+ if (led_tv.new_code)
+ led_tv.brightness = LED_FULL;
+ else
+ led_tv.brightness = LED_OFF;
+}
+
+static void led_tv_brightness_set_sysfs(struct led_classdev *led_cdev,
+ enum led_brightness brightness)
+{
+ switch (brightness) {
+ case LED_OFF:
+ led_tv.new_code = LED_OFF;
+ break;
+ case LED_FULL:
+ led_tv.new_code = LED_ON;
+ break;
+ default:
+ return;
+ }
+ queue_work(led_wq, &led_tv.work);
+}
+
+static enum led_brightness led_tv_brightness_get_sysfs(
+ struct led_classdev *led_cdev)
+{
+ return led_tv.brightness;
+}
+
+static int led_tv_blink_set_sysfs(struct led_classdev *led_cdev,
+ unsigned long *delay_on,
+ unsigned long *delay_off)
+{
+ if (*delay_on == 0 && *delay_off == 0) {
+ /* If we can choose the flash rate, use dimmed blinking --
+ it looks better */
+ led_tv.new_code = LED_ON |
+ LED_BLINK | LED_DIM;
+ *delay_on = 2000;
+ *delay_off = 2000;
+ } else if (*delay_on + *delay_off == 4000) {
+ /* User wants dimmed blinking */
+ led_tv.new_code = LED_ON |
+ LED_BLINK | LED_DIM;
+ } else if (*delay_on == 7250 && *delay_off == 500) {
+ /* User wants standard blinking mode */
+ led_tv.new_code = LED_ON | LED_BLINK;
+ } else
+ return -EINVAL;
+ queue_work(led_wq, &led_tv.work);
+ return 0;
+}
+
+static void led_exit(void)
+{
+ led_set_tvls(LED_OFF);
+ destroy_workqueue(led_wq);
+ if (led_tv.supported) {
+ led_classdev_unregister(&led_tv.cdev);
+ led_tv.supported = 0;
+ }
+}
+
+static int led_init(void)
+{
+ int res;
+
+ led_wq = create_singlethread_workqueue(LED_WQ_NAME);
+ if (!led_wq) {
+ pr_err("Failed to create a workqueue\n");
+ return -ENOMEM;
+ }
+
+ memset(&led_tv, 0, sizeof(led_tv));
+ led_tv.cdev.brightness_get = led_tv_brightness_get_sysfs;
+ led_tv.cdev.brightness_set = led_tv_brightness_set_sysfs;
+ led_tv.cdev.blink_set = led_tv_blink_set_sysfs;
+ led_tv.cdev.name = LED_NAME;
+ INIT_WORK(&led_tv.work, led_tv_worker);
+ led_set_tvls(LED_ON);
+ res = led_classdev_register(&lenovo_sl_laptop_pdev->dev, &led_tv.cdev);
+ if (res) {
+ pr_warning("Failed to register LED device\n");
+ return res;
+ }
+ led_tv.supported = 1;
+ return 0;
+}
+
+#else /* CONFIG_NEW_LEDS */
+
+static void led_exit(void)
+{
+}
+
+static int led_init(void)
+{
+ return -ENODEV;
+}
+
+#endif /* CONFIG_NEW_LEDS */
+
+/*************************************************************************
+ hwmon & fans
+ *************************************************************************/
+
+static struct device *hwmon_device;
+
+static inline int hwmon_get_tach(int *value, int fan)
+{
+ return lensl_acpi_int_func(lenovo_sl_laptop_ec0_handle, "TACH", value,
+ 1, fan);
+}
+
+static ssize_t hwmon_fan1_input_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ int res;
+ int rpm;
+
+ res = hwmon_get_tach(&rpm, 0);
+ if (res)
+ return res;
+ return snprintf(buf, PAGE_SIZE, "%u\n", rpm);
+}
+
+static struct device_attribute hwmon_fan1_input =
+ __ATTR(fan1_input, S_IRUGO, hwmon_fan1_input_show, NULL);
+
+static struct attribute *hwmon_attributes[] = {
+ &hwmon_fan1_input.attr,
+ NULL
+};
+
+static const struct attribute_group hwmon_attr_group = {
+ .attrs = hwmon_attributes,
+};
+
+static void hwmon_exit(void)
+{
+ if (!hwmon_device)
+ return;
+
+ sysfs_remove_group(&hwmon_device->kobj, &hwmon_attr_group);
+ hwmon_device_unregister(hwmon_device);
+ hwmon_device = NULL;
+}
+
+static int hwmon_init(void)
+{
+ int res;
+
+ hwmon_device = hwmon_device_register(&lenovo_sl_laptop_pdev->dev);
+ if (!hwmon_device) {
+ pr_err("Failed to register hwmon device\n");
+ return -ENODEV;
+ }
+
+ res = sysfs_create_group(&hwmon_device->kobj, &hwmon_attr_group);
+ if (res < 0) {
+ pr_err("Failed to create hwmon sysfs group\n");
+ hwmon_device_unregister(hwmon_device);
+ hwmon_device = NULL;
+ return -ENODEV;
+ }
+ return 0;
+}
+
+/*************************************************************************
+ hotkeys
+ *************************************************************************/
+
+typedef int (*acpi_ec_query_func) (void *data);
+extern int acpi_ec_add_query_handler(void *ec, u8 query_bit,
+ acpi_handle handle,
+ acpi_ec_query_func func,
+ void *data);
+extern void acpi_ec_remove_query_handler(void *ec, u8 query_bit);
+
+struct key_entry {
+ char type;
+ u8 scancode;
+ int keycode;
+};
+
+enum { KE_KEY, KE_END };
+
+static struct input_dev *hkey_inputdev;
+
+static struct key_entry hkey_keymap[] = {
+ {KE_KEY, 0x0B, KEY_COFFEE },
+ {KE_KEY, 0x0C, KEY_BATTERY },
+ {KE_KEY, 0x0D, KEY_SLEEP },
+ {KE_KEY, 0x0E, KEY_WLAN },
+ {KE_KEY, 0x10, KEY_SWITCHVIDEOMODE },
+ {KE_KEY, 0x11, KEY_PROG1 },
+ {KE_KEY, 0x12, KEY_PROG2 },
+ {KE_KEY, 0x15, KEY_SUSPEND },
+ {KE_KEY, 0x69, KEY_VOLUMEUP },
+ {KE_KEY, 0x6A, KEY_VOLUMEDOWN },
+ {KE_KEY, 0x6B, KEY_MUTE },
+ {KE_KEY, 0x6C, KEY_BRIGHTNESSDOWN },
+ {KE_KEY, 0x6D, KEY_BRIGHTNESSUP },
+ {KE_KEY, 0x71, KEY_ZOOM },
+ {KE_KEY, 0x80, KEY_VENDOR },
+ {KE_END, 0},
+};
+
+static int hkey_action(void *data)
+{
+ int keycode;
+ struct key_entry *this_key = data;
+
+ if (!data)
+ return -EINVAL;
+ keycode = this_key->keycode;
+
+ if (keycode != KEY_RESERVED) {
+ input_report_key(hkey_inputdev, keycode, 1);
+ input_sync(hkey_inputdev);
+ input_report_key(hkey_inputdev, keycode, 0);
+ input_sync(hkey_inputdev);
+ }
+
+ return 0;
+}
+
+static int hkey_add(struct acpi_device *device)
+{
+ int result;
+ struct key_entry *key;
+
+ for (key = hkey_keymap; key->type != KE_END; key++) {
+ result = acpi_ec_add_query_handler(
+ acpi_driver_data(device->parent),
+ key->scancode, NULL,
+ hkey_action, key);
+ if (result) {
+ pr_err("Failed to register hotkey notification.\n");
+ return -ENODEV;
+ }
+ }
+ return 0;
+}
+
+static int hkey_remove(struct acpi_device *device, int type)
+{
+ struct key_entry *key;
+
+ for (key = hkey_keymap; key->type != KE_END; key++) {
+ acpi_ec_remove_query_handler(
+ acpi_driver_data(device->parent),
+ key->scancode);
+ }
+ return 0;
+}
+
+static const struct acpi_device_id hkey_ids[] = {
+ {"LEN0014", 0},
+ {"", 0},
+};
+
+static struct acpi_driver hkey_driver = {
+ .name = "lenovo-sl-laptop-hotkey",
+ .class = "lenovo",
+ .ids = hkey_ids,
+ .owner = THIS_MODULE,
+ .ops = {
+ .add = hkey_add,
+ .remove = hkey_remove,
+ },
+};
+
+static void hkey_inputdev_exit(void)
+{
+ if (hkey_inputdev)
+ input_unregister_device(hkey_inputdev);
+}
+
+static int hkey_inputdev_init(void)
+{
+ int result;
+ struct key_entry *key;
+
+ hkey_inputdev = input_allocate_device();
+ if (!hkey_inputdev) {
+ pr_err("Failed to allocate hotkey input device\n");
+ return -ENODEV;
+ }
+ hkey_inputdev->name = "Lenovo ThinkPad SL Series extra buttons";
+ hkey_inputdev->phys = LENOVO_SL_MODULE_NAME "/input0";
+ hkey_inputdev->uniq = LENOVO_SL_MODULE_NAME;
+ hkey_inputdev->id.bustype = BUS_HOST;
+ hkey_inputdev->id.vendor = PCI_VENDOR_ID_LENOVO;
+ hkey_inputdev->dev.parent = &lenovo_sl_laptop_pdev->dev;
+ set_bit(EV_KEY, hkey_inputdev->evbit);
+
+ for (key = hkey_keymap; key->type != KE_END; key++)
+ set_bit(key->keycode, hkey_inputdev->keybit);
+
+ result = input_register_device(hkey_inputdev);
+ if (result) {
+ pr_err("Failed to register hotkey input device\n");
+ input_free_device(hkey_inputdev);
+ hkey_inputdev = NULL;
+ return -ENODEV;
+ }
+ return 0;
+}
+
+static void hkey_init(void)
+{
+ int result;
+
+ result = hkey_inputdev_init();
+ if (result) {
+ pr_err("Failed to register input device for hotkeys\n");
+ return;
+ }
+ result = acpi_bus_register_driver(&hkey_driver);
+ if (result)
+ pr_err("Failed to register hotkey driver\n");
+ return;
+}
+
+static void hkey_exit(void)
+{
+ hkey_inputdev_exit();
+ acpi_bus_unregister_driver(&hkey_driver);
+}
+
+/*************************************************************************
+ init/exit
+ *************************************************************************/
+
+static int __init lenovo_sl_laptop_init(void)
+{
+ int ret;
+ acpi_status status;
+
+ if (acpi_disabled)
+ return -ENODEV;
+
+ lenovo_sl_laptop_hkey_handle = lenovo_sl_laptop_ec0_handle = NULL;
+ status = acpi_get_handle(NULL, ACPI_HKEY_PATH,
+ &lenovo_sl_laptop_hkey_handle);
+ if (ACPI_FAILURE(status)) {
+ pr_err("Failed to get ACPI handle for %s\n", ACPI_HKEY_PATH);
+ return -ENODEV;
+ }
+ status = acpi_get_handle(NULL, ACPI_EC0_PATH,
+ &lenovo_sl_laptop_ec0_handle);
+ if (ACPI_FAILURE(status)) {
+ pr_err("Failed to get ACPI handle for %s\n", ACPI_EC0_PATH);
+ return -ENODEV;
+ }
+
+ lenovo_sl_laptop_pdev = platform_device_register_simple(
+ LENOVO_SL_MODULE_NAME,
+ -1, NULL, 0);
+ if (IS_ERR(lenovo_sl_laptop_pdev)) {
+ ret = PTR_ERR(lenovo_sl_laptop_pdev);
+ lenovo_sl_laptop_pdev = NULL;
+ pr_err("Failed to register platform device\n");
+ return ret;
+ }
+
+ radio_init(RADIO_BLUETOOTH);
+ radio_init(RADIO_WWAN);
+ radio_init(RADIO_UWB);
+
+ hkey_init();
+ led_init();
+ hwmon_init();
+
+ return 0;
+}
+
+static void __exit lenovo_sl_laptop_exit(void)
+{
+ hwmon_exit();
+ led_exit();
+ hkey_exit();
+
+ radio_exit(RADIO_UWB);
+ radio_exit(RADIO_WWAN);
+ radio_exit(RADIO_BLUETOOTH);
+
+ if (lenovo_sl_laptop_pdev)
+ platform_device_unregister(lenovo_sl_laptop_pdev);
+}
+
+MODULE_DEVICE_TABLE(acpi, hkey_ids);
+
+module_init(lenovo_sl_laptop_init);
+module_exit(lenovo_sl_laptop_exit);
--
1.6.3.3