Send status information to UDP server

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

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

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

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

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

git-svn-id: svn+ssh://svn.code.sf.net/p/wsjt/wsjt/branches/wsjtx@5225 ab8295b8-cf94-4d9e-aec4-7959e3be5d79
This commit is contained in:
Bill Somerville 2015-04-15 16:40:49 +00:00
parent bbc359d043
commit c5c6feb41c
19 changed files with 1933 additions and 113 deletions

View File

@ -198,6 +198,8 @@ set (wsjt_qt_CXXSRCS
HamlibTransceiver.cpp
HRDTransceiver.cpp
DXLabSuiteCommanderTransceiver.cpp
NetworkMessage.cpp
MessageClient.cpp
)
set (jt9_CXXSRCS
@ -372,11 +374,17 @@ set (wsjtx_UISRCS
Configuration.ui
)
set (message_aggregator_CXXSRCS
MessageServer.cpp
MessageAggregator.cpp
)
set (all_CXXSRCS
${wsjt_CXXSRCS}
${wsjt_qt_CXXSRCS}
${jt9_CXXSRCS}
${wsjtx_CXXSRCS}
${message_aggregator_CXXSRCS}
)
set (all_C_and_CXXSRCS
@ -850,6 +858,17 @@ set_target_properties (wsjtx PROPERTIES
target_link_libraries (wsjtx wsjt_fort wsjt_cxx wsjt_qt ${hamlib_LIBRARIES} ${FFTW3_LIBRARIES})
qt5_use_modules (wsjtx Widgets OpenGL Network Multimedia SerialPort)
add_executable (message_aggregator
${message_aggregator_CXXSRCS}
wsjtx.rc
)
target_link_libraries (message_aggregator wsjt_qt)
qt5_use_modules (message_aggregator Widgets OpenGL Network)
if (WSJT_CREATE_WINMAIN)
set_target_properties (message_aggregator PROPERTIES WIN32_EXECUTABLE ON)
endif (WSJT_CREATE_WINMAIN)
set (SYSTEM_NAME ${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR})
if (WIN32)
set (SYSTEM_NAME "${SYSTEM_NAME}i386")
@ -887,7 +906,7 @@ install (TARGETS wsjtx
BUNDLE DESTINATION . COMPONENT runtime
)
install (TARGETS jt9 jt65code jt9code
install (TARGETS jt9 jt65code jt9code message_aggregator
RUNTIME DESTINATION ${WSJT_BIN_DESTINATION} COMPONENT runtime
BUNDLE DESTINATION ${WSJT_BIN_DESTINATION} COMPONENT runtime
)

View File

@ -131,6 +131,7 @@
#include <iterator>
#include <algorithm>
#include <functional>
#include <limits>
#include <QApplication>
#include <QMetaType>
@ -148,6 +149,7 @@
#include <QStringListModel>
#include <QLineEdit>
#include <QRegExpValidator>
#include <QIntValidator>
#include <QThread>
#include <QTimer>
#include <QStandardPaths>
@ -169,6 +171,7 @@
#include "Bands.hpp"
#include "FrequencyList.hpp"
#include "StationList.hpp"
#include "NetworkServerLookup.hpp"
#include "pimpl_impl.hpp"
@ -374,6 +377,7 @@ class Configuration::impl final
public:
using FrequencyDelta = Radio::FrequencyDelta;
using port_type = Configuration::port_type;
explicit impl (Configuration * self, QSettings * settings, QWidget * parent);
~impl ();
@ -562,6 +566,11 @@ private:
bool disable_TX_on_73_;
bool watchdog_;
bool TX_messages_;
QString udp_server_name_;
port_type udp_server_port_;
bool accept_udp_requests_;
bool udpWindowToFront_;
bool udpWindowRestore_;
DataMode data_mode_;
QAudioDeviceInfo audio_input_device_;
@ -631,6 +640,11 @@ bool Configuration::split_mode () const
{
return !m_->rig_is_dummy_ && m_->rig_params_.split_mode_ != TransceiverFactory::split_mode_none;
}
QString Configuration::udp_server_name () const {return m_->udp_server_name_;}
auto Configuration::udp_server_port () const -> port_type {return m_->udp_server_port_;}
bool Configuration::accept_udp_requests () const {return m_->accept_udp_requests_;}
bool Configuration::udpWindowToFront () const {return m_->udpWindowToFront_;}
bool Configuration::udpWindowRestore () const {return m_->udpWindowRestore_;}
Bands * Configuration::bands () {return &m_->bands_;}
StationList * Configuration::stations () {return &m_->stations_;}
FrequencyList * Configuration::frequencies () {return &m_->frequencies_;}
@ -846,6 +860,9 @@ Configuration::impl::impl (Configuration * self, QSettings * settings, QWidget *
ui_->grid_line_edit->setValidator (new QRegExpValidator {QRegExp {"[A-Ra-r]{2,2}[0-9]{2,2}[A-Xa-x]{0,2}"}, this});
ui_->add_macro_line_edit->setValidator (new QRegExpValidator {message_alphabet, this});
ui_->udp_server_port_spin_box->setMinimum (1);
ui_->udp_server_port_spin_box->setMaximum (std::numeric_limits<port_type>::max ());
//
// assign ids to radio buttons
//
@ -1035,6 +1052,11 @@ void Configuration::impl::initialise_models ()
ui_->CAT_RTS_check_box->setChecked (rig_params_.CAT_RTS_high_);
ui_->TX_audio_source_button_group->button (rig_params_.TX_audio_source_)->setChecked (true);
ui_->CAT_poll_interval_spin_box->setValue (rig_params_.CAT_poll_interval_);
ui_->udp_server_line_edit->setText (udp_server_name_);
ui_->udp_server_port_spin_box->setValue (udp_server_port_);
ui_->accept_udp_requests_check_box->setChecked (accept_udp_requests_);
ui_->udpWindowToFront->setChecked(udpWindowToFront_);
ui_->udpWindowRestore->setChecked(udpWindowRestore_);
if (rig_params_.PTT_port_.isEmpty ())
{
@ -1211,6 +1233,11 @@ void Configuration::impl::read_settings ()
TX_messages_ = settings_->value ("Tx2QSO", false).toBool ();
rig_params_.CAT_poll_interval_ = settings_->value ("Polling", 0).toInt ();
rig_params_.split_mode_ = settings_->value ("SplitMode", QVariant::fromValue (TransceiverFactory::split_mode_none)).value<TransceiverFactory::SplitMode> ();
udp_server_name_ = settings_->value ("UDPServer", "localhost").toString ();
udp_server_port_ = settings_->value ("UDPServerPort", 2237).toUInt ();
accept_udp_requests_ = settings_->value ("AcceptUDPRequests", false).toBool ();
udpWindowToFront_ = settings_->value ("udpWindowToFront",false).toBool ();
udpWindowRestore_ = settings_->value ("udpWindowRestore",false).toBool ();
}
void Configuration::impl::write_settings ()
@ -1288,6 +1315,11 @@ void Configuration::impl::write_settings ()
settings_->setValue ("TXAudioSource", QVariant::fromValue (rig_params_.TX_audio_source_));
settings_->setValue ("Polling", rig_params_.CAT_poll_interval_);
settings_->setValue ("SplitMode", QVariant::fromValue (rig_params_.split_mode_));
settings_->setValue ("UDPServer", udp_server_name_);
settings_->setValue ("UDPServerPort", udp_server_port_);
settings_->setValue ("AcceptUDPRequests", accept_udp_requests_);
settings_->setValue ("udpWindowToFront", udpWindowToFront_);
settings_->setValue ("udpWindowRestore", udpWindowRestore_);
}
void Configuration::impl::set_rig_invariants ()
@ -1639,6 +1671,24 @@ void Configuration::impl::accept ()
data_mode_ = static_cast<DataMode> (ui_->TX_mode_button_group->checkedId ());
save_directory_ = ui_->save_path_display_label->text ();
auto new_server = ui_->udp_server_line_edit->text ();
if (new_server != udp_server_name_)
{
udp_server_name_ = new_server;
Q_EMIT self_->udp_server_changed (new_server);
}
auto new_port = ui_->udp_server_port_spin_box->value ();
if (new_port != udp_server_port_)
{
udp_server_port_ = new_port;
Q_EMIT self_->udp_server_port_changed (new_port);
}
accept_udp_requests_ = ui_->accept_udp_requests_check_box->isChecked ();
udpWindowToFront_ = ui_->udpWindowToFront->isChecked ();
udpWindowRestore_ = ui_->udpWindowRestore->isChecked ();
if (macros_.stringList () != next_macros_.stringList ())
{
macros_.setStringList (next_macros_.stringList ());
@ -2426,7 +2476,6 @@ void Configuration::impl::fill_port_combo_box (QComboBox * cb)
cb->setEditText (current_text);
}
inline
bool operator != (RigParams const& lhs, RigParams const& rhs)
{

View File

@ -19,6 +19,7 @@ class Bands;
class FrequencyList;
class StationList;
class QStringListModel;
class QHostAddress;
//
// Class Configuration
@ -60,6 +61,7 @@ public:
using MODE = Transceiver::MODE;
using TransceiverState = Transceiver::TransceiverState;
using Frequency = Radio::Frequency;
using port_type = quint16;
enum DataMode {data_mode_none, data_mode_USB, data_mode_data};
enum Type2MsgGen {type_2_msg_1_full, type_2_msg_3_full, type_2_msg_5_only};
@ -105,6 +107,12 @@ public:
bool watchdog () const;
bool TX_messages () const;
bool split_mode () const;
bool post_decodes () const;
QString udp_server_name () const;
port_type udp_server_port () const;
bool accept_udp_requests () const;
bool udpWindowToFront () const;
bool udpWindowRestore () const;
Bands * bands ();
FrequencyList * frequencies ();
StationList * stations ();
@ -166,6 +174,12 @@ public:
//
Q_SIGNAL void decoded_text_font_changed (QFont);
//
// This signal is emitted when the UDP server changes
//
Q_SIGNAL void udp_server_changed (QString const& udp_server);
Q_SIGNAL void udp_server_port_changed (port_type server_port);
//
// These signals are emitted and reflect transceiver state changes

View File

@ -267,16 +267,6 @@
<string>Behavior</string>
</property>
<layout class="QGridLayout" name="gridLayout_8">
<item row="3" column="0">
<widget class="QCheckBox" name="watchdog_check_box">
<property name="toolTip">
<string>Stop transmitting automatically after five periods.</string>
</property>
<property name="text">
<string>Runaway Tx &amp;watchdog</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="disable_TX_on_73_check_box">
<property name="toolTip">
@ -288,6 +278,16 @@ text message.</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="watchdog_check_box">
<property name="toolTip">
<string>Stop transmitting automatically after five periods.</string>
</property>
<property name="text">
<string>Runaway Tx &amp;watchdog</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="quick_call_check_box">
<property name="toolTip">
@ -311,7 +311,7 @@ text message.</string>
</property>
</widget>
</item>
<item row="4" column="0" colspan="2">
<item row="5" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_9">
<item>
<widget class="QCheckBox" name="CW_id_after_73_check_box">
@ -370,16 +370,6 @@ quiet period when decoding is done.</string>
</item>
</layout>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="tx_QSY_check_box">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Some rigs are not able to process CAT commands while transmitting. This means that if you are operating in split mode you may have to uncheck this option.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Allow Tx frequency changes while transmitting</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="monitor_last_used_check_box">
<property name="toolTip">
@ -390,6 +380,16 @@ quiet period when decoding is done.</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="tx_QSY_check_box">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Some rigs are not able to process CAT commands while transmitting. This means that if you are operating in split mode you may have to uncheck this option.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Allow Tx frequency changes while transmitting</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@ -1627,10 +1627,10 @@ and DX Grid fields when a 73 or free text message is sent.</string>
<item>
<widget class="QGroupBox" name="network_group_box">
<property name="title">
<string>Network</string>
<string>Network Services</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_7">
<item>
<layout class="QGridLayout" name="gridLayout_17">
<item row="0" column="0">
<widget class="QCheckBox" name="psk_reporter_check_box">
<property name="toolTip">
<string>The program can send your station details and all
@ -1646,6 +1646,105 @@ for assessing propagation and system performance.</string>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_4">
<property name="title">
<string>UDP Server</string>
</property>
<layout class="QGridLayout" name="gridLayout_16">
<item row="0" column="0">
<layout class="QFormLayout" name="formLayout_6">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="udp_server_label">
<property name="text">
<string>UDP Server:</string>
</property>
<property name="buddy">
<cstring>udp_server_line_edit</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<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">
<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 name="inputMethodHints">
<set>Qt::ImhDigitsOnly</set>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>UDP Server port number:</string>
</property>
<property name="buddy">
<cstring>udp_server_port_spin_box</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="udp_server_port_spin_box">
<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>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>65534</number>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="1">
<layout class="QVBoxLayout" name="verticalLayout_11">
<item>
<widget class="QCheckBox" name="accept_udp_requests_check_box">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;With this enabled WSJT-X will accept certain requests back from a UDP server that receives decode messages.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Accept UDP requests</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="udpWindowToFront">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Indicate acceptance of an incoming UDP request. The effect of this option varies depending on the operating system and window manager, its intent is to notify the acceptance of an incoming UDP request even if this application is minimized or hidden.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Notify on accepted UDP request</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="udpWindowRestore">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Restore the window from minimized if an UDP request is accepted.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Accepted UDP request restores window</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_4">
<property name="orientation">
@ -1986,7 +2085,6 @@ soundcard changes</string>
<tabstop>decoded_text_font_push_button</tabstop>
<tabstop>monitor_off_check_box</tabstop>
<tabstop>monitor_last_used_check_box</tabstop>
<tabstop>tx_QSY_check_box</tabstop>
<tabstop>quick_call_check_box</tabstop>
<tabstop>tx_QSY_check_box</tabstop>
<tabstop>disable_TX_on_73_check_box</tabstop>
@ -2038,6 +2136,11 @@ soundcard changes</string>
<tabstop>report_in_comments_check_box</tabstop>
<tabstop>clear_DX_check_box</tabstop>
<tabstop>psk_reporter_check_box</tabstop>
<tabstop>udp_server_line_edit</tabstop>
<tabstop>udp_server_port_spin_box</tabstop>
<tabstop>accept_udp_requests_check_box</tabstop>
<tabstop>udpWindowToFront</tabstop>
<tabstop>udpWindowRestore</tabstop>
<tabstop>frequencies_table_view</tabstop>
<tabstop>stations_table_view</tabstop>
<tabstop>pbCQmsg</tabstop>
@ -2055,8 +2158,8 @@ soundcard changes</string>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>236</x>
<y>540</y>
<x>245</x>
<y>605</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
@ -2071,8 +2174,8 @@ soundcard changes</string>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>304</x>
<y>540</y>
<x>313</x>
<y>605</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
@ -2080,22 +2183,6 @@ soundcard changes</string>
</hint>
</hints>
</connection>
<connection>
<sender>add_macro_line_edit</sender>
<signal>returnPressed()</signal>
<receiver>add_macro_push_button</receiver>
<slot>setFocus()</slot>
<hints>
<hint type="sourcelabel">
<x>188</x>
<y>62</y>
</hint>
<hint type="destinationlabel">
<x>406</x>
<y>62</y>
</hint>
</hints>
</connection>
<connection>
<sender>add_macro_push_button</sender>
<signal>clicked()</signal>
@ -2103,11 +2190,27 @@ soundcard changes</string>
<slot>setFocus()</slot>
<hints>
<hint type="sourcelabel">
<x>406</x>
<x>576</x>
<y>62</y>
</hint>
<hint type="destinationlabel">
<x>188</x>
<x>199</x>
<y>60</y>
</hint>
</hints>
</connection>
<connection>
<sender>add_macro_line_edit</sender>
<signal>returnPressed()</signal>
<receiver>add_macro_push_button</receiver>
<slot>setFocus()</slot>
<hints>
<hint type="sourcelabel">
<x>199</x>
<y>60</y>
</hint>
<hint type="destinationlabel">
<x>576</x>
<y>62</y>
</hint>
</hints>

462
MessageAggregator.cpp Normal file
View File

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

283
MessageClient.cpp Normal file
View File

@ -0,0 +1,283 @@
#include "MessageClient.hpp"
#include <QUdpSocket>
#include <QHostInfo>
#include <QTimer>
#include "NetworkMessage.hpp"
#include "pimpl_impl.hpp"
#include "moc_MessageClient.cpp"
class MessageClient::impl
: public QUdpSocket
{
Q_OBJECT;
public:
impl (QString const& id, port_type server_port, MessageClient * self)
: self_ {self}
, id_ {id}
, server_port_ {server_port}
, heartbeat_timer_ {new QTimer {this}}
{
connect (heartbeat_timer_, &QTimer::timeout, this, &impl::heartbeat);
connect (this, &QIODevice::readyRead, this, &impl::pending_datagrams);
heartbeat_timer_->start (NetworkMessage::pulse * 1000);
// bind to an ephemeral port
bind ();
}
~impl ()
{
closedown ();
}
void parse_message (QByteArray const& msg);
void pending_datagrams ();
void heartbeat ();
void closedown ();
bool check_status (QDataStream const&) const;
Q_SLOT void host_info_results (QHostInfo);
MessageClient * self_;
QString id_;
QHostAddress server_;
port_type server_port_;
QTimer * heartbeat_timer_;
};
#include "MessageClient.moc"
void MessageClient::impl::host_info_results (QHostInfo host_info)
{
if (QHostInfo::NoError != host_info.error ())
{
Q_EMIT self_->error ("UDP server lookup failed:\n" + host_info.errorString ());
}
else if (host_info.addresses ().size ())
{
server_ = host_info.addresses ()[0];
}
}
void MessageClient::impl::pending_datagrams ()
{
while (hasPendingDatagrams ())
{
QByteArray datagram;
datagram.resize (pendingDatagramSize ());
QHostAddress sender_address;
port_type sender_port;
if (0 <= readDatagram (datagram.data (), datagram.size (), &sender_address, &sender_port))
{
parse_message (datagram);
}
}
}
void MessageClient::impl::parse_message (QByteArray const& msg)
{
//
// message format is described in NetworkMessage.hpp
//
NetworkMessage::Reader in {msg};
if (id_ == in.id ()) // for us
{
//
// message format is described in NetworkMessage.hpp
//
switch (in.type ())
{
case NetworkMessage::Reply:
{
// unpack message
QTime time;
qint32 snr;
float delta_time;
quint32 delta_frequency;
QByteArray mode;
QByteArray message;
in >> time >> snr >> delta_time >> delta_frequency >> mode >> message;
if (check_status (in))
{
Q_EMIT self_->reply (time, snr, delta_time, delta_frequency
, QString::fromUtf8 (mode), QString::fromUtf8 (message));
}
}
break;
case NetworkMessage::Replay:
if (check_status (in))
{
Q_EMIT self_->replay ();
}
break;
default:
// Ignore
break;
}
}
}
void MessageClient::impl::heartbeat ()
{
if (server_port_ && !server_.isNull ())
{
QByteArray message;
NetworkMessage::Builder hb {&message, NetworkMessage::Heartbeat, id_};
if (check_status (hb))
{
writeDatagram (message, server_, server_port_);
}
}
}
void MessageClient::impl::closedown ()
{
if (server_port_ && !server_.isNull ())
{
QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::Close, id_};
if (check_status (out))
{
writeDatagram (message, server_, server_port_);
}
}
}
bool MessageClient::impl::check_status (QDataStream const& stream) const
{
auto stat = stream.status ();
switch (stat)
{
case QDataStream::ReadPastEnd:
Q_EMIT self_->error ("Message serialization error: read failed");
break;
case QDataStream::ReadCorruptData:
Q_EMIT self_->error ("Message serialization error: read corrupt data");
break;
case QDataStream::WriteFailed:
Q_EMIT self_->error ("Message serialization error: write error");
break;
default:
break;
}
return QDataStream::Ok == stat;
}
MessageClient::MessageClient (QString const& id, QString const& server, port_type server_port, QObject * self)
: QObject {self}
, m_ {id, server_port, this}
{
connect (&*m_, static_cast<void (impl::*) (impl::SocketError)> (&impl::error)
, [this] (impl::SocketError /* e */)
{
Q_EMIT error (m_->errorString ());
});
set_server (server);
}
QHostAddress MessageClient::server_address () const
{
return m_->server_;
}
auto MessageClient::server_port () const -> port_type
{
return m_->server_port_;
}
void MessageClient::set_server (QString const& server)
{
m_->server_.clear ();
if (!server.isEmpty ())
{
// queue a host address lookup
QHostInfo::lookupHost (server, &*m_, SLOT (host_info_results (QHostInfo)));
}
}
void MessageClient::set_server_port (port_type server_port)
{
m_->server_port_ = server_port;
}
void MessageClient::send_raw_datagram (QByteArray const& message, QHostAddress const& dest_address
, port_type dest_port)
{
if (dest_port && !dest_address.isNull ())
{
m_->writeDatagram (message, dest_address, dest_port);
}
}
void MessageClient::status_update (Frequency f, QString const& mode, QString const& dx_call
, QString const& report, QString const& tx_mode)
{
if (m_->server_port_ && !m_->server_.isNull ())
{
QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::Status, m_->id_};
out << f << mode.toUtf8 () << dx_call.toUtf8 () << report.toUtf8 () << tx_mode.toUtf8 ();
if (m_->check_status (out))
{
m_->writeDatagram (message, m_->server_, m_->server_port_);
}
}
}
void MessageClient::decode (bool is_new, QTime time, qint32 snr, float delta_time, quint32 delta_frequency
, QString const& mode, QString const& message_text)
{
if (m_->server_port_ && !m_->server_.isNull ())
{
QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::Decode, m_->id_};
out << is_new << time << snr << delta_time << delta_frequency << mode.toUtf8 () << message_text.toUtf8 ();
if (m_->check_status (out))
{
m_->writeDatagram (message, m_->server_, m_->server_port_);
}
}
}
void MessageClient::clear_decodes ()
{
if (m_->server_port_ && !m_->server_.isNull ())
{
QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::Clear, m_->id_};
if (m_->check_status (out))
{
m_->writeDatagram (message, m_->server_, m_->server_port_);
}
}
}
void MessageClient::qso_logged (QDateTime time, QString const& dx_call, QString const& dx_grid
, Frequency dial_frequency, QString const& mode, QString const& report_sent
, QString const& report_received, QString const& tx_power
, QString const& comments, QString const& name)
{
if (m_->server_port_ && !m_->server_.isNull ())
{
QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::QSOLogged, m_->id_};
out << time << dx_call.toUtf8 () << dx_grid.toUtf8 () << dial_frequency << mode.toUtf8 ()
<< report_sent.toUtf8 () << report_received.toUtf8 () << tx_power.toUtf8 () << comments.toUtf8 () << name.toUtf8 ();
if (m_->check_status (out))
{
m_->writeDatagram (message, m_->server_, m_->server_port_);
}
}
}

