EVerest: Fix initial connection race condition and implement automatic reconnect mechanism

This commit is contained in:
Simon Stürz 2026-10-15 14:53:11 +02:00
parent 073ff88f32
commit 44158b8aec
3 changed files with 162 additions and 1 deletions

View File

@ -48,6 +48,18 @@ EverestConnection::EverestConnection(quint16 port, QObject *parent)
}
});
// Connection refused or failed init does not change availability, so availableChanged alone never triggers a retry
connect(m_client, &EverestJsonRpcClient::connectionErrorOccurred, this, [this](){
if (!m_running || m_reconnectTimer.isActive())
return;
if (m_monitor && !m_monitor->reachable())
return;
qCDebug(dcEverest()) << "Connection error occurred. Starting reconnect timer for JsonRpc client to" << m_client->serverUrl().toString();
m_reconnectTimer.start();
});
// Reconnect timer in case the network device is reachable but the websocketserver not yet
m_reconnectTimer.setInterval(5000);
m_reconnectTimer.setSingleShot(false);
@ -107,7 +119,7 @@ void EverestConnection::setMonitor(NetworkDeviceMonitor *monitor)
connect(m_monitor, &NetworkDeviceMonitor::reachableChanged, this, &EverestConnection::onMonitorReachableChanged);
}
if (m_monitor->reachable())
if (m_monitor && m_monitor->reachable())
onMonitorReachableChanged(true);
}

View File

