Trois polls échoués d'affilée marquent la borne injoignable. Sur le lien dégradé du 2026-08-28 (~70 % de perte), cela donne 12 bascules « unreachable » en vingt minutes. Une borne qui disparaît toutes les deux minutes ne se pilote pas, quel que soit le code : LM-1204-b, le plancher réel et EV_GRID_START porteraient tous ce bruit. Rien n'est changé dans le comportement — seulement documenté, parce que le seuil ne PEUT PAS être tranché sur un lien malade. Le protocole tient en trois étapes, à dérouler une fois le point d'accès rapproché : mesurer le taux de polls avortés, compter les bascules sur la même fenêtre, puis conclure. Si les bascules disparaissent, le seuil est bon et détecte vite une borne réellement absente ; si elles persistent malgré un bon signal, c'est lui qui est trop serré. Le relever maintenant masquerait le vrai défaut au lieu de le corriger, et retarderait d'autant la détection d'une borne vraiment partie. Commentaire seul côté code : le .so n'est pas modifié, pas de redéploiement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A41CTzMvir1Y5ABPXW8xpB
121 lines
4.8 KiB
C++
121 lines
4.8 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
#ifndef TRYDANMODBUSTCPMASTER_H
|
|
#define TRYDANMODBUSTCPMASTER_H
|
|
|
|
#include "trydanmodbusmaster.h"
|
|
|
|
#include <QHostAddress>
|
|
#include <QList>
|
|
#include <QPair>
|
|
#include <functional>
|
|
|
|
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<void(bool ok, quint16 high, quint16 low)>;
|
|
using ReadStep = QPair<quint16, ReadCallback>;
|
|
|
|
/*!
|
|
* \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<ReadStep> steps, std::function<void(bool)> onDone);
|
|
void doNextRead(QList<ReadStep> steps, int index, std::function<void(bool)> 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<void(quint16 addr, bool ok)> callback);
|
|
|
|
void onConnectionStateChanged(bool connected);
|
|
|
|
/*! \brief Increment the error counter and mark unreachable once it hits the limit. */
|
|
void handleError();
|
|
|
|
QList<ReadStep> buildInitSteps();
|
|
QList<ReadStep> 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.
|
|
//
|
|
// \warning SEUIL À RÉÉVALUER, pas encore tranché (2026-08-28). Sur un lien
|
|
// WiFi dégradé (70 % de polls perdus mesurés sur le banc), trois échecs
|
|
// d'affilée arrivent constamment : 12 bascules « unreachable » en vingt
|
|
// minutes, et chaque bascule fait DISPARAÎTRE la borne de l'arbitrage.
|
|
// Le protocole de décision est dans PORTING_STATUS.md — il faut mesurer
|
|
// AVEC UN LIEN SAIN avant de toucher à cette valeur : si les bascules
|
|
// persistent, c'est le seuil ; si elles disparaissent, il est bon. Monter
|
|
// le seuil sur un lien malade masquerait le vrai défaut au lieu de le
|
|
// corriger, et retarderait d'autant la détection d'une borne réellement
|
|
// absente.
|
|
static constexpr int k_errorLimit = 3;
|
|
};
|
|
|
|
#endif // TRYDANMODBUSTCPMASTER_H
|