1#/usr/bin/env python3
2#
3# Copyright (C) 2018 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"""Bluetooth 1st time force pair and connect test implementation."""
17# Quick way to get the Apollo serial number:
18# python3.5 -c "from acts.controllers.buds_lib.apollo_lib import get_devices; [print(d['serial_number']) for d in get_devices()]"
19
20import statistics
21import time
22
23from acts import logger
24from acts.base_test import BaseTestClass
25from acts.controllers.buds_lib.test_actions.bt_utils import BTUtils
26from acts.controllers.buds_lib.test_actions.bt_utils import BTUtilsError
27from acts.controllers.buds_lib.test_actions.apollo_acts import ApolloTestActions
28from acts.signals import TestFailure
29from acts.signals import TestPass
30from acts.test_decorators import test_tracker_info
31from acts.test_utils.bt.BluetoothBaseTest import BluetoothBaseTest
32from acts.test_utils.bt.bt_test_utils import factory_reset_bluetooth
33from acts.test_utils.bt.bt_test_utils import enable_bluetooth
34from acts.test_utils.bt.bt_test_utils import setup_multiple_devices_for_bt_test
35from acts.test_utils.bt.loggers.bluetooth_metric_logger import BluetoothMetricLogger
36from acts.utils import set_location_service
37
38
39# The number of pairing and connection attempts
40PAIR_CONNECT_ATTEMPTS = 200
41
42
43class BluetoothPairConnectError(TestFailure):
44    pass
45
46
47class BluetoothPairAndConnectTest(BaseTestClass):
48    """Pairs and connects a phone and an Apollo buds device.
49
50   Attributes:
51       phone: An Android phone object
52       apollo: An Apollo earbuds object
53       apollo_act: An Apollo test action object
54       dut_bt_addr: The Bluetooth address of the Apollo earbuds
55       bt_utils: BTUtils test action object
56    """
57
58    def setup_class(self):
59        super().setup_class()
60        # Sanity check of the devices under test
61        # TODO(b/119051823): Investigate using a config validator to replace this.
62        if not self.android_devices:
63            raise ValueError(
64                'Cannot find android phone (need at least one).')
65        self.phone = self.android_devices[0]
66
67        if not self.buds_devices:
68            raise ValueError(
69                'Cannot find apollo device (need at least one).')
70        self.apollo = self.buds_devices[0]
71        self.log.info('Successfully found needed devices.')
72
73        # Staging the test, create result object, etc.
74        self.apollo_act = ApolloTestActions(self.apollo, self.log)
75        self.dut_bt_addr = self.apollo.bluetooth_address
76        self.bt_utils = BTUtils()
77        self.bt_logger = BluetoothMetricLogger.for_test_case()
78
79    def setup_test(self):
80        setup_multiple_devices_for_bt_test(self.android_devices)
81        # Make sure Bluetooth is on
82        enable_bluetooth(self.phone.droid, self.phone.ed)
83        set_location_service(self.phone, True)
84        self.apollo_act.factory_reset()
85        self.log.info('===== START BLUETOOTH PAIR AND CONNECT TEST  =====')
86
87    def teardown_test(self):
88        self.log.info('Teardown test, shutting down all services...')
89        self.apollo_act.factory_reset()
90        self.apollo.close()
91
92    def _get_device_pair_and_connect_times(self):
93        """Gets the pair and connect times of the phone and buds device.
94
95        Pairs the phone with the buds device. Gets the pair and connect times.
96        Unpairs the devices.
97
98        Returns:
99            pair_time: The time it takes to pair the devices in ms.
100            connection_time: The time it takes to connect the devices for the
101                             first time after pairing.
102
103        Raises:
104            BluetoothPairConnectError
105        """
106
107        try:
108            pair_time = self.bt_utils.bt_pair(self.phone, self.apollo)
109        except BTUtilsError:
110            raise BluetoothPairConnectError('Failed to pair devices')
111
112        pair_time *= 1000
113        connection_start_time = time.perf_counter()
114        if not self.apollo_act.wait_for_bluetooth_a2dp_hfp(30):
115            raise BluetoothPairConnectError('Failed to connect devices')
116        connection_end_time = time.perf_counter()
117        connection_time = (connection_end_time -
118                           connection_start_time) * 1000
119
120        return pair_time, connection_time
121
122    @BluetoothBaseTest.bt_test_wrap
123    @test_tracker_info(uuid='c914fd08-350d-465a-96cf-970d40e71060')
124    def test_bluetooth_connect(self):
125        # Store metrics
126        metrics = {}
127        pair_connect_success = 0
128        pair_connect_failures = []
129        pair_times = []
130        connect_times = []
131        first_connection_failure = None
132
133        for attempt in range(PAIR_CONNECT_ATTEMPTS):
134            self.log.info('Pair and connection attempt {}'.format(attempt + 1))
135            pair_connect_timestamp = time.strftime(logger.log_line_time_format,
136                                                   time.localtime())
137            try:
138                pair_time, connect_time = (self.
139                                           _get_device_pair_and_connect_times())
140            except BluetoothPairConnectError as err:
141                self.log.error(err)
142                failure_data = {'timestamp': pair_connect_timestamp,
143                                'error': str(err),
144                                'pair_and_connect_attempt': attempt + 1}
145                pair_connect_failures.append(failure_data)
146                if not first_connection_failure:
147                    first_connection_failure = err
148            else:
149                connect_times.append(connect_time)
150                pair_times.append(pair_time)
151                pair_connect_success += 1
152
153            factory_reset_bluetooth([self.phone])
154            self.log.info('Factory resetting Apollo device...')
155            self.apollo_act.factory_reset()
156
157            # Buffer between pair and connect attempts
158            time.sleep(3)
159
160        metrics['pair_attempt_count'] = PAIR_CONNECT_ATTEMPTS
161        metrics['pair_successful_count'] = pair_connect_success
162        metrics['pair_failed_count'] = (PAIR_CONNECT_ATTEMPTS -
163                                        pair_connect_success)
164
165        if len(pair_times) > 0:
166            metrics['pair_max_time_millis'] = int(max(pair_times))
167            metrics['pair_min_time_millis'] = int(min(pair_times))
168            metrics['pair_avg_time_millis'] = int(statistics.mean(pair_times))
169
170        if len(connect_times) > 0:
171            metrics['first_connection_max_time_millis'] = int(
172                max(connect_times))
173            metrics['first_connection_min_time_millis'] = int(
174                min(connect_times))
175            metrics['first_connection_avg_time_millis'] = int(
176                (statistics.mean(connect_times)))
177
178        if pair_connect_failures:
179            metrics['pair_conn_failure_info'] = pair_connect_failures
180
181        proto = self.bt_logger.get_results(metrics,
182                                           self.__class__.__name__,
183                                           self.phone,
184                                           self.apollo)
185
186        self.log.info('Metrics: {}'.format(metrics))
187
188        if PAIR_CONNECT_ATTEMPTS != pair_connect_success:
189            raise TestFailure(str(first_connection_failure), extras=proto)
190        else:
191            raise TestPass('Bluetooth pair and connect test passed',
192                           extras=proto)
193