Compare commits

..

No commits in common. "ed9a57a938b9165a35c8f873a3fc2bb25a3b200c" and "349dab1b75d36b2329d100de71dda5338664b942" have entirely different histories.

2 changed files with 34 additions and 88 deletions

View file

@ -55,24 +55,15 @@ 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)")")
@ -83,7 +74,6 @@ 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 {

View file

@ -327,10 +327,10 @@ struct ScanView: View {
.foregroundStyle(Color.payfritGreen)
.modifier(PulseEffectModifier())
Text("Connected — Beacon is Flashing")
Text("Beacon is Flashing")
.font(.title2.bold())
Text("Confirm the beacon LED is flashing, then tap Write Config to program it.\n\nThe beacon will timeout if you wait too long.")
Text("Confirm the beacon LED is flashing, then tap Write Config to program it.")
.font(.subheadline)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
@ -355,11 +355,6 @@ struct ScanView: View {
}
.foregroundStyle(.secondary)
// Show diagnostic log
if !provisionLog.entries.isEmpty {
diagnosticLogView
}
Spacer()
}
}
@ -376,58 +371,7 @@ 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)
}
}
@ -467,7 +411,33 @@ struct ScanView: View {
// Diagnostic log
if !provisionLog.entries.isEmpty {
diagnosticLogView
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) {
@ -586,7 +556,7 @@ struct ScanView: View {
let token = appState.token else { return }
provisioningState = .connecting
statusMessage = "Allocating beacon config"
statusMessage = "Connecting to \(beacon.displayName)"
errorMessage = nil
provisionLog.reset()
provisionLog.log("init", "Starting provisioning: \(beacon.displayName) (\(beacon.type.rawValue)) RSSI:\(beacon.rssi)")
@ -614,34 +584,20 @@ struct ScanView: View {
// Create appropriate provisioner
let provisioner = makeProvisioner(for: beacon)
// 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)"
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 [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)"
}
Task { @MainActor in
provisionLog?.log("disconnect", "Unexpected disconnect: \(error?.localizedDescription ?? "no error")", isError: true)
}
}
}
try await provisioner.connect()
provisionLog.log("connect", "Connected and authenticated successfully")
provisionLog.log("connect", "Connected successfully")
// DXSmart: stop at connected state, wait for user to confirm flashing
if beacon.type == .dxsmart {