diff --git a/PayfritBeacon/Provisioners/KBeaconProvisioner.swift b/PayfritBeacon/Provisioners/KBeaconProvisioner.swift index f2e702e..7f693ee 100644 --- a/PayfritBeacon/Provisioners/KBeaconProvisioner.swift +++ b/PayfritBeacon/Provisioners/KBeaconProvisioner.swift @@ -47,6 +47,9 @@ final class KBeaconProvisioner: NSObject, BeaconProvisioner { var diagnosticLog: ProvisionLog? var bleManager: BLEManager? + /// Status callback — provisioner reports what phase it's in so UI can update + var onStatusUpdate: ((String) -> Void)? + // MARK: - Init init(peripheral: CBPeripheral, centralManager: CBCentralManager) { @@ -62,14 +65,27 @@ final class KBeaconProvisioner: NSObject, BeaconProvisioner { // Connect with retry for attempt in 1...GATTConstants.maxRetries { do { + let attemptLabel = GATTConstants.maxRetries > 1 ? " (attempt \(attempt)/\(GATTConstants.maxRetries))" : "" + await MainActor.run { onStatusUpdate?("Connecting to beacon…\(attemptLabel)") } + await diagnosticLog?.log("connect", "Attempt \(attempt)/\(GATTConstants.maxRetries)") try await connectOnce() + + await MainActor.run { onStatusUpdate?("Discovering services…") } + await diagnosticLog?.log("connect", "Connected — discovering services…") try await discoverServices() + await diagnosticLog?.log("connect", "Services found — write:\(writeChar != nil) notify:\(notifyChar != nil)") + + await MainActor.run { onStatusUpdate?("Authenticating…") } + await diagnosticLog?.log("auth", "Trying \(Self.passwords.count) passwords…") try await authenticate() + await diagnosticLog?.log("auth", "Auth success") isConnected = true return } catch { + await diagnosticLog?.log("connect", "Attempt \(attempt) failed: \(error.localizedDescription)", isError: true) disconnect() if attempt < GATTConstants.maxRetries { + await MainActor.run { onStatusUpdate?("Retrying… (\(attempt)/\(GATTConstants.maxRetries))") } try await Task.sleep(nanoseconds: UInt64(GATTConstants.retryDelay * 1_000_000_000)) } else { throw error @@ -111,17 +127,23 @@ final class KBeaconProvisioner: NSObject, BeaconProvisioner { params.append(UInt8(config.advInterval & 0xFF)) // Send CMD_WRITE_PARAMS + await MainActor.run { onStatusUpdate?("Writing beacon parameters…") } + await diagnosticLog?.log("write", "Sending CMD_WRITE_PARAMS (\(params.count) bytes)…") let writeCmd = Data([CMD.writeParams.rawValue]) + params let writeResp = try await sendCommand(writeCmd) guard writeResp.first == CMD.writeParams.rawValue else { throw ProvisionError.writeFailed("Unexpected write response") } + await diagnosticLog?.log("write", "Params written OK — saving to flash…") // Send CMD_SAVE to flash + await MainActor.run { onStatusUpdate?("Saving to flash…") } let saveResp = try await sendCommand(Data([CMD.save.rawValue])) guard saveResp.first == CMD.save.rawValue else { throw ProvisionError.saveFailed } + await MainActor.run { onStatusUpdate?("Config saved ✓") } + await diagnosticLog?.log("write", "Save confirmed") } func disconnect() { @@ -180,14 +202,20 @@ final class KBeaconProvisioner: NSObject, BeaconProvisioner { } private func authenticate() async throws { - for password in Self.passwords { + for (index, password) in Self.passwords.enumerated() { + let passwordLabel = String(data: password.prefix(6), encoding: .utf8) ?? "binary" + await MainActor.run { onStatusUpdate?("Authenticating… (\(index + 1)/\(Self.passwords.count))") } + await diagnosticLog?.log("auth", "Trying password \(index + 1)/\(Self.passwords.count): \(passwordLabel)…") let cmd = Data([CMD.auth.rawValue]) + password do { let resp = try await sendCommand(cmd) if resp.first == CMD.auth.rawValue && resp.count > 1 && resp[1] == 0x00 { + await MainActor.run { onStatusUpdate?("Authenticated ✓") } return // Auth success } + await diagnosticLog?.log("auth", "Password \(index + 1) rejected (response: \(resp.map { String(format: "%02X", $0) }.joined()))") } catch { + await diagnosticLog?.log("auth", "Password \(index + 1) timeout: \(error.localizedDescription)") continue } } diff --git a/PayfritBeacon/Views/ScanView.swift b/PayfritBeacon/Views/ScanView.swift index bfb1698..6a02741 100644 --- a/PayfritBeacon/Views/ScanView.swift +++ b/PayfritBeacon/Views/ScanView.swift @@ -619,6 +619,10 @@ struct ScanView: View { dxProvisioner.onStatusUpdate = { [weak self] status in self?.statusMessage = status } + } else if let kbProvisioner = provisioner as? KBeaconProvisioner { + kbProvisioner.onStatusUpdate = { [weak self] status in + self?.statusMessage = status + } } statusMessage = "Connecting to \(beacon.displayName)…" @@ -630,9 +634,10 @@ struct ScanView: View { Task { @MainActor [weak self] in let reason = error?.localizedDescription ?? "beacon timed out" provisionLog?.log("disconnect", "Unexpected disconnect: \(reason)", isError: true) - // If we're still in connecting or connected state, show failure + // If we're in any active provisioning state, show failure if let self = self, - self.provisioningState == .connecting || self.provisioningState == .connected { + self.provisioningState == .connecting || self.provisioningState == .connected || + self.provisioningState == .writing || self.provisioningState == .verifying { self.provisioningState = .failed self.errorMessage = "Beacon disconnected: \(reason)" }