- Python 51.2%
- C 45.7%
- Makefile 3.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| tests | ||
| tools | ||
| .gitignore | ||
| LICENSE | ||
| Makefile | ||
| mouse-debounce.c | ||
| README.md | ||
mouse-debounce
The left button on my MX Anywhere 2S started double-clicking. One press, two clicks; a soft press mid-drag would drop the selection halfway through. The switch is worn - it bounces open for a few tens of milliseconds right after it closes, and everything upstream reads that bounce as a real release.
This is an interception-tools filter that takes the bounce back out.
Why not libinput
libinput debounces buttons already, and it's on by default. The trouble is the window: 12 ms for a plain bounce, 25 ms once a button has been flagged as bouncy. There's no knob for it either; the documentation says so plainly. Which is fair - it's a sensible default for a switch that's noisy rather than dying.
Mine bounces for 36-60 ms. That sails straight through. So the filter has to sit below libinput, on the raw evdev stream, and that's what interception-tools is for.
The rule
A release arriving within --short-ms of its press is suspect, so the filter withholds it for --hold-ms. Press the button again inside that window and the release is dropped along with the new press - as far as anything upstream is concerned, the button never came up. Otherwise the release goes out late.
Two knobs, and both come from measurement rather than taste. Here's a real trace off my mouse - one clean click, then a bounce:
1.228s LEFT press 1123.9 ms
1.320s LEFT release 92.1 ms
4.280s LEFT press 1256.0 ms
4.340s LEFT release 60.0 ms <-- bounce
4.384s LEFT press 44.0 ms <-- and back
5.380s LEFT release 996.1 ms
The separation is the useful part. Every bounce released 36-60 ms after its press; every real click held for 92-120 ms. So --short-ms 80 splits them cleanly, and --hold-ms 70 covers the 44-52 ms gaps the switch left open with room to spare.
And because only short presses get withheld, an ordinary click keeps the latency it always had. Nothing is delayed unless it already looks like a bounce.
Build
$ make
$ make check # needs python3; replays recorded traces through the filter
$ sudo make install # PREFIX=/usr/local
Nothing to install first - libc and the Linux headers are the whole dependency list.
Use
The filter reads struct input_event on stdin and writes it on stdout, so it drops straight into an interception-tools pipeline:
$ intercept -g /dev/input/event15 | mouse-debounce | uinput -d /dev/input/event15
Both ends need root: intercept -g grabs the device, and uinput writes to /dev/uinput. Try it the first time on a machine with a touchpad, or otherwise have a way back in - while the device is grabbed, the only pointer you've got is the one coming out of the pipe.
In practice you want udevmon driving it, so the job follows the device across reconnects:
- JOB: "intercept -g $DEVNODE | mouse-debounce --hold-ms 70 --short-ms 80 | uinput -d $DEVNODE"
DEVICE:
NAME: "MX Anywhere 2S Mouse"
EVENTS:
EV_KEY: [BTN_LEFT]
NAME is a full-match regex over the evdev device name. The EVENTS clause matters more than it looks; a Logitech mouse presents several event nodes, and that's what pins the job to the one carrying the buttons.
Options
| Option | Default | What it does |
|---|---|---|
--hold-ms N |
70 | How long a suspect release is withheld |
--short-ms N |
80 | A release this soon after its press is suspect |
--buttons L |
272 |
evdev codes to debounce, comma separated |
272 is BTN_LEFT; 273 is BTN_RIGHT, 274 is BTN_MIDDLE. Anything you don't name passes through untouched.
If your switch bounces at arbitrary points during a hold rather than just after the press, set --short-ms very high (100000, say). Every release then gets withheld, which costs you --hold-ms on every click. It works - just check that you need it first.
Picking your own numbers
Don't copy mine. tools/measure-chatter.py reads the evdev node directly, so it sees the stream before libinput has had a go at it:
$ ./tools/measure-chatter.py "MX Anywhere"
Click normally for a minute - soft clicks especially, and a few drag-selects - then Ctrl-C. You get every transition with the gap since the last one, and a summary of the short ones at the end. Put --hold-ms above the longest bounce gap and below the gap you leave between the two halves of a deliberate double-click; put --short-ms above the longest bounce press-to-release and below that of a real click.
You need read access to /dev/input/event*, which usually means being in the input group.
Guix
Packaged in the panther channel as mouse-debounce, along with px-mouse-debounce-service-type, which writes the udevmon config and runs it under Shepherd:
(service px-mouse-debounce-service-type
(px-mouse-debounce-configuration
(device-name "MX Anywhere 2S Mouse")))
What it can't do
Two genuine clicks, each shorter than --short-ms and less than --hold-ms apart, are indistinguishable from a bounce - they are the same waveform, and the filter will merge them. With the numbers above that's two sub-80 ms clicks less than 70 ms apart. I can't produce that deliberately; someone faster might.
Then the obvious one: this is a workaround on a switch that's mechanically failing. The bounce window widens as it wears, and once it overlaps a real double-click there's no threshold left that separates them. Re-measure every few months. The durable fix is a soldering iron and a new Omron.
License
GPL-3.0-or-later. See LICENSE.