81
MessageClient.hpp Normal file
View File

@ -0,0 +1,81 @@
#ifndef MESSAGE_CLIENT_HPP__
#define MESSAGE_CLIENT_HPP__
#include <QObject>
#include <QHostAddress>
#include <QTime>
#include <QDateTime>
#include <QString>
#include <QByteArray>
#include "Radio.hpp"
#include "pimpl_h.hpp"
//
// MessageClient - Manage messages sent and replies received from a
// matching server (MessageServer) at the other end of
// the wire
//
//
// Each outgoing message type is a Qt slot
//
class MessageClient
: public QObject
{
Q_OBJECT;
public:
using Frequency = Radio::Frequency;
using port_type = quint16;
// instantiate and initiate a host lookup on the server
//
// messages will be silently dropped until a server host lookup is complete
MessageClient (QString const& id, QString const& server, port_type server_port, QObject * parent = nullptr);
// query server details
QHostAddress server_address () const;
port_type server_port () const;
// initiate a new server host lookup or is the server name is empty
// the sending of messages is disabled
Q_SLOT void set_server (QString const& server = QString {});
// change the server port messages are sent to
Q_SLOT void set_server_port (port_type server_port = 0u);
// outgoing messages
Q_SLOT void status_update (Frequency, QString const& mode, QString const& dx_call, QString const& report
, QString const& tx_mode);
Q_SLOT void decode (bool is_new, QTime time, qint32 snr, float delta_time, quint32 delta_frequency
, QString const& mode, QString const& message);
Q_SLOT void clear_decodes ();
Q_SLOT void qso_logged (QDateTime time, QString const& dx_call, QString const& dx_grid
, Frequency dial_frequency, QString const& mode, QString const& report_sent
, QString const& report_received, QString const& tx_power, QString const& comments
, QString const& name);
// this slot may be used to send arbitrary UDP datagrams to and
// destination allowing the underlying socket to be used for general
// UDP messaging if desired
Q_SLOT void send_raw_datagram (QByteArray const&, QHostAddress const& dest_address, port_type dest_port);
// this signal is emitted if the server sends us a reply, the only
// reply supported is reply to a prior CQ or QRZ message
Q_SIGNAL void reply (QTime, qint32 snr, float delta_time, quint32 delta_frequency, QString const& mode
, QString const& message_text);
// this signal is emitted if the server has requested a replay of
// all decodes
Q_SIGNAL void replay ();
// this signal is emitted when network errors occur or if a host
// lookup fails
Q_SIGNAL void error (QString const&) const;
private:
class impl;
pimpl<impl> m_;
};
#endif

