blob: ff6feac205217efa4450d8740268e3351ccd5fe0 [file] [log] [blame]
// 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.
package firmware
/*
This file implements functions to check or switch the DUT's boot mode.
*/
import (
"context"
"time"
fwCommon "chromiumos/tast/common/firmware"
"chromiumos/tast/common/servo"
"chromiumos/tast/errors"
fwpb "chromiumos/tast/services/cros/firmware"
"chromiumos/tast/ssh"
"chromiumos/tast/testing"
)
const (
// cmdTimeout is a short duration used for sending commands.
cmdTimeout = 6 * time.Second
// offTimeout is the timeout to wait for the DUT to be unreachable after powering off.
offTimeout = 3 * time.Minute
// PowerStateTimeout is the timeout to wait for the DUT reach a powerstate.
PowerStateTimeout = 20 * time.Second
// PowerStateInterval is the interval to wait before polling DUT powerstate.
PowerStateInterval = 250 * time.Millisecond
// reconnectTimeout is the timeout to wait to reconnect to the DUT after rebooting.
reconnectTimeout = 3 * time.Minute
// usbVisibleTime is the time to wait after making the USB stick visible to DUT
usbVisibleTime = 5 * time.Second
)
// ModeSwitcher enables booting the DUT into different firmware boot modes (normal, dev, rec).
type ModeSwitcher struct {
Helper *Helper
}
// NewModeSwitcher creates a new ModeSwitcher. It relies on a firmware Helper to track dependent objects, such as servo and RPC client.
func NewModeSwitcher(ctx context.Context, h *Helper) (*ModeSwitcher, error) {
if err := h.RequireConfig(ctx); err != nil {
return nil, errors.Wrap(err, "requiring firmware config")
}
return &ModeSwitcher{
Helper: h,
}, nil
}
// ModeSwitchOption allows mode-switching methods to exhibit different behaviors.
type ModeSwitchOption int
const (
// AllowGBBForce allows the DUT to force rebooting into dev mode via GBB flags.
// This way of switching is more reliable, but is not appropriate for all tests.
AllowGBBForce ModeSwitchOption = iota
// AssumeGBBFlagsCorrect skips setting the GBB flags when switching modes.
// This can save some time if the GBB flags are known to be in the desired state.
AssumeGBBFlagsCorrect ModeSwitchOption = iota
// CopyTastFiles copies the Tast files from the DUT before rebooting, and writes them back to the DUT afterwards.
// This is necessary if you want to use any gRPC services.
CopyTastFiles ModeSwitchOption = iota
// SkipModeCheckAfterReboot can be passed in as an option to ModeAwareReboot, skipping
// boot mode check after resetting DUT. One instance where this can be useful is
// when verifying that FWMP prevents DUT from booting into dev mode.
SkipModeCheckAfterReboot ModeSwitchOption = iota
// PressEnterAtToNorm presses ENTER to allow DUT to continue to boot when dev mode disabled by FWMP.
PressEnterAtToNorm ModeSwitchOption = iota
)
// msOptsContain determines whether a slice of ModeSwitchOptions contains a specific Option.
func msOptsContain(opts []ModeSwitchOption, want ModeSwitchOption) bool {
for _, o := range opts {
if o == want {
return true
}
}
return false
}
// RebootToMode reboots the DUT into the specified boot mode.
// This has the side-effect of disconnecting the RPC client.
// Requires `SoftwareDeps: []string{"crossystem", "flashrom"},`.
func (ms ModeSwitcher) RebootToMode(ctx context.Context, toMode fwCommon.BootMode, opts ...ModeSwitchOption) error {
h := ms.Helper
if err := h.RequireServo(ctx); err != nil {
return errors.Wrap(err, "requiring servo")
}
fromMode, err := h.Reporter.CurrentBootMode(ctx)
if err != nil {
return errors.Wrap(err, "determining boot mode at the start of RebootToMode")
}
// Unless AssumeGBBFlagsCorrect is passed, fix the GBB flags for the desired boot mode.
if !msOptsContain(opts, AssumeGBBFlagsCorrect) {
flags := fwpb.GBBFlagsState{}
if msOptsContain(opts, AllowGBBForce) {
switch toMode {
case fwCommon.BootModeDev:
flags.Clear = append(flags.Clear, fwpb.GBBFlag_FORCE_DEV_BOOT_USB)
flags.Set = append(flags.Set, fwpb.GBBFlag_FORCE_DEV_SWITCH_ON, fwpb.GBBFlag_DEV_SCREEN_SHORT_DELAY)
case fwCommon.BootModeUSBDev:
flags.Set = append(flags.Set, fwpb.GBBFlag_FORCE_DEV_BOOT_USB, fwpb.GBBFlag_FORCE_DEV_SWITCH_ON, fwpb.GBBFlag_DEV_SCREEN_SHORT_DELAY)
default:
flags.Clear = append(flags.Clear, fwpb.GBBFlag_FORCE_DEV_SWITCH_ON, fwpb.GBBFlag_DEV_SCREEN_SHORT_DELAY, fwpb.GBBFlag_FORCE_DEV_BOOT_USB)
}
} else {
flags.Clear = append(flags.Clear, fwpb.GBBFlag_FORCE_DEV_SWITCH_ON, fwpb.GBBFlag_DEV_SCREEN_SHORT_DELAY, fwpb.GBBFlag_FORCE_DEV_BOOT_USB)
}
if err := fwCommon.ClearAndSetGBBFlags(ctx, h.DUT, &flags); err != nil {
return errors.Wrap(err, "setting GBB flags")
}
}
// When booting to a different image, such as normal vs. recovery, the new image might
// not have Tast host files installed. So, store those files on the test server and reinstall later.
fromModeUsb := false
toModeUsb := false
if fromMode == fwCommon.BootModeRecovery || fromMode == fwCommon.BootModeUSBDev {
fromModeUsb = true
}
if toMode == fwCommon.BootModeRecovery || toMode == fwCommon.BootModeUSBDev {
toModeUsb = true
}
if fromModeUsb != toModeUsb && !h.DoesServerHaveTastHostFiles() && msOptsContain(opts, CopyTastFiles) {
if err := h.CopyTastFilesFromDUT(ctx); err != nil {
return errors.Wrap(err, "copying Tast files from DUT to test server")
}
// Remember which image the Tast files came from.
if fromModeUsb {
h.dutUsbHasTastFiles = true
} else {
h.dutInternalStorageHasTastFiles = true
}
}
// Perform sync prior to reboot, then close the RPC connection.
if err := h.DUT.Conn().CommandContext(ctx, "sync").Run(ssh.DumpLogOnError); err != nil {
testing.ContextLogf(ctx, "Failed to sync DUT: %s", err)
}
h.CloseRPCConnection(ctx)
// Booting from rec to anything else will cause EC to restart, potentally breaking the servo watchdog.
if fromMode == fwCommon.BootModeRecovery {
if err := h.Servo.WatchdogRemove(ctx, servo.WatchdogCCD); err != nil {
return errors.Wrap(err, "failed to remove watchdog for ccd")
}
}
switch toMode {
case fwCommon.BootModeNormal:
hasSerialAP := false
if fromMode != fwCommon.BootModeNormal {
hasSerialAP = ms.hasSerialAPFirmware(ctx)
}
if err := ms.PowerOff(ctx); err != nil {
return errors.Wrap(err, "powering off DUT")
}
if err := h.Servo.SetUSBMuxState(ctx, servo.USBMuxOff); err != nil {
return errors.Wrap(err, "disable usb for normal")
}
if err := h.Servo.SetPowerState(ctx, servo.PowerStateOn); err != nil {
return err
}
if fromMode != fwCommon.BootModeNormal {
if err := ms.fwScreenToNormalMode(ctx, hasSerialAP); err != nil {
return errors.Wrap(err, "moving from firmware screen to normal mode")
}
}
// Reconnect to the DUT.
testing.ContextLog(ctx, "Reestablishing connection to DUT")
connectCtx, cancel := context.WithTimeout(ctx, reconnectTimeout)
defer cancel()
if err := h.WaitConnect(connectCtx); err != nil {
return errors.Wrapf(err, "failed to reconnect to DUT after booting to %s", toMode)
}
case fwCommon.BootModeRecovery:
// Recovery mode requires the DUT to boot the image on the USB.
// Thus, the servo must show the USB to the DUT.
if err := ms.enableRecMode(ctx, servo.USBMuxDUT); err != nil {
return err
}
// Reconnect to the DUT.
testing.ContextLog(ctx, "Reestablishing connection to DUT")
connectCtx, cancel := context.WithTimeout(ctx, reconnectTimeout)
defer cancel()
if err := h.WaitConnect(connectCtx); err != nil {
return errors.Wrapf(err, "failed to reconnect to DUT after booting to %s", toMode)
}
case fwCommon.BootModeDev:
testing.ContextLog(ctx, "Disabling dev_boot_usb, disabling dev_boot_signed_only")
if err := h.DUT.Conn().CommandContext(ctx, "crossystem", "dev_boot_usb=0", "dev_boot_signed_only=0", "dev_default_boot=disk").Run(ssh.DumpLogOnError); err != nil {
return errors.Wrap(err, "disabling dev_boot_usb")
}
if msOptsContain(opts, AllowGBBForce) {
// 1. Set the GBB flag which forces dev mode upon reboot.
// This was handled earlier in this function, prior to terminating the RPC connection.
// 2. Reboot the DUT.
if err := h.DUT.Reboot(ctx); err != nil {
return errors.Wrap(err, "rebooting DUT to force dev mode via GBB")
}
break
}
hasSerialAP := ms.hasSerialAPFirmware(ctx)
transitionToDev := true
// Recovery -> Dev sometimes gets stuck on the recovery screen. Try a normal reboot first.
// Even if it doesn't get us back to Dev, rebooting from Normal -> Dev is less flaky.
if fromMode == fwCommon.BootModeRecovery || fromMode == fwCommon.BootModeUSBDev {
if err := h.Servo.SetPowerState(ctx, servo.PowerStateWarmReset); err != nil {
return err
}
// Depending on how we got to to dev mode, we might end up in normal mode or the recovery
// menu, so navigate to dev mode, but it that fails, fall through to the next attempt below.
if err := ms.fwScreenToDevMode(ctx, hasSerialAP); err == nil {
newMode, err := h.Reporter.CurrentBootMode(ctx)
if err != nil {
return errors.Wrap(err, "determining boot mode after simple reboot")
}
testing.ContextLogf(ctx, "Warm reset finished, DUT in %s", newMode)
transitionToDev = newMode != fwCommon.BootModeDev
}
}
if transitionToDev {
// 1. Set power_state to 'rec', but don't show the DUT a USB image to boot from.
// 2. From the firmware screen that appears, press keys to transition to dev mode.
// The specific keypresses will depend on the DUT's ModeSwitcherType.
if err := ms.enableRecMode(ctx, servo.USBMuxOff); err != nil {
return err
}
if err := ms.fwScreenToDevMode(ctx, hasSerialAP); err != nil {
return errors.Wrap(err, "moving from firmware screen to dev mode")
}
} else {
// Reconnect to the DUT.
testing.ContextLog(ctx, "Reestablishing connection to DUT")
connectCtx, cancel := context.WithTimeout(ctx, reconnectTimeout)
defer cancel()
if err := h.WaitConnect(connectCtx); err != nil {
return errors.Wrapf(err, "failed to reconnect to DUT after booting to %s", toMode)
}
}
case fwCommon.BootModeUSBDev:
transitionToDev := true
transitionToDevUsb := true
if msOptsContain(opts, AllowGBBForce) || fromMode == fwCommon.BootModeDev {
transitionToDev = false
}
hasSerialAP := ms.hasSerialAPFirmware(ctx)
// Recovery -> Dev sometimes gets stuck on the recovery screen. Try a normal reboot first.
// Even if it doesn't get us back to Dev, rebooting from Normal -> Dev is less flaky.
if fromMode == fwCommon.BootModeRecovery {
testing.ContextLog(ctx, "Rebooting to leave recovery mode")
if err := h.Servo.SetPowerState(ctx, servo.PowerStateWarmReset); err != nil {
return err
}
// Depending on how we got to to rec mode, we might end up in normal mode or the recovery
// menu, so navigate to dev mode, but it that fails, fall through to the next attempt below.
if err := ms.fwScreenToDevMode(ctx, hasSerialAP); err == nil {
newMode, err := h.Reporter.CurrentBootMode(ctx)
if err != nil {
return errors.Wrap(err, "determining boot mode after simple reboot")
}
testing.ContextLogf(ctx, "Warm reset finished, DUT in %s", newMode)
switch newMode {
case fwCommon.BootModeDev:
transitionToDev = false
case fwCommon.BootModeUSBDev:
transitionToDev = false
transitionToDevUsb = false
}
}
}
if transitionToDev {
// 1. Set power_state to 'rec', but don't show the DUT a USB image to boot from.
// 2. From the firmware screen that appears, press keys to transition to dev mode.
// The specific keypresses will depend on the DUT's ModeSwitcherType.
testing.ContextLog(ctx, "Rebooting to enter dev mode first")
if err := ms.enableRecMode(ctx, servo.USBMuxOff); err != nil {
return err
}
if err := ms.fwScreenToDevMode(ctx, hasSerialAP); err != nil {
return errors.Wrap(err, "moving from firmware screen to dev mode")
}
newMode, err := h.Reporter.CurrentBootMode(ctx)
if err != nil {
return errors.Wrap(err, "determining boot mode after reboot to dev")
}
testing.ContextLogf(ctx, "Reboot to dev finished, DUT in %s", newMode)
}
if transitionToDevUsb {
// 1. Set power_state to 'rec', but don't show the DUT a USB image to boot from.
// 2. From the firmware screen that appears, press keys to transition to dev mode.
// The specific keypresses will depend on the DUT's ModeSwitcherType.
testing.ContextLog(ctx, "Enabling dev_boot_usb, disabling dev_boot_signed_only")
if err := h.DUT.Conn().CommandContext(ctx, "crossystem", "dev_boot_usb=1", "dev_boot_signed_only=0", "dev_default_boot=usb").Run(ssh.DumpLogOnError); err != nil {
return errors.Wrap(err, "enabling dev_boot_usb")
}
testing.ContextLog(ctx, "Enabling USB")
if err := h.Servo.SetUSBMuxState(ctx, servo.USBMuxDUT); err != nil {
return err
}
testing.ContextLogf(ctx, "Sleeping %s to let USB become visible to DUT", usbVisibleTime)
if err := testing.Sleep(ctx, usbVisibleTime); err != nil {
return err
}
testing.ContextLog(ctx, "Rebooting")
powerOffCtx, cancel := context.WithTimeout(ctx, cmdTimeout)
defer cancel()
// Since the DUT will power off, deadline exceeded is expected here.
if err := h.DUT.Conn().CommandContext(powerOffCtx, "reboot").Run(); err != nil && !errors.Is(err, context.DeadlineExceeded) {
return errors.Wrapf(err, "DUT poweroff %T", err)
}
offCtx, cancel := context.WithTimeout(ctx, offTimeout)
defer cancel()
if err := ms.Helper.DUT.WaitUnreachable(offCtx); err != nil {
return errors.Wrap(err, "waiting for DUT to be unreachable after reboot")
}
if err := ms.fwScreenToUSBDevMode(ctx); err != nil {
return errors.Wrap(err, "moving from firmware screen to usb dev mode")
}
}
// Reconnect to the DUT.
testing.ContextLog(ctx, "Reestablishing connection to DUT")
connectCtx, cancel := context.WithTimeout(ctx, reconnectTimeout)
defer cancel()
if err := h.WaitConnect(connectCtx); err != nil {
return errors.Wrapf(err, "failed to reconnect to DUT after booting to %s", toMode)
}
default:
return errors.Errorf("unsupported firmware boot mode: %s", toMode)
}
// Send Tast files back to DUT.
needSync := (toModeUsb != fromModeUsb) && msOptsContain(opts, CopyTastFiles)
if toModeUsb {
needSync = needSync && !h.dutUsbHasTastFiles
} else {
needSync = needSync && !h.dutInternalStorageHasTastFiles
}
if needSync {
if err := h.SyncTastFilesToDUT(ctx); err != nil {
return errors.Wrapf(err, "syncing Tast files to DUT after booting to %s", toMode)
}
if toModeUsb {
h.dutUsbHasTastFiles = true
} else {
h.dutInternalStorageHasTastFiles = true
}
}
// Verify successful reboot.
if curr, err := h.Reporter.CurrentBootMode(ctx); err != nil {
return errors.Wrapf(err, "checking boot mode after reboot to %s", toMode)
} else if curr != toMode {
return errors.Errorf("incorrect boot mode after RebootToMode: got %s; want %s", curr, toMode)
}
testing.ContextLogf(ctx, "DUT is now in %s mode", toMode)
return nil
}
// ResetType is an enum of ways to reset a DUT: warm and cold.
type ResetType string
// There are two ResetTypes: warm and cold.
const (
// WarmReset uses the Servo control power_state=warm_reset.
WarmReset ResetType = "warm"
// ColdReset uses the Servo control power_state=reset.
// It is identical to setting the power_state to off, then on.
// It also resets the EC, as by the 'cold_reset' signal.
ColdReset ResetType = "cold"
)
// Each ResetType is associated with a particular servo.PowerStateValue.
var resetTypePowerState = map[ResetType]servo.PowerStateValue{
WarmReset: servo.PowerStateWarmReset,
ColdReset: servo.PowerStateReset,
}
// ModeAwareReboot resets the DUT with awareness of the DUT boot mode.
// Dev mode will be retained, but rec mode will default back to normal mode.
// This has the side-effect of disconnecting the RPC connection.
func (ms *ModeSwitcher) ModeAwareReboot(ctx context.Context, resetType ResetType, opts ...ModeSwitchOption) error {
h := ms.Helper
if err := h.RequireServo(ctx); err != nil {
return errors.Wrap(err, "requiring servo")
}
fromMode, err := h.Reporter.CurrentBootMode(ctx)
if err != nil {
return errors.Wrap(err, "determining boot mode at the start of ModeAwareReboot")
}
// Memorize the boot ID, so that we can compare later.
origBootID, err := h.Reporter.BootID(ctx)
if err != nil {
return errors.Wrap(err, "determining boot ID before reboot")
}
hasSerialAP := false
if fromMode == fwCommon.BootModeDev {
hasSerialAP = ms.hasSerialAPFirmware(ctx)
}
// Perform sync prior to reboot, then close the RPC connection.
if err := h.DUT.Conn().CommandContext(ctx, "sync").Run(ssh.DumpLogOnError); err != nil {
testing.ContextLogf(ctx, "Failed to sync DUT: %s", err)
}
h.CloseRPCConnection(ctx)
if fromMode == fwCommon.BootModeUSBDev {
// The USB stick should already be visible, but set the direction just to be sure.
testing.ContextLog(ctx, "Enabling USB")
if err := h.Servo.SetUSBMuxState(ctx, servo.USBMuxDUT); err != nil {
return err
}
testing.ContextLogf(ctx, "Sleeping %s to let USB become visible to DUT", usbVisibleTime)
if err := testing.Sleep(ctx, usbVisibleTime); err != nil {
return err
}
}
// Reset DUT, and wait for it to be unreachable.
powerState, ok := resetTypePowerState[resetType]
if !ok {
return errors.Errorf("no power state associated with resetType %v", resetType)
}
if err := h.Servo.SetPowerState(ctx, powerState); err != nil {
return err
}
// If in dev mode, bypass the TO_DEV screen.
if fromMode == fwCommon.BootModeDev {
if err := ms.fwScreenToDevMode(ctx, hasSerialAP, opts...); err != nil {
return errors.Wrap(err, "bypassing fw screen")
}
} else if fromMode == fwCommon.BootModeUSBDev {
if err := ms.fwScreenToUSBDevMode(ctx); err != nil {
return errors.Wrap(err, "bypassing fw screen")
}
} else {
testing.ContextLog(ctx, "Reestablishing connection to DUT")
connectCtx, cancel := context.WithTimeout(ctx, reconnectTimeout)
defer cancel()
if err := h.WaitConnect(connectCtx); err != nil {
return errors.Wrap(err, "failed to connect to DUT")
}
}
if bootID, err := h.Reporter.BootID(ctx); err != nil {
return errors.Wrap(err, "reporting boot ID")
} else if bootID == origBootID {
return errors.Errorf("new boot ID == old boot ID: %s", bootID)
}
// Verify successful reboot.
// Dev mode should be preserved, but recovery mode will be lost in the reset.
var expectMode fwCommon.BootMode
if fromMode == fwCommon.BootModeRecovery {
expectMode = fwCommon.BootModeNormal
} else {
expectMode = fromMode
}
if curr, err := h.Reporter.CurrentBootMode(ctx); err != nil {
return errors.Wrapf(err, "checking boot mode after resetting from %s", fromMode)
} else if curr != expectMode && !msOptsContain(opts, SkipModeCheckAfterReboot) {
return errors.Errorf("incorrect boot mode after resetting DUT: got %s; want %s", curr, expectMode)
}
return nil
}
// fwScreenToNormalMode moves the DUT from the firmware bootup screen to Normal mode.
// This should be called immediately after powering on.
// The actual behavior depends on the ModeSwitcherType.
func (ms *ModeSwitcher) fwScreenToNormalMode(ctx context.Context, hasSerialAP bool) error {
h := ms.Helper
if err := h.RequireServo(ctx); err != nil {
return errors.Wrap(err, "requiring servo")
}
testing.ContextLogf(ctx, "Sleeping %s (FirmwareScreen)", h.Config.FirmwareScreen)
if err := testing.Sleep(ctx, h.Config.FirmwareScreen); err != nil {
return errors.Wrapf(err, "sleeping for %s (FirmwareScreen) to wait for INSERT screen", h.Config.FirmwareScreen)
}
if hasSerialAP {
testing.ContextLogf(ctx, "Sleeping %s (SerialFirmwareBootDelay)", h.Config.SerialFirmwareBootDelay)
if err := testing.Sleep(ctx, h.Config.SerialFirmwareBootDelay); err != nil {
return errors.Wrapf(err, "sleeping for %s (SerialFirmwareBootDelay) while enabling dev mode", h.Config.SerialFirmwareBootDelay)
}
}
switch h.Config.ModeSwitcherType {
case KeyboardDevSwitcher:
// 1. Sleep for [FirmwareScreen] seconds.
// 2. Press enter.
// 3. Sleep for [KeypressDelay] seconds.
// 4. Press enter.
testing.ContextLog(ctx, "Pressing ENTER")
if err := h.Servo.KeypressWithDuration(ctx, servo.Enter, servo.DurTab); err != nil {
return errors.Wrap(err, "pressing Enter on firmware screen while disabling dev mode")
}
testing.ContextLogf(ctx, "Sleeping %s (KeypressDelay)", h.Config.KeypressDelay)
if err := testing.Sleep(ctx, h.Config.KeypressDelay); err != nil {
return errors.Wrapf(err, "sleeping for %s (KeypressDelay) while disabling dev mode", h.Config.KeypressDelay)
}
testing.ContextLog(ctx, "Pressing ENTER")
if err := h.Servo.KeypressWithDuration(ctx, servo.Enter, servo.DurTab); err != nil {
return errors.Wrap(err, "pressing Enter on confirm screen while disabling dev mode")
}
case MenuSwitcher:
// 1. Sleep for [FirmwareScreen] seconds.
// 2. Press Ctrl+S.
// 3. Sleep for [KeypressDelay] seconds.
// 4. Press enter.
testing.ContextLog(ctx, "Pressing Ctrl-S")
if err := h.Servo.KeypressWithDuration(ctx, servo.CtrlS, servo.DurTab); err != nil {
return errors.Wrap(err, "pressing Ctrl-S on firmware screen while disabling dev mode")
}
testing.ContextLogf(ctx, "Sleeping %s (KeypressDelay)", h.Config.KeypressDelay)
if err := testing.Sleep(ctx, h.Config.KeypressDelay); err != nil {
return errors.Wrapf(err, "sleeping for %s (KeypressDelay) while disabling dev mode", h.Config.KeypressDelay)
}
testing.ContextLog(ctx, "Pressing ENTER")
if err := h.Servo.KeypressWithDuration(ctx, servo.Enter, servo.DurTab); err != nil {
return errors.Wrap(err, "pressing Enter on confirm screen while disabling dev mode")
}
case TabletDetachableSwitcher:
// 1. Wait until the firmware screen appears.
// 2. Hold volume_up for 100ms to highlight the previous menu item (Enable Root Verification).
// 3. Sleep for [KeypressDelay] seconds to confirm keypress.
// 4. Press power to select Enable Root Verification.
// 5. Sleep for [KeypressDelay] seconds to confirm keypress.
// 6. Wait until the TO_NORM screen appears.
// 7. Press power to select Confirm Enabling Verified Boot.
if err := h.Servo.SetInt(ctx, servo.VolumeUpHold, 100); err != nil {
return errors.Wrap(err, "changing menu selection to 'Enable Root Verification'")
}
if err := testing.Sleep(ctx, h.Config.KeypressDelay); err != nil {
return errors.Wrapf(err, "sleeping for %s (KeypressDelay) while disabling dev mode", h.Config.KeypressDelay)
}
if err := h.Servo.KeypressWithDuration(ctx, servo.PowerKey, servo.DurTab); err != nil {
return errors.Wrap(err, "selecting menu option 'Enable Root Verification'")
}
if err := testing.Sleep(ctx, h.Config.KeypressDelay); err != nil {
return errors.Wrapf(err, "sleeping for %s (KeypressDelay) while disabling dev mode", h.Config.KeypressDelay)
}
if err := h.Servo.KeypressWithDuration(ctx, servo.PowerKey, servo.DurTab); err != nil {
return errors.Wrap(err, "selecting menu option 'Confirm Enabling Verified Boot'")
}
default:
return errors.Errorf("unsupported ModeSwitcherType %s for fwScreenToNormalMode", h.Config.ModeSwitcherType)
}
return nil
}
// fwScreenToDevMode moves the DUT from the firmware bootup screen to Dev mode.
// This should be called immediately after powering on.
// The actual behavior depends on the ModeSwitcherType.
func (ms *ModeSwitcher) fwScreenToDevMode(ctx context.Context, hasSerialAP bool, opts ...ModeSwitchOption) error {
h := ms.Helper
if err := h.RequireServo(ctx); err != nil {
return errors.Wrap(err, "requiring servo")
}
testing.ContextLogf(ctx, "Sleeping %s (FirmwareScreen)", h.Config.FirmwareScreen)
if err := testing.Sleep(ctx, h.Config.FirmwareScreen); err != nil {
return errors.Wrapf(err, "sleeping for %s (FirmwareScreen) to wait for INSERT screen", h.Config.FirmwareScreen)
}
if hasSerialAP {
testing.ContextLogf(ctx, "Sleeping %s (SerialFirmwareBootDelay)", h.Config.SerialFirmwareBootDelay)
if err := testing.Sleep(ctx, h.Config.SerialFirmwareBootDelay); err != nil {
return errors.Wrapf(err, "sleeping for %s (SerialFirmwareBootDelay) while enabling dev mode", h.Config.SerialFirmwareBootDelay)
}
}
switch h.Config.ModeSwitcherType {
case MenuSwitcher:
// Same as KeyboardDevSwitcher.
fallthrough
case KeyboardDevSwitcher:
// 1. Wait until the firmware screen appears.
// 2. Press Ctrl-D to move to the confirm screen.
// 3. Wait until the confirm screen appears.
// 4. Push some button depending on the DUT's config: toggle the rec button, press power, or press enter.
testing.ContextLog(ctx, "Pressing Ctrl-D")
if err := h.Servo.KeypressWithDuration(ctx, servo.CtrlD, servo.DurTab); err != nil {
return err
}
testing.ContextLogf(ctx, "Sleeping %s (KeypressDelay)", h.Config.KeypressDelay)
if err := testing.Sleep(ctx, h.Config.KeypressDelay); err != nil {
return err
}
if h.Config.RecButtonDevSwitch {
testing.ContextLog(ctx, "Toggling RecMode")
if err := h.Servo.ToggleOnOff(ctx, servo.RecMode); err != nil {
return err
}
} else if h.Config.PowerButtonDevSwitch {
testing.ContextLog(ctx, "Pressing power key")
if err := h.Servo.KeypressWithDuration(ctx, servo.PowerKey, servo.DurPress); err != nil {
return err
}
} else {
testing.ContextLog(ctx, "Pressing enter key")
if err := h.Servo.KeypressWithDuration(ctx, servo.Enter, servo.DurTab); err != nil {
return err
}
}
testing.ContextLogf(ctx, "Sleeping %s", 2*time.Second)
if err := testing.Sleep(ctx, 2*time.Second); err != nil {
return err
}
testing.ContextLog(ctx, "Set DFP mode")
if err := h.Servo.SetDUTPDDataRole(ctx, servo.DFP); err != nil {
testing.ContextLogf(ctx, "Failed to set pd data role to DFP: %s", err)
}
pressingKeyTillConnected := func(key string, connectTimeout time.Duration) error {
testing.ContextLogf(ctx, "Pressing %s", key)
switch key {
case "CTRL-D":
if err := h.Servo.KeypressWithDuration(ctx, servo.CtrlD, servo.DurTab); err != nil {
return err
}
case "ENTER":
if err := h.Servo.KeypressWithDuration(ctx, servo.Enter, servo.DurTab); err != nil {
return err
}
}
ctx, cancel := context.WithTimeout(ctx, connectTimeout)
defer cancel()
connectTimeout += time.Second
return h.DUT.WaitConnect(ctx)
}
connectTimeout := 2 * time.Second
if err := testing.Poll(ctx, func(ctx context.Context) error {
// Keep pressing CTRL-D until connected, but wait a little longer for the connect each time.
if err := pressingKeyTillConnected("CTRL-D", connectTimeout); err != nil {
return err
}
return nil
}, &testing.PollOptions{Timeout: reconnectTimeout}); err != nil && !msOptsContain(opts, PressEnterAtToNorm) {
return errors.Wrap(err, "failed to reconnect to DUT")
} else if err != nil && msOptsContain(opts, PressEnterAtToNorm) {
// DUTs would boot into the to_norm screen if dev mode was disabled by FWMP.
// Keep pressing ENTER to bypass the to_norm screen, until connection is established.
if err := testing.Poll(ctx, func(ctx context.Context) error {
if err := pressingKeyTillConnected("ENTER", connectTimeout); err != nil {
return err
}
return nil
}, &testing.PollOptions{Timeout: reconnectTimeout}); err != nil {
return errors.Wrap(err, "failed to reconnect to DUT")
}
}
case TabletDetachableSwitcher:
// 1. Wait [FirmwareScreen] seconds for the INSERT screen to appear.
// 2. Hold both VolumeUp and VolumeDown for 100ms to trigger TO_DEV screen.
// 3. Wait [KeypressDelay] seconds to confirm keypress.
// 4. Hold VolumeUp for 100ms to change menu selection to 'Confirm enabling developer mode'.
// 5. Wait [KeypressDelay] seconds to confirm keypress.
// 6. Press PowerKey to select menu item.
// 7. Wait [KeypressDelay] seconds to confirm keypress.
// 8. Wait [FirmwareScreen] seconds to transition screens.
if err := h.Servo.SetInt(ctx, servo.VolumeUpDownHold, 100); err != nil {
return errors.Wrap(err, "triggering TO_DEV screen")
}
if err := testing.Sleep(ctx, h.Config.KeypressDelay); err != nil {
return errors.Wrapf(err, "sleeping for %s (KeypressDelay) to confirm triggering TO_DEV screen", h.Config.KeypressDelay)
}
if err := h.Servo.SetInt(ctx, servo.VolumeUpHold, 100); err != nil {
return errors.Wrap(err, "changing menu selection to 'Confirm enabling developer mode' on TO_DEV screen")
}
if err := testing.Sleep(ctx, h.Config.KeypressDelay); err != nil {
return errors.Wrapf(err, "sleeping for %s (KeypressDelay) to confirm changing menu selection on TO_DEV screen", h.Config.KeypressDelay)
}
if err := h.Servo.KeypressWithDuration(ctx, servo.PowerKey, servo.DurTab); err != nil {
return errors.Wrap(err, "selecting menu item 'Confirm enabling developer mode' on TO_DEV screen")
}
// Reconnect to the DUT.
connectCtx, cancel := context.WithTimeout(ctx, reconnectTimeout)
defer cancel()
if err := h.WaitConnect(connectCtx); err != nil {
return errors.Wrap(err, "failed to reconnect to DUT")
}
default:
return errors.Errorf("booting to dev mode: unsupported ModeSwitcherType: %s", h.Config.ModeSwitcherType)
}
return nil
}
// fwScreenToUSBDevMode moves the DUT from the firmware bootup screen to USB Dev mode.
// This should be called immediately after powering on.
// The actual behavior depends on the ModeSwitcherType.
func (ms *ModeSwitcher) fwScreenToUSBDevMode(ctx context.Context) error {
h := ms.Helper
if err := h.RequireServo(ctx); err != nil {
return errors.Wrap(err, "requiring servo")
}
switch h.Config.ModeSwitcherType {
case MenuSwitcher:
// Same as KeyboardDevSwitcher.
fallthrough
case KeyboardDevSwitcher:
// 1. Wait until the firmware screen appears.
// 2. Press Ctrl-U to move to the confirm screen.
// 3. Wait until the confirm screen appears.
// 4. Push some button depending on the DUT's config: toggle the rec button, press power, or press enter.
testing.ContextLog(ctx, "Set DFP mode")
if err := h.Servo.SetDUTPDDataRole(ctx, servo.DFP); err != nil {
testing.ContextLogf(ctx, "Failed to set pd data role to DFP: %s", err)
}
// Keep pressing CTRL-U until connected, but wait a little longer for the connect each time.
connectTimeout := 2 * time.Second
if err := testing.Poll(ctx, func(ctx context.Context) error {
testing.ContextLog(ctx, "Pressing CTRL-U")
if err := h.Servo.KeypressWithDuration(ctx, servo.CtrlU, servo.DurTab); err != nil {
return err
}
ctx, cancel := context.WithTimeout(ctx, connectTimeout)
defer cancel()
connectTimeout += time.Second
return h.DUT.WaitConnect(ctx)
}, &testing.PollOptions{Timeout: reconnectTimeout}); err != nil {
return errors.Wrap(err, "failed to reconnect to DUT")
}
default:
return errors.Errorf("booting to dev mode: unsupported ModeSwitcherType: %s", h.Config.ModeSwitcherType)
}
return nil
}
// enableRecMode powers the DUT into the "rec" state, but does not wait to reconnect to the DUT.
// If booting into rec mode, usbMux should point to the DUT, so that the DUT can finish booting into recovery mode.
// Otherwise, usbMux should be off. This will prevent the DUT from transitioning to rec mode, so other operations can be performed (such as bypassing to dev mode).
func (ms *ModeSwitcher) enableRecMode(ctx context.Context, usbMux servo.USBMuxState) error {
h := ms.Helper
if err := h.RequireServo(ctx); err != nil {
return errors.Wrap(err, "requiring servo")
}
if err := ms.PowerOff(ctx); err != nil {
return errors.Wrap(err, "powering off DUT")
}
// Powering off the USB mux has some side effects that take some time. Specifically, you can't turn
// it back on again too quickly or the USB stick fails.
if err := testing.Sleep(ctx, 2*time.Second); err != nil {
return errors.Wrapf(err, "sleeping before setting usb mux state to %s", usbMux)
}
if err := h.Servo.SetUSBMuxState(ctx, usbMux); err != nil {
return errors.Wrapf(err, "setting usb mux state to %s while DUT is off", usbMux)
}
if err := h.Servo.SetPowerState(ctx, servo.PowerStateRec); err != nil {
return errors.Wrapf(err, "setting power state to %s", servo.PowerStateRec)
}
return nil
}
// PowerOff safely powers off the DUT with the "poweroff" command, then waits for the DUT to be unreachable.
func (ms *ModeSwitcher) PowerOff(ctx context.Context) error {
h := ms.Helper
if err := h.RequireServo(ctx); err != nil {
return errors.Wrap(err, "requiring servo")
}
testing.ContextLog(ctx, "Powering off DUT")
powerOffCtx, cancel := context.WithTimeout(ctx, cmdTimeout)
defer cancel()
// Since the DUT will power off, deadline exceeded is expected here.
if err := h.DUT.Conn().CommandContext(powerOffCtx, "poweroff").Run(); err != nil && !errors.Is(err, context.DeadlineExceeded) {
return errors.Wrapf(err, "DUT poweroff %T", err)
}
// Try reading the power state from the EC.
err := h.WaitForPowerStates(ctx, PowerStateInterval, PowerStateTimeout, "G3", "S5")
if err == nil {
return nil
}
testing.ContextLogf(ctx, "Failed to get G3 or S5 power state: %s", err)
// We didn't reach G3/S5 so try having servo power off instead.
if err := h.Servo.SetPowerState(ctx, servo.PowerStateOff); err != nil {
return errors.Wrap(err, "set power_state:off")
}
// If the EC didn't return a power state, try wait unreachable instead.
if err := ms.waitUnreachable(ctx); err != nil {
return errors.Wrap(err, "waiting for DUT to be unreachable after sending poweroff command")
}
return nil
}
func (ms *ModeSwitcher) waitUnreachable(ctx context.Context) error {
offCtx, cancel := context.WithTimeout(ctx, offTimeout)
defer cancel()
if err := ms.Helper.DUT.WaitUnreachable(offCtx); err != nil {
return errors.Wrap(err, "waiting for DUT to be unreachable after powering off")
}
return nil
}
func (ms *ModeSwitcher) hasSerialAPFirmware(ctx context.Context) bool {
// TODO(b/206004543): Get this working. Reading CONFIG_CONSOLE_SERIAL doesn't work.
return false
}