blob: 6c4f1fcdb463e8690553626c114e13b303078fdf [file] [log] [blame]
# Copyright 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.
"""Test that charger can charge/discharge battery for certain amount
of change within certain time under certain load.
"""
import logging
import os
import time
import factory_common # pylint: disable=unused-import
from cros.factory.device import device_utils
from cros.factory.test import event_log # TODO(chuntsen): Deprecate event log.
from cros.factory.test.i18n import _
from cros.factory.test import session
from cros.factory.test import test_case
from cros.factory.test.utils import stress_manager
from cros.factory.testlog import testlog
from cros.factory.utils.arg_utils import Arg
from cros.factory.utils import file_utils
CHARGE_TOLERANCE = 0.001
class ChargerTest(test_case.TestCase):
"""This class tests that charger can charge/discharge battery for certain
amount of change within certain time under certain load.
Properties:
_power: The Power object to get AC/Battery info and charge percentage.
"""
ARGS = [
Arg('min_starting_charge_pct', (int, float),
'minimum starting charge level when testing', default=20.0),
Arg('max_starting_charge_pct', (int, float),
'maximum starting charge level when testing', default=90.0),
Arg('starting_timeout_secs', int, 'Maximum allowed time to regulate'
'battery to starting_charge_pct', default=300),
Arg('check_battery_current', bool, 'Check battery current > 0'
'when charging and < 0 when discharging', default=True),
Arg('battery_check_delay_sec', int, 'Delay of checking battery current. '
'This can be used to handle slowly settled battery current.',
default=3),
Arg('verbose_log_period_secs', int, 'Log debug data every x seconds '
'to verbose log file.', default=3),
Arg('log_period_secs', int, 'Log test data every x seconds.',
default=60),
Arg('use_percentage', bool, 'True if using percentage as charge unit '
'in spec list. False if using mAh.', default=True),
Arg('charger_type', str, 'Type of charger required.', default=None),
Arg('spec_list', list, 'A list of [charge_change, timeout_secs, load]\n'
'Charger needs to achieve charge_change difference within\n'
'timeout_secs seconds under load.\n'
'Positive charge_change is for charging and negative one is\n'
'for discharging.\n'
'One unit of load is one thread doing memory copy in stressapptest.\n'
'The default value for load is the number of processor',
default=[[2, 300, 1], [-2, 300]])
]
def setUp(self):
"""Sets the test ui, template and the thread that runs ui. Initializes
_board and _power."""
self._dut = device_utils.CreateDUTInterface()
self._power = self._dut.power
self._min_starting_charge = float(self.args.min_starting_charge_pct)
self._max_starting_charge = float(self.args.max_starting_charge_pct)
for spec in self.args.spec_list:
self.assertTrue(2 <= len(spec) <= 3,
'spec_list item %r should have length 2 or 3' % spec)
verbose_log_path = session.GetVerboseTestLogPath()
file_utils.TryMakeDirs(os.path.dirname(verbose_log_path))
logging.info('Raw verbose logs saved in %s', verbose_log_path)
self._verbose_log = open(verbose_log_path, 'a')
# Group checker for Testlog.
self._group_checker = testlog.GroupParam(
'charge_info', ['load', 'target', 'charge', 'elapsed', 'status'])
testlog.UpdateParam('target', param_type=testlog.PARAM_TYPE.argument)
def _GetLabelWithUnit(self, value):
return '%.2f%s' % (value, '%' if self.args.use_percentage else 'mAh')
def _GetRegulateChargeText(self, charge, target, timeout, load,
battery_current):
"""Makes label to show subtest information
Args:
charge: current battery charge percentage.
target: target battery charge percentage.
timeout: remaining time for this subtest.
load: load argument for this subtest.
battery_current: battery current.
Returns:
A html label to show in test ui.
"""
action = _('Discharging') if charge > target else _('Charging')
return [
_('{action} to {target} '
'(Current charge: {charge}, battery current: {battery_current} mA)'
' under load {load}.',
action=action,
target=self._GetLabelWithUnit(target),
charge=self._GetLabelWithUnit(charge),
battery_current=battery_current,
load=load), '<br>',
_('Time remaining: {timeout:.0f} sec.', timeout=timeout)
]
def _NormalizeCharge(self, charge_pct):
if self.args.use_percentage:
return charge_pct
else:
return charge_pct * self._power.GetChargeFull() / 100.0
def _CheckPower(self):
"""Checks battery and AC power adapter are present."""
self.assertTrue(self._power.CheckBatteryPresent(), 'Cannot find battery.')
self.assertTrue(self._power.CheckACPresent(), 'Cannot find AC power.')
if self.args.charger_type:
self.assertEqual(self._power.GetACType(), self.args.charger_type,
'Incorrect charger type: %s' % self._power.GetACType())
def _GetCharge(self, use_percentage=True):
"""Gets charge level through power interface"""
if use_percentage:
charge = self._power.GetChargePct(get_float=True)
else:
charge = float(self._power.GetChargeMedian())
self.assertIsNotNone(charge, 'Error getting battery charge state.')
return charge
def _GetBatteryCurrent(self):
"""Gets battery current through board"""
try:
return self._power.GetBatteryCurrent()
except Exception as e:
self.fail('Cannot get battery current on this board. %s' % e)
def _GetChargerCurrent(self):
"""Gets current that charger wants to drive through board"""
try:
return self._power.GetChargerCurrent()
except NotImplementedError:
return None
def _GetPowerInfo(self):
"""Gets power info on this board"""
try:
return self._power.GetPowerInfo()
except NotImplementedError:
return None
def _Meet(self, charge, target, moving_up):
"""Checks if charge has meet the target.
Args:
charge: The current charge value.
target: The target charge value.
moving_up: The direction of charging. Should be True or False.
Returns:
True if charge is close to target enough, or charge > target when
moving up, or charge < target when moving down.
False otherwise.
"""
if abs(charge - target) < CHARGE_TOLERANCE:
return True
if moving_up:
return charge > target
else:
return charge < target
def _RegulateCharge(self, charge_change, timeout_secs, load=None):
"""Checks if the charger can meet the spec.
Checks if charge percentage and battery current are available.
Decides whether to charge or discharge battery based on
charge_change.
Sets the load and tries to meet the difference within timeout.
Args:
charge_change: The difference of charge percentage that should be
achieved.
timeout_secs: Timeout in seconds.
load: One unit of load is one thread doing memory copy in stressapptest.
Default to number of cpu core.
"""
if load is None:
load = self._dut.info.cpu_count
charge = self._GetCharge(self.args.use_percentage)
battery_current = self._GetBatteryCurrent()
target = charge + charge_change
moving_up = None
if abs(target - charge) < CHARGE_TOLERANCE:
logging.warning('Current charge is %s, target is %s.'
' They are too close so there is no need to'
'charge/discharge.', self._GetLabelWithUnit(charge),
self._GetLabelWithUnit(target))
event_log.Log('target_too_close', charge=charge, target=target)
with self._group_checker:
testlog.LogParam('status', 'target_too_close')
testlog.LogParam('target', target)
testlog.LogParam('charge', charge)
testlog.LogParam('load', load)
testlog.LogParam('elapsed', 0)
return
elif charge > target:
logging.info('Current charge is %s, discharge the battery to %s.',
self._GetLabelWithUnit(charge),
self._GetLabelWithUnit(target))
self.ui.SetState(_('Testing discharge'))
self._SetDischarge()
moving_up = False
elif charge < target:
logging.info('Current charge is %s, charge the battery to %s.',
self._GetLabelWithUnit(charge),
self._GetLabelWithUnit(target))
self.ui.SetState(_('Testing charger'))
self._SetCharge()
moving_up = True
assert moving_up is not None
if load > 0:
stress_manager_instance = stress_manager.StressManager(self._dut)
else:
stress_manager_instance = stress_manager.DummyStressManager()
with stress_manager_instance.Run(num_threads=load):
start_time = time.time()
last_verbose_log_time = None
last_log_time = None
spec_end_time = start_time + timeout_secs
while time.time() < spec_end_time:
elapsed = time.time() - start_time
self.ui.SetState(
self._GetRegulateChargeText(charge, target, timeout_secs - elapsed,
load, battery_current))
self._CheckPower()
charge = self._GetCharge(self.args.use_percentage)
battery_current = self._GetBatteryCurrent()
with self._group_checker:
testlog.LogParam('target', target)
testlog.LogParam('charge', charge)
testlog.LogParam('load', load)
testlog.LogParam('elapsed', elapsed)
testlog.LogParam('status', 'running')
if self._Meet(charge, target, moving_up):
logging.info('Meet difference from %s to %s'
' in %d secs under %d load.',
self._GetLabelWithUnit(target - charge_change),
self._GetLabelWithUnit(target), elapsed, load)
event_log.Log('meet', elapsed=elapsed, load=load, target=target,
charge=charge)
with self._group_checker:
testlog.LogParam('target', target)
testlog.LogParam('charge', charge)
testlog.LogParam('load', load)
testlog.LogParam('elapsed', elapsed)
testlog.LogParam('status', 'meet')
self.ui.SetState(
_('OK! Meet {target}', target=self._GetLabelWithUnit(target)))
self.Sleep(1)
return
elif elapsed >= self.args.battery_check_delay_sec:
charger_current = self._GetChargerCurrent()
if (not last_verbose_log_time or
elapsed - last_verbose_log_time >
self.args.verbose_log_period_secs):
self._VerboseLog(charge, charger_current, battery_current)
last_verbose_log_time = elapsed
if (not last_log_time or
elapsed - last_log_time > self.args.log_period_secs):
self._Log(charge, charger_current, battery_current)
last_log_time = elapsed
if charge < target:
self._CheckCharge(charger_current, battery_current)
else:
self._CheckDischarge(battery_current)
self.Sleep(1)
event_log.Log('not_meet', load=load, target=target, charge=charge)
with self._group_checker:
elapsed = time.time() - start_time
testlog.LogParam('target', target)
testlog.LogParam('charge', charge)
testlog.LogParam('load', load)
testlog.LogParam('elapsed', elapsed)
testlog.LogParam('status', 'not_meet')
self.fail('Cannot regulate battery to %s in %d seconds.' %
(self._GetLabelWithUnit(target), timeout_secs))
def _VerboseLog(self, charge, charger_current, battery_current):
"""Log data to verbose log"""
self._verbose_log.write(time.strftime('%Y-%m-%d %H:%M:%S\n', time.gmtime()))
self._verbose_log.write('Charge = %s\n' % self._GetLabelWithUnit(charge))
if charger_current is not None:
self._verbose_log.write('Charger current = %d\n' % charger_current)
self._verbose_log.write('Battery current = %d\n' % battery_current)
self._verbose_log.write('Power info =\n%s\n' % self._GetPowerInfo())
self._verbose_log.flush()
def _Log(self, charge, charger_current, battery_current):
"""Log data"""
logging.info('Charge = %s', self._GetLabelWithUnit(charge))
if charger_current is not None:
logging.info('Charger current = %d', charger_current)
logging.info('Battery current = %d', battery_current)
def _CheckCharge(self, charger_current, battery_current):
"""Checks current in charging state"""
if charger_current:
self.assertGreater(charger_current, 0, 'Abnormal charger current')
if self.args.check_battery_current:
self.assertGreater(battery_current, 0, 'Abnormal battery current')
def _CheckDischarge(self, battery_current):
"""Checks current in discharging state"""
if self.args.check_battery_current:
self.assertLess(battery_current, 0, 'Abnormal battery current')
def _SetCharge(self):
"""Sets charger state to CHARGE"""
try:
self._power.SetChargeState(self._power.ChargeState.CHARGE)
except Exception as e:
self.fail('Cannot set charger state to CHARGE on this board. %s' % e)
else:
self.Sleep(1)
def _SetDischarge(self):
"""Sets charger state to DISCHARGE"""
try:
self._power.SetChargeState(self._power.ChargeState.DISCHARGE)
except Exception as e:
self.fail('Cannot set charger state to DISCHARGE on this board. %s' % e)
else:
self.Sleep(1)
def runTest(self):
"""Main entrance of charger test."""
self._CheckPower()
charge = self._GetCharge(self.args.use_percentage)
min_charge = self._NormalizeCharge(self._min_starting_charge)
max_charge = self._NormalizeCharge(self._max_starting_charge)
if charge < min_charge:
start_charge_diff = min_charge - charge
elif charge > max_charge:
start_charge_diff = max_charge - charge
else:
start_charge_diff = None
# Try to meet start_charge_diff as soon as possible.
# When trying to charge, use 0 load.
# When trying to discharge, use full load.
if start_charge_diff:
self._RegulateCharge(start_charge_diff, self.args.starting_timeout_secs,
(0 if start_charge_diff > 0 else None))
# Start testing the specs when battery charge is between
# min_starting_charge_pct and max_starting_charge_pct.
for spec in self.args.spec_list:
self._RegulateCharge(*spec)
def tearDown(self):
# Must enable charger to charge or we will drain the battery!
self._SetCharge()