#!/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 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)
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(libevdev.Device):
def __init__(self, path):
if path is None:
self.path = self.find_touch_device()
else:
self.path = path
fd = open(self.path, 'rb')
super().__init__(fd)
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]:
print_bold('WARNING: fuzz mismatch ABS_X: {}, ABS_MT_POSITION_X: {}'.format(axes[0x00], axes[0x35]))
if axes[0x36] is not None:
if axes[0x36] != axes[0x01]:
print_bold('WARNING: fuzz mismatch ABS_Y: {}, ABS_MT_POSITION_Y: {}'.format(axes[0x01], axes[0x36]))
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_axes(self):
'''
Returns a tuple of (xfuzz, yfuzz) with the fuzz as set on the device
axis. Returns None if no fuzz is set.
'''
if not self.has(libevdev.EV_ABS.ABS_X) or not self.has(libevdev.EV_ABS.ABS_Y):
raise InvalidDeviceError('device does not have x/y axes')
if self.has(libevdev.EV_ABS.ABS_MT_POSITION_X) != self.has(libevdev.EV_ABS.ABS_MT_POSITION_Y):
raise InvalidDeviceError('device does not have both multitouch axes')
xfuzz = self.absinfo[libevdev.EV_ABS.ABS_X].fuzz or \
self.absinfo[libevdev.EV_ABS.ABS_MT_POSITION_X].fuzz
yfuzz = self.absinfo[libevdev.EV_ABS.ABS_Y].fuzz or \
self.absinfo[libevdev.EV_ABS.ABS_MT_POSITION_Y].fuzz
if xfuzz is 0 and yfuzz is 0:
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])]
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_axes()
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:
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 and/or suggest udev rules for the fuzz to be adjusted.'
)
parser.add_argument('path', metavar='/dev/input/event0',
nargs='?', type=str, help='Path to device (optional)')
parser.add_argument('--fuzz', type=int, help='Suggested 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_axes()
print_fuzz('axes', fuzz)
userfuzz = args.fuzz
if userfuzz is not None:
write_udev_rule(device, userfuzz)
except PermissionError:
print('Permission denied, please re-run as root')
except InvalidConfigurationError as e:
print('Error: {}'.format(e))
except InvalidDeviceError as e:
print('Error: {}'.format(e))
except KeyboardInterrupt:
print('Exited on user request')
if __name__ == '__main__':
main(sys.argv)