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_bluetooth_hid_mouse(self):
333        """Gets the emulated Bluetooth (BR/EDR) HID mouse on Chameleon.
334
335        @return: A BluetoothHIDMouseFlow object.
336        """
337        return self._chameleond_proxy.bluetooth_mouse
338
339
340    def get_bluetooth_hog_mouse(self):
341        """Gets the emulated Bluetooth Low Energy HID mouse on Chameleon.
342
343        Note that this uses HID over GATT, or HOG.
344
345        @return: A BluetoothHOGMouseFlow object.
346        """
347        return self._chameleond_proxy.bluetooth_hog_mouse
348
349
350    def get_avsync_probe(self):
351        """Gets the avsync probe device on Chameleon.
352
353        @return: An AVSyncProbeFlow object.
354        """
355        return self._chameleond_proxy.avsync_probe
356
357
358    def get_motor_board(self):
359        """Gets the motor_board device on Chameleon.
360
361        @return: An MotorBoard object.
362        """
363        return self._chameleond_proxy.motor_board
364
365
366    def get_mac_address(self):
367        """Gets the MAC address of Chameleon.
368
369        @return: A string for MAC address.
370        """
371        return self._chameleond_proxy.GetMacAddress()
372
373
374class ChameleonPort(object):
375    """ChameleonPort is an abstraction of a general port of a Chameleon board.
376
377    It only contains some common methods shared with audio and video ports.
378
379    A Chameleond RPC proxy and an port_id are passed to the construction.
380    The port_id is the unique identity to the port.
381    """
382
383    def __init__(self, chameleond_proxy, port_id):
384        """Construct a ChameleonPort.
385
386        @param chameleond_proxy: Chameleond RPC proxy object.
387        @param port_id: The ID of the input port.
388        """
389        self.chameleond_proxy = chameleond_proxy
390        self.port_id = port_id
391
392
393    def get_connector_id(self):
394        """Returns the connector ID.
395
396        @return: A number of connector ID.
397        """
398        return self.port_id
399
400
401    def get_connector_type(self):
402        """Returns the human readable string for the connector type.
403
404        @return: A string, like "VGA", "DVI", "HDMI", or "DP".
405        """
406        return self.chameleond_proxy.GetConnectorType(self.port_id)
407
408
409    def has_audio_support(self):
410        """Returns if the input has audio support.
411
412        @return: True if the input has audio support; otherwise, False.
413        """
414        return self.chameleond_proxy.HasAudioSupport(self.port_id)
415
416
417    def has_video_support(self):
418        """Returns if the input has video support.
419
420        @return: True if the input has video support; otherwise, False.
421        """
422        return self.chameleond_proxy.HasVideoSupport(self.port_id)
423
424
425    def plug(self):
426        """Asserts HPD line to high, emulating plug."""
427        logging.info('Plug Chameleon port %d', self.port_id)
428        self.chameleond_proxy.Plug(self.port_id)
429
430
431    def unplug(self):
432        """Deasserts HPD line to low, emulating unplug."""
433        logging.info('Unplug Chameleon port %d', self.port_id)
434        self.chameleond_proxy.Unplug(self.port_id)
435
436
437    def set_plug(self, plug_status):
438        """Sets plug/unplug by plug_status.
439
440        @param plug_status: True to plug; False to unplug.
441        """
442        if plug_status:
443            self.plug()
444        else:
445            self.unplug()
446
447
448    @property
449    def plugged(self):
450        """
451        @returns True if this port is plugged to Chameleon, False otherwise.
452
453        """
454        return self.chameleond_proxy.IsPlugged(self.port_id)
455
456
457class ChameleonVideoInput(ChameleonPort):
458    """ChameleonVideoInput is an abstraction of a video input port.
459
460    It contains some special methods to control a video input.
461    """
462
463    _DUT_STABILIZE_TIME = 3
464    _DURATION_UNPLUG_FOR_EDID = 5
465    _TIMEOUT_VIDEO_STABLE_PROBE = 10
466    _EDID_ID_DISABLE = -1
467    _FRAME_RATE = 60
468
469    def __init__(self, chameleon_port):
470        """Construct a ChameleonVideoInput.
471
472        @param chameleon_port: A general ChameleonPort object.
473        """
474        self.chameleond_proxy = chameleon_port.chameleond_proxy
475        self.port_id = chameleon_port.port_id
476        self._original_edid = None
477
478
479    def wait_video_input_stable(self, timeout=None):
480        """Waits the video input stable or timeout.
481
482        @param timeout: The time period to wait for.
483
484        @return: True if the video input becomes stable within the timeout
485                 period; otherwise, False.
486        """
487        is_input_stable = self.chameleond_proxy.WaitVideoInputStable(
488                                self.port_id, timeout)
489
490        # If video input of Chameleon has been stable, wait for DUT software
491        # layer to be stable as well to make sure all the configurations have
492        # been propagated before proceeding.
493        if is_input_stable:
494            logging.info('Video input has been stable. Waiting for the DUT'
495                         ' to be stable...')
496            time.sleep(self._DUT_STABILIZE_TIME)
497        return is_input_stable
498
499
500    def read_edid(self):
501        """Reads the EDID.
502
503        @return: An Edid object or NO_EDID.
504        """
505        edid_binary = self.chameleond_proxy.ReadEdid(self.port_id)
506        if edid_binary is None:
507            return edid_lib.NO_EDID
508        # Read EDID without verify. It may be made corrupted as intended
509        # for the test purpose.
510        return edid_lib.Edid(edid_binary.data, skip_verify=True)
511
512
513    def apply_edid(self, edid):
514        """Applies the given EDID.
515
516        @param edid: An Edid object or NO_EDID.
517        """
518        if edid is edid_lib.NO_EDID:
519          self.chameleond_proxy.ApplyEdid(self.port_id, self._EDID_ID_DISABLE)
520        else:
521          edid_binary = xmlrpclib.Binary(edid.data)
522          edid_id = self.chameleond_proxy.CreateEdid(edid_binary)
523          self.chameleond_proxy.ApplyEdid(self.port_id, edid_id)
524          self.chameleond_proxy.DestroyEdid(edid_id)
525
526
527    def set_edid_from_file(self, filename):
528        """Sets EDID from a file.
529
530        The method is similar to set_edid but reads EDID from a file.
531
532        @param filename: path to EDID file.
533        """
534        self.set_edid(edid_lib.Edid.from_file(filename))
535
536
537    def set_edid(self, edid):
538        """The complete flow of setting EDID.
539
540        Unplugs the port if needed, sets EDID, plugs back if it was plugged.
541        The original EDID is stored so user can call restore_edid after this
542        call.
543
544        @param edid: An Edid object.
545        """
546        plugged = self.plugged
547        if plugged:
548            self.unplug()
549
550        self._original_edid = self.read_edid()
551
552        logging.info('Apply EDID on port %d', self.port_id)
553        self.apply_edid(edid)
554
555        if plugged:
556            time.sleep(self._DURATION_UNPLUG_FOR_EDID)
557            self.plug()
558            self.wait_video_input_stable(self._TIMEOUT_VIDEO_STABLE_PROBE)
559
560
561    def restore_edid(self):
562        """Restores original EDID stored when set_edid was called."""
563        current_edid = self.read_edid()
564        if (self._original_edid and
565            self._original_edid.data != current_edid.data):
566            logging.info('Restore the original EDID.')
567            self.apply_edid(self._original_edid)
568
569
570    @contextmanager
571    def use_edid(self, edid):
572        """Uses the given EDID in a with statement.
573
574        It sets the EDID up in the beginning and restores to the original
575        EDID in the end. This function is expected to be used in a with
576        statement, like the following:
577
578            with chameleon_port.use_edid(edid):
579                do_some_test_on(chameleon_port)
580
581        @param edid: An EDID object.
582        """
583        # Set the EDID up in the beginning.
584        self.set_edid(edid)
585
586        try:
587            # Yeild to execute the with statement.
588            yield
589        finally:
590            # Restore the original EDID in the end.
591            self.restore_edid()
592
593
594    def use_edid_file(self, filename):
595        """Uses the given EDID file in a with statement.
596
597        It sets the EDID up in the beginning and restores to the original
598        EDID in the end. This function is expected to be used in a with
599        statement, like the following:
600
601            with chameleon_port.use_edid_file(filename):
602                do_some_test_on(chameleon_port)
603
604        @param filename: A path to the EDID file.
605        """
606        return self.use_edid(edid_lib.Edid.from_file(filename))
607
608
609    def fire_hpd_pulse(self, deassert_interval_usec, assert_interval_usec=None,
610                       repeat_count=1, end_level=1):
611
612        """Fires one or more HPD pulse (low -> high -> low -> ...).
613
614        @param deassert_interval_usec: The time in microsecond of the
615                deassert pulse.
616        @param assert_interval_usec: The time in microsecond of the
617                assert pulse. If None, then use the same value as
618                deassert_interval_usec.
619        @param repeat_count: The count of HPD pulses to fire.
620        @param end_level: HPD ends with 0 for LOW (unplugged) or 1 for
621                HIGH (plugged).
622        """
623        self.chameleond_proxy.FireHpdPulse(
624                self.port_id, deassert_interval_usec,
625                assert_interval_usec, repeat_count, int(bool(end_level)))
626
627
628    def fire_mixed_hpd_pulses(self, widths):
629        """Fires one or more HPD pulses, starting at low, of mixed widths.
630
631        One must specify a list of segment widths in the widths argument where
632        widths[0] is the width of the first low segment, widths[1] is that of
633        the first high segment, widths[2] is that of the second low segment...
634        etc. The HPD line stops at low if even number of segment widths are
635        specified; otherwise, it stops at high.
636
637        @param widths: list of pulse segment widths in usec.
638        """
639        self.chameleond_proxy.FireMixedHpdPulses(self.port_id, widths)
640
641
642    def capture_screen(self):
643        """Captures Chameleon framebuffer.
644
645        @return An Image object.
646        """
647        return Image.fromstring(
648                'RGB',
649                self.get_resolution(),
650                self.chameleond_proxy.DumpPixels(self.port_id).data)
651
652
653    def get_resolution(self):
654        """Gets the source resolution.
655
656        @return: A (width, height) tuple.
657        """
658        # The return value of RPC is converted to a list. Convert it back to
659        # a tuple.
660        return tuple(self.chameleond_proxy.DetectResolution(self.port_id))
661
662
663    def set_content_protection(self, enable):
664        """Sets the content protection state on the port.
665
666        @param enable: True to enable; False to disable.
667        """
668        self.chameleond_proxy.SetContentProtection(self.port_id, enable)
669
670
671    def is_content_protection_enabled(self):
672        """Returns True if the content protection is enabled on the port.
673
674        @return: True if the content protection is enabled; otherwise, False.
675        """
676        return self.chameleond_proxy.IsContentProtectionEnabled(self.port_id)
677
678
679    def is_video_input_encrypted(self):
680        """Returns True if the video input on the port is encrypted.
681
682        @return: True if the video input is encrypted; otherwise, False.
683        """
684        return self.chameleond_proxy.IsVideoInputEncrypted(self.port_id)
685
686
687    def start_monitoring_audio_video_capturing_delay(self):
688        """Starts an audio/video synchronization utility."""
689        self.chameleond_proxy.StartMonitoringAudioVideoCapturingDelay()
690
691
692    def get_audio_video_capturing_delay(self):
693        """Gets the time interval between the first audio/video cpatured data.
694
695        @return: A floating points indicating the time interval between the
696                 first audio/video data captured. If the result is negative,
697                 then the first video data is earlier, otherwise the first
698                 audio data is earlier.
699        """
700        return self.chameleond_proxy.GetAudioVideoCapturingDelay()
701
702
703    def start_capturing_video(self, box=None):
704        """
705        Captures video frames. Asynchronous, returns immediately.
706
707        @param box: int tuple, (x, y, width, height) pixel coordinates.
708                    Defines the rectangular boundary within which to capture.
709        """
710
711        if box is None:
712            self.chameleond_proxy.StartCapturingVideo(self.port_id)
713        else:
714            self.chameleond_proxy.StartCapturingVideo(self.port_id, *box)
715
716
717    def stop_capturing_video(self):
718        """
719        Stops the ongoing video frame capturing.
720
721        """
722        self.chameleond_proxy.StopCapturingVideo()
723
724
725    def get_captured_frame_count(self):
726        """
727        @return: int, the number of frames that have been captured.
728
729        """
730        return self.chameleond_proxy.GetCapturedFrameCount()
731
732
733    def read_captured_frame(self, index):
734        """
735        @param index: int, index of the desired captured frame.
736        @return: xmlrpclib.Binary object containing a byte-array of the pixels.
737
738        """
739
740        frame = self.chameleond_proxy.ReadCapturedFrame(index)
741        return Image.fromstring('RGB',
742                                self.get_captured_resolution(),
743                                frame.data)
744
745
746    def get_captured_checksums(self, start_index=0, stop_index=None):
747        """
748        @param start_index: int, index of the frame to start with.
749        @param stop_index: int, index of the frame (excluded) to stop at.
750        @return: a list of checksums of frames captured.
751
752        """
753        return self.chameleond_proxy.GetCapturedChecksums(start_index,
754                                                          stop_index)
755
756
757    def get_captured_fps_list(self, time_to_start=0, total_period=None):
758        """
759        @param time_to_start: time in second, support floating number, only
760                              measure the period starting at this time.
761                              If negative, it is the time before stop, e.g.
762                              -2 meaning 2 seconds before stop.
763        @param total_period: time in second, integer, the total measuring
764                             period. If not given, use the maximum time
765                             (integer) to the end.
766        @return: a list of fps numbers, or [-1] if any error.
767
768        """
769        checksums = self.get_captured_checksums()
770
771        frame_to_start = int(round(time_to_start * self._FRAME_RATE))
772        if total_period is None:
773            # The default is the maximum time (integer) to the end.
774            total_period = (len(checksums) - frame_to_start) / self._FRAME_RATE
775        frame_to_stop = frame_to_start + total_period * self._FRAME_RATE
776
777        if frame_to_start >= len(checksums) or frame_to_stop >= len(checksums):
778            logging.error('The given time interval is out-of-range.')
779            return [-1]
780
781        # Only pick the checksum we are interested.
782        checksums = checksums[frame_to_start:frame_to_stop]
783
784        # Count the unique checksums per second, i.e. FPS
785        logging.debug('Output the fps info below:')
786        fps_list = []
787        for i in xrange(0, len(checksums), self._FRAME_RATE):
788            unique_count = 0
789            debug_str = ''
790            for j in xrange(i, i + self._FRAME_RATE):
791                if j == 0 or checksums[j] != checksums[j - 1]:
792                    unique_count += 1
793                    debug_str += '*'
794                else:
795                    debug_str += '.'
796            fps_list.append(unique_count)
797            logging.debug('%2dfps %s', unique_count, debug_str)
798
799        return fps_list
800
801
802    def search_fps_pattern(self, pattern_diff_frame, pattern_window=None,
803                           time_to_start=0):
804        """Search the captured frames and return the time where FPS is greater
805        than given FPS pattern.
806
807        A FPS pattern is described as how many different frames in a sliding
808        window. For example, 5 differnt frames in a window of 60 frames.
809
810        @param pattern_diff_frame: number of different frames for the pattern.
811        @param pattern_window: number of frames for the sliding window. Default
812                               is 1 second.
813        @param time_to_start: time in second, support floating number,
814                              start to search from the given time.
815        @return: the time matching the pattern. -1.0 if not found.
816
817        """
818        if pattern_window is None:
819            pattern_window = self._FRAME_RATE
820
821        checksums = self.get_captured_checksums()
822
823        frame_to_start = int(round(time_to_start * self._FRAME_RATE))
824        first_checksum = checksums[frame_to_start]
825
826        for i in xrange(frame_to_start + 1, len(checksums) - pattern_window):
827            unique_count = 0
828            for j in xrange(i, i + pattern_window):
829                if j == 0 or checksums[j] != checksums[j - 1]:
830                    unique_count += 1
831            if unique_count >= pattern_diff_frame:
832                return float(i) / self._FRAME_RATE
833
834        return -1.0
835
836
837    def get_captured_resolution(self):
838        """
839        @return: (width, height) tuple, the resolution of captured frames.
840
841        """
842        return self.chameleond_proxy.GetCapturedResolution()
843
844
845
846class ChameleonAudioInput(ChameleonPort):
847    """ChameleonAudioInput is an abstraction of an audio input port.
848
849    It contains some special methods to control an audio input.
850    """
851
852    def __init__(self, chameleon_port):
853        """Construct a ChameleonAudioInput.
854
855        @param chameleon_port: A general ChameleonPort object.
856        """
857        self.chameleond_proxy = chameleon_port.chameleond_proxy
858        self.port_id = chameleon_port.port_id
859
860
861    def start_capturing_audio(self):
862        """Starts capturing audio."""
863        return self.chameleond_proxy.StartCapturingAudio(self.port_id)
864
865
866    def stop_capturing_audio(self):
867        """Stops capturing audio.
868
869        Returns:
870          A tuple (remote_path, format).
871          remote_path: The captured file path on Chameleon.
872          format: A dict containing:
873            file_type: 'raw' or 'wav'.
874            sample_format: 'S32_LE' for 32-bit signed integer in little-endian.
875              Refer to aplay manpage for other formats.
876            channel: channel number.
877            rate: sampling rate.
878        """
879        remote_path, data_format = self.chameleond_proxy.StopCapturingAudio(
880                self.port_id)
881        return remote_path, data_format
882
883
884class ChameleonAudioOutput(ChameleonPort):
885    """ChameleonAudioOutput is an abstraction of an audio output port.
886
887    It contains some special methods to control an audio output.
888    """
889
890    def __init__(self, chameleon_port):
891        """Construct a ChameleonAudioOutput.
892
893        @param chameleon_port: A general ChameleonPort object.
894        """
895        self.chameleond_proxy = chameleon_port.chameleond_proxy
896        self.port_id = chameleon_port.port_id
897
898
899    def start_playing_audio(self, path, data_format):
900        """Starts playing audio.
901
902        @param path: The path to the file to play on Chameleon.
903        @param data_format: A dict containing data format. Currently Chameleon
904                            only accepts data format:
905                            dict(file_type='raw', sample_format='S32_LE',
906                                 channel=8, rate=48000).
907
908        """
909        self.chameleond_proxy.StartPlayingAudio(self.port_id, path, data_format)
910
911
912    def stop_playing_audio(self):
913        """Stops capturing audio."""
914        self.chameleond_proxy.StopPlayingAudio(self.port_id)
915
916
917def make_chameleon_hostname(dut_hostname):
918    """Given a DUT's hostname, returns the hostname of its Chameleon.
919
920    @param dut_hostname: Hostname of a DUT.
921
922    @return Hostname of the DUT's Chameleon.
923    """
924    host_parts = dut_hostname.split('.')
925    host_parts[0] = host_parts[0] + '-chameleon'
926    return '.'.join(host_parts)
927
928
929def create_chameleon_board(dut_hostname, args):
930    """Given either DUT's hostname or argments, creates a ChameleonBoard object.
931
932    If the DUT's hostname is in the lab zone, it connects to the Chameleon by
933    append the hostname with '-chameleon' suffix. If not, checks if the args
934    contains the key-value pair 'chameleon_host=IP'.
935
936    @param dut_hostname: Hostname of a DUT.
937    @param args: A string of arguments passed from the command line.
938
939    @return A ChameleonBoard object.
940
941    @raise ChameleonConnectionError if unknown hostname.
942    """
943    connection = None
944    hostname = make_chameleon_hostname(dut_hostname)
945    if utils.host_is_in_lab_zone(hostname):
946        connection = ChameleonConnection(hostname)
947    else:
948        args_dict = utils.args_to_dict(args)
949        hostname = args_dict.get('chameleon_host', None)
950        port = args_dict.get('chameleon_port', CHAMELEON_PORT)
951        if hostname:
952            connection = ChameleonConnection(hostname, port)
953        else:
954            raise ChameleonConnectionError('No chameleon_host is given in args')
955
956    return ChameleonBoard(connection)
957