xref: /aosp_15_r20/external/autotest/server/site_tests/firmware_ECCharging/firmware_ECCharging.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1# Copyright (c) 2012 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
5import logging
6import time
7from xml.parsers import expat
8
9from autotest_lib.client.common_lib import error
10from autotest_lib.server.cros.faft.firmware_test import FirmwareTest
11from autotest_lib.server.cros.servo import servo
12
13
14class firmware_ECCharging(FirmwareTest):
15    """
16    Servo based EC charging control test.
17    """
18    version = 1
19
20    # Flags set by battery
21    BATT_FLAG_WANT_CHARGE = 0x1
22    STATUS_FULLY_CHARGED = 0x20
23
24    # Threshold of trickle charging current in mA
25    TRICKLE_CHARGE_THRESHOLD = 100
26
27    # We wait for up to 60 minutes for the battery to allow charging.
28    # kodama in particular takes a long time to discharge
29    DISCHARGE_TIMEOUT = 60 * 60
30
31    # The period to check battery state while discharging.
32    CHECK_BATT_STATE_WAIT = 60
33
34    # The delay to wait for the AC state to update.
35    AC_STATE_UPDATE_DELAY = 3
36
37    # Wait a few seconds after discharging for voltage to stabilize
38    BEGIN_CHARGING_TIMEOUT = 120
39
40    # Sleep for a second between retries when waiting for voltage to stabilize
41    BEGIN_CHARGING_RETRY_TIME = 1
42
43    # After the battery reports it is not full, keep discharging for this long.
44    # This should be >= BEGIN_CHARGING_TIMEOUT
45    EXTRA_DISCHARGE_TIME = BEGIN_CHARGING_TIMEOUT + 30
46
47    def initialize(self, host, cmdline_args):
48        super(firmware_ECCharging, self).initialize(host, cmdline_args)
49        # Don't bother if there is no Chrome EC.
50        if not self.check_ec_capability():
51            raise error.TestNAError(
52                    "Nothing needs to be tested on this device")
53        # Only run in normal mode
54        self.switcher.setup_mode('normal')
55        self.ec.send_command("chan 0")
56
57    def cleanup(self):
58        try:
59            self.ec.send_command("chan 0xffffffff")
60        except Exception as e:
61            logging.error("Caught exception: %s", str(e))
62        super(firmware_ECCharging, self).cleanup()
63
64    def _retry_send_cmd(self, command, regex_list):
65        """Send an EC command, and retry if it fails."""
66        retries = 3
67        while retries > 0:
68            retries -= 1
69            try:
70                return self.ec.send_command_get_output(command, regex_list)
71            except (servo.UnresponsiveConsoleError,
72                    servo.ResponsiveConsoleError, expat.ExpatError) as e:
73                if retries <= 0:
74                    raise
75                logging.warning('Failed to send EC cmd. %s', e)
76
77    def _get_charge_state(self):
78        """Get charger and battery information in a single call."""
79        output = self._retry_send_cmd("chgstate", [
80                r"chg\.\*:",
81                r"voltage = (-?\d+)mV",
82                r"current = (-?\d+)mA",
83                r"batt\.\*:",
84                r"voltage = (-?\d+)mV",
85                r"current = (-?\d+)mA",
86                r"desired_voltage = (-?\d+)mV",
87                r"desired_current = (-?\d+)mA",
88        ])
89        result = {
90                "charger_target_voltage": int(output[1][1]),
91                "charger_target_current": int(output[2][1]),
92                "battery_actual_voltage": int(output[4][1]),
93                "battery_actual_current": int(output[5][1]),
94                "battery_desired_voltage": int(output[6][1]),
95                "battery_desired_current": int(output[7][1]),
96        }
97        logging.info("Charger & battery info: %s", result)
98        return result
99
100    def _get_trickle_charging(self):
101        """Check if we are trickle charging battery."""
102        return (self.ec.get_battery_desired_current() <
103                self.TRICKLE_CHARGE_THRESHOLD)
104
105    def _check_voltages_and_currents(self):
106        """Check that the battery and charger voltages and currents are within
107        acceptable limits.
108
109        Raise:
110          error.TestFail: Raised when check fails.
111        """
112        state = self._get_charge_state()
113        target_voltage = state['charger_target_voltage']
114        desired_voltage = state['battery_desired_voltage']
115        target_current = state['charger_target_current']
116        desired_current = state['battery_desired_current']
117        actual_voltage = state['battery_actual_voltage']
118        actual_current = state['battery_actual_current']
119        logging.info("Checking charger target values...")
120        if (target_voltage >= 1.05 * desired_voltage):
121            raise error.TestFail(
122                    "Charger target voltage is too high. %d/%d=%f" %
123                    (target_voltage, desired_voltage,
124                     float(target_voltage) / desired_voltage))
125        if (target_current >= 1.05 * desired_current):
126            raise error.TestFail(
127                    "Charger target current is too high. %d/%d=%f" %
128                    (target_current, desired_current,
129                     float(target_current) / desired_current))
130
131        logging.info("Checking battery actual values...")
132        if (actual_voltage >= 1.05 * target_voltage):
133            raise error.TestFail(
134                    "Battery actual voltage is too high. %d/%d=%f" %
135                    (actual_voltage, target_voltage,
136                     float(actual_voltage) / target_voltage))
137        if (actual_current >= 1.05 * target_current):
138            raise error.TestFail(
139                    "Battery actual current is too high. %d/%d=%f" %
140                    (actual_current, target_current,
141                     float(actual_current) / target_current))
142
143    def _check_if_discharge_on_ac(self):
144        """Check if DUT is performing discharge on AC"""
145        match = self._retry_send_cmd("battery", [
146                r"Status:\s*(0x[0-9a-f]+)\s", r"Param flags:\s*([0-9a-f]+)\s"
147        ])
148        status = int(match[0][1], 16)
149        params = int(match[1][1], 16)
150
151        if (not (params & self.BATT_FLAG_WANT_CHARGE) and
152                (status & self.STATUS_FULLY_CHARGED)):
153            return True
154
155        return False
156
157    def _check_battery_discharging(self):
158        """Check if AC is attached and if charge control is normal."""
159        # chg_ctl_mode may look like: chg_ctl_mode = 2
160        # or: chg_ctl_mode = DISCHARGE (2)
161        # The regex needs to match either one.
162        output = self._retry_send_cmd("chgstate", [
163                r"ac\s*=\s*(\d)\s*",
164                r"chg_ctl_mode\s*=\s*(\S* \(\d+\)|\d+)\r\n"
165        ])
166        ac_state = int(output[0][1])
167        chg_ctl_mode = output[1][1]
168        if ac_state == 0:
169            return True
170        if chg_ctl_mode == "2" or chg_ctl_mode == "DISCHARGE (2)":
171            return True
172        return False
173
174    def _set_battery_discharge(self):
175        """Instruct the EC to drain the battery."""
176        # Ask EC to drain the battery
177        output = self._retry_send_cmd("chgstate discharge on", [
178                r"state =|Parameter 1 invalid",
179        ])
180        logging.debug("chgstate returned %s", output)
181        if output[0] == 'Parameter 1 invalid':
182            raise error.TestNAError(
183                    "Device doesn't support CHARGER_DISCHARGE_ON_AC, "
184                    "please drain battery below full and run the test again.")
185        time.sleep(self.AC_STATE_UPDATE_DELAY)
186
187        # Verify discharging. Either AC off or charge control discharge is
188        # good.
189        if not self._check_battery_discharging():
190            raise error.TestFail("Battery is not discharging.")
191
192    def _set_battery_normal(self):
193        """Instruct the EC to charge the battery as normal."""
194        self.ec.send_command("chgstate discharge off")
195        time.sleep(self.AC_STATE_UPDATE_DELAY)
196
197        # Verify AC is on and charge control is normal.
198        if self._check_battery_discharging():
199            raise error.TestFail("Fail to plug AC and enable charging.")
200        self.ec.update_battery_info()
201
202    def _consume_battery(self, deadline):
203        """Perform battery intensive operation to make the battery discharge
204        faster."""
205        # Switch to servo drain after b/140965614.
206        stress_time = deadline - time.time()
207        if stress_time > self.CHECK_BATT_STATE_WAIT:
208            stress_time = self.CHECK_BATT_STATE_WAIT
209        self._client.run("stressapptest -s %d " % stress_time,
210                         ignore_status=True)
211
212    def _discharge_below_100(self):
213        """Remove AC power until the battery is not full."""
214        self._set_battery_discharge()
215        logging.info(
216                "Keep discharging until the battery reports charging allowed.")
217
218        try:
219            # Wait until DISCHARGE_TIMEOUT or charging allowed
220            deadline = time.time() + self.DISCHARGE_TIMEOUT
221            while time.time() < deadline:
222                self.ec.update_battery_info()
223                if self.ec.get_battery_charging_allowed():
224                    break
225                logging.info("Wait for the battery to discharge (%d mAh).",
226                             self.ec.get_battery_remaining())
227                self._consume_battery(deadline)
228            else:
229                raise error.TestFail(
230                        "The battery does not report charging allowed "
231                        "before timeout is reached.")
232
233            # Wait another EXTRA_DISCHARGE_TIME just to be sure
234            deadline = time.time() + self.EXTRA_DISCHARGE_TIME
235            while time.time() < deadline:
236                self.ec.update_battery_info()
237                logging.info(
238                        "Wait for the battery to discharge even more (%d mAh).",
239                        self.ec.get_battery_remaining())
240                self._consume_battery(deadline)
241        finally:
242            self._set_battery_normal()
243
244        # For many devices, it takes some time after discharging for the
245        # battery to actually start charging.
246        deadline = time.time() + self.BEGIN_CHARGING_TIMEOUT
247        while time.time() < deadline:
248            self.ec.update_battery_info()
249            if self.ec.get_battery_actual_current() >= 0:
250                break
251            logging.info(
252                    'Battery actual current (%d) too low, wait a bit. (%d mAh)',
253                    self.ec.get_battery_actual_current(),
254                    self.ec.get_battery_remaining())
255            self._consume_battery(deadline)
256
257    def run_once(self):
258        """Execute the main body of the test.
259        """
260        if not self.check_ec_capability(['battery', 'charging']):
261            raise error.TestNAError(
262                    "Nothing needs to be tested on this device")
263        if not self.ec.get_battery_charging_allowed(
264        ) or self.ec.get_battery_actual_current() < 0:
265            logging.info(
266                    "Battery is full or discharging. Forcing battery discharge "
267                    "to test charging.")
268            self._discharge_below_100()
269            if not self.ec.get_battery_charging_allowed():
270                raise error.TestFail(
271                        "Battery reports charging is not allowed, even after "
272                        "discharging.")
273        if self._check_if_discharge_on_ac():
274            raise error.TestNAError(
275                    "DUT is performing discharge on AC. Unable to test.")
276        if self._get_trickle_charging():
277            raise error.TestNAError(
278                    "Trickling charging battery. Unable to test.")
279        if self.ec.get_battery_actual_current() < 0:
280            raise error.TestFail(
281                    "The device is not charging. Is the test run with AC "
282                    "plugged?")
283
284        self._check_voltages_and_currents()
285