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>
This commit is contained in:
Patrick Schurig 2026-07-03 12:14:44 +02:00
parent 368c80688a
commit cbfb5df23e
5 changed files with 98 additions and 19 deletions

View File

@ -101,17 +101,23 @@ void AbbTerraTcpDiscovery::cleanupConnection(AbbTerraModbusTcpConnection *connec
return;
// Stop reacting to any further signals from this connection (and its Modbus
// master) before tearing it down. Otherwise disconnectDevice() below can
// synchronously emit reachableChanged()/connectionErrorOccurred() and
// re-enter this method while we are still inside one of the connection's own
// signal handlers (e.g. the QModbusReply "finished" lambda that keeps
// dereferencing the reply after the emit) — a use-after-free that crashes
// nymead with SIGSEGV during network discovery.
// master) so the teardown below cannot re-enter this method.
disconnect(connection, nullptr, this, nullptr);
disconnect(connection->modbusTcpMaster(), nullptr, this, nullptr);
connection->disconnectDevice();
connection->deleteLater();
// 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()

View File

@ -34,9 +34,17 @@ inline QString connectorTypeName(quint8 connectorType)
}
}
// The numeric fields of the serial-number and firmware registers are BCD-encoded:
// the hex representation of the byte IS its decimal value (per ABB spec §5.1,
// e.g. 0x49 = week 49, 0x22 = 22 kW), not a plain binary number (0x49 = 73).
inline quint8 bcdToDec(quint8 value)
{
return static_cast<quint8>(((value >> 4) & 0x0f) * 10 + (value & 0x0f));
}
inline int ratedPowerFromCode(quint8 ratedPowerCode)
{
switch (ratedPowerCode) {
switch (bcdToDec(ratedPowerCode)) {
case 7:
return 7;
case 11:
@ -54,11 +62,11 @@ inline DeviceInfo deviceInfoFromValues(quint64 serialNumberRaw, quint32 firmware
const quint8 connectorType = static_cast<quint8>((serialNumberRaw >> 56) & 0xff);
const quint8 ratedPowerCode = static_cast<quint8>((serialNumberRaw >> 48) & 0xff);
const quint8 plantId = static_cast<quint8>((serialNumberRaw >> 40) & 0xff);
const quint8 productionWeek = static_cast<quint8>((serialNumberRaw >> 24) & 0xff);
const quint8 productionYear = static_cast<quint8>((serialNumberRaw >> 16) & 0xff);
const quint8 uniqueHigh = static_cast<quint8>((serialNumberRaw >> 8) & 0xff);
const quint8 uniqueLow = static_cast<quint8>(serialNumberRaw & 0xff);
const quint8 plantId = bcdToDec(static_cast<quint8>((serialNumberRaw >> 40) & 0xff));
const quint8 productionWeek = bcdToDec(static_cast<quint8>((serialNumberRaw >> 24) & 0xff));
const quint8 productionYear = bcdToDec(static_cast<quint8>((serialNumberRaw >> 16) & 0xff));
const quint8 uniqueHigh = bcdToDec(static_cast<quint8>((serialNumberRaw >> 8) & 0xff));
const quint8 uniqueLow = bcdToDec(static_cast<quint8>(serialNumberRaw & 0xff));
const int ratedPower = ratedPowerFromCode(ratedPowerCode);
const QString connectorName = connectorTypeName(connectorType);
@ -77,9 +85,9 @@ inline DeviceInfo deviceInfoFromValues(quint64 serialNumberRaw, quint32 firmware
.arg(uniqueLow, 2, 10, QLatin1Char('0'));
deviceInfo.productName = QStringLiteral("ABB Terra AC %1 kW %2").arg(ratedPower).arg(connectorName);
deviceInfo.firmwareVersion = QStringLiteral("%1.%2.%3")
.arg((firmwareVersionRaw >> 24) & 0xff)
.arg((firmwareVersionRaw >> 16) & 0xff)
.arg((firmwareVersionRaw >> 8) & 0xff);
.arg(bcdToDec(static_cast<quint8>((firmwareVersionRaw >> 24) & 0xff)))
.arg(bcdToDec(static_cast<quint8>((firmwareVersionRaw >> 16) & 0xff)))
.arg(bcdToDec(static_cast<quint8>((firmwareVersionRaw >> 8) & 0xff)));
if (maxCurrentRaw >= 6000 && maxCurrentRaw <= 32000) {
deviceInfo.maxChargingCurrent = maxCurrentRaw / 1000.0;
}

View File

@ -314,6 +314,23 @@ void IntegrationPluginAbbterra::setupTcpThing(ThingSetupInfo *info)
applyTimeoutSetting(thing, connection);
}
});
// Actually start the connection once the network device is online. Without
// this the connection is never established, reachableChanged() never fires,
// initialize() is never called and the setup hangs until it times out
// ("Thing setup failed"). Gate on the monitor so we only connect when the
// device's IP is known and reachable, and (re)connect if it comes back.
connect(monitor, &NetworkDeviceMonitor::reachableChanged, connection, [connection, monitor](bool reachable) {
if (reachable) {
connection->modbusTcpMaster()->setHostAddress(monitor->networkDeviceInfo().address());
if (!connection->modbusTcpMaster()->connected())
connection->connectDevice();
}
});
if (monitor->reachable()) {
connection->modbusTcpMaster()->setHostAddress(monitor->networkDeviceInfo().address());
connection->connectDevice();
}
}
void IntegrationPluginAbbterra::setupRtuThing(ThingSetupInfo *info)

View File

@ -19,10 +19,8 @@
"networkdevice"
],
"createMethods": [
"discovery",
"user"
],
"discoveryType": "weak",
"paramTypes": [
{
"id": "9b3332aa-6c26-4399-b3dc-d7fdbe3f4420",

50
debian/changelog vendored
View File

@ -1,3 +1,53 @@
etm-powersync-plugins-modbus (1.15.2+etm5) trixie; urgency=medium
* abbterra: decode the serial-number and firmware register fields as BCD, as
specified by the ABB Modbus spec §5.1 (e.g. 0x49 = week 49, 0x22 = 22 kW),
instead of as plain binary. The binary reading made deviceInfoFromValues()
reject every real charger whose BCD production-week byte exceeded 0x35
(e.g. week 46 = 0x46 = 70 > 53), failing setup with "The device does not
match the ABB Terra AC Modbus register map", and mis-decoded the 11 kW/22 kW
rated-power codes. Manual TCP add of the charger now completes.
-- ETM-Schurig SARL <contact@etm-schurig.eu> Fri, 03 Jul 2026 13:15:00 +0200
etm-powersync-plugins-modbus (1.15.2+etm4) trixie; urgency=medium
* abbterra: fix TCP manual setup never establishing the connection.
setupTcpThing() wired all the connection signals but never called
connectDevice(), so reachableChanged()/initializationFinished() never fired
and the setup hung until timeout ("Thing setup failed"). Now the connection
is started when the NetworkDeviceMonitor reports the device online.
-- ETM-Schurig SARL <contact@etm-schurig.eu> Fri, 03 Jul 2026 12:55:00 +0200
etm-powersync-plugins-modbus (1.15.2+etm3) trixie; urgency=medium
* abbterra: disable TCP auto-discovery (createMethods = "user" only) for the
Terra AC Charger (TCP) thing class. Discovery blind-scans the LAN and blows
up nymead with a SIGSEGV as soon as any Modbus device answers :502 with an
exception response — the crash is in the generated reachability handler and
cannot be fixed from the discovery code alone (two attempts failed). For the
Waveshare RS485-gateway use case discovery can't determine the slave id
anyway; the charger must be added manually (address + port + slaveId). This
unblocks adding the charger and stops the crash loop. The underlying
generated-code crash is tracked separately.
-- ETM-Schurig SARL <contact@etm-schurig.eu> Fri, 03 Jul 2026 12:40:00 +0200
etm-powersync-plugins-modbus (1.15.2+etm2) trixie; urgency=medium
* abbterra: fix nymead SIGSEGV during ABB Terra AC TCP discovery. On a
reachability-check exception (e.g. any Modbus device on the LAN answering
:502 with "illegal data address"), AbbTerraTcpDiscovery::cleanupConnection()
tore the connection down synchronously from inside the connection's own
QModbusReply "finished" handler, freeing the in-flight reply that the
generated code still dereferences at abbterramodbustcpconnection.cpp:2062.
The teardown is now deferred (Qt::QueuedConnection) and made idempotent.
* abbterra: restore checkReachableRegister ("serialNumber"), required by the
libnymea-modbus 1.15 connection-class generator.
-- ETM-Schurig SARL <contact@etm-schurig.eu> Fri, 03 Jul 2026 12:00:00 +0200
etm-powersync-plugins-modbus (1.15.2+etm1) trixie; urgency=medium
* Build 1.15,2