Skip to content

Commit a847239

Browse files
ten9876M7HNF-Ianclaude
authored
fix(cluster): restore auto-reconnect after failed connection attempt (#2380) (#2394)
SpotHub (DX cluster / RBN) did not automatically reconnect after a Wi-Fi drop or any other failed connection attempt. The app would try once when the network came back, fail, and stop retrying — requiring a manual reconnect. Root cause: Qt's QAbstractSocket only emits disconnected() on a Connected → Unconnected transition. When a socket fails during ConnectingState (host blocked, refused, or timed out), Qt emits errorOccurred but NOT disconnected. The reconnect timer was only armed inside onDisconnected(), so a single failed connect attempt permanently halted the backoff chain. Two additional failure paths had the same gap: onSocketError() emitted the UI error only and never re-armed the timer, and the 10-second connect timeout lambda called abort() without scheduling a retry. Fix: extracts a scheduleReconnect() helper called from all three failure paths, with two guards — m_intentionalDisconnect (don't override user-initiated disconnect) and m_reconnectTimer->isActive() (prevents double-scheduling when errorOccurred and the connect timeout both fire for the same failed attempt). A per-call epoch counter (m_connectEpoch) captured by the timeout lambda ensures a stale timeout from attempt N cannot abort a later attempt N+1 that has since succeeded. connectToCluster() also now rejects calls when the socket is already in ConnectingState to prevent overlapping attempts during rapid retries. Backoff sequence is unchanged: 5 s → 10 s → 20 s → 40 s → 60 s (cap). Cherry-picked from PR #2388 (M7HNF-Ian). The rest of that branch contained already-merged commits (#2349 Spot Lines toggle, #2386 TCI crash fix), the rejected #2387 spectrum fix, and two unrelated spot-lines naming-refinement commits that belong in their own focused PR. Co-authored-by: Ian M7HNF <[email protected]> Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
1 parent 2d915d7 commit a847239

2 files changed

Lines changed: 32 additions & 11 deletions

File tree

src/core/DxClusterClient.cpp

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ QString DxClusterClient::logFilePath() const
4141

4242
void DxClusterClient::connectToCluster(const QString& host, quint16 port, const QString& callsign)
4343
{
44-
if (m_connected) {
45-
qCWarning(lcDxCluster) << "DxClusterClient: already connected";
44+
if (m_connected || m_socket->state() == QAbstractSocket::ConnectingState) {
45+
qCWarning(lcDxCluster) << "DxClusterClient: connect attempt already in progress";
4646
return;
4747
}
4848

@@ -56,12 +56,16 @@ void DxClusterClient::connectToCluster(const QString& host, quint16 port, const
5656
qCDebug(lcDxCluster) << "DxClusterClient: connecting to" << host << ":" << port;
5757
m_socket->connectToHost(host, port);
5858

59-
// Connection timeout
60-
QTimer::singleShot(ConnectTimeoutMs, this, [this] {
59+
// Connection timeout — capture epoch so a stale timeout from attempt N cannot
60+
// abort a later attempt that has already succeeded (#2380).
61+
const int epoch = ++m_connectEpoch;
62+
QTimer::singleShot(ConnectTimeoutMs, this, [this, epoch] {
63+
if (m_connectEpoch != epoch) return; // superseded by a later attempt
6164
if (!m_connected && m_socket->state() != QAbstractSocket::ConnectedState) {
6265
qCWarning(lcDxCluster) << "DxClusterClient: connection timeout";
6366
m_socket->abort();
6467
emit connectionError("Connection timeout");
68+
scheduleReconnect();
6569
}
6670
});
6771
}
@@ -121,20 +125,32 @@ void DxClusterClient::onDisconnected()
121125
if (wasConnected)
122126
emit disconnected();
123127

124-
if (!m_intentionalDisconnect) {
125-
int delay = std::min(InitialReconnectDelayMs * (1 << m_reconnectAttempts),
126-
MaxReconnectDelayMs);
127-
qCDebug(lcDxCluster) << "DxClusterClient: reconnecting in" << delay << "ms";
128-
m_reconnectTimer->start(delay);
129-
m_reconnectAttempts++;
130-
}
128+
scheduleReconnect();
131129
}
132130

133131
void DxClusterClient::onSocketError(QAbstractSocket::SocketError /*err*/)
134132
{
135133
QString msg = m_socket->errorString();
136134
qCWarning(lcDxCluster) << "DxClusterClient: socket error:" << msg;
137135
emit connectionError(msg);
136+
// When a connection attempt fails (ConnectingState → error), Qt does NOT emit
137+
// disconnected(), so onDisconnected() never fires. Arm the reconnect timer
138+
// here so the chain continues after a failed attempt (#2380).
139+
if (!m_connected) {
140+
scheduleReconnect();
141+
}
142+
}
143+
144+
void DxClusterClient::scheduleReconnect()
145+
{
146+
if (m_intentionalDisconnect) return;
147+
if (m_reconnectTimer->isActive()) return; // already scheduled — don't compound backoff
148+
int delay = std::min(InitialReconnectDelayMs * (1 << m_reconnectAttempts),
149+
MaxReconnectDelayMs);
150+
qCDebug(lcDxCluster) << "DxClusterClient: reconnecting in" << delay
151+
<< "ms (attempt" << m_reconnectAttempts + 1 << ")";
152+
m_reconnectTimer->start(delay);
153+
m_reconnectAttempts++;
138154
}
139155

140156
void DxClusterClient::onReconnectTimer()

src/core/DxClusterClient.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ private slots:
6262
bool isLoginPrompt(const QString& line) const;
6363
void handleLine(const QString& line);
6464
void stripTelnetIAC();
65+
// Arm the exponential-backoff reconnect timer. Guards against double-scheduling
66+
// (errorOccurred + timeout can both fire for one failed attempt). No-op when
67+
// m_intentionalDisconnect is set or the timer is already active (#2380).
68+
void scheduleReconnect();
6569

6670
QTcpSocket* m_socket;
6771
QByteArray m_readBuffer;
@@ -76,6 +80,7 @@ private slots:
7680
bool m_loggedIn{false};
7781
bool m_intentionalDisconnect{false};
7882
int m_reconnectAttempts{0};
83+
int m_connectEpoch{0}; // incremented each connectToCluster(); guards stale timeouts
7984

8085
static constexpr int MaxReconnectDelayMs = 60000;
8186
static constexpr int InitialReconnectDelayMs = 5000;

0 commit comments

Comments
 (0)