Skip to content

Commit b1c1cf7

Browse files
committed
Implemented Auto Reconnect
1 parent 5312eeb commit b1c1cf7

File tree

5 files changed

+407
-56
lines changed

5 files changed

+407
-56
lines changed

android/local.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ sdk.dir=/opt/homebrew/share/android-commandlinetools
22
flutter.sdk=/opt/homebrew/share/flutter
33
flutter.buildMode=release
44
flutter.versionName=1.0.0
5-
flutter.versionCode=1
5+
flutter.versionCode=1770265889

lib/models/connection_state.dart

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ enum ConnectionStatus {
2121
enum ConnectionStep {
2222
/// Initial disconnected state
2323
disconnected,
24-
24+
25+
/// Auto-reconnecting after unexpected BLE disconnect
26+
reconnecting,
27+
2528
/// Step 1: BLE GATT connect
2629
bleConnecting,
2730

@@ -80,6 +83,8 @@ extension ConnectionStepExtension on ConnectionStep {
8083
switch (this) {
8184
case ConnectionStep.disconnected:
8285
return 'Disconnected';
86+
case ConnectionStep.reconnecting:
87+
return 'Reconnecting...';
8388
case ConnectionStep.bleConnecting:
8489
return 'Connecting to device...';
8590
case ConnectionStep.protocolHandshake:
@@ -108,6 +113,8 @@ extension ConnectionStepExtension on ConnectionStep {
108113
switch (this) {
109114
case ConnectionStep.disconnected:
110115
return 0;
116+
case ConnectionStep.reconnecting:
117+
return 0;
111118
case ConnectionStep.bleConnecting:
112119
return 1;
113120
case ConnectionStep.protocolHandshake:

lib/providers/app_state_provider.dart

Lines changed: 261 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,17 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
189189
bool _isSwitchingMode = false;
190190
String? _modeSwitchError; // Error message if mode switch fails
191191

192+
// Auto-reconnect state
193+
bool _userRequestedDisconnect = false;
194+
bool _isAutoReconnecting = false;
195+
int _reconnectAttempt = 0;
196+
Timer? _reconnectTimer;
197+
Timer? _reconnectTimeoutTimer;
198+
bool _autoPingWasEnabled = false;
199+
AutoMode _autoModeBeforeReconnect = AutoMode.active;
200+
static const int _maxReconnectAttempts = 3;
201+
static const Duration _reconnectDelay = Duration(seconds: 3);
202+
192203
// Map navigation trigger (for navigating to log entry coordinates)
193204
({double lat, double lon})? _mapNavigationTarget;
194205
int _mapNavigationTrigger = 0; // Increment to trigger navigation
@@ -307,6 +318,10 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
307318
bool get isSwitchingMode => _isSwitchingMode;
308319
String? get modeSwitchError => _modeSwitchError;
309320

321+
// Auto-reconnect getters
322+
bool get isAutoReconnecting => _isAutoReconnecting;
323+
int get reconnectAttempt => _reconnectAttempt;
324+
310325
// Repeater markers getters
311326
List<Repeater> get repeaters => List.unmodifiable(_repeaters);
312327

@@ -477,61 +492,21 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
477492
_bluetoothService.connectionStream.listen((status) async {
478493
_connectionStatus = status;
479494
if (status == ConnectionStatus.disconnected) {
480-
_connectionStep = ConnectionStep.disconnected;
481-
482-
// Stop heartbeat immediately on BLE disconnect
483-
_apiService.disableHeartbeat();
484-
debugLog('[CONN] Heartbeat disabled due to BLE disconnect');
485-
486-
// Stop auto-ping timers
487-
_autoPingTimer.stop();
488-
_rxWindowTimer.stop();
489-
_cooldownTimer.stop();
490-
if (_autoPingEnabled) {
491-
_autoPingEnabled = false;
492-
debugLog('[AUTO] Auto-ping disabled due to BLE disconnect');
493-
}
494-
495-
// End noise floor session on BLE disconnect
496-
await _endNoiseFloorSession();
497-
498-
// Stop RX logger
499-
_rxLogger?.stopWardriving(trigger: 'ble_disconnect');
500-
501-
// Force upload any pending items BEFORE releasing session
502-
// This ensures data is submitted while session is still valid
503-
// Waits for TX items in hold period (up to 6 seconds) to become eligible
504-
if (_apiService.hasSession) {
505-
debugLog('[CONN] Flushing API queue before session release');
506-
try {
507-
await _apiQueueService.forceUploadWithHoldWait();
508-
} catch (e) {
509-
debugError('[CONN] Failed to flush API queue: $e');
510-
}
511-
}
512-
513-
// Clear any remaining items and stop batch timer
514-
await _apiQueueService.clearOnDisconnect();
515-
516-
// Release API session (best effort - don't block on failure)
517-
if (_devicePublicKey != null && _apiService.hasSession) {
518-
debugLog('[CONN] Releasing API session due to BLE disconnect');
519-
try {
520-
await _apiService.requestAuth(
521-
reason: 'disconnect',
522-
publicKey: _devicePublicKey!,
523-
);
524-
debugLog('[CONN] API session released successfully');
525-
} catch (e) {
526-
debugError('[CONN] Failed to release API session: $e');
527-
}
495+
// Check if this is an unexpected disconnect during active wardriving
496+
final wasConnected = _connectionStep == ConnectionStep.connected;
497+
final hasRemembered = _rememberedDevice != null;
498+
final isUnexpected = !_userRequestedDisconnect && !_isAutoReconnecting;
499+
500+
if (wasConnected && hasRemembered && isUnexpected && !kIsWeb) {
501+
debugLog('[CONN] Unexpected BLE disconnect detected - starting auto-reconnect');
502+
await _startAutoReconnect();
503+
} else if (!_isAutoReconnecting) {
504+
// Normal disconnect (user-requested or no remembered device)
505+
await _fullDisconnectCleanup();
506+
} else {
507+
// Disconnected during a reconnect attempt - _attemptReconnect handles retry
508+
debugLog('[CONN] BLE disconnect during reconnect attempt - will retry');
528509
}
529-
530-
// Existing cleanup
531-
_meshCoreConnection?.dispose();
532-
_meshCoreConnection = null;
533-
_pingService?.dispose();
534-
_pingService = null;
535510
}
536511
notifyListeners();
537512
});
@@ -1571,8 +1546,237 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
15711546
debugLog('[APP] Unified RX handler created and listening');
15721547
}
15731548

1549+
/// Full disconnect cleanup - called on normal BLE disconnect (user-requested or no remembered device)
1550+
/// Extracted from the original BLE disconnect listener
1551+
Future<void> _fullDisconnectCleanup() async {
1552+
_connectionStep = ConnectionStep.disconnected;
1553+
1554+
// Stop heartbeat immediately on BLE disconnect
1555+
_apiService.disableHeartbeat();
1556+
debugLog('[CONN] Heartbeat disabled due to BLE disconnect');
1557+
1558+
// Stop auto-ping timers
1559+
_autoPingTimer.stop();
1560+
_rxWindowTimer.stop();
1561+
_cooldownTimer.stop();
1562+
if (_autoPingEnabled) {
1563+
_autoPingEnabled = false;
1564+
debugLog('[AUTO] Auto-ping disabled due to BLE disconnect');
1565+
}
1566+
1567+
// End noise floor session on BLE disconnect
1568+
await _endNoiseFloorSession();
1569+
1570+
// Stop RX logger
1571+
_rxLogger?.stopWardriving(trigger: 'ble_disconnect');
1572+
1573+
// Force upload any pending items BEFORE releasing session
1574+
if (_apiService.hasSession) {
1575+
debugLog('[CONN] Flushing API queue before session release');
1576+
try {
1577+
await _apiQueueService.forceUploadWithHoldWait();
1578+
} catch (e) {
1579+
debugError('[CONN] Failed to flush API queue: $e');
1580+
}
1581+
}
1582+
1583+
// Clear any remaining items and stop batch timer
1584+
await _apiQueueService.clearOnDisconnect();
1585+
1586+
// Release API session (best effort - don't block on failure)
1587+
if (_devicePublicKey != null && _apiService.hasSession) {
1588+
debugLog('[CONN] Releasing API session due to BLE disconnect');
1589+
try {
1590+
await _apiService.requestAuth(
1591+
reason: 'disconnect',
1592+
publicKey: _devicePublicKey!,
1593+
);
1594+
debugLog('[CONN] API session released successfully');
1595+
} catch (e) {
1596+
debugError('[CONN] Failed to release API session: $e');
1597+
}
1598+
}
1599+
1600+
// Existing cleanup
1601+
_meshCoreConnection?.dispose();
1602+
_meshCoreConnection = null;
1603+
_pingService?.dispose();
1604+
_pingService = null;
1605+
}
1606+
1607+
/// Start auto-reconnect after unexpected BLE disconnect
1608+
Future<void> _startAutoReconnect() async {
1609+
_isAutoReconnecting = true;
1610+
_reconnectAttempt = 0;
1611+
_connectionStep = ConnectionStep.reconnecting;
1612+
1613+
// Remember auto-ping state before cleanup
1614+
_autoPingWasEnabled = _autoPingEnabled;
1615+
_autoModeBeforeReconnect = _autoMode;
1616+
1617+
// Stop auto-ping timers (don't dispose)
1618+
_autoPingTimer.stop();
1619+
_rxWindowTimer.stop();
1620+
_cooldownTimer.stop();
1621+
_autoPingEnabled = false;
1622+
1623+
// Stop heartbeat
1624+
_apiService.disableHeartbeat();
1625+
1626+
// End noise floor session
1627+
await _endNoiseFloorSession();
1628+
1629+
// Flush RX logger
1630+
_rxLogger?.stopWardriving(trigger: 'reconnect');
1631+
1632+
// Stop background service
1633+
await BackgroundServiceManager.stopService();
1634+
1635+
// Clean up dead BLE-dependent objects
1636+
_logRxDataSubscription?.cancel();
1637+
_logRxDataSubscription = null;
1638+
_unifiedRxHandler?.dispose();
1639+
_unifiedRxHandler = null;
1640+
_txTracker = null;
1641+
_rxLogger = null;
1642+
await _noiseFloorSubscription?.cancel();
1643+
_noiseFloorSubscription = null;
1644+
await _batterySubscription?.cancel();
1645+
_batterySubscription = null;
1646+
_meshCoreConnection?.dispose();
1647+
_meshCoreConnection = null;
1648+
_pingService?.dispose();
1649+
_pingService = null;
1650+
1651+
// Do NOT release API session or clear API queue
1652+
debugLog('[CONN] Auto-reconnect: preserved API session, cleaned up BLE objects');
1653+
1654+
notifyListeners();
1655+
1656+
// Start overall timeout (30 seconds)
1657+
_reconnectTimeoutTimer = Timer(const Duration(seconds: 30), () {
1658+
debugLog('[CONN] Auto-reconnect timed out after 30s');
1659+
_abandonAutoReconnect();
1660+
});
1661+
1662+
// Start first attempt
1663+
_attemptReconnect();
1664+
}
1665+
1666+
/// Attempt a single reconnection
1667+
void _attemptReconnect() {
1668+
if (_reconnectAttempt >= _maxReconnectAttempts) {
1669+
debugLog('[CONN] Auto-reconnect: max attempts reached ($_maxReconnectAttempts)');
1670+
_abandonAutoReconnect();
1671+
return;
1672+
}
1673+
1674+
_reconnectAttempt++;
1675+
debugLog('[CONN] Auto-reconnect attempt $_reconnectAttempt of $_maxReconnectAttempts');
1676+
notifyListeners();
1677+
1678+
// Delay before attempting reconnection
1679+
_reconnectTimer = Timer(_reconnectDelay, () async {
1680+
if (!_isAutoReconnecting) return; // Cancelled while waiting
1681+
1682+
try {
1683+
debugLog('[CONN] Auto-reconnect: calling reconnectToRememberedDevice()');
1684+
await reconnectToRememberedDevice();
1685+
1686+
// If we get here and connection step is 'connected', success!
1687+
if (_connectionStep == ConnectionStep.connected) {
1688+
debugLog('[CONN] Auto-reconnect succeeded on attempt $_reconnectAttempt');
1689+
_onReconnectSuccess();
1690+
} else if (_isAutoReconnecting) {
1691+
// Connection failed but didn't throw - try again
1692+
debugLog('[CONN] Auto-reconnect: connection did not complete, retrying...');
1693+
_connectionStep = ConnectionStep.reconnecting;
1694+
notifyListeners();
1695+
_attemptReconnect();
1696+
}
1697+
} catch (e) {
1698+
debugError('[CONN] Auto-reconnect attempt $_reconnectAttempt failed: $e');
1699+
if (_isAutoReconnecting) {
1700+
// Reset step back to reconnecting for UI
1701+
_connectionStep = ConnectionStep.reconnecting;
1702+
_connectionError = null;
1703+
notifyListeners();
1704+
_attemptReconnect();
1705+
}
1706+
}
1707+
});
1708+
}
1709+
1710+
/// Called when auto-reconnect succeeds
1711+
void _onReconnectSuccess() {
1712+
// Cancel timers
1713+
_reconnectTimer?.cancel();
1714+
_reconnectTimer = null;
1715+
_reconnectTimeoutTimer?.cancel();
1716+
_reconnectTimeoutTimer = null;
1717+
1718+
final wasAutoPing = _autoPingWasEnabled;
1719+
final previousMode = _autoModeBeforeReconnect;
1720+
1721+
// Clear reconnect state
1722+
_isAutoReconnecting = false;
1723+
_reconnectAttempt = 0;
1724+
_autoPingWasEnabled = false;
1725+
1726+
debugLog('[CONN] Auto-reconnect complete, restoring state (autoPing=$wasAutoPing, mode=$previousMode)');
1727+
1728+
// Restore auto-ping if it was active
1729+
if (wasAutoPing) {
1730+
// Use a short delay to ensure connection is fully set up
1731+
Timer(const Duration(milliseconds: 500), () {
1732+
if (_connectionStep == ConnectionStep.connected) {
1733+
toggleAutoPing(previousMode);
1734+
debugLog('[CONN] Auto-ping restored after reconnect (mode=$previousMode)');
1735+
}
1736+
});
1737+
}
1738+
1739+
notifyListeners();
1740+
}
1741+
1742+
/// Cancel auto-reconnect (called from UI cancel button)
1743+
void cancelAutoReconnect() {
1744+
debugLog('[CONN] Auto-reconnect cancelled by user');
1745+
_abandonAutoReconnect();
1746+
}
1747+
1748+
/// Abandon auto-reconnect and do full cleanup
1749+
void _abandonAutoReconnect() {
1750+
// Cancel timers
1751+
_reconnectTimer?.cancel();
1752+
_reconnectTimer = null;
1753+
_reconnectTimeoutTimer?.cancel();
1754+
_reconnectTimeoutTimer = null;
1755+
1756+
// Clear reconnect state
1757+
_isAutoReconnecting = false;
1758+
_reconnectAttempt = 0;
1759+
_autoPingWasEnabled = false;
1760+
1761+
// Do full disconnect cleanup (releases API session, etc.)
1762+
_fullDisconnectCleanup();
1763+
notifyListeners();
1764+
}
1765+
15741766
/// Disconnect from current device
15751767
Future<void> disconnect() async {
1768+
// Mark as user-requested so BLE disconnect listener doesn't trigger auto-reconnect
1769+
_userRequestedDisconnect = true;
1770+
1771+
// Cancel any active auto-reconnect
1772+
_reconnectTimer?.cancel();
1773+
_reconnectTimer = null;
1774+
_reconnectTimeoutTimer?.cancel();
1775+
_reconnectTimeoutTimer = null;
1776+
_isAutoReconnecting = false;
1777+
_reconnectAttempt = 0;
1778+
_autoPingWasEnabled = false;
1779+
15761780
// Disable heartbeat immediately on disconnect
15771781
_apiService.disableHeartbeat();
15781782

@@ -1657,6 +1861,9 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
16571861
// Clear discovered devices so user must scan fresh
16581862
_discoveredDevices = [];
16591863

1864+
// Reset user-requested flag
1865+
_userRequestedDisconnect = false;
1866+
16601867
notifyListeners();
16611868

16621869
// Auto-exit app if preference is enabled (Android only)

0 commit comments

Comments
 (0)