Converting push-button events to keyboard events in the Raspberry Pi
I'm building a self-contained media player using a Raspberry Pi (isn't everybody?) This unit has a built-in screen, and can be controlled by a keyboard. However, I don't envisage using a keyboard: the main input device is an infra-red remote control with a USB receiver. This remote control generates keyboard-like events; for example, pressing the 'play/pause' button sends a 'space' key-press. So if I detect the 'space' key in my application, and use it to start and stop media playback, then the remote control will do what I expect. Of course, my software has to interpret all the other key-presses that the remote control sends -- but mostly they are fairly predictable.
But I also want push-button controls on the front panel of the unit. As a minimum I want a four-way navigation pad, and a play/pause button. And perhaps a 'play something at random' button. How to implement that?
I did toy with the idea of using a separate microcontroller (Pro-micro would be a reasonable, cheap choice) to scan the buttons and generate USB keyboard events. But, while that would work fine, the Raspberry Pi already has GPIO pins that can be connected to buttons. There's no need to use an external controller just for the buttons.
The problem is: how do I feed the push-button events into an application that is looking for key-presses? Since it's my software, I could readily modify it so that it checks for GPIO events as well as keyboard events. The problem with that approach is that it's ugly. By doing this, I've converted a media player application that will work on any Linux system, into an application for the Raspberry Pi. This approach also creates unhelpful tight coupling between different parts of the software -- not good for long-term maintenance.
I described in another article how to detect GPIO push-button presses in a reasonably robust way, allowing for contact bounce and system time changes. In that article the push-button events were fed into a named pipe, which could be read by any other application. Using a pipe decouples the button driver from the main application, but it requires the main application to be aware of the pipe, and know how to interpret the data. It reduces the tight coupling, but doesn't change the fact that the main application has two, completely different, sources of input, rather than one.
Ideally, I'd like to be able to add my push-button controls without changing the main user interface software in any way. This means that I need a way to detect push-button events on the GPIO pins, and convert them into keyboard events. The program to do that needs to run as a separate process and, ideally, contribute little-to-nothing to CPU load.
This article is about generating keyboard events in Linux. My earlier article describes how to handle the GPIO interface -- I won't be changing that part. Since this article is specifically about the kernel's keyboard handling events, it should apply to most Linux systems, not just the Pi. I've tested it on a desktop Fedora system as well.
Simulating keyboard input
Console-like input devices (keyboard, mouse, touch-screen...) have a complicated data path on Linux. It's particularly complicated when a graphical user interface (X, Wayland) is in use. There are various places in this data path where keyboard events can be injected. My application runs in a Linux console, so I need a low-level way to insert the keystrokes. This means that it will work with both console and graphical user interfaces. However, the technique I describe will not be useful if you want to target keyboard input to some specific application -- this method sends input exactly where the platform would send it. In a graphical system, that typically means whichever window has the input focus.
The uinput
module
The key to injecting keyboard data at a low level is the
uinput
kernel module. This module is not installed by
default on any Linux system I've used: you'll need to install it
manually, using something like:
$ sudo modprobe uinput
When the module is installed, it creates a new pseudo-device
/dev/uinput
. I've heard that on some Linux versions it's
/dev/input/uinput
, but I've not seen that myself.
uinput
is a character device; events are sent by writing it,
but first
it has to be set up using ioctl()
calls.
So to begin using the uinput
module, we open it
for writing, like this:
int fd = open ("/dev/uinput", O_WRONLY | O_NONBLOCK);
You will almost certainly need root
permissions to
do this.
Setting up uinput
To set up the uinput
module, we tell it to create
a new input device. The procedure will result in the creation of
a new pseudo-device /dev/input/eventNNN
. The exact
number NNN depends on the number of devices currently in use, and
isn't actually relevant -- no application code will ever read
this input device. Instead, its data is merged by the kernel's
console input subsystem into whatever is the current console device.
Note:
All the C constants specific to theuinput
driver are defined inlinux/uinput.h
and the files it includes. Of particular importance are the key codes, which are inlinux/input-event-codes.h
Because we're simulating an actual input device, we have to give it characteristics of a real device, typically a USB device. So the set-up looks like this:
struct uinput_setup usetup; memset (&usetup, 0, sizeof(usetup)); usetup.id.bustype = BUS_USB; usetup.id.vendor = 0x1234; // Arbitrary usetup.id.product = 0x5678; // Arbitrary strcpy (usetup.name, "Dummy device"); ioctl (fd, UI_DEV_SETUP, &usetup); ioctl (fd, UI_DEV_CREATE);
So far as I know, it doesn't matter what USB vendor/product code or device name you use, although probably the name should not clash with another device.
Note:
I'm not showing any error handling in this article. All theioctl
calls used in this example should return zero on success. You should check that these calls do, in fact, return zero
The next step is to tell the uinput
module which events
will be generated. It isn't sufficient to indicate merely that we'll
be generating keyboard events -- we will have to do that, but we'll
also have to enumerate all the different, specific keystrokes you
will use. You'll also have to generate synchronization events, to
tell the kernel that there is new keyboard data. Oddly, it doesn't seem
to be necessary to declare synchronization events. So the
rest of the set-up looks like this:
ioctl (fd, UI_SET_EVBIT, EV_KEY); // I will generate keyboard events ioctl (fd, UI_SET_KEYBIT, KEY_SPACE); // I will generate space... ioctl (fd, UI_SET_KEYBIT, KEY_UP); ioctl (fd, UI_SET_KEYBIT, KEY_DOWN); // And all the other keys of interest
If there is a way to say 'I might generate any key', I haven't found it.
Sending the events
With that set-up out of the way, we're in a position to generate the actual events. We'll need to generate key-down events, key-up events, and synchronization events.
To generate an event, we populate an input_event
structure, and send it to the uinput
module using
a write()
call. Each event has a type, a code, and
a value. There are also timestamps with microsecond precision, but
I don't believe the kernel interprets these for keyboard events. So I
usually just set them to zero.
Here is a function emit_event()
that will populate the
data structure and send it to the kernel.
void emit_event (int fd, int type, int code, int val) { struct input_event ie; ie.type = type; ie.code = code; ie.value = val; ie.time.tv_sec = 0; ie.time.tv_usec = 0; write (fd, &ie, sizeof(ie)); }
On the Raspberry Pi, input_event
is 16 bytes in size. It's
worth checking that the write()
call returns the correct
number of bytes, because it's possible to have errors here, even when
everything seems to have been set up successfully. If you're lucky,
errno
will give you a clue what the problem is, but I've
only ever seen it set to EINVAL
(invalid argument).
Using the emit_event()
function, we can send a keystroke
like this:
emit_event (fd, EV_KEY, KEY_SPACE, 1); // Down emit_event (fd, EV_SYN, SYN_REPORT, 0); emit_event (fd, EV_KEY, KEY_SPACE, 0); // Up emit_event (fd, EV_SYN, SYN_REPORT, 0);
I'll repeat -- since this has caused me problems -- that you can
only emit a keystroke that you have already registered with the
uinput
module.
About key codes
The key codes used by uinput
are the ancient BIOS
scan codes, even on the Raspberry Pi, which doesn't have a BIOS in
that sense. They don't correspond to ASCII codes, or anything else
you're likely to recognize. They aren't USB keyboard codes either,
even though we're simulating a USB keyboard. 'space', for example, is 57.
If you want to generate, say, a key combination with shift or control,
you'll have to generate all the individual keystrokes, just as a
keyboard would.
Things to watch for
The keystrokes generated using the technique I describe here go to the current console device. That could be a terminal window, if you're using a graphical desktop, or the real console if you're not. So far as I know, the current console device will never be an SSH session. So if you connect to a Linux machine and run the keystroke-generating code in an SSH session, you won't see the keystrokes. That's generally useful, in fact, when it comes to testing. For testing purposes, I find it helpful to SSH to my Raspberry Pi from another computer. That way I don't have to find a way to stop the keystrokes going to the same place that I'm running the keystroke generator.
Whatever application is running in the console has to be able to process the keystrokes that are generated. Of course the application dictates which keystrokes these are. For testing purposes, I find it easiest to have a simple shell in the terminal, so I can see the keystrokes as they are generated. However, a shell might well respond to some keystrokes (arrow keys, for example) in shell-specific ways. A bit of head-scratching might be necessary.
Be aware that it takes a little while -- perhaps as much as a
second -- for the kernel's input subsystem to detect that a new input
device has appeared. Creating a new device using uinput
is a little like plugging in a USB keyboard in this respect. This
is actually less of a problem in a real application than it is in
'Hello, World' testing, because the real application is likely to
be longer-lived.
And finally...
The technique I described in this article also works for other types of input device. You can, for example, simulate mouse movements and touch-screen taps. However, documentation in this area is quite scant, so expect a fair amount of trial-and-error.