Matthias Nott
3 days ago cda5ac96c4802f7c33f8f0099a8c9c34423dde4a
lib/services/mqtt_service.dart
....@@ -68,6 +68,13 @@
6868 // (Per-session subscriptions removed — single pailot/out topic now)
6969 static const int _maxSeenIds = 500;
7070
71
+ // Reconnect backoff
72
+ Timer? _reconnectTimer;
73
+ Timer? _stabilityTimer;
74
+ int _reconnectAttempt = 0;
75
+ static const int _maxReconnectDelay = 30000; // 30s cap
76
+ static const int _stabilityThresholdMs = 10000; // 10s stable = reset backoff
77
+
7178 // Callbacks
7279 void Function(ConnectionStatus status)? onStatusChanged;
7380 void Function(String detail)? onStatusDetail; // "Probing local...", "Scanning network..."
....@@ -120,16 +127,16 @@
120127 }
121128
122129 /// Fast reconnect to a known host — skips discovery, short timeout.
123
- Future<void> _fastReconnect(String host) async {
130
+ /// Returns true if connected, false if failed.
131
+ Future<bool> _fastReconnect(String host) async {
124132 _mqttLog('MQTT: fast reconnect to $host');
125133 final clientId = await _getClientId();
126134 if (await _tryConnect(host, clientId, timeout: 2000)) {
127135 connectedHost = host;
128
- return;
136
+ return true;
129137 }
130
- // Fast path failed — fall back to full connect
131
- _mqttLog('MQTT: fast reconnect failed, full connect...');
132
- connect();
138
+ _mqttLog('MQTT: fast reconnect failed');
139
+ return false;
133140 }
134141
135142 /// Connect to the MQTT broker.
....@@ -440,7 +447,9 @@
440447 );
441448 _mqttLog('MQTT: connect result=${result?.state}');
442449 if (result?.state == MqttConnectionState.connected) {
443
- client.autoReconnect = true;
450
+ // Don't use autoReconnect — it has no backoff and causes tight reconnect loops.
451
+ // We handle reconnection manually in _onDisconnected with exponential backoff.
452
+ _reconnectAttempt = 0;
444453 return true;
445454 }
446455 _client = null;
....@@ -454,6 +463,17 @@
454463
455464 void _onConnected() {
456465 _mqttLog('MQTT: _onConnected fired');
466
+ _reconnectTimer?.cancel();
467
+ // Don't reset _reconnectAttempt here — only after the connection has been
468
+ // STABLE for 10+ seconds. This prevents flap loops where each brief connect
469
+ // resets the backoff and we hammer the server every 5s forever.
470
+ _stabilityTimer?.cancel();
471
+ _stabilityTimer = Timer(const Duration(milliseconds: _stabilityThresholdMs), () {
472
+ if (_status == ConnectionStatus.connected) {
473
+ _mqttLog('MQTT: connection stable for ${_stabilityThresholdMs}ms — resetting backoff');
474
+ _reconnectAttempt = 0;
475
+ }
476
+ });
457477 _setStatus(ConnectionStatus.connected);
458478 _subscribe();
459479 _listenMessages();
....@@ -461,6 +481,7 @@
461481 }
462482
463483 void _onDisconnected() {
484
+ _stabilityTimer?.cancel();
464485 _updatesSub?.cancel();
465486 _updatesSub = null;
466487
....@@ -470,15 +491,41 @@
470491 } else {
471492 _setStatus(ConnectionStatus.reconnecting);
472493 onReconnecting?.call();
494
+ _scheduleReconnect();
473495 }
474496 }
475497
498
+ void _scheduleReconnect() {
499
+ _reconnectTimer?.cancel();
500
+ // Exponential backoff: 1s, 2s, 4s, 8s, 16s, 30s cap
501
+ final delayMs = (1000 * (1 << _reconnectAttempt)).clamp(1000, _maxReconnectDelay);
502
+ _reconnectAttempt++;
503
+ _mqttLog('MQTT: scheduling reconnect in ${delayMs}ms (attempt $_reconnectAttempt)');
504
+ _reconnectTimer = Timer(Duration(milliseconds: delayMs), () async {
505
+ if (_intentionalClose || _status == ConnectionStatus.connected) return;
506
+ final host = connectedHost ?? _lastDiscoveredHost;
507
+ if (host != null) {
508
+ _mqttLog('MQTT: reconnect attempt $_reconnectAttempt to $host');
509
+ final ok = await _fastReconnect(host);
510
+ if (!ok && !_intentionalClose) {
511
+ _scheduleReconnect(); // Try again with increased backoff
512
+ }
513
+ } else {
514
+ _mqttLog('MQTT: no known host, running full connect');
515
+ await connect();
516
+ }
517
+ });
518
+ }
519
+
476520 void _onAutoReconnect() {
521
+ // Unused — autoReconnect is disabled, but keep callback for safety
477522 _setStatus(ConnectionStatus.reconnecting);
478523 onReconnecting?.call();
479524 }
480525
481526 void _onAutoReconnected() {
527
+ // Unused — autoReconnect is disabled, but keep callback for safety
528
+ _reconnectAttempt = 0;
482529 _setStatus(ConnectionStatus.connected);
483530 _subscribe();
484531 _listenMessages();
....@@ -764,6 +811,11 @@
764811 /// Disconnect intentionally.
765812 void disconnect() {
766813 _intentionalClose = true;
814
+ _reconnectTimer?.cancel();
815
+ _reconnectTimer = null;
816
+ _stabilityTimer?.cancel();
817
+ _stabilityTimer = null;
818
+ _reconnectAttempt = 0;
767819 _updatesSub?.cancel();
768820 _updatesSub = null;
769821 _connectivitySub?.cancel();
....@@ -799,8 +851,12 @@
799851 case AppLifecycleState.resumed:
800852 if (_intentionalClose) break;
801853 _mqttLog('MQTT: app resumed');
802
- // Let autoReconnect handle dead connections (keepalive timeout).
803
- // Just trigger catch_up to fetch missed messages and rebuild UI.
854
+ // If disconnected, trigger immediate reconnect (reset backoff).
855
+ if (_status != ConnectionStatus.connected) {
856
+ _reconnectAttempt = 0;
857
+ _scheduleReconnect();
858
+ }
859
+ // Trigger catch_up to fetch missed messages and rebuild UI.
804860 onResume?.call();
805861 case AppLifecycleState.paused:
806862 break;