From 4a9d3e8796092c0cab44da5c43e57e91458fcb08 Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Sat, 28 Mar 2020 13:26:17 +1000 Subject: [PATCH] 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 --- doc/user/absolute-coordinate-ranges.rst | 100 ++++--- meson.build | 2 + tools/libinput-measure-touchpad-size.man | 39 +++ tools/libinput-measure-touchpad-size.py | 340 +++++++++++++++++++++++ tools/libinput-measure.man | 3 + 5 files changed, 446 insertions(+), 38 deletions(-) create mode 100644 tools/libinput-measure-touchpad-size.man create mode 100755 tools/libinput-measure-touchpad-size.py diff --git a/doc/user/absolute-coordinate-ranges.rst b/doc/user/absolute-coordinate-ranges.rst index db61376f..f587083e 100644 --- a/doc/user/absolute-coordinate-ranges.rst +++ b/doc/user/absolute-coordinate-ranges.rst @@ -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 `_. Detailed explanations are below. -`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 `_'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 - Touchpad size as listed by the kernel: 49x66mm - Calculate resolution as: - x axis: 2088/ - y axis: 2808/ + Kernel specified touchpad size: 99.7x75.9mm + User specified touchpad size: 100.0x55.0mm - Suggested udev rule: - # - 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: - EVDEV_ABS_01=3464:4071: - EVDEV_ABS_35=2445:4252: - EVDEV_ABS_36=3464:4071: + 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. + + ... + +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 `_. +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 `_. An example commit can be found `here `_. -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 diff --git a/meson.build b/meson.build index 18fb21bc..c13052d5 100644 --- a/meson.build +++ b/meson.build @@ -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', diff --git a/tools/libinput-measure-touchpad-size.man b/tools/libinput-measure-touchpad-size.man new file mode 100644 index 00000000..fdc60c81 --- /dev/null +++ b/tools/libinput-measure-touchpad-size.man @@ -0,0 +1,39 @@ +.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 diff --git a/tools/libinput-measure-touchpad-size.py b/tools/libinput-measure-touchpad-size.py new file mode 100755 index 00000000..27c618c4 --- /dev/null +++ b/tools/libinput-measure-touchpad-size.py @@ -0,0 +1,340 @@ +#!/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)) diff --git a/tools/libinput-measure.man b/tools/libinput-measure.man index d8664f4e..44ec68d3 100644 --- a/tools/libinput-measure.man +++ b/tools/libinput-measure.man @@ -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