Linux & Dotnet – Read from a device file
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 Position | Type | Name |
0 | UINT32 | TSec (Timestamp seconds) – E3-00-75-5C + skip 4 bytes in 64bit arch |
8 | UINT32 | TUsec (Timestamp microseconds) – 3F-72-09 + skip 4 bytes in 64bit arch |
16 | UINT16 | Type – 4A-01 |
18 | UINT16 | Code – 01-00 |
20 | INT32 | Value – 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.
- NuGet Library: https://www.nuget.org/packages/HidSharp/
- GitHub Page: https://github.com/directhex/hidsharp
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.
[…] many ways of doing that. There are online documentations on how to do it in Python with evdev and C# on .NET Core. I didn’t try them out. Instead what I try is to write a simple Golang application to process […]