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