[ SKIP TO MAIN CONTENT ↵ ]
← ALL WRITINGSOCT 31, 20268 min

108 Units of Pressure: Teaching a 12-Year-Old Machine the Art of Never Leaving the Keyboard

Resurrecting tap-to-click, double-click, and drag gestures on an ALPS DualPoint trackstick by probing raw kernel event streams and writing a native userspace daemon.

PUBLISHED:OCT 31, 2026
READ TIME:8 min
TOPICS:
#Linux#Hardware#Systems Programming#C#Kernel#evdev
OCT 31, 2026
PUBLISHED

There is a specific kind of affection reserved for old gadgets.

I have an old 2010s laptop which I am pretty comfortable working with despite weighing as much as an anvil. It comes wrapped in magnesium alloy, still compiles code like a champ, and features a really decent keyboard with a little rubber joystick nestled between the G, H, and B keys —— the nub.

I remember, back in the day, that the machine was able to read clicks through the nub, which was quite enabling and made life a lot easier and faster, since I almost never had to leave typing to deal with window managers or the web. I lost it along the way though, when I upgraded the OS and wasn't able to find an official workaround for this.

Having my muscle memory built on this beast of a machine makes me want to stick with it rather than working on my newer machines. I'd rather get creative with all sorts of remote communications to work on every server or machine just to stick with the comfortable space I have.

So, I thought, why not get the magic back?!

The Situation

Though the nub gives me control over the cursor, and even has comfortable scroll mechanics, it does not allow me to click, double-click, or drag.

The fact that it did click before tells me there must be some kind of pressure detection embedded in the hardware. To find the ghost in the machine, I turned to the raw event debugger of the Linux input subsystem: evtest.

I opened a root shell, pointed evtest at the device node for the AlpsPS/2 ALPS DualPoint Stick, and hovered my finger over the keyboard. I pressed straight down on the rubber cap.

The terminal output confirmed it immediately:

ALPS DUALPOINT EVENT STREAM[/root]
$evtest /dev/input/event4
// TEXT
Event: time 1793181381.717732, type 3 (EV_ABS), code 24 (ABS_PRESSURE), value 108
Event: time 1793181381.730497, type 3 (EV_ABS), code 24 (ABS_PRESSURE), value 92
Event: time 1793181381.744682, type 3 (EV_ABS), code 24 (ABS_PRESSURE), value 68
Event: time 1793181381.758503, type 3 (EV_ABS), code 24 (ABS_PRESSURE), value 44
Event: time 1793181381.771276, type 3 (EV_ABS), code 24 (ABS_PRESSURE), value 0

Look at those timestamps and values. From value 108 down to 0 in exactly 53 milliseconds.

Every single time I tap that little piece of rubber, the hardware speaks to the kernel.

The kernel's alps.c driver was dutifully packaging the pressure as an ABS_PRESSURE event and handing it up the chain to libinput (the modern Linux display server driver). And libinput, seeing that this device was a pointing stick and not a touchpad, politely ignored it and discarded the event.

The signal was there. It just had no brain on the other end.

The Goldilocks Dilemma

I decided to write the driver logic myself in userspace.

The plan was simple: listen to the pointing stick’s raw event stream in /dev/input/, detect downward pressure, and inject synthetic mouse clicks back into the desktop using /dev/uinput.

In Python, setting up this bridge requires only a few lines with evdev:

// PYTHON
import time
import evdev
from evdev import ecodes, UInput

# 1. Locate the physical hardware node in /dev/input/event*
devices = [evdev.InputDevice(path) for path in evdev.list_devices()]
stick = next(d for d in devices if "DualPoint Stick" in d.name)

# 2. Spawn a virtual mouse via /dev/uinput capable of emitting left clicks
ui = UInput({ecodes.EV_KEY: [ecodes.BTN_LEFT]}, name="alps-tap-emitter")

On paper, mapping pressure spikes to button clicks sounds like a three-line if statement. In practice, you run straight into a classic Goldilocks problem: every obvious threshold is either too sensitive or too stiff, and every blunt fix breaks something else.

Direct Pressure Threshold: The Pointer Wobble

The first prototype tested the simplest mental model: treat pressure crossing a fixed threshold as an immediate mouse press.

// PYTHON
# Prototype 1: Direct pressure-to-click mapping
# Premise: Downward pressure spikes -> emit BTN_LEFT down. Pressure drops -> emit BTN_LEFT up.
button_pressed = False

for event in stick.read_loop():
    if event.type == ecodes.EV_ABS and event.code == ecodes.ABS_PRESSURE:
        if event.value >= 70 and not button_pressed:
            ui.write(ecodes.EV_KEY, ecodes.BTN_LEFT, 1)
            ui.syn()
            button_pressed = True
        elif event.value < 10 and button_pressed:
            ui.write(ecodes.EV_KEY, ecodes.BTN_LEFT, 0)
            ui.syn()
            button_pressed = False

It clicked, but human fingers aren't hydraulic presses. Striking a 4mm rubber dome always imparts slight lateral shear. Because the click fired on touchdown while pressure climbed, the cursor twitched a couple of pixels sideways upon contact, slipping right off hyperlinks and buttons.

