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