diff --git a/Configuration.cpp b/Configuration.cpp index 301f42a..d220f82 100644 --- a/Configuration.cpp +++ b/Configuration.cpp @@ -769,7 +769,7 @@ bool Configuration::clear_DX () const {return m_->clear_DX_;} bool Configuration::miles () const {return m_->miles_;} bool Configuration::quick_call () const {return m_->quick_call_;} bool Configuration::disable_TX_on_73 () const {return m_->disable_TX_on_73_;} -int Configuration::beacon () const {return m_->beacon_;} +int Configuration::beacon () const {return qMax(10, qMin(m_->beacon_, 1440));} int Configuration::watchdog () const {return m_->watchdog_;} bool Configuration::TX_messages () const {return m_->TX_messages_;} bool Configuration::enable_VHF_features () const {return m_->enable_VHF_features_;} diff --git a/Configuration.ui b/Configuration.ui index 884eb97..e6ce461 100644 --- a/Configuration.ui +++ b/Configuration.ui @@ -530,10 +530,10 @@ - 15 + 10 - 60 + 1440 1 diff --git a/mainwindow.cpp b/mainwindow.cpp index 1e2cda8..04fcc35 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -904,7 +904,7 @@ MainWindow::MainWindow(QDir const& temp_directory, bool multiple, connect(&TxAgainTimer, SIGNAL(timeout()), this, SLOT(TxAgain())); beaconTimer.setSingleShot(false); - connect(&beaconTimer, &QTimer::timeout, this, &MainWindow::checkBacon); + connect(&beaconTimer, &QTimer::timeout, this, &MainWindow::checkBeacon); connect(m_wideGraph.data (), SIGNAL(setFreq3(int,int)),this, SLOT(setFreq4(int,int))); @@ -3719,7 +3719,18 @@ void MainWindow::readFromStdout() //readFromStdout cd.bits = decodedtext.bits(); if(decodedtext.isBeacon()){ - logCallActivity(cd, true); + // convert BEACON to a directed command and process... + CommandDetail d = {}; + d.from = cd.call; + d.to = "ALLCALL"; + d.cmd = " BEACON"; + d.snr = cd.snr; + d.bits = cd.bits; + d.extra = cd.grid; + d.freq = cd.freq; + d.utcTimestamp = cd.utcTimestamp; + m_rxCommandQueue.append(d); + } else { qDebug() << "buffering compound call" << cd.call << cd.bits; m_messageBuffer[cd.freq/10*10].compound.append(cd); @@ -6200,6 +6211,11 @@ void MainWindow::enqueueMessage(int priority, QString message, int freq, Callbac ); } +void MainWindow::enqueueBeacon(QString message){ + m_txBeaconQueue.enqueue(message); + scheduleBeacon(true); +} + void MainWindow::resetMessage(){ resetMessageUI(); resetMessageTransmitQueue(); @@ -6356,13 +6372,19 @@ QStringList MainWindow::buildFT8MessageFrames(QString const& text){ bool hasData = false; // remove our callsign from the start of the line... - if(line.startsWith(mycall + ":")){ + if(line.startsWith(mycall + ":") || line.startsWith(mycall + " ")){ line = lstrip(line.mid(mycall.length() + 1)); } - if(line.startsWith(basecall + ":")){ + if(line.startsWith(basecall + ":") || line.startsWith(basecall + " ")){ line = lstrip(line.mid(basecall.length() + 1)); } + // remove trailing whitespace as long as there are characters left afterwards + auto rline = rstrip(line); + if(!rline.isEmpty()){ + line = rline; + } + #if AUTO_PREPEND_DIRECTED // see if we need to prepend the directed call to the line... // if we have a selected call and the text doesn't start with that call... @@ -6650,7 +6672,7 @@ bool MainWindow::prepareNextMessageFrame() if(ui->beaconButton->isChecked()){ // bump beacon - scheduleBacon(false); + scheduleBeacon(false); } return true; @@ -6711,7 +6733,7 @@ int MainWindow::findFreeFreqOffset(int fmin, int fmax, int bw){ } // scheduleBeacon -void MainWindow::scheduleBacon(bool first){ +void MainWindow::scheduleBeacon(bool first){ auto timestamp = DriftingDateTime::currentDateTimeUtc(); auto orig = timestamp; @@ -6742,7 +6764,7 @@ void MainWindow::scheduleBacon(bool first){ } // pauseBeacon -void MainWindow::pauseBacon(){ +void MainWindow::pauseBeacon(){ ui->beaconButton->setChecked(false); m_nextBeaconPaused = true; @@ -6752,40 +6774,52 @@ void MainWindow::pauseBacon(){ } // checkBeacon -void MainWindow::checkBacon(){ +void MainWindow::checkBeacon(){ if(!ui->beaconButton->isChecked()){ return; } - if(DriftingDateTime::currentDateTimeUtc().secsTo(m_nextBeacon) > 5){ + auto secondsUntilBeacon = DriftingDateTime::currentDateTimeUtc().secsTo(m_nextBeacon); + if(secondsUntilBeacon > 5 && m_txBeaconQueue.isEmpty()){ return; } if(m_nextBeaconQueued){ return; } - prepareBacon(); + prepareBeacon(); } // prepareBeacon -void MainWindow::prepareBacon(){ +void MainWindow::prepareBeacon(){ QStringList lines; QString mycall = m_config.my_callsign(); QString mygrid = m_config.my_grid().left(4); // FT8Call Style - lines.append(QString("%1: BEACON %2").arg(mycall).arg(mygrid)); - - bool shouldTransmitTwoBeacons = true; - if(shouldTransmitTwoBeacons){ + if(m_txBeaconQueue.isEmpty()){ lines.append(QString("%1: BEACON %2").arg(mycall).arg(mygrid)); + + bool shouldTransmitTwoBeacons = true; + if(shouldTransmitTwoBeacons){ + lines.append(QString("%1: BEACON %2").arg(mycall).arg(mygrid)); + } + } else { + while(!m_txBeaconQueue.isEmpty() && lines.length() < 2){ + lines.append(m_txBeaconQueue.dequeue()); + } } // Choose a beacon frequency auto f = findFreeFreqOffset(500, 1000, 50); + auto text = lines.join(QChar('\n')); + if(text.isEmpty()){ + return; + } + // Queue the beacon - enqueueMessage(PriorityLow, lines.join(QChar('\n')), f, [this](){ + enqueueMessage(PriorityLow, text, f, [this](){ m_nextBeaconQueued = false; }); @@ -7616,6 +7650,8 @@ void MainWindow::buildQueryMenu(QMenu * menu, QString 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(); + auto callAction = menu->addAction(QString("Send a directed message to selected callsign")); connect(callAction, &QAction::triggered, this, [this](){ @@ -7729,17 +7765,6 @@ void MainWindow::buildQueryMenu(QMenu * menu, QString call){ if(m_config.transmit_directed()) toggleTx(true); }); - auto qsoQueryAction = menu->addAction(QString("%1 QSO [CALLSIGN]? - Can you communicate directly with [CALLSIGN]?").arg(call).trimmed()); - connect(qsoQueryAction, &QAction::triggered, this, [this](){ - - QString selectedCall = callsignSelected(); - if(selectedCall.isEmpty()){ - return; - } - - addMessageText(QString("%1 QSO [CALLSIGN]?").arg(selectedCall), true, true); - }); - auto hashAction = menu->addAction(QString("%1#[MESSAGE] - Please ACK if you receive this message in its entirety").arg(call).trimmed()); hashAction->setDisabled(isAllCall); connect(hashAction, &QAction::triggered, this, [this](){ @@ -7778,10 +7803,22 @@ void MainWindow::buildQueryMenu(QMenu * menu, QString call){ addMessageText(QString("%1>[MESSAGE]").arg(selectedCall), true, true); }); + auto qsoQueryAction = menu->addAction(QString("%1 BEACON REQ [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 BEACON REQ [CALLSIGN]?").arg(selectedCall), true, true); + }); + menu->addSeparator(); bool emptyQTC = m_config.my_station().isEmpty(); - bool emptyQTH = m_config.my_qth().isEmpty() && m_config.my_grid().isEmpty(); + bool emptyQTH = m_config.my_qth().isEmpty(); + bool emptyGrid = m_config.my_grid().isEmpty(); auto qtcAction = menu->addAction(QString("%1 QTC - Send my station message").arg(call).trimmed()); qtcAction->setDisabled(emptyQTC); @@ -7811,8 +7848,9 @@ void MainWindow::buildQueryMenu(QMenu * menu, QString call){ if(m_config.transmit_directed()) toggleTx(true); }); - auto grid = m_config.my_grid(); + 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(); @@ -8294,9 +8332,9 @@ void MainWindow::on_pbT2R_clicked() void MainWindow::on_beaconButton_clicked() { if(ui->beaconButton->isChecked()){ - scheduleBacon(true); + scheduleBeacon(true); } else { - pauseBacon(); + pauseBeacon(); } } @@ -9653,17 +9691,11 @@ void MainWindow::processCommandActivity() { // construct a reply, if needed QString reply; + int priority = PriorityNormal; + int freq = -1; // QUERIED SNR - if (d.cmd == "?") { - // do not respond to allcall ? if: - // 1. we recently responded to one - // 2. or, we are in a directed qso...(i.e., we have a callsign selected that isn't ALLCALL) - auto selectedCall = callsignSelected(); - if(isAllCall && !selectedCall.isEmpty() && selectedCall != "ALLCALL" && selectedCall != d.from){ - continue; - } - + if (d.cmd == "?" && !isAllCall) { reply = QString("%1 SNR %2").arg(d.from).arg(Varicode::formatSNR(d.snr)); } @@ -9810,8 +9842,22 @@ void MainWindow::processCommandActivity() { reply = m_lastTxMessage; } - // PROCESS BUFFERED QSO QUERY - else if (d.cmd == " QSO"){ + // PROCESS BEACON + else if (d.cmd == " BEACON" && ui->beaconButton->isChecked()){ + reply = QString("%1 BEACON ACK %2").arg(d.from).arg(Varicode::formatSNR(d.snr)); + + enqueueBeacon(reply); + + if(isAllCall){ + // since all beacons are technically ALLCALL, let's bump the allcall cache here... + m_txAllcallCommandCache.insert(d.from, new QDateTime(now), 25); + } + + continue; + } + + // PROCESS BUFFERED BEACON REQ QUERY + else if (d.cmd == " BEACON REQ" && ui->beaconButton->isChecked()){ auto who = d.text; if(who.isEmpty()){ continue; @@ -9831,11 +9877,22 @@ void MainWindow::processCommandActivity() { } if(baseCall == cd.call || baseCall == Radio::base_callsign(cd.call)){ - auto r = QString("%1 ACK %2 %3 (%4)").arg(d.from).arg(cd.call).arg(Varicode::formatSNR(cd.snr)).arg(since(cd.utcTimestamp)); + auto r = QString("%1 BEACON ACK %2").arg(cd.call).arg(Varicode::formatSNR(cd.snr)); replies.append(r); } } reply = replies.join("\n"); + + if(!reply.isEmpty()){ + enqueueBeacon(reply); + + if(isAllCall){ + // since all beacons are technically ALLCALL, let's bump the allcall cache here... + m_txAllcallCommandCache.insert(d.from, new QDateTime(now), 25); + } + } + + continue; } // PROCESS BUFFERED APRS: @@ -9882,8 +9939,8 @@ void MainWindow::processCommandActivity() { continue; } - // do not queue ALLCALL replies if auto-reply is not checked - if(!ui->autoReplyButton->isChecked() && isAllCall){ + // do not queue ALLCALL replies if auto-reply is not checked or it's a beacon reply + if(!ui->autoReplyButton->isChecked() && isAllCall && !d.cmd.contains("BEACON")){ continue; } @@ -9896,7 +9953,7 @@ void MainWindow::processCommandActivity() { // 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(PriorityNormal, reply, -1, nullptr); + enqueueMessage(priority, reply, freq, nullptr); } } diff --git a/mainwindow.h b/mainwindow.h index f91b3cd..e934761 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -145,6 +145,7 @@ public slots: void prependMessageText(QString text); void addMessageText(QString text, bool clear=false, bool selectFirstPlaceholder=false); void enqueueMessage(int priority, QString message, int freq, Callback c); + void enqueueBeacon(QString message); void resetMessage(); void resetMessageUI(); void restoreMessage(); @@ -297,10 +298,10 @@ private slots: bool prepareNextMessageFrame(); bool isFreqOffsetFree(int f, int bw); int findFreeFreqOffset(int fmin, int fmax, int bw); - void scheduleBacon(bool first=false); - void pauseBacon(); - void checkBacon(); - void prepareBacon(); + void scheduleBeacon(bool first=false); + void pauseBeacon(); + void checkBeacon(); + void prepareBeacon(); QString calculateDistance(QString const& grid, int *pDistance=nullptr); void on_driftSpinBox_valueChanged(int n); void on_driftSyncButton_clicked(); @@ -772,6 +773,7 @@ private: QMap> m_bandActivity; // freq -> [(text, last timestamp), ...] QMap m_messageBuffer; // freq -> (cmd, [frames, ...]) QMap m_callActivity; // call -> (last freq, last timestamp) + QQueue m_txBeaconQueue; // beacon frames to be sent QMap> m_callActivityCache; // band -> call activity QMap>> m_bandActivityCache; // band -> band activity diff --git a/varicode.cpp b/varicode.cpp index 537b64a..782db8a 100644 --- a/varicode.cpp +++ b/varicode.cpp @@ -52,16 +52,18 @@ QMap directed_cmds = { // {"=", 9 }, // unused // {"/", 10 }, // unused - // {"/", 11 }, // unused - // {"/", 12 }, // unused - // {"/", 13 }, // unused // directed responses - {" QSO", 13 }, // can you communicate with? i can communicate with + {" BEACON", -1 }, // this is my beacon (unused except for faux processing of beacons as directed commands) + {" BEACON ACK", 12 }, // i received your beacon at this snr + {" BEACON REQ", 13 }, // can you transmit a beacon to callsign? + {" APRS:", 14 }, // send an aprs packet + {" GRID", 15 }, // this is my current grid locator {" QTC", 16 }, // this is my qtc message {" QTH", 17 }, // this is my qth message + {" FB", 18 }, // fine business {" HW CPY?", 19 }, // how do you copy? {" HEARING", 20 }, // i am hearing the following stations @@ -78,14 +80,14 @@ QMap directed_cmds = { {" ", 31 }, // send freetext }; -QSet allowed_cmds = {0, 1, 2, 3, 4, 5, /*6,*/ /*7,*/ 8, /*...*/ 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, /*24,*/ 25, 26, 27, 28, 29, 30, 31}; +QSet allowed_cmds = {-1, 0, 1, 2, 3, 4, 5, /*6,*/ /*7,*/ 8, /*...*/ 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, /*24,*/ 25, 26, 27, 28, 29, 30, 31}; -QSet buffered_cmds = {5, /*6,*/ /*7,*/ 8, 13, 14, 15}; +QSet buffered_cmds = {3, 5, /*6,*/ /*7,*/ 8, 13, 14, 15}; + +QSet snr_cmds = {12, 25}; QMap checksum_cmds = { { 5, 16 }, - /*{ 6, 16 },*/ - /*{ 7, 16 },*/ { 8, 32 }, { 13, 16 }, { 14, 16 }, @@ -93,10 +95,10 @@ QMap checksum_cmds = { }; QString callsign_pattern = QString("(?[A-Z0-9/]+)"); -QString optional_cmd_pattern = QString("(?\\s?(?:AGN[?]|ACK|73|YES|NO|SNR|QSL[?]?|RR|HEARING|HW CPY[?]|FB|QTH|QTC|GRID|APRS[:]|QSO|[?@&$%#^> ]))?"); +QString optional_cmd_pattern = QString("(?\\s?(?:AGN[?]|ACK|73|YES|NO|SNR|QSL[?]?|RR|HEARING|HW CPY[?]|FB|QTH|QTC|GRID|APRS[:]|BEACON (ACK|REQ)|[?@&$%#^> ]))?"); QString optional_grid_pattern = QString("(?\\s?[A-R]{2}[0-9]{2})?"); QString optional_extended_grid_pattern = QString("^(?\\s?(?:[A-R]{2}[0-9]{2}(?:[A-X]{2}(?:[0-9]{2})?)*))?"); -QString optional_num_pattern = QString("(?(?<=SNR|HEARING)\\s?[-+]?(?:3[01]|[0-2]?[0-9]))?"); +QString optional_num_pattern = QString("(?(?<=SNR|HEARING|BEACON ACK)\\s?[-+]?(?:3[01]|[0-2]?[0-9]))?"); QRegularExpression directed_re("^" + callsign_pattern + @@ -1084,11 +1086,13 @@ quint8 Varicode::packCmd(quint8 cmd, quint8 num, bool *pPackedNum){ // if cmd == snr quint8 value = 0; - if(cmd == directed_cmds[" SNR"]){ + auto cmdStr = directed_cmds.key(cmd); + if(Varicode::isSNRCommand(cmdStr)){ // 8 bits - 1 bit flag + 1 bit type + 6 bit number // [1][X][6] // X = 0 == SNR - value = (1 << 1) << 6; + // X = 1 == BEACON ACK + value = ((1 << 1) | (int)(cmdStr == " BEACON ACK")) << 6; value = value + (num & ((1<<6)-1)); if(pPackedNum) *pPackedNum = true; } else { @@ -1103,13 +1107,22 @@ quint8 Varicode::unpackCmd(quint8 value, quint8 *pNum){ // if the first bit is 1, this is an SNR with a number encoded in the lower 6 bits if(value & (1<<7)){ if(pNum) *pNum = value & ((1<<6)-1); - return directed_cmds[" SNR"]; + + auto cmd = directed_cmds[" SNR"]; + if(value & (1<<6)){ + cmd = directed_cmds[" BEACON ACK"]; + } + return cmd; } else { if(pNum) *pNum = 0; return value & ((1<<7)-1); } } +bool Varicode::isSNRCommand(const QString &cmd){ + return directed_cmds.contains(cmd) && snr_cmds.contains(directed_cmds[cmd]); +} + bool Varicode::isCommandAllowed(const QString &cmd){ return directed_cmds.contains(cmd) && allowed_cmds.contains(directed_cmds[cmd]); } @@ -1288,10 +1301,11 @@ QStringList Varicode::unpackCompoundMessage(const QString &text, quint8 *pType, } else if (nusergrid <= extra && extra < nmaxgrid) { quint8 num = 0; auto cmd = Varicode::unpackCmd(extra - nusergrid, &num); + auto cmdStr = directed_cmds.key(cmd); - unpacked.append(directed_cmds.key(cmd)); + unpacked.append(cmdStr); - if(cmd == directed_cmds[" SNR"]){ + if(Varicode::isSNRCommand(cmdStr)){ unpacked.append(Varicode::formatSNR(num - 31)); } } @@ -1498,8 +1512,7 @@ QStringList Varicode::unpackDirectedMessage(const QString &text, quint8 *pType){ unpacked.append(cmd); if(extra != 0){ - // TODO: jsherer - should we decide which format to use on the command, or something else? - if(packed_cmd == directed_cmds[" SNR"]) { + if(Varicode::isSNRCommand(cmd)){ unpacked.append(Varicode::formatSNR((int)extra-31)); } else { unpacked.append(QString("%1").arg(extra-31)); diff --git a/varicode.h b/varicode.h index ffa4605..8ad340f 100644 --- a/varicode.h +++ b/varicode.h @@ -123,6 +123,7 @@ public: static quint8 packCmd(quint8 cmd, quint8 num, bool *pPackedNum); static quint8 unpackCmd(quint8 value, quint8 *pNum); + static bool isSNRCommand(const QString &cmd); static bool isCommandAllowed(const QString &cmd); static bool isCommandBuffered(const QString &cmd); static int isCommandChecksumed(const QString &cmd);