2

I would like to simulate a graphics tablet (like wacom's ones) from a script. The "good" way to proceed seems to use libevent that is an abstract layer above uinput. So I tried to write a script that sends some event like EV_ABS.ABS_X or EV_ABS.ABS_PRESSURE, using the python library of libevent. Unfortunately, when I use Krita/Gimp/... to test it, the line that it draws does not change it's shape depending on the pressure, and xinput test-xi2 does not display any pressure event. Any idea why?

Thanks!

Steps to reproduce

Execute the code below:

sudo pip3 install libevdev
chmod +x ./simulate_graphics_tablet.py
sudo ./simulate_graphics_tablet.py

Then you have 30 seconds to:

  • either run xinput list and then xinput test-xi2 <number of Tablet alone>
  • or open Gimp, go to 'Edit/Input device' and configure the device 'Tablet alone' to 'Screen', save and close the pop up,create a new file (Ctrl-N), Zoom and press 'tab' to have a drawing surface covering most of the screen. Switch to brush using 'p' key, by making sure the brush is set to Pressure size.

What I get: a uniform line, and on xinput I don't have any reference to pressure and I just have things like:

EVENT type 17 (RawMotion)
    device: 11 (11)
    detail: 0
    flags: 
    valuators:
          0: 29897.54 (29897.54)
          1: 29897.54 (29897.54)

What I expect: a line whose size is not constatn (the script increase the pressure linearly), or to see some pressure-related events on xinput.

#!/usr/bin/env python3
import sys
import libevdev
import time

def main(args):
    dev = libevdev.Device()
    dev.name = "Tablet alone"
    dev.enable(libevdev.EV_ABS.ABS_X,
               libevdev.InputAbsInfo(minimum=0, maximum=32767))
    dev.enable(libevdev.EV_ABS.ABS_Y,
               libevdev.InputAbsInfo(minimum=0, maximum=32767))
    dev.enable(libevdev.EV_ABS.ABS_Z,
               libevdev.InputAbsInfo(minimum=0, maximum=8191))
    # dev.enable(libevdev.EV_ABS.ABS_0B,
    #            libevdev.InputAbsInfo(minimum=0, maximum=8191))
    # dev.enable(libevdev.EV_ABS.ABS_DISTANCE,
    #            libevdev.InputAbsInfo(minimum=0, maximum=8191))
    dev.enable(libevdev.EV_ABS.ABS_PRESSURE,
               libevdev.InputAbsInfo(minimum=0, maximum=8191))
    dev.enable(libevdev.EV_MSC.MSC_SCAN)
    dev.enable(libevdev.EV_KEY.KEY_P)
    dev.enable(libevdev.EV_KEY.BTN_LEFT)
    dev.enable(libevdev.EV_KEY.BTN_RIGHT)
    dev.enable(libevdev.EV_KEY.BTN_MIDDLE)
    dev.enable(libevdev.EV_KEY.BTN_TOUCH)
    dev.enable(libevdev.EV_SYN.SYN_REPORT)
    dev.enable(libevdev.EV_SYN.SYN_CONFIG)
    dev.enable(libevdev.EV_SYN.SYN_MT_REPORT)
    dev.enable(libevdev.EV_SYN.SYN_DROPPED)
    dev.enable(libevdev.EV_SYN.SYN_04)
    dev.enable(libevdev.EV_SYN.SYN_05)
    dev.enable(libevdev.EV_SYN.SYN_06)
    dev.enable(libevdev.EV_SYN.SYN_07)
    dev.enable(libevdev.EV_SYN.SYN_08)
    dev.enable(libevdev.EV_SYN.SYN_09)
    dev.enable(libevdev.EV_SYN.SYN_0A)
    dev.enable(libevdev.EV_SYN.SYN_0B)
    dev.enable(libevdev.EV_SYN.SYN_0C)
    dev.enable(libevdev.EV_SYN.SYN_0D)
    dev.enable(libevdev.EV_SYN.SYN_0E)
    dev.enable(libevdev.EV_SYN.SYN_MAX)
    try:
        uinput = dev.create_uinput_device()
        print("New device at {} ({})".format(uinput.devnode, uinput.syspath))
        # Sleep for a bit so udev, libinput, Xorg, Wayland, ...
        # all have had a chance to see the device and initialize
        # it. Otherwise the event will be sent by the kernel but
        # nothing is ready to listen to the device yet.
        print("Waiting 30s to let you:")
        print("1) open Gimp")
        print("2) Go to 'Edit/Input device' and configure the device 'Tablet alone' to 'Screen'.")
        print("3) Save and close the pop up")
        print("4) Create a new file (Ctrl-N)")
        print("5) Zoom and press 'tab' to have a drawing surface coverint most of the screen.")
        print("6) Switch to brush using 'p' key.")
        time.sleep(30)

        pc = 0
        direc = +1
        already_pressed_one = False
        # uinput.send_events([
        #     libevdev.InputEvent(libevdev.EV_KEY.KEY_P, 1),
        #     libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT, 0),
        # ])
        # time.sleep(0.1)
        # uinput.send_events([
        #     libevdev.InputEvent(libevdev.EV_KEY.KEY_P, 0),
        #     libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT, 0),
        # ])
        for i in range(250):
            pc_ = pc/100
            val_x = int(pc_*10000 + (1-pc_)*17767)
            val_y = int(pc_*5000 + (1-pc_)*22767)
            val_pres = int(pc_*10 + (1-pc_)*6000)
            print("Will send: x={}, y={}, press={} (pc={})".format(
                val_x,
                val_y,
                val_pres,
                pc))
            uinput.send_events([
                libevdev.InputEvent(libevdev.EV_ABS.ABS_PRESSURE, val_pres),
                libevdev.InputEvent(libevdev.EV_ABS.ABS_X, val_y),
                libevdev.InputEvent(libevdev.EV_ABS.ABS_Y, val_y),
                libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT, 0),
            ])
            pc += direc
            if not already_pressed_one:
                print("Press!")
                uinput.send_events([
                    libevdev.InputEvent(libevdev.EV_KEY.BTN_LEFT, 1),
                    libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT, 0),
                ])                
                already_pressed_one = True
            if pc >= 100 or pc <=0 :
                print("Release click.")
                uinput.send_events([
                    libevdev.InputEvent(libevdev.EV_KEY.BTN_LEFT, 0),
                    libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT, 0),
                ])
                if pc >= 100:
                    pc = 100
                    direc = -1
                if pc <= 0:
                    pc = 0
                    direc = +1
                time.sleep(10)
                print("Press!")
                uinput.send_events([
                    libevdev.InputEvent(libevdev.EV_KEY.BTN_LEFT, 1),
                    libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT, 0),
                ])
                already_pressed_one = True
            time.sleep(0.1)

    except KeyboardInterrupt:
        pass
    except OSError as e:
        print(e)


if __name__ == "__main__":
    if len(sys.argv) > 2:
        print("Usage: {}")
        sys.exit(1)
    main(sys.argv)

EDIT: I tried to play with ABS_KEY.BTN_TOOL_PEN as explained here, but I don't know why the second I enable it it stops being detected by xinput list:

dev.enable(libevdev.EV_KEY.BTN_TOOL_PEN)

1 Answer 1

4

After some time and doc/code reading, I finally found the solution.

If you just want to test the script (at the end of this answer), just run it as root with:

$ chmod +x completely_fake_tablet.py
$ sudo pip3 install libevdev
$ sudo ./completely_fake_tablet.py

then open gimp, add the device as an input device, choose a brush dynamic like Pencil Generic and enjoy. If you want to have more details, read the following:

To sum up, the system is very picky, and you want to make sure:

  • You enable libevdev.INPUT_PROP_DIRECT to say you have a tablet-like device (see doc link above)
  • You enable all tablet-like tools like :

    • libevdev.EV_KEY.BTN_TOOL_PEN that will be used to say when then pen is close to the tablet or not, libevdev.EV_KEY.BTN_TOUCH to say when you click
    • libevdev.EV_KEY.BTN_STYLUS/libevdev.EV_KEY.BTN_STYLUS2 that corresponds to the button present on the pen
    • libevdev.EV_ABS.ABS_{X,Y} for the position (make sure to specify minimum value, maximum value, and resolution: without resolution, the device will never be detected!)
    • libevdev.EV_ABS.ABS_PRESSURE for the pressure
    • and libevdev.EV_SYN.SYN_REPORT that must be sent every time your send a block of information. If you don't send this, the kernel will not proceed the events, or with a very slow rate like 1/s.

You also want to make sure you wait at least one second after enabling the device before sending any event, otherwise the tablet will not be recognized in the futur. I also noticed that the Pen part of the mouse is not listed in xinput until I send a first event. Note that xinput will list two devices, one for the buttons I guess as a keyboard, and one as a pen (named Tablet alone Pen (0) and Tablet alone:

$ xinput list
⎡ Virtual core pointer                          id=2    [master pointer  (3)]
⎜   ↳ Virtual core XTEST pointer                id=4    [slave  pointer  (2)]
⎜   ↳ ETPS/2 Elantech Touchpad                  id=17   [slave  pointer  (2)]
⎜   ↳ lircd-uinput                              id=18   [slave  pointer  (2)]
⎜   ↳ Tablet alone Pen (0)                      id=12   [slave  pointer  (2)]
⎣ Virtual core keyboard                         id=3    [master keyboard (2)]
    ↳ Virtual core XTEST keyboard               id=5    [slave  keyboard (3)]
    ↳ Power Button                              id=6    [slave  keyboard (3)]
    ↳ Asus Wireless Radio Control               id=7    [slave  keyboard (3)]
    ↳ Video Bus                                 id=8    [slave  keyboard (3)]
    ↳ Video Bus                                 id=9    [slave  keyboard (3)]
    ↳ Sleep Button                              id=10   [slave  keyboard (3)]
    ↳ USB2.0 HD UVC WebCam: USB2.0 HD           id=14   [slave  keyboard (3)]
    ↳ Asus WMI hotkeys                          id=15   [slave  keyboard (3)]
    ↳ AT Translated Set 2 keyboard              id=16   [slave  keyboard (3)]
    ↳ lircd-uinput                              id=19   [slave  keyboard (3)]
    ↳ Tablet alone                              id=11   [slave  keyboard (3)]

If you test with Gimp, make sure you open your software after xinput is able to list it, otherwise it won't be listed in input devices and you will need to restart gimp (note that you can then restart the script without restarting gimp). You also want to set in Edit/input device the device Tablet alone to Screen, and to choose a brush whose dynamic is set to something like Pencil Generic like in the screenshot. For the script to work, you may also want as well to press Tab to have a wider drawing area (Tab again to come back to normal window), and to zoom until all the area is covered.

enter image description here

Script:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import libevdev
import time
## Some doc needed for this project
# http://www.infradead.org/~mchehab/kernel_docs_pdf/linux-input.pdf

## Some code to get inspiration from
# https://github.com/linuxwacom/input-wacom/blob/master/4.5/wacom_w8001.c

## Some doc to read at some point in my life:
# https://lwn.net/Kernel/LDD3/
# https://www.kernel.org/doc/html/v4.11/driver-api/index.html

def main(args):
    dev = libevdev.Device()
    dev.name = "Tablet alone"
    ### NB: all the following information needs to be enabled
    ### in order to recognize the device as a tablet.
    # Say that the device will send "absolute" values
    dev.enable(libevdev.INPUT_PROP_DIRECT)
    # Say that we are using the pen (not the erasor), and should be set to 1 when we are at proximity to the device.
    # See http://www.infradead.org/~mchehab/kernel_docs_pdf/linux-input.pdf page 9 (=13) and guidelines page 12 (=16), or the https://github.com/linuxwacom/input-wacom/blob/master/4.5/wacom_w8001.c (rdy=proximity)
    dev.enable(libevdev.EV_KEY.BTN_TOOL_PEN)
    dev.enable(libevdev.EV_KEY.BTN_TOOL_RUBBER)
    # Click
    dev.enable(libevdev.EV_KEY.BTN_TOUCH)
    # Press button 1 on pen
    dev.enable(libevdev.EV_KEY.BTN_STYLUS)
    # Press button 2 on pen, see great doc
    dev.enable(libevdev.EV_KEY.BTN_STYLUS2)
    # Send absolute X coordinate
    dev.enable(libevdev.EV_ABS.ABS_X,
               libevdev.InputAbsInfo(minimum=0, maximum=32767, resolution=100))
    # Send absolute Y coordinate
    dev.enable(libevdev.EV_ABS.ABS_Y,
               libevdev.InputAbsInfo(minimum=0, maximum=32767, resolution=100))
    # Send absolute pressure
    dev.enable(libevdev.EV_ABS.ABS_PRESSURE,
               libevdev.InputAbsInfo(minimum=0, maximum=8191))
    # Use to confirm that we finished to send the informations
    # (to be sent after every burst of information, otherwise
    # the kernel does not proceed the information)
    dev.enable(libevdev.EV_SYN.SYN_REPORT)
    # Report buffer overflow
    dev.enable(libevdev.EV_SYN.SYN_DROPPED)
    try:
        uinput = dev.create_uinput_device()
        print("New device at {} ({})".format(uinput.devnode, uinput.syspath))
        # Sleep for a bit so udev, libinput, Xorg, Wayland, ...
        # all have had a chance to see the device and initialize
        # it. Otherwise the event will be sent by the kernel but
        # nothing is ready to listen to the device yet. And it
        # will never be detected in the futur ;-)
        time.sleep(1) 
        # Reports that the PEN is close to the surface
        # Important to make sure xinput can detect (and list)
        # the pen. Otherwise, it won't write anything in gimp.
        uinput.send_events([
            libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH,
                                value=0),
            libevdev.InputEvent(libevdev.EV_KEY.BTN_TOOL_PEN,
                                value=1),
            libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT,
                                value=0),
        ])
        # Says that the pen it out of range of the tablet. Useful
        # to make sure you can move your mouse, and to avoid
        # strange things during the first draw.
        uinput.send_events([
            libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH,
                                value=0),
            libevdev.InputEvent(libevdev.EV_KEY.BTN_TOOL_PEN,
                                value=0),
            libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT,
                                value=0),
        ])
        print("Waiting 30s to let you:")
        print("1) open Gimp")
        print("2) Go to 'Edit/Input device' and configure the device 'Tablet alone' to 'Screen'.")
        print("3) Save and close the pop up")
        print("4) Create a new file (Ctrl-N)")
        print("5) Zoom and press 'tab' to have a drawing surface coverint most of the screen.")
        print("6) Switch to brush using 'p' key.")
        time.sleep(25)

        pc = 0
        direc = +1
        already_pressed_one = False
        for i in range(250):
            pc_ = pc/100
            val_x = int(pc_*10000 + (1-pc_)*17767)
            val_y = int(pc_*5000 + (1-pc_)*22767)
            val_pres = int(pc_*10 + (1-pc_)*6000)
            print("Will send: x={}, y={}, press={} (pc={})".format(
                val_x,
                val_y,
                val_pres,
                pc))
            uinput.send_events([
                libevdev.InputEvent(libevdev.EV_ABS.ABS_X,
                                    value=val_y),
                libevdev.InputEvent(libevdev.EV_ABS.ABS_Y,
                                    value=val_y),
                libevdev.InputEvent(libevdev.EV_ABS.ABS_PRESSURE,
                                    value=val_pres),
                libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH,
                                    value=1),
                libevdev.InputEvent(libevdev.EV_KEY.BTN_STYLUS,
                                    value=0),
                libevdev.InputEvent(libevdev.EV_KEY.BTN_STYLUS2,
                                    value=0),
                libevdev.InputEvent(libevdev.EV_KEY.BTN_TOOL_PEN,
                                    value=1),
                libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT,
                                    value=0),
            ])
            pc += direc
            if not already_pressed_one:
                print("Press!")
                uinput.send_events([
                    # Pen close to device
                    libevdev.InputEvent(libevdev.EV_KEY.BTN_TOOL_PEN,
                                        value=1),
                    libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH,
                                        value=1),
                    libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT,
                                        value=0),
                ])                
                already_pressed_one = True
            if pc >= 100 or pc <=0 :
                print("Release click.")
                uinput.send_events([
                    libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH,
                                        value=0),
                    # Pen outside of the position
                    libevdev.InputEvent(libevdev.EV_KEY.BTN_TOOL_PEN,
                                        value=0),
                    libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT,
                                        value=0),
                ])
                if pc >= 100:
                    pc = 100
                    direc = -1
                if pc <= 0:
                    pc = 0
                    direc = +1
                time.sleep(5)
                print("Press!")
                uinput.send_events([
                    # Pen close to device
                    libevdev.InputEvent(libevdev.EV_KEY.BTN_TOOL_PEN,
                                        value=1),
                    libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH,
                                        value=1),
                    libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT,
                                        value=0),
                ])
                already_pressed_one = True
            time.sleep(0.1)

    except KeyboardInterrupt:
        pass
    except OSError as e:
        print(e)


if __name__ == "__main__":
    if len(sys.argv) > 2:
        print("Usage: {}")
        sys.exit(1)
    main(sys.argv)

Note that now, xinput test <id you get with xinput list> displays as well the pressure:

$ xinput test 12
motion a[0]=4151295 a[1]=4151295 a[2]=241 
motion a[0]=4060671 a[1]=4060671 a[2]=226 
motion a[0]=3969535 a[1]=3969535 a[2]=211 
motion a[0]=3878399 a[1]=3878399 a[2]=196 
motion a[0]=3787775 a[1]=3787775 a[2]=181 
motion a[0]=3696639 a[1]=3696639 a[2]=166 
motion a[0]=3605503 a[1]=3605503 a[2]=151 
motion a[0]=3514879 a[1]=3514879 a[2]=137 
motion a[0]=3423743 a[1]=3423743 a[2]=122 
motion a[0]=3332607 a[1]=3332607 a[2]=107 
motion a[0]=3241983 a[1]=3241983 a[2]=92 
motion a[0]=3150847 a[1]=3150847 a[2]=77 
motion a[0]=3059711 a[1]=3059711 a[2]=62 
motion a[0]=2969087 a[1]=2969087 a[2]=47 
motion a[0]=2877951 a[1]=2877951 a[2]=32 
motion a[0]=2650623 a[1]=2650623 a[2]=17 
button release 1

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .