etm-powersync-plugins-modbus/v2c/trydanmodbusmaster.cpp
Patrick Schurig 14f07cc638 feat(v2c): add Modbus transport abstraction + TCP implementation
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>
2026-06-12 13:49:11 +02:00

33 lines
974 B
C++

// SPDX-License-Identifier: GPL-3.0-or-later
#include "trydanmodbusmaster.h"
#include <cstring>
TrydanModbusMaster::TrydanModbusMaster(QObject *parent)
: QObject(parent)
{}
float TrydanModbusMaster::decodeFloat32BB(quint16 high, quint16 low)
{
// High word is the most-significant 16 bits (Big word order).
// Within each word bytes are already in big-endian order on the wire.
// This mirrors pymodbus BinaryPayloadDecoder(byteorder=Endian.Big, wordorder=Endian.Big).
quint32 raw = (static_cast<quint32>(high) << 16) | static_cast<quint32>(low);
float result;
std::memcpy(&result, &raw, sizeof(float));
return result;
}
int TrydanModbusMaster::decodeIntFromFloat32(quint16 high, quint16 low)
{
return static_cast<int>(qRound(decodeFloat32BB(high, low)));
}
void TrydanModbusMaster::setReachable(bool reachable)
{
if (m_reachable == reachable)
return;
m_reachable = reachable;
emit reachableChanged(reachable);
}