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 httplib
      6 import logging
      7 import socket
      8 import time
      9 import xmlrpclib
     10 from contextlib import contextmanager
     11 
     12 from PIL import Image
     13 
     14 from autotest_lib.client.bin import utils
     15 from autotest_lib.client.common_lib import error
     16 from autotest_lib.client.cros.chameleon import audio_board
     17 from autotest_lib.client.cros.chameleon import edid as edid_lib
     18 from autotest_lib.client.cros.chameleon import usb_controller
     19 
     20 
     21 CHAMELEON_PORT = 9992
     22 
     23 
     24 class ChameleonConnectionError(error.TestError):
     25     """Indicates that connecting to Chameleon failed.
     26 
     27     It is fatal to the test unless caught.
     28     """
     29     pass
     30 
     31 
     32 class ChameleonConnection(object):
     33     """ChameleonConnection abstracts the network connection to the board.
     34 
     35     ChameleonBoard and ChameleonPort use it for accessing Chameleon RPC.
     36 
     37     """
     38 
     39     def __init__(self, hostname, port=CHAMELEON_PORT):
     40         """Constructs a ChameleonConnection.
     41 
     42         @param hostname: Hostname the chameleond process is running.
     43         @param port: Port number the chameleond process is listening on.
     44 
     45         @raise ChameleonConnectionError if connection failed.
     46         """
     47         self.chameleond_proxy = ChameleonConnection._create_server_proxy(
     48                 hostname, port)
     49 
     50 
     51     @staticmethod
     52     def _create_server_proxy(hostname, port):
     53         """Creates the chameleond server proxy.
     54 
     55         @param hostname: Hostname the chameleond process is running.
     56         @param port: Port number the chameleond process is listening on.
     57 
     58         @return ServerProxy object to chameleond.
     59 
     60         @raise ChameleonConnectionError if connection failed.
     61         """
     62         remote = 'http://%s:%s' % (hostname, port)
     63         chameleond_proxy = xmlrpclib.ServerProxy(remote, allow_none=True)
     64         # Call a RPC to test.
     65         try:
     66             chameleond_proxy.GetSupportedPorts()
     67         except (socket.error,
     68                 xmlrpclib.ProtocolError,
     69                 httplib.BadStatusLine) as e:
     70             raise ChameleonConnectionError(e)
     71         return chameleond_proxy
     72 
     73 
     74 class ChameleonBoard(object):
     75     """ChameleonBoard is an abstraction of a Chameleon board.
     76 
     77     A Chameleond RPC proxy is passed to the construction such that it can
     78     use this proxy to control the Chameleon board.
     79 
     80     User can use host to access utilities that are not provided by
     81     Chameleond XMLRPC server, e.g. send_file and get_file, which are provided by
     82     ssh_host.SSHHost, which is the base class of ChameleonHost.
     83 
     84     """
     85 
     86     def __init__(self, chameleon_connection, chameleon_host=None):
     87         """Construct a ChameleonBoard.
     88 
     89         @param chameleon_connection: ChameleonConnection object.
     90         @param chameleon_host: ChameleonHost object. None if this ChameleonBoard
     91                                is not created by a ChameleonHost.
     92         """
     93         self.host = chameleon_host
     94         self._chameleond_proxy = chameleon_connection.chameleond_proxy
     95         self._usb_ctrl = usb_controller.USBController(chameleon_connection)
     96         if self._chameleond_proxy.HasAudioBoard():
     97             self._audio_board = audio_board.AudioBoard(chameleon_connection)
     98         else:
     99             self._audio_board = None
    100             logging.info('There is no audio board on this Chameleon.')
    101 
    102     def reset(self):
    103         """Resets Chameleon board."""
    104         self._chameleond_proxy.Reset()
    105 
    106 
    107     def get_all_ports(self):
    108         """Gets all the ports on Chameleon board which are connected.
    109 
    110         @return: A list of ChameleonPort objects.
    111         """
    112         ports = self._chameleond_proxy.ProbePorts()
    113         return [ChameleonPort(self._chameleond_proxy, port) for port in ports]
    114 
    115 
    116     def get_all_inputs(self):
    117         """Gets all the input ports on Chameleon board which are connected.
    118 
    119         @return: A list of ChameleonPort objects.
    120         """
    121         ports = self._chameleond_proxy.ProbeInputs()
    122         return [ChameleonPort(self._chameleond_proxy, port) for port in ports]
    123 
    124 
    125     def get_all_outputs(self):
    126         """Gets all the output ports on Chameleon board which are connected.
    127 
    128         @return: A list of ChameleonPort objects.
    129         """
    130         ports = self._chameleond_proxy.ProbeOutputs()
    131         return [ChameleonPort(self._chameleond_proxy, port) for port in ports]
    132 
    133 
    134     def get_label(self):
    135         """Gets the label which indicates the display connection.
    136 
    137         @return: A string of the label, like 'hdmi', 'dp_hdmi', etc.
    138         """
    139         connectors = []
    140         for port in self._chameleond_proxy.ProbeInputs():
    141             if self._chameleond_proxy.HasVideoSupport(port):
    142                 connector = self._chameleond_proxy.GetConnectorType(port).lower()
    143                 connectors.append(connector)
    144         # Eliminate duplicated ports. It simplifies the labels of dual-port
    145         # devices, i.e. dp_dp categorized into dp.
    146         return '_'.join(sorted(set(connectors)))
    147 
    148 
    149     def get_audio_board(self):
    150         """Gets the audio board on Chameleon.
    151 
    152         @return: An AudioBoard object.
    153         """
    154         return self._audio_board
    155 
    156 
    157     def get_usb_controller(self):
    158         """Gets the USB controller on Chameleon.
    159 
    160         @return: A USBController object.
    161         """
    162         return self._usb_ctrl
    163 
    164 
    165     def get_mac_address(self):
    166         """Gets the MAC address of Chameleon.
    167 
    168         @return: A string for MAC address.
    169         """
    170         return self._chameleond_proxy.GetMacAddress()
    171 
    172 
    173 class ChameleonPort(object):
    174     """ChameleonPort is an abstraction of a general port of a Chameleon board.
    175 
    176     It only contains some common methods shared with audio and video ports.
    177 
    178     A Chameleond RPC proxy and an port_id are passed to the construction.
    179     The port_id is the unique identity to the port.
    180     """
    181 
    182     def __init__(self, chameleond_proxy, port_id):
    183         """Construct a ChameleonPort.
    184 
    185         @param chameleond_proxy: Chameleond RPC proxy object.
    186         @param port_id: The ID of the input port.
    187         """
    188         self.chameleond_proxy = chameleond_proxy
    189         self.port_id = port_id
    190 
    191 
    192     def get_connector_id(self):
    193         """Returns the connector ID.
    194 
    195         @return: A number of connector ID.
    196         """
    197         return self.port_id
    198 
    199 
    200     def get_connector_type(self):
    201         """Returns the human readable string for the connector type.
    202 
    203         @return: A string, like "VGA", "DVI", "HDMI", or "DP".
    204         """
    205         return self.chameleond_proxy.GetConnectorType(self.port_id)
    206 
    207 
    208     def has_audio_support(self):
    209         """Returns if the input has audio support.
    210 
    211         @return: True if the input has audio support; otherwise, False.
    212         """
    213         return self.chameleond_proxy.HasAudioSupport(self.port_id)
    214 
    215 
    216     def has_video_support(self):
    217         """Returns if the input has video support.
    218 
    219         @return: True if the input has video support; otherwise, False.
    220         """
    221         return self.chameleond_proxy.HasVideoSupport(self.port_id)
    222 
    223 
    224     def plug(self):
    225         """Asserts HPD line to high, emulating plug."""
    226         logging.info('Plug Chameleon port %d', self.port_id)
    227         self.chameleond_proxy.Plug(self.port_id)
    228 
    229 
    230     def unplug(self):
    231         """Deasserts HPD line to low, emulating unplug."""
    232         logging.info('Unplug Chameleon port %d', self.port_id)
    233         self.chameleond_proxy.Unplug(self.port_id)
    234 
    235 
    236     def set_plug(self, plug_status):
    237         """Sets plug/unplug by plug_status.
    238 
    239         @param plug_status: True to plug; False to unplug.
    240         """
    241         if plug_status:
    242             self.plug()
    243         else:
    244             self.unplug()
    245 
    246 
    247     @property
    248     def plugged(self):
    249         """
    250         @returns True if this port is plugged to Chameleon, False otherwise.
    251 
    252         """
    253         return self.chameleond_proxy.IsPlugged(self.port_id)
    254 
    255 
    256 class ChameleonVideoInput(ChameleonPort):
    257     """ChameleonVideoInput is an abstraction of a video input port.
    258 
    259     It contains some special methods to control a video input.
    260     """
    261 
    262     _DUT_STABILIZE_TIME = 3
    263     _DURATION_UNPLUG_FOR_EDID = 5
    264     _TIMEOUT_VIDEO_STABLE_PROBE = 10
    265     _EDID_ID_DISABLE = -1
    266 
    267     def __init__(self, chameleon_port):
    268         """Construct a ChameleonVideoInput.
    269 
    270         @param chameleon_port: A general ChameleonPort object.
    271         """
    272         self.chameleond_proxy = chameleon_port.chameleond_proxy
    273         self.port_id = chameleon_port.port_id
    274 
    275 
    276     def wait_video_input_stable(self, timeout=None):
    277         """Waits the video input stable or timeout.
    278 
    279         @param timeout: The time period to wait for.
    280 
    281         @return: True if the video input becomes stable within the timeout
    282                  period; otherwise, False.
    283         """
    284         is_input_stable = self.chameleond_proxy.WaitVideoInputStable(
    285                                 self.port_id, timeout)
    286 
    287         # If video input of Chameleon has been stable, wait for DUT software
    288         # layer to be stable as well to make sure all the configurations have
    289         # been propagated before proceeding.
    290         if is_input_stable:
    291             logging.info('Video input has been stable. Waiting for the DUT'
    292                          ' to be stable...')
    293             time.sleep(self._DUT_STABILIZE_TIME)
    294         return is_input_stable
    295 
    296 
    297     def read_edid(self):
    298         """Reads the EDID.
    299 
    300         @return: An Edid object or NO_EDID.
    301         """
    302         edid_binary = self.chameleond_proxy.ReadEdid(self.port_id)
    303         if edid_binary is None:
    304             return edid_lib.NO_EDID
    305         # Read EDID without verify. It may be made corrupted as intended
    306         # for the test purpose.
    307         return edid_lib.Edid(edid_binary.data, skip_verify=True)
    308 
    309 
    310     def apply_edid(self, edid):
    311         """Applies the given EDID.
    312 
    313         @param edid: An Edid object or NO_EDID.
    314         """
    315         if edid is edid_lib.NO_EDID:
    316           self.chameleond_proxy.ApplyEdid(self.port_id, self._EDID_ID_DISABLE)
    317         else:
    318           edid_binary = xmlrpclib.Binary(edid.data)
    319           edid_id = self.chameleond_proxy.CreateEdid(edid_binary)
    320           self.chameleond_proxy.ApplyEdid(self.port_id, edid_id)
    321           self.chameleond_proxy.DestroyEdid(edid_id)
    322 
    323 
    324     @contextmanager
    325     def use_edid(self, edid):
    326         """Uses the given EDID in a with statement.
    327 
    328         It sets the EDID up in the beginning and restores to the original
    329         EDID in the end. This function is expected to be used in a with
    330         statement, like the following:
    331 
    332             with chameleon_port.use_edid(edid):
    333                 do_some_test_on(chameleon_port)
    334 
    335         @param edid: An EDID object.
    336         """
    337         # Set the EDID up in the beginning.
    338         plugged = self.plugged
    339         if plugged:
    340             self.unplug()
    341 
    342         original_edid = self.read_edid()
    343         logging.info('Apply EDID on port %d', self.port_id)
    344         self.apply_edid(edid)
    345 
    346         if plugged:
    347             time.sleep(self._DURATION_UNPLUG_FOR_EDID)
    348             self.plug()
    349             self.wait_video_input_stable(self._TIMEOUT_VIDEO_STABLE_PROBE)
    350 
    351         try:
    352             # Yeild to execute the with statement.
    353             yield
    354         finally:
    355             # Restore the original EDID in the end.
    356             current_edid = self.read_edid()
    357             if original_edid.data != current_edid.data:
    358                 logging.info('Restore the original EDID.')
    359                 self.apply_edid(original_edid)
    360 
    361 
    362     def use_edid_file(self, filename):
    363         """Uses the given EDID file in a with statement.
    364 
    365         It sets the EDID up in the beginning and restores to the original
    366         EDID in the end. This function is expected to be used in a with
    367         statement, like the following:
    368 
    369             with chameleon_port.use_edid_file(filename):
    370                 do_some_test_on(chameleon_port)
    371 
    372         @param filename: A path to the EDID file.
    373         """
    374         return self.use_edid(edid_lib.Edid.from_file(filename))
    375 
    376 
    377     def fire_hpd_pulse(self, deassert_interval_usec, assert_interval_usec=None,
    378                        repeat_count=1, end_level=1):
    379 
    380         """Fires one or more HPD pulse (low -> high -> low -> ...).
    381 
    382         @param deassert_interval_usec: The time in microsecond of the
    383                 deassert pulse.
    384         @param assert_interval_usec: The time in microsecond of the
    385                 assert pulse. If None, then use the same value as
    386                 deassert_interval_usec.
    387         @param repeat_count: The count of HPD pulses to fire.
    388         @param end_level: HPD ends with 0 for LOW (unplugged) or 1 for
    389                 HIGH (plugged).
    390         """
    391         self.chameleond_proxy.FireHpdPulse(
    392                 self.port_id, deassert_interval_usec,
    393                 assert_interval_usec, repeat_count, int(bool(end_level)))
    394 
    395 
    396     def fire_mixed_hpd_pulses(self, widths):
    397         """Fires one or more HPD pulses, starting at low, of mixed widths.
    398 
    399         One must specify a list of segment widths in the widths argument where
    400         widths[0] is the width of the first low segment, widths[1] is that of
    401         the first high segment, widths[2] is that of the second low segment...
    402         etc. The HPD line stops at low if even number of segment widths are
    403         specified; otherwise, it stops at high.
    404 
    405         @param widths: list of pulse segment widths in usec.
    406         """
    407         self.chameleond_proxy.FireMixedHpdPulses(self.port_id, widths)
    408 
    409 
    410     def capture_screen(self):
    411         """Captures Chameleon framebuffer.
    412 
    413         @return An Image object.
    414         """
    415         return Image.fromstring(
    416                 'RGB',
    417                 self.get_resolution(),
    418                 self.chameleond_proxy.DumpPixels(self.port_id).data)
    419 
    420 
    421     def get_resolution(self):
    422         """Gets the source resolution.
    423 
    424         @return: A (width, height) tuple.
    425         """
    426         # The return value of RPC is converted to a list. Convert it back to
    427         # a tuple.
    428         return tuple(self.chameleond_proxy.DetectResolution(self.port_id))
    429 
    430 
    431     def set_content_protection(self, enable):
    432         """Sets the content protection state on the port.
    433 
    434         @param enable: True to enable; False to disable.
    435         """
    436         self.chameleond_proxy.SetContentProtection(self.port_id, enable)
    437 
    438 
    439     def is_content_protection_enabled(self):
    440         """Returns True if the content protection is enabled on the port.
    441 
    442         @return: True if the content protection is enabled; otherwise, False.
    443         """
    444         return self.chameleond_proxy.IsContentProtectionEnabled(self.port_id)
    445 
    446 
    447     def is_video_input_encrypted(self):
    448         """Returns True if the video input on the port is encrypted.
    449 
    450         @return: True if the video input is encrypted; otherwise, False.
    451         """
    452         return self.chameleond_proxy.IsVideoInputEncrypted(self.port_id)
    453 
    454 
    455     def start_capturing_video(self, box=None):
    456         """
    457         Captures video frames. Asynchronous, returns immediately.
    458 
    459         @param box: int tuple, left, upper, right, lower pixel coordinates.
    460                     Defines the rectangular boundary within which to capture.
    461         """
    462 
    463         if box is None:
    464             self.chameleond_proxy.StartCapturingVideo(self.port_id)
    465         else:
    466             self.chameleond_proxy.StartCapturingVideo(self.port_id, *box)
    467 
    468 
    469     def stop_capturing_video(self):
    470         """
    471         Stops the ongoing video frame capturing.
    472 
    473         """
    474         self.chameleond_proxy.StopCapturingVideo(self.port_id)
    475 
    476 
    477     def get_captured_frame_count(self):
    478         """
    479         @return: int, the number of frames that have been captured.
    480 
    481         """
    482         return self.chameleond_proxy.GetCapturedFrameCount()
    483 
    484 
    485     def read_captured_frame(self, index):
    486         """
    487         @param index: int, index of the desired captured frame.
    488         @return: xmlrpclib.Binary object containing a byte-array of the pixels.
    489 
    490         """
    491 
    492         frame = self.chameleond_proxy.ReadCapturedFrame(index)
    493         return Image.fromstring('RGB',
    494                                 self.get_captured_resolution(),
    495                                 frame.data)
    496 
    497 
    498     def get_captured_checksums(self, start_index=0, stop_index=None):
    499         """
    500         @param start_index: int, index of the frame to start with.
    501         @param stop_index: int, index of the frame (excluded) to stop at.
    502         @return: a list of checksums of frames captured.
    503 
    504         """
    505         return self.chameleond_proxy.GetCapturedChecksums(start_index,
    506                                                           stop_index)
    507 
    508 
    509     def get_captured_resolution(self):
    510         """
    511         @return: (width, height) tuple, the resolution of captured frames.
    512 
    513         """
    514         return self.chameleond_proxy.GetCapturedResolution()
    515 
    516 
    517 
    518 class ChameleonAudioInput(ChameleonPort):
    519     """ChameleonAudioInput is an abstraction of an audio input port.
    520 
    521     It contains some special methods to control an audio input.
    522     """
    523 
    524     def __init__(self, chameleon_port):
    525         """Construct a ChameleonAudioInput.
    526 
    527         @param chameleon_port: A general ChameleonPort object.
    528         """
    529         self.chameleond_proxy = chameleon_port.chameleond_proxy
    530         self.port_id = chameleon_port.port_id
    531 
    532 
    533     def start_capturing_audio(self):
    534         """Starts capturing audio."""
    535         return self.chameleond_proxy.StartCapturingAudio(self.port_id)
    536 
    537 
    538     def stop_capturing_audio(self):
    539         """Stops capturing audio.
    540 
    541         Returns:
    542           A tuple (remote_path, format).
    543           remote_path: The captured file path on Chameleon.
    544           format: A dict containing:
    545             file_type: 'raw' or 'wav'.
    546             sample_format: 'S32_LE' for 32-bit signed integer in little-endian.
    547               Refer to aplay manpage for other formats.
    548             channel: channel number.
    549             rate: sampling rate.
    550         """
    551         remote_path, data_format = self.chameleond_proxy.StopCapturingAudio(
    552                 self.port_id)
    553         return remote_path, data_format
    554 
    555 
    556 class ChameleonAudioOutput(ChameleonPort):
    557     """ChameleonAudioOutput is an abstraction of an audio output port.
    558 
    559     It contains some special methods to control an audio output.
    560     """
    561 
    562     def __init__(self, chameleon_port):
    563         """Construct a ChameleonAudioOutput.
    564 
    565         @param chameleon_port: A general ChameleonPort object.
    566         """
    567         self.chameleond_proxy = chameleon_port.chameleond_proxy
    568         self.port_id = chameleon_port.port_id
    569 
    570 
    571     def start_playing_audio(self, path, data_format):
    572         """Starts playing audio.
    573 
    574         @param path: The path to the file to play on Chameleon.
    575         @param data_format: A dict containing data format. Currently Chameleon
    576                             only accepts data format:
    577                             dict(file_type='raw', sample_format='S32_LE',
    578                                  channel=8, rate=48000).
    579 
    580         """
    581         self.chameleond_proxy.StartPlayingAudio(self.port_id, path, data_format)
    582 
    583 
    584     def stop_playing_audio(self):
    585         """Stops capturing audio."""
    586         self.chameleond_proxy.StopPlayingAudio(self.port_id)
    587 
    588 
    589 def make_chameleon_hostname(dut_hostname):
    590     """Given a DUT's hostname, returns the hostname of its Chameleon.
    591 
    592     @param dut_hostname: Hostname of a DUT.
    593 
    594     @return Hostname of the DUT's Chameleon.
    595     """
    596     host_parts = dut_hostname.split('.')
    597     host_parts[0] = host_parts[0] + '-chameleon'
    598     return '.'.join(host_parts)
    599 
    600 
    601 def create_chameleon_board(dut_hostname, args):
    602     """Given either DUT's hostname or argments, creates a ChameleonBoard object.
    603 
    604     If the DUT's hostname is in the lab zone, it connects to the Chameleon by
    605     append the hostname with '-chameleon' suffix. If not, checks if the args
    606     contains the key-value pair 'chameleon_host=IP'.
    607 
    608     @param dut_hostname: Hostname of a DUT.
    609     @param args: A string of arguments passed from the command line.
    610 
    611     @return A ChameleonBoard object.
    612 
    613     @raise ChameleonConnectionError if unknown hostname.
    614     """
    615     connection = None
    616     hostname = make_chameleon_hostname(dut_hostname)
    617     if utils.host_is_in_lab_zone(hostname):
    618         connection = ChameleonConnection(hostname)
    619     else:
    620         args_dict = utils.args_to_dict(args)
    621         hostname = args_dict.get('chameleon_host', None)
    622         port = args_dict.get('chameleon_port', CHAMELEON_PORT)
    623         if hostname:
    624             connection = ChameleonConnection(hostname, port)
    625         else:
    626             raise ChameleonConnectionError('No chameleon_host is given in args')
    627 
    628     return ChameleonBoard(connection)
    629