Handle gamepad input by reading from /dev/input using C# on the PINE64 running Linux

Now that we have a gamepad connected over bluetooth, and we are able to detect it programmatically, the obvious next step would be to able to handle the input events from the gamepad. Since evdev makes devices available through character devices in the /dev/input directory, we can easily read and parse the events from the corresponding input device file using a C# FileStream, and create corresponding events using the custom event raising and handling delegate model in C#.

In order to get a list of input device files available, I can run ls -lF in the /dev/input directory. This is what the output looks like on my PINE64.

drwxr-xr-x 2 root root      80 Feb 11  2016 by-path/
crw-rw---- 1 root input 13, 64 Feb 11  2016 event0
crw-rw---- 1 root input 13, 65 Feb 11  2016 event1
crw-rw---- 1 root input 13, 66 Feb 11  2016 event2
crw-rw---- 1 root input 13, 67 Feb 11  2016 event3
crw-rw---- 1 root input 13, 68 Feb 11  2016 event4
crw-rw---- 1 root input 13, 69 Dec 19 10:48 event5
crw-rw-r-- 1 root input 13,  0 Dec 19 10:48 js0
crw-rw---- 1 root input 13, 63 Feb 11  2016 mice
crw-rw---- 1 root input 13, 32 Feb 11  2016 mouse0

If you are not running as root and you intend to be able to access the input device, you will need to add the user account to the input group. This can be done using usermod -aG input where is the current logged in user. Based on the output from my previous posts, the Xbox Wireless Controller will be accessible using /dev/input/input5.

The input event reader

Each evdev input event is represented as a struct which is defined in the uapi/linux/input.h header file.

struct input_event {
    struct timeval time;
    __u16 type;
    __u16 code;
    __s32 value;
};

The structure contains the following elements which can be read using a file stream in C#

  • time – a time value of 16 bytes. We will not actually be reading this with our code.
  • type – the event type, a short value of 2 bytes. Valid values can be found in the uapi/linux/input-event-codes.h header file.
  • code – the input event code, a short value of 2 bytes. Valid values can also be found in the uapi/linux/input-event-codes.h header file. The codes are determined based on the event type. For instance, an EV_KEY event will have an event code from one of the defined KEY_* values.
  • value – a value for the event, an integer value of 4 bytes. Dealing with a gamepad, for key events, this will be 0 and 1 corresponding to the up state and down state respectively, and for abs events, this would be a numeric value within the supported range.

Let’s create the aptly named EvdevReader class which will be used to open the file stream and read incoming input events. The constructor accepts an input Device as a parameter, which we created in the previous post for detecting the gamepad. The file stream is initialised in the constructor, and the class also implements IDisposable so that cleanup code for closing and releasing the stream can be called by the garbage collector.

public class EvdevReader : IDisposable
{
    private bool disposed;

    private bool open;

    private Device device;

    private FileStream stream;

