Compare commits
16 commits
schwifty/c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 66cf65f803 | |||
| 38600193b7 | |||
| 3c41ecb49d | |||
| a08d3db893 | |||
| 640bc32f92 | |||
| 157ab6d008 | |||
| ce81a1a3d8 | |||
| fcf427ee57 | |||
| 4bf4435feb | |||
| f082eeadad | |||
| 3720f496bd | |||
| a82a3697da | |||
| 9ce7b9571a | |||
| 734a18356f | |||
| f0fdb04e0e | |||
| 12048e5c88 |
5 changed files with 107 additions and 118 deletions
|
|
@ -365,8 +365,10 @@ class BeaconProvisioner: NSObject, ObservableObject {
|
||||||
isTerminating = true
|
isTerminating = true
|
||||||
DebugLog.shared.log("BLE: Provisioning success!")
|
DebugLog.shared.log("BLE: Provisioning success!")
|
||||||
state = .success
|
state = .success
|
||||||
disconnectPeripheral()
|
// Signal completion BEFORE disconnecting — the disconnect delegate fires
|
||||||
|
// synchronously and ScanView needs writesCompleted=true before it sees it
|
||||||
completion?(.success(macAddress: nil))
|
completion?(.success(macAddress: nil))
|
||||||
|
disconnectPeripheral()
|
||||||
cleanup()
|
cleanup()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ final class DXSmartProvisioner: NSObject, BeaconProvisioner {
|
||||||
private(set) var isConnected = false
|
private(set) var isConnected = false
|
||||||
private(set) var isFlashing = false // Beacon LED flashing after trigger
|
private(set) var isFlashing = false // Beacon LED flashing after trigger
|
||||||
private var useNewSDK = true // Prefer new SDK, fallback to old
|
private var useNewSDK = true // Prefer new SDK, fallback to old
|
||||||
|
private var disconnected = false // Set true when BLE link drops unexpectedly
|
||||||
var diagnosticLog: ProvisionLog?
|
var diagnosticLog: ProvisionLog?
|
||||||
var bleManager: BLEManager?
|
var bleManager: BLEManager?
|
||||||
|
|
||||||
|
|
@ -76,6 +77,14 @@ final class DXSmartProvisioner: NSObject, BeaconProvisioner {
|
||||||
await diagnosticLog?.log("auth", "Authenticating (trigger + password)…")
|
await diagnosticLog?.log("auth", "Authenticating (trigger + password)…")
|
||||||
try await authenticate()
|
try await authenticate()
|
||||||
await diagnosticLog?.log("auth", "Auth complete — SDK: \(useNewSDK ? "new (FFE2)" : "old (FFE1)")")
|
await diagnosticLog?.log("auth", "Auth complete — SDK: \(useNewSDK ? "new (FFE2)" : "old (FFE1)")")
|
||||||
|
|
||||||
|
// Register for unexpected disconnects so we fail fast instead of
|
||||||
|
// waiting for per-command ACK timeouts (5s × 2 = 10s of dead air).
|
||||||
|
bleManager?.onPeripheralDisconnected = { [weak self] disconnectedPeripheral, error in
|
||||||
|
guard disconnectedPeripheral.identifier == self?.peripheral.identifier else { return }
|
||||||
|
self?.handleUnexpectedDisconnect(error: error)
|
||||||
|
}
|
||||||
|
|
||||||
isConnected = true
|
isConnected = true
|
||||||
isFlashing = true
|
isFlashing = true
|
||||||
return
|
return
|
||||||
|
|
@ -125,6 +134,8 @@ final class DXSmartProvisioner: NSObject, BeaconProvisioner {
|
||||||
}
|
}
|
||||||
|
|
||||||
func disconnect() {
|
func disconnect() {
|
||||||
|
// Unregister disconnect handler so intentional disconnect doesn't trigger error path
|
||||||
|
bleManager?.onPeripheralDisconnected = nil
|
||||||
if peripheral.state == .connected || peripheral.state == .connecting {
|
if peripheral.state == .connected || peripheral.state == .connecting {
|
||||||
centralManager.cancelPeripheralConnection(peripheral)
|
centralManager.cancelPeripheralConnection(peripheral)
|
||||||
}
|
}
|
||||||
|
|
@ -171,8 +182,26 @@ final class DXSmartProvisioner: NSObject, BeaconProvisioner {
|
||||||
]
|
]
|
||||||
|
|
||||||
for (index, (name, packet)) in commands.enumerated() {
|
for (index, (name, packet)) in commands.enumerated() {
|
||||||
|
// Bail immediately if BLE link dropped between commands
|
||||||
|
if disconnected {
|
||||||
|
await diagnosticLog?.log("write", "Aborting — BLE disconnected", isError: true)
|
||||||
|
throw ProvisionError.writeFailed("BLE disconnected during write sequence")
|
||||||
|
}
|
||||||
|
|
||||||
await diagnosticLog?.log("write", "[\(index + 1)/\(commands.count)] \(name) (\(packet.count) bytes)")
|
await diagnosticLog?.log("write", "[\(index + 1)/\(commands.count)] \(name) (\(packet.count) bytes)")
|
||||||
|
|
||||||
|
// SaveConfig (last command) causes beacon MCU to reboot — it never sends an ACK.
|
||||||
|
// Use .withoutResponse so CoreBluetooth fires the bytes immediately into the
|
||||||
|
// BLE radio buffer without waiting for a GATT round-trip. With .withResponse,
|
||||||
|
// the beacon reboots before the ACK arrives, and CoreBluetooth may silently
|
||||||
|
// drop the write — leaving the config unsaved and the beacon still flashing.
|
||||||
|
if name == "SaveConfig" {
|
||||||
|
peripheral.writeValue(packet, for: writeChar, type: .withoutResponse)
|
||||||
|
await diagnosticLog?.log("write", "✅ [\(index + 1)/\(commands.count)] SaveConfig sent — beacon will reboot")
|
||||||
|
await diagnosticLog?.log("write", "✅ All commands written successfully")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Retry each command up to 2 times — beacon BLE stack can be flaky
|
// Retry each command up to 2 times — beacon BLE stack can be flaky
|
||||||
var lastError: Error?
|
var lastError: Error?
|
||||||
for writeAttempt in 1...2 {
|
for writeAttempt in 1...2 {
|
||||||
|
|
@ -193,8 +222,8 @@ final class DXSmartProvisioner: NSObject, BeaconProvisioner {
|
||||||
throw lastError
|
throw lastError
|
||||||
}
|
}
|
||||||
|
|
||||||
// 500ms between commands — beacon needs time to process
|
// 50ms between commands — beacon handles fast writes fine (was 150ms, 300ms, 500ms)
|
||||||
try await Task.sleep(nanoseconds: 500_000_000)
|
try await Task.sleep(nanoseconds: 50_000_000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -270,6 +299,32 @@ final class DXSmartProvisioner: NSObject, BeaconProvisioner {
|
||||||
return packet
|
return packet
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Disconnect Detection
|
||||||
|
|
||||||
|
/// Called when BLE link drops unexpectedly during provisioning.
|
||||||
|
/// Immediately resolves any pending continuations so we fail fast
|
||||||
|
/// instead of waiting for the 5s operationTimeout.
|
||||||
|
private func handleUnexpectedDisconnect(error: Error?) {
|
||||||
|
disconnected = true
|
||||||
|
isConnected = false
|
||||||
|
let disconnectError = ProvisionError.writeFailed("BLE disconnected unexpectedly: \(error?.localizedDescription ?? "unknown")")
|
||||||
|
Task { await diagnosticLog?.log("ble", "⚠️ Unexpected disconnect during provisioning", isError: true) }
|
||||||
|
|
||||||
|
// Cancel any pending write/response continuation immediately
|
||||||
|
if let cont = responseContinuation {
|
||||||
|
responseContinuation = nil
|
||||||
|
cont.resume(throwing: disconnectError)
|
||||||
|
}
|
||||||
|
if let cont = writeContinuation {
|
||||||
|
writeContinuation = nil
|
||||||
|
cont.resume(throwing: disconnectError)
|
||||||
|
}
|
||||||
|
if let cont = connectionContinuation {
|
||||||
|
connectionContinuation = nil
|
||||||
|
cont.resume(throwing: disconnectError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Private Helpers
|
// MARK: - Private Helpers
|
||||||
|
|
||||||
private func connectOnce() async throws {
|
private func connectOnce() async throws {
|
||||||
|
|
@ -329,15 +384,13 @@ final class DXSmartProvisioner: NSObject, BeaconProvisioner {
|
||||||
// Step 1: Trigger — fire and forget (matches Android's WRITE_TYPE_NO_RESPONSE)
|
// Step 1: Trigger — fire and forget (matches Android's WRITE_TYPE_NO_RESPONSE)
|
||||||
if let triggerData = Self.triggerPassword.data(using: .utf8) {
|
if let triggerData = Self.triggerPassword.data(using: .utf8) {
|
||||||
peripheral.writeValue(triggerData, for: ffe3, type: .withoutResponse)
|
peripheral.writeValue(triggerData, for: ffe3, type: .withoutResponse)
|
||||||
try await Task.sleep(nanoseconds: 100_000_000) // 100ms (matches Android SDK timer)
|
try await Task.sleep(nanoseconds: 50_000_000) // 50ms settle
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Auth password — fire and forget
|
// Step 2: Auth password — fire and forget
|
||||||
if let authData = Self.defaultPassword.data(using: .utf8) {
|
if let authData = Self.defaultPassword.data(using: .utf8) {
|
||||||
peripheral.writeValue(authData, for: ffe3, type: .withoutResponse)
|
peripheral.writeValue(authData, for: ffe3, type: .withoutResponse)
|
||||||
// 500ms settle after auth — beacon needs time to enter config mode,
|
try await Task.sleep(nanoseconds: 50_000_000) // 50ms settle
|
||||||
// especially if BLE stack was stressed by prior provisioner attempts
|
|
||||||
try await Task.sleep(nanoseconds: 500_000_000)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -201,8 +201,8 @@ actor APIClient {
|
||||||
guard resp.OK else {
|
guard resp.OK else {
|
||||||
throw APIError.serverError(resp.MESSAGE ?? resp.ERROR ?? "Failed to allocate minor")
|
throw APIError.serverError(resp.MESSAGE ?? resp.ERROR ?? "Failed to allocate minor")
|
||||||
}
|
}
|
||||||
guard let minor = resp.BeaconMinor, minor > 0 else {
|
guard let minor = resp.BeaconMinor, minor >= 0 else {
|
||||||
throw APIError.serverError("API returned invalid minor value: \(resp.BeaconMinor ?? 0). Service point may not be configured correctly.")
|
throw APIError.serverError("API returned invalid minor value: \(resp.BeaconMinor.map(String.init) ?? "nil"). Service point may not be configured correctly.")
|
||||||
}
|
}
|
||||||
return minor
|
return minor
|
||||||
}
|
}
|
||||||
|
|
@ -220,7 +220,7 @@ actor APIClient {
|
||||||
uuid: String,
|
uuid: String,
|
||||||
major: Int,
|
major: Int,
|
||||||
minor: Int,
|
minor: Int,
|
||||||
macAddress: String?,
|
hardwareId: String,
|
||||||
beaconType: String,
|
beaconType: String,
|
||||||
token: String
|
token: String
|
||||||
) async throws {
|
) async throws {
|
||||||
|
|
@ -230,9 +230,9 @@ actor APIClient {
|
||||||
"UUID": uuid,
|
"UUID": uuid,
|
||||||
"Major": major,
|
"Major": major,
|
||||||
"Minor": minor,
|
"Minor": minor,
|
||||||
|
"HardwareId": hardwareId,
|
||||||
"BeaconType": beaconType
|
"BeaconType": beaconType
|
||||||
]
|
]
|
||||||
if let mac = macAddress { body["MacAddress"] = mac }
|
|
||||||
let data = try await post(path: "/beacon-sharding/register_beacon_hardware.php", body: body, token: token, businessId: businessId)
|
let data = try await post(path: "/beacon-sharding/register_beacon_hardware.php", body: body, token: token, businessId: businessId)
|
||||||
let resp = try JSONDecoder().decode(OKResponse.self, from: data)
|
let resp = try JSONDecoder().decode(OKResponse.self, from: data)
|
||||||
guard resp.OK else {
|
guard resp.OK else {
|
||||||
|
|
@ -241,12 +241,13 @@ actor APIClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
func verifyBeaconBroadcast(
|
func verifyBeaconBroadcast(
|
||||||
|
hardwareId: String,
|
||||||
uuid: String,
|
uuid: String,
|
||||||
major: Int,
|
major: Int,
|
||||||
minor: Int,
|
minor: Int,
|
||||||
token: String
|
token: String
|
||||||
) async throws {
|
) async throws {
|
||||||
let body: [String: Any] = ["UUID": uuid, "Major": major, "Minor": minor]
|
let body: [String: Any] = ["HardwareId": hardwareId, "UUID": uuid, "Major": major, "Minor": minor]
|
||||||
let data = try await post(path: "/beacon-sharding/verify_beacon_broadcast.php", body: body, token: token)
|
let data = try await post(path: "/beacon-sharding/verify_beacon_broadcast.php", body: body, token: token)
|
||||||
let resp = try JSONDecoder().decode(OKResponse.self, from: data)
|
let resp = try JSONDecoder().decode(OKResponse.self, from: data)
|
||||||
guard resp.OK else {
|
guard resp.OK else {
|
||||||
|
|
|
||||||
|
|
@ -144,11 +144,8 @@ final class BLEManager: NSObject, ObservableObject {
|
||||||
|
|
||||||
// CP-28 also advertises FFF0 on some firmware
|
// CP-28 also advertises FFF0 on some firmware
|
||||||
if serviceStrings.contains(where: { $0.hasPrefix("0000FFF0") }) {
|
if serviceStrings.contains(where: { $0.hasPrefix("0000FFF0") }) {
|
||||||
if deviceName.contains("cp28") || deviceName.contains("cp-28") ||
|
// Any FFF0 device is likely CP-28 — don't filter by name
|
||||||
deviceName.contains("dx") || deviceName.contains("pddaxlque") ||
|
return .dxsmart
|
||||||
deviceName.isEmpty {
|
|
||||||
return .dxsmart
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -163,10 +160,11 @@ final class BLEManager: NSObject, ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Device name patterns for CP-28
|
// 3. Device name patterns for CP-28 (includes "payfrit" — our own provisioned name)
|
||||||
if deviceName.contains("cp28") || deviceName.contains("cp-28") ||
|
if deviceName.contains("cp28") || deviceName.contains("cp-28") ||
|
||||||
deviceName.contains("dx-cp") || deviceName.contains("dx-smart") ||
|
deviceName.contains("dx-cp") || deviceName.contains("dx-smart") ||
|
||||||
deviceName.contains("dxsmart") || deviceName.contains("pddaxlque") {
|
deviceName.contains("dxsmart") || deviceName.contains("pddaxlque") ||
|
||||||
|
deviceName.contains("payfrit") {
|
||||||
return .dxsmart
|
return .dxsmart
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -229,10 +227,8 @@ extension BLEManager: CBCentralManagerDelegate {
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|
||||||
// Only show CP-28 beacons — everything else is filtered out
|
// Detect beacon type — default to .dxsmart so ALL devices show up in scan
|
||||||
guard let type = self.detectBeaconType(name: name, serviceUUIDs: serviceUUIDs, manufacturerData: mfgData) else {
|
let type = self.detectBeaconType(name: name, serviceUUIDs: serviceUUIDs, manufacturerData: mfgData) ?? .dxsmart
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if let idx = self.discoveredBeacons.firstIndex(where: { $0.id == peripheralId }) {
|
if let idx = self.discoveredBeacons.firstIndex(where: { $0.id == peripheralId }) {
|
||||||
self.discoveredBeacons[idx].rssi = rssiValue
|
self.discoveredBeacons[idx].rssi = rssiValue
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ struct ScanView: View {
|
||||||
// Provisioning flow
|
// Provisioning flow
|
||||||
@State private var selectedBeacon: DiscoveredBeacon?
|
@State private var selectedBeacon: DiscoveredBeacon?
|
||||||
@State private var provisioningState: ProvisioningState = .idle
|
@State private var provisioningState: ProvisioningState = .idle
|
||||||
|
@State private var writesCompleted = false
|
||||||
@State private var statusMessage = ""
|
@State private var statusMessage = ""
|
||||||
@State private var errorMessage: String?
|
@State private var errorMessage: String?
|
||||||
@State private var showQRScanner = false
|
@State private var showQRScanner = false
|
||||||
|
|
@ -208,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)
|
||||||
|
|
@ -318,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
|
||||||
|
|
||||||
|
|
@ -613,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 {
|
||||||
|
|
@ -628,17 +587,19 @@ struct ScanView: View {
|
||||||
bleManager.onPeripheralDisconnected = { [weak provisionLog] peripheral, error in
|
bleManager.onPeripheralDisconnected = { [weak provisionLog] peripheral, error in
|
||||||
if peripheral.identifier == beacon.peripheral.identifier {
|
if peripheral.identifier == beacon.peripheral.identifier {
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
let reason = error?.localizedDescription ?? "beacon timed out"
|
|
||||||
provisionLog?.log("disconnect", "Unexpected disconnect: \(reason)", isError: true)
|
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
// CP-28: disconnect during .connected is expected — beacon keeps
|
let reason = error?.localizedDescription ?? "beacon timed out"
|
||||||
// flashing after BLE drops. We'll reconnect when user taps Write Config.
|
|
||||||
if self.provisioningState == .connected {
|
// Writes already finished — beacon rebooted after SaveConfig, this is expected
|
||||||
provisionLog?.log("disconnect", "CP-28 idle disconnect — beacon still flashing, ignoring")
|
if self.writesCompleted {
|
||||||
|
provisionLog?.log("disconnect", "Post-write disconnect (expected — beacon rebooted after save)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// For all other active states, treat disconnect as failure
|
|
||||||
if self.provisioningState == .connecting || self.provisioningState == .connected ||
|
provisionLog?.log("disconnect", "Unexpected disconnect: \(reason)", isError: true)
|
||||||
|
|
||||||
|
// For all active states, treat disconnect as failure
|
||||||
|
if self.provisioningState == .connecting ||
|
||||||
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)"
|
||||||
|
|
@ -650,43 +611,17 @@ 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
|
|
||||||
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
|
||||||
|
|
||||||
|
// Brief settle after SaveConfig before dropping the BLE link.
|
||||||
|
try? await Task.sleep(nanoseconds: 50_000_000)
|
||||||
provisioner.disconnect()
|
provisioner.disconnect()
|
||||||
|
|
||||||
try await APIClient.shared.registerBeaconHardware(
|
try await APIClient.shared.registerBeaconHardware(
|
||||||
|
|
@ -695,7 +630,7 @@ struct ScanView: View {
|
||||||
uuid: ns.uuid,
|
uuid: ns.uuid,
|
||||||
major: ns.major,
|
major: ns.major,
|
||||||
minor: Int(config.minor),
|
minor: Int(config.minor),
|
||||||
macAddress: nil,
|
hardwareId: selectedBeacon?.id.uuidString ?? "unknown-ios",
|
||||||
beaconType: BeaconType.dxsmart.rawValue,
|
beaconType: BeaconType.dxsmart.rawValue,
|
||||||
token: token
|
token: token
|
||||||
)
|
)
|
||||||
|
|
@ -704,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,
|
||||||
|
|
@ -728,7 +665,7 @@ struct ScanView: View {
|
||||||
uuid: ns.uuid,
|
uuid: ns.uuid,
|
||||||
major: ns.major,
|
major: ns.major,
|
||||||
minor: Int(config.minor),
|
minor: Int(config.minor),
|
||||||
macAddress: nil,
|
hardwareId: selectedBeacon?.id.uuidString ?? "unknown-ios",
|
||||||
beaconType: selectedBeacon?.type.rawValue ?? "Unknown",
|
beaconType: selectedBeacon?.type.rawValue ?? "Unknown",
|
||||||
token: token
|
token: token
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue