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>
268 lines
9.1 KiB
C++
268 lines
9.1 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
#include "trydanmodbustcpmaster.h"
|
|
|
|
#include <modbustcpmaster.h>
|
|
|
|
#include <QtSerialBus>
|
|
|
|
#include "extern-plugininfo.h"
|
|
|
|
TrydanModbusTcpMaster::TrydanModbusTcpMaster(const QHostAddress &address,
|
|
quint16 port,
|
|
quint16 slaveId,
|
|
QObject *parent)
|
|
: TrydanModbusMaster(parent),
|
|
m_slaveId(slaveId)
|
|
{
|
|
m_modbusTcpMaster = new ModbusTcpMaster(address, port, this);
|
|
m_modbusTcpMaster->setNumberOfRetries(1);
|
|
m_modbusTcpMaster->setTimeout(2000);
|
|
|
|
connect(m_modbusTcpMaster, &ModbusTcpMaster::connectionStateChanged,
|
|
this, &TrydanModbusTcpMaster::onConnectionStateChanged);
|
|
}
|
|
|
|
TrydanModbusTcpMaster::~TrydanModbusTcpMaster()
|
|
{
|
|
// m_modbusTcpMaster is a child QObject and will be deleted automatically.
|
|
}
|
|
|
|
ModbusTcpMaster *TrydanModbusTcpMaster::modbusTcpMaster() const
|
|
{
|
|
return m_modbusTcpMaster;
|
|
}
|
|
|
|
void TrydanModbusTcpMaster::setHostAddress(const QHostAddress &address)
|
|
{
|
|
m_modbusTcpMaster->setHostAddress(address);
|
|
}
|
|
|
|
void TrydanModbusTcpMaster::connectDevice()
|
|
{
|
|
m_modbusTcpMaster->connectDevice();
|
|
}
|
|
|
|
void TrydanModbusTcpMaster::disconnectDevice()
|
|
{
|
|
m_modbusTcpMaster->disconnectDevice();
|
|
setReachable(false);
|
|
}
|
|
|
|
bool TrydanModbusTcpMaster::initialize()
|
|
{
|
|
if (!m_modbusTcpMaster->connected() || m_initializing)
|
|
return false;
|
|
|
|
m_initializing = true;
|
|
runReadSequence(buildInitSteps(), [this](bool ok) {
|
|
m_initializing = false;
|
|
if (ok) {
|
|
// qBound to IEC 61851 absolute limits [6, 32]. The real installation
|
|
// ceiling comes from MaxIntensity read above (a 16 A mono Trydan reports
|
|
// 16, not 32). The lower guard (qMax 6 was wrong for aberrant 0 returns:
|
|
// qMin(32, 0)=0 → maxIntensity()==0 → every setpoint gets clamped to 0
|
|
// and then the <6 A path pauses charging silently). qBound floors to 6
|
|
// on a spurious 0 and caps to 32 on a spurious 65535.
|
|
m_minIntensity = qBound(6, m_minIntensity, 32);
|
|
m_maxIntensity = qBound(6, m_maxIntensity, 32);
|
|
}
|
|
emit initializationFinished(ok);
|
|
});
|
|
return true;
|
|
}
|
|
|
|
bool TrydanModbusTcpMaster::update()
|
|
{
|
|
if (!m_modbusTcpMaster->connected() || m_updating)
|
|
return false;
|
|
|
|
m_updating = true;
|
|
runReadSequence(buildPollSteps(), [this](bool ok) {
|
|
m_updating = false;
|
|
if (ok) {
|
|
m_errorCount = 0;
|
|
setReachable(true);
|
|
emit updateFinished();
|
|
} else {
|
|
handleError();
|
|
}
|
|
});
|
|
return true;
|
|
}
|
|
|
|
void TrydanModbusTcpMaster::writePauseState(quint16 value)
|
|
{
|
|
writeFC6(WRegPauseState, value,
|
|
[this](quint16 addr, bool ok) { emit writeCompleted(addr, ok); });
|
|
}
|
|
|
|
void TrydanModbusTcpMaster::writeLock(quint16 value)
|
|
{
|
|
writeFC6(WRegLock, value,
|
|
[this](quint16 addr, bool ok) { emit writeCompleted(addr, ok); });
|
|
}
|
|
|
|
void TrydanModbusTcpMaster::writeIntensity(quint16 amps)
|
|
{
|
|
writeFC6(WRegIntensity, amps,
|
|
[this](quint16 addr, bool ok) { emit writeCompleted(addr, ok); });
|
|
}
|
|
|
|
void TrydanModbusTcpMaster::writePauseDynamic(quint16 value)
|
|
{
|
|
// PauseDynamic is also emitted via writeCompleted so the plugin can update
|
|
// the optimizerSuspended state if needed; action handlers must filter by address.
|
|
writeFC6(WRegPauseDynamic, value,
|
|
[this](quint16 addr, bool ok) { emit writeCompleted(addr, ok); });
|
|
}
|
|
|
|
// --- Private implementation ---
|
|
|
|
QList<TrydanModbusTcpMaster::ReadStep> TrydanModbusTcpMaster::buildInitSteps()
|
|
{
|
|
return {
|
|
{ RegMinIntensity, [this](bool ok, quint16 h, quint16 l) {
|
|
if (ok) m_minIntensity = decodeIntFromFloat32(h, l);
|
|
}},
|
|
{ RegMaxIntensity, [this](bool ok, quint16 h, quint16 l) {
|
|
if (ok) m_maxIntensity = decodeIntFromFloat32(h, l);
|
|
}},
|
|
};
|
|
}
|
|
|
|
QList<TrydanModbusTcpMaster::ReadStep> TrydanModbusTcpMaster::buildPollSteps()
|
|
{
|
|
// Each register pair is a separate FC3 transaction (2 registers, float32 Big/Big).
|
|
// Order matters: Dynamic must always be read (last) to reflect the most current
|
|
// state before the plugin applies the conflict-management logic.
|
|
return {
|
|
{ RegChargeState, [this](bool ok, quint16 h, quint16 l) {
|
|
if (ok) m_chargeState = decodeIntFromFloat32(h, l);
|
|
}},
|
|
{ RegChargePower, [this](bool ok, quint16 h, quint16 l) {
|
|
if (ok) m_chargePower = decodeFloat32BB(h, l);
|
|
}},
|
|
{ RegChargeEnergy, [this](bool ok, quint16 h, quint16 l) {
|
|
if (ok) m_chargeEnergy = decodeFloat32BB(h, l);
|
|
}},
|
|
{ RegSlaveError, [this](bool ok, quint16 h, quint16 l) {
|
|
if (ok) m_slaveError = decodeIntFromFloat32(h, l);
|
|
}},
|
|
{ RegHousePower, [this](bool ok, quint16 h, quint16 l) {
|
|
if (ok) m_housePower = decodeFloat32BB(h, l);
|
|
}},
|
|
{ RegPowerFV, [this](bool ok, quint16 h, quint16 l) {
|
|
if (ok) m_powerFV = decodeFloat32BB(h, l);
|
|
}},
|
|
{ RegPauseState, [this](bool ok, quint16 h, quint16 l) {
|
|
if (ok) m_pauseState = decodeIntFromFloat32(h, l);
|
|
}},
|
|
{ RegLock, [this](bool ok, quint16 h, quint16 l) {
|
|
if (ok) m_lock = decodeIntFromFloat32(h, l);
|
|
}},
|
|
{ RegIntensity, [this](bool ok, quint16 h, quint16 l) {
|
|
if (ok) m_intensity = decodeIntFromFloat32(h, l);
|
|
}},
|
|
{ RegDynamic, [this](bool ok, quint16 h, quint16 l) {
|
|
// Critical: must be read every poll — the V2C app can toggle this at any time.
|
|
if (ok) m_dynamic = decodeIntFromFloat32(h, l);
|
|
}},
|
|
{ RegPauseDynamic, [this](bool ok, quint16 h, quint16 l) {
|
|
if (ok) m_pauseDynamic = decodeIntFromFloat32(h, l);
|
|
}},
|
|
};
|
|
}
|
|
|
|
void TrydanModbusTcpMaster::runReadSequence(QList<ReadStep> steps,
|
|
std::function<void(bool)> onDone)
|
|
{
|
|
doNextRead(std::move(steps), 0, std::move(onDone));
|
|
}
|
|
|
|
void TrydanModbusTcpMaster::doNextRead(QList<ReadStep> steps,
|
|
int index,
|
|
std::function<void(bool)> onDone)
|
|
{
|
|
if (index >= steps.count()) {
|
|
onDone(true);
|
|
return;
|
|
}
|
|
|
|
const quint16 address = steps.at(index).first;
|
|
const ReadCallback callback = steps.at(index).second;
|
|
|
|
// Individual FC3 read of exactly 2 registers (one float32 value).
|
|
// Never read a range spanning multiple values — overlapping windows would
|
|
// return garbage for addresses beyond the first. cf. modbus.py _read_register.
|
|
QModbusReply *reply = m_modbusTcpMaster->sendReadRequest(
|
|
QModbusDataUnit(QModbusDataUnit::HoldingRegisters, address, 2),
|
|
static_cast<int>(m_slaveId));
|
|
|
|
if (!reply) {
|
|
qCWarning(dcV2C()) << "TrydanModbusTcpMaster: sendReadRequest returned null for reg" << Qt::hex << address;
|
|
onDone(false);
|
|
return;
|
|
}
|
|
|
|
connect(reply, &QModbusReply::finished, this,
|
|
[this, reply, steps, index, callback, onDone]() mutable {
|
|
reply->deleteLater();
|
|
|
|
if (reply->error() != QModbusDevice::NoError) {
|
|
qCWarning(dcV2C()) << "TrydanModbusTcpMaster: read error for reg"
|
|
<< Qt::hex << steps.at(index).first
|
|
<< ":" << reply->errorString();
|
|
callback(false, 0, 0);
|
|
onDone(false);
|
|
return;
|
|
}
|
|
|
|
const auto values = reply->result().values();
|
|
callback(true, values.value(0), values.value(1));
|
|
doNextRead(std::move(steps), index + 1, std::move(onDone));
|
|
});
|
|
}
|
|
|
|
void TrydanModbusTcpMaster::writeFC6(quint16 address, quint16 value,
|
|
std::function<void(quint16, bool)> callback)
|
|
{
|
|
// FC6 = Write Single Register; QModbusTcpClient uses FC6 for single-word writes.
|
|
QModbusDataUnit unit(QModbusDataUnit::HoldingRegisters, address, 1);
|
|
unit.setValue(0, value);
|
|
|
|
QModbusReply *reply = m_modbusTcpMaster->sendWriteRequest(unit, static_cast<int>(m_slaveId));
|
|
if (!reply) {
|
|
qCWarning(dcV2C()) << "TrydanModbusTcpMaster: sendWriteRequest returned null for reg" << Qt::hex << address;
|
|
callback(address, false);
|
|
return;
|
|
}
|
|
|
|
connect(reply, &QModbusReply::finished, this, [reply, address, callback]() {
|
|
reply->deleteLater();
|
|
callback(address, reply->error() == QModbusDevice::NoError);
|
|
});
|
|
}
|
|
|
|
void TrydanModbusTcpMaster::onConnectionStateChanged(bool connected)
|
|
{
|
|
if (connected) {
|
|
m_errorCount = 0;
|
|
// Signal reachability so the plugin calls initialize().
|
|
setReachable(true);
|
|
} else {
|
|
m_updating = false;
|
|
m_initializing = false;
|
|
setReachable(false);
|
|
}
|
|
}
|
|
|
|
void TrydanModbusTcpMaster::handleError()
|
|
{
|
|
m_errorCount++;
|
|
if (m_errorCount >= k_errorLimit) {
|
|
qCWarning(dcV2C()) << "TrydanModbusTcpMaster: error limit reached, marking unreachable";
|
|
setReachable(false);
|
|
}
|
|
}
|