/** * @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 (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 (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 (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 (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 (ch->clientSockets.size ())); } } void JettingProxy::onClientDisconnected () { auto *socket = qobject_cast (sender ()); Channel *ch = channelForClientSocket (socket); if (ch) { ch->clientSockets.removeOne (socket); qInfo ("%s Client disconnected (%lld remaining)", qPrintable (logTag (*ch)), static_cast (ch->clientSockets.size ())); } socket->deleteLater (); } void JettingProxy::onClientDataReady () { auto *socket = qobject_cast (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"); }