    public EvdevReader(Device device)
    {
        this.device = device;
        stream = new FileStream(device.EvdevPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
        open = true;
    }

Next is the Read method. A 24-byte buffer is created in order to read the input events as they come in. Since the open flag essentially indicates that the stream is open, the loop will keep running while input events (every 24 bytes) are read. The time value is skipped because we have no use for it. The type, code and value values are then retrieved and then passed as arguments to the DispatchEvent which we will look at next.

public void Read()
{
    byte[] buffer = new byte[24];
    try
    {
        while (open)
        {
            stream.Read(buffer, 0, buffer.Length);

            // start after byte 16 to skip timeval since we don't actually need it
            int offset = 16;
            short type = BitConverter.ToInt16(new byte[] { buffer[offset], buffer[++offset] }, 0);
            short code = BitConverter.ToInt16(new byte[] { buffer[++offset], buffer[++offset] }, 0);
            int value = BitConverter.ToInt32(
                new byte[] { buffer[++offset], buffer[++offset], buffer[++offset], buffer[++offset] }, 0);

            // Dispatch corresponding gamepad event to any subscribed event handlers
            DispatchEvent(type, code, value);
        }
    }
    catch (Exception ex)
    {
        System.Diagnostics.Debug.WriteLine(ex.ToString());
    }
}

Since we’re only interested in gamepad events, we check that in the DispatchEvent method and then call the appropriate methods based on the event type. A subset of the event types and EV_ABS codes have been mapped as enums in the GamepadEventArgs class which will be used to store details about each event that is raised. Although only EV_ABS and EV_KEY events are being handled, you can extend the code to handle more event types depending on the particular input device.

private void DispatchEvent(short type, short code, int value)
{
    // We only want to deal with gamepad events for now
    if (device.IsGamepad && device is GenericGamepad)
    {
        GenericGamepad gamepad = device as GenericGamepad;
        GamepadEventArgs.EventType eventType = (GamepadEventArgs.EventType) type;
        switch (eventType)
        {
            case GamepadEventArgs.EventType.Syn:

                break;

            case GamepadEventArgs.EventType.Abs:
                gamepad.DispatchAbsEvent(code, value);
                break;

            case GamepadEventArgs.EventType.Key:
                // key down and key up events
                gamepad.DispatchKeyEvent(code, value);
                break;

            case GamepadEventArgs.EventType.Msc:

                break;
        }
    }
}

The full code listing for EvdevReader can be found at https://gitlab.com/akinwale/NaniteIo/blob/master/GhostMainframe/Control/Input/EvdevReader.cs.

From evdev input events to C# events

With EvdevReader reading input events from /dev/input, we can essentially raise custom events making use of the type, code and values and also handle them as may be required. I created the GenericGamepad class to do just this. Since every gamepad is expected to have buttons (or keys), we can dispatch button up and button down events for EV_KEY events. An input event value of 0 means that the button is up, while a value of 1 means that the button is pressed down. We already have an InputEventCode enum which corresponds to the buttons that may be available on a gamepad. I also created a Button enum with more meaningful names. The idea behind this is to create a helpful key map for input events that are being received from the device, which will be used in GamepadEventArgs.

public void DispatchKeyEvent(short code, int value)
{
    InputEventCode keyCode = (InputEventCode) code;
    GamepadEventArgs e = new GamepadEventArgs()
    {
        Type = GamepadEventArgs.EventType.Key,
        Button = IsInputEventKeyCodeSupported(keyCode) ? this[keyCode] : Button.None,
        InputEventCode = keyCode,
        Value = value // not exactly necessary for button down / up events.
    };

    if (value == 0)
        OnButtonUp(e);
    else
        OnButtonDown(e);
}

The DispatchKeyEvent method is quite simple. The input event code is checked using the IsInputEventKeyCodeSupported method to determine if the gamepad actually supports that key code. If it’s supported, the corresponding Button value is assigned to the event arguments, and the corresponding OnButtonUp or OnButtonDown event delegate is called based on the input event value (0 or 1).

public delegate void ButtonDownEventHandler(object sender, GamepadEventArgs e);

public delegate void ButtonUpEventHandler(object sender, GamepadEventArgs e);

public event ButtonDownEventHandler ButtonDown;

public event ButtonUpEventHandler ButtonUp;

public virtual void OnButtonDown(GamepadEventArgs e)
{
    ButtonDownEventHandler handler = ButtonDown;
    if (handler != null)
    {
        ButtonDown(this, e);
    }
}

public virtual void OnButtonUp(GamepadEventArgs e)
{
    ButtonUpEventHandler handler = ButtonUp;
    if (handler != null)
    {
        ButtonUp(this, e);
    }
}

Implementations of the GenericGamepad class are expected to initialise the buttons that are supported and a key map. The code from the XboxWirelessController implementation that I created shows how this can be achieved.

protected override void InitialiseButtons()
{
    Buttons = new HashSet

While EV_KEY events are fairly straightforward, EV_ABS events are a bit more involved, and may vary from gamepad to gamepad. I made the `DispatchAbsEvent` method a virtual method in GenericGamepad, meaning any class which extends the base class has to override the method. The XboxWirelessController class contains an implementation with some comments showing what the corresponding buttons and expected values are. EV_ABS events are raised by the dpad, the analog sticks and the triggers on the Xbox Wireless Controller.

public override void DispatchAbsEvent(short code, int value)
{
    /*
     * Digital DPAD
     *   ABS_HAT0X      -1  Left
     *   ABS_HAT0X       1  Right
     *   ABS_HAT0Y      -1  Up
     *   ABS_HAT0Y       1  Down
     * 
     * Triggers
     *   ABS_BRAKE      Left trigger
     *   ABS_GAS        Right trigger
     * 
     * Thumbsticks
     *   ABS_X, ABS_Y   Left thumbstick
     *   ABS_Z, ABS_RZ  Right thumbstick
     */

    GamepadEventArgs.AbsoluteAxes axis = (GamepadEventArgs.AbsoluteAxes) code;
    GamepadEventArgs e = new GamepadEventArgs() { Type = GamepadEventArgs.EventType.Abs, AbsoluteAxis = axis, Value = value };
    switch (axis)
    {
        case GamepadEventArgs.AbsoluteAxes.AbsHat0X:
            if (value == -1)
                e.Button = Button.DpadLeft;
            else if (value == 1)
                e.Button = Button.DpadRight;

            break;

        case GamepadEventArgs.AbsoluteAxes.AbsHat0Y:
            if (value == -1)
                e.Button = Button.DpadUp;
            else if (value == 1)
                e.Button = Button.DpadDown;

            break;

        case GamepadEventArgs.AbsoluteAxes.AbsBrake:
            e.Button = Button.LeftTrigger;
            break;

        case GamepadEventArgs.AbsoluteAxes.AbsGas:
            e.Button = Button.RightTrigger;
            break;

        case GamepadEventArgs.AbsoluteAxes.AbsX:
        case GamepadEventArgs.AbsoluteAxes.AbsY:
            e.Button = Button.LeftThumbstick;
            break;

        case GamepadEventArgs.AbsoluteAxes.AbsZ:
        case GamepadEventArgs.AbsoluteAxes.AbsRZ:
            e.Button = Button.RightThumbstick;
            break;
    }

    if (axis == GamepadEventArgs.AbsoluteAxes.AbsHat0X ||
        axis == GamepadEventArgs.AbsoluteAxes.AbsHat0Y ||
        axis == GamepadEventArgs.AbsoluteAxes.AbsBrake ||
        axis == GamepadEventArgs.AbsoluteAxes.AbsGas)
    {
        // button down / up events should suffice for the dpad and triggers
        if (value == 0)
            OnButtonUp(e);
        else
            OnButtonDown(e);
    }
    else
    {
        OnThumbstickMove(e);
    }
}

Finally, we can test all of this in addition to detecting the gamepad with a sample console application. This will try to detect a gamepad, assign event handlers if found and then output the event args stdout whenever an input event occurs.

public class Sample
{
    protected static void controller_ButtonUpEvent(object sender, GamepadEventArgs e)
    {
        Console.WriteLine("Button up: {0}", e);
    }

    protected static void controller_ButtonDownEvent(object sender, GamepadEventArgs e)
    {
        Console.WriteLine("Button down: {0}", e);
    }

    protected static void controller_ThumbstickMove(object sender, GamepadEventArgs e)
    {
        Console.WriteLine("{0} move: {1}", e.Button, e);
    }

    public static void Main(string[] args)
    {
        Device gamepad = InputDevices.DetectGamepad();
        if (gamepad != null)
        {
            XboxWirelessController controller = new XboxWirelessController(gamepad);
            Console.WriteLine("Gamepad = {0}, Path = {1}", controller.Name, controller.EvdevPath);

            controller.ButtonDown += controller_ButtonDownEvent;
            controller.ButtonUp += controller_ButtonUpEvent;
            controller.ThumbstickMove += controller_ThumbstickMove;

            Thread t = new Thread(()
            {
                using (EvdevReader reader = new EvdevReader(controller))
                {
                    reader.Read();
                }
            });

            t.Start();
            t.Join();
        }
        else
        {
            Console.WriteLine("No gamepad device found.");
        }

        Console.ReadLine();
    }
}

With this, it is very easy to build a program that can accept input system-wide and respond accordingly, and if you’re feeling adventurous, extend the code to support a plethora of input devices. You can obtain the full code listing for all the classes and enums named in this post from https://gitlab.com/akinwale/NaniteIo/tree/master/GhostMainframe.

How to programmatically detect if a gamepad is connected in Linux using C# and evdev

Since I managed to get the Xbox Wireless Controller connected to the PINE64 using bluetooth, I had to come up with a way to detect that a gamepad is connected, which is neater than having to create a configuration file containing the path to the evdev file, or if I felt like being lazy, hardcode the path in the program. evdev (short form of event device) is the generic input event interface used by the Linux kernel, making input device events available and readable using files in the /dev/input directory.

The /proc/bus/input/devices file contains information about the input devices currently connected to the system. We will have to read this file in order to determine whether or not a gamepad is connected to the system. Here is what the /proc/bus/input/devices file on my PINE64 looks like.

I: Bus=0019 Vendor=0001 Product=0001 Version=0100
N: Name="sunxi-keyboard"
P: Phys=sunxikbd/input0
S: Sysfs=/devices/virtual/input/input0
U: Uniq=
H: Handlers=kbd event0 autohotplug cpufreq_interactive 
B: PROP=0
B: EV=3
B: KEY=800 c004000000000 10000000

I: Bus=0019 Vendor=0001 Product=0001 Version=0100
N: Name="axp81x-supplyer"
P: Phys=m1kbd/input2
S: Sysfs=/devices/platform/axp81x_board/axp81x-supplyer.47/input/input1
U: Uniq=
H: Handlers=kbd event1 autohotplug cpufreq_interactive 
B: PROP=0
B: EV=7
B: KEY=10000000000000 0
B: REL=0

I: Bus=0019 Vendor=0001 Product=0001 Version=0100
N: Name="sunxi-ths"
P: Phys=sunxiths/input0
S: Sysfs=/devices/virtual/input/input2
U: Uniq=
H: Handlers=event2 
B: PROP=0
B: EV=9
B: ABS=10000000000

I: Bus=0019 Vendor=0001 Product=0001 Version=0100
N: Name="sunxi_ir_recv"
P: Phys=sunxi_ir_recv/input0
S: Sysfs=/devices/soc.0/1f02000.s_cir/rc/rc0/input3
U: Uniq=
H: Handlers=sysrq rfkill kbd event3 autohotplug cpufreq_interactive 
B: PROP=0
B: EV=100013
B: KEY=fffeffffffffffff ffffffffffffffff ffffffffffffffff fffffffffffffffe
B: MSC=10

I: Bus=0000 Vendor=0000 Product=0000 Version=0000
N: Name="MCE IR Keyboard/Mouse (sunxi-rc-recv)"
P: Phys=/input0
S: Sysfs=/devices/virtual/input/input4
U: Uniq=
H: Handlers=sysrq kbd mouse0 event4 
B: PROP=0
B: EV=100017
B: KEY=30000 7 ff87207ac14057ff febeffdfffefffff fffffffffffffffe
B: REL=3
B: MSC=10

I: Bus=0005 Vendor=045e Product=02fd Version=0903
N: Name="Xbox Wireless Controller"
P: Phys=bb:bb:bb:bb:bb:bb
S: Sysfs=/devices/soc.0/1c28400.uart/tty/ttyS1/hci0/hci0:1/input5
U: Uniq=xx:xx:xx:xx:xx:xx
H: Handlers=kbd js0 event5 
B: PROP=0
B: EV=1b
B: KEY=7fff000000000000 0 100040000000 0 0
B: ABS=10000030627
B: MSC=10

This gives us quite a decent amount of information to make use of when dealing with input devices. The I, N, P, S, U and H prefixes simply represent the first letter in the corresponding name value while B means bitmap, representing a bitmask for that particular property. The EV bitmap represents the type of events supported by a particular device, while the KEY bitmap represents the keys and buttons that the device has. So now that we have all this information, how do we detect if a particular input device is a gamepad? Referring to section 4.3 of the Linux Gamepad Specification in the kernel documentation, which is titled Detection, gamepads which follow the protocol are expected to have BTN_GAMEPAD mapped. BTN_GAMEPAD is a constant defined in the uapi/linux/input-event-codes.h kernel header file with a value of 0x130 (decimal 304). This means we will have to check if the bit corresponding to BTN_GAMEPAD (bit 304 counting from the rightmost bit to the left) is set to 1.

Moving on to the code, the first thing we should do is create a simple class that represents an input device. We’ll call it Device and it should have a name and a property to store the evdev path. The IsGamepad property is completely optional if you intend to deal with only gamepads.

public class Device
{
    public string Name { get; set; }

    public string EvdevPath { get; set; }

    public bool IsGamepad { get; set; }

    public Device()
    {
		
    }
}

Next, we’ll create an InputDevices class, with a static method which we will call DetectGamepad. The method will parse the /proc/bus/input/devices file, retrieve the name and evdev path and check the KEY bitmap for each device found. When a device that has the BTN_GAMEPAD mapped key is found, the method will return that particular device. If no input device with the mapped key is found, null will be returned.

public class InputDevices
{
    const string InputDevicesFile = "/proc/bus/input/devices";

    /// 
    /// Detects the first gamepad device connected to the system.
    /// 
    /// the first gamepad found in /proc/bus/input/devices, or null if no gamepad was found
    public static Device DetectGamepad()
    {
        Device gamepad = null;
        using (StreamReader reader = new StreamReader(new FileStream(
            InputDevicesFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)))
        {
            Device device = new Device();
            while (!reader.EndOfStream)
            {
                string line = reader.ReadLine();

We make use of StreamReader to open the /proc/bus/input/devices file as readonly, and then try to read each line until we reach the end of the file (or stream).


                // Get the device name
                if (line.StartsWith("N", StringComparison.InvariantCulture))
                {
                    device.Name = line.Substring(line.IndexOf('"') + 1, line.Length - line.IndexOf('"') - 2);
                }

Obtaining the name of the device is fairly easy. The line starts with the N prefix, so we check this, and then we retrieve a substring containing the device name by stripping the double quotes surrounding the name.


                // Get event* handler (for /dev/input/event*)
                if (line.StartsWith("H", StringComparison.InvariantCulture)) // handlers
                {
                    string[] handlers = line.Substring(line.IndexOf('=') + 1).Trim().Split(' ');

                    foreach (string handler in handlers)
                    {
                        if (handler.StartsWith("event", StringComparison.InvariantCulture))
                        {
                            device.EvdevPath = string.Format("/dev/input/{0}", handler);
                        }
                    }
                }

Next we obtain the evdev path by checking the handlers on the line prefixed with H. The handler values are separated by spaces, so we split the string based on the space and check for the handler that starts with event. Once this has been obtained, we combine this with the the /dev/input/ prefix to get the full evdev path. In an upcoming post, we will look at how to monitor the evdev file for events using C#.

Finally, we need to detect the keys defined in the KEY bitmap. Let’s take the value for the Xbox Wireless Controller KEY bitmap as an example.
7fff000000000000 0 100040000000 0 0

This is a bitmask made up of hexadecimal values which we will convert to binary strings in order to check which bits are set. There are 5 groups of values, with each group expected to be 64 bits each. Just as a quick refresher, computers deal in binary, so 1 = 0001, 2 = 0010, 3 = 0011 and so forth. Each hexadecimal character value corresponds to 4 bits, and the individual 0-value groups can be padded with zeroes to make up 64 bits. For the KEY bitmap above, the total expected number of bits is 320. A bit value of 1 indicates that a value has been mapped. The bit position (counting from the rightmost bit to the left) corresponds to the values defined in the uapi/linux/input-event-codes.h header file referred to earlier.

With all of that making sense, we can write the code to parse the bitmap.


                // Find the KEY bitmap and parse (if present)
                if (line.StartsWith("B", StringComparison.InvariantCulture) && line.Contains("KEY"))
                {
                    StringBuilder sb = new StringBuilder();
                    string key = line.Substring(line.IndexOf('=') + 1);
                    string[] groups = key.Trim().Split(' ');
                    foreach (string grp in groups)
                    {
                        if (grp == "0")
                        {
                            sb.Append(string.Empty.PadLeft(64, '0'));
                        }
                        else
                        {
                            StringBuilder grpBin = new StringBuilder();
                            foreach (char c in grp)
                            {
                                grpBin.Append(Convert.ToString(Convert.ToInt32(c.ToString(), 16), 2).PadLeft(4, '0'));
                            }
                            sb.Append(grpBin.ToString().PadLeft(64, '0'));
                        }
                    }

The bitmap value is split into groups using the spaces as the delimiter. If a group is equal to 0, then we pad the binary string with up to 64 zeroes, otherwise, we convert each hexadecimal value in the group to a binary string. Each hexadecimal value should be represented as 4 bits, so we pad to the left with zeroes if the conversion results in a string which is less than 4 bits. If a particular group’s binary string is less than 64 bits, we pad to the left with zeroes to make sure it’s up to 64.


                    string bin = sb.ToString();
                    List keybits = new List();
                    // Count the bits with j starting from right to left
                    for (int i = bin.Length - 1, j = 0; i > -1; i--, j++)
                    {
                        if (bin[i] == '1')
                        {
                            keybits.Add(j);
                        }
                    }

Now that we have the binary string, we count from the rightmost bit to the left and store the bits that are set to 1 (value mapped) in the keybits list. Next, we check if the value for BTN_GAMEPAD (304) exists in the list. If it does, then the input device is a gamepad, and we can break from the loop and return a reference to the input device. If a particular line is empty, which is checked using line.Trim().Length == 0, that signifies that the next device is about to be parsed from the file, so we create a new device instance at that point.


                    // BtnSouth / BtnGamepad key indicates that the device is a gamepad
                    if (keybits.Contains((int) InputEventCode.BtnGamepad))
                    {
                        device.IsGamepad = true;
                        gamepad = device;
                        break;
                    }

                    // Next device
                    if (line.Trim().Length == 0)
                    {
                        device = new Device();
                    }
                }
            }
        }

        return gamepad;
    }
}

InputEventCode is an enum based on a subset of values defined in the uapi/linux/input-event-codes.h kernel header file.

public enum InputEventCode
{    
    KeyBack     = 0x09e,    // 158
    KeyHomepage = 0x0ac,    // 172

    // Gamepad event codes
    BtnGamepad  = 0x130,    // 304
    BtnSouth    = 0x130,    // 304
    BtnA        = BtnSouth,
    BtnEast     = 0x131,    // 305
    BtnB        = BtnEast,
    BtnC        = 0x132,    // 306
    BtnNorth    = 0x133,    // 307
    BtnX        = BtnNorth,
    BtnWest     = 0x134,    // 308
    BtnY        = BtnWest,
    BtnZ        = 0x135,    // 309
    BtnTL       = 0x136,    // 310
    BtnTR       = 0x137,    // 311
    BtnTL2      = 0x138,    // 312
    BtnTR2      = 0x139,    // 313
    BtnSelect   = 0x13a,    // 314
    BtnStart    = 0x13b,    // 315
    BtnMode     = 0x13c,    // 316
    BtnThumbL   = 0x13d,    // 317
    BtnThumbR   = 0x13e     // 318
}

We can test the DetectGamepad method with a simple program to output the device name and evdev path to the console if found. Add the following code to the Main method of a console application to try it out.

Device gamepad = InputDevices.DetectGamepad();
if (gamepad != null)
{
    Console.WriteLine("Gamepad = {0}, Path = {1}", gamepad.Name, gamepad.EvdevPath);
}
else
{
    Console.WriteLine("No gamepad device found.");
}

With the /proc/bus/input/devices file, the device name and handlers can be determined, as well as supported event types and additional capabilities. If you’re feeling adventurous, you can extend the code to detect multiple gamepad devices if connected, or to identify all supported keys / buttons for a particular gamepad, or decode the other bitmaps for a particular device. In my next post, I’ll cover how to read the evdev file in order to detect the input device events and then handle them as may be required.

The full code listing of the InputDevices class can be found at https://gitlab.com/akinwale/NaniteIo/tree/master/GhostMainframe/Control/Input/InputDevices.cs.