diff --git a/PayfritBeacon/BeaconProvisioner.swift b/PayfritBeacon/BeaconProvisioner.swift index 89e8bd8..420a846 100644 --- a/PayfritBeacon/BeaconProvisioner.swift +++ b/PayfritBeacon/BeaconProvisioner.swift @@ -966,20 +966,22 @@ extension BeaconProvisioner: CBCentralManagerDelegate { } func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { - DebugLog.shared.log("BLE: Disconnected from \(peripheral.name ?? "unknown")") + DebugLog.shared.log("BLE: Disconnected from \(peripheral.name ?? "unknown") | state=\(state) mode=\(operationMode) writeIdx=\(dxSmartWriteIndex) queueCount=\(dxSmartCommandQueue.count) error=\(error?.localizedDescription ?? "none")") if operationMode == .readingConfig { if state != .success && state != .idle { finishRead() } } else if state != .success && state != .idle { if case .failed = state { - // Already failed - } else if state == .writing && dxSmartWriteIndex >= dxSmartCommandQueue.count - 1 { + // Already failed — disconnect is just cleanup + DebugLog.shared.log("BLE: Disconnect after failure, ignoring") + } else if state == .writing && dxSmartCommandQueue.count > 0 && dxSmartWriteIndex >= dxSmartCommandQueue.count - 1 { // SaveConfig (last command) was sent — beacon rebooted to apply config // This is expected behavior, treat as success - DebugLog.shared.log("BLE: Disconnect after SaveConfig — treating as success") + DebugLog.shared.log("BLE: Disconnect after SaveConfig (idx=\(dxSmartWriteIndex)/\(dxSmartCommandQueue.count)) — treating as success") succeed() } else { + DebugLog.shared.log("BLE: UNEXPECTED disconnect — state=\(state) writeIdx=\(dxSmartWriteIndex) queueCount=\(dxSmartCommandQueue.count)") fail("Unexpected disconnect", code: .disconnected) } } @@ -1106,14 +1108,21 @@ extension BeaconProvisioner: CBPeripheralDelegate { // Device name (0x71) and Frame 1 commands (steps 1-6) may be rejected by some firmware // Treat these as non-fatal: log and continue to next command let isNonFatalCommand = dxSmartWriteIndex < 6 // First 6 commands are optional - if isNonFatalCommand { + let isSaveConfig = dxSmartWriteIndex >= dxSmartCommandQueue.count - 1 + if isSaveConfig { + // SaveConfig (0x60) write "error" is expected — beacon reboots immediately + // after processing the save, which kills the BLE connection before the + // ATT write response can be delivered. This is success, not failure. + DebugLog.shared.log("BLE: SaveConfig write error (beacon rebooted) — treating as success") + succeed() + } else if isNonFatalCommand { DebugLog.shared.log("BLE: Non-fatal command failed at step \(dxSmartWriteIndex + 1), continuing...") dxSmartWriteIndex += 1 DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in self?.dxSmartSendNextCommand() } } else { - fail("Command write failed at step \(dxSmartWriteIndex + 1): \(error.localizedDescription)", code: .writeFailed) + fail("Command write failed at step \(dxSmartWriteIndex + 1)/\(dxSmartCommandQueue.count): \(error.localizedDescription)", code: .writeFailed) } } return