fix(mqtt): route both reconnect ladders through the stopped-client guard

reconnectSlotClient() checks isStarted() and calls connect() instead of
reconnect() when the client was stopped, but only the post-NTP stale-token
path used it. The ordinary backoff ladder and the circuit-breaker probe called
slot.client->reconnect() directly, and esp_mqtt_client_reconnect() is a no-op
on a client that is not started.

Two ways in. connect() sets _started only when esp_mqtt_client_start() returns
ESP_OK while setupSlot() sets initial_connect_done unconditionally, so a start
failure under heap pressure stranded the slot. More routinely, the WiFi-drop
handler calls disconnect() on every connected slot, which clears _started —
after that the ladder issued no-ops forever and the slot never came back.

Not caught by the soaks: the log line the guard prints can only come from the
NTP path, so a stranded slot and a slot that never entered the state produce
identical logs. Observed reconnects were broker-side drops with WiFi up, which
leave the client started.

The renewal-bounce path keeps its own isStarted() branch — it needs
softDisconnect(), which the helper does not do.
This commit is contained in:
agessaman committed 2026-08-14 09:49:16 -07:00
1 parent c0c823b6b0
commit 6ffadd6e72
1 file changed
+6 -5
+6 -5
View File
@@ -2280,10 +2280,10 @@ void MQTTBridge::maintainSlotConnection(int index, unsigned long now_millis, uns
(_radio && _radio->getLastRecvMillis() > 0) ? (_ms->getMillis() - _radio->getLastRecvMillis()) : 0);
if (slot_uses_jwt) {
prepareJwtReconnect(true, -1);
slot.client->reconnect();
} else {
slot.client->reconnect();
}
// Via the helper: reconnect() is a no-op on a client the WiFi-drop path
// stopped, which would probe forever without ever starting it.
reconnectSlotClient(index);
// If the connect callback fires and sets slot.connected = true,
// it will clear circuit_breaker_tripped via the onConnect handler
}
@@ -2314,12 +2314,13 @@ void MQTTBridge::maintainSlotConnection(int index, unsigned long now_millis, uns
_last_slot_reconnect_ms = now_millis;
if (slot_uses_jwt) {
prepareJwtReconnect(false, slot.reconnect_backoff);
slot.client->reconnect();
} else {
// Non-JWT slots — lightweight reconnect on existing client.
MQTT_DEBUG_PRINTLN("MQTT%d reconnect (non-JWT, backoff %d)", index + 1, slot.reconnect_backoff);
slot.client->reconnect();
}
// Via the helper: reconnect() is a no-op on a client the WiFi-drop path
// stopped, which would back off forever without ever starting it.
reconnectSlotClient(index);
}
}
}