Extend UDP status message - added Rx/Tx DF, call and grid information

Build now creates and installs a  UDP library that contains the server
side  of the  UDP messaging  facility.  This  library is  used by  the
udp_daemon and message_aggregator reference  examples. The new library
is  currently a  static archive  but  can also  be built  as a  shared
library.  The library  allows third  party Qt  applications to  easily
access UDP messages from WSJT-X.

Refactored  the  message_aggregator  reference example  to  split  out
classes into  separate translation  units. Added new  functionality to
exercise  the  new  UDP  status fields,  highlight  own  call,  CQ/QRZ
messages and decodes near Rx DF.

git-svn-id: svn+ssh://svn.code.sf.net/p/wsjt/wsjt/branches/wsjtx@6691 ab8295b8-cf94-4d9e-aec4-7959e3be5d79
This commit is contained in:
Bill Somerville 2016-05-24 10:08:35 +00:00
parent d865186de3
commit 3ec6d211c8
29 changed files with 1164 additions and 787 deletions

View File

@ -40,6 +40,9 @@ if (POLICY CMP0043)
cmake_policy (SET CMP0043 NEW) # ignore COMPILE_DEFINITIONS_<CONFIG>
endif (POLICY CMP0043)
if (POLICY CMP0063)
cmake_policy (SET CMP0063 NEW) # honour visibility properties for all library types
endif (POLICY CMP0063)
include (${PROJECT_SOURCE_DIR}/CMake/VersionCompute.cmake)
message (STATUS "Building ${CMAKE_PROJECT_NAME}-${wsjtx_VERSION}")
@ -50,7 +53,7 @@ message (STATUS "Building ${CMAKE_PROJECT_NAME}-${wsjtx_VERSION}")
set (PROJECT_NAME "WSJT-X")
set (PROJECT_VENDOR "Joe Taylor, K1JT")
set (PROJECT_CONTACT "Joe Taylor <k1jt@arrl.net>")
set (PROJECT_COPYRIGHT "Copyright (C) 2001-2015 by Joe Taylor, K1JT")
set (PROJECT_COPYRIGHT "Copyright (C) 2001-2016 by Joe Taylor, K1JT")
set (PROJECT_HOMEPAGE http://www.physics.princeton.edu/pulsar/K1JT/wsjtx.html)
set (PROJECT_MANUAL wsjtx-main)
set (PROJECT_MANUAL_DIRECTORY_URL http://www.physics.princeton.edu/pulsar/K1JT/wsjtx-doc/)
@ -104,6 +107,15 @@ endif ()
#
include (CMakeDependentOption)
# Allow the developer to select if Dynamic or Static libraries are built
OPTION (BUILD_SHARED_LIBS "Build Shared Libraries" OFF)
# Set the LIB_TYPE variable to STATIC
SET (LIB_TYPE STATIC)
if (BUILD_SHARED_LIBS)
# User wants to build Dynamic Libraries, so change the LIB_TYPE variable to CMake keyword 'SHARED'
set (LIB_TYPE SHARED)
endif (BUILD_SHARED_LIBS)
option (UPDATE_TRANSLATIONS "Update source translation translations/*.ts
files (WARNING: make clean will delete the source .ts files! Danger!)")
option (WSJT_SHARED_RUNTIME "Debugging option that allows running from a shared Cloud directory.")
@ -145,6 +157,7 @@ message (STATUS "******************************************************")
#
set (BIN_DESTINATION bin)
set (LIB_DESTINATION lib)
set (INCLUDE_DESTINATION include)
set (SHARE_DESTINATION share)
set (DOC_DESTINATION doc/${CMAKE_PROJECT_NAME})
set (DATA_DESTINATION ${CMAKE_PROJECT_NAME})
@ -171,6 +184,7 @@ endif (APPLE)
set (WSJT_BIN_DESTINATION ${BIN_DESTINATION} CACHE PATH "Path for executables")
set (WSJT_LIB_DESTINATION ${LIB_DESTINATION} CACHE PATH "Path for libraries")
set (WSJT_INCLUDE_DESTINATION ${INCLUDE_DESTINATION} CACHE PATH "Path for library headers")
set (WSJT_SHARE_DESTINATION ${SHARE_DESTINATION} CACHE PATH "Path for shared content")
set (WSJT_DOC_DESTINATION ${DOC_DESTINATION} CACHE PATH "Path for documentation")
set (WSJT_DATA_DESTINATION ${DATA_DESTINATION} CACHE PATH "Path for shared RO data")
@ -188,6 +202,7 @@ set (wsjt_qt_CXXSRCS
revision_utils.cpp
WFPalette.cpp
Radio.cpp
RadioMetaType.cpp
Bands.cpp
Modes.cpp
FrequencyList.cpp
@ -512,16 +527,23 @@ set (wsjtx_UISRCS
Configuration.ui
)
set (message_aggregator_CXXSRCS
MessageServer.cpp
MessageAggregator.cpp
)
set (UDPDaemon_CXXSRCS
set (UDP_library_CXXSRCS
Radio.cpp
RadioMetaType.cpp
NetworkMessage.cpp
MessageServer.cpp
Radio.cpp
UDPDaemon.cpp
)
set (message_aggregator_CXXSRCS
UDPExamples/MessageAggregator.cpp
UDPExamples/MessageAggregatorMainWindow.cpp
UDPExamples/DecodesModel.cpp
UDPExamples/BeaconsModel.cpp
UDPExamples/ClientWidget.cpp
)
set (message_aggregator_STYLESHEETS
UDPExamples/qss/default.qss
)
set (all_CXXSRCS
@ -530,8 +552,6 @@ set (all_CXXSRCS
${wsjt_qtmm_CXXSRCS}
${jt9_CXXSRCS}
${wsjtx_CXXSRCS}
${message_aggregator_CXXSRCS}
${UDPDaemon_CXXSRCS}
)
set (all_C_and_CXXSRCS
@ -541,10 +561,6 @@ set (all_C_and_CXXSRCS
${all_CXXSRCS}
)
set (message_aggregator_STYLESHEETS
qss/default.qss
)
set (TOP_LEVEL_RESOURCES
shortcuts.txt
mouse_commands.txt
@ -620,7 +636,7 @@ else (WSJT_QDEBUG_IN_RELEASE)
endif (WSJT_QDEBUG_IN_RELEASE)
set_property (SOURCE ${all_C_and_CXXSRCS} APPEND_STRING PROPERTY COMPILE_FLAGS " -include wsjtx_config.h")
set_property (SOURCE ${all_C_and_CXXSRCS} APPEND PROPERTY OBJECT_DEPENDS wsjtx_config.h)
set_property (SOURCE ${all_C_and_CXXSRCS} APPEND PROPERTY OBJECT_DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/wsjtx_config.h)
if (WIN32)
# generate the OmniRig COM interface source
@ -672,6 +688,18 @@ if (WSJT_GENERATE_DOCS)
add_subdirectory (doc)
endif (WSJT_GENERATE_DOCS)
#
# Library building setup
#
include (GenerateExportHeader)
set (CMAKE_CXX_VISIBILITY_PRESET hidden)
set (CMAKE_C_VISIBILITY_PRESET hidden)
set (CMAKE_Fortran_VISIBILITY_PRESET hidden)
set (CMAKE_VISIBILITY_INLINES_HIDDEN ON)
#set (CMAKE_INCLUDE_CURRENT_DIR_IN_INTERFACE ON)
#
# C & C++ setup
#
@ -688,7 +716,7 @@ if (NOT APPLE)
endif (NOT APPLE)
if (WIN32)
set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fno-keep-inline-dllexport")
set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS}")
endif (WIN32)
if (APPLE)
if (${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang")
@ -849,6 +877,7 @@ set (QT_MKSPECS_DIR ${QT_DATA_DIR}/mkspecs)
# Tell CMake to run moc when necessary
set (CMAKE_AUTOMOC ON)
include_directories (${CMAKE_CURRENT_BINARY_DIR})
# don't use Qt "keywords" signal, slot, emit in generated files to
# avoid compatability issue with other libraries
@ -883,7 +912,7 @@ add_custom_target (etags COMMAND ${ETAGS} -o ${CMAKE_SOURCE_DIR}/TAGS -R ${sourc
function (add_resources resources path)
foreach (resource_file_ ${ARGN})
get_filename_component (name_ ${resource_file_} NAME)
file (TO_NATIVE_PATH ${CMAKE_SOURCE_DIR}/${resource_file_} source_)
file (TO_NATIVE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/${resource_file_} source_)
file (TO_NATIVE_PATH ${path}/${name_} dest_)
set (resources_ "${resources_}\n <file alias=\"${dest_}\">${source_}</file>")
set (${resources} ${${resources}}${resources_} PARENT_SCOPE)
@ -942,6 +971,8 @@ endif (${OPENMP_FOUND} OR APPLE)
# build a library of package Qt functionality
add_library (wsjt_qt STATIC ${wsjt_qt_CXXSRCS} ${wsjt_qt_GENUISRCS} ${GENAXSRCS})
# set wsjtx_udp exports to static variants
set_target_properties (wsjt_qt PROPERTIES COMPILE_FLAGS -DUDP_STATIC_DEFINE)
target_link_libraries (wsjt_qt Qt5::Widgets Qt5::Network)
target_include_directories (wsjt_qt BEFORE PRIVATE ${hamlib_INCLUDE_DIRS})
if (WIN32)
@ -1033,24 +1064,34 @@ set_target_properties (wsjtx PROPERTIES
MACOSX_BUNDLE_GUI_IDENTIFIER "org.k1jt.wsjtx"
)
# set wsjtx_udp exports to static variants
set_target_properties (wsjtx PROPERTIES COMPILE_FLAGS -DUDP_STATIC_DEFINE)
target_link_libraries (wsjtx wsjt_fort wsjt_cxx wsjt_qt wsjt_qtmm ${hamlib_LIBRARIES} ${FFTW3_LIBRARIES})
qt5_use_modules (wsjtx SerialPort) # not sure why the interface link library syntax above doesn't work
# make a library for WSJT-X UDP servers
add_library (wsjtx_udp ${LIB_TYPE} ${UDP_library_CXXSRCS})
target_include_directories (wsjtx_udp
INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>
$<INSTALL_INTERFACE:${WSJT_INCLUDE_DESTINATION}/wsjtx>
)
qt5_use_modules (wsjtx_udp Network)
generate_export_header (wsjtx_udp BASE_NAME udp)
add_executable (udp_daemon UDPExamples/UDPDaemon.cpp UDPExamples/udp_daemon.rc)
target_link_libraries (udp_daemon wsjtx_udp)
add_resources (message_aggregator_RESOURCES /qss ${message_aggregator_STYLESHEETS})
configure_file (message_aggregator.qrc.in message_aggregator.qrc @ONLY)
qt5_add_resources (message_aggregator_RESOURCES_RCC ${CMAKE_BINARY_DIR}/message_aggregator.qrc)
configure_file (UDPExamples/message_aggregator.qrc.in message_aggregator.qrc @ONLY)
qt5_add_resources (message_aggregator_RESOURCES_RCC ${CMAKE_CURRENT_BINARY_DIR}/message_aggregator.qrc)
add_executable (message_aggregator
${message_aggregator_CXXSRCS}
wsjtx.rc
UDPExamples/message_aggregator.rc
${message_aggregator_RESOURCES_RCC}
)
target_link_libraries (message_aggregator wsjt_qt Qt5::Widgets)
add_executable (udp_daemon
${UDPDaemon_CXXSRCS}
wsjtx.rc
)
target_link_libraries (udp_daemon Qt5::Core Qt5::Network)
target_link_libraries (message_aggregator wsjt_qt Qt5::Widgets wsjtx_udp)
if (WSJT_CREATE_WINMAIN)
set_target_properties (message_aggregator PROPERTIES WIN32_EXECUTABLE ON)
@ -1075,7 +1116,25 @@ install (TARGETS wsjtx
BUNDLE DESTINATION . COMPONENT runtime
)
install (TARGETS jt9 jt65code jt9code jt4code wsprd message_aggregator udp_daemon
install (TARGETS wsjtx_udp EXPORT udp
DESTINATION ${WSJT_LIB_DESTINATION}
)
install (EXPORT udp NAMESPACE wsjtx::
DESTINATION ${WSJT_LIB_DESTINATION}/cmake/wsjtx
)
export (EXPORT udp NAMESPACE wsjtx:: FILE udp-exports.cmake)
install (FILES
Radio.hpp
MessageServer.hpp
${PROJECT_BINARY_DIR}/udp_export.h
DESTINATION ${WSJT_INCLUDE_DESTINATION}/wsjtx)
install (TARGETS udp_daemon message_aggregator
RUNTIME DESTINATION ${WSJT_BIN_DESTINATION} COMPONENT runtime
BUNDLE DESTINATION ${WSJT_BIN_DESTINATION} COMPONENT runtime
)
install (TARGETS jt9 jt65code jt9code jt4code wsprd
RUNTIME DESTINATION ${WSJT_BIN_DESTINATION} COMPONENT runtime
BUNDLE DESTINATION ${WSJT_BIN_DESTINATION} COMPONENT runtime
)
@ -1152,10 +1211,9 @@ add_dependencies(wsjt_qt revisiontag)
# versioning and configuration
#
configure_file (
"${PROJECT_SOURCE_DIR}/wsjtx_config.h.in"
"${PROJECT_BINARY_DIR}/wsjtx_config.h"
"${CMAKE_CURRENT_SOURCE_DIR}/wsjtx_config.h.in"
"${CMAKE_CURRENT_BINARY_DIR}/wsjtx_config.h"
)
include_directories (BEFORE "${PROJECT_BINARY_DIR}")
if (NOT WIN32 AND NOT APPLE)
@ -1258,7 +1316,7 @@ if (NOT is_debug_build)
#set (hamlib_lib_dir ${hamlib_lib_dir}/../bin)
get_filename_component (fftw_lib_dir ${FFTW3F_LIBRARY} PATH)
list (APPEND fixup_library_dirs ${fftw_lib_dir})
list (APPEND fixup_library_dirs ${WSJT_LIB_DESTINATION} ${fftw_lib_dir})
# install required Qt plugins
install (

View File

@ -2,6 +2,7 @@
#include <utility>
#include <QMetaType>
#include <QAbstractTableModel>
#include <QString>
#include <QList>

View File

@ -1,687 +0,0 @@
//
// MessageAggregator - an example application that utilizes the WSJT-X
// messaging facility
//
// This application is only provided as a simple GUI application
// example to demonstrate the WSJT-X messaging facility. It allows the
// user to set the server details either as a unicast UDP server or,
// if a multicast group address is provided, as a multicast server.
// The benefit of the multicast server is that multiple servers can be
// active at once each receiving all WSJT-X broadcast messages and
// each able to respond to individual WSJT_X clients. To utilize the
// multicast group features each WSJT-X client must set the same
// multicast group address as the UDP server address for example
// 239.255.0.0 for a site local multicast group.
//
// The UI is a small panel to input the service port number and
// optionally the multicast group address. Below that a table
// representing the log entries where any QSO logged messages
// broadcast from WSJT-X clients are displayed. The bottom of the
// application main window is a dock area where a dock window will
// appear for each WSJT-X client, this window contains a table of the
// current decode messages broadcast from that WSJT-X client and a
// status line showing the status update messages broadcast from the
// WSJT_X client. The dock windows may be arranged in a tab bar, side
// by side, below each other or, completely detached from the dock
// area as floating windows. Double clicking the dock window title bar
// or dragging and dropping with the mouse allows these different
// arrangements.
//
// The application also provides a simple menu bar including a view
// menu that allows each dock window to be hidden or revealed.
//
#include <iostream>
#include <exception>
#include <QtWidgets>
#include <QFile>
#include <QStandardItemModel>
#include <QStandardItem>
#include <QSortFilterProxyModel>
#include <QFont>
#include <QDateTime>
#include <QTime>
#include <QHash>
#include "MessageServer.hpp"
#include "NetworkMessage.hpp"
#include "qt_helpers.hpp"
using port_type = MessageServer::port_type;
using Frequency = MessageServer::Frequency;
//QRegExp message_alphabet {"[- A-Za-z0-9+./?]*"};
QRegExp message_alphabet {"[- @A-Za-z0-9+./?#<>]*"};
//
// Decodes Model - simple data model for all decodes
//
// The model is a basic table with uniform row format. Rows consist of
// QStandardItem instances containing the string representation of the
// column data and if the underlying field is not a string then the
// UserRole+1 role contains the underlying data item.
//
// Three slots are provided to add a new decode, remove all decodes
// for a client and, to build a reply to CQ message for a given row
// which is emitted as a signal respectively.
//
class DecodesModel
: public QStandardItemModel
{
Q_OBJECT;
public:
DecodesModel (QObject * parent = nullptr)
: QStandardItemModel {0, 7, parent}
, text_font_ {"Courier", 10}
{
setHeaderData (0, Qt::Horizontal, tr ("Client"));
setHeaderData (1, Qt::Horizontal, tr ("Time"));
setHeaderData (2, Qt::Horizontal, tr ("Snr"));
setHeaderData (3, Qt::Horizontal, tr ("DT"));
setHeaderData (4, Qt::Horizontal, tr ("DF"));
setHeaderData (5, Qt::Horizontal, tr ("Md"));
setHeaderData (6, Qt::Horizontal, tr ("Message"));
}
Q_SLOT void add_decode (bool is_new, QString const& client_id, QTime time, qint32 snr, float delta_time
, quint32 delta_frequency, QString const& mode, QString const& message)
{
if (!is_new)
{
int target_row {-1};
for (auto row = 0; row < rowCount (); ++row)
{
if (data (index (row, 0)).toString () == client_id)
{
auto row_time = item (row, 1)->data ().toTime ();
if (row_time == time
&& item (row, 2)->data ().toInt () == snr
&& item (row, 3)->data ().toFloat () == delta_time
&& item (row, 4)->data ().toUInt () == delta_frequency
&& data (index (row, 5)).toString () == mode
&& data (index (row, 6)).toString () == message)
{
return;
}
if (time <= row_time)
{
target_row = row; // last row with same time
}
}
}
if (target_row >= 0)
{
insertRow (target_row + 1, make_row (client_id, time, snr, delta_time, delta_frequency, mode, message));
return;
}
}
appendRow (make_row (client_id, time, snr, delta_time, delta_frequency, mode, message));
}
QList<QStandardItem *> make_row (QString const& client_id, QTime time, qint32 snr, float delta_time
, quint32 delta_frequency, QString const& mode, QString const& message) const
{
auto time_item = new QStandardItem {time.toString ("hh:mm")};
time_item->setData (time);
time_item->setTextAlignment (Qt::AlignRight);
auto snr_item = new QStandardItem {QString::number (snr)};
snr_item->setData (snr);
snr_item->setTextAlignment (Qt::AlignRight);
auto dt = new QStandardItem {QString::number (delta_time)};
dt->setData (delta_time);
dt->setTextAlignment (Qt::AlignRight);
auto df = new QStandardItem {QString::number (delta_frequency)};
df->setData (delta_frequency);
df->setTextAlignment (Qt::AlignRight);
auto md = new QStandardItem {mode};
md->setTextAlignment (Qt::AlignHCenter);
QList<QStandardItem *> row {
new QStandardItem {client_id}, time_item, snr_item, dt, df, md, new QStandardItem {message}};
Q_FOREACH (auto& item, row)
{
item->setEditable (false);
item->setFont (text_font_);
item->setTextAlignment (item->textAlignment () | Qt::AlignVCenter);
}
return row;
}
Q_SLOT void clear_decodes (QString const& client_id)
{
for (auto row = rowCount () - 1; row >= 0; --row)
{
if (data (index (row, 0)).toString () == client_id)
{
removeRow (row);
}
}
}
Q_SLOT void do_reply (QModelIndex const& source)
{
auto row = source.row ();
Q_EMIT reply (data (index (row, 0)).toString ()
, item (row, 1)->data ().toTime ()
, item (row, 2)->data ().toInt ()
, item (row, 3)->data ().toFloat ()
, item (row, 4)->data ().toInt ()
, data (index (row, 5)).toString ()
, data (index (row, 6)).toString ());
}
Q_SIGNAL void reply (QString const& id, QTime time, qint32 snr, float delta_time, quint32 delta_frequency
, QString const& mode, QString const& message);
private:
QFont text_font_;
};
//
// Beacons Model - simple data model for all beacon spots
//
// 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.
//
// Two slots are provided to add a new decode and remove all spots for
// a client.
//
class BeaconsModel
: public QStandardItemModel
{
Q_OBJECT;
public:
BeaconsModel (QObject * parent = nullptr)
: QStandardItemModel {0, 9, 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 ("Frequency"));
setHeaderData (5, Qt::Horizontal, tr ("Drift"));
setHeaderData (6, Qt::Horizontal, tr ("Callsign"));
setHeaderData (7, Qt::Horizontal, tr ("Grid"));
setHeaderData (8, Qt::Horizontal, tr ("Power"));
}
Q_SLOT void add_beacon_spot (bool is_new, QString const& client_id, QTime time, qint32 snr, float delta_time
, Frequency frequency, qint32 drift, QString const& callsign, QString const& grid
, qint32 power)
{
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 ().value<Frequency> () == frequency
&& data (index (row, 5)).toInt () == drift
&& data (index (row, 6)).toString () == callsign
&& data (index (row, 7)).toString () == grid
&& data (index (row, 8)).toInt () == power)
{
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, frequency, drift, callsign, grid, power));
return;
}
}
appendRow (make_row (client_id, time, snr, delta_time, frequency, drift, callsign, grid, power));
}
QList<QStandardItem *> make_row (QString const& client_id, QTime time, qint32 snr, float delta_time
, Frequency frequency, qint32 drift, QString const& callsign
, QString const& grid, qint32 power) 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 freq = new QStandardItem {Radio::pretty_frequency_MHz_string (frequency)};
freq->setData (frequency);
freq->setTextAlignment (Qt::AlignRight);
auto dri = new QStandardItem {QString::number (drift)};
dri->setData (drift);
dri->setTextAlignment (Qt::AlignRight);
auto gd = new QStandardItem {grid};
gd->setTextAlignment (Qt::AlignRight);
auto pwr = new QStandardItem {QString::number (power)};
pwr->setData (power);
pwr->setTextAlignment (Qt::AlignRight);
QList<QStandardItem *> row {
new QStandardItem {client_id}, time_item, snr_item, dt, freq, dri, new QStandardItem {callsign}, gd, pwr};
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);
}
}
}
private:
QFont text_font_;
};
class ClientWidget
: public QDockWidget
{
Q_OBJECT;
public:
explicit ClientWidget (QAbstractItemModel * decodes_model, QAbstractItemModel * beacons_model
, QString const& id, QWidget * parent = 0)
: QDockWidget {id, parent}
, id_ {id}
, decodes_table_view_ {new QTableView}
, beacons_table_view_ {new QTableView}
, message_line_edit_ {new QLineEdit}
, decodes_stack_ {new QStackedLayout}
, auto_off_button_ {new QPushButton {tr ("&Auto Off")}}
, halt_tx_button_ {new QPushButton {tr ("&Halt Tx")}}
, mode_label_ {new QLabel}
, dx_call_label_ {new QLabel}
, frequency_label_ {new QLabel}
, report_label_ {new QLabel}
{
// set up widgets
auto decodes_proxy_model = new IdFilterModel {id, this};
decodes_proxy_model->setSourceModel (decodes_model);
decodes_table_view_->setModel (decodes_proxy_model);
decodes_table_view_->verticalHeader ()->hide ();
decodes_table_view_->hideColumn (0);
decodes_table_view_->horizontalHeader ()->setStretchLastSection (true);
auto form_layout = new QFormLayout;
form_layout->addRow (tr ("Free text:"), message_line_edit_);
message_line_edit_->setValidator (new QRegExpValidator {message_alphabet, this});
connect (message_line_edit_, &QLineEdit::textEdited, [this] (QString const& text) {
Q_EMIT do_free_text (id_, text, false);
});
connect (message_line_edit_, &QLineEdit::editingFinished, [this] () {
Q_EMIT do_free_text (id_, message_line_edit_->text (), true);
});
auto decodes_page = new QWidget;
auto decodes_layout = new QVBoxLayout {decodes_page};
decodes_layout->setContentsMargins (QMargins {2, 2, 2, 2});
decodes_layout->addWidget (decodes_table_view_);
decodes_layout->addLayout (form_layout);
auto beacons_proxy_model = new IdFilterModel {id, this};
beacons_proxy_model->setSourceModel (beacons_model);
beacons_table_view_->setModel (beacons_proxy_model);
beacons_table_view_->verticalHeader ()->hide ();
beacons_table_view_->hideColumn (0);
beacons_table_view_->horizontalHeader ()->setStretchLastSection (true);
auto beacons_page = new QWidget;
auto beacons_layout = new QVBoxLayout {beacons_page};
beacons_layout->setContentsMargins (QMargins {2, 2, 2, 2});
beacons_layout->addWidget (beacons_table_view_);
decodes_stack_->addWidget (decodes_page);
decodes_stack_->addWidget (beacons_page);
// stack alternative views
auto content_layout = new QVBoxLayout;
content_layout->setContentsMargins (QMargins {2, 2, 2, 2});
content_layout->addLayout (decodes_stack_);
// set up controls
auto control_button_box = new QDialogButtonBox;
control_button_box->addButton (auto_off_button_, QDialogButtonBox::ActionRole);
control_button_box->addButton (halt_tx_button_, QDialogButtonBox::ActionRole);
connect (auto_off_button_, &QAbstractButton::clicked, [this] (bool /* checked */) {
Q_EMIT do_halt_tx (id_, true);
});
connect (halt_tx_button_, &QAbstractButton::clicked, [this] (bool /* checked */) {
Q_EMIT do_halt_tx (id_, false);
});
content_layout->addWidget (control_button_box);
// 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, decodes_proxy_model] (QModelIndex const& index) {
Q_EMIT do_reply (decodes_proxy_model->mapToSource (index));
});
}
Q_SLOT void update_status (QString const& id, Frequency f, QString const& mode, QString const& dx_call
, QString const& report, QString const& tx_mode, bool tx_enabled
, bool transmitting, bool decoding)
{
if (id == id_)
{
mode_label_->setText (QString {"Mode: %1%2"}
.arg (mode)
.arg (tx_mode.isEmpty () || tx_mode == mode ? "" : '(' + tx_mode + ')'));
dx_call_label_->setText ("DX CALL: " + dx_call);
frequency_label_->setText ("QRG: " + Radio::pretty_frequency_MHz_string (f));
report_label_->setText ("SNR: " + report);
update_dynamic_property (frequency_label_, "transmitting", transmitting);
auto_off_button_->setEnabled (tx_enabled);
halt_tx_button_->setEnabled (transmitting);
update_dynamic_property (mode_label_, "decoding", decoding);
}
}
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_stack_->setCurrentIndex (0);
decodes_table_view_->resizeColumnsToContents ();
decodes_table_view_->scrollToBottom ();
}
}
Q_SLOT void beacon_spot_added (bool /*is_new*/, QString const& client_id, QTime /*time*/, qint32 /*snr*/
, float /*delta_time*/, Frequency /*delta_frequency*/, qint32 /*drift*/, QString const& /*callsign*/
, QString const& /*grid*/, qint32 /*power*/)
{
if (client_id == id_)
{
decodes_stack_->setCurrentIndex (1);
beacons_table_view_->resizeColumnsToContents ();
beacons_table_view_->scrollToBottom ();
}
}
Q_SIGNAL void do_reply (QModelIndex const&);
Q_SIGNAL void do_halt_tx (QString const& id, bool auto_only);
Q_SIGNAL void do_free_text (QString const& id, QString const& text, bool);
private:
class IdFilterModel final
: public QSortFilterProxyModel
{
public:
IdFilterModel (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_;
QTableView * beacons_table_view_;
QLineEdit * message_line_edit_;
QStackedLayout * decodes_stack_;
QAbstractButton * auto_off_button_;
QAbstractButton * halt_tx_button_;
QLabel * mode_label_;
QLabel * dx_call_label_;
QLabel * frequency_label_;
QLabel * report_label_;
};
class MainWindow
: public QMainWindow
{
Q_OBJECT;
public:
MainWindow ()
: log_ {new QStandardItemModel {0, 10, this}}
, decodes_model_ {new DecodesModel {this}}
, beacons_model_ {new BeaconsModel {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::client_closed, beacons_model_, &BeaconsModel::clear_decodes);
connect (server_, &MessageServer::decode, decodes_model_, &DecodesModel::add_decode);
connect (server_, &MessageServer::WSPR_decode, beacons_model_, &BeaconsModel::add_beacon_spot);
connect (server_, &MessageServer::clear_decodes, decodes_model_, &DecodesModel::clear_decodes);
connect (server_, &MessageServer::clear_decodes, beacons_model_, &BeaconsModel::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_, beacons_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 (server_, &MessageServer::WSPR_decode, dock, &ClientWidget::beacon_spot_added);
connect (dock, &ClientWidget::do_reply, decodes_model_, &DecodesModel::do_reply);
connect (dock, &ClientWidget::do_halt_tx, server_, &MessageServer::halt_tx);
connect (dock, &ClientWidget::do_free_text, server_, &MessageServer::free_text);
connect (view_action, &QAction::toggled, dock, &ClientWidget::setVisible);
dock_widgets_[id] = dock;
server_->replay (id);
}
void remove_client (QString const& id)
{
auto iter = dock_widgets_.find (id);
if (iter != std::end (dock_widgets_))
{
(*iter)->close ();
dock_widgets_.erase (iter);
}
}
QStandardItemModel * log_;
QMenu * view_menu_;
DecodesModel * decodes_model_;
BeaconsModel * beacons_model_;
MessageServer * server_;
QLineEdit * multicast_group_line_edit_;
QTableView * log_table_view_;
// maps client id to widgets
QHash<QString, ClientWidget *> dock_widgets_;
};
#include "MessageAggregator.moc"
int main (int argc, char * argv[])
{
QApplication app {argc, argv};
try
{
QObject::connect (&app, SIGNAL (lastWindowClosed ()), &app, SLOT (quit ()));
app.setApplicationName ("WSJT-X Reference UDP Message Aggregator Server");
app.setApplicationVersion ("1.0");
{
QFile file {":/qss/default.qss"};
if (!file.open (QFile::ReadOnly))
{
throw_qstring ("failed to open \"" + file.fileName () + "\": " + file.errorString ());
}
app.setStyleSheet (file.readAll());
}
MainWindow window;
return app.exec ();
}
catch (std::exception const & e)
{
QMessageBox::critical (nullptr, app.applicationName (), e.what ());
std:: cerr << "Error: " << e.what () << '\n';
}
catch (...)
{
QMessageBox::critical (nullptr, app.applicationName (), QObject::tr ("Unexpected error"));
std:: cerr << "Unexpected error\n";
}
return -1;
}

View File

@ -331,14 +331,17 @@ void MessageClient::send_raw_datagram (QByteArray const& message, QHostAddress c
void MessageClient::status_update (Frequency f, QString const& mode, QString const& dx_call
, QString const& report, QString const& tx_mode
, bool tx_enabled, bool transmitting, bool decoding)
, bool tx_enabled, bool transmitting, bool decoding
, qint32 rx_df, qint32 tx_df, QString const& de_call
, QString const& de_grid, QString const& dx_grid)
{
if (m_->server_port_ && !m_->server_string_.isEmpty ())
{
QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::Status, m_->id_, m_->schema_};
out << f << mode.toUtf8 () << dx_call.toUtf8 () << report.toUtf8 () << tx_mode.toUtf8 ()
<< tx_enabled << transmitting << decoding;
<< tx_enabled << transmitting << decoding << rx_df << tx_df << de_call.toUtf8 ()
<< de_grid.toUtf8 () << dx_grid.toUtf8 ();
m_->send_message (out, message);
}
}

View File

@ -47,7 +47,9 @@ public:
// outgoing messages
Q_SLOT void status_update (Frequency, QString const& mode, QString const& dx_call, QString const& report
, QString const& tx_mode, bool tx_enabled, bool transmitting, bool decoding);
, QString const& tx_mode, bool tx_enabled, bool transmitting, bool decoding
, qint32 rx_df, qint32 tx_df, QString const& de_call, QString const& de_grid
, QString const& dx_grid);
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 WSPR_decode (bool is_new, QTime time, qint32 snr, float delta_time, Frequency

View File

@ -7,6 +7,7 @@
#include <QTimer>
#include <QHash>
#include "Radio.hpp"
#include "NetworkMessage.hpp"
#include "qt_helpers.hpp"
@ -25,6 +26,9 @@ public:
, port_ {0u}
, clock_ {new QTimer {this}}
{
// register the required types with Qt
Radio::register_types ();
connect (this, &QIODevice::readyRead, this, &MessageServer::impl::pending_datagrams);
connect (this, static_cast<void (impl::*) (SocketError)> (&impl::error)
, [this] (SocketError /* e */)
@ -194,12 +198,20 @@ void MessageServer::impl::parse_message (QHostAddress const& sender, port_type s
bool tx_enabled {false};
bool transmitting {false};
bool decoding {false};
in >> f >> mode >> dx_call >> report >> tx_mode >> tx_enabled >> transmitting >> decoding;
qint32 rx_df {-1};
qint32 tx_df {-1};
QByteArray de_call;
QByteArray de_grid;
QByteArray dx_grid;
in >> f >> mode >> dx_call >> report >> tx_mode >> tx_enabled >> transmitting >> decoding
>> rx_df >> tx_df >> de_call >> de_grid >> dx_grid;
if (check_status (in) != Fail)
{
Q_EMIT self_->status_update (id, f, QString::fromUtf8 (mode), QString::fromUtf8 (dx_call)
, QString::fromUtf8 (report), QString::fromUtf8 (tx_mode)
, tx_enabled, transmitting, decoding);
, tx_enabled, transmitting, decoding, rx_df, tx_df
, QString::fromUtf8 (de_call), QString::fromUtf8 (de_grid)
, QString::fromUtf8 (dx_grid));
}
}
break;

View File

@ -6,6 +6,7 @@
#include <QDateTime>
#include <QHostAddress>
#include "udp_export.h"
#include "Radio.hpp"
#include "pimpl_h.hpp"
@ -21,7 +22,7 @@ class QString;
// applications that use the Qt framework. Other applications should
// use this classes' implementation as a reference implementation.
//
class MessageServer
class UDP_EXPORT MessageServer
: public QObject
{
Q_OBJECT;
@ -61,7 +62,8 @@ public:
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, bool tx_enabled
, bool transmitting, bool decoding);
, bool transmitting, bool decoding, qint32 rx_df, qint32 tx_df
, QString const& de_call, QString const& de_grid, QString const& dx_grid);
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);
@ -77,7 +79,7 @@ public:
Q_SIGNAL void error (QString const&) const;
private:
class impl;
class UDP_NO_EXPORT impl;
pimpl<impl> m_;
};

