blob: 9844a208fda94fcbb8d254ba26cdf0de1ceb79b8 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Package cellular contains utilities for repairing cellular DUTs.
package cellular
import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
"go.chromium.org/luci/common/errors"
"infra/cros/recovery/internal/components"
"infra/cros/recovery/internal/log"
"infra/cros/recovery/internal/retry"
"infra/cros/recovery/tlw"
)
const (
modemManagerJob = "modemmanager"
detectCmd = "mmcli -m a -J"
mmcliCliPresentCmd = "which mmcli"
modemManagerJobPresentCmd = "initctl status modemmanager"
restartModemManagerCmd = "restart modemmanager"
getSignalStrengthCmd = "mmcli -m a --signal-get -J"
startModemManagerCmd = "start modemmanager"
shillInterface = "org.chromium.flimflam"
getCellularServiceCmd = "gdbus call --system --dest=org.chromium.flimflam" +
" -o / -m org.chromium.flimflam.Manager.FindMatchingService" +
" \"{'Type': 'cellular'}\" | cut -d\"'\" -f2"
)
// SIM state aliases.
const (
SIMStateUnspecified = tlw.Cellular_SIMProfileInfo_UNSPECIFIED
SIMStateBroken = tlw.Cellular_SIMProfileInfo_BROKEN
SIMStateLocked = tlw.Cellular_SIMProfileInfo_LOCKED
SIMStateNoNetwork = tlw.Cellular_SIMProfileInfo_NO_NETWORK
SIMStateWorking = tlw.Cellular_SIMProfileInfo_WORKING
)
// HasModemManagerCLI returns true if mmcli is present on the DUT.
func HasModemManagerCLI(ctx context.Context, runner components.Runner, timeout time.Duration) bool {
if _, err := runner(ctx, timeout, mmcliCliPresentCmd); err != nil {
return false
}
return true
}
// HasModemManagerJob returns true if modemmanager job is present on the DUT.
func HasModemManagerJob(ctx context.Context, runner components.Runner, timeout time.Duration) bool {
if _, err := runner(ctx, timeout, modemManagerJobPresentCmd); err != nil {
return false
}
return true
}
// StartModemManager starts modemmanager via upstart.
func StartModemManager(ctx context.Context, runner components.Runner, timeout time.Duration) error {
if _, err := runner(ctx, timeout, startModemManagerCmd); err != nil {
return errors.Annotate(err, "start modemmanager").Err()
}
return nil
}
// RestartModemManager restarts modemmanager via upstart.
func RestartModemManager(ctx context.Context, runner components.Runner, timeout time.Duration) error {
if _, err := runner(ctx, timeout, restartModemManagerCmd); err != nil {
return errors.Annotate(err, "restart modemmanager").Err()
}
return nil
}
// ConnectToDefaultService attempts a simple connection to the default cellular service.
func ConnectToDefaultService(ctx context.Context, runner components.Runner, timeout time.Duration) error {
info, err := WaitForModemInfo(ctx, runner, 15*time.Second)
if err != nil {
return errors.Annotate(err, "connect to default service: get modem info").Err()
}
// skip if already in connected state
if strings.EqualFold(info.GetState(), string(ModemStateConnected)) {
log.Infof(ctx, "connect to default service: modem is already connected to service")
return nil
}
// don't attempt to connect if in "connecting" state
if !strings.EqualFold(info.GetState(), string(ModemStateConnecting)) {
serviceName, err := runner(ctx, 5*time.Second, getCellularServiceCmd)
if err != nil {
return errors.Annotate(err, "connect to default service: get service name").Err()
}
connectCmd := fmt.Sprintf("dbus-send --system --fixed --print-reply --dest=%s %s %s.Service.Connect", shillInterface, serviceName, shillInterface)
if _, err := runner(ctx, 30*time.Second, connectCmd); err != nil {
return errors.Annotate(err, "connect to default service").Err()
}
}
if err := WaitForModemState(ctx, runner, timeout, ModemStateConnected); err != nil {
return errors.Annotate(err, "connect to default service: modem never entered connected state").Err()
}
return nil
}
// WaitForModemManager waits for the modemmanager job to be running via upstart.
func WaitForModemManager(ctx context.Context, runner components.Runner, timeout time.Duration) error {
cmd := fmt.Sprintf("status %s", modemManagerJob)
return retry.WithTimeout(ctx, time.Second, timeout, func() error {
if output, err := runner(ctx, 5*time.Second, cmd); err != nil {
return errors.Annotate(err, "get modemmanager status").Err()
} else if !strings.Contains(output, "start/running") {
return errors.Reason("modemmanager not running").Err()
}
return nil
}, "wait for modemmanager")
}
// WaitForModemState polls for the modem to enter a specific state.
func WaitForModemState(ctx context.Context, runner components.Runner, timeout time.Duration, state ModemState) error {
predicate := func(m *ModemInfo) error {
if m.GetState() == "" {
return errors.Reason("modem state is empty").Err()
}
if !strings.EqualFold(m.GetState(), string(state)) {
return errors.Reason("modem state not equal to %s", state).Err()
}
return nil
}
if _, err := WaitForModemInfo(ctx, runner, timeout, predicate); err != nil {
return errors.Annotate(err, "wait for modem state: wait for modem to enter requested state").Err()
}
return nil
}
// ModemInfo is a simplified version of the JSON output from ModemManager to get the modem connection state information.
type ModemInfo struct {
Modem *struct {
G3PP *struct {
Imei string `json:"imei,omitempty"`
} `json:"3gpp,omitempty"`
Generic *struct {
ActiveSIMSlot string `json:"primary-sim-slot,omitempty"`
State string `json:"state,omitempty"`
SIM string `json:"sim,omitempty"`
SIMSlots []string `json:"sim-slots,omitempty"`
OwnNumbers []string `json:"own-numbers,omitempty"`
} `json:"generic,omitempty"`
} `json:"modem,omitempty"`
}
// ModemState represents a valid cellular modem state.
type ModemState string
const (
// ModemStateConnected represents a modem connected to a cellular network.
ModemStateConnected ModemState = "CONNECTED"
// ModemStateConnecting represents a modem in the process of connecting to a cellular network.
ModemStateConnecting ModemState = "CONNECTING"
)
// ActiveSIMSlot returns the currently active modem SIM slot.
func (m *ModemInfo) ActiveSIMSlot() int32 {
if m == nil || m.Modem == nil || m.Modem.Generic == nil {
return 0
}
// If the modem only has 1 SIM slot, then ActiveSIMSlot will be empty but SIM will not be.
// In this case we should return "1" since sim slots are 1 indexed.
//
// e.g. {.., "primary-sim-slot":"--", "sim":"/org/freedesktop/ModemManager1/SIM/0", ...}
if m.Modem.Generic.ActiveSIMSlot == "--" && m.Modem.Generic.SIM != "" {
return 1
}
if m.Modem.Generic.ActiveSIMSlot == "" || m.Modem.Generic.ActiveSIMSlot == "--" {
return 0
}
i, err := strconv.ParseInt(m.Modem.Generic.ActiveSIMSlot, 10, 32)
if err != nil {
return 0
}
return int32(i)
}
// ActiveSIMID returns the ID of the active SIM slot.
func (m *ModemInfo) ActiveSIMID() string {
if m == nil || m.Modem == nil || m.Modem.Generic == nil {
return ""
}
if m.Modem.Generic.SIM == "" || m.Modem.Generic.SIM == "--" {
return ""
}
simPath := strings.Split(m.Modem.Generic.SIM, "/")
return simPath[len(simPath)-1]
}
// OwnNumber returns the modem's current phone number.
func (m *ModemInfo) OwnNumber() string {
if m == nil || m.Modem == nil || m.Modem.Generic == nil {
return ""
}
for _, number := range m.Modem.Generic.OwnNumbers {
// Strip the country code from the phone number e.g. +1
if len(number) > 10 {
return number[len(number)-10:]
}
if len(number) == 10 {
return number
}
}
return ""
}
// SIMSlotCount returns the number of SIM slots available on the device.
func (m *ModemInfo) SIMSlotCount() int32 {
if m == nil || m.Modem == nil || m.Modem.Generic == nil {
return 0
}
// If the modem only has 1 SIM slot, then SIMSlots will ben an empty list but SIM will not
// be empty. In this case we should return 1.
if len(m.Modem.Generic.SIMSlots) == 0 && m.Modem.Generic.SIM != "" {
return 1
}
return int32(len(m.Modem.Generic.SIMSlots))
}
// GetState returns the modems state as reported by ModemManager.
func (m *ModemInfo) GetState() string {
if m == nil || m.Modem == nil || m.Modem.Generic == nil {
return ""
}
return m.Modem.Generic.State
}
// GetSIMState returns the state of the SIM as reported by ModemManager.
//
// This is a wrapper for the ModemManagerState.
func (m *ModemInfo) GetSIMState() tlw.Cellular_SIMProfileInfo_State {
if m == nil || m.Modem == nil || m.Modem.Generic == nil {
return SIMStateUnspecified
}
state := m.Modem.Generic.State
switch {
case strings.EqualFold(state, "LOCKED"):
return SIMStateLocked
// Non-failed states but not connected.
case strings.EqualFold(state, "INITIALIZING"),
strings.EqualFold(state, "DISABLED"),
strings.EqualFold(state, "DISABLING"),
strings.EqualFold(state, "ENABLING"),
strings.EqualFold(state, "ENABLED"),
strings.EqualFold(state, "SEARCHING"),
strings.EqualFold(state, "REGISTERED"),
strings.EqualFold(state, "DISCONNECTING"),
strings.EqualFold(state, "CONNECTING"):
return SIMStateNoNetwork
case strings.EqualFold(state, "CONNECTED"):
return SIMStateWorking
// Any other state is broken e.g. FAILED.
default:
return SIMStateBroken
}
}
func (m *ModemInfo) GetImei() string {
// ModemManager may replace missing fields with "--"
if m == nil || m.Modem == nil || m.Modem.G3PP == nil || strings.EqualFold(m.Modem.G3PP.Imei, "--") {
return ""
}
return m.Modem.G3PP.Imei
}
// ModemPredicate returns an error if the modem is not in the correct state.
type ModemPredicate func(m *ModemInfo) error
// WaitForModemInfo polls for a modem to appear on the DUT, which can take up to two minutes on reboot.
func WaitForModemInfo(ctx context.Context, runner components.Runner, timeout time.Duration, predicates ...ModemPredicate) (*ModemInfo, error) {
var info *ModemInfo
if err := retry.WithTimeout(ctx, time.Second, timeout, func() error {
output, err := runner(ctx, 5*time.Second, detectCmd)
if err != nil {
return errors.Annotate(err, "call mmcli").Err()
}
// Note: info is defined in outer scope as retry.WithTimeout only allows returning errors.
info, err = parseModemInfo(ctx, output)
if err != nil {
return errors.Annotate(err, "parse mmcli response").Err()
}
if info == nil || info.Modem == nil {
return errors.Reason("no modem found on DUT").Err()
}
// Wait for any additional state requirements.
for _, predicate := range predicates {
if err := predicate(info); err != nil {
return errors.Annotate(err, "failed predicate").Err()
}
}
return nil
}, "wait for modem"); err != nil {
return nil, errors.Annotate(err, "wait for modem info: wait for ModemManager to export modem").Err()
}
return info, nil
}
// parseModemInfo unmarshals the modem properties json output from mmcli.
func parseModemInfo(ctx context.Context, output string) (*ModemInfo, error) {
info := &ModemInfo{}
if err := json.Unmarshal([]byte(output), info); err != nil {
return nil, err
}
return info, nil
}
// signalInfo is a simplified version of the JSON output from ModemManager to get the modem signal strength information
type signalInfo struct {
Modem *struct {
Signal *struct {
FiveG *struct {
RSRP string `json:"rsrp,omitempty"`
RSSI string `json:"rssi,omitempty"`
SNR string `json:"snr,omitempty"`
} `json:"5g,omitempty"`
LTE *struct {
RSRP string `json:"rsrp,omitempty"`
RSSI string `json:"rssi,omitempty"`
SNR string `json:"snr,omitempty"`
} `json:"lte,omitempty"`
} `json:"signal,omitempty"`
} `json:"modem,omitempty"`
}
// NetworkTechnology represents a cellular network technology.
type NetworkTechnology string
const (
// NetworkTechnologyLTE represents an LTE cellular network.
NetworkTechnologyLTE NetworkTechnology = "LTE"
// NetworkTechnology5G represents a 5G cellular network.
NetworkTechnology5G NetworkTechnology = "5G"
)
// SignalStrength represents a generic cellular signal measurement. Different modems may
// report all or only some of the possible measurements, unavailable measurements are set
// to nil.
type SignalStrength struct {
RSRP *float64
RSSI *float64
SNR *float64
Technology NetworkTechnology
}
func (s SignalStrength) HasValue() bool {
return s.RSRP != nil || s.RSSI != nil || s.SNR != nil
}
// GetSignalStrength fetches the available cellular signals and returns their strengths.
// Note: Multiple signal technologies may be available at one time (i.e. 5G and LTE) in which case
// all available will be returned.
func GetSignalStrength(ctx context.Context, runner components.Runner, timeout time.Duration) ([]SignalStrength, error) {
signalStrength := make([]SignalStrength, 0)
if err := retry.WithTimeout(ctx, time.Second, timeout, func() error {
output, err := runner(ctx, 5*time.Second, getSignalStrengthCmd)
if err != nil {
return errors.Annotate(err, "query signal").Err()
}
info, err := parseSignalInfo(ctx, output)
if err != nil {
return errors.Annotate(err, "parse signal response").Err()
}
if info == nil || info.Modem == nil || info.Modem.Signal == nil {
return errors.Reason("no signal info found on DUT").Err()
}
if info.Modem.Signal.FiveG != nil {
// CLI may return "--" or empty strings for missing signals so we need
// to verify by attempting to parse.
strength := SignalStrength{}
if rsrp, err := strconv.ParseFloat(info.Modem.Signal.FiveG.RSRP, 32); err == nil {
strength.RSRP = &rsrp
}
if rssi, err := strconv.ParseFloat(info.Modem.Signal.FiveG.RSSI, 32); err == nil {
strength.RSSI = &rssi
}
if snr, err := strconv.ParseFloat(info.Modem.Signal.FiveG.SNR, 32); err == nil {
strength.SNR = &snr
}
if strength.HasValue() {
strength.Technology = NetworkTechnology5G
signalStrength = append(signalStrength, strength)
}
}
if info.Modem.Signal.LTE != nil {
strength := SignalStrength{}
if rsrp, err := strconv.ParseFloat(info.Modem.Signal.LTE.RSRP, 32); err == nil {
strength.RSRP = &rsrp
}
if rssi, err := strconv.ParseFloat(info.Modem.Signal.LTE.RSSI, 32); err == nil {
strength.RSSI = &rssi
}
if snr, err := strconv.ParseFloat(info.Modem.Signal.LTE.SNR, 32); err == nil {
strength.SNR = &snr
}
if strength.HasValue() {
strength.Technology = NetworkTechnologyLTE
signalStrength = append(signalStrength, strength)
}
}
// if any available signals were found
if len(signalStrength) > 0 {
return nil
}
return errors.Reason("no signal info found on DUT").Err()
}, "wait for modem"); err != nil {
return nil, errors.Annotate(err, "wait for modem info: wait for ModemManager to export modem").Err()
}
return signalStrength, nil
}
// parseSignalInfo unmarshals the modem signal properties json output from mmcli.
func parseSignalInfo(ctx context.Context, output string) (*signalInfo, error) {
info := &signalInfo{}
if err := json.Unmarshal([]byte(output), info); err != nil {
return nil, err
}
return info, nil
}