From 2a53a1695196b81a77bcb7db04dd5863a23bc133 Mon Sep 17 00:00:00 2001 From: Florian <1technophile@users.noreply.github.com> Date: Sun, 28 Jun 2026 19:14:23 -0500 Subject: [PATCH] Add MQTT auto-reconnect with exponential backoff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/MqttManager.cpp | 86 ++++++++++++++++++++++++++++++++++++++++++++- src/MqttManager.h | 19 ++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/src/MqttManager.cpp b/src/MqttManager.cpp index ddb60c2..94c0360 100644 --- a/src/MqttManager.cpp +++ b/src/MqttManager.cpp @@ -31,6 +31,7 @@ #include #include +#include /* ************************************************************************** */ @@ -92,6 +93,10 @@ bool MqttManager::connect() { //qDebug() << "MqttManager::connect()"; + // We intend to stay connected from now on; an unexpected drop should + // arm the auto-reconnect backoff (see scheduleReconnect()). + m_reconnectWanted = true; + SettingsManager *sm = SettingsManager::getInstance(); m_mqttclient->setHostname(sm->getMqttHost()); m_mqttclient->setPort(sm->getMqttPort()); @@ -139,6 +144,14 @@ void MqttManager::disconnect() { #if defined(ENABLE_MQTT) + // Explicit disconnect (user toggled MQTT off, or a forced reconnect is + // about to re-dial): stop wanting a connection and cancel any pending + // backoff so the timer doesn't re-dial behind the user's back. Reset the + // interval so the next session starts from the base delay. + m_reconnectWanted = false; + m_reconnectInterval = kReconnectBaseMs; + if (m_reconnectTimer) m_reconnectTimer->stop(); + if (m_mqttclient) { //qDebug() << "MqttManager::disconnect()"; @@ -183,6 +196,63 @@ void MqttManager::reconnect() #endif } +void MqttManager::scheduleReconnect() +{ +#if defined(ENABLE_MQTT) + + // Only re-dial if we still want to be connected and MQTT is enabled. + if (!m_reconnectWanted) return; + + SettingsManager *sm = SettingsManager::getInstance(); + if (!sm || !sm->getMQTT()) return; + + if (!m_reconnectTimer) + { + m_reconnectTimer = new QTimer(this); + m_reconnectTimer->setSingleShot(true); + QObject::connect(m_reconnectTimer, &QTimer::timeout, + this, &MqttManager::reconnectTimerFired); + } + + // A burst of Disconnected state-changes must not stack timers or grow the + // backoff multiple times — one armed attempt at a time. + if (m_reconnectTimer->isActive()) return; + + m_reconnectTimer->start(m_reconnectInterval); + logLine(QString("auto-reconnect in %1s").arg(m_reconnectInterval / 1000)); + + // Exponential backoff for the *next* attempt, capped. Reset to the base + // interval happens on a successful connect (brokerConnected) or an + // explicit disconnect(). + m_reconnectInterval = qMin(m_reconnectInterval * 2, kReconnectMaxMs); + +#endif +} + +void MqttManager::reconnectTimerFired() +{ +#if defined(ENABLE_MQTT) + + if (!m_reconnectWanted) return; + + // Something already (re)connected us in the meantime — nothing to do. + if (m_mqttclient && + (m_mqttclient->state() == QMqttClient::Connected || + m_mqttclient->state() == QMqttClient::Connecting)) + { + return; + } + + logLine("auto-reconnect: attempting"); + connect(); + + // If this attempt fails, the client transitions back to Disconnected, + // updateStateChange() fires scheduleReconnect() again, and the next + // (longer) backoff interval is armed. + +#endif +} + /* ************************************************************************** */ /* ************************************************************************** */ @@ -292,7 +362,16 @@ void MqttManager::updateStateChange() //qDebug() << "MqttManager::updateStateChange()" << m_mqttclient->state(); Q_EMIT statusChanged(); - if (m_mqttclient->state() == QMqttClient::Disconnected) logLine("status: disconnected"); + if (m_mqttclient->state() == QMqttClient::Disconnected) + { + logLine("status: disconnected"); + + // Both unexpected drops (broker restart) and failed reconnect + // attempts (broker still down) land here as Disconnected. Arm the + // backoff; scheduleReconnect() no-ops unless we still want to be + // connected, so an explicit disconnect() won't re-dial. + scheduleReconnect(); + } else if (m_mqttclient->state() == QMqttClient::Connecting) logLine("status: connecting"); else if (m_mqttclient->state() == QMqttClient::Connected) logLine("status: connected"); } @@ -308,6 +387,11 @@ void MqttManager::brokerConnected() if (m_mqttclient) { + // Connection re-established: cancel any pending auto-reconnect and + // reset the backoff so the next drop starts from the base interval. + m_reconnectInterval = kReconnectBaseMs; + if (m_reconnectTimer) m_reconnectTimer->stop(); + // Clear drop bookkeeping: the banner/notification hides automatically // once droppedSinceDisconnect == 0 and disconnectedSince is invalid. if (m_droppedSinceDisconnect != 0) diff --git a/src/MqttManager.h b/src/MqttManager.h index 6b80b8e..6dd21e8 100644 --- a/src/MqttManager.h +++ b/src/MqttManager.h @@ -30,6 +30,8 @@ #include #endif +class QTimer; + /* ************************************************************************** */ class Broker: public QObject @@ -116,6 +118,18 @@ class MqttManager: public QObject qint64 m_droppedSinceDisconnect = 0; QDateTime m_disconnectedSince; + // Auto-reconnect with exponential backoff. QtMqtt does NOT reconnect on + // its own, so an unexpected drop (e.g. broker restart) would otherwise + // strand the app on the disconnect banner until the user taps retry. + // m_reconnectWanted tracks intent: true while we want to stay connected, + // false after an explicit disconnect()/MQTT-off, so the timer never + // fights the user. + static constexpr int kReconnectBaseMs = 2000; + static constexpr int kReconnectMaxMs = 60000; + QTimer *m_reconnectTimer = nullptr; + int m_reconnectInterval = kReconnectBaseMs; + bool m_reconnectWanted = false; + QList m_brokersAvailable; QVariant getBrokersAvailable() const { return QVariant::fromValue(m_brokersAvailable); } @@ -136,6 +150,7 @@ private slots: void updateStateChange(); void brokerConnected(); void brokerDisconnected(); + void reconnectTimerFired(); private: // Prepend a timestamped line to m_mqttLog (newest on top) and emit @@ -143,6 +158,10 @@ private slots: // state-change / error / TLS handlers. void logLine(const QString &msg); + // Arm the single-shot backoff timer for the next reconnect attempt. + // No-op unless m_reconnectWanted (and MQTT is still enabled in settings). + void scheduleReconnect(); + public: static MqttManager *getInstance();