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); connect(info, &ThingSetupInfo::aborted, master, &TrydanModbusTcpMaster::deleteLater);
// Reachable → initialize (reads MinIntensity, MaxIntensity). // Reachability is driven by Modbus poll results (not TCP state directly).
// Lost → set disconnected state with timestamp for SAV visibility. // 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, connect(master, &TrydanModbusMaster::reachableChanged, thing,
[this, thing, master](bool reachable) { [this, thing](bool reachable) {
if (reachable) { if (!reachable) {
master->initialize();
} else {
setDisconnectedState(thing, QStringLiteral("timeout Modbus — vérifier signal WiFi de la borne")); 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, connect(master, &TrydanModbusMaster::initializationFinished, thing,
[this, thing, master](bool success) { [this, thing, master](bool success) {
if (!success) { if (!success) {
@ -89,6 +90,9 @@ void IntegrationPluginV2c::setupThing(ThingSetupInfo *info)
thing->setStateMinMaxValues(trydanMaxChargingCurrentStateTypeId, thing->setStateMinMaxValues(trydanMaxChargingCurrentStateTypeId,
master->minIntensity(), master->minIntensity(),
master->maxIntensity()); 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, connect(master, &TrydanModbusMaster::initializationFinished, info,
@ -124,12 +128,12 @@ void IntegrationPluginV2c::postSetupThing(Thing *thing)
m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(30); m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(30);
connect(m_pluginTimer, &PluginTimer::timeout, this, [this]() { connect(m_pluginTimer, &PluginTimer::timeout, this, [this]() {
for (TrydanModbusTcpMaster *master : m_tcpMasters) { for (TrydanModbusTcpMaster *master : m_tcpMasters) {
if (master->reachable()) { // update() returns false only when the TCP socket is not connected
master->update(); // (or a poll/init is already running). connectDevice() is a no-op
} else { // if the socket is already connected — it will NOT close and reopen
// TCP connection may have dropped (charger WiFi glitch). // the connection, avoiding the SYN bursts that saturate the ESP32 WiFi.
// connectDevice() is idempotent on an already-connecting socket; // Normal path: TCP stays open, only Modbus frames exchanged every 30 s.
// the 30s timer provides the reconnect backoff — no busy-loop. if (!master->update()) {
master->connectDevice(); master->connectDevice();
} }
} }

View File

@ -44,6 +44,11 @@ void TrydanModbusTcpMaster::setHostAddress(const QHostAddress &address)
void TrydanModbusTcpMaster::connectDevice() 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(); m_modbusTcpMaster->connectDevice();
} }
@ -55,7 +60,10 @@ void TrydanModbusTcpMaster::disconnectDevice()
bool TrydanModbusTcpMaster::initialize() 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; return false;
m_initializing = true; m_initializing = true;
@ -70,6 +78,12 @@ bool TrydanModbusTcpMaster::initialize()
// on a spurious 0 and caps to 32 on a spurious 65535. // on a spurious 0 and caps to 32 on a spurious 65535.
m_minIntensity = qBound(6, m_minIntensity, 32); m_minIntensity = qBound(6, m_minIntensity, 32);
m_maxIntensity = qBound(6, m_maxIntensity, 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); emit initializationFinished(ok);
}); });
@ -78,7 +92,8 @@ bool TrydanModbusTcpMaster::initialize()
bool TrydanModbusTcpMaster::update() 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; return false;
m_updating = true; m_updating = true;
@ -261,9 +276,15 @@ void TrydanModbusTcpMaster::onConnectionStateChanged(bool connected)
{ {
if (connected) { if (connected) {
m_errorCount = 0; m_errorCount = 0;
// Signal reachability so the plugin calls initialize(). // Start init directly — do NOT call setReachable(true) here.
setReachable(true); // 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 { } 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_updating = false;
m_initializing = false; m_initializing = false;
setReachable(false); setReachable(false);