diff options
author | Peter Hutterer <peter.hutterer@who-t.net> | 2018-06-27 14:01:25 +1000 |
---|---|---|
committer | Peter Hutterer <peter.hutterer@who-t.net> | 2018-07-09 11:28:41 +1000 |
commit | 6be9c3c84e861332938e50d04e82e235a5e2765f (patch) | |
tree | 8c5b82f7db57f7227032fe67e5e59d00dfb1f20b /tools/libinput-measure-trackpoint-range.py | |
parent | 2caf557e1022a79e3812a84cd2aaf4651061ac63 (diff) |
tools: fake-build the other tools the same way as measure touchpad-tap
Doesn't actually do anything but this way they end up in the builddir and can
be picked up by ./builddir/libinput measure fuzz, etc.
And rename the source files to .py to signal that they are not supposed to be
directly executed.
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
Diffstat (limited to 'tools/libinput-measure-trackpoint-range.py')
-rwxr-xr-x | tools/libinput-measure-trackpoint-range.py | 232 |
1 files changed, 232 insertions, 0 deletions
diff --git a/tools/libinput-measure-trackpoint-range.py b/tools/libinput-measure-trackpoint-range.py new file mode 100755 index 00000000..90d34e9d --- /dev/null +++ b/tools/libinput-measure-trackpoint-range.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python3 +# vim: set expandtab shiftwidth=4: +# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil -*- */ +# +# 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. +# + +from math import atan, sqrt, pi, floor, ceil +import sys +import argparse +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) + +# This should match libinput's DEFAULT_TRACKPOINT_RANGE +DEFAULT_RANGE = 20 +MINIMUM_EVENT_COUNT = 1000 + + +class InvalidDeviceError(Exception): + pass + + +class Delta(object): + def __init__(self, x=0, y=0): + self.x = x + self.y = y + + def __bool__(self): + return self.x != 0 or self.y != 0 + + def r(self): + return sqrt(self.x**2 + self.y**2) + +class Device(object): + def __init__(self, path): + if path is None: + path = self._find_trackpoint_device() + self.path = path + + self.device = evdev.InputDevice(self.path) + + print("Using {}: {}\n".format(self.device.name, path)) + + self.deltas = [] + self.nxdeltas = 0 + self.nydeltas = 0 + + self.current_delta = Delta() + self.max_delta = Delta(0, 0) + + def _find_trackpoint_device(self): + context = pyudev.Context() + for device in context.list_devices(subsystem='input'): + if not device.get('ID_INPUT_POINTINGSTICK', 0): + continue + + if not device.device_node or \ + not device.device_node.startswith('/dev/input/event'): + continue + + return device.device_node + + raise InvalidDeviceError("Unable to find a trackpoint device") + + def handle_rel(self, event): + if event.code == evdev.ecodes.REL_X: + self.current_delta.x = event.value + if self.max_delta.x < abs(event.value): + self.max_delta.x = abs(event.value) + elif event.code == evdev.ecodes.REL_Y: + self.current_delta.y = event.value + if self.max_delta.y < abs(event.value): + self.max_delta.y = abs(event.value) + + def handle_syn(self, event): + self.deltas.append(self.current_delta) + if self.current_delta.x != 0: + self.nxdeltas += 1 + if self.current_delta.y != 0: + self.nydeltas += 1 + + self.current_delta = Delta() + + print("\rTrackpoint sends: max x:{:3d}, max y:{:3} samples [{}, {}]" + .format( + self.max_delta.x, self.max_delta.y, + self.nxdeltas, self.nydeltas, + ), end="") + + def read_events(self): + for event in self.device.read_loop(): + if event.type == evdev.ecodes.EV_REL: + self.handle_rel(event) + elif event.type == evdev.ecodes.EV_SYN: + self.handle_syn(event) + + def print_summary(self): + print("\n") # undo the \r from the status line + if not self.deltas: + return + + if len(self.deltas) < MINIMUM_EVENT_COUNT: + print("WARNING: *******************************************\n" + "WARNING: Insufficient samples, data is not reliable\n" + "WARNING: *******************************************\n") + + print("Histogram for x axis deltas, in counts of 5") + xs = [d.x for d in self.deltas] + minx = min(xs) + maxx = max(xs) + for i in range(minx, maxx + 1): + xc = len([x for x in xs if x == i]) + xc = int(xc/5) # counts of 5 is enough + print("{:4}: {}".format(i, "+" * xc, end="")) + + print("Histogram for y axis deltas, in counts of 5") + ys = [d.y for d in self.deltas] + miny = min(ys) + maxy = max(ys) + for i in range(miny, maxy + 1): + yc = len([y for y in ys if y == i]) + yc = int(yc/5) # counts of 5 is enough + print("{:4}: {}".format(i, "+" * yc, end="")) + + print("Histogram for radius (amplitude) deltas") + rs = [d.r() for d in self.deltas if d] + nr = 50 + minr = 0 + maxr = ceil(max(rs)) + for x in range(0, nr): + yc = len([y for y in rs if y >= x * maxr/nr + and y < (x+1) * maxr/nr]) + print("{:>6.1f}-{:<6.1f}: {:6} {}". + format(x * maxr/nr, (x+1) * maxr/nr, + yc, "+" * int(yc/5), end="")) + + minr = min(rs) + + axs = sorted([abs(x) for x in xs]) + ays = sorted([abs(y) for y in ys]) + ars = sorted([y for y in rs]) + + avgx = int(sum(axs)/len(axs)) + avgy = int(sum(ays)/len(ays)) + avgr = sum(ars)/len(ars) + + medx = axs[int(len(axs)/2)] + medy = ays[int(len(ays)/2)] + medr = ars[int(len(ars)/2)] + + pc95x = axs[int(len(axs) * 0.95)] + pc95y = ays[int(len(ays) * 0.95)] + pc95r = ars[int(len(ars) * 0.95)] + + print("Min r: {:6.1f}, Max r: {:6.1f}, Max/Min: {:6.1f}". + format(minr, max(rs), max(rs)/minr)) + print("Average for abs deltas: x: {:3} y: {:3} r: {:6.1f}".format(avgx, avgy, avgr)) + print("Median for abs deltas: x: {:3} y: {:3} r: {:6.1f}".format(medx, medy, medr)) + print("95% percentile for abs deltas: x: {:3} y: {:3} r: {:6.1f}" + .format(pc95x, pc95y, pc95r) + ) + if (minr > 2): + suggested = 10 * ceil(minr * DEFAULT_RANGE / 10) + print("""\ +The minimum amplitude is too big for precise pointer movements. +The recommended value for LIBINPUT_ATTR_TRACKPOINT_RANGE +is 20 * {} ~= {} or higher, which would result in a corrected +delta range of {:>.1f}-{:<.1f}. +""".format(ceil(minr), suggested, + minr*DEFAULT_RANGE/suggested, maxr*DEFAULT_RANGE/suggested)) + +def main(args): + parser = argparse.ArgumentParser( + description="Measure the trackpoint delta coordinate range" + ) + parser.add_argument('path', metavar='/dev/input/event0', + nargs='?', type=str, help='Path to device (optional)') + + args = parser.parse_args() + + try: + device = Device(args.path) + + print( + "This tool measures the commonly used pressure range of the\n" + "trackpoint. Start by pushing the trackpoint very gently in\n" + "slow, small circles. Slowly increase pressure until the pointer\n" + "moves quickly around the screen edges, but do not use excessive\n" + "pressure that would not be used during day-to-day movement.\n" + "Also make diagonal some movements, both slow and quick.\n" + "When you're done, start over, until the displayed event count\n" + "is {} or more for both x and y axis.\n\n" + "Hit Ctrl-C to stop the measurement and display results.\n" + "For best results, run this tool several times to get an idea\n" + "of the common range.\n".format(MINIMUM_EVENT_COUNT)) + device.read_events() + except KeyboardInterrupt: + device.print_summary() + except (PermissionError, OSError): + print("Error: failed to open device. Are you running as root?") + except InvalidDeviceError as e: + print("Error: {}".format(e)) + + +if __name__ == "__main__": + main(sys.argv) |