summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPeter Hutterer <peter.hutterer@who-t.net>2021-02-01 14:23:06 +1000
committerPeter Hutterer <peter.hutterer@who-t.net>2021-02-23 13:46:00 +1000
commit6a6435ae4bd415ea2e7da1486d9c0d910378ec0d (patch)
treea5bdb76bcb474f433861368d941eb3a18f4091ff
parent9323cdfc11629bdb9a990a82bc8302bb562ce982 (diff)
downloadlibinput-6a6435ae4bd415ea2e7da1486d9c0d910378ec0d.tar.gz
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.in2
-rw-r--r--meson.build2
-rw-r--r--tools/libinput-analyze-recording.man39
-rwxr-xr-xtools/libinput-analyze-recording.py189
-rw-r--r--tools/libinput-analyze.man4
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