Linux & Dotnet – Read from a device file

Linux & Dotnet – Read from a device file

2019-03-12 1 By Nordes

As you may know, Dotnet Core now runs within Linux. Let’s explore and play around enjoying the fact we can play in any environment now in Core.

Let’s consider that you have a touch panel where you would like to handle everything by yourself (touch, move, etc.). How in the world would you get the touch event?

Probably, if you never played around that low in the system, you probably don’t know. Interestingly, there’s the HID (Human Interface Devices). Most of the documentation is available if you search from your favorite engine. What you really want to know, is the binary data structure, once you have that, the fun can begin. In the case you can’t get any description anywhere, I guess the best choice is to do some reverse engineering (not recommended).

In this article we will only analyse a touch event generating a lot of binary data. I don’t want to cover the entire touch/move/etc of the binary data.

Let’s take the touch screen “touch” click event (KeyPress/Release)

Find the Screen Input Device

Within Linux system, there’s plenty of command in order to find your devices. The device files are usually located within the /dev/…. folder. In the past, we used to have all the existing device in the world in that folder. Nowadays, we simply have the bare minimum and that’s great. Most of the devices, if not all, are documented in the Linus repository. There’s many way to retrieve more details or at least the input device of your choice (touch panel).

One of those method consist in looking in all the USB (/proc/bus/usb/devices) or Input devices (/proc/bus/input/devices) in your system. The touch panel is usually linked to an “Event[1..5]” device.

An alternative is to configure your display to point towards your X-Server and then use the xinput command. If you fail to execute that command, it’s either not installed or you simply forgot to set your environment variable DISPLAY.

xinput --list

Second alternative. Without installing anything, you can use the device manager and look directly in the database and find your touch screen. The command for such a case is udevadm and of course you are required to run this as a root (sudo).

# Give all the devices available on your system. If you have a lot of
# devices connected, consider making the output redirected into a file.
# e.g.: udevadm info --export-db > myFile.txt
udevadm info --export-db 

An another alternative, just in case, is to simply look in the special folder “/dev/input/by-id” and “/dev/input/by-path“. The first give the currently connected device with input capabilities. So if you connect or disconnect an input device, you should be able to see it there. By doing a “ls -l” you will see which event file it’s using since it’s creating a symbolic link to it. For example in this output the event1 is my touchscreen:

myroot@mymachine:/dev/input/by-path# ls -l
total 0
lrwxrwxrwx 1 nobody nogroup 9 Mar  6 08:54 pci-0000:00:14.0-usb-0:13.1:1.1-event -> ../event1
lrwxrwxrwx 1 nobody nogroup 9 Mar  7 23:55 pci-0000:00:14.0-usb-0:13.4.2:1.0-event-kbd -> ../event2

Normally, from here, you know which device file correspond to your device. You might even have more details if you used the udevadm.

Read the binary content

Devices files are the raw data file. In order to be able to do something, you need to stream it as you would do for a TCP/UDP/File data. The structure of the content is structured following the driver rules. For example, the Keyboard and Mouse and a few other devices are considered as well known devices and are well documented all over the internet. You can even go directly in the Unix source code in order to see the .h file.

Let’s create a simple loop receiving the binary content while you touch the panel and then let’s output that raw data using an hex string.

