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_magnitude = self._make_timeseries()
86    self.raw_acceleration_axes = self._add_timeseries_axes(
87        1, 'Raw Acceleration', 'm/s^2', [-20, 20],
88        yticks=range(-15, 16, 5))
89    self.raw_acceleration_line_x = self._add_timeseries_line(
90        self.raw_acceleration_axes, 'x', 'red')
91    self.raw_acceleration_line_y = self._add_timeseries_line(
92        self.raw_acceleration_axes, 'y', 'green')
93    self.raw_acceleration_line_z = self._add_timeseries_line(
94        self.raw_acceleration_axes, 'z', 'blue')
95    self.raw_acceleration_line_magnitude = self._add_timeseries_line(
96        self.raw_acceleration_axes, 'magnitude', 'orange', linewidth=2)
97    self._add_timeseries_legend(self.raw_acceleration_axes)
98
99    shared_axis = self.raw_acceleration_axes
100
101    self.filtered_acceleration_x = self._make_timeseries()
102    self.filtered_acceleration_y = self._make_timeseries()
103    self.filtered_acceleration_z = self._make_timeseries()
104    self.filtered_acceleration_magnitude = self._make_timeseries()
105    self.filtered_acceleration_axes = self._add_timeseries_axes(
106        2, 'Filtered Acceleration', 'm/s^2', [-20, 20],
107        sharex=shared_axis,
108        yticks=range(-15, 16, 5))
109    self.filtered_acceleration_line_x = self._add_timeseries_line(
110        self.filtered_acceleration_axes, 'x', 'red')
111    self.filtered_acceleration_line_y = self._add_timeseries_line(
112        self.filtered_acceleration_axes, 'y', 'green')
113    self.filtered_acceleration_line_z = self._add_timeseries_line(
114        self.filtered_acceleration_axes, 'z', 'blue')
115    self.filtered_acceleration_line_magnitude = self._add_timeseries_line(
116        self.filtered_acceleration_axes, 'magnitude', 'orange', linewidth=2)
117    self._add_timeseries_legend(self.filtered_acceleration_axes)
118
119    self.tilt_angle = self._make_timeseries()
120    self.tilt_angle_axes = self._add_timeseries_axes(
121        3, 'Tilt Angle', 'degrees', [-105, 105],
122        sharex=shared_axis,
123        yticks=range(-90, 91, 30))
124    self.tilt_angle_line = self._add_timeseries_line(
125        self.tilt_angle_axes, 'tilt', 'black')
126    self._add_timeseries_legend(self.tilt_angle_axes)
127
128    self.orientation_angle = self._make_timeseries()
129    self.orientation_angle_axes = self._add_timeseries_axes(
130        4, 'Orientation Angle', 'degrees', [-25, 375],
131        sharex=shared_axis,
132        yticks=range(0, 361, 45))
133    self.orientation_angle_line = self._add_timeseries_line(
134        self.orientation_angle_axes, 'orientation', 'black')
135    self._add_timeseries_legend(self.orientation_angle_axes)
136
137    self.current_rotation = self._make_timeseries()
138    self.proposed_rotation = self._make_timeseries()
139    self.predicted_rotation = self._make_timeseries()
140    self.orientation_axes = self._add_timeseries_axes(
141        5, 'Current / Proposed Orientation', 'rotation', [-1, 4],
142        sharex=shared_axis,
143        yticks=range(0, 4))
144    self.current_rotation_line = self._add_timeseries_line(
145        self.orientation_axes, 'current', 'black', linewidth=2)
146    self.predicted_rotation_line = self._add_timeseries_line(
147        self.orientation_axes, 'predicted', 'purple', linewidth=3)
148    self.proposed_rotation_line = self._add_timeseries_line(
149        self.orientation_axes, 'proposed', 'green', linewidth=3)
150    self._add_timeseries_legend(self.orientation_axes)
151
152    self.time_until_settled = self._make_timeseries()
153    self.time_until_flat_delay_expired = self._make_timeseries()
154    self.time_until_swing_delay_expired = self._make_timeseries()
155    self.time_until_acceleration_delay_expired = self._make_timeseries()
156    self.stability_axes = self._add_timeseries_axes(
157        6, 'Proposal Stability', 'ms', [-10, 600],
158        sharex=shared_axis,
159        yticks=range(0, 600, 100))
160    self.time_until_settled_line = self._add_timeseries_line(
161        self.stability_axes, 'time until settled', 'black', linewidth=2)
162    self.time_until_flat_delay_expired_line = self._add_timeseries_line(
163        self.stability_axes, 'time until flat delay expired', 'green')
164    self.time_until_swing_delay_expired_line = self._add_timeseries_line(
165        self.stability_axes, 'time until swing delay expired', 'blue')
166    self.time_until_acceleration_delay_expired_line = self._add_timeseries_line(
167        self.stability_axes, 'time until acceleration delay expired', 'red')
168    self._add_timeseries_legend(self.stability_axes)
169
170    self.sample_latency = self._make_timeseries()
171    self.sample_latency_axes = self._add_timeseries_axes(
172        7, 'Accelerometer Sampling Latency', 'ms', [-10, 500],
173        sharex=shared_axis,
174        yticks=range(0, 500, 100))
175    self.sample_latency_line = self._add_timeseries_line(
176        self.sample_latency_axes, 'latency', 'black')
177    self._add_timeseries_legend(self.sample_latency_axes)
178
179    self.fig.canvas.mpl_connect('button_press_event', self._on_click)
180    self.paused = False
181
182    self.timer = self.fig.canvas.new_timer(interval=100)
183    self.timer.add_callback(lambda: self.update())
184    self.timer.start()
185
186    self.timebase = None
187    self._reset_parse_state()
188
189  # Handle a click event to pause or restart the timer.
190  def _on_click(self, ev):
191    if not self.paused:
192      self.paused = True
193      self.timer.stop()
194    else:
195      self.paused = False
196      self.timer.start()
197
198  # Initialize a time series.
199  def _make_timeseries(self):
200    return [[], []]
201
202  # Add a subplot to the figure for a time series.
203  def _add_timeseries_axes(self, index, title, ylabel, ylim, yticks, sharex=None):
204    num_graphs = 7
205    height = 0.9 / num_graphs
206    top = 0.95 - height * index
207    axes = self.fig.add_axes([0.1, top, 0.8, height],
208        xscale='linear',
209        xlim=[0, timespan],
210        ylabel=ylabel,
211        yscale='linear',
212        ylim=ylim,
213        sharex=sharex)
214    axes.text(0.02, 0.02, title, transform=axes.transAxes, fontsize=10, fontweight='bold')
215    axes.set_xlabel('time (s)', fontsize=10, fontweight='bold')
216    axes.set_ylabel(ylabel, fontsize=10, fontweight='bold')
217    axes.set_xticks(range(0, timespan + 1, timeticks))
218    axes.set_yticks(yticks)
219    axes.grid(True)
220
221    for label in axes.get_xticklabels():
222      label.set_fontsize(9)
223    for label in axes.get_yticklabels():
224      label.set_fontsize(9)
225
226    return axes
227
228  # Add a line to the axes for a time series.
229  def _add_timeseries_line(self, axes, label, color, linewidth=1):
230    return axes.plot([], label=label, color=color, linewidth=linewidth)[0]
231
232  # Add a legend to a time series.
233  def _add_timeseries_legend(self, axes):
234    axes.legend(
235        loc='upper left',
236        bbox_to_anchor=(1.01, 1),
237        borderpad=0.1,
238        borderaxespad=0.1,
239        prop={'size': 10})
240
241  # Resets the parse state.
242  def _reset_parse_state(self):
243    self.parse_raw_acceleration_x = None
244    self.parse_raw_acceleration_y = None
245    self.parse_raw_acceleration_z = None
246    self.parse_raw_acceleration_magnitude = None
247    self.parse_filtered_acceleration_x = None
248    self.parse_filtered_acceleration_y = None
249    self.parse_filtered_acceleration_z = None
250    self.parse_filtered_acceleration_magnitude = None
251    self.parse_tilt_angle = None
252    self.parse_orientation_angle = None
253    self.parse_current_rotation = None
254    self.parse_proposed_rotation = None
255    self.parse_predicted_rotation = None
256    self.parse_time_until_settled = None
257    self.parse_time_until_flat_delay_expired = None
258    self.parse_time_until_swing_delay_expired = None
259    self.parse_time_until_acceleration_delay_expired = None
260    self.parse_sample_latency = None
261
262  # Update samples.
263  def update(self):
264    timeindex = 0
265    while True:
266      try:
267        line = self.adbout.readline()
268      except EOFError:
269        plot.close()
270        return
271      if line is None:
272        break
273      print line
274
275      try:
276        timestamp = self._parse_timestamp(line)
277      except ValueError, e:
278        continue
279      if self.timebase is None:
280        self.timebase = timestamp
281      delta = timestamp - self.timebase
282      timeindex = delta.seconds + delta.microseconds * 0.000001
283
284      if line.find('Raw acceleration vector:') != -1:
285        self.parse_raw_acceleration_x = self._get_following_number(line, 'x=')
286        self.parse_raw_acceleration_y = self._get_following_number(line, 'y=')
287        self.parse_raw_acceleration_z = self._get_following_number(line, 'z=')
288        self.parse_raw_acceleration_magnitude = self._get_following_number(line, 'magnitude=')
289
290      if line.find('Filtered acceleration vector:') != -1:
291        self.parse_filtered_acceleration_x = self._get_following_number(line, 'x=')
292        self.parse_filtered_acceleration_y = self._get_following_number(line, 'y=')
293        self.parse_filtered_acceleration_z = self._get_following_number(line, 'z=')
294        self.parse_filtered_acceleration_magnitude = self._get_following_number(line, 'magnitude=')
295
296      if line.find('tiltAngle=') != -1:
297        self.parse_tilt_angle = self._get_following_number(line, 'tiltAngle=')
298
299      if line.find('orientationAngle=') != -1:
300        self.parse_orientation_angle = self._get_following_number(line, 'orientationAngle=')
301
302      if line.find('Result:') != -1:
303        self.parse_current_rotation = self._get_following_number(line, 'currentRotation=')
304        self.parse_proposed_rotation = self._get_following_number(line, 'proposedRotation=')
305        self.parse_predicted_rotation = self._get_following_number(line, 'predictedRotation=')
306        self.parse_sample_latency = self._get_following_number(line, 'timeDeltaMS=')
307        self.parse_time_until_settled = self._get_following_number(line, 'timeUntilSettledMS=')
308        self.parse_time_until_flat_delay_expired = self._get_following_number(line, 'timeUntilFlatDelayExpiredMS=')
309        self.parse_time_until_swing_delay_expired = self._get_following_number(line, 'timeUntilSwingDelayExpiredMS=')
310        self.parse_time_until_acceleration_delay_expired = self._get_following_number(line, 'timeUntilAccelerationDelayExpiredMS=')
311
312        self._append(self.raw_acceleration_x, timeindex, self.parse_raw_acceleration_x)
313        self._append(self.raw_acceleration_y, timeindex, self.parse_raw_acceleration_y)
314        self._append(self.raw_acceleration_z, timeindex, self.parse_raw_acceleration_z)
315        self._append(self.raw_acceleration_magnitude, timeindex, self.parse_raw_acceleration_magnitude)
316        self._append(self.filtered_acceleration_x, timeindex, self.parse_filtered_acceleration_x)
317        self._append(self.filtered_acceleration_y, timeindex, self.parse_filtered_acceleration_y)
318        self._append(self.filtered_acceleration_z, timeindex, self.parse_filtered_acceleration_z)
319        self._append(self.filtered_acceleration_magnitude, timeindex, self.parse_filtered_acceleration_magnitude)
320        self._append(self.tilt_angle, timeindex, self.parse_tilt_angle)
321        self._append(self.orientation_angle, timeindex, self.parse_orientation_angle)
322        self._append(self.current_rotation, timeindex, self.parse_current_rotation)
323        if self.parse_proposed_rotation >= 0:
324          self._append(self.proposed_rotation, timeindex, self.parse_proposed_rotation)
325        else:
326          self._append(self.proposed_rotation, timeindex, None)
327        if self.parse_predicted_rotation >= 0:
328          self._append(self.predicted_rotation, timeindex, self.parse_predicted_rotation)
329        else:
330          self._append(self.predicted_rotation, timeindex, None)
331        self._append(self.time_until_settled, timeindex, self.parse_time_until_settled)
332        self._append(self.time_until_flat_delay_expired, timeindex, self.parse_time_until_flat_delay_expired)
333        self._append(self.time_until_swing_delay_expired, timeindex, self.parse_time_until_swing_delay_expired)
334        self._append(self.time_until_acceleration_delay_expired, timeindex, self.parse_time_until_acceleration_delay_expired)
335        self._append(self.sample_latency, timeindex, self.parse_sample_latency)
336        self._reset_parse_state()
337
338    # Scroll the plots.
339    if timeindex > timespan:
340      bottom = int(timeindex) - timespan + scrolljump
341      self.timebase += timedelta(seconds=bottom)
342      self._scroll(self.raw_acceleration_x, bottom)
343      self._scroll(self.raw_acceleration_y, bottom)
344      self._scroll(self.raw_acceleration_z, bottom)
345      self._scroll(self.raw_acceleration_magnitude, bottom)
346      self._scroll(self.filtered_acceleration_x, bottom)
347      self._scroll(self.filtered_acceleration_y, bottom)
348      self._scroll(self.filtered_acceleration_z, bottom)
349      self._scroll(self.filtered_acceleration_magnitude, bottom)
350      self._scroll(self.tilt_angle, bottom)
351      self._scroll(self.orientation_angle, bottom)
352      self._scroll(self.current_rotation, bottom)
353      self._scroll(self.proposed_rotation, bottom)
354      self._scroll(self.predicted_rotation, bottom)
355      self._scroll(self.time_until_settled, bottom)
356      self._scroll(self.time_until_flat_delay_expired, bottom)
357      self._scroll(self.time_until_swing_delay_expired, bottom)
358      self._scroll(self.time_until_acceleration_delay_expired, bottom)
359      self._scroll(self.sample_latency, bottom)
360
361    # Redraw the plots.
362    self.raw_acceleration_line_x.set_data(self.raw_acceleration_x)
363    self.raw_acceleration_line_y.set_data(self.raw_acceleration_y)
364    self.raw_acceleration_line_z.set_data(self.raw_acceleration_z)
365    self.raw_acceleration_line_magnitude.set_data(self.raw_acceleration_magnitude)
366    self.filtered_acceleration_line_x.set_data(self.filtered_acceleration_x)
367    self.filtered_acceleration_line_y.set_data(self.filtered_acceleration_y)
368    self.filtered_acceleration_line_z.set_data(self.filtered_acceleration_z)
369    self.filtered_acceleration_line_magnitude.set_data(self.filtered_acceleration_magnitude)
370    self.tilt_angle_line.set_data(self.tilt_angle)
371    self.orientation_angle_line.set_data(self.orientation_angle)
372    self.current_rotation_line.set_data(self.current_rotation)
373    self.proposed_rotation_line.set_data(self.proposed_rotation)
374    self.predicted_rotation_line.set_data(self.predicted_rotation)
375    self.time_until_settled_line.set_data(self.time_until_settled)
376    self.time_until_flat_delay_expired_line.set_data(self.time_until_flat_delay_expired)
377    self.time_until_swing_delay_expired_line.set_data(self.time_until_swing_delay_expired)
378    self.time_until_acceleration_delay_expired_line.set_data(self.time_until_acceleration_delay_expired)
379    self.sample_latency_line.set_data(self.sample_latency)
380
381    self.fig.canvas.draw_idle()
382
383  # Scroll a time series.
384  def _scroll(self, timeseries, bottom):
385    bottom_index = bisect.bisect_left(timeseries[0], bottom)
386    del timeseries[0][:bottom_index]
387    del timeseries[1][:bottom_index]
388    for i, timeindex in enumerate(timeseries[0]):
389      timeseries[0][i] = timeindex - bottom
390
391  # Extract a word following the specified prefix.
392  def _get_following_word(self, line, prefix):
393    prefix_index = line.find(prefix)
394    if prefix_index == -1:
395      return None
396    start_index = prefix_index + len(prefix)
397    delim_index = line.find(',', start_index)
398    if delim_index == -1:
399      return line[start_index:]
400    else:
401      return line[start_index:delim_index]
402
403  # Extract a number following the specified prefix.
404  def _get_following_number(self, line, prefix):
405    word = self._get_following_word(line, prefix)
406    if word is None:
407      return None
408    return float(word)
409
410  # Extract an array of numbers following the specified prefix.
411  def _get_following_array_of_numbers(self, line, prefix):
412    prefix_index = line.find(prefix + '[')
413    if prefix_index == -1:
414      return None
415    start_index = prefix_index + len(prefix) + 1
416    delim_index = line.find(']', start_index)
417    if delim_index == -1:
418      return None
419
420    result = []
421    while start_index < delim_index:
422      comma_index = line.find(', ', start_index, delim_index)
423      if comma_index == -1:
424        result.append(float(line[start_index:delim_index]))
425        break;
426      result.append(float(line[start_index:comma_index]))
427      start_index = comma_index + 2
428    return result
429
430  # Add a value to a time series.
431  def _append(self, timeseries, timeindex, number):
432    timeseries[0].append(timeindex)
433    timeseries[1].append(number)
434
435  # Parse the logcat timestamp.
436  # Timestamp has the form '01-21 20:42:42.930'
437  def _parse_timestamp(self, line):
438    return datetime.strptime(line[0:18], '%m-%d %H:%M:%S.%f')
439
440# Notice
441print "Window Orientation Listener plotting tool"
442print "-----------------------------------------\n"
443print "Please turn on the Window Orientation Listener logging.  See README.txt."
444
445# Start adb.
446print "Starting adb logcat.\n"
447
448adb = subprocess.Popen(['adb', 'logcat', '-s', '-v', 'time', 'WindowOrientationListener:V'],
449    stdout=subprocess.PIPE)
450adbout = NonBlockingStream(adb.stdout)
451
452# Prepare plotter.
453plotter = Plotter(adbout)
454plotter.update()
455
456# Main loop.
457plot.show()
458