1# Copyright 2020 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4"""Facade to access the bluetooth-related functionality."""
5
6from __future__ import absolute_import
7from __future__ import division
8from __future__ import print_function
9
10import base64
11import collections
12from datetime import datetime
13import dbus
14import dbus.mainloop.glib
15import dbus.service
16import glob
17import gobject
18import json
19import logging
20import logging.handlers
21import os
22import re
23import subprocess
24import functools
25import time
26
27import common
28from autotest_lib.client.bin import utils
29from autotest_lib.client.common_lib.cros.bluetooth import bluetooth_socket
30from autotest_lib.client.common_lib import error
31from autotest_lib.client.cros import dbus_util
32from autotest_lib.client.cros.udev_helpers import UdevadmInfo, UdevadmTrigger
33from autotest_lib.client.cros import xmlrpc_server
34from autotest_lib.client.cros.audio import (audio_test_data as
35                                            audio_test_data_module)
36from autotest_lib.client.cros.audio import check_quality
37from autotest_lib.client.cros.audio import cras_utils
38from autotest_lib.client.cros.audio.sox_utils import (
39        convert_format, convert_raw_file, get_file_length,
40        trim_silence_from_wav_file)
41from autotest_lib.client.cros.bluetooth import advertisement
42from autotest_lib.client.cros.bluetooth import adv_monitor_helper
43from autotest_lib.client.cros.bluetooth import output_recorder
44from autotest_lib.client.cros.power import sys_power
45import six
46from six.moves import map
47from six.moves import range
48
49CheckQualityArgsClass = collections.namedtuple(
50        'args_type', ['filename', 'rate', 'channel', 'bit_width'])
51
52
53def _dbus_byte_array_to_b64_string(dbus_byte_array):
54    """Base64 encodes a dbus byte array for use with the xml rpc proxy."""
55    return base64.standard_b64encode(bytearray(dbus_byte_array))
56
57
58def _b64_string_to_dbus_byte_array(b64_string):
59    """Base64 decodes a dbus byte array for use with the xml rpc proxy."""
60    dbus_array = dbus.Array([], signature=dbus.Signature('y'))
61    bytes = bytearray(base64.standard_b64decode(b64_string))
62    for byte in bytes:
63        dbus_array.append(dbus.Byte(byte))
64    return dbus_array
65
66
67def dbus_print_error(default_return_value=False):
68    """Catch all DBus exceptions and return the error.
69
70    Wrap a function with a try block that catches DBus exceptions and
71    returns the error with the specified return status. The exception is logged
72    to aid in debugging.
73
74    @param wrapped_function function to wrap.
75
76    """
77
78    def decorator(wrapped_function):
79        """Call a function and catch DBus errors.
80
81        @param wrapped_function function to call in dbus safe context.
82        @return function return value or default_return_value on failure.
83
84        """
85
86        @functools.wraps(wrapped_function)
87        def wrapper(*args, **kwargs):
88            """Pass args and kwargs to a dbus safe function.
89
90            @param args formal python arguments.
91            @param kwargs keyword python arguments.
92            @return function return value or default_return_value on failure.
93
94            """
95            logging.debug('%s()', wrapped_function.__name__)
96            try:
97                return wrapped_function(*args, **kwargs)
98
99            except dbus.exceptions.DBusException as e:
100                logging.debug(
101                        'Exception while performing operation %s: %s: %s',
102                        wrapped_function.__name__, e.get_dbus_name(),
103                        e.get_dbus_message())
104                return (default_return_value, str(e))
105
106        return wrapper
107
108    return decorator
109
110
111class LogRecorder(object):
112    """The LogRecorder class helps to collect logs without a listening thread"""
113
114    class LoggingException(Exception):
115        """A dummy exception class for LogRecorder class."""
116        pass
117
118    def __init__(self, log_path):
119        """Initialize log recorder object
120
121        @param log_path: string path to log file to record
122
123        @raises: LogRecorder.LoggingException on non-existent log file
124        """
125        if not os.path.isfile(log_path):
126            msg = 'Requested log file {} does not exist'.format(log_path)
127            raise LogRecorder.LoggingException(msg)
128
129        self.log_path = log_path
130
131        self.initial_log_size = -1
132        self.log_contents = []
133
134    def StartRecording(self):
135        """Mark initial log size for later comparison"""
136
137        self.initial_log_size = os.path.getsize(self.log_path)
138        self.log_contents = []
139
140    def StopRecording(self):
141        """Gather the logs since StartRecording was called
142
143        @raises: LogRecorder.LoggingException if:
144                - Log file disappeared since StartRecording was called
145                - Log file is smaller than when logging began
146                - StartRecording was never called
147        """
148        now_size = os.path.getsize(self.log_path)
149
150        if not os.path.isfile(self.log_path):
151            msg = 'File {} disappeared unexpectedly'.format(self.log_path)
152            raise LogRecorder.LoggingException(msg)
153
154        if now_size < self.initial_log_size:
155            msg = 'Log became smaller unexpectedly'
156            raise LogRecorder.LoggingException(msg)
157
158        if self.initial_log_size < 0:
159            msg = 'Recording stopped before it started'
160            raise LogRecorder.LoggingException(msg)
161
162        with open(self.log_path, 'r') as mf:
163            # Skip to the point where we started recording
164            mf.seek(self.initial_log_size)
165
166            readsize = now_size - self.initial_log_size
167            self.log_contents = mf.read(readsize).split('\n')
168
169    def LogContains(self, search_str):
170        """Performs simple string checking on each line from the collected log
171
172        @param search_str: string to be located within log contents. This arg
173                is expected to not span between lines in the logs
174
175        @returns: True if search_str was located in the collected log contents,
176                False otherwise
177        """
178
179        for line in self.log_contents:
180            if search_str in line:
181                return True
182
183        return False
184
185
186class InterleaveLogger(LogRecorder):
187    """LogRecorder class that focus on interleave scan"""
188
189    SYSLOG_PATH = '/var/log/messages'
190
191    # Example bluetooth kernel log:
192    # "2020-11-23T07:52:31.395941Z DEBUG kernel: [ 6469.811135] Bluetooth: "
193    # "cancel_interleave_scan() hci0: hci0 cancelling interleave scan"
194    KERNEL_LOG_PATTERN = ('([^ ]+) DEBUG kernel: \[.*\] Bluetooth: '
195                          '{FUNCTION}\(\) hci0: {LOG_STR}')
196    STATE_PATTERN = KERNEL_LOG_PATTERN.format(
197            FUNCTION='add_le_interleave_adv_monitor_scan',
198            LOG_STR='next state: (.+)')
199    CANCEL_PATTERN = KERNEL_LOG_PATTERN.format(
200            FUNCTION='cancel_interleave_scan',
201            LOG_STR='hci0 cancelling interleave scan')
202    SYSTIME_LENGTH = len('2020-12-18T00:11:22.345678')
203
204    def __init__(self):
205        """ Initialize object
206        """
207        self.reset()
208        self.state_pattern = re.compile(self.STATE_PATTERN)
209        self.cancel_pattern = re.compile(self.CANCEL_PATTERN)
210        super(InterleaveLogger, self).__init__(self.SYSLOG_PATH)
211
212    def reset(self):
213        """ Clear data between each log collection attempt
214        """
215        self.records = []
216        self.cancel_events = []
217
218    def StartRecording(self):
219        """ Reset the previous data and start recording.
220        """
221        self.reset()
222        super(InterleaveLogger, self).StartRecording()
223
224    def StopRecording(self):
225        """ Stop recording and parse logs
226            The following data will be set after this call
227
228            - self.records: a dictionary where each item is a record of
229                            interleave |state| and the |time| the state starts.
230                            |state| could be {'no filter', 'allowlist'}
231                            |time| is system time in sec
232
233            - self.cancel_events: a list of |time| when a interleave cancel
234                                  event log was found
235                                  |time| is system time in sec
236
237            @returns: True if StopRecording success, False otherwise
238
239        """
240        try:
241            super(InterleaveLogger, self).StopRecording()
242        except Exception as e:
243            logging.error(e)
244            return False
245
246        success = True
247
248        def sys_time_to_timestamp(time_str):
249            """ Return timestamp of time_str """
250
251            # This is to remove the suffix of time string, in some cases the
252            # time string ends with an extra 'Z', in other cases, the string
253            # ends with time zone (ex. '+08:00')
254            time_str = time_str[:self.SYSTIME_LENGTH]
255
256            try:
257                dt = datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%S.%f")
258            except Exception as e:
259                logging.error(e)
260                success = False
261                return 0
262
263            return time.mktime(dt.timetuple()) + dt.microsecond * (10**-6)
264
265        for line in self.log_contents:
266            line = line.strip().replace('\\r\\n', '')
267            state_pattern = self.state_pattern.search(line)
268            cancel_pattern = self.cancel_pattern.search(line)
269
270            if cancel_pattern:
271                time_str = cancel_pattern.groups()[0]
272                time_sec = sys_time_to_timestamp(time_str)
273                self.cancel_events.append(time_sec)
274
275            if state_pattern:
276                time_str, state = state_pattern.groups()
277                time_sec = sys_time_to_timestamp(time_str)
278                self.records.append({'time': time_sec, 'state': state})
279
280        return success
281
282
283class PairingAgent(dbus.service.Object):
284    """The agent handling the authentication process of bluetooth pairing.
285
286    PairingAgent overrides RequestPinCode method to return a given pin code.
287    User can use this agent to pair bluetooth device which has a known
288    pin code.
289
290    TODO (josephsih): more pairing modes other than pin code would be
291    supported later.
292
293    """
294
295    def __init__(self, pin, *args, **kwargs):
296        super(PairingAgent, self).__init__(*args, **kwargs)
297        self._pin = pin
298
299    @dbus.service.method('org.bluez.Agent1',
300                         in_signature='o',
301                         out_signature='s')
302    def RequestPinCode(self, device_path):
303        """Requests pin code for a device.
304
305        Returns the known pin code for the request.
306
307        @param device_path: The object path of the device.
308
309        @returns: The known pin code.
310
311        """
312        logging.info('RequestPinCode for %s; return %s', device_path,
313                     self._pin)
314        return self._pin
315
316
317class BluetoothFacadeNative(object):
318    """Exposes DUT methods called remotely during Bluetooth autotests.
319
320    All instance methods of this object without a preceding '_' are exposed via
321    an XML-RPC server. This is not a stateless handler object, which means that
322    if you store state inside the delegate, that state will remain around for
323    future calls.
324    """
325
326    UPSTART_PATH = 'unix:abstract=/com/ubuntu/upstart'
327    UPSTART_MANAGER_PATH = '/com/ubuntu/Upstart'
328    UPSTART_MANAGER_IFACE = 'com.ubuntu.Upstart0_6'
329    UPSTART_JOB_IFACE = 'com.ubuntu.Upstart0_6.Job'
330
331    UPSTART_ERROR_UNKNOWNINSTANCE = \
332            'com.ubuntu.Upstart0_6.Error.UnknownInstance'
333    UPSTART_ERROR_ALREADYSTARTED = \
334            'com.ubuntu.Upstart0_6.Error.AlreadyStarted'
335
336    BLUETOOTHD_JOB = 'bluetoothd'
337
338    DBUS_ERROR_SERVICEUNKNOWN = 'org.freedesktop.DBus.Error.ServiceUnknown'
339
340    BLUETOOTH_SERVICE_NAME = 'org.chromium.Bluetooth'
341    BLUEZ_SERVICE_NAME = 'org.bluez'
342    BLUEZ_MANAGER_PATH = '/'
343    BLUEZ_DEBUG_LOG_PATH = '/org/chromium/Bluetooth'
344    BLUEZ_DEBUG_LOG_IFACE = 'org.chromium.Bluetooth.Debug'
345    BLUEZ_MANAGER_IFACE = 'org.freedesktop.DBus.ObjectManager'
346    BLUEZ_ADAPTER_IFACE = 'org.bluez.Adapter1'
347    BLUEZ_BATTERY_IFACE = 'org.bluez.Battery1'
348    BLUEZ_DEVICE_IFACE = 'org.bluez.Device1'
349    BLUEZ_GATT_SERV_IFACE = 'org.bluez.GattService1'
350    BLUEZ_GATT_CHAR_IFACE = 'org.bluez.GattCharacteristic1'
351    BLUEZ_GATT_DESC_IFACE = 'org.bluez.GattDescriptor1'
352    BLUEZ_LE_ADVERTISING_MANAGER_IFACE = 'org.bluez.LEAdvertisingManager1'
353    BLUEZ_ADV_MONITOR_MANAGER_IFACE = 'org.bluez.AdvertisementMonitorManager1'
354    BLUEZ_AGENT_MANAGER_PATH = '/org/bluez'
355    BLUEZ_AGENT_MANAGER_IFACE = 'org.bluez.AgentManager1'
356    BLUEZ_PROFILE_MANAGER_PATH = '/org/bluez'
357    BLUEZ_PROFILE_MANAGER_IFACE = 'org.bluez.ProfileManager1'
358    BLUEZ_ERROR_ALREADY_EXISTS = 'org.bluez.Error.AlreadyExists'
359    BLUEZ_PLUGIN_DEVICE_IFACE = 'org.chromium.BluetoothDevice'
360    DBUS_PROP_IFACE = 'org.freedesktop.DBus.Properties'
361    AGENT_PATH = '/test/agent'
362
363    BLUETOOTH_LIBDIR = '/var/lib/bluetooth'
364    BTMON_STOP_DELAY_SECS = 3
365
366    # Due to problems transferring a date object, we convert to stringtime first
367    # This is the standard format that we will use.
368    OUT_DATE_FORMAT = '%Y-%m-%d %H:%M:%S.%f'
369
370    # Timeout for how long we'll wait for BlueZ and the Adapter to show up
371    # after reset.
372    ADAPTER_TIMEOUT = 30
373
374    # How long to wait for hid device
375    HID_TIMEOUT = 15
376    HID_CHECK_SECS = 2
377
378    # How long we should wait for property update signal before we cancel it
379    PROPERTY_UPDATE_TIMEOUT_MILLI_SECS = 5000
380
381    def __init__(self):
382        # Open the Bluetooth Raw socket to the kernel which provides us direct,
383        # raw, access to the HCI controller.
384        self._raw = bluetooth_socket.BluetoothRawSocket()
385
386        # Open the Bluetooth Control socket to the kernel which provides us
387        # raw management access to the Bluetooth Host Subsystem. Read the list
388        # of adapter indexes to determine whether or not this device has a
389        # Bluetooth Adapter or not.
390        self._control = bluetooth_socket.BluetoothControlSocket()
391        self._has_adapter = len(self._control.read_index_list()) > 0
392
393        # Create an Advertisement Monitor App Manager instance.
394        # This needs to be created before making any dbus connections as
395        # AdvMonitorAppMgr internally forks a new helper process and due to
396        # a limitation of python, it is not possible to fork a new process
397        # once any dbus connections are established.
398        self.advmon_appmgr = adv_monitor_helper.AdvMonitorAppMgr()
399
400        # Set up the connection to Upstart so we can start and stop services
401        # and fetch the bluetoothd job.
402        self._upstart_conn = dbus.connection.Connection(self.UPSTART_PATH)
403        self._upstart = self._upstart_conn.get_object(
404                None, self.UPSTART_MANAGER_PATH)
405
406        bluetoothd_path = self._upstart.GetJobByName(
407                self.BLUETOOTHD_JOB, dbus_interface=self.UPSTART_MANAGER_IFACE)
408        self._bluetoothd = self._upstart_conn.get_object(None, bluetoothd_path)
409
410        # Arrange for the GLib main loop to be the default.
411        dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
412
413        # Set up the connection to the D-Bus System Bus, get the object for
414        # the Bluetooth Userspace Daemon (BlueZ) and that daemon's object for
415        # the Bluetooth Adapter, and the advertising manager.
416        self._system_bus = dbus.SystemBus()
417        self._update_bluez()
418        self._update_adapter()
419        self._update_advertising()
420        self._update_adv_monitor_manager()
421
422        # The agent to handle pin code request, which will be
423        # created when user calls pair_legacy_device method.
424        self._pairing_agent = None
425        # The default capability of the agent.
426        self._capability = 'KeyboardDisplay'
427
428        # Initailize a btmon object to record bluetoothd's activity.
429        self.btmon = output_recorder.OutputRecorder(
430                'btmon', stop_delay_secs=self.BTMON_STOP_DELAY_SECS)
431
432        # Initialize a messages object to record general logging.
433        self.messages = LogRecorder('/var/log/messages')
434
435        self._cras_test_client = cras_utils.CrasTestClient()
436
437        self.advertisements = []
438        self.advmon_interleave_logger = InterleaveLogger()
439        self._chrc_property = None
440        self._timeout_id = 0
441        self._signal_watch = None
442        self._dbus_mainloop = gobject.MainLoop()
443
444    @xmlrpc_server.dbus_safe(False)
445    def set_debug_log_levels(self, dispatcher_vb, newblue_vb, bluez_vb,
446                             kernel_vb):
447        """Enable or disable the debug logs of bluetooth
448
449        @param dispatcher_vb: verbosity of btdispatcher debug log, either 0 or 1
450        @param newblue_vb: verbosity of newblued debug log, either 0 or 1
451        @param bluez_vb: verbosity of bluez debug log, either 0 or 1
452        @param kernel_vb: verbosity of kernel debug log, either 0 or 1
453
454        """
455
456        # TODO(b/145163508, b/145749798): update when debug logs is migrated to
457        #                                 bluez.
458        debug_object = self._system_bus.get_object(self.BLUETOOTH_SERVICE_NAME,
459                                                   self.BLUEZ_DEBUG_LOG_PATH)
460        debug_object.SetLevels(dbus.Byte(dispatcher_vb),
461                               dbus.Byte(newblue_vb),
462                               dbus.Byte(bluez_vb),
463                               dbus.Byte(kernel_vb),
464                               dbus_interface=self.BLUEZ_DEBUG_LOG_IFACE)
465        return
466
467    def log_message(self, msg):
468        """ log a message to /var/log/messages."""
469        try:
470            cmd = ['logger', msg]
471            subprocess.call(cmd)
472        except Exception as e:
473            logging.error("log_message %s failed with %s", cmd, str(e))
474
475    def is_wrt_supported(self):
476        """Check if Bluetooth adapter support WRT logs
477
478        WRT is supported on Intel adapters other than (StP2 and WP2)
479
480        @returns : True if adapter is Intel made.
481        """
482        # Dict of Intel Adapters that support WRT and vid:pid
483        vid_pid_dict = {
484                'HrP2': '8086:02f0',
485                'ThP2': '8086:2526',
486                'JfP2': '8086:31dc',
487                'JfP2-2': '8086:9df0'
488        }  # On Sarien/Arcada
489
490        def _get_lspci_vid_pid(output):
491            """ parse output of lspci -knn and get the vid:pid
492
493            output is of the form '01:00.0 Network controller [0280]:
494            \Intel Corporation Device [8086:2526] (rev 29)\n'
495
496            @returns : 'vid:pid' or None
497            """
498            try:
499                for i in output.split(b'\n'):
500                    if 'Network controller' in i.decode('utf-8'):
501                        logging.debug('Got line %s', i)
502                        if 'Intel Corporation' in i.decode('utf-8'):
503                            return i.split(b'[')[2].split(b']')[0]
504                return None
505            except Exception as e:
506                logging.debug('Exception in _get_lspci_vidpid %s', str(e))
507                return None
508
509        try:
510            cmd = ['lspci', '-knn']
511            output = subprocess.check_output(cmd)
512            vid_pid = _get_lspci_vid_pid(output)
513            logging.debug("got vid_pid %s", vid_pid)
514            if vid_pid is not None:
515                if vid_pid in list(vid_pid_dict.values()):
516                    return True
517        except Exception as e:
518            logging.error('is_intel_adapter  failed with %s', cmd, str(e))
519            return False
520
521    def enable_wrt_logs(self):
522        """ Enable WRT logs for Intel Bluetooth adapters.
523
524            This is applicable only to Intel adapters.
525            Execute a series of custom hciconfig commands to
526            setup WRT log collection
527
528            Precondition :
529                1) Check if the DUT has Intel controller other than StP2
530                2) Make sure the controller is powered on
531        """
532        fw_trace_cmd = (
533                'hcitool cmd 3f 7c 01 10 00 00 00 FE 81 02 80 04 00 00'
534                ' 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
535                ' 00 00 00 00 00 00 00')
536        ddc_read_cmd = 'hcitool cmd 3f 8c 28 01'
537        ddc_write_cmd_prefix = 'hcitool cmd 3f 8b 03 28 01'
538        hw_trace_cmd = (
539                'hcitool cmd 3f 6f 01 08 00 00 00 00 00 00 00 00 01 00'
540                ' 00 03 01 03 03 03 10 03 6A 0A 6A 0A 6A 0A 6A 0A 00 00'
541                ' 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
542                ' 00 00 00 00 00 00')
543        multi_comm_trace_str = ('000000F600000000005002000000003F3F3F3'
544                                'F3F003F000000000000000001000000000000000000'
545                                '000000000000000000000000000000000000000000'
546                                '00000000000000000000000000000000000000000'
547                                '00000000000000000')
548        multi_comm_trace_file = ('/sys/kernel/debug/ieee80211'
549                                 '/phy0/iwlwifi/iwlmvm/send_hcmd')
550
551        def _execute_cmd(cmd_str, msg=''):
552            """Wrapper around subprocess.check_output.
553
554            @params cmd: Command to be executed as a string
555            @params msg: Optional description of the command
556
557            @returns: (True, output) if execution succeeded
558                  (False, None) if execution failed
559
560            """
561            try:
562                logging.info('Executing %s cmd', msg)
563                cmd = cmd_str.split(' ')
564                logging.debug('command is "%s"', cmd)
565                output = subprocess.check_output(cmd)
566                logging.info('%s cmd successfully executed', msg)
567                logging.debug('output is %s', output)
568                return (True, output)
569            except Exception as e:
570                logging.error('Exception %s while executing %s command',
571                              str(e), msg)
572                return (False, None)
573
574        def _get_ddc_write_cmd(ddc_read_result, ddc_write_cmd_prefix):
575            """ Create ddc_write_cmd from read command
576
577           This function performs the following
578           1) Take the output of ddc_read_cmd which is in following form
579              '< HCI Command: ogf 0x3f, ocf 0x008c, plen 1\n
580               01 \n>
581               HCI Event: 0x0e plen 6\n  01 8C FC 12 00 18 \n'
582           2) Take the last value of the output
583              01 8C FC 12 00 ===>> 18 <====
584           3) Bitwise or with 0x40
585              0x18 | 0x40 = 0x58
586           4) Add it to the end of the ddc_write_cmd
587              'hcitool 01 8C FC 00 28 01 ===> 58 <===='
588
589           """
590            last_line = [
591                    i for i in ddc_read_result.strip().split(b'\n') if i != ''
592            ][-1]
593            last_byte = [i for i in last_line.split(b' ') if i != ''][-1]
594            processed_byte = hex(int(last_byte, 16) | 0x40).split('0x')[1]
595            cmd = ddc_write_cmd_prefix + ' ' + processed_byte
596            logging.debug('ddc_write_cmd is %s', cmd)
597            return cmd
598
599        try:
600            logging.info('Enabling WRT logs')
601            status, _ = _execute_cmd(fw_trace_cmd, 'FW trace cmd')
602            if not status:
603                logging.info('FW trace command execution failed')
604                return False
605
606            status, ddc_read_result = _execute_cmd(ddc_read_cmd, 'DDC Read')
607            if not status:
608                logging.info('DDC Read command  execution failed')
609                return False
610
611            ddc_write_cmd = _get_ddc_write_cmd(ddc_read_result,
612                                               ddc_write_cmd_prefix)
613            logging.debug('DDC Write command  is %s', ddc_write_cmd)
614            status, _ = _execute_cmd(ddc_write_cmd, 'DDC Write')
615            if not status:
616                logging.info('DDC Write commanad execution failed')
617                return False
618
619            status, hw_trace_result = _execute_cmd(hw_trace_cmd, 'HW trace')
620            if not status:
621                logging.info('HW Trace command  execution failed')
622                return False
623
624            logging.debug('Executing the multi_comm_trace cmd %s to file %s',
625                          multi_comm_trace_str, multi_comm_trace_file)
626            with open(multi_comm_trace_file, 'w') as f:
627                f.write(multi_comm_trace_str + '\n')
628                f.flush()
629
630            logging.info('WRT Logs enabled')
631            return True
632        except Exception as e:
633            logging.error('Exception %s while enabling WRT logs', str(e))
634            return False
635
636    def collect_wrt_logs(self):
637        """Collect the WRT logs for Intel Bluetooth adapters
638
639           This is applicable only to Intel adapters.
640           Execute following command to collect WRT log. The logs are
641           copied to /var/spool/crash/
642
643           'echo 1 > sudo tee /sys/kernel/debug/ieee80211/phy0'
644                           '/iwlwifi/iwlmvm/fw_dbg_collect'
645           This is to be called only after enable_wrt_logs is called
646
647
648           Precondition:
649                 1) enable_wrt_logs has been called
650        """
651
652        def _collect_logs():
653            """Execute command to collect wrt logs."""
654            try:
655                with open(
656                        '/sys/kernel/debug/ieee80211/phy0/iwlwifi/'
657                        'iwlmvm/fw_dbg_collect', 'w') as f:
658                    f.write('1')
659                    f.flush()
660                # There is some flakiness in log collection. This sleep
661                # is due to the flakiness
662                time.sleep(10)
663                return True
664            except Exception as e:
665                logging.error('Exception %s in _collect logs ', str(e))
666                return False
667
668        def _get_num_log_files():
669            """Return number of WRT log files."""
670            try:
671                return len(glob.glob('/var/spool/crash/devcoredump_iwlwifi*'))
672            except Exception as e:
673                logging.debug('Exception %s raised in _get_num_log_files',
674                              str(e))
675                return 0
676
677        try:
678            logging.info('Collecting WRT logs')
679            #
680            # The command to trigger the logs does seems to work always.
681            # As a workaround for this flakiness, execute it multiple times
682            # until a new log is created
683            #
684            num_logs_present = _get_num_log_files()
685            logging.debug('%s logs present', num_logs_present)
686            for i in range(10):
687                time.sleep(1)
688                logging.debug('Executing command to collect WRT logs ')
689                if _collect_logs():
690                    logging.debug('Command to collect WRT logs executed')
691                else:
692                    logging.debug('Command to collect WRT logs failed')
693                    continue
694
695                if _get_num_log_files() > num_logs_present:
696                    logging.info('Successfully collected WRT logs ')
697                    return True
698                else:
699                    logging.debug('Log file not written. Trying again')
700
701            logging.info('Unable to collect WRT logs')
702            return False
703        except Exception as e:
704            logging.error('Exception %s while collecting WRT logs', str(e))
705            return False
706
707    @xmlrpc_server.dbus_safe(False)
708    def start_bluetoothd(self):
709        """start bluetoothd.
710
711        This includes powering up the adapter.
712
713        @returns: True if bluetoothd is started correctly.
714                  False otherwise.
715
716        """
717        try:
718            self._bluetoothd.Start(dbus.Array(signature='s'),
719                                   True,
720                                   dbus_interface=self.UPSTART_JOB_IFACE)
721        except dbus.exceptions.DBusException as e:
722            # if bluetoothd was already started, the exception looks like
723            #     dbus.exceptions.DBusException:
724            #     com.ubuntu.Upstart0_6.Error.AlreadyStarted: Job is already
725            #     running: bluetoothd
726            if e.get_dbus_name() != self.UPSTART_ERROR_ALREADYSTARTED:
727                logging.error('Error starting bluetoothd: %s', e)
728                return False
729
730        logging.debug('waiting for bluez start')
731        try:
732            utils.poll_for_condition(condition=self._update_bluez,
733                                     desc='Bluetooth Daemon has started.',
734                                     timeout=self.ADAPTER_TIMEOUT)
735        except Exception as e:
736            logging.error('timeout: error starting bluetoothd: %s', e)
737            return False
738
739        # Waiting for the self._adapter object.
740        # This does not mean that the adapter is powered on.
741        logging.debug('waiting for bluez to obtain adapter information')
742        try:
743            utils.poll_for_condition(
744                    condition=self._update_adapter,
745                    desc='Bluetooth Daemon has adapter information.',
746                    timeout=self.ADAPTER_TIMEOUT)
747        except Exception as e:
748            logging.error('timeout: error starting adapter: %s', e)
749            return False
750
751        # Waiting for the self._advertising interface object.
752        logging.debug('waiting for bluez to obtain interface manager.')
753        try:
754            utils.poll_for_condition(
755                    condition=self._update_advertising,
756                    desc='Bluetooth Daemon has advertising interface.',
757                    timeout=self.ADAPTER_TIMEOUT)
758        except utils.TimeoutError:
759            logging.error('timeout: error getting advertising interface')
760            return False
761
762        return True
763
764    @xmlrpc_server.dbus_safe(False)
765    def stop_bluetoothd(self):
766        """stop bluetoothd.
767
768        @returns: True if bluetoothd is stopped correctly.
769                  False otherwise.
770
771        """
772
773        def bluez_stopped():
774            """Checks the bluetooth daemon status.
775
776            @returns: True if bluez is stopped. False otherwise.
777
778            """
779            return not self._update_bluez()
780
781        try:
782            self._bluetoothd.Stop(dbus.Array(signature='s'),
783                                  True,
784                                  dbus_interface=self.UPSTART_JOB_IFACE)
785        except dbus.exceptions.DBusException as e:
786            # If bluetoothd was stopped already, the exception looks like
787            #    dbus.exceptions.DBusException:
788            #    com.ubuntu.Upstart0_6.Error.UnknownInstance: Unknown instance:
789            if e.get_dbus_name() != self.UPSTART_ERROR_UNKNOWNINSTANCE:
790                logging.error('Error stopping bluetoothd!')
791                return False
792
793        logging.debug('waiting for bluez stop')
794        try:
795            utils.poll_for_condition(condition=bluez_stopped,
796                                     desc='Bluetooth Daemon has stopped.',
797                                     timeout=self.ADAPTER_TIMEOUT)
798            bluetoothd_stopped = True
799        except Exception as e:
800            logging.error('timeout: error stopping bluetoothd: %s', e)
801            bluetoothd_stopped = False
802
803        return bluetoothd_stopped
804
805    def is_bluetoothd_running(self):
806        """Is bluetoothd running?
807
808        @returns: True if bluetoothd is running
809
810        """
811        return bool(self._get_dbus_proxy_for_bluetoothd())
812
813    def is_bluetoothd_proxy_valid(self):
814        """Checks whether the proxy object for bluetoothd is ok.
815
816        The dbus proxy object (self._bluez) can become unusable if bluetoothd
817        crashes or restarts for any reason. This method checks whether this has
818        happened by attempting to use the object proxy. If bluetoothd has
819        restarted (or is not available), then the session will no longer be
820        valid and this will result in a dbus exception.
821
822        Returns:
823            True if the bluez proxy is still usable. False otherwise.
824        """
825
826        try:
827            _ = self._bluez.GetManagedObjects(
828                    dbus_interface=self.BLUEZ_MANAGER_IFACE)
829        except dbus.exceptions.DBusException:
830            return False
831
832        return True
833
834    def _update_bluez(self):
835        """Store a D-Bus proxy for the Bluetooth daemon in self._bluez.
836
837        This may be called in a loop until it returns True to wait for the
838        daemon to be ready after it has been started.
839
840        @return True on success, False otherwise.
841
842        """
843        self._bluez = self._get_dbus_proxy_for_bluetoothd()
844        return bool(self._bluez)
845
846    @xmlrpc_server.dbus_safe(False)
847    def _get_dbus_proxy_for_bluetoothd(self):
848        """Get the D-Bus proxy for the Bluetooth daemon.
849
850        @return True on success, False otherwise.
851
852        """
853        bluez = None
854        try:
855            bluez = self._system_bus.get_object(self.BLUEZ_SERVICE_NAME,
856                                                self.BLUEZ_MANAGER_PATH)
857            logging.debug('bluetoothd is running')
858        except dbus.exceptions.DBusException as e:
859            # When bluetoothd is not running, the exception looks like
860            #     dbus.exceptions.DBusException:
861            #     org.freedesktop.DBus.Error.ServiceUnknown: The name org.bluez
862            #     was not provided by any .service files
863            if e.get_dbus_name() == self.DBUS_ERROR_SERVICEUNKNOWN:
864                logging.debug('bluetoothd is not running')
865            else:
866                logging.error('Error getting dbus proxy for Bluez: %s', e)
867        return bluez
868
869    def _update_adapter(self):
870        """Store a D-Bus proxy for the local adapter in self._adapter.
871
872        This may be called in a loop until it returns True to wait for the
873        daemon to be ready, and have obtained the adapter information itself,
874        after it has been started.
875
876        Since not all devices will have adapters, this will also return True
877        in the case where we have obtained an empty adapter index list from the
878        kernel.
879
880        Note that this method does not power on the adapter.
881
882        @return True on success, including if there is no local adapter,
883            False otherwise.
884
885        """
886        self._adapter = None
887        if self._bluez is None:
888            logging.warning('Bluez not found!')
889            return False
890        if not self._has_adapter:
891            logging.debug('Device has no adapter; returning')
892            return True
893        self._adapter = self._get_adapter()
894        return bool(self._adapter)
895
896    def _update_advertising(self):
897        """Store a D-Bus proxy for the local advertising interface manager.
898
899        This may be called repeatedly in a loop until True is returned;
900        otherwise we wait for bluetoothd to start. After bluetoothd starts, we
901        check the existence of a local adapter and proceed to get the
902        advertisement interface manager.
903
904        Since not all devices will have adapters, this will also return True
905        in the case where there is no adapter.
906
907        @return True on success, including if there is no local adapter,
908                False otherwise.
909
910        """
911        self._advertising = None
912        if self._bluez is None:
913            logging.warning('Bluez not found!')
914            return False
915        if not self._has_adapter:
916            logging.debug('Device has no adapter; returning')
917            return True
918        self._advertising = self._get_advertising()
919        return bool(self._advertising)
920
921    def _update_adv_monitor_manager(self):
922        """Store a D-Bus proxy for the local advertisement monitor manager.
923
924        This may be called repeatedly in a loop until True is returned;
925        otherwise we wait for bluetoothd to start. After bluetoothd starts, we
926        check the existence of a local adapter and proceed to get the
927        advertisement monitor manager interface.
928
929        Since not all devices will have adapters, this will also return True
930        in the case where there is no adapter.
931
932        @return True on success, including if there is no local adapter,
933                False otherwise.
934
935        """
936        self._adv_monitor_manager = None
937        if self._bluez is None:
938            logging.warning('Bluez not found!')
939            return False
940        if not self._has_adapter:
941            logging.debug('Device has no adapter; returning without '
942                          'advertisement monitor manager')
943            return True
944        self._adv_monitor_manager = self._get_adv_monitor_manager()
945        return bool(self._adv_monitor_manager)
946
947    @xmlrpc_server.dbus_safe(False)
948    def _get_adapter(self):
949        """Get the D-Bus proxy for the local adapter.
950
951        @return the adapter on success. None otherwise.
952
953        """
954        objects = self._bluez.GetManagedObjects(
955                dbus_interface=self.BLUEZ_MANAGER_IFACE)
956        for path, ifaces in six.iteritems(objects):
957            logging.debug('%s -> %r', path, list(ifaces.keys()))
958            if self.BLUEZ_ADAPTER_IFACE in ifaces:
959                logging.debug('using adapter %s', path)
960                adapter = self._system_bus.get_object(self.BLUEZ_SERVICE_NAME,
961                                                      path)
962                return adapter
963        else:
964            logging.warning('No adapter found in interface!')
965            return None
966
967    @xmlrpc_server.dbus_safe(False)
968    def _get_advertising(self):
969        """Get the D-Bus proxy for the local advertising interface.
970
971        @return the advertising interface object.
972
973        """
974        return dbus.Interface(self._adapter,
975                              self.BLUEZ_LE_ADVERTISING_MANAGER_IFACE)
976
977    @xmlrpc_server.dbus_safe(False)
978    def _get_adv_monitor_manager(self):
979        """Get the D-Bus proxy for the local advertisement monitor manager.
980
981        @return the advertisement monitor manager interface object.
982
983        """
984        return dbus.Interface(self._adapter,
985                              self.BLUEZ_ADV_MONITOR_MANAGER_IFACE)
986
987    @xmlrpc_server.dbus_safe(False)
988    def reset_on(self):
989        """Reset the adapter and settings and power up the adapter.
990
991        @return True on success, False otherwise.
992
993        """
994        return self._reset(set_power=True)
995
996    @xmlrpc_server.dbus_safe(False)
997    def reset_off(self):
998        """Reset the adapter and settings, leave the adapter powered off.
999
1000        @return True on success, False otherwise.
1001
1002        """
1003        return self._reset(set_power=False)
1004
1005    def has_adapter(self):
1006        """Return if an adapter is present.
1007
1008        This will only return True if we have determined both that there is
1009        a Bluetooth adapter on this device (kernel adapter index list is not
1010        empty) and that the Bluetooth daemon has exported an object for it.
1011
1012        @return True if an adapter is present, False if not.
1013
1014        """
1015        return self._has_adapter and self._adapter is not None
1016
1017    def is_wake_enabled(self):
1018        """Checks whether the bluetooth adapter has wake enabled.
1019
1020        This will walk through all parents of the hci0 sysfs path and try to
1021        find one with a 'power/wakeup' entry and returns whether its value is
1022        'enabled'.
1023
1024        @return True if 'power/wakeup' of an hci0 parent is 'enabled'
1025        """
1026        enabled = self._is_wake_enabled()
1027        return enabled
1028
1029    def set_wake_enabled(self, value):
1030        """Sets wake enabled to the value if path exists.
1031
1032        This will walk through all parents of the hci0 sysfs path and write the
1033        value to the first one it finds.
1034
1035        Args:
1036            value: Sets power/wakeup to "enabled" if value is true, else
1037                   "disabled"
1038
1039        @return True if it wrote value to a power/wakeup, False otherwise
1040        """
1041        return self._set_wake_enabled(value)
1042
1043    def wait_for_hid_device(self, device_address):
1044        """Waits for hid device with given device address.
1045
1046        Args:
1047            device_address: Peripheral address
1048        """
1049
1050        def match_hid_to_device(hidpath, device_address):
1051            """Check if given hid syspath is for the given device address """
1052            # If the syspath has a uniq property that matches the peripheral
1053            # device's address, then it has matched
1054            props = UdevadmInfo.GetProperties(hidpath)
1055            if props.get('uniq', '').lower() == device_address.lower():
1056                logging.info('Found hid device for address {} at {}'.format(
1057                        device_address, hidpath))
1058                return True
1059            else:
1060                logging.info('Path {} is not right device.'.format(hidpath))
1061
1062            return False
1063
1064        start = datetime.now()
1065
1066        # Keep scanning udev for correct hid device
1067        while (datetime.now() - start).seconds <= self.HID_TIMEOUT:
1068            existing_inputs = UdevadmTrigger(
1069                    subsystem_match=['input']).DryRun()
1070            for entry in existing_inputs:
1071                bt_hid = any([t in entry for t in ['uhid', 'hci']])
1072                logging.info('udevadm trigger entry is {}: {}'.format(
1073                        bt_hid, entry))
1074
1075                if bt_hid and match_hid_to_device(entry, device_address):
1076                    return True
1077
1078            time.sleep(self.HID_CHECK_SECS)
1079
1080        return False
1081
1082    def _reset(self, set_power=False):
1083        """Remove remote devices and set adapter to set_power state.
1084
1085        Do not restart bluetoothd as this may incur a side effect.
1086        The unhappy chrome may disable the adapter randomly.
1087
1088        @param set_power: adapter power state to set (True or False).
1089
1090        @return True on success, False otherwise.
1091
1092        """
1093        logging.debug('_reset')
1094
1095        if not self._adapter:
1096            logging.warning('Adapter not found!')
1097            return False
1098
1099        objects = self._bluez.GetManagedObjects(
1100                dbus_interface=self.BLUEZ_MANAGER_IFACE, byte_arrays=True)
1101
1102        devices = []
1103        for path, ifaces in six.iteritems(objects):
1104            if self.BLUEZ_DEVICE_IFACE in ifaces:
1105                devices.append(objects[path][self.BLUEZ_DEVICE_IFACE])
1106
1107        # Turn on the adapter in order to remove all remote devices.
1108        if not self._is_powered_on():
1109            if not self._set_powered(True):
1110                logging.warning('Unable to power on the adapter')
1111                return False
1112
1113        for device in devices:
1114            logging.debug('removing %s', device.get('Address'))
1115            self.remove_device_object(device.get('Address'))
1116
1117        # Toggle power to the adapter.
1118        if not self._set_powered(False):
1119            logging.warning('Unable to power off adapter')
1120            return False
1121        if set_power and not self._set_powered(True):
1122            logging.warning('Unable to power on adapter')
1123            return False
1124
1125        return True
1126
1127    @xmlrpc_server.dbus_safe(False)
1128    def set_powered(self, powered):
1129        """Set the adapter power state.
1130
1131        @param powered: adapter power state to set (True or False).
1132
1133        @return True on success, False otherwise.
1134
1135        """
1136        if not self._adapter:
1137            if not powered:
1138                # Return success if we are trying to power off an adapter that's
1139                # missing or gone away, since the expected result has happened.
1140                return True
1141            else:
1142                logging.warning('Adapter not found!')
1143                return False
1144        return self._set_powered(powered)
1145
1146    @xmlrpc_server.dbus_safe(False)
1147    def _set_powered(self, powered):
1148        """Set the adapter power state.
1149
1150        @param powered: adapter power state to set (True or False).
1151
1152        """
1153        logging.debug('_set_powered %r', powered)
1154        self._adapter.Set(self.BLUEZ_ADAPTER_IFACE,
1155                          'Powered',
1156                          dbus.Boolean(powered, variant_level=1),
1157                          dbus_interface=dbus.PROPERTIES_IFACE)
1158        return True
1159
1160    @xmlrpc_server.dbus_safe(False)
1161    def set_discoverable(self, discoverable):
1162        """Set the adapter discoverable state.
1163
1164        @param discoverable: adapter discoverable state to set (True or False).
1165
1166        @return True on success, False otherwise.
1167
1168        """
1169        if not discoverable and not self._adapter:
1170            # Return success if we are trying to make an adapter that's
1171            # missing or gone away, undiscoverable, since the expected result
1172            # has happened.
1173            return True
1174        self._adapter.Set(self.BLUEZ_ADAPTER_IFACE,
1175                          'Discoverable',
1176                          dbus.Boolean(discoverable, variant_level=1),
1177                          dbus_interface=dbus.PROPERTIES_IFACE)
1178        return True
1179
1180    @xmlrpc_server.dbus_safe(False)
1181    def get_discoverable_timeout(self):
1182        """Get the adapter discoverable_timeout.
1183
1184        @return True on success, False otherwise.
1185
1186        """
1187        return int(
1188                self._adapter.Get(self.BLUEZ_ADAPTER_IFACE,
1189                                  'DiscoverableTimeout',
1190                                  dbus_interface=dbus.PROPERTIES_IFACE))
1191
1192    @xmlrpc_server.dbus_safe(False)
1193    def set_discoverable_timeout(self, discoverable_timeout):
1194        """Set the adapter discoverable_timeout property.
1195
1196        @param discoverable_timeout: adapter discoverable_timeout value
1197               in seconds to set (Integer).
1198
1199        @return True on success, False otherwise.
1200
1201        """
1202        self._adapter.Set(self.BLUEZ_ADAPTER_IFACE,
1203                          'DiscoverableTimeout',
1204                          dbus.UInt32(discoverable_timeout, variant_level=1),
1205                          dbus_interface=dbus.PROPERTIES_IFACE)
1206        return True
1207
1208    @xmlrpc_server.dbus_safe(False)
1209    def get_pairable_timeout(self):
1210        """Get the adapter pairable_timeout.
1211
1212        @return True on success, False otherwise.
1213
1214        """
1215        return int(
1216                self._adapter.Get(self.BLUEZ_ADAPTER_IFACE,
1217                                  'PairableTimeout',
1218                                  dbus_interface=dbus.PROPERTIES_IFACE))
1219
1220    @xmlrpc_server.dbus_safe(False)
1221    def set_pairable_timeout(self, pairable_timeout):
1222        """Set the adapter pairable_timeout property.
1223
1224        @param pairable_timeout: adapter pairable_timeout value
1225               in seconds to set (Integer).
1226
1227        @return True on success, False otherwise.
1228
1229        """
1230        self._adapter.Set(self.BLUEZ_ADAPTER_IFACE,
1231                          'PairableTimeout',
1232                          dbus.UInt32(pairable_timeout, variant_level=1),
1233                          dbus_interface=dbus.PROPERTIES_IFACE)
1234        return True
1235
1236    @xmlrpc_server.dbus_safe(False)
1237    def set_pairable(self, pairable):
1238        """Set the adapter pairable state.
1239
1240        @param pairable: adapter pairable state to set (True or False).
1241
1242        @return True on success, False otherwise.
1243
1244        """
1245        self._adapter.Set(self.BLUEZ_ADAPTER_IFACE,
1246                          'Pairable',
1247                          dbus.Boolean(pairable, variant_level=1),
1248                          dbus_interface=dbus.PROPERTIES_IFACE)
1249        return True
1250
1251    @xmlrpc_server.dbus_safe(False)
1252    def set_adapter_alias(self, alias):
1253        """Set the adapter alias.
1254
1255        @param alias: adapter alias to set with type String
1256
1257        @return True on success, False otherwise.
1258        """
1259        self._adapter.Set(self.BLUEZ_ADAPTER_IFACE,
1260                          'Alias',
1261                          dbus.String(alias),
1262                          dbus_interface=dbus.PROPERTIES_IFACE)
1263        return True
1264
1265    @xmlrpc_server.dbus_safe(False)
1266    def _get_adapter_properties(self):
1267        """Read the adapter properties from the Bluetooth Daemon.
1268
1269        @return the properties as a JSON-encoded dictionary on success,
1270            the value False otherwise.
1271
1272        """
1273        if self._bluez and self._adapter:
1274            objects = self._bluez.GetManagedObjects(
1275                    dbus_interface=self.BLUEZ_MANAGER_IFACE)
1276            props = objects[self._adapter.object_path][
1277                    self.BLUEZ_ADAPTER_IFACE]
1278        else:
1279            props = {}
1280        logging.debug('get_adapter_properties')
1281        for i in props.items():
1282            logging.debug(i)
1283        return props
1284
1285    def get_adapter_properties(self):
1286        return json.dumps(self._get_adapter_properties())
1287
1288    def _is_powered_on(self):
1289        return bool(self._get_adapter_properties().get(u'Powered'))
1290
1291    def _get_wake_enabled_path(self):
1292        # Walk up the parents from hci0 sysfs path and find the first one with
1293        # a power/wakeup property. Return that path (including power/wakeup).
1294
1295        # Resolve hci path to get full device path (i.e. w/ usb or uart)
1296        search_at = os.path.realpath('/sys/class/bluetooth/hci0')
1297
1298        # Exit early if path doesn't exist
1299        if not os.path.exists(search_at):
1300            return None
1301
1302        # Walk up parents and try to find one with 'power/wakeup'
1303        for _ in range(search_at.count('/') - 1):
1304            search_at = os.path.normpath(os.path.join(search_at, '..'))
1305            try:
1306                path = os.path.join(search_at, 'power', 'wakeup')
1307                with open(path, 'r') as f:
1308                    return path
1309            except IOError:
1310                # No power wakeup at the given location so keep going
1311                continue
1312
1313        return None
1314
1315    def _is_wake_enabled(self):
1316        search_at = self._get_wake_enabled_path()
1317
1318        if search_at is not None:
1319            try:
1320                with open(search_at, 'r') as f:
1321                    value = f.read()
1322                    logging.info('Power/wakeup found at {}: {}'.format(
1323                            search_at, value))
1324                    return 'enabled' in value
1325            except IOError:
1326                # Path was not readable
1327                return False
1328
1329        logging.debug('No power/wakeup path found')
1330        return False
1331
1332    def _set_wake_enabled(self, value):
1333        path = self._get_wake_enabled_path()
1334        if path is not None:
1335            try:
1336                with open(path, 'w') as f:
1337                    f.write('enabled' if value else 'disabled')
1338                    return True
1339            except IOError:
1340                # Path was not writeable
1341                return False
1342
1343        return False
1344
1345    def read_version(self):
1346        """Read the version of the management interface from the Kernel.
1347
1348        @return the information as a JSON-encoded tuple of:
1349          ( version, revision )
1350
1351        """
1352        #TODO(howardchung): resolve 'cannot allocate memory' error when
1353        #                   BluetoothControlSocket idle too long(about 3 secs)
1354        #                   (b:137603211)
1355        _control = bluetooth_socket.BluetoothControlSocket()
1356        return json.dumps(_control.read_version())
1357
1358    def read_supported_commands(self):
1359        """Read the set of supported commands from the Kernel.
1360
1361        @return the information as a JSON-encoded tuple of:
1362          ( commands, events )
1363
1364        """
1365        #TODO(howardchung): resolve 'cannot allocate memory' error when
1366        #                   BluetoothControlSocket idle too long(about 3 secs)
1367        #                   (b:137603211)
1368        _control = bluetooth_socket.BluetoothControlSocket()
1369        return json.dumps(_control.read_supported_commands())
1370
1371    def read_index_list(self):
1372        """Read the list of currently known controllers from the Kernel.
1373
1374        @return the information as a JSON-encoded array of controller indexes.
1375
1376        """
1377        #TODO(howardchung): resolve 'cannot allocate memory' error when
1378        #                   BluetoothControlSocket idle too long(about 3 secs)
1379        #                   (b:137603211)
1380        _control = bluetooth_socket.BluetoothControlSocket()
1381        return json.dumps(_control.read_index_list())
1382
1383    def read_info(self):
1384        """Read the adapter information from the Kernel.
1385
1386        @return the information as a JSON-encoded tuple of:
1387          ( address, bluetooth_version, manufacturer_id,
1388            supported_settings, current_settings, class_of_device,
1389            name, short_name )
1390
1391        """
1392        #TODO(howardchung): resolve 'cannot allocate memory' error when
1393        #                   BluetoothControlSocket idle too long(about 3 secs)
1394        #                   (b:137603211)
1395        _control = bluetooth_socket.BluetoothControlSocket()
1396        return json.dumps(_control.read_info(0))
1397
1398    def add_device(self, address, address_type, action):
1399        """Add a device to the Kernel action list.
1400
1401        @param address: Address of the device to add.
1402        @param address_type: Type of device in @address.
1403        @param action: Action to take.
1404
1405        @return on success, a JSON-encoded typle of:
1406          ( address, address_type ), None on failure.
1407
1408        """
1409        #TODO(howardchung): resolve 'cannot allocate memory' error when
1410        #                   BluetoothControlSocket idle too long(about 3 secs)
1411        #                   (b:137603211)
1412        _control = bluetooth_socket.BluetoothControlSocket()
1413        return json.dumps(_control.add_device(0, address, address_type,
1414                                              action))
1415
1416    def remove_device(self, address, address_type):
1417        """Remove a device from the Kernel action list.
1418
1419        @param address: Address of the device to remove.
1420        @param address_type: Type of device in @address.
1421
1422        @return on success, a JSON-encoded typle of:
1423          ( address, address_type ), None on failure.
1424
1425        """
1426        #TODO(howardchung): resolve 'cannot allocate memory' error when
1427        #                   BluetoothControlSocket idle too long(about 3 secs)
1428        #                   (b:137603211)
1429        _control = bluetooth_socket.BluetoothControlSocket()
1430        return json.dumps(_control.remove_device(0, address, address_type))
1431
1432    @xmlrpc_server.dbus_safe(False)
1433    def _get_devices(self):
1434        """Read information about remote devices known to the adapter.
1435
1436        @return the properties of each device in a list
1437
1438        """
1439        objects = self._bluez.GetManagedObjects(
1440                dbus_interface=self.BLUEZ_MANAGER_IFACE, byte_arrays=True)
1441        devices = []
1442        for path, ifaces in six.iteritems(objects):
1443            if self.BLUEZ_DEVICE_IFACE in ifaces:
1444                devices.append(objects[path][self.BLUEZ_DEVICE_IFACE])
1445        return devices
1446
1447    def _encode_base64_json(self, data):
1448        """Base64 encode and json encode the data.
1449        Required to handle non-ascii data
1450
1451        @param data: data to be base64 and JSON encoded
1452
1453        @return: base64 and JSON encoded data
1454
1455        """
1456        logging.debug('_encode_base64_json raw data is %s', data)
1457        b64_encoded = utils.base64_recursive_encode(data)
1458        logging.debug('base64 encoded data is %s', b64_encoded)
1459        json_encoded = json.dumps(b64_encoded)
1460        logging.debug('JSON encoded data is %s', json_encoded)
1461        return json_encoded
1462
1463    def get_devices(self):
1464        """Read information about remote devices known to the adapter.
1465
1466        @return the properties of each device as a JSON-encoded array of
1467            dictionaries on success, the value False otherwise.
1468
1469        """
1470        devices = self._get_devices()
1471        return self._encode_base64_json(devices)
1472
1473    @xmlrpc_server.dbus_safe(None)
1474    def get_device_property(self, address, prop_name):
1475        """Read a property of BT device by directly querying device dbus object
1476
1477        @param address: Address of the device to query
1478        @param prop_name: Property to be queried
1479
1480        @return Base 64 JSON repr of property if device is found and has
1481                property, otherwise None on failure. JSON is a recursive
1482                converter, automatically converting dbus types to python natives
1483                and base64 allows us to pass special characters over xmlrpc.
1484                Decode is done in bluetooth_device.py
1485        """
1486
1487        prop_val = None
1488
1489        # Grab dbus object, _find_device will catch any thrown dbus error
1490        device_obj = self._find_device(address)
1491
1492        if device_obj:
1493            # Query dbus object for property
1494            prop_val = device_obj.Get(self.BLUEZ_DEVICE_IFACE,
1495                                      prop_name,
1496                                      dbus_interface=dbus.PROPERTIES_IFACE)
1497
1498        return self._encode_base64_json(prop_val)
1499
1500    @xmlrpc_server.dbus_safe(None)
1501    def get_battery_property(self, address, prop_name):
1502        """Read a property from Battery1 interface.
1503
1504        @param address: Address of the device to query
1505        @param prop_name: Property to be queried
1506
1507        @return The battery percentage value, or None if does not exist.
1508        """
1509
1510        prop_val = None
1511
1512        # Grab dbus object, _find_battery will catch any thrown dbus error
1513        battery_obj = self._find_battery(address)
1514
1515        if battery_obj:
1516            # Query dbus object for property
1517            prop_val = battery_obj.Get(self.BLUEZ_BATTERY_IFACE,
1518                                       prop_name,
1519                                       dbus_interface=dbus.PROPERTIES_IFACE)
1520
1521        return dbus_util.dbus2primitive(prop_val)
1522
1523    @xmlrpc_server.dbus_safe(False)
1524    def set_discovery_filter(self, filter):
1525        """Set the discovery filter.
1526
1527        @param filter: The discovery filter to set.
1528
1529        @return True on success, False otherwise.
1530
1531        """
1532        if not self._adapter:
1533            return False
1534        self._adapter.SetDiscoveryFilter(
1535                filter, dbus_interface=self.BLUEZ_ADAPTER_IFACE)
1536        return True
1537
1538    @xmlrpc_server.dbus_safe(False)
1539    @dbus_print_error()
1540    def start_discovery(self):
1541        """Start discovery of remote devices.
1542
1543        Obtain the discovered device information using get_devices(), called
1544        stop_discovery() when done.
1545
1546        @return True on success, False otherwise.
1547
1548        """
1549        if not self._adapter:
1550            return (False, "Adapter Not Found")
1551        self._adapter.StartDiscovery(dbus_interface=self.BLUEZ_ADAPTER_IFACE)
1552        return (True, None)
1553
1554    @dbus_print_error()
1555    def stop_discovery(self):
1556        """Stop discovery of remote devices.
1557
1558        @return True on success, False otherwise.
1559
1560        """
1561        if not self._adapter:
1562            return (False, "Adapter Not Found")
1563        self._adapter.StopDiscovery(dbus_interface=self.BLUEZ_ADAPTER_IFACE)
1564        return (True, None)
1565
1566    def get_dev_info(self):
1567        """Read raw HCI device information.
1568
1569        @return JSON-encoded tuple of:
1570                (index, name, address, flags, device_type, bus_type,
1571                       features, pkt_type, link_policy, link_mode,
1572                       acl_mtu, acl_pkts, sco_mtu, sco_pkts,
1573                       err_rx, err_tx, cmd_tx, evt_rx, acl_tx, acl_rx,
1574                       sco_tx, sco_rx, byte_rx, byte_tx) on success,
1575                None on failure.
1576
1577        """
1578        return json.dumps(self._raw.get_dev_info(0))
1579
1580    @dbus_print_error(None)
1581    def get_supported_capabilities(self):
1582        """ Get supported capabilities of the adapter
1583
1584        @returns (capabilities, None) on Success. (None, <error>) on failure
1585        """
1586        value = self._adapter.GetSupportedCapabilities(
1587                dbus_interface=self.BLUEZ_ADAPTER_IFACE)
1588        return (json.dumps(value), None)
1589
1590    @xmlrpc_server.dbus_safe(False)
1591    def register_profile(self, path, uuid, options):
1592        """Register new profile (service).
1593
1594        @param path: Path to the profile object.
1595        @param uuid: Service Class ID of the service as string.
1596        @param options: Dictionary of options for the new service, compliant
1597                        with BlueZ D-Bus Profile API standard.
1598
1599        @return True on success, False otherwise.
1600
1601        """
1602        profile_manager = dbus.Interface(
1603                self._system_bus.get_object(self.BLUEZ_SERVICE_NAME,
1604                                            self.BLUEZ_PROFILE_MANAGER_PATH),
1605                self.BLUEZ_PROFILE_MANAGER_IFACE)
1606        dbus_object = self._system_bus.get_object(self.BLUEZ_SERVICE_NAME,
1607                                                  path)
1608        profile_manager.RegisterProfile(
1609                dbus_object, uuid, dbus.Dictionary(options, signature='sv'))
1610        return True
1611
1612    def has_device(self, address):
1613        """Checks if the device with a given address exists.
1614
1615        @param address: Address of the device.
1616
1617        @returns: True if there is an interface object with that address.
1618                  False if the device is not found.
1619
1620        @raises: Exception if a D-Bus error is encountered.
1621
1622        """
1623        result = self._find_device(address)
1624        logging.debug('has_device result: %s', str(result))
1625
1626        # The result being False indicates that there is a D-Bus error.
1627        if result is False:
1628            raise Exception('dbus.Interface error')
1629
1630        # Return True if the result is not None, e.g. a D-Bus interface object;
1631        # False otherwise.
1632        return bool(result)
1633
1634    @xmlrpc_server.dbus_safe(False)
1635    def _find_device(self, address):
1636        """Finds the device with a given address.
1637
1638        Find the device with a given address and returns the
1639        device interface.
1640
1641        @param address: Address of the device.
1642
1643        @returns: An 'org.bluez.Device1' interface to the device.
1644                  None if device can not be found.
1645        """
1646        path = self._get_device_path(address)
1647        if path:
1648            obj = self._system_bus.get_object(self.BLUEZ_SERVICE_NAME, path)
1649            return dbus.Interface(obj, self.BLUEZ_DEVICE_IFACE)
1650        logging.info('Device not found')
1651        return None
1652
1653    @xmlrpc_server.dbus_safe(None)
1654    def _find_battery(self, address):
1655        """Finds the battery with a given address.
1656
1657        Find the battery with a given address and returns the
1658        battery interface.
1659
1660        @param address: Address of the device.
1661
1662        @returns: An 'org.bluez.Battery1' interface to the device.
1663                  None if device can not be found.
1664        """
1665        path = self._get_device_path(address)
1666        if path:
1667            obj = self._system_bus.get_object(self.BLUEZ_SERVICE_NAME, path)
1668            return dbus.Interface(obj, self.BLUEZ_BATTERY_IFACE)
1669        logging.info('Battery not found')
1670        return None
1671
1672    @xmlrpc_server.dbus_safe(False)
1673    def _get_device_path(self, address):
1674        """Gets the path for a device with a given address.
1675
1676        Find the device with a given address and returns the
1677        the path for the device.
1678
1679        @param address: Address of the device.
1680
1681        @returns: The path to the address of the device, or None if device is
1682            not found in the object tree.
1683
1684        """
1685
1686        # Create device path, i.e. '/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF' based
1687        # on path assignment scheme used in bluez
1688        address_up = address.replace(':', '_')
1689        device_path = '{}/dev_{}'.format(self._adapter.object_path, address_up)
1690
1691        # Verify the Address property agrees to confirm we have the device
1692        try:
1693            device = self._system_bus.get_object(self.BLUEZ_SERVICE_NAME,
1694                                                 device_path)
1695            found_addr = device.Get(self.BLUEZ_DEVICE_IFACE,
1696                                    'Address',
1697                                    dbus_interface=dbus.PROPERTIES_IFACE)
1698
1699            if found_addr == address:
1700                logging.info('Device found at {}'.format(device_path))
1701                return device_path
1702
1703        except dbus.exceptions.DBusException as e:
1704            log_msg = 'Couldn\'t reach device: {}'.format(str(e))
1705            logging.debug(log_msg)
1706
1707        logging.debug('No device found at {}'.format(device_path))
1708        return None
1709
1710    @xmlrpc_server.dbus_safe(False)
1711    def _setup_pairing_agent(self, pin):
1712        """Initializes and resiters a PairingAgent to handle authentication.
1713
1714        @param pin: The pin code this agent will answer.
1715
1716        """
1717        if self._pairing_agent:
1718            logging.info(
1719                    'Removing the old agent before initializing a new one')
1720            self._pairing_agent.remove_from_connection()
1721            self._pairing_agent = None
1722        self._pairing_agent = PairingAgent(pin, self._system_bus,
1723                                           self.AGENT_PATH)
1724        agent_manager = dbus.Interface(
1725                self._system_bus.get_object(self.BLUEZ_SERVICE_NAME,
1726                                            self.BLUEZ_AGENT_MANAGER_PATH),
1727                self.BLUEZ_AGENT_MANAGER_IFACE)
1728        try:
1729            agent_obj = self._system_bus.get_object(self.BLUEZ_SERVICE_NAME,
1730                                                    self.AGENT_PATH)
1731            agent_manager.RegisterAgent(agent_obj,
1732                                        dbus.String(self._capability))
1733        except dbus.exceptions.DBusException as e:
1734            if e.get_dbus_name() == self.BLUEZ_ERROR_ALREADY_EXISTS:
1735                logging.info('Unregistering old agent and registering the new')
1736                agent_manager.UnregisterAgent(agent_obj)
1737                agent_manager.RegisterAgent(agent_obj,
1738                                            dbus.String(self._capability))
1739            else:
1740                logging.error('Error setting up pin agent: %s', e)
1741                raise
1742        logging.info('Agent registered: %s', self.AGENT_PATH)
1743
1744    @xmlrpc_server.dbus_safe(False)
1745    def _is_paired(self, device):
1746        """Checks if a device is paired.
1747
1748        @param device: An 'org.bluez.Device1' interface to the device.
1749
1750        @returns: True if device is paired. False otherwise.
1751
1752        """
1753        props = dbus.Interface(device, dbus.PROPERTIES_IFACE)
1754        paired = props.Get(self.BLUEZ_DEVICE_IFACE, 'Paired')
1755        return bool(paired)
1756
1757    @xmlrpc_server.dbus_safe(False)
1758    def device_is_paired(self, address):
1759        """Checks if a device is paired.
1760
1761        @param address: address of the device.
1762
1763        @returns: True if device is paired. False otherwise.
1764
1765        """
1766        device = self._find_device(address)
1767        if not device:
1768            logging.error('Device not found')
1769            return False
1770        return self._is_paired(device)
1771
1772    @xmlrpc_server.dbus_safe(False)
1773    def _is_connected(self, device):
1774        """Checks if a device is connected.
1775
1776        @param device: An 'org.bluez.Device1' interface to the device.
1777
1778        @returns: True if device is connected. False otherwise.
1779
1780        """
1781        props = dbus.Interface(device, dbus.PROPERTIES_IFACE)
1782        connected = props.Get(self.BLUEZ_DEVICE_IFACE, 'Connected')
1783        logging.info('Got connected = %r', connected)
1784        return bool(connected)
1785
1786    @xmlrpc_server.dbus_safe(False)
1787    def _set_trusted_by_device(self, device, trusted=True):
1788        """Set the device trusted by device object.
1789
1790        @param device: the device object to set trusted.
1791        @param trusted: True or False indicating whether to set trusted or not.
1792
1793        @returns: True if successful. False otherwise.
1794
1795        """
1796        try:
1797            properties = dbus.Interface(device, self.DBUS_PROP_IFACE)
1798            properties.Set(self.BLUEZ_DEVICE_IFACE, 'Trusted',
1799                           dbus.Boolean(trusted, variant_level=1))
1800            return True
1801        except Exception as e:
1802            logging.error('_set_trusted_by_device: %s', e)
1803        except:
1804            logging.error('_set_trusted_by_device: unexpected error')
1805        return False
1806
1807    @xmlrpc_server.dbus_safe(False)
1808    def _set_trusted_by_path(self, device_path, trusted=True):
1809        """Set the device trusted by the device path.
1810
1811        @param device_path: the object path of the device.
1812        @param trusted: True or False indicating whether to set trusted or not.
1813
1814        @returns: True if successful. False otherwise.
1815
1816        """
1817        try:
1818            device = self._system_bus.get_object(self.BLUEZ_SERVICE_NAME,
1819                                                 device_path)
1820            return self._set_trusted_by_device(device, trusted)
1821        except Exception as e:
1822            logging.error('_set_trusted_by_path: %s', e)
1823        except:
1824            logging.error('_set_trusted_by_path: unexpected error')
1825        return False
1826
1827    @xmlrpc_server.dbus_safe(False)
1828    def set_trusted(self, address, trusted=True):
1829        """Set the device trusted by address.
1830
1831        @param address: The bluetooth address of the device.
1832        @param trusted: True or False indicating whether to set trusted or not.
1833
1834        @returns: True if successful. False otherwise.
1835
1836        """
1837        try:
1838            device = self._find_device(address)
1839            return self._set_trusted_by_device(device, trusted)
1840        except Exception as e:
1841            logging.error('set_trusted: %s', e)
1842        except:
1843            logging.error('set_trusted: unexpected error')
1844        return False
1845
1846    @xmlrpc_server.dbus_safe(False)
1847    def pair_legacy_device(self, address, pin, trusted, timeout=60):
1848        """Pairs a device with a given pin code.
1849
1850        Registers a agent who handles pin code request and
1851        pairs a device with known pin code. After pairing, this function will
1852        automatically connect to the device as well (prevents timing issues
1853        between pairing and connect and reduces overall test execution time).
1854
1855        @param address: Address of the device to pair.
1856        @param pin: The pin code of the device to pair.
1857        @param trusted: indicating whether to set the device trusted.
1858        @param timeout: The timeout in seconds for pairing.
1859
1860        @returns: True on success. False otherwise.
1861
1862        """
1863
1864        def connect_reply():
1865            """Handler when connect succeeded."""
1866            logging.info('Device connected: %s', device_path)
1867            mainloop.quit()
1868
1869        def connect_error(error):
1870            """Handler when connect failed.
1871
1872            @param error: one of the errors defined in org.bluez.Error
1873            representing the error in connect.
1874            """
1875            logging.error('Connect device failed: %s', error)
1876            mainloop.quit()
1877
1878        def pair_reply():
1879            """Handler when pairing succeeded."""
1880            logging.info('Device paired: %s', device_path)
1881            if trusted:
1882                self._set_trusted_by_path(device_path, trusted=True)
1883                logging.info('Device trusted: %s', device_path)
1884
1885            # On finishing pairing, also connect; let connect result exit
1886            # mainloop instead
1887            device.Connect(reply_handler=connect_reply,
1888                           error_handler=connect_error,
1889                           timeout=timeout * 1000)
1890
1891        def pair_error(error):
1892            """Handler when pairing failed.
1893
1894            @param error: one of errors defined in org.bluez.Error representing
1895                          the error in pairing.
1896
1897            """
1898            try:
1899                error_name = error.get_dbus_name()
1900                if error_name == 'org.freedesktop.DBus.Error.NoReply':
1901                    logging.error('Timed out after %d ms. Cancelling pairing.',
1902                                  timeout)
1903                    device.CancelPairing()
1904                else:
1905                    logging.error('Pairing device failed: %s', error)
1906            finally:
1907                mainloop.quit()
1908
1909        device = self._find_device(address)
1910        if not device:
1911            logging.error('Device not found')
1912            return False
1913
1914        device_path = device.object_path
1915        logging.info('Device %s is found.', device.object_path)
1916
1917        self._setup_pairing_agent(pin)
1918        mainloop = gobject.MainLoop()
1919
1920        try:
1921            if not self._is_paired(device):
1922                logging.info('Device is not paired. Pair and Connect.')
1923                device.Pair(reply_handler=pair_reply,
1924                            error_handler=pair_error,
1925                            timeout=timeout * 1000)
1926                mainloop.run()
1927            elif not self._is_connected(device):
1928                logging.info('Device is already paired. Connect.')
1929                device.Connect(reply_handler=connect_reply,
1930                               error_handler=connect_error,
1931                               timeout=timeout * 1000)
1932                mainloop.run()
1933        except Exception as e:
1934            logging.error('Exception %s in pair_legacy_device', e)
1935            return False
1936
1937        return self._is_paired(device) and self._is_connected(device)
1938
1939    @xmlrpc_server.dbus_safe(False)
1940    def remove_device_object(self, address):
1941        """Removes a device object and the pairing information.
1942
1943        Calls RemoveDevice method to remove remote device
1944        object and the pairing information.
1945
1946        @param address: Address of the device to unpair.
1947
1948        @returns: True on success. False otherwise.
1949
1950        """
1951        device = self._find_device(address)
1952        if not device:
1953            logging.error('Device not found')
1954            return False
1955        self._adapter.RemoveDevice(device.object_path,
1956                                   dbus_interface=self.BLUEZ_ADAPTER_IFACE)
1957        return True
1958
1959    @xmlrpc_server.dbus_safe(False)
1960    def connect_device(self, address):
1961        """Connects a device.
1962
1963        Connects a device if it is not connected.
1964
1965        @param address: Address of the device to connect.
1966
1967        @returns: True on success. False otherwise.
1968
1969        """
1970        device = self._find_device(address)
1971        if not device:
1972            logging.error('Device not found')
1973            return False
1974        if self._is_connected(device):
1975            logging.info('Device is already connected')
1976            return True
1977        device.Connect()
1978        return self._is_connected(device)
1979
1980    @xmlrpc_server.dbus_safe(False)
1981    def device_is_connected(self, address):
1982        """Checks if a device is connected.
1983
1984        @param address: Address of the device to connect.
1985
1986        @returns: True if device is connected. False otherwise.
1987
1988        """
1989        device = self._find_device(address)
1990        if not device:
1991            logging.error('Device not found')
1992            return False
1993        return self._is_connected(device)
1994
1995    @xmlrpc_server.dbus_safe(False)
1996    def disconnect_device(self, address):
1997        """Disconnects a device.
1998
1999        Disconnects a device if it is connected.
2000
2001        @param address: Address of the device to disconnect.
2002
2003        @returns: True on success. False otherwise.
2004
2005        """
2006        device = self._find_device(address)
2007        if not device:
2008            logging.error('Device not found')
2009            return False
2010        if not self._is_connected(device):
2011            logging.info('Device is not connected')
2012            return True
2013        device.Disconnect()
2014        return not self._is_connected(device)
2015
2016    @xmlrpc_server.dbus_safe(False)
2017    def _device_services_resolved(self, device):
2018        """Checks if services are resolved.
2019
2020        @param device: An 'org.bluez.Device1' interface to the device.
2021
2022        @returns: True if device is connected. False otherwise.
2023
2024        """
2025        logging.info('device for services resolved: %s', device)
2026        props = dbus.Interface(device, dbus.PROPERTIES_IFACE)
2027        resolved = props.Get(self.BLUEZ_DEVICE_IFACE, 'ServicesResolved')
2028        logging.info('Services resolved = %r', resolved)
2029        return bool(resolved)
2030
2031    @xmlrpc_server.dbus_safe(False)
2032    def device_services_resolved(self, address):
2033        """Checks if service discovery is complete on a device.
2034
2035        Checks whether service discovery has been completed..
2036
2037        @param address: Address of the remote device.
2038
2039        @returns: True on success. False otherwise.
2040
2041        """
2042        device = self._find_device(address)
2043        if not device:
2044            logging.error('Device not found')
2045            return False
2046
2047        if not self._is_connected(device):
2048            logging.info('Device is not connected')
2049            return False
2050
2051        return self._device_services_resolved(device)
2052
2053    def btmon_start(self):
2054        """Start btmon monitoring."""
2055        self.btmon.start()
2056
2057    def btmon_stop(self):
2058        """Stop btmon monitoring."""
2059        self.btmon.stop()
2060
2061    def btmon_get(self, search_str, start_str):
2062        """Get btmon output contents.
2063
2064        @param search_str: only lines with search_str would be kept.
2065        @param start_str: all lines before the occurrence of start_str would be
2066                filtered.
2067
2068        @returns: the recorded btmon output.
2069
2070        """
2071        return self.btmon.get_contents(search_str=search_str,
2072                                       start_str=start_str)
2073
2074    def btmon_find(self, pattern_str):
2075        """Find if a pattern string exists in btmon output.
2076
2077        @param pattern_str: the pattern string to find.
2078
2079        @returns: True on success. False otherwise.
2080
2081        """
2082        return self.btmon.find(pattern_str)
2083
2084    def messages_start(self):
2085        """Start messages monitoring.
2086
2087        @returns: True if logging started successfully, else False
2088        """
2089
2090        try:
2091            self.messages.StartRecording()
2092            return True
2093
2094        except Exception as e:
2095            logging.error('Failed to start log recording with error: %s', e)
2096
2097        return False
2098
2099    def messages_stop(self):
2100        """Stop messages monitoring.
2101
2102        @returns: True if logs were successfully gathered since logging started,
2103                else False
2104        """
2105        try:
2106            self.messages.StopRecording()
2107            return True
2108
2109        except Exception as e:
2110            logging.error('Failed to stop log recording with error: %s', e)
2111
2112        return False
2113
2114    def messages_find(self, pattern_str):
2115        """Find if a pattern string exists in messages output.
2116
2117        @param pattern_str: the pattern string to find.
2118
2119        @returns: True on success. False otherwise.
2120
2121        """
2122        return self.messages.LogContains(pattern_str)
2123
2124    @xmlrpc_server.dbus_safe(False)
2125    def dbus_async_method(self, dbus_method, reply_handler, error_handler,
2126                          *args):
2127        """Run an async dbus method.
2128
2129        @param dbus_method: the dbus async method to invoke.
2130        @param reply_handler: the reply handler for the dbus method.
2131        @param error_handler: the error handler for the dbus method.
2132        @param *args: additional arguments for the dbus method.
2133
2134        @returns: an empty string '' on success;
2135                  None if there is no _advertising interface manager; and
2136                  an error string if the dbus method fails or exception occurs
2137
2138        """
2139
2140        def successful_cb():
2141            """Called when the dbus_method completed successfully."""
2142            reply_handler()
2143            self.dbus_cb_msg = ''
2144            self._dbus_mainloop.quit()
2145
2146        def error_cb(error):
2147            """Called when the dbus_method failed."""
2148            error_handler(error)
2149            self.dbus_cb_msg = str(error)
2150            self._dbus_mainloop.quit()
2151
2152        # Call dbus_method with handlers.
2153        try:
2154            dbus_method(*args,
2155                        reply_handler=successful_cb,
2156                        error_handler=error_cb)
2157        except Exception as e:
2158            logging.error('Exception %s in dbus_async_method ', e)
2159            return str(e)
2160
2161        self._dbus_mainloop.run()
2162
2163        return self.dbus_cb_msg
2164
2165
2166    def advmon_check_manager_interface_exist(self):
2167        """Check if AdvertisementMonitorManager1 interface is available.
2168
2169        @returns: True if Manager interface is available, False otherwise.
2170
2171        """
2172        objects = self._bluez.GetManagedObjects(
2173                dbus_interface=self.BLUEZ_MANAGER_IFACE)
2174        for _, ifaces in six.iteritems(objects):
2175            if self.BLUEZ_ADV_MONITOR_MANAGER_IFACE in ifaces:
2176                return True
2177
2178        return False
2179
2180
2181    def advmon_read_supported_types(self):
2182        """Read the Advertisement Monitor supported monitor types.
2183
2184        Reads the value of 'SupportedMonitorTypes' property of the
2185        AdvertisementMonitorManager1 interface on the adapter.
2186
2187        @returns: the list of the supported monitor types.
2188
2189        """
2190        types = self._adapter.Get(self.BLUEZ_ADV_MONITOR_MANAGER_IFACE,
2191                                  'SupportedMonitorTypes',
2192                                  dbus_interface=self.DBUS_PROP_IFACE)
2193        return dbus_util.dbus2primitive(types)
2194
2195    def advmon_read_supported_features(self):
2196        """Read the Advertisement Monitor supported features.
2197
2198        Reads the value of 'SupportedFeatures' property of the
2199        AdvertisementMonitorManager1 interface on the adapter.
2200
2201        @returns: the list of the supported features.
2202
2203        """
2204        features = self._adapter.Get(self.BLUEZ_ADV_MONITOR_MANAGER_IFACE,
2205                                     'SupportedFeatures',
2206                                     dbus_interface=self.DBUS_PROP_IFACE)
2207        return dbus_util.dbus2primitive(features)
2208
2209    def advmon_create_app(self):
2210        """Create an advertisement monitor app.
2211
2212        @returns: app id, once the app is created.
2213
2214        """
2215        return self.advmon_appmgr.create_app()
2216
2217    def advmon_exit_app(self, app_id):
2218        """Exit an advertisement monitor app.
2219
2220        @param app_id: the app id.
2221
2222        @returns: True on success, False otherwise.
2223
2224        """
2225        return self.advmon_appmgr.exit_app(app_id)
2226
2227    def advmon_kill_app(self, app_id):
2228        """Kill an advertisement monitor app by sending SIGKILL.
2229
2230        @param app_id: the app id.
2231
2232        @returns: True on success, False otherwise.
2233
2234        """
2235        return self.advmon_appmgr.kill_app(app_id)
2236
2237    def advmon_register_app(self, app_id):
2238        """Register an advertisement monitor app.
2239
2240        @param app_id: the app id.
2241
2242        @returns: True on success, False otherwise.
2243
2244        """
2245        return self.advmon_appmgr.register_app(app_id)
2246
2247    def advmon_unregister_app(self, app_id):
2248        """Unregister an advertisement monitor app.
2249
2250        @param app_id: the app id.
2251
2252        @returns: True on success, False otherwise.
2253
2254        """
2255        return self.advmon_appmgr.unregister_app(app_id)
2256
2257    def advmon_add_monitor(self, app_id, monitor_data):
2258        """Create an Advertisement Monitor object.
2259
2260        @param app_id: the app id.
2261        @param monitor_data: the list containing monitor type, RSSI filter
2262                             values and patterns.
2263
2264        @returns: monitor id, once the monitor is created, None otherwise.
2265
2266        """
2267        return self.advmon_appmgr.add_monitor(app_id, monitor_data)
2268
2269    def advmon_remove_monitor(self, app_id, monitor_id):
2270        """Remove the Advertisement Monitor object.
2271
2272        @param app_id: the app id.
2273        @param monitor_id: the monitor id.
2274
2275        @returns: True on success, False otherwise.
2276
2277        """
2278        return self.advmon_appmgr.remove_monitor(app_id, monitor_id)
2279
2280    def advmon_get_event_count(self, app_id, monitor_id, event):
2281        """Read the count of a particular event on the given monitor.
2282
2283        @param app_id: the app id.
2284        @param monitor_id: the monitor id.
2285        @param event: name of the specific event or 'All' for all events.
2286
2287        @returns: count of the specific event or dict of counts of all events.
2288
2289        """
2290        return self.advmon_appmgr.get_event_count(app_id, monitor_id, event)
2291
2292    def advmon_reset_event_count(self, app_id, monitor_id, event):
2293        """Reset the count of a particular event on the given monitor.
2294
2295        @param app_id: the app id.
2296        @param monitor_id: the monitor id.
2297        @param event: name of the specific event or 'All' for all events.
2298
2299        @returns: True on success, False otherwise.
2300
2301        """
2302        return self.advmon_appmgr.reset_event_count(app_id, monitor_id, event)
2303
2304    def advmon_interleave_scan_logger_start(self):
2305        """ Start interleave logger recording
2306        """
2307        self.advmon_interleave_logger.StartRecording()
2308
2309    def advmon_interleave_scan_logger_stop(self):
2310        """ Stop interleave logger recording
2311
2312        @returns: True if logs were successfully collected,
2313                  False otherwise.
2314
2315        """
2316        return self.advmon_interleave_logger.StopRecording()
2317
2318    def advmon_interleave_scan_logger_get_records(self):
2319        """ Get records in previous log collections
2320
2321        @returns: a list of records, where each item is a record of
2322                  interleave |state| and the |time| the state starts.
2323                  |state| could be {'no filter', 'allowlist'}
2324                  |time| is system time in sec
2325
2326        """
2327        return self.advmon_interleave_logger.records
2328
2329    def advmon_interleave_scan_logger_get_cancel_events(self):
2330        """ Get cancel events in previous log collections
2331
2332        @returns: a list of cancel |time| when a interleave cancel event log
2333                  was found.
2334                  |time| is system time in sec
2335
2336        """
2337        return self.advmon_interleave_logger.cancel_events
2338
2339    def register_advertisement(self, advertisement_data):
2340        """Register an advertisement.
2341
2342        Note that rpc supports only conformable types. Hence, a
2343        dict about the advertisement is passed as a parameter such
2344        that the advertisement object could be constructed on the host.
2345
2346        @param advertisement_data: a dict of the advertisement to register.
2347
2348        @returns: True on success. False otherwise.
2349
2350        """
2351        adv = advertisement.Advertisement(self._system_bus, advertisement_data)
2352        self.advertisements.append(adv)
2353        return self.dbus_async_method(
2354                self._advertising.RegisterAdvertisement,
2355                # reply handler
2356                lambda: logging.info('register_advertisement: succeeded.'),
2357                # error handler
2358                lambda error: logging.error(
2359                        'register_advertisement: failed: %s', str(error)),
2360                # other arguments
2361                adv.get_path(),
2362                dbus.Dictionary({}, signature='sv'))
2363
2364    def unregister_advertisement(self, advertisement_data):
2365        """Unregister an advertisement.
2366
2367        Note that to unregister an advertisement, it is required to use
2368        the same self._advertising interface manager. This is because
2369        bluez only allows the same sender to invoke UnregisterAdvertisement
2370        method. Hence, watch out that the bluetoothd is not restarted or
2371        self.start_bluetoothd() is not executed between the time span that
2372        an advertisement is registered and unregistered.
2373
2374        @param advertisement_data: a dict of the advertisements to unregister.
2375
2376        @returns: True on success. False otherwise.
2377
2378        """
2379        path = advertisement_data.get('Path')
2380        for index, adv in enumerate(self.advertisements):
2381            if adv.get_path() == path:
2382                break
2383        else:
2384            logging.error('Fail to find the advertisement under the path: %s',
2385                          path)
2386            return False
2387
2388        result = self.dbus_async_method(
2389                self._advertising.UnregisterAdvertisement,
2390                # reply handler
2391                lambda: logging.info('unregister_advertisement: succeeded.'),
2392                # error handler
2393                lambda error: logging.error(
2394                        'unregister_advertisement: failed: %s', str(error)),
2395                # other arguments
2396                adv.get_path())
2397
2398        # Call remove_from_connection() so that the same path could be reused.
2399        adv.remove_from_connection()
2400        del self.advertisements[index]
2401
2402        return result
2403
2404    def set_advertising_intervals(self, min_adv_interval_ms,
2405                                  max_adv_interval_ms):
2406        """Set advertising intervals.
2407
2408        @param min_adv_interval_ms: the min advertising interval in ms.
2409        @param max_adv_interval_ms: the max advertising interval in ms.
2410
2411        @returns: True on success. False otherwise.
2412
2413        """
2414        return self.dbus_async_method(
2415                self._advertising.SetAdvertisingIntervals,
2416                # reply handler
2417                lambda: logging.info('set_advertising_intervals: succeeded.'),
2418                # error handler
2419                lambda error: logging.error(
2420                        'set_advertising_intervals: failed: %s', str(error)),
2421                # other arguments
2422                dbus.UInt16(min_adv_interval_ms),
2423                dbus.UInt16(max_adv_interval_ms))
2424
2425    def reset_advertising(self):
2426        """Reset advertising.
2427
2428        This includes un-registering all advertisements, reset advertising
2429        intervals, and disable advertising.
2430
2431        @returns: True on success. False otherwise.
2432
2433        """
2434        # It is required to execute remove_from_connection() to unregister the
2435        # object-path handler of each advertisement. In this way, we could
2436        # register an advertisement with the same path repeatedly.
2437        for adv in self.advertisements:
2438            adv.remove_from_connection()
2439        del self.advertisements[:]
2440
2441        return self.dbus_async_method(
2442                self._advertising.ResetAdvertising,
2443                # reply handler
2444                lambda: logging.info('reset_advertising: succeeded.'),
2445                # error handler
2446                lambda error: logging.error('reset_advertising: failed: %s',
2447                                            str(error)))
2448
2449    def create_audio_record_directory(self, audio_record_dir):
2450        """Create the audio recording directory.
2451
2452        @param audio_record_dir: the audio recording directory
2453
2454        @returns: True on success. False otherwise.
2455        """
2456        try:
2457            if not os.path.exists(audio_record_dir):
2458                os.makedirs(audio_record_dir)
2459            return True
2460        except Exception as e:
2461            logging.error('Failed to create %s on the DUT: %s',
2462                          audio_record_dir, e)
2463            return False
2464
2465    def start_capturing_audio_subprocess(self, audio_data, recording_device):
2466        """Start capturing audio in a subprocess.
2467
2468        @param audio_data: the audio test data
2469        @param recording_device: which device recorded the audio,
2470                possible values are 'recorded_by_dut' or 'recorded_by_peer'
2471
2472        @returns: True on success. False otherwise.
2473        """
2474        audio_data = json.loads(audio_data)
2475        return self._cras_test_client.start_capturing_subprocess(
2476                audio_data[recording_device],
2477                sample_format=audio_data['format'],
2478                channels=audio_data['channels'],
2479                rate=audio_data['rate'],
2480                duration=audio_data['duration'])
2481
2482    def stop_capturing_audio_subprocess(self):
2483        """Stop capturing audio.
2484
2485        @returns: True on success. False otherwise.
2486        """
2487        return self._cras_test_client.stop_capturing_subprocess()
2488
2489    def _generate_playback_file(self, audio_data):
2490        """Generate the playback file if it does not exist yet.
2491
2492        Some audio test files may be large. Generate them on the fly
2493        to save the storage of the source tree.
2494
2495        @param audio_data: the audio test data
2496        """
2497        if not os.path.exists(audio_data['file']):
2498            data_format = dict(file_type='raw',
2499                               sample_format='S16_LE',
2500                               channel=audio_data['channels'],
2501                               rate=audio_data['rate'])
2502
2503            # Make the audio file a bit longer to handle any delay
2504            # issue in capturing.
2505            duration = audio_data['duration'] + 3
2506            audio_test_data_module.GenerateAudioTestData(
2507                    data_format=data_format,
2508                    path=audio_data['file'],
2509                    duration_secs=duration,
2510                    frequencies=audio_data['frequencies'])
2511            logging.debug("Raw file generated: %s", audio_data['file'])
2512
2513    def start_playing_audio_subprocess(self, audio_data):
2514        """Start playing audio in a subprocess.
2515
2516        @param audio_data: the audio test data
2517
2518        @returns: True on success. False otherwise.
2519        """
2520        audio_data = json.loads(audio_data)
2521        self._generate_playback_file(audio_data)
2522        try:
2523            return self._cras_test_client.start_playing_subprocess(
2524                    audio_data['file'],
2525                    channels=audio_data['channels'],
2526                    rate=audio_data['rate'],
2527                    duration=audio_data['duration'])
2528        except Exception as e:
2529            logging.error("start_playing_subprocess() failed: %s", str(e))
2530            return False
2531
2532    def stop_playing_audio_subprocess(self):
2533        """Stop playing audio in the subprocess.
2534
2535        @returns: True on success. False otherwise.
2536        """
2537        return self._cras_test_client.stop_playing_subprocess()
2538
2539    def play_audio(self, audio_data):
2540        """Play audio.
2541
2542        It blocks until it has completed playing back the audio.
2543
2544        @param audio_data: the audio test data
2545
2546        @returns: True on success. False otherwise.
2547        """
2548        audio_data = json.loads(audio_data)
2549        self._generate_playback_file(audio_data)
2550        return self._cras_test_client.play(audio_data['file'],
2551                                           channels=audio_data['channels'],
2552                                           rate=audio_data['rate'],
2553                                           duration=audio_data['duration'])
2554
2555    def check_audio_frames_legitimacy(self, audio_test_data, recording_device,
2556                                      recorded_file):
2557        """Get the number of frames in the recorded audio file.
2558
2559        @param audio_test_data: the audio test data
2560        @param recording_device: which device recorded the audio,
2561                possible values are 'recorded_by_dut' or 'recorded_by_peer'
2562        @param recorded_file: the recorded file name
2563
2564        @returns: True if audio frames are legitimate.
2565        """
2566        if bool(recorded_file):
2567            recorded_filename = recorded_file
2568        else:
2569            audio_test_data = json.loads(audio_test_data)
2570            recorded_filename = audio_test_data[recording_device]
2571
2572        if recorded_filename.endswith('.raw'):
2573            # Make sure that the recorded file does not contain all zeros.
2574            filesize = os.path.getsize(recorded_filename)
2575            cmd_str = 'cmp -s -n %d %s /dev/zero' % (filesize,
2576                                                     recorded_filename)
2577            try:
2578                result = subprocess.call(cmd_str.split())
2579                return result != 0
2580            except Exception as e:
2581                logging.error("Failed: %s (%s)", cmd_str, str(e))
2582                return False
2583        else:
2584            # The recorded wav file should not be empty.
2585            wav_file = check_quality.WaveFile(recorded_filename)
2586            return wav_file.get_number_frames() > 0
2587
2588    def convert_audio_sample_rate(self, input_file, out_file, test_data,
2589                                  new_rate):
2590        """Convert audio file to new sample rate.
2591
2592        @param input_file: Path to file to upsample.
2593        @param out_file: Path to create upsampled file.
2594        @param test_data: Dictionary with information about file.
2595        @param new_rate: New rate to upsample file to.
2596
2597        @returns: True if upsampling succeeded, False otherwise.
2598        """
2599        test_data = json.loads(test_data)
2600        logging.debug('Resampling file {} to new rate {}'.format(
2601                input_file, new_rate))
2602
2603        convert_format(input_file,
2604                       test_data['channels'],
2605                       test_data['bit_width'],
2606                       test_data['rate'],
2607                       out_file,
2608                       test_data['channels'],
2609                       test_data['bit_width'],
2610                       new_rate,
2611                       1.0,
2612                       use_src_header=True,
2613                       use_dst_header=True)
2614
2615        return os.path.isfile(out_file)
2616
2617    def trim_wav_file(self,
2618                      in_file,
2619                      out_file,
2620                      new_duration,
2621                      test_data,
2622                      tolerance=0.1):
2623        """Trim long file to desired length.
2624
2625        Trims audio file to length by cutting out silence from beginning and
2626        end.
2627
2628        @param in_file: Path to audio file to be trimmed.
2629        @param out_file: Path to trimmed audio file to create.
2630        @param new_duration: A float representing the desired duration of
2631                the resulting trimmed file.
2632        @param test_data: Dictionary containing information about the test file.
2633        @param tolerance: (optional) A float representing the allowable
2634                difference between trimmed file length and desired duration
2635
2636        @returns: True if file was trimmed successfully, False otherwise.
2637        """
2638        test_data = json.loads(test_data)
2639        trim_silence_from_wav_file(in_file, out_file, new_duration)
2640        measured_length = get_file_length(out_file, test_data['channels'],
2641                                          test_data['bit_width'],
2642                                          test_data['rate'])
2643        return abs(measured_length - new_duration) <= tolerance
2644
2645    def unzip_audio_test_data(self, tar_path, data_dir):
2646        """Unzip audio test data files.
2647
2648        @param tar_path: Path to audio test data tarball on DUT.
2649        @oaram data_dir: Path to directory where to extract test data directory.
2650
2651        @returns: True if audio test data folder exists, False otherwise.
2652        """
2653        logging.debug('Downloading audio test data on DUT')
2654        # creates path to dir to extract test data to by taking name of the
2655        # tarball without the extension eg. <dir>/file.ext to data_dir/file/
2656        audio_test_dir = os.path.join(
2657                data_dir,
2658                os.path.split(tar_path)[1].split('.', 1)[0])
2659
2660        unzip_cmd = 'tar -xf {0} -C {1}'.format(tar_path, data_dir)
2661
2662        unzip_proc = subprocess.Popen(unzip_cmd.split(),
2663                                      stdout=subprocess.PIPE,
2664                                      stderr=subprocess.PIPE)
2665        _, stderr = unzip_proc.communicate()
2666
2667        if stderr:
2668            logging.error('Error occurred in unzipping audio data: {}'.format(
2669                    str(stderr)))
2670            return False
2671
2672        return unzip_proc.returncode == 0 and os.path.isdir(audio_test_dir)
2673
2674    def convert_raw_to_wav(self, input_file, output_file, test_data):
2675        """Convert raw audio file to wav file.
2676
2677        @oaram input_file: the location of the raw file
2678        @param output_file: the location to place the resulting wav file
2679        @param test_data: the data for the file being converted
2680
2681        @returns: True if conversion was successful otherwise false
2682        """
2683        test_data = json.loads(test_data)
2684        convert_raw_file(input_file, test_data['channels'],
2685                         test_data['bit_width'], test_data['rate'],
2686                         output_file)
2687
2688        return os.path.isfile(output_file)
2689
2690    def get_primary_frequencies(self, audio_test_data, recording_device,
2691                                recorded_file):
2692        """Get primary frequencies of the audio test file.
2693
2694        @param audio_test_data: the audio test data
2695        @param recording_device: which device recorded the audio,
2696                possible values are 'recorded_by_dut' or 'recorded_by_peer'
2697        @param recorded_file: the recorded file name
2698
2699        @returns: a list of primary frequencies of channels in the audio file
2700        """
2701        audio_test_data = json.loads(audio_test_data)
2702
2703        if bool(recorded_file):
2704            recorded_filename = recorded_file
2705        else:
2706            recorded_filename = audio_test_data[recording_device]
2707
2708        args = CheckQualityArgsClass(filename=recorded_filename,
2709                                     rate=audio_test_data['rate'],
2710                                     channel=audio_test_data['channels'],
2711                                     bit_width=16)
2712        raw_data, rate = check_quality.read_audio_file(args)
2713        checker = check_quality.QualityChecker(raw_data, rate)
2714        # The highest frequency recorded would be near 24 Khz
2715        # as the max sample rate is 48000 in our tests.
2716        # So let's set ignore_high_freq to be 48000.
2717        checker.do_spectral_analysis(ignore_high_freq=48000,
2718                                     check_quality=False,
2719                                     quality_params=None)
2720        spectra = checker._spectrals
2721        primary_freq = [
2722                float(spectra[i][0][0]) if spectra[i] else 0
2723                for i in range(len(spectra))
2724        ]
2725        primary_freq.sort()
2726        return primary_freq
2727
2728    def enable_wbs(self, value):
2729        """Enable or disable wideband speech (wbs) per the value.
2730
2731        @param value: True to enable wbs.
2732
2733        @returns: True if the operation succeeds.
2734        """
2735        return self._cras_test_client.enable_wbs(value)
2736
2737    def set_player_playback_status(self, status):
2738        """Set playback status for the registered media player.
2739
2740        @param status: playback status in string.
2741
2742        """
2743        return self._cras_test_client.set_player_playback_status(status)
2744
2745    def set_player_position(self, position):
2746        """Set media position for the registered media player.
2747
2748        @param position: position in micro seconds.
2749
2750        """
2751        return self._cras_test_client.set_player_position(position)
2752
2753    def set_player_metadata(self, metadata):
2754        """Set metadata for the registered media player.
2755
2756        @param metadata: dictionary of media metadata.
2757
2758        """
2759        return self._cras_test_client.set_player_metadata(metadata)
2760
2761    def set_player_length(self, length):
2762        """Set media length for the registered media player.
2763
2764        Media length is a part of metadata information. However, without
2765        specify its type to int64. dbus-python will guess the variant type to
2766        be int32 by default. Separate it from the metadata function to help
2767        prepare the data differently.
2768
2769        @param length: length in micro seconds.
2770
2771        """
2772        length_variant = dbus.types.Int64(length, variant_level=1)
2773        length_dict = dbus.Dictionary({'length': length_variant},
2774                                      signature='sv')
2775        return self._cras_test_client.set_player_length(length_dict)
2776
2777    def select_input_device(self, device_name):
2778        """Select the audio input device.
2779
2780        @param device_name: the name of the Bluetooth peer device
2781
2782        @returns: True if the operation succeeds.
2783        """
2784        return self._cras_test_client.select_input_device(device_name)
2785
2786    @xmlrpc_server.dbus_safe(None)
2787    def select_output_node(self, node_type):
2788        """Select the audio output node.
2789
2790        @param node_type: the node type of the Bluetooth peer device
2791
2792        @returns: True if the operation succeeds.
2793        """
2794        return cras_utils.set_single_selected_output_node(node_type)
2795
2796    @xmlrpc_server.dbus_safe(None)
2797    def get_selected_output_device_type(self):
2798        """Get the selected audio output node type.
2799
2800        @returns: the node type of the selected output device.
2801        """
2802        # Note: should convert the dbus.String to the regular string.
2803        return str(cras_utils.get_selected_output_device_type())
2804
2805    def get_gatt_attributes_map(self, address):
2806        """Return a JSON formatted string of the GATT attributes of a device,
2807        keyed by UUID
2808        @param address: a string of the MAC address of the device
2809
2810        @return: JSON formated string, stored the nested structure of the
2811        attributes. Each attribute has 'path' and
2812        ['characteristics' | 'descriptors'], which store their object path and
2813        children respectively.
2814
2815        """
2816        attribute_map = dict()
2817
2818        device_object_path = self._get_device_path(address)
2819        objects = self._bluez.GetManagedObjects(
2820                dbus_interface=self.BLUEZ_MANAGER_IFACE, byte_arrays=False)
2821        service_map = self._get_service_map(device_object_path, objects)
2822
2823        servs = dict()
2824        attribute_map['services'] = servs
2825
2826        for uuid, path in service_map.items():
2827
2828            servs[uuid] = dict()
2829            serv = servs[uuid]
2830
2831            serv['path'] = path
2832            serv['characteristics'] = dict()
2833            chrcs = serv['characteristics']
2834
2835            chrcs_map = self._get_characteristic_map(path, objects)
2836            for uuid, path in chrcs_map.items():
2837                chrcs[uuid] = dict()
2838                chrc = chrcs[uuid]
2839
2840                chrc['path'] = path
2841                chrc['descriptors'] = dict()
2842                descs = chrc['descriptors']
2843
2844                descs_map = self._get_descriptor_map(path, objects)
2845
2846                for uuid, path in descs_map.items():
2847                    descs[uuid] = dict()
2848                    desc = descs[uuid]
2849
2850                    desc['path'] = path
2851
2852        return json.dumps(attribute_map)
2853
2854    def _get_gatt_interface(self, uuid, object_path, interface):
2855        """Get dbus interface by uuid
2856        @param uuid: a string of uuid
2857        @param object_path: a string of the object path of the service
2858
2859        @return: a dbus interface
2860        """
2861
2862        return dbus.Interface(
2863                self._system_bus.get_object(self.BLUEZ_SERVICE_NAME,
2864                                            object_path), interface)
2865
2866    def get_gatt_service_property(self, object_path, property_name):
2867        """Get property from a service attribute
2868        @param object_path: a string of the object path of the service
2869        @param property_name: a string of a property, ex: 'Value', 'UUID'
2870
2871        @return: the property if success,
2872                 none otherwise
2873
2874        """
2875        return self.get_gatt_attribute_property(object_path,
2876                                                self.BLUEZ_GATT_SERV_IFACE,
2877                                                property_name)
2878
2879    def get_gatt_characteristic_property(self, object_path, property_name):
2880        """Get property from a characteristic attribute
2881        @param object_path: a string of the object path of the characteristic
2882        @param property_name: a string of a property, ex: 'Value', 'UUID'
2883
2884        @return: the property if success,
2885                 none otherwise
2886
2887        """
2888        return self.get_gatt_attribute_property(object_path,
2889                                                self.BLUEZ_GATT_CHAR_IFACE,
2890                                                property_name)
2891
2892    def get_gatt_descriptor_property(self, object_path, property_name):
2893        """Get property from descriptor attribute
2894        @param object_path: a string of the object path of the descriptor
2895        @param property_name: a string of a property, ex: 'Value', 'UUID'
2896
2897        @return: the property if success,
2898                 none otherwise
2899
2900        """
2901        return self.get_gatt_attribute_property(object_path,
2902                                                self.BLUEZ_GATT_DESC_IFACE,
2903                                                property_name)
2904
2905    @xmlrpc_server.dbus_safe(None)
2906    def get_gatt_attribute_property(self, object_path, interface,
2907                                    property_name):
2908        """Get property from attribute
2909        @param object_path: a string of the bject path
2910        @param property_name: a string of a property, ex: 'Value', 'UUID'
2911
2912        @return: the property if success,
2913                 none otherwise
2914
2915        """
2916        gatt_object = self._system_bus.get_object(self.BLUEZ_SERVICE_NAME,
2917                                                  object_path)
2918        prop = self._get_dbus_object_property(gatt_object, interface,
2919                                              property_name)
2920        logging.info(prop)
2921        if isinstance(prop, dbus.ByteArray):
2922            return _dbus_byte_array_to_b64_string(prop)
2923        if isinstance(prop, dbus.Boolean):
2924            return bool(prop)
2925        if isinstance(prop, dbus.String):
2926            return str(prop)
2927        if isinstance(prop, dbus.ObjectPath):
2928            return str(prop)
2929        if isinstance(prop, dbus.Array):
2930            return list(map(str, prop))
2931        return prop
2932
2933    @xmlrpc_server.dbus_safe(None)
2934    def gatt_characteristic_read_value(self, uuid, object_path):
2935        """Perform method ReadValue on a characteristic attribute
2936        @param uuid: a string of uuid
2937        @param object_path: a string of the object path of the characteristic
2938
2939        @return: base64 string of dbus bytearray
2940        """
2941
2942        dbus_interface = self._get_gatt_interface(uuid, object_path,
2943                                                  self.BLUEZ_GATT_CHAR_IFACE)
2944        value = dbus_interface.ReadValue(dbus.Dictionary({}, signature='sv'))
2945        return _dbus_byte_array_to_b64_string(value)
2946
2947    @xmlrpc_server.dbus_safe(None)
2948    def gatt_descriptor_read_value(self, uuid, object_path):
2949        """Perform method ReadValue on a descriptor attribute
2950        @param uuid: a string of uuid
2951        @param object_path: a string of the object path of the descriptor
2952
2953        @return: base64 string of dbus bytearray
2954        """
2955
2956        dbus_interface = self._get_gatt_interface(uuid, object_path,
2957                                                  self.BLUEZ_GATT_DESC_IFACE)
2958        value = dbus_interface.ReadValue(dbus.Dictionary({}, signature='sv'))
2959        return _dbus_byte_array_to_b64_string(value)
2960
2961    @xmlrpc_server.dbus_safe(False)
2962    def _get_attribute_map(self, object_path, dbus_interface, objects):
2963        """Gets a map of object paths under an object path.
2964
2965        Walks the object tree, and returns a map of UUIDs to object paths for
2966        all resolved gatt object.
2967
2968        @param object_path: The object path of the attribute to retrieve
2969            gatt  UUIDs and paths from.
2970        @param objects: The managed objects.
2971
2972        @returns: A dictionary of object paths, keyed by UUID.
2973
2974        """
2975        attr_map = {}
2976
2977        if object_path:
2978            for path, ifaces in six.iteritems(objects):
2979                if (dbus_interface in ifaces and path.startswith(object_path)):
2980                    uuid = ifaces[dbus_interface]['UUID'].lower()
2981                    attr_map[uuid] = path
2982
2983        else:
2984            logging.warning('object_path %s is not valid', object_path)
2985
2986        return attr_map
2987
2988    def _get_service_map(self, device_path, objects):
2989        """Gets a map of service paths for a device.
2990
2991        @param device_path: the object path of the device.
2992        @param objects: The managed objects.
2993        """
2994        return self._get_attribute_map(device_path, self.BLUEZ_GATT_SERV_IFACE,
2995                                       objects)
2996
2997    def _get_characteristic_map(self, serv_path, objects):
2998        """Gets a map of characteristic paths for a service.
2999
3000        @param serv_path: the object path of the service.
3001        @param objects: The managed objects.
3002        """
3003        return self._get_attribute_map(serv_path, self.BLUEZ_GATT_CHAR_IFACE,
3004                                       objects)
3005
3006    def _get_descriptor_map(self, chrc_path, objects):
3007        """Gets a map of descriptor paths for a characteristic.
3008
3009        @param chrc_path: the object path of the characteristic.
3010        @param objects: The managed objects.
3011        """
3012        return self._get_attribute_map(chrc_path, self.BLUEZ_GATT_DESC_IFACE,
3013                                       objects)
3014
3015    @xmlrpc_server.dbus_safe(None)
3016    def _get_dbus_object_property(self, dbus_object, dbus_interface,
3017                                  dbus_property):
3018        """Get the property in an object.
3019
3020        @param dbus_object: a dbus object
3021        @param dbus_property: a dbus property of the dbus object, as a string
3022
3023        @return: dbus type object if it success, e.g. dbus.Boolean, dbus.String,
3024                 none otherwise
3025
3026        """
3027        return dbus_object.Get(dbus_interface,
3028                               dbus_property,
3029                               dbus_interface=dbus.PROPERTIES_IFACE)
3030
3031    @xmlrpc_server.dbus_safe(False)
3032    def get_characteristic_map(self, address):
3033        """Gets a map of characteristic paths for a device.
3034
3035        Walks the object tree, and returns a map of uuids to object paths for
3036        all resolved gatt characteristics.
3037
3038        @param address: The MAC address of the device to retrieve
3039            gatt characteristic uuids and paths from.
3040
3041        @returns: A dictionary of characteristic paths, keyed by uuid.
3042
3043        """
3044        device_path = self._get_device_path(address)
3045        char_map = {}
3046
3047        if device_path:
3048            objects = self._bluez.GetManagedObjects(
3049                    dbus_interface=self.BLUEZ_MANAGER_IFACE, byte_arrays=False)
3050
3051            for path, ifaces in six.iteritems(objects):
3052                if (self.BLUEZ_GATT_CHAR_IFACE in ifaces
3053                            and path.startswith(device_path)):
3054                    uuid = ifaces[self.BLUEZ_GATT_CHAR_IFACE]['UUID'].lower()
3055                    char_map[uuid] = path
3056        else:
3057            logging.warning('Device %s not in object tree.', address)
3058
3059        return char_map
3060
3061    @xmlrpc_server.dbus_safe(None)
3062    def _get_char_object(self, uuid, address):
3063        """Gets a characteristic object.
3064
3065        Gets a characteristic object for a given UUID and address.
3066
3067        @param uuid: The UUID of the characteristic, as a string.
3068        @param address: The MAC address of the remote device.
3069
3070        @returns: A dbus interface for the characteristic if the uuid/address
3071                      is in the object tree.
3072                  None if the address/uuid is not found in the object tree.
3073
3074        """
3075        path = self.get_characteristic_map(address).get(uuid)
3076        if not path:
3077            logging.error("path not found: %s %s", uuid, address)
3078            return None
3079        return dbus.Interface(
3080                self._system_bus.get_object(self.BLUEZ_SERVICE_NAME, path),
3081                self.BLUEZ_GATT_CHAR_IFACE)
3082
3083    @xmlrpc_server.dbus_safe(None)
3084    def read_characteristic(self, uuid, address):
3085        """Reads the value of a gatt characteristic.
3086
3087        Reads the current value of a gatt characteristic. Base64 endcoding is
3088        used for compatibility with the XML RPC interface.
3089
3090        @param uuid: The uuid of the characteristic to read, as a string.
3091        @param address: The MAC address of the remote device.
3092
3093        @returns: A b64 encoded version of a byte array containing the value
3094                      if the uuid/address is in the object tree.
3095                  None if the uuid/address was not found in the object tree, or
3096                      if a DBus exception was raised by the read operation.
3097
3098        """
3099        char_obj = self._get_char_object(uuid, address)
3100        if char_obj is None:
3101            return None
3102        value = char_obj.ReadValue(dbus.Dictionary({}, signature='sv'))
3103        return _dbus_byte_array_to_b64_string(value)
3104
3105    @xmlrpc_server.dbus_safe(None)
3106    def write_characteristic(self, uuid, address, value):
3107        """Performs a write operation on a gatt characteristic.
3108
3109        Writes to a GATT characteristic on a remote device. Base64 endcoding is
3110        used for compatibility with the XML RPC interface.
3111
3112        @param uuid: The uuid of the characteristic to write to, as a string.
3113        @param address: The MAC address of the remote device, as a string.
3114        @param value: A byte array containing the data to write.
3115
3116        @returns: True if the write operation does not raise an exception.
3117                  None if the uuid/address was not found in the object tree, or
3118                      if a DBus exception was raised by the write operation.
3119
3120        """
3121        char_obj = self._get_char_object(uuid, address)
3122        if char_obj is None:
3123            return None
3124        dbus_value = _b64_string_to_dbus_byte_array(value)
3125        char_obj.WriteValue(dbus_value, dbus.Dictionary({}, signature='sv'))
3126        return True
3127
3128    @xmlrpc_server.dbus_safe(None)
3129    def exchange_messages(self, tx_object_path, rx_object_path, value):
3130        """Performs a write operation on a gatt characteristic and wait for
3131        the response on another characteristic.
3132
3133        @param tx_object_path: the object path of the characteristic to write.
3134        @param rx_object_path: the object path of the characteristic ti read.
3135        @param value: A byte array containing the data to write.
3136
3137        @returns: The value of the characteristic to read from.
3138                  None if the uuid/address was not found in the object tree, or
3139                      if a DBus exception was raised by the write operation.
3140
3141        """
3142        tx_obj = self._get_gatt_characteristic_object(tx_object_path)
3143
3144        if tx_obj is None:
3145            return None
3146
3147        self._chrc_property = None
3148
3149        self._signal_watch = self._system_bus.add_signal_receiver(
3150                self._property_changed,
3151                signal_name='PropertiesChanged',
3152                path=rx_object_path)
3153
3154        self._timeout_id = gobject.timeout_add(
3155                self.PROPERTY_UPDATE_TIMEOUT_MILLI_SECS,
3156                self._property_wait_timeout)
3157
3158        write_value = _b64_string_to_dbus_byte_array(value)
3159        tx_obj.WriteValue(write_value, dbus.Dictionary({}, signature='sv'))
3160
3161        self._dbus_mainloop.run()
3162
3163        return _dbus_byte_array_to_b64_string(self._chrc_property)
3164
3165    def _property_changed(self, *args, **kwargs):
3166        """Handler for properties changed signal."""
3167        gobject.source_remove(self._timeout_id)
3168        self._signal_watch.remove()
3169        changed_prop = args
3170
3171        logging.info(changed_prop)
3172        prop_dict = changed_prop[1]
3173        self._chrc_property = prop_dict['Value']
3174        if self._dbus_mainloop.is_running():
3175            self._dbus_mainloop.quit()
3176
3177    def _property_wait_timeout(self):
3178        """Timeout handler when waiting for properties update signal."""
3179        self._signal_watch.remove()
3180        if self._dbus_mainloop.is_running():
3181            logging.warn("quit main loop due to timeout")
3182            self._dbus_mainloop.quit()
3183        # Return false so that this method will not be called again.
3184        return False
3185
3186    @xmlrpc_server.dbus_safe(False)
3187    def _get_gatt_characteristic_object(self, object_path):
3188        return dbus.Interface(
3189                self._system_bus.get_object(self.BLUEZ_SERVICE_NAME,
3190                                            object_path),
3191                self.BLUEZ_GATT_CHAR_IFACE)
3192
3193    @xmlrpc_server.dbus_safe(False)
3194    def start_notify(self, object_path, cccd_value):
3195        """Starts the notification session on the gatt characteristic.
3196
3197        @param object_path: the object path of the characteristic.
3198        @param cccd_value: Possible CCCD values include
3199               0x00 - inferred from the remote characteristic's properties
3200               0x01 - notification
3201               0x02 - indication
3202
3203        @returns: True if the operation succeeds.
3204                  False if the characteristic is not found, or
3205                      if a DBus exception was raised by the operation.
3206
3207        """
3208        char_obj = self._get_gatt_characteristic_object(object_path)
3209        if char_obj is None:
3210            logging.error("characteristic not found: %s %s", object_path)
3211            return False
3212
3213        try:
3214            char_obj.StartNotify(dbus.Byte(cccd_value))
3215            return True
3216        except Exception as e:
3217            logging.error('start_notify: %s', e)
3218        except:
3219            logging.error('start_notify: unexpected error')
3220        return False
3221
3222    @xmlrpc_server.dbus_safe(False)
3223    def stop_notify(self, object_path):
3224        """Stops the notification session on the gatt characteristic.
3225
3226        @param object_path: the object path of the characteristic.
3227
3228        @returns: True if the operation succeeds.
3229                  False if the characteristic is not found, or
3230                      if a DBus exception was raised by the operation.
3231
3232        """
3233        char_obj = self._get_gatt_characteristic_object(object_path)
3234        if char_obj is None:
3235            logging.error("characteristic not found: %s %s", object_path)
3236            return False
3237
3238        try:
3239            char_obj.StopNotify()
3240            return True
3241        except Exception as e:
3242            logging.error('stop_notify: %s', e)
3243        except:
3244            logging.error('stop_notify: unexpected error')
3245        return False
3246
3247    @xmlrpc_server.dbus_safe(False)
3248    def is_notifying(self, object_path):
3249        """Is the GATT characteristic in a notifying session?
3250
3251        @param object_path: the object path of the characteristic.
3252
3253        @return True if it is in a notification session. False otherwise.
3254
3255        """
3256
3257        return self.get_gatt_characteristic_property(object_path, 'Notifying')
3258
3259    @xmlrpc_server.dbus_safe(False)
3260    def is_characteristic_path_resolved(self, uuid, address):
3261        """Checks whether a characteristic is in the object tree.
3262
3263        Checks whether a characteristic is curently found in the object tree.
3264
3265        @param uuid: The uuid of the characteristic to search for.
3266        @param address: The MAC address of the device on which to search for
3267            the characteristic.
3268
3269        @returns: True if the characteristic is found.
3270                  False if the characteristic path is not found.
3271
3272        """
3273        return bool(self.get_characteristic_map(address).get(uuid))
3274
3275    @xmlrpc_server.dbus_safe(False)
3276    def get_connection_info(self, address):
3277        """Get device connection info.
3278
3279        @param address: The MAC address of the device.
3280
3281        @returns: On success, a JSON-encoded tuple of:
3282                      ( RSSI, transmit_power, max_transmit_power )
3283                  None otherwise.
3284
3285        """
3286        plugin_device = self._get_plugin_device_interface(address)
3287        if plugin_device is None:
3288            return None
3289
3290        try:
3291            connection_info = plugin_device.GetConnInfo()
3292            return json.dumps(connection_info)
3293        except Exception as e:
3294            logging.error('get_connection_info: %s', e)
3295        except:
3296            logging.error('get_connection_info: unexpected error')
3297        return None
3298
3299    @xmlrpc_server.dbus_safe(False)
3300    def set_le_connection_parameters(self, address, parameters):
3301        """Set the LE connection parameters.
3302
3303        @param address: The MAC address of the device.
3304        @param parameters: The LE connection parameters to set.
3305
3306        @return: True on success. False otherwise.
3307
3308        """
3309        plugin_device = self._get_plugin_device_interface(address)
3310        if plugin_device is None:
3311            return False
3312
3313        return not self.dbus_async_method(
3314                plugin_device.SetLEConnectionParameters,
3315                # reply handler
3316                lambda: logging.info('set_le_connection_parameters: succeeded.'
3317                                     ),
3318                # error handler
3319                lambda error: logging.
3320                error('set_le_connection_parameters: failed: %s', str(error)),
3321                # other arguments
3322                parameters)
3323
3324    @xmlrpc_server.dbus_safe(False)
3325    def _get_plugin_device_interface(self, address):
3326        """Get the BlueZ Chromium device plugin interface.
3327
3328        This interface can be used to issue dbus requests such as
3329        GetConnInfo and SetLEConnectionParameters.
3330
3331        @param address: The MAC address of the device.
3332
3333        @return: On success, the BlueZ Chromium device plugin interface
3334                 None otherwise.
3335
3336        """
3337        path = self._get_device_path(address)
3338        if path is None:
3339            return None
3340
3341        return dbus.Interface(
3342                self._system_bus.get_object(self.BLUEZ_SERVICE_NAME, path),
3343                self.BLUEZ_PLUGIN_DEVICE_IFACE)
3344
3345    def _powerd_last_resume_details(self, before=5, after=0):
3346        """ Look at powerd logs for last suspend/resume attempt.
3347
3348        Note that logs are in reverse order (chronologically). Keep that in mind
3349        for the 'before' and 'after' parameters.
3350
3351        @param before: Number of context lines before search item to show.
3352        @param after: Number of context lines after search item to show.
3353
3354        @return Most recent lines containing suspend resume details or ''.
3355        """
3356        event_file = '/var/log/power_manager/powerd.LATEST'
3357
3358        # Each powerd_suspend wakeup has a log "powerd_suspend returned 0",
3359        # with the return code of the suspend. We search for the last
3360        # occurrence in the log, and then find the collocated event_count log,
3361        # indicating the wakeup cause. -B option for grep will actually grab the
3362        # *next* 5 logs in time, since we are piping the powerd file backwards
3363        # with tac command
3364        resume_indicator = 'powerd_suspend returned'
3365        cmd = 'tac {} | grep -A {} -B {} -m1 "{}"'.format(
3366                event_file, after, before, resume_indicator)
3367
3368        try:
3369            return utils.run(cmd).stdout
3370        except error.CmdError:
3371            logging.error('Could not locate recent suspend')
3372
3373        return ''
3374
3375    def bt_caused_last_resume(self):
3376        """Checks if last resume from suspend was caused by bluetooth
3377
3378        @return: True if BT wake path was cause of resume, False otherwise
3379        """
3380
3381        # When the resume cause is printed to powerd log, it omits the
3382        # /power/wakeup portion of wake path
3383        bt_wake_path = self._get_wake_enabled_path()
3384
3385        # If bluetooth does not have a valid wake path, it could not have caused
3386        # the resume
3387        if not bt_wake_path:
3388            return False
3389
3390        bt_wake_path = bt_wake_path.replace('/power/wakeup', '')
3391
3392        last_resume_details = self._powerd_last_resume_details()
3393
3394        # If BT caused wake, there will be a line describing the bt wake
3395        # path's event_count before and after the resume
3396        for line in last_resume_details.split('\n'):
3397            if 'event_count' in line:
3398                logging.info('Checking wake event: {}'.format(line))
3399                if bt_wake_path in line:
3400                    return True
3401
3402        return False
3403
3404    def find_last_suspend_via_powerd_logs(self):
3405        """ Finds the last suspend attempt via powerd logs.
3406
3407        Finds the last suspend attempt using powerd logs by searching backwards
3408        through the logs to find the latest entries with 'powerd_suspend'. If we
3409        can't find a suspend attempt, we return None.
3410
3411        @return: Tuple (suspend start time, suspend end time, suspend result) or
3412                None if we can't find a suspend attempt
3413        """
3414        # Logs look like this:
3415        # [1102/202036.973853:INFO:daemon.cc(704)] powerd_suspend returned 0
3416        # ... stuff in between ...
3417        # [1102/202025.785372:INFO:suspender.cc(574)] Starting suspend
3418
3419        # Date format for strptime and strftime
3420        date_format = '%m%d/%H%M%S.%f'
3421        date_group_re = '(?P<date>[0-9]+/[0-9]+[.][0-9]+)'
3422
3423        finish_suspend_re = re.compile(
3424                '^\\[{date_regex}'
3425                '.*daemon.*powerd_suspend returned '
3426                '(?P<exitcode>[0-9]+)'.format(date_regex=date_group_re))
3427        start_suspend_re = re.compile(
3428                '^\\[{date_regex}.*suspender.*'
3429                'Starting suspend'.format(date_regex=date_group_re))
3430
3431        now = datetime.now()
3432        last_resume_details = self._powerd_last_resume_details(before=0,
3433                                                               after=8)
3434        if last_resume_details:
3435            start_time, end_time, ret = None, None, None
3436            try:
3437                for line in last_resume_details.split('\n'):
3438                    logging.debug('Last suspend search: %s', line)
3439                    m = finish_suspend_re.match(line)
3440                    if m:
3441                        logging.debug('Found suspend end: date(%s) ret(%s)',
3442                                      m.group('date'), m.group('exitcode'))
3443                        end_time = datetime.strptime(
3444                                m.group('date'),
3445                                date_format).replace(year=now.year)
3446                        ret = int(m.group('exitcode'))
3447
3448                    m = start_suspend_re.match(line)
3449                    if m:
3450                        logging.debug('Found suspend start: date(%s)',
3451                                      m.group('date'))
3452                        start_time = datetime.strptime(
3453                                m.group('date'),
3454                                date_format).replace(year=now.year)
3455                        break
3456
3457                if all([x is not None for x in [start_time, end_time, ret]]):
3458                    # Return dates in string format due to inconsistency between
3459                    # python2/3 usage on host and dut
3460                    return (start_time.strftime(self.OUT_DATE_FORMAT),
3461                            end_time.strftime(self.OUT_DATE_FORMAT), ret)
3462                else:
3463                    logging.error(
3464                            'Failed to parse details from last suspend. %s %s %s',
3465                            str(start_time), str(end_time), str(ret))
3466            except Exception as e:
3467                logging.error('Failed to parse last suspend: %s', str(e))
3468        else:
3469            logging.error('No powerd_suspend attempt found')
3470
3471        return None
3472
3473    def do_suspend(self, seconds, expect_bt_wake):
3474        """Suspend DUT using the power manager.
3475
3476        @param seconds: The number of seconds to suspend the device.
3477        @param expect_bt_wake: Whether we expect bluetooth to wake us from
3478            suspend. If true, we expect this resume will occur early
3479
3480        @throws: SuspendFailure on resume with unexpected timing or wake source.
3481            The raised exception will be handled as a non-zero retcode over the
3482            RPC, signalling for the test to fail.
3483        """
3484        early_wake = False
3485        try:
3486            sys_power.do_suspend(seconds)
3487
3488        except sys_power.SpuriousWakeupError:
3489            logging.info('Early resume detected...')
3490            early_wake = True
3491
3492        # Handle error conditions based on test expectations, whether resume
3493        # was early, and cause of the resume
3494        bt_caused_wake = self.bt_caused_last_resume()
3495        logging.info('Cause for resume: {}'.format(
3496                'BT' if bt_caused_wake else 'Not BT'))
3497
3498        if not expect_bt_wake and bt_caused_wake:
3499            raise sys_power.SuspendFailure('BT woke us unexpectedly')
3500
3501        # TODO(b/160803597) - Uncomment when BT wake reason is correctly
3502        # captured in powerd log.
3503        #
3504        # if expect_bt_wake and not bt_caused_wake:
3505        #   raise sys_power.SuspendFailure('BT should have woken us')
3506        #
3507        # if bt_caused_wake and not early_wake:
3508        #   raise sys_power.SuspendFailure('BT wake did not come early')
3509
3510        return True
3511
3512    def get_wlan_vid_pid(self):
3513        """ Return vendor id and product id of the wlan chip on BT/WiFi module
3514
3515        @returns: (vid,pid) on success; (None,None) on failure
3516        """
3517        vid = None
3518        pid = None
3519        path_template = '/sys/class/net/%s/device/'
3520        for dev_name in ['wlan0', 'mlan0']:
3521            if os.path.exists(path_template % dev_name):
3522                path_v = path_template % dev_name + 'vendor'
3523                path_d = path_template % dev_name + 'device'
3524                logging.debug('Paths are %s %s', path_v, path_d)
3525                try:
3526                    vid = open(path_v).read().strip('\n')
3527                    pid = open(path_d).read().strip('\n')
3528                    break
3529                except Exception as e:
3530                    logging.error('Exception %s while reading vid/pid', str(e))
3531        logging.debug('returning vid:%s pid:%s', vid, pid)
3532        return (vid, pid)
3533
3534    def get_bt_module_name(self):
3535        """ Return bluetooth module name for non-USB devices
3536
3537        @returns '' on failure. On success return chipset name, if found in
3538                 dict.Otherwise it returns the raw string read.
3539        """
3540        # map the string read from device to chipset name
3541        chipset_string_dict = {'qcom,wcn3991-bt\x00': 'WCN3991'}
3542
3543        hci_device = '/sys/class/bluetooth/hci0'
3544        real_path = os.path.realpath(hci_device)
3545
3546        logging.debug('real path is %s', real_path)
3547        if 'usb' in real_path:
3548            return ''
3549
3550        device_path = os.path.join(real_path, 'device', 'of_node',
3551                                   'compatible')
3552        try:
3553            chipset_string = open(device_path).read()
3554            logging.debug('read string %s from %s', chipset_string,
3555                          device_path)
3556        except Exception as e:
3557            logging.error('Exception %s while reading from file', str(e),
3558                          device_path)
3559            return ''
3560
3561        if chipset_string in chipset_string_dict:
3562            return chipset_string_dict[chipset_string]
3563        else:
3564            logging.debug("Chipset not known. Returning %s", chipset_string)
3565            return chipset_string
3566
3567    def get_device_time(self):
3568        """ Get the current device time. """
3569        return datetime.now().strftime(self.OUT_DATE_FORMAT)
3570
3571    def cleanup(self):
3572        """Cleanup before exiting the client xmlrpc process."""
3573
3574        self.advmon_appmgr.destroy()
3575