Merge lp:~jpakkane/grail/touchviewer into lp:grail

Proposed by Jussi Pakkanen on 2011-08-17
Status: Rejected
Rejected by: Chase Douglas on 2011-09-02
Proposed branch: lp:~jpakkane/grail/touchviewer
Merge into: lp:grail
Diff against target: 501 lines (+487/-0)
3 files modified
test/eventparser.py (+240/-0)
test/eventviewer.glade (+57/-0)
test/eventviewer.py (+190/-0)
To merge this branch: bzr merge lp:~jpakkane/grail/touchviewer
Reviewer Review Type Date Requested Status
Chase Douglas (community) 2011-08-17 Needs Fixing on 2011-08-30
Review via email: mp+71893@code.launchpad.net

Description of the change

This branch adds a standalone touch trace file visualizer.

It only draws the points by directly parsing the evemu data file. It uses the raw data so that you can inspect it rather than what gets spit out by the uTouch stack. It does not print Grail's output for each frame. The main reason being that there are no Python bindings for Grail.

It does not install upon "make install". I'm not sure that it even needs to, as this is solely a dev tool.

This has been developed in Grail's branch but I guess it could go to evemu as well.

To post a comment you must log in.
Chase Douglas (chasedouglas) wrote :

I would rather see utouch-evemu extended to read directly from the file instead of sending events through uinput. Are there any issues with doing that?

Jussi Pakkanen (jpakkane) wrote :

This app does not use utouch-evemu at all, it parses the data files directly. So no. :)

Chase Douglas (chasedouglas) wrote :

That's the problem: it doesn't use utouch-evemu. We should make utouch-evemu read directly from its file, and then use utouch-evemu to get events. Otherwise, we'll be copying code like this around to every place we want to replay events directly from an evemu recording.

I realize this code is in python and utouch-evemu doesn't have any python bindings, but it seems to me that we should rectify utouch-evemu rather than reinvent it in python here.

review: Needs Fixing
Jussi Pakkanen (jpakkane) wrote :

If we do that, then the tool is displaying touch events _as reported by evemu_. The application currently displays touch events _as they are in the file_. The latter case allows us to use this application to debug issues in evemu. Whether or not something like that is required is, of course, another issue entirely.

Chase Douglas (chasedouglas) wrote :

I guess I'm confused. After looking through the diff, it doesn't appear that there is any interaction with grail here. (I just re-read the proposal description, it's plainly obvious there, my mistake. :)

If I understand correctly, you created this eventviewer to show events that are recorded in an evemu recording, the purpose of which is to verify that evemu recorded the events properly? I was assuming it was a viewer tool with the sole purpose of showing what the data in the recording is.

If we want to verify that utouch-evemu is recording properly, we should have unit tests that check input vs output.

If we want a viewer tool to visually show what the data in a recording is, we should build it on top of utouch-evemu itself. Otherwise, if we need to change the format of recordings, we would need to update this parser instead of letting the evemu library handle it internally.

And this should all be proposed against utouch-evemu instead of utouch-grail :).

Jussi Pakkanen (jpakkane) wrote :

I created a merge proposal to evemu, so this one can be rejected now:

https://code.launchpad.net/~jpakkane/utouch-evemu/touchviewer/+merge/73815

Unmerged revisions

172. By Jussi Pakkanen on 2011-08-12

Merged trunk changes.

171. By Jussi Pakkanen on 2011-07-07

M

170. By Jussi Pakkanen on 2011-07-07

Some security.

169. By Jussi Pakkanen on 2011-06-14

Guard against poking a just deleted slot.

168. By Jussi Pakkanen on 2011-06-10

Better status text.

167. By Jussi Pakkanen on 2011-06-10

Use a toggle button instead of a check button.

166. By Jussi Pakkanen on 2011-06-10

Parse also traces missing headers.

165. By Jussi Pakkanen on 2011-06-10

Give all extra space to canvas.

164. By Jussi Pakkanen on 2011-06-10

Draw source surface canvas.

163. By Jussi Pakkanen on 2011-06-08

