Compare commits
2 commits
349dab1b75
...
ed9a57a938
| Author | SHA1 | Date | |
|---|---|---|---|
| ed9a57a938 | |||
| c3f2b4faab |
2 changed files with 88 additions and 34 deletions
|
|
@ -55,15 +55,24 @@ final class DXSmartProvisioner: NSObject, BeaconProvisioner {
|
|||
|
||||
// MARK: - BeaconProvisioner
|
||||
|
||||
/// Status callback — provisioner reports what phase it's in so UI can update
|
||||
var onStatusUpdate: ((String) -> Void)?
|
||||
|
||||
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 {
|
||||
let attemptLabel = GATTConstants.maxRetries > 1 ? " (attempt \(attempt)/\(GATTConstants.maxRetries))" : ""
|
||||
await MainActor.run { onStatusUpdate?("Connecting to beacon…\(attemptLabel)") }
|
||||
await diagnosticLog?.log("connect", "Connecting (timeout: \(GATTConstants.connectionTimeout)s)…")
|
||||
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 — FFE1:\(ffe1Char != nil) FFE2:\(ffe2Char != nil) FFE3:\(ffe3Char != nil)")
|
||||
|
||||
await MainActor.run { onStatusUpdate?("Authenticating…") }
|
||||
await diagnosticLog?.log("auth", "Authenticating (trigger + password)…")
|
||||
try await authenticate()
|
||||
await diagnosticLog?.log("auth", "Auth complete — SDK: \(useNewSDK ? "new (FFE2)" : "old (FFE1)")")
|
||||
|
|
@ -74,6 +83,7 @@ final class DXSmartProvisioner: NSObject, BeaconProvisioner {
|
|||
await diagnosticLog?.log("connect", "Attempt \(attempt) failed: \(error.localizedDescription)", isError: true)
|
||||
disconnect()
|
||||
if attempt < GATTConstants.maxRetries {
|
||||
await MainActor.run { onStatusUpdate?("Retrying… (\(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 {
|
||||
|
|
|
|||
|
|
@ -327,10 +327,10 @@ struct ScanView: View {
|
|||
.foregroundStyle(Color.payfritGreen)
|
||||
.modifier(PulseEffectModifier())
|
||||
|
||||
Text("Beacon is Flashing")
|
||||
Text("Connected — Beacon is Flashing")
|
||||
.font(.title2.bold())
|
||||
|
||||
Text("Confirm the beacon LED is flashing, then tap Write Config to program it.")
|
||||
Text("Confirm the beacon LED is flashing, then tap Write Config to program it.\n\nThe beacon will timeout if you wait too long.")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
|
@ -355,6 +355,11 @@ struct ScanView: View {
|
|||
}
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
// Show diagnostic log
|
||||
if !provisionLog.entries.isEmpty {
|
||||
diagnosticLogView
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
|
@ -371,7 +376,58 @@ struct ScanView: View {
|
|||
Text(message)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
// Show live diagnostic log during connecting/writing
|
||||
if !provisionLog.entries.isEmpty {
|
||||
diagnosticLogView
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Cancel") {
|
||||
cancelProvisioning()
|
||||
}
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
}
|
||||
|
||||
/// Reusable diagnostic log view
|
||||
private var diagnosticLogView: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Text("Log (\(provisionLog.elapsed))")
|
||||
.font(.caption.bold())
|
||||
Spacer()
|
||||
ShareLink(item: provisionLog.fullText) {
|
||||
Label("Share", systemImage: "square.and.arrow.up")
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
|
||||
ScrollViewReader { proxy in
|
||||
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)
|
||||
.id(entry.id)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
.onChange(of: provisionLog.entries.count) { _ in
|
||||
if let last = provisionLog.entries.last {
|
||||
proxy.scrollTo(last.id, anchor: .bottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: 160)
|
||||
.background(Color(.systemGray6))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -411,33 +467,7 @@ struct ScanView: View {
|
|||
|
||||
// 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)
|
||||
}
|
||||
diagnosticLogView
|
||||
}
|
||||
|
||||
HStack(spacing: 16) {
|
||||
|
|
@ -556,7 +586,7 @@ struct ScanView: View {
|
|||
let token = appState.token else { return }
|
||||
|
||||
provisioningState = .connecting
|
||||
statusMessage = "Connecting to \(beacon.displayName)…"
|
||||
statusMessage = "Allocating beacon config…"
|
||||
errorMessage = nil
|
||||
provisionLog.reset()
|
||||
provisionLog.log("init", "Starting provisioning: \(beacon.displayName) (\(beacon.type.rawValue)) RSSI:\(beacon.rssi)")
|
||||
|
|
@ -584,20 +614,34 @@ struct ScanView: View {
|
|||
// Create appropriate provisioner
|
||||
let provisioner = makeProvisioner(for: beacon)
|
||||
|
||||
statusMessage = "Authenticating with \(beacon.type.rawValue)…"
|
||||
// Wire up real-time status updates from provisioner
|
||||
if let dxProvisioner = provisioner as? DXSmartProvisioner {
|
||||
dxProvisioner.onStatusUpdate = { [weak self] status in
|
||||
self?.statusMessage = status
|
||||
}
|
||||
}
|
||||
|
||||
statusMessage = "Connecting to \(beacon.displayName)…"
|
||||
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)
|
||||
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 let self = self,
|
||||
self.provisioningState == .connecting || self.provisioningState == .connected {
|
||||
self.provisioningState = .failed
|
||||
self.errorMessage = "Beacon disconnected: \(reason)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try await provisioner.connect()
|
||||
provisionLog.log("connect", "Connected successfully")
|
||||
provisionLog.log("connect", "Connected and authenticated successfully")
|
||||
|
||||
// DXSmart: stop at connected state, wait for user to confirm flashing
|
||||
if beacon.type == .dxsmart {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue