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
17import statistics
18from acts import asserts
19from acts.base_test import BaseTestClass
20from acts.signals import TestPass
21from acts.test_utils.bt.BluetoothBaseTest import BluetoothBaseTest
22from acts.test_utils.bt.bt_test_utils import orchestrate_rfcomm_connection
23from acts.test_utils.bt.bt_test_utils import setup_multiple_devices_for_bt_test
24from acts.test_utils.bt.bt_test_utils import verify_server_and_client_connected
25from acts.test_utils.bt.loggers.bluetooth_metric_logger import BluetoothMetricLogger
26from acts.test_utils.bt.loggers.protos import bluetooth_metric_pb2 as proto_module
27from acts.utils import set_location_service
28
29
30class BluetoothThroughputTest(BaseTestClass):
31    """Connects two Android phones and tests the throughput between them.
32
33    Attributes:
34         client_device: An Android device object that will be sending data
35         server_device: An Android device object that will be receiving data
36         bt_logger: The proxy logger instance for each test case
37         data_transfer_type: Data transfer protocol used for the test
38    """
39
40    def setup_class(self):
41        super().setup_class()
42
43        # Sanity check of the devices under test
44        # TODO(b/119051823): Investigate using a config validator to replace this.
45        if len(self.android_devices) < 2:
46            raise ValueError(
47                'Not enough android phones detected (need at least two)')
48
49        # Data will be sent from the client_device to the server_device
50        self.client_device = self.android_devices[0]
51        self.server_device = self.android_devices[1]
52        self.bt_logger = BluetoothMetricLogger.for_test_case()
53        self.data_transfer_type = proto_module.BluetoothDataTestResult.RFCOMM
54        self.log.info('Successfully found required devices.')
55
56    def setup_test(self):
57        setup_multiple_devices_for_bt_test(self.android_devices)
58        self._connect_rfcomm()
59
60    def teardown_test(self):
61        if verify_server_and_client_connected(
62                self.client_device, self.server_device, log=False):
63            self.client_device.droid.bluetoothSocketConnStop()
64            self.server_device.droid.bluetoothSocketConnStop()
65
66    def _connect_rfcomm(self):
67        """Establishes an RFCOMM connection between two phones.
68
69        Connects the client device to the server device given the hardware
70        address of the server device.
71        """
72
73        set_location_service(self.client_device, True)
74        set_location_service(self.server_device, True)
75        server_address = self.server_device.droid.bluetoothGetLocalAddress()
76        self.log.info('Pairing and connecting devices')
77        asserts.assert_true(self.client_device.droid
78                            .bluetoothDiscoverAndBond(server_address),
79                            'Failed to pair and connect devices')
80
81        # Create RFCOMM connection
82        asserts.assert_true(orchestrate_rfcomm_connection
83                            (self.client_device, self.server_device),
84                            'Failed to establish RFCOMM connection')
85
86    def _measure_throughput(self, num_of_buffers, buffer_size):
87        """Measures the throughput of a data transfer.
88
89        Sends data from the client device that is read by the server device.
90        Calculates the throughput for the transfer.
91
92        Args:
93            num_of_buffers: An integer value designating the number of buffers
94                          to be sent.
95            buffer_size: An integer value designating the size of each buffer,
96                       in bytes.
97
98        Returns:
99            The throughput of the transfer in bytes per second.
100        """
101
102        # TODO(b/119638242): Need to fix throughput send/receive methods
103        (self.client_device.droid
104         .bluetoothConnectionThroughputSend(num_of_buffers, buffer_size))
105
106        throughput = (self.server_device.droid
107                      .bluetoothConnectionThroughputRead(num_of_buffers,
108                                                         buffer_size))
109        return throughput
110
111    @BluetoothBaseTest.bt_test_wrap
112    def test_bluetooth_throughput_large_buffer(self):
113        """Tests the throughput over a series of data transfers with large
114        buffer size.
115        """
116
117        metrics = {}
118        throughput_list = []
119
120        for transfer in range(300):
121            throughput = self._measure_throughput(1, 300)
122            self.log.info('Throughput: {} bytes-per-sec'.format(throughput))
123            throughput_list.append(throughput)
124
125        metrics['data_transfer_protocol'] = self.data_transfer_type
126        metrics['data_packet_size'] = 300
127        metrics['data_throughput_min_bytes_per_second'] = int(
128            min(throughput_list))
129        metrics['data_throughput_max_bytes_per_second'] = int(
130            max(throughput_list))
131        metrics['data_throughput_avg_bytes_per_second'] = int(statistics.mean(
132            throughput_list))
133
134        proto = self.bt_logger.get_results(metrics,
135                                           self.__class__.__name__,
136                                           self.server_device,
137                                           self.client_device)
138
139        asserts.assert_true(metrics['data_throughput_min_bytes_per_second'] > 0,
140                            'Minimum throughput must be greater than 0!',
141                            extras=proto)
142        raise TestPass('Throughput test (large buffer) completed successfully',
143                       extras=proto)
144
145    @BluetoothBaseTest.bt_test_wrap
146    def test_bluetooth_throughput_medium_buffer(self):
147        """Tests the throughput over a series of data transfers with medium
148        buffer size.
149        """
150
151        metrics = {}
152        throughput_list = []
153
154        for transfer in range(300):
155            throughput = self._measure_throughput(1, 100)
156            self.log.info('Throughput: {} bytes-per-sec'.format(throughput))
157            throughput_list.append(throughput)
158
159        metrics['data_transfer_protocol'] = self.data_transfer_type
160        metrics['data_packet_size'] = 100
161        metrics['data_throughput_min_bytes_per_second'] = int(
162            min(throughput_list))
163        metrics['data_throughput_max_bytes_per_second'] = int(
164            max(throughput_list))
165        metrics['data_throughput_avg_bytes_per_second'] = int(statistics.mean(
166            throughput_list))
167
168        proto = self.bt_logger.get_results(metrics,
169                                           self.__class__.__name__,
170                                           self.server_device,
171                                           self.client_device)
172
173        asserts.assert_true(metrics['data_throughput_min_bytes_per_second'] > 0,
174                            'Minimum throughput must be greater than 0!',
175                            extras=proto)
176        raise TestPass('Throughput test (medium buffer) completed successfully',
177                       extras=proto)
178
179    @BluetoothBaseTest.bt_test_wrap
180    def test_bluetooth_throughput_small_buffer(self):
181        """Tests the throughput over a series of data transfers with small
182        buffer size.
183        """
184
185        metrics = {}
186        throughput_list = []
187
188        for transfer in range(300):
189            throughput = self._measure_throughput(1, 10)
190            self.log.info('Throughput: {} bytes-per-sec'.format(throughput))
191            throughput_list.append(throughput)
192
193        metrics['data_transfer_protocol'] = self.data_transfer_type
194        metrics['data_packet_size'] = 10
195        metrics['data_throughput_min_bytes_per_second'] = int(
196            min(throughput_list))
197        metrics['data_throughput_max_bytes_per_second'] = int(
198            max(throughput_list))
199        metrics['data_throughput_avg_bytes_per_second'] = int(statistics.mean(
200            throughput_list))
201
202        proto = self.bt_logger.get_results(metrics,
203                                           self.__class__.__name__,
204                                           self.server_device,
205                                           self.client_device)
206
207        asserts.assert_true(metrics['data_throughput_min_bytes_per_second'] > 0,
208                            'Minimum throughput must be greater than 0!',
209                            extras=proto)
210        raise TestPass('Throughput test (small buffer) completed successfully',
211                       extras=proto)
212
213    @BluetoothBaseTest.bt_test_wrap
214    def test_maximum_buffer_size(self):
215        """Calculates the maximum allowed buffer size for one packet."""
216
217        current_buffer_size = 1
218        while True:
219            self.log.info('Trying buffer size {}'.format(current_buffer_size))
220            try:
221                throughput = self._measure_throughput(1, current_buffer_size)
222            except Exception:
223                buffer_msg = ('Max buffer size: {} bytes'.
224                              format(current_buffer_size - 1))
225                throughput_msg = ('Max throughput: {} bytes-per-second'.
226                                  format(throughput))
227                self.log.info(buffer_msg)
228                self.log.info(throughput_msg)
229                return True
230            current_buffer_size += 1
231