v2c: connexion TCP persistante — fin des rafales SYN

Diagnostic tcpdump : le plugin ouvrait/fermait la socket Modbus TCP à
chaque cycle de poll, générant 6-7 paquets SYN consécutifs qui saturaient
l'ESP32 WiFi de la borne (timeouts en boucle). mbpoll avec une connexion
persistante = 0 % d'échec.

Corrections :
- connectDevice() : guard "si déjà connecté → no-op" (élimine le warning
  "already in ConnectedState" et les FIN/RESET parasites).
- onConnectionStateChanged(true) : appelle initialize() directement au lieu
  de setReachable(true) — évite la ré-entrée initialize() via reachableChanged.
- initialize() : setReachable(true) uniquement au succès, après lecture
  MinIntensity/MaxIntensity ; guard || m_updating ajouté.
- update() : guard || m_initializing ajouté (pas de poll concurrent avec init).
- Timer : if (!master->update()) master->connectDevice() — le socket reste
  ouvert tant que la connexion TCP est active ; connectDevice() n'est appelé
  que quand le TCP est réellement coupé.
- initializationFinished(thing) : master->update() immédiat après reconnect.
- reachableChanged handler : suppression de l'appel à initialize() (déplacé).

Résultat attendu tcpdump : 1 seul SYN à l'établissement, puis uniquement
des échanges Modbus (P. length 12/13) toutes les 30 s, aucun FIN/RESET.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Patrick Schurig 2026-06-13 10:37:09 +02:00
parent 189d58d9ce
commit 186f195900
2 changed files with 42 additions and 17 deletions

View File

@ -67,18 +67,19 @@ void IntegrationPluginV2c::setupThing(ThingSetupInfo *info)
connect(info, &ThingSetupInfo::aborted, master, &TrydanModbusTcpMaster::deleteLater);
// Reachable → initialize (reads MinIntensity, MaxIntensity).
// Lost → set disconnected state with timestamp for SAV visibility.
// Reachability is driven by Modbus poll results (not TCP state directly).
// initialize() is now called from TrydanModbusTcpMaster::onConnectionStateChanged(true)
// so we no longer need to call it here — this handler only updates the UI.
connect(master, &TrydanModbusMaster::reachableChanged, thing,
[this, thing, master](bool reachable) {
if (reachable) {
master->initialize();
} else {
[this, thing](bool reachable) {
if (!reachable) {
setDisconnectedState(thing, QStringLiteral("timeout Modbus — vérifier signal WiFi de la borne"));
}
// reachable=true: UI cleared by next successful poll (updateThingStates).
});
// After init, expose the configured current range and finish setup.
// After init (first time or reconnect): expose current range and kick an immediate poll
// so states are refreshed without waiting up to 30 s for the next timer tick.
connect(master, &TrydanModbusMaster::initializationFinished, thing,
[this, thing, master](bool success) {
if (!success) {
@ -89,6 +90,9 @@ void IntegrationPluginV2c::setupThing(ThingSetupInfo *info)
thing->setStateMinMaxValues(trydanMaxChargingCurrentStateTypeId,
master->minIntensity(),
master->maxIntensity());
// Immediate first poll. For setup the info handler below also calls update();
// the m_updating guard makes the second call a no-op.
master->update();
});
connect(master, &TrydanModbusMaster::initializationFinished, info,
@ -124,12 +128,12 @@ void IntegrationPluginV2c::postSetupThing(Thing *thing)
m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(30);
connect(m_pluginTimer, &PluginTimer::timeout, this, [this]() {
for (TrydanModbusTcpMaster *master : m_tcpMasters) {
if (master->reachable()) {
master->update();
} else {
// TCP connection may have dropped (charger WiFi glitch).
// connectDevice() is idempotent on an already-connecting socket;
// the 30s timer provides the reconnect backoff — no busy-loop.
// update() returns false only when the TCP socket is not connected
// (or a poll/init is already running). connectDevice() is a no-op
// if the socket is already connected — it will NOT close and reopen
// the connection, avoiding the SYN bursts that saturate the ESP32 WiFi.
// Normal path: TCP stays open, only Modbus frames exchanged every 30 s.
if (!master->update()) {
master->connectDevice();
}
}

View File

@ -44,6 +44,11 @@ void TrydanModbusTcpMaster::setHostAddress(const QHostAddress &address)
void TrydanModbusTcpMaster::connectDevice()
{
// Guard: calling connectDevice() on an already-connected QModbusTcpClient
// logs a warning AND closes+reopens the TCP socket, generating a SYN burst
// that saturates the Trydan ESP32 WiFi stack. Skip if already connected.
if (m_modbusTcpMaster->connected())
return;
m_modbusTcpMaster->connectDevice();
}
@ -55,7 +60,10 @@ void TrydanModbusTcpMaster::disconnectDevice()
bool TrydanModbusTcpMaster::initialize()
{
if (!m_modbusTcpMaster->connected() || m_initializing)
// Also guard against an ongoing poll: init and update share the same sequential
// read pipeline; running both concurrently would interleave FC3 transactions
// and corrupt both sequences.
if (!m_modbusTcpMaster->connected() || m_initializing || m_updating)
return false;
m_initializing = true;
@ -70,6 +78,12 @@ bool TrydanModbusTcpMaster::initialize()
// on a spurious 0 and caps to 32 on a spurious 65535.
m_minIntensity = qBound(6, m_minIntensity, 32);
m_maxIntensity = qBound(6, m_maxIntensity, 32);
// TCP is live and init succeeded — mark reachable so the plugin can
// update states. This is the ONLY place setReachable(true) is called
// from the connection path; onConnectionStateChanged(true) no longer
// does it directly, avoiding a re-entrant initialize() call via the
// reachableChanged signal.
setReachable(true);
}
emit initializationFinished(ok);
});
@ -78,7 +92,8 @@ bool TrydanModbusTcpMaster::initialize()
bool TrydanModbusTcpMaster::update()
{
if (!m_modbusTcpMaster->connected() || m_updating)
// Guard against concurrent init: both use the same sequential FC3 pipeline.
if (!m_modbusTcpMaster->connected() || m_updating || m_initializing)
return false;
m_updating = true;
@ -261,9 +276,15 @@ void TrydanModbusTcpMaster::onConnectionStateChanged(bool connected)
{
if (connected) {
m_errorCount = 0;
// Signal reachability so the plugin calls initialize().
setReachable(true);
// Start init directly — do NOT call setReachable(true) here.
// setReachable(true) is deferred to initialize()'s success callback so the
// plugin's reachableChanged handler can no longer trigger a second initialize()
// call via signal re-entry. The connection sequence is:
// TCP up → initialize() → setReachable(true) → reachableChanged(true) → UI update
initialize();
} else {
// TCP actually dropped: abort any in-flight sequence and go offline.
// The 30s poll timer will call connectDevice() → new TCP handshake.
m_updating = false;
m_initializing = false;
setReachable(false);