1# Lint as: python2, python3
2# Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6from __future__ import absolute_import
7from __future__ import division
8from __future__ import print_function
9import atexit
10import six.moves.http_client
11import logging
12import os
13import socket
14import time
15from six.moves import range
16import six.moves.xmlrpc_client
17from contextlib import contextmanager
18
19try:
20    from PIL import Image
21except ImportError:
22    Image = None
23
24from autotest_lib.client.bin import utils
25from autotest_lib.client.common_lib import error
26from autotest_lib.client.cros.chameleon import audio_board
27from autotest_lib.client.cros.chameleon import chameleon_bluetooth_audio
28from autotest_lib.client.cros.chameleon import edid as edid_lib
29from autotest_lib.client.cros.chameleon import usb_controller
30
31
32CHAMELEON_PORT = 9992
33CHAMELEOND_LOG_REMOTE_PATH = '/var/log/chameleond'
34DAEMON_LOG_REMOTE_PATH = '/var/log/daemon.log'
35BTMON_LOG_REMOTE_PATH = '/var/log/btsnoop.log'
36CHAMELEON_READY_TEST = 'GetSupportedPorts'
37
38
39class ChameleonConnectionError(error.TestError):
40    """Indicates that connecting to Chameleon failed.
41
42    It is fatal to the test unless caught.
43    """
44    pass
45
46
47class _Method(object):
48    """Class to save the name of the RPC method instead of the real object.
49
50    It keeps the name of the RPC method locally first such that the RPC method
51    can be evaluated to a real object while it is called. Its purpose is to
52    refer to the latest RPC proxy as the original previous-saved RPC proxy may
53    be lost due to reboot.
54
55    The call_server is the method which does refer to the latest RPC proxy.
56
57    This class and the re-connection mechanism in ChameleonConnection is
58    copied from third_party/autotest/files/server/cros/faft/rpc_proxy.py
59
60    """
61    def __init__(self, call_server, name):
62        """Constructs a _Method.
63
64        @param call_server: the call_server method
65        @param name: the method name or instance name provided by the
66                     remote server
67
68        """
69        self.__call_server = call_server
70        self._name = name
71
72
73    def __getattr__(self, name):
74        """Support a nested method.
75
76        For example, proxy.system.listMethods() would need to use this method
77        to get system and then to get listMethods.
78
79        @param name: the method name or instance name provided by the
80                     remote server
81
82        @return: a callable _Method object.
83
84        """
85        return _Method(self.__call_server, "%s.%s" % (self._name, name))
86
87
88    def __call__(self, *args, **dargs):
89        """The call method of the object.
90
91        @param args: arguments for the remote method.
92        @param kwargs: keyword arguments for the remote method.
93
94        @return: the result returned by the remote method.
95
96        """
97        return self.__call_server(self._name, *args, **dargs)
98
99
100class ChameleonConnection(object):
101    """ChameleonConnection abstracts the network connection to the board.
102
103    When a chameleon board is rebooted, a xmlrpc call would incur a
104    socket error. To fix the error, a client has to reconnect to the server.
105    ChameleonConnection is a wrapper of chameleond proxy created by
106    xmlrpclib.ServerProxy(). ChameleonConnection has the capability to
107    automatically reconnect to the server when such socket error occurs.
108    The nice feature is that the auto re-connection is performed inside this
109    wrapper and is transparent to the caller.
110
111    Note:
112    1. When running chameleon autotests in lab machines, it is
113       ChameleonConnection._create_server_proxy() that is invoked.
114    2. When running chameleon autotests in local chroot, it is
115       rpc_server_tracker.xmlrpc_connect() in server/hosts/chameleon_host.py
116       that is invoked.
117
118    ChameleonBoard and ChameleonPort use it for accessing Chameleon RPC.
119
120    """
121
122    def __init__(self, hostname, port=CHAMELEON_PORT, proxy_generator=None,
123                 ready_test_name=CHAMELEON_READY_TEST):
124        """Constructs a ChameleonConnection.
125
126        @param hostname: Hostname the chameleond process is running.
127        @param port: Port number the chameleond process is listening on.
128        @param proxy_generator: a function to generate server proxy.
129        @param ready_test_name: run this method on the remote server ot test
130                if the server is connected correctly.
131
132        @raise ChameleonConnectionError if connection failed.
133        """
134        self._hostname = hostname
135        self._port = port
136
137        # Note: it is difficult to put the lambda function as the default
138        # value of the proxy_generator argument. In that case, the binding
139        # of arguments (hostname and port) would be delayed until run time
140        # which requires to pass an instance as an argument to labmda.
141        # That becomes cumbersome since server/hosts/chameleon_host.py
142        # would also pass a lambda without argument to instantiate this object.
143        # Use the labmda function as follows would bind the needed arguments
144        # immediately which is much simpler.
145        self._proxy_generator = proxy_generator or self._create_server_proxy
146
147        self._ready_test_name = ready_test_name
148        self._chameleond_proxy = None
149
150
151    def _create_server_proxy(self):
152        """Creates the chameleond server proxy.
153
154        @param hostname: Hostname the chameleond process is running.
155        @param port: Port number the chameleond process is listening on.
156
157        @return ServerProxy object to chameleond.
158
159        @raise ChameleonConnectionError if connection failed.
160
161        """
162        remote = 'http://%s:%s' % (self._hostname, self._port)
163        chameleond_proxy = six.moves.xmlrpc_client.ServerProxy(remote, allow_none=True)
164        logging.info('ChameleonConnection._create_server_proxy() called')
165        # Call a RPC to test.
166        try:
167            getattr(chameleond_proxy, self._ready_test_name)()
168        except (socket.error,
169                six.moves.xmlrpc_client.ProtocolError,
170                six.moves.http_client.BadStatusLine) as e:
171            raise ChameleonConnectionError(e)
172        return chameleond_proxy
173
174
175    def _reconnect(self):
176        """Reconnect to chameleond."""
177        self._chameleond_proxy = self._proxy_generator()
178
179
180    def __call_server(self, name, *args, **kwargs):
181        """Bind the name to the chameleond proxy and execute the method.
182
183        @param name: the method name or instance name provided by the
184                     remote server.
185        @param args: arguments for the remote method.
186        @param kwargs: keyword arguments for the remote method.
187
188        @return: the result returned by the remote method.
189
190        @raise ChameleonConnectionError if the call failed after a reconnection.
191
192        """
193        try:
194            return getattr(self._chameleond_proxy, name)(*args, **kwargs)
195        except (AttributeError, socket.error):
196            # Reconnect and invoke the method again.
197            logging.info('Reconnecting chameleond proxy: %s', name)
198            self._reconnect()
199            try:
200                return getattr(self._chameleond_proxy, name)(*args, **kwargs)
201            except (socket.error) as e:
202                raise ChameleonConnectionError(
203                        ("The RPC call %s still failed with %s"
204                         " after a reconnection.") % (name, e))
205        return None
206
207    def __getattr__(self, name):
208        """Get the callable _Method object.
209
210        @param name: the method name or instance name provided by the
211                     remote server
212
213        @return: a callable _Method object.
214
215        """
216        return _Method(self.__call_server, name)
217
218
219class ChameleonBoard(object):
220    """ChameleonBoard is an abstraction of a Chameleon board.
221
222    A Chameleond RPC proxy is passed to the construction such that it can
223    use this proxy to control the Chameleon board.
224
225    User can use host to access utilities that are not provided by
226    Chameleond XMLRPC server, e.g. send_file and get_file, which are provided by
227    ssh_host.SSHHost, which is the base class of ChameleonHost.
228
229    """
230
231    def __init__(self, chameleon_connection, chameleon_host=None):
232        """Construct a ChameleonBoard.
233
234        @param chameleon_connection: ChameleonConnection object.
235        @param chameleon_host: ChameleonHost object. None if this ChameleonBoard
236                               is not created by a ChameleonHost.
237        """
238        self.host = chameleon_host
239        self._output_log_file = None
240        self._chameleond_proxy = chameleon_connection
241        self._usb_ctrl = usb_controller.USBController(chameleon_connection)
242        if self._chameleond_proxy.HasAudioBoard():
243            self._audio_board = audio_board.AudioBoard(chameleon_connection)
244        else:
245            self._audio_board = None
246            logging.info('There is no audio board on this Chameleon.')
247        self._bluetooth_ref_controller = (
248            chameleon_bluetooth_audio.
249            BluetoothRefController(chameleon_connection)
250            )
251
252
253    def reset(self):
254        """Resets Chameleon board."""
255        self._chameleond_proxy.Reset()
256
257
258    def setup_and_reset(self, output_dir=None):
259        """Setup and reset Chameleon board.
260
261        @param output_dir: Setup the output directory.
262                           None for just reset the board.
263        """
264        if output_dir and self.host is not None:
265            logging.info('setup_and_reset: dir %s, chameleon host %s',
266                         output_dir, self.host.hostname)
267            log_dir = os.path.join(output_dir, 'chameleond', self.host.hostname)
268            # Only clear the chameleon board log and register get log callback
269            # when we first create the log_dir.
270            if not os.path.exists(log_dir):
271                # remove old log.
272                self.host.run('>%s' % CHAMELEOND_LOG_REMOTE_PATH)
273                os.makedirs(log_dir)
274                self._output_log_file = os.path.join(log_dir, 'log')
275                atexit.register(self._get_log)
276        self.reset()
277
278
279    def register_raspPi_log(self, output_dir):
280        """Register log for raspberry Pi
281
282        This method log bluetooth related files on Raspberry Pi.
283        If the host is not running on Raspberry Pi, some files may be ignored.
284        """
285        log_dir = os.path.join(output_dir, 'chameleond', self.host.hostname)
286
287        if not os.path.exists(log_dir):
288            os.makedirs(log_dir)
289
290        def log_new_gen(source_path):
291            """Generate function to save logs logging during the test
292
293            @param source_path: The log file path that want to be saved
294
295            @return: Function to save the logs if file in source_path exists,
296                     None otherwise.
297            """
298
299            # Check if the file exists
300            file_exist = self.host.run('[ -f %s ] || echo "not found"' %
301                                        source_path).stdout.strip()
302            if file_exist == 'not found':
303                return None
304
305            byte_to_skip = self.host.run('stat --printf="%%s" %s' %
306                                         source_path).stdout.strip()
307            file_name = os.path.basename(source_path)
308            target_path = os.path.join(log_dir, file_name)
309
310            def log_new():
311                """Save the newly added logs"""
312                tmp_file_path = source_path+'.new'
313
314                # Store a temporary file with newly added content
315                # Set the start point as byte_to_skip + 1
316                self.host.run('tail -c +%s %s > %s' % (int(byte_to_skip)+1,
317                                                       source_path,
318                                                       tmp_file_path))
319                self.host.get_file(tmp_file_path, target_path)
320                self.host.run('rm %s' % tmp_file_path)
321            return log_new
322
323        for source_path in [CHAMELEOND_LOG_REMOTE_PATH, DAEMON_LOG_REMOTE_PATH]:
324            log_new_func = log_new_gen(source_path)
325            if log_new_func:
326                atexit.register(log_new_func)
327
328
329        def btmon_atexit_gen(btmon_pid):
330            """Generate a function to kill the btmon process and save the log
331
332            @param btmon_pid: PID of the btmon process
333            """
334
335            def btmon_atexit():
336                """Kill the btmon with specified PID and save the log"""
337
338                file_name = os.path.basename(BTMON_LOG_REMOTE_PATH)
339                target_path = os.path.join(log_dir, file_name)
340
341                self.host.run('kill %d' % btmon_pid)
342                self.host.get_file(BTMON_LOG_REMOTE_PATH, target_path)
343            return btmon_atexit
344
345
346        # Kill all btmon process before creating a new one
347        self.host.run('pkill btmon || true')
348
349        # Get available btmon options in the chameleon host
350        btmon_options = ''
351        btmon_help = self.host.run('btmon --help').stdout
352
353        for option in 'SA':
354            if '-%s' % option in btmon_help:
355                btmon_options += option
356
357        # Store btmon log
358        btmon_pid = int(self.host.run_background('btmon -%sw %s'
359                                                % (btmon_options,
360                                                BTMON_LOG_REMOTE_PATH)))
361        if btmon_pid > 0:
362            atexit.register(btmon_atexit_gen(btmon_pid))
363
364
365    def reboot(self):
366        """Reboots Chameleon board."""
367        self._chameleond_proxy.Reboot()
368
369
370    def get_bt_commit_hash(self):
371        """ Read the current git commit hash of chameleond."""
372        return self._chameleond_proxy.get_bt_commit_hash()
373
374
375    def _get_log(self):
376        """Get log from chameleon. It will be registered by atexit.
377
378        It's a private method. We will setup output_dir before using this
379        method.
380        """
381        self.host.get_file(CHAMELEOND_LOG_REMOTE_PATH, self._output_log_file)
382
383    def log_message(self, msg):
384        """Log a message in chameleond log and system log."""
385        self._chameleond_proxy.log_message(msg)
386
387    def get_all_ports(self):
388        """Gets all the ports on Chameleon board which are connected.
389
390        @return: A list of ChameleonPort objects.
391        """
392        ports = self._chameleond_proxy.ProbePorts()
393        return [ChameleonPort(self._chameleond_proxy, port) for port in ports]
394
395
396    def get_all_inputs(self):
397        """Gets all the input ports on Chameleon board which are connected.
398
399        @return: A list of ChameleonPort objects.
400        """
401        ports = self._chameleond_proxy.ProbeInputs()
402        return [ChameleonPort(self._chameleond_proxy, port) for port in ports]
403
404
405    def get_all_outputs(self):
406        """Gets all the output ports on Chameleon board which are connected.
407
408        @return: A list of ChameleonPort objects.
409        """
410        ports = self._chameleond_proxy.ProbeOutputs()
411        return [ChameleonPort(self._chameleond_proxy, port) for port in ports]
412
413
414    def get_label(self):
415        """Gets the label which indicates the display connection.
416
417        @return: A string of the label, like 'hdmi', 'dp_hdmi', etc.
418        """
419        connectors = []
420        for port in self._chameleond_proxy.ProbeInputs():
421            if self._chameleond_proxy.HasVideoSupport(port):
422                connector = self._chameleond_proxy.GetConnectorType(port).lower()
423                connectors.append(connector)
424        # Eliminate duplicated ports. It simplifies the labels of dual-port
425        # devices, i.e. dp_dp categorized into dp.
426        return '_'.join(sorted(set(connectors)))
427
428
429    def get_audio_board(self):
430        """Gets the audio board on Chameleon.
431
432        @return: An AudioBoard object.
433        """
434        return self._audio_board
435
436
437    def get_usb_controller(self):
438        """Gets the USB controller on Chameleon.
439
440        @return: A USBController object.
441        """
442        return self._usb_ctrl
443
444
445    def get_bluetooth_base(self):
446        """Gets the Bluetooth base object on Chameleon.
447
448        This is a base object that does not emulate any Bluetooth device.
449
450        @return: A BluetoothBaseFlow object.
451        """
452        return self._chameleond_proxy.bluetooth_base
453
454
455    def get_bluetooth_tester(self):
456        """Gets the Bluetooth tester object on Chameleon.
457
458        @return: A BluetoothTester object.
459        """
460        return self._chameleond_proxy.bluetooth_tester
461
462
463    def get_bluetooth_audio(self):
464        """Gets the Bluetooth audio object on Chameleon.
465
466        @return: A RaspiBluetoothAudioFlow object.
467        """
468        return self._chameleond_proxy.bluetooth_audio
469
470
471    def get_bluetooth_hid_mouse(self):
472        """Gets the emulated Bluetooth (BR/EDR) HID mouse on Chameleon.
473
474        @return: A BluetoothHIDMouseFlow object.
475        """
476        return self._chameleond_proxy.bluetooth_mouse
477
478
479    def get_bluetooth_hid_keyboard(self):
480        """Gets the emulated Bluetooth (BR/EDR) HID keyboard on Chameleon.
481
482        @return: A BluetoothHIDKeyboardFlow object.
483        """
484        return self._chameleond_proxy.bluetooth_keyboard
485
486
487    def get_bluetooth_ref_controller(self):
488        """Gets the emulated BluetoothRefController.
489
490        @return: A BluetoothRefController object.
491        """
492        return self._bluetooth_ref_controller
493
494
495    def get_avsync_probe(self):
496        """Gets the avsync probe device on Chameleon.
497
498        @return: An AVSyncProbeFlow object.
499        """
500        return self._chameleond_proxy.avsync_probe
501
502
503    def get_motor_board(self):
504        """Gets the motor_board device on Chameleon.
505
506        @return: An MotorBoard object.
507        """
508        return self._chameleond_proxy.motor_board
509
510
511    def get_usb_printer(self):
512        """Gets the printer device on Chameleon.
513
514        @return: A printer object.
515        """
516        return self._chameleond_proxy.printer
517
518
519    def get_mac_address(self):
520        """Gets the MAC address of Chameleon.
521
522        @return: A string for MAC address.
523        """
524        return self._chameleond_proxy.GetMacAddress()
525
526
527    def get_bluetooth_a2dp_sink(self):
528        """Gets the Bluetooth A2DP sink on chameleon host.
529
530        @return: A BluetoothA2DPSinkFlow object.
531        """
532        return self._chameleond_proxy.bluetooth_a2dp_sink
533
534    def get_ble_mouse(self):
535        """Gets the BLE mouse (nRF52) on chameleon host.
536
537        @return: A BluetoothHIDFlow object.
538        """
539        return self._chameleond_proxy.ble_mouse
540
541    def get_ble_keyboard(self):
542        """Gets the BLE keyboard on chameleon host.
543
544        @return: A BluetoothHIDFlow object.
545        """
546        return self._chameleond_proxy.ble_keyboard
547
548    def get_ble_phone(self):
549        """Gets the emulated Bluetooth phone on Chameleon.
550
551        @return: A RaspiPhone object.
552        """
553        return self._chameleond_proxy.ble_phone
554
555    def get_platform(self):
556        """ Get the Hardware Platform of the chameleon host
557
558        @return: CHROMEOS/RASPI
559        """
560        return self._chameleond_proxy.get_platform()
561
562
563class ChameleonPort(object):
564    """ChameleonPort is an abstraction of a general port of a Chameleon board.
565
566    It only contains some common methods shared with audio and video ports.
567
568    A Chameleond RPC proxy and an port_id are passed to the construction.
569    The port_id is the unique identity to the port.
570    """
571
572    def __init__(self, chameleond_proxy, port_id):
573        """Construct a ChameleonPort.
574
575        @param chameleond_proxy: Chameleond RPC proxy object.
576        @param port_id: The ID of the input port.
577        """
578        self.chameleond_proxy = chameleond_proxy
579        self.port_id = port_id
580
581
582    def get_connector_id(self):
583        """Returns the connector ID.
584
585        @return: A number of connector ID.
586        """
587        return self.port_id
588
589
590    def get_connector_type(self):
591        """Returns the human readable string for the connector type.
592
593        @return: A string, like "VGA", "DVI", "HDMI", or "DP".
594        """
595        return self.chameleond_proxy.GetConnectorType(self.port_id)
596
597
598    def has_audio_support(self):
599        """Returns if the input has audio support.
600
601        @return: True if the input has audio support; otherwise, False.
602        """
603        return self.chameleond_proxy.HasAudioSupport(self.port_id)
604
605
606    def has_video_support(self):
607        """Returns if the input has video support.
608
609        @return: True if the input has video support; otherwise, False.
610        """
611        return self.chameleond_proxy.HasVideoSupport(self.port_id)
612
613
614    def plug(self):
615        """Asserts HPD line to high, emulating plug."""
616        logging.info('Plug Chameleon port %d', self.port_id)
617        self.chameleond_proxy.Plug(self.port_id)
618
619
620    def unplug(self):
621        """Deasserts HPD line to low, emulating unplug."""
622        logging.info('Unplug Chameleon port %d', self.port_id)
623        self.chameleond_proxy.Unplug(self.port_id)
624
625
626    def set_plug(self, plug_status):
627        """Sets plug/unplug by plug_status.
628
629        @param plug_status: True to plug; False to unplug.
630        """
631        if plug_status:
632            self.plug()
633        else:
634            self.unplug()
635
636
637    @property
638    def plugged(self):
639        """
640        @returns True if this port is plugged to Chameleon, False otherwise.
641
642        """
643        return self.chameleond_proxy.IsPlugged(self.port_id)
644
645
646class ChameleonVideoInput(ChameleonPort):
647    """ChameleonVideoInput is an abstraction of a video input port.
648
649    It contains some special methods to control a video input.
650    """
651
652    _DUT_STABILIZE_TIME = 3
653    _DURATION_UNPLUG_FOR_EDID = 5
654    _TIMEOUT_VIDEO_STABLE_PROBE = 10
655    _EDID_ID_DISABLE = -1
656    _FRAME_RATE = 60
657
658    def __init__(self, chameleon_port):
659        """Construct a ChameleonVideoInput.
660
661        @param chameleon_port: A general ChameleonPort object.
662        """
663        self.chameleond_proxy = chameleon_port.chameleond_proxy
664        self.port_id = chameleon_port.port_id
665        self._original_edid = None
666
667
668    def wait_video_input_stable(self, timeout=None):
669        """Waits the video input stable or timeout.
670
671        @param timeout: The time period to wait for.
672
673        @return: True if the video input becomes stable within the timeout
674                 period; otherwise, False.
675        """
676        is_input_stable = self.chameleond_proxy.WaitVideoInputStable(
677                                self.port_id, timeout)
678
679        # If video input of Chameleon has been stable, wait for DUT software
680        # layer to be stable as well to make sure all the configurations have
681        # been propagated before proceeding.
682        if is_input_stable:
683            logging.info('Video input has been stable. Waiting for the DUT'
684                         ' to be stable...')
685            time.sleep(self._DUT_STABILIZE_TIME)
686        return is_input_stable
687
688
689    def read_edid(self):
690        """Reads the EDID.
691
692        @return: An Edid object or NO_EDID.
693        """
694        edid_binary = self.chameleond_proxy.ReadEdid(self.port_id)
695        if edid_binary is None:
696            return edid_lib.NO_EDID
697        # Read EDID without verify. It may be made corrupted as intended
698        # for the test purpose.
699        return edid_lib.Edid(edid_binary.data, skip_verify=True)
700
701
702    def apply_edid(self, edid):
703        """Applies the given EDID.
704
705        @param edid: An Edid object or NO_EDID.
706        """
707        if edid is edid_lib.NO_EDID:
708            self.chameleond_proxy.ApplyEdid(self.port_id, self._EDID_ID_DISABLE)
709        else:
710            edid_binary = six.moves.xmlrpc_client.Binary(edid.data)
711            edid_id = self.chameleond_proxy.CreateEdid(edid_binary)
712            self.chameleond_proxy.ApplyEdid(self.port_id, edid_id)
713            self.chameleond_proxy.DestroyEdid(edid_id)
714
715    def set_edid_from_file(self, filename, check_video_input=True):
716        """Sets EDID from a file.
717
718        The method is similar to set_edid but reads EDID from a file.
719
720        @param filename: path to EDID file.
721        @param check_video_input: False to disable wait_video_input_stable.
722        """
723        self.set_edid(edid_lib.Edid.from_file(filename),
724                      check_video_input=check_video_input)
725
726    def set_edid(self, edid, check_video_input=True):
727        """The complete flow of setting EDID.
728
729        Unplugs the port if needed, sets EDID, plugs back if it was plugged.
730        The original EDID is stored so user can call restore_edid after this
731        call.
732
733        @param edid: An Edid object.
734        @param check_video_input: False to disable wait_video_input_stable.
735        """
736        plugged = self.plugged
737        if plugged:
738            self.unplug()
739
740        self._original_edid = self.read_edid()
741
742        logging.info('Apply EDID on port %d', self.port_id)
743        self.apply_edid(edid)
744
745        if plugged:
746            time.sleep(self._DURATION_UNPLUG_FOR_EDID)
747            self.plug()
748            if check_video_input:
749                self.wait_video_input_stable(self._TIMEOUT_VIDEO_STABLE_PROBE)
750
751    def restore_edid(self):
752        """Restores original EDID stored when set_edid was called."""
753        current_edid = self.read_edid()
754        if (self._original_edid and
755            self._original_edid.data != current_edid.data):
756            logging.info('Restore the original EDID.')
757            self.apply_edid(self._original_edid)
758
759
760    @contextmanager
761    def use_edid(self, edid, check_video_input=True):
762        """Uses the given EDID in a with statement.
763
764        It sets the EDID up in the beginning and restores to the original
765        EDID in the end. This function is expected to be used in a with
766        statement, like the following:
767
768            with chameleon_port.use_edid(edid):
769                do_some_test_on(chameleon_port)
770
771        @param edid: An EDID object.
772        @param check_video_input: False to disable wait_video_input_stable.
773        """
774        # Set the EDID up in the beginning.
775        self.set_edid(edid, check_video_input=check_video_input)
776
777        try:
778            # Yeild to execute the with statement.
779            yield
780        finally:
781            # Restore the original EDID in the end.
782            self.restore_edid()
783
784    def use_edid_file(self, filename, check_video_input=True):
785        """Uses the given EDID file in a with statement.
786
787        It sets the EDID up in the beginning and restores to the original
788        EDID in the end. This function is expected to be used in a with
789        statement, like the following:
790
791            with chameleon_port.use_edid_file(filename):
792                do_some_test_on(chameleon_port)
793
794        @param filename: A path to the EDID file.
795        @param check_video_input: False to disable wait_video_input_stable.
796        """
797        return self.use_edid(edid_lib.Edid.from_file(filename),
798                             check_video_input=check_video_input)
799
800    def fire_hpd_pulse(self, deassert_interval_usec, assert_interval_usec=None,
801                       repeat_count=1, end_level=1):
802
803        """Fires one or more HPD pulse (low -> high -> low -> ...).
804
805        @param deassert_interval_usec: The time in microsecond of the
806                deassert pulse.
807        @param assert_interval_usec: The time in microsecond of the
808                assert pulse. If None, then use the same value as
809                deassert_interval_usec.
810        @param repeat_count: The count of HPD pulses to fire.
811        @param end_level: HPD ends with 0 for LOW (unplugged) or 1 for
812                HIGH (plugged).
813        """
814        self.chameleond_proxy.FireHpdPulse(
815                self.port_id, deassert_interval_usec,
816                assert_interval_usec, repeat_count, int(bool(end_level)))
817
818
819    def fire_mixed_hpd_pulses(self, widths):
820        """Fires one or more HPD pulses, starting at low, of mixed widths.
821
822        One must specify a list of segment widths in the widths argument where
823        widths[0] is the width of the first low segment, widths[1] is that of
824        the first high segment, widths[2] is that of the second low segment...
825        etc. The HPD line stops at low if even number of segment widths are
826        specified; otherwise, it stops at high.
827
828        @param widths: list of pulse segment widths in usec.
829        """
830        self.chameleond_proxy.FireMixedHpdPulses(self.port_id, widths)
831
832
833    def capture_screen(self):
834        """Captures Chameleon framebuffer.
835
836        @return An Image object.
837        """
838        return Image.fromstring(
839                'RGB',
840                self.get_resolution(),
841                self.chameleond_proxy.DumpPixels(self.port_id).data)
842
843
844    def get_resolution(self):
845        """Gets the source resolution.
846
847        @return: A (width, height) tuple.
848        """
849        # The return value of RPC is converted to a list. Convert it back to
850        # a tuple.
851        return tuple(self.chameleond_proxy.DetectResolution(self.port_id))
852
853
854    def set_content_protection(self, enable):
855        """Sets the content protection state on the port.
856
857        @param enable: True to enable; False to disable.
858        """
859        self.chameleond_proxy.SetContentProtection(self.port_id, enable)
860
861
862    def is_content_protection_enabled(self):
863        """Returns True if the content protection is enabled on the port.
864
865        @return: True if the content protection is enabled; otherwise, False.
866        """
867        return self.chameleond_proxy.IsContentProtectionEnabled(self.port_id)
868
869
870    def is_video_input_encrypted(self):
871        """Returns True if the video input on the port is encrypted.
872
873        @return: True if the video input is encrypted; otherwise, False.
874        """
875        return self.chameleond_proxy.IsVideoInputEncrypted(self.port_id)
876
877
878    def start_monitoring_audio_video_capturing_delay(self):
879        """Starts an audio/video synchronization utility."""
880        self.chameleond_proxy.StartMonitoringAudioVideoCapturingDelay()
881
882
883    def get_audio_video_capturing_delay(self):
884        """Gets the time interval between the first audio/video cpatured data.
885
886        @return: A floating points indicating the time interval between the
887                 first audio/video data captured. If the result is negative,
888                 then the first video data is earlier, otherwise the first
889                 audio data is earlier.
890        """
891        return self.chameleond_proxy.GetAudioVideoCapturingDelay()
892
893
894    def start_capturing_video(self, box=None):
895        """
896        Captures video frames. Asynchronous, returns immediately.
897
898        @param box: int tuple, (x, y, width, height) pixel coordinates.
899                    Defines the rectangular boundary within which to capture.
900        """
901
902        if box is None:
903            self.chameleond_proxy.StartCapturingVideo(self.port_id)
904        else:
905            self.chameleond_proxy.StartCapturingVideo(self.port_id, *box)
906
907
908    def stop_capturing_video(self):
909        """
910        Stops the ongoing video frame capturing.
911
912        """
913        self.chameleond_proxy.StopCapturingVideo()
914
915
916    def get_captured_frame_count(self):
917        """
918        @return: int, the number of frames that have been captured.
919
920        """
921        return self.chameleond_proxy.GetCapturedFrameCount()
922
923
924    def read_captured_frame(self, index):
925        """
926        @param index: int, index of the desired captured frame.
927        @return: xmlrpclib.Binary object containing a byte-array of the pixels.
928
929        """
930
931        frame = self.chameleond_proxy.ReadCapturedFrame(index)
932        return Image.fromstring('RGB',
933                                self.get_captured_resolution(),
934                                frame.data)
935
936
937    def get_captured_checksums(self, start_index=0, stop_index=None):
938        """
939        @param start_index: int, index of the frame to start with.
940        @param stop_index: int, index of the frame (excluded) to stop at.
941        @return: a list of checksums of frames captured.
942
943        """
944        return self.chameleond_proxy.GetCapturedChecksums(start_index,
945                                                          stop_index)
946
947
948    def get_captured_fps_list(self, time_to_start=0, total_period=None):
949        """
950        @param time_to_start: time in second, support floating number, only
951                              measure the period starting at this time.
952                              If negative, it is the time before stop, e.g.
953                              -2 meaning 2 seconds before stop.
954        @param total_period: time in second, integer, the total measuring
955                             period. If not given, use the maximum time
956                             (integer) to the end.
957        @return: a list of fps numbers, or [-1] if any error.
958
959        """
960        checksums = self.get_captured_checksums()
961
962        frame_to_start = int(round(time_to_start * self._FRAME_RATE))
963        if total_period is None:
964            # The default is the maximum time (integer) to the end.
965            total_period = (len(checksums) - frame_to_start) // self._FRAME_RATE
966        frame_to_stop = frame_to_start + total_period * self._FRAME_RATE
967
968        if frame_to_start >= len(checksums) or frame_to_stop >= len(checksums):
969            logging.error('The given time interval is out-of-range.')
970            return [-1]
971
972        # Only pick the checksum we are interested.
973        checksums = checksums[frame_to_start:frame_to_stop]
974
975        # Count the unique checksums per second, i.e. FPS
976        logging.debug('Output the fps info below:')
977        fps_list = []
978        for i in range(0, len(checksums), self._FRAME_RATE):
979            unique_count = 0
980            debug_str = ''
981            for j in range(i, i + self._FRAME_RATE):
982                if j == 0 or checksums[j] != checksums[j - 1]:
983                    unique_count += 1
984                    debug_str += '*'
985                else:
986                    debug_str += '.'
987            fps_list.append(unique_count)
988            logging.debug('%2dfps %s', unique_count, debug_str)
989
990        return fps_list
991
992
993    def search_fps_pattern(self, pattern_diff_frame, pattern_window=None,
994                           time_to_start=0):
995        """Search the captured frames and return the time where FPS is greater
996        than given FPS pattern.
997
998        A FPS pattern is described as how many different frames in a sliding
999        window. For example, 5 differnt frames in a window of 60 frames.
1000
1001        @param pattern_diff_frame: number of different frames for the pattern.
1002        @param pattern_window: number of frames for the sliding window. Default
1003                               is 1 second.
1004        @param time_to_start: time in second, support floating number,
1005                              start to search from the given time.
1006        @return: the time matching the pattern. -1.0 if not found.
1007
1008        """
1009        if pattern_window is None:
1010            pattern_window = self._FRAME_RATE
1011
1012        checksums = self.get_captured_checksums()
1013
1014        frame_to_start = int(round(time_to_start * self._FRAME_RATE))
1015        first_checksum = checksums[frame_to_start]
1016
1017        for i in range(frame_to_start + 1, len(checksums) - pattern_window):
1018            unique_count = 0
1019            for j in range(i, i + pattern_window):
1020                if j == 0 or checksums[j] != checksums[j - 1]:
1021                    unique_count += 1
1022            if unique_count >= pattern_diff_frame:
1023                return float(i) / self._FRAME_RATE
1024
1025        return -1.0
1026
1027
1028    def get_captured_resolution(self):
1029        """
1030        @return: (width, height) tuple, the resolution of captured frames.
1031
1032        """
1033        return self.chameleond_proxy.GetCapturedResolution()
1034
1035
1036
1037class ChameleonAudioInput(ChameleonPort):
1038    """ChameleonAudioInput is an abstraction of an audio input port.
1039
1040    It contains some special methods to control an audio input.
1041    """
1042
1043    def __init__(self, chameleon_port):
1044        """Construct a ChameleonAudioInput.
1045
1046        @param chameleon_port: A general ChameleonPort object.
1047        """
1048        self.chameleond_proxy = chameleon_port.chameleond_proxy
1049        self.port_id = chameleon_port.port_id
1050
1051
1052    def start_capturing_audio(self):
1053        """Starts capturing audio."""
1054        return self.chameleond_proxy.StartCapturingAudio(self.port_id)
1055
1056
1057    def stop_capturing_audio(self):
1058        """Stops capturing audio.
1059
1060        Returns:
1061          A tuple (remote_path, format).
1062          remote_path: The captured file path on Chameleon.
1063          format: A dict containing:
1064            file_type: 'raw' or 'wav'.
1065            sample_format: 'S32_LE' for 32-bit signed integer in little-endian.
1066              Refer to aplay manpage for other formats.
1067            channel: channel number.
1068            rate: sampling rate.
1069        """
1070        remote_path, data_format = self.chameleond_proxy.StopCapturingAudio(
1071                self.port_id)
1072        return remote_path, data_format
1073
1074
1075class ChameleonAudioOutput(ChameleonPort):
1076    """ChameleonAudioOutput is an abstraction of an audio output port.
1077
1078    It contains some special methods to control an audio output.
1079    """
1080
1081    def __init__(self, chameleon_port):
1082        """Construct a ChameleonAudioOutput.
1083
1084        @param chameleon_port: A general ChameleonPort object.
1085        """
1086        self.chameleond_proxy = chameleon_port.chameleond_proxy
1087        self.port_id = chameleon_port.port_id
1088
1089
1090    def start_playing_audio(self, path, data_format):
1091        """Starts playing audio.
1092
1093        @param path: The path to the file to play on Chameleon.
1094        @param data_format: A dict containing data format. Currently Chameleon
1095                            only accepts data format:
1096                            dict(file_type='raw', sample_format='S32_LE',
1097                                 channel=8, rate=48000).
1098
1099        """
1100        self.chameleond_proxy.StartPlayingAudio(self.port_id, path, data_format)
1101
1102
1103    def stop_playing_audio(self):
1104        """Stops capturing audio."""
1105        self.chameleond_proxy.StopPlayingAudio(self.port_id)
1106
1107
1108def make_chameleon_hostname(dut_hostname):
1109    """Given a DUT's hostname, returns the hostname of its Chameleon.
1110
1111    @param dut_hostname: Hostname of a DUT.
1112
1113    @return Hostname of the DUT's Chameleon.
1114    """
1115    host_parts = dut_hostname.split('.')
1116    host_parts[0] = host_parts[0] + '-chameleon'
1117    return '.'.join(host_parts)
1118
1119
1120def make_btpeer_hostnames(dut_hostname):
1121    """Given a DUT's hostname, returns the hostname of its bluetooth peers.
1122
1123    A DUT can have up to 4 Bluetooth peers named  hostname-btpeer[1-4]
1124    @param dut_hostname: Hostname of a DUT.
1125
1126    @return List of hostname of the DUT's Bluetooth peer devices
1127    """
1128    hostnames = []
1129    host_parts = dut_hostname.split('.')
1130    for i in range(1,5):
1131        hostname_prefix = host_parts[0] + '-btpeer' +str(i)
1132        hostname = [hostname_prefix]
1133        hostname.extend(host_parts[1:])
1134        hostnames.append('.'.join(hostname))
1135    return hostnames
1136
1137def create_chameleon_board(dut_hostname, args):
1138    """Creates a ChameleonBoard object with either DUT's hostname or arguments.
1139
1140    If the DUT's hostname is in the lab zone, it connects to the Chameleon by
1141    append the hostname with '-chameleon' suffix. If not, checks if the args
1142    contains the key-value pair 'chameleon_host=IP'.
1143
1144    @param dut_hostname: Hostname of a DUT.
1145    @param args: A string of arguments passed from the command line.
1146
1147    @return A ChameleonBoard object.
1148
1149    @raise ChameleonConnectionError if unknown hostname.
1150    """
1151    connection = None
1152    hostname = make_chameleon_hostname(dut_hostname)
1153    if utils.host_is_in_lab_zone(hostname):
1154        connection = ChameleonConnection(hostname)
1155    else:
1156        args_dict = utils.args_to_dict(args)
1157        hostname = args_dict.get('chameleon_host', None)
1158        port = args_dict.get('chameleon_port', CHAMELEON_PORT)
1159        if hostname:
1160            connection = ChameleonConnection(hostname, port)
1161        else:
1162            raise ChameleonConnectionError('No chameleon_host is given in args')
1163
1164    return ChameleonBoard(connection)
1165