Commit 4a9d3e87 authored by Peter Hutterer's avatar Peter Hutterer

tools: add a measure touchpad-size tool

Replacement for the touchpad-edge-detector tool with a slightly more
expressive design, hopefully cutting down on some of the bug reports.
Signed-off-by: Peter Hutterer's avatarPeter Hutterer <peter.hutterer@who-t.net>
parent 101cbe16
......@@ -33,59 +33,83 @@ Measuring and fixing touchpad ranges
To fix the touchpad you need to:
#. measure the physical size of your touchpad in mm
#. run touchpad-edge-detector
#. trim the udev match rule to something sensible
#. replace the resolution with the calculated resolution based on physical settings
#. run the ``libinput measure touchpad-size`` tool
#. verify the hwdb entry provided by this tool
#. test locally
#. send a patch to the systemd project
#. send a patch to the `systemd project <https://github.com/systemd/systemd>`_.
Detailed explanations are below.
`libevdev <http://freedesktop.org/wiki/Software/libevdev/>`_ provides a tool
called **touchpad-edge-detector** that allows measuring the touchpad's input
ranges. Run the tool as root against the device node of your touchpad device
and repeatedly move a finger around the whole outside area of the
touchpad. Then control+c the process and note the output.
An example output is below:
.. note:: ``libinput measure touchpad-size`` was introduced in libinput
1.16. For earlier versions, use `libevdev <http://freedesktop.org/wiki/Software/libevdev/>`_'s
``touchpad-edge-detector`` tool.
The ``libinput measure touchpad-size`` tool is an interactive tool. It must
be called with the physical dimensions of the touchpad in mm. In the example
below, we use 100mm wide and 55mm high. The tool will find the touchpad device
automatically.
::
$> sudo touchpad-edge-detector /dev/input/event4
Touchpad SynPS/2 Synaptics TouchPad on /dev/input/event4
Move one finger around the touchpad to detect the actual edges
Kernel says: x [1024..3112], y [2024..4832]
Touchpad sends: x [2445..4252], y [3464..4071]
$> sudo libinput measure touchpad-size 100x55
Using "Touchpad SynPS/2 Synaptics TouchPad": /dev/input/event4
Kernel specified touchpad size: 99.7x75.9mm
User specified touchpad size: 100.0x55.0mm
Kernel axis range: x [1024..5112], y [2024..4832]
Detected axis range: x [ 0.. 0], y [ 0.. 0]
Move one finger along all edges of the touchpad
until the detected axis range stops changing.
Touchpad size as listed by the kernel: 49x66mm
Calculate resolution as:
x axis: 2088/<width in mm>
y axis: 2808/<height in mm>
...
Suggested udev rule:
# <Laptop model description goes here>
evdev:name:SynPS/2 Synaptics TouchPad:dmi:bvnLENOVO:bvrGJET72WW(2.22):bd02/21/2014:svnLENOVO:pn20ARS25701:pvrThinkPadT440s:rvnLENOVO:rn20ARS25701:rvrSDK0E50512STD:cvnLENOVO:ct10:cvrNotAvailable:*
EVDEV_ABS_00=2445:4252:<x resolution>
EVDEV_ABS_01=3464:4071:<y resolution>
EVDEV_ABS_35=2445:4252:<x resolution>
EVDEV_ABS_36=3464:4071:<y resolution>
Move the finger around until the detected axis range matches the data sent
by the device. ``Ctrl+C`` terminates the tool and prints a
suggested hwdb entry. ::
...
Kernel axis range: x [1024..5112], y [2024..4832]
^C
Detected axis range: x [2072..4880], y [2159..4832]
Resolutions calculated based on user-specified size: x 28, y 49 units/mm
Suggested hwdb entry:
Note: the dmi modalias match is a guess based on your machine's modalias:
dmi:bvnLENOVO:bvrGJET72WW(2.22):bd02/21/2014:svnLENOVO:pn20ARS25701:pvrThinkPadT440s:rvnLENOVO:rn20ARS25701:rvrSDK0E50512STD:cvnLENOVO:ct10:cvrNotAvailable:
Please verify that this is the most sensible match and adjust if necessary.
-8<--------------------------
# Laptop model description (e.g. Lenovo X1 Carbon 5th)
evdev:name:SynPS/2 Synaptics TouchPad:dmi:*svnLENOVO:*pvrThinkPadT440s*
EVDEV_ABS_00=2072:4880:28
EVDEV_ABS_01=2159:4832:49
EVDEV_ABS_35=2072:4880:28
EVDEV_ABS_36=2159:4832:49
-8<--------------------------
Instructions on what to do with this snippet are in /usr/lib/udev/hwdb.d/60-evdev.hwdb
Note the discrepancy between the coordinate range the kernels advertises vs.
what the touchpad sends.
To fix the advertised ranges, the udev rule should be taken and trimmed
before being sent to the `systemd project <https://github.com/systemd/systemd>`_.
If there are discrepancies between the coordinate range the kernels
advertises and what what the touchpad sends, the hwdb entry should be added to the
``60-evdev.hwdb`` file provided by the `systemd project <https://github.com/systemd/systemd>`_.
An example commit can be found
`here <https://github.com/systemd/systemd/commit/26f667eac1c5e89b689aa0a1daef6a80f473e045>`_.
In most cases the match can and should be trimmed to the system vendor (svn)
and the product version (pvr), with everything else replaced by a wildcard
(*). In this case, a Lenovo T440s, a suitable match string would be:
The ``libinput measure touchpad-size`` tool attempts to provide the correct
dmi match but it does require user verification.
In most cases the dmi match can and should be trimmed to the system vendor (``svn``)
and the product version (``pvr``) or product name (``pn``), with everything else
replaced by a wildcard (``*``). In the above case, the match string is:
::
evdev:name:SynPS/2 Synaptics TouchPad:dmi:*svnLENOVO:*pvrThinkPadT440s*
As a general rule: for Lenovo devices use ``pvr`` and for all others use
``pn``.
.. note:: hwdb match strings only allow for alphanumeric ascii characters. Use a
wildcard (* or ?, whichever appropriate) for special characters.
......@@ -95,19 +119,19 @@ The actual axis overrides are in the form:
::
# axis number=min:max:resolution
EVDEV_ABS_00=2445:4252:42
EVDEV_ABS_00=2072:4880:28
or, if the range is correct but the resolution is wrong
::
# axis number=::resolution
EVDEV_ABS_00=::42
EVDEV_ABS_00=::28
Note the leading single space. The axis numbers are in hex and can be found
in *linux/input-event-codes.h*. For touchpads ABS_X, ABS_Y,
ABS_MT_POSITION_X and ABS_MT_POSITION_Y are required.
in ``linux/input-event-codes.h``. For touchpads ``ABS_X``, ``ABS_Y``,
``ABS_MT_POSITION_X`` and ``ABS_MT_POSITION_Y`` are required.
.. note:: The touchpad's ranges and/or resolution should only be fixed when
there is a significant discrepancy. A few units do not make a
......
......@@ -595,6 +595,7 @@ configure_file(input : 'tools/libinput-analyze.man',
src_python_tools = files(
'tools/libinput-analyze-per-slot-delta.py',
'tools/libinput-measure-fuzz.py',
'tools/libinput-measure-touchpad-size.py',
'tools/libinput-measure-touchpad-tap.py',
'tools/libinput-measure-touchpad-pressure.py',
'tools/libinput-measure-touch-size.py',
......@@ -617,6 +618,7 @@ endforeach
src_man = files(
'tools/libinput-measure-fuzz.man',
'tools/libinput-measure-touchpad-size.man',
'tools/libinput-measure-touchpad-tap.man',
'tools/libinput-measure-touchpad-pressure.man',
'tools/libinput-measure-touch-size.man',
......
.TH libinput-measure-touchpad-size "1"
.SH NAME
libinput\-measure\-touchpad\-size \- measure the size of a touchpad
.SH SYNOPSIS
.B libinput measure touchpad\-size [\-\-help] WxH [\fI/dev/input/event0\fI]
.SH DESCRIPTION
.PP
The
.B "libinput measure touchpad\-size"
tool measures the size of a touchpad. This is an interactive tool. When
executed, the tool will prompt the user to interact with the touchpad. The
tool records the axis ranges and calculates the size and resolution based on
those. On termination, the tool prints a hwdb entry that can be added to the
.B 60-evdev.hwdb
file provided by system.
.PP
For details see the online documentation here:
.I https://wayland.freedesktop.org/libinput/doc/latest/absolute-coordinate-ranges.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
This tool must be provided the physical dimensions of the device in mm.
For example, if your touchpad is 100mm wide and 55mm heigh, run this tool as
.B libinput measure touchpad-size 100x55
.PP
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
.SH LIBINPUT
Part of the
.B libinput(1)
suite
#!/usr/bin/env python3
# vim: set expandtab shiftwidth=4:
# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil -*- */
#
# Copyright © 2020 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
try:
import libevdev
import pyudev
except ModuleNotFoundError as e:
print('Error: {}'.format(str(e)), file=sys.stderr)
print('One or more python modules are missing. Please install those '
'modules and re-run this tool.')
sys.exit(1)
class DeviceError(Exception):
pass
class Point:
def __init__(self, x=None, y=None):
self.x = x
self.y = y
class Touchpad(object):
def __init__(self, evdev):
x = evdev.absinfo[libevdev.EV_ABS.ABS_X]
y = evdev.absinfo[libevdev.EV_ABS.ABS_Y]
if not x or not y:
raise DeviceError('Device does not have an x or axis')
if not x.resolution or not y.resolution:
print('Device does not have resolutions.', file=sys.stderr)
x.resolution = 1
y.resolution = 1
self.xrange = (x.maximum - x.minimum)
self.yrange = (y.maximum - y.minimum)
self.width = self.xrange / x.resolution
self.height = self.yrange / y.resolution
self._x = x
self._y = y
# We try to make the touchpad at least look proportional. The
# terminal character space is (guesswork) ca 2.3 times as high as
# wide.
self.columns = 30
self.rows = int(self.columns * (self.yrange // y.resolution) // (self.xrange // x.resolution) / 2.3)
self.pos = Point(0, 0)
self.min = Point()
self.max = Point()
@property
def x(self):
return self._x
@property
def y(self):
return self._y
@x.setter
def x(self, x):
self._x.minimum = min(self.x.minimum, x)
self._x.maximum = max(self.x.maximum, x)
self.min.x = min(x, self.min.x or 0xffffffff)
self.max.x = max(x, self.max.x or -0xffffffff)
# we calculate the position based on the original range.
# this means on devices with a narrower range than advertised, not
# all corners may be reachable in the touchpad drawing.
self.pos.x = min(0.99, (x - self._x.minimum) / self.xrange)
@y.setter
def y(self, y):
self._y.minimum = min(self.y.minimum, y)
self._y.maximum = max(self.y.maximum, y)
self.min.y = min(y, self.min.y or 0xffffffff)
self.max.y = max(y, self.max.y or -0xffffffff)
# we calculate the position based on the original range.
# this means on devices with a narrower range than advertised, not
# all corners may be reachable in the touchpad drawing.
self.pos.y = min(0.99, (y - self._y.minimum) / self.yrange)
def update_from_data(self):
if None in [self.min.x, self.min.y, self.max.x, self.max.y]:
raise DeviceError('Insufficient data to continue')
self._x.minimum = self.min.x
self._x.maximum = self.max.x
self._y.minimum = self.min.y
self._y.maximum = self.max.y
def draw(self):
print('Detected axis range: x [{:4d}..{:4d}], y [{:4d}..{:4d}]'.format(
self.min.x if self.min.x is not None else 0,
self.max.x if self.max.x is not None else 0,
self.min.y if self.min.y is not None else 0,
self.max.y if self.max.y is not None else 0))
print()
print('Move one finger along all edges of the touchpad'.center(self.columns))
print('until the detected axis range stops changing.'.center(self.columns))
top = int(self.pos.y * self.rows)
print('+{}+'.format(''.ljust(self.columns, '-')))
for row in range(0, top):
print('|{}|'.format(''.ljust(self.columns)))
left = int(self.pos.x * self.columns)
right = max(0, self.columns - 1 - left)
print('|{}{}{}|'.format(
''.ljust(left),
'O',
''.ljust(right)))
for row in range(top + 1, self.rows):
print('|{}|'.format(''.ljust(self.columns)))
print('+{}+'.format(''.ljust(self.columns, '-')))
print('Press Ctrl+C to stop'.center(self.columns))
print('\033[{}A'.format(self.rows + 8), flush=True)
self.rows_printed = self.rows + 8
def erase(self):
# Erase all previous lines so we're not left with rubbish
for row in range(self.rows_printed):
print('\033[K')
print('\033[{}A'.format(self.rows_printed))
def dimension(string):
try:
ts = string.split('x')
t = tuple([int(x) for x in ts])
if len(t) == 2:
return t
except: # noqa
pass
msg = "{} is not in format WxH".format(string)
raise argparse.ArgumentTypeError(msg)
def between(v1, v2, deviation):
return v1 - deviation < v2 < v1 + deviation
def dmi_modalias_match(modalias):
modalias = modalias.split(':')
dmi = {'svn': None, 'pvr': None, 'pn': None}
for m in modalias:
for key in dmi:
if m.startswith(key):
dmi[key] = m[len(key):]
# Based on the current 60-evdev.hwdb, Lenovo uses pvr and everyone else
# uses pn to provide a human-identifiable match
if dmi['svn'] == 'LENOVO':
return 'dmi:*svn{}:*pvr{}*'.format(dmi['svn'], dmi['pvr'])
else:
return 'dmi:*svn{}:*pn{}*'.format(dmi['svn'], dmi['pn'])
def main(args):
parser = argparse.ArgumentParser(
description="Measure the touchpad size"
)
parser.add_argument(
'size', metavar='WxH', type=dimension,
help='Touchpad size (width by height) in mm',
)
parser.add_argument(
'path', metavar='/dev/input/event0', nargs='?', type=str,
help='Path to device (optional)'
)
context = pyudev.Context()
args = parser.parse_args()
if not args.path:
for device in context.list_devices(subsystem='input'):
if (device.get('ID_INPUT_TOUCHPAD', 0) and
(device.device_node or '').startswith('/dev/input/event')):
args.path = device.device_node
name = 'unknown'
parent = device
while parent is not None:
n = parent.get('NAME', None)
if n:
name = n
break
parent = parent.parent
print('Using {}: {}'.format(name, device.device_node))
break
else:
print('Unable to find a touchpad device.', file=sys.stderr)
return 1
dev = pyudev.Devices.from_device_file(context, args.path)
overrides = [p for p in dev.properties if p.startswith('EVDEV_ABS')]
if overrides:
print()
print('********************************************************************')
print('WARNING: axis overrides already in place for this device:')
for prop in overrides:
print(' {}={}'.format(prop, dev.properties[prop]))
print('The systemd hwdb already overrides the axis ranges and/or resolution.')
print('This tool is not needed unless you want to verify the axis overrides.')
print('********************************************************************')
print()
try:
fd = open(args.path, 'rb')
evdev = libevdev.Device(fd)
touchpad = Touchpad(evdev)
print('Kernel specified touchpad size: {:.1f}x{:.1f}mm'.format(touchpad.width, touchpad.height))
print('User specified touchpad size: {:.1f}x{:.1f}mm'.format(*args.size))
print()
print('Kernel axis range: x [{:4d}..{:4d}], y [{:4d}..{:4d}]'.format(
touchpad.x.minimum, touchpad.x.maximum,
touchpad.y.minimum, touchpad.y.maximum))
print('Put your finger on the touchpad to start\033[1A')
try:
touchpad.draw()
while True:
for event in evdev.events():
if event.matches(libevdev.EV_ABS.ABS_X):
touchpad.x = event.value
elif event.matches(libevdev.EV_ABS.ABS_Y):
touchpad.y = event.value
elif event.matches(libevdev.EV_SYN.SYN_REPORT):
touchpad.draw()
except KeyboardInterrupt:
touchpad.erase()
touchpad.update_from_data()
print('Detected axis range: x [{:4d}..{:4d}], y [{:4d}..{:4d}]'.format(
touchpad.x.minimum, touchpad.x.maximum,
touchpad.y.minimum, touchpad.y.maximum))
touchpad.x.resolution = round((touchpad.x.maximum - touchpad.x.minimum) / args.size[0])
touchpad.y.resolution = round((touchpad.y.maximum - touchpad.y.minimum) / args.size[1])
print('Resolutions calculated based on user-specified size: x {}, y {} units/mm'.format(
touchpad.x.resolution, touchpad.y.resolution))
# If both x/y are within some acceptable deviation, we skip the axis
# overrides and only override the resolution
xorig = evdev.absinfo[libevdev.EV_ABS.ABS_X]
yorig = evdev.absinfo[libevdev.EV_ABS.ABS_Y]
deviation = 1.5 * touchpad.x.resolution # 1.5 mm rounding on each side
skip = between(xorig.minimum, touchpad.x.minimum, deviation)
skip = skip and between(xorig.maximum, touchpad.x.maximum, deviation)
deviation = 1.5 * touchpad.y.resolution # 1.5 mm rounding on each side
skip = skip and between(yorig.minimum, touchpad.y.minimum, deviation)
skip = skip and between(yorig.maximum, touchpad.y.maximum, deviation)
if skip:
print()
print('Note: Axis ranges within acceptable deviation, skipping min/max override')
print()
print()
print('Suggested hwdb entry:')
use_dmi = evdev.id['bustype'] not in [0x03, 0x05] # USB, Bluetooth
if use_dmi:
modalias = open('/sys/class/dmi/id/modalias').read().strip()
print('Note: the dmi modalias match is a guess based on your machine\'s modalias:')
print(' ', modalias)
print('Please verify that this is the most sensible match and adjust if necessary.')
print('-8<--------------------------')
print('# Laptop model description (e.g. Lenovo X1 Carbon 5th)')
if use_dmi:
print('evdev:name:{}:{}*'.format(evdev.name, dmi_modalias_match(modalias)))
else:
print('evdev:input:b{:04X}v{:04X}p{:04X}*'.format(
evdev.id['bustype'], evdev.id['vendor'], evdev.id['product']))
print(' EVDEV_ABS_00={}:{}:{}'.format(
touchpad.x.minimum if not skip else '',
touchpad.x.maximum if not skip else '',
touchpad.x.resolution))
print(' EVDEV_ABS_01={}:{}:{}'.format(
touchpad.y.minimum if not skip else '',
touchpad.y.maximum if not skip else '',
touchpad.y.resolution))
if evdev.absinfo[libevdev.EV_ABS.ABS_MT_POSITION_X]:
print(' EVDEV_ABS_35={}:{}:{}'.format(
touchpad.x.minimum if not skip else '',
touchpad.x.maximum if not skip else '',
touchpad.x.resolution))
print(' EVDEV_ABS_36={}:{}:{}'.format(
touchpad.y.minimum if not skip else '',
touchpad.y.maximum if not skip else '',
touchpad.y.resolution))
print('-8<--------------------------')
print('Instructions on what to do with this snippet are in /usr/lib/udev/hwdb.d/60-evdev.hwdb')
except DeviceError as e:
print('Error: {}'.format(e), file=sys.stderr)
return 1
except PermissionError:
print('Unable to open device. Please run me as root', file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))
......@@ -28,6 +28,9 @@ Measure touch fuzz to avoid pointer jitter
.B libinput\-measure\-touch\-size(1)
Measure touch size and orientation
.TP 8
.B libinput\-measure\-touchpad\-size(1)
Measure the size of a touchpad
.TP 8
.B libinput\-measure\-touchpad\-tap(1)
Measure tap-to-click time
.TP 8
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment