Skip to content

Commit dcad9e9

Browse files
committed
- Fixed iOS reconnect failures when the device drops BLE bond keys mid-session (every 13-23 min on affected devices). iOS would cache stale keys and all reconnect attempts would fail, requiring a manual forget/re-pair in Settings. The app now detects the pairing error and clears the stale bond automatically before retrying
1 parent 6734995 commit dcad9e9

4 files changed

Lines changed: 67 additions & 6 deletions

File tree

lib/providers/app_state_provider.dart

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,8 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
260260
int _reconnectRestoreGeneration = 0;
261261
static const int _maxReconnectAttempts = 3;
262262
static const Duration _reconnectDelay = Duration(seconds: 3);
263+
static const Duration _reconnectDelayAfterBondError = Duration(seconds: 5);
264+
bool _lastReconnectWasBondError = false;
263265

264266
// Map navigation trigger (for navigating to log entry coordinates)
265267
({double lat, double lon})? _mapNavigationTarget;
@@ -2241,6 +2243,7 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
22412243
_cancelPendingAutoPingRestore();
22422244
_isAutoReconnecting = true;
22432245
_reconnectAttempt = 0;
2246+
_lastReconnectWasBondError = false;
22442247
_connectionStep = ConnectionStep.reconnecting;
22452248

22462249
// Remember auto-ping state before cleanup
@@ -2309,8 +2312,11 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
23092312
debugLog('[CONN] Auto-reconnect attempt $_reconnectAttempt of $_maxReconnectAttempts');
23102313
notifyListeners();
23112314

2315+
// Use longer delay after bond errors to give iOS time to clear stale keys
2316+
final delay = _lastReconnectWasBondError ? _reconnectDelayAfterBondError : _reconnectDelay;
2317+
23122318
// Delay before attempting reconnection
2313-
_reconnectTimer = Timer(_reconnectDelay, () async {
2319+
_reconnectTimer = Timer(delay, () async {
23142320
if (!_isAutoReconnecting) return; // Cancelled while waiting
23152321

23162322
try {
@@ -2320,6 +2326,7 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
23202326
// If we get here and connection step is 'connected', success!
23212327
if (_connectionStep == ConnectionStep.connected) {
23222328
debugLog('[CONN] Auto-reconnect succeeded on attempt $_reconnectAttempt');
2329+
_lastReconnectWasBondError = false;
23232330
_onReconnectSuccess();
23242331
} else if (_isAutoReconnecting) {
23252332
// Connection failed but didn't throw - try again
@@ -2331,6 +2338,10 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
23312338
} catch (e) {
23322339
debugError('[CONN] Auto-reconnect attempt $_reconnectAttempt failed: $e');
23332340
if (_isAutoReconnecting) {
2341+
// Check for iOS apple-code 14 (Peer removed pairing information)
2342+
// The MeshCore device cleared its bond keys — clear iOS stale bond before retrying
2343+
await _handleBondErrorIfNeeded(e);
2344+
23342345
// Reset step back to reconnecting for UI
23352346
_connectionStep = ConnectionStep.reconnecting;
23362347
_connectionError = null;
@@ -2341,6 +2352,19 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
23412352
});
23422353
}
23432354

2355+
/// Detect iOS apple-code 14 bond errors and clear the stale bond before retry
2356+
Future<void> _handleBondErrorIfNeeded(Object error) async {
2357+
final errorStr = error.toString();
2358+
if (errorStr.contains('apple-code: 14') || errorStr.contains('Peer removed pairing information')) {
2359+
_lastReconnectWasBondError = true;
2360+
final deviceId = _rememberedDevice?.id;
2361+
if (deviceId != null) {
2362+
debugLog('[CONN] Bond error detected (apple-code 14) — clearing stale bond for $deviceId');
2363+
await _bluetoothService.removeBond(deviceId);
2364+
}
2365+
}
2366+
}
2367+
23442368
/// Called when auto-reconnect succeeds
23452369
void _onReconnectSuccess() {
23462370
// Cancel timers

lib/services/bluetooth/bluetooth_service.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@ abstract class BluetoothService {
8686
/// Used for remembered devices to ensure name is available during connect
8787
void cacheDeviceInfo(DiscoveredDevice device);
8888

89+
/// Remove BLE bond/pairing for a device
90+
/// On Android: removes the system bond entry
91+
/// On iOS: best-effort — calls cancelPeripheralConnection to nudge CoreBluetooth
92+
/// into clearing stale encryption keys (used after apple-code 14 errors)
93+
Future<void> removeBond(String deviceId);
94+
8995
/// Dispose of resources
9096
void dispose();
9197
}

lib/services/bluetooth/mobile_bluetooth.dart

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -397,13 +397,26 @@ class MobileBluetoothService implements BluetoothService {
397397
return; // Success - exit retry loop
398398

399399
} catch (e, stackTrace) {
400+
final errorStr = e.toString();
401+
400402
// Check for Android error 133 (GATT_ERROR) - a well-known Android BLE stack issue
401403
// that typically succeeds on retry
402-
final isError133 = Platform.isAndroid && e.toString().contains('android-code: 133');
403-
404-
if (isError133 && attempt < _maxRetries) {
405-
debugLog('[BLE] Error 133 on attempt $attempt, retrying after delay...');
406-
await Future.delayed(_retryDelay);
404+
final isError133 = Platform.isAndroid && errorStr.contains('android-code: 133');
405+
406+
// Check for iOS apple-code 14 (Peer removed pairing information)
407+
// The remote device cleared its bond keys — clear iOS stale bond and retry
408+
final isBondError = Platform.isIOS &&
409+
(errorStr.contains('apple-code: 14') || errorStr.contains('Peer removed pairing information'));
410+
411+
if ((isError133 || isBondError) && attempt < _maxRetries) {
412+
if (isBondError) {
413+
debugLog('[BLE] Bond error (apple-code 14) on attempt $attempt, removing bond and retrying...');
414+
await removeBond(deviceId);
415+
await Future.delayed(const Duration(seconds: 2));
416+
} else {
417+
debugLog('[BLE] Error 133 on attempt $attempt, retrying after delay...');
418+
await Future.delayed(_retryDelay);
419+
}
407420
// Force cleanup before retry
408421
try {
409422
await _bleDevice?.disconnect();
@@ -470,6 +483,19 @@ class MobileBluetoothService implements BluetoothService {
470483
debugLog('[BLE] Cached device info: ${device.name} (${device.id})');
471484
}
472485

486+
@override
487+
Future<void> removeBond(String deviceId) async {
488+
try {
489+
final device = fbp.BluetoothDevice.fromId(deviceId);
490+
debugLog('[BLE] Removing bond for $deviceId');
491+
await device.removeBond();
492+
debugLog('[BLE] Bond removed for $deviceId');
493+
} catch (e) {
494+
// removeBond may not be supported on all platforms/devices — log and continue
495+
debugLog('[BLE] removeBond failed (continuing): $e');
496+
}
497+
}
498+
473499
@override
474500
void dispose() {
475501
_isDisposed = true;

lib/services/bluetooth/web_bluetooth.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,11 @@ class WebBluetoothService implements BluetoothService {
262262
// No caching needed - this method is for mobile remembered devices
263263
}
264264

265+
@override
266+
Future<void> removeBond(String deviceId) async {
267+
// Web Bluetooth does not support bond management
268+
}
269+
265270
@override
266271
void dispose() {
267272
_notificationSubscription?.cancel();

0 commit comments

Comments
 (0)