uinput-mapper: Simple and powerful networked input (mapping)

This blog post covers input device manipulation software that I have been working on for the last year. On a basic level, the software allows reading and creating input devices. The homepage of the software can be found here: http://hetgrotebos.org/wiki/uinput-mapper The website contains documentation for most of the uinput-mapper components as well as configuration examples.

Features

So what does uinput-mapper do? Basically, it allows very simple creation of input devices based on existing input devices. The software is quite versatile, but the (currently discovered) features are:

  • Transparently relaying input from one Linux machine to another. This can be done using ssh or even netcat.
  • Creating an input device from another input device. A more concrete example: the software makes it trivial to create one or more joysticks from an existing input device, for example a keyboard. This is particularly useful with games that require you to use a joystick for multiplayer. Other examples include: creating a mouse from keyboard input, remapping input buttons on a touchscreen and mapping the middle mouse button of a thinkpad to keyboard key.
  • Debugging input devices (It has similar functionality to evtest)

The main (command line) programs are:

  • input-read
  • input-create

You can find the usage of both programs on the website: http://hetgrotebos.org/wiki/uinput-mapper#Usage

In very basic terms: input-read passes input events from an input device to stdout and input-create reads stdin and creates an input device based upon that stdin.

Use cases

Use case 0: once upon a time in a hotel (networked input)

During FOSDEM in 2014, some friends and I were staying in a hotel in Brussels, the hotel had free wireless and we had a 720p TV in our room. Since we all brought our laptops we considering playing a game on the TV. The problem: we didn't bring any joysticks, and using a laptop keyboard with four people is not particularly pleasant. Furthermore, most modern games require seperate joysticks (or input devices) for multiplayer, which means one keyboard would not suffice.

The solution ended up being quite ridiculous, but awesome all the same. We forwarded the keyboards from our laptops to a single laptop that was hooked up to the television. (This was possible because the hotel wireless put every in a single LAN without firewalling anything; but the same principle applies with a hotspot from your smartphone for example.)

The latency was about 5 to 10 milliseconds and Jamestown was very playable; the only lag was due to a weak GPU. For Jamestown we needed to create a joystick, since it allows only one player to use a keyboard. This is configuration we wrote (comments inlined):

from uinputmapper.cinput import *

# This dictionary contains the basic input-mapping:
config = {

    # Map key events to what this dict contains; EV_KEY means key events
    # and the '0' defines what INPUT-device it is read from, in this case,
    # first device
    (0, EV_KEY) : {
        # ABS_HATOY events should be generated if a KEY_UP event is,
        # generated on the INPUT-device. In this case, we take the value of
        # the event (either 0 or 1) and make it negative; since the joystick
        # keys can take negative values (on keyboard this makes no sense).
        # The 'prop' values define some joystick properties. For example,
        # the minimal and maximal values of HAT0Y
        KEY_UP: {
            'type' : (0, EV_ABS), 'code' : ABS_HAT0Y, 'value' : lambda _: -_,
            'prop' : { 'min' : -1, 'max' : 1, 'flat' : 0, 'fuzz' : 0 }
        },
        KEY_DOWN: {
            'type' : (0, EV_ABS), 'code' : ABS_HAT0Y, 'value' : lambda _: +_,
            'prop' : { 'min' : -1, 'max' : 1, 'flat' : 0, 'fuzz' : 0 }
        },
        KEY_LEFT: {
            'type' : (0, EV_ABS), 'code' : ABS_HAT0X, 'value' : lambda _: -_,
            'prop' : { 'min' : -1, 'max' : 1, 'flat' : 0, 'fuzz' : 0 }
        },
        KEY_RIGHT: {
            'type' : (0, EV_ABS), 'code' : ABS_HAT0X, 'value' : lambda _: +_,
            'prop' : { 'min' : -1, 'max' : 1, 'flat' : 0, 'fuzz' : 0 }
        },
        # Turn KEY_Z into Button 0 on the joystick. 'value' : None means
        # pass the value through, in other words: don't change the value
        KEY_Z: {
            'type' : (0, EV_KEY), 'code' : BTN_0, 'value' : None
        },
        KEY_X: {
            'type' : (0, EV_KEY), 'code' : BTN_1, 'value' : None
        },
        KEY_SPACE : {
            'type' : (0, EV_KEY), 'code' : BTN_2, 'value' : None
        },
        KEY_ENTER: {
            'type' : (0, EV_KEY), 'code' : BTN_3, 'value' : None
        },

        # KEY_O here is a dummy. For Linux to pick this device up as a
        # joystick, we need to export the weird button called BTN_JOYSTICK,
        # the button has no effect in games. KEY_O is picked randomly here
        # and pressing it has no effect.
        KEY_O: {
            'type' : (0, EV_KEY), 'code' : BTN_JOYSTICK, 'value' : None
        },
    }
}

names = {
    'Joystick 0 (Alice)', # Replace Alice with 'Bob' for the second player
}

def config_merge(c, n):
    # Config merge is passed two dictionaries (by reference...)
    # 'c' contains the current "configuration" (default is whatever keys
    # the input device exports. (In this case, all the keys of a keyboard)
    # Since we do not need any of this, we empty the dictionary.
    c.clear()
    # Then we merge the empty dictionary with our own code.
    c.update(config)
    # For clarity, we assign a name to our input device. This is useful
    # if exporting more of the same joysticks; it allows you and games to
    # differentiate between different (virtual) joysticks.
    n.update(names)

