1# Copyright 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"""Facade to access the display-related functionality.""" 6 7import logging 8import multiprocessing 9import numpy 10import os 11import re 12import shutil 13import time 14import json 15from autotest_lib.client.bin import utils 16from autotest_lib.client.common_lib import error 17from autotest_lib.client.common_lib import utils as common_utils 18from autotest_lib.client.common_lib.cros import retry 19from autotest_lib.client.cros import constants 20from autotest_lib.client.cros.graphics import graphics_utils 21from autotest_lib.client.cros.multimedia import facade_resource 22from autotest_lib.client.cros.multimedia import image_generator 23from autotest_lib.client.cros.power import sys_power 24from telemetry.internal.browser import web_contents 25 26class TimeoutException(Exception): 27 """Timeout Exception class.""" 28 pass 29 30 31_FLAKY_CALL_RETRY_TIMEOUT_SEC = 60 32_FLAKY_DISPLAY_CALL_RETRY_DELAY_SEC = 2 33 34_retry_display_call = retry.retry( 35 (KeyError, error.CmdError), 36 timeout_min=_FLAKY_CALL_RETRY_TIMEOUT_SEC / 60.0, 37 delay_sec=_FLAKY_DISPLAY_CALL_RETRY_DELAY_SEC) 38 39 40class DisplayFacadeNative(object): 41 """Facade to access the display-related functionality. 42 43 The methods inside this class only accept Python native types. 44 """ 45 46 CALIBRATION_IMAGE_PATH = '/tmp/calibration.png' 47 MINIMUM_REFRESH_RATE_EXPECTED = 25.0 48 DELAY_TIME = 3 49 MAX_TYPEC_PORT = 6 50 51 def __init__(self, resource): 52 """Initializes a DisplayFacadeNative. 53 54 @param resource: A FacadeResource object. 55 """ 56 self._resource = resource 57 self._image_generator = image_generator.ImageGenerator() 58 59 60 @facade_resource.retry_chrome_call 61 def get_display_info(self): 62 """Gets the display info from Chrome.system.display API. 63 64 @return array of dict for display info. 65 """ 66 extension = self._resource.get_extension( 67 constants.DISPLAY_TEST_EXTENSION) 68 extension.ExecuteJavaScript('window.__display_info = null;') 69 extension.ExecuteJavaScript( 70 "chrome.system.display.getInfo(function(info) {" 71 "window.__display_info = info;})") 72 utils.wait_for_value(lambda: ( 73 extension.EvaluateJavaScript("window.__display_info") != None), 74 expected_value=True) 75 return extension.EvaluateJavaScript("window.__display_info") 76 77 78 @facade_resource.retry_chrome_call 79 def get_window_info(self): 80 """Gets the current window info from Chrome.system.window API. 81 82 @return a dict for the information of the current window. 83 """ 84 extension = self._resource.get_extension() 85 extension.ExecuteJavaScript('window.__window_info = null;') 86 extension.ExecuteJavaScript( 87 "chrome.windows.getCurrent(function(info) {" 88 "window.__window_info = info;})") 89 utils.wait_for_value(lambda: ( 90 extension.EvaluateJavaScript("window.__window_info") != None), 91 expected_value=True) 92 return extension.EvaluateJavaScript("window.__window_info") 93 94 95 @facade_resource.retry_chrome_call 96 def create_window(self, url='chrome://newtab'): 97 """Creates a new window from chrome.windows.create API. 98 99 @param url: Optional URL for the new window. 100 101 @return Identifier for the new window. 102 103 @raise TimeoutException if it fails. 104 """ 105 extension = self._resource.get_extension() 106 107 extension.ExecuteJavaScript( 108 """ 109 var __new_window_id = null; 110 chrome.windows.create( 111 {url: '%s'}, 112 function(win) { 113 __new_window_id = win.id}); 114 """ % (url) 115 ) 116 extension.WaitForJavaScriptCondition( 117 "__new_window_id !== null", 118 timeout=web_contents.DEFAULT_WEB_CONTENTS_TIMEOUT) 119 120 return extension.EvaluateJavaScript("__new_window_id") 121 122 123 @facade_resource.retry_chrome_call 124 def update_window(self, window_id, state=None, bounds=None): 125 """Updates an existing window using the chrome.windows.update API. 126 127 @param window_id: Identifier for the window to update. 128 @param state: Optional string to set the state such as 'normal', 129 'maximized', or 'fullscreen'. 130 @param bounds: Optional dictionary with keys top, left, width, and 131 height to reposition the window. 132 133 @return True if success. 134 135 @raise TimeoutException if it fails. 136 """ 137 extension = self._resource.get_extension() 138 params = {} 139 140 if state: 141 params['state'] = state 142 if bounds: 143 params['top'] = bounds['top'] 144 params['left'] = bounds['left'] 145 params['width'] = bounds['width'] 146 params['height'] = bounds['height'] 147 148 if not params: 149 logging.info('Nothing to update for window_id={}'.format(window_id)) 150 return True 151 152 extension.ExecuteJavaScript( 153 """ 154 var __status = 'Running'; 155 chrome.windows.update(%d, %s, 156 function(win) { 157 __status = 'Done'}); 158 """ % (window_id, json.dumps(params)) 159 ) 160 extension.WaitForJavaScriptCondition( 161 "__status == 'Done'", 162 timeout=web_contents.DEFAULT_WEB_CONTENTS_TIMEOUT) 163 164 return True 165 166 167 def _get_display_by_id(self, display_id): 168 """Gets a display by ID. 169 170 @param display_id: id of the display. 171 172 @return: A dict of various display info. 173 """ 174 for display in self.get_display_info(): 175 if display['id'] == display_id: 176 return display 177 raise RuntimeError('Cannot find display ' + display_id) 178 179 180 def get_display_modes(self, display_id): 181 """Gets all the display modes for the specified display. 182 183 @param display_id: id of the display to get modes from. 184 185 @return: A list of DisplayMode dicts. 186 """ 187 display = self._get_display_by_id(display_id) 188 return display['modes'] 189 190 191 def get_display_rotation(self, display_id): 192 """Gets the display rotation for the specified display. 193 194 @param display_id: id of the display to get modes from. 195 196 @return: Degree of rotation. 197 """ 198 display = self._get_display_by_id(display_id) 199 return display['rotation'] 200 201 202 def get_display_notifications(self): 203 """Gets the display notifications 204 205 @return: Returns a list of display related notifications only. 206 """ 207 display_notifications = [] 208 for notification in self._resource.get_visible_notifications(): 209 if notification['id'] == 'chrome://settings/display': 210 display_notifications.append(notification) 211 return display_notifications 212 213 214 def set_display_rotation(self, display_id, rotation, 215 delay_before_rotation=0, delay_after_rotation=0): 216 """Sets the display rotation for the specified display. 217 218 @param display_id: id of the display to get modes from. 219 @param rotation: degree of rotation 220 @param delay_before_rotation: time in second for delay before rotation 221 @param delay_after_rotation: time in second for delay after rotation 222 """ 223 time.sleep(delay_before_rotation) 224 extension = self._resource.get_extension( 225 constants.DISPLAY_TEST_EXTENSION) 226 extension.ExecuteJavaScript( 227 """ 228 window.__set_display_rotation_has_error = null; 229 chrome.system.display.setDisplayProperties('%(id)s', 230 {"rotation": %(rotation)d}, () => { 231 if (chrome.runtime.lastError) { 232 console.error('Failed to set display rotation', 233 chrome.runtime.lastError); 234 window.__set_display_rotation_has_error = "failure"; 235 } else { 236 window.__set_display_rotation_has_error = "success"; 237 } 238 }); 239 """ 240 % {'id': display_id, 'rotation': rotation} 241 ) 242 utils.wait_for_value(lambda: ( 243 extension.EvaluateJavaScript( 244 'window.__set_display_rotation_has_error') != None), 245 expected_value=True) 246 time.sleep(delay_after_rotation) 247 result = extension.EvaluateJavaScript( 248 'window.__set_display_rotation_has_error') 249 if result != 'success': 250 raise RuntimeError('Failed to set display rotation: %r' % result) 251 252 253 def get_available_resolutions(self, display_id): 254 """Gets the resolutions from the specified display. 255 256 @return a list of (width, height) tuples. 257 """ 258 display = self._get_display_by_id(display_id) 259 modes = display['modes'] 260 if 'widthInNativePixels' not in modes[0]: 261 raise RuntimeError('Cannot find widthInNativePixels attribute') 262 if display['isInternal']: 263 logging.info("Getting resolutions of internal display") 264 return list(set([(mode['width'], mode['height']) for mode in 265 modes])) 266 return list(set([(mode['widthInNativePixels'], 267 mode['heightInNativePixels']) for mode in modes])) 268 269 270 def get_internal_display_id(self): 271 """Gets the internal display id. 272 273 @return the id of the internal display. 274 """ 275 for display in self.get_display_info(): 276 if display['isInternal']: 277 return display['id'] 278 raise RuntimeError('Cannot find internal display') 279 280 281 def get_first_external_display_id(self): 282 """Gets the first external display id. 283 284 @return the id of the first external display; -1 if not found. 285 """ 286 # Get the first external and enabled display 287 for display in self.get_display_info(): 288 if display['isEnabled'] and not display['isInternal']: 289 return display['id'] 290 return -1 291 292 293 def set_resolution(self, display_id, width, height, timeout=3): 294 """Sets the resolution of the specified display. 295 296 @param display_id: id of the display to set resolution for. 297 @param width: width of the resolution 298 @param height: height of the resolution 299 @param timeout: maximal time in seconds waiting for the new resolution 300 to settle in. 301 @raise TimeoutException when the operation is timed out. 302 """ 303 304 extension = self._resource.get_extension( 305 constants.DISPLAY_TEST_EXTENSION) 306 extension.ExecuteJavaScript( 307 """ 308 window.__set_resolution_progress = null; 309 chrome.system.display.getInfo((info_array) => { 310 var mode; 311 for (var info of info_array) { 312 if (info['id'] == '%(id)s') { 313 for (var m of info['modes']) { 314 if (m['width'] == %(width)d && 315 m['height'] == %(height)d) { 316 mode = m; 317 break; 318 } 319 } 320 break; 321 } 322 } 323 if (mode === undefined) { 324 console.error('Failed to select the resolution ' + 325 '%(width)dx%(height)d'); 326 window.__set_resolution_progress = "mode not found"; 327 return; 328 } 329 330 chrome.system.display.setDisplayProperties('%(id)s', 331 {'displayMode': mode}, () => { 332 if (chrome.runtime.lastError) { 333 window.__set_resolution_progress = "failed: " + 334 chrome.runtime.lastError.message; 335 } else { 336 window.__set_resolution_progress = "succeeded"; 337 } 338 } 339 ); 340 }); 341 """ 342 % {'id': display_id, 'width': width, 'height': height} 343 ) 344 utils.wait_for_value(lambda: ( 345 extension.EvaluateJavaScript( 346 'window.__set_resolution_progress') != None), 347 expected_value=True) 348 result = extension.EvaluateJavaScript( 349 'window.__set_resolution_progress') 350 if result != 'succeeded': 351 raise RuntimeError('Failed to set resolution: %r' % result) 352 353 354 @_retry_display_call 355 def get_external_resolution(self): 356 """Gets the resolution of the external screen. 357 358 @return The resolution tuple (width, height) 359 """ 360 return graphics_utils.get_external_resolution() 361 362 def get_internal_resolution(self): 363 """Gets the resolution of the internal screen. 364 365 @return The resolution tuple (width, height) or None if internal screen 366 is not available 367 """ 368 for display in self.get_display_info(): 369 if display['isInternal']: 370 bounds = display['bounds'] 371 return (bounds['width'], bounds['height']) 372 return None 373 374 375 def set_content_protection(self, state): 376 """Sets the content protection of the external screen. 377 378 @param state: One of the states 'Undesired', 'Desired', or 'Enabled' 379 """ 380 connector = self.get_external_connector_name() 381 graphics_utils.set_content_protection(connector, state) 382 383 384 def get_content_protection(self): 385 """Gets the state of the content protection. 386 387 @param output: The output name as a string. 388 @return: A string of the state, like 'Undesired', 'Desired', or 'Enabled'. 389 False if not supported. 390 """ 391 connector = self.get_external_connector_name() 392 return graphics_utils.get_content_protection(connector) 393 394 395 def get_external_crtc(self): 396 """Gets the external crtc. 397 398 @return The id of the external crtc.""" 399 return graphics_utils.get_external_crtc() 400 401 402 def get_internal_crtc(self): 403 """Gets the internal crtc. 404 405 @retrun The id of the internal crtc.""" 406 return graphics_utils.get_internal_crtc() 407 408 409 def take_internal_screenshot(self, path): 410 """Takes internal screenshot. 411 412 @param path: path to image file. 413 """ 414 self.take_screenshot_crtc(path, self.get_internal_crtc()) 415 416 417 def take_external_screenshot(self, path): 418 """Takes external screenshot. 419 420 @param path: path to image file. 421 """ 422 self.take_screenshot_crtc(path, self.get_external_crtc()) 423 424 425 def take_screenshot_crtc(self, path, id): 426 """Captures the DUT screenshot, use id for selecting screen. 427 428 @param path: path to image file. 429 @param id: The id of the crtc to screenshot. 430 """ 431 432 graphics_utils.take_screenshot_crop(path, crtc_id=id) 433 return True 434 435 436 def save_calibration_image(self, path): 437 """Save the calibration image to the given path. 438 439 @param path: path to image file. 440 """ 441 shutil.copy(self.CALIBRATION_IMAGE_PATH, path) 442 return True 443 444 445 def take_tab_screenshot(self, output_path, url_pattern=None): 446 """Takes a screenshot of the tab specified by the given url pattern. 447 448 @param output_path: A path of the output file. 449 @param url_pattern: A string of url pattern used to search for tabs. 450 Default is to look for .svg image. 451 """ 452 if url_pattern is None: 453 # If no URL pattern is provided, defaults to capture the first 454 # tab that shows SVG image. 455 url_pattern = '.svg' 456 457 tabs = self._resource.get_tabs() 458 for i in xrange(0, len(tabs)): 459 if url_pattern in tabs[i].url: 460 data = tabs[i].Screenshot(timeout=5) 461 # Flip the colors from BGR to RGB. 462 data = numpy.fliplr(data.reshape(-1, 3)).reshape(data.shape) 463 data.tofile(output_path) 464 break 465 return True 466 467 468 def toggle_mirrored(self): 469 """Toggles mirrored.""" 470 graphics_utils.screen_toggle_mirrored() 471 return True 472 473 474 def hide_cursor(self): 475 """Hides mouse cursor.""" 476 graphics_utils.hide_cursor() 477 return True 478 479 480 def hide_typing_cursor(self): 481 """Hides typing cursor.""" 482 graphics_utils.hide_typing_cursor() 483 return True 484 485 486 def is_mirrored_enabled(self): 487 """Checks the mirrored state. 488 489 @return True if mirrored mode is enabled. 490 """ 491 return bool(self.get_display_info()[0]['mirroringSourceId']) 492 493 494 def set_mirrored(self, is_mirrored): 495 """Sets mirrored mode. 496 497 @param is_mirrored: True or False to indicate mirrored state. 498 @return True if success, False otherwise. 499 """ 500 if self.is_mirrored_enabled() == is_mirrored: 501 return True 502 503 retries = 4 504 while retries > 0: 505 self.toggle_mirrored() 506 result = utils.wait_for_value(self.is_mirrored_enabled, 507 expected_value=is_mirrored, 508 timeout_sec=3) 509 if result == is_mirrored: 510 return True 511 retries -= 1 512 return False 513 514 515 def is_display_primary(self, internal=True): 516 """Checks if internal screen is primary display. 517 518 @param internal: is internal/external screen primary status requested 519 @return boolean True if internal display is primary. 520 """ 521 for info in self.get_display_info(): 522 if info['isInternal'] == internal and info['isPrimary']: 523 return True 524 return False 525 526 527 def suspend_resume(self, suspend_time=10): 528 """Suspends the DUT for a given time in second. 529 530 @param suspend_time: Suspend time in second. 531 """ 532 sys_power.do_suspend(suspend_time) 533 return True 534 535 536 def suspend_resume_bg(self, suspend_time=10): 537 """Suspends the DUT for a given time in second in the background. 538 539 @param suspend_time: Suspend time in second. 540 """ 541 process = multiprocessing.Process(target=self.suspend_resume, 542 args=(suspend_time,)) 543 process.start() 544 return True 545 546 547 @_retry_display_call 548 def get_external_connector_name(self): 549 """Gets the name of the external output connector. 550 551 @return The external output connector name as a string, if any. 552 Otherwise, return False. 553 """ 554 return graphics_utils.get_external_connector_name() 555 556 557 def get_internal_connector_name(self): 558 """Gets the name of the internal output connector. 559 560 @return The internal output connector name as a string, if any. 561 Otherwise, return False. 562 """ 563 return graphics_utils.get_internal_connector_name() 564 565 566 def wait_external_display_connected(self, display): 567 """Waits for the specified external display to be connected. 568 569 @param display: The display name as a string, like 'HDMI1', or 570 False if no external display is expected. 571 @return: True if display is connected; False otherwise. 572 """ 573 result = utils.wait_for_value(self.get_external_connector_name, 574 expected_value=display) 575 return result == display 576 577 578 @facade_resource.retry_chrome_call 579 def move_to_display(self, display_id): 580 """Moves the current window to the indicated display. 581 582 @param display_id: The id of the indicated display. 583 @return True if success. 584 585 @raise TimeoutException if it fails. 586 """ 587 display_info = self._get_display_by_id(display_id) 588 if not display_info['isEnabled']: 589 raise RuntimeError('Cannot find the indicated display') 590 target_bounds = display_info['bounds'] 591 592 extension = self._resource.get_extension() 593 # If the area of bounds is empty (here we achieve this by setting 594 # width and height to zero), the window_sizer will automatically 595 # determine an area which is visible and fits on the screen. 596 # For more details, see chrome/browser/ui/window_sizer.cc 597 # Without setting state to 'normal', if the current state is 598 # 'minimized', 'maximized' or 'fullscreen', the setting of 599 # 'left', 'top', 'width' and 'height' will be ignored. 600 # For more details, see chrome/browser/extensions/api/tabs/tabs_api.cc 601 extension.ExecuteJavaScript( 602 """ 603 var __status = 'Running'; 604 chrome.windows.update( 605 chrome.windows.WINDOW_ID_CURRENT, 606 {left: %d, top: %d, width: 0, height: 0, 607 state: 'normal'}, 608 function(info) { 609 if (info.left == %d && info.top == %d && 610 info.state == 'normal') 611 __status = 'Done'; }); 612 """ 613 % (target_bounds['left'], target_bounds['top'], 614 target_bounds['left'], target_bounds['top']) 615 ) 616 extension.WaitForJavaScriptCondition( 617 "__status == 'Done'", 618 timeout=web_contents.DEFAULT_WEB_CONTENTS_TIMEOUT) 619 return True 620 621 622 def is_fullscreen_enabled(self): 623 """Checks the fullscreen state. 624 625 @return True if fullscreen mode is enabled. 626 """ 627 return self.get_window_info()['state'] == 'fullscreen' 628 629 630 def set_fullscreen(self, is_fullscreen): 631 """Sets the current window to full screen. 632 633 @param is_fullscreen: True or False to indicate fullscreen state. 634 @return True if success, False otherwise. 635 """ 636 extension = self._resource.get_extension() 637 if not extension: 638 raise RuntimeError('Autotest extension not found') 639 640 if is_fullscreen: 641 window_state = "fullscreen" 642 else: 643 window_state = "normal" 644 extension.ExecuteJavaScript( 645 """ 646 var __status = 'Running'; 647 chrome.windows.update( 648 chrome.windows.WINDOW_ID_CURRENT, 649 {state: '%s'}, 650 function() { __status = 'Done'; }); 651 """ 652 % window_state) 653 utils.wait_for_value(lambda: ( 654 extension.EvaluateJavaScript('__status') == 'Done'), 655 expected_value=True) 656 return self.is_fullscreen_enabled() == is_fullscreen 657 658 659 def load_url(self, url): 660 """Loads the given url in a new tab. The new tab will be active. 661 662 @param url: The url to load as a string. 663 @return a str, the tab descriptor of the opened tab. 664 """ 665 return self._resource.load_url(url) 666 667 668 def load_calibration_image(self, resolution): 669 """Opens a new tab and loads a full screen calibration 670 image from the HTTP server. 671 672 @param resolution: A tuple (width, height) of resolution. 673 @return a str, the tab descriptor of the opened tab. 674 """ 675 path = self.CALIBRATION_IMAGE_PATH 676 self._image_generator.generate_image(resolution[0], resolution[1], path) 677 os.chmod(path, 0644) 678 tab_descriptor = self.load_url('file://%s' % path) 679 return tab_descriptor 680 681 682 def load_color_sequence(self, tab_descriptor, color_sequence): 683 """Displays a series of colors on full screen on the tab. 684 tab_descriptor is returned by any open tab API of display facade. 685 e.g., 686 tab_descriptor = load_url('about:blank') 687 load_color_sequence(tab_descriptor, color) 688 689 @param tab_descriptor: Indicate which tab to test. 690 @param color_sequence: An integer list for switching colors. 691 @return A list of the timestamp for each switch. 692 """ 693 tab = self._resource.get_tab_by_descriptor(tab_descriptor) 694 color_sequence_for_java_script = ( 695 'var color_sequence = [' + 696 ','.join("'#%06X'" % x for x in color_sequence) + 697 '];') 698 # Paints are synchronized to the fresh rate of the screen by 699 # window.requestAnimationFrame. 700 tab.ExecuteJavaScript(color_sequence_for_java_script + """ 701 function render(timestamp) { 702 window.timestamp_list.push(timestamp); 703 if (window.count < color_sequence.length) { 704 document.body.style.backgroundColor = 705 color_sequence[count]; 706 window.count++; 707 window.requestAnimationFrame(render); 708 } 709 } 710 window.count = 0; 711 window.timestamp_list = []; 712 window.requestAnimationFrame(render); 713 """) 714 715 # Waiting time is decided by following concerns: 716 # 1. MINIMUM_REFRESH_RATE_EXPECTED: the minimum refresh rate 717 # we expect it to be. Real refresh rate is related to 718 # not only hardware devices but also drivers and browsers. 719 # Most graphics devices support at least 60fps for a single 720 # monitor, and under mirror mode, since the both frames 721 # buffers need to be updated for an input frame, the refresh 722 # rate will decrease by half, so here we set it to be a 723 # little less than 30 (= 60/2) to make it more tolerant. 724 # 2. DELAY_TIME: extra wait time for timeout. 725 tab.WaitForJavaScriptCondition( 726 'window.count == color_sequence.length', 727 timeout=( 728 (len(color_sequence) / self.MINIMUM_REFRESH_RATE_EXPECTED) 729 + self.DELAY_TIME)) 730 return tab.EvaluateJavaScript("window.timestamp_list") 731 732 733 def close_tab(self, tab_descriptor): 734 """Disables fullscreen and closes the tab of the given tab descriptor. 735 tab_descriptor is returned by any open tab API of display facade. 736 e.g., 737 1. 738 tab_descriptor = load_url(url) 739 close_tab(tab_descriptor) 740 741 2. 742 tab_descriptor = load_calibration_image(resolution) 743 close_tab(tab_descriptor) 744 745 @param tab_descriptor: Indicate which tab to be closed. 746 """ 747 if tab_descriptor: 748 # set_fullscreen(False) is necessary here because currently there 749 # is a bug in tabs.Close(). If the current state is fullscreen and 750 # we call close_tab() without setting state back to normal, it will 751 # cancel fullscreen mode without changing system configuration, and 752 # so that the next time someone calls set_fullscreen(True), the 753 # function will find that current state is already 'fullscreen' 754 # (though it is not) and do nothing, which will break all the 755 # following tests. 756 self.set_fullscreen(False) 757 self._resource.close_tab(tab_descriptor) 758 else: 759 logging.error('close_tab: not a valid tab_descriptor') 760 761 return True 762 763 764 def reset_connector_if_applicable(self, connector_type): 765 """Resets Type-C video connector from host end if applicable. 766 767 It's the workaround sequence since sometimes Type-C dongle becomes 768 corrupted and needs to be re-plugged. 769 770 @param connector_type: A string, like "VGA", "DVI", "HDMI", or "DP". 771 """ 772 if connector_type != 'HDMI' and connector_type != 'DP': 773 return 774 # Decide if we need to add --name=cros_pd 775 usbpd_command = 'ectool --name=cros_pd usbpd' 776 try: 777 common_utils.run('%s 0' % usbpd_command) 778 except error.CmdError: 779 usbpd_command = 'ectool usbpd' 780 781 port = 0 782 while port < self.MAX_TYPEC_PORT: 783 # We use usbpd to get Role information and then power cycle the 784 # SRC one. 785 command = '%s %d' % (usbpd_command, port) 786 try: 787 output = common_utils.run(command).stdout 788 if re.compile('Role.*SRC').search(output): 789 logging.info('power-cycle Type-C port %d', port) 790 common_utils.run('%s sink' % command) 791 common_utils.run('%s auto' % command) 792 port += 1 793 except error.CmdError: 794 break 795