blob: 53e40d3da8803439862428cabdb031ef4cc96260 [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"
)
// HPDLevelValue is a type for storing a type-c alt state hpd level
type HPDLevelValue string
// Supported hpd levels
const (
HPDHigh HPDLevelValue = "h"
HPDLow HPDLevelValue = "l"
HPDExt HPDLevelValue = "ext"
HPDirq HPDLevelValue = "irq"
)
const (
servoPDStatePollTimeout time.Duration = 5 * time.Second
servoPDStatePollInterval time.Duration = 500 * time.Millisecond
)
// ServoSendDataSwapRequest initiates a data swap request from the servo's PD port.
func (s *Servo) ServoSendDataSwapRequest(ctx context.Context) (pdControlMsgType, error) {
if err := s.EnableServoConsoleChannel(ctx, "usbpd"); err != nil {
return PDCtrlReserved, errors.Wrap(err, "failed to enable usbpd logging channel")
}
// Enable PD message so we can check the response from the DUT.
err := s.RunServoCommand(ctx, "pd dump 2")
if err != nil {
return PDCtrlReserved, errors.Wrap(err, "failed to send enable PD debug")
}
// Always disable PD commands on exit.
defer s.RunServoCommand(ctx, "pd dump 0") //nolint
out, err := s.RunServoCommandGetOutput(ctx, "pd 1 swap data", []string{reEcPdRecv})
if err != nil {
return PDCtrlReserved, errors.Wrap(err, "failed to send servo data swap")
}
recvMsg, err := strconv.ParseUint(out[0][1], 16, 32)
if err != nil {
return PDCtrlReserved, errors.Wrapf(err, "failed to convert swap RECV message %q", out[0][1])
}
replyValue := int(recvMsg & PdControlMsgMask)
if reply, ok := pdControlMsg[replyValue]; ok {
return reply, nil
}
return PDCtrlReserved, errors.Errorf("unknown PD control message value %q", replyValue)
}
// ServoSendPowerSwapRequest sends power swap request to be initiated by the Servo.
func (s *Servo) ServoSendPowerSwapRequest(ctx context.Context) (pdControlMsgType, error) {
// Enable PD message so we can check the response from the DUT.
err := s.RunServoCommand(ctx, "pd dump 2")
if err != nil {
return PDCtrlReserved, errors.Wrap(err, "failed to send enable PD debug")
}
// Always disable PD commands on exit.
defer s.RunServoCommand(ctx, "pd dump 0") //nolint
out, err := s.RunServoCommandGetOutput(ctx, "pd 1 swap power", []string{reEcPdRecv})
if err != nil {
return PDCtrlReserved, errors.Wrap(err, "failed to send servo data swap")
}
recvMsg, err := strconv.ParseUint(out[0][1], 16, 32)
if err != nil {
return PDCtrlReserved, errors.Wrapf(err, "failed to convert swap RECV message %q", out[0][1])
}
replyValue := int(recvMsg & PdControlMsgMask)
if reply, ok := pdControlMsg[replyValue]; ok {
return reply, nil
}
return PDCtrlReserved, errors.Errorf("unknown PD control message value %q", replyValue)
}
// ServoGetDualRoleState accepts a port ID and checks for the PD DRP status of this port.
func (s *Servo) ServoGetDualRoleState(ctx context.Context) (USBPdDualRoleValue, error) {
port := 1
matchList := []string{`dual-role toggling:\s+([\w ]+)[\r\n]`}
// Try modern `pd N dualrole` command
cmd := fmt.Sprintf("pd %d dualrole", port)
out, err := s.RunServoCommandGetOutput(ctx, cmd, matchList)
if err != nil {
testing.ContextLogf(
ctx, "EC command %q failed. Trying older version. (%q)",
cmd, err,
)
// Older DUTs running firmware from before cl:1096654 don't have per-port
// dualrole settings. Fall back to the old command.
out, err = s.RunServoCommandGetOutput(ctx, "pd dualrole", matchList)
if err != nil {
// Servo does not support DRP
return "", errors.Wrapf(err, "ec command %q failed. No way to check dual role state", cmd)
}
}
testing.ContextLogf(ctx, "Port %d DRP status: %q", port, out[0][1])
return USBPdDualRoleValue(out[0][1]), nil
}
func (s *Servo) toggleServoDualRole(ctx context.Context) (int, error) {
drpCmd := "usbc_action drp"
drpRe := []string{`DRP\s=\s(\d)`}
// Send DRP toggle command to PDTester and get value of 'drp_enable'
testing.ContextLogf(ctx, "PD Tester running: %s", drpCmd)
out, err := s.RunServoCommandGetOutput(ctx, drpCmd, drpRe)
if err != nil {
return 0, errors.Wrapf(err, "command %q failed", drpCmd)
}
i, err := strconv.Atoi(out[0][1])
if err != nil {
return 0, errors.Wrap(err, "failed to convert string to int")
}
return i, nil
}
func (s *Servo) enableServoDualRole(ctx context.Context) error {
for range 2 {
if r, _ := s.toggleServoDualRole(ctx); r == 1 {
testing.ContextLog(ctx, "PDTester DRP mode enabled")
return nil
}
}
testing.ContextLog(ctx, "PDTester DRP mode set failure")
return nil
}
// ServoSetDualRole sets the PD DRP status of this port
func (s *Servo) ServoSetDualRole(ctx context.Context, val USBPdDualRoleValue) error {
// USBPdDualRoleSink and Source contain "force " prefix, strip this from the command
// sent to servo
action := strings.TrimPrefix(string(val), "force ")
state, _ := s.ServoGetDualRoleState(ctx)
if state == val {
return nil
}
if val == USBPdDualRoleOn {
_ = s.enableServoDualRole(ctx)
} else {
if state == USBPdDualRoleOn {
_, err := s.toggleServoDualRole(ctx)
return err
}
cmd := fmt.Sprintf("pd 1 dualrole %s", action)
testing.ContextLogf(ctx, "PD Tester running: %s", cmd)
if err := s.RunServoCommand(ctx, cmd); err != nil {
testing.ContextLogf(
ctx, "EC command %q failed. Trying older version. (%q)",
cmd, err,
)
cmd := fmt.Sprintf("pd dualrole %s", action)
if err := s.RunServoCommand(ctx, cmd); err != nil {
return errors.Wrapf(err, "ec command %q failed", cmd)
}
}
}
state, _ = s.ServoGetDualRoleState(ctx)
if state != val {
return errors.Errorf("failed to set dual role to %q", val)
}
return nil
}
// ServoSetDPConfigs sets the DP configs for the DUT connection
func (s *Servo) ServoSetDPConfigs(ctx context.Context, config *TypeCInfo, mfPref MultiFunctionPref) error {
if config.DPMode == DPEnable {
if err := s.RunServoCommand(ctx, "usbc_action dp enable"); err != nil {
return errors.Wrap(err, "failed to enable DP alt-mode")
}
} else {
if err := s.RunServoCommand(ctx, "usbc_action dp disable"); err != nil {
return errors.Wrap(err, "failed to disable DP alt-mode")
}
}
if err := s.RunServoCommand(ctx, fmt.Sprintf("usbc_action dp pins %s", config.PinsCDEF)); err != nil {
return errors.Wrap(err, "failed to set pin assignments")
}
if err := s.RunServoCommand(ctx, fmt.Sprintf("usbc_action dp mf %d", mfPref)); err != nil {
return errors.Wrap(err, "failed to set mf pref")
}
// we only use commands off and on to preserve the usbc state between resets
if err := s.ServoCcOff(ctx); err != nil {
return errors.Wrap(err, "failed to turn off cc")
}
if err := s.ServoCcOn(ctx); err != nil {
return errors.Wrap(err, "failed to turn on cc")
}
if err := testing.Poll(ctx, func(ctx context.Context) error {
ok, err := s.GetChargerAttached(ctx)
if err != nil {
testing.ContextLog(ctx, "GetChargerAttached failed: ", err)
return errors.Wrap(err, "error checking whether charger is attached")
} else if !ok {
testing.ContextLogf(ctx, "GetChargerAttached got %v, want %v", ok, true)
return errors.Errorf("expected charger attached state: %v", true)
}
return nil
}, &testing.PollOptions{Timeout: 300 * time.Second, Interval: 10 * time.Second}); err != nil {
return errors.Wrap(err, "failed to check if charger is attached")
}
return nil
}
// SetHPD sets the HPD value for an active dp-alt connection
func (s *Servo) SetHPD(ctx context.Context, HPDLevel HPDLevelValue) error {
if err := s.RunServoCommand(ctx, fmt.Sprintf("usbc_action dp hpd %s", HPDLevel)); err != nil {
return errors.Wrap(err, "failed to set hpd level")
}
return nil
}
// SetPlug sets the PLug status for an active dp-alt connection
func (s *Servo) SetPlug(ctx context.Context, Plug bool) error {
cmd := "usbc_action dp plug 0"
if Plug {
cmd = "usbc_action dp plug 1"
}
if err := s.RunServoCommand(ctx, cmd); err != nil {
return errors.Wrap(err, "failed to set plug status")
}
// we only use commands off and on to preserve the usbc state between resets
if err := s.ServoCcOff(ctx); err != nil {
return errors.Wrap(err, "failed to turn off cc")
}
if err := s.ServoCcOn(ctx); err != nil {
return errors.Wrap(err, "failed to turn on cc")
}
return nil
}
// ServoSetUSBVersion3 sets the DUT USB connection as version 2.0 or 3.0
func (s *Servo) ServoSetUSBVersion3(ctx context.Context, USBVersion3 bool) error {
enableVersion3 := "disable"
if USBVersion3 {
enableVersion3 = "enable"
}
cmd := fmt.Sprintf("dut_usb3 %s", enableVersion3)
if err := s.RunServoCommand(ctx, cmd); err != nil {
return errors.Wrap(err, "unable to set usb version")
}
return nil
}
// RequireChargerAttached verifies that the Servo charger port (#0) is an active sink
func (s *Servo) RequireChargerAttached(ctx context.Context) error {
if err := testing.Poll(ctx, func(ctx context.Context) error {
pdState, err := s.GetServoChargerPortPDState(ctx)
if err != nil {
return testing.PollBreak(
errors.Wrap(err, "cannot access charger port PD status"),
)
}
testing.ContextLogf(ctx, "C0 PE State is %s", pdState.PEStateName)
if !pdState.IsSinkReady() {
return errors.New("Servo charger port (C0) is not sink-ready")
}
return nil
}, &testing.PollOptions{Interval: time.Second, Timeout: 5 * time.Second}); err != nil {
return err
}
return nil
}
// EnableServoConsoleChannel enables the provided console log channel (and turns all but the console CLI off)
func (s *Servo) EnableServoConsoleChannel(ctx context.Context, channelName string) error {
// Get current console state
// 0 - full row
// 1 - channel index
// 2 - channel mask
// 3 - enabled flag
// 4 - channel name
out, err := s.RunServoCommandGetOutput(ctx, "chan", []string{fmt.Sprintf(`(\d+)\s+(\w+)\s+(\*?)\s+%s[\r\n]`, channelName)})
if err != nil {
return errors.Wrapf(err, "failed to query channel %q", channelName)
}
mask, err := strconv.ParseUint(out[0][2], 16, 32)
if err != nil {
return errors.Wrap(err, "cannot parse channel mask")
}
if out[0][3] == "*" {
// No update necessary
testing.ContextLogf(ctx, "Servo console channel %q already enabled", channelName)
return nil
}
testing.ContextLogf(ctx, "Enabling Servo console channel %q", channelName)
return s.RunServoCommand(ctx, fmt.Sprintf("chan 0x%08x", mask))
}
// EnableServoPDConsoleDebug enables PD console debugging level 2 on the Servo
func (s *Servo) EnableServoPDConsoleDebug(ctx context.Context) error {
cmd := "pd dump 2"
testing.ContextLog(ctx, "Enabling Servo PD Console Debug")
if err := s.RunServoCommand(ctx, cmd); err != nil {
return errors.Wrap(err, "servo pd command failed")
}
return nil
}
// DisableServoPDConsoleDebug disables PD console debugging on the Servo
func (s *Servo) DisableServoPDConsoleDebug(ctx context.Context) error {
cmd := "pd dump 0"
testing.ContextLog(ctx, "Disabling Servo PD Console Debug")
if err := s.RunServoCommand(ctx, cmd); err != nil {
return errors.Wrap(err, "servo pd command failed")
}
return nil
}
// checkSequenceInConsoleLog is a helper function to search through console
// output and ensure a provided sequence of PD state transitions exists.
func checkSequenceInConsoleLog(log string, port int, sequenceList []string) bool {
i := 0
for _, stateName := range sequenceList {
// Create a regexp object that matches the expected log entry
// for this state name.
re := regexp.MustCompile(
fmt.Sprintf(`C%d\s+[\w]+:?\s(%s)`, port, stateName),
)
idx := re.FindStringIndex(log[i:])
if idx == nil {
return false
}
// Continue the search for the next expected state after this
// log line
i += idx[1]
}
return true
}
// verifyStatesInConsoleLog is a helper function which extracts all of the PD
// state messages from servo console output and then verifies the states match
// in exact order tp the states in the parameter sequenceList
func verifyStatesInConsoleLog(ctx context.Context, log string, port int, sequenceList []string) bool {
// Create a regexp object that extracts all PD state entries from the log
re := regexp.MustCompile(
fmt.Sprintf(`C%d\s+st[\d]+\s([\w]+)`, port),
)
matches := re.FindAllStringSubmatch(log, -1)
states := make([]string, len(matches))
for idx, row := range matches {
states[idx] = row[1]
testing.ContextLogf(ctx, "act = %s <--> exp = %s", row[1], sequenceList[idx])
// As long as the states have matched all the expected states,
// then treat this as a match even if additional state messages
// exist beyond what was expected.
if idx >= len(sequenceList) {
break
}
if states[idx] != sequenceList[idx] {
testing.ContextLogf(ctx, "state list mismatch: %s", states)
return false
}
}
return true
}
// TriggerServoPDSoftReset triggers a USB-PD Soft Reset from the Servo-side
func (s *Servo) TriggerServoPDSoftReset(ctx context.Context) error {
// Get current port status
pdStateBefore, err := s.GetServoPDState(ctx)
if err != nil {
return errors.Wrap(err, "could not get Servo PD state")
}
if pdStateBefore.Connection != PDEnabled {
return errors.New("servo PD status reads disabled. Cannot test without a port partner")
}
if err := s.EnableServoConsoleChannel(ctx, "usbpd"); err != nil {
return err
}
if err := s.EnableServoPDConsoleDebug(ctx); err != nil {
return errors.Wrap(err, "could not enable Servo's PD debug logs")
}
// Go back to `pd dump 0` after.
defer s.DisableServoPDConsoleDebug(ctx) //nolint
// Depending on the current power role, set the list of expected
// PD states following the soft reset
var expectedResetSequence []string
if pdStateBefore.PowerRole == PowerRoleSNK {
expectedResetSequence = []string{
"SOFT_RESET",
"SNK_DISCOVERY",
"SNK_REQUESTED",
"SNK_TRANSITION",
"SNK_READY",
}
} else if pdStateBefore.PowerRole == PowerRoleSRC {
expectedResetSequence = []string{
"SOFT_RESET",
"SRC_DISCOVERY",
"SRC_NEGOCIATE", // [sic]
"SRC_ACCEPTED",
"SRC_POWERED",
"SRC_TRANSITION",
"SRC_READY",
}
} else {
return errors.New("unknown power role state")
}
// Run the command
out, err := s.RunServoCommandGetOutput(ctx, "pd 1 soft", []string{`(.*)(C1)\s+[\w]+:?\s([\w]+_READY)`})
if err != nil {
return errors.Wrap(err, "could not trigger soft reset on Servo")
}
if !checkSequenceInConsoleLog(out[0][0], 1, expectedResetSequence) {
return errors.New("expected reset state sequence not seen in Servo PD soft reset command console output")
}
// Poll until the pre- and post-reset states match or we time out.
if err := testing.Poll(ctx, func(ctx context.Context) error {
pdStateAfter, err := s.GetServoPDState(ctx)
if err != nil {
return errors.Wrap(err, "cannot read servo PD state")
}
return pdStateBefore.Compare(pdStateAfter)
}, &testing.PollOptions{Timeout: pdStatePollTimeout, Interval: pdStatePollInterval}); err != nil {
return errors.Wrap(err, "timed out waiting for states to match after servo soft reset")
}
// TODO (b/317808083) query the servo's soft reset counter here
return nil
}
// TriggerServoPDHardReset triggers a USB-PD Hard Reset from the Servo-side
func (s *Servo) TriggerServoPDHardReset(ctx context.Context) error {
// Get current port status
pdState, err := s.GetServoPDState(ctx)
if err != nil {
return errors.Wrap(err, "could not get Servo PD state")
}
if pdState.Connection != PDEnabled {
return errors.New("servo PD status reads disabled. Cannot test without a port partner")
}
if err := s.EnableServoPDConsoleDebug(ctx); err != nil {
return errors.Wrap(err, "could not enable Servo's PD debug logs")
}
// Go back to `pd dump 0` after.
defer s.DisableServoPDConsoleDebug(ctx) //nolint
// GoBigSleepLint: Let PD state settle before triggering hard reset.
if err := testing.Sleep(ctx, time.Second); err != nil {
return errors.Wrap(err, "failed to sleep")
}
// Depending on the current power role, set the list of expected
// PD states following the soft reset
var expectedResetSequence []string
if pdState.PowerRole == PowerRoleSNK {
expectedResetSequence = []string{
"HARD_RESET_SEND",
"HARD_RESET_EXECUTE",
"SNK_HARD_RESET_RECOVER",
"SNK_DISCOVERY",
"SNK_REQUESTED",
"SNK_TRANSITION",
"SNK_READY",
}
} else if pdState.PowerRole == PowerRoleSRC {
expectedResetSequence = []string{
"HARD_RESET_SEND",
"HARD_RESET_EXECUTE",
"SRC_HARD_RESET_RECOVER",
"SRC_STARTUP",
"SRC_DISCOVERY",
"SRC_NEGOCIATE", // [sic]
"SRC_ACCEPTED",
"SRC_POWERED",
"SRC_TRANSITION",
"SRC_READY",
}
} else {
return errors.New("unknown power role state")
}
// Run the command
out, err := s.RunServoCommandGetOutput(ctx, "pd 1 hard", []string{`(.*)(C1)\s+[\w]+:?\s([\w]+_READY)`})
if err != nil {
return errors.Wrap(err, "could not trigger hard reset on Servo")
}
// Verify hard reset happened and that the connection recovers as expected
if !verifyStatesInConsoleLog(ctx, out[0][0], 1, expectedResetSequence) {
return errors.New("expected reset state sequence not seen in Servo console output")
}
// Hard reset should result in the same power after as before, but the data
// role may be different as it may be the data role associated with the power
// role. Poll here to wait for the data role after the hard reset to be
// the same as before to ensure the PD connection is back to its steady
// state condition.
if err := testing.Poll(ctx, func(ctx context.Context) error {
if pdStateAfter, err := s.GetServoPDState(ctx); err == nil {
if pdState.DataRole != pdStateAfter.DataRole {
return errors.Wrap(err, "Data role does not match expected")
}
} else {
return errors.Wrap(err, "failed to get Servo PD state")
}
return nil
}, &testing.PollOptions{Timeout: servoPDStatePollTimeout, Interval: servoPDStatePollInterval}); err != nil {
return errors.Wrap(err, "Data roles did not match following servo initiated hard reset")
}
return nil
}
// ServoCcOff runs the `cc off` console command on the Servo.
func (s *Servo) ServoCcOff(ctx context.Context) error {
output, err := s.RunServoCommandGetOutput(ctx, "cc off", []string{`cc: (\w+)[\r\n]`})
if err == nil && output[0][1] != "off" {
return errors.New("CC state did not change to 'off'")
}
return err
}
// ServoCcOn runs the `cc on` console command on the Servo.
func (s *Servo) ServoCcOn(ctx context.Context) error {
output, err := s.RunServoCommandGetOutput(ctx, "cc on", []string{`cc: (\w+)[\r\n]`})
if err == nil && output[0][1] != "on" {
return errors.New("CC state did not change to 'on'")
}
return err
}
// ServoGetConnectedStateAfterCCReconnect get the connected state after disconnect/reconnect using PDTester
//
// PDTester supports a feature which simulates a USB Type C disconnect
// and reconnect. It returns the first connected state (either source or
// sink) after reconnect.
//
// @param disconnectTime: Time in seconds for disconnect period.
// @returns: The connected PD state.
func (s *Servo) ServoGetConnectedStateAfterCCReconnect(ctx context.Context, disconnectTime time.Duration) (string, error) {
discDelay := 100
port := 1
cmd := fmt.Sprintf("fakedisconnect %d %d", discDelay, disconnectTime.Milliseconds())
srcConnect := []string{"SRC_READY"}
snkConnect := []string{"SNK_READY"}
srcDisc := "SRC_DISCONNECTED"
sinkDisc := "SNK_DISCONNECTED"
drpAutoToggle := "DRP_AUTO_TOGGLE"
stateExp := `(C%d)\s+[\w]+:?\s(%s)`
disconnectedStates := strings.Join([]string{srcDisc, sinkDisc, drpAutoToggle}, `|`)
disconnectedExp := fmt.Sprintf(stateExp, port, disconnectedStates)
connectedStates := strings.Join(append(srcConnect, snkConnect...), `|`)
connectedExp := fmt.Sprintf(stateExp, port, connectedStates)
if err := s.EnableServoPDConsoleDebug(ctx); err != nil {
return "", errors.Wrap(err, "could not enable Servo's PD debug logs")
}
// Go back to `pd dump 0` after.
defer s.DisableServoPDConsoleDebug(ctx) //nolint
output, err := s.RunServoCommandGetOutput(ctx, cmd, []string{disconnectedExp, connectedExp})
if err != nil {
return "", errors.Wrap(err, "failed to run fakedisconnect cmd")
}
return output[1][2], nil
}