fix: resume writing from saved position on BLE disconnect instead of restarting
Previously, when the beacon disconnected mid-write, the reconnect handler cleared the entire command queue and reset writeIndex to 0, causing all 24 commands to be re-sent from scratch on every reconnect. This could confuse the beacon firmware with duplicate config writes and wasted reconnect cycles. Changes: - On disconnect during writing, PRESERVE command queue and write index - After reconnect + re-auth, resume from the last command instead of rebuilding - Increase MAX_DISCONNECT_RETRIES from 3 to 5 (resume is lightweight) - Increase inter-command delay from 150ms to 300ms for firmware breathing room - Increase global timeout from 45s to 90s to accommodate more retries - Add resumeWriteAfterDisconnect flag to control post-auth flow Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
292821622e
commit
aeab67ea64
1 changed files with 34 additions and 10 deletions
|
|
@ -159,6 +159,7 @@ class BeaconProvisioner: NSObject, ObservableObject {
|
||||||
private var awaitingDeviceInfoForProvisioning = false
|
private var awaitingDeviceInfoForProvisioning = false
|
||||||
private var skipDeviceInfoRead = false // set after disconnect during device info — skip MAC read on reconnect
|
private var skipDeviceInfoRead = false // set after disconnect during device info — skip MAC read on reconnect
|
||||||
private var isTerminating = false // guards against re-entrant disconnect handling
|
private var isTerminating = false // guards against re-entrant disconnect handling
|
||||||
|
private var resumeWriteAfterDisconnect = false // when true, skip queue rebuild on reconnect and resume from saved index
|
||||||
|
|
||||||
// Read config mode
|
// Read config mode
|
||||||
private enum OperationMode { case provisioning, readingConfig }
|
private enum OperationMode { case provisioning, readingConfig }
|
||||||
|
|
@ -182,7 +183,7 @@ class BeaconProvisioner: NSObject, ObservableObject {
|
||||||
private var disconnectRetryCount = 0
|
private var disconnectRetryCount = 0
|
||||||
private static let MAX_CONNECTION_RETRIES = 3
|
private static let MAX_CONNECTION_RETRIES = 3
|
||||||
private static let MAX_DEVICE_INFO_RETRIES = 2
|
private static let MAX_DEVICE_INFO_RETRIES = 2
|
||||||
private static let MAX_DISCONNECT_RETRIES = 3
|
private static let MAX_DISCONNECT_RETRIES = 5
|
||||||
private var currentBeacon: DiscoveredBeacon?
|
private var currentBeacon: DiscoveredBeacon?
|
||||||
|
|
||||||
// Per-write timeout (matches Android's 5-second per-write timeout)
|
// Per-write timeout (matches Android's 5-second per-write timeout)
|
||||||
|
|
@ -230,6 +231,7 @@ class BeaconProvisioner: NSObject, ObservableObject {
|
||||||
self.awaitingDeviceInfoForProvisioning = false
|
self.awaitingDeviceInfoForProvisioning = false
|
||||||
self.skipDeviceInfoRead = false
|
self.skipDeviceInfoRead = false
|
||||||
self.isTerminating = false
|
self.isTerminating = false
|
||||||
|
self.resumeWriteAfterDisconnect = false
|
||||||
self.connectionRetryCount = 0
|
self.connectionRetryCount = 0
|
||||||
self.deviceInfoRetryCount = 0
|
self.deviceInfoRetryCount = 0
|
||||||
self.disconnectRetryCount = 0
|
self.disconnectRetryCount = 0
|
||||||
|
|
@ -241,8 +243,8 @@ class BeaconProvisioner: NSObject, ObservableObject {
|
||||||
|
|
||||||
centralManager.connect(resolvedPeripheral, options: nil)
|
centralManager.connect(resolvedPeripheral, options: nil)
|
||||||
|
|
||||||
// Timeout after 45 seconds (increased from 30s to accommodate 3 disconnect retries with backoff)
|
// Timeout after 90 seconds (increased to accommodate 5 disconnect retries with resume)
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 45) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 90) { [weak self] in
|
||||||
if self?.state != .success && self?.state != .idle {
|
if self?.state != .success && self?.state != .idle {
|
||||||
self?.fail("Connection timeout", code: .connectionTimeout)
|
self?.fail("Connection timeout", code: .connectionTimeout)
|
||||||
}
|
}
|
||||||
|
|
@ -320,6 +322,7 @@ class BeaconProvisioner: NSObject, ObservableObject {
|
||||||
awaitingDeviceInfoForProvisioning = false
|
awaitingDeviceInfoForProvisioning = false
|
||||||
skipDeviceInfoRead = false
|
skipDeviceInfoRead = false
|
||||||
isTerminating = false
|
isTerminating = false
|
||||||
|
resumeWriteAfterDisconnect = false
|
||||||
connectionRetryCount = 0
|
connectionRetryCount = 0
|
||||||
deviceInfoRetryCount = 0
|
deviceInfoRetryCount = 0
|
||||||
disconnectRetryCount = 0
|
disconnectRetryCount = 0
|
||||||
|
|
@ -600,7 +603,7 @@ class BeaconProvisioner: NSObject, ObservableObject {
|
||||||
// Non-fatal commands (first 6) — skip and continue
|
// Non-fatal commands (first 6) — skip and continue
|
||||||
DebugLog.shared.log("BLE: Write timeout for non-fatal command \(current)/\(total) — skipping")
|
DebugLog.shared.log("BLE: Write timeout for non-fatal command \(current)/\(total) — skipping")
|
||||||
self.dxSmartWriteIndex += 1
|
self.dxSmartWriteIndex += 1
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
|
||||||
self?.dxSmartSendNextCommand()
|
self?.dxSmartSendNextCommand()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1121,20 +1124,31 @@ extension BeaconProvisioner: CBCentralManagerDelegate {
|
||||||
let isActivePhase = (state == .discoveringServices || state == .authenticating || state == .writing || state == .verifying)
|
let isActivePhase = (state == .discoveringServices || state == .authenticating || state == .writing || state == .verifying)
|
||||||
if isActivePhase && disconnectRetryCount < BeaconProvisioner.MAX_DISCONNECT_RETRIES {
|
if isActivePhase && disconnectRetryCount < BeaconProvisioner.MAX_DISCONNECT_RETRIES {
|
||||||
disconnectRetryCount += 1
|
disconnectRetryCount += 1
|
||||||
DebugLog.shared.log("BLE: Disconnect during \(state) — reconnecting (attempt \(disconnectRetryCount)/\(BeaconProvisioner.MAX_DISCONNECT_RETRIES))")
|
let wasWriting = (state == .writing && !dxSmartCommandQueue.isEmpty)
|
||||||
|
DebugLog.shared.log("BLE: Disconnect during \(state) — reconnecting (attempt \(disconnectRetryCount)/\(BeaconProvisioner.MAX_DISCONNECT_RETRIES)) wasWriting=\(wasWriting) writeIdx=\(dxSmartWriteIndex)")
|
||||||
progress = "Beacon disconnected, reconnecting (\(disconnectRetryCount)/\(BeaconProvisioner.MAX_DISCONNECT_RETRIES))..."
|
progress = "Beacon disconnected, reconnecting (\(disconnectRetryCount)/\(BeaconProvisioner.MAX_DISCONNECT_RETRIES))..."
|
||||||
|
|
||||||
// Reset connection state for clean reconnect
|
// Reset connection-level state, but PRESERVE command queue and write index
|
||||||
|
// so we can resume from where we left off instead of starting over
|
||||||
dxSmartAuthenticated = false
|
dxSmartAuthenticated = false
|
||||||
dxSmartNotifySubscribed = false
|
dxSmartNotifySubscribed = false
|
||||||
dxSmartCommandQueue.removeAll()
|
|
||||||
dxSmartWriteIndex = 0
|
|
||||||
passwordIndex = 0
|
passwordIndex = 0
|
||||||
characteristics.removeAll()
|
characteristics.removeAll()
|
||||||
responseBuffer.removeAll()
|
responseBuffer.removeAll()
|
||||||
|
|
||||||
|
if wasWriting {
|
||||||
|
// Resume mode: keep the command queue and write index intact
|
||||||
|
resumeWriteAfterDisconnect = true
|
||||||
|
DebugLog.shared.log("BLE: Will resume writing from command \(dxSmartWriteIndex + 1)/\(dxSmartCommandQueue.count) after reconnect")
|
||||||
|
} else {
|
||||||
|
// Full reset for non-writing phases
|
||||||
|
dxSmartCommandQueue.removeAll()
|
||||||
|
dxSmartWriteIndex = 0
|
||||||
|
resumeWriteAfterDisconnect = false
|
||||||
|
}
|
||||||
state = .connecting
|
state = .connecting
|
||||||
|
|
||||||
let delay = Double(disconnectRetryCount) + 2.0 // 3s, 4s, 5s backoff — give BLE time to settle
|
let delay = Double(disconnectRetryCount) + 2.0 // 3s, 4s, 5s, 6s, 7s backoff — give BLE time to settle
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
||||||
guard let self = self, let beacon = self.currentBeacon else { return }
|
guard let self = self, let beacon = self.currentBeacon else { return }
|
||||||
guard self.state == .connecting else { return }
|
guard self.state == .connecting else { return }
|
||||||
|
|
@ -1310,6 +1324,16 @@ extension BeaconProvisioner: CBPeripheralDelegate {
|
||||||
dxSmartAuthenticated = true
|
dxSmartAuthenticated = true
|
||||||
if operationMode == .readingConfig {
|
if operationMode == .readingConfig {
|
||||||
dxSmartReadQueryAfterAuth()
|
dxSmartReadQueryAfterAuth()
|
||||||
|
} else if resumeWriteAfterDisconnect {
|
||||||
|
// Reconnected after disconnect during writing — resume from saved position
|
||||||
|
resumeWriteAfterDisconnect = false
|
||||||
|
state = .writing
|
||||||
|
DebugLog.shared.log("BLE: Resuming write from command \(dxSmartWriteIndex + 1)/\(dxSmartCommandQueue.count) after reconnect")
|
||||||
|
progress = "Resuming config write..."
|
||||||
|
// Small delay to let the BLE stack settle before resuming writes
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
||||||
|
self?.dxSmartSendNextCommand()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Read device info first to get MAC address, then write config
|
// Read device info first to get MAC address, then write config
|
||||||
dxSmartReadDeviceInfoBeforeWrite()
|
dxSmartReadDeviceInfoBeforeWrite()
|
||||||
|
|
@ -1329,7 +1353,7 @@ extension BeaconProvisioner: CBPeripheralDelegate {
|
||||||
DebugLog.shared.log("BLE: Device info query sent, waiting for response...")
|
DebugLog.shared.log("BLE: Device info query sent, waiting for response...")
|
||||||
} else {
|
} else {
|
||||||
dxSmartWriteIndex += 1
|
dxSmartWriteIndex += 1
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
|
||||||
self?.dxSmartSendNextCommand()
|
self?.dxSmartSendNextCommand()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue