js8call/mainwindow.cpp
Jordan Sherer 9f2e87c076 Removed selcall and active flag
In effort to simplify the behavior of automatic responses as well as make the software easier to use, I have removed the SELCALL button and the ACTIVE flag. Now, the response to STATUS is one that contains actual status (AUTO ON/OFF, VERSION NUMBER, etc). HBs used this in their transmissions, but it was never really accurate because it relied on the user to toggle the switch. Hazardous really. So, I approached this by simplifying the behavior. If AUTO is on, you will reply to direct queries. If AUTO is off, you wont. Simple. If HB is on, you will heartbeat. If it is off, you wont. Simple. If both AUTO and HB is on, you will automatically reply to heartbeats with ACKs. If not, you wont. Simple. You can remove yourself from the ALLCALL group. This is the same behavior as the previous SELCALL function and now that we have simplified it I can build an actual SELCALL function (to selectively allow stations to call you) instead of a 1/2 SELCALL that it used to be. Bingo.
2018-12-26 14:05:44 -05:00

11104 lines
370 KiB
C++

//---------------------------------------------------------- MainWindow
#include "mainwindow.h"
#include <cmath>
#include <cinttypes>
#include <limits>
#include <functional>
#include <fstream>
#include <iterator>
#include <fftw3.h>
#include <QLineEdit>
#include <QRegExpValidator>
#include <QRegExp>
#include <QRegularExpression>
#include <QDesktopServices>
#include <QUrl>
#include <QStandardPaths>
#include <QDir>
#include <QDebug>
#include <QtConcurrent/QtConcurrentRun>
#include <QProgressDialog>
#include <QHostInfo>
#include <QVector>
#include <QCursor>
#include <QToolTip>
#include <QAction>
#include <QActionGroup>
#include <QSplashScreen>
#include <QSound>
#include "APRSISClient.h"
#include "revision_utils.hpp"
#include "qt_helpers.hpp"
#include "NetworkAccessManager.hpp"
#include "soundout.h"
#include "soundin.h"
#include "Modulator.hpp"
#include "Detector.hpp"
#include "plotter.h"
#include "echoplot.h"
#include "echograph.h"
#include "fastplot.h"
#include "fastgraph.h"
#include "about.h"
#include "messageaveraging.h"
#include "widegraph.h"
#include "sleep.h"
#include "logqso.h"
#include "decodedtext.h"
#include "Radio.hpp"
#include "Bands.hpp"
#include "TransceiverFactory.hpp"
#include "StationList.hpp"
#include "LiveFrequencyValidator.hpp"
#include "MessageClient.hpp"
#include "wsprnet.h"
#include "signalmeter.h"
#include "HelpTextWindow.hpp"
#include "Audio/BWFFile.hpp"
#include "MultiSettings.hpp"
#include "MaidenheadLocatorValidator.hpp"
#include "CallsignValidator.hpp"
#include "EqualizationToolsDialog.hpp"
#include "SelfDestructMessageBox.h"
#include "messagereplydialog.h"
#include "DriftingDateTime.h"
#include "jsc.h"
#include "ui_mainwindow.h"
#include "moc_mainwindow.cpp"
#define STATE_RX 1
#define STATE_TX 2
extern "C" {
//----------------------------------------------------- C and Fortran routines
void symspec_(struct dec_data *, int* k, int* ntrperiod, int* nsps, int* ingain,
int* minw, float* px, float s[], float* df3, int* nhsym, int* npts8,
float *m_pxmax);
void hspec_(short int d2[], int* k, int* nutc0, int* ntrperiod, int* nrxfreq, int* ntol,
bool* bmsk144, bool* bcontest, bool* btrain, double const pcoeffs[], int* ingain,
char mycall[], char hiscall[], bool* bshmsg, bool* bswl, char ddir[], float green[],
float s[], int* jh, float *pxmax, float *rmsNoGain, char line[], char mygrid[],
fortran_charlen_t, fortran_charlen_t, fortran_charlen_t, fortran_charlen_t,
fortran_charlen_t);
// float s[], int* jh, char line[], char mygrid[],
void genft8_(char* msg, char* MyGrid, bool* bcontest, int* i3bit, char* msgsent,
char ft8msgbits[], int itone[], fortran_charlen_t, fortran_charlen_t,
fortran_charlen_t);
void gen4_(char* msg, int* ichk, char* msgsent, int itone[],
int* itext, fortran_charlen_t, fortran_charlen_t);
void gen9_(char* msg, int* ichk, char* msgsent, int itone[],
int* itext, fortran_charlen_t, fortran_charlen_t);
void genmsk144_(char* msg, char* MyGrid, int* ichk, bool* bcontest,
char* msgsent, int itone[], int* itext, fortran_charlen_t,
fortran_charlen_t, fortran_charlen_t);
void gen65_(char* msg, int* ichk, char* msgsent, int itone[],
int* itext, fortran_charlen_t, fortran_charlen_t);
void genqra64_(char* msg, int* ichk, char* msgsent, int itone[],
int* itext, fortran_charlen_t, fortran_charlen_t);
void genwspr_(char* msg, char* msgsent, int itone[], fortran_charlen_t, fortran_charlen_t);
void genwspr_fsk8_(char* msg, char* msgsent, int itone[], fortran_charlen_t, fortran_charlen_t);
void geniscat_(char* msg, char* msgsent, int itone[], fortran_charlen_t, fortran_charlen_t);
void azdist_(char* MyGrid, char* HisGrid, double* utch, int* nAz, int* nEl,
int* nDmiles, int* nDkm, int* nHotAz, int* nHotABetter,
fortran_charlen_t, fortran_charlen_t);
void morse_(char* msg, int* icw, int* ncw, fortran_charlen_t);
int ptt_(int nport, int ntx, int* iptt, int* nopen);
void wspr_downsample_(short int d2[], int* k);
int savec2_(char* fname, int* TR_seconds, double* dial_freq, fortran_charlen_t);
void avecho_( short id2[], int* dop, int* nfrit, int* nqual, float* f1,
float* level, float* sigdb, float* snr, float* dfreq,
float* width);
void fast_decode_(short id2[], int narg[], int* ntrperiod,
char msg[], char mycall[], char hiscall[],
fortran_charlen_t, fortran_charlen_t, fortran_charlen_t);
void degrade_snr_(short d2[], int* n, float* db, float* bandwidth);
void wav12_(short d2[], short d1[], int* nbytes, short* nbitsam2);
void refspectrum_(short int d2[], bool* bclearrefspec,
bool* brefspec, bool* buseref, const char* c_fname, fortran_charlen_t);
void freqcal_(short d2[], int* k, int* nkhz,int* noffset, int* ntol,
char line[], fortran_charlen_t);
void fix_contest_msg_(char* MyGrid, char* msg, fortran_charlen_t, fortran_charlen_t);
void calibrate_(char data_dir[], int* iz, double* a, double* b, double* rms,
double* sigmaa, double* sigmab, int* irc, fortran_charlen_t);
void foxgen_();
void plotsave_(float swide[], int* m_w , int* m_h1, int* irow);
}
#ifndef TEST_FOX_WAVE_GEN
#define TEST_FOX_WAVE_GEN 0
#endif
#ifndef TEST_FOX_WAVE_GEN_SLOTS
#if TEST_FOX_WAVE_GEN
#define TEST_FOX_WAVE_GEN_SLOTS 2
#else
#define TEST_FOX_WAVE_GEN_SLOTS 1
#endif
#endif
const int NEAR_THRESHOLD_RX = 10;
int volatile itone[NUM_ISCAT_SYMBOLS]; //Audio tones for all Tx symbols
int volatile icw[NUM_CW_SYMBOLS]; //Dits for CW ID
struct dec_data dec_data; // for sharing with Fortran
int outBufSize;
int rc;
qint32 g_iptt {0};
wchar_t buffer[256];
float fast_green[703];
float fast_green2[703];
float fast_s[44992]; //44992=64*703
float fast_s2[44992];
int fast_jh {0};
int fast_jhpeak {0};
int fast_jh2 {0};
int narg[15];
QVector<QColor> g_ColorTbl;
namespace
{
Radio::Frequency constexpr default_frequency {14078000};
QRegExp message_alphabet {"[^\\x00-\\x1F]*"}; // base alphabet supported by JS8CALL
// grid exact match excluding RR73
QRegularExpression grid_regexp {"\\A(?![Rr]{2}73)[A-Ra-r]{2}[0-9]{2}([A-Xa-x]{2}){0,1}\\z"};
bool message_is_73 (int type, QStringList const& msg_parts)
{
return type >= 0
&& (((type < 6 || 7 == type)
&& (msg_parts.contains ("73") || msg_parts.contains ("RR73")))
|| (type == 6 && !msg_parts.filter ("73").isEmpty ()));
}
int ms_minute_error ()
{
auto const& now = DriftingDateTime::currentDateTime ();
auto const& time = now.time ();
auto second = time.second ();
return now.msecsTo (now.addSecs (second > 30 ? 60 - second : -second)) - time.msec ();
}
QString since(QDateTime time){
int delta = time.toUTC().secsTo(DriftingDateTime::currentDateTimeUtc());
if(delta < 15){
return QString("now");
}
int seconds = delta % 60;
delta = delta / 60;
int minutes = delta % 60;
delta = delta / 60;
int hours = delta % 24;
delta = delta / 24;
int days = delta;
if(days){
return QString("%1d").arg(days);
}
if(hours){
return QString("%1h").arg(hours);
}
if(minutes){
return QString("%1m").arg(minutes);
}
if(seconds){
return QString("%1s").arg(seconds - seconds%15);
}
return QString {};
}
void clearTableWidget(QTableWidget *widget){
if(!widget){
return;
}
for(int i = widget->rowCount(); i >= 0; i--){
widget->removeRow(i);
}
}
#if 0
int round(int numToRound, int multiple)
{
if(multiple == 0)
{
return numToRound;
}
int roundDown = ( (int) (numToRound) / multiple) * multiple;
if(numToRound - roundDown > multiple/2){
return roundDown + multiple;
}
return roundDown;
}
#endif
int roundUp(int numToRound, int multiple)
{
if(multiple == 0)
{
return numToRound;
}
int roundDown = ( (int) (numToRound) / multiple) * multiple;
return roundDown + multiple;
}
void setTextEditFont(QTextEdit *edit, QFont font){
edit->setFont(font);
edit->setFontFamily(font.family());
edit->setFontItalic(font.italic());
edit->setFontPointSize(font.pointSize());
edit->setFontUnderline(font.underline());
edit->setFontWeight(font.weight());
auto d = edit->document();
d->setDefaultFont(font);
edit->setDocument(d);
auto c = edit->textCursor();
c.select(QTextCursor::Document);
auto cf = c.blockCharFormat();
cf.setFont(font);
c.mergeBlockCharFormat(cf);
edit->updateGeometry();
}
void setTextEditStyle(QTextEdit *edit, QColor fg, QColor bg, QFont font){
edit->setStyleSheet(QString("QTextEdit { color:%1; background: %2; %3; }").arg(fg.name()).arg(bg.name()).arg(font_as_stylesheet(font)));
QTimer::singleShot(10, nullptr, [edit, fg, bg](){
QPalette p = edit->palette();
p.setColor(QPalette::Base, bg);
p.setColor(QPalette::Active, QPalette::Base, bg);
p.setColor(QPalette::Disabled, QPalette::Base, bg);
p.setColor(QPalette::Inactive, QPalette::Base, bg);
p.setColor(QPalette::Text, fg);
p.setColor(QPalette::Active, QPalette::Text, fg);
p.setColor(QPalette::Disabled, QPalette::Text, fg);
p.setColor(QPalette::Inactive, QPalette::Text, fg);
edit->setBackgroundRole(QPalette::Base);
edit->setForegroundRole(QPalette::Text);
edit->setPalette(p);
edit->updateGeometry();
edit->update();
});
}
/*
void setTextEditForeground(QTextEdit *edit, QColor color){
QTimer::singleShot(20, nullptr, [edit, color](){
QPalette p = edit->palette();
p.setColor(QPalette::Text, color);
p.setColor(QPalette::Active, QPalette::Text, color);
p.setColor(QPalette::Disabled, QPalette::Text, color);
p.setColor(QPalette::Inactive, QPalette::Text, color);
edit->setPalette(p);
edit->updateGeometry();
edit->update();
});
}
*/
void highlightBlock(QTextBlock block, QFont font, QColor foreground, QColor background){
QTextCursor cursor(block);
// Set background color
QTextBlockFormat blockFormat = cursor.blockFormat();
blockFormat.setBackground(background);
cursor.setBlockFormat(blockFormat);
// Set font
for (QTextBlock::iterator it = cursor.block().begin(); !(it.atEnd()); ++it) {
QTextCharFormat charFormat = it.fragment().charFormat();
charFormat.setFont(font);
charFormat.setForeground(QBrush(foreground));
QTextCursor tempCursor = cursor;
tempCursor.setPosition(it.fragment().position());
tempCursor.setPosition(it.fragment().position() + it.fragment().length(), QTextCursor::KeepAnchor);
tempCursor.setCharFormat(charFormat);
}
}
template<typename T>
QList<T> listCopyReverse(QList<T> const &list){
QList<T> newList = QList<T>();
auto iter = list.end();
while(iter != list.begin()){
newList.append(*(--iter));
}
return newList;
}
}
//--------------------------------------------------- MainWindow constructor
MainWindow::MainWindow(QDir const& temp_directory, bool multiple,
MultiSettings * multi_settings, QSharedMemory *shdmem,
unsigned downSampleFactor,
QSplashScreen * splash, QWidget *parent) :
QMainWindow(parent),
m_network_manager {this},
m_valid {true},
m_splash {splash},
m_revision {revision ()},
m_multiple {multiple},
m_multi_settings {multi_settings},
m_configurations_button {0},
m_settings {multi_settings->settings ()},
ui(new Ui::MainWindow),
m_config {temp_directory, m_settings, this},
m_WSPR_band_hopping {m_settings, &m_config, this},
m_WSPR_tx_next {false},
m_rigErrorMessageBox {MessageBox::Critical, tr ("Rig Control Error")
, MessageBox::Cancel | MessageBox::Ok | MessageBox::Retry},
m_wideGraph (new WideGraph(m_settings)),
m_echoGraph (new EchoGraph(m_settings)),
m_fastGraph (new FastGraph(m_settings)),
// no parent so that it has a taskbar icon
m_logDlg (new LogQSO (program_title (), m_settings, &m_config, nullptr)),
m_lastDialFreq {0},
m_dialFreqRxWSPR {0},
m_detector {new Detector {RX_SAMPLE_RATE, NTMAX, downSampleFactor}},
m_FFTSize {6192 / 2}, // conservative value to avoid buffer overruns
m_soundInput {new SoundInput},
m_modulator {new Modulator {TX_SAMPLE_RATE, NTMAX}},
m_soundOutput {new SoundOutput},
m_msErase {0},
m_secBandChanged {0},
m_freqNominal {0},
m_freqTxNominal {0},
m_s6 {0.},
m_tRemaining {0.},
m_DTtol {3.0},
m_waterfallAvg {1},
m_ntx {1},
m_gen_message_is_cq {false},
m_send_RR73 {false},
m_XIT {0},
m_sec0 {-1},
m_RxLog {1}, //Write Date and Time to RxLog
m_nutc0 {999999},
m_ntr {0},
m_tx {0},
m_TRperiod {60},
m_inGain {0},
m_secID {0},
m_idleMinutes {0},
m_nSubMode {0},
m_nclearave {1},
m_pctx {0},
m_nseq {0},
m_nWSPRdecodes {0},
m_k0 {9999999},
m_nPick {0},
m_frequency_list_fcal_iter {m_config.frequencies ()->begin ()},
m_nTx73 {0},
m_btxok {false},
m_diskData {false},
m_loopall {false},
m_auto {false},
m_restart {false},
m_startAnother {false},
m_saveDecoded {false},
m_saveAll {false},
m_widebandDecode {false},
m_dataAvailable {false},
m_blankLine {false},
m_decodedText2 {false},
m_freeText {false},
m_sentFirst73 {false},
m_currentMessageType {-1},
m_lastMessageType {-1},
m_bShMsgs {false},
m_bSWL {false},
m_uploading {false},
m_txNext {false},
m_grid6 {false},
m_tuneup {false},
m_bTxTime {false},
m_rxDone {false},
m_bSimplex {false},
m_bEchoTxOK {false},
m_bTransmittedEcho {false},
m_bEchoTxed {false},
m_bFastDecodeCalled {false},
m_bDoubleClickAfterCQnnn {false},
m_bRefSpec {false},
m_bClearRefSpec {false},
m_bTrain {false},
m_bAutoReply {false},
m_QSOProgress {CALLING},
m_ihsym {0},
m_nzap {0},
m_px {0.0},
m_iptt0 {0},
m_btxok0 {false},
m_nsendingsh {0},
m_onAirFreq0 {0.0},
m_first_error {true},
tx_status_label {"Receiving"},
wsprNet {new WSPRNet {&m_network_manager, this}},
m_appDir {QApplication::applicationDirPath ()},
m_palette {"Linrad"},
m_mode {"FT8"},
m_rpt {"-15"},
m_pfx {
"1A", "1S",
"3A", "3B6", "3B8", "3B9", "3C", "3C0", "3D2", "3D2C",
"3D2R", "3DA", "3V", "3W", "3X", "3Y", "3YB", "3YP",
"4J", "4L", "4S", "4U1I", "4U1U", "4W", "4X",
"5A", "5B", "5H", "5N", "5R", "5T", "5U", "5V", "5W", "5X", "5Z",
"6W", "6Y",
"7O", "7P", "7Q", "7X",
"8P", "8Q", "8R",
"9A", "9G", "9H", "9J", "9K", "9L", "9M2", "9M6", "9N",
"9Q", "9U", "9V", "9X", "9Y",
"A2", "A3", "A4", "A5", "A6", "A7", "A9", "AP",
"BS7", "BV", "BV9", "BY",
"C2", "C3", "C5", "C6", "C9", "CE", "CE0X", "CE0Y",
"CE0Z", "CE9", "CM", "CN", "CP", "CT", "CT3", "CU",
"CX", "CY0", "CY9",
"D2", "D4", "D6", "DL", "DU",
"E3", "E4", "E5", "EA", "EA6", "EA8", "EA9", "EI", "EK",
"EL", "EP", "ER", "ES", "ET", "EU", "EX", "EY", "EZ",
"F", "FG", "FH", "FJ", "FK", "FKC", "FM", "FO", "FOA",
"FOC", "FOM", "FP", "FR", "FRG", "FRJ", "FRT", "FT5W",
"FT5X", "FT5Z", "FW", "FY",
"M", "MD", "MI", "MJ", "MM", "MU", "MW",
"H4", "H40", "HA", "HB", "HB0", "HC", "HC8", "HH",
"HI", "HK", "HK0", "HK0M", "HL", "HM", "HP", "HR",
"HS", "HV", "HZ",
"I", "IS", "IS0",
"J2", "J3", "J5", "J6", "J7", "J8", "JA", "JDM",
"JDO", "JT", "JW", "JX", "JY",
"K", "KC4", "KG4", "KH0", "KH1", "KH2", "KH3", "KH4", "KH5",
"KH5K", "KH6", "KH7", "KH8", "KH9", "KL", "KP1", "KP2",
"KP4", "KP5",
"LA", "LU", "LX", "LY", "LZ",
"OA", "OD", "OE", "OH", "OH0", "OJ0", "OK", "OM", "ON",
"OX", "OY", "OZ",
"P2", "P4", "PA", "PJ2", "PJ7", "PY", "PY0F", "PT0S", "PY0T", "PZ",
"R1F", "R1M",
"S0", "S2", "S5", "S7", "S9", "SM", "SP", "ST", "SU",
"SV", "SVA", "SV5", "SV9",
"T2", "T30", "T31", "T32", "T33", "T5", "T7", "T8", "T9", "TA",
"TF", "TG", "TI", "TI9", "TJ", "TK", "TL", "TN", "TR", "TT",
"TU", "TY", "TZ",
"UA", "UA2", "UA9", "UK", "UN", "UR",
"V2", "V3", "V4", "V5", "V6", "V7", "V8", "VE", "VK", "VK0H",
"VK0M", "VK9C", "VK9L", "VK9M", "VK9N", "VK9W", "VK9X", "VP2E",
"VP2M", "VP2V", "VP5", "VP6", "VP6D", "VP8", "VP8G", "VP8H",
"VP8O", "VP8S", "VP9", "VQ9", "VR", "VU", "VU4", "VU7",
"XE", "XF4", "XT", "XU", "XW", "XX9", "XZ",
"YA", "YB", "YI", "YJ", "YK", "YL", "YN", "YO", "YS", "YU", "YV", "YV0",
"Z2", "Z3", "ZA", "ZB", "ZC4", "ZD7", "ZD8", "ZD9", "ZF", "ZK1N",
"ZK1S", "ZK2", "ZK3", "ZL", "ZL7", "ZL8", "ZL9", "ZP", "ZS", "ZS8"
},
m_sfx {"P", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A"},
mem_js8 {shdmem},
m_msAudioOutputBuffered (0u),
m_framesAudioInputBuffered (RX_SAMPLE_RATE / 10),
m_downSampleFactor (downSampleFactor),
m_audioThreadPriority (QThread::HighPriority),
m_bandEdited {false},
m_splitMode {false},
m_monitoring {false},
m_tx_when_ready {false},
m_transmitting {false},
m_tune {false},
m_tx_watchdog {false},
m_block_pwr_tooltip {false},
m_PwrBandSetOK {true},
m_lastMonitoredFrequency {default_frequency},
m_toneSpacing {0.},
m_firstDecode {0},
m_optimizingProgress {"Optimizing decoder FFTs for your CPU.\n"
"Please be patient,\n"
"this may take a few minutes", QString {}, 0, 1, this},
m_messageClient {new MessageClient {QApplication::applicationName (),
version (), revision (),
m_config.udp_server_name (), m_config.udp_server_port (),
this}},
m_aprsClient {new APRSISClient{"rotate.aprs2.net", 14580, this}},
psk_Reporter {new PSK_Reporter {m_messageClient, this}},
m_i3bit {0},
m_manual {&m_network_manager},
m_txFrameCount {0},
m_txTextDirty {false},
m_txFrameCountEstimate {0},
m_previousFreq {0},
m_hbHidden { false },
m_hbInterval {0},
m_cqInterval {0}
{
ui->setupUi(this);
createStatusBar();
add_child_to_event_filter (this);
ui->dxGridEntry->setValidator (new MaidenheadLocatorValidator {this});
ui->dxCallEntry->setValidator (new CallsignValidator {this});
ui->sbTR->values ({5, 10, 15, 30});
m_baseCall = Radio::base_callsign (m_config.my_callsign ());
m_opCall = m_config.opCall();
m_optimizingProgress.setWindowModality (Qt::WindowModal);
m_optimizingProgress.setAutoReset (false);
m_optimizingProgress.setMinimumDuration (15000); // only show after 15s delay
// Closedown.
connect (ui->actionExit, &QAction::triggered, this, &QMainWindow::close);
// parts of the rig error message box that are fixed
m_rigErrorMessageBox.setInformativeText (tr ("Do you want to reconfigure the radio interface?"));
m_rigErrorMessageBox.setDefaultButton (MessageBox::Ok);
// start audio thread and hook up slots & signals for shutdown management
// these objects need to be in the audio thread so that invoking
// their slots is done in a thread safe way
m_soundOutput->moveToThread (&m_audioThread);
m_modulator->moveToThread (&m_audioThread);
m_soundInput->moveToThread (&m_audioThread);
m_detector->moveToThread (&m_audioThread);
// hook up sound output stream slots & signals and disposal
connect (this, &MainWindow::initializeAudioOutputStream, m_soundOutput, &SoundOutput::setFormat);
connect (m_soundOutput, &SoundOutput::error, this, &MainWindow::showSoundOutError);
// connect (m_soundOutput, &SoundOutput::status, this, &MainWindow::showStatusMessage);
connect (this, &MainWindow::outAttenuationChanged, m_soundOutput, &SoundOutput::setAttenuation);
connect (&m_audioThread, &QThread::finished, m_soundOutput, &QObject::deleteLater);
// hook up Modulator slots and disposal
connect (this, &MainWindow::transmitFrequency, m_modulator, &Modulator::setFrequency);
connect (this, &MainWindow::endTransmitMessage, m_modulator, &Modulator::stop);
connect (this, &MainWindow::tune, m_modulator, &Modulator::tune);
connect (this, &MainWindow::sendMessage, m_modulator, &Modulator::start);
connect (&m_audioThread, &QThread::finished, m_modulator, &QObject::deleteLater);
// hook up the audio input stream signals, slots and disposal
connect (this, &MainWindow::startAudioInputStream, m_soundInput, &SoundInput::start);
connect (this, &MainWindow::suspendAudioInputStream, m_soundInput, &SoundInput::suspend);
connect (this, &MainWindow::resumeAudioInputStream, m_soundInput, &SoundInput::resume);
connect (this, &MainWindow::finished, m_soundInput, &SoundInput::stop);
connect(m_soundInput, &SoundInput::error, this, &MainWindow::showSoundInError);
// connect(m_soundInput, &SoundInput::status, this, &MainWindow::showStatusMessage);
connect (&m_audioThread, &QThread::finished, m_soundInput, &QObject::deleteLater);
connect (this, &MainWindow::finished, this, &MainWindow::close);
// hook up the detector signals, slots and disposal
connect (this, &MainWindow::FFTSize, m_detector, &Detector::setBlockSize);
connect(m_detector, &Detector::framesWritten, this, &MainWindow::dataSink);
connect (&m_audioThread, &QThread::finished, m_detector, &QObject::deleteLater);
// setup the waterfall
connect(m_wideGraph.data (), SIGNAL(freezeDecode2(int)),this,SLOT(freezeDecode(int)));
connect(m_wideGraph.data (), SIGNAL(f11f12(int)),this,SLOT(bumpFqso(int)));
connect(m_wideGraph.data (), SIGNAL(setXIT2(int)),this,SLOT(setXIT(int)));
connect (m_fastGraph.data (), &FastGraph::fastPick, this, &MainWindow::fastPick);
connect (this, &MainWindow::finished, m_wideGraph.data (), &WideGraph::close);
connect (this, &MainWindow::finished, m_echoGraph.data (), &EchoGraph::close);
connect (this, &MainWindow::finished, m_fastGraph.data (), &FastGraph::close);
// setup the log QSO dialog
connect (m_logDlg.data (), &LogQSO::acceptQSO, this, &MainWindow::acceptQSO);
connect (this, &MainWindow::finished, m_logDlg.data (), &LogQSO::close);
// Network message handlers
connect (m_messageClient, &MessageClient::error, this, &MainWindow::networkError);
connect (m_messageClient, &MessageClient::message, this, &MainWindow::networkMessage);
#if 0
// Hook up WSPR band hopping
connect (ui->band_hopping_schedule_push_button, &QPushButton::clicked
, &m_WSPR_band_hopping, &WSPRBandHopping::show_dialog);
connect (ui->sbTxPercent, static_cast<void (QSpinBox::*) (int)> (&QSpinBox::valueChanged)
, &m_WSPR_band_hopping, &WSPRBandHopping::set_tx_percent);
#endif
on_EraseButton_clicked ();
QActionGroup* modeGroup = new QActionGroup(this);
ui->actionFT8->setActionGroup(modeGroup);
ui->actionJT9->setActionGroup(modeGroup);
ui->actionJT65->setActionGroup(modeGroup);
ui->actionJT9_JT65->setActionGroup(modeGroup);
ui->actionJT4->setActionGroup(modeGroup);
ui->actionWSPR->setActionGroup(modeGroup);
ui->actionWSPR_LF->setActionGroup(modeGroup);
ui->actionEcho->setActionGroup(modeGroup);
ui->actionISCAT->setActionGroup(modeGroup);
ui->actionMSK144->setActionGroup(modeGroup);
ui->actionQRA64->setActionGroup(modeGroup);
ui->actionFreqCal->setActionGroup(modeGroup);
QActionGroup* saveGroup = new QActionGroup(this);
ui->actionNone->setActionGroup(saveGroup);
ui->actionSave_decoded->setActionGroup(saveGroup);
ui->actionSave_all->setActionGroup(saveGroup);
QActionGroup* DepthGroup = new QActionGroup(this);
ui->actionQuickDecode->setActionGroup(DepthGroup);
ui->actionMediumDecode->setActionGroup(DepthGroup);
ui->actionDeepDecode->setActionGroup(DepthGroup);
ui->actionDeepestDecode->setActionGroup(DepthGroup);
connect (ui->view_phase_response_action, &QAction::triggered, [this] () {
if (!m_equalizationToolsDialog)
{
m_equalizationToolsDialog.reset (new EqualizationToolsDialog {m_settings, m_config.writeable_data_dir (), m_phaseEqCoefficients, this});
connect (m_equalizationToolsDialog.data (), &EqualizationToolsDialog::phase_equalization_changed,
[this] (QVector<double> const& coeffs) {
m_phaseEqCoefficients = coeffs;
});
}
m_equalizationToolsDialog->show ();
});
QButtonGroup* txMsgButtonGroup = new QButtonGroup {this};
txMsgButtonGroup->addButton(ui->txrb1,1);
txMsgButtonGroup->addButton(ui->txrb2,2);
txMsgButtonGroup->addButton(ui->txrb3,3);
txMsgButtonGroup->addButton(ui->txrb4,4);
txMsgButtonGroup->addButton(ui->txrb5,5);
txMsgButtonGroup->addButton(ui->txrb6,6);
set_dateTimeQSO(-1);
connect(txMsgButtonGroup,SIGNAL(buttonClicked(int)),SLOT(set_ntx(int)));
// initialize decoded text font and hook up font change signals
// defer initialization until after construction otherwise menu
// fonts do not get set
QTimer::singleShot (0, this, SLOT (initialize_fonts ()));
connect (&m_config, &Configuration::gui_text_font_changed, [this] (QFont const& font) {
set_application_font (font);
});
connect (&m_config, &Configuration::table_font_changed, [this] (QFont const&) {
ui->tableWidgetRXAll->setFont(m_config.table_font());
ui->tableWidgetCalls->setFont(m_config.table_font());
});
connect (&m_config, &Configuration::rx_text_font_changed, [this] (QFont const&) {
setTextEditFont(ui->textEditRX, m_config.rx_text_font());
});
connect (&m_config, &Configuration::compose_text_font_changed, [this] (QFont const&) {
setTextEditFont(ui->extFreeTextMsgEdit, m_config.compose_text_font());
});
connect (&m_config, &Configuration::colors_changed, [this](){
setTextEditStyle(ui->textEditRX, m_config.color_rx_foreground(), m_config.color_rx_background(), m_config.rx_text_font());
setTextEditStyle(ui->extFreeTextMsgEdit, m_config.color_compose_foreground(), m_config.color_compose_background(), m_config.compose_text_font());
// rehighlight
auto d = ui->textEditRX->document();
if(d){
for(int i = 0; i < d->lineCount(); i++){
auto b = d->findBlockByLineNumber(i);
switch(b.userState()){
case STATE_RX:
highlightBlock(b, m_config.rx_text_font(), m_config.color_rx_foreground(), QColor(Qt::transparent));
break;
case STATE_TX:
highlightBlock(b, m_config.tx_text_font(), m_config.color_tx_foreground(), QColor(Qt::transparent));
break;
}
}
}
});
setWindowTitle (program_title ());
connect(&proc_js8, &QProcess::readyReadStandardOutput, this, &MainWindow::readFromStdout);
connect(&proc_js8, static_cast<void (QProcess::*) (QProcess::ProcessError)> (&QProcess::error),
[this] (QProcess::ProcessError error) {
subProcessError (&proc_js8, error);
});
connect(&proc_js8, static_cast<void (QProcess::*) (int, QProcess::ExitStatus)> (&QProcess::finished),
[this] (int exitCode, QProcess::ExitStatus status) {
subProcessFailed (&proc_js8, exitCode, status);
});
connect(&p1, &QProcess::readyReadStandardOutput, this, &MainWindow::p1ReadFromStdout);
connect(&p1, static_cast<void (QProcess::*) (QProcess::ProcessError)> (&QProcess::error),
[this] (QProcess::ProcessError error) {
subProcessError (&p1, error);
});
connect(&p1, static_cast<void (QProcess::*) (int, QProcess::ExitStatus)> (&QProcess::finished),
[this] (int exitCode, QProcess::ExitStatus status) {
subProcessFailed (&p1, exitCode, status);
});
connect(&p3, static_cast<void (QProcess::*) (QProcess::ProcessError)> (&QProcess::error),
[this] (QProcess::ProcessError error) {
subProcessError (&p3, error);
});
connect(&p3, static_cast<void (QProcess::*) (int, QProcess::ExitStatus)> (&QProcess::finished),
[this] (int exitCode, QProcess::ExitStatus status) {
subProcessFailed (&p3, exitCode, status);
});
// hook up save WAV file exit handling
connect (&m_saveWAVWatcher, &QFutureWatcher<QString>::finished, [this] {
// extract the promise from the future
auto const& result = m_saveWAVWatcher.future ().result ();
if (!result.isEmpty ()) // error
{
MessageBox::critical_message (this, tr("Error Writing WAV File"), result);
}
});
// Hook up working frequencies.
ui->bandComboBox->setModel (m_config.frequencies ());
ui->bandComboBox->setModelColumn (FrequencyList_v2::frequency_mhz_column);
// combo box drop down width defaults to the line edit + decorator width,
// here we change that to the column width size hint of the model column
ui->bandComboBox->view ()->setMinimumWidth (ui->bandComboBox->view ()->sizeHintForColumn (FrequencyList_v2::frequency_mhz_column));
// Enable live band combo box entry validation and action.
auto band_validator = new LiveFrequencyValidator {ui->bandComboBox
, m_config.bands ()
, m_config.frequencies ()
, &m_freqNominal
, this};
ui->bandComboBox->setValidator (band_validator);
// Hook up signals.
connect (band_validator, &LiveFrequencyValidator::valid, this, &MainWindow::band_changed);
connect (ui->bandComboBox->lineEdit (), &QLineEdit::textEdited, [this] (QString const&) {m_bandEdited = true;});
// hook up configuration signals
connect (&m_config, &Configuration::transceiver_update, this, &MainWindow::handle_transceiver_update);
connect (&m_config, &Configuration::transceiver_failure, this, &MainWindow::handle_transceiver_failure);
connect (&m_config, &Configuration::udp_server_changed, m_messageClient, &MessageClient::set_server);
connect (&m_config, &Configuration::udp_server_port_changed, m_messageClient, &MessageClient::set_server_port);
connect (&m_config, &Configuration::band_schedule_changed, this, [this](){
this->m_bandHopped = true;
});
// set up configurations menu
connect (m_multi_settings, &MultiSettings::configurationNameChanged, [this] (QString const& name) {
if ("Default" != name) {
config_label.setText (name);
config_label.show ();
}
else {
config_label.hide ();
}
});
m_multi_settings->create_menu_actions (this, ui->menuConfig);
m_configurations_button = m_rigErrorMessageBox.addButton (tr ("Configurations...")
, QMessageBox::ActionRole);
// set up message text validators
ui->tx1->setValidator (new QRegExpValidator {message_alphabet, this});
ui->tx2->setValidator (new QRegExpValidator {message_alphabet, this});
ui->tx3->setValidator (new QRegExpValidator {message_alphabet, this});
ui->tx4->setValidator (new QRegExpValidator {message_alphabet, this});
ui->tx5->setValidator (new QRegExpValidator {message_alphabet, this});
ui->tx6->setValidator (new QRegExpValidator {message_alphabet, this});
ui->freeTextMsg->setValidator (new QRegExpValidator {message_alphabet, this});
ui->nextFreeTextMsg->setValidator (new QRegExpValidator {message_alphabet, this});
//ui->extFreeTextMsg->setValidator (new QRegExpValidator {message_alphabet, this});
// Free text macros model to widget hook up.
//ui->tx5->setModel (m_config.macros ());
//connect (ui->tx5->lineEdit(), &QLineEdit::editingFinished,
// [this] () {on_tx5_currentTextChanged (ui->tx5->lineEdit()->text());});
//ui->freeTextMsg->setModel (m_config.macros ());
connect (ui->freeTextMsg->lineEdit ()
, &QLineEdit::editingFinished
, [this] () {on_freeTextMsg_currentTextChanged (ui->freeTextMsg->lineEdit ()->text ());});
connect (ui->nextFreeTextMsg
, &QLineEdit::editingFinished
, [this] () {on_nextFreeTextMsg_currentTextChanged (ui->nextFreeTextMsg->text ());});
connect (ui->extFreeTextMsgEdit
, &QTextEdit::textChanged
, [this] () {on_extFreeTextMsgEdit_currentTextChanged (ui->extFreeTextMsgEdit->toPlainText ());});
m_guiTimer.setSingleShot(true);
connect(&m_guiTimer, &QTimer::timeout, this, &MainWindow::guiUpdate);
m_guiTimer.start(100); //### Don't change the 100 ms! ###
ptt0Timer.setSingleShot(true);
connect(&ptt0Timer, &QTimer::timeout, this, &MainWindow::stopTx2);
ptt1Timer.setSingleShot(true);
connect(&ptt1Timer, &QTimer::timeout, this, &MainWindow::startTx2);
p1Timer.setSingleShot(true);
connect(&p1Timer, &QTimer::timeout, this, &MainWindow::startP1);
logQSOTimer.setSingleShot(true);
connect(&logQSOTimer, &QTimer::timeout, this, &MainWindow::on_logQSOButton_clicked);
tuneButtonTimer.setSingleShot(true);
connect(&tuneButtonTimer, &QTimer::timeout, this, &MainWindow::on_stopTxButton_clicked);
tuneATU_Timer.setSingleShot(true);
connect(&tuneATU_Timer, &QTimer::timeout, this, &MainWindow::stopTuneATU);
killFileTimer.setSingleShot(true);
connect(&killFileTimer, &QTimer::timeout, this, &MainWindow::killFile);
uploadTimer.setSingleShot(true);
connect(&uploadTimer, SIGNAL(timeout()), this, SLOT(uploadSpots()));
TxAgainTimer.setSingleShot(true);
connect(&TxAgainTimer, SIGNAL(timeout()), this, SLOT(TxAgain()));
repeatTimer.setSingleShot(false);
repeatTimer.setInterval(1000);
connect(&repeatTimer, &QTimer::timeout, this, &MainWindow::checkRepeat);
connect(m_wideGraph.data (), SIGNAL(setFreq3(int,int)),this,
SLOT(setFreq4(int,int)));
connect(m_wideGraph.data(), &WideGraph::qsy, this, &MainWindow::qsy);
decodeBusy(false);
QString t1[28]={"1 uW","2 uW","5 uW","10 uW","20 uW","50 uW","100 uW","200 uW","500 uW",
"1 mW","2 mW","5 mW","10 mW","20 mW","50 mW","100 mW","200 mW","500 mW",
"1 W","2 W","5 W","10 W","20 W","50 W","100 W","200 W","500 W","1 kW"};
m_msg[0][0]=0;
m_bQRAsyncWarned=false;
ui->labDXped->setVisible(false);
for(int i=0; i<28; i++) { //Initialize dBm values
float dbm=(10.0*i)/3.0 - 30.0;
int ndbm=0;
if(dbm<0) ndbm=int(dbm-0.5);
if(dbm>=0) ndbm=int(dbm+0.5);
QString t;
t.sprintf("%d dBm ",ndbm);
t+=t1[i];
ui->TxPowerComboBox->addItem(t);
}
ui->labAz->setStyleSheet("border: 0px;");
// ui->labDist->setStyleSheet("border: 0px;");
auto t = "UTC dB DT Freq Message";
ui->decodedTextLabel->setText(t);
ui->decodedTextLabel2->setText(t);
readSettings(); //Restore user's setup params
m_audioThread.start (m_audioThreadPriority);
#ifdef WIN32
if (!m_multiple)
{
while(true)
{
int iret=killbyname("js8.exe");
if(iret == 603) break;
if(iret != 0)
MessageBox::warning_message (this, tr ("Error Killing js8.exe Process")
, tr ("KillByName return code: %1")
.arg (iret));
}
}
#endif
{
//delete any .quit file that might have been left lying around
//since its presence will cause jt9 to exit a soon as we start it
//and decodes will hang
QFile quitFile {m_config.temp_dir ().absoluteFilePath (".quit")};
while (quitFile.exists ())
{
if (!quitFile.remove ())
{
MessageBox::query_message (this, tr ("Error removing \"%1\"").arg (quitFile.fileName ())
, tr ("Click OK to retry"));
}
}
}
//Create .lock so jt9 will wait
QFile {m_config.temp_dir ().absoluteFilePath (".lock")}.open(QIODevice::ReadWrite);
QStringList js8_args {
"-s", QApplication::applicationName () // shared memory key,
// includes rig
#ifdef NDEBUG
, "-w", "1" //FFTW patience - release
#else
, "-w", "1" //FFTW patience - debug builds for speed
#endif
// The number of threads for FFTW specified here is chosen as
// three because that gives the best throughput of the large
// FFTs used in jt9. The count is the minimum of (the number
// available CPU threads less one) and three. This ensures that
// there is always at least one free CPU thread to run the other
// mode decoder in parallel.
, "-m", QString::number (qMin (qMax (QThread::idealThreadCount () - 1, 1), 3)) //FFTW threads
, "-e", QDir::toNativeSeparators (m_appDir)
, "-a", QDir::toNativeSeparators (m_config.writeable_data_dir ().absolutePath ())
, "-t", QDir::toNativeSeparators (m_config.temp_dir ().absolutePath ())
};
QProcessEnvironment env {QProcessEnvironment::systemEnvironment ()};
env.insert ("OMP_STACKSIZE", "4M");
proc_js8.setProcessEnvironment (env);
proc_js8.start(QDir::toNativeSeparators (m_appDir) + QDir::separator () +
"js8", js8_args, QIODevice::ReadWrite | QIODevice::Unbuffered);
QString fname {QDir::toNativeSeparators(m_config.writeable_data_dir ().absoluteFilePath ("wsjtx_wisdom.dat"))};
QByteArray cfname=fname.toLocal8Bit();
fftwf_import_wisdom_from_filename(cfname);
m_ntx = 6;
ui->txrb6->setChecked(true);
connect (&m_wav_future_watcher, &QFutureWatcher<void>::finished, this, &MainWindow::diskDat);
connect(&watcher3, SIGNAL(finished()),this,SLOT(fast_decode_done()));
// Q_EMIT startAudioInputStream (m_config.audio_input_device (), m_framesAudioInputBuffered, &m_detector, m_downSampleFactor, m_config.audio_input_channel ());
Q_EMIT startAudioInputStream (m_config.audio_input_device (), m_framesAudioInputBuffered, m_detector, m_downSampleFactor, m_config.audio_input_channel ());
Q_EMIT initializeAudioOutputStream (m_config.audio_output_device (), AudioDevice::Mono == m_config.audio_output_channel () ? 1 : 2, m_msAudioOutputBuffered);
Q_EMIT transmitFrequency (ui->TxFreqSpinBox->value () - m_XIT);
enable_DXCC_entity (m_config.DXCC ()); // sets text window proportions and (re)inits the logbook
ui->label_9->setStyleSheet("QLabel{background-color: #aabec8}");
ui->label_10->setStyleSheet("QLabel{background-color: #aabec8}");
// this must be done before initializing the mode as some modes need
// to turn off split on the rig e.g. WSPR
m_config.transceiver_online ();
bool vhf {m_config.enable_VHF_features ()};
morse_(const_cast<char *> (m_config.my_callsign ().toLatin1().constData()),
const_cast<int *> (icw), &m_ncw, m_config.my_callsign ().length());
on_actionWide_Waterfall_triggered();
ui->cbShMsgs->setChecked(m_bShMsgs);
ui->cbSWL->setChecked(m_bSWL);
if(m_bFast9) m_bFastMode=true;
ui->cbFast9->setChecked(m_bFast9 or m_bFastMode);
if(true || m_mode=="FT8") on_actionFT8_triggered();
ui->sbSubmode->setValue (vhf ? m_nSubMode : 0);
if(m_mode=="MSK144") {
Q_EMIT transmitFrequency (1000.0);
} else {
Q_EMIT transmitFrequency (ui->TxFreqSpinBox->value() - m_XIT);
}
m_saveDecoded=ui->actionSave_decoded->isChecked();
m_saveAll=ui->actionSave_all->isChecked();
ui->sbTxPercent->setValue(m_pctx);
ui->TxPowerComboBox->setCurrentIndex(int(0.3*(m_dBm + 30.0)+0.2));
ui->cbUploadWSPR_Spots->setChecked(m_uploadSpots);
if((m_ndepth&7)==1) ui->actionQuickDecode->setChecked(true);
if((m_ndepth&7)==2) ui->actionMediumDecode->setChecked(true);
if((m_ndepth&7)==3) ui->actionDeepDecode->setChecked(true);
if((m_ndepth&7)==4) ui->actionDeepestDecode->setChecked(true);
ui->actionInclude_averaging->setChecked(m_ndepth&16);
ui->actionInclude_correlation->setChecked(m_ndepth&32);
ui->actionEnable_AP_DXcall->setChecked(m_ndepth&64);
m_UTCdisk=-1;
m_fCPUmskrtd=0.0;
m_bFastDone=false;
m_bAltV=false;
m_bNoMoreFiles=false;
m_bVHFwarned=false;
m_bDoubleClicked=false;
m_bCallingCQ=false;
m_bCheckedContest=false;
m_bDisplayedOnce=false;
m_wait=0;
m_isort=-3;
m_max_dB=30;
m_CQtype="CQ";
if(m_mode.startsWith ("WSPR") and m_pctx>0) {
QPalette palette {ui->sbTxPercent->palette ()};
palette.setColor(QPalette::Base,Qt::yellow);
ui->sbTxPercent->setPalette(palette);
}
fixStop();
VHF_features_enabled(m_config.enable_VHF_features());
m_wideGraph->setVHF(m_config.enable_VHF_features());
connect( wsprNet, SIGNAL(uploadStatus(QString)), this, SLOT(uploadResponse(QString)));
statusChanged();
m_fastGraph->setMode(m_mode);
m_wideGraph->setMode(m_mode);
m_wideGraph->setModeTx(m_modeTx);
connect (&minuteTimer, &QTimer::timeout, this, &MainWindow::on_the_minute);
minuteTimer.setSingleShot (true);
minuteTimer.start (ms_minute_error () + 60 * 1000);
//connect (&splashTimer, &QTimer::timeout, this, &MainWindow::splash_done);
//splashTimer.setSingleShot (true);
//splashTimer.start (20 * 1000);
// TODO: jsherer - need to remove this eventually...
QTimer::singleShot (0, this, SLOT (checkStartupWarnings ()));
if(!ui->cbMenus->isChecked()) {
ui->cbMenus->setChecked(true);
ui->cbMenus->setChecked(false);
}
//UI Customizations
m_wideGraph.data()->installEventFilter(new EscapeKeyPressEater());
ui->mdiArea->addSubWindow(m_wideGraph.data(), Qt::Dialog | Qt::FramelessWindowHint | Qt::CustomizeWindowHint | Qt::Tool)->showMaximized();
//ui->menuDecode->setEnabled(true);
ui->menuMode->setVisible(false);
ui->menuSave->setEnabled(true);
ui->menuTools->setEnabled(false);
ui->menuView->setEnabled(false);
foreach(auto action, ui->menuBar->actions()){
if(action->text() == "Old View") ui->menuBar->removeAction(action);
if(action->text() == "Old Mode") ui->menuBar->removeAction(action);
if(action->text() == "Old Tools") ui->menuBar->removeAction(action);
}
ui->dxCallEntry->clear();
ui->dxGridEntry->clear();
auto f = findFreeFreqOffset(1000, 2000, 50);
setFreqOffsetForRestore(f, false);
ui->cbVHFcontest->setChecked(false); // this needs to always be false
ui->spotButton->setChecked(m_config.spot_to_reporting_networks());
auto enterFilter = new EnterKeyPressEater();
connect(enterFilter, &EnterKeyPressEater::enterKeyPressed, this, [this](QObject *, QKeyEvent *, bool *pProcessed){
if(QApplication::keyboardModifiers() & Qt::ShiftModifier){
if(pProcessed) *pProcessed = false;
return;
}
if(ui->extFreeTextMsgEdit->isReadOnly()){
if(pProcessed) *pProcessed = false;
return;
}
if(pProcessed) *pProcessed = true;
if(ui->extFreeTextMsgEdit->toPlainText().trimmed().isEmpty()){
return;
}
if(!ensureCallsignSet(true)){
return;
}
toggleTx(true);
});
ui->extFreeTextMsgEdit->installEventFilter(enterFilter);
auto clearActionSep = new QAction(nullptr);
clearActionSep->setSeparator(true);
auto clearActionAll = new QAction(QString("Clear All"), nullptr);
connect(clearActionAll, &QAction::triggered, this, &MainWindow::clearActivity);
// setup tablewidget context menus
auto clearAction1 = new QAction(QString("Clear"), ui->textEditRX);
connect(clearAction1, &QAction::triggered, this, [this](){ this->on_clearAction_triggered(ui->textEditRX); });
ui->textEditRX->setContextMenuPolicy(Qt::CustomContextMenu);
connect(ui->textEditRX, &QTableWidget::customContextMenuRequested, this, [this, clearAction1, clearActionAll](QPoint const &point){
QMenu * menu = new QMenu(ui->textEditRX);
buildEditMenu(menu, ui->textEditRX);
menu->addSeparator();
menu->addAction(clearAction1);
menu->addAction(clearActionAll);
menu->popup(ui->textEditRX->mapToGlobal(point));
});
auto clearAction2 = new QAction(QString("Clear"), ui->extFreeTextMsgEdit);
connect(clearAction2, &QAction::triggered, this, [this](){ this->on_clearAction_triggered(ui->extFreeTextMsgEdit); });
auto restoreAction = new QAction(QString("Restore Previous Message"), ui->extFreeTextMsgEdit);
connect(restoreAction, &QAction::triggered, this, [this](){ this->restoreMessage(); });
ui->extFreeTextMsgEdit->setContextMenuPolicy(Qt::CustomContextMenu);
connect(ui->extFreeTextMsgEdit, &QTableWidget::customContextMenuRequested, this, [this, clearAction2, clearActionAll, restoreAction](QPoint const &point){
QMenu * menu = new QMenu(ui->extFreeTextMsgEdit);
auto selectedCall = callsignSelected();
bool missingCallsign = selectedCall.isEmpty();
restoreAction->setDisabled(m_lastTxMessage.isEmpty());
menu->addAction(restoreAction);
menu->addSeparator();
auto savedMenu = menu->addMenu("Saved messages...");
buildSavedMessagesMenu(savedMenu);
auto directedMenu = menu->addMenu(QString("Directed to %1...").arg(selectedCall));
directedMenu->setDisabled(missingCallsign);
buildQueryMenu(directedMenu, selectedCall);
auto relayMenu = menu->addMenu("Relay via...");
relayMenu->setDisabled(ui->extFreeTextMsgEdit->toPlainText().isEmpty() || m_callActivity.isEmpty());
buildRelayMenu(relayMenu);
menu->addSeparator();
buildEditMenu(menu, ui->extFreeTextMsgEdit);
menu->addSeparator();
menu->addAction(clearAction2);
menu->addAction(clearActionAll);
menu->popup(ui->extFreeTextMsgEdit->mapToGlobal(point));
displayActivity(true);
});
auto clearAction3 = new QAction(QString("Clear"), ui->tableWidgetRXAll);
connect(clearAction3, &QAction::triggered, this, [this](){ this->on_clearAction_triggered(ui->tableWidgetRXAll); });
auto removeActivity = new QAction(QString("Remove Activity"), ui->tableWidgetRXAll);
connect(removeActivity, &QAction::triggered, this, [this](){
if(ui->tableWidgetRXAll->selectedItems().isEmpty()){
return;
}
auto selectedItems = ui->tableWidgetRXAll->selectedItems();
int selectedOffset = selectedItems.first()->text().toInt();
m_bandActivity.remove(selectedOffset);
displayActivity(true);
});
auto logAction = new QAction(QString("Log..."), ui->tableWidgetCalls);
connect(logAction, &QAction::triggered, this, &MainWindow::on_logQSOButton_clicked);
ui->tableWidgetRXAll->setContextMenuPolicy(Qt::CustomContextMenu);
connect(ui->tableWidgetRXAll, &QTableWidget::customContextMenuRequested, this, [this, clearAction3, clearActionAll, removeActivity, logAction](QPoint const &point){
QMenu * menu = new QMenu(ui->tableWidgetRXAll);
// clear the selection of the call widget on right click
ui->tableWidgetCalls->selectionModel()->clearSelection();
QString selectedCall = callsignSelected();
bool missingCallsign = selectedCall.isEmpty();
bool isAllCall = isAllCallIncluded(selectedCall);
int selectedOffset = -1;
if(!ui->tableWidgetRXAll->selectedItems().isEmpty()){
auto selectedItems = ui->tableWidgetRXAll->selectedItems();
selectedOffset = selectedItems.first()->data(Qt::UserRole).toInt();
}
if(selectedOffset != -1){
auto qsyAction = menu->addAction(QString("Jump to %1Hz...").arg(selectedOffset));
connect(qsyAction, &QAction::triggered, this, [this, selectedOffset](){
setFreqOffsetForRestore(selectedOffset, false);
});
menu->addSeparator();
}
menu->addAction(logAction);
logAction->setDisabled(missingCallsign || isAllCall);
menu->addSeparator();
auto savedMenu = menu->addMenu("Saved messages...");
buildSavedMessagesMenu(savedMenu);
auto directedMenu = menu->addMenu(QString("Directed to %1...").arg(selectedCall));
directedMenu->setDisabled(missingCallsign);
buildQueryMenu(directedMenu, selectedCall);
auto relayAction = buildRelayAction(selectedCall);
relayAction->setText(QString("Relay via %1...").arg(selectedCall));
relayAction->setDisabled(missingCallsign);
menu->addActions({ relayAction });
auto deselectAction = menu->addAction(QString("Deselect %1").arg(selectedCall));
deselectAction->setDisabled(missingCallsign);
connect(deselectAction, &QAction::triggered, this, [this](){
ui->tableWidgetRXAll->clearSelection();
ui->tableWidgetCalls->clearSelection();
});
menu->addSeparator();
removeActivity->setDisabled(selectedOffset == -1);
menu->addAction(removeActivity);
menu->addSeparator();
menu->addAction(clearAction3);
menu->addAction(clearActionAll);
menu->popup(ui->tableWidgetRXAll->mapToGlobal(point));
displayActivity(true);
});
auto clearAction4 = new QAction(QString("Clear"), ui->tableWidgetCalls);
connect(clearAction4, &QAction::triggered, this, [this](){ this->on_clearAction_triggered(ui->tableWidgetCalls); });
auto addStation = new QAction(QString("Add New Station or Group..."), ui->tableWidgetCalls);
connect(addStation, &QAction::triggered, this, [this](){
bool ok = false;
QString callsign = QInputDialog::getText(this, tr("Add New Station or Group..."),
tr("Station or Group Callsign:"), QLineEdit::Normal,
"", &ok).toUpper().trimmed();
if(!ok || callsign.trimmed().isEmpty()){
return;
}
if(callsign.startsWith("@")){
if(Varicode::isCompoundCallsign(callsign)){
m_config.addGroup(callsign);
} else {
MessageBox::critical_message (this, QString("%1 is not a valid group").arg(callsign));
}
} else {
CallDetail cd = {};
cd.call = callsign;
m_callActivity[callsign] = cd;
}
displayActivity(true);
});
auto removeStation = new QAction(QString("Remove Station"), ui->tableWidgetCalls);
connect(removeStation, &QAction::triggered, this, [this](){
QString selectedCall = callsignSelected();
if(selectedCall.isEmpty()){
return;
}
if (selectedCall.startsWith("@")){
m_config.removeGroup(selectedCall);
} else if(m_callActivity.contains(selectedCall)){
m_callActivity.remove(selectedCall);
}
displayActivity(true);
});
ui->tableWidgetCalls->setContextMenuPolicy(Qt::CustomContextMenu);
connect(ui->tableWidgetCalls, &QTableWidget::customContextMenuRequested, this, [this, logAction, clearAction4, clearActionAll, addStation, removeStation](QPoint const &point){
QMenu * menu = new QMenu(ui->tableWidgetCalls);
ui->tableWidgetRXAll->selectionModel()->clearSelection();
QString selectedCall = callsignSelected();
bool isAllCall = isAllCallIncluded(selectedCall);
bool missingCallsign = selectedCall.isEmpty();
if(!missingCallsign && !isAllCall){
int selectedOffset = m_callActivity[selectedCall].freq;
if(selectedOffset != -1){
auto qsyAction = menu->addAction(QString("Jump to %1Hz...").arg(selectedOffset));
connect(qsyAction, &QAction::triggered, this, [this, selectedOffset](){
setFreqOffsetForRestore(selectedOffset, false);
});
menu->addSeparator();
}
}
menu->addAction(logAction);
logAction->setDisabled(missingCallsign || isAllCall);
menu->addSeparator();
auto savedMenu = menu->addMenu("Saved messages...");
buildSavedMessagesMenu(savedMenu);
auto directedMenu = menu->addMenu(QString("Directed to %1...").arg(selectedCall));
directedMenu->setDisabled(missingCallsign);
buildQueryMenu(directedMenu, selectedCall);
auto relayAction = buildRelayAction(selectedCall);
relayAction->setText(QString("Relay via %1...").arg(selectedCall));
relayAction->setDisabled(missingCallsign || isAllCall);
menu->addActions({ relayAction });
auto deselect = menu->addAction(QString("Deselect %1").arg(selectedCall));
deselect->setDisabled(missingCallsign);
connect(deselect, &QAction::triggered, this, [this](){
ui->tableWidgetRXAll->clearSelection();
ui->tableWidgetCalls->clearSelection();
});
menu->addSeparator();
menu->addAction(addStation);
removeStation->setDisabled(missingCallsign || isAllCall);
removeStation->setText(selectedCall.startsWith("@") ? "Remove Group" : "Remove Station");
menu->addAction(removeStation);
menu->addSeparator();
menu->addAction(clearAction4);
menu->addAction(clearActionAll);
menu->popup(ui->tableWidgetCalls->mapToGlobal(point));
});
connect(ui->tableWidgetRXAll->selectionModel(), &QItemSelectionModel::selectionChanged, this, &MainWindow::on_tableWidgetRXAll_selectionChanged);
connect(ui->tableWidgetCalls->selectionModel(), &QItemSelectionModel::selectionChanged, this, &MainWindow::on_tableWidgetCalls_selectionChanged);
auto p = ui->tableWidgetRXAll->palette();
p.setColor(QPalette::Inactive, QPalette::Highlight, p.color(QPalette::Active, QPalette::Highlight));
ui->tableWidgetRXAll->setPalette(p);
p = ui->tableWidgetCalls->palette();
p.setColor(QPalette::Inactive, QPalette::Highlight, p.color(QPalette::Active, QPalette::Highlight));
ui->tableWidgetCalls->setPalette(p);
ui->hbMacroButton->setContextMenuPolicy(Qt::CustomContextMenu);
connect(ui->hbMacroButton, &QPushButton::customContextMenuRequested, this, [this](QPoint const &point){
QMenu * menu = new QMenu(ui->hbMacroButton);
buildHeartbeatMenu(menu);
menu->popup(ui->hbMacroButton->mapToGlobal(point));
});
ui->cqMacroButton->setContextMenuPolicy(Qt::CustomContextMenu);
connect(ui->cqMacroButton, &QPushButton::customContextMenuRequested, this, [this](QPoint const &point){
QMenu * menu = new QMenu(ui->cqMacroButton);
buildCQMenu(menu);
menu->popup(ui->cqMacroButton->mapToGlobal(point));
});
// Don't block heartbeat's first run...
m_lastTxTime = DriftingDateTime::currentDateTimeUtc().addSecs(-300);
int width = 75;
/*
QList<QPushButton*> btns;
foreach(auto child, ui->buttonGrid->children()){
if(!child->isWidgetType()){
continue;
}
if(!child->objectName().contains("Button")){
continue;
}
auto b = qobject_cast<QPushButton*>(child);
width = qMax(width, b->geometry().width());
btns.append(b);
}
*/
auto buttonLayout = ui->buttonGrid->layout();
auto gridButtonLayout = qobject_cast<QGridLayout*>(buttonLayout);
gridButtonLayout->setColumnMinimumWidth(0, width);
gridButtonLayout->setColumnMinimumWidth(1, width);
gridButtonLayout->setColumnMinimumWidth(2, width);
gridButtonLayout->setColumnStretch(0, 1);
gridButtonLayout->setColumnStretch(1, 1);
gridButtonLayout->setColumnStretch(2, 1);
pskSetLocal();
aprsSetLocal();
clearActivity();
displayActivity(true);
/*
QTimer::singleShot(1000, this, [this](){
QPalette p;
p.setBrush(QPalette::Text, QColor(Qt::red));
p.setColor(QPalette::Text, QColor(Qt::red));
ui->extFreeTextMsgEdit->setPalette(p);
ui->extFreeTextMsgEdit->updateGeometry();
ui->extFreeTextMsgEdit->update();
});
*/
m_txTextDirtyDebounce.setSingleShot(true);
connect(&m_txTextDirtyDebounce, &QTimer::timeout, this, &MainWindow::refreshTextDisplay);
QTimer::singleShot(0, this, &MainWindow::initializeDummyData);
// this must be the last statement of constructor
if (!m_valid) throw std::runtime_error {"Fatal initialization exception"};
}
QDate eol(2019, 1, 2);
void MainWindow::checkExpiryWarningMessage()
{
if(QDateTime::currentDateTimeUtc().date() > eol){
MessageBox::critical_message (this, QString("This pre-release development build of JS8Call has expired. Please upgrade to the latest version."));
return;
}
}
void MainWindow::checkStartupWarnings ()
{
MessageBox::critical_message (this,
QString("This version of %1 is a pre-release development\n"
"build and will expire after %2 (UTC), upon which you\n"
"will need to upgrade to the latest version. \n\n"
"Use of development versions of JS8Call are at your own risk \n"
"and carry a responsiblity to report any problems to:\n"
"Jordan Sherer (KN4CRD) kn4crd@gmail.com\n\n").arg(QApplication::applicationName()).arg(eol.toString()));
checkExpiryWarningMessage();
ensureCallsignSet(false);
}
void MainWindow::initializeDummyData(){
if(!QApplication::applicationName().contains("dummy")){
return;
}
auto d = DecodedText("h+vWp6mRPprH", 6);
qDebug() << d.message() << buildMessageFrames(d.message());
d = DecodedText("bYG4CKYT0cKG", 7);
qDebug() << d.message();
// qDebug() << Varicode::isValidCallsign("@GROUP1", nullptr);
// qDebug() << Varicode::packAlphaNumeric50("VE7/KN4CRD");
// qDebug() << Varicode::unpackAlphaNumeric50(Varicode::packAlphaNumeric50("VE7/KN4CRD"));
// qDebug() << Varicode::unpackAlphaNumeric50(Varicode::packAlphaNumeric50("@GROUP/42"));
// qDebug() << Varicode::unpackAlphaNumeric50(Varicode::packAlphaNumeric50("SP1ATOM"));
QList<QString> calls = {
"@GROUP/42",
"KN4CRD",
"VE7/KN4CRD",
"KN4CRD/P",
"KC9QNE",
"KI6SSI",
"K0OG",
"LB9YH",
"VE7/LB9YH",
"M0IAX",
"N0JDS",
"OH8STN",
"VA3OSO",
"VK1MIC",
"W0FW"
};
auto dt = DriftingDateTime::currentDateTimeUtc().addSecs(-300);
int i = 0;
foreach(auto call, calls){
CallDetail cd = {};
cd.call = call;
cd.freq = 500 + 100*i;
cd.snr = i == 3 ? -100 : i;
cd.ackTimestamp = i == 1 ? dt.addSecs(-900) : QDateTime{};
cd.utcTimestamp = dt;
cd.grid = i == 5 ? "J042" : i == 6 ? " FN42FN42FN" : "";
cd.tdrift = 0.1*i;
logCallActivity(cd, false);
ActivityDetail ad = {};
ad.snr = i == 3 ? -100 : i;
ad.freq = 500 + 100*i;
ad.text = QString("%1: %2 TEST").arg(call).arg(m_config.my_callsign());
ad.utcTimestamp = dt;
m_bandActivity[500+100*i] = { ad };
markOffsetDirected(500+100*i, false);
i++;
}
displayTextForFreq("KN4CRD: @ALLCALL? \u2301 ", 42, DriftingDateTime::currentDateTimeUtc().addSecs(-315), true, true, true);
displayTextForFreq("J1Y: KN4CRD SNR -05 \u2301 ", 42, DriftingDateTime::currentDateTimeUtc().addSecs(-300), false, true, true);
displayTextForFreq("HELLO BRAVE NEW WORLD \u2301 ", 42, DriftingDateTime::currentDateTimeUtc().addSecs(-300), false, true, true);
displayActivity(true);
}
void MainWindow::initialize_fonts ()
{
set_application_font (m_config.text_font ());
setTextEditFont(ui->textEditRX, m_config.rx_text_font());
setTextEditFont(ui->extFreeTextMsgEdit, m_config.tx_text_font());
displayActivity(true);
}
void MainWindow::splash_done ()
{
m_splash && m_splash->close ();
}
void MainWindow::on_the_minute ()
{
if (minuteTimer.isSingleShot ())
{
minuteTimer.setSingleShot (false);
minuteTimer.start (60 * 1000); // run free
}
else
{
auto const& ms_error = ms_minute_error ();
if (qAbs (ms_error) > 1000) // keep drift within +-1s
{
minuteTimer.setSingleShot (true);
minuteTimer.start (ms_error + 60 * 1000);
}
}
if (m_config.watchdog ())
{
incrementIdleTimer();
update_watchdog_label ();
}
else
{
tx_watchdog (false);
}
}
void MainWindow::tryBandHop(){
// see if we need to hop bands...
if(!m_config.auto_switch_bands()){
return;
}
// make sure we're not transmitting
if(isMessageQueuedForTransmit()){
return;
}
// get the current band
auto dialFreq = dialFrequency();
auto currentBand = m_config.bands()->find(dialFreq);
// get the stations list
auto stations = m_config.stations()->station_list();
// order stations by (switch_at, switch_until) time tuple
qStableSort(stations.begin(), stations.end(), [](StationList::Station const &a, StationList::Station const &b){
return (a.switch_at_ < b.switch_at_) || (a.switch_at_ == b.switch_at_ && a.switch_until_ < b.switch_until_);
});
// we just set the date to a known y/m/d to make the comparisons easier
QDateTime d = DriftingDateTime::currentDateTimeUtc();
d.setDate(QDate(2000, 1, 1));
QDateTime startOfDay = QDateTime(QDate(2000, 1, 1), QTime(0, 0));
QDateTime endOfDay = QDateTime(QDate(2000, 1, 1), QTime(23, 59));
// see if we can find a needed band switch...
foreach(auto station, stations){
// we can switch to this frequency if we're in the time range, inclusive of switch_at, exclusive of switch_until
// and if we are switching to a different frequency than the last hop. this allows us to switch bands at that time,
// but then later we can later switch to a different band if needed without the automatic band switching to take over
bool inTimeRange = (
(station.switch_at_ <= d && d <= station.switch_until_) || // <- normal range, 12-16 && 6-8, evalued as 12 <= d <= 16 || 6 <= d <= 8
(station.switch_until_ < station.switch_at_ && ( // <- say for a range of 12->2 & 2->12; 12->2,
(station.switch_at_ <= d && d <= endOfDay) || // should be evaluated as 12 <= d <= 23:59 || 00:00 <= d <= 2
(startOfDay <= d && d <= station.switch_until_)
))
);
bool noOverride = (
(m_bandHopped || (!m_bandHopped && station.frequency_ != m_bandHoppedFreq))
);
bool freqIsDifferent = (station.frequency_ != dialFreq);
bool canSwitch = (
inTimeRange &&
noOverride &&
freqIsDifferent
);
// switch, if we can and the band is different than our current band
if(canSwitch){
Frequency frequency = station.frequency_;
m_bandHopped = false;
m_bandHoppedFreq = frequency;
SelfDestructMessageBox * m = new SelfDestructMessageBox(30,
"Scheduled Frequency Change",
QString("A scheduled frequency change has arrived. The rig frequency will be changed to %1 MHz in %2 second(s).").arg(Radio::frequency_MHz_string(station.frequency_)),
QMessageBox::Information,
QMessageBox::Ok | QMessageBox::Cancel,
QMessageBox::Ok,
this);
connect(m, &SelfDestructMessageBox::accepted, this, [this, frequency](){
m_bandHopped = true;
setRig(frequency);
});
m->show();
#if 0
// TODO: jsherer - this is totally a hack because of the signal that gets emitted to clearActivity on band change...
QTimer *t = new QTimer(this);
t->setInterval(250);
t->setSingleShot(true);
connect(t, &QTimer::timeout, this, [this, station, dialFreq](){
auto message = QString("Scheduled frequency switch from %1 MHz to %2 MHz");
message = message.arg(Radio::frequency_MHz_string(dialFreq));
message = message.arg(Radio::frequency_MHz_string(station.frequency_));
writeNoticeTextToUI(DriftingDateTime::currentDateTimeUtc(), message);
});
t->start();
#endif
return;
}
}
}
//--------------------------------------------------- MainWindow destructor
MainWindow::~MainWindow()
{
m_astroWidget.reset ();
QString fname {QDir::toNativeSeparators(m_config.writeable_data_dir ().absoluteFilePath ("wsjtx_wisdom.dat"))};
QByteArray cfname=fname.toLocal8Bit();
fftwf_export_wisdom_to_filename(cfname);
m_audioThread.quit ();
m_audioThread.wait ();
remove_child_from_event_filter (this);
}
//-------------------------------------------------------- writeSettings()
void MainWindow::writeSettings()
{
m_settings->beginGroup("MainWindow");
m_settings->setValue ("geometry", saveGeometry ());
m_settings->setValue ("geometryNoControls", m_geometryNoControls);
m_settings->setValue ("state", saveState ());
m_settings->setValue("MRUdir", m_path);
m_settings->setValue("DXcall",ui->dxCallEntry->text());
m_settings->setValue("DXgrid",ui->dxGridEntry->text());
m_settings->setValue ("AstroDisplayed", m_astroWidget && m_astroWidget->isVisible());
m_settings->setValue ("MsgAvgDisplayed", m_msgAvgWidget && m_msgAvgWidget->isVisible());
m_settings->setValue ("FreeText", ui->freeTextMsg->currentText ());
m_settings->setValue("ShowMenus",ui->cbMenus->isChecked());
m_settings->setValue("CallFirst",ui->cbFirst->isChecked());
m_settings->setValue("MainSplitter", ui->mainSplitter->saveState());
m_settings->setValue("TextHorizontalSplitter", ui->textHorizontalSplitter->saveState());
m_settings->setValue("BandActivityVisible", ui->tableWidgetRXAll->isVisible());
m_settings->setValue("TextVerticalSplitter", ui->textVerticalSplitter->saveState());
m_settings->setValue("ShowTimeDrift", ui->driftSyncFrame->isVisible());
m_settings->setValue("TimeDrift", ui->driftSpinBox->value());
m_settings->setValue("ShowTooltips", ui->actionShow_Tooltips->isChecked());
m_settings->endGroup();
m_settings->beginGroup("Common");
m_settings->setValue("Mode",m_mode);
m_settings->setValue("ModeTx",m_modeTx);
m_settings->setValue("SaveNone",ui->actionNone->isChecked());
m_settings->setValue("SaveDecoded",ui->actionSave_decoded->isChecked());
m_settings->setValue("SaveAll",ui->actionSave_all->isChecked());
m_settings->setValue("NDepth",m_ndepth);
m_settings->setValue("RxFreq",ui->RxFreqSpinBox->value());
m_settings->setValue("TxFreq",ui->TxFreqSpinBox->value());
m_settings->setValue("WSPRfreq",ui->WSPRfreqSpinBox->value());
m_settings->setValue("SubMode",ui->sbSubmode->value());
m_settings->setValue("DTtol",m_DTtol);
m_settings->setValue("Ftol", ui->sbFtol->value ());
m_settings->setValue("MinSync",m_minSync);
m_settings->setValue ("AutoSeq", ui->cbAutoSeq->isChecked ());
m_settings->setValue ("RxAll", ui->cbRxAll->isChecked ());
m_settings->setValue ("VHFcontest", ui->cbVHFcontest->isChecked ());
m_settings->setValue("ShMsgs",m_bShMsgs);
m_settings->setValue("SWL",ui->cbSWL->isChecked());
m_settings->setValue ("DialFreq", QVariant::fromValue(m_lastMonitoredFrequency));
m_settings->setValue("OutAttenuation", ui->outAttenuation->value ());
m_settings->setValue("NoSuffix",m_noSuffix);
m_settings->setValue("GUItab",ui->tabWidget->currentIndex());
m_settings->setValue("OutBufSize",outBufSize);
m_settings->setValue ("HoldTxFreq", ui->cbHoldTxFreq->isChecked ());
m_settings->setValue("PctTx",m_pctx);
m_settings->setValue("dBm",m_dBm);
m_settings->setValue ("WSPRPreferType1", ui->WSPR_prefer_type_1_check_box->isChecked ());
m_settings->setValue("UploadSpots",m_uploadSpots);
m_settings->setValue ("BandHopping", ui->band_hopping_group_box->isChecked ());
m_settings->setValue ("TRPeriod", ui->sbTR->value ());
m_settings->setValue("FastMode",m_bFastMode);
m_settings->setValue("Fast9",m_bFast9);
m_settings->setValue ("CQTxfreq", ui->sbCQTxFreq->value ());
m_settings->setValue("pwrBandTxMemory",m_pwrBandTxMemory);
m_settings->setValue("pwrBandTuneMemory",m_pwrBandTuneMemory);
m_settings->setValue ("FT8AP", ui->actionEnable_AP_FT8->isChecked ());
m_settings->setValue ("JT65AP", ui->actionEnable_AP_JT65->isChecked ());
m_settings->setValue("SortBy", QVariant(m_sortCache));
m_settings->setValue("ShowColumns", QVariant(m_showColumnsCache));
m_settings->setValue("HBHidden", m_hbHidden);
m_settings->setValue("HBInterval", m_hbInterval);
m_settings->setValue("CQInterval", m_cqInterval);
// TODO: jsherer - need any other customizations?
/*m_settings->setValue("PanelLeftGeometry", ui->tableWidgetRXAll->geometry());
m_settings->setValue("PanelRightGeometry", ui->tableWidgetCalls->geometry());
m_settings->setValue("PanelTopGeometry", ui->extFreeTextMsg->geometry());
m_settings->setValue("PanelBottomGeometry", ui->extFreeTextMsgEdit->geometry());
m_settings->setValue("PanelWaterfallGeometry", ui->bandHorizontalWidget->geometry());*/
//m_settings->setValue("MainSplitter", QVariant::fromValue(ui->mainSplitter->sizes()));
{
QList<QVariant> coeffs; // suitable for QSettings
for (auto const& coeff : m_phaseEqCoefficients)
{
coeffs << coeff;
}
m_settings->setValue ("PhaseEqualizationCoefficients", QVariant {coeffs});
}
m_settings->endGroup();
}
//---------------------------------------------------------- readSettings()
void MainWindow::readSettings()
{
ui->cbVHFcontest->setVisible(false);
ui->cbAutoSeq->setVisible(false);
ui->cbFirst->setVisible(false);
m_settings->beginGroup("MainWindow");
setMinimumSize(800, 400);
restoreGeometry (m_settings->value ("geometry", saveGeometry ()).toByteArray ());
setMinimumSize(800, 400);
m_geometryNoControls = m_settings->value ("geometryNoControls",saveGeometry()).toByteArray();
restoreState (m_settings->value ("state", saveState ()).toByteArray ());
ui->dxCallEntry->setText (m_settings->value ("DXcall", QString {}).toString ());
ui->dxGridEntry->setText (m_settings->value ("DXgrid", QString {}).toString ());
m_path = m_settings->value("MRUdir", m_config.save_directory ().absolutePath ()).toString ();
auto displayAstro = m_settings->value ("AstroDisplayed", false).toBool ();
auto displayMsgAvg = m_settings->value ("MsgAvgDisplayed", false).toBool ();
if (m_settings->contains ("FreeText")) ui->freeTextMsg->setCurrentText (
m_settings->value ("FreeText").toString ());
ui->cbMenus->setChecked(m_settings->value("ShowMenus",true).toBool());
ui->cbFirst->setChecked(m_settings->value("CallFirst",true).toBool());
auto mainSplitterState = m_settings->value("MainSplitter").toByteArray();
if(!mainSplitterState.isEmpty()){
ui->mainSplitter->restoreState(mainSplitterState);
}
auto horizontalState = m_settings->value("TextHorizontalSplitter").toByteArray();
if(!horizontalState.isEmpty()){
ui->textHorizontalSplitter->restoreState(horizontalState);
auto hsizes = ui->textHorizontalSplitter->sizes();
ui->tableWidgetRXAll->setVisible(hsizes.at(0) > 0);
ui->tableWidgetCalls->setVisible(hsizes.at(2) > 0);
}
m_bandActivityWasVisible = m_settings->value("BandActivityVisible", true).toBool();
ui->tableWidgetRXAll->setVisible(m_bandActivityWasVisible);
auto verticalState = m_settings->value("TextVerticalSplitter").toByteArray();
if(!verticalState.isEmpty()){
ui->textVerticalSplitter->restoreState(verticalState);
}
ui->driftSyncFrame->setVisible(m_settings->value("ShowTimeDrift", false).toBool());
ui->driftSpinBox->setValue(m_settings->value("TimeDrift", 0).toInt());
ui->actionShow_Tooltips->setChecked(m_settings->value("ShowTooltips", true).toBool());
m_settings->endGroup();
// do this outside of settings group because it uses groups internally
ui->actionAstronomical_data->setChecked (displayAstro);
m_settings->beginGroup("Common");
m_mode=m_settings->value("Mode","JT9").toString();
m_modeTx=m_settings->value("ModeTx","JT9").toString();
if(m_modeTx.mid(0,3)=="JT9") ui->pbTxMode->setText("Tx JT9 @");
if(m_modeTx=="JT65") ui->pbTxMode->setText("Tx JT65 #");
// these save settings should never be enabled unless specifically called out by the user for every session.
ui->actionNone->setChecked(true);
ui->actionSave_decoded->setChecked(false);
ui->actionSave_all->setChecked(false);
ui->RxFreqSpinBox->setValue(0); // ensure a change is signaled
ui->RxFreqSpinBox->setValue(m_settings->value("RxFreq",1500).toInt());
m_nSubMode=m_settings->value("SubMode",0).toInt();
ui->sbFtol->setValue (m_settings->value("Ftol", 20).toInt());
m_minSync=m_settings->value("MinSync",0).toInt();
ui->syncSpinBox->setValue(m_minSync);
ui->cbAutoSeq->setChecked (m_settings->value ("AutoSeq", false).toBool());
ui->cbRxAll->setChecked (m_settings->value ("RxAll", false).toBool());
ui->cbVHFcontest->setChecked (m_settings->value ("VHFcontest", false).toBool());
m_bShMsgs=m_settings->value("ShMsgs",false).toBool();
m_bSWL=m_settings->value("SWL",false).toBool();
m_bFast9=m_settings->value("Fast9",false).toBool();
m_bFastMode=m_settings->value("FastMode",false).toBool();
ui->sbTR->setValue (m_settings->value ("TRPeriod", 30).toInt());
m_lastMonitoredFrequency = m_settings->value ("DialFreq",
QVariant::fromValue<Frequency> (default_frequency)).value<Frequency> ();
ui->WSPRfreqSpinBox->setValue(0); // ensure a change is signaled
ui->WSPRfreqSpinBox->setValue(m_settings->value("WSPRfreq",1500).toInt());
ui->TxFreqSpinBox->setValue(0); // ensure a change is signaled
ui->TxFreqSpinBox->setValue(m_settings->value("TxFreq",1500).toInt());
m_ndepth=m_settings->value("NDepth",3).toInt();
m_pctx=m_settings->value("PctTx",20).toInt();
m_dBm=m_settings->value("dBm",37).toInt();
ui->WSPR_prefer_type_1_check_box->setChecked (m_settings->value ("WSPRPreferType1", true).toBool ());
m_uploadSpots=m_settings->value("UploadSpots",false).toBool();
if(!m_uploadSpots) ui->cbUploadWSPR_Spots->setStyleSheet("QCheckBox{background-color: yellow}");
ui->band_hopping_group_box->setChecked (m_settings->value ("BandHopping", false).toBool());
// setup initial value of tx attenuator
m_block_pwr_tooltip = true;
ui->outAttenuation->setValue (m_settings->value ("OutAttenuation", 0).toInt ());
m_block_pwr_tooltip = false;
ui->sbCQTxFreq->setValue (m_settings->value ("CQTxFreq", 260).toInt());
m_noSuffix=m_settings->value("NoSuffix",false).toBool();
int n=m_settings->value("GUItab",0).toInt();
ui->tabWidget->setCurrentIndex(n);
outBufSize=m_settings->value("OutBufSize",4096).toInt();
ui->cbHoldTxFreq->setChecked (m_settings->value ("HoldTxFreq", false).toBool ());
m_pwrBandTxMemory=m_settings->value("pwrBandTxMemory").toHash();
m_pwrBandTuneMemory=m_settings->value("pwrBandTuneMemory").toHash();
ui->actionEnable_AP_FT8->setChecked (m_settings->value ("FT8AP", false).toBool());
ui->actionEnable_AP_JT65->setChecked (m_settings->value ("JT65AP", false).toBool());
m_sortCache = m_settings->value("SortBy").toMap();
m_showColumnsCache = m_settings->value("ShowColumns").toMap();
m_hbHidden = m_settings->value("HBHidden", false).toBool();
m_hbInterval = m_settings->value("HBInterval", 0).toInt();
m_cqInterval = m_settings->value("CQInterval", 0).toInt();
// TODO: jsherer - any other customizations?
//ui->mainSplitter->setSizes(m_settings->value("MainSplitter", QVariant::fromValue(ui->mainSplitter->sizes())).value<QList<int> >());
//ui->tableWidgetRXAll->restoreGeometry(m_settings->value("PanelLeftGeometry", ui->tableWidgetRXAll->saveGeometry()).toByteArray());
//ui->tableWidgetCalls->restoreGeometry(m_settings->value("PanelRightGeometry", ui->tableWidgetCalls->saveGeometry()).toByteArray());
//ui->extFreeTextMsg->setGeometry( m_settings->value("PanelTopGeometry", ui->extFreeTextMsg->geometry()).toRect());
//ui->extFreeTextMsgEdit->setGeometry( m_settings->value("PanelBottomGeometry", ui->extFreeTextMsgEdit->geometry()).toRect());
//ui->bandHorizontalWidget->setGeometry( m_settings->value("PanelWaterfallGeometry", ui->bandHorizontalWidget->geometry()).toRect());
//qDebug() << m_settings->value("PanelTopGeometry") << ui->extFreeTextMsg;
setTextEditStyle(ui->textEditRX, m_config.color_rx_foreground(), m_config.color_rx_background(), m_config.rx_text_font());
setTextEditStyle(ui->extFreeTextMsgEdit, m_config.color_compose_foreground(), m_config.color_compose_background(), m_config.compose_text_font());
{
auto const& coeffs = m_settings->value ("PhaseEqualizationCoefficients"
, QList<QVariant> {0., 0., 0., 0., 0.}).toList ();
m_phaseEqCoefficients.clear ();
for (auto const& coeff : coeffs)
{
m_phaseEqCoefficients.append (coeff.value<double> ());
}
}
m_settings->endGroup();
// use these initialisation settings to tune the audio o/p buffer
// size and audio thread priority
m_settings->beginGroup ("Tune");
m_msAudioOutputBuffered = m_settings->value ("Audio/OutputBufferMs").toInt ();
m_framesAudioInputBuffered = m_settings->value ("Audio/InputBufferFrames", RX_SAMPLE_RATE / 10).toInt ();
m_audioThreadPriority = static_cast<QThread::Priority> (m_settings->value ("Audio/ThreadPriority", QThread::HighPriority).toInt () % 8);
m_settings->endGroup ();
if (displayMsgAvg) on_actionMessage_averaging_triggered();
}
void MainWindow::set_application_font (QFont const& font)
{
qApp->setFont (font);
// set font in the application style sheet as well in case it has
// been modified in the style sheet which has priority
qApp->setStyleSheet (qApp->styleSheet () + "* {" + font_as_stylesheet (font) + '}');
for (auto& widget : qApp->topLevelWidgets ())
{
widget->updateGeometry ();
}
}
void MainWindow::setDecodedTextFont (QFont const& font)
{
ui->decodedTextBrowser->setContentFont (font);
ui->decodedTextBrowser2->setContentFont (font);
ui->textBrowser4->setContentFont(font);
ui->textBrowser4->displayFoxToBeCalled(" ","#ffffff");
ui->textBrowser4->setText("");
auto style_sheet = "QLabel {" + font_as_stylesheet (font) + '}';
ui->decodedTextLabel->setStyleSheet (ui->decodedTextLabel->styleSheet () + style_sheet);
ui->decodedTextLabel2->setStyleSheet (ui->decodedTextLabel2->styleSheet () + style_sheet);
if (m_msgAvgWidget) {
m_msgAvgWidget->changeFont (font);
}
updateGeometry ();
}
void MainWindow::fixStop()
{
m_hsymStop=179;
if(m_mode=="WSPR") {
m_hsymStop=396;
} else if(m_mode=="WSPR-LF") {
m_hsymStop=813;
} else if(m_mode=="Echo") {
m_hsymStop=9;
} else if (m_mode=="JT4"){
m_hsymStop=176;
if(m_config.decode_at_52s()) m_hsymStop=179;
} else if (m_mode=="JT9"){
m_hsymStop=173;
if(m_config.decode_at_52s()) m_hsymStop=179;
} else if (m_mode=="JT65" or m_mode=="JT9+JT65"){
m_hsymStop=174;
if(m_config.decode_at_52s()) m_hsymStop=179;
} else if (m_mode=="QRA64"){
m_hsymStop=179;
if(m_config.decode_at_52s()) m_hsymStop=186;
} else if (m_mode=="FreqCal"){
m_hsymStop=((int(m_TRperiod/0.288))/8)*8;
} else if (m_mode=="FT8") {
m_hsymStop=50;
}
}
//-------------------------------------------------------------- dataSink()
void MainWindow::dataSink(qint64 frames)
{
static float s[NSMAX];
char line[80];
int k (frames);
QString fname {QDir::toNativeSeparators(m_config.writeable_data_dir ().absoluteFilePath ("refspec.dat"))};
QByteArray bafname = fname.toLatin1();
const char *c_fname = bafname.data();
int len=fname.length();
if(m_diskData) {
dec_data.params.ndiskdat=1;
} else {
dec_data.params.ndiskdat=0;
}
m_bUseRef=m_wideGraph->useRef();
refspectrum_(&dec_data.d2[k-m_nsps/2],&m_bClearRefSpec,&m_bRefSpec,
&m_bUseRef,c_fname,len);
m_bClearRefSpec=false;
if(m_mode=="ISCAT" or m_mode=="MSK144" or m_bFast9) {
fastSink(frames);
if(m_bFastMode) return;
}
// Get power, spectrum, and ihsym
int trmin=m_TRperiod/60;
// int k (frames - 1);
dec_data.params.nfa=m_wideGraph->nStartFreq();
dec_data.params.nfb=m_wideGraph->Fmax();
int nsps=m_nsps;
if(m_bFastMode) nsps=6912;
int nsmo=m_wideGraph->smoothYellow()-1;
symspec_(&dec_data,&k,&trmin,&nsps,&m_inGain,&nsmo,&m_px,s,&m_df3,&m_ihsym,&m_npts8,&m_pxmax);
if(m_mode=="WSPR") wspr_downsample_(dec_data.d2,&k);
if(m_ihsym <=0) return;
if(ui) ui->signal_meter_widget->setValue(m_px,m_pxmax); // Update thermometer
if(m_monitoring || m_diskData) {
m_wideGraph->dataSink2(s,m_df3,m_ihsym,m_diskData);
}
if(m_mode=="MSK144") return;
fixStop();
if (m_mode == "FreqCal"
// only calculate after 1st chunk, also skip chunk where rig
// changed frequency
&& !(m_ihsym % 8) && m_ihsym > 8 && m_ihsym <= m_hsymStop) {
int RxFreq=ui->RxFreqSpinBox->value ();
int nkhz=(m_freqNominal+RxFreq)/1000;
int ftol = ui->sbFtol->value ();
freqcal_(&dec_data.d2[0],&k,&nkhz,&RxFreq,&ftol,&line[0],80);
QString t=QString::fromLatin1(line);
DecodedText decodedtext {t, false, m_config.my_grid ()};
ui->decodedTextBrowser->displayDecodedText (decodedtext,m_baseCall,m_config.DXCC(),
m_logBook,m_config.color_CQ(),m_config.color_MyCall(),m_config.color_DXCC(),
m_config.color_NewCall(),m_config.ppfx());
if (ui->measure_check_box->isChecked ()) {
// Append results text to file "fmt.all".
QFile f {m_config.writeable_data_dir ().absoluteFilePath ("fmt.all")};
if (f.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append)) {
QTextStream out(&f);
out << t << endl;
f.close();
} else {
MessageBox::warning_message (this, tr ("File Open Error")
, tr ("Cannot open \"%1\" for append: %2")
.arg (f.fileName ()).arg (f.errorString ()));
}
}
if(m_ihsym==m_hsymStop && ui->actionFrequency_calibration->isChecked()) {
freqCalStep();
}
}
if(m_ihsym==3*m_hsymStop/4) {
m_dialFreqRxWSPR=m_freqNominal;
}
if(m_ihsym == m_hsymStop) {
if(m_mode=="Echo") {
float snr=0;
int nfrit=0;
int nqual=0;
float f1=1500.0;
float xlevel=0.0;
float sigdb=0.0;
float dfreq=0.0;
float width=0.0;
echocom_.nclearave=m_nclearave;
int nDop=0;
avecho_(dec_data.d2,&nDop,&nfrit,&nqual,&f1,&xlevel,&sigdb,
&snr,&dfreq,&width);
QString t;
t.sprintf("%3d %7.1f %7.1f %7.1f %7.1f %3d",echocom_.nsum,xlevel,sigdb,
dfreq,width,nqual);
t=DriftingDateTime::currentDateTimeUtc().toString("hh:mm:ss ") + t;
if (ui) ui->decodedTextBrowser->appendText(t);
if(m_echoGraph->isVisible()) m_echoGraph->plotSpec();
m_nclearave=0;
//Don't restart Monitor after an Echo transmission
if(m_bEchoTxed and !m_auto) {
monitor(false);
m_bEchoTxed=false;
}
return;
}
if(m_mode=="FreqCal") {
return;
}
if( m_dialFreqRxWSPR==0) m_dialFreqRxWSPR=m_freqNominal;
m_dataAvailable=true;
dec_data.params.npts8=(m_ihsym*m_nsps)/16;
dec_data.params.newdat=1;
dec_data.params.nagain=0;
dec_data.params.nzhsym=m_hsymStop;
QDateTime now {DriftingDateTime::currentDateTimeUtc ()};
m_dateTime = now.toString ("yyyy-MMM-dd hh:mm");
if(!m_mode.startsWith ("WSPR")) decode(); //Start decoder
if(!m_diskData) { //Always save; may delete later
if(m_mode=="FT8") {
int n=now.time().second() % m_TRperiod;
if(n<(m_TRperiod/2)) n=n+m_TRperiod;
auto const& period_start=now.addSecs(-n);
m_fnameWE=m_config.save_directory().absoluteFilePath (period_start.toString("yyMMdd_hhmmss"));
} else {
auto const& period_start = now.addSecs (-(now.time ().minute () % (m_TRperiod / 60)) * 60);
m_fnameWE=m_config.save_directory ().absoluteFilePath (period_start.toString ("yyMMdd_hhmm"));
}
m_fileToSave.clear ();
if(m_saveAll or m_bAltV or (m_bDecoded and m_saveDecoded) or (m_mode!="MSK144" and m_mode!="FT8")) {
m_bAltV=false;
// the following is potential a threading hazard - not a good
// idea to pass pointer to be processed in another thread
m_saveWAVWatcher.setFuture (QtConcurrent::run (std::bind (&MainWindow::save_wave_file,
this, m_fnameWE, &dec_data.d2[0], m_TRperiod, m_config.my_callsign(),
m_config.my_grid(), m_mode, m_nSubMode, m_freqNominal, m_hisCall, m_hisGrid)));
}
}
m_rxDone=true;
}
}
void MainWindow::startP1()
{
p1.start(m_cmndP1);
}
QString MainWindow::save_wave_file (QString const& name, short const * data, int seconds,
QString const& my_callsign, QString const& my_grid, QString const& mode, qint32 sub_mode,
Frequency frequency, QString const& his_call, QString const& his_grid) const
{
//
// This member function runs in a thread and should not access
// members that may be changed in the GUI thread or any other thread
// without suitable synchronization.
//
QAudioFormat format;
format.setCodec ("audio/pcm");
format.setSampleRate (12000);
format.setChannelCount (1);
format.setSampleSize (16);
format.setSampleType (QAudioFormat::SignedInt);
auto source = QString {"%1, %2"}.arg (my_callsign).arg (my_grid);
auto comment = QString {"Mode=%1%2, Freq=%3%4"}
.arg (mode)
.arg (QString {mode.contains ('J') && !mode.contains ('+')
? QString {", Sub Mode="} + QChar {'A' + sub_mode}
: QString {}})
.arg (Radio::frequency_MHz_string (frequency))
.arg (QString {!mode.startsWith ("WSPR") ? QString {", DXCall=%1, DXGrid=%2"}
.arg (his_call)
.arg (his_grid).toLocal8Bit () : ""});
BWFFile::InfoDictionary list_info {
{{{'I','S','R','C'}}, source.toLocal8Bit ()},
{{{'I','S','F','T'}}, program_title (revision ()).simplified ().toLocal8Bit ()},
{{{'I','C','R','D'}}, DriftingDateTime::currentDateTime ()
.toString ("yyyy-MM-ddTHH:mm:ss.zzzZ").toLocal8Bit ()},
{{{'I','C','M','T'}}, comment.toLocal8Bit ()},
};
auto file_name = name + ".wav";
qDebug() << "saving" << file_name;
BWFFile wav {format, file_name, list_info};
if (!wav.open (BWFFile::WriteOnly)
|| 0 > wav.write (reinterpret_cast<char const *> (data)
, sizeof (short) * seconds * format.sampleRate ()))
{
return file_name + ": " + wav.errorString ();
}
return QString {};
}
//-------------------------------------------------------------- fastSink()
void MainWindow::fastSink(qint64 frames)
{
int k (frames);
bool decodeNow=false;
if(k < m_k0) { //New sequence ?
memcpy(fast_green2,fast_green,4*703); //Copy fast_green[] to fast_green2[]
memcpy(fast_s2,fast_s,4*703*64); //Copy fast_s[] into fast_s2[]
fast_jh2=fast_jh;
if(!m_diskData) memset(dec_data.d2,0,2*30*12000); //Zero the d2[] array
m_bFastDecodeCalled=false;
m_bDecoded=false;
}
QDateTime tnow=DriftingDateTime::currentDateTimeUtc();
int ihr=tnow.toString("hh").toInt();
int imin=tnow.toString("mm").toInt();
int isec=tnow.toString("ss").toInt();
isec=isec - isec%m_TRperiod;
int nutc0=10000*ihr + 100*imin + isec;
if(m_diskData) nutc0=m_UTCdisk;
char line[80];
bool bmsk144=((m_mode=="MSK144") and (m_monitoring or m_diskData));
line[0]=0;
int RxFreq=ui->RxFreqSpinBox->value ();
int nTRpDepth=m_TRperiod + 1000*(m_ndepth & 3);
qint64 ms0 = DriftingDateTime::currentMSecsSinceEpoch();
strncpy(dec_data.params.mycall, (m_baseCall+" ").toLatin1(),12);
QString hisCall {ui->dxCallEntry->text ()};
bool bshmsg=ui->cbShMsgs->isChecked();
bool bcontest=ui->cbVHFcontest->isChecked();
bool bswl=ui->cbSWL->isChecked();
strncpy(dec_data.params.hiscall,(Radio::base_callsign (hisCall) + " ").toLatin1 ().constData (), 12);
strncpy(dec_data.params.mygrid, (m_config.my_grid()+" ").toLatin1(),6);
QString dataDir;
dataDir = m_config.writeable_data_dir ().absolutePath ();
char ddir[512];
strncpy(ddir,dataDir.toLatin1(), sizeof (ddir) - 1);
float pxmax = 0;
float rmsNoGain = 0;
int ftol = ui->sbFtol->value ();
hspec_(dec_data.d2,&k,&nutc0,&nTRpDepth,&RxFreq,&ftol,&bmsk144,&bcontest,
&m_bTrain,m_phaseEqCoefficients.constData(),&m_inGain,&dec_data.params.mycall[0],
&dec_data.params.hiscall[0],&bshmsg,&bswl,
&ddir[0],fast_green,fast_s,&fast_jh,&pxmax,&rmsNoGain,&line[0],&dec_data.params.mygrid[0],
12,12,512,80,6);
float px = fast_green[fast_jh];
QString t;
t.sprintf(" Rx noise: %5.1f ",px);
ui->signal_meter_widget->setValue(rmsNoGain,pxmax); // Update thermometer
m_fastGraph->plotSpec(m_diskData,m_UTCdisk);
if(bmsk144 and (line[0]!=0)) {
QString message {QString::fromLatin1 (line)};
DecodedText decodedtext {message.replace (QChar::LineFeed, ""), bcontest, m_config.my_grid ()};
ui->decodedTextBrowser->displayDecodedText (decodedtext,m_baseCall,m_config.DXCC(),
m_logBook,m_config.color_CQ(),m_config.color_MyCall(),m_config.color_DXCC(),
m_config.color_NewCall(),m_config.ppfx());
m_bDecoded=true;
if (m_mode != "ISCAT") postDecode (true, decodedtext.string ());
writeAllTxt(message, decodedtext.bits());
bool stdMsg = decodedtext.report(m_baseCall,
Radio::base_callsign(ui->dxCallEntry->text()),m_rptRcvd);
//if (stdMsg) pskPost (decodedtext);
}
float fracTR=float(k)/(12000.0*m_TRperiod);
decodeNow=false;
if(fracTR>0.92) {
m_dataAvailable=true;
fast_decode_done();
m_bFastDone=true;
}
m_k0=k;
if(m_diskData and m_k0 >= dec_data.params.kin - 7 * 512) decodeNow=true;
if(!m_diskData and m_tRemaining<0.35 and !m_bFastDecodeCalled) decodeNow=true;
if(m_mode=="MSK144") decodeNow=false;
if(decodeNow) {
m_dataAvailable=true;
m_t0=0.0;
m_t1=k/12000.0;
m_kdone=k;
dec_data.params.newdat=1;
if(!m_decoderBusy) {
m_bFastDecodeCalled=true;
decode();
}
}
if(decodeNow or m_bFastDone) {
if(!m_diskData) {
QDateTime now {DriftingDateTime::currentDateTimeUtc()};
int n=now.time().second() % m_TRperiod;
if(n<(m_TRperiod/2)) n=n+m_TRperiod;
auto const& period_start = now.addSecs (-n);
m_fnameWE = m_config.save_directory ().absoluteFilePath (period_start.toString ("yyMMdd_hhmmss"));
m_fileToSave.clear ();
if(m_saveAll or m_bAltV or (m_bDecoded and m_saveDecoded) or (m_mode!="MSK144" and m_mode!="FT8")) {
m_bAltV=false;
// the following is potential a threading hazard - not a good
// idea to pass pointer to be processed in another thread
m_saveWAVWatcher.setFuture (QtConcurrent::run (std::bind (&MainWindow::save_wave_file,
this, m_fnameWE, &dec_data.d2[0], m_TRperiod, m_config.my_callsign(),
m_config.my_grid(), m_mode, m_nSubMode, m_freqNominal, m_hisCall, m_hisGrid)));
}
if(m_mode!="MSK144") {
killFileTimer.start (3*1000*m_TRperiod/4); //Kill 3/4 period from now
}
}
m_bFastDone=false;
}
float tsec=0.001*(DriftingDateTime::currentMSecsSinceEpoch() - ms0);
m_fCPUmskrtd=0.9*m_fCPUmskrtd + 0.1*tsec;
}
void MainWindow::showSoundInError(const QString& errorMsg)
{
if (m_splash && m_splash->isVisible ()) m_splash->hide ();
MessageBox::critical_message (this, tr ("Error in Sound Input"), errorMsg);
}
void MainWindow::showSoundOutError(const QString& errorMsg)
{
if (m_splash && m_splash->isVisible ()) m_splash->hide ();
MessageBox::critical_message (this, tr ("Error in Sound Output"), errorMsg);
}
void MainWindow::showStatusMessage(const QString& statusMsg)
{
statusBar()->showMessage(statusMsg);
}
/**
* This function forces the menuBar to rebuild a QAction that has a submenu
* on OSX fixing a weird bug where they aren't displayed correctly.
*/
void rebuildMacQAction(QMenu *menu, QAction *existingAction){
auto dummyAction = new QAction("...", menu);
menu->insertAction(existingAction, dummyAction);
menu->insertAction(dummyAction, existingAction);
menu->removeAction(dummyAction);
}
void MainWindow::on_menuControl_aboutToShow(){
ui->actionEnable_Spotting->setChecked(ui->spotButton->isChecked());
ui->actionEnable_Auto_Reply->setChecked(ui->autoReplyButton->isChecked());
QMenu * heartbeatMenu = new QMenu(this->menuBar());
buildHeartbeatMenu(heartbeatMenu);
ui->actionHeartbeat->setMenu(heartbeatMenu);
#if __APPLE__
rebuildMacQAction(ui->menuControl, ui->actionHeartbeat);
#endif
QMenu * cqMenu = new QMenu(this->menuBar());
buildCQMenu(cqMenu);
ui->actionCQ->setMenu(cqMenu);
#if __APPLE__
rebuildMacQAction(ui->menuControl, ui->actionCQ);
#endif
}
void MainWindow::on_actionEnable_Spotting_toggled(bool checked){
ui->spotButton->setChecked(checked);
}
void MainWindow::on_actionEnable_Auto_Reply_toggled(bool checked){
ui->autoReplyButton->setChecked(checked);
}
void MainWindow::on_menuWindow_aboutToShow(){
ui->actionShow_Fullscreen->setChecked((windowState() & Qt::WindowFullScreen) == Qt::WindowFullScreen);
auto hsizes = ui->textHorizontalSplitter->sizes();
ui->actionShow_Band_Activity->setChecked(hsizes.at(0) > 0);
ui->actionShow_Call_Activity->setChecked(hsizes.at(2) > 0);
auto vsizes = ui->mainSplitter->sizes();
ui->actionShow_Frequency_Clock->setChecked(vsizes.first() > 0);
ui->actionShow_Waterfall->setChecked(vsizes.last() > 0);
ui->actionShow_Waterfall_Controls->setChecked(m_wideGraph->controlsVisible());
ui->actionShow_Waterfall_Controls->setEnabled(ui->actionShow_Waterfall->isChecked());
ui->actionShow_Time_Drift_Controls->setChecked(ui->driftSyncFrame->isVisible());
ui->actionShow_Time_Drift_Controls->setEnabled(ui->actionShow_Waterfall->isChecked());
QMenu * sortBandMenu = new QMenu(this->menuBar()); //ui->menuWindow);
buildBandActivitySortByMenu(sortBandMenu);
ui->actionSort_Band_Activity->setMenu(sortBandMenu);
ui->actionSort_Band_Activity->setEnabled(ui->actionShow_Band_Activity->isChecked());
#if __APPLE__
rebuildMacQAction(ui->menuWindow, ui->actionSort_Band_Activity);
#endif
QMenu * sortCallMenu = new QMenu(this->menuBar()); //ui->menuWindow);
buildCallActivitySortByMenu(sortCallMenu);
ui->actionSort_Call_Activity->setMenu(sortCallMenu);
ui->actionSort_Call_Activity->setEnabled(ui->actionShow_Call_Activity->isChecked());
#if __APPLE__
rebuildMacQAction(ui->menuWindow, ui->actionSort_Call_Activity);
#endif
QMenu * showBandMenu = new QMenu(this->menuBar()); //ui->menuWindow);
buildShowColumnsMenu(showBandMenu, "band");
ui->actionShow_Band_Activity_Columns->setMenu(showBandMenu);
ui->actionShow_Band_Activity_Columns->setEnabled(ui->actionShow_Band_Activity->isChecked());
#if __APPLE__
rebuildMacQAction(ui->menuWindow, ui->actionShow_Band_Activity_Columns);
#endif
QMenu * showCallMenu = new QMenu(this->menuBar()); //ui->menuWindow);
buildShowColumnsMenu(showCallMenu, "call");
ui->actionShow_Call_Activity_Columns->setMenu(showCallMenu);
ui->actionShow_Call_Activity_Columns->setEnabled(ui->actionShow_Call_Activity->isChecked());
#if __APPLE__
rebuildMacQAction(ui->menuWindow, ui->actionShow_Call_Activity_Columns);
#endif
ui->actionShow_Band_Heartbeats_and_ACKs->setChecked(!m_hbHidden);
ui->actionShow_Band_Heartbeats_and_ACKs->setEnabled(ui->actionShow_Band_Activity->isChecked());
}
void MainWindow::on_actionShow_Fullscreen_triggered(bool checked){
auto state = windowState();
if(checked){
state |= Qt::WindowFullScreen;
} else {
state &= ~Qt::WindowFullScreen;
}
setWindowState(state);
}
void MainWindow::on_actionShow_Frequency_Clock_triggered(bool checked){
auto vsizes = ui->mainSplitter->sizes();
vsizes[0] = checked ? ui->logHorizontalWidget->minimumHeight() : 0;
ui->logHorizontalWidget->setVisible(checked);
ui->mainSplitter->setSizes(vsizes);
}
void MainWindow::on_actionShow_Band_Activity_triggered(bool checked){
auto hsizes = ui->textHorizontalSplitter->sizes();
if(m_bandActivityWidth == 0){
m_bandActivityWidth = ui->textHorizontalSplitter->width()/4;
}
if(m_callActivityWidth == 0){
m_callActivityWidth = ui->textHorizontalSplitter->width()/4;
}
if(m_textActivityWidth == 0){
m_textActivityWidth = ui->textHorizontalSplitter->width()/2;
}
if(checked){
hsizes[0] = m_bandActivityWidth;
hsizes[1] = m_textActivityWidth;
if(hsizes[2]) hsizes[2] = m_callActivityWidth;
} else {
if(hsizes[0]) m_bandActivityWidth = hsizes[0];
if(hsizes[1]) m_textActivityWidth = hsizes[1];
if(hsizes[2]) m_callActivityWidth = hsizes[2];
hsizes[0] = 0;
}
ui->textHorizontalSplitter->setSizes(hsizes);
ui->tableWidgetRXAll->setVisible(checked);
m_bandActivityWasVisible = checked;
}
void MainWindow::on_actionShow_Band_Heartbeats_and_ACKs_triggered(bool checked){
m_hbHidden = !checked;
displayBandActivity();
}
void MainWindow::on_actionShow_Call_Activity_triggered(bool checked){
auto hsizes = ui->textHorizontalSplitter->sizes();
if(m_bandActivityWidth == 0){
m_bandActivityWidth = ui->textHorizontalSplitter->width()/4;
}
if(m_callActivityWidth == 0){
m_callActivityWidth = ui->textHorizontalSplitter->width()/4;
}
if(m_textActivityWidth == 0){
m_textActivityWidth = ui->textHorizontalSplitter->width()/2;
}
if(checked){
if(hsizes[0]) hsizes[0] = m_bandActivityWidth;
hsizes[1] = m_textActivityWidth;
hsizes[2] = m_callActivityWidth;
} else {
if(hsizes[0]) m_bandActivityWidth = hsizes[0];
if(hsizes[1]) m_textActivityWidth = hsizes[1];
if(hsizes[2]) m_callActivityWidth = hsizes[2];
hsizes[2] = 0;
}
ui->textHorizontalSplitter->setSizes(hsizes);
ui->tableWidgetCalls->setVisible(checked);
}
void MainWindow::on_actionShow_Waterfall_triggered(bool checked){
auto vsizes = ui->mainSplitter->sizes();
if(m_waterfallHeight == 0){
m_waterfallHeight = ui->mainSplitter->height()/4;
}
if(checked){
vsizes[vsizes.length() - 1] = m_waterfallHeight;
} else {
m_waterfallHeight = vsizes[vsizes.length() - 1];
vsizes[1] += m_waterfallHeight;
vsizes[vsizes.length() - 1] = 0;
}
ui->mainSplitter->setSizes(vsizes);
ui->bandHorizontalWidget->setVisible(checked);
}
void MainWindow::on_actionShow_Waterfall_Controls_triggered(bool checked){
m_wideGraph->setControlsVisible(checked);
}
void MainWindow::on_actionShow_Time_Drift_Controls_triggered(bool checked){
ui->driftSyncFrame->setVisible(checked);
}
void MainWindow::on_actionReset_Window_Sizes_triggered(){
auto size = this->centralWidget()->size();
ui->mainSplitter->setSizes({
ui->logHorizontalWidget->minimumHeight(),
ui->mainSplitter->height()/2,
ui->macroHorizonalWidget->minimumHeight(),
ui->mainSplitter->height()/4
});
ui->textHorizontalSplitter->setSizes({
ui->textHorizontalSplitter->width()/4,
ui->textHorizontalSplitter->width()/2,
ui->textHorizontalSplitter->width()/4
});
ui->textVerticalSplitter->setSizes({
ui->textVerticalSplitter->height()/2,
ui->textVerticalSplitter->height()/2
});
}
void MainWindow::on_actionSettings_triggered(){
openSettings();
}
void MainWindow::openSettings(int tab){
m_config.select_tab(tab);
// things that might change that we need know about
auto callsign = m_config.my_callsign ();
auto my_grid = m_config.my_grid ();
if (QDialog::Accepted == m_config.exec ()) {
if (m_config.my_callsign () != callsign) {
m_baseCall = Radio::base_callsign (m_config.my_callsign ());
morse_(const_cast<char *> (m_config.my_callsign ().toLatin1().constData()),
const_cast<int *> (icw), &m_ncw, m_config.my_callsign ().length());
}
if (m_config.my_callsign () != callsign || m_config.my_grid () != my_grid) {
statusUpdate ();
}
enable_DXCC_entity (m_config.DXCC ()); // sets text window proportions and (re)inits the logbook
prepareSpotting();
if(m_config.restart_audio_input ()) {
Q_EMIT startAudioInputStream (m_config.audio_input_device (),
m_framesAudioInputBuffered, m_detector, m_downSampleFactor,
m_config.audio_input_channel ());
}
if(m_config.restart_audio_output ()) {
Q_EMIT initializeAudioOutputStream (m_config.audio_output_device (),
AudioDevice::Mono == m_config.audio_output_channel () ? 1 : 2,
m_msAudioOutputBuffered);
}
ui->bandComboBox->view ()->setMinimumWidth (ui->bandComboBox->view ()->sizeHintForColumn (FrequencyList_v2::frequency_mhz_column));
displayDialFrequency ();
displayActivity(true);
bool vhf {m_config.enable_VHF_features()};
m_wideGraph->setVHF(vhf);
if (!vhf) ui->sbSubmode->setValue (0);
setup_status_bar (vhf);
bool b = vhf && (m_mode=="JT4" or m_mode=="JT65" or m_mode=="ISCAT" or
m_mode=="JT9" or m_mode=="MSK144" or m_mode=="QRA64");
if(b) VHF_features_enabled(b);
if(m_mode=="FT8") on_actionFT8_triggered();
if(b) VHF_features_enabled(b);
m_config.transceiver_online ();
if(!m_bFastMode) setXIT (ui->TxFreqSpinBox->value ());
if(m_config.single_decode() or m_mode=="JT4") {
ui->label_6->setText("Single-Period Decodes");
ui->label_7->setText("Average Decodes");
} else {
// ui->label_6->setText("Band Activity");
// ui->label_7->setText("Rx Frequency");
}
update_watchdog_label ();
if(!m_splitMode) ui->cbCQTx->setChecked(false);
if(!m_config.enable_VHF_features()) {
ui->actionInclude_averaging->setVisible(false);
ui->actionInclude_correlation->setVisible (false);
ui->actionInclude_averaging->setChecked(false);
ui->actionInclude_correlation->setChecked(false);
ui->actionEnable_AP_JT65->setVisible(false);
}
m_opCall=m_config.opCall();
}
}
void MainWindow::prepareSpotting(){
if(m_config.spot_to_reporting_networks ()){
pskSetLocal();
aprsSetLocal();
m_aprsClient->setServer(m_config.aprs_server_name(), m_config.aprs_server_port());
m_aprsClient->setPaused(false);
ui->spotButton->setChecked(true);
} else {
m_aprsClient->setPaused(true);
ui->spotButton->setChecked(false);
}
}
void MainWindow::on_spotButton_clicked(bool checked){
// 1. save setting
m_config.set_spot_to_reporting_networks(checked);
// 2. prepare
prepareSpotting();
}
void MainWindow::on_monitorButton_clicked (bool checked)
{
if (!m_transmitting) {
auto prior = m_monitoring;
monitor (checked);
if (checked && !prior) {
if (m_config.monitor_last_used ()) {
// put rig back where it was when last in control
setRig (m_lastMonitoredFrequency);
setXIT (ui->TxFreqSpinBox->value ());
}
// ensure FreqCal triggers
on_RxFreqSpinBox_valueChanged (ui->RxFreqSpinBox->value ());
}
//Get Configuration in/out of strict split and mode checking
Q_EMIT m_config.sync_transceiver (true, checked);
} else {
ui->monitorButton->setChecked (false); // disallow
}
}
void MainWindow::monitor (bool state)
{
ui->monitorButton->setChecked (state);
if (state) {
m_diskData = false; // no longer reading WAV files
if (!m_monitoring) Q_EMIT resumeAudioInputStream ();
} else {
Q_EMIT suspendAudioInputStream ();
}
m_monitoring = state;
}
void MainWindow::on_actionAbout_triggered() //Display "About"
{
CAboutDlg {this}.exec ();
}
void MainWindow::on_autoButton_clicked (bool checked)
{
m_auto = checked;
if (checked
&& ui->cbFirst->isVisible () && ui->cbFirst->isChecked()
&& CALLING == m_QSOProgress) {
m_bAutoReply = false; // ready for next
m_bCallingCQ = true; // allows tail-enders to be picked up
ui->cbFirst->setStyleSheet ("QCheckBox{color:red}");
} else {
ui->cbFirst->setStyleSheet("");
}
if (!checked) m_bCallingCQ = false;
statusUpdate ();
m_bEchoTxOK=false;
if(m_auto and (m_mode=="Echo")) {
m_nclearave=1;
echocom_.nsum=0;
}
if(m_mode.startsWith ("WSPR")) {
QPalette palette {ui->sbTxPercent->palette ()};
if(m_auto or m_pctx==0) {
palette.setColor(QPalette::Base,Qt::white);
} else {
palette.setColor(QPalette::Base,Qt::yellow);
}
ui->sbTxPercent->setPalette(palette);
}
m_tAutoOn=DriftingDateTime::currentMSecsSinceEpoch()/1000;
// stop tx, reset the ui and message queue
if(!checked){
on_stopTxButton_clicked();
}
}
void MainWindow::on_autoReplyButton_toggled(bool checked){
resetPushButtonToggleText(ui->autoReplyButton);
}
void MainWindow::on_monitorButton_toggled(bool checked){
resetPushButtonToggleText(ui->monitorButton);
}
void MainWindow::on_monitorTxButton_toggled(bool checked){
resetPushButtonToggleText(ui->monitorTxButton);
}
void MainWindow::on_tuneButton_toggled(bool checked){
resetPushButtonToggleText(ui->tuneButton);
}
void MainWindow::on_spotButton_toggled(bool checked){
resetPushButtonToggleText(ui->spotButton);
}
void MainWindow::auto_tx_mode (bool state)
{
ui->autoButton->setChecked (state);
on_autoButton_clicked (state);
}
void MainWindow::keyPressEvent (QKeyEvent * e)
{
switch (e->key()) {
case Qt::Key_Escape:
on_stopTxButton_clicked();
stopTx();
return;
case Qt::Key_F5:
on_logQSOButton_clicked();
return;
}
QMainWindow::keyPressEvent (e);
}
void MainWindow::bumpFqso(int n) //bumpFqso()
{
int i;
bool ctrl = (n>=100);
n=n%100;
i=ui->RxFreqSpinBox->value();
bool bTrackTx=ui->TxFreqSpinBox->value() == i;
if(n==11) i--;
if(n==12) i++;
if (ui->RxFreqSpinBox->isEnabled ()) {
ui->RxFreqSpinBox->setValue (i);
}
if(ctrl and m_mode.startsWith ("WSPR")) {
ui->WSPRfreqSpinBox->setValue(i);
} else {
if(ctrl and bTrackTx) {
ui->TxFreqSpinBox->setValue (i);
}
}
}
Radio::Frequency MainWindow::dialFrequency() {
return Frequency {m_rigState.ptt () && m_rigState.split () ?
m_rigState.tx_frequency () : m_rigState.frequency ()};
}
void MainWindow::displayDialFrequency (){
#if 0
qDebug() << "rx nominal" << m_freqNominal;
qDebug() << "tx nominal" << m_freqTxNominal;
qDebug() << "offset set to" << ui->RxFreqSpinBox->value() << ui->TxFreqSpinBox->value();
#endif
auto dial_frequency = dialFrequency();
auto audio_frequency = currentFreqOffset();
// lookup band
auto const& band_name = m_config.bands ()->find (dial_frequency);
if (m_lastBand != band_name){
cacheActivity(m_lastBand);
// only change this when necessary as we get called a lot and it
// would trash any user input to the band combo box line edit
ui->bandComboBox->setCurrentText (band_name);
m_wideGraph->setRxBand (band_name);
m_lastBand = band_name;
band_changed(dial_frequency);
clearActivity();
restoreActivity(m_lastBand);
}
// TODO: jsherer - this doesn't validate anything else right? we are disabling this because as long as you're in a band, it's valid.
/*
// search working frequencies for one we are within 10kHz of (1 Mhz
// of on VHF and up)
bool valid {false};
quint64 min_offset {99999999};
for (auto const& item : *m_config.frequencies ())
{
// we need to do specific checks for above and below here to
// ensure that we can use unsigned Radio::Frequency since we
// potentially use the full 64-bit unsigned range.
auto const& working_frequency = item.frequency_;
auto const& offset = dial_frequency > working_frequency ?
dial_frequency - working_frequency :
working_frequency - dial_frequency;
if (offset < min_offset) {
min_offset = offset;
}
}
if (min_offset < 10000u || (m_config.enable_VHF_features() && min_offset < 1000000u)) {
valid = true;
}
*/
bool valid = !band_name.isEmpty();
update_dynamic_property (ui->labDialFreq, "oob", !valid);
ui->labDialFreq->setText (Radio::pretty_frequency_MHz_string (dial_frequency));
if(m_splitMode && m_transmitting){
audio_frequency -= m_XIT;
}
ui->labDialFreqOffset->setText(QString("%1 Hz").arg(audio_frequency));
}
void MainWindow::statusChanged()
{
statusUpdate ();
}
bool MainWindow::eventFilter (QObject * object, QEvent * event)
{
switch (event->type())
{
case QEvent::KeyPress:
// fall through
case QEvent::MouseButtonPress:
// reset the Tx watchdog
resetIdleTimer();
tx_watchdog (false);
break;
case QEvent::ChildAdded:
// ensure our child widgets get added to our event filter
add_child_to_event_filter (static_cast<QChildEvent *> (event)->child ());
break;
case QEvent::ChildRemoved:
// ensure our child widgets get d=removed from our event filter
remove_child_from_event_filter (static_cast<QChildEvent *> (event)->child ());
break;
case QEvent::ToolTip:
if(!ui->actionShow_Tooltips->isChecked()){
return true;
}
break;
default: break;
}
return QObject::eventFilter(object, event);
}
void MainWindow::createStatusBar() //createStatusBar
{
tx_status_label.setAlignment (Qt::AlignHCenter);
tx_status_label.setMinimumSize (QSize {150, 18});
tx_status_label.setStyleSheet ("QLabel{background-color: #22ff22}");
tx_status_label.setFrameStyle (QFrame::Panel | QFrame::Sunken);
statusBar()->addWidget (&tx_status_label);
config_label.setAlignment (Qt::AlignHCenter);
config_label.setMinimumSize (QSize {80, 18});
config_label.setFrameStyle (QFrame::Panel | QFrame::Sunken);
statusBar()->addWidget (&config_label);
config_label.hide (); // only shown for non-default configuration
mode_label.setAlignment (Qt::AlignHCenter);
mode_label.setMinimumSize (QSize {80, 18});
mode_label.setFrameStyle (QFrame::Panel | QFrame::Sunken);
statusBar()->addWidget (&mode_label);
last_tx_label.setAlignment (Qt::AlignHCenter);
last_tx_label.setMinimumSize (QSize {150, 18});
last_tx_label.setFrameStyle (QFrame::Panel | QFrame::Sunken);
statusBar()->addWidget (&last_tx_label);
band_hopping_label.setAlignment (Qt::AlignHCenter);
band_hopping_label.setMinimumSize (QSize {90, 18});
band_hopping_label.setFrameStyle (QFrame::Panel | QFrame::Sunken);
statusBar()->addPermanentWidget(&progressBar, 1);
progressBar.setMinimumSize (QSize {100, 18});
progressBar.setFormat ("%v/%m");
statusBar()->addPermanentWidget(&wpm_label);
wpm_label.setMinimumSize (QSize {120, 18});
wpm_label.setFrameStyle (QFrame::Panel | QFrame::Sunken);
wpm_label.setAlignment(Qt::AlignHCenter);
statusBar ()->addPermanentWidget (&watchdog_label);
update_watchdog_label ();
}
void MainWindow::setup_status_bar (bool vhf)
{
auto submode = current_submode ();
if (vhf && submode != QChar::Null)
{
mode_label.setText (m_mode + " " + submode);
}
else
{
if(m_mode == "FT8"){
mode_label.setText("JS8");
} else {
mode_label.setText (m_mode);
}
}
if ("ISCAT" == m_mode) {
mode_label.setStyleSheet ("QLabel{background-color: #ff9933}");
} else if ("JT9" == m_mode) {
mode_label.setStyleSheet ("QLabel{background-color: #ff6ec7}");
} else if ("JT4" == m_mode) {
mode_label.setStyleSheet ("QLabel{background-color: #cc99ff}");
} else if ("Echo" == m_mode) {
mode_label.setStyleSheet ("QLabel{background-color: #66ffff}");
} else if ("JT9+JT65" == m_mode) {
mode_label.setStyleSheet ("QLabel{background-color: #ffff66}");
} else if ("JT65" == m_mode) {
mode_label.setStyleSheet ("QLabel{background-color: #66ff66}");
} else if ("QRA64" == m_mode) {
mode_label.setStyleSheet ("QLabel{background-color: #99ff33}");
} else if ("MSK144" == m_mode) {
mode_label.setStyleSheet ("QLabel{background-color: #ff6666}");
} else if ("FT8" == m_mode) {
mode_label.setStyleSheet ("QLabel{background-color: #6699ff}");
} else if ("FreqCal" == m_mode) {
mode_label.setStyleSheet ("QLabel{background-color: #ff9933}"); }
last_tx_label.setText (QString {});
if (m_mode.contains (QRegularExpression {R"(^(Echo|ISCAT))"})) {
if (band_hopping_label.isVisible ()) statusBar ()->removeWidget (&band_hopping_label);
} else if (m_mode.startsWith ("WSPR")) {
mode_label.setStyleSheet ("QLabel{background-color: #ff66ff}");
if (!band_hopping_label.isVisible ()) {
statusBar ()->addWidget (&band_hopping_label);
band_hopping_label.show ();
}
} else {
if (band_hopping_label.isVisible ()) statusBar ()->removeWidget (&band_hopping_label);
}
}
void MainWindow::subProcessFailed (QProcess * process, int exit_code, QProcess::ExitStatus status)
{
if (m_valid && (exit_code || QProcess::NormalExit != status))
{
QStringList arguments;
for (auto argument: process->arguments ())
{
if (argument.contains (' ')) argument = '"' + argument + '"';
arguments << argument;
}
if (m_splash && m_splash->isVisible ()) m_splash->hide ();
MessageBox::critical_message (this, tr ("Subprocess Error")
, tr ("Subprocess failed with exit code %1")
.arg (exit_code)
, tr ("Running: %1\n%2")
.arg (process->program () + ' ' + arguments.join (' '))
.arg (QString {process->readAllStandardError()}));
QTimer::singleShot (0, this, SLOT (close ()));
m_valid = false; // ensures exit if still constructing
}
}
void MainWindow::subProcessError (QProcess * process, QProcess::ProcessError)
{
if (m_valid)
{
QStringList arguments;
for (auto argument: process->arguments ())
{
if (argument.contains (' ')) argument = '"' + argument + '"';
arguments << argument;
}
if (m_splash && m_splash->isVisible ()) m_splash->hide ();
MessageBox::critical_message (this, tr ("Subprocess error")
, tr ("Running: %1\n%2")
.arg (process->program () + ' ' + arguments.join (' '))
.arg (process->errorString ()));
QTimer::singleShot (0, this, SLOT (close ()));
m_valid = false; // ensures exit if still constructing
}
}
void MainWindow::closeEvent(QCloseEvent * e)
{
m_valid = false; // suppresses subprocess errors
m_config.transceiver_offline ();
writeSettings ();
m_astroWidget.reset ();
m_guiTimer.stop ();
m_prefixes.reset ();
m_shortcuts.reset ();
m_mouseCmnds.reset ();
if(m_mode!="MSK144" and m_mode!="FT8") killFile();
float sw=0.0;
int nw=400;
int nh=100;
int irow=-99;
plotsave_(&sw,&nw,&nh,&irow);
mem_js8->detach();
QFile quitFile {m_config.temp_dir ().absoluteFilePath (".quit")};
quitFile.open(QIODevice::ReadWrite);
QFile {m_config.temp_dir ().absoluteFilePath (".lock")}.remove(); // Allow jt9 to terminate
bool b=proc_js8.waitForFinished(1000);
if(!b) proc_js8.close();
quitFile.remove();
Q_EMIT finished ();
QMainWindow::closeEvent (e);
}
void MainWindow::on_labDialFreq_clicked() //dialFrequency
{
ui->bandComboBox->setFocus();
}
void MainWindow::on_stopButton_clicked() //stopButton
{
monitor (false);
m_loopall=false;
if(m_bRefSpec) {
MessageBox::information_message (this, tr ("Reference spectrum saved"));
m_bRefSpec=false;
}
}
void MainWindow::on_actionAdd_Log_Entry_triggered(){
on_logQSOButton_clicked();
}
void MainWindow::on_actionRelease_Notes_triggered ()
{
QDesktopServices::openUrl (QUrl {"http://physics.princeton.edu/pulsar/k1jt/Release_Notes.txt"});
}
void MainWindow::on_actionFT8_DXpedition_Mode_User_Guide_triggered()
{
QDesktopServices::openUrl (QUrl {"http://physics.princeton.edu/pulsar/k1jt/FT8_DXpedition_Mode.pdf"});
}
void MainWindow::on_actionOnline_User_Guide_triggered() //Display manual
{
}
//Display local copy of manual
void MainWindow::on_actionLocal_User_Guide_triggered()
{
}
void MainWindow::on_actionWide_Waterfall_triggered() //Display Waterfalls
{
m_wideGraph->show();
}
void MainWindow::on_actionEcho_Graph_triggered()
{
m_echoGraph->show();
}
void MainWindow::on_actionFast_Graph_triggered()
{
m_fastGraph->show();
}
void MainWindow::on_actionSolve_FreqCal_triggered()
{
QString dpath{QDir::toNativeSeparators(m_config.writeable_data_dir().absolutePath()+"/")};
char data_dir[512];
int len=dpath.length();
int iz,irc;
double a,b,rms,sigmaa,sigmab;
strncpy(data_dir,dpath.toLatin1(),len);
calibrate_(data_dir,&iz,&a,&b,&rms,&sigmaa,&sigmab,&irc,len);
QString t2;
if(irc==-1) t2="Cannot open " + dpath + "fmt.all";
if(irc==-2) t2="Cannot open " + dpath + "fcal2.out";
if(irc==-3) t2="Insufficient data in fmt.all";
if(irc==-4) t2 = tr ("Invalid data in fmt.all at line %1").arg (iz);
if(irc>0 or rms>1.0) t2="Check fmt.all for possible bad data.";
if (irc < 0 || irc > 0 || rms > 1.) {
MessageBox::warning_message (this, "Calibration Error", t2);
}
else if (MessageBox::Apply == MessageBox::query_message (this
, tr ("Good Calibration Solution")
, tr ("<pre>"
"%1%L2 ±%L3 ppm\n"
"%4%L5 ±%L6 Hz\n\n"
"%7%L8\n"
"%9%L10 Hz"
"</pre>")
.arg ("Slope: ", 12).arg (b, 0, 'f', 3).arg (sigmab, 0, 'f', 3)
.arg ("Intercept: ", 12).arg (a, 0, 'f', 2).arg (sigmaa, 0, 'f', 2)
.arg ("N: ", 12).arg (iz)
.arg ("StdDev: ", 12).arg (rms, 0, 'f', 2)
, QString {}
, MessageBox::Cancel | MessageBox::Apply)) {
m_config.set_calibration (Configuration::CalibrationParams {a, b});
if (MessageBox::Yes == MessageBox::query_message (this
, tr ("Delete Calibration Measurements")
, tr ("The \"fmt.all\" file will be renamed as \"fmt.bak\""))) {
// rename fmt.all as we have consumed the resulting calibration
// solution
auto const& backup_file_name = m_config.writeable_data_dir ().absoluteFilePath ("fmt.bak");
QFile::remove (backup_file_name);
QFile::rename (m_config.writeable_data_dir ().absoluteFilePath ("fmt.all"), backup_file_name);
}
}
}
void MainWindow::on_actionCopyright_Notice_triggered()
{
auto const& message = tr("If you make fair use of any part of this program under terms of the GNU "
"General Public License, you must display the following copyright "
"notice prominently in your derivative work:\n\n"
"\"The algorithms, source code, look-and-feel of WSJT-X and related "
"programs, and protocol specifications for the modes FSK441, FT8, JT4, "
"JT6M, JT9, JT65, JTMS, QRA64, ISCAT, MSK144 are Copyright (C) "
"2001-2018 by one or more of the following authors: Joseph Taylor, "
"K1JT; Bill Somerville, G4WJS; Steven Franke, K9AN; Nico Palermo, "
"IV3NWV; Greg Beam, KI7MT; Michael Black, W9MDB; Edson Pereira, PY2SDR; "
"Philip Karn, KA9Q; and other members of the WSJT Development Group.\n\n"
"Further, the source code of JS8Call contains material Copyright (C) "
"2018 by Jordan Sherer, KN4CRD.\"");
MessageBox::warning_message(this, message);
}
// This allows the window to shrink by removing certain things
// and reducing space used by controls
void MainWindow::hideMenus(bool checked)
{
int spacing = checked ? 1 : 6;
if (checked) {
statusBar ()->removeWidget (&auto_tx_label);
minimumSize().setHeight(450);
minimumSize().setWidth(700);
restoreGeometry(m_geometryNoControls);
updateGeometry();
} else {
m_geometryNoControls = saveGeometry();
statusBar ()->addWidget(&auto_tx_label);
minimumSize().setHeight(520);
minimumSize().setWidth(770);
}
ui->menuBar->setVisible(!checked);
if(m_mode!="FreqCal" and m_mode!="WSPR") {
ui->label_6->setVisible(!checked);
ui->label_7->setVisible(!checked);
ui->decodedTextLabel2->setVisible(!checked);
// ui->line_2->setVisible(!checked);
}
// ui->line->setVisible(!checked);
ui->decodedTextLabel->setVisible(!checked);
ui->gridLayout_5->layout()->setSpacing(spacing);
ui->horizontalLayout->layout()->setSpacing(spacing);
ui->horizontalLayout_2->layout()->setSpacing(spacing);
ui->horizontalLayout_3->layout()->setSpacing(spacing);
ui->horizontalLayout_5->layout()->setSpacing(spacing);
ui->horizontalLayout_6->layout()->setSpacing(spacing);
ui->horizontalLayout_7->layout()->setSpacing(spacing);
ui->horizontalLayout_8->layout()->setSpacing(spacing);
ui->horizontalLayout_9->layout()->setSpacing(spacing);
ui->horizontalLayout_10->layout()->setSpacing(spacing);
ui->horizontalLayout_11->layout()->setSpacing(spacing);
ui->horizontalLayout_12->layout()->setSpacing(spacing);
ui->horizontalLayout_13->layout()->setSpacing(spacing);
ui->horizontalLayout_14->layout()->setSpacing(spacing);
ui->verticalLayout->layout()->setSpacing(spacing);
ui->verticalLayout_2->layout()->setSpacing(spacing);
ui->verticalLayout_3->layout()->setSpacing(spacing);
ui->verticalLayout_4->layout()->setSpacing(spacing);
ui->verticalLayout_5->layout()->setSpacing(spacing);
ui->verticalLayout_7->layout()->setSpacing(spacing);
ui->verticalLayout_8->layout()->setSpacing(spacing);
ui->tab->layout()->setSpacing(spacing);
}
void MainWindow::on_actionAstronomical_data_toggled (bool checked)
{
if (checked)
{
m_astroWidget.reset (new Astro {m_settings, &m_config});
// hook up termination signal
connect (this, &MainWindow::finished, m_astroWidget.data (), &Astro::close);
connect (m_astroWidget.data (), &Astro::tracking_update, [this] {
m_astroCorrection = {};
setRig ();
setXIT (ui->TxFreqSpinBox->value ());
displayDialFrequency ();
});
m_astroWidget->showNormal();
m_astroWidget->raise ();
m_astroWidget->activateWindow ();
m_astroWidget->nominal_frequency (m_freqNominal, m_freqTxNominal);
}
else
{
m_astroWidget.reset ();
}
}
void MainWindow::on_actionFox_Log_triggered()
{
on_actionMessage_averaging_triggered();
m_msgAvgWidget->foxLogSetup();
}
void MainWindow::on_actionMessage_averaging_triggered()
{
#if 0
if (!m_msgAvgWidget)
{
m_msgAvgWidget.reset (new MessageAveraging {m_settings, m_config.decoded_text_font ()});
// Connect signals from Message Averaging window
connect (this, &MainWindow::finished, m_msgAvgWidget.data (), &MessageAveraging::close);
}
m_msgAvgWidget->showNormal();
m_msgAvgWidget->raise ();
m_msgAvgWidget->activateWindow ();
#endif
}
void MainWindow::on_actionOpen_triggered() //Open File
{
monitor (false);
QString fname;
fname=QFileDialog::getOpenFileName(this, "Open File", m_path,
"WSJT Files (*.wav)");
if(!fname.isEmpty ()) {
m_path=fname;
int i1=fname.lastIndexOf("/");
QString baseName=fname.mid(i1+1);
tx_status_label.setStyleSheet("QLabel{background-color: #99ffff}");
tx_status_label.setText(" " + baseName + " ");
on_stopButton_clicked();
m_diskData=true;
read_wav_file (fname);
}
}
void MainWindow::read_wav_file (QString const& fname)
{
// call diskDat() when done
int i0=fname.lastIndexOf("_");
int i1=fname.indexOf(".wav");
m_nutc0=m_UTCdisk;
m_UTCdisk=fname.mid(i0+1,i1-i0-1).toInt();
m_wav_future_watcher.setFuture (QtConcurrent::run ([this, fname] {
auto basename = fname.mid (fname.lastIndexOf ('/') + 1);
auto pos = fname.indexOf (".wav", 0, Qt::CaseInsensitive);
// global variables and threads do not mix well, this needs changing
dec_data.params.nutc = 0;
if (pos > 0)
{
if (pos == fname.indexOf ('_', -11) + 7)
{
dec_data.params.nutc = fname.mid (pos - 6, 6).toInt ();
}
else
{
dec_data.params.nutc = 100 * fname.mid (pos - 4, 4).toInt ();
}
}
BWFFile file {QAudioFormat {}, fname};
bool ok=file.open (BWFFile::ReadOnly);
if(ok) {
auto bytes_per_frame = file.format ().bytesPerFrame ();
qint64 max_bytes = std::min (std::size_t (m_TRperiod * RX_SAMPLE_RATE),
sizeof (dec_data.d2) / sizeof (dec_data.d2[0]))* bytes_per_frame;
auto n = file.read (reinterpret_cast<char *> (dec_data.d2),
std::min (max_bytes, file.size ()));
int frames_read = n / bytes_per_frame;
// zero unfilled remaining sample space
std::memset(&dec_data.d2[frames_read],0,max_bytes - n);
if (11025 == file.format ().sampleRate ()) {
short sample_size = file.format ().sampleSize ();
wav12_ (dec_data.d2, dec_data.d2, &frames_read, &sample_size);
}
dec_data.params.kin = frames_read;
dec_data.params.newdat = 1;
} else {
dec_data.params.kin = 0;
dec_data.params.newdat = 0;
}
if(basename.mid(0,10)=="000000_000" && m_mode == "FT8") {
dec_data.params.nutc=15*basename.mid(10,3).toInt();
}
}));
}
void MainWindow::on_actionOpen_next_in_directory_triggered() //Open Next
{
monitor (false);
int i,len;
QFileInfo fi(m_path);
QStringList list;
list= fi.dir().entryList().filter(".wav",Qt::CaseInsensitive);
for (i = 0; i < list.size()-1; ++i) {
len=list.at(i).length();
if(list.at(i)==m_path.right(len)) {
int n=m_path.length();
QString fname=m_path.replace(n-len,len,list.at(i+1));
m_path=fname;
int i1=fname.lastIndexOf("/");
QString baseName=fname.mid(i1+1);
tx_status_label.setStyleSheet("QLabel{background-color: #99ffff}");
tx_status_label.setText(" " + baseName + " ");
m_diskData=true;
read_wav_file (fname);
if(m_loopall and (i==list.size()-2)) {
m_loopall=false;
m_bNoMoreFiles=true;
}
return;
}
}
}
//Open all remaining files
void MainWindow::on_actionDecode_remaining_files_in_directory_triggered()
{
m_loopall=true;
on_actionOpen_next_in_directory_triggered();
}
void MainWindow::diskDat() //diskDat()
{
if(dec_data.params.kin>0) {
int k;
int kstep=m_FFTSize;
m_diskData=true;
float db=m_config.degrade();
float bw=m_config.RxBandwidth();
if(db > 0.0) degrade_snr_(dec_data.d2,&dec_data.params.kin,&db,&bw);
for(int n=1; n<=m_hsymStop; n++) { // Do the waterfall spectra
k=(n+1)*kstep;
if(k > dec_data.params.kin) break;
dec_data.params.npts8=k/8;
dataSink(k);
qApp->processEvents(); //Update the waterfall
}
} else {
MessageBox::information_message(this, tr("No data read from disk. Wrong file format?"));
}
}
//Delete ../save/*.wav
void MainWindow::on_actionDelete_all_wav_files_in_SaveDir_triggered()
{
auto button = MessageBox::query_message (this, tr ("Confirm Delete"),
tr ("Are you sure you want to delete all *.wav files in \"%1\"?")
.arg (QDir::toNativeSeparators (m_config.save_directory ().absolutePath ())));
if (MessageBox::Yes == button) {
Q_FOREACH (auto const& file
, m_config.save_directory ().entryList ({"*.wav", "*.c2"}, QDir::Files | QDir::Writable)) {
m_config.save_directory ().remove (file);
}
}
}
void MainWindow::on_actionNone_triggered() //Save None
{
m_saveDecoded=false;
m_saveAll=false;
ui->actionNone->setChecked(true);
}
void MainWindow::on_actionSave_decoded_triggered()
{
m_saveDecoded=true;
m_saveAll=false;
ui->actionSave_decoded->setChecked(true);
}
void MainWindow::on_actionSave_all_triggered() //Save All
{
m_saveDecoded=false;
m_saveAll=true;
ui->actionSave_all->setChecked(true);
}
void MainWindow::on_actionKeyboard_shortcuts_triggered()
{
}
void MainWindow::on_actionSpecial_mouse_commands_triggered()
{
}
void MainWindow::on_DecodeButton_clicked (bool /* checked */) //Decode request
{
if(m_mode=="MSK144") {
ui->DecodeButton->setChecked(false);
} else {
if(!m_mode.startsWith ("WSPR") && !m_decoderBusy) {
dec_data.params.newdat=0;
dec_data.params.nagain=1;
m_blankLine=false; // don't insert the separator again
decode();
}
}
}
void MainWindow::freezeDecode(int n) //freezeDecode()
{
if((n%100)==2) on_DecodeButton_clicked (true);
}
void MainWindow::on_ClrAvgButton_clicked()
{
m_nclearave=1;
if(m_msgAvgWidget != NULL) {
if(m_msgAvgWidget->isVisible()) m_msgAvgWidget->displayAvg("");
}
}
void MainWindow::msgAvgDecode2()
{
on_DecodeButton_clicked (true);
}
void MainWindow::decode() //decode()
{
QDateTime now = DriftingDateTime::currentDateTime();
if( m_dateTimeLastTX.isValid () ) {
qint64 isecs_since_tx = m_dateTimeLastTX.secsTo(now);
dec_data.params.lapcqonly= (isecs_since_tx > 600);
// QTextStream(stdout) << "last tx " << isecs_since_tx << endl;
} else {
m_dateTimeLastTX = now.addSecs(-900);
dec_data.params.lapcqonly=true;
}
if( m_diskData ) {
dec_data.params.lapcqonly=false;
}
m_msec0=DriftingDateTime::currentMSecsSinceEpoch();
if(!m_dataAvailable or m_TRperiod==0) return;
ui->DecodeButton->setChecked (true);
if(!dec_data.params.nagain && m_diskData && !m_bFastMode && m_mode!="FT8") {
dec_data.params.nutc=dec_data.params.nutc/100;
}
if(dec_data.params.nagain==0 && dec_data.params.newdat==1 && (!m_diskData)) {
qint64 ms = DriftingDateTime::currentMSecsSinceEpoch() % 86400000;
int imin=ms/60000;
int ihr=imin/60;
imin=imin % 60;
if(m_TRperiod>=60) imin=imin - (imin % (m_TRperiod/60));
dec_data.params.nutc=100*ihr + imin;
if(m_mode=="ISCAT" or m_mode=="MSK144" or m_bFast9 or m_mode=="FT8") {
QDateTime t=DriftingDateTime::currentDateTimeUtc().addSecs(2-m_TRperiod);
ihr=t.toString("hh").toInt();
imin=t.toString("mm").toInt();
int isec=t.toString("ss").toInt();
isec=isec - isec%m_TRperiod;
dec_data.params.nutc=10000*ihr + 100*imin + isec;
}
}
if(m_nPick==1 and !m_diskData) {
QDateTime t=DriftingDateTime::currentDateTimeUtc();
int ihr=t.toString("hh").toInt();
int imin=t.toString("mm").toInt();
int isec=t.toString("ss").toInt();
isec=isec - isec%m_TRperiod;
dec_data.params.nutc=10000*ihr + 100*imin + isec;
}
if(m_nPick==2) dec_data.params.nutc=m_nutc0;
dec_data.params.nQSOProgress = m_QSOProgress;
dec_data.params.nfqso=m_wideGraph->rxFreq();
dec_data.params.nftx = ui->TxFreqSpinBox->value ();
qint32 depth {m_ndepth};
if (!ui->actionInclude_averaging->isVisible ()) depth &= ~16;
if (!ui->actionInclude_correlation->isVisible ()) depth &= ~32;
if (!ui->actionEnable_AP_DXcall->isVisible ()) depth &= ~64;
dec_data.params.ndepth=depth;
dec_data.params.n2pass=1;
if(m_config.twoPass()) dec_data.params.n2pass=2;
dec_data.params.nranera=m_config.ntrials();
dec_data.params.naggressive=m_config.aggressive();
dec_data.params.nrobust=0;
dec_data.params.ndiskdat=0;
if(m_diskData) dec_data.params.ndiskdat=1;
dec_data.params.nfa=m_wideGraph->nStartFreq();
dec_data.params.nfSplit=m_wideGraph->Fmin();
dec_data.params.nfb=m_wideGraph->Fmax();
//if(m_mode=="FT8" and m_config.bHound() and !ui->cbRxAll->isChecked()) dec_data.params.nfb=1000;
//if(m_mode=="FT8" and m_config.bFox()) dec_data.params.nfqso=200;
dec_data.params.ntol=ui->sbFtol->value ();
if(m_mode=="JT9+JT65" or !m_config.enable_VHF_features()) {
dec_data.params.ntol=20;
dec_data.params.naggressive=0;
}
if(dec_data.params.nutc < m_nutc0) m_RxLog = 1; //Date and Time to ALL.TXT
if(dec_data.params.newdat==1 and !m_diskData) m_nutc0=dec_data.params.nutc;
dec_data.params.ntxmode=9;
if(m_modeTx=="JT65") dec_data.params.ntxmode=65;
dec_data.params.nmode=9;
if(m_mode=="JT65") dec_data.params.nmode=65;
if(m_mode=="JT65") dec_data.params.ljt65apon = ui->actionEnable_AP_JT65->isVisible () && ui->actionEnable_AP_JT65->isChecked ();
if(m_mode=="QRA64") dec_data.params.nmode=164;
if(m_mode=="QRA64") dec_data.params.ntxmode=164;
if(m_mode=="JT9+JT65") dec_data.params.nmode=9+65; // = 74
if(m_mode=="JT4") {
dec_data.params.nmode=4;
dec_data.params.ntxmode=4;
}
if(m_mode=="FT8") dec_data.params.nmode=8;
if(m_mode=="FT8") dec_data.params.lft8apon = ui->actionEnable_AP_FT8->isVisible () && ui->actionEnable_AP_FT8->isChecked ();
if(m_mode=="FT8") dec_data.params.napwid=50;
dec_data.params.ntrperiod=m_TRperiod;
dec_data.params.nsubmode=m_nSubMode;
if(m_mode=="QRA64") dec_data.params.nsubmode=100 + m_nSubMode;
dec_data.params.minw=0;
dec_data.params.nclearave=m_nclearave;
if(m_nclearave!=0) {
QFile f(m_config.temp_dir ().absoluteFilePath ("avemsg.txt"));
f.remove();
}
dec_data.params.dttol=m_DTtol;
dec_data.params.emedelay=0.0;
if(m_config.decode_at_52s()) dec_data.params.emedelay=2.5;
dec_data.params.minSync=ui->syncSpinBox->isVisible () ? m_minSync : 0;
dec_data.params.nexp_decode=0;
if(m_config.single_decode()) dec_data.params.nexp_decode += 32;
if(m_config.enable_VHF_features()) dec_data.params.nexp_decode += 64;
if(ui->cbVHFcontest->isChecked()) dec_data.params.nexp_decode += 128;
strncpy(dec_data.params.datetime, m_dateTime.toLatin1(), 20);
strncpy(dec_data.params.mycall, (m_config.my_callsign()+" ").toLatin1(),12);
strncpy(dec_data.params.mygrid, (m_config.my_grid()+" ").toLatin1(),6);
QString hisCall {ui->dxCallEntry->text ()};
QString hisGrid {ui->dxGridEntry->text ()};
strncpy(dec_data.params.hiscall,(hisCall + " ").toLatin1 ().constData (), 12);
strncpy(dec_data.params.hisgrid,(hisGrid + " ").toLatin1 ().constData (), 6);
//newdat=1 ==> this is new data, must do the big FFT
//nagain=1 ==> decode only at fQSO +/- Tol
char *to = (char*)mem_js8->data();
char *from = (char*) dec_data.ss;
int size=sizeof(struct dec_data);
if(dec_data.params.newdat==0) {
int noffset {offsetof (struct dec_data, params.nutc)};
to += noffset;
from += noffset;
size -= noffset;
}
if(m_mode=="ISCAT" or m_mode=="MSK144" or m_bFast9) {
float t0=m_t0;
float t1=m_t1;
qApp->processEvents(); //Update the waterfall
if(m_nPick > 0) {
t0=m_t0Pick;
t1=m_t1Pick;
}
static short int d2b[360000];
narg[0]=dec_data.params.nutc;
if(m_kdone>12000*m_TRperiod) {
m_kdone=12000*m_TRperiod;
}
narg[1]=m_kdone;
narg[2]=m_nSubMode;
narg[3]=dec_data.params.newdat;
narg[4]=dec_data.params.minSync;
narg[5]=m_nPick;
narg[6]=1000.0*t0;
narg[7]=1000.0*t1;
narg[8]=2; //Max decode lines per decode attempt
if(dec_data.params.minSync<0) narg[8]=50;
if(m_mode=="ISCAT") narg[9]=101; //ISCAT
if(m_mode=="JT9") narg[9]=102; //Fast JT9
if(m_mode=="MSK144") narg[9]=104; //MSK144
narg[10]=ui->RxFreqSpinBox->value();
narg[11]=ui->sbFtol->value ();
narg[12]=0;
narg[13]=-1;
narg[14]=m_config.aggressive();
memcpy(d2b,dec_data.d2,2*360000);
watcher3.setFuture (QtConcurrent::run (std::bind (fast_decode_,&d2b[0],
&narg[0],&m_TRperiod,&m_msg[0][0],
dec_data.params.mycall,dec_data.params.hiscall,8000,12,12)));
} else {
memcpy(to, from, qMin(mem_js8->size(), size));
QFile {m_config.temp_dir ().absoluteFilePath (".lock")}.remove (); // Allow jt9 to start
decodeBusy(true);
}
}
void::MainWindow::fast_decode_done()
{
float t,tmax=-99.0;
dec_data.params.nagain=false;
dec_data.params.ndiskdat=false;
// if(m_msg[0][0]==0) m_bDecoded=false;
for(int i=0; m_msg[i][0] && i<100; i++) {
QString message=QString::fromLatin1(m_msg[i]);
m_msg[i][0]=0;
if(message.length()>80) message=message.left (80);
if(narg[13]/8==narg[12]) message=message.trimmed().replace("<...>",m_calls);
//Left (Band activity) window
DecodedText decodedtext {message.replace (QChar::LineFeed, ""), "FT8" == m_mode &&
ui->cbVHFcontest->isChecked(), m_config.my_grid ()};
if(!m_bFastDone) {
ui->decodedTextBrowser->displayDecodedText (decodedtext,m_baseCall,m_config.DXCC(),
m_logBook,m_config.color_CQ(),m_config.color_MyCall(),m_config.color_DXCC(),
m_config.color_NewCall(),m_config.ppfx());
}
t=message.mid(10,5).toFloat();
if(t>tmax) {
tmax=t;
m_bDecoded=true;
}
postDecode (true, decodedtext.string ());
writeAllTxt(message, decodedtext.bits());
if(m_mode=="JT9" or m_mode=="MSK144") {
// find and extract any report for myCall
bool stdMsg = decodedtext.report(m_baseCall,
Radio::base_callsign(ui->dxCallEntry->text()), m_rptRcvd);
// extract details and send to PSKreporter
//if (stdMsg) pskPost (decodedtext);
}
}
m_startAnother=m_loopall;
m_nPick=0;
ui->DecodeButton->setChecked (false);
m_bFastDone=false;
}
void MainWindow::writeAllTxt(QString message, int bits)
{
// Write decoded text to file "ALL.TXT".
QFile f {m_config.writeable_data_dir ().absoluteFilePath ("ALL.TXT")};
if (f.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append)) {
QTextStream out(&f);
if(m_RxLog==1) {
out << DriftingDateTime::currentDateTimeUtc().toString("yyyy-MM-dd hh:mm:ss")
<< " " << qSetRealNumberPrecision (12) << (m_freqNominal / 1.e6) << " MHz "
<< "JS8" << endl;
m_RxLog=0;
}
auto dt = DecodedText(message, bits);
out << dt.message() << endl;
f.close();
} else {
MessageBox::warning_message (this, tr ("File Open Error")
, tr ("Cannot open \"%1\" for append: %2")
.arg (f.fileName ()).arg (f.errorString ()));
}
}
QDateTime MainWindow::nextTransmitCycle(){
auto timestamp = DriftingDateTime::currentDateTimeUtc();
// remove milliseconds
auto t = timestamp.time();
t.setHMS(t.hour(), t.minute(), t.second());
timestamp.setTime(t);
// round to 15 second increment
int secondsSinceEpoch = (timestamp.toMSecsSinceEpoch()/1000);
int delta = roundUp(secondsSinceEpoch, 15) + 1 - secondsSinceEpoch;
timestamp = timestamp.addSecs(delta);
return timestamp;
}
void MainWindow::resetAutomaticIntervalTransmissions(bool stopCQ, bool stopHB){
resetCQTimer(stopCQ);
resetHeartbeatTimer(stopHB);
}
void MainWindow::resetCQTimer(bool stop){
if(ui->cqMacroButton->isChecked() && m_cqInterval > 0){
ui->cqMacroButton->setChecked(false);
if(!stop){
ui->cqMacroButton->setChecked(true);
}
}
}
void MainWindow::resetHeartbeatTimer(bool stop){
// toggle the heartbeat timer if we have a repeating heartbeat
if(ui->hbMacroButton->isChecked() && m_hbInterval > 0){
ui->hbMacroButton->setChecked(false);
if(!stop){
ui->hbMacroButton->setChecked(true);
}
}
}
void MainWindow::decodeDone ()
{
dec_data.params.nagain=0;
dec_data.params.ndiskdat=0;
m_nclearave=0;
QFile {m_config.temp_dir ().absoluteFilePath (".lock")}.open(QIODevice::ReadWrite);
ui->DecodeButton->setChecked (false);
decodeBusy(false);
m_RxLog=0;
m_blankLine=true;
}
void MainWindow::readFromStdout() //readFromStdout
{
while(proc_js8.canReadLine()) {
QByteArray t=proc_js8.readLine();
qDebug() << "JS8: " << QString(t);
bool bAvgMsg=false;
int navg=0;
if(t.indexOf("<DecodeFinished>") >= 0) {
if(m_mode=="QRA64") m_wideGraph->drawRed(0,0);
m_bDecoded = t.mid(20).trimmed().toInt() > 0;
int mswait=3*1000*m_TRperiod/4;
if(!m_diskData) killFileTimer.start(mswait); //Kill in 3/4 period
decodeDone ();
m_startAnother=m_loopall;
if(m_bNoMoreFiles) {
MessageBox::information_message(this, tr("No more files to open."));
m_bNoMoreFiles=false;
}
return;
} else {
if(m_mode=="JT4" or m_mode=="JT65" or m_mode=="QRA64" or m_mode=="FT8") {
int n=t.indexOf("f");
if(n<0) n=t.indexOf("d");
if(n>0) {
QString tt=t.mid(n+1,1);
navg=tt.toInt();
if(navg==0) {
char c = tt.data()->toLatin1();
if(int(c)>=65 and int(c)<=90) navg=int(c)-54;
}
if(navg>1 or t.indexOf("f*")>0) bAvgMsg=true;
}
}
QFile f {m_config.writeable_data_dir ().absoluteFilePath ("ALL.TXT")};
if (f.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append)) {
QTextStream out(&f);
if(m_RxLog==1) {
out << DriftingDateTime::currentDateTimeUtc().toString("yyyy-MM-dd hh:mm:ss")
<< " " << qSetRealNumberPrecision (12) << (m_freqNominal / 1.e6) << " MHz "
<< "JS8" << endl;
m_RxLog=0;
}
int n=t.length();
auto logText = t.mid(0, n-2);
auto dt = DecodedText(logText, false, m_config.my_grid());
out << logText << " " << dt.message() << endl;
f.close();
} else {
MessageBox::warning_message (this, tr ("File Open Error")
, tr ("Cannot open \"%1\" for append: %2")
.arg (f.fileName ()).arg (f.errorString ()));
}
DecodedText decodedtext {QString::fromUtf8 (t.constData ()).remove (QRegularExpression {"\r|\n"}), "FT8" == m_mode &&
ui->cbVHFcontest->isChecked(), m_config.my_grid ()};
bool bValidFrame = decodedtext.snr() > -24;
qDebug() << "valid" << bValidFrame << "decoded text" << decodedtext.message();
ActivityDetail d = {};
CallDetail cd = {};
CommandDetail cmd = {};
CallDetail td = {};
//Left (Band activity) window
if(bValidFrame) {
// Parse General Activity
#if 1
bool shouldParseGeneralActivity = true;
if(shouldParseGeneralActivity && !decodedtext.messageWords().isEmpty()){
int offset = decodedtext.frequencyOffset();
if(!m_bandActivity.contains(offset)){
QList<int> offsets = {
// offset - 60, offset - 61, offset - 62, offset - 63, offset - 64, offset - 65, offset - 66, offset - 67, offset - 68, offset - 69,
// offset + 60, offset + 61, offset + 62, offset + 63, offset + 64, offset + 65, offset + 66, offset + 67, offset + 68, offset + 69,
offset - 1, offset - 2, offset - 3, offset - 4, offset - 5, offset - 6, offset - 7, offset - 8, offset - 9, offset - 10,
offset + 1, offset + 2, offset + 3, offset + 4, offset + 5, offset + 6, offset + 7, offset + 8, offset + 9, offset + 10
};
foreach(int prevOffset, offsets){
if(!m_bandActivity.contains(prevOffset)){ continue; }
m_bandActivity[offset] = m_bandActivity[prevOffset];
m_bandActivity.remove(prevOffset);
break;
}
}
//ActivityDetail d = {};
d.isLowConfidence = decodedtext.isLowConfidence();
d.isFree = !decodedtext.isStandardMessage();
d.isCompound = decodedtext.isCompound();
d.isDirected = decodedtext.isDirectedMessage();
d.bits = decodedtext.bits();
d.freq = offset;
d.text = decodedtext.message();
d.utcTimestamp = DriftingDateTime::currentDateTimeUtc();
d.snr = decodedtext.snr();
d.isBuffered = false;
d.tdrift = decodedtext.dt();
// if we have any "first" frame, and a buffer is already established, clear it...
int prevBufferOffset = -1;
if(((d.bits & Varicode::JS8CallFirst) == Varicode::JS8CallFirst) && hasExistingMessageBuffer(d.freq, true, &prevBufferOffset)){
qDebug() << "first message encountered, clearing existing buffer" << prevBufferOffset;
m_messageBuffer.remove(d.freq);
}
// if we have a data frame, and a message buffer has been established, buffer it...
if(hasExistingMessageBuffer(d.freq, true, &prevBufferOffset) && !decodedtext.isCompound() && !decodedtext.isDirectedMessage()){
qDebug() << "buffering data" << d.freq << d.text;
d.isBuffered = true;
m_messageBuffer[d.freq].msgs.append(d);
// TODO: incremental display if it's "to" me.
}
m_rxActivityQueue.append(d);
m_bandActivity[offset].append(d);
while(m_bandActivity[offset].count() > 10){
m_bandActivity[offset].removeFirst();
}
}
#endif
// Process compound callsign commands (put them in cache)"
#if 1
qDebug() << "decoded" << decodedtext.frameType() << decodedtext.isCompound() << decodedtext.isDirectedMessage() << decodedtext.isHeartbeat();
bool shouldProcessCompound = true;
if(shouldProcessCompound && decodedtext.isCompound() && !decodedtext.isDirectedMessage()){
cd.call = decodedtext.compoundCall();
cd.grid = decodedtext.extra(); // compound calls via pings may contain grid...
cd.snr = decodedtext.snr();
cd.freq = decodedtext.frequencyOffset();
cd.utcTimestamp = DriftingDateTime::currentDateTimeUtc();
cd.bits = decodedtext.bits();
cd.tdrift = decodedtext.dt();
// Only respond to HEARTBEATS...remember that CQ messages are "Alt" pings
if(decodedtext.isHeartbeat()){
if(decodedtext.isAlt()){
// this is a cq with a compound call, ala "KN4CRD/P: CQCQCQ"
// it is not processed elsewhere, so we need to just log it here.
logCallActivity(cd, true);
// play cq notification
playSoundNotification(m_config.sound_cq_path());
} else {
// convert HEARTBEAT to a directed command and process...
cmd.from = cd.call;
cmd.to = "@ALLCALL";
cmd.cmd = " HB";
cmd.snr = cd.snr;
cmd.bits = cd.bits;
cmd.grid = cd.grid;
cmd.freq = cd.freq;
cmd.utcTimestamp = cd.utcTimestamp;
cmd.tdrift = cd.tdrift;
m_rxCommandQueue.append(cmd);
}
} else {
qDebug() << "buffering compound call" << cd.freq << cd.call << cd.bits;
hasExistingMessageBuffer(cd.freq, true, nullptr);
m_messageBuffer[cd.freq].compound.append(cd);
}
}
#endif
// Parse commands
// KN4CRD K1JT ?
#if 1
bool shouldProcessDirected = true;
if(shouldProcessDirected && decodedtext.isDirectedMessage()){
auto parts = decodedtext.directedMessage();
cmd.from = parts.at(0);
cmd.to = parts.at(1);
cmd.cmd = parts.at(2);
cmd.freq = decodedtext.frequencyOffset();
cmd.snr = decodedtext.snr();
cmd.utcTimestamp = DriftingDateTime::currentDateTimeUtc();
cmd.bits = decodedtext.bits();
cmd.extra = parts.length() > 2 ? parts.mid(3).join(" ") : "";
cmd.tdrift = decodedtext.dt();
// if the command is a buffered command and its not the last frame OR we have from or to in a separate message (compound call)
if((Varicode::isCommandBuffered(cmd.cmd) && (cmd.bits & Varicode::JS8CallLast) != Varicode::JS8CallLast) || cmd.from == "<....>" || cmd.to == "<....>"){
qDebug() << "buffering cmd" << cmd.freq << cmd.cmd << cmd.from << cmd.to;
// log complete buffered callsigns immediately
if(cmd.from != "<....>" && cmd.to != "<....>"){
CallDetail cmdcd = {};
cmdcd.call = cmd.from;
cmdcd.bits = cmd.bits;
cmdcd.snr = cmd.snr;
cmdcd.freq = cmd.freq;
cmdcd.utcTimestamp = cmd.utcTimestamp;
cmdcd.ackTimestamp = cmd.to == m_config.my_callsign() ? cmd.utcTimestamp : QDateTime{};
cmdcd.tdrift = cmd.tdrift;
logCallActivity(cmdcd, false);
}
hasExistingMessageBuffer(cmd.freq, true, nullptr);
if(cmd.to == m_config.my_callsign()){
d.shouldDisplay = true;
}
m_messageBuffer[cmd.freq].cmd = cmd;
m_messageBuffer[cmd.freq].msgs.clear();
} else {
m_rxCommandQueue.append(cmd);
}
// check to see if this is a station we've heard 3rd party
bool shouldCaptureThirdPartyCallsigns = false;
if(shouldCaptureThirdPartyCallsigns && Radio::base_callsign(cmd.to) != Radio::base_callsign(m_config.my_callsign())){
QString relayCall = QString("%1|%2").arg(Radio::base_callsign(cmd.from)).arg(Radio::base_callsign(cmd.to));
int snr = -100;
if(parts.length() == 4){
snr = QString(parts.at(3)).toInt();
}
//CallDetail td = {};
td.through = cmd.from;
td.call = cmd.to;
td.grid = "";
td.snr = snr;
td.freq = cmd.freq;
td.utcTimestamp = cmd.utcTimestamp;
td.tdrift = cmd.tdrift;
logCallActivity(td, true);
}
}
#endif
// Parse CQs
#if 0
bool shouldParseCQs = true;
if(shouldParseCQs && decodedtext.isStandardMessage()){
QString theircall;
QString theirgrid;
decodedtext.deCallAndGrid(theircall, theirgrid);
QStringList calls = Varicode::parseCallsigns(theircall);
if(!calls.isEmpty() && !calls.first().isEmpty()){
theircall = calls.first();
CallDetail d = {};
d.bits = decodedtext.bits();
d.call = theircall;
d.grid = theirgrid;
d.snr = decodedtext.snr();
d.freq = decodedtext.frequencyOffset();
d.utcTimestamp = DriftingDateTime::currentDateTimeUtc();
m_callActivity[d.call] = d;
}
}
#endif
// Parse standard message callsigns
// K1JT KN4CRD EM73
// KN4CRD K1JT -21
// K1JT KN4CRD R-12
// DE KN4CRD
// KN4CRD
#if 0
bool shouldParseCallsigns = false;
if(shouldParseCallsigns){
QStringList callsigns = Varicode::parseCallsigns(decodedtext.message());
if(!callsigns.isEmpty()){
// one callsign
// de [from]
// cq [from]
// two callsigns
// [from]: [to] ...
// [to] [from] [grid|signal]
QStringList grids = Varicode::parseGrids(decodedtext.message());
// one callsigns are handled above... so we only need to handle two callsigns if it's a standard message
if(decodedtext.isStandardMessage()){
if(callsigns.length() == 2){
auto de_callsign = callsigns.last();
// TODO: jsherer - put this in a function to record a callsign...
CallDetail d;
d.call = de_callsign;
d.grid = !grids.empty() ? grids.first() : "";
d.snr = decodedtext.snr();
d.freq = decodedtext.frequencyOffset();
d.utcTimestamp = DriftingDateTime::currentDateTimeUtc();
m_callActivity[Radio::base_callsign(de_callsign)] = d;
}
}
}
}
#endif
}
#if 0
//Right (Rx Frequency) window
bool bDisplayRight=bAvgMsg;
int audioFreq=decodedtext.frequencyOffset();
if(abs(audioFreq - m_wideGraph->rxFreq()) <= 10){
bDisplayRight=true;
}
if (bDisplayRight) {
// This msg is within 10 hertz of our tuned frequency, or a JT4 or JT65 avg,
// or contains MyCall
ui->decodedTextBrowser2->displayDecodedText(decodedtext,m_baseCall,false,
m_logBook,m_config.color_CQ(),m_config.color_MyCall(),
m_config.color_DXCC(),m_config.color_NewCall(),m_config.ppfx());
if(m_mode!="JT4") {
bool b65=decodedtext.isJT65();
if(b65 and m_modeTx!="JT65") on_pbTxMode_clicked();
if(!b65 and m_modeTx=="JT65") on_pbTxMode_clicked();
}
m_QSOText = decodedtext.string ().trimmed ();
}
if(m_mode!="FT8" or !m_config.bHound()) {
postDecode (true, decodedtext.string ());
// find and extract any report for myCall, but save in m_rptRcvd only if it's from DXcall
QString rpt;
bool stdMsg = decodedtext.report(m_baseCall,
Radio::base_callsign(ui->dxCallEntry->text()), rpt);
QString deCall;
QString grid;
decodedtext.deCallAndGrid(/*out*/deCall,grid);
{
QString t=Radio::base_callsign(ui->dxCallEntry->text());
if((t==deCall or t=="") and rpt!="") m_rptRcvd=rpt;
}
// extract details and send to PSKreporter
int nsec=DriftingDateTime::currentMSecsSinceEpoch()/1000-m_secBandChanged;
bool okToPost=(nsec>(4*m_TRperiod)/5);
//if (stdMsg && okToPost) pskPost(decodedtext);
if((m_mode=="JT4" or m_mode=="JT65" or m_mode=="QRA64") and m_msgAvgWidget!=NULL) {
if(m_msgAvgWidget->isVisible()) {
QFile f(m_config.temp_dir ().absoluteFilePath ("avemsg.txt"));
if(f.open(QIODevice::ReadOnly | QIODevice::Text)) {
QTextStream s(&f);
QString t=s.readAll();
m_msgAvgWidget->displayAvg(t);
}
}
}
}
#endif
}
}
// See MainWindow::postDecode for displaying the latest decodes
}
void MainWindow::playSoundNotification(const QString &path){
if(path.isEmpty()){
return;
}
qDebug() << "Trying to play sound file" << path;
QSound::play(path);
}
bool MainWindow::hasExistingMessageBuffer(int offset, bool drift, int *pPrevOffset){
if(m_messageBuffer.contains(offset)){
if(pPrevOffset) *pPrevOffset = offset;
return true;
}
QList<int> offsets = {
//offset - 60, offset - 61, offset - 62, offset - 63, offset - 64, offset - 65, offset - 66, offset - 67, offset - 68, offset - 69,
//offset + 60, offset + 61, offset + 62, offset + 63, offset + 64, offset + 65, offset + 66, offset + 67, offset + 68, offset + 69,
offset - 1, offset - 2, offset - 3, offset - 4, offset - 5, offset - 6, offset - 7, offset - 8, offset - 9, offset - 10,
offset + 1, offset + 2, offset + 3, offset + 4, offset + 5, offset + 6, offset + 7, offset + 8, offset + 9, offset + 10
};
foreach(int prevOffset, offsets){
if(!m_messageBuffer.contains(prevOffset)){ continue; }
if(drift){
m_messageBuffer[offset] = m_messageBuffer[prevOffset];
m_messageBuffer.remove(prevOffset);
}
if(pPrevOffset) *pPrevOffset = prevOffset;
return true;
}
return false;
}
void MainWindow::logCallActivity(CallDetail d, bool spot){
if(d.call.trimmed().isEmpty()){
return;
}
if(m_callActivity.contains(d.call)){
// update (keep grid)
CallDetail old = m_callActivity[d.call];
if(d.grid.isEmpty() && !old.grid.isEmpty()){
d.grid = old.grid;
}
if(!d.ackTimestamp.isValid() && old.ackTimestamp.isValid()){
d.ackTimestamp = old.ackTimestamp;
}
m_callActivity[d.call] = d;
} else {
// create
m_callActivity[d.call] = d;
}
// enqueue for spotting to psk reporter
if(spot){
m_rxCallQueue.append(d);
}
}
QString MainWindow::lookupCallInCompoundCache(QString const &call){
QString myBaseCall = Radio::base_callsign(m_config.my_callsign());
if(call == myBaseCall){
return m_config.my_callsign();
}
return m_compoundCallCache.value(call, call);
}
void MainWindow::pskLogReport(QString mode, int offset, int snr, QString callsign, QString grid){
if(!m_config.spot_to_reporting_networks()) return;
Frequency frequency = m_freqNominal + offset;
psk_Reporter->addRemoteStation(
callsign,
grid,
QString::number(frequency),
mode,
QString::number(snr),
QString::number(DriftingDateTime::currentDateTime().toTime_t()));
}
void MainWindow::aprsLogReport(int offset, int snr, QString callsign, QString grid){
if(!m_config.spot_to_reporting_networks()) return;
Frequency frequency = m_freqNominal + offset;
if(grid.length() < 6){
qDebug() << "APRSISClient Spot Skipped:" << callsign << grid;
return;
}
auto comment = QString("%1MHz %2dB").arg(Radio::frequency_MHz_string(frequency)).arg(Varicode::formatSNR(snr));
if(callsign.contains("/")){
comment = QString("%1 %2").arg(callsign).arg(comment);
}
auto base = Radio::base_callsign(callsign);
callsign = APRSISClient::replaceCallsignSuffixWithSSID(callsign, base);
if(m_aprsCallCache.contains(callsign)){
qDebug() << "APRSISClient Spot Skipped For Cache:" << callsign << grid;
return;
}
m_aprsClient->enqueueSpot(callsign, grid, comment);
m_aprsCallCache.insert(callsign, DriftingDateTime::currentDateTimeUtc());
}
void MainWindow::killFile ()
{
if (m_fnameWE.size () &&
!(m_saveAll || (m_saveDecoded && m_bDecoded) || m_fnameWE == m_fileToSave)) {
QFile f1 {m_fnameWE + ".wav"};
if(f1.exists()) f1.remove();
if(m_mode.startsWith ("WSPR")) {
QFile f2 {m_fnameWE + ".c2"};
if(f2.exists()) f2.remove();
}
}
}
void MainWindow::on_EraseButton_clicked ()
{
qint64 ms=DriftingDateTime::currentMSecsSinceEpoch();
ui->decodedTextBrowser2->erase ();
if(m_mode.startsWith ("WSPR") or m_mode=="Echo" or m_mode=="ISCAT") {
ui->decodedTextBrowser->erase ();
} else {
if((ms-m_msErase)<500) {
ui->decodedTextBrowser->erase ();
}
}
m_msErase=ms;
}
void MainWindow::decodeBusy(bool b) //decodeBusy()
{
if (!b) m_optimizingProgress.reset ();
m_decoderBusy=b;
ui->DecodeButton->setEnabled(!b);
ui->actionOpen->setEnabled(!b);
ui->actionOpen_next_in_directory->setEnabled(!b);
ui->actionDecode_remaining_files_in_directory->setEnabled(!b);
statusUpdate ();
}
//------------------------------------------------------------- //guiUpdate()
void MainWindow::guiUpdate()
{
static quint64 lastLoop;
static char message[29];
static char msgsent[29];
static int msgibits;
double txDuration;
QString rt;
quint64 thisLoop = QDateTime::currentMSecsSinceEpoch();
if(lastLoop == 0){
lastLoop = thisLoop;
}
quint64 delta = thisLoop - lastLoop;
if(delta > (100 + 10)){
qDebug() << "guiupdate overrun" << (delta-100);
}
lastLoop = thisLoop;
if(m_TRperiod==0) m_TRperiod=60;
txDuration=0.0;
if(m_modeTx=="FT8") txDuration=1.0 + NUM_FT8_SYMBOLS*1920/12000.0; // FT8
double tx1=0.0;
double tx2=txDuration;
if(m_mode=="FT8") icw[0]=0; //No CW ID in FT8 mode
if((icw[0]>0) and (!m_bFast9)) tx2 += icw[0]*2560.0/48000.0; //Full length including CW ID
if(tx2>m_TRperiod) tx2=m_TRperiod;
qint64 ms = DriftingDateTime::currentMSecsSinceEpoch() % 86400000;
int nsec=ms/1000;
double tsec=0.001*ms;
double t2p=fmod(tsec, m_TRperiod);
m_s6=fmod(tsec,6.0);
m_nseq = nsec % m_TRperiod;
m_tRemaining=m_TRperiod - fmod(tsec,double(m_TRperiod));
// how long is the tx?
m_bTxTime = (t2p >= tx1) and (t2p < tx2);
if(m_tune) m_bTxTime=true; // "Tune" and tones take precedence
if(m_transmitting or m_auto or m_tune) {
m_dateTimeLastTX = DriftingDateTime::currentDateTime ();
// Don't transmit another mode in the 30 m WSPR sub-band
Frequency onAirFreq = m_freqNominal + ui->TxFreqSpinBox->value();
//qDebug() << "transmitting on" << onAirFreq;
if ((onAirFreq > 10139900 and onAirFreq < 10140320) and
!m_mode.startsWith ("WSPR")) {
m_bTxTime=false;
if (m_auto) auto_tx_mode (false);
if(onAirFreq!=m_onAirFreq0) {
m_onAirFreq0=onAirFreq;
auto const& message = tr ("Please choose another Tx frequency."
" The app will not knowingly transmit another"
" mode in the WSPR sub-band on 30m.");
#if QT_VERSION >= 0x050400
QTimer::singleShot (0, [=] { // don't block guiUpdate
MessageBox::warning_message (this, tr ("WSPR Guard Band"), message);
});
#else
MessageBox::warning_message (this, tr ("WSPR Guard Band"), message);
#endif
}
}
// watchdog!
// if (m_config.watchdog() && !m_mode.startsWith ("WSPR")
// && m_idleMinutes >= m_config.watchdog ()) {
// tx_watchdog (true); // disable transmit
// }
float fTR=float((ms%(1000*m_TRperiod)))/(1000*m_TRperiod);
QString txMsg;
if(m_ntx == 1) txMsg=ui->tx1->text();
if(m_ntx == 2) txMsg=ui->tx2->text();
if(m_ntx == 3) txMsg=ui->tx3->text();
if(m_ntx == 4) txMsg=ui->tx4->text();
if(m_ntx == 5) txMsg=ui->tx5->currentText();
if(m_ntx == 6) txMsg=ui->tx6->text();
if(m_ntx == 7) txMsg=ui->genMsg->text();
if(m_ntx == 8) txMsg=ui->freeTextMsg->currentText();
if(m_ntx == 9) txMsg=ui->nextFreeTextMsg->text();
int msgLength=txMsg.trimmed().length();
// TODO: stop
if(msgLength==0 and !m_tune) on_stopTxButton_clicked();
// 15.0 - 12.6
if(fTR > 1.0-(2.4/15.0) && fTR < 1.0){
if(!m_deadAirTone){
qDebug() << "should start dead air tone";
m_deadAirTone = true;
}
} else {
if(m_deadAirTone){
qDebug() << "should stop dead air tone";
m_deadAirTone = false;
}
}
float lateThreshold=(2.5 - m_config.txDelay())/15.0; // 0.75;
if(g_iptt==0 and ((m_bTxTime and fTR<lateThreshold and msgLength>0) or m_tune)) {
//### Allow late starts
icw[0]=m_ncw;
g_iptt = 1;
setRig ();
setXIT (ui->TxFreqSpinBox->value ());
Q_EMIT m_config.transceiver_ptt (true); //Assert the PTT
m_tx_when_ready = true;
qDebug() << "start threshold" << fTR << lateThreshold;
}
// TODO: stop
if(!m_bTxTime and !m_tune) m_btxok=false; //Time to stop transmitting
}
// Calculate Tx tones when needed
if((g_iptt==1 && m_iptt0==0) || m_restart) {
//----------------------------------------------------------------------
QByteArray ba;
QByteArray ba0;
if(m_ntx == 1) ba=ui->tx1->text().toLocal8Bit();
if(m_ntx == 2) ba=ui->tx2->text().toLocal8Bit();
if(m_ntx == 3) ba=ui->tx3->text().toLocal8Bit();
if(m_ntx == 4) ba=ui->tx4->text().toLocal8Bit();
if(m_ntx == 5) ba=ui->tx5->currentText().toLocal8Bit();
if(m_ntx == 6) ba=ui->tx6->text().toLocal8Bit();
if(m_ntx == 7) ba=ui->genMsg->text().toLocal8Bit();
if(m_ntx == 8) ba=ui->freeTextMsg->currentText().toLocal8Bit();
if(m_ntx == 9) ba=ui->nextFreeTextMsg->text().toLocal8Bit();
ba2msg(ba,message);
int ichk=0;
if (m_lastMessageSent != m_currentMessage
|| m_lastMessageType != m_currentMessageType)
{
m_lastMessageSent = m_currentMessage;
m_lastMessageType = m_currentMessageType;
}
m_currentMessageType = 0;
if(m_tune) {
itone[0]=0;
} else if(m_modeTx=="FT8") {
bool bcontest=false;
char MyCall[6];
char MyGrid[6];
strncpy(MyCall, (m_config.my_callsign()+" ").toLatin1(),6);
strncpy(MyGrid, (m_config.my_grid()+" ").toLatin1(),6);
// 0: [000] <- this is standard set
// 1: [001] <- this is fox/hound
//m_i3bit=0;
qDebug() << "genft8" << message;
char ft8msgbits[75 + 12]; //packed 75 bit ft8 message plus 12-bit CRC
genft8_(message, MyGrid, &bcontest, &m_i3bit, msgsent, const_cast<char *> (ft8msgbits),
const_cast<int *> (itone), 22, 6, 22);
msgibits = m_i3bit;
msgsent[22]=0;
m_currentMessage = QString::fromLatin1(msgsent);
m_currentMessageBits = msgibits;
#if TEST_FOX_WAVE_GEN
if(ui->turboButton->isChecked()) {
foxcom_.nslots=1;
foxcom_.nfreq=ui->TxFreqSpinBox->value();
if(m_config.split_mode()) foxcom_.nfreq = foxcom_.nfreq - m_XIT; //Fox Tx freq
strncpy(&foxcom_.cmsg[0][0], QString::fromStdString(message).toLatin1(), 12);
foxcom_.i3bit[0] = m_i3bit | Varicode::JS8CallExtended;
int i = 1;
while(!m_txFrameQueue.isEmpty() && foxcom_.nslots < TEST_FOX_WAVE_GEN_SLOTS){
auto pair = m_txFrameQueue.dequeue();
strncpy(&foxcom_.cmsg[i][0], pair.first.toLatin1(), 12);
foxcom_.i3bit[i] = pair.second | Varicode::JS8CallExtended;
foxcom_.nslots += 1;
//m_currentMessage.append(pair.first);
//m_currentMessageBits |= pair.second;
i += 1;
}
if(i > 1){
updateTxButtonDisplay();
}
foxgen_();
}
#endif
}
m_bCallingCQ = CALLING == m_QSOProgress
|| m_currentMessage.contains (QRegularExpression {"^(CQ|QRZ) "});
if(m_mode=="FT8") {
if(m_bCallingCQ && ui->cbFirst->isVisible () && ui->cbFirst->isChecked ()) {
ui->cbFirst->setStyleSheet("QCheckBox{color:red}");
} else {
ui->cbFirst->setStyleSheet("");
}
}
if (m_tune) {
m_currentMessage = "TUNE";
m_currentMessageType = -1;
}
if(m_restart) {
write_transmit_entry ("ALL.TXT");
if (m_config.TX_messages ()) {
ui->decodedTextBrowser2->displayTransmittedText(m_currentMessage,m_modeTx,
ui->TxFreqSpinBox->value(),m_config.color_rx_background(),m_bFastMode);
}
}
auto t2 = DriftingDateTime::currentDateTimeUtc ().toString ("hhmm");
icw[0] = 0;
auto msg_parts = m_currentMessage.split (' ', QString::SkipEmptyParts);
if (msg_parts.size () > 2) {
// clean up short code forms
msg_parts[0].remove (QChar {'<'});
msg_parts[1].remove (QChar {'>'});
}
auto is_73 = m_QSOProgress >= ROGER_REPORT
&& message_is_73 (m_currentMessageType, msg_parts);
m_sentFirst73 = is_73
&& !message_is_73 (m_lastMessageType, m_lastMessageSent.split (' ', QString::SkipEmptyParts));
if (m_sentFirst73) {
m_qsoStop=t2;
if(m_config.id_after_73 ()) {
icw[0] = m_ncw;
}
if (m_config.prompt_to_log () && !m_tune) {
logQSOTimer.start (0);
}
}
bool b=(m_mode=="FT8") and ui->cbAutoSeq->isChecked() and ui->cbFirst->isChecked();
if(is_73 and (m_config.disable_TX_on_73() or b)) {
auto_tx_mode (false);
if(b) {
m_ntx=6;
ui->txrb6->setChecked(true);
m_QSOProgress = CALLING;
}
}
if(m_config.id_interval () >0) {
int nmin=(m_sec0-m_secID)/60;
if(m_sec0<m_secID) nmin=m_config.id_interval();
if(nmin >= m_config.id_interval()) {
icw[0]=m_ncw;
m_secID=m_sec0;
}
}
if ((m_currentMessageType < 6 || 7 == m_currentMessageType)
&& msg_parts.length() >= 3
&& (msg_parts[1] == m_config.my_callsign () ||
msg_parts[1] == m_baseCall))
{
int i1;
bool ok;
i1 = msg_parts[2].toInt(&ok);
if(ok and i1>=-50 and i1<50)
{
m_rptSent = msg_parts[2];
m_qsoStart = t2;
} else {
if (msg_parts[2].mid (0, 1) == "R")
{
i1 = msg_parts[2].mid (1).toInt (&ok);
if (ok and i1 >= -50 and i1 < 50)
{
m_rptSent = msg_parts[2].mid (1);
m_qsoStart = t2;
}
}
}
}
m_restart=false;
//----------------------------------------------------------------------
}
if (g_iptt == 1 && m_iptt0 == 0)
{
auto const& current_message = QString::fromLatin1 (msgsent);
if(m_config.watchdog () && !m_mode.startsWith ("WSPR")
&& current_message != m_msgSent0) {
// new messages don't reset the idle timer :|
// tx_watchdog (false); // in case we are auto sequencing
m_msgSent0 = current_message;
}
if(!m_tune) {
write_transmit_entry ("ALL.TXT");
}
// TODO: jsherer - perhaps an on_transmitting signal?
m_lastTxTime = DriftingDateTime::currentDateTimeUtc();
m_transmitting = true;
transmitDisplay (true);
statusUpdate ();
}
// TODO: stop
if(!m_btxok && m_btxok0 && g_iptt==1) stopTx();
if(m_startAnother) {
if(m_mode=="MSK144") {
m_wait++;
}
if(m_mode!="MSK144" or m_wait>=4) {
m_wait=0;
m_startAnother=false;
on_actionOpen_next_in_directory_triggered();
}
}
//Once per second:
if(nsec != m_sec0) {
if(m_freqNominal!=0 and m_freqNominal<50000000 and m_config.enable_VHF_features()) {
if(!m_bVHFwarned) vhfWarning();
} else {
m_bVHFwarned=false;
}
if(m_monitoring or m_transmitting) {
progressBar.setMaximum(m_TRperiod);
int isec=int(fmod(tsec,m_TRperiod));
progressBar.setValue(isec);
} else {
progressBar.setValue(0);
}
astroUpdate ();
if(m_transmitting) {
char s[41];
auto dt = DecodedText(msgsent, msgibits);
sprintf(s,"Tx: %s", dt.message().toLocal8Bit().mid(0, 41).data());
m_nsendingsh=0;
if(s[4]==64) m_nsendingsh=1;
if(m_nsendingsh==1 or m_currentMessageType==7) {
tx_status_label.setStyleSheet("QLabel{background-color: #ff2222; color:#000; }");
} else if(m_nsendingsh==-1 or m_currentMessageType==6) {
tx_status_label.setStyleSheet("QLabel{background-color: #ff2222; color:#000; }");
} else {
tx_status_label.setStyleSheet("QLabel{background-color: #ff2222; color:#000; }");
}
if(m_tune) {
tx_status_label.setText("Tx: TUNE");
} else {
if(m_mode=="Echo") {
tx_status_label.setText("Tx: ECHO");
} else {
s[40]=0;
QString t{QString::fromLatin1(s)};
tx_status_label.setText(t.trimmed());
}
}
} else if(m_monitoring) {
if (m_tx_watchdog) {
tx_status_label.setStyleSheet ("QLabel{background-color: #000000; color:#ffffff}");
tx_status_label.setText ("Inactive watchdog");
} else {
tx_status_label.setStyleSheet("QLabel{background-color: #22ff22}");
QString t;
t="Receiving";
if(m_mode=="MSK144") {
int npct=int(100.0*m_fCPUmskrtd/0.298667);
if(npct>90) tx_status_label.setStyleSheet("QLabel{background-color: #ff2222; color:#000; }");
t.sprintf("Receiving %2d%%",npct);
}
tx_status_label.setText (t);
}
transmitDisplay(false);
} else if (!m_diskData && !m_tx_watchdog) {
tx_status_label.setStyleSheet("");
tx_status_label.setText("");
}
auto drift = DriftingDateTime::drift();
QDateTime t = DriftingDateTime::currentDateTimeUtc();
QStringList parts;
parts << (t.time().toString() + (!drift ? " " : QString(" (%1%2ms)").arg(drift > 0 ? "+" : "").arg(drift)));
parts << t.date().toString("yyyy MMM dd");
ui->labUTC->setText(parts.join("\n"));
#if 0
auto delta = t.secsTo(m_nextHeartbeat);
QString ping;
if(heartbeatTimer.isActive()){
if(delta > 0){
ping = QString("%1 s").arg(delta);
} else {
ping = "queued!";
}
} else if (m_nextHeartPaused) {
ping = "paused";
} else {
ping = "on demand";
}
ui->labHeartbeat->setText(QString("Next Heartbeat: %1").arg(ping));
#endif
auto callLabel = m_config.my_callsign();
if(m_config.use_dynamic_grid() && !m_config.my_grid().isEmpty()){
callLabel = QString("%1 - %2").arg(callLabel).arg(m_config.my_grid());
}
ui->labCallsign->setText(callLabel);
if(!m_monitoring and !m_diskData) {
ui->signal_meter_widget->setValue(0,0);
}
m_sec0=nsec;
// once per period
if(m_sec0 % m_TRperiod == 0){
tryBandHop();
}
// at the end of the period
bool forceDirty = false;
if(m_sec0 % (m_TRperiod-2) == 0 ||
m_sec0 % (m_TRperiod) == 0 ||
m_sec0 % (m_TRperiod+2) == 0 ){
// force rx dirty at the end of the period
forceDirty = true;
}
// update the dial frequency once per second..
displayDialFrequency();
// update repeat button text once per second..
updateRepeatButtonDisplay();
// once per second...but not when we're transmitting, unless it's in the first second...
if(!m_transmitting || (m_sec0 % (m_TRperiod))){
// process all received activity...
processActivity(forceDirty);
// process outgoing tx queue...
processTxQueue();
// once processed, lets update the display...
displayActivity(forceDirty);
updateButtonDisplay();
updateTextDisplay();
}
}
// once per 100ms
displayTransmit();
m_iptt0=g_iptt;
m_btxok0=m_btxok;
// compute the processing time and adjust loop to hit the next 100ms
auto endLoop = QDateTime::currentMSecsSinceEpoch();
auto processingTime = endLoop - thisLoop;
auto nextLoopMs = 0;
if(processingTime < 100){
nextLoopMs = 100 - processingTime;
}
m_guiTimer.start(nextLoopMs);
} //End of guiUpdate
void MainWindow::startTx()
{
#if IDLE_BLOCKS_TX
if(m_tx_watchdog){
return;
}
#endif
if(!prepareNextMessageFrame()){
return;
}
m_ntx=9;
m_QSOProgress = CALLING;
set_dateTimeQSO(-1);
ui->rbNextFreeTextMsg->setChecked(true);
if (m_transmitting) m_restart=true;
// hack the auto button to kick off the transmit
if(!ui->autoButton->isChecked()){
ui->autoButton->setEnabled(true);
ui->autoButton->click();
ui->autoButton->setEnabled(false);
}
// disallow editing of the text while transmitting
ui->extFreeTextMsgEdit->setReadOnly(true);
update_dynamic_property(ui->extFreeTextMsgEdit, "transmitting", true);
// update the tx button display
updateTxButtonDisplay();
}
void MainWindow::startTx2()
{
if (!m_modulator->isActive ()) { // TODO - not thread safe
double fSpread=0.0;
double snr=99.0;
QString t=ui->tx5->currentText();
if(t.mid(0,1)=="#") fSpread=t.mid(1,5).toDouble();
m_modulator->setSpread(fSpread); // TODO - not thread safe
t=ui->tx6->text();
if(t.mid(0,1)=="#") snr=t.mid(1,5).toDouble();
if(snr>0.0 or snr < -50.0) snr=99.0;
transmit (snr);
ui->signal_meter_widget->setValue(0,0);
}
}
void MainWindow::stopTx()
{
Q_EMIT endTransmitMessage ();
auto dt = DecodedText(m_currentMessage.trimmed(), m_currentMessageBits);
last_tx_label.setText("Last Tx: " + dt.message()); //m_currentMessage.trimmed());
m_btxok = false;
m_transmitting = false;
g_iptt=0;
if (!m_tx_watchdog) {
tx_status_label.setStyleSheet("");
tx_status_label.setText("");
}
#if IDLE_BLOCKS_TX
bool shouldContinue = !m_tx_watchdog && prepareNextMessageFrame();
#else
bool shouldContinue = prepareNextMessageFrame();
#endif
if(!shouldContinue){
// TODO: jsherer - split this up...
ui->extFreeTextMsgEdit->setReadOnly(false);
update_dynamic_property(ui->extFreeTextMsgEdit, "transmitting", false);
on_stopTxButton_clicked();
tryRestoreFreqOffset();
}
ptt0Timer.start(200); //end-of-transmission sequencer delay stopTx2
monitor (true);
statusUpdate ();
}
void MainWindow::stopTx2()
{
Q_EMIT m_config.transceiver_ptt (false); //Lower PTT
}
void MainWindow::ba2msg(QByteArray ba, char message[]) //ba2msg()
{
int iz=ba.length();
for(int i=0;i<28; i++) {
if(i<iz) {
message[i]=ba[i];
} else {
message[i]=32;
}
}
message[28]=0;
}
void MainWindow::set_dateTimeQSO(int m_ntx)
{
// m_ntx = -1 resets to default time
// Our QSO start time can be fairly well determined from Tx 2 and Tx 3 -- the grid reports
// If we CQ'd and sending sigrpt then 2 minutes ago n=2
// If we're on msg 3 then 3 minutes ago n=3 -- might have sat on msg1 for a while
// If we've already set our time on just return.
// This should mean that Tx2 or Tx3 has been repeated so don't update the start time
// We reset it in several places
if (m_ntx == -1) { // we use a default date to detect change
m_dateTimeQSOOn = QDateTime {};
}
else if (m_dateTimeQSOOn.isValid ()) {
return;
}
else { // we also take of m_TRperiod/2 to allow for late clicks
auto now = DriftingDateTime::currentDateTimeUtc();
m_dateTimeQSOOn = now.addSecs (-(m_ntx - 2) * m_TRperiod - (now.time ().second () % m_TRperiod));
}
}
void MainWindow::set_ntx(int n) //set_ntx()
{
m_ntx=n;
}
void MainWindow::on_txrb1_toggled (bool status)
{
if (status) {
if (ui->tx1->isEnabled ()) {
m_ntx = 1;
set_dateTimeQSO (-1); // we reset here as tx2/tx3 is used for start times
}
else {
QTimer::singleShot (0, ui->txrb2, SLOT (click ()));
}
}
}
void MainWindow::on_txrb1_doubleClicked ()
{
if(m_mode=="FT8" and m_config.bHound()) return;
// skip Tx1, only allowed if not a type 2 compound callsign
auto const& my_callsign = m_config.my_callsign ();
auto is_compound = my_callsign != m_baseCall;
ui->tx1->setEnabled ((is_compound && shortList (my_callsign)) || !ui->tx1->isEnabled ());
if (!ui->tx1->isEnabled ()) {
// leave time for clicks to complete before setting txrb2
QTimer::singleShot (500, ui->txrb2, SLOT (click ()));
}
}
void MainWindow::on_txrb2_toggled (bool status)
{
// Tx 2 means we already have CQ'd so good reference
if (status) {
m_ntx = 2;
set_dateTimeQSO (m_ntx);
}
}
void MainWindow::on_txrb3_toggled(bool status)
{
// Tx 3 means we should havel already have done Tx 1 so good reference
if (status) {
m_ntx=3;
set_dateTimeQSO(m_ntx);
}
}
void MainWindow::on_txrb4_toggled (bool status)
{
if (status) {
m_ntx=4;
}
}
void MainWindow::on_txrb4_doubleClicked ()
{
}
void MainWindow::on_txrb5_toggled (bool status)
{
if (status) {
m_ntx = 5;
}
}
void MainWindow::on_txrb5_doubleClicked ()
{
}
void MainWindow::on_txrb6_toggled(bool status)
{
if (status) {
m_ntx=6;
if (ui->txrb6->text().contains (QRegularExpression {"^(CQ|QRZ) "})) set_dateTimeQSO(-1);
}
}
void MainWindow::on_txb1_clicked()
{
if (ui->tx1->isEnabled ()) {
m_ntx=1;
m_QSOProgress = REPLYING;
ui->txrb1->setChecked(true);
if (m_transmitting) m_restart=true;
}
else {
on_txb2_clicked ();
}
}
void MainWindow::on_txb1_doubleClicked()
{
if(m_mode=="FT8" and m_config.bHound()) return;
// skip Tx1, only allowed if not a type 1 compound callsign
auto const& my_callsign = m_config.my_callsign ();
auto is_compound = my_callsign != m_baseCall;
ui->tx1->setEnabled ((is_compound && shortList (my_callsign)) || !ui->tx1->isEnabled ());
}
void MainWindow::on_txb2_clicked()
{
m_ntx=2;
m_QSOProgress = REPORT;
ui->txrb2->setChecked(true);
if (m_transmitting) m_restart=true;
}
void MainWindow::on_txb3_clicked()
{
m_ntx=3;
m_QSOProgress = ROGER_REPORT;
ui->txrb3->setChecked(true);
if (m_transmitting) m_restart=true;
}
void MainWindow::on_txb4_clicked()
{
m_ntx=4;
m_QSOProgress = ROGERS;
ui->txrb4->setChecked(true);
if (m_transmitting) m_restart=true;
}
void MainWindow::on_txb4_doubleClicked()
{
}
void MainWindow::on_txb5_clicked()
{
m_ntx=5;
m_QSOProgress = SIGNOFF;
ui->txrb5->setChecked(true);
if (m_transmitting) m_restart=true;
}
void MainWindow::on_txb5_doubleClicked()
{
}
void MainWindow::on_txb6_clicked()
{
m_ntx=6;
m_QSOProgress = CALLING;
set_dateTimeQSO(-1);
ui->txrb6->setChecked(true);
if (m_transmitting) m_restart=true;
}
void MainWindow::TxAgain()
{
auto_tx_mode(true);
}
void MainWindow::clearDX ()
{
if (m_QSOProgress != CALLING)
{
auto_tx_mode (false);
}
ui->dxCallEntry->clear ();
ui->dxGridEntry->clear ();
m_lastCallsign.clear ();
m_rptSent.clear ();
m_rptRcvd.clear ();
m_qsoStart.clear ();
m_qsoStop.clear ();
if (ui->tabWidget->currentIndex() == 1) {
ui->genMsg->setText(ui->tx6->text());
m_ntx=7;
m_gen_message_is_cq = true;
ui->rbGenMsg->setChecked(true);
} else {
if(m_mode=="FT8" and m_config.bHound()) {
m_ntx=1;
ui->txrb1->setChecked(true);
} else {
m_ntx=6;
ui->txrb6->setChecked(true);
}
}
m_QSOProgress = CALLING;
}
void MainWindow::lookup() //lookup()
{
QString hisCall {ui->dxCallEntry->text()};
if (!hisCall.size ()) return;
QFile f {m_config.writeable_data_dir ().absoluteFilePath ("CALL3.TXT")};
if (f.open (QIODevice::ReadOnly | QIODevice::Text))
{
char c[132];
qint64 n=0;
for(int i=0; i<999999; i++) {
n=f.readLine(c,sizeof(c));
if(n <= 0) {
ui->dxGridEntry->clear ();
break;
}
QString t=QString(c);
if(t.indexOf(hisCall)==0) {
int i1=t.indexOf(",");
QString hisgrid=t.mid(i1+1,6);
i1=hisgrid.indexOf(",");
if(i1>0) {
hisgrid=hisgrid.mid(0,4);
} else {
hisgrid=hisgrid.mid(0,4) + hisgrid.mid(4,2).toLower();
}
ui->dxGridEntry->setText(hisgrid);
break;
}
}
f.close();
}
}
void MainWindow::on_lookupButton_clicked() //Lookup button
{
lookup();
}
void MainWindow::on_addButton_clicked() //Add button
{
if(!ui->dxGridEntry->text ().size ()) {
MessageBox::warning_message (this, tr ("Add to CALL3.TXT")
, tr ("Please enter a valid grid locator"));
return;
}
m_call3Modified=false;
QString hisCall=ui->dxCallEntry->text();
QString hisgrid=ui->dxGridEntry->text();
QString newEntry=hisCall + "," + hisgrid;
// int ret = MessageBox::query_message(this, tr ("Add to CALL3.TXT"),
// tr ("Is %1 known to be active on EME?").arg (newEntry));
// if(ret==MessageBox::Yes) {
// newEntry += ",EME,,";
// } else {
newEntry += ",,,";
// }
QFile f1 {m_config.writeable_data_dir ().absoluteFilePath ("CALL3.TXT")};
if(!f1.open(QIODevice::ReadWrite | QIODevice::Text)) {
MessageBox::warning_message (this, tr ("Add to CALL3.TXT")
, tr ("Cannot open \"%1\" for read/write: %2")
.arg (f1.fileName ()).arg (f1.errorString ()));
return;
}
if(f1.size()==0) {
QTextStream out(&f1);
out << "ZZZZZZ" << endl;
f1.close();
f1.open(QIODevice::ReadOnly | QIODevice::Text);
}
QFile f2 {m_config.writeable_data_dir ().absoluteFilePath ("CALL3.TMP")};
if(!f2.open(QIODevice::WriteOnly | QIODevice::Text)) {
MessageBox::warning_message (this, tr ("Add to CALL3.TXT")
, tr ("Cannot open \"%1\" for writing: %2")
.arg (f2.fileName ()).arg (f2.errorString ()));
return;
}
QTextStream in(&f1); //Read from CALL3.TXT
QTextStream out(&f2); //Copy into CALL3.TMP
QString hc=hisCall;
QString hc1="";
QString hc2="000000";
QString s;
do {
s=in.readLine();
hc1=hc2;
if(s.mid(0,2)=="//") {
out << s + QChar::LineFeed; //Copy all comment lines
} else {
int i1=s.indexOf(",");
hc2=s.mid(0,i1);
if(hc>hc1 && hc<hc2) {
out << newEntry + QChar::LineFeed;
out << s + QChar::LineFeed;
m_call3Modified=true;
} else if(hc==hc2) {
QString t {tr ("%1\nis already in CALL3.TXT"
", do you wish to replace it?").arg (s)};
int ret = MessageBox::query_message (this, tr ("Add to CALL3.TXT"), t);
if(ret==MessageBox::Yes) {
out << newEntry + QChar::LineFeed;
m_call3Modified=true;
}
} else {
if(s!="") out << s + QChar::LineFeed;
}
}
} while(!s.isNull());
f1.close();
if(hc>hc1 && !m_call3Modified) out << newEntry + QChar::LineFeed;
if(m_call3Modified) {
QFile f0 {m_config.writeable_data_dir ().absoluteFilePath ("CALL3.OLD")};
if(f0.exists()) f0.remove();
QFile f1 {m_config.writeable_data_dir ().absoluteFilePath ("CALL3.TXT")};
f1.rename(m_config.writeable_data_dir ().absoluteFilePath ("CALL3.OLD"));
f2.rename(m_config.writeable_data_dir ().absoluteFilePath ("CALL3.TXT"));
f2.close();
}
}
void MainWindow::msgtype(QString t, QLineEdit* tx) //msgtype()
{
char message[29];
char msgsent[29];
int itone0[NUM_ISCAT_SYMBOLS]; //Dummy array, data not used
int len1=22;
QByteArray s=t.toUpper().toLocal8Bit();
ba2msg(s,message);
int ichk=1,itype=0;
gen65_(message,&ichk,msgsent,itone0,&itype,len1,len1);
msgsent[22]=0;
bool text=false;
bool shortMsg=false;
if(itype==6) text=true;
if(itype==7 and m_config.enable_VHF_features() and
m_mode=="JT65") shortMsg=true;
if(m_mode=="MSK144" and t.mid(0,1)=="<") text=false;
if((m_mode=="MSK144" or m_mode=="FT8") and ui->cbVHFcontest->isChecked()) {
int i0=t.trimmed().length()-7;
if(t.mid(i0,3)==" R ") text=false;
}
QPalette p(tx->palette());
if(text) {
p.setColor(QPalette::Base,"#ffccff");
} else {
if(shortMsg) {
p.setColor(QPalette::Base,"#66ffff");
} else {
p.setColor(QPalette::Base,Qt::transparent);
if(m_mode=="MSK144" and t.mid(0,1)=="<") {
p.setColor(QPalette::Base,"#00ffff");
}
}
}
tx->setPalette(p);
auto pos = tx->cursorPosition ();
tx->setText(t.toUpper());
tx->setCursorPosition (pos);
}
void MainWindow::on_tx1_editingFinished() //tx1 edited
{
QString t=ui->tx1->text();
msgtype(t, ui->tx1);
}
void MainWindow::on_tx2_editingFinished() //tx2 edited
{
QString t=ui->tx2->text();
msgtype(t, ui->tx2);
}
void MainWindow::on_tx3_editingFinished() //tx3 edited
{
QString t=ui->tx3->text();
msgtype(t, ui->tx3);
}
void MainWindow::on_tx4_editingFinished() //tx4 edited
{
QString t=ui->tx4->text();
msgtype(t, ui->tx4);
}
void MainWindow::on_tx5_currentTextChanged (QString const& text) //tx5 edited
{
msgtype(text, ui->tx5->lineEdit ());
}
void MainWindow::on_tx6_editingFinished() //tx6 edited
{
QString t=ui->tx6->text().toUpper();
if(t.indexOf(" ")>0) {
QString t1=t.split(" ").at(1);
m_CQtype="CQ";
if(t1.size()==2) m_CQtype="CQ " + t1;
}
msgtype(t, ui->tx6);
}
void MainWindow::cacheActivity(QString key){
m_callActivityCache[key] = m_callActivity;
m_bandActivityCache[key] = m_bandActivity;
m_rxTextCache[key] = ui->textEditRX->toHtml();
}
void MainWindow::restoreActivity(QString key){
if(m_callActivityCache.contains(key)){
m_callActivity = m_callActivityCache[key];
}
if(m_bandActivityCache.contains(key)){
m_bandActivity = m_bandActivityCache[key];
}
if(m_rxTextCache.contains(key)){
ui->textEditRX->setHtml(m_rxTextCache[key]);
}
displayActivity(true);
}
void MainWindow::clearActivity(){
m_bandActivity.clear();
m_callActivity.clear();
m_callSeenHeartbeat.clear();
m_compoundCallCache.clear();
m_rxCallCache.clear();
m_rxCallQueue.clear();
m_rxRecentCache.clear();
m_rxDirectedCache.clear();
m_rxFrameBlockNumbers.clear();
m_rxActivityQueue.clear();
m_rxCommandQueue.clear();
m_lastTxMessage.clear();
resetTimeDeltaAverage();
clearTableWidget(ui->tableWidgetCalls);
createAllcallTableRows(ui->tableWidgetCalls, "");
clearTableWidget(ui->tableWidgetRXAll);
ui->textEditRX->clear();
ui->freeTextMsg->clear();
ui->extFreeTextMsg->clear();
// make sure to clear the read only and transmitting flags so there's always a "way out"
ui->extFreeTextMsgEdit->clear();
ui->extFreeTextMsgEdit->setReadOnly(false);
update_dynamic_property(ui->extFreeTextMsgEdit, "transmitting", false);
displayActivity(true);
}
void MainWindow::createAllcallTableRows(QTableWidget *table, QString const &selectedCall){
int count = 0;
auto now = DriftingDateTime::currentDateTimeUtc();
int callsignAging = m_config.callsign_aging();
int startCol = 1;
if(!m_config.avoid_allcall())
{
table->insertRow(table->rowCount());
foreach(auto cd, m_callActivity.values()){
if (cd.call.trimmed().isEmpty()){
continue;
}
if (callsignAging && cd.utcTimestamp.secsTo(now) / 60 >= callsignAging) {
continue;
}
count++;
}
auto emptyItem = new QTableWidgetItem("");
emptyItem->setData(Qt::UserRole, QVariant("@ALLCALL"));
table->setItem(table->rowCount() - 1, 0, emptyItem);
auto item = new QTableWidgetItem(count == 0 ? QString("@ALLCALL") : QString("@ALLCALL (%1)").arg(count));
item->setData(Qt::UserRole, QVariant("@ALLCALL"));
table->setItem(table->rowCount() - 1, startCol, item);
table->setSpan(table->rowCount() - 1, startCol, 1, table->columnCount());
if(selectedCall == "@ALLCALL"){
table->item(table->rowCount()-1, 0)->setSelected(true);
table->item(table->rowCount()-1, startCol)->setSelected(true);
}
}
auto groups = m_config.my_groups().toList();
qSort(groups);
foreach(auto group, groups){
table->insertRow(table->rowCount());
auto emptyItem = new QTableWidgetItem("");
emptyItem->setData(Qt::UserRole, QVariant(group));
table->setItem(table->rowCount() - 1, 0, emptyItem);
auto item = new QTableWidgetItem(group);
item->setData(Qt::UserRole, QVariant(group));
table->setItem(table->rowCount() - 1, startCol, item);
table->setSpan(table->rowCount() - 1, startCol, 1, table->columnCount());
if(selectedCall == group){
table->item(table->rowCount()-1, 0)->setSelected(true);
table->item(table->rowCount()-1, startCol)->setSelected(true);
}
}
}
void MainWindow::displayTextForFreq(QString text, int freq, QDateTime date, bool isTx, bool isNewLine, bool isLast){
int lowFreq = freq/10*10;
int highFreq = lowFreq + 10;
int block = -1;
if(m_rxFrameBlockNumbers.contains(freq)){
block = m_rxFrameBlockNumbers[freq];
} else if(m_rxFrameBlockNumbers.contains(lowFreq)){
block = m_rxFrameBlockNumbers[lowFreq];
freq = lowFreq;
} else if(m_rxFrameBlockNumbers.contains(highFreq)){
block = m_rxFrameBlockNumbers[highFreq];
freq = highFreq;
}
if(isNewLine){
m_rxFrameBlockNumbers.remove(freq);
m_rxFrameBlockNumbers.remove(lowFreq);
m_rxFrameBlockNumbers.remove(highFreq);
block = -1;
}
block = writeMessageTextToUI(date, text, freq, isTx, block);
// never cache tx or last lines
if(isTx || isLast) {
// reset the cache so we're always progressing forward
m_rxFrameBlockNumbers.clear();
} else {
m_rxFrameBlockNumbers.insert(freq, block);
m_rxFrameBlockNumbers.insert(lowFreq, block);
m_rxFrameBlockNumbers.insert(highFreq, block);
}
}
void MainWindow::writeNoticeTextToUI(QDateTime date, QString text){
auto c = ui->textEditRX->textCursor();
c.movePosition(QTextCursor::End);
if(c.block().length() > 1){
c.insertBlock();
}
text = text.toHtmlEscaped();
c.insertBlock();
c.insertHtml(QString("<strong>%1 - %2</strong>").arg(date.time().toString()).arg(text));
c.movePosition(QTextCursor::End);
ui->textEditRX->ensureCursorVisible();
ui->textEditRX->verticalScrollBar()->setValue(ui->textEditRX->verticalScrollBar()->maximum());
}
int MainWindow::writeMessageTextToUI(QDateTime date, QString text, int freq, bool bold, int block){
auto c = ui->textEditRX->textCursor();
// find an existing block (that does not contain an EOT marker)
bool found = false;
if(block != -1){
QTextBlock b = c.document()->findBlockByNumber(block);
c.setPosition(b.position());
c.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
auto blockText = c.selectedText();
c.clearSelection();
c.movePosition(QTextCursor::EndOfBlock, QTextCursor::MoveAnchor);
if(!blockText.contains("\u2301")){
found = true;
}
}
if(!found){
c.movePosition(QTextCursor::End);
if(c.block().length() > 1){
c.insertBlock();
}
}
if(found && !bold){
c.clearSelection();
c.insertText(text);
} else {
text = text.toHtmlEscaped();
text = text.replace(" ", "&nbsp;&nbsp;");
if(bold){
text = QString("<strong>%1</strong>").arg(text);
}
c.insertBlock();
c.insertHtml(QString("<strong>%1 - (%2)</strong> - %3").arg(date.time().toString()).arg(freq).arg(text));
}
if(bold){
c.block().setUserState(STATE_TX);
highlightBlock(c.block(), m_config.tx_text_font(), m_config.color_tx_foreground(), QColor(Qt::transparent));
} else {
c.block().setUserState(STATE_RX);
highlightBlock(c.block(), m_config.rx_text_font(), m_config.color_rx_foreground(), QColor(Qt::transparent));
}
ui->textEditRX->ensureCursorVisible();
ui->textEditRX->verticalScrollBar()->setValue(ui->textEditRX->verticalScrollBar()->maximum());
return c.blockNumber();
}
bool MainWindow::isMessageQueuedForTransmit(){
return m_transmitting || m_txFrameCount > 0;
}
void MainWindow::prependMessageText(QString text){
// don't add message text if we already have a transmission queued...
if(isMessageQueuedForTransmit()){
return;
}
auto c = QTextCursor(ui->extFreeTextMsgEdit->textCursor());
c.movePosition(QTextCursor::Start);
c.insertText(text);
}
void MainWindow::addMessageText(QString text, bool clear, bool selectFirstPlaceholder){
// don't add message text if we already have a transmission queued...
if(isMessageQueuedForTransmit()){
return;
}
if(clear){
ui->extFreeTextMsgEdit->clear();
}
QTextCursor c = ui->extFreeTextMsgEdit->textCursor();
if(c.hasSelection()){
c.removeSelectedText();
}
int pos = c.position();
c.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
bool isSpace = c.selectedText().isEmpty() || c.selectedText().at(0).isSpace();
c.clearSelection();
c.setPosition(pos);
if(!isSpace){
c.insertText(" ");
}
c.insertText(text);
if(selectFirstPlaceholder){
auto match = QRegularExpression("(\\[.+\\])").match(ui->extFreeTextMsgEdit->toPlainText());
if(match.hasMatch()){
c.setPosition(match.capturedStart());
c.setPosition(match.capturedEnd(), QTextCursor::KeepAnchor);
ui->extFreeTextMsgEdit->setTextCursor(c);
}
}
ui->extFreeTextMsgEdit->setFocus();
}
void MainWindow::enqueueMessage(int priority, QString message, int freq, Callback c){
m_txMessageQueue.enqueue(
PrioritizedMessage{
DriftingDateTime::currentDateTimeUtc(), priority, message, freq, c
}
);
}
void MainWindow::enqueueHeartbeat(QString message){
m_txHeartbeatQueue.enqueue(message);
}
void MainWindow::resetMessage(){
resetMessageUI();
resetMessageTransmitQueue();
}
void MainWindow::resetMessageUI(){
ui->nextFreeTextMsg->clear();
ui->extFreeTextMsg->clear();
ui->extFreeTextMsgEdit->clear();
ui->extFreeTextMsgEdit->setReadOnly(false);
update_dynamic_property (ui->extFreeTextMsgEdit, "transmitting", false);
if(ui->startTxButton->isChecked()){
ui->startTxButton->setChecked(false);
}
}
bool MainWindow::ensureCallsignSet(bool alert){
if(m_config.my_callsign().trimmed().isEmpty()){
if(alert) MessageBox::warning_message(this, tr ("Please enter your callsign in the settings."));
openSettings();
return false;
}
if(m_config.my_grid().trimmed().isEmpty()){
if(alert) MessageBox::warning_message(this, tr ("Please enter your grid locator in the settings."));
openSettings();
return false;
}
return true;
}
bool MainWindow::ensureKeyNotStuck(QString const& text){
// be annoying and drop messages with all the same character to reduce spam...
if(text.length() > 5 && QString(text).replace(text.at(0), "").trimmed().isEmpty()){
return false;
}
return true;
}
bool MainWindow::ensureNotIdle(){
if (!m_config.watchdog()){
return true;
}
if(m_idleMinutes < m_config.watchdog ()){
return true;
}
tx_watchdog (true); // disable transmit and auto replies
return false;
}
void MainWindow::createMessage(QString const& text){
if(!ensureCallsignSet()){
on_stopTxButton_clicked();
return;
}
if(!ensureNotIdle()){
on_stopTxButton_clicked();
return;
}
if(!ensureKeyNotStuck(text)){
on_stopTxButton_clicked();
ui->monitorButton->setChecked(false);
on_monitorButton_clicked(false);
foreach(auto obj, this->children()){
if(obj->isWidgetType()){
auto wid = qobject_cast<QWidget*>(obj);
wid->setEnabled(false);
}
}
return;
}
if(text.contains("APRS:") && !m_aprsClient->isPasscodeValid()){
MessageBox::warning_message(this, tr ("Please ensure a valid APRS passcode is set in the settings when sending an APRS packet."));
return;
}
resetMessageTransmitQueue();
createMessageTransmitQueue(replaceMacros(text, buildMacroValues(), false));
}
void MainWindow::createMessageTransmitQueue(QString const& text){
auto frames = buildMessageFrames(text);
m_txFrameQueue.append(frames);
m_txFrameCount = frames.length();
int freq = currentFreqOffset();
qDebug() << "creating message for freq" << freq;
QStringList lines;
foreach(auto frame, frames){
auto dt = DecodedText(frame.first, frame.second);
lines.append(dt.message());
}
displayTextForFreq(lines.join("") + " \u2301 ", freq, DriftingDateTime::currentDateTimeUtc(), true, true, true);
// if we're transmitting a message to be displayed, we should bump the repeat buttons...
resetAutomaticIntervalTransmissions(false, false);
// keep track of the last message text sent
m_lastTxMessage = text;
}
void MainWindow::restoreMessage(){
if(m_lastTxMessage.isEmpty()){
return;
}
addMessageText(m_lastTxMessage, true);
}
void MainWindow::resetMessageTransmitQueue(){
m_txFrameCount = 0;
m_txFrameQueue.clear();
m_txMessageQueue.clear();
}
QPair<QString, int> MainWindow::popMessageFrame(){
if(m_txFrameQueue.isEmpty()){
return QPair<QString, int>{};
}
return m_txFrameQueue.dequeue();
}
void MainWindow::on_nextFreeTextMsg_currentTextChanged (QString const& text)
{
msgtype(text, ui->nextFreeTextMsg);
}
void MainWindow::on_extFreeTextMsgEdit_currentTextChanged (QString const& text)
{
QString x;
QString::const_iterator i;
for(i = text.constBegin(); i != text.constEnd(); i++){
auto ch = (*i).toUpper().toLatin1();
if(ch == 10 || (32 <= ch && ch <= 126)){
// newline or printable 7-bit ascii
x += ch;
}
}
if(x != text){
int pos = ui->extFreeTextMsgEdit->textCursor().position();
int maxpos = x.size();
ui->extFreeTextMsgEdit->setPlainText(x);
QTextCursor c = ui->extFreeTextMsgEdit->textCursor();
c.setPosition(pos < maxpos ? pos : maxpos, QTextCursor::MoveAnchor);
highlightBlock(c.block(), m_config.compose_text_font(), m_config.color_compose_foreground(), QColor(Qt::transparent));
ui->extFreeTextMsgEdit->setTextCursor(c);
}
m_txTextDirty = x != m_txTextDirtyLastText;
m_txTextDirtyLastText = x;
// immediately update the display
updateButtonDisplay();
updateTextDisplay();
}
int MainWindow::currentFreqOffset(){
return ui->RxFreqSpinBox->value();
}
QList<QPair<QString, int>> MainWindow::buildMessageFrames(const QString &text){
// prepare selected callsign for directed message
QString selectedCall = callsignSelected();
// prepare compound
//bool compound = Varicode::isCompoundCallsign(/*Radio::is_compound_callsign(*/m_config.my_callsign());
QString mycall = m_config.my_callsign();
QString mygrid = m_config.my_grid().left(4);
// QString basecall = Radio::base_callsign(m_config.my_callsign());
// if(basecall != mycall){
// basecall = "<....>";
// }
auto frames = Varicode::buildMessageFrames(
mycall,
//basecall,
mygrid,
//compound,
selectedCall,
text);
#if 0
qDebug() << "frames:";
foreach(auto frame, frames){
auto dt = DecodedText(frame.frame, frame.bits);
qDebug() << "->" << frame << dt.message() << Varicode::frameTypeString(dt.frameType());
}
#endif
return frames;
}
bool MainWindow::prepareNextMessageFrame()
{
m_i3bit = Varicode::JS8Call;
QPair<QString, int> f = popMessageFrame();
auto frame = f.first;
auto bits = f.second;
if(frame.isEmpty()){
ui->nextFreeTextMsg->clear();
updateTxButtonDisplay();
return false;
} else {
ui->nextFreeTextMsg->setText(frame);
m_i3bit = bits;
updateTxButtonDisplay();
// TODO: bump heartbeat
return true;
}
}
bool MainWindow::isFreqOffsetFree(int f, int bw){
// if this frequency is our current frequency, it's always "free"
if(currentFreqOffset() == f){
return true;
}
// if this frequency is in our directed cache, it's always "free"
if(isDirectedOffset(f, nullptr)){
return true;
}
foreach(int offset, m_bandActivity.keys()){
auto d = m_bandActivity[offset];
if(d.isEmpty()){
continue;
}
// if we last received on this more than 30 seconds ago, it's free
if(d.last().utcTimestamp.secsTo(DriftingDateTime::currentDateTimeUtc()) >= 30){
continue;
}
// otherwise, if this is an occupied slot within our bw of where we'd like to transmit... it's not free...
if(qAbs(offset - f) < bw){
return false;
}
}
return true;
}
int MainWindow::findFreeFreqOffset(int fmin, int fmax, int bw){
int nslots = (fmax-fmin)/bw;
int f = fmin;
for(int i = 0; i < nslots; i++){
f = fmin + bw * (qrand() % nslots);
if(isFreqOffsetFree(f, bw)){
return f;
}
}
for(int i = 0; i < nslots; i++){
f = fmin + (qrand() % (fmax-fmin));
if(isFreqOffsetFree(f, bw)){
return f;
}
}
// return fmin if there's no free offset
return fmin;
}
#if 0
// schedulePing
void MainWindow::scheduleHeartbeat(bool first){
auto timestamp = DriftingDateTime::currentDateTimeUtc();
// if we have the heartbeat interval disabled, return early, unless this is a "heartbeat now"
if(!m_config.heartbeat() && !first){
heartbeatTimer.stop();
return;
}
// remove milliseconds
auto t = timestamp.time();
t.setHMS(t.hour(), t.minute(), t.second());
timestamp.setTime(t);
// round to 15 second increment
int secondsSinceEpoch = (timestamp.toMSecsSinceEpoch()/1000);
int delta = roundUp(secondsSinceEpoch, 15) + 1 + (first ? 0 : qMax(1, m_config.heartbeat()) * 60) - secondsSinceEpoch;
timestamp = timestamp.addSecs(delta);
// 25% of the time, switch intervals
float prob = (float) qrand() / (RAND_MAX);
if(prob < 0.25){
timestamp = timestamp.addSecs(15);
}
m_nextHeartbeat = timestamp;
m_nextHeartbeatQueued = false;
m_nextHeartPaused = false;
if(!heartbeatTimer.isActive()){
heartbeatTimer.setInterval(1000);
heartbeatTimer.start();
}
}
// pausePing
void MainWindow::pauseHeartbeat(){
m_nextHeartPaused = true;
if(heartbeatTimer.isActive()){
heartbeatTimer.stop();
}
}
// unpausePing
void MainWindow::unpauseHeartbeat(){
scheduleHeartbeat(false);
}
// checkPing
void MainWindow::checkHeartbeat(){
if(m_config.heartbeat() <= 0){
return;
}
auto secondsUntilHeartbeat = DriftingDateTime::currentDateTimeUtc().secsTo(m_nextHeartbeat);
if(secondsUntilHeartbeat > 5 && m_txHeartbeatQueue.isEmpty()){
return;
}
if(m_nextHeartbeatQueued){
return;
}
if(m_tx_watchdog){
return;
}
// idle heartbeat watchdog!
if (m_config.watchdog() && m_idleMinutes >= m_config.watchdog ()){
tx_watchdog (true); // disable transmit
return;
}
prepareHeartbeat();
}
// preparePing
void MainWindow::prepareHeartbeat(){
QStringList lines;
QString mycall = m_config.my_callsign();
QString mygrid = m_config.my_grid().left(4);
// JS8Call Style
if(m_txHeartbeatQueue.isEmpty()){
lines.append(QString("%1: HEARTBEAT %2").arg(mycall).arg(mygrid));
} else {
while(!m_txHeartbeatQueue.isEmpty() && lines.length() < 1){
lines.append(m_txHeartbeatQueue.dequeue());
}
}
// Choose a ping frequency
auto f = m_config.heartbeat_anywhere() ? -1 : findFreeFreqOffset(500, 1000, 50);
auto text = lines.join(QChar('\n'));
if(text.isEmpty()){
return;
}
// Queue the ping
enqueueMessage(PriorityLow, text, f, [this](){
m_nextHeartbeatQueued = false;
});
m_nextHeartbeatQueued = true;
}
#endif
void MainWindow::checkRepeat(){
if(ui->hbMacroButton->isChecked() && m_hbInterval > 0 && m_nextHeartbeat.isValid()){
if(DriftingDateTime::currentDateTimeUtc().secsTo(m_nextHeartbeat) <= 0){
sendHeartbeat();
}
}
if(ui->cqMacroButton->isChecked() && m_cqInterval > 0 && m_nextCQ.isValid()){
if(DriftingDateTime::currentDateTimeUtc().secsTo(m_nextCQ) <= 0){
sendCQ(true);
}
}
}
QString MainWindow::calculateDistance(QString const& value, int *pDistance, int *pAzimuth)
{
QString grid = value.trimmed();
if(grid.isEmpty() || grid.length() < 4){
return QString{};
}
qint64 nsec = (DriftingDateTime::currentMSecsSinceEpoch()/1000) % 86400;
double utch=nsec/3600.0;
int nAz,nEl,nDmiles,nDkm,nHotAz,nHotABetter;
azdist_(const_cast <char *> ((m_config.my_grid () + " ").left (6).toLatin1().constData()),
const_cast <char *> ((grid + " ").left (6).toLatin1().constData()),&utch,
&nAz,&nEl,&nDmiles,&nDkm,&nHotAz,&nHotABetter,6,6);
if(pAzimuth) *pAzimuth = nAz;
if(m_config.miles()){
if(pDistance) *pDistance = nDmiles;
return QString("%1 mi / %2°").arg(nDmiles).arg(nAz);
}
if(pDistance) *pDistance = nDkm;
return QString("%1 km / %2°").arg(nDkm).arg(nAz);
}
// this function is called by auto_tx_mode, which is called by autoButton.clicked
void MainWindow::on_startTxButton_toggled(bool checked)
{
if(checked){
createMessage(ui->extFreeTextMsgEdit->toPlainText());
startTx();
} else {
resetMessage();
stopTx();
on_stopTxButton_clicked();
}
}
void MainWindow::toggleTx(bool start){
if(start && ui->startTxButton->isChecked()) { return; }
if(!start && !ui->startTxButton->isChecked()) { return; }
ui->startTxButton->setChecked(start);
}
void MainWindow::on_rbNextFreeTextMsg_toggled (bool status)
{
if (status) {
m_ntx = 9;
}
}
void MainWindow::on_dxCallEntry_textChanged (QString const& call)
{
}
void MainWindow::on_dxCallEntry_returnPressed ()
{
}
void MainWindow::on_dxGridEntry_textChanged (QString const& grid)
{
}
void MainWindow::on_genStdMsgsPushButton_clicked()
{
}
void MainWindow::on_logQSOButton_clicked() //Log QSO button
{
/*
if (!m_hisCall.size ()) {
MessageBox::warning_message (this, tr ("Warning: DX Call field is empty."));
}
*/
// m_dateTimeQSOOn should really already be set but we'll ensure it gets set to something just in case
if (!m_dateTimeQSOOn.isValid ()) {
m_dateTimeQSOOn = DriftingDateTime::currentDateTimeUtc();
}
auto dateTimeQSOOff = DriftingDateTime::currentDateTimeUtc();
if (dateTimeQSOOff < m_dateTimeQSOOn) dateTimeQSOOff = m_dateTimeQSOOn;
QString call=callsignSelected();
if(call.startsWith("@")){
call = "";
}
QString grid="";
if(m_callActivity.contains(call)){
grid = m_callActivity[call].grid;
}
m_logDlg->initLogQSO (call.trimmed(), grid.trimmed(), m_modeTx == "FT8" ? "JS8" : m_modeTx, m_rptSent, m_rptRcvd,
m_dateTimeQSOOn, dateTimeQSOOff, m_freqNominal + ui->TxFreqSpinBox->value(),
m_config.my_callsign(), m_config.my_grid(),
m_config.log_as_DATA(), m_config.report_in_comments(),
m_config.bFox(), m_opCall);
}
void MainWindow::acceptQSO (QDateTime const& QSO_date_off, QString const& call, QString const& grid
, Frequency dial_freq, QString const& mode, QString const &submode
, QString const& rpt_sent, QString const& rpt_received
, QString const& tx_power, QString const& comments
, QString const& name, QDateTime const& QSO_date_on, QString const& operator_call
, QString const& my_call, QString const& my_grid, QByteArray const& ADIF)
{
QString date = QSO_date_on.toString("yyyyMMdd");
m_logBook.addAsWorked (m_hisCall, m_config.bands ()->find (m_freqNominal), mode, submode, date);
#if 0
m_messageClient->qso_logged (QSO_date_off, call, grid, dial_freq, mode, rpt_sent, rpt_received, tx_power, comments, name, QSO_date_on, operator_call, my_call, my_grid);
m_messageClient->logged_ADIF (ADIF);
#endif
sendNetworkMessage("LOG.QSO", QString(ADIF), {
{"UTC.ON", QVariant(QSO_date_on.toMSecsSinceEpoch())},
{"UTC.OFF", QVariant(QSO_date_off.toMSecsSinceEpoch())},
{"CALL", QVariant(call)},
{"GRID", QVariant(grid)},
{"FREQ", QVariant(dial_freq)},
{"MODE", QVariant(mode)},
{"SUBMODE", QVariant(submode)},
{"RPT.SENT", QVariant(rpt_sent)},
{"RPT.RECV", QVariant(rpt_received)},
{"NAME", QVariant(name)},
{"COMMENTS", QVariant(comments)},
{"STATION.OP", QVariant(operator_call)},
{"STATION.CALL", QVariant(my_call)},
{"STATION.GRID", QVariant(my_grid)}
});
m_logBook.init();
if (m_config.clear_callsign ()){
clearCallsignSelected();
}
displayCallActivity();
m_dateTimeQSOOn = QDateTime {};
}
qint64 MainWindow::nWidgets(QString t)
{
Q_ASSERT(t.length()==N_WIDGETS);
qint64 n=0;
for(int i=0; i<N_WIDGETS; i++) {
n=n + n + t.mid(i,1).toInt();
}
return n;
}
void MainWindow::displayWidgets(qint64 n)
{
/* See text file "displayWidgets.txt" for widget numbers */
qint64 j=qint64(1)<<(N_WIDGETS-1);
bool b;
for(int i=0; i<N_WIDGETS; i++) {
b=(n&j) != 0;
if(i==0) ui->txFirstCheckBox->setVisible(b);
if(i==1) ui->TxFreqSpinBox->setVisible(b);
if(i==2) ui->RxFreqSpinBox->setVisible(b);
if(i==3) ui->sbFtol->setVisible(b);
if(i==4) ui->rptSpinBox->setVisible(b);
if(i==5) ui->sbTR->setVisible(b);
if(i==6) {
ui->sbCQTxFreq->setVisible (b);
ui->cbCQTx->setVisible (b);
auto is_compound = m_config.my_callsign () != m_baseCall;
ui->cbCQTx->setEnabled (b && (!is_compound || shortList (m_config.my_callsign ())));
}
if(i==7) ui->cbShMsgs->setVisible(b);
if(i==8) ui->cbFast9->setVisible(b);
if(i==9) ui->cbAutoSeq->setVisible(b);
if(i==10) ui->cbTx6->setVisible(b);
if(i==11) ui->pbTxMode->setVisible(b);
if(i==12) ui->pbR2T->setVisible(b);
if(i==13) ui->pbT2R->setVisible(b);
if(i==14) ui->cbHoldTxFreq->setVisible(b);
if(i==14 and (!b)) ui->cbHoldTxFreq->setChecked(false);
if(i==15) ui->sbSubmode->setVisible(b);
if(i==16) ui->syncSpinBox->setVisible(b);
if(i==17) ui->WSPR_controls_widget->setVisible(b);
//if(i==18) ui->ClrAvgButton->setVisible(b);
if(i==19) ui->actionQuickDecode->setEnabled(b);
if(i==19) ui->actionMediumDecode->setEnabled(b);
if(i==19) ui->actionDeepDecode->setEnabled(b);
if(i==19) ui->actionDeepestDecode->setEnabled(b);
if(i==20) ui->actionInclude_averaging->setVisible (b);
if(i==21) ui->actionInclude_correlation->setVisible (b);
if(i==22) {
if(!b && m_echoGraph->isVisible()) m_echoGraph->hide();
}
if(i==23) ui->cbSWL->setVisible(b);
//if(i==24) ui->actionEnable_AP_FT8->setVisible (b);
//if(i==25) ui->actionEnable_AP_JT65->setVisible (b);
//if(i==26) ui->actionEnable_AP_DXcall->setVisible (b);
if(i==27) ui->cbFirst->setVisible(b);
if(i==28) ui->cbVHFcontest->setVisible(b);
if(i==29) ui->measure_check_box->setVisible(b);
if(i==30) ui->labDXped->setVisible(b);
if(i==31) ui->cbRxAll->setVisible(b);
//if(i==32) ui->cbCQonly->setVisible(b);
j=j>>1;
}
ui->tabWidget->setTabEnabled(3, "FT8" == m_mode);
m_lastCallsign.clear (); // ensures Tx5 is updated for new modes
}
void MainWindow::on_actionFT8_triggered()
{
m_mode="FT8";
bool bVHF=m_config.enable_VHF_features();
m_bFast9=false;
m_bFastMode=false;
WSPR_config(false);
switch_mode (Modes::JS8);
m_modeTx="FT8";
m_nsps=6912;
m_FFTSize = m_nsps / 2;
Q_EMIT FFTSize (m_FFTSize);
m_hsymStop=50;
setup_status_bar (bVHF);
m_toneSpacing=0.0; //???
ui->actionFT8->setChecked(true); //???
m_wideGraph->setMode(m_mode);
m_wideGraph->setModeTx(m_modeTx);
VHF_features_enabled(bVHF);
ui->cbAutoSeq->setChecked(true);
m_TRperiod=15;
m_fastGraph->hide();
m_wideGraph->show();
ui->decodedTextLabel2->setText(" UTC dB DT Freq Message");
m_wideGraph->setPeriod(m_TRperiod,m_nsps);
m_modulator->setTRPeriod(m_TRperiod); // TODO - not thread safe
m_detector->setTRPeriod(m_TRperiod); // TODO - not thread safe
ui->label_7->setText("Rx Frequency");
if(m_config.bFox()) {
ui->label_6->setText("Stations calling DXpedition " + m_config.my_callsign());
ui->decodedTextLabel->setText( "Call Grid dB Freq Dist Age Continent");
} else {
ui->label_6->setText("Band Activity");
ui->decodedTextLabel->setText( " UTC dB DT Freq Message");
}
if(!bVHF) {
displayWidgets(nWidgets("111010000100111000010000100100001"));
// Make sure that VHF contest mode is unchecked if VHF features is not enabled.
ui->cbVHFcontest->setChecked(false);
} else {
displayWidgets(nWidgets("111010000100111000010000100110001"));
}
ui->txrb2->setEnabled(true);
ui->txrb4->setEnabled(true);
ui->txrb5->setEnabled(true);
ui->txrb6->setEnabled(true);
ui->txb2->setEnabled(true);
ui->txb4->setEnabled(true);
ui->txb5->setEnabled(true);
ui->txb6->setEnabled(true);
ui->txFirstCheckBox->setEnabled(true);
ui->cbAutoSeq->setEnabled(true);
statusChanged();
}
void MainWindow::switch_mode (Mode mode)
{
m_fastGraph->setMode(m_mode);
m_config.frequencies ()->filter (m_config.region (), mode);
#if 0
auto const& row = m_config.frequencies ()->best_working_frequency (m_freqNominal);
if (row >= 0) {
ui->bandComboBox->setCurrentIndex (row);
on_bandComboBox_activated (row);
}
#endif
ui->rptSpinBox->setSingleStep(1);
ui->rptSpinBox->setMinimum(-50);
ui->rptSpinBox->setMaximum(49);
ui->sbFtol->values ({10, 20, 50, 100, 200, 500, 1000});
if(m_mode=="MSK144") {
ui->RxFreqSpinBox->setMinimum(1400);
ui->RxFreqSpinBox->setMaximum(1600);
ui->RxFreqSpinBox->setSingleStep(25);
} else {
ui->RxFreqSpinBox->setMinimum(0);
ui->RxFreqSpinBox->setMaximum(5000);
ui->RxFreqSpinBox->setSingleStep(1);
}
m_bVHFwarned=false;
bool b=m_mode=="FreqCal";
ui->tabWidget->setVisible(!b);
if(b) {
ui->DX_controls_widget->setVisible(false);
ui->decodedTextBrowser2->setVisible(false);
ui->decodedTextLabel2->setVisible(false);
ui->label_6->setVisible(false);
ui->label_7->setVisible(false);
}
}
void MainWindow::WSPR_config(bool b)
{
ui->decodedTextBrowser2->setVisible(!b);
ui->decodedTextLabel2->setVisible(!b and ui->cbMenus->isChecked());
ui->controls_stack_widget->setCurrentIndex (b && m_mode != "Echo" ? 1 : 0);
//ui->QSO_controls_widget->setVisible (!b);
//ui->DX_controls_widget->setVisible (!b);
ui->WSPR_controls_widget->setVisible (b);
ui->label_6->setVisible(!b and ui->cbMenus->isChecked());
ui->label_7->setVisible(!b and ui->cbMenus->isChecked());
//ui->logQSOButton->setVisible(!b);
ui->DecodeButton->setEnabled(!b);
if(b and (m_mode!="Echo")) {
QString t="UTC dB DT Freq Drift Call Grid dBm ";
if(m_config.miles()) t += " mi";
if(!m_config.miles()) t += " km";
ui->decodedTextLabel->setText(t);
if (m_config.is_transceiver_online ()) {
Q_EMIT m_config.transceiver_tx_frequency (0); // turn off split
}
m_bSimplex = true;
} else {
ui->decodedTextLabel->setText("UTC dB DT Freq Message");
m_bSimplex = false;
}
enable_DXCC_entity (m_config.DXCC ()); // sets text window proportions and (re)inits the logbook
}
void MainWindow::fast_config(bool b)
{
m_bFastMode=b;
ui->TxFreqSpinBox->setEnabled(!b);
ui->sbTR->setVisible(b);
if(b and (m_bFast9 or m_mode=="MSK144" or m_mode=="ISCAT")) {
m_wideGraph->hide();
m_fastGraph->show();
} else {
m_wideGraph->show();
m_fastGraph->hide();
}
}
void MainWindow::on_TxFreqSpinBox_valueChanged(int n)
{
m_wideGraph->setTxFreq(n);
// if (ui->cbHoldTxFreq->isChecked ()) ui->RxFreqSpinBox->setValue(n);
if(m_mode!="MSK144") {
Q_EMIT transmitFrequency (n - m_XIT);
}
statusUpdate ();
}
void MainWindow::on_RxFreqSpinBox_valueChanged(int n)
{
m_wideGraph->setRxFreq(n);
if (m_mode == "FreqCal") {
setRig ();
}
statusUpdate ();
}
void MainWindow::on_actionQuickDecode_toggled (bool checked)
{
m_ndepth ^= (-checked ^ m_ndepth) & 0x00000001;
}
void MainWindow::on_actionMediumDecode_toggled (bool checked)
{
m_ndepth ^= (-checked ^ m_ndepth) & 0x00000002;
}
void MainWindow::on_actionDeepDecode_toggled (bool checked)
{
m_ndepth ^= (-checked ^ m_ndepth) & 0x00000003;
}
void MainWindow::on_actionDeepestDecode_toggled (bool checked)
{
m_ndepth ^= (-checked ^ m_ndepth) & 0x00000004;
}
void MainWindow::on_actionInclude_averaging_toggled (bool checked)
{
m_ndepth ^= (-checked ^ m_ndepth) & 0x00000010;
}
void MainWindow::on_actionInclude_correlation_toggled (bool checked)
{
m_ndepth ^= (-checked ^ m_ndepth) & 0x00000020;
}
void MainWindow::on_actionEnable_AP_DXcall_toggled (bool checked)
{
m_ndepth ^= (-checked ^ m_ndepth) & 0x00000040;
}
void MainWindow::on_actionErase_ALL_TXT_triggered() //Erase ALL.TXT
{
int ret = MessageBox::query_message (this, tr ("Confirm Erase"),
tr ("Are you sure you want to erase file ALL.TXT?"));
if(ret==MessageBox::Yes) {
QFile f {m_config.writeable_data_dir ().absoluteFilePath ("ALL.TXT")};
f.remove();
m_RxLog=1;
}
}
void MainWindow::on_actionErase_FoxQSO_txt_triggered()
{
int ret = MessageBox::query_message(this, tr("Confirm Erase"),
tr("Are you sure you want to erase file FoxQSO.txt?"));
if(ret==MessageBox::Yes) {
QFile f{m_config.writeable_data_dir().absoluteFilePath("FoxQSO.txt")};
f.remove();
}
}
void MainWindow::on_actionErase_js8call_log_adi_triggered()
{
int ret = MessageBox::query_message (this, tr ("Confirm Erase"),
tr ("Are you sure you want to erase file js8call_log.adi?"));
if(ret==MessageBox::Yes) {
QFile f {m_config.writeable_data_dir ().absoluteFilePath ("js8call_log.adi")};
f.remove();
m_logBook.init();
}
}
void MainWindow::on_actionOpen_log_directory_triggered ()
{
QDesktopServices::openUrl (QUrl::fromLocalFile (m_config.writeable_data_dir ().absolutePath ()));
}
void MainWindow::on_actionOpen_Save_Directory_triggered(){
QDesktopServices::openUrl (QUrl::fromLocalFile (m_config.save_directory().absolutePath ()));
}
void MainWindow::on_bandComboBox_currentIndexChanged (int index)
{
auto const& frequencies = m_config.frequencies ();
auto const& source_index = frequencies->mapToSource (frequencies->index (index, FrequencyList_v2::frequency_column));
Frequency frequency {m_freqNominal};
if (source_index.isValid ())
{
frequency = frequencies->frequency_list ()[source_index.row ()].frequency_;
}
// Lookup band
auto const& band = m_config.bands ()->find (frequency);
if (!band.isEmpty ())
{
ui->bandComboBox->lineEdit ()->setStyleSheet ({});
ui->bandComboBox->setCurrentText (band);
}
else
{
ui->bandComboBox->lineEdit ()->setStyleSheet ("QLineEdit {color: yellow; background-color : red;}");
ui->bandComboBox->setCurrentText (m_config.bands ()->oob ());
}
displayDialFrequency ();
}
void MainWindow::on_bandComboBox_activated (int index)
{
auto const& frequencies = m_config.frequencies ();
auto const& source_index = frequencies->mapToSource (frequencies->index (index, FrequencyList_v2::frequency_column));
Frequency frequency {m_freqNominal};
if (source_index.isValid ())
{
frequency = frequencies->frequency_list ()[source_index.row ()].frequency_;
}
m_bandEdited = true;
band_changed (frequency);
m_wideGraph->setRxBand (m_config.bands ()->find (frequency));
}
void MainWindow::band_changed (Frequency f)
{
// bool monitor_off=!m_monitoring;
// Set the attenuation value if options are checked
QString curBand = ui->bandComboBox->currentText();
if (m_config.pwrBandTxMemory() && !m_tune) {
if (m_pwrBandTxMemory.contains(curBand)) {
ui->outAttenuation->setValue(m_pwrBandTxMemory[curBand].toInt());
}
else {
m_pwrBandTxMemory[curBand] = ui->outAttenuation->value();
}
}
if (m_bandEdited) {
if (!m_mode.startsWith ("WSPR")) { // band hopping preserves auto Tx
if (f + m_wideGraph->nStartFreq () > m_freqNominal + ui->TxFreqSpinBox->value ()
|| f + m_wideGraph->nStartFreq () + m_wideGraph->fSpan () <=
m_freqNominal + ui->TxFreqSpinBox->value ()) {
// qDebug () << "start f:" << m_wideGraph->nStartFreq () << "span:" << m_wideGraph->fSpan () << "DF:" << ui->TxFreqSpinBox->value ();
// disable auto Tx if "blind" QSY outside of waterfall
ui->stopTxButton->click (); // halt any transmission
auto_tx_mode (false); // disable auto Tx
m_send_RR73 = false; // force user to reassess on new band
}
}
// TODO: jsherer - is this relied upon anywhere?
//m_lastBand.clear ();
m_bandEdited = false;
psk_Reporter->sendReport(); // Upload any queued spots before changing band
m_aprsClient->sendReports();
if (!m_transmitting) monitor (true);
if ("FreqCal" == m_mode)
{
m_frequency_list_fcal_iter = m_config.frequencies ()->find (f);
}
float r=m_freqNominal/(f+0.0001);
if(r<0.9 or r>1.1) m_bVHFwarned=false;
setRig (f);
setXIT (ui->TxFreqSpinBox->value ());
// if(monitor_off) monitor(false);
}
}
void MainWindow::vhfWarning()
{
MessageBox::warning_message (this, tr ("VHF features warning"),
"VHF/UHF/Microwave features is enabled on a lower frequency band.");
m_bVHFwarned=true;
}
void MainWindow::enable_DXCC_entity (bool /*on*/)
{
m_logBook.init(); // re-read the log and cty.dat files
updateGeometry ();
}
void MainWindow::on_pbCallCQ_clicked()
{
ui->genMsg->setText(ui->tx6->text());
m_ntx=7;
m_QSOProgress = CALLING;
m_gen_message_is_cq = true;
ui->rbGenMsg->setChecked(true);
if(m_transmitting) m_restart=true;
set_dateTimeQSO(-1);
}
void MainWindow::on_pbAnswerCaller_clicked()
{
QString t=ui->tx3->text();
int i0=t.indexOf(" R-");
if(i0<0) i0=t.indexOf(" R+");
t=t.mid(0,i0+1)+t.mid(i0+2,3);
ui->genMsg->setText(t);
m_ntx=7;
m_QSOProgress = REPORT;
m_gen_message_is_cq = false;
ui->rbGenMsg->setChecked(true);
if(m_transmitting) m_restart=true;
set_dateTimeQSO(2);
}
void MainWindow::on_pbSendRRR_clicked()
{
ui->genMsg->setText(ui->tx4->text());
m_ntx=7;
m_QSOProgress = ROGERS;
m_gen_message_is_cq = false;
ui->rbGenMsg->setChecked(true);
if(m_transmitting) m_restart=true;
}
void MainWindow::on_pbAnswerCQ_clicked()
{
ui->genMsg->setText(ui->tx1->text());
QString t=ui->tx2->text();
int i0=t.indexOf("/");
int i1=t.indexOf(" ");
if(i0>0 and i0<i1) ui->genMsg->setText(t);
m_ntx=7;
m_QSOProgress = REPLYING;
m_gen_message_is_cq = false;
ui->rbGenMsg->setChecked(true);
if(m_transmitting) m_restart=true;
}
void MainWindow::on_pbSendReport_clicked()
{
ui->genMsg->setText(ui->tx3->text());
m_ntx=7;
m_QSOProgress = ROGER_REPORT;
m_gen_message_is_cq = false;
ui->rbGenMsg->setChecked(true);
if(m_transmitting) m_restart=true;
set_dateTimeQSO(3);
}
void MainWindow::on_pbSend73_clicked()
{
ui->genMsg->setText(ui->tx5->currentText());
m_ntx=7;
m_QSOProgress = SIGNOFF;
m_gen_message_is_cq = false;
ui->rbGenMsg->setChecked(true);
if(m_transmitting) m_restart=true;
}
void MainWindow::on_rbGenMsg_clicked(bool checked)
{
m_freeText=!checked;
if(!m_freeText) {
if(m_ntx != 7 && m_transmitting) m_restart=true;
m_ntx=7;
// would like to set m_QSOProgress but what to? So leave alone and
// assume it is correct
}
}
void MainWindow::on_rbFreeText_clicked(bool checked)
{
m_freeText=checked;
if(m_freeText) {
m_ntx=8;
// would like to set m_QSOProgress but what to? So leave alone and
// assume it is correct. Perhaps should store old value to be
// restored above in on_rbGenMsg_clicked
if (m_transmitting) m_restart=true;
}
}
void MainWindow::on_clearAction_triggered(QObject * sender){
// TODO: jsherer - abstract this into a tableWidgetRXAllReset function
if(sender == ui->tableWidgetRXAll){
m_bandActivity.clear();
clearTableWidget(ui->tableWidgetRXAll);
resetTimeDeltaAverage();
displayBandActivity();
}
// TODO: jsherer - abstract this into a tableWidgetCallsReset function
if(sender == ui->tableWidgetCalls){
m_callActivity.clear();
clearTableWidget((ui->tableWidgetCalls));
createAllcallTableRows(ui->tableWidgetCalls, "");
resetTimeDeltaAverage();
displayCallActivity();
}
if(sender == ui->extFreeTextMsgEdit){
resetMessage();
m_lastTxMessage.clear();
}
if(sender == ui->textEditRX){
// TODO: jsherer - move these
ui->textEditRX->clear();
m_rxFrameBlockNumbers.clear();
m_rxActivityQueue.clear();
}
}
void MainWindow::buildHeartbeatMenu(QMenu *menu){
buildRepeatMenu(menu, ui->hbMacroButton, &m_hbInterval);
menu->addSeparator();
auto now = menu->addAction("Send Heartbeat Now");
connect(now, &QAction::triggered, this, &MainWindow::sendHeartbeat);
}
void MainWindow::buildCQMenu(QMenu *menu){
buildRepeatMenu(menu, ui->cqMacroButton, &m_cqInterval);
menu->addSeparator();
auto now = menu->addAction("Send CQ Message Now");
connect(now, &QAction::triggered, this, [this](){ sendCQ(true); });
}
void MainWindow::buildRepeatMenu(QMenu *menu, QPushButton * button, int * interval){
QList<QPair<QString, int>> items = {
{"On demand / do not repeat", 0},
{"Repeat every 1 minute", 1},
{"Repeat every 5 minutes", 5},
{"Repeat every 10 minutes", 10},
{"Repeat every 15 minutes", 15},
{"Repeat every 30 minutes", 30},
{"Repeat every 60 minutes", 60},
};
QActionGroup * group = new QActionGroup(menu);
foreach(auto pair, items){
int minutes = pair.second;
auto action = menu->addAction(pair.first);
action->setData(pair.second);
action->setCheckable(true);
action->setChecked(*interval == minutes);
group->addAction(action);
connect(action, &QAction::toggled, this, [this, minutes, interval, button](bool checked){
if(checked){
*interval = minutes;
if(minutes > 0){
// force a re-toggle
button->setChecked(false);
}
button->setChecked(minutes > 0);
}
});
}
}
void MainWindow::sendHeartbeat(){
QString mycall = m_config.my_callsign();
QString mygrid = m_config.my_grid().left(4);
QString message = QString("%1: HB %2").arg(mycall).arg(mygrid).trimmed();
auto f = m_config.heartbeat_anywhere() ? -1 : findFreeFreqOffset(500, 1000, 50);
enqueueMessage(PriorityLow, message, f, [this](){ /* */ });
}
void MainWindow::sendHeartbeatAck(QString to, int snr){
auto message = QString("%1 ACK %2").arg(to).arg(Varicode::formatSNR(snr));
auto f = m_config.heartbeat_anywhere() ? -1 : findFreeFreqOffset(500, 1000, 50);
enqueueMessage(PriorityLow, message, f, [this](){ /* */ });
}
void MainWindow::on_hbMacroButton_toggled(bool checked){
if(checked){
if(m_hbInterval){
m_nextHeartbeat = nextTransmitCycle().addSecs(m_hbInterval * 60);
if(!repeatTimer.isActive()){
repeatTimer.start();
}
} else {
sendHeartbeat();
// make this button emulate a single press button
ui->hbMacroButton->setChecked(false);
}
} else {
m_nextHeartbeat = QDateTime{};
}
updateRepeatButtonDisplay();
}
void MainWindow::on_hbMacroButton_clicked(){
}
void MainWindow::sendCQ(bool repeat){
auto message = m_config.cq_message();
if(message.isEmpty()){
QString mygrid = m_config.my_grid().left(4);
message = QString("CQCQCQ %1").arg(mygrid).trimmed();
}
clearCallsignSelected();
addMessageText(replaceMacros(message, buildMacroValues(), true));
if(repeat || m_config.transmit_directed()) toggleTx(true);
}
void MainWindow::on_cqMacroButton_toggled(bool checked){
if(checked){
if(m_cqInterval){
m_nextCQ = nextTransmitCycle().addSecs(m_cqInterval * 60);
if(!repeatTimer.isActive()){
repeatTimer.start();
}
} else {
sendCQ();
// make this button emulate a single press button
ui->cqMacroButton->setChecked(false);
}
} else {
m_nextCQ= QDateTime{};
}
updateRepeatButtonDisplay();
}
void MainWindow::on_cqMacroButton_clicked(){
}
void MainWindow::on_replyMacroButton_clicked(){
QString call = callsignSelected();
if(call.isEmpty()){
return;
}
auto message = m_config.reply_message();
message = replaceMacros(message, buildMacroValues(), true);
addMessageText(QString("%1 %2").arg(call).arg(message));
if(m_config.transmit_directed()) toggleTx(true);
}
void MainWindow::on_snrMacroButton_clicked(){
QString call = callsignSelected();
if(call.isEmpty()){
return;
}
auto now = DriftingDateTime::currentDateTimeUtc();
int callsignAging = m_config.callsign_aging();
if(!m_callActivity.contains(call)){
return;
}
auto cd = m_callActivity[call];
if (callsignAging && cd.utcTimestamp.secsTo(now) / 60 >= callsignAging) {
return;
}
auto snr = Varicode::formatSNR(cd.snr);
addMessageText(QString("%1 SNR %2").arg(call).arg(snr));
if(m_config.transmit_directed()) toggleTx(true);
}
void MainWindow::on_qthMacroButton_clicked(){
QString qth = m_config.my_qth();
if(qth.isEmpty()){
return;
}
addMessageText(QString("QTH %1").arg(replaceMacros(qth, buildMacroValues(), true)));
if(m_config.transmit_directed()) toggleTx(true);
}
void MainWindow::on_qtcMacroButton_clicked(){
QString qtc = m_config.my_station();
if(qtc.isEmpty()){
return;
}
addMessageText(QString("QTC %1").arg(replaceMacros(qtc, buildMacroValues(), true)));
if(m_config.transmit_directed()) toggleTx(true);
}
void MainWindow::setShowColumn(QString tableKey, QString columnKey, bool value){
m_showColumnsCache[tableKey + columnKey] = QVariant(value);
displayBandActivity();
displayCallActivity();
}
bool MainWindow::showColumn(QString tableKey, QString columnKey, bool default_){
return m_showColumnsCache.value(tableKey + columnKey, QVariant(default_)).toBool();
}
void MainWindow::buildShowColumnsMenu(QMenu *menu, QString tableKey){
QList<QPair<QString, QString>> columnKeys = {
{"Frequency Offset", "offset"},
{"Last heard timestamp", "timestamp"},
{"SNR", "snr"},
{"Time Delta", "tdrift"},
};
QMap<QString, bool> defaultOverride = {
{"tdrift", false},
{"grid", false},
{"distance", false}
};
if(tableKey == "call"){
columnKeys.prepend({"Worked Before Flag", "flag"});
columnKeys.prepend({"Callsign", "callsign"});
columnKeys.append({
{"Grid Locator", "grid"},
{"Distance", "distance"}
});
}
columnKeys.prepend({"Show Column Labels", "labels"});
bool first = true;
foreach(auto p, columnKeys){
auto columnLabel = p.first;
auto columnKey = p.second;
auto a = menu->addAction(columnLabel);
a->setCheckable(true);
bool showByDefault = true;
if(defaultOverride.contains(columnKey)){
showByDefault = defaultOverride[columnKey];
}
a->setChecked(showColumn(tableKey, columnKey, showByDefault));
connect(a, &QAction::triggered, this, [this, a, tableKey, columnKey](){
setShowColumn(tableKey, columnKey, a->isChecked());
});
if(first){
menu->addSeparator();
first = false;
}
}
}
void MainWindow::setSortBy(QString key, QString value){
m_sortCache[key] = QVariant(value);
displayBandActivity();
displayCallActivity();
}
QString MainWindow::getSortBy(QString key, QString defaultValue){
return m_sortCache.value(key, QVariant(defaultValue)).toString();
}
void MainWindow::buildSortByMenu(QMenu * menu, QString key, QString defaultValue, QList<QPair<QString, QString>> values){
auto currentSortBy = getSortBy(key, defaultValue);
QActionGroup * g = new QActionGroup(menu);
g->setExclusive(true);
foreach(auto p, values){
auto k = p.first;
auto v = p.second;
auto a = menu->addAction(k);
a->setCheckable(true);
a->setChecked(v == currentSortBy);
a->setActionGroup(g);
connect(a, &QAction::triggered, this, [this, a, key, v](){
if(a->isChecked()){
setSortBy(key, v);
}
});
}
}
void MainWindow::buildBandActivitySortByMenu(QMenu * menu){
buildSortByMenu(menu, "bandActivity", "offset", {
{"Frequency offset", "offset"},
{"Last heard timestamp (oldest first)", "timestamp"},
{"Last heard timestamp (recent first)", "-timestamp"},
{"SNR (weakest first)", "snr"},
{"SNR (strongest first)", "-snr"}
});
}
void MainWindow::buildCallActivitySortByMenu(QMenu * menu){
buildSortByMenu(menu, "callActivity", "callsign", {
{"Callsign", "callsign"},
{"Callsigns Replied (recent first)", "ackTimestamp"},
{"Frequency offset", "offset"},
{"Distance (closest first)", "distance"},
{"Distance (farthest first)", "-distance"},
{"Last heard timestamp (oldest first)", "timestamp"},
{"Last heard timestamp (recent first)", "-timestamp"},
{"SNR (weakest first)", "snr"},
{"SNR (strongest first)", "-snr"}
});
}
void MainWindow::buildQueryMenu(QMenu * menu, QString call){
bool isAllCall = isAllCallIncluded(call);
// for now, we're going to omit displaying the call...delete this if we want the other functionality
call = "";
auto grid = m_config.my_grid();
bool emptyQTC = m_config.my_station().isEmpty();
bool emptyQTH = m_config.my_qth().isEmpty();
bool emptyGrid = m_config.my_grid().isEmpty();
auto callAction = menu->addAction(QString("Send a directed message to selected callsign"));
connect(callAction, &QAction::triggered, this, [this](){
QString selectedCall = callsignSelected();
if(selectedCall.isEmpty()){
return;
}
addMessageText(QString("%1 ").arg(selectedCall), true);
});
menu->addSeparator();
auto sendReplyAction = menu->addAction(QString("%1 Reply - Send reply message to selected callsign").arg(call).trimmed());
connect(sendReplyAction, &QAction::triggered, this, [this](){
QString selectedCall = callsignSelected();
if(selectedCall.isEmpty()){
return;
}
auto message = m_config.reply_message();
message = replaceMacros(message, buildMacroValues(), true);
addMessageText(QString("%1 %2").arg(selectedCall).arg(message), true);
});
auto sendSNRAction = menu->addAction(QString("%1 SNR - Send a signal report to the selected callsign").arg(call).trimmed());
sendSNRAction->setEnabled(m_callActivity.contains(callsignSelected()));
connect(sendSNRAction, &QAction::triggered, this, [this](){
QString selectedCall = callsignSelected();
if(selectedCall.isEmpty()){
return;
}
if(!m_callActivity.contains(selectedCall)){
return;
}
auto d = m_callActivity[selectedCall];
addMessageText(QString("%1 SNR %2").arg(selectedCall).arg(Varicode::formatSNR(d.snr)), true);
if(m_config.transmit_directed()) toggleTx(true);
});
auto qtcAction = menu->addAction(QString("%1 QTC - Send my station message").arg(call).trimmed());
qtcAction->setDisabled(emptyQTC);
connect(qtcAction, &QAction::triggered, this, [this](){
QString selectedCall = callsignSelected();
if(selectedCall.isEmpty()){
return;
}
addMessageText(QString("%1 QTC %2").arg(selectedCall).arg(m_config.my_station()), true);
if(m_config.transmit_directed()) toggleTx(true);
});
auto qthAction = menu->addAction(QString("%1 QTH - Send my station location message").arg(call).trimmed());
qthAction->setDisabled(emptyQTH);
connect(qthAction, &QAction::triggered, this, [this](){
QString selectedCall = callsignSelected();
if(selectedCall.isEmpty()){
return;
}
addMessageText(QString("%1 QTH %2").arg(selectedCall).arg(m_config.my_qth()), true);
if(m_config.transmit_directed()) toggleTx(true);
});
auto gridAction = menu->addAction(QString("%1 GRID %2 - Send my current station Maidenhead grid locator").arg(call).arg(grid).trimmed());
gridAction->setDisabled(emptyGrid);
connect(gridAction, &QAction::triggered, this, [this](){
QString selectedCall = callsignSelected();
if(selectedCall.isEmpty()){
return;
}
addMessageText(QString("%1 GRID %2").arg(selectedCall).arg(m_config.my_grid()), true);
if(m_config.transmit_directed()) toggleTx(true);
});
menu->addSeparator();
auto snrQueryAction = menu->addAction(QString("%1 SNR? - What is my signal report?").arg(call).trimmed());
snrQueryAction->setDisabled(isAllCall);
connect(snrQueryAction, &QAction::triggered, this, [this](){
QString selectedCall = callsignSelected();
if(selectedCall.isEmpty()){
return;
}
addMessageText(QString("%1 SNR?").arg(selectedCall), true);
if(m_config.transmit_directed()) toggleTx(true);
});
auto qthQueryAction = menu->addAction(QString("%1 QTH? - What is your QTH message?").arg(call).trimmed());
qthQueryAction->setDisabled(isAllCall);
connect(qthQueryAction, &QAction::triggered, this, [this](){
QString selectedCall = callsignSelected();
if(selectedCall.isEmpty()){
return;
}
addMessageText(QString("%1 QTH?").arg(selectedCall), true);
if(m_config.transmit_directed()) toggleTx(true);
});
auto gridQueryAction = menu->addAction(QString("%1 GRID? - What is your current grid locator?").arg(call).trimmed());
gridQueryAction->setDisabled(isAllCall);
connect(gridQueryAction, &QAction::triggered, this, [this](){
QString selectedCall = callsignSelected();
if(selectedCall.isEmpty()){
return;
}
addMessageText(QString("%1 GRID?").arg(selectedCall), true);
if(m_config.transmit_directed()) toggleTx(true);
});
auto stationMessageQueryAction = menu->addAction(QString("%1 QTC? - What is your station message?").arg(call).trimmed());
stationMessageQueryAction->setDisabled(isAllCall);
connect(stationMessageQueryAction, &QAction::triggered, this, [this](){
QString selectedCall = callsignSelected();
if(selectedCall.isEmpty()){
return;
}
addMessageText(QString("%1 QTC?").arg(selectedCall), true);
if(m_config.transmit_directed()) toggleTx(true);
});
auto stationIdleQueryAction = menu->addAction(QString("%1 STATUS? - What is the status of your station (auto, version, etc)?").arg(call).trimmed());
stationIdleQueryAction->setDisabled(isAllCall);
connect(stationIdleQueryAction, &QAction::triggered, this, [this](){
QString selectedCall = callsignSelected();
if(selectedCall.isEmpty()){
return;
}
addMessageText(QString("%1 STATUS?").arg(selectedCall), true);
if(m_config.transmit_directed()) toggleTx(true);
});
auto heardQueryAction = menu->addAction(QString("%1 HEARING? - What are the stations are you hearing? (Top 4 ranked by most recently heard)").arg(call).trimmed());
heardQueryAction->setDisabled(isAllCall);
connect(heardQueryAction, &QAction::triggered, this, [this](){
QString selectedCall = callsignSelected();
if(selectedCall.isEmpty()){
return;
}
addMessageText(QString("%1 HEARING?").arg(selectedCall), true);
if(m_config.transmit_directed()) toggleTx(true);
});
#if 0
auto retransmitAction = menu->addAction(QString("%1|[MESSAGE] - Please ACK and retransmit the following message").arg(call).trimmed());
retransmitAction->setDisabled(isAllCall);
connect(retransmitAction, &QAction::triggered, this, [this](){
QString selectedCall = callsignSelected();
if(selectedCall.isEmpty()){
return;
}
addMessageText(QString("%1|[MESSAGE]").arg(selectedCall), true, true);
});
#endif
auto alertAction = menu->addAction(QString("%1>[MESSAGE] - Please save this message or relay it to its destination").arg(call).trimmed());
alertAction->setDisabled(isAllCall);
connect(alertAction, &QAction::triggered, this, [this](){
QString selectedCall = callsignSelected();
if(selectedCall.isEmpty()){
return;
}
addMessageText(QString("%1>[MESSAGE]").arg(selectedCall), true, true);
});
auto qsoQueryAction = menu->addAction(QString("%1 QUERY [CALLSIGN]? - Please acknowledge you can communicate directly with [CALLSIGN]").arg(call).trimmed());
connect(qsoQueryAction, &QAction::triggered, this, [this](){
QString selectedCall = callsignSelected();
if(selectedCall.isEmpty()){
return;
}
addMessageText(QString("%1 QUERY [CALLSIGN]?").arg(selectedCall), true, true);
});
auto agnAction = menu->addAction(QString("%1 AGN? - Please automatically repeat your last transmission").arg(call).trimmed());
connect(agnAction, &QAction::triggered, this, [this](){
QString selectedCall = callsignSelected();
if(selectedCall.isEmpty()){
return;
}
addMessageText(QString("%1 AGN?").arg(selectedCall), true);
if(m_config.transmit_directed()) toggleTx(true);
});
menu->addSeparator();
auto qslQueryAction = menu->addAction(QString("%1 QSL? - Did you receive my last transmission?").arg(call).trimmed());
connect(qslQueryAction, &QAction::triggered, this, [this](){
QString selectedCall = callsignSelected();
if(selectedCall.isEmpty()){
return;
}
addMessageText(QString("%1 QSL?").arg(selectedCall), true);
if(m_config.transmit_directed()) toggleTx(true);
});
auto qslAction = menu->addAction(QString("%1 QSL - I confirm I received your last transmission").arg(call).trimmed());
connect(qslAction, &QAction::triggered, this, [this](){
QString selectedCall = callsignSelected();
if(selectedCall.isEmpty()){
return;
}
addMessageText(QString("%1 QSL").arg(selectedCall), true);
if(m_config.transmit_directed()) toggleTx(true);
});
auto yesAction = menu->addAction(QString("%1 YES - I confirm your last inquiry").arg(call).trimmed());
connect(yesAction, &QAction::triggered, this, [this](){
QString selectedCall = callsignSelected();
if(selectedCall.isEmpty()){
return;
}
addMessageText(QString("%1 YES").arg(selectedCall), true);
if(m_config.transmit_directed()) toggleTx(true);
});
auto noAction = menu->addAction(QString("%1 NO - I do not confirm your last inquiry").arg(call).trimmed());
connect(noAction, &QAction::triggered, this, [this](){
QString selectedCall = callsignSelected();
if(selectedCall.isEmpty()){
return;
}
addMessageText(QString("%1 NO").arg(selectedCall), true);
if(m_config.transmit_directed()) toggleTx(true);
});
auto hwAction = menu->addAction(QString("%1 HW CPY? - How do you copy?").arg(call).trimmed());
connect(hwAction, &QAction::triggered, this, [this](){
QString selectedCall = callsignSelected();
if(selectedCall.isEmpty()){
return;
}
addMessageText(QString("%1 HW CPY?").arg(selectedCall), true);
if(m_config.transmit_directed()) toggleTx(true);
});
auto rrAction = menu->addAction(QString("%1 RR - Roger. Received. I copy.").arg(call).trimmed());
connect(rrAction, &QAction::triggered, this, [this](){
QString selectedCall = callsignSelected();
if(selectedCall.isEmpty()){
return;
}
addMessageText(QString("%1 RR").arg(selectedCall), true);
if(m_config.transmit_directed()) toggleTx(true);
});
auto fbAction = menu->addAction(QString("%1 FB - Fine Business").arg(call).trimmed());
connect(fbAction, &QAction::triggered, this, [this](){
QString selectedCall = callsignSelected();
if(selectedCall.isEmpty()){
return;
}
addMessageText(QString("%1 FB").arg(selectedCall), true);
if(m_config.transmit_directed()) toggleTx(true);
});
auto tuAction = menu->addAction(QString("%1 TU - Thank You").arg(call).trimmed());
connect(tuAction, &QAction::triggered, this, [this](){
QString selectedCall = callsignSelected();
if(selectedCall.isEmpty()){
return;
}
addMessageText(QString("%1 TU").arg(selectedCall), true);
if(m_config.transmit_directed()) toggleTx(true);
});
auto sevenThreeAction = menu->addAction(QString("%1 73 - I send my best regards").arg(call).trimmed());
connect(sevenThreeAction, &QAction::triggered, this, [this](){
QString selectedCall = callsignSelected();
if(selectedCall.isEmpty()){
return;
}
addMessageText(QString("%1 73").arg(selectedCall), true);
if(m_config.transmit_directed()) toggleTx(true);
});
auto skAction = menu->addAction(QString("%1 SK - End of contact").arg(call).trimmed());
connect(skAction, &QAction::triggered, this, [this](){
QString selectedCall = callsignSelected();
if(selectedCall.isEmpty()){
return;
}
addMessageText(QString("%1 SK").arg(selectedCall), true);
if(m_config.transmit_directed()) toggleTx(true);
});
}
void MainWindow::buildRelayMenu(QMenu *menu){
auto now = DriftingDateTime::currentDateTimeUtc();
int callsignAging = m_config.callsign_aging();
foreach(auto cd, m_callActivity.values()){
if (callsignAging && cd.utcTimestamp.secsTo(now) / 60 >= callsignAging) {
continue;
}
menu->addAction(buildRelayAction(cd.call));
}
}
QAction* MainWindow::buildRelayAction(QString call){
QAction *a = new QAction(call, nullptr);
connect(a, &QAction::triggered, this, [this, call](){
prependMessageText(QString("%1>").arg(call));
});
return a;
}
void MainWindow::buildEditMenu(QMenu *menu, QTextEdit *edit){
bool hasSelection = !edit->textCursor().selectedText().isEmpty();
auto cut = menu->addAction("Cu&t");
cut->setEnabled(hasSelection && !edit->isReadOnly());
connect(edit, &QTextEdit::copyAvailable, this, [this, edit, cut](bool copyAvailable){
cut->setEnabled(copyAvailable && !edit->isReadOnly());
});
connect(cut, &QAction::triggered, this, [this, edit](){
edit->copy();
edit->textCursor().removeSelectedText();
});
auto copy = menu->addAction("&Copy");
copy->setEnabled(hasSelection);
connect(edit, &QTextEdit::copyAvailable, this, [this, copy](bool copyAvailable){
copy->setEnabled(copyAvailable);
});
connect(copy, &QAction::triggered, edit, &QTextEdit::copy);
auto paste = menu->addAction("&Paste");
paste->setEnabled(edit->canPaste());
connect(paste, &QAction::triggered, edit, &QTextEdit::paste);
}
QMap<QString, QString> MainWindow::buildMacroValues(){
QMap<QString, QString> values = {
{"<MYCALL>", m_config.my_callsign()},
{"<MYGRID4>", m_config.my_grid().left(4)},
{"<MYGRID12>", m_config.my_grid().left(12)},
{"<MYQTC>", m_config.my_station()},
{"<MYQTH>", m_config.my_qth()},
{"<MYCQ>", m_config.cq_message()},
{"<MYREPLY>", m_config.reply_message()}
};
auto selectedCall = callsignSelected();
if(m_callActivity.contains(selectedCall)){
auto cd = m_callActivity[selectedCall];
values["<CALL>"] = selectedCall;
values["<TDELTA>"] = QString("%1 ms").arg((int)(1000*cd.tdrift));
if(cd.snr > -31){
values["<SNR>"] = Varicode::formatSNR(cd.snr);
}
}
return values;
}
QString MainWindow::replaceMacros(QString const &text, QMap<QString, QString> values, bool prune){
QString output = QString(text);
foreach(auto key, values.keys()){
output = output.replace(key, values[key]);
}
if(prune){
output = output.replace(QRegularExpression("[<](?:[^>]+)[>]"), "");
}
return output;
}
void MainWindow::buildSavedMessagesMenu(QMenu *menu){
auto values = buildMacroValues();
foreach(QString macro, m_config.macros()->stringList()){
QAction *action = menu->addAction(replaceMacros(macro, values, false));
connect(action, &QAction::triggered, this, [this, macro](){
auto values = buildMacroValues();
addMessageText(replaceMacros(macro, values, true));
if(m_config.transmit_directed()) toggleTx(true);
});
}
menu->addSeparator();
auto editAction = new QAction(QString("&Edit Saved Messages"), menu);
menu->addAction(editAction);
connect(editAction, &QAction::triggered, this, [this](){
openSettings(5);
});
auto saveAction = new QAction(QString("&Save Current Message"), menu);
saveAction->setDisabled(ui->extFreeTextMsgEdit->toPlainText().isEmpty());
menu->addAction(saveAction);
connect(saveAction, &QAction::triggered, this, [this](){
auto macros = m_config.macros();
if(macros->insertRow(macros->rowCount())){
auto index = macros->index(macros->rowCount()-1);
macros->setData(index, ui->extFreeTextMsgEdit->toPlainText());
writeSettings();
}
});
}
void MainWindow::on_queryButton_pressed(){
QMenu *menu = ui->queryButton->menu();
if(!menu){
menu = new QMenu(ui->queryButton);
}
menu->clear();
buildQueryMenu(menu, callsignSelected());
ui->queryButton->setMenu(menu);
ui->queryButton->showMenu();
}
void MainWindow::on_macrosMacroButton_pressed(){
QMenu *menu = ui->macrosMacroButton->menu();
if(!menu){
menu = new QMenu(ui->macrosMacroButton);
}
menu->clear();
buildSavedMessagesMenu(menu);
ui->macrosMacroButton->setMenu(menu);
ui->macrosMacroButton->showMenu();
}
void MainWindow::on_deselectButton_pressed(){
clearCallsignSelected();
}
void MainWindow::on_tableWidgetRXAll_cellClicked(int /*row*/, int /*col*/){
ui->tableWidgetCalls->selectionModel()->select(
ui->tableWidgetCalls->selectionModel()->selection(),
QItemSelectionModel::Deselect);
displayCallActivity();
}
void MainWindow::on_tableWidgetRXAll_cellDoubleClicked(int row, int col){
on_tableWidgetRXAll_cellClicked(row, col);
// TODO: jsherer - could also parse the messages for the last callsign?
auto item = ui->tableWidgetRXAll->item(row, 0);
int offset = item->text().replace(" Hz", "").toInt();
// switch to the offset of this row
setFreqOffsetForRestore(offset, false);
// print the history in the main window...
int activityAging = m_config.activity_aging();
QDateTime now = DriftingDateTime::currentDateTimeUtc();
QDateTime firstActivity = now;
QString activityText;
bool isLast = false;
foreach(auto d, m_bandActivity[offset]){
if(activityAging && d.utcTimestamp.secsTo(now)/60 >= activityAging){
continue;
}
if(activityText.isEmpty()){
firstActivity = d.utcTimestamp;
}
activityText.append(d.text);
isLast = (d.bits & Varicode::JS8CallLast) == Varicode::JS8CallLast;
if(isLast){
// can also use \u0004 \u2666 \u2404
activityText.append(" \u2301 ");
}
}
if(!activityText.isEmpty()){
displayTextForFreq(activityText, offset, firstActivity, false, true, isLast);
}
}
void MainWindow::on_tableWidgetRXAll_selectionChanged(const QItemSelection &/*selected*/, const QItemSelection &/*deselected*/){
on_extFreeTextMsgEdit_currentTextChanged(ui->extFreeTextMsgEdit->toPlainText());
auto placeholderText = QString("Type your outgoing messages here.");
auto selectedCall = callsignSelected();
if(!selectedCall.isEmpty()){
placeholderText = QString("Type your outgoing directed message to %1 here.").arg(selectedCall);
}
ui->extFreeTextMsgEdit->setPlaceholderText(placeholderText);
// immediately update the display);
updateButtonDisplay();
updateTextDisplay();
}
void MainWindow::on_tableWidgetCalls_cellClicked(int /*row*/, int /*col*/){
ui->tableWidgetRXAll->selectionModel()->select(
ui->tableWidgetRXAll->selectionModel()->selection(),
QItemSelectionModel::Deselect);
displayBandActivity();
}
void MainWindow::on_tableWidgetCalls_cellDoubleClicked(int row, int col){
on_tableWidgetCalls_cellClicked(row, col);
auto call = callsignSelected();
if(m_rxCallsignCommandQueue.contains(call) && !m_rxCallsignCommandQueue[call].isEmpty()){
CommandDetail d = m_rxCallsignCommandQueue[call].first();
m_rxCallsignCommandQueue[call].removeFirst();
processAlertReplyForCommand(d, d.relayPath, d.cmd);
} else {
addMessageText(call);
}
}
void MainWindow::on_tableWidgetCalls_selectionChanged(const QItemSelection &selected, const QItemSelection &deselected){
on_tableWidgetRXAll_selectionChanged(selected, deselected);
}
void MainWindow::on_freeTextMsg_currentTextChanged (QString const& text)
{
msgtype(text, ui->freeTextMsg->lineEdit ());
}
void MainWindow::on_driftSpinBox_valueChanged(int n){
if(n == DriftingDateTime::drift()){
return;
}
setDrift(n);
}
void MainWindow::on_driftSyncButton_clicked(){
auto now = QDateTime::currentDateTimeUtc();
int n = 0;
int nPos = 15 - now.time().second() % m_TRperiod;
int nNeg = now.time().second() % m_TRperiod - 15;
if(abs(nNeg) < nPos){
n = nNeg;
} else {
n = nPos;
}
setDrift(n * 1000);
}
void MainWindow::on_driftSyncEndButton_clicked(){
auto now = QDateTime::currentDateTimeUtc();
int n = 0;
int nPos = 15 - now.time().second() % m_TRperiod;
int nNeg = now.time().second() % m_TRperiod - 15;
if(abs(nNeg) < nPos){
n = nNeg + 2;
} else {
n = nPos - 2;
}
setDrift(n * 1000);
}
void MainWindow::on_driftSyncResetButton_clicked(){
setDrift(0);
resetTimeDeltaAverage();
}
void MainWindow::setDrift(int n){
DriftingDateTime::setDrift(n);
qDebug() << qSetRealNumberPrecision(12) << "Average delta:" << m_timeDeltaMsMMA;
qDebug() << qSetRealNumberPrecision(12) << "Drift milliseconds:" << n;
qDebug() << qSetRealNumberPrecision(12) << "Clock time:" << QDateTime::currentDateTimeUtc();
qDebug() << qSetRealNumberPrecision(12) << "Drifted time:" << DriftingDateTime::currentDateTimeUtc();
if(ui->driftSpinBox->value() != n){
ui->driftSpinBox->setValue(n);
}
}
void MainWindow::on_rptSpinBox_valueChanged(int n)
{
int step=ui->rptSpinBox->singleStep();
if(n%step !=0) {
n++;
ui->rptSpinBox->setValue(n);
}
m_rpt=QString::number(n);
int ntx0=m_ntx;
m_ntx=ntx0;
if(m_ntx==1) ui->txrb1->setChecked(true);
if(m_ntx==2) ui->txrb2->setChecked(true);
if(m_ntx==3) ui->txrb3->setChecked(true);
if(m_ntx==4) ui->txrb4->setChecked(true);
if(m_ntx==5) ui->txrb5->setChecked(true);
if(m_ntx==6) ui->txrb6->setChecked(true);
statusChanged();
}
void MainWindow::on_tuneButton_clicked (bool checked)
{
static bool lastChecked = false;
if (lastChecked == checked) return;
lastChecked = checked;
QString curBand = ui->bandComboBox->currentText();
if (checked && m_tune==false) { // we're starting tuning so remember Tx and change pwr to Tune value
if (m_config.pwrBandTuneMemory ()) {
m_pwrBandTxMemory[curBand] = ui->outAttenuation->value(); // remember our Tx pwr
m_PwrBandSetOK = false;
if (m_pwrBandTuneMemory.contains(curBand)) {
ui->outAttenuation->setValue(m_pwrBandTuneMemory[curBand].toInt()); // set to Tune pwr
}
m_PwrBandSetOK = true;
}
}
else { // we're turning off so remember our Tune pwr setting and reset to Tx pwr
if (m_config.pwrBandTuneMemory() || m_config.pwrBandTxMemory()) {
m_pwrBandTuneMemory[curBand] = ui->outAttenuation->value(); // remember our Tune pwr
m_PwrBandSetOK = false;
ui->outAttenuation->setValue(m_pwrBandTxMemory[curBand].toInt()); // set to Tx pwr
m_PwrBandSetOK = true;
}
}
if (m_tune) {
tuneButtonTimer.start(250);
} else {
m_sentFirst73=false;
itone[0]=0;
on_monitorButton_clicked (true);
m_tune=true;
}
Q_EMIT tune (checked);
}
void MainWindow::stop_tuning ()
{
on_tuneButton_clicked(false);
ui->tuneButton->setChecked (false);
m_bTxTime=false;
m_tune=false;
}
void MainWindow::stopTuneATU()
{
on_tuneButton_clicked(false);
m_bTxTime=false;
}
void MainWindow::resetPushButtonToggleText(QPushButton *btn){
bool checked = btn->isChecked();
auto style = btn->styleSheet();
if(checked){
style = style.replace("font-weight:normal;", "font-weight:bold;");
} else {
style = style.replace("font-weight:bold;", "font-weight:normal;");
}
btn->setStyleSheet(style);
#if PUSH_BUTTON_CHECKMARK
auto on = "";
auto text = btn->text();
if(checked){
btn->setText(on + text.replace(on, ""));
} else {
btn->setText(text.replace(on, ""));
}
#endif
#if PUSH_BUTTON_MIN_WIDTH
int width = 0;
QList<QPushButton*> btns;
foreach(auto child, ui->buttonGrid->children()){
if(!child->isWidgetType()){
continue;
}
if(!child->objectName().contains("Button")){
continue;
}
auto b = qobject_cast<QPushButton*>(child);
width = qMax(width, b->geometry().width());
btns.append(b);
}
foreach(auto child, btns){
child->setMinimumWidth(width);
}
#endif
}
void MainWindow::on_monitorTxButton_clicked(){
ui->monitorTxButton->setChecked(false);
on_stopTxButton_clicked();
}
void MainWindow::on_stopTxButton_clicked() //Stop Tx
{
if (m_tune) stop_tuning ();
if (m_auto and !m_tuneup) auto_tx_mode (false);
m_btxok=false;
m_bCallingCQ = false;
m_bAutoReply = false; // ready for next
ui->cbFirst->setStyleSheet ("");
resetMessage();
resetAutomaticIntervalTransmissions(false, false);
}
void MainWindow::rigOpen ()
{
update_dynamic_property (ui->readFreq, "state", "warning");
ui->readFreq->setText ("CAT");
ui->readFreq->setEnabled (true);
m_config.transceiver_online ();
Q_EMIT m_config.sync_transceiver (true, true);
}
void MainWindow::on_pbR2T_clicked()
{
ui->TxFreqSpinBox->setValue(ui->RxFreqSpinBox->value ());
}
void MainWindow::on_pbT2R_clicked()
{
if (ui->RxFreqSpinBox->isEnabled ())
{
ui->RxFreqSpinBox->setValue (ui->TxFreqSpinBox->value ());
}
}
void MainWindow::on_turboButton_clicked(){
m_wideGraph->setTurbo(ui->turboButton->isChecked());
m_txTextDirty = true;
updateTextDisplay();
}
void MainWindow::on_readFreq_clicked()
{
if (m_transmitting) return;
if (m_config.transceiver_online ())
{
Q_EMIT m_config.sync_transceiver (true, true);
}
}
void MainWindow::on_pbTxMode_clicked()
{
if(m_modeTx=="JT9") {
m_modeTx="JT65";
ui->pbTxMode->setText("Tx JT65 #");
} else {
m_modeTx="JT9";
ui->pbTxMode->setText("Tx JT9 @");
}
m_wideGraph->setModeTx(m_modeTx);
statusChanged();
}
void MainWindow::setXIT(int n, Frequency base)
{
if (m_transmitting && !m_config.tx_qsy_allowed ()) return;
// If "CQ nnn ..." feature is active, set the proper Tx frequency
if(m_config.split_mode () && ui->cbCQTx->isEnabled () && ui->cbCQTx->isVisible () &&
ui->cbCQTx->isChecked())
{
if (6 == m_ntx || (7 == m_ntx && m_gen_message_is_cq))
{
// All conditions are met, use calling frequency
base = m_freqNominal / 1000000 * 1000000 + 1000 * ui->sbCQTxFreq->value () + m_XIT;
}
}
if (!base) base = m_freqNominal;
m_XIT = 0;
if (!m_bSimplex) {
// m_bSimplex is false, so we can use split mode if requested
if (m_config.split_mode () && (!m_config.enable_VHF_features () || m_mode == "FT8")) {
// Don't use XIT for VHF & up
m_XIT=(n/500)*500 - 1500;
}
if ((m_monitoring || m_transmitting)
&& m_config.is_transceiver_online ()
&& m_config.split_mode ())
{
// All conditions are met, reset the transceiver Tx dial
// frequency
m_freqTxNominal = base + m_XIT;
if (m_astroWidget) m_astroWidget->nominal_frequency (m_freqNominal, m_freqTxNominal);
Q_EMIT m_config.transceiver_tx_frequency (m_freqTxNominal + m_astroCorrection.tx);
}
}
//Now set the audio Tx freq
Q_EMIT transmitFrequency (ui->TxFreqSpinBox->value () - m_XIT);
}
void MainWindow::qsy(int hzDelta){
setRig(m_freqNominal + hzDelta);
setFreqOffsetForRestore(m_wideGraph->centerFreq(), false);
// adjust band activity frequencies
QMap<int, QList<ActivityDetail>> newActivity;
foreach(auto offset, m_bandActivity.keys()){
if(m_bandActivity[offset].isEmpty()){
continue;
}
newActivity[offset - hzDelta] = m_bandActivity[offset];
newActivity[offset - hzDelta].last().freq -= hzDelta;
}
m_bandActivity.clear();
m_bandActivity.unite(newActivity);
// adjust call activity frequencies
foreach(auto call, m_callActivity.keys()){
m_callActivity[call].freq -= hzDelta;
}
displayActivity(true);
}
void MainWindow::setFreqOffsetForRestore(int freq, bool shouldRestore){
setFreq4(freq, freq);
if(shouldRestore){
m_shouldRestoreFreq = true;
} else {
m_previousFreq = 0;
m_shouldRestoreFreq = false;
}
}
bool MainWindow::tryRestoreFreqOffset(){
if(!m_shouldRestoreFreq || m_previousFreq == 0){
return false;
}
setFreqOffsetForRestore(m_previousFreq, false);
return true;
}
void MainWindow::setFreq4(int rxFreq, int txFreq) {
// don't allow QSY if we've already queued a transmission, unless we have that functionality enabled.
if(isMessageQueuedForTransmit() && !m_config.tx_qsy_allowed()){
return;
}
if(rxFreq != txFreq){
txFreq = rxFreq;
}
// TODO: jsherer - here's where we'd set minimum frequency again (later?)
rxFreq = max(0, rxFreq);
txFreq = max(0, txFreq);
m_previousFreq = currentFreqOffset();
ui->RxFreqSpinBox->setValue(rxFreq);
ui->TxFreqSpinBox->setValue(txFreq);
displayDialFrequency();
}
void MainWindow::handle_transceiver_update (Transceiver::TransceiverState const& s)
{
//qDebug () << "MainWindow::handle_transceiver_update:" << s;
Transceiver::TransceiverState old_state {m_rigState};
//transmitDisplay (s.ptt ());
if (s.ptt () && !m_rigState.ptt ()) // safe to start audio
// (caveat - DX Lab Suite Commander)
{
if (m_tx_when_ready && g_iptt) // waiting to Tx and still needed
{
ptt1Timer.start(1000 * m_config.txDelay ()); //Start-of-transmission sequencer delay
}
m_tx_when_ready = false;
}
m_rigState = s;
auto old_freqNominal = m_freqNominal;
if (!old_freqNominal)
{
// always take initial rig frequency to avoid start up problems
// with bogus Tx frequencies
m_freqNominal = s.frequency ();
}
if (old_state.online () == false && s.online () == true)
{
// initializing
on_monitorButton_clicked (!m_config.monitor_off_at_startup ());
ui->autoReplyButton->setChecked(!m_config.autoreply_off_at_startup());
}
if (s.frequency () != old_state.frequency () || s.split () != m_splitMode)
{
m_splitMode = s.split ();
if (!s.ptt ())
{
m_freqNominal = s.frequency () - m_astroCorrection.rx;
if (old_freqNominal != m_freqNominal)
{
m_freqTxNominal = m_freqNominal;
}
if (m_monitoring)
{
m_lastMonitoredFrequency = m_freqNominal;
}
if (m_lastDialFreq != m_freqNominal &&
(m_mode != "MSK144"
|| !(ui->cbCQTx->isEnabled () && ui->cbCQTx->isVisible () && ui->cbCQTx->isChecked()))) {
m_lastDialFreq = m_freqNominal;
m_secBandChanged=DriftingDateTime::currentMSecsSinceEpoch()/1000;
if(m_freqNominal != m_bandHoppedFreq){
m_bandHopped = false;
}
if(s.frequency () < 30000000u && !m_mode.startsWith ("WSPR")) {
// Write freq changes to ALL.TXT only below 30 MHz.
QFile f2 {m_config.writeable_data_dir ().absoluteFilePath ("ALL.TXT")};
if (f2.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append)) {
QTextStream out(&f2);
out << DriftingDateTime::currentDateTimeUtc().toString("yyyy-MM-dd hh:mm:ss")
<< " " << qSetRealNumberPrecision (12) << (m_freqNominal / 1.e6) << " MHz "
<< "JS8" << endl;
f2.close();
} else {
MessageBox::warning_message (this, tr ("File Error")
,tr ("Cannot open \"%1\" for append: %2")
.arg (f2.fileName ()).arg (f2.errorString ()));
}
}
if (m_config.spot_to_reporting_networks ()) {
pskSetLocal();
aprsSetLocal();
}
statusChanged();
m_wideGraph->setDialFreq(m_freqNominal / 1.e6);
}
} else {
m_freqTxNominal = s.split () ? s.tx_frequency () - m_astroCorrection.tx : s.frequency ();
}
if (m_astroWidget) m_astroWidget->nominal_frequency (m_freqNominal, m_freqTxNominal);
}
// ensure frequency display is correct
if (m_astroWidget && old_state.ptt () != s.ptt ()) setRig ();
displayDialFrequency ();
update_dynamic_property (ui->readFreq, "state", "ok");
ui->readFreq->setEnabled (false);
ui->readFreq->setText (s.split () ? "CAT/S" : "CAT");
}
void MainWindow::handle_transceiver_failure (QString const& reason)
{
update_dynamic_property (ui->readFreq, "state", "error");
ui->readFreq->setEnabled (true);
on_stopTxButton_clicked ();
rigFailure (reason);
}
void MainWindow::rigFailure (QString const& reason)
{
if (m_first_error)
{
// one automatic retry
QTimer::singleShot (0, this, SLOT (rigOpen ()));
m_first_error = false;
}
else
{
if (m_splash && m_splash->isVisible ()) m_splash->hide ();
m_rigErrorMessageBox.setDetailedText (reason);
// don't call slot functions directly to avoid recursion
m_rigErrorMessageBox.exec ();
auto const clicked_button = m_rigErrorMessageBox.clickedButton ();
if (clicked_button == m_configurations_button)
{
ui->menuConfig->exec (QCursor::pos ());
}
else
{
switch (m_rigErrorMessageBox.standardButton (clicked_button))
{
case MessageBox::Ok:
m_config.select_tab (1);
QTimer::singleShot (0, this, SLOT (on_actionSettings_triggered ()));
break;
case MessageBox::Retry:
QTimer::singleShot (0, this, SLOT (rigOpen ()));
break;
case MessageBox::Cancel:
QTimer::singleShot (0, this, SLOT (close ()));
break;
default: break; // squashing compile warnings
}
}
m_first_error = true; // reset
}
}
void MainWindow::transmit (double snr)
{
double toneSpacing=0.0;
if (m_modeTx == "JT65") {
if(m_nSubMode==0) toneSpacing=11025.0/4096.0;
if(m_nSubMode==1) toneSpacing=2*11025.0/4096.0;
if(m_nSubMode==2) toneSpacing=4*11025.0/4096.0;
Q_EMIT sendMessage (NUM_JT65_SYMBOLS,
4096.0*12000.0/11025.0, ui->TxFreqSpinBox->value () - m_XIT,
toneSpacing, m_soundOutput, m_config.audio_output_channel (),
true, false, snr, m_TRperiod);
}
if (m_modeTx == "FT8") {
toneSpacing=12000.0/1920.0;
if(m_config.x2ToneSpacing()) toneSpacing=2*12000.0/1920.0;
if(m_config.x4ToneSpacing()) toneSpacing=4*12000.0/1920.0;
if(m_config.bFox() and !m_tune) toneSpacing=-1;
if(TEST_FOX_WAVE_GEN && ui->turboButton->isChecked() && !m_tune) toneSpacing=-1;
Q_EMIT sendMessage (NUM_FT8_SYMBOLS,
1920.0, ui->TxFreqSpinBox->value () - m_XIT,
toneSpacing, m_soundOutput, m_config.audio_output_channel (),
true, false, snr, m_TRperiod);
}
if (m_modeTx == "QRA64") {
if(m_nSubMode==0) toneSpacing=12000.0/6912.0;
if(m_nSubMode==1) toneSpacing=2*12000.0/6912.0;
if(m_nSubMode==2) toneSpacing=4*12000.0/6912.0;
if(m_nSubMode==3) toneSpacing=8*12000.0/6912.0;
if(m_nSubMode==4) toneSpacing=16*12000.0/6912.0;
Q_EMIT sendMessage (NUM_QRA64_SYMBOLS,
6912.0, ui->TxFreqSpinBox->value () - m_XIT,
toneSpacing, m_soundOutput, m_config.audio_output_channel (),
true, false, snr, m_TRperiod);
}
if (m_modeTx == "JT9") {
int nsub=pow(2,m_nSubMode);
int nsps[]={480,240,120,60};
double sps=m_nsps;
m_toneSpacing=nsub*12000.0/6912.0;
if(m_config.x2ToneSpacing()) m_toneSpacing=2.0*m_toneSpacing;
if(m_config.x4ToneSpacing()) m_toneSpacing=4.0*m_toneSpacing;
bool fastmode=false;
if(m_bFast9 and (m_nSubMode>=4)) {
fastmode=true;
sps=nsps[m_nSubMode-4];
m_toneSpacing=12000.0/sps;
}
Q_EMIT sendMessage (NUM_JT9_SYMBOLS, sps,
ui->TxFreqSpinBox->value() - m_XIT, m_toneSpacing,
m_soundOutput, m_config.audio_output_channel (),
true, fastmode, snr, m_TRperiod);
}
if (m_modeTx == "MSK144") {
m_nsps=6;
double f0=1000.0;
if(!m_bFastMode) {
m_nsps=192;
f0=ui->TxFreqSpinBox->value () - m_XIT - 0.5*m_toneSpacing;
}
m_toneSpacing=6000.0/m_nsps;
m_FFTSize = 7 * 512;
Q_EMIT FFTSize (m_FFTSize);
int nsym;
nsym=NUM_MSK144_SYMBOLS;
if(itone[40] < 0) nsym=40;
Q_EMIT sendMessage (nsym, double(m_nsps), f0, m_toneSpacing,
m_soundOutput, m_config.audio_output_channel (),
true, true, snr, m_TRperiod);
}
if (m_modeTx == "JT4") {
if(m_nSubMode==0) toneSpacing=4.375;
if(m_nSubMode==1) toneSpacing=2*4.375;
if(m_nSubMode==2) toneSpacing=4*4.375;
if(m_nSubMode==3) toneSpacing=9*4.375;
if(m_nSubMode==4) toneSpacing=18*4.375;
if(m_nSubMode==5) toneSpacing=36*4.375;
if(m_nSubMode==6) toneSpacing=72*4.375;
Q_EMIT sendMessage (NUM_JT4_SYMBOLS,
2520.0*12000.0/11025.0, ui->TxFreqSpinBox->value () - m_XIT,
toneSpacing, m_soundOutput, m_config.audio_output_channel (),
true, false, snr, m_TRperiod);
}
if (m_mode=="WSPR") {
int nToneSpacing=1;
if(m_config.x2ToneSpacing()) nToneSpacing=2;
if(m_config.x4ToneSpacing()) nToneSpacing=4;
Q_EMIT sendMessage (NUM_WSPR_SYMBOLS, 8192.0,
ui->TxFreqSpinBox->value() - 1.5 * 12000 / 8192,
m_toneSpacing*nToneSpacing, m_soundOutput,
m_config.audio_output_channel(),true, false, snr,
m_TRperiod);
}
if (m_mode=="WSPR-LF") {
Q_EMIT sendMessage (NUM_WSPR_LF_SYMBOLS, 24576.0,
ui->TxFreqSpinBox->value(),
m_toneSpacing, m_soundOutput,
m_config.audio_output_channel(),true, false, snr,
m_TRperiod);
}
if(m_mode=="Echo") {
//??? should use "fastMode = true" here ???
Q_EMIT sendMessage (27, 1024.0, 1500.0, 0.0, m_soundOutput,
m_config.audio_output_channel(),
false, false, snr, m_TRperiod);
}
if(m_mode=="ISCAT") {
double sps,f0;
if(m_nSubMode==0) {
sps=512.0*12000.0/11025.0;
toneSpacing=11025.0/512.0;
f0=47*toneSpacing;
} else {
sps=256.0*12000.0/11025.0;
toneSpacing=11025.0/256.0;
f0=13*toneSpacing;
}
Q_EMIT sendMessage (NUM_ISCAT_SYMBOLS, sps, f0, toneSpacing, m_soundOutput,
m_config.audio_output_channel(),
true, true, snr, m_TRperiod);
}
// In auto-sequencing mode, stop after 5 transmissions of "73" message.
if (m_bFastMode || m_bFast9) {
if (ui->cbAutoSeq->isVisible () && ui->cbAutoSeq->isChecked ()) {
if(m_ntx==5) {
m_nTx73 += 1;
} else {
m_nTx73=0;
}
}
}
}
void MainWindow::on_outAttenuation_valueChanged (int a)
{
QString tt_str;
qreal dBAttn {a / 10.}; // slider interpreted as dB / 100
if (m_tune && m_config.pwrBandTuneMemory()) {
tt_str = tr ("Tune digital gain ");
} else {
tt_str = tr ("Transmit digital gain ");
}
tt_str += (a ? QString::number (-dBAttn, 'f', 1) : "0") + "dB";
if (!m_block_pwr_tooltip) {
QToolTip::showText (QCursor::pos (), tt_str, ui->outAttenuation);
}
QString curBand = ui->bandComboBox->currentText();
if (m_PwrBandSetOK && !m_tune && m_config.pwrBandTxMemory ()) {
m_pwrBandTxMemory[curBand] = a; // remember our Tx pwr
}
if (m_PwrBandSetOK && m_tune && m_config.pwrBandTuneMemory()) {
m_pwrBandTuneMemory[curBand] = a; // remember our Tune pwr
}
Q_EMIT outAttenuationChanged (dBAttn);
}
void MainWindow::on_actionShort_list_of_add_on_prefixes_and_suffixes_triggered()
{
}
bool MainWindow::shortList(QString callsign)
{
int n=callsign.length();
int i1=callsign.indexOf("/");
Q_ASSERT(i1>0 and i1<n);
QString t1=callsign.mid(0,i1);
QString t2=callsign.mid(i1+1,n-i1-1);
bool b=(m_pfx.contains(t1) or m_sfx.contains(t2));
return b;
}
void MainWindow::pskSetLocal ()
{
psk_Reporter->setLocalStation(m_config.my_callsign (), m_config.my_grid (),
m_config.my_station(), QString {"JS8Call v" + version() }.simplified ());
}
void MainWindow::aprsSetLocal ()
{
auto call = m_config.my_callsign();
auto base = Radio::base_callsign(call);
auto grid = m_config.my_grid();
auto passcode = m_config.aprs_passcode();
call = APRSISClient::replaceCallsignSuffixWithSSID(call, base);
qDebug() << "APRSISClient Set Local Station:" << call << grid << passcode;
m_aprsClient->setLocalStation(call, grid, passcode);
}
void MainWindow::transmitDisplay (bool transmitting)
{
ui->monitorTxButton->setChecked(transmitting);
if (transmitting == m_transmitting) {
if (transmitting) {
ui->signal_meter_widget->setValue(0,0);
if (m_monitoring) monitor (false);
m_btxok=true;
}
auto QSY_allowed = !transmitting or m_config.tx_qsy_allowed () or
!m_config.split_mode ();
if (ui->cbHoldTxFreq->isChecked ()) {
ui->RxFreqSpinBox->setEnabled (QSY_allowed);
ui->pbT2R->setEnabled (QSY_allowed);
}
if (!m_mode.startsWith ("WSPR")) {
if(m_config.enable_VHF_features ()) {
//### During tests, at least, allow use of Tx Freq spinner with VHF features enabled.
// used fixed 1000Hz Tx DF for VHF & up QSO modes
// ui->TxFreqSpinBox->setValue(1000);
// ui->TxFreqSpinBox->setEnabled (false);
ui->TxFreqSpinBox->setEnabled (true);
//###
} else {
ui->TxFreqSpinBox->setEnabled (QSY_allowed and !m_bFastMode);
ui->pbR2T->setEnabled (QSY_allowed);
ui->cbHoldTxFreq->setEnabled (QSY_allowed);
}
}
// the following are always disallowed in transmit
//ui->menuMode->setEnabled (!transmitting);
//ui->bandComboBox->setEnabled (!transmitting);
if (!transmitting) {
if (m_mode == "JT9+JT65") {
// allow mode switch in Rx when in dual mode
ui->pbTxMode->setEnabled (true);
}
} else {
ui->pbTxMode->setEnabled (false);
}
}
updateTxButtonDisplay();
}
void MainWindow::on_sbFtol_valueChanged(int value)
{
m_wideGraph->setTol (value);
}
void::MainWindow::VHF_features_enabled(bool b)
{
if(m_mode!="JT4" and m_mode!="JT65") b=false;
if(b and (ui->actionInclude_averaging->isChecked() or
ui->actionInclude_correlation->isChecked())) {
ui->actionDeepestDecode->setChecked (true);
}
ui->actionInclude_averaging->setVisible (b);
ui->actionInclude_correlation->setVisible (b);
ui->actionMessage_averaging->setEnabled(b);
ui->actionEnable_AP_DXcall->setVisible (m_mode=="QRA64");
ui->actionEnable_AP_JT65->setVisible (b && m_mode=="JT65");
if(!b && m_msgAvgWidget and !m_config.bFox()) {
if(m_msgAvgWidget->isVisible()) m_msgAvgWidget->close();
}
}
void MainWindow::on_sbTR_valueChanged(int value)
{
// if(!m_bFastMode and n>m_nSubMode) m_MinW=m_nSubMode;
if(m_bFastMode or m_mode=="FreqCal") {
m_TRperiod = value;
m_fastGraph->setTRPeriod (value);
m_modulator->setTRPeriod (value); // TODO - not thread safe
m_detector->setTRPeriod (value); // TODO - not thread safe
m_wideGraph->setPeriod (value, m_nsps);
progressBar.setMaximum (value);
}
if(m_monitoring) {
on_stopButton_clicked();
on_monitorButton_clicked(true);
}
if(m_transmitting) {
on_stopTxButton_clicked();
}
}
QChar MainWindow::current_submode () const
{
QChar submode {0};
if (m_mode.contains (QRegularExpression {R"(^(JT65|JT9|JT4|ISCAT|QRA64)$)"})
&& (m_config.enable_VHF_features () || "JT4" == m_mode || "ISCAT" == m_mode))
{
submode = m_nSubMode + 65;
}
return submode;
}
void MainWindow::on_sbSubmode_valueChanged(int n)
{
m_nSubMode=n;
m_wideGraph->setSubMode(m_nSubMode);
auto submode = current_submode ();
if (submode != QChar::Null)
{
mode_label.setText (m_mode + " " + submode);
}
else
{
mode_label.setText (m_mode);
}
if(m_mode=="ISCAT") {
if(m_nSubMode==0) ui->TxFreqSpinBox->setValue(1012);
if(m_nSubMode==1) ui->TxFreqSpinBox->setValue(560);
}
if(m_mode=="JT9") {
if(m_nSubMode<4) {
ui->cbFast9->setChecked(false);
on_cbFast9_clicked(false);
ui->cbFast9->setEnabled(false);
ui->sbTR->setVisible(false);
m_TRperiod=60;
} else {
ui->cbFast9->setEnabled(true);
}
ui->sbTR->setVisible(m_bFast9);
if(m_bFast9) ui->TxFreqSpinBox->setValue(700);
}
if(m_transmitting and m_bFast9 and m_nSubMode>=4) transmit (99.0);
statusUpdate ();
}
void MainWindow::on_cbFast9_clicked(bool b)
{
if(m_mode=="JT9") {
m_bFast9=b;
// ui->cbAutoSeq->setVisible(b);
}
if(b) {
m_TRperiod = ui->sbTR->value ();
} else {
m_TRperiod=60;
}
progressBar.setMaximum(m_TRperiod);
m_wideGraph->setPeriod(m_TRperiod,m_nsps);
fast_config(b);
statusChanged ();
}
void MainWindow::on_cbShMsgs_toggled(bool b)
{
ui->cbTx6->setEnabled(b);
m_bShMsgs=b;
if(b) ui->cbSWL->setChecked(false);
if(m_bShMsgs and (m_mode=="MSK144")) ui->rptSpinBox->setValue(1);
int itone0=itone[0];
int ntx=m_ntx;
m_lastCallsign.clear (); // ensure Tx5 gets updated
itone[0]=itone0;
if(ntx==1) ui->txrb1->setChecked(true);
if(ntx==2) ui->txrb2->setChecked(true);
if(ntx==3) ui->txrb3->setChecked(true);
if(ntx==4) ui->txrb4->setChecked(true);
if(ntx==5) ui->txrb5->setChecked(true);
if(ntx==6) ui->txrb6->setChecked(true);
}
void MainWindow::on_cbSWL_toggled(bool b)
{
if(b) ui->cbShMsgs->setChecked(false);
}
void MainWindow::on_cbTx6_toggled(bool)
{
}
void MainWindow::locationChange (QString const& location)
{
QString grid {location.trimmed ()};
int len;
// string 6 chars or fewer, interpret as a grid, or use with a 'GRID:' prefix
if (grid.size () > 6) {
if (grid.toUpper ().startsWith ("GRID:")) {
grid = grid.mid (5).trimmed ();
}
else {
// TODO - support any other formats, e.g. latlong? Or have that conversion done external to wsjtx
return;
}
}
if (MaidenheadLocatorValidator::Acceptable == MaidenheadLocatorValidator ().validate (grid, len)) {
qDebug() << "locationChange: Grid supplied is " << grid;
if (m_config.my_grid () != grid) {
m_config.set_dynamic_location (grid);
statusUpdate ();
}
} else {
qDebug() << "locationChange: Invalid grid " << grid;
}
}
void MainWindow::replayDecodes ()
{
// we accept this request even if the setting to accept UDP requests
// is not checked
// attempt to parse the decoded text
Q_FOREACH (auto const& message
, ui->decodedTextBrowser->toPlainText ().split (QChar::LineFeed,
QString::SkipEmptyParts))
{
if (message.size() >= 4 && message.left (4) != "----")
{
auto const& parts = message.split (' ', QString::SkipEmptyParts);
if (parts.size () >= 5 && parts[3].contains ('.')) // WSPR
{
postWSPRDecode (false, parts);
}
else
{
auto eom_pos = message.indexOf (' ', 35);
// we always want at least the characters to position 35
if (eom_pos < 35)
{
eom_pos = message.size () - 1;
}
// TODO - how to skip ISCAT decodes
postDecode (false, message.left (eom_pos + 1));
}
}
}
statusChanged ();
}
void MainWindow::postDecode (bool is_new, QString const& message)
{
#if 0
auto const& decode = message.trimmed ();
auto const& parts = decode.left (22).split (' ', QString::SkipEmptyParts);
if (parts.size () >= 5)
{
auto has_seconds = parts[0].size () > 4;
m_messageClient->decode (is_new
, QTime::fromString (parts[0], has_seconds ? "hhmmss" : "hhmm")
, parts[1].toInt ()
, parts[2].toFloat (), parts[3].toUInt (), parts[4]
, decode.mid (has_seconds ? 24 : 22, 21)
, QChar {'?'} == decode.mid (has_seconds ? 24 + 21 : 22 + 21, 1)
, m_diskData);
}
#endif
if(is_new){
m_rxDirty = true;
}
}
void MainWindow::displayTransmit(){
// Transmit Activity
update_dynamic_property (ui->startTxButton, "transmitting", m_transmitting);
}
void MainWindow::updateButtonDisplay(){
bool isTransmitting = m_transmitting || m_txFrameCount > 0;
auto selectedCallsign = callsignSelected(true);
bool emptyCallsign = selectedCallsign.isEmpty();
ui->hbMacroButton->setDisabled(isTransmitting);
ui->cqMacroButton->setDisabled(isTransmitting);
ui->replyMacroButton->setDisabled(isTransmitting || emptyCallsign);
ui->snrMacroButton->setDisabled(isTransmitting || emptyCallsign);
ui->qtcMacroButton->setDisabled(isTransmitting || m_config.my_station().isEmpty());
ui->qthMacroButton->setDisabled(isTransmitting || m_config.my_qth().isEmpty());
ui->macrosMacroButton->setDisabled(isTransmitting);
ui->queryButton->setDisabled(isTransmitting || emptyCallsign);
ui->deselectButton->setDisabled(isTransmitting || emptyCallsign);
ui->queryButton->setText(emptyCallsign ? "Directed" : QString("Directed to %1").arg(selectedCallsign));
// refresh repeat button text too
updateRepeatButtonDisplay();
}
void MainWindow::updateRepeatButtonDisplay(){
if(ui->hbMacroButton->isChecked() && m_hbInterval > 0 && m_nextHeartbeat.isValid()){
auto secs = DriftingDateTime::currentDateTimeUtc().secsTo(m_nextHeartbeat);
if(secs > 0){
ui->hbMacroButton->setText(QString("HB (%1)").arg(secs));
} else {
ui->hbMacroButton->setText(QString("HB (now)"));
}
} else {
ui->hbMacroButton->setText("HB");
}
if(ui->cqMacroButton->isChecked() && m_cqInterval > 0 && m_nextCQ.isValid()){
auto secs = DriftingDateTime::currentDateTimeUtc().secsTo(m_nextCQ);
if(secs > 0){
ui->cqMacroButton->setText(QString("CQ (%1)").arg(secs));
} else {
ui->cqMacroButton->setText(QString("CQ (now)"));
}
} else {
ui->cqMacroButton->setText("CQ");
}
}
void MainWindow::updateTextDisplay(){
bool isTransmitting = m_transmitting || m_txFrameCount > 0;
bool emptyText = ui->extFreeTextMsgEdit->toPlainText().isEmpty();
ui->startTxButton->setDisabled(isTransmitting || emptyText);
if(m_txTextDirty){
// debounce frame and word count
if(m_txTextDirtyDebounce.isActive()){
m_txTextDirtyDebounce.stop();
}
m_txTextDirtyDebounce.setSingleShot(true);
m_txTextDirtyDebounce.start(100);
m_txTextDirty = false;
}
}
#if __APPLE__
#define USE_SYNC_FRAME_COUNT 0
#else
#define USE_SYNC_FRAME_COUNT 0
#endif
void MainWindow::refreshTextDisplay(){
qDebug() << "refreshing text display...";
auto text = ui->extFreeTextMsgEdit->toPlainText();
#if USE_SYNC_FRAME_COUNT
auto frames = buildMessageFrames(text);
QStringList textList;
qDebug() << "frames:";
foreach(auto frame, frames){
auto dt = DecodedText(frame.first, frame.second);
qDebug() << "->" << frame << dt.message() << Varicode::frameTypeString(dt.frameType());
textList.append(dt.message());
}
auto transmitText = textList.join("");
auto count = frames.length();
// ugh...i hate these globals
m_txTextDirtyLastSelectedCall = callsignSelected(true);
m_txTextDirtyLastText = text;
m_txFrameCountEstimate = count;
m_txTextDirty = false;
updateTextStatsDisplay(transmitText, count);
updateTxButtonDisplay();
#else
// prepare selected callsign for directed message
QString selectedCall = callsignSelected();
//qDebug() << "selected callsign for directed" << selectedCall;
// prepare compound
//bool compound = Varicode::isCompoundCallsign(/*Radio::is_compound_callsign(*/m_config.my_callsign());
QString mycall = m_config.my_callsign();
QString mygrid = m_config.my_grid().left(4);
//QString basecall = Radio::base_callsign(m_config.my_callsign());
//if(basecall != mycall){
// basecall = "<....>";
//}
BuildMessageFramesThread *t = new BuildMessageFramesThread(
mycall,
//basecall,
mygrid,
//compound,
selectedCall,
text
);
connect(t, &BuildMessageFramesThread::finished, t, &QObject::deleteLater);
connect(t, &BuildMessageFramesThread::resultReady, this, [this, text](QString transmitText, int frames){
// ugh...i hate these globals
m_txTextDirtyLastSelectedCall = callsignSelected(true);
m_txTextDirtyLastText = text;
#if TEST_FOX_WAVE_GEN
m_txFrameCountEstimate = ui->turboButton->isChecked() ? (int)ceil(float(frames)/TEST_FOX_WAVE_GEN_SLOTS) : frames;
#else
m_txFrameCountEstimate = frames;
#endif
m_txTextDirty = false;
updateTextStatsDisplay(transmitText, m_txFrameCountEstimate);
updateTxButtonDisplay();
});
t->start();
#endif
}
void MainWindow::updateTextStatsDisplay(QString text, int count){
if(count > 0){
auto words = text.split(" ", QString::SkipEmptyParts).length();
auto wpm = QString::number(words/(count/4.0), 'f', 1);
auto cpm = QString::number(text.length()/(count/4.0), 'f', 1);
wpm_label.setText(QString("%1wpm / %2cpm").arg(wpm).arg(cpm));
wpm_label.setVisible(true);
} else {
wpm_label.setVisible(false);
wpm_label.clear();
}
}
void MainWindow::updateTxButtonDisplay(){
// update transmit button
if(m_tune || m_transmitting || m_txFrameCount > 0){
int count = m_txFrameCount;
#if TEST_FOX_WAVE_GEN
if(ui->turboButton->isChecked()){
count = qMax(1, (int)ceil(float(count)/TEST_FOX_WAVE_GEN_SLOTS));
}
int left = m_txFrameQueue.count();
if(ui->turboButton->isChecked()){
left = (int)ceil(float(left)/TEST_FOX_WAVE_GEN_SLOTS);
}
int sent = qMax(1, count - left);
ui->startTxButton->setText(m_tune ? "Tuning" : QString("%1 (%2/%3)").arg(ui->turboButton->isChecked() ? "Turbo" : "Send").arg(sent).arg(count));
#else
int sent = count - m_txFrameQueue.count();
ui->startTxButton->setText(
m_tune ? "Tuning" : QString("%1 (%2/%3)").arg(m_transmitting ? "Sending" : "Ready").arg(sent).arg(count));
#endif
ui->startTxButton->setEnabled(false);
ui->startTxButton->setFlat(true);
} else {
ui->startTxButton->setText(m_txFrameCountEstimate <= 0 ? QString("Send") : QString("Send (%1)").arg(m_txFrameCountEstimate));
ui->startTxButton->setEnabled(m_txFrameCountEstimate > 0);
ui->startTxButton->setFlat(false);
}
}
QString MainWindow::callsignSelected(bool useInputText){
if(!ui->tableWidgetCalls->selectedItems().isEmpty()){
auto selectedCalls = ui->tableWidgetCalls->selectedItems();
if(!selectedCalls.isEmpty()){
auto call = selectedCalls.first()->data(Qt::UserRole).toString();
if(!call.isEmpty()){
return call;
}
}
}
if(!ui->tableWidgetRXAll->selectedItems().isEmpty()){
int selectedOffset = -1;
auto selectedItems = ui->tableWidgetRXAll->selectedItems();
selectedOffset = selectedItems.first()->data(Qt::UserRole).toInt();
auto keys = m_callActivity.keys();
qStableSort(keys.begin(), keys.end(), [this](QString const &a, QString const &b){
auto tA = m_callActivity[a].utcTimestamp;
auto tB = m_callActivity[b].utcTimestamp;
if(tA == tB){
return a < b;
}
return tB < tA;
});
foreach(auto call, keys){
auto d = m_callActivity[call];
if(d.freq == selectedOffset){
return d.call;
}
}
}
#if ALLOW_USE_INPUT_TEXT_CALLSIGN
if(useInputText){
auto text = ui->extFreeTextMsgEdit->toPlainText().left(11); // Maximum callsign is 6 + / + 4 = 11 characters
auto calls = Varicode::parseCallsigns(text);
if(!calls.isEmpty() && text.startsWith(calls.first()) && calls.first() != m_config.my_callsign()){
return calls.first();
}
}
#endif
return QString();
}
void MainWindow::clearCallsignSelected(){
ui->tableWidgetCalls->clearSelection();
ui->tableWidgetRXAll->clearSelection();
}
bool MainWindow::isRecentOffset(int offset){
if(abs(offset - currentFreqOffset()) <= NEAR_THRESHOLD_RX){
return true;
}
return (
m_rxRecentCache.contains(offset/10*10) &&
m_rxRecentCache[offset/10*10]->secsTo(DriftingDateTime::currentDateTimeUtc()) < 120
);
}
void MainWindow::markOffsetRecent(int offset){
m_rxRecentCache.insert(offset/10*10, new QDateTime(DriftingDateTime::currentDateTimeUtc()), 10);
m_rxRecentCache.insert(offset/10*10+10, new QDateTime(DriftingDateTime::currentDateTimeUtc()), 10);
}
bool MainWindow::isDirectedOffset(int offset, bool *pIsAllCall){
bool isDirected = (
m_rxDirectedCache.contains(offset/10*10) &&
m_rxDirectedCache[offset/10*10]->date.secsTo(DriftingDateTime::currentDateTimeUtc()) < 120
);
if(isDirected){
if(pIsAllCall) *pIsAllCall = m_rxDirectedCache[offset/10*10]->isAllcall;
}
return isDirected;
}
void MainWindow::markOffsetDirected(int offset, bool isAllCall){
CachedDirectedType *d1 = new CachedDirectedType{ isAllCall, DriftingDateTime::currentDateTimeUtc() };
CachedDirectedType *d2 = new CachedDirectedType{ isAllCall, DriftingDateTime::currentDateTimeUtc() };
m_rxDirectedCache.insert(offset/10*10, d1, 10);
m_rxDirectedCache.insert(offset/10*10+10, d2, 10);
}
void MainWindow::clearOffsetDirected(int offset){
m_rxDirectedCache.remove(offset/10*10);
m_rxDirectedCache.remove(offset/10*10+10);
}
bool MainWindow::isMyCallIncluded(const QString &text){
QString myCall = Radio::base_callsign(m_config.my_callsign());
if(myCall.isEmpty()){
return false;
}
return text.contains(myCall);
}
bool MainWindow::isAllCallIncluded(const QString &text){
return text.contains("@ALLCALL");
}
bool MainWindow::isGroupCallIncluded(const QString &text){
return m_config.my_groups().contains(text);
}
void MainWindow::processActivity(bool force) {
if (!m_rxDirty && !force) {
return;
}
// Recent Rx Activity
processRxActivity();
// Process Idle Activity
processIdleActivity();
// Grouped Compound Activity
processCompoundActivity();
// Buffered Activity
processBufferedActivity();
// Command Activity
processCommandActivity();
// Process PSKReporter Spots
processSpots();
m_rxDirty = false;
}
void MainWindow::observeTimeDeltaForAverage(float delta){
// delta can only be +/- 15 seconds
delta = qMax(-15.0F, qMin(delta, 15.0F));
// compute average drift
if(m_timeDeltaMsMMA_N == 0){
m_timeDeltaMsMMA_N++;
m_timeDeltaMsMMA = (int)(delta*1000);
} else {
m_timeDeltaMsMMA_N++;
m_timeDeltaMsMMA = (((m_timeDeltaMsMMA_N-1)*m_timeDeltaMsMMA) + (int)(delta*1000))/ min(m_timeDeltaMsMMA_N, 100);
}
// display average
if(m_timeDeltaMsMMA < -15.0F || m_timeDeltaMsMMA > 15.0F){
resetTimeDeltaAverage();
}
ui->driftAvgLabel->setText(QString("Avg Time Delta: %1 ms").arg((int)m_timeDeltaMsMMA));
}
void MainWindow::resetTimeDeltaAverage(){
m_timeDeltaMsMMA = 0;
m_timeDeltaMsMMA_N = 0;
// observe zero for reset
observeTimeDeltaForAverage(0);
}
void MainWindow::processIdleActivity() {
auto now = DriftingDateTime::currentDateTimeUtc();
// if we detect an idle offset, insert an ellipsis into the activity queue and band activity
foreach(auto offset, m_bandActivity.keys()){
auto details = m_bandActivity[offset];
if(details.isEmpty()){
continue;
}
auto last = details.last();
if((last.bits & Varicode::JS8CallLast) == Varicode::JS8CallLast){
continue;
}
if(last.utcTimestamp.secsTo(now) < m_TRperiod){
continue;
}
if(last.text == " . . . "){
continue;
}
ActivityDetail d = {};
d.text = " . . . ";
d.isFree = true;
d.utcTimestamp = last.utcTimestamp;
d.snr = last.snr;
d.tdrift = last.tdrift;
d.freq = last.freq;
if(hasExistingMessageBuffer(offset, false, nullptr)){
m_messageBuffer[offset].msgs.append(d);
}
m_rxActivityQueue.append(d);
m_bandActivity[offset].append(d);
}
}
void MainWindow::processRxActivity() {
if(m_rxActivityQueue.isEmpty()){
return;
}
int freqOffset = currentFreqOffset();
while (!m_rxActivityQueue.isEmpty()) {
ActivityDetail d = m_rxActivityQueue.dequeue();
observeTimeDeltaForAverage(d.tdrift);
// use the actual frequency and check its delta from our current frequency
// meaning, if our current offset is 1502 and the d.freq is 1492, the delta is <= 10;
bool shouldDisplay = abs(d.freq - freqOffset) <= NEAR_THRESHOLD_RX;
int prevOffset = d.freq;
if(hasExistingMessageBuffer(d.freq, false, &prevOffset) && (
(m_messageBuffer[prevOffset].cmd.to == m_config.my_callsign()) ||
// (isAllCallIncluded(m_messageBuffer[prevOffset].cmd.to)) || // uncomment this if we want to incrementally print allcalls
(isGroupCallIncluded(m_messageBuffer[prevOffset].cmd.to))
)
){
d.isBuffered = true;
shouldDisplay = true;
if(!m_messageBuffer[prevOffset].compound.isEmpty()){
//qDebug() << "should display compound too because at this point it hasn't been displayed" << m_messageBuffer[prevOffset].compound.last().call;
auto lastCompound = m_messageBuffer[prevOffset].compound.last();
// fixup compound call incremental text
d.text = QString("%1: %2").arg(lastCompound.call).arg(d.text);
d.utcTimestamp = qMin(d.utcTimestamp, lastCompound.utcTimestamp);
}
} else {
// if this is a _partial_ directed message, skip until the complete call comes through.
if(d.isDirected && d.text.contains("<....>")){
continue;
}
if(d.isDirected && d.text.contains(": HB ")){ // TODO: HEARTBEAT
continue;
}
}
// TODO: incremental printing of directed messages
// Display if:
// 1) this is a directed message header "to" us and should be buffered...
// 2) or, this is a buffered message frame for a buffer with us as the recipient.
if(!shouldDisplay){
continue;
}
bool isFirst = (d.bits & Varicode::JS8CallFirst) == Varicode::JS8CallFirst;
bool isLast = (d.bits & Varicode::JS8CallLast) == Varicode::JS8CallLast;
// if we're the last message, let's display our EOT character
if (isLast) {
// can also use \u0004 \u2666 \u2404
d.text = QString("%1 \u2301 ").arg(d.text);
}
// log it to the display!
displayTextForFreq(d.text, d.freq, d.utcTimestamp, false, isFirst, isLast);
// if we've received a message to be displayed, we should bump the repeat buttons...
resetAutomaticIntervalTransmissions(true, false);
if(isLast){
clearOffsetDirected(d.freq);
}
if(isLast && !d.isBuffered){
// buffered commands need the rxFrameBlockNumbers cache so it can fixup its display
// all other "last" data frames can clear the rxFrameBlockNumbers cache so the next message will be on a new line.
m_rxFrameBlockNumbers.remove(d.freq);
}
}
#if 0
// TODO: this works but should also print in the rx window.
foreach(auto offset, m_bandActivity.keys()){
if(seen.contains(offset)){
continue;
}
if(m_bandActivity[offset].isEmpty()){
continue;
}
auto last = m_bandActivity[offset].last();
if((last.bits & Varicode::JS8CallLast) == Varicode::JS8CallLast){
continue;
}
auto now = DriftingDateTime::currentDateTimeUtc();
if(last.utcTimestamp.secsTo(now) < m_TRperiod){
continue;
}
ActivityDetail d = {};
d.text = " . . . ";
d.isFree = true;
d.utcTimestamp = now;
d.snr = -99;
m_bandActivity[offset].append(d);
}
#endif
}
void MainWindow::processCompoundActivity() {
if(m_messageBuffer.isEmpty()){
return;
}
// group compound callsign and directed commands together.
foreach(auto freq, m_messageBuffer.keys()) {
QMap < int, MessageBuffer > ::iterator i = m_messageBuffer.find(freq);
MessageBuffer & buffer = i.value();
qDebug() << "-> grouping buffer for freq" << freq;
if (buffer.compound.isEmpty()) {
qDebug() << "-> buffer.compound is empty...skip";
continue;
}
// if we don't have an initialized command, skip...
int bits = buffer.cmd.bits;
bool validBits = (
bits == Varicode::JS8Call ||
((bits & Varicode::JS8CallFirst) == Varicode::JS8CallFirst) ||
((bits & Varicode::JS8CallLast) == Varicode::JS8CallLast) ||
((bits & Varicode::JS8CallFlag) == Varicode::JS8CallFlag)
);
if (!validBits) {
qDebug() << "-> buffer.cmd bits is invalid...skip";
continue;
}
// if we need two compound calls, but less than two have arrived...skip
if (buffer.cmd.from == "<....>" && buffer.cmd.to == "<....>" && buffer.compound.length() < 2) {
qDebug() << "-> buffer needs two compound, but has less...skip";
continue;
}
// if we need one compound call, but non have arrived...skip
if ((buffer.cmd.from == "<....>" || buffer.cmd.to == "<....>") && buffer.compound.length() < 1) {
qDebug() << "-> buffer needs one compound, but has less...skip";
continue;
}
if (buffer.cmd.from == "<....>") {
auto d = buffer.compound.dequeue();
buffer.cmd.from = d.call;
buffer.cmd.grid = d.grid;
buffer.cmd.isCompound = true;
buffer.cmd.utcTimestamp = qMin(buffer.cmd.utcTimestamp, d.utcTimestamp);
if ((d.bits & Varicode::JS8CallLast) == Varicode::JS8CallLast) {
buffer.cmd.bits = d.bits;
}
}
if (buffer.cmd.to == "<....>") {
auto d = buffer.compound.dequeue();
buffer.cmd.to = d.call;
buffer.cmd.isCompound = true;
buffer.cmd.utcTimestamp = qMin(buffer.cmd.utcTimestamp, d.utcTimestamp);
if ((d.bits & Varicode::JS8CallLast) == Varicode::JS8CallLast) {
buffer.cmd.bits = d.bits;
}
}
if ((buffer.cmd.bits & Varicode::JS8CallLast) != Varicode::JS8CallLast) {
qDebug() << "-> still not last message...skip";
continue;
}
// fixup the datetime with the "minimum" dt seen
// this will allow us to delete the activity lines
// when the compound buffered command comes in.
auto dt = buffer.cmd.utcTimestamp;
foreach(auto c, buffer.compound){
dt = qMin(dt, c.utcTimestamp);
}
foreach(auto m, buffer.msgs){
dt = qMin(dt, m.utcTimestamp);
}
buffer.cmd.utcTimestamp = dt;
qDebug() << "buffered compound command ready" << buffer.cmd.from << buffer.cmd.to << buffer.cmd.cmd;
m_rxCommandQueue.append(buffer.cmd);
m_messageBuffer.remove(freq);
}
}
void MainWindow::processBufferedActivity() {
if(m_messageBuffer.isEmpty()){
return;
}
foreach(auto freq, m_messageBuffer.keys()) {
auto buffer = m_messageBuffer[freq];
// check to make sure we empty old buffers by getting the latest timestamp
// and checking to see if it's older than one minute.
auto dt = DriftingDateTime::currentDateTimeUtc().addDays(-1);
if(buffer.cmd.utcTimestamp.isValid()){
dt = qMax(dt, buffer.cmd.utcTimestamp);
}
if(!buffer.compound.isEmpty()){
dt = qMax(dt, buffer.compound.last().utcTimestamp);
}
if(!buffer.msgs.isEmpty()){
dt = qMax(dt, buffer.msgs.last().utcTimestamp);
}
if(dt.secsTo(DriftingDateTime::currentDateTimeUtc()) > 60){
m_messageBuffer.remove(freq);
continue;
}
if (buffer.msgs.isEmpty()) {
continue;
}
if ((buffer.msgs.last().bits & Varicode::JS8CallLast) != Varicode::JS8CallLast) {
continue;
}
QString message;
foreach(auto part, buffer.msgs) {
message.append(part.text);
}
message = Varicode::rstrip(message);
QString checksum;
bool valid = false;
if(Varicode::isCommandBuffered(buffer.cmd.cmd)){
int checksumSize = Varicode::isCommandChecksumed(buffer.cmd.cmd);
if(checksumSize == 32) {
message = Varicode::lstrip(message);
checksum = message.right(6);
message = message.left(message.length() - 7);
valid = Varicode::checksum32Valid(checksum, message);
} else if(checksumSize == 16) {
message = Varicode::lstrip(message);
checksum = message.right(3);
message = message.left(message.length() - 4);
valid = Varicode::checksum16Valid(checksum, message);
} else if (checksumSize == 0) {
valid = true;
}
} else {
valid = true;
}
if (valid) {
buffer.cmd.bits |= Varicode::JS8CallLast;
buffer.cmd.text = message;
buffer.cmd.isBuffered = true;
m_rxCommandQueue.append(buffer.cmd);
} else {
qDebug() << "Buffered message failed checksum...discarding";
qDebug() << "Checksum:" << checksum;
qDebug() << "Message:" << message;
}
// regardless of valid or not, remove the "complete" buffered message from the buffer cache
m_messageBuffer.remove(freq);
}
}
void MainWindow::processCommandActivity() {
#if 0
if (!m_txFrameQueue.isEmpty()) {
return;
}
#endif
if (m_rxCommandQueue.isEmpty()) {
return;
}
#if 0
bool processed = false;
int f = currentFreq();
#endif
auto now = DriftingDateTime::currentDateTimeUtc();
while (!m_rxCommandQueue.isEmpty()) {
auto d = m_rxCommandQueue.dequeue();
bool isAllCall = isAllCallIncluded(d.to);
bool isGroupCall = isGroupCallIncluded(d.to);
qDebug() << "try processing command" << d.from << d.to << d.cmd << d.freq << d.grid << d.extra << isAllCall << isGroupCall;
// if we need a compound callsign but never got one...skip
if (d.from == "<....>" || d.to == "<....>") {
continue;
}
// we're only processing a subset of queries at this point
if (!Varicode::isCommandAllowed(d.cmd)) {
continue;
}
// is this to me?
bool toMe = d.to == m_config.my_callsign().trimmed() || d.to == Radio::base_callsign(m_config.my_callsign()).trimmed();
// log call activity...
CallDetail cd = {};
cd.call = d.from;
cd.grid = d.grid;
cd.snr = d.snr;
cd.freq = d.freq;
cd.bits = d.bits;
cd.ackTimestamp = d.text.contains(": ACK") || toMe ? d.utcTimestamp : QDateTime{};
cd.utcTimestamp = d.utcTimestamp;
cd.tdrift = d.tdrift;
logCallActivity(cd, true);
// we're only responding to allcall, groupcalls, and our callsign at this point, so we'll end after logging the callsigns we've heard
if (!isAllCall && !toMe && !isGroupCall) {
continue;
}
// we're only responding to allcalls if we are participating in the allcall group
// but, don't avoid for heartbeats...those are technically allcalls but are processed differently
if(isAllCall && m_config.avoid_allcall() && d.cmd != " HB"){
continue;
}
// display the command activity
ActivityDetail ad = {};
ad.isLowConfidence = false;
ad.isFree = true;
ad.isDirected = true;
ad.bits = d.bits;
ad.freq = d.freq;
ad.snr = d.snr;
ad.text = QString("%1: %2%3 ").arg(d.from).arg(d.to).arg(d.cmd);
if(!d.extra.isEmpty()){
ad.text += d.extra;
}
if(!d.text.isEmpty()){
ad.text += d.text;
}
bool isLast = (ad.bits & Varicode::JS8CallLast) == Varicode::JS8CallLast;
if (isLast) {
// can also use \u0004 \u2666 \u2404
ad.text += QString(" \u2301 ");
}
ad.utcTimestamp = d.utcTimestamp;
// we'd be double printing here if were on frequency, so let's be "smart" about this...
bool shouldDisplay = true;
// don't display ping allcalls
if(isAllCall && (d.cmd != " " || ad.text.contains(": HB "))){
shouldDisplay = false;
}
if(shouldDisplay){
auto c = ui->textEditRX->textCursor();
c.movePosition(QTextCursor::End);
ui->textEditRX->setTextCursor(c);
// ACKs are the most likely source of items to be overwritten (multiple responses at once)...
// so don't overwrite those (i.e., print each on a new line)
bool shouldOverwrite = (!d.cmd.contains(" ACK")); /* && isRecentOffset(d.freq);*/
if(shouldOverwrite && ui->textEditRX->find(d.utcTimestamp.time().toString(), QTextDocument::FindBackward)){
// ... maybe we could delete the last line that had this message on this frequency...
c = ui->textEditRX->textCursor();
c.movePosition(QTextCursor::StartOfBlock);
c.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
qDebug() << "should display directed message, erasing last rx activity line..." << c.selectedText();
c.deletePreviousChar();
c.deletePreviousChar();
c.deleteChar();
c.deleteChar();
}
// log it to the display!
displayTextForFreq(ad.text, ad.freq, ad.utcTimestamp, false, true, false);
// and send it to the network in case we want to interact with it from an external app...
sendNetworkMessage("RX.DIRECTED", ad.text, {
{"FROM", QVariant(d.from)},
{"TO", QVariant(d.to)},
{"CMD", QVariant(d.cmd)},
{"GRID", QVariant(d.grid)},
{"EXTRA", QVariant(d.extra)},
{"TEXT", QVariant(d.text)},
{"FREQ", QVariant(ad.freq)},
{"SNR", QVariant(ad.snr)},
{"UTC", QVariant(ad.utcTimestamp.toMSecsSinceEpoch())}
});
if(!isAllCall){
// if we've received a message to be displayed, we should bump the repeat buttons...
resetAutomaticIntervalTransmissions(true, false);
// and we should play the sound notification if there is one...
playSoundNotification(m_config.sound_dm_path());
}
writeDirectedCommandToFile(d);
}
// if this is an allcall, check to make sure we haven't replied to their allcall recently (in the past ten minutes)
// that way we never get spammed by allcalls at too high of a frequency
if (isAllCall && m_txAllcallCommandCache.contains(d.from) && m_txAllcallCommandCache[d.from]->secsTo(now) / 60 < 10) {
continue;
}
// and mark the offset as a directed offset so future free text is displayed
// markOffsetDirected(ad.freq, isAllCall);
// construct a reply, if needed
QString reply;
int priority = PriorityNormal;
int freq = -1;
// QUERIED SNR
if (d.cmd == " SNR?" && !isAllCall) {
reply = QString("%1 SNR %2").arg(d.from).arg(Varicode::formatSNR(d.snr));
}
// QUERIED QTH
else if (d.cmd == " QTH?" && !isAllCall) {
QString qth = m_config.my_qth();
if (qth.isEmpty()) {
continue;
}
reply = QString("%1 QTH %2").arg(d.from).arg(replaceMacros(qth, buildMacroValues(), true));
}
// QUERIED ACTIVE
else if (d.cmd == " STATUS?" && !isAllCall) {
reply = QString("%1 AUTO:%2 VER:%3").arg(d.from).arg(ui->autoReplyButton->isChecked() ? "ON" : "OFF").arg(version());
}
// QUERIED GRID
else if (d.cmd == " GRID?" && !isAllCall) {
QString grid = m_config.my_grid();
if (grid.isEmpty()) {
continue;
}
reply = QString("%1 GRID %2").arg(d.from).arg(grid);
}
// QUERIED STATION MESSAGE
else if (d.cmd == " QTC?" && !isAllCall) {
QString qtc = m_config.my_station();
if(qtc.isEmpty()) {
continue;
}
reply = QString("%1 QTC %2").arg(d.from).arg(replaceMacros(qtc, buildMacroValues(), true));
}
// QUERIED STATIONS HEARD
else if (d.cmd == " HEARING?" && !isAllCall) {
int i = 0;
int maxStations = 4;
auto calls = m_callActivity.keys();
qStableSort(calls.begin(), calls.end(), [this](QString
const & a, QString
const & b) {
auto left = m_callActivity[a];
auto right = m_callActivity[b];
return right.utcTimestamp < left.utcTimestamp;
});
QStringList lines;
int callsignAging = m_config.callsign_aging();
foreach(auto call, calls) {
if (i >= maxStations) {
break;
}
if(call == d.from){
continue;
}
auto cd = m_callActivity[call];
if (callsignAging && cd.utcTimestamp.secsTo(now) / 60 >= callsignAging) {
continue;
}
lines.append(cd.call);
i++;
}
lines.prepend(QString("%1 HEARING").arg(d.from));
reply = lines.join(' ');
}
// PROCESS RELAY
else if (d.cmd == ">" && !isAllCall && !isGroupCall) {
// 1. see if there are any more hops to process
// 2. if so, forward
// 3. otherwise, display alert & reply dialog
QString callToPattern = {R"(^(?<callsign>\b(?<prefix>[A-Z0-9]{1,4}\/)?(?<base>([0-9A-Z])?([0-9A-Z])([0-9])([A-Z])?([A-Z])?([A-Z])?)(?<suffix>\/[A-Z0-9]{1,4})?(?<type>[> ]))\b)"};
QRegularExpression re(callToPattern);
auto text = d.text;
auto match = re.match(text);
// if the text starts with a callsign, and relay is not disabled, then relay.
if(match.hasMatch() && !m_config.relay_off()){
// replace freetext with relayed free text
if(match.captured("type") != ">"){
text = text.replace(match.capturedStart("type"), match.capturedLength("type"), ">");
}
reply = QString("%1 DE %2").arg(text).arg(d.from);
// otherwise, as long as we're not an ACK...alert the user and either send an ACK or Message
} else if(!d.text.startsWith("ACK")) {
QStringList calls;
QString callDePattern = {R"(\sDE\s(?<callsign>\b(?<prefix>[A-Z0-9]{1,4}\/)?(?<base>([0-9A-Z])?([0-9A-Z])([0-9])([A-Z])?([A-Z])?([A-Z])?)(?<suffix>\/[A-Z0-9]{1,4})?)\b)"};
QRegularExpression re(callDePattern);
auto iter = re.globalMatch(text);
while(iter.hasNext()){
auto match = iter.next();
calls.prepend(match.captured("callsign"));
}
// put these third party calls in the heard list
foreach(auto call, calls){
CallDetail cd = {};
cd.call = call;
cd.snr = -64;
cd.freq = d.freq;
cd.through = d.from;
cd.utcTimestamp = DriftingDateTime::currentDateTimeUtc();
cd.tdrift = d.tdrift;
logCallActivity(cd, false);
}
calls.prepend(d.from);
auto relayPath = calls.join('>');
reply = QString("%1 ACK").arg(relayPath);
// put message in inbox instead...
d.relayPath = relayPath;
m_rxCallsignCommandQueue[d.from].append(d);
}
}
// PROCESS AGN
else if (d.cmd == " AGN?" && !isAllCall && !isGroupCall && !m_lastTxMessage.isEmpty()) {
reply = m_lastTxMessage;
}
// PROCESS ACTIVE HEARTBEAT
// if we have auto reply enabled and we are heartbeating and selcall is not enabled
else if (d.cmd == " HB" && ui->autoReplyButton->isChecked() && ui->hbMacroButton->isChecked() && m_hbInterval > 0){
sendHeartbeatAck(d.from, d.snr);
if(isAllCall){
// since all pings are technically @ALLCALL, let's bump the allcall cache here...
m_txAllcallCommandCache.insert(d.from, new QDateTime(now), 5);
}
// make sure this is explicit
continue;
}
// PROCESS BUFFERED QUERY
else if (d.cmd == " QUERY" && ui->autoReplyButton->isChecked()){
auto who = d.text;
if(who.isEmpty()){
continue;
}
auto callsigns = Varicode::parseCallsigns(who);
if(callsigns.isEmpty()){
continue;
}
QStringList replies;
int callsignAging = m_config.callsign_aging();
auto baseCall = callsigns.first();
foreach(auto cd, m_callActivity.values()){
if (callsignAging && cd.utcTimestamp.secsTo(now) / 60 >= callsignAging) {
continue;
}
if(baseCall == cd.call || baseCall == Radio::base_callsign(cd.call)){
auto r = QString("%1 ACK %2").arg(cd.call).arg(Varicode::formatSNR(cd.snr));
replies.append(r);
}
}
reply = replies.join("\n");
if(!reply.isEmpty()){
if(isAllCall){
// since all pings are technically @ALLCALL, let's bump the allcall cache here...
m_txAllcallCommandCache.insert(d.from, new QDateTime(now), 25);
}
}
}
// PROCESS BUFFERED APRS:
else if(d.cmd == " APRS:" && m_config.spot_to_reporting_networks() && m_aprsClient->isPasscodeValid()){
m_aprsClient->enqueueThirdParty(Radio::base_callsign(d.from), d.text);
// make sure this is explicit
continue;
}
// PROCESS BUFFERED QTH
else if (d.cmd == " GRID"){
// 1. parse grids
// 2. log it to reporting networks
auto grids = Varicode::parseGrids(d.text);
foreach(auto grid, grids){
CallDetail cd = {};
cd.bits = d.bits;
cd.call = d.from;
cd.freq = d.freq;
cd.grid = grid;
cd.snr = d.snr;
cd.utcTimestamp = d.utcTimestamp;
cd.tdrift = d.tdrift;
m_aprsCallCache.remove(cd.call);
m_aprsCallCache.remove(APRSISClient::replaceCallsignSuffixWithSSID(cd.call, Radio::base_callsign(cd.call)));
logCallActivity(cd, true);
}
// make sure this is explicit
continue;
}
#if 0
// PROCESS ALERT
else if (d.cmd == "!" && !isAllCall) {
// create alert dialog
processAlertReplyForCommand(d, d.from, " ");
// make sure this is explicit
continue;
}
#endif
// well, if there's no reply, don't do anything...
if (reply.isEmpty()) {
continue;
}
// do not queue @ALLCALL replies if auto-reply is not checked
if(!ui->autoReplyButton->isChecked() && isAllCall){
continue;
}
// do not queue a reply if it's a HB and HB is not active
if((!ui->hbMacroButton->isChecked() || m_hbInterval <= 0) && d.cmd.contains("HB")){
}
// do not queue for reply if there's text in the window
if(!ui->extFreeTextMsgEdit->toPlainText().isEmpty()){
continue;
}
// add @ALLCALLs to the @ALLCALL cache
if(isAllCall){
m_txAllcallCommandCache.insert(d.from, new QDateTime(now), 25);
}
// queue the reply here to be sent when a free interval is available on the frequency that was sent
// unless, this is an allcall, to which we should be responding on a clear frequency offset
// we always want to make sure that the directed cache has been updated at this point so we have the
// most information available to make a frequency selection.
enqueueMessage(priority, reply, freq, nullptr);
}
}
void MainWindow::writeDirectedCommandToFile(CommandDetail d){
// open file /save/messages/[callsign].txt and append a message log entry...
QFile f(QDir::toNativeSeparators(m_config.writeable_data_dir ().absolutePath()) + QString("/save/messages/%1.txt").arg(Radio::base_callsign(d.from)));
if (f.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append)) {
QTextStream out(&f);
auto df = dialFrequency();
auto text = QString("%1\t%2MHz\t%3Hz\t%4dB\t%5");
text = text.arg(d.utcTimestamp.toString("yyyy-MM-dd hh:mm:ss"));
text = text.arg(Radio::frequency_MHz_string(df));
text = text.arg(d.freq);
text = text.arg(Varicode::formatSNR(d.snr));
text = text.arg(d.text.isEmpty() ? QString("%1 %2").arg(d.cmd).arg(d.extra).trimmed() : d.text);
out << text << endl;
f.close();
}
}
void MainWindow::processAlertReplyForCommand(CommandDetail d, QString from, QString cmd){
QMessageBox * msgBox = new QMessageBox(this);
msgBox->setIcon(QMessageBox::Information);
QList<QString> calls = listCopyReverse<QString>(from.split(">"));
auto fromLabel = calls.join(" via ");
calls.removeLast();
QString fromReplace = QString{};
foreach(auto call, calls){
fromReplace.append(" DE ");
fromReplace.append(call);
}
auto text = d.text;
if(!fromReplace.isEmpty()){
text = text.replace(fromReplace, "");
}
auto header = QString("Message from %3 at %1 (%2):");
header = header.arg(d.utcTimestamp.time().toString());
header = header.arg(d.freq);
header = header.arg(fromLabel);
msgBox->setText(header);
msgBox->setInformativeText(text);
auto rb = msgBox->addButton("Reply", QMessageBox::AcceptRole);
auto db = msgBox->addButton("Discard", QMessageBox::NoRole);
connect(msgBox, &QMessageBox::buttonClicked, this, [this, cmd, from, fromLabel, d, db, rb](QAbstractButton * btn) {
if (btn == db) {
displayCallActivity();
return;
}
if(btn == rb){
#if USE_RELAY_REPLY_DIALOG
auto diag = new MessageReplyDialog(this);
diag->setWindowTitle("Message Reply");
diag->setLabel(QString("Message to send to %1:").arg(fromLabel));
connect(diag, &MessageReplyDialog::accepted, this, [this, diag, from, cmd, d](){
enqueueMessage(PriorityHigh, QString("%1%2%3").arg(from).arg(cmd).arg(diag->textValue()), -1, nullptr);
});
diag->show();
#else
addMessageText(QString("%1%2[MESSAGE]").arg(from).arg(cmd), true, true);
#endif
}
});
playSoundNotification(m_config.sound_am_path());
msgBox->setModal(false);
msgBox->show();
}
void MainWindow::processSpots() {
if(!ui->spotButton->isChecked()){
m_rxCallQueue.clear();
return;
}
if(m_rxCallQueue.isEmpty()){
return;
}
// Is it ok to post spots to PSKReporter?
int nsec = DriftingDateTime::currentMSecsSinceEpoch() / 1000 - m_secBandChanged;
bool okToPost = (nsec > (4 * m_TRperiod) / 5);
if (!okToPost) {
return;
}
// Process spots to be sent...
pskSetLocal();
aprsSetLocal();
while(!m_rxCallQueue.isEmpty()){
CallDetail d = m_rxCallQueue.dequeue();
if(d.call.isEmpty()){
continue;
}
qDebug() << "spotting call to reporting networks" << d.call << d.snr << d.freq;
pskLogReport("JS8", d.freq, d.snr, d.call, d.grid);
aprsLogReport(d.freq, d.snr, d.call, d.grid);
}
}
void MainWindow::processTxQueue(){
#if IDLE_BLOCKS_TX
if(m_tx_watchdog){
return;
}
#endif
if(m_txMessageQueue.isEmpty()){
return;
}
// grab the next message...
auto head = m_txMessageQueue.head();
// decide if it's ok to transmit...
int f = head.freq;
if(f == -1){
f = currentFreqOffset();
}
// we need a valid frequency...
if(f <= 0){
return;
}
// tx frame queue needs to be empty...
if(!m_txFrameQueue.isEmpty()){
return;
}
// our message box needs to be empty...
if(!ui->extFreeTextMsgEdit->toPlainText().isEmpty()){
return;
}
// and if we are a low priority message, we need to have not transmitted in the past 30 seconds...
if(head.priority <= PriorityLow && m_lastTxTime.secsTo(DriftingDateTime::currentDateTimeUtc()) <= 30){
return;
}
// if so... dequeue the next message from the queue...
auto message = m_txMessageQueue.dequeue();
// add the message to the outgoing message text box
addMessageText(message.message, true);
// check to see if this is a high priority message, or if we have autoreply enabled, or if this is a ping and the ping button is enabled
if(message.priority >= PriorityHigh ||
message.message.contains(" HB ") ||
message.message.contains(" ACK ") ||
ui->autoReplyButton->isChecked()
){
// then try to set the frequency...
setFreqOffsetForRestore(f, true);
// then prepare to transmit...
toggleTx(true);
}
if(message.callback){
message.callback();
}
}
void MainWindow::displayActivity(bool force) {
if (!m_rxDisplayDirty && !force) {
return;
}
// Band Activity
displayBandActivity();
// Call Activity
displayCallActivity();
m_rxDisplayDirty = false;
}
// updateBandActivity
void MainWindow::displayBandActivity() {
auto now = DriftingDateTime::currentDateTimeUtc();
ui->tableWidgetRXAll->setFont(m_config.table_font());
// Selected Offset
int selectedOffset = -1;
auto selectedItems = ui->tableWidgetRXAll->selectedItems();
if (!selectedItems.isEmpty()) {
selectedOffset = selectedItems.first()->data(Qt::UserRole).toInt();
}
ui->tableWidgetRXAll->setUpdatesEnabled(false);
{
// Scroll Position
auto currentScrollPos = ui->tableWidgetRXAll->verticalScrollBar()->value();
// Clear the table
clearTableWidget(ui->tableWidgetRXAll);
// Sort!
QList < int > keys = m_bandActivity.keys();
auto sortBy = getSortBy("bandActivity", "offset");
bool reverse = false;
if(sortBy.startsWith("-")){
sortBy = sortBy.mid(1);
reverse = true;
}
auto compareTimestamp = [this](const int left, int right) {
auto leftItems = m_bandActivity[left];
auto rightItems = m_bandActivity[right];
if(leftItems.isEmpty()){
return false;
}
if(rightItems.isEmpty()){
return true;
}
auto leftLast = leftItems.last();
auto rightLast = rightItems.last();
return leftLast.utcTimestamp < rightLast.utcTimestamp;
};
auto compareSNR = [this, reverse](const int left, int right) {
auto leftItems = m_bandActivity[left];
auto rightItems = m_bandActivity[right];
if(leftItems.isEmpty()){
return false;
}
if(rightItems.isEmpty()){
return true;
}
auto leftActivity = leftItems.last();
auto rightActivity = rightItems.last();
if(leftActivity.snr < -60 || leftActivity.snr > 60) {
leftActivity.snr *= reverse ? 1 : -1;
}
if(rightActivity.snr < -60 || rightActivity.snr > 60) {
rightActivity.snr *= reverse ? 1 : -1;
}
return leftActivity.snr < rightActivity.snr;
};
// compare offset
qStableSort(keys.begin(), keys.end());
if(sortBy == "timestamp"){
qStableSort(keys.begin(), keys.end(), compareTimestamp);
} else if(sortBy == "snr"){
qStableSort(keys.begin(), keys.end(), compareSNR);
}
if(reverse){
keys = listCopyReverse(keys);
}
// Build the table
foreach(int offset, keys) {
bool isOffsetSelected = (offset == selectedOffset);
QList < ActivityDetail > items = m_bandActivity[offset];
if (items.length() > 0) {
QStringList text;
QString age;
int snr = 0;
float tdrift = 0;
int activityAging = m_config.activity_aging();
foreach(ActivityDetail item, items) {
if (!isOffsetSelected && activityAging && item.utcTimestamp.secsTo(now) / 60 >= activityAging) {
continue;
}
if (m_hbHidden && (item.text.contains(" HB ") || item.text.contains(" ACK "))){
// hide heartbeats and acks if we have heartbeating hidden
continue;
}
if (item.text.isEmpty()) {
continue;
}
#if 0
if (item.isCompound || (item.isDirected && item.text.contains("<....>"))){
//continue;
item.text = "[...]";
}
#endif
if (item.isLowConfidence) {
item.text = QString("[%1]").arg(item.text);
}
if ((item.bits & Varicode::JS8CallLast) == Varicode::JS8CallLast) {
// can also use \u0004 \u2666 \u2404
item.text = QString("%1 \u2301 ").arg(item.text);
}
text.append(item.text);
snr = item.snr;
age = since(item.utcTimestamp);
tdrift = item.tdrift;
}
auto joined = text.join("");
if (joined.isEmpty()) {
continue;
}
ui->tableWidgetRXAll->insertRow(ui->tableWidgetRXAll->rowCount());
int row = ui->tableWidgetRXAll->rowCount() - 1;
int col = 0;
auto offsetItem = new QTableWidgetItem(QString("%1 Hz").arg(offset));
offsetItem->setData(Qt::UserRole, QVariant(offset));
ui->tableWidgetRXAll->setItem(row, col++, offsetItem);
auto ageItem = new QTableWidgetItem(QString("%1").arg(age));
ageItem->setTextAlignment(Qt::AlignCenter | Qt::AlignVCenter);
ui->tableWidgetRXAll->setItem(row, col++, ageItem);
auto snrItem = new QTableWidgetItem(QString("%1 dB").arg(Varicode::formatSNR(snr)));
snrItem->setTextAlignment(Qt::AlignCenter | Qt::AlignVCenter);
ui->tableWidgetRXAll->setItem(row, col++, snrItem);
auto tdriftItem = new QTableWidgetItem(QString("%1 ms").arg((int)(1000*tdrift)));
tdriftItem->setData(Qt::UserRole, QVariant(tdrift));
ui->tableWidgetRXAll->setItem(row, col++, tdriftItem);
// align right if eliding...
int colWidth = ui->tableWidgetRXAll->columnWidth(3);
auto textItem = new QTableWidgetItem(joined);
QFontMetrics fm(textItem->font());
auto elidedText = fm.elidedText(joined, Qt::ElideLeft, colWidth);
auto flag = Qt::AlignLeft | Qt::AlignVCenter;
if (elidedText != joined) {
flag = Qt::AlignRight | Qt::AlignVCenter;
textItem->setText(joined);
}
textItem->setTextAlignment(flag);
if (
Varicode::startsWithCQ(text.last()) ||
text.last().contains(QRegularExpression {"\\b(CQCQCQ|CQ)\\b"})
){
offsetItem->setBackground(QBrush(m_config.color_CQ()));
tdriftItem->setBackground(QBrush(m_config.color_CQ()));
ageItem->setBackground(QBrush(m_config.color_CQ()));
snrItem->setBackground(QBrush(m_config.color_CQ()));
textItem->setBackground(QBrush(m_config.color_CQ()));
}
bool isDirectedAllCall = false;
// TODO: jsherer - there's a potential here for a previous allcall to poison the highlight.
if (
(isDirectedOffset(offset, &isDirectedAllCall) && !isDirectedAllCall) ||
(text.last().contains(Radio::base_callsign(m_config.my_callsign())))
) {
offsetItem->setBackground(QBrush(m_config.color_MyCall()));
tdriftItem->setBackground(QBrush(m_config.color_MyCall()));
ageItem->setBackground(QBrush(m_config.color_MyCall()));
snrItem->setBackground(QBrush(m_config.color_MyCall()));
textItem->setBackground(QBrush(m_config.color_MyCall()));
}
ui->tableWidgetRXAll->setItem(row, col++, textItem);
if (isOffsetSelected) {
for(int i = 0; i < ui->tableWidgetRXAll->columnCount(); i++){
ui->tableWidgetRXAll->item(row, i)->setSelected(true);
}
}
}
}
// Set table color
auto style = QString("QTableWidget { background:%1; selection-background-color:%2; alternate-background-color:%1; color:%3; }");
style = style.arg(m_config.color_table_background().name());
style = style.arg(m_config.color_table_highlight().name());
style = style.arg(m_config.color_table_foreground().name());
ui->tableWidgetRXAll->setStyleSheet(style);
// Set the table palette for inactive selected row
auto p = ui->tableWidgetRXAll->palette();
p.setColor(QPalette::Inactive, QPalette::Highlight, p.color(QPalette::Active, QPalette::Highlight));
ui->tableWidgetRXAll->setPalette(p);
// Set item fonts
for(int row = 0; row < ui->tableWidgetRXAll->rowCount(); row++){
for(int col = 0; col < ui->tableWidgetRXAll->columnCount(); col++){
auto item = ui->tableWidgetRXAll->item(row, col);
if(item){
item->setFont(m_config.table_font());
}
}
}
// Column labels
ui->tableWidgetRXAll->horizontalHeader()->setVisible(showColumn("band", "labels"));
// Hide columns
ui->tableWidgetRXAll->setColumnHidden(0, !showColumn("band", "offset"));
ui->tableWidgetRXAll->setColumnHidden(1, !showColumn("band", "timestamp"));
ui->tableWidgetRXAll->setColumnHidden(2, !showColumn("band", "snr"));
ui->tableWidgetRXAll->setColumnHidden(3, !showColumn("band", "tdrift", false));
// Resize the table columns
ui->tableWidgetRXAll->resizeColumnToContents(0);
ui->tableWidgetRXAll->resizeColumnToContents(1);
ui->tableWidgetRXAll->resizeColumnToContents(2);
ui->tableWidgetRXAll->resizeColumnToContents(3);
// Reset the scroll position
ui->tableWidgetRXAll->verticalScrollBar()->setValue(currentScrollPos);
}
ui->tableWidgetRXAll->setUpdatesEnabled(true);
}
// updateCallActivity
void MainWindow::displayCallActivity() {
auto now = DriftingDateTime::currentDateTimeUtc();
ui->tableWidgetCalls->setFont(m_config.table_font());
// Selected callsign
QString selectedCall = callsignSelected();
auto currentScrollPos = ui->tableWidgetCalls->verticalScrollBar()->value();
ui->tableWidgetCalls->setUpdatesEnabled(false);
{
// Clear the table
clearTableWidget(ui->tableWidgetCalls);
createAllcallTableRows(ui->tableWidgetCalls, selectedCall); // isAllCallIncluded(selectedCall)); // || isGroupCallIncluded(selectedCall));
// Build the table
QList < QString > keys = m_callActivity.keys();
auto sortBy = getSortBy("callActivity", "callsign");
bool reverse = false;
if(sortBy.startsWith("-")){
sortBy = sortBy.mid(1);
reverse = true;
}
auto compareOffset = [this](const QString left, QString right) {
auto leftActivity = m_callActivity[left];
auto rightActivity = m_callActivity[right];
return leftActivity.freq < rightActivity.freq;
};
auto compareDistance = [this, reverse](const QString left, QString right) {
auto leftActivity = m_callActivity[left];
auto rightActivity = m_callActivity[right];
int leftDistance = reverse ? -100000 : 100000;
int rightDistance = reverse ? -100000 : 100000;
if(!leftActivity.grid.isEmpty()){
calculateDistance(leftActivity.grid, &leftDistance);
}
if(!rightActivity.grid.isEmpty()){
calculateDistance(rightActivity.grid, &rightDistance);
}
return leftDistance < rightDistance;
};
auto compareTimestamp = [this](const QString left, QString right) {
auto leftActivity = m_callActivity[left];
auto rightActivity = m_callActivity[right];
return leftActivity.utcTimestamp < rightActivity.utcTimestamp;
};
auto compareAckTimestamp = [this, reverse](const QString left, QString right) {
auto leftActivity = m_callActivity[left];
auto rightActivity = m_callActivity[right];
return rightActivity.ackTimestamp < leftActivity.ackTimestamp;
};
auto compareSNR = [this, reverse](const QString left, QString right) {
auto leftActivity = m_callActivity[left];
auto rightActivity = m_callActivity[right];
if(leftActivity.snr < -60 || leftActivity.snr > 60) {
leftActivity.snr *= reverse ? 1 : -1;
}
if(rightActivity.snr < -60 || rightActivity.snr > 60) {
rightActivity.snr *= reverse ? 1 : -1;
}
return leftActivity.snr < rightActivity.snr;
};
// compare callsign
qStableSort(keys.begin(), keys.end());
if(sortBy == "offset"){
qStableSort(keys.begin(), keys.end(), compareOffset);
} else if(sortBy == "distance"){
qStableSort(keys.begin(), keys.end(), compareDistance);
} else if(sortBy == "timestamp"){
qStableSort(keys.begin(), keys.end(), compareTimestamp);
} else if(sortBy == "ackTimestamp"){
qStableSort(keys.begin(), keys.end(), compareAckTimestamp);
} else if(sortBy == "snr"){
qStableSort(keys.begin(), keys.end(), compareSNR);
}
if(reverse){
keys = listCopyReverse(keys);
}
// pin messages to the top
qStableSort(keys.begin(), keys.end(), [this](const QString left, QString right){
int leftHas = (int)!(m_rxCallsignCommandQueue.contains(left) && !m_rxCallsignCommandQueue[left].isEmpty());
int rightHas = (int)!(m_rxCallsignCommandQueue.contains(right) && !m_rxCallsignCommandQueue[right].isEmpty());
return leftHas < rightHas;
});
bool showIconColumn = false;
int callsignAging = m_config.callsign_aging();
foreach(QString call, keys) {
if(call.trimmed().isEmpty()){
continue;
}
CallDetail d = m_callActivity[call];
if(d.call.trimmed().isEmpty()){
continue;
}
bool isCallSelected = (call == selectedCall);
if (!isCallSelected && callsignAging && d.utcTimestamp.secsTo(now) / 60 >= callsignAging) {
continue;
}
ui->tableWidgetCalls->insertRow(ui->tableWidgetCalls->rowCount());
int row = ui->tableWidgetCalls->rowCount() - 1;
int col = 0;
#if SHOW_THROUGH_CALLS
QString displayCall = d.through.isEmpty() ? d.call : QString("%1>%2").arg(d.through).arg(d.call);
#else
// unicode star
QString displayCall = d.call;
#endif
QString flag;
if(m_logBook.hasWorkedBefore(d.call, "")){
// unicode checkmark
flag = "\u2713";
}
// icon column (flag -> star -> empty)
bool hasMessage = m_rxCallsignCommandQueue.contains(d.call) && !m_rxCallsignCommandQueue[d.call].isEmpty();
bool hasAck = d.ackTimestamp.isValid();
auto iconItem = new QTableWidgetItem(hasMessage ? "\u2691" : hasAck ? "\u2605" : "");
iconItem->setData(Qt::UserRole, QVariant((d.call)));
iconItem->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
ui->tableWidgetCalls->setItem(row, col++, iconItem);
if(hasMessage || hasAck){
showIconColumn = true;
}
auto displayItem = new QTableWidgetItem(displayCall);
displayItem->setData(Qt::UserRole, QVariant(d.call));
ui->tableWidgetCalls->setItem(row, col++, displayItem);
auto flagItem = new QTableWidgetItem(flag);
flagItem->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
ui->tableWidgetCalls->setItem(row, col++, flagItem);
if(d.utcTimestamp.isValid()){
ui->tableWidgetCalls->setItem(row, col++, new QTableWidgetItem(QString("%1").arg(since(d.utcTimestamp))));
ui->tableWidgetCalls->setItem(row, col++, new QTableWidgetItem(QString("%1 dB").arg(Varicode::formatSNR(d.snr))));
auto offsetItem = new QTableWidgetItem(QString("%1 Hz").arg(d.freq));
offsetItem->setData(Qt::UserRole, QVariant(d.freq));
ui->tableWidgetCalls->setItem(row, col++, offsetItem);
ui->tableWidgetCalls->setItem(row, col++, new QTableWidgetItem(QString("%1 ms").arg((int)(1000*d.tdrift))));
auto gridItem = new QTableWidgetItem(QString("%1").arg(d.grid.trimmed().left(4)));
gridItem->setToolTip(d.grid.trimmed());
ui->tableWidgetCalls->setItem(row, col++, gridItem);
auto distanceItem = new QTableWidgetItem(calculateDistance(d.grid));
distanceItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
ui->tableWidgetCalls->setItem(ui->tableWidgetCalls->rowCount() - 1, col++, distanceItem);
} else {
ui->tableWidgetCalls->setItem(row, col++, new QTableWidgetItem("")); // age
ui->tableWidgetCalls->setItem(row, col++, new QTableWidgetItem("")); // snr
ui->tableWidgetCalls->setItem(row, col++, new QTableWidgetItem("")); // freq
ui->tableWidgetCalls->setItem(row, col++, new QTableWidgetItem("")); // tdrift
ui->tableWidgetCalls->setItem(row, col++, new QTableWidgetItem("")); // grid
ui->tableWidgetCalls->setItem(row, col++, new QTableWidgetItem("")); // distance
//ui->tableWidgetCalls->setItem(row, col++, new QTableWidgetItem(""));
}
if (isCallSelected) {
for(int i = 0; i < ui->tableWidgetCalls->columnCount(); i++){
ui->tableWidgetCalls->item(row, i)->setSelected(true);
}
}
}
// Set table color
auto style = QString("QTableWidget { background:%1; selection-background-color:%2; alternate-background-color:%1; color:%3; }");
style = style.arg(m_config.color_table_background().name());
style = style.arg(m_config.color_table_highlight().name());
style = style.arg(m_config.color_table_foreground().name());
ui->tableWidgetCalls->setStyleSheet(style);
// Set the table palette for inactive selected row
auto p = ui->tableWidgetCalls->palette();
p.setColor(QPalette::Inactive, QPalette::Highlight, p.color(QPalette::Active, QPalette::Highlight));
ui->tableWidgetCalls->setPalette(p);
// Set item fonts
for(int row = 0; row < ui->tableWidgetCalls->rowCount(); row++){
auto bold = ui->tableWidgetCalls->item(row, 0)->text() == "\u2691";
for(int col = 0; col < ui->tableWidgetCalls->columnCount(); col++){
auto item = ui->tableWidgetCalls->item(row, col);
if(item){
auto f = m_config.table_font();
if(bold){
f.setBold(true);
}
item->setFont(f);
}
}
}
// Column labels
ui->tableWidgetCalls->horizontalHeader()->setVisible(showColumn("call", "labels"));
// Hide columns
ui->tableWidgetCalls->setColumnHidden(0, !showIconColumn);
ui->tableWidgetCalls->setColumnHidden(1, !showColumn("call", "callsign"));
ui->tableWidgetCalls->setColumnHidden(2, !showColumn("call", "flag"));
ui->tableWidgetCalls->setColumnHidden(3, !showColumn("call", "timestamp"));
ui->tableWidgetCalls->setColumnHidden(4, !showColumn("call", "snr"));
ui->tableWidgetCalls->setColumnHidden(5, !showColumn("call", "offset"));
ui->tableWidgetCalls->setColumnHidden(6, !showColumn("call", "tdrift", false));
ui->tableWidgetCalls->setColumnHidden(7, !showColumn("call", "grid", false));
ui->tableWidgetCalls->setColumnHidden(8, !showColumn("call", "distance", false));
// Resize the table columns
ui->tableWidgetCalls->resizeColumnToContents(0);
ui->tableWidgetCalls->resizeColumnToContents(1);
ui->tableWidgetCalls->resizeColumnToContents(2);
ui->tableWidgetCalls->resizeColumnToContents(3);
ui->tableWidgetCalls->resizeColumnToContents(4);
ui->tableWidgetCalls->resizeColumnToContents(5);
ui->tableWidgetCalls->resizeColumnToContents(6);
ui->tableWidgetCalls->resizeColumnToContents(7);
// Reset the scroll position
ui->tableWidgetCalls->verticalScrollBar()->setValue(currentScrollPos);
}
ui->tableWidgetCalls->setUpdatesEnabled(true);
}
void MainWindow::postWSPRDecode (bool is_new, QStringList parts)
{
#if 0
if (parts.size () < 8)
{
parts.insert (6, "");
}
m_messageClient->WSPR_decode (is_new, QTime::fromString (parts[0], "hhmm"), parts[1].toInt ()
, parts[2].toFloat (), Radio::frequency (parts[3].toFloat (), 6)
, parts[4].toInt (), parts[5], parts[6], parts[7].toInt ()
, m_diskData);
#endif
}
void MainWindow::networkMessage(Message const &message)
{
if(!m_config.accept_udp_requests()){
return;
}
auto type = message.type();
if(type == "PING"){
return;
}
// Inspired by FLDigi
// TODO: MAIN.RX - Turn on RX
// TODO: MAIN.TX - Transmit
// TODO: MAIN.TUNE - Tune
// TODO: MAIN.HALT - Halt
// RIG.GET_FREQ - Get the current Frequency
// RIG.SET_FREQ - Set the current Frequency
if(type == "RIG.GET_FREQ"){
sendNetworkMessage("RIG.FREQ", "", {
{"DIAL", QVariant((quint64)dialFrequency())},
{"OFFSET", QVariant((quint64)currentFreqOffset())}
});
return;
}
if(type == "RIG.SET_FREQ"){
auto params = message.params();
if(params.contains("DIAL")){
bool ok = false;
auto f = params["DIAL"].toInt(&ok);
if(ok){
setRig(f);
displayDialFrequency();
}
}
if(params.contains("OFFSET")){
bool ok = false;
auto f = params["OFFSET"].toInt(&ok);
if(ok){
setFreqOffsetForRestore(f, false);
}
}
}
// STATION.GET_CALLSIGN - Get the current callsign
// STATION.GET_GRID - Get the current grid locator
// STATION.SET_GRID - Set the current grid locator
// STATION.GET_QTC - Get the current station message
// STATION.SET_QTC - Set the current station message
if(type == "STATION.GET_CALLSIGN"){
sendNetworkMessage("STATION.CALLSIGN", m_config.my_callsign());
return;
}
if(type == "STATION.GET_GRID"){
sendNetworkMessage("STATION.GRID", m_config.my_grid());
return;
}
if(type == "STATION.SET_GRID"){
m_config.set_dynamic_location(message.value());
sendNetworkMessage("STATION.GRID", m_config.my_grid());
return;
}
if(type == "STATION.GET_QTC"){
sendNetworkMessage("STATION.QTC", m_config.my_station());
return;
}
if(type == "STATION.SET_QTC"){
m_config.set_dynamic_station_message(message.value());
sendNetworkMessage("STATION.QTC", m_config.my_station());
return;
}
// RX.GET_CALL_ACTIVITY
// RX.GET_CALL_SELECTED
// RX.GET_BAND_ACTIVITY
// RX.GET_TEXT
if(type == "RX.GET_CALL_ACTIVITY"){
auto now = DriftingDateTime::currentDateTimeUtc();
int callsignAging = m_config.callsign_aging();
QMap<QString, QVariant> calls;
foreach(auto cd, m_callActivity.values()){
if (callsignAging && cd.utcTimestamp.secsTo(now) / 60 >= callsignAging) {
continue;
}
QMap<QString, QVariant> detail;
detail["SNR"] = QVariant(cd.snr);
detail["GRID"] = QVariant(cd.grid);
detail["UTC"] = QVariant(cd.utcTimestamp.toMSecsSinceEpoch());
calls[cd.call] = QVariant(detail);
}
sendNetworkMessage("RX.CALL_ACTIVITY", "", calls);
return;
}
if(type == "RX.GET_CALL_SELECTED"){
sendNetworkMessage("RX.CALL_SELECTED", callsignSelected());
return;
}
if(type == "RX.GET_BAND_ACTIVITY"){
QMap<QString, QVariant> offsets;
foreach(auto offset, m_bandActivity.keys()){
auto activity = m_bandActivity[offset];
if(activity.isEmpty()){
continue;
}
auto d = activity.last();
QMap<QString, QVariant> detail;
detail["FREQ"] = QVariant(d.freq);
detail["TEXT"] = QVariant(d.text);
detail["SNR"] = QVariant(d.snr);
detail["UTC"] = QVariant(d.utcTimestamp.toMSecsSinceEpoch());
offsets[QString("%1").arg(offset)] = QVariant(detail);
}
sendNetworkMessage("RX.BAND_ACTIVITY", "", offsets);
return;
}
if(type == "RX.GET_TEXT"){
sendNetworkMessage("RX.TEXT", ui->textEditRX->toPlainText());
return;
}
// TX.GET_TEXT
// TX.SET_TEXT
// TX.SEND_MESSAGE
if(type == "TX.GET_TEXT"){
sendNetworkMessage("TX.TEXT", ui->extFreeTextMsgEdit->toPlainText());
return;
}
if(type == "TX.SET_TEXT"){
addMessageText(message.value(), true);
return;
}
if(type == "TX.SEND_MESSAGE"){
auto text = message.value();
if(!text.isEmpty()){
enqueueMessage(PriorityNormal, text, -1, nullptr);
return;
}
}
// WINDOW.RAISE
if(type == "WINDOW.RAISE"){
setWindowState(Qt::WindowActive);
activateWindow();
return;
}
qDebug() << "Unable to process networkMessage:" << type;
}
void MainWindow::sendNetworkMessage(QString const &type, QString const &message){
m_messageClient->send(Message(type, message));
}
void MainWindow::sendNetworkMessage(QString const &type, QString const &message, QMap<QString, QVariant> const &params)
{
m_messageClient->send(Message(type, message, params));
}
void MainWindow::networkError (QString const& e)
{
if(!m_config.accept_udp_requests()){
return;
}
if (m_splash && m_splash->isVisible ()) m_splash->hide ();
if (MessageBox::Retry == MessageBox::warning_message (this, tr ("Network Error")
, tr ("Error: %1\nUDP server %2:%3")
.arg (e)
.arg (m_config.udp_server_name ())
.arg (m_config.udp_server_port ())
, QString {}
, MessageBox::Cancel | MessageBox::Retry
, MessageBox::Cancel))
{
// retry server lookup
m_messageClient->set_server (m_config.udp_server_name ());
}
}
void MainWindow::on_syncSpinBox_valueChanged(int n)
{
m_minSync=n;
}
void MainWindow::p1ReadFromStdout() //p1readFromStdout
{
QString t1;
while(p1.canReadLine()) {
QString t(p1.readLine());
if(t.indexOf("<DecodeFinished>") >= 0) {
m_bDecoded = m_nWSPRdecodes > 0;
if(!m_diskData) {
WSPR_history(m_dialFreqRxWSPR, m_nWSPRdecodes);
if(m_nWSPRdecodes==0 and ui->band_hopping_group_box->isChecked()) {
t = " Receiving " + m_mode + " ----------------------- " +
m_config.bands ()->find (m_dialFreqRxWSPR);
t=WSPR_hhmm(-60) + ' ' + t.rightJustified (66, '-');
ui->decodedTextBrowser->appendText(t);
}
killFileTimer.start (45*1000); //Kill in 45s (for slow modes)
}
m_nWSPRdecodes=0;
ui->DecodeButton->setChecked (false);
if(m_uploadSpots
&& m_config.is_transceiver_online ()) { // need working rig control
float x=qrand()/((double)RAND_MAX + 1.0);
int msdelay=20000*x;
uploadTimer.start(msdelay); //Upload delay
} else {
QFile f(QDir::toNativeSeparators(m_config.writeable_data_dir ().absolutePath()) + "/wspr_spots.txt");
if(f.exists()) f.remove();
}
m_RxLog=0;
m_startAnother=m_loopall;
m_blankLine=true;
m_decoderBusy = false;
statusUpdate ();
} else {
int n=t.length();
t=t.mid(0,n-2) + " ";
t.remove(QRegExp("\\s+$"));
QStringList rxFields = t.split(QRegExp("\\s+"));
QString rxLine;
QString grid="";
if ( rxFields.count() == 8 ) {
rxLine = QString("%1 %2 %3 %4 %5 %6 %7 %8")
.arg(rxFields.at(0), 4)
.arg(rxFields.at(1), 4)
.arg(rxFields.at(2), 5)
.arg(rxFields.at(3), 11)
.arg(rxFields.at(4), 4)
.arg(rxFields.at(5).leftJustified (12))
.arg(rxFields.at(6), -6)
.arg(rxFields.at(7), 3);
postWSPRDecode (true, rxFields);
grid = rxFields.at(6);
} else if ( rxFields.count() == 7 ) { // Type 2 message
rxLine = QString("%1 %2 %3 %4 %5 %6 %7 %8")
.arg(rxFields.at(0), 4)
.arg(rxFields.at(1), 4)
.arg(rxFields.at(2), 5)
.arg(rxFields.at(3), 11)
.arg(rxFields.at(4), 4)
.arg(rxFields.at(5).leftJustified (12))
.arg("", -6)
.arg(rxFields.at(6), 3);
postWSPRDecode (true, rxFields);
} else {
rxLine = t;
}
if(grid!="") {
double utch=0.0;
int nAz,nEl,nDmiles,nDkm,nHotAz,nHotABetter;
azdist_(const_cast <char *> (m_config.my_grid ().toLatin1().constData()),
const_cast <char *> (grid.toLatin1().constData()),&utch,
&nAz,&nEl,&nDmiles,&nDkm,&nHotAz,&nHotABetter,6,6);
QString t1;
if(m_config.miles()) {
t1.sprintf("%7d",nDmiles);
} else {
t1.sprintf("%7d",nDkm);
}
rxLine += t1;
}
if (m_config.insert_blank () && m_blankLine) {
QString band;
Frequency f=1000000.0*rxFields.at(3).toDouble()+0.5;
band = ' ' + m_config.bands ()->find (f);
ui->decodedTextBrowser->appendText(band.rightJustified (71, '-'));
m_blankLine = false;
}
m_nWSPRdecodes += 1;
ui->decodedTextBrowser->appendText(rxLine);
}
}
}
QString MainWindow::WSPR_hhmm(int n)
{
QDateTime t=DriftingDateTime::currentDateTimeUtc().addSecs(n);
int m=t.toString("hhmm").toInt()/2;
QString t1;
t1.sprintf("%04d",2*m);
return t1;
}
void MainWindow::WSPR_history(Frequency dialFreq, int ndecodes)
{
QDateTime t=DriftingDateTime::currentDateTimeUtc().addSecs(-60);
QString t1=t.toString("yyMMdd");
QString t2=WSPR_hhmm(-60);
QString t3;
t3.sprintf("%13.6f",0.000001*dialFreq);
if(ndecodes<0) {
t1=t1 + " " + t2 + t3 + " T";
} else {
QString t4;
t4.sprintf("%4d",ndecodes);
t1=t1 + " " + t2 + t3 + " R" + t4;
}
QFile f {m_config.writeable_data_dir ().absoluteFilePath ("WSPR_history.txt")};
if (f.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append)) {
QTextStream out(&f);
out << t1 << endl;
f.close();
} else {
MessageBox::warning_message (this, tr ("File Error")
, tr ("Cannot open \"%1\" for append: %2")
.arg (f.fileName ()).arg (f.errorString ()));
}
}
void MainWindow::uploadSpots()
{
// do not spot replays or if rig control not working
if(m_diskData || !m_config.is_transceiver_online ()) return;
if(m_uploading) {
qDebug() << "Previous upload has not completed, spots were lost";
wsprNet->abortOutstandingRequests ();
m_uploading = false;
}
QString rfreq = QString("%1").arg(0.000001*(m_dialFreqRxWSPR + 1500), 0, 'f', 6);
QString tfreq = QString("%1").arg(0.000001*(m_dialFreqRxWSPR +
ui->TxFreqSpinBox->value()), 0, 'f', 6);
wsprNet->upload(m_config.my_callsign(), m_config.my_grid(), rfreq, tfreq,
m_mode, QString::number(ui->autoButton->isChecked() ? m_pctx : 0),
QString::number(m_dBm), version(),
QDir::toNativeSeparators(m_config.writeable_data_dir ().absolutePath()) + "/wspr_spots.txt");
m_uploading = true;
}
void MainWindow::uploadResponse(QString response)
{
if (response == "done") {
m_uploading=false;
} else {
if (response.startsWith ("Upload Failed")) {
m_uploading=false;
}
qDebug () << "WSPRnet.org status:" << response;
}
}
void MainWindow::on_TxPowerComboBox_currentIndexChanged(const QString &arg1)
{
int i1=arg1.indexOf(" ");
m_dBm=arg1.mid(0,i1).toInt();
}
void MainWindow::on_sbTxPercent_valueChanged(int n)
{
m_pctx=n;
if(m_pctx>0) {
ui->pbTxNext->setEnabled(true);
} else {
m_txNext=false;
ui->pbTxNext->setChecked(false);
ui->pbTxNext->setEnabled(false);
}
}
void MainWindow::on_cbUploadWSPR_Spots_toggled(bool b)
{
m_uploadSpots=b;
if(m_uploadSpots) ui->cbUploadWSPR_Spots->setStyleSheet("");
if(!m_uploadSpots) ui->cbUploadWSPR_Spots->setStyleSheet(
"QCheckBox{background-color: yellow}");
}
void MainWindow::on_WSPRfreqSpinBox_valueChanged(int n)
{
ui->TxFreqSpinBox->setValue(n);
}
void MainWindow::on_pbTxNext_clicked(bool b)
{
m_txNext=b;
}
void MainWindow::WSPR_scheduling ()
{
m_WSPR_tx_next = false;
if (m_config.is_transceiver_online () // need working rig control for hopping
&& !m_config.is_dummy_rig ()
&& ui->band_hopping_group_box->isChecked ()) {
auto hop_data = m_WSPR_band_hopping.next_hop (m_auto);
qDebug () << "hop data: period:" << hop_data.period_name_
<< "frequencies index:" << hop_data.frequencies_index_
<< "tune:" << hop_data.tune_required_
<< "tx:" << hop_data.tx_next_;
m_WSPR_tx_next = hop_data.tx_next_;
if (hop_data.frequencies_index_ >= 0) { // new band
ui->bandComboBox->setCurrentIndex (hop_data.frequencies_index_);
on_bandComboBox_activated (hop_data.frequencies_index_);
m_cmnd.clear ();
QStringList prefixes {".bat", ".cmd", ".exe", ""};
for (auto const& prefix : prefixes)
{
auto const& path = m_appDir + "/user_hardware" + prefix;
QFile f {path};
if (f.exists ()) {
m_cmnd = QDir::toNativeSeparators (f.fileName ()) + ' ' +
m_config.bands ()->find (m_freqNominal).remove ('m');
}
}
if(m_cmnd!="") p3.start(m_cmnd); // Execute user's hardware controller
// Produce a short tuneup signal
m_tuneup = false;
if (hop_data.tune_required_) {
m_tuneup = true;
on_tuneButton_clicked (true);
tuneATU_Timer.start (2500);
}
}
// Display grayline status
band_hopping_label.setText (hop_data.period_name_);
}
else {
m_WSPR_tx_next = m_WSPR_band_hopping.next_is_tx ("WSPR-LF" == m_mode);
}
}
void MainWindow::astroUpdate ()
{
if (m_astroWidget)
{
// no Doppler correction while CTRL pressed allows manual tuning
if (Qt::ControlModifier & QApplication::queryKeyboardModifiers ()) return;
auto correction = m_astroWidget->astroUpdate(DriftingDateTime::currentDateTimeUtc (),
m_config.my_grid(), m_hisGrid,
m_freqNominal,
"Echo" == m_mode, m_transmitting,
!m_config.tx_qsy_allowed (), m_TRperiod);
// no Doppler correction in Tx if rig can't do it
if (m_transmitting && !m_config.tx_qsy_allowed ()) return;
if (!m_astroWidget->doppler_tracking ()) return;
if ((m_monitoring || m_transmitting)
// no Doppler correction below 6m
&& m_freqNominal >= 50000000
&& m_config.split_mode ())
{
// adjust for rig resolution
if (m_config.transceiver_resolution () > 2)
{
correction.rx = (correction.rx + 50) / 100 * 100;
correction.tx = (correction.tx + 50) / 100 * 100;
}
else if (m_config.transceiver_resolution () > 1)
{
correction.rx = (correction.rx + 10) / 20 * 20;
correction.tx = (correction.tx + 10) / 20 * 20;
}
else if (m_config.transceiver_resolution () > 0)
{
correction.rx = (correction.rx + 5) / 10 * 10;
correction.tx = (correction.tx + 5) / 10 * 10;
}
else if (m_config.transceiver_resolution () < -2)
{
correction.rx = correction.rx / 100 * 100;
correction.tx = correction.tx / 100 * 100;
}
else if (m_config.transceiver_resolution () < -1)
{
correction.rx = correction.rx / 20 * 20;
correction.tx = correction.tx / 20 * 20;
}
else if (m_config.transceiver_resolution () < 0)
{
correction.rx = correction.rx / 10 * 10;
correction.tx = correction.tx / 10 * 10;
}
m_astroCorrection = correction;
}
else
{
m_astroCorrection = {};
}
setRig ();
}
}
void MainWindow::setRig (Frequency f)
{
if (f)
{
m_freqNominal = f;
m_freqTxNominal = m_freqNominal;
if (m_astroWidget) m_astroWidget->nominal_frequency (m_freqNominal, m_freqTxNominal);
}
if (m_mode == "FreqCal"
&& m_frequency_list_fcal_iter != m_config.frequencies ()->end ()) {
m_freqNominal = m_frequency_list_fcal_iter->frequency_ - ui->RxFreqSpinBox->value ();
}
if(m_transmitting && !m_config.tx_qsy_allowed ()) return;
if ((m_monitoring || m_transmitting) && m_config.transceiver_online ())
{
if (m_transmitting && m_config.split_mode ())
{
Q_EMIT m_config.transceiver_tx_frequency (m_freqTxNominal + m_astroCorrection.tx);
}
else
{
Q_EMIT m_config.transceiver_frequency (m_freqNominal + m_astroCorrection.rx);
}
}
}
void MainWindow::fastPick(int x0, int x1, int y)
{
float pixPerSecond=12000.0/512.0;
if(m_TRperiod<30) pixPerSecond=12000.0/256.0;
if(m_mode!="ISCAT" and m_mode!="MSK144") return;
if(!m_decoderBusy) {
dec_data.params.newdat=0;
dec_data.params.nagain=1;
m_blankLine=false; // don't insert the separator again
m_nPick=1;
if(y > 120) m_nPick=2;
m_t0Pick=x0/pixPerSecond;
m_t1Pick=x1/pixPerSecond;
m_dataAvailable=true;
decode();
}
}
void MainWindow::on_actionMeasure_reference_spectrum_triggered()
{
if(!m_monitoring) on_monitorButton_clicked (true);
m_bRefSpec=true;
}
void MainWindow::on_actionMeasure_phase_response_triggered()
{
if(m_bTrain) {
m_bTrain=false;
MessageBox::information_message (this, tr ("Phase Training Disabled"));
} else {
m_bTrain=true;
MessageBox::information_message (this, tr ("Phase Training Enabled"));
}
}
void MainWindow::on_actionErase_reference_spectrum_triggered()
{
m_bClearRefSpec=true;
}
void MainWindow::freqCalStep()
{
if (m_frequency_list_fcal_iter == m_config.frequencies ()->end ()
|| ++m_frequency_list_fcal_iter == m_config.frequencies ()->end ()) {
m_frequency_list_fcal_iter = m_config.frequencies ()->begin ();
}
// allow for empty list
if (m_frequency_list_fcal_iter != m_config.frequencies ()->end ()) {
setRig (m_frequency_list_fcal_iter->frequency_ - ui->RxFreqSpinBox->value ());
}
}
void MainWindow::on_sbCQTxFreq_valueChanged(int)
{
setXIT (ui->TxFreqSpinBox->value ());
}
void MainWindow::on_cbCQTx_toggled(bool b)
{
ui->sbCQTxFreq->setEnabled(b);
if(b) {
ui->txrb6->setChecked(true);
m_ntx=6;
m_QSOProgress = CALLING;
}
setRig ();
setXIT (ui->TxFreqSpinBox->value ());
}
void MainWindow::statusUpdate () const
{
#if 0
if (!ui) return;
auto submode = current_submode ();
m_messageClient->status_update (m_freqNominal, m_mode, m_hisCall,
QString::number (ui->rptSpinBox->value ()),
m_modeTx, ui->autoButton->isChecked (),
m_transmitting, m_decoderBusy,
ui->RxFreqSpinBox->value (), ui->TxFreqSpinBox->value (),
m_config.my_callsign (), m_config.my_grid (),
m_hisGrid, m_tx_watchdog,
submode != QChar::Null ? QString {submode} : QString {}, m_bFastMode);
#endif
}
void MainWindow::childEvent (QChildEvent * e)
{
if (e->child ()->isWidgetType ())
{
switch (e->type ())
{
case QEvent::ChildAdded: add_child_to_event_filter (e->child ()); break;
case QEvent::ChildRemoved: remove_child_from_event_filter (e->child ()); break;
default: break;
}
}
QMainWindow::childEvent (e);
}
// add widget and any child widgets to our event filter so that we can
// take action on key press ad mouse press events anywhere in the main window
void MainWindow::add_child_to_event_filter (QObject * target)
{
if (target && target->isWidgetType ())
{
target->installEventFilter (this);
}
auto const& children = target->children ();
for (auto iter = children.begin (); iter != children.end (); ++iter)
{
add_child_to_event_filter (*iter);
}
}
// recursively remove widget and any child widgets from our event filter
void MainWindow::remove_child_from_event_filter (QObject * target)
{
auto const& children = target->children ();
for (auto iter = children.begin (); iter != children.end (); ++iter)
{
remove_child_from_event_filter (*iter);
}
if (target && target->isWidgetType ())
{
target->removeEventFilter (this);
}
}
void MainWindow::resetIdleTimer(){
if(m_idleMinutes){
m_idleMinutes = 0;
qDebug() << "idle" << m_idleMinutes << "minutes";
}
}
void MainWindow::incrementIdleTimer(){
m_idleMinutes++;
qDebug() << "increment idle to" << m_idleMinutes << "minutes";
}
void MainWindow::tx_watchdog (bool triggered)
{
auto prior = m_tx_watchdog;
m_tx_watchdog = triggered;
if (triggered)
{
m_bTxTime=false;
if (m_tune) stop_tuning ();
if (m_auto) auto_tx_mode (false);
stopTx();
tx_status_label.setStyleSheet ("QLabel{background-color: #000000; color:#ffffff; }");
tx_status_label.setText ("Inactive watchdog");
// if the watchdog is triggered...we're no longer active
bool wasAuto = ui->autoReplyButton->isChecked();
bool wasHB = ui->hbMacroButton->isChecked();
bool wasCQ = ui->cqMacroButton->isChecked();
// save the button states
ui->autoReplyButton->setChecked(false);
ui->hbMacroButton->setChecked(false);
ui->cqMacroButton->setChecked(false);
MessageBox::warning_message(this, QString("You have been inactive for more than %1 minutes.").arg(m_config.watchdog()));
// restore the button states
ui->autoReplyButton->setChecked(wasAuto);
ui->hbMacroButton->setChecked(wasHB);
ui->cqMacroButton->setChecked(wasCQ);
}
else
{
// m_idleMinutes = 0;
update_watchdog_label ();
}
if (prior != triggered) statusUpdate ();
}
void MainWindow::update_watchdog_label ()
{
watchdog_label.setVisible (false);
#if 0
if (m_config.watchdog () && !m_mode.startsWith ("WSPR"))
{
watchdog_label.setText (QString {"WD:%1m"}.arg (m_config.watchdog () - m_idleMinutes));
watchdog_label.setVisible (true);
}
else
{
watchdog_label.setText (QString {});
watchdog_label.setVisible (false);
}
#endif
}
void MainWindow::on_cbMenus_toggled(bool b)
{
hideMenus(!b);
}
void MainWindow::on_cbCQonly_toggled(bool)
{
QFile {m_config.temp_dir().absoluteFilePath(".lock")}.remove(); // Allow jt9 to start
decodeBusy(true);
}
void MainWindow::on_cbFirst_toggled(bool b)
{
if (b) {
if (m_auto && CALLING == m_QSOProgress) {
ui->cbFirst->setStyleSheet ("QCheckBox{color:red}");
}
} else {
ui->cbFirst->setStyleSheet ("");
}
}
void MainWindow::on_cbAutoSeq_toggled(bool b)
{
if(!b) ui->cbFirst->setChecked(false);
ui->cbFirst->setVisible((m_mode=="FT8") and b);
}
void MainWindow::on_measure_check_box_stateChanged (int state)
{
m_config.enable_calibration (Qt::Checked != state);
}
void MainWindow::write_transmit_entry (QString const& file_name)
{
QFile f {m_config.writeable_data_dir ().absoluteFilePath (file_name)};
if (f.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append))
{
QTextStream out(&f);
auto time = DriftingDateTime::currentDateTimeUtc ();
time = time.addSecs (-(time.time ().second () % m_TRperiod));
auto dt = DecodedText(m_currentMessage, m_currentMessageBits);
out << time.toString("yyyy-MM-dd hh:mm:ss")
<< " Transmitting " << qSetRealNumberPrecision (12) << (m_freqNominal / 1.e6)
<< " MHz " << "JS8"
<< ": " << dt.message() << endl;
f.close();
}
else
{
auto const& message = tr ("Cannot open \"%1\" for append: %2")
.arg (f.fileName ()).arg (f.errorString ());
#if QT_VERSION >= 0x050400
QTimer::singleShot (0, [=] { // don't block guiUpdate
MessageBox::warning_message (this, tr ("Log File Error"), message);
});
#else
MessageBox::warning_message (this, tr ("Log File Error"), message);
#endif
}
}