blog · git · desktop · images · contact & privacy · gopher
2020-12-05
In early 2019, I looked for keyboards that were more “ergonomic”. My job demands a lot of typing, there’s no way to avoid it, so how can I improve the situation? It is obvious that a standard keyboard layout is not optimal: Just look at your hands while typing and notice all the weird twists and contortions you have to do.
Many “ergonomic keyboards” have some kind of split layout, but very few models are truly split. I think that’s pretty important, because with a true split keyboard, you can move it into a position that’s good for you.
I ended up buying an “R-Go Tools Split Keyboard”:
There are a couple of magnets built in, so you can move it back together and pretend it’s a standard keyboard, which is great for visitors and colleagues. (Also means you can “break it into pieces”, which is a funny little anger outlet.)
For a while, I was also experimenting with tenting devices with varying angles, made using a 3D printer:
They have rubber feet and there was supposed to be an additional piece to make sure they keep their distance.
They didn’t work that great, though. Your hands have no way to rest on the keyboard or on the table anymore, you have to actively keep them in the correct position. I’m not an expert on ergonomics, but it just feels like this causes additional strain and it’s not worth it.
In general, I’m pretty sold to the idea of split keyboards. They allow you to type at much more natural angles. Pretty comfortable.
There is just one super annoying thing about this keyboard: It registers as two individual keyboards. This confuses the living hell out of Firefox (on X11). It queries the keyboard layout from X11 every time it detects a “new” keyboard. In my case: The letter “a” is on a different device than the letter “u”, so Firefox sees a new device all the time. I haven’t investigated this bug further, but it causes Firefox to slow down dramatically after typing for a while. It’s unusable and I have to restart it. This only affects Firefox, though, which is not my main browser at the moment. I also haven’t tested this on Wayland, yet.
– edit 2020-12-18: This problem does not appear to be present on Wayland according to a quick test with Weston. Firefox was riddled with graphical glitches, though.
– edit 2022-01-28: It doesn’t happen anymore with current versions of Firefox, not even on X11.
– edit 2022-02-02: However, some other programs sometimes act up now. The strategy of connecting as two individual USB devices to the system appears to be a bit flaky.
As you can see on the photos above, there is a little “R” on the
keyboard. By default, it just fades in and out. You can turn this off by
pressing Fn + A
, which is handled directly by the keyboard’s firmware.
This LED is supposed to serve a purpose: The manufacturer provides software to keep track of how much you’re typing. After a longer streak, the LED lights up to remind you to take a break.
Of course, this is proprietary software and it doesn’t work on Linux. It is possible to do USB sniffing in QEMU, but, ugh, I’m too lazy. Most of all, I don’t have a Windows VM at hand. I’ve always been curious if I could make the LED work on Linux, but it was just a gimmick and I didn’t find the motivation.
Recently, I just asked the manufacturer by e-mail. Lo and behold, they replied. They actually told me which data I have to send to the device in order to control the LED! This is amazing. I never expected to get any reply at all, because hardware manufacturers tend to be “black holes”. Not this time.
The keyboard registers as several USB devices:
$ lsusb
Bus 003 Device 008: ID 0911:2188 Philips Speech Processing
Bus 003 Device 009: ID 0911:2188 Philips Speech Processing USB Keyboard
Bus 003 Device 007: ID 05e3:0608 Genesys Logic, Inc. Hub
Bus 003 Device 006: ID 05e3:0608 Genesys Logic, Inc. Hub
We can ignore the hubs: There are USB ports at the top of the keyboard to connect an additional numpad. Also, the right half of the keyboard is connected via USB as well and registers as an individual device.
There are no “OUT” endpoints, though:
$ lsusb -d 0911:2188 -v 2>&1 | grep bEndpointAddress
bEndpointAddress 0x81 EP 1 IN
bEndpointAddress 0x82 EP 2 IN
bEndpointAddress 0x81 EP 1 IN
bEndpointAddress 0x82 EP 2 IN
But the keyboard does have standard LEDs like numlock which can be controlled by Linux, so how does Linux send data to the device? It’s done using a special “control” endpoint, which – as I learned – is not so special after all. It’s endpoint 0 and it’s supported by every USB device, because it’s the basic communication channel for a lot of things, including device identification and all that. Sending data on this channel is called a “control transfer”.
Now, when you connect a USB device, Linux checks if it knows it. If so, it automatically attaches a kernel device driver to it. This is, of course, what happens for a standard USB keyboard. This is usually a problem if you want to send custom data: To do that, you have to detach the kernel driver first, then claim the device yourself, send your data, and finally reattach the kernel driver. Not only does this take time (about 0.8 seconds), it also causes the keyboard to be unresponsive, because, well, there’s no keyboard driver attached anymore – the device disappears from X11 and everything.
Luckily, the data we have to send happens to be in a standard format: We have to send a “feature report”. (USB has strange terminology.)
Have a quick look at section 7.2.2 of the USB HID spec:
https://www.usb.org/sites/default/files/documents/hid1_11.pdf
A SET_REPORT
request with the high byte of wValue
set to 0x03 means
sending a “feature report”. In our case, the report ID has to be 0x30.
And that’s basically it – the data
field is the magic payload that
chooses the color of the LED:
uint8_t red[] = {0x30, 0x91, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00};
uint8_t green[] = {0x30, 0x91, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00};
uint8_t yellow[] = {0x30, 0x91, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00};
uint8_t off[] = {0x30, 0x91, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00};
And when captured via usbmon, the packet looks like this:
To do all that, we can use Linux’s “hidraw” devices, as they allow you to send feature reports without detaching the actual driver:
https://www.kernel.org/doc/html/v5.9/hid/hidraw.html
In short, open the correct file in /dev
and issue an ioctl()
. That’s
it. You can do that from user space, no need to write a kernel driver or
anything. So, it’s … almost trivial. You just have to know the report
ID and the magic payload.
Of course, a normal user does not have the permission to do that, so you must either install some udev rules, or just use sudo. (I went for sudo, because it’s easier for me to maintain.)
I put together a tiny little program to control the “R” LED:
https://uninformativ.de/git/r-go-tools-led
It’s minimalist, it really only controls the LED and that’s it, so it’s meant to be used by other tools, like flashing the LED on new mail or when your tea is ready or whatever. It’s up to the user what you want to do with it.
(The program is Linux-specific and not portable. Let’s face it, I’m probably the only person on this planet to ever use it and it’s really, really simple to rewrite it for another platform. Or, for more portability, it would probably be wise to use something like hidapi. Maybe I’ll do that when I need it, but for now, I’d like to avoid the dependency.)
The manufacturer didn’t mention the report ID, nor the fact that it has to be a feature report. I just took the risk and tried to send an “output report”, which didn’t work, and then a feature report. Also, hidraw automatically uses the first byte from the payload as report ID and that just happens to be correct. So, yeah, a little bit of luck was involved here. Or this is mandatory by the USB spec, but I didn’t find confirmation of that. (0x04 for “off” was a guess, too, since 0x00 didn’t work.)
By the way, controlling the standard LEDs (numlock and friends) works almost the same way:
https://wiki.osdev.org/USB_Human_Interface_Devices#LED_lamps
It’s not really practical to do it via hidraw, though, because the
kernel keeps its own state of the LEDs. So, yes, you can use hidraw to
activate the scroll lock LED, for example, but then kernel might
overwrite it at some point (e.g., when you press numlock). To control
the standard LEDs, it’s much better to use something like xset led 3
.
As I said, the original software by R-Go Tools tracks your typing and
alerts you on long streaks. And that’s exactly what I’m using the LED
for at the moment. My r-go-tools-led repo contains a little script
called r-go-break-reminder
which does just that. You can put it in
your ~/.xinitrc
, for example. I’ve been using that for a while now and
it’s really surprising to see how quickly 30 minutes pass. I can only
guess that I’ve grown the habit of typing 1-2 hours straight without
even a short break. Ouch.
I don’t know if I’ll keep using this script or if I’ll repurpose the LED for something else – or if I’ll go back to always turning it off. The whole thing was much more about “can I finally get this to work on Linux?” Yes, I can. And it didn’t even require endless hours of hackery and reverse-engineering. I just asked and they responded! What a pleasant experience!
I mean, for me, knowing which USB packets to send is the optimal solution. It allows me to write a little bit of code which does exactly what I want it to do. I wish all manufacturers would publish their specs already.
(By the way, for various reasons, I wouldn’t use the original R-Go Tools software, even if it existed for Linux. It’s a complicated topic, though, and should be discussed some other time.)