diff --git a/.gitignore b/.gitignore index 3ffc4aed5..80e3c4b27 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ ~* TAGS tags +GPATH +GRTAGS +GTAGS *~ junk* jnq* @@ -9,5 +12,13 @@ jnq* *.mod *.pro.user *.txt +*.bak +!**/CMakeLists.txt +__pycache__ cmake-build-debug cmake-build-release +CMakeFiles +fnd +lib/77bit/tmp +lib/tmp +lib/ftrsd diff --git a/Audio/Audio.pri b/Audio/Audio.pri new file mode 100644 index 000000000..e2237bbd1 --- /dev/null +++ b/Audio/Audio.pri @@ -0,0 +1,5 @@ +SOURCES += Audio/AudioDevice.cpp Audio/BWFFile.cpp Audio/soundin.cpp \ + Audio/soundout.cpp + +HEADERS += Audio/AudioDevice.hpp Audio/BWFFile.hpp Audio/soundin.h \ + Audio/soundout.h diff --git a/Audio/AudioDevice.cpp b/Audio/AudioDevice.cpp index 2b8525826..16fec3148 100644 --- a/Audio/AudioDevice.cpp +++ b/Audio/AudioDevice.cpp @@ -7,4 +7,3 @@ bool AudioDevice::initialize (OpenMode mode, Channel channel) // open and ensure we are unbuffered if possible return QIODevice::open (mode | QIODevice::Unbuffered); } - diff --git a/Audio/AudioDevice.hpp b/Audio/AudioDevice.hpp index c4354687c..2d47af89b 100644 --- a/Audio/AudioDevice.hpp +++ b/Audio/AudioDevice.hpp @@ -33,8 +33,8 @@ public: Channel channel () const {return m_channel;} protected: - AudioDevice (QObject * parent = 0) - : QIODevice (parent) + AudioDevice (QObject * parent = nullptr) + : QIODevice {parent} { } @@ -43,23 +43,23 @@ protected: qint16 const * begin (reinterpret_cast (source)); for ( qint16 const * i = begin; i != begin + numFrames * (bytesPerFrame () / sizeof (qint16)); i += bytesPerFrame () / sizeof (qint16)) { - switch (m_channel) - { - case Mono: - *dest++ = *i; - break; + switch (m_channel) + { + case Mono: + *dest++ = *i; + break; - case Right: - *dest++ = *(i + 1); - break; + case Right: + *dest++ = *(i + 1); + break; - case Both: // should be able to happen but if it - // does we'll take left - Q_ASSERT (Both == m_channel); - case Left: - *dest++ = *i; - break; - } + case Both: // should be able to happen but if it + // does we'll take left + Q_ASSERT (Both == m_channel); + case Left: + *dest++ = *i; + break; + } } } @@ -68,23 +68,23 @@ protected: switch (m_channel) { case Mono: - *dest++ = sample; - break; + *dest++ = sample; + break; case Left: - *dest++ = sample; - *dest++ = 0; - break; + *dest++ = sample; + *dest++ = 0; + break; case Right: - *dest++ = 0; - *dest++ = sample; - break; + *dest++ = 0; + *dest++ = sample; + break; case Both: - *dest++ = sample; - *dest++ = sample; - break; + *dest++ = sample; + *dest++ = sample; + break; } return dest; } diff --git a/Audio/soundin.cpp b/Audio/soundin.cpp index e08d45271..92368016b 100644 --- a/Audio/soundin.cpp +++ b/Audio/soundin.cpp @@ -1,5 +1,7 @@ #include "soundin.h" +#include +#include #include #include #include @@ -8,11 +10,9 @@ #include "moc_soundin.cpp" -bool SoundInput::audioError () const +bool SoundInput::checkStream () { - bool result (true); - - Q_ASSERT_X (m_stream, "SoundInput", "programming error"); + bool result (false); if (m_stream) { switch (m_stream->error ()) @@ -34,14 +34,15 @@ bool SoundInput::audioError () const break; case QAudio::NoError: - result = false; + result = true; break; } } return result; } -void SoundInput::start(QAudioDeviceInfo const& device, int framesPerBuffer, AudioDevice * sink, unsigned downSampleFactor, AudioDevice::Channel channel) +void SoundInput::start(QAudioDeviceInfo const& device, int framesPerBuffer, AudioDevice * sink + , unsigned downSampleFactor, AudioDevice::Channel channel) { Q_ASSERT (sink); @@ -62,28 +63,37 @@ void SoundInput::start(QAudioDeviceInfo const& device, int framesPerBuffer, Audi Q_EMIT error (tr ("Requested input audio format is not valid.")); return; } - - if (!device.isFormatSupported (format)) + else if (!device.isFormatSupported (format)) { // qDebug () << "Nearest supported audio format:" << device.nearestFormat (format); Q_EMIT error (tr ("Requested input audio format is not supported on device.")); return; } -// qDebug () << "Selected audio input format:" << format; + // qDebug () << "Selected audio input format:" << format; m_stream.reset (new QAudioInput {device, format}); - if (audioError ()) + if (!checkStream ()) { return; } connect (m_stream.data(), &QAudioInput::stateChanged, this, &SoundInput::handleStateChanged); + connect (m_stream.data(), &QAudioInput::notify, [this] () {checkStream ();}); - m_stream->setBufferSize (m_stream->format ().bytesForFrames (framesPerBuffer)); - if (sink->initialize (QIODevice::WriteOnly, channel)) + //qDebug () << "SoundIn default buffer size (bytes):" << m_stream->bufferSize () << "period size:" << m_stream->periodSize (); + // the Windows MME version of QAudioInput uses 1/5 of the buffer + // size for period size other platforms seem to optimize themselves +#if defined (Q_OS_WIN) + m_stream->setBufferSize (m_stream->format ().bytesForFrames (framesPerBuffer * 5)); +#else + Q_UNUSED (framesPerBuffer); +#endif + if (m_sink->initialize (QIODevice::WriteOnly, channel)) { m_stream->start (sink); - audioError (); + checkStream (); + cummulative_lost_usec_ = -1; + //qDebug () << "SoundIn selected buffer size (bytes):" << m_stream->bufferSize () << "peirod size:" << m_stream->periodSize (); } else { @@ -96,7 +106,7 @@ void SoundInput::suspend () if (m_stream) { m_stream->suspend (); - audioError (); + checkStream (); } } @@ -111,14 +121,12 @@ void SoundInput::resume () if (m_stream) { m_stream->resume (); - audioError (); + checkStream (); } } -void SoundInput::handleStateChanged (QAudio::State newState) const +void SoundInput::handleStateChanged (QAudio::State newState) { - // qDebug () << "SoundInput::handleStateChanged: newState:" << newState; - switch (newState) { case QAudio::IdleState: @@ -126,6 +134,7 @@ void SoundInput::handleStateChanged (QAudio::State newState) const break; case QAudio::ActiveState: + reset (false); Q_EMIT status (tr ("Receiving")); break; @@ -140,7 +149,7 @@ void SoundInput::handleStateChanged (QAudio::State newState) const #endif case QAudio::StoppedState: - if (audioError ()) + if (!checkStream ()) { Q_EMIT status (tr ("Error")); } @@ -152,6 +161,28 @@ void SoundInput::handleStateChanged (QAudio::State newState) const } } +void SoundInput::reset (bool report_dropped_frames) +{ + if (m_stream) + { + auto elapsed_usecs = m_stream->elapsedUSecs (); + while (std::abs (elapsed_usecs - m_stream->processedUSecs ()) + > 24 * 60 * 60 * 500000ll) // half day + { + // QAudioInput::elapsedUSecs() wraps after 24 hours + elapsed_usecs += 24 * 60 * 60 * 1000000ll; + } + // don't report first time as we don't yet known latency + if (cummulative_lost_usec_ != std::numeric_limits::min () && report_dropped_frames) + { + auto lost_usec = elapsed_usecs - m_stream->processedUSecs () - cummulative_lost_usec_; + Q_EMIT dropped_frames (m_stream->format ().framesForDuration (lost_usec), lost_usec); + //qDebug () << "SoundInput::reset: frames dropped:" << m_stream->format ().framesForDuration (lost_usec) << "sec:" << lost_usec / 1.e6; + } + cummulative_lost_usec_ = elapsed_usecs - m_stream->processedUSecs (); + } +} + void SoundInput::stop() { if (m_stream) @@ -159,11 +190,6 @@ void SoundInput::stop() m_stream->stop (); } m_stream.reset (); - - if (m_sink) - { - m_sink->close (); - } } SoundInput::~SoundInput () diff --git a/Audio/soundin.h b/Audio/soundin.h index 325d5e89d..c35b3d7d8 100644 --- a/Audio/soundin.h +++ b/Audio/soundin.h @@ -2,6 +2,7 @@ #ifndef SOUNDIN_H__ #define SOUNDIN_H__ +#include #include #include #include @@ -23,7 +24,7 @@ class SoundInput public: SoundInput (QObject * parent = nullptr) : QObject {parent} - , m_sink {nullptr} + , cummulative_lost_usec_ {std::numeric_limits::min ()} { } @@ -35,18 +36,21 @@ public: Q_SLOT void suspend (); Q_SLOT void resume (); Q_SLOT void stop (); + Q_SLOT void reset (bool report_dropped_frames); Q_SIGNAL void error (QString message) const; Q_SIGNAL void status (QString message) const; + Q_SIGNAL void dropped_frames (qint32 dropped, qint64 usec); private: // used internally - Q_SLOT void handleStateChanged (QAudio::State) const; + Q_SLOT void handleStateChanged (QAudio::State); - bool audioError () const; + bool checkStream (); QScopedPointer m_stream; QPointer m_sink; + qint64 cummulative_lost_usec_; }; #endif diff --git a/Audio/soundout.cpp b/Audio/soundout.cpp index 0264ad91e..fe7c58fbf 100644 --- a/Audio/soundout.cpp +++ b/Audio/soundout.cpp @@ -7,20 +7,13 @@ #include #include +#include "Audio/AudioDevice.hpp" + #include "moc_soundout.cpp" -/* -#if defined (WIN32) -# define MS_BUFFERED 1000u -#else -# define MS_BUFFERED 2000u -#endif -*/ -# define MS_BUFFERED 200u - -bool SoundOutput::audioError () const +bool SoundOutput::checkStream () const { - bool result (true); + bool result {false}; Q_ASSERT_X (m_stream, "SoundOutput", "programming error"); if (m_stream) { @@ -43,69 +36,83 @@ bool SoundOutput::audioError () const break; case QAudio::NoError: - result = false; + result = true; break; } } return result; } -void SoundOutput::setFormat (QAudioDeviceInfo const& device, unsigned channels, unsigned msBuffered) +void SoundOutput::setFormat (QAudioDeviceInfo const& device, unsigned channels, int frames_buffered) { Q_ASSERT (0 < channels && channels < 3); - - m_msBuffered = msBuffered; - - QAudioFormat format (device.preferredFormat ()); -// qDebug () << "Preferred audio output format:" << format; - format.setChannelCount (channels); - format.setCodec ("audio/pcm"); - format.setSampleRate (48000); - format.setSampleType (QAudioFormat::SignedInt); - format.setSampleSize (16); - format.setByteOrder (QAudioFormat::Endian (QSysInfo::ByteOrder)); - if (!format.isValid ()) - { - Q_EMIT error (tr ("Requested output audio format is not valid.")); - } - if (!device.isFormatSupported (format)) - { - Q_EMIT error (tr ("Requested output audio format is not supported on device.")); - } -// qDebug () << "Selected audio output format:" << format; - - m_stream.reset (new QAudioOutput (device, format)); - audioError (); - m_stream->setVolume (m_volume); - m_stream->setNotifyInterval(100); - - connect (m_stream.data(), &QAudioOutput::stateChanged, this, &SoundOutput::handleStateChanged); - - // qDebug() << "A" << m_volume << m_stream->notifyInterval(); + m_device = device; + m_channels = channels; + m_framesBuffered = frames_buffered; } void SoundOutput::restart (QIODevice * source) { - Q_ASSERT (m_stream); + if (!m_device.isNull ()) + { + QAudioFormat format (m_device.preferredFormat ()); + // qDebug () << "Preferred audio output format:" << format; + format.setChannelCount (m_channels); + format.setCodec ("audio/pcm"); + format.setSampleRate (48000); + format.setSampleType (QAudioFormat::SignedInt); + format.setSampleSize (16); + format.setByteOrder (QAudioFormat::Endian (QSysInfo::ByteOrder)); + if (!format.isValid ()) + { + Q_EMIT error (tr ("Requested output audio format is not valid.")); + } + else if (!m_device.isFormatSupported (format)) + { + Q_EMIT error (tr ("Requested output audio format is not supported on device.")); + } + else + { + // qDebug () << "Selected audio output format:" << format; + m_stream.reset (new QAudioOutput (m_device, format)); + checkStream (); + m_stream->setVolume (m_volume); + m_stream->setNotifyInterval(1000); + error_ = false; + + connect (m_stream.data(), &QAudioOutput::stateChanged, this, &SoundOutput::handleStateChanged); + connect (m_stream.data(), &QAudioOutput::notify, [this] () {checkStream ();}); + + // qDebug() << "A" << m_volume << m_stream->notifyInterval(); + } + } + if (!m_stream) + { + if (!error_) + { + error_ = true; // only signal error once + Q_EMIT error (tr ("No audio output device configured.")); + } + return; + } + else + { + error_ = false; + } - // - // This buffer size is critical since for proper sound streaming. If - // it is too short; high activity levels on the machine can starve - // the audio buffer. On the other hand the Windows implementation - // seems to take the length of the buffer in time to stop the audio - // stream even if reset() is used. - // - // 2 seconds seems a reasonable compromise except for Windows - // where things are probably broken. - // // we have to set this before every start on the stream because the // Windows implementation seems to forget the buffer size after a // stop. - m_stream->setBufferSize (m_stream->format().bytesForDuration((m_msBuffered ? m_msBuffered : MS_BUFFERED) * 1000)); - // qDebug() << "B" << m_stream->bufferSize() << - // m_stream->periodSize() << m_stream->notifyInterval(); + //qDebug () << "SoundOut default buffer size (bytes):" << m_stream->bufferSize () << "period size:" << m_stream->periodSize (); + if (m_framesBuffered) + { +#if defined (Q_OS_WIN) + m_stream->setBufferSize (m_stream->format().bytesForFrames (m_framesBuffered)); +#endif + } m_stream->setCategory ("production"); m_stream->start (source); + // qDebug () << "SoundOut selected buffer size (bytes):" << m_stream->bufferSize () << "period size:" << m_stream->periodSize (); } void SoundOutput::suspend () @@ -113,7 +120,7 @@ void SoundOutput::suspend () if (m_stream && QAudio::ActiveState == m_stream->state ()) { m_stream->suspend (); - audioError (); + checkStream (); } } @@ -122,7 +129,7 @@ void SoundOutput::resume () if (m_stream && QAudio::SuspendedState == m_stream->state ()) { m_stream->resume (); - audioError (); + checkStream (); } } @@ -131,7 +138,7 @@ void SoundOutput::reset () if (m_stream) { m_stream->reset (); - audioError (); + checkStream (); } } @@ -139,9 +146,10 @@ void SoundOutput::stop () { if (m_stream) { + m_stream->reset (); m_stream->stop (); - audioError (); } + m_stream.reset (); } qreal SoundOutput::attenuation () const @@ -171,8 +179,6 @@ void SoundOutput::resetAttenuation () void SoundOutput::handleStateChanged (QAudio::State newState) { - // qDebug () << "SoundOutput::handleStateChanged: newState:" << newState; - switch (newState) { case QAudio::IdleState: @@ -194,7 +200,7 @@ void SoundOutput::handleStateChanged (QAudio::State newState) #endif case QAudio::StoppedState: - if (audioError ()) + if (!checkStream ()) { Q_EMIT status (tr ("Error")); } diff --git a/Audio/soundout.h b/Audio/soundout.h index 1e4b3a948..76711660d 100644 --- a/Audio/soundout.h +++ b/Audio/soundout.h @@ -7,6 +7,7 @@ #include #include +class QIODevice; class QAudioDeviceInfo; // An instance of this sends audio data to a specified soundcard. @@ -18,15 +19,16 @@ class SoundOutput public: SoundOutput () - : m_msBuffered {0u} + : m_framesBuffered {0} , m_volume {1.0} + , error_ {false} { } qreal attenuation () const; public Q_SLOTS: - void setFormat (QAudioDeviceInfo const& device, unsigned channels, unsigned msBuffered = 0u); + void setFormat (QAudioDeviceInfo const& device, unsigned channels, int frames_buffered = 0); void restart (QIODevice *); void suspend (); void resume (); @@ -40,15 +42,18 @@ Q_SIGNALS: void status (QString message) const; private: - bool audioError () const; + bool checkStream () const; private Q_SLOTS: void handleStateChanged (QAudio::State); private: + QAudioDeviceInfo m_device; + unsigned m_channels; QScopedPointer m_stream; - unsigned m_msBuffered; + int m_framesBuffered; qreal m_volume; + bool error_; }; #endif diff --git a/CMake/Modules/FindFFTW3.cmake b/CMake/Modules/FindFFTW3.cmake index 10349cb69..999c2ffe3 100644 --- a/CMake/Modules/FindFFTW3.cmake +++ b/CMake/Modules/FindFFTW3.cmake @@ -34,6 +34,12 @@ # # $Id: FindFFTW3.cmake 15918 2010-06-25 11:12:42Z loose $ +# Compatibily with old style MinGW packages with no .dll.a files +# needed since CMake v3.17 because of fix for #20019 +if (MINGW) + set (CMAKE_FIND_LIBRARY_SUFFIXES ".dll" ".dll.a" ".a" ".lib") +endif () + # Use double precision by default. if (FFTW3_FIND_COMPONENTS MATCHES "^$") set (_components double) diff --git a/CMakeLists.txt b/CMakeLists.txt index cb9b0e33a..f24abbbeb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -222,9 +222,12 @@ set (WSJT_QT_CONF_DESTINATION ${QT_CONF_DESTINATION} CACHE PATH "Path for the qt # # Project sources # +set (fort_qt_CXXSRCS + lib/shmem.cpp + ) + set (wsjt_qt_CXXSRCS qt_helpers.cpp - lib/shmem.cpp widgets/MessageBox.cpp MetaDataRegistry.cpp Network/NetworkServerLookup.cpp @@ -289,6 +292,8 @@ set (wsjt_qt_CXXSRCS logbook/AD1CCty.cpp logbook/WorkedBefore.cpp logbook/Multiplier.cpp + Network/NetworkAccessManager.cpp + widgets/LazyFillComboBox.cpp ) set (wsjt_qtmm_CXXSRCS @@ -302,7 +307,7 @@ set (jt9_FSRCS set (wsjtx_CXXSRCS logbook/logbook.cpp - Network/psk_reporter.cpp + Network/PSKReporter.cpp Modulator/Modulator.cpp Detector/Detector.cpp widgets/logqso.cpp @@ -358,6 +363,8 @@ endif (WIN32) set (wsjt_FSRCS # put module sources first in the hope that they get rebuilt before use + lib/types.f90 + lib/C_interface_module.f90 lib/shmem.f90 lib/crc.f90 lib/fftw3mod.f90 @@ -369,6 +376,7 @@ set (wsjt_FSRCS lib/jt65_mod.f90 lib/ft8_decode.f90 lib/ft4_decode.f90 + lib/fst4_decode.f90 lib/jt9_decode.f90 lib/options.f90 lib/packjt.f90 @@ -395,6 +403,7 @@ set (wsjt_FSRCS lib/badmsg.f90 lib/ft8/baseline.f90 lib/ft4/ft4_baseline.f90 + lib/blanker.f90 lib/bpdecode40.f90 lib/bpdecode128_90.f90 lib/ft8/bpdecode174_91.f90 @@ -544,6 +553,7 @@ set (wsjt_FSRCS lib/qra64a.f90 lib/refspectrum.f90 lib/savec2.f90 + lib/sec0.f90 lib/sec_midn.f90 lib/setup65.f90 lib/sh65.f90 @@ -595,6 +605,21 @@ set (wsjt_FSRCS lib/wqencode.f90 lib/wspr_downsample.f90 lib/zplot9.f90 + lib/fst4/decode240_101.f90 + lib/fst4/decode240_74.f90 + lib/fst4/encode240_101.f90 + lib/fst4/encode240_74.f90 + lib/fst4/fst4sim.f90 + lib/fst4/gen_fst4wave.f90 + lib/fst4/genfst4.f90 + lib/fst4/get_fst4_bitmetrics.f90 + lib/fst4/get_fst4_bitmetrics2.f90 + lib/fst4/ldpcsim240_101.f90 + lib/fst4/ldpcsim240_74.f90 + lib/fst4/osd240_101.f90 + lib/fst4/osd240_74.f90 + lib/fst4/get_crc24.f90 + lib/fst4/fst4_baseline.f90 ) # temporary workaround for a gfortran v7.3 ICE on Fedora 27 64-bit @@ -710,6 +735,7 @@ set (qcp_CXXSRCS set (all_CXXSRCS ${wsjt_CXXSRCS} + ${fort_qt_CXXSRCS} ${wsjt_qt_CXXSRCS} ${wsjt_qtmm_CXXSRCS} ${wsjtx_CXXSRCS} @@ -724,10 +750,6 @@ set (all_C_and_CXXSRCS ) set (TOP_LEVEL_RESOURCES - shortcuts.txt - mouse_commands.txt - prefixes.txt - cty.dat icons/Darwin/wsjtx.iconset/icon_128x128.png contrib/gpl-v3-logo.svg artwork/splash.png @@ -846,6 +868,8 @@ endif (APPLE) # # find some useful tools # +include (CheckSymbolExists) + find_program(CTAGS ctags) find_program(ETAGS etags) @@ -866,7 +890,7 @@ find_package (OpenMP) # # fftw3 single precision library # -find_package (FFTW3 COMPONENTS double single threads REQUIRED) +find_package (FFTW3 COMPONENTS single threads REQUIRED) # # libhamlib setup @@ -881,6 +905,10 @@ message (STATUS "hamlib_INCLUDE_DIRS: ${hamlib_INCLUDE_DIRS}") message (STATUS "hamlib_LIBRARIES: ${hamlib_LIBRARIES}") message (STATUS "hamlib_LIBRARY_DIRS: ${hamlib_LIBRARY_DIRS}") +set (CMAKE_REQUIRED_INCLUDES "${hamlib_INCLUDE_DIRS}") +set (CMAKE_REQUIRED_LIBRARIES "${hamlib_LIBRARIES}") +check_symbol_exists (rig_set_cache_timeout_ms "hamlib/rig.h" HAVE_HAMLIB_CACHING) + # # Qt5 setup @@ -903,7 +931,9 @@ endif () if (WSJT_GENERATE_DOCS) add_subdirectory (doc) endif (WSJT_GENERATE_DOCS) - +if (EXISTS ${CMAKE_SOURCE_DIR}/tests AND IS_DIRECTORY ${CMAKE_SOURCE_DIR}/tests) + add_subdirectory (tests) +endif () # # Library building setup @@ -1097,14 +1127,19 @@ add_custom_target (etags COMMAND ${ETAGS} -o ${CMAKE_SOURCE_DIR}/TAGS -R ${sourc # Qt i18n - always include the country generic if any regional variant is included set (LANGUAGES + ca # Catalan + da # Danish en # English (we need this to stop # translation loaders loading the # second preference UI languge, it # doesn't need to be populated) en_GB # English UK es # Spanish - ca # Catalan + it # Italian ja # Japanese + #no # Norwegian + #pt # Portuguese + #sv # Swedish zh # Chinese zh_HK # Chinese per Hong Kong it # Italian @@ -1127,6 +1162,7 @@ if (UPDATE_TRANSLATIONS) qt5_create_translation ( QM_FILES ${wsjt_qt_UISRCS} ${wsjtx_UISRCS} ${wsjt_qt_CXXSRCS} ${wsjtx_CXXSRCS} ${TS_FILES} + OPTIONS -I${CMAKE_CURRENT_SOURCE_DIR} ) else () qt5_add_translation (QM_FILES ${TS_FILES}) @@ -1227,6 +1263,11 @@ if (WIN32) target_link_libraries (wsjt_qt Qt5::AxContainer Qt5::AxBase) endif (WIN32) +# build a library of package Qt functionality used in Fortran utilities +add_library (fort_qt STATIC ${fort_qt_CXXSRCS}) +target_link_libraries (fort_qt Qt5::Core) + +# build a library of WSJT Qt multimedia components add_library (wsjt_qtmm STATIC ${wsjt_qtmm_CXXSRCS} ${wsjt_qtmm_GENUISRCS}) target_link_libraries (wsjt_qtmm Qt5::Multimedia) @@ -1274,9 +1315,9 @@ if (${OPENMP_FOUND} OR APPLE) LINK_FLAGS -Wl,--stack,16777216 ) endif () - target_link_libraries (jt9 wsjt_fort_omp wsjt_cxx wsjt_qt) + target_link_libraries (jt9 wsjt_fort_omp wsjt_cxx fort_qt) else (${OPENMP_FOUND} OR APPLE) - target_link_libraries (jt9 wsjt_fort wsjt_cxx Qt5::Core) + target_link_libraries (jt9 wsjt_fort wsjt_cxx fort_qt) endif (${OPENMP_FOUND} OR APPLE) if(WSJT_BUILD_UTILS) @@ -1346,6 +1387,15 @@ target_link_libraries (ft4sim_mult wsjt_fort wsjt_cxx) add_executable (record_time_signal Audio/tools/record_time_signal.cpp) target_link_libraries (record_time_signal wsjt_cxx wsjt_qtmm wsjt_qt) +add_executable (fst4sim lib/fst4/fst4sim.f90 wsjtx.rc) +target_link_libraries (fst4sim wsjt_fort wsjt_cxx) + +add_executable (ldpcsim240_101 lib/fst4/ldpcsim240_101.f90 wsjtx.rc) +target_link_libraries (ldpcsim240_101 wsjt_fort wsjt_cxx) + +add_executable (ldpcsim240_74 lib/fst4/ldpcsim240_74.f90 wsjtx.rc) +target_link_libraries (ldpcsim240_74 wsjt_fort wsjt_cxx) + endif(WSJT_BUILD_UTILS) # build the main application @@ -1485,7 +1535,7 @@ install (TARGETS jt9 wsprd fmtave fcal fmeasure if(WSJT_BUILD_UTILS) install (TARGETS ft8code jt65code qra64code qra64sim jt9code jt4code - msk144code + msk144code fst4sim RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime ) @@ -1525,6 +1575,7 @@ install (FILES ) install (FILES + cty.dat contrib/Ephemeris/JPLEPH DESTINATION ${CMAKE_INSTALL_DATADIR}/${CMAKE_PROJECT_NAME} #COMPONENT runtime @@ -1805,11 +1856,11 @@ endif () set (CPACK_DEBIAN_PACKAGE_DESCRIPTION "${PROJECT_DESCRIPTION}") set (CPACK_DEBIAN_PACKAGE_HOMEPAGE "${PROJECT_HOMEPAGE}") -set (CPACK_DEBIAN_PACKAGE_DEPENDS "libgfortran3 (>=4.8.2), libqt5serialport5 (>=5.5), libqt5multimedia5-plugins (>=5.5), libqt5widgets5 (>=5.5), libqt5sql5-sqlite (>=5.5), libusb-1.0-0, libudev1, libc6 (>=2.19)") +set (CPACK_DEBIAN_PACKAGE_DEPENDS "libgfortran5 (>=10), libfftw3-single3 (>=3.3.8), libgomp1 (>=10), libqt5serialport5 (>=5.12.8), libqt5multimedia5-plugins (>=5.12.8), libqt5widgets5 (>=5.12.8), libqt5network5 (>=5.12.8), libqt5printsupport5 (>=5.12.8), libqt5sql5-sqlite (>=5.12.8), libusb-1.0-0 (>=1.0.23)") set (CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) set (CPACK_RPM_PACKAGE_ARCHITECTURE ${CMAKE_SYSTEM_PROCESSOR}) -set (CPACK_RPM_PACKAGE_REQUIRES "qt5-qtserialport >= 5.5, qt5-qtmultimedia >= 5.5, qt5-qtsvg, libusb, systemd-udev, glibc >= 2, libgfortran >= 4.8.2") +set (CPACK_RPM_PACKAGE_REQUIRES "qt5-qtbase >= 5.13.2, qt5-qtserialport >= 5.13.2, qt5-qtmultimedia >= 5.13.2, qt5-qtsvg >= 5.13.2, libusbx >= 1.0.23, libgfortran >= 10.0.1, libgomp >= 10.0.1, fftw-libs-single >= 3.3.8") set (CPACK_RPM_EXCLUDE_FROM_AUTO_FILELIST_ADDITION /usr/share/pixmaps /usr/share/applications /usr/share/man /usr/share/man1) configure_file ("${PROJECT_SOURCE_DIR}/CMakeCPackOptions.cmake.in" diff --git a/Configuration.cpp b/Configuration.cpp index 1ca9ae25a..0466fd662 100644 --- a/Configuration.cpp +++ b/Configuration.cpp @@ -135,8 +135,11 @@ #include #include +#include #include #include +#include +#include #include #include #include @@ -186,6 +189,7 @@ #include "Network/LotWUsers.hpp" #include "models/DecodeHighlightingModel.hpp" #include "logbook/logbook.h" +#include "widgets/LazyFillComboBox.hpp" #include "ui_Configuration.h" #include "moc_Configuration.cpp" @@ -401,6 +405,7 @@ class Configuration::impl final public: using FrequencyDelta = Radio::FrequencyDelta; using port_type = Configuration::port_type; + using audio_info_type = QPair >; explicit impl (Configuration * self , QNetworkAccessManager * network_manager @@ -429,9 +434,13 @@ private: void read_settings (); void write_settings (); - bool load_audio_devices (QAudio::Mode, QComboBox *, QAudioDeviceInfo *); + void find_audio_devices (); + QAudioDeviceInfo find_audio_device (QAudio::Mode, QComboBox *, QString const& device_name); + void load_audio_devices (QAudio::Mode, QComboBox *, QAudioDeviceInfo *); void update_audio_channels (QComboBox const *, int, QComboBox *, bool); + void find_tab (QWidget *); + void initialize_models (); bool split_mode () const { @@ -477,8 +486,6 @@ private: Q_SLOT void on_force_DTR_combo_box_currentIndexChanged (int); Q_SLOT void on_force_RTS_combo_box_currentIndexChanged (int); Q_SLOT void on_rig_combo_box_currentIndexChanged (int); - Q_SLOT void on_sound_input_combo_box_currentTextChanged (QString const&); - Q_SLOT void on_sound_output_combo_box_currentTextChanged (QString const&); Q_SLOT void on_add_macro_push_button_clicked (bool = false); Q_SLOT void on_delete_macro_push_button_clicked (bool = false); Q_SLOT void on_PTT_method_button_group_buttonClicked (int); @@ -604,6 +611,7 @@ private: bool id_after_73_; bool tx_QSY_allowed_; bool spot_to_psk_reporter_; + bool psk_reporter_tcpip_; bool monitor_off_at_startup_; bool monitor_last_used_; bool log_as_RTTY_; @@ -646,11 +654,13 @@ private: bool pwrBandTuneMemory_; QAudioDeviceInfo audio_input_device_; - bool default_audio_input_device_selected_; + QAudioDeviceInfo next_audio_input_device_; AudioDevice::Channel audio_input_channel_; + AudioDevice::Channel next_audio_input_channel_; QAudioDeviceInfo audio_output_device_; - bool default_audio_output_device_selected_; + QAudioDeviceInfo next_audio_output_device_; AudioDevice::Channel audio_output_channel_; + AudioDevice::Channel next_audio_output_channel_; friend class Configuration; }; @@ -701,6 +711,7 @@ bool Configuration::spot_to_psk_reporter () const // rig must be open and working to spot externally return is_transceiver_online () && m_->spot_to_psk_reporter_; } +bool Configuration::psk_reporter_tcpip () const {return m_->psk_reporter_tcpip_;} bool Configuration::monitor_off_at_startup () const {return m_->monitor_off_at_startup_;} bool Configuration::monitor_last_used () const {return m_->rig_is_dummy_ || m_->monitor_last_used_;} bool Configuration::log_as_RTTY () const {return m_->log_as_RTTY_;} @@ -850,6 +861,16 @@ void Configuration::sync_transceiver (bool force_signal, bool enforce_mode_and_s } } +void Configuration::invalidate_audio_input_device (QString /* error */) +{ + m_->audio_input_device_ = QAudioDeviceInfo {}; +} + +void Configuration::invalidate_audio_output_device (QString /* error */) +{ + m_->audio_output_device_ = QAudioDeviceInfo {}; +} + bool Configuration::valid_n1mm_info () const { // do very rudimentary checking on the n1mm server name and port number. @@ -895,7 +916,7 @@ auto Configuration::special_op_id () const -> SpecialOperatingActivity void Configuration::set_location (QString const& grid_descriptor) { // change the dynamic grid - qDebug () << "Configuration::set_location - location:" << grid_descriptor; + // qDebug () << "Configuration::set_location - location:" << grid_descriptor; m_->dynamic_grid_ = grid_descriptor.trimmed (); } @@ -974,8 +995,6 @@ Configuration::impl::impl (Configuration * self, QNetworkAccessManager * network , transceiver_command_number_ {0} , degrade_ {0.} // initialize to zero each run, not // saved in settings - , default_audio_input_device_selected_ {false} - , default_audio_output_device_selected_ {false} { ui_->setupUi (this); @@ -1025,6 +1044,21 @@ Configuration::impl::impl (Configuration * self, QNetworkAccessManager * network // this must be done after the default paths above are set read_settings (); + connect (ui_->sound_input_combo_box, &LazyFillComboBox::about_to_show_popup, [this] () { + QGuiApplication::setOverrideCursor (QCursor {Qt::WaitCursor}); + load_audio_devices (QAudio::AudioInput, ui_->sound_input_combo_box, &next_audio_input_device_); + update_audio_channels (ui_->sound_input_combo_box, ui_->sound_input_combo_box->currentIndex (), ui_->sound_input_channel_combo_box, false); + ui_->sound_input_channel_combo_box->setCurrentIndex (next_audio_input_channel_); + QGuiApplication::restoreOverrideCursor (); + }); + connect (ui_->sound_output_combo_box, &LazyFillComboBox::about_to_show_popup, [this] () { + QGuiApplication::setOverrideCursor (QCursor {Qt::WaitCursor}); + load_audio_devices (QAudio::AudioOutput, ui_->sound_output_combo_box, &next_audio_output_device_); + update_audio_channels (ui_->sound_output_combo_box, ui_->sound_output_combo_box->currentIndex (), ui_->sound_output_channel_combo_box, true); + ui_->sound_output_channel_combo_box->setCurrentIndex (next_audio_output_channel_); + QGuiApplication::restoreOverrideCursor (); + }); + // set up LoTW users CSV file fetching connect (&lotw_users_, &LotWUsers::load_finished, [this] () { ui_->LotW_CSV_fetch_push_button->setEnabled (true); @@ -1128,7 +1162,9 @@ Configuration::impl::impl (Configuration * self, QNetworkAccessManager * network ui_->frequencies_table_view->setModel (&next_frequencies_); ui_->frequencies_table_view->horizontalHeader ()->setSectionResizeMode (QHeaderView::ResizeToContents); + ui_->frequencies_table_view->horizontalHeader ()->setResizeContentsPrecision (0); ui_->frequencies_table_view->verticalHeader ()->setSectionResizeMode (QHeaderView::ResizeToContents); + ui_->frequencies_table_view->verticalHeader ()->setResizeContentsPrecision (0); ui_->frequencies_table_view->sortByColumn (FrequencyList_v2::frequency_column, Qt::AscendingOrder); ui_->frequencies_table_view->setColumnHidden (FrequencyList_v2::frequency_mhz_column, true); @@ -1168,7 +1204,9 @@ Configuration::impl::impl (Configuration * self, QNetworkAccessManager * network stations_.sort (StationList::band_column); ui_->stations_table_view->setModel (&next_stations_); ui_->stations_table_view->horizontalHeader ()->setSectionResizeMode (QHeaderView::ResizeToContents); + ui_->stations_table_view->horizontalHeader ()->setResizeContentsPrecision (0); ui_->stations_table_view->verticalHeader ()->setSectionResizeMode (QHeaderView::ResizeToContents); + ui_->stations_table_view->verticalHeader ()->setResizeContentsPrecision (0); ui_->stations_table_view->sortByColumn (StationList::band_column, Qt::AscendingOrder); // stations delegates @@ -1187,21 +1225,14 @@ Configuration::impl::impl (Configuration * self, QNetworkAccessManager * network // ui_->highlighting_list_view->setModel (&next_decode_highlighing_model_); - // - // load combo boxes with audio setup choices - // - default_audio_input_device_selected_ = load_audio_devices (QAudio::AudioInput, ui_->sound_input_combo_box, &audio_input_device_); - default_audio_output_device_selected_ = load_audio_devices (QAudio::AudioOutput, ui_->sound_output_combo_box, &audio_output_device_); - - update_audio_channels (ui_->sound_input_combo_box, ui_->sound_input_combo_box->currentIndex (), ui_->sound_input_channel_combo_box, false); - update_audio_channels (ui_->sound_output_combo_box, ui_->sound_output_combo_box->currentIndex (), ui_->sound_output_channel_combo_box, true); - - ui_->sound_input_channel_combo_box->setCurrentIndex (audio_input_channel_); - ui_->sound_output_channel_combo_box->setCurrentIndex (audio_output_channel_); - enumerate_rigs (); initialize_models (); + audio_input_device_ = next_audio_input_device_; + audio_input_channel_ = next_audio_input_channel_; + audio_output_device_ = next_audio_output_device_; + audio_output_channel_ = next_audio_output_channel_; + transceiver_thread_ = new QThread {this}; transceiver_thread_->start (); } @@ -1215,6 +1246,16 @@ Configuration::impl::~impl () void Configuration::impl::initialize_models () { + next_audio_input_device_ = audio_input_device_; + next_audio_input_channel_ = audio_input_channel_; + next_audio_output_device_ = audio_output_device_; + next_audio_output_channel_ = audio_output_channel_; + restart_sound_input_device_ = false; + restart_sound_output_device_ = false; + { + SettingsGroup g {settings_, "Configuration"}; + find_audio_devices (); + } auto pal = ui_->callsign_line_edit->palette (); if (my_callsign_.isEmpty ()) { @@ -1236,11 +1277,13 @@ void Configuration::impl::initialize_models () ui_->sbDegrade->setValue (degrade_); ui_->sbBandwidth->setValue (RxBandwidth_); ui_->PTT_method_button_group->button (rig_params_.ptt_type)->setChecked (true); + ui_->save_path_display_label->setText (save_directory_.absolutePath ()); ui_->azel_path_display_label->setText (azel_directory_.absolutePath ()); ui_->CW_id_after_73_check_box->setChecked (id_after_73_); ui_->tx_QSY_check_box->setChecked (tx_QSY_allowed_); ui_->psk_reporter_check_box->setChecked (spot_to_psk_reporter_); + ui_->psk_reporter_tcpip_check_box->setChecked (psk_reporter_tcpip_); ui_->monitor_off_check_box->setChecked (monitor_off_at_startup_); ui_->monitor_last_used_check_box->setChecked (monitor_last_used_); ui_->log_as_RTTY_check_box->setChecked (log_as_RTTY_); @@ -1387,67 +1430,12 @@ void Configuration::impl::read_settings () save_directory_.setPath (settings_->value ("SaveDir", default_save_directory_.absolutePath ()).toString ()); azel_directory_.setPath (settings_->value ("AzElDir", default_azel_directory_.absolutePath ()).toString ()); - { - // - // retrieve audio input device - // - auto saved_name = settings_->value ("SoundInName").toString (); - - // deal with special Windows default audio devices - auto default_device = QAudioDeviceInfo::defaultInputDevice (); - if (saved_name == default_device.deviceName ()) - { - audio_input_device_ = default_device; - default_audio_input_device_selected_ = true; - } - else - { - default_audio_input_device_selected_ = false; - Q_FOREACH (auto const& p, QAudioDeviceInfo::availableDevices (QAudio::AudioInput)) // available audio input devices - { - if (p.deviceName () == saved_name) - { - audio_input_device_ = p; - } - } - } - } - - { - // - // retrieve audio output device - // - auto saved_name = settings_->value("SoundOutName").toString(); - - // deal with special Windows default audio devices - auto default_device = QAudioDeviceInfo::defaultOutputDevice (); - if (saved_name == default_device.deviceName ()) - { - audio_output_device_ = default_device; - default_audio_output_device_selected_ = true; - } - else - { - default_audio_output_device_selected_ = false; - Q_FOREACH (auto const& p, QAudioDeviceInfo::availableDevices (QAudio::AudioOutput)) // available audio output devices - { - if (p.deviceName () == saved_name) - { - audio_output_device_ = p; - } - } - } - } - - // retrieve audio channel info - audio_input_channel_ = AudioDevice::fromString (settings_->value ("AudioInputChannel", "Mono").toString ()); - audio_output_channel_ = AudioDevice::fromString (settings_->value ("AudioOutputChannel", "Mono").toString ()); - type_2_msg_gen_ = settings_->value ("Type2MsgGen", QVariant::fromValue (Configuration::type_2_msg_3_full)).value (); monitor_off_at_startup_ = settings_->value ("MonitorOFF", false).toBool (); monitor_last_used_ = settings_->value ("MonitorLastUsed", false).toBool (); spot_to_psk_reporter_ = settings_->value ("PSKReporter", false).toBool (); + psk_reporter_tcpip_ = settings_->value ("PSKReporterTCPIP", false).toBool (); id_after_73_ = settings_->value ("After73", false).toBool (); tx_QSY_allowed_ = settings_->value ("TxQSYAllowed", false).toBool (); use_dynamic_grid_ = settings_->value ("AutoGrid", false).toBool (); @@ -1543,6 +1531,33 @@ void Configuration::impl::read_settings () pwrBandTuneMemory_ = settings_->value("pwrBandTuneMemory",false).toBool (); } +void Configuration::impl::find_audio_devices () +{ + // + // retrieve audio input device + // + auto saved_name = settings_->value ("SoundInName").toString (); + if (next_audio_input_device_.deviceName () != saved_name || next_audio_input_device_.isNull ()) + { + next_audio_input_device_ = find_audio_device (QAudio::AudioInput, ui_->sound_input_combo_box, saved_name); + next_audio_input_channel_ = AudioDevice::fromString (settings_->value ("AudioInputChannel", "Mono").toString ()); + update_audio_channels (ui_->sound_input_combo_box, ui_->sound_input_combo_box->currentIndex (), ui_->sound_input_channel_combo_box, false); + ui_->sound_input_channel_combo_box->setCurrentIndex (next_audio_input_channel_); + } + + // + // retrieve audio output device + // + saved_name = settings_->value("SoundOutName").toString(); + if (next_audio_output_device_.deviceName () != saved_name || next_audio_output_device_.isNull ()) + { + next_audio_output_device_ = find_audio_device (QAudio::AudioOutput, ui_->sound_output_combo_box, saved_name); + next_audio_output_channel_ = AudioDevice::fromString (settings_->value ("AudioOutputChannel", "Mono").toString ()); + update_audio_channels (ui_->sound_output_combo_box, ui_->sound_output_combo_box->currentIndex (), ui_->sound_output_channel_combo_box, true); + ui_->sound_output_channel_combo_box->setCurrentIndex (next_audio_output_channel_); + } +} + void Configuration::impl::write_settings () { SettingsGroup g {settings_, "Configuration"}; @@ -1562,31 +1577,21 @@ void Configuration::impl::write_settings () settings_->setValue ("PTTport", rig_params_.ptt_port); settings_->setValue ("SaveDir", save_directory_.absolutePath ()); settings_->setValue ("AzElDir", azel_directory_.absolutePath ()); - - if (default_audio_input_device_selected_) - { - settings_->setValue ("SoundInName", QAudioDeviceInfo::defaultInputDevice ().deviceName ()); - } - else + if (!audio_input_device_.isNull ()) { settings_->setValue ("SoundInName", audio_input_device_.deviceName ()); + settings_->setValue ("AudioInputChannel", AudioDevice::toString (audio_input_channel_)); } - - if (default_audio_output_device_selected_) - { - settings_->setValue ("SoundOutName", QAudioDeviceInfo::defaultOutputDevice ().deviceName ()); - } - else + if (!audio_output_device_.isNull ()) { settings_->setValue ("SoundOutName", audio_output_device_.deviceName ()); + settings_->setValue ("AudioOutputChannel", AudioDevice::toString (audio_output_channel_)); } - - settings_->setValue ("AudioInputChannel", AudioDevice::toString (audio_input_channel_)); - settings_->setValue ("AudioOutputChannel", AudioDevice::toString (audio_output_channel_)); settings_->setValue ("Type2MsgGen", QVariant::fromValue (type_2_msg_gen_)); settings_->setValue ("MonitorOFF", monitor_off_at_startup_); settings_->setValue ("MonitorLastUsed", monitor_last_used_); settings_->setValue ("PSKReporter", spot_to_psk_reporter_); + settings_->setValue ("PSKReporterTCPIP", psk_reporter_tcpip_); settings_->setValue ("After73", id_after_73_); settings_->setValue ("TxQSYAllowed", tx_QSY_allowed_); settings_->setValue ("Macros", macros_.stringList ()); @@ -1653,6 +1658,7 @@ void Configuration::impl::write_settings () settings_->setValue ("pwrBandTuneMemory", pwrBandTuneMemory_); settings_->setValue ("Region", QVariant::fromValue (region_)); settings_->setValue ("AutoGrid", use_dynamic_grid_); + settings_->sync (); } void Configuration::impl::set_rig_invariants () @@ -1785,17 +1791,27 @@ void Configuration::impl::set_rig_invariants () bool Configuration::impl::validate () { if (ui_->sound_input_combo_box->currentIndex () < 0 - && !QAudioDeviceInfo::availableDevices (QAudio::AudioInput).empty ()) + && next_audio_input_device_.isNull ()) { + find_tab (ui_->sound_input_combo_box); + MessageBox::critical_message (this, tr ("Invalid audio input device")); + return false; + } + + if (ui_->sound_input_channel_combo_box->currentIndex () < 0 + && next_audio_input_device_.isNull ()) + { + find_tab (ui_->sound_input_combo_box); MessageBox::critical_message (this, tr ("Invalid audio input device")); return false; } if (ui_->sound_output_combo_box->currentIndex () < 0 - && !QAudioDeviceInfo::availableDevices (QAudio::AudioOutput).empty ()) + && next_audio_output_device_.isNull ()) { - MessageBox::critical_message (this, tr ("Invalid audio out device")); - return false; + find_tab (ui_->sound_output_combo_box); + MessageBox::information_message (this, tr ("Invalid audio output device")); + // don't reject as we can work without an audio output } if (!ui_->PTT_method_button_group->checkedButton ()->isEnabled ()) @@ -1817,16 +1833,7 @@ bool Configuration::impl::validate () if (ui_->rbField_Day->isEnabled () && ui_->rbField_Day->isChecked () && !ui_->Field_Day_Exchange->hasAcceptableInput ()) { - for (auto * parent = ui_->Field_Day_Exchange->parentWidget (); parent; parent = parent->parentWidget ()) - { - auto index = ui_->configuration_tabs->indexOf (parent); - if (index != -1) - { - ui_->configuration_tabs->setCurrentIndex (index); - break; - } - } - ui_->Field_Day_Exchange->setFocus (); + find_tab (ui_->Field_Day_Exchange); MessageBox::critical_message (this, tr ("Invalid Contest Exchange") , tr ("You must input a valid ARRL Field Day exchange")); return false; @@ -1835,16 +1842,7 @@ bool Configuration::impl::validate () if (ui_->rbRTTY_Roundup->isEnabled () && ui_->rbRTTY_Roundup->isChecked () && !ui_->RTTY_Exchange->hasAcceptableInput ()) { - for (auto * parent = ui_->RTTY_Exchange->parentWidget (); parent; parent = parent->parentWidget ()) - { - auto index = ui_->configuration_tabs->indexOf (parent); - if (index != -1) - { - ui_->configuration_tabs->setCurrentIndex (index); - break; - } - } - ui_->RTTY_Exchange->setFocus (); + find_tab (ui_->RTTY_Exchange); MessageBox::critical_message (this, tr ("Invalid Contest Exchange") , tr ("You must input a valid ARRL RTTY Roundup exchange")); return false; @@ -1865,6 +1863,7 @@ int Configuration::impl::exec () rig_changed_ = false; initialize_models (); + return QDialog::exec(); } @@ -1962,85 +1961,67 @@ void Configuration::impl::accept () // related configuration parameters rig_is_dummy_ = TransceiverFactory::basic_transceiver_name_ == rig_params_.rig_name; - // Check to see whether SoundInThread must be restarted, - // and save user parameters. { - auto const& device_name = ui_->sound_input_combo_box->currentText (); - if (device_name != audio_input_device_.deviceName ()) + auto const& selected_device = ui_->sound_input_combo_box->currentData ().value ().first; + if (selected_device != next_audio_input_device_) { - auto const& default_device = QAudioDeviceInfo::defaultInputDevice (); - if (device_name == default_device.deviceName ()) - { - audio_input_device_ = default_device; - } - else - { - bool found {false}; - Q_FOREACH (auto const& d, QAudioDeviceInfo::availableDevices (QAudio::AudioInput)) - { - if (device_name == d.deviceName ()) - { - audio_input_device_ = d; - found = true; - } - } - if (!found) - { - audio_input_device_ = default_device; - } - } - restart_sound_input_device_ = true; + next_audio_input_device_ = selected_device; } } { - auto const& device_name = ui_->sound_output_combo_box->currentText (); - if (device_name != audio_output_device_.deviceName ()) + auto const& selected_device = ui_->sound_output_combo_box->currentData ().value ().first; + if (selected_device != next_audio_output_device_) { - auto const& default_device = QAudioDeviceInfo::defaultOutputDevice (); - if (device_name == default_device.deviceName ()) - { - audio_output_device_ = default_device; - } - else - { - bool found {false}; - Q_FOREACH (auto const& d, QAudioDeviceInfo::availableDevices (QAudio::AudioOutput)) - { - if (device_name == d.deviceName ()) - { - audio_output_device_ = d; - found = true; - } - } - if (!found) - { - audio_output_device_ = default_device; - } - } - restart_sound_output_device_ = true; + next_audio_output_device_ = selected_device; } } - if (audio_input_channel_ != static_cast (ui_->sound_input_channel_combo_box->currentIndex ())) + if (next_audio_input_channel_ != static_cast (ui_->sound_input_channel_combo_box->currentIndex ())) { - audio_input_channel_ = static_cast (ui_->sound_input_channel_combo_box->currentIndex ()); + next_audio_input_channel_ = static_cast (ui_->sound_input_channel_combo_box->currentIndex ()); + } + Q_ASSERT (next_audio_input_channel_ <= AudioDevice::Right); + + if (next_audio_output_channel_ != static_cast (ui_->sound_output_channel_combo_box->currentIndex ())) + { + next_audio_output_channel_ = static_cast (ui_->sound_output_channel_combo_box->currentIndex ()); + } + Q_ASSERT (next_audio_output_channel_ <= AudioDevice::Both); + + if (audio_input_device_ != next_audio_input_device_ || next_audio_input_device_.isNull ()) + { + audio_input_device_ = next_audio_input_device_; restart_sound_input_device_ = true; } - Q_ASSERT (audio_input_channel_ <= AudioDevice::Right); - - if (audio_output_channel_ != static_cast (ui_->sound_output_channel_combo_box->currentIndex ())) + if (audio_input_channel_ != next_audio_input_channel_) { - audio_output_channel_ = static_cast (ui_->sound_output_channel_combo_box->currentIndex ()); + audio_input_channel_ = next_audio_input_channel_; + restart_sound_input_device_ = true; + } + if (audio_output_device_ != next_audio_output_device_ || next_audio_output_device_.isNull ()) + { + audio_output_device_ = next_audio_output_device_; restart_sound_output_device_ = true; } - Q_ASSERT (audio_output_channel_ <= AudioDevice::Both); + if (audio_output_channel_ != next_audio_output_channel_) + { + audio_output_channel_ = next_audio_output_channel_; + restart_sound_output_device_ = true; + } + // qDebug () << "Configure::accept: audio i/p:" << audio_input_device_.deviceName () + // << "chan:" << audio_input_channel_ + // << "o/p:" << audio_output_device_.deviceName () + // << "chan:" << audio_output_channel_ + // << "reset i/p:" << restart_sound_input_device_ + // << "reset o/p:" << restart_sound_output_device_; my_callsign_ = ui_->callsign_line_edit->text (); my_grid_ = ui_->grid_line_edit->text (); FD_exchange_= ui_->Field_Day_Exchange->text ().toUpper (); RTTY_exchange_= ui_->RTTY_Exchange->text ().toUpper (); spot_to_psk_reporter_ = ui_->psk_reporter_check_box->isChecked (); + psk_reporter_tcpip_ = ui_->psk_reporter_tcpip_check_box->isChecked (); id_interval_ = ui_->CW_id_interval_spin_box->value (); ntrials_ = ui_->sbNtrials->value (); txDelay_ = ui_->sbTxDelay->value (); @@ -2172,6 +2153,13 @@ void Configuration::impl::reject () } } + // qDebug () << "Configure::reject: audio i/p:" << audio_input_device_.deviceName () + // << "chan:" << audio_input_channel_ + // << "o/p:" << audio_output_device_.deviceName () + // << "chan:" << audio_output_channel_ + // << "reset i/p:" << restart_sound_input_device_ + // << "reset o/p:" << restart_sound_output_device_; + QDialog::reject (); } @@ -2300,16 +2288,6 @@ void Configuration::impl::on_PTT_method_button_group_buttonClicked (int /* id */ set_rig_invariants (); } -void Configuration::impl::on_sound_input_combo_box_currentTextChanged (QString const& text) -{ - default_audio_input_device_selected_ = QAudioDeviceInfo::defaultInputDevice ().deviceName () == text; -} - -void Configuration::impl::on_sound_output_combo_box_currentTextChanged (QString const& text) -{ - default_audio_output_device_selected_ = QAudioDeviceInfo::defaultOutputDevice ().deviceName () == text; -} - void Configuration::impl::on_add_macro_line_edit_editingFinished () { ui_->add_macro_line_edit->setText (ui_->add_macro_line_edit->text ().toUpper ()); @@ -2688,6 +2666,7 @@ void Configuration::impl::transceiver_frequency (Frequency f) current_offset_ = stations_.offset (f); cached_rig_state_.frequency (apply_calibration (f + current_offset_)); + // qDebug () << "Configuration::impl::transceiver_frequency: n:" << transceiver_command_number_ + 1 << "f:" << f; Q_EMIT set_transceiver (cached_rig_state_, ++transceiver_command_number_); } @@ -2713,6 +2692,7 @@ void Configuration::impl::transceiver_tx_frequency (Frequency f) cached_rig_state_.tx_frequency (apply_calibration (f + current_tx_offset_)); } + // qDebug () << "Configuration::impl::transceiver_tx_frequency: n:" << transceiver_command_number_ + 1 << "f:" << f; Q_EMIT set_transceiver (cached_rig_state_, ++transceiver_command_number_); } } @@ -2721,6 +2701,7 @@ void Configuration::impl::transceiver_mode (MODE m) { cached_rig_state_.online (true); // we want the rig online cached_rig_state_.mode (m); + // qDebug () << "Configuration::impl::transceiver_mode: n:" << transceiver_command_number_ + 1 << "m:" << m; Q_EMIT set_transceiver (cached_rig_state_, ++transceiver_command_number_); } @@ -2729,6 +2710,7 @@ void Configuration::impl::transceiver_ptt (bool on) cached_rig_state_.online (true); // we want the rig online set_cached_mode (); cached_rig_state_.ptt (on); + // qDebug () << "Configuration::impl::transceiver_ptt: n:" << transceiver_command_number_ + 1 << "on:" << on; Q_EMIT set_transceiver (cached_rig_state_, ++transceiver_command_number_); } @@ -2827,72 +2809,68 @@ void Configuration::impl::close_rig () } } -// load the available audio devices into the selection combo box and -// select the default device if the current device isn't set or isn't -// available -bool Configuration::impl::load_audio_devices (QAudio::Mode mode, QComboBox * combo_box, QAudioDeviceInfo * device) +// find the audio device that matches the specified name, also +// populate into the selection combo box with any devices we find in +// the search +QAudioDeviceInfo Configuration::impl::find_audio_device (QAudio::Mode mode, QComboBox * combo_box + , QString const& device_name) { using std::copy; using std::back_inserter; - bool result {false}; + if (device_name.size ()) + { + Q_EMIT self_->enumerating_audio_devices (); + auto const& devices = QAudioDeviceInfo::availableDevices (mode); + Q_FOREACH (auto const& p, devices) + { + // qDebug () << "Configuration::impl::find_audio_device: input:" << (QAudio::AudioInput == mode) << "name:" << p.deviceName () << "preferred format:" << p.preferredFormat () << "endians:" << p.supportedByteOrders () << "codecs:" << p.supportedCodecs () << "channels:" << p.supportedChannelCounts () << "rates:" << p.supportedSampleRates () << "sizes:" << p.supportedSampleSizes () << "types:" << p.supportedSampleTypes (); + if (p.deviceName () == device_name) + { + // convert supported channel counts into something we can store in the item model + QList channel_counts; + auto scc = p.supportedChannelCounts (); + copy (scc.cbegin (), scc.cend (), back_inserter (channel_counts)); + combo_box->insertItem (0, device_name, QVariant::fromValue (audio_info_type {p, channel_counts})); + combo_box->setCurrentIndex (0); + return p; + } + } + // insert a place holder for the not found device + combo_box->insertItem (0, device_name + " (" + tr ("Not found", "audio device missing") + ")", QVariant::fromValue (audio_info_type {})); + combo_box->setCurrentIndex (0); + } + return {}; +} + +// load the available audio devices into the selection combo box +void Configuration::impl::load_audio_devices (QAudio::Mode mode, QComboBox * combo_box + , QAudioDeviceInfo * device) +{ + using std::copy; + using std::back_inserter; combo_box->clear (); + Q_EMIT self_->enumerating_audio_devices (); int current_index = -1; - int default_index = -1; - - int extra_items {0}; - - auto const& default_device = (mode == QAudio::AudioInput ? QAudioDeviceInfo::defaultInputDevice () : QAudioDeviceInfo::defaultOutputDevice ()); - - // deal with special default audio devices on Windows - if ("Default Input Device" == default_device.deviceName () - || "Default Output Device" == default_device.deviceName ()) + auto const& devices = QAudioDeviceInfo::availableDevices (mode); + Q_FOREACH (auto const& p, devices) { - default_index = 0; - - QList channel_counts; - auto scc = default_device.supportedChannelCounts (); - copy (scc.cbegin (), scc.cend (), back_inserter (channel_counts)); - - combo_box->addItem (default_device.deviceName (), channel_counts); - ++extra_items; - if (default_device == *device) - { - current_index = 0; - result = true; - } - } - - Q_FOREACH (auto const& p, QAudioDeviceInfo::availableDevices (mode)) - { -// qDebug () << "Audio device: input:" << (QAudio::AudioInput == mode) << "name:" << p.deviceName () << "preferred format:" << p.preferredFormat () << "endians:" << p.supportedByteOrders () << "codecs:" << p.supportedCodecs () << "channels:" << p.supportedChannelCounts () << "rates:" << p.supportedSampleRates () << "sizes:" << p.supportedSampleSizes () << "types:" << p.supportedSampleTypes (); + // qDebug () << "Configuration::impl::load_audio_devices: input:" << (QAudio::AudioInput == mode) << "name:" << p.deviceName () << "preferred format:" << p.preferredFormat () << "endians:" << p.supportedByteOrders () << "codecs:" << p.supportedCodecs () << "channels:" << p.supportedChannelCounts () << "rates:" << p.supportedSampleRates () << "sizes:" << p.supportedSampleSizes () << "types:" << p.supportedSampleTypes (); // convert supported channel counts into something we can store in the item model QList channel_counts; auto scc = p.supportedChannelCounts (); copy (scc.cbegin (), scc.cend (), back_inserter (channel_counts)); - combo_box->addItem (p.deviceName (), channel_counts); + combo_box->addItem (p.deviceName (), QVariant::fromValue (audio_info_type {p, channel_counts})); if (p == *device) { current_index = combo_box->count () - 1; } - else if (p == default_device) - { - default_index = combo_box->count () - 1; - } - } - if (current_index < 0) // not found - use default - { - *device = default_device; - result = true; - current_index = default_index; } combo_box->setCurrentIndex (current_index); - - return result; } // enable only the channels that are supported by the selected audio device @@ -2904,7 +2882,8 @@ void Configuration::impl::update_audio_channels (QComboBox const * source_combo_ combo_box->setItemData (i, combo_box_item_disabled, Qt::UserRole - 1); } - Q_FOREACH (QVariant const& v, source_combo_box->itemData (index).toList ()) + Q_FOREACH (QVariant const& v + , (source_combo_box->itemData (index).value ().second)) { // enable valid options int n {v.toInt ()}; @@ -2924,6 +2903,20 @@ void Configuration::impl::update_audio_channels (QComboBox const * source_combo_ } } +void Configuration::impl::find_tab (QWidget * target) +{ + for (auto * parent = target->parentWidget (); parent; parent = parent->parentWidget ()) + { + auto index = ui_->configuration_tabs->indexOf (parent); + if (index != -1) + { + ui_->configuration_tabs->setCurrentIndex (index); + break; + } + } + target->setFocus (); +} + // load all the supported rig names into the selection combo box void Configuration::impl::enumerate_rigs () { diff --git a/Configuration.hpp b/Configuration.hpp index 43267d6ce..8594a4a6f 100644 --- a/Configuration.hpp +++ b/Configuration.hpp @@ -113,6 +113,7 @@ public: bool id_after_73 () const; bool tx_QSY_allowed () const; bool spot_to_psk_reporter () const; + bool psk_reporter_tcpip () const; bool monitor_off_at_startup () const; bool monitor_last_used () const; bool log_as_RTTY () const; @@ -259,6 +260,8 @@ public: // i.e. the transceiver is ready for use. Q_SLOT void sync_transceiver (bool force_signal = false, bool enforce_mode_and_split = false); + Q_SLOT void invalidate_audio_input_device (QString error); + Q_SLOT void invalidate_audio_output_device (QString error); // // These signals indicate a font has been selected and accepted for @@ -292,6 +295,12 @@ public: // the fault condition has been rectified or is transient. Q_SIGNAL void transceiver_failure (QString const& reason) const; + // signal announces audio devices are being enumerated + // + // As this can take some time, particularly on Linux, consumers + // might like to notify the user. + Q_SIGNAL void enumerating_audio_devices (); + private: class impl; pimpl m_; diff --git a/Configuration.ui b/Configuration.ui index fa16476e9..d22032d53 100644 --- a/Configuration.ui +++ b/Configuration.ui @@ -6,8 +6,8 @@ 0 0 - 557 - 561 + 554 + 556 @@ -357,7 +357,7 @@ - Enable VHF/UHF/Microwave features + Enable VHF and submode features @@ -577,6 +577,9 @@ quiet period when decoding is done. 0 + + Serial Port Parameters + Serial Port Parameters @@ -659,6 +662,9 @@ quiet period when decoding is done. <html><head/><body><p>Number of data bits used to communicate with your radio's CAT interface (usually eight).</p></body></html> + + Data bits + Data Bits @@ -710,6 +716,9 @@ quiet period when decoding is done. <html><head/><body><p>Number of stop bits used when communicating with your radio's CAT interface</p><p>(consult you radio's manual for details).</p></body></html> + + Stop bits + Stop Bits @@ -758,6 +767,9 @@ quiet period when decoding is done. <html><head/><body><p>Flow control protocol used between this computer and your radio's CAT interface (usually &quot;None&quot; but some require &quot;Hardware&quot;).</p></body></html> + + Handshake + Handshake @@ -824,6 +836,9 @@ a few, particularly some Kenwood rigs, require it). Special control of CAT port control lines. + + Force Control Lines + Force Control Lines @@ -1350,7 +1365,7 @@ radio interface behave as expected. - + 1 @@ -1366,29 +1381,6 @@ transmitting periods. - - - - - 1 - 0 - - - - Select the audio CODEC to use for receiving. - - - - - - - &Input: - - - sound_input_combo_box - - - @@ -1416,6 +1408,29 @@ transmitting periods. + + + + + 1 + 0 + + + + Select the audio CODEC to use for receiving. + + + + + + + Ou&tput: + + + sound_output_combo_box + + + @@ -1446,13 +1461,13 @@ both here. - - + + - Ou&tput: + &Input: - sound_output_combo_box + sound_input_combo_box @@ -1493,7 +1508,8 @@ both here. false - background-color: rgb(255, 255, 255); + background-color: rgb(255, 255, 255); +color: rgb(0, 0, 0); TextLabel @@ -1541,7 +1557,8 @@ both here. - background-color: rgb(255, 255, 255); + background-color: rgb(255, 255, 255); +color: rgb(0, 0, 0); TextLabel @@ -1800,20 +1817,27 @@ and DX Grid fields when a 73 or free text message is sent. Network Services - - + + - The program can send your station details and all -decoded signals as spots to the http://pskreporter.info web site. -This is used for reverse beacon analysis which is very useful -for assessing propagation and system performance. + <html><head/><body><p>The program can send your station details and all decoded signals with grid squares as spots to the http://pskreporter.info web site.</p><p>This is used for reverse beacon analysis which is very useful for assessing propagation and system performance.</p></body></html> Enable &PSK Reporter Spotting + + + + <html><head/><body><p>Check this option if a reliable connection is needed</p><p>Most users do not need this, the default uses UDP which is more efficient. Only check this if you have evidence that UDP traffic from you to PSK Reporter is being lost.</p></body></html> + + + Use TCP/IP connection + + + @@ -2337,6 +2361,9 @@ Right click for insert and delete options. <html><head/><body><p>URL of the ARRL LotW user's last upload dates and times data file which is used to highlight decodes from stations that are known to upload their log file to LotW.</p></body></html> + + URL + https://lotw.arrl.org/lotw-user-activity.csv @@ -2369,6 +2396,9 @@ Right click for insert and delete options. <html><head/><body><p>Adjust this spin box to set the age threshold of LotW user's last upload date that is accepted as a current LotW user.</p></body></html> + + Days since last upload + days @@ -2504,6 +2534,9 @@ Right click for insert and delete options. <html><head/><body><p>FT8 DXpedition mode: Hound operator calling the DX.</p></body></html> + + Hound + Hound @@ -2526,6 +2559,9 @@ Right click for insert and delete options. <html><head/><body><p>North American VHF/UHF/Microwave contests and others in which a 4-character grid locator is the required exchange.</p></body></html> + + NA VHF Contest + NA VHF Contest @@ -2539,6 +2575,9 @@ Right click for insert and delete options. <html><head/><body><p>FT8 DXpedition mode: Fox (DXpedition) operator.</p></body></html> + + Fox + Fox @@ -2561,6 +2600,9 @@ Right click for insert and delete options. <html><head/><body><p>European VHF+ contests requiring a signal report, serial number, and 6-character locator.</p></body></html> + + EU VHF Contest + EU VHF Contest @@ -2589,6 +2631,9 @@ Right click for insert and delete options. <html><head/><body><p>ARRL RTTY Roundup and similar contests. Exchange is US state, Canadian province, or &quot;DX&quot;.</p></body></html> + + R T T Y Roundup + RTTY Roundup messages @@ -2614,6 +2659,9 @@ Right click for insert and delete options. + + RTTY Roundup exchange + RTTY RU Exch: @@ -2652,6 +2700,9 @@ Right click for insert and delete options. <html><head/><body><p>ARRL Field Day exchange: number of transmitters, Class, and ARRL/RAC section or &quot;DX&quot;.</p></body></html> + + A R R L Field Day + ARRL Field Day @@ -2677,6 +2728,9 @@ Right click for insert and delete options. + + Field Day exchange + FD Exch: @@ -2719,6 +2773,9 @@ Right click for insert and delete options. <html><head/><body><p>World-Wide Digi-mode contest</p><p><br/></p></body></html> + + WW Digital Contest + WW Digi Contest @@ -2831,6 +2888,9 @@ Right click for insert and delete options. 50 + + Tone spacing + Tone spacing @@ -2869,6 +2929,9 @@ Right click for insert and delete options. 50 + + Waterfall spectra + Waterfall spectra @@ -2934,6 +2997,11 @@ Right click for insert and delete options. QListView
widgets/DecodeHighlightingListView.hpp
+ + LazyFillComboBox + QComboBox +
widgets/LazyFillComboBox.hpp
+
configuration_tabs @@ -2942,14 +3010,20 @@ Right click for insert and delete options. use_dynamic_grid region_combo_box type_2_msg_gen_combo_box + decodes_from_top_check_box + insert_blank_check_box + miles_check_box TX_messages_check_box DXCC_check_box + ppfx_check_box font_push_button decoded_text_font_push_button monitor_off_check_box monitor_last_used_check_box quick_call_check_box disable_TX_on_73_check_box + force_call_1st_check_box + alternate_bindings_check_box CW_id_after_73_check_box enable_VHF_features_check_box tx_QSY_check_box @@ -2968,8 +3042,8 @@ Right click for insert and delete options. CAT_one_stop_bit_radio_button CAT_two_stop_bit_radio_button CAT_handshake_default_radio_button - CAT_handshake_none_radio_button CAT_handshake_xon_radio_button + CAT_handshake_none_radio_button CAT_handshake_hardware_radio_button force_DTR_combo_box force_RTS_combo_box @@ -3007,6 +3081,7 @@ Right click for insert and delete options. clear_DX_check_box opCallEntry psk_reporter_check_box + psk_reporter_tcpip_check_box udp_server_line_edit udp_server_port_spin_box accept_udp_requests_check_box @@ -3021,9 +3096,13 @@ Right click for insert and delete options. stations_table_view highlighting_list_view reset_highlighting_to_defaults_push_button + highlight_by_mode_check_box + only_fields_check_box + include_WAE_check_box + rescan_log_push_button LotW_CSV_URL_line_edit - LotW_CSV_fetch_push_button LotW_days_since_upload_spin_box + LotW_CSV_fetch_push_button sbNtrials sbAggressive cbTwoPass @@ -3032,13 +3111,18 @@ Right click for insert and delete options. sbTxDelay cbx2ToneSpacing cbx4ToneSpacing + rbLowSidelobes + rbMaxSensitivity + gbSpecialOpActivity rbFox + rbHound rbNA_VHF_Contest - rbEU_VHF_Contest rbField_Day Field_Day_Exchange + rbEU_VHF_Contest rbRTTY_Roundup RTTY_Exchange + rbWW_DIGI @@ -3108,13 +3192,13 @@ Right click for insert and delete options. - - + + + - - - + + diff --git a/Decoder/decodedtext.cpp b/Decoder/decodedtext.cpp index e476ecb9b..04a5cc87e 100644 --- a/Decoder/decodedtext.cpp +++ b/Decoder/decodedtext.cpp @@ -165,7 +165,13 @@ QString DecodedText::call() const // get the second word, most likely the de call and the third word, most likely grid void DecodedText::deCallAndGrid(/*out*/QString& call, QString& grid) const { - auto const& match = words_re.match (message_); + auto msg = message_; + auto p = msg.indexOf ("; "); + if (p >= 0) + { + msg = msg.mid (p + 2); + } + auto const& match = words_re.match (msg); call = match.captured ("word2"); grid = match.captured ("word3"); if ("R" == grid) grid = match.captured ("word4"); diff --git a/Decoder/decodedtext.pri b/Decoder/decodedtext.pri new file mode 100644 index 000000000..6aa33dc36 --- /dev/null +++ b/Decoder/decodedtext.pri @@ -0,0 +1,3 @@ +SOURCES += Decoder/decodedtext.cpp + +HEADERS += Decoder/decodedtext.h diff --git a/Detector/Detector.cpp b/Detector/Detector.cpp index 06b9c6b2d..164db2ab2 100644 --- a/Detector/Detector.cpp +++ b/Detector/Detector.cpp @@ -36,7 +36,7 @@ void Detector::setBlockSize (unsigned n) bool Detector::reset () { clear (); - // don't call base call reset because it calls seek(0) which causes + // don't call base class reset because it calls seek(0) which causes // a warning return isOpen (); } @@ -119,8 +119,7 @@ qint64 Detector::writeData (char const * data, qint64 maxSize) remaining -= numFramesProcessed; } - - - return maxSize; // we drop any data past the end of the buffer on - // the floor until the next period starts + // we drop any data past the end of the buffer on the floor until + // the next period starts + return maxSize; } diff --git a/Detector/Detector.pri b/Detector/Detector.pri new file mode 100644 index 000000000..a98051194 --- /dev/null +++ b/Detector/Detector.pri @@ -0,0 +1,3 @@ +SOURCES += Detector/Detector.cpp + +HEADERS += Detector/Detector.hpp diff --git a/EqualizationToolsDialog.cpp b/EqualizationToolsDialog.cpp index 2d96fce56..4f5196200 100644 --- a/EqualizationToolsDialog.cpp +++ b/EqualizationToolsDialog.cpp @@ -292,7 +292,7 @@ EqualizationToolsDialog::impl::impl (EqualizationToolsDialog * self | QDialogButtonBox::RestoreDefaults | QDialogButtonBox::Close , Qt::Vertical} { - setWindowTitle (windowTitle () + ' ' + tr (title)); + setWindowTitle (windowTitle () + ' ' + tr ("Equalization Tools")); resize (500, 600); { SettingsGroup g {settings_, title}; diff --git a/INSTALL b/INSTALL index 42169fb38..c96e7d3b3 100644 --- a/INSTALL +++ b/INSTALL @@ -28,7 +28,7 @@ For MS Windows see the section "Building from Source on MS Windows" below. For Apple Mac see the section "Building from Source on Apple Mac". -Qt v5, preferably v5.5 or later is required to build WSJT-X. +Qt v5, preferably v5.9 or later is required to build WSJT-X. Qt v5 multimedia support, serial port, and Linguist is necessary as well as the core Qt v5 components, normally installing the Qt @@ -43,8 +43,8 @@ the libfftw library development package. Normally installing the library development package pulls in all the FFTW v3 libraries including the single precision variant. -The Hamlib library optionally requires the libusb-1.0 library, if the -development version (libusb-1.0-dev) is available Hamlib will +The Hamlib library optionally requires the libusb-1.0-1 library, if +the development version (libusb-1.0-0-dev) is available Hamlib will configure its custom USB device back end drivers. Most rigs do not require this so normally you can choose not to install libusb-1.0-dev but if you have a SoftRock USB or similar SDR that uses a custom USB @@ -89,7 +89,8 @@ $ git clone git://git.code.sf.net/p/wsjt/wsjtx src To build WSJT-X you will need CMake and asciidoc installed. $ cd ~/wsjtx-prefix/build -$ cmake -D CMAKE_PREFIX_PATH=~/hamlib-prefix ../src +$ cmake -D CMAKE_PREFIX_PATH=~/hamlib-prefix -DWSJT_SKIP_MANPAGES=ON \ + -DWSJT_GENERATE_DOCS=OFF ../src $ cmake --build . $ cmake --build . --target install @@ -99,7 +100,8 @@ configure step like: $ cd ~/wsjtx-prefix/build $ cmake -D CMAKE_PREFIX_PATH=~/hamlib-prefix \ - -D CMAKE_INSTALL_PREFIX=~/wsjtx-prefix ../src + -DWSJT_SKIP_MANPAGES=ON -DWSJT_GENERATE_DOCS=OFF \ + -D CMAKE_INSTALL_PREFIX=~/wsjtx-prefix ../src $ cmake --build . $ cmake --build . --target install @@ -316,7 +318,7 @@ configure: $ cd ~/wsjtx-prefix/build $ FC=gfortran-mp-5 \ cmake \ - -D CMAKE_PREFIX_PATH="~/Qt/5.7/clang_64;~/hamlib-prefix;/opt/local" \ + -D CMAKE_PREFIX_PATH="~/Qt/5.9/clang_64;~/hamlib-prefix;/opt/local" \ -D CMAKE_INSTALL_PREFIX=~/wsjtx-prefix \ -D CMAKE_OSX_SYSROOT=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk \ ~/wsjtx-prefix/src diff --git a/Modulator/Modulator.cpp b/Modulator/Modulator.cpp index e527ce3e1..96963029f 100644 --- a/Modulator/Modulator.cpp +++ b/Modulator/Modulator.cpp @@ -45,11 +45,12 @@ Modulator::Modulator (unsigned frameRate, double periodLengthInSeconds, { } -void Modulator::start (unsigned symbolsLength, double framesPerSymbol, +void Modulator::start (QString mode, unsigned symbolsLength, double framesPerSymbol, double frequency, double toneSpacing, SoundOutput * stream, Channel channel, bool synchronize, bool fastMode, double dBSNR, double TRperiod) { + // qDebug () << "mode:" << mode << "symbolsLength:" << symbolsLength << "framesPerSymbol:" << framesPerSymbol << "frequency:" << frequency << "toneSpacing:" << toneSpacing << "channel:" << channel << "synchronize:" << synchronize << "fastMode:" << fastMode << "dBSNR:" << dBSNR << "TRperiod:" << TRperiod; Q_ASSERT (stream); // Time according to this computer which becomes our base time qint64 ms0 = QDateTime::currentMSecsSinceEpoch() % 86400000; @@ -69,8 +70,8 @@ void Modulator::start (unsigned symbolsLength, double framesPerSymbol, m_bFastMode=fastMode; m_TRperiod=TRperiod; unsigned delay_ms=1000; - if(m_nsps==1920) delay_ms=500; //FT8 - if(m_nsps==576) delay_ms=300; //FT4 + if(mode=="FT8" or (mode=="FST4" and m_nsps==720)) delay_ms=500; //FT8, FST4-15 + if(mode=="FT4") delay_ms=300; //FT4 // noise generator parameters if (m_addNoise) { @@ -79,28 +80,39 @@ void Modulator::start (unsigned symbolsLength, double framesPerSymbol, if (m_snr > 1.0) m_fac = 3000.0 / m_snr; } -// round up to an exact portion of a second that allows for startup delays - m_ic = (mstr / delay_ms) * m_frameRate * delay_ms / 1000; - - if(m_bFastMode) m_ic=0; - m_silentFrames = 0; -// calculate number of silent frames to send, so that audio will start at -// the nominal time "delay_ms" into the Tx sequence. - if (synchronize && !m_tuning && !m_bFastMode) { - m_silentFrames = m_ic + m_frameRate / (1000 / delay_ms) - (mstr * (m_frameRate / 1000)); - } - -// qDebug() << "aa" << QDateTime::currentDateTimeUtc().toString("hh:mm:ss.zzz") -// << m_ic << m_silentFrames << m_silentFrames/48000.0 -// << mstr << fmod(double(ms0),1000.0*m_period); - + m_ic=0; + if (!m_tuning && !m_bFastMode) + { + // calculate number of silent frames to send, so that audio will + // start at the nominal time "delay_ms" into the Tx sequence. + if (synchronize) + { + if(delay_ms > mstr) m_silentFrames = (delay_ms - mstr) * m_frameRate / 1000; + } + + // adjust for late starts + if(!m_silentFrames && mstr >= delay_ms) + { + m_ic = (mstr - delay_ms) * m_frameRate / 1000; + } + } initialize (QIODevice::ReadOnly, channel); Q_EMIT stateChanged ((m_state = (synchronize && m_silentFrames) ? Synchronizing : Active)); + + // qDebug() << "delay_ms:" << delay_ms << "mstr:" << mstr << "m_silentFrames:" << m_silentFrames << "m_ic:" << m_ic << "m_state:" << m_state; + m_stream = stream; - if (m_stream) m_stream->restart (this); + if (m_stream) + { + m_stream->restart (this); + } + else + { + qDebug () << "Modulator::start: no audio output stream assigned"; + } } void Modulator::tune (bool newState) @@ -152,21 +164,25 @@ qint64 Modulator::readData (char * data, qint64 maxSize) qint16 * end (samples + numFrames * (bytesPerFrame () / sizeof (qint16))); qint64 framesGenerated (0); -// if(m_ic==0) qDebug() << "Modulator::readData" << 0.001*(QDateTime::currentMSecsSinceEpoch() % (1000*m_TRperiod)); +// if(m_ic==0) qDebug() << "aa" << 0.001*(QDateTime::currentMSecsSinceEpoch() % qint64(1000*m_TRperiod)) +// << m_state << m_TRperiod << m_silentFrames << m_ic << foxcom_.wave[m_ic]; switch (m_state) { case Synchronizing: { - if (m_silentFrames) { // send silence up to first second + if (m_silentFrames) { // send silence up to end of start delay framesGenerated = qMin (m_silentFrames, numFrames); - for ( ; samples != end; samples = load (0, samples)) { // silence - } - m_silentFrames -= framesGenerated; - return framesGenerated * bytesPerFrame (); + do + { + samples = load (0, samples); // silence + } while (--m_silentFrames && samples != end); + if (!m_silentFrames) + { + Q_EMIT stateChanged ((m_state = Active)); + } } - Q_EMIT stateChanged ((m_state = Active)); m_cwLevel = false; m_ramp = 0; // prepare for CW wave shaping } @@ -260,7 +276,7 @@ qint64 Modulator::readData (char * data, qint64 maxSize) qint16 sample; - for (unsigned i = 0; i < numFrames && m_ic <= i1; ++i) { + while (samples != end && m_ic <= i1) { isym=0; if(!m_tuning and m_TRperiod!=3.0) isym=m_ic/(4.0*m_nsps); //Actual fsample=48000 if(m_bFastMode) isym=isym%m_symbolsLength; @@ -305,12 +321,19 @@ qint64 Modulator::readData (char * data, qint64 maxSize) m_amp=32767.0; sample=qRound(m_amp*foxcom_.wave[m_ic]); } - +/* + if((m_ic<1000 or (4*m_symbolsLength*m_nsps - m_ic) < 1000) and (m_ic%10)==0) { + qDebug() << "cc" << QDateTime::currentDateTimeUtc().toString("hh:mm:ss.zzz") << m_ic << sample; + } +*/ samples = load(postProcessSample(sample), samples); ++framesGenerated; ++m_ic; } +// qDebug() << "dd" << QDateTime::currentDateTimeUtc().toString("hh:mm:ss.zzz") +// << m_ic << i1 << foxcom_.wave[m_ic] << framesGenerated; + if (m_amp == 0.0) { // TODO G4WJS: compare double with zero might not be wise if (icw[0] == 0) { // no CW ID to send diff --git a/Modulator/Modulator.hpp b/Modulator/Modulator.hpp index 1043be697..c25074efe 100644 --- a/Modulator/Modulator.hpp +++ b/Modulator/Modulator.hpp @@ -35,7 +35,7 @@ public: void set_nsym(int n) {m_symbolsLength=n;} void set_ms0(qint64 ms) {m_ms0=ms;} - Q_SLOT void start (unsigned symbolsLength, double framesPerSymbol, double frequency, + Q_SLOT void start (QString mode, unsigned symbolsLength, double framesPerSymbol, double frequency, double toneSpacing, SoundOutput *, Channel = Mono, bool synchronize = true, bool fastMode = false, double dBSNR = 99., double TRperiod=60.0); diff --git a/Modulator/Modulator.pri b/Modulator/Modulator.pri new file mode 100644 index 000000000..a90a55f7a --- /dev/null +++ b/Modulator/Modulator.pri @@ -0,0 +1,3 @@ +SOURCES += Modulator/Modulator.cpp + +HEADERS += Modulator/Mpdulator.hpp diff --git a/NEWS b/NEWS index 097282e70..6f7fad9c7 100644 --- a/NEWS +++ b/NEWS @@ -13,6 +13,32 @@ Copyright 2001 - 2020 by Joe Taylor, K1JT. + Release: WSJT-X 2.3.0-rc1 + Sept 28, 2020 + ------------------------- + +WSJT-X 2.3.0 is a program upgrade offering two new modes designed +especially for use on the LF and MF bands. FST4 is for 2-way QSOs, +and FST4W is for WSPR-like transmissions. Both modes offer a range of +options for T/R sequence lengths and threshold decoding sensitivities +extending well into the -40 dB range. Early tests have shown these +modes frequently spanning intercontinental distances on the 2200 m and +630 m bands. Further details and operating hints can be found in the +"Quick-Start Guide to FST4 and FST4W", posted on the WSJT web site: + +https://physics.princeton.edu/pulsar/k1jt/FST4_Quick_Start.pdf + +WSJT-X 2.3.0-rc1 is a beta-quality release candidate for a program +upgrade that provides a number of new features and capabilities. +These include: + + - New modes FST4 and FST4W + + - The *On Dx Echo* Doppler compensation method has been modified in + response to feedback from Users. Basic functionality is unchanged. + See the User Guide (Section 8.1) for more information. + + Release: WSJT-X 2.2.2 June 22, 2020 --------------------- diff --git a/Network/MessageClient.cpp b/Network/MessageClient.cpp index d33b2a636..3c208fd3f 100644 --- a/Network/MessageClient.cpp +++ b/Network/MessageClient.cpp @@ -36,6 +36,7 @@ public: impl (QString const& id, QString const& version, QString const& revision, port_type server_port, MessageClient * self) : self_ {self} + , dns_lookup_id_ {0} , enabled_ {false} , id_ {id} , version_ {version} @@ -81,6 +82,7 @@ public: Q_SLOT void host_info_results (QHostInfo); MessageClient * self_; + int dns_lookup_id_; bool enabled_; QString id_; QString version_; @@ -101,6 +103,7 @@ public: void MessageClient::impl::host_info_results (QHostInfo host_info) { + if (host_info.lookupId () != dns_lookup_id_) return; if (QHostInfo::NoError != host_info.error ()) { Q_EMIT self_->error ("UDP server lookup failed:\n" + host_info.errorString ()); @@ -187,7 +190,13 @@ void MessageClient::impl::parse_message (QByteArray const& msg) quint8 modifiers {0}; in >> time >> snr >> delta_time >> delta_frequency >> mode >> message >> low_confidence >> modifiers; - TRACE_UDP ("Reply: time:" << time << "snr:" << snr << "dt:" << delta_time << "df:" << delta_frequency << "mode:" << mode << "message:" << message << "low confidence:" << low_confidence << "modifiers: 0x" << hex << modifiers); + TRACE_UDP ("Reply: time:" << time << "snr:" << snr << "dt:" << delta_time << "df:" << delta_frequency << "mode:" << mode << "message:" << message << "low confidence:" << low_confidence << "modifiers: 0x" +#if QT_VERSION >= QT_VERSION_CHECK (5, 15, 0) + << Qt::hex +#else + << hex +#endif + << modifiers); if (check_status (in) != Fail) { Q_EMIT self_->reply (time, snr, delta_time, delta_frequency @@ -423,30 +432,24 @@ MessageClient::MessageClient (QString const& id, QString const& version, QString : QObject {self} , m_ {id, version, revision, server_port, this} { + connect (&*m_ #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) - connect (&*m_, static_cast (&impl::error) - , [this] (impl::SocketError e) - { + , static_cast (&impl::error), [this] (impl::SocketError e) +#else + , &impl::errorOccurred, [this] (impl::SocketError e) +#endif + { #if defined (Q_OS_WIN) - if (e != impl::NetworkError // take this out when Qt 5.5 - // stops doing this - // spuriously - && e != impl::ConnectionRefusedError) // not - // interested - // in this with - // UDP socket + if (e != impl::NetworkError // take this out when Qt 5.5 stops doing this spuriously + && e != impl::ConnectionRefusedError) // not interested in this with UDP socket + { #else - Q_UNUSED (e); + { + Q_UNUSED (e); #endif - { - Q_EMIT error (m_->errorString ()); - } - }); -#else - connect (&*m_, &impl::errorOccurred, [this] (impl::SocketError) { Q_EMIT error (m_->errorString ()); - }); -#endif + } + }); set_server (server); } @@ -464,11 +467,15 @@ void MessageClient::set_server (QString const& server) { m_->server_.clear (); m_->server_string_ = server; - if (!server.isEmpty ()) + if (server.size ()) { // queue a host address lookup TRACE_UDP ("server host DNS lookup:" << server); - QHostInfo::lookupHost (server, &*m_, SLOT (host_info_results (QHostInfo))); +#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0) + m_->dns_lookup_id_ = QHostInfo::lookupHost (server, &*m_, &MessageClient::impl::host_info_results); +#else + m_->dns_lookup_id_ = QHostInfo::lookupHost (server, &*m_, SLOT (host_info_results (QHostInfo))); +#endif } } @@ -477,27 +484,6 @@ void MessageClient::set_server_port (port_type server_port) m_->server_port_ = server_port; } -qint64 MessageClient::send_raw_datagram (QByteArray const& message, QHostAddress const& dest_address - , port_type dest_port) -{ - if (dest_port && !dest_address.isNull ()) - { - return m_->writeDatagram (message, dest_address, dest_port); - } - return 0; -} - -void MessageClient::add_blocked_destination (QHostAddress const& a) -{ - m_->blocked_addresses_.push_back (a); - if (a == m_->server_) - { - m_->server_.clear (); - Q_EMIT error ("UDP server blocked, please try another"); - m_->pending_messages_.clear (); // discard - } -} - void MessageClient::enable (bool flag) { m_->enabled_ = flag; @@ -573,7 +559,7 @@ void MessageClient::qso_logged (QDateTime time_off, QString const& dx_call, QStr , QString const& comments, QString const& name, QDateTime time_on , QString const& operator_call, QString const& my_call , QString const& my_grid, QString const& exchange_sent - , QString const& exchange_rcvd) + , QString const& exchange_rcvd, QString const& propmode) { if (m_->server_port_ && !m_->server_string_.isEmpty ()) { @@ -582,8 +568,8 @@ void MessageClient::qso_logged (QDateTime time_off, QString const& dx_call, QStr out << time_off << dx_call.toUtf8 () << dx_grid.toUtf8 () << dial_frequency << mode.toUtf8 () << report_sent.toUtf8 () << report_received.toUtf8 () << tx_power.toUtf8 () << comments.toUtf8 () << name.toUtf8 () << time_on << operator_call.toUtf8 () << my_call.toUtf8 () << my_grid.toUtf8 () - << exchange_sent.toUtf8 () << exchange_rcvd.toUtf8 (); - TRACE_UDP ("time off:" << time_off << "DX:" << dx_call << "DX grid:" << dx_grid << "dial:" << dial_frequency << "mode:" << mode << "sent:" << report_sent << "rcvd:" << report_received << "pwr:" << tx_power << "comments:" << comments << "name:" << name << "time on:" << time_on << "op:" << operator_call << "DE:" << my_call << "DE grid:" << my_grid << "exch sent:" << exchange_sent << "exch rcvd:" << exchange_rcvd); + << exchange_sent.toUtf8 () << exchange_rcvd.toUtf8 () << propmode.toUtf8 (); + TRACE_UDP ("time off:" << time_off << "DX:" << dx_call << "DX grid:" << dx_grid << "dial:" << dial_frequency << "mode:" << mode << "sent:" << report_sent << "rcvd:" << report_received << "pwr:" << tx_power << "comments:" << comments << "name:" << name << "time on:" << time_on << "op:" << operator_call << "DE:" << my_call << "DE grid:" << my_grid << "exch sent:" << exchange_sent << "exch rcvd:" << exchange_rcvd << "prop_mode:" << propmode); m_->send_message (out, message); } } diff --git a/Network/MessageClient.hpp b/Network/MessageClient.hpp index 20f39b019..afe361fab 100644 --- a/Network/MessageClient.hpp +++ b/Network/MessageClient.hpp @@ -69,21 +69,13 @@ public: , QString const& report_received, QString const& tx_power, QString const& comments , QString const& name, QDateTime time_on, QString const& operator_call , QString const& my_call, QString const& my_grid - , QString const& exchange_sent, QString const& exchange_rcvd); + , QString const& exchange_sent, QString const& exchange_rcvd + , QString const& propmode); // ADIF_record argument should be valid ADIF excluding any end // of record marker Q_SLOT void logged_ADIF (QByteArray const& ADIF_record); - // this may be used to send arbitrary UDP datagrams to and - // destination allowing the underlying socket to be used for general - // UDP messaging if desired - qint64 send_raw_datagram (QByteArray const&, QHostAddress const& dest_address, port_type dest_port); - - // disallowed message destination (does not block datagrams sent - // with send_raw_datagram() above) - Q_SLOT void add_blocked_destination (QHostAddress const&); - // this signal is emitted if the server has requested a decode // window clear action Q_SIGNAL void clear_decodes (quint8 window); diff --git a/Network/NetworkAccessManager.cpp b/Network/NetworkAccessManager.cpp new file mode 100644 index 000000000..e0b06be71 --- /dev/null +++ b/Network/NetworkAccessManager.cpp @@ -0,0 +1,62 @@ +#include "Network/NetworkAccessManager.hpp" + +#include +#include + +#include "moc_NetworkAccessManager.cpp" + +NetworkAccessManager::NetworkAccessManager (QWidget * parent) + : QNetworkAccessManager (parent) + , parent_widget_ {parent} +{ + // handle SSL errors that have not been cached as allowed + // exceptions and offer them to the user to add to the ignored + // exception cache + connect (this, &QNetworkAccessManager::sslErrors, this, &NetworkAccessManager::filter_SSL_errors); +} + +void NetworkAccessManager::filter_SSL_errors (QNetworkReply * reply, QList const& errors) +{ + QString message; + QList new_errors; + for (auto const& error: errors) + { + if (!allowed_ssl_errors_.contains (error)) + { + new_errors << error; + message += '\n' + reply->request ().url ().toDisplayString () + ": " + error.errorString (); + } + } + if (new_errors.size ()) + { + QString certs; + for (auto const& cert : reply->sslConfiguration ().peerCertificateChain ()) + { + certs += cert.toText () + '\n'; + } + if (MessageBox::Ignore == MessageBox::query_message (parent_widget_ + , tr ("Network SSL/TLS Errors") + , message, certs + , MessageBox::Abort | MessageBox::Ignore)) + { + // accumulate new SSL error exceptions that have been allowed + allowed_ssl_errors_.append (new_errors); + reply->ignoreSslErrors (allowed_ssl_errors_); + } + } + else + { + // no new exceptions so silently ignore the ones already allowed + reply->ignoreSslErrors (allowed_ssl_errors_); + } +} + +QNetworkReply * NetworkAccessManager::createRequest (Operation operation, QNetworkRequest const& request + , QIODevice * outgoing_data) +{ + auto reply = QNetworkAccessManager::createRequest (operation, request, outgoing_data); + // errors are usually certificate specific so passing all cached + // exceptions here is ok + reply->ignoreSslErrors (allowed_ssl_errors_); + return reply; +} diff --git a/Network/NetworkAccessManager.hpp b/Network/NetworkAccessManager.hpp index 1d1b79bce..d68ad2153 100644 --- a/Network/NetworkAccessManager.hpp +++ b/Network/NetworkAccessManager.hpp @@ -4,8 +4,6 @@ #include #include #include -#include -#include #include "widgets/MessageBox.hpp" @@ -18,58 +16,18 @@ class QWidget; class NetworkAccessManager : public QNetworkAccessManager { + Q_OBJECT + public: - NetworkAccessManager (QWidget * parent) - : QNetworkAccessManager (parent) - { - // handle SSL errors that have not been cached as allowed - // exceptions and offer them to the user to add to the ignored - // exception cache - connect (this, &QNetworkAccessManager::sslErrors, [this, &parent] (QNetworkReply * reply, QList const& errors) { - QString message; - QList new_errors; - for (auto const& error: errors) - { - if (!allowed_ssl_errors_.contains (error)) - { - new_errors << error; - message += '\n' + reply->request ().url ().toDisplayString () + ": " - + error.errorString (); - } - } - if (new_errors.size ()) - { - QString certs; - for (auto const& cert : reply->sslConfiguration ().peerCertificateChain ()) - { - certs += cert.toText () + '\n'; - } - if (MessageBox::Ignore == MessageBox::query_message (parent, tr ("Network SSL Errors"), message, certs, MessageBox::Abort | MessageBox::Ignore)) - { - // accumulate new SSL error exceptions that have been allowed - allowed_ssl_errors_.append (new_errors); - reply->ignoreSslErrors (allowed_ssl_errors_); - } - } - else - { - // no new exceptions so silently ignore the ones already allowed - reply->ignoreSslErrors (allowed_ssl_errors_); - } - }); - } + explicit NetworkAccessManager (QWidget * parent); protected: - QNetworkReply * createRequest (Operation operation, QNetworkRequest const& request, QIODevice * outgoing_data = nullptr) override - { - auto reply = QNetworkAccessManager::createRequest (operation, request, outgoing_data); - // errors are usually certificate specific so passing all cached - // exceptions here is ok - reply->ignoreSslErrors (allowed_ssl_errors_); - return reply; - } + QNetworkReply * createRequest (Operation, QNetworkRequest const&, QIODevice * = nullptr) override; private: + void filter_SSL_errors (QNetworkReply * reply, QList const& errors); + + QWidget * parent_widget_; QList allowed_ssl_errors_; }; diff --git a/Network/NetworkMessage.hpp b/Network/NetworkMessage.hpp index 3c5c9268f..c484efb23 100644 --- a/Network/NetworkMessage.hpp +++ b/Network/NetworkMessage.hpp @@ -308,6 +308,7 @@ * My grid utf8 * Exchange sent utf8 * Exchange received utf8 + * ADIF Propagation mode utf8 * * The QSO logged message is sent to the server(s) when the * WSJT-X user accepts the "Log QSO" dialog by clicking the "OK" diff --git a/Network/PSKReporter.cpp b/Network/PSKReporter.cpp new file mode 100644 index 000000000..fcdebf2c5 --- /dev/null +++ b/Network/PSKReporter.cpp @@ -0,0 +1,521 @@ +#include "PSKReporter.hpp" + +// Interface for posting spots to PSK Reporter web site +// Implemented by Edson Pereira PY2SDR +// Updated by Bill Somerville, G4WJS +// +// Reports will be sent in batch mode every 5 minutes. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) +#include +#endif + +#include "Configuration.hpp" +#include "pimpl_impl.hpp" + + +#include "moc_PSKReporter.cpp" + +namespace +{ + QLatin1String HOST {"report.pskreporter.info"}; + // QLatin1String HOST {"127.0.0.1"}; + quint16 SERVICE_PORT {4739}; + // quint16 SERVICE_PORT {14739}; + int MIN_SEND_INTERVAL {15}; // in seconds + int FLUSH_INTERVAL {4 * 5}; // in send intervals + bool ALIGNMENT_PADDING {true}; + int MIN_PAYLOAD_LENGTH {508}; + int MAX_PAYLOAD_LENGTH {1400}; +} + +class PSKReporter::impl final + : public QObject +{ + Q_OBJECT + +public: + impl (PSKReporter * self, Configuration const * config, QString const& program_info) + : self_ {self} + , config_ {config} + , sequence_number_ {0u} + , send_descriptors_ {0} + , send_receiver_data_ {0} + , flush_counter_ {0u} + , prog_id_ {program_info} + { +#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) + observation_id_ = qrand(); +#else + observation_id_ = QRandomGenerator::global ()->generate (); +#endif + + // This timer sets the interval to check for spots to send. + connect (&report_timer_, &QTimer::timeout, [this] () {send_report ();}); + + // This timer repeats the sending of IPFIX templates and receiver + // information if we are using UDP, in case server has been + // restarted ans lost cached information. + connect (&descriptor_timer_, &QTimer::timeout, [this] () { + if (socket_ + && QAbstractSocket::UdpSocket == socket_->socketType ()) + { + // send templates again + send_descriptors_ = 3; // three times + // send receiver data set again + send_receiver_data_ = 3; // three times + } + }); + } + + void check_connection () + { + if (!socket_ + || QAbstractSocket::UnconnectedState == socket_->state () + || (socket_->socketType () != config_->psk_reporter_tcpip () ? QAbstractSocket::TcpSocket : QAbstractSocket::UdpSocket)) + { + // we need to create the appropriate socket + if (socket_ + && QAbstractSocket::UnconnectedState != socket_->state () + && QAbstractSocket::ClosingState != socket_->state ()) + { + // handle re-opening asynchronously + auto connection = QSharedPointer::create (); + *connection = connect (socket_.data (), &QAbstractSocket::disconnected, [this, connection] () { + disconnect (*connection); + check_connection (); + }); + // close gracefully + send_report (true); + socket_->close (); + } + else + { + reconnect (); + } + } + } + + void handle_socket_error (QAbstractSocket::SocketError e) + { + switch (e) + { + case QAbstractSocket::RemoteHostClosedError: + socket_->disconnectFromHost (); + break; + + case QAbstractSocket::TemporaryError: + break; + + default: + spots_.clear (); + Q_EMIT self_->errorOccurred (socket_->errorString ()); + break; + } + } + + void reconnect () + { + // Using deleteLater for the deleter as we may eventually + // be called from the disconnected handler above. + if (config_->psk_reporter_tcpip ()) + { + socket_.reset (new QTcpSocket, &QObject::deleteLater); + send_descriptors_ = 1; + send_receiver_data_ = 1; + } + else + { + socket_.reset (new QUdpSocket, &QObject::deleteLater); + send_descriptors_ = 3; + send_receiver_data_ = 3; + } + +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + connect (socket_.get (), &QAbstractSocket::errorOccurred, this, &PSKReporter::impl::handle_socket_error); +#else + connect (socket_.data (), QOverload::of (&QAbstractSocket::error), this, &PSKReporter::impl::handle_socket_error); +#endif + + // use this for pseudo connection with UDP, allows us to use + // QIODevice::write() instead of QUDPSocket::writeDatagram() + socket_->connectToHost (HOST, SERVICE_PORT, QAbstractSocket::WriteOnly); + + if (!report_timer_.isActive ()) + { + report_timer_.start (MIN_SEND_INTERVAL * 1000); + } + if (!descriptor_timer_.isActive ()) + { + descriptor_timer_.start (1 * 60 * 60 * 1000); // hourly + } + } + + void stop () + { + if (socket_) + { + socket_->disconnectFromHost (); + } + descriptor_timer_.stop (); + report_timer_.stop (); + } + + void send_report (bool send_residue = false); + void build_preamble (QDataStream&); + + bool flushing () + { + return FLUSH_INTERVAL && !(++flush_counter_ % FLUSH_INTERVAL); + } + + PSKReporter * self_; + Configuration const * config_; + QSharedPointer socket_; + int dns_lookup_id_; + QByteArray payload_; + quint32 sequence_number_; + int send_descriptors_; + + // Currently PSK Reporter requires that a receiver data set is sent + // in every data flow. This memeber variable can be used to only + // send that information at session start (3 times for UDP), when it + // changes (3 times for UDP), or once per hour (3 times) if using + // UDP. Uncomment the relevant code to enable that fuctionality. + int send_receiver_data_; + + unsigned flush_counter_; + quint32 observation_id_; + QString rx_call_; + QString rx_grid_; + QString rx_ant_; + QString prog_id_; + QByteArray tx_data_; + QByteArray tx_residue_; + struct Spot + { + bool operator == (Spot const& rhs) + { + return + call_ == rhs.call_ + && grid_ == rhs.grid_ + && mode_ == rhs.mode_ + && std::abs (Radio::FrequencyDelta (freq_ - rhs.freq_)) < 50; + } + + QString call_; + QString grid_; + int snr_; + Radio::Frequency freq_; + QString mode_; + QDateTime time_; + }; + QQueue spots_; + QTimer report_timer_; + QTimer descriptor_timer_; +}; + +#include "PSKReporter.moc" + +namespace +{ + void writeUtfString (QDataStream& out, QString const& s) + { + auto const& utf = s.toUtf8 ().left (254); + out << quint8 (utf.size ()); + out.writeRawData (utf, utf.size ()); + } + + int num_pad_bytes (int len) + { + return ALIGNMENT_PADDING ? (4 - len % 4) % 4 : 0; + } + + void set_length (QDataStream& out, QByteArray& b) + { + // pad with nulls modulo 4 + auto pad_len = num_pad_bytes (b.size ()); + out.writeRawData (QByteArray {pad_len, '\0'}.constData (), pad_len); + auto pos = out.device ()->pos (); + out.device ()->seek (sizeof (quint16)); + // insert length + out << static_cast (b.size ()); + out.device ()->seek (pos); + } +} + +void PSKReporter::impl::build_preamble (QDataStream& message) +{ + // Message Header + message + << quint16 (10u) // Version Number + << quint16 (0u) // Length (place-holder filled in later) + << quint32 (0u) // Export Time (place-holder filled in later) + << ++sequence_number_ // Sequence Number + << observation_id_; // Observation Domain ID + + if (send_descriptors_) + { + --send_descriptors_; + { + // Sender Information descriptor + QByteArray descriptor; + QDataStream out {&descriptor, QIODevice::WriteOnly}; + out + << quint16 (2u) // Template Set ID + << quint16 (0u) // Length (place-holder) + << quint16 (0x50e3) // Link ID + << quint16 (7u) // Field Count + << quint16 (0x8000 + 1u) // Option 1 Information Element ID (senderCallsign) + << quint16 (0xffff) // Option 1 Field Length (variable) + << quint32 (30351u) // Option 1 Enterprise Number + << quint16 (0x8000 + 5u) // Option 2 Information Element ID (frequency) + << quint16 (4u) // Option 2 Field Length + << quint32 (30351u) // Option 2 Enterprise Number + << quint16 (0x8000 + 6u) // Option 3 Information Element ID (sNR) + << quint16 (1u) // Option 3 Field Length + << quint32 (30351u) // Option 3 Enterprise Number + << quint16 (0x8000 + 10u) // Option 4 Information Element ID (mode) + << quint16 (0xffff) // Option 4 Field Length (variable) + << quint32 (30351u) // Option 4 Enterprise Number + << quint16 (0x8000 + 3u) // Option 5 Information Element ID (senderLocator) + << quint16 (0xffff) // Option 5 Field Length (variable) + << quint32 (30351u) // Option 5 Enterprise Number + << quint16 (0x8000 + 11u) // Option 6 Information Element ID (informationSource) + << quint16 (1u) // Option 6 Field Length + << quint32 (30351u) // Option 6 Enterprise Number + << quint16 (150u) // Option 7 Information Element ID (dateTimeSeconds) + << quint16 (4u); // Option 7 Field Length + // insert Length and move to payload + set_length (out, descriptor); + message.writeRawData (descriptor.constData (), descriptor.size ()); + } + { + // Receiver Information descriptor + QByteArray descriptor; + QDataStream out {&descriptor, QIODevice::WriteOnly}; + out + << quint16 (3u) // Options Template Set ID + << quint16 (0u) // Length (place-holder) + << quint16 (0x50e2) // Link ID + << quint16 (4u) // Field Count + << quint16 (0u) // Scope Field Count + << quint16 (0x8000 + 2u) // Option 1 Information Element ID (receiverCallsign) + << quint16 (0xffff) // Option 1 Field Length (variable) + << quint32 (30351u) // Option 1 Enterprise Number + << quint16 (0x8000 + 4u) // Option 2 Information Element ID (receiverLocator) + << quint16 (0xffff) // Option 2 Field Length (variable) + << quint32 (30351u) // Option 2 Enterprise Number + << quint16 (0x8000 + 8u) // Option 3 Information Element ID (decodingSoftware) + << quint16 (0xffff) // Option 3 Field Length (variable) + << quint32 (30351u) // Option 3 Enterprise Number + << quint16 (0x8000 + 9u) // Option 4 Information Element ID (antennaInformation) + << quint16 (0xffff) // Option 4 Field Length (variable) + << quint32 (30351u); // Option 4 Enterprise Number + // insert Length + set_length (out, descriptor); + message.writeRawData (descriptor.constData (), descriptor.size ()); + } + } + + // if (send_receiver_data_) + { + // --send_receiver_data_; + + // Receiver information + QByteArray data; + QDataStream out {&data, QIODevice::WriteOnly}; + + // Set Header + out + << quint16 (0x50e2) // Template ID + << quint16 (0u); // Length (place-holder) + + // Set data + writeUtfString (out, rx_call_); + writeUtfString (out, rx_grid_); + writeUtfString (out, prog_id_); + writeUtfString (out, rx_ant_); + + // insert Length and move to payload + set_length (out, data); + message.writeRawData (data.constData (), data.size ()); + } +} + +void PSKReporter::impl::send_report (bool send_residue) +{ + if (QAbstractSocket::ConnectedState != socket_->state ()) return; + + QDataStream message {&payload_, QIODevice::WriteOnly | QIODevice::Append}; + QDataStream tx_out {&tx_data_, QIODevice::WriteOnly | QIODevice::Append}; + + if (!payload_.size ()) + { + // Build header, optional descriptors, and receiver information + build_preamble (message); + } + + auto flush = flushing () || send_residue; + while (spots_.size () || flush) + { + if (!payload_.size ()) + { + // Build header, optional descriptors, and receiver information + build_preamble (message); + } + + if (!tx_data_.size () && (spots_.size () || tx_residue_.size ())) + { + // Set Header + tx_out + << quint16 (0x50e3) // Template ID + << quint16 (0u); // Length (place-holder) + } + + // insert any residue + if (tx_residue_.size ()) + { + tx_out.writeRawData (tx_residue_.constData (), tx_residue_.size ()); + tx_residue_.clear (); + } + + while (spots_.size () || flush) + { + auto tx_data_size = tx_data_.size (); + if (spots_.size ()) + { + auto const& spot = spots_.dequeue (); + + // Sender information + writeUtfString (tx_out, spot.call_); + tx_out + << static_cast (spot.freq_) + << static_cast (spot.snr_); + writeUtfString (tx_out, spot.mode_); + writeUtfString (tx_out, spot.grid_); + tx_out + << quint8 (1u) // REPORTER_SOURCE_AUTOMATIC + << static_cast ( +#if QT_VERSION >= QT_VERSION_CHECK(5, 8, 0) + spot.time_.toSecsSinceEpoch () +#else + spot.time_.toMSecsSinceEpoch () / 1000 +#endif + ); + } + + auto len = payload_.size () + tx_data_.size (); + len += num_pad_bytes (tx_data_.size ()); + len += num_pad_bytes (len); + if (len > MAX_PAYLOAD_LENGTH // our upper datagram size limit + || (!spots_.size () && len > MIN_PAYLOAD_LENGTH) // spots drained and above lower datagram size limit + || (flush && !spots_.size ())) // send what we have, possibly no spots + { + if (tx_data_.size ()) + { + if (len <= MAX_PAYLOAD_LENGTH) + { + tx_data_size = tx_data_.size (); + } + QByteArray tx {tx_data_.left (tx_data_size)}; + QDataStream out {&tx, QIODevice::WriteOnly | QIODevice::Append}; + // insert Length + set_length (out, tx); + message.writeRawData (tx.constData (), tx.size ()); + } + + // insert Length and Export Time + set_length (message, payload_); + message.device ()->seek (2 * sizeof (quint16)); + message << static_cast ( +#if QT_VERSION >= QT_VERSION_CHECK(5, 8, 0) + QDateTime::currentDateTime ().toSecsSinceEpoch () +#else + QDateTime::currentDateTime ().toMSecsSinceEpoch () / 1000 +#endif + ); + + // Send data to PSK Reporter site + socket_->write (payload_); // TODO: handle errors + flush = false; // break loop + message.device ()->seek (0u); + payload_.clear (); // Fresh message + // Save unsent spots + tx_residue_ = tx_data_.right (tx_data_.size () - tx_data_size); + tx_out.device ()->seek (0u); + tx_data_.clear (); + break; + } + } + } +} + +PSKReporter::PSKReporter (Configuration const * config, QString const& program_info) + : m_ {this, config, program_info} +{ +} + +PSKReporter::~PSKReporter () +{ + // m_->send_report (true); // send any pending spots +} + +void PSKReporter::reconnect () +{ + m_->reconnect (); +} + +void PSKReporter::setLocalStation (QString const& call, QString const& gridSquare, QString const& antenna) +{ + m_->check_connection (); + if (call != m_->rx_call_ || gridSquare != m_->rx_grid_ || antenna != m_->rx_ant_) + { + m_->send_receiver_data_ = m_->socket_ + && QAbstractSocket::UdpSocket == m_->socket_->socketType () ? 3 : 1; + m_->rx_call_ = call; + m_->rx_grid_ = gridSquare; + m_->rx_ant_ = antenna; + } +} + +bool PSKReporter::addRemoteStation (QString const& call, QString const& grid, Radio::Frequency freq + , QString const& mode, int snr) +{ + m_->check_connection (); + if (m_->socket_ && m_->socket_->isValid ()) + { + if (QAbstractSocket::UnconnectedState == m_->socket_->state ()) + { + reconnect (); + } + m_->spots_.enqueue ({call, grid, snr, freq, mode, QDateTime::currentDateTimeUtc ()}); + return true; + } + return false; +} + +void PSKReporter::sendReport (bool last) +{ + m_->check_connection (); + if (m_->socket_ && QAbstractSocket::ConnectedState == m_->socket_->state ()) + { + m_->send_report (true); + } + if (last) + { + m_->stop (); + } +} diff --git a/Network/PSKReporter.hpp b/Network/PSKReporter.hpp new file mode 100644 index 000000000..1757e63af --- /dev/null +++ b/Network/PSKReporter.hpp @@ -0,0 +1,41 @@ +#ifndef PSK_REPORTER_HPP_ +#define PSK_REPORTER_HPP_ + +#include +#include "Radio.hpp" +#include "pimpl_h.hpp" + +class QString; +class Configuration; + +class PSKReporter final + : public QObject +{ + Q_OBJECT + +public: + explicit PSKReporter (Configuration const *, QString const& program_info); + ~PSKReporter (); + + void reconnect (); + + void setLocalStation (QString const& call, QString const& grid, QString const& antenna); + + // + // Returns false if PSK Reporter connection is not available + // + bool addRemoteStation (QString const& call, QString const& grid, Radio::Frequency freq, QString const& mode, int snr); + + // + // Flush any pending spots to PSK Reporter + // + void sendReport (bool last = false); + + Q_SIGNAL void errorOccurred (QString const& reason); + +private: + class impl; + pimpl m_; +}; + +#endif diff --git a/Network/psk_reporter.cpp b/Network/psk_reporter.cpp deleted file mode 100644 index f84da5ecd..000000000 --- a/Network/psk_reporter.cpp +++ /dev/null @@ -1,140 +0,0 @@ -// KISS Interface for posting spots to PSK Reporter web site -// Implemented by Edson Pereira PY2SDR -// -// Reports will be sent in batch mode every 5 minutes. - -#include "psk_reporter.h" - -#include -#include -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) -#include -#endif - -#include "Network/MessageClient.hpp" - -#include "moc_psk_reporter.cpp" - -namespace -{ - int constexpr MAX_PAYLOAD_LENGTH {1400}; -} - -PSK_Reporter::PSK_Reporter(MessageClient * message_client, QObject *parent) : - QObject {parent}, - m_messageClient {message_client}, - reportTimer {new QTimer {this}}, - m_sequenceNumber {0} -{ - m_header_h = "000Allllttttttttssssssssiiiiiiii"; - - // We use 50E2 and 50E3 for link Id - m_rxInfoDescriptor_h = "0003002C50E200040000" - "8002FFFF0000768F" // 2. Rx Call - "8004FFFF0000768F" // 4. Rx Grid - "8008FFFF0000768F" // 8. Rx Soft - "8009FFFF0000768F" // 9. Rx Antenna - "0000"; - - m_txInfoDescriptor_h = "0002003C50E30007" - "8001FFFF0000768F" // 1. Tx Call - "800500040000768F" // 5. Tx Freq - "800600010000768F" // 6. Tx snr - "800AFFFF0000768F" // 10. Tx Mode - "8003FFFF0000768F" // 3. Tx Grid - "800B00010000768F" // 11. Tx info src - "00960004"; // Report time - - -#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) - m_randomId_h = QString("%1").arg(qrand(),8,16,QChar('0')); -#else - m_randomId_h = QString("%1").arg(QRandomGenerator::global ()->generate (), 8, 16, QChar('0')); -#endif - - QHostInfo::lookupHost("report.pskreporter.info", this, SLOT(dnsLookupResult(QHostInfo))); - - connect(reportTimer, SIGNAL(timeout()), this, SLOT(sendReport())); - reportTimer->start(5*60*1000); // 5 minutes; -} - -void PSK_Reporter::setLocalStation(QString call, QString gridSquare, QString antenna, QString programInfo) -{ - m_rxCall = call; - m_rxGrid = gridSquare; - m_rxAnt = antenna; - m_progId = programInfo; -} - -void PSK_Reporter::addRemoteStation(QString call, QString grid, QString freq, QString mode, QString snr, QString time ) -{ - QHash spot; - spot["call"] = call; - spot["grid"] = grid; - spot["snr"] = snr; - spot["freq"] = freq; - spot["mode"] = mode; - spot["time"] = time; - m_spotQueue.enqueue(spot); -} - -void PSK_Reporter::sendReport() -{ - while (!m_spotQueue.isEmpty()) { - QString report_h; - - // Header - QString header_h = m_header_h; - header_h.replace("tttttttt", QString("%1").arg(QDateTime::currentDateTime().toTime_t(),8,16,QChar('0'))); - header_h.replace("ssssssss", QString("%1").arg(++m_sequenceNumber,8,16,QChar('0'))); - header_h.replace("iiiiiiii", m_randomId_h); - - // Receiver information - QString rxInfoData_h = "50E2llll"; - rxInfoData_h += QString("%1").arg(m_rxCall.length(),2,16,QChar('0')) + m_rxCall.toUtf8().toHex(); - rxInfoData_h += QString("%1").arg(m_rxGrid.length(),2,16,QChar('0')) + m_rxGrid.toUtf8().toHex(); - rxInfoData_h += QString("%1").arg(m_progId.length(),2,16,QChar('0')) + m_progId.toUtf8().toHex(); - rxInfoData_h += QString("%1").arg(m_rxAnt.length(),2,16,QChar('0')) + m_rxAnt.toUtf8().toHex(); - rxInfoData_h += "0000"; - rxInfoData_h.replace("50E2llll", "50E2" + QString("%1").arg(rxInfoData_h.length()/2,4,16,QChar('0'))); - - // Sender information - QString txInfoData_h = "50E3llll"; - while (!m_spotQueue.isEmpty() - && (header_h.size () + m_rxInfoDescriptor_h.size () + m_txInfoDescriptor_h.size () + rxInfoData_h.size () + txInfoData_h.size ()) / 2 < MAX_PAYLOAD_LENGTH) { - QHash spot = m_spotQueue.dequeue(); - txInfoData_h += QString("%1").arg(spot["call"].length(),2,16,QChar('0')) + spot["call"].toUtf8().toHex(); - txInfoData_h += QString("%1").arg(spot["freq"].toLongLong(),8,16,QChar('0')); - txInfoData_h += QString("%1").arg(spot["snr"].toInt(),8,16,QChar('0')).right(2); - txInfoData_h += QString("%1").arg(spot["mode"].length(),2,16,QChar('0')) + spot["mode"].toUtf8().toHex(); - txInfoData_h += QString("%1").arg(spot["grid"].length(),2,16,QChar('0')) + spot["grid"].toUtf8().toHex(); - txInfoData_h += QString("%1").arg(1,2,16,QChar('0')); // REPORTER_SOURCE_AUTOMATIC - txInfoData_h += QString("%1").arg(spot["time"].toInt(),8,16,QChar('0')); - } - txInfoData_h += "0000"; - txInfoData_h.replace("50E3llll", "50E3" + QString("%1").arg(txInfoData_h.length()/2,4,16,QChar('0'))); - report_h = header_h + m_rxInfoDescriptor_h + m_txInfoDescriptor_h + rxInfoData_h + txInfoData_h; - //qDebug() << "Sending Report TX: "; - - report_h.replace("000Allll", "000A" + QString("%1").arg(report_h.length()/2,4,16,QChar('0'))); - QByteArray report = QByteArray::fromHex(report_h.toUtf8()); - - // Send data to PSK Reporter site - if (!m_pskReporterAddress.isNull()) { - m_messageClient->send_raw_datagram (report, m_pskReporterAddress, 4739); - } - } -} - -void PSK_Reporter::dnsLookupResult(QHostInfo info) -{ - if (!info.addresses().isEmpty()) { - m_pskReporterAddress = info.addresses().at(0); - // qDebug() << "PSK Reporter IP: " << m_pskReporterAddress; - - // deal with miss-configured settings that attempt to set a - // Pskreporter Internet address for the WSJT-X UDP protocol - // server address - m_messageClient->add_blocked_destination (m_pskReporterAddress); - } -} diff --git a/Network/psk_reporter.h b/Network/psk_reporter.h deleted file mode 100644 index 16afcf437..000000000 --- a/Network/psk_reporter.h +++ /dev/null @@ -1,54 +0,0 @@ -// -*- Mode: C++ -*- -#ifndef PSK_REPORTER_H -#define PSK_REPORTER_H - -#include -#include -#include -#include -#include - -class MessageClient; -class QTimer; -class QHostInfo; - -class PSK_Reporter : public QObject -{ - Q_OBJECT -public: - explicit PSK_Reporter(MessageClient *, QObject *parent = nullptr); - void setLocalStation(QString call, QString grid, QString antenna, QString programInfo); - void addRemoteStation(QString call, QString grid, QString freq, QString mode, QString snr, QString time); - -signals: - -public slots: - void sendReport(); - -private slots: - void dnsLookupResult(QHostInfo info); - -private: - QString m_header_h; - QString m_rxInfoDescriptor_h; - QString m_txInfoDescriptor_h; - QString m_randomId_h; - QString m_linkId_h; - - QString m_rxCall; - QString m_rxGrid; - QString m_rxAnt; - QString m_progId; - - QHostAddress m_pskReporterAddress; - - QQueue< QHash > m_spotQueue; - - MessageClient * m_messageClient; - - QTimer *reportTimer; - - int m_sequenceNumber; -}; - -#endif // PSK_REPORTER_H diff --git a/Network/tests/PSKReporter/PSKReporter.IESpec b/Network/tests/PSKReporter/PSKReporter.IESpec new file mode 100644 index 000000000..8e95ffd08 --- /dev/null +++ b/Network/tests/PSKReporter/PSKReporter.IESpec @@ -0,0 +1,12 @@ +senderCallsign(30351/1)[65535] +receiverCallsign(30351/2)[65535] +senderLocator(30351/3)[65535] +receiverLocator(30351/4)[65535] +frequency(30351/5)[4] +sNR(30351/6)[1] +iMD(30351/7)[1] +decoderSoftware(30351/8)[65535] +antennaInformation(30351/9)[65535] +mode(30351/10)[65535] +informationSource(30351/11)[1] +persistentIdentifier(30351/12)[65535] diff --git a/Network/tests/PSKReporter/listener.py b/Network/tests/PSKReporter/listener.py new file mode 100644 index 000000000..b2e830e34 --- /dev/null +++ b/Network/tests/PSKReporter/listener.py @@ -0,0 +1,94 @@ +from __future__ import unicode_literals +import sys +import argparse +import logging +import time +import threading +import ipfix.ie +import ipfix.reader +import ipfix.message +import socketserver +import socket + +class IPFixDatagramHandler (socketserver.DatagramRequestHandler): + + def handle (self): + logging.info (f'Connection from {self.client_address}') + try: + self.server.msg_buffer.from_bytes (self.packet) + for rec in self.server.msg_buffer.namedict_iterator (): + logging.info (f't: {self.server.msg_buffer.get_export_time()}: {rec}') + except: + logging.error ('Unexpected exception:', sys.exc_info ()[0]) + + +class IPFixUdpServer (socketserver.UDPServer): + + def __init__ (self, *args, **kwargs): + + self.msg_buffer = ipfix.message.MessageBuffer () + super ().__init__ (*args, **kwargs) + +class IPFixStreamHandler (socketserver.StreamRequestHandler): + + def handle (self): + logging.info (f'Connection from {self.client_address}') + try: + msg_reader = ipfix.reader.from_stream (self.rfile) + for rec in msg_reader.namedict_iterator (): + logging.info (f't: {msg_reader.msg.get_export_time()}: {rec}') + logging.info (f'{self.client_address} closed their connection') + except ConnectionResetError: + logging.info (f'{self.client_address} connection reset') + except TimeoutError: + logging.info (f'{self.client_address} connection timed out') + except: + logging.error ('Unexpected exception:', sys.exc_info ()[0]) + + +if __name__ == "__main__": + INTERFACE, PORT = '', 4739 + + ap = argparse.ArgumentParser (description='Dump IPFIX data collectedover UDP') + ap.add_argument ('-l', '--log', metavar='loglevel', default='WARNING', help='logging level') + ap.add_argument ('-s', '--spec', metavar='specfile', help='iespec file to read') + args = ap.parse_args () + + log_level = getattr (logging, args.log.upper (), None) + if not isinstance (log_level, int): + raise ValueError (f'Invalif log level: {log_level}') + logging.basicConfig ( + level=log_level + , format='[%(levelname)s] (%(threadName)-10s) %(message)s' + , + ) + + logging.info (f'Starting IPFix servers on service port: {PORT}') + + ipfix.ie.use_iana_default () + ipfix.ie.use_5103_default () + if args.spec: + ipfix.ie.use_specfile (args.spec) + + udp_server = IPFixUdpServer ((INTERFACE, PORT), IPFixDatagramHandler) + udp_thread = threading.Thread (name='UDP Server', target=udp_server.serve_forever) + + tcp_server = socketserver.TCPServer ((INTERFACE, PORT), IPFixStreamHandler) + tcp_server.allow_reuse_address = True + tcp_server.socket.setsockopt (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + tcp_thread = threading.Thread (name='TCP/IP Server', target=tcp_server.serve_forever) + + udp_thread.start () + tcp_thread.start () + + try: + while True: + time.sleep (1000) + except KeyboardInterrupt: + logging.warning ('Closing down servers') + udp_server.shutdown () + tcp_server.shutdown () + + udp_thread.join () + tcp_thread.join () + logging.info ('Servers closed') diff --git a/Network/tests/PSKReporter/python-ipfix.patch b/Network/tests/PSKReporter/python-ipfix.patch new file mode 100644 index 000000000..cb6797094 --- /dev/null +++ b/Network/tests/PSKReporter/python-ipfix.patch @@ -0,0 +1,64 @@ +The following changes since commit e487dfbead9965c1a251d110dc55039a7ba4afef: + + sphinx doc update (2017-09-20 16:07:22 +0200) + +are available in the Git repository at: + + git@github.com:g4wjs/python-ipfix.git varlen-and-padding + +for you to fetch changes up to 6dcc106f22d25ac21b27f8ccb9b82be12e7eb18e: + + Parse and emit data elements correctly when variable length items included (2020-06-19 19:53:33 +0100) + +---------------------------------------------------------------- +Bill Somerville (2): + Allow for alignment padding when parsing sets + Parse and emit data elements correctly when variable length items included + + ipfix/message.py | 3 ++- + ipfix/template.py | 4 ++-- + 2 files changed, 4 insertions(+), 3 deletions(-) + +diff --git a/ipfix/message.py b/ipfix/message.py +index 0af2fad..71c1328 100644 +--- a/ipfix/message.py ++++ b/ipfix/message.py +@@ -398,7 +398,7 @@ class MessageBuffer(object): + offset += _sethdr_st.size # skip set header in decode + if setid == template.TEMPLATE_SET_ID or\ + setid == template.OPTIONS_SET_ID: +- while offset < setend: ++ while offset + 4 < setend: # allow for padding up to 4 byte alignment + (tmpl, offset) = template.decode_template_from( + self.mbuf, offset, setid) + # FIXME handle withdrawal +@@ -431,6 +431,7 @@ class MessageBuffer(object): + # KeyError on template lookup - unknown data set + self.unknown_data_set_hook(self, + self.mbuf[offset-_sethdr_st.size:setend]) ++ offset = setend # real end may be greater that accumulated offset + + def namedict_iterator(self): + """ +diff --git a/ipfix/template.py b/ipfix/template.py +index 1203e55..26cd8c1 100644 +--- a/ipfix/template.py ++++ b/ipfix/template.py +@@ -187,7 +187,7 @@ class Template(object): + offset += packplan.st.size + + # short circuit on no varlen +- if not self.varlenslice: ++ if self.varlenslice is None: + return (vals, offset) + + # direct iteration over remaining IEs +@@ -239,7 +239,7 @@ class Template(object): + offset += packplan.st.size + + # shortcircuit no varlen +- if not self.varlenslice: ++ if self.varlenslice is None: + return offset + + # direct iteration over remaining IEs diff --git a/Network/wsprnet.cpp b/Network/wsprnet.cpp index cdfed7683..669ab1554 100644 --- a/Network/wsprnet.cpp +++ b/Network/wsprnet.cpp @@ -8,6 +8,8 @@ #include #include +#include +#include #include #include #include @@ -18,215 +20,314 @@ namespace { - char const * const wsprNetUrl = "http://wsprnet.org/post?"; - // char const * const wsprNetUrl = "http://127.0.0.1/post?"; + char const * const wsprNetUrl = "http://wsprnet.org/post/"; + //char const * const wsprNetUrl = "http://127.0.0.1:5000/post/"; + + // + // tested with this python REST mock of WSPRNet.org + // + /* +# Mock WSPRNet.org RESTful API +from flask import Flask, request, url_for +from flask_restful import Resource, Api + +app = Flask(__name__) + +@app.route ('/post/', methods=['GET', 'POST']) +def spot (): + if request.method == 'POST': + print (request.form) + return "1 spot(s) added" + +with app.test_request_context (): + print (url_for ('spot')) + */ + + // regexp to parse FST4W decodes + QRegularExpression fst4_re {R"( + (?