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