blob: 09095b46bc7c65560dae2d22ae7f512e158fa160 [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 charge provides necessary functionality to perform battery charge, drain on DUT.
package charge
import (
"context"
"time"
"chromiumos/tast/common/servo"
"chromiumos/tast/ctxutil"
"chromiumos/tast/errors"
"chromiumos/tast/local/chrome"
"chromiumos/tast/local/power"
"chromiumos/tast/testing"
)
// EnsureBatteryWithinRange ensures the device battery within the specified min & max percentages.
// Powerd reports battery_percent & battery_display_percent and for all the operations the latter
// one has been taken into consideration.
// This function queries the current battery display percentage of DUT and initiates charging or draining as required.
func EnsureBatteryWithinRange(ctx context.Context, cr *chrome.Chrome, s *servo.Servo, minPercentage, maxPercentage float64) error {
if minPercentage < 0.0 || minPercentage > 100.0 {
return errors.New("invalid min percentage, it should be within [0.0, 100.0]")
}
if maxPercentage < 0.0 || maxPercentage > 100.0 {
return errors.New("invalid max percentage, it should be within [0.0, 100.0]")
}
// Shorten deadline to leave time for cleanup.
cleanupCtx := ctx
ctx, cancel := ctxutil.Shorten(ctx, 10*time.Second)
defer cancel()
status, err := power.GetStatus(ctx)
if err != nil {
return errors.Wrap(err, "failed to obtain DUT power status")
}
// Battery already is in required range.
if p := status.BatteryDisplayPercent; p > minPercentage && p < maxPercentage {
return nil
}
role, err := s.GetPDRole(ctx) // storing the current servo role to perform a deferred restore
if err != nil {
return errors.Wrap(err, "failed to get current servo power delivery (pd) role")
}
if role == servo.PDRoleNA {
return errors.New(`requires "servo v4" for operating DUT power delivery through servo_pd_role`)
}
defer func(ctx context.Context) {
if err := s.SetPDRole(ctx, role); err != nil {
testing.ContextLogf(ctx, "Failed to restore servo_pd_role to %s during cleanup: %v", role, err)
}
}(cleanupCtx)
if status.BatteryDisplayPercent < minPercentage { // charging
if err := s.SetPDRole(ctx, servo.PDRoleSrc); err != nil {
return errors.Wrapf(err, "unable to set servo_pd_role to %s", servo.PDRoleSrc)
}
testing.ContextLogf(ctx, "Battery charging has been initiated. Target percentage: %.2f%%", minPercentage)
if err := charge(ctx, minPercentage); err != nil {
return err
}
} else { // discharging
if err := s.SetPDRole(ctx, servo.PDRoleSnk); err != nil {
return errors.Wrapf(err, "unable to set servo_pd_role to %s", servo.PDRoleSnk)
}
testing.ContextLogf(ctx, "Battery discharging has been initiated. Target percentage: %.2f%%", maxPercentage)
if err := drain(ctx, cr, maxPercentage); err != nil {
return err
}
}
return nil
}
// charge charges the device to the specified display percentage.
func charge(ctx context.Context, displayPercentage float64) error {
// Shorten deadline to leave time for cleanup.
cleanupCtx := ctx
ctx, cancel := ctxutil.Shorten(ctx, 5*time.Second)
defer cancel()
if err := powerSourceStatus(ctx, true); err != nil {
return err
}
// Dimming DUT screen.
pm, err := power.NewPowerManager(ctx)
if err != nil {
return errors.Wrap(err, "failed to create a PowerManager object")
}
brightness, err := pm.GetScreenBrightnessPercent(ctx)
if err := pm.SetScreenBrightness(ctx, 10); err != nil {
return errors.Wrap(err, "failed to update screen brightness")
}
defer func(ctx context.Context) {
if err := pm.SetScreenBrightness(ctx, brightness); err != nil {
testing.ContextLogf(ctx, "Failed to reset screen brightness to %.2f%%: %v", brightness, err)
}
}(cleanupCtx)
if err := testing.Poll(ctx, func(context.Context) error {
status, err := power.GetStatus(ctx)
if err != nil {
return testing.PollBreak(errors.Wrap(err, "failed to obtain DUT power status"))
}
if status.BatteryDischarging {
return testing.PollBreak(errors.Wrap(err, "power line isn't connected, can't charge device"))
}
if status.BatteryDisplayPercent < displayPercentage {
return errors.Errorf("still charging from %.2f%% to %.2f%%", status.BatteryDisplayPercent, displayPercentage)
}
return nil
}, &testing.PollOptions{
Interval: time.Second,
}); err != nil {
return errors.Wrapf(err, "failed to charge battery to %.2f%%", displayPercentage)
}
return nil
}
// drain discharges the device battery to the specified display percentage by rendering a resource heavy WebGL graphics.
func drain(ctx context.Context, cr *chrome.Chrome, displayPercentage float64) error {
// Shorten deadline to leave time for cleanup.
cleanupCtx := ctx
ctx, cancel := ctxutil.Shorten(ctx, 10*time.Second)
defer cancel()
if err := powerSourceStatus(ctx, false); err != nil {
return err
}
// Maxing out screen brightness to drain faster.
pm, err := power.NewPowerManager(ctx)
if err != nil {
return errors.Wrap(err, "failed to create a PowerManager object")
}
brightness, err := pm.GetScreenBrightnessPercent(ctx)
if err := pm.SetScreenBrightness(ctx, 100); err != nil {
return errors.Wrap(err, "failed to update screen brightness")
}
defer func(ctx context.Context) {
if err := pm.SetScreenBrightness(ctx, brightness); err != nil {
testing.ContextLogf(ctx, "Failed to reset screen brightness to %.2f%%: %v", brightness, err)
}
}(cleanupCtx)
// Rendering a WebGL website to consume power quickly.
conn, err := cr.NewConn(ctx, "https://crospower.page.link/power_BatteryDrain")
if err != nil {
testing.ContextLog(ctx, "Failed to open page: ", err)
}
defer conn.Close()
defer conn.CloseTarget(cleanupCtx)
if err := testing.Poll(ctx, func(context.Context) error {
status, err := power.GetStatus(ctx)
if err != nil {
return testing.PollBreak(errors.Wrap(err, "failed to obtain DUT power status"))
}
if status.LinePowerConnected {
return testing.PollBreak(errors.New("battery draining requires device disconnected from the power source"))
}
if status.BatteryDisplayPercent > displayPercentage {
return errors.Errorf("still discharging from %.2f%% to %.2f%%", displayPercentage, status.BatteryDisplayPercent)
}
return nil
}, &testing.PollOptions{
Interval: time.Second,
}); err != nil {
return errors.Wrapf(err, "failed to drain battery to %.2f%%", displayPercentage)
}
return nil
}
// powerSourceStatus polls device power source connection status to tackle the micro delay between flipping
// servo role and DUT reflects the status.
func powerSourceStatus(ctx context.Context, acConnected bool) error {
return testing.Poll(ctx, func(context.Context) error {
status, err := power.GetStatus(ctx)
if err != nil {
return testing.PollBreak(errors.Wrap(err, "failed to obtain DUT power status"))
}
if acConnected && !status.LinePowerConnected {
return errors.New("battery charging requires device conntected to an active power source")
}
if !acConnected && status.LinePowerConnected {
return errors.New("battery draining requires device disconnected from the power source")
}
return nil
}, &testing.PollOptions{
Timeout: 20 * time.Second,
})
}