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