The Trydan ESP32 WiFi stack saturates under nymea's periodic ICMP pings
(observed: latency > 14 s, Modbus timeouts, charger drops off).
Changes:
- NetworkDeviceMonitor removed entirely from setupThing() and thingRemoved().
The charger is expected to have a DHCP-reserved IP (documented in code).
Address comes from trydanThingAddressParamTypeId param — already the correct
path after the previous setup fix.
- Connectivity now derives from Modbus poll success/failure only (reachable()
from TrydanModbusTcpMaster), NOT from network ping. A ping response does
not imply Modbus usability on this hardware.
- postSetupThing() timer (30 s): if not reachable, calls connectDevice() for
reconnect; if reachable, calls update(). The 30 s period IS the backoff —
no aggressive retry loop.
- Modbus timeout: 2 s → 5 s (tolerates residual WiFi latency spikes).
- Modbus retries per read: 1 → 0 (abort fast on first timeout; full poll
sequence already aborts at first error via doNextRead).
- k_errorLimit: 5 → 3 (3 × 5 s = 15 s before marking unreachable).
- New state "statusMessage" (QString): set on disconnect with timestamp +
cause ("timeout Modbus — vérifier signal WiFi de la borne"); cleared on
successful poll. Visible in nymea-app; helps SAV without SSH access.
- "networkdevice" removed from JSON interfaces (contract broken without monitor).
Invariants preserved: Big/Big float32 decode, no block read, PauseState+Lock
mirror, PauseDynamic conflict management, writeCompleted address filtering.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
110 lines
4.1 KiB
C++
110 lines
4.1 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.
|
|
static constexpr int k_errorLimit = 3;
|
|
};
|
|
|
|
#endif // TRYDANMODBUSTCPMASTER_H
|