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