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 httplib 6import logging 7import socket 8import time 9import xmlrpclib 10from contextlib import contextmanager 11 12from PIL import Image 13 14from autotest_lib.client.bin import utils 15from autotest_lib.client.common_lib import error 16from autotest_lib.client.cros.chameleon import audio_board 17from autotest_lib.client.cros.chameleon import edid as edid_lib 18from autotest_lib.client.cros.chameleon import usb_controller 19 20 21CHAMELEON_PORT = 9992 22 23 24class ChameleonConnectionError(error.TestError): 25 """Indicates that connecting to Chameleon failed. 26 27 It is fatal to the test unless caught. 28 """ 29 pass 30 31 32class ChameleonConnection(object): 33 """ChameleonConnection abstracts the network connection to the board. 34 35 ChameleonBoard and ChameleonPort use it for accessing Chameleon RPC. 36 37 """ 38 39 def __init__(self, hostname, port=CHAMELEON_PORT): 40 """Constructs a ChameleonConnection. 41 42 @param hostname: Hostname the chameleond process is running. 43 @param port: Port number the chameleond process is listening on. 44 45 @raise ChameleonConnectionError if connection failed. 46 """ 47 self.chameleond_proxy = ChameleonConnection._create_server_proxy( 48 hostname, port) 49 50 51 @staticmethod 52 def _create_server_proxy(hostname, port): 53 """Creates the chameleond server proxy. 54 55 @param hostname: Hostname the chameleond process is running. 56 @param port: Port number the chameleond process is listening on. 57 58 @return ServerProxy object to chameleond. 59 60 @raise ChameleonConnectionError if connection failed. 61 """ 62 remote = 'http://%s:%s' % (hostname, port) 63 chameleond_proxy = xmlrpclib.ServerProxy(remote, allow_none=True) 64 # Call a RPC to test. 65 try: 66 chameleond_proxy.GetSupportedPorts() 67 except (socket.error, 68 xmlrpclib.ProtocolError, 69 httplib.BadStatusLine) as e: 70 raise ChameleonConnectionError(e) 71 return chameleond_proxy 72 73 74class ChameleonBoard(object): 75 """ChameleonBoard is an abstraction of a Chameleon board. 76 77 A Chameleond RPC proxy is passed to the construction such that it can 78 use this proxy to control the Chameleon board. 79 80 User can use host to access utilities that are not provided by 81 Chameleond XMLRPC server, e.g. send_file and get_file, which are provided by 82 ssh_host.SSHHost, which is the base class of ChameleonHost. 83 84 """ 85 86 def __init__(self, chameleon_connection, chameleon_host=None): 87 """Construct a ChameleonBoard. 88 89 @param chameleon_connection: ChameleonConnection object. 90 @param chameleon_host: ChameleonHost object. None if this ChameleonBoard 91 is not created by a ChameleonHost. 92 """ 93 self.host = chameleon_host 94 self._chameleond_proxy = chameleon_connection.chameleond_proxy 95 self._usb_ctrl = usb_controller.USBController(chameleon_connection) 96 if self._chameleond_proxy.HasAudioBoard(): 97 self._audio_board = audio_board.AudioBoard(chameleon_connection) 98 else: 99 self._audio_board = None 100 logging.info('There is no audio board on this Chameleon.') 101 102 def reset(self): 103 """Resets Chameleon board.""" 104 self._chameleond_proxy.Reset() 105 106 107 def get_all_ports(self): 108 """Gets all the ports on Chameleon board which are connected. 109 110 @return: A list of ChameleonPort objects. 111 """ 112 ports = self._chameleond_proxy.ProbePorts() 113 return [ChameleonPort(self._chameleond_proxy, port) for port in ports] 114 115 116 def get_all_inputs(self): 117 """Gets all the input ports on Chameleon board which are connected. 118 119 @return: A list of ChameleonPort objects. 120 """ 121 ports = self._chameleond_proxy.ProbeInputs() 122 return [ChameleonPort(self._chameleond_proxy, port) for port in ports] 123 124 125 def get_all_outputs(self): 126 """Gets all the output ports on Chameleon board which are connected. 127 128 @return: A list of ChameleonPort objects. 129 """ 130 ports = self._chameleond_proxy.ProbeOutputs() 131 return [ChameleonPort(self._chameleond_proxy, port) for port in ports] 132 133 134 def get_label(self): 135 """Gets the label which indicates the display connection. 136 137 @return: A string of the label, like 'hdmi', 'dp_hdmi', etc. 138 """ 139 connectors = [] 140 for port in self._chameleond_proxy.ProbeInputs(): 141 if self._chameleond_proxy.HasVideoSupport(port): 142 connector = self._chameleond_proxy.GetConnectorType(port).lower() 143 connectors.append(connector) 144 # Eliminate duplicated ports. It simplifies the labels of dual-port 145 # devices, i.e. dp_dp categorized into dp. 146 return '_'.join(sorted(set(connectors))) 147 148 149 def get_audio_board(self): 150 """Gets the audio board on Chameleon. 151 152 @return: An AudioBoard object. 153 """ 154 return self._audio_board 155 156 157 def get_usb_controller(self): 158 """Gets the USB controller on Chameleon. 159 160 @return: A USBController object. 161 """ 162 return self._usb_ctrl 163 164 165 def get_mac_address(self): 166 """Gets the MAC address of Chameleon. 167 168 @return: A string for MAC address. 169 """ 170 return self._chameleond_proxy.GetMacAddress() 171 172 173class ChameleonPort(object): 174 """ChameleonPort is an abstraction of a general port of a Chameleon board. 175 176 It only contains some common methods shared with audio and video ports. 177 178 A Chameleond RPC proxy and an port_id are passed to the construction. 179 The port_id is the unique identity to the port. 180 """ 181 182 def __init__(self, chameleond_proxy, port_id): 183 """Construct a ChameleonPort. 184 185 @param chameleond_proxy: Chameleond RPC proxy object. 186 @param port_id: The ID of the input port. 187 """ 188 self.chameleond_proxy = chameleond_proxy 189 self.port_id = port_id 190 191 192 def get_connector_id(self): 193 """Returns the connector ID. 194 195 @return: A number of connector ID. 196 """ 197 return self.port_id 198 199 200 def get_connector_type(self): 201 """Returns the human readable string for the connector type. 202 203 @return: A string, like "VGA", "DVI", "HDMI", or "DP". 204 """ 205 return self.chameleond_proxy.GetConnectorType(self.port_id) 206 207 208 def has_audio_support(self): 209 """Returns if the input has audio support. 210 211 @return: True if the input has audio support; otherwise, False. 212 """ 213 return self.chameleond_proxy.HasAudioSupport(self.port_id) 214 215 216 def has_video_support(self): 217 """Returns if the input has video support. 218 219 @return: True if the input has video support; otherwise, False. 220 """ 221 return self.chameleond_proxy.HasVideoSupport(self.port_id) 222 223 224 def plug(self): 225 """Asserts HPD line to high, emulating plug.""" 226 logging.info('Plug Chameleon port %d', self.port_id) 227 self.chameleond_proxy.Plug(self.port_id) 228 229 230 def unplug(self): 231 """Deasserts HPD line to low, emulating unplug.""" 232 logging.info('Unplug Chameleon port %d', self.port_id) 233 self.chameleond_proxy.Unplug(self.port_id) 234 235 236 def set_plug(self, plug_status): 237 """Sets plug/unplug by plug_status. 238 239 @param plug_status: True to plug; False to unplug. 240 """ 241 if plug_status: 242 self.plug() 243 else: 244 self.unplug() 245 246 247 @property 248 def plugged(self): 249 """ 250 @returns True if this port is plugged to Chameleon, False otherwise. 251 252 """ 253 return self.chameleond_proxy.IsPlugged(self.port_id) 254 255 256class ChameleonVideoInput(ChameleonPort): 257 """ChameleonVideoInput is an abstraction of a video input port. 258 259 It contains some special methods to control a video input. 260 """ 261 262 _DUT_STABILIZE_TIME = 3 263 _DURATION_UNPLUG_FOR_EDID = 5 264 _TIMEOUT_VIDEO_STABLE_PROBE = 10 265 _EDID_ID_DISABLE = -1 266 267 def __init__(self, chameleon_port): 268 """Construct a ChameleonVideoInput. 269 270 @param chameleon_port: A general ChameleonPort object. 271 """ 272 self.chameleond_proxy = chameleon_port.chameleond_proxy 273 self.port_id = chameleon_port.port_id 274 275 276 def wait_video_input_stable(self, timeout=None): 277 """Waits the video input stable or timeout. 278 279 @param timeout: The time period to wait for. 280 281 @return: True if the video input becomes stable within the timeout 282 period; otherwise, False. 283 """ 284 is_input_stable = self.chameleond_proxy.WaitVideoInputStable( 285 self.port_id, timeout) 286 287 # If video input of Chameleon has been stable, wait for DUT software 288 # layer to be stable as well to make sure all the configurations have 289 # been propagated before proceeding. 290 if is_input_stable: 291 logging.info('Video input has been stable. Waiting for the DUT' 292 ' to be stable...') 293 time.sleep(self._DUT_STABILIZE_TIME) 294 return is_input_stable 295 296 297 def read_edid(self): 298 """Reads the EDID. 299 300 @return: An Edid object or NO_EDID. 301 """ 302 edid_binary = self.chameleond_proxy.ReadEdid(self.port_id) 303 if edid_binary is None: 304 return edid_lib.NO_EDID 305 # Read EDID without verify. It may be made corrupted as intended 306 # for the test purpose. 307 return edid_lib.Edid(edid_binary.data, skip_verify=True) 308 309 310 def apply_edid(self, edid): 311 """Applies the given EDID. 312 313 @param edid: An Edid object or NO_EDID. 314 """ 315 if edid is edid_lib.NO_EDID: 316 self.chameleond_proxy.ApplyEdid(self.port_id, self._EDID_ID_DISABLE) 317 else: 318 edid_binary = xmlrpclib.Binary(edid.data) 319 edid_id = self.chameleond_proxy.CreateEdid(edid_binary) 320 self.chameleond_proxy.ApplyEdid(self.port_id, edid_id) 321 self.chameleond_proxy.DestroyEdid(edid_id) 322 323 324 @contextmanager 325 def use_edid(self, edid): 326 """Uses the given EDID in a with statement. 327 328 It sets the EDID up in the beginning and restores to the original 329 EDID in the end. This function is expected to be used in a with 330 statement, like the following: 331 332 with chameleon_port.use_edid(edid): 333 do_some_test_on(chameleon_port) 334 335 @param edid: An EDID object. 336 """ 337 # Set the EDID up in the beginning. 338 plugged = self.plugged 339 if plugged: 340 self.unplug() 341 342 original_edid = self.read_edid() 343 logging.info('Apply EDID on port %d', self.port_id) 344 self.apply_edid(edid) 345 346 if plugged: 347 time.sleep(self._DURATION_UNPLUG_FOR_EDID) 348 self.plug() 349 self.wait_video_input_stable(self._TIMEOUT_VIDEO_STABLE_PROBE) 350 351 try: 352 # Yeild to execute the with statement. 353 yield 354 finally: 355 # Restore the original EDID in the end. 356 current_edid = self.read_edid() 357 if original_edid.data != current_edid.data: 358 logging.info('Restore the original EDID.') 359 self.apply_edid(original_edid) 360 361 362 def use_edid_file(self, filename): 363 """Uses the given EDID file in a with statement. 364 365 It sets the EDID up in the beginning and restores to the original 366 EDID in the end. This function is expected to be used in a with 367 statement, like the following: 368 369 with chameleon_port.use_edid_file(filename): 370 do_some_test_on(chameleon_port) 371 372 @param filename: A path to the EDID file. 373 """ 374 return self.use_edid(edid_lib.Edid.from_file(filename)) 375 376 377 def fire_hpd_pulse(self, deassert_interval_usec, assert_interval_usec=None, 378 repeat_count=1, end_level=1): 379 380 """Fires one or more HPD pulse (low -> high -> low -> ...). 381 382 @param deassert_interval_usec: The time in microsecond of the 383 deassert pulse. 384 @param assert_interval_usec: The time in microsecond of the 385 assert pulse. If None, then use the same value as 386 deassert_interval_usec. 387 @param repeat_count: The count of HPD pulses to fire. 388 @param end_level: HPD ends with 0 for LOW (unplugged) or 1 for 389 HIGH (plugged). 390 """ 391 self.chameleond_proxy.FireHpdPulse( 392 self.port_id, deassert_interval_usec, 393 assert_interval_usec, repeat_count, int(bool(end_level))) 394 395 396 def fire_mixed_hpd_pulses(self, widths): 397 """Fires one or more HPD pulses, starting at low, of mixed widths. 398 399 One must specify a list of segment widths in the widths argument where 400 widths[0] is the width of the first low segment, widths[1] is that of 401 the first high segment, widths[2] is that of the second low segment... 402 etc. The HPD line stops at low if even number of segment widths are 403 specified; otherwise, it stops at high. 404 405 @param widths: list of pulse segment widths in usec. 406 """ 407 self.chameleond_proxy.FireMixedHpdPulses(self.port_id, widths) 408 409 410 def capture_screen(self): 411 """Captures Chameleon framebuffer. 412 413 @return An Image object. 414 """ 415 return Image.fromstring( 416 'RGB', 417 self.get_resolution(), 418 self.chameleond_proxy.DumpPixels(self.port_id).data) 419 420 421 def get_resolution(self): 422 """Gets the source resolution. 423 424 @return: A (width, height) tuple. 425 """ 426 # The return value of RPC is converted to a list. Convert it back to 427 # a tuple. 428 return tuple(self.chameleond_proxy.DetectResolution(self.port_id)) 429 430 431 def set_content_protection(self, enable): 432 """Sets the content protection state on the port. 433 434 @param enable: True to enable; False to disable. 435 """ 436 self.chameleond_proxy.SetContentProtection(self.port_id, enable) 437 438 439 def is_content_protection_enabled(self): 440 """Returns True if the content protection is enabled on the port. 441 442 @return: True if the content protection is enabled; otherwise, False. 443 """ 444 return self.chameleond_proxy.IsContentProtectionEnabled(self.port_id) 445 446 447 def is_video_input_encrypted(self): 448 """Returns True if the video input on the port is encrypted. 449 450 @return: True if the video input is encrypted; otherwise, False. 451 """ 452 return self.chameleond_proxy.IsVideoInputEncrypted(self.port_id) 453 454 455 def start_capturing_video(self, box=None): 456 """ 457 Captures video frames. Asynchronous, returns immediately. 458 459 @param box: int tuple, left, upper, right, lower pixel coordinates. 460 Defines the rectangular boundary within which to capture. 461 """ 462 463 if box is None: 464 self.chameleond_proxy.StartCapturingVideo(self.port_id) 465 else: 466 self.chameleond_proxy.StartCapturingVideo(self.port_id, *box) 467 468 469 def stop_capturing_video(self): 470 """ 471 Stops the ongoing video frame capturing. 472 473 """ 474 self.chameleond_proxy.StopCapturingVideo(self.port_id) 475 476 477 def get_captured_frame_count(self): 478 """ 479 @return: int, the number of frames that have been captured. 480 481 """ 482 return self.chameleond_proxy.GetCapturedFrameCount() 483 484 485 def read_captured_frame(self, index): 486 """ 487 @param index: int, index of the desired captured frame. 488 @return: xmlrpclib.Binary object containing a byte-array of the pixels. 489 490 """ 491 492 frame = self.chameleond_proxy.ReadCapturedFrame(index) 493 return Image.fromstring('RGB', 494 self.get_captured_resolution(), 495 frame.data) 496 497 498 def get_captured_checksums(self, start_index=0, stop_index=None): 499 """ 500 @param start_index: int, index of the frame to start with. 501 @param stop_index: int, index of the frame (excluded) to stop at. 502 @return: a list of checksums of frames captured. 503 504 """ 505 return self.chameleond_proxy.GetCapturedChecksums(start_index, 506 stop_index) 507 508 509 def get_captured_resolution(self): 510 """ 511 @return: (width, height) tuple, the resolution of captured frames. 512 513 """ 514 return self.chameleond_proxy.GetCapturedResolution() 515 516 517 518class ChameleonAudioInput(ChameleonPort): 519 """ChameleonAudioInput is an abstraction of an audio input port. 520 521 It contains some special methods to control an audio input. 522 """ 523 524 def __init__(self, chameleon_port): 525 """Construct a ChameleonAudioInput. 526 527 @param chameleon_port: A general ChameleonPort object. 528 """ 529 self.chameleond_proxy = chameleon_port.chameleond_proxy 530 self.port_id = chameleon_port.port_id 531 532 533 def start_capturing_audio(self): 534 """Starts capturing audio.""" 535 return self.chameleond_proxy.StartCapturingAudio(self.port_id) 536 537 538 def stop_capturing_audio(self): 539 """Stops capturing audio. 540 541 Returns: 542 A tuple (remote_path, format). 543 remote_path: The captured file path on Chameleon. 544 format: A dict containing: 545 file_type: 'raw' or 'wav'. 546 sample_format: 'S32_LE' for 32-bit signed integer in little-endian. 547 Refer to aplay manpage for other formats. 548 channel: channel number. 549 rate: sampling rate. 550 """ 551 remote_path, data_format = self.chameleond_proxy.StopCapturingAudio( 552 self.port_id) 553 return remote_path, data_format 554 555 556class ChameleonAudioOutput(ChameleonPort): 557 """ChameleonAudioOutput is an abstraction of an audio output port. 558 559 It contains some special methods to control an audio output. 560 """ 561 562 def __init__(self, chameleon_port): 563 """Construct a ChameleonAudioOutput. 564 565 @param chameleon_port: A general ChameleonPort object. 566 """ 567 self.chameleond_proxy = chameleon_port.chameleond_proxy 568 self.port_id = chameleon_port.port_id 569 570 571 def start_playing_audio(self, path, data_format): 572 """Starts playing audio. 573 574 @param path: The path to the file to play on Chameleon. 575 @param data_format: A dict containing data format. Currently Chameleon 576 only accepts data format: 577 dict(file_type='raw', sample_format='S32_LE', 578 channel=8, rate=48000). 579 580 """ 581 self.chameleond_proxy.StartPlayingAudio(self.port_id, path, data_format) 582 583 584 def stop_playing_audio(self): 585 """Stops capturing audio.""" 586 self.chameleond_proxy.StopPlayingAudio(self.port_id) 587 588 589def make_chameleon_hostname(dut_hostname): 590 """Given a DUT's hostname, returns the hostname of its Chameleon. 591 592 @param dut_hostname: Hostname of a DUT. 593 594 @return Hostname of the DUT's Chameleon. 595 """ 596 host_parts = dut_hostname.split('.') 597 host_parts[0] = host_parts[0] + '-chameleon' 598 return '.'.join(host_parts) 599 600 601def create_chameleon_board(dut_hostname, args): 602 """Given either DUT's hostname or argments, creates a ChameleonBoard object. 603 604 If the DUT's hostname is in the lab zone, it connects to the Chameleon by 605 append the hostname with '-chameleon' suffix. If not, checks if the args 606 contains the key-value pair 'chameleon_host=IP'. 607 608 @param dut_hostname: Hostname of a DUT. 609 @param args: A string of arguments passed from the command line. 610 611 @return A ChameleonBoard object. 612 613 @raise ChameleonConnectionError if unknown hostname. 614 """ 615 connection = None 616 hostname = make_chameleon_hostname(dut_hostname) 617 if utils.host_is_in_lab_zone(hostname): 618 connection = ChameleonConnection(hostname) 619 else: 620 args_dict = utils.args_to_dict(args) 621 hostname = args_dict.get('chameleon_host', None) 622 port = args_dict.get('chameleon_port', CHAMELEON_PORT) 623 if hostname: 624 connection = ChameleonConnection(hostname, port) 625 else: 626 raise ChameleonConnectionError('No chameleon_host is given in args') 627 628 return ChameleonBoard(connection) 629