From e3eda9db6aa12b6cd29db98bcbc4ef8899af226f Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Wed, 5 Jul 2017 18:50:52 +1000 Subject: [PATCH] tools: add a tool to measure touch size and orientation Signed-off-by: Peter Hutterer --- meson.build | 8 + tools/libinput-measure-touch-size | 335 ++++++++++++++++++++++++++ tools/libinput-measure-touch-size.man | 57 +++++ tools/libinput-measure.man | 3 + tools/libinput.man | 3 + 5 files changed, 406 insertions(+) create mode 100755 tools/libinput-measure-touch-size create mode 100644 tools/libinput-measure-touch-size.man diff --git a/meson.build b/meson.build index 9b0fa246..5ab7454a 100644 --- a/meson.build +++ b/meson.build @@ -431,6 +431,14 @@ configure_file(input : 'tools/libinput-measure-touchpad-pressure.man', install : true, install_dir : join_paths(get_option('mandir'), 'man1') ) +install_data('tools/libinput-measure-touch-size', + install_dir : libinput_tool_path) +configure_file(input : 'tools/libinput-measure-touch-size.man', + output : 'libinput-measure-touch-size.1', + configuration : man_config, + install : true, + install_dir : join_paths(get_option('mandir'), 'man1') + ) if get_option('debug-gui') dep_gtk = dependency('gtk+-3.0') diff --git a/tools/libinput-measure-touch-size b/tools/libinput-measure-touch-size new file mode 100755 index 00000000..66c7571e --- /dev/null +++ b/tools/libinput-measure-touch-size @@ -0,0 +1,335 @@ +#!/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. +# + +import sys +import argparse +import evdev +import evdev.ecodes +import pyudev + + +class Range(object): + """Class to keep a min/max of a value around""" + def __init__(self): + self.min = float('inf') + self.max = float('-inf') + + def update(self, value): + self.min = min(self.min, value) + self.max = max(self.max, value) + + +class Touch(object): + """A single data point of a sequence (i.e. one event frame)""" + + def __init__(self, major=None, minor=None, orientation=None): + self._major = major + self._minor = minor + self._orientation = orientation + self.dirty = False + + @property + def major(self): + return self._major + + @major.setter + def major(self, major): + self._major = major + self.dirty = True + + @property + def minor(self): + return self._minor + + @minor.setter + def minor(self, minor): + self._minor = minor + self.dirty = True + + @property + def orientation(self): + return self._orientation + + @orientation.setter + def orientation(self, orientation): + self._orientation = orientation + self.dirty = True + + def __str__(self): + s = "Touch: major {:3d}".format(self.major) + if self.minor is not None: + s += ", minor {:3d}".format(self.minor) + if self.orientation is not None: + s += ", orientation {:+3d}".format(self.orientation) + return s + + +class TouchSequence(object): + """A touch sequence from beginning to end""" + + def __init__(self, device, tracking_id): + self.device = device + self.tracking_id = tracking_id + self.points = [] + + self.is_active = True + + self.is_down = False + self.was_down = False + self.is_palm = False + self.was_palm = False + + self.major_range = Range() + self.minor_range = Range() + + def append(self, touch): + """Add a Touch to the sequence""" + self.points.append(touch) + self.major_range.update(touch.major) + self.minor_range.update(touch.minor) + + if touch.major < self.device.up or \ + touch.minor < self.device.up: + self.is_down = False + elif touch.major > self.device.down or \ + touch.minor > self.device.down: + self.is_down = True + self.was_down = True + + self.is_palm = touch.major > self.device.palm + if self.is_palm: + self.was_palm = True + + def finalize(self): + """Mark the TouchSequence as complete (finger is up)""" + self.is_active = False + + def avg(self): + """Average pressure value of this sequence""" + return int(sum([p.pressure for p in self.points])/len(self.points)) + + def median(self): + """Median pressure value of this sequence""" + ps = sorted([p.pressure for p in self.points]) + idx = int(len(self.points)/2) + return ps[idx] + + def __str__(self): + return self._str_state() if self.is_active else self._str_summary() + + def _str_summary(self): + if not self.points: + return "Sequence: no major/minor values recorded" + + s = "Sequence: major: [{:3d}..{:3d}] ".format( + self.major_range.min, self.major_range.max + ) + if self.device.has_minor: + s += "minor: [{:3d}..{:3d}] ".format( + self.minor_range.min, self.minor_range.max + ) + if self.was_down: + s += " down" + if self.was_palm: + s += " palm" + + return s + + def _str_state(self): + touch = self.points[-1] + s = "{}, tags: {} {}".format( + touch, + "down" if self.is_down else " ", + "palm" if self.is_palm else " " + ) + return s + + +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) + # 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 pressure 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_MT_TOUCH_MAJOR not in codes: + raise InvalidDeviceError("device does not have ABS_MT_TOUCH_MAJOR") + + self.has_minor = evdev.ecodes.ABS_MT_TOUCH_MINOR in codes + self.has_orientation = evdev.ecodes.ABS_MT_ORIENTATION in codes + + self.up = 0 + self.down = 0 + self.palm = 0 + + self._init_thresholds_from_udev() + self.sequences = [] + self.touch = Touch(0, 0) + + def find_touch_device(self): + context = pyudev.Context() + for device in context.list_devices(subsystem='input'): + if not device.get('ID_INPUT_TOUCHPAD', 0) and \ + not device.get('ID_INPUT_TOUCHSCREEN', 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 _init_thresholds_from_udev(self): + context = pyudev.Context() + ud = pyudev.Devices.from_device_file(context, self.path) + v = ud.get('LIBINPUT_ATTR_TOUCH_SIZE_RANGE') + if v: + self.up, self.down = colon_tuple(v) + + v = ud.get('LIBINPUT_ATTR_PALM_SIZE_THRESHOLD') + if v: + self.palm = int(v) + + def start_new_sequence(self, tracking_id): + self.sequences.append(TouchSequence(self, tracking_id)) + + def current_sequence(self): + return self.sequences[-1] + + def handle_key(self, event): + tapcodes = [evdev.ecodes.BTN_TOOL_DOUBLETAP, + evdev.ecodes.BTN_TOOL_TRIPLETAP, + evdev.ecodes.BTN_TOOL_QUADTAP, + evdev.ecodes.BTN_TOOL_QUINTTAP] + if event.code in tapcodes and event.value > 0: + print("\rThis tool cannot handle multiple fingers, " + "output will be invalid", file=sys.stderr) + + def handle_abs(self, event): + if event.code == evdev.ecodes.ABS_MT_TRACKING_ID: + if event.value > -1: + self.start_new_sequence(event.value) + else: + s = self.current_sequence() + s.finalize() + print("\r{}".format(s)) + elif event.code == evdev.ecodes.ABS_MT_TOUCH_MAJOR: + self.touch.major = event.value + elif event.code == evdev.ecodes.ABS_MT_TOUCH_MINOR: + self.touch.minor = event.value + elif event.code == evdev.ecodes.ABS_MT_ORIENTATION: + self.touch.orientation = event.value + + def handle_syn(self, event): + if self.touch.dirty: + self.current_sequence().append(self.touch) + print("\r{}".format(self.current_sequence()), end="") + self.touch = Touch(major=self.touch.major, + minor=self.touch.minor, + orientation=self.touch.orientation) + + def handle_event(self, event): + if event.type == evdev.ecodes.EV_ABS: + self.handle_abs(event) + elif event.type == evdev.ecodes.EV_KEY: + self.handle_key(event) + elif event.type == evdev.ecodes.EV_SYN: + self.handle_syn(event) + + def read_events(self): + print("Ready for recording data.") + print("Touch sizes used: {}:{}".format(self.down, self.up)) + print("Palm size used: {}".format(self.palm)) + print("Place a single finger on the device to measure touch size.\n" + "Ctrl+C to exit\n") + + for event in self.device.read_loop(): + self.handle_event(event) + + +def colon_tuple(string): + try: + ts = string.split(':') + t = tuple([int(x) for x in ts]) + if len(t) == 2 and t[0] >= t[1]: + return t + except: + pass + + msg = "{} is not in format N:M (N >= M)".format(string) + raise argparse.ArgumentTypeError(msg) + + +def main(args): + parser = argparse.ArgumentParser( + description="Measure touch size and orientation" + ) + parser.add_argument('path', metavar='/dev/input/event0', + nargs='?', type=str, help='Path to device (optional)') + parser.add_argument('--touch-thresholds', metavar='down:up', + type=colon_tuple, + help='Thresholds when a touch is logically down or up') + parser.add_argument('--palm-threshold', metavar='t', + type=int, help='Threshold when a touch is a palm') + args = parser.parse_args() + + try: + device = Device(args.path) + + if args.touch_thresholds is not None: + device.down, device.up = args.touch_thresholds + + if args.palm_threshold is not None: + device.palm = args.palm_threshold + + device.read_events() + except KeyboardInterrupt: + pass + except (PermissionError, OSError): + print("Error: failed to open device") + except InvalidDeviceError as e: + print("Error: {}".format(e)) + +if __name__ == "__main__": + main(sys.argv) diff --git a/tools/libinput-measure-touch-size.man b/tools/libinput-measure-touch-size.man new file mode 100644 index 00000000..f33f13c1 --- /dev/null +++ b/tools/libinput-measure-touch-size.man @@ -0,0 +1,57 @@ +.TH libinput-measure-touch-size "1" +.SH NAME +libinput\-measure\-touch-size \- measure touch size and orientation of devices +.SH SYNOPSIS +.B libinput measure touch\-size [\-\-help] [options] +[\fI/dev/input/event0\fI] +.SH DESCRIPTION +.PP +The +.B "libinput measure touch\-size" +tool measures the size and orientation of a touch as provided by the kernel. +an interactive tool. When executed, the tool will prompt the user to +interact with the touch device. On termination, the tool prints a summary of the +values seen. This data should be attached to any +touch\-size\-related bug report. +.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 touch-capable device and +uses that node. +.TP 8 +.B \-\-help +Print help +.TP 8 +.B \-\-touch\-thresholds=\fI"down:up"\fR +Set the logical touch size thresholds to +.I down +and +.I up, +respectively. When a touch exceeds the size in +.I down +it is considered logically down. If a touch is logically down and goes below +the size in +.I up, +it is considered logically up. The thresholds have to be in +device-specific pressure values and it is required that +.I down +>= +.I up. +.TP 8 +.B \-\-palm\-threshold=\fIN\fR +Assume a palm threshold of +.I N. +The threshold has to be in device-specific pressure values. +.PP +If the touch-thresholds or the palm-threshold are not provided, +this tool uses the thresholds provided by the udev hwdb (if any) or the +built-in defaults. +.SH LIBINPUT +Part of the +.B libinput(1) +suite diff --git a/tools/libinput-measure.man b/tools/libinput-measure.man index d91afdd0..703900b7 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\-touch\-size(1) +Measure touch size and orientation +.TP 8 .B libinput\-measure\-touchpad\-tap\-time(1) Measure tap-to-click time .TP 8 diff --git a/tools/libinput.man b/tools/libinput.man index fef7cdca..16cf7bd7 100644 --- a/tools/libinput.man +++ b/tools/libinput.man @@ -45,6 +45,9 @@ List all devices recognized by libinput .B libinput\-measure(1) Measure various properties of devices .TP 8 +.B libinput\-measure\-touch\-size(1) +Measure touch size and orientation +.TP 8 .B libinput\-measure\-touchpad\-tap(1) Measure tap-to-click time .TP 8