Re: Add INPUT support to toshiba_acpi

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



On 6/7/07, Richard Hughes <[email protected]> wrote:
On Sun, 2007-06-03 at 00:48 -0400, Dmitry Torokhov wrote:
>
>
> It looks like KEY_COFFE comes from 0x0c/0x19e - AL Terminal
> Lock/Screensaver
> so your interpretation is indeed correct. I guess I better add an
> alias to
> input.h

What's the status of this patch? Good for merging?

Not my call - John is the official maintainer of the driver. However
from input layer POV it looks good.

Do you want me to
redo the current patch using input-polldev

The patch I send to you earlier already does this. I am attaching a
refreshed version that used KEY_COFFEE instead of KEY_BREAK.

and the new setkeycode stuff?

And I have the patch doing this as well (attached). If you could
please give it a try I'd appreciate it.

--
Dmitry
From: Richard Hughes <[email protected]>
Subject: toshiba_acpi: integrate with INPUT layer

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.

Signed-off-by: Dmitry Torokhov <[email protected]>
---
 Documentation/kernel-parameters.txt |   11 +
 drivers/acpi/Kconfig                |    1 
 drivers/acpi/toshiba_acpi.c         |  333 ++++++++++++++++++++++++++++--------
 3 files changed, 278 insertions(+), 67 deletions(-)

Index: linux/drivers/acpi/Kconfig
===================================================================
--- linux.orig/drivers/acpi/Kconfig
+++ linux/drivers/acpi/Kconfig
@@ -222,6 +222,7 @@ config ACPI_TOSHIBA
 	tristate "Toshiba Laptop Extras"
 	depends on X86
 	select BACKLIGHT_CLASS_DEVICE
+	select INPUT_POLLDEV
 	---help---
 	  This driver adds support for access to certain system settings
 	  on "legacy free" Toshiba laptops.  These laptops can be recognized by
Index: linux/drivers/acpi/toshiba_acpi.c
===================================================================
--- linux.orig/drivers/acpi/toshiba_acpi.c
+++ linux/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 for hotkeys
  *
  *  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,7 @@
 #include <linux/types.h>
 #include <linux/proc_fs.h>
 #include <linux/backlight.h>
+#include <linux/input-polldev.h>
 
 #include <asm/uaccess.h>
 
@@ -55,6 +56,7 @@ MODULE_LICENSE("GPL");
 #define MY_ERR KERN_ERR MY_LOGPREFIX
 #define MY_NOTICE KERN_NOTICE MY_LOGPREFIX
 #define MY_INFO KERN_INFO MY_LOGPREFIX
+#define MY_DEBUG KERN_DEBUG MY_LOGPREFIX
 
 /* Toshiba ACPI method paths */
 #define METHOD_LCD_BRIGHTNESS	"\\_SB_.PCI0.VGA_.LCD_._BCM"
@@ -99,6 +101,17 @@ MODULE_LICENSE("GPL");
 #define HCI_VIDEO_OUT_CRT		0x2
 #define HCI_VIDEO_OUT_TV		0x4
 
+/* key definitions */
+#define HCI_HKEY_FN			0x0100
+#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,10 +226,21 @@ static acpi_status hci_read1(u32 reg, u3
 
 static struct proc_dir_entry *toshiba_proc_dir /*= 0*/ ;
 static struct backlight_device *toshiba_backlight_device;
+static struct input_polled_dev *toshiba_poll_dev;
 static int force_fan;
 static int last_key_event;
 static int key_event_valid;
 
+static int hotkeys_over_input = 1;
+module_param(hotkeys_over_input, bool, 0444);
+MODULE_PARM_DESC(hotkeys_over_input,
+		"Enable delivery of hotkey events via input layer.");
+
+static int hotkeys_poll_interval = 500;	/* msecs */
+module_param(hotkeys_poll_interval, uint, 0444);
+MODULE_PARM_DESC(hotkeys_poll_interval,
+		"How often to poll hotkey state (default is 500 msec)");
+
 typedef struct _ProcItem {
 	const char *name;
 	char *(*read_func) (char *);
@@ -438,32 +462,47 @@ static unsigned long write_fan(const cha
 	return count;
 }
 
+static u32 hci_poll_keys_once(u32 *value)
+{
+	u32 hci_result;
+
+	hci_read1(HCI_SYSTEM_EVENT, value, &hci_result);
+	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_DEBUG "Re-enabled hotkeys\n");
+		hci_result = HCI_EMPTY;
+	}
+
+	return hci_result;
+}
+
 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_result = hci_poll_keys_once(&value);
+			if (hci_result == HCI_SUCCESS) {
+				key_event_valid = 1;
+				last_key_event = value;
+			} else if (hci_result != HCI_EMPTY) {
+				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;
@@ -501,60 +540,204 @@ static ProcItem proc_items[] = {
 	{"fan", read_fan, write_fan},
 	{"keys", read_keys, write_keys},
 	{"version", read_version, NULL},
-	{NULL}
 };
 
-static acpi_status __init add_device(void)
+static int __init add_proc_entries(void)
 {
 	struct proc_dir_entry *proc;
 	ProcItem *item;
+	int mode;
+	int i;
+
+	toshiba_proc_dir = proc_mkdir(PROC_TOSHIBA, acpi_root_dir);
+	if (!toshiba_proc_dir) {
+		printk(MY_ERR "failed to create toshiba proc directory\n");
+		goto err_out;
+	}
+
+	toshiba_proc_dir->owner = THIS_MODULE;
+
+	for (i = 0; i < ARRAY_SIZE(proc_items); i++) {
+		item = &proc_items[i];
 
-	for (item = proc_items; item->name; ++item) {
-		proc = create_proc_read_entry(item->name,
-					      S_IFREG | S_IRUGO | S_IWUSR,
-					      toshiba_proc_dir,
-					      (read_proc_t *) dispatch_read,
-					      item);
-		if (proc)
-			proc->owner = THIS_MODULE;
-		if (proc && item->write_func)
+		mode = S_IFREG | S_IRUGO;
+		if (item->write_func)
+			mode = S_IWUSR;
+
+		proc = create_proc_entry(item->name, mode, toshiba_proc_dir);
+		if (!proc) {
+			printk(MY_ERR "failed to create %s proc entry\n",
+				item->name);
+			goto err_remove_proc;
+		}
+
+		proc->owner = THIS_MODULE;
+		proc->data = item;
+		proc->read_proc = (read_proc_t *) dispatch_read;
+		if (item->write_func)
 			proc->write_proc = (write_proc_t *) dispatch_write;
 	}
 
-	return AE_OK;
+	return 0;
+
+ err_remove_proc:
+	while (-- i >= 0)
+		remove_proc_entry(proc_items[i].name, toshiba_proc_dir);
+	remove_proc_entry(PROC_TOSHIBA, acpi_root_dir);
+ err_out:
+	return -EBUSY; /* arbitrary */
 }
 
-static acpi_status __exit remove_device(void)
+static void __exit remove_proc_entries(void)
 {
-	ProcItem *item;
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(proc_items); i++)
+		remove_proc_entry(proc_items[i].name, toshiba_proc_dir);
 
-	for (item = proc_items; item->name; ++item)
-		remove_proc_entry(item->name, toshiba_proc_dir);
-	return AE_OK;
+	remove_proc_entry(PROC_TOSHIBA, acpi_root_dir);
 }
 
 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_acpi_exit(void)
+static void toshiba_deliver_button_event(struct input_dev *input, u32 value)
 {
-	if (toshiba_backlight_device)
-		backlight_device_unregister(toshiba_backlight_device);
+	int keycode;
+	int key_down;
 
-	remove_device();
+	/* translate MSB to key up */
+	key_down = !(value & 0x80);
+	value &= ~0x80;
+
+	/* translate keys to keycodes */
+	switch (value) {
+	case HCI_HKEY_FN:
+		keycode = KEY_RESERVED; /* ignore FN on its own */
+		return;
+	case HCI_HKEY_MUTE:
+		keycode = KEY_MUTE;
+		break;
+	case HCI_HKEY_BREAK:
+		keycode = KEY_COFFEE;	/* AKA KEY_SCREENLOCK */
+		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;
+		break;
+	}
+
+	if (keycode != KEY_RESERVED) {
+		input_report_key(input, keycode, key_down);
+		input_event(input, EV_MSC, MSC_SCAN, value);
+		input_sync(input);
+	}
+}
 
-	if (toshiba_proc_dir)
-		remove_proc_entry(PROC_TOSHIBA, acpi_root_dir);
+static void toshiba_keys_flush(struct input_polled_dev *dev)
+{
+	int dropped = 0;
+	int clear_queue = 0;
+	u32 value;
+
+	/*
+	 * We don't want to get stuck here; older toshibas such as the
+	 * A60 may load and then return junk during the hci_read
+	 * so limit reads to 16 attempts.
+	 */
+	do {
+		if (hci_poll_keys_once(&value) != HCI_SUCCESS)
+			break;
+
+		dropped++;
+	} while (clear_queue++ < 16);
 
-	return;
+	printk(MY_DEBUG "Dropped %d keys from the queue on startup\n",
+		dropped);
+}
+
+static void toshiba_keys_poll(struct input_polled_dev *dev)
+{
+	struct input_dev *input = dev->input;
+	u32 value;
+
+	while (hci_poll_keys_once(&value) == HCI_SUCCESS)
+		toshiba_deliver_button_event(input, value);
+}
+
+static int __init toshiba_input_polldev_init(void)
+{
+	int error;
+	struct input_dev *input;
+
+	/* use INPUT for key events */
+	toshiba_poll_dev = input_allocate_polled_device();
+	if (!toshiba_poll_dev) {
+		printk(MY_ERR "could not allocate input device\n");
+		error = -ENOMEM;
+		goto err_no_mem;
+	}
+
+	toshiba_poll_dev->flush = toshiba_keys_flush;
+	toshiba_poll_dev->poll = toshiba_keys_poll;
+
+	/* sanitise polling to something sane */
+	if (hotkeys_poll_interval < 100)
+		hotkeys_poll_interval = 100;
+	toshiba_poll_dev->poll_interval = hotkeys_poll_interval;
+
+	/* create one 'keyboard' virtual input device for all the acpi events */
+	input = toshiba_poll_dev->input;
+	input->name = "Toshiba Extra Buttons";
+	input->phys = "toshiba/input0";
+	input->id.bustype = BUS_HOST;
+	input->evbit[0] = BIT(EV_KEY);
+	set_bit(KEY_MUTE, input->keybit);
+	set_bit(KEY_BREAK, input->keybit); /* probably should be KEY_LOCK */
+	set_bit(KEY_SEARCH, input->keybit);
+	set_bit(KEY_SUSPEND, input->keybit);
+	set_bit(KEY_SLEEP, input->keybit);
+	set_bit(KEY_BRIGHTNESSDOWN, input->keybit);
+	set_bit(KEY_BRIGHTNESSUP, input->keybit);
+	set_bit(KEY_WLAN, input->keybit);
+
+	error = input_register_polled_device(toshiba_poll_dev);
+	if (error) {
+		printk(MY_ERR "could not register input device\n");
+		goto err_free_input;
+	}
+
+	return 0;
+
+ err_free_input:
+	input_free_polled_device(toshiba_poll_dev);
+ err_no_mem:
+	return error;
 }
 
 static int __init toshiba_acpi_init(void)
 {
-	acpi_status status = AE_OK;
 	u32 hci_result;
+	int error;
 
 	if (acpi_disabled)
 		return -ENODEV;
@@ -571,33 +754,49 @@ static int __init toshiba_acpi_init(void
 	       TOSHIBA_ACPI_VERSION);
 	printk(MY_INFO "    HCI method: %s\n", method_hci);
 
-	force_fan = 0;
-	key_event_valid = 0;
-
 	/* enable event fifo */
 	hci_write1(HCI_SYSTEM_EVENT, 1, &hci_result);
 
-	toshiba_proc_dir = proc_mkdir(PROC_TOSHIBA, acpi_root_dir);
-	if (!toshiba_proc_dir) {
-		status = AE_ERROR;
-	} else {
-		toshiba_proc_dir->owner = THIS_MODULE;
-		status = add_device();
-		if (ACPI_FAILURE(status))
-			remove_proc_entry(PROC_TOSHIBA, acpi_root_dir);
-	}
-
-	toshiba_backlight_device = backlight_device_register("toshiba",NULL,
-						NULL,
-						&toshiba_backlight_data);
-        if (IS_ERR(toshiba_backlight_device)) {
-		printk(KERN_ERR "Could not register toshiba backlight device\n");
-		toshiba_backlight_device = NULL;
-		toshiba_acpi_exit();
+	error = add_proc_entries();
+	if (error)
+		goto err_out;
+
+	toshiba_backlight_device = backlight_device_register("toshiba", NULL,
+					NULL, &toshiba_backlight_data);
+	if (IS_ERR(toshiba_backlight_device)) {
+		printk(MY_ERR "Could not register toshiba backlight device\n");
+		goto err_remove_proc;
+	}
+	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_poll_interval) {
+		error = toshiba_input_polldev_init();
+		if (error)
+			goto err_remove_backlight;
+	}
+
+	return 0;
+
+ err_remove_backlight:
+	backlight_device_unregister(toshiba_backlight_device);
+ err_remove_proc:
+	remove_proc_entries();
+ err_out:
+	return error;
+}
+
+static void __exit toshiba_acpi_exit(void)
+{
+	backlight_device_unregister(toshiba_backlight_device);
+
+	if (toshiba_poll_dev) {
+		input_unregister_polled_device(toshiba_poll_dev);
+		input_free_polled_device(toshiba_poll_dev);
 	}
-        toshiba_backlight_device->props.max_brightness = HCI_LCD_BRIGHTNESS_LEVELS - 1;
 
-	return (ACPI_SUCCESS(status)) ? 0 : -ENODEV;
+	remove_proc_entries();
 }
 
 module_init(toshiba_acpi_init);
Index: linux/Documentation/kernel-parameters.txt
===================================================================
--- linux.orig/Documentation/kernel-parameters.txt
+++ linux/Documentation/kernel-parameters.txt
@@ -1875,6 +1875,17 @@ and is between 256 and 4096 characters. 
 			See comment before function dc390_setup() in
 			drivers/scsi/tmscsim.c.
 
+	toshiba_acpi.hotkeys_over_input
+			[HW,ACPI]
+			Instructs the driver to deliver hotkey events
+			via input layer as opposed to /proc. Enabled
+			by default.
+	toshiba_acpi.hotkeys_poll_interval =
+			[HW,ACPI]
+			Determines how often the driver polls hotkey
+			state when hotkeys are delivered via input
+			layer. Default is 500 msecs.
+
 	tp720=		[HW,PS2]
 
 	trix=		[HW,OSS] MediaTrix AudioTrix Pro
Subject: toshiba_acpi: add support for altering keymap

Implement getkeycode and setkeycode methods of input device so that
users can adjust driver's keymap if they wish to do so.

Signed-off-by: Dmitry Torokhov <[email protected]>
---
 drivers/acpi/toshiba_acpi.c |  114 +++++++++++++++++++++++++++++---------------
 1 file changed, 77 insertions(+), 37 deletions(-)

Index: linux/drivers/acpi/toshiba_acpi.c
===================================================================
--- linux.orig/drivers/acpi/toshiba_acpi.c
+++ linux/drivers/acpi/toshiba_acpi.c
@@ -603,51 +603,88 @@ static struct backlight_ops toshiba_back
 	.update_status  = set_lcd_status,
 };
 
+static struct toshiba_key {
+	int code;
+	int keycode;
+} toshiba_keymap[] = {
+	{  HCI_HKEY_FN,			KEY_RESERVED }, /* ignore lone FN */
+	{  HCI_HKEY_MUTE,		KEY_MUTE },
+	{  HCI_HKEY_BREAK,		KEY_COFFEE }, /* AKA KEY_SCREENLOCK */
+	{  HCI_HKEY_SEARCH,		KEY_SEARCH },
+	{  HCI_HKEY_SUSPEND,		KEY_SLEEP },
+	{  HCI_HKEY_HIBERNATE,		KEY_SUSPEND },
+	{  HCI_HKEY_BRIGHTNESSDOWN,	KEY_BRIGHTNESSDOWN },
+	{  HCI_HKEY_BRIGHTNESSUP,	KEY_BRIGHTNESSUP },
+	{  HCI_HKEY_WLAN,		KEY_WLAN },
+};
+
+static struct toshiba_key *toshiba_get_key_by_scancode(int code)
+{
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(toshiba_keymap); i++)
+		if (code == toshiba_keymap[i].code)
+			return &toshiba_keymap[i];
+
+	return NULL;
+}
+
+static struct toshiba_key *toshiba_get_key_by_keycode(int keycode)
+{
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(toshiba_keymap); i++)
+		if (keycode == toshiba_keymap[i].keycode)
+			return &toshiba_keymap[i];
+
+	return NULL;
+}
+
+static int toshiba_getkeycode(struct input_dev *dev, int scancode, int *keycode)
+{
+	const struct toshiba_key *key = toshiba_get_key_by_scancode(scancode);
+
+	if (key) {
+		*keycode = key->keycode;
+		return 0;
+	}
+
+	return -EINVAL;
+}
+
+static int toshiba_setkeycode(struct input_dev *dev, int scancode, int keycode)
+{
+	struct toshiba_key *key;
+	int old_keycode;
+
+	if (keycode < 0 || keycode > KEY_MAX)
+		return -EINVAL;
+
+	key = toshiba_get_key_by_scancode(scancode);
+	if (key) {
+		old_keycode = key->keycode;
+		key->keycode = keycode;
+		set_bit(keycode, dev->keybit);
+		if (!toshiba_get_key_by_keycode(old_keycode))
+			clear_bit(old_keycode, dev->keybit);
+		return 0;
+	}
+
+	return -EINVAL;
+}
+
 static void toshiba_deliver_button_event(struct input_dev *input, u32 value)
 {
-	int keycode;
+	struct toshiba_key *key;
 	int key_down;
 
 	/* translate MSB to key up */
 	key_down = !(value & 0x80);
 	value &= ~0x80;
 
-	/* translate keys to keycodes */
-	switch (value) {
-	case HCI_HKEY_FN:
-		keycode = KEY_RESERVED; /* ignore FN on its own */
-		return;
-	case HCI_HKEY_MUTE:
-		keycode = KEY_MUTE;
-		break;
-	case HCI_HKEY_BREAK:
-		keycode = KEY_COFFEE;	/* AKA KEY_SCREENLOCK */
-		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;
-		break;
-	}
-
-	if (keycode != KEY_RESERVED) {
-		input_report_key(input, keycode, key_down);
+	key = toshiba_get_key_by_scancode(value);
+	if (key && key->keycode != KEY_RESERVED) {
+		input_report_key(input, key->keycode, key_down);
 		input_event(input, EV_MSC, MSC_SCAN, value);
 		input_sync(input);
 	}
@@ -720,6 +757,9 @@ static int __init toshiba_input_polldev_
 	set_bit(KEY_BRIGHTNESSUP, input->keybit);
 	set_bit(KEY_WLAN, input->keybit);
 
+	input->getkeycode = toshiba_getkeycode;
+	input->setkeycode = toshiba_setkeycode;
+
 	error = input_register_polled_device(toshiba_poll_dev);
 	if (error) {
 		printk(MY_ERR "could not register input device\n");

[Index of Archives]     [Kernel Newbies]     [Netfilter]     [Bugtraq]     [Photo]     [Stuff]     [Gimp]     [Yosemite News]     [MIPS Linux]     [ARM Linux]     [Linux Security]     [Linux RAID]     [Video 4 Linux]     [Linux for the blind]     [Linux Resources]
  Powered by Linux