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.

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.

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.

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.

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.

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.

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).

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.

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.

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.

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.