#!/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 SlotFormatter: width = 16 def __init__( self, is_absolute=False, resolution=None, threshold=None, ignore_below=None ): self.threshold = threshold self.ignore_below = ignore_below self.resolution = resolution self.is_absolute = is_absolute self.slots = [] self.have_data = False self.filtered = False def __str__(self): return " | ".join(self.slots) def format_slot(self, slot): if slot.state == SlotState.BEGIN: self.slots.append("+++++++".center(self.width)) self.have_data = True elif slot.state == SlotState.END: self.slots.append("-------".center(self.width)) self.have_data = True elif slot.state == SlotState.NONE: self.slots.append(("*" * (self.width - 2)).center(self.width)) elif not slot.dirty: self.slots.append(" ".center(self.width)) else: if self.resolution is not None: dx, dy = slot.dx / self.resolution[0], slot.dy / self.resolution[1] else: dx, dy = slot.dx, slot.dy if dx != 0 and dy != 0: t = math.atan2(dx, dy) t += math.pi # in [0, 2pi] range now if t == 0: t = 0.01 else: t = t * 180.0 / math.pi directions = ["↖↑", "↖←", "↙←", "↙↓", "↓↘", "→↘", "→↗", "↑↗"] direction = directions[int(t / 45)] elif dy == 0: if dx < 0: direction = "←←" else: direction = "→→" else: if dy < 0: direction = "↑↑" else: direction = "↓↓" color = "" reset = "" if not self.is_absolute: if self.ignore_below is not None or self.threshold is not None: dist = math.hypot(dx, dy) if self.ignore_below is not None and dist < self.ignore_below: self.slots.append(" ".center(self.width)) self.filtered = True return if self.threshold is not None and dist >= self.threshold: color = COLOR_RED reset = COLOR_RESET if isinstance(dx, int) and isinstance(dy, int): string = "{} {}{:+4d}/{:+4d}{}".format( direction, color, dx, dy, reset ) else: string = "{} {}{:+3.2f}/{:+03.2f}{}".format( direction, color, dx, dy, reset ) else: x, y = slot.x, slot.y string = "{} {}{:4d}/{:4d}{}".format(direction, color, x, y, reset) self.have_data = True self.slots.append(string.ljust(self.width + len(color) + len(reset))) class SlotState: NONE = 0 BEGIN = 1 UPDATE = 2 END = 3 class Slot: state = SlotState.NONE x = 0 y = 0 dx = 0 dy = 0 used = False dirty = False def __init__(self, index): self.index = index 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" ) parser.add_argument( "--threshold", type=float, default=None, help="Mark any delta above this threshold", ) parser.add_argument( "--ignore-below", type=float, default=None, help="Ignore any delta below this threshold", ) 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(i) for i in range(0, nslots)] slots[0].used = True 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) if args.use_st: print("Warning: slot coordinates on FINGER/DOUBLETAP change may be incorrect") slots[0].used = True slot = 0 last_time = None tool_bits = { libevdev.EV_KEY.BTN_TOUCH: 0, libevdev.EV_KEY.BTN_TOOL_DOUBLETAP: 0, libevdev.EV_KEY.BTN_TOOL_TRIPLETAP: 0, libevdev.EV_KEY.BTN_TOOL_QUADTAP: 0, libevdev.EV_KEY.BTN_TOOL_QUINTTAP: 0, } btn_state = { libevdev.EV_KEY.BTN_LEFT: 0, libevdev.EV_KEY.BTN_MIDDLE: 0, libevdev.EV_KEY.BTN_RIGHT: 0, } nskipped_lines = 0 for event in device["events"]: for evdev in event["evdev"]: s = slots[slot] e = libevdev.InputEvent( code=libevdev.evbit(evdev[2], evdev[3]), value=evdev[4], sec=evdev[0], usec=evdev[1], ) if e.code in tool_bits: tool_bits[e.code] = e.value if e.code in btn_state: btn_state[e.code] = e.value 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 ( e.code == libevdev.EV_KEY.BTN_TOOL_FINGER or e.code == 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 e.code == 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 e.code == libevdev.EV_ABS.ABS_X: # If recording started after touch down if s.state == SlotState.NONE: s.state = SlotState.BEGIN s.dx, s.dy = 0, 0 elif s.state == SlotState.UPDATE: s.dx = e.value - s.x s.x = e.value s.dirty = True elif e.code == libevdev.EV_ABS.ABS_Y: # If recording started after touch down if s.state == SlotState.NONE: s.state = SlotState.BEGIN s.dx, s.dy = 0, 0 elif s.state == SlotState.UPDATE: s.dy = e.value - s.y s.y = e.value s.dirty = True else: if e.code == libevdev.EV_ABS.ABS_MT_SLOT: slot = e.value s = slots[slot] s.dirty = True # bcm5974 cycles through slot numbers, so let's say all below # our current slot number was used for sl in slots[: slot + 1]: sl.used = True elif e.code == 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 e.code == libevdev.EV_ABS.ABS_MT_POSITION_X: # If recording started after touch down if s.state == SlotState.NONE: s.state = SlotState.BEGIN s.dx, s.dy = 0, 0 elif s.state == SlotState.UPDATE: s.dx = e.value - s.x s.x = e.value s.dirty = True elif e.code == libevdev.EV_ABS.ABS_MT_POSITION_Y: # If recording started after touch down if s.state == SlotState.NONE: s.state = SlotState.BEGIN s.dx, s.dy = 0, 0 elif s.state == SlotState.UPDATE: s.dy = e.value - s.y s.y = e.value s.dirty = True if e.code == 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 tools = [ (libevdev.EV_KEY.BTN_TOOL_QUINTTAP, "QIN"), (libevdev.EV_KEY.BTN_TOOL_QUADTAP, "QAD"), (libevdev.EV_KEY.BTN_TOOL_TRIPLETAP, "TRI"), (libevdev.EV_KEY.BTN_TOOL_DOUBLETAP, "DBL"), (libevdev.EV_KEY.BTN_TOUCH, "TOU"), ] for bit, string in tools: if tool_bits[bit]: tool_state = string break else: tool_state = " " buttons = [ (libevdev.EV_KEY.BTN_LEFT, "L"), (libevdev.EV_KEY.BTN_MIDDLE, "M"), (libevdev.EV_KEY.BTN_RIGHT, "R"), ] button_state = ( "".join([string for bit, string in buttons if btn_state[bit]]) or "." ) fmt = SlotFormatter( is_absolute=args.use_absolute, resolution=(xres, yres) if args.use_mm else None, threshold=args.threshold, ignore_below=args.ignore_below, ) for sl in [s for s in slots if s.used]: fmt.format_slot(sl) sl.dirty = False sl.dx = 0 sl.dy = 0 if sl.state == SlotState.BEGIN: sl.state = SlotState.UPDATE elif sl.state == SlotState.END: sl.state = SlotState.NONE if fmt.have_data: if nskipped_lines > 0: print("") nskipped_lines = 0 print( "{:2d}.{:06d} {:+5d}ms {} {} {}".format( e.sec, e.usec, tdelta, tool_state, button_state, fmt ) ) elif fmt.filtered: nskipped_lines += 1 print( "\r", " " * 21, "... {} below threshold".format(nskipped_lines), flush=True, end="", ) if __name__ == "__main__": try: main(sys.argv) except KeyboardInterrupt: pass