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>
151 lines
6.3 KiB
C++
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();
|
|
}
|