Compare commits

..

4 commits

Author SHA1 Message Date
720c560760 Merge remote-tracking branch 'origin/schwifty/fix-device-info-retry-counter' 2026-03-21 23:37:28 +00:00
07c5a22315 Merge remote-tracking branch 'origin/schwifty/fix-device-info-disconnect-retry' 2026-03-21 23:37:26 +00:00
64e3684209 fix: use dedicated retry counter for device info disconnect
The device info disconnect handler was sharing connectionRetryCount with
the initial connection retry logic. If earlier connection attempts burned
through retries, the device info handler had zero retries left and
immediately hit "retries exhausted" — causing the "Disconnected while
reading device information" error John reported.

Now uses a separate deviceInfoRetryCount (max 2) so device info retries
are independent of connection retries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 23:35:15 +00:00
3d56a1e31d 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>
2026-03-21 23:32:14 +00:00

View file

@ -157,6 +157,7 @@ class BeaconProvisioner: NSObject, ObservableObject {
private var dxSmartWriteIndex = 0 private var dxSmartWriteIndex = 0
private var provisioningMacAddress: String? private var provisioningMacAddress: String?
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 isTerminating = false // guards against re-entrant disconnect handling private var isTerminating = false // guards against re-entrant disconnect handling
// Read config mode // Read config mode
@ -177,7 +178,9 @@ class BeaconProvisioner: NSObject, ObservableObject {
// Connection retry state // Connection retry state
private var connectionRetryCount = 0 private var connectionRetryCount = 0
private var deviceInfoRetryCount = 0
private static let MAX_CONNECTION_RETRIES = 3 private static let MAX_CONNECTION_RETRIES = 3
private static let MAX_DEVICE_INFO_RETRIES = 2
private var currentBeacon: DiscoveredBeacon? private var currentBeacon: DiscoveredBeacon?
override init() { override init() {
@ -215,8 +218,10 @@ class BeaconProvisioner: NSObject, ObservableObject {
self.dxSmartWriteIndex = 0 self.dxSmartWriteIndex = 0
self.provisioningMacAddress = nil self.provisioningMacAddress = nil
self.awaitingDeviceInfoForProvisioning = false self.awaitingDeviceInfoForProvisioning = false
self.skipDeviceInfoRead = false
self.isTerminating = false self.isTerminating = false
self.connectionRetryCount = 0 self.connectionRetryCount = 0
self.deviceInfoRetryCount = 0
self.currentBeacon = beacon self.currentBeacon = beacon
state = .connecting state = .connecting
@ -264,6 +269,7 @@ class BeaconProvisioner: NSObject, ObservableObject {
self.dxReadQueryIndex = 0 self.dxReadQueryIndex = 0
self.allDiscoveredServices.removeAll() self.allDiscoveredServices.removeAll()
self.connectionRetryCount = 0 self.connectionRetryCount = 0
self.deviceInfoRetryCount = 0
self.isTerminating = false self.isTerminating = false
self.currentBeacon = beacon self.currentBeacon = beacon
self.servicesToExplore.removeAll() self.servicesToExplore.removeAll()
@ -298,8 +304,10 @@ class BeaconProvisioner: NSObject, ObservableObject {
dxSmartWriteIndex = 0 dxSmartWriteIndex = 0
provisioningMacAddress = nil provisioningMacAddress = nil
awaitingDeviceInfoForProvisioning = false awaitingDeviceInfoForProvisioning = false
skipDeviceInfoRead = false
isTerminating = false isTerminating = false
connectionRetryCount = 0 connectionRetryCount = 0
deviceInfoRetryCount = 0
currentBeacon = nil currentBeacon = nil
state = .idle state = .idle
progress = "" progress = ""
@ -406,6 +414,14 @@ class BeaconProvisioner: NSObject, ObservableObject {
/// Read device info (MAC address) before writing config /// Read device info (MAC address) before writing config
private func dxSmartReadDeviceInfoBeforeWrite() { 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 { guard let commandChar = characteristics[BeaconProvisioner.DXSMART_COMMAND_CHAR] else {
DebugLog.shared.log("BLE: FFE2 not found, proceeding without MAC") DebugLog.shared.log("BLE: FFE2 not found, proceeding without MAC")
dxSmartWriteConfig() dxSmartWriteConfig()
@ -894,6 +910,7 @@ class BeaconProvisioner: NSObject, ObservableObject {
configService = nil configService = nil
characteristics.removeAll() characteristics.removeAll()
connectionRetryCount = 0 connectionRetryCount = 0
deviceInfoRetryCount = 0
currentBeacon = nil currentBeacon = nil
operationMode = .provisioning operationMode = .provisioning
state = .idle state = .idle
@ -1012,16 +1029,43 @@ extension BeaconProvisioner: CBCentralManagerDelegate {
return return
} }
// Disconnect during device info read (post-auth, pre-write) beacon may have // Disconnect during device info read (post-auth, pre-write) beacon dropped
// dropped the connection during the MAC address query. We authenticated but // connection during the optional MAC address query. Instead of failing, reconnect
// lost connection before writing config, so this is a failure but a known // and skip the device info step (MAC is nice-to-have, not required).
// one with a clear retry path, not an unexplained "Unexpected disconnect".
if state == .authenticating && awaitingDeviceInfoForProvisioning && dxSmartAuthenticated { 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 awaitingDeviceInfoForProvisioning = false
// Connection lost can't write config without it, fail with specific message skipDeviceInfoRead = true // on reconnect, go straight to config write
fail("Disconnected after auth during device info read — please retry", code: .disconnected)
return if deviceInfoRetryCount < BeaconProvisioner.MAX_DEVICE_INFO_RETRIES {
deviceInfoRetryCount += 1
let delay = Double(deviceInfoRetryCount)
progress = "Reconnecting (skip MAC read)..."
DebugLog.shared.log("BLE: Disconnect during device info read — reconnecting (\(deviceInfoRetryCount)/\(BeaconProvisioner.MAX_DEVICE_INFO_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 device info retries exhausted (\(deviceInfoRetryCount)/\(BeaconProvisioner.MAX_DEVICE_INFO_RETRIES))")
fail("Disconnected while reading device information (retries exhausted)", code: .disconnected)
return
}
} }
// All other disconnects are unexpected // All other disconnects are unexpected