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