diff --git a/PayfritBeacon/BeaconProvisioner.swift b/PayfritBeacon/BeaconProvisioner.swift index a2b1026..35100d5 100644 --- a/PayfritBeacon/BeaconProvisioner.swift +++ b/PayfritBeacon/BeaconProvisioner.swift @@ -157,6 +157,7 @@ class BeaconProvisioner: NSObject, ObservableObject { private var dxSmartWriteIndex = 0 private var provisioningMacAddress: String? private var awaitingDeviceInfoForProvisioning = false + private var skipDeviceInfoRead = false // set after disconnect during device info — skip MAC read on reconnect private var isTerminating = false // guards against re-entrant disconnect handling // Read config mode @@ -215,6 +216,7 @@ class BeaconProvisioner: NSObject, ObservableObject { self.dxSmartWriteIndex = 0 self.provisioningMacAddress = nil self.awaitingDeviceInfoForProvisioning = false + self.skipDeviceInfoRead = false self.isTerminating = false self.connectionRetryCount = 0 self.currentBeacon = beacon @@ -298,6 +300,7 @@ class BeaconProvisioner: NSObject, ObservableObject { dxSmartWriteIndex = 0 provisioningMacAddress = nil awaitingDeviceInfoForProvisioning = false + skipDeviceInfoRead = false isTerminating = false connectionRetryCount = 0 currentBeacon = nil @@ -406,6 +409,14 @@ class BeaconProvisioner: NSObject, ObservableObject { /// Read device info (MAC address) before writing config private func dxSmartReadDeviceInfoBeforeWrite() { + // If we previously disconnected during device info read, skip it entirely + if skipDeviceInfoRead { + DebugLog.shared.log("BLE: Skipping device info read (reconnect after previous disconnect)") + skipDeviceInfoRead = false + dxSmartWriteConfig() + return + } + guard let commandChar = characteristics[BeaconProvisioner.DXSMART_COMMAND_CHAR] else { DebugLog.shared.log("BLE: FFE2 not found, proceeding without MAC") dxSmartWriteConfig() @@ -1012,16 +1023,43 @@ extension BeaconProvisioner: CBCentralManagerDelegate { return } - // Disconnect during device info read (post-auth, pre-write) — beacon may have - // dropped the connection during the MAC address query. We authenticated but - // lost connection before writing config, so this is a failure — but a known - // one with a clear retry path, not an unexplained "Unexpected disconnect". + // Disconnect during device info read (post-auth, pre-write) — beacon dropped + // connection during the optional MAC address query. Instead of failing, reconnect + // and skip the device info step (MAC is nice-to-have, not required). if state == .authenticating && awaitingDeviceInfoForProvisioning && dxSmartAuthenticated { - DebugLog.shared.log("BLE: Disconnect during device info read (post-auth) — connection lost before config write, failing with retry prompt") awaitingDeviceInfoForProvisioning = false - // Connection lost — can't write config without it, fail with specific message - fail("Disconnected after auth during device info read — please retry", code: .disconnected) - return + skipDeviceInfoRead = true // on reconnect, go straight to config write + + if connectionRetryCount < BeaconProvisioner.MAX_CONNECTION_RETRIES { + connectionRetryCount += 1 + let delay = Double(connectionRetryCount) + progress = "Reconnecting (skip MAC read)..." + DebugLog.shared.log("BLE: Disconnect during device info read — reconnecting (\(connectionRetryCount)/\(BeaconProvisioner.MAX_CONNECTION_RETRIES)), will skip MAC read") + + // Reset BLE state for reconnect + dxSmartAuthenticated = false + dxSmartNotifySubscribed = false + dxSmartCommandQueue.removeAll() + dxSmartWriteIndex = 0 + characteristics.removeAll() + responseBuffer.removeAll() + state = .connecting + + 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 + } else { + DebugLog.shared.log("BLE: Disconnect during device info read — max retries exhausted") + fail("Disconnected while reading device information (retries exhausted)", code: .disconnected) + return + } } // All other disconnects are unexpected