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 time
19import acts.test_utils.bt.bt_test_utils as bt_utils
20import acts.test_utils.wifi.wifi_performance_test_utils as wifi_utils
21
22from acts import asserts
23from acts.test_utils.bt.BtSarBaseTest import BtSarBaseTest
24
25
26class BtSarPowerLimitTest(BtSarBaseTest):
27    """Class to define BT SAR power cap tests.
28
29    This class defines tests that iterate over and force different
30    states in the BT SAR table and calculates the TX power at the
31    antenna port.
32    """
33    def setup_test(self):
34        super().setup_test()
35
36        # BokehFigure object
37        self.plot = wifi_utils.BokehFigure(title='{}'.format(
38            self.current_test_name),
39                                           x_label='Scenarios',
40                                           primary_y_label='TX power(dBm)')
41
42    def save_sar_plot(self, df, results_file_path):
43        """ Saves SAR plot to the path given.
44
45        Args:
46            df: Processed SAR table sweep results
47            results_file_path: path where the bokeh figure is to saved
48        """
49        self.plot.add_line(df.index,
50                           df['expected_tx_power'],
51                           'expected',
52                           marker='circle')
53        self.plot.add_line(df.index,
54                           df['measured_tx_power'],
55                           'measured',
56                           marker='circle')
57
58        results_file_path = os.path.join(
59            self.log_path, '{}.html'.format(self.current_test_name))
60        self.plot.generate_figure()
61        wifi_utils.BokehFigure.save_figures([self.plot], results_file_path)
62
63    def sweep_table(self, sort_order=''):
64        """Iterates over the BT SAR table and forces signal states.
65
66        Iterates over BT SAR table and forces signal states,
67        measuring RSSI and power level for each state.
68
69        Args:
70            sort_order: decides the sort order of the BT SAR Table.
71        Returns:
72            sar_df : SAR table sweep results in pandas dataframe
73        """
74
75        sar_df = self.bt_sar_df.copy()
76        sar_df['power_cap'] = -128
77        sar_df['slave_rssi'] = -128
78        sar_df['master_rssi'] = -128
79        sar_df['pwlv'] = -1
80
81        # Sorts the table
82        if sort_order:
83            if sort_order.lower() == 'ascending':
84                sar_df = sar_df.sort_values(by=['BluetoothPower'],
85                                            ascending=True)
86            else:
87                sar_df = sar_df.sort_values(by=['BluetoothPower'],
88                                            ascending=False)
89        # Sweeping BT SAR table
90        for scenario in range(sar_df.shape[0]):
91            # Reading BT SAR Scenario from the table
92            read_scenario = sar_df.loc[scenario].to_dict()
93
94            start_time = self.dut.adb.shell('date +%s.%m')
95            time.sleep(1)
96            self.set_sar_state(self.dut, read_scenario)
97
98            # Appending BT metrics to the table
99            sar_df.loc[scenario, 'power_cap'] = self.get_current_power_cap(
100                self.dut, start_time)
101            processed_bqr_results = bt_utils.get_bt_metric(
102                self.android_devices, self.duration)
103            sar_df.loc[scenario, 'slave_rssi'] = processed_bqr_results['rssi'][
104                self.bt_device_controller.serial]
105            sar_df.loc[scenario, 'master_rssi'] = processed_bqr_results[
106                'rssi'][self.dut.serial]
107            sar_df.loc[scenario, 'pwlv'] = processed_bqr_results['pwlv'][
108                self.dut.serial]
109            self.log.info(
110                'scenario:{}, power_cap:{},  s_rssi:{}, m_rssi:{}, m_pwlv:{}'.
111                format(scenario, sar_df.loc[scenario, 'power_cap'],
112                       sar_df.loc[scenario, 'slave_rssi'],
113                       sar_df.loc[scenario, 'master_rssi'],
114                       sar_df.loc[scenario, 'pwlv']))
115        self.log.info('BT SAR Table swept')
116
117        return sar_df
118
119    def process_table(self, sar_df):
120        """Processes the results of sweep_table and computes BT TX power.
121
122        Processes the results of sweep_table and computes BT TX power
123        after factoring in the path loss and FTM offsets.
124
125        Args:
126             sar_df: BT SAR table after the sweep
127
128        Returns:
129            sar_df: processed BT SAR table
130        """
131
132        pathloss = self.calibration_params['pathloss']
133
134        # Adding a target power column
135        sar_df['target_power'] = sar_df['pwlv'].astype(str).map(
136            self.calibration_params['target_power'])
137
138        # Adding a ftm  power column
139        sar_df['ftm_power'] = sar_df['pwlv'].astype(str).map(
140            self.calibration_params['ftm_power'])
141
142        # BT SAR Backoff for each scenario
143        sar_df['backoff'] = sar_df['target_power'] - sar_df['power_cap'] / 4
144
145        sar_df['measured_tx_power'] = sar_df['slave_rssi'] + pathloss
146        sar_df['expected_tx_power'] = sar_df['ftm_power'] - sar_df['backoff']
147
148        sar_df['delta'] = sar_df['expected_tx_power'] - sar_df[
149            'measured_tx_power']
150        self.log.info('Sweep results processed')
151
152        results_file_path = os.path.join(self.log_path, self.current_test_name)
153        sar_df.to_csv('{}.csv'.format(results_file_path))
154        self.save_sar_plot(sar_df, results_file_path)
155
156        return sar_df
157
158    def process_results(self, sar_df):
159        """Determines the test results of the sweep.
160
161         Parses the processed table with computed BT TX power values
162         to return pass or fail.
163
164        Args:
165             sar_df: processed BT SAR table
166        """
167
168        # checks for errors at particular points in the sweep
169        max_error_result = abs(sar_df['delta']) > self.max_error_threshold
170        if False in max_error_result:
171            asserts.fail('Maximum Error Threshold Exceeded')
172
173        # checks for error accumulation across the sweep
174        if sar_df['delta'].sum() > self.agg_error_threshold:
175            asserts.fail(
176                'Aggregate Error Threshold Exceeded. Error: {} Threshold: {}'.
177                format(sar_df['delta'].sum(), self.agg_error_threshold))
178
179        else:
180            asserts.explicit_pass('Measured and Expected Power Values in line')
181
182    def test_bt_sar_table(self):
183        sar_df = self.sweep_table()
184        sar_df = self.process_table(sar_df)
185        self.process_results(sar_df)
186
187    def test_bt_sar_custom_table(self):
188        # Pushes the custom file
189        backup_sar_path = os.path.join(self.dut.device_log_path,
190                                       self.BACKUP_BT_SAR_TABLE_NAME)
191        self.push_table(self.dut, self.custom_sar_path, backup_sar_path)
192
193        # Connect master and slave
194        bt_utils.connect_phone_to_headset(self.dut, self.bt_device, 60)
195
196        sar_df = self.sweep_table()
197        sar_df = self.process_table(sar_df)
198
199        # Pushing the backup table back
200        self.push_table(self.dut, backup_sar_path)
201
202        self.process_results(sar_df)
203