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