fix: auto-write config immediately after connect — no manual tap needed

Removes the two-phase flow where the user had to confirm beacon flashing
and tap "Write Config". Now goes straight from connect → write → register
in one shot. The dxsmartConnectedView UI is removed.
This commit is contained in:
Schwifty 2026-03-23 04:16:04 +00:00
parent 3c41ecb49d
commit 38600193b7

View file

@ -209,8 +209,8 @@ struct ScanView: View {
progressView(title: "Connecting…", message: statusMessage) progressView(title: "Connecting…", message: statusMessage)
case .connected: case .connected:
// DXSmart: beacon is flashing, show write button // Legacy auto-write skips this state now
dxsmartConnectedView progressView(title: "Connected…", message: statusMessage)
case .writing: case .writing:
progressView(title: "Writing Config…", message: statusMessage) progressView(title: "Writing Config…", message: statusMessage)
@ -319,51 +319,7 @@ struct ScanView: View {
// MARK: - DXSmart Connected View // MARK: - DXSmart Connected View
private var dxsmartConnectedView: some View { // dxsmartConnectedView removed auto-write skips the manual confirmation step
VStack(spacing: 24) {
Spacer()
Image(systemName: "light.beacon.max")
.font(.system(size: 64))
.foregroundStyle(Color.payfritGreen)
.modifier(PulseEffectModifier())
Text("Connected — 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.")
.font(.subheadline)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, 32)
Button {
Task { await writeConfigToConnectedBeacon() }
} label: {
HStack {
Image(systemName: "arrow.down.doc")
Text("Write Config")
}
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.tint(Color.payfritGreen)
.controlSize(.large)
.padding(.horizontal, 32)
Button("Cancel") {
cancelProvisioning()
}
.foregroundStyle(.secondary)
// Show diagnostic log
if !provisionLog.entries.isEmpty {
diagnosticLogView
}
Spacer()
}
}
// MARK: - Progress / Success / Failed Views // MARK: - Progress / Success / Failed Views
@ -614,6 +570,8 @@ struct ScanView: View {
// Create appropriate provisioner // Create appropriate provisioner
let provisioner = makeProvisioner(for: beacon) let provisioner = makeProvisioner(for: beacon)
pendingProvisioner = provisioner
pendingConfig = config
// Wire up real-time status updates from provisioner // Wire up real-time status updates from provisioner
if let dxProvisioner = provisioner as? DXSmartProvisioner { if let dxProvisioner = provisioner as? DXSmartProvisioner {
@ -640,14 +598,8 @@ struct ScanView: View {
provisionLog?.log("disconnect", "Unexpected disconnect: \(reason)", isError: true) provisionLog?.log("disconnect", "Unexpected disconnect: \(reason)", isError: true)
// CP-28: disconnect during .connected is expected beacon keeps // For all active states, treat disconnect as failure
// flashing after BLE drops. We'll reconnect when user taps Write Config. if self.provisioningState == .connecting ||
if self.provisioningState == .connected {
provisionLog?.log("disconnect", "CP-28 idle disconnect — beacon still flashing, ignoring")
return
}
// For all other active states, treat disconnect as failure
if self.provisioningState == .connecting || self.provisioningState == .connected ||
self.provisioningState == .writing || self.provisioningState == .verifying { self.provisioningState == .writing || self.provisioningState == .verifying {
self.provisioningState = .failed self.provisioningState = .failed
self.errorMessage = "Beacon disconnected: \(reason)" self.errorMessage = "Beacon disconnected: \(reason)"
@ -659,48 +611,16 @@ struct ScanView: View {
try await provisioner.connect() try await provisioner.connect()
provisionLog.log("connect", "Connected and authenticated successfully") provisionLog.log("connect", "Connected and authenticated successfully")
// CP-28: stop at connected state, wait for user to confirm flashing // Auto-fire write immediately no pause needed
provisioningState = .connected provisioningState = .writing
pendingConfig = config writesCompleted = false
pendingProvisioner = provisioner statusMessage = "Writing config to DX-Smart…"
provisionLog.log("write", "Auto-writing config (no user tap needed)…")
} catch {
provisionLog.log("error", "Provisioning failed after \(provisionLog.elapsed): \(error.localizedDescription)", isError: true)
provisioningState = .failed
errorMessage = error.localizedDescription
}
}
// Store for DXSmart two-phase flow
@State private var pendingConfig: BeaconConfig?
@State private var pendingProvisioner: (any BeaconProvisioner)?
private func writeConfigToConnectedBeacon() async {
guard let config = pendingConfig,
let provisioner = pendingProvisioner,
let sp = selectedServicePoint,
let ns = namespace,
let token = appState.token else { return }
provisioningState = .writing
writesCompleted = false
statusMessage = "Writing config to DX-Smart…"
do {
// Reconnect if the beacon dropped BLE during the "confirm flashing" wait
if !provisioner.isConnected {
provisionLog.log("write", "Beacon disconnected while waiting — reconnecting…")
statusMessage = "Reconnecting to beacon…"
try await provisioner.connect()
provisionLog.log("write", "Reconnected — writing config…")
statusMessage = "Writing config to DX-Smart…"
}
try await provisioner.writeConfig(config) try await provisioner.writeConfig(config)
writesCompleted = true writesCompleted = true
// Give the beacon 200ms to process SaveConfig before dropping the BLE link. // Give the beacon 200ms to process SaveConfig before dropping the BLE link.
// The beacon MCU needs the connection terminated to finalize its reboot/save cycle.
try? await Task.sleep(nanoseconds: 200_000_000) try? await Task.sleep(nanoseconds: 200_000_000)
provisioner.disconnect() provisioner.disconnect()
@ -719,14 +639,16 @@ struct ScanView: View {
statusMessage = "\(sp.name) — DX-Smart\nUUID: \(config.formattedUUID.prefix(13))\nMajor: \(config.major) Minor: \(config.minor)" statusMessage = "\(sp.name) — DX-Smart\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
} }
pendingConfig = nil
pendingProvisioner = nil
} }
// Kept for cancel/reset and registerAnywayAfterFailure fallback
@State private var pendingConfig: BeaconConfig?
@State private var pendingProvisioner: (any BeaconProvisioner)?
private func registerAnywayAfterFailure() async { private func registerAnywayAfterFailure() async {
guard let sp = selectedServicePoint, guard let sp = selectedServicePoint,
let ns = namespace, let ns = namespace,