From 01249bd115979b900d0423c58d79e0d0af5017ad Mon Sep 17 00:00:00 2001 From: Jordan Sherer Date: Tue, 24 Jul 2018 02:53:01 -0400 Subject: [PATCH] Support compound callsigns --- decodedtext.cpp | 24 ++++++- decodedtext.h | 7 +- mainwindow.cpp | 165 +++++++++++++++++++++++++++++++++++------------- mainwindow.h | 4 +- 4 files changed, 152 insertions(+), 48 deletions(-) diff --git a/decodedtext.cpp b/decodedtext.cpp index bed5a55..c8128c9 100644 --- a/decodedtext.cpp +++ b/decodedtext.cpp @@ -63,6 +63,9 @@ DecodedText::DecodedText (QString const& the_string, bool contest_mode, QString if(!is_standard_){ bool unpacked = false; + if(!unpacked){ + unpacked = tryUnpackCompound(); + } if(!unpacked){ unpacked = tryUnpackDirected(); } @@ -72,6 +75,25 @@ DecodedText::DecodedText (QString const& the_string, bool contest_mode, QString } } +bool DecodedText::tryUnpackCompound(){ + QString m = message().trimmed(); + + // directed calls will always be 12+ chars and contain no spaces. + if(m.length() < 12 || m.contains(' ')){ + return false; + } + + QStringList parts = Varicode::unpackCompoundMessage(m); + + if(parts.isEmpty() || parts.length() < 2){ + return false; + } + + compound_ = QString("%1/%2").arg(parts.at(0), parts.at(1)); + message_ = QString("%1:").arg(compound_); + return true; +} + bool DecodedText::tryUnpackDirected(){ QString m = message().trimmed(); @@ -94,7 +116,7 @@ bool DecodedText::tryUnpackDirected(){ message_ = QString("%1: %2%3 %4").arg(parts.at(0), parts.at(1), parts.at(2), parts.at(3)); } else { // replace it with the correct unpacked (freetext) - message_ = QString(parts.join(QChar())); + message_ = QString(parts.join("")); } directed_ = parts; diff --git a/decodedtext.h b/decodedtext.h index bd954e4..fe75079 100644 --- a/decodedtext.h +++ b/decodedtext.h @@ -32,11 +32,15 @@ class DecodedText public: explicit DecodedText (QString const& message, bool, QString const& my_grid); + bool tryUnpackCompound(); bool tryUnpackDirected(); bool tryUnpackData(); + QString compoundCall() const { return compound_; } + bool isCompoundMessage() const { return !compound_.isEmpty(); } + QStringList directedMessage() const { return directed_; } - bool isDirectedMessage() const { return !directed_.isEmpty(); } + bool isDirectedMessage() const { return !directed_.isEmpty() && directed_.length() > 2; } QString string() const { return string_; } QString message() const { return message_; } @@ -85,6 +89,7 @@ private: column_mode = 19, column_qsoText = 22 }; + QString compound_; QStringList directed_; QString string_; int padding_; diff --git a/mainwindow.cpp b/mainwindow.cpp index 52cdba0..b32a61a 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -1046,10 +1046,23 @@ MainWindow::MainWindow(QDir const& temp_directory, bool multiple, auto clearAction4 = new QAction(QIcon::fromTheme("edit-clear"), QString("Clear"), ui->tableWidgetCalls); connect(clearAction4, &QAction::triggered, this, [this](){ this->on_clearAction_triggered(ui->tableWidgetCalls); }); - ui->tableWidgetCalls->setContextMenuPolicy(Qt::ActionsContextMenu); - ui->tableWidgetCalls->addAction(clearAction4); - ui->tableWidgetCalls->addAction(clearActionSep); - ui->tableWidgetCalls->addAction(clearActionAll); + + ui->tableWidgetCalls->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->tableWidgetCalls, &QTableWidget::customContextMenuRequested, this, [this, clearAction4, clearActionAll](QPoint const &point){ + QMenu * menu = new QMenu(ui->tableWidgetCalls); + + auto directedMenu = menu->addMenu("Directed"); + directedMenu->setDisabled(callsignSelected().isEmpty()); + buildQueryMenu(directedMenu); + + menu->addSeparator(); + menu->addAction(clearAction4); + menu->addSeparator(); + menu->addAction(clearActionAll); + + menu->popup(ui->tableWidgetCalls->mapToGlobal(point)); + }); + m_lastTxTime = QDateTime::currentDateTimeUtc(); @@ -1091,6 +1104,35 @@ MainWindow::MainWindow(QDir const& temp_directory, bool multiple, frames++; qDebug() << "HuffFrames" << frames; qDebug() << Varicode::packCallsignPrefixSuffix("VE3") << Varicode::unpackCallsignPrefixSuffix(Varicode::packCallsignPrefixSuffix("VE3"); + + auto calls = Varicode::parseCallsigns("VE3/KN4CRD 9E/KN4CRD KN4CRD/7 KN4CRD/P KN4CRD"); + foreach(auto call, calls){ + qDebug() << call << Radio::base_callsign(call) << QString(call).replace(Radio::base_callsign(call), ""); + + auto base = Radio::base_callsign(call); + auto fix = call.replace(base, "").replace("/", ""); + qDebug() << fix; + + auto packedCall = ((quint64)Varicode::packCallsign(base) << 32) | Varicode::packCallsignPrefixSuffix(fix); + + auto packed = Varicode::pack64bits(packedCall); + + qDebug() << call << packedCall << packed; + } + + auto call = QString(m_config.my_callsign()); + qDebug() << call; + auto basecall = Radio::base_callsign(call); + qDebug() << call << basecall; + auto fix = QString(call).replace(basecall, ""); + qDebug() << call << basecall << fix; + auto prefix = !fix.startsWith("/"); + fix = fix.replace("/", ""); + + auto packed = Varicode::packCompoundMessage(basecall, fix, prefix, 99); + qDebug() << packed << Varicode::unpackCompoundMessage(packed); + + m_valid = false; #endif // this must be the last statement of constructor @@ -3257,7 +3299,7 @@ void MainWindow::readFromStdout() //readFromStdout auto parts = decodedtext.directedMessage(); CommandDetail d; - d.from = parts.at(0); + d.from = lookupCallInCompoundCache(parts.at(0)); d.to = parts.at(1); d.cmd = parts.at(2); d.freq = decodedtext.frequencyOffset(); @@ -3268,8 +3310,8 @@ void MainWindow::readFromStdout() //readFromStdout CallDetail cd; cd.call = d.from; cd.grid = ""; - cd.snr = decodedtext.snr(); - cd.freq = decodedtext.frequencyOffset(); + cd.snr = d.snr; + cd.freq = d.freq; cd.utcTimestamp = d.utcTimestamp; m_callActivity[Radio::base_callsign(cd.call)] = cd; @@ -3280,6 +3322,13 @@ void MainWindow::readFromStdout() //readFromStdout pskLogReport("FT8Call", d.freq, d.snr, d.from, ""); } } + + // Process compound callsign commands (put them in cache)" + bool shouldProcessCompound = true; + if(shouldProcessCompound && decodedtext.isCompoundMessage()){ + QString compoundCall = decodedtext.compoundCall(); + m_compoundCallCache[Radio::base_callsign(compoundCall)] = compoundCall; + } } } @@ -3416,6 +3465,10 @@ void MainWindow::readFromStdout() //readFromStdout // See MainWindow::postDecode for displaying the latest decodes } +QString MainWindow::lookupCallInCompoundCache(QString const &call){ + return m_compoundCallCache.value(call, call); +} + // // start_tolerance - only respond to "DE ..." and free text 73 // messages within +/- this value @@ -5483,7 +5536,13 @@ QPair MainWindow::buildFT8MessageFrames(QString const& QStringList frames; QStringList lines; - QString mycall = Radio::base_callsign(m_config.my_callsign()); + // prepare compound + bool compound = Radio::is_compound_callsign(m_config.my_callsign()); + QString mycall = m_config.my_callsign(); + QString basecall = Radio::base_callsign(m_config.my_callsign()); + QString fix = QString(m_config.my_callsign()).replace(basecall, ""); + bool prefix = !fix.startsWith("/"); + fix = fix.replace("/", ""); foreach(QString line, text.split(QRegExp("[\\r\\n]"), QString::SkipEmptyParts)){ @@ -5509,7 +5568,7 @@ QPair MainWindow::buildFT8MessageFrames(QString const& QString stdFrame = parseFT8Message(line, &isFree); int n = 0; - QString dirFrame = Varicode::packDirectedMessage(line, mycall, &n); + QString dirFrame = Varicode::packDirectedMessage(line, basecall, &n); int m = 0; QString datFrame = Varicode::packDataMessage(line.left(21) + "\x04", &m); // 63 / 3 = 21 (maximum number of 3bit chars we could possibly stuff in here) @@ -5546,11 +5605,21 @@ QPair MainWindow::buildFT8MessageFrames(QString const& } if(useDir){ - frames.append(frame); - // TODO: jsherer - would be nice to clean this up and have an object that can just decode the actual transmitted frames instead. - if(!line.startsWith(mycall)){ - lines.append(QString("%1: ").arg(mycall)); + if(compound){ + QString compoundFrame = Varicode::packCompoundMessage(basecall, fix, prefix, 0); + if(!compoundFrame.isEmpty()){ + frames.append(compoundFrame); + lines.append(QString("%1: ").arg(mycall)); + } } + + frames.append(frame); + + // TODO: jsherer - would be nice to clean this up and have an object that can just decode the actual transmitted frames instead. + if(!line.startsWith(basecall) && !compound){ + lines.append(QString("%1: ").arg(basecall)); + } + lines.append(line.left(n)); line = line.mid(n); } @@ -5566,7 +5635,7 @@ QPair MainWindow::buildFT8MessageFrames(QString const& #if 1 qDebug() << "parsed frames:"; foreach(auto frame, frames){ - qDebug() << "->" << frame << Varicode::unpackDataMessage(frame) << Varicode::unpackDirectedMessage(frame); + qDebug() << "->" << frame << Varicode::unpackDataMessage(frame) << Varicode::unpackDirectedMessage(frame) << Varicode::unpackCompoundMessage(frame); } qDebug() << "lines:"; @@ -6910,14 +6979,6 @@ void MainWindow::on_qtcMacroButton_clicked(){ addMessageText(qtc); } -void MainWindow::on_replyMacroButton_clicked(){ - QString selectedCall = callsignSelected(); - if(selectedCall.isEmpty()){ - return; - } - addMessageText(selectedCall); -} - void MainWindow::on_qthMacroButton_clicked(){ QString qth = m_config.my_qth(); if(qth.isEmpty()){ @@ -6929,20 +6990,6 @@ void MainWindow::on_qthMacroButton_clicked(){ addMessageText(qth); } -void MainWindow::on_snrMacroButton_clicked(){ - 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))); -} - void MainWindow::buildQueryMenu(QMenu * menu){ QString call = callsignSelected(); if(call.isEmpty()){ @@ -6951,6 +6998,38 @@ void MainWindow::buildQueryMenu(QMenu * menu){ bool isAllCall = call == "ALLCALL"; + auto sendReplyAction = menu->addAction("Reply to Callsign"); + + connect(sendReplyAction, &QAction::triggered, this, [this](){ + + QString selectedCall = callsignSelected(); + if(selectedCall.isEmpty()){ + return; + } + + addMessageText(QString("%1").arg(selectedCall), true); + }); + + + auto sendSNRAction = menu->addAction("Send Signal Report"); + sendSNRAction->setEnabled(m_callActivity.contains(call)); + 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); + }); + + menu->addSeparator(); + auto snrAction = menu->addAction("? - What is my signal report?"); connect(snrAction, &QAction::triggered, this, [this](){ @@ -8080,12 +8159,10 @@ void MainWindow::updateButtonDisplay(){ bool emptyQTH = m_config.my_qth().isEmpty() && m_config.my_grid().isEmpty(); ui->cqMacroButton->setDisabled(isTransmitting); - ui->replyMacroButton->setDisabled(isTransmitting || emptyCallsign); ui->qtcMacroButton->setDisabled(isTransmitting || emptyQTC); ui->qthMacroButton->setDisabled(isTransmitting || emptyQTH); - ui->snrMacroButton->setDisabled(isTransmitting || emptyCallsign); - ui->queryButton->setDisabled(isTransmitting || emptyCallsign); ui->macrosMacroButton->setDisabled(isTransmitting); + ui->queryButton->setDisabled(isTransmitting || emptyCallsign); } QString MainWindow::callsignSelected(){ @@ -8356,7 +8433,7 @@ void MainWindow::displayActivity(bool force){ } // TODO: jsherer - check to make sure we haven't replied to their allcall recently (in the past beacon interval) - if(isAllCall && m_txAllcallCommandCache.contains(d.from) && m_txAllcallCommandCache[d.from]->secsTo(QDateTime::currentDateTimeUtc())/60 < m_config.beacon()){ + if(isAllCall && m_txAllcallCommandCache.contains(Radio::base_callsign(d.from)) && m_txAllcallCommandCache[Radio::base_callsign(d.from)]->secsTo(QDateTime::currentDateTimeUtc())/60 < m_config.beacon()){ continue; } @@ -8365,7 +8442,7 @@ void MainWindow::displayActivity(bool force){ // SNR if(d.cmd == "?"){ - reply = QString("%1 SNR %2").arg(d.from).arg(Varicode::formatSNR(d.snr)); + reply = QString("%1 SNR %2").arg(Radio::base_callsign(d.from)).arg(Varicode::formatSNR(d.snr)); } // QTH else if(d.cmd == "@" && !isAllCall){ @@ -8378,11 +8455,11 @@ void MainWindow::displayActivity(bool force){ qth = grid; } - reply = QString("%1 %2").arg(d.from).arg(qth); + reply = QString("%1 %2").arg(Radio::base_callsign(d.from)).arg(qth); } // STATION MESSAGE else if(d.cmd == "&" && !isAllCall){ - reply = QString("%1 %2").arg(d.from).arg(m_config.my_station()); + reply = QString("%1 %2").arg(Radio::base_callsign(d.from)).arg(m_config.my_station()); } else { continue; } @@ -8395,7 +8472,7 @@ void MainWindow::displayActivity(bool force){ // if we're responding via allcall, pick a different frequency and mark it in the cache. if(d.to == "ALLCALL"){ f = findFreeFreqOffset(qMax(0, f-100), qMin(f+100, 2500), 50); - m_txAllcallCommandCache.insert(d.from, new QDateTime(QDateTime::currentDateTimeUtc()), 25); + m_txAllcallCommandCache.insert(Radio::base_callsign(d.from), new QDateTime(QDateTime::currentDateTimeUtc()), 25); } processed = true; diff --git a/mainwindow.h b/mainwindow.h index 9839558..4e320ab 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -121,6 +121,7 @@ public slots: void msgAvgDecode2(); void fastPick(int x0, int x1, int y); + QString lookupCallInCompoundCache(QString const &call); void clearActivity(); int logRxTxMessageText(QDateTime date, bool isFree, QString text, int freq, bool tx, int block=-1); void addMessageText(QString text, bool clear=false); @@ -238,9 +239,7 @@ private slots: void on_clearAction_triggered(QObject * sender); void on_cqMacroButton_clicked(); void on_qtcMacroButton_clicked(); - void on_replyMacroButton_clicked(); void on_qthMacroButton_clicked(); - void on_snrMacroButton_clicked(); void buildQueryMenu(QMenu *); void on_queryButton_pressed(); void on_macrosMacroButton_pressed(); @@ -684,6 +683,7 @@ private: QQueue m_txFrameQueue; QQueue m_rxFrameQueue; QQueue m_rxCommandQueue; + QMap m_compoundCallCache; // base callsign -> compound callsign QCache m_txAllcallCommandCache; // callsign -> last tx QCache m_rxRecentCache; // freq -> last rx QCache m_rxDirectedCache; // freq -> last directed rx