mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-19 22:33:43 +08:00
814 lines
25 KiB
Go
814 lines
25 KiB
Go
package device
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/iniwex5/quectel-qmi-go/pkg/qmi"
|
|
|
|
"vocat/internal/qmiport"
|
|
)
|
|
|
|
type qmiRadioSession interface {
|
|
GetOperatingMode(context.Context) (qmi.OperatingMode, error)
|
|
SetOperatingMode(context.Context, qmi.OperatingMode) error
|
|
Close() error
|
|
}
|
|
|
|
type qmiRadioSessionOpener func(context.Context, string) (qmiRadioSession, error)
|
|
|
|
type nativeQMIICCIDSession interface {
|
|
GetICCID(context.Context) (string, error)
|
|
}
|
|
|
|
type nativeQMIIMEISession interface {
|
|
GetIMEI(context.Context) (string, error)
|
|
}
|
|
|
|
type nativeQMIEuiccSession interface {
|
|
qmiRadioSession
|
|
OpenLogicalChannel(context.Context, uint8, []byte) (byte, error)
|
|
CloseLogicalChannel(context.Context, uint8, uint8) error
|
|
SendAPDU(context.Context, uint8, uint8, []byte) ([]byte, error)
|
|
}
|
|
|
|
// nativeQMIRefreshSession is implemented by production QMI sessions that can
|
|
// participate in the modem's UIM REFRESH state machine. Keep it separate from
|
|
// nativeQMIEuiccSession so transcript fakes and older QMI implementations can
|
|
// continue to use the APDU transport without pretending to handle indications.
|
|
type nativeQMIRefreshSession interface {
|
|
RegisterUIMRefresh(context.Context) error
|
|
CompleteUIMRefresh(context.Context) error
|
|
AcknowledgeUIMRefresh(context.Context) error
|
|
}
|
|
|
|
type nativeQMIUIMResetSession interface {
|
|
ResetUIM(context.Context) error
|
|
}
|
|
|
|
type nativeQMIVoWiFiSession interface {
|
|
qmiRadioSession
|
|
GetICCID(context.Context) (string, error)
|
|
GetIMEI(context.Context) (string, error)
|
|
GetIMSI(context.Context) (string, error)
|
|
GetNativeMCCMNC(context.Context) (string, string, error)
|
|
GetUSIMAID(context.Context) ([]byte, error)
|
|
GetISIMAID(context.Context) ([]byte, error)
|
|
GetServingSystem(context.Context) (*qmi.ServingSystem, error)
|
|
AttachDetach(context.Context, bool) error
|
|
OpenLogicalChannel(context.Context, uint8, []byte) (byte, error)
|
|
CloseLogicalChannel(context.Context, uint8, uint8) error
|
|
SendAPDU(context.Context, uint8, uint8, []byte) ([]byte, error)
|
|
PowerOffSIM(context.Context, uint8) error
|
|
PowerOnSIM(context.Context, uint8) error
|
|
}
|
|
|
|
// nativeQMIControl identifies the QMI control node exposed by native WWAN
|
|
// devices. USB serial modems may also advertise a control path, but only the
|
|
// wwanN/qmiN pairing is safe to operate through the native QMI path.
|
|
func (manager *Manager) nativeQMIControl(id string) (string, bool, error) {
|
|
state, err := manager.lookup(id)
|
|
if err != nil {
|
|
return "", false, err
|
|
}
|
|
candidate := manager.candidateFor(state)
|
|
controlDevice := strings.TrimSpace(candidate.QMIControl)
|
|
deviceID := strings.TrimSpace(candidate.ID)
|
|
if !nativeQMIControlMatches(deviceID, controlDevice) {
|
|
return "", false, nil
|
|
}
|
|
return controlDevice, true, nil
|
|
}
|
|
|
|
type productionQMIRadioSession struct {
|
|
client *qmi.Client
|
|
dms *qmi.DMSService
|
|
nas *qmi.NASService
|
|
nasErr error
|
|
catID uint8
|
|
uimMu sync.Mutex
|
|
uim *qmi.UIMService
|
|
lease *qmiport.Lease
|
|
}
|
|
|
|
// The native WWAN path uses the same QMI NAS client for radio wake-up,
|
|
// operator selection, and registration. Keep these methods optional on the
|
|
// qmiRadioSession interface so the older transcript-backed tests and AT-only
|
|
// devices do not need to grow a fake NAS implementation.
|
|
func (session *productionQMIRadioSession) nasService() (*qmi.NASService, error) {
|
|
if session == nil {
|
|
return nil, errors.New("QMI NAS session is unavailable")
|
|
}
|
|
if session.nas == nil {
|
|
if session.nasErr != nil {
|
|
return nil, session.nasErr
|
|
}
|
|
return nil, errors.New("QMI NAS session is unavailable")
|
|
}
|
|
return session.nas, nil
|
|
}
|
|
|
|
func (session *productionQMIRadioSession) GetServingSystem(ctx context.Context) (*qmi.ServingSystem, error) {
|
|
nas, err := session.nasService()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return nas.GetServingSystem(ctx)
|
|
}
|
|
|
|
func (session *productionQMIRadioSession) GetSystemSelectionPreference(ctx context.Context) (*qmi.SystemSelectionPreference, error) {
|
|
nas, err := session.nasService()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return nas.GetSystemSelectionPreference(ctx)
|
|
}
|
|
|
|
func (session *productionQMIRadioSession) SetSystemSelectionPreference(ctx context.Context, pref qmi.SystemSelectionPreference) error {
|
|
nas, err := session.nasService()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nas.SetSystemSelectionPreference(ctx, pref)
|
|
}
|
|
|
|
func (session *productionQMIRadioSession) InitiateNetworkRegister(ctx context.Context, req qmi.NASInitiateNetworkRegisterRequest) error {
|
|
nas, err := session.nasService()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nas.InitiateNetworkRegister(ctx, req)
|
|
}
|
|
|
|
func (session *productionQMIRadioSession) ForceNetworkSearch(ctx context.Context) error {
|
|
nas, err := session.nasService()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nas.ForceNetworkSearch(ctx)
|
|
}
|
|
|
|
func (session *productionQMIRadioSession) AttachDetach(ctx context.Context, attached bool) error {
|
|
nas, err := session.nasService()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nas.AttachDetach(ctx, attached)
|
|
}
|
|
|
|
// openQMIRadioSession controls native WWAN radios through QMI DMS. OpenStick
|
|
// 410 firmware rejects AT+CFUN=1 even though the equivalent DMS online request
|
|
// is supported, so native WWAN devices must not fall back to the AT path.
|
|
func openQMIRadioSession(ctx context.Context, controlDevice string) (qmiRadioSession, error) {
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
openContext, cancel := context.WithTimeout(ctx, 15*time.Second)
|
|
defer cancel()
|
|
lease, err := qmiport.Acquire(openContext, controlDevice)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
opts := qmi.DefaultClientOptions()
|
|
opts.UseProxy = true
|
|
opts.Logf = func(qmi.ClientLogLevel, string, ...any) {}
|
|
client, err := qmi.NewClientWithOptions(openContext, controlDevice, opts)
|
|
if err != nil {
|
|
lease.Release()
|
|
return nil, err
|
|
}
|
|
dms, err := qmi.NewDMSServiceWithContext(openContext, client)
|
|
if err != nil {
|
|
_ = client.Close()
|
|
lease.Release()
|
|
return nil, err
|
|
}
|
|
// NAS is optional for ordinary radio controls. Some firmware exposes DMS
|
|
// but rejects NAS client allocation; keep radio control usable and report
|
|
// that limitation only to native registration/RF queries.
|
|
nas, nasErr := qmi.NewNASServiceWithContext(openContext, client)
|
|
return &productionQMIRadioSession{
|
|
client: client,
|
|
dms: dms,
|
|
nas: nas,
|
|
nasErr: nasErr,
|
|
lease: lease,
|
|
}, nil
|
|
}
|
|
|
|
func (session *productionQMIRadioSession) GetICCID(ctx context.Context) (string, error) {
|
|
uim, err := session.uimService(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return uim.GetICCID(ctx)
|
|
}
|
|
|
|
func (session *productionQMIRadioSession) GetIMSI(ctx context.Context) (string, error) {
|
|
uim, err := session.uimService(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return uim.GetIMSI(ctx)
|
|
}
|
|
|
|
func (session *productionQMIRadioSession) GetNativeMCCMNC(ctx context.Context) (string, string, error) {
|
|
uim, err := session.uimService(ctx)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
return uim.GetNativeMCCMNC(ctx)
|
|
}
|
|
|
|
func (session *productionQMIRadioSession) GetUSIMAID(ctx context.Context) ([]byte, error) {
|
|
uim, err := session.uimService(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return uim.GetUSIMAID(ctx)
|
|
}
|
|
|
|
func (session *productionQMIRadioSession) GetISIMAID(ctx context.Context) ([]byte, error) {
|
|
uim, err := session.uimService(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return uim.GetISIMAID(ctx)
|
|
}
|
|
|
|
func (session *productionQMIRadioSession) PowerOffSIM(ctx context.Context, slot uint8) error {
|
|
uim, err := session.uimService(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return uim.PowerOffSIM(ctx, slot)
|
|
}
|
|
|
|
func (session *productionQMIRadioSession) PowerOnSIM(ctx context.Context, slot uint8) error {
|
|
uim, err := session.uimService(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return uim.PowerOnSIM(ctx, slot)
|
|
}
|
|
|
|
func (session *productionQMIRadioSession) ResetUIM(ctx context.Context) error {
|
|
uim, err := session.uimService(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return uim.Reset(ctx)
|
|
}
|
|
|
|
func (session *productionQMIRadioSession) uimService(ctx context.Context) (*qmi.UIMService, error) {
|
|
if session == nil || session.client == nil {
|
|
return nil, errors.New("QMI UIM session is unavailable")
|
|
}
|
|
session.uimMu.Lock()
|
|
defer session.uimMu.Unlock()
|
|
if session.uim == nil {
|
|
uim, err := qmi.NewUIMServiceWithContext(ctx, session.client)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
session.uim = uim
|
|
}
|
|
return session.uim, nil
|
|
}
|
|
|
|
func (session *productionQMIRadioSession) OpenLogicalChannel(ctx context.Context, slot uint8, aid []byte) (byte, error) {
|
|
uim, err := session.uimService(ctx)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return uim.OpenLogicalChannel(ctx, slot, aid)
|
|
}
|
|
|
|
func (session *productionQMIRadioSession) CloseLogicalChannel(ctx context.Context, slot, channel uint8) error {
|
|
uim, err := session.uimService(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return uim.CloseLogicalChannel(ctx, slot, channel)
|
|
}
|
|
|
|
func (session *productionQMIRadioSession) SendAPDU(ctx context.Context, slot, channel uint8, command []byte) ([]byte, error) {
|
|
uim, err := session.uimService(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return uim.SendAPDU(ctx, slot, channel, command)
|
|
}
|
|
|
|
// RegisterUIMRefresh mirrors the terminal registration used by libqmi for a
|
|
// physical card slot. EnableProfile(refresh=true) may cause the eUICC to issue
|
|
// a proactive REFRESH; without a registered terminal the card remains CAT busy
|
|
// after the profile has changed and rejects the next profile operation.
|
|
func (session *productionQMIRadioSession) RegisterUIMRefresh(ctx context.Context) error {
|
|
uim, err := session.uimService(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := uim.RefreshRegisterAll(ctx, qmi.UIMRefreshRegisterAllRequest{
|
|
SessionType: qmi.UIMSessionTypeCardSlot1,
|
|
RegisterFlag: true,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
if session.catID == 0 {
|
|
clientID, err := session.client.AllocateClientIDWithContext(ctx, qmi.ServiceCAT2)
|
|
if err != nil {
|
|
return fmt.Errorf("allocate QMI CAT2 client: %w", err)
|
|
}
|
|
session.catID = clientID
|
|
}
|
|
configuration, configErr := session.client.SendRequest(ctx, qmi.ServiceCAT2, session.catID, 0x002E, nil)
|
|
if configErr == nil && configuration.CheckResult() == nil {
|
|
if modeTLV := qmi.FindTLV(configuration.TLVs, 0x10); modeTLV != nil && len(modeTLV.Value) > 0 {
|
|
slog.Info("QMI CAT2 configuration", "mode", modeTLV.Value[0])
|
|
}
|
|
}
|
|
response, err := session.client.SendRequest(ctx, qmi.ServiceCAT2, session.catID, 0x0001, []qmi.TLV{
|
|
// Claim the raw proactive-command events implemented by this CAT2
|
|
// generation (bits 0..22 and 24..25). A profile can leave any STK
|
|
// command pending, not only REFRESH, and SGP.22 forbids profile changes
|
|
// while that proactive session is unanswered.
|
|
{Type: 0x10, Value: []byte{0xFF, 0xFF, 0x7F, 0x03}},
|
|
// Slot mask bit 0 selects slot 1.
|
|
{Type: 0x12, Value: []byte{0x01}},
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("register QMI CAT2 refresh: %w", err)
|
|
}
|
|
if err := response.CheckResult(); err != nil {
|
|
return fmt.Errorf("register QMI CAT2 refresh: %w", err)
|
|
}
|
|
for _, tlv := range response.TLVs {
|
|
if tlv.Type >= 0x10 && tlv.Type <= 0x12 {
|
|
slog.Info("QMI CAT2 registration response", "tlv", fmt.Sprintf("0x%02X", tlv.Type), "value", fmt.Sprintf("%X", tlv.Value))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CompleteUIMRefresh consumes refresh indications on the same QMI client that
|
|
// registered for them. Qualcomm requires RefreshComplete only for START
|
|
// indications whose mode is not RESET; RESET is completed by the modem itself.
|
|
func (session *productionQMIRadioSession) CompleteUIMRefresh(ctx context.Context) error {
|
|
if session == nil || session.client == nil {
|
|
return errors.New("QMI UIM refresh session is unavailable")
|
|
}
|
|
uim, err := session.uimService(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
refreshCompleted := false
|
|
uimEnded := false
|
|
catEnded := false
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
// Some firmware handles a RESET internally and never forwards an
|
|
// indication to this client. A missing indication is therefore not
|
|
// a failed profile commit.
|
|
return nil
|
|
case event, ok := <-session.client.Events():
|
|
if !ok {
|
|
return nil
|
|
}
|
|
if event.ServiceID == qmi.ServiceCAT2 && event.MessageID == 0x0001 {
|
|
for _, eventTLV := range event.Packet.TLVs {
|
|
slog.Info("QMI CAT2 event", "tlv", fmt.Sprintf("0x%02X", eventTLV.Type), "length", len(eventTLV.Value))
|
|
}
|
|
if tlv := qmi.FindTLV(event.Packet.TLVs, 0x19); tlv != nil && len(tlv.Value) >= 4 {
|
|
mode := uint16(tlv.Value[0]) | uint16(tlv.Value[1])<<8
|
|
stage := uint16(tlv.Value[2]) | uint16(tlv.Value[3])<<8
|
|
slog.Info("QMI CAT2 profile refresh", "stage", stage, "mode", mode)
|
|
if stage == 3 {
|
|
return errors.New("QMI CAT2 refresh ended with failure")
|
|
}
|
|
}
|
|
// UIM refresh completion is not a CAT terminal response. Qualcomm
|
|
// delivers the raw proactive command in a command-specific TLV; send
|
|
// a response carrying that command's reference ID. Unsupported UI STK
|
|
// commands receive the standards-defined "beyond terminal
|
|
// capabilities" result, which still closes the proactive session.
|
|
for _, commandTLV := range event.Packet.TLVs {
|
|
if !isRawCATCommandTLV(commandTLV.Type) {
|
|
continue
|
|
}
|
|
ref, terminalResponse, commandType, responseOK := catProactiveTerminalResponse(commandTLV.Value)
|
|
if !responseOK {
|
|
continue
|
|
}
|
|
if err := session.sendCATTerminalResponse(ctx, ref, terminalResponse); err != nil {
|
|
return err
|
|
}
|
|
slog.Info("QMI CAT2 terminal response sent", "reference", ref, "command", fmt.Sprintf("0x%02X", commandType))
|
|
break
|
|
}
|
|
if tlv := qmi.FindTLV(event.Packet.TLVs, 0x1A); tlv != nil && len(tlv.Value) > 0 {
|
|
// Older MDM8916 CAT2 firmware encodes this enum in one byte;
|
|
// newer interface descriptions model it as a 32-bit value.
|
|
reason := uint32(tlv.Value[0])
|
|
if len(tlv.Value) >= 4 {
|
|
reason |= uint32(tlv.Value[1])<<8 | uint32(tlv.Value[2])<<16 | uint32(tlv.Value[3])<<24
|
|
}
|
|
slog.Info("QMI CAT2 proactive session ended", "reason", reason)
|
|
catEnded = true
|
|
if uimEnded {
|
|
return nil
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
if event.Type != qmi.EventUIMRefresh {
|
|
continue
|
|
}
|
|
info, parseErr := qmi.ParseUIMRefreshIndication(event.Packet)
|
|
if parseErr != nil {
|
|
return parseErr
|
|
}
|
|
const (
|
|
refreshStageWaitForOK = uint8(0)
|
|
refreshStageStart = uint8(1)
|
|
refreshStageSuccess = uint8(2)
|
|
refreshStageFailure = uint8(3)
|
|
refreshModeReset = uint8(0)
|
|
)
|
|
slog.Info("QMI UIM profile refresh", "stage", info.Stage, "mode", info.Mode)
|
|
switch info.Stage {
|
|
case refreshStageWaitForOK:
|
|
// Registration without a vote advances on its own. Keep the UIM
|
|
// client alive for the subsequent START and END indications.
|
|
continue
|
|
case refreshStageStart:
|
|
if info.Mode == refreshModeReset || refreshCompleted {
|
|
continue
|
|
}
|
|
// libqmi intentionally uses CARD_SLOT_1 here rather than echoing
|
|
// the provisioning session from the indication.
|
|
_ = uim.RefreshComplete(ctx, qmi.UIMRefreshCompleteRequest{
|
|
SessionType: qmi.UIMSessionTypeCardSlot1,
|
|
RefreshSuccess: true,
|
|
})
|
|
refreshCompleted = true
|
|
continue
|
|
case refreshStageSuccess:
|
|
uimEnded = true
|
|
if catEnded {
|
|
return nil
|
|
}
|
|
continue
|
|
case refreshStageFailure:
|
|
return errors.New("QMI UIM refresh ended with failure")
|
|
default:
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (session *productionQMIRadioSession) sendCATTerminalResponse(ctx context.Context, reference uint32, terminalResponse []byte) error {
|
|
value := make([]byte, 0, 6+len(terminalResponse))
|
|
value = binary.LittleEndian.AppendUint32(value, reference)
|
|
value = binary.LittleEndian.AppendUint16(value, uint16(len(terminalResponse)))
|
|
value = append(value, terminalResponse...)
|
|
response, err := session.client.SendRequest(ctx, qmi.ServiceCAT2, session.catID, 0x0021, []qmi.TLV{
|
|
{Type: 0x01, Value: value},
|
|
{Type: 0x10, Value: []byte{0x01}}, // CAT slot 1 (not a slot mask)
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("send QMI CAT2 refresh terminal response: %w", err)
|
|
}
|
|
if err := response.CheckResult(); err != nil {
|
|
return fmt.Errorf("send QMI CAT2 refresh terminal response: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// catProactiveTerminalResponse extracts a raw CAT command carried as
|
|
// {reference:uint32LE, length:uint16LE, BER-TLV command} and creates the
|
|
// standards-shaped terminal response. VoCat has no interactive STK UI, so
|
|
// commands other than REFRESH/MORE TIME are explicitly reported unsupported.
|
|
func catProactiveTerminalResponse(raw []byte) (uint32, []byte, byte, bool) {
|
|
if len(raw) < 8 {
|
|
return 0, nil, 0, false
|
|
}
|
|
reference := binary.LittleEndian.Uint32(raw[:4])
|
|
commandLength := int(binary.LittleEndian.Uint16(raw[4:6]))
|
|
if commandLength <= 0 || commandLength > len(raw)-6 {
|
|
return 0, nil, 0, false
|
|
}
|
|
command := raw[6 : 6+commandLength]
|
|
if len(command) < 2 || command[0] != 0xD0 {
|
|
return 0, nil, 0, false
|
|
}
|
|
bodyLength, lengthBytes, ok := catBERLength(command[1:])
|
|
if !ok || 1+lengthBytes+bodyLength > len(command) {
|
|
return 0, nil, 0, false
|
|
}
|
|
body := command[1+lengthBytes : 1+lengthBytes+bodyLength]
|
|
for offset := 0; offset < len(body); {
|
|
tag := body[offset]
|
|
offset++
|
|
length, consumed, ok := catBERLength(body[offset:])
|
|
if !ok || offset+consumed+length > len(body) {
|
|
return 0, nil, 0, false
|
|
}
|
|
offset += consumed
|
|
value := body[offset : offset+length]
|
|
offset += length
|
|
if tag&0x7F != 0x01 || len(value) < 3 {
|
|
continue
|
|
}
|
|
result := byte(0x30) // command beyond terminal capabilities
|
|
if value[1] == 0x01 || value[1] == 0x02 { // REFRESH or MORE TIME
|
|
result = 0x00 // command performed successfully
|
|
}
|
|
terminalResponse := []byte{
|
|
0x81, 0x03, value[0], value[1], value[2], // command details
|
|
0x82, 0x02, 0x82, 0x81, // terminal -> UICC
|
|
0x83, 0x01, result,
|
|
}
|
|
return reference, terminalResponse, value[1], true
|
|
}
|
|
return 0, nil, 0, false
|
|
}
|
|
|
|
func catRefreshTerminalResponse(raw []byte) (uint32, []byte, bool) {
|
|
reference, response, commandType, ok := catProactiveTerminalResponse(raw)
|
|
return reference, response, ok && commandType == 0x01
|
|
}
|
|
|
|
func isRawCATCommandTLV(tag byte) bool {
|
|
switch tag {
|
|
case 0x10, 0x11, 0x12, 0x13, 0x14, 0x17, 0x18,
|
|
0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F,
|
|
0x51, 0x52, 0x53, 0x54, 0x66, 0x6A:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func catBERLength(raw []byte) (length int, consumed int, ok bool) {
|
|
if len(raw) == 0 {
|
|
return 0, 0, false
|
|
}
|
|
switch raw[0] {
|
|
case 0x81:
|
|
if len(raw) < 2 {
|
|
return 0, 0, false
|
|
}
|
|
return int(raw[1]), 2, true
|
|
case 0x82:
|
|
if len(raw) < 3 {
|
|
return 0, 0, false
|
|
}
|
|
return int(raw[1])<<8 | int(raw[2]), 3, true
|
|
default:
|
|
if raw[0]&0x80 != 0 {
|
|
return 0, 0, false
|
|
}
|
|
return int(raw[0]), 1, true
|
|
}
|
|
}
|
|
|
|
// AcknowledgeUIMRefresh is a recovery vote for a refresh that predates this
|
|
// QMI client. Qualcomm documents RefreshComplete as harmless when no vote is
|
|
// pending; it lets a newly started service release a stale CAT-busy condition
|
|
// left by an interrupted LPA/terminal transaction.
|
|
func (session *productionQMIRadioSession) AcknowledgeUIMRefresh(ctx context.Context) error {
|
|
uim, err := session.uimService(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return uim.RefreshComplete(ctx, qmi.UIMRefreshCompleteRequest{
|
|
SessionType: qmi.UIMSessionTypeCardSlot1,
|
|
RefreshSuccess: true,
|
|
})
|
|
}
|
|
|
|
func (session *productionQMIRadioSession) GetIMEI(ctx context.Context) (string, error) {
|
|
if session == nil || session.dms == nil {
|
|
return "", errors.New("QMI DMS identity session is unavailable")
|
|
}
|
|
info, err := session.dms.GetDeviceSerialNumbers(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return info.IMEI, nil
|
|
}
|
|
|
|
func (session *productionQMIRadioSession) GetOperatingMode(ctx context.Context) (qmi.OperatingMode, error) {
|
|
return session.dms.GetOperatingMode(ctx)
|
|
}
|
|
|
|
func (session *productionQMIRadioSession) SetOperatingMode(ctx context.Context, mode qmi.OperatingMode) error {
|
|
return session.dms.SetOperatingMode(ctx, mode)
|
|
}
|
|
|
|
func (session *productionQMIRadioSession) Close() error {
|
|
if session == nil {
|
|
return nil
|
|
}
|
|
var closeErrors []error
|
|
session.uimMu.Lock()
|
|
if session.uim != nil {
|
|
closeErrors = append(closeErrors, session.uim.Close())
|
|
session.uim = nil
|
|
}
|
|
session.uimMu.Unlock()
|
|
if session.dms != nil {
|
|
closeErrors = append(closeErrors, session.dms.Close())
|
|
session.dms = nil
|
|
}
|
|
if session.nas != nil {
|
|
closeErrors = append(closeErrors, session.nas.Close())
|
|
session.nas = nil
|
|
}
|
|
if session.client != nil && session.catID != 0 {
|
|
closeErrors = append(closeErrors, session.client.ReleaseClientID(qmi.ServiceCAT2, session.catID))
|
|
session.catID = 0
|
|
}
|
|
if session.client != nil {
|
|
closeErrors = append(closeErrors, session.client.Close())
|
|
session.client = nil
|
|
}
|
|
if session.lease != nil {
|
|
session.lease.Release()
|
|
session.lease = nil
|
|
}
|
|
return errors.Join(closeErrors...)
|
|
}
|
|
|
|
func (manager *Manager) setNativeQMIFlight(
|
|
ctx context.Context,
|
|
id string,
|
|
state *managedDevice,
|
|
enabled bool,
|
|
) (FlightResult, bool, error) {
|
|
controlDevice, native, err := manager.nativeQMIControl(id)
|
|
if err != nil {
|
|
return FlightResult{}, true, err
|
|
}
|
|
if !native {
|
|
return FlightResult{}, false, nil
|
|
}
|
|
if manager.qmiRadioOpener == nil {
|
|
return FlightResult{}, true, errors.New("QMI DMS radio control is unavailable")
|
|
}
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
openContext, cancelOpen := manager.withTimeout(ctx, manager.commandTimeout*5)
|
|
session, err := manager.qmiRadioOpener(openContext, controlDevice)
|
|
cancelOpen()
|
|
if err != nil {
|
|
return FlightResult{}, true, fmt.Errorf("open QMI DMS radio control: %w", err)
|
|
}
|
|
defer session.Close()
|
|
|
|
readContext, cancelRead := manager.withTimeout(ctx, manager.commandTimeout)
|
|
previousQMI, err := session.GetOperatingMode(readContext)
|
|
cancelRead()
|
|
if err != nil {
|
|
return FlightResult{}, true, fmt.Errorf("read QMI operating mode: %w", err)
|
|
}
|
|
previous := qmiModeAsCFUN(previousQMI)
|
|
targetQMI := previousQMI
|
|
if enabled {
|
|
if !isQMIRadioOffMode(previousQMI) {
|
|
targetQMI = qmi.ModeLowPower
|
|
}
|
|
} else if previousQMI != qmi.ModeOnline {
|
|
targetQMI = qmi.ModeOnline
|
|
}
|
|
changed := targetQMI != previousQMI
|
|
if changed {
|
|
setContext, cancelSet := manager.withTimeout(ctx, manager.commandTimeout)
|
|
err = session.SetOperatingMode(setContext, targetQMI)
|
|
cancelSet()
|
|
if err != nil {
|
|
return FlightResult{
|
|
PreviousMode: previous,
|
|
CurrentMode: previous,
|
|
FlightMode: isQMIRadioOffMode(previousQMI),
|
|
RadioOff: isQMIRadioOffMode(previousQMI),
|
|
}, true, fmt.Errorf("set QMI operating mode: %w", err)
|
|
}
|
|
}
|
|
currentQMI, err := manager.waitForQMIRadioState(ctx, session, enabled, targetQMI)
|
|
if err != nil {
|
|
currentRadioOff := isQMIRadioOffMode(currentQMI)
|
|
return FlightResult{
|
|
PreviousMode: previous,
|
|
CurrentMode: qmiModeAsCFUN(currentQMI),
|
|
Changed: changed,
|
|
FlightMode: currentRadioOff,
|
|
RadioOff: currentRadioOff,
|
|
}, true, err
|
|
}
|
|
current := qmiModeAsCFUN(currentQMI)
|
|
currentRadioOff := isQMIRadioOffMode(currentQMI)
|
|
manager.updateSnapshotMode(id, state, current)
|
|
if !enabled && !currentRadioOff {
|
|
// DMS Online is only the radio half of the recovery. Continue with a
|
|
// background NAS registration/PS-attach reconcile after the flight-mode
|
|
// transition without holding the radio QMI session open.
|
|
manager.startNativeQMIRegistrationReconcile(id)
|
|
}
|
|
return FlightResult{
|
|
PreviousMode: previous,
|
|
CurrentMode: current,
|
|
Changed: changed,
|
|
FlightMode: currentRadioOff,
|
|
RadioOff: currentRadioOff,
|
|
}, true, nil
|
|
}
|
|
|
|
func (manager *Manager) waitForQMIRadioState(
|
|
ctx context.Context,
|
|
session qmiRadioSession,
|
|
radioOff bool,
|
|
fallback qmi.OperatingMode,
|
|
) (qmi.OperatingMode, error) {
|
|
verifyTimeout := manager.commandTimeout * 2
|
|
if verifyTimeout < 5*time.Second {
|
|
verifyTimeout = 5 * time.Second
|
|
}
|
|
verifyContext, cancel := manager.withTimeout(ctx, verifyTimeout)
|
|
defer cancel()
|
|
current := fallback
|
|
var lastErr error
|
|
for {
|
|
mode, err := session.GetOperatingMode(verifyContext)
|
|
if err == nil {
|
|
current = mode
|
|
lastErr = nil
|
|
if qmiModeMatchesFlight(mode, radioOff) {
|
|
return mode, nil
|
|
}
|
|
} else {
|
|
lastErr = err
|
|
}
|
|
timer := time.NewTimer(250 * time.Millisecond)
|
|
select {
|
|
case <-verifyContext.Done():
|
|
if !timer.Stop() {
|
|
select {
|
|
case <-timer.C:
|
|
default:
|
|
}
|
|
}
|
|
if lastErr != nil {
|
|
return current, fmt.Errorf("verify QMI operating mode: %w", lastErr)
|
|
}
|
|
return current, fmt.Errorf(
|
|
"QMI operating mode did not reach requested radio state (mode %d): %w",
|
|
current,
|
|
verifyContext.Err(),
|
|
)
|
|
case <-timer.C:
|
|
}
|
|
}
|
|
}
|
|
|
|
func qmiModeMatchesFlight(mode qmi.OperatingMode, radioOff bool) bool {
|
|
if radioOff {
|
|
return isQMIRadioOffMode(mode)
|
|
}
|
|
return mode == qmi.ModeOnline
|
|
}
|
|
|
|
func isQMIRadioOffMode(mode qmi.OperatingMode) bool {
|
|
switch mode {
|
|
case qmi.ModeLowPower, qmi.ModeOffline, qmi.ModeShutdown, qmi.ModePersistLow, qmi.ModeOnlyLowPower:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// FlightResult and Snapshot historically expose AT+CFUN values. Preserve that
|
|
// API contract while sourcing the real radio state from QMI DMS.
|
|
func qmiModeAsCFUN(mode qmi.OperatingMode) int {
|
|
switch mode {
|
|
case qmi.ModeOnline:
|
|
return 1
|
|
case qmi.ModeLowPower, qmi.ModePersistLow:
|
|
return 0
|
|
case qmi.ModeOffline, qmi.ModeShutdown, qmi.ModeOnlyLowPower:
|
|
return 7
|
|
default:
|
|
return 1
|
|
}
|
|
}
|