From 186f1959005079c8fc999f981a89703e355e5f8f Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Sat, 13 Jun 2026 10:37:09 +0200 Subject: [PATCH] =?UTF-8?q?v2c:=20connexion=20TCP=20persistante=20?= =?UTF-8?q?=E2=80=94=20fin=20des=20rafales=20SYN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- v2c/integrationpluginv2c.cpp | 30 +++++++++++++++++------------- v2c/trydanmodbustcpmaster.cpp | 29 +++++++++++++++++++++++++---- 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/v2c/integrationpluginv2c.cpp b/v2c/integrationpluginv2c.cpp index b9e86fd..b10180a 100644 --- a/v2c/integrationpluginv2c.cpp +++ b/v2c/integrationpluginv2c.cpp @@ -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(); } } diff --git a/v2c/trydanmodbustcpmaster.cpp b/v2c/trydanmodbustcpmaster.cpp index 1afa278..90dc7a3 100644 --- a/v2c/trydanmodbustcpmaster.cpp +++ b/v2c/trydanmodbustcpmaster.cpp @@ -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);