Send status information to UDP server

To  facilitate interaction  with other  applications WSJT-X  now sends
status  updates  to  a  predefined   UDP  server  or  multicast  group
address. The  status updates include the  information currently posted
to  the  decodes.txt and  wsjtx_status.txt  files.   An optional  back
communications  channel is  also implemented  allowing the  UDP server
application to control some basic actions in WSJT-X.

A reference implementaion of a typical UDP server written in C++ using
Qt is  provided to demonstrate  these facilities. This  application is
not intended  as a user  tool but  only as an  example of how  a third
party application may interact with WSJT-X.

The  UDP messages  Use QDataStream  based serialization.  Messages are
documented in  NetworkMessage.hpp along with some  helper classes that
simplify the building and decoding of messages.

Two  message  handling  classes   are  introduced,  MessageClient  and
MessageServer.  WSJT-X uses the MessageClient class to manage outgoing
and  incoming  UDP  messages   that  allow  communication  with  other
applications.   The MessageServer  class implements  the kind  of code
that a  potential cooperating  application might use.   Although these
classes  use  Qt serialization  facilities,  the  message formats  are
easily  read and  written  by  applications that  do  not  use the  Qt
framework.

MessageAggregator   is   a   demonstration   application   that   uses
MessageServer and  presents a GUI  that displays messages from  one or
more  WSJT-X instances  and  allows sending  back a  CQ  or QRZ  reply
invocation  by double  clicking  a decode.   This  application is  not
intended as  a user facing tool  but rather as a  demonstration of the
WSJT-X UDP messaging facility. It  also demonstrates being a multicast
UDP server by allowing multiple instances to run concurrently. This is
enabled by using an appropriate  multicast group address as the server
address.  Cooperating   applications  need  not   implement  multicast
techniques but  it is recomended  otherwise only a  single appliaction
can act as a broadcast message (from WSJT-X) recipient.

