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__( 47 self, is_absolute=False, resolution=None, threshold=None, ignore_below=None 48 ): 49 self.threshold = threshold 50 self.ignore_below = ignore_below 51 self.resolution = resolution 52 self.is_absolute = is_absolute 53 self.slots = [] 54 self.have_data = False 55 self.filtered = False 56 57 def __str__(self): 58 return " | ".join(self.slots) 59 60 def format_slot(self, slot): 61 if slot.state == SlotState.BEGIN: 62 self.slots.append("+++++++".center(self.width)) 63 self.have_data = True 64 elif slot.state == SlotState.END: 65 self.slots.append("-------".center(self.width)) 66 self.have_data = True 67 elif slot.state == SlotState.NONE: 68 self.slots.append(("*" * (self.width - 2)).center(self.width)) 69 elif not slot.dirty: 70 self.slots.append(" ".center(self.width)) 71 else: 72 if self.resolution is not None: 73 dx, dy = slot.dx / self.resolution[0], slot.dy / self.resolution[1] 74 else: 75 dx, dy = slot.dx, slot.dy 76 if dx != 0 and dy != 0: 77 t = math.atan2(dx, dy) 78 t += math.pi # in [0, 2pi] range now 79 80 if t == 0: 81 t = 0.01 82 else: 83 t = t * 180.0 / math.pi 84 85 directions = ["↖↑", "↖←", "↙←", "↙↓", "↓↘", "→↘", "→↗", "↑↗"] 86 direction = directions[int(t / 45)] 87 elif dy == 0: 88 if dx < 0: 89 direction = "←←" 90 else: 91 direction = "→→" 92 else: 93 if dy < 0: 94 direction = "↑↑" 95 else: 96 direction = "↓↓" 97 98 color = "" 99 reset = "" 100 if not self.is_absolute: 101 if self.ignore_below is not None or self.threshold is not None: 102 dist = math.hypot(dx, dy) 103 if self.ignore_below is not None and dist < self.ignore_below: 104 self.slots.append(" ".center(self.width)) 105 self.filtered = True 106 return 107 if self.threshold is not None and dist >= self.threshold: 108 color = COLOR_RED 109 reset = COLOR_RESET 110 if isinstance(dx, int) and isinstance(dy, int): 111 string = "{} {}{:+4d}/{:+4d}{}".format( 112 direction, color, dx, dy, reset 113 ) 114 else: 115 string = "{} {}{:+3.2f}/{:+03.2f}{}".format( 116 direction, color, dx, dy, reset 117 ) 118 else: 119 x, y = slot.x, slot.y 120 string = "{} {}{:4d}/{:4d}{}".format(direction, color, x, y, reset) 121 self.have_data = True 122 self.slots.append(string.ljust(self.width + len(color) + len(reset))) 123 124 125class SlotState: 126 NONE = 0 127 BEGIN = 1 128 UPDATE = 2 129 END = 3 130 131 132class Slot: 133 state = SlotState.NONE 134 x = 0 135 y = 0 136 dx = 0 137 dy = 0 138 used = False 139 dirty = False 140 141 def __init__(self, index): 142 self.index = index 143 144 145def main(argv): 146 global COLOR_RESET 147 global COLOR_RED 148 149 slots = [] 150 xres, yres = 1, 1 151 152 parser = argparse.ArgumentParser( 153 description="Measure delta between event frames for each slot" 154 ) 155 parser.add_argument( 156 "--use-mm", action="store_true", help="Use mm instead of device deltas" 157 ) 158 parser.add_argument( 159 "--use-st", 160 action="store_true", 161 help="Use ABS_X/ABS_Y instead of ABS_MT_POSITION_X/Y", 162 ) 163 parser.add_argument( 164 "--use-absolute", 165 action="store_true", 166 help="Use absolute coordinates, not deltas", 167 ) 168 parser.add_argument( 169 "path", metavar="recording", nargs=1, help="Path to libinput-record YAML file" 170 ) 171 parser.add_argument( 172 "--threshold", 173 type=float, 174 default=None, 175 help="Mark any delta above this threshold", 176 ) 177 parser.add_argument( 178 "--ignore-below", 179 type=float, 180 default=None, 181 help="Ignore any delta below this threshold", 182 ) 183 args = parser.parse_args() 184 185 if not sys.stdout.isatty(): 186 COLOR_RESET = "" 187 COLOR_RED = "" 188 189 yml = yaml.safe_load(open(args.path[0])) 190 device = yml["devices"][0] 191 absinfo = device["evdev"]["absinfo"] 192 try: 193 nslots = absinfo[libevdev.EV_ABS.ABS_MT_SLOT.value][1] + 1 194 except KeyError: 195 args.use_st = True 196 197 if args.use_st: 198 nslots = 1 199 200 slots = [Slot(i) for i in range(0, nslots)] 201 slots[0].used = True 202 203 if args.use_mm: 204 xres = 1.0 * absinfo[libevdev.EV_ABS.ABS_X.value][4] 205 yres = 1.0 * absinfo[libevdev.EV_ABS.ABS_Y.value][4] 206 if not xres or not yres: 207 print("Error: device doesn't have a resolution, cannot use mm") 208 sys.exit(1) 209 210 if args.use_st: 211 print("Warning: slot coordinates on FINGER/DOUBLETAP change may be incorrect") 212 slots[0].used = True 213 214 slot = 0 215 last_time = None 216 tool_bits = { 217 libevdev.EV_KEY.BTN_TOUCH: 0, 218 libevdev.EV_KEY.BTN_TOOL_DOUBLETAP: 0, 219 libevdev.EV_KEY.BTN_TOOL_TRIPLETAP: 0, 220 libevdev.EV_KEY.BTN_TOOL_QUADTAP: 0, 221 libevdev.EV_KEY.BTN_TOOL_QUINTTAP: 0, 222 } 223 btn_state = { 224 libevdev.EV_KEY.BTN_LEFT: 0, 225 libevdev.EV_KEY.BTN_MIDDLE: 0, 226 libevdev.EV_KEY.BTN_RIGHT: 0, 227 } 228 229 nskipped_lines = 0 230 231 for event in device["events"]: 232 for evdev in event["evdev"]: 233 s = slots[slot] 234 e = libevdev.InputEvent( 235 code=libevdev.evbit(evdev[2], evdev[3]), 236 value=evdev[4], 237 sec=evdev[0], 238 usec=evdev[1], 239 ) 240 241 if e.code in tool_bits: 242 tool_bits[e.code] = e.value 243 if e.code in btn_state: 244 btn_state[e.code] = e.value 245 246 if args.use_st: 247 # Note: this relies on the EV_KEY events to come in before the 248 # x/y events, otherwise the last/first event in each slot will 249 # be wrong. 250 if ( 251 e.code == libevdev.EV_KEY.BTN_TOOL_FINGER 252 or e.code == libevdev.EV_KEY.BTN_TOOL_PEN 253 ): 254 slot = 0 255 s = slots[slot] 256 s.dirty = True 257 if e.value: 258 s.state = SlotState.BEGIN 259 else: 260 s.state = SlotState.END 261 elif e.code == libevdev.EV_KEY.BTN_TOOL_DOUBLETAP: 262 if len(slots) > 1: 263 slot = 1 264 s = slots[slot] 265 s.dirty = True 266 if e.value: 267 s.state = SlotState.BEGIN 268 else: 269 s.state = SlotState.END 270 elif e.code == libevdev.EV_ABS.ABS_X: 271 # If recording started after touch down 272 if s.state == SlotState.NONE: 273 s.state = SlotState.BEGIN 274 s.dx, s.dy = 0, 0 275 elif s.state == SlotState.UPDATE: 276 s.dx = e.value - s.x 277 s.x = e.value 278 s.dirty = True 279 elif e.code == libevdev.EV_ABS.ABS_Y: 280 # If recording started after touch down 281 if s.state == SlotState.NONE: 282 s.state = SlotState.BEGIN 283 s.dx, s.dy = 0, 0 284 elif s.state == SlotState.UPDATE: 285 s.dy = e.value - s.y 286 s.y = e.value 287 s.dirty = True 288 else: 289 if e.code == libevdev.EV_ABS.ABS_MT_SLOT: 290 slot = e.value 291 s = slots[slot] 292 s.dirty = True 293 # bcm5974 cycles through slot numbers, so let's say all below 294 # our current slot number was used 295 for sl in slots[: slot + 1]: 296 sl.used = True 297 elif e.code == libevdev.EV_ABS.ABS_MT_TRACKING_ID: 298 if e.value == -1: 299 s.state = SlotState.END 300 else: 301 s.state = SlotState.BEGIN 302 s.dx = 0 303 s.dy = 0 304 s.dirty = True 305 elif e.code == libevdev.EV_ABS.ABS_MT_POSITION_X: 306 # If recording started after touch down 307 if s.state == SlotState.NONE: 308 s.state = SlotState.BEGIN 309 s.dx, s.dy = 0, 0 310 elif s.state == SlotState.UPDATE: 311 s.dx = e.value - s.x 312 s.x = e.value 313 s.dirty = True 314 elif e.code == libevdev.EV_ABS.ABS_MT_POSITION_Y: 315 # If recording started after touch down 316 if s.state == SlotState.NONE: 317 s.state = SlotState.BEGIN 318 s.dx, s.dy = 0, 0 319 elif s.state == SlotState.UPDATE: 320 s.dy = e.value - s.y 321 s.y = e.value 322 s.dirty = True 323 324 if e.code == libevdev.EV_SYN.SYN_REPORT: 325 if last_time is None: 326 last_time = e.sec * 1000000 + e.usec 327 tdelta = 0 328 else: 329 t = e.sec * 1000000 + e.usec 330 tdelta = int((t - last_time) / 1000) # ms 331 last_time = t 332 333 tools = [ 334 (libevdev.EV_KEY.BTN_TOOL_QUINTTAP, "QIN"), 335 (libevdev.EV_KEY.BTN_TOOL_QUADTAP, "QAD"), 336 (libevdev.EV_KEY.BTN_TOOL_TRIPLETAP, "TRI"), 337 (libevdev.EV_KEY.BTN_TOOL_DOUBLETAP, "DBL"), 338 (libevdev.EV_KEY.BTN_TOUCH, "TOU"), 339 ] 340 341 for bit, string in tools: 342 if tool_bits[bit]: 343 tool_state = string 344 break 345 else: 346 tool_state = " " 347 348 buttons = [ 349 (libevdev.EV_KEY.BTN_LEFT, "L"), 350 (libevdev.EV_KEY.BTN_MIDDLE, "M"), 351 (libevdev.EV_KEY.BTN_RIGHT, "R"), 352 ] 353 354 button_state = ( 355 "".join([string for bit, string in buttons if btn_state[bit]]) 356 or "." 357 ) 358 359 fmt = SlotFormatter( 360 is_absolute=args.use_absolute, 361 resolution=(xres, yres) if args.use_mm else None, 362 threshold=args.threshold, 363 ignore_below=args.ignore_below, 364 ) 365 for sl in [s for s in slots if s.used]: 366 fmt.format_slot(sl) 367 368 sl.dirty = False 369 sl.dx = 0 370 sl.dy = 0 371 if sl.state == SlotState.BEGIN: 372 sl.state = SlotState.UPDATE 373 elif sl.state == SlotState.END: 374 sl.state = SlotState.NONE 375 376 if fmt.have_data: 377 if nskipped_lines > 0: 378 print("") 379 nskipped_lines = 0 380 print( 381 "{:2d}.{:06d} {:+5d}ms {} {} {}".format( 382 e.sec, e.usec, tdelta, tool_state, button_state, fmt 383 ) 384 ) 385 elif fmt.filtered: 386 nskipped_lines += 1 387 print( 388 "\r", 389 " " * 21, 390 "... {} below threshold".format(nskipped_lines), 391 flush=True, 392 end="", 393 ) 394 395 396if __name__ == "__main__": 397 try: 398 main(sys.argv) 399 except KeyboardInterrupt: 400 pass 401