From cb3e8107fcec0ffc2071220d5cea71feeefcc039 Mon Sep 17 00:00:00 2001 From: Schwifty Date: Sun, 22 Mar 2026 02:38:36 +0000 Subject: [PATCH] fix: show live provisioning progress + improve disconnect resilience - Wire provisioner.progress into assignment sheet UI (was showing static "Provisioning beacon..." instead of live updates like "Connecting...", "Authenticating...", "Writing config 1/24...") - Increase disconnect retries from 2 to 3 with longer backoff (3s/4s/5s) - Cancel write timeout timer on disconnect to prevent double-failure - Bump global timeout from 30s to 45s for extra retry headroom - Improve error message with actionable guidance ("move closer and retry") --- PayfritBeacon/BeaconProvisioner.swift | 13 ++++++++----- PayfritBeacon/ScanView.swift | 4 ++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/PayfritBeacon/BeaconProvisioner.swift b/PayfritBeacon/BeaconProvisioner.swift index cb23fd7..1a3731d 100644 --- a/PayfritBeacon/BeaconProvisioner.swift +++ b/PayfritBeacon/BeaconProvisioner.swift @@ -182,7 +182,7 @@ class BeaconProvisioner: NSObject, ObservableObject { private var disconnectRetryCount = 0 private static let MAX_CONNECTION_RETRIES = 3 private static let MAX_DEVICE_INFO_RETRIES = 2 - private static let MAX_DISCONNECT_RETRIES = 2 + private static let MAX_DISCONNECT_RETRIES = 3 private var currentBeacon: DiscoveredBeacon? // Per-write timeout (matches Android's 5-second per-write timeout) @@ -241,8 +241,8 @@ class BeaconProvisioner: NSObject, ObservableObject { centralManager.connect(resolvedPeripheral, options: nil) - // Timeout after 30 seconds - DispatchQueue.main.asyncAfter(deadline: .now() + 30) { [weak self] in + // Timeout after 45 seconds (increased from 30s to accommodate 3 disconnect retries with backoff) + DispatchQueue.main.asyncAfter(deadline: .now() + 45) { [weak self] in if self?.state != .success && self?.state != .idle { self?.fail("Connection timeout", code: .connectionTimeout) } @@ -1114,6 +1114,9 @@ extension BeaconProvisioner: CBCentralManagerDelegate { return } + // Cancel any pending write timeout — disconnect supersedes it + cancelWriteTimeout() + // Unexpected disconnect during any active provisioning phase — retry with full reconnect let isActivePhase = (state == .discoveringServices || state == .authenticating || state == .writing || state == .verifying) if isActivePhase && disconnectRetryCount < BeaconProvisioner.MAX_DISCONNECT_RETRIES { @@ -1131,7 +1134,7 @@ extension BeaconProvisioner: CBCentralManagerDelegate { responseBuffer.removeAll() state = .connecting - let delay = Double(disconnectRetryCount) + 1.0 // 2s, 3s backoff + let delay = Double(disconnectRetryCount) + 2.0 // 3s, 4s, 5s backoff — give BLE time to settle DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in guard let self = self, let beacon = self.currentBeacon else { return } guard self.state == .connecting else { return } @@ -1145,7 +1148,7 @@ extension BeaconProvisioner: CBCentralManagerDelegate { // All retries exhausted or disconnect in unexpected state — fail DebugLog.shared.log("BLE: UNEXPECTED disconnect — state=\(state) writeIdx=\(dxSmartWriteIndex) queueCount=\(dxSmartCommandQueue.count) authenticated=\(dxSmartAuthenticated) disconnectRetries=\(disconnectRetryCount)") - fail("Unexpected disconnect (state: \(state))", code: .disconnected) + fail("Beacon disconnected \(disconnectRetryCount + 1) times during \(state). Move closer to the beacon and try again.", code: .disconnected) } } diff --git a/PayfritBeacon/ScanView.swift b/PayfritBeacon/ScanView.swift index 09df0c9..16fb357 100644 --- a/PayfritBeacon/ScanView.swift +++ b/PayfritBeacon/ScanView.swift @@ -385,11 +385,11 @@ struct ScanView: View { .font(.title3) } - // Provisioning progress + // Provisioning progress — show live updates from provisioner if isProvisioning { HStack { ProgressView() - Text(provisioningProgress) + Text(provisioner.progress.isEmpty ? provisioningProgress : provisioner.progress) .font(.callout) } .padding() -- 2.43.0