Add TCP spot client

knobtest
Phil Taylor 2022-09-30 17:05:42 +01:00
rodzic e7e7821f2b
commit 7d141608bf
5 zmienionych plików z 492 dodań i 78 usunięć

Wyświetl plik

@ -26,7 +26,6 @@ void dxClusterClient::enableUdp(bool enable)
bool result = udpSocket->bind(QHostAddress::AnyIPv4, udpPort);
qInfo(logCluster()) << "Starting udpSocket() on:" << udpPort << "Result:" << result;
//udpDataReceived();
connect(udpSocket, SIGNAL(readyRead()), this, SLOT(udpDataReceived()), Qt::QueuedConnection);
}
@ -34,8 +33,11 @@ void dxClusterClient::enableUdp(bool enable)
else {
if (udpSocket != Q_NULLPTR)
{
qInfo(logCluster()) << "Stopping udpSocket() on:" << udpPort;
udpSocket->disconnect();
delete udpSocket;
udpSocket = Q_NULLPTR;
}
}
}
@ -43,12 +45,43 @@ void dxClusterClient::enableUdp(bool enable)
void dxClusterClient::enableTcp(bool enable)
{
tcpEnable = enable;
if (enable)
{
tcpRegex = QRegularExpression("^DX de ([a-z|A-Z|0-9|/]+):\\s+([0-9|.]+)\\s+([a-z|A-Z|0-9|/]+)+\\s+(.*)\\s+(\\d{4}Z)");
if (tcpSocket == Q_NULLPTR)
{
tcpSocket = new QTcpSocket(this);
tcpSocket->connectToHost(tcpServerName, tcpPort);
qInfo(logCluster()) << "Starting tcpSocket() on:" << tcpPort;
connect(tcpSocket, SIGNAL(readyRead()), this, SLOT(tcpDataReceived()), Qt::QueuedConnection);
tcpCleanupTimer = new QTimer(this);
tcpCleanupTimer->setInterval(1000 * 10); // Run once a minute
connect(tcpCleanupTimer, SIGNAL(timeout()), this, SLOT(tcpCleanup()));
tcpCleanupTimer->start();
}
}
else {
if (tcpSocket != Q_NULLPTR)
{
qInfo(logCluster()) << "Disconnecting tcpSocket() on:" << tcpPort;
if (tcpCleanupTimer != Q_NULLPTR)
{
tcpCleanupTimer->stop();
delete tcpCleanupTimer;
tcpCleanupTimer = Q_NULLPTR;
}
tcpSocket->disconnect();
delete tcpSocket;
tcpSocket = Q_NULLPTR;
}
}
}
void dxClusterClient::udpDataReceived()
{
QMutexLocker locker(&udpMutex); // Not sure if this is needed?
QHostAddress sender;
quint16 port;
QByteArray datagram;
@ -63,18 +96,17 @@ void dxClusterClient::udpDataReceived()
// This is a spot?
QString action = spot.firstChildElement("action").text();
if (action == "add") {
spotData data;
data.dxcall = spot.firstChildElement("dxcall").text();
data.frequency = spot.firstChildElement("frequency").text().toDouble()/1000.0;
data.spottercall = spot.firstChildElement("spottercall").text();
data.timestamp = QDateTime::fromString(spot.firstChildElement("timestamp").text());
data.mode = spot.firstChildElement("mode").text();
data.comment = spot.firstChildElement("comment").text();
spotData* data = new spotData();
data->dxcall = spot.firstChildElement("dxcall").text();
data->frequency = spot.firstChildElement("frequency").text().toDouble() / 1000.0;
data->spottercall = spot.firstChildElement("spottercall").text();
data->timestamp = QDateTime::fromString(spot.firstChildElement("timestamp").text(),"yyyy-MM-dd hh:mm:ss");
data->mode = spot.firstChildElement("mode").text();
data->comment = spot.firstChildElement("comment").text();
emit addSpot(data);
qInfo(logCluster()) << "ADD DX=" << data.dxcall <<
"SPOTTER=" << data.spottercall <<
"FREQ=" << data.frequency <<
"MODE=" << data.mode;
emit(sendOutput(QString("UDP: SPOT:%1 SPOTTER:%2 FREQ:%3 MODE:%4 DATE:%5 COMMENT:%6\n")
.arg(data->dxcall).arg(data->spottercall).arg(data->frequency).arg(data->mode)
.arg(data->timestamp.toString()).arg(data->comment)));
}
else if (action == "delete")
{
@ -86,3 +118,55 @@ void dxClusterClient::udpDataReceived()
}
}
void dxClusterClient::tcpDataReceived()
{
QString data = QString(tcpSocket->readAll());
emit(sendOutput(data));
if (data.contains("login:")) {
sendTcpData(QString("%1\n").arg(tcpUserName));
return;
}
if (data.contains("password:")) {
sendTcpData(QString("%1\n").arg(tcpPassword));
return;
}
QRegularExpressionMatchIterator i = tcpRegex.globalMatch(data);
while (i.hasNext()) {
QRegularExpressionMatch match = i.next();
if (match.hasMatch()) {
spotData* data = new spotData();
data->spottercall = match.captured(1);
data->frequency = match.captured(2).toFloat() / 1000.0;
data->dxcall = match.captured(3);
data->comment = match.captured(4).trimmed();
data->timestamp = QDateTime::currentDateTimeUtc();
//data.timestamp = QDateTime::fromString(match.captured(5), "hhmmZ");
emit addSpot(data);
emit(sendOutput(QString("TCP: SPOT:%1 SPOTTER:%2 FREQ:%3 MODE:%4 DATE:%5 COMMENT:%6\n")
.arg(data->dxcall).arg(data->spottercall).arg(data->frequency).arg(data->mode)
.arg(data->timestamp.toString()).arg(data->comment)));
}
}
}
void dxClusterClient::sendTcpData(QString data)
{
qInfo(logCluster()) << "Sending:" << data;
if (tcpSocket != Q_NULLPTR && tcpSocket->isValid() && tcpSocket->isOpen())
{
tcpSocket->write(data.toLatin1());
}
else
{
qInfo(logCluster()) << "socket not open!";
}
}
void dxClusterClient::tcpCleanup()
{
emit deleteOldSpots(tcpTimeout);
}

