blob: e21c8df8ca28847f64972ccfb97d8d19eb3b0346 [file] [log] [blame]
# Copyright 2014 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 USB type-C port charging function with Plankton-Raiden board.
Description
-----------
This test can be tested locally or in remote ADB connection manner.
Use Plankton-Raiden board control to verify device battery current. It supports
charge-5V, charge-12V, charge-20V, and discharge. This test also takes INA
current and voltage value on Plankton-Raiden board into account as a judgement.
Sometimes the charge current may not be stable, we have to wait for several
seconds until the current stable then sampling the current value.
Test Procedure
--------------
This is an automated test without user interaction, but the environment must
be setup first:
1. Connect the testing USB type-C port to Plankton-Raiden board.
2. Connect the Plankton-Raiden serial commanding port to Plankton-Raiden board.
3. Specify the type of the `bft_fixutre` connecting to.
4. Specify the charging/discharing voltage/current and its minimun/maximun
threshold.
5. Start the test.
Dependency
----------
- Connections to Plankton-Raiden board must setup before starting.
Examples
--------
To test 5V charge, add this in test list::
{
"pytest_name": "plankton_usb_c_charge",
"args": {
"bft_fixture": {
"class_name":
"cros.factory.test.fixture.dummy_bft_fixture.DummyBFTFixture",
"params": {}
},
"min_charge_5V_current_mA": 0
}
}
Test 20V charge without checking the input current::
{
"pytest_name": "plankton_usb_c_charge",
"args": {
"bft_fixture": {
"class_name":
"cros.factory.test.fixture.dummy_bft_fixture.DummyBFTFixture",
"params": {}
},
"min_charge_20V_current_mA": 0,
"check_ina_current": false
}
}
"""
import logging
import factory_common # pylint: disable=unused-import
from cros.factory.device import device_utils
from cros.factory.device.links import adb
from cros.factory.test.fixture import bft_fixture
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.utils.arg_utils import Arg
from cros.factory.utils import sync_utils
from cros.factory.utils import time_utils
from cros.factory.utils import type_utils
class PlanktonChargeBFTTest(test_case.TestCase):
"""Tests usb_c port charge functionality."""
ARGS = [
Arg('bft_fixture', dict, bft_fixture.TEST_ARG_HELP),
Arg('charge_duration_secs', (int, float),
'The duration in seconds to charge the battery', default=5),
Arg('discharge_duration_secs', (int, float),
'The duration in seconds to discharge the battery', default=5),
Arg('wait_after_engage_secs', (int, float),
'The duration in seconds to wait after engage / disengage '
'charge device', default=0),
Arg('min_charge_5V_current_mA', (int, float),
'The minimum charge current in mA that the battery '
'needs to reach during charge-5V test, if is None, '
'it would not check on this voltage level', default=None),
Arg('min_charge_12V_current_mA', (int, float),
'The minimum charge current in mA that the battery '
'needs to reach during charge-12V test, if is None, '
'it would not check on this voltage level', default=None),
Arg('min_charge_20V_current_mA', (int, float),
'The minimum charge current in mA that the battery '
'needs to reach during charge-20V test, if is None, '
'it would not check on this voltage level', default=None),
Arg('min_discharge_current_mA', (int, float),
'The minimum discharge current in mA that the battery '
'needs to reach during discharging test, if is None, '
'it would not check on this voltage level', default=None),
Arg('current_sampling_period_secs', (int, float),
'The period in seconds to sample '
'charge/discharge current during test',
default=0.5),
Arg('check_battery_cycle', bool,
'Whether to check battery cycle count is lower than threshold',
default=False),
Arg('battery_cycle_threshold', int,
'The threshold for battery cycle count',
default=0),
Arg('check_protect_ina_current', bool,
'If set True, it would check if Plankton 5V INA current is within '
'protect_ina_current_range first for high-V protection',
default=True),
Arg('protect_ina_current_range', list,
'A list for indicating reasonable current in mA of charge-5V from '
'Plankton INA for high-V protection',
default=[2000, 3400]),
Arg('protect_ina_retry_times', int,
'Retry times for checking 5V INA current for high-V protection, '
'interval with 1 second',
default=5),
Arg('check_ina_current', bool,
'If set True, it would check Plankton INA current during testing '
'whether within ina_current_charge_range for charging, or '
'ina_current_discharge_range for discharge',
default=True),
Arg('ina_current_charge_range', list,
'A list for indicating reasonable current in mA during charging '
'from Plankton INA',
default=[2000, 3400]),
Arg('ina_current_discharge_range', list,
'A list for indicating reasonable current in mA during discharging '
'from Plankton INA',
default=[-3500, 0]),
Arg('ina_voltage_tolerance', float,
'Tolerance ratio for Plankton INA voltage compared to expected '
'charging voltage',
default=0.12),
Arg('monitor_plankton_voltage_only', bool,
'If set True, it would only check Plankton INA voltage whether as '
'expected (not check DUT side)',
default=False)
]
_SUPPORT_CHARGE_VOLT = [5, 12, 20] # supported charging voltage
_DISCHARGE_VOLT = 5 # discharging voltage
def setUp(self):
self.ui.ToggleTemplateClass('font-large', True)
self._dut = device_utils.CreateDUTInterface()
self._power = self._dut.power
self.VerifyArgs()
self._bft_fixture = bft_fixture.CreateBFTFixture(**self.args.bft_fixture)
self._adb_remote_test = isinstance(self._dut.link, adb.ADBLink)
self._remote_test = not self._dut.link.IsLocal()
if self._adb_remote_test:
self.ui.SetState(_('Waiting for ADB device connection...'))
self._bft_fixture.SetDeviceEngaged('ADB_HOST', engage=True)
def tearDown(self):
if self._adb_remote_test:
# Set back the state of ADB-connected before leaving test. It can make
# sequential tests smoother by reducing ADB connection wait time.
self._bft_fixture.SetDeviceEngaged('ADB_HOST', engage=True)
self._bft_fixture.Disconnect()
def VerifyArgs(self):
"""Verifies arguments feasibility.
Raises:
TestFailure: If arguments are not reasonable.
"""
if (self.args.check_protect_ina_current and
(self.args.protect_ina_current_range[0] >
self.args.protect_ina_current_range[1])):
raise type_utils.TestFailure(
'protect_ina_current_range range is invalid')
if (self.args.check_ina_current and
(self.args.ina_current_charge_range[0] >
self.args.ina_current_charge_range[1])):
raise type_utils.TestFailure(
'ina_current_charge_range range is invalid')
if (self.args.check_ina_current and
(self.args.ina_current_discharge_range[0] >
self.args.ina_current_discharge_range[1])):
raise type_utils.TestFailure(
'ina_current_discharge_range range is invalid')
if (self.args.min_charge_5V_current_mA is not None and
self.args.min_charge_5V_current_mA < 0):
raise type_utils.TestFailure(
'min_charge_5V_current_mA must not be less than zero')
if (self.args.min_charge_12V_current_mA is not None and
self.args.min_charge_12V_current_mA < 0):
raise type_utils.TestFailure(
'min_charge_12V_current_mA must not be less than zero')
if (self.args.min_charge_20V_current_mA is not None and
self.args.min_charge_20V_current_mA < 0):
raise type_utils.TestFailure(
'min_charge_20V_current_mA must not be less than zero')
if (self.args.min_discharge_current_mA is not None and
not self.args.min_discharge_current_mA < 0):
raise type_utils.TestFailure(
'min_discharge_current_mA must be less than zero')
def SampleCurrentAndVoltage(self, duration_secs, charging):
"""Samples battery current and Plankton INA current/voltage for duration.
Args:
duration_secs: The duration in seconds to sample current.
charging: True if testing charging; False if discharging.
Returns:
A tuple of three lists (sampled battery current, sampled INA current,
sampled INA voltage).
"""
sampled_battery_current = []
sampled_ina_current = []
sampled_ina_voltage = []
end_time = time_utils.MonotonicTime() + duration_secs
while time_utils.MonotonicTime() < end_time:
# Skip sampling remote target current while discharging since we might
# loss connection during that time.
if not (self._remote_test and not charging):
sampled_battery_current.append(self._power.GetBatteryCurrent())
ina_values = self._bft_fixture.ReadINAValues()
sampled_ina_current.append(ina_values['current'])
sampled_ina_voltage.append(ina_values['voltage'])
self.Sleep(self.args.current_sampling_period_secs)
if not (self._remote_test and not charging):
logging.info('Sampled battery current: %s', str(sampled_battery_current))
logging.info('Sampled ina current: %s', str(sampled_ina_current))
logging.info('Sampled ina voltage: %s', str(sampled_ina_voltage))
return (sampled_battery_current, sampled_ina_current, sampled_ina_voltage)
def MonitorINAVoltage(self, timeout_secs, testing_volt):
"""Polls Plankton INA voltage until it reaches the expected voltage.
Args:
timeout_secs: The duration of polling interval in seconds.
testing_volt: The testing voltage in V.
Returns:
True if voltage meets as expected during polling cycle; otherwise False.
"""
tolerance = testing_volt * 1000 * self.args.ina_voltage_tolerance
def _PollINAVoltage():
ina_voltage = self._bft_fixture.ReadINAValues()['voltage']
logging.info('Monitored ina voltage: %d', ina_voltage)
return abs(ina_voltage - testing_volt * 1000.0) <= tolerance
try:
sync_utils.WaitFor(_PollINAVoltage, timeout_secs,
self.args.current_sampling_period_secs)
return True
except type_utils.TimeoutError:
ina_voltage = self._bft_fixture.ReadINAValues()['voltage']
session.console.error('Expected voltage: %d mV, got: %d mV',
testing_volt * 1000, ina_voltage)
return False
def Check5VINACurrent(self):
"""Checks if Plankton INA is within range for charge-5V.
If charge-5V current is within range, returns immediately. Otherwise, retry
args.protect_ina_retry_times before failing the test.
"""
if not self.args.check_protect_ina_current:
return
current_min, current_max = self.args.protect_ina_current_range
self.ui.SetState(_('Checking Plankton INA current for protection...'))
self._bft_fixture.SetDeviceEngaged('CHARGE_5V', engage=True)
self.Sleep(self.args.wait_after_engage_secs)
ina_current = 0
retry = self.args.protect_ina_retry_times
while retry > 0:
self.Sleep(1) # Wait for INA stable
ina_current = self._bft_fixture.ReadINAValues()['current']
logging.info('Current of plankton INA = %d mA', ina_current)
if current_min <= ina_current <= current_max:
break
retry -= 1
if retry <= 0:
self.fail('Plankton INA current %d mA out of range [%d, %d] '
'after %d retry.' %
(ina_current, current_min, current_max,
self.args.protect_ina_retry_times))
def TestCharging(self, current_min_threshold, testing_volt):
"""Tests charge scenario. It will monitor within args.charge_duration_secs.
Args:
current_min_threshold: Minimum threshold to check charging current. If
None, skip the test.
testing_volt: An integer to specify testing charge voltage.
Raises:
TestFailure: If the sampled battery charge current does not pass
the given threshold in dargs.
"""
if current_min_threshold is None:
return
if testing_volt not in self._SUPPORT_CHARGE_VOLT:
raise type_utils.TestFailure(
'Specified test voltage %d is not in supported list: %r' %
(testing_volt, self._SUPPORT_CHARGE_VOLT))
command_device = 'CHARGE_%dV' % testing_volt
logging.info('Testing %s...', command_device)
self.ui.SetState(
_('Testing battery {voltage}V charging...', voltage=testing_volt))
# Plankton-Raiden board setting: engage
self._bft_fixture.SetDeviceEngaged(command_device, engage=True)
self.Sleep(self.args.wait_after_engage_secs)
if self.args.monitor_plankton_voltage_only:
if not self.MonitorINAVoltage(self.args.charge_duration_secs,
testing_volt):
raise type_utils.TestFailure(
'INA voltage did not meet the expected one.')
return
(sampled_battery_current, sampled_ina_current, sampled_ina_voltage) = (
self.SampleCurrentAndVoltage(self.args.charge_duration_secs,
charging=True))
# Fail if all battery current samples are below threshold.
if not any(c > current_min_threshold for c in sampled_battery_current):
raise type_utils.TestFailure(
'Battery charge current did not reach defined threshold %f mA' %
current_min_threshold)
# Fail if all Plankton INA voltage samples are not within specified range.
self.CheckINASampleVoltage(sampled_ina_voltage, testing_volt, charging=True)
# If args.check_ina_current, fail if average of Plankton INA current
# samples is not within specified range.
self.CheckINASampleCurrent(sampled_ina_current, charging=True)
def TestDischarging(self):
"""Tests discharging within args.discharge_duration_secs.
For local test scenario, the test runs under high system load to maximize
battery discharge current.
For adb remote test, it only measures Plankton INA voltage and current since
we lose adb connection during device discharging.
Raises:
TestFailure: If the sampled battery discharge current does not pass
the given threshold in dargs.
"""
current_min_threshold = self.args.min_discharge_current_mA
if current_min_threshold is None:
return
logging.info('Testing discharge...')
self.ui.SetState(_('Testing battery discharging...'))
self._bft_fixture.SetDeviceEngaged('CHARGE_5V', engage=False)
self.Sleep(self.args.wait_after_engage_secs)
if self.args.monitor_plankton_voltage_only:
if not self.MonitorINAVoltage(self.args.charge_duration_secs, 5):
raise type_utils.TestFailure(
'INA voltage did not meet the expected one.')
return
if self._remote_test:
(unused_sampled_battery_current, sampled_ina_current,
sampled_ina_voltage) = (self.SampleCurrentAndVoltage(
self.args.discharge_duration_secs, charging=False))
else:
# Discharge under high system load.
with stress_manager.StressManager(self._dut).Run(
self.args.discharge_duration_secs):
(sampled_battery_current, sampled_ina_current, sampled_ina_voltage) = (
self.SampleCurrentAndVoltage(
self.args.discharge_duration_secs, charging=False))
# Fail if all samples are over threshold.
if not any(c < current_min_threshold for c in sampled_battery_current):
raise type_utils.TestFailure(
'Battery discharge current did not reach defined threshold %f mA' %
current_min_threshold)
# Fail if all Plankton INA voltage samples are not within specified range.
self.CheckINASampleVoltage(
sampled_ina_voltage, self._DISCHARGE_VOLT, charging=False)
# If args.check_ina_current, fail if average of Plankton INA current
# samples is not within specified range.
self.CheckINASampleCurrent(sampled_ina_current, charging=False)
def CheckINASampleVoltage(self, ina_sample, testing_volt, charging):
"""Checks if average INA voltage is within range on Plankton.
Args:
ina_sample: A list of sampled Plankton INA voltage.
testing_volt: Expected testing voltage in V.
charging: True if testing charging; False if discharging.
Raises:
TestFailure if samples are not within range.
"""
tolerance = testing_volt * 1000 * self.args.ina_voltage_tolerance
# Fail if error ratios of all voltage samples are higher than tolerance
if not any(abs(v - testing_volt * 1000.0) <= tolerance for v in ina_sample):
raise type_utils.TestFailure(
'Plankton INA voltage did not meet expected %s %dV, sampled voltage '
'= %s' % ('charge' if charging else 'discharge',
testing_volt, str(ina_sample)))
def CheckINASampleCurrent(self, ina_sample, charging):
"""Checks if average INA current is within range on Plankton.
Args:
ina_sample: A list of sampled Plankton INA current.
charging: True if testing charging; False if discharging.
Raises:
TestFailure if samples are not within range.
"""
if not self.args.check_ina_current:
return
if charging:
ina_min, ina_max = self.args.ina_current_charge_range
else:
ina_min, ina_max = self.args.ina_current_discharge_range
# Fail if average is not within range.
# Neglect first 2 samples since they may on current up-lifting stage.
ina_sample = ina_sample if len(ina_sample) < 2 else ina_sample[2:]
average = sum(ina_sample) / len(ina_sample)
logging.info('Average Plankton INA current = %d mA', average)
if not ina_min < average < ina_max:
raise type_utils.TestFailure(
'Plankton INA %s current average %d mA did not within %d - %d' %
('charge' if charging else 'discharge', average, ina_min, ina_max))
def runTest(self):
"""Runs charge test.
Raises:
TestFailure: Battery attribute error.
"""
if not self._power.CheckBatteryPresent():
raise type_utils.TestFailure(
'Cannot detect battery. Missing battery?')
if (self.args.check_battery_cycle and
self._power.GetBatteryCycleCount() > self.args.battery_cycle_threshold):
raise type_utils.TestFailure('Battery cycle count is higher than %d' %
self.args.battery_cycle_threshold)
if self._remote_test:
# Get remote target battery capacity and warn if almost full
capacity = self._power.GetChargePct()
session.console.info('Current battery capacity = %d %%', capacity)
if capacity > 95:
session.console.warning('Current battery capacity is almost full. '
'It may cause charge failure!!')
else:
# Set charge state to 'charge'
self._power.SetChargeState(self._power.ChargeState.CHARGE)
logging.info('Set charge state: CHARGE')
self.Check5VINACurrent()
self.TestCharging(self.args.min_charge_5V_current_mA, testing_volt=5)
self.TestCharging(self.args.min_charge_12V_current_mA, testing_volt=12)
self.TestCharging(self.args.min_charge_20V_current_mA, testing_volt=20)
self.TestDischarging()