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