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 disconnect and reconnect verification.""" 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 22from acts import asserts 23from acts.base_test import BaseTestClass 24from acts.controllers.buds_lib.test_actions.apollo_acts import ApolloTestActions 25from acts.signals import TestFailure 26from acts.signals import TestPass 27from acts.test_decorators import test_tracker_info 28from acts.test_utils.bt.BluetoothBaseTest import BluetoothBaseTest 29from acts.test_utils.bt.bt_test_utils import enable_bluetooth 30from acts.test_utils.bt.bt_test_utils import setup_multiple_devices_for_bt_test 31from acts.test_utils.bt.loggers.bluetooth_metric_logger import BluetoothMetricLogger 32from acts.utils import set_location_service 33 34# The number of reconnections to be attempted during the test 35RECONNECTION_ATTEMPTS = 200 36 37 38class BluetoothReconnectError(TestFailure): 39 pass 40 41 42class BluetoothReconnectTest(BaseTestClass): 43 """Connects a phone to Apollo earbuds to test Bluetooth reconnection. 44 45 Attributes: 46 phone: An Android phone object 47 apollo: An Apollo earbuds object 48 apollo_act: An Apollo test action object 49 dut_bt_addr: The Bluetooth address of the Apollo earbuds 50 """ 51 52 def setup_class(self): 53 super().setup_class() 54 # sanity check of the dut devices. 55 # TODO(b/119051823): Investigate using a config validator to replace this. 56 if not self.android_devices: 57 raise ValueError( 58 'Cannot find android phone (need at least one).') 59 self.phone = self.android_devices[0] 60 61 if not self.buds_devices: 62 raise ValueError( 63 'Cannot find apollo device (need at least one).') 64 self.apollo = self.buds_devices[0] 65 self.log.info('Successfully found needed devices.') 66 67 # Staging the test, create result object, etc. 68 self.apollo_act = ApolloTestActions(self.apollo, self.log) 69 self.dut_bt_addr = self.apollo.bluetooth_address 70 self.bt_logger = BluetoothMetricLogger.for_test_case() 71 72 def setup_test(self): 73 setup_multiple_devices_for_bt_test(self.android_devices) 74 # Make sure Bluetooth is on 75 enable_bluetooth(self.phone.droid, self.phone.ed) 76 set_location_service(self.phone, True) 77 self.apollo_act.factory_reset() 78 79 # Initial pairing and connection of devices 80 self.phone.droid.bluetoothDiscoverAndBond(self.dut_bt_addr) 81 paired_and_connected = self.apollo_act.wait_for_bluetooth_a2dp_hfp() 82 asserts.assert_true(paired_and_connected, 83 'Failed to pair and connect devices') 84 time.sleep(20) 85 self.log.info('===== START BLUETOOTH RECONNECT 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 _reconnect_bluetooth_from_phone(self): 93 """Reconnects Bluetooth from the phone. 94 95 Disables and then re-enables Bluetooth from the phone when Bluetooth 96 disconnection has been verified. Measures the reconnection time. 97 98 Returns: 99 The time it takes to connect Bluetooth in milliseconds. 100 101 Raises: 102 BluetoothReconnectError 103 """ 104 105 # Disconnect Bluetooth from the phone side 106 self.log.info('Disconnecting Bluetooth from phone') 107 self.phone.droid.bluetoothDisconnectConnected(self.dut_bt_addr) 108 if not self.apollo_act.wait_for_bluetooth_disconnection(): 109 raise BluetoothReconnectError('Failed to disconnect Bluetooth') 110 self.log.info('Bluetooth disconnected successfully') 111 112 # Buffer between disconnect and reconnect 113 time.sleep(3) 114 115 # Reconnect Bluetooth from the phone side 116 self.log.info('Connecting Bluetooth from phone') 117 start_time = time.perf_counter() 118 self.phone.droid.bluetoothConnectBonded(self.dut_bt_addr) 119 self.log.info('Bluetooth connected successfully') 120 if not self.apollo_act.wait_for_bluetooth_a2dp_hfp(): 121 raise BluetoothReconnectError('Failed to connect Bluetooth') 122 end_time = time.perf_counter() 123 return (end_time - start_time) * 1000 124 125 @BluetoothBaseTest.bt_test_wrap 126 @test_tracker_info(uuid='da921903-92d0-471d-ae01-456058cc1297') 127 def test_bluetooth_reconnect(self): 128 """Reconnects Bluetooth between a phone and Apollo device a specified 129 number of times and reports connection time statistics.""" 130 131 # Store metrics 132 metrics = {} 133 connection_success = 0 134 connection_times = [] 135 reconnection_failures = [] 136 first_connection_failure = None 137 138 for attempt in range(RECONNECTION_ATTEMPTS): 139 self.log.info("Reconnection attempt {}".format(attempt + 1)) 140 reconnect_timestamp = time.strftime('%Y-%m-%d %H:%M:%S', 141 time.localtime()) 142 try: 143 connection_time = self._reconnect_bluetooth_from_phone() 144 except BluetoothReconnectError as err: 145 self.log.error(err) 146 failure_data = {'timestamp': reconnect_timestamp, 147 'error': str(err), 148 'reconnect_attempt': attempt + 1} 149 reconnection_failures.append(failure_data) 150 if not first_connection_failure: 151 first_connection_failure = err 152 else: 153 connection_times.append(connection_time) 154 connection_success += 1 155 156 # Buffer between reconnection attempts 157 time.sleep(3) 158 159 metrics['connection_attempt_count'] = RECONNECTION_ATTEMPTS 160 metrics['connection_successful_count'] = connection_success 161 metrics['connection_failed_count'] = (RECONNECTION_ATTEMPTS 162 - connection_success) 163 if len(connection_times) > 0: 164 metrics['connection_max_time_millis'] = int(max(connection_times)) 165 metrics['connection_min_time_millis'] = int(min(connection_times)) 166 metrics['connection_avg_time_millis'] = int(statistics.mean( 167 connection_times)) 168 169 if reconnection_failures: 170 metrics['connection_failure_info'] = reconnection_failures 171 172 proto = self.bt_logger.get_results(metrics, 173 self.__class__.__name__, 174 self.phone, 175 self.apollo) 176 177 self.log.info('Metrics: {}'.format(metrics)) 178 179 if RECONNECTION_ATTEMPTS != connection_success: 180 raise TestFailure(str(first_connection_failure), extras=proto) 181 else: 182 raise TestPass('Bluetooth reconnect test passed', extras=proto) 183