fix: add real-time status updates to KBeacon provisioner + fix disconnect handler
KBeaconProvisioner had no onStatusUpdate callback, so the UI showed a static "Connecting..." message during the entire auth cycle (5 passwords × 5s timeout × 3 retries = 75s of dead silence). Now reports each phase: connecting, discovering services, authenticating (with password attempt count), writing, saving. Also fixed ScanView disconnect handler to cover .writing and .verifying states — previously only handled .connecting/.connected, so a mid-write disconnect left the UI permanently stuck. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c3f2b4faab
commit
61862adfa8
2 changed files with 36 additions and 3 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue