Skip to content

Instantly share code, notes, and snippets.

@wallabra
Created July 10, 2026 21:49
Show Gist options
  • Select an option

  • Save wallabra/cca416b514946e8bd8b763b0427e8bde to your computer and use it in GitHub Desktop.

Select an option

Save wallabra/cca416b514946e8bd8b763b0427e8bde to your computer and use it in GitHub Desktop.
Drawing Tablet Phone Jail, or How I Learned to Stop Worrying and Break Out of It Myself

Drawing Tablet Phone Jail, or How I Learned to Stop Worrying and Break Out of It Myself

by wallabra

Abstract

This is a retelling of a drawing tablet Linux driver hacking journey, about how I got a budget tablet, had trouble with the generic driver, mistook where the issue was, found a better userspace driver, then realized it was miscalibrated. I close it off with potential roadmap points.

Setup

I use CachyOS, with is an Arch Linux derived distribution, and the Niri Wayland provider. I was suspicious of Niri and of Wayland in general, but read on as I exonerate them.

The journey

So I got this cheap drawing tablet from Shopee, Tomate MTM-1106. Not surprisingly, it's a white label rebrand of a generic one that's probably manufactured en masse somewhere in China. But it should work fine - the specs promise something actually okay. But that's besides the point.

I plug it in, and voilà, it works right off the bat! ... Sort of.

Spatial Symptoms

As I was yet to learn, in the absence of a specialized driver for this device, Linux loads the generic usbhid module to somewhat interpret the values this tablet reports, and then your userland stack (in my case, libinput through Smithay through Niri) interprets these events and remaps them and processes them as input events and redistributes them to your windows. usbhid is fine, but using it directly in Niri is not, and we're about to talk about why.

First, we need to talk about rectangles.

The tablet has two area markings. One of them spans effectively the entire surface of the sensitive area. The other is a subset, probably used for Phone Mode.

It turns out that the tablet comes with Windows and MacOS drivers. It even reports its own read-only driver storage as a "USB CD-ROM device", which is a little funny. You're supposed to install the drivers, but because usbhid was using the generic interface, the tablet was detecting this, thinking it's plugged to a phone (which are Android, so also Linux), and putting itself in Phone Mode. This means you can only use the small area when using the tablet in your system. Which really messed it up!

It meant that you had to draw in a really tight area for it to actually be in your screem.

Makeshifty

At first I thought this issue was caused by the system not loading the coordinate mapping the right way because it used some generic device. I eventually realized that Niri had its own tablet input mapping option. So I tried to manually calibrate the matrix there.

// ~/.config/niri/config.kdl
input {
  tablet {
    map-to-focused-output
    calibration-matrix 0.84 0.0 0.16 0.0 1.0 0.0 // <-- the matrix
  }
}

Note

Your destination space isn't anchored at the top, but rather at the bottom. If it's clipping under your screen, push the Y offset up (the third value in the line), not the Y scale (the first value).

This is because, in libinput, the origin of the destination coordinate space is the bottom left. This means that while a value is mapped as origin + scale * value, it's interpreted as going up, effectively being inverted when translated to the actual logical coordinate space of the screen (where Y usually means going down). I don't know why this is the case.

It might also have to do with my second screen display being rotated 270 degrees in Niri, though the behavior is the same in my main screen so that is unlikely.

This stretches the destination space so that the inverse transformation of the source space (the upper half) gets squished back into the original designed aspect ratio, roughly.

But here's the thing. The input is sliced from the full tablet into just one half. Now I was basically making the source area even smaller within that slice, just to restore the original aspect ratio. It made drawing incredibly annoying! I knew I couldn't settle for this.

Hope

I looked in the Niri source code, Niri was based on Smithay; so I looked at the Smithay source code, and Smithay's input was handled in libinput; and I looked in libinput, and I didn't find any errors. Duh! They deal with input devices in a general way. How would they catch or have a quirk, even with a generic usbhid?

It took me embarrassingly long to realize the issue was the tablet was in phone mode and that I simply had to find a way to work around that.

So after some digging, I found an userland driver made for exactly this kind of chipset: mx002. This version was itself a fork with some adjustments to pressure calibration. Huh.

So I compiled it, copied it to /usr/local/bin (it would need root, anyway). Then I disabled the system's own udev (which Niri was grabbing through libinput) through the following udev rules file.

SUBSYSTEM=="input", ATTRS{name}=="SZ PENG YI LTD.*", ENV{ID_INPUT}="", ENV{ID_INPUT_TABLET}="", ENV{ID_INPUT_TOUCHPAD}="", ENV{ID_INPUT_MOUSE}="", ENV{ID_INPUT_KEYBOARD}="", ENV{LIBINPUT_IGNORE_DEVICE}="1"

Basically it detects my tablet's USB device and tells libinput to ignore it and also forces the system to not register all those tablet, mouse, keyboard, CDROM etc devices. This way there would be no conflict.

To apply the rules and check that the rules applied, I did this:

$ sudo udevadm control --reload-rules && sudo udevadm trigger

$ udevadm info --query=property --name=/dev/input/by-id/usb-SZ_PENG_YI_LTD.__T501__Driver_Inside_Tablet_Internal_CDROM__001-event-if01
 | grep -E "LIBINPUT_IGNORE_DEVICE"
LIBINPUT_IGNORE_DEVICE=1    # Jackpot!

