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);