1# Copyright 2023 Google LLC 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14"""All floss utils functions.""" 15 16import functools 17import logging 18import threading 19import time 20import uuid 21from typing import List, Optional 22import traceback 23 24from floss.pandora.floss import floss_enums 25from gi.repository import GLib 26from google.protobuf import any_pb2 27from pandora import host_pb2 28from pandora_experimental import os_pb2 29 30# All GLIB method calls should wait this many seconds by default 31GLIB_METHOD_CALL_TIMEOUT = 2 32 33# GLib thread name that will run the mainloop. 34GLIB_THREAD_NAME = 'glib' 35 36 37def poll_for_condition(condition, exception=None, timeout=10, sleep_interval=0.1, desc=None): 38 """Polls until a condition is evaluated to true. 39 40 Args: 41 condition: 42 function taking no args and returning anything that will 43 evaluate to True in a conditional check. 44 exception: 45 exception to throw if condition doesn't evaluate to true. 46 timeout: 47 maximum number of seconds to wait. 48 sleep_interval: 49 time to sleep between polls. 50 desc: 51 description of default TimeoutError used if 'exception' is 52 None. 53 54 Raises: 55 TimeoutError: 'exception' arg if supplied; TimeoutError otherwise 56 57 Returns: 58 The evaluated value that caused the poll loop to terminate. 59 """ 60 start_time = time.time() 61 while True: 62 value = condition() 63 if value: 64 return value 65 if time.time() + sleep_interval - start_time > timeout: 66 if exception: 67 logging.error('Will raise error %r due to unexpected return: %r', exception, value) 68 raise exception # pylint: disable=raising-bad-type 69 70 if desc: 71 desc = 'Timed out waiting for condition: ' + desc 72 else: 73 desc = 'Timed out waiting for unnamed condition' 74 logging.error(desc) 75 raise TimeoutError() 76 77 # TODO: b/292696514 - Use event base system and remove this. 78 time.sleep(sleep_interval) 79 80 81def dbus_safe(default_return_value, return_error=False): 82 """Catches all DBus exceptions and return a default value instead. 83 84 Wrap a function with a try block that catches DBus exceptions and returns the error with the specified return 85 status. The exception is logged to aid in debugging. 86 87 If |return_error| is set, the call will return a tuple with (default_return_value, str(error)). 88 89 Args: 90 default_return_value: What value to return in case of errors. 91 return_error: Whether to return the error string as well. 92 93 Returns: 94 Either the return value from the method call if successful or the |default_return_value| or 95 a tuple(default_return_value, str(error)). 96 """ 97 98 def decorator(wrapped_function): 99 """Calls a function and catch DBus errors. 100 101 Args: 102 wrapped_function: Function to call in dbus safe context. 103 104 Returns: 105 Function return value or default_return_value on failure. 106 """ 107 108 @functools.wraps(wrapped_function) 109 def wrapper(*args, **kwargs): 110 """Passes args and kwargs to a dbus safe function. 111 112 Args: 113 args: Formal python arguments. 114 kwargs: Keyword python arguments. 115 116 Returns: 117 Function return value or default_return_value on failure. 118 """ 119 logging.debug('%s()', wrapped_function.__name__) 120 try: 121 return wrapped_function(*args, **kwargs) 122 except GLib.Error as e: 123 logging.debug('Exception while performing operation %s: %s', wrapped_function.__name__, e) 124 125 if return_error: 126 return (default_return_value, str(e)) 127 else: 128 return default_return_value 129 except Exception as e: 130 logging.debug('Exception in %s: %s', wrapped_function.__name__, e) 131 logging.debug(traceback.format_exc()) 132 raise 133 134 return wrapper 135 136 return decorator 137 138 139def generate_dbus_cb_objpath(name, hci=None): 140 """Generates a DBus callbacks object path with a suffix that won't conflict. 141 142 Args: 143 name: 144 The last component of the path. Note that the suffix is appended right after this. 145 hci: 146 The hci number. If specified, an additional 'hciX' component is added before @name. 147 148 Returns: 149 dbus callback object path. 150 """ 151 time_ms = int(time.time() * 1000) 152 if hci is None: 153 return '/org/chromium/bluetooth/{}{}'.format(name, time_ms) 154 return '/org/chromium/bluetooth/hci{}/{}{}'.format(hci, name, time_ms) 155 156 157def dbus_optional_value(value_format, value): 158 """Makes a struct for optional value D-Bus. 159 160 Args: 161 value_format: 162 D-Bus format string (ex: a{sv}). 163 value: 164 The value to convert. 165 166 Returns: 167 An empty dictionary if value is None, otherwise dictionary 168 of optional value. 169 """ 170 if not value: 171 return {} 172 return {'optional_value': GLib.Variant(value_format, value)} 173 174 175def make_kv_optional_value(value): 176 """Makes a struct for optional value D-Bus with 'a{sv}' format. 177 178 Args: 179 value: 180 The value to convert. 181 182 Returns: 183 An empty dictionary if value is None, otherwise dictionary 184 of optional value. 185 """ 186 return dbus_optional_value('a{sv}', value) 187 188 189class GlibDeadlockException(Exception): 190 """Detected a situation that will cause a deadlock in GLib. 191 192 This exception should be emitted when we detect that a deadlock is likely to 193 occur. For example, a method call running in the mainloop context is making 194 a function call that is wrapped with @glib_call. 195 """ 196 pass 197 198 199def glib_call(default_result=None, timeout=GLIB_METHOD_CALL_TIMEOUT, thread_name=GLIB_THREAD_NAME): 200 """Threads method call to glib thread and waits for result. 201 202 The dbus-python package does not support multi-threaded access. As a result, 203 we pipe all dbus function to the mainloop using GLib.idle_add which runs the 204 method as part of the mainloop. 205 206 Args: 207 default_result: 208 The default return value from the function call if it fails or times out. 209 timeout: 210 How long to wait for the method call to complete. 211 thread_name: 212 Name of the thread that should be running GLib.Mainloop. 213 """ 214 215 def decorator(method): 216 """Internal wrapper.""" 217 218 def call_and_signal(data): 219 """Calls a function and signals completion. 220 221 This method is called by GLib and added via GLib.idle_add. It will 222 be run in the same thread as the GLib mainloop. 223 224 Args: 225 data: 226 Dict containing data to be passed. Must have keys: 227 event, method, args, kwargs and result. The value for 228 result should be the default value and will be set 229 before return. 230 231 Returns: 232 False so that glib doesn't reschedule this to run again. 233 """ 234 (event, method, args, kwargs) = (data['event'], data['method'], data['args'], data['kwargs']) 235 logging.info('%s: Running %s', threading.current_thread().name, str(method)) 236 err = None 237 try: 238 data['result'] = method(*args, **kwargs) 239 except Exception as e: 240 logging.error('Exception during %s: %s', str(method), str(e)) 241 err = e 242 243 event.set() 244 245 # If method callback is set, this will call that method with results 246 # of this method call and any error that may have resulted. 247 if 'method_callback' in data: 248 data['method_callback'](err, data['result']) 249 250 return False 251 252 @functools.wraps(method) 253 def wrapper(*args, **kwargs): 254 """Sends method call to GLib and waits for its completion. 255 256 Args: 257 *args: 258 Positional arguments to method. 259 **kwargs: 260 Keyword arguments to method. Some special keywords: 261 |method_callback|: Returns result via callback without blocking. 262 """ 263 264 method_callback = None 265 # If a method callback is given, we will not block on the completion 266 # of the call but expect the response in the callback instead. The 267 # callback has the signature: def callback(err, result) 268 if 'method_callback' in kwargs: 269 method_callback = kwargs['method_callback'] 270 del kwargs['method_callback'] 271 272 # Make sure we're not scheduling in the GLib thread since that'll 273 # cause a deadlock. An exception is if we have a method callback 274 # which is async. 275 current_thread_name = threading.current_thread().name 276 if current_thread_name is thread_name and not method_callback: 277 raise GlibDeadlockException('{} called in GLib thread'.format(method)) 278 279 done_event = threading.Event() 280 data = { 281 'event': done_event, 282 'method': method, 283 'args': args, 284 'kwargs': kwargs, 285 'result': default_result, 286 } 287 if method_callback: 288 data['method_callback'] = method_callback 289 290 logging.info('%s: Adding %s to GLib.idle_add', threading.current_thread().name, str(method)) 291 GLib.idle_add(call_and_signal, data) 292 293 if not method_callback: 294 # Wait for the result from the GLib call 295 if not done_event.wait(timeout=timeout): 296 logging.warning('%s timed out after %d s', str(method), timeout) 297 298 return data['result'] 299 300 return wrapper 301 302 return decorator 303 304 305def glib_callback(thread_name=GLIB_THREAD_NAME): 306 """Marks callbacks that are called by GLib and checks for errors.""" 307 308 def _decorator(method): 309 310 @functools.wraps(method) 311 def _wrapper(*args, **kwargs): 312 current_thread_name = threading.current_thread().name 313 if current_thread_name is not thread_name: 314 raise GlibDeadlockException('{} should be called by GLib'.format(method)) 315 316 return method(*args, **kwargs) 317 318 return _wrapper 319 320 return _decorator 321 322 323class PropertySet: 324 """Helper class with getters and setters for properties.""" 325 326 class MissingProperty(Exception): 327 """Raised when property is missing in PropertySet.""" 328 pass 329 330 class PropertyGetterMissing(Exception): 331 """Raised when get is called on a property that doesn't support it.""" 332 pass 333 334 class PropertySetterMissing(Exception): 335 """Raised when set is called on a property that doesn't support it.""" 336 pass 337 338 def __init__(self, property_set): 339 """Constructor. 340 341 Args: 342 property_set: 343 Dictionary with proxy methods for get/set of named 344 properties. These are NOT normal DBus properties 345 that are implemented via org.freedesktop.DBus.Properties. 346 """ 347 self.pset = property_set 348 349 def get_property_names(self): 350 """Gets all registered properties names.""" 351 352 return self.pset.keys() 353 354 def get(self, prop_name, *args): 355 """Calls the getter function for a property if it exists. 356 357 Args: 358 prop_name: 359 The property name to call the getter function on. 360 *args: 361 Any positional arguments to pass to getter function. 362 363 Raises: 364 self.MissingProperty: Raised when property is missing in PropertySet. 365 self.PropertyGetterMissing: Raised when get is called on a property that doesn't support it. 366 367 Returns: 368 Result from calling the getter function with given args. 369 """ 370 if prop_name not in self.pset: 371 raise self.MissingProperty('{} is unknown.'.format(prop_name)) 372 373 (getter, _) = self.pset[prop_name] 374 375 if not getter: 376 raise self.PropertyGetterMissing('{} has no getter.'.format(prop_name)) 377 378 return getter(*args) 379 380 def set(self, prop_name, *args): 381 """Calls the setter function for a property if it exists. 382 383 Args: 384 prop_name: 385 The property name to call the setter function on. 386 *args: 387 Any positional arguments to pass to the setter function. 388 389 Raises: 390 self.MissingProperty: Raised when property is missing in PropertySet. 391 self.PropertySetterMissing: Raised when set is called on a property that doesn't support it. 392 393 Returns: 394 Result from calling the setter function with given args. 395 """ 396 if prop_name not in self.pset: 397 raise self.MissingProperty('{} is unknown.'.format(prop_name)) 398 399 (_, setter) = self.pset[prop_name] 400 401 if not setter: 402 raise self.PropertySetterMissing('{} has no getter.'.format(prop_name)) 403 404 return setter(*args) 405 406 407class Connection: 408 """A Bluetooth connection.""" 409 410 def __init__(self, address: str, transport: floss_enums.BtTransport): 411 self.address = address 412 self.transport = transport 413 414 415def connection_to(connection: Connection): 416 """Converts Connection from Floss format to gRPC format.""" 417 internal_connection_ref = os_pb2.InternalConnectionRef(address=address_to(connection.address), 418 transport=connection.transport) 419 cookie = any_pb2.Any(value=internal_connection_ref.SerializeToString()) 420 return host_pb2.Connection(cookie=cookie) 421 422 423def connection_from(connection: host_pb2.Connection): 424 """Converts Connection from gRPC format to Floss format.""" 425 internal_connection_ref = os_pb2.InternalConnectionRef() 426 internal_connection_ref.ParseFromString(connection.cookie.value) 427 return Connection(address=address_from(internal_connection_ref.address), 428 transport=internal_connection_ref.transport) 429 430 431def address_from(request_address: bytes): 432 """Converts address from gRPC format to Floss format.""" 433 address = request_address.hex() 434 address = f'{address[:2]}:{address[2:4]}:{address[4:6]}:{address[6:8]}:{address[8:10]}:{address[10:12]}' 435 return address.upper() 436 437 438def address_to(address: str): 439 """Converts address from Floss format to gRPC format.""" 440 request_address = bytes.fromhex(address.replace(':', '')) 441 return request_address 442 443 444def uuid16_to_uuid128(uuid16: str): 445 return f'0000{uuid16}-0000-1000-8000-00805f9b34fb' 446 447 448def uuid32_to_uuid128(uuid32: str): 449 return f'{uuid32}-0000-1000-8000-00805f9b34fb' 450 451 452def get_uuid_as_list(str_uuid): 453 """Converts string uuid to a list of bytes. 454 455 Args: 456 str_uuid: String UUID. 457 458 Returns: 459 UUID string as list of bytes. 460 """ 461 return list(uuid.UUID(str_uuid).bytes) 462 463 464def advertise_data_from(request_data: host_pb2.DataTypes): 465 """Mapping DataTypes to a dict. 466 467 The dict content follows the format of Floss AdvertiseData. 468 469 Args: 470 request_data : advertising data. 471 472 Raises: 473 NotImplementedError: if request data is not implemented. 474 475 Returns: 476 dict: advertising data. 477 """ 478 advertise_data = { 479 'service_uuids': [], 480 'solicit_uuids': [], 481 'transport_discovery_data': [], 482 'manufacturer_data': {}, 483 'service_data': {}, 484 'include_tx_power_level': False, 485 'include_device_name': False, 486 } 487 488 # incomplete_service_class_uuids 489 if (request_data.incomplete_service_class_uuids16 or request_data.incomplete_service_class_uuids32 or 490 request_data.incomplete_service_class_uuids128): 491 raise NotImplementedError('Incomplete service class uuid not supported') 492 493 # service_uuids 494 for uuid16 in request_data.complete_service_class_uuids16: 495 advertise_data['service_uuids'].append(uuid16_to_uuid128(uuid16)) 496 497 for uuid32 in request_data.complete_service_class_uuids32: 498 advertise_data['service_uuids'].append(uuid32_to_uuid128(uuid32)) 499 500 for uuid128 in request_data.complete_service_class_uuids128: 501 advertise_data['service_uuids'].append(uuid128) 502 503 # solicit_uuids 504 for uuid16 in request_data.service_solicitation_uuids16: 505 advertise_data['solicit_uuids'].append(uuid16_to_uuid128(uuid16)) 506 507 for uuid32 in request_data.service_solicitation_uuids32: 508 advertise_data['solicit_uuids'].append(uuid32_to_uuid128(uuid32)) 509 510 for uuid128 in request_data.service_solicitation_uuids128: 511 advertise_data['solicit_uuids'].append(uuid128) 512 513 # service_data 514 for (uuid16, data) in request_data.service_data_uuid16: 515 advertise_data['service_data'][uuid16_to_uuid128(uuid16)] = data 516 517 for (uuid32, data) in request_data.service_data_uuid32: 518 advertise_data['service_data'][uuid32_to_uuid128(uuid32)] = data 519 520 for (uuid128, data) in request_data.service_data_uuid128: 521 advertise_data['service_data'][uuid128] = data 522 523 advertise_data['manufacturer_data'][hex( 524 floss_enums.CompanyIdentifiers.GOOGLE)] = request_data.manufacturer_specific_data 525 526 # The name is derived from adapter directly in floss. 527 if request_data.WhichOneof('shortened_local_name_oneof') in ('include_shortened_local_name', 528 'include_complete_local_name'): 529 advertise_data['include_device_name'] = getattr(request_data, 530 request_data.WhichOneof('shortened_local_name_oneof')).value 531 532 # The tx power level is decided by the lower layers. 533 if request_data.WhichOneof('tx_power_level_oneof') == 'include_tx_power_level': 534 advertise_data['include_tx_power_level'] = request_data.include_tx_power_level 535 return advertise_data 536 537 538def create_observer_name(observer): 539 """Generates an unique name for an observer. 540 541 Args: 542 observer: an observer class to observer the bluetooth callbacks. 543 544 Returns: 545 str: an unique name. 546 """ 547 return observer.__class__.__name__ + str(id(observer)) 548 549 550# anext build-in is new in python3.10. Deprecate this function 551# when we are able to use it. 552async def anext(ait): 553 return await ait.__anext__() 554 555 556def parse_advertiging_data(adv_data: List[int]) -> host_pb2.DataTypes: 557 index = 0 558 data = host_pb2.DataTypes() 559 560 # advertising data packet is repeated by many advertising data and each one with the following format: 561 # | Length (0) | Type (1) | Data Payload (2~N-1) 562 # Take an advertising data packet [2, 1, 6, 5, 3, 0, 24, 1, 24] as an example, 563 # The first 3 numbers 2, 1, 6: 564 # 2 is the data length is 2 including the data type, 565 # 1 is the data type is Flags (0x01), 566 # 6 is the data payload. 567 while index < len(adv_data): 568 # Extract data length. 569 data_length = adv_data[index] 570 index = index + 1 571 572 if data_length <= 0: 573 break 574 575 # Extract data type. 576 data_type = adv_data[index] 577 index = index + 1 578 579 # Extract data payload. 580 if data_type == floss_enums.AdvertisingDataType.COMPLETE_LOCAL_NAME: 581 data.complete_local_name = parse_complete_local_name(adv_data[index:index + data_length - 1]) 582 logging.info('complete_local_name: %s', data.complete_local_name) 583 else: 584 logging.debug('Unsupported advertising data type to parse: %s', data_type) 585 586 index = index + data_length - 1 587 588 logging.info('Parsed data: %s', data) 589 return data 590 591 592def parse_complete_local_name(data: List[int]) -> Optional[str]: 593 return ''.join(chr(char) for char in data) if data else None 594