285 lines
11 KiB
Python
285 lines
11 KiB
Python
# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
|
|
import logging
|
|
import time
|
|
from xml.parsers import expat
|
|
|
|
from autotest_lib.client.common_lib import error
|
|
from autotest_lib.server.cros.faft.firmware_test import FirmwareTest
|
|
from autotest_lib.server.cros.servo import servo
|
|
|
|
|
|
class firmware_ECCharging(FirmwareTest):
|
|
"""
|
|
Servo based EC charging control test.
|
|
"""
|
|
version = 1
|
|
|
|
# Flags set by battery
|
|
BATT_FLAG_WANT_CHARGE = 0x1
|
|
STATUS_FULLY_CHARGED = 0x20
|
|
|
|
# Threshold of trickle charging current in mA
|
|
TRICKLE_CHARGE_THRESHOLD = 100
|
|
|
|
# We wait for up to 60 minutes for the battery to allow charging.
|
|
# kodama in particular takes a long time to discharge
|
|
DISCHARGE_TIMEOUT = 60 * 60
|
|
|
|
# The period to check battery state while discharging.
|
|
CHECK_BATT_STATE_WAIT = 60
|
|
|
|
# The delay to wait for the AC state to update.
|
|
AC_STATE_UPDATE_DELAY = 3
|
|
|
|
# Wait a few seconds after discharging for voltage to stabilize
|
|
BEGIN_CHARGING_TIMEOUT = 120
|
|
|
|
# Sleep for a second between retries when waiting for voltage to stabilize
|
|
BEGIN_CHARGING_RETRY_TIME = 1
|
|
|
|
# After the battery reports it is not full, keep discharging for this long.
|
|
# This should be >= BEGIN_CHARGING_TIMEOUT
|
|
EXTRA_DISCHARGE_TIME = BEGIN_CHARGING_TIMEOUT + 30
|
|
|
|
def initialize(self, host, cmdline_args):
|
|
super(firmware_ECCharging, self).initialize(host, cmdline_args)
|
|
# Don't bother if there is no Chrome EC.
|
|
if not self.check_ec_capability():
|
|
raise error.TestNAError(
|
|
"Nothing needs to be tested on this device")
|
|
# Only run in normal mode
|
|
self.switcher.setup_mode('normal')
|
|
self.ec.send_command("chan 0")
|
|
|
|
def cleanup(self):
|
|
try:
|
|
self.ec.send_command("chan 0xffffffff")
|
|
except Exception as e:
|
|
logging.error("Caught exception: %s", str(e))
|
|
super(firmware_ECCharging, self).cleanup()
|
|
|
|
def _retry_send_cmd(self, command, regex_list):
|
|
"""Send an EC command, and retry if it fails."""
|
|
retries = 3
|
|
while retries > 0:
|
|
retries -= 1
|
|
try:
|
|
return self.ec.send_command_get_output(command, regex_list)
|
|
except (servo.UnresponsiveConsoleError,
|
|
servo.ResponsiveConsoleError, expat.ExpatError) as e:
|
|
if retries <= 0:
|
|
raise
|
|
logging.warning('Failed to send EC cmd. %s', e)
|
|
|
|
def _get_charge_state(self):
|
|
"""Get charger and battery information in a single call."""
|
|
output = self._retry_send_cmd("chgstate", [
|
|
r"chg\.\*:",
|
|
r"voltage = (-?\d+)mV",
|
|
r"current = (-?\d+)mA",
|
|
r"batt\.\*:",
|
|
r"voltage = (-?\d+)mV",
|
|
r"current = (-?\d+)mA",
|
|
r"desired_voltage = (-?\d+)mV",
|
|
r"desired_current = (-?\d+)mA",
|
|
])
|
|
result = {
|
|
"charger_target_voltage": int(output[1][1]),
|
|
"charger_target_current": int(output[2][1]),
|
|
"battery_actual_voltage": int(output[4][1]),
|
|
"battery_actual_current": int(output[5][1]),
|
|
"battery_desired_voltage": int(output[6][1]),
|
|
"battery_desired_current": int(output[7][1]),
|
|
}
|
|
logging.info("Charger & battery info: %s", result)
|
|
return result
|
|
|
|
def _get_trickle_charging(self):
|
|
"""Check if we are trickle charging battery."""
|
|
return (self.ec.get_battery_desired_current() <
|
|
self.TRICKLE_CHARGE_THRESHOLD)
|
|
|
|
def _check_voltages_and_currents(self):
|
|
"""Check that the battery and charger voltages and currents are within
|
|
acceptable limits.
|
|
|
|
Raise:
|
|
error.TestFail: Raised when check fails.
|
|
"""
|
|
state = self._get_charge_state()
|
|
target_voltage = state['charger_target_voltage']
|
|
desired_voltage = state['battery_desired_voltage']
|
|
target_current = state['charger_target_current']
|
|
desired_current = state['battery_desired_current']
|
|
actual_voltage = state['battery_actual_voltage']
|
|
actual_current = state['battery_actual_current']
|
|
logging.info("Checking charger target values...")
|
|
if (target_voltage >= 1.05 * desired_voltage):
|
|
raise error.TestFail(
|
|
"Charger target voltage is too high. %d/%d=%f" %
|
|
(target_voltage, desired_voltage,
|
|
float(target_voltage) / desired_voltage))
|
|
if (target_current >= 1.05 * desired_current):
|
|
raise error.TestFail(
|
|
"Charger target current is too high. %d/%d=%f" %
|
|
(target_current, desired_current,
|
|
float(target_current) / desired_current))
|
|
|
|
logging.info("Checking battery actual values...")
|
|
if (actual_voltage >= 1.05 * target_voltage):
|
|
raise error.TestFail(
|
|
"Battery actual voltage is too high. %d/%d=%f" %
|
|
(actual_voltage, target_voltage,
|
|
float(actual_voltage) / target_voltage))
|
|
if (actual_current >= 1.05 * target_current):
|
|
raise error.TestFail(
|
|
"Battery actual current is too high. %d/%d=%f" %
|
|
(actual_current, target_current,
|
|
float(actual_current) / target_current))
|
|
|
|
def _check_if_discharge_on_ac(self):
|
|
"""Check if DUT is performing discharge on AC"""
|
|
match = self._retry_send_cmd("battery", [
|
|
r"Status:\s*(0x[0-9a-f]+)\s", r"Param flags:\s*([0-9a-f]+)\s"
|
|
])
|
|
status = int(match[0][1], 16)
|
|
params = int(match[1][1], 16)
|
|
|
|
if (not (params & self.BATT_FLAG_WANT_CHARGE) and
|
|
(status & self.STATUS_FULLY_CHARGED)):
|
|
return True
|
|
|
|
return False
|
|
|
|
def _check_battery_discharging(self):
|
|
"""Check if AC is attached and if charge control is normal."""
|
|
# chg_ctl_mode may look like: chg_ctl_mode = 2
|
|
# or: chg_ctl_mode = DISCHARGE (2)
|
|
# The regex needs to match either one.
|
|
output = self._retry_send_cmd("chgstate", [
|
|
r"ac\s*=\s*(\d)\s*",
|
|
r"chg_ctl_mode\s*=\s*(\S* \(\d+\)|\d+)\r\n"
|
|
])
|
|
ac_state = int(output[0][1])
|
|
chg_ctl_mode = output[1][1]
|
|
if ac_state == 0:
|
|
return True
|
|
if chg_ctl_mode == "2" or chg_ctl_mode == "DISCHARGE (2)":
|
|
return True
|
|
return False
|
|
|
|
def _set_battery_discharge(self):
|
|
"""Instruct the EC to drain the battery."""
|
|
# Ask EC to drain the battery
|
|
output = self._retry_send_cmd("chgstate discharge on", [
|
|
r"state =|Parameter 1 invalid",
|
|
])
|
|
logging.debug("chgstate returned %s", output)
|
|
if output[0] == 'Parameter 1 invalid':
|
|
raise error.TestNAError(
|
|
"Device doesn't support CHARGER_DISCHARGE_ON_AC, "
|
|
"please drain battery below full and run the test again.")
|
|
time.sleep(self.AC_STATE_UPDATE_DELAY)
|
|
|
|
# Verify discharging. Either AC off or charge control discharge is
|
|
# good.
|
|
if not self._check_battery_discharging():
|
|
raise error.TestFail("Battery is not discharging.")
|
|
|
|
def _set_battery_normal(self):
|
|
"""Instruct the EC to charge the battery as normal."""
|
|
self.ec.send_command("chgstate discharge off")
|
|
time.sleep(self.AC_STATE_UPDATE_DELAY)
|
|
|
|
# Verify AC is on and charge control is normal.
|
|
if self._check_battery_discharging():
|
|
raise error.TestFail("Fail to plug AC and enable charging.")
|
|
self.ec.update_battery_info()
|
|
|
|
def _consume_battery(self, deadline):
|
|
"""Perform battery intensive operation to make the battery discharge
|
|
faster."""
|
|
# Switch to servo drain after b/140965614.
|
|
stress_time = deadline - time.time()
|
|
if stress_time > self.CHECK_BATT_STATE_WAIT:
|
|
stress_time = self.CHECK_BATT_STATE_WAIT
|
|
self._client.run("stressapptest -s %d " % stress_time,
|
|
ignore_status=True)
|
|
|
|
def _discharge_below_100(self):
|
|
"""Remove AC power until the battery is not full."""
|
|
self._set_battery_discharge()
|
|
logging.info(
|
|
"Keep discharging until the battery reports charging allowed.")
|
|
|
|
try:
|
|
# Wait until DISCHARGE_TIMEOUT or charging allowed
|
|
deadline = time.time() + self.DISCHARGE_TIMEOUT
|
|
while time.time() < deadline:
|
|
self.ec.update_battery_info()
|
|
if self.ec.get_battery_charging_allowed():
|
|
break
|
|
logging.info("Wait for the battery to discharge (%d mAh).",
|
|
self.ec.get_battery_remaining())
|
|
self._consume_battery(deadline)
|
|
else:
|
|
raise error.TestFail(
|
|
"The battery does not report charging allowed "
|
|
"before timeout is reached.")
|
|
|
|
# Wait another EXTRA_DISCHARGE_TIME just to be sure
|
|
deadline = time.time() + self.EXTRA_DISCHARGE_TIME
|
|
while time.time() < deadline:
|
|
self.ec.update_battery_info()
|
|
logging.info(
|
|
"Wait for the battery to discharge even more (%d mAh).",
|
|
self.ec.get_battery_remaining())
|
|
self._consume_battery(deadline)
|
|
finally:
|
|
self._set_battery_normal()
|
|
|
|
# For many devices, it takes some time after discharging for the
|
|
# battery to actually start charging.
|
|
deadline = time.time() + self.BEGIN_CHARGING_TIMEOUT
|
|
while time.time() < deadline:
|
|
self.ec.update_battery_info()
|
|
if self.ec.get_battery_actual_current() >= 0:
|
|
break
|
|
logging.info(
|
|
'Battery actual current (%d) too low, wait a bit. (%d mAh)',
|
|
self.ec.get_battery_actual_current(),
|
|
self.ec.get_battery_remaining())
|
|
self._consume_battery(deadline)
|
|
|
|
def run_once(self):
|
|
"""Execute the main body of the test.
|
|
"""
|
|
if not self.check_ec_capability(['battery', 'charging']):
|
|
raise error.TestNAError(
|
|
"Nothing needs to be tested on this device")
|
|
if not self.ec.get_battery_charging_allowed(
|
|
) or self.ec.get_battery_actual_current() < 0:
|
|
logging.info(
|
|
"Battery is full or discharging. Forcing battery discharge "
|
|
"to test charging.")
|
|
self._discharge_below_100()
|
|
if not self.ec.get_battery_charging_allowed():
|
|
raise error.TestFail(
|
|
"Battery reports charging is not allowed, even after "
|
|
"discharging.")
|
|
if self._check_if_discharge_on_ac():
|
|
raise error.TestNAError(
|
|
"DUT is performing discharge on AC. Unable to test.")
|
|
if self._get_trickle_charging():
|
|
raise error.TestNAError(
|
|
"Trickling charging battery. Unable to test.")
|
|
if self.ec.get_battery_actual_current() < 0:
|
|
raise error.TestFail(
|
|
"The device is not charging. Is the test run with AC "
|
|
"plugged?")
|
|
|
|
self._check_voltages_and_currents()
|