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