| # Copyright 2013 The ChromiumOS Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """A factory test to test battery charging/discharging current. |
| |
| Description |
| ----------- |
| Test battery charging and discharging current. |
| |
| If `usbpd_info` is set, also prompt operator to insert a power adapter of given |
| voltage to the given USB type C port. |
| |
| The `usbpd_info` is a sequence `(usbpd_port, min_millivolt, max_millivolt)`, |
| represent the USB type C port to insert power adapter: |
| |
| - ``usbpd_port``: (int) usbpd_port number. Specify which port to insert power |
| line. |
| - ``min_millivolt``: (int) The minimum millivolt the power must provide. |
| - ``max_millivolt``: (int) The maximum millivolt the power must provide. |
| |
| Test Procedure |
| -------------- |
| 1. If `max_battery_level` is set, check that initial battery level is lower |
| than the value. |
| 2. If `usbpd_info` is set, prompt the operator to insert a power adapter of |
| given voltage to the given USB type C port, and pass this step when one is |
| detected. |
| 3. If `min_charging_current` is set, force the power into charging mode, and |
| check if the charging current is larger than the value. |
| 4. If `min_discharging_current` is set, force the power into discharging mode, |
| and check if the discharging current is larger than the value. |
| 5. If `current_difference` is set, force the power into charging mode first, |
| and record the charging current. Then force the power into discharging mode, |
| and also record the discharging current. Pass the test if the (average |
| charging current - average discharging current) is greater or equal to the |
| value. |
| |
| Each step would fail after `timeout_secs` seconds. |
| |
| Dependency |
| ---------- |
| Device API cros.factory.device.power. |
| |
| If `usbpd_info` is set, device API cros.factory.device.usb_c.GetPDPowerStatus |
| is also used. |
| |
| Examples |
| -------- |
| To check battery can charge and discharge, add this in test list: |
| |
| .. test_list:: |
| |
| generic_battery_examples:BatteryTests.ChargeDischargeCurrent |
| |
| Sometimes, the system consumes more power than the charger. In that case, we |
| could set the min_charging_current to negative value, and the test would pass |
| if the battery discharges less than 150 mA. See b/183679223#comment25: |
| |
| .. test_list:: |
| |
| generic_battery_examples:BatteryTests. |
| ChargeDischargeCurrentExpectNoChargeWhenCharging |
| |
| Alternatively, we could also set current_difference to just test the difference |
| between charge and discharge: |
| |
| .. test_list:: |
| |
| generic_battery_examples:BatteryTests.ChargeDischargeCurrentDifference |
| |
| To check that a 20V USB type C power adapter is connected to port 0, add this |
| in test list: |
| |
| .. test_list:: |
| |
| generic_battery_examples:BatteryTests.Charger20VInPort0 |
| |
| """ |
| |
| import logging |
| |
| from cros.factory.device import device_utils |
| from cros.factory.test.i18n import _ |
| from cros.factory.test.i18n import arg_utils as i18n_arg_utils |
| from cros.factory.test import test_case |
| from cros.factory.utils.arg_utils import Arg |
| from cros.factory.utils import sync_utils |
| from cros.factory.utils import type_utils |
| |
| |
| def _GetPromptText(current, target): |
| return _( |
| 'Waiting for {target_status} current to meet {target_current} mA.' |
| ' (Currently {status} at {current} mA)', |
| target_status=_('charging') if target >= 0 else _('discharging'), |
| target_current=abs(target), |
| status=_('charging') if current >= 0 else _('discharging'), |
| current=abs(current)) |
| |
| |
| class BatteryCurrentTest(test_case.TestCase): |
| """A factory test to test battery charging/discharging current.""" |
| related_components = (test_case.TestCategory.BATTERY, |
| test_case.TestCategory.USB_INTEGRATED) |
| ARGS = [ |
| Arg('min_charging_current', int, 'minimum allowed charging current', |
| default=None), |
| Arg('min_discharging_current', int, 'minimum allowed discharging current', |
| default=None), |
| Arg( |
| 'current_difference', int, |
| 'The minimum current difference between the charging mode and' |
| ' discharging mode.', default=None), |
| Arg('retry_times', int, 'Retry for a number of times if the' |
| ' current_difference test fails', default=2), |
| Arg('timeout_secs', int, 'Test timeout value', default=30), |
| Arg('max_battery_level', int, 'maximum allowed starting battery level', |
| default=None), |
| Arg( |
| 'usbpd_info', list, |
| 'A sequence [usbpd_port, min_millivolt, max_millivolt] used to ' |
| 'select a particular port from a multi-port DUT.', default=None), |
| Arg( |
| 'use_max_voltage', bool, |
| 'Use the negotiated max voltage in `ectool usbpdpower` to check ' |
| 'charger voltage, in case that instant voltage is not supported.', |
| default=False), |
| i18n_arg_utils.I18nArg('usbpd_prompt', |
| 'prompt operator which port to insert', default='') |
| ] |
| |
| def setUp(self): |
| self._dut = device_utils.CreateDUTInterface() |
| self._power = self._dut.power |
| # yapf: disable |
| if self.args.usbpd_info: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long |
| # yapf: enable |
| # yapf: disable |
| self._CheckUSBPDInfoArg(self.args.usbpd_info) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long |
| # yapf: enable |
| # yapf: disable |
| self._usbpd_port = self.args.usbpd_info[0] # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long |
| # yapf: enable |
| # yapf: disable |
| self._usbpd_min_millivolt = self.args.usbpd_info[1] # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long |
| # yapf: enable |
| # yapf: disable |
| self._usbpd_max_millivolt = self.args.usbpd_info[2] # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long |
| # yapf: enable |
| # yapf: disable |
| self._usbpd_prompt = self.args.usbpd_prompt # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long |
| # yapf: enable |
| |
| def _CheckUSBPDInfoArg(self, info): |
| if len(info) == 5: |
| check_types = (int, str, str, int, int) |
| elif len(info) == 3: |
| # yapf: disable |
| check_types = (int, int, int) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long |
| # yapf: enable |
| else: |
| raise ValueError('ERROR: invalid usbpd_info item: ' + str(info)) |
| |
| for i, (info_i, check_type) in enumerate(zip(info, check_types)): |
| if not isinstance(info_i, check_type): |
| logging.error('(%s)usbpd_info[%d] type is not %s', type(info_i), i, |
| check_type) |
| raise ValueError(f'ERROR: invalid usbpd_info[{int(i)}]: ' + str(info)) |
| |
| def _LogCurrent(self, current): |
| if current >= 0: |
| logging.info('Charging current = %d mA', current) |
| else: |
| logging.info('Discharging current = %d mA', -current) |
| |
| def _CheckUSBPD(self) -> bool: |
| for unused_i in range(10): |
| try: |
| status = self._dut.usb_c.GetPDPowerStatus() |
| # yapf: disable |
| voltage_field = ('max_millivolt' if self.args.use_max_voltage else 'millivolt') # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long |
| # yapf: enable |
| if voltage_field not in status[self._usbpd_port]: |
| # yapf: disable |
| self.ui.SetState( # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long |
| # yapf: enable |
| _('Insert power to {prompt}({voltage}mV)', |
| prompt=self._usbpd_prompt, |
| voltage=0)) |
| logging.info('No millivolt detected in port %d', self._usbpd_port) |
| return False |
| millivolt = status[self._usbpd_port][voltage_field] |
| logging.info('millivolt %d, acceptable range (%d, %d)', millivolt, |
| self._usbpd_min_millivolt, self._usbpd_max_millivolt) |
| # yapf: disable |
| self.ui.SetState( # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long |
| # yapf: enable |
| _('Insert power to {prompt}({voltage}mV)', |
| prompt=self._usbpd_prompt, |
| voltage=millivolt)) |
| if not (self._usbpd_min_millivolt <= millivolt <= |
| self._usbpd_max_millivolt): |
| return False |
| except Exception as e: |
| logging.exception('Error checking USB-PD status: %s', e) |
| return False |
| self.Sleep(0.1) |
| return True |
| |
| |
| def _CheckCharge(self): |
| current = self._power.GetBatteryCurrent() |
| # yapf: disable |
| target = self.args.min_charging_current # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long |
| # yapf: enable |
| self._LogCurrent(current) |
| # yapf: disable |
| self.ui.SetState(_GetPromptText(current, target)) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long |
| # yapf: enable |
| return current >= target |
| |
| def _CheckDischarge(self): |
| current = self._power.GetBatteryCurrent() |
| # yapf: disable |
| target = self.args.min_discharging_current # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long |
| # yapf: enable |
| self._LogCurrent(current) |
| # yapf: disable |
| self.ui.SetState(_GetPromptText(current, -target)) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long |
| # yapf: enable |
| return -current >= target |
| |
| def _GetAverageCurrent(self, total_measure_secs=3, |
| poll_interval_secs=0.2) -> float: |
| """Average current for a given period of time.""" |
| times_to_count = int(total_measure_secs / poll_interval_secs) |
| acc_current = 0 |
| for _ in range(times_to_count): |
| acc_current += self._power.GetBatteryCurrent() |
| self.Sleep(poll_interval_secs) |
| |
| return acc_current / times_to_count |
| |
| def _CheckCurrentDifference(self): |
| # yapf: disable |
| target = self.args.current_difference # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long |
| # yapf: enable |
| |
| self._power.SetChargeState(self._power.ChargeState.CHARGE) |
| # It takes 1~2 seconds for the power state to change, |
| # so sleeping for 3 seconds should be enough. |
| self.Sleep(3) |
| charging_current = self._GetAverageCurrent() |
| logging.info('Average current in charging mode: %f', charging_current) |
| |
| self._power.SetChargeState(self._power.ChargeState.DISCHARGE) |
| self.Sleep(3) |
| discharging_current = self._GetAverageCurrent() |
| logging.info('Average current in discharging mode: %f', discharging_current) |
| present_difference = charging_current - discharging_current |
| |
| # yapf: disable |
| self.ui.SetState( # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long |
| # yapf: enable |
| _( |
| 'Average current in charging mode is {charging_current}<br>' |
| 'Average current in discharging mode is {discharging_current}<br>' |
| 'Target current difference is {target}, but present current' |
| ' difference is {present_difference}', |
| charging_current=charging_current, |
| discharging_current=discharging_current, target=target, |
| present_difference=present_difference)) |
| return present_difference >= target |
| |
| def _CheckCurrentDifferenceWithRetry(self): |
| # yapf: disable |
| self.ui.SetState( # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long |
| # yapf: enable |
| _('Calculating the current difference between charging mode and' |
| ' discharging mode.')) |
| |
| times = 0 |
| |
| # yapf: disable |
| @sync_utils.RetryDecorator(max_attempt_count=self.args.retry_times, # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long |
| # yapf: enable |
| target_condition=lambda x: x) |
| def _CheckCurrentDiffs(): |
| if self._CheckCurrentDifference(): |
| return True |
| logging.info('CheckCurrentDifference failed. This is the %d try.', |
| times + 1) |
| # yapf: disable |
| self.ui.SetState( # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long |
| # yapf: enable |
| _('CheckCurrentDifference failed. This is the {times} try', |
| times=times + 1), append=True) |
| return False |
| |
| try: |
| _CheckCurrentDiffs() |
| except type_utils.MaxRetryError: |
| self.FailTask('battery_current test failed.') |
| |
| def runTest(self): |
| """Main entrance of charger test.""" |
| self.assertTrue(self._power.CheckBatteryPresent(), 'No battery present') |
| # yapf: disable |
| if self.args.max_battery_level: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long |
| # yapf: enable |
| self.assertLessEqual( |
| self._power.GetChargePct(), |
| # yapf: disable |
| self.args.max_battery_level, # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long |
| # yapf: enable |
| 'Starting battery level too high') |
| # yapf: disable |
| if self.args.usbpd_info is not None: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long |
| # yapf: enable |
| sync_utils.PollForCondition( |
| poll_method=self._CheckUSBPD, poll_interval_secs=0.5, |
| condition_name='CheckUSBPD', |
| # yapf: disable |
| timeout_secs=self.args.timeout_secs) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long |
| # yapf: enable |
| # yapf: disable |
| if self.args.min_charging_current: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long |
| # yapf: enable |
| self._power.SetChargeState(self._power.ChargeState.CHARGE) |
| sync_utils.PollForCondition( |
| poll_method=self._CheckCharge, poll_interval_secs=0.5, |
| condition_name='ChargeCurrent', |
| # yapf: disable |
| timeout_secs=self.args.timeout_secs) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long |
| # yapf: enable |
| # yapf: disable |
| if self.args.min_discharging_current: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long |
| # yapf: enable |
| self._power.SetChargeState(self._power.ChargeState.DISCHARGE) |
| sync_utils.PollForCondition(poll_method=self._CheckDischarge, |
| poll_interval_secs=0.5, |
| condition_name='DischargeCurrent', |
| # yapf: disable |
| timeout_secs=self.args.timeout_secs) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long |
| # yapf: enable |
| # yapf: disable |
| if self.args.current_difference: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long |
| # yapf: enable |
| self._CheckCurrentDifferenceWithRetry() |
| |
| def tearDown(self): |
| # Must enable charger to charge or we will drain the battery! |
| self._power.SetChargeState(self._power.ChargeState.CHARGE) |