Compare commits

..

3 commits

Author SHA1 Message Date
f7a554e282 Merge pull request 'fix: handle expected BLE disconnect after SaveConfig' (#3) from schwifty/fix-saveconfig-disconnect into main 2026-03-21 22:49:05 +00:00
c62dace54d 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.
2026-03-21 22:47:11 +00:00
e387b9ceb1 fix: handle expected BLE disconnect after SaveConfig command
The DX-Smart CP28 beacon reboots after receiving SaveConfig (0x60) to
persist config to flash. This drops the BLE connection before the write
callback fires, causing the app to report "Unexpected disconnect" even
though the config was successfully saved.

Now we check if we're on the last command (SaveConfig) when disconnect
occurs — if so, treat it as success instead of failure.

Co-Authored-By: Luna <luna@payfrit.com>
2026-03-21 22:28:47 +00:00

View file

@ -966,15 +966,22 @@ extension BeaconProvisioner: CBCentralManagerDelegate {
} }
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { 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 operationMode == .readingConfig {
if state != .success && state != .idle { if state != .success && state != .idle {
finishRead() finishRead()
} }
} else if state != .success && state != .idle { } else if state != .success && state != .idle {
if case .failed = state { if case .failed = state {
// Already failed // 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 (idx=\(dxSmartWriteIndex)/\(dxSmartCommandQueue.count)) — treating as success")
succeed()
} else { } else {
DebugLog.shared.log("BLE: UNEXPECTED disconnect — state=\(state) writeIdx=\(dxSmartWriteIndex) queueCount=\(dxSmartCommandQueue.count)")
fail("Unexpected disconnect", code: .disconnected) fail("Unexpected disconnect", code: .disconnected)
} }
} }
@ -1101,14 +1108,21 @@ extension BeaconProvisioner: CBPeripheralDelegate {
// Device name (0x71) and Frame 1 commands (steps 1-6) may be rejected by some firmware // 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 // Treat these as non-fatal: log and continue to next command
let isNonFatalCommand = dxSmartWriteIndex < 6 // First 6 commands are optional 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...") DebugLog.shared.log("BLE: Non-fatal command failed at step \(dxSmartWriteIndex + 1), continuing...")
dxSmartWriteIndex += 1 dxSmartWriteIndex += 1
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
self?.dxSmartSendNextCommand() self?.dxSmartSendNextCommand()
} }
} else { } 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 return