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.
1 2 3 4 5 6 7 8 9 10 | 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.
1 2 3 4 5 6 | 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, anEV_KEY
event will have an event code from one of the definedKEY_*
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | 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
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | protected override void InitialiseButtons() { Buttons = new HashSet<button>() { Button.A, Button.B, Button.X, Button.Y, Button.Start, Button.Select, Button.Mode, Button.DpadLeft, Button.DpadUp, Button.DpadRight, Button.DpadDown, Button.LeftBumper, Button.LeftTrigger, Button.LeftThumbstick, Button.RightBumper, Button.RightTrigger, Button.RightThumbstick }; } /// /// The Xbox Wireless Controller gets BtnNorth and BtnWest wrong, so we fix it here. /// protected override void InitialiseKeymap() { // LeftTrigger, RightTrigger and Dpad* buttons are identified as EV_ABS, // so no direct event code button map will be defined here. Keymap = new Dictionary<InputEventCode, Button>() { { InputEventCode.BtnSouth, Button.A }, { InputEventCode.BtnEast, Button.B }, { InputEventCode.BtnNorth, Button.X }, { InputEventCode.BtnWest, Button.Y }, { InputEventCode.BtnStart, Button.Start }, { InputEventCode.KeyBack, Button.Select }, { InputEventCode.KeyHomepage, Button.Mode }, { InputEventCode.BtnTL, Button.LeftBumper }, { InputEventCode.BtnTR, Button.RightBumper }, { InputEventCode.BtnThumbL, Button.LeftThumbstick }, { InputEventCode.BtnThumbR, Button.RightThumbstick } }; } |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | 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.