1# Lint as: python2, python3 2# Copyright 2018 The Chromium OS Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6import logging 7import re 8import time 9 10from autotest_lib.client.common_lib import autotest_enum, error 11from autotest_lib.server import test 12from autotest_lib.server.cros import servo_keyboard_utils 13from autotest_lib.server.cros.dark_resume_utils import DarkResumeUtils 14from autotest_lib.server.cros.faft.utils.config import Config as FAFTConfig 15from autotest_lib.server.cros.power import servo_charger 16from autotest_lib.server.cros.servo import chrome_ec 17 18 19# Possible states base can be forced into. 20BASE_STATE = autotest_enum.AutotestEnum('ATTACH', 'DETACH', 'RESET') 21 22# Possible states for tablet mode as defined in common/tablet_mode.c via 23# crrev.com/c/1797370. 24TABLET_MODE = autotest_enum.AutotestEnum('ON', 'OFF', 'RESET') 25 26# List of wake sources expected to cause a full resume. 27FULL_WAKE_SOURCES = [ 28 'PWR_BTN', 'LID_OPEN', 'BASE_ATTACH', 'BASE_DETACH', 'INTERNAL_KB', 29 'USB_KB', 'TABLET_MODE_ON', 'TABLET_MODE_OFF' 30] 31 32# List of wake sources expected to cause a dark resume. 33DARK_RESUME_SOURCES = ['RTC', 'AC_CONNECTED', 'AC_DISCONNECTED'] 34 35# Time in future after which RTC goes off when testing wake due to RTC alarm. 36RTC_WAKE_SECS = 20 37 38# Max time taken by the device to suspend. This includes the time powerd takes 39# trigger the suspend after receiving the suspend request from autotest script. 40SECS_FOR_SUSPENDING = 20 41 42# Time to allow lid transition to take effect. 43WAIT_TIME_LID_TRANSITION_SECS = 5 44 45# Time to wait for the DUT to see USB keyboard after restting the Atmega USB 46# emulator on Servo. 47USB_PRESENT_DELAY = 1 48 49 50class power_WakeSources(test.test): 51 """ 52 Verify that wakes from input devices can trigger a full 53 resume. Currently tests : 54 1. power button 55 2. lid open 56 3. base attach 57 4. base detach 58 59 Also tests that dark resume wake sources work as expected, such as: 60 1. RTC 61 2. AC_CONNECTED 62 3. AC_DISCONNECTED 63 64 """ 65 version = 1 66 67 def _after_resume(self, wake_source): 68 """Cleanup to perform after resuming the device. 69 70 @param wake_source: Wake source that has been tested. 71 """ 72 if wake_source in ['BASE_ATTACH', 'BASE_DETACH']: 73 self._force_base_state(BASE_STATE.RESET) 74 elif wake_source in ['TABLET_MODE_ON', 'TABLET_MODE_OFF']: 75 self._force_tablet_mode(TABLET_MODE.RESET) 76 elif wake_source in ['AC_CONNECTED', 'AC_DISCONNECTED']: 77 self._chg_manager.start_charging() 78 79 def _before_suspend(self, wake_source): 80 """Prep before suspend. 81 82 @param wake_source: Wake source that is going to be tested. 83 84 @return: Boolean, whether _before_suspend action is successful. 85 """ 86 if wake_source == 'BASE_ATTACH': 87 # Force detach before suspend so that attach won't be ignored. 88 self._force_base_state(BASE_STATE.DETACH) 89 elif wake_source == 'BASE_DETACH': 90 # Force attach before suspend so that detach won't be ignored. 91 self._force_base_state(BASE_STATE.ATTACH) 92 elif wake_source == 'LID_OPEN': 93 # Set the power policy for lid closed action to suspend. 94 return self._host.run( 95 'set_power_policy --lid_closed_action suspend', 96 ignore_status=True).exit_status == 0 97 elif wake_source == 'USB_KB': 98 # Initialize USB keyboard. 99 self._host.servo.set_nocheck('init_usb_keyboard', 'on') 100 elif wake_source == 'TABLET_MODE_ON': 101 self._force_tablet_mode(TABLET_MODE.OFF) 102 elif wake_source == 'TABLET_MODE_OFF': 103 self._force_tablet_mode(TABLET_MODE.ON) 104 elif wake_source == 'AC_CONNECTED': 105 self._chg_manager.stop_charging() 106 elif wake_source == 'AC_DISCONNECTED': 107 self._chg_manager.start_charging() 108 return True 109 110 def _force_tablet_mode(self, mode): 111 """Send EC command to force the tablet mode. 112 113 @param mode: mode to force. One of the |TABLET_MODE| enum. 114 """ 115 ec_cmd = 'tabletmode ' 116 ec_arg = { 117 TABLET_MODE.ON: 'on', 118 TABLET_MODE.OFF: 'off', 119 TABLET_MODE.RESET: 'r' 120 } 121 122 ec_cmd += ec_arg[mode] 123 self._ec.send_command(ec_cmd) 124 125 def _force_base_state(self, base_state): 126 """Send EC command to force the |base_state|. 127 128 @param base_state: State to force base to. One of |BASE_STATE| enum. 129 """ 130 ec_cmd = 'basestate ' 131 ec_arg = { 132 BASE_STATE.ATTACH: 'a', 133 BASE_STATE.DETACH: 'd', 134 BASE_STATE.RESET: 'r' 135 } 136 137 ec_cmd += ec_arg[base_state] 138 self._ec.send_command(ec_cmd) 139 140 def _x86_get_ec_wake_mask(self): 141 # Check both the S0ix and S3 wake masks. 142 try: 143 s0ix_wake_mask = int(self._host.run( 144 'ectool hostevent get %d' % 145 chrome_ec.EC_HOST_EVENT_LAZY_WAKE_MASK_S0IX).stdout, 146 base=16) 147 except error.AutoservRunError as e: 148 s0ix_wake_mask = 0 149 logging.info( 150 '"ectool hostevent get" failed for s0ix wake mask with' 151 ' exception: %s', str(e)) 152 153 try: 154 s3_wake_mask = int(self._host.run( 155 'ectool hostevent get %d' % 156 chrome_ec.EC_HOST_EVENT_LAZY_WAKE_MASK_S3).stdout, 157 base=16) 158 except error.AutoservRunError as e: 159 s3_wake_mask = 0 160 logging.info( 161 '"ectool hostevent get" failed for s3 wake mask with' 162 ' exception: %s', str(e)) 163 164 return s0ix_wake_mask | s3_wake_mask 165 166 def _arm_get_ec_wake_mask(self): 167 try: 168 s3_mkbpwakemask_out = self._host.run( 169 'ectool mkbpwakemask get hostevent').stdout 170 match = re.match(r'MBKP hostevent wake mask: (0x[0-9A-Fa-f]+)', 171 s3_mkbpwakemask_out) 172 if match: 173 return int(match.group(1), base=16) 174 else: 175 logging.info( 176 '"ectool mkbpwakemask get hostevent" returned: %s', 177 s3_mkbpwakemask_out) 178 except error.AutoservRunError as e: 179 logging.info( 180 '"ectool mkbpwakemask get hostevent" failed with' 181 ' exception: %s', str(e)) 182 183 return 0 184 185 def _is_valid_wake_source(self, wake_source): 186 """Check if |wake_source| is valid for DUT. 187 188 @param wake_source: wake source to verify. 189 @return: False if |wake_source| is not valid for DUT, True otherwise 190 """ 191 if wake_source in ['BASE_ATTACH', 'BASE_DETACH']: 192 return self._ec.has_command('basestate') 193 if wake_source in ['TABLET_MODE_ON', 'TABLET_MODE_OFF']: 194 return self._ec.has_command('tabletmode') 195 if wake_source == 'LID_OPEN': 196 return self._dr_utils.host_has_lid() 197 if wake_source == 'INTERNAL_KB': 198 return self._faft_config.has_keyboard 199 if wake_source == 'USB_KB': 200 # Initialize USB keyboard. 201 self._host.servo.set_nocheck('init_usb_keyboard', 'on') 202 time.sleep(USB_PRESENT_DELAY) 203 # Check if DUT can see a wake capable Atmel USB keyboard. 204 if servo_keyboard_utils.is_servo_usb_keyboard_present( 205 self._host): 206 if servo_keyboard_utils.is_servo_usb_wake_capable( 207 self._host): 208 return True 209 else: 210 logging.warning( 211 'Atmel USB keyboard does not have wake capability.' 212 ' Please run firmware_FlashServoKeyboardMap Autotest ' 213 'to update the Atmel firmware.') 214 return False 215 else: 216 logging.warning( 217 'DUT cannot see a Atmel USB keyboard. ' 218 ' Please plug in USB C charger into Servo if using V4.') 219 220 return False 221 if wake_source in ['AC_CONNECTED', 'AC_DISCONNECTED']: 222 arch = self._host.get_architecture() 223 wake_mask = 0 224 if not self._chg_manager: 225 logging.warning( 226 'Unable to test AC connect/disconnect with this ' 227 'servo setup') 228 return False 229 elif arch.startswith('x86'): 230 wake_mask = self._x86_get_ec_wake_mask() 231 elif arch.startswith('arm'): 232 wake_mask = self._arm_get_ec_wake_mask() 233 234 supported = False 235 if wake_source == 'AC_CONNECTED': 236 supported = wake_mask & chrome_ec.HOSTEVENT_AC_CONNECTED 237 elif wake_source == 'AC_DISCONNECTED': 238 supported = wake_mask & chrome_ec.HOSTEVENT_AC_DISCONNECTED 239 240 if not supported: 241 logging.info( 242 '%s not supported. Platforms launched in 2020 or before' 243 ' may not require it. Wake mask: 0x%x', wake_source, 244 wake_mask) 245 return False 246 247 return True 248 249 def _test_wake(self, wake_source, full_wake): 250 """Test if |wake_source| triggers a full resume. 251 252 @param wake_source: wake source to test. One of |FULL_WAKE_SOURCES|. 253 @return: True, if we are able to successfully test the |wake source| 254 triggers a full wake. 255 """ 256 is_success = True 257 logging.info( 258 'Testing wake by %s triggers a %s wake when dark resume is ' 259 'enabled.', wake_source, 'full' if full_wake else 'dark') 260 if not self._before_suspend(wake_source): 261 logging.error('Before suspend action failed for %s', wake_source) 262 # Still run the _after_resume callback since we can do things like 263 # stop charging. 264 self._after_resume(wake_source) 265 return False 266 267 count_before = self._dr_utils.count_dark_resumes() 268 self._dr_utils.suspend(SECS_FOR_SUSPENDING + RTC_WAKE_SECS) 269 logging.info('DUT suspended! Waiting to resume...') 270 # Wait at least |SECS_FOR_SUSPENDING| secs for the kernel to 271 # fully suspend. 272 time.sleep(SECS_FOR_SUSPENDING) 273 self._trigger_wake(wake_source) 274 275 # Wait until it would be unclear if the RTC or wake_source triggered the 276 # wake. 277 if not self._host.wait_up(timeout=RTC_WAKE_SECS - 1): 278 logging.error( 279 'Device did not resume from suspend for %s.' 280 ' Waking system with power button then RTC.', wake_source) 281 self._trigger_wake('PWR_BTN') 282 self._after_resume(wake_source) 283 if not self._host.is_up(): 284 raise error.TestFail( 285 'Device failed to wakeup from backup wake sources' 286 ' (power button and RTC).') 287 288 return False 289 290 count_after = self._dr_utils.count_dark_resumes() 291 if full_wake: 292 if count_before != count_after: 293 logging.error('%s incorrectly caused a dark resume.', 294 wake_source) 295 is_success = False 296 elif is_success: 297 logging.info('%s caused a full resume.', wake_source) 298 else: 299 if count_before == count_after: 300 logging.error('%s incorrectly caused a full resume.', 301 wake_source) 302 is_success = False 303 elif is_success: 304 logging.info('%s caused a dark resume.', wake_source) 305 306 self._after_resume(wake_source) 307 return is_success 308 309 def _trigger_wake(self, wake_source): 310 """Trigger wake using the given |wake_source|. 311 312 @param wake_source : wake_source that is being tested. 313 One of |FULL_WAKE_SOURCES|. 314 """ 315 if wake_source == 'PWR_BTN': 316 self._host.servo.power_short_press() 317 elif wake_source == 'LID_OPEN': 318 self._host.servo.lid_close() 319 time.sleep(WAIT_TIME_LID_TRANSITION_SECS) 320 self._host.servo.lid_open() 321 elif wake_source == 'BASE_ATTACH': 322 self._force_base_state(BASE_STATE.ATTACH) 323 elif wake_source == 'BASE_DETACH': 324 self._force_base_state(BASE_STATE.DETACH) 325 elif wake_source == 'TABLET_MODE_ON': 326 self._force_tablet_mode(TABLET_MODE.ON) 327 elif wake_source == 'TABLET_MODE_OFF': 328 self._force_tablet_mode(TABLET_MODE.OFF) 329 elif wake_source == 'INTERNAL_KB': 330 self._host.servo.ctrl_key() 331 elif wake_source == 'USB_KB': 332 self._host.servo.set_nocheck('usb_keyboard_enter_key', '10') 333 elif wake_source == 'RTC': 334 # The RTC will wake on its own. We just need to wait 335 time.sleep(RTC_WAKE_SECS) 336 elif wake_source == 'AC_CONNECTED': 337 self._chg_manager.start_charging() 338 elif wake_source == 'AC_DISCONNECTED': 339 self._chg_manager.stop_charging() 340 341 def cleanup(self): 342 """cleanup.""" 343 self._dr_utils.stop_resuspend_on_dark_resume(False) 344 self._dr_utils.teardown() 345 346 def initialize(self, host): 347 """Initialize wake sources tests. 348 349 @param host: Host on which the test will be run. 350 """ 351 self._host = host 352 self._dr_utils = DarkResumeUtils(host) 353 self._dr_utils.stop_resuspend_on_dark_resume() 354 self._ec = chrome_ec.ChromeEC(self._host.servo) 355 self._faft_config = FAFTConfig(self._host.get_platform()) 356 self._kstr = host.get_kernel_version() 357 # TODO(b/168939843) : Look at implementing AC plug/unplug w/ non-PD RPMs 358 # in the lab. 359 try: 360 self._chg_manager = servo_charger.ServoV4ChargeManager( 361 host, host.servo) 362 except error.TestNAError: 363 logging.warning('Servo does not support AC switching.') 364 self._chg_manager = None 365 366 def run_once(self): 367 """Body of the test.""" 368 369 test_ws = set( 370 ws for ws in FULL_WAKE_SOURCES if self._is_valid_wake_source(ws)) 371 passed_ws = set(ws for ws in test_ws if self._test_wake(ws, True)) 372 failed_ws = test_ws.difference(passed_ws) 373 skipped_ws = set(FULL_WAKE_SOURCES).difference(test_ws) 374 375 test_dark_ws = set(ws for ws in DARK_RESUME_SOURCES 376 if self._is_valid_wake_source(ws)) 377 skipped_ws.update(set(DARK_RESUME_SOURCES).difference(test_dark_ws)) 378 for ws in test_dark_ws: 379 if self._test_wake(ws, False): 380 passed_ws.add(ws) 381 else: 382 failed_ws.add(ws) 383 384 test_keyval = {} 385 386 for ws in passed_ws: 387 test_keyval.update({ws: 'PASS'}) 388 for ws in failed_ws: 389 test_keyval.update({ws: 'FAIL'}) 390 for ws in skipped_ws: 391 test_keyval.update({ws: 'SKIPPED'}) 392 self.write_test_keyval(test_keyval) 393 394 if passed_ws: 395 logging.info('[%s] woke the device as expected.', 396 ''.join(str(elem) + ', ' for elem in passed_ws)) 397 398 if skipped_ws: 399 logging.info( 400 '[%s] are not wake sources on this platform. ' 401 'Please test manually if not the case.', 402 ''.join(str(elem) + ', ' for elem in skipped_ws)) 403 404 if failed_ws: 405 raise error.TestFail( 406 '[%s] wake sources did not behave as expected.' % 407 (''.join(str(elem) + ', ' for elem in failed_ws))) 408