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>
154 lines
5.0 KiB
C++
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> ®isters)
|
|
{
|
|
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
|