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 httplib
6import logging
7import socket
8import time
9import xmlrpclib
10from contextlib import contextmanager
11
12from PIL import Image
13
14from autotest_lib.client.bin import utils
15from autotest_lib.client.common_lib import error
16from autotest_lib.client.cros.chameleon import audio_board
17from autotest_lib.client.cros.chameleon import edid as edid_lib
18from autotest_lib.client.cros.chameleon import usb_controller
19
20
21CHAMELEON_PORT = 9992
22
23
24class ChameleonConnectionError(error.TestError):
25    """Indicates that connecting to Chameleon failed.
26
27    It is fatal to the test unless caught.
28    """
29    pass
30
31
32class ChameleonConnection(object):
33    """ChameleonConnection abstracts the network connection to the board.
34
35    ChameleonBoard and ChameleonPort use it for accessing Chameleon RPC.
36
37    """
38
39    def __init__(self, hostname, port=CHAMELEON_PORT):
40        """Constructs a ChameleonConnection.
41
42        @param hostname: Hostname the chameleond process is running.
43        @param port: Port number the chameleond process is listening on.
44
45        @raise ChameleonConnectionError if connection failed.
46        """
47        self.chameleond_proxy = ChameleonConnection._create_server_proxy(
48                hostname, port)
49
50
51    @staticmethod
52    def _create_server_proxy(hostname, port):
53        """Creates the chameleond server proxy.
54
55        @param hostname: Hostname the chameleond process is running.
56        @param port: Port number the chameleond process is listening on.
57
58        @return ServerProxy object to chameleond.
59
60        @raise ChameleonConnectionError if connection failed.
61        """
62        remote = 'http://%s:%s' % (hostname, port)
63        chameleond_proxy = xmlrpclib.ServerProxy(remote, allow_none=True)
64        # Call a RPC to test.
65        try:
66            chameleond_proxy.GetSupportedPorts()
67        except (socket.error,
68                xmlrpclib.ProtocolError,
69                httplib.BadStatusLine) as e:
70            raise ChameleonConnectionError(e)
71        return chameleond_proxy
72
73
74class ChameleonBoard(object):
75    """ChameleonBoard is an abstraction of a Chameleon board.
76
77    A Chameleond RPC proxy is passed to the construction such that it can
78    use this proxy to control the Chameleon board.
79
80    User can use host to access utilities that are not provided by
81    Chameleond XMLRPC server, e.g. send_file and get_file, which are provided by
82    ssh_host.SSHHost, which is the base class of ChameleonHost.
83
84    """
85
86    def __init__(self, chameleon_connection, chameleon_host=None):
87        """Construct a ChameleonBoard.
88
89        @param chameleon_connection: ChameleonConnection object.
90        @param chameleon_host: ChameleonHost object. None if this ChameleonBoard
91                               is not created by a ChameleonHost.
92        """
93        self.host = chameleon_host
94        self._chameleond_proxy = chameleon_connection.chameleond_proxy
95        self._usb_ctrl = usb_controller.USBController(chameleon_connection)
96        if self._chameleond_proxy.HasAudioBoard():
97            self._audio_board = audio_board.AudioBoard(chameleon_connection)
98        else:
99            self._audio_board = None
100            logging.info('There is no audio board on this Chameleon.')
101
102    def reset(self):
103        """Resets Chameleon board."""
104        self._chameleond_proxy.Reset()
105
106
107    def get_all_ports(self):
108        """Gets all the ports on Chameleon board which are connected.
109
110        @return: A list of ChameleonPort objects.
111        """
112        ports = self._chameleond_proxy.ProbePorts()
113        return [ChameleonPort(self._chameleond_proxy, port) for port in ports]
114
115
116    def get_all_inputs(self):
117        """Gets all the input ports on Chameleon board which are connected.
118
119        @return: A list of ChameleonPort objects.
120        """
121        ports = self._chameleond_proxy.ProbeInputs()
122        return [ChameleonPort(self._chameleond_proxy, port) for port in ports]
123
124
125    def get_all_outputs(self):
126        """Gets all the output ports on Chameleon board which are connected.
127
128        @return: A list of ChameleonPort objects.
129        """
130        ports = self._chameleond_proxy.ProbeOutputs()
131        return [ChameleonPort(self._chameleond_proxy, port) for port in ports]
132
133
134    def get_label(self):
135        """Gets the label which indicates the display connection.
136
137        @return: A string of the label, like 'hdmi', 'dp_hdmi', etc.
138        """
139        connectors = []
140        for port in self._chameleond_proxy.ProbeInputs():
141            if self._chameleond_proxy.HasVideoSupport(port):
142                connector = self._chameleond_proxy.GetConnectorType(port).lower()
143                connectors.append(connector)
144        # Eliminate duplicated ports. It simplifies the labels of dual-port
145        # devices, i.e. dp_dp categorized into dp.
146        return '_'.join(sorted(set(connectors)))
147
148
149    def get_audio_board(self):
150        """Gets the audio board on Chameleon.
151
152        @return: An AudioBoard object.
153        """
154        return self._audio_board
155
156
157    def get_usb_controller(self):
158        """Gets the USB controller on Chameleon.
159
160        @return: A USBController object.
161        """
162        return self._usb_ctrl
163
164
165    def get_mac_address(self):
166        """Gets the MAC address of Chameleon.
167
168        @return: A string for MAC address.
169        """
170        return self._chameleond_proxy.GetMacAddress()
171
172
173class ChameleonPort(object):
174    """ChameleonPort is an abstraction of a general port of a Chameleon board.
175
176    It only contains some common methods shared with audio and video ports.
177
178    A Chameleond RPC proxy and an port_id are passed to the construction.
179    The port_id is the unique identity to the port.
180    """
181
182    def __init__(self, chameleond_proxy, port_id):
183        """Construct a ChameleonPort.
184
185        @param chameleond_proxy: Chameleond RPC proxy object.
186        @param port_id: The ID of the input port.
187        """
188        self.chameleond_proxy = chameleond_proxy
189        self.port_id = port_id
190
191
192    def get_connector_id(self):
193        """Returns the connector ID.
194
195        @return: A number of connector ID.
196        """
197        return self.port_id
198
199
200    def get_connector_type(self):
201        """Returns the human readable string for the connector type.
202
203        @return: A string, like "VGA", "DVI", "HDMI", or "DP".
204        """
205        return self.chameleond_proxy.GetConnectorType(self.port_id)
206
207
208    def has_audio_support(self):
209        """Returns if the input has audio support.
210
211        @return: True if the input has audio support; otherwise, False.
212        """
213        return self.chameleond_proxy.HasAudioSupport(self.port_id)
214
215
216    def has_video_support(self):
217        """Returns if the input has video support.
218
219        @return: True if the input has video support; otherwise, False.
220        """
221        return self.chameleond_proxy.HasVideoSupport(self.port_id)
222
223
224    def plug(self):
225        """Asserts HPD line to high, emulating plug."""
226        logging.info('Plug Chameleon port %d', self.port_id)
227        self.chameleond_proxy.Plug(self.port_id)
228
229
230    def unplug(self):
231        """Deasserts HPD line to low, emulating unplug."""
232        logging.info('Unplug Chameleon port %d', self.port_id)
233        self.chameleond_proxy.Unplug(self.port_id)
234
235
236    def set_plug(self, plug_status):
237        """Sets plug/unplug by plug_status.
238
239        @param plug_status: True to plug; False to unplug.
240        """
241        if plug_status:
242            self.plug()
243        else:
244            self.unplug()
245
246
247    @property
248    def plugged(self):
249        """
250        @returns True if this port is plugged to Chameleon, False otherwise.
251
252        """
253        return self.chameleond_proxy.IsPlugged(self.port_id)
254
255
256class ChameleonVideoInput(ChameleonPort):
257    """ChameleonVideoInput is an abstraction of a video input port.
258
259    It contains some special methods to control a video input.
260    """
261
262    _DUT_STABILIZE_TIME = 3
263    _DURATION_UNPLUG_FOR_EDID = 5
264    _TIMEOUT_VIDEO_STABLE_PROBE = 10
265    _EDID_ID_DISABLE = -1
266
267    def __init__(self, chameleon_port):
268        """Construct a ChameleonVideoInput.
269
270        @param chameleon_port: A general ChameleonPort object.
271        """
272        self.chameleond_proxy = chameleon_port.chameleond_proxy
273        self.port_id = chameleon_port.port_id
274
275
276    def wait_video_input_stable(self, timeout=None):
277        """Waits the video input stable or timeout.
278
279        @param timeout: The time period to wait for.
280
281        @return: True if the video input becomes stable within the timeout
282                 period; otherwise, False.
283        """
284        is_input_stable = self.chameleond_proxy.WaitVideoInputStable(
285                                self.port_id, timeout)
286
287        # If video input of Chameleon has been stable, wait for DUT software
288        # layer to be stable as well to make sure all the configurations have
289        # been propagated before proceeding.
290        if is_input_stable:
291            logging.info('Video input has been stable. Waiting for the DUT'
292                         ' to be stable...')
293            time.sleep(self._DUT_STABILIZE_TIME)
294        return is_input_stable
295
296
297    def read_edid(self):
298        """Reads the EDID.
299
300        @return: An Edid object or NO_EDID.
301        """
302        edid_binary = self.chameleond_proxy.ReadEdid(self.port_id)
303        if edid_binary is None:
304            return edid_lib.NO_EDID
305        # Read EDID without verify. It may be made corrupted as intended
306        # for the test purpose.
307        return edid_lib.Edid(edid_binary.data, skip_verify=True)
308
309
310    def apply_edid(self, edid):
311        """Applies the given EDID.
312
313        @param edid: An Edid object or NO_EDID.
314        """
315        if edid is edid_lib.NO_EDID:
316          self.chameleond_proxy.ApplyEdid(self.port_id, self._EDID_ID_DISABLE)
317        else:
318          edid_binary = xmlrpclib.Binary(edid.data)
319          edid_id = self.chameleond_proxy.CreateEdid(edid_binary)
320          self.chameleond_proxy.ApplyEdid(self.port_id, edid_id)
321          self.chameleond_proxy.DestroyEdid(edid_id)
322
323
324    @contextmanager
325    def use_edid(self, edid):
326        """Uses the given EDID in a with statement.
327
328        It sets the EDID up in the beginning and restores to the original
329        EDID in the end. This function is expected to be used in a with
330        statement, like the following:
331
332            with chameleon_port.use_edid(edid):
333                do_some_test_on(chameleon_port)
334
335        @param edid: An EDID object.
336        """
337        # Set the EDID up in the beginning.
338        plugged = self.plugged
339        if plugged:
340            self.unplug()
341
342        original_edid = self.read_edid()
343        logging.info('Apply EDID on port %d', self.port_id)
344        self.apply_edid(edid)
345
346        if plugged:
347            time.sleep(self._DURATION_UNPLUG_FOR_EDID)
348            self.plug()
349            self.wait_video_input_stable(self._TIMEOUT_VIDEO_STABLE_PROBE)
350
351        try:
352            # Yeild to execute the with statement.
353            yield
354        finally:
355            # Restore the original EDID in the end.
356            current_edid = self.read_edid()
357            if original_edid.data != current_edid.data:
358                logging.info('Restore the original EDID.')
359                self.apply_edid(original_edid)
360
361
362    def use_edid_file(self, filename):
363        """Uses the given EDID file in a with statement.
364
365        It sets the EDID up in the beginning and restores to the original
366        EDID in the end. This function is expected to be used in a with
367        statement, like the following:
368
369            with chameleon_port.use_edid_file(filename):
370                do_some_test_on(chameleon_port)
371
372        @param filename: A path to the EDID file.
373        """
374        return self.use_edid(edid_lib.Edid.from_file(filename))
375
376
377    def fire_hpd_pulse(self, deassert_interval_usec, assert_interval_usec=None,
378                       repeat_count=1, end_level=1):
379
380        """Fires one or more HPD pulse (low -> high -> low -> ...).
381
382        @param deassert_interval_usec: The time in microsecond of the
383                deassert pulse.
384        @param assert_interval_usec: The time in microsecond of the
385                assert pulse. If None, then use the same value as
386                deassert_interval_usec.
387        @param repeat_count: The count of HPD pulses to fire.
388        @param end_level: HPD ends with 0 for LOW (unplugged) or 1 for
389                HIGH (plugged).
390        """
391        self.chameleond_proxy.FireHpdPulse(
392                self.port_id, deassert_interval_usec,
393                assert_interval_usec, repeat_count, int(bool(end_level)))
394
395
396    def fire_mixed_hpd_pulses(self, widths):
397        """Fires one or more HPD pulses, starting at low, of mixed widths.
398
399        One must specify a list of segment widths in the widths argument where
400        widths[0] is the width of the first low segment, widths[1] is that of
401        the first high segment, widths[2] is that of the second low segment...
402        etc. The HPD line stops at low if even number of segment widths are
403        specified; otherwise, it stops at high.
404
405        @param widths: list of pulse segment widths in usec.
406        """
407        self.chameleond_proxy.FireMixedHpdPulses(self.port_id, widths)
408
409
410    def capture_screen(self):
411        """Captures Chameleon framebuffer.
412
413        @return An Image object.
414        """
415        return Image.fromstring(
416                'RGB',
417                self.get_resolution(),
418                self.chameleond_proxy.DumpPixels(self.port_id).data)
419
420
421    def get_resolution(self):
422        """Gets the source resolution.
423
424        @return: A (width, height) tuple.
425        """
426        # The return value of RPC is converted to a list. Convert it back to
427        # a tuple.
428        return tuple(self.chameleond_proxy.DetectResolution(self.port_id))
429
430
431    def set_content_protection(self, enable):
432        """Sets the content protection state on the port.
433
434        @param enable: True to enable; False to disable.
435        """
436        self.chameleond_proxy.SetContentProtection(self.port_id, enable)
437
438
439    def is_content_protection_enabled(self):
440        """Returns True if the content protection is enabled on the port.
441
442        @return: True if the content protection is enabled; otherwise, False.
443        """
444        return self.chameleond_proxy.IsContentProtectionEnabled(self.port_id)
445
446
447    def is_video_input_encrypted(self):
448        """Returns True if the video input on the port is encrypted.
449
450        @return: True if the video input is encrypted; otherwise, False.
451        """
452        return self.chameleond_proxy.IsVideoInputEncrypted(self.port_id)
453
454
455    def start_capturing_video(self, box=None):
456        """
457        Captures video frames. Asynchronous, returns immediately.
458
459        @param box: int tuple, left, upper, right, lower pixel coordinates.
460                    Defines the rectangular boundary within which to capture.
461        """
462
463        if box is None:
464            self.chameleond_proxy.StartCapturingVideo(self.port_id)
465        else:
466            self.chameleond_proxy.StartCapturingVideo(self.port_id, *box)
467
468
469    def stop_capturing_video(self):
470        """
471        Stops the ongoing video frame capturing.
472
473        """
474        self.chameleond_proxy.StopCapturingVideo(self.port_id)
475
476
477    def get_captured_frame_count(self):
478        """
479        @return: int, the number of frames that have been captured.
480
481        """
482        return self.chameleond_proxy.GetCapturedFrameCount()
483
484
485    def read_captured_frame(self, index):
486        """
487        @param index: int, index of the desired captured frame.
488        @return: xmlrpclib.Binary object containing a byte-array of the pixels.
489
490        """
491
492        frame = self.chameleond_proxy.ReadCapturedFrame(index)
493        return Image.fromstring('RGB',
494                                self.get_captured_resolution(),
495                                frame.data)
496
497
498    def get_captured_checksums(self, start_index=0, stop_index=None):
499        """
500        @param start_index: int, index of the frame to start with.
501        @param stop_index: int, index of the frame (excluded) to stop at.
502        @return: a list of checksums of frames captured.
503
504        """
505        return self.chameleond_proxy.GetCapturedChecksums(start_index,
506                                                          stop_index)
507
508
509    def get_captured_resolution(self):
510        """
511        @return: (width, height) tuple, the resolution of captured frames.
512
513        """
514        return self.chameleond_proxy.GetCapturedResolution()
515
516
517
518class ChameleonAudioInput(ChameleonPort):
519    """ChameleonAudioInput is an abstraction of an audio input port.
520
521    It contains some special methods to control an audio input.
522    """
523
524    def __init__(self, chameleon_port):
525        """Construct a ChameleonAudioInput.
526
527        @param chameleon_port: A general ChameleonPort object.
528        """
529        self.chameleond_proxy = chameleon_port.chameleond_proxy
530        self.port_id = chameleon_port.port_id
531
532
533    def start_capturing_audio(self):
534        """Starts capturing audio."""
535        return self.chameleond_proxy.StartCapturingAudio(self.port_id)
536
537
538    def stop_capturing_audio(self):
539        """Stops capturing audio.
540
541        Returns:
542          A tuple (remote_path, format).
543          remote_path: The captured file path on Chameleon.
544          format: A dict containing:
545            file_type: 'raw' or 'wav'.
546            sample_format: 'S32_LE' for 32-bit signed integer in little-endian.
547              Refer to aplay manpage for other formats.
548            channel: channel number.
549            rate: sampling rate.
550        """
551        remote_path, data_format = self.chameleond_proxy.StopCapturingAudio(
552                self.port_id)
553        return remote_path, data_format
554
555
556class ChameleonAudioOutput(ChameleonPort):
557    """ChameleonAudioOutput is an abstraction of an audio output port.
558
559    It contains some special methods to control an audio output.
560    """
561
562    def __init__(self, chameleon_port):
563        """Construct a ChameleonAudioOutput.
564
565        @param chameleon_port: A general ChameleonPort object.
566        """
567        self.chameleond_proxy = chameleon_port.chameleond_proxy
568        self.port_id = chameleon_port.port_id
569
570
571    def start_playing_audio(self, path, data_format):
572        """Starts playing audio.
573
574        @param path: The path to the file to play on Chameleon.
575        @param data_format: A dict containing data format. Currently Chameleon
576                            only accepts data format:
577                            dict(file_type='raw', sample_format='S32_LE',
578                                 channel=8, rate=48000).
579
580        """
581        self.chameleond_proxy.StartPlayingAudio(self.port_id, path, data_format)
582
583
584    def stop_playing_audio(self):
585        """Stops capturing audio."""
586        self.chameleond_proxy.StopPlayingAudio(self.port_id)
587
588
589def make_chameleon_hostname(dut_hostname):
590    """Given a DUT's hostname, returns the hostname of its Chameleon.
591
592    @param dut_hostname: Hostname of a DUT.
593
594    @return Hostname of the DUT's Chameleon.
595    """
596    host_parts = dut_hostname.split('.')
597    host_parts[0] = host_parts[0] + '-chameleon'
598    return '.'.join(host_parts)
599
600
601def create_chameleon_board(dut_hostname, args):
602    """Given either DUT's hostname or argments, creates a ChameleonBoard object.
603
604    If the DUT's hostname is in the lab zone, it connects to the Chameleon by
605    append the hostname with '-chameleon' suffix. If not, checks if the args
606    contains the key-value pair 'chameleon_host=IP'.
607
608    @param dut_hostname: Hostname of a DUT.
609    @param args: A string of arguments passed from the command line.
610
611    @return A ChameleonBoard object.
612
613    @raise ChameleonConnectionError if unknown hostname.
614    """
615    connection = None
616    hostname = make_chameleon_hostname(dut_hostname)
617    if utils.host_is_in_lab_zone(hostname):
618        connection = ChameleonConnection(hostname)
619    else:
620        args_dict = utils.args_to_dict(args)
621        hostname = args_dict.get('chameleon_host', None)
622        port = args_dict.get('chameleon_port', CHAMELEON_PORT)
623        if hostname:
624            connection = ChameleonConnection(hostname, port)
625        else:
626            raise ChameleonConnectionError('No chameleon_host is given in args')
627
628    return ChameleonBoard(connection)
629