js8call/JS8_Main/AprsInboundRelay.cpp

208 lines
6 KiB
C++

/**
* @file AprsInboundRelay.cpp
* @brief Implements inbound APRS-IS relay processing.
*/
/**
* @brief Usage notes for APRS inbound relay.
*
* Create an AprsInboundRelay with callbacks for heard-list lookup, UI notices,
* When function is enabled in settings JS8Call will listen for APRS messages
* via APRS-IS When it detects a message to a station on the heard-list it
* relays it as a message to the station with an @APRSIS MSG TO:<DEST> <TEXT> DE
* <CALLINGSTATION> The destination station puts it in it's inbox.
*/
#include "AprsInboundRelay.h"
#include "JS8_Main/DriftingDateTime.h"
#include "JS8_Main/Radio.h"
#include "JS8_UI/Configuration.h"
#include <QDebug>
#include <QLoggingCategory>
#include <utility>
#include "moc_AprsInboundRelay.cpp"
Q_DECLARE_LOGGING_CATEGORY(mainwindow_js8)
static QString extractMessageId(QString &message) {
auto trimmed = message.trimmed();
auto braceIndex = trimmed.lastIndexOf('{');
if (braceIndex < 0) {
return {};
}
auto suffix = trimmed.mid(braceIndex + 1);
if (suffix.endsWith('}')) {
suffix.chop(1);
}
if (suffix.isEmpty()) {
return {};
}
for (auto const &ch : suffix) {
if (!ch.isLetterOrNumber()) {
return {};
}
}
message = trimmed.left(braceIndex).trimmed();
return suffix;
}
static bool isAckMessage(QString const &message) {
auto trimmed = message.trimmed();
if (trimmed.size() < 4) {
return false;
}
if (!trimmed.startsWith("ack", Qt::CaseInsensitive)) {
return false;
}
auto suffix = trimmed.mid(3);
if (suffix.isEmpty()) {
return false;
}
for (auto const &ch : suffix) {
if (!ch.isLetterOrNumber()) {
return false;
}
}
return true;
}
/**
* @brief Construct a new AprsInboundRelay handler.
* @param config Configuration instance for enable/aging checks.
* @param callLookup Callback to check heard list activity.
* @param noticeFn Callback to post UI notices.
* @param enqueueFn Callback to enqueue relay messages for transmit.
* @param parent QObject parent.
*/
AprsInboundRelay::AprsInboundRelay(Configuration const *config,
CallActivityLookup callLookup,
NoticeFn noticeFn, EnqueueFn enqueueFn,
AckFn ackFn, QObject *parent)
: QObject(parent), m_config(config), m_callLookup(std::move(callLookup)),
m_notice(std::move(noticeFn)), m_enqueue(std::move(enqueueFn)),
m_ack(std::move(ackFn)) {}
/**
* @brief Process an APRS-IS message for relay.
* @param from APRS sender callsign.
* @param to APRS destination callsign.
* @param message APRS message payload (may include checksum).
*/
void AprsInboundRelay::onMessageReceived(QString from, QString to,
QString message, QString messageId) {
qCDebug(mainwindow_js8)
<< "APRS Message Received from" << from << "to" << to << ":" << message
<< "id" << messageId;
// Explicitly log to ensure we see it
qDebug() << "DEBUG: APRS Message Received from" << from << "to" << to << ":"
<< message << "id" << messageId;
if (!m_config || !m_config->spot_to_aprs_relay()) {
qDebug() << "DEBUG: APRS relay disabled";
return;
}
if (m_config->spot_blacklist().contains(to) ||
m_config->spot_blacklist().contains(Radio::base_callsign(to))) {
qDebug() << "DEBUG: Destination in spot blacklist:" << to;
return;
}
CallActivityInfo info;
if (m_callLookup) {
info = m_callLookup(to);
}
// Check if we have heard the destination station
if (!info.heard) {
qDebug() << "DEBUG: Destination not in heard list:" << to;
return;
}
// Check if the station is "active" if aging is enabled
if (m_config->callsign_aging() > 0) {
if (info.lastHeardUtc.secsTo(DriftingDateTime::currentDateTimeUtc()) >
m_config->callsign_aging() * 60) {
qDebug() << "DEBUG: Destination aged out:" << to;
return;
}
}
if (messageId.isEmpty()) {
auto recoveredId = extractMessageId(message);
if (!recoveredId.isEmpty()) {
messageId = recoveredId;
qDebug() << "DEBUG: APRS Message ID recovered" << messageId;
}
} else {
extractMessageId(message);
}
qDebug() << "DEBUG: APRS Message after checksum strip:" << message;
if (isAckMessage(message)) {
return;
}
constexpr int kAckDedupSeconds = 120;
bool isDuplicate = false;
if (!messageId.isEmpty()) {
auto dedupeKey = QString("%1|%2|%3").arg(from, to, messageId).toUpper();
auto now = DriftingDateTime::currentDateTimeUtc();
auto cutoff = now.addSecs(-kAckDedupSeconds);
auto it = m_ackDedupCache.begin();
while (it != m_ackDedupCache.end()) {
if (it.value() < cutoff) {
it = m_ackDedupCache.erase(it);
} else {
++it;
}
}
if (m_ackDedupCache.contains(dedupeKey)) {
isDuplicate = true;
} else {
m_ackDedupCache.insert(dedupeKey, now);
}
if (m_ack) {
m_ack(to, from, messageId);
}
}
if (isDuplicate) {
qCDebug(mainwindow_js8) << "APRS relay dedupe skip" << from << to
<< messageId;
return;
}
// Construct the relay message
// @APRSIS MSG to:<DESTCALL> <MESSAGE> DE <SENDER>
QString relayMsg =
QString("@APRSIS MSG to:%1 %2 DE %3").arg(to).arg(message).arg(from);
qCDebug(mainwindow_js8)
<< "Relaying APRS message from" << from << "to" << to << ":" << message;
if (m_notice) {
m_notice(DriftingDateTime::currentDateTimeUtc(),
QString("APRS-IS Relay: %1 -> %2: %3")
.arg(from)
.arg(to)
.arg(message));
}
if (m_enqueue) {
m_enqueue(relayMsg);
}
}