mirror of
				https://github.com/saitohirga/WSJT-X.git
				synced 2025-10-31 13:10:19 -04: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
							
								
									3ff3a192ee
								
							
						
					
					
						commit
						ae3749bb46
					
				
							
								
								
									
										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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user