Re: Add INPUT support to toshiba_acpi

From: Richard Hughes
Date: Thu May 31 2007 - 09:45:00 EST


On Thu, 2007-05-31 at 13:53 +0100, Bastien Nocera wrote:
> On Thu, 2007-05-31 at 13:36 +0100, Richard Hughes wrote:
> > Attached patch adds a kernel thread to do polling on Toshiba hardware.
> >
> > Toshiba hardware is a little oddball, and does not provide ACPI events
> > on some key presses, typically Fn hotkey buttons. The key interface is
> > now polled, and events now matched to a list of toshiba specific
> > scancodes, and are squirted to userspace using the INPUT subsystem.
> >
> > This means that toshiba laptops buttons "just work" without any
> > userspace daemon (using uinput) such as fnfx or bodges such as using a
> > userspace hal addon. Doing the polling in kernel is more efficient, and
> > makes stuff just work out of the box. You can assign the keys using
> > standard X keymaps, or using tools such as gnome-keybinding-properties.
> >
> > This is similar to other patches sent for the thinkpad_acpi driver, and
> > is part of the "Unf*ck my keyboard" initiative to make multimedia keys
> > just work.
> >
> > Changes from the first patch involve switching to a workqueue for the
> > polling, not breaking the spaces in "hotkeys_via_input" and also masking
> > out the fn key button up.
>
> A couple of things:
> - use a switch statement in toshiba_deliver_button_event(), would look a
> bit cleaner.

Agree, done.

> - make the magic number #defines

Agree, done.

> - not sure if it's possible, but you should disable the workqueue
> altogether if nothing has the input device opened.

Do we get an event when the [input]->users value changes? If not, we
could do 1Hz (users > 0) checking when the input device is unclaimed,
and then switch to 4Hz polling when the input device is open.

> Hopefully we'll find a way to receive an interrupt, or some kind of
> event when the keys are pressed in the future, and completely avoid the
> polling. In the meantime, it should be minimised.

Nope, impossible AFAICS. The hardware is just broken. Windows XP has an
toshiba supplied daemon that polls, so I think we have to just bite the
bullet.

New patch attached.

Richard.

diff --git a/drivers/acpi/toshiba_acpi.c b/drivers/acpi/toshiba_acpi.c
index 3906d47..cf4ab76 100644
--- a/drivers/acpi/toshiba_acpi.c
+++ b/drivers/acpi/toshiba_acpi.c
@@ -27,13 +27,13 @@
* engineering the Windows drivers
* Yasushi Nagato - changes for linux kernel 2.4 -> 2.5
* Rob Miller - TV out and hotkeys help
- *
+ * Richard Hughes - emit INPUT events, based on a patch from Daniel Silverstone
*
* TODO
*
*/

-#define TOSHIBA_ACPI_VERSION "0.18"
+#define TOSHIBA_ACPI_VERSION "0.19"
#define PROC_INTERFACE_VERSION 1

#include <linux/kernel.h>
@@ -42,6 +42,8 @@
#include <linux/types.h>
#include <linux/proc_fs.h>
#include <linux/backlight.h>
+#include <linux/input.h>
+#include <linux/workqueue.h>

#include <asm/uaccess.h>

@@ -99,6 +101,16 @@ MODULE_LICENSE("GPL");
#define HCI_VIDEO_OUT_CRT 0x2
#define HCI_VIDEO_OUT_TV 0x4

+/* key definitions */
+#define HCI_HKEY_MUTE 0x0101
+#define HCI_HKEY_BREAK 0x013b
+#define HCI_HKEY_SEARCH 0x013c
+#define HCI_HKEY_SUSPEND 0x013d
+#define HCI_HKEY_HIBERNATE 0x013e
+#define HCI_HKEY_BRIGHTNESSDOWN 0x0140
+#define HCI_HKEY_BRIGHTNESSUP 0x0141
+#define HCI_HKEY_WLAN 0x0142
+
/* utility
*/

@@ -213,9 +225,15 @@ static acpi_status hci_read1(u32 reg, u32 * out1, u32 * result)

static struct proc_dir_entry *toshiba_proc_dir /*= 0*/ ;
static struct backlight_device *toshiba_backlight_device;
+static struct input_dev *toshiba_input;
static int force_fan;
static int last_key_event;
static int key_event_valid;
+static int hotkeys_over_input = 1;
+static int hotkeys_check_per_sec = 2;
+
+module_param(hotkeys_over_input, uint, 0400);
+module_param(hotkeys_check_per_sec, uint, 0400);

