Move decoder process handling to separate thread to avoid exhaustuion
This commit is contained in:
parent
2a320ddcdd
commit
1a03619a2f
@ -10,6 +10,7 @@
|
||||
#define JS8_USE_IHSYM 0 // compute ihsym manually instead of from symspec
|
||||
#define JS8_RING_BUFFER 1 // use a ring buffer instead of clearing the decode frames
|
||||
#define JS8_SINGLE_DECODE 0 // single submode decode per instantiation of the decoder
|
||||
#define JS8_DECODE_THREAD 1 // use a separate thread for decode process handling
|
||||
|
||||
#ifdef QT_DEBUG
|
||||
#define JS8_DEBUG_DECODE 1 // emit debug statements for the decode pipeline
|
||||
|
@ -170,7 +170,7 @@ subroutine multimode_decoder(ss,id2,params,nfsample)
|
||||
call flush(6)
|
||||
|
||||
ndecoded = my_js8a%decoded + my_js8b%decoded + my_js8c%decoded + my_js8e%decoded
|
||||
call sleep_msec(3000)
|
||||
!call sleep_msec(3000)
|
||||
write(*,1010) ndecoded
|
||||
1010 format('<DecodeFinished>',i4)
|
||||
call flush(6)
|
||||
|
717
mainwindow.cpp
717
mainwindow.cpp
@ -536,6 +536,9 @@ MainWindow::MainWindow(QDir const& temp_directory, bool multiple,
|
||||
, &m_WSPR_band_hopping, &WSPRBandHopping::set_tx_percent);
|
||||
#endif
|
||||
|
||||
// decoder queue handler
|
||||
connect(this, &MainWindow::decodedLineReady, this, &MainWindow::processDecodedLine);
|
||||
|
||||
on_EraseButton_clicked ();
|
||||
|
||||
QActionGroup* modeGroup = new QActionGroup(this);
|
||||
@ -1603,11 +1606,21 @@ void MainWindow::initDecoderSubprocess(){
|
||||
|
||||
if(JS8_DEBUG_DECODE) qDebug() << "decoder subprocess starting...";
|
||||
|
||||
auto proc = new QProcess(this);
|
||||
#if JS8_DECODE_THREAD
|
||||
auto thread = new QThread(nullptr);
|
||||
#endif
|
||||
|
||||
auto proc = new QProcess(nullptr);
|
||||
|
||||
connect(proc, &QProcess::readyReadStandardOutput, this,
|
||||
[this, proc](){
|
||||
#if JS8_DECODE_THREAD
|
||||
while(proc->canReadLine()){
|
||||
emit decodedLineReady(proc->readLine());
|
||||
}
|
||||
#else
|
||||
readFromStdout(proc);
|
||||
#endif
|
||||
});
|
||||
|
||||
connect(proc, static_cast<void (QProcess::*) (QProcess::ProcessError)> (&QProcess::error),
|
||||
@ -1617,8 +1630,10 @@ void MainWindow::initDecoderSubprocess(){
|
||||
|
||||
connect(proc, static_cast<void (QProcess::*) (int, QProcess::ExitStatus)> (&QProcess::finished),
|
||||
[this, proc] (int exitCode, QProcess::ExitStatus status) {
|
||||
#if JS8_DECODE_THREAD
|
||||
proc->deleteLater();
|
||||
proc->thread()->quit();
|
||||
#endif
|
||||
subProcessFailed (proc, exitCode, status);
|
||||
});
|
||||
|
||||
@ -1626,6 +1641,15 @@ void MainWindow::initDecoderSubprocess(){
|
||||
proc->start(QDir::toNativeSeparators (m_appDir) + QDir::separator () +
|
||||
"js8", js8_args, QIODevice::ReadWrite | QIODevice::Unbuffered);
|
||||
|
||||
#if JS8_DECODE_THREAD
|
||||
if(JS8_DEBUG_DECODE) qDebug() << "decoder subprocess moving to new thread...";
|
||||
// move process handling into its own thread
|
||||
proc->moveToThread(thread);
|
||||
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
|
||||
thread->moveToThread(qApp->thread());
|
||||
thread->start(QThread::HighPriority);
|
||||
#endif
|
||||
|
||||
// create a process watcher looking for stdout read...
|
||||
// seems like we're starving the event loop or something?
|
||||
// auto watcher = new QTimer(proc);
|
||||
@ -4681,355 +4705,358 @@ void MainWindow::readFromStdout(QProcess * proc) //r
|
||||
}
|
||||
|
||||
while(proc->canReadLine()) {
|
||||
QByteArray t = proc->readLine();
|
||||
qDebug() << "JS8: " << QString(t);
|
||||
|
||||
bool bAvgMsg=false;
|
||||
int navg=0;
|
||||
if(t.indexOf("<DecodeDebug>") >= 0) {
|
||||
if(JS8_DEBUG_DECODE) qDebug() << "--> busy?" << m_decoderBusy << "lock exists?" << ( QFile{m_config.temp_dir ().absoluteFilePath (".lock")}.exists());
|
||||
return;
|
||||
}
|
||||
|
||||
if(t.indexOf("<DecodeStarted>") >= 0) {
|
||||
if(JS8_DEBUG_DECODE) qDebug() << "--> busy?" << m_decoderBusy << "lock exists?" << ( QFile{m_config.temp_dir ().absoluteFilePath (".lock")}.exists());
|
||||
return;
|
||||
}
|
||||
|
||||
if(t.indexOf("<DecodeFinished>") >= 0) {
|
||||
if(m_mode=="QRA64") m_wideGraph->drawRed(0,0);
|
||||
m_bDecoded = t.mid(16).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;
|
||||
}
|
||||
|
||||
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() >= rxSnrThreshold(decodedtext.submode());
|
||||
|
||||
// dupe check
|
||||
auto frame = decodedtext.message();
|
||||
auto frameOffset = decodedtext.frequencyOffset();
|
||||
if(m_messageDupeCache.contains(frame)){
|
||||
// check to see if the frequency is near our previous frame
|
||||
auto cachedFreq = m_messageDupeCache.value(frame, 0);
|
||||
if(qAbs(cachedFreq - frameOffset) <= rxThreshold(decodedtext.submode())){
|
||||
qDebug() << "duplicate frame from" << cachedFreq << "and" << frameOffset;
|
||||
bValidFrame = false;
|
||||
}
|
||||
} else {
|
||||
// cache for this decode cycle
|
||||
m_messageDupeCache[frame] = frameOffset;
|
||||
}
|
||||
|
||||
qDebug() << "valid" << bValidFrame << submodeName(decodedtext.submode()) << "decoded text" << decodedtext.message();
|
||||
|
||||
// skip if invalid
|
||||
if(!bValidFrame) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ActivityDetail d = {};
|
||||
CallDetail cd = {};
|
||||
CommandDetail cmd = {};
|
||||
CallDetail td = {};
|
||||
|
||||
// Parse General Activity
|
||||
#if 1
|
||||
bool shouldParseGeneralActivity = true;
|
||||
if(shouldParseGeneralActivity && !decodedtext.messageWords().isEmpty()){
|
||||
int offset = decodedtext.frequencyOffset();
|
||||
|
||||
if(!m_bandActivity.contains(offset)){
|
||||
int range = rxThreshold(decodedtext.submode());
|
||||
|
||||
QList<int> offsets = generateOffsets(offset-range, offset+range);
|
||||
|
||||
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();
|
||||
d.submode = decodedtext.submode();
|
||||
|
||||
// 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(decodedtext.submode(), 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(decodedtext.submode(), 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();
|
||||
cd.submode = decodedtext.submode();
|
||||
|
||||
// Only respond to HEARTBEATS...remember that CQ messages are "Alt" pings
|
||||
if(decodedtext.isHeartbeat()){
|
||||
if(decodedtext.isAlt()){
|
||||
// this is a cq with a standard or compound call, ala "KN4CRD/P: CQCQCQ"
|
||||
cd.cqTimestamp = DriftingDateTime::currentDateTimeUtc();
|
||||
|
||||
// it is not processed elsewhere, so we need to just log it here.
|
||||
logCallActivity(cd, true);
|
||||
|
||||
// notification for cq
|
||||
tryNotify("cq");
|
||||
|
||||
} 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;
|
||||
cmd.submode = cd.submode;
|
||||
m_rxCommandQueue.append(cmd);
|
||||
|
||||
// notification for hb
|
||||
tryNotify("hb");
|
||||
}
|
||||
|
||||
} else {
|
||||
qDebug() << "buffering compound call" << cd.freq << cd.call << cd.bits;
|
||||
|
||||
hasExistingMessageBuffer(cd.submode, 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();
|
||||
cmd.submode = decodedtext.submode();
|
||||
|
||||
// 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;
|
||||
cmdcd.submode = cmd.submode;
|
||||
logCallActivity(cmdcd, false);
|
||||
logHeardGraph(cmd.from, cmd.to);
|
||||
}
|
||||
|
||||
// merge any existing buffer to this frequency
|
||||
hasExistingMessageBuffer(cmd.submode, 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;
|
||||
td.submode = cmd.submode;
|
||||
logCallActivity(td, true);
|
||||
logHeardGraph(cmd.from, cmd.to);
|
||||
}
|
||||
}
|
||||
#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
|
||||
processDecodedLine(proc->readLine());
|
||||
}
|
||||
|
||||
// See MainWindow::postDecode for displaying the latest decodes
|
||||
}
|
||||
|
||||
void MainWindow::processDecodedLine(QByteArray t){
|
||||
qDebug() << "JS8: " << QString(t);
|
||||
|
||||
bool bAvgMsg=false;
|
||||
int navg=0;
|
||||
if(t.indexOf("<DecodeDebug>") >= 0) {
|
||||
if(JS8_DEBUG_DECODE) qDebug() << "--> busy?" << m_decoderBusy << "lock exists?" << ( QFile{m_config.temp_dir ().absoluteFilePath (".lock")}.exists());
|
||||
return;
|
||||
}
|
||||
|
||||
if(t.indexOf("<DecodeStarted>") >= 0) {
|
||||
if(JS8_DEBUG_DECODE) qDebug() << "--> busy?" << m_decoderBusy << "lock exists?" << ( QFile{m_config.temp_dir ().absoluteFilePath (".lock")}.exists());
|
||||
return;
|
||||
}
|
||||
|
||||
if(t.indexOf("<DecodeFinished>") >= 0) {
|
||||
if(m_mode=="QRA64") m_wideGraph->drawRed(0,0);
|
||||
m_bDecoded = t.mid(16).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;
|
||||
}
|
||||
|
||||
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() >= rxSnrThreshold(decodedtext.submode());
|
||||
|
||||
// dupe check
|
||||
auto frame = decodedtext.message();
|
||||
auto frameOffset = decodedtext.frequencyOffset();
|
||||
if(m_messageDupeCache.contains(frame)){
|
||||
// check to see if the frequency is near our previous frame
|
||||
auto cachedFreq = m_messageDupeCache.value(frame, 0);
|
||||
if(qAbs(cachedFreq - frameOffset) <= rxThreshold(decodedtext.submode())){
|
||||
qDebug() << "duplicate frame from" << cachedFreq << "and" << frameOffset;
|
||||
bValidFrame = false;
|
||||
}
|
||||
} else {
|
||||
// cache for this decode cycle
|
||||
m_messageDupeCache[frame] = frameOffset;
|
||||
}
|
||||
|
||||
qDebug() << "valid" << bValidFrame << submodeName(decodedtext.submode()) << "decoded text" << decodedtext.message();
|
||||
|
||||
// skip if invalid
|
||||
if(!bValidFrame) {
|
||||
return;
|
||||
}
|
||||
|
||||
ActivityDetail d = {};
|
||||
CallDetail cd = {};
|
||||
CommandDetail cmd = {};
|
||||
CallDetail td = {};
|
||||
|
||||
// Parse General Activity
|
||||
#if 1
|
||||
bool shouldParseGeneralActivity = true;
|
||||
if(shouldParseGeneralActivity && !decodedtext.messageWords().isEmpty()){
|
||||
int offset = decodedtext.frequencyOffset();
|
||||
|
||||
if(!m_bandActivity.contains(offset)){
|
||||
int range = rxThreshold(decodedtext.submode());
|
||||
|
||||
QList<int> offsets = generateOffsets(offset-range, offset+range);
|
||||
|
||||
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();
|
||||
d.submode = decodedtext.submode();
|
||||
|
||||
// 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(decodedtext.submode(), 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(decodedtext.submode(), 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();
|
||||
cd.submode = decodedtext.submode();
|
||||
|
||||
// Only respond to HEARTBEATS...remember that CQ messages are "Alt" pings
|
||||
if(decodedtext.isHeartbeat()){
|
||||
if(decodedtext.isAlt()){
|
||||
// this is a cq with a standard or compound call, ala "KN4CRD/P: CQCQCQ"
|
||||
cd.cqTimestamp = DriftingDateTime::currentDateTimeUtc();
|
||||
|
||||
// it is not processed elsewhere, so we need to just log it here.
|
||||
logCallActivity(cd, true);
|
||||
|
||||
// notification for cq
|
||||
tryNotify("cq");
|
||||
|
||||
} 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;
|
||||
cmd.submode = cd.submode;
|
||||
m_rxCommandQueue.append(cmd);
|
||||
|
||||
// notification for hb
|
||||
tryNotify("hb");
|
||||
}
|
||||
|
||||
} else {
|
||||
qDebug() << "buffering compound call" << cd.freq << cd.call << cd.bits;
|
||||
|
||||
hasExistingMessageBuffer(cd.submode, 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();
|
||||
cmd.submode = decodedtext.submode();
|
||||
|
||||
// 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;
|
||||
cmdcd.submode = cmd.submode;
|
||||
logCallActivity(cmdcd, false);
|
||||
logHeardGraph(cmd.from, cmd.to);
|
||||
}
|
||||
|
||||
// merge any existing buffer to this frequency
|
||||
hasExistingMessageBuffer(cmd.submode, 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;
|
||||
td.submode = cmd.submode;
|
||||
logCallActivity(td, true);
|
||||
logHeardGraph(cmd.from, cmd.to);
|
||||
}
|
||||
}
|
||||
#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
|
||||
}
|
||||
|
||||
bool MainWindow::hasExistingMessageBufferToMe(int *pOffset){
|
||||
foreach(auto offset, m_messageBuffer.keys()){
|
||||
auto buffer = m_messageBuffer[offset];
|
||||
|
@ -164,6 +164,7 @@ public slots:
|
||||
void tryNotify(const QString &key);
|
||||
int rxThreshold(int submode);
|
||||
int rxSnrThreshold(int submode);
|
||||
void processDecodedLine(QByteArray t);
|
||||
|
||||
protected:
|
||||
void keyPressEvent (QKeyEvent *) override;
|
||||
@ -430,6 +431,7 @@ private slots:
|
||||
void refreshTextDisplay();
|
||||
|
||||
private:
|
||||
Q_SIGNAL void decodedLineReady(QByteArray t);
|
||||
Q_SIGNAL void playNotification(const QString &name);
|
||||
Q_SIGNAL void initializeNotificationAudioOutputStream(const QAudioDeviceInfo &, unsigned, unsigned) const;
|
||||
Q_SIGNAL void initializeAudioOutputStream (QAudioDeviceInfo,
|
||||
|
Loading…
Reference in New Issue
Block a user