blob: 024fb09366893b3a245d9a18f6f2bd650f949e41 [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.
// This file implements functions to interact with the DUT's embedded controller (EC)
// via the host command `ectool`.
package firmware
import (
"context"
"fmt"
"regexp"
"strconv"
"strings"
"chromiumos/tast/dut"
"chromiumos/tast/errors"
"chromiumos/tast/ssh"
"chromiumos/tast/testing"
)
// ECToolName specifies which of the many Chromium EC based MCUs ectool will
// be communicated with.
// Some options are cros_ec, cros_fp, cros_pd, cros_scp, and cros_ish.
type ECToolName string
const (
// ECToolNameMain selects the main EC using cros_ec.
ECToolNameMain ECToolName = "cros_ec"
// ECToolNameFingerprint selects the FPMCU using cros_fp.
ECToolNameFingerprint ECToolName = "cros_fp"
)
// ECTool allows for interaction with the host command `ectool`.
type ECTool struct {
dut *dut.DUT
name ECToolName
}
// NewECTool creates an ECTool.
func NewECTool(d *dut.DUT, name ECToolName) *ECTool {
return &ECTool{dut: d, name: name}
}
// Regexps to capture values outputted by ectool version.
var (
reFirmwareCopy = regexp.MustCompile(`Firmware copy:\s*(RO|RW)`)
reROVersion = regexp.MustCompile(`RO version:\s*(\S+)\s`)
reRWVersion = regexp.MustCompile(`RW version:\s*(\S+)\s`)
reECHash = regexp.MustCompile(`hash:\s*(\S+)\s*`)
reTabletModeAng = regexp.MustCompile(`tablet_mode_angle=(\d+) hys=(\d+)`)
reI2CLookup = regexp.MustCompile(`Bus: I2C; Port: (\S+); Address: (\S+)`)
)
// Command return the prebuilt ssh Command with options and args applied.
func (ec *ECTool) Command(ctx context.Context, args ...string) *ssh.Cmd {
args = append([]string{"--name=" + string(ec.name)}, args...)
return ec.dut.Conn().CommandContext(ctx, "ectool", args...)
}
// Version returns the EC version of the active firmware.
func (ec *ECTool) Version(ctx context.Context) (string, error) {
output, err := ec.Command(ctx, "version").Output(ssh.DumpLogOnError)
if err != nil {
return "", errors.Wrap(err, "running 'ectool version' on DUT")
}
// Parse output to determine whether RO or RW is the active firmware.
match := reFirmwareCopy.FindSubmatch(output)
if len(match) == 0 {
return "", errors.Errorf("did not find firmware copy in 'ectool version' output: %s", output)
}
var reActiveFWVersion *regexp.Regexp
switch string(match[1]) {
case "RO":
reActiveFWVersion = reROVersion
case "RW":
reActiveFWVersion = reRWVersion
default:
return "", errors.Errorf("unexpected match from reFirmwareCopy: got %s; want RO or RW", match[1])
}
// Parse either RO version line or RW version line, depending on which is active, to find the active firmware version.
match = reActiveFWVersion.FindSubmatch(output)
if len(match) == 0 {
return "", errors.Errorf("failed to match regexp %s in ectool version output: %s", reActiveFWVersion, output)
}
return string(match[1]), nil
}
// Hash returns the EC hash of the active firmware.
func (ec *ECTool) Hash(ctx context.Context) (string, error) {
out, err := ec.Command(ctx, "echash").Output(ssh.DumpLogOnError)
if err != nil {
return "", errors.Wrap(err, "running 'ectool echash' on DUT")
}
// Parse output to determine whether RO or RW is the active firmware.
match := reECHash.FindSubmatch(out)
if len(match) == 0 {
return "", errors.Errorf("did not find ec hash 'ectool hash' output: %s", out)
}
return string(match[1]), nil
}
// BatteryCutoff runs the ectool batterycutoff command.
func (ec *ECTool) BatteryCutoff(ctx context.Context) error {
if err := ec.Command(ctx, "batterycutoff").Start(); err != nil {
return errors.Wrap(err, "running 'ectool batterycutoff' on DUT")
}
return nil
}
// SaveTabletModeAngles runs 'ectool motionsense tablet_mode_angle' to save the current angles for tablet mode.
func (ec *ECTool) SaveTabletModeAngles(ctx context.Context) (string, string, error) {
out, err := ec.Command(ctx, "motionsense", "tablet_mode_angle").Output(ssh.DumpLogOnError)
if err != nil {
return "", "", errors.Wrap(err, "running 'ectool motionsense tablet_mode_angle' on DUT")
}
matches := reTabletModeAng.FindStringSubmatch(string(out))
if len(matches) != 3 {
return "", "", errors.Errorf("unable to retrieve tablet mode angles from 'ectool motionsense tablet_mode_angle' output: %s", out)
}
return matches[1], matches[2], nil
}
// ForceTabletModeAngle emulates rotation angles to change DUT's tablet mode setting.
func (ec *ECTool) ForceTabletModeAngle(ctx context.Context, tabletModeAngle, hys string) error {
if err := ec.Command(ctx, "motionsense", "tablet_mode_angle", tabletModeAngle, hys).Start(); err != nil {
return errors.Wrap(err, "failed to set tablet_mode_angle to 0")
}
return nil
}
// GpioName type holds commands for 'ectool gpioget'.
type GpioName string
const (
// ECCbiWp for the 'ectool gpioget ec_cbi_wp' cmd.
ECCbiWp GpioName = "ec_cbi_wp"
// ENPP3300POGO for the 'ectool gpioget EN_PP3300_POGO' cmd.
ENPP3300POGO GpioName = "EN_PP3300_POGO"
// ENBASE for the 'ectool gpioget EN_BASE' cmd.
ENBASE GpioName = "EN_BASE"
)
// FindBaseGpio iterates through a passed in list of gpios, relevant to control on a detachable base,
// and checks if any one of them exists.
func (ec *ECTool) FindBaseGpio(ctx context.Context, gpios []GpioName) (map[GpioName]string, error) {
// Create a local map to save the gpios found and their current values from the passed in list.
results := make(map[GpioName]string)
for _, name := range gpios {
// Regular expressions
reFoundGpio := regexp.MustCompile(fmt.Sprintf(`GPIO\s+%s[^\n\r]*`, name))
reGpioVal := regexp.MustCompile(`\s+(0|1)`)
// Check whether the gpio exists, and if it does, also check its value.
out, err := ec.Command(ctx, "gpioget", string(name)).CombinedOutput()
if err != nil {
msg := strings.Split(strings.TrimSpace(string(out)), "\n")
testing.ContextLogf(ctx, "running 'ectool gpioget %s' on DUT failed: %v, and received: %v", name, err, msg)
}
match := reFoundGpio.FindSubmatch(out)
if len(match) == 0 {
testing.ContextLogf(ctx, "Did not find gpio with name %s", string(name))
} else {
val := reGpioVal.FindSubmatch(out)
gpioVal := strings.TrimSpace(string(val[0]))
testing.ContextLogf(ctx, "Found gpio with name %s, and value: %s", string(name), gpioVal)
results[name] = gpioVal
}
}
if len(results) == 0 {
return nil, errors.New("Unable to find any of the gpios passed in. Consider expanding on the list")
}
return results, nil
}
// I2CLookupInfo is a way to access the port and address of the i2c.
type I2CLookupInfo struct {
Port int
Address int
}
// I2CLookup runs ectool locatechip 0 0 to get Port and Address for I2C.
func (ec *ECTool) I2CLookup(ctx context.Context) (*I2CLookupInfo, error) {
out, err := ec.Command(ctx, "locatechip", "0", "0").Output(ssh.DumpLogOnError)
if err != nil {
return nil, errors.Wrap(err, "running 'ectool locatechip 0 0' on DUT")
}
match := reI2CLookup.FindSubmatch(out)
if match == nil || len(match) == 0 {
return nil, errors.Wrapf(err, "lookup for I2C failed, got %q", string(out))
}
parsedPort, err := strconv.ParseInt(string(match[1]), 0, 0)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse port val %q as int", string(match[1]))
}
parsedAddr, err := strconv.ParseInt(string(match[2]), 0, 0)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse addr val %q as int", string(match[2]))
}
return &I2CLookupInfo{Port: int(parsedPort), Address: int(parsedAddr)}, nil
}
// I2CCmd type holds commands for interacting with i2c using the ectool.
type I2CCmd string
const (
// I2CRead for the 'ectool i2cread <8 | 16> <port> <addr8> <offset>' cmd.
I2CRead I2CCmd = "i2cread"
// I2CSpeed for the 'ectool i2cspeed <port> [speed in kHz]' cmd.
I2CSpeed I2CCmd = "i2cspeed"
// I2CWrite for the 'ectool i2cwrite <8 | 16> <port> <addr8> <offset> <data>' cmd.
I2CWrite I2CCmd = "i2cwrite"
// I2Cxfer for the 'ectool i2cxfer <port> <addr7> <read_count> [bytes...]' cmd.
I2Cxfer I2CCmd = "i2cxfer"
)
// I2C runs the 'ectool i2c*' with provided command and args.
func (ec *ECTool) I2C(ctx context.Context, cmd I2CCmd, args ...string) (string, error) {
cmdAndArgs := append([]string{string(cmd)}, args...)
out, err := ec.Command(ctx, cmdAndArgs...).Output(ssh.DumpLogOnError)
if err != nil {
return "", errors.Wrapf(err, "running 'ectool %s' on DUT with args %v", string(cmd), args)
}
return string(out), nil
}
// CBICmd type holds commands for interacting with cbi using the ectool.
type CBICmd string
const (
// CBIGet for the 'ectool cbi get <tag> [get_flag]'.
CBIGet CBICmd = "get"
// CBISet for the 'ectool cbi set <tag> <value/string> <size> [set_flag]'.
CBISet CBICmd = "set"
// CBIRemove for the 'ectool cbi remove <tag> [set_flag]'.
CBIRemove CBICmd = "remove"
)
// CBI runs the 'ectool cbi' with provided command and args.
func (ec *ECTool) CBI(ctx context.Context, cmd CBICmd, args ...string) (string, error) {
cmdAndArgs := []string{"cbi", string(cmd)}
cmdAndArgs = append(cmdAndArgs, args...)
testing.ContextLogf(ctx, "Running cmd: 'ectool %s'", strings.Join(cmdAndArgs, " "))
out, err := ec.Command(ctx, cmdAndArgs...).Output(ssh.DumpLogOnError)
if err != nil {
return "", errors.Wrapf(err, "running 'ectool %s' on DUT with args %v, got: %v", string(cmd), args, string(out))
}
return string(out), nil
}