typedef struct _ProcItem {
const char *name;
@@ -443,27 +461,33 @@ static char *read_keys(char *p)
u32 hci_result;
u32 value;

- if (!key_event_valid) {
- hci_read1(HCI_SYSTEM_EVENT, &value, &hci_result);
- if (hci_result == HCI_SUCCESS) {
- key_event_valid = 1;
- last_key_event = value;
- } else if (hci_result == HCI_EMPTY) {
- /* better luck next time */
- } else if (hci_result == HCI_NOT_SUPPORTED) {
- /* This is a workaround for an unresolved issue on
- * some machines where system events sporadically
- * become disabled. */
- hci_write1(HCI_SYSTEM_EVENT, 1, &hci_result);
- printk(MY_NOTICE "Re-enabled hotkeys\n");
- } else {
- printk(MY_ERR "Error reading hotkey status\n");
- goto end;
+ if (!hotkeys_over_input) {
+ if (!key_event_valid) {
+ hci_read1(HCI_SYSTEM_EVENT, &value, &hci_result);
+ if (hci_result == HCI_SUCCESS) {
+ key_event_valid = 1;
+ last_key_event = value;
+ } else if (hci_result == HCI_EMPTY) {
+ /* better luck next time */
+ } else if (hci_result == HCI_NOT_SUPPORTED) {
+ /* This is a workaround for an unresolved issue on
+ * some machines where system events sporadically
+ * become disabled. */
+ hci_write1(HCI_SYSTEM_EVENT, 1, &hci_result);
+ printk(MY_NOTICE "Re-enabled hotkeys\n");
+ } else {
+ printk(MY_ERR "Error reading hotkey status\n");
+ goto end;
+ }
}
+ } else {
+ key_event_valid = 0;
+ last_key_event = 0;
}

p += sprintf(p, "hotkey_ready: %d\n", key_event_valid);
p += sprintf(p, "hotkey: 0x%04x\n", last_key_event);
+ p += sprintf(p, "hotkeys_via_input: %d\n", hotkeys_over_input);

end:
return p;
@@ -534,15 +558,133 @@ static acpi_status __exit remove_device(void)
}

static struct backlight_ops toshiba_backlight_data = {
- .get_brightness = get_lcd,
- .update_status = set_lcd_status,
+ .get_brightness = get_lcd,
+ .update_status = set_lcd_status,
};

+static void toshiba_keys_fire(struct work_struct *work);
+static struct workqueue_struct *toshiba_keys_wq;
+static DECLARE_DELAYED_WORK(toshiba_keys_work, toshiba_keys_fire);
+
+static void toshiba_deliver_button_event(u32 value)
+{
+ int keycode = KEY_UNKNOWN;
+ int key_down = 1;
+
+ if (!toshiba_input)
+ return;
+
+ /* translate MSB to key up */
+ if (value & 0x80) {
+ value &= ~0x80;
+ key_down = 0;
+ }
+
+ /* ignore FN on its own */
+ if (value == 0x0100)
+ return;
+
+ /* translate keys to keycodes */
+ switch (value) {
+ case HCI_HKEY_MUTE:
+ keycode = KEY_MUTE;
+ break;
+ case HCI_HKEY_BREAK:
+ keycode = KEY_BREAK;
+ break;
+ case HCI_HKEY_SEARCH:
+ keycode = KEY_SEARCH;
+ break;
+ case HCI_HKEY_SUSPEND:
+ keycode = KEY_SLEEP;
+ break;
+ case HCI_HKEY_HIBERNATE:
+ keycode = KEY_SUSPEND;
+ break;
+ case HCI_HKEY_BRIGHTNESSDOWN:
+ keycode = KEY_BRIGHTNESSDOWN;
+ break;
+ case HCI_HKEY_BRIGHTNESSUP:
+ keycode = KEY_BRIGHTNESSUP;
+ break;
+ case HCI_HKEY_WLAN:
+ keycode = KEY_WLAN;
+ break;
+ default:
+ keycode = KEY_UNKNOWN;
+ }
+
+ if (keycode != KEY_UNKNOWN) {
+ printk(MY_INFO "mapped hkey %i to keycode %i [%i]\n", value, keycode, key_down);
+ input_report_key(toshiba_input, keycode, key_down);
+ input_sync(toshiba_input);
+ }
+}
+
+static void toshiba_keys_clear(void)
+{
+ int dropped = 0;
+ int clear_queue = 0;
+ u32 hci_result, value;
+
+ do {
+ /* We don't want to get stuck here; older toshibas such as the
+ * A60 may load and then return junk during the hci_read */
+ if (clear_queue++ > 16)
+ break;
+
+ hci_read1(HCI_SYSTEM_EVENT, &value, &hci_result);
+ if (hci_result == HCI_SUCCESS) {
+ dropped++;
+ } else if (hci_result == HCI_EMPTY) {
+ /* better luck next time */
+ } else if (hci_result == HCI_NOT_SUPPORTED) {
+ /* This is a workaround for an unresolved issue on
+ * some machines where system events sporadically
+ * become disabled. */
+ hci_write1(HCI_SYSTEM_EVENT, 1, &hci_result);
+ printk(MY_NOTICE "Re-enabled hotkeys\n");
+ }
+ } while (hci_result != HCI_EMPTY);
+
+ printk(MY_INFO "Dropped %d keys from the queue on startup\n", dropped);
+}
+
+static void toshiba_keys_fire(struct work_struct *work)
+{
+ u32 hci_result, value;
+
+ do {
+ hci_read1(HCI_SYSTEM_EVENT, &value, &hci_result);
+ if (hci_result == HCI_SUCCESS) {
+ /* we got a button event */
+ toshiba_deliver_button_event(value);
+ } else if (hci_result == HCI_EMPTY) {
+ /* better luck next time */
+ } else if (hci_result == HCI_NOT_SUPPORTED) {
+ /* This is a workaround for an
+ * unresolved issue on some machines
+ * where system events sporadically
+ * become disabled. */
+ hci_write1(HCI_SYSTEM_EVENT, 1, &hci_result);
+ printk(MY_NOTICE "Re-enabled hotkeys\n");
+ }
+ } while (hci_result == HCI_SUCCESS);
+
+ /* reschedule another check */
+ queue_delayed_work(toshiba_keys_wq, &toshiba_keys_work, HZ / hotkeys_check_per_sec);
+}
+
static void __exit toshiba_acpi_exit(void)
{
if (toshiba_backlight_device)
backlight_device_unregister(toshiba_backlight_device);

+ if (toshiba_keys_wq) {
+ cancel_delayed_work(&toshiba_keys_work);
+ destroy_workqueue(toshiba_keys_wq);
+ }
+
remove_device();

if (toshiba_proc_dir)
@@ -551,6 +693,45 @@ static void __exit toshiba_acpi_exit(void)
return;
}

+static int __init toshiba_input_init(void)
+{
+ int error;
+
+ /* use INPUT for key events */
+ toshiba_input = input_allocate_device();
+ if (!toshiba_input) {
+ error = -ENOMEM;
+ goto err_no_mem;
+ }
+
+ toshiba_keys_clear();
+
+ /* create one 'keyboard' virtual input device for all the acpi events */
+ toshiba_input->name = "Toshiba Extra Buttons";
+ toshiba_input->phys = "toshiba/input0";
+ toshiba_input->id.bustype = BUS_HOST;
+ toshiba_input->id.product = 0x0001;
+ toshiba_input->evbit[0] = BIT(EV_KEY);
+ set_bit(KEY_MUTE, toshiba_input->keybit);
+ set_bit(KEY_BREAK, toshiba_input->keybit); /* probably should be KEY_LOCK */
+ set_bit(KEY_SEARCH, toshiba_input->keybit);
+ set_bit(KEY_SUSPEND, toshiba_input->keybit);
+ set_bit(KEY_SLEEP, toshiba_input->keybit);
+ set_bit(KEY_BRIGHTNESSDOWN, toshiba_input->keybit);
+ set_bit(KEY_BRIGHTNESSUP, toshiba_input->keybit);
+ set_bit(KEY_WLAN, toshiba_input->keybit);
+
+ error = input_register_device(toshiba_input);
+ if (error)
+ goto err_free_input;
+ return 0;
+
+err_free_input:
+ input_free_device(toshiba_input);
+err_no_mem:
+ return error;
+}
+
static int __init toshiba_acpi_init(void)
{
acpi_status status = AE_OK;
@@ -590,12 +771,38 @@ static int __init toshiba_acpi_init(void)
toshiba_backlight_device = backlight_device_register("toshiba",NULL,
NULL,
&toshiba_backlight_data);
- if (IS_ERR(toshiba_backlight_device)) {
+ if (IS_ERR(toshiba_backlight_device)) {
printk(KERN_ERR "Could not register toshiba backlight device\n");
toshiba_backlight_device = NULL;
toshiba_acpi_exit();
}
- toshiba_backlight_device->props.max_brightness = HCI_LCD_BRIGHTNESS_LEVELS - 1;
+ toshiba_backlight_device->props.max_brightness = HCI_LCD_BRIGHTNESS_LEVELS - 1;
+
+ /* we have to poll the device as we do not get events */
+ if (hotkeys_over_input && hotkeys_check_per_sec > 0) {
+
+ toshiba_input_init ();
+ /* just abort */
+ if (!toshiba_input) {
+ printk(KERN_ERR "could not allocate input device\n");
+ toshiba_acpi_exit();
+ }
+
+ /* setup workqueue */
+ toshiba_keys_wq = create_singlethread_workqueue("ktoshkeyd");
+ if (!toshiba_keys_wq) {
+ printk(KERN_ERR "failed to create workqueue\n");
+ toshiba_acpi_exit();
+ }
+
+ /* sanitise to something sane */
+ if (hotkeys_check_per_sec > 10)
+ hotkeys_check_per_sec = 10;
+ printk(KERN_INFO "ktoshkeyd checks per second : %d\n", hotkeys_check_per_sec);
+
+ /* start polling after delay */
+ queue_delayed_work(toshiba_keys_wq, &toshiba_keys_work, HZ / hotkeys_check_per_sec);
+ }

return (ACPI_SUCCESS(status)) ? 0 : -ENODEV;
}