• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2#
3#   Copyright 2019 - 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
17import os
18import re
19import time
20import logging
21import pandas as pd
22
23from acts import asserts
24from acts.libs.proc import job
25from acts.base_test import BaseTestClass
26
27from acts.metrics.loggers.blackbox import BlackboxMetricLogger
28from acts_contrib.test_utils.bt.bt_power_test_utils import MediaControl
29from acts_contrib.test_utils.bt.ble_performance_test_utils import run_ble_throughput_and_read_rssi
30from acts_contrib.test_utils.abstract_devices.bluetooth_handsfree_abstract_device import BluetoothHandsfreeAbstractDeviceFactory as bt_factory
31
32import acts_contrib.test_utils.bt.bt_test_utils as bt_utils
33import acts_contrib.test_utils.wifi.wifi_performance_test_utils as wifi_utils
34
35PHONE_MUSIC_FILE_DIRECTORY = '/sdcard/Music'
36
37FORCE_SAR_ADB_COMMAND = ('am broadcast -n'
38                         'com.google.android.apps.scone/.coex.TestReceiver -a '
39                         'com.google.android.apps.scone.coex.SIMULATE_STATE ')
40
41SLEEP_DURATION = 2
42
43DEFAULT_DURATION = 5
44DEFAULT_MAX_ERROR_THRESHOLD = 2
45DEFAULT_AGG_MAX_ERROR_THRESHOLD = 2
46FIXED_ATTENUATION = 36
47
48
49class BtSarBaseTest(BaseTestClass):
50    """ Base class for all BT SAR Test classes.
51
52        This class implements functions common to BT SAR test Classes.
53    """
54    BACKUP_BT_SAR_TABLE_NAME = 'backup_bt_sar_table.csv'
55
56    def __init__(self, controllers):
57        BaseTestClass.__init__(self, controllers)
58        self.power_file_paths = [
59            '/vendor/etc/bluetooth_power_limits.csv',
60            '/data/vendor/radio/bluetooth_power_limits.csv'
61        ]
62        self.sar_test_result = BlackboxMetricLogger.for_test_case(
63            metric_name='pass')
64        self.sar_file_name = os.path.basename(self.power_file_paths[0])
65        self.power_column = 'BluetoothPower'
66        self.REG_DOMAIN_DICT = {
67            ('us', 'ca', 'in'): 'US',
68            ('uk', 'fr', 'es', 'de', 'it', 'ie', 'sg', 'au', 'tw'): 'EU',
69            ('jp', ): 'JP'
70        }
71
72    def setup_class(self):
73        """Initializes common test hardware and parameters.
74
75        This function initializes hardware and compiles parameters that are
76        common to all tests in this class and derived classes.
77        """
78        super().setup_class()
79
80        self.test_params = self.user_params.get('bt_sar_test_params', {})
81        if not self.test_params:
82            self.log.warning(
83                'bt_sar_test_params was not found in the config file.')
84
85        self.user_params.update(self.test_params)
86        req_params = ['bt_devices', 'calibration_params', 'custom_files']
87
88        self.unpack_userparams(
89            req_params,
90            country_code='us',
91            duration=DEFAULT_DURATION,
92            sort_order=None,
93            max_error_threshold=DEFAULT_MAX_ERROR_THRESHOLD,
94            agg_error_threshold=DEFAULT_AGG_MAX_ERROR_THRESHOLD,
95            tpc_threshold=[2, 8],
96            sar_margin={
97                'BDR': 0,
98                'EDR': 0,
99                'BLE': 0
100            })
101
102        self.attenuator = self.attenuators[0]
103        self.dut = self.android_devices[0]
104
105        for key in self.REG_DOMAIN_DICT.keys():
106            if self.country_code.lower() in key:
107                self.reg_domain = self.REG_DOMAIN_DICT[key]
108
109        self.sar_version_2 = False
110
111        if 'Error' not in self.dut.adb.shell('bluetooth_sar_test -r'):
112            #Flag for SAR version 2
113            self.sar_version_2 = True
114            self.power_column = 'BluetoothEDRPower'
115            self.power_file_paths[0] = os.path.join(
116                os.path.dirname(self.power_file_paths[0]),
117                'bluetooth_power_limits_{}.csv'.format(self.reg_domain))
118            self.sar_file_name = os.path.basename(self.power_file_paths[0])
119
120        if self.sar_version_2:
121            custom_file_suffix = 'version2'
122        else:
123            custom_file_suffix = 'version1'
124
125        for file in self.custom_files:
126            if 'custom_sar_table_{}.csv'.format(custom_file_suffix) in file:
127                self.custom_sar_path = file
128                break
129        else:
130            raise RuntimeError('Custom Sar File is missing')
131
132        self.sar_file_path = self.power_file_paths[0]
133        self.atten_min = 0
134        self.atten_max = int(self.attenuator.get_max_atten())
135
136        # Get music file and push it to the phone and initialize Media controller
137        music_files = self.user_params.get('music_files', [])
138        if music_files:
139            music_src = music_files[0]
140            music_dest = PHONE_MUSIC_FILE_DIRECTORY
141            success = self.dut.push_system_file(music_src, music_dest)
142            if success:
143                self.music_file = os.path.join(PHONE_MUSIC_FILE_DIRECTORY,
144                                               os.path.basename(music_src))
145            # Initialize media_control class
146            self.media = MediaControl(self.dut, self.music_file)
147
148        #Initializing BT device controller
149        if self.bt_devices:
150            attr, idx = self.bt_devices.split(':')
151            self.bt_device_controller = getattr(self, attr)[int(idx)]
152            self.bt_device = bt_factory().generate(self.bt_device_controller)
153        else:
154            self.log.error('No BT devices config is provided!')
155
156        bt_utils.enable_bqr(self.android_devices)
157
158        self.log_path = os.path.join(logging.log_path, 'results')
159        os.makedirs(self.log_path, exist_ok=True)
160
161        # Reading BT SAR table from the phone
162        self.bt_sar_df = self.read_sar_table(self.dut)
163
164    def setup_test(self):
165        super().setup_test()
166
167        #Reset SAR test result to 0 before every test
168        self.sar_test_result.metric_value = 0
169
170        # Starting BT on the master
171        self.dut.droid.bluetoothFactoryReset()
172        bt_utils.enable_bluetooth(self.dut.droid, self.dut.ed)
173
174        # Starting BT on the slave
175        self.bt_device.reset()
176        self.bt_device.power_on()
177
178        # Connect master and slave
179        bt_utils.connect_phone_to_headset(self.dut, self.bt_device, 60)
180
181        # Playing music
182        self.media.play()
183
184        # Find and set PL10 level for the DUT
185        self.pl10_atten = self.set_PL10_atten_level(self.dut)
186        self.attenuator.set_atten(self.pl10_atten)
187
188    def teardown_test(self):
189        #Stopping Music
190        if hasattr(self, 'media'):
191            self.media.stop()
192
193        # Stopping BT on slave
194        self.bt_device.reset()
195        self.bt_device.power_off()
196
197        #Stopping BT on master
198        bt_utils.disable_bluetooth(self.dut.droid)
199
200        #Resetting the atten to initial levels
201        self.attenuator.set_atten(self.atten_min)
202        self.log.info('Attenuation set to {} dB'.format(self.atten_min))
203
204    def teardown_class(self):
205
206        super().teardown_class()
207        self.dut.droid.bluetoothFactoryReset()
208
209        # Stopping BT on slave
210        self.bt_device.reset()
211        self.bt_device.power_off()
212
213        #Stopping BT on master
214        bt_utils.disable_bluetooth(self.dut.droid)
215
216    def save_sar_plot(self, df):
217        """ Saves SAR plot to the path given.
218
219        Args:
220            df: Processed SAR table sweep results
221        """
222        self.plot.add_line(
223            df.index,
224            df['expected_tx_power'],
225            legend='expected',
226            marker='circle')
227        self.plot.add_line(
228            df.index,
229            df['measured_tx_power'],
230            legend='measured',
231            marker='circle')
232        self.plot.add_line(
233            df.index, df['delta'], legend='delta', marker='circle')
234
235        results_file_path = os.path.join(self.log_path, '{}.html'.format(
236            self.current_test_name))
237        self.plot.generate_figure()
238        wifi_utils.BokehFigure.save_figures([self.plot], results_file_path)
239
240    def sweep_power_cap(self):
241        sar_df = self.bt_sar_df
242        sar_df['BDR_power_cap'] = -128
243        sar_df['EDR_power_cap'] = -128
244        sar_df['BLE_power_cap'] = -128
245
246        if self.sar_version_2:
247            power_column_dict = {
248                'BDR': 'BluetoothBDRPower',
249                'EDR': 'BluetoothEDRPower',
250                'BLE': 'BluetoothLEPower'
251            }
252        else:
253            power_column_dict = {'EDR': self.power_column}
254
255        power_cap_error = False
256
257        for type, column_name in power_column_dict.items():
258
259            self.log.info("Performing sanity test on {}".format(type))
260            # Iterating through the BT SAR scenarios
261            for scenario in range(0, self.bt_sar_df.shape[0]):
262                # Reading BT SAR table row into dict
263                read_scenario = sar_df.loc[scenario].to_dict()
264                start_time = self.dut.adb.shell('date +%s.%m')
265                time.sleep(SLEEP_DURATION)
266
267                # Setting SAR state to the read BT SAR row
268                self.set_sar_state(self.dut, read_scenario, self.country_code)
269
270                # Reading device power cap from logcat after forcing SAR State
271                scenario_power_cap = self.get_current_power_cap(
272                    self.dut, start_time, type=type)
273                sar_df.loc[scenario, '{}_power_cap'.format(
274                    type)] = scenario_power_cap
275                self.log.info(
276                    'scenario: {}, '
277                    'sar_power: {}, power_cap:{}'.format(
278                        scenario, sar_df.loc[scenario, column_name],
279                        sar_df.loc[scenario, '{}_power_cap'.format(type)]))
280
281        if not sar_df['{}_power_cap'.format(type)].equals(sar_df[column_name]):
282            power_cap_error = True
283
284        results_file_path = os.path.join(self.log_path, '{}.csv'.format(
285            self.current_test_name))
286        sar_df.to_csv(results_file_path)
287
288        return power_cap_error
289
290    def sweep_table(self,
291                    client_ad=None,
292                    server_ad=None,
293                    client_conn_id=None,
294                    gatt_server=None,
295                    gatt_callback=None,
296                    isBLE=False):
297        """Iterates over the BT SAR table and forces signal states.
298
299        Iterates over BT SAR table and forces signal states,
300        measuring RSSI and power level for each state.
301
302        Args:
303            client_ad: the Android device performing the connection.
304            server_ad: the Android device accepting the connection.
305            client_conn_id: the client connection ID.
306            gatt_server: the gatt server
307            gatt_callback: Gatt callback objec
308            isBLE : boolean variable for BLE connection
309        Returns:
310            sar_df : SAR table sweep results in pandas dataframe
311        """
312
313        sar_df = self.bt_sar_df.copy()
314        sar_df['power_cap'] = -128
315        sar_df['slave_rssi'] = -128
316        sar_df['master_rssi'] = -128
317        sar_df['ble_rssi'] = -128
318        sar_df['pwlv'] = -1
319
320        # Sorts the table
321        if self.sort_order:
322            if self.sort_order.lower() == 'ascending':
323                sar_df = sar_df.sort_values(
324                    by=[self.power_column], ascending=True)
325            else:
326                sar_df = sar_df.sort_values(
327                    by=[self.power_column], ascending=False)
328            sar_df = sar_df.reset_index(drop=True)
329
330        # Sweeping BT SAR table
331        for scenario in range(sar_df.shape[0]):
332            # Reading BT SAR Scenario from the table
333            read_scenario = sar_df.loc[scenario].to_dict()
334
335            start_time = self.dut.adb.shell('date +%s.%m')
336            time.sleep(SLEEP_DURATION)
337
338            #Setting SAR State
339            self.set_sar_state(self.dut, read_scenario, self.country_code)
340
341            if isBLE:
342                sar_df.loc[scenario, 'power_cap'] = self.get_current_power_cap(
343                    self.dut, start_time, type='BLE')
344
345                sar_df.loc[
346                    scenario, 'ble_rssi'] = run_ble_throughput_and_read_rssi(
347                        client_ad, server_ad, client_conn_id, gatt_server,
348                        gatt_callback)
349
350                self.log.info('scenario:{}, power_cap:{},  ble_rssi:{}'.format(
351                    scenario, sar_df.loc[scenario, 'power_cap'],
352                    sar_df.loc[scenario, 'ble_rssi']))
353            else:
354                sar_df.loc[scenario, 'power_cap'] = self.get_current_power_cap(
355                    self.dut, start_time)
356
357                processed_bqr_results = bt_utils.get_bt_metric(
358                    self.android_devices, self.duration)
359                sar_df.loc[scenario, 'slave_rssi'] = processed_bqr_results[
360                    'rssi'][self.bt_device_controller.serial]
361                sar_df.loc[scenario, 'master_rssi'] = processed_bqr_results[
362                    'rssi'][self.dut.serial]
363                sar_df.loc[scenario, 'pwlv'] = processed_bqr_results['pwlv'][
364                    self.dut.serial]
365                self.log.info(
366                    'scenario:{}, power_cap:{},  s_rssi:{}, m_rssi:{}, m_pwlv:{}'
367                    .format(scenario, sar_df.loc[scenario, 'power_cap'],
368                            sar_df.loc[scenario, 'slave_rssi'],
369                            sar_df.loc[scenario, 'master_rssi'],
370                            sar_df.loc[scenario, 'pwlv']))
371
372        self.log.info('BT SAR Table swept')
373
374        return sar_df
375
376    def process_table(self, sar_df):
377        """Processes the results of sweep_table and computes BT TX power.
378
379        Processes the results of sweep_table and computes BT TX power
380        after factoring in the path loss and FTM offsets.
381
382        Args:
383             sar_df: BT SAR table after the sweep
384
385        Returns:
386            sar_df: processed BT SAR table
387        """
388
389        sar_df['pathloss'] = self.calibration_params['pathloss']
390
391        if hasattr(self, 'pl10_atten'):
392            sar_df['atten'] = self.pl10_atten
393        else:
394            sar_df['atten'] = FIXED_ATTENUATION
395
396        # BT SAR Backoff for each scenario
397        if self.sar_version_2:
398            #Reads OTP values from the phone
399            self.otp = bt_utils.read_otp(self.dut)
400
401            #OTP backoff
402            edr_otp = min(0, float(self.otp['EDR']['10']))
403            bdr_otp = min(0, float(self.otp['BR']['10']))
404            ble_otp = min(0, float(self.otp['BLE']['10']))
405
406            # EDR TX Power for PL10
407            edr_tx_power_pl10 = self.calibration_params['target_power']['EDR']['10'] - edr_otp
408
409            # BDR TX Power for PL10
410            bdr_tx_power_pl10 = self.calibration_params['target_power']['BDR']['10'] - bdr_otp
411
412            # RSSI being measured is BDR
413            offset = bdr_tx_power_pl10 - edr_tx_power_pl10
414
415            # BDR-EDR offset
416            sar_df['offset'] = offset
417
418            # Max TX power permissible
419            sar_df['max_power'] = self.calibration_params['max_power']
420
421            # Adding a target power column
422            if 'ble_rssi' in sar_df.columns:
423                sar_df[
424                    'target_power'] = self.calibration_params['target_power']['BLE']['10'] - ble_otp
425            else:
426                sar_df['target_power'] = sar_df['pwlv'].astype(str).map(
427                    self.calibration_params['target_power']['EDR']) - edr_otp
428
429            #Translates power_cap values to expected TX power level
430            sar_df['cap_tx_power'] = sar_df['power_cap'] / 4.0
431
432            sar_df['expected_tx_power'] = sar_df[[
433                'cap_tx_power', 'target_power', 'max_power'
434            ]].min(axis=1)
435
436            if hasattr(self, 'pl10_atten'):
437                sar_df[
438                    'measured_tx_power'] = sar_df['slave_rssi'] + sar_df['pathloss'] + self.pl10_atten - offset
439            else:
440                sar_df[
441                    'measured_tx_power'] = sar_df['ble_rssi'] + sar_df['pathloss'] + FIXED_ATTENUATION
442
443        else:
444
445            # Adding a target power column
446            sar_df['target_power'] = sar_df['pwlv'].astype(str).map(
447                self.calibration_params['target_power']['EDR']['10'])
448
449            # Adding a ftm  power column
450            sar_df['ftm_power'] = sar_df['pwlv'].astype(str).map(
451                self.calibration_params['ftm_power']['EDR'])
452            sar_df[
453                'backoff'] = sar_df['target_power'] - sar_df['power_cap'] / 4.0
454
455            sar_df[
456                'expected_tx_power'] = sar_df['ftm_power'] - sar_df['backoff']
457            sar_df[
458                'measured_tx_power'] = sar_df['slave_rssi'] + sar_df['pathloss'] + self.pl10_atten
459
460        sar_df[
461            'delta'] = sar_df['expected_tx_power'] - sar_df['measured_tx_power']
462
463        self.log.info('Sweep results processed')
464
465        results_file_path = os.path.join(self.log_path, self.current_test_name)
466        sar_df.to_csv('{}.csv'.format(results_file_path))
467        self.save_sar_plot(sar_df)
468
469        return sar_df
470
471    def process_results(self, sar_df, type='EDR'):
472        """Determines the test results of the sweep.
473
474         Parses the processed table with computed BT TX power values
475         to return pass or fail.
476
477        Args:
478             sar_df: processed BT SAR table
479        """
480        if self.sar_version_2:
481            breach_error_result = (
482                sar_df['expected_tx_power'] + self.sar_margin[type] >
483                sar_df['measured_tx_power']).all()
484            if not breach_error_result:
485                asserts.fail('Measured TX power exceeds expected')
486
487        else:
488            # checks for errors at particular points in the sweep
489            max_error_result = abs(
490                sar_df['delta']) > self.max_error_threshold[type]
491            if max_error_result:
492                asserts.fail('Maximum Error Threshold Exceeded')
493
494            # checks for error accumulation across the sweep
495            if sar_df['delta'].sum() > self.agg_error_threshold[type]:
496                asserts.fail(
497                    'Aggregate Error Threshold Exceeded. Error: {} Threshold: {}'.
498                    format(sar_df['delta'].sum(), self.agg_error_threshold))
499
500        self.sar_test_result.metric_value = 1
501        asserts.explicit_pass('Measured and Expected Power Values in line')
502
503    def set_sar_state(self, ad, signal_dict, country_code='us'):
504        """Sets the SAR state corresponding to the BT SAR signal.
505
506        The SAR state is forced using an adb command that takes
507        device signals as input.
508
509        Args:
510            ad: android_device object.
511            signal_dict: dict of BT SAR signals read from the SAR file.
512        Returns:
513            enforced_state: dict of device signals.
514        """
515        signal_dict = {k: max(int(v), 0) for (k, v) in signal_dict.items()}
516        signal_dict["Wifi"] = signal_dict['WIFI5Ghz']
517        signal_dict['WIFI2Ghz'] = 0 if signal_dict['WIFI5Ghz'] else 1
518
519        device_state_dict = {
520            ('Earpiece', 'earpiece'): signal_dict['Head'],
521            ('Wifi', 'wifi'): signal_dict['WIFI5Ghz'],
522            ('Wifi 2.4G', 'wifi_24g'): signal_dict['WIFI2Ghz'],
523            ('Voice', 'voice'): 0,
524            ('Wifi AP', 'wifi_ap'): signal_dict['HotspotVoice'],
525            ('Bluetooth', 'bluetooth'): 1,
526            ('Bluetooth media', 'bt_media'): signal_dict['BTMedia'],
527            ('Radio', 'radio_power'): signal_dict['Cell'],
528            ('Motion', 'motion'): signal_dict['IMU'],
529            ('Bluetooth connected', 'bt_connected'): 1
530        }
531
532        if 'BTHotspot' in signal_dict.keys():
533            device_state_dict[('Bluetooth tethering',
534                               'bt_tethering')] = signal_dict['BTHotspot']
535
536        enforced_state = {}
537        sar_state_command = FORCE_SAR_ADB_COMMAND
538        for key in device_state_dict:
539            enforced_state[key[0]] = device_state_dict[key]
540            sar_state_command = '{} --ei {} {}'.format(
541                sar_state_command, key[1], device_state_dict[key])
542        if self.sar_version_2:
543            sar_state_command = '{} --es country_iso "{}"'.format(
544                sar_state_command, country_code.lower())
545
546        #Forcing the SAR state
547        adb_output = ad.adb.shell(sar_state_command)
548
549        # Checking if command was successfully enforced
550        if 'result=0' in adb_output:
551            self.log.info('Requested BT SAR state successfully enforced.')
552            return enforced_state
553        else:
554            self.log.error("Couldn't force BT SAR state.")
555
556    def parse_bt_logs(self, ad, begin_time, regex=''):
557        """Returns bt software stats by parsing logcat since begin_time.
558
559        The quantity to be fetched is dictated by the regex provided.
560
561        Args:
562             ad: android_device object.
563             begin_time: time stamp to start the logcat parsing.
564             regex: regex for fetching the required BT software stats.
565
566        Returns:
567             stat: the desired BT stat.
568        """
569        # Waiting for logcat to update
570        time.sleep(SLEEP_DURATION)
571        bt_adb_log = ad.adb.logcat('-b all -t %s' % begin_time)
572        for line in bt_adb_log.splitlines():
573            if re.findall(regex, line):
574                stat = re.findall(regex, line)[0]
575                return stat
576
577    def set_country_code(self, ad, cc):
578        """Sets the SAR regulatory domain as per given country code
579
580        The SAR regulatory domain is forced using an adb command that takes
581        country code as input.
582
583        Args:
584            ad: android_device object.
585            cc: country code
586        """
587
588        ad.adb.shell("{} --es country_iso {}".format(FORCE_SAR_ADB_COMMAND,
589                                                     cc))
590        self.log.info("Country Code set to {}".format(cc))
591
592    def get_country_code(self, ad, begin_time):
593        """Returns the enforced regulatory domain since begin_time
594
595        Returns enforced regulatory domain since begin_time by parsing logcat.
596        Function should follow a function call to set a country code
597
598        Args:
599            ad : android_device obj
600            begin_time: time stamp to start
601
602        Returns:
603            read enforced regulatory domain
604        """
605
606        reg_domain_regex = "updateRegulatoryDomain:\s+(\S+)"
607        reg_domain = self.parse_bt_logs(ad, begin_time, reg_domain_regex)
608        return reg_domain
609
610    def get_current_power_cap(self, ad, begin_time, type='EDR'):
611        """ Returns the enforced software EDR power cap since begin_time.
612
613        Returns the enforced EDR power cap since begin_time by parsing logcat.
614        Function should follow a function call that forces a SAR state
615
616        Args:
617            ad: android_device obj.
618            begin_time: time stamp to start.
619
620        Returns:
621            read enforced power cap
622        """
623        power_cap_regex_dict = {
624            'BDR': [
625                'Bluetooth powers: BR:\s+(\d+), EDR:\s+\d+',
626                'Bluetooth Tx Power Cap\s+(\d+)'
627            ],
628            'EDR': [
629                'Bluetooth powers: BR:\s+\d+, EDR:\s+(\d+)',
630                'Bluetooth Tx Power Cap\s+(\d+)'
631            ],
632            'BLE': [
633                'Bluetooth powers: BR:\s+\d+, EDR:\s+\d+, BLE:\s+(\d+)',
634                'Bluetooth Tx Power Cap\s+(\d+)'
635            ]
636        }
637
638        power_cap_regex_list = power_cap_regex_dict[type]
639
640        for power_cap_regex in power_cap_regex_list:
641            power_cap = self.parse_bt_logs(ad, begin_time, power_cap_regex)
642            if power_cap:
643                return int(power_cap)
644
645        raise ValueError('Failed to get TX power cap')
646
647    def get_current_device_state(self, ad, begin_time):
648        """ Returns the device state of the android dut since begin_time.
649
650        Returns the device state of the android dut by parsing logcat since
651        begin_time. Function should follow a function call that forces
652        a SAR state.
653
654        Args:
655            ad: android_device obj.
656            begin_time: time stamp to start.
657
658        Returns:
659            device_state: device state of the android device.
660        """
661
662        device_state_regex = 'updateDeviceState: DeviceState: ([\s*\S+\s]+)'
663        time.sleep(SLEEP_DURATION)
664        device_state = self.parse_bt_logs(ad, begin_time, device_state_regex)
665        if device_state:
666            return device_state
667
668        raise ValueError("Couldn't fetch device state")
669
670    def read_sar_table(self, ad, output_path=''):
671        """Extracts the BT SAR table from the phone.
672
673        Extracts the BT SAR table from the phone into the android device
674        log path directory.
675
676        Args:
677            ad: android_device object.
678            output_path: path to custom sar table
679        Returns:
680            df : BT SAR table (as pandas DataFrame).
681        """
682        if not output_path:
683            output_path = os.path.join(ad.device_log_path, self.sar_file_name)
684            ad.adb.pull('{} {}'.format(self.sar_file_path, output_path))
685
686        df = pd.read_csv(output_path)
687        self.log.info('BT SAR table read from the phone')
688        return df
689
690    def push_table(self, ad, src_path, dest_path=''):
691        """Pushes a BT SAR table to the phone.
692
693        Pushes a BT SAR table to the android device and reboots the device.
694        Also creates a backup file if backup flag is True.
695
696        Args:
697            ad: android_device object.
698            src_path: path to the  BT SAR table.
699        """
700        #Copying the to-be-pushed file for logging
701        if os.path.dirname(src_path) != ad.device_log_path:
702            job.run('cp {} {}'.format(src_path, ad.device_log_path))
703
704        #Pushing the file provided in the config
705        if dest_path:
706            ad.push_system_file(src_path, dest_path)
707        else:
708            ad.push_system_file(src_path, self.sar_file_path)
709        self.log.info('BT SAR table pushed')
710        ad.reboot()
711
712        self.bt_sar_df = self.read_sar_table(self.dut, src_path)
713
714    def set_PL10_atten_level(self, ad):
715        """Finds the attenuation level at which the phone is at PL10
716
717        Finds PL10 attenuation level by sweeping the attenuation range.
718        If the power level is not achieved during sweep,
719        returns the max atten level
720
721        Args:
722            ad: android object class
723        Returns:
724            atten : attenuation level when the phone is at PL10
725        """
726        BT_SAR_ATTEN_STEP = 3
727
728        for atten in range(self.atten_min, self.atten_max, BT_SAR_ATTEN_STEP):
729            self.attenuator.set_atten(atten)
730            # Sleep required for BQR to reflect the change in parameters
731            time.sleep(SLEEP_DURATION)
732            metrics = bt_utils.get_bt_metric(ad)
733            if metrics['pwlv'][ad.serial] == 10:
734                self.log.info(
735                    'PL10 located at {}'.format(atten + BT_SAR_ATTEN_STEP))
736                return atten + BT_SAR_ATTEN_STEP
737
738        self.log.warn(
739            "PL10 couldn't be located in the given attenuation range")
740