308
MessageServer.cpp Normal file
View File

@ -0,0 +1,308 @@
#include "MessageServer.hpp"
#include <unordered_map>
#include <QUdpSocket>
#include <QTimer>
#include "NetworkMessage.hpp"
#include "qt_helpers.hpp"
#include "pimpl_impl.hpp"
#include "moc_MessageServer.cpp"
class MessageServer::impl
: public QUdpSocket
{
Q_OBJECT;
public:
impl (MessageServer * self)
: self_ {self}
, port_ {0u}
, clock_ {new QTimer {this}}
{
connect (this, &QIODevice::readyRead, this, &MessageServer::impl::pending_datagrams);
connect (this, static_cast<void (impl::*) (SocketError)> (&impl::error)
, [this] (SocketError /* e */)
{
Q_EMIT self_->error (errorString ());
});
connect (clock_, &QTimer::timeout, this, &impl::tick);
clock_->start (NetworkMessage::pulse * 1000);
}
void leave_multicast_group ();
void join_multicast_group ();
void parse_message (QHostAddress const& sender, port_type sender_port, QByteArray const& msg);
void tick ();
void pending_datagrams ();
bool check_status (QDataStream const&) const;
MessageServer * self_;
port_type port_;
QHostAddress multicast_group_address_;
static BindMode const bind_mode_;
struct Client
{
QHostAddress sender_address_;
port_type sender_port_;
QDateTime last_activity_;
};
using client_hash = std::unordered_map<QString, Client>; // maps id to Client
client_hash clients_;
QTimer * clock_;
};
#include "MessageServer.moc"
MessageServer::impl::BindMode const MessageServer::impl::bind_mode_ = ShareAddress | ReuseAddressHint;
void MessageServer::impl::leave_multicast_group ()
{
if (!multicast_group_address_.isNull () && BoundState == state ())
{
leaveMulticastGroup (multicast_group_address_);
}
}
void MessageServer::impl::join_multicast_group ()
{
if (BoundState == state ()
&& !multicast_group_address_.isNull ())
{
if (IPv4Protocol == multicast_group_address_.protocol ()
&& IPv4Protocol != localAddress ().protocol ())
{
close ();
bind (QHostAddress::AnyIPv4, port_, bind_mode_);
}
if (!joinMulticastGroup (multicast_group_address_))
{
multicast_group_address_.clear ();
}
}
}
void MessageServer::impl::pending_datagrams ()
{
while (hasPendingDatagrams ())
{
QByteArray datagram;
datagram.resize (pendingDatagramSize ());
QHostAddress sender_address;
port_type sender_port;
if (0 <= readDatagram (datagram.data (), datagram.size (), &sender_address, &sender_port))
{
parse_message (sender_address, sender_port, datagram);
}
}
}
void MessageServer::impl::parse_message (QHostAddress const& sender, port_type sender_port, QByteArray const& msg)
{
//
// message format is described in NetworkMessage.hpp
//
NetworkMessage::Reader in {msg};
auto id = in.id ();
bool new_client {false};
if (std::end (clients_) == clients_.find (id))
{
new_client = true;
}
clients_[id] = {sender, sender_port, QDateTime::currentDateTime ()};
if (new_client)
{
Q_EMIT self_->client_opened (id);
}
//
// message format is described in NetworkMessage.hpp
//
switch (in.type ())
{
case NetworkMessage::Heartbeat:
//nothing to do here as time out handling deals with lifetime
break;
case NetworkMessage::Clear:
Q_EMIT self_->clear_decodes (id);
break;
case NetworkMessage::Status:
{
// unpack message
Frequency f;
QByteArray mode;
QByteArray dx_call;
QByteArray report;
QByteArray tx_mode;
in >> f >> mode >> dx_call >> report >> tx_mode;
if (check_status (in))
{
Q_EMIT self_->status_update (id, f, QString::fromUtf8 (mode), QString::fromUtf8 (dx_call)
, QString::fromUtf8 (report), QString::fromUtf8 (tx_mode));
}
}
break;
case NetworkMessage::Decode:
{
// unpack message
bool is_new;
QTime time;
qint32 snr;
float delta_time;
quint32 delta_frequency;
QByteArray mode;
QByteArray message;
in >> is_new >> time >> snr >> delta_time >> delta_frequency >> mode >> message;
if (check_status (in))
{
Q_EMIT self_->decode (is_new, id, time, snr, delta_time, delta_frequency
, QString::fromUtf8 (mode), QString::fromUtf8 (message));
}
}
break;
case NetworkMessage::QSOLogged:
{
QDateTime time;
QByteArray dx_call;
QByteArray dx_grid;
Frequency dial_frequency;
QByteArray mode;
QByteArray report_sent;
QByteArray report_received;
QByteArray tx_power;
QByteArray comments;
QByteArray name;
in >> time >> dx_call >> dx_grid >> dial_frequency >> mode >> report_sent >> report_received
>> tx_power >> comments >> name;
if (check_status (in))
{
Q_EMIT self_->qso_logged (id, time, QString::fromUtf8 (dx_call), QString::fromUtf8 (dx_grid)
, dial_frequency, QString::fromUtf8 (mode), QString::fromUtf8 (report_sent)
, QString::fromUtf8 (report_received), QString::fromUtf8 (tx_power)
, QString::fromUtf8 (comments), QString::fromUtf8 (name));
}
}
break;
case NetworkMessage::Close:
if (check_status (in))
{
Q_EMIT self_->client_closed (id);
clients_.erase (id);
}
break;
default:
// Ignore
break;
}
}
void MessageServer::impl::tick ()
{
auto now = QDateTime::currentDateTime ();
for (auto iter = std::begin (clients_); iter != std::end (clients_);)
{
if (now > (*iter).second.last_activity_.addSecs (NetworkMessage::pulse))
{
Q_EMIT self_->clear_decodes ((*iter).first);
Q_EMIT self_->client_closed ((*iter).first);
iter = clients_.erase (iter);
}
else
{
++iter;
}
}
}
bool MessageServer::impl::check_status (QDataStream const& stream) const
{
auto stat = stream.status ();
switch (stat)
{
case QDataStream::ReadPastEnd:
Q_EMIT self_->error ("Message serialization error: read failed");
break;
case QDataStream::ReadCorruptData:
Q_EMIT self_->error ("Message serialization error: read corrupt data");
break;
case QDataStream::WriteFailed:
Q_EMIT self_->error ("Message serialization error: write error");
break;
default:
break;
}
return QDataStream::Ok == stat;
}
MessageServer::MessageServer (QObject * parent)
: QObject {parent}
, m_ {this}
{
}
void MessageServer::start (port_type port, QHostAddress const& multicast_group_address)
{
if (port != m_->port_
|| multicast_group_address != m_->multicast_group_address_)
{
m_->leave_multicast_group ();
if (impl::BoundState == m_->state ())
{
m_->close ();
}
m_->multicast_group_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;
m_->join_multicast_group ();
}
else
{
m_->port_ = 0;
}
}
}
void MessageServer::reply (QString const& id, QTime time, qint32 snr, float delta_time, quint32 delta_frequency, QString const& mode, QString const& message_text)
{
auto iter = m_->clients_.find (id);
if (iter != std::end (m_->clients_))
{
QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::Reply, id};
out << time << snr << delta_time << delta_frequency << mode.toUtf8 () << message_text.toUtf8 ();
if (m_->check_status (out))
{
m_->writeDatagram (message, (*iter).second.sender_address_, (*iter).second.sender_port_);
}
}
}
void MessageServer::replay (QString const& id)
{
auto iter = m_->clients_.find (id);
if (iter != std::end (m_->clients_))
{
QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::Replay, id};
if (m_->check_status (out))
{
m_->writeDatagram (message, (*iter).second.sender_address_, (*iter).second.sender_port_);
}
}
}

