[RFC][PATCH] POSKeyboard driver for exclusive keyboard access

From: Niels de Vos
Date: Fri Aug 22 2008 - 12:17:41 EST


This new driver makes it possible for middleware like JavaPOS to use
a POSKeyboard (connected to PS/2) with exclusive access. This is
required by the UnifiedPOS Specification which is available from
http://www.nrf-arts.org/UnifiedPOS. Any middleware using this driver
should implement the full exception-handling in user-space. Therefor
it is possible to use specific POS-extensions of POSKeyboards, without
abusing other keyboard-drivers.

Opening /dev/poskeyboard will route all scancodes to this device. The
scancodes will not be processes by the input-subsystem anymore. Reading
/dev/poskeyboard results in receiving the scancodes as raw data for
further processing by the reader. Sending commands to the hardware can
be done by writing to /dev/poskeyboard.

If the driver is loaded and /dev/poskeyboard is not opened, all
scancodes are given to the input-subsystem. This allows 'normal' use of
the keyboard.

Making the driver active involves some commands like the following:
echo -n serio1 > /sys/bus/serio/drivers/atkbd/unbind
echo -n serio1 > /sys/bus/serio/drivers/poskbd/bind

Open questions:
- How to achieve the same with USB-HID complaint keyboards?
- Does serio_unregister_port() a kfree() on the port?

CC: Jiri Kosina <jkosina@xxxxxxx>
Signed-off-by: Niels de Vos <niels.devos@xxxxxxxxxxxxxxxxxx>

---
drivers/input/keyboard/Kconfig | 12 +
drivers/input/keyboard/Makefile | 1 +
drivers/input/keyboard/poskbd.c | 445 +++++++++++++++++++++++++++++++++++++++
3 files changed, 458 insertions(+), 0 deletions(-)
---
diff --git a/drivers/input/keyboard/Kconfig b/drivers/input/keyboard/Kconfig
index efd70a9..0ed311f 100644
--- a/drivers/input/keyboard/Kconfig
+++ b/drivers/input/keyboard/Kconfig
@@ -323,4 +323,16 @@ config KEYBOARD_SH_KEYSC