Grabbed event viewer from another branch.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'test/eventparser.py'
2--- test/eventparser.py 1970-01-01 00:00:00 +0000
3+++ test/eventparser.py 2011-08-17 14:03:35 +0000
4@@ -0,0 +1,240 @@
5+#!/usr/bin/python -tt
6+#
7+# @file eventparser
8+# @brief a demo/test program to parse multitouch input files
9+#
10+# Copyright (C) 2011 Canonical Ltd
11+#
12+# This program is free software; you can redistribute it and/or modify
13+# it under the terms of the GNU General Public License as published by
14+# the Free Software Foundation; either version 3 of the License, or
15+# (at your option) any later version.
16+#
17+# This program is distributed in the hope that it will be useful,
18+# but WITHOUT ANY WARRANTY; without even the implied warranty of
19+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20+# GNU General Public License for more details.
21+#
22+# You should have received a copy of the GNU General Public License
23+# along with this program. If not, see <http://www.gnu.org/licenses/>.
24+#
25+
26+import sys, copy
27+
28+# Evdev protocol constants
29+
30+SYN_REPORT = 0
31+SYN_CONFIG = 1
32+SYN_MT_REPORT = 2
33+
34+ABS_MT_SLOT = 0x2f # MT slot being modified
35+ABS_MT_TOUCH_MAJOR = 0x30 # Major axis of touching ellipse
36+ABS_MT_TOUCH_MINOR = 0x31 # Minor axis (omit if circular)
37+ABS_MT_WIDTH_MAJOR = 0x32 # Major axis of approaching ellipse
38+ABS_MT_WIDTH_MINOR = 0x33 # Minor axis (omit if circular)
39+ABS_MT_ORIENTATION = 0x34 # Ellipse orientation
40+ABS_MT_POSITION_X = 0x35 # Center X ellipse position
41+ABS_MT_POSITION_Y = 0x36 # Center Y ellipse position
42+ABS_MT_TOOL_TYPE = 0x37 # Type of touching device
43+ABS_MT_BLOB_ID = 0x38 # Group a set of packets as a blob
44+ABS_MT_TRACKING_ID = 0x39 # Unique ID of initiated contact
45+ABS_MT_PRESSURE = 0x3a # Pressure on contact area
46+ABS_MT_DISTANCE = 0x3b # Contact hover distance
47+
48+class RawEvent():
49+ def __init__(self, time, type, code, value):
50+ self.time = time
51+ self.type = type
52+ self.code = code
53+ self.value = value
54+
55+class Absolute():
56+ def __init__(self, minimum, maximum, fuzz, flat):
57+ self.minimum = minimum
58+ self.maximum = maximum
59+ self.fuzz = fuzz
60+ self.flat = flat
61+
62+class Slot():
63+ def __init__(self):
64+ self.tracking_id = -1
65+ self.x = -1
66+ self.y = -1
67+
68+ def set_tracking_id(self, newid):
69+ self.tracking_id = newid
70+
71+ def set_x(self, x):
72+ self.x = x
73+
74+ def set_y(self, y):
75+ self.y = y
76+
77+class State():
78+ def __init__(self, time, slots):
79+ self.time = time
80+ self.slots = slots
81+
82+class StateTracker():
83+
84+ def __init__(self, reader):
85+ self.reader = reader
86+ self.raw_event_id = 0
87+ self.active_slot_num = 0
88+ self.slots = {}
89+ self.slots[self.active_slot_num] = Slot()
90+ self.start_time = self.reader.events[0].time
91+ self.current_time = self.reader.events[0].time
92+
93+
94+ def active_slot(self):
95+ # If a slot has been destroyed and it immediately
96+ # accessed again, we need to create a new slot.
97+ if not self.active_slot_num in self.slots:
98+ self.slots[self.active_slot_num] = Slot()
99+ return self.slots[self.active_slot_num]
100+
101+ def get_next_state(self):
102+ events = self.reader.events
103+ if self.raw_event_id >= len(events):
104+ return None
105+ while events[self.raw_event_id].type != SYN_REPORT:
106+ e = events[self.raw_event_id]
107+ code = e.code
108+ if code == ABS_MT_SLOT:
109+ self.active_slot_num = e.value
110+ if not self.active_slot_num in self.slots:
111+ self.slots[self.active_slot_num] = Slot()
112+ elif code == ABS_MT_TRACKING_ID:
113+ id = e.value
114+ if id < 0:
115+ del self.slots[self.active_slot_num]
116+ else:
117+ self.active_slot().set_tracking_id(id)
118+ elif code == ABS_MT_POSITION_X:
119+ self.active_slot().set_x(e.value)
120+ elif code == ABS_MT_POSITION_Y:
121+ self.active_slot().set_y(e.value)
122+ self.raw_event_id += 1
123+ self.current_time = events[self.raw_event_id].time
124+ self.raw_event_id += 1
125+ return self.slots
126+
127+ def get_current_time(self):
128+ return self.current_time - self.start_time
129+
130+def event_from_string(string):
131+ arr = string.split()
132+ if len(arr) != 5:
133+ return None
134+ time = float(arr[1])
135+ type = int(arr[2], 16)
136+ code = int(arr[3], 16)
137+ value = int(arr[4])
138+ return RawEvent(time, type, code, value)
139+
140+def build_state_array(state_tracker):
141+ states = []
142+ current_state = state_tracker.get_next_state()
143+ while current_state is not None:
144+ current_time = state_tracker.current_time
145+ state_copy = copy.deepcopy(current_state)
146+ states.append(State(current_time, state_copy))
147+ current_state = state_tracker.get_next_state()
148+ return states
149+
150+def absolute_from_string(string):
151+ arr = string.split()
152+ assert(len(arr) == 6)
153+ index = int(arr[1], 16)
154+ minimum = int(arr[2])
155+ maximum = int(arr[3])
156+ fuzz = int(arr[4])
157+ flat = int(arr[5])
158+ a = Absolute(minimum, maximum, fuzz, flat)
159+ return (index, a)
160+class EventReader():
161+ def __init__(self, ifilename):
162+ self.ifilename = ifilename
163+ self.read_events()
164+
165+
166+ def parse_lines(self, infile):
167+ for line in infile:
168+ line = line.strip()
169+ if line.startswith('P:'):
170+ self.properties.append(line)
171+ elif line.startswith('B:'):
172+ self.masks.append(line)
173+ elif line.startswith('A:'):
174+ self.add_abs(line)
175+ elif line.startswith('E:'):
176+ e = event_from_string(line)
177+ if e is None:
178+ print 'Malformed line in input'
179+ else:
180+ self.events.append(e)
181+ else:
182+ raise RuntimeError('Unparseable line in input: ' + line)
183+
184+ def read_events(self):
185+ self.description = 'No description'
186+ self.masks = []
187+ self.bustype = ''
188+ self.vendor = ''
189+ self.product = ''
190+ self.version = ''
191+ self.masks = []
192+ self.properties = []
193+ self.abs = {}
194+ self.events = []
195+ infile = file(self.ifilename, 'r')
196+
197+ first = infile.readline()
198+ if first.startswith('N:'):
199+ self.description = first.strip().split(' ', 1)[1]
200+ (dummy, self.bustype, self.vendor, self.product, self.version) = \
201+ infile.readline().strip().split()
202+ elif first.startswith('E:'):
203+ self.description = 'MISSING'
204+ self.bustype = 'MISSING'
205+ self.vendor = 'MISSING'
206+ self.product = 'MISSING'
207+ self.version = 'MISSING'
208+ infile.seek(0)
209+ else:
210+ raise RuntimeError('Tried to open unsupported file.')
211+ self.parse_lines(infile)
212+
213+ def add_abs(self, line):
214+ (index, a) = absolute_from_string(line)
215+ self.abs[index] = a
216+
217+
218+ def get_absolutes(self):
219+ return self.abs
220+
221+ def print_stats(self):
222+ print "RawEvent source:", self.description
223+ print "USB Bustype %s vendor %s product %s version %s" % \
224+ (self.bustype, self.vendor, self.product, self.version)
225+ print "Number of masks:", len(self.masks)
226+ print "Number of properties:", len(self.properties)
227+ print "Number of absolutes:", len(self.abs)
228+ print "Number of events:", len(self.events)
229+ if len(self.events) > 0:
230+ print "Duration:", self.events[-1].time - self.events[0].time
231+
232+if __name__ == '__main__':
233+ input = sys.argv[1]
234+ try:
235+ reader = EventReader(input)
236+ except RuntimeError, e:
237+ print "Processing failed:", e
238+ sys.exit(2)
239+ reader.print_stats()
240+ state = StateTracker(reader)
241+ num_states = 0
242+ while state.get_next_state() != None:
243+ num_states += 1
244+ print "Unique states:", num_states
245
246=== added file 'test/eventviewer.glade'
247--- test/eventviewer.glade 1970-01-01 00:00:00 +0000
248+++ test/eventviewer.glade 2011-08-17 14:03:35 +0000
249@@ -0,0 +1,57 @@
250+<?xml version="1.0" encoding="UTF-8"?>
251+<interface>
252+ <requires lib="gtk+" version="2.24"/>
253+ <!-- interface-naming-policy project-wide -->
254+ <object class="GtkWindow" id="MainWindow">
255+ <property name="can_focus">False</property>
256+ <child>
257+ <object class="GtkTable" id="table1">
258+ <property name="visible">True</property>
259+ <property name="can_focus">False</property>
260+ <property name="n_rows">2</property>
261+ <property name="n_columns">2</property>
262+ <child>
263+ <object class="GtkDrawingArea" id="canvas">
264+ <property name="width_request">800</property>
265+ <property name="height_request">600</property>
266+ <property name="visible">True</property>
267+ <property name="can_focus">False</property>
268+ </object>
269+ <packing>
270+ <property name="right_attach">2</property>
271+ </packing>
272+ </child>
273+ <child>
274+ <object class="GtkLabel" id="StatusLabel">
275+ <property name="visible">True</property>
276+ <property name="can_focus">False</property>
277+ <property name="xalign">0.81999999284744263</property>
278+ <property name="label" translatable="yes">This is some text</property>
279+ </object>
280+ <packing>
281+ <property name="left_attach">1</property>
282+ <property name="right_attach">2</property>
283+ <property name="top_attach">1</property>
284+ <property name="bottom_attach">2</property>
285+ <property name="y_options"></property>
286+ </packing>
287+ </child>
288+ <child>
289+ <object class="GtkToggleButton" id="animate">
290+ <property name="label" translatable="yes">Animate</property>
291+ <property name="visible">True</property>
292+ <property name="can_focus">True</property>
293+ <property name="receives_default">True</property>
294+ <property name="use_action_appearance">False</property>
295+ <property name="xalign">0</property>
296+ </object>
297+ <packing>
298+ <property name="top_attach">1</property>
299+ <property name="bottom_attach">2</property>
300+ <property name="y_options"></property>
301+ </packing>
302+ </child>
303+ </object>
304+ </child>
305+ </object>
306+</interface>
307
308=== added file 'test/eventviewer.py'
309--- test/eventviewer.py 1970-01-01 00:00:00 +0000
310+++ test/eventviewer.py 2011-08-17 14:03:35 +0000
311@@ -0,0 +1,190 @@
312+#!/usr/bin/python -tt
313+#
314+# @file event-viewer
315+# @brief a demo/test program that shows the progress of touches
316+#
317+# Copyright (C) 2011 Canonical Ltd
318+#
319+# This program is free software; you can redistribute it and/or modify
320+# it under the terms of the GNU General Public License as published by
321+# the Free Software Foundation; either version 3 of the License, or
322+# (at your option) any later version.
323+#
324+# This program is distributed in the hope that it will be useful,
325+# but WITHOUT ANY WARRANTY; without even the implied warranty of
326+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
327+# GNU General Public License for more details.
328+#
329+# You should have received a copy of the GNU General Public License
330+# along with this program. If not, see <http://www.gnu.org/licenses/>.
331+
332+from eventparser import *
333+
334+import glib, gtk, gobject
335+from math import pi
336+import sys, os, time
337+
338+class EventViewer():
339+
340+ def __init__(self, state_file_name):
341+ self.state_file_name = state_file_name
342+ self.glade_filename = os.path.join(os.path.split(__file__)[0], 'eventviewer.glade')
343+ self.build_gui()
344+ self.window.set_title('Multitouch viewer: ' + os.path.basename(state_file_name))
345+ self.load_states()
346+ self.current_index = 0
347+ #self.max_x = 1020.0 # FIXME, get from event stream absolutes
348+ #self.max_y = 600.0
349+ self.max_x = 32762.0 # FIXME, get from event stream absolutes
350+ self.max_y = 32762.0
351+ self.source_aspect = self.max_x/float(self.max_y)
352+ self.set_status_text()
353+
354+ def load_states(self):
355+ state_tracker = StateTracker(EventReader(self.state_file_name))
356+ self.states = build_state_array(state_tracker)
357+
358+ def get_current_relative_time(self):
359+ min_time = self.states[0].time
360+ cur_time = self.get_current_time()
361+ return (cur_time - min_time)
362+
363+ def move_through_time(self, delta):
364+ self.current_index += delta
365+ if self.current_index < 0:
366+ self.current_index = 0
367+ if self.current_index >= len(self.states):
368+ self.current_index = len(self.states)-1
369+ self.set_status_text()
370+ return self.states[self.current_index]
371+
372+ def set_status_text(self):
373+ state = self.states[self.current_index]
374+ self.status_label.set_text('%d touches at %.3f s, frame %d/%d' %\
375+ (len(state.slots), self.get_current_relative_time(),\
376+ self.current_index, len(self.states)))
377+
378+ def build_gui(self):
379+ self.builder = gtk.Builder()
380+ self.builder.add_from_file(self.glade_filename)
381+ self.window = self.builder.get_object('MainWindow')
382+ self.canvas = self.builder.get_object('canvas')
383+ self.status_label = self.builder.get_object('StatusLabel')
384+ self.animate = self.builder.get_object('animate')
385+ self.window.connect("destroy", gtk.main_quit)
386+ self.window.connect("expose-event", self.canvas_expose)
387+ self.window.connect("key-press-event", self.key_pressed)
388+ self.animate.connect('toggled', self.animate_toggled)
389+ self.window.show_all()
390+
391+ def key_pressed(self, widget, event):
392+ if event.keyval == gtk.keysyms.Right:
393+ self.move_through_time(1)
394+ self.canvas_expose(None, None)
395+ if event.keyval == gtk.keysyms.Left:
396+ self.move_through_time(-1)
397+ self.canvas_expose(None, None)
398+ if event.keyval == gtk.keysyms.Up:
399+ self.move_through_time(10)
400+ self.canvas_expose(None, None)
401+ if event.keyval == gtk.keysyms.Down:
402+ self.move_through_time(-10)
403+ self.canvas_expose(None, None)
404+ if event.keyval == gtk.keysyms.Page_Up:
405+ self.move_through_time(100)
406+ self.canvas_expose(None, None)
407+ if event.keyval == gtk.keysyms.Page_Down:
408+ self.move_through_time(-100)
409+ if event.keyval == gtk.keysyms.q or \
410+ event.keyval == gtk.keysyms.Escape:
411+ gtk.main_quit()
412+
413+ def get_current_state(self):
414+ return self.states[self.current_index]
415+
416+ def get_current_slots(self):
417+ return self.get_current_state().slots
418+
419+ def get_current_time(self):
420+ return self.get_current_state().time
421+
422+ def animate_toggled(self, dummy):
423+ if self.animate.get_active():
424+ self.start_animation()
425+ else:
426+ self.stop_animation()
427+
428+ def canvas_expose(self, widget, event):
429+ cr = self.canvas.window.cairo_create()
430+ # Erase all
431+ cr.set_source_rgb(1.0, 1.0, 1.0)
432+ (w, h) = self.canvas.window.get_size()
433+ cr.rectangle(0, 0, w, h)
434+ cr.fill()
435+
436+ canvas_aspect = w/float(h)
437+ if canvas_aspect > self.source_aspect:
438+ area_y = int(0.05*h)
439+ area_h = h - 2*area_y
440+ area_w = self.source_aspect*area_h
441+ area_x = (w - area_w)/2
442+ else:
443+ area_x = int(0.05*w)
444+ area_w = (w - 2*area_x)
445+ area_h= area_w/self.source_aspect
446+ area_y = (h - area_h)/2
447+
448+ # Draw area box.
449+ cr.set_source_rgb(0.0, 0.0, 0.0)
450+ cr.rectangle(area_x, area_y, area_w, area_h)
451+ cr.stroke()
452+ # Draw spots
453+ for v in self.get_current_slots().values():
454+ plot_x = area_x + v.x/self.max_x*area_w
455+ plot_y = area_y + v.y/self.max_y*area_h
456+ cr.arc(plot_x, plot_y, 10, 0, 2*pi)
457+ cr.fill()
458+
459+
460+ def set_animation_callback(self):
461+ delay = self.delay_to_next_frame()
462+ if delay >= 0:
463+ gobject.timeout_add(int(delay*1000), self.animation_loop)
464+ else:
465+ self.animate.set_active(False)
466+ self.current_index = 0
467+ self.animate.set_active(True)
468+
469+ def start_animation(self):
470+ # If necessary, evaluate and store animation start time.
471+ #self.current_index = 0
472+ self.set_animation_callback()
473+
474+ def stop_animation(self):
475+ pass
476+
477+ def animation_loop(self):
478+ self.move_through_time(1)
479+ self.canvas_expose(None, None)
480+ if self.animate.get_active():
481+ self.set_animation_callback()
482+
483+
484+ def delay_to_next_frame(self):
485+ try:
486+ # Note that this will drift because we just add
487+ # delta t:s one after the other. This should not be
488+ # an issue, but if it is we need to evaluate times
489+ # relative to a specified point in time.
490+ now_time = self.states[self.current_index].time
491+ next_time = self.states[self.current_index+1].time
492+ delay = next_time - now_time
493+ return delay
494+ except IndexError:
495+ return -1
496+
497+if __name__ == '__main__':
498+ if len(sys.argv) != 2:
499+ print sys.argv[0], '<evemu file>'
500+ viewer = EventViewer(sys.argv[1])
501+ gtk.main()

Subscribers

People subscribed via source and target branches