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.
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 | 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public class InputDevices { const string InputDevicesFile = "/proc/bus/input/devices"; /// <summary> /// Detects the first gamepad device connected to the system. /// </summary> /// <returns>the first gamepad found in /proc/bus/input/devices, or null if no gamepad was found</returns> 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).
1 2 3 4 5 6 7 | // 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // 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.
1 2 3 4 5 6 7 8 9 10 11 12 | string bin = sb.ToString(); List<int> keybits = new List<int>(); // 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // 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.
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 | 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.
1 2 3 4 5 6 7 8 9 | 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.