orientationplot.py revision c0347aa19f354a8e1ff4fcd5372b134c0c7c16ad
1#!/usr/bin/env python2.6
2#
3# Copyright (C) 2011 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#      http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16#
17
18#
19# Plots debug log output from WindowOrientationListener.
20# See README.txt for details.
21#
22
23import numpy as np
24import matplotlib.pyplot as plot
25import subprocess
26import re
27import fcntl
28import os
29import errno
30import bisect
31from datetime import datetime, timedelta
32
33# Parameters.
34timespan = 15 # seconds total span shown
35scrolljump = 5 # seconds jump when scrolling
36timeticks = 1 # seconds between each time tick
37
38# Non-blocking stream wrapper.
39class NonBlockingStream:
40  def __init__(self, stream):
41    fcntl.fcntl(stream, fcntl.F_SETFL, os.O_NONBLOCK)
42    self.stream = stream
43    self.buffer = ''
44    self.pos = 0
45
46  def readline(self):
47    while True:
48      index = self.buffer.find('\n', self.pos)
49      if index != -1:
50        result = self.buffer[self.pos:index]
51        self.pos = index + 1
52        return result
53
54      self.buffer = self.buffer[self.pos:]
55      self.pos = 0
56      try:
57        chunk = os.read(self.stream.fileno(), 4096)
58      except OSError, e:
59        if e.errno == errno.EAGAIN:
60          return None
61        raise e
62      if len(chunk) == 0:
63        if len(self.buffer) == 0:
64          raise(EOFError)
65        else:
66          result = self.buffer
67          self.buffer = ''
68          self.pos = 0
69          return result
70      self.buffer += chunk
71
72# Plotter
73class Plotter:
74  def __init__(self, adbout):
75    self.adbout = adbout
76
77    self.fig = plot.figure(1)
78    self.fig.suptitle('Window Orientation Listener', fontsize=12)
79    self.fig.set_dpi(96)
80    self.fig.set_size_inches(16, 12, forward=True)
81
82    self.raw_acceleration_x = self._make_timeseries()
83    self.raw_acceleration_y = self._make_timeseries()
84    self.raw_acceleration_z = self._make_timeseries()
85    self.raw_acceleration_axes = self._add_timeseries_axes(
86        1, 'Raw Acceleration', 'm/s^2', [-20, 20],
87        yticks=range(-15, 16, 5))
88    self.raw_acceleration_line_x = self._add_timeseries_line(
89        self.raw_acceleration_axes, 'x', 'red')
90    self.raw_acceleration_line_y = self._add_timeseries_line(
91        self.raw_acceleration_axes, 'y', 'green')
92    self.raw_acceleration_line_z = self._add_timeseries_line(
93        self.raw_acceleration_axes, 'z', 'blue')
94    self._add_timeseries_legend(self.raw_acceleration_axes)
95
96    shared_axis = self.raw_acceleration_axes
97
98    self.filtered_acceleration_x = self._make_timeseries()
99    self.filtered_acceleration_y = self._make_timeseries()
100    self.filtered_acceleration_z = self._make_timeseries()
101    self.magnitude = self._make_timeseries()
102    self.filtered_acceleration_axes = self._add_timeseries_axes(
103        2, 'Filtered Acceleration', 'm/s^2', [-20, 20],
104        sharex=shared_axis,
105        yticks=range(-15, 16, 5))
106    self.filtered_acceleration_line_x = self._add_timeseries_line(
107        self.filtered_acceleration_axes, 'x', 'red')
108    self.filtered_acceleration_line_y = self._add_timeseries_line(
109        self.filtered_acceleration_axes, 'y', 'green')
110    self.filtered_acceleration_line_z = self._add_timeseries_line(
111        self.filtered_acceleration_axes, 'z', 'blue')
112    self.magnitude_line = self._add_timeseries_line(
113        self.filtered_acceleration_axes, 'magnitude', 'orange', linewidth=2)
114    self._add_timeseries_legend(self.filtered_acceleration_axes)
115
116    self.tilt_angle = self._make_timeseries()
117    self.tilt_angle_axes = self._add_timeseries_axes(
118        3, 'Tilt Angle', 'degrees', [-105, 105],
119        sharex=shared_axis,
120        yticks=range(-90, 91, 30))
121    self.tilt_angle_line = self._add_timeseries_line(
122        self.tilt_angle_axes, 'tilt', 'black')
123    self._add_timeseries_legend(self.tilt_angle_axes)
124
125    self.orientation_angle = self._make_timeseries()
126    self.orientation_angle_axes = self._add_timeseries_axes(
127        4, 'Orientation Angle', 'degrees', [-25, 375],
128        sharex=shared_axis,
129        yticks=range(0, 361, 45))
130    self.orientation_angle_line = self._add_timeseries_line(
131        self.orientation_angle_axes, 'orientation', 'black')
132    self._add_timeseries_legend(self.orientation_angle_axes)
133
134    self.current_rotation = self._make_timeseries()
135    self.proposed_rotation = self._make_timeseries()
136    self.proposal_rotation = self._make_timeseries()
137    self.orientation_axes = self._add_timeseries_axes(
138        5, 'Current / Proposed Orientation and Confidence', 'rotation', [-1, 4],
139        sharex=shared_axis,
140        yticks=range(0, 4))
141    self.current_rotation_line = self._add_timeseries_line(
142        self.orientation_axes, 'current', 'black', linewidth=2)
143    self.proposal_rotation_line = self._add_timeseries_line(
144        self.orientation_axes, 'proposal', 'purple', linewidth=3)
145    self.proposed_rotation_line = self._add_timeseries_line(
146        self.orientation_axes, 'proposed', 'green', linewidth=3)
147    self._add_timeseries_legend(self.orientation_axes)
148
149    self.proposal_confidence = [[self._make_timeseries(), self._make_timeseries()]
150      for i in range(0, 4)]
151    self.proposal_confidence_polys = []
152
153    self.sample_latency = self._make_timeseries()
154    self.sample_latency_axes = self._add_timeseries_axes(
155        6, 'Accelerometer Sampling Latency', 'ms', [-10, 500],
156        sharex=shared_axis,
157        yticks=range(0, 500, 100))
158    self.sample_latency_line = self._add_timeseries_line(
159        self.sample_latency_axes, 'latency', 'black')
160    self._add_timeseries_legend(self.sample_latency_axes)
161
162    self.timer = self.fig.canvas.new_timer(interval=100)
163    self.timer.add_callback(lambda: self.update())
164    self.timer.start()
165
166    self.timebase = None
167    self._reset_parse_state()
168
169  # Initialize a time series.
170  def _make_timeseries(self):
171    return [[], []]
172
173  # Add a subplot to the figure for a time series.
174  def _add_timeseries_axes(self, index, title, ylabel, ylim, yticks, sharex=None):
175    num_graphs = 6
176    height = 0.9 / num_graphs
177    top = 0.95 - height * index
178    axes = self.fig.add_axes([0.1, top, 0.8, height],
179        xscale='linear',
180        xlim=[0, timespan],
181        ylabel=ylabel,
182        yscale='linear',
183        ylim=ylim,
184        sharex=sharex)
185    axes.text(0.02, 0.02, title, transform=axes.transAxes, fontsize=10, fontweight='bold')
186    axes.set_xlabel('time (s)', fontsize=10, fontweight='bold')
187    axes.set_ylabel(ylabel, fontsize=10, fontweight='bold')
188    axes.set_xticks(range(0, timespan + 1, timeticks))
189    axes.set_yticks(yticks)
190    axes.grid(True)
191
192    for label in axes.get_xticklabels():
193      label.set_fontsize(9)
194    for label in axes.get_yticklabels():
195      label.set_fontsize(9)
196
197    return axes
198
199  # Add a line to the axes for a time series.
200  def _add_timeseries_line(self, axes, label, color, linewidth=1):
201    return axes.plot([], label=label, color=color, linewidth=linewidth)[0]
202
203  # Add a legend to a time series.
204  def _add_timeseries_legend(self, axes):
205    axes.legend(
206        loc='upper left',
207        bbox_to_anchor=(1.01, 1),
208        borderpad=0.1,
209        borderaxespad=0.1,
210        prop={'size': 10})
211
212  # Resets the parse state.
213  def _reset_parse_state(self):
214    self.parse_raw_acceleration_x = None
215    self.parse_raw_acceleration_y = None
216    self.parse_raw_acceleration_z = None
217    self.parse_filtered_acceleration_x = None
218    self.parse_filtered_acceleration_y = None
219    self.parse_filtered_acceleration_z = None
220    self.parse_magnitude = None
221    self.parse_tilt_angle = None
222    self.parse_orientation_angle = None
223    self.parse_current_rotation = None
224    self.parse_proposed_rotation = None
225    self.parse_proposal_rotation = None
226    self.parse_proposal_confidence = None
227    self.parse_sample_latency = None
228
229  # Update samples.
230  def update(self):
231    timeindex = 0
232    while True:
233      try:
234        line = self.adbout.readline()
235      except EOFError:
236        plot.close()
237        return
238      if line is None:
239        break
240      print line
241
242      try:
243        timestamp = self._parse_timestamp(line)
244      except ValueError, e:
245        continue
246      if self.timebase is None:
247        self.timebase = timestamp
248      delta = timestamp - self.timebase
249      timeindex = delta.seconds + delta.microseconds * 0.000001
250
251      if line.find('Raw acceleration vector:') != -1:
252        self.parse_raw_acceleration_x = self._get_following_number(line, 'x=')
253        self.parse_raw_acceleration_y = self._get_following_number(line, 'y=')
254        self.parse_raw_acceleration_z = self._get_following_number(line, 'z=')
255
256      if line.find('Filtered acceleration vector:') != -1:
257        self.parse_filtered_acceleration_x = self._get_following_number(line, 'x=')
258        self.parse_filtered_acceleration_y = self._get_following_number(line, 'y=')
259        self.parse_filtered_acceleration_z = self._get_following_number(line, 'z=')
260
261      if line.find('magnitude=') != -1:
262        self.parse_magnitude = self._get_following_number(line, 'magnitude=')
263
264      if line.find('tiltAngle=') != -1:
265        self.parse_tilt_angle = self._get_following_number(line, 'tiltAngle=')
266
267      if line.find('orientationAngle=') != -1:
268        self.parse_orientation_angle = self._get_following_number(line, 'orientationAngle=')
269
270      if line.find('Result:') != -1:
271        self.parse_current_rotation = self._get_following_number(line, 'currentRotation=')
272        self.parse_proposed_rotation = self._get_following_number(line, 'proposedRotation=')
273        self.parse_proposal_rotation = self._get_following_number(line, 'proposalRotation=')
274        self.parse_proposal_confidence = self._get_following_number(line, 'proposalConfidence=')
275        self.parse_sample_latency = self._get_following_number(line, 'timeDeltaMS=')
276
277        self._append(self.raw_acceleration_x, timeindex, self.parse_raw_acceleration_x)
278        self._append(self.raw_acceleration_y, timeindex, self.parse_raw_acceleration_y)
279        self._append(self.raw_acceleration_z, timeindex, self.parse_raw_acceleration_z)
280        self._append(self.filtered_acceleration_x, timeindex, self.parse_filtered_acceleration_x)
281        self._append(self.filtered_acceleration_y, timeindex, self.parse_filtered_acceleration_y)
282        self._append(self.filtered_acceleration_z, timeindex, self.parse_filtered_acceleration_z)
283        self._append(self.magnitude, timeindex, self.parse_magnitude)
284        self._append(self.tilt_angle, timeindex, self.parse_tilt_angle)
285        self._append(self.orientation_angle, timeindex, self.parse_orientation_angle)
286        self._append(self.current_rotation, timeindex, self.parse_current_rotation)
287        if self.parse_proposed_rotation >= 0:
288          self._append(self.proposed_rotation, timeindex, self.parse_proposed_rotation)
289        else:
290          self._append(self.proposed_rotation, timeindex, None)
291        if self.parse_proposal_rotation >= 0:
292          self._append(self.proposal_rotation, timeindex, self.parse_proposal_rotation)
293        else:
294          self._append(self.proposal_rotation, timeindex, None)
295        for i in range(0, 4):
296          self._append(self.proposal_confidence[i][0], timeindex, i)
297          if i == self.parse_proposal_rotation:
298            self._append(self.proposal_confidence[i][1], timeindex,
299              i + self.parse_proposal_confidence)
300          else:
301            self._append(self.proposal_confidence[i][1], timeindex, i)
302        self._append(self.sample_latency, timeindex, self.parse_sample_latency)
303        self._reset_parse_state()
304
305    # Scroll the plots.
306    if timeindex > timespan:
307      bottom = int(timeindex) - timespan + scrolljump
308      self.timebase += timedelta(seconds=bottom)
309      self._scroll(self.raw_acceleration_x, bottom)
310      self._scroll(self.raw_acceleration_y, bottom)
311      self._scroll(self.raw_acceleration_z, bottom)
312      self._scroll(self.filtered_acceleration_x, bottom)
313      self._scroll(self.filtered_acceleration_y, bottom)
314      self._scroll(self.filtered_acceleration_z, bottom)
315      self._scroll(self.magnitude, bottom)
316      self._scroll(self.tilt_angle, bottom)
317      self._scroll(self.orientation_angle, bottom)
318      self._scroll(self.current_rotation, bottom)
319      self._scroll(self.proposed_rotation, bottom)
320      self._scroll(self.proposal_rotation, bottom)
321      for i in range(0, 4):
322        self._scroll(self.proposal_confidence[i][0], bottom)
323        self._scroll(self.proposal_confidence[i][1], bottom)
324      self._scroll(self.sample_latency, bottom)
325
326    # Redraw the plots.
327    self.raw_acceleration_line_x.set_data(self.raw_acceleration_x)
328    self.raw_acceleration_line_y.set_data(self.raw_acceleration_y)
329    self.raw_acceleration_line_z.set_data(self.raw_acceleration_z)
330    self.filtered_acceleration_line_x.set_data(self.filtered_acceleration_x)
331    self.filtered_acceleration_line_y.set_data(self.filtered_acceleration_y)
332    self.filtered_acceleration_line_z.set_data(self.filtered_acceleration_z)
333    self.magnitude_line.set_data(self.magnitude)
334    self.tilt_angle_line.set_data(self.tilt_angle)
335    self.orientation_angle_line.set_data(self.orientation_angle)
336    self.current_rotation_line.set_data(self.current_rotation)
337    self.proposed_rotation_line.set_data(self.proposed_rotation)
338    self.proposal_rotation_line.set_data(self.proposal_rotation)
339    self.sample_latency_line.set_data(self.sample_latency)
340
341    for poly in self.proposal_confidence_polys:
342      poly.remove()
343    self.proposal_confidence_polys = []
344    for i in range(0, 4):
345      self.proposal_confidence_polys.append(self.orientation_axes.fill_between(
346        self.proposal_confidence[i][0][0],
347        self.proposal_confidence[i][0][1],
348        self.proposal_confidence[i][1][1],
349        facecolor='goldenrod', edgecolor='goldenrod'))
350
351    self.fig.canvas.draw_idle()
352
353  # Scroll a time series.
354  def _scroll(self, timeseries, bottom):
355    bottom_index = bisect.bisect_left(timeseries[0], bottom)
356    del timeseries[0][:bottom_index]
357    del timeseries[1][:bottom_index]
358    for i, timeindex in enumerate(timeseries[0]):
359      timeseries[0][i] = timeindex - bottom
360
361  # Extract a word following the specified prefix.
362  def _get_following_word(self, line, prefix):
363    prefix_index = line.find(prefix)
364    if prefix_index == -1:
365      return None
366    start_index = prefix_index + len(prefix)
367    delim_index = line.find(',', start_index)
368    if delim_index == -1:
369      return line[start_index:]
370    else:
371      return line[start_index:delim_index]
372
373  # Extract a number following the specified prefix.
374  def _get_following_number(self, line, prefix):
375    word = self._get_following_word(line, prefix)
376    if word is None:
377      return None
378    return float(word)
379
380  # Extract an array of numbers following the specified prefix.
381  def _get_following_array_of_numbers(self, line, prefix):
382    prefix_index = line.find(prefix + '[')
383    if prefix_index == -1:
384      return None
385    start_index = prefix_index + len(prefix) + 1
386    delim_index = line.find(']', start_index)
387    if delim_index == -1:
388      return None
389
390    result = []
391    while start_index < delim_index:
392      comma_index = line.find(', ', start_index, delim_index)
393      if comma_index == -1:
394        result.append(float(line[start_index:delim_index]))
395        break;
396      result.append(float(line[start_index:comma_index]))
397      start_index = comma_index + 2
398    return result
399
400  # Add a value to a time series.
401  def _append(self, timeseries, timeindex, number):
402    timeseries[0].append(timeindex)
403    timeseries[1].append(number)
404
405  # Parse the logcat timestamp.
406  # Timestamp has the form '01-21 20:42:42.930'
407  def _parse_timestamp(self, line):
408    return datetime.strptime(line[0:18], '%m-%d %H:%M:%S.%f')
409
410# Notice
411print "Window Orientation Listener plotting tool"
412print "-----------------------------------------\n"
413print "Please turn on the Window Orientation Listener logging in Development Settings."
414
415# Start adb.
416print "Starting adb logcat.\n"
417
418adb = subprocess.Popen(['adb', 'logcat', '-s', '-v', 'time', 'WindowOrientationListener:V'],
419    stdout=subprocess.PIPE)
420adbout = NonBlockingStream(adb.stdout)
421
422# Prepare plotter.
423plotter = Plotter(adbout)
424plotter.update()
425
426# Main loop.
427plot.show()
428