tools: add a tool to print a libinput recording as a table
authorPeter Hutterer <peter.hutterer@who-t.net>
Mon, 1 Feb 2021 04:23:06 +0000 (14:23 +1000)
committerPeter Hutterer <peter.hutterer@who-t.net>
Tue, 23 Feb 2021 03:46:00 +0000 (13:46 +1000)
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>
.gitlab-ci/libinput.spec.in
meson.build
tools/libinput-analyze-recording.man [new file with mode: 0644]
tools/libinput-analyze-recording.py [new file with mode: 0755]
tools/libinput-analyze.man

index 670afd1..5b5f0a0 100644 (file)
@@ -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
index f9d8cca..d1b5fdb 100644 (file)
@@ -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 (file)
index 0000000..13a44af
--- /dev/null
@@ -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 (executable)
index 0000000..ab8ba51
--- /dev/null
@@ -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
index 194c223..bd2a83d 100644 (file)
@@ -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