[ SKIP TO MAIN CONTENT ↵ ]
← ALL WRITINGSNOV 7, 20269 min

The Last Millisecond: What It Actually Takes to Make an Analog Trackstick Click in Linux

The prototype worked on paper. Daily driving broke it. The story of scrapping heuristic state machines for a zero-latency transducer, and why an input daemon must never touch pointer ballistics.

PUBLISHED:NOV 7, 2026
READ TIME:9 min
TOPICS:
#Linux#Hardware#Systems Programming#C#Kernel#evdev#KDE
NOV 7, 2026
PUBLISHED

There is a dangerous trap in hardware hacking that every programmer falls into at least once: the illusion of the working prototype.

You probe the raw kernel stream, you find the pressure signals hiding in the device node, you whip up a quick userspace bridge with uinput, and you tap the nub. A click fires. You smile, write a victory post, and tell yourself the problem is solved.

Then Monday morning arrives, and you actually try to do real work with it.

Within twenty minutes, reality asserts itself. You try to double-click a word in your text editor, and it takes three frantic, deliberate strikes to highlight it. You try to drag a window across the desktop, and it drops out of your grip mid-screen. You click a hyperlink and move your hand to type, only to realize the cursor is dragging half the webpage behind it.

What looked elegant on an oscilloscope turned into an exhausting wrestling match against my own fingers.

Making an analog strain gauge behave like a crisp mechanical mouse switch isn't a problem of thresholds. It is a problem of human biomechanics, desktop compositor timing, and knowing when to get your code out of the kernel's way.

The Heuristic Trap: How We Over-Engineered the State Machine

When a gesture engine misclassifies human input, the programmer's immediate instinct is always the same: add more states, add more timers.

In the initial daemon, single taps and drags were managed by a basic three-state machine. When edge cases cropped up—taps getting confused with drags, rebounds triggering unwanted second clicks—the solution seemed obvious: make the state engine smarter.

The result was an intricate, multi-stage trajectory engine:

// TEXT
[ The Over-Engineered Pipeline ]
Tap 1 Lift-off ➔ Synthesize 5ms pulse ➔ Wait 450ms Primed Window
                                              │
              Tap 2 Lift-off ─────────────────┘
                     │
                     ▼
           [ 40ms Silence Gating ]  <-- The Fatal Flaw
           ├── > 4 motion counts?  ➔ Abort double-click, lock accidental DRAG
           └── 40ms elapsed?       ➔ Delayed Click 2 emitted (>350ms total latency)

On paper, this looked bulletproof. To verify that a second tap was truly an intentional double-click and not the start of a drag, the daemon waited for the finger to lift off, and then waited another 40 milliseconds of complete mechanical silence to verify the finger was floating in mid-air.

In real life, it was a disaster:

  1. Sluggish, Disconnected Double-Clicks: By the time Tap 2 touched down, lifted off, and cleared the 40ms silence window, over 350 milliseconds had elapsed since the start of the gesture. The desktop compositor (KDE/X11) had already expired its internal double-click window. The OS saw two disconnected single clicks.
  2. Accidental Drags on Natural Tremor: If the finger vibrated by even 4 counts during that 40ms silence—natural mechanical cap bounce—the daemon aborted the double-click and grabbed the screen.
  3. Premature Drops: To make dragging feel responsive, the release grace window was set to 130 milliseconds. When navigating across a high-resolution display, a human hesitates or changes direction for roughly 100 to 150ms. The daemon saw 130ms of silence, assumed the user had let go, and dropped the window right in the middle of the screen.

The code was fighting the operating system, fighting the desktop environment, and fighting the hand on the keyboard.

The Epiphany: The Virtual Button Transducer

The fundamental mistake was attempting to be an arbiter of high-level desktop semantics in userspace.

Think about a physical microswitch inside a mouse. Does the switch know what a double-click is? Does it know what a drag is?

Of course not. A mouse switch is a dumb physical contact:

  • Finger presses down $\to$ BTN_LEFT 1
  • Finger releases $\to$ BTN_LEFT 0

The operating system already has decades of hardened, polished code to handle double-clicks, triple-clicks, drag-and-drop thresholds, and selection locks. It does not need a userspace daemon trying to guess the future.

The entire architecture needed to be scrapped and rebuilt as a Zero-Latency Physical Virtual Transducer:

// TEXT
[ Zero-Latency Transducer Architecture ]

1. Tap 1 Down/Up (Z >= 28, < 180ms) ➔ Clean Discrete Click (15ms hold)
                                          │
                           [ 260ms Primed Window ]
                           ├── Steering Motion (>= 3 counts)? ➔ Cancel primed state immediately
                           └── Z >= 18 (Rebound Tap 2)?
                                          │
                                          ▼
                         EMIT BTN_LEFT 1 IMMEDIATELY (0ms latency!)
                                          │
                 ┌────────────────────────┴────────────────────────┐
                 ▼                                                 ▼
        [ Quick Lift-Off ]                                [ Steering Motion >= 6 ]
   (Z <= 10, motion < 6 counts)                                    │
                 │                                                 ▼
        EMIT BTN_LEFT 0                                    [ Active Drag Lock ]
                 │                                         Continuous EV_REL keeps lock alive
        NATIVE DOUBLE-CLICK                                (140ms grace window bridges pauses)