View File

@ -23,22 +23,16 @@ QItemEditorFactory * item_editor_factory ()
void register_types ()
{
// types in Radio.hpp are registered in their own translation unit
// as they are needed in the wsjtx_udp shared library too
// we still have to register the fully qualified names of enum types
// used as signal/slot connection arguments since the new Qt 5.5
// Q_ENUM macro only seems to register the unqualified name
// Radio namespace
auto frequency_type_id = qRegisterMetaType<Radio::Frequency> ("Frequency");
qRegisterMetaType<Radio::Frequencies> ("Frequencies");
// This is required to preserve v1.5 "frequencies" setting for
// backwards compatibility, without it the setting gets trashed by
// later versions.
qRegisterMetaTypeStreamOperators<Radio::Frequencies> ("Frequencies");
item_editor_factory ()->registerEditor (frequency_type_id, new QStandardItemEditorCreator<FrequencyLineEdit> ());
auto frequency_delta_type_id = qRegisterMetaType<Radio::FrequencyDelta> ("FrequencyDelta");
item_editor_factory ()->registerEditor (frequency_delta_type_id, new QStandardItemEditorCreator<FrequencyDeltaLineEdit> ());
item_editor_factory ()->registerEditor (qMetaTypeId<Radio::Frequency> (), new QStandardItemEditorCreator<FrequencyLineEdit> ());
//auto frequency_delta_type_id = qRegisterMetaType<Radio::FrequencyDelta> ("FrequencyDelta");
item_editor_factory ()->registerEditor (qMetaTypeId<Radio::FrequencyDelta> (), new QStandardItemEditorCreator<FrequencyDeltaLineEdit> ());
// Frequency list model
qRegisterMetaType<FrequencyList::Item> ("Item");

View File

@ -114,6 +114,11 @@
* Tx Enabled bool
* Transmitting bool
* Decoding bool
* Rx DF qint32
* Tx DF qint32
* DE call utf8
* DE grid utf8
* DX grid utf8
*
* WSJT-X sends this status message when various internal state
* changes to allow the server to track the relevant state of each
@ -129,7 +134,11 @@
* Changes to the "Rpt" spinner,
* After an old decodes replay sequence (see Replay below),
* When switching between Tx and Rx mode,
* At the start and end of decoding.
* At the start and end of decoding,
* When the Rx DF changes,
* When the Tx DF changes,
* When the DE call or grid changes (currently when settings are exited),
* When the DX call or grid changes.
*
*
* Decode Out 2 quint32

View File

@ -4,8 +4,6 @@
#include <QString>
#include <QChar>
#include <QDebug>
#include <QDataStream>
#include <QRegularExpression>
namespace Radio

View File

@ -1,8 +1,11 @@
#ifndef RADIO_HPP_
#define RADIO_HPP_
#ifndef RADIO_HPP__
#define RADIO_HPP__
#include <QObject>
#include <QLocale>
#include <QList>
#include "udp_export.h"
class QVariant;
class QString;
@ -20,30 +23,35 @@ namespace Radio
using Frequencies = QList<Frequency>;
using FrequencyDelta = qint64;
//
// Qt type registration
//
void UDP_NO_EXPORT register_types ();
//
// Frequency type conversion.
//
// QVariant argument is convertible to double and is assumed to
// be scaled by (10 ** -scale).
//
Frequency frequency (QVariant const&, int scale, QLocale const& = QLocale ());
FrequencyDelta frequency_delta (QVariant const&, int scale, QLocale const& = QLocale ());
Frequency UDP_EXPORT frequency (QVariant const&, int scale, QLocale const& = QLocale ());
FrequencyDelta UDP_EXPORT frequency_delta (QVariant const&, int scale, QLocale const& = QLocale ());
//
// Frequency type formatting
//
QString frequency_MHz_string (Frequency, QLocale const& = QLocale ());
QString frequency_MHz_string (FrequencyDelta, QLocale const& = QLocale ());
QString pretty_frequency_MHz_string (Frequency, QLocale const& = QLocale ());
QString pretty_frequency_MHz_string (double, int scale, QLocale const& = QLocale ());
QString pretty_frequency_MHz_string (FrequencyDelta, QLocale const& = QLocale ());
QString UDP_EXPORT frequency_MHz_string (Frequency, QLocale const& = QLocale ());
QString UDP_EXPORT frequency_MHz_string (FrequencyDelta, QLocale const& = QLocale ());
QString UDP_EXPORT pretty_frequency_MHz_string (Frequency, QLocale const& = QLocale ());
QString UDP_EXPORT pretty_frequency_MHz_string (double, int scale, QLocale const& = QLocale ());
QString UDP_EXPORT pretty_frequency_MHz_string (FrequencyDelta, QLocale const& = QLocale ());
//
// Callsigns
//
bool is_callsign (QString const&);
bool is_compound_callsign (QString const&);
QString base_callsign (QString);
bool UDP_EXPORT is_callsign (QString const&);
bool UDP_EXPORT is_compound_callsign (QString const&);
QString UDP_EXPORT base_callsign (QString);
}
Q_DECLARE_METATYPE (Radio::Frequency);

