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