diff options
author | Peter Hutterer <peter.hutterer@who-t.net> | 2021-02-01 14:23:06 +1000 |
---|---|---|
committer | Peter Hutterer <peter.hutterer@who-t.net> | 2021-02-23 13:46:00 +1000 |
commit | 6a6435ae4bd415ea2e7da1486d9c0d910378ec0d (patch) | |
tree | a5bdb76bcb474f433861368d941eb3a18f4091ff | |
parent | 9323cdfc11629bdb9a990a82bc8302bb562ce982 (diff) |
tools: add a tool to print a libinput recording as a table
This makes it easier to visualize changes in various axes or key states that
should not be there, doubly so for long recordings.
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
-rw-r--r-- | .gitlab-ci/libinput.spec.in | 2 | ||||
-rw-r--r-- | meson.build | 2 | ||||
-rw-r--r-- | tools/libinput-analyze-recording.man | 39 | ||||
-rwxr-xr-x | tools/libinput-analyze-recording.py | 189 | ||||
-rw-r--r-- | tools/libinput-analyze.man | 4 |
5 files changed, 236 insertions, 0 deletions
diff --git a/.gitlab-ci/libinput.spec.in b/.gitlab-ci/libinput.spec.in index 670afd1a..5b5f0a05 100644 --- a/.gitlab-ci/libinput.spec.in +++ b/.gitlab-ci/libinput.spec.in @@ -111,6 +111,7 @@ intended to be run by users. %{_libexecdir}/libinput/libinput-replay %{_libexecdir}/libinput/libinput-analyze %{_libexecdir}/libinput/libinput-analyze-per-slot-delta +%{_libexecdir}/libinput/libinput-analyze-recording %{_libexecdir}/libinput/libinput-analyze-touch-down-state %{_mandir}/man1/libinput-debug-gui.1* %{_mandir}/man1/libinput-debug-tablet.1* @@ -127,6 +128,7 @@ intended to be run by users. %{_mandir}/man1/libinput-replay.1* %{_mandir}/man1/libinput-analyze.1* %{_mandir}/man1/libinput-analyze-per-slot-delta.1* +%{_mandir}/man1/libinput-analyze-recording.1* %{_mandir}/man1/libinput-analyze-touch-down-state.1* %files test diff --git a/meson.build b/meson.build index f9d8ccac..d1b5fdba 100644 --- a/meson.build +++ b/meson.build @@ -519,6 +519,7 @@ executable('libinput-analyze', src_python_tools = files( 'tools/libinput-analyze-per-slot-delta.py', + 'tools/libinput-analyze-recording.py', 'tools/libinput-analyze-touch-down-state.py', 'tools/libinput-measure-fuzz.py', 'tools/libinput-measure-touchpad-size.py', @@ -912,6 +913,7 @@ src_man += files( 'tools/libinput.man', 'tools/libinput-analyze.man', 'tools/libinput-analyze-per-slot-delta.man', + 'tools/libinput-analyze-recording.man', 'tools/libinput-analyze-touch-down-state.man', 'tools/libinput-debug-events.man', 'tools/libinput-debug-tablet.man', diff --git a/tools/libinput-analyze-recording.man b/tools/libinput-analyze-recording.man new file mode 100644 index 00000000..13a44af7 --- /dev/null +++ b/tools/libinput-analyze-recording.man @@ -0,0 +1,39 @@ +.TH libinput-analyze-recording "1" +.SH NAME +libinput\-analyze\-recording \- analyze a device recording +.SH SYNOPSIS +.B libinput analyze recording [\-\-help] [options] \fIrecording.yml\fI +.SH DESCRIPTION +.PP +The +.B "libinput analyze recording" +tool analyzes a recording made with +.B "libinput record" +and prints a tabular summary of the events in that recording. +.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 +.SH OUTPUT +An example output for a tablet sequence is below. +.PP +.nf +.sf +Time | X | Y | PRESSURE | DISTANCE | MISC | SERIAL +----------------------------------------------------------------- + 0.000 | 9717 | 6266 | | 63 | 0x862 | 0x9a805597 | BTN_TOOL_PEN + 0.005 | 9709 | | | | | 0x9a805597 | BTN_TOOL_PEN + 0.012 | 9701 | | | | | 0x9a805597 | BTN_TOOL_PEN + 0.020 | 9692 | 6269 | | | | 0x9a805597 | BTN_TOOL_PEN + 0.028 | 9680 | 6277 | | | | 0x9a805597 | BTN_TOOL_PEN + 0.034 | 9668 | 6279 | | | | 0x9a805597 | BTN_TOOL_PEN + 0.042 | 9654 | 6282 | | | | 0x9a805597 | BTN_TOOL_PEN +.fi +.in +.SH LIBINPUT +Part of the +.B libinput(1) +suite diff --git a/tools/libinput-analyze-recording.py b/tools/libinput-analyze-recording.py new file mode 100755 index 00000000..ab8ba51c --- /dev/null +++ b/tools/libinput-analyze-recording.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 +# vim: set expandtab shiftwidth=4: +# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil -*- */ +# +# Copyright © 2021 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. +# +# Prints the data from a libinput recording in a table format to ease +# debugging. +# +# Input is a libinput record yaml file + +import argparse +import sys +import yaml +import libevdev + +# minimum width of a field in the table +MIN_FIELD_WIDTH = 6 + + +# Default is to just return the value of an axis, but some axes want special +# formatting. +def format_value(code, value): + if code in (libevdev.EV_ABS.ABS_MISC, libevdev.EV_MSC.MSC_SERIAL): + return f"{value & 0xFFFFFFFF:#x}" + + # Rel axes we always print the sign + if code.type == libevdev.EV_REL: + return f"{value:+d}" + + return f"{value}" + + +# The list of axes we want to track +def is_tracked_axis(code): + if code.type in (libevdev.EV_KEY, libevdev.EV_SW, libevdev.EV_SYN): + return False + + # We don't do slots in this tool + if code.type == libevdev.EV_ABS: + if libevdev.EV_ABS.ABS_MT_SLOT <= code <= libevdev.EV_ABS.ABS_MAX: + return False + + return True + + +def main(argv): + parser = argparse.ArgumentParser( + description="Display a recording in a tabular format" + ) + parser.add_argument( + "path", metavar="recording", nargs=1, help="Path to libinput-record YAML file" + ) + args = parser.parse_args() + + yml = yaml.safe_load(open(args.path[0])) + if yml["ndevices"] > 1: + print(f"WARNING: Using only first {yml['ndevices']} devices in recording") + device = yml["devices"][0] + + if not device["events"]: + print(f"No events found in recording") + sys.exit(1) + + def events(): + """ + Yields the next event in the recording + """ + for event in device["events"]: + for evdev in event.get("evdev", []): + yield libevdev.InputEvent( + code=libevdev.evbit(evdev[2], evdev[3]), + value=evdev[4], + sec=evdev[0], + usec=evdev[1], + ) + + def interesting_axes(events): + """ + Yields the libevdev codes with the axes in this recording + """ + used_axes = [] + for e in events: + if e.code not in used_axes and is_tracked_axis(e.code): + yield e.code + used_axes.append(e.code) + + # Compile all axes that we want to print first + axes = sorted( + interesting_axes(events()), key=lambda x: x.type.value * 1000 + x.value + ) + # Strip the REL_/ABS_ prefix for the headers + headers = [a.name[4:].rjust(MIN_FIELD_WIDTH) for a in axes] + # for easier formatting later, we keep the header field width in a dict + axes = {a: len(h) for a, h in zip(axes, headers)} + + # Time is a special case, always the first entry + # Format uses ms only, we rarely ever care about µs + headers = [f"{'Time':<7s}"] + headers + ["Keys"] + header_line = f"{' | '.join(headers)}" + print(header_line) + print("-" * len(header_line)) + + current_frame = {} # {evdev-code: value} + axes_in_use = {} # to print axes never sending events + last_fields = [] # to skip duplicate lines + continuation_count = 0 + + keystate = {} + keystate_changed = False + + for e in events(): + axes_in_use[e.code] = True + + if e.code.type == libevdev.EV_KEY: + keystate[e.code] = e.value + keystate_changed = True + elif is_tracked_axis(e.code): + current_frame[e.code] = e.value + elif e.code == libevdev.EV_SYN.SYN_REPORT: + fields = [] + for a in axes: + s = format_value(a, current_frame[a]) if a in current_frame else " " + fields.append(s.rjust(max(MIN_FIELD_WIDTH, axes[a]))) + current_frame = {} + + if last_fields != fields or keystate_changed: + last_fields = fields.copy() + keystate_changed = False + + if continuation_count: + continuation_count = 0 + print("") + + fields.insert(0, f"{e.sec: 3d}.{e.usec//1000:03d}") + keys_down = [k.name for k, v in keystate.items() if v] + fields.append(", ".join(keys_down)) + print(" | ".join(fields)) + else: + continuation_count += 1 + print(f"\r ... +{continuation_count}", end="", flush=True) + + # Print out any rel/abs axes that not generate events in + # this recording + unused_axes = [] + for evtype, evcodes in device["evdev"]["codes"].items(): + for c in evcodes: + code = libevdev.evbit(int(evtype), int(c)) + if is_tracked_axis(code) and code not in axes_in_use: + unused_axes.append(code) + + if unused_axes: + print( + f"Axes present but without events: {', '.join([a.name for a in unused_axes])}" + ) + + for e in events(): + if libevdev.EV_ABS.ABS_MT_SLOT <= code <= libevdev.EV_ABS.ABS_MAX: + print( + "WARNING: This recording contains multitouch data that is not supported by this tool." + ) + break + + +if __name__ == "__main__": + try: + main(sys.argv) + except BrokenPipeError: + pass diff --git a/tools/libinput-analyze.man b/tools/libinput-analyze.man index 194c2234..bd2a83da 100644 --- a/tools/libinput-analyze.man +++ b/tools/libinput-analyze.man @@ -25,6 +25,10 @@ Features that can be analyzed include .B libinput\-analyze\-per-slot-delta(1) analyze the delta per event per slot .TP 8 +.B libinput\-analyze\-recording(1) +analyze a recording made with +.B libinput\-record(1) +.TP 8 .B libinput\-analyze\-touch-down-state(1) analyze the state of each touch in a recording .SH LIBINPUT |