From d123d2561a9f939cd0250a594d8bd125e0001faf Mon Sep 17 00:00:00 2001 From: Schwifty Date: Sun, 22 Mar 2026 03:54:29 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20slim=20provisioning=20=E2=80=94=20skip?= =?UTF-8?q?=20extra=20frame=20disables=20+=20full=20reconnect=20on=20FFE2?= =?UTF-8?q?=20miss?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two key changes: 1. Remove frames 3-6 disable commands (was steps 16-23, 8 extra BLE writes). We only configure Frame 1 (device info) and Frame 2 (iBeacon), then save. Fewer writes = fewer chances for supervision timeout disconnects. 2. When FFE2 characteristic goes missing after a disconnect, do a full disconnect → reconnect → re-discover services → re-auth → resume cycle instead of trying to rediscover characteristics on the same (stale GATT) connection. CoreBluetooth returns cached results on the same connection, so FFE2 stays missing. Full reconnect forces a fresh GATT discovery. Write sequence is now 16 steps (down from 24). Co-Authored-By: Claude Opus 4.6 (1M context) --- PayfritBeacon/BeaconProvisioner.swift | 81 ++++++++++++++++----------- 1 file changed, 49 insertions(+), 32 deletions(-) diff --git a/PayfritBeacon/BeaconProvisioner.swift b/PayfritBeacon/BeaconProvisioner.swift index 6d5464c..232f1de 100644 --- a/PayfritBeacon/BeaconProvisioner.swift +++ b/PayfritBeacon/BeaconProvisioner.swift @@ -470,8 +470,8 @@ class BeaconProvisioner: NSObject, ObservableObject { dxSmartWriteConfig() } - /// Build the full command sequence and start writing - /// New 24-step write sequence for DX-Smart CP28: + /// Build the command sequence and start writing + /// Slim write sequence for DX-Smart CP28 (no extra frame overwrites): /// 1. DeviceName 0x71 [name bytes] — service point name (max 20 ASCII chars) /// 2. Frame1_Select 0x11 — select frame 1 /// 3. Frame1_Type 0x61 — enable as device info (broadcasts name) @@ -487,8 +487,11 @@ class BeaconProvisioner: NSObject, ObservableObject { /// 13. AdvInterval 0x78 [advInterval] /// 14. TxPower 0x79 [txPower] /// 15. TriggerOff 0xA0 - /// 16-23. Frames 3-6 select + 0xFF (disable each) - /// 24. SaveConfig 0x60 — persist to flash + /// 16. SaveConfig 0x60 — persist to flash + /// + /// NOTE: Frames 3-6 are intentionally left untouched. Disabling extra frames + /// caused additional BLE writes that increased disconnect risk. We only write + /// the two frames we care about (device info + iBeacon) and save. private func dxSmartWriteConfig() { guard let config = config else { fail("No config provided", code: .noConfig) @@ -553,21 +556,9 @@ class BeaconProvisioner: NSObject, ObservableObject { // 15. TriggerOff (0xA0) dxSmartCommandQueue.append(buildDXPacket(cmd: .triggerOff, data: [])) - // --- Frames 3-6: Disable each --- - // 16-17. Frame 3: select (0x13) + disable (0xFF) - dxSmartCommandQueue.append(buildDXPacket(cmd: .frameSelectSlot2, data: [])) - dxSmartCommandQueue.append(buildDXPacket(cmd: .frameDisable, data: [])) - // 18-19. Frame 4: select (0x14) + disable (0xFF) - dxSmartCommandQueue.append(buildDXPacket(cmd: .frameSelectSlot3, data: [])) - dxSmartCommandQueue.append(buildDXPacket(cmd: .frameDisable, data: [])) - // 20-21. Frame 5: select (0x15) + disable (0xFF) - dxSmartCommandQueue.append(buildDXPacket(cmd: .frameSelectSlot4, data: [])) - dxSmartCommandQueue.append(buildDXPacket(cmd: .frameDisable, data: [])) - // 22-23. Frame 6: select (0x16) + disable (0xFF) - dxSmartCommandQueue.append(buildDXPacket(cmd: .frameSelectSlot5, data: [])) - dxSmartCommandQueue.append(buildDXPacket(cmd: .frameDisable, data: [])) + // Frames 3-6 intentionally left untouched — fewer writes = fewer disconnects - // 24. SaveConfig (0x60) — persist to flash + // 16. SaveConfig (0x60) — persist to flash dxSmartCommandQueue.append(buildDXPacket(cmd: .saveConfig, data: [])) DebugLog.shared.log("BLE: DX-Smart command queue built with \(dxSmartCommandQueue.count) commands") @@ -590,23 +581,43 @@ class BeaconProvisioner: NSObject, ObservableObject { progress = "Writing config (\(current)/\(total))..." guard let commandChar = characteristics[BeaconProvisioner.DXSMART_COMMAND_CHAR] else { - // After disconnect+reconnect, characteristic discovery may return incomplete results. - // Re-discover characteristics instead of hard-failing. - if charRediscoveryCount < BeaconProvisioner.MAX_CHAR_REDISCOVERY, let service = configService { + // FFE2 missing — the GATT cache is stale after a disconnect. Rediscovering + // characteristics on the same connection doesn't work because CoreBluetooth + // returns cached (stale) results. We need a full disconnect → reconnect → + // re-discover services → re-discover characteristics cycle. + if charRediscoveryCount < BeaconProvisioner.MAX_CHAR_REDISCOVERY { charRediscoveryCount += 1 - DebugLog.shared.log("BLE: FFE2 missing — re-discovering characteristics (attempt \(charRediscoveryCount)/\(BeaconProvisioner.MAX_CHAR_REDISCOVERY))") - progress = "Re-discovering characteristics..." - state = .discoveringServices - scheduleCharRediscoveryTimeout() - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in - self?.peripheral?.discoverCharacteristics([ - BeaconProvisioner.DXSMART_NOTIFY_CHAR, - BeaconProvisioner.DXSMART_COMMAND_CHAR, - BeaconProvisioner.DXSMART_PASSWORD_CHAR - ], for: service) + DebugLog.shared.log("BLE: FFE2 missing — triggering full disconnect/reconnect (attempt \(charRediscoveryCount)/\(BeaconProvisioner.MAX_CHAR_REDISCOVERY))") + progress = "FFE2 missing, reconnecting..." + + // Use the existing disconnect retry path — it handles reconnect + re-auth + resume + resumeWriteAfterDisconnect = true + dxSmartAuthenticated = false + dxSmartNotifySubscribed = false + passwordIndex = 0 + characteristics.removeAll() + responseBuffer.removeAll() + awaitingCommandResponse = false + cancelResponseGateTimeout() + state = .connecting + + // Disconnect — didDisconnectPeripheral won't re-enter because we set state = .connecting + // and handle reconnect manually here + if let peripheral = peripheral, peripheral.state == .connected { + centralManager.cancelPeripheralConnection(peripheral) + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [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 + DebugLog.shared.log("BLE: Reconnecting after FFE2 miss...") + self.centralManager.connect(resolvedPeripheral, options: nil) } } else { - fail("DX-Smart command characteristic (FFE2) not found after \(charRediscoveryCount) rediscovery attempts", code: .serviceNotFound) + fail("DX-Smart command characteristic (FFE2) not found after \(charRediscoveryCount) reconnect attempts", code: .serviceNotFound) } return } @@ -1227,6 +1238,12 @@ extension BeaconProvisioner: CBCentralManagerDelegate { if state == .success || state == .idle { return } + + // Intentional disconnect for FFE2 reconnect — we're already handling reconnect + if state == .connecting && resumeWriteAfterDisconnect { + DebugLog.shared.log("BLE: Intentional disconnect for FFE2 reconnect, ignoring") + return + } if case .failed = state { DebugLog.shared.log("BLE: Disconnect after failure, ignoring") return