73
MessageServer.hpp Normal file
View File

@ -0,0 +1,73 @@
#ifndef MESSAGE_SERVER_HPP__
#define MESSAGE_SERVER_HPP__
#include <QObject>
#include <QTime>
#include <QDateTime>
#include <QString>
#include <QByteArray>
#include <QHostAddress>
#include "Radio.hpp"
#include "pimpl_h.hpp"
//
// MessageServer - a reference implementation of a message server
// matching the MessageClient class at the other end
// of the wire
//
// This class is fully functioning and suitable for use in C++
// applications that use the Qt framework. Other applications should
// use this classes' implementation as a reference implementation.
//
class MessageServer
: public QObject
{
Q_OBJECT;
public:
using port_type = quint16;
using Frequency = Radio::Frequency;
MessageServer (QObject * parent = nullptr);
// start or restart the server, if the multicast_group_address
// argument is given it is assumed to be a multicast group address
// which the server will join
Q_SLOT void start (port_type port, QHostAddress const& multicast_group_address = QHostAddress {});
// ask the client with identification 'id' to make the same action
// as a double click on the decode would
//
// 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
Q_SLOT void reply (QString const& id, QTime time, qint32 snr, float delta_time, quint32 delta_frequency
, QString const& mode, QString const& message);
// ask the client with identification 'id' to replay all decodes
Q_SLOT void replay (QString const& id);
// the following signals are emitted when a client broadcasts the
// matching message
Q_SIGNAL void client_opened (QString const& id);
Q_SIGNAL void status_update (QString const& id, Frequency, QString const& mode, QString const& dx_call
, QString const& report, QString const& tx_mode);
Q_SIGNAL void client_closed (QString const& id);
Q_SIGNAL void decode (bool is_new, QString const& id, QTime time, qint32 snr, float delta_time
, quint32 delta_frequency, QString const& mode, QString const& message);
Q_SIGNAL void qso_logged (QString const& id, QDateTime time, QString const& dx_call, QString const& dx_grid
, Frequency dial_frequency, QString const& mode, QString const& report_sent
, QString const& report_received, QString const& tx_power, QString const& comments
, QString const& name);
Q_SIGNAL void clear_decodes (QString const& id);
// this signal is emitted when a network error occurs
Q_SIGNAL void error (QString const&) const;
private:
class impl;
pimpl<impl> m_;
};
#endif

