# Copyright 2018 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. import logging import time from autotest_lib.client.common_lib import autotest_enum, error from autotest_lib.server import test from autotest_lib.server.cros import servo_keyboard_utils from autotest_lib.server.cros.dark_resume_utils import DarkResumeUtils from autotest_lib.server.cros.faft.utils.config import Config as FAFTConfig from autotest_lib.server.cros.power import servo_charger from autotest_lib.server.cros.servo import chrome_ec # Possible states base can be forced into. BASE_STATE = autotest_enum.AutotestEnum('ATTACH', 'DETACH', 'RESET') # Possible states for tablet mode as defined in common/tablet_mode.c via # crrev.com/c/1797370. TABLET_MODE = autotest_enum.AutotestEnum('ON', 'OFF', 'RESET') # List of wake sources expected to cause a full resume. FULL_WAKE_SOURCES = [ 'PWR_BTN', 'LID_OPEN', 'BASE_ATTACH', 'BASE_DETACH', 'INTERNAL_KB', 'USB_KB', 'TABLET_MODE_ON', 'TABLET_MODE_OFF' ] # List of wake sources expected to cause a dark resume. DARK_RESUME_SOURCES = ['RTC', 'AC_CONNECTED', 'AC_DISCONNECTED'] # Max time taken by the device to resume. This gives enough time for the device # to establish network connection with the autotest server SECS_FOR_RESUMING = 15 # Time in future after which RTC goes off when testing wake due to RTC alarm. RTC_WAKE_SECS = 20 # Max time taken by the device to suspend. This includes the time powerd takes # trigger the suspend after receiving the suspend request from autotest script. SECS_FOR_SUSPENDING = 20 # Time to allow lid transition to take effect. WAIT_TIME_LID_TRANSITION_SECS = 5 # Time to wait for the DUT to see USB keyboard after restting the Atmega USB # emulator on Servo. USB_PRESENT_DELAY = 1 class power_WakeSources(test.test): """ Verify that wakes from input devices can trigger a full resume. Currently tests : 1. power button 2. lid open 3. base attach 4. base detach Also tests that dark resume wake sources work as expected, such as: 1. RTC 2. AC_CONNECTED 3. AC_DISCONNECTED """ version = 1 def _after_resume(self, wake_source): """Cleanup to perform after resuming the device. @param wake_source: Wake source that has been tested. """ if wake_source in ['BASE_ATTACH', 'BASE_DETACH']: self._force_base_state(BASE_STATE.RESET) elif wake_source in ['TABLET_MODE_ON', 'TABLET_MODE_OFF']: self._force_tablet_mode(TABLET_MODE.RESET) elif wake_source in ['AC_CONNECTED', 'AC_DISCONNECTED']: self._chg_manager.start_charging() def _before_suspend(self, wake_source): """Prep before suspend. @param wake_source: Wake source that is going to be tested. @return: Boolean, whether _before_suspend action is successful. """ if wake_source == 'BASE_ATTACH': # Force detach before suspend so that attach won't be ignored. self._force_base_state(BASE_STATE.DETACH) elif wake_source == 'BASE_DETACH': # Force attach before suspend so that detach won't be ignored. self._force_base_state(BASE_STATE.ATTACH) elif wake_source == 'LID_OPEN': # Set the power policy for lid closed action to suspend. return self._host.run( 'set_power_policy --lid_closed_action suspend', ignore_status=True).exit_status == 0 elif wake_source == 'USB_KB': # Initialize USB keyboard. self._host.servo.set_nocheck('init_usb_keyboard', 'on') elif wake_source == 'TABLET_MODE_ON': self._force_tablet_mode(TABLET_MODE.OFF) elif wake_source == 'TABLET_MODE_OFF': self._force_tablet_mode(TABLET_MODE.ON) elif wake_source == 'AC_CONNECTED': self._chg_manager.stop_charging() elif wake_source == 'AC_DISCONNECTED': self._chg_manager.start_charging() return True def _force_tablet_mode(self, mode): """Send EC command to force the tablet mode. @param mode: mode to force. One of the |TABLET_MODE| enum. """ ec_cmd = 'tabletmode ' ec_arg = { TABLET_MODE.ON: 'on', TABLET_MODE.OFF: 'off', TABLET_MODE.RESET: 'r' } ec_cmd += ec_arg[mode] self._ec.send_command(ec_cmd) def _force_base_state(self, base_state): """Send EC command to force the |base_state|. @param base_state: State to force base to. One of |BASE_STATE| enum. """ ec_cmd = 'basestate ' ec_arg = { BASE_STATE.ATTACH: 'a', BASE_STATE.DETACH: 'd', BASE_STATE.RESET: 'r' } ec_cmd += ec_arg[base_state] self._ec.send_command(ec_cmd) def _is_valid_wake_source(self, wake_source): """Check if |wake_source| is valid for DUT. @param wake_source: wake source to verify. @return: False if |wake_source| is not valid for DUT, True otherwise """ if wake_source in ['BASE_ATTACH', 'BASE_DETACH']: return self._ec.has_command('basestate') if wake_source in ['TABLET_MODE_ON', 'TABLET_MODE_OFF']: return self._ec.has_command('tabletmode') if wake_source == 'LID_OPEN': return self._dr_utils.host_has_lid() if wake_source == 'INTERNAL_KB': return self._faft_config.has_keyboard if wake_source == 'USB_KB': # Initialize USB keyboard. self._host.servo.set_nocheck('init_usb_keyboard', 'on') time.sleep(USB_PRESENT_DELAY) # Check if DUT can see a wake capable Atmel USB keyboard. if servo_keyboard_utils.is_servo_usb_keyboard_present( self._host): if servo_keyboard_utils.is_servo_usb_wake_capable( self._host): return True else: logging.warning( 'Atmel USB keyboard does not have wake capability.' ' Please run firmware_FlashServoKeyboardMap Autotest ' 'to update the Atmel firmware.') return False else: logging.warning( 'DUT cannot see a Atmel USB keyboard. ' ' Please plug in USB C charger into Servo if using V4.') return False if wake_source in ['AC_CONNECTED', 'AC_DISCONNECTED']: if not self._chg_manager: logging.warning( 'Unable to test AC connect/disconnect with this ' 'servo setup') return False # Check both the S0ix and S3 wake masks. try: s0ix_wake_mask = int(self._host.run( 'ectool hostevent get %d' % chrome_ec.EC_HOST_EVENT_LAZY_WAKE_MASK_S0IX).stdout, base=16) except error.AutoservRunError as e: s0ix_wake_mask = 0 logging.info( '"ectool hostevent get" failed for s0ix wake mask with' ' exception: %s', str(e)) try: s3_wake_mask = int(self._host.run( 'ectool hostevent get %d' % chrome_ec.EC_HOST_EVENT_LAZY_WAKE_MASK_S3).stdout, base=16) except error.AutoservRunError as e: s3_wake_mask = 0 logging.info( '"ectool hostevent get" failed for s3 wake mask with' ' exception: %s', str(e)) wake_mask = s0ix_wake_mask | s3_wake_mask supported = False if wake_source == 'AC_CONNECTED': supported = wake_mask & chrome_ec.HOSTEVENT_AC_CONNECTED elif wake_source == 'AC_DISCONNECTED': supported = wake_mask & chrome_ec.HOSTEVENT_AC_DISCONNECTED if not supported: logging.info( '%s not supported. Platforms launched in 2020 or before' ' may not require it. S0ix wake mask: 0x%x S3 wake' ' mask: 0x%x', wake_source, s0ix_wake_mask, s3_wake_mask) return False return True def _test_wake(self, wake_source, full_wake): """Test if |wake_source| triggers a full resume. @param wake_source: wake source to test. One of |FULL_WAKE_SOURCES|. @return: True, if we are able to successfully test the |wake source| triggers a full wake. """ is_success = True logging.info( 'Testing wake by %s triggers a ' 'full wake when dark resume is enabled.', wake_source) if not self._before_suspend(wake_source): logging.error('Before suspend action failed for %s', wake_source) # Still run the _after_resume callback since we can do things like # stop charging. self._after_resume(wake_source) return False count_before = self._dr_utils.count_dark_resumes() self._dr_utils.suspend(SECS_FOR_SUSPENDING + RTC_WAKE_SECS) logging.info('DUT suspended! Waiting to resume...') # Wait at least |SECS_FOR_SUSPENDING| secs for the kernel to # fully suspend. time.sleep(SECS_FOR_SUSPENDING) self._trigger_wake(wake_source) # Wait at least |SECS_FOR_RESUMING| secs for the device to # resume. time.sleep(SECS_FOR_RESUMING) if not self._host.is_up_fast(): logging.error( 'Device did not resume from suspend for %s.' ' Waking system with power button then RTC.', wake_source) self._trigger_wake('PWR_BTN') self._after_resume(wake_source) if not self._host.is_up(): raise error.TestFail( 'Device failed to wakeup from backup wake sources' ' (power button and RTC).') return False count_after = self._dr_utils.count_dark_resumes() if full_wake: if count_before != count_after: logging.error('%s incorrectly caused a dark resume.', wake_source) is_success = False elif is_success: logging.info('%s caused a full resume.', wake_source) else: if count_before == count_after: logging.error('%s incorrectly caused a full resume.', wake_source) is_success = False elif is_success: logging.info('%s caused a dark resume.', wake_source) self._after_resume(wake_source) return is_success def _trigger_wake(self, wake_source): """Trigger wake using the given |wake_source|. @param wake_source : wake_source that is being tested. One of |FULL_WAKE_SOURCES|. """ if wake_source == 'PWR_BTN': self._host.servo.power_short_press() elif wake_source == 'LID_OPEN': self._host.servo.lid_close() time.sleep(WAIT_TIME_LID_TRANSITION_SECS) self._host.servo.lid_open() elif wake_source == 'BASE_ATTACH': self._force_base_state(BASE_STATE.ATTACH) elif wake_source == 'BASE_DETACH': self._force_base_state(BASE_STATE.DETACH) elif wake_source == 'TABLET_MODE_ON': self._force_tablet_mode(TABLET_MODE.ON) elif wake_source == 'TABLET_MODE_OFF': self._force_tablet_mode(TABLET_MODE.OFF) elif wake_source == 'INTERNAL_KB': self._host.servo.ctrl_key() elif wake_source == 'USB_KB': self._host.servo.set_nocheck('usb_keyboard_enter_key', '10') elif wake_source == 'RTC': # The RTC will wake on its own. We just need to wait time.sleep(RTC_WAKE_SECS) elif wake_source == 'AC_CONNECTED': self._chg_manager.start_charging() elif wake_source == 'AC_DISCONNECTED': self._chg_manager.stop_charging() def cleanup(self): """cleanup.""" self._dr_utils.stop_resuspend_on_dark_resume(False) self._dr_utils.teardown() def initialize(self, host): """Initialize wake sources tests. @param host: Host on which the test will be run. """ self._host = host self._dr_utils = DarkResumeUtils(host) self._dr_utils.stop_resuspend_on_dark_resume() self._ec = chrome_ec.ChromeEC(self._host.servo) self._faft_config = FAFTConfig(self._host.get_platform()) self._kstr = host.get_kernel_version() # TODO(b/168939843) : Look at implementing AC plug/unplug w/ non-PD RPMs # in the lab. try: self._chg_manager = servo_charger.ServoV4ChargeManager( host, host.servo) except error.TestNAError: logging.warning('Servo does not support AC switching.') self._chg_manager = None def run_once(self): """Body of the test.""" test_ws = set( ws for ws in FULL_WAKE_SOURCES if self._is_valid_wake_source(ws)) passed_ws = set(ws for ws in test_ws if self._test_wake(ws, True)) failed_ws = test_ws.difference(passed_ws) skipped_ws = set(FULL_WAKE_SOURCES).difference(test_ws) test_dark_ws = set(ws for ws in DARK_RESUME_SOURCES if self._is_valid_wake_source(ws)) skipped_ws.update(set(DARK_RESUME_SOURCES).difference(test_dark_ws)) for ws in test_dark_ws: if self._test_wake(ws, False): passed_ws.add(ws) else: failed_ws.add(ws) test_keyval = {} for ws in passed_ws: test_keyval.update({ws: 'PASS'}) for ws in failed_ws: test_keyval.update({ws: 'FAIL'}) for ws in skipped_ws: test_keyval.update({ws: 'SKIPPED'}) self.write_test_keyval(test_keyval) if passed_ws: logging.info('[%s] woke the device as expected.', ''.join(str(elem) + ', ' for elem in passed_ws)) if skipped_ws: logging.info( '[%s] are not wake sources on this platform. ' 'Please test manually if not the case.', ''.join(str(elem) + ', ' for elem in skipped_ws)) if failed_ws: raise error.TestFail( '[%s] wake sources did not behave as expected.' % (''.join(str(elem) + ', ' for elem in failed_ws)))