Exclusive Grabbing: Breaking Hardware Ballistics

If the cursor shifts during a tap, why not freeze it?

In Linux, a process can claim exclusive ownership of an input node using ioctl(fd, EVIOCGRAB, 1)—exposed in evdev as stick.grab(). This diverts all raw events solely to our script, hiding them from libinput and X11 so we can act as a man-in-the-middle filter:

// PYTHON
# Prototype 2: Isolate the hardware via kernel grab to eliminate cursor twitch
# ioctl(fd, EVIOCGRAB, 1) revokes display server access; only this script receives events.
stick.grab()
button_pressed = False

for event in stick.read_loop():
    # 1. Track vertical pressure state
    if event.type == ecodes.EV_ABS and event.code == ecodes.ABS_PRESSURE:
        if event.value >= 70 and not button_pressed:
            ui.write(ecodes.EV_KEY, ecodes.BTN_LEFT, 1)
            ui.syn()
            button_pressed = True
        elif event.value < 10 and button_pressed:
            ui.write(ecodes.EV_KEY, ecodes.BTN_LEFT, 0)
            ui.syn()
            button_pressed = False

    # 2. Proxy motion: swallow relative motion during taps, pass through when steering
    elif event.type == ecodes.EV_REL:
        if not button_pressed:
            ui.write(event.type, event.code, event.value)
            ui.syn()

Freezing motion during a tap cured the jitter, but broke cursor ballistics entirely. libinput treats pointing sticks with specialized dynamic acceleration curves so tiny physical tilts can traverse a 1080p display smoothly. Our generic /dev/uinput proxy only passed flat 1:1 scaling. Crossing the screen now felt like pushing a brick.

The physical hardware had to remain ungrabbed so libinput could manage acceleration natively.

Tuning the Floor: The Static Threshold Trap

With the stick left ungrabbed, the next attempt tried separating taps from navigation force using a higher static threshold:

// PYTHON
# Prototype 3: Keep device ungrabbed; attempt to tune static pressure threshold
TAP_PRESSURE = 45

for event in stick.read_loop():
    if event.type == ecodes.EV_ABS and event.code == ecodes.ABS_PRESSURE:
        if event.value >= TAP_PRESSURE:
            handle_click()

A static threshold is a dead end. Set it low (20–30), and steering across the desktop tilts the stick enough to trigger accidental clicks. Set it high (45–70), and clicking requires deliberate, fatiguing force. A single scalar number cannot tell the difference between heavy steering and a tap.

Solving the problem required looking beyond pressure magnitude and analyzing how human touch actually unfolds across time and motion.

The Physics of a Tap

The flaw in the first three prototypes was a false assumption borrowed from physical mice: Down = Click.

On an analog strain gauge, touchdown is ambiguous—the machine cannot tell whether initial downward force is an intentional tap or the start of a steering tilt. Modern touchpads solve this by ignoring touchdown and registering taps on lift-off.

Looking back at the 53-millisecond spike in the initial evtest dump, a true tap is an impulse collision. By waiting for release, the entire contact window can be evaluated in retrospect:

  • Steering: Continuous lateral motion or sustained duration (> 180ms).
  • Tapping: Sharp downward pressure, negligible lateral tremor, completed in under 180ms.

Clicking on Release Instead of Touch

Firing on lift-off cures the pointer wobble mechanically: by the time BTN_LEFT emits, the finger has already left the rubber.

// PYTHON
# Solution Rule 1: A tap is an impulse collision.
# Click fires on LIFT-OFF, gated by lateral displacement and elapsed contact time.
TAP_PRESSURE = 24       # Feather-light activation floor
RELEASE_PRESSURE = 10   # Finger lift-off baseline
MAX_TAP_DURATION = 0.18 # Must be an impulse (< 180ms), not a sustained hold
MAX_TAP_MOTION = 6      # Maximum allowed lateral tremor counts

state = "IDLE"
press_time = 0.0
tap_motion = 0

for event in stick.read_loop():
    # 1. Touchdown: record timestamp and zero the lateral motion accumulator
    if event.type == ecodes.EV_ABS and event.code == ecodes.ABS_PRESSURE:
        if event.value >= TAP_PRESSURE and state == "IDLE":
            press_time = time.monotonic()
            tap_motion = 0
            state = "TAP_DOWN"

        # 2. Lift-off: ONLY click if contact was brief and the stick stayed stationary
        elif event.value <= RELEASE_PRESSURE and state == "TAP_DOWN":
            duration = time.monotonic() - press_time
            if duration <= MAX_TAP_DURATION and tap_motion <= MAX_TAP_MOTION:
                emit_quick_click()      # Emits BTN_LEFT down + syn + up + syn
                state = "AWAITING_TAP2" # Arm window for double-click / drag
            else:
                state = "IDLE"          # Disqualified: duration > 180ms or motion > 6 (steering)

    # 3. While pressed, track any lateral deflection (steering detection)
    elif event.type == ecodes.EV_REL and state == "TAP_DOWN":
        tap_motion += abs(event.value)

Because lateral tremor is capped at 6 counts, steering across the desktop can never trigger an unintended button press, regardless of downward force.

