SVN r8748
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
parameter (MAXTEST=75,NTEST=68)
|
||||
character*40 testmsg(MAXTEST)
|
||||
character*40 testmsgchk(MAXTEST)
|
||||
! Test msgs should include the extremes for the different types
|
||||
! See pfx.f90
|
||||
! Type 1 P & A
|
||||
! Type 1 1A & E5
|
||||
data testmsg(1:NTEST)/ &
|
||||
"CQ WB9XYZ EN34", &
|
||||
"CQ DX WB9XYZ EN34", &
|
||||
"QRZ WB9XYZ EN34", &
|
||||
"KA1ABC WB9XYZ EN34", &
|
||||
"KA1ABC WB9XYZ RO", &
|
||||
"KA1ABC WB9XYZ -21", &
|
||||
"KA1ABC WB9XYZ R-19", &
|
||||
"KA1ABC WB9XYZ RRR", &
|
||||
"KA1ABC WB9XYZ 73", &
|
||||
"KA1ABC WB9XYZ", &
|
||||
"CQ 000 WB9XYZ EN34", &
|
||||
"CQ 999 WB9XYZ EN34", &
|
||||
"CQ EU WB9XYZ EN34", &
|
||||
"CQ WY WB9XYZ EN34", &
|
||||
"1A/KA1ABC WB9XYZ", &
|
||||
"E5/KA1ABC WB9XYZ", &
|
||||
"KA1ABC 1A/WB9XYZ", &
|
||||
"KA1ABC E5/WB9XYZ", &
|
||||
"KA1ABC/P WB9XYZ", &
|
||||
"KA1ABC/A WB9XYZ", &
|
||||
"KA1ABC WB9XYZ/P", &
|
||||
"KA1ABC WB9XYZ/A", &
|
||||
"CQ KA1ABC/P", &
|
||||
"CQ WB9XYZ/A", &
|
||||
"QRZ KA1ABC/P", &
|
||||
"QRZ WB9XYZ/A", &
|
||||
"DE KA1ABC/P", &
|
||||
"DE WB9XYZ/A", &
|
||||
"CQ 1A/KA1ABC", &
|
||||
"CQ E5/KA1ABC", &
|
||||
"DE 1A/KA1ABC", &
|
||||
"DE E5/KA1ABC", &
|
||||
"QRZ 1A/KA1ABC", &
|
||||
"QRZ E5/KA1ABC", &
|
||||
"CQ WB9XYZ/1A", &
|
||||
"CQ WB9XYZ/E5", &
|
||||
"QRZ WB9XYZ/1A", &
|
||||
"QRZ WB9XYZ/E5", &
|
||||
"DE WB9XYZ/1A", &
|
||||
"DE WB9XYZ/E5", &
|
||||
"CQ A000/KA1ABC FM07", &
|
||||
"CQ ZZZZ/KA1ABC FM07", &
|
||||
"QRZ W4/KA1ABC FM07", &
|
||||
"DE W4/KA1ABC FM07", &
|
||||
"CQ W4/KA1ABC -22", &
|
||||
"DE W4/KA1ABC -22", &
|
||||
"QRZ W4/KA1ABC -22", &
|
||||
"CQ W4/KA1ABC R-22", &
|
||||
"DE W4/KA1ABC R-22", &
|
||||
"QRZ W4/KA1ABC R-22", &
|
||||
"DE W4/KA1ABC 73", &
|
||||
"CQ KA1ABC FM07", &
|
||||
"QRZ KA1ABC FM07", &
|
||||
"DE KA1ABC/VE6 FM07", &
|
||||
"CQ KA1ABC/VE6 -22", &
|
||||
"DE KA1ABC/VE6 -22", &
|
||||
"QRZ KA1ABC/VE6 -22", &
|
||||
"CQ KA1ABC/VE6 R-22", &
|
||||
"DE KA1ABC/VE6 R-22", &
|
||||
"QRZ KA1ABC/VE6 R-22", &
|
||||
"DE KA1ABC 73", &
|
||||
"HELLO WORLD", &
|
||||
"ZL4/KA1ABC 73", &
|
||||
"KA1ABC XL/WB9XYZ", &
|
||||
"KA1ABC WB9XYZ/W4", &
|
||||
"DE KA1ABC/QRP 2W", &
|
||||
"KA1ABC/1 WB9XYZ/1", &
|
||||
"123456789ABCDEFGH"/
|
||||
data testmsgchk(1:NTEST)/ &
|
||||
"CQ WB9XYZ EN34", &
|
||||
"CQ DX WB9XYZ EN34", &
|
||||
"QRZ WB9XYZ EN34", &
|
||||
"KA1ABC WB9XYZ EN34", &
|
||||
"KA1ABC WB9XYZ RO", &
|
||||
"KA1ABC WB9XYZ -21", &
|
||||
"KA1ABC WB9XYZ R-19", &
|
||||
"KA1ABC WB9XYZ RRR", &
|
||||
"KA1ABC WB9XYZ 73", &
|
||||
"KA1ABC WB9XYZ", &
|
||||
"CQ 000 WB9XYZ EN34", &
|
||||
"CQ 999 WB9XYZ EN34", &
|
||||
"CQ EU WB9XYZ EN34", &
|
||||
"CQ WY WB9XYZ EN34", &
|
||||
"1A/KA1ABC WB9XYZ", &
|
||||
"E5/KA1ABC WB9XYZ", &
|
||||
"KA1ABC 1A/WB9XYZ", &
|
||||
"KA1ABC E5/WB9XYZ", &
|
||||
"KA1ABC/P WB9XYZ", &
|
||||
"KA1ABC/A WB9XYZ", &
|
||||
"KA1ABC WB9XYZ/P", &
|
||||
"KA1ABC WB9XYZ/A", &
|
||||
"CQ KA1ABC/P", &
|
||||
"CQ WB9XYZ/A", &
|
||||
"QRZ KA1ABC/P", &
|
||||
"QRZ WB9XYZ/A", &
|
||||
"DE KA1ABC/P", &
|
||||
"DE WB9XYZ/A", &
|
||||
"CQ 1A/KA1ABC", &
|
||||
"CQ E5/KA1ABC", &
|
||||
"DE 1A/KA1ABC", &
|
||||
"DE E5/KA1ABC", &
|
||||
"QRZ 1A/KA1ABC", &
|
||||
"QRZ E5/KA1ABC", &
|
||||
"CQ WB9XYZ/1A", &
|
||||
"CQ WB9XYZ/E5", &
|
||||
"QRZ WB9XYZ/1A", &
|
||||
"QRZ WB9XYZ/E5", &
|
||||
"DE WB9XYZ/1A", &
|
||||
"DE WB9XYZ/E5", &
|
||||
"CQ A000/KA1ABC FM07", &
|
||||
"CQ ZZZZ/KA1ABC FM07", &
|
||||
"QRZ W4/KA1ABC FM07", &
|
||||
"DE W4/KA1ABC FM07", &
|
||||
"CQ W4/KA1ABC -22", &
|
||||
"DE W4/KA1ABC -22", &
|
||||
"QRZ W4/KA1ABC -22", &
|
||||
"CQ W4/KA1ABC R-22", &
|
||||
"DE W4/KA1ABC R-22", &
|
||||
"QRZ W4/KA1ABC R-22", &
|
||||
"DE W4/KA1ABC 73", &
|
||||
"CQ KA1ABC FM07", &
|
||||
"QRZ KA1ABC FM07", &
|
||||
"DE KA1ABC/VE6 FM07", &
|
||||
"CQ KA1ABC/VE6 -22", &
|
||||
"DE KA1ABC/VE6 -22", &
|
||||
"QRZ KA1ABC/VE6 -22", &
|
||||
"CQ KA1ABC/VE6 R-22", &
|
||||
"DE KA1ABC/VE6 R-22", &
|
||||
"QRZ KA1ABC/VE6 R-22", &
|
||||
"DE KA1ABC 73", &
|
||||
"HELLO WORLD", &
|
||||
"ZL4/KA1ABC 73", &
|
||||
"KA1ABC XL/WB9", &
|
||||
"KA1ABC WB9XYZ", &
|
||||
"DE KA1ABC/QRP", &
|
||||
"KA1ABC/1 WB9X", &
|
||||
"123456789ABCD"/
|
||||
@@ -0,0 +1,337 @@
|
||||
#include "Modulator.hpp"
|
||||
#include <limits>
|
||||
#include <qmath.h>
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include "mainwindow.h"
|
||||
#include "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, unsigned periodLengthInSeconds,
|
||||
QObject * parent)
|
||||
: AudioDevice {parent}
|
||||
, m_quickClose {false}
|
||||
, m_phi {0.0}
|
||||
, m_toneSpacing {0.0}
|
||||
, m_fSpread {0.0}
|
||||
, m_frameRate {frameRate}
|
||||
, m_period {periodLengthInSeconds}
|
||||
, m_state {Idle}
|
||||
, m_tuning {false}
|
||||
, m_cwLevel {false}
|
||||
, m_j0 {-1}
|
||||
, m_toneFrequency0 {1500.0}
|
||||
{
|
||||
}
|
||||
|
||||
void Modulator::start (unsigned symbolsLength, double framesPerSymbol,
|
||||
double frequency, double toneSpacing,
|
||||
SoundOutput * stream, Channel channel,
|
||||
bool synchronize, bool fastMode, double dBSNR, int TRperiod)
|
||||
{
|
||||
Q_ASSERT (stream);
|
||||
// Time according to this computer which becomes our base time
|
||||
qint64 ms0 = QDateTime::currentMSecsSinceEpoch() % 86400000;
|
||||
|
||||
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 = 1920 == m_nsps && 15 == m_period ? 500 : 1000;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
unsigned mstr = ms0 % (1000 * m_period); // ms in period
|
||||
|
||||
// round up to an exact portion of a second that allows for startup
|
||||
// delays
|
||||
m_ic = (mstr / delay_ms) * m_frameRate * delay_ms / 1000;
|
||||
|
||||
if(m_bFastMode) m_ic=0;
|
||||
|
||||
m_silentFrames = 0;
|
||||
// calculate number of silent frames to send, so that audio will start at
|
||||
// the nominal time "delay_ms" into the Tx sequence.
|
||||
if (synchronize && !m_tuning && !m_bFastMode) {
|
||||
m_silentFrames = m_ic + m_frameRate / (1000 / delay_ms) - (mstr * (m_frameRate / 1000));
|
||||
}
|
||||
|
||||
initialize (QIODevice::ReadOnly, channel);
|
||||
Q_EMIT stateChanged ((m_state = (synchronize && m_silentFrames) ?
|
||||
Synchronizing : Active));
|
||||
m_stream = stream;
|
||||
if (m_stream) m_stream->restart (this);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
switch (m_state)
|
||||
{
|
||||
case Synchronizing:
|
||||
{
|
||||
if (m_silentFrames) { // send silence up to first second
|
||||
framesGenerated = qMin (m_silentFrames, numFrames);
|
||||
for ( ; samples != end; samples = load (0, samples)) { // silence
|
||||
}
|
||||
m_silentFrames -= framesGenerated;
|
||||
return framesGenerated * bytesPerFrame ();
|
||||
}
|
||||
|
||||
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;
|
||||
// qDebug() << "Mod A" << m_toneSpacing << m_ic;
|
||||
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) slowCwId=false;
|
||||
bool fastCwId=false;
|
||||
static bool bCwId=false;
|
||||
qint64 ms = QDateTime::currentMSecsSinceEpoch();
|
||||
float tsec=0.001*(ms % (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
|
||||
|
||||
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 - 24000;
|
||||
i0=i1-816;
|
||||
}
|
||||
|
||||
qint16 sample;
|
||||
for (unsigned i = 0; i < numFrames && m_ic <= i1; ++i) {
|
||||
isym=0;
|
||||
if(!m_tuning and m_TRperiod!=3) 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;
|
||||
}
|
||||
}
|
||||
// qDebug() << "Mod B" << m_bFastMode << m_ic << numFrames << isym << itone[isym]
|
||||
// << m_toneFrequency0 << m_nsps;
|
||||
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) {
|
||||
float x1=(float)qrand()/RAND_MAX;
|
||||
float x2=(float)qrand()/RAND_MAX;
|
||||
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));
|
||||
if(m_toneSpacing < 0) sample=qRound(m_amp*foxcom_.wave[m_ic]);
|
||||
|
||||
// if(m_ic < 100) qDebug() << "Mod C" << m_ic << m_amp << foxcom_.wave[m_ic] << sample;
|
||||
|
||||
samples = load(postProcessSample(sample), samples);
|
||||
++framesGenerated;
|
||||
++m_ic;
|
||||
}
|
||||
|
||||
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
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
#include "MessageAggregatorMainWindow.hpp"
|
||||
|
||||
#include <QtWidgets>
|
||||
#include <QDateTime>
|
||||
|
||||
#include "DecodesModel.hpp"
|
||||
#include "BeaconsModel.hpp"
|
||||
#include "ClientWidget.hpp"
|
||||
|
||||
using port_type = MessageServer::port_type;
|
||||
|
||||
namespace
|
||||
{
|
||||
char const * const headings[] = {
|
||||
QT_TRANSLATE_NOOP ("MessageAggregatorMainWindow", "Time On"),
|
||||
QT_TRANSLATE_NOOP ("MessageAggregatorMainWindow", "Time Off"),
|
||||
QT_TRANSLATE_NOOP ("MessageAggregatorMainWindow", "Callsign"),
|
||||
QT_TRANSLATE_NOOP ("MessageAggregatorMainWindow", "Grid"),
|
||||
QT_TRANSLATE_NOOP ("MessageAggregatorMainWindow", "Name"),
|
||||
QT_TRANSLATE_NOOP ("MessageAggregatorMainWindow", "Frequency"),
|
||||
QT_TRANSLATE_NOOP ("MessageAggregatorMainWindow", "Mode"),
|
||||
QT_TRANSLATE_NOOP ("MessageAggregatorMainWindow", "Sent"),
|
||||
QT_TRANSLATE_NOOP ("MessageAggregatorMainWindow", "Rec'd"),
|
||||
QT_TRANSLATE_NOOP ("MessageAggregatorMainWindow", "Power"),
|
||||
QT_TRANSLATE_NOOP ("MessageAggregatorMainWindow", "Operator"),
|
||||
QT_TRANSLATE_NOOP ("MessageAggregatorMainWindow", "My Call"),
|
||||
QT_TRANSLATE_NOOP ("MessageAggregatorMainWindow", "My Grid"),
|
||||
QT_TRANSLATE_NOOP ("MessageAggregatorMainWindow", "Comments"),
|
||||
};
|
||||
}
|
||||
|
||||
MessageAggregatorMainWindow::MessageAggregatorMainWindow ()
|
||||
: log_ {new QStandardItemModel {0, 14, this}}
|
||||
, decodes_model_ {new DecodesModel {this}}
|
||||
, beacons_model_ {new BeaconsModel {this}}
|
||||
, server_ {new MessageServer {this}}
|
||||
, multicast_group_line_edit_ {new QLineEdit}
|
||||
, log_table_view_ {new QTableView}
|
||||
, add_call_of_interest_action_ {new QAction {tr ("&Add callsign"), this}}
|
||||
, delete_call_of_interest_action_ {new QAction {tr ("&Delete callsign"), this}}
|
||||
, last_call_of_interest_action_ {new QAction {tr ("&Highlight last only"), this}}
|
||||
, call_of_interest_bg_colour_action_ {new QAction {tr ("&Background colour"), this}}
|
||||
, call_of_interest_fg_colour_action_ {new QAction {tr ("&Foreground colour"), this}}
|
||||
{
|
||||
// logbook
|
||||
int column {0};
|
||||
for (auto const& heading : headings)
|
||||
{
|
||||
log_->setHeaderData (column++, Qt::Horizontal, tr (heading));
|
||||
}
|
||||
connect (server_, &MessageServer::qso_logged, this, &MessageAggregatorMainWindow::log_qso);
|
||||
|
||||
// menu bar
|
||||
auto file_menu = menuBar ()->addMenu (tr ("&File"));
|
||||
|
||||
auto exit_action = new QAction {tr ("E&xit"), this};
|
||||
exit_action->setShortcuts (QKeySequence::Quit);
|
||||
exit_action->setToolTip (tr ("Exit the application"));
|
||||
file_menu->addAction (exit_action);
|
||||
connect (exit_action, &QAction::triggered, this, &MessageAggregatorMainWindow::close);
|
||||
|
||||
view_menu_ = menuBar ()->addMenu (tr ("&View"));
|
||||
|
||||
// central layout
|
||||
auto central_layout = new QVBoxLayout;
|
||||
|
||||
// server details
|
||||
auto port_spin_box = new QSpinBox;
|
||||
port_spin_box->setMinimum (1);
|
||||
port_spin_box->setMaximum (std::numeric_limits<port_type>::max ());
|
||||
auto group_box_layout = new QFormLayout;
|
||||
group_box_layout->addRow (tr ("Port number:"), port_spin_box);
|
||||
group_box_layout->addRow (tr ("Multicast Group (blank for unicast server):"), multicast_group_line_edit_);
|
||||
auto group_box = new QGroupBox {tr ("Server Details")};
|
||||
group_box->setLayout (group_box_layout);
|
||||
central_layout->addWidget (group_box);
|
||||
|
||||
log_table_view_->setModel (log_);
|
||||
log_table_view_->verticalHeader ()->hide ();
|
||||
central_layout->addWidget (log_table_view_);
|
||||
|
||||
// central widget
|
||||
auto central_widget = new QWidget;
|
||||
central_widget->setLayout (central_layout);
|
||||
|
||||
// main window setup
|
||||
setCentralWidget (central_widget);
|
||||
setDockOptions (AnimatedDocks | AllowNestedDocks | AllowTabbedDocks);
|
||||
setTabPosition (Qt::BottomDockWidgetArea, QTabWidget::North);
|
||||
|
||||
QDockWidget * calls_dock {new QDockWidget {tr ("Calls of Interest"), this}};
|
||||
calls_dock->setAllowedAreas (Qt::RightDockWidgetArea);
|
||||
calls_of_interest_ = new QListWidget {calls_dock};
|
||||
calls_of_interest_->setContextMenuPolicy (Qt::ActionsContextMenu);
|
||||
calls_of_interest_->insertAction (nullptr, add_call_of_interest_action_);
|
||||
connect (add_call_of_interest_action_, &QAction::triggered, [this] () {
|
||||
auto item = new QListWidgetItem {};
|
||||
item->setFlags (item->flags () | Qt::ItemIsEditable);
|
||||
item->setData (Qt::UserRole, QString {});
|
||||
calls_of_interest_->addItem (item);
|
||||
calls_of_interest_->editItem (item);
|
||||
});
|
||||
calls_of_interest_->insertAction (nullptr, delete_call_of_interest_action_);
|
||||
connect (delete_call_of_interest_action_, &QAction::triggered, [this] () {
|
||||
for (auto item : calls_of_interest_->selectedItems ())
|
||||
{
|
||||
auto old_call = item->data (Qt::UserRole);
|
||||
if (old_call.isValid ()) change_highlighting (old_call.toString ());
|
||||
delete item;
|
||||
}
|
||||
});
|
||||
calls_of_interest_->insertAction (nullptr, last_call_of_interest_action_);
|
||||
connect (last_call_of_interest_action_, &QAction::triggered, [this] () {
|
||||
for (auto item : calls_of_interest_->selectedItems ())
|
||||
{
|
||||
auto old_call = item->data (Qt::UserRole);
|
||||
change_highlighting (old_call.toString ());
|
||||
change_highlighting (old_call.toString ()
|
||||
, item->background ().color (), item->foreground ().color (), true);
|
||||
delete item;
|
||||
}
|
||||
});
|
||||
calls_of_interest_->insertAction (nullptr, call_of_interest_bg_colour_action_);
|
||||
connect (call_of_interest_bg_colour_action_, &QAction::triggered, [this] () {
|
||||
for (auto item : calls_of_interest_->selectedItems ())
|
||||
{
|
||||
auto old_call = item->data (Qt::UserRole);
|
||||
auto new_colour = QColorDialog::getColor (item->background ().color ()
|
||||
, this, tr ("Select background color"));
|
||||
if (new_colour.isValid ())
|
||||
{
|
||||
change_highlighting (old_call.toString (), new_colour, item->foreground ().color ());
|
||||
item->setBackground (new_colour);
|
||||
}
|
||||
}
|
||||
});
|
||||
calls_of_interest_->insertAction (nullptr, call_of_interest_fg_colour_action_);
|
||||
connect (call_of_interest_fg_colour_action_, &QAction::triggered, [this] () {
|
||||
for (auto item : calls_of_interest_->selectedItems ())
|
||||
{
|
||||
auto old_call = item->data (Qt::UserRole);
|
||||
auto new_colour = QColorDialog::getColor (item->foreground ().color ()
|
||||
, this, tr ("Select foreground color"));
|
||||
if (new_colour.isValid ())
|
||||
{
|
||||
change_highlighting (old_call.toString (), item->background ().color (), new_colour);
|
||||
item->setForeground (new_colour);
|
||||
}
|
||||
}
|
||||
});
|
||||
connect (calls_of_interest_, &QListWidget::itemChanged, [this] (QListWidgetItem * item) {
|
||||
auto old_call = item->data (Qt::UserRole);
|
||||
auto new_call = item->text ().toUpper ();
|
||||
if (new_call != old_call)
|
||||
{
|
||||
// tell all clients
|
||||
if (old_call.isValid ())
|
||||
{
|
||||
change_highlighting (old_call.toString ());
|
||||
}
|
||||
item->setData (Qt::UserRole, new_call);
|
||||
item->setText (new_call);
|
||||
auto bg = item->listWidget ()->palette ().text ().color ();
|
||||
auto fg = item->listWidget ()->palette ().base ().color ();
|
||||
item->setBackground (bg);
|
||||
item->setForeground (fg);
|
||||
change_highlighting (new_call, bg, fg);
|
||||
}
|
||||
});
|
||||
|
||||
calls_dock->setWidget (calls_of_interest_);
|
||||
addDockWidget (Qt::RightDockWidgetArea, calls_dock);
|
||||
view_menu_->addAction (calls_dock->toggleViewAction ());
|
||||
view_menu_->addSeparator ();
|
||||
|
||||
// connect up server
|
||||
connect (server_, &MessageServer::error, [this] (QString const& message) {
|
||||
QMessageBox::warning (this, QApplication::applicationName (), tr ("Network Error"), message);
|
||||
});
|
||||
connect (server_, &MessageServer::client_opened, this, &MessageAggregatorMainWindow::add_client);
|
||||
connect (server_, &MessageServer::client_closed, this, &MessageAggregatorMainWindow::remove_client);
|
||||
connect (server_, &MessageServer::client_closed, decodes_model_, &DecodesModel::clear_decodes);
|
||||
connect (server_, &MessageServer::client_closed, beacons_model_, &BeaconsModel::clear_decodes);
|
||||
connect (server_, &MessageServer::decode, [this] (bool is_new, QString const& id, QTime time
|
||||
, qint32 snr, float delta_time
|
||||
, quint32 delta_frequency, QString const& mode
|
||||
, QString const& message, bool low_confidence
|
||||
, bool off_air) {
|
||||
decodes_model_->add_decode (is_new, id, time, snr, delta_time, delta_frequency, mode, message
|
||||
, low_confidence, off_air, dock_widgets_[id]->fast_mode ());});
|
||||
connect (server_, &MessageServer::WSPR_decode, beacons_model_, &BeaconsModel::add_beacon_spot);
|
||||
connect (server_, &MessageServer::clear_decodes, decodes_model_, &DecodesModel::clear_decodes);
|
||||
connect (server_, &MessageServer::clear_decodes, beacons_model_, &BeaconsModel::clear_decodes);
|
||||
connect (decodes_model_, &DecodesModel::reply, server_, &MessageServer::reply);
|
||||
|
||||
// UI behaviour
|
||||
connect (port_spin_box, static_cast<void (QSpinBox::*)(int)> (&QSpinBox::valueChanged)
|
||||
, [this] (port_type port) {server_->start (port);});
|
||||
connect (multicast_group_line_edit_, &QLineEdit::editingFinished, [this, port_spin_box] () {
|
||||
server_->start (port_spin_box->value (), QHostAddress {multicast_group_line_edit_->text ()});
|
||||
});
|
||||
|
||||
port_spin_box->setValue (2237); // start up in unicast mode
|
||||
show ();
|
||||
}
|
||||
|
||||
void MessageAggregatorMainWindow::log_qso (QString const& /*id*/, QDateTime time_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)
|
||||
{
|
||||
QList<QStandardItem *> row;
|
||||
row << new QStandardItem {time_on.toString ("dd-MMM-yyyy hh:mm:ss")}
|
||||
<< new QStandardItem {time_off.toString ("dd-MMM-yyyy hh:mm:ss")}
|
||||
<< new QStandardItem {dx_call}
|
||||
<< new QStandardItem {dx_grid}
|
||||
<< new QStandardItem {name}
|
||||
<< new QStandardItem {Radio::frequency_MHz_string (dial_frequency)}
|
||||
<< new QStandardItem {mode}
|
||||
<< new QStandardItem {report_sent}
|
||||
<< new QStandardItem {report_received}
|
||||
<< new QStandardItem {tx_power}
|
||||
<< new QStandardItem {operator_call}
|
||||
<< new QStandardItem {my_call}
|
||||
<< new QStandardItem {my_grid}
|
||||
<< new QStandardItem {comments};
|
||||
log_->appendRow (row);
|
||||
log_table_view_->resizeColumnsToContents ();
|
||||
log_table_view_->horizontalHeader ()->setStretchLastSection (true);
|
||||
log_table_view_->scrollToBottom ();
|
||||
}
|
||||
|
||||
void MessageAggregatorMainWindow::add_client (QString const& id, QString const& version, QString const& revision)
|
||||
{
|
||||
auto dock = new ClientWidget {decodes_model_, beacons_model_, id, version, revision, calls_of_interest_, this};
|
||||
dock->setAttribute (Qt::WA_DeleteOnClose);
|
||||
auto view_action = dock->toggleViewAction ();
|
||||
view_action->setEnabled (true);
|
||||
view_menu_->addAction (view_action);
|
||||
addDockWidget (Qt::BottomDockWidgetArea, dock);
|
||||
connect (server_, &MessageServer::status_update, dock, &ClientWidget::update_status);
|
||||
connect (server_, &MessageServer::decode, dock, &ClientWidget::decode_added);
|
||||
connect (server_, &MessageServer::WSPR_decode, dock, &ClientWidget::beacon_spot_added);
|
||||
connect (server_, &MessageServer::clear_decodes, dock, &ClientWidget::clear_decodes);
|
||||
connect (dock, &ClientWidget::do_reply, decodes_model_, &DecodesModel::do_reply);
|
||||
connect (dock, &ClientWidget::do_halt_tx, server_, &MessageServer::halt_tx);
|
||||
connect (dock, &ClientWidget::do_free_text, server_, &MessageServer::free_text);
|
||||
connect (dock, &ClientWidget::location, server_, &MessageServer::location);
|
||||
connect (view_action, &QAction::toggled, dock, &ClientWidget::setVisible);
|
||||
connect (dock, &ClientWidget::highlight_callsign, server_, &MessageServer::highlight_callsign);
|
||||
dock_widgets_[id] = dock;
|
||||
server_->replay (id); // request decodes and status
|
||||
}
|
||||
|
||||
void MessageAggregatorMainWindow::remove_client (QString const& id)
|
||||
{
|
||||
auto iter = dock_widgets_.find (id);
|
||||
if (iter != std::end (dock_widgets_))
|
||||
{
|
||||
(*iter)->close ();
|
||||
dock_widgets_.erase (iter);
|
||||
}
|
||||
}
|
||||
|
||||
MessageAggregatorMainWindow::~MessageAggregatorMainWindow ()
|
||||
{
|
||||
for (auto client : dock_widgets_)
|
||||
{
|
||||
delete client;
|
||||
}
|
||||
}
|
||||
|
||||
void MessageAggregatorMainWindow::change_highlighting (QString const& call, QColor const& bg, QColor const& fg
|
||||
, bool last_only)
|
||||
{
|
||||
for (auto id : dock_widgets_.keys ())
|
||||
{
|
||||
server_->highlight_callsign (id, call, bg, fg, last_only);
|
||||
}
|
||||
}
|
||||
|
||||
#include "moc_MessageAggregatorMainWindow.cpp"
|
||||
Reference in New Issue
Block a user