After a few more months of contemplation and some helpful suggestions from Ed Nisley (you can check out his blog here), I have discovered a viable way to automate running the userspace driver for my keyboard when it is plugged in.
I’ve already had to tangle with udev
once during this project, so it came as little surprise that the solution arose from there.
I was already using udev
to change the permissions on the files that represented both the uinput
virtual device and the raw input files for the keyboard, but I needed to carry it further. Ed suggested that I try creating symlinks to those raw input files that made it clear that they were distinct from the rest of the /dev/hidraw
device files. It didn’t take long to discover that this is a trivial task for udev
. I just had to add a pattern for the symlink names to my existing udev rule:
KERNEL=="hidraw*", ATTRS{idVendor}=="058f", ATTRS{idProduct}=="9410", MODE:="0660", GROUP="chris", SYMLINK="kinesis%n"
The important part is the SYMLINK="kinesis%n"
. This asks udev
to create a symlink with a name like kinesisX
corresponding to each hidrawX
device that matches those vendor and product IDs (hopefully just this keyboard). The %n
in the rule is replaced by the kernel number for the device (0 for hidraw0
, 1 for hidraw1
, etc…).
With that rule in place, when I plug in the keyboard two new devices appear in /dev/
: kinesisX
and kinesisY
. Often X=2 and Y=3, but the numbers that are used depend on how many other input devices were connected before the keyboard, so I can’t predict which two numbers they will occupy.
Now that we can isolate which devices are the keyboard, the question remains: How do we determine which one is the device that emits the function key events? I used something of a hack here. It seems that the two devices are always advertised in the same order by the keyboard. It first registers the normal key input device, followed by the special function key one. However, this is anecdotal. I have no proof that this is always the case, it’s just always the case for Ed, myself, and the author of this github issue. That’s a large enough sample that I’m okay making this assumption until it explodes in my face.
Since the input device that advertises the function keys is always found second, it always has the higher kernel number of the two kinesis devices. I wrote a simple script to launch kfreestyle2d
pointed at the higher of the two:
#!/bin/sh exec ./kfreestyle2d $(ls /dev/kinesis* | sort | tail -1)
This seems to always work, though I suspect that I can simplify it if I try harder. Feel free to drop suggestions in the comments.
Anyway, this script works every time. It always chooses the higher device number from among the newly-created symlinks, which is always (as far as I know) the correct device.
At this point, there’s only one thing left to do: automatically run this script when the device is plugged in. From my previous experience with udev
, I remembered that udev
is not meant to launch daemon processes directly, but it can ask systemd
to do so on its behalf. A few searches later and I found this Arch forum post about how to do it.
I just needed to create a systemd
service file for launching the daemon and then ask udev
to launch it. I looked at the systemd.service
man pages and at this site for references on how to write the unit. This what I came up with:
# /etc/systemd/system/kfreestyle2d.service [Unit] Description=Userspace driver for Kinesis Freestyle 2 Keyboard [Service] Type=simple Restart=no TimeoutSec=1 ExecStart=/home/chris/Code/builds/kfreestyle2d/sort-and-run.sh
This would correctly invoke the script, but would fail to start if I tried to start it manually with sudo systemctl start kfreestyle2d.service
. It took a long time to discover that the error: 127
that I was getting was because the script didn’t refer to the executable using an absolute path, and systemd’s current working directory was not the directory that I was using to store this stuff. The fixed script looks like this:
#!/bin/sh exec /home/chris/Code/builds/kfreestyle2d/kfreestyle2d $(ls /dev/kinesis* | sort | tail -1)
At this point, I could use sudo systemctl start kfreestyle2d.service
and the daemon would launch (and open the correct input device). All that remained was asking udev
to launch it for me.
That was only one quick tweak to the existing udev
rule:
KERNEL=="hidraw*", ATTRS{idVendor}=="058f", ATTRS{idProduct}=="9410", MODE:="0660", GROUP="chris", SYMLINK="kinesis%n", RUN+="/bin/systemctl --no-block start kfreestyle2d.service"
The operative part is, of course, RUN+="/bin/systemctl --no-block start kfreestyle2d.service"
. This asks systemd
to start that unit file when this udev
rule fires, which is all we need.
After I got the right syntax for that, the whole system started working flawlessly. When I plug the keyboard in, udev
matches it, changes permissions, and launches the driver. When I remove the keyboard, the driver gets an I/O error reading the raw input file and kills itself. Then, when I plug the driver back in, the driver is launched again.
This system probably wouldn’t work if there were more than one of this keyboard plugged into the same computer, but that’s not my use-case. Now all I need to do is write a script to automate installing the different components of the driver on a new system. I’d also like to create a new group for the driver to use instead of running as the current user.
In any event, I can now use the function keys easily on at least one computer. Hazzah!
Image by: Stefan Lins
Image License: CC BY-NC 2.0