1#!/usr/bin/env python3
2#
3# Copyright (C) 2016 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"""
17Ble libraries
18"""
19
20import time
21import queue
22import logging
23
24from cert.bt_constants import ble_advertise_settings_modes
25from cert.bt_constants import small_timeout
26from cert.bt_constants import adv_fail
27from cert.bt_constants import adv_succ
28from cert.bt_constants import advertising_set_on_own_address_read
29from cert.bt_constants import advertising_set_started
30from cert.bt_constants import bluetooth_on
31from cert.bt_constants import bluetooth_off
32from cert.bt_constants import bt_default_timeout
33
34
35def enable_bluetooth(droid, ed):
36    if droid.bluetoothCheckState():
37        return True
38
39    droid.bluetoothToggleState(True)
40    expected_bluetooth_on_event_name = bluetooth_on
41    try:
42        ed.pop_event(expected_bluetooth_on_event_name, bt_default_timeout)
43    except Exception:
44        logging.info("Failed to toggle Bluetooth on (no broadcast received)")
45        if droid.bluetoothCheckState():
46            logging.info(".. actual state is ON")
47            return True
48        logging.info(".. actual state is OFF")
49        return False
50
51    return True
52
53
54def disable_bluetooth(droid, ed):
55    if not droid.bluetoothCheckState():
56        return True
57    droid.bluetoothToggleState(False)
58    expected_bluetooth_off_event_name = bluetooth_off
59    try:
60        ed.pop_event(expected_bluetooth_off_event_name, bt_default_timeout)
61    except Exception:
62        logging.info("Failed to toggle Bluetooth off (no broadcast received)")
63        if droid.bluetoothCheckState():
64            logging.info(".. actual state is ON")
65            return False
66        logging.info(".. actual state is OFF")
67        return True
68    return True
69
70
71def generate_ble_scan_objects(droid):
72    """Generate generic LE scan objects.
73
74    Args:
75        droid: The droid object to generate LE scan objects from.
76
77    Returns:
78        filter_list: The generated scan filter list id.
79        scan_settings: The generated scan settings id.
80        scan_callback: The generated scan callback id.
81    """
82    filter_list = droid.bleGenFilterList()
83    scan_settings = droid.bleBuildScanSetting()
84    scan_callback = droid.bleGenScanCallback()
85    return filter_list, scan_settings, scan_callback
86
87
88def generate_ble_advertise_objects(droid):
89    """Generate generic LE advertise objects.
90
91    Args:
92        droid: The droid object to generate advertise LE objects from.
93
94    Returns:
95        advertise_callback: The generated advertise callback id.
96        advertise_data: The generated advertise data id.
97        advertise_settings: The generated advertise settings id.
98    """
99    advertise_callback = droid.bleGenBleAdvertiseCallback()
100    advertise_data = droid.bleBuildAdvertiseData()
101    advertise_settings = droid.bleBuildAdvertiseSettings()
102    return advertise_callback, advertise_data, advertise_settings
103
104
105class BleLib():
106
107    def __init__(self, log, dut):
108        self.advertisement_list = []
109        self.dut = dut
110        self.log = log
111        self.default_timeout = 5
112        self.set_advertisement_list = []
113        self.generic_uuid = "0000{}-0000-1000-8000-00805f9b34fb"
114
115    def _verify_ble_adv_started(self, advertise_callback):
116        """Helper for verifying if an advertisment started or not"""
117        regex = "({}|{})".format(adv_succ.format(advertise_callback), adv_fail.format(advertise_callback))
118        try:
119            event = self.dut.ed.pop_events(regex, 5, small_timeout)
120        except queue.Empty:
121            self.dut.log.error("Failed to get success or failed event.")
122            return
123        if event[0]["name"] == adv_succ.format(advertise_callback):
124            self.dut.log.info("Advertisement started successfully.")
125            return True
126        else:
127            self.dut.log.info("Advertisement failed to start.")
128            return False
129
130    def start_generic_connectable_advertisement(self, line):
131        """Start a connectable LE advertisement"""
132        scan_response = None
133        if line:
134            scan_response = bool(line)
135        self.dut.droid.bleSetAdvertiseSettingsAdvertiseMode(ble_advertise_settings_modes['low_latency'])
136        self.dut.droid.bleSetAdvertiseSettingsIsConnectable(True)
137        advertise_callback, advertise_data, advertise_settings = (generate_ble_advertise_objects(self.dut.droid))
138        if scan_response:
139            self.dut.droid.bleStartBleAdvertisingWithScanResponse(advertise_callback, advertise_data,
140                                                                  advertise_settings, advertise_data)
141        else:
142            self.dut.droid.bleStartBleAdvertising(advertise_callback, advertise_data, advertise_settings)
143        if self._verify_ble_adv_started(advertise_callback):
144            self.log.info("Tracking Callback ID: {}".format(advertise_callback))
145            self.advertisement_list.append(advertise_callback)
146            self.log.info(self.advertisement_list)
147
148    def start_connectable_advertisement_set(self, line):
149        """Start Connectable Advertisement Set"""
150        adv_callback = self.dut.droid.bleAdvSetGenCallback()
151        adv_data = {
152            "includeDeviceName": True,
153        }
154        self.dut.droid.bleAdvSetStartAdvertisingSet({
155            "connectable": True,
156            "legacyMode": False,
157            "primaryPhy": "PHY_LE_1M",
158            "secondaryPhy": "PHY_LE_1M",
159            "interval": 320
160        }, adv_data, None, None, None, 0, 0, adv_callback)
161        evt = self.dut.ed.pop_event(advertising_set_started.format(adv_callback), self.default_timeout)
162        set_id = evt['data']['setId']
163        self.log.error("did not receive the set started event!")
164        evt = self.dut.ed.pop_event(advertising_set_on_own_address_read.format(set_id), self.default_timeout)
165        address = evt['data']['address']
166        self.log.info("Advertiser address is: {}".format(str(address)))
167        self.set_advertisement_list.append(adv_callback)
168
169    def stop_all_advertisement_set(self, line):
170        """Stop all Advertisement Sets"""
171        for adv in self.set_advertisement_list:
172            try:
173                self.dut.droid.bleAdvSetStopAdvertisingSet(adv)
174            except Exception as err:
175                self.log.error("Failed to stop advertisement: {}".format(err))
176
177    def adv_add_service_uuid_list(self, line):
178        """Add service UUID to the LE advertisement inputs:
179         [uuid1 uuid2 ... uuidN]"""
180        uuids = line.split()
181        uuid_list = []
182        for uuid in uuids:
183            if len(uuid) == 4:
184                uuid = self.generic_uuid.format(line)
185            uuid_list.append(uuid)
186        self.dut.droid.bleSetAdvertiseDataSetServiceUuids(uuid_list)
187
188    def adv_data_include_local_name(self, is_included):
189        """Include local name in the advertisement. inputs: [true|false]"""
190        self.dut.droid.bleSetAdvertiseDataIncludeDeviceName(bool(is_included))
191
192    def adv_data_include_tx_power_level(self, is_included):
193        """Include tx power level in the advertisement. inputs: [true|false]"""
194        self.dut.droid.bleSetAdvertiseDataIncludeTxPowerLevel(bool(is_included))
195
196    def adv_data_add_manufacturer_data(self, line):
197        """Include manufacturer id and data to the advertisment:
198        [id data1 data2 ... dataN]"""
199        info = line.split()
200        manu_id = int(info[0])
201        manu_data = []
202        for data in info[1:]:
203            manu_data.append(int(data))
204        self.dut.droid.bleAddAdvertiseDataManufacturerId(manu_id, manu_data)
205
206    def start_generic_nonconnectable_advertisement(self, line):
207        """Start a nonconnectable LE advertisement"""
208        self.dut.droid.bleSetAdvertiseSettingsAdvertiseMode(ble_advertise_settings_modes['low_latency'])
209        self.dut.droid.bleSetAdvertiseSettingsIsConnectable(False)
210        advertise_callback, advertise_data, advertise_settings = (generate_ble_advertise_objects(self.dut.droid))
211        self.dut.droid.bleStartBleAdvertising(advertise_callback, advertise_data, advertise_settings)
212        if self._verify_ble_adv_started(advertise_callback):
213            self.log.info("Tracking Callback ID: {}".format(advertise_callback))
214            self.advertisement_list.append(advertise_callback)
215            self.log.info(self.advertisement_list)
216
217    def stop_all_advertisements(self, line):
218        """Stop all LE advertisements"""
219        for callback_id in self.advertisement_list:
220            self.log.info("Stopping Advertisement {}".format(callback_id))
221            self.dut.droid.bleStopBleAdvertising(callback_id)
222            time.sleep(1)
223        self.advertisement_list = []
224
225    def ble_stop_advertisement(self, callback_id):
226        """Stop an LE advertisement"""
227        if not callback_id:
228            self.log.info("Need a callback ID")
229            return
230        callback_id = int(callback_id)
231        if callback_id not in self.advertisement_list:
232            self.log.info("Callback not in list of advertisements.")
233            return
234        self.dut.droid.bleStopBleAdvertising(callback_id)
235        self.advertisement_list.remove(callback_id)
236
237    def start_max_advertisements(self, line):
238        scan_response = None
239        if line:
240            scan_response = bool(line)
241        while (True):
242            try:
243                self.dut.droid.bleSetAdvertiseSettingsAdvertiseMode(ble_advertise_settings_modes['low_latency'])
244                self.dut.droid.bleSetAdvertiseSettingsIsConnectable(True)
245                advertise_callback, advertise_data, advertise_settings = (generate_ble_advertise_objects(
246                    self.dut.droid))
247                if scan_response:
248                    self.dut.droid.bleStartBleAdvertisingWithScanResponse(advertise_callback, advertise_data,
249                                                                          advertise_settings, advertise_data)
250                else:
251                    self.dut.droid.bleStartBleAdvertising(advertise_callback, advertise_data, advertise_settings)
252                if self._verify_ble_adv_started(advertise_callback):
253                    self.log.info("Tracking Callback ID: {}".format(advertise_callback))
254                    self.advertisement_list.append(advertise_callback)
255                    self.log.info(self.advertisement_list)
256                else:
257                    self.log.info("Advertisements active: {}".format(len(self.advertisement_list)))
258                    return False
259            except Exception as err:
260                self.log.info("Advertisements active: {}".format(len(self.advertisement_list)))
261                return True
262