mirror of
https://gitlab.freedesktop.org/libinput/libinput.git
synced 2026-02-03 13:30:27 +01:00
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:
parent
25d54b90db
commit
cf0d442ad3
6 changed files with 367 additions and 48 deletions
|
|
@ -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
|
||||
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
259
tools/libinput-measure-touchpad-pressure
Executable file
259
tools/libinput-measure-touchpad-pressure
Executable 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)
|
||||
63
tools/libinput-measure-touchpad-pressure.man
Normal file
63
tools/libinput-measure-touchpad-pressure.man
Normal 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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue