• 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__(
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