Wyświetl plik

@ -5,12 +5,12 @@
#include <QDebug>
#include <QUdpSocket>
#include <QTcpSocket>
#include <QXmlSimpleReader>
#include <QXmlInputSource>
#include <QDomDocument>
#include <QMutex>
#include <QMutexLocker>
#include <QDateTime>
#include <QRegularExpression>
#include <QTimer>
#include <qcustomplot.h>
struct spotData {
@ -23,6 +23,14 @@ struct spotData {
QCPItemText* text = Q_NULLPTR;
};
struct clusterSettings {
QString server;
int port;
QString userName;
QString password;
int timeout=30;
bool default;
};
class dxClusterClient : public QObject
{
@ -33,31 +41,42 @@ public:
virtual ~dxClusterClient();
signals:
void addSpot(spotData spot);
void addSpot(spotData* spot);
void deleteSpot(QString dxcall);
void deleteOldSpots(int minutes);
void sendOutput(QString text);
public slots:
void udpDataReceived();
void tcpDataReceived();
void enableUdp(bool enable);
void enableTcp(bool enable);
void setUdpPort(int p) { udpPort = p; }
void setTcpServerName(QString s) { tcpServerName = s; }
void setTcpPort(int p) { tcpPort = p; }
void setTcpUserName(QString s) { tcpUserName = s; }
void setTcpPassword(QString s) { tcpPassword = s; }
void setTcpTimeout(int p) { tcpTimeout = p; }
void tcpCleanup();
private:
void sendTcpData(QString data);
bool udpEnable;
bool tcpEnable;
QUdpSocket* udpSocket=Q_NULLPTR;
QTcpSocket* tcpSocket=Q_NULLPTR;
int udpPort;
QString tcpServerName;
int tcpPort;
QString tcpUserName;
QString tcpPassword;
int tcpTimeout;
QDomDocument udpSpotReader;
QXmlInputSource udpXmlSource;
QMutex udpMutex;
QRegularExpression tcpRegex;
QMutex mutex;
bool authenticated=false;
QTimer* tcpCleanupTimer=Q_NULLPTR;
};
#endif

Wyświetl plik

@ -113,19 +113,43 @@ wfmain::wfmain(const QString settingsFile, const QString logFile, bool debugMode
cluster = new dxClusterClient();
clusterThread = new QThread(this);
clusterThread->setObjectName("dxcluster()");
cluster->moveToThread(clusterThread);
connect(this, SIGNAL(setClusterEnableUdp(bool)), cluster, SLOT(enableUdp(bool)));
connect(this, SIGNAL(setClusterEnableTcp(bool)), cluster, SLOT(enableTcp(bool)));
connect(this, SIGNAL(setClusterUdpPort(int)), cluster, SLOT(setUdpPort(int)));
connect(cluster, SIGNAL(addSpot(spotData)), this, SLOT(addClusterSpot(spotData)));
connect(this, SIGNAL(setClusterServerName(QString)), cluster, SLOT(setTcpServerName(QString)));
connect(this, SIGNAL(setClusterTcpPort(int)), cluster, SLOT(setTcpPort(int)));
connect(this, SIGNAL(setClusterUserName(QString)), cluster, SLOT(setTcpUserName(QString)));
connect(this, SIGNAL(setClusterPassword(QString)), cluster, SLOT(setTcpPassword(QString)));
connect(this, SIGNAL(setClusterTimeout(int)), cluster, SLOT(setTcpTimeout(int)));
connect(cluster, SIGNAL(addSpot(spotData*)), this, SLOT(addClusterSpot(spotData*)));
connect(cluster, SIGNAL(deleteSpot(QString)), this, SLOT(deleteClusterSpot(QString)));
connect(cluster, SIGNAL(deleteOldSpots(int)), this, SLOT(deleteOldClusterSpots(int)));
connect(cluster, SIGNAL(sendOutput(QString)), this, SLOT(receiveClusterOutput(QString)));
connect(clusterThread, SIGNAL(finished()), cluster, SLOT(deleteLater()));
clusterThread->start();
emit setClusterUdpPort(12060);
emit setClusterEnableUdp(true);
emit setClusterUdpPort(prefs.clusterUdpPort);
emit setClusterEnableUdp(prefs.clusterUdpEnable);
for (int f = 0; f < clusters.size(); f++)
{
if (clusters[f].default)
{
emit setClusterServerName(clusters[f].server);
emit setClusterTcpPort(clusters[f].port);
emit setClusterUserName(clusters[f].userName);
emit setClusterPassword(clusters[f].password);
emit setClusterTimeout(clusters[f].timeout);
}
}
emit setClusterEnableTcp(prefs.clusterTcpEnable);
setServerToPrefs();
@ -1821,6 +1845,66 @@ void wfmain::loadSettings()
settings->endArray();
settings->endGroup();
settings->beginGroup("Cluster");
prefs.clusterUdpEnable = settings->value("UdpEnabled", false).toBool();
prefs.clusterTcpEnable = settings->value("TcpEnabled", false).toBool();
prefs.clusterUdpPort = settings->value("UdpPort", 12060).toInt();
ui->clusterUdpPortLineEdit->setText(QString::number(prefs.clusterUdpPort));
ui->clusterUdpEnable->setChecked(prefs.clusterUdpEnable);
ui->clusterTcpEnable->setChecked(prefs.clusterTcpEnable);
int numClusters = settings->beginReadArray("Servers");
clusters.clear();
if (numClusters > 0) {
{
for (int f = 0; f < numClusters; f++)
{
settings->setArrayIndex(f);
clusterSettings c;
c.server = settings->value("ServerName", "").toString();
c.port = settings->value("Port", 7300).toInt();
c.userName = settings->value("UserName", "").toString();
c.password = settings->value("Password", "").toString();
c.timeout = settings->value("Timeout", 0).toInt();
c.default = settings->value("Default", false).toBool();
if (!c.server.isEmpty()) {
clusters.append(c);
}
}
int defaultCluster = 0;
ui->clusterServerNameCombo->blockSignals(true);
for (int f = 0; f < clusters.size(); f++)
{
ui->clusterServerNameCombo->addItem(clusters[f].server);
if (clusters[f].default) {
defaultCluster = f;
}
}
ui->clusterServerNameCombo->blockSignals(false);
if (clusters.size() > defaultCluster)
{
ui->clusterServerNameCombo->setCurrentIndex(defaultCluster);
ui->clusterTcpPortLineEdit->blockSignals(true);
ui->clusterUsernameLineEdit->blockSignals(true);
ui->clusterPasswordLineEdit->blockSignals(true);
ui->clusterTimeoutLineEdit->blockSignals(true);
ui->clusterTcpPortLineEdit->setText(QString::number(clusters[defaultCluster].port));
ui->clusterUsernameLineEdit->setText(clusters[defaultCluster].userName);
ui->clusterPasswordLineEdit->setText(clusters[defaultCluster].password);
ui->clusterTimeoutLineEdit->setText(QString::number(clusters[defaultCluster].timeout));
ui->clusterTcpPortLineEdit->blockSignals(false);
ui->clusterUsernameLineEdit->blockSignals(false);
ui->clusterPasswordLineEdit->blockSignals(false);
ui->clusterTimeoutLineEdit->blockSignals(false);
}
}
}
settings->endArray();
settings->endGroup();
}
void wfmain::serverAddUserLine(const QString& user, const QString& pass, const int& type)
@ -2153,11 +2237,29 @@ void wfmain::saveSettings()
}
settings->endArray();
qInfo() << "Server config stored";
settings->endGroup();
settings->beginGroup("Cluster");
settings->setValue("UdpEnabled", prefs.clusterUdpEnable);
settings->setValue("TcpEnabled", prefs.clusterTcpEnable);
settings->setValue("UdpPort", prefs.clusterUdpPort);
settings->beginWriteArray("Servers");
for (int f = 0; f < clusters.count(); f++)
{
settings->setArrayIndex(f);
settings->setValue("ServerName", clusters[f].server);
settings->setValue("UserName", clusters[f].userName);
settings->setValue("Port", clusters[f].port);
settings->setValue("Password", clusters[f].password);
settings->setValue("Timeout", clusters[f].timeout);
settings->setValue("Default", clusters[f].default);
}
settings->endArray();
settings->endGroup();
settings->sync(); // Automatic, not needed (supposedly)
}
@ -7385,57 +7487,182 @@ errMsg:
}
void wfmain::clusterCheck() {
void wfmain::receiveClusterOutput(QString text) {
ui->clusterOutputTextEdit->moveCursor(QTextCursor::End);
ui->clusterOutputTextEdit->insertPlainText(text);
ui->clusterOutputTextEdit->moveCursor(QTextCursor::End);
}
void wfmain::addClusterSpot(spotData spot) {
void wfmain::addClusterSpot(spotData* s) {
spot.text = new QCPItemText(plot);
spot.text->setAntialiased(true);
spot.text->setColor(QColor(Qt::red));
spot.text->setText(spot.dxcall);
spot.text->setFont(QFont(font().family(), 10));
spot.text->setPositionAlignment(Qt::AlignTop | Qt::AlignHCenter);
spot.text->setClipAxisRect(false);
spot.text->position->setType(QCPItemPosition::ptPlotCoords);
s->text = new QCPItemText(plot);
s->text->setAntialiased(true);
s->text->setColor(QColor(Qt::red));
s->text->setText(s->dxcall);
s->text->setFont(QFont(font().family(), 10));
s->text->setPositionAlignment(Qt::AlignTop | Qt::AlignHCenter);
s->text->setClipAxisRect(false);
s->text->position->setType(QCPItemPosition::ptPlotCoords);
//bool conflict = true;
//QCPAxisRect* rect = spot.text->position->axisRect();
double left = spot.frequency;
double left = s->frequency;
double top = rigCaps.spectAmpMax - 50.0;
/*
while (conflict) {
QCPItemText* textItem = plot->itemAt<QCPItemText>(QPointF(left,top));
if (textItem != Q_NULLPTR) {
qInfo(logGui()) << "Found conflicting spot";
if (top > 20.0) {
top = top - 20.0;
}
else {
top = rigCaps.spectAmpMax - 50.0;
left = left + 0.2;
}
}
else {
conflict = false;
}
}
*/
spot.text->position->setCoords(left, top);
s->text->position->setCoords(left, top);
//QList<QGraphicsItem*> col_it = spot.text->item(Qt::IntersectsItemBoundingRect);
clusterSpots.insert(spot.dxcall, &spot);
qInfo(logGui()) << "Number of cluster spots" << clusterSpots.size();
QMutexLocker locker(&clusterMutex);
clusterSpots.insert(s->dxcall, s);
//qInfo(logGui()) << "Number of cluster spots" << clusterSpots.size();
}
void wfmain::deleteClusterSpot(QString dxcall) {
QMutexLocker locker(&clusterMutex);
QMap<QString, spotData*>::iterator spot = clusterSpots.find(dxcall);
while (spot != clusterSpots.end() && spot.key() == dxcall) {
if (spot.value()->text != Q_NULLPTR)
{
plot->removeItem(spot.value()->text);
}
delete spot.value();
spot = clusterSpots.erase(spot);
}
}
void wfmain::on_clusterUdpEnable_clicked(bool enable)
{
prefs.clusterUdpEnable = enable;
emit setClusterEnableUdp(enable);
}
void wfmain::on_clusterTcpEnable_clicked(bool enable)
{
prefs.clusterTcpEnable = enable;
emit setClusterEnableTcp(enable);
}
void wfmain::on_clusterUdpPortLineEdit_editingFinished()
{
prefs.clusterUdpPort = ui->clusterUdpPortLineEdit->text().toInt();
emit setClusterUdpPort(prefs.clusterUdpPort);
}
void wfmain::on_clusterServerNameCombo_currentIndexChanged(int index)
{
if (index < 0)
return;
QString text = ui->clusterServerNameCombo->currentText();
if (clusters.size() <= index)
{
qInfo(logGui) << "Adding Cluster server" << text;
clusterSettings c;
c.server = text;
clusters.append(c);
}
else {
qInfo(logGui) << "Editing Cluster server" << text;
clusters[index].server = text;
}
ui->clusterUsernameLineEdit->blockSignals(true);
ui->clusterPasswordLineEdit->blockSignals(true);
ui->clusterTimeoutLineEdit->blockSignals(true);
ui->clusterTcpPortLineEdit->setText(QString::number(clusters[index].port));
ui->clusterUsernameLineEdit->setText(clusters[index].userName);
ui->clusterPasswordLineEdit->setText(clusters[index].password);
ui->clusterTimeoutLineEdit->setText(QString::number(clusters[index].timeout));
ui->clusterUsernameLineEdit->blockSignals(false);
ui->clusterPasswordLineEdit->blockSignals(false);
ui->clusterTimeoutLineEdit->blockSignals(false);
for (int i = 0; i < clusters.size(); i++) {
if (i == index)
clusters[index].default = true;
else
clusters[index].default = false;
}
emit setClusterServerName(clusters[index].server);
emit setClusterTcpPort(clusters[index].port);
emit setClusterUserName(clusters[index].userName);
emit setClusterPassword(clusters[index].password);
emit setClusterTimeout(clusters[index].timeout);
}
void wfmain::on_clusterServerNameCombo_currentTextChanged(QString text)
{
if (text.isEmpty()) {
int index = ui->clusterServerNameCombo->currentIndex();
ui->clusterServerNameCombo->removeItem(index);
clusters.removeAt(index);
}
}
void wfmain::on_clusterTcpPortLineEdit_editingFinished()
{
int index = ui->clusterServerNameCombo->currentIndex();
if (index < clusters.size())
{
clusters[index].port = ui->clusterTcpPortLineEdit->displayText().toInt();
emit setClusterTcpPort(clusters[index].port);
}
}
void wfmain::on_clusterUsernameLineEdit_editingFinished()
{
int index = ui->clusterServerNameCombo->currentIndex();
if (index < clusters.size())
{
clusters[index].userName = ui->clusterUsernameLineEdit->text();
emit setClusterUserName(clusters[index].userName);
}
}
void wfmain::on_clusterPasswordLineEdit_editingFinished()
{
int index = ui->clusterServerNameCombo->currentIndex();
if (index < clusters.size())
{
clusters[index].password = ui->clusterPasswordLineEdit->text();
emit setClusterPassword(clusters[index].password);
}
}
void wfmain::on_clusterTimeoutLineEdit_editingFinished()
{
int index = ui->clusterServerNameCombo->currentIndex();
if (index < clusters.size())
{
clusters[index].timeout = ui->clusterTimeoutLineEdit->displayText().toInt();
emit setClusterTimeout(clusters[index].timeout);
}
}
void wfmain::deleteOldClusterSpots(int timeout)
{
QMutexLocker locker(&clusterMutex);
QDateTime time=QDateTime::currentDateTimeUtc();
//qDebug(logGui()) << "Deleting old Cluster spots";
QMap<QString, spotData*>::iterator spot = clusterSpots.begin();
while (spot != clusterSpots.end()) {
if (spot.value()->timestamp.addSecs(timeout * 60) < time) {
if (spot.value()->text != Q_NULLPTR)
{
plot->removeItem(spot.value()->text);
}
//qDebug(logGui()) << "Deleting:" << spot.value()->dxcall << "Timestamp" << spot.value()->timestamp.addSecs(timeout * 60) << "is lower than" << time;
delete spot.value(); // Stop memory leak?
spot = clusterSpots.erase(spot);
}
else {
//qDebug(logGui()) << "Spot:" << spot.value()->dxcall << "Timestamp" << spot.value()->timestamp.addSecs(timeout * 60) << "not lower than" << time;
++spot;
}
}
}

Wyświetl plik

@ -192,10 +192,16 @@ signals:
void setClusterUdpPort(int port);
void setClusterEnableUdp(bool udp);
void setClusterEnableTcp(bool tcp);
void setClusterServerName(QString name);
void setClusterTcpPort(int port);
void setClusterUserName(QString name);
void setClusterPassword(QString pass);
void setClusterTimeout(int timeout);
private slots:
void addClusterSpot(spotData spot);
void addClusterSpot(spotData* spot);
void deleteClusterSpot(QString dxcall);
void deleteOldClusterSpots(int timeout);
void updateSizes(int tabIndex);
void shortcutF1();
void shortcutF2();
@ -658,7 +664,18 @@ private slots:
void on_customEdgeBtn_clicked();
void clusterCheck();
void on_clusterUdpEnable_clicked(bool enable);
void on_clusterTcpEnable_clicked(bool enable);
void on_clusterUdpPortLineEdit_editingFinished();
void on_clusterServerNameCombo_currentTextChanged(QString text);
void on_clusterServerNameCombo_currentIndexChanged(int index);
void on_clusterTcpPortLineEdit_editingFinished();
void on_clusterUsernameLineEdit_editingFinished();
void on_clusterPasswordLineEdit_editingFinished();
void on_clusterTimeoutLineEdit_editingFinished();
void receiveClusterOutput(QString text);
private:
Ui::wfmain *ui;
@ -912,6 +929,13 @@ private:
quint16 tcpPort;
quint8 waterfallFormat;
audioType audioSystem;
bool clusterUdpEnable;
bool clusterTcpEnable;
int clusterUdpPort;
QString clusterTcpServerName;
QString clusterTcpUserName;
QString clusterTcpPassword;
int clusterTimeout;
} prefs;
preferences defPrefs;
@ -1052,7 +1076,8 @@ private:
QMap<QString, spotData*> clusterSpots;
QTimer clusterTimer;
QCPItemText* text=Q_NULLPTR;
QList<clusterSettings> clusters;
QMutex clusterMutex;
};
Q_DECLARE_METATYPE(struct rigCapabilities)

