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