• 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
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