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

154 lines
5.0 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
#ifndef ABBTERRAUTILS_H
#define ABBTERRAUTILS_H
#include <QString>
#include <QVector>
#include <modbusdatautils.h>
namespace AbbTerraUtils {
struct DeviceInfo {
bool valid = false;
QString serialNumber;
QString productName;
QString firmwareVersion;
double maxChargingCurrent = 32.0;
};
inline QString connectorTypeName(quint8 connectorType)
{
switch (connectorType) {
case 'G':
return QStringLiteral("Cable");
case 'P':
return QStringLiteral("Outlet");
case 'S':
return QStringLiteral("Socket");
case 'T':
return QStringLiteral("Socket");
default:
return QString();
}
}
// 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 (bcdToDec(ratedPowerCode)) {
case 7:
return 7;
case 11:
return 11;
case 22:
return 22;
default:
return -1;
}
}
inline DeviceInfo deviceInfoFromValues(quint64 serialNumberRaw, quint32 firmwareVersionRaw, quint32 maxCurrentRaw)
{
DeviceInfo deviceInfo;
const quint8 connectorType = static_cast<quint8>((serialNumberRaw >> 56) & 0xff);
const quint8 ratedPowerCode = static_cast<quint8>((serialNumberRaw >> 48) & 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);
if (ratedPower <= 0 || connectorName.isEmpty() || productionWeek == 0 || productionWeek > 53) {
return deviceInfo;
}
deviceInfo.valid = true;
deviceInfo.serialNumber = QStringLiteral("TACW%1-%2-%3%4-%5%6%7")
.arg(ratedPower)
.arg(plantId)
.arg(productionWeek, 2, 10, QLatin1Char('0'))
.arg(productionYear, 2, 10, QLatin1Char('0'))
.arg(QChar(static_cast<char>(connectorType)))
.arg(uniqueHigh, 2, 10, QLatin1Char('0'))
.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(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;
}
return deviceInfo;
}
inline DeviceInfo deviceInfoFromRegisters(const QVector<quint16> &registers)
{
if (registers.count() < 8) {
return DeviceInfo();
}
return deviceInfoFromValues(
ModbusDataUtils::convertToUInt64(registers.mid(0, 4), ModbusDataUtils::ByteOrderBigEndian),
ModbusDataUtils::convertToUInt32(registers.mid(4, 2), ModbusDataUtils::ByteOrderBigEndian),
ModbusDataUtils::convertToUInt32(registers.mid(6, 2), ModbusDataUtils::ByteOrderBigEndian)
);
}
inline quint8 chargingStateCode(quint32 chargingStateRaw)
{
return static_cast<quint8>((chargingStateRaw >> 8) & 0x7f);
}
inline bool chargingLimitedByCar(quint32 chargingStateRaw)
{
return ((chargingStateRaw >> 15) & 0x1) == 0x1;
}
inline bool isVehiclePluggedIn(quint32 chargingStateRaw, quint32 socketLockState)
{
const quint8 chargingState = chargingStateCode(chargingStateRaw);
if (chargingState >= 3 && chargingState <= 6) {
return true;
}
return socketLockState == 0x0101 || socketLockState == 0x0111;
}
inline bool isCharging(quint32 chargingStateRaw, quint32 activePower)
{
return chargingStateCode(chargingStateRaw) == 6 || activePower > 100;
}
inline uint phaseCount(quint32 voltageL1, quint32 voltageL2, quint32 voltageL3)
{
uint phases = 0;
if (voltageL1 > 1000) {
phases++;
}
if (voltageL2 > 1000) {
phases++;
}
if (voltageL3 > 1000) {
phases++;
}
return qMax(phases, 1u);
}
} // namespace AbbTerraUtils
#endif // ABBTERRAUTILS_H