blob: 02f39c0908ffe166f0ce9bc4c677839cb398068e [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
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"go.chromium.org/luci/common/errors"
"infra/cros/recovery/internal/components"
"infra/cros/recovery/internal/log"
"infra/cros/recovery/tlw"
)
// simInfo is a simplified version of the JSON output from ModemManager containing the SIM information.
type simInfo struct {
SIM *struct {
Properties *struct {
ICCID string `json:"iccid,omitempty"`
EID string `json:"eid,omitempty"`
OperatorName string `json:"operator-name,omitempty"`
OperatorCode string `json:"operator-code,omitempty"`
TypeString string `json:"sim-type,omitempty"`
} `json:"properties,omitempty"`
} `json:"sim,omitempty"`
}
// Type returns the type of SIM represented..
func (s *simInfo) Type() tlw.Cellular_SIMType {
if s == nil || s.SIM == nil || s.SIM.Properties == nil {
return tlw.Cellular_SIM_UNSPECIFIED
}
// If SIM has an EID or is explicitly eSIM then it is 'digital'
if s.EID() != "" || strings.EqualFold(s.SIM.Properties.TypeString, "esim") {
return tlw.Cellular_SIM_DIGITAL
}
return tlw.Cellular_SIM_PHYSICAL
}
// ICCID returns the SIMs ICCID.
func (s *simInfo) ICCID() string {
if s == nil || s.SIM == nil || s.SIM.Properties == nil {
return ""
}
// Modem manager may populate empty properties with "--"
if s.SIM.Properties.ICCID == "--" {
return ""
}
return s.SIM.Properties.ICCID
}
// EID returns the SIMs EID.
func (s *simInfo) EID() string {
if s == nil || s.SIM == nil || s.SIM.Properties == nil {
return ""
}
// Modem manager may populate empty properties with "--"
if s.SIM.Properties.EID == "--" {
return ""
}
return s.SIM.Properties.EID
}
// knownCarriers is a map of known operator_code to carrier mappings present in
// the lab, these are generally fixed and should only really need to be updated when
// adding new carriers to the lab..
var knownCarriers = map[string]tlw.Cellular_NetworkProvider{
"00101": tlw.Cellular_NETWORK_AMARISOFT,
"001010": tlw.Cellular_NETWORK_AMARISOFT,
"23415": tlw.Cellular_NETWORK_VODAFONE,
"23430": tlw.Cellular_NETWORK_EE,
"302220": tlw.Cellular_NETWORK_TELUS,
"302720": tlw.Cellular_NETWORK_ROGER,
"310260": tlw.Cellular_NETWORK_TMOBILE,
"311882": tlw.Cellular_NETWORK_TMOBILE,
"310280": tlw.Cellular_NETWORK_ATT,
"310410": tlw.Cellular_NETWORK_ATT,
"311480": tlw.Cellular_NETWORK_VERIZON,
"44010": tlw.Cellular_NETWORK_DOCOMO,
"44011": tlw.Cellular_NETWORK_RAKUTEN,
"44020": tlw.Cellular_NETWORK_SOFTBANK,
"44051": tlw.Cellular_NETWORK_KDDI,
}
// CarrierName returns the SIMs operator/carrier name.
func (s *simInfo) CarrierName() tlw.Cellular_NetworkProvider {
if s == nil || s.SIM == nil || s.SIM.Properties == nil {
return tlw.Cellular_NETWORK_UNSPECIFIED
}
// First try to determine Carrier Name from OperatorCode as it is more reliable,
// if that fails then fallback to the OperatorName.
oc := s.SIM.Properties.OperatorCode
if _, ok := knownCarriers[oc]; ok {
return knownCarriers[oc]
}
on := s.SIM.Properties.OperatorName
switch {
case strings.EqualFold(on, "AT&T"):
return tlw.Cellular_NETWORK_ATT
case strings.EqualFold(on, "VERIZON WIRELESS"):
return tlw.Cellular_NETWORK_VERIZON
case strings.EqualFold(on, "T-MOBILE"):
return tlw.Cellular_NETWORK_TMOBILE
case strings.EqualFold(on, "USIM"):
return tlw.Cellular_NETWORK_AMARISOFT
case strings.EqualFold(on, "NTT DOCOMO"):
return tlw.Cellular_NETWORK_DOCOMO
case strings.EqualFold(on, "EE"):
return tlw.Cellular_NETWORK_EE
case strings.EqualFold(on, "KDDI"):
return tlw.Cellular_NETWORK_KDDI
case strings.EqualFold(on, "RAKUTEN"):
return tlw.Cellular_NETWORK_RAKUTEN
case strings.EqualFold(on, "SOFTBANK"):
return tlw.Cellular_NETWORK_SOFTBANK
case strings.EqualFold(on, "GOOGLE FI"):
return tlw.Cellular_NETWORK_FI
case strings.EqualFold(on, "SPRINT"):
return tlw.Cellular_NETWORK_SPRINT
// vodafone may be 'vodafone UK' or some other variant.
case strings.Contains(strings.ToLower(on), "vodafone"):
return tlw.Cellular_NETWORK_VODAFONE
// If empty, then return unspecified rather than unsupported.
case on == "" || on == "--":
return tlw.Cellular_NETWORK_UNSPECIFIED
default:
return tlw.Cellular_NETWORK_UNSUPPORTED
}
}
// GetAllSIMInfo queries all SIM cards on the DUT and populates their information.
func GetAllSIMInfo(ctx context.Context, runner components.Runner) ([]*tlw.Cellular_SIMInfo, error) {
modemInfo, err := WaitForModemInfo(ctx, runner, 15*time.Second)
if err != nil {
return nil, errors.Annotate(err, "get all sim info: wait for ModemManager to export modem").Err()
}
oldSlot := modemInfo.ActiveSIMSlot()
defer func() {
if err := SwitchSIMSlot(ctx, runner, oldSlot); err != nil {
log.Errorf(ctx, "get all sim info: failed to restore original SIM slot: ", err)
}
}()
res := make([]*tlw.Cellular_SIMInfo, 0)
for i := int32(0); i < modemInfo.SIMSlotCount(); i++ {
if err := SwitchSIMSlot(ctx, runner, i+1); err != nil {
return nil, errors.Annotate(err, "get all sim info: switch to requested SIM slot").Err()
}
simInfo, err := GetSIMInfo(ctx, runner)
if err != nil {
return nil, errors.Annotate(err, "get all sim info: failed to query info for sim slot: %d", i+1).Err()
}
if simInfo != nil {
res = append(res, simInfo)
}
}
return res, nil
}
// SwitchSIMSlot switches the active SIM slot to the requested index.
func SwitchSIMSlot(ctx context.Context, runner components.Runner, slotNumber int32) error {
modemInfo, err := WaitForModemInfo(ctx, runner, 15*time.Second)
if err != nil {
return errors.Annotate(err, "switch sim slot: wait for ModemManager to export modem").Err()
}
if modemInfo.ActiveSIMSlot() == slotNumber {
log.Debugf(ctx, "sim slot already on %d", slotNumber)
return nil
}
if _, err := runner(ctx, 5*time.Second, fmt.Sprintf("mmcli -m a --set-primary-sim-slot=%d", slotNumber)); err != nil {
return errors.Annotate(err, "call mmcli").Err()
}
predicate := func(m *ModemInfo) error {
if m.ActiveSIMSlot() != slotNumber {
return errors.Reason("active modem slot not equal to %d", slotNumber).Err()
}
return nil
}
// Wait for up to 45 seconds for Modem to re-appear after slot switch.
if _, err := WaitForModemInfo(ctx, runner, 120*time.Second, predicate); err != nil {
return errors.Annotate(err, "switch sim slot: wait for ModemManager to export modem").Err()
}
return nil
}
// GetSIMInfo queries the SIM information in the current slot or returns nil if there is no SIM in the slot.
func GetSIMInfo(ctx context.Context, runner components.Runner) (*tlw.Cellular_SIMInfo, error) {
modemInfo, err := WaitForModemInfo(ctx, runner, 15*time.Second)
if err != nil {
return nil, errors.Annotate(err, "get sim info: wait for ModemManager to export modem").Err()
}
// If this is a pSIM slot with no SIM, then there will be no SIM dbus path.
// No sim detected in the slot -> return with no error.
simID := modemInfo.ActiveSIMID()
if simID == "" {
log.Infof(ctx, "get sim info: no SIM detected in slot: %d, empty sim dbus path", modemInfo.ActiveSIMSlot())
return nil, nil
}
output, err := runner(ctx, 5*time.Second, "mmcli -J -i "+simID)
if err != nil {
return nil, errors.Annotate(err, "get sim info: failed to call mmcli").Err()
}
si, err := parseSIMInfo(ctx, output)
if err != nil {
return nil, errors.Annotate(err, "get sim info: failed parsing mmcli response").Err()
}
// If this is an eSIM slot with no SIM profile, then there will be an empty ICCID even if the SIM dbus exists.
if si.ICCID() == "" {
log.Infof(ctx, "get sim info: no SIM detected in slot: %d, empty sim ICCID", modemInfo.ActiveSIMSlot())
return nil, nil
}
// Verify some required information before returning
if modemInfo.ActiveSIMSlot() == 0 {
return nil, errors.Reason("get sim info: SIM slot ID is empty.").Err()
}
if si.Type() == tlw.Cellular_SIM_UNSPECIFIED {
return nil, errors.Reason("get sim info: SIM type is empty.").Err()
}
return &tlw.Cellular_SIMInfo{
SlotId: modemInfo.ActiveSIMSlot(),
Type: si.Type(),
Eid: si.EID(),
ProfileInfos: []*tlw.Cellular_SIMProfileInfo{
{
Iccid: si.ICCID(),
CarrierName: si.CarrierName(),
OwnNumber: modemInfo.OwnNumber(),
},
},
}, nil
}
// parseSIMInfo unmarshals the SIM properties json output from mmcli.
func parseSIMInfo(ctx context.Context, output string) (*simInfo, error) {
info := &simInfo{}
if err := json.Unmarshal([]byte(output), info); err != nil {
return nil, err
}
return info, nil
}