96
NetworkMessage.cpp Normal file
View File

@ -0,0 +1,96 @@
#include "NetworkMessage.hpp"
#include <exception>
#include <QString>
#include <QByteArray>
#include "pimpl_impl.hpp"
namespace NetworkMessage
{
Builder::Builder (QIODevice * device, Type type, QString const& id)
: QDataStream {device}
{
common_initialization (type, id);
}
Builder::Builder (QByteArray * a, Type type, QString const& id)
: QDataStream {a, QIODevice::WriteOnly}
{
common_initialization (type, id);
}
void Builder::common_initialization (Type type, QString const& id)
{
*this << magic;
*this << schema_number;
setVersion (QDataStream::Qt_5_0); // Qt schema version
*this << static_cast<quint32> (type) << id.toUtf8 ();
}
class Reader::impl
{
public:
void common_initialization (Reader * parent)
{
quint32 magic;
*parent >> magic;
if (magic != Builder::magic)
{
throw std::runtime_error {"Invalid message format"};
}
*parent >> schema_;
if (schema_ > Builder::schema_number)
{
throw std::runtime_error {"Unrecognized message schema"};
}
if (schema_ <= 1)
{
parent->setVersion (QDataStream::Qt_5_0);
}
quint32 type;
*parent >> type >> id_;
if (type >= maximum_message_type_)
{
throw std::runtime_error {"Unrecognized message type"};
}
type_ = static_cast<Type> (type);
}
quint32 schema_;
Type type_;
QByteArray id_;
};
Reader::Reader (QIODevice * device)
: QDataStream {device}
{
m_->common_initialization (this);
}
Reader::Reader (QByteArray const& a)
: QDataStream {a}
{
m_->common_initialization (this);
}
Reader::~Reader ()
{
}
quint32 Reader::schema () const
{
return m_->schema_;
}
Type Reader::type () const
{
return static_cast<Type> (m_->type_);
}
QString Reader::id () const
{
return QString::fromUtf8 (m_->id_);
}
}

