From c243235237b7eee93b00a5936b6b5e314766223b Mon Sep 17 00:00:00 2001 From: Schwifty Date: Sun, 22 Mar 2026 23:12:06 +0000 Subject: [PATCH] fix: connection callback bug + add provisioning diagnostics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BIG FIX: Provisioners were calling centralManager.connect() but BLEManager is the CBCentralManagerDelegate — provisioners never received didConnect/didFailToConnect callbacks, so connections ALWAYS timed out after 5s regardless. This is why provisioning kept failing. Fixed by: 1. Adding didConnect/didFailToConnect/didDisconnect to BLEManager 2. Provisioners register connection callbacks via bleManager 3. Increased connection timeout from 5s to 10s DIAGNOSTICS: Added ProvisionLog system so failures show a timestamped step-by-step log of what happened (with Share button). Every phase is logged: init, API calls, connect attempts, service discovery, auth, write commands, and errors. --- PayfritBeacon.xcodeproj/project.pbxproj | 4 ++ .../Provisioners/BlueCharmProvisioner.swift | 19 +++++ .../Provisioners/DXSmartProvisioner.swift | 46 +++++++++++- .../Provisioners/FallbackProvisioner.swift | 29 ++++++-- .../Provisioners/KBeaconProvisioner.swift | 19 +++++ .../Provisioners/ProvisionerProtocol.swift | 8 ++- PayfritBeacon/Services/BLEManager.swift | 26 +++++++ PayfritBeacon/Services/ProvisionLog.swift | 56 +++++++++++++++ PayfritBeacon/Views/ScanView.swift | 72 ++++++++++++++++--- 9 files changed, 263 insertions(+), 16 deletions(-) create mode 100644 PayfritBeacon/Services/ProvisionLog.swift diff --git a/PayfritBeacon.xcodeproj/project.pbxproj b/PayfritBeacon.xcodeproj/project.pbxproj index 1600d9b..83c1d66 100644 --- a/PayfritBeacon.xcodeproj/project.pbxproj +++ b/PayfritBeacon.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ A01000000031 /* APIConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A02000000031 /* APIConfig.swift */; }; A01000000032 /* BLEManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A02000000032 /* BLEManager.swift */; }; A01000000033 /* SecureStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A02000000033 /* SecureStorage.swift */; }; + A01000000034 /* ProvisionLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = A02000000034 /* ProvisionLog.swift */; }; A01000000040 /* BeaconBanList.swift in Sources */ = {isa = PBXBuildFile; fileRef = A02000000040 /* BeaconBanList.swift */; }; A01000000041 /* BeaconShardPool.swift in Sources */ = {isa = PBXBuildFile; fileRef = A02000000041 /* BeaconShardPool.swift */; }; A01000000042 /* UUIDFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = A02000000042 /* UUIDFormatting.swift */; }; @@ -57,6 +58,7 @@ A02000000031 /* APIConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIConfig.swift; sourceTree = ""; }; A02000000032 /* BLEManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEManager.swift; sourceTree = ""; }; A02000000033 /* SecureStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureStorage.swift; sourceTree = ""; }; + A02000000034 /* ProvisionLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProvisionLog.swift; sourceTree = ""; }; A02000000040 /* BeaconBanList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeaconBanList.swift; sourceTree = ""; }; A02000000043 /* BrandColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrandColors.swift; sourceTree = ""; }; A02000000041 /* BeaconShardPool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeaconShardPool.swift; sourceTree = ""; }; @@ -159,6 +161,7 @@ A02000000031 /* APIConfig.swift */, A02000000032 /* BLEManager.swift */, A02000000033 /* SecureStorage.swift */, + A02000000034 /* ProvisionLog.swift */, ); path = Services; sourceTree = ""; @@ -276,6 +279,7 @@ A01000000031 /* APIConfig.swift in Sources */, A01000000032 /* BLEManager.swift in Sources */, A01000000033 /* SecureStorage.swift in Sources */, + A01000000034 /* ProvisionLog.swift in Sources */, A01000000040 /* BeaconBanList.swift in Sources */, A01000000041 /* BeaconShardPool.swift in Sources */, A01000000042 /* UUIDFormatting.swift in Sources */, diff --git a/PayfritBeacon/Provisioners/BlueCharmProvisioner.swift b/PayfritBeacon/Provisioners/BlueCharmProvisioner.swift index 7e497b1..a25091c 100644 --- a/PayfritBeacon/Provisioners/BlueCharmProvisioner.swift +++ b/PayfritBeacon/Provisioners/BlueCharmProvisioner.swift @@ -57,6 +57,8 @@ final class BlueCharmProvisioner: NSObject, BeaconProvisioner { private var writeOKContinuation: CheckedContinuation? private(set) var isConnected = false + var diagnosticLog: ProvisionLog? + var bleManager: BLEManager? // MARK: - Init @@ -249,6 +251,23 @@ final class BlueCharmProvisioner: NSObject, BeaconProvisioner { private func connectOnce() async throws { try await withCheckedThrowingContinuation { (cont: CheckedContinuation) in connectionContinuation = cont + + // Register for connection callbacks via BLEManager (the CBCentralManagerDelegate) + bleManager?.onPeripheralConnected = { [weak self] connectedPeripheral in + guard connectedPeripheral.identifier == self?.peripheral.identifier else { return } + if let c = self?.connectionContinuation { + self?.connectionContinuation = nil + c.resume() + } + } + bleManager?.onPeripheralFailedToConnect = { [weak self] failedPeripheral, error in + guard failedPeripheral.identifier == self?.peripheral.identifier else { return } + if let c = self?.connectionContinuation { + self?.connectionContinuation = nil + c.resume(throwing: error ?? ProvisionError.connectionTimeout) + } + } + centralManager.connect(peripheral, options: nil) DispatchQueue.main.asyncAfter(deadline: .now() + GATTConstants.connectionTimeout) { [weak self] in diff --git a/PayfritBeacon/Provisioners/DXSmartProvisioner.swift b/PayfritBeacon/Provisioners/DXSmartProvisioner.swift index bb2acdb..03ab017 100644 --- a/PayfritBeacon/Provisioners/DXSmartProvisioner.swift +++ b/PayfritBeacon/Provisioners/DXSmartProvisioner.swift @@ -41,6 +41,8 @@ final class DXSmartProvisioner: NSObject, BeaconProvisioner { private(set) var isConnected = false private(set) var isFlashing = false // Beacon LED flashing after trigger private var useNewSDK = true // Prefer new SDK, fallback to old + var diagnosticLog: ProvisionLog? + var bleManager: BLEManager? // MARK: - Init @@ -55,18 +57,27 @@ final class DXSmartProvisioner: NSObject, BeaconProvisioner { func connect() async throws { for attempt in 1...GATTConstants.maxRetries { + await diagnosticLog?.log("connect", "Attempt \(attempt)/\(GATTConstants.maxRetries) — peripheral: \(peripheral.name ?? "unnamed"), state: \(peripheral.state.rawValue)") do { + await diagnosticLog?.log("connect", "Connecting (timeout: \(GATTConstants.connectionTimeout)s)…") try await connectOnce() + await diagnosticLog?.log("connect", "Connected — discovering services…") try await discoverServices() + await diagnosticLog?.log("connect", "Services found — FFE1:\(ffe1Char != nil) FFE2:\(ffe2Char != nil) FFE3:\(ffe3Char != nil)") + await diagnosticLog?.log("auth", "Authenticating (trigger + password)…") try await authenticate() + await diagnosticLog?.log("auth", "Auth complete — SDK: \(useNewSDK ? "new (FFE2)" : "old (FFE1)")") isConnected = true isFlashing = true return } catch { + await diagnosticLog?.log("connect", "Attempt \(attempt) failed: \(error.localizedDescription)", isError: true) disconnect() if attempt < GATTConstants.maxRetries { + await diagnosticLog?.log("connect", "Retrying in \(GATTConstants.retryDelay)s…") try await Task.sleep(nanoseconds: UInt64(GATTConstants.retryDelay * 1_000_000_000)) } else { + await diagnosticLog?.log("connect", "All \(GATTConstants.maxRetries) attempts exhausted", isError: true) throw error } } @@ -75,23 +86,31 @@ final class DXSmartProvisioner: NSObject, BeaconProvisioner { func writeConfig(_ config: BeaconConfig) async throws { guard isConnected else { + await diagnosticLog?.log("write", "Not connected — aborting write", isError: true) throw ProvisionError.notConnected } let uuidBytes = config.uuid.hexToBytes guard uuidBytes.count == 16 else { + await diagnosticLog?.log("write", "Invalid UUID length: \(uuidBytes.count) bytes", isError: true) throw ProvisionError.writeFailed("Invalid UUID length") } + await diagnosticLog?.log("write", "Config: UUID=\(config.uuid.prefix(8))… Major=\(config.major) Minor=\(config.minor) TxPower=\(config.txPower) AdvInt=\(config.advInterval)") + // Try new SDK first (FFE2), fall back to old SDK (FFE1) if useNewSDK, let ffe2 = ffe2Char { + await diagnosticLog?.log("write", "Using new SDK (FFE2) — 22 commands to write") try await writeConfigNewSDK(config, uuidBytes: uuidBytes, writeChar: ffe2) } else if let ffe1 = ffe1Char { + await diagnosticLog?.log("write", "Using old SDK (FFE1) — 7 commands to write") try await writeConfigOldSDK(config, uuidBytes: uuidBytes, writeChar: ffe1) } else { + await diagnosticLog?.log("write", "No write characteristic available", isError: true) throw ProvisionError.characteristicNotFound } + await diagnosticLog?.log("write", "All commands written successfully") isFlashing = false } @@ -141,8 +160,14 @@ final class DXSmartProvisioner: NSObject, BeaconProvisioner { ("SaveConfig", buildProtocolPacket(cmd: 0x60, data: Data())), ] - for (name, packet) in commands { - try await writeToCharAndWaitACK(writeChar, data: packet, label: name) + for (index, (name, packet)) in commands.enumerated() { + await diagnosticLog?.log("write", "[\(index + 1)/\(commands.count)] \(name) (\(packet.count) bytes)") + do { + try await writeToCharAndWaitACK(writeChar, data: packet, label: name) + } catch { + await diagnosticLog?.log("write", "[\(index + 1)/\(commands.count)] \(name) FAILED: \(error.localizedDescription)", isError: true) + throw error + } // 200ms between commands (matches Android SDK timer interval) try await Task.sleep(nanoseconds: 200_000_000) } @@ -225,6 +250,23 @@ final class DXSmartProvisioner: NSObject, BeaconProvisioner { private func connectOnce() async throws { try await withCheckedThrowingContinuation { (cont: CheckedContinuation) in connectionContinuation = cont + + // Register for connection callbacks via BLEManager (the CBCentralManagerDelegate) + bleManager?.onPeripheralConnected = { [weak self] connectedPeripheral in + guard connectedPeripheral.identifier == self?.peripheral.identifier else { return } + if let c = self?.connectionContinuation { + self?.connectionContinuation = nil + c.resume() + } + } + bleManager?.onPeripheralFailedToConnect = { [weak self] failedPeripheral, error in + guard failedPeripheral.identifier == self?.peripheral.identifier else { return } + if let c = self?.connectionContinuation { + self?.connectionContinuation = nil + c.resume(throwing: error ?? ProvisionError.connectionTimeout) + } + } + centralManager.connect(peripheral, options: nil) DispatchQueue.main.asyncAfter(deadline: .now() + GATTConstants.connectionTimeout) { [weak self] in diff --git a/PayfritBeacon/Provisioners/FallbackProvisioner.swift b/PayfritBeacon/Provisioners/FallbackProvisioner.swift index d9580de..769bc75 100644 --- a/PayfritBeacon/Provisioners/FallbackProvisioner.swift +++ b/PayfritBeacon/Provisioners/FallbackProvisioner.swift @@ -10,6 +10,8 @@ final class FallbackProvisioner: BeaconProvisioner { private var activeProvisioner: (any BeaconProvisioner)? private(set) var isConnected: Bool = false + var diagnosticLog: ProvisionLog? + var bleManager: BLEManager? init(peripheral: CBPeripheral, centralManager: CBCentralManager) { self.peripheral = peripheral @@ -18,23 +20,42 @@ final class FallbackProvisioner: BeaconProvisioner { func connect() async throws { let provisioners: [() -> any BeaconProvisioner] = [ - { KBeaconProvisioner(peripheral: self.peripheral, centralManager: self.centralManager) }, - { DXSmartProvisioner(peripheral: self.peripheral, centralManager: self.centralManager) }, - { BlueCharmProvisioner(peripheral: self.peripheral, centralManager: self.centralManager) }, + { [self] in + var p = KBeaconProvisioner(peripheral: peripheral, centralManager: centralManager) + p.diagnosticLog = diagnosticLog + p.bleManager = bleManager + return p + }, + { [self] in + var p = DXSmartProvisioner(peripheral: peripheral, centralManager: centralManager) + p.diagnosticLog = diagnosticLog + p.bleManager = bleManager + return p + }, + { [self] in + var p = BlueCharmProvisioner(peripheral: peripheral, centralManager: centralManager) + p.diagnosticLog = diagnosticLog + p.bleManager = bleManager + return p + }, ] + let typeNames = ["KBeacon", "DXSmart", "BlueCharm"] var lastError: Error = ProvisionError.connectionTimeout - for makeProvisioner in provisioners { + for (index, makeProvisioner) in provisioners.enumerated() { + await diagnosticLog?.log("fallback", "Trying \(typeNames[index]) provisioner…") let provisioner = makeProvisioner() do { try await provisioner.connect() activeProvisioner = provisioner isConnected = true + await diagnosticLog?.log("fallback", "\(typeNames[index]) connected successfully") return } catch { provisioner.disconnect() lastError = error + await diagnosticLog?.log("fallback", "\(typeNames[index]) failed: \(error.localizedDescription)", isError: true) } } diff --git a/PayfritBeacon/Provisioners/KBeaconProvisioner.swift b/PayfritBeacon/Provisioners/KBeaconProvisioner.swift index 398bc4b..f2e702e 100644 --- a/PayfritBeacon/Provisioners/KBeaconProvisioner.swift +++ b/PayfritBeacon/Provisioners/KBeaconProvisioner.swift @@ -44,6 +44,8 @@ final class KBeaconProvisioner: NSObject, BeaconProvisioner { private var writeContinuation: CheckedContinuation? private(set) var isConnected = false + var diagnosticLog: ProvisionLog? + var bleManager: BLEManager? // MARK: - Init @@ -134,6 +136,23 @@ final class KBeaconProvisioner: NSObject, BeaconProvisioner { private func connectOnce() async throws { try await withCheckedThrowingContinuation { (cont: CheckedContinuation) in connectionContinuation = cont + + // Register for connection callbacks via BLEManager (the CBCentralManagerDelegate) + bleManager?.onPeripheralConnected = { [weak self] connectedPeripheral in + guard connectedPeripheral.identifier == self?.peripheral.identifier else { return } + if let c = self?.connectionContinuation { + self?.connectionContinuation = nil + c.resume() + } + } + bleManager?.onPeripheralFailedToConnect = { [weak self] failedPeripheral, error in + guard failedPeripheral.identifier == self?.peripheral.identifier else { return } + if let c = self?.connectionContinuation { + self?.connectionContinuation = nil + c.resume(throwing: error ?? ProvisionError.connectionTimeout) + } + } + centralManager.connect(peripheral, options: nil) // Timeout diff --git a/PayfritBeacon/Provisioners/ProvisionerProtocol.swift b/PayfritBeacon/Provisioners/ProvisionerProtocol.swift index da93d77..7f0118d 100644 --- a/PayfritBeacon/Provisioners/ProvisionerProtocol.swift +++ b/PayfritBeacon/Provisioners/ProvisionerProtocol.swift @@ -14,6 +14,12 @@ protocol BeaconProvisioner { /// Whether we're currently connected var isConnected: Bool { get } + + /// Optional diagnostic log for tracing provisioning steps + var diagnosticLog: ProvisionLog? { get set } + + /// BLE manager reference for connection callbacks + var bleManager: BLEManager? { get set } } /// GATT UUIDs shared across provisioner types @@ -32,7 +38,7 @@ enum GATTConstants { static let cccd = CBUUID(string: "00002902-0000-1000-8000-00805F9B34FB") // Timeouts (matching Android) - static let connectionTimeout: TimeInterval = 5.0 + static let connectionTimeout: TimeInterval = 10.0 // Increased from 5s — BLE connections can be slow static let operationTimeout: TimeInterval = 5.0 static let maxRetries = 3 static let retryDelay: TimeInterval = 1.0 diff --git a/PayfritBeacon/Services/BLEManager.swift b/PayfritBeacon/Services/BLEManager.swift index 373f4a6..2255166 100644 --- a/PayfritBeacon/Services/BLEManager.swift +++ b/PayfritBeacon/Services/BLEManager.swift @@ -27,6 +27,14 @@ final class BLEManager: NSObject, ObservableObject { // DX-Smart factory default iBeacon UUID static let dxSmartDefaultUUID = "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0" + // MARK: - Connection Callbacks (used by provisioners) + // Provisioners call centralManager.connect() but BLEManager is the delegate, + // so we need to forward connection events back to provisioners via closures. + + var onPeripheralConnected: ((CBPeripheral) -> Void)? + var onPeripheralFailedToConnect: ((CBPeripheral, Error?) -> Void)? + var onPeripheralDisconnected: ((CBPeripheral, Error?) -> Void)? + // MARK: - Private private(set) var centralManager: CBCentralManager! @@ -232,6 +240,24 @@ extension BLEManager: CBCentralManagerDelegate { } } + nonisolated func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { + Task { @MainActor in + onPeripheralConnected?(peripheral) + } + } + + nonisolated func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { + Task { @MainActor in + onPeripheralFailedToConnect?(peripheral, error) + } + } + + nonisolated func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { + Task { @MainActor in + onPeripheralDisconnected?(peripheral, error) + } + } + nonisolated func centralManager( _ central: CBCentralManager, didDiscover peripheral: CBPeripheral, diff --git a/PayfritBeacon/Services/ProvisionLog.swift b/PayfritBeacon/Services/ProvisionLog.swift new file mode 100644 index 0000000..2f2a8c9 --- /dev/null +++ b/PayfritBeacon/Services/ProvisionLog.swift @@ -0,0 +1,56 @@ +import Foundation + +/// Timestamped diagnostic log for beacon provisioning. +/// Captures every step so we can diagnose failures. +@MainActor +final class ProvisionLog: ObservableObject { + struct Entry: Identifiable { + let id = UUID() + let timestamp: Date + let phase: String // "connect", "discover", "auth", "write", "verify" + let message: String + let isError: Bool + + var formatted: String { + let t = Self.formatter.string(from: timestamp) + let prefix = isError ? "❌" : "✅" + return "\(t) [\(phase)] \(prefix) \(message)" + } + + private static let formatter: DateFormatter = { + let f = DateFormatter() + f.dateFormat = "HH:mm:ss.SSS" + return f + }() + } + + @Published private(set) var entries: [Entry] = [] + private var startTime: Date? + + /// Clear log for a new provisioning attempt + func reset() { + entries = [] + startTime = Date() + } + + /// Add a log entry + func log(_ phase: String, _ message: String, isError: Bool = false) { + let entry = Entry(timestamp: Date(), phase: phase, message: message, isError: isError) + entries.append(entry) + } + + /// Elapsed time since reset + var elapsed: String { + guard let start = startTime else { return "0.0s" } + let seconds = Date().timeIntervalSince(start) + return String(format: "%.1fs", seconds) + } + + /// Full log as shareable text + var fullText: String { + let header = "Payfrit Beacon Diagnostic Log" + let time = "Session: \(elapsed)" + let lines = entries.map { $0.formatted } + return ([header, time, "---"] + lines).joined(separator: "\n") + } +} diff --git a/PayfritBeacon/Views/ScanView.swift b/PayfritBeacon/Views/ScanView.swift index 8f4bd1f..776884a 100644 --- a/PayfritBeacon/Views/ScanView.swift +++ b/PayfritBeacon/Views/ScanView.swift @@ -27,6 +27,7 @@ struct ScanView: View { @State private var errorMessage: String? @State private var showQRScanner = false @State private var scannedMAC: String? + @StateObject private var provisionLog = ProvisionLog() enum ProvisioningState { case idle @@ -396,10 +397,9 @@ struct ScanView: View { } private var failedView: some View { - VStack(spacing: 24) { - Spacer() + VStack(spacing: 16) { Image(systemName: "xmark.circle.fill") - .font(.system(size: 64)) + .font(.system(size: 48)) .foregroundStyle(Color.errorRed) Text("Provisioning Failed") .font(.title2.bold()) @@ -409,6 +409,37 @@ struct ScanView: View { .multilineTextAlignment(.center) .padding(.horizontal, 32) + // Diagnostic log + if !provisionLog.entries.isEmpty { + VStack(alignment: .leading, spacing: 4) { + HStack { + Text("Diagnostic Log (\(provisionLog.elapsed))") + .font(.caption.bold()) + Spacer() + ShareLink(item: provisionLog.fullText) { + Label("Share", systemImage: "square.and.arrow.up") + .font(.caption) + } + } + .padding(.horizontal, 16) + + ScrollView { + LazyVStack(alignment: .leading, spacing: 2) { + ForEach(provisionLog.entries) { entry in + Text(entry.formatted) + .font(.system(.caption2, design: .monospaced)) + .foregroundStyle(entry.isError ? Color.errorRed : .primary) + } + } + .padding(.horizontal, 16) + } + .frame(maxHeight: 200) + .background(Color(.systemGray6)) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .padding(.horizontal, 16) + } + } + HStack(spacing: 16) { Button("Try Again") { if let beacon = selectedBeacon { @@ -427,8 +458,8 @@ struct ScanView: View { resetProvisioningState() } .foregroundStyle(.secondary) - Spacer() } + .padding(.vertical, 8) } // MARK: - Create Service Point Sheet @@ -527,12 +558,17 @@ struct ScanView: View { provisioningState = .connecting statusMessage = "Connecting to \(beacon.displayName)…" errorMessage = nil + provisionLog.reset() + provisionLog.log("init", "Starting provisioning: \(beacon.displayName) (\(beacon.type.rawValue)) RSSI:\(beacon.rssi)") + provisionLog.log("init", "Service point: \(sp.name), Business: \(business.name)") do { // Allocate minor for this service point + provisionLog.log("api", "Allocating minor for service point \(sp.id)…") let minor = try await APIClient.shared.allocateMinor( businessId: business.id, servicePointId: sp.id, token: token ) + provisionLog.log("api", "Minor allocated: \(minor)") let config = BeaconConfig( uuid: ns.uuid.normalizedUUID, @@ -549,7 +585,19 @@ struct ScanView: View { let provisioner = makeProvisioner(for: beacon) statusMessage = "Authenticating with \(beacon.type.rawValue)…" + provisionLog.log("connect", "Connecting to \(beacon.type.rawValue) provisioner…") + + // Monitor for unexpected disconnects during provisioning + bleManager.onPeripheralDisconnected = { [weak provisionLog] peripheral, error in + if peripheral.identifier == beacon.peripheral.identifier { + Task { @MainActor in + provisionLog?.log("disconnect", "Unexpected disconnect: \(error?.localizedDescription ?? "no error")", isError: true) + } + } + } + try await provisioner.connect() + provisionLog.log("connect", "Connected successfully") // DXSmart: stop at connected state, wait for user to confirm flashing if beacon.type == .dxsmart { @@ -563,7 +611,9 @@ struct ScanView: View { // KBeacon / BlueCharm: write immediately provisioningState = .writing statusMessage = "Writing \(config.formattedUUID.prefix(8))… Major:\(config.major) Minor:\(config.minor)" + provisionLog.log("write", "Writing config…") try await provisioner.writeConfig(config) + provisionLog.log("write", "Config written — disconnecting") provisioner.disconnect() // Register with backend @@ -598,6 +648,7 @@ struct ScanView: View { statusMessage = "\(sp.name) — \(beacon.type.rawValue)\nUUID: \(config.formattedUUID.prefix(13))…\nMajor: \(config.major) Minor: \(config.minor)" } catch { + provisionLog.log("error", "Provisioning failed after \(provisionLog.elapsed): \(error.localizedDescription)", isError: true) provisioningState = .failed errorMessage = error.localizedDescription } @@ -733,17 +784,20 @@ struct ScanView: View { } private func makeProvisioner(for beacon: DiscoveredBeacon) -> any BeaconProvisioner { + var provisioner: any BeaconProvisioner switch beacon.type { case .kbeacon: - return KBeaconProvisioner(peripheral: beacon.peripheral, centralManager: bleManager.centralManager) + provisioner = KBeaconProvisioner(peripheral: beacon.peripheral, centralManager: bleManager.centralManager) case .dxsmart: - return DXSmartProvisioner(peripheral: beacon.peripheral, centralManager: bleManager.centralManager) + provisioner = DXSmartProvisioner(peripheral: beacon.peripheral, centralManager: bleManager.centralManager) case .bluecharm: - return BlueCharmProvisioner(peripheral: beacon.peripheral, centralManager: bleManager.centralManager) + provisioner = BlueCharmProvisioner(peripheral: beacon.peripheral, centralManager: bleManager.centralManager) case .unknown: - // Try all provisioners in sequence (matches Android fallback behavior) - return FallbackProvisioner(peripheral: beacon.peripheral, centralManager: bleManager.centralManager) + provisioner = FallbackProvisioner(peripheral: beacon.peripheral, centralManager: bleManager.centralManager) } + provisioner.bleManager = bleManager + provisioner.diagnosticLog = provisionLog + return provisioner } } -- 2.43.0