blob: 3ab34e2a8ca7322c3e418f14b6933e7c631ffb20 [file] [log] [blame]
# 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.
"""Test that charger can charge/discharge battery for certain amount
of change within certain time under certain load.
"""
from collections import namedtuple
import logging
import threading
import time
import unittest
import factory_common # pylint: disable=unused-import
from cros.factory.device import device_utils
from cros.factory.test import event_log
from cros.factory.test import factory
from cros.factory.test.i18n import _
from cros.factory.test.i18n import test_ui as i18n_test_ui
from cros.factory.test import test_ui
from cros.factory.test import ui_templates
from cros.factory.test.utils import stress_manager
from cros.factory.utils.arg_utils import Arg
_TEST_TITLE = i18n_test_ui.MakeI18nLabel('Charger Test')
def _REGULATE_CHARGE_TEXT(charge, target, timeout, load,
battery_current, use_percentage):
"""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.
use_percentage: Whether to use percentage or mAh.
Returns:
A html label to show in test ui.
"""
unit = '%' if use_percentage else 'mAh'
action = _('Discharging') if charge > target else _('Charging')
return i18n_test_ui.MakeI18nLabel(
'{action} to {target:.2f}{unit} '
'(Current charge: {charge:.2f}{unit},'
' battery current: {battery_current} mA)'
' under load {load}.<br>'
'Time remaining: {timeout} sec.',
action=action, target=target, unit=unit, charge=charge,
battery_current=battery_current, load=load, timeout=timeout)
def _MEET_TEXT(target, use_percentage):
"""Makes label to show subtest completes.
Args:
target: target battery charge percentage of this subtest.
use_percentage: Whether to use percentage or mAh.
Returns:
A html label to show in test ui.
"""
unit = '%' if use_percentage else 'mAh'
return i18n_test_ui.MakeI18nLabel(
'OK! Meet {target:.2f}{unit}', target=target, unit=unit)
_CHARGE_TEXT = i18n_test_ui.MakeI18nLabel('Testing charger')
_DISCHARGE_TEXT = i18n_test_ui.MakeI18nLabel('Testing discharge')
Spec = namedtuple('Spec', 'charge_change timeout_secs load')
CHARGE_TOLERANCE = 0.001
class ChargerTest(unittest.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.
_ui: Test UI.
_template: Test template.
_thread: The thread to run ui.
"""
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,
optional=True),
Arg('spec_list', list, 'A list of tuples. Each tuple contains\n'
'(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._ui = test_ui.UI()
self._template = ui_templates.OneSection(self._ui)
self._template.SetTitle(_TEST_TITLE)
self._thread = threading.Thread(target=self._ui.Run)
self._min_starting_charge = float(self.args.min_starting_charge_pct)
self._max_starting_charge = float(self.args.max_starting_charge_pct)
self._unit = '%' if self.args.use_percentage else 'mAh'
self._verbose_log = factory.get_verbose_log_file()
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.assertTrue(charge is not None, 'Error getting battery charge state.')
return charge
def _GetBatteryCurrent(self):
"""Gets battery current through board"""
try:
battery_current = self._power.GetBatteryCurrent()
except Exception, e:
self.fail('Cannot get battery current on this board. %s' % e)
else:
return battery_current
def _DumpBatteryRegisters(self):
"""Dumps battery registers through board"""
try:
battery_registers = self._power.GetBatteryRegisters()
except Exception, e:
logging.exception('Can not get battery registers. %s', e)
else:
logging.info('Dump battery registers:')
for offset, value in battery_registers.iteritems():
logging.info('offset: 0x%x, value: 0x%x', offset, value)
def _GetChargerCurrent(self):
"""Gets current that charger wants to drive through board"""
try:
charger_current = self._power.GetChargerCurrent()
except NotImplementedError:
return None
else:
return charger_current
def _GetPowerInfo(self):
"""Gets power info on this board"""
try:
power_info = self._power.GetPowerInfo()
except NotImplementedError:
return None
else:
return power_info
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.
"""
self.assertTrue(moving_up is not None)
if abs(charge - target) < CHARGE_TOLERANCE:
return True
if moving_up:
return charge > target
else:
return charge < target
def _RegulateCharge(self, spec):
"""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
spec.charge_change.
Sets the load and tries to meet the difference within timeout.
Args:
spec: A Spec namedtuple.
"""
charge = self._GetCharge(self.args.use_percentage)
battery_current = self._GetBatteryCurrent()
target = charge + spec.charge_change
moving_up = None
if abs(target - charge) < CHARGE_TOLERANCE:
logging.warning('Current charge is %.2f%s, target is %.2f%s.'
' They are too close so there is no need to'
'charge/discharge.', charge, self._unit,
target, self._unit)
event_log.Log('target_too_close', charge=charge, target=target)
return
elif charge > target:
logging.info('Current charge is %.2f%s, discharge the battery to %.2f%s.',
charge, self._unit, target, self._unit)
self._SetDischarge()
moving_up = False
elif charge < target:
logging.info('Current charge is %.2f%s, charge the battery to %.2f%s.',
charge, self._unit, target, self._unit)
self._SetCharge()
moving_up = True
# charge should move up or down.
self.assertTrue(moving_up is not None)
if spec.load > 0:
stress_manager_instance = stress_manager.StressManager(self._dut)
else:
stress_manager_instance = stress_manager.DummyStressManager()
with stress_manager_instance.Run(num_threads=spec.load):
start_time = time.time()
last_verbose_log_time = None
last_log_time = None
spec_end_time = start_time + spec.timeout_secs
while time.time() < spec_end_time:
elapsed = time.time() - start_time
self._template.SetState(_REGULATE_CHARGE_TEXT(
charge, target, spec.timeout_secs - elapsed, spec.load,
battery_current, self.args.use_percentage))
time.sleep(1)
self._CheckPower()
charge = self._GetCharge(self.args.use_percentage)
battery_current = self._GetBatteryCurrent()
if self._Meet(charge, target, moving_up):
logging.info('Meet difference from %.2f%s to %.2f%s'
' in %d secs under %d load.',
target - spec.charge_change, self._unit,
target, self._unit,
elapsed, spec.load)
event_log.Log('meet', elapsed=elapsed, load=spec.load, target=target,
charge=charge)
self._template.SetState(_MEET_TEXT(target, self.args.use_percentage))
time.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)
event_log.Log('not_meet', load=spec.load, target=target, charge=charge)
self._DumpBatteryRegisters()
self.fail('Cannot regulate battery to %.2f%s in %d seconds.' %
(target, self._unit, spec.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 = %.2f%s\n' % (charge, self._unit))
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 to factory log"""
logging.info('Charge = %.2f%s', charge, self._unit)
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.assertTrue(charger_current > 0, 'Abnormal charger current')
if self.args.check_battery_current:
self.assertTrue(battery_current > 0, 'Abnormal battery current')
def _CheckDischarge(self, battery_current):
"""Checks current in discharging state"""
if self.args.check_battery_current:
self.assertTrue(battery_current < 0, 'Abnormal battery current')
def _SetCharge(self, update_ui=True):
"""Sets charger state to CHARGE"""
if update_ui:
self._template.SetState(_CHARGE_TEXT)
try:
self._power.SetChargeState(self._power.ChargeState.CHARGE)
except Exception, e:
self.fail('Cannot set charger state to CHARGE on this board. %s' % e)
else:
time.sleep(1)
def _SetDischarge(self):
"""Sets charger state to DISCHARGE"""
self._template.SetState(_DISCHARGE_TEXT)
try:
self._power.SetChargeState(self._power.ChargeState.DISCHARGE)
except Exception, e:
self.fail('Cannot set charger state to DISCHARGE on this board. %s' % e)
else:
time.sleep(1)
def _GetSpec(self, charge_change, timeout_secs, load=None):
"""Gets Spec with default load value set as number of cpus"""
if load is None:
load = self._dut.info.cpu_count
return Spec(charge_change, timeout_secs, load)
def runTest(self):
"""Main entrance of charger test."""
self._thread.start()
try:
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(
self._GetSpec(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(self._GetSpec(*spec))
except Exception, e:
self._ui.Fail(str(e))
raise
else:
self._ui.Pass()
def tearDown(self):
# Must enable charger to charge or we will drain the battery!
self._SetCharge(update_ui=False)
self._thread.join()