diff options
Diffstat (limited to 'jetting-proxy/JettingProxy.cpp')
| -rw-r--r-- | jetting-proxy/JettingProxy.cpp | 266 |
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"); +} |
