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