fix: handle SaveConfig write-error path + add disconnect diagnostics

The previous fix only caught the disconnect callback path. But CoreBluetooth
can also fire didWriteValueFor with an error when the beacon reboots mid-ATT
response. This was hitting fail() at the isNonFatalCommand check instead of
being treated as success.

Now handles both paths:
1. didDisconnectPeripheral with state=.writing at last command → succeed()
2. didWriteValueFor error for SaveConfig (last command) → succeed()

Also added detailed state/index logging to disconnect handler for diagnostics.
This commit is contained in:
Schwifty 2026-03-21 22:47:11 +00:00
parent e387b9ceb1
commit c62dace54d

View file

@ -966,20 +966,22 @@ extension BeaconProvisioner: CBCentralManagerDelegate {
}
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
DebugLog.shared.log("BLE: Disconnected from \(peripheral.name ?? "unknown")")
DebugLog.shared.log("BLE: Disconnected from \(peripheral.name ?? "unknown") | state=\(state) mode=\(operationMode) writeIdx=\(dxSmartWriteIndex) queueCount=\(dxSmartCommandQueue.count) error=\(error?.localizedDescription ?? "none")")
if operationMode == .readingConfig {
if state != .success && state != .idle {
finishRead()
}
} else if state != .success && state != .idle {
if case .failed = state {
// Already failed
} else if state == .writing && dxSmartWriteIndex >= dxSmartCommandQueue.count - 1 {
// Already failed disconnect is just cleanup
DebugLog.shared.log("BLE: Disconnect after failure, ignoring")
} else if state == .writing && dxSmartCommandQueue.count > 0 && dxSmartWriteIndex >= dxSmartCommandQueue.count - 1 {
// SaveConfig (last command) was sent beacon rebooted to apply config
// This is expected behavior, treat as success
DebugLog.shared.log("BLE: Disconnect after SaveConfig — treating as success")
DebugLog.shared.log("BLE: Disconnect after SaveConfig (idx=\(dxSmartWriteIndex)/\(dxSmartCommandQueue.count)) — treating as success")
succeed()
} else {
DebugLog.shared.log("BLE: UNEXPECTED disconnect — state=\(state) writeIdx=\(dxSmartWriteIndex) queueCount=\(dxSmartCommandQueue.count)")
fail("Unexpected disconnect", code: .disconnected)
}
}
@ -1106,14 +1108,21 @@ extension BeaconProvisioner: CBPeripheralDelegate {
// Device name (0x71) and Frame 1 commands (steps 1-6) may be rejected by some firmware
// Treat these as non-fatal: log and continue to next command
let isNonFatalCommand = dxSmartWriteIndex < 6 // First 6 commands are optional
if isNonFatalCommand {
let isSaveConfig = dxSmartWriteIndex >= dxSmartCommandQueue.count - 1
if isSaveConfig {
// SaveConfig (0x60) write "error" is expected beacon reboots immediately
// after processing the save, which kills the BLE connection before the
// ATT write response can be delivered. This is success, not failure.
DebugLog.shared.log("BLE: SaveConfig write error (beacon rebooted) — treating as success")
succeed()
} else if isNonFatalCommand {
DebugLog.shared.log("BLE: Non-fatal command failed at step \(dxSmartWriteIndex + 1), continuing...")
dxSmartWriteIndex += 1
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
self?.dxSmartSendNextCommand()
}
} else {
fail("Command write failed at step \(dxSmartWriteIndex + 1): \(error.localizedDescription)", code: .writeFailed)
fail("Command write failed at step \(dxSmartWriteIndex + 1)/\(dxSmartCommandQueue.count): \(error.localizedDescription)", code: .writeFailed)
}
}
return