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 <noreply@anthropic.com>
- Decode SlaveError (reg 0x0BC5) with full table (codes 0-10) into new
slaveErrorMessage state; code 04 (WiFi reconnect) also surfaces in
statusMessage for immediate SAV visibility.
- Add L1/L2/L3 phase powers (regs 0x0BD9-0x0BDB, confirmed hw) as
diagnostic states polled every 30 s.
- Add RegChargeTime (0x0BC6) to enum for completeness (not yet exposed).
- Collapse debian/changelog to single 1.15.0+etm1 entry (new plugin).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Build fixes found during first local compilation (qmake6 + g++ -Werror):
1. Logging category: nymea-plugininfocompiler generates 'dcV2C' (uppercase C)
from the JSON vendor name "V2C". All three source files used 'dcV2c'
(lowercase c) — corrected to 'dcV2C' in integrationpluginv2c.cpp,
trydanmodbustcpmaster.cpp, v2ctcpdiscovery.cpp.
2. modbus.pri: hardcoded include(/usr/include/nymea-modbus/modbus-tool.pri)
fails when libnymea-modbus-dev is not system-installed. Changed to a
conditional: checks $$[QT_INSTALL_PREFIX] first, then NYMEA_MODBUS_PATH
override, with a graceful fallback (warning + manual C++17 CONFIG flag).
The v2c plugin has MODBUS_CONNECTIONS empty so the tool loop is a no-op
either way; the fallback is safe.
3. debian/: add powersync-plugin-v2c package entry (control, .install,
changelog entry 1.15.0+etm4).
Build verified: libnymea_integrationpluginv2c.so links against libnymea.so.1,
libnymea-modbus.so.1, Qt6Network/SerialBus/Core, 0 compiler warnings
(with -Werror -Wall -Wextra -std=c++17).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
qMin(32, m_maxIntensity) gives 0 when firmware returns 0 for MaxIntensity
(aberrant value). maxIntensity()==0 then causes every setMaxChargingCurrent
call to be clamped to 0, falling into the <6 A path which pauses charging
silently — the user sees the action succeed but the charger stops.
qBound(6, val, 32) is correct in all cases:
- firmware reports 16 A (mono Trydan) → 16 (real installation limit kept)
- firmware reports 0 (aberrant) → 6 (safe floor, does not claim 32 A)
- firmware reports 65535 (aberrant) → 32 (IEC absolute ceiling)
The real installation limit still comes from MaxIntensity read on the device;
the bounds [6, 32] are only IEC guardrails, not hardcoded defaults.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Architecture choice: separate the register map / business logic from the
physical transport. TrydanModbusMaster (abstract QObject) holds:
- the complete ReadRegister / WriteRegister enums with addresses
- float32 Big/Big decode helpers (memcpy pattern, matches pymodbus
BinaryPayloadDecoder byteorder=Endian.Big wordorder=Endian.Big)
- all last-polled cached values accessible via simple getters
TrydanModbusTcpMaster (concrete, Étape 1) wraps libnymea-modbus
ModbusTcpMaster. Each register read is a distinct FC3(addr, 2)
transaction via a recursive runReadSequence/doNextRead pattern —
never a block read, because the V2C firmware's register windows
overlap (0x0BC2 and 0x0BC3 each span 2 registers, so a range read
starting at 0x0BC2 for ≥3 registers returns garbage for the second
value). Writes use FC6 (single uint16, not float).
writeCompleted(quint16 address, bool success) carries the originating
register address so that concurrent background writes (e.g. PauseDynamic
from the conflict manager firing during an action) do not interfere with
action handlers waiting on a specific register's acknowledgement.
0x177E (Dynamic) is intentionally absent from WriteRegister: writing it
to 0 silences ChargePower telemetry even though the charger keeps running.
cf. evcc charger/trydan.go and github.com/evcc-io/evcc/issues/28047.
Étape 2 (RTU) will add TrydanModbusRtuMaster on the same interface;
no changes to plugin or register logic will be required.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>