20
RadioMetaType.cpp Normal file
View File

@ -0,0 +1,20 @@
#include "Radio.hpp"
#include <QMetaType>
#include <QDebug>
#include <QDataStream>
namespace Radio
{
void register_types ()
{
qRegisterMetaType<Radio::Frequency> ("Frequency");
qRegisterMetaType<Radio::FrequencyDelta> ("FrequencyDelta");
qRegisterMetaType<Radio::Frequencies> ("Frequencies");
// This is required to preserve v1.5 "frequencies" setting for
// backwards compatibility, without it the setting gets trashed
// by later versions.
qRegisterMetaTypeStreamOperators<Radio::Frequencies> ("Frequencies");
}
}

View File

@ -0,0 +1,125 @@
#include "BeaconsModel.hpp"
#include <QStandardItem>
#include <QFont>
namespace
{
char const * const headings[] = {
QT_TRANSLATE_NOOP ("BeaconsModel", "Client"),
QT_TRANSLATE_NOOP ("BeaconsModel", "Time"),
QT_TRANSLATE_NOOP ("BeaconsModel", "Snr"),
QT_TRANSLATE_NOOP ("BeaconsModel", "DT"),
QT_TRANSLATE_NOOP ("BeaconsModel", "Frequency"),
QT_TRANSLATE_NOOP ("BeaconsModel", "Drift"),
QT_TRANSLATE_NOOP ("BeaconsModel", "Callsign"),
QT_TRANSLATE_NOOP ("BeaconsModel", "Grid"),
QT_TRANSLATE_NOOP ("BeaconsModel", "Power"),
};
QFont text_font {"Courier", 10};
QList<QStandardItem *> make_row (QString const& client_id, QTime time, qint32 snr, float delta_time
, Frequency frequency, qint32 drift, QString const& callsign
, QString const& grid, qint32 power)
{
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 freq = new QStandardItem {Radio::pretty_frequency_MHz_string (frequency)};
freq->setData (frequency);
freq->setTextAlignment (Qt::AlignRight);
auto dri = new QStandardItem {QString::number (drift)};
dri->setData (drift);
dri->setTextAlignment (Qt::AlignRight);
auto gd = new QStandardItem {grid};
gd->setTextAlignment (Qt::AlignRight);
auto pwr = new QStandardItem {QString::number (power)};
pwr->setData (power);
pwr->setTextAlignment (Qt::AlignRight);
QList<QStandardItem *> row {
new QStandardItem {client_id}, time_item, snr_item, dt, freq, dri, new QStandardItem {callsign}, gd, pwr};
Q_FOREACH (auto& item, row)
{
item->setEditable (false);
item->setFont (text_font);
item->setTextAlignment (item->textAlignment () | Qt::AlignVCenter);
}
return row;
}
}
BeaconsModel::BeaconsModel (QObject * parent)
: QStandardItemModel {0, 9, parent}
{
int column {0};
for (auto const& heading : headings)
{
setHeaderData (column++, Qt::Horizontal, tr (heading));
}
}
void BeaconsModel::add_beacon_spot (bool is_new, QString const& client_id, QTime time, qint32 snr, float delta_time
, Frequency frequency, qint32 drift, QString const& callsign
, QString const& grid, qint32 power)
{
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 ().value<Frequency> () == frequency
&& data (index (row, 5)).toInt () == drift
&& data (index (row, 6)).toString () == callsign
&& data (index (row, 7)).toString () == grid
&& data (index (row, 8)).toInt () == power)
{
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, frequency, drift, callsign, grid, power));
return;
}
}
appendRow (make_row (client_id, time, snr, delta_time, frequency, drift, callsign, grid, power));
}
void BeaconsModel::clear_decodes (QString const& client_id)
{
for (auto row = rowCount () - 1; row >= 0; --row)
{
if (data (index (row, 0)).toString () == client_id)
{
removeRow (row);
}
}
}
#include "moc_BeaconsModel.cpp"

View File

@ -0,0 +1,38 @@
#ifndef WSJTX_UDP_BEACONS_MODEL_HPP__
#define WSJTX_UDP_BEACONS_MODEL_HPP__
#include <QStandardItemModel>
#include "MessageServer.hpp"
using Frequency = MessageServer::Frequency;
class QString;
class QTime;
//
// Beacons Model - simple data model for all beacon spots
//
// 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.
//
// Two slots are provided to add a new decode and remove all spots for
// a client.
//
class BeaconsModel
: public QStandardItemModel
{
Q_OBJECT;
public:
explicit BeaconsModel (QObject * parent = nullptr);
Q_SLOT void add_beacon_spot (bool is_new, QString const& client_id, QTime time, qint32 snr, float delta_time
, Frequency frequency, qint32 drift, QString const& callsign, QString const& grid
, qint32 power);
Q_SLOT void clear_decodes (QString const& client_id);
};
#endif

View File

@ -0,0 +1,241 @@
#include "ClientWidget.hpp"
#include <QRegExp>
#include <QColor>
namespace
{
//QRegExp message_alphabet {"[- A-Za-z0-9+./?]*"};
QRegExp message_alphabet {"[- @A-Za-z0-9+./?#<>]*"};
QRegularExpression cq_re {"[^A-Z0-9]*(CQ|QRZ)[^A-Z0-9]*"};
void update_dynamic_property (QWidget * widget, char const * property, QVariant const& value)
{
widget->setProperty (property, value);
widget->style ()->unpolish (widget);
widget->style ()->polish (widget);
widget->update ();
}
}
ClientWidget::IdFilterModel::IdFilterModel (QString const& client_id)
: client_id_ {client_id}
, rx_df_ (-1)
{
}
QVariant ClientWidget::IdFilterModel::data (QModelIndex const& proxy_index, int role) const
{
if (role == Qt::BackgroundRole)
{
switch (proxy_index.column ())
{
case 6: // message
{
auto message = QSortFilterProxyModel::data (proxy_index).toString ();
if (base_call_re_.pattern ().size ()
&& message.contains (base_call_re_))
{
return QColor {255,200,200};
}
if (message.contains (cq_re))
{
return QColor {200, 255, 200};
}
}
break;
case 4: // DF
if (qAbs (QSortFilterProxyModel::data (proxy_index).toInt () - rx_df_) <= 10)
{
return QColor {255, 200, 200};
}
break;
default:
break;
}
}
return QSortFilterProxyModel::data (proxy_index, role);
}
bool ClientWidget::IdFilterModel::filterAcceptsRow (int source_row
, QModelIndex const& source_parent) const
{
auto source_index_col0 = sourceModel ()->index (source_row, 0, source_parent);
return sourceModel ()->data (source_index_col0).toString () == client_id_;
}
void ClientWidget::IdFilterModel::de_call (QString const& call)
{
if (call != call_)
{
beginResetModel ();
if (call.size ())
{
base_call_re_.setPattern ("[^A-Z0-9]*" + Radio::base_callsign (call) + "[^A-Z0-9]*");
}
else
{
base_call_re_.setPattern (QString {});
}
call_ = call;
endResetModel ();
}
}
void ClientWidget::IdFilterModel::rx_df (int df)
{
if (df != rx_df_)
{
beginResetModel ();
rx_df_ = df;
endResetModel ();
}
}
ClientWidget::ClientWidget (QAbstractItemModel * decodes_model, QAbstractItemModel * beacons_model
, QString const& id, QWidget * parent)
: QDockWidget {id, parent}
, id_ {id}
, decodes_proxy_model_ {id_}
, decodes_table_view_ {new QTableView}
, beacons_table_view_ {new QTableView}
, message_line_edit_ {new QLineEdit}
, decodes_stack_ {new QStackedLayout}
, auto_off_button_ {new QPushButton {tr ("&Auto Off")}}
, halt_tx_button_ {new QPushButton {tr ("&Halt Tx")}}
, mode_label_ {new QLabel}
, frequency_label_ {new QLabel}
, rx_df_label_ {new QLabel}
, tx_df_label_ {new QLabel}
, report_label_ {new QLabel}
{
// set up widgets
decodes_proxy_model_.setSourceModel (decodes_model);
decodes_table_view_->setModel (&decodes_proxy_model_);
decodes_table_view_->verticalHeader ()->hide ();
decodes_table_view_->hideColumn (0);
decodes_table_view_->horizontalHeader ()->setStretchLastSection (true);
auto form_layout = new QFormLayout;
form_layout->addRow (tr ("Free text:"), message_line_edit_);
message_line_edit_->setValidator (new QRegExpValidator {message_alphabet, this});
connect (message_line_edit_, &QLineEdit::textEdited, [this] (QString const& text) {
Q_EMIT do_free_text (id_, text, false);
});
connect (message_line_edit_, &QLineEdit::editingFinished, [this] () {
Q_EMIT do_free_text (id_, message_line_edit_->text (), true);
});
auto decodes_page = new QWidget;
auto decodes_layout = new QVBoxLayout {decodes_page};
decodes_layout->setContentsMargins (QMargins {2, 2, 2, 2});
decodes_layout->addWidget (decodes_table_view_);
decodes_layout->addLayout (form_layout);
auto beacons_proxy_model = new IdFilterModel {id_};
beacons_proxy_model->setSourceModel (beacons_model);
beacons_table_view_->setModel (beacons_proxy_model);
beacons_table_view_->verticalHeader ()->hide ();
beacons_table_view_->hideColumn (0);
beacons_table_view_->horizontalHeader ()->setStretchLastSection (true);
auto beacons_page = new QWidget;
auto beacons_layout = new QVBoxLayout {beacons_page};
beacons_layout->setContentsMargins (QMargins {2, 2, 2, 2});
beacons_layout->addWidget (beacons_table_view_);
decodes_stack_->addWidget (decodes_page);
decodes_stack_->addWidget (beacons_page);
// stack alternative views
auto content_layout = new QVBoxLayout;
content_layout->setContentsMargins (QMargins {2, 2, 2, 2});
content_layout->addLayout (decodes_stack_);
// set up controls
auto control_button_box = new QDialogButtonBox;
control_button_box->addButton (auto_off_button_, QDialogButtonBox::ActionRole);
control_button_box->addButton (halt_tx_button_, QDialogButtonBox::ActionRole);
connect (auto_off_button_, &QAbstractButton::clicked, [this] (bool /* checked */) {
Q_EMIT do_halt_tx (id_, true);
});
connect (halt_tx_button_, &QAbstractButton::clicked, [this] (bool /* checked */) {
Q_EMIT do_halt_tx (id_, false);
});
content_layout->addWidget (control_button_box);
// set up status area
auto status_bar = new QStatusBar;
status_bar->addPermanentWidget (mode_label_);
status_bar->addPermanentWidget (frequency_label_);
status_bar->addPermanentWidget (rx_df_label_);
status_bar->addPermanentWidget (tx_df_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] (QModelIndex const& index) {
Q_EMIT do_reply (decodes_proxy_model_.mapToSource (index));
});
}
void ClientWidget::update_status (QString const& id, Frequency f, QString const& mode, QString const& /*dx_call*/
, QString const& report, QString const& tx_mode, bool tx_enabled
, bool transmitting, bool decoding, qint32 rx_df, qint32 tx_df
, QString const& de_call, QString const& /*de_grid*/, QString const& /*dx_grid*/)
{
if (id == id_)
{
decodes_proxy_model_.de_call (de_call);
decodes_proxy_model_.rx_df (rx_df);
mode_label_->setText (QString {"Mode: %1%2"}
.arg (mode)
.arg (tx_mode.isEmpty () || tx_mode == mode ? "" : '(' + tx_mode + ')'));
frequency_label_->setText ("QRG: " + Radio::pretty_frequency_MHz_string (f));
rx_df_label_->setText (rx_df >= 0 ? QString {"Rx: %1"}.arg (rx_df) : "");
tx_df_label_->setText (tx_df >= 0 ? QString {"Tx: %1"}.arg (tx_df) : "");
report_label_->setText ("SNR: " + report);
update_dynamic_property (frequency_label_, "transmitting", transmitting);
auto_off_button_->setEnabled (tx_enabled);
halt_tx_button_->setEnabled (transmitting);
update_dynamic_property (mode_label_, "decoding", decoding);
}
}
void ClientWidget::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_stack_->setCurrentIndex (0);
decodes_table_view_->resizeColumnsToContents ();
decodes_table_view_->scrollToBottom ();
}
}
void ClientWidget::beacon_spot_added (bool /*is_new*/, QString const& client_id, QTime /*time*/, qint32 /*snr*/
, float /*delta_time*/, Frequency /*delta_frequency*/, qint32 /*drift*/
, QString const& /*callsign*/, QString const& /*grid*/, qint32 /*power*/)
{
if (client_id == id_)
{
decodes_stack_->setCurrentIndex (1);
beacons_table_view_->resizeColumnsToContents ();
beacons_table_view_->scrollToBottom ();
}
}
#include "moc_ClientWidget.cpp"

View File

@ -0,0 +1,76 @@
#ifndef WSJTX_UDP_CLIENT_WIDGET_MODEL_HPP__
#define WSJTX_UDP_CLIENT_WIDGET_MODEL_HPP__
#include <QObject>
#include <QSortFilterProxyModel>
#include <QString>
#include <QRegularExpression>
#include <QtWidgets>
#include "MessageServer.hpp"
class QAbstractItemModel;
class QModelIndex;
using Frequency = MessageServer::Frequency;
class ClientWidget
: public QDockWidget
{
Q_OBJECT;
public:
explicit ClientWidget (QAbstractItemModel * decodes_model, QAbstractItemModel * beacons_model
, QString const& id, QWidget * parent = nullptr);
Q_SLOT void update_status (QString const& id, Frequency f, QString const& mode, QString const& dx_call
, QString const& report, QString const& tx_mode, bool tx_enabled
, bool transmitting, bool decoding, qint32 rx_df, qint32 tx_df
, QString const& de_call, QString const& de_grid, QString const& dx_grid);
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*/);
Q_SLOT void beacon_spot_added (bool /*is_new*/, QString const& client_id, QTime /*time*/, qint32 /*snr*/
, float /*delta_time*/, Frequency /*delta_frequency*/, qint32 /*drift*/
, QString const& /*callsign*/, QString const& /*grid*/, qint32 /*power*/);
Q_SIGNAL void do_reply (QModelIndex const&);
Q_SIGNAL void do_halt_tx (QString const& id, bool auto_only);
Q_SIGNAL void do_free_text (QString const& id, QString const& text, bool);
private:
QString id_;
class IdFilterModel final
: public QSortFilterProxyModel
{
public:
IdFilterModel (QString const& client_id);
void de_call (QString const&);
void rx_df (int);
QVariant data (QModelIndex const& proxy_index, int role = Qt::DisplayRole) const override;
protected:
bool filterAcceptsRow (int source_row, QModelIndex const& source_parent) const override;
private:
QString client_id_;
QString call_;
QRegularExpression base_call_re_;
int rx_df_;
} decodes_proxy_model_;
QTableView * decodes_table_view_;
QTableView * beacons_table_view_;
QLineEdit * message_line_edit_;
QStackedLayout * decodes_stack_;
QAbstractButton * auto_off_button_;
QAbstractButton * halt_tx_button_;
QLabel * mode_label_;
QLabel * frequency_label_;
QLabel * rx_df_label_;
QLabel * tx_df_label_;
QLabel * report_label_;
};
#endif

View File

@ -0,0 +1,127 @@
#include "DecodesModel.hpp"
#include <QStandardItem>
#include <QModelIndex>
#include <QTime>
#include <QString>
#include <QFont>
#include <QList>
namespace
{
char const * const headings[] = {
QT_TRANSLATE_NOOP ("DecodesModel", "Client"),
QT_TRANSLATE_NOOP ("DecodesModel", "Time"),
QT_TRANSLATE_NOOP ("DecodesModel", "Snr"),
QT_TRANSLATE_NOOP ("DecodesModel", "DT"),
QT_TRANSLATE_NOOP ("DecodesModel", "DF"),
QT_TRANSLATE_NOOP ("DecodesModel", "Md"),
QT_TRANSLATE_NOOP ("DecodesModel", "Message"),
};
QFont text_font {"Courier", 10};
QList<QStandardItem *> make_row (QString const& client_id, QTime time, qint32 snr, float delta_time
, quint32 delta_frequency, QString const& mode, QString const& message)
{
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;
}
}
DecodesModel::DecodesModel (QObject * parent)
: QStandardItemModel {0, 7, parent}
{
int column {0};
for (auto const& heading : headings)
{
setHeaderData (column++, Qt::Horizontal, tr (heading));
}
}
void DecodesModel::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));
}
void DecodesModel::clear_decodes (QString const& client_id)
{
for (auto row = rowCount () - 1; row >= 0; --row)
{
if (data (index (row, 0)).toString () == client_id)
{
removeRow (row);
}
}
}
void DecodesModel::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 ());
}
#include "moc_DecodesModel.cpp"

View File

@ -0,0 +1,43 @@
#ifndef WSJTX_UDP_DECODES_MODEL_HPP__
#define WSJTX_UDP_DECODES_MODEL_HPP__
#include <QStandardItemModel>
#include "MessageServer.hpp"
using Frequency = MessageServer::Frequency;
class QTime;
class QString;
class QModelIndex;
//
// 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:
explicit DecodesModel (QObject * parent = nullptr);
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);
Q_SLOT void clear_decodes (QString const& client_id);
Q_SLOT void do_reply (QModelIndex const& source);
Q_SIGNAL void reply (QString const& id, QTime time, qint32 snr, float delta_time, quint32 delta_frequency
, QString const& mode, QString const& message);
};
#endif

View File

@ -0,0 +1,90 @@
//
// 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 <clocale>
#include <iostream>
#include <exception>
#include <QFile>
#include <QApplication>
#include <QMessageBox>
#include <QObject>
#include "MessageAggregatorMainWindow.hpp"
// deduce the size of an array
template<class T, size_t N>
inline
size_t size (T (&)[N]) {return N;}
int main (int argc, char * argv[])
{
QApplication app {argc, argv};
try
{
setlocale (LC_NUMERIC, "C"); // ensure number forms are in
// consistent format, do this after
// instantiating QApplication so
// that GUI has correct l18n
app.setApplicationName ("WSJT-X Reference UDP Message Aggregator Server");
app.setApplicationVersion ("1.0");
QObject::connect (&app, SIGNAL (lastWindowClosed ()), &app, SLOT (quit ()));
{
QFile file {":/qss/default.qss"};
if (!file.open (QFile::ReadOnly))
{
throw std::runtime_error {
QString {"failed to open \"" + file.fileName () + "\": " + file.errorString ()}
.toLocal8Bit ().constData ()};
}
app.setStyleSheet (file.readAll());
}
MessageAggregatorMainWindow 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;
}

View File

@ -0,0 +1,158 @@
#include "MessageAggregatorMainWindow.hpp"
#include <QtWidgets>
#include <QDateTime>
#include "DecodesModel.hpp"
#include "BeaconsModel.hpp"
#include "ClientWidget.hpp"
using port_type = MessageServer::port_type;
namespace
{
char const * const headings[] = {
QT_TRANSLATE_NOOP ("MessageAggregatorMainWindow", "Date/Time"),
QT_TRANSLATE_NOOP ("MessageAggregatorMainWindow", "Callsign"),
QT_TRANSLATE_NOOP ("MessageAggregatorMainWindow", "Grid"),
QT_TRANSLATE_NOOP ("MessageAggregatorMainWindow", "Name"),
QT_TRANSLATE_NOOP ("MessageAggregatorMainWindow", "Frequency"),
QT_TRANSLATE_NOOP ("MessageAggregatorMainWindow", "Mode"),
QT_TRANSLATE_NOOP ("MessageAggregatorMainWindow", "Sent"),
QT_TRANSLATE_NOOP ("MessageAggregatorMainWindow", "Rec'd"),
QT_TRANSLATE_NOOP ("MessageAggregatorMainWindow", "Power"),
QT_TRANSLATE_NOOP ("MessageAggregatorMainWindow", "Comments"),
};
}
MessageAggregatorMainWindow::MessageAggregatorMainWindow ()
: log_ {new QStandardItemModel {0, 10, this}}
, decodes_model_ {new DecodesModel {this}}
, beacons_model_ {new BeaconsModel {this}}
, server_ {new MessageServer {this}}
, multicast_group_line_edit_ {new QLineEdit}
, log_table_view_ {new QTableView}
{
// logbook
int column {0};
for (auto const& heading : headings)
{
log_->setHeaderData (column++, Qt::Horizontal, tr (heading));
}
connect (server_, &MessageServer::qso_logged, this, &MessageAggregatorMainWindow::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, &MessageAggregatorMainWindow::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, &MessageAggregatorMainWindow::add_client);
connect (server_, &MessageServer::client_closed, this, &MessageAggregatorMainWindow::remove_client);
connect (server_, &MessageServer::client_closed, decodes_model_, &DecodesModel::clear_decodes);
connect (server_, &MessageServer::client_closed, beacons_model_, &BeaconsModel::clear_decodes);
connect (server_, &MessageServer::decode, decodes_model_, &DecodesModel::add_decode);
connect (server_, &MessageServer::WSPR_decode, beacons_model_, &BeaconsModel::add_beacon_spot);
connect (server_, &MessageServer::clear_decodes, decodes_model_, &DecodesModel::clear_decodes);
connect (server_, &MessageServer::clear_decodes, beacons_model_, &BeaconsModel::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 ();
}
void MessageAggregatorMainWindow::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 ();
}
void MessageAggregatorMainWindow::add_client (QString const& id)
{
auto dock = new ClientWidget {decodes_model_, beacons_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 (server_, &MessageServer::WSPR_decode, dock, &ClientWidget::beacon_spot_added);
connect (dock, &ClientWidget::do_reply, decodes_model_, &DecodesModel::do_reply);
connect (dock, &ClientWidget::do_halt_tx, server_, &MessageServer::halt_tx);
connect (dock, &ClientWidget::do_free_text, server_, &MessageServer::free_text);
connect (view_action, &QAction::toggled, dock, &ClientWidget::setVisible);
dock_widgets_[id] = dock;
server_->replay (id);
}
void MessageAggregatorMainWindow::remove_client (QString const& id)
{
auto iter = dock_widgets_.find (id);
if (iter != std::end (dock_widgets_))
{
(*iter)->close ();
dock_widgets_.erase (iter);
}
}
#include "moc_MessageAggregatorMainWindow.cpp"

View File

@ -0,0 +1,51 @@
#ifndef WSJTX_MESSAGE_AGGREGATOR_MAIN_WINDOW_MODEL_HPP__
#define WSJTX_MESSAGE_AGGREGATOR_MAIN_WINDOW_MODEL_HPP__
#include <QMainWindow>
#include <QHash>
#include <QString>
#include "MessageServer.hpp"
class QDateTime;
class QStandardItemModel;
class QMenu;
class DecodesModel;
class BeaconsModel;
class QLineEdit;
class QTableView;
class ClientWidget;
using Frequency = MessageServer::Frequency;
class MessageAggregatorMainWindow
: public QMainWindow
{
Q_OBJECT;
public:
MessageAggregatorMainWindow ();
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);
private:
void add_client (QString const& id);
void remove_client (QString const& id);
QStandardItemModel * log_;
QMenu * view_menu_;
DecodesModel * decodes_model_;
BeaconsModel * beacons_model_;
MessageServer * server_;
QLineEdit * multicast_group_line_edit_;
QTableView * log_table_view_;
// maps client id to widgets
using ClientsDictionary = QHash<QString, ClientWidget *>;
ClientsDictionary dock_widgets_;
};
#endif

View File

@ -47,7 +47,8 @@ public:
Q_SLOT void update_status (QString const& id, Frequency f, QString const& /*mode*/, QString const& /*dx_call*/
, QString const& /*report*/, QString const& /*tx_mode*/, bool /*tx_enabled*/
, bool /*transmitting*/, bool /*decoding*/)
, bool /*transmitting*/, bool /*decoding*/, qint32 /*rx_df*/, qint32 /*tx_df*/
, QString const& /*de_call*/, QString const& /*de_grid*/, QString const& /*dx_grid*/)
{
if (id == id_)
{

View File

@ -0,0 +1 @@
IDI_ICON1 ICON DISCARDABLE "../icons/windows-icons/wsjtx.ico"

View File

@ -0,0 +1 @@
IDI_ICON1 ICON DISCARDABLE "../icons/windows-icons/wsjtx.ico"

View File

@ -33,6 +33,8 @@
#include "mainwindow.h"
#include "commons.h"
#include "lib/init_random_seed.h"
#include "Radio.hpp"
#include "FrequencyList.hpp"
namespace
{
@ -80,9 +82,11 @@ int main(int argc, char *argv[])
init_random_seed ();
register_types (); // make the Qt magic happen
// make the Qt type magic happen
Radio::register_types ();
register_types ();
// Multiple instances:
// Multiple instances communicate with jt9 via this
QSharedMemory mem_jt9;
QApplication a(argc, argv);

View File

@ -1044,10 +1044,8 @@ void MainWindow::dataSink(qint64 frames)
cmnd=t3.mid(0,i1+7) + t3.mid(i1+7);
if (ui) ui->DecodeButton->setChecked (true);
p1.start(QDir::toNativeSeparators(cmnd));
if (ui) m_messageClient->status_update (m_freqNominal, m_mode, m_hisCall,
QString::number (ui->rptSpinBox->value ()),
m_modeTx, ui->autoButton->isChecked (),
m_transmitting, (m_decoderBusy = true));
m_decoderBusy = true;
statusUpdate ();
}
m_rxDone=true;
}
@ -1260,10 +1258,7 @@ void MainWindow::on_actionAbout_triggered() //Display "About"
void MainWindow::on_autoButton_clicked (bool checked)
{
m_auto = checked;
m_messageClient->status_update (m_freqNominal, m_mode, m_hisCall,
QString::number (ui->rptSpinBox->value ()),
m_modeTx, ui->autoButton->isChecked (),
m_transmitting, m_decoderBusy);
statusUpdate ();
m_bEchoTxOK=false;
if(m_auto and (m_mode=="Echo")) {
m_nclearave=1;
@ -1427,11 +1422,7 @@ void MainWindow::displayDialFrequency ()
void MainWindow::statusChanged()
{
m_messageClient->status_update (m_freqNominal, m_mode, m_hisCall,
QString::number (ui->rptSpinBox->value ()),
m_modeTx, ui->autoButton->isChecked (),
m_transmitting, m_decoderBusy);
statusUpdate ();
QFile f {m_config.temp_dir ().absoluteFilePath ("wsjtx_status.txt")};
if(f.open(QFile::WriteOnly | QIODevice::Text)) {
QTextStream out(&f);
@ -2268,10 +2259,7 @@ void MainWindow::decodeBusy(bool b) //decodeBusy()
ui->actionOpen_next_in_directory->setEnabled(!b);
ui->actionDecode_remaining_files_in_directory->setEnabled(!b);
m_messageClient->status_update (m_freqNominal, m_mode, m_hisCall,
QString::number (ui->rptSpinBox->value ()),
m_modeTx, ui->autoButton->isChecked (),
m_transmitting, m_decoderBusy);
statusUpdate ();
}
//------------------------------------------------------------- //guiUpdate()
@ -2601,10 +2589,7 @@ void MainWindow::guiUpdate()
m_transmitting = true;
transmitDisplay (true);
m_messageClient->status_update (m_freqNominal, m_mode, m_hisCall,
QString::number (ui->rptSpinBox->value ()),
m_modeTx, ui->autoButton->isChecked (),
m_transmitting, m_decoderBusy);
statusUpdate ();
}
if(!m_btxok && m_btxok0 && g_iptt==1) stopTx();
@ -2727,10 +2712,7 @@ void MainWindow::stopTx()
tx_status_label->setText("");
ptt0Timer->start(200); //Sequencer delay
monitor (true);
m_messageClient->status_update (m_freqNominal, m_mode, m_hisCall,
QString::number (ui->rptSpinBox->value ()),
m_modeTx, ui->autoButton->isChecked (),
m_transmitting, m_decoderBusy);
statusUpdate ();
}
void MainWindow::stopTx2()
@ -3480,6 +3462,7 @@ void MainWindow::on_dxCallEntry_textChanged(const QString &t) //dxCall changed
m_hisCall=t.toUpper().trimmed();
ui->dxCallEntry->setText(m_hisCall);
statusChanged();
statusUpdate ();
}
void MainWindow::on_dxGridEntry_textChanged(const QString &t) //dxGrid changed
@ -3519,6 +3502,7 @@ void MainWindow::on_dxGridEntry_textChanged(const QString &t) //dxGrid changed
ui->labAz->setText("");
ui->labDist->setText("");
}
statusUpdate ();
}
void MainWindow::on_genStdMsgsPushButton_clicked() //genStdMsgs button
@ -3977,6 +3961,7 @@ void MainWindow::on_TxFreqSpinBox_valueChanged(int n)
m_wideGraph->setTxFreq(n);
if(m_lockTxFreq) ui->RxFreqSpinBox->setValue(n);
Q_EMIT transmitFrequency (n - m_XIT);
statusUpdate ();
}
void MainWindow::on_RxFreqSpinBox_valueChanged(int n)
@ -3986,6 +3971,10 @@ void MainWindow::on_RxFreqSpinBox_valueChanged(int n)
{
ui->TxFreqSpinBox->setValue (n);
}
else
{
statusUpdate ();
}
}
void MainWindow::on_actionQuickDecode_triggered()
@ -5082,10 +5071,8 @@ void MainWindow::p1ReadFromStdout() //p1readFromStdout
m_RxLog=0;
m_startAnother=m_loopall;
m_blankLine=true;
m_messageClient->status_update (m_freqNominal, m_mode, m_hisCall,
QString::number (ui->rptSpinBox->value ()),
m_modeTx, ui->autoButton->isChecked (),
m_transmitting, (m_decoderBusy = false));
m_decoderBusy = false;
statusUpdate ();
} else {
int n=t.length();
@ -5412,3 +5399,16 @@ void MainWindow::CQRxFreq()
}
}
void MainWindow::statusUpdate () const
{
if (ui)
{
m_messageClient->status_update (m_freqNominal, m_mode, m_hisCall,
QString::number (ui->rptSpinBox->value ()),
m_modeTx, ui->autoButton->isChecked (),
m_transmitting, m_decoderBusy,
ui->RxFreqSpinBox->value (), ui->TxFreqSpinBox->value (),
m_config.my_callsign (), m_config.my_grid (),
m_hisGrid);
}
}

View File

@ -572,6 +572,7 @@ private:
void decodeDone ();
void subProcessFailed (QProcess *, int exit_code, QProcess::ExitStatus);
void subProcessError (QProcess *, QProcess::ProcessError);
void statusUpdate () const;
};
extern int killbyname(const char* progName);