blob: 06c9c29efe5404095bebb17374a5c8bf3be45658 [file] [log] [blame]
// Copyright 2021 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package firmware
import (
"context"
"strings"
"time"
"github.com/golang/protobuf/ptypes/empty"
"go.chromium.org/tast-tests/cros/common/servo"
"go.chromium.org/tast-tests/cros/remote/firmware"
"go.chromium.org/tast-tests/cros/remote/firmware/fixture"
"go.chromium.org/tast-tests/cros/remote/powercontrol"
"go.chromium.org/tast/core/ctxutil"
"go.chromium.org/tast/core/dut"
"go.chromium.org/tast/core/errors"
"go.chromium.org/tast/core/testing"
"go.chromium.org/tast/core/testing/hwdep"
)
type testArgsForECWakeOnCharge struct {
formFactor string
tabletModeOff string
hasLid bool
}
type retriableErr struct {
*errors.E
}
type debugInformation struct {
servoConnectionType string
servoType string
hasMicroOrC2D2 bool
hasCCD bool
}
func init() {
testing.AddTest(&testing.Test{
Func: ECWakeOnCharge,
Desc: "Checks that device will charge when EC is in a low-power mode, as a replacement for manual test 1.4.11",
LacrosStatus: testing.LacrosVariantUnneeded,
Contacts: []string{
"chromeos-faft@google.com",
"arthur.chuang@cienet.com",
},
BugComponent: "b:792402", // ChromeOS > Platform > Enablement > Firmware > FAFT
// TODO: When stable, change firmware_unstable to a different attr.
Attr: []string{"group:firmware", "firmware_unstable", "firmware_bringup"},
SoftwareDeps: []string{"chrome"},
Vars: []string{"board", "model"},
Fixture: fixture.NormalMode,
ServiceDeps: []string{"tast.cros.firmware.UtilsService"},
HardwareDeps: hwdep.D(hwdep.ChromeEC(), hwdep.Battery()),
Timeout: 40 * time.Minute,
Params: []testing.Param{{
Name: "chromeslate",
ExtraHardwareDeps: hwdep.D(hwdep.FormFactor(hwdep.Chromeslate)),
Val: &testArgsForECWakeOnCharge{
formFactor: "chromeslate",
hasLid: false,
},
}, {
Name: "convertible",
ExtraHardwareDeps: hwdep.D(hwdep.FormFactor(hwdep.Convertible)),
Val: &testArgsForECWakeOnCharge{
formFactor: "convertible",
tabletModeOff: "tabletmode off",
hasLid: true,
},
}, {
Name: "detachable",
ExtraHardwareDeps: hwdep.D(hwdep.FormFactor(hwdep.Detachable)),
Val: &testArgsForECWakeOnCharge{
formFactor: "detachable",
tabletModeOff: "basestate attach",
hasLid: true,
},
}, {
ExtraHardwareDeps: hwdep.D(hwdep.FormFactor(hwdep.Clamshell)),
Val: &testArgsForECWakeOnCharge{
formFactor: "clamshell",
hasLid: true,
},
}},
})
}
var (
tabletModeAngleHasChanged bool
checkedInfo debugInformation
)
func ECWakeOnCharge(ctx context.Context, s *testing.State) {
h := s.FixtValue().(*fixture.Value).Helper
board, _ := s.Var("board")
model, _ := s.Var("model")
h.OverridePlatform(ctx, board, model)
if err := h.RequireConfig(ctx); err != nil {
s.Fatal("Failed to get config: ", err)
}
if err := h.RequireServo(ctx); err != nil {
s.Fatal("Failed to connect to servo: ", err)
}
// Increase timeout in getting response from ec uart.
if err := h.Servo.SetString(ctx, "ec_uart_timeout", "10"); err != nil {
s.Fatal("Failed to extend ec uart timeout: ", err)
}
defer func() {
testing.ContextLog(ctx, "Restoring ec uart timeout to the default value of 3 seconds")
if err := h.Servo.SetString(ctx, "ec_uart_timeout", "3"); err != nil {
s.Fatal("Failed to restore default ec uart timeout: ", err)
}
}()
// At start of test, save some information that may
// be useful for debugging.
if err := checkInformation(ctx, h, &checkedInfo); err != nil {
s.Fatal("Unable to log information at start of test: ", err)
}
// To prevent leaving DUT in an offline state, open lid or perform a cold reset at the end of test.
cleanupCtx := ctx
ctx, cancel := ctxutil.Shorten(ctx, 10*time.Minute)
defer cancel()
args := s.Param().(*testArgsForECWakeOnCharge)
defer func(ctx context.Context, args *testArgsForECWakeOnCharge) {
if args.hasLid {
if err := h.Servo.OpenLid(ctx); err != nil {
s.Fatal("Failed to set lid state: ", err)
}
}
if err := h.EnsureDUTBooted(ctx); err != nil {
s.Fatal("Failed to ensure DUT booted: ", err)
}
if args.formFactor == "convertible" && tabletModeAngleHasChanged {
// Check for tablet mode angles, and if they are different
// than the default values, restore them to default.
cmd := firmware.NewECTool(s.DUT(), firmware.ECToolNameMain)
tabletModeAngleInit, hysInit, err := cmd.SaveTabletModeAngles(ctx)
if err != nil {
s.Fatal("Failed to read tablet mode angles: ", err)
} else if tabletModeAngleInit != "180" || hysInit != "20" {
s.Log("Restoring ectool tablet mode angles to the default settings")
if err := cmd.ForceTabletModeAngle(ctx, "180", "20"); err != nil {
s.Fatal("Failed to restore tablet mode angles to the default settings: ", err)
}
}
}
}(cleanupCtx, args)
// Monitor servo connection in the background, and attempt to reconnect if the connection drops.
done := make(chan struct{})
defer func() {
done <- struct{}{}
}()
go func() {
monitorServo:
for {
select {
case <-done:
break monitorServo
default:
// GoBigSleepLint: Ping servo periodically to monitor whether it's alive.
if err := testing.Sleep(ctx, time.Second); err != nil {
s.Error("Failed to sleep while monitoring servo: ", err)
}
if _, err := h.Servo.Echo(ctx, "ping"); err != nil {
s.Log("Failed to ping servo, reconnecting: ", err)
err := h.ServoProxy.Reconnect(ctx)
if err != nil {
s.Error("Failed to connect to servo: ", err)
}
}
}
}
}()
for _, tc := range []struct {
lidOpen servo.LidOpenValue
}{
{servo.LidOpenYes},
{servo.LidOpenNo},
} {
// Only repeat the test in lid closed when device has a lid.
if tc.lidOpen == servo.LidOpenNo && !args.hasLid {
break
}
s.Log("Stopping AC Power")
if err := powercontrol.PlugUnplugCharger(ctx, h, false); err != nil {
s.Fatal("Failed to stop power supply: ", err)
}
// Skip setting the lid state for DUTs that don't have a lid, i.e. Chromeslates.
if args.hasLid {
if tc.lidOpen == servo.LidOpenNo {
// Closing lid might cause servod exit if the CCD watchdog is not removed.
s.Log("Removing ccd watchdogs")
if err := h.Servo.RemoveCCDWatchdogs(ctx); err != nil {
s.Fatal("Failed to remove CCD watchdog: ", err)
}
}
s.Logf("-------------Test with lid open: %s-------------", tc.lidOpen)
if err := testing.Poll(ctx, func(ctx context.Context) error {
if err := h.Servo.SetStringAndCheck(ctx, servo.LidOpen, string(tc.lidOpen)); err != nil {
// This error may be temporary.
if strings.Contains(err.Error(), "No data was sent from the pty") ||
strings.Contains(err.Error(), "Timed out waiting for interfaces to become available") {
return err
}
return testing.PollBreak(err)
}
return nil
}, &testing.PollOptions{Timeout: 3 * time.Minute, Interval: 1 * time.Second}); err != nil {
s.Fatal("Failed to set lid state: ", err)
}
// There's a chance that CCD would close when lid closed.
// Open CCD to prevent errors from running EC commands.
if tc.lidOpen == servo.LidOpenNo {
if err := h.OpenCCD(ctx, true, true); err != nil {
s.Fatal("Failed to open CCD after closing lid: ", err)
}
}
}
if h.Config.Hibernate {
if err := hibernateDUT(ctx, h, s.DUT(), checkedInfo.hasMicroOrC2D2, tc.lidOpen, args.formFactor, args.tabletModeOff); err != nil {
s.Fatal("Failed to hibernate DUT: ", err)
}
} else {
// For DUTs that do not support the ec hibernation command, when lid is open, we could use
// a long power button press instead to put DUT in deep sleep. But, when lid is closed without
// log-in, power state will eventually reach G3.
if tc.lidOpen == servo.LidOpenYes {
s.Logf("Long pressing on power key for %s to put DUT into deep sleep mode", h.Config.HoldPwrButtonPowerOff)
if err := h.Servo.KeypressWithDuration(ctx, servo.PowerKey, servo.Dur(h.Config.HoldPwrButtonPowerOff)); err != nil {
s.Fatal("Failed to set a KeypressControl by servo: ", err)
}
}
s.Log("Waiting for power state to become G3 or S3")
if err := h.WaitForPowerStates(ctx, firmware.PowerStateInterval, 1*time.Minute, "G3", "S3"); err != nil {
s.Fatal("Failed to get powerstates at G3 or S3: ", err)
}
}
s.Log("Reconnecting AC")
if err := checkECWakesFromACReconnected(ctx, h, string(tc.lidOpen)); err != nil {
_, ok := err.(*retriableErr)
if ok && !h.Config.ACOnCanWakeApFromUlp {
s.Log("AC on event cannot wake AP, putting AP back to SO with power button")
if err := h.WaitForPowerStates(ctx, firmware.PowerStateInterval, firmware.PowerStateTimeout, "G3"); err != nil {
s.Fatal("Failed to get G3 power state: ", err)
}
durToWakeToS0 := servo.DurTab
if h.Config.Platform == "kukui" {
// Long press the power button to boot the device to S0.
durToWakeToS0 = servo.DurLongPress
}
if err := h.Servo.KeypressWithDuration(ctx, servo.PowerKey, durToWakeToS0); err != nil {
s.Fatal("Failed to press power button: ", err)
}
s.Log("Waiting for DUT to reconnect")
waitConnectCtx, cancelWaitConnect := context.WithTimeout(ctx, h.Config.DelayRebootToPing)
defer cancelWaitConnect()
if err := h.WaitConnect(waitConnectCtx); err != nil {
s.Fatal("Failed to reconnect to DUT: ", err)
}
} else {
s.Fatal("Failed to wake EC from AC reconnected: ", err)
}
}
// Testhaus results showed that on Nami DUTs, when tested with lid closed,
// lid's state changed after waking up from hibernation. More research
// might be needed, but for now skip on checking lid state for Nami devices.
if args.hasLid && h.Config.Platform != "nami" {
// Verify DUT's lid current state remains the same as the initial state.
s.Log("Checking lid state hasn't changed")
lidStateFinal, err := h.Servo.LidOpenState(ctx)
if err != nil {
s.Fatal("Failed to check the final lid state: ", err)
}
if lidStateFinal != string(tc.lidOpen) {
s.Fatalf("DUT's lid_open state has changed from %s to %s", tc.lidOpen, lidStateFinal)
}
}
}
}
// checkTabletModeStatus checks whether ChromeOS is in tablet mode through the utils service.
func checkTabletModeStatus(ctx context.Context, h *firmware.Helper) (bool, error) {
if err := h.RequireRPCUtils(ctx); err != nil {
return false, errors.Wrap(err, "requiring RPC utils")
}
if _, err := h.RPCUtils.NewChrome(ctx, &empty.Empty{}); err != nil {
return false, errors.Wrap(err, "failed to create instance of chrome")
}
defer h.RPCUtils.CloseChrome(ctx, &empty.Empty{})
res, err := h.RPCUtils.EvalTabletMode(ctx, &empty.Empty{})
if err != nil {
return false, err
}
return res.TabletModeEnabled, nil
}
func checkInformation(ctx context.Context, h *firmware.Helper, info *debugInformation) error {
var wantInfo = []string{"hasMicroOrC2D2", "hasCCD", "servoType", "servoConnectionType"}
for _, val := range wantInfo {
var err error
switch val {
case "hasMicroOrC2D2":
info.hasMicroOrC2D2, err = h.Servo.PreferDebugHeader(ctx)
case "hasCCD":
info.hasCCD, err = h.Servo.HasCCD(ctx)
case "servoType":
info.servoType, err = h.Servo.GetServoType(ctx)
case "servoConnectionType":
info.servoConnectionType, err = h.Servo.GetString(ctx, servo.DUTConnectionType)
}
if err != nil {
return errors.Wrapf(err, "failed to check for %s", val)
}
}
testing.ContextLogf(ctx, "DUT has micro or c2d2: %t, has CCD: %t, servo type: %s, servo connection type: %s",
info.hasMicroOrC2D2, info.hasCCD, info.servoType, info.servoConnectionType)
return nil
}
func ensureClamshellMode(ctx context.Context, h *firmware.Helper, dut *dut.DUT, formFactor, tabletModeOff string) error {
inTabletMode, err := checkTabletModeStatus(ctx, h)
if err != nil {
return errors.Wrap(err, "unable to check for DUT's tablet mode status")
}
if inTabletMode {
testing.ContextLog(ctx, "DUT is in tablet mode. Attempting to turn tablet mode off")
out, err := h.Servo.RunTabletModeCommandGetOutput(ctx, tabletModeOff)
if err != nil {
if formFactor == "convertible" {
testing.ContextLogf(ctx, "Failed to run %s: %v. Attempting to set rotation angles with ectool instead", tabletModeOff, err)
cmd := firmware.NewECTool(dut, firmware.ECToolNameMain)
// Setting tabletModeAngle to 360 will force DUT into clamshell mode.
if err := cmd.ForceTabletModeAngle(ctx, "360", "0"); err != nil {
return errors.Wrap(err, "failed to set DUT in clamshell mode")
}
tabletModeAngleHasChanged = true
return nil
}
return errors.Wrapf(err, "failed to run %s", tabletModeOff)
}
testing.ContextLogf(ctx, "Tablet mode status: %s", out)
}
return nil
}
func hibernateDUT(ctx context.Context, h *firmware.Helper, dut *dut.DUT, hasMicroOrC2D2 bool, lidOpen servo.LidOpenValue, formFactor, tabletModeOff string) error {
switch lidOpen {
case servo.LidOpenNo:
if hasMicroOrC2D2 {
// In cases where lid is closed, and there's a servo_micro or C2D2 connection,
// use console command to hibernate. Using keyboard presses might trigger DUT
// to wake, and interrupt lid emulation.
testing.ContextLog(ctx, "Putting DUT in hibernation with EC console command")
if err := h.Servo.ECHibernate(ctx, h.Model, servo.UseConsole); err != nil {
return errors.Wrap(err, "failed to hibernate")
}
return nil
}
// Note: In most cases, when lid is closed without log-in, power state transitions from S0 to S5,
// and then eventually to G3, which would be equivalent to long pressing on power to put DUT asleep.
// However, recent Testhaus results showed that dedede(kracko) remained connected with power state
// at S0 after lid closed. Attempt power-off if closing lid didn't put DUTs in G3.
testing.ContextLog(ctx, "Waiting for power state to become G3 or S5")
if err := h.WaitForPowerStates(ctx, firmware.PowerStateInterval, 5*time.Minute, "G3", "S5"); err != nil {
// Sometimes EC becomes unresponsive when lid is closed. But, for this test's
// purposes, we're more concerned about whether DUT wakes when AC is re-attached.
if strings.Contains(err.Error(), "No data was sent from the pty") ||
strings.Contains(err.Error(), "Timed out waiting for interfaces to become available") {
testing.ContextLog(ctx, "DUT appears to be completely offline. We're okay as long as reconnecting power resumes it")
return nil
}
if h.DUT.Connected(ctx) {
testing.ContextLog(ctx, "Found dut still connected after lid closed, attempting power-off")
if err := h.DUT.Conn().CommandContext(ctx, "poweroff").Start(); err != nil {
return errors.Wrap(err, "failed to run poweroff cmd")
}
if err := h.DUT.WaitUnreachable(ctx); err != nil {
return errors.Wrap(err, "failed to wait for DUT unreachable")
}
return nil
}
return errors.Wrap(err, "failed to get powerstates at G3 or S5")
}
case servo.LidOpenYes:
if formFactor == "convertible" || formFactor == "detachable" {
// For convertibles and detachables, if DUT was left in tablet mode by previous tests,
// key presses may be inactive. Always turn tablet mode off before using keyboard to hibernate.
testing.ContextLogf(ctx, "DUT is a %s. Checking tablet mode status before using keyboard to hibernate", formFactor)
if err := ensureClamshellMode(ctx, h, dut, formFactor, tabletModeOff); err != nil {
return err
}
}
h.DisconnectDUT(ctx)
testing.ContextLog(ctx, "Putting DUT in hibernation with key presses")
if err := h.Servo.ECHibernate(ctx, h.Model, servo.UseKeyboard); err != nil {
testing.ContextLogf(ctx, "Failed to hibernate: %v. Retry with using EC console command to hibernate", err)
if err := h.Servo.ECHibernate(ctx, h.Model, servo.UseConsole); err != nil {
return errors.Wrap(err, "failed to hibernate")
}
}
}
return nil
}
// checkECWakesFromACReconnected plugs the charger, probes the ec for
// response, and expect the DUT to reconnect if the lid was open, or
// remain at G3 if the lid was closed. It is called to wake the DUT
// from hibernation.
func checkECWakesFromACReconnected(ctx context.Context, h *firmware.Helper, lidOpen string) error {
// getChargerPollOptions sets the time to retry the GetChargerAttached command.
var getChargerPollOptions = testing.PollOptions{
Timeout: 1 * time.Minute,
Interval: 1 * time.Second,
}
// getPDRolePollOptions sets the time to retry the GetPDRole command.
var getPDRolePollOptions = testing.PollOptions{
Timeout: 10 * time.Second,
Interval: 1 * time.Second,
}
// runECPollOptions sets the time to retry the RunECCommandGetOutput command.
var runECPollOptions = testing.PollOptions{
Timeout: 1 * time.Minute,
Interval: 1 * time.Second,
}
if err := h.SetDUTPower(ctx, true); err != nil {
return errors.Wrap(err, "failed to connect charger")
}
if checkedInfo.servoConnectionType == "type-c" {
if err := testing.Poll(ctx, func(ctx context.Context) error {
role, err := h.Servo.GetPDRole(ctx)
if err != nil {
return errors.Wrap(err, "failed to retrieve USB PD role for servo")
}
if role != servo.PDRoleSrc {
return errors.Wrapf(err, "setting pd role to src, but got: %q", role)
}
return nil
}, &getPDRolePollOptions); err != nil {
return err
}
}
// Verify EC console is responsive.
if err := testing.Poll(ctx, func(ctx context.Context) error {
// Cr50 goes to sleep during hibernation, and when DUT wakes, CCD state might be locked.
// Open CCD before talking to the EC.
if err := h.OpenCCD(ctx, true, true); err != nil {
return errors.Wrap(err, "failed to open CCD")
}
if _, err := h.Servo.RunECCommandGetOutput(ctx, "version", []string{`.`}); err != nil {
return errors.Wrap(err, "EC is not responsive after reconnecting power supply to DUT")
}
return nil
}, &runECPollOptions); err != nil {
return errors.Wrap(err, "failed to wait for EC to become responsive")
}
testing.ContextLog(ctx, "EC is responsive")
// Verify that DUT is charging with power supply connected.
if err := testing.Poll(ctx, func(ctx context.Context) error {
ok, err := h.Servo.GetChargerAttached(ctx)
if err != nil {
return errors.Wrap(err, "error checking whether charger is attached")
}
if !ok {
return errors.New("DUT is not charging after connecting the power supply")
}
return nil
}, &getChargerPollOptions); err != nil {
return errors.Wrap(err, "failed to check for charger after connecting power")
}
testing.ContextLog(ctx, "DUT is charging")
if lidOpen == string(servo.LidOpenYes) {
testing.ContextLog(ctx, "Waiting for DUT to power ON")
waitConnectCtx, cancelWaitConnect := context.WithTimeout(ctx, h.Config.DelayRebootToPing)
defer cancelWaitConnect()
var opts []firmware.WaitConnectOption
opts = append(opts, firmware.FromHibernation)
err := h.WaitConnect(waitConnectCtx, opts...)
if errors.As(err, &context.DeadlineExceeded) {
// For some devices, the ac on event cannot wake the AP.
// Return a retriableErr and allow the test to continue by
// booting up the device with a short power button press.
return &retriableErr{E: errors.Wrap(err, "timeout in reconnecting to DUT")}
}
return err
}
// When lid is closed, DUTs remain in G3 after waking up from
// hibernation with charger reconnected.
testing.ContextLog(ctx, "Checking for power state at G3")
if err := h.WaitForPowerStates(ctx, firmware.PowerStateInterval, firmware.PowerStateTimeout, "G3"); err != nil {
return errors.Wrap(err, "failed to get power state at G3")
}
return nil
}