diff --git a/meson.build b/meson.build index 38174f6c..ffa2faac 100644 --- a/meson.build +++ b/meson.build @@ -577,7 +577,22 @@ configure_file(input : 'tools/libinput-measure.man', install_dir : dir_man1, ) +libinput_analyze_sources = [ 'tools/libinput-analyze.c' ] +executable('libinput-analyze', + libinput_analyze_sources, + dependencies : deps_tools, + include_directories : [includes_src, includes_include], + install_dir : libinput_tool_path, + install : true, + ) +configure_file(input : 'tools/libinput-analyze.man', + output : 'libinput-analyze.1', + configuration : man_config, + install_dir : dir_man1, + ) + src_python_tools = files( + 'tools/libinput-analyze-per-slot-delta.py', 'tools/libinput-measure-fuzz.py', 'tools/libinput-measure-touchpad-tap.py', 'tools/libinput-measure-touchpad-pressure.py', diff --git a/tools/libinput-analyze-per-slot-delta.man b/tools/libinput-analyze-per-slot-delta.man new file mode 100644 index 00000000..567ba574 --- /dev/null +++ b/tools/libinput-analyze-per-slot-delta.man @@ -0,0 +1,69 @@ +.TH libinput-analyze-per-slot-delta "1" +.SH NAME +libinput\-analyze\-per\-slot\-delta \- analyze the per-event delta movement for touch slots +.SH SYNOPSIS +.B libinput analyze per-slot-delta [\-\-help] [options] \fIrecording.yml\fI +.SH DESCRIPTION +.PP +The +.B "libinput analyze per\-slot\-delta" +tool analyzes a recording made with +.B "libinput record" +and prints the delta movement per touch slot. +.PP +This is a debugging tool only, its output may change at any time. Do not +rely on the output. +.SH OPTIONS +.TP 8 +.B \-\-help +Print help +.TP 8 +.B \-\-use-mm +Print data in mm instead of device units +.TP 8 +.B \-\-use-st +Use the single-touch ABS_X/ABS_Y instead of the multitouch axes +.TP 8 +.B \-\-use-absolute +Print absolute coordinates, not deltas +.SH OUTPUT +An example output for a single finger touch on a touchpad supporting two +slots is below. This output is with the use of the +.B --use-mm +flag. +.PP +.nf +.sf + 0.000000 +0ms: ++++++ | ************* | + 0.021900 +21ms: →↘ +1.10/+0.14 | ************* | + 0.033468 +11ms: →↘ +1.15/+0.19 | ************* | + 0.043856 +10ms: →↘ +1.76/+0.22 | ************* | + 0.053237 +9ms: →↘ +2.20/+0.19 | ************* | +.fi +.in +.PP +The entry +.B ++++++ +indicates a finger has been put down, +.B ------ +indicates the finger has lifted. +The left-most column is the absolute timestamp in seconds.microseconds +followed by the relative time of the event to the previous event. The arrows +indicate the approximate direction on a 16-point compass. +.PP +Each multitouch slot supported by the hardware has one column, where the +column shows asterisk +.B ******** +no finger is down for that slot. Where the column shows spaces only, a +finger is down but no data is currently available. +.PP +In the above example, the third events occurs ~33ms into the recording, is +11ms after the previous event and has an east south-east direction. The +movement for this event was x: 1.15 and y: 0.19 mm. A second finger is not +currently down. +.SH LIBINPUT +Part of the +.B libinput(1) +suite + + diff --git a/tools/libinput-analyze-per-slot-delta.py b/tools/libinput-analyze-per-slot-delta.py new file mode 100755 index 00000000..3ee385ec --- /dev/null +++ b/tools/libinput-analyze-per-slot-delta.py @@ -0,0 +1,264 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 +# 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. +# +# +# Measures the relative motion between touch events (based on slots) +# +# Input is a libinput record yaml file + +import argparse +import math +import sys +import yaml +import libevdev + + +COLOR_RESET = '\x1b[0m' +COLOR_RED = '\x1b[6;31m' + + +class SlotState: + NONE = 0 + BEGIN = 1 + UPDATE = 2 + END = 3 + + +class Slot: + index = 0 + state = SlotState.NONE + x = 0 + y = 0 + dx = 0 + dy = 0 + dirty = False + + +class InputEvent: + def __init__(self, data): + self.sec = data[0] + self.usec = data[1] + self.evtype = data[2] + self.evcode = data[3] + self.value = data[4] + + +def main(argv): + global COLOR_RESET + global COLOR_RED + + slots = [] + xres, yres = 1, 1 + + parser = argparse.ArgumentParser(description="Measure delta between event frames for each slot") + parser.add_argument("--use-mm", action='store_true', help="Use mm instead of device deltas") + parser.add_argument("--use-st", action='store_true', help="Use ABS_X/ABS_Y instead of ABS_MT_POSITION_X/Y") + parser.add_argument("--use-absolute", action='store_true', help="Use absolute coordinates, not deltas") + parser.add_argument("path", metavar="recording", + nargs=1, help="Path to libinput-record YAML file") + args = parser.parse_args() + + if not sys.stdout.isatty(): + COLOR_RESET = '' + COLOR_RED = '' + + yml = yaml.safe_load(open(args.path[0])) + device = yml['devices'][0] + absinfo = device['evdev']['absinfo'] + try: + nslots = absinfo[libevdev.EV_ABS.ABS_MT_SLOT.value][1] + 1 + except KeyError: + args.use_st = True + + if args.use_st: + nslots = 1 + + slots = [Slot() for _ in range(0, nslots)] + + marker_begin_slot = " ++++++ | " # noqa + marker_end_slot = " ------ | " # noqa + marker_empty_slot = " *********** | " # noqa + marker_no_data = " | " # noqa + marker_button = "..............." # noqa + + if args.use_mm: + xres = 1.0 * absinfo[libevdev.EV_ABS.ABS_X.value][4] + yres = 1.0 * absinfo[libevdev.EV_ABS.ABS_Y.value][4] + if not xres or not yres: + print("Error: device doesn't have a resolution, cannot use mm") + sys.exit(1) + + marker_empty_slot = " ************* | " # noqa + marker_no_data = " | " # noqa + marker_begin_slot = " ++++++ | " # noqa + marker_end_slot = " ------ | " # noqa + + if args.use_st: + print("Warning: slot coordinates on FINGER/DOUBLETAP change may be incorrect") + + slot = 0 + last_time = None + + for event in device['events']: + for evdev in event['evdev']: + s = slots[slot] + e = InputEvent(evdev) + evbit = libevdev.evbit(e.evtype, e.evcode) + + if args.use_st: + # Note: this relies on the EV_KEY events to come in before the + # x/y events, otherwise the last/first event in each slot will + # be wrong. + if (evbit == libevdev.EV_KEY.BTN_TOOL_FINGER or + evbit == libevdev.EV_KEY.BTN_TOOL_PEN): + slot = 0 + s = slots[slot] + s.dirty = True + if e.value: + s.state = SlotState.BEGIN + else: + s.state = SlotState.END + elif evbit == libevdev.EV_KEY.BTN_TOOL_DOUBLETAP: + if len(slots) > 1: + slot = 1 + s = slots[slot] + s.dirty = True + if e.value: + s.state = SlotState.BEGIN + else: + s.state = SlotState.END + elif evbit == libevdev.EV_ABS.ABS_X: + if s.state == SlotState.UPDATE: + s.dx = e.value - s.x + s.x = e.value + s.dirty = True + elif evbit == libevdev.EV_ABS.ABS_Y: + if s.state == SlotState.UPDATE: + s.dy = e.value - s.y + s.y = e.value + s.dirty = True + else: + if evbit == libevdev.EV_ABS.ABS_MT_SLOT: + slot = e.value + s = slots[slot] + s.dirty = True + elif evbit == libevdev.EV_ABS.ABS_MT_TRACKING_ID: + if e.value == -1: + s.state = SlotState.END + else: + s.state = SlotState.BEGIN + s.dx = 0 + s.dy = 0 + s.dirty = True + elif evbit == libevdev.EV_ABS.ABS_MT_POSITION_X: + if s.state == SlotState.UPDATE: + s.dx = e.value - s.x + s.x = e.value + s.dirty = True + elif evbit == libevdev.EV_ABS.ABS_MT_POSITION_Y: + if s.state == SlotState.UPDATE: + s.dy = e.value - s.y + s.y = e.value + s.dirty = True + + if (evbit == libevdev.EV_KEY.BTN_TOUCH or + (evbit == libevdev.EV_KEY.BTN_TOOL_DOUBLETAP and nslots < 2) or + (evbit == libevdev.EV_KEY.BTN_TOOL_TRIPLETAP and nslots < 3) or + (evbit == libevdev.EV_KEY.BTN_TOOL_QUADTAP and nslots < 4) or + (evbit == libevdev.EV_KEY.BTN_TOOL_QUINTTAP and nslots < 5)): + print(' {} {} {} {}'.format(marker_button, + evbit.name, + e.value, + marker_button)) + + if evbit == libevdev.EV_SYN.SYN_REPORT: + if last_time is None: + last_time = e.sec * 1000000 + e.usec + tdelta = 0 + else: + t = e.sec * 1000000 + e.usec + tdelta = int((t - last_time) / 1000) # ms + last_time = t + + print("{:2d}.{:06d} {:+4d}ms: ".format(e.sec, e.usec, tdelta), end='') + for sl in slots: + if sl.state == SlotState.NONE: + print(marker_empty_slot, end='') + elif sl.state == SlotState.BEGIN: + print(marker_begin_slot, end='') + elif sl.state == SlotState.END: + print(marker_end_slot, end='') + elif not sl.dirty: + print(marker_no_data, end='') + else: + if sl.dx != 0 and sl.dy != 0: + t = math.atan2(sl.dx, sl.dy) + t += math.pi # in [0, 2pi] range now + + if t == 0: + t = 0.01 + else: + t = t * 180.0 / math.pi + + directions = ['↖↑', '↖←', '↙←', '↙↓', '↓↘', '→↘', '→↗', '↑↗'] + direction = "{:3.0f}".format(t) + direction = directions[int(t / 45)] + elif sl.dy == 0: + if sl.dx < 0: + direction = '←←' + else: + direction = '→→' + else: + if sl.dy < 0: + direction = '↑↑' + else: + direction = '↓↓' + + color = COLOR_RESET + + if args.use_mm: + sl.dx /= xres + sl.dy /= yres + if math.hypot(sl.dx, sl.dy) > 7: + color = COLOR_RED + print("{} {}{:+3.2f}/{:+03.2f}{} | ".format(direction, color, sl.dx, sl.dy, COLOR_RESET), end='') + elif args.use_absolute: + print("{} {}{:4d}/{:4d}{} | ".format(direction, color, sl.x, sl.y, COLOR_RESET), end='') + else: + print("{} {}{:4d}/{:4d}{} | ".format(direction, color, sl.dx, sl.dy, COLOR_RESET), end='') + s.dx = 0 + s.dy = 0 + if sl.state == SlotState.BEGIN: + sl.state = SlotState.UPDATE + elif sl.state == SlotState.END: + sl.state = SlotState.NONE + + sl.dirty = False + print("") + + +if __name__ == '__main__': + main(sys.argv) diff --git a/tools/libinput-analyze.c b/tools/libinput-analyze.c new file mode 100644 index 00000000..17e67dd7 --- /dev/null +++ b/tools/libinput-analyze.c @@ -0,0 +1,72 @@ +/* + * 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. + */ + +#include "config.h" + +#include +#include + +#include "shared.h" + +static inline void +usage(void) +{ + printf("Usage: libinput analyze [--help] \n"); +} + +int +main(int argc, char **argv) +{ + int option_index = 0; + + while (1) { + int c; + static struct option opts[] = { + { "help", no_argument, 0, 'h' }, + { 0, 0, 0, 0} + }; + + c = getopt_long(argc, argv, "+h", opts, &option_index); + if (c == -1) + break; + + switch(c) { + case 'h': + usage(); + return EXIT_SUCCESS; + default: + usage(); + return EXIT_FAILURE; + } + } + + if (optind >= argc) { + usage(); + return EXIT_FAILURE; + } + + argc--; + argv++; + + return tools_exec_command("libinput-analyze", argc, argv); +} diff --git a/tools/libinput-analyze.man b/tools/libinput-analyze.man new file mode 100644 index 00000000..dd55a50e --- /dev/null +++ b/tools/libinput-analyze.man @@ -0,0 +1,30 @@ +.TH libinput-analyze "1" "" "libinput @LIBINPUT_VERSION@" "libinput Manual" +.SH NAME +libinput\-analyze \- analyze device data +.SH SYNOPSIS +.B libinput analyze [\-\-help] \fI []\fR +.SH DESCRIPTION +.PP +The +.B "libinput analyze" +tool analyzes device data. Depending on what is to +be analyzed, this tool may not create a libinput context. +.PP +This is a debugging tool only, its output may change at any time. Do not +rely on the output. +.PP +This tool may need to be run as root to have access to the +/dev/input/eventX nodes. +.SH OPTIONS +.TP 8 +.B \-\-help +Print help +.SH FEATURES +Features that can be analyzed include +.TP 8 +.B libinput\-analyze\-per-slot-delta(1) +analyze the delta per event per slot +.SH LIBINPUT +Part of the +.B libinput(1) +suite