# 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'])