Statistics, Science, Random Ramblings

A blog about data and other interesting things

Configuring a rotary encoder on Linux

Posted at — May 20, 2024

Recently, I got a rotary encoder. You might have seen one of these on your favourite microblogging platform, showcased by someone doing something cool looking with it. I mean, at least some of the use cases look kind of useful: media controls are an obvious case, undo/redo or scrolling as well. Probably you can think of many more if the idea of such a device sounds appealing to you.

The tl;dr

Use evsieve. It is feature-rich and well documented and can be used even for rather complex mappings.

The device

It is pretty much a large knob. You can turn it both left and right and you can press it like a big key. So in theory there should be three keys to configure. I wondered whether there might be a limit to the rotation in each direction, potentially adding two more events.

Goals

What I want to achieve with this device is the following:

Out of the box

As a long-time user of Linux, I have seen quite some ups and downs with hardware, leading to generally low expectations when connecting a new device to my machine. To my pleasant surprise, the rotary encoder worked out of the box. It acted as a volume control with the large button muting the sound. Given the device looks a lot like a large volume knob, this seems like a very sane default. I like sane defaults.

Configuration

Of course using the device solely as a large volume knob is not where I want to stop. Trying to figure out is there a straightforward way to configure the device using Gnome’s control centre turned out to be futile. The Gnome desktop seems to assume that there is only ever one keyboard and one mouse present. At least the device did not show up in either of these configuration screens and there is no way to select a device to configure. Trying my luck with setting up keyboard shortcuts did not turn out to be worthwhile either, because while I could map the events triggered by the rotary encoder to commands, this interfered with the media keys on my keyboard. Of course, if I remap VOLUME_DOWN to UNDO then all VOLUME_DOWNs will be UNDOs.

So, it was time to dig a bit deeper. I have to admit, though that the last time I configured specific keys to do specific things on a Linux system was ca. 2009 when configuring media keys on a mediocre laptop. In 2009, things were probably a bit different than today, but nonetheless my experience from 2009 (yup, that was 15 years ago) at least provided me with some pointers. The basic idea back then was that keys have codes which are mapped to all sorts of things like letters and events (such as VOLUME_DOWN). These things are not set in stone, but you can tell your system to do different things than the defaults. Great. Which leads us to xev. xev is an utility of the X window system allowing you to get information about inputs. Pressing a key it provides you with something like this:

KeyPress event, serial 37, synthetic NO, window 0x2c00001,
    root 0x1d3, subw 0x2c00002, time 9199222, (37,31), root:(37,933),
    state 0x0, keycode 111 (keysym 0xff52, Up), same_screen YES,
    XLookupString gives 0 bytes:
    XmbLookupString gives 0 bytes:
    XFilterEvent returns: False

The interesting things here are mainly the keycode and the things in the parentheses after the keycode, where you can see that keycode 111 corresponds to the up key.

However, in case of media keys things might look a bit less useful.

KeymapNotify event, serial 38, synthetic NO, window 0x0,
    keys:  80  0   0   0   0   0   0   0   0   0   0   0   0   0   0   0
           0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0

This does not tell us too many useful things.

Now, the approach of solving this with something like .Xmodmap does not seem too promising. On the one hand we can not get useful events for these keys, on the other hand re-assigning VOLUME_UP to something else will most certainly cause the same issues as above. We need something else.

Introducing evtest

Querying my preferred search engine with the right words pointed me towards using evtest to see which events are triggered and udev to assign rules to what these events should do.

Running evtest as root provides you with a long list of input devices, in my case a total of 23. The list dose include audio devices, keyboards and four entries for the rotary encoder. Finding the correct entry for the rotary encoder took four tries, but when asked to read the correct device, evtest does provide much saner output than xev does (at least for my purposes).

