Merge branch 'feat-outgoing-udp-interface' into develop

This commit is contained in:
Bill Somerville 2020-11-10 20:09:48 +00:00
commit de0af5a2ed
No known key found for this signature in database
GPG Key ID: D864B06D1E81618F
27 changed files with 1187 additions and 407 deletions

View File

@ -241,6 +241,7 @@ set (wsjt_qt_CXXSRCS
logbook/Multiplier.cpp logbook/Multiplier.cpp
Network/NetworkAccessManager.cpp Network/NetworkAccessManager.cpp
widgets/LazyFillComboBox.cpp widgets/LazyFillComboBox.cpp
widgets/CheckableItemComboBox.cpp
) )
set (wsjt_qtmm_CXXSRCS set (wsjt_qtmm_CXXSRCS
@ -1490,7 +1491,7 @@ add_executable (message_aggregator
${message_aggregator_RESOURCES_RCC} ${message_aggregator_RESOURCES_RCC}
${message_aggregator_VERSION_RESOURCES} ${message_aggregator_VERSION_RESOURCES}
) )
target_link_libraries (message_aggregator Qt5::Widgets wsjtx_udp-static) target_link_libraries (message_aggregator wsjt_qt Qt5::Widgets wsjtx_udp-static)
if (WSJT_CREATE_WINMAIN) if (WSJT_CREATE_WINMAIN)
set_target_properties (message_aggregator PROPERTIES WIN32_EXECUTABLE ON) set_target_properties (message_aggregator PROPERTIES WIN32_EXECUTABLE ON)

View File

@ -163,6 +163,10 @@
#include <QFontDialog> #include <QFontDialog>
#include <QSerialPortInfo> #include <QSerialPortInfo>
#include <QScopedPointer> #include <QScopedPointer>
#include <QNetworkInterface>
#include <QHostInfo>
#include <QHostAddress>
#include <QStandardItem>
#include <QDebug> #include <QDebug>
#include "pimpl_impl.hpp" #include "pimpl_impl.hpp"
@ -439,6 +443,12 @@ private:
void load_audio_devices (QAudio::Mode, QComboBox *, QAudioDeviceInfo *); void load_audio_devices (QAudio::Mode, QComboBox *, QAudioDeviceInfo *);
void update_audio_channels (QComboBox const *, int, QComboBox *, bool); void update_audio_channels (QComboBox const *, int, QComboBox *, bool);
void load_network_interfaces (CheckableItemComboBox *, QStringList current);
Q_SLOT void validate_network_interfaces (QString const&);
QStringList get_selected_network_interfaces (CheckableItemComboBox *);
Q_SLOT void host_info_results (QHostInfo);
void check_multicast (QHostAddress const&);
void find_tab (QWidget *); void find_tab (QWidget *);
void initialize_models (); void initialize_models ();
@ -492,6 +502,8 @@ private:
Q_SLOT void on_add_macro_line_edit_editingFinished (); Q_SLOT void on_add_macro_line_edit_editingFinished ();
Q_SLOT void delete_macro (); Q_SLOT void delete_macro ();
void delete_selected_macros (QModelIndexList); void delete_selected_macros (QModelIndexList);
Q_SLOT void on_udp_server_line_edit_textChanged (QString const&);
Q_SLOT void on_udp_server_line_edit_editingFinished ();
Q_SLOT void on_save_path_select_push_button_clicked (bool); Q_SLOT void on_save_path_select_push_button_clicked (bool);
Q_SLOT void on_azel_path_select_push_button_clicked (bool); Q_SLOT void on_azel_path_select_push_button_clicked (bool);
Q_SLOT void on_calibration_intercept_spin_box_valueChanged (double); Q_SLOT void on_calibration_intercept_spin_box_valueChanged (double);
@ -641,7 +653,12 @@ private:
bool use_dynamic_grid_; bool use_dynamic_grid_;
QString opCall_; QString opCall_;
QString udp_server_name_; QString udp_server_name_;
bool udp_server_name_edited_;
int dns_lookup_id_;
port_type udp_server_port_; port_type udp_server_port_;
QStringList udp_interface_names_;
QString loopback_interface_name_;
int udp_TTL_;
QString n1mm_server_name_; QString n1mm_server_name_;
port_type n1mm_server_port_; port_type n1mm_server_port_;
bool broadcast_to_n1mm_; bool broadcast_to_n1mm_;
@ -741,6 +758,8 @@ QString Configuration::opCall() const {return m_->opCall_;}
void Configuration::opCall (QString const& call) {m_->opCall_ = call;} void Configuration::opCall (QString const& call) {m_->opCall_ = call;}
QString Configuration::udp_server_name () const {return m_->udp_server_name_;} QString Configuration::udp_server_name () const {return m_->udp_server_name_;}
auto Configuration::udp_server_port () const -> port_type {return m_->udp_server_port_;} auto Configuration::udp_server_port () const -> port_type {return m_->udp_server_port_;}
QStringList Configuration::udp_interface_names () const {return m_->udp_interface_names_;}
int Configuration::udp_TTL () const {return m_->udp_TTL_;}
bool Configuration::accept_udp_requests () const {return m_->accept_udp_requests_;} bool Configuration::accept_udp_requests () const {return m_->accept_udp_requests_;}
QString Configuration::n1mm_server_name () const {return m_->n1mm_server_name_;} QString Configuration::n1mm_server_name () const {return m_->n1mm_server_name_;}
auto Configuration::n1mm_server_port () const -> port_type {return m_->n1mm_server_port_;} auto Configuration::n1mm_server_port () const -> port_type {return m_->n1mm_server_port_;}
@ -995,6 +1014,8 @@ Configuration::impl::impl (Configuration * self, QNetworkAccessManager * network
, transceiver_command_number_ {0} , transceiver_command_number_ {0}
, degrade_ {0.} // initialize to zero each run, not , degrade_ {0.} // initialize to zero each run, not
// saved in settings // saved in settings
, udp_server_name_edited_ {false}
, dns_lookup_id_ {-1}
{ {
ui_->setupUi (this); ui_->setupUi (this);
@ -1044,6 +1065,7 @@ Configuration::impl::impl (Configuration * self, QNetworkAccessManager * network
// this must be done after the default paths above are set // this must be done after the default paths above are set
read_settings (); read_settings ();
// set up dynamic loading of audio devices
connect (ui_->sound_input_combo_box, &LazyFillComboBox::about_to_show_popup, [this] () { connect (ui_->sound_input_combo_box, &LazyFillComboBox::about_to_show_popup, [this] () {
QGuiApplication::setOverrideCursor (QCursor {Qt::WaitCursor}); QGuiApplication::setOverrideCursor (QCursor {Qt::WaitCursor});
load_audio_devices (QAudio::AudioInput, ui_->sound_input_combo_box, &next_audio_input_device_); load_audio_devices (QAudio::AudioInput, ui_->sound_input_combo_box, &next_audio_input_device_);
@ -1059,6 +1081,14 @@ Configuration::impl::impl (Configuration * self, QNetworkAccessManager * network
QGuiApplication::restoreOverrideCursor (); QGuiApplication::restoreOverrideCursor ();
}); });
// set up dynamic loading of network interfaces
connect (ui_->udp_interfaces_combo_box, &LazyFillComboBox::about_to_show_popup, [this] () {
QGuiApplication::setOverrideCursor (QCursor {Qt::WaitCursor});
load_network_interfaces (ui_->udp_interfaces_combo_box, udp_interface_names_);
QGuiApplication::restoreOverrideCursor ();
});
connect (ui_->udp_interfaces_combo_box, &QComboBox::currentTextChanged, this, &Configuration::impl::validate_network_interfaces);
// set up LoTW users CSV file fetching // set up LoTW users CSV file fetching
connect (&lotw_users_, &LotWUsers::load_finished, [this] () { connect (&lotw_users_, &LotWUsers::load_finished, [this] () {
ui_->LotW_CSV_fetch_push_button->setEnabled (true); ui_->LotW_CSV_fetch_push_button->setEnabled (true);
@ -1335,7 +1365,14 @@ void Configuration::impl::initialize_models ()
ui_->CAT_poll_interval_spin_box->setValue (rig_params_.poll_interval); ui_->CAT_poll_interval_spin_box->setValue (rig_params_.poll_interval);
ui_->opCallEntry->setText (opCall_); ui_->opCallEntry->setText (opCall_);
ui_->udp_server_line_edit->setText (udp_server_name_); ui_->udp_server_line_edit->setText (udp_server_name_);
on_udp_server_line_edit_editingFinished ();
ui_->udp_server_port_spin_box->setValue (udp_server_port_); ui_->udp_server_port_spin_box->setValue (udp_server_port_);
load_network_interfaces (ui_->udp_interfaces_combo_box, udp_interface_names_);
if (!udp_interface_names_.size ())
{
udp_interface_names_ = get_selected_network_interfaces (ui_->udp_interfaces_combo_box);
}
ui_->udp_TTL_spin_box->setValue (udp_TTL_);
ui_->accept_udp_requests_check_box->setChecked (accept_udp_requests_); ui_->accept_udp_requests_check_box->setChecked (accept_udp_requests_);
ui_->n1mm_server_name_line_edit->setText (n1mm_server_name_); ui_->n1mm_server_name_line_edit->setText (n1mm_server_name_);
ui_->n1mm_server_port_spin_box->setValue (n1mm_server_port_); ui_->n1mm_server_port_spin_box->setValue (n1mm_server_port_);
@ -1513,6 +1550,8 @@ void Configuration::impl::read_settings ()
rig_params_.split_mode = settings_->value ("SplitMode", QVariant::fromValue (TransceiverFactory::split_mode_none)).value<TransceiverFactory::SplitMode> (); rig_params_.split_mode = settings_->value ("SplitMode", QVariant::fromValue (TransceiverFactory::split_mode_none)).value<TransceiverFactory::SplitMode> ();
opCall_ = settings_->value ("OpCall", "").toString (); opCall_ = settings_->value ("OpCall", "").toString ();
udp_server_name_ = settings_->value ("UDPServer", "127.0.0.1").toString (); udp_server_name_ = settings_->value ("UDPServer", "127.0.0.1").toString ();
udp_interface_names_ = settings_->value ("UDPInterface").toStringList ();
udp_TTL_ = settings_->value ("UDPTTL", 1).toInt ();
udp_server_port_ = settings_->value ("UDPServerPort", 2237).toUInt (); udp_server_port_ = settings_->value ("UDPServerPort", 2237).toUInt ();
n1mm_server_name_ = settings_->value ("N1MMServer", "127.0.0.1").toString (); n1mm_server_name_ = settings_->value ("N1MMServer", "127.0.0.1").toString ();
n1mm_server_port_ = settings_->value ("N1MMServerPort", 2333).toUInt (); n1mm_server_port_ = settings_->value ("N1MMServerPort", 2333).toUInt ();
@ -1641,6 +1680,8 @@ void Configuration::impl::write_settings ()
settings_->setValue ("OpCall", opCall_); settings_->setValue ("OpCall", opCall_);
settings_->setValue ("UDPServer", udp_server_name_); settings_->setValue ("UDPServer", udp_server_name_);
settings_->setValue ("UDPServerPort", udp_server_port_); settings_->setValue ("UDPServerPort", udp_server_port_);
settings_->setValue ("UDPInterface", QVariant::fromValue (udp_interface_names_));
settings_->setValue ("UDPTTL", udp_TTL_);
settings_->setValue ("N1MMServer", n1mm_server_name_); settings_->setValue ("N1MMServer", n1mm_server_name_);
settings_->setValue ("N1MMServerPort", n1mm_server_port_); settings_->setValue ("N1MMServerPort", n1mm_server_port_);
settings_->setValue ("BroadcastToN1MM", broadcast_to_n1mm_); settings_->setValue ("BroadcastToN1MM", broadcast_to_n1mm_);
@ -1843,6 +1884,12 @@ bool Configuration::impl::validate ()
return false; return false;
} }
if (dns_lookup_id_ > -1)
{
MessageBox::information_message (this, tr ("Pending DNS lookup, please try again later"));
return false;
}
return true; return true;
} }
@ -2061,20 +2108,30 @@ void Configuration::impl::accept ()
pwrBandTxMemory_ = ui_->checkBoxPwrBandTxMemory->isChecked (); pwrBandTxMemory_ = ui_->checkBoxPwrBandTxMemory->isChecked ();
pwrBandTuneMemory_ = ui_->checkBoxPwrBandTuneMemory->isChecked (); pwrBandTuneMemory_ = ui_->checkBoxPwrBandTuneMemory->isChecked ();
opCall_=ui_->opCallEntry->text(); opCall_=ui_->opCallEntry->text();
auto new_server = ui_->udp_server_line_edit->text ();
if (new_server != udp_server_name_) auto new_server = ui_->udp_server_line_edit->text ().trimmed ();
auto new_interfaces = get_selected_network_interfaces (ui_->udp_interfaces_combo_box);
if (new_server != udp_server_name_ || new_interfaces != udp_interface_names_)
{ {
udp_server_name_ = new_server; udp_server_name_ = new_server;
Q_EMIT self_->udp_server_changed (new_server); udp_interface_names_ = new_interfaces;
Q_EMIT self_->udp_server_changed (udp_server_name_, udp_interface_names_);
} }
auto new_port = ui_->udp_server_port_spin_box->value (); auto new_port = ui_->udp_server_port_spin_box->value ();
if (new_port != udp_server_port_) if (new_port != udp_server_port_)
{ {
udp_server_port_ = new_port; udp_server_port_ = new_port;
Q_EMIT self_->udp_server_port_changed (new_port); Q_EMIT self_->udp_server_port_changed (udp_server_port_);
} }
auto new_TTL = ui_->udp_TTL_spin_box->value ();
if (new_TTL != udp_TTL_)
{
udp_TTL_ = new_TTL;
Q_EMIT self_->udp_TTL_changed (udp_TTL_);
}
if (ui_->accept_udp_requests_check_box->isChecked () != accept_udp_requests_) if (ui_->accept_udp_requests_check_box->isChecked () != accept_udp_requests_)
{ {
accept_udp_requests_ = ui_->accept_udp_requests_check_box->isChecked (); accept_udp_requests_ = ui_->accept_udp_requests_check_box->isChecked ();
@ -2130,6 +2187,12 @@ void Configuration::impl::accept ()
void Configuration::impl::reject () void Configuration::impl::reject ()
{ {
if (dns_lookup_id_ > -1)
{
QHostInfo::abortHostLookup (dns_lookup_id_);
dns_lookup_id_ = -1;
}
initialize_models (); // reverts to settings as at exec () initialize_models (); // reverts to settings as at exec ()
// check if the Transceiver instance changed, in which case we need // check if the Transceiver instance changed, in which case we need
@ -2344,6 +2407,72 @@ void Configuration::impl::on_add_macro_push_button_clicked (bool /* checked */)
} }
} }
void Configuration::impl::on_udp_server_line_edit_textChanged (QString const&)
{
udp_server_name_edited_ = true;
}
void Configuration::impl::on_udp_server_line_edit_editingFinished ()
{
if (udp_server_name_edited_)
{
auto const& server = ui_->udp_server_line_edit->text ().trimmed ();
QHostAddress ha {server};
if (server.size () && ha.isNull ())
{
// queue a host address lookup
// qDebug () << "server host DNS lookup:" << server;
#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
dns_lookup_id_ = QHostInfo::lookupHost (server, this, &Configuration::impl::host_info_results);
#else
dns_lookup_id_ = QHostInfo::lookupHost (server, this, SLOT (host_info_results (QHostInfo)));
#endif
}
else
{
check_multicast (ha);
}
}
}
void Configuration::impl::host_info_results (QHostInfo host_info)
{
if (host_info.lookupId () != dns_lookup_id_) return;
dns_lookup_id_ = -1;
if (QHostInfo::NoError != host_info.error ())
{
MessageBox::critical_message (this, tr ("UDP server DNS lookup failed"), host_info.errorString ());
}
else
{
auto const& server_addresses = host_info.addresses ();
// qDebug () << "message server addresses:" << server_addresses;
if (server_addresses.size ())
{
check_multicast (server_addresses[0]);
}
}
}
void Configuration::impl::check_multicast (QHostAddress const& ha)
{
auto is_multicast = is_multicast_address (ha);
ui_->udp_interfaces_label->setVisible (is_multicast);
ui_->udp_interfaces_combo_box->setVisible (is_multicast);
ui_->udp_TTL_label->setVisible (is_multicast);
ui_->udp_TTL_spin_box->setVisible (is_multicast);
if (isVisible ())
{
if (is_MAC_ambiguous_multicast_address (ha))
{
MessageBox::warning_message (this, tr ("MAC-ambiguous multicast groups addresses not supported"));
find_tab (ui_->udp_server_line_edit);
ui_->udp_server_line_edit->clear ();
}
}
udp_server_name_edited_ = false;
}
void Configuration::impl::delete_frequencies () void Configuration::impl::delete_frequencies ()
{ {
auto selection_model = ui_->frequencies_table_view->selectionModel (); auto selection_model = ui_->frequencies_table_view->selectionModel ();
@ -2868,6 +2997,85 @@ void Configuration::impl::load_audio_devices (QAudio::Mode mode, QComboBox * com
combo_box->setCurrentIndex (current_index); combo_box->setCurrentIndex (current_index);
} }
// load the available network interfaces into the selection combo box
void Configuration::impl::load_network_interfaces (CheckableItemComboBox * combo_box, QStringList current)
{
combo_box->clear ();
for (auto const& net_if : QNetworkInterface::allInterfaces ())
{
auto flags = QNetworkInterface::IsUp | QNetworkInterface::CanMulticast;
if ((net_if.flags () & flags) == flags)
{
if (net_if.flags () & QNetworkInterface::IsLoopBack)
{
loopback_interface_name_ = net_if.name ();
}
auto item = combo_box->addCheckItem (net_if.humanReadableName ()
, net_if.name ()
, current.contains (net_if.name ()) ? Qt::Checked : Qt::Unchecked);
auto tip = QString {"name(index): %1(%2) - %3"}.arg (net_if.name ()).arg (net_if.index ())
.arg (net_if.flags () & QNetworkInterface::IsUp ? "Up" : "Down");
auto hw_addr = net_if.hardwareAddress ();
if (hw_addr.size ())
{
tip += QString {"\nhw: %1"}.arg (net_if.hardwareAddress ());
}
auto aes = net_if.addressEntries ();
if (aes.size ())
{
tip += "\naddresses:";
for (auto const& ae : aes)
{
tip += QString {"\n ip: %1/%2"}.arg (ae.ip ().toString ()).arg (ae.prefixLength ());
}
}
item->setToolTip (tip);
}
}
}
// get the select network interfaces from the selection combo box
void Configuration::impl::validate_network_interfaces (QString const& /*text*/)
{
auto model = static_cast<QStandardItemModel *> (ui_->udp_interfaces_combo_box->model ());
bool has_checked {false};
int loopback_row {-1};
for (int row = 0; row < model->rowCount (); ++row)
{
if (model->item (row)->data ().toString () == loopback_interface_name_)
{
loopback_row = row;
}
else if (Qt::Checked == model->item (row)->checkState ())
{
has_checked = true;
}
}
if (loopback_row >= 0)
{
if (!has_checked)
{
model->item (loopback_row)->setCheckState (Qt::Checked);
}
model->item (loopback_row)->setEnabled (has_checked);
}
}
// get the select network interfaces from the selection combo box
QStringList Configuration::impl::get_selected_network_interfaces (CheckableItemComboBox * combo_box)
{
QStringList interfaces;
auto model = static_cast<QStandardItemModel *> (combo_box->model ());
for (int row = 0; row < model->rowCount (); ++row)
{
if (Qt::Checked == model->item (row)->checkState ())
{
interfaces << model->item (row)->data ().toString ();
}
}
return interfaces;
}
// enable only the channels that are supported by the selected audio device // enable only the channels that are supported by the selected audio device
void Configuration::impl::update_audio_channels (QComboBox const * source_combo_box, int index, QComboBox * combo_box, bool allow_both) void Configuration::impl::update_audio_channels (QComboBox const * source_combo_box, int index, QComboBox * combo_box, bool allow_both)
{ {

View File

@ -3,6 +3,7 @@
#include <QObject> #include <QObject>
#include <QFont> #include <QFont>
#include <QString>
#include "Radio.hpp" #include "Radio.hpp"
#include "models/IARURegions.hpp" #include "models/IARURegions.hpp"
@ -14,14 +15,12 @@
class QSettings; class QSettings;
class QWidget; class QWidget;
class QAudioDeviceInfo; class QAudioDeviceInfo;
class QString;
class QDir; class QDir;
class QNetworkAccessManager; class QNetworkAccessManager;
class Bands; class Bands;
class FrequencyList_v2; class FrequencyList_v2;
class StationList; class StationList;
class QStringListModel; class QStringListModel;
class QHostAddress;
class LotWUsers; class LotWUsers;
class DecodeHighlightingModel; class DecodeHighlightingModel;
class LogBook; class LogBook;
@ -152,6 +151,8 @@ public:
void opCall (QString const&); void opCall (QString const&);
QString udp_server_name () const; QString udp_server_name () const;
port_type udp_server_port () const; port_type udp_server_port () const;
QStringList udp_interface_names () const;
int udp_TTL () const;
QString n1mm_server_name () const; QString n1mm_server_name () const;
port_type n1mm_server_port () const; port_type n1mm_server_port () const;
bool valid_n1mm_info () const; bool valid_n1mm_info () const;
@ -273,8 +274,9 @@ public:
// //
// This signal is emitted when the UDP server changes // This signal is emitted when the UDP server changes
// //
Q_SIGNAL void udp_server_changed (QString const& udp_server) const; Q_SIGNAL void udp_server_changed (QString& udp_server_name, QStringList const& network_interfaces) const;
Q_SIGNAL void udp_server_port_changed (port_type server_port) const; Q_SIGNAL void udp_server_port_changed (port_type server_port) const;
Q_SIGNAL void udp_TTL_changed (int TTL) const;
Q_SIGNAL void accept_udp_requests_changed (bool checked) const; Q_SIGNAL void accept_udp_requests_changed (bool checked) const;
// signal updates to decode highlighting // signal updates to decode highlighting

View File

@ -7,7 +7,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>554</width> <width>554</width>
<height>556</height> <height>560</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@ -1864,12 +1864,6 @@ and DX Grid fields when a 73 or free text message is sent.</string>
</item> </item>
<item row="0" column="1"> <item row="0" column="1">
<widget class="QLineEdit" name="udp_server_line_edit"> <widget class="QLineEdit" name="udp_server_line_edit">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip"> <property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Optional hostname of network service to receive decodes.&lt;/p&gt;&lt;p&gt;Formats:&lt;/p&gt;&lt;ul style=&quot;margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;&quot;&gt;&lt;li style=&quot; margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;hostname&lt;/li&gt;&lt;li style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;IPv4 address&lt;/li&gt;&lt;li style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;IPv6 address&lt;/li&gt;&lt;li style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;IPv4 multicast group address&lt;/li&gt;&lt;li style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;IPv6 multicast group address&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Clearing this field will disable the broadcasting of UDP status updates.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Optional hostname of network service to receive decodes.&lt;/p&gt;&lt;p&gt;Formats:&lt;/p&gt;&lt;ul style=&quot;margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;&quot;&gt;&lt;li style=&quot; margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;hostname&lt;/li&gt;&lt;li style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;IPv4 address&lt;/li&gt;&lt;li style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;IPv6 address&lt;/li&gt;&lt;li style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;IPv4 multicast group address&lt;/li&gt;&lt;li style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;IPv6 multicast group address&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Clearing this field will disable the broadcasting of UDP status updates.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property> </property>
@ -1891,13 +1885,53 @@ and DX Grid fields when a 73 or free text message is sent.</string>
<item row="1" column="1"> <item row="1" column="1">
<widget class="QSpinBox" name="udp_server_port_spin_box"> <widget class="QSpinBox" name="udp_server_port_spin_box">
<property name="toolTip"> <property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enter the service port number of the UDP server that WSJT-X should send updates to. If this is zero no updates will be broadcast.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enter the service port number of the UDP server that WSJT-X should send updates to. If this is zero no updates will be sent.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property> </property>
<property name="maximum"> <property name="maximum">
<number>65534</number> <number>65534</number>
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="0">
<widget class="QLabel" name="udp_interfaces_label">
<property name="text">
<string>Outgoing interfaces:</string>
</property>
<property name="buddy">
<cstring>udp_interfaces_combo_box</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="CheckableItemComboBox" name="udp_interfaces_combo_box">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;When sending updates to a multicast group address it is necessary to specify which network interface(s) to send them to. If the loop-back interface is multicast capable then at least that one will be selected.&lt;/p&gt;&lt;p&gt;For most users the loop-back interface is all that is needed, that will allow multiple other applications on the same machine to interoperate with WSJT-X. If applications running on other hosts are to receive status updates then a suitable network interface should be used.&lt;/p&gt;&lt;p&gt;On some Linux systems it may be necessary to enable multicast on the loop-back network interface.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QSpinBox" name="udp_TTL_spin_box">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Sets the number or router hops that multicast datagrams are allowed to make. Almost everyone should set this to 1 to keep outgoing multicast traffic withn the local subnet.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="maximum">
<number>255</number>
</property>
<property name="value">
<number>1</number>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="udp_TTL_label">
<property name="text">
<string>Multicast TTL:</string>
</property>
<property name="buddy">
<cstring>udp_TTL_spin_box</cstring>
</property>
</widget>
</item>
</layout> </layout>
</item> </item>
<item row="0" column="1"> <item row="0" column="1">
@ -3002,6 +3036,11 @@ Right click for insert and delete options.</string>
<extends>QComboBox</extends> <extends>QComboBox</extends>
<header>widgets/LazyFillComboBox.hpp</header> <header>widgets/LazyFillComboBox.hpp</header>
</customwidget> </customwidget>
<customwidget>
<class>CheckableItemComboBox</class>
<extends>QComboBox</extends>
<header>widgets/CheckableItemComboBox.hpp</header>
</customwidget>
</customwidgets> </customwidgets>
<tabstops> <tabstops>
<tabstop>configuration_tabs</tabstop> <tabstop>configuration_tabs</tabstop>
@ -3084,6 +3123,8 @@ Right click for insert and delete options.</string>
<tabstop>psk_reporter_tcpip_check_box</tabstop> <tabstop>psk_reporter_tcpip_check_box</tabstop>
<tabstop>udp_server_line_edit</tabstop> <tabstop>udp_server_line_edit</tabstop>
<tabstop>udp_server_port_spin_box</tabstop> <tabstop>udp_server_port_spin_box</tabstop>
<tabstop>udp_interfaces_combo_box</tabstop>
<tabstop>udp_TTL_spin_box</tabstop>
<tabstop>accept_udp_requests_check_box</tabstop> <tabstop>accept_udp_requests_check_box</tabstop>
<tabstop>udpWindowToFront</tabstop> <tabstop>udpWindowToFront</tabstop>
<tabstop>udpWindowRestore</tabstop> <tabstop>udpWindowRestore</tabstop>
@ -3101,8 +3142,8 @@ Right click for insert and delete options.</string>
<tabstop>include_WAE_check_box</tabstop> <tabstop>include_WAE_check_box</tabstop>
<tabstop>rescan_log_push_button</tabstop> <tabstop>rescan_log_push_button</tabstop>
<tabstop>LotW_CSV_URL_line_edit</tabstop> <tabstop>LotW_CSV_URL_line_edit</tabstop>
<tabstop>LotW_days_since_upload_spin_box</tabstop>
<tabstop>LotW_CSV_fetch_push_button</tabstop> <tabstop>LotW_CSV_fetch_push_button</tabstop>
<tabstop>LotW_days_since_upload_spin_box</tabstop>
<tabstop>sbNtrials</tabstop> <tabstop>sbNtrials</tabstop>
<tabstop>sbAggressive</tabstop> <tabstop>sbAggressive</tabstop>
<tabstop>cbTwoPass</tabstop> <tabstop>cbTwoPass</tabstop>
@ -3118,11 +3159,11 @@ Right click for insert and delete options.</string>
<tabstop>rbHound</tabstop> <tabstop>rbHound</tabstop>
<tabstop>rbNA_VHF_Contest</tabstop> <tabstop>rbNA_VHF_Contest</tabstop>
<tabstop>rbField_Day</tabstop> <tabstop>rbField_Day</tabstop>
<tabstop>Field_Day_Exchange</tabstop>
<tabstop>rbEU_VHF_Contest</tabstop> <tabstop>rbEU_VHF_Contest</tabstop>
<tabstop>rbRTTY_Roundup</tabstop> <tabstop>rbRTTY_Roundup</tabstop>
<tabstop>RTTY_Exchange</tabstop>
<tabstop>rbWW_DIGI</tabstop> <tabstop>rbWW_DIGI</tabstop>
<tabstop>Field_Day_Exchange</tabstop>
<tabstop>RTTY_Exchange</tabstop>
</tabstops> </tabstops>
<resources/> <resources/>
<connections> <connections>
@ -3192,13 +3233,13 @@ Right click for insert and delete options.</string>
</connection> </connection>
</connections> </connections>
<buttongroups> <buttongroups>
<buttongroup name="PTT_method_button_group"/>
<buttongroup name="CAT_data_bits_button_group"/>
<buttongroup name="special_op_activity_button_group"/>
<buttongroup name="CAT_stop_bits_button_group"/>
<buttongroup name="TX_audio_source_button_group"/> <buttongroup name="TX_audio_source_button_group"/>
<buttongroup name="split_mode_button_group"/> <buttongroup name="CAT_stop_bits_button_group"/>
<buttongroup name="TX_mode_button_group"/>
<buttongroup name="CAT_handshake_button_group"/> <buttongroup name="CAT_handshake_button_group"/>
<buttongroup name="CAT_data_bits_button_group"/>
<buttongroup name="TX_mode_button_group"/>
<buttongroup name="special_op_activity_button_group"/>
<buttongroup name="PTT_method_button_group"/>
<buttongroup name="split_mode_button_group"/>
</buttongroups> </buttongroups>
</ui> </ui>

View File

@ -6,16 +6,16 @@
#include <limits> #include <limits>
#include <QUdpSocket> #include <QUdpSocket>
#include <QNetworkInterface>
#include <QHostInfo> #include <QHostInfo>
#include <QTimer> #include <QTimer>
#include <QQueue> #include <QQueue>
#include <QByteArray> #include <QByteArray>
#include <QHostAddress>
#include <QColor> #include <QColor>
#include <QDebug> #include <QDebug>
#include "NetworkMessage.hpp" #include "NetworkMessage.hpp"
#include "qt_helpers.hpp"
#include "pimpl_impl.hpp" #include "pimpl_impl.hpp"
#include "moc_MessageClient.cpp" #include "moc_MessageClient.cpp"
@ -34,14 +34,15 @@ class MessageClient::impl
public: public:
impl (QString const& id, QString const& version, QString const& revision, impl (QString const& id, QString const& version, QString const& revision,
port_type server_port, MessageClient * self) port_type server_port, int TTL, MessageClient * self)
: self_ {self} : self_ {self}
, dns_lookup_id_ {0}
, enabled_ {false} , enabled_ {false}
, id_ {id} , id_ {id}
, version_ {version} , version_ {version}
, revision_ {revision} , revision_ {revision}
, dns_lookup_id_ {-1}
, server_port_ {server_port} , server_port_ {server_port}
, TTL_ {TTL}
, schema_ {2} // use 2 prior to negotiation not 1 which is broken , schema_ {2} // use 2 prior to negotiation not 1 which is broken
, heartbeat_timer_ {new QTimer {this}} , heartbeat_timer_ {new QTimer {this}}
{ {
@ -49,9 +50,6 @@ public:
connect (this, &QIODevice::readyRead, this, &impl::pending_datagrams); connect (this, &QIODevice::readyRead, this, &impl::pending_datagrams);
heartbeat_timer_->start (NetworkMessage::pulse * 1000); heartbeat_timer_->start (NetworkMessage::pulse * 1000);
// bind to an ephemeral port
bind ();
} }
~impl () ~impl ()
@ -61,35 +59,37 @@ public:
enum StreamStatus {Fail, Short, OK}; enum StreamStatus {Fail, Short, OK};
void parse_message (QByteArray const& msg); void set_server (QString const& server_name, QStringList const& network_interface_names);
Q_SLOT void host_info_results (QHostInfo);
void start ();
void parse_message (QByteArray const&);
void pending_datagrams (); void pending_datagrams ();
void heartbeat (); void heartbeat ();
void closedown (); void closedown ();
StreamStatus check_status (QDataStream const&) const; StreamStatus check_status (QDataStream const&) const;
void send_message (QByteArray const&); void send_message (QByteArray const&, bool queue_if_pending = true);
void send_message (QDataStream const& out, QByteArray const& message) void send_message (QDataStream const& out, QByteArray const& message, bool queue_if_pending = true)
{ {
if (OK == check_status (out)) if (OK == check_status (out))
{ {
send_message (message); send_message (message, queue_if_pending);
} }
else else
{ {
Q_EMIT self_->error ("Error creating UDP message"); Q_EMIT self_->error ("Error creating UDP message");
} }
} }
Q_SLOT void host_info_results (QHostInfo);
MessageClient * self_; MessageClient * self_;
int dns_lookup_id_;
bool enabled_; bool enabled_;
QString id_; QString id_;
QString version_; QString version_;
QString revision_; QString revision_;
QString server_string_; int dns_lookup_id_;
port_type server_port_;
QHostAddress server_; QHostAddress server_;
port_type server_port_;
int TTL_;
std::vector<QNetworkInterface> network_interfaces_;
quint32 schema_; quint32 schema_;
QTimer * heartbeat_timer_; QTimer * heartbeat_timer_;
std::vector<QHostAddress> blocked_addresses_; std::vector<QHostAddress> blocked_addresses_;
@ -101,36 +101,99 @@ public:
#include "MessageClient.moc" #include "MessageClient.moc"
void MessageClient::impl::set_server (QString const& server_name, QStringList const& network_interface_names)
{
// qDebug () << "MessageClient server:" << server_name << "port:" << server_port_ << "interfaces:" << network_interface_names;
server_.setAddress (server_name);
network_interfaces_.clear ();
for (auto const& net_if_name : network_interface_names)
{
network_interfaces_.push_back (QNetworkInterface::interfaceFromName (net_if_name));
}
if (server_.isNull () && server_name.size ()) // DNS lookup required
{
// queue a host address lookup
#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
dns_lookup_id_ = QHostInfo::lookupHost (server_name, this, &MessageClient::impl::host_info_results);
#else
dns_lookup_id_ = QHostInfo::lookupHost (server_name, this, SLOT (host_info_results (QHostInfo)));
#endif
}
else
{
start ();
}
}
void MessageClient::impl::host_info_results (QHostInfo host_info) void MessageClient::impl::host_info_results (QHostInfo host_info)
{ {
if (host_info.lookupId () != dns_lookup_id_) return; if (host_info.lookupId () != dns_lookup_id_) return;
dns_lookup_id_ = -1;
if (QHostInfo::NoError != host_info.error ()) if (QHostInfo::NoError != host_info.error ())
{ {
Q_EMIT self_->error ("UDP server lookup failed:\n" + host_info.errorString ()); Q_EMIT self_->error ("UDP server DNS lookup failed: " + host_info.errorString ());
pending_messages_.clear (); // discard
} }
else if (host_info.addresses ().size ()) else
{ {
auto server = host_info.addresses ()[0]; auto const& server_addresses = host_info.addresses ();
if (blocked_addresses_.end () == std::find (blocked_addresses_.begin (), blocked_addresses_.end (), server)) if (server_addresses.size ())
{ {
server_ = server; server_ = server_addresses[0];
TRACE_UDP ("resulting server:" << server);
// send initial heartbeat which allows schema negotiation
heartbeat ();
// clear any backlog
while (pending_messages_.size ())
{
send_message (pending_messages_.dequeue ());
}
} }
else }
start ();
}
void MessageClient::impl::start ()
{
if (server_.isNull ())
{
Q_EMIT self_->close ();
pending_messages_.clear (); // discard
return;
}
if (is_broadcast_address (server_))
{
Q_EMIT self_->error ("IPv4 broadcast not supported, please specify the loop-back address, a server host address, or multicast group address");
pending_messages_.clear (); // discard
return;
}
if (blocked_addresses_.end () != std::find (blocked_addresses_.begin (), blocked_addresses_.end (), server_))
{
Q_EMIT self_->error ("UDP server blocked, please try another");
pending_messages_.clear (); // discard
return;
}
TRACE_UDP ("Trying server:" << server_.toString ());
QHostAddress interface_addr {IPv6Protocol == server_.protocol () ? QHostAddress::AnyIPv6 : QHostAddress::AnyIPv4};
if (localAddress () != interface_addr)
{
if (UnconnectedState != state () || state ())
{ {
Q_EMIT self_->error ("UDP server blocked, please try another"); close ();
pending_messages_.clear (); // discard
} }
// bind to an ephemeral port on the selected interface and set
// up for sending datagrams
bind (interface_addr);
// qDebug () << "Bound to UDP port:" << localPort () << "on:" << localAddress ();
// set multicast TTL to limit scope when sending to multicast
// group addresses
setSocketOption (MulticastTtlOption, TTL_);
}
// send initial heartbeat which allows schema negotiation
heartbeat ();
// clear any backlog
while (pending_messages_.size ())
{
send_message (pending_messages_.dequeue (), false);
} }
} }
@ -358,14 +421,11 @@ void MessageClient::impl::heartbeat ()
if (server_port_ && !server_.isNull ()) if (server_port_ && !server_.isNull ())
{ {
QByteArray message; QByteArray message;
NetworkMessage::Builder hb {&message, NetworkMessage::Heartbeat, id_, schema_}; NetworkMessage::Builder out {&message, NetworkMessage::Heartbeat, id_, schema_};
hb << NetworkMessage::Builder::schema_number // maximum schema number accepted out << NetworkMessage::Builder::schema_number // maximum schema number accepted
<< version_.toUtf8 () << revision_.toUtf8 (); << version_.toUtf8 () << revision_.toUtf8 ();
if (OK == check_status (hb)) TRACE_UDP ("schema:" << schema_ << "max schema:" << NetworkMessage::Builder::schema_number << "version:" << version_ << "revision:" << revision_);
{ send_message (out, message, false);
TRACE_UDP ("schema:" << schema_ << "max schema:" << NetworkMessage::Builder::schema_number << "version:" << version_ << "revision:" << revision_);
writeDatagram (message, server_, server_port_);
}
} }
} }
@ -375,15 +435,12 @@ void MessageClient::impl::closedown ()
{ {
QByteArray message; QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::Close, id_, schema_}; NetworkMessage::Builder out {&message, NetworkMessage::Close, id_, schema_};
if (OK == check_status (out)) TRACE_UDP ("");
{ send_message (out, message, false);
TRACE_UDP ("");
writeDatagram (message, server_, server_port_);
}
} }
} }
void MessageClient::impl::send_message (QByteArray const& message) void MessageClient::impl::send_message (QByteArray const& message, bool queue_if_pending)
{ {
if (server_port_) if (server_port_)
{ {
@ -391,11 +448,25 @@ void MessageClient::impl::send_message (QByteArray const& message)
{ {
if (message != last_message_) // avoid duplicates if (message != last_message_) // avoid duplicates
{ {
writeDatagram (message, server_, server_port_); if (is_multicast_address (server_))
{
// send datagram on each selected network interface
std::for_each (network_interfaces_.begin (), network_interfaces_.end ()
, [&] (QNetworkInterface const& net_if) {
setMulticastInterface (net_if);
// qDebug () << "Multicast UDP datagram sent to:" << server_ << "port:" << server_port_ << "on:" << multicastInterface ().humanReadableName ();
writeDatagram (message, server_, server_port_);
});
}
else
{
// qDebug () << "Unicast UDP datagram sent to:" << server_ << "port:" << server_port_;
writeDatagram (message, server_, server_port_);
}
last_message_ = message; last_message_ = message;
} }
} }
else else if (queue_if_pending)
{ {
pending_messages_.enqueue (message); pending_messages_.enqueue (message);
} }
@ -428,9 +499,11 @@ auto MessageClient::impl::check_status (QDataStream const& stream) const -> Stre
} }
MessageClient::MessageClient (QString const& id, QString const& version, QString const& revision, MessageClient::MessageClient (QString const& id, QString const& version, QString const& revision,
QString const& server, port_type server_port, QObject * self) QString const& server_name, port_type server_port,
QStringList const& network_interface_names,
int TTL, QObject * self)
: QObject {self} : QObject {self}
, m_ {id, version, revision, server_port, this} , m_ {id, version, revision, server_port, TTL, this}
{ {
connect (&*m_ connect (&*m_
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
@ -449,8 +522,8 @@ MessageClient::MessageClient (QString const& id, QString const& version, QString
#endif #endif
Q_EMIT error (m_->errorString ()); Q_EMIT error (m_->errorString ());
} }
}); });
set_server (server); m_->set_server (server_name, network_interface_names);
} }
QHostAddress MessageClient::server_address () const QHostAddress MessageClient::server_address () const
@ -463,20 +536,9 @@ auto MessageClient::server_port () const -> port_type
return m_->server_port_; return m_->server_port_;
} }
void MessageClient::set_server (QString const& server) void MessageClient::set_server (QString const& server_name, QStringList const& network_interface_names)
{ {
m_->server_.clear (); m_->set_server (server_name, network_interface_names);
m_->server_string_ = server;
if (server.size ())
{
// queue a host address lookup
TRACE_UDP ("server host DNS lookup:" << server);
#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
m_->dns_lookup_id_ = QHostInfo::lookupHost (server, &*m_, &MessageClient::impl::host_info_results);
#else
m_->dns_lookup_id_ = QHostInfo::lookupHost (server, &*m_, SLOT (host_info_results (QHostInfo)));
#endif
}
} }
void MessageClient::set_server_port (port_type server_port) void MessageClient::set_server_port (port_type server_port)
@ -484,6 +546,12 @@ void MessageClient::set_server_port (port_type server_port)
m_->server_port_ = server_port; m_->server_port_ = server_port;
} }
void MessageClient::set_TTL (int TTL)
{
m_->TTL_ = TTL;
m_->setSocketOption (QAbstractSocket::MulticastTtlOption, m_->TTL_);
}
void MessageClient::enable (bool flag) void MessageClient::enable (bool flag)
{ {
m_->enabled_ = flag; m_->enabled_ = flag;
@ -499,7 +567,7 @@ void MessageClient::status_update (Frequency f, QString const& mode, QString con
, quint32 frequency_tolerance, quint32 tr_period , quint32 frequency_tolerance, quint32 tr_period
, QString const& configuration_name) , QString const& configuration_name)
{ {
if (m_->server_port_ && !m_->server_string_.isEmpty ()) if (m_->server_port_ && !m_->server_.isNull ())
{ {
QByteArray message; QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::Status, m_->id_, m_->schema_}; NetworkMessage::Builder out {&message, NetworkMessage::Status, m_->id_, m_->schema_};
@ -516,7 +584,7 @@ void MessageClient::decode (bool is_new, QTime time, qint32 snr, float delta_tim
, QString const& mode, QString const& message_text, bool low_confidence , QString const& mode, QString const& message_text, bool low_confidence
, bool off_air) , bool off_air)
{ {
if (m_->server_port_ && !m_->server_string_.isEmpty ()) if (m_->server_port_ && !m_->server_.isNull ())
{ {
QByteArray message; QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::Decode, m_->id_, m_->schema_}; NetworkMessage::Builder out {&message, NetworkMessage::Decode, m_->id_, m_->schema_};
@ -531,7 +599,7 @@ void MessageClient::WSPR_decode (bool is_new, QTime time, qint32 snr, float delt
, qint32 drift, QString const& callsign, QString const& grid, qint32 power , qint32 drift, QString const& callsign, QString const& grid, qint32 power
, bool off_air) , bool off_air)
{ {
if (m_->server_port_ && !m_->server_string_.isEmpty ()) if (m_->server_port_ && !m_->server_.isNull ())
{ {
QByteArray message; QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::WSPRDecode, m_->id_, m_->schema_}; NetworkMessage::Builder out {&message, NetworkMessage::WSPRDecode, m_->id_, m_->schema_};
@ -544,7 +612,7 @@ void MessageClient::WSPR_decode (bool is_new, QTime time, qint32 snr, float delt
void MessageClient::decodes_cleared () void MessageClient::decodes_cleared ()
{ {
if (m_->server_port_ && !m_->server_string_.isEmpty ()) if (m_->server_port_ && !m_->server_.isNull ())
{ {
QByteArray message; QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::Clear, m_->id_, m_->schema_}; NetworkMessage::Builder out {&message, NetworkMessage::Clear, m_->id_, m_->schema_};
@ -561,7 +629,7 @@ void MessageClient::qso_logged (QDateTime time_off, QString const& dx_call, QStr
, QString const& my_grid, QString const& exchange_sent , QString const& my_grid, QString const& exchange_sent
, QString const& exchange_rcvd, QString const& propmode) , QString const& exchange_rcvd, QString const& propmode)
{ {
if (m_->server_port_ && !m_->server_string_.isEmpty ()) if (m_->server_port_ && !m_->server_.isNull ())
{ {
QByteArray message; QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::QSOLogged, m_->id_, m_->schema_}; NetworkMessage::Builder out {&message, NetworkMessage::QSOLogged, m_->id_, m_->schema_};
@ -576,7 +644,7 @@ void MessageClient::qso_logged (QDateTime time_off, QString const& dx_call, QStr
void MessageClient::logged_ADIF (QByteArray const& ADIF_record) void MessageClient::logged_ADIF (QByteArray const& ADIF_record)
{ {
if (m_->server_port_ && !m_->server_string_.isEmpty ()) if (m_->server_port_ && !m_->server_.isNull ())
{ {
QByteArray message; QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::LoggedADIF, m_->id_, m_->schema_}; NetworkMessage::Builder out {&message, NetworkMessage::LoggedADIF, m_->id_, m_->schema_};

View File

@ -5,6 +5,7 @@
#include <QTime> #include <QTime>
#include <QDateTime> #include <QDateTime>
#include <QString> #include <QString>
#include <QHostAddress>
#include "Radio.hpp" #include "Radio.hpp"
#include "pimpl_h.hpp" #include "pimpl_h.hpp"
@ -34,19 +35,25 @@ public:
// //
// messages will be silently dropped until a server host lookup is complete // messages will be silently dropped until a server host lookup is complete
MessageClient (QString const& id, QString const& version, QString const& revision, MessageClient (QString const& id, QString const& version, QString const& revision,
QString const& server, port_type server_port, QObject * parent = nullptr); QString const& server_name, port_type server_port,
QStringList const& network_interface_names,
int TTL, QObject * parent = nullptr);
// query server details // query server details
QHostAddress server_address () const; QHostAddress server_address () const;
port_type server_port () const; port_type server_port () const;
// initiate a new server host lookup or is the server name is empty // initiate a new server host lookup or if the server name is empty
// the sending of messages is disabled // the sending of messages is disabled, if an interface is specified
Q_SLOT void set_server (QString const& server = QString {}); // then that interface is used for outgoing datagrams
Q_SLOT void set_server (QString const& server_name, QStringList const& network_interface_names);
// change the server port messages are sent to // change the server port messages are sent to
Q_SLOT void set_server_port (port_type server_port = 0u); Q_SLOT void set_server_port (port_type server_port = 0u);
// change the server port messages are sent to
Q_SLOT void set_TTL (int TTL);
// enable incoming messages // enable incoming messages
Q_SLOT void enable (bool); Q_SLOT void enable (bool);

View File

@ -28,13 +28,13 @@ namespace NetworkMessage
{ {
setVersion (QDataStream::Qt_5_0); // Qt schema version setVersion (QDataStream::Qt_5_0); // Qt schema version
} }
#if QT_VERSION >= 0x050200 #if QT_VERSION >= QT_VERSION_CHECK (5, 2, 0)
else if (schema <= 2) else if (schema <= 2)
{ {
setVersion (QDataStream::Qt_5_2); // Qt schema version setVersion (QDataStream::Qt_5_2); // Qt schema version
} }
#endif #endif
#if QT_VERSION >= 0x050400 #if QT_VERSION >= QT_VERSION_CHECK (5, 4, 0)
else if (schema <= 3) else if (schema <= 3)
{ {
setVersion (QDataStream::Qt_5_4); // Qt schema version setVersion (QDataStream::Qt_5_4); // Qt schema version
@ -73,13 +73,13 @@ namespace NetworkMessage
{ {
parent->setVersion (QDataStream::Qt_5_0); parent->setVersion (QDataStream::Qt_5_0);
} }
#if QT_VERSION >= 0x050200 #if QT_VERSION >= QT_VERSION_CHECK (5, 2, 0)
else if (schema_ <= 2) else if (schema_ <= 2)
{ {
parent->setVersion (QDataStream::Qt_5_2); parent->setVersion (QDataStream::Qt_5_2);
} }
#endif #endif
#if QT_VERSION >= 0x050400 #if QT_VERSION >= QT_VERSION_CHECK (5, 4, 0)
else if (schema_ <= 3) else if (schema_ <= 3)
{ {
parent->setVersion (QDataStream::Qt_5_4); parent->setVersion (QDataStream::Qt_5_4);

View File

@ -540,9 +540,9 @@ namespace NetworkMessage
// increment this if a newer Qt schema is required and add decode // increment this if a newer Qt schema is required and add decode
// logic to the Builder and Reader class implementations // logic to the Builder and Reader class implementations
#if QT_VERSION >= 0x050400 #if QT_VERSION >= QT_VERSION_CHECK (5, 4, 0)
static quint32 constexpr schema_number {3}; static quint32 constexpr schema_number {3};
#elif QT_VERSION >= 0x050200 #elif QT_VERSION >= QT_VERSION_CHECK (5, 2, 0)
static quint32 constexpr schema_number {2}; static quint32 constexpr schema_number {2};
#else #else
// Schema 1 (Qt_5_0) is broken // Schema 1 (Qt_5_0) is broken

View File

@ -25,10 +25,13 @@ namespace
QFont text_font {"Courier", 10}; QFont text_font {"Courier", 10};
QList<QStandardItem *> make_row (QString const& client_id, QTime time, qint32 snr, float delta_time QList<QStandardItem *> make_row (MessageServer::ClientKey const& key, QTime time, qint32 snr, float delta_time
, Frequency frequency, qint32 drift, QString const& callsign , Frequency frequency, qint32 drift, QString const& callsign
, QString const& grid, qint32 power, bool off_air) , QString const& grid, qint32 power, bool off_air)
{ {
auto client_item = new QStandardItem {QString {"%1(%2)"}.arg (key.second).arg (key.first.toString ())};
client_item->setData (QVariant::fromValue (key));
auto time_item = new QStandardItem {time.toString ("hh:mm")}; auto time_item = new QStandardItem {time.toString ("hh:mm")};
time_item->setData (time); time_item->setData (time);
time_item->setTextAlignment (Qt::AlignRight); time_item->setTextAlignment (Qt::AlignRight);
@ -60,7 +63,7 @@ namespace
live->setTextAlignment (Qt::AlignHCenter); live->setTextAlignment (Qt::AlignHCenter);
QList<QStandardItem *> row { QList<QStandardItem *> row {
new QStandardItem {client_id}, time_item, snr_item, dt, freq, dri, gd, pwr, live, new QStandardItem {callsign}}; client_item, time_item, snr_item, dt, freq, dri, gd, pwr, live, new QStandardItem {callsign}};
Q_FOREACH (auto& item, row) Q_FOREACH (auto& item, row)
{ {
item->setEditable (false); item->setEditable (false);
@ -81,7 +84,7 @@ BeaconsModel::BeaconsModel (QObject * parent)
} }
} }
void BeaconsModel::add_beacon_spot (bool is_new, QString const& client_id, QTime time, qint32 snr, float delta_time void BeaconsModel::add_beacon_spot (bool is_new, ClientKey const& key, QTime time, qint32 snr, float delta_time
, Frequency frequency, qint32 drift, QString const& callsign , Frequency frequency, qint32 drift, QString const& callsign
, QString const& grid, qint32 power, bool off_air) , QString const& grid, qint32 power, bool off_air)
{ {
@ -90,7 +93,7 @@ void BeaconsModel::add_beacon_spot (bool is_new, QString const& client_id, QTime
int target_row {-1}; int target_row {-1};
for (auto row = 0; row < rowCount (); ++row) for (auto row = 0; row < rowCount (); ++row)
{ {
if (data (index (row, 0)).toString () == client_id) if (item (row, 0)->data ().value<ClientKey> () == key)
{ {
auto row_time = item (row, 1)->data ().toTime (); auto row_time = item (row, 1)->data ().toTime ();
if (row_time == time if (row_time == time
@ -113,19 +116,19 @@ void BeaconsModel::add_beacon_spot (bool is_new, QString const& client_id, QTime
} }
if (target_row >= 0) if (target_row >= 0)
{ {
insertRow (target_row + 1, make_row (client_id, time, snr, delta_time, frequency, drift, callsign, grid, power, off_air)); insertRow (target_row + 1, make_row (key, time, snr, delta_time, frequency, drift, callsign, grid, power, off_air));
return; return;
} }
} }
appendRow (make_row (client_id, time, snr, delta_time, frequency, drift, callsign, grid, power, off_air)); appendRow (make_row (key, time, snr, delta_time, frequency, drift, callsign, grid, power, off_air));
} }
void BeaconsModel::decodes_cleared (QString const& client_id) void BeaconsModel::decodes_cleared (ClientKey const& key)
{ {
for (auto row = rowCount () - 1; row >= 0; --row) for (auto row = rowCount () - 1; row >= 0; --row)
{ {
if (data (index (row, 0)).toString () == client_id) if (item (row, 0)->data ().value<ClientKey> () == key)
{ {
removeRow (row); removeRow (row);
} }

View File

@ -26,13 +26,15 @@ class BeaconsModel
{ {
Q_OBJECT; Q_OBJECT;
using ClientKey = MessageServer::ClientKey;
public: public:
explicit BeaconsModel (QObject * parent = nullptr); explicit BeaconsModel (QObject * parent = nullptr);
Q_SLOT void add_beacon_spot (bool is_new, QString const& client_id, QTime time, qint32 snr, float delta_time Q_SLOT void add_beacon_spot (bool is_new, ClientKey const&, QTime time, qint32 snr, float delta_time
, Frequency frequency, qint32 drift, QString const& callsign, QString const& grid , Frequency frequency, qint32 drift, QString const& callsign, QString const& grid
, qint32 power, bool off_air); , qint32 power, bool off_air);
Q_SLOT void decodes_cleared (QString const& client_id); Q_SLOT void decodes_cleared (ClientKey const&);
}; };
#endif #endif

View File

@ -27,9 +27,9 @@ namespace
} }
} }
ClientWidget::IdFilterModel::IdFilterModel (QString const& client_id, QObject * parent) ClientWidget::IdFilterModel::IdFilterModel (ClientKey const& key, QObject * parent)
: QSortFilterProxyModel {parent} : QSortFilterProxyModel {parent}
, client_id_ {client_id} , key_ {key}
, rx_df_ (quint32_max) , rx_df_ (quint32_max)
{ {
} }
@ -73,7 +73,7 @@ bool ClientWidget::IdFilterModel::filterAcceptsRow (int source_row
, QModelIndex const& source_parent) const , QModelIndex const& source_parent) const
{ {
auto source_index_col0 = sourceModel ()->index (source_row, 0, source_parent); auto source_index_col0 = sourceModel ()->index (source_row, 0, source_parent);
return sourceModel ()->data (source_index_col0).toString () == client_id_; return sourceModel ()->data (source_index_col0).value<ClientKey> () == key_;
} }
void ClientWidget::IdFilterModel::de_call (QString const& call) void ClientWidget::IdFilterModel::de_call (QString const& call)
@ -106,9 +106,9 @@ void ClientWidget::IdFilterModel::rx_df (quint32 df)
namespace namespace
{ {
QString make_title (QString const& id, QString const& version, QString const& revision) QString make_title (MessageServer::ClientKey const& key, QString const& version, QString const& revision)
{ {
QString title {id}; QString title {QString {"%1(%2)"}.arg (key.second).arg (key.first.toString ())};
if (version.size ()) if (version.size ())
{ {
title += QString {" v%1"}.arg (version); title += QString {" v%1"}.arg (version);
@ -122,14 +122,14 @@ namespace
} }
ClientWidget::ClientWidget (QAbstractItemModel * decodes_model, QAbstractItemModel * beacons_model ClientWidget::ClientWidget (QAbstractItemModel * decodes_model, QAbstractItemModel * beacons_model
, QString const& id, QString const& version, QString const& revision , ClientKey const& key, QString const& version, QString const& revision
, QListWidget const * calls_of_interest, QWidget * parent) , QListWidget const * calls_of_interest, QWidget * parent)
: QDockWidget {make_title (id, version, revision), parent} : QDockWidget {make_title (key, version, revision), parent}
, id_ {id} , key_ {key}
, done_ {false} , done_ {false}
, calls_of_interest_ {calls_of_interest} , calls_of_interest_ {calls_of_interest}
, decodes_proxy_model_ {id} , decodes_proxy_model_ {key}
, beacons_proxy_model_ {id} , beacons_proxy_model_ {key}
, erase_action_ {new QAction {tr ("&Erase Band Activity"), this}} , erase_action_ {new QAction {tr ("&Erase Band Activity"), this}}
, erase_rx_frequency_action_ {new QAction {tr ("Erase &Rx Frequency"), this}} , erase_rx_frequency_action_ {new QAction {tr ("Erase &Rx Frequency"), this}}
, erase_both_action_ {new QAction {tr ("Erase &Both"), this}} , erase_both_action_ {new QAction {tr ("Erase &Both"), this}}
@ -209,56 +209,56 @@ ClientWidget::ClientWidget (QAbstractItemModel * decodes_model, QAbstractItemMod
horizontal_layout_->addLayout (subform3_layout_); horizontal_layout_->addLayout (subform3_layout_);
connect (message_line_edit_, &QLineEdit::textEdited, [this] (QString const& text) { connect (message_line_edit_, &QLineEdit::textEdited, [this] (QString const& text) {
Q_EMIT do_free_text (id_, text, false); Q_EMIT do_free_text (key_, text, false);
}); });
connect (message_line_edit_, &QLineEdit::editingFinished, [this] () { connect (message_line_edit_, &QLineEdit::editingFinished, [this] () {
Q_EMIT do_free_text (id_, message_line_edit_->text (), true); Q_EMIT do_free_text (key_, message_line_edit_->text (), true);
}); });
connect (grid_line_edit_, &QLineEdit::editingFinished, [this] () { connect (grid_line_edit_, &QLineEdit::editingFinished, [this] () {
Q_EMIT location (id_, grid_line_edit_->text ()); Q_EMIT location (key_, grid_line_edit_->text ());
}); });
connect (configuration_line_edit_, &QLineEdit::editingFinished, [this] () { connect (configuration_line_edit_, &QLineEdit::editingFinished, [this] () {
Q_EMIT switch_configuration (id_, configuration_line_edit_->text ()); Q_EMIT switch_configuration (key_, configuration_line_edit_->text ());
}); });
connect (mode_line_edit_, &QLineEdit::editingFinished, [this] () { connect (mode_line_edit_, &QLineEdit::editingFinished, [this] () {
QString empty; QString empty;
Q_EMIT configure (id_, mode_line_edit_->text (), quint32_max, empty, fast_mode () Q_EMIT configure (key_, mode_line_edit_->text (), quint32_max, empty, fast_mode ()
, quint32_max, quint32_max, empty, empty, false); , quint32_max, quint32_max, empty, empty, false);
}); });
connect (frequency_tolerance_spin_box_, static_cast<void (QSpinBox::*) (int)> (&QSpinBox::valueChanged), [this] (int i) { connect (frequency_tolerance_spin_box_, static_cast<void (QSpinBox::*) (int)> (&QSpinBox::valueChanged), [this] (int i) {
QString empty; QString empty;
auto f = frequency_tolerance_spin_box_->specialValueText ().size () ? quint32_max : i; auto f = frequency_tolerance_spin_box_->specialValueText ().size () ? quint32_max : i;
Q_EMIT configure (id_, empty, f, empty, fast_mode () Q_EMIT configure (key_, empty, f, empty, fast_mode ()
, quint32_max, quint32_max, empty, empty, false); , quint32_max, quint32_max, empty, empty, false);
}); });
connect (submode_line_edit_, &QLineEdit::editingFinished, [this] () { connect (submode_line_edit_, &QLineEdit::editingFinished, [this] () {
QString empty; QString empty;
Q_EMIT configure (id_, empty, quint32_max, submode_line_edit_->text (), fast_mode () Q_EMIT configure (key_, empty, quint32_max, submode_line_edit_->text (), fast_mode ()
, quint32_max, quint32_max, empty, empty, false); , quint32_max, quint32_max, empty, empty, false);
}); });
connect (fast_mode_check_box_, &QCheckBox::stateChanged, [this] (int state) { connect (fast_mode_check_box_, &QCheckBox::stateChanged, [this] (int state) {
QString empty; QString empty;
Q_EMIT configure (id_, empty, quint32_max, empty, Qt::Checked == state Q_EMIT configure (key_, empty, quint32_max, empty, Qt::Checked == state
, quint32_max, quint32_max, empty, empty, false); , quint32_max, quint32_max, empty, empty, false);
}); });
connect (tr_period_spin_box_, static_cast<void (QSpinBox::*) (int)> (&QSpinBox::valueChanged), [this] (int i) { connect (tr_period_spin_box_, static_cast<void (QSpinBox::*) (int)> (&QSpinBox::valueChanged), [this] (int i) {
QString empty; QString empty;
Q_EMIT configure (id_, empty, quint32_max, empty, fast_mode () Q_EMIT configure (key_, empty, quint32_max, empty, fast_mode ()
, i, quint32_max, empty, empty, false); , i, quint32_max, empty, empty, false);
}); });
connect (rx_df_spin_box_, static_cast<void (QSpinBox::*) (int)> (&QSpinBox::valueChanged), [this] (int i) { connect (rx_df_spin_box_, static_cast<void (QSpinBox::*) (int)> (&QSpinBox::valueChanged), [this] (int i) {
QString empty; QString empty;
Q_EMIT configure (id_, empty, quint32_max, empty, fast_mode () Q_EMIT configure (key_, empty, quint32_max, empty, fast_mode ()
, quint32_max, i, empty, empty, false); , quint32_max, i, empty, empty, false);
}); });
connect (dx_call_line_edit_, &QLineEdit::editingFinished, [this] () { connect (dx_call_line_edit_, &QLineEdit::editingFinished, [this] () {
QString empty; QString empty;
Q_EMIT configure (id_, empty, quint32_max, empty, fast_mode () Q_EMIT configure (key_, empty, quint32_max, empty, fast_mode ()
, quint32_max, quint32_max, dx_call_line_edit_->text (), empty, false); , quint32_max, quint32_max, dx_call_line_edit_->text (), empty, false);
}); });
connect (dx_grid_line_edit_, &QLineEdit::editingFinished, [this] () { connect (dx_grid_line_edit_, &QLineEdit::editingFinished, [this] () {
QString empty; QString empty;
Q_EMIT configure (id_, empty, quint32_max, empty, fast_mode () Q_EMIT configure (key_, empty, quint32_max, empty, fast_mode ()
, quint32_max, quint32_max, empty, dx_grid_line_edit_->text (), false); , quint32_max, quint32_max, empty, dx_grid_line_edit_->text (), false);
}); });
@ -289,14 +289,14 @@ ClientWidget::ClientWidget (QAbstractItemModel * decodes_model, QAbstractItemMod
halt_tx_button_ = control_button_box_->addButton (tr ("&Halt Tx"), QDialogButtonBox::ActionRole); halt_tx_button_ = control_button_box_->addButton (tr ("&Halt Tx"), QDialogButtonBox::ActionRole);
connect (generate_messages_push_button_, &QAbstractButton::clicked, [this] (bool /*checked*/) { connect (generate_messages_push_button_, &QAbstractButton::clicked, [this] (bool /*checked*/) {
QString empty; QString empty;
Q_EMIT configure (id_, empty, quint32_max, empty, fast_mode () Q_EMIT configure (key_, empty, quint32_max, empty, fast_mode ()
, quint32_max, quint32_max, empty, empty, true); , quint32_max, quint32_max, empty, empty, true);
}); });
connect (auto_off_button_, &QAbstractButton::clicked, [this] (bool /* checked */) { connect (auto_off_button_, &QAbstractButton::clicked, [this] (bool /* checked */) {
Q_EMIT do_halt_tx (id_, true); Q_EMIT do_halt_tx (key_, true);
}); });
connect (halt_tx_button_, &QAbstractButton::clicked, [this] (bool /* checked */) { connect (halt_tx_button_, &QAbstractButton::clicked, [this] (bool /* checked */) {
Q_EMIT do_halt_tx (id_, false); Q_EMIT do_halt_tx (key_, false);
}); });
content_layout_->addWidget (control_button_box_); content_layout_->addWidget (control_button_box_);
@ -318,13 +318,13 @@ ClientWidget::ClientWidget (QAbstractItemModel * decodes_model, QAbstractItemMod
// connect context menu actions // connect context menu actions
connect (erase_action_, &QAction::triggered, [this] (bool /*checked*/) { connect (erase_action_, &QAction::triggered, [this] (bool /*checked*/) {
Q_EMIT do_clear_decodes (id_); Q_EMIT do_clear_decodes (key_);
}); });
connect (erase_rx_frequency_action_, &QAction::triggered, [this] (bool /*checked*/) { connect (erase_rx_frequency_action_, &QAction::triggered, [this] (bool /*checked*/) {
Q_EMIT do_clear_decodes (id_, 1); Q_EMIT do_clear_decodes (key_, 1);
}); });
connect (erase_both_action_, &QAction::triggered, [this] (bool /*checked*/) { connect (erase_both_action_, &QAction::triggered, [this] (bool /*checked*/) {
Q_EMIT do_clear_decodes (id_, 2); Q_EMIT do_clear_decodes (key_, 2);
}); });
// connect up table view signals // connect up table view signals
@ -335,7 +335,7 @@ ClientWidget::ClientWidget (QAbstractItemModel * decodes_model, QAbstractItemMod
// tell new client about calls of interest // tell new client about calls of interest
for (int row = 0; row < calls_of_interest_->count (); ++row) for (int row = 0; row < calls_of_interest_->count (); ++row)
{ {
Q_EMIT highlight_callsign (id_, calls_of_interest_->item (row)->text (), QColor {Qt::blue}, QColor {Qt::yellow}); Q_EMIT highlight_callsign (key_, calls_of_interest_->item (row)->text (), QColor {Qt::blue}, QColor {Qt::yellow});
} }
} }
@ -349,7 +349,7 @@ void ClientWidget::closeEvent (QCloseEvent *e)
{ {
if (!done_) if (!done_)
{ {
Q_EMIT do_close (id_); Q_EMIT do_close (key_);
e->ignore (); // defer closure until client actually closes e->ignore (); // defer closure until client actually closes
} }
else else
@ -363,7 +363,7 @@ ClientWidget::~ClientWidget ()
for (int row = 0; row < calls_of_interest_->count (); ++row) for (int row = 0; row < calls_of_interest_->count (); ++row)
{ {
// tell client to forget calls of interest // tell client to forget calls of interest
Q_EMIT highlight_callsign (id_, calls_of_interest_->item (row)->text ()); Q_EMIT highlight_callsign (key_, calls_of_interest_->item (row)->text ());
} }
} }
@ -395,7 +395,7 @@ namespace
} }
} }
void ClientWidget::update_status (QString const& id, Frequency f, QString const& mode, QString const& dx_call void ClientWidget::update_status (ClientKey const& key, Frequency f, QString const& mode, QString const& dx_call
, QString const& report, QString const& tx_mode, bool tx_enabled , QString const& report, QString const& tx_mode, bool tx_enabled
, bool transmitting, bool decoding, quint32 rx_df, quint32 tx_df , bool transmitting, bool decoding, quint32 rx_df, quint32 tx_df
, QString const& de_call, QString const& de_grid, QString const& dx_grid , QString const& de_call, QString const& de_grid, QString const& dx_grid
@ -403,7 +403,7 @@ void ClientWidget::update_status (QString const& id, Frequency f, QString const&
, quint8 special_op_mode, quint32 frequency_tolerance, quint32 tr_period , quint8 special_op_mode, quint32 frequency_tolerance, quint32 tr_period
, QString const& configuration_name) , QString const& configuration_name)
{ {
if (id == id_) if (key == key_)
{ {
fast_mode_check_box_->setChecked (fast_mode); fast_mode_check_box_->setChecked (fast_mode);
decodes_proxy_model_.de_call (de_call); decodes_proxy_model_.de_call (de_call);
@ -447,11 +447,11 @@ void ClientWidget::update_status (QString const& id, Frequency f, QString const&
} }
} }
void ClientWidget::decode_added (bool /*is_new*/, QString const& client_id, QTime /*time*/, qint32 /*snr*/ void ClientWidget::decode_added (bool /*is_new*/, ClientKey const& key, QTime /*time*/, qint32 /*snr*/
, float /*delta_time*/, quint32 /*delta_frequency*/, QString const& /*mode*/ , float /*delta_time*/, quint32 /*delta_frequency*/, QString const& /*mode*/
, QString const& /*message*/, bool /*low_confidence*/, bool /*off_air*/) , QString const& /*message*/, bool /*low_confidence*/, bool /*off_air*/)
{ {
if (client_id == id_ && !columns_resized_) if (key == key_ && !columns_resized_)
{ {
decodes_stack_->setCurrentIndex (0); decodes_stack_->setCurrentIndex (0);
decodes_table_view_->resizeColumnsToContents (); decodes_table_view_->resizeColumnsToContents ();
@ -460,12 +460,12 @@ void ClientWidget::decode_added (bool /*is_new*/, QString const& client_id, QTim
decodes_table_view_->scrollToBottom (); decodes_table_view_->scrollToBottom ();
} }
void ClientWidget::beacon_spot_added (bool /*is_new*/, QString const& client_id, QTime /*time*/, qint32 /*snr*/ void ClientWidget::beacon_spot_added (bool /*is_new*/, ClientKey const& key, QTime /*time*/, qint32 /*snr*/
, float /*delta_time*/, Frequency /*delta_frequency*/, qint32 /*drift*/ , float /*delta_time*/, Frequency /*delta_frequency*/, qint32 /*drift*/
, QString const& /*callsign*/, QString const& /*grid*/, qint32 /*power*/ , QString const& /*callsign*/, QString const& /*grid*/, qint32 /*power*/
, bool /*off_air*/) , bool /*off_air*/)
{ {
if (client_id == id_ && !columns_resized_) if (key == key_ && !columns_resized_)
{ {
decodes_stack_->setCurrentIndex (1); decodes_stack_->setCurrentIndex (1);
beacons_table_view_->resizeColumnsToContents (); beacons_table_view_->resizeColumnsToContents ();
@ -474,9 +474,9 @@ void ClientWidget::beacon_spot_added (bool /*is_new*/, QString const& client_id,
beacons_table_view_->scrollToBottom (); beacons_table_view_->scrollToBottom ();
} }
void ClientWidget::decodes_cleared (QString const& client_id) void ClientWidget::decodes_cleared (ClientKey const& key)
{ {
if (client_id == id_) if (key == key_)
{ {
columns_resized_ = false; columns_resized_ = false;
} }

View File

@ -35,42 +35,44 @@ class ClientWidget
{ {
Q_OBJECT; Q_OBJECT;
using ClientKey = MessageServer::ClientKey;
public: public:
explicit ClientWidget (QAbstractItemModel * decodes_model, QAbstractItemModel * beacons_model explicit ClientWidget (QAbstractItemModel * decodes_model, QAbstractItemModel * beacons_model
, QString const& id, QString const& version, QString const& revision , ClientKey const& key, QString const& version, QString const& revision
, QListWidget const * calls_of_interest, QWidget * parent = nullptr); , QListWidget const * calls_of_interest, QWidget * parent = nullptr);
void dispose (); void dispose ();
~ClientWidget (); ~ClientWidget ();
bool fast_mode () const; bool fast_mode () const;
Q_SLOT void update_status (QString const& id, Frequency f, QString const& mode, QString const& dx_call Q_SLOT void update_status (ClientKey const& key, Frequency f, QString const& mode, QString const& dx_call
, QString const& report, QString const& tx_mode, bool tx_enabled , QString const& report, QString const& tx_mode, bool tx_enabled
, bool transmitting, bool decoding, quint32 rx_df, quint32 tx_df , bool transmitting, bool decoding, quint32 rx_df, quint32 tx_df
, QString const& de_call, QString const& de_grid, QString const& dx_grid , QString const& de_call, QString const& de_grid, QString const& dx_grid
, bool watchdog_timeout, QString const& sub_mode, bool fast_mode , bool watchdog_timeout, QString const& sub_mode, bool fast_mode
, quint8 special_op_mode, quint32 frequency_tolerance, quint32 tr_period , quint8 special_op_mode, quint32 frequency_tolerance, quint32 tr_period
, QString const& configuration_name); , QString const& configuration_name);
Q_SLOT void decode_added (bool is_new, QString const& client_id, QTime, qint32 snr Q_SLOT void decode_added (bool is_new, ClientKey const& key, QTime, qint32 snr
, float delta_time, quint32 delta_frequency, QString const& mode , float delta_time, quint32 delta_frequency, QString const& mode
, QString const& message, bool low_confidence, bool off_air); , QString const& message, bool low_confidence, bool off_air);
Q_SLOT void beacon_spot_added (bool is_new, QString const& client_id, QTime, qint32 snr Q_SLOT void beacon_spot_added (bool is_new, ClientKey const& key, QTime, qint32 snr
, float delta_time, Frequency delta_frequency, qint32 drift , float delta_time, Frequency delta_frequency, qint32 drift
, QString const& callsign, QString const& grid, qint32 power , QString const& callsign, QString const& grid, qint32 power
, bool off_air); , bool off_air);
Q_SLOT void decodes_cleared (QString const& client_id); Q_SLOT void decodes_cleared (ClientKey const& key);
Q_SIGNAL void do_clear_decodes (QString const& id, quint8 window = 0); Q_SIGNAL void do_clear_decodes (ClientKey const& key, quint8 window = 0);
Q_SIGNAL void do_close (QString const& id); Q_SIGNAL void do_close (ClientKey const& key);
Q_SIGNAL void do_reply (QModelIndex const&, quint8 modifier); Q_SIGNAL void do_reply (QModelIndex const&, quint8 modifier);
Q_SIGNAL void do_halt_tx (QString const& id, bool auto_only); Q_SIGNAL void do_halt_tx (ClientKey const& key, bool auto_only);
Q_SIGNAL void do_free_text (QString const& id, QString const& text, bool); Q_SIGNAL void do_free_text (ClientKey const& key, QString const& text, bool);
Q_SIGNAL void location (QString const& id, QString const& text); Q_SIGNAL void location (ClientKey const& key, QString const& text);
Q_SIGNAL void highlight_callsign (QString const& id, QString const& call Q_SIGNAL void highlight_callsign (ClientKey const& key, QString const& call
, QColor const& bg = QColor {}, QColor const& fg = QColor {} , QColor const& bg = QColor {}, QColor const& fg = QColor {}
, bool last_only = false); , bool last_only = false);
Q_SIGNAL void switch_configuration (QString const& id, QString const& configuration_name); Q_SIGNAL void switch_configuration (ClientKey const& key, QString const& configuration_name);
Q_SIGNAL void configure (QString const& id, QString const& mode, quint32 frequency_tolerance Q_SIGNAL void configure (ClientKey const& key, QString const& mode, quint32 frequency_tolerance
, QString const& submode, bool fast_mode, quint32 tr_period, quint32 rx_df , QString const& submode, bool fast_mode, quint32 tr_period, quint32 rx_df
, QString const& dx_call, QString const& dx_grid, bool generate_messages); , QString const& dx_call, QString const& dx_grid, bool generate_messages);
@ -79,7 +81,7 @@ private:
: public QSortFilterProxyModel : public QSortFilterProxyModel
{ {
public: public:
IdFilterModel (QString const& client_id, QObject * = nullptr); IdFilterModel (ClientKey const& key, QObject * = nullptr);
void de_call (QString const&); void de_call (QString const&);
void rx_df (quint32); void rx_df (quint32);
@ -88,7 +90,7 @@ private:
private: private:
bool filterAcceptsRow (int source_row, QModelIndex const& source_parent) const override; bool filterAcceptsRow (int source_row, QModelIndex const& source_parent) const override;
QString client_id_; ClientKey key_;
QString call_; QString call_;
QRegularExpression base_call_re_; QRegularExpression base_call_re_;
quint32 rx_df_; quint32 rx_df_;
@ -96,7 +98,7 @@ private:
void closeEvent (QCloseEvent *) override; void closeEvent (QCloseEvent *) override;
QString id_; ClientKey key_;
bool done_; bool done_;
QListWidget const * calls_of_interest_; QListWidget const * calls_of_interest_;
IdFilterModel decodes_proxy_model_; IdFilterModel decodes_proxy_model_;

View File

@ -2,6 +2,7 @@
#include <QStandardItem> #include <QStandardItem>
#include <QModelIndex> #include <QModelIndex>
#include <QVariant>
#include <QTime> #include <QTime>
#include <QString> #include <QString>
#include <QFont> #include <QFont>
@ -33,10 +34,13 @@ namespace
QFont text_font {"Courier", 10}; QFont text_font {"Courier", 10};
QList<QStandardItem *> make_row (QString const& client_id, QTime time, qint32 snr, float delta_time QList<QStandardItem *> make_row (MessageServer::ClientKey const& key, QTime time, qint32 snr
, quint32 delta_frequency, QString const& mode, QString const& message , float delta_time, quint32 delta_frequency, QString const& mode
, bool low_confidence, bool off_air, bool is_fast) , QString const& message, bool low_confidence, bool off_air, bool is_fast)
{ {
auto client_item = new QStandardItem {QString {"%1(%2)"}.arg (key.second).arg (key.first.toString ())};
client_item->setData (QVariant::fromValue (key));
auto time_item = new QStandardItem {time.toString (is_fast || "~" == mode ? "hh:mm:ss" : "hh:mm")}; auto time_item = new QStandardItem {time.toString (is_fast || "~" == mode ? "hh:mm:ss" : "hh:mm")};
time_item->setData (time); time_item->setData (time);
time_item->setTextAlignment (Qt::AlignRight); time_item->setTextAlignment (Qt::AlignRight);
@ -63,7 +67,7 @@ namespace
live->setTextAlignment (Qt::AlignHCenter); live->setTextAlignment (Qt::AlignHCenter);
QList<QStandardItem *> row { QList<QStandardItem *> row {
new QStandardItem {client_id}, time_item, snr_item, dt, df, md, confidence, live, new QStandardItem {message}}; client_item, time_item, snr_item, dt, df, md, confidence, live, new QStandardItem {message}};
Q_FOREACH (auto& item, row) Q_FOREACH (auto& item, row)
{ {
item->setEditable (false); item->setEditable (false);
@ -84,7 +88,7 @@ DecodesModel::DecodesModel (QObject * parent)
} }
} }
void DecodesModel::add_decode (bool is_new, QString const& client_id, QTime time, qint32 snr, float delta_time void DecodesModel::add_decode (bool is_new, ClientKey const& key, QTime time, qint32 snr, float delta_time
, quint32 delta_frequency, QString const& mode, QString const& message , quint32 delta_frequency, QString const& mode, QString const& message
, bool low_confidence, bool off_air, bool is_fast) , bool low_confidence, bool off_air, bool is_fast)
{ {
@ -93,7 +97,7 @@ void DecodesModel::add_decode (bool is_new, QString const& client_id, QTime time
int target_row {-1}; int target_row {-1};
for (auto row = 0; row < rowCount (); ++row) for (auto row = 0; row < rowCount (); ++row)
{ {
if (data (index (row, 0)).toString () == client_id) if (item (row, 0)->data ().value<ClientKey> () == key)
{ {
auto row_time = item (row, 1)->data ().toTime (); auto row_time = item (row, 1)->data ().toTime ();
if (row_time == time if (row_time == time
@ -115,21 +119,21 @@ void DecodesModel::add_decode (bool is_new, QString const& client_id, QTime time
} }
if (target_row >= 0) if (target_row >= 0)
{ {
insertRow (target_row + 1, make_row (client_id, time, snr, delta_time, delta_frequency, mode insertRow (target_row + 1, make_row (key, time, snr, delta_time, delta_frequency, mode
, message, low_confidence, off_air, is_fast)); , message, low_confidence, off_air, is_fast));
return; return;
} }
} }
appendRow (make_row (client_id, time, snr, delta_time, delta_frequency, mode, message, low_confidence appendRow (make_row (key, time, snr, delta_time, delta_frequency, mode, message, low_confidence
, off_air, is_fast)); , off_air, is_fast));
} }
void DecodesModel::decodes_cleared (QString const& client_id) void DecodesModel::decodes_cleared (ClientKey const& key)
{ {
for (auto row = rowCount () - 1; row >= 0; --row) for (auto row = rowCount () - 1; row >= 0; --row)
{ {
if (data (index (row, 0)).toString () == client_id) if (item (row, 0)->data ().value<ClientKey> () == key)
{ {
removeRow (row); removeRow (row);
} }
@ -139,7 +143,7 @@ void DecodesModel::decodes_cleared (QString const& client_id)
void DecodesModel::do_reply (QModelIndex const& source, quint8 modifiers) void DecodesModel::do_reply (QModelIndex const& source, quint8 modifiers)
{ {
auto row = source.row (); auto row = source.row ();
Q_EMIT reply (data (index (row, 0)).toString () Q_EMIT reply (item (row, 0)->data ().value<ClientKey> ()
, item (row, 1)->data ().toTime () , item (row, 1)->data ().toTime ()
, item (row, 2)->data ().toInt () , item (row, 2)->data ().toInt ()
, item (row, 3)->data ().toFloat () , item (row, 3)->data ().toFloat ()

View File

@ -5,8 +5,6 @@
#include "MessageServer.hpp" #include "MessageServer.hpp"
using Frequency = MessageServer::Frequency;
class QTime; class QTime;
class QString; class QString;
class QModelIndex; class QModelIndex;
@ -28,16 +26,18 @@ class DecodesModel
{ {
Q_OBJECT; Q_OBJECT;
using ClientKey = MessageServer::ClientKey;
public: public:
explicit DecodesModel (QObject * parent = nullptr); explicit DecodesModel (QObject * parent = nullptr);
Q_SLOT void add_decode (bool is_new, QString const& client_id, QTime time, qint32 snr, float delta_time Q_SLOT void add_decode (bool is_new, ClientKey const&, QTime, qint32 snr, float delta_time
, quint32 delta_frequency, QString const& mode, QString const& message , quint32 delta_frequency, QString const& mode, QString const& message
, bool low_confidence, bool off_air, bool is_fast); , bool low_confidence, bool off_air, bool is_fast);
Q_SLOT void decodes_cleared (QString const& client_id); Q_SLOT void decodes_cleared (ClientKey const&);
Q_SLOT void do_reply (QModelIndex const& source, quint8 modifiers); Q_SLOT void do_reply (QModelIndex const& source, quint8 modifiers);
Q_SIGNAL void reply (QString const& id, QTime time, qint32 snr, float delta_time, quint32 delta_frequency Q_SIGNAL void reply (ClientKey const&, QTime, qint32 snr, float delta_time, quint32 delta_frequency
, QString const& mode, QString const& message, bool low_confidence, quint8 modifiers); , QString const& mode, QString const& message, bool low_confidence, quint8 modifiers);
}; };

View File

@ -2,6 +2,8 @@
#include <QtWidgets> #include <QtWidgets>
#include <QDateTime> #include <QDateTime>
#include <QNetworkInterface>
#include <QSet>
#include "DecodesModel.hpp" #include "DecodesModel.hpp"
#include "BeaconsModel.hpp" #include "BeaconsModel.hpp"
@ -37,8 +39,10 @@ MessageAggregatorMainWindow::MessageAggregatorMainWindow ()
, decodes_model_ {new DecodesModel {this}} , decodes_model_ {new DecodesModel {this}}
, beacons_model_ {new BeaconsModel {this}} , beacons_model_ {new BeaconsModel {this}}
, server_ {new MessageServer {this}} , server_ {new MessageServer {this}}
, multicast_group_line_edit_ {new QLineEdit} , port_spin_box_ {new QSpinBox {this}}
, log_table_view_ {new QTableView} , multicast_group_line_edit_ {new QLineEdit {this}}
, network_interfaces_combo_box_ {new CheckableItemComboBox {this}}
, log_table_view_ {new QTableView {this}}
, add_call_of_interest_action_ {new QAction {tr ("&Add callsign"), this}} , add_call_of_interest_action_ {new QAction {tr ("&Add callsign"), this}}
, delete_call_of_interest_action_ {new QAction {tr ("&Delete callsign"), this}} , delete_call_of_interest_action_ {new QAction {tr ("&Delete callsign"), this}}
, last_call_of_interest_action_ {new QAction {tr ("&Highlight last only"), this}} , last_call_of_interest_action_ {new QAction {tr ("&Highlight last only"), this}}
@ -68,16 +72,71 @@ MessageAggregatorMainWindow::MessageAggregatorMainWindow ()
auto central_layout = new QVBoxLayout; auto central_layout = new QVBoxLayout;
// server details // server details
auto port_spin_box = new QSpinBox; port_spin_box_->setMinimum (1);
port_spin_box->setMinimum (1); port_spin_box_->setMaximum (std::numeric_limits<port_type>::max ());
port_spin_box->setMaximum (std::numeric_limits<port_type>::max ());
auto group_box_layout = new QFormLayout; auto group_box_layout = new QFormLayout;
group_box_layout->addRow (tr ("Port number:"), port_spin_box); 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_); group_box_layout->addRow (tr ("Multicast Group (blank for unicast server):"), multicast_group_line_edit_);
group_box_layout->addRow (tr ("Network interfaces:"), network_interfaces_combo_box_);
int row;
QFormLayout::ItemRole role;
group_box_layout->getWidgetPosition (network_interfaces_combo_box_, &row, &role);
Q_ASSERT (row >= 0);
network_interfaces_form_label_widget_ = static_cast<QLabel *> (group_box_layout->itemAt (row, QFormLayout::LabelRole)->widget ());
network_interfaces_form_label_widget_->hide ();
network_interfaces_form_label_widget_->buddy ()->hide ();
connect (multicast_group_line_edit_, &QLineEdit::editingFinished, [this] {
if (multicast_group_line_edit_->text ().size ())
{
network_interfaces_form_label_widget_->show ();
network_interfaces_form_label_widget_->buddy ()->show ();
}
else
{
network_interfaces_form_label_widget_->hide ();
network_interfaces_form_label_widget_->buddy ()->hide ();
}
});
auto group_box = new QGroupBox {tr ("Server Details")}; auto group_box = new QGroupBox {tr ("Server Details")};
group_box->setLayout (group_box_layout); group_box->setLayout (group_box_layout);
central_layout->addWidget (group_box); central_layout->addWidget (group_box);
// populate network interface list
for (auto const& net_if : QNetworkInterface::allInterfaces ())
{
auto flags = QNetworkInterface::IsRunning | QNetworkInterface::CanMulticast;
if ((net_if.flags () & flags) == flags)
{
if (net_if.flags () & QNetworkInterface::IsLoopBack)
{
loopback_interface_name_ = net_if.name ();
}
auto item = network_interfaces_combo_box_->addCheckItem (net_if.humanReadableName ()
, net_if.name ()
, Qt::Unchecked);
auto tip = QString {"name(index): %1(%2) - %3"}
.arg (net_if.name ()).arg (net_if.index ())
.arg (net_if.flags () & QNetworkInterface::IsUp ? "Up" : "Down");
auto hw_addr = net_if.hardwareAddress ();
if (hw_addr.size ())
{
tip += QString {"\nhw: %1"}.arg (net_if.hardwareAddress ());
}
auto aes = net_if.addressEntries ();
if (aes.size ())
{
tip += "\naddresses:";
for (auto const& ae : aes)
{
tip += QString {"\n ip: %1/%2"}.arg (ae.ip ().toString ()).arg (ae.prefixLength ());
}
}
item->setToolTip (tip);
}
}
connect (network_interfaces_combo_box_, &QComboBox::currentTextChanged, this, &MessageAggregatorMainWindow::validate_network_interfaces);
validate_network_interfaces (QString {});
log_table_view_->setModel (log_); log_table_view_->setModel (log_);
log_table_view_->verticalHeader ()->hide (); log_table_view_->verticalHeader ()->hide ();
central_layout->addWidget (log_table_view_); central_layout->addWidget (log_table_view_);
@ -184,30 +243,52 @@ MessageAggregatorMainWindow::MessageAggregatorMainWindow ()
connect (server_, &MessageServer::client_closed, this, &MessageAggregatorMainWindow::remove_client); connect (server_, &MessageServer::client_closed, this, &MessageAggregatorMainWindow::remove_client);
connect (server_, &MessageServer::client_closed, decodes_model_, &DecodesModel::decodes_cleared); connect (server_, &MessageServer::client_closed, decodes_model_, &DecodesModel::decodes_cleared);
connect (server_, &MessageServer::client_closed, beacons_model_, &BeaconsModel::decodes_cleared); connect (server_, &MessageServer::client_closed, beacons_model_, &BeaconsModel::decodes_cleared);
connect (server_, &MessageServer::decode, [this] (bool is_new, QString const& id, QTime time connect (server_, &MessageServer::decode, [this] (bool is_new, ClientKey const& key, QTime time
, qint32 snr, float delta_time , qint32 snr, float delta_time
, quint32 delta_frequency, QString const& mode , quint32 delta_frequency, QString const& mode
, QString const& message, bool low_confidence , QString const& message, bool low_confidence
, bool off_air) { , bool off_air) {
decodes_model_->add_decode (is_new, id, time, snr, delta_time, delta_frequency, mode, message decodes_model_->add_decode (is_new, key, time, snr, delta_time
, low_confidence, off_air, dock_widgets_[id]->fast_mode ());}); , delta_frequency, mode, message
, low_confidence, off_air
, dock_widgets_[key]->fast_mode ());
});
connect (server_, &MessageServer::WSPR_decode, beacons_model_, &BeaconsModel::add_beacon_spot); connect (server_, &MessageServer::WSPR_decode, beacons_model_, &BeaconsModel::add_beacon_spot);
connect (server_, &MessageServer::decodes_cleared, decodes_model_, &DecodesModel::decodes_cleared); connect (server_, &MessageServer::decodes_cleared, decodes_model_, &DecodesModel::decodes_cleared);
connect (server_, &MessageServer::decodes_cleared, beacons_model_, &BeaconsModel::decodes_cleared); connect (server_, &MessageServer::decodes_cleared, beacons_model_, &BeaconsModel::decodes_cleared);
connect (decodes_model_, &DecodesModel::reply, server_, &MessageServer::reply); connect (decodes_model_, &DecodesModel::reply, server_, &MessageServer::reply);
// UI behaviour // UI behaviour
connect (port_spin_box, static_cast<void (QSpinBox::*)(int)> (&QSpinBox::valueChanged) connect (port_spin_box_, static_cast<void (QSpinBox::*)(int)> (&QSpinBox::valueChanged)
, [this] (port_type port) {server_->start (port);}); , [this] (int /*port*/) {restart_server ();});
connect (multicast_group_line_edit_, &QLineEdit::editingFinished, [this, port_spin_box] () { connect (multicast_group_line_edit_, &QLineEdit::editingFinished, [this] () {restart_server ();});
server_->start (port_spin_box->value (), QHostAddress {multicast_group_line_edit_->text ()}); connect (network_interfaces_combo_box_, &QComboBox::currentTextChanged, [this] () {restart_server ();});
});
port_spin_box->setValue (2237); // start up in unicast mode port_spin_box_->setValue (2237); // start up in unicast mode
show (); show ();
} }
void MessageAggregatorMainWindow::log_qso (QString const& /*id*/, QDateTime time_off, QString const& dx_call void MessageAggregatorMainWindow::restart_server ()
{
QSet<QString> net_ifs;
if (network_interfaces_combo_box_->isVisible ())
{
auto model = static_cast<QStandardItemModel *> (network_interfaces_combo_box_->model ());
for (int row = 0; row < model->rowCount (); ++row)
{
if (Qt::Checked == model->item (row)->checkState ())
{
net_ifs << model->item (row)->data ().toString ();
}
}
}
server_->start (port_spin_box_->value ()
, QHostAddress {multicast_group_line_edit_->text ()}
, net_ifs);
}
void MessageAggregatorMainWindow::log_qso (ClientKey const& /*key*/, QDateTime time_off
, QString const& dx_call
, QString const& dx_grid, Frequency dial_frequency, QString const& mode , QString const& dx_grid, Frequency dial_frequency, QString const& mode
, QString const& report_sent, QString const& report_received , QString const& report_sent, QString const& report_received
, QString const& tx_power, QString const& comments , QString const& tx_power, QString const& comments
@ -240,9 +321,9 @@ void MessageAggregatorMainWindow::log_qso (QString const& /*id*/, QDateTime time
log_table_view_->scrollToBottom (); log_table_view_->scrollToBottom ();
} }
void MessageAggregatorMainWindow::add_client (QString const& id, QString const& version, QString const& revision) void MessageAggregatorMainWindow::add_client (ClientKey const& key, QString const& version, QString const& revision)
{ {
auto dock = new ClientWidget {decodes_model_, beacons_model_, id, version, revision, calls_of_interest_, this}; auto dock = new ClientWidget {decodes_model_, beacons_model_, key, version, revision, calls_of_interest_, this};
dock->setAttribute (Qt::WA_DeleteOnClose); dock->setAttribute (Qt::WA_DeleteOnClose);
auto view_action = dock->toggleViewAction (); auto view_action = dock->toggleViewAction ();
view_action->setEnabled (true); view_action->setEnabled (true);
@ -262,13 +343,13 @@ void MessageAggregatorMainWindow::add_client (QString const& id, QString const&
connect (dock, &ClientWidget::highlight_callsign, server_, &MessageServer::highlight_callsign); connect (dock, &ClientWidget::highlight_callsign, server_, &MessageServer::highlight_callsign);
connect (dock, &ClientWidget::switch_configuration, server_, &MessageServer::switch_configuration); connect (dock, &ClientWidget::switch_configuration, server_, &MessageServer::switch_configuration);
connect (dock, &ClientWidget::configure, server_, &MessageServer::configure); connect (dock, &ClientWidget::configure, server_, &MessageServer::configure);
dock_widgets_[id] = dock; dock_widgets_[key] = dock;
server_->replay (id); // request decodes and status server_->replay (key); // request decodes and status
} }
void MessageAggregatorMainWindow::remove_client (QString const& id) void MessageAggregatorMainWindow::remove_client (ClientKey const& key)
{ {
auto iter = dock_widgets_.find (id); auto iter = dock_widgets_.find (key);
if (iter != std::end (dock_widgets_)) if (iter != std::end (dock_widgets_))
{ {
(*iter)->dispose (); (*iter)->dispose ();
@ -287,9 +368,35 @@ MessageAggregatorMainWindow::~MessageAggregatorMainWindow ()
void MessageAggregatorMainWindow::change_highlighting (QString const& call, QColor const& bg, QColor const& fg void MessageAggregatorMainWindow::change_highlighting (QString const& call, QColor const& bg, QColor const& fg
, bool last_only) , bool last_only)
{ {
for (auto id : dock_widgets_.keys ()) for (auto key : dock_widgets_.keys ())
{ {
server_->highlight_callsign (id, call, bg, fg, last_only); server_->highlight_callsign (key, call, bg, fg, last_only);
}
}
void MessageAggregatorMainWindow::validate_network_interfaces (QString const& /*text*/)
{
auto model = static_cast<QStandardItemModel *> (network_interfaces_combo_box_->model ());
bool has_checked {false};
int loopback_row {-1};
for (int row = 0; row < model->rowCount (); ++row)
{
if (model->item (row)->data ().toString () == loopback_interface_name_)
{
loopback_row = row;
}
else if (Qt::Checked == model->item (row)->checkState ())
{
has_checked = true;
}
}
if (loopback_row >= 0)
{
if (!has_checked)
{
model->item (loopback_row)->setCheckState (Qt::Checked);
}
model->item (loopback_row)->setEnabled (has_checked);
} }
} }

View File

@ -6,6 +6,7 @@
#include <QString> #include <QString>
#include "MessageServer.hpp" #include "MessageServer.hpp"
#include "widgets/CheckableItemComboBox.hpp"
class QDateTime; class QDateTime;
class QStandardItemModel; class QStandardItemModel;
@ -16,6 +17,8 @@ class QLineEdit;
class QTableView; class QTableView;
class ClientWidget; class ClientWidget;
class QListWidget; class QListWidget;
class QLabel;
class QSpinBox;
using Frequency = MessageServer::Frequency; using Frequency = MessageServer::Frequency;
@ -24,11 +27,13 @@ class MessageAggregatorMainWindow
{ {
Q_OBJECT; Q_OBJECT;
using ClientKey = MessageServer::ClientKey;
public: public:
MessageAggregatorMainWindow (); MessageAggregatorMainWindow ();
~MessageAggregatorMainWindow (); ~MessageAggregatorMainWindow ();
Q_SLOT void log_qso (QString const& /*id*/, QDateTime time_off, QString const& dx_call, QString const& dx_grid Q_SLOT void log_qso (ClientKey const&, QDateTime time_off, QString const& dx_call, QString const& dx_grid
, Frequency dial_frequency, QString const& mode, QString const& report_sent , Frequency dial_frequency, QString const& mode, QString const& report_sent
, QString const& report_received, QString const& tx_power, QString const& comments , QString const& report_received, QString const& tx_power, QString const& comments
, QString const& name, QDateTime time_on, QString const& operator_call , QString const& name, QDateTime time_on, QString const& operator_call
@ -36,13 +41,15 @@ public:
, QString const& exchange_sent, QString const& exchange_rcvd, QString const& prop_mode); , QString const& exchange_sent, QString const& exchange_rcvd, QString const& prop_mode);
private: private:
void add_client (QString const& id, QString const& version, QString const& revision); void restart_server ();
void remove_client (QString const& id); void add_client (ClientKey const&, QString const& version, QString const& revision);
void remove_client (ClientKey const&);
void change_highlighting (QString const& call, QColor const& bg = QColor {}, QColor const& fg = QColor {}, void change_highlighting (QString const& call, QColor const& bg = QColor {}, QColor const& fg = QColor {},
bool last_only = false); bool last_only = false);
Q_SLOT void validate_network_interfaces (QString const&);
// maps client id to widgets // maps client id to widgets
using ClientsDictionary = QHash<QString, ClientWidget *>; using ClientsDictionary = QHash<ClientKey, ClientWidget *>;
ClientsDictionary dock_widgets_; ClientsDictionary dock_widgets_;
QStandardItemModel * log_; QStandardItemModel * log_;
@ -50,7 +57,11 @@ private:
DecodesModel * decodes_model_; DecodesModel * decodes_model_;
BeaconsModel * beacons_model_; BeaconsModel * beacons_model_;
MessageServer * server_; MessageServer * server_;
QSpinBox * port_spin_box_;
QLineEdit * multicast_group_line_edit_; QLineEdit * multicast_group_line_edit_;
CheckableItemComboBox * network_interfaces_combo_box_;
QString loopback_interface_name_;
QLabel * network_interfaces_form_label_widget_;
QTableView * log_table_view_; QTableView * log_table_view_;
QListWidget * calls_of_interest_; QListWidget * calls_of_interest_;
QAction * add_call_of_interest_action_; QAction * add_call_of_interest_action_;

View File

@ -5,7 +5,6 @@
#include <QNetworkInterface> #include <QNetworkInterface>
#include <QUdpSocket> #include <QUdpSocket>
#include <QString>
#include <QTimer> #include <QTimer>
#include <QHash> #include <QHash>
@ -32,7 +31,6 @@ public:
: self_ {self} : self_ {self}
, version_ {version} , version_ {version}
, revision_ {revision} , revision_ {revision}
, port_ {0u}
, clock_ {new QTimer {this}} , clock_ {new QTimer {this}}
{ {
// register the required types with Qt // register the required types with Qt
@ -78,15 +76,14 @@ public:
MessageServer * self_; MessageServer * self_;
QString version_; QString version_;
QString revision_; QString revision_;
port_type port_;
QHostAddress multicast_group_address_; QHostAddress multicast_group_address_;
QSet<QString> network_interfaces_;
static BindMode constexpr bind_mode_ = ShareAddress | ReuseAddressHint; static BindMode constexpr bind_mode_ = ShareAddress | ReuseAddressHint;
struct Client struct Client
{ {
Client () = default; Client () = default;
Client (QHostAddress const& sender_address, port_type const& sender_port) Client (port_type const& sender_port)
: sender_address_ {sender_address} : sender_port_ {sender_port}
, sender_port_ {sender_port}
, negotiated_schema_number_ {2} // not 1 because it's broken , negotiated_schema_number_ {2} // not 1 because it's broken
, last_activity_ {QDateTime::currentDateTime ()} , last_activity_ {QDateTime::currentDateTime ()}
{ {
@ -94,12 +91,11 @@ public:
Client (Client const&) = default; Client (Client const&) = default;
Client& operator= (Client const&) = default; Client& operator= (Client const&) = default;
QHostAddress sender_address_;
port_type sender_port_; port_type sender_port_;
quint32 negotiated_schema_number_; quint32 negotiated_schema_number_;
QDateTime last_activity_; QDateTime last_activity_;
}; };
QHash<QString, Client> clients_; // maps id to Client QHash<ClientKey, Client> clients_; // maps id to Client
QTimer * clock_; QTimer * clock_;
}; };
@ -109,56 +105,39 @@ MessageServer::impl::BindMode constexpr MessageServer::impl::bind_mode_;
void MessageServer::impl::leave_multicast_group () void MessageServer::impl::leave_multicast_group ()
{ {
if (!multicast_group_address_.isNull () && BoundState == state () if (BoundState == state () && is_multicast_address (multicast_group_address_))
#if QT_VERSION >= 0x050600
&& multicast_group_address_.isMulticast ()
#endif
)
{ {
for (auto const& interface : QNetworkInterface::allInterfaces ()) for (auto const& if_name : network_interfaces_)
{ {
if (QNetworkInterface::CanMulticast & interface.flags ()) leaveMulticastGroup (multicast_group_address_, QNetworkInterface::interfaceFromName (if_name));
{
leaveMulticastGroup (multicast_group_address_, interface);
}
} }
} }
} }
void MessageServer::impl::join_multicast_group () void MessageServer::impl::join_multicast_group ()
{ {
if (BoundState == state () if (BoundState == state () && is_multicast_address (multicast_group_address_))
&& !multicast_group_address_.isNull ()
#if QT_VERSION >= 0x050600
&& multicast_group_address_.isMulticast ()
#endif
)
{ {
auto mcast_iface = multicastInterface (); if (network_interfaces_.size ())
if (IPv4Protocol == multicast_group_address_.protocol ()
&& IPv4Protocol != localAddress ().protocol ())
{ {
close (); for (auto const& if_name : network_interfaces_)
bind (QHostAddress::AnyIPv4, port_, bind_mode_);
}
bool joined {false};
for (auto const& interface : QNetworkInterface::allInterfaces ())
{
if (QNetworkInterface::CanMulticast & interface.flags ())
{ {
// Windows requires outgoing interface to match joinMulticastGroup (multicast_group_address_, QNetworkInterface::interfaceFromName (if_name));
// interface to be joined while joining, at least for
// IPv4 it seems to
setMulticastInterface (interface);
joined |= joinMulticastGroup (multicast_group_address_, interface);
} }
} }
if (!joined) else
{ {
multicast_group_address_.clear (); // find the loop-back interface and join on that
for (auto const& net_if : QNetworkInterface::allInterfaces ())
{
auto flags = QNetworkInterface::IsUp | QNetworkInterface::IsLoopBack | QNetworkInterface::CanMulticast;
if ((net_if.flags () & flags) == flags)
{
joinMulticastGroup (multicast_group_address_, net_if);
break;
}
}
} }
setMulticastInterface (mcast_iface);
} }
} }
@ -189,9 +168,10 @@ void MessageServer::impl::parse_message (QHostAddress const& sender, port_type s
auto id = in.id (); auto id = in.id ();
if (OK == check_status (in)) if (OK == check_status (in))
{ {
if (!clients_.contains (id)) auto client_key = ClientKey {sender, id};
if (!clients_.contains (client_key))
{ {
auto& client = (clients_[id] = {sender, sender_port}); auto& client = (clients_[client_key] = {sender_port});
QByteArray client_version; QByteArray client_version;
QByteArray client_revision; QByteArray client_revision;
@ -212,7 +192,7 @@ void MessageServer::impl::parse_message (QHostAddress const& sender, port_type s
<< version_.toUtf8 () << revision_.toUtf8 (); << version_.toUtf8 () << revision_.toUtf8 ();
if (impl::OK == check_status (hb)) if (impl::OK == check_status (hb))
{ {
writeDatagram (message, client.sender_address_, client.sender_port_); writeDatagram (message, client_key.first, sender_port);
} }
else else
{ {
@ -222,10 +202,10 @@ void MessageServer::impl::parse_message (QHostAddress const& sender, port_type s
// we don't care if this fails to read // we don't care if this fails to read
in >> client_version >> client_revision; in >> client_version >> client_revision;
} }
Q_EMIT self_->client_opened (id, QString::fromUtf8 (client_version), Q_EMIT self_->client_opened (client_key, QString::fromUtf8 (client_version),
QString::fromUtf8 (client_revision)); QString::fromUtf8 (client_revision));
} }
clients_[id].last_activity_ = QDateTime::currentDateTime (); clients_[client_key].last_activity_ = QDateTime::currentDateTime ();
// //
// message format is described in NetworkMessage.hpp // message format is described in NetworkMessage.hpp
@ -237,7 +217,7 @@ void MessageServer::impl::parse_message (QHostAddress const& sender, port_type s
break; break;
case NetworkMessage::Clear: case NetworkMessage::Clear:
Q_EMIT self_->decodes_cleared (id); Q_EMIT self_->decodes_cleared (client_key);
break; break;
case NetworkMessage::Status: case NetworkMessage::Status:
@ -268,7 +248,8 @@ void MessageServer::impl::parse_message (QHostAddress const& sender, port_type s
>> fast_mode >> special_op_mode >> frequency_tolerance >> tr_period >> configuration_name; >> fast_mode >> special_op_mode >> frequency_tolerance >> tr_period >> configuration_name;
if (check_status (in) != Fail) if (check_status (in) != Fail)
{ {
Q_EMIT self_->status_update (id, f, QString::fromUtf8 (mode), QString::fromUtf8 (dx_call) Q_EMIT self_->status_update (client_key, f, QString::fromUtf8 (mode)
, QString::fromUtf8 (dx_call)
, QString::fromUtf8 (report), QString::fromUtf8 (tx_mode) , QString::fromUtf8 (report), QString::fromUtf8 (tx_mode)
, tx_enabled, transmitting, decoding, rx_df, tx_df , tx_enabled, transmitting, decoding, rx_df, tx_df
, QString::fromUtf8 (de_call), QString::fromUtf8 (de_grid) , QString::fromUtf8 (de_call), QString::fromUtf8 (de_grid)
@ -296,7 +277,7 @@ void MessageServer::impl::parse_message (QHostAddress const& sender, port_type s
>> message >> low_confidence >> off_air; >> message >> low_confidence >> off_air;
if (check_status (in) != Fail) if (check_status (in) != Fail)
{ {
Q_EMIT self_->decode (is_new, id, time, snr, delta_time, delta_frequency Q_EMIT self_->decode (is_new, client_key, time, snr, delta_time, delta_frequency
, QString::fromUtf8 (mode), QString::fromUtf8 (message) , QString::fromUtf8 (mode), QString::fromUtf8 (message)
, low_confidence, off_air); , low_confidence, off_air);
} }
@ -320,7 +301,7 @@ void MessageServer::impl::parse_message (QHostAddress const& sender, port_type s
>> off_air; >> off_air;
if (check_status (in) != Fail) if (check_status (in) != Fail)
{ {
Q_EMIT self_->WSPR_decode (is_new, id, time, snr, delta_time, frequency, drift Q_EMIT self_->WSPR_decode (is_new, client_key, time, snr, delta_time, frequency, drift
, QString::fromUtf8 (callsign), QString::fromUtf8 (grid) , QString::fromUtf8 (callsign), QString::fromUtf8 (grid)
, power, off_air); , power, off_air);
} }
@ -351,8 +332,10 @@ void MessageServer::impl::parse_message (QHostAddress const& sender, port_type s
>> exchange_sent >> exchange_rcvd >> prop_mode; >> exchange_sent >> exchange_rcvd >> prop_mode;
if (check_status (in) != Fail) if (check_status (in) != Fail)
{ {
Q_EMIT self_->qso_logged (id, time_off, QString::fromUtf8 (dx_call), QString::fromUtf8 (dx_grid) Q_EMIT self_->qso_logged (client_key, time_off, QString::fromUtf8 (dx_call)
, dial_frequency, QString::fromUtf8 (mode), QString::fromUtf8 (report_sent) , QString::fromUtf8 (dx_grid)
, dial_frequency, QString::fromUtf8 (mode)
, QString::fromUtf8 (report_sent)
, QString::fromUtf8 (report_received), QString::fromUtf8 (tx_power) , QString::fromUtf8 (report_received), QString::fromUtf8 (tx_power)
, QString::fromUtf8 (comments), QString::fromUtf8 (name), time_on , QString::fromUtf8 (comments), QString::fromUtf8 (name), time_on
, QString::fromUtf8 (operator_call), QString::fromUtf8 (my_call) , QString::fromUtf8 (operator_call), QString::fromUtf8 (my_call)
@ -363,8 +346,8 @@ void MessageServer::impl::parse_message (QHostAddress const& sender, port_type s
break; break;
case NetworkMessage::Close: case NetworkMessage::Close:
Q_EMIT self_->client_closed (id); Q_EMIT self_->client_closed (client_key);
clients_.remove (id); clients_.remove (client_key);
break; break;
case NetworkMessage::LoggedADIF: case NetworkMessage::LoggedADIF:
@ -373,7 +356,7 @@ void MessageServer::impl::parse_message (QHostAddress const& sender, port_type s
in >> ADIF; in >> ADIF;
if (check_status (in) != Fail) if (check_status (in) != Fail)
{ {
Q_EMIT self_->logged_ADIF (id, ADIF); Q_EMIT self_->logged_ADIF (client_key, ADIF);
} }
} }
break; break;
@ -406,7 +389,7 @@ void MessageServer::impl::tick ()
{ {
if (now > (*iter).last_activity_.addSecs (NetworkMessage::pulse)) if (now > (*iter).last_activity_.addSecs (NetworkMessage::pulse))
{ {
Q_EMIT self_->clear_decodes (iter.key ()); Q_EMIT self_->decodes_cleared (iter.key ());
Q_EMIT self_->client_closed (iter.key ()); Q_EMIT self_->client_closed (iter.key ());
iter = clients_.erase (iter); // safe while iterating as doesn't rehash iter = clients_.erase (iter); // safe while iterating as doesn't rehash
} }
@ -448,152 +431,162 @@ MessageServer::MessageServer (QObject * parent, QString const& version, QString
{ {
} }
void MessageServer::start (port_type port, QHostAddress const& multicast_group_address) void MessageServer::start (port_type port, QHostAddress const& multicast_group_address
, QSet<QString> const& network_interface_names)
{ {
if (port != m_->port_ // qDebug () << "MessageServer::start port:" << port << "multicast addr:" << multicast_group_address.toString () << "network interfaces:" << network_interface_names;
|| multicast_group_address != m_->multicast_group_address_) if (port != m_->localPort ()
|| multicast_group_address != m_->multicast_group_address_
|| network_interface_names != m_->network_interfaces_)
{ {
m_->leave_multicast_group (); m_->leave_multicast_group ();
if (impl::BoundState == m_->state ()) if (impl::UnconnectedState != m_->state ())
{ {
m_->close (); m_->close ();
} }
m_->multicast_group_address_ = multicast_group_address; if (!(multicast_group_address.isNull () || is_multicast_address (multicast_group_address)))
auto address = m_->multicast_group_address_.isNull ()
|| impl::IPv4Protocol != m_->multicast_group_address_.protocol () ? QHostAddress::Any : QHostAddress::AnyIPv4;
if (port && m_->bind (address, port, m_->bind_mode_))
{ {
m_->port_ = port; Q_EMIT error ("Invalid multicast group address");
m_->join_multicast_group (); }
else if (is_MAC_ambiguous_multicast_address (multicast_group_address))
{
Q_EMIT error ("MAC-ambiguous IPv4 multicast group address not supported");
} }
else else
{ {
m_->port_ = 0; m_->multicast_group_address_ = multicast_group_address;
m_->network_interfaces_ = network_interface_names;
QHostAddress local_addr {is_multicast_address (multicast_group_address)
&& impl::IPv4Protocol == multicast_group_address.protocol () ? QHostAddress::AnyIPv4 : QHostAddress::Any};
if (port && m_->bind (local_addr, port, m_->bind_mode_))
{
m_->join_multicast_group ();
}
} }
} }
} }
void MessageServer::clear_decodes (QString const& id, quint8 window) void MessageServer::clear_decodes (ClientKey const& key, quint8 window)
{ {
auto iter = m_->clients_.find (id); auto iter = m_->clients_.find (key);
if (iter != std::end (m_->clients_)) if (iter != std::end (m_->clients_))
{ {
QByteArray message; QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::Clear, id, (*iter).negotiated_schema_number_}; NetworkMessage::Builder out {&message, NetworkMessage::Clear, key.second, (*iter).negotiated_schema_number_};
out << window; out << window;
m_->send_message (out, message, iter.value ().sender_address_, (*iter).sender_port_); m_->send_message (out, message, key.first, (*iter).sender_port_);
} }
} }
void MessageServer::reply (QString const& id, QTime time, qint32 snr, float delta_time void MessageServer::reply (ClientKey const& key, QTime time, qint32 snr, float delta_time
, quint32 delta_frequency, QString const& mode , quint32 delta_frequency, QString const& mode
, QString const& message_text, bool low_confidence, quint8 modifiers) , QString const& message_text, bool low_confidence, quint8 modifiers)
{ {
auto iter = m_->clients_.find (id); auto iter = m_->clients_.find (key);
if (iter != std::end (m_->clients_)) if (iter != std::end (m_->clients_))
{ {
QByteArray message; QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::Reply, id, (*iter).negotiated_schema_number_}; NetworkMessage::Builder out {&message, NetworkMessage::Reply, key.second, (*iter).negotiated_schema_number_};
out << time << snr << delta_time << delta_frequency << mode.toUtf8 () out << time << snr << delta_time << delta_frequency << mode.toUtf8 ()
<< message_text.toUtf8 () << low_confidence << modifiers; << message_text.toUtf8 () << low_confidence << modifiers;
m_->send_message (out, message, iter.value ().sender_address_, (*iter).sender_port_); m_->send_message (out, message, key.first, (*iter).sender_port_);
} }
} }
void MessageServer::replay (QString const& id) void MessageServer::replay (ClientKey const& key)
{ {
auto iter = m_->clients_.find (id); auto iter = m_->clients_.find (key);
if (iter != std::end (m_->clients_)) if (iter != std::end (m_->clients_))
{ {
QByteArray message; QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::Replay, id, (*iter).negotiated_schema_number_}; NetworkMessage::Builder out {&message, NetworkMessage::Replay, key.second, (*iter).negotiated_schema_number_};
m_->send_message (out, message, iter.value ().sender_address_, (*iter).sender_port_); m_->send_message (out, message, key.first, (*iter).sender_port_);
} }
} }
void MessageServer::close (QString const& id) void MessageServer::close (ClientKey const& key)
{ {
auto iter = m_->clients_.find (id); auto iter = m_->clients_.find (key);
if (iter != std::end (m_->clients_)) if (iter != std::end (m_->clients_))
{ {
QByteArray message; QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::Close, id, (*iter).negotiated_schema_number_}; NetworkMessage::Builder out {&message, NetworkMessage::Close, key.second, (*iter).negotiated_schema_number_};
m_->send_message (out, message, iter.value ().sender_address_, (*iter).sender_port_); m_->send_message (out, message, key.first, (*iter).sender_port_);
} }
} }
void MessageServer::halt_tx (QString const& id, bool auto_only) void MessageServer::halt_tx (ClientKey const& key, bool auto_only)
{ {
auto iter = m_->clients_.find (id); auto iter = m_->clients_.find (key);
if (iter != std::end (m_->clients_)) if (iter != std::end (m_->clients_))
{ {
QByteArray message; QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::HaltTx, id, (*iter).negotiated_schema_number_}; NetworkMessage::Builder out {&message, NetworkMessage::HaltTx, key.second, (*iter).negotiated_schema_number_};
out << auto_only; out << auto_only;
m_->send_message (out, message, iter.value ().sender_address_, (*iter).sender_port_); m_->send_message (out, message, key.first, (*iter).sender_port_);
} }
} }
void MessageServer::free_text (QString const& id, QString const& text, bool send) void MessageServer::free_text (ClientKey const& key, QString const& text, bool send)
{ {
auto iter = m_->clients_.find (id); auto iter = m_->clients_.find (key);
if (iter != std::end (m_->clients_)) if (iter != std::end (m_->clients_))
{ {
QByteArray message; QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::FreeText, id, (*iter).negotiated_schema_number_}; NetworkMessage::Builder out {&message, NetworkMessage::FreeText, key.second, (*iter).negotiated_schema_number_};
out << text.toUtf8 () << send; out << text.toUtf8 () << send;
m_->send_message (out, message, iter.value ().sender_address_, (*iter).sender_port_); m_->send_message (out, message, key.first, (*iter).sender_port_);
} }
} }
void MessageServer::location (QString const& id, QString const& loc) void MessageServer::location (ClientKey const& key, QString const& loc)
{ {
auto iter = m_->clients_.find (id); auto iter = m_->clients_.find (key);
if (iter != std::end (m_->clients_)) if (iter != std::end (m_->clients_))
{ {
QByteArray message; QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::Location, id, (*iter).negotiated_schema_number_}; NetworkMessage::Builder out {&message, NetworkMessage::Location, key.second, (*iter).negotiated_schema_number_};
out << loc.toUtf8 (); out << loc.toUtf8 ();
m_->send_message (out, message, iter.value ().sender_address_, (*iter).sender_port_); m_->send_message (out, message, key.first, (*iter).sender_port_);
} }
} }
void MessageServer::highlight_callsign (QString const& id, QString const& callsign void MessageServer::highlight_callsign (ClientKey const& key, QString const& callsign
, QColor const& bg, QColor const& fg, bool last_only) , QColor const& bg, QColor const& fg, bool last_only)
{ {
auto iter = m_->clients_.find (id); auto iter = m_->clients_.find (key);
if (iter != std::end (m_->clients_)) if (iter != std::end (m_->clients_))
{ {
QByteArray message; QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::HighlightCallsign, id, (*iter).negotiated_schema_number_}; NetworkMessage::Builder out {&message, NetworkMessage::HighlightCallsign, key.second, (*iter).negotiated_schema_number_};
out << callsign.toUtf8 () << bg << fg << last_only; out << callsign.toUtf8 () << bg << fg << last_only;
m_->send_message (out, message, iter.value ().sender_address_, (*iter).sender_port_); m_->send_message (out, message, key.first, (*iter).sender_port_);
} }
} }
void MessageServer::switch_configuration (QString const& id, QString const& configuration_name) void MessageServer::switch_configuration (ClientKey const& key, QString const& configuration_name)
{ {
auto iter = m_->clients_.find (id); auto iter = m_->clients_.find (key);
if (iter != std::end (m_->clients_)) if (iter != std::end (m_->clients_))
{ {
QByteArray message; QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::SwitchConfiguration, id, (*iter).negotiated_schema_number_}; NetworkMessage::Builder out {&message, NetworkMessage::SwitchConfiguration, key.second, (*iter).negotiated_schema_number_};
out << configuration_name.toUtf8 (); out << configuration_name.toUtf8 ();
m_->send_message (out, message, iter.value ().sender_address_, (*iter).sender_port_); m_->send_message (out, message, key.first, (*iter).sender_port_);
} }
} }
void MessageServer::configure (QString const& id, QString const& mode, quint32 frequency_tolerance void MessageServer::configure (ClientKey const& key, QString const& mode, quint32 frequency_tolerance
, QString const& submode, bool fast_mode, quint32 tr_period, quint32 rx_df , QString const& submode, bool fast_mode, quint32 tr_period, quint32 rx_df
, QString const& dx_call, QString const& dx_grid, bool generate_messages) , QString const& dx_call, QString const& dx_grid, bool generate_messages)
{ {
auto iter = m_->clients_.find (id); auto iter = m_->clients_.find (key);
if (iter != std::end (m_->clients_)) if (iter != std::end (m_->clients_))
{ {
QByteArray message; QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::Configure, id, (*iter).negotiated_schema_number_}; NetworkMessage::Builder out {&message, NetworkMessage::Configure, key.second, (*iter).negotiated_schema_number_};
out << mode.toUtf8 () << frequency_tolerance << submode.toUtf8 () << fast_mode << tr_period << rx_df out << mode.toUtf8 () << frequency_tolerance << submode.toUtf8 () << fast_mode << tr_period << rx_df
<< dx_call.toUtf8 () << dx_grid.toUtf8 () << generate_messages; << dx_call.toUtf8 () << dx_grid.toUtf8 () << generate_messages;
m_->send_message (out, message, iter.value ().sender_address_, (*iter).sender_port_); m_->send_message (out, message, key.first, (*iter).sender_port_);
} }
} }

View File

@ -2,6 +2,9 @@
#define MESSAGE_SERVER_HPP__ #define MESSAGE_SERVER_HPP__
#include <QObject> #include <QObject>
#include <QPair>
#include <QString>
#include <QSet>
#include <QTime> #include <QTime>
#include <QDateTime> #include <QDateTime>
#include <QHostAddress> #include <QHostAddress>
@ -31,6 +34,7 @@ class UDP_EXPORT MessageServer
public: public:
using port_type = quint16; using port_type = quint16;
using Frequency = Radio::Frequency; using Frequency = Radio::Frequency;
using ClientKey = QPair<QHostAddress, QString>;
MessageServer (QObject * parent = nullptr, MessageServer (QObject * parent = nullptr,
QString const& version = QString {}, QString const& revision = QString {}); QString const& version = QString {}, QString const& revision = QString {});
@ -38,77 +42,77 @@ public:
// start or restart the server, if the multicast_group_address // start or restart the server, if the multicast_group_address
// argument is given it is assumed to be a multicast group address // argument is given it is assumed to be a multicast group address
// which the server will join // which the server will join
Q_SLOT void start (port_type port, Q_SLOT void start (port_type port
QHostAddress const& multicast_group_address = QHostAddress {}); , QHostAddress const& multicast_group_address = QHostAddress {}
, QSet<QString> const& network_interface_names = QSet<QString> {});
// ask the client to clear one or both of the decode windows // ask the client to clear one or both of the decode windows
Q_SLOT void clear_decodes (QString const& id, quint8 window = 0); Q_SLOT void clear_decodes (ClientKey const&, quint8 window = 0);
// ask the client with identification 'id' to make the same action // ask the client with identification 'id' to make the same action
// as a double click on the decode would // as a double click on the decode would
// //
// note that the client is not obliged to take any action and only // note that the client is not obliged to take any action and only
// takes any action if the decode is present and is a CQ or QRZ message // takes any action if the decode is present and is a CQ or QRZ message
Q_SLOT void reply (QString const& id, QTime time, qint32 snr, float delta_time, quint32 delta_frequency Q_SLOT void reply (ClientKey const&, QTime time, qint32 snr, float delta_time, quint32 delta_frequency
, QString const& mode, QString const& message, bool low_confidence, quint8 modifiers); , QString const& mode, QString const& message, bool low_confidence, quint8 modifiers);
// ask the client with identification 'id' to close down gracefully // ask the client to close down gracefully
Q_SLOT void close (QString const& id); Q_SLOT void close (ClientKey const&);
// ask the client with identification 'id' to replay all decodes // ask the client to replay all decodes
Q_SLOT void replay (QString const& id); Q_SLOT void replay (ClientKey const&);
// ask the client with identification 'id' to halt transmitting // ask the client to halt transmitting auto_only just disables auto
// auto_only just disables auto Tx, otherwise halt is immediate // Tx, otherwise halt is immediate
Q_SLOT void halt_tx (QString const& id, bool auto_only); Q_SLOT void halt_tx (ClientKey const&, bool auto_only);
// ask the client with identification 'id' to set the free text // ask the client to set the free text message and optionally send
// message and optionally send it ASAP // it ASAP
Q_SLOT void free_text (QString const& id, QString const& text, bool send); Q_SLOT void free_text (ClientKey const&, QString const& text, bool send);
// ask the client with identification 'id' to set the location provided // ask the client to set the location provided
Q_SLOT void location (QString const& id, QString const& location); Q_SLOT void location (ClientKey const&, QString const& location);
// ask the client with identification 'id' to highlight the callsign // ask the client to highlight the callsign specified with the given
// specified with the given colors // colors
Q_SLOT void highlight_callsign (QString const& id, QString const& callsign Q_SLOT void highlight_callsign (ClientKey const&, QString const& callsign
, QColor const& bg = QColor {}, QColor const& fg = QColor {} , QColor const& bg = QColor {}, QColor const& fg = QColor {}
, bool last_only = false); , bool last_only = false);
// ask the client with identification 'id' to switch to // ask the client to switch to configuration 'configuration_name'
// configuration 'configuration_name' Q_SLOT void switch_configuration (ClientKey const&, QString const& configuration_name);
Q_SLOT void switch_configuration (QString const& id, QString const& configuration_name);
// ask the client with identification 'id' to change configuration // ask the client to change configuration
Q_SLOT void configure (QString const& id, QString const& mode, quint32 frequency_tolerance Q_SLOT void configure (ClientKey const&, QString const& mode, quint32 frequency_tolerance
, QString const& submode, bool fast_mode, quint32 tr_period, quint32 rx_df , QString const& submode, bool fast_mode, quint32 tr_period, quint32 rx_df
, QString const& dx_call, QString const& dx_grid, bool generate_messages); , QString const& dx_call, QString const& dx_grid, bool generate_messages);
// the following signals are emitted when a client broadcasts the // the following signals are emitted when a client broadcasts the
// matching message // matching message
Q_SIGNAL void client_opened (QString const& id, QString const& version, QString const& revision); Q_SIGNAL void client_opened (ClientKey const&, QString const& version, QString const& revision);
Q_SIGNAL void status_update (QString const& id, Frequency, QString const& mode, QString const& dx_call Q_SIGNAL void status_update (ClientKey const&, Frequency, QString const& mode, QString const& dx_call
, QString const& report, QString const& tx_mode, bool tx_enabled , QString const& report, QString const& tx_mode, bool tx_enabled
, bool transmitting, bool decoding, quint32 rx_df, quint32 tx_df , bool transmitting, bool decoding, quint32 rx_df, quint32 tx_df
, QString const& de_call, QString const& de_grid, QString const& dx_grid , QString const& de_call, QString const& de_grid, QString const& dx_grid
, bool watchdog_timeout, QString const& sub_mode, bool fast_mode , bool watchdog_timeout, QString const& sub_mode, bool fast_mode
, quint8 special_op_mode, quint32 frequency_tolerance, quint32 tr_period , quint8 special_op_mode, quint32 frequency_tolerance, quint32 tr_period
, QString const& configuration_name); , QString const& configuration_name);
Q_SIGNAL void client_closed (QString const& id); Q_SIGNAL void client_closed (ClientKey const&);
Q_SIGNAL void decode (bool is_new, QString const& id, QTime time, qint32 snr, float delta_time Q_SIGNAL void decode (bool is_new, ClientKey const&, QTime time, qint32 snr, float delta_time
, quint32 delta_frequency, QString const& mode, QString const& message , quint32 delta_frequency, QString const& mode, QString const& message
, bool low_confidence, bool off_air); , bool low_confidence, bool off_air);
Q_SIGNAL void WSPR_decode (bool is_new, QString const& id, QTime time, qint32 snr, float delta_time, Frequency Q_SIGNAL void WSPR_decode (bool is_new, ClientKey const&, QTime time, qint32 snr, float delta_time, Frequency
, qint32 drift, QString const& callsign, QString const& grid, qint32 power , qint32 drift, QString const& callsign, QString const& grid, qint32 power
, bool off_air); , bool off_air);
Q_SIGNAL void qso_logged (QString const& id, QDateTime time_off, QString const& dx_call, QString const& dx_grid Q_SIGNAL void qso_logged (ClientKey const&, QDateTime time_off, QString const& dx_call, QString const& dx_grid
, Frequency dial_frequency, QString const& mode, QString const& report_sent , Frequency dial_frequency, QString const& mode, QString const& report_sent
, QString const& report_received, QString const& tx_power, QString const& comments , QString const& report_received, QString const& tx_power, QString const& comments
, QString const& name, QDateTime time_on, QString const& operator_call , QString const& name, QDateTime time_on, QString const& operator_call
, QString const& my_call, QString const& my_grid , QString const& my_call, QString const& my_grid
, QString const& exchange_sent, QString const& exchange_rcvd, QString const& prop_mode); , QString const& exchange_sent, QString const& exchange_rcvd, QString const& prop_mode);
Q_SIGNAL void decodes_cleared (QString const& id); Q_SIGNAL void decodes_cleared (ClientKey const&);
Q_SIGNAL void logged_ADIF (QString const& id, QByteArray const& ADIF); Q_SIGNAL void logged_ADIF (ClientKey const&, QByteArray const& ADIF);
// this signal is emitted when a network error occurs // this signal is emitted when a network error occurs
Q_SIGNAL void error (QString const&) const; Q_SIGNAL void error (QString const&) const;
@ -118,4 +122,6 @@ private:
pimpl<impl> m_; pimpl<impl> m_;
}; };
Q_DECLARE_METATYPE (MessageServer::ClientKey);
#endif #endif

View File

@ -17,9 +17,14 @@
#include <iostream> #include <iostream>
#include <exception> #include <exception>
#include <cstdlib>
#include <QCoreApplication> #include <QCoreApplication>
#include <QCommandLineParser> #include <QCommandLineParser>
#include <QCommandLineOption>
#include <QString>
#include <QStringList>
#include <QNetworkInterface>
#include <QDateTime> #include <QDateTime>
#include <QTime> #include <QTime>
#include <QHash> #include <QHash>
@ -38,15 +43,17 @@ class Client
{ {
Q_OBJECT Q_OBJECT
using ClientKey = MessageServer::ClientKey;
public: public:
explicit Client (QString const& id, QObject * parent = nullptr) explicit Client (ClientKey const& key, QObject * parent = nullptr)
: QObject {parent} : QObject {parent}
, id_ {id} , key_ {key}
, dial_frequency_ {0u} , dial_frequency_ {0u}
{ {
} }
Q_SLOT void update_status (QString const& id, Frequency f, QString const& mode, QString const& /*dx_call*/ Q_SLOT void update_status (ClientKey const& key, Frequency f, QString const& mode, QString const& /*dx_call*/
, QString const& /*report*/, QString const& /*tx_mode*/, bool /*tx_enabled*/ , QString const& /*report*/, QString const& /*tx_mode*/, bool /*tx_enabled*/
, bool /*transmitting*/, bool /*decoding*/, qint32 /*rx_df*/, qint32 /*tx_df*/ , bool /*transmitting*/, bool /*decoding*/, qint32 /*rx_df*/, qint32 /*tx_df*/
, QString const& /*de_call*/, QString const& /*de_grid*/, QString const& /*dx_grid*/ , QString const& /*de_call*/, QString const& /*de_grid*/, QString const& /*dx_grid*/
@ -54,77 +61,85 @@ public:
, quint8 /*special_op_mode*/, quint32 /*frequency_tolerance*/, quint32 /*tr_period*/ , quint8 /*special_op_mode*/, quint32 /*frequency_tolerance*/, quint32 /*tr_period*/
, QString const& /*configuration_name*/) , QString const& /*configuration_name*/)
{ {
if (id == id_) if (key == key_)
{ {
if (f != dial_frequency_) if (f != dial_frequency_)
{ {
std::cout << tr ("%1: Dial frequency changed to %2").arg (id_).arg (f).toStdString () << std::endl; std::cout << QString {"%1(%2): "}.arg (key_.second).arg (key_.first.toString ()).toStdString ()
<< QString {"Dial frequency changed to %1"}.arg (f).toStdString () << std::endl;
dial_frequency_ = f; dial_frequency_ = f;
} }
if (mode + sub_mode != mode_) if (mode + sub_mode != mode_)
{ {
std::cout << tr ("%1: Mode changed to %2").arg (id_).arg (mode + sub_mode).toStdString () << std::endl; std::cout << QString {"%1(%2): "}.arg (key_.second).arg (key_.first.toString ()).toStdString ()
<< QString {"Mode changed to %1"}.arg (mode + sub_mode).toStdString () << std::endl;
mode_ = mode + sub_mode; mode_ = mode + sub_mode;
} }
} }
} }
Q_SLOT void decode_added (bool is_new, QString const& client_id, QTime time, qint32 snr Q_SLOT void decode_added (bool is_new, ClientKey const& key, QTime time, qint32 snr
, float delta_time, quint32 delta_frequency, QString const& mode , float delta_time, quint32 delta_frequency, QString const& mode
, QString const& message, bool low_confidence, bool off_air) , QString const& message, bool low_confidence, bool off_air)
{ {
if (client_id == id_) if (key == key_)
{ {
qDebug () << "new:" << is_new << "t:" << time << "snr:" << snr qDebug () << "new:" << is_new << "t:" << time << "snr:" << snr
<< "Dt:" << delta_time << "Df:" << delta_frequency << "Dt:" << delta_time << "Df:" << delta_frequency
<< "mode:" << mode << "Confidence:" << (low_confidence ? "low" : "high") << "mode:" << mode << "Confidence:" << (low_confidence ? "low" : "high")
<< "On air:" << !off_air; << "On air:" << !off_air;
std::cout << tr ("%1: Decoded %2").arg (id_).arg (message).toStdString () << std::endl; std::cout << QString {"%1(%2): "}.arg (key_.second).arg (key_.first.toString ()).toStdString ()
<< QString {"Decoded %1"}.arg (message).toStdString () << std::endl;
} }
} }
Q_SLOT void beacon_spot_added (bool is_new, QString const& client_id, QTime time, qint32 snr Q_SLOT void beacon_spot_added (bool is_new, ClientKey const& key, QTime time, qint32 snr
, float delta_time, Frequency delta_frequency, qint32 drift, QString const& callsign , float delta_time, Frequency delta_frequency, qint32 drift, QString const& callsign
, QString const& grid, qint32 power, bool off_air) , QString const& grid, qint32 power, bool off_air)
{ {
if (client_id == id_) if (key == key_)
{ {
qDebug () << "new:" << is_new << "t:" << time << "snr:" << snr qDebug () << "new:" << is_new << "t:" << time << "snr:" << snr
<< "Dt:" << delta_time << "Df:" << delta_frequency << "Dt:" << delta_time << "Df:" << delta_frequency
<< "drift:" << drift; << "drift:" << drift;
std::cout << tr ("%1: WSPR decode %2 grid %3 power: %4").arg (id_).arg (callsign).arg (grid).arg (power).toStdString () std::cout << QString {"%1(%2): "}.arg (key_.second).arg (key_.first.toString ()).toStdString ()
<< QString {"WSPR decode %1 grid %2 power: %3"}
.arg (callsign).arg (grid).arg (power).toStdString ()
<< "On air:" << !off_air << std::endl; << "On air:" << !off_air << std::endl;
} }
} }
Q_SLOT void qso_logged (QString const&client_id, QDateTime time_off, QString const& dx_call, QString const& dx_grid Q_SLOT void qso_logged (ClientKey const& key, QDateTime time_off, QString const& dx_call, QString const& dx_grid
, Frequency dial_frequency, QString const& mode, QString const& report_sent , Frequency dial_frequency, QString const& mode, QString const& report_sent
, QString const& report_received, QString const& tx_power , QString const& report_received, QString const& tx_power
, QString const& comments, QString const& name, QDateTime time_on , QString const& comments, QString const& name, QDateTime time_on
, QString const& operator_call, QString const& my_call, QString const& my_grid , QString const& operator_call, QString const& my_call, QString const& my_grid
, QString const& exchange_sent, QString const& exchange_rcvd, QString const& prop_mode) , QString const& exchange_sent, QString const& exchange_rcvd, QString const& prop_mode)
{ {
if (client_id == id_) if (key == key_)
{ {
qDebug () << "time_on:" << time_on << "time_off:" << time_off << "dx_call:" << dx_call << "grid:" << dx_grid qDebug () << "time_on:" << time_on << "time_off:" << time_off << "dx_call:"
<< dx_call << "grid:" << dx_grid
<< "freq:" << dial_frequency << "mode:" << mode << "rpt_sent:" << report_sent << "freq:" << dial_frequency << "mode:" << mode << "rpt_sent:" << report_sent
<< "rpt_rcvd:" << report_received << "Tx_pwr:" << tx_power << "comments:" << comments << "rpt_rcvd:" << report_received << "Tx_pwr:" << tx_power << "comments:" << comments
<< "name:" << name << "operator_call:" << operator_call << "my_call:" << my_call << "name:" << name << "operator_call:" << operator_call << "my_call:" << my_call
<< "my_grid:" << my_grid << "exchange_sent:" << exchange_sent << "my_grid:" << my_grid << "exchange_sent:" << exchange_sent
<< "exchange_rcvd:" << exchange_rcvd << "prop_mode:" << prop_mode; << "exchange_rcvd:" << exchange_rcvd << "prop_mode:" << prop_mode;
std::cout << QByteArray {80, '-'}.data () << '\n'; std::cout << QByteArray {80, '-'}.data () << '\n';
std::cout << tr ("%1: Logged %2 grid: %3 power: %4 sent: %5 recd: %6 freq: %7 time_off: %8 op: %9 my_call: %10 my_grid: %11 exchange_sent: %12 exchange_rcvd: %13 comments: %14 prop_mode: %15") std::cout << QString {"%1(%2): "}.arg (key_.second).arg (key_.first.toString ()).toStdString ()
.arg (id_).arg (dx_call).arg (dx_grid).arg (tx_power).arg (report_sent).arg (report_received) << QString {"Logged %1 grid: %2 power: %3 sent: %4 recd: %5 freq: %6 time_off: %7 op: %8 my_call: %9 my_grid: %10 exchange_sent: %11 exchange_rcvd: %12 comments: %13 prop_mode: %14"}
.arg (dial_frequency).arg (time_off.toString("yyyy-MM-dd hh:mm:ss.z")).arg (operator_call) .arg (dx_call).arg (dx_grid).arg (tx_power)
.arg (my_call).arg (my_grid).arg (exchange_sent).arg (exchange_rcvd) .arg (report_sent).arg (report_received)
.arg (comments).arg (prop_mode).toStdString () .arg (dial_frequency).arg (time_off.toString("yyyy-MM-dd hh:mm:ss.z")).arg (operator_call)
.arg (my_call).arg (my_grid).arg (exchange_sent).arg (exchange_rcvd)
.arg (comments).arg (prop_mode).toStdString ()
<< std::endl; << std::endl;
} }
} }
Q_SLOT void logged_ADIF (QString const&client_id, QByteArray const& ADIF) Q_SLOT void logged_ADIF (ClientKey const& key, QByteArray const& ADIF)
{ {
if (client_id == id_) if (key == key_)
{ {
qDebug () << "ADIF:" << ADIF; qDebug () << "ADIF:" << ADIF;
std::cout << QByteArray {80, '-'}.data () << '\n'; std::cout << QByteArray {80, '-'}.data () << '\n';
@ -133,7 +148,7 @@ public:
} }
private: private:
QString id_; ClientKey key_;
Frequency dial_frequency_; Frequency dial_frequency_;
QString mode_; QString mode_;
}; };
@ -143,8 +158,10 @@ class Server
{ {
Q_OBJECT Q_OBJECT
using ClientKey = MessageServer::ClientKey;
public: public:
Server (port_type port, QHostAddress const& multicast_group) Server (port_type port, QHostAddress const& multicast_group, QStringList const& network_interface_names)
: server_ {new MessageServer {this}} : server_ {new MessageServer {this}}
{ {
// connect up server // connect up server
@ -154,21 +171,26 @@ public:
connect (server_, &MessageServer::client_opened, this, &Server::add_client); connect (server_, &MessageServer::client_opened, this, &Server::add_client);
connect (server_, &MessageServer::client_closed, this, &Server::remove_client); connect (server_, &MessageServer::client_closed, this, &Server::remove_client);
server_->start (port, multicast_group); #if QT_VERSION >= QT_VERSION_CHECK (5, 14, 0)
server_->start (port, multicast_group, QSet<QString> {network_interface_names.begin (), network_interface_names.end ()});
#else
server_->start (port, multicast_group, network_interface_names.toSet ());
#endif
} }
private: private:
void add_client (QString const& id, QString const& version, QString const& revision) void add_client (ClientKey const& key, QString const& version, QString const& revision)
{ {
auto client = new Client {id}; auto client = new Client {key};
connect (server_, &MessageServer::status_update, client, &Client::update_status); connect (server_, &MessageServer::status_update, client, &Client::update_status);
connect (server_, &MessageServer::decode, client, &Client::decode_added); connect (server_, &MessageServer::decode, client, &Client::decode_added);
connect (server_, &MessageServer::WSPR_decode, client, &Client::beacon_spot_added); connect (server_, &MessageServer::WSPR_decode, client, &Client::beacon_spot_added);
connect (server_, &MessageServer::qso_logged, client, &Client::qso_logged); connect (server_, &MessageServer::qso_logged, client, &Client::qso_logged);
connect (server_, &MessageServer::logged_ADIF, client, &Client::logged_ADIF); connect (server_, &MessageServer::logged_ADIF, client, &Client::logged_ADIF);
clients_[id] = client; clients_[key] = client;
server_->replay (id); server_->replay (key);
std::cout << "Discovered WSJT-X instance: " << id.toStdString (); std::cout << "Discovered WSJT-X instance: " << key.second.toStdString ()
<< '(' << key.first.toString ().toStdString () << ')';
if (version.size ()) if (version.size ())
{ {
std::cout << " v" << version.toStdString (); std::cout << " v" << version.toStdString ();
@ -180,23 +202,60 @@ private:
std::cout << std::endl; std::cout << std::endl;
} }
void remove_client (QString const& id) void remove_client (ClientKey const& key)
{ {
auto iter = clients_.find (id); auto iter = clients_.find (key);
if (iter != std::end (clients_)) if (iter != std::end (clients_))
{ {
clients_.erase (iter); clients_.erase (iter);
(*iter)->deleteLater (); (*iter)->deleteLater ();
} }
std::cout << "Removed WSJT-X instance: " << id.toStdString () << std::endl; std::cout << "Removed WSJT-X instance: " << key.second.toStdString ()
<< '(' << key.first.toString ().toStdString () << ')' << std::endl;
} }
MessageServer * server_; MessageServer * server_;
// maps client id to clients // maps client key to clients
QHash<QString, Client *> clients_; QHash<ClientKey, Client *> clients_;
}; };
void list_interfaces ()
{
for (auto const& net_if : QNetworkInterface::allInterfaces ())
{
if (net_if.flags () & QNetworkInterface::IsUp)
{
std::cout << net_if.humanReadableName ().toStdString () << ":\n"
" id: " << net_if.name ().toStdString () << " (" << net_if.index () << ")\n"
" addr: " << net_if.hardwareAddress ().toStdString () << "\n"
" flags: ";
if (net_if.flags () & QNetworkInterface::IsRunning)
{
std::cout << "Running ";
}
if (net_if.flags () & QNetworkInterface::CanBroadcast)
{
std::cout << "Broadcast ";
}
if (net_if.flags () & QNetworkInterface::CanMulticast)
{
std::cout << "Multicast ";
}
if (net_if.flags () & QNetworkInterface::IsLoopBack)
{
std::cout << "Loop-back ";
}
std::cout << "\n addresses:\n";
for (auto const& ae : net_if.addressEntries ())
{
std::cout << " " << ae.ip ().toString ().toStdString () << '\n';
}
std::cout << '\n';
}
}
}
#include "UDPDaemon.moc" #include "UDPDaemon.moc"
int main (int argc, char * argv[]) int main (int argc, char * argv[])
@ -217,6 +276,11 @@ int main (int argc, char * argv[])
auto help_option = parser.addHelpOption (); auto help_option = parser.addHelpOption ();
auto version_option = parser.addVersionOption (); auto version_option = parser.addVersionOption ();
QCommandLineOption list_option (QStringList {"l", "list-interfaces"},
app.translate ("UDPDaemon",
"Print the available network interfaces."));
parser.addOption (list_option);
QCommandLineOption port_option (QStringList {"p", "port"}, QCommandLineOption port_option (QStringList {"p", "port"},
app.translate ("UDPDaemon", app.translate ("UDPDaemon",
"Where <PORT> is the UDP service port number to listen on.\n" "Where <PORT> is the UDP service port number to listen on.\n"
@ -232,9 +296,25 @@ int main (int argc, char * argv[])
app.translate ("UDPDaemon", "GROUP")); app.translate ("UDPDaemon", "GROUP"));
parser.addOption (multicast_addr_option); parser.addOption (multicast_addr_option);
QCommandLineOption network_interface_option (QStringList {"i", "network-interface"},
app.translate ("UDPDaemon",
"Where <INTERFACE> is the network interface name to join on.\n"
"This option can be passed more than once to specify multiple network interfaces\n"
"The default is use just the loop back interface."),
app.translate ("UDPDaemon", "INTERFACE"));
parser.addOption (network_interface_option);
parser.process (app); parser.process (app);
Server server {static_cast<port_type> (parser.value (port_option).toUInt ()), QHostAddress {parser.value (multicast_addr_option)}}; if (parser.isSet (list_option))
{
list_interfaces ();
return EXIT_SUCCESS;
}
Server server {static_cast<port_type> (parser.value (port_option).toUInt ())
, QHostAddress {parser.value (multicast_addr_option).trimmed ()}
, parser.values (network_interface_option)};
return app.exec (); return app.exec ();
} }

BIN
lib/fsk4hf/.DS_Store vendored

Binary file not shown.

View File

@ -117,6 +117,42 @@ namespace std
} }
#endif #endif
inline
bool is_broadcast_address (QHostAddress const& host_addr)
{
#if QT_VERSION >= QT_VERSION_CHECK (5, 11, 0)
return host_addr.isBroadcast ();
#else
bool ok;
return host_addr.toIPv4Address (&ok) == 0xffffffffu && ok;
#endif
}
inline
bool is_multicast_address (QHostAddress const& host_addr)
{
#if QT_VERSION >= QT_VERSION_CHECK (5, 6, 0)
return host_addr.isMulticast ();
#else
bool ok;
return (((host_addr.toIPv4Address (&ok) & 0xf0000000u) == 0xe0000000u) && ok)
|| host_addr.toIPv6Address ()[0] == 0xff;
#endif
}
inline
bool is_MAC_ambiguous_multicast_address (QHostAddress const& host_addr)
{
// sub-ranges 224.128.0.0/24, 225.0.0.0/24, 225.128.0.0/24,
// 226.0.0.0/24, 226.128.0.0/24, ..., 239.0.0.0/24, 239.128.0.0/24
// are not supported as they are inefficient due to ambiguous
// mappings to Ethernet MAC addresses. 224.0.0.0/24 alone is allowed
// from these ranges
bool ok;
auto ipv4 = host_addr.toIPv4Address (&ok);
return ok && !((ipv4 & 0xffffff00u) == 0xe0000000) && (ipv4 & 0xf07fff00) == 0xe0000000;
}
// Register some useful Qt types with QMetaType // Register some useful Qt types with QMetaType
Q_DECLARE_METATYPE (QHostAddress); Q_DECLARE_METATYPE (QHostAddress);

View File

@ -131,6 +131,80 @@ private:
QDateTime dt {QDate {2020, 8, 6}, QTime {14, 15, 22, 501}}; QDateTime dt {QDate {2020, 8, 6}, QTime {14, 15, 22, 501}};
QCOMPARE (qt_truncate_date_time_to (dt, 7500), QDateTime (QDate (2020, 8, 6), QTime (14, 15, 22, 500))); QCOMPARE (qt_truncate_date_time_to (dt, 7500), QDateTime (QDate (2020, 8, 6), QTime (14, 15, 22, 500)));
} }
Q_SLOT void is_multicast_address_data ()
{
QTest::addColumn<QString> ("addr");
QTest::addColumn<bool> ("result");
QTest::newRow ("loopback") << "127.0.0.1" << false;
QTest::newRow ("looback IPv6") << "::1" << false;
QTest::newRow ("lowest-") << "223.255.255.255" << false;
QTest::newRow ("lowest") << "224.0.0.0" << true;
QTest::newRow ("lowest- IPv6") << "feff:ffff:ffff:ffff:ffff:ffff:ffff:ffff" << false;
QTest::newRow ("lowest IPv6") << "ff00::" << true;
QTest::newRow ("highest") << "239.255.255.255" << true;
QTest::newRow ("highest+") << "240.0.0.0" << false;
QTest::newRow ("highest IPv6") << "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff" << true;
}
Q_SLOT void is_multicast_address ()
{
QFETCH (QString, addr);
QFETCH (bool, result);
QCOMPARE (::is_multicast_address (QHostAddress {addr}), result);
}
Q_SLOT void is_MAC_ambiguous_multicast_address_data ()
{
QTest::addColumn<QString> ("addr");
QTest::addColumn<bool> ("result");
QTest::newRow ("loopback") << "127.0.0.1" << false;
QTest::newRow ("looback IPv6") << "::1" << false;
QTest::newRow ("lowest- R1") << "223.255.255.255" << false;
QTest::newRow ("lowest R1") << "224.0.0.0" << false;
QTest::newRow ("highest R1") << "224.0.0.255" << false;
QTest::newRow ("highest+ R1") << "224.0.1.0" << false;
QTest::newRow ("lowest- R1A") << "224.127.255.255" << false;
QTest::newRow ("lowest R1A") << "224.128.0.0" << true;
QTest::newRow ("highest R1A") << "224.128.0.255" << true;
QTest::newRow ("highest+ R1A") << "224.128.1.0" << false;
QTest::newRow ("lowest- R2") << "224.255.255.255" << false;
QTest::newRow ("lowest R2") << "225.0.0.0" << true;
QTest::newRow ("highest R2") << "225.0.0.255" << true;
QTest::newRow ("highest+ R2") << "225.0.1.0" << false;
QTest::newRow ("lowest- R2A") << "225.127.255.255" << false;
QTest::newRow ("lowest R2A") << "225.128.0.0" << true;
QTest::newRow ("highest R2A") << "225.128.0.255" << true;
QTest::newRow ("highest+ R2A") << "225.128.1.0" << false;
QTest::newRow ("lowest- R3") << "238.255.255.255" << false;
QTest::newRow ("lowest R3") << "239.0.0.0" << true;
QTest::newRow ("highest R3") << "239.0.0.255" << true;
QTest::newRow ("highest+ R3") << "239.0.1.0" << false;
QTest::newRow ("lowest- R3A") << "239.127.255.255" << false;
QTest::newRow ("lowest R3A") << "239.128.0.0" << true;
QTest::newRow ("highest R3A") << "239.128.0.255" << true;
QTest::newRow ("highest+ R3A") << "239.128.1.0" << false;
QTest::newRow ("lowest- IPv6") << "feff:ffff:ffff:ffff:ffff:ffff:ffff:ffff" << false;
QTest::newRow ("lowest IPv6") << "ff00::" << false;
QTest::newRow ("highest") << "239.255.255.255" << false;
QTest::newRow ("highest+") << "240.0.0.0" << false;
QTest::newRow ("highest IPv6") << "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff" << false;
}
Q_SLOT void is_MAC_ambiguous_multicast_address ()
{
QFETCH (QString, addr);
QFETCH (bool, result);
QCOMPARE (::is_MAC_ambiguous_multicast_address (QHostAddress {addr}), result);
}
}; };
QTEST_MAIN (TestQtHelpers); QTEST_MAIN (TestQtHelpers);

View File

@ -0,0 +1,93 @@
#include "CheckableItemComboBox.hpp"
#include <QStyledItemDelegate>
#include <QStandardItemModel>
#include <QStandardItem>
#include <QLineEdit>
#include <QEvent>
#include <QListView>
class CheckableItemComboBoxStyledItemDelegate
: public QStyledItemDelegate
{
public:
explicit CheckableItemComboBoxStyledItemDelegate (QObject * parent = nullptr)
: QStyledItemDelegate {parent}
{
}
void paint (QPainter * painter, QStyleOptionViewItem const& option, QModelIndex const& index) const override
{
QStyleOptionViewItem& mutable_option = const_cast<QStyleOptionViewItem&> (option);
mutable_option.showDecorationSelected = false;
QStyledItemDelegate::paint (painter, mutable_option, index);
}
};
CheckableItemComboBox::CheckableItemComboBox (QWidget * parent)
: LazyFillComboBox {parent}
, model_ {new QStandardItemModel()}
{
setModel (model_.data ());
setEditable (true);
lineEdit ()->setReadOnly (true);
lineEdit ()->installEventFilter (this);
setItemDelegate (new CheckableItemComboBoxStyledItemDelegate {this});
connect (lineEdit(), &QLineEdit::selectionChanged, lineEdit(), &QLineEdit::deselect);
connect (static_cast<QListView *> (view ()), &QListView::pressed, this, &CheckableItemComboBox::item_pressed);
connect (model_.data (), &QStandardItemModel::dataChanged, this, &CheckableItemComboBox::model_data_changed);
}
QStandardItem * CheckableItemComboBox::addCheckItem (QString const& label, QVariant const& data
, Qt::CheckState checkState)
{
auto * item = new QStandardItem {label};
item->setCheckState (checkState);
item->setData (data);
item->setFlags (Qt::ItemIsUserCheckable | Qt::ItemIsEnabled);
model_->appendRow (item);
update_text ();
return item;
}
bool CheckableItemComboBox::eventFilter (QObject * object, QEvent * event)
{
if (object == lineEdit() && event->type () == QEvent::MouseButtonPress)
{
showPopup();
return true;
}
return false;
}
void CheckableItemComboBox::update_text()
{
QString text;
for (int i = 0; i < model_->rowCount (); ++i)
{
if (model_->item (i)->checkState () == Qt::Checked)
{
if (text.size ())
{
text+= ", ";
}
text += model_->item (i)->data ().toString ();
}
}
lineEdit ()->setText (text);
}
void CheckableItemComboBox::model_data_changed ()
{
update_text ();
}
void CheckableItemComboBox::item_pressed (QModelIndex const& index)
{
QStandardItem * item = model_->itemFromIndex (index);
item->setCheckState (item->checkState () == Qt::Checked ? Qt::Unchecked : Qt::Checked);
}
#include "widgets/moc_CheckableItemComboBox.cpp"

View File

@ -0,0 +1,37 @@
#ifndef CHECKABLE_ITEM_COMBO_BOX_HPP__
#define CHECKABLE_ITEM_COMBO_BOX_HPP__
#include <QScopedPointer>
#include "LazyFillComboBox.hpp"
class QStandardItemModel;
class QStandardItem;
/**
* @brief QComboBox with support of checkboxes
* http://stackoverflow.com/questions/8422760/combobox-of-checkboxes
*/
class CheckableItemComboBox
: public LazyFillComboBox
{
Q_OBJECT
public:
explicit CheckableItemComboBox (QWidget * parent = nullptr);
QStandardItem * addCheckItem (QString const& label, QVariant const& data, Qt::CheckState checkState);
protected:
bool eventFilter (QObject *, QEvent *) override;
private:
void update_text();
Q_SLOT void model_data_changed ();
Q_SLOT void item_pressed (QModelIndex const&);
private:
QScopedPointer<QStandardItemModel> model_;
};
#endif

View File

@ -10,7 +10,7 @@ class QWidget;
// //
// QComboBox derivative that signals show and hide of the pop up list. // QComboBox derivative that signals show and hide of the pop up list.
// //
class LazyFillComboBox final class LazyFillComboBox
: public QComboBox : public QComboBox
{ {
Q_OBJECT Q_OBJECT

View File

@ -417,6 +417,7 @@ MainWindow::MainWindow(QDir const& temp_directory, bool multiple,
m_messageClient {new MessageClient {QApplication::applicationName (), m_messageClient {new MessageClient {QApplication::applicationName (),
version (), revision (), version (), revision (),
m_config.udp_server_name (), m_config.udp_server_port (), m_config.udp_server_name (), m_config.udp_server_port (),
m_config.udp_interface_names (), m_config.udp_TTL (),
this}}, this}},
m_psk_Reporter {&m_config, QString {"WSJT-X v" + version () + " " + m_revision}.simplified ()}, m_psk_Reporter {&m_config, QString {"WSJT-X v" + version () + " " + m_revision}.simplified ()},
m_manual {&m_network_manager}, m_manual {&m_network_manager},
@ -785,6 +786,7 @@ MainWindow::MainWindow(QDir const& temp_directory, bool multiple,
connect (&m_config, &Configuration::transceiver_failure, this, &MainWindow::handle_transceiver_failure); 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_changed, m_messageClient, &MessageClient::set_server);
connect (&m_config, &Configuration::udp_server_port_changed, m_messageClient, &MessageClient::set_server_port); connect (&m_config, &Configuration::udp_server_port_changed, m_messageClient, &MessageClient::set_server_port);
connect (&m_config, &Configuration::udp_TTL_changed, m_messageClient, &MessageClient::set_TTL);
connect (&m_config, &Configuration::accept_udp_requests_changed, m_messageClient, &MessageClient::enable); connect (&m_config, &Configuration::accept_udp_requests_changed, m_messageClient, &MessageClient::enable);
connect (&m_config, &Configuration::enumerating_audio_devices, [this] () { connect (&m_config, &Configuration::enumerating_audio_devices, [this] () {
showStatusMessage (tr ("Enumerating audio devices")); showStatusMessage (tr ("Enumerating audio devices"));
@ -7835,7 +7837,7 @@ void MainWindow::networkError (QString const& e)
, MessageBox::Cancel)) , MessageBox::Cancel))
{ {
// retry server lookup // retry server lookup
m_messageClient->set_server (m_config.udp_server_name ()); m_messageClient->set_server (m_config.udp_server_name (), m_config.udp_interface_names ());
} }
} }

View File

@ -10,7 +10,9 @@ SOURCES += \
widgets/AbstractLogWindow.cpp \ widgets/AbstractLogWindow.cpp \
widgets/FrequencyLineEdit.cpp widgets/FrequencyDeltaLineEdit.cpp \ widgets/FrequencyLineEdit.cpp widgets/FrequencyDeltaLineEdit.cpp \
widgets/FoxLogWindow.cpp widgets/CabrilloLogWindow.cpp \ widgets/FoxLogWindow.cpp widgets/CabrilloLogWindow.cpp \
widgets/HelpTextWindow.cpp widgets/RestrictedSpinBox.cpp widgets/HelpTextWindow.cpp widgets/RestrictedSpinBox.cpp \
widgets/LazyFillComboBox.cpp widgets/CheckableItemComboBox.cpp
HEADERS += \ HEADERS += \
widgets/mainwindow.h widgets/plotter.h \ widgets/mainwindow.h widgets/plotter.h \
widgets/about.h widgets/widegraph.h \ widgets/about.h widgets/widegraph.h \
@ -22,7 +24,8 @@ HEADERS += \
widgets/ExportCabrillo.h widgets/AbstractLogWindow.hpp \ widgets/ExportCabrillo.h widgets/AbstractLogWindow.hpp \
widgets/FoxLogWindow.hpp widgets/CabrilloLogWindow.hpp \ widgets/FoxLogWindow.hpp widgets/CabrilloLogWindow.hpp \
widgets/DateTimeEdit.hpp widgets/HelpTextWindow.hpp \ widgets/DateTimeEdit.hpp widgets/HelpTextWindow.hpp \
widgets/RestrictedSpinBox.hpp widgets/RestrictedSpinBox.hpp \
widgets/LazyFillComboBox.hpp widgets/CheckableItemComboBox.hpp
FORMS += \ FORMS += \
widgets/mainwindow.ui widgets/about.ui \ widgets/mainwindow.ui widgets/about.ui \