blob: 6b1d77d71442e3e81c151e0bd1decc1db306439d [file] [log] [blame] [edit]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package servo
import (
"context"
"fmt"
"regexp"
"strconv"
"strings"
"time"
"go.chromium.org/infra/cros/servo/errors"
"go.chromium.org/infra/cros/servo/testing"
)
// These are the EC Servo controls which can be get/set with a string value.
const (
ECBoard StringControl = "ec_board"
ECSystemPowerState StringControl = "ec_system_powerstate"
ECUARTCmd StringControl = "ec_uart_cmd"
ECUARTRegexp StringControl = "ec_uart_regexp"
ECUARTStream StringControl = "ec_uart_stream"
ECChip StringControl = "ec_chip"
ECFlashSize StringControl = "ec_flash_size"
DUTPDDataRole StringControl = "dut_pd_data_role"
)
// These controls accept only "on" and "off" as values.
const (
ECUARTCapture OnOffControl = "ec_uart_capture"
ECUARTTimestamp OnOffControl = "ec_uart_timestamp"
ECUARTFlush OnOffControl = "ec_uart_flush"
)
// Cmd constants for RunECCommand.
const (
// Using with no additional arguments returns current backlight level
// If additional int arg (0-100) provided, sets backlight to that level
kbLight string = "kblight"
)
// Pattern expression for RunCommandGetOutput.
const (
reKBBacklight string = `Keyboard backlight: (\d+)\%`
reCheckKBLight string = `Keyboard backlight: \d+\%|Command 'kblight' not found or ambiguous`
reTabletmodeNotFound string = `Command 'tabletmode' not found or ambiguous`
reBasestateNotFound string = `Command 'basestate' not found or ambiguous`
reTabletmodeStatus string = `\[\S+ tablet mode\s?(enabled|disabled)?|clamshell mode\]`
reBasestateStatus string = `\[\S+ base state: (attached|detached)\]`
reBdStatus string = `\[\S+ BD forced (connected|disconnected)\]`
reLidAccel string = `\[\S+ Lid Accel ODR:[^\n\r]*(1|0)\S+]`
reVupBtnPressed string = `\[\S+ Button \'Volume Up\' was pressed(.|\n)*buttons: 2\]`
reVupBtnReleased string = `\[\S+ Button \'Volume Up\' was released(.|\n)*buttons: 0\]`
reVdownBtnPressed string = `\[\S+ Button \'Volume Down\' was pressed(.|\n)*buttons: 4\]`
reVdownBtnReleased string = `\[\S+ Button \'Volume Down\' was released(.|\n)*buttons: 0\]`
rePwrBtnPressed string = `\[\S+ power button pressed(.|\n)*buttons: 1\]`
rePwrBtnReleased string = `\[\S+ power button released(.|\n)*buttons: 0\]`
reSKUID string = `SKU_ID:\s+(\d+)`
)
// USBCDataRole is a USB-C data role.
type USBCDataRole string
// USB-C data roles.
const (
// UFP is Upward facing partner, i.e. a peripheral. The servo should normally be in this role.
UFP USBCDataRole = "UFP"
// DFP is Downward facing partner, i.e. a host. The DUT should normally be in this role.
DFP USBCDataRole = "DFP"
)
// HibernationOpt is an option for hibernating DUT.
type HibernationOpt string
// Available options for triggering hibernation.
const (
// UseKeyboard uses keyboard shortcut for hibernating DUT: alt+vol_up+h.
UseKeyboard HibernationOpt = "keyboard"
// UseConsole uses the EC command `hibernate` to put DUT in hibernation.
UseConsole HibernationOpt = "console"
)
// USBPdDualRoleValue contains a gettable/settable string accepted by
// the ec command, 'pd <port> dualrole'.
type USBPdDualRoleValue string
// These are acceptable states for the USB PD dual-role.
const (
USBPdDualRoleOn USBPdDualRoleValue = "on"
USBPdDualRoleOff USBPdDualRoleValue = "off"
USBPdDualRoleFreeze USBPdDualRoleValue = "freeze"
USBPdDualRoleSink USBPdDualRoleValue = "force sink"
USBPdDualRoleSource USBPdDualRoleValue = "force source"
)
// RunECCommand runs the given command on the EC on the device.
func (s *Servo) RunECCommand(ctx context.Context, cmd string) error {
if err := s.SetString(ctx, ECUARTRegexp, "None"); err != nil {
return errors.Wrap(err, "Clearing EC UART Regexp")
}
return s.SetString(ctx, ECUARTCmd, cmd)
}
// RunECCommandGetOutput runs the given command on the EC on the device and returns the output matching patterns.
// It is recommended to send "chan save", "chan 0" just before and "chan restore" afterwards to prevent other
// logging from interrupting your command. (RunECCommandGetOutputNoConsoleLogs() can handle this for you)
func (s *Servo) RunECCommandGetOutput(ctx context.Context, cmd string, patterns []string) ([][]string, error) {
err := s.SetStringList(ctx, ECUARTRegexp, patterns)
if err != nil {
return nil, errors.Wrapf(err, "setting ECUARTRegexp to %s", patterns)
}
defer s.SetString(ctx, ECUARTRegexp, "None") //nolint
err = s.SetString(ctx, ECUARTCmd, cmd)
if err != nil {
return nil, errors.Wrapf(err, "setting ECUARTCmd to %s", cmd)
}
iList, err := s.GetStringList(ctx, ECUARTCmd)
if err != nil {
return nil, errors.Wrap(err, "decoding string list")
}
return ConvertToStringArrayArray(ctx, iList)
}
func (s *Servo) runECCommandGetOutputNoConsoleLogsHelper(ctx context.Context, cmd string, patterns []string, allowRetries bool) (output [][]string, retErr error) {
// EC console can be extremely chatty. Log messages are liable to interrupt
// the console output, breaking the regex pattern. Turn off all other channels
// and restore after.
if err := s.SaveDUTConsoleChannelMask(ctx); err != nil {
return nil, errors.Wrap(err, "cannot save current chan mask")
}
// 0 turns off all channels except console output, which is always on.
if err := s.SetDUTConsoleChannelMask(ctx, 0); err != nil {
return nil, errors.Wrap(err, "cannot disable console logs pre-cmd")
}
// Restore original mask
defer func() {
if err := s.RestoreDUTConsoleChannelMask(ctx); err != nil {
retErr = errors.Join(errors.Wrap(err, "cannot restore console chanel mask to original state"))
}
}()
if allowRetries {
// Use a polling loop to retry the command if not successful.
var output [][]string
if err := testing.Poll(ctx, func(ctx context.Context) error {
var errCmd error
output, errCmd = s.RunECCommandGetOutput(ctx, cmd, patterns)
if errCmd != nil {
testing.ContextLogf(ctx, "EC Command %q failed: %q", cmd, errCmd)
}
return errCmd
}, &testing.PollOptions{Timeout: 6 * time.Second, Interval: 2 * time.Second}); err != nil {
return nil, err
}
return output, nil
}
// If not allowing retries, just try command once and return
return s.RunECCommandGetOutput(ctx, cmd, patterns)
}
// RunECCommandGetOutputNoConsoleLogs works like RunECCommandGetOutput but automatically disables
// all console logging, which could interfere with capturing command output.
func (s *Servo) RunECCommandGetOutputNoConsoleLogs(ctx context.Context, cmd string, patterns []string) ([][]string, error) {
return s.runECCommandGetOutputNoConsoleLogsHelper(ctx, cmd, patterns, false)
}
// RunECCommandGetOutputNoConsoleLogsAllowRetries works like RunECCommandGetOutputNoConsoleLogs but
// allows the command to be retried up to twice if unsuccessful.
func (s *Servo) RunECCommandGetOutputNoConsoleLogsAllowRetries(ctx context.Context, cmd string, patterns []string) ([][]string, error) {
return s.runECCommandGetOutputNoConsoleLogsHelper(ctx, cmd, patterns, true)
}
// GetECSystemPowerState returns the power state, like "S0" or "G3"
func (s *Servo) GetECSystemPowerState(ctx context.Context) (string, error) {
return s.GetString(ctx, ECSystemPowerState)
}
// ECHibernate puts the EC into hibernation mode, after removing the servo watchdogs.
func (s *Servo) ECHibernate(ctx context.Context, model string, option HibernationOpt) error {
if err := s.RemoveCCDWatchdogs(ctx); err != nil {
return errors.Wrap(err, "failed to remove watchdog for ccd")
}
switch option {
case "keyboard":
testing.ContextLog(ctx, "Pressing and holding alt+f10+h")
if err := s.PressKeys(ctx, []string{"<alt_l>", "<f10>", "h"}, DurTab); err != nil {
return errors.Wrap(err, "failed to press keys")
}
case "console":
if err := s.RunECCommand(ctx, "hibernate"); err != nil {
return errors.Wrap(err, "failed to run EC command: hibernate")
}
}
// Delay for a few seconds to allow proper propagation of the
// hibernation command, prior to checking EC unresponsive.
// GoBigSleepLint: sleep to let hibernation propagate
if err := testing.Sleep(ctx, 5*time.Second); err != nil {
return errors.Wrap(err, "failed to sleep")
}
if err := s.CheckUnresponsiveEC(ctx); err != nil {
return errors.Wrap(err, "while verifying whether EC is unresponsive after hibernating DUT")
}
return nil
}
// GetECFlashSize returns the size of EC in KB e.g. 512
func (s *Servo) GetECFlashSize(ctx context.Context) (int, error) {
sizeStr, err := s.GetString(ctx, ECFlashSize)
if err != nil {
return 0, errors.Wrap(err, "failed to get value for ec size")
}
// ECFlashSize method matches an int regex so Atoi should always work
return strconv.Atoi(sizeStr)
}
// GetECChip returns the DUT chip e.g. "npcx_uut"
func (s *Servo) GetECChip(ctx context.Context) (string, error) {
return s.GetString(ctx, ECChip)
}
// SetDUTPDDataRole tries to find the port attached to the servo, and performs a data role swap if the role doesn't match `role`.
// Will fail if there is no chromeos EC.
func (s *Servo) SetDUTPDDataRole(ctx context.Context, role USBCDataRole) error {
return s.SetString(ctx, DUTPDDataRole, string(role))
}
// SetKBBacklight sets the DUT keyboards backlight to the given value (0 - 100).
func (s *Servo) SetKBBacklight(ctx context.Context, percent int) error {
testing.ContextLog(ctx, "Setting keyboard backlight to: ", percent)
err := s.RunECCommand(ctx, fmt.Sprintf("%v %v", kbLight, percent))
if err != nil {
return errors.Wrapf(err, "running '%v %v' on DUT", kbLight, percent)
}
return nil
}
// GetKBBacklight gets the DUT keyboards backlight value in percent (0 - 100).
func (s *Servo) GetKBBacklight(ctx context.Context) (int, error) {
testing.ContextLog(ctx, "Getting current keyboard backlight percent")
out, err := s.RunECCommandGetOutput(ctx, kbLight, []string{reKBBacklight})
if err != nil {
return 0, errors.Wrapf(err, "running %v on DUT", kbLight)
}
return strconv.Atoi(out[0][1])
}
// HasKBBacklight checks if the DUT keyboards has backlight functionality.
func (s *Servo) HasKBBacklight(ctx context.Context) bool {
testing.ContextLog(ctx, "Checking if DUT keyboard supports backlight")
out, _ := s.RunECCommandGetOutput(ctx, kbLight, []string{reCheckKBLight})
expMatch := regexp.MustCompile(reKBBacklight)
match := expMatch.FindStringSubmatch(out[0][0])
return match != nil
}
// CheckUnresponsiveEC verifies that EC console is unresponsive in situations such as
// hibernation and battery cutoff. Ignore null chars, sometimes the servo returns null
// when the EC is off.
func (s *Servo) CheckUnresponsiveEC(ctx context.Context) error {
return testing.Poll(ctx, func(ctx context.Context) error {
out, err := s.RunECCommandGetOutput(ctx, "version", []string{`[^\x00]+`})
if err == nil {
return errors.Errorf("EC is still active: got %v; expected error", out)
}
if !strings.Contains(err.Error(), "No data was sent from the pty") &&
!strings.Contains(err.Error(), "EC: Timeout waiting for response.") &&
!strings.Contains(err.Error(), "Timed out waiting for interfaces to become available") &&
!strings.Contains(err.Error(), "Client.Timeout exceeded while awaiting headers") {
return errors.Wrap(err, "unexpected EC error")
}
return nil
}, &testing.PollOptions{Interval: 1 * time.Second, Timeout: 1 * time.Minute})
}
// TabletModeCmdUnsupportedErr is the error returned by
// RunTabletModeCommandGetOutput when the ec command is found unsupported.
// To detect this error, use something like:
// if _, ok := err.(*TabletModeCmdUnsupportedErr); ok {}
type TabletModeCmdUnsupportedErr struct {
*errors.E
}
// RunTabletModeCommandGetOutput runs EC commands to set tablet mode and
// returns the output matching pattern for the resulting tablet mode state.
// Before calling RunTabletModeCommand(), a test can call
// h.GetECTabletLaptopModeCommand() to determine the corresponding command.
func (s *Servo) RunTabletModeCommandGetOutput(ctx context.Context, command string) (string, error) {
// regular expressions.
reStr := strings.Join([]string{reTabletmodeNotFound, reTabletmodeStatus,
reBasestateNotFound, reBasestateStatus, reBdStatus, reLidAccel}, "|")
checkTabletMode := fmt.Sprintf("%s%s%s", "(", reStr, ")")
// Run EC command to check tablet mode setting.
out, err := s.RunECCommandGetOutput(ctx, command, []string{checkTabletMode})
if err != nil {
return "", errors.Wrapf(err, "failed to run command %q", command)
}
tabletModeUnavailable := []*regexp.Regexp{regexp.MustCompile(reTabletmodeNotFound),
regexp.MustCompile(reBasestateNotFound)}
for _, v := range tabletModeUnavailable {
if match := v.FindStringSubmatch(out[0][0]); match != nil {
return "", &TabletModeCmdUnsupportedErr{E: errors.Errorf("device does not support tablet mode: %q", match)}
}
}
return string(out[0][1]), nil
}
// OpenCCD checks if a CCD connection exists, and then opens CCD if it's locked.
func (s *Servo) OpenCCD(ctx context.Context) error {
if hasCCD, err := s.HasCCD(ctx); err != nil {
return errors.Wrap(err, "while checking if servo has a CCD connection")
} else if hasCCD {
if val, err := s.GetString(ctx, GSCCCDLevel); err != nil {
return errors.Wrap(err, "failed to get gsc_ccd_level")
} else if val != Open {
testing.ContextLogf(ctx, "CCD is not open, got %q. Attempting to unlock", val)
if err := s.SetString(ctx, GSCTestlab, Open); err != nil {
return errors.Wrap(err, "failed to unlock CCD")
}
}
// For debugging purposes, log CCD state after unlocking CCD.
checkedVal, err := s.GetString(ctx, GSCCCDLevel)
if err != nil {
return errors.Wrap(err, "failed to get gsc_ccd_level after unlocking CCD")
}
testing.ContextLogf(ctx, "CCD State: %q", checkedVal)
}
return nil
}
// ECHostevent holds int codes for EC hostevents
type ECHostevent int64
// Hostevent codes, copied from ec/include/ec_commands.h.
const (
// HosteventLidClosed is the event code for lid closed.
HosteventLidClosed ECHostevent = 0x00000001
// HosteventLidOpen is the event code for lid open.
HosteventLidOpen ECHostevent = 0x00000002
// HosteventPowerButton is the event code for power button.
HosteventPowerButton ECHostevent = 0x00000004
// HosteventAcConnected is the event code for ac connected.
HosteventAcConnected ECHostevent = 0x00000008
// HosteventAcDisconnected is the event code for ac disconnected.
HosteventAcDisconnected ECHostevent = 0x00000010
// HosteventBatteryLow is the event code for low battery.
HosteventBatteryLow ECHostevent = 0x00000020
// HosteventBatteryCritical is the event code for critical battery.
HosteventBatteryCritical ECHostevent = 0x00000040
// HosteventBattery is the event code for battery.
HosteventBattery ECHostevent = 0x00000080
// HosteventThermalThreshold is the event code for thermal threshold.
HosteventThermalThreshold ECHostevent = 0x00000100
// HosteventThermalOverload is the event code for thermal overload.
HosteventThermalOverload ECHostevent = 0x00000200
// HosteventThermal is the event code for thermal.
HosteventThermal ECHostevent = 0x00000400
// HosteventUsbCharger is the event code for usb charger.
HosteventUsbCharger ECHostevent = 0x00000800
// HosteventKeyPressed is the event code for key press.
HosteventKeyPressed ECHostevent = 0x00001000
// HosteventInterfaceReady is the event code for interface ready.
HosteventInterfaceReady ECHostevent = 0x00002000
// HosteventKeyboardRecovery is the event code for keyboard recovery combo has been pressed
HosteventKeyboardRecovery ECHostevent = 0x00004000
// HosteventThermalShutdown is the event code for shutdown due to thermal overload.
HosteventThermalShutdown ECHostevent = 0x00008000
// HosteventBatteryShutdown is the event code for shutdown due to battery level too low.
HosteventBatteryShutdown ECHostevent = 0x00010000
// HosteventInvalid is the event code for invalid host event.
HosteventInvalid ECHostevent = 0x80000000
)
// SetHostevent sets host event in ec console using `hostevet set` cmd.
func (s *Servo) SetHostevent(ctx context.Context, event ECHostevent) error {
hosteventCmd := fmt.Sprintf("hostevent set 0x%08x", event)
testing.ContextLogf(ctx, "Setting hostevent: %q", hosteventCmd)
if err := s.RunECCommand(ctx, hosteventCmd); err != nil {
return errors.Wrap(err, "failed to set hostevent")
}
return nil
}
// ECChannelName holds the ec channel names.
type ECChannelName string
// These are some of the ec channel names available.
// To-do: expand when necessary.
const (
ECChanKeyboard ECChannelName = "keyboard"
ECChanSwitch ECChannelName = "switch"
)
// FindECChanMask accepts an ec channel name, and runs ec 'chan' command to look for
// the corresponding mask value.
func (s *Servo) FindECChanMask(ctx context.Context, chanName ECChannelName) (maskVal string, retErr error) {
if err := s.RunECCommand(ctx, "chan save"); err != nil {
return "", errors.Wrap(err, "failed to send 'chan save' to EC")
}
if err := s.RunECCommand(ctx, "chan 0"); err != nil {
return "", errors.Wrap(err, "failed to send 'chan 0' to EC")
}
defer func() {
testing.ContextLog(ctx, "Restoring chan")
if err := s.RunECCommand(ctx, "chan restore"); err != nil {
if retErr == nil {
retErr = errors.Wrap(err, "failed to send 'chan restore' to EC")
} else {
testing.ContextLog(ctx, "Failed to send 'chan restore' to EC: ", err)
}
}
}()
match := fmt.Sprintf(`([0-9a-fA-F]{8})\s+\W?\s+%s`, string(chanName))
out, err := s.RunECCommandGetOutput(ctx, "chan", []string{match})
if err != nil {
return "", err
}
if out == nil || len(out[0]) < 2 {
return "", errors.Errorf("failed to parse chan output correctly, got: %v", out)
}
return out[0][1], nil
}
// SetECChanMasks accepts a map of ec channel names with their masks, and sets them.
func (s *Servo) SetECChanMasks(ctx context.Context, ecChanMasks map[ECChannelName]string) error {
var maskFinal int64
for name, mask := range ecChanMasks {
decimalVal, err := strconv.ParseInt(mask, 16, 64)
if err != nil {
return errors.Errorf("failed to parse mask value: %s, for ec chan: %s", mask, name)
}
maskFinal += decimalVal
}
testing.ContextLogf(ctx, "Setting chan mask: %d", maskFinal)
if err := s.RunECCommand(ctx, fmt.Sprintf("chan %d", maskFinal)); err != nil {
return errors.Wrap(err, "setting chan mask failed")
}
return nil
}
// DetachableECButton holds ec button controls for a detachable,
// which can take customized durations in milliseconds.
type DetachableECButton string
// These are the available ec button controls for a detachable.
const (
ECVupButton DetachableECButton = "button vup"
ECVdownButton DetachableECButton = "button vdown"
ECPwrButton DetachableECButton = "powerbtn"
)
// PressECBtnVerifyOutput sends a DetachableECButton and verifies in the output that
// the button was successfully pressed and released. Call FindECChanMask first to find
// the mask values for ECChanKeyboard and ECChanSwitch, and pass them to PressECBtnVerifyOutput.
func (s *Servo) PressECBtnVerifyOutput(ctx context.Context, button DetachableECButton, duration int, ecChanMasks map[ECChannelName]string) error {
requiredMasks := []ECChannelName{ECChanKeyboard, ECChanSwitch}
for _, name := range requiredMasks {
if _, ok := ecChanMasks[name]; !ok {
return errors.Errorf("missing mask value for ec chan: %s", name)
}
}
if err := s.SetECChanMasks(ctx, ecChanMasks); err != nil {
return err
}
var checkPressEffective string
switch button {
case ECVupButton:
checkPressEffective = `(` + reVupBtnPressed + `(.|\n)*` + reVupBtnReleased + `)`
case ECVdownButton:
checkPressEffective = `(` + reVdownBtnPressed + `(.|\n)*` + reVdownBtnReleased + `)`
case ECPwrButton:
checkPressEffective = `(` + rePwrBtnPressed + `(.|\n)*` + rePwrBtnReleased + `)`
default:
return errors.Errorf("unable to recognize %s", button)
}
testing.ContextLogf(ctx, "Pressing %s for %d milliseconds", button, duration)
control := fmt.Sprintf("%s %s", button, strconv.Itoa(duration))
if _, err := s.RunECCommandGetOutput(ctx, control, []string{checkPressEffective}); err != nil {
return errors.Wrapf(err, "pressing %s failed", button)
}
return nil
}
var keyboardReadyRe *regexp.Regexp = regexp.MustCompile(`KB enable`)
// WaitFirmwareKeyboardNoCmd waits until the DUT is in firmware with keyboard enabled
// On entry the DUT can be in firmware or kernel.
// This function works by waiting for the "KB enable" message shown by the EC when it boots.
// It does not issue commands to the EC.
func (s *Servo) WaitFirmwareKeyboardNoCmd(ctx context.Context, timeout time.Duration) (retErr error) {
closeUART, err := s.EnableUARTCapture(ctx, ECUARTCapture)
if err != nil {
return errors.Wrap(err, "failed to enable capture EC UART")
}
defer func() { retErr = errors.Join(retErr, closeUART(ctx)) }()
if found, err := s.PollForRegexp(ctx, ECUARTStream, keyboardReadyRe, timeout); err != nil {
return errors.Wrap(err, "gsc output parsing failed")
} else if !found {
return errors.Errorf("did not capture %s", keyboardReadyRe)
}
return nil
}
// GetSkuID runs the ec command 'cbi' and checks for the sku-id.
func (s *Servo) GetSkuID(ctx context.Context) (int, error) {
skuID, err := s.RunECCommandGetOutput(ctx, "cbi", []string{reSKUID})
if err != nil {
return -1, err
}
return strconv.Atoi(skuID[0][1])
}