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