diff --git a/internal/device/esim.go b/internal/device/esim.go index 71990b3..d4c434e 100644 --- a/internal/device/esim.go +++ b/internal/device/esim.go @@ -1071,15 +1071,14 @@ func (manager *Manager) renameCachedProfile(id, iccid, nickname string) { manager.esimCacheMu.Unlock() } -// recoverAfterProfileSwitch owns the post-commit reset independently of the -// initiating HTTP request. EC20 commonly drops the AT port while processing -// CFUN=1,1, so the reset error is intentionally followed by discovery retries. +// recoverAfterProfileSwitch owns the post-commit SIM reset independently of the +// initiating HTTP request. func (manager *Manager) recoverAfterProfileSwitch(id string) { resetContext, cancelReset := context.WithTimeout(context.Background(), manager.longTimeout) if native, err := manager.powerCycleNativeQMISIM(resetContext, id); native { cancelReset() if err == nil { - time.Sleep(1500 * time.Millisecond) + time.Sleep(1 * time.Second) } // Native WWAN identity and profile verification are both QMI-backed. // Do not enter the AT refresh path: OpenStick firmware can accept the @@ -1088,52 +1087,39 @@ func (manager *Manager) recoverAfterProfileSwitch(id string) { } cancelReset() if !manager.isPCSCDevice(id) { - resetContext, cancelReset := context.WithTimeout(context.Background(), manager.longTimeout) - _ = manager.rebootForProfileSwitch(resetContext, id) + resetContext, cancelReset := context.WithTimeout(context.Background(), manager.commandTimeout*2) + _ = manager.softResetForProfileSwitch(resetContext, id) cancelReset() } manager.refreshAfterProfileSwitch(id) } // refreshAfterProfileSwitch repopulates the device snapshot in the background -// after an eSIM profile switch + modem reboot. /overview only serves the cached -// snapshot, and nothing else live-reads post-switch, so without this the card -// stays on "--" forever. The EC20 takes ~10-15s to come back from AT+CFUN=1,1, -// so we delay first, then retry with backoff. Transport errors during the -// reboot window are fine — Fix 1 discards the poisoned client and reopens on -// the next attempt. All errors are swallowed: this is best-effort self-healing -// and setResult already records the last failure for the UI. +// after an eSIM profile switch. func (manager *Manager) refreshAfterProfileSwitch(id string) { if manager.isPCSCDevice(id) { - time.Sleep(750 * time.Millisecond) - for attempt := 0; attempt < 10; attempt++ { - ctx, cancel := context.WithTimeout(context.Background(), manager.commandTimeout*4) + time.Sleep(500 * time.Millisecond) + for attempt := 0; attempt < 5; attempt++ { + ctx, cancel := context.WithTimeout(context.Background(), manager.commandTimeout*2) _, _ = manager.Discover(ctx) _, err := manager.Refresh(ctx, id) cancel() if err == nil { return } - time.Sleep(time.Second) + time.Sleep(500 * time.Millisecond) } return } const ( - settle = 8 * time.Second - interval = 4 * time.Second - attempts = 6 + settle = 1 * time.Second + interval = 1 * time.Second + attempts = 5 ) time.Sleep(settle) for attempt := 0; attempt < attempts; attempt++ { - ctx, cancel := context.WithTimeout(context.Background(), manager.commandTimeout*4) - _, _ = manager.Discover(ctx) - _, flightErr := manager.SetFlight(ctx, id, true) - var err error - if flightErr == nil { - _, err = manager.Refresh(ctx, id) - } else { - err = flightErr - } + ctx, cancel := context.WithTimeout(context.Background(), manager.commandTimeout*2) + _, err := manager.Refresh(ctx, id) cancel() if err == nil { return @@ -1233,7 +1219,7 @@ func (manager *Manager) canVerifyProfileSwitchWithoutRestart(id string) bool { // is finalized by REFRESH/reset. The UI must not report success until the modem // is actually exposing the requested ICCID. func (manager *Manager) verifySwitchedICCID(ctx context.Context, id, expected string) error { - return manager.verifySwitchedICCIDAttempts(ctx, id, expected, 6, 2*time.Second) + return manager.verifySwitchedICCIDAttempts(ctx, id, expected, 6, 1*time.Second) } func (manager *Manager) verifySwitchedICCIDAttempts( diff --git a/internal/device/manager.go b/internal/device/manager.go index 4aa0088..dd02b11 100644 --- a/internal/device/manager.go +++ b/internal/device/manager.go @@ -631,13 +631,11 @@ func (manager *Manager) Reboot(ctx context.Context, id string) error { return err } -// rebootForProfileSwitch is the post-EnableProfile modem reset. After the eUICC -// marks a new profile active, the modem keeps the old SIM cached and lands in -// SIM failure (-CME 13) until it is bounced. ESIMSwitchProfile has already -// released opMu by the time it calls this, so the reset is safe to take the -// lock. This mirrors Reboot but is separate so the call site can't recurse into -// a guarded-reset path. -func (manager *Manager) rebootForProfileSwitch(ctx context.Context, id string) error { +// softResetForProfileSwitch resets the baseband SIM stack using a soft CFUN sequence +// (AT+CFUN=0 -> AT+CFUN=1/4) instead of rebooting the entire hardware module (AT+CFUN=1,1). +// This causes the baseband to reload the new eSIM profile files within ~1-2 seconds +// without disconnecting USB/PCIe or dropping serial communication ports. +func (manager *Manager) softResetForProfileSwitch(ctx context.Context, id string) error { state, err := manager.lookup(id) if err != nil { return err @@ -652,14 +650,25 @@ func (manager *Manager) rebootForProfileSwitch(ctx context.Context, id string) e manager.setResult(id, state, nil, err) return err } - commandCtx, cancel := manager.withTimeout(ctx, manager.longTimeout) + commandCtx, cancel := manager.withTimeout(ctx, manager.commandTimeout) defer cancel() - _, err = client.Execute(commandCtx, "AT+CFUN=1,1") - if closeErr := client.Close(); err == nil { - err = closeErr + + // 1. Cycle SIM interface to minimum functionality / clear cached SIM files + _, _ = client.Execute(commandCtx, "AT+CFUN=0") + + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(500 * time.Millisecond): } - state.client = nil - state.preFlightMode = nil + + // 2. Restore radio to trigger fresh USIM file reading + targetCFUN := "AT+CFUN=1" + if state.snapshot != nil && state.snapshot.FlightMode { + targetCFUN = "AT+CFUN=4" + } + _, err = client.Execute(commandCtx, targetCFUN) + manager.clearSnapshot(id, state) manager.setResult(id, state, nil, err) return err diff --git a/internal/vowifi/ims/sms_runtime.go b/internal/vowifi/ims/sms_runtime.go index a5b223a..e2b6f31 100644 --- a/internal/vowifi/ims/sms_runtime.go +++ b/internal/vowifi/ims/sms_runtime.go @@ -56,7 +56,6 @@ type ReceivedSMS struct { RawTPDU string DecodeError string } - // ReceivedSMSStatus is network delivery evidence for one submitted SMS part. type ReceivedSMSStatus struct { DeviceID string