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 dbus 8import dbus.mainloop.glib 9import dbus.service 10import gobject 11import json 12import logging 13import logging.handlers 14import os 15import shutil 16 17import common 18from autotest_lib.client.bin import utils 19from autotest_lib.client.common_lib.cros.bluetooth import bluetooth_socket 20from autotest_lib.client.cros import constants 21from autotest_lib.client.cros import xmlrpc_server 22 23 24class _PinAgent(dbus.service.Object): 25 """The agent handling bluetooth device with a known pin code. 26 27 _PinAgent overrides RequestPinCode method to return a given pin code. 28 User can use this agent to pair bluetooth device which has a known pin code. 29 30 """ 31 def __init__(self, pin, *args, **kwargs): 32 super(_PinAgent, self).__init__(*args, **kwargs) 33 self._pin = pin 34 35 36 @dbus.service.method('org.bluez.Agent1', in_signature="o", out_signature="s") 37 def RequestPinCode(self, device_path): 38 """Requests pin code for a device. 39 40 Returns the known pin code for the request. 41 42 @param device_path: The object path of the device. 43 44 @returns: The known pin code. 45 46 """ 47 logging.info('RequestPinCode for %s, return %s', device_path, self._pin) 48 return self._pin 49 50 51class BluetoothDeviceXmlRpcDelegate(xmlrpc_server.XmlRpcDelegate): 52 """Exposes DUT methods called remotely during Bluetooth autotests. 53 54 All instance methods of this object without a preceding '_' are exposed via 55 an XML-RPC server. This is not a stateless handler object, which means that 56 if you store state inside the delegate, that state will remain around for 57 future calls. 58 """ 59 60 UPSTART_PATH = 'unix:abstract=/com/ubuntu/upstart' 61 UPSTART_MANAGER_PATH = '/com/ubuntu/Upstart' 62 UPSTART_MANAGER_IFACE = 'com.ubuntu.Upstart0_6' 63 UPSTART_JOB_IFACE = 'com.ubuntu.Upstart0_6.Job' 64 65 UPSTART_ERROR_UNKNOWNINSTANCE = \ 66 'com.ubuntu.Upstart0_6.Error.UnknownInstance' 67 68 BLUETOOTHD_JOB = 'bluetoothd' 69 70 DBUS_ERROR_SERVICEUNKNOWN = 'org.freedesktop.DBus.Error.ServiceUnknown' 71 72 BLUEZ_SERVICE_NAME = 'org.bluez' 73 BLUEZ_MANAGER_PATH = '/' 74 BLUEZ_MANAGER_IFACE = 'org.freedesktop.DBus.ObjectManager' 75 BLUEZ_ADAPTER_IFACE = 'org.bluez.Adapter1' 76 BLUEZ_DEVICE_IFACE = 'org.bluez.Device1' 77 BLUEZ_AGENT_MANAGER_PATH = '/org/bluez' 78 BLUEZ_AGENT_MANAGER_IFACE = 'org.bluez.AgentManager1' 79 BLUEZ_PROFILE_MANAGER_PATH = '/org/bluez' 80 BLUEZ_PROFILE_MANAGER_IFACE = 'org.bluez.ProfileManager1' 81 BLUEZ_ERROR_ALREADY_EXISTS = 'org.bluez.Error.AlreadyExists' 82 83 BLUETOOTH_LIBDIR = '/var/lib/bluetooth' 84 85 # Timeout for how long we'll wait for BlueZ and the Adapter to show up 86 # after reset. 87 ADAPTER_TIMEOUT = 30 88 89 def __init__(self): 90 super(BluetoothDeviceXmlRpcDelegate, self).__init__() 91 92 # Open the Bluetooth Raw socket to the kernel which provides us direct, 93 # raw, access to the HCI controller. 94 self._raw = bluetooth_socket.BluetoothRawSocket() 95 96 # Open the Bluetooth Control socket to the kernel which provides us 97 # raw management access to the Bluetooth Host Subsystem. Read the list 98 # of adapter indexes to determine whether or not this device has a 99 # Bluetooth Adapter or not. 100 self._control = bluetooth_socket.BluetoothControlSocket() 101 self._has_adapter = len(self._control.read_index_list()) > 0 102 103 # Set up the connection to Upstart so we can start and stop services 104 # and fetch the bluetoothd job. 105 self._upstart_conn = dbus.connection.Connection(self.UPSTART_PATH) 106 self._upstart = self._upstart_conn.get_object( 107 None, 108 self.UPSTART_MANAGER_PATH) 109 110 bluetoothd_path = self._upstart.GetJobByName( 111 self.BLUETOOTHD_JOB, 112 dbus_interface=self.UPSTART_MANAGER_IFACE) 113 self._bluetoothd = self._upstart_conn.get_object( 114 None, 115 bluetoothd_path) 116 117 # Arrange for the GLib main loop to be the default. 118 dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) 119 120 # Set up the connection to the D-Bus System Bus, get the object for 121 # the Bluetooth Userspace Daemon (BlueZ) and that daemon's object for 122 # the Bluetooth Adapter. 123 self._system_bus = dbus.SystemBus() 124 self._update_bluez() 125 self._update_adapter() 126 127 # The agent to handle pin code request, which will be 128 # created when user calls pair_legacy_device method. 129 self._pin_agent = None 130 131 132 def _update_bluez(self): 133 """Store a D-Bus proxy for the Bluetooth daemon in self._bluez. 134 135 This may be called in a loop until it returns True to wait for the 136 daemon to be ready after it has been started. 137 138 @return True on success, False otherwise. 139 140 """ 141 self._bluez = None 142 try: 143 self._bluez = self._system_bus.get_object( 144 self.BLUEZ_SERVICE_NAME, 145 self.BLUEZ_MANAGER_PATH) 146 logging.debug('bluetoothd is running') 147 return True 148 except dbus.exceptions.DBusException, e: 149 if e.get_dbus_name() == self.DBUS_ERROR_SERVICEUNKNOWN: 150 logging.debug('bluetoothd is not running') 151 self._bluez = None 152 return False 153 else: 154 logging.error('Error updating Bluez!') 155 raise 156 157 158 def _update_adapter(self): 159 """Store a D-Bus proxy for the local adapter in self._adapter. 160 161 This may be called in a loop until it returns True to wait for the 162 daemon to be ready, and have obtained the adapter information itself, 163 after it has been started. 164 165 Since not all devices will have adapters, this will also return True 166 in the case where we have obtained an empty adapter index list from the 167 kernel. 168 169 @return True on success, including if there is no local adapter, 170 False otherwise. 171 172 """ 173 self._adapter = None 174 if self._bluez is None: 175 logging.warning('Bluez not found!') 176 return False 177 if not self._has_adapter: 178 logging.debug('Device has no adapter; returning') 179 return True 180 181 objects = self._bluez.GetManagedObjects( 182 dbus_interface=self.BLUEZ_MANAGER_IFACE) 183 for path, ifaces in objects.iteritems(): 184 logging.debug('%s -> %r', path, ifaces.keys()) 185 if self.BLUEZ_ADAPTER_IFACE in ifaces: 186 logging.debug('using adapter %s', path) 187 self._adapter = self._system_bus.get_object( 188 self.BLUEZ_SERVICE_NAME, 189 path) 190 return True 191 else: 192 logging.warning('No adapter found in interface!') 193 return False 194 195 196 @xmlrpc_server.dbus_safe(False) 197 def reset_on(self): 198 """Reset the adapter and settings and power up the adapter. 199 200 @return True on success, False otherwise. 201 202 """ 203 self._reset() 204 if not self._adapter: 205 return False 206 self._set_powered(True) 207 return True 208 209 210 @xmlrpc_server.dbus_safe(False) 211 def reset_off(self): 212 """Reset the adapter and settings, leave the adapter powered off. 213 214 @return True on success, False otherwise. 215 216 """ 217 self._reset() 218 return True 219 220 221 def has_adapter(self): 222 """Return if an adapter is present. 223 224 This will only return True if we have determined both that there is 225 a Bluetooth adapter on this device (kernel adapter index list is not 226 empty) and that the Bluetooth daemon has exported an object for it. 227 228 @return True if an adapter is present, False if not. 229 230 """ 231 return self._has_adapter and self._adapter is not None 232 233 234 def _reset(self): 235 """Reset the Bluetooth adapter and settings.""" 236 logging.debug('_reset') 237 if self._adapter: 238 self._set_powered(False) 239 240 try: 241 self._bluetoothd.Stop(dbus.Array(signature='s'), True, 242 dbus_interface=self.UPSTART_JOB_IFACE) 243 except dbus.exceptions.DBusException, e: 244 if e.get_dbus_name() != self.UPSTART_ERROR_UNKNOWNINSTANCE: 245 logging.error('Error resetting adapter!') 246 raise 247 248 def bluez_stopped(): 249 """Checks the bluetooth daemon status. 250 251 @returns: True if bluez is stopped. False otherwise. 252 253 """ 254 return not self._update_bluez() 255 256 logging.debug('waiting for bluez stop') 257 utils.poll_for_condition( 258 condition=bluez_stopped, 259 desc='Bluetooth Daemon has stopped.', 260 timeout=self.ADAPTER_TIMEOUT) 261 262 for subdir in os.listdir(self.BLUETOOTH_LIBDIR): 263 shutil.rmtree(os.path.join(self.BLUETOOTH_LIBDIR, subdir)) 264 265 self._bluetoothd.Start(dbus.Array(signature='s'), True, 266 dbus_interface=self.UPSTART_JOB_IFACE) 267 268 logging.debug('waiting for bluez start') 269 utils.poll_for_condition( 270 condition=self._update_bluez, 271 desc='Bluetooth Daemon has started.', 272 timeout=self.ADAPTER_TIMEOUT) 273 274 logging.debug('waiting for bluez to obtain adapter information') 275 utils.poll_for_condition( 276 condition=self._update_adapter, 277 desc='Bluetooth Daemon has adapter information.', 278 timeout=self.ADAPTER_TIMEOUT) 279 280 281 @xmlrpc_server.dbus_safe(False) 282 def set_powered(self, powered): 283 """Set the adapter power state. 284 285 @param powered: adapter power state to set (True or False). 286 287 @return True on success, False otherwise. 288 289 """ 290 if not self._adapter: 291 if not powered: 292 # Return success if we are trying to power off an adapter that's 293 # missing or gone away, since the expected result has happened. 294 return True 295 else: 296 logging.warning('Adapter not found!') 297 return False 298 self._set_powered(powered) 299 return True 300 301 302 def _set_powered(self, powered): 303 """Set the adapter power state. 304 305 @param powered: adapter power state to set (True or False). 306 307 """ 308 logging.debug('_set_powered %r', powered) 309 self._adapter.Set(self.BLUEZ_ADAPTER_IFACE, 'Powered', powered, 310 dbus_interface=dbus.PROPERTIES_IFACE) 311 312 313 @xmlrpc_server.dbus_safe(False) 314 def set_discoverable(self, discoverable): 315 """Set the adapter discoverable state. 316 317 @param discoverable: adapter discoverable state to set (True or False). 318 319 @return True on success, False otherwise. 320 321 """ 322 if not discoverable and not self._adapter: 323 # Return success if we are trying to make an adapter that's 324 # missing or gone away, undiscoverable, since the expected result 325 # has happened. 326 return True 327 self._adapter.Set(self.BLUEZ_ADAPTER_IFACE, 328 'Discoverable', discoverable, 329 dbus_interface=dbus.PROPERTIES_IFACE) 330 return True 331 332 333 @xmlrpc_server.dbus_safe(False) 334 def set_pairable(self, pairable): 335 """Set the adapter pairable state. 336 337 @param pairable: adapter pairable state to set (True or False). 338 339 @return True on success, False otherwise. 340 341 """ 342 self._adapter.Set(self.BLUEZ_ADAPTER_IFACE, 'Pairable', pairable, 343 dbus_interface=dbus.PROPERTIES_IFACE) 344 return True 345 346 347 @xmlrpc_server.dbus_safe(False) 348 def get_adapter_properties(self): 349 """Read the adapter properties from the Bluetooth Daemon. 350 351 @return the properties as a JSON-encoded dictionary on success, 352 the value False otherwise. 353 354 """ 355 objects = self._bluez.GetManagedObjects( 356 dbus_interface=self.BLUEZ_MANAGER_IFACE) 357 adapter = objects[self._adapter.object_path][self.BLUEZ_ADAPTER_IFACE] 358 return json.dumps(adapter) 359 360 361 def read_version(self): 362 """Read the version of the management interface from the Kernel. 363 364 @return the information as a JSON-encoded tuple of: 365 ( version, revision ) 366 367 """ 368 return json.dumps(self._control.read_version()) 369 370 371 def read_supported_commands(self): 372 """Read the set of supported commands from the Kernel. 373 374 @return the information as a JSON-encoded tuple of: 375 ( commands, events ) 376 377 """ 378 return json.dumps(self._control.read_supported_commands()) 379 380 381 def read_index_list(self): 382 """Read the list of currently known controllers from the Kernel. 383 384 @return the information as a JSON-encoded array of controller indexes. 385 386 """ 387 return json.dumps(self._control.read_index_list()) 388 389 390 def read_info(self): 391 """Read the adapter information from the Kernel. 392 393 @return the information as a JSON-encoded tuple of: 394 ( address, bluetooth_version, manufacturer_id, 395 supported_settings, current_settings, class_of_device, 396 name, short_name ) 397 398 """ 399 return json.dumps(self._control.read_info(0)) 400 401 402 def add_device(self, address, address_type, action): 403 """Add a device to the Kernel action list. 404 405 @param address: Address of the device to add. 406 @param address_type: Type of device in @address. 407 @param action: Action to take. 408 409 @return on success, a JSON-encoded typle of: 410 ( address, address_type ), None on failure. 411 412 """ 413 return json.dumps(self._control.add_device( 414 0, address, address_type, action)) 415 416 417 def remove_device(self, address, address_type): 418 """Remove a device from the Kernel action list. 419 420 @param address: Address of the device to remove. 421 @param address_type: Type of device in @address. 422 423 @return on success, a JSON-encoded typle of: 424 ( address, address_type ), None on failure. 425 426 """ 427 return json.dumps(self._control.remove_device( 428 0, address, address_type)) 429 430 431 @xmlrpc_server.dbus_safe(False) 432 def get_devices(self): 433 """Read information about remote devices known to the adapter. 434 435 @return the properties of each device as a JSON-encoded array of 436 dictionaries on success, the value False otherwise. 437 438 """ 439 objects = self._bluez.GetManagedObjects( 440 dbus_interface=self.BLUEZ_MANAGER_IFACE, byte_arrays=True) 441 devices = [] 442 for path, ifaces in objects.iteritems(): 443 if self.BLUEZ_DEVICE_IFACE in ifaces: 444 devices.append(objects[path][self.BLUEZ_DEVICE_IFACE]) 445 return json.dumps(devices) 446 447 448 @xmlrpc_server.dbus_safe(False) 449 def start_discovery(self): 450 """Start discovery of remote devices. 451 452 Obtain the discovered device information using get_devices(), called 453 stop_discovery() when done. 454 455 @return True on success, False otherwise. 456 457 """ 458 if not self._adapter: 459 return False 460 self._adapter.StartDiscovery(dbus_interface=self.BLUEZ_ADAPTER_IFACE) 461 return True 462 463 464 @xmlrpc_server.dbus_safe(False) 465 def stop_discovery(self): 466 """Stop discovery of remote devices. 467 468 @return True on success, False otherwise. 469 470 """ 471 if not self._adapter: 472 return False 473 self._adapter.StopDiscovery(dbus_interface=self.BLUEZ_ADAPTER_IFACE) 474 return True 475 476 477 def get_dev_info(self): 478 """Read raw HCI device information. 479 480 @return JSON-encoded tuple of: 481 (index, name, address, flags, device_type, bus_type, 482 features, pkt_type, link_policy, link_mode, 483 acl_mtu, acl_pkts, sco_mtu, sco_pkts, 484 err_rx, err_tx, cmd_tx, evt_rx, acl_tx, acl_rx, 485 sco_tx, sco_rx, byte_rx, byte_tx) on success, 486 None on failure. 487 488 """ 489 return json.dumps(self._raw.get_dev_info(0)) 490 491 492 @xmlrpc_server.dbus_safe(False) 493 def register_profile(self, path, uuid, options): 494 """Register new profile (service). 495 496 @param path: Path to the profile object. 497 @param uuid: Service Class ID of the service as string. 498 @param options: Dictionary of options for the new service, compliant 499 with BlueZ D-Bus Profile API standard. 500 501 @return True on success, False otherwise. 502 503 """ 504 profile_manager = dbus.Interface( 505 self._system_bus.get_object( 506 self.BLUEZ_SERVICE_NAME, 507 self.BLUEZ_PROFILE_MANAGER_PATH), 508 self.BLUEZ_PROFILE_MANAGER_IFACE) 509 profile_manager.RegisterProfile(path, uuid, options) 510 return True 511 512 513 @xmlrpc_server.dbus_safe(False) 514 def has_device(self, address): 515 """Checks if the device with a given address exists. 516 517 @param address: Address of the device. 518 519 @returns: True if there is a device with that address. 520 False otherwise. 521 522 """ 523 return self._find_device(address) != None 524 525 526 def _find_device(self, address): 527 """Finds the device with a given address. 528 529 Find the device with a given address and returns the 530 device interface. 531 532 @param address: Address of the device. 533 534 @returns: An 'org.bluez.Device1' interface to the device. 535 None if device can not be found. 536 537 """ 538 objects = self._bluez.GetManagedObjects( 539 dbus_interface=self.BLUEZ_MANAGER_IFACE) 540 for path, ifaces in objects.iteritems(): 541 device = ifaces.get(self.BLUEZ_DEVICE_IFACE) 542 if device is None: 543 continue 544 if (device['Address'] == address and 545 path.startswith(self._adapter.object_path)): 546 obj = self._system_bus.get_object( 547 self.BLUEZ_SERVICE_NAME, path) 548 return dbus.Interface(obj, self.BLUEZ_DEVICE_IFACE) 549 logging.error('Device not found') 550 return None 551 552 553 def _setup_pin_agent(self, pin): 554 """Initializes a _PinAgent and registers it to handle pin code request. 555 556 @param pin: The pin code this agent will answer. 557 558 """ 559 agent_path = '/test/agent' 560 if self._pin_agent: 561 logging.info('Removing the old agent before initializing a new one') 562 self._pin_agent.remove_from_connection() 563 self._pin_agent = None 564 self._pin_agent = _PinAgent(pin, self._system_bus, agent_path) 565 agent_manager = dbus.Interface( 566 self._system_bus.get_object(self.BLUEZ_SERVICE_NAME, 567 self.BLUEZ_AGENT_MANAGER_PATH), 568 self.BLUEZ_AGENT_MANAGER_IFACE) 569 try: 570 agent_manager.RegisterAgent(agent_path, 'NoInputNoOutput') 571 except dbus.exceptions.DBusException, e: 572 if e.get_dbus_name() == self.BLUEZ_ERROR_ALREADY_EXISTS: 573 logging.info('Unregistering old agent and registering the new') 574 agent_manager.UnregisterAgent(agent_path) 575 agent_manager.RegisterAgent(agent_path, 'NoInputNoOutput') 576 else: 577 logging.error('Error setting up pin agent: %s', e) 578 raise 579 logging.info('Agent registered') 580 581 582 def _is_paired(self, device): 583 """Checks if a device is paired. 584 585 @param device: An 'org.bluez.Device1' interface to the device. 586 587 @returns: True if device is paired. False otherwise. 588 589 """ 590 props = dbus.Interface(device, dbus.PROPERTIES_IFACE) 591 paired = props.Get(self.BLUEZ_DEVICE_IFACE, 'Paired') 592 return bool(paired) 593 594 595 def _is_connected(self, device): 596 """Checks if a device is connected. 597 598 @param device: An 'org.bluez.Device1' interface to the device. 599 600 @returns: True if device is connected. False otherwise. 601 602 """ 603 props = dbus.Interface(device, dbus.PROPERTIES_IFACE) 604 connected = props.Get(self.BLUEZ_DEVICE_IFACE, 'Connected') 605 logging.info('Got connected = %r', connected) 606 return bool(connected) 607 608 609 @xmlrpc_server.dbus_safe(False) 610 def pair_legacy_device(self, address, pin, timeout): 611 """Pairs a device with a given pin code. 612 613 Registers a agent who handles pin code request and 614 pairs a device with known pin code. 615 616 @param address: Address of the device to pair. 617 @param pin: The pin code of the device to pair. 618 @param timeout: The timeout in seconds for pairing. 619 620 @returns: True on success. False otherwise. 621 622 """ 623 device = self._find_device(address) 624 if not device: 625 logging.error('Device not found') 626 return False 627 if self._is_paired(device): 628 logging.info('Device is already paired') 629 return True 630 631 self._setup_pin_agent(pin) 632 mainloop = gobject.MainLoop() 633 634 635 def pair_reply(): 636 """Handler when pairing succeeded.""" 637 logging.info('Device paired') 638 mainloop.quit() 639 640 641 def pair_error(error): 642 """Handler when pairing failed. 643 644 @param error: one of errors defined in org.bluez.Error representing 645 the error in pairing. 646 647 """ 648 try: 649 error_name = error.get_dbus_name() 650 if error_name == 'org.freedesktop.DBus.Error.NoReply': 651 logging.error('Timed out. Cancelling pairing') 652 device.CancelPairing() 653 else: 654 logging.error('Pairing device failed: %s', error) 655 finally: 656 mainloop.quit() 657 658 659 device.Pair(reply_handler=pair_reply, error_handler=pair_error, 660 timeout=timeout * 1000) 661 mainloop.run() 662 return self._is_paired(device) 663 664 665 @xmlrpc_server.dbus_safe(False) 666 def remove_device_object(self, address): 667 """Removes a device object and the pairing information. 668 669 Calls RemoveDevice method to remove remote device 670 object and the pairing information. 671 672 @param address: Address of the device to unpair. 673 674 @returns: True on success. False otherwise. 675 676 """ 677 device = self._find_device(address) 678 if not device: 679 logging.error('Device not found') 680 return False 681 self._adapter.RemoveDevice( 682 device.object_path, dbus_interface=self.BLUEZ_ADAPTER_IFACE) 683 return True 684 685 686 @xmlrpc_server.dbus_safe(False) 687 def connect_device(self, address): 688 """Connects a device. 689 690 Connects a device if it is not connected. 691 692 @param address: Address of the device to connect. 693 694 @returns: True on success. False otherwise. 695 696 """ 697 device = self._find_device(address) 698 if not device: 699 logging.error('Device not found') 700 return False 701 if self._is_connected(device): 702 logging.info('Device is already connected') 703 return True 704 device.Connect() 705 return self._is_connected(device) 706 707 708 @xmlrpc_server.dbus_safe(False) 709 def device_is_connected(self, address): 710 """Checks if a device is connected. 711 712 @param address: Address of the device to connect. 713 714 @returns: True if device is connected. False otherwise. 715 716 """ 717 device = self._find_device(address) 718 if not device: 719 logging.error('Device not found') 720 return False 721 return self._is_connected(device) 722 723 724 @xmlrpc_server.dbus_safe(False) 725 def disconnect_device(self, address): 726 """Disconnects a device. 727 728 Disconnects a device if it is connected. 729 730 @param address: Address of the device to disconnect. 731 732 @returns: True on success. False otherwise. 733 734 """ 735 device = self._find_device(address) 736 if not device: 737 logging.error('Device not found') 738 return False 739 if not self._is_connected(device): 740 logging.info('Device is not connected') 741 return True 742 device.Disconnect() 743 return not self._is_connected(device) 744 745 746if __name__ == '__main__': 747 logging.basicConfig(level=logging.DEBUG) 748 handler = logging.handlers.SysLogHandler(address='/dev/log') 749 formatter = logging.Formatter( 750 'bluetooth_device_xmlrpc_server: [%(levelname)s] %(message)s') 751 handler.setFormatter(formatter) 752 logging.getLogger().addHandler(handler) 753 logging.debug('bluetooth_device_xmlrpc_server main...') 754 server = xmlrpc_server.XmlRpcServer( 755 'localhost', 756 constants.BLUETOOTH_DEVICE_XMLRPC_SERVER_PORT) 757 server.register_delegate(BluetoothDeviceXmlRpcDelegate()) 758 server.run() 759