// SPDX-License-Identifier: GPL-3.0-or-later #ifndef TRYDANMODBUSTCPMASTER_H #define TRYDANMODBUSTCPMASTER_H #include "trydanmodbusmaster.h" #include #include #include #include class ModbusTcpMaster; class QModbusReply; /*! * \brief Modbus TCP transport for the V2C Trydan (Étape 1). * * Wraps a libnymea-modbus ModbusTcpMaster. All register reads are individual * FC3 transactions of exactly 2 registers — never batched — to avoid the * overlapping-window firmware bug of the V2C Trydan. * cf. modbus.py _read_register and AGENTS.md § Décodage lecture. * * Writes use FC6 (single-register, uint16, via ModbusTcpMaster::sendWriteRequest). * * Étape 2 (RTU) will add TrydanModbusRtuMaster on the same TrydanModbusMaster * interface without touching this file. */ class TrydanModbusTcpMaster : public TrydanModbusMaster { Q_OBJECT public: /*! * \param address IP address of the charger * \param port Modbus TCP port (default 502) * \param slaveId Modbus unit ID (always 1 for Trydan) */ explicit TrydanModbusTcpMaster(const QHostAddress &address, quint16 port, quint16 slaveId, QObject *parent = nullptr); ~TrydanModbusTcpMaster() override; /*! \brief Underlying TCP master — exposed so the plugin can update the host address. */ ModbusTcpMaster *modbusTcpMaster() const; /*! \brief Update the target IP without reconnecting (call reconnectDevice() separately). */ void setHostAddress(const QHostAddress &address); void connectDevice() override; void disconnectDevice() override; bool initialize() override; bool update() override; void writePauseState(quint16 value) override; void writeLock(quint16 value) override; void writeIntensity(quint16 amps) override; void writePauseDynamic(quint16 value) override; private: // Callback type for a single 2-register read result using ReadCallback = std::function; using ReadStep = QPair; /*! * \brief Execute \a steps sequentially, one FC3 read per register pair. * * The sequence aborts on the first failed read; \a onDone receives false in * that case and the error counter is incremented. On full success \a onDone * receives true and the error counter is reset. * * Sequential execution is required because concurrent reads from the same * slave on a Modbus TCP connection would not be incorrect per protocol, but * the firmware's overlapping register windows make ordering critical. */ void runReadSequence(QList steps, std::function onDone); void doNextRead(QList steps, int index, std::function onDone); /*! * \brief Send an FC6 (Write Single Register) request and invoke \a callback. * FC6 is used for all writes per the V2C spec (modbus.py _write_register). * \param callback Receives (address, success) so callers can emit writeCompleted * with the originating register address, enabling action handlers * to distinguish expected writes from background writes. */ void writeFC6(quint16 address, quint16 value, std::function callback); void onConnectionStateChanged(bool connected); /*! \brief Increment the error counter and mark unreachable once it hits the limit. */ void handleError(); QList buildInitSteps(); QList buildPollSteps(); ModbusTcpMaster *m_modbusTcpMaster = nullptr; quint16 m_slaveId = 1; int m_errorCount = 0; bool m_updating = false; bool m_initializing = false; // 3 consecutive failures → mark unreachable. With a 5 s timeout and no // retry, 3 failed polls abort within 15 s — acceptable for HEMS decisions. static constexpr int k_errorLimit = 3; }; #endif // TRYDANMODBUSTCPMASTER_H