etm-powersync-plugins-modbus/abbterra/abbterratcpdiscovery.cpp
Patrick Schurig cbfb5df23e abbterra: make ABB Terra AC addable over Modbus TCP (etm2..etm5)
Follow-up to the etm1 fixes, driven by live debugging against a real ABB
Terra AC 7 kW behind a Waveshare RS485->TCP gateway (192.168.1.132, slave 2):

- etm3: disable TCP auto-discovery (createMethods = "user" only). Discovery
  blind-scans the LAN and SIGSEGVs nymead as soon as any Modbus device answers
  :502 with an exception response. The crash is in the generated reachability
  handler (abbterramodbustcpconnection.cpp:2062 dereferences the check-reachable
  reply after it is freed); it could not be fixed from the discovery code alone.
  A gateway's slave id can't be discovered anyway, so manual add is required.
- etm2: still defer/guard AbbTerraTcpDiscovery::cleanupConnection() teardown
  (kept for correctness even though discovery is now off by default).
- etm4: setupTcpThing() never called connectDevice(), so manual setup hung until
  timeout. Start the connection when the network monitor reports the device.
- etm5: decode the serial-number and firmware register fields as BCD per ABB
  spec §5.1 (0x46 = week 46, not 70; 0x22 = 22 kW). The binary reading rejected
  real chargers ("does not match the ABB Terra AC Modbus register map").

Verified end to end: charger adds cleanly (TACW7-4-4624-S0034), states poll and
actions execute, no SIGSEGV. The underlying discovery/generated-code crash is
tracked separately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 12:14:44 +02:00

151 lines
6.3 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
#include "abbterratcpdiscovery.h"
#include "abbterrautils.h"
#include "extern-plugininfo.h"
#include <QTime>
#include <QTimer>
#include <utility>
AbbTerraTcpDiscovery::AbbTerraTcpDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent)
: QObject(parent),
m_networkDeviceDiscovery(networkDeviceDiscovery)
{
}
void AbbTerraTcpDiscovery::startDiscovery()
{
qCInfo(dcAbbTerra()) << "Discovery: Starting to search for ABB Terra AC chargers on the network...";
m_startDateTime = QDateTime::currentDateTime();
m_networkDeviceInfos.clear();
m_temporaryResults.clear();
m_results.clear();
NetworkDeviceDiscoveryReply *discoveryReply = m_networkDeviceDiscovery->discover();
connect(discoveryReply, &NetworkDeviceDiscoveryReply::hostAddressDiscovered, this, &AbbTerraTcpDiscovery::checkNetworkDevice);
connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, discoveryReply, &NetworkDeviceDiscoveryReply::deleteLater);
connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [this, discoveryReply]() {
m_networkDeviceInfos = discoveryReply->networkDeviceInfos();
QTimer::singleShot(3000, this, &AbbTerraTcpDiscovery::finishDiscovery);
});
}
QList<AbbTerraTcpDiscovery::Result> AbbTerraTcpDiscovery::results() const
{
return m_results;
}
void AbbTerraTcpDiscovery::checkNetworkDevice(const QHostAddress &address)
{
AbbTerraModbusTcpConnection *connection = new AbbTerraModbusTcpConnection(address, 502, 1, this);
m_connections.append(connection);
connect(connection, &AbbTerraModbusTcpConnection::reachableChanged, this, [this, connection](bool reachable) {
if (!reachable) {
cleanupConnection(connection);
return;
}
connection->initialize();
});
connect(connection, &AbbTerraModbusTcpConnection::initializationFinished, this, [this, connection, address](bool success) {
if (!success) {
cleanupConnection(connection);
return;
}
const AbbTerraUtils::DeviceInfo deviceInfo = AbbTerraUtils::deviceInfoFromValues(connection->serialNumber(),
connection->firmwareVersionRaw(),
connection->userSettableMaxCurrent());
if (deviceInfo.valid) {
Result result;
result.serialNumber = deviceInfo.serialNumber;
result.productName = deviceInfo.productName;
result.firmwareVersion = deviceInfo.firmwareVersion;
result.networkDeviceInfo = m_networkDeviceInfos.get(address);
if (result.networkDeviceInfo.address().isNull()) {
NetworkDeviceInfo info;
info.setAddress(address);
result.networkDeviceInfo = info;
}
m_temporaryResults.append(result);
}
cleanupConnection(connection);
});
connect(connection->modbusTcpMaster(), &ModbusTcpMaster::connectionErrorOccurred, this, [this, connection](QModbusDevice::Error error) {
if (error != QModbusDevice::NoError) {
cleanupConnection(connection);
}
});
connect(connection, &AbbTerraModbusTcpConnection::checkReachabilityFailed, this, [this, connection]() {
cleanupConnection(connection);
});
connection->connectDevice();
}
void AbbTerraTcpDiscovery::cleanupConnection(AbbTerraModbusTcpConnection *connection)
{
// A single reachability failure fans out into several signals
// (reachableChanged, checkReachabilityFailed, connectionErrorOccurred), each
// wired to this method. removeAll() returns the number of entries actually
// removed, so the first call tears the connection down and every subsequent
// (possibly re-entrant) call becomes a no-op.
if (m_connections.removeAll(connection) == 0)
return;
// Stop reacting to any further signals from this connection (and its Modbus
// master) so the teardown below cannot re-enter this method.
disconnect(connection, nullptr, this, nullptr);
disconnect(connection->modbusTcpMaster(), nullptr, this, nullptr);
// Defer the actual teardown to the next event-loop iteration. cleanupConnection
// is reached *synchronously* from inside the connection's QModbusReply "finished"
// handler (the testReachability() lambda) via checkReachabilityFailed(): that
// handler still dereferences m_checkReachableReply after emitting the signal
// (abbterramodbustcpconnection.cpp:2062, "m_checkReachableReply->deleteLater()").
// Calling disconnectDevice() here synchronously frees/invalidates that in-flight
// reply mid-handler, so line 2062 dereferences freed memory -> SIGSEGV in
// QObject::deleteLater(). Queuing the teardown lets the finished handler unwind
// completely (freeing the reply itself) before we disconnect and delete.
QMetaObject::invokeMethod(connection, [connection]() {
connection->disconnectDevice();
connection->deleteLater();
}, Qt::QueuedConnection);
}
void AbbTerraTcpDiscovery::finishDiscovery()
{
foreach (const Result &result, m_temporaryResults) {
bool known = false;
foreach (const Result &existing, m_results) {
if (existing.serialNumber == result.serialNumber || existing.networkDeviceInfo.address() == result.networkDeviceInfo.address()) {
known = true;
break;
}
}
if (!known) {
qCDebug(dcAbbTerra()) << "Discovery: Found" << result.productName << result.networkDeviceInfo;
m_results.append(result);
}
}
const QList<AbbTerraModbusTcpConnection *> leftoverConnections = m_connections;
foreach (AbbTerraModbusTcpConnection *connection, leftoverConnections) {
cleanupConnection(connection);
}
const qint64 durationMilliSeconds = QDateTime::currentMSecsSinceEpoch() - m_startDateTime.toMSecsSinceEpoch();
qCInfo(dcAbbTerra()) << "Discovery: Finished ABB Terra AC network discovery in"
<< QTime::fromMSecsSinceStartOfDay(durationMilliSeconds).toString("mm:ss.zzz")
<< "with" << m_results.count() << "result(s).";
emit discoveryFinished();
}