public void ReadDeviceStream(CancellationToken stoppingToken) 
{
  // Use the device file
  var targetFile = new FileInfo("/dev/inputs/event1");

  // Open a stream
  using (FileStream fs = targetFile.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
  {
    stoppingToken.Register(() => fs?.Close());
    int blockId = 1;

    // A big buffer, for simplicity purpose and to receive the entire touch report. We should use
    // the proper buffer size based on the event size. Note that we could also
    // use the binary reader
    var buffer = new byte[1024];

    // Read until the token gets cancelled
    while (!stoppingToken.IsCancellationRequested && fs.Read(buffer) > 0)
    {
      ShowBinaryContent(blockId, buffer);
      blockId++;
    }
  }
}

public void ShowBinaryContent(int blockId, byte[] buffer)
{
  Console.WriteLine($"Block #{blockId}");
  Console.WriteLine(BitConverter.ToString(bytes)); // Hex format: AB-1D...
  Console.WriteLine(string.Empty);
}

How to interpret the data

The result of the previous small program is displayed here

// Block #1
59-9A-74-5C-00-00-00-00-9C-C7-04-00-00-00-00-00-03-00-39-00-03-01-00-00-
59-9A-74-5C-00-00-00-00-9C-C7-04-00-00-00-00-00-03-00-35-00-31-01-00-00-
59-9A-74-5C-00-00-00-00-9C-C7-04-00-00-00-00-00-03-00-36-00-E8-01-00-00-
59-9A-74-5C-00-00-00-00-9C-C7-04-00-00-00-00-00-01-00-4A-01-01-00-00-00-
59-9A-74-5C-00-00-00-00-9C-C7-04-00-00-00-00-00-03-00-00-00-31-01-00-00-
59-9A-74-5C-00-00-00-00-9C-C7-04-00-00-00-00-00-03-00-01-00-E8-01-00-00-
59-9A-74-5C-00-00-00-00-9C-C7-04-00-00-00-00-00-04-00-05-00-00-00-00-00-
59-9A-74-5C-00-00-00-00-9C-C7-04-00-00-00-00-00-00-00-00-00-00-00-00-00

// Block #2
59-9A-74-5C-00-00-00-00-6F-40-05-00-00-00-00-00-03-00-35-00-2C-01-00-00-
59-9A-74-5C-00-00-00-00-6F-40-05-00-00-00-00-00-03-00-00-00-2C-01-00-00-
59-9A-74-5C-00-00-00-00-6F-40-05-00-00-00-00-00-04-00-05-00-10-27-00-00-
59-9A-74-5C-00-00-00-00-6F-40-05-00-00-00-00-00-00-00-00-00-00-00-00-00
            
// Block #3
59-9A-74-5C-00-00-00-00-5D-96-05-00-00-00-00-00-04-00-05-00-20-4E-00-00-
59-9A-74-5C-00-00-00-00-5D-96-05-00-00-00-00-00-00-00-00-00-00-00-00-00

// Block #4
59-9A-74-5C-00-00-00-00-7D-9A-05-00-00-00-00-00-03-00-39-00-FF-FF-FF-FF-
59-9A-74-5C-00-00-00-00-7D-9A-05-00-00-00-00-00-01-00-4A-01-00-00-00-00-
59-9A-74-5C-00-00-00-00-7D-9A-05-00-00-00-00-00-04-00-05-00-30-75-00-00-
59-9A-74-5C-00-00-00-00-7D-9A-05-00-00-00-00-00-00-00-00-00-00-00-00-00

As you can see, we receive many events at the same time and each block contains their own timestamp (few first bytes on each lines). A block here contains more than one instruction. For example, it tells you where you pushed and if it was a push event or release. Let’s focus on the data looking like the Block #1.

public static void Main()
	{
		// Release
		 var binaryStrings = 
		 new [] { "E3-00-75-5C-00-00-00-00-3F-72-09-00-00-00-00-00-03-00-39-00-0C-01-00-00",
                "E3-00-75-5C-00-00-00-00-3F-72-09-00-00-00-00-00-03-00-35-00-51-01-00-00",
                "E3-00-75-5C-00-00-00-00-3F-72-09-00-00-00-00-00-03-00-36-00-93-01-00-00",
                "E3-00-75-5C-00-00-00-00-3F-72-09-00-00-00-00-00-01-00-4A-01-01-00-00-00",
                "E3-00-75-5C-00-00-00-00-3F-72-09-00-00-00-00-00-03-00-00-00-51-01-00-00",
                "E3-00-75-5C-00-00-00-00-3F-72-09-00-00-00-00-00-03-00-01-00-93-01-00-00",
                "E3-00-75-5C-00-00-00-00-3F-72-09-00-00-00-00-00-04-00-05-00-00-00-00-00",
                "E3-00-75-5C-00-00-00-00-3F-72-09-00-00-00-00-00-00-00-00-00-00-00-00-00"};

		foreach (var bs in binaryStrings) {
			byte[] data = bs.Split('-').Select(b => Convert.ToByte(b, 16)).ToArray();

			Console.Write($"[Sec: {BitConverter.ToUInt32(data, 0)}]");
			Console.Write($"\t[USec: {BitConverter.ToUInt32(data, 8)}]");
			Console.Write($"\t[Type: {BitConverter.ToUInt16(data, 16)}]");
			Console.Write($"\t[Code: {BitConverter.ToUInt16(data, 18)}]");
			Console.Write($"\t[Value: {BitConverter.ToInt32(data, 20)}]");
			Console.WriteLine();
		}
	}
		/*
Ref: https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h
Ref: https://www.kernel.org/doc/Documentation/input/event-codes.txt

[Sec: 1551171811]    [USec: 619071]    [Type: 3 EV_ABS]    [Code: 57 ABS_MT_TRACKING_ID]    [Value: 268]  Unique ID of initiated contact
[Sec: 1551171811]    [USec: 619071]    [Type: 3 EV_ABS]    [Code: 53 ABS_MT_POSITION_X]    [Value: 337] Center X touch position 
[Sec: 1551171811]    [USec: 619071]    [Type: 3 EV_ABS]    [Code: 54 ABS_MT_POSITION_Y]    [Value: 403] Center Y touch position
[Sec: 1551171811]    [USec: 619071]    [Type: 1 EV_KEY]    [Code: 330 BTN_TOUCH]    [Value: 1] 
[Sec: 1551171811]    [USec: 619071]    [Type: 3 EV_ABS]    [Code: 0 ABS_X]    [Value: 337] 
[Sec: 1551171811]    [USec: 619071]    [Type: 3 EV_ABS]    [Code: 1 ABS_Y]    [Value: 403]
[Sec: 1551171811]    [USec: 619071]    [Type: 4 EV_MSC]    [Code: 5 MSC_TIMESTAMP]    [Value: 0] 
[Sec: 1551171811]    [USec: 619071]    [Type: 0 EV_SYN]    [Code: 0 SYN_REPORT]    [Value: 0] Indicate the batch is completed and we can proceed
		*/

All the documentation about the types, code, etc. is available from source of Linux (https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h).

The structure look like the following (4bit):

(C lang)

struct input_event {
	struct timeval time; // 2 unsigned uint32 or uint64
	unsigned short type; // 4 bit
	unsigned short code; // 4 bit
	unsigned int value;
};
Start PositionTypeName
0UINT32TSec (Timestamp seconds) – E3-00-75-5C + skip 4 bytes in 64bit arch
8UINT32TUsec (Timestamp microseconds) – 3F-72-09 + skip 4 bytes in 64bit arch
16UINT16Type – 4A-01
18UINT16Code – 01-00
20INT32Value – 00-00

The official documentation can be found at https://www.kernel.org/doc/Documentation/input/input.txt under the section 3.2.4 evdev and we can read the following:

The event codes are the same on all architectures and are hardware independent.

https://www.kernel.org/doc/Documentation/input/input.txt

How should we test?

It looks obvious, but in windows you might not be able to test/debug. You need a Linux system (Not subsystem). Since I use a touch panel with Linux, I simply logged the event output while I was experimenting the touch panel. I then replayed that same data while writing UT (TDD). After your TDD passes and that your event gets out as expected, you can then have the desired flow within your application. If you’ve never experimented, know that you can do remote debugging from Visual Studio (or code) to a Linux host. It works well and you might be able to debug more easily.

Existing library to support you

Here we go directly in the raw content, however, there’s a NuGet package that might help you realize what you need. As discussed in the beginning of the article, the HID is the keyword you want to look for, for such development. I haven’t played that much with the following library, but you should definitely use it in case you want to manage the devices from Dotnet. That library is OS agnostics and does not really care if you are on Windows, Linux or MacOS.

The only issue with that library is the lack of documentation. The best is to read the code itself or simply code everything by yourself.