1*9c5db199SXin Li# Lint as: python2, python3 2*9c5db199SXin Li# Copyright 2018 The Chromium OS Authors. All rights reserved. 3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be 4*9c5db199SXin Li# found in the LICENSE file. 5*9c5db199SXin Li 6*9c5db199SXin Li"""Helper class for managing charging the DUT with Servo v4.""" 7*9c5db199SXin Li 8*9c5db199SXin Lifrom __future__ import absolute_import 9*9c5db199SXin Lifrom __future__ import division 10*9c5db199SXin Lifrom __future__ import print_function 11*9c5db199SXin Li 12*9c5db199SXin Liimport logging 13*9c5db199SXin Lifrom six.moves import range 14*9c5db199SXin Liimport time 15*9c5db199SXin Li 16*9c5db199SXin Lifrom autotest_lib.client.common_lib import error 17*9c5db199SXin Lifrom autotest_lib.client.common_lib.cros import retry 18*9c5db199SXin Li 19*9c5db199SXin Li# Base delay time in seconds for Servo role change and PD negotiation. 20*9c5db199SXin Li_DELAY_SEC = 0.1 21*9c5db199SXin Li# Total delay time in minutes for Servo role change and PD negotiation. 22*9c5db199SXin Li_TIMEOUT_MIN = 0.3 23*9c5db199SXin Li# Exponential backoff for Servo role change and PD negotiation. 24*9c5db199SXin Li_BACKOFF = 2 25*9c5db199SXin Li# Number of attempts to recover Servo v4. 26*9c5db199SXin Li_RETRYS = 3 27*9c5db199SXin Li# Seconds to wait after resetting the role on a recovery attempt 28*9c5db199SXin Li# before trying to set it to the intended role again. 29*9c5db199SXin Li_RECOVERY_WAIT_SEC = 1 30*9c5db199SXin Li# Delay to wait before polling whether the role as been changed successfully. 31*9c5db199SXin Li_ROLE_SETTLING_DELAY_SEC = 1 32*9c5db199SXin Li# Timeout in minutes to attempt checking AC information over ssh. 33*9c5db199SXin Li# Ethernet connection through the v4 flickers on role change. The usb 34*9c5db199SXin Li# adapter needs to reenumerate and the DUT reconnect before information can be 35*9c5db199SXin Li# queried. This delay has proven sufficient to overcome this in the current 36*9c5db199SXin Li# implementation. 37*9c5db199SXin Li_ETH_REENUMERATE_TIMEOUT_MIN = 1 38*9c5db199SXin Li 39*9c5db199SXin Li 40*9c5db199SXin Lidef _invert_role(role): 41*9c5db199SXin Li """Helper to invert the role. 42*9c5db199SXin Li 43*9c5db199SXin Li @param role: role to invert 44*9c5db199SXin Li 45*9c5db199SXin Li @returns: 46*9c5db199SXin Li 'src' if |role| is 'snk' 47*9c5db199SXin Li 'snk' if |role| is 'src' 48*9c5db199SXin Li """ 49*9c5db199SXin Li return 'src' if role == 'snk' else 'snk' 50*9c5db199SXin Li 51*9c5db199SXin Liclass ServoV4ChargeManager(object): 52*9c5db199SXin Li """A helper class for managing charging the DUT with Servo v4.""" 53*9c5db199SXin Li 54*9c5db199SXin Li def __init__(self, host, servo): 55*9c5db199SXin Li """Check for correct Servo setup. 56*9c5db199SXin Li 57*9c5db199SXin Li Make sure that Servo is v4 and can manage charging. Make sure that DUT 58*9c5db199SXin Li responds to Servo charging commands. Restore Servo v4 power role after 59*9c5db199SXin Li confidence check. 60*9c5db199SXin Li 61*9c5db199SXin Li @param host: CrosHost object representing the DUT or None. 62*9c5db199SXin Li If host is None, then the is_ac_connected check on the 63*9c5db199SXin Li host object is skipped. 64*9c5db199SXin Li @param servo: a proxy for servod. 65*9c5db199SXin Li """ 66*9c5db199SXin Li super(ServoV4ChargeManager, self).__init__() 67*9c5db199SXin Li self._host = host 68*9c5db199SXin Li self._servo = servo 69*9c5db199SXin Li if not self._servo.supports_built_in_pd_control(): 70*9c5db199SXin Li raise error.TestNAError('Servo setup does not support PD control. ' 71*9c5db199SXin Li 'Check logs for details.') 72*9c5db199SXin Li 73*9c5db199SXin Li self._original_role = self._servo.get('servo_pd_role') 74*9c5db199SXin Li if self._original_role == 'snk': 75*9c5db199SXin Li self.start_charging() 76*9c5db199SXin Li self.stop_charging() 77*9c5db199SXin Li elif self._original_role == 'src': 78*9c5db199SXin Li self.stop_charging() 79*9c5db199SXin Li self.start_charging() 80*9c5db199SXin Li else: 81*9c5db199SXin Li raise error.TestNAError('Unrecognized Servo v4 power role: %s.' % 82*9c5db199SXin Li self._original_role) 83*9c5db199SXin Li 84*9c5db199SXin Li # TODO(b/129882930): once both sides are stable, remove the _retry_wrapper 85*9c5db199SXin Li # wrappers as they aren't needed anymore. The current motivation for the 86*9c5db199SXin Li # retry loop in the autotest framework is to have a 'stable' library i.e. 87*9c5db199SXin Li # retries but also a mechanism and and easy to remove bridge once the bug 88*9c5db199SXin Li # is fixed, and we don't require the bandaid anymore. 89*9c5db199SXin Li 90*9c5db199SXin Li def _retry_wrapper(self, role, verify): 91*9c5db199SXin Li """Try up to |_RETRYS| times to set the v4 to |role|. 92*9c5db199SXin Li 93*9c5db199SXin Li @param role: string 'src' or 'snk'. If 'src' connect DUT to AC power; if 94*9c5db199SXin Li 'snk' disconnect DUT from AC power. 95*9c5db199SXin Li @param verify: bool to verify that charging started/stopped. 96*9c5db199SXin Li 97*9c5db199SXin Li @returns: number of retries needed for success 98*9c5db199SXin Li """ 99*9c5db199SXin Li for retry in range(_RETRYS): 100*9c5db199SXin Li try: 101*9c5db199SXin Li self._change_role(role, verify) 102*9c5db199SXin Li return retry 103*9c5db199SXin Li except error.TestError as e: 104*9c5db199SXin Li if retry < _RETRYS - 1: 105*9c5db199SXin Li # Ensure this retry loop and logging isn't run on the 106*9c5db199SXin Li # last iteration. 107*9c5db199SXin Li logging.warning('Failed to set to %s %d times. %s ' 108*9c5db199SXin Li 'Trying to cycle through %s to ' 109*9c5db199SXin Li 'recover.', role, retry + 1, str(e), 110*9c5db199SXin Li _invert_role(role)) 111*9c5db199SXin Li # Cycle through the other state before retrying. Do not 112*9c5db199SXin Li # verify as this is strictly a recovery mechanism - sleep 113*9c5db199SXin Li # instead. 114*9c5db199SXin Li self._change_role(_invert_role(role), verify=False) 115*9c5db199SXin Li time.sleep(_RECOVERY_WAIT_SEC) 116*9c5db199SXin Li logging.error('Giving up on %s.', role) 117*9c5db199SXin Li raise e 118*9c5db199SXin Li 119*9c5db199SXin Li def stop_charging(self, verify=True): 120*9c5db199SXin Li """Cut off AC power supply to DUT with Servo. 121*9c5db199SXin Li 122*9c5db199SXin Li @param verify: whether to verify that charging stopped. 123*9c5db199SXin Li 124*9c5db199SXin Li @returns: number of retries needed for success 125*9c5db199SXin Li """ 126*9c5db199SXin Li return self._retry_wrapper('snk', verify) 127*9c5db199SXin Li 128*9c5db199SXin Li def start_charging(self, verify=True): 129*9c5db199SXin Li """Connect AC power supply to DUT with Servo. 130*9c5db199SXin Li 131*9c5db199SXin Li @param verify: whether to verify that charging started. 132*9c5db199SXin Li 133*9c5db199SXin Li @returns: number of retries needed for success 134*9c5db199SXin Li """ 135*9c5db199SXin Li return self._retry_wrapper('src', verify) 136*9c5db199SXin Li 137*9c5db199SXin Li def restore_original_setting(self, verify=True): 138*9c5db199SXin Li """Restore Servo to original charging setting. 139*9c5db199SXin Li 140*9c5db199SXin Li @param verify: whether to verify that original role was restored. 141*9c5db199SXin Li """ 142*9c5db199SXin Li self._retry_wrapper(self._original_role, verify) 143*9c5db199SXin Li 144*9c5db199SXin Li def _change_role(self, role, verify=True): 145*9c5db199SXin Li """Change Servo PD role and check if DUT responded accordingly. 146*9c5db199SXin Li 147*9c5db199SXin Li @param role: string 'src' or 'snk'. If 'src' connect DUT to AC power; if 148*9c5db199SXin Li 'snk' disconnect DUT from AC power. 149*9c5db199SXin Li @param verify: bool to verify that charging started/stopped. 150*9c5db199SXin Li 151*9c5db199SXin Li @raises error.TestError: if the role did not change successfully. 152*9c5db199SXin Li """ 153*9c5db199SXin Li self._servo.set_nocheck('servo_pd_role', role) 154*9c5db199SXin Li # Sometimes the role reverts quickly. Add a short delay to let the new 155*9c5db199SXin Li # role stabilize. 156*9c5db199SXin Li time.sleep(_ROLE_SETTLING_DELAY_SEC) 157*9c5db199SXin Li 158*9c5db199SXin Li if not verify: 159*9c5db199SXin Li return 160*9c5db199SXin Li 161*9c5db199SXin Li @retry.retry(error.TestError, timeout_min=_TIMEOUT_MIN, 162*9c5db199SXin Li delay_sec=_DELAY_SEC, backoff=_BACKOFF) 163*9c5db199SXin Li def check_servo_role(role): 164*9c5db199SXin Li """Check if servo role is as expected, if not, retry.""" 165*9c5db199SXin Li if self._servo.get('servo_pd_role') != role: 166*9c5db199SXin Li raise error.TestError('Servo v4 failed to set its PD role to ' 167*9c5db199SXin Li '%s.' % role) 168*9c5db199SXin Li check_servo_role(role) 169*9c5db199SXin Li 170*9c5db199SXin Li connected = True if role == 'src' else False 171*9c5db199SXin Li 172*9c5db199SXin Li @retry.retry(error.TestError, timeout_min=_TIMEOUT_MIN, 173*9c5db199SXin Li delay_sec=_DELAY_SEC, backoff=_BACKOFF) 174*9c5db199SXin Li def check_ac_connected(connected): 175*9c5db199SXin Li """Check if the EC believes an AC charger is connected.""" 176*9c5db199SXin Li if not self._servo.has_control('charger_connected'): 177*9c5db199SXin Li # TODO(coconutruben): remove this check once labs have the 178*9c5db199SXin Li # latest hdctools with the required control. 179*9c5db199SXin Li logging.warning('Could not verify %r control as the ' 180*9c5db199SXin Li 'control is not available on servod.', 181*9c5db199SXin Li 'charger_connected') 182*9c5db199SXin Li return 183*9c5db199SXin Li ec_opinion = self._servo.get('charger_connected') 184*9c5db199SXin Li if ec_opinion != connected: 185*9c5db199SXin Li str_lookup = {True: 'connected', False: 'disconnected'} 186*9c5db199SXin Li msg = ('EC thinks charger is %s but it should be %s.' 187*9c5db199SXin Li % (str_lookup[ec_opinion], 188*9c5db199SXin Li str_lookup[connected])) 189*9c5db199SXin Li raise error.TestError(msg) 190*9c5db199SXin Li 191*9c5db199SXin Li check_ac_connected(connected) 192*9c5db199SXin Li 193*9c5db199SXin Li @retry.retry(error.TestError, timeout_min=_ETH_REENUMERATE_TIMEOUT_MIN, 194*9c5db199SXin Li delay_sec=_DELAY_SEC, backoff=_BACKOFF) 195*9c5db199SXin Li def check_host_ac(connected): 196*9c5db199SXin Li """Check if DUT AC power is as expected, if not, retry.""" 197*9c5db199SXin Li if self._host.is_ac_connected() != connected: 198*9c5db199SXin Li intent = 'connect' if connected else 'disconnect' 199*9c5db199SXin Li raise error.TestError('DUT failed to %s AC power.'% intent) 200*9c5db199SXin Li 201*9c5db199SXin Li if self._host and self._host.is_up_fast(): 202*9c5db199SXin Li # If the DUT has been charging in S3/S5/G3, cannot verify. 203*9c5db199SXin Li check_host_ac(connected) 204