Compare commits

...

No commits in common. "2.6.5" and "master" have entirely different histories.

1595 changed files with 349126 additions and 36 deletions

6
.gitattributes vendored Normal file
View File

@ -0,0 +1,6 @@
.gitattributes export-ignore
/samples export-ignore
/lib/fsk4hf export-ignore
/lib/fsk4hf export-ignore
/robots export-ignore
/plots export-ignore

60
.gitignore vendored
View File

@ -1,37 +1,25 @@
*.pyc
dist/
build/
docs/
*.egg-info/
__pycache__
*.tar
*.tar.*
*.log
*.log.*
*.sig
pkg/
src/
# stdeb files
*.tar.gz
deb_dist
/gpg_key
# gedit temp files
~*
TAGS
tags
GPATH
GRTAGS
GTAGS
*~
# hackedit project files
.hackedit
# vs code project files
.vscode
.mypy_cache
.cache
.env
./.idea
/.idea/
.tox
junk*
jnq*
*.exe
*.o
*.mod
*.pro.user
*.txt
*.bak
!**/CMakeLists.txt
!**/*.txt
__pycache__
cmake-build-debug
cmake-build-release
CMakeFiles
fnd
lib/77bit/tmp
lib/tmp
lib/ftrsd

4
AUTHORS Normal file
View File

@ -0,0 +1,4 @@
Joe Taylor, K1JT <k1jt@arrl.net>
See also about.cpp or "Help->About WSJT-X" in the application for
details of other contributions.

43
AppVersion/AppVersion.cpp Normal file
View File

@ -0,0 +1,43 @@
//
// wsjtx_app_version - a console application that outputs WSJT-X
// application version
//
// This application is only provided as a simple console application
//
//
#include <cstdlib>
#include <iostream>
#include <exception>
#include <QCoreApplication>
#include <QCommandLineParser>
#include <QCommandLineOption>
#include "revision_utils.hpp"
int main (int argc, char * argv[])
{
QCoreApplication app {argc, argv};
try
{
app.setApplicationName ("WSJT-X");
app.setApplicationVersion (version());
QCommandLineParser parser;
// parser.setApplicationDescription ("\n" PROJECT_DESCRIPTION);
parser.addHelpOption ();
parser.addVersionOption ();
parser.process (app);
return EXIT_SUCCESS;
}
catch (std::exception const& e)
{
std::cerr << "Error: " << e.what () << '\n';
}
catch (...)
{
std::cerr << "Unexpected error\n";
}
return -1;
}

5
Audio/Audio.pri Normal file
View File

@ -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

9
Audio/AudioDevice.cpp Normal file
View File

@ -0,0 +1,9 @@
#include "AudioDevice.hpp"
bool AudioDevice::initialize (OpenMode mode, Channel channel)
{
m_channel = channel;
// open and ensure we are unbuffered if possible
return QIODevice::open (mode | QIODevice::Unbuffered);
}

98
Audio/AudioDevice.hpp Normal file
View File

@ -0,0 +1,98 @@
#ifndef AUDIODEVICE_HPP__
#define AUDIODEVICE_HPP__
#include <QIODevice>
class QDataStream;
//
// abstract base class for audio devices
//
class AudioDevice : public QIODevice
{
public:
enum Channel {Mono, Left, Right, Both}; // these are mapped to combobox index so don't change
static char const * toString (Channel c)
{
return Mono == c ? "Mono" : Left == c ? "Left" : Right == c ? "Right" : "Both";
}
static Channel fromString (QString const& str)
{
QString s (str.toCaseFolded ().trimmed ().toLatin1 ());
return "both" == s ? Both : "right" == s ? Right : "left" == s ? Left : Mono;
}
bool initialize (OpenMode mode, Channel channel);
bool isSequential () const override {return true;}
size_t bytesPerFrame () const {return sizeof (qint16) * (Mono == m_channel ? 1 : 2);}
Channel channel () const {return m_channel;}
protected:
AudioDevice (QObject * parent = nullptr)
: QIODevice {parent}
{
}
void store (char const * source, size_t numFrames, qint16 * dest)
{
qint16 const * begin (reinterpret_cast<qint16 const *> (source));
for ( qint16 const * i = begin; i != begin + numFrames * (bytesPerFrame () / sizeof (qint16)); i += bytesPerFrame () / sizeof (qint16))
{
switch (m_channel)
{
case Mono:
*dest++ = *i;
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;
}
}
}
qint16 * load (qint16 const sample, qint16 * dest)
{
switch (m_channel)
{
case Mono:
*dest++ = sample;
break;
case Left:
*dest++ = sample;
*dest++ = 0;
break;
case Right:
*dest++ = 0;
*dest++ = sample;
break;
case Both:
*dest++ = sample;
*dest++ = sample;
break;
}
return dest;
}
private:
Channel m_channel;
};
Q_DECLARE_METATYPE (AudioDevice::Channel);
#endif

1081
Audio/BWFFile.cpp Normal file

File diff suppressed because it is too large Load Diff

210
Audio/BWFFile.hpp Normal file
View File

@ -0,0 +1,210 @@
#ifndef BWF_FILE_HPP__
#define BWF_FILE_HPP__
#include <array>
#include <QFile>
#include <QMap>
#include <QByteArray>
#include "pimpl_h.hpp"
class QObject;
class QString;
class QAudioFormat;
//
// BWFFile - Broadcast Wave Format File (a.k.a. WAV file)
//
// The BWF file format is a backward compatible variation of the
// Microsoft WAV file format. It contains an extra chunk with id
// 'bext' that contains metadata defined by the EBU in:
//
// https://tech.ebu.ch/docs/tech/tech3285.pdf
//
// Also relevant is the recommendation document:
//
// https://tech.ebu.ch/docs/r/r098.pdf
//
// which suggests a format to the free text coding history field.
//
// This class also supports the LIST-INFO chunk type which also allows
// metadata to be added to a WAV file, the defined INFO tag ids are
// documented here:
//
// http://bwfmetaedit.sourceforge.net/listinfo.html
//
// These ids are not enforced but they are recommended as most
// operating systems and audio applications recognize some or more of
// them. Notably Microsoft Windows is not one of the operating systems
// that does :( In fact there seems to be no documented metadata
// tagging format that Windows Explorer recognizes.
//
// Changes to the 'bext' fields and the LIST-INFO dictionary may be
// made right up until the file is closed as the relevant chunks are
// saved to the end of the file after the end of the sample data.
//
// This class emulates the QFile class, in fact it uses a QFile object
// instance internally and forwards many of its operations directly to
// it.
//
// BWFFile is a QIODevice subclass and the implementation provides
// access to the audio sample data contained in the BWF file as if
// only that data were in the file. I.e. the first sample is at file
// offset zero and the size of the file is the size of the sample
// data. The headers, trailers and metadata are hidden but can be
// accessed by the operations below.
//
class BWFFile
: public QIODevice
{
Q_OBJECT
public:
using FileHandleFlags = QFile::FileHandleFlags;
using Permissions = QFile::Permissions;
using FileError = QFile::FileError;
using MemoryMapFlags = QFile::MemoryMapFlags;
using InfoDictionary = QMap<std::array<char, 4>, QByteArray>;
using UMID = std::array<quint8, 64>;
explicit BWFFile (QAudioFormat const&, QObject * parent = nullptr);
explicit BWFFile (QAudioFormat const&, QString const& name,
QObject * parent = nullptr);
// The InfoDictionary should contain valid WAV format LIST-INFO
// identifiers as keys, a list of them can be found here:
//
// http://bwfmetaedit.sourceforge.net/listinfo.html
//
// For files opened for ReadOnly access the dictionary is not
// written to the file. For files opened ReadWrite, any existing
// LIST-INFO tags will be merged into the dictionary when the file
// is opened and if the file is modified the merged dictionary will
// be written back to the file.
//
// Note that the sample data may no be in the native endian, it is
// the callers responsibility to do any required endian
// conversions. The internal data is always in native endian with
// conversions being handled automatically. Use the BWF::format()
// operation to access the format including the
// QAudioFormat::byteOrder() operation to determine the data byte
// ordering.
//
explicit BWFFile (QAudioFormat const&, QString const& name,
InfoDictionary const&, QObject * parent = nullptr);
~BWFFile ();
QAudioFormat const& format () const;
InfoDictionary& list_info ();
//
// Broadcast Audio Extension fields
//
// If any of these modifiers are called then a "bext" chunk will be
// written to the file if the file is writeable and the sample data
// is modified.
//
enum class BextVersion : quint16 {v_0, v_1, v_2};
BextVersion bext_version () const;
void bext_version (BextVersion = BextVersion::v_2);
QByteArray bext_description () const;
void bext_description (QByteArray const&); // max 256 bytes
QByteArray bext_originator () const;
void bext_originator (QByteArray const&); // max 32 bytes
QByteArray bext_originator_reference () const;
void bext_originator_reference (QByteArray const&); // max 32 bytes
QDateTime bext_origination_date_time () const;
void bext_origination_date_time (QDateTime const&); // 1s resolution
quint64 bext_time_reference () const;
void bext_time_reference (quint64); // samples since midnight at start
UMID bext_umid () const; // bext version >= 1 only
void bext_umid (UMID const&);
quint16 bext_loudness_value () const;
void bext_loudness_value (quint16); // bext version >= 2 only
quint16 bext_loudness_range () const;
void bext_loudness_range (quint16); // bext version >= 2 only
quint16 bext_max_true_peak_level () const;
void bext_max_true_peak_level (quint16); // bext version >= 2 only
quint16 bext_max_momentary_loudness () const;
void bext_max_momentary_loudness (quint16); // bext version >= 2 only
quint16 bext_max_short_term_loudness () const;
void bext_max_short_term_loudness (quint16); // bext version >= 2 only
QByteArray bext_coding_history () const;
void bext_coding_history (QByteArray const&); // See EBU R 98
// Emulate QFile interface
bool open (OpenMode) override;
bool open (FILE *, OpenMode, FileHandleFlags = QFile::DontCloseHandle);
bool open (int fd, OpenMode, FileHandleFlags = QFile::DontCloseHandle);
bool copy (QString const& new_name);
bool exists () const;
bool link (QString const& link_name);
bool remove ();
bool rename (QString const& new_name);
void setFileName (QString const& name);
QString symLinkTarget () const;
QString fileName () const;
Permissions permissions () const;
// Resize is of the sample data portion, header and trailer chunks
// are excess to the given size
bool resize (qint64 new_size);
bool setPermissions (Permissions permissions);
FileError error () const;
bool flush ();
int handle () const;
// The mapping offset is relative to the start of the sample data
uchar * map (qint64 offset, qint64 size,
MemoryMapFlags = QFile::NoOptions);
bool unmap (uchar * address);
void unsetError ();
//
// QIODevice implementation
//
// The size returned is of the sample data only, header and trailer
// chunks are hidden and handled internally
qint64 size () const override;
bool isSequential () const override;
// The reset operation clears the 'bext' and LIST-INFO as if they
// were never supplied. If the file is writable the 'bext' and
// LIST-INFO chunks will not be written making the resulting file a
// lowest common denominator WAV file.
bool reset () override;
// Seek offsets are relative to the start of the sample data
bool seek (qint64) override;
// this can fail due to updating header issues, errors are ignored
void close () override;
protected:
qint64 readData (char * data, qint64 max_size) override;
qint64 writeData (char const* data, qint64 max_size) override;
private:
class impl;
pimpl<impl> m_;
};
#endif

212
Audio/soundin.cpp Normal file
View File

@ -0,0 +1,212 @@
#include "soundin.h"
#include <cstdlib>
#include <cmath>
#include <iomanip>
#include <QAudioDeviceInfo>
#include <QAudioFormat>
#include <QAudioInput>
#include <QSysInfo>
#include <QDebug>
#include "Logger.hpp"
#include "moc_soundin.cpp"
bool SoundInput::checkStream ()
{
bool result (false);
if (m_stream)
{
switch (m_stream->error ())
{
case QAudio::OpenError:
Q_EMIT error (tr ("An error opening the audio input device has occurred."));
break;
case QAudio::IOError:
Q_EMIT error (tr ("An error occurred during read from the audio input device."));
break;
// case QAudio::UnderrunError:
// Q_EMIT error (tr ("Audio data not being fed to the audio input device fast enough."));
// break;
case QAudio::FatalError:
Q_EMIT error (tr ("Non-recoverable error, audio input device not usable at this time."));
break;
case QAudio::UnderrunError: // TODO G4WJS: stop ignoring this
// when we find the cause on macOS
case QAudio::NoError:
result = true;
break;
}
}
return result;
}
void SoundInput::start(QAudioDeviceInfo const& device, int framesPerBuffer, AudioDevice * sink
, unsigned downSampleFactor, AudioDevice::Channel channel)
{
Q_ASSERT (sink);
stop ();
m_sink = sink;
QAudioFormat format (device.preferredFormat());
// qDebug () << "Preferred audio input format:" << format;
format.setChannelCount (AudioDevice::Mono == channel ? 1 : 2);
format.setCodec ("audio/pcm");
format.setSampleRate (12000 * downSampleFactor);
format.setSampleType (QAudioFormat::SignedInt);
format.setSampleSize (16);
format.setByteOrder (QAudioFormat::Endian (QSysInfo::ByteOrder));
if (!format.isValid ())
{
Q_EMIT error (tr ("Requested input audio format is not valid."));
return;
}
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;
m_stream.reset (new QAudioInput {device, format});
if (!checkStream ())
{
return;
}
connect (m_stream.data(), &QAudioInput::stateChanged, this, &SoundInput::handleStateChanged);
connect (m_stream.data(), &QAudioInput::notify, [this] () {checkStream ();});
//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 (framesPerBuffer > 0)
{
m_stream->setBufferSize (m_stream->format ().bytesForFrames (framesPerBuffer));
}
if (m_sink->initialize (QIODevice::WriteOnly, channel))
{
m_stream->start (sink);
checkStream ();
cummulative_lost_usec_ = -1;
LOG_DEBUG ("Selected buffer size (bytes): " << m_stream->bufferSize () << " period size: " << m_stream->periodSize ());
}
else
{
Q_EMIT error (tr ("Failed to initialize audio sink device"));
}
}
void SoundInput::suspend ()
{
if (m_stream)
{
m_stream->suspend ();
checkStream ();
}
}
void SoundInput::resume ()
{
// qDebug() << "Resume" << fmod(0.001*QDateTime::currentMSecsSinceEpoch(),6.0);
if (m_sink)
{
m_sink->reset ();
}
if (m_stream)
{
m_stream->resume ();
checkStream ();
}
}
void SoundInput::handleStateChanged (QAudio::State newState)
{
switch (newState)
{
case QAudio::IdleState:
Q_EMIT status (tr ("Idle"));
break;
case QAudio::ActiveState:
reset (false);
Q_EMIT status (tr ("Receiving"));
break;
case QAudio::SuspendedState:
Q_EMIT status (tr ("Suspended"));
break;
#if QT_VERSION >= QT_VERSION_CHECK (5, 10, 0)
case QAudio::InterruptedState:
Q_EMIT status (tr ("Interrupted"));
break;
#endif
case QAudio::StoppedState:
if (!checkStream ())
{
Q_EMIT status (tr ("Error"));
}
else
{
Q_EMIT status (tr ("Stopped"));
}
break;
}
}
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<qint64>::min () && report_dropped_frames)
{
auto lost_usec = elapsed_usecs - m_stream->processedUSecs () - cummulative_lost_usec_;
if (std::abs (lost_usec) > 48000 / 5)
{
LOG_WARN ("Detected dropped audio source samples: "
<< m_stream->format ().framesForDuration (lost_usec)
<< " (" << std::setprecision (4) << lost_usec / 1.e6 << " S)");
}
else if (std::abs (lost_usec) > 5 * 48000)
{
LOG_ERROR ("Detected excessive dropped audio source samples: "
<< m_stream->format ().framesForDuration (lost_usec)
<< " (" << std::setprecision (4) << lost_usec / 1.e6 << " S)");
}
}
cummulative_lost_usec_ = elapsed_usecs - m_stream->processedUSecs ();
}
}
void SoundInput::stop()
{
if (m_stream)
{
m_stream->stop ();
}
m_stream.reset ();
}
SoundInput::~SoundInput ()
{
stop ();
}

55
Audio/soundin.h Normal file
View File

@ -0,0 +1,55 @@
// -*- Mode: C++ -*-
#ifndef SOUNDIN_H__
#define SOUNDIN_H__
#include <limits>
#include <QObject>
#include <QString>
#include <QDateTime>
#include <QScopedPointer>
#include <QPointer>
#include <QAudioInput>
#include "Audio/AudioDevice.hpp"
class QAudioDeviceInfo;
class QAudioInput;
// Gets audio data from sound sample source and passes it to a sink device
class SoundInput
: public QObject
{
Q_OBJECT;
public:
SoundInput (QObject * parent = nullptr)
: QObject {parent}
, cummulative_lost_usec_ {std::numeric_limits<qint64>::min ()}
{
}
~SoundInput ();
// sink must exist from the start call until the next start call or
// stop call
Q_SLOT void start(QAudioDeviceInfo const&, int framesPerBuffer, AudioDevice * sink, unsigned downSampleFactor, AudioDevice::Channel = AudioDevice::Mono);
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;
private:
// used internally
Q_SLOT void handleStateChanged (QAudio::State);
bool checkStream ();
QScopedPointer<QAudioInput> m_stream;
QPointer<AudioDevice> m_sink;
qint64 cummulative_lost_usec_;
};
#endif

212
Audio/soundout.cpp Normal file
View File

@ -0,0 +1,212 @@
#include "soundout.h"
#include <QDateTime>
#include <QAudioDeviceInfo>
#include <QAudioOutput>
#include <QSysInfo>
#include <qmath.h>
#include <QDebug>
#include "Logger.hpp"
#include "Audio/AudioDevice.hpp"
#include "moc_soundout.cpp"
bool SoundOutput::checkStream () const
{
bool result {false};
Q_ASSERT_X (m_stream, "SoundOutput", "programming error");
if (m_stream) {
switch (m_stream->error ())
{
case QAudio::OpenError:
Q_EMIT error (tr ("An error opening the audio output device has occurred."));
break;
case QAudio::IOError:
Q_EMIT error (tr ("An error occurred during write to the audio output device."));
break;
case QAudio::UnderrunError:
Q_EMIT error (tr ("Audio data not being fed to the audio output device fast enough."));
break;
case QAudio::FatalError:
Q_EMIT error (tr ("Non-recoverable error, audio output device not usable at this time."));
break;
case QAudio::NoError:
result = true;
break;
}
}
return result;
}
void SoundOutput::setFormat (QAudioDeviceInfo const& device, unsigned channels, int frames_buffered)
{
Q_ASSERT (0 < channels && channels < 3);
m_device = device;
m_channels = channels;
m_framesBuffered = frames_buffered;
}
void SoundOutput::restart (QIODevice * source)
{
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;
}
// we have to set this before every start on the stream because the
// Windows implementation seems to forget the buffer size after a
// stop.
//qDebug () << "SoundOut default buffer size (bytes):" << m_stream->bufferSize () << "period size:" << m_stream->periodSize ();
if (m_framesBuffered > 0)
{
m_stream->setBufferSize (m_stream->format().bytesForFrames (m_framesBuffered));
}
m_stream->setCategory ("production");
m_stream->start (source);
LOG_DEBUG ("Selected buffer size (bytes): " << m_stream->bufferSize () << " period size: " << m_stream->periodSize ());
}
void SoundOutput::suspend ()
{
if (m_stream && QAudio::ActiveState == m_stream->state ())
{
m_stream->suspend ();
checkStream ();
}
}
void SoundOutput::resume ()
{
if (m_stream && QAudio::SuspendedState == m_stream->state ())
{
m_stream->resume ();
checkStream ();
}
}
void SoundOutput::reset ()
{
if (m_stream)
{
m_stream->reset ();
checkStream ();
}
}
void SoundOutput::stop ()
{
if (m_stream)
{
m_stream->reset ();
m_stream->stop ();
}
m_stream.reset ();
}
qreal SoundOutput::attenuation () const
{
return -(20. * qLn (m_volume) / qLn (10.));
}
void SoundOutput::setAttenuation (qreal a)
{
Q_ASSERT (0. <= a && a <= 999.);
m_volume = qPow(10.0, -a/20.0);
// qDebug () << "SoundOut: attn = " << a << ", vol = " << m_volume;
if (m_stream)
{
m_stream->setVolume (m_volume);
}
}
void SoundOutput::resetAttenuation ()
{
m_volume = 1.;
if (m_stream)
{
m_stream->setVolume (m_volume);
}
}
void SoundOutput::handleStateChanged (QAudio::State newState)
{
switch (newState)
{
case QAudio::IdleState:
Q_EMIT status (tr ("Idle"));
break;
case QAudio::ActiveState:
Q_EMIT status (tr ("Sending"));
break;
case QAudio::SuspendedState:
Q_EMIT status (tr ("Suspended"));
break;
#if QT_VERSION >= QT_VERSION_CHECK (5, 10, 0)
case QAudio::InterruptedState:
Q_EMIT status (tr ("Interrupted"));
break;
#endif
case QAudio::StoppedState:
if (!checkStream ())
{
Q_EMIT status (tr ("Error"));
}
else
{
Q_EMIT status (tr ("Stopped"));
}
break;
}
}

59
Audio/soundout.h Normal file
View File

@ -0,0 +1,59 @@
// -*- Mode: C++ -*-
#ifndef SOUNDOUT_H__
#define SOUNDOUT_H__
#include <QObject>
#include <QString>
#include <QAudioOutput>
#include <QAudioDeviceInfo>
class QIODevice;
class QAudioDeviceInfo;
// An instance of this sends audio data to a specified soundcard.
class SoundOutput
: public QObject
{
Q_OBJECT;
public:
SoundOutput ()
: m_framesBuffered {0}
, m_volume {1.0}
, error_ {false}
{
}
qreal attenuation () const;
public Q_SLOTS:
void setFormat (QAudioDeviceInfo const& device, unsigned channels, int frames_buffered = 0);
void restart (QIODevice *);
void suspend ();
void resume ();
void reset ();
void stop ();
void setAttenuation (qreal); /* unsigned */
void resetAttenuation (); /* to zero */
Q_SIGNALS:
void error (QString message) const;
void status (QString message) const;
private:
bool checkStream () const;
private Q_SLOTS:
void handleStateChanged (QAudio::State);
private:
QAudioDeviceInfo m_device;
unsigned m_channels;
QScopedPointer<QAudioOutput> m_stream;
int m_framesBuffered;
qreal m_volume;
bool error_;
};
#endif

View File

@ -0,0 +1,460 @@
#include <iostream>
#include <exception>
#include <stdexcept>
#include <string>
#include <memory>
#include <locale>
#include <QCoreApplication>
#include <QTextStream>
#include <QCommandLineParser>
#include <QCommandLineOption>
#include <QStringList>
#include <QFileInfo>
#include <QAudioFormat>
#include <QAudioDeviceInfo>
#include <QAudioInput>
#include <QAudioOutput>
#include <QTimer>
#include <QDateTime>
#include <QDebug>
#include "revision_utils.hpp"
#include "Audio/BWFFile.hpp"
namespace
{
QTextStream qtout {stdout};
}
class Record final
: public QObject
{
Q_OBJECT;
public:
Record (int start, int duration, QAudioDeviceInfo const& source_device, BWFFile * output, int notify_interval, int buffer_size)
: source_ {source_device, output->format ()}
, notify_interval_ {notify_interval}
, output_ {output}
, duration_ {duration}
{
if (buffer_size) source_.setBufferSize (output_->format ().bytesForFrames (buffer_size));
if (notify_interval_)
{
source_.setNotifyInterval (notify_interval);
connect (&source_, &QAudioInput::notify, this, &Record::notify);
}
if (start == -1)
{
start_recording ();
}
else
{
auto now = QDateTime::currentDateTimeUtc ();
auto time = now.time ();
auto then = now;
then.setTime (QTime {time.hour (), time.minute (), start});
auto delta_ms = (now.msecsTo (then) + (60 * 1000)) % (60 * 1000);
QTimer::singleShot (int (delta_ms), Qt::PreciseTimer, this, &Record::start_recording);
}
}
Q_SIGNAL void done ();
private:
Q_SLOT void start_recording ()
{
qtout << "started recording at " << QDateTime::currentDateTimeUtc ().toString ("hh:mm:ss.zzz UTC")
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
<< Qt::endl
#else
<< endl
#endif
;
source_.start (output_);
if (!notify_interval_) QTimer::singleShot (duration_ * 1000, Qt::PreciseTimer, this, &Record::stop_recording);
qtout << QString {"buffer size used is: %1"}.arg (source_.bufferSize ())
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
<< Qt::endl
#else
<< endl
#endif
;
}
Q_SLOT void notify ()
{
auto length = source_.elapsedUSecs ();
qtout << QString {"%1 μs recorded\r"}.arg (length)
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
<< Qt::flush
#else
<< flush
#endif
;
if (length >= duration_ * 1000 * 1000) stop_recording ();
}
Q_SLOT void stop_recording ()
{
auto length = source_.elapsedUSecs ();
source_.stop ();
qtout << QString {"%1 μs recorded "}.arg (length) << '(' << source_.format ().framesForBytes (output_->size ()) << " frames recorded)\n";
qtout << "stopped recording at " << QDateTime::currentDateTimeUtc ().toString ("hh:mm:ss.zzz UTC")
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
<< Qt::endl
#else
<< endl
#endif
;
Q_EMIT done ();
}
QAudioInput source_;
int notify_interval_;
BWFFile * output_;
int duration_;
};
class Playback final
: public QObject
{
Q_OBJECT;
public:
Playback (int start, BWFFile * input, QAudioDeviceInfo const& sink_device, int notify_interval, int buffer_size, QString const& category)
: input_ {input}
, sink_ {sink_device, input->format ()}
, notify_interval_ {notify_interval}
{
if (buffer_size) sink_.setBufferSize (input_->format ().bytesForFrames (buffer_size));
if (category.size ()) sink_.setCategory (category);
if (notify_interval_)
{
sink_.setNotifyInterval (notify_interval);
connect (&sink_, &QAudioOutput::notify, this, &Playback::notify);
}
connect (&sink_, &QAudioOutput::stateChanged, this, &Playback::sink_state_changed);
if (start == -1)
{
start_playback ();
}
else
{
auto now = QDateTime::currentDateTimeUtc ();
auto time = now.time ();
auto then = now;
then.setTime (QTime {time.hour (), time.minute (), start});
auto delta_ms = (now.msecsTo (then) + (60 * 1000)) % (60 * 1000);
QTimer::singleShot (int (delta_ms), Qt::PreciseTimer, this, &Playback::start_playback);
}
}
Q_SIGNAL void done ();
private:
Q_SLOT void start_playback ()
{
qtout << "started playback at " << QDateTime::currentDateTimeUtc ().toString ("hh:mm:ss.zzz UTC")
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
<< Qt::endl
#else
<< endl
#endif
;
sink_.start (input_);
qtout << QString {"buffer size used is: %1 (%2 frames)"}.arg (sink_.bufferSize ()).arg (sink_.format ().framesForBytes (sink_.bufferSize ()))
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
<< Qt::endl
#else
<< endl
#endif
;
}
Q_SLOT void notify ()
{
auto length = sink_.elapsedUSecs ();
qtout << QString {"%1 μs rendered\r"}.arg (length) <<
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
Qt::flush
#else
flush
#endif
;
}
Q_SLOT void sink_state_changed (QAudio::State state)
{
switch (state)
{
case QAudio::ActiveState:
qtout << "\naudio output state changed to active\n";
break;
case QAudio::SuspendedState:
qtout << "\naudio output state changed to suspended\n";
break;
case QAudio::StoppedState:
qtout << "\naudio output state changed to stopped\n";
break;
case QAudio::IdleState:
stop_playback ();
qtout << "\naudio output state changed to idle\n";
break;
#if QT_VERSION >= QT_VERSION_CHECK (5, 10, 0)
case QAudio::InterruptedState:
qtout << "\naudio output state changed to interrupted\n";
break;
#endif
}
}
Q_SLOT void stop_playback ()
{
auto length = sink_.elapsedUSecs ();
sink_.stop ();
qtout << QString {"%1 μs rendered "}.arg (length) << '(' << sink_.format ().framesForBytes (input_->size ()) << " frames rendered)\n";
qtout << "stopped playback at " << QDateTime::currentDateTimeUtc ().toString ("hh:mm:ss.zzz UTC")
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
<< Qt::endl
#else
<< endl
#endif
;
Q_EMIT done ();
}
BWFFile * input_;
QAudioOutput sink_;
int notify_interval_;
};
#include "record_time_signal.moc"
int main(int argc, char *argv[])
{
QCoreApplication app {argc, argv};
try
{
// ensure number forms are in consistent format, do this after
// instantiating QApplication so that Qt has correct l18n
std::locale::global (std::locale::classic ());
// Override programs executable basename as application name.
app.setApplicationName ("WSJT-X Record Time Signal");
app.setApplicationVersion (version ());
QCommandLineParser parser;
parser.setApplicationDescription (
"\nTool to determine and experiment with QAudioInput latencies\n\n"
"\tUse the -I option to list available recording device numbers\n"
);
auto help_option = parser.addHelpOption ();
auto version_option = parser.addVersionOption ();
parser.addOptions ({
{{"I", "list-audio-inputs"},
app.translate ("main", "List the available audio input devices")},
{{"O", "list-audio-outputs"},
app.translate ("main", "List the available audio output devices")},
{{"s", "start-time"},
app.translate ("main", "Record from <start-time> seconds, default start immediately"),
app.translate ("main", "start-time")},
{{"d", "duration"},
app.translate ("main", "Recording <duration> seconds"),
app.translate ("main", "duration")},
{{"o", "output"},
app.translate ("main", "Save output as <output-file>"),
app.translate ("main", "output-file")},
{{"i", "input"},
app.translate ("main", "Playback <input-file>"),
app.translate ("main", "input-file")},
{{"f", "force"},
app.translate ("main", "Overwrite existing file")},
{{"r", "sample-rate"},
app.translate ("main", "Record at <sample-rate>, default 48000 Hz"),
app.translate ("main", "sample-rate")},
{{"c", "num-channels"},
app.translate ("main", "Record <num> channels, default 2"),
app.translate ("main", "num")},
{{"R", "recording-device-number"},
app.translate ("main", "Record from <device-number>"),
app.translate ("main", "device-number")},
{{"P", "playback-device-number"},
app.translate ("main", "Playback to <device-number>"),
app.translate ("main", "device-number")},
{{"C", "category"},
app.translate ("main", "Playback <category-name>"),
app.translate ("main", "category-name")},
{{"n", "notify-interval"},
app.translate ("main", "use notify signals every <interval> milliseconds, zero to use a timer"),
app.translate ("main", "interval")},
{{"b", "buffer-size"},
app.translate ("main", "audio buffer size <frames>"),
app.translate ("main", "frames")},
});
parser.process (app);
auto input_devices = QAudioDeviceInfo::availableDevices (QAudio::AudioInput);
if (parser.isSet ("I"))
{
int n {0};
for (auto const& device : input_devices)
{
qtout << ++n << " - [" << device.deviceName () << ']'
#if QT_VERSION >= QT_VERSION_CHECK (5, 15, 0)
<< Qt::endl
#else
<< endl
#endif
;
}
return 0;
}
auto output_devices = QAudioDeviceInfo::availableDevices (QAudio::AudioOutput);
if (parser.isSet ("O"))
{
int n {0};
for (auto const& device : output_devices)
{
qtout << ++n << " - [" << device.deviceName () << ']'
#if QT_VERSION >= QT_VERSION_CHECK (5, 15, 0)
<< Qt::endl
#else
<< endl
#endif
;
}
return 0;
}
bool ok;
int start {-1};
if (parser.isSet ("s"))
{
start = parser.value ("s").toInt (&ok);
if (!ok) throw std::invalid_argument {"start time not a number"};
if (0 > start || start > 59) throw std::invalid_argument {"0 > start > 59"};
}
int sample_rate {48000};
if (parser.isSet ("r"))
{
sample_rate = parser.value ("r").toInt (&ok);
if (!ok) throw std::invalid_argument {"sample rate not a number"};
}
int num_channels {2};
if (parser.isSet ("c"))
{
num_channels = parser.value ("c").toInt (&ok);
if (!ok) throw std::invalid_argument {"channel count not a number"};
}
int notify_interval {0};
if (parser.isSet ("n"))
{
notify_interval = parser.value ("n").toInt (&ok);
if (!ok) throw std::invalid_argument {"notify interval not a number"};
}
int buffer_size {0};
if (parser.isSet ("b"))
{
buffer_size = parser.value ("b").toInt (&ok);
if (!ok) throw std::invalid_argument {"buffer size not a number"};
}
int input_device {0};
if (parser.isSet ("R"))
{
input_device = parser.value ("R").toInt (&ok);
if (!ok || 0 >= input_device || input_device > input_devices.size ())
{
throw std::invalid_argument {"invalid recording device"};
}
}
int output_device {0};
if (parser.isSet ("P"))
{
output_device = parser.value ("P").toInt (&ok);
if (!ok || 0 >= output_device || output_device > output_devices.size ())
{
throw std::invalid_argument {"invalid playback device"};
}
}
if (!(parser.isSet ("o") || parser.isSet ("i"))) throw std::invalid_argument {"file required"};
if (parser.isSet ("o") && parser.isSet ("i")) throw std::invalid_argument {"specify either input or output"};
QAudioFormat audio_format;
if (parser.isSet ("o")) // Record
{
int duration = parser.value ("d").toInt (&ok);
if (!ok) throw std::invalid_argument {"duration not a number"};
QFileInfo ofi {parser.value ("o")};
if (!ofi.suffix ().size () && ofi.fileName ()[ofi.fileName ().size () - 1] != QChar {'.'})
{
ofi.setFile (ofi.filePath () + ".wav");
}
if (!parser.isSet ("f") && ofi.isFile ())
{
throw std::invalid_argument {"set the `-force' option to overwrite an existing output file"};
}
audio_format.setSampleRate (sample_rate);
audio_format.setChannelCount (num_channels);
audio_format.setSampleSize (16);
audio_format.setSampleType (QAudioFormat::SignedInt);
audio_format.setCodec ("audio/pcm");
auto source = input_device ? input_devices[input_device - 1] : QAudioDeviceInfo::defaultInputDevice ();
if (!source.isFormatSupported (audio_format))
{
qtout << "warning, requested format not supported, using nearest"
#if QT_VERSION >= QT_VERSION_CHECK (5, 15, 0)
<< Qt::endl
#else
<< endl
#endif
;
audio_format = source.nearestFormat (audio_format);
}
BWFFile output_file {audio_format, ofi.filePath ()};
if (!output_file.open (BWFFile::WriteOnly)) throw std::invalid_argument {QString {"cannot open output file \"%1\""}.arg (ofi.filePath ()).toStdString ()};
// run the application
Record record {start, duration, source, &output_file, notify_interval, buffer_size};
QObject::connect (&record, &Record::done, &app, &QCoreApplication::quit);
return app.exec();
}
else // Playback
{
QFileInfo ifi {parser.value ("i")};
if (!ifi.isFile () && !ifi.suffix ().size () && ifi.fileName ()[ifi.fileName ().size () - 1] != QChar {'.'})
{
ifi.setFile (ifi.filePath () + ".wav");
}
BWFFile input_file {audio_format, ifi.filePath ()};
if (!input_file.open (BWFFile::ReadOnly)) throw std::invalid_argument {QString {"cannot open input file \"%1\""}.arg (ifi.filePath ()).toStdString ()};
auto sink = output_device ? output_devices[output_device - 1] : QAudioDeviceInfo::defaultOutputDevice ();
if (!sink.isFormatSupported (input_file.format ()))
{
throw std::invalid_argument {"audio output device does not support input file audio format"};
}
// run the application
Playback play {start, &input_file, sink, notify_interval, buffer_size, parser.value ("category")};
QObject::connect (&play, &Playback::done, &app, &QCoreApplication::quit);
return app.exec();
}
}
catch (std::exception const& e)
{
std::cerr << "Error: " << e.what () << '\n';
}
catch (...)
{
std::cerr << "Unexpected fatal error\n";
throw; // hoping the runtime might tell us more about the exception
}
return -1;
}

14
BUGS Normal file
View File

@ -0,0 +1,14 @@
__ __ ______ _____ ________ __ __
| \ _ | \ / \ | \| \ | \ | \
| $$ / \ | $$| $$$$$$\ \$$$$$ \$$$$$$$$ | $$ | $$
| $$/ $\| $$| $$___\$$ | $$ | $$ ______ \$$\/ $$
| $$ $$$\ $$ \$$ \ __ | $$ | $$| \ >$$ $$
| $$ $$\$$\$$ _\$$$$$$\| \ | $$ | $$ \$$$$$$/ $$$$\
| $$$$ \$$$$| \__| $$| $$__| $$ | $$ | $$ \$$\
| $$$ \$$$ \$$ $$ \$$ $$ | $$ | $$ | $$
\$$ \$$ \$$$$$$ \$$$$$$ \$$ \$$ \$$
There are some defects remaining in WSJT-X.

View File

@ -0,0 +1,103 @@
# - Try to find FFTW3.
# Usage: find_package(FFTW3 [COMPONENTS [single double long-double threads]])
#
# Variables used by this module:
# FFTW3_ROOT_DIR - FFTW3 root directory
# Variables defined by this module:
# FFTW3_FOUND - system has FFTW3
# FFTW3_INCLUDE_DIR - the FFTW3 include directory (cached)
# FFTW3_INCLUDE_DIRS - the FFTW3 include directories
# (identical to FFTW3_INCLUDE_DIR)
# FFTW3[FL]?_LIBRARY - the FFTW3 library - double, single(F),
# long-double(L) precision (cached)
# FFTW3[FL]?_THREADS_LIBRARY - the threaded FFTW3 library - double, single(F),
# long-double(L) precision (cached)
# FFTW3_LIBRARIES - list of all FFTW3 libraries found
# Copyright (C) 2009-2010
# ASTRON (Netherlands Institute for Radio Astronomy)
# P.O.Box 2, 7990 AA Dwingeloo, The Netherlands
#
# This file is part of the LOFAR software suite.
# The LOFAR software suite is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# The LOFAR software suite is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with the LOFAR software suite. If not, see <http://www.gnu.org/licenses/>.
#
# $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)
else ()
set (_components ${FFTW3_FIND_COMPONENTS})
endif ()
# Loop over each component.
set (_libraries)
foreach (_comp ${_components})
if (_comp STREQUAL "single")
list (APPEND _libraries fftw3f)
elseif (_comp STREQUAL "double")
list (APPEND _libraries fftw3)
elseif (_comp STREQUAL "long-double")
list (APPEND _libraries fftw3l)
elseif (_comp STREQUAL "threads")
set (_use_threads ON)
else (_comp STREQUAL "single")
message (FATAL_ERROR "FindFFTW3: unknown component `${_comp}' specified. "
"Valid components are `single', `double', `long-double', and `threads'.")
endif (_comp STREQUAL "single")
endforeach (_comp ${_components})
# If using threads, we need to link against threaded libraries as well - except on Windows.
if (NOT WIN32 AND _use_threads)
set (_thread_libs)
foreach (_lib ${_libraries})
list (APPEND _thread_libs ${_lib}_threads)
endforeach (_lib ${_libraries})
set (_libraries ${_thread_libs} ${_libraries})
endif (NOT WIN32 AND _use_threads)
# Keep a list of variable names that we need to pass on to
# find_package_handle_standard_args().
set (_check_list)
# Search for all requested libraries.
foreach (_lib ${_libraries})
string (TOUPPER ${_lib} _LIB)
find_library (${_LIB}_LIBRARY NAMES ${_lib} ${_lib}-3
HINTS ${FFTW3_ROOT_DIR} PATH_SUFFIXES lib)
mark_as_advanced (${_LIB}_LIBRARY)
list (APPEND FFTW3_LIBRARIES ${${_LIB}_LIBRARY})
list (APPEND _check_list ${_LIB}_LIBRARY)
endforeach (_lib ${_libraries})
# Search for the header file.
find_path (FFTW3_INCLUDE_DIR fftw3.h
HINTS ${FFTW3_ROOT_DIR} PATH_SUFFIXES include)
mark_as_advanced (FFTW3_INCLUDE_DIR)
list(APPEND _check_list FFTW3_INCLUDE_DIR)
# Handle the QUIETLY and REQUIRED arguments and set FFTW_FOUND to TRUE if
# all listed variables are TRUE
include (FindPackageHandleStandardArgs)
find_package_handle_standard_args (FFTW3 DEFAULT_MSG ${_check_list})
if (FFTW3_FOUND)
set (FFTW3_INCLUDE_DIRS ${FFTW3_INCLUDE_DIR})
endif (FFTW3_FOUND)

View File

@ -0,0 +1,75 @@
# - Extract information from a git-svn working copy
# The module defines the following variables:
#
# If the command line client executable is found two macros are defined:
# GitSubversion_WC_INFO(<dir> <var-prefix>)
# GitSubversion_WC_INFO extracts information of a subversion working copy at
# a given location. This macro defines the following variables:
# <var-prefix>_WC_URL - url of the repository (at <dir>)
# <var-prefix>_WC_ROOT - root url of the repository
# <var-prefix>_WC_REVISION - current revision
# <var-prefix>_WC_LAST_CHANGED_AUTHOR - author of last commit
# <var-prefix>_WC_LAST_CHANGED_DATE - date of last commit
# <var-prefix>_WC_LAST_CHANGED_REV - revision of last commit
# <var-prefix>_WC_INFO - output of command `svn info <dir>'
# Example usage:
# find_package(Subversion)
# if(SUBVERSION_FOUND)
# GitSubversion_WC_INFO(${PROJECT_SOURCE_DIR} Project)
# message("Current revision is ${Project_WC_REVISION}")
# endif()
find_package (Git)
if(GIT_FOUND)
# the git-svn commands should be executed with the C locale, otherwise
# the message (which are parsed) may be translated, Alex
set(_Subversion_SAVED_LC_ALL "$ENV{LC_ALL}")
set(ENV{LC_ALL} C)
# execute_process(COMMAND ${Subversion_SVN_EXECUTABLE} --version
# OUTPUT_VARIABLE Subversion_VERSION_SVN
# OUTPUT_STRIP_TRAILING_WHITESPACE)
# restore the previous LC_ALL
set(ENV{LC_ALL} ${_Subversion_SAVED_LC_ALL})
# string(REGEX REPLACE "^(.*\n)?svn, version ([.0-9]+).*"
# "\\2" Subversion_VERSION_SVN "${Subversion_VERSION_SVN}")
macro(GitSubversion_WC_INFO dir prefix)
# the subversion commands should be executed with the C locale, otherwise
# the message (which are parsed) may be translated, Alex
set(_Subversion_SAVED_LC_ALL "$ENV{LC_ALL}")
set(ENV{LC_ALL} C)
execute_process(COMMAND ${GIT_EXECUTABLE} --git-dir=${dir}/.git svn info
OUTPUT_VARIABLE ${prefix}_WC_INFO
ERROR_VARIABLE Git_git_svn_info_error
RESULT_VARIABLE Git_git_svn_info_result
OUTPUT_STRIP_TRAILING_WHITESPACE)
if(NOT ${Git_git_svn_info_result} EQUAL 0)
message(SEND_ERROR "Command \"${GIT_EXECUTABLE} --git-dir=${dir}/.git svn info\" failed with output:\n${Git_git_svn_info_error}")
else()
string(REGEX REPLACE "^(.*\n)?URL: ([^\n]+).*"
"\\2" ${prefix}_WC_URL "${${prefix}_WC_INFO}")
string(REGEX REPLACE "^(.*\n)?Repository Root: ([^\n]+).*"
"\\2" ${prefix}_WC_ROOT "${${prefix}_WC_INFO}")
string(REGEX REPLACE "^(.*\n)?Revision: ([^\n]+).*"
"\\2" ${prefix}_WC_REVISION "${${prefix}_WC_INFO}")
string(REGEX REPLACE "^(.*\n)?Last Changed Author: ([^\n]+).*"
"\\2" ${prefix}_WC_LAST_CHANGED_AUTHOR "${${prefix}_WC_INFO}")
string(REGEX REPLACE "^(.*\n)?Last Changed Rev: ([^\n]+).*"
"\\2" ${prefix}_WC_LAST_CHANGED_REV "${${prefix}_WC_INFO}")
string(REGEX REPLACE "^(.*\n)?Last Changed Date: ([^\n]+).*"
"\\2" ${prefix}_WC_LAST_CHANGED_DATE "${${prefix}_WC_INFO}")
endif()
# restore the previous LC_ALL
set(ENV{LC_ALL} ${_Subversion_SAVED_LC_ALL})
endmacro()
endif()

View File

@ -0,0 +1,64 @@
#
# Find the hamlib library
#
# This will define the following variables::
#
# Hamlib_FOUND - True if the system has the usb library
# Hamlib_VERSION - The verion of the usb library which was found
#
# and the following imported targets::
#
# Hamlib::Hamlib - The hamlib library
#
include (LibFindMacros)
libfind_pkg_detect (Hamlib hamlib
FIND_PATH hamlib/rig.h PATH_SUFFIXES hamlib
FIND_LIBRARY hamlib
)
libfind_package (Hamlib Usb)
libfind_process (Hamlib)
if (NOT Hamlib_PKGCONF_FOUND)
if (WIN32)
set (Hamlib_LIBRARIES ${Hamlib_LIBRARIES};${Usb_LIBRARY};ws2_32)
else ()
set (Hamlib_LIBRARIES ${Hamlib_LIBRARIES};m;dl)
endif ()
elseif (UNIX AND NOT APPLE)
set (Hamlib_LIBRARIES ${Hamlib_PKGCONF_STATIC_LDFLAGS})
endif ()
# fix up extra link libraries for macOS as target_link_libraries gets
# it wrong for frameworks
unset (_next_is_framework)
unset (_Hamlib_EXTRA_LIBS)
foreach (_lib IN LISTS Hamlib_LIBRARIES Hamlib_PKGCONF_LDFLAGS)
if (_next_is_framework)
list (APPEND _Hamlib_EXTRA_LIBS "-framework ${_lib}")
unset (_next_is_framework)
elseif (${_lib} STREQUAL "-framework")
set (_next_is_framework TRUE)
else ()
list (APPEND _Hamlib_EXTRA_LIBS ${_lib})
endif ()
endforeach ()
if (Hamlib_FOUND AND NOT TARGET Hamlib::Hamlib)
add_library (Hamlib::Hamlib UNKNOWN IMPORTED)
set_target_properties (Hamlib::Hamlib PROPERTIES
IMPORTED_LOCATION "${Hamlib_LIBRARY}"
INTERFACE_COMPILE_OPTIONS "${Hamlib_PKGCONF_STATIC_OTHER}"
INTERFACE_INCLUDE_DIRECTORIES "${Hamlib_INCLUDE_DIR}"
INTERFACE_LINK_LIBRARIES "${_Hamlib_EXTRA_LIBS}"
)
endif ()
mark_as_advanced (
Hamlib_INCLUDE_DIR
Hamlib_LIBRARY
Hamlib_LIBRARIES
)

View File

@ -0,0 +1,50 @@
# - Try to find portaudio
#
# Once done, this will define:
#
# Portaudio_FOUND - system has portaudio
# Portaudio_VERSION - The version of the portaudio library which was found
#
# and the following imported targets::
#
# Portaudio::Portaudio - The portaudio library
#
include (LibFindMacros)
libfind_pkg_detect (Portaudio portaudio-2.0
FIND_PATH portaudio.h
FIND_LIBRARY portaudio
)
libfind_process (Portaudio)
# fix up extra link libraries for macOS as target_link_libraries gets
# it wrong for frameworks
unset (_next_is_framework)
unset (_Portaudio_EXTRA_LIBS)
foreach (_lib IN LISTS Portaudio_PKGCONF_LDFLAGS)
if (_next_is_framework)
list (APPEND _Portaudio_EXTRA_LIBS "-framework ${_lib}")
unset (_next_is_framework)
elseif (${_lib} STREQUAL "-framework")
set (_next_is_framework TRUE)
else ()
list (APPEND _Portaudio_EXTRA_LIBS ${_lib})
endif ()
endforeach ()
if (Portaudio_FOUND AND NOT TARGET Portaudio::Portaudio)
add_library (Portaudio::Portaudio UNKNOWN IMPORTED)
set_target_properties (Portaudio::Portaudio PROPERTIES
IMPORTED_LOCATION "${Portaudio_LIBRARY}"
INTERFACE_COMPILE_OPTIONS "${Portaudio_PKGCONF_CFLAGS_OTHER}"
INTERFACE_INCLUDE_DIRECTORIES "${Portaudio_INCLUDE_DIRS}"
INTERFACE_LINK_LIBRARIES "${_Portaudio_EXTRA_LIBS}"
)
endif ()
mark_as_advanced (
Portaudio_INCLUDE_DIR
Portaudio_LIBRARY
)

View File

@ -0,0 +1,49 @@
# Findlibusb
# ==========
#
# Find the usb library
#
# This will define the following variables::
#
# Usb_FOUND - True if the system has the usb library
# Usb_VERSION - The verion of the usb library which was found
#
# and the following imported targets::
#
# Usb::Usb - The libusb library
#
include (LibFindMacros)
if (WIN32)
# Use path suffixes on MS Windows as we probably shouldn't
# trust the PATH envvar. PATH will still be searched to find the
# library as last resort.
if (CMAKE_SIZEOF_VOID_P MATCHES "8")
set (_library_options PATH_SUFFIXES MinGW64/dll MinGW64/static)
else ()
set (_library_options PATH_SUFFIXES MinGW32/dll MinGW32/static)
endif ()
endif ()
libfind_pkg_detect (Usb usb-1.0
FIND_PATH libusb.h PATH_SUFFIXES libusb-1.0
FIND_LIBRARY usb-1.0 ${_library_options}
)
libfind_process (Usb)
if (Usb_FOUND AND NOT TARGET Usb::Usb)
add_library (Usb::Usb UNKNOWN IMPORTED)
set_target_properties (Usb::Usb PROPERTIES
IMPORTED_LOCATION "${Usb_LIBRARY}"
INTERFACE_COMPILE_OPTIONS "${Usb_PKGCONF_CFLAGS_OTHER}"
INTERFACE_INCLUDE_DIRECTORIES "${Usb_INCLUDE_DIRS}"
INTERFACE_LINK_LIBRARIES "${Usb_LIBRARIES}"
)
endif ()
mark_as_advanced (
Usb_INCLUDE_DIR
Usb_LIBRARY
Usb_LIBRARIES
)

View File

@ -0,0 +1,168 @@
# - Returns a version string from Git
#
# These functions force a re-configure on each git commit so that you can
# trust the values of the variables in your build system.
#
# get_git_head_revision(<source_dir> <refspecvar> <hashvar> [<additional arguments to git describe> ...])
#
# Returns the refspec and sha hash of the current head revision
#
# git_describe(<source-dir> <var> [<additional arguments to git describe> ...])
#
# Returns the results of git describe on the source tree, and adjusting
# the output so that it tests false if an error occurs.
#
# git_get_exact_tag(<source-dir> <var> [<additional arguments to git describe> ...])
#
# Returns the results of git describe --exact-match on the source tree,
# and adjusting the output so that it tests false if there was no exact
# matching tag.
#
# git_local_changes(<source-dir> <var>)
#
# Returns either "CLEAN" or "DIRTY" with respect to uncommitted changes.
# Uses the return code of "git diff-index --quiet HEAD --".
# Does not regard untracked files.
#
# Requires CMake 2.6 or newer (uses the 'function' command)
#
# Original Author:
# 2009-2010 Ryan Pavlik <rpavlik@iastate.edu> <abiryan@ryand.net>
# http://academic.cleardefinition.com
# Iowa State University HCI Graduate Program/VRAC
#
# Copyright Iowa State University 2009-2010.
# Distributed under the Boost Software License, Version 1.0.
# (See accompanying file LICENSE_1_0.txt or copy at
# http://www.boost.org/LICENSE_1_0.txt)
if(__get_git_revision_description)
return()
endif()
set(__get_git_revision_description YES)
# We must run the following at "include" time, not at function call time,
# to find the path to this module rather than the path to a calling list file
get_filename_component(_gitdescmoddir ${CMAKE_CURRENT_LIST_FILE} PATH)
function(get_git_head_revision _source_dir _refspecvar _hashvar)
set(GIT_PARENT_DIR "${_source_dir}")
set(GIT_DIR "${GIT_PARENT_DIR}/.git")
while(NOT EXISTS "${GIT_DIR}") # .git dir not found, search parent directories
set(GIT_PREVIOUS_PARENT "${GIT_PARENT_DIR}")
get_filename_component(GIT_PARENT_DIR ${GIT_PARENT_DIR} PATH)
if(GIT_PARENT_DIR STREQUAL GIT_PREVIOUS_PARENT)
# We have reached the root directory, we are not in git
set(${_refspecvar} "GITDIR-NOTFOUND" PARENT_SCOPE)
set(${_hashvar} "GITDIR-NOTFOUND" PARENT_SCOPE)
return()
endif()
set(GIT_DIR "${GIT_PARENT_DIR}/.git")
endwhile()
# check if this is a submodule
if(NOT IS_DIRECTORY ${GIT_DIR})
file(READ ${GIT_DIR} submodule)
string(REGEX REPLACE "gitdir: (.*)\n$" "\\1" GIT_DIR_RELATIVE ${submodule})
get_filename_component(SUBMODULE_DIR ${GIT_DIR} PATH)
get_filename_component(GIT_DIR ${SUBMODULE_DIR}/${GIT_DIR_RELATIVE} ABSOLUTE)
endif()
set(GIT_DATA "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/git-data")
if(NOT EXISTS "${GIT_DATA}")
file(MAKE_DIRECTORY "${GIT_DATA}")
endif()
if(NOT EXISTS "${GIT_DIR}/HEAD")
return()
endif()
set(HEAD_FILE "${GIT_DATA}/HEAD")
configure_file("${GIT_DIR}/HEAD" "${HEAD_FILE}" COPYONLY)
configure_file("${_gitdescmoddir}/GetGitRevisionDescription.cmake.in"
"${GIT_DATA}/grabRef.cmake"
@ONLY)
include("${GIT_DATA}/grabRef.cmake")
set(${_refspecvar} "${HEAD_REF}" PARENT_SCOPE)
set(${_hashvar} "${HEAD_HASH}" PARENT_SCOPE)
endfunction()
function(git_describe _source_dir _var)
if(NOT GIT_FOUND)
find_package(Git QUIET)
endif()
get_git_head_revision(${_source_dir} refspec hash)
if(NOT GIT_FOUND)
set(${_var} "GIT-NOTFOUND" PARENT_SCOPE)
return()
endif()
if(NOT hash)
set(${_var} "HEAD-HASH-NOTFOUND" PARENT_SCOPE)
return()
endif()
# TODO sanitize
#if((${ARGN}" MATCHES "&&") OR
# (ARGN MATCHES "||") OR
# (ARGN MATCHES "\\;"))
# message("Please report the following error to the project!")
# message(FATAL_ERROR "Looks like someone's doing something nefarious with git_describe! Passed arguments ${ARGN}")
#endif()
#message(STATUS "Arguments to execute_process: ${ARGN}")
execute_process(COMMAND
"${GIT_EXECUTABLE}"
describe
${hash}
${ARGN}
WORKING_DIRECTORY
"${_source_dir}"
RESULT_VARIABLE
res
OUTPUT_VARIABLE
out
ERROR_QUIET
OUTPUT_STRIP_TRAILING_WHITESPACE)
if(NOT res EQUAL 0)
set(out "${out}-${res}-NOTFOUND")
endif()
set(${_var} "${out}" PARENT_SCOPE)
endfunction()
function(git_get_exact_tag _source_dir _var)
git_describe(${_source_dir} out --exact-match ${ARGN})
set(${_var} "${out}" PARENT_SCOPE)
endfunction()
function(git_local_changes _source_dir _var)
if(NOT GIT_FOUND)
find_package(Git QUIET)
endif()
get_git_head_revision(${_source_dir} refspec hash)
if(NOT GIT_FOUND)
set(${_var} "GIT-NOTFOUND" PARENT_SCOPE)
return()
endif()
if(NOT hash)
set(${_var} "HEAD-HASH-NOTFOUND" PARENT_SCOPE)
return()
endif()
execute_process(COMMAND
"${GIT_EXECUTABLE}"
diff-index --quiet HEAD --
WORKING_DIRECTORY
"${_source_dir}"
RESULT_VARIABLE
res
OUTPUT_VARIABLE
out
ERROR_QUIET
OUTPUT_STRIP_TRAILING_WHITESPACE)
if(res EQUAL 0)
set(${_var} "CLEAN" PARENT_SCOPE)
else()
set(${_var} "DIRTY" PARENT_SCOPE)
endif()
endfunction()

View File

@ -0,0 +1,41 @@
#
# Internal file for GetGitRevisionDescription.cmake
#
# Requires CMake 2.6 or newer (uses the 'function' command)
#
# Original Author:
# 2009-2010 Ryan Pavlik <rpavlik@iastate.edu> <abiryan@ryand.net>
# http://academic.cleardefinition.com
# Iowa State University HCI Graduate Program/VRAC
#
# Copyright Iowa State University 2009-2010.
# Distributed under the Boost Software License, Version 1.0.
# (See accompanying file LICENSE_1_0.txt or copy at
# http://www.boost.org/LICENSE_1_0.txt)
set(HEAD_HASH)
file(READ "@HEAD_FILE@" HEAD_CONTENTS LIMIT 1024)
string(STRIP "${HEAD_CONTENTS}" HEAD_CONTENTS)
if(HEAD_CONTENTS MATCHES "ref")
# named branch
string(REPLACE "ref: " "" HEAD_REF "${HEAD_CONTENTS}")
if(EXISTS "@GIT_DIR@/${HEAD_REF}")
configure_file("@GIT_DIR@/${HEAD_REF}" "@GIT_DATA@/head-ref" COPYONLY)
else()
configure_file("@GIT_DIR@/packed-refs" "@GIT_DATA@/packed-refs" COPYONLY)
file(READ "@GIT_DATA@/packed-refs" PACKED_REFS)
if(${PACKED_REFS} MATCHES "([0-9a-z]*) ${HEAD_REF}")
set(HEAD_HASH "${CMAKE_MATCH_1}")
endif()
endif()
else()
# detached HEAD
configure_file("@GIT_DIR@/HEAD" "@GIT_DATA@/head-ref" COPYONLY)
endif()
if(NOT HEAD_HASH)
file(READ "@GIT_DATA@/head-ref" HEAD_HASH LIMIT 1024)
string(STRIP "${HEAD_HASH}" HEAD_HASH)
endif()

View File

@ -0,0 +1,269 @@
# Version 2.3
# Public Domain, originally written by Lasse Kärkkäinen <tronic>
# Maintained at https://github.com/Tronic/cmake-modules
# Please send your improvements as pull requests on Github.
# Find another package and make it a dependency of the current package.
# This also automatically forwards the "REQUIRED" argument.
# Usage: libfind_package(<prefix> <another package> [extra args to find_package])
macro (libfind_package PREFIX PKG)
set(${PREFIX}_args ${PKG} ${ARGN})
if (${PREFIX}_FIND_REQUIRED)
set(${PREFIX}_args ${${PREFIX}_args} REQUIRED)
endif()
find_package(${${PREFIX}_args})
set(${PREFIX}_DEPENDENCIES ${${PREFIX}_DEPENDENCIES};${PKG})
unset(${PREFIX}_args)
endmacro()
# A simple wrapper to make pkg-config searches a bit easier.
# Works the same as CMake's internal pkg_check_modules but is always quiet.
macro (libfind_pkg_check_modules)
find_package(PkgConfig QUIET)
if (PKG_CONFIG_FOUND)
pkg_check_modules(${ARGN} QUIET)
endif()
endmacro()
# Avoid useless copy&pasta by doing what most simple libraries do anyway:
# pkg-config, find headers, find library.
# Usage: libfind_pkg_detect(<prefix> <pkg-config args> FIND_PATH <name> [other args] FIND_LIBRARY <name> [other args])
# E.g. libfind_pkg_detect(SDL2 sdl2 FIND_PATH SDL.h PATH_SUFFIXES SDL2 FIND_LIBRARY SDL2)
function (libfind_pkg_detect PREFIX)
# Parse arguments
set(argname pkgargs)
foreach (i ${ARGN})
if ("${i}" STREQUAL "FIND_PATH")
set(argname pathargs)
elseif ("${i}" STREQUAL "FIND_LIBRARY")
set(argname libraryargs)
else()
set(${argname} ${${argname}} ${i})
endif()
endforeach()
if (NOT pkgargs)
message(FATAL_ERROR "libfind_pkg_detect requires at least a pkg_config package name to be passed.")
endif()
# Find library
libfind_pkg_check_modules(${PREFIX}_PKGCONF ${pkgargs})
if (pathargs)
find_path(${PREFIX}_INCLUDE_DIR NAMES ${pathargs} HINTS ${${PREFIX}_PKGCONF_INCLUDE_DIRS})
endif()
if (libraryargs)
find_library(${PREFIX}_LIBRARY NAMES ${libraryargs} HINTS ${${PREFIX}_PKGCONF_LIBRARY_DIRS})
endif()
# Read pkg-config version
if (${PREFIX}_PKGCONF_VERSION)
set(${PREFIX}_VERSION ${${PREFIX}_PKGCONF_VERSION} PARENT_SCOPE)
endif()
endfunction()
# Extracts a version #define from a version.h file, output stored to <PREFIX>_VERSION.
# Usage: libfind_version_header(Foobar foobar/version.h FOOBAR_VERSION_STR)
# Fourth argument "QUIET" may be used for silently testing different define names.
# This function does nothing if the version variable is already defined.
function (libfind_version_header PREFIX VERSION_H DEFINE_NAME)
# Skip processing if we already have a version or if the include dir was not found
if (${PREFIX}_VERSION OR NOT ${PREFIX}_INCLUDE_DIR)
return()
endif()
set(quiet ${${PREFIX}_FIND_QUIETLY})
# Process optional arguments
foreach(arg ${ARGN})
if (arg STREQUAL "QUIET")
set(quiet TRUE)
else()
message(AUTHOR_WARNING "Unknown argument ${arg} to libfind_version_header ignored.")
endif()
endforeach()
# Read the header and parse for version number
set(filename "${${PREFIX}_INCLUDE_DIR}/${VERSION_H}")
if (NOT EXISTS ${filename})
if (NOT quiet)
message(AUTHOR_WARNING "Unable to find ${${PREFIX}_INCLUDE_DIR}/${VERSION_H}")
endif()
return()
endif()
file(READ "${filename}" header)
string(REGEX REPLACE ".*#[ \t]*define[ \t]*${DEFINE_NAME}[ \t]*\"([^\n]*)\".*" "\\1" match "${header}")
# No regex match?
if (match STREQUAL header)
if (NOT quiet)
message(AUTHOR_WARNING "Unable to find \#define ${DEFINE_NAME} \"<version>\" from ${${PREFIX}_INCLUDE_DIR}/${VERSION_H}")
endif()
return()
endif()
# Export the version string
set(${PREFIX}_VERSION "${match}" PARENT_SCOPE)
endfunction()
# Do the final processing once the paths have been detected.
# If include dirs are needed, ${PREFIX}_PROCESS_INCLUDES should be set to contain
# all the variables, each of which contain one include directory.
# Ditto for ${PREFIX}_PROCESS_LIBS and library files.
# Will set ${PREFIX}_FOUND, ${PREFIX}_INCLUDE_DIRS and ${PREFIX}_LIBRARIES.
# Also handles errors in case library detection was required, etc.
function (libfind_process PREFIX)
# Skip processing if already processed during this configuration run
if (${PREFIX}_FOUND)
return()
endif()
set(found TRUE) # Start with the assumption that the package was found
# Did we find any files? Did we miss includes? These are for formatting better error messages.
set(some_files FALSE)
set(missing_headers FALSE)
# Shorthands for some variables that we need often
set(quiet ${${PREFIX}_FIND_QUIETLY})
set(required ${${PREFIX}_FIND_REQUIRED})
set(exactver ${${PREFIX}_FIND_VERSION_EXACT})
set(findver "${${PREFIX}_FIND_VERSION}")
set(version "${${PREFIX}_VERSION}")
# Lists of config option names (all, includes, libs)
unset(configopts)
set(includeopts ${${PREFIX}_PROCESS_INCLUDES})
set(libraryopts ${${PREFIX}_PROCESS_LIBS})
# Process deps to add to
foreach (i ${PREFIX} ${${PREFIX}_DEPENDENCIES})
if (DEFINED ${i}_INCLUDE_OPTS OR DEFINED ${i}_LIBRARY_OPTS)
# The package seems to export option lists that we can use, woohoo!
list(APPEND includeopts ${${i}_INCLUDE_OPTS})
list(APPEND libraryopts ${${i}_LIBRARY_OPTS})
else()
# If plural forms don't exist or they equal singular forms
if ((NOT DEFINED ${i}_INCLUDE_DIRS AND NOT DEFINED ${i}_LIBRARIES) OR
(${i}_INCLUDE_DIR STREQUAL ${i}_INCLUDE_DIRS AND ${i}_LIBRARY STREQUAL ${i}_LIBRARIES))
# Singular forms can be used
if (DEFINED ${i}_INCLUDE_DIR)
list(APPEND includeopts ${i}_INCLUDE_DIR)
endif()
if (DEFINED ${i}_LIBRARY)
list(APPEND libraryopts ${i}_LIBRARY)
endif()
else()
# Oh no, we don't know the option names
message(FATAL_ERROR "We couldn't determine config variable names for ${i} includes and libs. Aieeh!")
endif()
endif()
endforeach()
if (includeopts)
list(REMOVE_DUPLICATES includeopts)
endif()
if (libraryopts)
list(REMOVE_DUPLICATES libraryopts)
endif()
string(REGEX REPLACE ".*[ ;]([^ ;]*(_INCLUDE_DIRS|_LIBRARIES))" "\\1" tmp "${includeopts} ${libraryopts}")
if (NOT tmp STREQUAL "${includeopts} ${libraryopts}")
message(AUTHOR_WARNING "Plural form ${tmp} found in config options of ${PREFIX}. This works as before but is now deprecated. Please only use singular forms INCLUDE_DIR and LIBRARY, and update your find scripts for LibFindMacros > 2.0 automatic dependency system (most often you can simply remove the PROCESS variables entirely).")
endif()
# Include/library names separated by spaces (notice: not CMake lists)
unset(includes)
unset(libs)
# Process all includes and set found false if any are missing
foreach (i ${includeopts})
list(APPEND configopts ${i})
if (NOT "${${i}}" STREQUAL "${i}-NOTFOUND")
list(APPEND includes "${${i}}")
else()
set(found FALSE)
set(missing_headers TRUE)
endif()
endforeach()
# Process all libraries and set found false if any are missing
foreach (i ${libraryopts})
list(APPEND configopts ${i})
if (NOT "${${i}}" STREQUAL "${i}-NOTFOUND")
list(APPEND libs "${${i}}")
else()
set (found FALSE)
endif()
endforeach()
# Version checks
if (found AND findver)
if (NOT version)
message(WARNING "The find module for ${PREFIX} does not provide version information, so we'll just assume that it is OK. Please fix the module or remove package version requirements to get rid of this warning.")
elseif (version VERSION_LESS findver OR (exactver AND NOT version VERSION_EQUAL findver))
set(found FALSE)
set(version_unsuitable TRUE)
endif()
endif()
# If all-OK, hide all config options, export variables, print status and exit
if (found)
foreach (i ${configopts})
mark_as_advanced(${i})
endforeach()
if (NOT quiet)
message(STATUS "Found ${PREFIX} ${${PREFIX}_VERSION}")
if (LIBFIND_DEBUG)
message(STATUS " ${PREFIX}_DEPENDENCIES=${${PREFIX}_DEPENDENCIES}")
message(STATUS " ${PREFIX}_INCLUDE_OPTS=${includeopts}")
message(STATUS " ${PREFIX}_INCLUDE_DIRS=${includes}")
message(STATUS " ${PREFIX}_LIBRARY_OPTS=${libraryopts}")
message(STATUS " ${PREFIX}_LIBRARIES=${libs}")
endif()
endif()
set (${PREFIX}_INCLUDE_OPTS ${includeopts} PARENT_SCOPE)
set (${PREFIX}_LIBRARY_OPTS ${libraryopts} PARENT_SCOPE)
set (${PREFIX}_INCLUDE_DIRS ${includes} PARENT_SCOPE)
set (${PREFIX}_LIBRARIES ${libs} PARENT_SCOPE)
set (${PREFIX}_FOUND TRUE PARENT_SCOPE)
return()
endif()
# Format messages for debug info and the type of error
set(vars "Relevant CMake configuration variables:\n")
foreach (i ${configopts})
mark_as_advanced(CLEAR ${i})
set(val ${${i}})
if ("${val}" STREQUAL "${i}-NOTFOUND")
set (val "<not found>")
elseif (val AND NOT EXISTS ${val})
set (val "${val} (does not exist)")
else()
set(some_files TRUE)
endif()
set(vars "${vars} ${i}=${val}\n")
endforeach()
set(vars "${vars}You may use CMake GUI, cmake -D or ccmake to modify the values. Delete CMakeCache.txt to discard all values and force full re-detection if necessary.\n")
if (version_unsuitable)
set(msg "${PREFIX} ${${PREFIX}_VERSION} was found but")
if (exactver)
set(msg "${msg} only version ${findver} is acceptable.")
else()
set(msg "${msg} version ${findver} is the minimum requirement.")
endif()
else()
if (missing_headers)
set(msg "We could not find development headers for ${PREFIX}. Do you have the necessary dev package installed?")
elseif (some_files)
set(msg "We only found some files of ${PREFIX}, not all of them. Perhaps your installation is incomplete or maybe we just didn't look in the right place?")
if(findver)
set(msg "${msg} This could also be caused by incompatible version (if it helps, at least ${PREFIX} ${findver} should work).")
endif()
else()
set(msg "We were unable to find package ${PREFIX}.")
endif()
endif()
# Fatal error out if REQUIRED
if (required)
set(msg "REQUIRED PACKAGE NOT FOUND\n${msg} This package is REQUIRED and you need to install it or adjust CMake configuration in order to continue building ${CMAKE_PROJECT_NAME}.")
message(FATAL_ERROR "${msg}\n${vars}")
endif()
# Otherwise just print a nasty warning
if (NOT quiet)
message(WARNING "WARNING: MISSING PACKAGE\n${msg} This package is NOT REQUIRED and you may ignore this warning but by doing so you may miss some functionality of ${CMAKE_PROJECT_NAME}. \n${vars}")
endif()
endfunction()

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,36 @@
#
# Macros for processing ActiveX and COM controls with ActiveQt
#
if (WIN32)
include (CMakeParseArguments)
find_program (DUMPCPP_Executable dumpcpp.exe)
# wrap_ax_server (outfiles inputfile ...)
function (WRAP_AX_SERVER outfiles)
set (options)
set (oneValueArgs)
set (multiValueArgs OPTIONS)
cmake_parse_arguments (_WRAP_AX_SERVER "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
set (ax_server_files ${_WRAP_AX_SERVER_UNPARSED_ARGUMENTS})
set (ax_server_options ${_WRAP_AX_SERVER_OPTIONS})
foreach (it ${ax_server_files})
get_filename_component (outfile ${it} NAME_WE)
get_filename_component (infile ${it} ABSOLUTE)
set (outfile ${CMAKE_CURRENT_BINARY_DIR}/${outfile})
add_custom_command (
OUTPUT ${outfile}.h ${outfile}.cpp
COMMAND ${DUMPCPP_Executable}
ARGS ${ax_server_options} -o "${outfile}" "${infile}"
MAIN_DEPENDENCY ${infile} VERBATIM)
list (APPEND ${outfiles} ${outfile}.cpp)
endforeach()
set(${outfiles} ${${outfiles}} PARENT_SCOPE)
endfunction ()
endif (WIN32)

View File

@ -0,0 +1,94 @@
#pragma once
#ifndef VERSION_INFO_@PRODUCT_NAME@_H__
#define VERSION_INFO_@PRODUCT_NAME@_H__
#include "scs_version.h"
#ifndef PRODUCT_VERSION_MAJOR
#define PRODUCT_VERSION_MAJOR @PRODUCT_VERSION_MAJOR@
#endif
#ifndef PRODUCT_VERSION_MINOR
#define PRODUCT_VERSION_MINOR @PRODUCT_VERSION_MINOR@
#endif
#ifndef PRODUCT_VERSION_PATCH
#define PRODUCT_VERSION_PATCH @PRODUCT_VERSION_PATCH@
#endif
#ifndef PRODUCT_VERSION_TWEAK
#define PRODUCT_VERSION_TWEAK @PRODUCT_VERSION_TWEAK@
#endif
#ifndef PRODUCT_VERSION_REVISION
#define PRODUCT_VERSION_REVISION @PRODUCT_VERSION_REVISION@
#endif
#ifndef FILE_VERSION_MAJOR
#define FILE_VERSION_MAJOR @PRODUCT_VERSION_MAJOR@
#endif
#ifndef FILE_VERSION_MINOR
#define FILE_VERSION_MINOR @PRODUCT_VERSION_MINOR@
#endif
#ifndef FILE_VERSION_PATCH
#define FILE_VERSION_PATCH @PRODUCT_VERSION_PATCH@
#endif
#ifndef FILE_VERSION_TWEAK
#define FILE_VERSION_TWEAK @PRODUCT_VERSION_TWEAK@
#endif
#ifndef TO_STRING__
#define TO_STRING_IMPL__(x) #x
#define TO_STRING__(x) TO_STRING_IMPL__(x)
#endif
#define PRODUCT_VERSION_MAJOR_MINOR_STR TO_STRING__ (PRODUCT_VERSION_MAJOR) "." TO_STRING__(PRODUCT_VERSION_MINOR)
#define PRODUCT_VERSION_MAJOR_MINOR_PATCH_STR PRODUCT_VERSION_MAJOR_MINOR_STR "." TO_STRING__ (PRODUCT_VERSION_PATCH)
#define PRODUCT_VERSION_FULL_STR PRODUCT_VERSION_MAJOR_MINOR_PATCH_STR TO_STRING__ (PRODUCT_VERSION_REVISION)
#define PRODUCT_VERSION_RESOURCE PRODUCT_VERSION_MAJOR,PRODUCT_VERSION_MINOR,PRODUCT_VERSION_PATCH,PRODUCT_VERSION_TWEAK
#define PRODUCT_VERSION_RESOURCE_STR PRODUCT_VERSION_FULL_STR " " SCS_VERSION_STR "\0"
#define FILE_VERSION_MAJOR_MINOR_STR TO_STRING__ (FILE_VERSION_MAJOR) "." TO_STRING__ (FILE_VERSION_MINOR)
#define FILE_VERSION_MAJOR_MINOR_PATCH_STR FILE_VERSION_MAJOR_MINOR_STR "." TO_STRING__ (FILE_VERSION_PATCH)
#define FILE_VERSION_FULL_STR FILE_VERSION_MAJOR_MINOR_PATCH_STR TO_STRING__ (PRODUCT_VERSION_REVISION)
#define FILE_VERSION_RESOURCE FILE_VERSION_MAJOR,FILE_VERSION_MINOR,FILE_VERSION_PATCH,FILE_VERSION_TWEAK
#define FILE_VERSION_RESOURCE_STR FILE_VERSION_FULL_STR "\0"
#ifndef PRODUCT_ICON
#define PRODUCT_ICON "@PRODUCT_ICON@"
#endif
#ifndef PRODUCT_COMMENTS
#define PRODUCT_COMMENTS "@PRODUCT_COMMENTS@\0"
#endif
#ifndef PRODUCT_VENDOR_NAME
#define PRODUCT_VENDOR_NAME "@PRODUCT_VENDOR_NAME@\0"
#endif
#ifndef PRODUCT_LEGAL_COPYRIGHT
#define PRODUCT_LEGAL_COPYRIGHT "@PRODUCT_LEGAL_COPYRIGHT@\0"
#endif
#ifndef PRODUCT_FILE_DESCRIPTION
#define PRODUCT_FILE_DESCRIPTION "@PRODUCT_FILE_DESCRIPTION@\0"
#endif
#ifndef PRODUCT_INTERNAL_NAME
#define PRODUCT_INTERNAL_NAME "@PRODUCT_NAME@\0"
#endif
#ifndef PRODUCT_ORIGINAL_FILENAME
#define PRODUCT_ORIGINAL_FILENAME "@PRODUCT_ORIGINAL_FILENAME@\0"
#endif
#ifndef PRODUCT_BUNDLE
#define PRODUCT_BUNDLE "@PRODUCT_BUNDLE@\0"
#endif
#endif

View File

@ -0,0 +1,73 @@
#include "VersionInfo_@PRODUCT_NAME@.h"
#ifdef RC_INVOKED
#if defined (__MINGW64__) || defined (__MINGW32__)
/* MinGW-w64 or MinGW */
#if defined (__has_include) && __has_include (<windres.h>)
#include <windres.h>
#else
#include <afxres.h>
#include <winresrc.h>
#endif
#else
/* MSVC, Windows SDK */
#include <winres.h>
#endif
#ifndef _NDEBUG
#define PRODUCT_DEBUGFLAG 0x0L
#else
#define PRODUCT_DEBUGFLAG VS_FF_DEBUG
#endif
#ifdef SCS_VERSION_IS_DIRTY
#define PRODUCT_SPECIALFLAG VS_FF_SPECIALBUILD
#else
#define PRODUCT_SPECIALFLAG 0x0L
#endif
#if PRODUCT_PRERELEASE
#define PRODUCT_PRERELEASEFLAG VS_FF_PRERELEASE
#else
#define PRODUCT_PRERELEASEFLAG 0x0L
#endif
IDI_ICON1 ICON DISCARDABLE PRODUCT_ICON
LANGUAGE LANG_ENGLISH, SUBLANG_NEUTRAL
VS_VERSION_INFO VERSIONINFO
FILEVERSION FILE_VERSION_RESOURCE
PRODUCTVERSION PRODUCT_VERSION_RESOURCE
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
FILEFLAGS (PRODUCT_DEBUGFLAG | PRODUCT_SPECIALFLAG | PRODUCT_PRERELEASEFLAG)
FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_APP
FILESUBTYPE 0x0L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0"
BEGIN
VALUE "Comments", PRODUCT_COMMENTS
VALUE "CompanyName", PRODUCT_VENDOR_NAME
VALUE "FileDescription", PRODUCT_FILE_DESCRIPTION
VALUE "FileVersion", FILE_VERSION_RESOURCE_STR
VALUE "LegalCopyright", PRODUCT_LEGAL_COPYRIGHT
VALUE "ProductName", PRODUCT_BUNDLE
VALUE "ProductVersion", PRODUCT_VERSION_RESOURCE_STR
VALUE "InternalName", PRODUCT_INTERNAL_NAME
VALUE "OriginalFileName", PRODUCT_ORIGINAL_FILENAME
#if SCS_VERSION_IS_DIRTY
VALUE "SpecialBuild", SCS_VERSION_STR "\0"
#endif
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x0409, 0x04b /* en-US Unicode */
END
END
#endif

View File

@ -0,0 +1,167 @@
include (CMakeParseArguments)
set (_THIS_MODULE_BASE_DIR "${CMAKE_CURRENT_LIST_DIR}")
# generate_product_version() function
#
# This function uses VersionInfo.in template file and VersionResource.rc file
# to generate WIN32 resource with version information and general resource strings.
#
# Usage:
# generate_product_version(
# SomeOutputResourceVariable
# NAME MyVersinedTarget
# ICON ${PATH_TO_ICON}
# VERSION_MAJOR 2
# VERSION_MINOR 3
# VERSION_PATCH 1
# VERSION_REVISION sha1
# )
#
# You can use generated resource for your executable targets:
# add_executable(target-name ${target-files} ${SomeOutputResourceVariable})
# add_library (target-name SHARED ${target-files} ${SomeOutputResourceVariable})
#
# You can specify resource strings in arguments:
# NAME - name of executable (no defaults, ex: Microsoft Word)
# BUNDLE - bundle (${PROJECT_NAME} or ${NAME} is default, ex: Microsoft Office)
# ICON - path to application icon, default: ${CMAKE_SOURCE_DIR}/icons/windows-icons/${NAME}.ico
# VERSION_MAJOR - default: 1
# VERSION_MINOR - deafult: 0
# VERSION_PATCH - deafult: 0
# VERSION_REVISION - deafult: 0
# VENDOR_NAME - your vendor name, default: ${PROJECT_VENDOR}
# LEGAL_COPYRIGHT - default: ${PROJECT_COPYRIGHT}
# COMMENTS - default: ${PROJECT_DESCRIPTION}
# ORIGINAL_FILENAME - default: ${NAME}
# INTERNAL_NAME - default: ${NAME}
# FILE_DESCRIPTION - default: ${COMMENTS}
function(generate_version_info outfiles)
set (options)
set (oneValueArgs
NAME
BUNDLE
ICON
VERSION_MAJOR
VERSION_MINOR
VERSION_PATCH
VERSION_REVISION
VENDOR_NAME
LEGAL_COPYRIGHT
COMMENTS
ORIGINAL_FILENAME
INTERNAL_NAME
FILE_DESCRIPTION)
set (multiValueArgs)
cmake_parse_arguments(PRODUCT "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
if (NOT PRODUCT_BUNDLE)
if (PROJECT_NAME)
set (PRODUCT_BUNDLE "${PROJECT_NAME}")
else ()
set (PRODUCT_BUNDLE "${PRODUCT_NAME}")
endif ()
endif()
if (NOT PRODUCT_ICON)
set (PRODUCT_ICON "${CMAKE_SOURCE_DIR}/icons/windows-icons/${PRODUCT_NAME}.ico")
endif ()
if (NOT PRODUCT_VERSION_MAJOR)
if (PROJECT_VERSION_MAJOR)
set (PRODUCT_VERSION_MAJOR ${PROJECT_VERSION_MAJOR})
else ()
set (PRODUCT_VERSION_MAJOR 1)
endif ()
else ()
if (NOT ${PRODUCT_VERSION_MAJOR} MATCHES "^[0-9]+$")
message (FATAL_ERROR "Numeric major version number required")
endif ()
endif ()
if (NOT PRODUCT_VERSION_MINOR)
if (PROJECT_VERSION_MINOR)
set (PRODUCT_VERSION_MINOR ${PROJECT_VERSION_MINOR})
else ()
set (PRODUCT_VERSION_MINOR 0)
endif ()
else ()
if (NOT ${PRODUCT_VERSION_MINOR} MATCHES "^[0-9]+$")
message (FATAL_ERROR "Numeric minor version number required")
endif ()
endif()
if (NOT PRODUCT_VERSION_PATCH)
if (PROJECT_VERSION_PATCH)
set (PRODUCT_VERSION_PATCH ${PROJECT_VERSION_PATCH})
else ()
set (PRODUCT_VERSION_PATCH 0)
endif ()
else ()
if (NOT ${PRODUCT_VERSION_PATCH} MATCHES "^[0-9]+$")
message (FATAL_ERROR "Numeric patch version number required")
endif ()
endif()
if (NOT PRODUCT_VERSION_TWEAK)
if (PROJECT_VERSION_TWEAK)
set (PRODUCT_VERSION_TWEAK ${PROJECT_VERSION_TWEAK})
else ()
set (PRODUCT_VERSION_TWEAK 0)
endif ()
else()
if (NOT ${PRODUCT_VERSION_TWEAK} MATCHES "^[0-9]+$")
message (FATAL_ERROR "Numeric tweak version number required")
endif()
endif()
if (NOT PROJECT_VERSION_REVISION AND BUILD_TYPE_REVISION)
set (PRODUCT_VERSION_REVISION ${BUILD_TYPE_REVISION})
endif ()
if (NOT PRODUCT_VENDOR_NAME AND PROJECT_VENDOR)
set (PRODUCT_VENDOR_NAME ${PROJECT_VENDOR})
endif ()
if (NOT PRODUCT_LEGAL_COPYRIGHT)
if (PROJECT_COPYRIGHT)
set (PRODUCT_LEGAL_COPYRIGHT ${PROJECT_COPYRIGHT})
else ()
string(TIMESTAMP PRODUCT_CURRENT_YEAR "%Y")
set(PRODUCT_LEGAL_COPYRIGHT "${PRODUCT_VENDOR_NAME} (C) Copyright ${PRODUCT_CURRENT_YEAR}")
endif ()
endif()
if (NOT PRODUCT_COMMENTS)
if (PROJECT_DESCRIPTION)
set(PRODUCT_COMMENTS ${PROJECT_DESCRIPTION})
else ()
set(PRODUCT_COMMENTS "${PRODUCT_NAME} v${PRODUCT_VERSION_MAJOR}.${PRODUCT_VERSION_MINOR}.${PRODUCT_VERSION_PATCH}")
endif ()
endif()
if (NOT PRODUCT_ORIGINAL_FILENAME)
set(PRODUCT_ORIGINAL_FILENAME "${PRODUCT_NAME}")
endif()
if (NOT PRODUCT_INTERNAL_NAME)
set(PRODUCT_INTERNAL_NAME "${PRODUCT_NAME}")
endif()
if (NOT PRODUCT_FILE_DESCRIPTION)
set(PRODUCT_FILE_DESCRIPTION "${PRODUCT_COMMENTS}")
endif()
set (_VersionInfoFile VersionInfo_${PRODUCT_NAME}.h)
set (_VersionResourceFile VersionResource_${PRODUCT_NAME}.rc)
configure_file(
${_THIS_MODULE_BASE_DIR}/VersionInfo.h.in
${_VersionInfoFile}
@ONLY)
configure_file(
${_THIS_MODULE_BASE_DIR}/VersionResource.rc.in
${_VersionResourceFile}
@ONLY)
list(APPEND ${outfiles} ${CMAKE_CURRENT_BINARY_DIR}/${_VersionInfoFile} ${CMAKE_CURRENT_BINARY_DIR}/${_VersionResourceFile})
set (${outfiles} ${${outfiles}} PARENT_SCOPE)
endfunction()

View File

@ -0,0 +1,42 @@
include (CMakeParseArguments)
# set_build_type() function
#
# Configure the output artefacts and their names for development,
# Release Candidate (RC), or General Availability (GA) build type.
#
# Usage:
# set_build_type ()
# set_build_type (RC n)
# set_build_type (GA)
#
# With no arguments or with RC 0 a development is specified. The
# variable BUILD_TYPE_REVISION is set to "-devel".
#
# With RC n with n>0 specifies a Release Candidate build. The
# variable BUIlD_TYPE_REVISION is set to "-rcn".
#
# With GA a General Availability release is specified and the
# variable BUIlD_TYPE_REVISION is unset.
#
macro (set_build_type)
set (options GA)
set (oneValueArgs RC)
set (multiValueArgs)
cmake_parse_arguments (BUILD_TYPE "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
if (BUILD_TYPE_UNPARSED_ARGUMENTS)
message (FATAL_ERROR "Unrecognized macro arguments: \"${BUILD_TYPE_UNPARSED_ARGUMENTS}\"")
endif ()
if (BUILD_TYPE_GA AND BUILD_TYPE_RC)
message (FATAL_ERROR "Only specify one build type from RC or GA.")
endif ()
if (NOT BUILD_TYPE_GA)
if (BUILD_TYPE_RC)
set (BUILD_TYPE_REVISION "-rc${BUILD_TYPE_RC}")
else ()
set (BUILD_TYPE_REVISION "-devel")
endif ()
endif ()
message (STATUS "Building ${PROJECT_NAME} v${PROJECT_VERSION}${BUILD_TYPE_REVISION}")
endmacro ()

View File

@ -0,0 +1,22 @@
if (NOT EXISTS "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt")
message (FATAL_ERROR "Cannot find install manifest: \"@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt\"")
endif ()
file (READ "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt" files)
string (REGEX REPLACE "\n" ";" files "${files}")
foreach (file ${files})
message (STATUS "Uninstalling \"$ENV{DESTDIR}${file}\"")
if (EXISTS "$ENV{DESTDIR}${file}")
exec_program (
"@CMAKE_COMMAND@" ARGS "-E remove \"$ENV{DESTDIR}${file}\""
OUTPUT_VARIABLE rm_out
RETURN_VALUE rm_retval
)
if ("${rm_retval}" STREQUAL 0)
else ()
message (FATAL_ERROR "Problem when removing \"$ENV{DESTDIR}${file}\"")
endif ()
else ()
message (STATUS "File \"$ENV{DESTDIR}${file}\" does not exist.")
endif ()
endforeach ()

85
CMake/getsvn.cmake Normal file
View File

@ -0,0 +1,85 @@
message (STATUS "Checking for revision information")
set (__scs_header_file ${BINARY_DIR}/scs_version.h.txt)
file (WRITE ${__scs_header_file} "/* SCS version information */\n\n")
if (EXISTS "${SOURCE_DIR}/.svn")
message (STATUS "Checking for Subversion revision information")
find_package (Subversion QUIET REQUIRED)
# the FindSubversion.cmake module is part of the standard distribution
include (FindSubversion)
# extract working copy information for SOURCE_DIR into MY_XXX variables
Subversion_WC_INFO (${SOURCE_DIR} MY)
message ("${MY_WC_INFO}")
# determine if the working copy has outstanding changes
execute_process (COMMAND ${Subversion_SVN_EXECUTABLE} status ${SOURCE_DIR}
OUTPUT_FILE "${BINARY_DIR}/svn_status.txt"
OUTPUT_STRIP_TRAILING_WHITESPACE)
file (STRINGS "${BINARY_DIR}/svn_status.txt" __svn_changes
REGEX "^[^?].*$"
)
set (SCS_VERSION "r${MY_WC_LAST_CHANGED_REV}")
if (__svn_changes)
message (STATUS "Source tree based on revision ${SCS_VERSION} appears to have local changes")
file (APPEND ${__scs_header_file} "#define SCS_VERSION_IS_DIRTY 1\n")
set (SCS_VERSION_STR "${SCS_VERSION}-dirty")
foreach (__svn_change ${__svn_changes})
message (STATUS "${__svn_change}")
endforeach (__svn_change ${__svn_changes})
else ()
set (SCS_VERSION_STR "${SCS_VERSION}")
endif ()
message (STATUS "${SOURCE_DIR} contains a .svn and is revision ${SCS_VERSION}")
elseif (EXISTS "${SOURCE_DIR}/.git")
if (EXISTS "${SOURCE_DIR}/.git/svn/.metadata") # try git-svn
message (STATUS "Checking for Subversion revision information using git-svn")
include (${SOURCE_DIR}/CMake/Modules/FindGitSubversion.cmake)
# extract working copy information for SOURCE_DIR into MY_XXX variables
GitSubversion_WC_INFO (${SOURCE_DIR} MY)
message ("${MY_WC_INFO}")
set (SCS_VERSION "r${MY_WC_LAST_CHANGED_REV}")
# try and determine if the working copy has outstanding changes
execute_process (COMMAND ${GIT_EXECUTABLE} --git-dir=${SOURCE_DIR}/.git --work-tree=${SOURCE_DIR} svn dcommit --dry-run
RESULT_VARIABLE __git_svn_status
OUTPUT_FILE "${BINARY_DIR}/svn_status.txt"
ERROR_QUIET
OUTPUT_STRIP_TRAILING_WHITESPACE)
file (STRINGS "${BINARY_DIR}/svn_status.txt" __svn_changes
REGEX "^diff-tree"
)
if ((NOT ${__git_svn_status} EQUAL 0) OR __svn_changes)
message (STATUS "Source tree based on revision ${MY_WC_LAST_CHANGED_REV} appears to have local changes")
file (APPEND ${__scs_header_file} "#define SCS_VERSION_IS_DIRTY 1\n")
set (SCS_VERSION_STR "${SCS_VERSION}-dirty")
else ()
set (SCS_VERSION_STR "${SCS_VERSION}")
endif ()
else ()
#
# try git
#
message (STATUS "Checking for gitrevision information")
include (${SOURCE_DIR}/CMake/Modules/GetGitRevisionDescription.cmake)
get_git_head_revision (${SOURCE_DIR} GIT_REFSPEC GIT_SHA1)
git_local_changes (${SOURCE_DIR} GIT_SANITARY)
string (SUBSTRING "${GIT_SHA1}" 0 6 SCS_VERSION)
if ("${GIT_SANITARY}" STREQUAL "DIRTY")
message (STATUS "Source tree based on revision ${GIT_REFSPEC} ${SCS_VERSION} appears to have local changes")
file (APPEND ${__scs_header_file} "#define SCS_VERSION_IS_DIRTY 1\n")
set (SCS_VERSION_STR "${SCS_VERSION}-dirty")
execute_process (COMMAND ${GIT_EXECUTABLE} --git-dir=${SOURCE_DIR}/.git --work-tree=${SOURCE_DIR} status
ERROR_QUIET
OUTPUT_STRIP_TRAILING_WHITESPACE)
else ()
set (SCS_VERSION_STR "${SCS_VERSION}")
endif ()
message (STATUS "refspec: ${GIT_REFSPEC} - SHA1: ${SCS_VERSION}")
endif ()
else()
message (STATUS "No SCS found")
endif ()
file (APPEND ${__scs_header_file} "#define SCS_VERSION ${SCS_VERSION}\n")
file (APPEND ${__scs_header_file} "#define SCS_VERSION_STR \"${SCS_VERSION_STR}\"\n")
# copy the file to the final header only if the version changes
# reduces needless rebuilds
execute_process (COMMAND ${CMAKE_COMMAND} -E copy_if_different ${__scs_header_file} "${OUTPUT_DIR}/scs_version.h")
file (REMOVE ${__scs_header_file})

View File

@ -0,0 +1,98 @@
# This file is configured at cmake time, and loaded at cpack time.
# To pass variables to cpack from cmake, they must be configured
# in this file.
set (CPACK_PACKAGE_VENDOR "@PROJECT_VENDOR@")
set (CPACK_PACKAGE_CONTACT "@PROJECT_CONTACT@")
set (CPACK_RESOURCE_FILE_LICENSE "@PROJECT_SOURCE_DIR@/COPYING")
set (CPACK_PACKAGE_INSTALL_DIRECTORY ${CPACK_PACKAGE_NAME})
set (CPACK_PACKAGE_EXECUTABLES wsjtx "@PROJECT_NAME@")
set (CPACK_CREATE_DESKTOP_LINKS wsjtx)
set (CPACK_STRIP_FILES TRUE)
#
# components
#
#set (CPACK_COMPONENTS_ALL runtime)
#set (CPACK_COMPONENT_RUNTIME_DISPLAY_NAME "@PROJECT_NAME@ Application")
#set (CPACK_COMPONENT_RUNTIME_DESCRIPTION "@WSJTX_DESCRIPTION_SUMMARY@")
if (CPACK_GENERATOR MATCHES "NSIS")
set (CPACK_STRIP_FILES FALSE) # breaks Qt packaging on Windows
set (CPACK_NSIS_INSTALL_ROOT "C:\\WSJT")
# set the install/unistall icon used for the installer itself
# There is a bug in NSI that does not handle full unix paths properly.
set (CPACK_NSIS_MUI_ICON "@PROJECT_SOURCE_DIR@/icons/windows-icons\\wsjtx.ico")
set (CPACK_NSIS_MUI_UNIICON "@PROJECT_SOURCE_DIR@/icons/windows-icons\\wsjtx.ico")
# set the package header icon for MUI
set (CPACK_PACKAGE_ICON "@PROJECT_SOURCE_DIR@/icons/windows-icons\\installer_logo.bmp")
# tell cpack to create links to the doc files
set (CPACK_NSIS_MENU_LINKS
"@PROJECT_MANUAL_DIRECTORY_URL@/@PROJECT_MANUAL@" "@PROJECT_NAME@ Documentation"
"@PROJECT_HOMEPAGE@" "@PROJECT_NAME@ Web Site"
)
# Use the icon from wsjtx for add-remove programs
set (CPACK_NSIS_INSTALLED_ICON_NAME "bin\\\\wsjtx.exe")
set (CPACK_NSIS_DISPLAY_NAME "${CPACK_PACKAGE_DESCRIPTION_SUMMARY}")
set (CPACK_NSIS_HELP_LINK "@PROJECT_MANUAL_DIRECTORY_URL@/@PROJECT_MANUAL@")
set (CPACK_NSIS_URL_INFO_ABOUT "@PROJECT_HOMEPAGE@")
set (CPACK_NSIS_CONTACT "${CPACK_PACKAGE_CONTACT}")
set (CPACK_NSIS_MUI_FINISHPAGE_RUN "wsjtx.exe")
set (CPACK_NSIS_MODIFY_PATH ON)
# Setup the installer and uninstall VersionInfo resource
set (CPACK_NSIS_EXTRA_DEFINES
"VIProductVersion @PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.@PROJECT_VERSION_PATCH@.@PROJECT_VERSION_TWEAK@
VIFileVersion @PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.@PROJECT_VERSION_PATCH@.@PROJECT_VERSION_TWEAK@
VIAddVersionKey /LANG=0 \"ProductName\" \"@PROJECT_NAME@\"
VIAddVersionKey /LANG=0 \"ProductVersion\" \"v@PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.@PROJECT_VERSION_PATCH@@BUILD_TYPE_REVISION@\"
VIAddVersionKey /LANG=0 \"Comments\" \"@PROJECT_DESCRIPTION@\"
VIAddVersionKey /LANG=0 \"CompanyName\" \"@PROJECT_VENDOR@\"
VIAddVersionKey /LANG=0 \"LegalCopyright\" \"@PROJECT_COPYRIGHT@\"
VIAddVersionKey /LANG=0 \"FileDescription\" \"@PROJECT_NAME@ Installer\"
VIAddVersionKey /LANG=0 \"FileVersion\" \"v@PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.@PROJECT_VERSION_PATCH@@BUILD_TYPE_REVISION@\""
)
endif ()
if ("${CPACK_GENERATOR}" STREQUAL "PackageMaker")
set (CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_FILE_NAME}-pkg")
set (CPACK_PACKAGE_DEFAULT_LOCATION "/Applications")
set (CPACK_PACKAGING_INSTALL_PREFIX "/")
endif ()
if ("${CPACK_GENERATOR}" STREQUAL "DragNDrop")
set (CPACK_DMG_VOLUME_NAME "@PROJECT_BUNDLE_NAME@")
set (CPACK_DMG_BACKGROUND_IMAGE "@PROJECT_SOURCE_DIR@/icons/Darwin/DragNDrop Background.png")
set (CPACK_DMG_DS_STORE "@PROJECT_SOURCE_DIR@/Darwin/wsjtx_DMG.DS_Store")
set (CPACK_BUNDLE_NAME "@WSJTX_BUNDLE_NAME@")
set (CPACK_PACKAGE_ICON "@PROJECT_BINARY_DIR@/wsjtx.icns")
set (CPACK_BUNDLE_ICON "@PROJECT_BINARY_DIR@/wsjtx.icns")
set (CPACK_BUNDLE_STARTUP_COMMAND "@PROJECT_SOURCE_DIR@/Mac-wsjtx-startup.sh")
endif ()
if ("${CPACK_GENERATOR}" STREQUAL "WIX")
# Reset CPACK_PACKAGE_VERSION to deal with WiX restriction.
# But the file names still use the full CMake_VERSION value:
set (CPACK_PACKAGE_FILE_NAME
"${CPACK_PACKAGE_NAME}-@wsjtx_VERSION@-${CPACK_SYSTEM_NAME}")
set (CPACK_SOURCE_PACKAGE_FILE_NAME
"${CPACK_PACKAGE_NAME}-@wsjtx_VERSION@-Source")
if (NOT CPACK_WIX_SIZEOF_VOID_P)
set (CPACK_WIX_SIZEOF_VOID_P "@CMAKE_SIZEOF_VOID_P@")
endif ()
endif ()
if ("${CPACK_GENERATOR}" STREQUAL "DEB")
set (CPACK_PACKAGE_FILE_NAME ${CPACK_PACKAGE_NAME}_${CPACK_PACKAGE_VERSION}_${CPACK_DEBIAN_PACKAGE_ARCHITECTURE})
set (CPACK_COMPONENTS_ALL ${CPACK_COMPONENTS_ALL} Debian)
endif ("${CPACK_GENERATOR}" STREQUAL "DEB")
if ("${CPACK_GENERATOR}" STREQUAL "RPM")
set (CPACK_PACKAGE_FILE_NAME ${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}.${CPACK_RPM_PACKAGE_ARCHITECTURE})
endif ("${CPACK_GENERATOR}" STREQUAL "RPM")
message (STATUS "CMAKE_INSTALL_PREFIX: ${CMAKE_INSTALL_PREFIX}")

1781
CMakeLists.txt Normal file

File diff suppressed because it is too large Load Diff

151
COPYING Normal file
View File

@ -0,0 +1,151 @@
GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works.
The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too.
When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.
Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions.
Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and modification follow.
TERMS AND CONDITIONS 0. Definitions. “This License” refers to version 3 of the GNU General Public License.
“Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.
“The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations.
To “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work.
A “covered work” means either the unmodified Program or a work based on the Program.
To “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.
To “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
1. Source Code. The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work.
A “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
The “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.
The “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work.
The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.
The Corresponding Source for a work in source code form is that same work.
2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.
When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.
4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”. c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.
6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:
a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.
A “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.
“Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.
If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).
The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.
7. Additional Terms. “Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way.
8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).
However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10.
9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License.
An “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.
11. Patents. A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”.
A contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.
In the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.
A patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such.
14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation.
If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.
Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.
15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
---------------------------

3237
Configuration.cpp Normal file

File diff suppressed because it is too large Load Diff

324
Configuration.hpp Normal file
View File

@ -0,0 +1,324 @@
#ifndef CONFIGURATION_HPP_
#define CONFIGURATION_HPP_
#include <QObject>
#include <QFont>
#include <QString>
#include "Radio.hpp"
#include "models/IARURegions.hpp"
#include "Audio/AudioDevice.hpp"
#include "Transceiver/Transceiver.hpp"
#include "pimpl_h.hpp"
class QSettings;
class QWidget;
class QAudioDeviceInfo;
class QDir;
class QNetworkAccessManager;
class Bands;
class FrequencyList_v2;
class StationList;
class QStringListModel;
class LotWUsers;
class DecodeHighlightingModel;
class LogBook;
//
// Class Configuration
//
// Encapsulates the control, access and, persistence of user defined
// settings for the wsjtx GUI. Setting values are accessed through a
// QDialog window containing concept orientated tab windows.
//
// Responsibilities
//
// Provides management of the CAT and PTT rig interfaces, providing
// control access via a minimal generic set of Qt slots and status
// updates via Qt signals. Internally the rig control capability is
// farmed out to a separate thread since many of the rig control
// functions are blocking.
//
// All user settings required by the wsjtx GUI are exposed through
// query methods. Settings only become visible once they have been
// accepted by the user which is done by clicking the "OK" button on
// the settings dialog.
//
// The QSettings instance passed to the constructor is used to read
// and write user settings.
//
// Pointers to three QAbstractItemModel objects are provided to give
// access to amateur band information, user working frequencies and,
// user operating band information. These porovide consistent data
// models that can be used in GUI lists or tables or simply queried
// for user defined bands, default operating frequencies and, station
// descriptions.
//
class Configuration final
: public QObject
{
Q_OBJECT
public:
using MODE = Transceiver::MODE;
using TransceiverState = Transceiver::TransceiverState;
using Frequency = Radio::Frequency;
using port_type = quint16;
enum DataMode {data_mode_none, data_mode_USB, data_mode_data};
Q_ENUM (DataMode)
enum Type2MsgGen {type_2_msg_1_full, type_2_msg_3_full, type_2_msg_5_only};
Q_ENUM (Type2MsgGen)
explicit Configuration (QNetworkAccessManager *, QDir const& temp_directory, QSettings * settings,
LogBook * logbook, QWidget * parent = nullptr);
~Configuration ();
void select_tab (int);
int exec ();
bool is_active () const;
QDir temp_dir () const;
QDir doc_dir () const;
QDir data_dir () const;
QDir writeable_data_dir () const;
QAudioDeviceInfo const& audio_input_device () const;
AudioDevice::Channel audio_input_channel () const;
QAudioDeviceInfo const& audio_output_device () const;
AudioDevice::Channel audio_output_channel () const;
// These query methods should be used after a call to exec() to
// determine if either the audio input or audio output stream
// parameters have changed. The respective streams should be
// re-opened if they return true.
bool restart_audio_input () const;
bool restart_audio_output () const;
QString my_callsign () const;
QString my_grid () const;
QString Field_Day_Exchange() const;
QString RTTY_Exchange() const;
void setEU_VHF_Contest();
QFont text_font () const;
QFont decoded_text_font () const;
qint32 id_interval () const;
qint32 ntrials() const;
qint32 aggressive() const;
qint32 RxBandwidth() const;
double degrade() const;
double txDelay() const;
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;
bool report_in_comments () const;
bool prompt_to_log () const;
bool autoLog() const;
bool decodes_from_top () const;
bool insert_blank () const;
bool DXCC () const;
bool ppfx() const;
bool clear_DX () const;
bool miles () const;
bool quick_call () const;
bool disable_TX_on_73 () const;
bool force_call_1st() const;
bool alternate_bindings() const;
int watchdog () const;
bool TX_messages () const;
bool split_mode () const;
bool enable_VHF_features () const;
bool decode_at_52s () const;
bool Tune_watchdog_disabled () const;
bool single_decode () const;
bool twoPass() const;
bool bFox() const;
bool bHound() const;
bool bLowSidelobes() const;
bool x2ToneSpacing() const;
bool x4ToneSpacing() const;
bool MyDx() const;
bool CQMyN() const;
bool NDxG() const;
bool NN() const;
bool EMEonly() const;
bool post_decodes () const;
QString opCall() const;
void opCall (QString const&);
QString udp_server_name () const;
port_type udp_server_port () const;
QStringList udp_interface_names () const;
int udp_TTL () const;
QString n1mm_server_name () const;
port_type n1mm_server_port () const;
bool valid_n1mm_info () const;
bool broadcast_to_n1mm() const;
bool lowSidelobes() const;
bool accept_udp_requests () const;
bool udpWindowToFront () const;
bool udpWindowRestore () const;
Bands * bands ();
Bands const * bands () const;
IARURegions::Region region () const;
FrequencyList_v2 * frequencies ();
FrequencyList_v2 const * frequencies () const;
StationList * stations ();
StationList const * stations () const;
QStringListModel * macros ();
QStringListModel const * macros () const;
QDir save_directory () const;
QDir azel_directory () const;
QString rig_name () const;
Type2MsgGen type_2_msg_gen () const;
bool pwrBandTxMemory () const;
bool pwrBandTuneMemory () const;
LotWUsers const& lotw_users () const;
DecodeHighlightingModel const& decode_highlighting () const;
bool highlight_by_mode () const;
bool highlight_only_fields () const;
bool include_WAE_entities () const;
bool highlight_73 () const;
void setSpecial_Hound();
void setSpecial_Fox();
void setSpecial_None();
bool highlight_DXcall () const;
bool highlight_DXgrid () const;
enum class SpecialOperatingActivity {NONE, NA_VHF, EU_VHF, FIELD_DAY, RTTY, WW_DIGI, ARRL_DIGI, FOX, HOUND};
SpecialOperatingActivity special_op_id () const;
struct CalibrationParams
{
CalibrationParams ()
: intercept {0.}
, slope_ppm {0.}
{
}
CalibrationParams (double the_intercept, double the_slope_ppm)
: intercept {the_intercept}
, slope_ppm {the_slope_ppm}
{
}
double intercept; // Hertz
double slope_ppm; // Hertz
};
// Temporarily enable or disable calibration adjustments.
void enable_calibration (bool = true);
// Set the calibration parameters and enable calibration corrections.
void set_calibration (CalibrationParams);
// Set the dynamic grid which is only used if configuration setting is enabled.
void set_location (QString const&);
// This method queries if a CAT and PTT connection is operational.
bool is_transceiver_online () const;
// Start the rig connection, safe and normal to call when rig is
// already open.
bool transceiver_online ();
// check if a real rig is configured
bool is_dummy_rig () const;
// Frequency resolution of the rig
//
// 0 - 1Hz
// 1 - 10Hz rounded
// -1 - 10Hz truncated
// 2 - 100Hz rounded
// -2 - 100Hz truncated
int transceiver_resolution () const;
// Close down connection to rig.
void transceiver_offline ();
// Set transceiver frequency in Hertz.
Q_SLOT void transceiver_frequency (Frequency);
// Setting a non zero TX frequency means split operation
// rationalise_mode means ensure TX uses same mode as RX.
Q_SLOT void transceiver_tx_frequency (Frequency = 0u);
// Set transceiver mode.
Q_SLOT void transceiver_mode (MODE);
// Set/unset PTT.
//
// Note that this must be called even if VOX PTT is selected since
// the "Emulate Split" mode requires PTT information to coordinate
// frequency changes.
Q_SLOT void transceiver_ptt (bool = true);
// Attempt to (re-)synchronise transceiver state.
//
// Force signal guarantees either a transceiver_update or a
// transceiver_failure signal.
//
// The enforce_mode_and_split parameter ensures that future
// transceiver updates have the correct mode and split setting
// 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
// the application text and decoded text respectively.
//
Q_SIGNAL void text_font_changed (QFont) const;
Q_SIGNAL void decoded_text_font_changed (QFont) const;
//
// This signal is emitted when the UDP server changes
//
Q_SIGNAL void udp_server_changed (QString& udp_server_name, QStringList const& network_interfaces) const;
Q_SIGNAL void udp_server_port_changed (port_type server_port) const;
Q_SIGNAL void udp_TTL_changed (int TTL) const;
Q_SIGNAL void accept_udp_requests_changed (bool checked) const;
// signal updates to decode highlighting
Q_SIGNAL void decode_highlighting_changed (DecodeHighlightingModel const&) const;
//
// These signals are emitted and reflect transceiver state changes
//
// signals a change in one of the TransceiverState members
Q_SIGNAL void transceiver_update (Transceiver::TransceiverState const&) const;
// Signals a failure of a control rig CAT or PTT connection.
//
// A failed rig CAT or PTT connection is fatal and the underlying
// connections are closed automatically. The connections can be
// re-established with a call to transceiver_online(true) assuming
// 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<impl> m_;
};
ENUM_QDATASTREAM_OPS_DECL (Configuration, DataMode);
ENUM_QDATASTREAM_OPS_DECL (Configuration, Type2MsgGen);
ENUM_CONVERSION_OPS_DECL (Configuration, DataMode);
ENUM_CONVERSION_OPS_DECL (Configuration, Type2MsgGen);
#endif

3289
Configuration.ui Normal file

File diff suppressed because it is too large Load Diff

44
Darwin/Info.plist.in Normal file
View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
<key>CFBundleGetInfoString</key>
<string>${MACOSX_BUNDLE_INFO_STRING}</string>
<key>CFBundleIconFile</key>
<string>${MACOSX_BUNDLE_ICON_FILE}</string>
<key>CFBundleIdentifier</key>
<string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleLongVersionString</key>
<string>${MACOSX_BUNDLE_LONG_VERSION_STRING}</string>
<key>CFBundleName</key>
<string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
<key>CSResourcesFileMapped</key>
<true/>
<key>LSRequiresCarbon</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>${MACOSX_BUNDLE_COPYRIGHT}</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSHighResolutionCapable</key>
<string>True</string>
<key>NSRequiresAquaSystemAppearance</key>
<true/>
<key>NSMicrophoneUsageDescription</key>
<string>This app requires microphone access to receive signals.</string>
</dict>
</plist>

View File

@ -0,0 +1,16 @@
#!/bin/sh
WSJTX_BUNDLE="`echo "$0" | sed -e 's/\/Contents\/MacOS\/.*//'`"
WSJTX_RESOURCES="$WSJTX_BUNDLE/Contents/Resources"
WSJTX_TEMP="/tmp/wsjtx/$UID"
echo "running $0"
echo "WSJTX_BUNDLE: $WSJTX_BUNDLE"
# Setup temporary runtime files
rm -rf "$WSJTX_TEMP"
export "DYLD_LIBRARY_PATH=$WSJTX_RESOURCES/lib"
export "PATH=$WSJTX_RESOURCES/bin:$PATH"
#export
exec "$WSJTX_RESOURCES/bin/wsjtx"

113
Darwin/ReadMe.txt Normal file
View File

@ -0,0 +1,113 @@
Notes on WSJT-X Installation for Mac OS X
-----------------------------------------
If you have already downloaded a previous version of WSJT-X then I suggest
you change the name in the Applications folder from WSJT-X to WSJT-X_previous
before proceeding.
I recommend that you follow the installation instructions especially if you
are moving from v2.3 to v2.4 or later, of WSJT-X or you have upgraded macOS.
Double-click on the wsjtx-...-Darwin.dmg file you have downloaded from K1JT's web-site.
Now open a Terminal window by going to Applications->Utilities and clicking on Terminal.
Along with this ReadMe file there is a file: com.wsjtx.sysctl.plist which must be copied to a
system area by typing this line in the Terminal window and then pressing the Return key.
sudo cp /Volumes/WSJT-X/com.wsjtx.sysctl.plist /Library/LaunchDaemons
you will be asked for your normal password because authorisation is needed to copy this file.
(Your password will not be echoed but press the Return key when completed.)
Now re-boot your Mac. This is necessary to install the changes. After the
reboot you should re-open the Terminal window as before and you can check that the
change has been made by typing:
sysctl -a | grep sysv.shm
If shmmax is not shown as 52428800 then contact me since WSJT-X might fail to load with
an error message: "Unable to create shared memory segment".
You can now close the Terminal window. It will not be necessary to repeat this procedure
again, even when you download an updated version of WSJT-X. It might be necessary if you
upgrade macOS.
Drag the WSJT-X app to your preferred location, such as Applications.
You need to configure your sound card. Visit Applications > Utilities > Audio MIDI
Setup and select your sound card and then set Format to be "48000Hz 2ch-16bit" for
input and output.
Now double-click on the WSJT-X app and two windows will appear. Select Preferences
under the WSJT-X Menu and fill in various station details on the General panel.
I recommend checking the 4 boxes under the Display heading and the first 4 boxes under
the Behaviour heading.
Depending on your macOS you might see a pop-up window suggesting that wsjtx wants to use the
microphone. What this means is that audio input must be allowed. Agree.
Next visit the Audio panel and select the Audio Codec you use to communicate between
WSJT-X and your rig. There are so many audio interfaces available that it is not
possible to give detailed advice on selection. If you have difficulties contact me.
Note the location of the Save Directory. Decoded wave forms are located here.
Look at the Reporting panel. If you check the "Prompt me" box, a logging panel will appear
at the end of the QSO. Visit Section 11 of the User Guide for information about log files
and how to access them.
Finally, visit the Radio panel. WSJT-X is most effective when operated with CAT
control. You will need to install the relevant Mac device driver for your rig,
and then re-launch WSJT-X. Return to the Radio panel in Preferences and in
the "Serial port" panel select your driver from the list that is presented. If you
do not know where to get an appropriate driver, contact me.
WSJT-X needs the Mac clock to be accurate. Visit System Preferences > Date & Time
and make sure that Date and Time are set automatically. The drop-down menu will
normally offer you several time servers to choose from.
On the Help menu, have a look at the new Online User's Guide for operational hints
and tips and possible solutions to any problem you might have.
Please email me if you have problems.
--- John G4KLA (g4kla@rmnjmn.co.uk)
Additional Notes:
1. Information about com.wsjtx.sysctl.plist and multiple instances of WSJT-X
WSJT-X makes use of a block of memory which is shared between different parts of
the code. The normal allocation of shared memory on a Mac is insufficient and this
has to be increased. The com.wsjtx.sysctl.plist file is used for this purpose. You can
use a Mac editor to examine the file. (Do not use another editor - the file
would probably be corrupted.)
It is possible to run two instances of WSJT-X simultaneously. See "Section 16.2
Frequently asked Questions" in the User Guide. If you wish to run more than two instances
simultaneously, the shmall parameter in the com.wsjtx.sysctl.plist file needs to be modified as follows.
The shmall parameter determines the amount of shared memory which is allocated in 4096 byte pages
with 50MB (52428800) required for each instance. The shmall parameter is calculated as:
(n * 52428800)/4096 where 'n' is the number of instances required to run simultaneously.
Replace your new version of this file in /Library/LaunchDaemons and remember to reboot your
Mac afterwards.
Note that the shmmax parameter remains unchanged. This is the maximum amount of shared memory that
any one instance is allowed to request from the total shared memory allocation and should not
be changed.
If two instances of WSJT-X are running, it is likely that you might need additional
audio devices, from two rigs for example. Visit Audio MIDI Setup and create an Aggregate Device
which will allow you to specify more than one interface. I recommend you consult Apple's guide
on combining multiple audio interfaces which is at https://support.apple.com/en-us/HT202000.
2. Preventing WSJT-X from being put into 'sleep' mode (App Nap).
In normal circumstances an application which has not been directly accessed for a while can be
subject to App Nap which means it is suspended until such time as its windows are accessed. If
it is intended that WSJT-X should be submitting continued reports to, for example, PSK Reporter
then reporting will be interrupted. App Nap can be disabled as follows, but first quit WSJT-X:
Open a Terminal window and type: defaults write org.k1jt.wsjtx NSAppSleepDisable -bool YES
If you type: defaults read org.k1jt.wsjtx then the response will be: NSAppSleepDisable = 1;
Close the Terminal window and launch WSJT-X. (If you 'Hide' WSJT-X, this scheme will be suspended.)

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.wsjtx.sysctl</string>
<key>Program</key>
<string>/usr/sbin/sysctl</string>
<key>ProgramArguments</key>
<array>
<string>/usr/sbin/sysctl</string>
<string>kern.sysv.shmmax=52428800</string>
<string>kern.sysv.shmall=25600</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,64 @@
Changing the content of the DragNDrop DMG root folder.
======================================================
The files and links in this folder are populated by the WSJT-X CMake build script. There are install commands which are only run on Apple hosts, this is important because they will get installed at the install root on other platforms, which would be very bad on Linux for example since that is /usr normally!
The symlink to /Applications, the background image (derived from ".../artwork/DragNDrop Background.svg") and, the custom .DS_Store file (".../Darwin/wsjtx_DMG.DS_Store") are all handled specifically by the CPack DragNDrop packager so you don't need to install those.
Modifying the .DS_Store folder options for the DragNDrop DMG root folder.
=========================================================================
The DragNDrop installer is a generated a DMG file that has a custom .DS_Store file that defines the layout, background image and, folder view options of the DMG root folder.
To modify this file, first you need to make a DragNDrop package then mount the DMG file, then modify the root .DS_Store file using Finder. Once you are happy with the results, you check into source control the modified .DS_Store file and then future package builds will use that file.
The installer DMG is read only and shrunk to exactly the size of the contents, also the .DS_Store file is read only and the background image PNG file is hidden. You need to undo all of these things before changing the .DS_Store file. Don't forget to redo these things before checking in a new version of the custom .DS_Store file.
The following recipe shows how to amend the content and layout of the DMG root folder:
# convert the DMG to a R/W copy (substitute the DMG you have built)
hdiutil convert wsjtx-1.7.0-rc1-Darwin.dmg -format UDRW -o rw.dmg
# expand the R/W copy to make room for changes
# first find the current number of sectors
hdiutil resize -limits rw.dmg
# the output looks like:
#
# min cur max
#109696 109696 33037872
#
# you need to increase the sector count to something a bit bigger than current
# e.g. in this case use 110000
hdiutil resize -sectors 110000 rw.dmg
# now you can mount the R/W DMG
hdiutil attach rw.dmg
# make the .DS_Store file writeable
chmod 644 /Volumes/WSJT-X/.DS_Store
# now you can change Finder view options, rearrange icons etc. Remember that you are
# only changing the folder options, not the folder content as that is controlled by
# the install steps in the project CMakeLists.txt if you are adding or removing a file
# to the DMG root folder, you need to have changed the install steps before doing this
# procedure so the content changes are reflected in the installer DMG you start with.
# when you are happy with the layout etc. move the Finder window by any amount, this
# ensures that the .DS_Store file is updated from Finder's cache.
# make the .DS_Store file read only
chmod 444 /Volumes/WSJT-X/.DS_Store
# update the custom .DS_Store file in the source repository (NOTE the file name)
cp /Volumes/WSJT-X/.DS_Store .../wsjtx/wsjtx_DMG.DS_Store
# dismount and eject the R/W DMG and discard it
hdiutil detach /Volumes/WSJT-X
rm rw.dmg
# build a new package and try out the new installer to test your changes
# if all is well commit the changes
# That's all Folks!

3
Darwin/postflight.sh.in Normal file
View File

@ -0,0 +1,3 @@
#!/bin/bash
"$2@CMAKE_INSTALL_SUBDIR@/@WSJTX_BUNDLE_NAME@.app/Contents/MacOS/@WSJTX_BUNDLE_NAME@" --mac-install
exit 0

2
Darwin/postupgrade.sh.in Normal file
View File

@ -0,0 +1,2 @@
#!/bin/bash
exit 0

BIN
Darwin/wsjtx_DMG.DS_Store Normal file

Binary file not shown.

255
Decoder/decodedtext.cpp Normal file
View File

@ -0,0 +1,255 @@
#include "decodedtext.h"
#include <QStringList>
#include <QRegularExpression>
#include <QDebug>
#include "qt_helpers.hpp"
extern "C" {
bool stdmsg_(char const * msg, fortran_charlen_t);
}
namespace
{
QRegularExpression tokens_re {R"(
^
(?:(?<dual>[A-Z0-9/]+)\sRR73;\s)? # dual reply DXpedition message
(?:
(?<word1>
(?:CQ|DE|QRZ)
(?:\s?DX|\s
(?:[A-Z]{1,4}|\d{3}) # directional CQ
)
| [A-Z0-9/]+ # DX call
|\.{3} # unknown hash code
)\s
)
(?:
(?<word2>[A-Z0-9/]+) # DE call
(?:\s
(?<word3>[-+A-Z0-9]+) # report
(?:\s
(?<word4>
(?:
OOO # EME
| (?!RR73)[A-R]{2}[0-9]{2} # grid square (not RR73)
| 5[0-9]{5} # EU VHF Contest report & serial
)
)
(?:\s
(?<word5>[A-R]{2}[0-9]{2}[A-X]{2}) # EU VHF Contest grid locator
)?
)?
)?
)?
)"
, QRegularExpression::ExtendedPatternSyntaxOption};
}
DecodedText::DecodedText (QString const& the_string)
: string_ {the_string.left (the_string.indexOf (QChar::Nbsp))} // discard appended info
, clean_string_ {string_}
, padding_ {string_.indexOf (" ") > 4 ? 2 : 0} // allow for
// seconds
, message_ {string_.mid (column_qsoText + padding_).trimmed ()}
, is_standard_ {false}
{
// discard appended AP info
clean_string_.replace (QRegularExpression {R"(^(.*?)(?:\?\s)?[aq][0-9].*$)"}, "\\1");
// qDebug () << "DecodedText: the_string:" << the_string << "Nbsp pos:" << the_string.indexOf (QChar::Nbsp);
if (message_.length() >= 1)
{
// remove appended confidence (?) and ap designators before truncating the message
message_ = clean_string_.mid (column_qsoText + padding_).trimmed ();
message0_ = message_.left(37);
message_ = message_.left(37).remove (QRegularExpression {"[<>]"});
int i1 = message_.indexOf ('\r');
if (i1 > 0)
{
message_ = message_.left (i1 - 1);
}
if (message_.contains (QRegularExpression {"^(CQ|QRZ)\\s"}))
{
// TODO this magic position 16 is guaranteed to be after the
// last space in a decoded CQ or QRZ message but before any
// appended DXCC entity name or worked before information
auto eom_pos = message_.indexOf (' ', 16);
// we always want at least the characters to position 16
if (eom_pos < 16) eom_pos = message_.size () - 1;
// remove DXCC entity and worked B4 status. TODO need a better way to do this
message_ = message_.left (eom_pos + 1);
}
// stdmsg is a Fortran routine that packs the text, unpacks it
// and compares the result
auto message_c_string = message0_.toLocal8Bit ();
message_c_string += QByteArray {37 - message_c_string.size (), ' '};
is_standard_ = stdmsg_(message_c_string.constData(),37);
}
};
QStringList DecodedText::messageWords () const
{
if(is_standard_) {
// extract up to the first four message words
QString t=message_;
if(t.left(4)=="TU; ") t=message_.mid(4,-1);
return tokens_re.match(t).capturedTexts();
}
// simple word split for free text messages
auto words = message_.split (' ', SkipEmptyParts);
// add whole message and two empty strings as item 0 & 1 to mimic RE
// capture list
words.prepend (QString {});
words.prepend (message_);
return words;
}
QString DecodedText::CQersCall() const
{
QRegularExpression callsign_re {R"(^(CQ|DE|QRZ)(\s?DX|\s([A-Z]{1,4}|\d{3}))?\s(?<callsign>[A-Z0-9/]{2,})(\s[A-R]{2}[0-9]{2})?)"};
return callsign_re.match (message_).captured ("callsign");
}
bool DecodedText::isJT65() const
{
return string_.indexOf("#") == column_mode + padding_;
}
bool DecodedText::isJT9() const
{
return string_.indexOf("@") == column_mode + padding_;
}
bool DecodedText::isTX() const
{
int i = string_.indexOf("Tx");
return (i >= 0 && i < 15); // TODO guessing those numbers. Does Tx ever move?
}
bool DecodedText::isLowConfidence () const
{
return QChar {'?'} == string_.mid (padding_ + column_qsoText + 36, 1);
}
int DecodedText::frequencyOffset() const
{
return string_.mid(column_freq + padding_,4).toInt();
}
int DecodedText::snr() const
{
int i1=string_.indexOf(" ")+1;
return string_.mid(i1,3).toInt();
}
float DecodedText::dt() const
{
return string_.mid(column_dt + padding_,5).toFloat();
}
/*
2343 -11 0.8 1259 # YV6BFE F6GUU R-08
2343 -19 0.3 718 # VE6WQ SQ2NIJ -14
2343 -7 0.3 815 # KK4DSD W7VP -16
2343 -13 0.1 3627 @ CT1FBK IK5YZT R+02
0605 Tx 1259 # CQ VK3ACF QF22
*/
// find and extract any report. Returns true if this is a standard message
bool DecodedText::report(QString const& myBaseCall, QString const& dxBaseCall, /*mod*/QString& report) const
{
if (message_.size () < 1) return false;
QStringList const& w = message_.split(" ", SkipEmptyParts);
int offset {0};
if (w.size () > 2)
{
if ("RR73;" == w[1] && w.size () > 3)
{
offset = 2;
}
if (is_standard_ && (w[offset] == myBaseCall
|| w[offset].endsWith ("/" + myBaseCall)
|| w[offset].startsWith (myBaseCall + "/")
|| (w.size () > offset + 1 && !dxBaseCall.isEmpty ()
&& (w[offset + 1] == dxBaseCall
|| w[offset + 1].endsWith ("/" + dxBaseCall)
|| w[offset + 1].startsWith (dxBaseCall + "/")))))
{
bool ok;
auto tt = w[offset + 2];
auto i1=tt.toInt(&ok);
if (ok and i1>=-50 and i1<50)
{
report = tt;
}
else
{
if (tt.mid(0,1)=="R")
{
i1=tt.mid(1).toInt(&ok);
if(ok and i1>=-50 and i1<50)
{
report = tt.mid(1);
}
}
}
}
}
return is_standard_;
}
// get the first text word, usually the call
QString DecodedText::call() const
{
return tokens_re.match (message_).captured ("word1");
}
// 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 msg = message_;
auto p = msg.indexOf ("; ");
if (p >= 0)
{
msg = msg.mid (p + 2);
}
auto const& match = tokens_re.match (msg);
call = match.captured ("word2");
grid = match.captured ("word3");
if ("R" == grid) grid = match.captured ("word4");
}
unsigned DecodedText::timeInSeconds() const
{
return 3600 * string_.mid (column_time, 2).toUInt ()
+ 60 * string_.mid (column_time + 2, 2).toUInt()
+ (padding_ ? string_.mid (column_time + 2 + padding_, 2).toUInt () : 0U);
}
QString DecodedText::report() const // returns a string of the SNR field with a leading + or - followed by two digits
{
int sr = snr();
if (sr<-50)
sr = -50;
else
if (sr > 49)
sr = 49;
QString rpt;
rpt = rpt.asprintf("%d",abs(sr));
if (sr > 9)
rpt = "+" + rpt;
else
if (sr >= 0)
rpt = "+0" + rpt;
else
if (sr >= -9)
rpt = "-0" + rpt;
else
rpt = "-" + rpt;
return rpt;
}

87
Decoder/decodedtext.h Normal file
View File

@ -0,0 +1,87 @@
// -*- Mode: C++ -*-
/*
* Class to handle the formatted string as returned from the fortran decoder
*
* VK3ACF August 2013
*/
#ifndef DECODEDTEXT_H
#define DECODEDTEXT_H
#include <QString>
/*
012345678901234567890123456789012345678901
^ ^ ^ ^ ^ ^
2343 -11 0.8 1259 # CQ VP2X/GM4WJS GL33
2343 -11 0.8 1259 # CQ 999 VP2V/GM4WJS
2343 -11 0.8 1259 # YV6BFE F6GUU R-08
2343 -19 0.3 718 # VE6WQ SQ2NIJ -14
2343 -7 0.3 815 # KK4DSD W7VP -16
2343 -13 0.1 3627 @ CT1FBK IK5YZT R+02
0605 Tx 1259 # CQ VK3ACF QF22
*/
class DecodedText
{
public:
explicit DecodedText (QString const& message);
QString string() const { return string_; };
QString clean_string() const { return clean_string_; };
QStringList messageWords () const;
int indexOf(QString s) const { return string_.indexOf(s); };
int indexOf(QString s, int i) const { return string_.indexOf(s,i); };
QString mid(int f, int t) const { return string_.mid(f,t); };
QString left(int i) const { return string_.left(i); };
void clear() { string_.clear(); };
QString CQersCall() const;
bool isJT65() const;
bool isJT9() const;
bool isTX() const;
bool isStandardMessage () const {return is_standard_;}
bool isLowConfidence () const;
int frequencyOffset() const; // hertz offset from the tuned dial or rx frequency, aka audio frequency
int snr() const;
float dt() const;
// find and extract any report. Returns true if this is a standard message
bool report(QString const& myBaseCall, QString const& dxBaseCall, /*mod*/QString& report) const;
// get the first message text word, usually the call
QString call() const;
// get the second word, most likely the de call and the third word, most likely grid
void deCallAndGrid(/*out*/QString& call, QString& grid) const;
unsigned timeInSeconds() const;
// returns a string of the SNR field with a leading + or - followed by two digits
QString report() const;
private:
// These define the columns in the decoded text where fields are to be found.
// We rely on these columns being the same in the fortran code (lib/decoder.f90) that formats the decoded text
enum Columns {column_time = 0,
column_snr = 5,
column_dt = 9,
column_freq = 14,
column_mode = 19,
column_qsoText = 22 };
QString string_;
QString clean_string_;
int padding_;
QString message_;
QString message0_;
bool is_standard_;
};
#endif // DECODEDTEXT_H

3
Decoder/decodedtext.pri Normal file
View File

@ -0,0 +1,3 @@
SOURCES += Decoder/decodedtext.cpp
HEADERS += Decoder/decodedtext.h

125
Detector/Detector.cpp Normal file
View File

@ -0,0 +1,125 @@
#include "Detector.hpp"
#include <QDateTime>
#include <QtAlgorithms>
#include <QDebug>
#include <math.h>
#include "commons.h"
#include "moc_Detector.cpp"
extern "C" {
void fil4_(qint16*, qint32*, qint16*, qint32*);
}
extern dec_data_t dec_data;
Detector::Detector (unsigned frameRate, double periodLengthInSeconds,
unsigned downSampleFactor, QObject * parent)
: AudioDevice (parent)
, m_frameRate (frameRate)
, m_period (periodLengthInSeconds)
, m_downSampleFactor (downSampleFactor)
, m_samplesPerFFT {max_buffer_size}
, m_buffer ((downSampleFactor > 1) ?
new short [max_buffer_size * downSampleFactor] : nullptr)
, m_bufferPos (0)
{
(void)m_frameRate; // quell compiler warning
clear ();
}
void Detector::setBlockSize (unsigned n)
{
m_samplesPerFFT = n;
}
bool Detector::reset ()
{
clear ();
// don't call base class reset because it calls seek(0) which causes
// a warning
return isOpen ();
}
void Detector::clear ()
{
// set index to roughly where we are in time (1ms resolution)
// qint64 now (QDateTime::currentMSecsSinceEpoch ());
// unsigned msInPeriod ((now % 86400000LL) % (m_period * 1000));
// dec_data.params.kin = qMin ((msInPeriod * m_frameRate) / 1000, static_cast<unsigned> (sizeof (dec_data.d2) / sizeof (dec_data.d2[0])));
dec_data.params.kin = 0;
m_bufferPos = 0;
// fill buffer with zeros (G4WJS commented out because it might cause decoder hangs)
// qFill (dec_data.d2, dec_data.d2 + sizeof (dec_data.d2) / sizeof (dec_data.d2[0]), 0);
}
qint64 Detector::writeData (char const * data, qint64 maxSize)
{
static unsigned mstr0=999999;
qint64 ms0 = QDateTime::currentMSecsSinceEpoch() % 86400000;
unsigned mstr = ms0 % int(1000.0*m_period); // ms into the nominal Tx start time
if(mstr < mstr0) { //When mstr has wrapped around to 0, restart the buffer
dec_data.params.kin = 0;
m_bufferPos = 0;
}
mstr0=mstr;
// no torn frames
Q_ASSERT (!(maxSize % static_cast<qint64> (bytesPerFrame ())));
// these are in terms of input frames (not down sampled)
size_t framesAcceptable ((sizeof (dec_data.d2) /
sizeof (dec_data.d2[0]) - dec_data.params.kin) * m_downSampleFactor);
size_t framesAccepted (qMin (static_cast<size_t> (maxSize /
bytesPerFrame ()), framesAcceptable));
if (framesAccepted < static_cast<size_t> (maxSize / bytesPerFrame ())) {
qDebug () << "dropped " << maxSize / bytesPerFrame () - framesAccepted
<< " frames of data on the floor!"
<< dec_data.params.kin << mstr;
}
for (unsigned remaining = framesAccepted; remaining; ) {
size_t numFramesProcessed (qMin (m_samplesPerFFT *
m_downSampleFactor - m_bufferPos, remaining));
if(m_downSampleFactor > 1) {
store (&data[(framesAccepted - remaining) * bytesPerFrame ()],
numFramesProcessed, &m_buffer[m_bufferPos]);
m_bufferPos += numFramesProcessed;
if(m_bufferPos==m_samplesPerFFT*m_downSampleFactor) {
qint32 framesToProcess (m_samplesPerFFT * m_downSampleFactor);
qint32 framesAfterDownSample (m_samplesPerFFT);
if(m_downSampleFactor > 1 && dec_data.params.kin>=0 &&
dec_data.params.kin < (NTMAX*12000 - framesAfterDownSample)) {
fil4_(&m_buffer[0], &framesToProcess, &dec_data.d2[dec_data.params.kin],
&framesAfterDownSample);
dec_data.params.kin += framesAfterDownSample;
} else {
// qDebug() << "framesToProcess = " << framesToProcess;
// qDebug() << "dec_data.params.kin = " << dec_data.params.kin;
// qDebug() << "secondInPeriod = " << secondInPeriod();
// qDebug() << "framesAfterDownSample" << framesAfterDownSample;
}
Q_EMIT framesWritten (dec_data.params.kin);
m_bufferPos = 0;
}
} else {
store (&data[(framesAccepted - remaining) * bytesPerFrame ()],
numFramesProcessed, &dec_data.d2[dec_data.params.kin]);
m_bufferPos += numFramesProcessed;
dec_data.params.kin += numFramesProcessed;
if (m_bufferPos == static_cast<unsigned> (m_samplesPerFFT)) {
Q_EMIT framesWritten (dec_data.params.kin);
m_bufferPos = 0;
}
}
remaining -= numFramesProcessed;
}
// we drop any data past the end of the buffer on the floor until
// the next period starts
return maxSize;
}

58
Detector/Detector.hpp Normal file
View File

@ -0,0 +1,58 @@
#ifndef DETECTOR_HPP__
#define DETECTOR_HPP__
#include "Audio/AudioDevice.hpp"
#include <QScopedArrayPointer>
//
// output device that distributes data in predefined chunks via a signal
//
// the underlying device for this abstraction is just the buffer that
// stores samples throughout a receiving period
//
class Detector : public AudioDevice
{
Q_OBJECT;
public:
//
// if the data buffer were not global storage and fixed size then we
// might want maximum size passed as constructor arguments
//
// we down sample by a factor of 4
//
// the samplesPerFFT argument is the number after down sampling
//
Detector (unsigned frameRate, double periodLengthInSeconds, unsigned downSampleFactor = 4u,
QObject * parent = 0);
void setTRPeriod(double p) {m_period=p;}
bool reset () override;
Q_SIGNAL void framesWritten (qint64) const;
Q_SLOT void setBlockSize (unsigned);
protected:
qint64 readData (char * /* data */, qint64 /* maxSize */) override
{
return -1; // we don't produce data
}
qint64 writeData (char const * data, qint64 maxSize) override;
private:
void clear (); // discard buffer contents
unsigned m_frameRate;
double m_period;
unsigned m_downSampleFactor;
qint32 m_samplesPerFFT; // after any down sampling
static size_t const max_buffer_size {7 * 512};
QScopedArrayPointer<short> m_buffer; // de-interleaved sample buffer
// big enough for all the
// samples for one increment of
// data (a signals worth) at
// the input sample rate
unsigned m_bufferPos;
};
#endif

3
Detector/Detector.pri Normal file
View File

@ -0,0 +1,3 @@
SOURCES += Detector/Detector.cpp
HEADERS += Detector/Detector.hpp

159
DisplayManual.cpp Normal file
View File

@ -0,0 +1,159 @@
#include "DisplayManual.hpp"
#include <QObject>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QUrl>
#include <QString>
#include <QDir>
#include <QFileInfo>
#include <QDesktopServices>
#include <QLocale>
#include "revision_utils.hpp"
#include "pimpl_impl.hpp"
namespace
{
class token
: public QObject
{
Q_OBJECT
public:
token (QUrl const& url, QString const& lang, QString const& name_we, QObject * parent = nullptr)
: QObject {parent}
, url_ {url}
, lang_ {lang}
, name_we_ {name_we}
{
}
QUrl url_;
QString lang_;
QString name_we_;
};
}
class DisplayManual::impl final
: public QObject
{
Q_OBJECT
public:
impl (QNetworkAccessManager * qnam)
: qnam_ {qnam}
{
connect (qnam_, &QNetworkAccessManager::finished, this, &DisplayManual::impl::reply_finished);
}
void display (QUrl const& url, QString const& name_we)
{
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
if (QNetworkAccessManager::Accessible != qnam_->networkAccessible ()) {
// try and recover network access for QNAM
qnam_->setNetworkAccessible (QNetworkAccessManager::Accessible);
}
#endif
// try and find a localized manual
auto lang = QLocale::system ().name ();
// try for language and country first
auto file = name_we + '_' + lang + '-' + version () + ".html";
auto target = url.resolved (file);
QNetworkRequest request {target};
request.setRawHeader ("User-Agent", "WSJT-X Manual Checker");
request.setOriginatingObject (new token {url, lang, name_we, this});
auto * reply = qnam_->head (request);
outstanding_requests_ << reply;
}
void reply_finished (QNetworkReply * reply)
{
if (outstanding_requests_.contains (reply))
{
QUrl target;
if (reply->error ())
{
if (auto * tok = qobject_cast<token *> (reply->request ().originatingObject ()))
{
auto pos = tok->lang_.lastIndexOf ('_');
QString file;
if (pos >= 0)
{
tok->lang_.truncate (pos);
file = tok->name_we_ + '_' + tok->lang_ + '-' + version () + ".html";
target = tok->url_.resolved (file);
QNetworkRequest request {target};
request.setRawHeader ("User-Agent", "WSJT-X Manual Checker");
request.setOriginatingObject (tok);
auto * reply = qnam_->head (request);
outstanding_requests_ << reply;
}
else
{
// give up looking and request the default
file = tok->name_we_ + '-' + version () + ".html";
target = tok->url_.resolved (file);
QDesktopServices::openUrl (target);
delete tok;
}
}
}
else
{
// found it
if (auto * tok = qobject_cast<token *> (reply->request ().originatingObject ()))
{
delete tok;
}
QDesktopServices::openUrl (reply->request ().url ());
}
outstanding_requests_.removeOne (reply);
reply->deleteLater ();
}
}
QNetworkAccessManager * qnam_;
QList<QNetworkReply *> outstanding_requests_;
};
#include "DisplayManual.moc"
DisplayManual::DisplayManual (QNetworkAccessManager * qnam, QObject * parent)
: QObject {parent}
, m_ {qnam}
{
}
DisplayManual::~DisplayManual ()
{
}
void DisplayManual::display_html_url (QUrl const& url, QString const& name_we)
{
m_->display (url, name_we);
}
void DisplayManual::display_html_file (QDir const& dir, QString const& name_we)
{
// try and find a localized manual
auto lang = QLocale::system ().name ();
// try for language and country first
auto file = dir.absoluteFilePath (name_we + '_' + lang + '-' + version () + ".html");
if (!QFileInfo::exists (file))
{
// try for language
lang.truncate (lang.lastIndexOf ('_'));
file = dir.absoluteFilePath (name_we + '_' + lang + '-' + version () + ".html");
if (!QFileInfo::exists (file))
{
// use default
file = dir.absoluteFilePath (name_we + '-' + version () + ".html");
}
}
// may fail but browser 404 error is a good as anything
QDesktopServices::openUrl (QUrl {"file:///" + file});
}

27
DisplayManual.hpp Normal file
View File

@ -0,0 +1,27 @@
#ifndef DISPLAY_MANUAL_HPP__
#define DISPLAY_MANUAL_HPP__
#include <QObject>
#include "pimpl_h.hpp"
class QNetworkAccessManager;
class QDir;
class QUrl;
class QString;
class DisplayManual
: public QObject
{
public:
DisplayManual (QNetworkAccessManager *, QObject * = nullptr);
~DisplayManual ();
void display_html_url (QUrl const& url, QString const& name_we);
void display_html_file (QDir const& dir, QString const& name_we);
private:
class impl;
pimpl<impl> m_;
};
#endif

547
EqualizationToolsDialog.cpp Normal file
View File

@ -0,0 +1,547 @@
#include "EqualizationToolsDialog.hpp"
#include <iterator>
#include <algorithm>
#include <fstream>
#include <limits>
#include <cmath>
#include <QDir>
#include <QVector>
#include <QHBoxLayout>
#include <QDialog>
#include <QDialogButtonBox>
#include <QPushButton>
#include <QFileDialog>
#include <QSettings>
#include "SettingsGroup.hpp"
#include "qcustomplot.h"
#include "pimpl_impl.hpp"
namespace
{
float constexpr PI = 3.1415927f;
char const * const title = "Equalization Tools";
size_t constexpr intervals = 144;
// plot data loaders - wraps a plot providing value_type and
// push_back so that a std::back_inserter output iterator can be
// used to load plot data
template<typename T, typename A>
struct plot_data_loader
{
public:
typedef T value_type;
// the adjust argument is a function that is passed the plot
// pointer, the graph index and a data point, it returns a
// possibly adjusted data point and can modify the graph including
// adding extra points or gaps (quiet_NaN)
plot_data_loader (QCustomPlot * plot, int graph_index, A adjust)
: plot_ {plot}
, index_ {graph_index}
, adjust_ (adjust)
{
}
// load point into graph
void push_back (value_type const& d)
{
plot_->graph (index_)->data ()->add (adjust_ (plot_, index_, d));
}
private:
QCustomPlot * plot_;
int index_;
A adjust_;
};
// helper function template to make a plot_data_loader instance
template<typename A>
auto make_plot_data_loader (QCustomPlot * plot, int index, A adjust)
-> plot_data_loader<QCPGraphData, decltype (adjust)>
{
return plot_data_loader<QCPGraphData, decltype (adjust)> {plot, index, adjust};
}
// identity adjust function when no adjustment is needed with the
// above instantiation helper
QCPGraphData adjust_identity (QCustomPlot *, int, QCPGraphData const& v) {return v;}
// a plot_data_loader adjustment function that wraps Y values of
// (-1..+1) plotting discontinuities as gaps in the graph data
auto wrap_pi = [] (QCustomPlot * plot, int index, QCPGraphData d)
{
double constexpr limit {1};
static unsigned wrap_count {0};
static double last_x {std::numeric_limits<double>::lowest ()};
d.value += 2 * limit * wrap_count;
if (d.value > limit)
{
// insert a gap in the graph
plot->graph (index)->data ()->add ({last_x + (d.key - last_x) / 2
, std::numeric_limits<double>::quiet_NaN ()});
while (d.value > limit)
{
--wrap_count;
d.value -= 2 * limit;
}
}
else if (d.value < -limit)
{
// insert a gap into the graph
plot->graph (index)->data ()->add ({last_x + (d.key - last_x) / 2
, std::numeric_limits<double>::quiet_NaN ()});
while (d.value < -limit)
{
++wrap_count;
d.value += 2 * limit;
}
}
last_x = d.key;
return d;
};
// generate points of type R from a function of type F for X in
// (-1..+1) with N intervals and function of type SX to scale X and
// of type SY to scale Y
//
// it is up to the user to call the generator sufficient times which
// is interval+1 times to reach +1
template<typename R, typename F, typename SX, typename SY>
struct graph_generator
{
public:
graph_generator (F f, size_t intervals, SX x_scaling, SY y_scaling)
: x_ {0}
, f_ (f)
, intervals_ {intervals}
, x_scaling_ (x_scaling)
, y_scaling_ (y_scaling)
{
}
R operator () ()
{
typename F::value_type x {x_++ * 2.f / intervals_ - 1.f};
return {x_scaling_ (x), y_scaling_ (f_ (x))};
}
private:
int x_;
F f_;
size_t intervals_;
SX x_scaling_;
SY y_scaling_;
};
// helper function template to make a graph_generator instance for
// QCPGraphData type points with intervals intervals
template<typename F, typename SX, typename SY>
auto make_graph_generator (F function, SX x_scaling, SY y_scaling)
-> graph_generator<QCPGraphData, F, decltype (x_scaling), decltype (y_scaling)>
{
return graph_generator<QCPGraphData, F, decltype (x_scaling), decltype (y_scaling)>
{function, intervals, x_scaling, y_scaling};
}
// template function object for a polynomial with coefficients
template<typename C>
class polynomial
{
public:
typedef typename C::value_type value_type;
explicit polynomial (C const& coefficients)
: c_ {coefficients}
{
}
value_type operator () (value_type const& x)
{
value_type y {};
for (typename C::size_type i = c_.size (); i > 0; --i)
{
y = c_[i - 1] + x * y;
}
return y;
}
private:
C c_;
};
// helper function template to instantiate a polynomial instance
template<typename C>
auto make_polynomial (C const& coefficients) -> polynomial<C>
{
return polynomial<C> (coefficients);
}
// template function object for a group delay with coefficients
template<typename C>
class group_delay
{
public:
typedef typename C::value_type value_type;
explicit group_delay (C const& coefficients)
: c_ {coefficients}
{
}
value_type operator () (value_type const& x)
{
value_type tau {};
for (typename C::size_type i = 2; i < c_.size (); ++i)
{
tau += i * c_[i] * std::pow (x, i - 1);
}
return -1 / (2 * PI) * tau;
}
private:
C c_;
};
// helper function template to instantiate a group_delay function
// object
template<typename C>
auto make_group_delay (C const& coefficients) -> group_delay<C>
{
return group_delay<C> (coefficients);
}
// handy identity function
template<typename T> T identity (T const& v) {return v;}
// a lambda that scales the X axis from normalized to (500..2500)Hz
auto freq_scaling = [] (float v) -> float {return 1500.f + 1000.f * v;};
// a lambda that scales the phase Y axis from radians to units of Pi
auto pi_scaling = [] (float v) -> float {return v / PI;};
}
class EqualizationToolsDialog::impl final
: public QDialog
{
Q_OBJECT
public:
explicit impl (EqualizationToolsDialog * self, QSettings * settings
, QDir const& data_directory, QVector<double> const& coefficients
, QWidget * parent);
~impl () {save_window_state ();}
protected:
void closeEvent (QCloseEvent * e) override
{
save_window_state ();
QDialog::closeEvent (e);
}
private:
void save_window_state ()
{
SettingsGroup g (settings_, title);
settings_->setValue ("geometry", saveGeometry ());
}
void plot_current ();
void plot_phase ();
void plot_amplitude ();
EqualizationToolsDialog * self_;
QSettings * settings_;
QDir data_directory_;
QHBoxLayout layout_;
QVector<double> current_coefficients_;
QVector<double> new_coefficients_;
unsigned amp_poly_low_;
unsigned amp_poly_high_;
QVector<double> amp_coefficients_;
QCustomPlot plot_;
QDialogButtonBox button_box_;
};
#include "EqualizationToolsDialog.moc"
EqualizationToolsDialog::EqualizationToolsDialog (QSettings * settings
, QDir const& data_directory
, QVector<double> const& coefficients
, QWidget * parent)
: m_ {this, settings, data_directory, coefficients, parent}
{
}
void EqualizationToolsDialog::show ()
{
m_->show ();
}
EqualizationToolsDialog::impl::impl (EqualizationToolsDialog * self
, QSettings * settings
, QDir const& data_directory
, QVector<double> const& coefficients
, QWidget * parent)
: QDialog {parent}
, self_ {self}
, settings_ {settings}
, data_directory_ {data_directory}
, current_coefficients_ {coefficients}
, amp_poly_low_ {0}
, amp_poly_high_ {6000}
, button_box_ {QDialogButtonBox::Apply
| QDialogButtonBox::RestoreDefaults | QDialogButtonBox::Close
, Qt::Vertical}
{
setWindowTitle (windowTitle () + ' ' + tr ("Equalization Tools"));
resize (500, 600);
{
SettingsGroup g {settings_, title};
restoreGeometry (settings_->value ("geometry", saveGeometry ()).toByteArray ());
}
auto legend_title = new QCPTextElement {&plot_, tr ("Phase"), QFont {"sans", 9, QFont::Bold}};
legend_title->setLayer (plot_.legend->layer ());
plot_.legend->addElement (0, 0, legend_title);
plot_.legend->setVisible (true);
plot_.xAxis->setLabel (tr ("Freq (Hz)"));
plot_.xAxis->setRange (500, 2500);
plot_.yAxis->setLabel (tr ("Phase (Π)"));
plot_.yAxis->setRange (-1, +1);
plot_.yAxis2->setLabel (tr ("Delay (ms)"));
plot_.axisRect ()->setRangeDrag (Qt::Vertical);
plot_.axisRect ()->setRangeZoom (Qt::Vertical);
plot_.yAxis2->setVisible (true);
plot_.axisRect ()->setRangeDragAxes (nullptr, plot_.yAxis2);
plot_.axisRect ()->setRangeZoomAxes (nullptr, plot_.yAxis2);
plot_.axisRect ()->insetLayout ()->setInsetAlignment (0, Qt::AlignBottom|Qt::AlignRight);
plot_.setInteractions (QCP::iRangeDrag | QCP::iRangeZoom | QCP::iSelectPlottables);
plot_.addGraph ()->setName (tr ("Measured"));
plot_.graph ()->setPen (QPen {Qt::blue});
plot_.graph ()->setVisible (false);
plot_.graph ()->removeFromLegend ();
plot_.addGraph ()->setName (tr ("Proposed"));
plot_.graph ()->setPen (QPen {Qt::red});
plot_.graph ()->setVisible (false);
plot_.graph ()->removeFromLegend ();
plot_.addGraph ()->setName (tr ("Current"));
plot_.graph ()->setPen (QPen {Qt::green});
plot_.addGraph (plot_.xAxis, plot_.yAxis2)->setName (tr ("Group Delay"));
plot_.graph ()->setPen (QPen {Qt::darkGreen});
plot_.plotLayout ()->addElement (new QCPAxisRect {&plot_});
plot_.plotLayout ()->setRowStretchFactor (1, 0.5);
auto amp_legend = new QCPLegend;
plot_.axisRect (1)->insetLayout ()->addElement (amp_legend, Qt::AlignTop | Qt::AlignRight);
plot_.axisRect (1)->insetLayout ()->setMargins (QMargins {12, 12, 12, 12});
amp_legend->setVisible (true);
amp_legend->setLayer (QLatin1String {"legend"});
legend_title = new QCPTextElement {&plot_, tr ("Amplitude"), QFont {"sans", 9, QFont::Bold}};
legend_title->setLayer (amp_legend->layer ());
amp_legend->addElement (0, 0, legend_title);
plot_.axisRect (1)->axis (QCPAxis::atBottom)->setLabel (tr ("Freq (Hz)"));
plot_.axisRect (1)->axis (QCPAxis::atBottom)->setRange (0, 6000);
plot_.axisRect (1)->axis (QCPAxis::atLeft)->setLabel (tr ("Relative Power (dB)"));
plot_.axisRect (1)->axis (QCPAxis::atLeft)->setRangeLower (0);
plot_.axisRect (1)->setRangeDragAxes (nullptr, nullptr);
plot_.axisRect (1)->setRangeZoomAxes (nullptr, nullptr);
plot_.addGraph (plot_.axisRect (1)->axis (QCPAxis::atBottom)
, plot_.axisRect (1)->axis (QCPAxis::atLeft))->setName (tr ("Reference"));
plot_.graph ()->setPen (QPen {Qt::blue});
plot_.graph ()->removeFromLegend ();
plot_.graph ()->addToLegend (amp_legend);
layout_.addWidget (&plot_);
auto load_phase_button = button_box_.addButton (tr ("Phase ..."), QDialogButtonBox::ActionRole);
auto refresh_button = button_box_.addButton (tr ("Refresh"), QDialogButtonBox::ActionRole);
auto discard_measured_button = button_box_.addButton (tr ("Discard Measured"), QDialogButtonBox::ActionRole);
layout_.addWidget (&button_box_);
setLayout (&layout_);
connect (&button_box_, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect (&button_box_, &QDialogButtonBox::clicked, [=] (QAbstractButton * button) {
if (button == load_phase_button)
{
plot_phase ();
}
else if (button == refresh_button)
{
plot_current ();
}
else if (button == button_box_.button (QDialogButtonBox::Apply))
{
if (plot_.graph (0)->dataCount ()) // something loaded
{
current_coefficients_ = new_coefficients_;
Q_EMIT self_->phase_equalization_changed (current_coefficients_);
plot_current ();
}
}
else if (button == button_box_.button (QDialogButtonBox::RestoreDefaults))
{
current_coefficients_ = QVector<double> {0., 0., 0., 0., 0.};
Q_EMIT self_->phase_equalization_changed (current_coefficients_);
plot_current ();
}
else if (button == discard_measured_button)
{
new_coefficients_ = QVector<double> {0., 0., 0., 0., 0.};
plot_.graph (0)->data ()->clear ();
plot_.graph (0)->setVisible (false);
plot_.graph (0)->removeFromLegend ();
plot_.graph (1)->data ()->clear ();
plot_.graph (1)->setVisible (false);
plot_.graph (1)->removeFromLegend ();
plot_.replot ();
}
});
plot_current ();
}
struct PowerSpectrumPoint
{
operator QCPGraphData () const
{
return QCPGraphData {freq_, power_};
}
float freq_;
float power_;
};
// read an amplitude point line from a stream (refspec.dat)
std::istream& operator >> (std::istream& is, PowerSpectrumPoint& r)
{
float y1, y3, y4; // discard these
is >> r.freq_ >> y1 >> r.power_ >> y3 >> y4;
return is;
}
void EqualizationToolsDialog::impl::plot_current ()
{
auto phase_graph = make_plot_data_loader (&plot_, 2, wrap_pi);
plot_.graph (2)->data ()->clear ();
std::generate_n (std::back_inserter (phase_graph), intervals + 1
, make_graph_generator (make_polynomial (current_coefficients_), freq_scaling, pi_scaling));
auto group_delay_graph = make_plot_data_loader (&plot_, 3, adjust_identity);
plot_.graph (3)->data ()->clear ();
std::generate_n (std::back_inserter (group_delay_graph), intervals + 1
, make_graph_generator (make_group_delay (current_coefficients_), freq_scaling, identity<double>));
plot_.graph (3)->rescaleValueAxis ();
QFileInfo refspec_file_info {data_directory_.absoluteFilePath ("refspec.dat")};
std::ifstream refspec_file (refspec_file_info.absoluteFilePath ().toLocal8Bit ().constData (), std::ifstream::in);
unsigned n;
if (refspec_file >> amp_poly_low_ >> amp_poly_high_ >> n)
{
std::istream_iterator<double> isi {refspec_file};
amp_coefficients_.clear ();
std::copy_n (isi, n, std::back_inserter (amp_coefficients_));
}
else
{
// may be old format refspec.dat with no header so rewind
refspec_file.clear ();
refspec_file.seekg (0);
}
auto reference_spectrum_graph = make_plot_data_loader (&plot_, 4, adjust_identity);
plot_.graph (4)->data ()->clear ();
std::copy (std::istream_iterator<PowerSpectrumPoint> {refspec_file},
std::istream_iterator<PowerSpectrumPoint> {},
std::back_inserter (reference_spectrum_graph));
plot_.graph (4)->rescaleValueAxis (true);
plot_.replot ();
}
struct PhasePoint
{
operator QCPGraphData () const
{
return QCPGraphData {freq_, phase_};
}
double freq_;
double phase_;
};
// read a phase point line from a stream (pcoeff file)
std::istream& operator >> (std::istream& is, PhasePoint& c)
{
double pp, sigmay; // discard these
if (is >> c.freq_ >> pp >> c.phase_ >> sigmay)
{
c.freq_ = 1500. + 1000. * c.freq_; // scale frequency to Hz
c.phase_ /= PI; // scale to units of Pi
}
return is;
}
void EqualizationToolsDialog::impl::plot_phase ()
{
auto const& phase_file_name = QFileDialog::getOpenFileName (this
, "Select Phase Response Coefficients"
, data_directory_.absolutePath ()
, "Phase Coefficient Files (*.pcoeff)");
if (!phase_file_name.size ()) return;
std::ifstream phase_file (phase_file_name.toLocal8Bit ().constData (), std::ifstream::in);
int n;
float chi;
float rmsdiff;
unsigned freq_low;
unsigned freq_high;
unsigned terms;
// read header information
if (phase_file >> n >> chi >> rmsdiff >> freq_low >> freq_high >> terms)
{
std::istream_iterator<double> isi {phase_file};
new_coefficients_.clear ();
std::copy_n (isi, terms, std::back_inserter (new_coefficients_));
if (phase_file)
{
plot_.graph (0)->data ()->clear ();
plot_.graph (1)->data ()->clear ();
// read the phase data and plot as graph 0
auto graph = make_plot_data_loader (&plot_, 0, adjust_identity);
std::copy_n (std::istream_iterator<PhasePoint> {phase_file},
intervals + 1, std::back_inserter (graph));
if (phase_file)
{
plot_.graph(0)->setLineStyle(QCPGraph::lsNone);
plot_.graph(0)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc, 4));
plot_.graph (0)->setVisible (true);
plot_.graph (0)->addToLegend ();
// generate the proposed polynomial plot as graph 1
auto graph = make_plot_data_loader (&plot_, 1, wrap_pi);
std::generate_n (std::back_inserter (graph), intervals + 1
, make_graph_generator (make_polynomial (new_coefficients_)
, freq_scaling, pi_scaling));
plot_.graph (1)->setVisible (true);
plot_.graph (1)->addToLegend ();
}
plot_.replot ();
}
}
}
#include "moc_EqualizationToolsDialog.cpp"

View File

@ -0,0 +1,31 @@
#ifndef EQUALIZATION_TOOLS_DIALOG_HPP__
#define EQUALIZATION_TOOLS_DIALOG_HPP__
#include <QObject>
#include "pimpl_h.hpp"
class QWidget;
class QSettings;
class QDir;
class EqualizationToolsDialog
: public QObject
{
Q_OBJECT
public:
explicit EqualizationToolsDialog (QSettings *
, QDir const& data_directory
, QVector<double> const& coefficients
, QWidget * = nullptr);
Q_SLOT void show ();
Q_SIGNAL void phase_equalization_changed (QVector<double> const&);
private:
class impl;
pimpl<impl> m_;
};
#endif

View File

@ -0,0 +1,49 @@
#ifndef EXCEPTION_CATCHING_APPLICATION_HPP__
#define EXCEPTION_CATCHING_APPLICATION_HPP__
#include <QApplication>
#include <boost/log/core.hpp>
#include "Logger.hpp"
class QObject;
class QEvent;
//
// We can't use the GUI after QApplication::exit() is called so
// uncaught exceptions can get lost on Windows systems where there is
// no console terminal, so here we override QApplication::notify() and
// wrap the base class call with a try block to catch and log any
// uncaught exceptions.
//
class ExceptionCatchingApplication
: public QApplication
{
public:
explicit ExceptionCatchingApplication (int& argc, char * * argv)
: QApplication {argc, argv}
{
}
bool notify (QObject * receiver, QEvent * e) override
{
try
{
return QApplication::notify (receiver, e);
}
catch (std::exception const& e)
{
LOG_FATAL ("Unexpected exception caught in event loop: " << e.what ());
}
catch (...)
{
LOG_FATAL ("Unexpected unknown exception caught in event loop");
}
// There's nowhere to go from here as Qt will not pass exceptions
// through the event loop, so we must abort.
boost::log::core::get ()->flush ();
qFatal ("Aborting");
return false;
}
};
#endif

32
GUIcontrols.txt Normal file
View File

@ -0,0 +1,32 @@
JT4 JT9 9+65 JT65 QRA SCAT M144 WSPR Echo
---------------------------------------------------------------------
0. txFirstCheckBox 11 111 1 11 11 11 1
1. TxFreqSpinBox 11 111 1 11 11
2. RxFreqSpinBox 11 111 1 11 11 1
3. sbFtol 1 11 1 11 11 1
4. rptSpinBox 11 111 1 11 11 11 1
5. sbTR 11 1
6. sbCQRxFreq 1
7. cbShMsgs 1 1
8. cbFast9 11
9. cbAutoSeq 1
10. cbTx6 1
11. pbTxMode 1
12. pbR2T 11 11 1 11 11
13. pbT2R 11 11 1 11 11
14. cbTxLock 1 11 1 11 11
15. sbSubMode 1 1 1 11 11
16. syncSpinBox 1 1 1 11 11
17. WSPR_Controls_Widget 1
18. ClrAvgButton 11 1
---------------------------------------------------------------------
19. FastNormalDeep 11 11 1 11 1 1
20. IncludeAveraging 1 1
21. IncludeCorrelation 1 1
22. EchoGraph 1
---------------------------------------------------------------------
For each mode:
Column 1 applies when VHF features is OFF (or col 2 absent)
Column 2 (if present) applies when VHF features is ON
Column 3 (JT9 only) applies for submodes E-H with Fast checked

75
GetUserId.cpp Normal file
View File

@ -0,0 +1,75 @@
#include "GetUserId.hpp"
#include <stdexcept>
#include <QApplication>
#include <QString>
#include <QDialog>
#include <QLineEdit>
#include <QRegExpValidator>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QVBoxLayout>
//
// Dialog to get callsign
//
class CallsignDialog final
: public QDialog
{
Q_OBJECT;
private:
Q_DISABLE_COPY (CallsignDialog);
public:
explicit CallsignDialog (QWidget * parent = nullptr)
: QDialog {parent}
{
setWindowTitle (QApplication::applicationName () + " - " + tr ("Callsign"));
callsign_.setValidator (new QRegExpValidator {QRegExp {"[A-Za-z0-9]+"}, this});
auto form_layout = new QFormLayout ();
form_layout->addRow ("&Callsign:", &callsign_);
auto main_layout = new QVBoxLayout (this);
main_layout->addLayout (form_layout);
auto button_box = new QDialogButtonBox {QDialogButtonBox::Ok | QDialogButtonBox::Cancel};
main_layout->addWidget (button_box);
connect (button_box, &QDialogButtonBox::accepted, this, &CallsignDialog::accept);
connect (button_box, &QDialogButtonBox::rejected, this, &CallsignDialog::reject);
}
QString callsign () const {return callsign_.text ();}
private:
QLineEdit callsign_;
};
#include "GetUserId.moc"
QString get_user_id ()
{
// get the users callsign so we can use it to persist the
// settings and log file against a unique tag
QString id;
{
CallsignDialog dialog;
while (id.isEmpty ())
{
if (QDialog::Accepted == dialog.exec ())
{
id = dialog.callsign ().toUpper ();
}
else
{
throw std::runtime_error ("Callsign required");
}
}
}
return id;
}

8
GetUserId.hpp Normal file
View File

@ -0,0 +1,8 @@
#ifndef GETUSERID_HPP_
#define GETUSERID_HPP_
#include <QString>
QString get_user_id ();
#endif

383
INSTALL Normal file
View File

@ -0,0 +1,383 @@
__ __ ______ _____ ________ __ __
| \ _ | \ / \ | \| \ | \ | \
| $$ / \ | $$| $$$$$$\ \$$$$$ \$$$$$$$$ | $$ | $$
| $$/ $\| $$| $$___\$$ | $$ | $$ ______ \$$\/ $$
| $$ $$$\ $$ \$$ \ __ | $$ | $$| \ >$$ $$
| $$ $$\$$\$$ _\$$$$$$\| \ | $$ | $$ \$$$$$$/ $$$$\
| $$$$ \$$$$| \__| $$| $$__| $$ | $$ | $$ \$$\
| $$$ \$$$ \$$ $$ \$$ $$ | $$ | $$ | $$
\$$ \$$ \$$$$$$ \$$$$$$ \$$ \$$ \$$
Installing WSJT-X
=================
Binary packages of WSJT-X are available from the project web site:
http://www.physics.princeton.edu/pulsar/K1JT/wsjtx.html
Building from Source
====================
On Linux systems some of the prerequisite libraries are available in
the mainstream distribution repositories. They are Qt v5, FFTW v3, and
the Boost C++ libraries. 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.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
multimedia development, Qt serialport development packages, and the Qt
Linguist packages are sufficient to pull in all the required Qt
components and dependants as a single transaction. On some systems
the Qt multimedia plugin component is separate in the distribution
repository an it may also need installing.
The single precision FFTW v3 library libfftw3f is required along with
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 requires the readline development package and
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 interface then it
is required.
The Hamlib library is required. Currently WSJT-X needs to be built
using a forked version of the Hamlib git master. This fork contains
patches not yet accepted by the Hamlib development team which are
essential for correct operation of WSJT-X. To build the Hamlib fork
from sources something like the following recipe should suffice:
$ mkdir ~/hamlib-prefix
$ cd ~/hamlib-prefix
$ git clone git://git.code.sf.net/u/bsomervi/hamlib src
$ cd src
$ git checkout integration
$ ./bootstrap
$ mkdir ../build
$ cd ../build
$ ../src/configure --prefix=$HOME/hamlib-prefix \
--disable-shared --enable-static \
--without-cxx-binding --disable-winradio \
CFLAGS="-g -O2 -fdata-sections -ffunction-sections" \
LDFLAGS="-Wl,--gc-sections"
$ make
$ make install-strip
This will build a binary hamlib package located at ~/hamlib-prefix so
you will need to add that to your CMAKE_PREFIX_PATH variable in your
WSJT-X build. On Linux that is probably the only path you have on
CMAKE_PREFIX_PATH unless you are using a locally installed Qt
installation.
To get the sources either download and extract a source tarball from
the project web site or preferably fetch the sources directly from the
project's subversion repository.
$ mkdir -p ~/wsjtx-prefix/build
$ cd ~/wsjtx-prefix
$ 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 -DWSJT_SKIP_MANPAGES=ON \
-DWSJT_GENERATE_DOCS=OFF ../src
$ cmake --build .
$ cmake --build . --target install
The recipe above will install into /usr by default, if you wish to
install in you own directory you can add a prefix-path to the
configure step like:
$ cd ~/wsjtx-prefix/build
$ cmake -D CMAKE_PREFIX_PATH=~/hamlib-prefix \
-DWSJT_SKIP_MANPAGES=ON -DWSJT_GENERATE_DOCS=OFF \
-D CMAKE_INSTALL_PREFIX=~/wsjtx-prefix ../src
$ cmake --build .
$ cmake --build . --target install
this will install WSJT-X at ~/wsjtx-prefix.
Building from Source on MS Windows
==================================
Because building on MS Windows is quite complicated there is an
Software Development Kit available that provides all the prerequisite
libraries and tools for building WSJT-X. This SDK is called JT-SDK-QT
which is documented here:
http://physics.princeton.edu/pulsar/K1JT/wsjtx-doc/dev-guide-main.html
If you need to build Hamlib rather than use the Hamlib kit included in
the JT-SDK the following recipe should help. Reasons for building
Hamlib from source might include picking up the very latest patches or
building a different branch that you wish to contribute to.
Hamlib optionally depends upon libusb-1.0, see "Building from Source"
above for more details. If you wish to include support for the
optional custom USB Hamlib rig drivers then you must install
libusb-1.0 before building Hamlib. The package may be obtained from
http://libusb.info/, install it in a convenient location like
C:\Tools.
On Windows there is a complication in that the compilers used to build
Qt and WSJT-X are the MinGW ones bundled with the Qt package but
Hamlib needs to be build from an MSYS shell with the tools required to
build an autotools project. This means that you need to tell the
Hamlib configuration to use the Qt bundled MinGW compilers (if you
don't then the thread support library use by Hamlib will be
incompatible with that used by Qt and WSJT-X). So on Windows the
Hamlib build recipe is something like:
In an MSYS shell:-
$ mkdir ~/hamib-prefix
$ cd ~/hamlib-prefix
$ git clone git://git.code.sf.net/u/bsomervi/hamlib src
$ cd src
$ git checkout integration
$ ./bootstrap
$ mkdir ../build
$ cd ../build
../src/configure --prefix=$HOME/hamlib-prefix \
--disable-shared --enable-static \
--without-cxx-binding --disable-winradio \
CC=<path-to-Qt-MinGW-tools>/gcc \
CXX=<path-to-Qt-MinGW-tools>/g++ \
CFLAGS="-g -O2 -fdata-sections -ffunction-sections -I<path-to-libusb-1.0>/include" \
LDFLAGS="-Wl,--gc-sections" \
LIBUSB_LIBS="-L<path-to-libusb-1.0>/MinGW32/dll -lusb-1.0"
$ make
$ make install
NOTE: <path-to-Qt-MinGQ-tools> should be substituted with the actual
path to your Qt bundled tools e.g on my system it is
C:\Tools\Qt\Tools\mingw530_32\bin
NOTE: <path-to-libusb-1.0> should be substituted with the actual path
to your libusb-1.0 installation directory e.g. on my system it is
C:\Tools\libusb-1.0.20
This will leave a Hamlib binary package installed at
c:/Users/<user-name>/hamlib-prefix which is what needs to be on your
CMAKE_PREFIX_PATH. On Windows you almost certainly will be using a
CMake tool chain file and this is where you will need to specify the
Hamlib binary location as one of the paths in CMAKE_PREFIX_PATH.
Building from Source on Apple Mac
=================================
These instructions are adapted from my Evernote page at:
https://www.evernote.com/pub/bsomervi/wsjt-xmacbuilds
There are several ways to get the required GNU and other open source
tools and libraries installed, my preference is MacPorts because it is
easy to use and does everything we need.
You will need Xcode, MacPorts, CMake and, Qt. The Xcode install
instructions are included in the MacPorts documentation.
MacPorts
--------
Install MacPorts from instructions here:
http://www.macports.org/install.php
More detailed instructions are available in the documentation:
https://guide.macports.org
The ports that need to be installed are:
autoconf
automake
libtool
pkgconfig
texinfo
gcc5
fftw-3-single +gcc5
asciidoc
libusb-devel
These are installed by typing:
$ sudo port install autoconf automake \
libtool pkgconfig texinfo gcc5 asciidoc \
fftw-3-single +gcc5 libusb-devel
Once complete you should have all the tools required to build WSJT-X.
Uninstalling MacPorts
---------------------
If at some point you wish to remove the ports from your machine. The
instructions are here:
https://guide.macports.org/#installing.macports.uninstalling .
Hamlib
------
First fetch hamlib from the repository, in this case my fork of Hamlib
3 until the official repository has all the fixes we need:
$ mkdir -p ~/hamlib-prefix/build
$ cd ~/hamlib-prefix
$ git clone git://git.code.sf.net/u/bsomervi/hamlib src
$ cd src
$ git checkout integration
$ ./bootstrap
The integration branch is my system testing branch which has all my
latest published changes.
To build:
$ cd ~/hamlib-prefix/build
$ ../src/configure \
--enable-static \
--disable-shared \
--disable-winradio \
--prefix=$HOME/hamlib-prefix \
CFLAGS="-g -O2 -mmacosx-version-min=10.7 -I/opt/local/include" \
LIBUSB_LIBS="-L/opt/local/lib -lusb-1.0"
$ make
$ make install-strip
The above commands will build hamlib and install it into
~/hamlib-prefix. If `make install-strip` fails, try `make install`.
CMake
-----
Although CMake is available via MacPorts I prefer to use the binary
installer from cake.org as the MacPorts port doesn't include the
graphical CMake tool cmake-gui which I find quite useful.
Fetch the latest CMake universal 64-bit DMG from
http://www.cmake.org/download/ open the DMG then drag and drop the
application bundle onto the supplied /Applications link.
To complete the install process you need to run the CMake-gui
application as root from a terminal shell as follows:
$ sudo "/Applications/CMake.app/Contents/MacOS/cmake" --install
that installs the CMake command line tools which you can verify by
typing into a terminal window:
$ cmake --version
If the install command above fails with a "No such file or directory"
error, that probably means that /usr/local/bin does not exist. You can
create it correctly with the following commands:
$ sudo mkdir -p /usr/local/bin
$ sudo chmod 755 /usr/local/bin
$ sudo chgrp wheel /usr/local/bin
and then retry the install command.
Qt
--
Download the latest on-line installer package from the Qt web site and
isntall the latest Qt stable version development package.
WSJT-X
------
First fetch the source from the repository:
$ mkdir -p ~/wsjtx-prefix/build
$ cd ~/wsjtx-prefix
$ git clone git://git.code.sf.net/p/wsjt/wsjtx src
this links to the Subversion repository in a read-only fashion, if you
intend to contribute to the project then you probably want to get a
developer login and use a read-write checkout. Even if you don't it
can be upgraded at a later date.
The checkout is of the latest code on the project trunk, i.e. the
development branch. You can easily switch the checkout to another
branch or even a tag if you want to build a prior published
generation. For now we will build the latest development sources. To
configure:
$ cd ~/wsjtx-prefix/build
$ FC=gfortran-mp-5 \
cmake \
-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
Substitute the Mac OS X SDK version you have installed in the above
command if you have a different version from 10.11.
The CMAKE_PREFIX_PATH variable specifies where CMake should look first
for other packages, the two elements may be different depending where
you have installed Qt and what version you have (~/local/qt-macx-clang
if you have built Qt from sources as described above in the Qt
section) and where you installed Hamlib (i.e. the --prefix configure
option above in the Hamlib section).
If you already have the fftw3-dev package installed on your system it
may well get selected in preference to the one you built above in the
MacPorts installation. It is unlikely that a prior installation of
libfftw3f is correctly configured for use in a WSJT-X package, the
CMAKE_PREFIX_PATH above is augmented with the MacPorts installation
location (/opt/local) to ensure the correct libfftw3f.dylib and
headers are located.
To build:
$ cmake --build .
$ cmake --build . --target install
which installs the WSJT-X application bundle into ~/wsjtx-prefix
Updating and Rebuilding Hamlib
==============================
From time to time new fixes will be pushed to the Hamlib fork
repository integration branch. To pick them up type:
$ cd ~/hamlib-prefix/src
$ git pull
To rebuild hamlib with the changed sources:
$ cd ~/hamlib-prefix/build
$ make
$ make install-strip
Updating and Rebuilding WSJT-X
==============================
To update to the latest sources type:
$ cd ~/wsjtx-prefix/src
$ git pull
$ cd ~/wsjtx-prefix/build
$ cmake --build .
$ cmake --build . --target install
73
Bill
G4WJS.

221
L10nLoader.cpp Normal file
View File

@ -0,0 +1,221 @@
#include "L10nLoader.hpp"
#include <vector>
#include <memory>
#include <QApplication>
#include <QLocale>
#include <QTranslator>
#include <QRegularExpression>
#include <QDebug>
#include "qt_helpers.hpp"
#include "Logger.hpp"
#include "pimpl_impl.hpp"
class L10nLoader::impl final
{
public:
explicit impl(QApplication * app)
: app_ {app}
{
}
bool load_translator (QString const& filename
, QString const& directory = QString {}
, QString const& search_delimiters = QString {}
, QString const& suffix = QString {})
{
std::unique_ptr<QTranslator> translator {new QTranslator};
if (translator->load (filename, directory, search_delimiters, suffix))
{
install (std::move (translator));
return true;
}
return false;
}
bool load_translator (QLocale const& locale, QString const& filename
, QString const& prefix = QString {}
, QString const& directory = QString {}
, QString const& suffix = QString {})
{
std::unique_ptr<QTranslator> translator {new QTranslator};
if (translator->load (locale, filename, prefix, directory, suffix))
{
install (std::move (translator));
return true;
}
return false;
}
void install (std::unique_ptr<QTranslator> translator)
{
app_->installTranslator (translator.get ());
translators_.push_back (std::move (translator));
}
QApplication * app_;
std::vector<std::unique_ptr<QTranslator>> translators_;
};
L10nLoader::L10nLoader (QApplication * app, QLocale const& locale, QString const& language_override)
: m_ {app}
{
LOG_INFO (QString {"locale: language: %1 script: %2 country: %3 ui-languages: %4"}
.arg (QLocale::languageToString (locale.language ()))
.arg (QLocale::scriptToString (locale.script ()))
.arg (QLocale::countryToString (locale.country ()))
.arg (locale.uiLanguages ().join (", ")));
// we don't load translators if the language override is 'en',
// 'en_US', or 'en-US'. In these cases we assume the user is trying
// to disable translations loaded because of their locale. We cannot
// load any locale based translations in this case.
auto skip_locale = language_override.contains (QRegularExpression {"^(?:en|en[-_]US)$"});
//
// Enable base i18n
//
QString translations_dir {":/Translations"};
if (!skip_locale)
{
LOG_TRACE ("Looking for locale based Qt translations in resources filesystem");
if (m_->load_translator (locale, "qt", "_", translations_dir))
{
LOG_INFO ("Loaded Qt translations for current locale from resources");
}
// Default translations for releases use translations stored in
// the resources file system under the Translations
// directory. These are built by the CMake build system from .ts
// files in the translations source directory. New languages are
// added by enabling the UPDATE_TRANSLATIONS CMake option and
// building with the new language added to the LANGUAGES CMake
// list variable. UPDATE_TRANSLATIONS will preserve existing
// translations but should only be set when adding new
// languages. The resulting .ts files should be checked info
// source control for translators to access and update.
// try and load the base translation
LOG_TRACE ("Looking for WSJT-X translations based on UI languages in the resources filesystem");
for (QString locale_name : locale.uiLanguages ())
{
auto language = locale_name.left (2);
if (locale.uiLanguages ().front ().left (2) == language)
{
LOG_TRACE (QString {"Trying %1"}.arg (language));
if (m_->load_translator ("wsjtx_" + language, translations_dir))
{
LOG_INFO (QString {"Loaded WSJT-X base translation file from %1 based on language %2"}
.arg (translations_dir)
.arg (language));
break;
}
}
}
// now try and load the most specific translations (may be a
// duplicate but we shouldn't care)
LOG_TRACE ("Looking for WSJT-X translations based on locale in the resources filesystem");
if (m_->load_translator (locale, "wsjtx", "_", translations_dir))
{
LOG_INFO ("Loaded WSJT-X translations for current locale from resources");
}
}
// Load any matching translation from the current directory
// using the command line option language override. This allows
// translators to easily test their translations by releasing
// (lrelease) a .qm file into the current directory with a
// suitable name (e.g. wsjtx_en_GB.qm), then running wsjtx to
// view the results.
if (language_override.size ())
{
auto language = language_override;
language.replace ('-', '_');
// try and load the base translation
auto base_language = language.left (2);
LOG_TRACE ("Looking for WSJT-X translations based on command line region override in the resources filesystem");
if (m_->load_translator ("wsjtx_" + base_language, translations_dir))
{
LOG_INFO (QString {"Loaded base translation file from %1 based on language %2"}
.arg (translations_dir)
.arg (base_language));
}
// now load the requested translations (may be a duplicate
// but we shouldn't care)
LOG_TRACE ("Looking for WSJT-X translations based on command line override country in the resources filesystem");
if (m_->load_translator ("wsjtx_" + language, translations_dir))
{
LOG_INFO (QString {"Loaded translation file from %1 based on language %2"}
.arg (translations_dir)
.arg (language));
}
}
// Load any matching translation from the current directory using
// the current locale. This allows translators to easily test their
// translations by releasing (lrelease) a .qm file into the current
// directory with a suitable name (e.g. wsjtx_en_GB.qm), then
// running wsjtx to view the results. The system locale setting will
// be used to select the translation file which can be overridden by
// the LANG environment variable on non-Windows system.
// try and load the base translation
LOG_TRACE ("Looking for WSJT-X translations based on command line override country in the current directory");
for (QString locale_name : locale.uiLanguages ())
{
auto language = locale_name.left (2);
if (locale.uiLanguages ().front ().left (2) == language)
{
LOG_TRACE (QString {"Trying %1"}.arg (language));
if (m_->load_translator ("wsjtx_" + language))
{
LOG_INFO (QString {"Loaded base translation file from $cwd based on language %1"}.arg (language));
break;
}
}
}
if (!skip_locale)
{
// now try and load the most specific translations (may be a
// duplicate but we shouldn't care)
LOG_TRACE ("Looking for WSJT-X translations based on locale in the resources filesystem");
if (m_->load_translator (locale, "wsjtx", "_"))
{
LOG_INFO ("loaded translations for current locale from a file");
}
}
// Load any matching translation from the current directory using
// the command line option language override. This allows
// translators to easily test their translations on Windows by
// releasing (lrelease) a .qm file into the current directory with a
// suitable name (e.g. wsjtx_en_GB.qm), then running wsjtx to view
// the results.
if (language_override.size ())
{
auto language = language_override;
language.replace ('-', '_');
// try and load the base translation
auto base_language = language.left (2);
LOG_TRACE ("Looking for WSJT-X translations based on command line override country in the current directory");
if (m_->load_translator ("wsjtx_" + base_language))
{
LOG_INFO (QString {"Loaded base translation file from $cwd based on language %1"}.arg (base_language));
}
// now load the requested translations (may be a duplicate
// but we shouldn't care)
LOG_TRACE ("Looking for WSJT-X translations based on command line region in the current directory");
if (m_->load_translator ("wsjtx_" + language))
{
LOG_INFO (QString {"loaded translation file from $cwd based on language %1"}.arg (language));
}
}
}
L10nLoader::~L10nLoader ()
{
}

21
L10nLoader.hpp Normal file
View File

@ -0,0 +1,21 @@
#ifndef WSJTX_L10N_LOADER_HPP__
#define WSJTX_L10N_LOADER_HPP__
#include <QString>
#include "pimpl_h.hpp"
class QApplication;
class QLocale;
class L10nLoader final
{
public:
explicit L10nLoader (QApplication *, QLocale const&, QString const& language_override = QString {});
~L10nLoader ();
private:
class impl;
pimpl<impl> m_;
};
#endif

185
Logger.cpp Normal file
View File

@ -0,0 +1,185 @@
#include "Logger.hpp"
#include <boost/algorithm/string/predicate.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>
#include <boost/log/core.hpp>
#include <boost/log/common.hpp>
#include <boost/log/sinks.hpp>
#include <boost/log/expressions.hpp>
#include <boost/log/expressions/keyword.hpp>
#include <boost/log/attributes.hpp>
#include <boost/log/attributes/clock.hpp>
#include <boost/log/attributes/counter.hpp>
#include <boost/log/attributes/current_process_id.hpp>
#include <boost/log/attributes/current_thread_id.hpp>
#include <boost/log/utility/setup/console.hpp>
#include <boost/log/utility/setup/filter_parser.hpp>
#include <boost/log/utility/setup/from_stream.hpp>
#include <boost/log/utility/setup/settings.hpp>
#include <boost/log/sinks/sync_frontend.hpp>
#include <boost/log/sinks/text_ostream_backend.hpp>
#include <boost/log/support/date_time.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/make_shared.hpp>
#include <boost/filesystem/fstream.hpp>
#include <string>
namespace fs = boost::filesystem;
namespace logging = boost::log;
namespace srcs = logging::sources;
namespace sinks = logging::sinks;
namespace keywords = logging::keywords;
namespace expr = logging::expressions;
namespace attrs = logging::attributes;
namespace ptime = boost::posix_time;
BOOST_LOG_GLOBAL_LOGGER_CTOR_ARGS (sys,
srcs::severity_channel_logger_mt<logging::trivial::severity_level>,
(keywords::channel = "SYSLOG"));
BOOST_LOG_GLOBAL_LOGGER_CTOR_ARGS (data,
srcs::severity_channel_logger_mt<logging::trivial::severity_level>,
(keywords::channel = "DATALOG"));
namespace Logger
{
namespace
{
// Custom formatter factory to add TimeStamp format support in config ini file.
// Allows %TimeStamp(format=\"%Y.%m.%d %H:%M:%S.%f\")% to be used in ini config file for property Format.
class TimeStampFormatterFactory
: public logging::basic_formatter_factory<char, ptime::ptime>
{
public:
formatter_type create_formatter (logging::attribute_name const& name, args_map const& args)
{
args_map::const_iterator it = args.find ("format");
if (it != args.end ())
{
return expr::stream
<< expr::format_date_time<ptime::ptime>
(
expr::attr<ptime::ptime> (name), it->second
);
}
else
{
return expr::stream
<< expr::attr<ptime::ptime> (name);
}
}
};
// Custom formatter factory to add Uptime format support in config ini file.
// Allows %Uptime(format=\"%O:%M:%S.%f\")% to be used in ini config file for property Format.
// attrs::timer value type is ptime::time_duration
class UptimeFormatterFactory
: public logging::basic_formatter_factory<char, ptime::time_duration>
{
public:
formatter_type create_formatter (logging::attribute_name const& name, args_map const& args)
{
args_map::const_iterator it = args.find ("format");
if (it != args.end ())
{
return expr::stream
<< expr::format_date_time<ptime::time_duration>
(
expr::attr<ptime::time_duration> (name), it->second
);
}
else
{
return expr::stream
<< expr::attr<ptime::time_duration> (name);
}
}
};
class CommonInitialization
{
public:
CommonInitialization ()
{
// Add attributes: LineID, TimeStamp, ProcessID, ThreadID, and Uptime
auto core = logging::core::get ();
core->add_global_attribute ("LineID", attrs::counter<unsigned int> (1));
core->add_global_attribute ("TimeStamp", attrs::utc_clock ());
core->add_global_attribute ("ProcessID", attrs::current_process_id ());
core->add_global_attribute ("ThreadID", attrs::current_thread_id ());
core->add_global_attribute ("Uptime", attrs::timer ());
// Allows %Severity% to be used in ini config file for property Filter.
logging::register_simple_filter_factory<logging::trivial::severity_level, char> ("Severity");
// Allows %Severity% to be used in ini config file for property Format.
logging::register_simple_formatter_factory<logging::trivial::severity_level, char> ("Severity");
// Allows %TimeStamp(format=\"%Y.%m.%d %H:%M:%S.%f\")% to be used in ini config file for property Format.
logging::register_formatter_factory ("TimeStamp", boost::make_shared<TimeStampFormatterFactory> ());
// Allows %Uptime(format=\"%O:%M:%S.%f\")% to be used in ini config file for property Format.
logging::register_formatter_factory ("Uptime", boost::make_shared<UptimeFormatterFactory> ());
}
~CommonInitialization ()
{
}
};
}
void init ()
{
CommonInitialization ci;
}
void init_from_config (std::wistream& stream)
{
CommonInitialization ci;
try
{
// Still can throw even with the exception suppressor above.
logging::init_from_stream (stream);
}
catch (std::exception& e)
{
std::string err = "Caught exception initializing boost logging: ";
err += e.what ();
// Since we cannot be sure of boost log state, output to cerr and cout.
std::cerr << "ERROR: " << err << std::endl;
LOG_ERROR (err);
throw;
}
}
void disable ()
{
logging::core::get ()->set_logging_enabled (false);
}
void add_datafile_log (std::wstring const& log_file_name)
{
// Create a text file sink
boost::shared_ptr<sinks::wtext_ostream_backend> backend
(
new sinks::wtext_ostream_backend()
);
backend->add_stream (boost::shared_ptr<std::wostream> (new fs::wofstream (log_file_name)));
// Flush after each log record
backend->auto_flush (true);
// Create a sink for the backend
typedef sinks::synchronous_sink<sinks::wtext_ostream_backend> sink_t;
boost::shared_ptr<sink_t> sink (new sink_t (backend));
// The log output formatter
sink->set_formatter (expr::format (L"[%1%][%2%] %3%")
% expr::attr<ptime::ptime> ("TimeStamp")
% logging::trivial::severity
% expr::message
);
// Filter by severity and by DATALOG channel
sink->set_filter (logging::trivial::severity >= logging::trivial::info &&
expr::attr<std::string> ("Channel") == "DATALOG");
// Add it to the core
logging::core::get ()->add_sink (sink);
}
}

58
Logger.hpp Normal file
View File

@ -0,0 +1,58 @@
#ifndef LOGGER_HPP__
#define LOGGER_HPP__
#include <boost/log/trivial.hpp>
#include <boost/log/sources/global_logger_storage.hpp>
#include <boost/log/sources/severity_channel_logger.hpp>
#include <boost/log/sources/record_ostream.hpp>
#include <boost/log/attributes/mutable_constant.hpp>
#include <boost/log/utility/manipulators/add_value.hpp>
#include <iosfwd>
#include <string>
BOOST_LOG_GLOBAL_LOGGER (sys,
boost::log::sources::severity_channel_logger_mt<boost::log::trivial::severity_level>);
BOOST_LOG_GLOBAL_LOGGER (data,
boost::log::sources::severity_channel_logger_mt<boost::log::trivial::severity_level>);
namespace Logger
{
// trivial logging to console
void init ();
// define logger(s) and sinks from a configuration stream
void init_from_config (std::wistream& config_stream);
// disable logging - useful for unit testing etc.
void disable ();
// add a new file sink for LOG_DATA_* for Severity >= INFO
// this file sink will be used alongside any configured above
void add_data_file_log (std::wstring const& log_file_name);
}
#define LOG_LOG_LOCATION(LOGGER, LEVEL, ARG) \
BOOST_LOG_SEV (LOGGER, boost::log::trivial::LEVEL) \
<< boost::log::add_value ("Line", __LINE__) \
<< boost::log::add_value ("File", __FILE__) \
<< boost::log::add_value ("Function", __FUNCTION__) << ARG
/// System Log macros.
/// TRACE < DEBUG < INFO < WARN < ERROR < FATAL
#define LOG_TRACE(ARG) LOG_LOG_LOCATION (sys::get(), trace, ARG)
#define LOG_DEBUG(ARG) LOG_LOG_LOCATION (sys::get(), debug, ARG)
#define LOG_INFO(ARG) LOG_LOG_LOCATION (sys::get(), info, ARG)
#define LOG_WARN(ARG) LOG_LOG_LOCATION (sys::get(), warning, ARG)
#define LOG_ERROR(ARG) LOG_LOG_LOCATION (sys::get(), error, ARG)
#define LOG_FATAL(ARG) LOG_LOG_LOCATION (sys::get(), fatal, ARG)
/// Data Log macros. Does not include LINE, FILE, FUNCTION.
/// TRACE < DEBUG < INFO < WARN < ERROR < FATAL
#define LOG_DATA_TRACE(ARG) BOOST_LOG_SEV (data::get(), boost::log::trivial::trace) << ARG
#define LOG_DATA_DEBUG(ARG) BOOST_LOG_SEV (data::get(), boost::log::trivial::debug) << ARG
#define LOG_DATA_INFO(ARG) BOOST_LOG_SEV (data::get(), boost::log::trivial::info) << ARG
#define LOG_DATA_WARN(ARG) BOOST_LOG_SEV (data::get(), boost::log::trivial::warning) << ARG
#define LOG_DATA_ERROR(ARG) BOOST_LOG_SEV (data::get(), boost::log::trivial::error) << ARG
#define LOG_DATA_FATAL(ARG) BOOST_LOG_SEV (data::get(), boost::log::trivial::fatal) << ARG
#endif

99
MetaDataRegistry.cpp Normal file
View File

@ -0,0 +1,99 @@
#include "MetaDataRegistry.hpp"
#include <QMetaType>
#include <QItemEditorFactory>
#include <QStandardItemEditorCreator>
#include "Radio.hpp"
#include "models/FrequencyList.hpp"
#include "Audio/AudioDevice.hpp"
#include "Configuration.hpp"
#include "models/StationList.hpp"
#include "Transceiver/Transceiver.hpp"
#include "Transceiver/TransceiverFactory.hpp"
#include "WFPalette.hpp"
#include "models/IARURegions.hpp"
#include "models/DecodeHighlightingModel.hpp"
#include "widgets/DateTimeEdit.hpp"
namespace
{
class ItemEditorFactory final
: public QItemEditorFactory
{
public:
ItemEditorFactory ()
: default_factory_ {QItemEditorFactory::defaultFactory ()}
{
}
QWidget * createEditor (int user_type, QWidget * parent) const override
{
auto editor = QItemEditorFactory::createEditor (user_type, parent);
return editor ? editor : default_factory_->createEditor (user_type, parent);
}
private:
QItemEditorFactory const * default_factory_;
};
}
void register_types ()
{
auto item_editor_factory = new ItemEditorFactory;
QItemEditorFactory::setDefaultFactory (item_editor_factory);
// types in Radio.hpp are registered in their own translation unit
// as they are needed in the wsjtx_udp shared library too
// we still have to register the fully qualified names of enum types
// used as signal/slot connection arguments since the new Qt 5.5
// Q_ENUM macro only seems to register the unqualified name
item_editor_factory->registerEditor (qMetaTypeId<QDateTime> (), new QStandardItemEditorCreator<DateTimeEdit> ());
// Frequency list model
qRegisterMetaTypeStreamOperators<FrequencyList_v2::Item> ("Item_v2");
QMetaType::registerConverter<FrequencyList_v2::Item, QString> (&FrequencyList_v2::Item::toString);
qRegisterMetaTypeStreamOperators<FrequencyList_v2::FrequencyItems> ("FrequencyItems_v2");
// defunct old versions
qRegisterMetaTypeStreamOperators<FrequencyList::Item> ("Item");
qRegisterMetaTypeStreamOperators<FrequencyList::FrequencyItems> ("FrequencyItems");
// Audio device
qRegisterMetaType<AudioDevice::Channel> ("AudioDevice::Channel");
// Configuration
qRegisterMetaTypeStreamOperators<Configuration::DataMode> ("Configuration::DataMode");
qRegisterMetaTypeStreamOperators<Configuration::Type2MsgGen> ("Configuration::Type2MsgGen");
// Station details
qRegisterMetaType<StationList::Station> ("Station");
QMetaType::registerConverter<StationList::Station, QString> (&StationList::Station::toString);
qRegisterMetaType<StationList::Stations> ("Stations");
qRegisterMetaTypeStreamOperators<StationList::Station> ("Station");
qRegisterMetaTypeStreamOperators<StationList::Stations> ("Stations");
// Transceiver
qRegisterMetaType<Transceiver::TransceiverState> ("Transceiver::TransceiverState");
// Transceiver factory
qRegisterMetaTypeStreamOperators<TransceiverFactory::DataBits> ("TransceiverFactory::DataBits");
qRegisterMetaTypeStreamOperators<TransceiverFactory::StopBits> ("TransceiverFactory::StopBits");
qRegisterMetaTypeStreamOperators<TransceiverFactory::Handshake> ("TransceiverFactory::Handshake");
qRegisterMetaTypeStreamOperators<TransceiverFactory::PTTMethod> ("TransceiverFactory::PTTMethod");
qRegisterMetaTypeStreamOperators<TransceiverFactory::TXAudioSource> ("TransceiverFactory::TXAudioSource");
qRegisterMetaTypeStreamOperators<TransceiverFactory::SplitMode> ("TransceiverFactory::SplitMode");
// Waterfall palette
qRegisterMetaTypeStreamOperators<WFPalette::Colours> ("Colours");
// IARURegions
qRegisterMetaTypeStreamOperators<IARURegions::Region> ("IARURegions::Region");
// DecodeHighlightingModel
qRegisterMetaTypeStreamOperators<DecodeHighlightingModel::HighlightInfo> ("HighlightInfo");
QMetaType::registerConverter<DecodeHighlightingModel::HighlightInfo, QString> (&DecodeHighlightingModel::HighlightInfo::toString);
qRegisterMetaTypeStreamOperators<DecodeHighlightingModel::HighlightItems> ("HighlightItems");
}

6
MetaDataRegistry.hpp Normal file
View File

@ -0,0 +1,6 @@
#ifndef META_DATA_REGISTRY_HPP__
#define META_DATA_REGISTRY_HPP__
void register_types ();
#endif

382
Modulator/Modulator.cpp Normal file
View File

@ -0,0 +1,382 @@
#include "Modulator.hpp"
#include <limits>
#include <qmath.h>
#include <QDateTime>
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
#include <QRandomGenerator>
#endif
#include <QDebug>
#include "widgets/mainwindow.h" // TODO: G4WJS - break this dependency
#include "Audio/soundout.h"
#include "commons.h"
#include "moc_Modulator.cpp"
extern float gran(); // Noise generator (for tests only)
#define RAMP_INCREMENT 64 // MUST be an integral factor of 2^16
#if defined (WSJT_SOFT_KEYING)
# define SOFT_KEYING WSJT_SOFT_KEYING
#else
# define SOFT_KEYING 1
#endif
double constexpr Modulator::m_twoPi;
// float wpm=20.0;
// unsigned m_nspd=1.2*48000.0/wpm;
// m_nspd=3072; //18.75 WPM
Modulator::Modulator (unsigned frameRate, double periodLengthInSeconds,
QObject * parent)
: AudioDevice {parent}
, m_quickClose {false}
, m_phi {0.0}
, m_toneSpacing {0.0}
, m_fSpread {0.0}
, m_period {periodLengthInSeconds}
, m_frameRate {frameRate}
, m_state {Idle}
, m_tuning {false}
, m_cwLevel {false}
, m_j0 {-1}
, m_toneFrequency0 {1500.0}
{
}
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;
unsigned mstr = ms0 % int(1000.0*m_period); // ms into the nominal Tx start time
if(m_state != Idle) stop();
m_quickClose = false;
m_symbolsLength = symbolsLength;
m_isym0 = std::numeric_limits<unsigned>::max (); // big number
m_frequency0 = 0.;
m_phi = 0.;
m_addNoise = dBSNR < 0.;
m_nsps = framesPerSymbol;
m_frequency = frequency;
m_amp = std::numeric_limits<qint16>::max ();
m_toneSpacing = toneSpacing;
m_bFastMode=fastMode;
m_TRperiod=TRperiod;
unsigned delay_ms=1000;
if(mode=="FT8" or (mode=="FST4" and m_nsps==720)) delay_ms=500; //FT8, FST4-15
if(mode=="Q65" and m_nsps<=3600) delay_ms=500; //Q65-15 and Q65-30
if(mode=="FT4") delay_ms=300; //FT4
// noise generator parameters
if (m_addNoise) {
m_snr = qPow (10.0, 0.05 * (dBSNR - 6.0));
m_fac = 3000.0;
if (m_snr > 1.0) m_fac = 3000.0 / m_snr;
}
m_silentFrames = 0;
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);
}
else
{
qDebug () << "Modulator::start: no audio output stream assigned";
}
}
void Modulator::tune (bool newState)
{
m_tuning = newState;
if (!m_tuning) stop (true);
}
void Modulator::stop (bool quick)
{
m_quickClose = quick;
close ();
}
void Modulator::close ()
{
if (m_stream)
{
if (m_quickClose)
{
m_stream->reset ();
}
else
{
m_stream->stop ();
}
}
if (m_state != Idle)
{
Q_EMIT stateChanged ((m_state = Idle));
}
AudioDevice::close ();
}
qint64 Modulator::readData (char * data, qint64 maxSize)
{
double toneFrequency=1500.0;
if(m_nsps==6) {
toneFrequency=1000.0;
m_frequency=1000.0;
m_frequency0=1000.0;
}
if(maxSize==0) return 0;
Q_ASSERT (!(maxSize % qint64 (bytesPerFrame ()))); // no torn frames
Q_ASSERT (isOpen ());
qint64 numFrames (maxSize / bytesPerFrame ());
qint16 * samples (reinterpret_cast<qint16 *> (data));
qint16 * end (samples + numFrames * (bytesPerFrame () / sizeof (qint16)));
qint64 framesGenerated (0);
// 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 end of start delay
framesGenerated = qMin (m_silentFrames, numFrames);
do
{
samples = load (0, samples); // silence
} while (--m_silentFrames && samples != end);
if (!m_silentFrames)
{
Q_EMIT stateChanged ((m_state = Active));
}
}
m_cwLevel = false;
m_ramp = 0; // prepare for CW wave shaping
}
// fall through
case Active:
{
unsigned int isym=0;
if(!m_tuning) isym=m_ic/(4.0*m_nsps); // Actual fsample=48000
bool slowCwId=((isym >= m_symbolsLength) && (icw[0] > 0)) && (!m_bFastMode);
if(m_TRperiod==3.0) slowCwId=false;
bool fastCwId=false;
static bool bCwId=false;
qint64 ms = QDateTime::currentMSecsSinceEpoch();
float tsec=0.001*(ms % int(1000*m_TRperiod));
if(m_bFastMode and (icw[0]>0) and (tsec > (m_TRperiod-5.0))) fastCwId=true;
if(!m_bFastMode) m_nspd=2560; // 22.5 WPM
// qDebug() << "Mod A" << m_ic << isym << tsec;
if(slowCwId or fastCwId) { // Transmit CW ID?
m_dphi = m_twoPi*m_frequency/m_frameRate;
if(m_bFastMode and !bCwId) {
m_frequency=1500; // Set params for CW ID
m_dphi = m_twoPi*m_frequency/m_frameRate;
m_symbolsLength=126;
m_nsps=4096.0*12000.0/11025.0;
m_ic=2246949;
m_nspd=2560; // 22.5 WPM
if(icw[0]*m_nspd/48000.0 > 4.0) m_nspd=4.0*48000.0/icw[0]; //Faster CW for long calls
}
bCwId=true;
unsigned ic0 = m_symbolsLength * 4 * m_nsps;
unsigned j(0);
while (samples != end) {
j = (m_ic - ic0)/m_nspd + 1; // symbol of this sample
bool level {bool (icw[j])};
m_phi += m_dphi;
if (m_phi > m_twoPi) m_phi -= m_twoPi;
qint16 sample=0;
float amp=32767.0;
float x=0;
if(m_ramp!=0) {
x=qSin(float(m_phi));
if(SOFT_KEYING) {
amp=qAbs(qint32(m_ramp));
if(amp>32767.0) amp=32767.0;
}
sample=round(amp*x);
}
if(m_bFastMode) {
sample=0;
if(level) sample=32767.0*x;
}
if (int (j) <= icw[0] && j < NUM_CW_SYMBOLS) { // stop condition
samples = load (postProcessSample (sample), samples);
++framesGenerated;
++m_ic;
} else {
Q_EMIT stateChanged ((m_state = Idle));
return framesGenerated * bytesPerFrame ();
}
// adjust ramp
if ((m_ramp != 0 && m_ramp != std::numeric_limits<qint16>::min ()) || level != m_cwLevel) {
// either ramp has terminated at max/min or direction has changed
m_ramp += RAMP_INCREMENT; // ramp
}
m_cwLevel = level;
}
return framesGenerated * bytesPerFrame ();
} else {
bCwId=false;
} //End of code for CW ID
double const baud (12000.0 / m_nsps);
// fade out parameters (no fade out for tuning)
unsigned int i0,i1;
if(m_tuning) {
i1 = i0 = (m_bFastMode ? 999999 : 9999) * m_nsps;
} else {
i0=(m_symbolsLength - 0.017) * 4.0 * m_nsps;
i1= m_symbolsLength * 4.0 * m_nsps;
}
if(m_bFastMode and !m_tuning) {
i1=m_TRperiod*48000.0 - 24000.0;
i0=i1-816;
}
qint16 sample;
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;
if (isym != m_isym0 || m_frequency != m_frequency0) {
if(itone[0]>=100) {
m_toneFrequency0=itone[0];
} else {
if(m_toneSpacing==0.0) {
m_toneFrequency0=m_frequency + itone[isym]*baud;
} else {
m_toneFrequency0=m_frequency + itone[isym]*m_toneSpacing;
}
}
m_dphi = m_twoPi * m_toneFrequency0 / m_frameRate;
m_isym0 = isym;
m_frequency0 = m_frequency; //???
}
int j=m_ic/480;
if(m_fSpread>0.0 and j!=m_j0) {
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
float x1=QRandomGenerator::global ()->generateDouble ();
float x2=QRandomGenerator::global ()->generateDouble ();
#else
float x1=(float)qrand()/RAND_MAX;
float x2=(float)qrand()/RAND_MAX;
#endif
toneFrequency = m_toneFrequency0 + 0.5*m_fSpread*(x1+x2-1.0);
m_dphi = m_twoPi * toneFrequency / m_frameRate;
m_j0=j;
}
m_phi += m_dphi;
if (m_phi > m_twoPi) m_phi -= m_twoPi;
if (m_ic > i0) m_amp = 0.98 * m_amp;
if (m_ic > i1) m_amp = 0.0;
sample=qRound(m_amp*qSin(m_phi));
//Here's where we transmit from a precomputed wave[] array:
if(!m_tuning and (m_toneSpacing < 0) and (itone[0]<100)) {
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
Q_EMIT stateChanged ((m_state = Idle));
return framesGenerated * bytesPerFrame ();
}
m_phi = 0.0;
}
m_frequency0 = m_frequency;
// done for this chunk - continue on next call
// qDebug() << "Mod B" << m_ic << i1 << 0.001*(QDateTime::currentMSecsSinceEpoch() % (1000*m_TRperiod));
while (samples != end) // pad block with silence
{
samples = load (0, samples);
++framesGenerated;
}
return framesGenerated * bytesPerFrame ();
}
// fall through
case Idle:
break;
}
Q_ASSERT (Idle == m_state);
return 0;
}
qint16 Modulator::postProcessSample (qint16 sample) const
{
if (m_addNoise) { // Test frame, we'll add noise
qint32 s = m_fac * (gran () + sample * m_snr / 32768.0);
if (s > std::numeric_limits<qint16>::max ()) {
s = std::numeric_limits<qint16>::max ();
}
if (s < std::numeric_limits<qint16>::min ()) {
s = std::numeric_limits<qint16>::min ();
}
sample = s;
}
return sample;
}

96
Modulator/Modulator.hpp Normal file
View File

@ -0,0 +1,96 @@
#ifndef MODULATOR_HPP__
#define MODULATOR_HPP__
#include <QAudio>
#include <QPointer>
#include "Audio/AudioDevice.hpp"
class SoundOutput;
//
// Input device that generates PCM audio frames that encode a message
// and an optional CW ID.
//
// Output can be muted while underway, preserving waveform timing when
// transmission is resumed.
//
class Modulator
: public AudioDevice
{
Q_OBJECT;
public:
enum ModulatorState {Synchronizing, Active, Idle};
Modulator (unsigned frameRate, double periodLengthInSeconds, QObject * parent = nullptr);
void close () override;
bool isTuning () const {return m_tuning;}
double frequency () const {return m_frequency;}
bool isActive () const {return m_state != Idle;}
void setSpread(double s) {m_fSpread=s;}
void setTRPeriod(double p) {m_period=p;}
void set_nsym(int n) {m_symbolsLength=n;}
void set_ms0(qint64 ms) {m_ms0=ms;}
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);
Q_SLOT void stop (bool quick = false);
Q_SLOT void tune (bool newState = true);
Q_SLOT void setFrequency (double newFrequency) {m_frequency = newFrequency;}
Q_SIGNAL void stateChanged (ModulatorState) const;
protected:
qint64 readData (char * data, qint64 maxSize) override;
qint64 writeData (char const * /* data */, qint64 /* maxSize */) override
{
return -1; // we don't consume data
}
private:
qint16 postProcessSample (qint16 sample) const;
QPointer<SoundOutput> m_stream;
bool m_quickClose;
unsigned m_symbolsLength;
static double constexpr m_twoPi = 2.0 * 3.141592653589793238462;
unsigned m_nspd = 2048 + 512; // CW ID WPM factor = 22.5 WPM
double m_phi;
double m_dphi;
double m_amp;
double m_nsps;
double m_frequency;
double m_frequency0;
double m_snr;
double m_fac;
double m_toneSpacing;
double m_fSpread;
double m_TRperiod;
double m_period;
qint64 m_silentFrames;
qint64 m_ms0;
qint16 m_ramp;
unsigned m_frameRate;
ModulatorState m_state;
bool m_tuning;
bool m_addNoise;
bool m_bFastMode;
bool m_cwLevel;
unsigned m_ic;
unsigned m_isym0;
int m_j0;
double m_toneFrequency0;
};
#endif

3
Modulator/Modulator.pri Normal file
View File

@ -0,0 +1,3 @@
SOURCES += Modulator/Modulator.cpp
HEADERS += Modulator/Modulator.hpp

831
MultiSettings.cpp Normal file
View File

@ -0,0 +1,831 @@
#include "MultiSettings.hpp"
#include <stdexcept>
#include <QObject>
#include <QSettings>
#include <QString>
#include <QStringList>
#include <QDir>
#include <QFont>
#include <QApplication>
#include <QStandardPaths>
#include <QMainWindow>
#include <QMenu>
#include <QAction>
#include <QActionGroup>
#include <QDialog>
#include <QLineEdit>
#include <QRegularExpression>
#include <QRegularExpressionValidator>
#include <QFormLayout>
#include <QVBoxLayout>
#include <QDialogButtonBox>
#include <QPushButton>
#include <QComboBox>
#include <QLabel>
#include <QList>
#include <QMetaObject>
#include "SettingsGroup.hpp"
#include "qt_helpers.hpp"
#include "SettingsGroup.hpp"
#include "widgets/MessageBox.hpp"
#include "pimpl_impl.hpp"
namespace
{
char const * default_string = QT_TRANSLATE_NOOP ("MultiSettings", "Default");
char const * multi_settings_root_group = "MultiSettings";
char const * multi_settings_current_group_key = "CurrentMultiSettingsConfiguration";
char const * multi_settings_current_name_key = "CurrentName";
char const * multi_settings_place_holder_key = "MultiSettingsPlaceHolder";
QString unescape_ampersands (QString s)
{
return s.replace ("&&", "&");
}
// calculate a useable and unique settings file path
QString settings_path ()
{
auto const& config_directory = QStandardPaths::writableLocation (QStandardPaths::ConfigLocation);
QDir config_path {config_directory}; // will be "." if config_directory is empty
if (!config_path.mkpath ("."))
{
throw std::runtime_error {"Cannot find a usable configuration path \"" + config_path.path ().toStdString () + '"'};
}
return config_path.absoluteFilePath (QApplication::applicationName () + ".ini");
}
//
// Dialog to get a valid new configuration name
//
class NameDialog final
: public QDialog
{
Q_OBJECT
public:
explicit NameDialog (QString const& current_name,
QStringList const& current_names,
QWidget * parent = nullptr)
: QDialog {parent}
{
setWindowTitle (tr ("New Configuration Name"));
auto form_layout = new QFormLayout ();
form_layout->addRow (tr ("Old name:"), &old_name_label_);
old_name_label_.setText (current_name);
form_layout->addRow (tr ("&New name:"), &name_line_edit_);
auto main_layout = new QVBoxLayout (this);
main_layout->addLayout (form_layout);
auto button_box = new QDialogButtonBox {QDialogButtonBox::Ok | QDialogButtonBox::Cancel};
button_box->button (QDialogButtonBox::Ok)->setEnabled (false);
main_layout->addWidget (button_box);
auto name_validator = new QRegularExpressionValidator {QRegularExpression {R"([^/\\]+)"}, this};
name_line_edit_.setValidator (name_validator);
connect (&name_line_edit_, &QLineEdit::textChanged, [current_names, button_box] (QString const& name) {
bool valid {!current_names.contains (name.trimmed ())};
button_box->button (QDialogButtonBox::Ok)->setEnabled (valid);
});
connect (button_box, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect (button_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
}
QString new_name () const
{
return name_line_edit_.text ().trimmed ();
}
private:
QLabel old_name_label_;
QLineEdit name_line_edit_;
};
//
// Dialog to get a valid new existing name
//
class ExistingNameDialog final
: public QDialog
{
Q_OBJECT
public:
explicit ExistingNameDialog (QStringList const& current_names, QWidget * parent = nullptr)
: QDialog {parent}
{
setWindowTitle (tr ("Configuration to Clone From"));
name_combo_box_.addItems (current_names);
auto form_layout = new QFormLayout ();
form_layout->addRow (tr ("&Source Configuration Name:"), &name_combo_box_);
auto main_layout = new QVBoxLayout (this);
main_layout->addLayout (form_layout);
auto button_box = new QDialogButtonBox {QDialogButtonBox::Ok | QDialogButtonBox::Cancel};
main_layout->addWidget (button_box);
connect (button_box, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect (button_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
}
QString name () const
{
return name_combo_box_.currentText ();
}
private:
QComboBox name_combo_box_;
};
}
class MultiSettings::impl final
: public QObject
{
Q_OBJECT
public:
explicit impl (MultiSettings const * parent, QString const& config_name);
bool reposition ();
void create_menu_actions (QMainWindow * main_window, QMenu * menu);
bool exit ();
QSettings settings_;
QString current_;
// switch to this configuration
void select_configuration (QString const& target_name);
private:
using Dictionary = QMap<QString, QVariant>;
// create a configuration maintenance sub menu
QMenu * create_sub_menu (QMenu * parent,
QString const& menu_title,
QActionGroup * = nullptr);
// extract all settings from the current QSettings group
Dictionary get_settings () const;
// write the settings values from the dictionary to the current group
void load_from (Dictionary const&, bool add_placeholder = true);
// clone this configuration
void clone_configuration (QMenu *, QMenu const *);
// update this configuration from another
void clone_into_configuration (QMenu const *);
// reset configuration to default values
void reset_configuration (QMenu const *);
// change configuration name
void rename_configuration (QMenu *);
// remove a configuration
void delete_configuration (QMenu *);
// action to take on restart
enum class RepositionType {unchanged, replace, save_and_replace};
void restart (RepositionType);
MultiSettings const * parent_; // required for emitting signals
QMainWindow * main_window_;
bool name_change_emit_pending_; // delayed until menu built
QFont original_font_;
RepositionType reposition_type_;
Dictionary new_settings_;
bool exit_flag_; // false means loop around with new
// configuration
QActionGroup * configurations_group_;
};
#include "MultiSettings.moc"
#include "moc_MultiSettings.cpp"
MultiSettings::MultiSettings (QString const& config_name)
: m_ {this, config_name}
{
}
MultiSettings::~MultiSettings ()
{
}
QSettings * MultiSettings::settings ()
{
return &m_->settings_;
}
QVariant MultiSettings::common_value (QString const& key, QVariant const& default_value) const
{
QVariant value;
QSettings * mutable_settings {const_cast<QSettings *> (&m_->settings_)};
auto const& current_group = mutable_settings->group ();
if (current_group.size ()) mutable_settings->endGroup ();
{
SettingsGroup alternatives {mutable_settings, multi_settings_root_group};
value = mutable_settings->value (key, default_value);
}
if (current_group.size ()) mutable_settings->beginGroup (current_group);
return value;
}
void MultiSettings::set_common_value (QString const& key, QVariant const& value)
{
auto const& current_group = m_->settings_.group ();
if (current_group.size ()) m_->settings_.endGroup ();
{
SettingsGroup alternatives {&m_->settings_, multi_settings_root_group};
m_->settings_.setValue (key, value);
}
if (current_group.size ()) m_->settings_.beginGroup (current_group);
}
void MultiSettings::remove_common_value (QString const& key)
{
if (!key.size ()) return; // we don't allow global delete as it
// would break this classes data model
auto const& current_group = m_->settings_.group ();
if (current_group.size ()) m_->settings_.endGroup ();
{
SettingsGroup alternatives {&m_->settings_, multi_settings_root_group};
m_->settings_.remove (key);
}
if (current_group.size ()) m_->settings_.beginGroup (current_group);
}
void MultiSettings::create_menu_actions (QMainWindow * main_window, QMenu * menu)
{
m_->create_menu_actions (main_window, menu);
}
void MultiSettings::select_configuration (QString const& name)
{
m_->select_configuration (name);
}
QString MultiSettings::configuration_name () const
{
return m_->current_;
}
bool MultiSettings::exit ()
{
return m_->exit ();
}
MultiSettings::impl::impl (MultiSettings const * parent, QString const& config_name)
: settings_ {settings_path (), QSettings::IniFormat}
, parent_ {parent}
, main_window_ {nullptr}
, name_change_emit_pending_ {true}
, reposition_type_ {RepositionType::unchanged}
, exit_flag_ {true}
, configurations_group_ {new QActionGroup {this}}
{
if (!settings_.isWritable ())
{
throw std::runtime_error {QString {"Cannot access \"%1\" for writing"}
.arg (settings_.fileName ()).toStdString ()};
}
// deal with transient, now defunct, settings key
if (settings_.contains (multi_settings_current_group_key))
{
current_ = settings_.value (multi_settings_current_group_key).toString ();
settings_.remove (multi_settings_current_group_key);
if (current_.size ())
{
{
SettingsGroup alternatives {&settings_, multi_settings_root_group};
{
SettingsGroup source_group {&settings_, current_};
new_settings_ = get_settings ();
}
settings_.setValue (multi_settings_current_name_key, tr (default_string));
}
reposition_type_ = RepositionType::save_and_replace;
reposition ();
}
else
{
SettingsGroup alternatives {&settings_, multi_settings_root_group};
settings_.setValue (multi_settings_current_name_key, tr (default_string));
}
}
// bootstrap
QStringList available_configurations;
{
SettingsGroup alternatives {&settings_, multi_settings_root_group};
available_configurations = settings_.childGroups ();
// use last selected configuration
current_ = settings_.value (multi_settings_current_name_key).toString ();
if (!current_.size ())
{
// no configurations so use default name
current_ = tr (default_string);
settings_.setValue (multi_settings_current_name_key, current_);
}
}
if (config_name.size () && available_configurations.contains (config_name) && config_name != current_)
{
// switch to specified configuration
{
SettingsGroup alternatives {&settings_, multi_settings_root_group};
// save the target settings
SettingsGroup target_group {&settings_, config_name};
new_settings_ = get_settings ();
}
current_ = config_name;
reposition_type_ = RepositionType::save_and_replace;
reposition ();
}
settings_.sync ();
}
// do actions that can only be done once all the windows are closed
bool MultiSettings::impl::reposition ()
{
auto const& current_group = settings_.group ();
if (current_group.size ()) settings_.endGroup ();
switch (reposition_type_)
{
case RepositionType::save_and_replace:
{
// save the current settings with the other alternatives
Dictionary saved_settings {get_settings ()};
SettingsGroup alternatives {&settings_, multi_settings_root_group};
// get the current configuration name
auto const& previous_group_name = settings_.value (multi_settings_current_name_key, tr (default_string)).toString ();
SettingsGroup save_group {&settings_, previous_group_name};
load_from (saved_settings);
}
// fall through
case RepositionType::replace:
// and purge current settings
for (auto const& key: settings_.allKeys ())
{
if (!key.contains (multi_settings_root_group))
{
settings_.remove (key);
}
}
// insert the new settings
load_from (new_settings_, false);
if (!new_settings_.size ())
{
// if we are clearing the current settings then we must
// reset the application font and the font in the
// application style sheet, this is necessary since the
// application instance is not recreated
qApp->setFont (original_font_);
qApp->setStyleSheet (qApp->styleSheet () + "* {" + font_as_stylesheet (original_font_) + '}');
}
// now we have set up the new current we can safely purge it
// from the alternatives
{
SettingsGroup alternatives {&settings_, multi_settings_root_group};
{
SettingsGroup purge_group {&settings_, current_};
settings_.remove (QString {}); // purge entire group
}
// switch to the specified configuration name
settings_.setValue (multi_settings_current_name_key, current_);
}
settings_.sync ();
// fall through
case RepositionType::unchanged:
new_settings_.clear ();
break;
}
if (current_group.size ()) settings_.beginGroup (current_group);
reposition_type_ = RepositionType::unchanged; // reset
bool exit {exit_flag_};
exit_flag_ = true; // reset exit flag so normal exit works
return exit;
}
// populate a pop up menu with the configurations sub-menus for
// maintenance including select, clone, clone from, delete, rename
// and, reset
void MultiSettings::impl::create_menu_actions (QMainWindow * main_window, QMenu * menu)
{
main_window_ = main_window;
auto const& current_group = settings_.group ();
if (current_group.size ()) settings_.endGroup ();
SettingsGroup alternatives {&settings_, multi_settings_root_group};
// get the current configuration name
auto const& current_configuration_name = settings_.value (multi_settings_current_name_key, tr (default_string)).toString ();
// add the default configuration sub menu
QMenu * default_menu = create_sub_menu (menu, current_configuration_name, configurations_group_);
// and set as the current configuration
default_menu->menuAction ()->setChecked (true);
// get the existing alternatives
auto const& available_configurations = settings_.childGroups ();
// add all the other configurations
for (auto const& configuration_name: available_configurations)
{
create_sub_menu (menu, configuration_name, configurations_group_);
}
if (current_group.size ()) settings_.beginGroup (current_group);
if (name_change_emit_pending_)
{
Q_EMIT parent_->configurationNameChanged (unescape_ampersands (current_));
name_change_emit_pending_ = false;
}
}
// call this at the end of the main program loop to determine if the
// main window really wants to quit or to run again with a new configuration
bool MultiSettings::impl::exit ()
{
// ensure that configuration name changed signal gets fired on restart
name_change_emit_pending_ = true;
// do any configuration swap required and return exit flag
return reposition ();
}
QMenu * MultiSettings::impl::create_sub_menu (QMenu * parent_menu,
QString const& menu_title,
QActionGroup * action_group)
{
auto sub_menu = parent_menu->addMenu (menu_title);
if (action_group) action_group->addAction (sub_menu->menuAction ());
sub_menu->menuAction ()->setCheckable (true);
// populate sub-menu actions before showing
connect (sub_menu, &QMenu::aboutToShow, [this, parent_menu, sub_menu] () {
// depopulate before populating and showing because on Mac OS X
// there is an issue with depopulating in QMenu::aboutToHide()
// with connections being disconnected before they are actioned
while (!sub_menu->actions ().isEmpty ())
{
sub_menu->removeAction (sub_menu->actions ().last ());
}
bool is_current {sub_menu->menuAction ()->text () == current_};
if (!is_current)
{
auto select_action = new QAction {tr ("&Switch To"), this};
sub_menu->addAction (select_action);
connect (select_action, &QAction::triggered, [this, sub_menu] (bool) {
select_configuration (sub_menu->title ());
});
sub_menu->addSeparator ();
}
auto clone_action = new QAction {tr ("&Clone"), this};
sub_menu->addAction (clone_action);
connect (clone_action, &QAction::triggered, [this, parent_menu, sub_menu] (bool) {
clone_configuration (parent_menu, sub_menu);
});
auto const& current_group = settings_.group ();
if (current_group.size ()) settings_.endGroup ();
SettingsGroup alternatives {&settings_, multi_settings_root_group};
if (settings_.childGroups ().size ())
{
auto clone_into_action = new QAction {tr ("Clone &Into ..."), this};
sub_menu->addAction (clone_into_action);
connect (clone_into_action, &QAction::triggered, [this, sub_menu] (bool) {
clone_into_configuration (sub_menu);
});
}
if (current_group.size ()) settings_.beginGroup (current_group);
auto reset_action = new QAction {tr ("R&eset"), this};
sub_menu->addAction (reset_action);
connect (reset_action, &QAction::triggered, [this, sub_menu] (bool) {
reset_configuration (sub_menu);
});
auto rename_action = new QAction {tr ("&Rename ..."), this};
sub_menu->addAction (rename_action);
connect (rename_action, &QAction::triggered, [this, sub_menu] (bool) {
rename_configuration (sub_menu);
});
if (!is_current)
{
auto delete_action = new QAction {tr ("&Delete"), this};
sub_menu->addAction (delete_action);
connect (delete_action, &QAction::triggered, [this, sub_menu] (bool) {
delete_configuration (sub_menu);
});
}
});
return sub_menu;
}
auto MultiSettings::impl::get_settings () const -> Dictionary
{
Dictionary settings;
for (auto const& key: settings_.allKeys ())
{
// filter out multi settings group and empty settings
// placeholder
if (!key.contains (multi_settings_root_group)
&& !key.contains (multi_settings_place_holder_key))
{
settings[key] = settings_.value (key);
}
}
return settings;
}
void MultiSettings::impl::load_from (Dictionary const& dictionary, bool add_placeholder)
{
if (dictionary.size ())
{
for (Dictionary::const_iterator iter = dictionary.constBegin ();
iter != dictionary.constEnd (); ++iter)
{
settings_.setValue (iter.key (), iter.value ());
}
}
else if (add_placeholder)
{
// add a placeholder key to stop the alternative configuration
// name from disappearing
settings_.setValue (multi_settings_place_holder_key, QVariant {});
}
settings_.sync ();
}
void MultiSettings::impl::select_configuration (QString const& target_name)
{
if (main_window_ && target_name != current_)
{
bool changed {false};
{
auto const& current_group = settings_.group ();
if (current_group.size ()) settings_.endGroup ();
// position to the alternative settings
SettingsGroup alternatives {&settings_, multi_settings_root_group};
if (settings_.childGroups ().contains (target_name))
{
changed = true;
// save the target settings
SettingsGroup target_group {&settings_, target_name};
new_settings_ = get_settings ();
}
if (current_group.size ()) settings_.beginGroup (current_group);
}
if (changed)
{
// and set up the restart
current_ = target_name;
Q_EMIT parent_->configurationNameChanged (unescape_ampersands (current_));
restart (RepositionType::save_and_replace);
}
}
}
void MultiSettings::impl::clone_configuration (QMenu * parent_menu, QMenu const * menu)
{
auto const& current_group = settings_.group ();
if (current_group.size ()) settings_.endGroup ();
auto const& source_name = menu->title ();
// settings to clone
Dictionary source_settings;
if (source_name == current_)
{
// grab the data to clone from the current settings
source_settings = get_settings ();
}
SettingsGroup alternatives {&settings_, multi_settings_root_group};
if (source_name != current_)
{
SettingsGroup source_group {&settings_, source_name};
source_settings = get_settings ();
}
// find a new unique name
QString new_name_root {source_name + " - Copy"};;
QString new_name {new_name_root};
unsigned index {0};
do
{
if (index++) new_name = new_name_root + '(' + QString::number (index) + ')';
}
while (settings_.childGroups ().contains (new_name) || new_name == current_);
SettingsGroup new_group {&settings_, new_name};
load_from (source_settings);
// insert the new configuration sub menu in the parent menu
create_sub_menu (parent_menu, new_name, configurations_group_);
if (current_group.size ()) settings_.beginGroup (current_group);
}
void MultiSettings::impl::clone_into_configuration (QMenu const * menu)
{
Q_ASSERT (main_window_);
if (!main_window_) return;
auto const& current_group = settings_.group ();
if (current_group.size ()) settings_.endGroup ();
auto const& target_name = menu->title ();
// get the current configuration name
QString current_group_name;
QStringList sources;
{
SettingsGroup alternatives {&settings_, multi_settings_root_group};
current_group_name = settings_.value (multi_settings_current_name_key).toString ();
{
// get the source configuration name for the clone
sources = settings_.childGroups ();
sources << current_group_name;
sources.removeOne (target_name);
}
}
// pick a source configuration
ExistingNameDialog dialog {sources, main_window_};
if (sources.size () && (1 == sources.size () || QDialog::Accepted == dialog.exec ()))
{
QString source_name {1 == sources.size () ? sources.at (0) : dialog.name ()};
if (main_window_
&& MessageBox::Yes == MessageBox::query_message (main_window_,
tr ("Clone Into Configuration"),
tr ("Confirm overwrite of all values for configuration \"%1\" with values from \"%2\"?")
.arg (unescape_ampersands (target_name))
.arg (unescape_ampersands (source_name))))
{
// grab the data to clone from
if (source_name == current_group_name)
{
// grab the data to clone from the current settings
new_settings_ = get_settings ();
}
else
{
SettingsGroup alternatives {&settings_, multi_settings_root_group};
SettingsGroup source_group {&settings_, source_name};
new_settings_ = get_settings ();
}
// purge target settings and replace
if (target_name == current_)
{
// restart with new settings
restart (RepositionType::replace);
}
else
{
SettingsGroup alternatives {&settings_, multi_settings_root_group};
SettingsGroup target_group {&settings_, target_name};
settings_.remove (QString {}); // purge entire group
load_from (new_settings_);
new_settings_.clear ();
}
}
}
if (current_group.size ()) settings_.beginGroup (current_group);
}
void MultiSettings::impl::reset_configuration (QMenu const * menu)
{
Q_ASSERT (main_window_);
if (!main_window_) return;
auto const& target_name = menu->title ();
if (!main_window_
|| MessageBox::Yes != MessageBox::query_message (main_window_,
tr ("Reset Configuration"),
tr ("Confirm reset to default values for configuration \"%1\"?")
.arg (unescape_ampersands (target_name))))
{
return;
}
if (target_name == current_)
{
// restart with default settings
new_settings_.clear ();
restart (RepositionType::replace);
}
else
{
auto const& current_group = settings_.group ();
if (current_group.size ()) settings_.endGroup ();
SettingsGroup alternatives {&settings_, multi_settings_root_group};
SettingsGroup target_group {&settings_, target_name};
settings_.remove (QString {}); // purge entire group
// add a placeholder to stop alternative configuration name
// being lost
settings_.setValue (multi_settings_place_holder_key, QVariant {});
settings_.sync ();
if (current_group.size ()) settings_.beginGroup (current_group);
}
}
void MultiSettings::impl::rename_configuration (QMenu * menu)
{
Q_ASSERT (main_window_);
if (!main_window_) return;
auto const& current_group = settings_.group ();
if (current_group.size ()) settings_.endGroup ();
auto const& target_name = menu->title ();
// gather names we cannot use
SettingsGroup alternatives {&settings_, multi_settings_root_group};
auto invalid_names = settings_.childGroups ();
invalid_names << settings_.value (multi_settings_current_name_key).toString ();
// get the new name
NameDialog dialog {target_name, invalid_names, main_window_};
if (QDialog::Accepted == dialog.exec ())
{
if (target_name == current_)
{
settings_.setValue (multi_settings_current_name_key, dialog.new_name ());
settings_.sync ();
current_ = dialog.new_name ();
Q_EMIT parent_->configurationNameChanged (unescape_ampersands (current_));
}
else
{
// switch to the target group and fetch the configuration data
Dictionary target_settings;
{
// grab the target configuration settings
SettingsGroup target_group {&settings_, target_name};
target_settings = get_settings ();
// purge the old configuration data
settings_.remove (QString {}); // purge entire group
}
// load into new configuration group name
SettingsGroup target_group {&settings_, dialog.new_name ()};
load_from (target_settings);
}
// change the action text in the menu
menu->setTitle (dialog.new_name ());
}
if (current_group.size ()) settings_.beginGroup (current_group);
}
void MultiSettings::impl::delete_configuration (QMenu * menu)
{
Q_ASSERT (main_window_);
auto const& target_name = menu->title ();
if (target_name == current_)
{
return; // suicide not allowed here
}
else
{
if (!main_window_
|| MessageBox::Yes != MessageBox::query_message (main_window_,
tr ("Delete Configuration"),
tr ("Confirm deletion of configuration \"%1\"?")
.arg (unescape_ampersands (target_name))))
{
return;
}
auto const& current_group = settings_.group ();
if (current_group.size ()) settings_.endGroup ();
SettingsGroup alternatives {&settings_, multi_settings_root_group};
SettingsGroup target_group {&settings_, target_name};
// purge the configuration data
settings_.remove (QString {}); // purge entire group
settings_.sync ();
if (current_group.size ()) settings_.beginGroup (current_group);
}
// update the menu
menu->deleteLater ();
}
void MultiSettings::impl::restart (RepositionType type)
{
Q_ASSERT (main_window_);
reposition_type_ = type;
exit_flag_ = false;
main_window_->close ();
main_window_ = nullptr;
}

109
MultiSettings.hpp Normal file
View File

@ -0,0 +1,109 @@
#ifndef MULTISETTINGS_HPP__
#define MULTISETTINGS_HPP__
#include <QObject>
#include <QVariant>
#include <QString>
#include "pimpl_h.hpp"
class QSettings;
class QMainWindow;
class QMenu;
//
// MultiSettings - Manage multiple configuration names
//
// Responsibilities:
//
// MultiSettings allows a Qt application to be run with alternative
// settings as stored in a QSettings INI style file. As far as the
// application is concerned it uses the QSettings instance returned
// by the MultiSettings::settings() method as if it were the one and
// only QSettings object. The alternative settings are stored as
// QSettings groups which are children of a root level group called
// MultiSettings. The current settings are themselves stored at the
// root so the QSettings group name MultiSettings is reserved. Also
// at the root level a key called CurrentMultiSettingsConfiguration
// is reserved to store the current configuration name.
//
//
// Example Usage:
//
// #include <QApplication>
// #include "MultiSettings.hpp"
// #include "MyMainWindow.hpp"
//
// int main (int argc, char * argv[]) {
// QApplication a {argc, argv};
// MultiSettings multi_settings;
// int result;
// do {
// MyMainWindow main_window {&multi_settings};
// main_window.show ();
// result = a.exec ();
// } while (!result && !multi_settings.exit ());
// return result;
// }
//
// In the main window call MultiSettings::create_menu_actions() to
// populate an existing QMenu widget with the configuration switching
// and maintenance actions. This would normally be done in the main
// window class constructor:
//
// MyMainWindow::MyMainWindow (MultiSettings * multi_settings) {
// QSettings * settings {multi_settings->settings ()};
// // ...
// multi_settings->create_menu_actions (this, ui->configurations_menu);
// // ...
// }
//
class MultiSettings
: public QObject
{
Q_OBJECT
public:
// config_name will be selected if it is an existing configuration
// name otherwise the last used configuration will be selected or
// the default configuration if none exist
explicit MultiSettings (QString const& config_name = QString {});
MultiSettings (MultiSettings const&) = delete;
MultiSettings& operator = (MultiSettings const&) = delete;
~MultiSettings ();
// Add multiple configurations navigation and maintenance actions to
// a provided menu. The provided main window object instance will
// have its close() function called when a "Switch To" configuration
// action is triggered.
void create_menu_actions (QMainWindow *, QMenu *);
// switch to this configuration if it exists
Q_SLOT void select_configuration (QString const& name);
QString configuration_name () const;
// Access to the QSettings object instance.
QSettings * settings ();
// Access to values in a common section
QVariant common_value (QString const& key, QVariant const& default_value = QVariant {}) const;
void set_common_value (QString const& key, QVariant const& value);
void remove_common_value (QString const& key);
// Call this to determine if the application is terminating, if it
// returns false then the application main window should be
// recreated, shown and the application exec() function called
// again.
bool exit ();
// emitted when the name of the current configuration changes
Q_SIGNAL void configurationNameChanged (QString name) const;
private:
class impl;
pimpl<impl> m_;
};
#endif

3264
NEWS Normal file

File diff suppressed because it is too large Load Diff

299
Network/LotWUsers.cpp Normal file
View File

@ -0,0 +1,299 @@
#include "LotWUsers.hpp"
#include <future>
#include <chrono>
#include <QHash>
#include <QString>
#include <QDate>
#include <QFile>
#include <QTextStream>
#include <QDir>
#include <QFileInfo>
#include <QPointer>
#include <QSaveFile>
#include <QUrl>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QDebug>
#include "pimpl_impl.hpp"
#include "moc_LotWUsers.cpp"
namespace
{
// Dictionary mapping call sign to date of last upload to LotW
using dictionary = QHash<QString, QDate>;
}
class LotWUsers::impl final
: public QObject
{
Q_OBJECT
public:
impl (LotWUsers * self, QNetworkAccessManager * network_manager)
: self_ {self}
, network_manager_ {network_manager}
, url_valid_ {false}
, redirect_count_ {0}
, age_constraint_ {365}
{
}
void load (QString const& url, bool fetch, bool forced_fetch)
{
abort (); // abort any active download
auto csv_file_name = csv_file_.fileName ();
auto exists = QFileInfo::exists (csv_file_name);
if (fetch && (!exists || forced_fetch))
{
current_url_.setUrl (url);
if (current_url_.isValid () && !QSslSocket::supportsSsl ())
{
current_url_.setScheme ("http");
}
redirect_count_ = 0;
download (current_url_);
}
else
{
if (exists)
{
// load the database asynchronously
future_load_ = std::async (std::launch::async, &LotWUsers::impl::load_dictionary, this, csv_file_name);
}
}
}
void download (QUrl url)
{
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
if (QNetworkAccessManager::Accessible != network_manager_->networkAccessible ())
{
// try and recover network access for QNAM
network_manager_->setNetworkAccessible (QNetworkAccessManager::Accessible);
}
#endif
QNetworkRequest request {url};
request.setRawHeader ("User-Agent", "WSJT LotW User Downloader");
request.setOriginatingObject (this);
// this blocks for a second or two the first time it is used on
// Windows - annoying
if (!url_valid_)
{
reply_ = network_manager_->head (request);
}
else
{
reply_ = network_manager_->get (request);
}
connect (reply_.data (), &QNetworkReply::finished, this, &LotWUsers::impl::reply_finished);
connect (reply_.data (), &QNetworkReply::readyRead, this, &LotWUsers::impl::store);
}
void reply_finished ()
{
if (!reply_)
{
Q_EMIT self_->load_finished ();
return; // we probably deleted it in an earlier call
}
QUrl redirect_url {reply_->attribute (QNetworkRequest::RedirectionTargetAttribute).toUrl ()};
if (reply_->error () == QNetworkReply::NoError && !redirect_url.isEmpty ())
{
if ("https" == redirect_url.scheme () && !QSslSocket::supportsSsl ())
{
Q_EMIT self_->LotW_users_error (tr ("Network Error - SSL/TLS support not installed, cannot fetch:\n\'%1\'")
.arg (redirect_url.toDisplayString ()));
url_valid_ = false; // reset
Q_EMIT self_->load_finished ();
}
else if (++redirect_count_ < 10) // maintain sanity
{
// follow redirect
download (reply_->url ().resolved (redirect_url));
}
else
{
Q_EMIT self_->LotW_users_error (tr ("Network Error - Too many redirects:\n\'%1\'")
.arg (redirect_url.toDisplayString ()));
url_valid_ = false; // reset
Q_EMIT self_->load_finished ();
}
}
else if (reply_->error () != QNetworkReply::NoError)
{
csv_file_.cancelWriting ();
csv_file_.commit ();
url_valid_ = false; // reset
// report errors that are not due to abort
if (QNetworkReply::OperationCanceledError != reply_->error ())
{
Q_EMIT self_->LotW_users_error (tr ("Network Error:\n%1")
.arg (reply_->errorString ()));
}
Q_EMIT self_->load_finished ();
}
else
{
if (url_valid_ && !csv_file_.commit ())
{
Q_EMIT self_->LotW_users_error (tr ("File System Error - Cannot commit changes to:\n\"%1\"")
.arg (csv_file_.fileName ()));
url_valid_ = false; // reset
Q_EMIT self_->load_finished ();
}
else
{
if (!url_valid_)
{
// now get the body content
url_valid_ = true;
download (reply_->url ().resolved (redirect_url));
}
else
{
url_valid_ = false; // reset
// load the database asynchronously
future_load_ = std::async (std::launch::async, &LotWUsers::impl::load_dictionary, this, csv_file_.fileName ());
}
}
}
if (reply_ && reply_->isFinished ())
{
reply_->deleteLater ();
}
}
void store ()
{
if (url_valid_)
{
if (!csv_file_.isOpen ())
{
// create temporary file in the final location
if (!csv_file_.open (QSaveFile::WriteOnly))
{
abort ();
Q_EMIT self_->LotW_users_error (tr ("File System Error - Cannot open file:\n\"%1\"\nError(%2): %3")
.arg (csv_file_.fileName ())
.arg (csv_file_.error ())
.arg (csv_file_.errorString ()));
}
}
if (csv_file_.write (reply_->read (reply_->bytesAvailable ())) < 0)
{
abort ();
Q_EMIT self_->LotW_users_error (tr ("File System Error - Cannot write to file:\n\"%1\"\nError(%2): %3")
.arg (csv_file_.fileName ())
.arg (csv_file_.error ())
.arg (csv_file_.errorString ()));
}
}
}
void abort ()
{
if (reply_ && reply_->isRunning ())
{
reply_->abort ();
}
}
// Load the database from the given file name
//
// Expects the file to be in CSV format with no header with one
// record per line. Record fields are call sign followed by upload
// date in yyyy-MM-dd format followed by upload time (ignored)
dictionary load_dictionary (QString const& lotw_csv_file)
{
dictionary result;
QFile f {lotw_csv_file};
if (f.open (QFile::ReadOnly | QFile::Text))
{
QTextStream s {&f};
for (auto l = s.readLine (); !l.isNull (); l = s.readLine ())
{
auto pos = l.indexOf (',');
result[l.left (pos)] = QDate::fromString (l.mid (pos + 1, l.indexOf (',', pos + 1) - pos - 1), "yyyy-MM-dd");
}
// qDebug () << "LotW User Data Loaded";
}
else
{
throw std::runtime_error {QObject::tr ("Failed to open LotW users CSV file: '%1'").arg (f.fileName ()).toStdString ()};
}
return result;
}
LotWUsers * self_;
QNetworkAccessManager * network_manager_;
QSaveFile csv_file_;
bool url_valid_;
QUrl current_url_; // may be a redirect
int redirect_count_;
QPointer<QNetworkReply> reply_;
std::future<dictionary> future_load_;
dictionary last_uploaded_;
qint64 age_constraint_; // days
};
#include "LotWUsers.moc"
LotWUsers::LotWUsers (QNetworkAccessManager * network_manager, QObject * parent)
: QObject {parent}
, m_ {this, network_manager}
{
}
LotWUsers::~LotWUsers ()
{
}
void LotWUsers::set_local_file_path (QString const& path)
{
m_->csv_file_.setFileName (path);
}
void LotWUsers::load (QString const& url, bool fetch, bool force_download)
{
m_->load (url, fetch, force_download);
}
void LotWUsers::set_age_constraint (qint64 uploaded_since_days)
{
m_->age_constraint_ = uploaded_since_days;
}
bool LotWUsers::user (QString const& call) const
{
// check if a pending asynchronous load is ready
if (m_->future_load_.valid ()
&& std::future_status::ready == m_->future_load_.wait_for (std::chrono::seconds {0}))
{
try
{
// wait for the load to finish if necessary
const_cast<dictionary&> (m_->last_uploaded_) = const_cast<std::future<dictionary>&> (m_->future_load_).get ();
}
catch (std::exception const& e)
{
Q_EMIT LotW_users_error (e.what ());
}
Q_EMIT load_finished ();
}
if (m_->last_uploaded_.size ())
{
auto p = m_->last_uploaded_.constFind (call);
if (p != m_->last_uploaded_.end ())
{
return p.value ().daysTo (QDate::currentDate ()) <= m_->age_constraint_;
}
}
return false;
}

41
Network/LotWUsers.hpp Normal file
View File

@ -0,0 +1,41 @@
#ifndef LOTW_USERS_HPP_
#define LOTW_USERS_HPP_
#include <boost/core/noncopyable.hpp>
#include <QObject>
#include "pimpl_h.hpp"
class QString;
class QDate;
class QNetworkAccessManager;
//
// LotWUsers - Lookup Logbook of the World users
//
class LotWUsers final
: public QObject
{
Q_OBJECT
public:
explicit LotWUsers (QNetworkAccessManager *, QObject * parent = 0);
~LotWUsers ();
void set_local_file_path (QString const&);
Q_SLOT void load (QString const& url, bool fetch = true, bool force_download = false);
Q_SLOT void set_age_constraint (qint64 uploaded_since_days);
// returns true if the specified call sign 'call' has uploaded their
// log to LotW in the last 'age_constraint_days' days
bool user (QString const& call) const;
Q_SIGNAL void LotW_users_error (QString const& reason) const;
Q_SIGNAL void load_finished () const;
private:
class impl;
pimpl<impl> m_;
};
#endif

662
Network/MessageClient.cpp Normal file
View File

@ -0,0 +1,662 @@
#include "MessageClient.hpp"
#include <stdexcept>
#include <vector>
#include <algorithm>
#include <limits>
#include <QUdpSocket>
#include <QNetworkInterface>
#include <QHostInfo>
#include <QTimer>
#include <QQueue>
#include <QByteArray>
#include <QColor>
#include <QDebug>
#include "NetworkMessage.hpp"
#include "qt_helpers.hpp"
#include "pimpl_impl.hpp"
#include "moc_MessageClient.cpp"
// some trace macros
#if WSJT_TRACE_UDP
#define TRACE_UDP(MSG) qDebug () << QString {"MessageClient::%1:"}.arg (__func__) << MSG
#else
#define TRACE_UDP(MSG)
#endif
class MessageClient::impl
: public QUdpSocket
{
Q_OBJECT;
public:
impl (QString const& id, QString const& version, QString const& revision,
port_type server_port, int TTL, MessageClient * self)
: self_ {self}
, enabled_ {false}
, id_ {id}
, version_ {version}
, revision_ {revision}
, dns_lookup_id_ {-1}
, server_port_ {server_port}
, TTL_ {TTL}
, schema_ {2} // use 2 prior to negotiation not 1 which is broken
, heartbeat_timer_ {new QTimer {this}}
{
connect (heartbeat_timer_, &QTimer::timeout, this, &impl::heartbeat);
connect (this, &QIODevice::readyRead, this, &impl::pending_datagrams);
heartbeat_timer_->start (NetworkMessage::pulse * 1000);
}
~impl ()
{
closedown ();
if (dns_lookup_id_ != -1)
{
QHostInfo::abortHostLookup (dns_lookup_id_);
}
}
enum StreamStatus {Fail, Short, OK};
void set_server (QString const& server_name, QStringList const& network_interface_names);
Q_SLOT void host_info_results (QHostInfo);
void start ();
void parse_message (QByteArray const&);
void pending_datagrams ();
void heartbeat ();
void closedown ();
StreamStatus check_status (QDataStream const&) const;
void send_message (QByteArray const&, bool queue_if_pending = true, bool allow_duplicates = false);
void send_message (QDataStream const& out, QByteArray const& message, bool queue_if_pending = true, bool allow_duplicates = false)
{
if (OK == check_status (out))
{
send_message (message, queue_if_pending, allow_duplicates);
}
else
{
Q_EMIT self_->error ("Error creating UDP message");
}
}
MessageClient * self_;
bool enabled_;
QString id_;
QString version_;
QString revision_;
int dns_lookup_id_;
QHostAddress server_;
port_type server_port_;
int TTL_;
std::vector<QNetworkInterface> network_interfaces_;
quint32 schema_;
QTimer * heartbeat_timer_;
std::vector<QHostAddress> blocked_addresses_;
// hold messages sent before host lookup completes asynchronously
QQueue<QByteArray> pending_messages_;
QByteArray last_message_;
};
#include "MessageClient.moc"
void MessageClient::impl::set_server (QString const& server_name, QStringList const& network_interface_names)
{
// qDebug () << "MessageClient server:" << server_name << "port:" << server_port_ << "interfaces:" << network_interface_names;
server_.setAddress (server_name);
network_interfaces_.clear ();
for (auto const& net_if_name : network_interface_names)
{
network_interfaces_.push_back (QNetworkInterface::interfaceFromName (net_if_name));
}
if (server_.isNull () && server_name.size ()) // DNS lookup required
{
// queue a host address lookup
#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
dns_lookup_id_ = QHostInfo::lookupHost (server_name, this, &MessageClient::impl::host_info_results);
#else
dns_lookup_id_ = QHostInfo::lookupHost (server_name, this, SLOT (host_info_results (QHostInfo)));
#endif
}
else
{
start ();
}
}
void MessageClient::impl::host_info_results (QHostInfo host_info)
{
if (host_info.lookupId () != dns_lookup_id_) return;
dns_lookup_id_ = -1;
if (QHostInfo::NoError != host_info.error ())
{
Q_EMIT self_->error ("UDP server DNS lookup failed: " + host_info.errorString ());
}
else
{
auto const& server_addresses = host_info.addresses ();
if (server_addresses.size ())
{
server_ = server_addresses[0];
}
}
start ();
}
void MessageClient::impl::start ()
{
if (server_.isNull ())
{
Q_EMIT self_->close ();
pending_messages_.clear (); // discard
return;
}
if (is_broadcast_address (server_))
{
Q_EMIT self_->error ("IPv4 broadcast not supported, please specify the loop-back address, a server host address, or multicast group address");
pending_messages_.clear (); // discard
return;
}
if (blocked_addresses_.end () != std::find (blocked_addresses_.begin (), blocked_addresses_.end (), server_))
{
Q_EMIT self_->error ("UDP server blocked, please try another");
pending_messages_.clear (); // discard
return;
}
TRACE_UDP ("Trying server:" << server_.toString ());
QHostAddress interface_addr {IPv6Protocol == server_.protocol () ? QHostAddress::AnyIPv6 : QHostAddress::AnyIPv4};
if (localAddress () != interface_addr)
{
if (UnconnectedState != state () || state ())
{
close ();
}
// bind to an ephemeral port on the selected interface and set
// up for sending datagrams
bind (interface_addr);
// qDebug () << "Bound to UDP port:" << localPort () << "on:" << localAddress ();
// set multicast TTL to limit scope when sending to multicast
// group addresses
setSocketOption (MulticastTtlOption, TTL_);
}
// send initial heartbeat which allows schema negotiation
heartbeat ();
// clear any backlog
while (pending_messages_.size ())
{
send_message (pending_messages_.dequeue (), true, false);
}
}
void MessageClient::impl::pending_datagrams ()
{
while (hasPendingDatagrams ())
{
QByteArray datagram;
datagram.resize (pendingDatagramSize ());
QHostAddress sender_address;
port_type sender_port;
if (0 <= readDatagram (datagram.data (), datagram.size (), &sender_address, &sender_port))
{
TRACE_UDP ("message received from:" << sender_address << "port:" << sender_port);
parse_message (datagram);
}
}
}
void MessageClient::impl::parse_message (QByteArray const& msg)
{
try
{
//
// message format is described in NetworkMessage.hpp
//
NetworkMessage::Reader in {msg};
if (OK == check_status (in))
{
if (schema_ < in.schema ()) // one time record of server's
// negotiated schema
{
schema_ = in.schema ();
}
if (!enabled_)
{
TRACE_UDP ("message processing disabled for id:" << in.id ());
return;
}
//
// message format is described in NetworkMessage.hpp
//
switch (in.type ())
{
case NetworkMessage::Reply:
{
// unpack message
QTime time;
qint32 snr;
float delta_time;
quint32 delta_frequency;
QByteArray mode;
QByteArray message;
bool low_confidence {false};
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"
#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
, QString::fromUtf8 (mode), QString::fromUtf8 (message)
, low_confidence, modifiers);
}
}
break;
case NetworkMessage::Clear:
{
quint8 window {0};
in >> window;
TRACE_UDP ("Clear window:" << window);
if (check_status (in) != Fail)
{
Q_EMIT self_->clear_decodes (window);
}
}
break;
case NetworkMessage::Close:
TRACE_UDP ("Close");
if (check_status (in) != Fail)
{
last_message_.clear ();
Q_EMIT self_->close ();
}
break;
case NetworkMessage::Replay:
TRACE_UDP ("Replay");
if (check_status (in) != Fail)
{
last_message_.clear ();
Q_EMIT self_->replay ();
}
break;
case NetworkMessage::HaltTx:
{
bool auto_only {false};
in >> auto_only;
TRACE_UDP ("Halt Tx auto_only:" << auto_only);
if (check_status (in) != Fail)
{
Q_EMIT self_->halt_tx (auto_only);
}
}
break;
case NetworkMessage::FreeText:
{
QByteArray message;
bool send {true};
in >> message >> send;
TRACE_UDP ("FreeText message:" << message << "send:" << send);
if (check_status (in) != Fail)
{
Q_EMIT self_->free_text (QString::fromUtf8 (message), send);
}
}
break;
case NetworkMessage::Location:
{
QByteArray location;
in >> location;
TRACE_UDP ("Location location:" << location);
if (check_status (in) != Fail)
{
Q_EMIT self_->location (QString::fromUtf8 (location));
}
}
break;
case NetworkMessage::HighlightCallsign:
{
QByteArray call;
QColor bg; // default invalid color
QColor fg; // default invalid color
bool last_only {false};
in >> call >> bg >> fg >> last_only;
TRACE_UDP ("HighlightCallsign call:" << call << "bg:" << bg << "fg:" << fg << "last only:" << last_only);
if (check_status (in) != Fail && call.size ())
{
Q_EMIT self_->highlight_callsign (QString::fromUtf8 (call), bg, fg, last_only);
}
}
break;
case NetworkMessage::SwitchConfiguration:
{
QByteArray configuration_name;
in >> configuration_name;
TRACE_UDP ("Switch Configuration name:" << configuration_name);
if (check_status (in) != Fail)
{
Q_EMIT self_->switch_configuration (QString::fromUtf8 (configuration_name));
}
}
break;
case NetworkMessage::Configure:
{
QByteArray mode;
quint32 frequency_tolerance;
QByteArray submode;
bool fast_mode {false};
quint32 tr_period {std::numeric_limits<quint32>::max ()};
quint32 rx_df {std::numeric_limits<quint32>::max ()};
QByteArray dx_call;
QByteArray dx_grid;
bool generate_messages {false};
in >> mode >> frequency_tolerance >> submode >> fast_mode >> tr_period >> rx_df
>> dx_call >> dx_grid >> generate_messages;
TRACE_UDP ("Configure mode:" << mode << "frequency tolerance:" << frequency_tolerance << "submode:" << submode << "fast mode:" << fast_mode << "T/R period:" << tr_period << "rx df:" << rx_df << "dx call:" << dx_call << "dx grid:" << dx_grid << "generate messages:" << generate_messages);
if (check_status (in) != Fail)
{
Q_EMIT self_->configure (QString::fromUtf8 (mode), frequency_tolerance
, QString::fromUtf8 (submode), fast_mode, tr_period, rx_df
, QString::fromUtf8 (dx_call), QString::fromUtf8 (dx_grid)
, generate_messages);
}
}
break;
default:
// Ignore
//
// Note that although server heartbeat messages are not
// parsed here they are still partially parsed in the
// message reader class to negotiate the maximum schema
// number being used on the network.
if (NetworkMessage::Heartbeat != in.type ())
{
TRACE_UDP ("ignoring message type:" << in.type ());
}
break;
}
}
else
{
TRACE_UDP ("ignored message for id:" << in.id ());
}
}
catch (std::exception const& e)
{
Q_EMIT self_->error (QString {"MessageClient exception: %1"}.arg (e.what ()));
}
catch (...)
{
Q_EMIT self_->error ("Unexpected exception in MessageClient");
}
}
void MessageClient::impl::heartbeat ()
{
if (server_port_ && !server_.isNull ())
{
QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::Heartbeat, id_, schema_};
out << NetworkMessage::Builder::schema_number // maximum schema number accepted
<< version_.toUtf8 () << revision_.toUtf8 ();
TRACE_UDP ("schema:" << schema_ << "max schema:" << NetworkMessage::Builder::schema_number << "version:" << version_ << "revision:" << revision_);
send_message (out, message, false, true);
}
}
void MessageClient::impl::closedown ()
{
if (server_port_ && !server_.isNull ())
{
QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::Close, id_, schema_};
TRACE_UDP ("");
send_message (out, message, false);
}
}
void MessageClient::impl::send_message (QByteArray const& message, bool queue_if_pending, bool allow_duplicates)
{
if (server_port_)
{
if (!server_.isNull ())
{
if (allow_duplicates || message != last_message_) // avoid duplicates
{
if (is_multicast_address (server_))
{
// send datagram on each selected network interface
std::for_each (network_interfaces_.begin (), network_interfaces_.end ()
, [&] (QNetworkInterface const& net_if) {
setMulticastInterface (net_if);
// qDebug () << "Multicast UDP datagram sent to:" << server_ << "port:" << server_port_ << "on:" << multicastInterface ().humanReadableName ();
writeDatagram (message, server_, server_port_);
});
}
else
{
// qDebug () << "Unicast UDP datagram sent to:" << server_ << "port:" << server_port_;
writeDatagram (message, server_, server_port_);
}
last_message_ = message;
}
}
else if (queue_if_pending)
{
pending_messages_.enqueue (message);
}
}
}
auto MessageClient::impl::check_status (QDataStream const& stream) const -> StreamStatus
{
auto stat = stream.status ();
StreamStatus result {Fail};
switch (stat)
{
case QDataStream::ReadPastEnd:
result = Short;
break;
case QDataStream::ReadCorruptData:
Q_EMIT self_->error ("Message serialization error: read corrupt data");
break;
case QDataStream::WriteFailed:
Q_EMIT self_->error ("Message serialization error: write error");
break;
default:
result = OK;
break;
}
return result;
}
MessageClient::MessageClient (QString const& id, QString const& version, QString const& revision,
QString const& server_name, port_type server_port,
QStringList const& network_interface_names,
int TTL, QObject * self)
: QObject {self}
, m_ {id, version, revision, server_port, TTL, this}
{
connect (&*m_
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
, static_cast<void (impl::*) (impl::SocketError)> (&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
{
#else
{
Q_UNUSED (e);
#endif
Q_EMIT error (m_->errorString ());
}
});
m_->set_server (server_name, network_interface_names);
}
QHostAddress MessageClient::server_address () const
{
return m_->server_;
}
auto MessageClient::server_port () const -> port_type
{
return m_->server_port_;
}
void MessageClient::set_server (QString const& server_name, QStringList const& network_interface_names)
{
m_->set_server (server_name, network_interface_names);
}
void MessageClient::set_server_port (port_type server_port)
{
m_->server_port_ = server_port;
}
void MessageClient::set_TTL (int TTL)
{
m_->TTL_ = TTL;
m_->setSocketOption (QAbstractSocket::MulticastTtlOption, m_->TTL_);
}
void MessageClient::enable (bool flag)
{
m_->enabled_ = flag;
}
void MessageClient::status_update (Frequency f, QString const& mode, QString const& dx_call
, QString const& report, QString const& tx_mode
, bool tx_enabled, bool transmitting, bool decoding
, quint32 rx_df, quint32 tx_df, QString const& de_call
, QString const& de_grid, QString const& dx_grid
, bool watchdog_timeout, QString const& sub_mode
, bool fast_mode, quint8 special_op_mode
, quint32 frequency_tolerance, quint32 tr_period
, QString const& configuration_name
, QString const& tx_message)
{
if (m_->server_port_ && !m_->server_.isNull ())
{
QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::Status, m_->id_, m_->schema_};
out << f << mode.toUtf8 () << dx_call.toUtf8 () << report.toUtf8 () << tx_mode.toUtf8 ()
<< tx_enabled << transmitting << decoding << rx_df << tx_df << de_call.toUtf8 ()
<< de_grid.toUtf8 () << dx_grid.toUtf8 () << watchdog_timeout << sub_mode.toUtf8 ()
<< fast_mode << special_op_mode << frequency_tolerance << tr_period << configuration_name.toUtf8 ()
<< tx_message.toUtf8 ();
TRACE_UDP ("frequency:" << f << "mode:" << mode << "DX:" << dx_call << "report:" << report << "Tx mode:" << tx_mode << "tx_enabled:" << tx_enabled << "Tx:" << transmitting << "decoding:" << decoding << "Rx df:" << rx_df << "Tx df:" << tx_df << "DE:" << de_call << "DE grid:" << de_grid << "DX grid:" << dx_grid << "w/d t/o:" << watchdog_timeout << "sub_mode:" << sub_mode << "fast mode:" << fast_mode << "spec op mode:" << special_op_mode << "frequency tolerance:" << frequency_tolerance << "T/R period:" << tr_period << "configuration name:" << configuration_name << "Tx message:" << tx_message);
m_->send_message (out, message);
}
}
void MessageClient::decode (bool is_new, QTime time, qint32 snr, float delta_time, quint32 delta_frequency
, QString const& mode, QString const& message_text, bool low_confidence
, bool off_air)
{
if (m_->server_port_ && !m_->server_.isNull ())
{
QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::Decode, m_->id_, m_->schema_};
out << is_new << time << snr << delta_time << delta_frequency << mode.toUtf8 ()
<< message_text.toUtf8 () << low_confidence << off_air;
TRACE_UDP ("new" << is_new << "time:" << time << "snr:" << snr << "dt:" << delta_time << "df:" << delta_frequency << "mode:" << mode << "text:" << message_text << "low conf:" << low_confidence << "off air:" << off_air);
m_->send_message (out, message);
}
}
void MessageClient::WSPR_decode (bool is_new, QTime time, qint32 snr, float delta_time, Frequency frequency
, qint32 drift, QString const& callsign, QString const& grid, qint32 power
, bool off_air)
{
if (m_->server_port_ && !m_->server_.isNull ())
{
QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::WSPRDecode, m_->id_, m_->schema_};
out << is_new << time << snr << delta_time << frequency << drift << callsign.toUtf8 ()
<< grid.toUtf8 () << power << off_air;
TRACE_UDP ("new:" << is_new << "time:" << time << "snr:" << snr << "dt:" << delta_time << "frequency:" << frequency << "drift:" << drift << "call:" << callsign << "grid:" << grid << "pwr:" << power << "off air:" << off_air);
m_->send_message (out, message);
}
}
void MessageClient::decodes_cleared ()
{
if (m_->server_port_ && !m_->server_.isNull ())
{
QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::Clear, m_->id_, m_->schema_};
TRACE_UDP ("");
m_->send_message (out, message);
}
}
void MessageClient::qso_logged (QDateTime time_off, QString const& dx_call, QString const& dx_grid
, Frequency dial_frequency, QString const& mode, QString const& report_sent
, QString const& report_received, QString const& tx_power
, QString const& comments, QString const& name, 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& propmode)
{
if (m_->server_port_ && !m_->server_.isNull ())
{
QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::QSOLogged, m_->id_, m_->schema_};
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 () << 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);
}
}
void MessageClient::logged_ADIF (QByteArray const& ADIF_record)
{
if (m_->server_port_ && !m_->server_.isNull ())
{
QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::LoggedADIF, m_->id_, m_->schema_};
QByteArray ADIF {"\n<adif_ver:5>3.1.0\n<programid:6>WSJT-X\n<EOH>\n" + ADIF_record + " <EOR>"};
out << ADIF;
TRACE_UDP ("ADIF:" << ADIF);
m_->send_message (out, message);
}
}

140
Network/MessageClient.hpp Normal file
View File

@ -0,0 +1,140 @@
#ifndef MESSAGE_CLIENT_HPP__
#define MESSAGE_CLIENT_HPP__
#include <QObject>
#include <QTime>
#include <QDateTime>
#include <QString>
#include <QHostAddress>
#include "Radio.hpp"
#include "pimpl_h.hpp"
class QByteArray;
class QHostAddress;
class QColor;
//
// MessageClient - Manage messages sent and replies received from a
// matching server (MessageServer) at the other end of
// the wire
//
//
// Each outgoing message type is a Qt slot
//
class MessageClient
: public QObject
{
Q_OBJECT;
public:
using Frequency = Radio::Frequency;
using port_type = quint16;
// instantiate and initiate a host lookup on the server
//
// messages will be silently dropped until a server host lookup is complete
MessageClient (QString const& id, QString const& version, QString const& revision,
QString const& server_name, port_type server_port,
QStringList const& network_interface_names,
int TTL, QObject * parent = nullptr);
// query server details
QHostAddress server_address () const;
port_type server_port () const;
// initiate a new server host lookup or if the server name is empty
// the sending of messages is disabled, if an interface is specified
// then that interface is used for outgoing datagrams
Q_SLOT void set_server (QString const& server_name, QStringList const& network_interface_names);
// change the server port messages are sent to
Q_SLOT void set_server_port (port_type server_port = 0u);
// change the server port messages are sent to
Q_SLOT void set_TTL (int TTL);
// enable incoming messages
Q_SLOT void enable (bool);
// outgoing messages
Q_SLOT void status_update (Frequency, QString const& mode, QString const& dx_call, QString const& report
, QString const& tx_mode, bool tx_enabled, bool transmitting, bool decoding
, quint32 rx_df, quint32 tx_df, QString const& de_call, QString const& de_grid
, QString const& dx_grid, bool watchdog_timeout, QString const& sub_mode
, bool fast_mode, quint8 special_op_mode, quint32 frequency_tolerance
, quint32 tr_period, QString const& configuration_name
, QString const& tx_message);
Q_SLOT void decode (bool is_new, QTime time, qint32 snr, float delta_time, quint32 delta_frequency
, QString const& mode, QString const& message, bool low_confidence
, bool off_air);
Q_SLOT void WSPR_decode (bool is_new, QTime time, qint32 snr, float delta_time, Frequency
, qint32 drift, QString const& callsign, QString const& grid, qint32 power
, bool off_air);
Q_SLOT void decodes_cleared ();
Q_SLOT void qso_logged (QDateTime time_off, QString const& dx_call, QString const& dx_grid
, Frequency dial_frequency, QString const& mode, QString const& report_sent
, QString const& report_received, QString const& tx_power, QString const& comments
, QString const& name, 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& propmode);
// ADIF_record argument should be valid ADIF excluding any <EOR> end
// of record marker
Q_SLOT void logged_ADIF (QByteArray const& ADIF_record);
// this signal is emitted if the server has requested a decode
// window clear action
Q_SIGNAL void clear_decodes (quint8 window);
// this signal is emitted if the server sends us a reply, the only
// reply supported is reply to a prior CQ or QRZ message
Q_SIGNAL void reply (QTime, qint32 snr, float delta_time, quint32 delta_frequency, QString const& mode
, QString const& message_text, bool low_confidence, quint8 modifiers);
// this signal is emitted if the server has requested this client to
// close down gracefully
Q_SIGNAL void close ();
// this signal is emitted if the server has requested a replay of
// all decodes
Q_SIGNAL void replay ();
// this signal is emitted if the server has requested immediate (or
// auto Tx if auto_only is true) transmission to halt
Q_SIGNAL void halt_tx (bool auto_only);
// this signal is emitted if the server has requested a new free
// message text
Q_SIGNAL void free_text (QString const&, bool send);
// this signal is emitted if the server has sent a highlight
// callsign request for the specified call
Q_SIGNAL void highlight_callsign (QString const& callsign, QColor const& bg, QColor const& fg, bool last_only);
// this signal is emitted if the server has requested a
// configuration switch
Q_SIGNAL void switch_configuration (QString const& configuration_name);
// this signal is emitted if the server has requested a
// configuration change
Q_SIGNAL void configure (QString const& mode, quint32 frequency_tolerance, QString const& submode
, bool fast_mode, quint32 tr_period, quint32 rx_df, QString const& dx_call
, QString const& dx_grid, bool generate_messages);
// this signal is emitted when network errors occur or if a host
// lookup fails
Q_SIGNAL void error (QString const&) const;
// this signal is emitted if the message obtains a location from a
// server. (It doesn't have to be new, could be a periodic location
// update)
Q_SIGNAL void location (QString const&);
private:
class impl;
pimpl<impl> m_;
};
#endif

View File

@ -0,0 +1,62 @@
#include "Network/NetworkAccessManager.hpp"
#include <QString>
#include <QNetworkReply>
#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<QSslError> const& errors)
{
QString message;
QList<QSslError> 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;
}

View File

@ -0,0 +1,34 @@
#ifndef NETWORK_ACCESS_MANAGER_HPP__
#define NETWORK_ACCESS_MANAGER_HPP__
#include <QNetworkAccessManager>
#include <QList>
#include <QSslError>
#include "widgets/MessageBox.hpp"
class QNetworkRequest;
class QIODevice;
class QWidget;
// sub-class QNAM to keep a list of accepted SSL errors and allow
// them in future replies
class NetworkAccessManager
: public QNetworkAccessManager
{
Q_OBJECT
public:
explicit NetworkAccessManager (QWidget * parent);
protected:
QNetworkReply * createRequest (Operation, QNetworkRequest const&, QIODevice * = nullptr) override;
private:
void filter_SSL_errors (QNetworkReply * reply, QList<QSslError> const& errors);
QWidget * parent_widget_;
QList<QSslError> allowed_ssl_errors_;
};
#endif

136
Network/NetworkMessage.cpp Normal file
View File

@ -0,0 +1,136 @@
#include "NetworkMessage.hpp"
#include <exception>
#include <QString>
#include <QByteArray>
#include <QDebug>
#include "pimpl_impl.hpp"
namespace NetworkMessage
{
Builder::Builder (QIODevice * device, Type type, QString const& id, quint32 schema)
: QDataStream {device}
{
common_initialization (type, id, schema);
}
Builder::Builder (QByteArray * a, Type type, QString const& id, quint32 schema)
: QDataStream {a, QIODevice::WriteOnly}
{
common_initialization (type, id, schema);
}
void Builder::common_initialization (Type type, QString const& id, quint32 schema)
{
if (schema <= 1)
{
setVersion (QDataStream::Qt_5_0); // Qt schema version
}
#if QT_VERSION >= QT_VERSION_CHECK (5, 2, 0)
else if (schema <= 2)
{
setVersion (QDataStream::Qt_5_2); // Qt schema version
}
#endif
#if QT_VERSION >= QT_VERSION_CHECK (5, 4, 0)
else if (schema <= 3)
{
setVersion (QDataStream::Qt_5_4); // Qt schema version
}
#endif
else
{
throw std::runtime_error {"Unrecognized message schema"};
}
// the following two items assume that the quint32 encoding is
// unchanged over QDataStream versions
*this << magic;
*this << schema;
*this << static_cast<quint32> (type) << id.toUtf8 ();
}
class Reader::impl
{
public:
void common_initialization (Reader * parent)
{
quint32 magic;
*parent >> magic;
if (magic != Builder::magic)
{
throw std::runtime_error {"Invalid message format"};
}
*parent >> schema_;
if (schema_ > Builder::schema_number)
{
throw std::runtime_error {"Unrecognized message schema"};
}
if (schema_ <= 1)
{
parent->setVersion (QDataStream::Qt_5_0);
}
#if QT_VERSION >= QT_VERSION_CHECK (5, 2, 0)
else if (schema_ <= 2)
{
parent->setVersion (QDataStream::Qt_5_2);
}
#endif
#if QT_VERSION >= QT_VERSION_CHECK (5, 4, 0)
else if (schema_ <= 3)
{
parent->setVersion (QDataStream::Qt_5_4);
}
#endif
quint32 type;
*parent >> type >> id_;
if (type >= maximum_message_type_)
{
qDebug () << "Unrecognized message type:" << type << "from id:" << id_;
type_ = maximum_message_type_;
}
else
{
type_ = static_cast<Type> (type);
}
}
quint32 schema_;
Type type_;
QByteArray id_;
};
Reader::Reader (QIODevice * device)
: QDataStream {device}
{
m_->common_initialization (this);
}
Reader::Reader (QByteArray const& a)
: QDataStream {a}
{
m_->common_initialization (this);
}
Reader::~Reader ()
{
}
quint32 Reader::schema () const
{
return m_->schema_;
}
Type Reader::type () const
{
return static_cast<Type> (m_->type_);
}
QString Reader::id () const
{
return QString::fromUtf8 (m_->id_);
}
}

590
Network/NetworkMessage.hpp Normal file
View File

@ -0,0 +1,590 @@
#ifndef NETWORK_MESSAGE_HPP__
#define NETWORK_MESSAGE_HPP__
/*
* WSJT-X Message Formats
* ======================
*
* All messages are written or read using the QDataStream derivatives
* defined below, note that we are using the default for floating
* point precision which means all are double precision i.e. 64-bit
* IEEE format.
*
* Message is big endian format
*
* Header format:
*
* 32-bit unsigned integer magic number 0xadbccbda
* 32-bit unsigned integer schema number
*
* Payload format:
*
* As per the QDataStream format, see below for version used and
* here:
*
* http://doc.qt.io/qt-5/datastreamformat.html
*
* for the serialization details for each type, at the time of
* writing the above document is for Qt_5_0 format which is buggy
* so we use Qt_5_4 format, differences are:
*
* QDateTime:
* QDate qint64 Julian day number
* QTime quint32 Milli-seconds since midnight
* timespec quint8 0=local, 1=UTC, 2=Offset from UTC
* (seconds)
* 3=time zone
* offset qint32 only present if timespec=2
* timezone several-fields only present if timespec=3
*
* we will avoid using QDateTime fields with time zones for
* simplicity.
*
* Type utf8 is a utf-8 byte string formatted as a QByteArray for
* serialization purposes (currently a quint32 size followed by size
* bytes, no terminator is present or counted).
*
* The QDataStream format document linked above is not complete for
* the QByteArray serialization format, it is similar to the QString
* serialization format in that it differentiates between empty
* strings and null strings. Empty strings have a length of zero
* whereas null strings have a length field of 0xffffffff.
*
*
* Schema Negotiation
* ------------------
*
* The NetworkMessage::Builder class specifies a schema number which
* may be incremented from time to time. It represents a version of
* the underlying encoding schemes used to store data items. Since the
* underlying encoding is defined by the Qt project in it's
* QDataStream stream operators, it is essential that clients and
* servers of this protocol can agree on a common scheme. The
* NetworkMessage utility classes below exchange the schema number
* actually used. The handling of the schema is backwards compatible
* to an extent, so long as clients and servers are written
* correctly. For example a server written to any particular schema
* version can communicate with a client written to a later schema.
*
* Schema Version 1:- this schema used the QDataStream::Qt_5_0 version
* which is broken.
*
* Schema Version 2:- this schema uses the QDataStream::Qt_5_2 version.
*
* Schema Version 3:- this schema uses the QDataStream::Qt_5_4 version.
*
*
* Backward Compatibility
* ----------------------
*
* It is important that applications developed at different times
* remain compatible with this protocol and with older or newer
* versions of WSJT-X. This is achieved by both third-party
* applications and WSJT-X honouring two basic rules.
*
* 1. New message types may be added to the protocol in the future,
* third-party applications and WSJT-X shall ignore silently any
* message types they do not recognize.
*
* 2. New fields may be added to existing message types, they will
* always be added to the end of the existing fields and the number
* and type of existing fields shall not change. If a field type
* must be changed; a new field will be added and the existing
* field will remain. The originator of such a message shall
* populate both the new and old field with reasonable
* values. Third-party applications and WSJT-X shall ignore
* silently any extra data received in datagrams after the fields
* they know about.
*
* Note that these rules are unrelated to the schema number above
* whose purpose is to distinguish between non-compatible encodings of
* field data types. New message types and extra fields in existing
* messages can and will be added without any change in schema number.
*
*
* Message Types
* -------------
*
* Message Direction Value Type
* ------------- --------- ---------------------- -----------
* Heartbeat Out/In 0 quint32
* Id (unique key) utf8
* Maximum schema number quint32
* version utf8
* revision utf8
*
* The heartbeat message shall be sent on a periodic basis every
* NetworkMessage::pulse seconds (see below), the WSJT-X
* application does that using the MessageClient class. This
* message is intended to be used by servers to detect the presence
* of a client and also the unexpected disappearance of a client
* and by clients to learn the schema negotiated by the server
* after it receives the initial heartbeat message from a client.
* The message_aggregator reference server does just that using the
* MessageServer class. Upon initial startup a client must send a
* heartbeat message as soon as is practical, this message is used
* to negotiate the maximum schema number common to the client and
* server. Note that the server may not be able to support the
* client's requested maximum schema number, in which case the
* first message received from the server will specify a lower
* schema number (never a higher one as that is not allowed). If a
* server replies with a lower schema number then no higher than
* that number shall be used for all further outgoing messages from
* either clients or the server itself.
*
* Note: the "Maximum schema number" field was introduced at the
* same time as schema 3, therefore servers and clients must assume
* schema 2 is the highest schema number supported if the Heartbeat
* message does not contain the "Maximum schema number" field.
*
*
* Status Out 1 quint32
* Id (unique key) utf8
* Dial Frequency (Hz) quint64
* Mode utf8
* DX call utf8
* Report utf8
* Tx Mode utf8
* Tx Enabled bool
* Transmitting bool
* Decoding bool
* Rx DF quint32
* Tx DF quint32
* DE call utf8
* DE grid utf8
* DX grid utf8
* Tx Watchdog bool
* Sub-mode utf8
* Fast mode bool
* Special Operation Mode quint8
* Frequency Tolerance quint32
* T/R Period quint32
* Configuration Name utf8
* Tx Message utf8
*
* WSJT-X sends this status message when various internal state
* changes to allow the server to track the relevant state of each
* client without the need for polling commands. The current state
* changes that generate status messages are:
*
* Application start up,
* "Enable Tx" button status changes,
* dial frequency changes,
* changes to the "DX Call" field,
* operating mode, sub-mode or fast mode changes,
* transmit mode changed (in dual JT9+JT65 mode),
* changes to the "Rpt" spinner,
* after an old decodes replay sequence (see Replay below),
* when switching between Tx and Rx mode,
* at the start and end of decoding,
* when the Rx DF changes,
* when the Tx DF changes,
* when settings are exited,
* when the DX call or grid changes,
* when the Tx watchdog is set or reset,
* when the frequency tolerance is changed,
* when the T/R period is changed,
* when the configuration name changes,
* when the message being transmitted changes.
*
* The Special operation mode is an enumeration that indicates the
* setting selected in the WSJT-X "Settings->Advanced->Special
* operating activity" panel. The values are as follows:
*
* 0 -> NONE
* 1 -> NA VHF
* 2 -> EU VHF
* 3 -> FIELD DAY
* 4 -> RTTY RU
* 5 -> WW DIGI
* 6 -> FOX
* 7 -> HOUND
*
* The Frequency Tolerance and T/R period fields may have a value
* of the maximum quint32 value which implies the field is not
* applicable.
*
*
* Decode Out 2 quint32
* Id (unique key) utf8
* New bool
* Time QTime
* snr qint32
* Delta time (S) float (serialized as double)
* Delta frequency (Hz) quint32
* Mode utf8
* Message utf8
* Low confidence bool
* Off air bool
*
* The decode message is sent when a new decode is completed, in
* this case the 'New' field is true. It is also used in response
* to a "Replay" message where each old decode in the "Band
* activity" window, that has not been erased, is sent in order
* as a one of these messages with the 'New' field set to false.
* See the "Replay" message below for details of usage. Low
* confidence decodes are flagged in protocols where the decoder
* has knows that a decode has a higher than normal probability
* of being false, they should not be reported on publicly
* accessible services without some attached warning or further
* validation. Off air decodes are those that result from playing
* back a .WAV file.
*
*
* Clear Out/In 3 quint32
* Id (unique key) utf8
* Window quint8 (In only)
*
* This message is send when all prior "Decode" messages in the
* "Band Activity" window have been discarded and therefore are
* no long available for actioning with a "Reply" message. It is
* sent when the user erases the "Band activity" window and when
* WSJT-X closes down normally. The server should discard all
* decode messages upon receipt of this message.
*
* It may also be sent to a WSJT-X instance in which case it
* clears one or both of the "Band Activity" and "Rx Frequency"
* windows. The Window argument can be one of the following
* values:
*
* 0 - clear the "Band Activity" window (default)
* 1 - clear the "Rx Frequency" window
* 2 - clear both "Band Activity" and "Rx Frequency" windows
*
*
* Reply In 4 quint32
* Id (target unique key) utf8
* Time QTime
* snr qint32
* Delta time (S) float (serialized as double)
* Delta frequency (Hz) quint32
* Mode utf8
* Message utf8
* Low confidence bool
* Modifiers quint8
*
* In order for a server to provide a useful cooperative service
* to WSJT-X it is possible for it to initiate a QSO by sending
* this message to a client. WSJT-X filters this message and only
* acts upon it if the message exactly describes a prior decode
* and that decode is a CQ or QRZ message. The action taken is
* exactly equivalent to the user double clicking the message in
* the "Band activity" window. The intent of this message is for
* servers to be able to provide an advanced look up of potential
* QSO partners, for example determining if they have been worked
* before or if working them may advance some objective like
* award progress. The intention is not to provide a secondary
* user interface for WSJT-X, it is expected that after QSO
* initiation the rest of the QSO is carried out manually using
* the normal WSJT-X user interface.
*
* The Modifiers field allows the equivalent of keyboard
* modifiers to be sent "as if" those modifier keys where pressed
* while double-clicking the specified decoded message. The
* modifier values (hexadecimal) are as follows:
*
* no modifier 0x00
* SHIFT 0x02
* CTRL 0x04 CMD on Mac
* ALT 0x08
* META 0x10 Windows key on MS Windows
* KEYPAD 0x20 Keypad or arrows
* Group switch 0x40 X11 only
*
*
* QSO Logged Out 5 quint32
* Id (unique key) utf8
* Date & Time Off QDateTime
* DX call utf8
* DX grid utf8
* Tx frequency (Hz) quint64
* Mode utf8
* Report sent utf8
* Report received utf8
* Tx power utf8
* Comments utf8
* Name utf8
* Date & Time On QDateTime
* Operator call utf8
* My call utf8
* 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"
* button.
*
*
* Close Out/In 6 quint32
* Id (unique key) utf8
*
* Close is sent by a client immediately prior to it shutting
* down gracefully. When sent by a server it requests the target
* client to close down gracefully.
*
*
* Replay In 7 quint32
* Id (unique key) utf8
*
* When a server starts it may be useful for it to determine the
* state of preexisting clients. Sending this message to each
* client as it is discovered will cause that client (WSJT-X) to
* send a "Decode" message for each decode currently in its "Band
* activity" window. Each "Decode" message sent will have the
* "New" flag set to false so that they can be distinguished from
* new decodes. After all the old decodes have been broadcast a
* "Status" message is also broadcast. If the server wishes to
* determine the status of a newly discovered client; this
* message should be used.
*
*
* Halt Tx In 8
* Id (unique key) utf8
* Auto Tx Only bool
*
* The server may stop a client from transmitting messages either
* immediately or at the end of the current transmission period
* using this message.
*
*
* Free Text In 9
* Id (unique key) utf8
* Text utf8
* Send bool
*
* This message allows the server to set the current free text
* message content. Sending this message with a non-empty "Text"
* field is equivalent to typing a new message (old contents are
* discarded) in to the WSJT-X free text message field or "Tx5"
* field (both are updated) and if the "Send" flag is set then
* clicking the "Now" radio button for the "Tx5" field if tab one
* is current or clicking the "Free msg" radio button if tab two
* is current.
*
* It is the responsibility of the sender to limit the length of
* the message text and to limit it to legal message
* characters. Despite this, it may be difficult for the sender
* to determine the maximum message length without reimplementing
* the complete message encoding protocol. Because of this is may
* be better to allow any reasonable message length and to let
* the WSJT-X application encode and possibly truncate the actual
* on-air message.
*
* If the message text is empty the meaning of the message is
* refined to send the current free text unchanged when the
* "Send" flag is set or to clear the current free text when the
* "Send" flag is unset. Note that this API does not include a
* command to determine the contents of the current free text
* message.
*
*
* WSPRDecode Out 10 quint32
* Id (unique key) utf8
* New bool
* Time QTime
* snr qint32
* Delta time (S) float (serialized as double)
* Frequency (Hz) quint64
* Drift (Hz) qint32
* Callsign utf8
* Grid utf8
* Power (dBm) qint32
* Off air bool
*
* The decode message is sent when a new decode is completed, in
* this case the 'New' field is true. It is also used in response
* to a "Replay" message where each old decode in the "Band
* activity" window, that has not been erased, is sent in order
* as a one of these messages with the 'New' field set to
* false. See the "Replay" message below for details of
* usage. The off air field indicates that the decode was decoded
* from a played back recording.
*
*
* Location In 11
* Id (unique key) utf8
* Location utf8
*
* This message allows the server to set the current current
* geographical location of operation. The supplied location is
* not persistent but is used as a session lifetime replacement
* loction that overrides the Maidenhead grid locater set in the
* application settings. The intent is to allow an external
* application to update the operating location dynamically
* during a mobile period of operation.
*
* Currently only Maidenhead grid squares or sub-squares are
* accepted, i.e. 4- or 6-digit locators. Other formats may be
* accepted in future.
*
*
* Logged ADIF Out 12 quint32
* Id (unique key) utf8
* ADIF text utf8
*
* The logged ADIF message is sent to the server(s) when the
* WSJT-X user accepts the "Log QSO" dialog by clicking the "OK"
* button. The "ADIF text" field consists of a valid ADIF file
* such that the WSJT-X UDP header information is encapsulated
* into a valid ADIF header. E.g.:
*
* <magic-number><schema-number><type><id><32-bit-count> # binary encoded fields
* # the remainder is the contents of the ADIF text field
* <adif_ver:5>3.0.7
* <programid:6>WSJT-X
* <EOH>
* ADIF log data fields ...<EOR>
*
* Note that receiving applications can treat the whole message
* as a valid ADIF file with one record without special parsing.
*
*
* Highlight Callsign In 13 quint32
* Id (unique key) utf8
* Callsign utf8
* Background Color QColor
* Foreground Color QColor
* Highlight last bool
*
* The server may send this message at any time. The message
* specifies the background and foreground color that will be
* used to highlight the specified callsign in the decoded
* messages printed in the Band Activity panel. The WSJT-X
* clients maintain a list of such instructions and apply them to
* all decoded messages in the band activity window. To clear
* and cancel highlighting send an invalid QColor value for
* either or both of the background and foreground fields. When
* using this mode the total number of callsign highlighting
* requests should be limited otherwise the performance of WSJT-X
* decoding may be impacted. A rough rule of thumb might be too
* limit the number of active highlighting requests to no more
* than 100.
*
* The "Highlight last" field allows the sender to request that
* all instances of "Callsign" in the last period only, instead
* of all instances in all periods, be highlighted.
*
*
* SwitchConfiguration In 14 quint32
* Id (unique key) utf8
* Configuration Name utf8
*
* The server may send this message at any time. The message
* specifies the name of the configuration to switch to. The new
* configuration must exist.
*
*
* Configure In 15 quint32
* Id (unique key) utf8
* Mode utf8
* Frequency Tolerance quint32
* Submode utf8
* Fast Mode bool
* T/R Period quint32
* Rx DF quint32
* DX Call utf8
* DX Grid utf8
* Generate Messages bool
*
* The server may send this message at any time. The message
* specifies various configuration options. For utf8 string
* fields an empty value implies no change, for the quint32 Rx DF
* and Frequency Tolerance fields the maximum quint32 value
* implies no change. Invalid or unrecognized values will be
* silently ignored.
*/
#include <QDataStream>
#include "pimpl_h.hpp"
class QIODevice;
class QByteArray;
class QString;
namespace NetworkMessage
{
// NEVER DELETE MESSAGE TYPES
enum Type
{
Heartbeat,
Status,
Decode,
Clear,
Reply,
QSOLogged,
Close,
Replay,
HaltTx,
FreeText,
WSPRDecode,
Location,
LoggedADIF,
HighlightCallsign,
SwitchConfiguration,
Configure,
maximum_message_type_ // ONLY add new message types
// immediately before here
};
quint32 constexpr pulse {15}; // seconds
//
// NetworkMessage::Builder - build a message containing serialized Qt types
//
class Builder
: public QDataStream
{
public:
static quint32 constexpr magic {0xadbccbda}; // never change this
// increment this if a newer Qt schema is required and add decode
// logic to the Builder and Reader class implementations
#if QT_VERSION >= QT_VERSION_CHECK (5, 4, 0)
static quint32 constexpr schema_number {3};
#elif QT_VERSION >= QT_VERSION_CHECK (5, 2, 0)
static quint32 constexpr schema_number {2};
#else
// Schema 1 (Qt_5_0) is broken
#error "Qt version 5.2 or greater required"
#endif
explicit Builder (QIODevice *, Type, QString const& id, quint32 schema);
explicit Builder (QByteArray *, Type, QString const& id, quint32 schema);
Builder (Builder const&) = delete;
Builder& operator = (Builder const&) = delete;
private:
void common_initialization (Type type, QString const& id, quint32 schema);
};
//
// NetworkMessage::Reader - read a message containing serialized Qt types
//
// Message is as per NetworkMessage::Builder above, the schema()
// member may be used to determine the schema of the original
// message.
//
class Reader
: public QDataStream
{
public:
explicit Reader (QIODevice *);
explicit Reader (QByteArray const&);
Reader (Reader const&) = delete;
Reader& operator = (Reader const&) = delete;
~Reader ();
quint32 schema () const;
Type type () const;
QString id () const;
private:
class impl;
pimpl<impl> m_;
};
}
#endif

View File

@ -0,0 +1,85 @@
#include "NetworkServerLookup.hpp"
#include <stdexcept>
#include <QHostInfo>
#include <QString>
std::tuple<QHostAddress, quint16>
network_server_lookup (QString query
, quint16 default_service_port
, QHostAddress default_host_address
, QAbstractSocket::NetworkLayerProtocol required_protocol)
{
query = query.trimmed ();
QHostAddress host_address {default_host_address};
quint16 service_port {default_service_port};
QString host_name;
if (!query.isEmpty ())
{
int port_colon_index {-1};
if ('[' == query[0])
{
// assume IPv6 combined address/port syntax [<address>]:<port>
auto close_bracket_index = query.lastIndexOf (']');
host_name = query.mid (1, close_bracket_index - 1);
port_colon_index = query.indexOf (':', close_bracket_index);
}
else
{
port_colon_index = query.lastIndexOf (':');
host_name = query.left (port_colon_index);
}
host_name = host_name.trimmed ();
if (port_colon_index >= 0)
{
bool ok;
service_port = query.mid (port_colon_index + 1).trimmed ().toUShort (&ok);
if (!ok)
{
throw std::runtime_error {"network server lookup error: invalid port"};
}
}
}
if (!host_name.isEmpty ())
{
auto host_info = QHostInfo::fromName (host_name);
if (host_info.addresses ().isEmpty ())
{
throw std::runtime_error {"network server lookup error: host name lookup failed"};
}
bool found {false};
for (int i {0}; i < host_info.addresses ().size () && !found; ++i)
{
host_address = host_info.addresses ().at (i);
switch (required_protocol)
{
case QAbstractSocket::IPv4Protocol:
case QAbstractSocket::IPv6Protocol:
if (required_protocol != host_address.protocol ())
{
break;
}
// fall through
case QAbstractSocket::AnyIPProtocol:
found = true;
break;
default:
throw std::runtime_error {"network server lookup error: invalid required protocol"};
}
}
if (!found)
{
throw std::runtime_error {"network server lookup error: no suitable host address found"};
}
}
return std::make_tuple (host_address, service_port);
}

View File

@ -0,0 +1,38 @@
#ifndef NETWORK_SERVER_LOOKUP_HPP__
#define NETWORK_SERVER_LOOKUP_HPP__
#include <tuple>
#include <QHostAddress>
#include <QAbstractSocket>
class QString;
//
// Do a blocking DNS lookup using query as a destination host address
// and port.
//
// query can be one of:
//
// 1) "" (empty string) - use defaults
// 2) ":nnnnn" - override default service port with port nnnnn
// 3) "<valid-host-name>" - override default host address with DNS lookup
// 4) "nnn.nnn.nnn.nnn" - override default host address with the IPv4 address given by nnn.nnn.nnn.nnn
// 5) "[<valid-IPv6-address]" - override default host address with the given IPv6 address
// 6) "<valid-host-name>:nnnnn" - use as per (3) & (2)
// 7) "nnn.nnn.nnn.nnn:nnnnn" - use as per (4) & (2)
// 8) "[<valid-IPv6-address]:nnnnn" - use as per (5) & (2)
//
// The first host address matching the protocol and the service port
// number are returned.
//
// If no suitable host address is found QHostAddress::Null will be
// returned in the first member of the result tuple.
//
std::tuple<QHostAddress, quint16>
network_server_lookup (QString query
, quint16 default_service_port
, QHostAddress default_host_address = QHostAddress::LocalHost
, QAbstractSocket::NetworkLayerProtocol protocol = QAbstractSocket::AnyIPProtocol);
#endif

552
Network/PSKReporter.cpp Normal file
View File

@ -0,0 +1,552 @@
#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 <cmath>
#include <QObject>
#include <QString>
#include <QDateTime>
#include <QSharedPointer>
#include <QUdpSocket>
#include <QTcpSocket>
#include <QHostInfo>
#include <QQueue>
#include <QByteArray>
#include <QDataStream>
#include <QTimer>
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
#include <QRandomGenerator>
#endif
#include "Logger.hpp"
#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
using logger_type = boost::log::sources::severity_channel_logger_mt<boost::log::trivial::severity_level>;
public:
impl (PSKReporter * self, Configuration const * config, QString const& program_info)
: logger_ {boost::log::keywords::channel = "PSKRPRT"}
, 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 ())
{
LOG_LOG_LOCATION (logger_, trace, "enable descriptor resend");
// 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 ())
{
LOG_LOG_LOCATION (logger_, trace, "create/recreate socket");
// handle re-opening asynchronously
auto connection = QSharedPointer<QMetaObject::Connection>::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)
{
LOG_LOG_LOCATION (logger_, warning, "socket error: " << socket_->errorString ());
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 ())
{
LOG_LOG_LOCATION (logger_, trace, "create TCP/IP socket");
socket_.reset (new QTcpSocket, &QObject::deleteLater);
send_descriptors_ = 1;
send_receiver_data_ = 1;
}
else
{
LOG_LOG_LOCATION (logger_, trace, "create UDP/IP socket");
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);
#elif QT_VERSION >= QT_VERSION_CHECK(5, 7, 0)
connect (socket_.data (), QOverload<QAbstractSocket::SocketError>::of (&QAbstractSocket::error), this, &PSKReporter::impl::handle_socket_error);
#else
connect (socket_.data (), static_cast<void (QAbstractSocket::*) (QAbstractSocket::SocketError)> (&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);
LOG_LOG_LOCATION (logger_, debug, "remote host: " << HOST << " port: " << SERVICE_PORT);
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_)
{
LOG_LOG_LOCATION (logger_, trace, "disconnecting");
socket_->disconnectFromHost ();
}
descriptor_timer_.stop ();
report_timer_.stop ();
}
void send_report (bool send_residue = false);
void build_preamble (QDataStream&);
bool flushing ()
{
bool flush = FLUSH_INTERVAL && !(++flush_counter_ % FLUSH_INTERVAL);
LOG_LOG_LOCATION (logger_, trace, "flush: " << flush);
return flush;
}
logger_type mutable logger_;
PSKReporter * self_;
Configuration const * config_;
QSharedPointer<QAbstractSocket> 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<Spot> 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<quint16> (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
LOG_LOG_LOCATION (logger_, trace, "#: " << sequence_number_);
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 ());
LOG_LOG_LOCATION (logger_, debug, "sent descriptors");
}
}
// 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 ());
LOG_LOG_LOCATION (logger_, debug, "sent local information");
}
}
void PSKReporter::impl::send_report (bool send_residue)
{
LOG_LOG_LOCATION (logger_, trace, "sending residue: " << 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 ());
LOG_LOG_LOCATION (logger_, debug, "sent residue");
tx_residue_.clear ();
}
LOG_LOG_LOCATION (logger_, debug, "pending spots: " << spots_.size ());
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<quint32> (spot.freq_)
<< static_cast<qint8> (spot.snr_);
writeUtfString (tx_out, spot.mode_);
writeUtfString (tx_out, spot.grid_);
tx_out
<< quint8 (1u) // REPORTER_SOURCE_AUTOMATIC
<< static_cast<quint32> (
#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<quint32> (
#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
LOG_LOG_LOCATION (logger_, debug, "sent spots");
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;
}
}
LOG_LOG_LOCATION (logger_, debug, "remaining spots: " << spots_.size ());
}
}
PSKReporter::PSKReporter (Configuration const * config, QString const& program_info)
: m_ {this, config, program_info}
{
LOG_LOG_LOCATION (m_->logger_, trace, "Started for: " << program_info);
}
PSKReporter::~PSKReporter ()
{
// m_->send_report (true); // send any pending spots
LOG_LOG_LOCATION (m_->logger_, trace, "Ended");
}
void PSKReporter::reconnect ()
{
LOG_LOG_LOCATION (m_->logger_, trace, "");
m_->reconnect ();
}
void PSKReporter::setLocalStation (QString const& call, QString const& gridSquare, QString const& antenna)
{
LOG_LOG_LOCATION (m_->logger_, trace, "call: " << call << " grid: " << gridSquare << " ant: " << antenna);
m_->check_connection ();
if (call != m_->rx_call_ || gridSquare != m_->rx_grid_ || antenna != m_->rx_ant_)
{
LOG_LOG_LOCATION (m_->logger_, trace, "updating information");
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)
{
LOG_LOG_LOCATION (m_->logger_, trace, "call: " << call << " grid: " << grid << " freq: " << freq << " mode: " << mode << " snr: " << 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)
{
LOG_LOG_LOCATION (m_->logger_, trace, "last: " << last);
m_->check_connection ();
if (m_->socket_ && QAbstractSocket::ConnectedState == m_->socket_->state ())
{
m_->send_report (true);
}
if (last)
{
m_->stop ();
}
}

41
Network/PSKReporter.hpp Normal file
View File

@ -0,0 +1,41 @@
#ifndef PSK_REPORTER_HPP_
#define PSK_REPORTER_HPP_
#include <QObject>
#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<impl> m_;
};
#endif

View File

@ -0,0 +1,12 @@
senderCallsign(30351/1)<string>[65535]
receiverCallsign(30351/2)<string>[65535]
senderLocator(30351/3)<string>[65535]
receiverLocator(30351/4)<string>[65535]
frequency(30351/5)<unsigned32>[4]
sNR(30351/6)<signed8>[1]
iMD(30351/7)<signed8>[1]
decoderSoftware(30351/8)<string>[65535]
antennaInformation(30351/9)<string>[65535]
mode(30351/10)<string>[65535]
informationSource(30351/11)<signed8>[1]
persistentIdentifier(30351/12)<string>[65535]

View File

@ -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 collected over 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')

View File

@ -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

342
Network/wsprnet.cpp Normal file
View File

@ -0,0 +1,342 @@
// Interface to WSPRnet website
//
// by Edson Pereira - PY2SDR
#include "wsprnet.h"
#include <cmath>
#include <QTimer>
#include <QFile>
#include <QRegExp>
#include <QRegularExpression>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QUrl>
#include <QDebug>
#include "moc_wsprnet.cpp"
namespace
{
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"(
(?<time>\d{4})
\s+(?<db>[-+]?\d+)
\s+(?<dt>[-+]?\d+\.\d+)
\s+(?<freq>\d+)
\s+`
\s+<?(?<call>[A-Z0-9/]+)>?(?:\s(?<grid>[A-R]{2}[0-9]{2}(?:[A-X]{2})?))?(?:\s+(?<dBm>\d+))?
)", QRegularExpression::ExtendedPatternSyntaxOption};
// regexp to parse wspr_spots.txt from wsprd
//
// 130223 2256 7 -21 -0.3 14.097090 DU1MGA PK04 37 0 40 0
// Date Time Sync dBm DT Freq Msg
// 1 2 3 4 5 6 -------7------ 8 9 10
QRegularExpression wspr_re(R"(^(\d+)\s+(\d+)\s+(\d+)\s+([+-]?\d+)\s+([+-]?\d+\.\d+)\s+(\d+\.\d+)\s+([^ ].*[^ ])\s+([+-]?\d+)\s+([+-]?\d+)\s+([+-]?\d+))");
};
WSPRNet::WSPRNet (QNetworkAccessManager * manager, QObject *parent)
: QObject {parent}
, network_manager_ {manager}
, spots_to_send_ {0}
{
connect (network_manager_, &QNetworkAccessManager::finished, this, &WSPRNet::networkReply);
connect (&upload_timer_, &QTimer::timeout, this, &WSPRNet::work);
}
void WSPRNet::upload (QString const& call, QString const& grid, QString const& rfreq, QString const& tfreq,
QString const& mode, float TR_period, QString const& tpct, QString const& dbm,
QString const& version, QString const& fileName)
{
m_call = call;
m_grid = grid;
m_rfreq = rfreq;
m_tfreq = tfreq;
m_mode = mode;
TR_period_ = TR_period;
m_tpct = tpct;
m_dbm = dbm;
m_vers = version;
m_file = fileName;
// Open the wsprd.out file
if (m_uploadType != 3)
{
QFile wsprdOutFile (fileName);
if (!wsprdOutFile.open (QIODevice::ReadOnly | QIODevice::Text) || !wsprdOutFile.size ())
{
spot_queue_.enqueue (urlEncodeNoSpot ());
m_uploadType = 1;
}
else
{
// Read the contents
while (!wsprdOutFile.atEnd())
{
SpotQueue::value_type query;
if (decodeLine (wsprdOutFile.readLine(), query))
{
// Prevent reporting data ouside of the current frequency band
float f = fabs (m_rfreq.toFloat() - query.queryItemValue ("tqrg", QUrl::FullyDecoded).toFloat());
if (f < 0.01) // MHz
{
spot_queue_.enqueue(urlEncodeSpot (query));
m_uploadType = 2;
}
}
}
}
}
spots_to_send_ = spot_queue_.size ();
upload_timer_.start (200);
}
void WSPRNet::post (QString const& call, QString const& grid, QString const& rfreq, QString const& tfreq,
QString const& mode, float TR_period, QString const& tpct, QString const& dbm,
QString const& version, QString const& decode_text)
{
m_call = call;
m_grid = grid;
m_rfreq = rfreq;
m_tfreq = tfreq;
m_mode = mode;
TR_period_ = TR_period;
m_tpct = tpct;
m_dbm = dbm;
m_vers = version;
if (!decode_text.size ())
{
if (!spot_queue_.size ())
{
spot_queue_.enqueue (urlEncodeNoSpot ());
m_uploadType = 3;
}
}
else
{
auto const& match = fst4_re.match (decode_text);
if (match.hasMatch ())
{
SpotQueue::value_type query;
// Prevent reporting data ouside of the current frequency
// band - removed by G4WJS to accommodate FST4W spots
// outside of WSPR segments
auto tqrg = match.captured ("freq").toInt ();
// if (tqrg >= 1400 && tqrg <= 1600)
{
query.addQueryItem ("function", "wspr");
// use time as at 3/4 of T/R period before current to
// ensure date is in Rx period
auto const& date = QDateTime::currentDateTimeUtc ().addSecs (-TR_period * 3. / 4.).date ();
query.addQueryItem ("date", date.toString ("yyMMdd"));
query.addQueryItem ("time", match.captured ("time"));
query.addQueryItem ("sig", match.captured ("db"));
query.addQueryItem ("dt", match.captured ("dt"));
query.addQueryItem ("tqrg", QString::number (rfreq.toDouble () + (tqrg - 1500) / 1e6, 'f', 6));
query.addQueryItem ("tcall", match.captured ("call"));
query.addQueryItem ("drift", "0");
query.addQueryItem ("tgrid", match.captured ("grid"));
query.addQueryItem ("dbm", match.captured ("dBm"));
spot_queue_.enqueue (urlEncodeSpot (query));
m_uploadType = 2;
}
}
}
}
void WSPRNet::networkReply (QNetworkReply * reply)
{
// check if request was ours
if (m_outstandingRequests.removeOne (reply))
{
if (QNetworkReply::NoError != reply->error ())
{
Q_EMIT uploadStatus (QString {"Error: %1"}.arg (reply->error ()));
// not clearing queue or halting queuing as it may be a
// transient one off request error
}
else
{
QString serverResponse = reply->readAll ();
if (m_uploadType == 2)
{
if (!serverResponse.contains(QRegExp("spot\\(s\\) added")))
{
Q_EMIT uploadStatus (QString {"Upload Failed: %1"}.arg (serverResponse));
spot_queue_.clear ();
upload_timer_.stop ();
}
}
if (!spot_queue_.size ())
{
Q_EMIT uploadStatus("done");
QFile f {m_file};
if (f.exists ()) f.remove ();
upload_timer_.stop ();
}
}
qDebug () << QString {"WSPRnet.org %1 outstanding requests"}.arg (m_outstandingRequests.size ());
// delete request object instance on return to the event loop otherwise it is leaked
reply->deleteLater ();
}
}
bool WSPRNet::decodeLine (QString const& line, SpotQueue::value_type& query) const
{
auto const& rx_match = wspr_re.match (line);
if (rx_match.hasMatch ()) {
int msgType = 0;
QString msg = rx_match.captured (7);
QString call, grid, dbm;
QRegularExpression msgRx;
// Check for Message Type 1
msgRx.setPattern(R"(^([A-Z0-9]{3,6})\s+([A-R]{2}\d{2})\s+(\d+))");
auto match = msgRx.match (msg);
if (match.hasMatch ()) {
msgType = 1;
call = match.captured (1);
grid = match.captured (2);
dbm = match.captured (3);
}
// Check for Message Type 2
msgRx.setPattern(R"(^([A-Z0-9/]+)\s+(\d+))");
match = msgRx.match (msg);
if (match.hasMatch ()) {
msgType = 2;
call = match.captured (1);
grid = "";
dbm = match.captured (2);
}
// Check for Message Type 3
msgRx.setPattern(R"(^<([A-Z0-9/]+)>\s+([A-R]{2}\d{2}[A-X]{2})\s+(\d+))");
match = msgRx.match (msg);
if (match.hasMatch ()) {
msgType = 3;
call = match.captured (1);
grid = match.captured (2);
dbm = match.captured (3);
}
// Unknown message format
if (!msgType) {
return false;
}
query.addQueryItem ("function", "wspr");
query.addQueryItem ("date", rx_match.captured (1));
query.addQueryItem ("time", rx_match.captured (2));
query.addQueryItem ("sig", rx_match.captured (4));
query.addQueryItem ("dt", rx_match.captured(5));
query.addQueryItem ("drift", rx_match.captured(8));
query.addQueryItem ("tqrg", rx_match.captured(6));
query.addQueryItem ("tcall", call);
query.addQueryItem ("tgrid", grid);
query.addQueryItem ("dbm", dbm);
} else {
return false;
}
return true;
}
QString WSPRNet::encode_mode () const
{
if (m_mode == "WSPR") return "2";
if (m_mode == "WSPR-15") return "15";
if (m_mode == "FST4W")
{
auto tr = static_cast<int> ((TR_period_ / 60.)+.5);
if (2 == tr || 15 == tr)
{
tr += 1; // distinguish from WSPR-2 and WSPR-15
}
return QString::number (tr);
}
return "";
}
auto WSPRNet::urlEncodeNoSpot () const -> SpotQueue::value_type
{
SpotQueue::value_type query;
query.addQueryItem ("function", "wsprstat");
query.addQueryItem ("rcall", m_call);
query.addQueryItem ("rgrid", m_grid);
query.addQueryItem ("rqrg", m_rfreq);
query.addQueryItem ("tpct", m_tpct);
query.addQueryItem ("tqrg", m_tfreq);
query.addQueryItem ("dbm", m_dbm);
query.addQueryItem ("version", m_vers);
query.addQueryItem ("mode", encode_mode ());
return query;;
}
auto WSPRNet::urlEncodeSpot (SpotQueue::value_type& query) const -> SpotQueue::value_type
{
query.addQueryItem ("version", m_vers);
query.addQueryItem ("rcall", m_call);
query.addQueryItem ("rgrid", m_grid);
query.addQueryItem ("rqrg", m_rfreq);
query.addQueryItem ("mode", encode_mode ());
return query;
}
void WSPRNet::work()
{
if (spots_to_send_ && spot_queue_.size ())
{
#if QT_VERSION < QT_VERSION_CHECK (5, 15, 0)
if (QNetworkAccessManager::Accessible != network_manager_->networkAccessible ()) {
// try and recover network access for QNAM
network_manager_->setNetworkAccessible (QNetworkAccessManager::Accessible);
}
#endif
QNetworkRequest request (QUrl {wsprNetUrl});
request.setHeader (QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
auto const& spot = spot_queue_.dequeue ();
m_outstandingRequests << network_manager_->post (request, spot.query (QUrl::FullyEncoded).toUtf8 ());
Q_EMIT uploadStatus(QString {"Uploading Spot %1/%2"}.arg (spots_to_send_ - spot_queue_.size()).arg (spots_to_send_));
}
else
{
upload_timer_.stop ();
}
}
void WSPRNet::abortOutstandingRequests () {
spot_queue_.clear ();
for (auto& request : m_outstandingRequests) {
request->abort ();
}
}

60
Network/wsprnet.h Normal file
View File

@ -0,0 +1,60 @@
#ifndef WSPRNET_H
#define WSPRNET_H
#include <QObject>
#include <QTimer>
#include <QString>
#include <QList>
#include <QUrlQuery>
#include <QQueue>
class QNetworkAccessManager;
class QNetworkReply;
class WSPRNet : public QObject
{
Q_OBJECT
using SpotQueue = QQueue<QUrlQuery>;
public:
explicit WSPRNet (QNetworkAccessManager *, QObject *parent = nullptr);
void upload (QString const& call, QString const& grid, QString const& rfreq, QString const& tfreq,
QString const& mode, float TR_peirod, QString const& tpct, QString const& dbm,
QString const& version, QString const& fileName);
void post (QString const& call, QString const& grid, QString const& rfreq, QString const& tfreq,
QString const& mode, float TR_period, QString const& tpct, QString const& dbm,
QString const& version, QString const& decode_text = QString {});
signals:
void uploadStatus (QString);
public slots:
void networkReply (QNetworkReply *);
void work ();
void abortOutstandingRequests ();
private:
bool decodeLine (QString const& line, SpotQueue::value_type& query) const;
SpotQueue::value_type urlEncodeNoSpot () const;
SpotQueue::value_type urlEncodeSpot (SpotQueue::value_type& spot) const;
QString encode_mode () const;
QNetworkAccessManager * network_manager_;
QList<QNetworkReply *> m_outstandingRequests;
QString m_call;
QString m_grid;;
QString m_rfreq;
QString m_tfreq;
QString m_mode;
QString m_tpct;
QString m_dbm;
QString m_vers;
QString m_file;
float TR_period_;
int spots_to_send_;
SpotQueue spot_queue_;
QTimer upload_timer_;
int m_uploadType;
};
#endif // WSPRNET_H

116
NonInheritingProcess.cpp Normal file
View File

@ -0,0 +1,116 @@
#include "NonInheritingProcess.hpp"
#ifdef Q_OS_WIN
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#ifndef _UNICODE
#define _UNICODE
#endif
#ifdef _WIN32_WINNT
#undef _WIN32_WINNT
#endif
#define _WIN32_WINNT 0x0601
#include <windows.h>
#endif
#include <memory>
#include <functional>
#include "pimpl_impl.hpp"
namespace
{
#ifdef Q_OS_WIN
struct start_info_deleter
{
void operator () (STARTUPINFOEXW * si)
{
if (si->lpAttributeList)
{
::DeleteProcThreadAttributeList (si->lpAttributeList);
}
delete si;
}
};
#endif
}
class NonInheritingProcess::impl
{
public:
#ifdef Q_OS_WIN
void extend_CreateProcessArguments (QProcess::CreateProcessArguments * args)
{
//
// Here we modify the CreateProcessArguments structure to use a
// STARTUPINFOEX extended argument to CreateProcess. In that we
// set up a list of handles for the new process to inherit. By
// doing this we stop all inherited handles from being
// inherited. Unfortunately UpdateProcThreadAttribute does not let
// us set up an empty handle list, so we populate the list with
// the three standard stream handles that QProcess::start has set
// up as Pipes to do IPC. Even though these Pipe handles are
// created with inheritance disabled, UpdateProcThreadAtribute and
// CreateProcess don't seem to mind, which suits us fine.
//
// Note: that we cannot just clear the inheritHandles flag as that
// stops the standard stream handles being inherited which breaks
// our IPC using std(in|out|err). Only be using a
// PROC_THREAD_ATTRIBUTE_HANDLE_LIST attribute in a STARTUPINFOEX
// structure can we avoid the all or nothing behaviour of
// CreateProcess /w respect to handle inheritance.
//
BOOL fSuccess;
SIZE_T size {0};
LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList = nullptr;
::InitializeProcThreadAttributeList (nullptr, 1, 0, &size);
lpAttributeList = reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST> (::HeapAlloc (::GetProcessHeap (), 0, size));
fSuccess = !!lpAttributeList;
if (fSuccess)
{
fSuccess = ::InitializeProcThreadAttributeList (lpAttributeList, 1, 0, &size);
}
if (fSuccess)
{
// empty list of handles
fSuccess = ::UpdateProcThreadAttribute (lpAttributeList, 0,
PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
&args->startupInfo->hStdInput, 3 * sizeof (HANDLE),
nullptr, 0);
}
if (fSuccess)
{
start_info_.reset (new STARTUPINFOEXW);
start_info_->StartupInfo = *args->startupInfo;
start_info_->StartupInfo.cb = sizeof (STARTUPINFOEXW);
start_info_->lpAttributeList = lpAttributeList;
args->startupInfo = reinterpret_cast<Q_STARTUPINFO*> (start_info_.get ());
args->flags |= EXTENDED_STARTUPINFO_PRESENT;
}
}
using start_info_type = std::unique_ptr<STARTUPINFOEXW, start_info_deleter>;
start_info_type start_info_;
#endif
};
NonInheritingProcess::NonInheritingProcess (QObject * parent)
: QProcess {parent}
{
#ifdef Q_OS_WIN
using namespace std::placeholders;
// enable cleanup after process starts or fails to start
connect (this, &QProcess::started, [this] {m_->start_info_.reset ();});
connect (this, &QProcess::errorOccurred, [this] (QProcess::ProcessError) {m_->start_info_.reset ();});
setCreateProcessArgumentsModifier (std::bind (&NonInheritingProcess::impl::extend_CreateProcessArguments, &*m_, _1));
#endif
}
NonInheritingProcess::~NonInheritingProcess ()
{
}

35
NonInheritingProcess.hpp Normal file
View File

@ -0,0 +1,35 @@
#ifndef NON_INHERITING_PROCESS_HPP__
#define NON_INHERITING_PROCESS_HPP__
#include <QProcess>
#include "pimpl_h.hpp"
class QObject;
//
// class NonInheritingProcess - Manage a process without it inheriting
// all inheritable handles
//
// On MS Windows QProcess creates sub-processes which inherit all
// inheritable handles, and handles on Windows are inheritable by
// default. This can cause the lifetime of objects to be unexpectedly
// extended, which in turn can cause unexpected errors. The motivation
// for this class was implementing log file rotation using the Boost
// log library. The current log file's handle gets inherited by any
// long running sub-process started by QProcess and that causes a
// sharing violation when attempting to rename the log file on
// rotation, even though the log library closes the current log file
// before trying to rename it.
//
class NonInheritingProcess
: public QProcess
{
public:
NonInheritingProcess (QObject * parent = nullptr);
~NonInheritingProcess ();
private:
class impl;
pimpl<impl> m_;
};
#endif

9
Palettes/Banana.pal Normal file
View File

@ -0,0 +1,9 @@
0; 0; 0
59; 59; 27
119;119; 59
179;179; 91
227;227;123
235;235;151
239;239;183
247;247;219
255;255;255

9
Palettes/Blue1.pal Normal file
View File

@ -0,0 +1,9 @@
0; 0; 2
0; 0; 64
7; 11;128
39; 47;192
95;115;217
151;179;231
187;203;239
219;227;247
255;255;255

9
Palettes/Blue2.pal Normal file
View File

@ -0,0 +1,9 @@
3; 3; 64
7; 11;128
39; 47;192
95;115;217
151;179;231
187;203;239
219;227;247
255;255;255
255;253;108

9
Palettes/Blue3.pal Normal file
View File

@ -0,0 +1,9 @@
0; 0; 0
31; 31; 31
63; 63; 63
91; 91;167
119;119;191
155;155;219
191;191;191
223;223;223
255;255;255

9
Palettes/Brown.pal Normal file
View File

@ -0,0 +1,9 @@
0; 0; 0
107; 63; 11
175; 95; 31
199;119; 43
215;163; 63
231;211; 87
243;247;111
247;251;179
255;255;255

9
Palettes/Cyan1.pal Normal file
View File

@ -0,0 +1,9 @@
0; 0; 0
5; 10; 10
22; 42; 42
52; 99; 99
94;175;175
131;209;209
162;224;224
202;239;239
255;255;255

9
Palettes/Cyan2.pal Normal file
View File

@ -0,0 +1,9 @@
0; 0; 0
35; 51; 51
75;103;103
115;159;159
155;211;211
183;231;231
203;239;239
227;247;247
255;255;255

9
Palettes/Cyan3.pal Normal file
View File

@ -0,0 +1,9 @@
0; 0; 0
94;114;114
138;162;162
171;201;201
199;232;232
216;243;243
228;247;247
241;251;251
255;255;255

9
Palettes/Default.pal Normal file
View File

@ -0,0 +1,9 @@
0; 0; 0
0; 6;136
0; 19;198
0; 32;239
172;167;105
194;198; 49
225;228;107
255;255; 0
255; 51; 0

Some files were not shown because too many files have changed in this diff Show More