Wyświetl plik

@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>1075</width>
<width>1125</width>
<height>660</height>
</rect>
</property>
@ -4898,7 +4898,7 @@
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="clusterUdpPort">
<widget class="QLineEdit" name="clusterUdpPortLineEdit">
<property name="inputMask">
<string>00000</string>
</property>
@ -4919,8 +4919,8 @@
<rect>
<x>330</x>
<y>50</y>
<width>241</width>
<height>181</height>
<width>311</width>
<height>241</height>
</rect>
</property>
<property name="title">
@ -4931,8 +4931,8 @@
<rect>
<x>20</x>
<y>40</y>
<width>201</width>
<height>121</height>
<width>271</width>
<height>181</height>
</rect>
</property>
<layout class="QFormLayout" name="formLayout_2">
@ -4943,28 +4943,29 @@
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="clusterServerName"/>
</item>
<item row="2" column="0">
<item row="3" column="0">
<widget class="QLabel" name="label_46">
<property name="text">
<string>Username</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="clusterUsername"/>
<item row="3" column="1">
<widget class="QLineEdit" name="clusterUsernameLineEdit"/>
</item>
<item row="3" column="0">
<item row="4" column="0">
<widget class="QLabel" name="label_47">
<property name="text">
<string>Password</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="clusterPassword"/>
<item row="4" column="1">
<widget class="QLineEdit" name="clusterPasswordLineEdit">
<property name="echoMode">
<enum>QLineEdit::PasswordEchoOnEdit</enum>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="clusterTcpEnable">
@ -4973,12 +4974,70 @@
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="clusterServerNameCombo">
<property name="editable">
<bool>true</bool>
</property>
<property name="duplicatesEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_48">
<property name="text">
<string>Spot Timeout (minutes)</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLineEdit" name="clusterTimeoutLineEdit">
<property name="inputMask">
<string>0000</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_49">
<property name="text">
<string>Server Port</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="clusterTcpPortLineEdit">
<property name="inputMask">
<string>00000</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<widget class="QPlainTextEdit" name="clusterOutputTextEdit">
<property name="geometry">
<rect>
<x>10</x>
<y>320</y>
<width>811</width>
<height>141</height>
</rect>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<property name="sizeAdjustPolicy">
<enum>QAbstractScrollArea::AdjustIgnored</enum>
</property>
</widget>
<zorder>groupBox_9</zorder>
<zorder>horizontalLayoutWidget</zorder>
<zorder>groupBox_10</zorder>
<zorder>clusterOutputTextEdit</zorder>
</widget>
<widget class="QWidget" name="experimental">
<layout class="QVBoxLayout" name="verticalLayout_28">
@ -5146,7 +5205,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>1075</width>
<width>1125</width>
<height>21</height>
</rect>
</property>
@ -5174,7 +5233,7 @@
<resources/>
<connections/>
<buttongroups>
<buttongroup name="buttonGroup"/>
<buttongroup name="underlayButtonGroup"/>
<buttongroup name="buttonGroup"/>
</buttongroups>
</ui>