165
NetworkMessage.hpp Normal file
View File

@ -0,0 +1,165 @@
#ifndef NETWORK_MESSAGE_HPP__
#define NETWORK_MESSAGE_HPP__
/*
* WSJT-X Message Formats
* ======================
*
* All messages are written or read using the QDataStream derivatives
* defined below.
*
* Message is big endian format
*
* Header format:
*
* 32-bit unsigned integer magic number 0xadbccbda
* 32-bit unsigned integer schema number
*
* Payload format:
*
* As per the QDataStream format, see below for version used and
* here:
*
* http://doc.qt.io/qt-5/datastreamformat.html
*
* for the serialization details for each type.
*
* Type utf8 is a utf-8 byte string formatted as a QByteArray for
* serialization purposes (currently a quint32 size followed by size
* bytes, no terminator is present or counted).
*
* Schema Version 1:
* -----------------
*
* Message Direction Value Type
* ------------- --------- ---------------------- -----------
* Heartbeat Out 0 quint32
* Id (unique key) utf8
*
* Status Out 1 quint32
* Id (unique key) utf8
* Dial Frequency (Hz) quint64
* Mode utf8
* DX call utf8
* Report utf8
* Tx Mode utf8
*
* Decode Out 2 quint32
* Id (unique key) utf8
* New bool
* Time QTime
* snr qint32
* Delta time (S) float
* Delta frequency (Hz) quint32
* Mode utf8
* Message utf8
*
* Clear Out 3 quint32
* Id (unique key) utf8
*
* Reply In 4 quint32
* Id (target unique key) utf8
* Time QTime
* snr qint32
* Delta time (S) float
* Delta frequency (Hz) quint32
* Mode utf8
* Message utf8
*
* QSO Logged Out 5 quint32
* Id (unique key) utf8
* Date & Time QDateTime
* DX call utf8
* DX grid utf8
* Dial frequency (Hz) quint64
* Mode utf8
* Report send utf8
* Report received utf8
* Tx power utf8
* Comments utf8
* Name utf8
*
* Close Out 6 quint32
* Id (unique key) utf8
*
* Replay In 7 quint32
* Id (unique key) utf8
*/
#include <QDataStream>
#include "pimpl_h.hpp"
class QIODevice;
class QByteArray;
class QString;
namespace NetworkMessage
{
// NEVER DELETE MESSAGE TYPES
enum Type
{
Heartbeat,
Status,
Decode,
Clear,
Reply,
QSOLogged,
Close,
Replay,
maximum_message_type_ // ONLY add new message types
// immediately before here
};
quint32 constexpr pulse {15}; // seconds
//
// NetworkMessage::Build - build a message containing serialized Qt types
//
class Builder
: public QDataStream
{
public:
static quint32 constexpr magic {0xadbccbda}; // never change this
// increment this if a newer Qt schema is required and add decode
// logic to InputMessageStream below
static quint32 constexpr schema_number {1};
explicit Builder (QIODevice *, Type, QString const& id);
explicit Builder (QByteArray *, Type, QString const& id);
Builder (Builder const&) = delete;
Builder& operator = (Builder const&) = delete;
private:
void common_initialization (Type type, QString const& id);
};
//
// NetworkMessage::Reader - read a message containing serialized Qt types
//
// Message is as per NetworkMessage::Builder above, the schema()
// member may be used to determine the schema of the original
// message.
//
class Reader
: public QDataStream
{
public:
explicit Reader (QIODevice *);
explicit Reader (QByteArray const&);
Reader (Reader const&) = delete;
Reader& operator = (Reader const&) = delete;
~Reader ();
quint32 schema () const;
Type type () const;
QString id () const;
private:
class impl;
pimpl<impl> m_;
};
}
#endif

