From cda5ac96c4802f7c33f8f0099a8c9c34423dde4a Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Sat, 11 Apr 2026 09:27:07 +0200
Subject: [PATCH] feat: PDF and document file viewing support
---
lib/services/mqtt_service.dart | 72 ++++++++++++++++++++++++++++++++----
1 files changed, 64 insertions(+), 8 deletions(-)
diff --git a/lib/services/mqtt_service.dart b/lib/services/mqtt_service.dart
index 0c0502e..0718063 100644
--- a/lib/services/mqtt_service.dart
+++ b/lib/services/mqtt_service.dart
@@ -68,6 +68,13 @@
// (Per-session subscriptions removed — single pailot/out topic now)
static const int _maxSeenIds = 500;
+ // Reconnect backoff
+ Timer? _reconnectTimer;
+ Timer? _stabilityTimer;
+ int _reconnectAttempt = 0;
+ static const int _maxReconnectDelay = 30000; // 30s cap
+ static const int _stabilityThresholdMs = 10000; // 10s stable = reset backoff
+
// Callbacks
void Function(ConnectionStatus status)? onStatusChanged;
void Function(String detail)? onStatusDetail; // "Probing local...", "Scanning network..."
@@ -120,16 +127,16 @@
}
/// Fast reconnect to a known host — skips discovery, short timeout.
- Future<void> _fastReconnect(String host) async {
+ /// Returns true if connected, false if failed.
+ Future<bool> _fastReconnect(String host) async {
_mqttLog('MQTT: fast reconnect to $host');
final clientId = await _getClientId();
if (await _tryConnect(host, clientId, timeout: 2000)) {
connectedHost = host;
- return;
+ return true;
}
- // Fast path failed — fall back to full connect
- _mqttLog('MQTT: fast reconnect failed, full connect...');
- connect();
+ _mqttLog('MQTT: fast reconnect failed');
+ return false;
}
/// Connect to the MQTT broker.
@@ -440,7 +447,9 @@
);
_mqttLog('MQTT: connect result=${result?.state}');
if (result?.state == MqttConnectionState.connected) {
- client.autoReconnect = true;
+ // Don't use autoReconnect — it has no backoff and causes tight reconnect loops.
+ // We handle reconnection manually in _onDisconnected with exponential backoff.
+ _reconnectAttempt = 0;
return true;
}
_client = null;
@@ -454,6 +463,17 @@
void _onConnected() {
_mqttLog('MQTT: _onConnected fired');
+ _reconnectTimer?.cancel();
+ // Don't reset _reconnectAttempt here — only after the connection has been
+ // STABLE for 10+ seconds. This prevents flap loops where each brief connect
+ // resets the backoff and we hammer the server every 5s forever.
+ _stabilityTimer?.cancel();
+ _stabilityTimer = Timer(const Duration(milliseconds: _stabilityThresholdMs), () {
+ if (_status == ConnectionStatus.connected) {
+ _mqttLog('MQTT: connection stable for ${_stabilityThresholdMs}ms — resetting backoff');
+ _reconnectAttempt = 0;
+ }
+ });
_setStatus(ConnectionStatus.connected);
_subscribe();
_listenMessages();
@@ -461,6 +481,7 @@
}
void _onDisconnected() {
+ _stabilityTimer?.cancel();
_updatesSub?.cancel();
_updatesSub = null;
@@ -470,15 +491,41 @@
} else {
_setStatus(ConnectionStatus.reconnecting);
onReconnecting?.call();
+ _scheduleReconnect();
}
}
+ void _scheduleReconnect() {
+ _reconnectTimer?.cancel();
+ // Exponential backoff: 1s, 2s, 4s, 8s, 16s, 30s cap
+ final delayMs = (1000 * (1 << _reconnectAttempt)).clamp(1000, _maxReconnectDelay);
+ _reconnectAttempt++;
+ _mqttLog('MQTT: scheduling reconnect in ${delayMs}ms (attempt $_reconnectAttempt)');
+ _reconnectTimer = Timer(Duration(milliseconds: delayMs), () async {
+ if (_intentionalClose || _status == ConnectionStatus.connected) return;
+ final host = connectedHost ?? _lastDiscoveredHost;
+ if (host != null) {
+ _mqttLog('MQTT: reconnect attempt $_reconnectAttempt to $host');
+ final ok = await _fastReconnect(host);
+ if (!ok && !_intentionalClose) {
+ _scheduleReconnect(); // Try again with increased backoff
+ }
+ } else {
+ _mqttLog('MQTT: no known host, running full connect');
+ await connect();
+ }
+ });
+ }
+
void _onAutoReconnect() {
+ // Unused — autoReconnect is disabled, but keep callback for safety
_setStatus(ConnectionStatus.reconnecting);
onReconnecting?.call();
}
void _onAutoReconnected() {
+ // Unused — autoReconnect is disabled, but keep callback for safety
+ _reconnectAttempt = 0;
_setStatus(ConnectionStatus.connected);
_subscribe();
_listenMessages();
@@ -764,6 +811,11 @@
/// Disconnect intentionally.
void disconnect() {
_intentionalClose = true;
+ _reconnectTimer?.cancel();
+ _reconnectTimer = null;
+ _stabilityTimer?.cancel();
+ _stabilityTimer = null;
+ _reconnectAttempt = 0;
_updatesSub?.cancel();
_updatesSub = null;
_connectivitySub?.cancel();
@@ -799,8 +851,12 @@
case AppLifecycleState.resumed:
if (_intentionalClose) break;
_mqttLog('MQTT: app resumed');
- // Let autoReconnect handle dead connections (keepalive timeout).
- // Just trigger catch_up to fetch missed messages and rebuild UI.
+ // If disconnected, trigger immediate reconnect (reset backoff).
+ if (_status != ConnectionStatus.connected) {
+ _reconnectAttempt = 0;
+ _scheduleReconnect();
+ }
+ // Trigger catch_up to fetch missed messages and rebuild UI.
onResume?.call();
case AppLifecycleState.paused:
break;
--
Gitblit v1.3.1