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