Event: time 1713014408.647600, type 4 (EV_MSC), code 4 (MSC_SCAN), value c00e2
Event: time 1713014408.647600, type 1 (EV_KEY), code 113 (KEY_MUTE), value 1
Event: time 1713014408.647600, -------------- SYN_REPORT ------------
Event: time 1713014408.954602, type 4 (EV_MSC), code 4 (MSC_SCAN), value c00e2
Event: time 1713014408.954602, type 1 (EV_KEY), code 113 (KEY_MUTE), value 0
Event: time 1713014408.954602, -------------- SYN_REPORT ------------

Above the logged events evtest also provides a list of supported events, like so:

Event code 1 (KEY_ESC)
Event code 28 (KEY_ENTER)
Event code 74 (KEY_KPMINUS)
Event code 78 (KEY_KPPLUS)
Event code 103 (KEY_UP)
Event code 105 (KEY_LEFT)
Event code 106 (KEY_RIGHT)
Event code 108 (KEY_DOWN)

Of interest here are the actual event names, as well as the values in the output of evtest, which identify the key. This information can then be used to craft udev rules.

We need one more thing, though. This is an unique identifier for the device, which we can find looking at /sys/class/input/eventXX/device/modalias, where XX is the number of the device as reported by evtest.

Putting everything together I created a file in /etc/udev/hwdb.d called 10-bnr1.hwdb with the content:

# Configure rotary encoder BNR-1
evdev:input:b0003v4249p4241e0111*
 KEYBOARD_KEY_c00ea=key_down
 KEYBOARD_KEY_c00e9=key_up

The spaces before the rules are really important, so is the lower-case spelling of the event names. Then calling

sudo systemd-hwdb reload
sudo udevadm trigger

turned the rotary encoder into a fancy scroll wheel. Two problems quickly become apparent:

  1. The whole process of reloading the udev rules causes a very noticeable lag.
  2. You can only do one mapping per key. However, even if pressing down on the rotary encoder can be linked to a new set of rules, see 1.

The approach is probably too low level to achieve the desired result.

A myriad of options

The Arch Linux wiki comes in quite handy rather often, despite me not using Arch, but Debian. Nonetheless pointing out options and basic documentation should work across all distributions more or less equally. Luckily, there is a page on input remap utilities. After briefly evaluating the options here, evsieve seemed like a decent starting point.

Building evsieve was done rather quickly and easily and the documentations is quite decent. Doing the first steps I came up with something like:

sudo ./evsieve \
     --input /dev/input/by-id/usb-device grab \ # abbreviated for readability
     --map key:up key:a \
     --output

This is of course a rather absurd set-up remapping the up key to the a key, turning our fancy scroll wheel into a weird contraption. But it proves a point and we can work from here. At this point we should also probably get rid of our udev rules we set up before, because this seems like something that would creep up on me in several years time causing headaches trying to find where the weird behaviour of the device comes from.

Overall, this looks like something we can work with. Furthermore, reading the documentation of evsieve it looks very much like you can create several modes with it. Which is exactly what I want.

sudo ./evsieve \
	--input /dev/input/by-id/usb-device grab \
    --hook key:mute toggle \
    --block key:mute \
    --toggle "" @e0 @e1 \
    --map yield key:volumeup@e1 key:nextsong \
    --map yield key:volumedown@e1 key:previoussong \
    --output

This is still a bit of a toy example, but pretty much what I want. Pressing down on the rotary encoder, i.e. pressing what is originally the mute key, toggles between the layouts e0 and e1, where e0 is the default. All other events are blocked, so switching layouts does not cause the music to go silent. The layer e0 keeps the original association of volume control, but the e1 layer is here remapped to previous and next song.

Of course this setup is still not exactly what I want to achieve, but it is close enough that I am confident I can do what I want with my new device. Given that modes without clear indication are bad UX, I should probably also create some sort of indication which layout is currently in use. From the evsieve documentation it seems like this can also be achieved with the software. As I recently switched back to using a tiling window manager (after many many years with Gnome), displaying the mode somewhere in the UI should be easy enough.