git-svn-id: svn+ssh://svn.code.sf.net/p/wsjt/wsjt/branches/wsjtx@5225 ab8295b8-cf94-4d9e-aec4-7959e3be5d79
This commit is contained in:
Bill Somerville
2015-04-15 16:40:49 +00:00
parent a5642a4807
commit 3f75b4144a
19 changed files with 1933 additions and 113 deletions
+148 -30
View File
@@ -16,6 +16,7 @@
#include <QDebug>
#include <QtConcurrent/QtConcurrentRun>
#include <QProgressDialog>
#include <QHostInfo>
#include "revision_utils.hpp"
#include "soundout.h"
@@ -33,6 +34,7 @@
#include "StationList.hpp"
#include "LiveFrequencyValidator.hpp"
#include "FrequencyItemDelegate.hpp"
#include "MessageClient.hpp"
#include "ui_mainwindow.h"
#include "moc_mainwindow.cpp"
@@ -102,7 +104,6 @@ MainWindow::MainWindow(bool multiple, QSettings * settings, QSharedMemory *shdme
m_lastMessageType {-1},
m_appDir {QApplication::applicationDirPath ()},
mem_jt9 {shdmem},
psk_Reporter (new PSK_Reporter (this)),
m_msAudioOutputBuffered (0u),
m_framesAudioInputBuffered (RX_SAMPLE_RATE / 10),
m_downSampleFactor (downSampleFactor),
@@ -117,7 +118,9 @@ MainWindow::MainWindow(bool multiple, QSettings * settings, QSharedMemory *shdme
m_firstDecode {0},
m_optimizingProgress {"Optimizing decoder FFTs for your CPU.\n"
"Please be patient,\n"
"this may take a few minutes", QString {}, 0, 1, this}
"this may take a few minutes", QString {}, 0, 1, this},
m_messageClient {new MessageClient {QApplication::applicationName (), m_config.udp_server_name (), m_config.udp_server_port (), this}},
psk_Reporter {new PSK_Reporter {m_messageClient, this}}
{
ui->setupUi(this);
@@ -188,7 +191,12 @@ MainWindow::MainWindow(bool multiple, QSettings * settings, QSharedMemory *shdme
connect (this, &MainWindow::finished, m_logDlg.data (), &LogQSO::close);
on_EraseButton_clicked();
// Network message handlers
connect (m_messageClient, &MessageClient::reply, this, &MainWindow::replyToCQ);
connect (m_messageClient, &MessageClient::replay, this, &MainWindow::replayDecodes);
connect (m_messageClient, &MessageClient::error, this, &MainWindow::networkError);
on_EraseButton_clicked ();
QActionGroup* modeGroup = new QActionGroup(this);
ui->actionJT9_1->setActionGroup(modeGroup);
@@ -264,6 +272,8 @@ MainWindow::MainWindow(bool multiple, QSettings * settings, QSharedMemory *shdme
// hook up configuration signals
connect (&m_config, &Configuration::transceiver_update, this, &MainWindow::handle_transceiver_update);
connect (&m_config, &Configuration::transceiver_failure, this, &MainWindow::handle_transceiver_failure);
connect (&m_config, &Configuration::udp_server_changed, m_messageClient, &MessageClient::set_server);
connect (&m_config, &Configuration::udp_server_port_changed, m_messageClient, &MessageClient::set_server_port);
// set up message text validators
ui->tx1->setValidator (new QRegExpValidator {message_alphabet, this});
@@ -673,6 +683,7 @@ void MainWindow::on_actionSettings_triggered() //Setup Dialog
ui->readFreq->setStyleSheet("");
ui->readFreq->setEnabled(false);
// things that might change that we need know about
auto callsign = m_config.my_callsign ();
if (QDialog::Accepted == m_config.exec ())
@@ -973,6 +984,8 @@ void MainWindow::displayDialFrequency ()
void MainWindow::statusChanged()
{
m_messageClient->status_update (m_dialFreq, m_mode, m_hisCall, QString::number (ui->rptSpinBox->value ()), m_modeTx);
QFile f {m_config.temp_dir ().absoluteFilePath ("wsjtx_status.txt")};
if(f.open(QFile::WriteOnly | QIODevice::Text)) {
QTextStream out(&f);
@@ -1351,6 +1364,8 @@ void MainWindow::readFromStderr() //readFromStderr
void MainWindow::readFromStdout() //readFromStdout
{
QString band = m_config.bands ()->data (m_config.bands ()->find (m_dialFreq)).toString();
while(proc_jt9.canReadLine())
{
QByteArray t=proc_jt9.readLine();
@@ -1430,6 +1445,8 @@ void MainWindow::readFromStdout() //readFromStdout
m_QSOText=decodedtext;
}
postDecode (true, decodedtext.string ());
// find and extract any report for myCall
bool stdMsg = decodedtext.report(m_baseCall
, Radio::base_callsign (ui->dxCallEntry-> text ().toUpper ().trimmed ())
@@ -1476,6 +1493,7 @@ void MainWindow::on_EraseButton_clicked() //Erase
m_QSOText.clear();
if((ms-m_msErase)<500) {
ui->decodedTextBrowser->clear();
m_messageClient->clear_decodes ();
QFile f(m_config.temp_dir ().absoluteFilePath ("decoded.txt"));
if(f.exists()) f.remove();
}
@@ -1949,17 +1967,23 @@ void MainWindow::doubleClickOnCall(bool shift, bool ctrl)
if(!m_decodedText2) cursor=ui->decodedTextBrowser2->textCursor();
if(m_decodedText2) cursor=ui->decodedTextBrowser->textCursor();
cursor.select(QTextCursor::LineUnderCursor);
int i2=cursor.position();
if(shift and i2==-9999) return; //Silence compiler warning
int position {cursor.position()};
if(shift && position==-9999) return; //Silence compiler warning
QString t;
if(!m_decodedText2) t= ui->decodedTextBrowser2->toPlainText(); //Full contents
if(m_decodedText2) t= ui->decodedTextBrowser->toPlainText();
QString messages;
if(!m_decodedText2) messages= ui->decodedTextBrowser2->toPlainText();
//Full contents
if(m_decodedText2) messages= ui->decodedTextBrowser->toPlainText();
QString t1 = t.mid(0,i2); //contents up to \n on selected line
processMessage(messages, position, ctrl);
}
void MainWindow::processMessage(QString const& messages, int position, bool ctrl)
{
QString t1 = messages.mid(0,position); //contents up to \n on selected line
int i1=t1.lastIndexOf("\n") + 1; //points to first char of line
DecodedText decodedtext;
decodedtext = t1.mid(i1,i2-i1); //selected line
decodedtext = messages.mid(i1,position-i1); //selected line
if (decodedtext.indexOf(" CQ ") > 0)
{
@@ -2509,7 +2533,7 @@ void MainWindow::on_logQSOButton_clicked() //Log QSO button
, m_rptSent
, m_rptRcvd
, m_dateTimeQSO
, (m_dialFreq + ui->TxFreqSpinBox->value ()) / 1.e6
, m_dialFreq + ui->TxFreqSpinBox->value ()
, m_config.my_callsign ()
, m_config.my_grid ()
, m_noSuffix
@@ -2518,27 +2542,28 @@ void MainWindow::on_logQSOButton_clicked() //Log QSO button
);
}
void MainWindow::acceptQSO2(bool accepted)
void MainWindow::acceptQSO2(QDateTime const& QSO_date, QString const& call, QString const& grid
, Frequency dial_freq, QString const& mode
, QString const& rpt_sent, QString const& rpt_received
, QString const& tx_power, QString const& comments
, QString const& name)
{
if(accepted)
{
auto const& bands_model = m_config.bands ();
auto band = bands_model->data (bands_model->find (m_dialFreq + ui->TxFreqSpinBox->value ())).toString ();
QString date = m_dateTimeQSO.toString("yyyy-MM-dd");
date=date.mid(0,4) + date.mid(5,2) + date.mid(8,2);
m_logBook.addAsWorked(m_hisCall,band,m_modeTx,date);
QString band = ADIF::bandFromFrequency ((m_dialFreq + ui->TxFreqSpinBox->value ()) / 1.e6);
QString date = m_dateTimeQSO.toString("yyyyMMdd");
m_logBook.addAsWorked(m_hisCall,band,m_modeTx,date);
if (m_config.clear_DX ())
{
m_hisCall="";
ui->dxCallEntry->setText("");
m_hisGrid="";
ui->dxGridEntry->setText("");
m_rptSent="";
m_rptRcvd="";
m_qsoStart="";
m_qsoStop="";
}
m_messageClient->qso_logged (QSO_date, call, grid, dial_freq, mode, rpt_sent, rpt_received, tx_power, comments, name);
if (m_config.clear_DX ())
{
m_hisCall="";
ui->dxCallEntry->setText("");
m_hisGrid="";
ui->dxGridEntry->setText("");
m_rptSent="";
m_rptRcvd="";
m_qsoStart="";
m_qsoStop="";
}
}
@@ -3260,3 +3285,96 @@ void MainWindow::transmitDisplay (bool transmitting)
}
}
}
// Takes a decoded CQ line and sets it up for reply
void MainWindow::replyToCQ (QTime time, qint32 snr, float delta_time, quint32 delta_frequency, QString const& mode, QString const& message_text)
{
if (!m_config.accept_udp_requests ())
{
return;
}
auto decode_parts = message_text.split (' ', QString::SkipEmptyParts);
if (decode_parts.contains ("CQ") || decode_parts.contains ("QRZ"))
{
// a message we are willing to accept
auto cqtext = QString {"%1 %2 %3 %4 %5 %6"}.arg (time.toString ("hhmm"))
.arg (snr, 3)
.arg (delta_time, 4, 'f', 1)
.arg (delta_frequency, 4)
.arg (mode)
.arg (message_text);
auto messages = ui->decodedTextBrowser->toPlainText ();
auto position = messages.lastIndexOf (cqtext);
if (position >= 0)
{
if (m_config.udpWindowToFront ())
{
show ();
raise ();
activateWindow ();
}
if (m_config.udpWindowRestore () && isMinimized ())
{
showNormal ();
raise ();
}
// find the linefeed at the end of the line
position = ui->decodedTextBrowser->toPlainText().indexOf("\n",position);
processMessage (messages, position, false);
}
else
{
qDebug () << "reply to CQ request ignored, decode not found:" << cqtext;
}
}
else
{
qDebug () << "rejecting UDP request to reply as decode is not a CQ or QRZ";
}
}
void MainWindow::replayDecodes ()
{
// we accept this request even if the setting to accept UDP requests
// is not checked
Q_FOREACH (auto const& message, ui->decodedTextBrowser->toPlainText ().split ('\n', QString::SkipEmptyParts))
{
if (message.size() >= 4 && message.left (4) != "----")
{
auto eom_pos = message.indexOf (' ', 35);
// we always want at least the characters to position 35
if (eom_pos < 35)
{
eom_pos = message.size () - 1;
}
postDecode (false, message.left (eom_pos + 1));
}
}
}
void MainWindow::postDecode (bool is_new, QString const& message)
{
auto decode = message.trimmed ();
auto parts = decode.left (21).split (' ', QString::SkipEmptyParts);
if (parts.size () >= 5)
{
m_messageClient->decode (is_new, QTime::fromString (parts[0], "hhmm"), parts[1].toInt ()
, parts[2].toFloat (), parts[3].toUInt (), parts[4], decode.mid (21));
}
}
void MainWindow::networkError (QString const& e)
{
if (QMessageBox::Retry == QMessageBox::warning (this, tr ("Network Error")
, tr ("Error: %1\nUDP server %2:%3")
.arg (e)
.arg (m_config.udp_server_name ())
.arg (m_config.udp_server_port ())
, QMessageBox::Cancel | QMessageBox::Retry, QMessageBox::Cancel))
{
// retry server lookup
m_messageClient->set_server (m_config.udp_server_name ());
}
}