fix: stabilize OpenStick 410 VoWiFi startup (#74)

* fix: stabilize OpenStick 410 VoWiFi startup

* fix: recover OpenStick 410 eUICC channel allocation

---------

Co-authored-by: MengMengCode <[email protected]>
This commit is contained in:
Rulio
2026-08-21 19:26:37 +08:00
committed by GitHub
co-authored by MengMengCode
parent 06ea65558c
commit 76af0784e1
5 changed files with 129 additions and 4 deletions
+63 -1
View File
@@ -684,7 +684,18 @@ func configureVoWiFiRuntime(
)
}
}
if _, err := manager.RequestEnabled(deviceConfig.ID, true); err != nil {
requestEnable := func() error {
_, requestErr := manager.RequestEnabled(deviceConfig.ID, true)
return requestErr
}
if err := requestVoWiFiStartup(
ctx,
logger,
deviceConfig.DeviceType,
deviceConfig.ID,
wifi410VoWiFiStartupDelay,
requestEnable,
); err != nil {
_ = manager.Close(context.Background())
return nil, fmt.Errorf("start device %q VoWiFi policy: %w", deviceConfig.ID, err)
}
@@ -696,8 +707,55 @@ func configureVoWiFiRuntime(
const (
vowifiStartupRadioAttempts = 3
vowifiStartupRadioDelay = time.Second
wifi410VoWiFiStartupDelay = 80 * time.Second
)
// requestVoWiFiStartup delays only the persisted startup policy for OpenStick
// 410 devices. Their Qualcomm UIM and Vodafone ePDG path need a short quiet
// period after a cold boot; user-triggered reconnects and every other device
// type continue to execute immediately.
func requestVoWiFiStartup(
ctx context.Context,
logger *slog.Logger,
deviceType string,
deviceID string,
delay time.Duration,
request func() error,
) error {
if deviceType != store.DeviceTypeWiFi410 || delay <= 0 {
return request()
}
if logger == nil {
logger = slog.Default()
}
logger.Info(
"OpenStick 410 VoWiFi startup delayed",
"device_id", deviceID,
"delay", delay,
)
go func() {
timer := time.NewTimer(delay)
defer timer.Stop()
select {
case <-ctx.Done():
return
case <-timer.C:
}
if err := request(); err != nil {
logger.Warn(
"OpenStick 410 delayed VoWiFi startup failed",
"device_id", deviceID,
"error", err,
)
}
}()
return nil
}
func shouldDelayWiFi410VoWiFi(deviceType string, now, notBefore time.Time) bool {
return deviceType == store.DeviceTypeWiFi410 && now.Before(notBefore)
}
type flightModeSetter interface {
SetFlight(context.Context, string, bool) (device.FlightResult, error)
}
@@ -1136,6 +1194,7 @@ func reconcileCardPolicies(
vowifiManager *vowifiruntime.Manager,
) {
observedCards := make(map[string]string)
wifi410StartupNotBefore := time.Now().Add(wifi410VoWiFiStartupDelay)
reconcile := func() {
policies, policyListErr := database.ListCardPolicies(ctx)
if policyListErr == nil {
@@ -1217,6 +1276,9 @@ func reconcileCardPolicies(
}
switch {
case stateErr != nil || !state.Enabled:
if shouldDelayWiFi410VoWiFi(config.DeviceType, time.Now(), wifi410StartupNotBefore) {
continue
}
_, _ = vowifiManager.RequestEnabled(config.ID, true)
case state.ICCID != "" && !strings.EqualFold(strings.TrimSpace(state.ICCID), iccid):
_, _ = vowifiManager.RequestReconnect(config.ID)
+3
View File
@@ -439,6 +439,9 @@ func (manager *Manager) openQMIEuiccOnceAID(ctx context.Context, id string, cand
}
const slot uint8 = 1
logicalChannel, err := session.OpenLogicalChannel(openContext, slot, aid)
if recoverySession, recoveryOK := session.(nativeQMIChannelRecoverySession); recoveryOK && isQMIInsufficientResources(err) {
logicalChannel, err = openNativeQMIChannelWithRecovery(openContext, recoverySession, slot, aid)
}
if err != nil {
_ = session.Close()
return nil, fmt.Errorf("%w: %v", errNoEUICC, err)
+57 -1
View File
@@ -6,6 +6,8 @@ import (
"fmt"
"strings"
"time"
"github.com/iniwex5/quectel-qmi-go/pkg/qmi"
)
func (manager *Manager) withNativeQMIVoWiFiSession(ctx context.Context, id string, fn func(nativeQMIVoWiFiSession) error) error {
@@ -70,7 +72,7 @@ func (manager *Manager) ProbeNativeQMIApplication(ctx context.Context, id, prefe
func (manager *Manager) AuthenticateNativeQMI(ctx context.Context, id string, aid, apdu []byte) (response []byte, err error) {
err = manager.withNativeQMIVoWiFiSession(ctx, id, func(session nativeQMIVoWiFiSession) error {
channel, openErr := session.OpenLogicalChannel(ctx, 1, aid)
channel, openErr := openNativeQMIChannelWithRecovery(ctx, session, 1, aid)
if openErr != nil {
return fmt.Errorf("open QMI UIM logical channel: %w", openErr)
}
@@ -102,6 +104,60 @@ func (manager *Manager) AuthenticateNativeQMI(ctx context.Context, id string, ai
return
}
type nativeQMIChannelRecoverySession interface {
OpenLogicalChannel(context.Context, uint8, []byte) (byte, error)
PowerOffSIM(context.Context, uint8) error
PowerOnSIM(context.Context, uint8) error
}
// OpenStick 410 can leave the physical UICC powered but unable to allocate a
// logical channel after a SIM hot-swap. A UIM service reset alone does not
// clear that state; cycling the affected physical slot does. Recover only the
// precise QMI InsufficientResources response, then retry the original AID once.
func openNativeQMIChannelWithRecovery(
ctx context.Context,
session nativeQMIChannelRecoverySession,
slot uint8,
aid []byte,
) (byte, error) {
channel, err := session.OpenLogicalChannel(ctx, slot, aid)
if err == nil || !isQMIInsufficientResources(err) {
return channel, err
}
if resetter, ok := session.(nativeQMIUIMResetSession); ok {
_ = resetter.ResetUIM(ctx)
}
if powerErr := session.PowerOffSIM(ctx, slot); powerErr != nil {
return 0, errors.Join(err, fmt.Errorf("power off QMI UIM slot %d: %w", slot, powerErr))
}
if waitErr := waitNativeQMIRecovery(ctx, 3*time.Second); waitErr != nil {
return 0, errors.Join(err, waitErr)
}
if powerErr := session.PowerOnSIM(ctx, slot); powerErr != nil {
return 0, errors.Join(err, fmt.Errorf("power on QMI UIM slot %d: %w", slot, powerErr))
}
if waitErr := waitNativeQMIRecovery(ctx, 5*time.Second); waitErr != nil {
return 0, errors.Join(err, waitErr)
}
return session.OpenLogicalChannel(ctx, slot, aid)
}
func isQMIInsufficientResources(err error) bool {
qmiErr := qmi.GetQMIError(err)
return qmiErr != nil && qmiErr.Service == qmi.ServiceUIM && qmiErr.ErrorCode == 0x0044
}
var waitNativeQMIRecovery = func(ctx context.Context, delay time.Duration) error {
timer := time.NewTimer(delay)
defer timer.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
return nil
}
}
func (manager *Manager) NativeQMIRadioSnapshot(ctx context.Context, id string) (mode int, psAttached bool, err error) {
err = manager.withNativeQMIVoWiFiSession(ctx, id, func(session nativeQMIVoWiFiSession) error {
qmiMode, modeErr := session.GetOperatingMode(ctx)
+5 -1
View File
@@ -36,8 +36,12 @@ func resolveEPDG(ctx context.Context, resolver *net.Resolver, host string) ([]ne
var systemErr error
for _, targetHost := range hostsToTry {
addresses, err := resolver.LookupIPAddr(ctx, targetHost)
ips, err := resolver.LookupIP(ctx, "ip4", targetHost)
if err == nil {
addresses := make([]net.IPAddr, 0, len(ips))
for _, ip := range ips {
addresses = append(addresses, net.IPAddr{IP: ip})
}
valid := filterValidPublicEPDGAddresses(addresses)
if len(valid) > 0 {
return valid, nil
+1 -1
View File
@@ -144,7 +144,7 @@ func (adapter *NativeQMIAdapter) AuthenticateWithPreference(ctx context.Context,
}
raw, err := adapter.controller.AuthenticateNativeQMI(ctx, binding.deviceID, binding.aid, buildUSIMAuthenticateAPDU(challenge))
if err != nil {
return AKAResult{}, ErrEC20AKACommand
return AKAResult{}, fmt.Errorf("%w: %v", ErrEC20AKACommand, err)
}
return parseUSIMAuthenticateResponse(raw)
}