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