LJIT2libevdev – input device tracking on LinuxPosted: September 9, 2015
Once you start going down into the rabbit hole that is UI on Linux, there seems to be no end. I was wanting to get to the bottom of the stack as it were, because I just want to get raw keyboard and mouse events, and do stuff with them. There is a library that helps you do that call libevdev. Here is the luajit binding to it:
As it turns out, getting keyboard and mouse activity is highly dependent on what environment you’re in of course. Are you sitting at a Terminal, in which cases ncurses or similar might be your best choice. If you’re looking at a graphics display, then something related to X, or the desktop manager might be appropriate. At the very bottom of it all though is the kernel, and it’s ability to read the keyboard and mouse, and report what it finds up the chain to interested parties. Down there at the very bottom is a userspace library libevdev, which takes care of making the ioctl calls into the kernel to get the raw values. Great! Only caveat is that you need to be setup with the proper permissions to do it because you’re getting ALL of the keyboard and mouse events on the system. Great for key loggers…
Alright, so what does this mean in the context of Lua? Well, libevdev is a straight up C interface to which is a very thin veneer atop the ioctl calls. It would not be that hard to actually replace the ioctl calls with ioctl calls from luajit directly, but the maintainers of libevdev seem to have it covered quite nicely, so ffi calls to the library are sufficient. The library provides some conveniences like tables of strings to convert from the integer values of things to their string name equivalents. These could probably be replaced with the same within lua land, and save the round trips and string conversions. As a low level interface, it does not provide managedment of the various input devices. You can not ask it “give me the logitech mouse”. You have to know which device is the mouse in the first place before you can start asking for input. Similarly, it’s giving you a ton of raw data that you may not be interested in. Things like the sync signals, indicating the end of an event train. Or the skipped data events, so you can catch up if you prefer not to lose any. How to manage it all?
Let’s start at the beginning.
I have found it challenging to find appropriate discussions relating to UI on Linux. Linux has such a long history, and for most of it, the UI subsystems have been evolving and changing in fundamental ways. So, as soon as you find that juicy article dated from 2002, it’s been replaced by something in 2006, and then again in 2012. It also depends on whether you’re talking about X11, Wayland, Qt, Gnome, SDL, terminal, or some other context.
Recently, I was trying to track down the following scenario: I want to start reading input from the attached logitech mouse on my laptop. Not the track pad under my thumbs, and not the little red nubby stick in the middle of the keyboard, but that mouse specifically. How do I do that?
libevdev is the right library to use, but in order to use it, you need a file descriptor for the specific device. The interwebs tell me you simply open up the appropriate /dev/input/eventxxx file and start reading from it? Right. And how do I know which is the correct ‘eventxxx’ file I should be reading from? You can simply do:
$ cat /proc/bus/input
Look at the output, find the device you’re interested in, look at which event it indicates it’s attached to, then go open up that event…
And how do I do that programatically, and consistently such that it will be the same when I move the mouse to a different system? Ah yes, there’s a library for that, and why don’t you just use Python, and…
Or, how about this:
local EVContext = require("EVContext") local function isLogitech(dev) return dev:name():lower():find("logitech") ~= nil end local dev = EVContext:getMouse(isLogitech); assert(dev, "no mouse found") print(string.format("Input device name: \"%s\"", dev:name())); print(string.format("Input device ID: bus %#x vendor %#x product %#x\n", dev:busType(), dev:vendorId(), dev:productId())); -- print out a constant stream of events for _, ev in dev:events() do print(string.format("Event: %s %s %d", ev:typeName(), ev:codeName(), ev:value())); end
How can I get to this state? First, how about that EVContext thing, and the ‘getMouse()’ call?
EVContext is a convenience class which wraps up all the things in libevdev which aren’t related to a specific instance of a device. So, doing things like iterating over devices, setting the logging level, getting a specific device, etc. Device iteration is a core piece of the puzzle. So, here it is.
function EVContext.devices(self) local function dev_iter(param, idx) local devname = "/dev/input/event"..tostring(idx); local dev, err = EVDevice(devname) if not dev then return nil; end return idx+1, dev end return dev_iter, self, 0 end
That’s a quick and dirty iterator that will get the job done. Basically, just construct a string of the form ‘/dev/input/eventxxx’, and vary the ‘xxx’ with numbers until you can no longer open up devices. For each one, create a EVDevice object from that name. A bit wasteful, but highly beneficial. Once we can iterate all the input devices, we can leverage this for greater mischief.
Looking back at our code, there was this bit to get the keyboard:
local function isLogitech(dev) return dev:name():lower():find("logitech") ~= nil end local dev = EVContext:getMouse(isLogitech);
It looks like we could just call the ‘EVContext:getMouse()’ function and be done with it. What’s with the extra ‘isLogitech()’ part? Well, on its own, getMouse() will simply return the first device which reportedly is like a mouse. That code looks like this:
function EVDevice.isLikeMouse(self) if (self:hasEventType(EV_REL) and self:hasEventCode(EV_REL, REL_X) and self:hasEventCode(EV_REL, REL_Y) and self:hasEventCode(EV_KEY, BTN_LEFT)) then return true; end return false; end
It’s basically saying, a ‘mouse’ is something that has relative movements, at least an x and y axis, and a ‘left’ button. On my laptop, the little mouse nub on the keyboard qualifies, and since it has a lower /dev/input/event number (3), it will be reported before any other mouse on my laptop. So, I need a way to filter on anything that reports to be a mouse, as well as having “logitech” in its name. The code for that is the following from EVContext:
function EVContext.getDevice(self, predicate) for _, dev in self:devices() do if predicate then if predicate(dev) then return dev end else return dev end end return nil; end function EVContext.getMouse(self, predicate) local function isMouse(dev) if dev:isLikeMouse() then if predicate then return predicate(dev); end return true; end return false; end return self:getDevice(isMouse); end
As you can see, ‘EVContext:getDevice()’ takes a predicate (a function that returns true or false). It will iterate through all the devices, applying the predicate to each device in turn. When it finds a device matching the predicate, it will return that device. Of course, you could easily change this to return ALL the devices that match the predicate, but that’s a different story.
The ‘predicate’ in this case is the internal ‘isMouse’ function within ‘getMouse()’, which in turn applies two filters. The first is calling the ‘isLikeMouse()’ function on the device. If that’s satisfied, then it will call the predicate that was passed in, which in this case would be our ‘isLogitech()’ function. If that is satisfied, then the device is returned.
In the end, here’s some output:
Input device name: "Logitech USB Optical Mouse" Input device ID: bus 0x3 vendor 0x46d product 0xc018 Event: EV_REL REL_Y -1 Event: EV_SYN SYN_REPORT 0 Event: EV_REL REL_Y -1 Event: EV_SYN SYN_REPORT 0 Event: EV_REL REL_X -1 Event: EV_REL REL_Y -2 Event: EV_SYN SYN_REPORT 0 Event: EV_REL REL_Y -1 Event: EV_SYN SYN_REPORT 0 Event: EV_MSC MSC_SCAN 589827 Event: EV_KEY BTN_MIDDLE 1 Event: EV_SYN SYN_REPORT 0 Event: EV_MSC MSC_SCAN 589827 Event: EV_KEY BTN_MIDDLE 0 Event: EV_SYN SYN_REPORT 0 Event: EV_REL REL_Y -2 Event: EV_SYN SYN_REPORT 0 Event: EV_REL REL_X 1 Event: EV_SYN SYN_REPORT 0 Event: EV_REL REL_Y -1
Some relative movements, a middle button press/release, some more movement.
The libevdev library represents some pretty low level stuff, and for the moment it seems to be the ‘correct’ way to deal with system level input device event handling. The LJIT2libevdev binding provide both the fundamental access to the library as well as the higher level device access which is sorely needed in this environment. I’m sure over time it will be beneficial to pull some of the conveniences that libevdev provides directly into the binding, further shrinking the required size of the library. For now though, I am simply happy that I can get my keyboard and mouse events into my application without too much fuss.