diff options
author | Peter Hutterer <peter.hutterer@who-t.net> | 2018-04-26 11:47:54 +1000 |
---|---|---|
committer | Peter Hutterer <peter.hutterer@who-t.net> | 2018-05-15 13:51:33 +1000 |
commit | 3251ba2af0c2510418c5ea75d880de4223a88f63 (patch) | |
tree | 0195d9f7a50465825750a3e5efd586a664573b71 /tools | |
parent | d7ff5a8f0d876b3125978fd357716ac0f5619de0 (diff) |
touchpad: add a tool to measure the touchpad fuzz
Well, I say "measure" but really at this point it just reads the
properties/axes and then does it's best to auto-generate a hwdb entry that
matches the user's hardware and sets a fuzz value on the device. Ideally this
reduces the number of hand-holding required in bugzillas. There are plenty of
things that can go wrong, so our fallback is still to throw up our hands and
point to the documentation.
https://bugs.freedesktop.org/show_bug.cgi?id=98839
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
Diffstat (limited to 'tools')
-rwxr-xr-x | tools/libinput-measure-fuzz | 464 | ||||
-rw-r--r-- | tools/libinput-measure-fuzz.man | 30 | ||||
-rw-r--r-- | tools/libinput-measure.man | 3 |
3 files changed, 497 insertions, 0 deletions
diff --git a/tools/libinput-measure-fuzz b/tools/libinput-measure-fuzz new file mode 100755 index 00000000..23386fe2 --- /dev/null +++ b/tools/libinput-measure-fuzz @@ -0,0 +1,464 @@ +#!/usr/bin/env python3 +# vim: set expandtab shiftwidth=4: +# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil -*- */ +# +# Copyright © 2018 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 os +import sys +import argparse +import subprocess +try: + import evdev + import evdev.ecodes + 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) + + +DEFAULT_HWDB_FILE = '/usr/lib/udev/hwdb.d/60-evdev.hwdb' +OVERRIDE_HWDB_FILE = '/etc/udev/hwdb.d/99-touchpad-fuzz-override.hwdb' + + +class tcolors: + GREEN = '\033[92m' + RED = '\033[91m' + BOLD = '\033[1m' + NORMAL = '\033[0m' + + +def print_bold(msg, **kwargs): + print(tcolors.BOLD + msg + tcolors.NORMAL, **kwargs) + + +def print_green(msg, **kwargs): + print(tcolors.BOLD + tcolors.GREEN + msg + tcolors.NORMAL, **kwargs) + + +def print_red(msg, **kwargs): + print(tcolors.BOLD + tcolors.RED + msg + tcolors.NORMAL, **kwargs) + + +class InvalidConfigurationError(Exception): + pass + + +class InvalidDeviceError(Exception): + pass + + +class Device(object): + def __init__(self, path): + if path is None: + self.path = self.find_touch_device() + else: + self.path = path + + self.device = evdev.InputDevice(self.path) + self.name = self.device.name + context = pyudev.Context() + self.udev_device = pyudev.Devices.from_device_file(context, self.path) + + def find_touch_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 touch device.', file=sys.stderr) + sys.exit(1) + + def check_property(self): + '''Return a tuple of (xfuzz, yfuzz) with the fuzz as set in the libinput + property. Returns None if the property doesn't exist''' + + axes = { + 0x00: self.udev_device.get('LIBINPUT_FUZZ_00'), + 0x01: self.udev_device.get('LIBINPUT_FUZZ_01'), + 0x35: self.udev_device.get('LIBINPUT_FUZZ_35'), + 0x36: self.udev_device.get('LIBINPUT_FUZZ_36'), + } + + if axes[0x35] is not None: + if axes[0x35] != axes[0x00]: + raise InvalidConfigurationError('fuzz for ABS_X differs from ABS_MT_POSITION_X') + + if axes[0x36] is not None: + if axes[0x36] != axes[0x01]: + raise InvalidConfigurationError('fuzz for ABS_Y differs from ABS_MT_POSITION_Y') + + xfuzz = axes[0x35] or axes[0x00] + yfuzz = axes[0x36] or axes[0x01] + + if xfuzz is None and yfuzz is None: + return None + + if ((xfuzz is not None and yfuzz is None) or + (xfuzz is None and yfuzz is not None)): + raise InvalidConfigurationError('fuzz should be set for both axes') + + return (xfuzz, yfuzz) + + def check_axis(self): + ''' + Returns a duple of (xfuzz, yfuzz) with the fuzz as set on the device + axis. Returns None if no fuzz is set. + ''' + # capabilities returns 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 touch major absinfo from that + caps = self.device.capabilities(absinfo=True).get( + evdev.ecodes.EV_ABS, [] + ) + codes = [cap[0] for cap in caps] + + if evdev.ecodes.ABS_X not in codes or evdev.ecodes.ABS_Y not in codes: + raise InvalidDeviceError('device does not have x/y axes') + + if (evdev.ecodes.ABS_MT_POSITION_X in codes) != (evdev.ecodes.ABS_MT_POSITION_Y in codes): + raise InvalidDeviceError('device does not have both multitouch axes') + + axes = { + 0x00: None, + 0x01: None, + 0x35: None, + 0x36: None, + } + + for c in caps: + if c[0] in axes.keys(): + axes[c[0]] = c[1].fuzz + + xfuzz = axes[0x35] or axes[0x00] + yfuzz = axes[0x36] or axes[0x01] + + if xfuzz is None and yfuzz is None: + return None + + return (xfuzz, yfuzz) + + +def print_fuzz(what, fuzz): + print(' Checking {}... '.format(what), end='') + if fuzz is None: + print('not set') + elif fuzz == (0, 0): + print('is zero') + else: + print('x={} y={}'.format(*fuzz)) + + +def handle_existing_entry(device, fuzz): + # This is getting messy because we don't really know where the entry + # could be or how the match rule looks like. So we just check the + # default location only. + # For the match comparison, we search for the property value in the + # file. If there is more than one entry that uses the same + # overrides this will generate false positives. + # If the lines aren't in the same order in the file, it'll be a false + # negative. + overrides = { + 0x00: device.udev_device.get('EVDEV_ABS_00'), + 0x01: device.udev_device.get('EVDEV_ABS_01'), + 0x35: device.udev_device.get('EVDEV_ABS_35'), + 0x36: device.udev_device.get('EVDEV_ABS_36'), + } + + has_existing_rules = False + for key, value in overrides.items(): + if value is not None: + has_existing_rules = True + break + if not has_existing_rules: + return False + + print_red('Error! ', end='') + print('This device already has axis overrides defined') + print('') + print_bold('Searching for existing override...') + + # Construct a template that looks like a hwdb entry (values only) from + # the udev property values + template = [' EVDEV_ABS_00={}'.format(overrides[0x00]), + ' EVDEV_ABS_01={}'.format(overrides[0x01])] + if overrides[0x35] is not None: + template += [' EVDEV_ABS_35={}'.format(overrides[0x35]), + ' EVDEV_ABS_36={}'.format(overrides[0x36])] + + found = False + print('Checking in {}... '.format(OVERRIDE_HWDB_FILE), end='') + entry, prefix, lineno = check_file_for_lines(OVERRIDE_HWDB_FILE, template) + if entry is not None: + print_green('found') + print('The existing hwdb entry can be overwritten'); + return False + else: + print_red('not found') + print('Checking in {}... '.format(DEFAULT_HWDB_FILE, template), end='') + entry, prefix, lineno = check_file_for_lines(DEFAULT_HWDB_FILE, template) + if entry is not None: + print_green('found') + else: + print_red('not found') + print('The device has a hwdb override defined but it\'s not where I expected it to be.'); + print('Please look at the libinput documentation for more details.') + print('Exiting now.') + return True + + print_bold('Probable entry for this device found in line {}:'.format(lineno)) + print('\n'.join(prefix + entry)) + print('') + + print_bold('Suggested new entry for this device:') + new_entry = [] + for i in range(0, len(template)): + parts = entry[i].split(':') + while len(parts) < 4: + parts.append('') + parts[3] = str(fuzz) + new_entry.append(':'.join(parts)) + print('\n'.join(prefix + new_entry)) + print('') + + # Not going to overwrite the 60-evdev.hwdb entry with this program, too + # risky. And it may not be our device match anyway. + print_bold('You must now:') + print('\n'.join(( + '1. Check the above suggestion for sanity. Does it match your device?', + '2. Open {} and amend the existing entry'.format(DEFAULT_HWDB_FILE), + ' as recommended above', + '', + ' The property format is:', + ' EVDEV_ABS_00=min:max:resolution:fuzz', + '', + ' Leave the entry as-is and only add or amend the fuzz value.', + ' A non-existent value can be skipped, e.g. this entry sets the ', + ' resolution to 32 and the fuzz to 8', + ' EVDEV_ABS_00=::32:8', + '', + '3. Save the edited file', + '4. Say Y to the next prompt'))) + + cont = input('Continue? [Y/n] ') + if cont == 'n': + raise KeyboardInterrupt + + if test_hwdb_entry(device, fuzz): + print_bold('Please test the new fuzz setting by restarting libinput') + print_bold('Then submit a pull request for this hwdb entry change to ' + 'to systemd at http://github.com/systemd/systemd') + else: + print_bold('The new fuzz setting did not take effect.') + print_bold('Did you edit the correct file?') + print('Please look at the libinput documentation for more details.') + print('Exiting now.') + + return True + + +def reload_and_trigger_udev(device): + import time + + print('Running udevadm hwdb --update') + subprocess.run(['udevadm', 'hwdb', '--update'], check=True) + syspath = device.path.replace('/dev/input/', '/sys/class/input/') + time.sleep(1) + print('Running udevadm trigger {}'.format(syspath)) + subprocess.run(['udevadm', 'trigger', syspath], check=True) + time.sleep(1) + + +def test_hwdb_entry(device, fuzz): + reload_and_trigger_udev(device) + print_bold('Testing... ', end='') + + d = Device(device.path) + f = d.check_axis() + if fuzz == f[0] and fuzz == f[1]: + print_green('Success') + return True + else: + print_red('Error') + return False + + +def check_file_for_lines(path, template): + ''' + Checks file at path for the lines given in template. If found, the + return value is a tuple of the matching lines and the prefix (i.e. the + two lines before the matching lines) + ''' + try: + lines = [l[:-1] for l in open(path).readlines()] + idx = -1 + try: + while idx < len(lines) - 1: + idx += 1 + line = lines[idx] + if not line.startswith(' EVDEV_ABS_00'): + continue + if lines[idx:idx + len(template)] != template: + continue + + return (lines[idx:idx + len(template)], lines[idx - 2:idx], idx) + + except IndexError: + pass + except FileNotFoundError: + pass + + return (None, None, None) + + +def write_udev_rule(device, fuzz): + '''Write out a udev rule that may match the device, run udevadm trigger and + check if the udev rule worked. Of course, there's plenty to go wrong... + ''' + print('') + print_bold('Guessing a udev rule to overwrite the fuzz') + + # Some devices match better on pvr, others on pn, so we get to try both. yay + modalias = open('/sys/class/dmi/id/modalias').readlines()[0] + ms = modalias.split(':') + svn, pn, pvr = None, None, None + for m in ms: + if m.startswith('svn'): + svn = m + elif m.startswith('pn'): + pn = m + elif m.startswith('pvr'): + pvr = m + + # Let's print out both to inform and/or confuse the user + template = '\n'.join(('# {} {}', + 'evdev:name:{}:dmi:*:{}*:{}*:', + ' EVDEV_ABS_00=:::{}', + ' EVDEV_ABS_01=:::{}', + ' EVDEV_ABS_35=:::{}', + ' EVDEV_ABS_36=:::{}', + '')) + rule1 = template.format(svn[3:], device.name, device.name, svn, pvr, fuzz, fuzz, fuzz, fuzz) + rule2 = template.format(svn[3:], device.name, device.name, svn, pn, fuzz, fuzz, fuzz, fuzz) + + print('Full modalias is: {}'.format(modalias)) + print() + print_bold('Suggested udev rule, option 1:') + print(rule1) + print() + print_bold('Suggested udev rule, option 2:') + print(rule2) + print('') + + # The weird hwdb matching behavior means we match on the least specific + # rule (i.e. most wildcards) first although that was supposed to be fixed in + # systemd 3a04b789c6f1. + # Our rule uses dmi strings and will be more specific than what 60-evdev.hwdb + # already has. So we basically throw up our hands because we can't do anything + # then. + if handle_existing_entry(device, fuzz): + return + + while True: + print_bold('Wich rule do you want to to test? 1 or 2? ', end='') + yesno = input('Ctrl+C to exit ') + + if yesno == '1': + rule = rule1 + break + elif yesno == '2': + rule = rule2 + break + + fname = OVERRIDE_HWDB_FILE + try: + fd = open(fname, 'x') + except FileExistsError as e: + yesno = input('File {} exists, overwrite? [Y/n] '.format(fname)) + if yesno.lower == 'n': + return + + fd = open(fname, 'w') + + fd.write('# File generated by libinput measure fuzz\n\n') + fd.write(rule) + fd.close() + + if test_hwdb_entry(device, fuzz): + print('Your hwdb override file is in {}'.format(fname)) + print_bold('Please test the new fuzz setting by restarting libinput') + print_bold('Then submit a pull request for this hwdb entry to ' + 'systemd at http://github.com/systemd/systemd') + else: + print('The hwdb entry failed to apply to the device.') + print('Removing hwdb file again.') + os.remove(fname) + reload_and_trigger_udev(device) + print_bold('What now?') + print('1. Re-run this program and try the other suggested udev rule. If that fails,') + print('2. File a bug with the suggested udev rule at http://github.com/systemd/systemd') + + +def main(args): + parser = argparse.ArgumentParser( + description='Print fuzz settings' + ) + parser.add_argument('path', metavar='/dev/input/event0', + nargs='?', type=str, help='Path to device (optional)') + parser.add_argument('--fuzz', type=int, help='Suggsted fuzz') + args = parser.parse_args() + + try: + device = Device(args.path) + print_bold('Using {}: {}'.format(device.name, device.path)) + + fuzz = device.check_property() + print_fuzz('udev property', fuzz) + + fuzz = device.check_axis() + print_fuzz('axes', fuzz) + + userfuzz = args.fuzz + if userfuzz is not None: + write_udev_rule(device, userfuzz) + + except PermissionError as e: + print('Permission denied, please re-run as root') + except InvalidConfigurationError as e: + print('Error: {}'.format(e.message)) + except KeyboardInterrupt as e: + print('Exited on user request') + + +if __name__ == '__main__': + main(sys.argv) diff --git a/tools/libinput-measure-fuzz.man b/tools/libinput-measure-fuzz.man new file mode 100644 index 00000000..74b319cc --- /dev/null +++ b/tools/libinput-measure-fuzz.man @@ -0,0 +1,30 @@ +.TH libinput-measure-fuzz "1" +.SH NAME +libinput\-measure\-fuzz \- measure absolute axis fuzz +.SH SYNOPSIS +.B libinput measure fuzz [\-\-help] [options] +[\fI/dev/input/event0\fI] +.SH DESCRIPTION +.PP +The +.B "libinput measure fuzz" +tool measures the fuzz for an absolute axis on a kernel device. The current +implementation does not actually measure anything, it only prints the +relevant information available and suggests a udev rule. +.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 device 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.man b/tools/libinput-measure.man index 9e52e5e1..09267a97 100644 --- a/tools/libinput-measure.man +++ b/tools/libinput-measure.man @@ -22,6 +22,9 @@ Print help .SH FEATURES Features that can be measured include .TP 8 +.B libinput\-measure\-fuzz(1) +Measure touch fuzz to avoid pointer jitter +.TP 8 .B libinput\-measure\-touch\-size(1) Measure touch size and orientation .TP 8 |