Notice that this device's vendor and device ID are 08f2:6811. To check this:

$ lsusb -d 08f2:6811
Bus 003 Device 037: ID 08f2:6811 Gotop Information Inc. [T501] Driver Inside Tablet

After this, I was able to run sudo mx002, and it worked right out of the box! It completely fixed the phone mode problem, because it turned out it was entirely bypassing whatever the firmware wanted to report as the coordinates, and just took the raw USB-HID data through the rusb library. At least, I think that's how it works, but I'm conjecturing.

So, sunshine and rainbows, right?

Well, almost.

Pressure

While the coordinate troubles were gone, this new driver made it really hard to draw. I had to press down the driver really hard, and there was little range; the lines were already thick.

I noticed the repository was in Rust, so I did what I do best: I dove into the source code.

In virtual_device.rs, there is a linear mapping meant to take the raw sensor distance values reported by the tablet and convert them into pressure values for touch inputs. The code is as follows:

fn normalize_pressure(raw_pressure: i32) -> i32 {
    let proximity_threshold = 600; // Adjust for proximity sensitivity
    let strength_scaling = 2; // Adjust for strength of the press

    match 1740 - raw_pressure {
        x if x <= proximity_threshold => 0,
        x => x * strength_scaling, // Scale the pressure for stronger presses
    }
}

Aha! That's the proximity_threshold. It's very high and it was making it hard to draw without poking a hole through my tablet.

Also, are you noticing a problem here? When x is bigger than the threshold, it doesn't start at 0. It starts already at the threshold (times scaling)!

Also, these values are pretty arbitrary. Whoever wote this function was having trouble with ghost lines as hovering the pen laid strokes prematurely, and overreacted here.

The scarier hypothesis is that different models have different behavior with the same chipset, so some models might give different values at different proximities and pressures. Hold that thought.

Normalized pressure

In response, I rewrote this function to be a more standard linear mapper, and moved the magic numbers out to constants.

I added a println! to see the raw values the tablet was reporting. Knowing which values it was at at the highest possible hover point, at the lightest touch point, and at the hardest pressure point, I could use those to very loosely calibrate the actual linear mapping with deadzone.

The code turned out to be very simple with those constants made known!

fn normalize_pressure(raw_pressure: i32) -> i32 {
    // println!("{}", raw_pressure)   // -- uncomment this if you want to do your own calibration :)
    let value = SENSOR_MAX - raw_pressure;

    let normalized = value - TOUCH_FLOOR;
    let normalized = normalized.clamp(0, TOUCH_EXTENT);

    normalized
}

Basically I subtract it so that the lightest possible touch is a 0.

Also notice that I moved scaling out of the function, simply setting the value when defining the device's axes, in DeviceDispatcher::virtual_pen_bulder:

let abs_pressure_setup = UinputAbsSetup::new(
    AbsoluteAxisType::ABS_PRESSURE,
    AbsInfo::new(0, 0, TOUCH_EXTENT, 0, 0, 1), // earlier was 4096
);

Here are the values for my particular model:

const SENSOR_MAX: i32 = 1623;
const TOUCH_FLOOR: i32 = 1623 - 1574;
const TOUCH_CEILING: i32 = 1623 - 727;
const TOUCH_EXTENT: i32 = TOUCH_CEILING - TOUCH_FLOOR;

This made the linear values returned by the userland driver a lot saner and more satisfying. After some remapping in the Krita global pressure response curve, I could draw relatively effortlessly! (As effortlessly as a budget tablet will allow, anyway.)

Roadmap

Okay, so clearly this driver isn't great still. For one, the values are hardcoded. I don't know if other makes report different values or have different physical configurations that could affect their readings (the thought about different proximities and pressures!). If I had more time (or went unemployed again), I'd add more polish and reusability before releasing my version.

  • Presets instead of hardcoded values
    • Restore a standard value for the pressure axis resolution (like 4096 or 8192) and remap the pressure values again, but without the threshold/match discontinuity
  • Configuration file (to put the presets in)
  • A wizard tool that automatically helps you find the floor and ceiling
  • (optional) Autodetect your specific model to pick the right preset automatically
  • Better logging and maybe some unit tests

Those are all things that I can't do now and there's probably even more ways this project could be polished. But I'm leaving this document behind in place of that, if anyone wants to take up on that, or if anyone encounters any of these issues and wants to know how I tackled them, or in case I ever revisit this project.

Conclusion

I had fun doing this project, even if it started as annoying troubleshooting, it was satisfying to see the constants lined up with the raw_pressure values println! reported and the linear mapping restored to its full glory. Due credit goes to the authors of the original repositories, whose work I'm merely iterating, and even then barely, because I barely understand the code they actually used to interact with the devices at such a low level. It's the work of hundreds or thousands of great developers like these that puts together the incredible web of interlocking parts that is even a simple graphical Linux system.

I did just keep the hardcoded values that suited me. I could've left it at that forever. I could have wiped the source tree from /tmp forever and never told anyone about it. But it felt selfish to do this, to encounter problems so cryptic and figure them out and not share my specific solutions. I don't want to be a driver maintainer, but at least I wanted to make the life of whoever comes after me a little easier.

If anyone wants to take up on this work and do one better, I'm more than happy. And if anyone has further questions, feel free to

Thanks for reading,

- wallabra

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment