fix: add reconnect retry for unexpected disconnects during auth/write

Instead of immediately failing on disconnect during authenticating or
writing states, retry up to 2 times with backoff. Resets passwordIndex
on reconnect so re-auth starts fresh (fixes issue where burned password
attempts caused retry failures). Also fixes passwordIndex reset in the
device-info safety-net reconnect path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Schwifty 2026-03-21 23:54:13 +00:00
parent e9b668cb20
commit df6601c50b

View file

@ -179,8 +179,10 @@ class BeaconProvisioner: NSObject, ObservableObject {
// Connection retry state // Connection retry state
private var connectionRetryCount = 0 private var connectionRetryCount = 0
private var deviceInfoRetryCount = 0 private var deviceInfoRetryCount = 0
private var disconnectRetryCount = 0
private static let MAX_CONNECTION_RETRIES = 3 private static let MAX_CONNECTION_RETRIES = 3
private static let MAX_DEVICE_INFO_RETRIES = 2 private static let MAX_DEVICE_INFO_RETRIES = 2
private static let MAX_DISCONNECT_RETRIES = 2
private var currentBeacon: DiscoveredBeacon? private var currentBeacon: DiscoveredBeacon?
override init() { override init() {
@ -222,6 +224,7 @@ class BeaconProvisioner: NSObject, ObservableObject {
self.isTerminating = false self.isTerminating = false
self.connectionRetryCount = 0 self.connectionRetryCount = 0
self.deviceInfoRetryCount = 0 self.deviceInfoRetryCount = 0
self.disconnectRetryCount = 0
self.currentBeacon = beacon self.currentBeacon = beacon
state = .connecting state = .connecting
@ -270,6 +273,7 @@ class BeaconProvisioner: NSObject, ObservableObject {
self.allDiscoveredServices.removeAll() self.allDiscoveredServices.removeAll()
self.connectionRetryCount = 0 self.connectionRetryCount = 0
self.deviceInfoRetryCount = 0 self.deviceInfoRetryCount = 0
self.disconnectRetryCount = 0
self.isTerminating = false self.isTerminating = false
self.currentBeacon = beacon self.currentBeacon = beacon
self.servicesToExplore.removeAll() self.servicesToExplore.removeAll()
@ -308,6 +312,7 @@ class BeaconProvisioner: NSObject, ObservableObject {
isTerminating = false isTerminating = false
connectionRetryCount = 0 connectionRetryCount = 0
deviceInfoRetryCount = 0 deviceInfoRetryCount = 0
disconnectRetryCount = 0
currentBeacon = nil currentBeacon = nil
state = .idle state = .idle
progress = "" progress = ""
@ -888,6 +893,7 @@ class BeaconProvisioner: NSObject, ObservableObject {
characteristics.removeAll() characteristics.removeAll()
connectionRetryCount = 0 connectionRetryCount = 0
deviceInfoRetryCount = 0 deviceInfoRetryCount = 0
disconnectRetryCount = 0
currentBeacon = nil currentBeacon = nil
operationMode = .provisioning operationMode = .provisioning
state = .idle state = .idle
@ -1019,6 +1025,7 @@ extension BeaconProvisioner: CBCentralManagerDelegate {
dxSmartNotifySubscribed = false dxSmartNotifySubscribed = false
dxSmartCommandQueue.removeAll() dxSmartCommandQueue.removeAll()
dxSmartWriteIndex = 0 dxSmartWriteIndex = 0
passwordIndex = 0
characteristics.removeAll() characteristics.removeAll()
responseBuffer.removeAll() responseBuffer.removeAll()
state = .connecting state = .connecting
@ -1034,8 +1041,36 @@ extension BeaconProvisioner: CBCentralManagerDelegate {
return return
} }
// All other disconnects are unexpected // Unexpected disconnect during auth or writing retry with full reconnect
DebugLog.shared.log("BLE: UNEXPECTED disconnect — state=\(state) writeIdx=\(dxSmartWriteIndex) queueCount=\(dxSmartCommandQueue.count) authenticated=\(dxSmartAuthenticated)") if (state == .authenticating || state == .writing) && disconnectRetryCount < BeaconProvisioner.MAX_DISCONNECT_RETRIES {
disconnectRetryCount += 1
DebugLog.shared.log("BLE: Disconnect during \(state) — reconnecting (attempt \(disconnectRetryCount)/\(BeaconProvisioner.MAX_DISCONNECT_RETRIES))")
progress = "Beacon disconnected, reconnecting (\(disconnectRetryCount)/\(BeaconProvisioner.MAX_DISCONNECT_RETRIES))..."
// Reset connection state for clean reconnect
dxSmartAuthenticated = false
dxSmartNotifySubscribed = false
dxSmartCommandQueue.removeAll()
dxSmartWriteIndex = 0
passwordIndex = 0
characteristics.removeAll()
responseBuffer.removeAll()
state = .connecting
let delay = Double(disconnectRetryCount) + 1.0 // 2s, 3s backoff
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 }
let resolvedPeripheral = self.resolvePeripheral(beacon)
self.peripheral = resolvedPeripheral
resolvedPeripheral.delegate = self
self.centralManager.connect(resolvedPeripheral, options: nil)
}
return
}
// 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("Unexpected disconnect (state: \(state))", code: .disconnected)
} }
} }