blob: 0ea0105c57ba5e3cd0519befc449cb1555a340d4 [file] [log] [blame]
// Copyright 2021 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 servo
import (
"context"
"fmt"
"regexp"
"strconv"
"strings"
"time"
"chromiumos/tast/errors"
"chromiumos/tast/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"
)
// 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 (enabled|disabled)\]`
reBasestateStatus string = `\[\S+ base state: (attached|detached)\]`
reBdStatus string = `\[\S+ BD forced (connected|disconnected)\]`
reLidAccel string = `\[\S+ Lid Accel ODR:(?i)[^\n\r]*(?i)(1|0)\S+]`
)
// 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"
)
// KBMatrixPair is a struct to store key row and col for the kbpress cmd
type KBMatrixPair struct {
row int
col int
}
// KeyMatrix is a map that stores a row/col pair for each key using KBMatrixPair
// It's stored in order of appearance in a keyboard
var KeyMatrix = map[string]KBMatrixPair{
"<esc>": KBMatrixPair{1, 1},
"<f1>": KBMatrixPair{0, 2},
"<f2>": KBMatrixPair{3, 2},
"<f3>": KBMatrixPair{2, 2},
"<f4>": KBMatrixPair{1, 2},
"<f5>": KBMatrixPair{3, 4},
"<f6>": KBMatrixPair{2, 4},
"<f7>": KBMatrixPair{1, 4},
"<f8>": KBMatrixPair{2, 9},
"<f9>": KBMatrixPair{1, 9},
"<f10>": KBMatrixPair{0, 4},
"`": KBMatrixPair{3, 1},
"1": KBMatrixPair{6, 1},
"2": KBMatrixPair{6, 4},
"3": KBMatrixPair{6, 2},
"4": KBMatrixPair{6, 3},
"5": KBMatrixPair{3, 3},
"6": KBMatrixPair{3, 6},
"7": KBMatrixPair{6, 6},
"8": KBMatrixPair{6, 5},
"9": KBMatrixPair{6, 9},
"0": KBMatrixPair{6, 8},
"-": KBMatrixPair{3, 8},
"=": KBMatrixPair{0, 8},
"<backspace>": KBMatrixPair{1, 11},
"<tab>": KBMatrixPair{2, 1},
"q": KBMatrixPair{7, 1},
"w": KBMatrixPair{7, 4},
"e": KBMatrixPair{7, 2},
"r": KBMatrixPair{7, 3},
"t": KBMatrixPair{2, 3},
"y": KBMatrixPair{2, 6},
"u": KBMatrixPair{7, 6},
"i": KBMatrixPair{7, 5},
"o": KBMatrixPair{7, 9},
"p": KBMatrixPair{7, 8},
"[": KBMatrixPair{2, 8},
"]": KBMatrixPair{2, 5},
"\\": KBMatrixPair{3, 11},
"<search>": KBMatrixPair{0, 1},
"a": KBMatrixPair{4, 1},
"s": KBMatrixPair{4, 4},
"d": KBMatrixPair{4, 2},
"f": KBMatrixPair{4, 3},
"g": KBMatrixPair{1, 3},
"h": KBMatrixPair{1, 6},
"j": KBMatrixPair{4, 6},
"k": KBMatrixPair{4, 5},
"l": KBMatrixPair{4, 9},
";": KBMatrixPair{4, 8},
"'": KBMatrixPair{1, 8},
"<enter>": KBMatrixPair{4, 11},
"<shift_l>": KBMatrixPair{5, 7},
"z": KBMatrixPair{5, 1},
"x": KBMatrixPair{5, 4},
"c": KBMatrixPair{5, 2},
"v": KBMatrixPair{5, 3},
"b": KBMatrixPair{0, 3},
"n": KBMatrixPair{0, 6},
"m": KBMatrixPair{5, 6},
",": KBMatrixPair{5, 5},
".": KBMatrixPair{5, 9},
"/": KBMatrixPair{5, 8},
"<shift_r>": KBMatrixPair{7, 7},
"<ctrl_l>": KBMatrixPair{2, 0},
"<alt_l>": KBMatrixPair{6, 10},
" ": KBMatrixPair{5, 1},
"<alt_r>": KBMatrixPair{0, 10},
"<ctrl_r>": KBMatrixPair{4, 0},
"<left>": KBMatrixPair{7, 12},
"<up>": KBMatrixPair{7, 11},
"<down>": KBMatrixPair{6, 11},
"<right>": KBMatrixPair{6, 12},
}
// 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"
)
// 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.
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")
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)
}
// 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 watchdog for CCD if necessary.
func (s *Servo) ECHibernate(ctx context.Context, option HibernationOpt) error {
if err := s.WatchdogRemove(ctx, WatchdogCCD); err != nil {
return errors.Wrap(err, "failed to remove watchdog for ccd")
}
switch option {
case "keyboard":
if err := func(ctx context.Context) error {
for _, targetKey := range []string{"<alt_l>", "<f10>", "h"} {
row, col, err := s.GetKeyRowCol(targetKey)
if err != nil {
return errors.Wrapf(err, "failed to get key %s column and row", targetKey)
}
targetKeyName := targetKey
targetKeyHold := fmt.Sprintf("kbpress %d %d 1", col, row)
targetKeyRelease := fmt.Sprintf("kbpress %d %d 0", col, row)
testing.ContextLogf(ctx, "Pressing and holding key %s", targetKey)
if err := s.RunECCommand(ctx, targetKeyHold); err != nil {
return errors.Wrapf(err, "failed to press and hold key %s", targetKey)
}
defer func(releaseKey, name string) error {
testing.ContextLogf(ctx, "Releasing key %s", name)
if err := s.RunECCommand(ctx, releaseKey); err != nil {
return errors.Wrapf(err, "failed to release key %s", releaseKey)
}
return nil
}(targetKeyRelease, targetKeyName)
}
return nil
}(ctx); err != nil {
return err
}
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.
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))
}
// GetKeyRowCol returns the key row and column for kbpress cmd
func (s *Servo) GetKeyRowCol(key string) (int, int, error) {
pair, ok := KeyMatrix[key]
if !ok {
return 0, 0, errors.New("failed to find key in KeyMatrix map")
}
return pair.row, pair.col, nil
}
// ECPressKey simulates a keypress on the DUT from the servo using kbpress.
func (s *Servo) ECPressKey(ctx context.Context, key string) error {
row, col, err := s.GetKeyRowCol(key)
if err != nil {
return errors.Wrapf(err, "failed to get key %q in key matrix", key)
}
if err := s.RunECCommand(ctx, fmt.Sprintf("kbpress %d %d 1", col, row)); err != nil {
return errors.Wrapf(err, "failed to press key %q", key)
}
if err := s.RunECCommand(ctx, fmt.Sprintf("kbpress %d %d 0", col, row)); err != nil {
return errors.Wrapf(err, "failed to release key %q", key)
}
return nil
}
// 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") {
return errors.Wrap(err, "unexpected EC error")
}
return nil
}, &testing.PollOptions{Interval: 1 * time.Second, Timeout: 20 * time.Second})
}
// CheckAndRunTabletModeCommand checks if relevant EC commands exist and use them for setting tablet mode.
// For example, detachables use 'basestate (attach|detach)', and convertibles use 'tabletmode (on|off)'.
func (s *Servo) CheckAndRunTabletModeCommand(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 "", 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, CR50Testlab, 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
}