This configuration was used twice (with only the names dictionary changed). The command used, on the host side:

nc -p 9999 -l | input-create -C configs/key2joy.py

And on the client (one of our laptops) side (replace X with the proper number):

input-read /dev/input/eventX -DC | nc 192.168.100.42 9999

The same commands were used for the other laptops, just with a different port.

After executing these commands, we all lay back on our bed, with our laptop on our lap, using their keyboards as virtual joysticks over the hotel wireless.

Use case 1: Arcade Machines and joysticks (mapping a keyboard to two joysticks)

At the hackerspace Technologia Incognita we have a Arcade Machine with a PC hidden inside of it. The arcade controls are ultimately delivered to the PC over PS2. (The JAMMA connector is converted to PS2, for more details see http://wiki.techinc.nl/index.php/Arcade_Machine)

Most games that we play on the Arcade Machine (apart from Emulators) require virtual joysticks. The following configuration achieves this (comments only added where useful; please see the previous example for more basic comments):

from uinputmapper.cinput import *

config = {
    (0, EV_KEY) : {
        # Mapping for Joystick 0
        KEY_UP : {
            'type' : (0, EV_ABS), 'code' : ABS_HAT0Y, 'value' : lambda _: -_,
            'prop' : { 'min' : -1, 'max' : 1, 'flat' : 0, 'fuzz' : 0 }
        },
        KEY_DOWN : {
            'type' : (0, EV_ABS), 'code' : ABS_HAT0Y, 'value' : lambda _: +_,
            'prop' : { 'min' : -1, 'max' : 1, 'flat' : 0, 'fuzz' : 0 }
        },
        KEY_LEFT : {
            'type' : (0, EV_ABS), 'code' : ABS_HAT0X, 'value' : lambda _: -_,
            'prop' : { 'min' : -1, 'max' : 1, 'flat' : 0, 'fuzz' : 0 }
        },
        KEY_RIGHT : {
            'type' : (0, EV_ABS), 'code' : ABS_HAT0X, 'value' : lambda _: +_,
            'prop' : { 'min' : -1, 'max' : 1, 'flat' : 0, 'fuzz' : 0 }
        },
        KEY_LEFTCTRL : {
            'type' : (0, EV_KEY), 'code' : BTN_0, 'value' : None
        },
        KEY_LEFTALT: {
            'type' : (0, EV_KEY), 'code' : BTN_1, 'value' : None
        },
        KEY_SPACE : {
            'type' : (0, EV_KEY), 'code' : BTN_2, 'value' : None
        },
        KEY_1 : {
            'type' : (0, EV_KEY), 'code' : BTN_3, 'value' : None
        },
        # Don't forget the ``fake'' button
        KEY_4 : {
            'type' : (0, EV_KEY), 'code' : BTN_JOYSTICK, 'value' : None
        },
        # Mapping for Joystick 1
        KEY_R : {
            'type' : (1, EV_ABS), 'code' : ABS_HAT0Y, 'value' : lambda _: -_,
            'prop' : { 'min' : -1, 'max' : 1, 'flat' : 0, 'fuzz' : 0 }
        },
        KEY_F: {
            'type' : (1, EV_ABS), 'code' : ABS_HAT0Y, 'value' : lambda _: +_,
            'prop' : { 'min' : -1, 'max' : 1, 'flat' : 0, 'fuzz' : 0 }
        },
        KEY_D : {
            'type' : (1, EV_ABS), 'code' : ABS_HAT0X, 'value' : lambda _: -_,
            'prop' : { 'min' : -1, 'max' : 1, 'flat' : 0, 'fuzz' : 0 }
        },
        KEY_G: {
            'type' : (1, EV_ABS), 'code' : ABS_HAT0X, 'value' : lambda _: +_,
            'prop' : { 'min' : -1, 'max' : 1, 'flat' : 0, 'fuzz' : 0 }
        },
        KEY_A : {
            'type' : (1, EV_KEY), 'code' : BTN_0, 'value' : None
        },
        KEY_S: {
            'type' : (1, EV_KEY), 'code' : BTN_1, 'value' : None
        },
        KEY_Q : {
            'type' : (1, EV_KEY), 'code' : BTN_2, 'value' : None
        },
        KEY_2 : {
            'type' : (1, EV_KEY), 'code' : BTN_3, 'value' : None
        },
        # Don't forget the ``fake'' button
        KEY_3 : {
            'type' : (1, EV_KEY), 'code' : BTN_JOYSTICK, 'value' : None
        },
    }
}
names = {
    0 : 'Joystick 0',
    1 : 'Joystick 1',
}

def config_merge(c, n):
    c.clear()
    c.update(config)
    n.update(names)

Final notes

Grabbing

A feature not discussed in this article is the grab option. This tells uinput-mapper to grab all events from an input device (that is, exclusively recieve the events). This way the events are not passed on onto the X server, for example. We did not use this at the hotel because it would become impossible for us to actually kill uinput-mapper once our keyboard was grabbed. (We would not be able to press control+c)

We just started the xev program to catch all our events, to make sure they were not passed to programs that were not supposed to catch our keypresses.

Security

It should be noted that using netcat for this purpose is not safe, since uinput-mapper currently using pickle to marshall all input data. pickle is exploitable; never accept pickle data from untrusted parties. (Also do not let untrusted parties create random input devices on your machines...)

In the case of ssh, this is more secure (if you allow ssh access, you probably trust the user). In the future I will add support for marshalling using JSON.