From 3630d81f1c260effc094f02c89d7a2094a824e87 Mon Sep 17 00:00:00 2001 From: Yuri D'Elia Date: Tue, 21 Apr 2015 19:05:40 +0200 Subject: [PATCH] Adding TabletTester. --- README.rst | 1 + TabletTester.bat | 2 + bin/tablettester.py | 268 ++++++++++++++++++++++++++++++++ bin/visualizer.pyw | 11 +- src/lib/DrawingRecorder/Data.py | 21 ++- 5 files changed, 297 insertions(+), 6 deletions(-) create mode 100644 TabletTester.bat create mode 100755 bin/tablettester.py diff --git a/README.rst b/README.rst index 36d1220..23b069d 100644 --- a/README.rst +++ b/README.rst @@ -120,6 +120,7 @@ Version changes 1.8: * DrawingRecorder file format 1.5. +* Added ``TabletTester`` to debug tablet-related issues. * DrawingRecorder now also includes lower-level packet timestamps/serials on Windows which are better suited for analysis. diff --git a/TabletTester.bat b/TabletTester.bat new file mode 100644 index 0000000..76ad0b1 --- /dev/null +++ b/TabletTester.bat @@ -0,0 +1,2 @@ +cd %0\..\ +start bin\tablettester.py %* diff --git a/bin/tablettester.py b/bin/tablettester.py new file mode 100755 index 0000000..d2d4c6e --- /dev/null +++ b/bin/tablettester.py @@ -0,0 +1,268 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Tablet tester""" + +from __future__ import print_function + +# setup path +import os, sys +sys.path.append(os.path.abspath(os.path.join(__file__, '../../src/lib'))) + +# local modules +from DrawingRecorder import Consts +import HiResTime +import QExtTabletWindow + +# system modules +import argparse +import datetime +import os +from PyQt4 import QtCore, QtGui + + +class MainWindow(QExtTabletWindow.QExtTabletWindow): + def __init__(self): + super(MainWindow, self).__init__() + + # scene setup + self._scene = QtGui.QGraphicsScene(self) + self._scene.setBackgroundBrush(QtGui.QBrush(Consts.FILL_COLOR)) + self._view = QtGui.QGraphicsView(self._scene) + self._view.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.TextAntialiasing) + self._view.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self._view.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self._view.setCacheMode(QtGui.QGraphicsView.CacheNone) + self._view.setOptimizationFlag(QtGui.QGraphicsView.DontSavePainterState) + self._view.setInteractive(False) + self._view.setFrameStyle(0) + self.setCursor(QtCore.Qt.BlankCursor) + self.setCentralWidget(self._view) + self.setWindowModality(QtCore.Qt.ApplicationModal) + self._screen_group = QtGui.QGraphicsItemGroup(scene=self._scene) + + # cursor + tmp = QtGui.QPainterPath() + tmp.moveTo(0, -Consts.CURSOR_LEN) + tmp.lineTo(0, Consts.CURSOR_LEN) + tmp.moveTo(-Consts.CURSOR_LEN, 0) + tmp.lineTo(Consts.CURSOR_LEN, 0) + tmp.moveTo(-Consts.CURSOR_LEN / 4, -Consts.CURSOR_LEN / 4) + tmp.lineTo(Consts.CURSOR_LEN / 4, Consts.CURSOR_LEN / 4) + tmp.moveTo(Consts.CURSOR_LEN / 4, -Consts.CURSOR_LEN / 4) + tmp.lineTo(-Consts.CURSOR_LEN / 4, Consts.CURSOR_LEN / 4) + self._cursor = QtGui.QGraphicsPathItem(tmp, self._screen_group) + + # main text + self._main_text = QtGui.QGraphicsSimpleTextItem(self._screen_group) + self._main_text.setBrush(QtGui.QBrush(QtCore.Qt.gray)) + self._main_text.setText("TABLET TESTER") + font = self._main_text.font() + font.setPointSize(Consts.MAIN_TEXT_SIZE) + font.setBold(True) + self._main_text.setFont(font) + + # sub text + self._sub_text = QtGui.QGraphicsTextItem(self._screen_group) + self._sub_text.setDefaultTextColor(QtCore.Qt.gray) + self._sub_text.setHtml("Waiting for first event ...") + font = self._sub_text.font() + font.setFamily("Consolas") + font.setPointSize(Consts.NORM_TEXT_SIZE) + self._sub_text.setFont(font) + + # initial state + self.first_ev = None + self.last_ev = None + self.dev_off = None + self.ev_drops = None + self.os_hr_max = 0. + self.ev_rate = float("nan") + self._drawing_state = False + self._tracking_state = False + self._cursor.hide() + self._cursor.setPen(QtGui.QPen(Consts.CURSOR_INACTIVE)) + self.showFullScreen() + self.startTimer(Consts.REFRESH_DELAY) + + + def rate_init(self, now): + self.rate_buckets = [0] * 61 + self.rate_last = now + self.rate_first = now + self.rate_status = 'waiting' + + + def rate_update(self, now): + l_sec = int((self.rate_last - self.rate_first).total_seconds()) + n_sec = int((now - self.rate_first).total_seconds()) + bucket = n_sec % len(self.rate_buckets) + + if n_sec != l_sec: + # clear new bucket + self.rate_buckets[bucket] = 0 + + if l_sec >= (len(self.rate_buckets) - 1): + # full + acc = 0 + for b in range(len(self.rate_buckets)): + if b != bucket: + acc += self.rate_buckets[b] + self.ev_rate = float(acc) / (len(self.rate_buckets) - 1) + self.rate_status = '{}s avg'.format((len(self.rate_buckets) - 1)) + elif l_sec > 1: + # partial + acc = sum([self.rate_buckets[s % (len(self.rate_buckets) - 1)] for s in range(1, l_sec)]) + self.ev_rate = float(acc) / (l_sec - 1) + self.rate_status = '{}s avg'.format(l_sec) + + self.rate_buckets[bucket] += 1 + self.rate_last = now + + + def resizeEvent(self, ev): + # resize to match current screen resolution + size = ev.size() + self._view.setSceneRect(0, 0, size.width(), size.height()) + + # layout text + tmp = self._view.sceneRect().topLeft() + self._main_text.setPos(tmp) + tmp.setY(tmp.y() + self._main_text.boundingRect().height()) + self._sub_text.setPos(tmp) + + + def extTabletEvent(self, ev): + # only consider pen events and only when visible + if self.isVisible() is False: + return + + # update drawing state + if ev.subtype == QtCore.QEvent.TabletPress: + self._cursor.setPen(QtGui.QPen(Consts.CURSOR_ACTIVE)) + self._tracking_state = True + self._drawing_state = True + elif ev.subtype == QtCore.QEvent.TabletRelease: + self._cursor.setPen(QtGui.QPen(Consts.CURSOR_INACTIVE)) + self._tracking_state = True + self._drawing_state = False + elif ev.subtype == QtCore.QEvent.TabletEnterProximity: + self._tracking_state = True + self._drawing_state = False + elif ev.subtype == QtCore.QEvent.TabletLeaveProximity: + self._tracking_state = False + self._drawing_state = False + + # also update the cursor position + self._cursor.setPos(ev.position) + self._cursor.setVisible(self._tracking_state) + + # refresh data + if self.first_ev is None: + self.first_ev = ev + self.rate_init(ev.os_stamp) + if self.dev_off is None and ev.dev_stamp is not None: + self.dev_off = (ev.dev_stamp, HiResTime.now()) + self.ev_drops = 0 + if self.last_ev is not None and self.last_ev.dev_serial is not None and ev.dev_serial is not None: + # take into account wrap-arounds + if self.last_ev.dev_serial < ev.dev_serial: + self.ev_drops += (ev.dev_serial - self.last_ev.dev_serial) - 1 + self.last_ev = ev + self.rate_update(ev.os_stamp) + self.update() + + + def timerEvent(self, ev): + self.update() + + + def update(self): + if self.first_ev is None: + return + + # basic state + msg = "Tracking: " + ("TRUE" if self._tracking_state else "false") + msg += "\nDrawing: " + ("TRUE" if self._drawing_state else "false") + + # data + msg += "\nPressure: {:8.3f}".format(self.last_ev.pressure) + msg += "\nPosition: {:8.3f} {:8.3f}".format(self.last_ev.position.x(), self.last_ev.position.y()) + msg += "\nTilt: {:8.3f} {:8.3f}".format(*self.last_ev.tilt) + msg += "\n" + + # timing + os_time = datetime.datetime.now() + hr_time = HiResTime.now() + os_hr_delta = (os_time - hr_time).total_seconds() * 1000. + self.os_hr_max = max(self.os_hr_max, abs(os_hr_delta)) + + dev_time = None + hr_dv_delta = float("nan") + hr_dv_dsc = '' + if self.dev_off is not None and self.last_ev.dev_stamp is not None: + off = self.last_ev.dev_stamp - self.dev_off[0] + dev_time = self.dev_off[1] + datetime.timedelta(seconds=(off / 1000.)) + hr_dv_delta = (hr_time - dev_time).total_seconds() * 1000. + if abs(hr_dv_delta) >= 25.: + hr_dv_pc = abs(hr_dv_delta) * 100. / off + hr_dv_dsc = " [{:.3f}% DV]".format(hr_dv_pc) + + msg += "\nOS Time: " + str(os_time) + msg += "\nHR Time: " + str(hr_time) + msg += "\nEV Time: " + str(self.last_ev.os_stamp) + msg += "\nDV Time: " + str(dev_time) + msg += "\n" + + # clock deltas + msg += "\nOS-HR delta ms: {:8.3f} [{:.3} max]".format(os_hr_delta, self.os_hr_max) + msg += "\nHR-DV delta ms: {:8.3f}{}".format(hr_dv_delta, hr_dv_dsc) + msg += "\nEV Rate HZ: {:8.3f} [{}]".format(self.ev_rate, self.rate_status) + msg += "\n" + + # device stamp/serial/rate + msg += "\nDV Stamp: " + str(self.last_ev.dev_stamp) + msg += "\nDV Serial: " + str(self.last_ev.dev_serial) + msg += "\nDV Drop: " + str(self.ev_drops) + + # update text + msg = msg.replace(" ", " ") + msg = msg.replace("\n", "
") + self._sub_text.setHtml(msg) + + + def keyEvent(self, ev): + if ev.key() == QtCore.Qt.Key_Escape: + self.close() + + + def event(self, ev): + if ev.type() == QExtTabletWindow.EVENT_TYPE: + self.extTabletEvent(ev) + ev.accept() + return True + elif ev.type() == QtCore.QEvent.KeyPress and not ev.isAutoRepeat(): + self.keyEvent(ev) + ev.accept() + return True + return super(MainWindow, self).event(ev) + + +# main application +class Application(QtGui.QApplication): + def __init__(self, args): + super(Application, self).__init__(args) + + # command-line flags + ap = argparse.ArgumentParser(description='Tablet tester') + ap.add_argument('dir', nargs='?', help='project directory') + args = ap.parse_args(map(unicode, args[1:])) + + # initialize + self.main_window = MainWindow() + self.main_window.show() + + +# main module +if __name__ == '__main__': + app = Application(sys.argv) + sys.exit(app.exec_()) diff --git a/bin/visualizer.pyw b/bin/visualizer.pyw index c6d874c..c1030c3 100755 --- a/bin/visualizer.pyw +++ b/bin/visualizer.pyw @@ -48,9 +48,11 @@ def drawArrow(group, pen, start, end, angle=10., ext=Consts.LIFT_RADIUS): def speedAtPoint(events, stamp, window): w_events = [] for w_event in events: - if abs((w_event.stamp - stamp).total_seconds()) < window: + if w_event.dev_stamp is None: + continue + if abs((w_event.dev_stamp - stamp) / 1000.) < window: w_events.append(w_event) - w_secs = (w_events[-1].stamp - w_events[0].stamp).total_seconds() + w_secs = (w_events[-1].dev_stamp - w_events[0].dev_stamp) / 1000. if not w_secs: return -1 @@ -65,7 +67,10 @@ def speedAtPoint(events, stamp, window): def sampleSpeed(events, window): for event in events: - event.speed = speedAtPoint(events, event.stamp, 0.05) + if event.dev_stamp is None: + event.speed = 0 + else: + event.speed = speedAtPoint(events, event.dev_stamp, 0.05) def ctrb(x, a, b, ctrl, bias): i = (x - a) / (b - a) + bias - 1. diff --git a/src/lib/DrawingRecorder/Data.py b/src/lib/DrawingRecorder/Data.py index 005ad91..67d9376 100644 --- a/src/lib/DrawingRecorder/Data.py +++ b/src/lib/DrawingRecorder/Data.py @@ -335,10 +335,25 @@ def save(cls, record, path): @classmethod def save_text(cls, record, path): - fd = Tab.TabWriter(path, ["TIME", "X", "Y", "Z", "W", "T"]) - start = record.recording.events[0].stamp + fd = Tab.TabWriter(path, ["TIME", "T2", "TYPE", "X", "Y", "Z", "W", "T"]) + os_start = record.recording.events[0].stamp + + # look for the first valid device timestamp + dev_off = None + for event in record.recording.events: + if event.dev_stamp is not None: + off = (event.stamp - os_start).total_seconds() * 1000. + dev_off = event.dev_stamp - off + break + for event in record.recording.events: - fd.write({'TIME': (event.stamp - start).total_seconds() * 1000., + if dev_off is None or event.dev_stamp is None: + time = (event.stamp - os_start).total_seconds() * 1000. + else: + time = event.dev_stamp - dev_off + fd.write({'TIME': time, + 'T2': (event.stamp - os_start).total_seconds() * 1000., + 'TYPE': EVENT_MAP[event.typ], 'X': event.coords_drawing[0], 'Y': event.coords_drawing[1], 'Z': event.pressure,