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

Proposed by Jussi Pakkanen
Status: Needs review
Proposed branch: lp:~jpakkane/evemu/touchviewer
Merge into: lp:evemu
Diff against target: 501 lines (+487/-0)
3 files modified
tools/eventparser.py (+240/-0)
tools/eventviewer.glade (+57/-0)
tools/eventviewer.py (+190/-0)
To merge this branch: bzr merge lp:~jpakkane/evemu/touchviewer
Reviewer Review Type Date Requested Status
PS Jenkins bot (community) continuous-integration Needs Fixing
Chase Douglas (community) Needs Fixing
Review via email: mp+73815@code.launchpad.net

Description of the change

This branch adds the touch viewer application originally developed under Grail: https://code.launchpad.net/~jpakkane/utouch-grail/touchviewer/+merge/71893

It reads the touches data from a trace file and plots it graphically. It parses the data file itself rather than use evemu's output. The main reason for this being that there are no Python bindings for evemu as of yet.

The app can be extended to have an evemu output backend when the bindings appear. It could even parse both at the same time and plot their results next to each other, making debugging easier.

To post a comment you must log in.
Revision history for this message
Chase Douglas (chasedouglas) wrote :

Just to carry over from the previous review, I think the concept is good but it should use the evemu library to get the events. That means it's blocked on adding python bindings to evemu.

I don't think it would be too difficult to add python bindings. If for some reason it is, we can revisit whether to merge this without the evemu library linkage.

review: Needs Fixing
Revision history for this message
Stephen M. Webb (bregma) wrote :

On 09/02/2011 01:42 PM, Chase Douglas wrote:
> Review: Needs Fixing
> Just to carry over from the previous review, I think the concept is good but it should use the evemu library to get the events. That means it's blocked on adding python bindings to evemu.
>
> I don't think it would be too difficult to add python bindings. If for some reason it is, we can revisit whether to merge this without the evemu library linkage.
See bug #731678 and the branch
lp:~oubiwann/utouch-evemu/731678-python-wrapper.

--
Stephen M. Webb <email address hidden>
Canonical Ltd.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

FAILED: Continuous integration, rev:42
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https://code.launchpad.net/~jpakkane/evemu/touchviewer/+merge/73815/+edit-commit-message

http://jenkins.qa.ubuntu.com/job/evemu-ci/3/
Executed test runs:
    SUCCESS: http://jenkins.qa.ubuntu.com/job/evemu-raring-amd64-ci/3//console
    SUCCESS: http://jenkins.qa.ubuntu.com/job/evemu-raring-armhf-ci/3//console
    SUCCESS: http://jenkins.qa.ubuntu.com/job/evemu-raring-i386-ci/3//console

Click here to trigger a rebuild:
http://jenkins.qa.ubuntu.com/job/evemu-ci/3//rebuild/?

review: Needs Fixing (continuous-integration)

Unmerged revisions

42. By Jussi Pakkanen

Imported touch viewer from Grail repo.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'tools/eventparser.py'
2--- tools/eventparser.py 1970-01-01 00:00:00 +0000
3+++ tools/eventparser.py 2011-09-02 13:53:23 +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 'tools/eventviewer.glade'
247--- tools/eventviewer.glade 1970-01-01 00:00:00 +0000
248+++ tools/eventviewer.glade 2011-09-02 13:53:23 +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 'tools/eventviewer.py'
309--- tools/eventviewer.py 1970-01-01 00:00:00 +0000
310+++ tools/eventviewer.py 2011-09-02 13:53:23 +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