mirror of
https://github.com/saitohirga/WSJT-X.git
synced 2024-11-26 22:28:41 -05:00
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:
parent
d865186de3
commit
3ec6d211c8
124
CMakeLists.txt
124
CMakeLists.txt
@ -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 (
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include <QMetaType>
|
||||
#include <QAbstractTableModel>
|
||||
#include <QString>
|
||||
#include <QList>
|
||||
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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_;
|
||||
};
|
||||
|
||||
|
@ -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");
|
||||
|
@ -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
|
||||
|
@ -4,8 +4,6 @@
|
||||
|
||||
#include <QString>
|
||||
#include <QChar>
|
||||
#include <QDebug>
|
||||
#include <QDataStream>
|
||||
#include <QRegularExpression>
|
||||
|
||||
namespace Radio
|
||||
|
32
Radio.hpp
32
Radio.hpp
@ -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
20
RadioMetaType.cpp
Normal 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");
|
||||
}
|
||||
}
|
125
UDPExamples/BeaconsModel.cpp
Normal file
125
UDPExamples/BeaconsModel.cpp
Normal 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"
|
38
UDPExamples/BeaconsModel.hpp
Normal file
38
UDPExamples/BeaconsModel.hpp
Normal 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
|
241
UDPExamples/ClientWidget.cpp
Normal file
241
UDPExamples/ClientWidget.cpp
Normal 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"
|
76
UDPExamples/ClientWidget.hpp
Normal file
76
UDPExamples/ClientWidget.hpp
Normal 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
|
127
UDPExamples/DecodesModel.cpp
Normal file
127
UDPExamples/DecodesModel.cpp
Normal 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"
|
43
UDPExamples/DecodesModel.hpp
Normal file
43
UDPExamples/DecodesModel.hpp
Normal 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
|
90
UDPExamples/MessageAggregator.cpp
Normal file
90
UDPExamples/MessageAggregator.cpp
Normal 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;
|
||||
}
|
158
UDPExamples/MessageAggregatorMainWindow.cpp
Normal file
158
UDPExamples/MessageAggregatorMainWindow.cpp
Normal 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"
|
51
UDPExamples/MessageAggregatorMainWindow.hpp
Normal file
51
UDPExamples/MessageAggregatorMainWindow.hpp
Normal 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
|
@ -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_)
|
||||
{
|
1
UDPExamples/message_aggregator.rc
Normal file
1
UDPExamples/message_aggregator.rc
Normal file
@ -0,0 +1 @@
|
||||
IDI_ICON1 ICON DISCARDABLE "../icons/windows-icons/wsjtx.ico"
|
1
UDPExamples/udp_daemon.rc
Normal file
1
UDPExamples/udp_daemon.rc
Normal file
@ -0,0 +1 @@
|
||||
IDI_ICON1 ICON DISCARDABLE "../icons/windows-icons/wsjtx.ico"
|
8
main.cpp
8
main.cpp
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user