diff --git a/PayfritBeacon/BeaconProvisioner.swift b/PayfritBeacon/BeaconProvisioner.swift index a36b34a..6d5464c 100644 --- a/PayfritBeacon/BeaconProvisioner.swift +++ b/PayfritBeacon/BeaconProvisioner.swift @@ -192,6 +192,8 @@ class BeaconProvisioner: NSObject, ObservableObject { private var writeTimeoutTimer: DispatchWorkItem? private var charRediscoveryCount = 0 private static let MAX_CHAR_REDISCOVERY = 2 + private var charRediscoveryTimer: DispatchWorkItem? + private static let CHAR_REDISCOVERY_TIMEOUT: Double = 5.0 private static let WRITE_TIMEOUT_SECONDS: Double = 5.0 private var writeRetryCount = 0 private static let MAX_WRITE_RETRIES = 1 // Retry once per command before failing @@ -331,6 +333,7 @@ class BeaconProvisioner: NSObject, ObservableObject { private func cleanup() { cancelWriteTimeout() cancelResponseGateTimeout() + cancelCharRediscoveryTimeout() awaitingCommandResponse = false peripheral = nil config = nil @@ -594,6 +597,7 @@ class BeaconProvisioner: NSObject, ObservableObject { DebugLog.shared.log("BLE: FFE2 missing — re-discovering characteristics (attempt \(charRediscoveryCount)/\(BeaconProvisioner.MAX_CHAR_REDISCOVERY))") progress = "Re-discovering characteristics..." state = .discoveringServices + scheduleCharRediscoveryTimeout() DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in self?.peripheral?.discoverCharacteristics([ BeaconProvisioner.DXSMART_NOTIFY_CHAR, @@ -666,6 +670,42 @@ class BeaconProvisioner: NSObject, ObservableObject { writeTimeoutTimer = nil } + /// Schedule a timeout for characteristic rediscovery. + /// If didDiscoverCharacteristicsFor doesn't fire within 5 seconds, + /// either retry or fail instead of hanging forever. + private func scheduleCharRediscoveryTimeout() { + cancelCharRediscoveryTimeout() + let timer = DispatchWorkItem { [weak self] in + guard let self = self else { return } + guard self.state == .discoveringServices else { return } + + let attempt = self.charRediscoveryCount + DebugLog.shared.log("BLE: Characteristic rediscovery timeout (attempt \(attempt)/\(BeaconProvisioner.MAX_CHAR_REDISCOVERY))") + + if attempt < BeaconProvisioner.MAX_CHAR_REDISCOVERY, let service = self.configService { + // Try another rediscovery attempt + self.charRediscoveryCount += 1 + DebugLog.shared.log("BLE: Retrying characteristic rediscovery (attempt \(self.charRediscoveryCount)/\(BeaconProvisioner.MAX_CHAR_REDISCOVERY))") + self.scheduleCharRediscoveryTimeout() + self.peripheral?.discoverCharacteristics([ + BeaconProvisioner.DXSMART_NOTIFY_CHAR, + BeaconProvisioner.DXSMART_COMMAND_CHAR, + BeaconProvisioner.DXSMART_PASSWORD_CHAR + ], for: service) + } else { + self.fail("Characteristic rediscovery timed out after \(attempt) attempts", code: .timeout) + } + } + charRediscoveryTimer = timer + DispatchQueue.main.asyncAfter(deadline: .now() + BeaconProvisioner.CHAR_REDISCOVERY_TIMEOUT, execute: timer) + } + + /// Cancel any pending characteristic rediscovery timeout + private func cancelCharRediscoveryTimeout() { + charRediscoveryTimer?.cancel() + charRediscoveryTimer = nil + } + /// Calculate adaptive delay after writing a command. /// Frame selection (0x11-0x16) and type commands (0x61, 0x62) trigger internal /// state changes on the beacon MCU that need extra processing time. @@ -1368,6 +1408,7 @@ extension BeaconProvisioner: CBPeripheralDelegate { exploreNextService() } else if charRediscoveryCount > 0 && dxSmartAuthenticated && !dxSmartCommandQueue.isEmpty { // Rediscovery during active write — resume writing directly (already authenticated) + cancelCharRediscoveryTimeout() DebugLog.shared.log("BLE: Characteristics re-discovered after FFE2 miss — resuming write") state = .writing DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in