At the exact millisecond Tap 2 makes physical contact with the rubber dome ($Z \ge 18$), emit BTN_LEFT 1 immediately. No waiting for release. No silence gating.

  • If the user lifts off within 180ms without lateral motion ($< 6$ counts) $\to$ emit BTN_LEFT 0. The operating system sees two crisp, immediate pulses and registers a native double-click.
  • If the user pushes the stick sideways to drag $\to$ the virtual button is already down. The selection begins from the very first pixel of movement.

Zero latency. Zero synthetic delays. Zero guessing.

The Physics of the Last Mile

Switching to a direct transducer established the right mental model, but tuning an analog strain gauge to feel like a mechanical switch requires confronting the subtle physics of human touch.

1. Rebound Asymmetry: The $Z$-Axis Drop

When you strike a trackstick for the first time, your finger drops from a few centimeters in the air, imparting heavy downward momentum ($Z \approx 35\text{--}70$).

When you double-tap, your finger rebounds from barely 1mm above the cap. The downward velocity is substantially lower, imparting far less vertical force ($Z \approx 18\text{--}25$). If you use a single scalar pressure threshold for both strikes, the user has to "try hard" and slam the stick on the second strike to trigger a double-click.

The solution is an asymmetric two-tier activation floor:

  • Tap 1 Trigger: $Z \ge 28$ (strict, rejecting palm brushing and resting contact).
  • Tap 2 Trigger: $Z \ge 18$ (sensitive, capturing natural low-amplitude rebound strikes).

2. The Lateral Shear Paradox

When steering the cursor across the screen, the user pushes the rubber cap laterally ($X, Y$). Because lateral force tilts the central stem sideways, vertical downward pressure drops to absolute zero (ABS_PRESSURE: 0), even while the user is shoving the stick firmly.

This created two interlocking bugs:

  • The Steering Drop: When dragging an object, the instant the user started steering sideways, vertical pressure vanished. If the daemon relied on pressure to keep the button down, the drag dropped immediately.
    • Fix: Relative motion events (EV_REL) sustain the drag lock regardless of $Z=0$.
  • The Grab Hesitation: In early builds, we required 15 counts of lateral motion before confirming a drag. But because pressure dropped to 0 in the first 10ms of a lateral push, the daemon mistook the pressure dip for a double-click lift-off and released the button before 15 counts could accumulate!
    • Fix: Slashed DRAG_START_MOTION from 15 down to 6 counts ($\approx 10\text{ms}$). As soon as the stick shifts 6 counts in any direction, drag lock engages instantly with zero grab hesitation.

3. The 140ms Sweet Spot: Sticky Drops vs Premature Releases

How long should the button stay held down when the pointer stops moving during a drag?

  • At 250ms, lifting your finger to drop a window felt sluggish—the item "stuck" to the cursor for a quarter-second after you let go.
  • At 130ms, pausing to read a menu or reversing direction dropped the selection mid-flight.
  • At 140ms, we found the Goldilocks zone: long enough to bridge human hesitation and direction changes, but snappy enough that drops feel instantaneous.

4. The First-Packet Cancel

The most frustrating edge case occurs when you click a button and immediately move the pointer somewhere else. If the double-tap window is armed, the initial touch of your finger as you begin navigating can easily spike pressure above 15, accidentally dragging what you just clicked.

The fix is immediate motion cancellation: if the stick receives just 3 counts of relative motion (AWAIT_MOTION_CANCEL) while primed, the primed state is aborted instantly. Normal navigation proceeds without a hitch.

5. The Target Slip Trap and the Anti-Drift Snap

When we initially tried to prevent steering misclicks by raising TAP1_PRESSURE to 28, we fell into a classic hardware trade-off: to hit 28, the user had to strike the rubber dome hard ("tap quite unsmoothly"). And striking a 4mm rubber dome hard imparts heavy lateral shear.

Because the hardware is ungrabbed, those lateral counts stream straight to the display server. The cursor slipped across the screen during the tap, missing small hyperlinks, or exceeding the motion cap and dropping the click entirely.

