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