tools: add a tool to measure touch pressure

And update the documentation for how to use the new tool. It's much more
interactive than evemu and easier to grasp, so let's advertise that.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
This commit is contained in:
Peter Hutterer 2017-06-27 14:16:57 +10:00
parent 25d54b90db
commit cf0d442ad3
6 changed files with 367 additions and 48 deletions

View file

@ -47,62 +47,44 @@ locally. Note that the hwdb entry is **not public API** and **may change at
any time**. Users are advised to @ref reporting_bugs "report a bug" with the
updated pressure ranges when testing has completed.
First, install the "evemu" package providing the ```evemu-record``` tool.
Run ```evemu-record``` as root (without arguments) to see a list of devices
and select the touchpad device. Pipe the actual output of the tool into a
file for later analysis. For example:
Use the ```libinput measure touchpad-pressure``` tool provided by libinput.
This tool will search for your touchpad device and print some pressure
statistics, including whether a touch is/was considered logically down.
Example output of the tool is below:
<pre>
$ sudo evemu-record > touchpad-pressure.txt
Available devices:
/dev/input/event0: Lid Switch
/dev/input/event1: Sleep Button
/dev/input/event2: Power Button
/dev/input/event3: AT Translated Set 2 keyboard
/dev/input/event4: SynPS/2 Synaptics TouchPad
/dev/input/event5: ELAN Touchscreen
[...]
Select the device event number [0-19]: 4
# Ctrl+C to quit, the output will be in touchpad-pressure.txt
$ sudo libinput measure touchpad-pressure
Ready for recording data.
Pressure range used: 8:10
Palm pressure range used: 65535
Place a single finger on the touchpad to measure pressure values.
Ctrl+C to exit
&nbsp;
Sequence 1190 pressure: min: 39 max: 48 avg: 43 median: 44 tags: down
Sequence 1191 pressure: min: 49 max: 65 avg: 62 median: 64 tags: down
Sequence 1192 pressure: min: 40 max: 78 avg: 64 median: 66 tags: down
Sequence 1193 pressure: min: 36 max: 83 avg: 70 median: 73 tags: down
Sequence 1194 pressure: min: 43 max: 76 avg: 72 median: 74 tags: down
Touchpad pressure: 47 min: 47 max: 86 tags: down
</pre>
Now move a finger at **normal pressure** several times around the touchpad,
as if moving the cursor normally around the screen. Avoid any accidental
palm touches or any excessive or light pressure.
The example output shows five completed touch sequences and one ongoing one.
For each, the respective minimum and maximum pressure values are printed as
well as some statistics. The ```tags``` show that sequence was considered
logically down at some point. This is an interactive tool and its output may
change frequently. Refer to the <i>libinput-measure-touchpad-pressure(1)</i> man
page for more details.
The event recording is then filtered for pressure information, which is
sorted and exported to a new file:
By default, this tool uses the udev hwdb entries for the pressure range. To
narrow down on the best values for your device, specify the 'logically down'
and 'logically up' pressure thresholds with the ```--touch-thresholds``
argument:
<pre>
$ grep --only-matching "ABS_MT_PRESSURE[ ]*[0-9]*" touchpad-pressure.txt | \
sed -e "s/ABS_MT_PRESSURE[ ]*//" | \
sort -n | uniq -c > touchpad-pressure-statistics.txt
$ sudo libinput measure touchpad-pressure --touch-thresholds=10:8
</pre>
The file contains a list of (count, pressure-value) tuples which can be
visualized with gnuplot. Copy the following into a file named
```touchpad-pressure-statistics.gnuplot```:
<pre>
set style data lines
plot 'touchpad-pressure-statistics.txt' using 2:1
pause -1
</pre>
Now, you can visualize the touchpad pressure curve with the following
command:
<pre>
$ gnuplot touchpad-pressure-statistics.gnuplot
</pre>
The visualization will show a curve with the various pressure ranges, see
[this bugzilla attachment](https://bugs.freedesktop.org/attachment.cgi?id=130659).
In most cases, the thresholds can be guessed based on this curve. libinput
employes a [Schmitt trigger](https://en.wikipedia.org/wiki/Schmitt_trigger)
with an upper threshold and a lower threshold. A touch is detected when the
pressure goes above the high threshold, a release is detected when the
pressure fallse below the low threshold. Thus, an ideal threshold
combination is with a high threshold slightly above the minimum threshold, a
low threshold on the minimum threshold.
Interact with the touchpad and check if the output of this tool matches your
expectations.
Once the thresholds are decided on (e.g. 10 and 8), they can be enabled with
the following hwdb file:

View file

@ -421,6 +421,15 @@ configure_file(input : 'tools/libinput-measure-touchpad-tap.man',
install_dir : join_paths(get_option('mandir'), 'man1')
)
install_data('tools/libinput-measure-touchpad-pressure',
install_dir : libinput_tool_path)
configure_file(input : 'tools/libinput-measure-touchpad-pressure.man',
output : 'libinput-measure-touchpad-pressure.1',
configuration : man_config,
install : true,
install_dir : join_paths(get_option('mandir'), 'man1')
)
if get_option('debug-gui')
dep_gtk = dependency('gtk+-3.0')
dep_cairo = dependency('cairo')

View file

@ -0,0 +1,259 @@
#!/usr/bin/env python3
#
# Copyright © 2017 Red Hat, Inc.
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice (including the next
# paragraph) shall be included in all copies or substantial portions of the
# Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
#
import sys
import argparse
import evdev
import evdev.ecodes
import pyudev
class Range(object):
"""Class to keep a min/max of a value around"""
def __init__(self):
self.min = float('inf')
self.max = float('-inf')
def update(self, value):
self.min = min(self.min, value)
self.max = max(self.max, value)
class Touch(object):
"""A single data point of a sequence (i.e. one event frame)"""
def __init__(self, pressure=None):
self.pressure = pressure
class TouchSequence(object):
"""A touch sequence from beginning to end"""
def __init__(self, device, tracking_id):
self.device = device
self.tracking_id = tracking_id
self.points = []
self.is_active = True
self.is_down = False
self.was_down = False
self.is_palm = False
self.was_palm = False
self.prange = Range()
def append(self, touch):
"""Add a Touch to the sequence"""
self.points.append(touch)
self.prange.update(touch.pressure)
if touch.pressure < self.device.up:
self.is_down = False
elif touch.pressure > self.device.down:
self.is_down = True
self.was_down = True
self.is_palm = touch.pressure > self.device.palm
if self.is_palm:
self.was_palm = True
def finalize(self):
"""Mark the TouchSequence as complete (finger is up)"""
self.is_active = False
def avg(self):
"""Average pressure value of this sequence"""
return int(sum([p.pressure for p in self.points])/len(self.points))
def median(self):
"""Median pressure value of this sequence"""
ps = sorted([p.pressure for p in self.points])
idx = int(len(self.points)/2)
return ps[idx]
def __str__(self):
return self._str_state() if self.is_active else self._str_summary()
def _str_summary(self):
s = "Sequence {} pressure: min: {:3d} max: {:3d} avg: {:3d} median: {:3d} tags:".format(
self.tracking_id,
self.prange.min,
self.prange.max,
self.avg(),
self.median())
if self.was_down:
s += " down"
if self.was_palm:
s += " palm"
return s
def _str_state(self):
s = "Touchpad pressure: {:3d} min: {:3d} max: {:3d} tags: {} {}".format(
self.points[-1].pressure,
self.prange.min,
self.prange.max,
"down" if self.is_down else " ",
"palm" if self.is_palm else " "
)
return s
class InvalidDeviceError(Exception):
pass
class Device(object):
def __init__(self, path):
if path is None:
self.path = self.find_touchpad_device()
else:
self.path = path
self.device = evdev.InputDevice(self.path)
# capabilities rturns a dict with the EV_* codes as key,
# each of which is a list of tuples of (code, AbsInfo)
#
# Get the abs list first (or empty list if missing),
# then extract the pressure absinfo from that
caps = self.device.capabilities(absinfo=True).get(evdev.ecodes.EV_ABS, [])
p = [cap[1] for cap in caps if cap[0] == evdev.ecodes.ABS_MT_PRESSURE]
if not p:
raise InvalidDeviceError("device does not have ABS_MT_PRESSURE")
p = p[0]
prange = p.max - p.min
# libinput defaults
self.up = int(p.min + 0.12 * prange)
self.down = int(p.min + 0.10 * prange)
self.palm = 130 # the libinput default
self._init_thresholds_from_udev()
self.sequences = []
def find_touchpad_device(self):
context = pyudev.Context()
for device in context.list_devices(subsystem='input'):
if not device.get('ID_INPUT_TOUCHPAD', 0):
continue
if not device.device_node or not device.device_node.startswith('/dev/input/event'):
continue
return device.device_node
print("Unable to find a touchpad device.", file=sys.stderr)
sys.exit(1)
def _init_thresholds_from_udev(self):
context = pyudev.Context()
ud = pyudev.Devices.from_device_file(context, self.path)
v = ud.get('LIBINPUT_ATTR_PRESSURE_RANGE')
if v:
self.up, self.down = colon_tuple(v)
v = ud.get('LIBINPUT_ATTR_PALM_PRESSURE_THRESHOLD')
if v:
self.palm = int(v)
def start_new_sequence(self, tracking_id):
self.sequences.append(TouchSequence(self, tracking_id))
def current_sequence(self):
return self.sequences[-1]
def handle_key(device, event):
tapcodes = [evdev.ecodes.BTN_TOOL_DOUBLETAP,
evdev.ecodes.BTN_TOOL_TRIPLETAP,
evdev.ecodes.BTN_TOOL_QUADTAP,
evdev.ecodes.BTN_TOOL_QUINTTAP]
if event.code in tapcodes and event.value > 0:
print("\rThis tool cannot handle multiple fingers, output will be invalid", file=sys.stderr)
def handle_abs(device, event):
if event.code == evdev.ecodes.ABS_MT_TRACKING_ID:
if event.value > -1:
device.start_new_sequence(event.value)
else:
s = device.current_sequence()
s.finalize()
print("\r{}".format(s))
elif event.code == evdev.ecodes.ABS_MT_PRESSURE:
s = device.current_sequence()
s.append(Touch(pressure=event.value))
print("\r{}".format(s), end="")
def handle_event(device, event):
if event.type == evdev.ecodes.EV_ABS:
handle_abs(device, event)
elif event.type == evdev.ecodes.EV_KEY:
handle_key(device, event)
def loop(device):
print("Ready for recording data.")
print("Pressure range used: {}:{}".format(device.down, device.up))
print("Palm pressure range used: {}".format(device.palm))
print("Place a single finger on the touchpad to measure pressure values.\n"
"Ctrl+C to exit\n")
for event in device.device.read_loop():
handle_event(device, event)
def colon_tuple(string):
try:
ts = string.split(':')
t = tuple([int(x) for x in ts])
if len(t) == 2 and t[0] >= t[1]:
return t
except:
pass
msg = "{} is not in format N:M (N >= M)".format(string)
raise argparse.ArgumentTypeError(msg)
def main(args):
parser = argparse.ArgumentParser(description="Measure touchpad pressure values")
parser.add_argument('path', metavar='/dev/input/event0',
nargs='?', type=str, help='Path to device (optional)' )
parser.add_argument('--touch-thresholds', metavar='down:up',
type=colon_tuple, help='Thresholds when a touch is logically down or up')
parser.add_argument('--palm-threshold', metavar='t',
type=int, help='Threshold when a touch is a palm')
args = parser.parse_args()
try:
device = Device(args.path)
if args.touch_thresholds is not None:
device.down, device.up = args.touch_thresholds
if args.palm_threshold is not None:
device.palm = args.palm_threshold
loop(device)
except KeyboardInterrupt:
pass
except (PermissionError, OSError):
print("Error: failed to open device")
except InvalidDeviceError as e:
print("Error: {}".format(e))
if __name__ == "__main__":
main(sys.argv)

View file

@ -0,0 +1,63 @@
.TH libinput-measure-touchpad-pressure "1"
.SH NAME
libinput\-measure\-touchpad\-pressure \- measure pressure properties of devices
.SH SYNOPSIS
.B libinput measure touchpad\-pressure [\-\-help] [options]
[\fI/dev/input/event0\fI]
.SH DESCRIPTION
.PP
The
.B "libinput measure touchpad\-pressure"
tool measures the pressure of touches on a touchpad. This is
an interactive tool. When executed, the tool will prompt the user to
interact with the touchpad. On termination, the tool prints a summary of the
pressure values seen. This data should be attached to any
pressure\-related bug report.
.PP
For a full description on how libinput's pressure-to-click behavior works, see
the online documentation here:
.I https://wayland.freedesktop.org/libinput/doc/latest/touchpad_pressure.html
and
.I https://wayland.freedesktop.org/libinput/doc/latest/palm_detection.html
.PP
This is a debugging tool only, its output may change at any time. Do not
rely on the output.
.PP
This tool usually needs to be run as root to have access to the
/dev/input/eventX nodes.
.SH OPTIONS
If a device node is given, this tool opens that device node. Otherwise, this
tool searches for the first node that looks like a touchpad and uses that
node.
.TP 8
.B \-\-help
Print help
.TP 8
.B \-\-touch\-thresholds=\fI"down:up"\fR
Set the logical touch pressure thresholds to
.I down
and
.I up,
respectively. When a touch exceeds the pressure in
.I down
it is considered logically down. If a touch is logically down and goes below
the pressure in
.I up,
it is considered logically up. The thresholds have to be in
device-specific pressure values and it is required that
.I down
>=
.I up.
.TP 8
.B \-\-palm\-threshold=\fIN\fR
Assume a palm threshold of
.I N.
The threshold has to be in device-specific pressure values.
.PP
If the touch-thresholds or the palm-threshold are not provided,
this tool uses the thresholds provided by the udev hwdb (if any) or the
built-in defaults.
.SH LIBINPUT
Part of the
.B libinput(1)
suite

View file

@ -24,6 +24,9 @@ Features that can be measured include
.TP 8
.B libinput\-measure\-touchpad\-tap\-time(1)
Measure tap-to-click time
.TP 8
.B libinput\-measure\-touchpad\-pressure(1)
Measure touch pressure
.SH LIBINPUT
Part of the
.B libinput(1)

View file

@ -47,6 +47,9 @@ Measure various properties of devices
.TP 8
.B libinput\-measure\-touchpad\-tap(1)
Measure tap-to-click time
.TP 8
.B libinput\-measure\-touchpad\-pressure(1)
Measure touch pressure.
.SH LIBINPUT
Part of the
.B libinput(1)