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