The solution required breaking the trade-off with two innovations:

  1. The Force-Ratio Discriminator ($Z \ge \text{motion} \times 1.5$): Instead of a blunt scalar threshold, we compare vertical force against lateral displacement. A steering micro-nudge (moving 8 pixels to target a button) has high motion and low pressure ($Z \approx 15$, motion $\approx 10$). A tap is dominated by vertical collision ($Z \ge 25$, motion $\le 6$). By enforcing that pressure must dominate motion, we dropped the activation floor back down to a smooth, feather-light $Z=22$.
  2. Hardware Anti-Drift Snap: Because the button is the pointer, striking the rubber dome always imparts 2 to 6 counts of unavoidable deflection ($dx, dy$). In v0.4.0, the daemon records this contact drift. Upon lift-off, before emitting BTN_LEFT, it emits an equal and opposite relative pulse ($-dx, -dy$) via uinput in the exact same event frame. The cursor snaps back to its pre-tap coordinate at the exact instant the click fires. Zero target slip.

The Unseen Saboteur: Pointer Ballistics

With the transducer calibrated, the gesture engine felt fast and accurate. But testing it revealed a strange, subtle uncanny valley: the speed and acceleration of the nub felt manipulated.

When you have spent years on a specific machine, your neuromuscular system maps physical force directly to cursor displacement. If the pointer curve is altered even slightly, the stick feels wrong—sluggish at low speeds, twitchy at high speeds, like pushing through molasses.

I dug into the codebase to find where the distortion was coming from, and found an old prototype hack staring right back at me:

// C
// The hidden saboteur inherited from early testing:
float speed = 0.8f;

xinput set-prop "AlpsPS/2 ALPS DualPoint Stick" "libinput Accel Speed" 0.80
xinput set-prop "AlpsPS/2 ALPS DualPoint Stick" "libinput Accel Profile Enabled" 1 0

Every time the daemon started, it was executing xinput behind the scenes, overriding the pointer speed to 0.80 and forcing the Adaptive acceleration profile (1 0).

In my actual KDE desktop configuration (~/.config/kcminputrc), my preference had always been:

// INI
[Libinput][2][8][AlpsPS/2 ALPS DualPoint Stick]
PointerAcceleration=0.200

[Mouse]
X11LibInputXAccelProfileFlat=true

The daemon was quad-boosting the speed to 0.80 and replacing my linear Flat profile with an aggressive non-linear curve. It was sabotaging my own muscle memory.

Mechanism, Not Policy (Redux)

This was a stark reminder of the Unix philosophy: Mechanism, not policy.

An input daemon responsible for synthesizing tap gestures has no mandate to alter cursor ballistics. Pointer speed, acceleration curves, and deceleration thresholds belong exclusively to the desktop environment (KDE, GNOME, or Sway) and the user's personal configuration.

In v0.3.2, pointer manipulation was stripped out entirely:

  • By default, speed is unset (-999.0f).
  • The daemon never touches libinput Accel Speed or libinput Accel Profile Enabled.
  • Native desktop settings remain in complete, untampered control.

The moment the override was removed, the stick returned to its true physical nature.

The Final Architecture (v0.4.0)

The final daemon is pure C, compiled with gcc -O2, running on Linux epoll:

ParameterCalibrated ValueEngineering Rationale
TAP1_PRESSURE22Feather-light activation floor; effortless, smooth tapping
TAP2_REBOUND_PRESSURE15Captures light rebound strikes without hair-trigger misclicks
RELEASE_PRESSURE10Mechanical unload baseline
MAX_TAP_DURATION_MS180 msMaximum allowable contact duration for an impulse tap
MAX_TAP_MOTION16 countsMaximum contact shear (rejects continuous steering)
DOUBLE_TAP_WINDOW_MS260 msNatural human cadence without a lingering drag window
DRAG_START_MOTION6 countsLocks drag in $\approx 10\text{ms}$; eliminates grab hesitation
DRAG_RELEASE_GRACE_MS140 msCrisp drop release; eliminates sticky linger
AWAIT_MOTION_CANCEL3 countsFirst motion packet aborts primed state for clean navigation
PRE_MOTION_LIMIT10 countsDecaying velocity memory rejects downward pressure during flight
ANTI_DRIFT_SNAPActiveReverses contact shear ($-dx, -dy$) to freeze pointer on target

Managed by systemd as a 25 KB binary with a 380 KB resident memory footprint, it starts on boot and handles device reconnects cleanly.

The complete codebase, calibration tools, and systemd service files are available on GitHub:

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

The Art of Restraint

In software engineering, complexity is easy to add and painful to remove. When an analog signal behaves unpredictably, the temptation to surround it with heuristics, timeouts, and predictive state machines is almost irresistible.

The breakthrough came from doing the exact opposite: stripping away the guessing, accepting that the operating system already knows how to handle clicks, and turning the daemon into a direct, faithful physical transducer.

Now, a light bounce on the rubber cap clicks instantly. Double-tapping selects words without rushing. Drags grip on the first pixel and drop the millisecond you let go. And the cursor ballistics are completely untouched, moving with the exact linear feel my hands learned a decade ago.

The machine hasn't changed. But the conversation between my fingers and the kernel finally speaks the same language.

[ 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