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 diagnosticLog: ProvisionLog?
|
||||||
var bleManager: BLEManager?
|
var bleManager: BLEManager?
|
||||||
|
|
||||||
|
/// Status callback — provisioner reports what phase it's in so UI can update
|
||||||
|
var onStatusUpdate: ((String) -> Void)?
|
||||||
|
|
||||||
// MARK: - Init
|
// MARK: - Init
|
||||||
|
|
||||||
init(peripheral: CBPeripheral, centralManager: CBCentralManager) {
|
init(peripheral: CBPeripheral, centralManager: CBCentralManager) {
|
||||||
|
|
@ -62,14 +65,27 @@ final class KBeaconProvisioner: NSObject, BeaconProvisioner {
|
||||||
// Connect with retry
|
// Connect with retry
|
||||||
for attempt in 1...GATTConstants.maxRetries {
|
for attempt in 1...GATTConstants.maxRetries {
|
||||||
do {
|
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()
|
try await connectOnce()
|
||||||
|
|
||||||
|
await MainActor.run { onStatusUpdate?("Discovering services…") }
|
||||||
|
await diagnosticLog?.log("connect", "Connected — discovering services…")
|
||||||
try await discoverServices()
|
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()
|
try await authenticate()
|
||||||
|
await diagnosticLog?.log("auth", "Auth success")
|
||||||
isConnected = true
|
isConnected = 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 MainActor.run { onStatusUpdate?("Retrying… (\(attempt)/\(GATTConstants.maxRetries))") }
|
||||||
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 {
|
||||||
throw error
|
throw error
|
||||||
|
|
@ -111,17 +127,23 @@ final class KBeaconProvisioner: NSObject, BeaconProvisioner {
|
||||||
params.append(UInt8(config.advInterval & 0xFF))
|
params.append(UInt8(config.advInterval & 0xFF))
|
||||||
|
|
||||||
// Send CMD_WRITE_PARAMS
|
// 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 writeCmd = Data([CMD.writeParams.rawValue]) + params
|
||||||
let writeResp = try await sendCommand(writeCmd)
|
let writeResp = try await sendCommand(writeCmd)
|
||||||
guard writeResp.first == CMD.writeParams.rawValue else {
|
guard writeResp.first == CMD.writeParams.rawValue else {
|
||||||
throw ProvisionError.writeFailed("Unexpected write response")
|
throw ProvisionError.writeFailed("Unexpected write response")
|
||||||
}
|
}
|
||||||
|
await diagnosticLog?.log("write", "Params written OK — saving to flash…")
|
||||||
|
|
||||||
// Send CMD_SAVE to flash
|
// Send CMD_SAVE to flash
|
||||||
|
await MainActor.run { onStatusUpdate?("Saving to flash…") }
|
||||||
let saveResp = try await sendCommand(Data([CMD.save.rawValue]))
|
let saveResp = try await sendCommand(Data([CMD.save.rawValue]))
|
||||||
guard saveResp.first == CMD.save.rawValue else {
|
guard saveResp.first == CMD.save.rawValue else {
|
||||||
throw ProvisionError.saveFailed
|
throw ProvisionError.saveFailed
|
||||||
}
|
}
|
||||||
|
await MainActor.run { onStatusUpdate?("Config saved ✓") }
|
||||||
|
await diagnosticLog?.log("write", "Save confirmed")
|
||||||
}
|
}
|
||||||
|
|
||||||
func disconnect() {
|
func disconnect() {
|
||||||
|
|
@ -180,14 +202,20 @@ final class KBeaconProvisioner: NSObject, BeaconProvisioner {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func authenticate() async throws {
|
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
|
let cmd = Data([CMD.auth.rawValue]) + password
|
||||||
do {
|
do {
|
||||||
let resp = try await sendCommand(cmd)
|
let resp = try await sendCommand(cmd)
|
||||||
if resp.first == CMD.auth.rawValue && resp.count > 1 && resp[1] == 0x00 {
|
if resp.first == CMD.auth.rawValue && resp.count > 1 && resp[1] == 0x00 {
|
||||||
|
await MainActor.run { onStatusUpdate?("Authenticated ✓") }
|
||||||
return // Auth success
|
return // Auth success
|
||||||
}
|
}
|
||||||
|
await diagnosticLog?.log("auth", "Password \(index + 1) rejected (response: \(resp.map { String(format: "%02X", $0) }.joined()))")
|
||||||
} catch {
|
} catch {
|
||||||
|
await diagnosticLog?.log("auth", "Password \(index + 1) timeout: \(error.localizedDescription)")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -619,6 +619,10 @@ struct ScanView: View {
|
||||||
dxProvisioner.onStatusUpdate = { [weak self] status in
|
dxProvisioner.onStatusUpdate = { [weak self] status in
|
||||||
self?.statusMessage = status
|
self?.statusMessage = status
|
||||||
}
|
}
|
||||||
|
} else if let kbProvisioner = provisioner as? KBeaconProvisioner {
|
||||||
|
kbProvisioner.onStatusUpdate = { [weak self] status in
|
||||||
|
self?.statusMessage = status
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
statusMessage = "Connecting to \(beacon.displayName)…"
|
statusMessage = "Connecting to \(beacon.displayName)…"
|
||||||
|
|
@ -630,9 +634,10 @@ struct ScanView: View {
|
||||||
Task { @MainActor [weak self] in
|
Task { @MainActor [weak self] in
|
||||||
let reason = error?.localizedDescription ?? "beacon timed out"
|
let reason = error?.localizedDescription ?? "beacon timed out"
|
||||||
provisionLog?.log("disconnect", "Unexpected disconnect: \(reason)", isError: true)
|
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,
|
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.provisioningState = .failed
|
||||||
self.errorMessage = "Beacon disconnected: \(reason)"
|
self.errorMessage = "Beacon disconnected: \(reason)"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue