2026-01-04 12:54:34 -06:00
|
|
|
#include "JS8_UI/mainwindow.h"
|
|
|
|
|
|
2026-01-05 08:23:33 -06:00
|
|
|
/** \file
|
|
|
|
|
* @brief member function of the MainWindow class
|
|
|
|
|
* processes JS8 commands
|
|
|
|
|
*/
|
|
|
|
|
|
2026-01-04 12:54:34 -06:00
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
auto selectedCallsign = callsignSelected();
|
|
|
|
|
bool isAllCall = isAllCallIncluded(d.to);
|
2026-01-14 10:32:00 -08:00
|
|
|
bool isGroupCall = isGroupCallIncluded(d.to) || d.to == "@APRSIS";
|
2026-01-04 12:54:34 -06:00
|
|
|
|
|
|
|
|
qCDebug(mainwindow_js8)
|
|
|
|
|
<< "try processing command" << d.from << d.to << d.cmd << d.dial
|
|
|
|
|
<< d.offset << 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.dial = d.dial;
|
|
|
|
|
cd.offset = d.offset;
|
|
|
|
|
cd.bits = d.bits;
|
|
|
|
|
cd.ackTimestamp =
|
|
|
|
|
d.text.contains(": ACK") || toMe ? d.utcTimestamp : QDateTime{};
|
|
|
|
|
cd.utcTimestamp = d.utcTimestamp;
|
|
|
|
|
cd.tdrift = d.tdrift;
|
|
|
|
|
cd.submode = d.submode;
|
|
|
|
|
logCallActivity(cd, true);
|
|
|
|
|
logHeardGraph(d.from, d.to);
|
|
|
|
|
|
|
|
|
|
// Placeholder for callbacks to be passed to confirmThenEnqueueMessage
|
|
|
|
|
// and enqueueMessage
|
|
|
|
|
Callback callback = nullptr;
|
|
|
|
|
|
|
|
|
|
// PROCESS BUFFERED HEARING FOR EVERYONE
|
|
|
|
|
if (d.cmd == " HEARING") {
|
|
|
|
|
// 1. parse callsigns
|
|
|
|
|
// 2. log it to the heard graph
|
|
|
|
|
auto calls = Varicode::parseCallsigns(d.text);
|
|
|
|
|
foreach (auto call, calls) {
|
|
|
|
|
logHeardGraph(d.from, call);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// PROCESS BUFFERED GRID FOR EVERYONE
|
|
|
|
|
if (d.cmd == " GRID") {
|
|
|
|
|
// 1. parse grids
|
|
|
|
|
// 2. log it to our call activity
|
|
|
|
|
auto grids = Varicode::parseGrids(d.text);
|
|
|
|
|
foreach (auto grid, grids) {
|
|
|
|
|
CallDetail cd = {};
|
|
|
|
|
cd.bits = d.bits;
|
|
|
|
|
cd.call = d.from;
|
|
|
|
|
cd.dial = d.dial;
|
|
|
|
|
cd.offset = d.offset;
|
|
|
|
|
cd.grid = grid;
|
|
|
|
|
cd.snr = d.snr;
|
|
|
|
|
cd.utcTimestamp = d.utcTimestamp;
|
|
|
|
|
cd.tdrift = d.tdrift;
|
|
|
|
|
cd.submode = d.submode;
|
|
|
|
|
|
|
|
|
|
// PROCESS GRID SPOTS TO APRSIS FOR EVERYONE
|
2026-01-14 10:32:00 -08:00
|
|
|
/**
|
|
|
|
|
* APRS relay: inbound `MSG TO:` stores for our local station.
|
|
|
|
|
* Dedupe within 60s using TO + TEXT + DE <SENDER>.
|
|
|
|
|
*/
|
|
|
|
|
if (d.to == "@APRSIS") {
|
|
|
|
|
|
2026-01-04 12:54:34 -06:00
|
|
|
spotAprsGrid(cd.dial, cd.offset, cd.snr, cd.call, cd.grid);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-14 10:32:00 -08:00
|
|
|
|
2026-01-04 12:54:34 -06:00
|
|
|
logCallActivity(cd, true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// PROCESS @JS8NET, @APRSIS, AND OTHER GROUP SPOTS FOR EVERYONE
|
|
|
|
|
if (d.to.startsWith("@")) {
|
|
|
|
|
spotCmd(d);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// PROCESS @APRSIS CMD SPOTS FOR EVERYONE
|
|
|
|
|
if (d.to == "@APRSIS") {
|
|
|
|
|
spotAprsCmd(d);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// PREPARE CMD TEXT STRING
|
|
|
|
|
QStringList textList = {
|
|
|
|
|
QString("%1: %2%3").arg(d.from).arg(d.to).arg(d.cmd)};
|
|
|
|
|
|
|
|
|
|
if (!d.extra.isEmpty()) {
|
|
|
|
|
textList.append(d.extra);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!d.text.isEmpty()) {
|
|
|
|
|
textList.append(d.text);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QString text = textList.join(" ");
|
|
|
|
|
bool isLast = (d.bits & Varicode::JS8CallLast) == Varicode::JS8CallLast;
|
|
|
|
|
if (isLast) {
|
|
|
|
|
// append the eot character to the text
|
|
|
|
|
text = QString("%1 %2 ")
|
|
|
|
|
.arg(Varicode::rstrip(text))
|
|
|
|
|
.arg(m_config.eot());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// log the text to directed txt log
|
|
|
|
|
writeMsgTxt(text, d.snr, d.offset);
|
|
|
|
|
|
|
|
|
|
// write all directed messages to api
|
|
|
|
|
if (canSendNetworkMessage()) {
|
|
|
|
|
sendNetworkMessage(
|
|
|
|
|
"RX.DIRECTED", text,
|
|
|
|
|
{{"_ID", QVariant(-1)},
|
|
|
|
|
{"FROM", QVariant(d.from)},
|
|
|
|
|
{"TO", QVariant(d.to)},
|
|
|
|
|
{"CMD", QVariant(d.cmd)},
|
|
|
|
|
{"GRID", QVariant(d.grid)},
|
|
|
|
|
{"EXTRA", QVariant(d.extra)},
|
2026-01-08 16:11:57 -05:00
|
|
|
{"TEXT", QVariant(d.text)},
|
2026-01-04 12:54:34 -06:00
|
|
|
{"FREQ", QVariant(d.dial + d.offset)},
|
|
|
|
|
{"DIAL", QVariant(d.dial)},
|
|
|
|
|
{"OFFSET", QVariant(d.offset)},
|
|
|
|
|
{"SNR", QVariant(d.snr)},
|
|
|
|
|
{"SPEED", QVariant(d.submode)},
|
|
|
|
|
{"TDRIFT", QVariant(d.tdrift)},
|
|
|
|
|
{"UTC", QVariant(d.utcTimestamp.toMSecsSinceEpoch())}});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 != " CQ" &&
|
|
|
|
|
d.cmd != " HB" && d.cmd != " HEARTBEAT") {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ActivityDetail ad = {};
|
|
|
|
|
ad.isLowConfidence = false;
|
|
|
|
|
ad.isDirected = true;
|
|
|
|
|
ad.bits = d.bits;
|
|
|
|
|
ad.dial = d.dial;
|
|
|
|
|
ad.offset = d.offset;
|
|
|
|
|
ad.snr = d.snr;
|
|
|
|
|
ad.text = text;
|
|
|
|
|
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 HEARTBEAT"))) {
|
|
|
|
|
shouldDisplay = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (shouldDisplay) {
|
|
|
|
|
auto c = ui->textEditRX->textCursor();
|
|
|
|
|
c.movePosition(QTextCursor::End);
|
|
|
|
|
ui->textEditRX->setTextCursor(c);
|
|
|
|
|
|
|
|
|
|
// ACKs and SNRs 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") &&
|
|
|
|
|
!d.cmd.contains(" SNR")); /* && 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);
|
|
|
|
|
qCDebug(mainwindow_js8) << "should display directed message, "
|
|
|
|
|
"erasing last rx activity line..."
|
|
|
|
|
<< c.selectedText().toUpper();
|
|
|
|
|
c.removeSelectedText();
|
|
|
|
|
c.deletePreviousChar();
|
|
|
|
|
c.deletePreviousChar();
|
|
|
|
|
/*
|
|
|
|
|
c.deleteChar();
|
|
|
|
|
c.deleteChar();
|
|
|
|
|
*/
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// log it to the display!
|
|
|
|
|
displayTextForFreq(ad.text, ad.offset, ad.utcTimestamp, false, true,
|
|
|
|
|
false);
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
// and send it to the network in case we want to interact with it
|
|
|
|
|
from an external app... if(canSendNetworkMessage()){
|
|
|
|
|
sendNetworkMessage("RX.DIRECTED.ME", ad.text, {
|
|
|
|
|
{"_ID", QVariant(-1)},
|
|
|
|
|
{"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.dial+ad.offset)},
|
|
|
|
|
{"DIAL", QVariant(ad.dial)},
|
|
|
|
|
{"OFFSET", QVariant(ad.offset)},
|
|
|
|
|
{"SNR", QVariant(ad.snr)},
|
|
|
|
|
{"SPEED", QVariant(ad.submode)},
|
|
|
|
|
{"TDRIFT", QVariant(ad.tdrift)},
|
|
|
|
|
{"UTC", QVariant(ad.utcTimestamp.toMSecsSinceEpoch())}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
if (!isAllCall) {
|
|
|
|
|
// If we've received a message to be displayed,
|
|
|
|
|
// we should no longer call CQ
|
|
|
|
|
if (m_cq_loop->isActive()) {
|
|
|
|
|
qCDebug(mainwindow_js8)
|
|
|
|
|
<< "Canceling CQ loop to prioritize incoming messages, "
|
|
|
|
|
"case II";
|
|
|
|
|
m_cq_loop->onLoopCancel();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// notification for directed message
|
|
|
|
|
tryNotify("directed");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// we're only responding to callsigns in our whitelist if we have one
|
|
|
|
|
// defined... make sure the whitelist is empty (no restrictions) or the
|
|
|
|
|
// from callsign or its base callsign is on it
|
|
|
|
|
auto whitelist = m_config.auto_whitelist();
|
|
|
|
|
if (!whitelist.isEmpty() &&
|
|
|
|
|
!(whitelist.contains(d.from) ||
|
|
|
|
|
whitelist.contains(Radio::base_callsign(d.from)))) {
|
|
|
|
|
qCDebug(mainwindow_js8)
|
|
|
|
|
<< "skipping command for whitelist" << d.from;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// we'll never reply to a blacklisted callsign or base callsign
|
|
|
|
|
auto blacklist = m_config.auto_blacklist();
|
|
|
|
|
if (!blacklist.isEmpty() &&
|
|
|
|
|
(blacklist.contains(d.from) ||
|
|
|
|
|
blacklist.contains(Radio::base_callsign(d.from)))) {
|
|
|
|
|
qCDebug(mainwindow_js8)
|
|
|
|
|
<< "skipping command for blacklist" << d.from;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 < 15) {
|
|
|
|
|
qCDebug(mainwindow_js8)
|
|
|
|
|
<< "skipping command for allcall timeout" << d.from;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// don't actually process any automatic message replies while in idle
|
|
|
|
|
if (m_tx_watchdog) {
|
|
|
|
|
qCDebug(mainwindow_js8)
|
|
|
|
|
<< "skipping command for idle timeout" << d.from;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// HACK: if this is an autoreply cmd and relay path is populated and cmd
|
|
|
|
|
// is not MSG or MSG TO:, then swap out the relay path
|
|
|
|
|
if (Varicode::isCommandAutoreply(d.cmd) && !d.relayPath.isEmpty() &&
|
|
|
|
|
!d.cmd.startsWith(" MSG") && !d.cmd.startsWith(" QUERY")) {
|
|
|
|
|
d.from = d.relayPath;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// construct a reply, if needed
|
|
|
|
|
QString reply;
|
|
|
|
|
int priority = PriorityNormal;
|
|
|
|
|
int freq = -1;
|
|
|
|
|
|
|
|
|
|
// QUERIED SNR
|
|
|
|
|
// Only reply to a SNR request if there is no incoming MSG's and is not
|
|
|
|
|
// directed to AllCall
|
|
|
|
|
if (d.cmd == " SNR?" && !isAllCall && m_messageBuffer.isEmpty()) {
|
|
|
|
|
reply = QString("%1 SNR %2")
|
|
|
|
|
.arg(d.from)
|
|
|
|
|
.arg(Varicode::formatSNR(d.snr));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// QUERIED INFO
|
|
|
|
|
// Only reply to a INFO request if there is no incoming MSG's and is not
|
|
|
|
|
// directed to AllCall
|
|
|
|
|
else if (d.cmd == " INFO?" && !isAllCall && m_messageBuffer.isEmpty()) {
|
|
|
|
|
QString info = m_config.my_info();
|
|
|
|
|
if (info.isEmpty()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
reply = QString("%1 INFO %2")
|
|
|
|
|
.arg(d.from)
|
|
|
|
|
.arg(replaceMacros(info, buildMacroValues(), true));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// QUERIED ACTIVE
|
|
|
|
|
// Only reply to a STATUS request if there is no incoming MSG's and is
|
|
|
|
|
// not directed to AllCall
|
|
|
|
|
else if (d.cmd == " STATUS?" && !isAllCall &&
|
|
|
|
|
m_messageBuffer.isEmpty()) {
|
|
|
|
|
QString status = m_config.my_status();
|
|
|
|
|
if (status.isEmpty()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
reply = QString("%1 STATUS %2")
|
|
|
|
|
.arg(d.from)
|
|
|
|
|
.arg(replaceMacros(status, buildMacroValues(), true));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// QUERIED GRID
|
|
|
|
|
// Only reply to a GRID request if there is no incoming MSG's and is not
|
|
|
|
|
// directed to AllCall
|
|
|
|
|
else if (d.cmd == " GRID?" && !isAllCall && m_messageBuffer.isEmpty()) {
|
|
|
|
|
QString grid = m_config.my_grid();
|
|
|
|
|
if (grid.isEmpty()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
reply = QString("%1 GRID %2").arg(d.from).arg(grid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// QUERIED STATIONS HEARD
|
|
|
|
|
// Only reply to a HEARING request if there is no incoming MSG's and is
|
|
|
|
|
// not directed to AllCall
|
|
|
|
|
else if (d.cmd == " HEARING?" && !isAllCall &&
|
|
|
|
|
m_messageBuffer.isEmpty()) {
|
|
|
|
|
auto calls = m_callActivity.keys();
|
|
|
|
|
|
|
|
|
|
std::stable_sort(calls.begin(), calls.end(),
|
|
|
|
|
[this](QString const &lhs, QString const &rhs) {
|
|
|
|
|
return m_callActivity[rhs].utcTimestamp <
|
|
|
|
|
m_callActivity[lhs].utcTimestamp;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
auto const callsignAging = m_config.callsign_aging();
|
|
|
|
|
auto const maxStations = 4;
|
|
|
|
|
auto i = 0;
|
|
|
|
|
QStringList lines;
|
|
|
|
|
|
|
|
|
|
for (auto const &call : calls) {
|
|
|
|
|
if (i >= maxStations)
|
|
|
|
|
break;
|
|
|
|
|
if (call == d.from)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
auto const &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 && !m_config.relay_off()) {
|
|
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
|
// and this is not a group callsign, then relay.
|
|
|
|
|
if (match.hasMatch() && !isGroupCall) {
|
|
|
|
|
// 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")) {
|
|
|
|
|
|
|
|
|
|
// parse out the callsign path
|
|
|
|
|
auto calls = parseRelayPathCallsigns(d.from, d.text);
|
|
|
|
|
|
|
|
|
|
// put these third party calls in the heard list
|
|
|
|
|
foreach (auto call, calls) {
|
|
|
|
|
CallDetail cd = {};
|
|
|
|
|
cd.call = call;
|
|
|
|
|
cd.snr = -64;
|
|
|
|
|
cd.dial = d.dial;
|
|
|
|
|
cd.offset = d.offset;
|
|
|
|
|
cd.through = d.from;
|
|
|
|
|
cd.utcTimestamp = DriftingDateTime::currentDateTimeUtc();
|
|
|
|
|
cd.tdrift = d.tdrift;
|
|
|
|
|
cd.submode = d.submode;
|
|
|
|
|
logCallActivity(cd, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
d.relayPath = calls.join('>');
|
|
|
|
|
|
|
|
|
|
reply = QString("%1 ACK").arg(d.relayPath);
|
|
|
|
|
|
|
|
|
|
// check to see if the relay text contains a command that should
|
|
|
|
|
// be replied to instead of an ack.
|
|
|
|
|
QStringList relayedCmds = d.text.split(" ");
|
|
|
|
|
if (!relayedCmds.isEmpty()) {
|
|
|
|
|
auto first = relayedCmds.first();
|
|
|
|
|
|
|
|
|
|
auto valid = Varicode::isCommandAllowed(first);
|
|
|
|
|
if (!valid) {
|
|
|
|
|
first = " " + first;
|
|
|
|
|
valid = Varicode::isCommandAllowed(first);
|
|
|
|
|
if (valid) {
|
|
|
|
|
relayedCmds.removeFirst();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// HACK: "MSG TO:" should be supported but contains a space
|
|
|
|
|
// :(
|
|
|
|
|
if (!relayedCmds.isEmpty()) {
|
|
|
|
|
if (first == " MSG") {
|
|
|
|
|
auto second = relayedCmds.first();
|
|
|
|
|
if (second == "TO:") {
|
|
|
|
|
first = " MSG TO:";
|
|
|
|
|
relayedCmds.removeFirst();
|
|
|
|
|
} else if (second.startsWith("TO:")) {
|
|
|
|
|
first = " MSG TO:";
|
|
|
|
|
relayedCmds.replace(0, second.mid(3));
|
|
|
|
|
}
|
|
|
|
|
} else if (first == " QUERY") {
|
|
|
|
|
auto second = relayedCmds.first();
|
|
|
|
|
if (second == "MSGS" || second == "MSGS?") {
|
|
|
|
|
first = " QUERY MSGS";
|
|
|
|
|
relayedCmds.removeFirst();
|
|
|
|
|
} else if (second == "CALL") {
|
|
|
|
|
first = " QUERY CALL";
|
|
|
|
|
relayedCmds.removeFirst();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Varicode::isCommandAllowed(first) &&
|
|
|
|
|
Varicode::isCommandAutoreply(first)) {
|
|
|
|
|
CommandDetail rd = {};
|
|
|
|
|
rd.bits = d.bits;
|
|
|
|
|
rd.cmd = first;
|
|
|
|
|
rd.dial = d.dial;
|
|
|
|
|
rd.offset = d.offset;
|
|
|
|
|
rd.from =
|
|
|
|
|
d.from; // note, MSG and QUERY commands are not set
|
|
|
|
|
// with from as the relay path.
|
|
|
|
|
rd.relayPath = d.relayPath;
|
|
|
|
|
rd.text = relayedCmds.join(" "); // d.text;
|
|
|
|
|
rd.to = d.to;
|
|
|
|
|
rd.utcTimestamp = d.utcTimestamp;
|
|
|
|
|
|
|
|
|
|
m_rxCommandQueue.insert(0, rd);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#if STORE_RELAY_MSGS_TO_INBOX
|
|
|
|
|
// if we make it here, this is a message
|
|
|
|
|
addCommandToMyInbox(d);
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// PROCESS MESSAGE STORAGE
|
2026-01-14 10:32:00 -08:00
|
|
|
else if (d.cmd == " MSG TO:" && !isAllCall) {
|
|
|
|
|
|
2026-01-04 12:54:34 -06:00
|
|
|
// store message
|
|
|
|
|
QStringList segs = d.text.split(" ");
|
|
|
|
|
if (segs.isEmpty()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-01-14 10:32:00 -08:00
|
|
|
|
2026-01-04 12:54:34 -06:00
|
|
|
auto to = segs.first();
|
|
|
|
|
segs.removeFirst();
|
2026-01-14 10:32:00 -08:00
|
|
|
|
2026-01-04 12:54:34 -06:00
|
|
|
auto text = segs.join(" ").trimmed();
|
2026-01-14 10:32:00 -08:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* APRS relay: inbound `MSG TO:` stores for our local station.
|
|
|
|
|
* Dedupe within 60s using TO + TEXT + DE <SENDER>.
|
|
|
|
|
*/
|
|
|
|
|
if (d.to == "@APRSIS") {
|
|
|
|
|
if (to != m_config.my_callsign().trimmed()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static QRegularExpression aprsDeRe("\\s+DE\\s+(\\S+)\\s*$");
|
|
|
|
|
auto aprsSender = QString("APRS");
|
|
|
|
|
auto deMatch = aprsDeRe.match(text);
|
|
|
|
|
if (deMatch.hasMatch()) {
|
|
|
|
|
aprsSender = deMatch.captured(1);
|
|
|
|
|
}
|
|
|
|
|
auto dedupeKey = QString("%1|%2|%3")
|
|
|
|
|
.arg(to, text, aprsSender)
|
|
|
|
|
.toUpper();
|
|
|
|
|
auto now = DriftingDateTime::currentDateTimeUtc();
|
|
|
|
|
auto cutoff = now.addSecs(-60);
|
|
|
|
|
auto it = m_aprsRelayDedupCache.begin();
|
|
|
|
|
while (it != m_aprsRelayDedupCache.end()) {
|
|
|
|
|
if (it.value() < cutoff) {
|
|
|
|
|
it = m_aprsRelayDedupCache.erase(it);
|
|
|
|
|
} else {
|
|
|
|
|
++it;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (m_aprsRelayDedupCache.contains(dedupeKey)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CommandDetail cd = {};
|
|
|
|
|
cd.bits = d.bits;
|
|
|
|
|
cd.cmd = " MSG ";
|
|
|
|
|
cd.extra = d.extra;
|
|
|
|
|
cd.dial = d.dial;
|
|
|
|
|
cd.offset = d.offset;
|
|
|
|
|
cd.from = "APRS";
|
|
|
|
|
cd.relayPath = "APRS";
|
|
|
|
|
cd.grid = d.grid;
|
|
|
|
|
cd.snr = d.snr;
|
|
|
|
|
cd.tdrift = d.tdrift;
|
|
|
|
|
cd.text = text;
|
|
|
|
|
cd.to = to;
|
|
|
|
|
cd.utcTimestamp = d.utcTimestamp;
|
|
|
|
|
cd.submode = d.submode;
|
|
|
|
|
|
|
|
|
|
addCommandToMyInbox(cd);
|
2026-01-04 12:54:34 -06:00
|
|
|
|
2026-01-14 10:32:00 -08:00
|
|
|
m_aprsRelayDedupCache.insert(dedupeKey, now);
|
|
|
|
|
tryNotify("inbox");
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (m_config.relay_off()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-04 12:54:34 -06:00
|
|
|
auto calls = parseRelayPathCallsigns(d.from, text);
|
|
|
|
|
d.relayPath = calls.join(">");
|
2026-01-14 10:32:00 -08:00
|
|
|
|
2026-01-04 12:54:34 -06:00
|
|
|
CommandDetail cd = {};
|
|
|
|
|
cd.bits = d.bits;
|
|
|
|
|
cd.cmd = d.cmd;
|
|
|
|
|
cd.extra = d.extra;
|
|
|
|
|
cd.dial = d.dial;
|
|
|
|
|
cd.offset = d.offset;
|
|
|
|
|
cd.from = d.from;
|
|
|
|
|
cd.grid = d.grid;
|
|
|
|
|
cd.relayPath = d.relayPath;
|
|
|
|
|
cd.snr = d.snr;
|
|
|
|
|
cd.tdrift = d.tdrift;
|
|
|
|
|
cd.text = text;
|
|
|
|
|
cd.to = Radio::base_callsign(to);
|
|
|
|
|
cd.utcTimestamp = d.utcTimestamp;
|
|
|
|
|
cd.submode = d.submode;
|
2026-01-14 10:32:00 -08:00
|
|
|
|
2026-01-04 12:54:34 -06:00
|
|
|
qCDebug(mainwindow_js8)
|
|
|
|
|
<< "storing message to" << to << ":" << text;
|
2026-01-14 10:32:00 -08:00
|
|
|
|
2026-01-04 12:54:34 -06:00
|
|
|
addCommandToStorage("STORE", cd);
|
2026-01-14 10:32:00 -08:00
|
|
|
|
2026-01-04 12:54:34 -06:00
|
|
|
// we haven't replaced the from with the relay path, so we have to
|
|
|
|
|
// use it for the ack if there is one
|
|
|
|
|
reply = QString("%1 ACK").arg(calls.length() > 1 ? d.relayPath
|
|
|
|
|
: d.from);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-14 10:32:00 -08:00
|
|
|
|
2026-01-04 12:54:34 -06:00
|
|
|
// PROCESS AGN
|
|
|
|
|
else if (d.cmd == " AGN?" && !isAllCall && !isGroupCall &&
|
|
|
|
|
!m_lastTxMessage.isEmpty()) {
|
|
|
|
|
reply = Varicode::rstrip(m_lastTxMessage);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// PROCESS ACTIVE HEARTBEAT
|
|
|
|
|
// if we have hb mode enabled and auto reply enabled <del>and auto ack
|
|
|
|
|
// enabled and no callsign is selected</del> update: if we're in HB
|
|
|
|
|
// mode, doesn't matter if a callsign is selected.
|
|
|
|
|
else if ((d.cmd == " HB" || d.cmd == " HEARTBEAT") &&
|
|
|
|
|
canCurrentModeSendHeartbeat() &&
|
|
|
|
|
ui->actionModeJS8HB->isChecked() &&
|
|
|
|
|
ui->actionModeAutoreply->isChecked() &&
|
|
|
|
|
ui->actionHeartbeatAcknowledgements->isChecked()) {
|
|
|
|
|
// do not process HB activity if buffer is not empty, this prevents
|
|
|
|
|
// broken incoming MSG's
|
|
|
|
|
if (!m_messageBuffer.isEmpty()) {
|
|
|
|
|
qCDebug(mainwindow_js8) << "hb paused for messageBuffer";
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// check to make sure we aren't pausing HB transmissions (ACKs)
|
|
|
|
|
// while a callsign is selected
|
|
|
|
|
if (m_config.heartbeat_qso_pause() && !selectedCallsign.isEmpty()) {
|
|
|
|
|
qCDebug(mainwindow_js8) << "hb paused during qso";
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// check to make sure this callsign isn't blacklisted
|
|
|
|
|
if (m_config.hb_blacklist().contains(d.from) ||
|
|
|
|
|
m_config.hb_blacklist().contains(
|
|
|
|
|
Radio::base_callsign(d.from))) {
|
|
|
|
|
qCDebug(mainwindow_js8) << "hb blacklist blocking" << d.from;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// check to see if we have a message for a station who is
|
|
|
|
|
// heartbeating
|
|
|
|
|
QString extra;
|
|
|
|
|
auto mid = getNextMessageIdForCallsign(d.from);
|
|
|
|
|
if (mid != -1) {
|
|
|
|
|
extra = QString("MSG ID %1").arg(mid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// group messaging - if isGroupCall, check to see if there's a
|
|
|
|
|
// message id for the group and return it if there's not an
|
|
|
|
|
// individual message
|
|
|
|
|
// TODO: include group name in response to differentiate from direct
|
|
|
|
|
// messages?
|
|
|
|
|
else if (isGroupCall) {
|
|
|
|
|
mid = getNextGroupMessageIdForCallsign(d.to, d.from);
|
|
|
|
|
if (mid != -1) {
|
|
|
|
|
extra = QString("MSG ID %1").arg(mid);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO: require confirmation?
|
|
|
|
|
sendHeartbeatAck(d.from, d.snr, extra);
|
|
|
|
|
|
|
|
|
|
if (isAllCall) {
|
|
|
|
|
// since all pings are technically @ALLCALL, let's bump the
|
|
|
|
|
// allcall cache here...
|
|
|
|
|
m_txAllcallCommandCache.insert(d.from, new QDateTime(now), 5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// PROCESS HEARTBEAT SNR
|
|
|
|
|
else if (d.cmd == " HEARTBEAT SNR") {
|
|
|
|
|
qCDebug(mainwindow_js8) << "skipping incoming hb snr" << d.text;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// PROCESS CQ
|
|
|
|
|
else if (d.cmd == " CQ") {
|
|
|
|
|
qCDebug(mainwindow_js8) << "skipping incoming cq" << d.text;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// PROCESS MSG
|
|
|
|
|
else if (d.cmd == " MSG" && !isAllCall) {
|
2026-01-14 10:32:00 -08:00
|
|
|
|
2026-01-04 12:54:34 -06:00
|
|
|
auto text = d.text;
|
2026-01-14 10:32:00 -08:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* APRS relay: inbound `MSG` where the payload starts with
|
|
|
|
|
* `TO:<DEST>` and includes `DE <SENDER>`.
|
|
|
|
|
* Dedupe within 60s using TO + TEXT + DE <SENDER>.
|
|
|
|
|
*/
|
|
|
|
|
if (d.to == "@APRSIS") {
|
|
|
|
|
static QRegularExpression aprsToRe(
|
|
|
|
|
"^TO:\\s*(\\S+)\\s+(.*)$");
|
|
|
|
|
auto match = aprsToRe.match(text);
|
|
|
|
|
if (match.hasMatch()) {
|
|
|
|
|
auto dest = match.captured(1);
|
|
|
|
|
auto aprsText = match.captured(2).trimmed();
|
|
|
|
|
if (dest == m_config.my_callsign().trimmed()) {
|
|
|
|
|
static QRegularExpression aprsDeRe(
|
|
|
|
|
"\\s+DE\\s+(\\S+)\\s*$");
|
|
|
|
|
auto aprsSender = QString("APRS");
|
|
|
|
|
auto deMatch = aprsDeRe.match(aprsText);
|
|
|
|
|
if (deMatch.hasMatch()) {
|
|
|
|
|
aprsSender = deMatch.captured(1);
|
|
|
|
|
}
|
|
|
|
|
auto dedupeKey = QString("%1|%2|%3")
|
|
|
|
|
.arg(dest, aprsText, aprsSender)
|
|
|
|
|
.toUpper();
|
|
|
|
|
auto now = DriftingDateTime::currentDateTimeUtc();
|
|
|
|
|
auto cutoff = now.addSecs(-60);
|
|
|
|
|
auto it = m_aprsRelayDedupCache.begin();
|
|
|
|
|
while (it != m_aprsRelayDedupCache.end()) {
|
|
|
|
|
if (it.value() < cutoff) {
|
|
|
|
|
it = m_aprsRelayDedupCache.erase(it);
|
|
|
|
|
} else {
|
|
|
|
|
++it;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (m_aprsRelayDedupCache.contains(dedupeKey)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CommandDetail cd = d;
|
|
|
|
|
cd.cmd = " MSG ";
|
|
|
|
|
cd.from = "APRS";
|
|
|
|
|
cd.relayPath = "APRS";
|
|
|
|
|
cd.to = dest;
|
|
|
|
|
cd.text = aprsText;
|
|
|
|
|
|
|
|
|
|
addCommandToMyInbox(cd);
|
|
|
|
|
|
|
|
|
|
m_aprsRelayDedupCache.insert(dedupeKey, now);
|
|
|
|
|
tryNotify("inbox");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-04 12:54:34 -06:00
|
|
|
qCDebug(mainwindow_js8) << "adding message to inbox" << text;
|
2026-01-14 10:32:00 -08:00
|
|
|
|
2026-01-04 12:54:34 -06:00
|
|
|
auto calls = parseRelayPathCallsigns(d.from, text);
|
2026-01-14 10:32:00 -08:00
|
|
|
|
2026-01-04 12:54:34 -06:00
|
|
|
d.cmd = " MSG ";
|
|
|
|
|
d.relayPath = calls.join(">");
|
|
|
|
|
d.text = text;
|
2026-01-14 10:32:00 -08:00
|
|
|
|
2026-01-04 12:54:34 -06:00
|
|
|
addCommandToMyInbox(d);
|
2026-01-14 10:32:00 -08:00
|
|
|
|
2026-01-04 12:54:34 -06:00
|
|
|
// notification
|
|
|
|
|
tryNotify("inbox");
|
2026-01-14 10:32:00 -08:00
|
|
|
|
2026-01-04 12:54:34 -06:00
|
|
|
// we haven't replaced the from with the relay path, so we have to
|
|
|
|
|
// use it for the ack if there is one
|
|
|
|
|
reply = QString("%1 ACK").arg(calls.length() > 1 ? d.relayPath
|
|
|
|
|
: d.from);
|
2026-01-14 10:32:00 -08:00
|
|
|
|
2026-01-04 12:54:34 -06:00
|
|
|
#define SHOW_ALERT_FOR_MSG 1
|
|
|
|
|
#if SHOW_ALERT_FOR_MSG
|
|
|
|
|
SelfDestructMessageBox *m = new SelfDestructMessageBox(
|
|
|
|
|
300, "New Message Received",
|
|
|
|
|
QString("A new message was received at %1 UTC from %2")
|
|
|
|
|
.arg(d.utcTimestamp.time().toString())
|
|
|
|
|
.arg(d.from),
|
|
|
|
|
QMessageBox::Information, QMessageBox::Ok, QMessageBox::Ok,
|
|
|
|
|
false, this);
|
2026-01-14 10:32:00 -08:00
|
|
|
|
2026-01-04 12:54:34 -06:00
|
|
|
m->show();
|
|
|
|
|
#endif
|
|
|
|
|
}
|
2026-01-14 10:32:00 -08:00
|
|
|
|
2026-01-04 12:54:34 -06:00
|
|
|
// PROCESS ACKS
|
|
|
|
|
else if (d.cmd == " ACK" && !isAllCall) {
|
|
|
|
|
qCDebug(mainwindow_js8) << "skipping incoming ack" << d.text;
|
|
|
|
|
|
|
|
|
|
// notification for ack
|
|
|
|
|
tryNotify("ack");
|
|
|
|
|
|
|
|
|
|
// make sure this is explicit
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// PROCESS BUFFERED CMD
|
|
|
|
|
else if (d.cmd == " CMD" && !isAllCall) {
|
|
|
|
|
qCDebug(mainwindow_js8) << "skipping incoming command" << d.text;
|
|
|
|
|
|
|
|
|
|
// make sure this is explicit
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// PROCESS BUFFERED QUERY
|
|
|
|
|
else if (d.cmd == " QUERY" && !isAllCall) {
|
|
|
|
|
auto who = d.from; // keep in mind, this is the sender, not the
|
|
|
|
|
// original requestor if relayed
|
|
|
|
|
auto replyPath = d.from;
|
|
|
|
|
|
|
|
|
|
if (d.relayPath.contains(">")) {
|
|
|
|
|
auto path = d.relayPath.split(">");
|
|
|
|
|
who = path.last();
|
|
|
|
|
replyPath = d.relayPath;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QStringList segs = d.text.split(" ");
|
|
|
|
|
if (segs.isEmpty()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto cmd = segs.first();
|
|
|
|
|
segs.removeFirst();
|
|
|
|
|
|
|
|
|
|
if (cmd == "MSG" && !segs.isEmpty()) {
|
|
|
|
|
auto inbox = Inbox(inboxPath());
|
|
|
|
|
if (!inbox.open()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool ok = false;
|
|
|
|
|
int mid = QString(segs.first()).toInt(&ok);
|
|
|
|
|
if (!ok) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto msg = inbox.value(mid);
|
|
|
|
|
auto params = msg.params();
|
|
|
|
|
if (params.isEmpty()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto from = params.value("FROM").toString().trimmed();
|
|
|
|
|
|
|
|
|
|
auto to = params.value("TO").toString().trimmed();
|
|
|
|
|
|
|
|
|
|
// group messaging - allow any message to a @GROUP to be
|
|
|
|
|
// retrieved by anybody
|
|
|
|
|
bool isGroupMsg = to.startsWith("@");
|
|
|
|
|
|
|
|
|
|
if (!isGroupMsg && to != who &&
|
|
|
|
|
to != Radio::base_callsign(who)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto text = params.value("TEXT").toString().trimmed();
|
|
|
|
|
if (text.isEmpty()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* mark as delivered (so subsequent HBs and QUERY MSGS don't
|
|
|
|
|
* receive this message)
|
|
|
|
|
*
|
|
|
|
|
* Do this in callbacks so that messages are only marked read
|
|
|
|
|
* after any potential confirmations have been made by the user,
|
|
|
|
|
* and the message has been processed in the transaction queue.
|
|
|
|
|
*/
|
|
|
|
|
if (!isGroupMsg) {
|
|
|
|
|
callback = [this, mid, msg]() {
|
|
|
|
|
this->markMsgDelivered(mid, msg);
|
|
|
|
|
};
|
|
|
|
|
} else {
|
|
|
|
|
callback = [this, mid, who]() {
|
|
|
|
|
this->markGroupMsgDeliveredForCallsign(mid, who);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto lookaheadMid = getLookaheadMessageIdForCallsign(who, mid);
|
|
|
|
|
if (lookaheadMid == -1 && isGroupMsg) {
|
|
|
|
|
lookaheadMid =
|
|
|
|
|
getLookaheadGroupMessageIdForCallsign(d.to, who, mid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// and reply
|
|
|
|
|
if (lookaheadMid != -1) {
|
|
|
|
|
reply = QString("%1 MSG %2 FROM %3 NEXT MSG ID %4");
|
|
|
|
|
reply = reply.arg(replyPath);
|
|
|
|
|
reply = reply.arg(text);
|
|
|
|
|
reply = reply.arg(from);
|
|
|
|
|
reply = reply.arg(lookaheadMid);
|
|
|
|
|
} else {
|
|
|
|
|
reply = QString("%1 MSG %2 FROM %3");
|
|
|
|
|
reply = reply.arg(replyPath);
|
|
|
|
|
reply = reply.arg(text);
|
|
|
|
|
reply = reply.arg(from);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// PROCESS BUFFERED QUERY MSGS
|
|
|
|
|
// Do not process this request if there is incoming MSG as it breaks the
|
|
|
|
|
// incoming
|
|
|
|
|
else if (d.cmd == " QUERY MSGS" &&
|
|
|
|
|
ui->actionModeAutoreply->isChecked() &&
|
|
|
|
|
m_messageBuffer.isEmpty()) {
|
|
|
|
|
auto who = d.from; // keep in mind, this is the sender, not the
|
|
|
|
|
// original requestor if relayed
|
|
|
|
|
auto replyPath = d.from;
|
|
|
|
|
|
|
|
|
|
if (d.relayPath.contains(">")) {
|
|
|
|
|
auto path = d.relayPath.split(">");
|
|
|
|
|
who = path.last();
|
|
|
|
|
replyPath = d.relayPath;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// if this is an allcall or a directed call, check to see if we have
|
|
|
|
|
// a stored message for user. we reply yes if the user would be able
|
|
|
|
|
// to retreive a stored message
|
|
|
|
|
auto mid = getNextMessageIdForCallsign(who);
|
|
|
|
|
if (mid != -1) {
|
|
|
|
|
reply = QString("%1 YES MSG ID %2").arg(replyPath).arg(mid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Group messaging - if isGroupCall, check to see if there's a
|
|
|
|
|
// message id for the group and return it if there's not an
|
|
|
|
|
// individual message
|
|
|
|
|
// TODO: include group name in response to differentiate from direct
|
|
|
|
|
// messages?
|
|
|
|
|
else if (isGroupCall) {
|
|
|
|
|
mid = getNextGroupMessageIdForCallsign(d.to, d.from);
|
|
|
|
|
if (mid != -1) {
|
|
|
|
|
reply = QString("%1 YES MSG ID %2").arg(replyPath).arg(mid);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// if this is not an allcall and we have no messages, reply no.
|
|
|
|
|
if (!isAllCall && reply.isEmpty()) {
|
|
|
|
|
reply = QString("%1 NO").arg(replyPath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// PROCESS BUFFERED QUERY CALL
|
|
|
|
|
// Do not process this request if there is incoming MSG as it breaks the
|
|
|
|
|
// incoming
|
|
|
|
|
else if (d.cmd == " QUERY CALL" &&
|
|
|
|
|
ui->actionModeAutoreply->isChecked() &&
|
|
|
|
|
m_messageBuffer.isEmpty()) {
|
|
|
|
|
auto replyPath = d.from;
|
|
|
|
|
if (d.relayPath.contains(">")) {
|
|
|
|
|
replyPath = d.relayPath;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 (%2)")
|
|
|
|
|
.arg(Varicode::formatSNR(cd.snr))
|
|
|
|
|
.arg(since(cd.utcTimestamp))
|
|
|
|
|
.trimmed();
|
|
|
|
|
replies.append(r);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!replies.isEmpty()) {
|
|
|
|
|
replies.prepend(QString("%1 YES").arg(replyPath));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
reply = replies.join(" ");
|
|
|
|
|
|
|
|
|
|
if (!reply.isEmpty()) {
|
|
|
|
|
if (isAllCall) {
|
|
|
|
|
m_txAllcallCommandCache.insert(d.from, new QDateTime(now),
|
|
|
|
|
25);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#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->actionModeAutoreply->isChecked() && isAllCall) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#if 0
|
|
|
|
|
// TODO: jsherer - HB issue here
|
|
|
|
|
// 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")){
|
|
|
|
|
// continue;
|
|
|
|
|
// }
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
// do not queue for reply if there's text in the window
|
|
|
|
|
if (!ui->extFreeTextMsgEdit->toPlainText().isEmpty()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// do not queue for reply if there's a buffer open to us
|
|
|
|
|
int bufferOffset = 0;
|
|
|
|
|
if (hasExistingMessageBufferToMe(&bufferOffset)) {
|
|
|
|
|
qCDebug(mainwindow_js8) << "skipping reply due to open buffer"
|
|
|
|
|
<< bufferOffset << m_messageBuffer.count();
|
|
|
|
|
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.
|
|
|
|
|
if (m_config.autoreply_confirmation()) {
|
|
|
|
|
confirmThenEnqueueMessage(90, priority, reply, freq, callback);
|
|
|
|
|
} else {
|
|
|
|
|
enqueueMessage(priority, reply, freq, callback);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|