230 lines
9.4 KiB
Python
230 lines
9.4 KiB
Python
# Copyright 2020 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.servo import servo
|
|
from autotest_lib.server.cros.faft.firmware_test import FirmwareTest
|
|
|
|
|
|
class firmware_ECChargingState(FirmwareTest):
|
|
"""
|
|
Type-C servo-v4 based EC charging state test.
|
|
"""
|
|
version = 1
|
|
|
|
# The delay to wait for the AC state to update.
|
|
AC_STATE_UPDATE_DELAY = 3
|
|
|
|
# We wait for up to 3 hrs for the battery to report fully charged.
|
|
FULL_CHARGE_TIMEOUT = 60 * 60 * 3
|
|
|
|
# The period to check battery state while charging.
|
|
CHECK_BATT_STATE_WAIT = 60
|
|
|
|
# The min battery charged percentage that can be considered "full" by
|
|
# powerd. Should be kPowerSupplyFullFactorPref, which defaults to 98%, but
|
|
# that is a pref so set it a little lower to be safe.
|
|
FULL_BATTERY_PERCENT = 95
|
|
|
|
# Battery status
|
|
STATUS_FULLY_CHARGED = 0x20
|
|
STATUS_DISCHARGING = 0x40
|
|
STATUS_TERMINATE_CHARGE_ALARM = 0x4000
|
|
STATUS_OVER_CHARGED_ALARM = 0x8000
|
|
# TERMINATE_CHARGE_ALARM and OVER_CHARGED_ALARM are alarms that shows up during normal use.
|
|
# Other alarms should not appear during testing.
|
|
STATUS_ALARM_MASK = (0xFF00 & ~STATUS_TERMINATE_CHARGE_ALARM
|
|
& ~STATUS_OVER_CHARGED_ALARM)
|
|
|
|
def initialize(self, host, cmdline_args):
|
|
super(firmware_ECChargingState, self).initialize(host, cmdline_args)
|
|
if not self.check_ec_capability(['battery', 'charging']):
|
|
raise error.TestNAError("Nothing needs to be tested on this DUT")
|
|
if not self.servo.is_servo_v4_type_c():
|
|
raise error.TestNAError(
|
|
"This test can only be run with servo-v4 Type-C.")
|
|
if host.is_ac_connected() != True:
|
|
raise error.TestFail("This test must be run with AC power.")
|
|
self.switcher.setup_mode('normal')
|
|
self.ec.send_command("chan save")
|
|
self.ec.send_command("chan 0")
|
|
self.set_dut_low_power_idle_delay(20)
|
|
|
|
def cleanup(self):
|
|
try:
|
|
self.ec.send_command("chan restore")
|
|
self.restore_dut_low_power_idle_delay()
|
|
except Exception as e:
|
|
logging.error("Caught exception: %s", str(e))
|
|
super(firmware_ECChargingState, self).cleanup()
|
|
|
|
def check_ac_state(self):
|
|
"""Check if AC is plugged."""
|
|
ac_state = int(
|
|
self.ec.send_command_get_output("chgstate",
|
|
["ac\s*=\s*(0|1)\s*"])[0][1])
|
|
if ac_state == 1:
|
|
return 'on'
|
|
elif ac_state == 0:
|
|
return 'off'
|
|
else:
|
|
return 'unknown'
|
|
|
|
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_battery_info(self):
|
|
"""Return information about the battery in a dict."""
|
|
match = self._retry_send_cmd("battery", [
|
|
r"Status:\s*(0x[0-9a-f]+)\s",
|
|
r"Param flags:\s*([0-9a-f]+)\s",
|
|
r"Charge:\s+(\d+)\s+",
|
|
])
|
|
status = int(match[0][1], 16)
|
|
params = int(match[1][1], 16)
|
|
level = int(match[2][1])
|
|
|
|
result = {
|
|
"status": status,
|
|
"flags": params,
|
|
"level": level,
|
|
}
|
|
|
|
if status & self.STATUS_ALARM_MASK != 0:
|
|
raise error.TestFail("Battery should not throw alarms: %s" %
|
|
result)
|
|
|
|
# The battery may raise a TERMINATE_CHARGE alarm transiently as
|
|
# it becomes fully charged. Exempt that case, but catch cases where
|
|
# it's yelling to stop for something like invalid charge parameters.
|
|
if (status & \
|
|
(self.STATUS_TERMINATE_CHARGE_ALARM | \
|
|
self.STATUS_FULLY_CHARGED)) == \
|
|
self.STATUS_TERMINATE_CHARGE_ALARM:
|
|
raise error.TestFail(
|
|
"Battery raising TERMINATE_CHARGE alarm non-full: %s" %
|
|
result)
|
|
return result
|
|
|
|
def _check_kernel_battery_state(
|
|
self,
|
|
sysfs_battery_state,
|
|
ec_battery_info,
|
|
):
|
|
if sysfs_battery_state == 'Charging':
|
|
# Charging is just not-discharging. There is no ec battery status
|
|
# for charging.
|
|
if ec_battery_info['status'] & self.STATUS_DISCHARGING != 0:
|
|
raise error.TestFail(
|
|
'Kernel reports battery %s, but actual state is %s',
|
|
sysfs_battery_state, ec_battery_info)
|
|
elif sysfs_battery_state == 'Fully charged':
|
|
# Powerd has it's own creative way of determining full, it doesn't
|
|
# use the status from the EC. So we will consider it acceptable if
|
|
# the battery level is actually full, or above
|
|
if (
|
|
ec_battery_info['status'] & self.STATUS_FULLY_CHARGED == 0
|
|
and ec_battery_info['level'] < self.FULL_BATTERY_PERCENT):
|
|
raise error.TestFail(
|
|
'Kernel reports battery %s, but actual state is %s',
|
|
sysfs_battery_state, ec_battery_info)
|
|
elif (sysfs_battery_state == 'Not charging'
|
|
or sysfs_battery_state == 'Discharging'):
|
|
if ec_battery_info['status'] & self.STATUS_DISCHARGING == 0:
|
|
raise error.TestFail(
|
|
'Kernel reports battery %s, but actual state is %s',
|
|
sysfs_battery_state, ec_battery_info)
|
|
else:
|
|
raise error.TestFail(
|
|
'Kernel reports battery %s, but actual state is %s',
|
|
sysfs_battery_state, ec_battery_info)
|
|
|
|
def run_once(self, host):
|
|
"""Execute the main body of the test."""
|
|
|
|
if host.is_ac_connected() != True:
|
|
raise error.TestFail("This test must be run with AC power.")
|
|
|
|
logging.info("Suspend, unplug AC, and then wake up the device.")
|
|
self.suspend()
|
|
self.switcher.wait_for_client_offline()
|
|
|
|
# Call set_servo_v4_role_to_snk() instead of directly setting
|
|
# servo_v4 role to snk, so servo_v4_role can be recovered to
|
|
# default src in cleanup().
|
|
self.set_servo_v4_role_to_snk()
|
|
time.sleep(self.AC_STATE_UPDATE_DELAY)
|
|
|
|
# Verify servo v4 is sinking power.
|
|
if self.check_ac_state() != 'off':
|
|
raise error.TestFail("Fail to unplug AC.")
|
|
|
|
self.servo.power_normal_press()
|
|
self.switcher.wait_for_client()
|
|
|
|
battery = self._get_battery_info()
|
|
sysfs_battery_state = host.get_battery_state()
|
|
if battery['status'] & self.STATUS_DISCHARGING == 0:
|
|
raise error.TestFail("Wrong battery status. Expected: "
|
|
"Discharging, got: %s." % battery)
|
|
self._check_kernel_battery_state(sysfs_battery_state, battery)
|
|
|
|
logging.info("Suspend, plug AC, and then wake up the device.")
|
|
self.suspend()
|
|
self.switcher.wait_for_client_offline()
|
|
self.servo.set_servo_v4_role('src')
|
|
time.sleep(self.AC_STATE_UPDATE_DELAY)
|
|
|
|
# Verify servo v4 is sourcing power.
|
|
if self.check_ac_state() != 'on':
|
|
raise error.TestFail("Fail to plug AC.")
|
|
|
|
self.servo.power_normal_press()
|
|
self.switcher.wait_for_client()
|
|
|
|
battery = self._get_battery_info()
|
|
sysfs_battery_state = host.get_battery_state()
|
|
if (battery['status'] & self.STATUS_FULLY_CHARGED == 0
|
|
and battery['status'] & self.STATUS_DISCHARGING != 0):
|
|
raise error.TestFail("Wrong battery state. Expected: "
|
|
"Charging/Fully charged, got: %s." % battery)
|
|
self._check_kernel_battery_state(host.get_battery_state(), battery)
|
|
logging.info("Keep charging until the battery reports fully charged.")
|
|
deadline = time.time() + self.FULL_CHARGE_TIMEOUT
|
|
while time.time() < deadline:
|
|
battery = self._get_battery_info()
|
|
if battery['status'] & self.STATUS_FULLY_CHARGED != 0:
|
|
logging.info("The battery reports fully charged.")
|
|
self._check_kernel_battery_state(host.get_battery_state(),
|
|
battery)
|
|
return
|
|
elif battery['status'] & self.STATUS_DISCHARGING == 0:
|
|
logging.info(
|
|
"Wait for the battery to be fully charged. "
|
|
"The current battery level is %d%%.", battery['level'])
|
|
else:
|
|
raise error.TestFail("Wrong battery state. Expected: "
|
|
"Charging/Fully charged, got: %s." %
|
|
battery)
|
|
time.sleep(self.CHECK_BATT_STATE_WAIT)
|
|
|
|
raise error.TestFail(
|
|
"The battery does not report fully charged "
|
|
"before timeout is reached. The final battery "
|
|
"level is %d%%.", battery['level'])
|