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