Merge lp:~jpakkane/evemu/touchviewer into lp:evemu
- touchviewer
- Merge into trunk
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 |
Related bugs: |
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 |
Commit message
Description of the change
This branch adds the touch viewer application originally developed under Grail: https:/
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.
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.
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:/
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Unmerged revisions
- 42. By Jussi Pakkanen
-
Imported touch viewer from Grail repo.
Preview Diff
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() |
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.