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 logging
18import os
19import pandas as pd
20import re
21import time
22import acts.test_utils.bt.bt_test_utils as bt_utils
23
24from acts.libs.proc import job
25from acts.base_test import BaseTestClass
26from acts.test_utils.bt.bt_power_test_utils import MediaControl
27from acts.test_utils.abstract_devices.bluetooth_handsfree_abstract_device import BluetoothHandsfreeAbstractDeviceFactory as bt_factory
28
29PHONE_MUSIC_FILE_DIRECTORY = '/sdcard/Music'
30FORCE_SAR_ADB_COMMAND = ('am broadcast -n'
31                         'com.google.android.apps.scone/.coex.TestReceiver -a '
32                         'com.google.android.apps.scone.coex.SIMULATE_STATE ')
33
34DEFAULT_DURATION = 5
35DEFAULT_MAX_ERROR_THRESHOLD = 2
36DEFAULT_AGG_MAX_ERROR_THRESHOLD = 2
37
38
39class BtSarBaseTest(BaseTestClass):
40    """ Base class for all BT SAR Test classes.
41
42        This class implements functions common to BT SAR test Classes.
43    """
44    BACKUP_BT_SAR_TABLE_NAME = 'backup_bt_sar_table.csv'
45
46    def __init__(self, controllers):
47        BaseTestClass.__init__(self, controllers)
48        self.power_file_paths = [
49            '/vendor/etc/bluetooth_power_limits.csv',
50            '/data/vendor/radio/bluetooth_power_limits.csv'
51        ]
52        self.sar_file_name = os.path.basename(self.power_file_paths[0])
53
54    def setup_class(self):
55        """Initializes common test hardware and parameters.
56
57        This function initializes hardware and compiles parameters that are
58        common to all tests in this class and derived classes.
59        """
60        super().setup_class()
61
62        self.test_params = self.user_params.get('bt_sar_test_params', {})
63        if not self.test_params:
64            self.log.warning(
65                'bt_sar_test_params was not found in the config file.')
66
67        self.user_params.update(self.test_params)
68        req_params = ['bt_devices', 'calibration_params']
69
70        self.unpack_userparams(
71            req_params,
72            duration=DEFAULT_DURATION,
73            custom_sar_path=None,
74            music_files=None,
75            sort_order=None,
76            max_error_threshold=DEFAULT_MAX_ERROR_THRESHOLD,
77            agg_error_threshold=DEFAULT_AGG_MAX_ERROR_THRESHOLD,
78            tpc_threshold=[2, 8],
79        )
80
81        self.attenuator = self.attenuators[0]
82        self.dut = self.android_devices[0]
83
84        # To prevent default file from being overwritten
85        self.dut.adb.shell('cp {} {}'.format(self.power_file_paths[0],
86                                             self.power_file_paths[1]))
87
88        self.sar_file_path = self.power_file_paths[1]
89        self.atten_min = 0
90        self.atten_max = int(self.attenuator.get_max_atten())
91
92        # Initializing media controller
93        if self.music_files:
94            music_src = self.music_files[0]
95            music_dest = PHONE_MUSIC_FILE_DIRECTORY
96            success = self.dut.push_system_file(music_src, music_dest)
97            if success:
98                self.music_file = os.path.join(PHONE_MUSIC_FILE_DIRECTORY,
99                                               os.path.basename(music_src))
100            # Initialize media_control class
101            self.media = MediaControl(self.dut, self.music_file)
102
103        #Initializing BT device controller
104        if self.bt_devices:
105            attr, idx = self.bt_devices.split(':')
106            self.bt_device_controller = getattr(self, attr)[int(idx)]
107            self.bt_device = bt_factory().generate(self.bt_device_controller)
108        else:
109            self.log.error('No BT devices config is provided!')
110
111        bt_utils.enable_bqr(self.android_devices)
112
113        self.log_path = os.path.join(logging.log_path, 'results')
114        os.makedirs(self.log_path, exist_ok=True)
115
116        # Reading BT SAR table from the phone
117        self.bt_sar_df = self.read_sar_table(self.dut)
118
119    def setup_test(self):
120        super().setup_test()
121
122        # Starting BT on the master
123        self.dut.droid.bluetoothFactoryReset()
124        bt_utils.enable_bluetooth(self.dut.droid, self.dut.ed)
125
126        # Starting BT on the slave
127        self.bt_device.reset()
128        self.bt_device.power_on()
129
130        # Connect master and slave
131        bt_utils.connect_phone_to_headset(self.dut, self.bt_device, 60)
132
133        # Playing music
134        self.media.play()
135
136        # Find and set PL10 level for the DUT
137        self.pl10_atten = self.set_PL10_atten_level(self.dut)
138
139    def teardown_test(self):
140
141        #Stopping Music
142        if hasattr(self, 'media'):
143            self.media.stop()
144
145        # Stopping BT on slave
146        self.bt_device.reset()
147        self.bt_device.power_off()
148
149        #Stopping BT on master
150        bt_utils.disable_bluetooth(self.dut.droid)
151
152        #Resetting the atten to initial levels
153        self.attenuator.set_atten(self.atten_min)
154        self.log.info('Attenuation set to {} dB'.format(self.atten_min))
155
156    def teardown_class(self):
157
158        super().teardown_class()
159        self.dut.droid.bluetoothFactoryReset()
160
161        # Stopping BT on slave
162        self.bt_device.reset()
163        self.bt_device.power_off()
164
165        #Stopping BT on master
166        bt_utils.disable_bluetooth(self.dut.droid)
167
168    def set_sar_state(self, ad, signal_dict):
169        """Sets the SAR state corresponding to the BT SAR signal.
170
171        The SAR state is forced using an adb command that takes
172        device signals as input.
173
174        Args:
175            ad: android_device object.
176            signal_dict: dict of BT SAR signals read from the SAR file.
177        Returns:
178            enforced_state: dict of device signals.
179        """
180
181        signal_dict = {k: max(int(v), 0) for (k, v) in signal_dict.items()}
182
183        #Reading signal_dict
184        head = signal_dict['Head']
185        wifi_5g = signal_dict['WIFI5Ghz']
186        hotspot_voice = signal_dict['HotspotVoice']
187        btmedia = signal_dict['BTMedia']
188        cell = signal_dict['Cell']
189        imu = signal_dict['IMU']
190
191        wifi = wifi_5g
192        wifi_24g = 0 if wifi_5g else 1
193
194        enforced_state = {
195            'Wifi': wifi,
196            'Wifi AP': hotspot_voice,
197            'Earpiece': head,
198            'Bluetooth': 1,
199            'Motion': imu,
200            'Voice': 0,
201            'Wifi 2.4G': wifi_24g,
202            'Radio': cell,
203            'Bluetooth connected': 1,
204            'Bluetooth media': btmedia
205        }
206
207        #Forcing the SAR state
208        adb_output = ad.adb.shell('{} '
209                                  '--ei earpiece {} '
210                                  '--ei wifi {} '
211                                  '--ei wifi_24g {} '
212                                  '--ei voice 0 '
213                                  '--ei wifi_ap {} '
214                                  '--ei bluetooth 1 '
215                                  '--ei bt_media {} '
216                                  '--ei radio_power {} '
217                                  '--ei motion {} '
218                                  '--ei bt_connected 1'.format(
219                                      FORCE_SAR_ADB_COMMAND, head, wifi,
220                                      wifi_24g, hotspot_voice, btmedia, cell,
221                                      imu))
222
223        # Checking if command was successfully enforced
224        if 'result=0' in adb_output:
225            self.log.info('Requested BT SAR state successfully enforced.')
226            return enforced_state
227        else:
228            self.log.error("Couldn't force BT SAR state.")
229
230    def parse_bt_logs(self, ad, begin_time, regex=''):
231        """Returns bt software stats by parsing logcat since begin_time.
232
233        The quantity to be fetched is dictated by the regex provided.
234
235        Args:
236             ad: android_device object.
237             begin_time: time stamp to start the logcat parsing.
238             regex: regex for fetching the required BT software stats.
239
240        Returns:
241             stat: the desired BT stat.
242        """
243        # Waiting for logcat to update
244        time.sleep(1)
245        bt_adb_log = ad.adb.logcat('-b all -t %s' % begin_time)
246        for line in bt_adb_log.splitlines():
247            if re.findall(regex, line):
248                stat = re.findall(regex, line)[0]
249                return stat
250
251        raise ValueError('Failed to parse BT logs.')
252
253    def get_current_power_cap(self, ad, begin_time):
254        """ Returns the enforced software power cap since begin_time.
255
256        Returns the enforced power cap since begin_time by parsing logcat.
257        Function should follow a function call that forces a SAR state
258
259        Args:
260            ad: android_device obj.
261            begin_time: time stamp to start.
262
263        Returns:
264            read enforced power cap
265        """
266        power_cap_regex = 'Bluetooth Tx Power Cap\s+(\d+)'
267        power_cap = self.parse_bt_logs(ad, begin_time, power_cap_regex)
268        return int(power_cap)
269
270    def get_current_device_state(self, ad, begin_time):
271        """ Returns the device state of the android dut since begin_time.
272
273        Returns the device state of the android dut by parsing logcat since
274        begin_time. Function should follow a function call that forces
275        a SAR state.
276
277        Args:
278            ad: android_device obj.
279            begin_time: time stamp to start.
280
281        Returns:
282            device_state: device state of the android device.
283        """
284
285        device_state_regex = 'updateDeviceState: DeviceState: ([\s*\S+\s]+)'
286        device_state = self.parse_bt_logs(ad, begin_time, device_state_regex)
287        return device_state
288
289    def read_sar_table(self, ad):
290        """Extracts the BT SAR table from the phone.
291
292        Extracts the BT SAR table from the phone into the android device
293        log path directory.
294
295        Args:
296            ad: android_device object.
297
298        Returns:
299            df : BT SAR table (as pandas DataFrame).
300        """
301        output_path = os.path.join(ad.device_log_path, self.sar_file_name)
302        ad.adb.pull('{} {}'.format(self.sar_file_path, output_path))
303        df = pd.read_csv(os.path.join(ad.device_log_path, self.sar_file_name))
304        self.log.info('BT SAR table read from the phone')
305        return df
306
307    def push_table(self, ad, src_path):
308        """Pushes a BT SAR table to the phone.
309
310        Pushes a BT SAR table to the android device and reboots the device.
311        Also creates a backup file if backup flag is True.
312
313        Args:
314            ad: android_device object.
315            src_path: path to the  BT SAR table.
316        """
317        #Copying the to-be-pushed file for logging
318        if os.path.dirname(src_path) != ad.device_log_path:
319            job.run('cp {} {}'.format(src_path, ad.device_log_path))
320
321        #Pushing the file provided in the config
322        ad.push_system_file(src_path, self.sar_file_path)
323        self.log.info('BT SAR table pushed')
324        ad.reboot()
325        self.bt_sar_df = self.read_sar_table(self.dut)
326
327    def set_PL10_atten_level(self, ad):
328        """Finds the attenuation level at which the phone is at PL10
329
330        Finds PL10 attenuation level by sweeping the attenuation range.
331        If the power level is not achieved during sweep,
332        returns the max atten level
333
334        Args:
335            ad: android object class
336        Returns:
337            atten : attenuation level when the phone is at PL10
338        """
339        BT_SAR_ATTEN_STEP = 3
340
341        for atten in range(self.atten_min, self.atten_max, BT_SAR_ATTEN_STEP):
342            self.attenuator.set_atten(atten)
343            # Sleep required for BQR to reflect the change in parameters
344            time.sleep(2)
345            metrics = bt_utils.get_bt_metric(ad)
346            if metrics['pwlv'][ad.serial] == 10:
347                self.log.info('PL10 located at {}'.format(atten +
348                                                          BT_SAR_ATTEN_STEP))
349                return atten + BT_SAR_ATTEN_STEP
350
351        self.log.warn(
352            "PL10 couldn't be located in the given attenuation range")
353        return atten
354