aboutsummaryrefslogtreecommitdiffstats
path: root/jetting-proxy/JettingProxy.cpp
diff options
context:
space:
mode:
authorThomas Vanbesien <tvanbesi@proton.me>2026-03-24 17:25:03 +0100
committerThomas Vanbesien <tvanbesi@proton.me>2026-03-24 17:29:52 +0100
commite9d8a8b052150f42ea00da2c07e3f78a9b7d2061 (patch)
tree2124701ae0991ed854c1a94e58d64558b6f78b48 /jetting-proxy/JettingProxy.cpp
parent8bcf948b76c9564cb38d3611228ccaf73890a548 (diff)
downloadQtXpl2-master.tar.gz
QtXpl2-master.zip
Add JettingProxy relay, convert Xpl2Client to active connection modelHEADmaster
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/JettingProxy.cpp')
-rw-r--r--jetting-proxy/JettingProxy.cpp266
1 files changed, 266 insertions, 0 deletions
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");
+}