xref: /aosp_15_r20/external/autotest/server/cros/power/servo_charger.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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