1#!/usr/bin/env python 2 3# Copyright (c) 2013 The Chromium OS Authors. All rights reserved. 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6 7import base64 8import dbus 9import dbus.mainloop.glib 10import dbus.service 11import gobject 12import json 13import logging 14import logging.handlers 15import os 16import shutil 17 18import common 19from autotest_lib.client.bin import utils 20from autotest_lib.client.common_lib.cros.bluetooth import bluetooth_socket 21from autotest_lib.client.cros import constants 22from autotest_lib.client.cros import xmlrpc_server 23from autotest_lib.client.cros.bluetooth import advertisement 24from autotest_lib.client.cros.bluetooth import output_recorder 25 26 27def _dbus_byte_array_to_b64_string(dbus_byte_array): 28 """Base64 encodes a dbus byte array for use with the xml rpc proxy.""" 29 return base64.standard_b64encode(bytearray(dbus_byte_array)) 30 31 32def _b64_string_to_dbus_byte_array(b64_string): 33 """Base64 decodes a dbus byte array for use with the xml rpc proxy.""" 34 dbus_array = dbus.Array([], signature=dbus.Signature('y')) 35 bytes = bytearray(base64.standard_b64decode(b64_string)) 36 for byte in bytes: 37 dbus_array.append(dbus.Byte(byte)) 38 return dbus_array 39 40 41class PairingAgent(dbus.service.Object): 42 """The agent handling the authentication process of bluetooth pairing. 43 44 PairingAgent overrides RequestPinCode method to return a given pin code. 45 User can use this agent to pair bluetooth device which has a known 46 pin code. 47 48 TODO (josephsih): more pairing modes other than pin code would be 49 supported later. 50 51 """ 52 53 def __init__(self, pin, *args, **kwargs): 54 super(PairingAgent, self).__init__(*args, **kwargs) 55 self._pin = pin 56 57 58 @dbus.service.method('org.bluez.Agent1', 59 in_signature='o', out_signature='s') 60 def RequestPinCode(self, device_path): 61 """Requests pin code for a device. 62 63 Returns the known pin code for the request. 64 65 @param device_path: The object path of the device. 66 67 @returns: The known pin code. 68 69 """ 70 logging.info('RequestPinCode for %s; return %s', device_path, self._pin) 71 return self._pin 72 73 74class BluetoothDeviceXmlRpcDelegate(xmlrpc_server.XmlRpcDelegate): 75 """Exposes DUT methods called remotely during Bluetooth autotests. 76 77 All instance methods of this object without a preceding '_' are exposed via 78 an XML-RPC server. This is not a stateless handler object, which means that 79 if you store state inside the delegate, that state will remain around for 80 future calls. 81 """ 82 83 UPSTART_PATH = 'unix:abstract=/com/ubuntu/upstart' 84 UPSTART_MANAGER_PATH = '/com/ubuntu/Upstart' 85 UPSTART_MANAGER_IFACE = 'com.ubuntu.Upstart0_6' 86 UPSTART_JOB_IFACE = 'com.ubuntu.Upstart0_6.Job' 87 88 UPSTART_ERROR_UNKNOWNINSTANCE = \ 89 'com.ubuntu.Upstart0_6.Error.UnknownInstance' 90 UPSTART_ERROR_ALREADYSTARTED = \ 91 'com.ubuntu.Upstart0_6.Error.AlreadyStarted' 92 93 BLUETOOTHD_JOB = 'bluetoothd' 94 95 DBUS_ERROR_SERVICEUNKNOWN = 'org.freedesktop.DBus.Error.ServiceUnknown' 96 97 BLUEZ_SERVICE_NAME = 'org.bluez' 98 BLUEZ_MANAGER_PATH = '/' 99 BLUEZ_MANAGER_IFACE = 'org.freedesktop.DBus.ObjectManager' 100 BLUEZ_ADAPTER_IFACE = 'org.bluez.Adapter1' 101 BLUEZ_DEVICE_IFACE = 'org.bluez.Device1' 102 BLUEZ_GATT_IFACE = 'org.bluez.GattCharacteristic1' 103 BLUEZ_LE_ADVERTISING_MANAGER_IFACE = 'org.bluez.LEAdvertisingManager1' 104 BLUEZ_AGENT_MANAGER_PATH = '/org/bluez' 105 BLUEZ_AGENT_MANAGER_IFACE = 'org.bluez.AgentManager1' 106 BLUEZ_PROFILE_MANAGER_PATH = '/org/bluez' 107 BLUEZ_PROFILE_MANAGER_IFACE = 'org.bluez.ProfileManager1' 108 BLUEZ_ERROR_ALREADY_EXISTS = 'org.bluez.Error.AlreadyExists' 109 DBUS_PROP_IFACE = 'org.freedesktop.DBus.Properties' 110 AGENT_PATH = '/test/agent' 111 112 BLUETOOTH_LIBDIR = '/var/lib/bluetooth' 113 BTMON_STOP_DELAY_SECS = 3 114 115 # Timeout for how long we'll wait for BlueZ and the Adapter to show up 116 # after reset. 117 ADAPTER_TIMEOUT = 30 118 119 def __init__(self): 120 super(BluetoothDeviceXmlRpcDelegate, self).__init__() 121 122 # Open the Bluetooth Raw socket to the kernel which provides us direct, 123 # raw, access to the HCI controller. 124 self._raw = bluetooth_socket.BluetoothRawSocket() 125 126 # Open the Bluetooth Control socket to the kernel which provides us 127 # raw management access to the Bluetooth Host Subsystem. Read the list 128 # of adapter indexes to determine whether or not this device has a 129 # Bluetooth Adapter or not. 130 self._control = bluetooth_socket.BluetoothControlSocket() 131 self._has_adapter = len(self._control.read_index_list()) > 0 132 133 # Set up the connection to Upstart so we can start and stop services 134 # and fetch the bluetoothd job. 135 self._upstart_conn = dbus.connection.Connection(self.UPSTART_PATH) 136 self._upstart = self._upstart_conn.get_object( 137 None, 138 self.UPSTART_MANAGER_PATH) 139 140 bluetoothd_path = self._upstart.GetJobByName( 141 self.BLUETOOTHD_JOB, 142 dbus_interface=self.UPSTART_MANAGER_IFACE) 143 self._bluetoothd = self._upstart_conn.get_object( 144 None, 145 bluetoothd_path) 146 147 # Arrange for the GLib main loop to be the default. 148 dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) 149 150 # Set up the connection to the D-Bus System Bus, get the object for 151 # the Bluetooth Userspace Daemon (BlueZ) and that daemon's object for 152 # the Bluetooth Adapter, and the advertising manager. 153 self._system_bus = dbus.SystemBus() 154 self._update_bluez() 155 self._update_adapter() 156 self._update_advertising() 157 158 # The agent to handle pin code request, which will be 159 # created when user calls pair_legacy_device method. 160 self._pairing_agent = None 161 # The default capability of the agent. 162 self._capability = 'KeyboardDisplay' 163 164 # Initailize a btmon object to record bluetoothd's activity. 165 self.btmon = output_recorder.OutputRecorder( 166 'btmon', stop_delay_secs=self.BTMON_STOP_DELAY_SECS) 167 168 self.advertisements = [] 169 self._adv_mainloop = gobject.MainLoop() 170 171 172 @xmlrpc_server.dbus_safe(False) 173 def start_bluetoothd(self): 174 """start bluetoothd. 175 176 This includes powering up the adapter. 177 178 @returns: True if bluetoothd is started correctly. 179 False otherwise. 180 181 """ 182 try: 183 self._bluetoothd.Start(dbus.Array(signature='s'), True, 184 dbus_interface=self.UPSTART_JOB_IFACE) 185 except dbus.exceptions.DBusException as e: 186 # if bluetoothd was already started, the exception looks like 187 # dbus.exceptions.DBusException: 188 # com.ubuntu.Upstart0_6.Error.AlreadyStarted: Job is already 189 # running: bluetoothd 190 if e.get_dbus_name() != self.UPSTART_ERROR_ALREADYSTARTED: 191 logging.error('Error starting bluetoothd: %s', e) 192 return False 193 194 logging.debug('waiting for bluez start') 195 try: 196 utils.poll_for_condition( 197 condition=self._update_bluez, 198 desc='Bluetooth Daemon has started.', 199 timeout=self.ADAPTER_TIMEOUT) 200 except Exception as e: 201 logging.error('timeout: error starting bluetoothd: %s', e) 202 return False 203 204 # Waiting for the self._adapter object. 205 # This does not mean that the adapter is powered on. 206 logging.debug('waiting for bluez to obtain adapter information') 207 try: 208 utils.poll_for_condition( 209 condition=self._update_adapter, 210 desc='Bluetooth Daemon has adapter information.', 211 timeout=self.ADAPTER_TIMEOUT) 212 except Exception as e: 213 logging.error('timeout: error starting adapter: %s', e) 214 return False 215 216 # Waiting for the self._advertising interface object. 217 logging.debug('waiting for bluez to obtain interface manager.') 218 try: 219 utils.poll_for_condition( 220 condition=self._update_advertising, 221 desc='Bluetooth Daemon has advertising interface.', 222 timeout=self.ADAPTER_TIMEOUT) 223 except utils.TimeoutError: 224 logging.error('timeout: error getting advertising interface') 225 return False 226 227 return True 228 229 230 @xmlrpc_server.dbus_safe(False) 231 def stop_bluetoothd(self): 232 """stop bluetoothd. 233 234 @returns: True if bluetoothd is stopped correctly. 235 False otherwise. 236 237 """ 238 def bluez_stopped(): 239 """Checks the bluetooth daemon status. 240 241 @returns: True if bluez is stopped. False otherwise. 242 243 """ 244 return not self._update_bluez() 245 246 try: 247 self._bluetoothd.Stop(dbus.Array(signature='s'), True, 248 dbus_interface=self.UPSTART_JOB_IFACE) 249 except dbus.exceptions.DBusException as e: 250 # If bluetoothd was stopped already, the exception looks like 251 # dbus.exceptions.DBusException: 252 # com.ubuntu.Upstart0_6.Error.UnknownInstance: Unknown instance: 253 if e.get_dbus_name() != self.UPSTART_ERROR_UNKNOWNINSTANCE: 254 logging.error('Error stopping bluetoothd!') 255 return False 256 257 logging.debug('waiting for bluez stop') 258 try: 259 utils.poll_for_condition( 260 condition=bluez_stopped, 261 desc='Bluetooth Daemon has stopped.', 262 timeout=self.ADAPTER_TIMEOUT) 263 bluetoothd_stopped = True 264 except Exception as e: 265 logging.error('timeout: error stopping bluetoothd: %s', e) 266 bluetoothd_stopped = False 267 268 return bluetoothd_stopped 269 270 271 def is_bluetoothd_running(self): 272 """Is bluetoothd running? 273 274 @returns: True if bluetoothd is running 275 276 """ 277 return bool(self._get_dbus_proxy_for_bluetoothd()) 278 279 280 def _update_bluez(self): 281 """Store a D-Bus proxy for the Bluetooth daemon in self._bluez. 282 283 This may be called in a loop until it returns True to wait for the 284 daemon to be ready after it has been started. 285 286 @return True on success, False otherwise. 287 288 """ 289 self._bluez = self._get_dbus_proxy_for_bluetoothd() 290 return bool(self._bluez) 291 292 293 @xmlrpc_server.dbus_safe(False) 294 def _get_dbus_proxy_for_bluetoothd(self): 295 """Get the D-Bus proxy for the Bluetooth daemon. 296 297 @return True on success, False otherwise. 298 299 """ 300 bluez = None 301 try: 302 bluez = self._system_bus.get_object(self.BLUEZ_SERVICE_NAME, 303 self.BLUEZ_MANAGER_PATH) 304 logging.debug('bluetoothd is running') 305 except dbus.exceptions.DBusException as e: 306 # When bluetoothd is not running, the exception looks like 307 # dbus.exceptions.DBusException: 308 # org.freedesktop.DBus.Error.ServiceUnknown: The name org.bluez 309 # was not provided by any .service files 310 if e.get_dbus_name() == self.DBUS_ERROR_SERVICEUNKNOWN: 311 logging.debug('bluetoothd is not running') 312 else: 313 logging.error('Error getting dbus proxy for Bluez: %s', e) 314 return bluez 315 316 317 def _update_adapter(self): 318 """Store a D-Bus proxy for the local adapter in self._adapter. 319 320 This may be called in a loop until it returns True to wait for the 321 daemon to be ready, and have obtained the adapter information itself, 322 after it has been started. 323 324 Since not all devices will have adapters, this will also return True 325 in the case where we have obtained an empty adapter index list from the 326 kernel. 327 328 Note that this method does not power on the adapter. 329 330 @return True on success, including if there is no local adapter, 331 False otherwise. 332 333 """ 334 self._adapter = None 335 if self._bluez is None: 336 logging.warning('Bluez not found!') 337 return False 338 if not self._has_adapter: 339 logging.debug('Device has no adapter; returning') 340 return True 341 self._adapter = self._get_adapter() 342 return bool(self._adapter) 343 344 def _update_advertising(self): 345 """Store a D-Bus proxy for the local advertising interface manager. 346 347 This may be called repeatedly in a loop until True is returned; 348 otherwise we wait for bluetoothd to start. After bluetoothd starts, we 349 check the existence of a local adapter and proceed to get the 350 advertisement interface manager. 351 352 Since not all devices will have adapters, this will also return True 353 in the case where there is no adapter. 354 355 @return True on success, including if there is no local adapter, 356 False otherwise. 357 358 """ 359 self._advertising = None 360 if self._bluez is None: 361 logging.warning('Bluez not found!') 362 return False 363 if not self._has_adapter: 364 logging.debug('Device has no adapter; returning') 365 return True 366 self._advertising = self._get_advertising() 367 return bool(self._advertising) 368 369 370 @xmlrpc_server.dbus_safe(False) 371 def _get_adapter(self): 372 """Get the D-Bus proxy for the local adapter. 373 374 @return the adapter on success. None otherwise. 375 376 """ 377 objects = self._bluez.GetManagedObjects( 378 dbus_interface=self.BLUEZ_MANAGER_IFACE) 379 for path, ifaces in objects.iteritems(): 380 logging.debug('%s -> %r', path, ifaces.keys()) 381 if self.BLUEZ_ADAPTER_IFACE in ifaces: 382 logging.debug('using adapter %s', path) 383 adapter = self._system_bus.get_object( 384 self.BLUEZ_SERVICE_NAME, 385 path) 386 return adapter 387 else: 388 logging.warning('No adapter found in interface!') 389 return None 390 391 392 @xmlrpc_server.dbus_safe(False) 393 def _get_advertising(self): 394 """Get the D-Bus proxy for the local advertising interface. 395 396 @return the advertising interface object. 397 398 """ 399 return dbus.Interface(self._adapter, 400 self.BLUEZ_LE_ADVERTISING_MANAGER_IFACE) 401 402 403 @xmlrpc_server.dbus_safe(False) 404 def reset_on(self): 405 """Reset the adapter and settings and power up the adapter. 406 407 @return True on success, False otherwise. 408 409 """ 410 return self._reset(set_power=True) 411 412 413 @xmlrpc_server.dbus_safe(False) 414 def reset_off(self): 415 """Reset the adapter and settings, leave the adapter powered off. 416 417 @return True on success, False otherwise. 418 419 """ 420 return self._reset(set_power=False) 421 422 423 def has_adapter(self): 424 """Return if an adapter is present. 425 426 This will only return True if we have determined both that there is 427 a Bluetooth adapter on this device (kernel adapter index list is not 428 empty) and that the Bluetooth daemon has exported an object for it. 429 430 @return True if an adapter is present, False if not. 431 432 """ 433 return self._has_adapter and self._adapter is not None 434 435 436 def _reset(self, set_power=False): 437 """Remove remote devices and set adapter to set_power state. 438 439 Do not restart bluetoothd as this may incur a side effect. 440 The unhappy chrome may disable the adapter randomly. 441 442 @param set_power: adapter power state to set (True or False). 443 444 @return True on success, False otherwise. 445 446 """ 447 logging.debug('_reset') 448 449 if not self._adapter: 450 logging.warning('Adapter not found!') 451 return False 452 453 objects = self._bluez.GetManagedObjects( 454 dbus_interface=self.BLUEZ_MANAGER_IFACE, byte_arrays=True) 455 456 devices = [] 457 for path, ifaces in objects.iteritems(): 458 if self.BLUEZ_DEVICE_IFACE in ifaces: 459 devices.append(objects[path][self.BLUEZ_DEVICE_IFACE]) 460 461 # Turn on the adapter in order to remove all remote devices. 462 if not self._is_powered_on(): 463 self._set_powered(True) 464 465 for device in devices: 466 logging.debug('removing %s', device.get('Address')) 467 self.remove_device_object(device.get('Address')) 468 469 if not set_power: 470 self._set_powered(False) 471 472 return True 473 474 475 @xmlrpc_server.dbus_safe(False) 476 def set_powered(self, powered): 477 """Set the adapter power state. 478 479 @param powered: adapter power state to set (True or False). 480 481 @return True on success, False otherwise. 482 483 """ 484 if not self._adapter: 485 if not powered: 486 # Return success if we are trying to power off an adapter that's 487 # missing or gone away, since the expected result has happened. 488 return True 489 else: 490 logging.warning('Adapter not found!') 491 return False 492 self._set_powered(powered) 493 return True 494 495 496 @xmlrpc_server.dbus_safe(False) 497 def _set_powered(self, powered): 498 """Set the adapter power state. 499 500 @param powered: adapter power state to set (True or False). 501 502 """ 503 logging.debug('_set_powered %r', powered) 504 self._adapter.Set(self.BLUEZ_ADAPTER_IFACE, 'Powered', powered, 505 dbus_interface=dbus.PROPERTIES_IFACE) 506 507 508 @xmlrpc_server.dbus_safe(False) 509 def set_discoverable(self, discoverable): 510 """Set the adapter discoverable state. 511 512 @param discoverable: adapter discoverable state to set (True or False). 513 514 @return True on success, False otherwise. 515 516 """ 517 if not discoverable and not self._adapter: 518 # Return success if we are trying to make an adapter that's 519 # missing or gone away, undiscoverable, since the expected result 520 # has happened. 521 return True 522 self._adapter.Set(self.BLUEZ_ADAPTER_IFACE, 523 'Discoverable', discoverable, 524 dbus_interface=dbus.PROPERTIES_IFACE) 525 return True 526 527 528 @xmlrpc_server.dbus_safe(False) 529 def set_pairable(self, pairable): 530 """Set the adapter pairable state. 531 532 @param pairable: adapter pairable state to set (True or False). 533 534 @return True on success, False otherwise. 535 536 """ 537 self._adapter.Set(self.BLUEZ_ADAPTER_IFACE, 'Pairable', pairable, 538 dbus_interface=dbus.PROPERTIES_IFACE) 539 return True 540 541 542 @xmlrpc_server.dbus_safe(False) 543 def _get_adapter_properties(self): 544 """Read the adapter properties from the Bluetooth Daemon. 545 546 @return the properties as a JSON-encoded dictionary on success, 547 the value False otherwise. 548 549 """ 550 if self._bluez: 551 objects = self._bluez.GetManagedObjects( 552 dbus_interface=self.BLUEZ_MANAGER_IFACE) 553 props = objects[self._adapter.object_path][self.BLUEZ_ADAPTER_IFACE] 554 else: 555 props = {} 556 logging.debug('get_adapter_properties: %s', props) 557 return props 558 559 560 def get_adapter_properties(self): 561 return json.dumps(self._get_adapter_properties()) 562 563 564 def _is_powered_on(self): 565 return bool(self._get_adapter_properties().get(u'Powered')) 566 567 568 def read_version(self): 569 """Read the version of the management interface from the Kernel. 570 571 @return the information as a JSON-encoded tuple of: 572 ( version, revision ) 573 574 """ 575 return json.dumps(self._control.read_version()) 576 577 578 def read_supported_commands(self): 579 """Read the set of supported commands from the Kernel. 580 581 @return the information as a JSON-encoded tuple of: 582 ( commands, events ) 583 584 """ 585 return json.dumps(self._control.read_supported_commands()) 586 587 588 def read_index_list(self): 589 """Read the list of currently known controllers from the Kernel. 590 591 @return the information as a JSON-encoded array of controller indexes. 592 593 """ 594 return json.dumps(self._control.read_index_list()) 595 596 597 def read_info(self): 598 """Read the adapter information from the Kernel. 599 600 @return the information as a JSON-encoded tuple of: 601 ( address, bluetooth_version, manufacturer_id, 602 supported_settings, current_settings, class_of_device, 603 name, short_name ) 604 605 """ 606 return json.dumps(self._control.read_info(0)) 607 608 609 def add_device(self, address, address_type, action): 610 """Add a device to the Kernel action list. 611 612 @param address: Address of the device to add. 613 @param address_type: Type of device in @address. 614 @param action: Action to take. 615 616 @return on success, a JSON-encoded typle of: 617 ( address, address_type ), None on failure. 618 619 """ 620 return json.dumps(self._control.add_device( 621 0, address, address_type, action)) 622 623 624 def remove_device(self, address, address_type): 625 """Remove a device from the Kernel action list. 626 627 @param address: Address of the device to remove. 628 @param address_type: Type of device in @address. 629 630 @return on success, a JSON-encoded typle of: 631 ( address, address_type ), None on failure. 632 633 """ 634 return json.dumps(self._control.remove_device( 635 0, address, address_type)) 636 637 638 @xmlrpc_server.dbus_safe(False) 639 def get_devices(self): 640 """Read information about remote devices known to the adapter. 641 642 @return the properties of each device as a JSON-encoded array of 643 dictionaries on success, the value False otherwise. 644 645 """ 646 objects = self._bluez.GetManagedObjects( 647 dbus_interface=self.BLUEZ_MANAGER_IFACE, byte_arrays=True) 648 devices = [] 649 for path, ifaces in objects.iteritems(): 650 if self.BLUEZ_DEVICE_IFACE in ifaces: 651 devices.append(objects[path][self.BLUEZ_DEVICE_IFACE]) 652 return json.dumps(devices) 653 654 655 @xmlrpc_server.dbus_safe(False) 656 def get_device_by_address(self, address): 657 """Read information about the remote device with the specified address. 658 659 @param address: Address of the device to get. 660 661 @return the properties of the device as a JSON-encoded dictionary 662 on success, the value False otherwise. 663 664 """ 665 objects = self._bluez.GetManagedObjects( 666 dbus_interface=self.BLUEZ_MANAGER_IFACE, byte_arrays=True) 667 devices = [] 668 for path, ifaces in objects.iteritems(): 669 if self.BLUEZ_DEVICE_IFACE in ifaces: 670 device = objects[path][self.BLUEZ_DEVICE_IFACE] 671 if device.get('Address') == address: 672 return json.dumps(device) 673 674 devices = json.loads(self.get_devices()) 675 for device in devices: 676 if device.get['Address'] == address: 677 return json.dumps(device) 678 return json.dumps(dict()) 679 680 681 @xmlrpc_server.dbus_safe(False) 682 def start_discovery(self): 683 """Start discovery of remote devices. 684 685 Obtain the discovered device information using get_devices(), called 686 stop_discovery() when done. 687 688 @return True on success, False otherwise. 689 690 """ 691 if not self._adapter: 692 return False 693 self._adapter.StartDiscovery(dbus_interface=self.BLUEZ_ADAPTER_IFACE) 694 return True 695 696 697 @xmlrpc_server.dbus_safe(False) 698 def stop_discovery(self): 699 """Stop discovery of remote devices. 700 701 @return True on success, False otherwise. 702 703 """ 704 if not self._adapter: 705 return False 706 self._adapter.StopDiscovery(dbus_interface=self.BLUEZ_ADAPTER_IFACE) 707 return True 708 709 710 def get_dev_info(self): 711 """Read raw HCI device information. 712 713 @return JSON-encoded tuple of: 714 (index, name, address, flags, device_type, bus_type, 715 features, pkt_type, link_policy, link_mode, 716 acl_mtu, acl_pkts, sco_mtu, sco_pkts, 717 err_rx, err_tx, cmd_tx, evt_rx, acl_tx, acl_rx, 718 sco_tx, sco_rx, byte_rx, byte_tx) on success, 719 None on failure. 720 721 """ 722 return json.dumps(self._raw.get_dev_info(0)) 723 724 725 @xmlrpc_server.dbus_safe(False) 726 def register_profile(self, path, uuid, options): 727 """Register new profile (service). 728 729 @param path: Path to the profile object. 730 @param uuid: Service Class ID of the service as string. 731 @param options: Dictionary of options for the new service, compliant 732 with BlueZ D-Bus Profile API standard. 733 734 @return True on success, False otherwise. 735 736 """ 737 profile_manager = dbus.Interface( 738 self._system_bus.get_object( 739 self.BLUEZ_SERVICE_NAME, 740 self.BLUEZ_PROFILE_MANAGER_PATH), 741 self.BLUEZ_PROFILE_MANAGER_IFACE) 742 profile_manager.RegisterProfile(path, uuid, options) 743 return True 744 745 746 def has_device(self, address): 747 """Checks if the device with a given address exists. 748 749 @param address: Address of the device. 750 751 @returns: True if there is an interface object with that address. 752 False if the device is not found. 753 754 @raises: Exception if a D-Bus error is encountered. 755 756 """ 757 result = self._find_device(address) 758 logging.debug('has_device result: %s', str(result)) 759 760 # The result being False indicates that there is a D-Bus error. 761 if result is False: 762 raise Exception('dbus.Interface error') 763 764 # Return True if the result is not None, e.g. a D-Bus interface object; 765 # False otherwise. 766 return bool(result) 767 768 769 @xmlrpc_server.dbus_safe(False) 770 def _find_device(self, address): 771 """Finds the device with a given address. 772 773 Find the device with a given address and returns the 774 device interface. 775 776 @param address: Address of the device. 777 778 @returns: An 'org.bluez.Device1' interface to the device. 779 None if device can not be found. 780 """ 781 path = self._get_device_path(address) 782 if path: 783 obj = self._system_bus.get_object( 784 self.BLUEZ_SERVICE_NAME, path) 785 return dbus.Interface(obj, self.BLUEZ_DEVICE_IFACE) 786 logging.info('Device not found') 787 return None 788 789 790 @xmlrpc_server.dbus_safe(False) 791 def _get_device_path(self, address): 792 """Gets the path for a device with a given address. 793 794 Find the device with a given address and returns the 795 the path for the device. 796 797 @param address: Address of the device. 798 799 @returns: The path to the address of the device, or None if device is 800 not found in the object tree. 801 802 """ 803 objects = self._bluez.GetManagedObjects( 804 dbus_interface=self.BLUEZ_MANAGER_IFACE) 805 for path, ifaces in objects.iteritems(): 806 device = ifaces.get(self.BLUEZ_DEVICE_IFACE) 807 if device is None: 808 continue 809 if (device['Address'] == address and 810 path.startswith(self._adapter.object_path)): 811 return path 812 logging.info('Device path not found') 813 814 815 @xmlrpc_server.dbus_safe(False) 816 def _setup_pairing_agent(self, pin): 817 """Initializes and resiters a PairingAgent to handle authentication. 818 819 @param pin: The pin code this agent will answer. 820 821 """ 822 if self._pairing_agent: 823 logging.info('Removing the old agent before initializing a new one') 824 self._pairing_agent.remove_from_connection() 825 self._pairing_agent = None 826 self._pairing_agent= PairingAgent(pin, self._system_bus, 827 self.AGENT_PATH) 828 agent_manager = dbus.Interface( 829 self._system_bus.get_object(self.BLUEZ_SERVICE_NAME, 830 self.BLUEZ_AGENT_MANAGER_PATH), 831 self.BLUEZ_AGENT_MANAGER_IFACE) 832 try: 833 agent_manager.RegisterAgent(self.AGENT_PATH, self._capability) 834 except dbus.exceptions.DBusException, e: 835 if e.get_dbus_name() == self.BLUEZ_ERROR_ALREADY_EXISTS: 836 logging.info('Unregistering old agent and registering the new') 837 agent_manager.UnregisterAgent(self.AGENT_PATH) 838 agent_manager.RegisterAgent(self.AGENT_PATH, self._capability) 839 else: 840 logging.error('Error setting up pin agent: %s', e) 841 raise 842 logging.info('Agent registered: %s', self.AGENT_PATH) 843 844 845 @xmlrpc_server.dbus_safe(False) 846 def _is_paired(self, device): 847 """Checks if a device is paired. 848 849 @param device: An 'org.bluez.Device1' interface to the device. 850 851 @returns: True if device is paired. False otherwise. 852 853 """ 854 props = dbus.Interface(device, dbus.PROPERTIES_IFACE) 855 paired = props.Get(self.BLUEZ_DEVICE_IFACE, 'Paired') 856 return bool(paired) 857 858 859 @xmlrpc_server.dbus_safe(False) 860 def device_is_paired(self, address): 861 """Checks if a device is paired. 862 863 @param address: address of the device. 864 865 @returns: True if device is paired. False otherwise. 866 867 """ 868 device = self._find_device(address) 869 if not device: 870 logging.error('Device not found') 871 return False 872 return self._is_paired(device) 873 874 875 @xmlrpc_server.dbus_safe(False) 876 def _is_connected(self, device): 877 """Checks if a device is connected. 878 879 @param device: An 'org.bluez.Device1' interface to the device. 880 881 @returns: True if device is connected. False otherwise. 882 883 """ 884 props = dbus.Interface(device, dbus.PROPERTIES_IFACE) 885 connected = props.Get(self.BLUEZ_DEVICE_IFACE, 'Connected') 886 logging.info('Got connected = %r', connected) 887 return bool(connected) 888 889 890 891 @xmlrpc_server.dbus_safe(False) 892 def _set_trusted_by_device(self, device, trusted=True): 893 """Set the device trusted by device object. 894 895 @param device: the device object to set trusted. 896 @param trusted: True or False indicating whether to set trusted or not. 897 898 @returns: True if successful. False otherwise. 899 900 """ 901 try: 902 properties = dbus.Interface(device, self.DBUS_PROP_IFACE) 903 properties.Set(self.BLUEZ_DEVICE_IFACE, 'Trusted', trusted) 904 return True 905 except Exception as e: 906 logging.error('_set_trusted_by_device: %s', e) 907 except: 908 logging.error('_set_trusted_by_device: unexpected error') 909 return False 910 911 912 @xmlrpc_server.dbus_safe(False) 913 def _set_trusted_by_path(self, device_path, trusted=True): 914 """Set the device trusted by the device path. 915 916 @param device_path: the object path of the device. 917 @param trusted: True or False indicating whether to set trusted or not. 918 919 @returns: True if successful. False otherwise. 920 921 """ 922 try: 923 device = self._system_bus.get_object(self.BLUEZ_SERVICE_NAME, 924 device_path) 925 return self._set_trusted_by_device(device, trusted) 926 except Exception as e: 927 logging.error('_set_trusted_by_path: %s', e) 928 except: 929 logging.error('_set_trusted_by_path: unexpected error') 930 return False 931 932 933 @xmlrpc_server.dbus_safe(False) 934 def set_trusted(self, address, trusted=True): 935 """Set the device trusted by address. 936 937 @param address: The bluetooth address of the device. 938 @param trusted: True or False indicating whether to set trusted or not. 939 940 @returns: True if successful. False otherwise. 941 942 """ 943 try: 944 device = self._find_device(address) 945 return self._set_trusted_by_device(device, trusted) 946 except Exception as e: 947 logging.error('set_trusted: %s', e) 948 except: 949 logging.error('set_trusted: unexpected error') 950 return False 951 952 953 @xmlrpc_server.dbus_safe(False) 954 def pair_legacy_device(self, address, pin, trusted, timeout=60): 955 """Pairs a device with a given pin code. 956 957 Registers a agent who handles pin code request and 958 pairs a device with known pin code. 959 960 Note that the adapter does not automatically connnect to the device 961 when pairing is done. The connect_device() method has to be invoked 962 explicitly to connect to the device. This provides finer control 963 for testing purpose. 964 965 @param address: Address of the device to pair. 966 @param pin: The pin code of the device to pair. 967 @param trusted: indicating whether to set the device trusted. 968 @param timeout: The timeout in seconds for pairing. 969 970 @returns: True on success. False otherwise. 971 972 """ 973 device = self._find_device(address) 974 if not device: 975 logging.error('Device not found') 976 return False 977 if self._is_paired(device): 978 logging.info('Device is already paired') 979 return True 980 981 device_path = device.object_path 982 logging.info('Device %s is found.', device.object_path) 983 984 self._setup_pairing_agent(pin) 985 mainloop = gobject.MainLoop() 986 987 988 def pair_reply(): 989 """Handler when pairing succeeded.""" 990 logging.info('Device paired: %s', device_path) 991 if trusted: 992 self._set_trusted_by_path(device_path, trusted=True) 993 logging.info('Device trusted: %s', device_path) 994 mainloop.quit() 995 996 997 def pair_error(error): 998 """Handler when pairing failed. 999 1000 @param error: one of errors defined in org.bluez.Error representing 1001 the error in pairing. 1002 1003 """ 1004 try: 1005 error_name = error.get_dbus_name() 1006 if error_name == 'org.freedesktop.DBus.Error.NoReply': 1007 logging.error('Timed out after %d ms. Cancelling pairing.', 1008 timeout) 1009 device.CancelPairing() 1010 else: 1011 logging.error('Pairing device failed: %s', error) 1012 finally: 1013 mainloop.quit() 1014 1015 1016 device.Pair(reply_handler=pair_reply, error_handler=pair_error, 1017 timeout=timeout * 1000) 1018 mainloop.run() 1019 return self._is_paired(device) 1020 1021 1022 @xmlrpc_server.dbus_safe(False) 1023 def remove_device_object(self, address): 1024 """Removes a device object and the pairing information. 1025 1026 Calls RemoveDevice method to remove remote device 1027 object and the pairing information. 1028 1029 @param address: Address of the device to unpair. 1030 1031 @returns: True on success. False otherwise. 1032 1033 """ 1034 device = self._find_device(address) 1035 if not device: 1036 logging.error('Device not found') 1037 return False 1038 self._adapter.RemoveDevice( 1039 device.object_path, dbus_interface=self.BLUEZ_ADAPTER_IFACE) 1040 return True 1041 1042 1043 @xmlrpc_server.dbus_safe(False) 1044 def connect_device(self, address): 1045 """Connects a device. 1046 1047 Connects a device if it is not connected. 1048 1049 @param address: Address of the device to connect. 1050 1051 @returns: True on success. False otherwise. 1052 1053 """ 1054 device = self._find_device(address) 1055 if not device: 1056 logging.error('Device not found') 1057 return False 1058 if self._is_connected(device): 1059 logging.info('Device is already connected') 1060 return True 1061 device.Connect() 1062 return self._is_connected(device) 1063 1064 1065 @xmlrpc_server.dbus_safe(False) 1066 def device_is_connected(self, address): 1067 """Checks if a device is connected. 1068 1069 @param address: Address of the device to connect. 1070 1071 @returns: True if device is connected. False otherwise. 1072 1073 """ 1074 device = self._find_device(address) 1075 if not device: 1076 logging.error('Device not found') 1077 return False 1078 return self._is_connected(device) 1079 1080 1081 @xmlrpc_server.dbus_safe(False) 1082 def disconnect_device(self, address): 1083 """Disconnects a device. 1084 1085 Disconnects a device if it is connected. 1086 1087 @param address: Address of the device to disconnect. 1088 1089 @returns: True on success. False otherwise. 1090 1091 """ 1092 device = self._find_device(address) 1093 if not device: 1094 logging.error('Device not found') 1095 return False 1096 if not self._is_connected(device): 1097 logging.info('Device is not connected') 1098 return True 1099 device.Disconnect() 1100 return not self._is_connected(device) 1101 1102 1103 @xmlrpc_server.dbus_safe(False) 1104 def _device_services_resolved(self, device): 1105 """Checks if services are resolved. 1106 1107 @param device: An 'org.bluez.Device1' interface to the device. 1108 1109 @returns: True if device is connected. False otherwise. 1110 1111 """ 1112 logging.info('device for services resolved: %s', device) 1113 props = dbus.Interface(device, dbus.PROPERTIES_IFACE) 1114 resolved = props.Get(self.BLUEZ_DEVICE_IFACE, 'ServicesResolved') 1115 logging.info('Services resolved = %r', resolved) 1116 return bool(resolved) 1117 1118 1119 @xmlrpc_server.dbus_safe(False) 1120 def device_services_resolved(self, address): 1121 """Checks if service discovery is complete on a device. 1122 1123 Checks whether service discovery has been completed.. 1124 1125 @param address: Address of the remote device. 1126 1127 @returns: True on success. False otherwise. 1128 1129 """ 1130 device = self._find_device(address) 1131 if not device: 1132 logging.error('Device not found') 1133 return False 1134 1135 if not self._is_connected(device): 1136 logging.info('Device is not connected') 1137 return False 1138 1139 return self._device_services_resolved(device) 1140 1141 1142 def btmon_start(self): 1143 """Start btmon monitoring.""" 1144 self.btmon.start() 1145 1146 1147 def btmon_stop(self): 1148 """Stop btmon monitoring.""" 1149 self.btmon.stop() 1150 1151 1152 def btmon_get(self, search_str, start_str): 1153 """Get btmon output contents. 1154 1155 @param search_str: only lines with search_str would be kept. 1156 @param start_str: all lines before the occurrence of start_str would be 1157 filtered. 1158 1159 @returns: the recorded btmon output. 1160 1161 """ 1162 return self.btmon.get_contents(search_str=search_str, 1163 start_str=start_str) 1164 1165 1166 def btmon_find(self, pattern_str): 1167 """Find if a pattern string exists in btmon output. 1168 1169 @param pattern_str: the pattern string to find. 1170 1171 @returns: True on success. False otherwise. 1172 1173 """ 1174 return self.btmon.find(pattern_str) 1175 1176 1177 @xmlrpc_server.dbus_safe(False) 1178 def advertising_async_method(self, dbus_method, 1179 reply_handler, error_handler, *args): 1180 """Run an async dbus method. 1181 1182 @param dbus_method: the dbus async method to invoke. 1183 @param reply_handler: the reply handler for the dbus method. 1184 @param error_handler: the error handler for the dbus method. 1185 @param *args: additional arguments for the dbus method. 1186 1187 @returns: an empty string '' on success; 1188 None if there is no _advertising interface manager; and 1189 an error string if the dbus method fails. 1190 1191 """ 1192 1193 def successful_cb(): 1194 """Called when the dbus_method completed successfully.""" 1195 reply_handler() 1196 self.advertising_cb_msg = '' 1197 self._adv_mainloop.quit() 1198 1199 1200 def error_cb(error): 1201 """Called when the dbus_method failed.""" 1202 error_handler(error) 1203 self.advertising_cb_msg = str(error) 1204 self._adv_mainloop.quit() 1205 1206 1207 if not self._advertising: 1208 return None 1209 1210 # Call dbus_method with handlers. 1211 dbus_method(*args, reply_handler=successful_cb, error_handler=error_cb) 1212 1213 self._adv_mainloop.run() 1214 1215 return self.advertising_cb_msg 1216 1217 1218 def register_advertisement(self, advertisement_data): 1219 """Register an advertisement. 1220 1221 Note that rpc supports only conformable types. Hence, a 1222 dict about the advertisement is passed as a parameter such 1223 that the advertisement object could be constructed on the host. 1224 1225 @param advertisement_data: a dict of the advertisement to register. 1226 1227 @returns: True on success. False otherwise. 1228 1229 """ 1230 adv = advertisement.Advertisement(self._system_bus, advertisement_data) 1231 self.advertisements.append(adv) 1232 return self.advertising_async_method( 1233 self._advertising.RegisterAdvertisement, 1234 # reply handler 1235 lambda: logging.info('register_advertisement: succeeded.'), 1236 # error handler 1237 lambda error: logging.error( 1238 'register_advertisement: failed: %s', str(error)), 1239 # other arguments 1240 adv.get_path(), {}) 1241 1242 1243 def unregister_advertisement(self, advertisement_data): 1244 """Unregister an advertisement. 1245 1246 Note that to unregister an advertisement, it is required to use 1247 the same self._advertising interface manager. This is because 1248 bluez only allows the same sender to invoke UnregisterAdvertisement 1249 method. Hence, watch out that the bluetoothd is not restarted or 1250 self.start_bluetoothd() is not executed between the time span that 1251 an advertisement is registered and unregistered. 1252 1253 @param advertisement_data: a dict of the advertisements to unregister. 1254 1255 @returns: True on success. False otherwise. 1256 1257 """ 1258 path = advertisement_data.get('Path') 1259 for index, adv in enumerate(self.advertisements): 1260 if adv.get_path() == path: 1261 break 1262 else: 1263 logging.error('Fail to find the advertisement under the path: %s', 1264 path) 1265 return False 1266 1267 result = self.advertising_async_method( 1268 self._advertising.UnregisterAdvertisement, 1269 # reply handler 1270 lambda: logging.info('unregister_advertisement: succeeded.'), 1271 # error handler 1272 lambda error: logging.error( 1273 'unregister_advertisement: failed: %s', str(error)), 1274 # other arguments 1275 adv.get_path()) 1276 1277 # Call remove_from_connection() so that the same path could be reused. 1278 adv.remove_from_connection() 1279 del self.advertisements[index] 1280 1281 return result 1282 1283 1284 def set_advertising_intervals(self, min_adv_interval_ms, 1285 max_adv_interval_ms): 1286 """Set advertising intervals. 1287 1288 @param min_adv_interval_ms: the min advertising interval in ms. 1289 @param max_adv_interval_ms: the max advertising interval in ms. 1290 1291 @returns: True on success. False otherwise. 1292 1293 """ 1294 return self.advertising_async_method( 1295 self._advertising.SetAdvertisingIntervals, 1296 # reply handler 1297 lambda: logging.info('set_advertising_intervals: succeeded.'), 1298 # error handler 1299 lambda error: logging.error( 1300 'set_advertising_intervals: failed: %s', str(error)), 1301 # other arguments 1302 min_adv_interval_ms, max_adv_interval_ms) 1303 1304 1305 def reset_advertising(self): 1306 """Reset advertising. 1307 1308 This includes un-registering all advertisements, reset advertising 1309 intervals, and disable advertising. 1310 1311 @returns: True on success. False otherwise. 1312 1313 """ 1314 # It is required to execute remove_from_connection() to unregister the 1315 # object-path handler of each advertisement. In this way, we could 1316 # register an advertisement with the same path repeatedly. 1317 for adv in self.advertisements: 1318 adv.remove_from_connection() 1319 del self.advertisements[:] 1320 1321 return self.advertising_async_method( 1322 self._advertising.ResetAdvertising, 1323 # reply handler 1324 lambda: logging.info('reset_advertising: succeeded.'), 1325 # error handler 1326 lambda error: logging.error( 1327 'reset_advertising: failed: %s', str(error))) 1328 1329 1330 @xmlrpc_server.dbus_safe(False) 1331 def get_characteristic_map(self, address): 1332 """Gets a map of characteristic paths for a device. 1333 1334 Walks the object tree, and returns a map of uuids to object paths for 1335 all resolved gatt characteristics. 1336 1337 @param address: The MAC address of the device to retrieve 1338 gatt characteristic uuids and paths from. 1339 1340 @returns: A dictionary of characteristic paths, keyed by uuid. 1341 1342 """ 1343 device_path = self._get_device_path(address) 1344 char_map = {} 1345 1346 if device_path: 1347 objects = self._bluez.GetManagedObjects( 1348 dbus_interface=self.BLUEZ_MANAGER_IFACE, byte_arrays=False) 1349 1350 for path, ifaces in objects.iteritems(): 1351 if (self.BLUEZ_GATT_IFACE in ifaces and 1352 path.startswith(device_path)): 1353 uuid = ifaces[self.BLUEZ_GATT_IFACE]['UUID'].lower() 1354 char_map[uuid] = path 1355 else: 1356 logging.warning('Device %s not in object tree.', address) 1357 1358 return char_map 1359 1360 1361 @xmlrpc_server.dbus_safe(False) 1362 def _get_char_object(self, uuid, address): 1363 """Gets a characteristic object. 1364 1365 Gets a characteristic object for a given uuid and address. 1366 1367 @param uuid: The uuid of the characteristic, as a string. 1368 @param address: The MAC address of the remote device. 1369 1370 @returns: A dbus interface for the characteristic if the uuid/address 1371 is in the object tree. 1372 None if the address/uuid is not found in the object tree. 1373 1374 """ 1375 path = self.get_characteristic_map(address).get(uuid) 1376 if not path: 1377 return None 1378 return dbus.Interface( 1379 self._system_bus.get_object(self.BLUEZ_SERVICE_NAME, path), 1380 self.BLUEZ_GATT_IFACE) 1381 1382 1383 @xmlrpc_server.dbus_safe(None) 1384 def read_characteristic(self, uuid, address): 1385 """Reads the value of a gatt characteristic. 1386 1387 Reads the current value of a gatt characteristic. Base64 endcoding is 1388 used for compatibility with the XML RPC interface. 1389 1390 @param uuid: The uuid of the characteristic to read, as a string. 1391 @param address: The MAC address of the remote device. 1392 1393 @returns: A b64 encoded version of a byte array containing the value 1394 if the uuid/address is in the object tree. 1395 None if the uuid/address was not found in the object tree, or 1396 if a DBus exception was raised by the read operation. 1397 1398 """ 1399 char_obj = self._get_char_object(uuid, address) 1400 if char_obj is None: 1401 return None 1402 value = char_obj.ReadValue(dbus.Dictionary()) 1403 return _dbus_byte_array_to_b64_string(value) 1404 1405 1406 @xmlrpc_server.dbus_safe(None) 1407 def write_characteristic(self, uuid, address, value): 1408 """Performs a write operation on a gatt characteristic. 1409 1410 Writes to a GATT characteristic on a remote device. Base64 endcoding is 1411 used for compatibility with the XML RPC interface. 1412 1413 @param uuid: The uuid of the characteristic to write to, as a string. 1414 @param address: The MAC address of the remote device, as a string. 1415 @param value: A byte array containing the data to write. 1416 1417 @returns: True if the write operation does not raise an exception. 1418 None if the uuid/address was not found in the object tree, or 1419 if a DBus exception was raised by the write operation. 1420 1421 """ 1422 char_obj = self._get_char_object(uuid, address) 1423 if char_obj is None: 1424 return None 1425 dbus_value = _b64_string_to_dbus_byte_array(value) 1426 char_obj.WriteValue(dbus_value, dbus.Dictionary()) 1427 return True 1428 1429 1430 @xmlrpc_server.dbus_safe(False) 1431 def is_characteristic_path_resolved(self, uuid, address): 1432 """Checks whether a characteristic is in the object tree. 1433 1434 Checks whether a characteristic is curently found in the object tree. 1435 1436 @param uuid: The uuid of the characteristic to search for. 1437 @param address: The MAC address of the device on which to search for 1438 the characteristic. 1439 1440 @returns: True if the characteristic is found. 1441 False if the characteristic path is not found. 1442 1443 """ 1444 return bool(self.get_characteristic_map(address).get(uuid)) 1445 1446 1447if __name__ == '__main__': 1448 logging.basicConfig(level=logging.DEBUG) 1449 handler = logging.handlers.SysLogHandler(address='/dev/log') 1450 formatter = logging.Formatter( 1451 'bluetooth_device_xmlrpc_server: [%(levelname)s] %(message)s') 1452 handler.setFormatter(formatter) 1453 logging.getLogger().addHandler(handler) 1454 logging.debug('bluetooth_device_xmlrpc_server main...') 1455 server = xmlrpc_server.XmlRpcServer( 1456 'localhost', 1457 constants.BLUETOOTH_DEVICE_XMLRPC_SERVER_PORT) 1458 server.register_delegate(BluetoothDeviceXmlRpcDelegate()) 1459 server.run() 1460