From ae3749bb46cbc6751ce4b0ccc31121abce0c95d6 Mon Sep 17 00:00:00 2001 From: Bill Somerville Date: Tue, 24 May 2016 10:08:35 +0000 Subject: [PATCH] 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 --- CMakeLists.txt | 124 +++- FrequencyList.cpp | 1 + MessageAggregator.cpp | 687 ------------------ MessageClient.cpp | 7 +- MessageClient.hpp | 4 +- MessageServer.cpp | 16 +- MessageServer.hpp | 8 +- MetaDataRegistry.cpp | 18 +- NetworkMessage.hpp | 11 +- Radio.cpp | 2 - Radio.hpp | 32 +- RadioMetaType.cpp | 20 + UDPExamples/BeaconsModel.cpp | 125 ++++ UDPExamples/BeaconsModel.hpp | 38 + UDPExamples/ClientWidget.cpp | 241 ++++++ UDPExamples/ClientWidget.hpp | 76 ++ UDPExamples/DecodesModel.cpp | 127 ++++ UDPExamples/DecodesModel.hpp | 43 ++ UDPExamples/MessageAggregator.cpp | 90 +++ UDPExamples/MessageAggregatorMainWindow.cpp | 158 ++++ UDPExamples/MessageAggregatorMainWindow.hpp | 51 ++ UDPDaemon.cpp => UDPExamples/UDPDaemon.cpp | 3 +- .../message_aggregator.qrc.in | 0 UDPExamples/message_aggregator.rc | 1 + {qss => UDPExamples/qss}/default.qss | 0 UDPExamples/udp_daemon.rc | 1 + main.cpp | 8 +- mainwindow.cpp | 58 +- mainwindow.h | 1 + 29 files changed, 1164 insertions(+), 787 deletions(-) delete mode 100644 MessageAggregator.cpp create mode 100644 RadioMetaType.cpp create mode 100644 UDPExamples/BeaconsModel.cpp create mode 100644 UDPExamples/BeaconsModel.hpp create mode 100644 UDPExamples/ClientWidget.cpp create mode 100644 UDPExamples/ClientWidget.hpp create mode 100644 UDPExamples/DecodesModel.cpp create mode 100644 UDPExamples/DecodesModel.hpp create mode 100644 UDPExamples/MessageAggregator.cpp create mode 100644 UDPExamples/MessageAggregatorMainWindow.cpp create mode 100644 UDPExamples/MessageAggregatorMainWindow.hpp rename UDPDaemon.cpp => UDPExamples/UDPDaemon.cpp (97%) rename message_aggregator.qrc.in => UDPExamples/message_aggregator.qrc.in (100%) create mode 100644 UDPExamples/message_aggregator.rc rename {qss => UDPExamples/qss}/default.qss (100%) create mode 100644 UDPExamples/udp_daemon.rc diff --git a/CMakeLists.txt b/CMakeLists.txt index b9816f0a9..d5a84e66b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,6 +40,9 @@ if (POLICY CMP0043) cmake_policy (SET CMP0043 NEW) # ignore COMPILE_DEFINITIONS_ 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 ") -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 ${source_}") 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 + $ + $ + $ + ) +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 ( diff --git a/FrequencyList.cpp b/FrequencyList.cpp index 3eb51993b..10ef51d7a 100644 --- a/FrequencyList.cpp +++ b/FrequencyList.cpp @@ -2,6 +2,7 @@ #include +#include #include #include #include diff --git a/MessageAggregator.cpp b/MessageAggregator.cpp deleted file mode 100644 index e949fb42d..000000000 --- a/MessageAggregator.cpp +++ /dev/null @@ -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 -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#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 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 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 - && 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 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 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::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 (&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 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 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; -} diff --git a/MessageClient.cpp b/MessageClient.cpp index 009097d4f..54b64a9c8 100644 --- a/MessageClient.cpp +++ b/MessageClient.cpp @@ -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); } } diff --git a/MessageClient.hpp b/MessageClient.hpp index 75efa9cbf..63a26b063 100644 --- a/MessageClient.hpp +++ b/MessageClient.hpp @@ -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 diff --git a/MessageServer.cpp b/MessageServer.cpp index d3c9e51d8..55944bb66 100644 --- a/MessageServer.cpp +++ b/MessageServer.cpp @@ -7,6 +7,7 @@ #include #include +#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 (&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; diff --git a/MessageServer.hpp b/MessageServer.hpp index 846df5005..29053d807 100644 --- a/MessageServer.hpp +++ b/MessageServer.hpp @@ -6,6 +6,7 @@ #include #include +#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 m_; }; diff --git a/MetaDataRegistry.cpp b/MetaDataRegistry.cpp index 07ce46d32..fdf64c8a4 100644 --- a/MetaDataRegistry.cpp +++ b/MetaDataRegistry.cpp @@ -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 ("Frequency"); - qRegisterMetaType ("Frequencies"); - - // This is required to preserve v1.5 "frequencies" setting for - // backwards compatibility, without it the setting gets trashed by - // later versions. - qRegisterMetaTypeStreamOperators ("Frequencies"); - - item_editor_factory ()->registerEditor (frequency_type_id, new QStandardItemEditorCreator ()); - auto frequency_delta_type_id = qRegisterMetaType ("FrequencyDelta"); - item_editor_factory ()->registerEditor (frequency_delta_type_id, new QStandardItemEditorCreator ()); + item_editor_factory ()->registerEditor (qMetaTypeId (), new QStandardItemEditorCreator ()); + //auto frequency_delta_type_id = qRegisterMetaType ("FrequencyDelta"); + item_editor_factory ()->registerEditor (qMetaTypeId (), new QStandardItemEditorCreator ()); // Frequency list model qRegisterMetaType ("Item"); diff --git a/NetworkMessage.hpp b/NetworkMessage.hpp index 14fdb1e30..a3394947a 100644 --- a/NetworkMessage.hpp +++ b/NetworkMessage.hpp @@ -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 diff --git a/Radio.cpp b/Radio.cpp index 929a88e93..9e211a689 100644 --- a/Radio.cpp +++ b/Radio.cpp @@ -4,8 +4,6 @@ #include #include -#include -#include #include namespace Radio diff --git a/Radio.hpp b/Radio.hpp index 309c27022..ca7cbf641 100644 --- a/Radio.hpp +++ b/Radio.hpp @@ -1,8 +1,11 @@ -#ifndef RADIO_HPP_ -#define RADIO_HPP_ +#ifndef RADIO_HPP__ +#define RADIO_HPP__ #include #include +#include + +#include "udp_export.h" class QVariant; class QString; @@ -20,30 +23,35 @@ namespace Radio using Frequencies = QList; 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); diff --git a/RadioMetaType.cpp b/RadioMetaType.cpp new file mode 100644 index 000000000..ca54e9e58 --- /dev/null +++ b/RadioMetaType.cpp @@ -0,0 +1,20 @@ +#include "Radio.hpp" + +#include +#include +#include + +namespace Radio +{ + void register_types () + { + qRegisterMetaType ("Frequency"); + qRegisterMetaType ("FrequencyDelta"); + qRegisterMetaType ("Frequencies"); + + // This is required to preserve v1.5 "frequencies" setting for + // backwards compatibility, without it the setting gets trashed + // by later versions. + qRegisterMetaTypeStreamOperators ("Frequencies"); + } +} diff --git a/UDPExamples/BeaconsModel.cpp b/UDPExamples/BeaconsModel.cpp new file mode 100644 index 000000000..ca72306eb --- /dev/null +++ b/UDPExamples/BeaconsModel.cpp @@ -0,0 +1,125 @@ +#include "BeaconsModel.hpp" + +#include +#include + +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 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 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 + && 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" diff --git a/UDPExamples/BeaconsModel.hpp b/UDPExamples/BeaconsModel.hpp new file mode 100644 index 000000000..df707d01b --- /dev/null +++ b/UDPExamples/BeaconsModel.hpp @@ -0,0 +1,38 @@ +#ifndef WSJTX_UDP_BEACONS_MODEL_HPP__ +#define WSJTX_UDP_BEACONS_MODEL_HPP__ + +#include + +#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 diff --git a/UDPExamples/ClientWidget.cpp b/UDPExamples/ClientWidget.cpp new file mode 100644 index 000000000..eea15576c --- /dev/null +++ b/UDPExamples/ClientWidget.cpp @@ -0,0 +1,241 @@ +#include "ClientWidget.hpp" + +#include +#include + +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" diff --git a/UDPExamples/ClientWidget.hpp b/UDPExamples/ClientWidget.hpp new file mode 100644 index 000000000..1e4db73f8 --- /dev/null +++ b/UDPExamples/ClientWidget.hpp @@ -0,0 +1,76 @@ +#ifndef WSJTX_UDP_CLIENT_WIDGET_MODEL_HPP__ +#define WSJTX_UDP_CLIENT_WIDGET_MODEL_HPP__ + +#include +#include +#include +#include +#include + +#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 diff --git a/UDPExamples/DecodesModel.cpp b/UDPExamples/DecodesModel.cpp new file mode 100644 index 000000000..59684ba43 --- /dev/null +++ b/UDPExamples/DecodesModel.cpp @@ -0,0 +1,127 @@ +#include "DecodesModel.hpp" + +#include +#include +#include +#include +#include +#include + +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 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 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" diff --git a/UDPExamples/DecodesModel.hpp b/UDPExamples/DecodesModel.hpp new file mode 100644 index 000000000..4425a7e9f --- /dev/null +++ b/UDPExamples/DecodesModel.hpp @@ -0,0 +1,43 @@ +#ifndef WSJTX_UDP_DECODES_MODEL_HPP__ +#define WSJTX_UDP_DECODES_MODEL_HPP__ + +#include + +#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 diff --git a/UDPExamples/MessageAggregator.cpp b/UDPExamples/MessageAggregator.cpp new file mode 100644 index 000000000..81515ac04 --- /dev/null +++ b/UDPExamples/MessageAggregator.cpp @@ -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 +#include +#include + +#include +#include +#include +#include + +#include "MessageAggregatorMainWindow.hpp" + +// deduce the size of an array +template +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; +} diff --git a/UDPExamples/MessageAggregatorMainWindow.cpp b/UDPExamples/MessageAggregatorMainWindow.cpp new file mode 100644 index 000000000..51598eb45 --- /dev/null +++ b/UDPExamples/MessageAggregatorMainWindow.cpp @@ -0,0 +1,158 @@ +#include "MessageAggregatorMainWindow.hpp" + +#include +#include + +#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::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 (&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 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" diff --git a/UDPExamples/MessageAggregatorMainWindow.hpp b/UDPExamples/MessageAggregatorMainWindow.hpp new file mode 100644 index 000000000..b15feb9a0 --- /dev/null +++ b/UDPExamples/MessageAggregatorMainWindow.hpp @@ -0,0 +1,51 @@ +#ifndef WSJTX_MESSAGE_AGGREGATOR_MAIN_WINDOW_MODEL_HPP__ +#define WSJTX_MESSAGE_AGGREGATOR_MAIN_WINDOW_MODEL_HPP__ + +#include +#include +#include + +#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; + ClientsDictionary dock_widgets_; +}; + +#endif diff --git a/UDPDaemon.cpp b/UDPExamples/UDPDaemon.cpp similarity index 97% rename from UDPDaemon.cpp rename to UDPExamples/UDPDaemon.cpp index 6f8f0240e..70e83dae8 100644 --- a/UDPDaemon.cpp +++ b/UDPExamples/UDPDaemon.cpp @@ -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_) { diff --git a/message_aggregator.qrc.in b/UDPExamples/message_aggregator.qrc.in similarity index 100% rename from message_aggregator.qrc.in rename to UDPExamples/message_aggregator.qrc.in diff --git a/UDPExamples/message_aggregator.rc b/UDPExamples/message_aggregator.rc new file mode 100644 index 000000000..faf73f803 --- /dev/null +++ b/UDPExamples/message_aggregator.rc @@ -0,0 +1 @@ +IDI_ICON1 ICON DISCARDABLE "../icons/windows-icons/wsjtx.ico" diff --git a/qss/default.qss b/UDPExamples/qss/default.qss similarity index 100% rename from qss/default.qss rename to UDPExamples/qss/default.qss diff --git a/UDPExamples/udp_daemon.rc b/UDPExamples/udp_daemon.rc new file mode 100644 index 000000000..faf73f803 --- /dev/null +++ b/UDPExamples/udp_daemon.rc @@ -0,0 +1 @@ +IDI_ICON1 ICON DISCARDABLE "../icons/windows-icons/wsjtx.ico" diff --git a/main.cpp b/main.cpp index 9eb610389..a8bf174b1 100644 --- a/main.cpp +++ b/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); diff --git a/mainwindow.cpp b/mainwindow.cpp index f148fa5d7..833d73914 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -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); + } +} diff --git a/mainwindow.h b/mainwindow.h index cc2df5c44..069ace871 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -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);