mht.wtf

A blog about computer science, programming, and whatnot.

Fixing My Wacom Tablet

June 21, 2020

A quick warning: this isn’t one of these “here’s the problem, here’s the solution, bam bam bam” type of write-ups. This was written while I tried to fix my tablet, and with very little editing after the fact. Don’t go in expecting a well though out story arc, as this is meant to reflect how I was working and what I was thinking. I think generally there’s way too little material online on how people work day to day, and too many write-ups of just the good parts, so this is me helping pushing the ratio a little in the right direction.

With that out of the way, let’s start with the background.

Over a year ago I bought a Wacom drawing tablet, having been increasingly annoyed with my handwritten notes and doodles. I figured if I got a tablet I could draw digitally which would simplify erasing, colors, and layout. And, of course, I would be able to access it digitally. Since then I’ve mainly used XournalPP for this, and I think it’s been working okay.

Not great though; there are definitely quirks with both XournalPP and the wacom driver, and it took some tinkering before I had a setup that was usable. Still, one thing that never worked is button presses on the drawing pad. The buttons on the stylus works fine, and the on/off button on the pad works, but none of the four remaining pad buttons do anything.

Worse yet, I’m using Wayland on my home computer, which I suspect will make things tougher.

Still, I figured after so long I’d try to properly fix this, whatever it takes. Just1 getting some button event presses shouldn’t be that hard, right?

libinput

libinput is a library to handle input devices in Wayland. My system also has the libinput tool for interfacing with this library, and the tool has, among other things, the command debug-events:

$ sudo libinput debug-events
-event1   DEVICE_ADDED     Power Button                      seat0 default group1  cap:k
-event0   DEVICE_ADDED     Power Button                      seat0 default group2  cap:k
-event20  DEVICE_ADDED     Logitech Performance MX           seat0 default group3  cap:p left scroll-nat scroll-button
-event3   DEVICE_ADDED     HDA ATI HDMI HDMI/DP,pcm=3        seat0 default group4  cap:
-event4   DEVICE_ADDED     HDA ATI HDMI HDMI/DP,pcm=7        seat0 default group4  cap:
-event5   DEVICE_ADDED     HDA ATI HDMI HDMI/DP,pcm=8        seat0 default group4  cap:
-event6   DEVICE_ADDED     HDA ATI HDMI HDMI/DP,pcm=9        seat0 default group4  cap:
-event7   DEVICE_ADDED     HDA ATI HDMI HDMI/DP,pcm=10       seat0 default group4  cap:
-event8   DEVICE_ADDED     HDA ATI HDMI HDMI/DP,pcm=11       seat0 default group4  cap:
-event21  DEVICE_ADDED     Kingsis Peripherals Evoluent VerticalMouse 4 seat0 default group5  cap:p left scroll-nat scroll-button
-event26  DEVICE_ADDED     HD Pro Webcam C920                seat0 default group6  cap:k
-event24  DEVICE_ADDED     Wacom Intuos BT M Pen             seat0 default group7  cap:T  size 216x135mm
-event25  DEVICE_ADDED     Wacom Intuos BT M Pad             seat0 default group7  cap:P buttons:4 strips:0 rings:0 mode groups:1
-event17  DEVICE_ADDED     ZSA Ergodox EZ                    seat0 default group8  cap:k
-event18  DEVICE_ADDED     ZSA Ergodox EZ Mouse              seat0 default group8  cap:p left scroll-nat scroll-button
-event19  DEVICE_ADDED     ZSA Ergodox EZ System Control     seat0 default group8  cap:k
-event22  DEVICE_ADDED     ZSA Ergodox EZ Consumer Control   seat0 default group8  cap:kp scroll-nat
-event23  DEVICE_ADDED     ZSA Ergodox EZ Keyboard           seat0 default group8  cap:k
-event10  DEVICE_ADDED     HD-Audio Generic Rear Mic         seat0 default group4  cap:
-event11  DEVICE_ADDED     HD-Audio Generic Line             seat0 default group4  cap:
-event12  DEVICE_ADDED     HD-Audio Generic Line Out Front   seat0 default group4  cap:
-event13  DEVICE_ADDED     HD-Audio Generic Line Out Surround seat0 default group4  cap:
-event14  DEVICE_ADDED     HD-Audio Generic Line Out CLFE    seat0 default group4  cap:
-event15  DEVICE_ADDED     HD-Audio Generic Line Out Side    seat0 default group4  cap:
-event16  DEVICE_ADDED     HD-Audio Generic Front Headphone  seat0 default group4  cap:
-event9   DEVICE_ADDED     HD-Audio Generic Front Mic        seat0 default group4  cap:

two of which looks pretty interesting:

-event24  DEVICE_ADDED     Wacom Intuos BT M Pen             seat0 default group7  cap:T  size 216x135mm
-event25  DEVICE_ADDED     Wacom Intuos BT M Pad             seat0 default group7  cap:P buttons:4 strips:0 rings:0 mode groups:1

The pad with its four buttons seems to be properly detected. Drawing on the pad while libinput debug-events is running spits out a bunch of lines of the form

 event24  TABLET_TOOL_AXIS +1.666s		121.71*/69.45*	distance: 0.94*
 event24  TABLET_TOOL_AXIS +1.674s		121.76*/69.46*	distance: 0.94
 event24  TABLET_TOOL_AXIS +1.682s		121.83*/69.47*	distance: 0.87*
 event24  TABLET_TOOL_AXIS +2.552s		106.09*/43.80*	pressure: 0.34*
 event24  TABLET_TOOL_AXIS +2.558s		106.06*/43.79*	pressure: 0.35*
 event24  TABLET_TOOL_AXIS +2.566s		106.05*/43.79	pressure: 0.35*

that is, events from the pen. We can see that we’re getting events that the pen is near, but not touching, the pad with the lines saying distance, and that the events corresponding to when we’re actually touching2 the pad have pressure. The numbers in the middle are positional coordinates, ranging from 0/0 at the top left to 216/135 at the bottom right, which I suspect are in millimeters3.

So far this seems to be working rather well. But uh oh, what happens when we try to press the pad buttons? Nothing. And thus begins the adventure.

libwacom

I figure that since the pen is working well, it might be the driver for the pad that’s lacking. This is slightly supported by the fact that the only apparent usage of the pad is to detect when the pen is near (see this2 footnote). Assuming this is handled across other pads in the same way, maybe the specifics of my pad, like the buttons, are problematic in the driver. The output from libinput did state correctly that it has 4 button though, but … uuuh, let’s just check anyways.

Let’s see what relevant kernel modules and packages we have:

$ lsmod | grep wacom
wacom                 126976  0
usbhid                 65536  2 wacom,hid_logitech_dj
hid                   143360  5 wacom,usbhid,hid_generic,hid_logitech_dj,hid_logitech_hidpp
$ pacman -Q | grep wacom
libwacom 1.3-1

Okay. A quick search reveals that libwacom is on Github, and the README contains the following helpful note:

Use the libwacom-list-local-devices tool to list all local devices recognized by libwacom. If your device is not listed, but it is available as an event device in the kernel (see /proc/bus/input/devices) and in the X session (see xinput list), the device is missing from libwacom’s database.

Again, we are using Wayland, and since the README assumes an X system we might run into trouble. Let’s give it a shot:

$ libwacom-list-local-devices
# Device node: /dev/input/event25
[Device]
Name=Wacom Intuos BT M
ModelName=CTL-6100WL
DeviceMatch=usb:056a:0378;bluetooth:056a:0379;
Class=Bamboo
Width=9
Height=5
IntegratedIn=
Layout=intuos-m-p3.svg
Styli=0x862;

[Features]
Reversible=false
Stylus=true
Ring=false
Ring2=false
Touch=false
TouchSwitch=false
# StatusLEDs=
NumStrips=0
Buttons=4

[Buttons]
# Left=
# Right=
Top=A;B;C;D;
# Bottom=
# Touchstrip=
# Touchstrip2=
# OLEDs=
# Ring=
# Ring2=
EvdevCodes=0x110;0x111;0x115;0x116;
RingNumModes=0
Ring2NumModes=0
StripsNumModes=0

---------------------------------------------------------------
# Device node: /dev/input/event24
[Device]
Name=Wacom Intuos BT M
ModelName=CTL-6100WL
DeviceMatch=usb:056a:0378;bluetooth:056a:0379;
Class=Bamboo
Width=9
Height=5
IntegratedIn=
Layout=intuos-m-p3.svg
Styli=0x862;

[Features]
Reversible=false
Stylus=true
Ring=false
Ring2=false
Touch=false
TouchSwitch=false
# StatusLEDs=
NumStrips=0
Buttons=4

[Buttons]
# Left=
# Right=
Top=A;B;C;D;
# Bottom=
# Touchstrip=
# Touchstrip2=
# OLEDs=
# Ring=
# Ring2=
EvdevCodes=0x110;0x111;0x115;0x116;
RingNumModes=0
Ring2NumModes=0
StripsNumModes=0

---------------------------------------------------------------

Recall from above that event24 is the pen and event25 is the pad. The driver seems to be confused as to the difference between the pad and the pen, as both devices have the exact same output; Maybe this is due to to the fact that they share a device id, or maybe it makes things simpler in the driver. For instance, having the pen be Width=9 and Height=5 is obviously not true, but those are the limits of the pen pressure events that you’d get since you always would use the pen together with the pad. I’ll assume that’s not a problem.

The output also states, once again, that we do have four buttons, but now they also correctly state that the buttons are on the top of the pad. They are labeled A-D. Since there are a listing of four numbers in EvdevCodes, I think those are the “scan codes”, so to speak, that are sent when the buttons are pressed. In case of confusion, let’s write those down in hex and decimal:

0x110 = 272
0x111 = 273
0x115 = 277
0x116 = 278

Just cat it

Come to think of it, why don’t we just cat the right event file? If there are any events coming through we would at least know that the hardware is recognizing that we’re using it and sending something into the driver. Then we would have narrowed down slightly more where in the stack the problems are.

