From 2ebff4879cebdea02bc30883c9736b1ca24934ff Mon Sep 17 00:00:00 2001 From: Koda Date: Sun, 22 Mar 2026 16:59:33 +0000 Subject: [PATCH] perf: aggressive BLE timeout tuning to match Android optimizations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port all timing reductions from payfrit-beacon-android PR #9: - BASE_WRITE_DELAY: 0.5s → 0.2s - HEAVY_WRITE_DELAY: 1.0s → 0.4s - LARGE_PAYLOAD_DELAY: 0.8s → 0.3s - RESPONSE_GATE_TIMEOUT: 1.0s → 0.5s - WRITE_TIMEOUT: 5.0s → 2.0s - GLOBAL_TIMEOUT: 90s → 30s - Read config timeout: 15s → 8s - Password retry delay: 0.5s → 0.2s - FFE2 reconnect delay: 2.0s → 1.0s - Connection retry backoff: halved (0.5s/1.0s/1.5s) - Disconnect retry backoff: halved - Read query delays: 0.4s → 0.2s - Reconnect resume delay: 1.5s → 0.5s - Final response wait: 2.0s → 1.0s --- PayfritBeacon/BeaconProvisioner.swift | 46 +++++++++++++-------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/PayfritBeacon/BeaconProvisioner.swift b/PayfritBeacon/BeaconProvisioner.swift index 7215149..85334ab 100644 --- a/PayfritBeacon/BeaconProvisioner.swift +++ b/PayfritBeacon/BeaconProvisioner.swift @@ -102,7 +102,7 @@ struct BeaconCheckResult { /// 3. Full reconnect on FFE2 miss (CoreBluetooth caches stale GATT) /// 4. SaveConfig write-error = success (beacon reboots immediately) /// 5. Response gating between writes prevents MCU overload -/// 6. Adaptive delays: heavy commands (frame select/type) need 1s, others 0.5s +/// 6. Adaptive delays: heavy commands (frame select/type) need 0.4s, others 0.2s class BeaconProvisioner: NSObject, ObservableObject { // MARK: - Constants @@ -140,13 +140,13 @@ class BeaconProvisioner: NSObject, ObservableObject { case frameDisable = 0xFF } - // Timing constants (tuned from extensive testing) - private static let BASE_WRITE_DELAY: Double = 0.5 // Default delay between commands - private static let HEAVY_WRITE_DELAY: Double = 1.0 // After frame select/type commands (MCU state change) - private static let LARGE_PAYLOAD_DELAY: Double = 0.8 // After UUID/large payload writes - private static let RESPONSE_GATE_TIMEOUT: Double = 1.0 // 1s matches Android's withTimeoutOrNull(1000L) - private static let WRITE_TIMEOUT_SECONDS: Double = 5.0 // Per-write timeout (matches Android) - private static let GLOBAL_TIMEOUT: Double = 90.0 // Overall provisioning timeout + // Timing constants (aggressively tuned to match Android BLE optimizations) + private static let BASE_WRITE_DELAY: Double = 0.2 // Default delay between commands (was 0.5) + private static let HEAVY_WRITE_DELAY: Double = 0.4 // After frame select/type commands (was 1.0) + private static let LARGE_PAYLOAD_DELAY: Double = 0.3 // After UUID/large payload writes (was 0.8) + private static let RESPONSE_GATE_TIMEOUT: Double = 0.5 // Response gate timeout (was 1.0) + private static let WRITE_TIMEOUT_SECONDS: Double = 2.0 // Per-write timeout (was 5.0, matches Android) + private static let GLOBAL_TIMEOUT: Double = 30.0 // Overall provisioning timeout (was 90.0) // Retry limits private static let MAX_CONNECTION_RETRIES = 3 @@ -284,14 +284,14 @@ class BeaconProvisioner: NSObject, ObservableObject { centralManager.connect(resolvedPeripheral, options: nil) - // 15-second timeout for read operations + // 8-second timeout for read operations (was 15s) let timeout = DispatchWorkItem { [weak self] in guard let self = self, self.operationMode == .readingConfig else { return } DebugLog.shared.log("BLE: Read timeout reached") self.finishRead() } readTimeout = timeout - DispatchQueue.main.asyncAfter(deadline: .now() + 15, execute: timeout) + DispatchQueue.main.asyncAfter(deadline: .now() + 8, execute: timeout) } // MARK: - State Reset @@ -428,7 +428,7 @@ class BeaconProvisioner: NSObject, ObservableObject { passwordIndex += 1 if passwordIndex < BeaconProvisioner.DXSMART_PASSWORDS.count { DebugLog.shared.log("BLE: Password rejected, trying next (\(passwordIndex + 1)/\(BeaconProvisioner.DXSMART_PASSWORDS.count))") - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in self?.authenticate() } } else if operationMode == .readingConfig { @@ -565,7 +565,7 @@ class BeaconProvisioner: NSObject, ObservableObject { disconnectPeripheral() - DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in + DispatchQueue.main.asyncAfter(deadline: .now() + 1.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) @@ -596,7 +596,7 @@ class BeaconProvisioner: NSObject, ObservableObject { guard let self = self else { return } guard self.awaitingResponse else { return } self.awaitingResponse = false - DebugLog.shared.log("BLE: No FFE1 response within 1s for command \(self.writeIndex + 1) — advancing (OK)") + DebugLog.shared.log("BLE: No FFE1 response within 0.5s for command \(self.writeIndex + 1) — advancing (OK)") self.advanceToNextCommand() } responseGateTimer = timer @@ -639,7 +639,7 @@ class BeaconProvisioner: NSObject, ObservableObject { // Non-fatal: skip DebugLog.shared.log("BLE: Write timeout for non-fatal command \(current)/\(total) — skipping") self.writeIndex += 1 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in self?.sendNextCommand() } } else { @@ -824,9 +824,9 @@ class BeaconProvisioner: NSObject, ObservableObject { private func sendNextReadQuery() { guard dxReadQueryIndex < dxReadQueries.count else { - DebugLog.shared.log("BLE: All read queries sent, waiting 2s for final responses") + DebugLog.shared.log("BLE: All read queries sent, waiting 1s for final responses") progress = "Collecting responses..." - DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in guard let self = self, self.operationMode == .readingConfig else { return } self.finishRead() } @@ -1081,7 +1081,7 @@ extension BeaconProvisioner: CBCentralManagerDelegate { if connectionRetryCount < BeaconProvisioner.MAX_CONNECTION_RETRIES { connectionRetryCount += 1 - let delay = Double(connectionRetryCount) + let delay = Double(connectionRetryCount) * 0.5 // 0.5s, 1.0s, 1.5s (was 1s, 2s, 3s) progress = "Connection failed, retrying (\(connectionRetryCount)/\(BeaconProvisioner.MAX_CONNECTION_RETRIES))..." DebugLog.shared.log("BLE: Retrying in \(delay)s...") @@ -1170,7 +1170,7 @@ extension BeaconProvisioner: CBCentralManagerDelegate { } state = .connecting - let delay = Double(disconnectRetryCount) + 2.0 // 3s, 4s, 5s... backoff + let delay = Double(disconnectRetryCount) * 0.5 + 0.5 // 1.0s, 1.5s, 2.0s... backoff (was 3s, 4s, 5s) 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 } @@ -1307,7 +1307,7 @@ extension BeaconProvisioner: CBPeripheralDelegate { if operationMode == .readingConfig { DebugLog.shared.log("BLE: Read query failed, skipping") dxReadQueryIndex += 1 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { [weak self] in + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in self?.sendNextReadQuery() } } else { @@ -1321,7 +1321,7 @@ extension BeaconProvisioner: CBPeripheralDelegate { } else if isNonFatal { DebugLog.shared.log("BLE: Non-fatal command failed at step \(writeIndex + 1), continuing...") writeIndex += 1 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in self?.sendNextCommand() } } else { @@ -1353,8 +1353,8 @@ extension BeaconProvisioner: CBPeripheralDelegate { state = .writing DebugLog.shared.log("BLE: Resuming write from command \(writeIndex + 1)/\(commandQueue.count)") progress = "Resuming config write..." - // 1.5s delay after reconnect for BLE stability - DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in + // 0.5s delay after reconnect for BLE stability (was 1.5s) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in self?.sendNextCommand() } } else { @@ -1369,7 +1369,7 @@ extension BeaconProvisioner: CBPeripheralDelegate { if characteristic.uuid == BeaconProvisioner.DXSMART_COMMAND_CHAR { if operationMode == .readingConfig { dxReadQueryIndex += 1 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { [weak self] in + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in self?.sendNextReadQuery() } } else { -- 2.43.0