View File

@ -50,7 +50,7 @@ void LogQSO::storeSettings () const
void LogQSO::initLogQSO(QString hisCall, QString hisGrid, QString mode,
QString rptSent, QString rptRcvd, QDateTime dateTime,
double dialFreq, QString myCall, QString myGrid,
Radio::Frequency dialFreq, QString myCall, QString myGrid,
bool noSuffix, bool toRTTY, bool dBtoComments)
{
ui->call->setText(hisCall);
@ -79,7 +79,7 @@ void LogQSO::initLogQSO(QString hisCall, QString hisGrid, QString mode,
m_dialFreq=dialFreq;
m_myCall=myCall;
m_myGrid=myGrid;
QString band= ADIF::bandFromFrequency(dialFreq);
QString band= ADIF::bandFromFrequency(dialFreq / 1.e6);
ui->band->setText(band);
show ();
@ -103,7 +103,7 @@ void LogQSO::accept()
m_txPower=ui->txPower->text();
comments=ui->comments->text();
m_comments=comments;
QString strDialFreq(QString::number(m_dialFreq,'f',6));
QString strDialFreq(QString::number(m_dialFreq / 1.e6,'f',6));
//Log this QSO to ADIF file "wsjtx_log.adi"
QString filename = "wsjtx_log.adi"; // TODO allow user to set
@ -125,28 +125,20 @@ void LogQSO::accept()
m.exec();
} else {
QString logEntry=m_dateTime.date().toString("yyyy-MMM-dd,") +
m_dateTime.time().toString("hh:mm,") + hisCall + "," +
hisGrid + "," + strDialFreq + "," + mode +
"," + rptSent + "," + rptRcvd;
if(m_txPower!="") logEntry += "," + m_txPower;
if(comments!="") logEntry += "," + comments;
if(name!="") logEntry += "," + name;
m_dateTime.time().toString("hh:mm,") + hisCall + "," +
hisGrid + "," + strDialFreq + "," + mode +
"," + rptSent + "," + rptRcvd + "," + m_txPower +
"," + comments; + "," + name;
QTextStream out(&f);
out << logEntry << endl;
f.close();
}
//Clean up and finish logging
Q_EMIT acceptQSO(true);
Q_EMIT acceptQSO (m_dateTime, hisCall, hisGrid, m_dialFreq, mode, rptSent, rptRcvd, m_txPower, comments, name);
QDialog::accept();
}
void LogQSO::reject()
{
Q_EMIT acceptQSO(false);
QDialog::reject();
}
// closeEvent is only called from the system menu close widget for a
// modeless dialog so we use the hideEvent override to store the
// window settings

View File

@ -10,6 +10,8 @@
#include <QScopedPointer>
#include "Radio.hpp"
namespace Ui {
class LogQSO;
}
@ -25,15 +27,18 @@ public:
~LogQSO();
void initLogQSO(QString hisCall, QString hisGrid, QString mode,
QString rptSent, QString rptRcvd, QDateTime dateTime,
double dialFreq, QString myCall, QString myGrid,
Radio::Frequency dialFreq, QString myCall, QString myGrid,
bool noSuffix, bool toRTTY, bool dBtoComments);
public slots:
void accept();
void reject();
signals:
void acceptQSO(bool accepted);
void acceptQSO (QDateTime const&, QString const& call, QString const& grid
, Radio::Frequency dial_freq, QString const& mode
, QString const& rpt_sent, QString const& rpt_received
, QString const& tx_power, QString const& comments
, QString const& name);
protected:
void hideEvent (QHideEvent *);
@ -46,7 +51,7 @@ private:
QSettings * m_settings;
QString m_txPower;
QString m_comments;
double m_dialFreq;
Radio::Frequency m_dialFreq;
QString m_myCall;
QString m_myGrid;
QDateTime m_dateTime;

View File

@ -91,7 +91,7 @@ int main(int argc, char *argv[])
auto temp_name = parser.value (rig_option);
if (!temp_name.isEmpty ())
{
if (temp_name.contains (QRegularExpression {R"([\\/])"}))
if (temp_name.contains (QRegularExpression {R"([\\/,])"}))
{
std::cerr << QObject::tr ("Invalid rig name - \\ & / not allowed").toLocal8Bit ().data () << std::endl;
parser.showHelp (-1);

View File

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

View File

@ -14,6 +14,8 @@
#include <QScopedPointer>
#include <QDir>
#include <QProgressDialog>
#include <QAbstractSocket>
#include <QHostAddress>
#include "soundin.h"
#include "AudioDevice.hpp"
@ -47,10 +49,13 @@ namespace Ui {
class QSettings;
class QLineEdit;
class QFont;
class QHostInfo;
class WideGraph;
class LogQSO;
class Transceiver;
class Astro;
class MessageClient;
class QTime;
class MainWindow : public QMainWindow
{
@ -158,7 +163,11 @@ private slots:
void on_tuneButton_clicked (bool);
void on_pbR2T_clicked();
void on_pbT2R_clicked();
void acceptQSO2(bool accepted);
void acceptQSO2(QDateTime const&, QString const& call, QString const& grid
, Frequency dial_freq, QString const& mode
, QString const& rpt_sent, QString const& rpt_received
, QString const& tx_power, QString const& comments
, QString const& name);
void on_bandComboBox_activated (int index);
void on_readFreq_clicked();
void on_pbTxMode_clicked();
@ -178,6 +187,7 @@ private slots:
void monitor (bool);
void stop_tuning ();
void auto_tx_mode (bool);
void networkError (QString const&);
private:
void enable_DXCC_entity (bool on);
@ -356,7 +366,6 @@ private:
QRect m_astroGeom;
QSharedMemory *mem_jt9;
PSK_Reporter *psk_Reporter;
SignalMeter *signalMeter;
LogBook m_logBook;
DecodedText m_QSOText;
@ -373,6 +382,9 @@ private:
double m_toneSpacing;
int m_firstDecode;
QProgressDialog m_optimizingProgress;
QTimer m_heartbeat;
MessageClient * m_messageClient;
PSK_Reporter *psk_Reporter;
//---------------------------------------------------- private functions
void readSettings();
@ -395,6 +407,10 @@ private:
void pskSetLocal ();
void displayDialFrequency ();
void transmitDisplay (bool);
void processMessage(QString const& messages, qint32 position, bool ctrl);
void replyToCQ (QTime, qint32 snr, float delta_time, quint32 delta_frequency, QString const& mode, QString const& message_text);
void replayDecodes ();
void postDecode (bool is_new, QString const& message);
};
extern void getfile(QString fname, int ntrperiod);

View File

@ -5,11 +5,18 @@
#include "psk_reporter.h"
#include <QHostInfo>
#include <QTimer>
#include "MessageClient.hpp"
#include "moc_psk_reporter.cpp"
PSK_Reporter::PSK_Reporter(QObject *parent) :
QObject(parent),
m_sequenceNumber(0)
PSK_Reporter::PSK_Reporter(MessageClient * message_client, QObject *parent) :
QObject {parent},
m_messageClient {message_client},
reportTimer {new QTimer {this}},
m_sequenceNumber {0}
{
m_header_h = "000Allllttttttttssssssssiiiiiiii";
@ -34,10 +41,8 @@ PSK_Reporter::PSK_Reporter(QObject *parent) :
qsrand(QDateTime::currentDateTime().toTime_t());
m_randomId_h = QString("%1").arg(qrand(),8,16,QChar('0'));
m_udpSocket = new QUdpSocket(this);
QHostInfo::lookupHost("report.pskreporter.info", this, SLOT(dnsLookupResult(QHostInfo)));
reportTimer = new QTimer(this);
connect(reportTimer, SIGNAL(timeout()), this, SLOT(sendReport()));
reportTimer->start(5*60*1000); // 5 minutes;
}
@ -107,7 +112,7 @@ void PSK_Reporter::sendReport()
// Send data to PSK Reporter site
if (!m_pskReporterAddress.isNull()) {
m_udpSocket->writeDatagram(report, m_pskReporterAddress, 4739);
m_messageClient->send_raw_datagram (report, m_pskReporterAddress, 4739);
}
}

View File

@ -2,15 +2,21 @@
#ifndef PSK_REPORTER_H
#define PSK_REPORTER_H
#include <QtCore>
#include <QUdpSocket>
#include <QHostInfo>
#include <QObject>
#include <QString>
#include <QHostAddress>
#include <QQueue>
#include <QHash>
class MessageClient;
class QTimer;
class QHostInfo;
class PSK_Reporter : public QObject
{
Q_OBJECT
public:
explicit PSK_Reporter(QObject *parent = 0);
explicit PSK_Reporter(MessageClient *, QObject *parent = nullptr);
void setLocalStation(QString call, QString grid, QString antenna, QString programInfo);
void addRemoteStation(QString call, QString grid, QString freq, QString mode, QString snr, QString time);
@ -38,7 +44,7 @@ private:
QQueue< QHash<QString,QString> > m_spotQueue;
QUdpSocket *m_udpSocket;
MessageClient * m_messageClient;
QTimer *reportTimer;

View File

@ -2,13 +2,17 @@
#define QT_HELPERS_HPP_
#include <stdexcept>
#include <functional>
#include <QDataStream>
#include <QMetaObject>
#include <QMetaType>
#include <QMetaEnum>
#include <QString>
#include <QByteArray>
#include <QDebug>
#include <QHostAddress>
#include <QHash>
#define ENUM_QDATASTREAM_OPS_DECL(CLASS, ENUM) \
QDataStream& operator << (QDataStream&, CLASS::ENUM); \
@ -60,6 +64,24 @@
return QString {mo.enumerator (mo.indexOfEnumerator (#ENUM)).valueToKey (m)}; \
}
namespace std
{
/*
* std::hash specialization for QString so it can be used
* as a key in std::unordered_map
*/
template<class Key> struct hash;
template<> struct hash<QString>
{
typedef QString Key;
typedef uint result_type;
inline uint operator () (const QString &s) const
{
return qHash (s);
}
};
}
inline
void throw_qstring (QString const& qs)
{
@ -68,4 +90,7 @@ void throw_qstring (QString const& qs)
QString font_as_stylesheet (QFont const&);
// Register some useful Qt types with QMetaType
Q_DECLARE_METATYPE (QHostAddress);
#endif