1#/usr/bin/env python3.4 2# 3# Copyright (C) 2016 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); you may not 6# use this file except in compliance with the License. You may obtain a copy of 7# 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, WITHOUT 13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14# License for the specific language governing permissions and limitations under 15# the License. 16""" 17This test script is the base class for Bluetooth power testing 18""" 19 20import json 21import os 22import statistics 23import time 24 25from acts import asserts 26from acts import utils 27from acts.controllers import monsoon 28from acts.test_utils.bt.BluetoothBaseTest import BluetoothBaseTest 29from acts.test_utils.bt.bt_test_utils import bluetooth_enabled_check 30from acts.test_utils.tel.tel_test_utils import set_phone_screen_on 31from acts.test_utils.tel.tel_test_utils import toggle_airplane_mode_by_adb 32from acts.test_utils.wifi import wifi_test_utils as wutils 33from acts.utils import create_dir 34from acts.utils import force_airplane_mode 35from acts.utils import get_current_human_time 36from acts.utils import set_adaptive_brightness 37from acts.utils import set_ambient_display 38from acts.utils import set_auto_rotate 39from acts.utils import set_location_service 40from acts.utils import sync_device_time 41 42 43class PowerBaseTest(BluetoothBaseTest): 44 # Monsoon output Voltage in V 45 MONSOON_OUTPUT_VOLTAGE = 4.2 46 # Monsoon output max current in A 47 MONSOON_MAX_CURRENT = 7.8 48 # Power mesaurement sampling rate in Hz 49 POWER_SAMPLING_RATE = 10 50 SCREEN_TIME_OFF = 5 51 # Wait time for PMC to start in seconds 52 WAIT_TIME = 60 53 # Wait time for PMC to write AlarmTimes 54 ALARM_WAIT_TIME = 600 55 56 START_PMC_CMD = ("am start -n com.android.pmc/com.android.pmc." 57 "PMCMainActivity") 58 PMC_VERBOSE_CMD = "setprop log.tag.PMC VERBOSE" 59 60 def setup_test(self): 61 self.timer_list = [] 62 for a in self.android_devices: 63 a.ed.clear_all_events() 64 a.droid.setScreenTimeout(20) 65 self.ad.go_to_sleep() 66 return True 67 68 def disable_location_scanning(self): 69 """Utility to disable location services from scanning. 70 71 Unless the device is in airplane mode, even if WiFi is disabled 72 the DUT will still perform occasional scans. This will greatly impact 73 the power numbers. 74 75 Returns: 76 True if airplane mode is disabled and Bluetooth is enabled; 77 False otherwise. 78 """ 79 self.ad.log.info("DUT phone Airplane is ON") 80 if not toggle_airplane_mode_by_adb(self.log, self.android_devices[0], 81 True): 82 self.log.error("FAILED to toggle Airplane on") 83 return False 84 self.ad.log.info("DUT phone Bluetooth is ON") 85 if not bluetooth_enabled_check(self.android_devices[0]): 86 self.log.error("FAILED to enable Bluetooth on") 87 return False 88 return True 89 90 def teardown_test(self): 91 return True 92 93 def setup_class(self): 94 # Not to call Base class setup_class() 95 # since it removes the bonded devices 96 for ad in self.android_devices: 97 sync_device_time(ad) 98 self.ad = self.android_devices[0] 99 self.mon = self.monsoons[0] 100 self.mon.set_voltage(self.MONSOON_OUTPUT_VOLTAGE) 101 self.mon.set_max_current(self.MONSOON_MAX_CURRENT) 102 # Monsoon phone 103 self.mon.attach_device(self.ad) 104 self.monsoon_log_path = os.path.join(self.log_path, "MonsoonLog") 105 create_dir(self.monsoon_log_path) 106 107 asserts.assert_true( 108 self.mon.usb("auto"), 109 "Failed to turn USB mode to auto on monsoon.") 110 111 asserts.assert_true( 112 force_airplane_mode(self.ad, True), 113 "Can not turn on airplane mode on: %s" % self.ad.serial) 114 asserts.assert_true( 115 bluetooth_enabled_check(self.ad), 116 "Failed to set Bluetooth state to enabled") 117 set_location_service(self.ad, False) 118 set_adaptive_brightness(self.ad, False) 119 set_ambient_display(self.ad, False) 120 self.ad.adb.shell("settings put system screen_brightness 0") 121 set_auto_rotate(self.ad, False) 122 123 wutils.wifi_toggle_state(self.ad, False) 124 125 # Start PMC app. 126 self.log.info("Start PMC app...") 127 self.ad.adb.shell(self.START_PMC_CMD) 128 self.ad.adb.shell(self.PMC_VERBOSE_CMD) 129 130 self.log.info("Check to see if PMC app started") 131 for _ in range(self.WAIT_TIME): 132 time.sleep(1) 133 try: 134 self.ad.adb.shell('ps -A | grep "S com.android.pmc"') 135 break 136 except adb.AdbError as e: 137 self.log.info("PMC app is NOT started yet") 138 139 def check_pmc_status(self, log_file, label, status_msg): 140 """Utility function to check if the log file contains certain label. 141 142 Args: 143 log_file: Log file name for PMC log file 144 label: the label to be looked for in the log file 145 status_msg: error message to be displayed when the expected label is not found 146 147 Returns: 148 A list of objects which contain start and end timestamps 149 """ 150 151 # Check if PMC is ready 152 start_time = time.time() 153 result = False 154 while time.time() < start_time + self.WAIT_TIME: 155 out = self.ad.adb.shell("cat /mnt/sdcard/Download/" + log_file) 156 self.log.info("READ file: " + out) 157 if -1 != out.find(label): 158 result = True 159 break 160 time.sleep(1) 161 162 if not result: 163 self.log.error(status_msg) 164 return False 165 else: 166 return True 167 168 def check_pmc_timestamps(self, log_file): 169 """Utility function to get timestampes from a log file. 170 171 Args: 172 log_file: Log file name for PMC log file 173 174 Returns: 175 A list of objects which contain start and end timestamps 176 """ 177 178 start_time = time.time() 179 alarm_written = False 180 while time.time() < start_time + self.ALARM_WAIT_TIME: 181 out = self.ad.adb.shell("cat /mnt/sdcard/Download/" + log_file) 182 self.log.info("READ file: " + out) 183 if -1 != out.find("READY"): 184 # AlarmTimes has not been written, wait 185 self.log.info("AlarmTimes has not been written, wait") 186 else: 187 alarm_written = True 188 break 189 time.sleep(1) 190 191 if alarm_written is False: 192 self.log.info("PMC never wrote alarm file") 193 json_data = json.loads(out) 194 if json_data["AlarmTimes"]: 195 return json_data["AlarmTimes"] 196 197 def save_logs_for_power_test(self, 198 monsoon_result, 199 time1, 200 time2, 201 single_value=True): 202 """Utility function to save power data into log file. 203 204 Steps: 205 1. Save power data into a file if being configed. 206 2. Create a bug report if being configured 207 208 Args: 209 monsoon_result: power data object 210 time1: A single value or a list 211 For single value it is time duration (sec) for measure power 212 For a list of values they are a list of start times 213 time2: A single value or a list 214 For single value it is time duration (sec) which is not 215 counted toward power measurement 216 For a list of values they are a list of end times 217 single_value: True means time1 and time2 are single values 218 Otherwise they are arrays of time values 219 220 Returns: 221 BtMonsoonData for Current Average and Std Deviation 222 """ 223 current_time = get_current_human_time() 224 file_name = "{}_{}".format(self.current_test_name, current_time) 225 226 if single_value: 227 bt_monsoon_result = BtMonsoonData(monsoon_result, time1, time2, 228 self.log) 229 else: 230 bt_monsoon_result = BtMonsoonDataWithPmcTimes( 231 monsoon_result, time1, time2, self.log) 232 233 bt_monsoon_result.save_to_text_file(bt_monsoon_result, 234 os.path.join( 235 self.monsoon_log_path, 236 file_name)) 237 238 self.ad.take_bug_report(self.current_test_name, current_time) 239 return (bt_monsoon_result.average_cur, bt_monsoon_result.stdev) 240 241 def check_test_pass(self, average_current, watermark_value): 242 """Compare watermark numbers for pass/fail criteria. 243 244 b/67960377 = for BT codec runs 245 b/67959834 = for BLE scan+GATT runs 246 247 Args: 248 average_current: the numbers calculated from Monsoon box 249 watermark_value: the reference numbers from config file 250 251 Returns: 252 True if the current is within 10%; False otherwise 253 254 """ 255 watermark_value = float(watermark_value) 256 variance_plus = watermark_value + (watermark_value * 0.1) 257 variance_minus = watermark_value - (watermark_value * 0.1) 258 if (average_current > variance_plus) or (average_current < 259 variance_minus): 260 self.log.error('==> FAILED criteria from check_test_pass method') 261 return False 262 self.log.info('==> PASS criteria from check_test_pass method') 263 return True 264 265 266class BtMonsoonData(monsoon.MonsoonData): 267 """A class for encapsulating power measurement data from monsoon. 268 It implements the power averaging and standard deviation for 269 mulitple cycles of the power data. Each cycle is defined by a constant 270 measure time and a constant idle time. Measure time is the time 271 duration when power data are included for calculation. 272 Idle time is the time when power data should be removed from calculation 273 274 """ 275 # Accuracy for current and power data 276 ACCURACY = 4 277 THOUSAND = 1000 278 279 def __init__(self, monsoon_data, measure_time, idle_time, log): 280 """Instantiates a MonsoonData object. 281 282 Args: 283 monsoon_data: A list of current values in Amp (float). 284 measure_time: Time for measuring power. 285 idle_time: Time for not measuring power. 286 log: log object to log info messages. 287 """ 288 289 super(BtMonsoonData, self).__init__( 290 monsoon_data.data_points, monsoon_data.timestamps, monsoon_data.hz, 291 monsoon_data.voltage, monsoon_data.offset) 292 293 # Change timestamp to use small granularity of time 294 # Monsoon libray uses the seconds as the time unit 295 # Using sample rate to calculate timestamps between the seconds 296 t0 = self.timestamps[0] 297 dt = 1.0 / monsoon_data.hz 298 index = 0 299 300 for ind, t in enumerate(self.timestamps): 301 if t == t0: 302 index = index + 1 303 self.timestamps[ind] = t + dt * index 304 else: 305 t0 = t 306 index = 1 307 308 self.measure_time = measure_time 309 self.idle_time = idle_time 310 self.log = log 311 self.average_cur = None 312 self.stdev = None 313 314 def _calculate_average_current_n_std_dev(self): 315 """Utility function to calculate average current and standard deviation 316 in the unit of mA. 317 318 Returns: 319 A tuple of average current and std dev as float 320 """ 321 if self.idle_time == 0: 322 # if idle time is 0 use Monsoon calculation 323 # in this case standard deviation is 0 324 return round(self.average_current, self.ACCURACY), 0 325 326 self.log.info( 327 "Measure time: {} Idle time: {} Total Data Points: {}".format( 328 self.measure_time, self.idle_time, len(self.data_points))) 329 330 # The base time to be used to calculate the relative time 331 base_time = self.timestamps[0] 332 333 # Index for measure and idle cycle index 334 measure_cycle_index = 0 335 # Measure end time of measure cycle 336 measure_end_time = self.measure_time 337 # Idle end time of measure cycle 338 idle_end_time = self.measure_time + self.idle_time 339 # Sum of current data points for a measure cycle 340 current_sum = 0 341 # Number of current data points for a measure cycle 342 data_point_count = 0 343 average_for_a_cycle = [] 344 # Total number of measure data point 345 total_measured_data_point_count = 0 346 347 # Flag to indicate whether the average is calculated for this cycle 348 # For 1 second there are multiple data points 349 # so time comparison will yield to multiple cases 350 done_average = False 351 352 for t, d in zip(self.timestamps, self.data_points): 353 relative_timepoint = t - base_time 354 # When time exceeds 1 cycle of measurement update 2 end times 355 if relative_timepoint > idle_end_time: 356 measure_cycle_index += 1 357 measure_end_time = measure_cycle_index * ( 358 self.measure_time + self.idle_time) + self.measure_time 359 idle_end_time = measure_end_time + self.idle_time 360 done_average = False 361 362 # Within measure time sum the current 363 if relative_timepoint <= measure_end_time: 364 current_sum += d 365 data_point_count += 1 366 elif not done_average: 367 # Calculate the average current for this cycle 368 average_for_a_cycle.append(current_sum / data_point_count) 369 total_measured_data_point_count += data_point_count 370 current_sum = 0 371 data_point_count = 0 372 done_average = True 373 374 # Calculate the average current and convert it into mA 375 current_avg = round( 376 statistics.mean(average_for_a_cycle) * self.THOUSAND, 377 self.ACCURACY) 378 # Calculate the min and max current and convert it into mA 379 current_min = round( 380 min(average_for_a_cycle) * self.THOUSAND, self.ACCURACY) 381 current_max = round( 382 max(average_for_a_cycle) * self.THOUSAND, self.ACCURACY) 383 384 # Calculate the standard deviation and convert it into mA 385 stdev = round( 386 statistics.stdev(average_for_a_cycle) * self.THOUSAND, 387 self.ACCURACY) 388 self.log.info("Total Counted Data Points: {}".format( 389 total_measured_data_point_count)) 390 self.log.info("Average Current: {} mA ".format(current_avg)) 391 self.log.info("Standard Deviation: {} mA".format(stdev)) 392 self.log.info("Min Current: {} mA ".format(current_min)) 393 self.log.info("Max Current: {} mA".format(current_max)) 394 395 return current_avg, stdev 396 397 def _format_header(self): 398 """Utility function to write the header info to the file. 399 The data is formated as tab delimited for spreadsheets. 400 401 Returns: 402 None 403 """ 404 strs = [""] 405 if self.tag: 406 strs.append("\t\t" + self.tag) 407 else: 408 strs.append("\t\tMonsoon Measurement Data") 409 average_cur, stdev = self._calculate_average_current_n_std_dev() 410 total_power = round(average_cur * self.voltage, self.ACCURACY) 411 412 self.average_cur = average_cur 413 self.stdev = stdev 414 415 strs.append("\t\tAverage Current: {} mA.".format(average_cur)) 416 strs.append("\t\tSTD DEV Current: {} mA.".format(stdev)) 417 strs.append("\t\tVoltage: {} V.".format(self.voltage)) 418 strs.append("\t\tTotal Power: {} mW.".format(total_power)) 419 strs.append( 420 ("\t\t{} samples taken at {}Hz, with an offset of {} samples." 421 ).format(len(self.data_points), self.hz, self.offset)) 422 return "\n".join(strs) 423 424 def _format_data_point(self): 425 """Utility function to format the data into a string. 426 The data is formated as tab delimited for spreadsheets. 427 428 Returns: 429 Average current as float 430 """ 431 strs = [] 432 strs.append(self._format_header()) 433 strs.append("\t\tTime\tAmp") 434 # Get the relative time 435 start_time = self.timestamps[0] 436 437 for t, d in zip(self.timestamps, self.data_points): 438 strs.append("{}\t{}".format( 439 round((t - start_time), 1), round(d, self.ACCURACY))) 440 441 return "\n".join(strs) 442 443 @staticmethod 444 def save_to_text_file(bt_monsoon_data, file_path): 445 """Save BtMonsoonData object to a text file. 446 The data is formated as tab delimited for spreadsheets. 447 448 Args: 449 bt_monsoon_data: A BtMonsoonData object to write to a text 450 file. 451 file_path: The full path of the file to save to, including the file 452 name. 453 """ 454 if not bt_monsoon_data: 455 self.log.error("Attempting to write empty Monsoon data to " 456 "file, abort") 457 return 458 459 utils.create_dir(os.path.dirname(file_path)) 460 try: 461 with open(file_path, 'w') as f: 462 f.write(bt_monsoon_data._format_data_point()) 463 f.write("\t\t" + bt_monsoon_data.delimiter) 464 except IOError: 465 self.log.error("Fail to write power data into file") 466 467 468class BtMonsoonDataWithPmcTimes(BtMonsoonData): 469 """A class for encapsulating power measurement data from monsoon. 470 It implements the power averaging and standard deviation for 471 mulitple cycles of the power data. Each cycle is defined by a start time 472 and an end time. The start time and the end time are the actual time 473 triggered by Android alarm in PMC. 474 475 """ 476 477 def __init__(self, bt_monsoon_data, start_times, end_times, log): 478 """Instantiates a MonsoonData object. 479 480 Args: 481 bt_monsoon_data: A list of current values in Amp (float). 482 start_times: A list of epoch timestamps (int). 483 end_times: A list of epoch timestamps (int). 484 log: log object to log info messages. 485 """ 486 super(BtMonsoonDataWithPmcTimes, self).__init__( 487 bt_monsoon_data, 0, 0, log) 488 self.start_times = start_times 489 self.end_times = end_times 490 491 def _calculate_average_current_n_std_dev(self): 492 """Utility function to calculate average current and standard deviation 493 in the unit of mA. 494 495 Returns: 496 A tuple of average current and std dev as float 497 """ 498 if len(self.start_times) == 0 or len(self.end_times) == 0: 499 return 0, 0 500 501 self.log.info( 502 "Start times: {} End times: {} Total Data Points: {}".format( 503 len(self.start_times), 504 len(self.end_times), len(self.data_points))) 505 506 # Index for measure and idle cycle index 507 measure_cycle_index = 0 508 509 # Measure end time of measure cycle 510 measure_end_time = self.end_times[0] 511 # Idle end time of measure cycle 512 idle_end_time = self.start_times[1] 513 # Sum of current data points for a measure cycle 514 current_sum = 0 515 # Number of current data points for a measure cycle 516 data_point_count = 0 517 average_for_a_cycle = [] 518 # Total number of measure data point 519 total_measured_data_point_count = 0 520 521 # Flag to indicate whether the average is calculated for this cycle 522 # For 1 second there are multiple data points 523 # so time comparison will yield to multiple cases 524 done_average = False 525 done_all = False 526 527 for t, d in zip(self.timestamps, self.data_points): 528 529 # Ignore the data before the first start time 530 if t < self.start_times[0]: 531 continue 532 533 # When time exceeds 1 cycle of measurement update 2 end times 534 if t >= idle_end_time: 535 measure_cycle_index += 1 536 if measure_cycle_index > (len(self.start_times) - 1): 537 break 538 539 measure_end_time = self.end_times[measure_cycle_index] 540 541 if measure_cycle_index < (len(self.start_times) - 2): 542 idle_end_time = self.start_times[measure_cycle_index + 1] 543 else: 544 idle_end_time = measure_end_time + self.THOUSAND 545 done_all = True 546 547 done_average = False 548 549 # Within measure time sum the current 550 if t <= measure_end_time: 551 current_sum += d 552 data_point_count += 1 553 elif not done_average: 554 # Calculate the average current for this cycle 555 if data_point_count > 0: 556 average_for_a_cycle.append(current_sum / data_point_count) 557 total_measured_data_point_count += data_point_count 558 559 if done_all: 560 break 561 current_sum = 0 562 data_point_count = 0 563 done_average = True 564 565 if not done_average and data_point_count > 0: 566 # Calculate the average current for this cycle 567 average_for_a_cycle.append(current_sum / data_point_count) 568 total_measured_data_point_count += data_point_count 569 570 self.log.info( 571 "Total Number of Cycles: {}".format(len(average_for_a_cycle))) 572 573 # Calculate the average current and convert it into mA 574 current_avg = round( 575 statistics.mean(average_for_a_cycle) * self.THOUSAND, 576 self.ACCURACY) 577 # Calculate the min and max current and convert it into mA 578 current_min = round( 579 min(average_for_a_cycle) * self.THOUSAND, self.ACCURACY) 580 current_max = round( 581 max(average_for_a_cycle) * self.THOUSAND, self.ACCURACY) 582 583 # Calculate the standard deviation and convert it into mA 584 stdev = round( 585 statistics.stdev(average_for_a_cycle) * self.THOUSAND, 586 self.ACCURACY) 587 self.log.info("Total Counted Data Points: {}".format( 588 total_measured_data_point_count)) 589 self.log.info("Average Current: {} mA ".format(current_avg)) 590 self.log.info("Standard Deviation: {} mA".format(stdev)) 591 self.log.info("Min Current: {} mA ".format(current_min)) 592 self.log.info("Max Current: {} mA".format(current_max)) 593 594 return current_avg, stdev 595