fix: auto-reconnect on disconnect during device info read instead of failing

The beacon sometimes drops BLE connection during the optional MAC address
query (0x30) after auth. Previously this failed with "Disconnected after
auth during device info read". Now we reconnect and skip the MAC read on
retry, going straight to config write. MAC is nice-to-have, not required.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Schwifty 2026-03-21 23:32:14 +00:00
parent 58be00cb38
commit 3d56a1e31d

View file

@ -157,6 +157,7 @@ class BeaconProvisioner: NSObject, ObservableObject {
private var dxSmartWriteIndex = 0
private var provisioningMacAddress: String?
private var awaitingDeviceInfoForProvisioning = false
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
// Read config mode
@ -215,6 +216,7 @@ class BeaconProvisioner: NSObject, ObservableObject {
self.dxSmartWriteIndex = 0
self.provisioningMacAddress = nil
self.awaitingDeviceInfoForProvisioning = false
self.skipDeviceInfoRead = false
self.isTerminating = false
self.connectionRetryCount = 0
self.currentBeacon = beacon
@ -298,6 +300,7 @@ class BeaconProvisioner: NSObject, ObservableObject {
dxSmartWriteIndex = 0
provisioningMacAddress = nil
awaitingDeviceInfoForProvisioning = false
skipDeviceInfoRead = false
isTerminating = false
connectionRetryCount = 0
currentBeacon = nil
@ -406,6 +409,14 @@ class BeaconProvisioner: NSObject, ObservableObject {
/// Read device info (MAC address) before writing config
private func dxSmartReadDeviceInfoBeforeWrite() {
// If we previously disconnected during device info read, skip it entirely
if skipDeviceInfoRead {
DebugLog.shared.log("BLE: Skipping device info read (reconnect after previous disconnect)")
skipDeviceInfoRead = false
dxSmartWriteConfig()
return
}
guard let commandChar = characteristics[BeaconProvisioner.DXSMART_COMMAND_CHAR] else {
DebugLog.shared.log("BLE: FFE2 not found, proceeding without MAC")
dxSmartWriteConfig()
@ -1012,16 +1023,43 @@ extension BeaconProvisioner: CBCentralManagerDelegate {
return
}
// Disconnect during device info read (post-auth, pre-write) beacon may have
// dropped the connection during the MAC address query. We authenticated but
// lost connection before writing config, so this is a failure but a known
// one with a clear retry path, not an unexplained "Unexpected disconnect".
// Disconnect during device info read (post-auth, pre-write) beacon dropped
// connection during the optional MAC address query. Instead of failing, reconnect
// and skip the device info step (MAC is nice-to-have, not required).
if state == .authenticating && awaitingDeviceInfoForProvisioning && dxSmartAuthenticated {
DebugLog.shared.log("BLE: Disconnect during device info read (post-auth) — connection lost before config write, failing with retry prompt")
awaitingDeviceInfoForProvisioning = false
// Connection lost can't write config without it, fail with specific message
fail("Disconnected after auth during device info read — please retry", code: .disconnected)
skipDeviceInfoRead = true // on reconnect, go straight to config write
if connectionRetryCount < BeaconProvisioner.MAX_CONNECTION_RETRIES {
connectionRetryCount += 1
let delay = Double(connectionRetryCount)
progress = "Reconnecting (skip MAC read)..."
DebugLog.shared.log("BLE: Disconnect during device info read — reconnecting (\(connectionRetryCount)/\(BeaconProvisioner.MAX_CONNECTION_RETRIES)), will skip MAC read")
// Reset BLE state for reconnect
dxSmartAuthenticated = false
dxSmartNotifySubscribed = false
dxSmartCommandQueue.removeAll()
dxSmartWriteIndex = 0
characteristics.removeAll()
responseBuffer.removeAll()
state = .connecting
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
guard let self = self, let beacon = self.currentBeacon else { return }
guard self.state == .connecting else { return }
let resolvedPeripheral = self.resolvePeripheral(beacon)
self.peripheral = resolvedPeripheral
resolvedPeripheral.delegate = self
self.centralManager.connect(resolvedPeripheral, options: nil)
}
return
} else {
DebugLog.shared.log("BLE: Disconnect during device info read — max retries exhausted")
fail("Disconnected while reading device information (retries exhausted)", code: .disconnected)
return
}
}
// All other disconnects are unexpected