To compile this driver as a module, choose M here: the
module will be called sh_keysc.
+
+config KEYBOARD_POSKBD
+ tristate "UnifiedPOS complaint POS Keyboard access"
+ default n
+ select SERIO
+ help
+ Say Y here if you need a driver which allows exclusive-use
+ to keyboard devices. This is required by the UnifiedPOS
+ Specification for the POS Keyboard device.
+
+ To compile this driver as a module, choose M here: the
+ module will be called poskbd.
endif
diff --git a/drivers/input/keyboard/Makefile b/drivers/input/keyboard/Makefile
index 0edc8f2..aa4ad17 100644
--- a/drivers/input/keyboard/Makefile
+++ b/drivers/input/keyboard/Makefile
@@ -27,3 +27,4 @@ obj-$(CONFIG_KEYBOARD_HP7XX) += jornada720_kbd.o
obj-$(CONFIG_KEYBOARD_MAPLE) += maple_keyb.o
obj-$(CONFIG_KEYBOARD_BFIN) += bf54x-keys.o
obj-$(CONFIG_KEYBOARD_SH_KEYSC) += sh_keysc.o
+obj-$(CONFIG_KEYBOARD_POSKBD) += poskbd.o
diff --git a/drivers/input/keyboard/poskbd.c b/drivers/input/keyboard/poskbd.c
new file mode 100644
index 0000000..7d7e1d5
--- /dev/null
+++ b/drivers/input/keyboard/poskbd.c
@@ -0,0 +1,445 @@
+/**
+ * Serio driver for exclusive-use keyboard access
+ *
+ * The UnifiedPOS Specification requires the POS keyboard to be an
+ * exclusive-use device. This means that the device is not allowed to
+ * send and/or receive events from processes other than the process
+ * which 'claimed' the device. This driver uses the serio-subsystem
+ * to get access to connected keyboard. After opening the associated
+ * device-node, the scancodes will only be available to the reader of
+ * the device-node. Therefore the reader must understand the protocol
+ * of the keyboard and implement the handling of exceptions and
+ * extensions by itself. Sending commands to the hardware is possible
+ * by writing the required bytes to the open file-descriptor.
+ *
+ * UnifiedPOS Sepecification: http://www.nrf-arts.org/UnifiedPOS
+ *
+ * This driver can be used by unbinding i.e. atkbd and binding poskbd:
+ * # echo -n serio1 > /sys/bus/serio/drivers/atkbd/unbind
+ * # echo -n serio1 > /sys/bus/serio/drivers/poskbd/bind
+ *
+ * Copyright (C) 2008 Wincor Nixdorf International
+ *
+ * Idea by and with help from SUSE Labs, Jiri Kosina <jkosina@xxxxxxx>.
+ *
+ * Author:
+ * Niels de Vos <niels.devos@xxxxxxxxxxxxxxxxxx>
+ *
+ * This work is licensed under the terms of the GNU GPL, version 2. See
+ * the COPYING file in the top-level directory.
+ *
+ */
+
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/moduleparam.h>
+
+#include <linux/serio.h>
+#include <linux/interrupt.h>
+#include <linux/irqreturn.h>
+
+#include <linux/types.h>
+#include <linux/wait.h>
+#include <linux/slab.h>
+#include <asm/uaccess.h>
+#include <linux/errno.h>
+#include <linux/list.h>
+
+#include <linux/fs.h>
+#include <linux/miscdevice.h>
+
+/* driver info */
+#define DRIVER_NAME "poskbd"
+#define DRIVER_DESC "POSKeyboard driver for exclusive keyboard access"
+#define DRIVER_AUTHOR "Niels de Vos, Wincor Nixdorf International"
+#define POSKBD_DEV "poskeyboard"
+
+
+/* enable debugging over a module-parameter */
+#define dbg(fmt, args...) \
+ if (debug >= 2) { printk(KERN_DEBUG "%s: " fmt, __func__, ##args); }
+#define verbose(fmt, args...) \
+ if (debug) { printk(KERN_INFO "%s: " fmt, __func__, ##args); }
+#define warning(fmt, args...) \
+ printk(KERN_WARNING "%s(%s): " fmt, DRIVER_NAME, __func__, ##args);
+
+
+/* for struct serio_driver*/
+static int poskbd_connect(struct serio *serio, struct serio_driver *drv);
+static void poskbd_disconnect(struct serio *serio);
+static irqreturn_t poskbd_interrupt(struct serio *serio,
+ unsigned char scancode, unsigned int flags);
+
+
+/* for struct serio */
+static int poskbd_port_open(struct serio *port);
+static void poskbd_port_close(struct serio *port);
+
+
+/* for struct file_operations */
+static int poskbd_fs_open(struct inode *, struct file *);
+static int poskbd_fs_release(struct inode *, struct file *);
+static ssize_t poskbd_fs_read(struct file *, char __user *, size_t, loff_t *);
+static ssize_t poskbd_fs_write(struct file *, const char __user *,
size_t, loff_t *);
+
+
+static struct serio_device_id poskbd_ids[] = {
+ {
+ .type = SERIO_8042_XL,
+ .proto = SERIO_ANY,
+ .id = SERIO_ANY,
+ .extra = SERIO_ANY,
+ },
+ { 0 }
+};
+
+
+static struct serio_driver poskbd_drv = {
+ .driver = {
+ .name = DRIVER_NAME,
+ },
+ .description = DRIVER_DESC,
+ .id_table = poskbd_ids,
+ .interrupt = poskbd_interrupt,
+ .connect = poskbd_connect,
+ .disconnect = poskbd_disconnect,
+};
+
+static struct file_operations poskbd_fs = {
+ .owner = THIS_MODULE,
+ .read = poskbd_fs_read,
+ .write = poskbd_fs_write,
+ .open = poskbd_fs_open,
+ .release = poskbd_fs_release,
+ .llseek = no_llseek,
+};
+
+
+static struct miscdevice poskbd_md = {
+ .minor = MISC_DYNAMIC_MINOR,
+ .name = POSKBD_DEV,
+ .fops = &poskbd_fs,
+};
+
+
+/**
+ * struct scancode_buf: list of buffered scancodes
+ *
+ * @list: for a dynamical list
+ * @scancode: the scancode
+ */
+struct scancode_buf {
+ struct list_head list;
+ unsigned char scancode;
+};
+
+
+/**
+ * struct poskbd_status: driver internal status
+ *
+ * @hw_port: the real port to write the commands to (struct serio from atkbd)
+ * @kbd_port: the port for the POSKeyboard
+ * @wq: blocked on while waiting for more data
+ * @data: the list of buffered scancodes
+ * @opened: counter for of opened device-nodes
+ */
+static struct poskbd_status {
+ struct serio *hw_port;
+ struct serio *kbd_port;
+ wait_queue_head_t wq;
+ struct scancode_buf *data;
+ short opened;
+} *poskbd_status;
+
+
+/* parameters for the driver */
+static int debug = 0;
+
+static int poskbd_port_open(struct serio *port)
+{
+ dbg("nothing to do for %s\n", port->name);
+ return 0;
+}
+
+static void poskbd_port_close(struct serio *port)
+{
+ dbg("nothing to do for %s\n", port->name);
+}
+
+
+/**
+ * called on binding a serio* to the module
+ * echo -n serio1 > /sys/bus/serio/drivers/wn_kbd/bind
+ */
+static int poskbd_connect(struct serio *port, struct serio_driver *drv)
+{
+ int ret = 0;
+ if (poskbd_status->hw_port) {
+ dbg("a hw_port has already been opened...\n");
+ return ret;
+ }
+
+ ret = serio_open(port, drv);
+
+ if (!ret) {
+ poskbd_status->hw_port = port;
+ }
+
+ dbg("serio_open() on %s was %s\n", port->name, (ret ? "failed" :
"was successful"));
+ return ret;
+}
+
+
+/**
+ * called on binding a serio* to the module
+ * echo -n serio1 > /sys/bus/serio/drivers/wn_kbd/unbind
+ */
+static void poskbd_disconnect(struct serio *port)
+{
+ dbg("going to do serio_close(%s)\n", port->name);
+ serio_close(port);
+ if (port == poskbd_status->hw_port)
+ poskbd_status->hw_port = NULL;
+}
+
+
+/**
+ * called on every keypress, keylock-rotate and card-swipe
+ */
+static irqreturn_t poskbd_interrupt(struct serio *port,
+ unsigned char scancode, unsigned int flags)
+{
+ struct scancode_buf *data;
+
+ if (poskbd_status->opened) {
+ /* add new scancode to the buffer */
+ data = kzalloc(sizeof(struct scancode_buf), GFP_KERNEL);
+ data->scancode = scancode;
+ list_add_tail(&(data->list), &poskbd_status->data->list);
+ dbg("added scancode (0x%.2x) to the buffer\n", scancode);
+
+ /* wake_up() the read() */
+ wake_up(&(poskbd_status->wq));
+ return IRQ_HANDLED;
+ }
+
+ if (!poskbd_status->kbd_port) {
+ dbg("BUG: no port has been assigned to poskbd_status!\n");
+ return IRQ_NONE;
+ }
+
+ dbg("passing scancode (0x%.2x) to serio_interrupt()\n", scancode);
+ return serio_interrupt(poskbd_status->kbd_port, scancode, flags);
+}
+
+
+/**
+ * poskbd_fs_open: exclusive open on PS/2 devices
+ *
+ * Only one open allowed, make it really exclusive.
+ */
+static int poskbd_fs_open(struct inode *inode, struct file *file)
+{
+ int ret = 0;
+
+ if (poskbd_status->opened) {
+ verbose("device already opened\n");
+ ret = -EBUSY;
+ } else {
+ poskbd_status->opened = 1;
+ }
+ return ret;
+}
+
+
+/**
+ * poskbd_fs_release: close()
+ */
+static int poskbd_fs_release(struct inode *inode, struct file *file)
+{
+ poskbd_status->opened = 0;
+ return 0;
+}
+
+
+/**
+ * poskbd_fs_read: block until data arrived
+ */
+static ssize_t poskbd_fs_read(struct file *file, char __user *data,
size_t size, loff_t *pos)
+{
+ struct scancode_buf *buf;
+ struct list_head *i, *q;
+ size_t cnt = 0;
+ int ret;
+
+ /* wait until data is available or read() is aborted */
+ verbose("userspace wants %d/%d of data\n", (int) (size - cnt), (int) size);
+ if (list_empty(&poskbd_status->data->list)) {
+ ret = wait_event_interruptible(poskbd_status->wq,
!list_empty(&poskbd_status->data->list));
+ if (ret) {
+ verbose("read() interrupted: %d\n", ret);
+ return ret;
+ }
+ dbg("I'm awake!\n");
+ }
+
+ /* loop through all buffered scancodes and send them to userspace */
+ do {
+ list_for_each_safe(i, q, &poskbd_status->data->list) {
+ buf = list_entry(i, struct scancode_buf, list);
+ dbg("sending scancode (0x%.2x) to userspace\n", buf->scancode);
+ if (copy_to_user((data + cnt), &(buf->scancode), sizeof(unsigned
char)) != 0) {
+ warning("could not copy data to userspace\n");
+ return -EACCES;
+ }
+ cnt++;
+
+ /* remove the scancode from the buffer */
+ list_del(i);
+ kfree(buf);
+ }
+ } while (!list_empty(&poskbd_status->data->list) || cnt == size);
+
+ verbose("sent %d bytes to userspace\n", (int) cnt);
+ return cnt;
+}
+
+
+/**
+ * poskbd_fs_write: send commands to the keyboard
+ */
+static ssize_t poskbd_fs_write(struct file *file, const char __user
*data, size_t len, loff_t *pos)
+{
+ size_t i;
+ unsigned char c;
+ int ret = 0;
+
+ if (!poskbd_status->hw_port) {
+ warning("no device connected\n");
+ return -ENODEV;
+ }
+
+ for (i = 0; i < len; i++) {
+ if (copy_from_user(&c, data + i, 1)) {
+ warning("could not get data from userspace\n");
+ return -EACCES;
+ }
+
+ dbg("going to serio_write(%s, 0x%.2x)\n", poskbd_status->hw_port->name, c);
+ ret = serio_write(poskbd_status->hw_port, c);
+ if (ret < 0)
+ return ret;
+ }
+ return i;
+}
+
+
+/**
+ * called on module loading
+ */
+static int __init poskbd_init(void)
+{
+ int err = 0;
+
+ poskbd_status = kzalloc(sizeof(struct poskbd_status), GFP_KERNEL);
+ if (!poskbd_status) {
+ warning("could not allocate enough memory for driver status\n");
+ return -ENOMEM;
+ }
+
+ poskbd_status->data = kzalloc(sizeof(struct scancode_buf), GFP_KERNEL);
+ if (!poskbd_status->data) {
+ warning("could not allocate enough memory for scancode buffer\n");
+ err = -ENOMEM;
+ goto free1_exit;
+ }
+
+ INIT_LIST_HEAD(&(poskbd_status->data->list));
+ init_waitqueue_head(&(poskbd_status->wq));
+
+ dbg("going to register_port()\n");
+ poskbd_status->kbd_port = kzalloc(sizeof(struct serio), GFP_KERNEL);
+ if (!poskbd_status->kbd_port) {
+ warning("could not allocate enough memory for port\n");
+ err = -ENOMEM;
+ goto free2_exit;
+ }
+ /* fill the struct serio with valid data */
+ poskbd_status->kbd_port->id.type = SERIO_8042_XL;
+ poskbd_status->kbd_port->open = poskbd_port_open;
+ poskbd_status->kbd_port->close = poskbd_port_close;
+ snprintf(poskbd_status->kbd_port->name,
sizeof(poskbd_status->kbd_port->name), "POSKeyboard");
+ snprintf(poskbd_status->kbd_port->phys,
sizeof(poskbd_status->kbd_port->name), "POSKeyboard");
+
+ serio_register_port(poskbd_status->kbd_port);
+
+ dbg("going to register_driver()\n");
+ err = serio_register_driver(&poskbd_drv);
+ if (err) {
+ warning("could not register driver (%d)\n", err);
+ goto free3_exit;
+ }
+
+ dbg("registering character device\n");
+ err = misc_register(&poskbd_md);
+ if (err) {
+ warning("could not register device (%d)\n", err);
+ goto free4_exit;
+ }
+ return 0;
+
+free4_exit:
+ serio_unregister_driver(&poskbd_drv);
+free3_exit:
+ serio_unregister_port(poskbd_status->kbd_port);
+ kfree(poskbd_status->kbd_port);
+free2_exit:
+ kfree(poskbd_status->data);
+free1_exit:
+ kfree(poskbd_status);
+ return err;
+}
+
+
+/**
+ * called on module unloading
+ */
+static void __exit poskbd_exit(void)
+{
+ int err = 0;
+ struct scancode_buf *data;
+ struct list_head *pos, *q;
+
+ dbg("unregistering character device\n");
+ err = misc_deregister(&poskbd_md);
+
+ dbg("going to unregister_port %s\n", poskbd_status->kbd_port->name);
+ serio_unregister_port(poskbd_status->kbd_port);
+#if 0 /* TODO: does serio_unregister_port() a kfree() on the port? */
+ kfree(poskbd_status->kbd_port);
+#endif
+
+ dbg("going to unregister_driver()\n");
+ serio_unregister_driver(&poskbd_drv);
+
+ if (!list_empty(&poskbd_status->data->list)) {
+ dbg("freeing buffer:\n");
+ list_for_each_safe(pos, q, &poskbd_status->data->list) {
+ data = list_entry(pos, struct scancode_buf, list);
+ list_del(pos);
+ kfree(data);
+ }
+ }
+ kfree(poskbd_status->data);
+ kfree(poskbd_status);
+}
+
+MODULE_AUTHOR(DRIVER_AUTHOR);
+MODULE_LICENSE("GPL");
+MODULE_DEVICE_TABLE(serio, poskbd_ids);
+MODULE_PARM_DESC(debug, "show additional debug information (default: 0)");
+
+module_param(debug, int, 0);
+
+module_init(poskbd_init);
+module_exit(poskbd_exit);
--
To unsubscribe from this list: send the line "unsubscribe linux-kernel" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at http://vger.kernel.org/majordomo-info.html
Please read the FAQ at http://www.tux.org/lkml/