1#!/usr/bin/env python3 2# -*- coding: utf-8 3# vim: set expandtab shiftwidth=4: 4# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil -*- */ 5# 6# Copyright © 2018 Red Hat, Inc. 7# 8# Permission is hereby granted, free of charge, to any person obtaining a 9# copy of this software and associated documentation files (the 'Software'), 10# to deal in the Software without restriction, including without limitation 11# the rights to use, copy, modify, merge, publish, distribute, sublicense, 12# and/or sell copies of the Software, and to permit persons to whom the 13# Software is furnished to do so, subject to the following conditions: 14# 15# The above copyright notice and this permission notice (including the next 16# paragraph) shall be included in all copies or substantial portions of the 17# Software. 18# 19# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 22# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25# DEALINGS IN THE SOFTWARE. 26# 27# 28# Measures the relative motion between touch events (based on slots) 29# 30# Input is a libinput record yaml file 31 32import argparse 33import math 34import sys 35import yaml 36import libevdev 37 38 39COLOR_RESET = '\x1b[0m' 40COLOR_RED = '\x1b[6;31m' 41 42 43class SlotFormatter(): 44 width = 16 45 46 def __init__(self, is_absolute=False, resolution=None, 47 threshold=None, ignore_below=None): 48 self.threshold = threshold 49 self.ignore_below = ignore_below 50 self.resolution = resolution 51 self.is_absolute = is_absolute 52 self.slots = [] 53 self.have_data = False 54 self.filtered = False 55 56 def __str__(self): 57 return ' | '.join(self.slots) 58 59 def format_slot(self, slot): 60 if slot.state == SlotState.BEGIN: 61 self.slots.append('+++++++'.center(self.width)) 62 self.have_data = True 63 elif slot.state == SlotState.END: 64 self.slots.append('-------'.center(self.width)) 65 self.have_data = True 66 elif slot.state == SlotState.NONE: 67 self.slots.append(('*' * (self.width - 2)).center(self.width)) 68 elif not slot.dirty: 69 self.slots.append(' '.center(self.width)) 70 else: 71 if self.resolution is not None: 72 dx, dy = slot.dx / self.resolution[0], slot.dy / self.resolution[1] 73 else: 74 dx, dy = slot.dx, slot.dy 75 if dx != 0 and dy != 0: 76 t = math.atan2(dx, dy) 77 t += math.pi # in [0, 2pi] range now 78 79 if t == 0: 80 t = 0.01 81 else: 82 t = t * 180.0 / math.pi 83 84 directions = ['↖↑', '↖←', '↙←', '↙↓', '↓↘', '→↘', '→↗', '↑↗'] 85 direction = directions[int(t / 45)] 86 elif dy == 0: 87 if dx < 0: 88 direction = '←←' 89 else: 90 direction = '→→' 91 else: 92 if dy < 0: 93 direction = '↑↑' 94 else: 95 direction = '↓↓' 96 97 color = '' 98 reset = '' 99 if not self.is_absolute: 100 if self.ignore_below is not None or self.threshold is not None: 101 dist = math.hypot(dx, dy) 102 if self.ignore_below is not None and dist < self.ignore_below: 103 self.slots.append(' '.center(self.width)) 104 self.filtered = True 105 return 106 if self.threshold is not None and dist >= self.threshold: 107 color = COLOR_RED 108 reset = COLOR_RESET 109 if isinstance(dx, int) and isinstance(dy, int): 110 string = "{} {}{:+4d}/{:+4d}{}".format(direction, color, dx, dy, reset) 111 else: 112 string = "{} {}{:+3.2f}/{:+03.2f}{}".format(direction, color, dx, dy, reset) 113 else: 114 x, y = slot.x, slot.y 115 string = "{} {}{:4d}/{:4d}{}".format(direction, color, x, y, reset) 116 self.have_data = True 117 self.slots.append(string.ljust(self.width + len(color) + len(reset))) 118 119 120class SlotState: 121 NONE = 0 122 BEGIN = 1 123 UPDATE = 2 124 END = 3 125 126 127class Slot: 128 state = SlotState.NONE 129 x = 0 130 y = 0 131 dx = 0 132 dy = 0 133 used = False 134 dirty = False 135 136 def __init__(self, index): 137 self.index = index 138 139 140def main(argv): 141 global COLOR_RESET 142 global COLOR_RED 143 144 slots = [] 145 xres, yres = 1, 1 146 147 parser = argparse.ArgumentParser(description="Measure delta between event frames for each slot") 148 parser.add_argument("--use-mm", action='store_true', help="Use mm instead of device deltas") 149 parser.add_argument("--use-st", action='store_true', help="Use ABS_X/ABS_Y instead of ABS_MT_POSITION_X/Y") 150 parser.add_argument("--use-absolute", action='store_true', help="Use absolute coordinates, not deltas") 151 parser.add_argument("path", metavar="recording", 152 nargs=1, help="Path to libinput-record YAML file") 153 parser.add_argument("--threshold", type=float, default=None, help="Mark any delta above this treshold") 154 parser.add_argument("--ignore-below", type=float, default=None, help="Ignore any delta below this theshold") 155 args = parser.parse_args() 156 157 if not sys.stdout.isatty(): 158 COLOR_RESET = '' 159 COLOR_RED = '' 160 161 yml = yaml.safe_load(open(args.path[0])) 162 device = yml['devices'][0] 163 absinfo = device['evdev']['absinfo'] 164 try: 165 nslots = absinfo[libevdev.EV_ABS.ABS_MT_SLOT.value][1] + 1 166 except KeyError: 167 args.use_st = True 168 169 if args.use_st: 170 nslots = 1 171 172 slots = [Slot(i) for i in range(0, nslots)] 173 slots[0].used = True 174 175 if args.use_mm: 176 xres = 1.0 * absinfo[libevdev.EV_ABS.ABS_X.value][4] 177 yres = 1.0 * absinfo[libevdev.EV_ABS.ABS_Y.value][4] 178 if not xres or not yres: 179 print("Error: device doesn't have a resolution, cannot use mm") 180 sys.exit(1) 181 182 if args.use_st: 183 print("Warning: slot coordinates on FINGER/DOUBLETAP change may be incorrect") 184 slots[0].used = True 185 186 slot = 0 187 last_time = None 188 tool_bits = { 189 libevdev.EV_KEY.BTN_TOUCH: 0, 190 libevdev.EV_KEY.BTN_TOOL_DOUBLETAP: 0, 191 libevdev.EV_KEY.BTN_TOOL_TRIPLETAP: 0, 192 libevdev.EV_KEY.BTN_TOOL_QUADTAP: 0, 193 libevdev.EV_KEY.BTN_TOOL_QUINTTAP: 0, 194 } 195 196 nskipped_lines = 0 197 198 for event in device['events']: 199 for evdev in event['evdev']: 200 s = slots[slot] 201 e = libevdev.InputEvent(code=libevdev.evbit(evdev[2], evdev[3]), 202 value=evdev[4], sec=evdev[0], usec=evdev[1]) 203 204 if e.code in tool_bits: 205 tool_bits[e.code] = e.value 206 207 if args.use_st: 208 # Note: this relies on the EV_KEY events to come in before the 209 # x/y events, otherwise the last/first event in each slot will 210 # be wrong. 211 if (e.code == libevdev.EV_KEY.BTN_TOOL_FINGER or 212 e.code == libevdev.EV_KEY.BTN_TOOL_PEN): 213 slot = 0 214 s = slots[slot] 215 s.dirty = True 216 if e.value: 217 s.state = SlotState.BEGIN 218 else: 219 s.state = SlotState.END 220 elif e.code == libevdev.EV_KEY.BTN_TOOL_DOUBLETAP: 221 if len(slots) > 1: 222 slot = 1 223 s = slots[slot] 224 s.dirty = True 225 if e.value: 226 s.state = SlotState.BEGIN 227 else: 228 s.state = SlotState.END 229 elif e.code == libevdev.EV_ABS.ABS_X: 230 # If recording started after touch down 231 if s.state == SlotState.NONE: 232 s.state = SlotState.BEGIN 233 s.dx, s.dy = 0, 0 234 elif s.state == SlotState.UPDATE: 235 s.dx = e.value - s.x 236 s.x = e.value 237 s.dirty = True 238 elif e.code == libevdev.EV_ABS.ABS_Y: 239 # If recording started after touch down 240 if s.state == SlotState.NONE: 241 s.state = SlotState.BEGIN 242 s.dx, s.dy = 0, 0 243 elif s.state == SlotState.UPDATE: 244 s.dy = e.value - s.y 245 s.y = e.value 246 s.dirty = True 247 else: 248 if e.code == libevdev.EV_ABS.ABS_MT_SLOT: 249 slot = e.value 250 s = slots[slot] 251 s.dirty = True 252 # bcm5974 cycles through slot numbers, so let's say all below 253 # our current slot number was used 254 for sl in slots[:slot + 1]: 255 sl.used = True 256 elif e.code == libevdev.EV_ABS.ABS_MT_TRACKING_ID: 257 if e.value == -1: 258 s.state = SlotState.END 259 else: 260 s.state = SlotState.BEGIN 261 s.dx = 0 262 s.dy = 0 263 s.dirty = True 264 elif e.code == libevdev.EV_ABS.ABS_MT_POSITION_X: 265 # If recording started after touch down 266 if s.state == SlotState.NONE: 267 s.state = SlotState.BEGIN 268 s.dx, s.dy = 0, 0 269 elif s.state == SlotState.UPDATE: 270 s.dx = e.value - s.x 271 s.x = e.value 272 s.dirty = True 273 elif e.code == libevdev.EV_ABS.ABS_MT_POSITION_Y: 274 # If recording started after touch down 275 if s.state == SlotState.NONE: 276 s.state = SlotState.BEGIN 277 s.dx, s.dy = 0, 0 278 elif s.state == SlotState.UPDATE: 279 s.dy = e.value - s.y 280 s.y = e.value 281 s.dirty = True 282 283 if e.code == libevdev.EV_SYN.SYN_REPORT: 284 if last_time is None: 285 last_time = e.sec * 1000000 + e.usec 286 tdelta = 0 287 else: 288 t = e.sec * 1000000 + e.usec 289 tdelta = int((t - last_time) / 1000) # ms 290 last_time = t 291 292 tools = [ 293 (libevdev.EV_KEY.BTN_TOOL_QUINTTAP, 'QIN'), 294 (libevdev.EV_KEY.BTN_TOOL_QUADTAP, 'QAD'), 295 (libevdev.EV_KEY.BTN_TOOL_TRIPLETAP, 'TRI'), 296 (libevdev.EV_KEY.BTN_TOOL_DOUBLETAP, 'DBL'), 297 (libevdev.EV_KEY.BTN_TOUCH, 'TOU'), 298 ] 299 300 for bit, string in tools: 301 if tool_bits[bit]: 302 tool_state = string 303 break 304 else: 305 tool_state = ' ' 306 307 fmt = SlotFormatter(is_absolute=args.use_absolute, 308 resolution=(xres, yres) if args.use_mm else None, 309 threshold=args.threshold, 310 ignore_below=args.ignore_below) 311 for sl in [s for s in slots if s.used]: 312 fmt.format_slot(sl) 313 314 sl.dirty = False 315 sl.dx = 0 316 sl.dy = 0 317 if sl.state == SlotState.BEGIN: 318 sl.state = SlotState.UPDATE 319 elif sl.state == SlotState.END: 320 sl.state = SlotState.NONE 321 322 if fmt.have_data: 323 if nskipped_lines > 0: 324 print("") 325 nskipped_lines = 0 326 print("{:2d}.{:06d} {:+5d}ms {}: {}".format(e.sec, e.usec, tdelta, tool_state, fmt)) 327 elif fmt.filtered: 328 nskipped_lines += 1 329 print("\r", " " * 21, "... {} below threshold".format(nskipped_lines), flush=True, end='') 330 331 332if __name__ == '__main__': 333 main(sys.argv) 334