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 VelocityTracker.
20# Enable DEBUG_VELOCITY to print the output.
21#
22# This code supports side-by-side comparison of two algorithms.
23# The old algorithm should be modified to emit debug log messages containing
24# the word "OLD".
25#
26
27import numpy as np
28import matplotlib.pyplot as plot
29import subprocess
30import re
31import fcntl
32import os
33import errno
34import bisect
35from datetime import datetime, timedelta
36
37# Parameters.
38timespan = 15 # seconds total span shown
39scrolljump = 5 # seconds jump when scrolling
40timeticks = 1 # seconds between each time tick
41
42# Non-blocking stream wrapper.
43class NonBlockingStream:
44  def __init__(self, stream):
45    fcntl.fcntl(stream, fcntl.F_SETFL, os.O_NONBLOCK)
46    self.stream = stream
47    self.buffer = ''
48    self.pos = 0
49
50  def readline(self):
51    while True:
52      index = self.buffer.find('\n', self.pos)
53      if index != -1:
54        result = self.buffer[self.pos:index]
55        self.pos = index + 1
56        return result
57
58      self.buffer = self.buffer[self.pos:]
59      self.pos = 0
60      try:
61        chunk = os.read(self.stream.fileno(), 4096)
62      except OSError, e:
63        if e.errno == errno.EAGAIN:
64          return None
65        raise e
66      if len(chunk) == 0:
67        if len(self.buffer) == 0:
68          raise(EOFError)
69        else:
70          result = self.buffer
71          self.buffer = ''
72          self.pos = 0
73          return result
74      self.buffer += chunk
75
76# Plotter
77class Plotter:
78  def __init__(self, adbout):
79    self.adbout = adbout
80
81    self.fig = plot.figure(1)
82    self.fig.suptitle('Velocity Tracker', fontsize=12)
83    self.fig.set_dpi(96)
84    self.fig.set_size_inches(16, 12, forward=True)
85
86    self.velocity_x = self._make_timeseries()
87    self.velocity_y = self._make_timeseries()
88    self.velocity_magnitude = self._make_timeseries()
89    self.velocity_axes = self._add_timeseries_axes(
90        1, 'Velocity', 'px/s', [-5000, 5000],
91        yticks=range(-5000, 5000, 1000))
92    self.velocity_line_x = self._add_timeseries_line(
93        self.velocity_axes, 'vx', 'red')
94    self.velocity_line_y = self._add_timeseries_line(
95        self.velocity_axes, 'vy', 'green')
96    self.velocity_line_magnitude = self._add_timeseries_line(
97        self.velocity_axes, 'magnitude', 'blue')
98    self._add_timeseries_legend(self.velocity_axes)
99
100    shared_axis = self.velocity_axes
101
102    self.old_velocity_x = self._make_timeseries()
103    self.old_velocity_y = self._make_timeseries()
104    self.old_velocity_magnitude = self._make_timeseries()
105    self.old_velocity_axes = self._add_timeseries_axes(
106        2, 'Old Algorithm Velocity', 'px/s', [-5000, 5000],
107        sharex=shared_axis,
108        yticks=range(-5000, 5000, 1000))
109    self.old_velocity_line_x = self._add_timeseries_line(
110        self.old_velocity_axes, 'vx', 'red')
111    self.old_velocity_line_y = self._add_timeseries_line(
112        self.old_velocity_axes, 'vy', 'green')
113    self.old_velocity_line_magnitude = self._add_timeseries_line(
114        self.old_velocity_axes, 'magnitude', 'blue')
115    self._add_timeseries_legend(self.old_velocity_axes)
116
117    self.timer = self.fig.canvas.new_timer(interval=100)
118    self.timer.add_callback(lambda: self.update())
119    self.timer.start()
120
121    self.timebase = None
122    self._reset_parse_state()
123
124  # Initialize a time series.
125  def _make_timeseries(self):
126    return [[], []]
127
128  # Add a subplot to the figure for a time series.
129  def _add_timeseries_axes(self, index, title, ylabel, ylim, yticks, sharex=None):
130    num_graphs = 2
131    height = 0.9 / num_graphs
132    top = 0.95 - height * index
133    axes = self.fig.add_axes([0.1, top, 0.8, height],
134        xscale='linear',
135        xlim=[0, timespan],
136        ylabel=ylabel,
137        yscale='linear',
138        ylim=ylim,
139        sharex=sharex)
140    axes.text(0.02, 0.02, title, transform=axes.transAxes, fontsize=10, fontweight='bold')
141    axes.set_xlabel('time (s)', fontsize=10, fontweight='bold')
142    axes.set_ylabel(ylabel, fontsize=10, fontweight='bold')
143    axes.set_xticks(range(0, timespan + 1, timeticks))
144    axes.set_yticks(yticks)
145    axes.grid(True)
146
147    for label in axes.get_xticklabels():
148      label.set_fontsize(9)
149    for label in axes.get_yticklabels():
150      label.set_fontsize(9)
151
152    return axes
153
154  # Add a line to the axes for a time series.
155  def _add_timeseries_line(self, axes, label, color, linewidth=1):
156    return axes.plot([], label=label, color=color, linewidth=linewidth)[0]
157
158  # Add a legend to a time series.
159  def _add_timeseries_legend(self, axes):
160    axes.legend(
161        loc='upper left',
162        bbox_to_anchor=(1.01, 1),
163        borderpad=0.1,
164        borderaxespad=0.1,
165        prop={'size': 10})
166
167  # Resets the parse state.
168  def _reset_parse_state(self):
169    self.parse_velocity_x = None
170    self.parse_velocity_y = None
171    self.parse_velocity_magnitude = None
172    self.parse_old_velocity_x = None
173    self.parse_old_velocity_y = None
174    self.parse_old_velocity_magnitude = None
175
176  # Update samples.
177  def update(self):
178    timeindex = 0
179    while True:
180      try:
181        line = self.adbout.readline()
182      except EOFError:
183        plot.close()
184        return
185      if line is None:
186        break
187      print line
188
189      try:
190        timestamp = self._parse_timestamp(line)
191      except ValueError, e:
192        continue
193      if self.timebase is None:
194        self.timebase = timestamp
195      delta = timestamp - self.timebase
196      timeindex = delta.seconds + delta.microseconds * 0.000001
197
198      if line.find(': position') != -1:
199        self.parse_velocity_x = self._get_following_number(line, 'vx=')
200        self.parse_velocity_y = self._get_following_number(line, 'vy=')
201        self.parse_velocity_magnitude = self._get_following_number(line, 'speed=')
202        self._append(self.velocity_x, timeindex, self.parse_velocity_x)
203        self._append(self.velocity_y, timeindex, self.parse_velocity_y)
204        self._append(self.velocity_magnitude, timeindex, self.parse_velocity_magnitude)
205
206      if line.find(': OLD') != -1:
207        self.parse_old_velocity_x = self._get_following_number(line, 'vx=')
208        self.parse_old_velocity_y = self._get_following_number(line, 'vy=')
209        self.parse_old_velocity_magnitude = self._get_following_number(line, 'speed=')
210        self._append(self.old_velocity_x, timeindex, self.parse_old_velocity_x)
211        self._append(self.old_velocity_y, timeindex, self.parse_old_velocity_y)
212        self._append(self.old_velocity_magnitude, timeindex, self.parse_old_velocity_magnitude)
213
214    # Scroll the plots.
215    if timeindex > timespan:
216      bottom = int(timeindex) - timespan + scrolljump
217      self.timebase += timedelta(seconds=bottom)
218      self._scroll(self.velocity_x, bottom)
219      self._scroll(self.velocity_y, bottom)
220      self._scroll(self.velocity_magnitude, bottom)
221      self._scroll(self.old_velocity_x, bottom)
222      self._scroll(self.old_velocity_y, bottom)
223      self._scroll(self.old_velocity_magnitude, bottom)
224
225    # Redraw the plots.
226    self.velocity_line_x.set_data(self.velocity_x)
227    self.velocity_line_y.set_data(self.velocity_y)
228    self.velocity_line_magnitude.set_data(self.velocity_magnitude)
229    self.old_velocity_line_x.set_data(self.old_velocity_x)
230    self.old_velocity_line_y.set_data(self.old_velocity_y)
231    self.old_velocity_line_magnitude.set_data(self.old_velocity_magnitude)
232
233    self.fig.canvas.draw_idle()
234
235  # Scroll a time series.
236  def _scroll(self, timeseries, bottom):
237    bottom_index = bisect.bisect_left(timeseries[0], bottom)
238    del timeseries[0][:bottom_index]
239    del timeseries[1][:bottom_index]
240    for i, timeindex in enumerate(timeseries[0]):
241      timeseries[0][i] = timeindex - bottom
242
243  # Extract a word following the specified prefix.
244  def _get_following_word(self, line, prefix):
245    prefix_index = line.find(prefix)
246    if prefix_index == -1:
247      return None
248    start_index = prefix_index + len(prefix)
249    delim_index = line.find(',', start_index)
250    if delim_index == -1:
251      return line[start_index:]
252    else:
253      return line[start_index:delim_index]
254
255  # Extract a number following the specified prefix.
256  def _get_following_number(self, line, prefix):
257    word = self._get_following_word(line, prefix)
258    if word is None:
259      return None
260    return float(word)
261
262  # Add a value to a time series.
263  def _append(self, timeseries, timeindex, number):
264    timeseries[0].append(timeindex)
265    timeseries[1].append(number)
266
267  # Parse the logcat timestamp.
268  # Timestamp has the form '01-21 20:42:42.930'
269  def _parse_timestamp(self, line):
270    return datetime.strptime(line[0:18], '%m-%d %H:%M:%S.%f')
271
272# Notice
273print "Velocity Tracker plotting tool"
274print "-----------------------------------------\n"
275print "Please enable debug logging and recompile the code."
276
277# Start adb.
278print "Starting adb logcat.\n"
279
280adb = subprocess.Popen(['adb', 'logcat', '-s', '-v', 'time', 'Input:*', 'VelocityTracker:*'],
281    stdout=subprocess.PIPE)
282adbout = NonBlockingStream(adb.stdout)
283
284# Prepare plotter.
285plotter = Plotter(adbout)
286plotter.update()
287
288# Main loop.
289plot.show()
290