@ -14,3 +14,21 @@ target_link_libraries(tst_rfidproviderclient PRIVATE
Qt${NYMEA_TEST_QT_MAJOR_VERSION}::Test Qt${NYMEA_TEST_QT_MAJOR_VERSION}::Network
Qt${NYMEA_TEST_QT_MAJOR_VERSION}::WebSockets)
add_test(NAME everest-rfid COMMAND ${DBUS_RUN_SESSION} -- $<TARGET_FILE:tst_rfidproviderclient>)
find_package(PkgConfig REQUIRED)
pkg_check_modules(NYMEA REQUIRED IMPORTED_TARGET nymea)
find_program(NYMEA_PLUGININFO_COMPILER nymea-plugininfocompiler REQUIRED)
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/plugininfo.h ${CMAKE_CURRENT_BINARY_DIR}/extern-plugininfo.h
COMMAND ${NYMEA_PLUGININFO_COMPILER} ${CMAKE_CURRENT_SOURCE_DIR}/../integrationplugineverest.json
-o ${CMAKE_CURRENT_BINARY_DIR}/plugininfo.h -e ${CMAKE_CURRENT_BINARY_DIR}/extern-plugininfo.h
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/../integrationplugineverest.json)
add_executable(tst_everestconnection tst_everestconnection.cpp
${CMAKE_CURRENT_BINARY_DIR}/plugininfo.h
../jsonrpc/everestconnection.cpp ../jsonrpc/everestevse.cpp ../jsonrpc/everestjsonrpcclient.cpp
../jsonrpc/everestjsonrpcinterface.cpp ../jsonrpc/everestjsonrpcreply.cpp)
target_include_directories(tst_everestconnection PRIVATE .. ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(tst_everestconnection PRIVATE PkgConfig::NYMEA
Qt${NYMEA_TEST_QT_MAJOR_VERSION}::Core Qt${NYMEA_TEST_QT_MAJOR_VERSION}::Test
Qt${NYMEA_TEST_QT_MAJOR_VERSION}::Network Qt${NYMEA_TEST_QT_MAJOR_VERSION}::WebSockets)
add_test(NAME everest-connection COMMAND $<TARGET_FILE:tst_everestconnection>)

View File

@ -0,0 +1,131 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QtTest>
#include <QTcpServer>
#include <QJsonDocument>
#include <QWebSocket>
#include <QWebSocketServer>
#include "plugininfo.h"
#include "loggingcategories.h"
#include "jsonrpc/everestconnection.h"
#include "jsonrpc/everestjsonrpcclient.h"
NYMEA_LOGGING_CATEGORY(dcEverestTraffic, "EverestTraffic")
class FakeMonitor : public NetworkDeviceMonitor
{
Q_OBJECT
public:
explicit FakeMonitor(QObject *parent = nullptr) : NetworkDeviceMonitor(parent)
{
m_info.setAddress(QHostAddress::LocalHost);
}
MacAddress macAddress() const override { return MacAddress(); }
QString hostName() const override { return QString(); }
QHostAddress address() const override { return QHostAddress::LocalHost; }
NetworkDeviceInfo::MonitorMode monitorMode() const override { return NetworkDeviceInfo::MonitorModeIp; }
NetworkDeviceInfo networkDeviceInfo() const override { return m_info; }
bool reachable() const override { return true; }
QDateTime lastSeen() const override { return QDateTime::currentDateTime(); }
uint pingRetries() const override { return 0; }
void setPingRetries(uint) override { }
private:
NetworkDeviceInfo m_info;
};
// Minimal RpcApi answering the init sequence of EverestJsonRpcClient
class FakeRpcApi : public QObject
{
Q_OBJECT
public:
QWebSocketServer server{"fake-everest", QWebSocketServer::NonSecureMode};
int connectionCount = 0;
bool listen(quint16 port)
{
connect(&server, &QWebSocketServer::newConnection, this, [this](){
QWebSocket *socket = server.nextPendingConnection();
connectionCount++;
connect(socket, &QWebSocket::disconnected, socket, &QWebSocket::deleteLater);
connect(socket, &QWebSocket::textMessageReceived, socket, [socket](const QString &message){
QVariantMap request = QJsonDocument::fromJson(message.toUtf8()).toVariant().toMap();
QVariantMap result;
const QString method = request.value("method").toString();
if (method == "API.Hello") {
result = {{"api_version", "1.0"}, {"everest_version", "test"}, {"authentication_required", false}, {"charger_info", QVariantMap()}};
} else if (method == "ChargePoint.GetEVSEInfos") {
result = {{"error", "NoError"}, {"infos", QVariantList()}};
} else {
result = {{"error", "NoError"}};
}
QVariantMap response = {{"jsonrpc", "2.0"}, {"id", request.value("id")}, {"result", result}};
socket->sendTextMessage(QString::fromUtf8(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Compact)));
});
});
return server.listen(QHostAddress::LocalHost, port);
}
};
class TestEverestConnection : public QObject
{
Q_OBJECT
static quint16 freePort()
{
QTcpServer probe;
probe.listen(QHostAddress::LocalHost, 0);
return probe.serverPort();
}
private slots:
void reconnectAfterConnectionRefused()
{
const quint16 port = freePort();
QVERIFY(port != 0);
FakeMonitor monitor;
EverestConnection connection(port);
connection.setMonitor(&monitor);
QSignalSpy errorSpy(connection.client(), &EverestJsonRpcClient::connectionErrorOccurred);
QSignalSpy availableSpy(&connection, &EverestConnection::availableChanged);
// Server not up yet, like EVerest still booting
connection.start();
QTRY_VERIFY_WITH_TIMEOUT(errorSpy.count() >= 1, 2000);
QVERIFY(!connection.available());
FakeRpcApi api;
QVERIFY(api.listen(port));
QTRY_VERIFY_WITH_TIMEOUT(connection.available(), 8000);
QCOMPARE(availableSpy.count(), 1);
QCOMPARE(availableSpy.first().first().toBool(), true);
}
void noReconnectWhenStopped()
{
const quint16 port = freePort();
QVERIFY(port != 0);
FakeMonitor monitor;
EverestConnection connection(port);
connection.setMonitor(&monitor);
QSignalSpy errorSpy(connection.client(), &EverestJsonRpcClient::connectionErrorOccurred);
connection.start();
QTRY_VERIFY_WITH_TIMEOUT(errorSpy.count() >= 1, 2000);
connection.stop();
FakeRpcApi api;
QVERIFY(api.listen(port));
QTest::qWait(6000);
QVERIFY(!connection.available());
QCOMPARE(api.connectionCount, 0);
}
};
QTEST_GUILESS_MAIN(TestEverestConnection)
#include "tst_everestconnection.moc"