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