Skip to content

Commit 2a53a16

Browse files
1technophileclaude
andcommitted
Add MQTT auto-reconnect with exponential backoff
QMqttClient does not reconnect on its own, so an unexpected disconnect (broker restart, transient network drop) left the app stuck on the "broker disconnected" banner until the user manually tapped retry — while every other MQTT client on the network self-heals. MqttManager now arms a single-shot QTimer on entering the Disconnected state and re-dials with exponential backoff (2s base, 60s cap), resetting on a successful connect. An m_reconnectWanted intent flag gates the timer so an explicit disconnect() / MQTT-off never triggers an unwanted re-dial. The Disconnected hook also covers failed reconnect attempts, so the backoff keeps escalating until the broker returns. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent a39200f commit 2a53a16

2 files changed

Lines changed: 104 additions & 1 deletion

File tree

src/MqttManager.cpp

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
#include <QFile>
3333
#include <QTime>
34+
#include <QTimer>
3435

3536
/* ************************************************************************** */
3637

@@ -92,6 +93,10 @@ bool MqttManager::connect()
9293
{
9394
//qDebug() << "MqttManager::connect()";
9495

96+
// We intend to stay connected from now on; an unexpected drop should
97+
// arm the auto-reconnect backoff (see scheduleReconnect()).
98+
m_reconnectWanted = true;
99+
95100
SettingsManager *sm = SettingsManager::getInstance();
96101
m_mqttclient->setHostname(sm->getMqttHost());
97102
m_mqttclient->setPort(sm->getMqttPort());
@@ -139,6 +144,14 @@ void MqttManager::disconnect()
139144
{
140145
#if defined(ENABLE_MQTT)
141146

147+
// Explicit disconnect (user toggled MQTT off, or a forced reconnect is
148+
// about to re-dial): stop wanting a connection and cancel any pending
149+
// backoff so the timer doesn't re-dial behind the user's back. Reset the
150+
// interval so the next session starts from the base delay.
151+
m_reconnectWanted = false;
152+
m_reconnectInterval = kReconnectBaseMs;
153+
if (m_reconnectTimer) m_reconnectTimer->stop();
154+
142155
if (m_mqttclient)
143156
{
144157
//qDebug() << "MqttManager::disconnect()";
@@ -183,6 +196,63 @@ void MqttManager::reconnect()
183196
#endif
184197
}
185198

199+
void MqttManager::scheduleReconnect()
200+
{
201+
#if defined(ENABLE_MQTT)
202+
203+
// Only re-dial if we still want to be connected and MQTT is enabled.
204+
if (!m_reconnectWanted) return;
205+
206+
SettingsManager *sm = SettingsManager::getInstance();
207+
if (!sm || !sm->getMQTT()) return;
208+
209+
if (!m_reconnectTimer)
210+
{
211+
m_reconnectTimer = new QTimer(this);
212+
m_reconnectTimer->setSingleShot(true);
213+
QObject::connect(m_reconnectTimer, &QTimer::timeout,
214+
this, &MqttManager::reconnectTimerFired);
215+
}
216+
217+
// A burst of Disconnected state-changes must not stack timers or grow the
218+
// backoff multiple times — one armed attempt at a time.
219+
if (m_reconnectTimer->isActive()) return;
220+
221+
m_reconnectTimer->start(m_reconnectInterval);
222+
logLine(QString("auto-reconnect in %1s").arg(m_reconnectInterval / 1000));
223+
224+
// Exponential backoff for the *next* attempt, capped. Reset to the base
225+
// interval happens on a successful connect (brokerConnected) or an
226+
// explicit disconnect().
227+
m_reconnectInterval = qMin(m_reconnectInterval * 2, kReconnectMaxMs);
228+
229+
#endif
230+
}
231+
232+
void MqttManager::reconnectTimerFired()
233+
{
234+
#if defined(ENABLE_MQTT)
235+
236+
if (!m_reconnectWanted) return;
237+
238+
// Something already (re)connected us in the meantime — nothing to do.
239+
if (m_mqttclient &&
240+
(m_mqttclient->state() == QMqttClient::Connected ||
241+
m_mqttclient->state() == QMqttClient::Connecting))
242+
{
243+
return;
244+
}
245+
246+
logLine("auto-reconnect: attempting");
247+
connect();
248+
249+
// If this attempt fails, the client transitions back to Disconnected,
250+
// updateStateChange() fires scheduleReconnect() again, and the next
251+
// (longer) backoff interval is armed.
252+
253+
#endif
254+
}
255+
186256
/* ************************************************************************** */
187257
/* ************************************************************************** */
188258

@@ -292,7 +362,16 @@ void MqttManager::updateStateChange()
292362
//qDebug() << "MqttManager::updateStateChange()" << m_mqttclient->state();
293363
Q_EMIT statusChanged();
294364

295-
if (m_mqttclient->state() == QMqttClient::Disconnected) logLine("status: disconnected");
365+
if (m_mqttclient->state() == QMqttClient::Disconnected)
366+
{
367+
logLine("status: disconnected");
368+
369+
// Both unexpected drops (broker restart) and failed reconnect
370+
// attempts (broker still down) land here as Disconnected. Arm the
371+
// backoff; scheduleReconnect() no-ops unless we still want to be
372+
// connected, so an explicit disconnect() won't re-dial.
373+
scheduleReconnect();
374+
}
296375
else if (m_mqttclient->state() == QMqttClient::Connecting) logLine("status: connecting");
297376
else if (m_mqttclient->state() == QMqttClient::Connected) logLine("status: connected");
298377
}
@@ -308,6 +387,11 @@ void MqttManager::brokerConnected()
308387

309388
if (m_mqttclient)
310389
{
390+
// Connection re-established: cancel any pending auto-reconnect and
391+
// reset the backoff so the next drop starts from the base interval.
392+
m_reconnectInterval = kReconnectBaseMs;
393+
if (m_reconnectTimer) m_reconnectTimer->stop();
394+
311395
// Clear drop bookkeeping: the banner/notification hides automatically
312396
// once droppedSinceDisconnect == 0 and disconnectedSince is invalid.
313397
if (m_droppedSinceDisconnect != 0)

src/MqttManager.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
#include <QtMqtt/QtMqtt>
3131
#endif
3232

33+
class QTimer;
34+
3335
/* ************************************************************************** */
3436

3537
class Broker: public QObject
@@ -116,6 +118,18 @@ class MqttManager: public QObject
116118
qint64 m_droppedSinceDisconnect = 0;
117119
QDateTime m_disconnectedSince;
118120

121+
// Auto-reconnect with exponential backoff. QtMqtt does NOT reconnect on
122+
// its own, so an unexpected drop (e.g. broker restart) would otherwise
123+
// strand the app on the disconnect banner until the user taps retry.
124+
// m_reconnectWanted tracks intent: true while we want to stay connected,
125+
// false after an explicit disconnect()/MQTT-off, so the timer never
126+
// fights the user.
127+
static constexpr int kReconnectBaseMs = 2000;
128+
static constexpr int kReconnectMaxMs = 60000;
129+
QTimer *m_reconnectTimer = nullptr;
130+
int m_reconnectInterval = kReconnectBaseMs;
131+
bool m_reconnectWanted = false;
132+
119133
QList <Broker *> m_brokersAvailable;
120134
QVariant getBrokersAvailable() const { return QVariant::fromValue(m_brokersAvailable); }
121135

@@ -136,13 +150,18 @@ private slots:
136150
void updateStateChange();
137151
void brokerConnected();
138152
void brokerDisconnected();
153+
void reconnectTimerFired();
139154

140155
private:
141156
// Prepend a timestamped line to m_mqttLog (newest on top) and emit
142157
// logChanged so the QML broker panel re-renders. Called from the
143158
// state-change / error / TLS handlers.
144159
void logLine(const QString &msg);
145160

161+
// Arm the single-shot backoff timer for the next reconnect attempt.
162+
// No-op unless m_reconnectWanted (and MQTT is still enabled in settings).
163+
void scheduleReconnect();
164+
146165
public:
147166
static MqttManager *getInstance();
148167

0 commit comments

Comments
 (0)