$ sudo cat /dev/input/event25
�`��`�(�`��*D	�*D	(�*D	�y
                                  �y
                                    (�y
                                       ����(���)�)(�)^C⏎        

That looks about right? Slightly unreadable tough; here are the output after having pressed the buttons one at a time, piped through hexdump with a newline in between each event:

$ sudo cat /dev/input/event25 | hexdump
0000000 04c1 5eee 0000 0000 ffb0 0005 0000 0000
0000010 0001 0100 0001 0000 04c1 5eee 0000 0000
0000020 ffb0 0005 0000 0000 0003 0028 000f 0000
0000030 04c1 5eee 0000 0000 ffb0 0005 0000 0000

0000040 0000 0000 0000 0000 04c1 5eee 0000 0000
0000050 39ff 0008 0000 0000 0001 0100 0000 0000
0000060 04c1 5eee 0000 0000 39ff 0008 0000 0000
0000070 0003 0028 0000 0000 04c1 5eee 0000 0000
0000080 39ff 0008 0000 0000 0000 0000 0000 0000

0000090 04c4 5eee 0000 0000 4288 0004 0000 0000
00000a0 0001 0101 0001 0000 04c4 5eee 0000 0000
00000b0 4288 0004 0000 0000 0003 0028 000f 0000
00000c0 04c4 5eee 0000 0000 4288 0004 0000 0000

00000d0 0000 0000 0000 0000 04c4 5eee 0000 0000
00000e0 d49a 0007 0000 0000 0001 0101 0000 0000
00000f0 04c4 5eee 0000 0000 d49a 0007 0000 0000
0000100 0003 0028 0000 0000 04c4 5eee 0000 0000
0000110 d49a 0007 0000 0000 0000 0000 0000 0000

0000120 04c4 5eee 0000 0000 d983 000e 0000 0000
0000130 0001 0102 0001 0000 04c4 5eee 0000 0000
0000140 d983 000e 0000 0000 0003 0028 000f 0000
0000150 04c4 5eee 0000 0000 d983 000e 0000 0000

0000160 0000 0000 0000 0000 04c5 5eee 0000 0000
0000170 0a14 0003 0000 0000 0001 0102 0000 0000
0000180 04c5 5eee 0000 0000 0a14 0003 0000 0000
0000190 0003 0028 0000 0000 04c5 5eee 0000 0000
00001a0 0a14 0003 0000 0000 0000 0000 0000 0000

00001b0 04c5 5eee 0000 0000 7e2c 000b 0000 0000
00001c0 0001 0103 0001 0000 04c5 5eee 0000 0000
00001d0 7e2c 000b 0000 0000 0003 0028 000f 0000
00001e0 04c5 5eee 0000 0000 7e2c 000b 0000 0000

00001f0 0000 0000 0000 0000 04c5 5eee 0000 0000
0000200 3f1e 000f 0000 0000 0001 0103 0000 0000
0000210 04c5 5eee 0000 0000 3f1e 000f 0000 0000
0000220 0003 0028 0000 0000 04c5 5eee 0000 0000
0000230 3f1e 000f 0000 0000 0000 0000 0000 0000

Somehow, the the down presses are less data than the releases. This might be true, but another explanation is that hexdump is buffering up the data so that it can output each line as 0x10 bytes. After carefully reading the man page of hexdump, and with some trial and error4, the following does the job:

$ sudo cat /dev/input/event25 | hexdump -ve "1/1 \"%02x\n\""
# A down                                              **    **
0a 0a ee 5e 00 00 00 00 9c 82 06 00 00 00 00 00 01 00 00 01 01 00 00 00 0a 0a ee 5e 00 00 00 00 9c 82 06 00 00 00 00 00 03 00 28 00 0f 00 00 00 0a 0a ee 5e 00 00 00 00 9c 82 06 00 00 00 00 00 00 00 00 00 00 00 00 00
# A up
0b 0a ee 5e 00 00 00 00 3b fa 03 00 00 00 00 00 01 00 00 01 00 00 00 00 0b 0a ee 5e 00 00 00 00 3b fa 03 00 00 00 00 00 03 00 28 00 00 00 00 00 0b 0a ee 5e 00 00 00 00 3b fa 03 00 00 00 00 00 00 00 00 00 00 00 00 00
# B down
0c 0a ee 5e 00 00 00 00 a9 ea 03 00 00 00 00 00 01 00 01 01 01 00 00 00 0c 0a ee 5e 00 00 00 00 a9 ea 03 00 00 00 00 00 03 00 28 00 0f 00 00 00 0c 0a ee 5e 00 00 00 00 a9 ea 03 00 00 00 00 00 00 00 00 00 00 00 00 00
# B up
0c 0a ee 5e 00 00 00 00 42 0e 0f 00 00 00 00 00 01 00 01 01 00 00 00 00 0c 0a ee 5e 00 00 00 00 42 0e 0f 00 00 00 00 00 03 00 28 00 00 00 00 00 0c 0a ee 5e 00 00 00 00 42 0e 0f 00 00 00 00 00 00 00 00 00 00 00 00 00
# C down
0e 0a ee 5e 00 00 00 00 f4 ee 01 00 00 00 00 00 01 00 02 01 01 00 00 00 0e 0a ee 5e 00 00 00 00 f4 ee 01 00 00 00 00 00 03 00 28 00 0f 00 00 00 0e 0a ee 5e 00 00 00 00 f4 ee 01 00 00 00 00 00 00 00 00 00 00 00 00 00
# C up
0e 0a ee 5e 00 00 00 00 4c 76 0c 00 00 00 00 00 01 00 02 01 00 00 00 00 0e 0a ee 5e 00 00 00 00 4c 76 0c 00 00 00 00 00 03 00 28 00 00 00 00 00 0e 0a ee 5e 00 00 00 00 4c 76 0c 00 00 00 00 00 00 00 00 00 00 00 00 00
# D down
0f 0a ee 5e 00 00 00 00 ee 8f 08 00 00 00 00 00 01 00 03 01 01 00 00 00 0f 0a ee 5e 00 00 00 00 ee 8f 08 00 00 00 00 00 03 00 28 00 0f 00 00 00 0f 0a ee 5e 00 00 00 00 ee 8f 08 00 00 00 00 00 00 00 00 00 00 00 00 00
# D up
10 0a ee 5e 00 00 00 00 c2 96 04 00 00 00 00 00 01 00 03 01 00 00 00 00 10 0a ee 5e 00 00 00 00 c2 96 04 00 00 00 00 00 03 00 28 00 00 00 00 00 10 0a ee 5e 00 00 00 00 c2 96 04 00 00 00 00 00 00 00 00 00 00 00 00 00

We can even see some signs of what data is sent through here. For instance, in the columns as marked by ** we see 00 through 03, likely the button number, and 01 for press and 00 for release. In other words, there seems to be reasonable data sent from the pad that we can read from /dev/input/event25.

Next, we need to find out on which side libwacom is; is that doing the mapping from whatever goes to over the wire and to what we just read, or is that supposed to read from event25 to the events that we did not get from libinput?

A Closer Look At That Data

kernel.org has some documentation for the Linux input subsystem, but I think it’s written in such a way that it’s not very helpful unless you already have a pretty good idea of what’s going on. However, Section 1.5 has the following info:

You can use blocking and nonblocking reads, and also select() on the /dev/input/eventX devices, and you’ll always get a whole number of input events on a read. Their layout is:

struct input_event {
    struct timeval time;
    unsigned short type;
    unsigned short code;
    unsigned int value;
};

This doesn’t seem right, since the size of input_event is way less than the data we read above. Unless, of course, we didn’t get only one event. Since the first member is the time we can assume that all events should start with more or less the same. In addition, we suspect that an event is about 8 + 2 + 2 + 4 = 16 bytes long. Or maybe struct timeval is 16 bytes big? That seems to align much better with the data we have read:

0a 0a ee 5e 00 00 00 00  9c 82 06 00 00 00 00 00  01 00  00 01  01 00 00 00
0a 0a ee 5e 00 00 00 00  9c 82 06 00 00 00 00 00  03 00  28 00  0f 00 00 00
0a 0a ee 5e 00 00 00 00  9c 82 06 00 00 00 00 00  00 00  00 00  00 00 00 00

man 3 timeval says that struct timeval has two members, a time_t and a suseconds_t; in addition, we’re on a big-endian machine, so our struct members should look like this5:

struct input_event {
    struct timeval {
        time_t       tv_sec = 0x000000005eee0a0a;
        suseconds_t tv_usec = 0x000000000006829c;
    }                                  // and for the other two events:
    unsigned short type  =     0x0001; //     0003 / 0000
    unsigned short code  =     0x0100; //     0028 / 0000
    unsigned int   value = 0x00000001; // 0000000f / 00000000
}

The time certainly looks reasonable:

$ printf "%016x\n" (date +"%s")
000000005eee1baa

The docs says that the types are defined in include/uapi/linux/input-event-codes.h, which I found on my system in /usr/include/linux/input-event-codes.h6. Looking through it we can infer that the events we’re reading are; The difficulty is that how to interpret the code or value of an event depends on the type of the event. §2.2.1 is helpful here. As far as I can tell, this is what’s going on:

Type Code Value Meaning
EV_KEY BTN_0 1 pressed
EV_ABS ABS_MISC 15 ?
EV_SYN SYN_REPORT 0 undef

Note that in §2.2.1 they say

EV_SYN event values are undefined. Their usage is defined only by when they are sent in the evdev event stream.

Here are all of the events from earlier, but this time one per line and annotated on the right:

# A down
0a 0a ee 5e 00 00 00 00  9c 82 06 00 00 00 00 00  01 00  00 01  01 00 00 00 # KEY/BTN_0 Press
0a 0a ee 5e 00 00 00 00  9c 82 06 00 00 00 00 00  03 00  28 00  0f 00 00 00 # ABS/MISC 15
0a 0a ee 5e 00 00 00 00  9c 82 06 00 00 00 00 00  00 00  00 00  00 00 00 00 # SYN/REPORT
# A up
0b 0a ee 5e 00 00 00 00  3b fa 03 00 00 00 00 00  01 00  00 01  00 00 00 00 # KEY/BTN_0 Release
0b 0a ee 5e 00 00 00 00  3b fa 03 00 00 00 00 00  03 00  28 00  00 00 00 00 # ABS/MISC 0
0b 0a ee 5e 00 00 00 00  3b fa 03 00 00 00 00 00  00 00  00 00  00 00 00 00 # SYN/REPORT
# B down
0c 0a ee 5e 00 00 00 00  a9 ea 03 00 00 00 00 00  01 00  01 01  01 00 00 00 # KEY/BTN_1 Press
0c 0a ee 5e 00 00 00 00  a9 ea 03 00 00 00 00 00  03 00  28 00  0f 00 00 00 # ABS/MISC 15
0c 0a ee 5e 00 00 00 00  a9 ea 03 00 00 00 00 00  00 00  00 00  00 00 00 00 # SYN/REPORT
# B up
0c 0a ee 5e 00 00 00 00  42 0e 0f 00 00 00 00 00  01 00  01 01  00 00 00 00 # KEY/BTN_1 Release
0c 0a ee 5e 00 00 00 00  42 0e 0f 00 00 00 00 00  03 00  28 00  00 00 00 00 # ABS/MISC 0
0c 0a ee 5e 00 00 00 00  42 0e 0f 00 00 00 00 00  00 00  00 00  00 00 00 00 # SYN/REPORT
# C down
0e 0a ee 5e 00 00 00 00  f4 ee 01 00 00 00 00 00  01 00  02 01  01 00 00 00 # KEY/BTN_2 Press
0e 0a ee 5e 00 00 00 00  f4 ee 01 00 00 00 00 00  03 00  28 00  0f 00 00 00 # ABS/MISC 15
0e 0a ee 5e 00 00 00 00  f4 ee 01 00 00 00 00 00  00 00  00 00  00 00 00 00 # SYN/REPORT
# C up
0e 0a ee 5e 00 00 00 00  4c 76 0c 00 00 00 00 00  01 00  02 01  00 00 00 00 # KEY/BTN_2 Release
0e 0a ee 5e 00 00 00 00  4c 76 0c 00 00 00 00 00  03 00  28 00  00 00 00 00 # ABS/MISC 0
0e 0a ee 5e 00 00 00 00  4c 76 0c 00 00 00 00 00  00 00  00 00  00 00 00 00 # SYN/REPORT
# D down
0f 0a ee 5e 00 00 00 00  ee 8f 08 00 00 00 00 00  01 00  03 01  01 00 00 00 # KEY/BTN_3 Press
0f 0a ee 5e 00 00 00 00  ee 8f 08 00 00 00 00 00  03 00  28 00  0f 00 00 00 # ABS/MISC 15
0f 0a ee 5e 00 00 00 00  ee 8f 08 00 00 00 00 00  00 00  00 00  00 00 00 00 # SYN/REPORT
# D up
10 0a ee 5e 00 00 00 00  c2 96 04 00 00 00 00 00  01 00  03 01  00 00 00 00 # KEY/BTN_3 Release
10 0a ee 5e 00 00 00 00  c2 96 04 00 00 00 00 00  03 00  28 00  00 00 00 00 # ABS/MISC 0
10 0a ee 5e 00 00 00 00  c2 96 04 00 00 00 00 00  00 00  00 00  00 00 00 00 # SYN/REPORT

In trying to find out more info about ABS_MISC I found a wiki page on the linuxwacom/input-wacom repository named Kernel Input Event Overview, which explains pretty well how the wacom driver works. They state the following:

In addition to the BTN_TOOL_* events for informing user land what tool the current events being sent belong to, there is a MSC_SERIAL event that contains a serial # to aid in tracking current tool as well as a ABS_MISC which is a hard code device ID. Of these two, the MSC_SERIAL is the most useful to user land.

So, uuh.. maybe I won’t worry too much about the ABS/MISC events. But this is good; we have confirmed that the data we’re reading from /dev/input/event25 is of the type input_event, and that it makes sense, more or less. From reading the wiki page it really does sound like the driver is mapping whatever goes over the wire to the Linux input subsystem format, which is input_event.

At this point I realize that linuxwacom has three primary components: input-wacom which is the kernel driver, which presumably does the mapping just mentioned; xf86-input-wacom the X driver, which I suppose makes kernel driver events into X events? and libwacom, which really just seems to be a utility for simpler querying of state and button mapping and so on.

Going back, we can see that libwacom-list-local-devices seems to work just fine, which I think means that the pad is properly detected. In addition, we know that we get “good” events to /dev/input/event25 so presumably the kernel driver also works fine. However, libinput debug-events did not list the button presses, so libinput doesn’t get those events, although it does get the stylus events.

Back to libinput

Next, we got back to libinput; looking through some of the docs it seems there’s another command, libinput record. According to man 1 libinput-record,

The libinput record tool records kernel events from a device and prints them in a format that can later be replayed with the libinput replay(1) tool.

Running it, and selecting our device, actually shows that the buttons are detected:

$ sudo libinput record
Available devices:
/dev/input/event0:	Power Button
/dev/input/event1:	Power Button
/dev/input/event2:	PC Speaker
/dev/input/event3:	HDA ATI HDMI HDMI/DP,pcm=3
/dev/input/event4:	HDA ATI HDMI HDMI/DP,pcm=7
/dev/input/event5:	HDA ATI HDMI HDMI/DP,pcm=8
/dev/input/event6:	HDA ATI HDMI HDMI/DP,pcm=9
/dev/input/event7:	HDA ATI HDMI HDMI/DP,pcm=10
/dev/input/event8:	HDA ATI HDMI HDMI/DP,pcm=11
/dev/input/event9:	HD-Audio Generic Front Mic
/dev/input/event10:	HD-Audio Generic Rear Mic
/dev/input/event11:	HD-Audio Generic Line
/dev/input/event12:	HD-Audio Generic Line Out Front
/dev/input/event13:	HD-Audio Generic Line Out Surround
/dev/input/event14:	HD-Audio Generic Line Out CLFE
/dev/input/event15:	HD-Audio Generic Line Out Side
/dev/input/event16:	HD-Audio Generic Front Headphone
/dev/input/event17:	ZSA Ergodox EZ
/dev/input/event18:	ZSA Ergodox EZ Mouse
/dev/input/event19:	ZSA Ergodox EZ System Control
/dev/input/event20:	Logitech Performance MX
/dev/input/event21:	Kingsis Peripherals Evoluent VerticalMouse 4
/dev/input/event22:	ZSA Ergodox EZ Consumer Control
/dev/input/event23:	ZSA Ergodox EZ Keyboard
/dev/input/event24:	Wacom Intuos BT M Pen
/dev/input/event25:	Wacom Intuos BT M Pad
/dev/input/event26:	HD Pro Webcam C920
Select the device event number: 25
Recording to 'stdout'.
version: 1
ndevices: 1
libinput:
  version: "1.15.5"
  git: "unknown"
system:
  kernel: "5.7.2-arch1-1"
  dmi: "dmi:bvnAmericanMegatrendsInc.:bvr3.D0:bd07/11/2018:svnMicro-StarInternationalCo.,Ltd.:pnMS-7A33:pvr2.0:rvnMSI:rnX370SLIPLUS(MS-7A33):rvr2.0:cvnMicro-StarInternationalCo.,Ltd.:ct3:cvr2.0:"
devices:
- node: /dev/input/event25
  evdev:
    # Name: Wacom Intuos BT M Pad
    # ID: bus 0x3 vendor 0x56a product 0x378 version 0x110
    # Size in mm: unknown, missing resolution
    # Supported Events:
    # Event type 0 (EV_SYN)
    # Event type 1 (EV_KEY)
    #   Event code 256 (BTN_0)
    #   Event code 257 (BTN_1)
    #   Event code 258 (BTN_2)
    #   Event code 259 (BTN_3)
    #   Event code 331 (BTN_STYLUS)
    # Event type 3 (EV_ABS)
    #   Event code 0 (ABS_X)
    #       Value           0
    #       Min             0
    #       Max             1
    #       Fuzz            0
    #       Flat            0
    #       Resolution      0
    #   Event code 1 (ABS_Y)
    #       Value           0
    #       Min             0
    #       Max             1
    #       Fuzz            0
    #       Flat            0
    #       Resolution      0
    #   Event code 40 (ABS_MISC)
    #       Value           0
    #       Min             0
    #       Max             0
    #       Fuzz            0
    #       Flat            0
    #       Resolution      0
    # Properties:
    name: "Wacom Intuos BT M Pad"
    id: [3, 1386, 888, 272]
    codes:
      0: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] # EV_SYN
      1: [256, 257, 258, 259, 331] # EV_KEY
      3: [0, 1, 40] # EV_ABS
    absinfo:
      0: [0, 1, 0, 0, 0]
      1: [0, 1, 0, 0, 0]
      40: [0, 0, 0, 0, 0]
    properties: []
  hid: [6, 13, 255, 9, 1, 161, 1, 133, 16, 9, 32, 53, 0, 69, 0, 21, 0, 37, 1, 161, 0, 9, 66, 9, 68, 9, 90, 37, 1, 117, 1, 149, 3, 129, 2, 149, 2, 129, 3, 9, 50, 9, 54, 149, 2, 129, 2, 149, 1, 129, 3, 10, 48, 1, 101, 17, 85, 13, 71, 96, 84, 0, 0, 39, 96, 84, 0, 0, 117, 24, 149, 1, 129, 2, 10, 49, 1, 71, 188, 52, 0, 0, 39, 188, 52, 0, 0, 129, 2, 9, 48, 85, 0, 101, 0, 38, 255, 15, 117, 16, 129, 2, 117, 8, 149, 6, 129, 3, 10, 50, 1, 37, 63, 117, 8, 149, 1, 129, 2, 9, 91, 9, 92, 23, 0, 0, 0, 128, 39, 255, 255, 255, 127, 117, 32, 149, 2, 129, 2, 9, 119, 21, 0, 38, 255, 15, 117, 16, 149, 1, 129, 2, 192, 133, 17, 101, 0, 85, 0, 53, 0, 69, 0, 9, 57, 161, 0, 10, 16, 9, 10, 17, 9, 10, 18, 9, 10, 19, 9, 21, 0, 37, 1, 117, 1, 149, 4, 129, 2, 149, 4, 129, 3, 117, 8, 149, 7, 129, 3, 192, 133, 19, 101, 0, 85, 0, 53, 0, 69, 0, 10, 19, 16, 161, 0, 10, 59, 4, 21, 0, 37, 100, 117, 7, 149, 1, 129, 2, 10, 4, 4, 37, 1, 117, 1, 129, 2, 9, 0, 38, 255, 0, 117, 8, 129, 2, 117, 8, 149, 6, 129, 3, 192, 9, 14, 161, 2, 133, 2, 10, 2, 16, 21, 2, 37, 2, 117, 8, 149, 1, 177, 2, 133, 3, 10, 3, 16, 21, 0, 38, 255, 0, 149, 1, 177, 2, 133, 4, 10, 4, 16, 21, 1, 37, 1, 149, 1, 177, 2, 133, 7, 10, 9, 16, 21, 0, 38, 255, 0, 149, 1, 177, 2, 177, 3, 10, 7, 16, 9, 0, 39, 255, 255, 0, 0, 117, 16, 149, 2, 177, 2, 117, 8, 149, 9, 177, 3, 133, 12, 10, 48, 13, 10, 49, 13, 10, 50, 13, 10, 51, 13, 101, 17, 85, 13, 53, 0, 70, 200, 0, 21, 0, 38, 144, 1, 117, 16, 149, 4, 177, 2, 133, 13, 10, 13, 16, 101, 0, 85, 0, 69, 0, 37, 1, 117, 8, 149, 1, 177, 2, 133, 20, 10, 20, 16, 38, 255, 0, 149, 13, 177, 2, 133, 204, 10, 204, 16, 149, 2, 177, 2, 133, 49, 10, 49, 16, 37, 100, 149, 3, 177, 2, 149, 2, 177, 3, 192, 10, 172, 16, 161, 2, 21, 0, 38, 255, 0, 117, 8, 133, 172, 9, 0, 150, 191, 0, 129, 2, 133, 21, 9, 0, 149, 14, 177, 2, 133, 51, 9, 0, 149, 18, 177, 2, 133, 68, 9, 0, 149, 4, 177, 2, 133, 69, 9, 0, 149, 32, 177, 2, 133, 96, 9, 0, 149, 63, 177, 2, 133, 97, 9, 0, 149, 62, 177, 2, 133, 98, 9, 0, 149, 62, 177, 2, 133, 101, 9, 0, 149, 4, 177, 2, 133, 102, 9, 0, 149, 4, 177, 2, 133, 103, 9, 0, 149, 4, 177, 2, 133, 104, 9, 0, 149, 17, 177, 2, 133, 111, 9, 0, 149, 62, 177, 2, 133, 205, 9, 0, 149, 2, 177, 2, 133, 22, 9, 0, 149, 14, 177, 2, 133, 53, 9, 0, 149, 10, 177, 2, 192, 133, 208, 9, 1, 150, 8, 0, 177, 2, 133, 209, 9, 1, 150, 4, 1, 177, 2, 133, 210, 9, 1, 150, 4, 1, 177, 2, 133, 211, 9, 1, 150, 4, 0, 177, 2, 133, 212, 9, 1, 150, 4, 0, 177, 2, 133, 213, 9, 1, 150, 4, 0, 177, 2, 133, 214, 9, 1, 150, 4, 0, 177, 2, 133, 215, 9, 1, 150, 8, 0, 177, 2, 133, 216, 9, 1, 150, 12, 0, 177, 2, 133, 217, 9, 1, 150, 0, 5, 177, 2, 133, 218, 9, 1, 150, 4, 2, 177, 2, 133, 219, 9, 1, 150, 6, 0, 177, 2, 133, 220, 9, 1, 150, 2, 0, 177, 2, 133, 221, 9, 1, 150, 4, 0, 177, 2, 133, 222, 9, 1, 150, 4, 0, 177, 2, 133, 223, 9, 1, 150, 34, 0, 177, 2, 133, 224, 9, 1, 150, 1, 0, 177, 2, 133, 225, 9, 1, 150, 2, 0, 177, 2, 133, 226, 9, 1, 150, 2, 0, 177, 2, 133, 227, 9, 1, 150, 2, 0, 177, 2, 133, 228, 9, 1, 150, 255, 1, 177, 2, 192 ]
  udev:
    properties:
    - ID_INPUT=1
    - ID_INPUT_TABLET=1
    - ID_INPUT_TABLET_PAD=1
    - LIBINPUT_DEVICE_GROUP=3/56a/378:usb-0000:29:00.3-2
  quirks:
  events:
  - evdev:
    - [  0,      0,   1, 256,       1] # EV_KEY / BTN_0                     1
  - evdev:
    - [  0,      0,   3,  40,      15] # EV_ABS / ABS_MISC                 15 (+15)
    - [  0,      0,   0,   0,       0] # ------------ SYN_REPORT (0) ---------- +0ms
  - evdev:
    - [  0, 214000,   1, 256,       0] # EV_KEY / BTN_0                     0
    - [  0, 214000,   3,  40,       0] # EV_ABS / ABS_MISC                  0 (-15)
    - [  0, 214000,   0,   0,       0] # ------------ SYN_REPORT (0) ---------- +214ms

These are exactly the same events as the ones we reverse engineered above, which is a good sign; libinput gets the same events as we are, but it seems to decide that they aren’t worth sending further. Maybe if we dig a bit into libinput we can find out how devices and events are treated, set a breakpoint somewhere when libinput is reading the button press event and see what happens.

The libinput docs has an overview over libinput.

evdev_device_create calls evdev_configure_device with the device as a parameter; device->devname contains the name of the device and will contain Wacom for the devices we’re interested in.

(gdb) break evdev_configure_device if ((int) strstr(device->devname, "acom"))
(gdb) c
Continuing.

Breakpoint 3, evdev_configure_device (device=0x55555566f000) at ../src/evdev.c:1775
1775		struct libevdev *evdev = device->evdev;
(gdb) p device->devname
$7 = 0x5555556365f0 "Wacom Intuos BT M Pad"
(gdb)

We’re stepping through the function to see whether anything strange is happening. We would like it to be recognized as a tablet pad so that the correct dispatch methods are set up.

(gdb) p udev_tags
$8 = (EVDEV_UDEV_TAG_INPUT | EVDEV_UDEV_TAG_TABLET | EVDEV_UDEV_TAG_TABLET_PAD)

So far so good. Continuing down it does correctly go into the if on line 1852, and calls evdev_tablet_pad_create. The pad is thus identified as a tablet pad. Now we need to find out how events are read in libinput.

Apparently, libinput uses libevdev which is a wrapper library for evdev devices. So instead of reading the files in /dev/input like we did above, we can get the events from handles that we get through libevdev.

The function libevdev_next_event is called in four places in src/evdev.c, but the most promising one is in evdev_device_dispatch. We can set a conditional breakpoint here for when our event is coming through7:

(gdb) break evdev.c:1061 if (ev->type == 1 && ev->code == 0x100)
No source file named evdev.c.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (evdev.c:1061 if (ev->type == 1 && ev->code == 0x100)) pending.
(gdb) run debug-events
Starting program: /home/mht/src/libinput/build/libinput debug-events
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
process 48478 is executing new program: /home/mht/src/libinput/build/libinput-debug-events
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
-event1   DEVICE_ADDED     Power Button                      seat0 default group1  cap:k
-event0   DEVICE_ADDED     Power Button                      seat0 default group2  cap:k
-event20  DEVICE_ADDED     Logitech Performance MX           seat0 default group3  cap:p left scroll-nat scroll-button
-event21  DEVICE_ADDED     Kingsis Peripherals Evoluent VerticalMouse 4 seat0 default group4  cap:p left scroll-nat scroll-button
-event26  DEVICE_ADDED     HD Pro Webcam C920                seat0 default group5  cap:k
-event24  DEVICE_ADDED     Wacom Intuos BT M Pen             seat0 default group6  cap:T  size 216x135mm
-event25  DEVICE_ADDED     Wacom Intuos BT M Pad             seat0 default group6  cap:P buttons:4 strips:0 rings:0 mode groups:1
-event17  DEVICE_ADDED     ZSA Ergodox EZ                    seat0 default group7  cap:k
-event18  DEVICE_ADDED     ZSA Ergodox EZ Mouse              seat0 default group7  cap:p left scroll-nat scroll-button
-event19  DEVICE_ADDED     ZSA Ergodox EZ System Control     seat0 default group7  cap:k
-event22  DEVICE_ADDED     ZSA Ergodox EZ Consumer Control   seat0 default group7  cap:kp scroll-nat
-event23  DEVICE_ADDED     ZSA Ergodox EZ Keyboard           seat0 default group7  cap:k

Breakpoint 1, evdev_device_dispatch (data=0x5555556718e0) at ../src/evdev.c:1061
1061			if (rc == LIBEVDEV_READ_STATUS_SYNC) {
(gdb) n
1075			} else if (rc == LIBEVDEV_READ_STATUS_SUCCESS) {
(gdb) n
1076				if (!once) {
(gdb) n
1077					evdev_note_time_delay(device, &ev);
(gdb) n
1078					once = true;
(gdb) n
1080				evdev_device_dispatch_one(device, &ev);
(gdb) s
evdev_device_dispatch_one (device=0x5555556718e0, ev=0x7fffffffdf60) at ../src/evdev.c:989
989	{
(gdb) n
990		if (!device->mtdev) {
(gdb) n
991			evdev_process_event(device, ev);
(gdb) s
evdev_process_event (device=0x5555556718e0, e=0x7fffffffdf60) at ../src/evdev.c:974
974		struct evdev_dispatch *dispatch = device->dispatch;
(gdb) n
975		uint64_t time = input_event_time(e);
(gdb) n
981		libinput_timer_flush(evdev_libinput_context(device), time);
(gdb) n
983		dispatch->interface->process(dispatch, device, e, time);
(gdb) s
pad_process (dispatch=0x555555674c00, device=0x5555556718e0, e=0x7fffffffdf60, time=33217800469) at ../src/evdev-tablet-pad.c:483
483		struct pad_dispatch *pad = pad_dispatch(dispatch);
(gdb) bt
#0  pad_process (dispatch=0x555555674c00, device=0x5555556718e0, e=0x7fffffffdf60, time=33217800469)
    at ../src/evdev-tablet-pad.c:483
#1  0x00007ffff7f7bd06 in evdev_process_event (device=0x5555556718e0, e=0x7fffffffdf60) at ../src/evdev.c:983
#2  0x00007ffff7f7bd4b in evdev_device_dispatch_one (device=0x5555556718e0, ev=0x7fffffffdf60) at ../src/evdev.c:991
#3  0x00007ffff7f7bfef in evdev_device_dispatch (data=0x5555556718e0) at ../src/evdev.c:1080
#4  0x00007ffff7f74f06 in libinput_dispatch (libinput=0x5555555773b0) at ../src/libinput.c:2125
#5  0x000055555555d1e0 in handle_and_print_events (li=0x5555555773b0) at ../tools/libinput-debug-events.c:827
#6  0x000055555555d6df in mainloop (li=0x5555555773b0) at ../tools/libinput-debug-events.c:953
#7  0x000055555555db1e in main (argc=1, argv=0x7fffffffe588) at ../tools/libinput-debug-events.c:1091
(gdb) list
478	pad_process(struct evdev_dispatch *dispatch,
479		    struct evdev_device *device,
480		    struct input_event *e,
481		    uint64_t time)
482	{
483		struct pad_dispatch *pad = pad_dispatch(dispatch);
484	
485		switch (e->type) {
486		case EV_ABS:
487			pad_process_absolute(pad, device, e, time);
(gdb) n
485		switch (e->type) {
(gdb) n
490			pad_process_key(pad, device, e, time);
(gdb) s
pad_process_key (pad=0x555555674c00, device=0x5555556718e0, e=0x7fffffffdf60, time=33217800469) at ../src/evdev-tablet-pad.c:332
332		uint32_t button = e->code;
(gdb) p e
$1 = (struct input_event *) 0x7fffffffdf60
(gdb) p *e
$2 = {time = {tv_sec = 33217, tv_usec = 800469}, type = 1, code = 256, value = 1}
(gdb) n
333		uint32_t is_press = e->value != 0;
(gdb) n
336		if (e->value == 2)
(gdb) n
339		pad_button_set_down(pad, button, is_press);
(gdb) s
pad_button_set_down (pad=0x555555674c00, button=256, is_down=true) at ../src/evdev-tablet-pad.c:88
88		struct button_state *state = &pad->button_state;
(gdb) list
83	static inline void
84	pad_button_set_down(struct pad_dispatch *pad,
85			    uint32_t button,
86			    bool is_down)
87	{
88		struct button_state *state = &pad->button_state;
89	
90		if (is_down) {
91			set_bit(state->bits, button);
92			pad_set_status(pad, PAD_BUTTONS_PRESSED);
(gdb) n
90		if (is_down) {
(gdb) n
91			set_bit(state->bits, button);
(gdb) n
92			pad_set_status(pad, PAD_BUTTONS_PRESSED);
(gdb) n
97	}

At this point it’s apparent that the event is going through the code successfully. That is, the pad is recognized as a pad, and events from libevdev are correctly setting the state of the evdev_device in libinput. But, we’re not getting any events, so where are events in libinput made?

Events in libinput

In order to find out how events in libinput works we can take a look in the only place so far that we know we’ve seen them: in libinput-debug-events8. libinput-debug-events.c has a main function that does some argument parsing and initialization, and then calls mainloop, which contains a do while loop, which polls a (the?) fd from libinput, and calls handle_and_print_events. This function gets all events with libinput_get_event, and has a giant switch to dispatch how the event should be printed. But, there is no default branch so, while unlikely, we might already hit a dead end. We jump back into gdb to test this (running with permissions; otherwise we get nothing!).

Oh, that’s right, my keyboard is also sending events, and so instead of cing though the initialization events until I get to press the button on the pad and see what happens, I’m getting swamped with events for me pressing c9! Okay; we’ll just add a default case to the switch, put a printf there, and break on it.

But oh, there’s nothing coming through.

Okay, so let’s see where libinput_get_event get its events. It’s here, from the circular buffer libinput->events. Let’s see where this is written to:

$ rg "events\[.*\]\s*=" src/
src/libinput.c
2971:	events[libinput->events_in] = event;

src/evdev-tablet.c
2000:	struct input_event events[2] = {

Oh, that’s five lines over the function we just looked at. The magical function in question is libinput_post_event, and so, presumably, all events we’re getting from libevdev should end up being sent to post_event, and our precious button click isn’t. This function is also sparingly called:

$ rg "libinput_post_event" src/
src/libinput.c
335:libinput_post_event(struct libinput *libinput,
2217:	libinput_post_event(libinput, event);
2244:	libinput_post_event(device->seat->libinput, event);
2923:libinput_post_event(struct libinput *libinput,

The first occurrence is the prototype, the second is in post_base_event, the third is in post_device_event which sounds promising, and the fourth is the function itself. The problem is that both of these are static, and so they have quite a few callers in the file. We want to get closer to where the mapping from libevdev events to struct libinput_events are, so maybe it makes sense to go hunting for where the libinput_events are initialized. There’s even a struct libinput_event_tablet_pad. Looking further, we find the tablet_pad_notify_button function which creates a libinput_event_tablet_pad, and sends to to post_device_event. This is probably where the button click should en up.

$ rg "tablet_pad_notify_button" src/
src/libinput.c
2700:tablet_pad_notify_button(struct libinput_device *device,

src/libinput-private.h
662:tablet_pad_notify_button(struct libinput_device *device,

src/evdev-tablet-pad.c
394:				tablet_pad_notify_button(base,

The call in evdev-tablet-pad.c comes from the function pad_notify_button_mask, which we’ll breakpoint.

/h/m/s/l/build$ sudo gdb ./libinput-debug-events
[sudo] password for mht:
GNU gdb (GDB) 9.2
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-pc-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./libinput-debug-events...
(gdb) break pad_notify_button_mask
Function "pad_notify_button_mask" not defined.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (pad_notify_button_mask) pending.
(gdb) run
Starting program: /home/mht/src/libinput/build/libinput-debug-events
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
-event1   DEVICE_ADDED     Power Button                      seat0 default group1  cap:k
-event0   DEVICE_ADDED     Power Button                      seat0 default group2  cap:k
-event20  DEVICE_ADDED     Logitech Performance MX           seat0 default group3  cap:p left scroll-nat scroll-button
-event21  DEVICE_ADDED     Kingsis Peripherals Evoluent VerticalMouse 4 seat0 default group4  cap:p left scroll-nat scroll-button
-event26  DEVICE_ADDED     HD Pro Webcam C920                seat0 default group5  cap:k
-event24  DEVICE_ADDED     Wacom Intuos BT M Pen             seat0 default group6  cap:T  size 216x135mm
-event25  DEVICE_ADDED     Wacom Intuos BT M Pad             seat0 default group6  cap:P buttons:4 strips:0 rings:0 mode groups:1
-event17  DEVICE_ADDED     ZSA Ergodox EZ                    seat0 default group7  cap:k
-event18  DEVICE_ADDED     ZSA Ergodox EZ Mouse              seat0 default group7  cap:p left scroll-nat scroll-button
-event19  DEVICE_ADDED     ZSA Ergodox EZ System Control     seat0 default group7  cap:k
-event22  DEVICE_ADDED     ZSA Ergodox EZ Consumer Control   seat0 default group7  cap:kp scroll-nat
-event23  DEVICE_ADDED     ZSA Ergodox EZ Keyboard           seat0 default group7  cap:k

Breakpoint 1, pad_notify_button_mask (pad=0x555555672b30, device=0x55555566f400, time=42251078117, buttons=0x7fffffffddb0,
    state=LIBINPUT_BUTTON_STATE_PRESSED) at ../src/evdev-tablet-pad.c:365
365		struct libinput_device *base = &device->base;

And it fires! We’re now here, and we would like to get to 394, or 402, which leads to tablet_pad_notify_key, which seems to be basically the same but different. We can set breakpoints and run, just in case we do end up in either:

(gdb) break 394
Breakpoint 2 at 0x7ffff7fa3b13: file ../src/evdev-tablet-pad.c, line 394.
(gdb) break 402
Breakpoint 3 at 0x7ffff7fa3b49: file ../src/evdev-tablet-pad.c, line 402.
(gdb) c
Continuing.

Breakpoint 1, pad_notify_button_mask (pad=0x555555672b30, device=0x55555566f400, time=42251378122, buttons=0x7fffffffddb0,
    state=LIBINPUT_BUTTON_STATE_RELEASED) at ../src/evdev-tablet-pad.c:365
365		struct libinput_device *base = &device->base;
(gdb) c
Continuing.

but we don’t. We just get the second event, which is the key release. This is good, because we know exactly where our event gets lost.

The Last Missing Piece?

Now it’s time to make sense of what’s going on in that for loop. A quick gdb p of buttons->bits shows that they’re mostly 0, so we’ll put another breakpoint on line 378, just inside the while loop, which we also hit. Here are the local variables at that time:

(gdb) info locals
enabled = 21845
map = {value = 1432824624}
buttons_slice = 1 '\001'
base = 0x55555566f400
group = 0x555555672bd8
code = 256
i = 32

Note that enabled and map are garbage values so far. Stepping down to the first if changes things a little:

(gdb) info locals
enabled = 1
map = {value = 1432824624}
buttons_slice = 0 '\000'
base = 0x55555566f400
group = 0x555555672bd8
code = 257
i = 32

which means we’re enabled. Good. Then we step past the map assignment

(gdb) p map
$4 = {value = 4294967295}

Now, since we know that we didn’t get to either tablet_pad_notify function, or the abort call, map_is_unmapped will be true, which it is:

(gdb) n
387					continue;

How does one know whether a map is unmapped? Well,

#define map_is_unmapped(x_) ((x_).value == (uint32_t)-1)

and (uint32_t) -1 == 4294967295, which means that we need to rewind, and look at the line

map = pad->button_map[code - 1];

Looking further at what’s in this map gives us a very important clue:

(gdb) p pad->button_map
$7 = {{value = 4294967295} <repeats 272 times>, {value = 0}, {value = 1}, {value = 4294967295}, {value = 4294967295}, {
    value = 4294967295}, {value = 2}, {value = 3}, {value = 4294967295} <repeats 489 times>}
(gdb) p pad->button_map[272]
$8 = {value = 0}
(gdb) p pad->button_map[273]
$9 = {value = 1}
(gdb) p pad->button_map[277]
$10 = {value = 2}
(gdb) p pad->button_map[278]
$11 = {value = 3}

These are exactly10 the numbers that we saw in the output from libwacom-list-local-devices, and that we bothered translating from hex to decimal! So the map is here, but we’re skipping the iteration with the button click because we’re mistaken in which index to look at. At this point, I really hoped this was a off-by-one thing and that code - 1 was 271. But,

(gdb) p code
$19 = 257

This is when clicking the first button, and clicking the second yields code == 258 and so on. In other words, it looks like we’re off by 16 bits.

Let’s get the overview: buttons->bits is a byte array of 96 bytes, and we’re looking at which bits are set. To do this, we look at each byte (this is the for loop), and look at each bit in that byte until buttons_slice, the current byte, is 0 (this is the while loop). Our problem is that code, which is the bit offset in the whole byte array, is off by 16, i.e. two bytes. In other words, we need to find out where buttons->bits are set.

For at least one caller of the function, pad_notify_buttons, the buttons are set in pad_get_buttons_{pressed,released}. Looking at the stack trace (with bt in gdb) we see this is indeed the place where we come from. But the logic there is very simple, and leaves no room for errors such as this. In addition, pad->button_state has the same error:

(gdb) p pad->button_state
$16 = {bits = '\000' <repeats 32 times>, "\001", '\000' <repeats 62 times>}

We know this is wrong, since we are supposed to end up at 272. Well, according to libwacom anyways.

Back to libwacom

At this point I’m getting suspicious. How certain are we really that the mapping isn’t set up wrong? After all, in the evdev events we read out from /dev/input/event25 was 0x0100 == BTN_0, and not 272 == 0x110 == BTN_LEFT, which I think is strangely well fitting for out problem. This would also make sense with libinput, since it presumably queries either the pad itself or libwacom to get the mapping, but there’s a mismatch between the mapping and what’s really being sent.

Let’s push our current bug hunt onto our mental stack, and try to look at this map instead. Okay, so where does libwacom-list-local-devices get those numbers from? tools/list-local-devices.c contains a call to libwacom_print_device_description() in libwacom.c, which again calls print_buttons_for_device, which again calls print_button_evdev_codes, which calls libwacom_get_button_evdev_code. This function basically indexes into device->button_codes, which we now assume are wrong.

The button codes are set in this file, but by simple inspection it’s not clear what’s wrong, so we clone the repository, and build the tool ourselves with the build instructions from the wiki. We compile, start up gdb, set the breakpoints, but uh oh, SIGSEGV. I revert to the libwacom-1.3 tag, and now we don’t segfault any more, but we get

Failed to initialize device database

which we solve by passing --database ../data when running. All is well, and the evdev codes are still 0x110 and counting. We run it in gdb:

(gdb) set args --database ../data
(gdb) break set_button_codes_from_heuristics if (device->model_name && ((int) strcmp(device->model_name, "CTL-6100WL")) == 0)
Breakpoint 5 at 0x7ffff7fc215e: file ../libwacom/libwacom-database.c, line 418.
(gdb) break set_button_codes_from_string if (device->model_name && ((int) strcmp(device->model_name, "CTL-6100WL")) == 0)
Breakpoint 6 at 0x7ffff7fc2024: file ../libwacom/libwacom-database.c, line 391.
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/mht/src/libwacom/builddir/libwacom-list-local-devices --database ../data
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".

Breakpoint 5, set_button_codes_from_heuristics (device=0x5555555acdd0) at ../libwacom/libwacom-database.c:418
418		for (i = 0; i < device->num_buttons; i++) {
(gdb) p *device
$16 = {name = 0x5555555a30c0 "Wacom Intuos BT M", model_name = 0x5555555acc90 "CTL-6100WL", width = 9, height = 5, match = 0,
  matches = 0x5555555acbb0, nmatches = 2, paired = 0x0, cls = WCLASS_BAMBOO, num_strips = 0, features = 1, integration_flags = 0,
  strips_num_modes = 0, ring_num_modes = 0, ring2_num_modes = 0, num_styli = 1, supported_styli = 0x5555555abbe0, num_buttons = 4,
  buttons = 0x5555555ad100, button_codes = 0x5555555ad090, num_leds = 0, status_leds = 0x0,
  layout = 0x555555586690 "../data/layouts/intuos-m-p3.svg", refcnt = 1}
(gdb) list
413	
414	static inline void
415	set_button_codes_from_heuristics(WacomDevice *device)
416	{
417		gint i;
418		for (i = 0; i < device->num_buttons; i++) {
419			if (device->cls == WCLASS_BAMBOO ||
420			    device->cls == WCLASS_GRAPHIRE) {
421				switch (i) {
422				case 0:
(gdb)

So we’re in set_button_codes_from_heuristics, and since our device class is BAMBOO, although I don’t know why that is, we default to BTN_LEFT as the first button, which is 0x110.

The Fix

I’m not really sure what the Class field does in this config, apart from heuristically setting key codes, but the fix that made it all word was simple: set the class to something else. I changed it on my system (the file was in /usr/share/libwacom/intuos-m-p3-wl.tablet), and submitted a PR upstream. All in all, this adventure took my entire Saturday, and the fix was one line, but I’m finally getting events when I’m pressing the buttons.

Now, how do I make these buttons to anything useful?

Thanks for reading.


  1. Never mind that XournalPP doesn’t have good (or decent, or any?) support for key rebinding. ↩︎

  2. Interestingly, we don’t even have to touch the pad itself; it seems to be sufficient for the tip of the pen to be pushed in for the action to be interpreted as drawing. ↩︎

  3. This is supported by the output of debug-events, which humorously states that the size of the pen is 216x135mm. ↩︎

  4. two things were confusing: the fact that in the format string you need a space in between the byte count and the format, which was not explicitly stated, and that “squeezing” is by default on, which completely messes up the output if you are defining your own format. ↩︎

  5. It would probably be easier to just write a small program and cast a pointer to an array with the data we read to the struct we suspect. ↩︎

  6. I guess uapi is for user space, and that the directory is superfluous when you’re not doing kernel dev? ↩︎

  7. This represents most of my workflow in gdb: set breakpoints, n or s down wherever, list unless I have the source code right by, and p expressions; sometimes I’ll also pt for when I don’t know the types of things. It’s… not great? But it’s alright. I would like to have better integration in my text editor, that is, I don’t really want to leave my text editor when debugging, since mentally I’m doing the same in both programs, but I haven’t actually bothered seeing what’s out there. My experience from trying out gdb integration in vim was pretty bad, and if it doesn’t work well in vim, I don’t see how semi-obscure editors stand a chance. ↩︎

  8. At least this is a place where we know that some of the libevdev events are coming through and some are not. ↩︎

  9. I’m sure there’s a way of breaking conditionally based on the event type, and I browsed through the types a little bit with gdb, but couldn’t find anything that seemed useful. When the alternative, adding a default branch to a switch in a codebase I already had cloned and build, was so simple, it makes sense to do, despite not really being what I wanted to do. ↩︎

  10. Had this been in a textbook I would think “yeah sure, that’s reeeaaallly convenient how that minor thing we did way back when turned out to be useful.”, but I promise, I did not go back and added the conversion after the fact! ↩︎

Creative Commons Licence
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License