Compare commits
No commits in common. "349dab1b75d36b2329d100de71dda5338664b942" and "c879ecd44263009f07a38d74649d3b773ecd1af1" have entirely different histories.
349dab1b75
...
c879ecd442
9 changed files with 16 additions and 263 deletions
|
|
@ -24,7 +24,6 @@
|
||||||
A01000000031 /* APIConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A02000000031 /* APIConfig.swift */; };
|
A01000000031 /* APIConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A02000000031 /* APIConfig.swift */; };
|
||||||
A01000000032 /* BLEManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A02000000032 /* BLEManager.swift */; };
|
A01000000032 /* BLEManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A02000000032 /* BLEManager.swift */; };
|
||||||
A01000000033 /* SecureStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A02000000033 /* SecureStorage.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 */; };
|
A01000000040 /* BeaconBanList.swift in Sources */ = {isa = PBXBuildFile; fileRef = A02000000040 /* BeaconBanList.swift */; };
|
||||||
A01000000041 /* BeaconShardPool.swift in Sources */ = {isa = PBXBuildFile; fileRef = A02000000041 /* BeaconShardPool.swift */; };
|
A01000000041 /* BeaconShardPool.swift in Sources */ = {isa = PBXBuildFile; fileRef = A02000000041 /* BeaconShardPool.swift */; };
|
||||||
A01000000042 /* UUIDFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = A02000000042 /* UUIDFormatting.swift */; };
|
A01000000042 /* UUIDFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = A02000000042 /* UUIDFormatting.swift */; };
|
||||||
|
|
@ -58,7 +57,6 @@
|
||||||
A02000000031 /* APIConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIConfig.swift; sourceTree = "<group>"; };
|
A02000000031 /* APIConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIConfig.swift; sourceTree = "<group>"; };
|
||||||
A02000000032 /* BLEManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEManager.swift; sourceTree = "<group>"; };
|
A02000000032 /* BLEManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEManager.swift; sourceTree = "<group>"; };
|
||||||
A02000000033 /* SecureStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureStorage.swift; sourceTree = "<group>"; };
|
A02000000033 /* SecureStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureStorage.swift; sourceTree = "<group>"; };
|
||||||
A02000000034 /* ProvisionLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProvisionLog.swift; sourceTree = "<group>"; };
|
|
||||||
A02000000040 /* BeaconBanList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeaconBanList.swift; sourceTree = "<group>"; };
|
A02000000040 /* BeaconBanList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeaconBanList.swift; sourceTree = "<group>"; };
|
||||||
A02000000043 /* BrandColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrandColors.swift; sourceTree = "<group>"; };
|
A02000000043 /* BrandColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrandColors.swift; sourceTree = "<group>"; };
|
||||||
A02000000041 /* BeaconShardPool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeaconShardPool.swift; sourceTree = "<group>"; };
|
A02000000041 /* BeaconShardPool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeaconShardPool.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -161,7 +159,6 @@
|
||||||
A02000000031 /* APIConfig.swift */,
|
A02000000031 /* APIConfig.swift */,
|
||||||
A02000000032 /* BLEManager.swift */,
|
A02000000032 /* BLEManager.swift */,
|
||||||
A02000000033 /* SecureStorage.swift */,
|
A02000000033 /* SecureStorage.swift */,
|
||||||
A02000000034 /* ProvisionLog.swift */,
|
|
||||||
);
|
);
|
||||||
path = Services;
|
path = Services;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -279,7 +276,6 @@
|
||||||
A01000000031 /* APIConfig.swift in Sources */,
|
A01000000031 /* APIConfig.swift in Sources */,
|
||||||
A01000000032 /* BLEManager.swift in Sources */,
|
A01000000032 /* BLEManager.swift in Sources */,
|
||||||
A01000000033 /* SecureStorage.swift in Sources */,
|
A01000000033 /* SecureStorage.swift in Sources */,
|
||||||
A01000000034 /* ProvisionLog.swift in Sources */,
|
|
||||||
A01000000040 /* BeaconBanList.swift in Sources */,
|
A01000000040 /* BeaconBanList.swift in Sources */,
|
||||||
A01000000041 /* BeaconShardPool.swift in Sources */,
|
A01000000041 /* BeaconShardPool.swift in Sources */,
|
||||||
A01000000042 /* UUIDFormatting.swift in Sources */,
|
A01000000042 /* UUIDFormatting.swift in Sources */,
|
||||||
|
|
|
||||||
|
|
@ -57,8 +57,6 @@ final class BlueCharmProvisioner: NSObject, BeaconProvisioner {
|
||||||
private var writeOKContinuation: CheckedContinuation<Void, Error>?
|
private var writeOKContinuation: CheckedContinuation<Void, Error>?
|
||||||
|
|
||||||
private(set) var isConnected = false
|
private(set) var isConnected = false
|
||||||
var diagnosticLog: ProvisionLog?
|
|
||||||
var bleManager: BLEManager?
|
|
||||||
|
|
||||||
// MARK: - Init
|
// MARK: - Init
|
||||||
|
|
||||||
|
|
@ -251,23 +249,6 @@ final class BlueCharmProvisioner: NSObject, BeaconProvisioner {
|
||||||
private func connectOnce() async throws {
|
private func connectOnce() async throws {
|
||||||
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
|
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
|
||||||
connectionContinuation = cont
|
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)
|
centralManager.connect(peripheral, options: nil)
|
||||||
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + GATTConstants.connectionTimeout) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + GATTConstants.connectionTimeout) { [weak self] in
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,6 @@ final class DXSmartProvisioner: NSObject, BeaconProvisioner {
|
||||||
private(set) var isConnected = false
|
private(set) var isConnected = false
|
||||||
private(set) var isFlashing = false // Beacon LED flashing after trigger
|
private(set) var isFlashing = false // Beacon LED flashing after trigger
|
||||||
private var useNewSDK = true // Prefer new SDK, fallback to old
|
private var useNewSDK = true // Prefer new SDK, fallback to old
|
||||||
var diagnosticLog: ProvisionLog?
|
|
||||||
var bleManager: BLEManager?
|
|
||||||
|
|
||||||
// MARK: - Init
|
// MARK: - Init
|
||||||
|
|
||||||
|
|
@ -57,27 +55,18 @@ final class DXSmartProvisioner: NSObject, BeaconProvisioner {
|
||||||
|
|
||||||
func connect() async throws {
|
func connect() async throws {
|
||||||
for attempt in 1...GATTConstants.maxRetries {
|
for attempt in 1...GATTConstants.maxRetries {
|
||||||
await diagnosticLog?.log("connect", "Attempt \(attempt)/\(GATTConstants.maxRetries) — peripheral: \(peripheral.name ?? "unnamed"), state: \(peripheral.state.rawValue)")
|
|
||||||
do {
|
do {
|
||||||
await diagnosticLog?.log("connect", "Connecting (timeout: \(GATTConstants.connectionTimeout)s)…")
|
|
||||||
try await connectOnce()
|
try await connectOnce()
|
||||||
await diagnosticLog?.log("connect", "Connected — discovering services…")
|
|
||||||
try await discoverServices()
|
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()
|
try await authenticate()
|
||||||
await diagnosticLog?.log("auth", "Auth complete — SDK: \(useNewSDK ? "new (FFE2)" : "old (FFE1)")")
|
|
||||||
isConnected = true
|
isConnected = true
|
||||||
isFlashing = true
|
isFlashing = true
|
||||||
return
|
return
|
||||||
} catch {
|
} catch {
|
||||||
await diagnosticLog?.log("connect", "Attempt \(attempt) failed: \(error.localizedDescription)", isError: true)
|
|
||||||
disconnect()
|
disconnect()
|
||||||
if attempt < GATTConstants.maxRetries {
|
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))
|
try await Task.sleep(nanoseconds: UInt64(GATTConstants.retryDelay * 1_000_000_000))
|
||||||
} else {
|
} else {
|
||||||
await diagnosticLog?.log("connect", "All \(GATTConstants.maxRetries) attempts exhausted", isError: true)
|
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -86,31 +75,23 @@ final class DXSmartProvisioner: NSObject, BeaconProvisioner {
|
||||||
|
|
||||||
func writeConfig(_ config: BeaconConfig) async throws {
|
func writeConfig(_ config: BeaconConfig) async throws {
|
||||||
guard isConnected else {
|
guard isConnected else {
|
||||||
await diagnosticLog?.log("write", "Not connected — aborting write", isError: true)
|
|
||||||
throw ProvisionError.notConnected
|
throw ProvisionError.notConnected
|
||||||
}
|
}
|
||||||
|
|
||||||
let uuidBytes = config.uuid.hexToBytes
|
let uuidBytes = config.uuid.hexToBytes
|
||||||
guard uuidBytes.count == 16 else {
|
guard uuidBytes.count == 16 else {
|
||||||
await diagnosticLog?.log("write", "Invalid UUID length: \(uuidBytes.count) bytes", isError: true)
|
|
||||||
throw ProvisionError.writeFailed("Invalid UUID length")
|
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)
|
// Try new SDK first (FFE2), fall back to old SDK (FFE1)
|
||||||
if useNewSDK, let ffe2 = ffe2Char {
|
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)
|
try await writeConfigNewSDK(config, uuidBytes: uuidBytes, writeChar: ffe2)
|
||||||
} else if let ffe1 = ffe1Char {
|
} 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)
|
try await writeConfigOldSDK(config, uuidBytes: uuidBytes, writeChar: ffe1)
|
||||||
} else {
|
} else {
|
||||||
await diagnosticLog?.log("write", "No write characteristic available", isError: true)
|
|
||||||
throw ProvisionError.characteristicNotFound
|
throw ProvisionError.characteristicNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
await diagnosticLog?.log("write", "All commands written successfully")
|
|
||||||
isFlashing = false
|
isFlashing = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -160,14 +141,8 @@ final class DXSmartProvisioner: NSObject, BeaconProvisioner {
|
||||||
("SaveConfig", buildProtocolPacket(cmd: 0x60, data: Data())),
|
("SaveConfig", buildProtocolPacket(cmd: 0x60, data: Data())),
|
||||||
]
|
]
|
||||||
|
|
||||||
for (index, (name, packet)) in commands.enumerated() {
|
for (name, packet) in commands {
|
||||||
await diagnosticLog?.log("write", "[\(index + 1)/\(commands.count)] \(name) (\(packet.count) bytes)")
|
try await writeToCharAndWaitACK(writeChar, data: packet, label: name)
|
||||||
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)
|
// 200ms between commands (matches Android SDK timer interval)
|
||||||
try await Task.sleep(nanoseconds: 200_000_000)
|
try await Task.sleep(nanoseconds: 200_000_000)
|
||||||
}
|
}
|
||||||
|
|
@ -250,23 +225,6 @@ final class DXSmartProvisioner: NSObject, BeaconProvisioner {
|
||||||
private func connectOnce() async throws {
|
private func connectOnce() async throws {
|
||||||
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
|
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
|
||||||
connectionContinuation = cont
|
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)
|
centralManager.connect(peripheral, options: nil)
|
||||||
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + GATTConstants.connectionTimeout) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + GATTConstants.connectionTimeout) { [weak self] in
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,6 @@ final class FallbackProvisioner: BeaconProvisioner {
|
||||||
private var activeProvisioner: (any BeaconProvisioner)?
|
private var activeProvisioner: (any BeaconProvisioner)?
|
||||||
|
|
||||||
private(set) var isConnected: Bool = false
|
private(set) var isConnected: Bool = false
|
||||||
var diagnosticLog: ProvisionLog?
|
|
||||||
var bleManager: BLEManager?
|
|
||||||
|
|
||||||
init(peripheral: CBPeripheral, centralManager: CBCentralManager) {
|
init(peripheral: CBPeripheral, centralManager: CBCentralManager) {
|
||||||
self.peripheral = peripheral
|
self.peripheral = peripheral
|
||||||
|
|
@ -20,42 +18,23 @@ final class FallbackProvisioner: BeaconProvisioner {
|
||||||
|
|
||||||
func connect() async throws {
|
func connect() async throws {
|
||||||
let provisioners: [() -> any BeaconProvisioner] = [
|
let provisioners: [() -> any BeaconProvisioner] = [
|
||||||
{ [self] in
|
{ KBeaconProvisioner(peripheral: self.peripheral, centralManager: self.centralManager) },
|
||||||
var p = KBeaconProvisioner(peripheral: peripheral, centralManager: centralManager)
|
{ DXSmartProvisioner(peripheral: self.peripheral, centralManager: self.centralManager) },
|
||||||
p.diagnosticLog = diagnosticLog
|
{ BlueCharmProvisioner(peripheral: self.peripheral, centralManager: self.centralManager) },
|
||||||
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
|
var lastError: Error = ProvisionError.connectionTimeout
|
||||||
|
|
||||||
for (index, makeProvisioner) in provisioners.enumerated() {
|
for makeProvisioner in provisioners {
|
||||||
await diagnosticLog?.log("fallback", "Trying \(typeNames[index]) provisioner…")
|
|
||||||
let provisioner = makeProvisioner()
|
let provisioner = makeProvisioner()
|
||||||
do {
|
do {
|
||||||
try await provisioner.connect()
|
try await provisioner.connect()
|
||||||
activeProvisioner = provisioner
|
activeProvisioner = provisioner
|
||||||
isConnected = true
|
isConnected = true
|
||||||
await diagnosticLog?.log("fallback", "\(typeNames[index]) connected successfully")
|
|
||||||
return
|
return
|
||||||
} catch {
|
} catch {
|
||||||
provisioner.disconnect()
|
provisioner.disconnect()
|
||||||
lastError = error
|
lastError = error
|
||||||
await diagnosticLog?.log("fallback", "\(typeNames[index]) failed: \(error.localizedDescription)", isError: true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,6 @@ final class KBeaconProvisioner: NSObject, BeaconProvisioner {
|
||||||
private var writeContinuation: CheckedContinuation<Data, Error>?
|
private var writeContinuation: CheckedContinuation<Data, Error>?
|
||||||
|
|
||||||
private(set) var isConnected = false
|
private(set) var isConnected = false
|
||||||
var diagnosticLog: ProvisionLog?
|
|
||||||
var bleManager: BLEManager?
|
|
||||||
|
|
||||||
// MARK: - Init
|
// MARK: - Init
|
||||||
|
|
||||||
|
|
@ -136,23 +134,6 @@ final class KBeaconProvisioner: NSObject, BeaconProvisioner {
|
||||||
private func connectOnce() async throws {
|
private func connectOnce() async throws {
|
||||||
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
|
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
|
||||||
connectionContinuation = cont
|
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)
|
centralManager.connect(peripheral, options: nil)
|
||||||
|
|
||||||
// Timeout
|
// Timeout
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,6 @@ protocol BeaconProvisioner {
|
||||||
|
|
||||||
/// Whether we're currently connected
|
/// Whether we're currently connected
|
||||||
var isConnected: Bool { get }
|
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
|
/// GATT UUIDs shared across provisioner types
|
||||||
|
|
@ -38,7 +32,7 @@ enum GATTConstants {
|
||||||
static let cccd = CBUUID(string: "00002902-0000-1000-8000-00805F9B34FB")
|
static let cccd = CBUUID(string: "00002902-0000-1000-8000-00805F9B34FB")
|
||||||
|
|
||||||
// Timeouts (matching Android)
|
// Timeouts (matching Android)
|
||||||
static let connectionTimeout: TimeInterval = 10.0 // Increased from 5s — BLE connections can be slow
|
static let connectionTimeout: TimeInterval = 5.0
|
||||||
static let operationTimeout: TimeInterval = 5.0
|
static let operationTimeout: TimeInterval = 5.0
|
||||||
static let maxRetries = 3
|
static let maxRetries = 3
|
||||||
static let retryDelay: TimeInterval = 1.0
|
static let retryDelay: TimeInterval = 1.0
|
||||||
|
|
|
||||||
|
|
@ -27,14 +27,6 @@ final class BLEManager: NSObject, ObservableObject {
|
||||||
// DX-Smart factory default iBeacon UUID
|
// DX-Smart factory default iBeacon UUID
|
||||||
static let dxSmartDefaultUUID = "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"
|
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
|
// MARK: - Private
|
||||||
|
|
||||||
private(set) var centralManager: CBCentralManager!
|
private(set) var centralManager: CBCentralManager!
|
||||||
|
|
@ -240,24 +232,6 @@ 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(
|
nonisolated func centralManager(
|
||||||
_ central: CBCentralManager,
|
_ central: CBCentralManager,
|
||||||
didDiscover peripheral: CBPeripheral,
|
didDiscover peripheral: CBPeripheral,
|
||||||
|
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -27,7 +27,6 @@ struct ScanView: View {
|
||||||
@State private var errorMessage: String?
|
@State private var errorMessage: String?
|
||||||
@State private var showQRScanner = false
|
@State private var showQRScanner = false
|
||||||
@State private var scannedMAC: String?
|
@State private var scannedMAC: String?
|
||||||
@StateObject private var provisionLog = ProvisionLog()
|
|
||||||
|
|
||||||
enum ProvisioningState {
|
enum ProvisioningState {
|
||||||
case idle
|
case idle
|
||||||
|
|
@ -397,9 +396,10 @@ struct ScanView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var failedView: some View {
|
private var failedView: some View {
|
||||||
VStack(spacing: 16) {
|
VStack(spacing: 24) {
|
||||||
|
Spacer()
|
||||||
Image(systemName: "xmark.circle.fill")
|
Image(systemName: "xmark.circle.fill")
|
||||||
.font(.system(size: 48))
|
.font(.system(size: 64))
|
||||||
.foregroundStyle(Color.errorRed)
|
.foregroundStyle(Color.errorRed)
|
||||||
Text("Provisioning Failed")
|
Text("Provisioning Failed")
|
||||||
.font(.title2.bold())
|
.font(.title2.bold())
|
||||||
|
|
@ -409,37 +409,6 @@ struct ScanView: View {
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.padding(.horizontal, 32)
|
.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) {
|
HStack(spacing: 16) {
|
||||||
Button("Try Again") {
|
Button("Try Again") {
|
||||||
if let beacon = selectedBeacon {
|
if let beacon = selectedBeacon {
|
||||||
|
|
@ -458,8 +427,8 @@ struct ScanView: View {
|
||||||
resetProvisioningState()
|
resetProvisioningState()
|
||||||
}
|
}
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
|
Spacer()
|
||||||
}
|
}
|
||||||
.padding(.vertical, 8)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Create Service Point Sheet
|
// MARK: - Create Service Point Sheet
|
||||||
|
|
@ -558,17 +527,12 @@ struct ScanView: View {
|
||||||
provisioningState = .connecting
|
provisioningState = .connecting
|
||||||
statusMessage = "Connecting to \(beacon.displayName)…"
|
statusMessage = "Connecting to \(beacon.displayName)…"
|
||||||
errorMessage = nil
|
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 {
|
do {
|
||||||
// Allocate minor for this service point
|
// Allocate minor for this service point
|
||||||
provisionLog.log("api", "Allocating minor for service point \(sp.id)…")
|
|
||||||
let minor = try await APIClient.shared.allocateMinor(
|
let minor = try await APIClient.shared.allocateMinor(
|
||||||
businessId: business.id, servicePointId: sp.id, token: token
|
businessId: business.id, servicePointId: sp.id, token: token
|
||||||
)
|
)
|
||||||
provisionLog.log("api", "Minor allocated: \(minor)")
|
|
||||||
|
|
||||||
let config = BeaconConfig(
|
let config = BeaconConfig(
|
||||||
uuid: ns.uuid.normalizedUUID,
|
uuid: ns.uuid.normalizedUUID,
|
||||||
|
|
@ -585,19 +549,7 @@ struct ScanView: View {
|
||||||
let provisioner = makeProvisioner(for: beacon)
|
let provisioner = makeProvisioner(for: beacon)
|
||||||
|
|
||||||
statusMessage = "Authenticating with \(beacon.type.rawValue)…"
|
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()
|
try await provisioner.connect()
|
||||||
provisionLog.log("connect", "Connected successfully")
|
|
||||||
|
|
||||||
// DXSmart: stop at connected state, wait for user to confirm flashing
|
// DXSmart: stop at connected state, wait for user to confirm flashing
|
||||||
if beacon.type == .dxsmart {
|
if beacon.type == .dxsmart {
|
||||||
|
|
@ -611,9 +563,7 @@ struct ScanView: View {
|
||||||
// KBeacon / BlueCharm: write immediately
|
// KBeacon / BlueCharm: write immediately
|
||||||
provisioningState = .writing
|
provisioningState = .writing
|
||||||
statusMessage = "Writing \(config.formattedUUID.prefix(8))… Major:\(config.major) Minor:\(config.minor)"
|
statusMessage = "Writing \(config.formattedUUID.prefix(8))… Major:\(config.major) Minor:\(config.minor)"
|
||||||
provisionLog.log("write", "Writing config…")
|
|
||||||
try await provisioner.writeConfig(config)
|
try await provisioner.writeConfig(config)
|
||||||
provisionLog.log("write", "Config written — disconnecting")
|
|
||||||
provisioner.disconnect()
|
provisioner.disconnect()
|
||||||
|
|
||||||
// Register with backend
|
// Register with backend
|
||||||
|
|
@ -648,7 +598,6 @@ struct ScanView: View {
|
||||||
statusMessage = "\(sp.name) — \(beacon.type.rawValue)\nUUID: \(config.formattedUUID.prefix(13))…\nMajor: \(config.major) Minor: \(config.minor)"
|
statusMessage = "\(sp.name) — \(beacon.type.rawValue)\nUUID: \(config.formattedUUID.prefix(13))…\nMajor: \(config.major) Minor: \(config.minor)"
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
provisionLog.log("error", "Provisioning failed after \(provisionLog.elapsed): \(error.localizedDescription)", isError: true)
|
|
||||||
provisioningState = .failed
|
provisioningState = .failed
|
||||||
errorMessage = error.localizedDescription
|
errorMessage = error.localizedDescription
|
||||||
}
|
}
|
||||||
|
|
@ -784,20 +733,17 @@ struct ScanView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeProvisioner(for beacon: DiscoveredBeacon) -> any BeaconProvisioner {
|
private func makeProvisioner(for beacon: DiscoveredBeacon) -> any BeaconProvisioner {
|
||||||
var provisioner: any BeaconProvisioner
|
|
||||||
switch beacon.type {
|
switch beacon.type {
|
||||||
case .kbeacon:
|
case .kbeacon:
|
||||||
provisioner = KBeaconProvisioner(peripheral: beacon.peripheral, centralManager: bleManager.centralManager)
|
return KBeaconProvisioner(peripheral: beacon.peripheral, centralManager: bleManager.centralManager)
|
||||||
case .dxsmart:
|
case .dxsmart:
|
||||||
provisioner = DXSmartProvisioner(peripheral: beacon.peripheral, centralManager: bleManager.centralManager)
|
return DXSmartProvisioner(peripheral: beacon.peripheral, centralManager: bleManager.centralManager)
|
||||||
case .bluecharm:
|
case .bluecharm:
|
||||||
provisioner = BlueCharmProvisioner(peripheral: beacon.peripheral, centralManager: bleManager.centralManager)
|
return BlueCharmProvisioner(peripheral: beacon.peripheral, centralManager: bleManager.centralManager)
|
||||||
case .unknown:
|
case .unknown:
|
||||||
provisioner = FallbackProvisioner(peripheral: beacon.peripheral, centralManager: bleManager.centralManager)
|
// Try all provisioners in sequence (matches Android fallback behavior)
|
||||||
|
return FallbackProvisioner(peripheral: beacon.peripheral, centralManager: bleManager.centralManager)
|
||||||
}
|
}
|
||||||
provisioner.bleManager = bleManager
|
|
||||||
provisioner.diagnosticLog = provisionLog
|
|
||||||
return provisioner
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue