mirror of
https://github.com/saitohirga/WSJT-X.git
synced 2024-10-31 15:47:10 -04:00
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:
parent
bbc359d043
commit
c5c6feb41c
@ -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
|
||||
)
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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
|
||||
|
197
Configuration.ui
197
Configuration.ui
@ -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 &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 &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><html><head/><body><p>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.</p></body></html></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><html><head/><body><p>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.</p></body></html></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><html><head/><body><p>Optional hostname of network service to receive decodes.</p><p>Formats:</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">hostname</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">IPv4 address</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">IPv6 address</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">IPv4 multicast group address</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">IPv6 multicast group address</li></ul><p>Clearing this field will disable the broadcasting of UDP status updates.</p></body></html></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><html><head/><body><p>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.</p></body></html></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><html><head/><body><p>With this enabled WSJT-X will accept certain requests back from a UDP server that receives decode messages.</p></body></html></string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Accept UDP requests</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="udpWindowToFront">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>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.</p></body></html></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><html><head/><body><p>Restore the window from minimized if an UDP request is accepted.</p></body></html></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
462
MessageAggregator.cpp
Normal 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
283
MessageClient.cpp
Normal 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
81
MessageClient.hpp
Normal 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
308
MessageServer.cpp
Normal 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
73
MessageServer.hpp
Normal 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
96
NetworkMessage.cpp
Normal 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
165
NetworkMessage.hpp
Normal 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
|
24
logqso.cpp
24
logqso.cpp
@ -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
|
||||
|
13
logqso.h
13
logqso.h
@ -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;
|
||||
|
2
main.cpp
2
main.cpp
@ -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);
|
||||
|
178
mainwindow.cpp
178
mainwindow.cpp
@ -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 ());
|
||||
}
|
||||
}
|
||||
|
20
mainwindow.h
20
mainwindow.h
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user