Skip to content

Commit

Permalink
Adding TabletTester.
Browse files Browse the repository at this point in the history
  • Loading branch information
wavexx committed Apr 21, 2015
1 parent 7a4d24e commit 3630d81
Show file tree
Hide file tree
Showing 5 changed files with 297 additions and 6 deletions.
1 change: 1 addition & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 2 additions & 0 deletions TabletTester.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
cd %0\..\
start bin\tablettester.py %*
268 changes: 268 additions & 0 deletions bin/tablettester.py
Original file line number Diff line number Diff line change
@@ -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: " + ("<b>TRUE</b>" if self._tracking_state else "false")
msg += "\nDrawing: " + ("<b>TRUE</b>" 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(" ", "&nbsp;")
msg = msg.replace("\n", "<br/>")
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_())
11 changes: 8 additions & 3 deletions bin/visualizer.pyw
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
Expand Down
21 changes: 18 additions & 3 deletions src/lib/DrawingRecorder/Data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 3630d81

Please sign in to comment.