diff options
| author | Thomas Vanbesien <tvanbesi@proton.me> | 2026-03-24 17:25:03 +0100 |
|---|---|---|
| committer | Thomas Vanbesien <tvanbesi@proton.me> | 2026-03-24 17:29:52 +0100 |
| commit | e9d8a8b052150f42ea00da2c07e3f78a9b7d2061 (patch) | |
| tree | 2124701ae0991ed854c1a94e58d64558b6f78b48 /jetting-proxy | |
| parent | 8bcf948b76c9564cb38d3611228ccaf73890a548 (diff) | |
| download | QtXpl2-e9d8a8b052150f42ea00da2c07e3f78a9b7d2061.tar.gz QtXpl2-e9d8a8b052150f42ea00da2c07e3f78a9b7d2061.zip | |
Insert a transparent TCP proxy between the controller and N clients:
- JettingProxy listens on 9110-9112 (controller) and 9210-9212 (clients)
- Broadcasts controller frames to all clients, forwards client→controller
- Independent KA_PING handling on both sides
Convert Xpl2Client from passive QTcpServer listener to active QTcpSocket
outbound connections with auto-retry. New QML API: host/commandPort
properties, connectToProxy()/disconnectFromProxy() replacing
startListening()/stopListening().
Diffstat (limited to 'jetting-proxy')
| -rw-r--r-- | jetting-proxy/CMakeLists.txt | 6 | ||||
| -rw-r--r-- | jetting-proxy/JettingProxy.cpp | 266 | ||||
| -rw-r--r-- | jetting-proxy/JettingProxy.h | 54 | ||||
| -rw-r--r-- | jetting-proxy/main.cpp | 37 |
4 files changed, 363 insertions, 0 deletions
diff --git a/jetting-proxy/CMakeLists.txt b/jetting-proxy/CMakeLists.txt new file mode 100644 index 0000000..6e3c055 --- /dev/null +++ b/jetting-proxy/CMakeLists.txt @@ -0,0 +1,6 @@ +qt_add_executable(JettingProxy main.cpp JettingProxy.h JettingProxy.cpp) + +set_target_properties(JettingProxy PROPERTIES RUNTIME_OUTPUT_DIRECTORY + "${PROJECT_BINARY_DIR}/bin") + +target_link_libraries(JettingProxy PRIVATE Qt6::Core Qt6::Network) diff --git a/jetting-proxy/JettingProxy.cpp b/jetting-proxy/JettingProxy.cpp new file mode 100644 index 0000000..30fe7df --- /dev/null +++ b/jetting-proxy/JettingProxy.cpp @@ -0,0 +1,266 @@ +/** + * @file JettingProxy.cpp + * @brief Transparent TCP relay between one controller and N clients. + */ +#include "JettingProxy.h" + +bool JettingProxy::s_wireDebug = false; + +void +JettingProxy::enableWireDebug () +{ + s_wireDebug = true; +} + +JettingProxy::JettingProxy (quint16 controllerPortBase, quint16 clientPortBase, + QObject *parent) + : QObject (parent) +{ + setupChannel (m_command, "Command", controllerPortBase, clientPortBase); + setupChannel (m_imaging, "Imaging", controllerPortBase + 1, + clientPortBase + 1); + setupChannel (m_status, "Status", controllerPortBase + 2, + clientPortBase + 2); + + connect (&m_tickTimer, &QTimer::timeout, this, &JettingProxy::tick); + m_tickTimer.start (1000); +} + +void +JettingProxy::setupChannel (Channel &channel, const char *name, + quint16 controllerPort, quint16 clientPort) +{ + channel.name = name; + + connect (&channel.controllerServer, &QTcpServer::newConnection, this, + &JettingProxy::onControllerConnected); + connect (&channel.clientServer, &QTcpServer::newConnection, this, + &JettingProxy::onClientConnected); + + if (!channel.controllerServer.listen (QHostAddress::Any, controllerPort)) + { + qCritical ("%s Failed to listen for controller on port %d: %s", + qPrintable (logTag (channel)), controllerPort, + qPrintable (channel.controllerServer.errorString ())); + return; + } + if (!channel.clientServer.listen (QHostAddress::Any, clientPort)) + { + qCritical ("%s Failed to listen for clients on port %d: %s", + qPrintable (logTag (channel)), clientPort, + qPrintable (channel.clientServer.errorString ())); + return; + } + + qInfo ("%s Listening — controller port %d, client port %d", + qPrintable (logTag (channel)), controllerPort, clientPort); +} + +/* ------------------------------------------------------------------ */ +/* Channel lookup helpers */ +/* ------------------------------------------------------------------ */ + +JettingProxy::Channel * +JettingProxy::channelForControllerServer (QTcpServer *server) +{ + if (server == &m_command.controllerServer) + return &m_command; + if (server == &m_imaging.controllerServer) + return &m_imaging; + if (server == &m_status.controllerServer) + return &m_status; + return nullptr; +} + +JettingProxy::Channel * +JettingProxy::channelForControllerSocket (QTcpSocket *socket) +{ + if (socket == m_command.controllerSocket) + return &m_command; + if (socket == m_imaging.controllerSocket) + return &m_imaging; + if (socket == m_status.controllerSocket) + return &m_status; + return nullptr; +} + +JettingProxy::Channel * +JettingProxy::channelForClientSocket (QTcpSocket *socket) +{ + for (auto *ch : { &m_command, &m_imaging, &m_status }) + if (ch->clientSockets.contains (socket)) + return ch; + return nullptr; +} + +QString +JettingProxy::logTag (const Channel &channel) const +{ + return QStringLiteral ("[%1]").arg (channel.name).leftJustified (11); +} + +/* ------------------------------------------------------------------ */ +/* Controller slots */ +/* ------------------------------------------------------------------ */ + +void +JettingProxy::onControllerConnected () +{ + auto *server = qobject_cast<QTcpServer *> (sender ()); + Channel *ch = channelForControllerServer (server); + + while (auto *pending = server->nextPendingConnection ()) + { + if (ch->controllerSocket) + { + qWarning ("%s Rejected extra controller connection", + qPrintable (logTag (*ch))); + pending->deleteLater (); + continue; + } + ch->controllerSocket = pending; + connect (pending, &QTcpSocket::readyRead, this, + &JettingProxy::onControllerDataReady); + connect (pending, &QTcpSocket::disconnected, this, + &JettingProxy::onControllerDisconnected); + qInfo ("%s Controller connected", qPrintable (logTag (*ch))); + } +} + +void +JettingProxy::onControllerDisconnected () +{ + auto *socket = qobject_cast<QTcpSocket *> (sender ()); + Channel *ch = channelForControllerSocket (socket); + if (ch) + { + qInfo ("%s Controller disconnected", qPrintable (logTag (*ch))); + ch->controllerSocket = nullptr; + } + socket->deleteLater (); +} + +void +JettingProxy::onControllerDataReady () +{ + auto *socket = qobject_cast<QTcpSocket *> (sender ()); + Channel *ch = channelForControllerSocket (socket); + if (!ch) + return; + + while (socket->canReadLine ()) + { + QByteArray line = socket->readLine (); + QByteArray trimmed = line.trimmed (); + + /* Extract command token (everything before the first comma). */ + int comma = trimmed.indexOf (','); + QByteArray cmd = (comma >= 0) ? trimmed.left (comma) : trimmed; + + if (cmd == "KA_PING") + { + /* Reply directly — never forward keepalive to clients. */ + socket->write ("KA_PING,1\n"); + continue; + } + + if (s_wireDebug) + qDebug ("%s Controller → clients: %s", qPrintable (logTag (*ch)), + trimmed.constData ()); + + /* Broadcast to all connected clients on this channel. */ + for (auto *client : ch->clientSockets) + client->write (line); + } +} + +/* ------------------------------------------------------------------ */ +/* Client slots */ +/* ------------------------------------------------------------------ */ + +void +JettingProxy::onClientConnected () +{ + auto *server = qobject_cast<QTcpServer *> (sender ()); + Channel *ch = nullptr; + if (server == &m_command.clientServer) + ch = &m_command; + else if (server == &m_imaging.clientServer) + ch = &m_imaging; + else + ch = &m_status; + + while (auto *pending = server->nextPendingConnection ()) + { + ch->clientSockets.append (pending); + connect (pending, &QTcpSocket::readyRead, this, + &JettingProxy::onClientDataReady); + connect (pending, &QTcpSocket::disconnected, this, + &JettingProxy::onClientDisconnected); + qInfo ("%s Client #%lld connected", qPrintable (logTag (*ch)), + static_cast<long long> (ch->clientSockets.size ())); + } +} + +void +JettingProxy::onClientDisconnected () +{ + auto *socket = qobject_cast<QTcpSocket *> (sender ()); + Channel *ch = channelForClientSocket (socket); + if (ch) + { + ch->clientSockets.removeOne (socket); + qInfo ("%s Client disconnected (%lld remaining)", + qPrintable (logTag (*ch)), + static_cast<long long> (ch->clientSockets.size ())); + } + socket->deleteLater (); +} + +void +JettingProxy::onClientDataReady () +{ + auto *socket = qobject_cast<QTcpSocket *> (sender ()); + Channel *ch = channelForClientSocket (socket); + if (!ch) + return; + + while (socket->canReadLine ()) + { + QByteArray line = socket->readLine (); + QByteArray trimmed = line.trimmed (); + + /* Extract command token. */ + int comma = trimmed.indexOf (','); + QByteArray cmd = (comma >= 0) ? trimmed.left (comma) : trimmed; + + /* Absorb keepalive replies — never forward to controller. */ + if (cmd == "KA_PING") + continue; + + if (s_wireDebug) + qDebug ("%s Client → controller: %s", qPrintable (logTag (*ch)), + trimmed.constData ()); + + /* Forward to controller. */ + if (ch->controllerSocket + && ch->controllerSocket->state () == QAbstractSocket::ConnectedState) + ch->controllerSocket->write (line); + else + qWarning ("%s No controller — dropped: %s", qPrintable (logTag (*ch)), + trimmed.constData ()); + } +} + +/* ------------------------------------------------------------------ */ +/* Tick */ +/* ------------------------------------------------------------------ */ + +void +JettingProxy::tick () +{ + for (auto *ch : { &m_command, &m_imaging, &m_status }) + for (auto *client : ch->clientSockets) + if (client->state () == QAbstractSocket::ConnectedState) + client->write ("KA_PING\n"); +} diff --git a/jetting-proxy/JettingProxy.h b/jetting-proxy/JettingProxy.h new file mode 100644 index 0000000..ec8c26d --- /dev/null +++ b/jetting-proxy/JettingProxy.h @@ -0,0 +1,54 @@ +/** + * @file JettingProxy.h + * @brief Transparent TCP relay between one controller and N clients. + */ +#pragma once + +#include <QList> +#include <QObject> +#include <QTcpServer> +#include <QTcpSocket> +#include <QTimer> + +class JettingProxy : public QObject +{ + Q_OBJECT + +public: + explicit JettingProxy (quint16 controllerPortBase, quint16 clientPortBase, + QObject *parent = nullptr); + static void enableWireDebug (); + +private slots: + void onControllerConnected (); + void onControllerDisconnected (); + void onControllerDataReady (); + void onClientConnected (); + void onClientDisconnected (); + void onClientDataReady (); + /* Send KA_PING to all connected clients on all channels. */ + void tick (); + +private: + struct Channel + { + const char *name = nullptr; + QTcpServer controllerServer; + QTcpSocket *controllerSocket = nullptr; + QTcpServer clientServer; + QList<QTcpSocket *> clientSockets; + }; + + void setupChannel (Channel &channel, const char *name, + quint16 controllerPort, quint16 clientPort); + Channel *channelForControllerServer (QTcpServer *server); + Channel *channelForControllerSocket (QTcpSocket *socket); + Channel *channelForClientSocket (QTcpSocket *socket); + QString logTag (const Channel &channel) const; + + Channel m_command; + Channel m_imaging; + Channel m_status; + QTimer m_tickTimer; + static bool s_wireDebug; +}; diff --git a/jetting-proxy/main.cpp b/jetting-proxy/main.cpp new file mode 100644 index 0000000..0e14054 --- /dev/null +++ b/jetting-proxy/main.cpp @@ -0,0 +1,37 @@ +/** + * @file main.cpp + * @brief Jetting proxy — relay between one controller and N clients. + */ +#include "JettingProxy.h" + +#include <QCommandLineParser> +#include <QCoreApplication> + +int +main (int argc, char *argv[]) +{ + qSetMessagePattern ("Proxy [%{time HH:mm:ss.zzz}] %{message}"); + + QCoreApplication app (argc, argv); + + QCommandLineParser parser; + parser.addOption ({ "wire-debug", "Log forwarded frames to dev log" }); + parser.addOption ({ "controller-port", + "Base port for controller side (default 9110)", "port", + "9110" }); + parser.addOption ({ "client-port", + "Base port for client side (default 9210)", "port", + "9210" }); + parser.addHelpOption (); + parser.process (app); + + if (parser.isSet ("wire-debug")) + JettingProxy::enableWireDebug (); + + quint16 controllerPort = parser.value ("controller-port").toUShort (); + quint16 clientPort = parser.value ("client-port").toUShort (); + + new JettingProxy (controllerPort, clientPort, &app); + + return app.exec (); +} |