Immediate Button Lock for Dragging

Tap-and-drag required a second rule. Waiting for a hold timer on the second tap creates noticeable latency before text highlights. The solution: lock BTN_LEFT down the millisecond Tap 2 touches down:

// PYTHON
# Solution Rule 2: Immediate hardware lock on second touchdown (zero drag slip)
# If a second press arrives within the 350ms double-tap window, lock BTN_LEFT immediately.
DOUBLE_TAP_WINDOW = 0.35

if state == "AWAITING_TAP2":
    if event.type == ecodes.EV_ABS and event.code == ecodes.ABS_PRESSURE:
        if event.value >= TAP_PRESSURE:
            # Immediate lock: no waiting for release or hold timers
            ui.write(ecodes.EV_KEY, ecodes.BTN_LEFT, 1)
            ui.syn()
            last_active_time = time.monotonic()
            state = "DRAG_LOCKED"

    # If the 350ms window expires without a second tap, state resets to IDLE
    elif (time.monotonic() - press_time) > DOUBLE_TAP_WINDOW:
        state = "IDLE"

Lift off quickly, and it releases as a clean double-click. Hold and steer, and your selection is already gripped from millisecond zero.

Motion-Coupled Drag Retention

Why did drags drop mid-flight? When tilting a stick sideways to steer, vertical downward pressure often momentarily drops to zero. The fix is letting active movement keep the button locked:

// PYTHON
# Solution Rule 3: Motion-coupled drag retention
# Problem: Tilting sideways to steer shears the stick, causing vertical pressure to drop to 0.
# Fix: Active relative motion confirms the drag is ongoing, bridging pressure dropouts.
DRAG_GRACE_TIME = 0.12 # 120ms silence window before releasing drag lock

if state == "DRAG_LOCKED":
    if event.type == ecodes.EV_REL:
        # Movement keeps the drag alive even if downward pressure dips below threshold
        last_active_time = time.monotonic()

    elif event.type == ecodes.EV_ABS and event.code == ecodes.ABS_PRESSURE:
        if event.value >= RELEASE_PRESSURE:
            last_active_time = time.monotonic()

# In the main loop: release button only after sustained movement AND pressure silence
if state == "DRAG_LOCKED" and (time.monotonic() - last_active_time) >= DRAG_GRACE_TIME:
    ui.write(ecodes.EV_KEY, ecodes.BTN_LEFT, 0)
    ui.syn()
    state = "IDLE"

When motion stops and the finger lifts, a 120ms silence window cleanly releases the selection.

Going Native: The C Rewrite

The Python daemon proved the state machine logic, but running an interpreted runtime continuously for a low-level input loop was unnecessary resource overhead.

I reimplemented the state machine in C using <linux/input.h>, <linux/uinput.h>, and epoll:

// C
// Native C Daemon: zero CPU overhead with epoll event dispatch
// Handles physical device events and timer expirations without busy-polling
struct epoll_event ev, events[4];
int epfd = epoll_create1(EPOLL_CLOEXEC);

ev.events = EPOLLIN;
ev.data.fd = stick_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, stick_fd, &ev);

while (running) {
    // Dynamic timeout: sleep indefinitely when idle (-1), or wake on drag grace expiry (120ms)
    int timeout_ms = get_state_machine_timeout(&sm);
    int nfds = epoll_wait(epfd, events, 4, timeout_ms);

    for (int i = 0; i < nfds; i++) {
        struct input_event ie;
        if (read(stick_fd, &ie, sizeof(ie)) == sizeof(ie)) {
            process_input_event(&sm, uinput_fd, &ie);
        }
    }
    // Fire timer expirations (double-tap timeout window, drag release)
    evaluate_timeouts(&sm, uinput_fd);
}

Compiled with gcc -O2, the resulting binary is 25 KB and runs with a resident memory footprint of 380 KB. Managed by a systemd unit (alps-stick.service), it starts automatically on boot and handles device reconnects cleanly.

The complete implementation—including the C daemon, Python prototype, Makefile, and systemd service configuration—is available on GitHub:

👉 github.com/pccria/alps-trackstick-daemon

Mechanism, Not Policy

The Linux kernel was never broken. The alps.c driver detected the strain gauge and broadcast its pressure as ABS_PRESSURE all along.

The kernel provides the mechanism—it exposes what the hardware can do. Deciding what that pressure means is policy, and policy belongs in userspace. Because modern desktop environments prioritize touchpads, trackstick taps were simply left unmapped.

With evdev and uinput, the plumbing was already there. A small background daemon was all it took to turn that raw pressure into reliable clicks.

Now, a light bounce on the rubber cap clicks instantly, dragging feels natural, and my hands never have to leave the home row.

[ PUBLICATION DETAILS ]MIT LICENSE · OPEN SOURCE SYSTEMS RESEARCH

Published in the pccria.dev systems journal. Formatted with typographic restraint, verifiable source logic, and architectural precision.

← ALL WRITINGSGITHUB REPOSITORY ↗
Vim navigation disabled. Press Alt+V or click to enable.Vim off (tap to enable)
Top