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