aboutsummaryrefslogtreecommitdiffstats
path: root/jetting-proxy
diff options
context:
space:
mode:
Diffstat (limited to 'jetting-proxy')
-rw-r--r--jetting-proxy/CMakeLists.txt6
-rw-r--r--jetting-proxy/JettingProxy.cpp266
-rw-r--r--jetting-proxy/JettingProxy.h54
-rw-r--r--jetting-proxy/main.cpp37
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 ();
+}