1# Copyright 2020 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.servo import servo
11from autotest_lib.server.cros.faft.firmware_test import FirmwareTest
12
13
14class firmware_ECChargingState(FirmwareTest):
15    """
16    Type-C servo-v4 based EC charging state test.
17    """
18    version = 1
19
20    # The delay to wait for the AC state to update.
21    AC_STATE_UPDATE_DELAY = 3
22
23    # We wait for up to 3 hrs for the battery to report fully charged.
24    FULL_CHARGE_TIMEOUT = 60 * 60 * 3
25
26    # The period to check battery state while charging.
27    CHECK_BATT_STATE_WAIT = 60
28
29    # The min battery charged percentage that can be considered "full" by
30    # powerd. Should be kPowerSupplyFullFactorPref, which defaults to 98%, but
31    # that is a pref so set it a little lower to be safe.
32    FULL_BATTERY_PERCENT = 95
33
34    # Battery status
35    STATUS_FULLY_CHARGED = 0x20
36    STATUS_DISCHARGING = 0x40
37    STATUS_TERMINATE_CHARGE_ALARM = 0x4000
38    STATUS_OVER_CHARGED_ALARM = 0x8000
39    # TERMINATE_CHARGE_ALARM and OVER_CHARGED_ALARM are alarms that shows up during normal use.
40    # Other alarms should not appear during testing.
41    STATUS_ALARM_MASK = (0xFF00 & ~STATUS_TERMINATE_CHARGE_ALARM
42                         & ~STATUS_OVER_CHARGED_ALARM)
43
44    def initialize(self, host, cmdline_args):
45        super(firmware_ECChargingState, self).initialize(host, cmdline_args)
46        if not self.check_ec_capability(['battery', 'charging']):
47            raise error.TestNAError("Nothing needs to be tested on this DUT")
48        if not self.servo.is_servo_v4_type_c():
49            raise error.TestNAError(
50                    "This test can only be run with servo-v4 Type-C.")
51        if host.is_ac_connected() != True:
52            raise error.TestFail("This test must be run with AC power.")
53        self.switcher.setup_mode('normal')
54        self.ec.send_command("chan save")
55        self.ec.send_command("chan 0")
56        self.set_dut_low_power_idle_delay(20)
57
58    def cleanup(self):
59        try:
60            self.ec.send_command("chan restore")
61            self.restore_dut_low_power_idle_delay()
62        except Exception as e:
63            logging.error("Caught exception: %s", str(e))
64        super(firmware_ECChargingState, self).cleanup()
65
66    def check_ac_state(self):
67        """Check if AC is plugged."""
68        ac_state = int(
69                self.ec.send_command_get_output("chgstate",
70                                                ["ac\s*=\s*(0|1)\s*"])[0][1])
71        if ac_state == 1:
72            return 'on'
73        elif ac_state == 0:
74            return 'off'
75        else:
76            return 'unknown'
77
78    def _retry_send_cmd(self, command, regex_list):
79        """Send an EC command, and retry if it fails."""
80        retries = 3
81        while retries > 0:
82            retries -= 1
83            try:
84                return self.ec.send_command_get_output(command, regex_list)
85            except (servo.UnresponsiveConsoleError,
86                    servo.ResponsiveConsoleError, expat.ExpatError) as e:
87                if retries <= 0:
88                    raise
89                logging.warning('Failed to send EC cmd. %s', e)
90
91    def _get_battery_info(self):
92        """Return information about the battery in a dict."""
93        match = self._retry_send_cmd("battery", [
94                r"Status:\s*(0x[0-9a-f]+)\s",
95                r"Param flags:\s*([0-9a-f]+)\s",
96                r"Charge:\s+(\d+)\s+",
97        ])
98        status = int(match[0][1], 16)
99        params = int(match[1][1], 16)
100        level = int(match[2][1])
101
102        result = {
103                "status": status,
104                "flags": params,
105                "level": level,
106        }
107
108        if status & self.STATUS_ALARM_MASK != 0:
109            raise error.TestFail("Battery should not throw alarms: %s" %
110                                 result)
111
112        # The battery may raise a TERMINATE_CHARGE alarm transiently as
113        # it becomes fully charged. Exempt that case, but catch cases where
114        # it's yelling to stop for something like invalid charge parameters.
115        if (status & \
116            (self.STATUS_TERMINATE_CHARGE_ALARM | \
117             self.STATUS_FULLY_CHARGED)) == \
118            self.STATUS_TERMINATE_CHARGE_ALARM:
119            raise error.TestFail(
120                    "Battery raising TERMINATE_CHARGE alarm non-full: %s" %
121                    result)
122        return result
123
124    def _check_kernel_battery_state(
125            self,
126            sysfs_battery_state,
127            ec_battery_info,
128    ):
129        if sysfs_battery_state == 'Charging':
130            # Charging is just not-discharging. There is no ec battery status
131            # for charging.
132            if ec_battery_info['status'] & self.STATUS_DISCHARGING != 0:
133                raise error.TestFail(
134                        'Kernel reports battery %s, but actual state is %s',
135                        sysfs_battery_state, ec_battery_info)
136        elif sysfs_battery_state == 'Fully charged':
137            # Powerd has it's own creative way of determining full, it doesn't
138            # use the status from the EC. So we will consider it acceptable if
139            # the battery level is actually full, or above
140            if (
141                    ec_battery_info['status'] & self.STATUS_FULLY_CHARGED == 0
142                    and ec_battery_info['level'] < self.FULL_BATTERY_PERCENT):
143                raise error.TestFail(
144                        'Kernel reports battery %s, but actual state is %s',
145                        sysfs_battery_state, ec_battery_info)
146        elif (sysfs_battery_state == 'Not charging'
147              or sysfs_battery_state == 'Discharging'):
148            if ec_battery_info['status'] & self.STATUS_DISCHARGING == 0:
149                raise error.TestFail(
150                        'Kernel reports battery %s, but actual state is %s',
151                        sysfs_battery_state, ec_battery_info)
152        else:
153            raise error.TestFail(
154                    'Kernel reports battery %s, but actual state is %s',
155                    sysfs_battery_state, ec_battery_info)
156
157    def run_once(self, host):
158        """Execute the main body of the test."""
159
160        if host.is_ac_connected() != True:
161            raise error.TestFail("This test must be run with AC power.")
162
163        logging.info("Suspend, unplug AC, and then wake up the device.")
164        self.suspend()
165        self.switcher.wait_for_client_offline()
166
167        # Call set_servo_v4_role_to_snk() instead of directly setting
168        # servo_v4 role to snk, so servo_v4_role can be recovered to
169        # default src in cleanup().
170        self.set_servo_v4_role_to_snk()
171        time.sleep(self.AC_STATE_UPDATE_DELAY)
172
173        # Verify servo v4 is sinking power.
174        if self.check_ac_state() != 'off':
175            raise error.TestFail("Fail to unplug AC.")
176
177        self.servo.power_normal_press()
178        self.switcher.wait_for_client()
179
180        battery = self._get_battery_info()
181        sysfs_battery_state = host.get_battery_state()
182        if battery['status'] & self.STATUS_DISCHARGING == 0:
183            raise error.TestFail("Wrong battery status. Expected: "
184                                 "Discharging, got: %s." % battery)
185        self._check_kernel_battery_state(sysfs_battery_state, battery)
186
187        logging.info("Suspend, plug AC, and then wake up the device.")
188        self.suspend()
189        self.switcher.wait_for_client_offline()
190        self.servo.set_servo_v4_role('src')
191        time.sleep(self.AC_STATE_UPDATE_DELAY)
192
193        # Verify servo v4 is sourcing power.
194        if self.check_ac_state() != 'on':
195            raise error.TestFail("Fail to plug AC.")
196
197        self.servo.power_normal_press()
198        self.switcher.wait_for_client()
199
200        battery = self._get_battery_info()
201        sysfs_battery_state = host.get_battery_state()
202        if (battery['status'] & self.STATUS_FULLY_CHARGED == 0
203                    and battery['status'] & self.STATUS_DISCHARGING != 0):
204            raise error.TestFail("Wrong battery state. Expected: "
205                                 "Charging/Fully charged, got: %s." % battery)
206        self._check_kernel_battery_state(host.get_battery_state(), battery)
207        logging.info("Keep charging until the battery reports fully charged.")
208        deadline = time.time() + self.FULL_CHARGE_TIMEOUT
209        while time.time() < deadline:
210            battery = self._get_battery_info()
211            if battery['status'] & self.STATUS_FULLY_CHARGED != 0:
212                logging.info("The battery reports fully charged.")
213                self._check_kernel_battery_state(host.get_battery_state(),
214                                                 battery)
215                return
216            elif battery['status'] & self.STATUS_DISCHARGING == 0:
217                logging.info(
218                        "Wait for the battery to be fully charged. "
219                        "The current battery level is %d%%.", battery['level'])
220            else:
221                raise error.TestFail("Wrong battery state. Expected: "
222                                     "Charging/Fully charged, got: %s." %
223                                     battery)
224            time.sleep(self.CHECK_BATT_STATE_WAIT)
225
226        raise error.TestFail(
227                "The battery does not report fully charged "
228                "before timeout is reached. The final battery "
229                "level is %d%%.", battery['level'])
230