WSJT-X/MessageAggregator.cpp
Bill Somerville 1937612685 Remove Tab 1/2 differences from UDP interface
The UDP "Free  text" message comamnd should  not expose implementation
differences between tab one and tab two. The "send next" capability is
not available on  tab two so use  of it has been removed  from the UDP
interface.

The ability  to trigger  the immediate  (or in next  Tx period  if the
current period  is Rx) sending  of the  current free text  message has
been added to the UDP interface. This is done by sending a "Free text"
UDP message containing an empty text message with the "Send" flag set.

git-svn-id: svn+ssh://svn.code.sf.net/p/wsjt/wsjt/branches/wsjtx@5725 ab8295b8-cf94-4d9e-aec4-7959e3be5d79
2015-07-27 11:34:11 +00:00

509 lines
19 KiB
C++

//
// MessageAggregator - an example application that utilizes the WSJT-X
// messaging facility
//
// This application is only provided as a simple GUI application
// example to demonstrate the WSJT-X messaging facility. It allows the
// user to set the server details either as a unicast UDP server or,
// if a multicast group address is provided, as a multicast server.
// The benefit of the multicast server is that multiple servers can be
// active at once each receiving all WSJT-X broadcast messages and
// each able to respond to individual WSJT_X clients. To utilize the
// multicast group features each WSJT-X client must set the same
// multicast group address as the UDP server address for example
// 239.255.0.0 for a site local multicast group.
//
// The UI is a small panel to input the service port number and
// optionally the multicast group address. Below that a table
// representing the log entries where any QSO logged messages
// broadcast from WSJT-X clients are displayed. The bottom of the
// application main window is a dock area where a dock window will
// appear for each WSJT-X client, this window contains a table of the
// current decode messages broadcast from that WSJT-X client and a
// status line showing the status update messages broadcast from the
// WSJT_X client. The dock windows may be arranged in a tab bar, side
// by side, below each other or, completely detached from the dock
// area as floating windows. Double clicking the dock window title bar
// or dragging and dropping with the mouse allows these different
// arrangements.
//
// The application also provides a simple menu bar including a view
// menu that allows each dock window to be hidden or revealed.
//
#include <iostream>
#include <exception>
#include <QtWidgets>
#include <QFile>
#include <QStandardItemModel>
#include <QStandardItem>
#include <QSortFilterProxyModel>
#include <QFont>
#include <QDateTime>
#include <QTime>
#include <QHash>
#include "MessageServer.hpp"
#include "NetworkMessage.hpp"
#include "qt_helpers.hpp"
using port_type = MessageServer::port_type;
using Frequency = MessageServer::Frequency;
QRegExp message_alphabet {"[- A-Za-z0-9+./?]*"};
//
// Decodes Model - simple data model for all decodes
//
// The model is a basic table with uniform row format. Rows consist of
// QStandardItem instances containing the string representation of the
// column data and if the underlying field is not a string then the
// UserRole+1 role contains the underlying data item.
//
// Three slots are provided to add a new decode, remove all decodes
// for a client and, to build a reply to CQ message for a given row
// which is emitted as a signal respectively.
//
class DecodesModel
: public QStandardItemModel
{
Q_OBJECT;
public:
DecodesModel (QObject * parent = nullptr)
: QStandardItemModel {0, 7, parent}
, text_font_ {"Courier", 10}
{
setHeaderData (0, Qt::Horizontal, tr ("Client"));
setHeaderData (1, Qt::Horizontal, tr ("Time"));
setHeaderData (2, Qt::Horizontal, tr ("Snr"));
setHeaderData (3, Qt::Horizontal, tr ("DT"));
setHeaderData (4, Qt::Horizontal, tr ("DF"));
setHeaderData (5, Qt::Horizontal, tr ("Md"));
setHeaderData (6, Qt::Horizontal, tr ("Message"));
}
Q_SLOT void add_decode (bool is_new, QString const& client_id, QTime time, qint32 snr, float delta_time
, quint32 delta_frequency, QString const& mode, QString const& message)
{
if (!is_new)
{
int target_row {-1};
for (auto row = 0; row < rowCount (); ++row)
{
if (data (index (row, 0)).toString () == client_id)
{
auto row_time = item (row, 1)->data ().toTime ();
if (row_time == time
&& item (row, 2)->data ().toInt () == snr
&& item (row, 3)->data ().toFloat () == delta_time
&& item (row, 4)->data ().toUInt () == delta_frequency
&& data (index (row, 5)).toString () == mode
&& data (index (row, 6)).toString () == message)
{
return;
}
if (time <= row_time)
{
target_row = row; // last row with same time
}
}
}
if (target_row >= 0)
{
insertRow (target_row + 1, make_row (client_id, time, snr, delta_time, delta_frequency, mode, message));
return;
}
}
appendRow (make_row (client_id, time, snr, delta_time, delta_frequency, mode, message));
}
QList<QStandardItem *> make_row (QString const& client_id, QTime time, qint32 snr, float delta_time
, quint32 delta_frequency, QString const& mode, QString const& message) const
{
auto time_item = new QStandardItem {time.toString ("hh:mm")};
time_item->setData (time);
time_item->setTextAlignment (Qt::AlignRight);
auto snr_item = new QStandardItem {QString::number (snr)};
snr_item->setData (snr);
snr_item->setTextAlignment (Qt::AlignRight);
auto dt = new QStandardItem {QString::number (delta_time)};
dt->setData (delta_time);
dt->setTextAlignment (Qt::AlignRight);
auto df = new QStandardItem {QString::number (delta_frequency)};
df->setData (delta_frequency);
df->setTextAlignment (Qt::AlignRight);
auto md = new QStandardItem {mode};
md->setTextAlignment (Qt::AlignHCenter);
QList<QStandardItem *> row {
new QStandardItem {client_id}, time_item, snr_item, dt, df, md, new QStandardItem {message}};
Q_FOREACH (auto& item, row)
{
item->setEditable (false);
item->setFont (text_font_);
item->setTextAlignment (item->textAlignment () | Qt::AlignVCenter);
}
return row;
}
Q_SLOT void clear_decodes (QString const& client_id)
{
for (auto row = rowCount () - 1; row >= 0; --row)
{
if (data (index (row, 0)).toString () == client_id)
{
removeRow (row);
}
}
}
Q_SLOT void do_reply (QModelIndex const& source)
{
auto row = source.row ();
Q_EMIT reply (data (index (row, 0)).toString ()
, item (row, 1)->data ().toTime ()
, item (row, 2)->data ().toInt ()
, item (row, 3)->data ().toFloat ()
, item (row, 4)->data ().toInt ()
, data (index (row, 5)).toString ()
, data (index (row, 6)).toString ());
}
Q_SIGNAL void reply (QString const& id, QTime time, qint32 snr, float delta_time, quint32 delta_frequency
, QString const& mode, QString const& message);
private:
QFont text_font_;
};
class ClientWidget
: public QDockWidget
{
Q_OBJECT;
public:
explicit ClientWidget (QAbstractItemModel * decodes_model, QString const& id, QWidget * parent = 0)
: QDockWidget {id, parent}
, id_ {id}
, decodes_table_view_ {new QTableView}
, message_line_edit_ {new QLineEdit}
, auto_off_button_ {new QPushButton {tr ("&Auto Off")}}
, halt_tx_button_ {new QPushButton {tr ("&Halt Tx")}}
, mode_label_ {new QLabel}
, dx_call_label_ {new QLabel}
, frequency_label_ {new QLabel}
, report_label_ {new QLabel}
{
auto content_layout = new QVBoxLayout;
content_layout->setContentsMargins (QMargins {2, 2, 2, 2});
// set up table
auto proxy_model = new DecodesFilterModel {id, this};
proxy_model->setSourceModel (decodes_model);
decodes_table_view_->setModel (proxy_model);
decodes_table_view_->verticalHeader ()->hide ();
decodes_table_view_->hideColumn (0);
content_layout->addWidget (decodes_table_view_);
// set up controls
auto control_layout = new QHBoxLayout;
auto form_layout = new QFormLayout;
form_layout->addRow (tr ("Free text:"), message_line_edit_);
message_line_edit_->setValidator (new QRegExpValidator {message_alphabet, this});
connect (message_line_edit_, &QLineEdit::textEdited, [this] (QString const& text) {
Q_EMIT do_free_text (id_, text, false);
});
connect (message_line_edit_, &QLineEdit::editingFinished, [this] () {
Q_EMIT do_free_text (id_, message_line_edit_->text (), true);
});
control_layout->addLayout (form_layout);
control_layout->addWidget (auto_off_button_);
control_layout->addWidget (halt_tx_button_);
connect (auto_off_button_, &QAbstractButton::clicked, [this] (bool /* checked */) {
Q_EMIT do_halt_tx (id_, true);
});
connect (halt_tx_button_, &QAbstractButton::clicked, [this] (bool /* checked */) {
Q_EMIT do_halt_tx (id_, false);
});
content_layout->addLayout (control_layout);
// set up status area
auto status_bar = new QStatusBar;
status_bar->addPermanentWidget (mode_label_);
status_bar->addPermanentWidget (dx_call_label_);
status_bar->addPermanentWidget (frequency_label_);
status_bar->addPermanentWidget (report_label_);
content_layout->addWidget (status_bar);
connect (this, &ClientWidget::topLevelChanged, status_bar, &QStatusBar::setSizeGripEnabled);
// set up central widget
auto content_widget = new QFrame;
content_widget->setFrameStyle (QFrame::StyledPanel | QFrame::Sunken);
content_widget->setLayout (content_layout);
setWidget (content_widget);
// setMinimumSize (QSize {550, 0});
setFeatures (DockWidgetMovable | DockWidgetFloatable);
setAllowedAreas (Qt::BottomDockWidgetArea);
// connect up table view signals
connect (decodes_table_view_, &QTableView::doubleClicked, this, [this, proxy_model] (QModelIndex const& index) {
Q_EMIT do_reply (proxy_model->mapToSource (index));
});
}
Q_SLOT void update_status (QString const& id, Frequency f, QString const& mode, QString const& dx_call
, QString const& report, QString const& tx_mode, bool tx_enabled, bool transmitting)
{
if (id == id_)
{
mode_label_->setText (QString {"Mode: %1%2"}.arg (mode).arg (tx_mode.isEmpty () ? tx_mode : '(' + tx_mode + ')'));
dx_call_label_->setText ("DX CALL: " + dx_call);
frequency_label_->setText ("QRG: " + Radio::pretty_frequency_MHz_string (f));
report_label_->setText ("SNR: " + report);
update_dynamic_property (frequency_label_, "transmitting", transmitting);
auto_off_button_->setEnabled (tx_enabled);
halt_tx_button_->setEnabled (transmitting);
}
}
Q_SLOT void decode_added (bool /*is_new*/, QString const& client_id, QTime /*time*/, qint32 /*snr*/
, float /*delta_time*/, quint32 /*delta_frequency*/, QString const& /*mode*/
, QString const& /*message*/)
{
if (client_id == id_)
{
decodes_table_view_->resizeColumnsToContents ();
decodes_table_view_->horizontalHeader ()->setStretchLastSection (true);
decodes_table_view_->scrollToBottom ();
}
}
Q_SIGNAL void do_reply (QModelIndex const&);
Q_SIGNAL void do_halt_tx (QString const& id, bool auto_only);
Q_SIGNAL void do_free_text (QString const& id, QString const& text, bool);
private:
class DecodesFilterModel final
: public QSortFilterProxyModel
{
public:
DecodesFilterModel (QString const& id, QObject * parent = nullptr)
: QSortFilterProxyModel {parent}
, id_ {id}
{}
protected:
bool filterAcceptsRow (int source_row, QModelIndex const& source_parent) const override
{
auto source_index_col0 = sourceModel ()->index (source_row, 0, source_parent);
return sourceModel ()->data (source_index_col0).toString () == id_;
}
private:
QString id_;
};
QString id_;
QTableView * decodes_table_view_;
QLineEdit * message_line_edit_;
QAbstractButton * auto_off_button_;
QAbstractButton * halt_tx_button_;
QLabel * mode_label_;
QLabel * dx_call_label_;
QLabel * frequency_label_;
QLabel * report_label_;
};
class MainWindow
: public QMainWindow
{
Q_OBJECT;
public:
MainWindow ()
: log_ {new QStandardItemModel {0, 10, this}}
, decodes_model_ {new DecodesModel {this}}
, server_ {new MessageServer {this}}
, multicast_group_line_edit_ {new QLineEdit}
, log_table_view_ {new QTableView}
{
// logbook
log_->setHeaderData (0, Qt::Horizontal, tr ("Date/Time"));
log_->setHeaderData (1, Qt::Horizontal, tr ("Callsign"));
log_->setHeaderData (2, Qt::Horizontal, tr ("Grid"));
log_->setHeaderData (3, Qt::Horizontal, tr ("Name"));
log_->setHeaderData (4, Qt::Horizontal, tr ("Frequency"));
log_->setHeaderData (5, Qt::Horizontal, tr ("Mode"));
log_->setHeaderData (6, Qt::Horizontal, tr ("Sent"));
log_->setHeaderData (7, Qt::Horizontal, tr ("Rec'd"));
log_->setHeaderData (8, Qt::Horizontal, tr ("Power"));
log_->setHeaderData (9, Qt::Horizontal, tr ("Comments"));
connect (server_, &MessageServer::qso_logged, this, &MainWindow::log_qso);
// menu bar
auto file_menu = menuBar ()->addMenu (tr ("&File"));
auto exit_action = new QAction {tr ("E&xit"), this};
exit_action->setShortcuts (QKeySequence::Quit);
exit_action->setToolTip (tr ("Exit the application"));
file_menu->addAction (exit_action);
connect (exit_action, &QAction::triggered, this, &MainWindow::close);
view_menu_ = menuBar ()->addMenu (tr ("&View"));
// central layout
auto central_layout = new QVBoxLayout;
// server details
auto port_spin_box = new QSpinBox;
port_spin_box->setMinimum (1);
port_spin_box->setMaximum (std::numeric_limits<port_type>::max ());
auto group_box_layout = new QFormLayout;
group_box_layout->addRow (tr ("Port number:"), port_spin_box);
group_box_layout->addRow (tr ("Multicast Group (blank for unicast server):"), multicast_group_line_edit_);
auto group_box = new QGroupBox {tr ("Server Details")};
group_box->setLayout (group_box_layout);
central_layout->addWidget (group_box);
log_table_view_->setModel (log_);
log_table_view_->verticalHeader ()->hide ();
central_layout->addWidget (log_table_view_);
// central widget
auto central_widget = new QWidget;
central_widget->setLayout (central_layout);
// main window setup
setCentralWidget (central_widget);
setDockOptions (AnimatedDocks | AllowNestedDocks | AllowTabbedDocks);
setTabPosition (Qt::BottomDockWidgetArea, QTabWidget::North);
// connect up server
connect (server_, &MessageServer::error, [this] (QString const& message) {
QMessageBox::warning (this, tr ("Network Error"), message);
});
connect (server_, &MessageServer::client_opened, this, &MainWindow::add_client);
connect (server_, &MessageServer::client_closed, this, &MainWindow::remove_client);
connect (server_, &MessageServer::client_closed, decodes_model_, &DecodesModel::clear_decodes);
connect (server_, &MessageServer::decode, decodes_model_, &DecodesModel::add_decode);
connect (server_, &MessageServer::clear_decodes, decodes_model_, &DecodesModel::clear_decodes);
connect (decodes_model_, &DecodesModel::reply, server_, &MessageServer::reply);
// UI behaviour
connect (port_spin_box, static_cast<void (QSpinBox::*)(int)> (&QSpinBox::valueChanged)
, [this] (port_type port) {server_->start (port);});
connect (multicast_group_line_edit_, &QLineEdit::editingFinished, [this, port_spin_box] () {
server_->start (port_spin_box->value (), QHostAddress {multicast_group_line_edit_->text ()});
});
port_spin_box->setValue (2237); // start up in unicast mode
show ();
}
Q_SLOT void log_qso (QString const& /*id*/, QDateTime time, QString const& dx_call, QString const& dx_grid
, Frequency dial_frequency, QString const& mode, QString const& report_sent
, QString const& report_received, QString const& tx_power, QString const& comments
, QString const& name)
{
QList<QStandardItem *> row;
row << new QStandardItem {time.toString ("dd-MMM-yyyy hh:mm")}
<< new QStandardItem {dx_call}
<< new QStandardItem {dx_grid}
<< new QStandardItem {name}
<< new QStandardItem {Radio::frequency_MHz_string (dial_frequency)}
<< new QStandardItem {mode}
<< new QStandardItem {report_sent}
<< new QStandardItem {report_received}
<< new QStandardItem {tx_power}
<< new QStandardItem {comments};
log_->appendRow (row);
log_table_view_->resizeColumnsToContents ();
log_table_view_->horizontalHeader ()->setStretchLastSection (true);
log_table_view_->scrollToBottom ();
}
private:
void add_client (QString const& id)
{
auto dock = new ClientWidget {decodes_model_, id, this};
dock->setAttribute (Qt::WA_DeleteOnClose);
auto view_action = dock->toggleViewAction ();
view_action->setEnabled (true);
view_menu_->addAction (view_action);
addDockWidget (Qt::BottomDockWidgetArea, dock);
connect (server_, &MessageServer::status_update, dock, &ClientWidget::update_status);
connect (server_, &MessageServer::decode, dock, &ClientWidget::decode_added);
connect (dock, &ClientWidget::do_reply, decodes_model_, &DecodesModel::do_reply);
connect (dock, &ClientWidget::do_halt_tx, server_, &MessageServer::halt_tx);
connect (dock, &ClientWidget::do_free_text, server_, &MessageServer::free_text);
connect (view_action, &QAction::toggled, dock, &ClientWidget::setVisible);
dock_widgets_[id] = dock;
server_->replay (id);
}
void remove_client (QString const& id)
{
auto iter = dock_widgets_.find (id);
if (iter != std::end (dock_widgets_))
{
(*iter)->close ();
dock_widgets_.erase (iter);
}
}
QStandardItemModel * log_;
QMenu * view_menu_;
DecodesModel * decodes_model_;
MessageServer * server_;
QLineEdit * multicast_group_line_edit_;
QTableView * log_table_view_;
// maps client id to widgets
QHash<QString, ClientWidget *> dock_widgets_;
};
#include "MessageAggregator.moc"
int main (int argc, char * argv[])
{
QApplication app {argc, argv};
try
{
QObject::connect (&app, SIGNAL (lastWindowClosed ()), &app, SLOT (quit ()));
app.setApplicationName ("WSJT-X Reference UDP Message Aggregator Server");
app.setApplicationVersion ("1.0");
{
QFile file {":/qss/default.qss"};
if (!file.open (QFile::ReadOnly))
{
throw_qstring ("failed to open \"" + file.fileName () + "\": " + file.errorString ());
}
app.setStyleSheet (file.readAll());
}
MainWindow window;
return app.exec ();
}
catch (std::exception const & e)
{
QMessageBox::critical (nullptr, app.applicationName (), e.what ());
std:: cerr << "Error: " << e.what () << '\n';
}
catch (...)
{
QMessageBox::critical (nullptr, app.applicationName (), QObject::tr ("Unexpected error"));
std:: cerr << "Unexpected error\n";
}
return -1;
}