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