• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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