2026-01-10 21:33:24 -05:00
|
|
|
/**
|
|
|
|
|
* @file APRSISClient.cpp
|
|
|
|
|
* @brief Implementation of APRS-IS client for JS8Call
|
|
|
|
|
*/
|
2018-08-27 21:19:38 -04:00
|
|
|
#include "APRSISClient.h"
|
2026-01-18 12:53:55 -06:00
|
|
|
#include "DriftingDateTime.h"
|
|
|
|
|
#include "Varicode.h"
|
|
|
|
|
|
2025-10-12 03:34:27 +02:00
|
|
|
#include <QLoggingCategory>
|
2024-08-31 09:40:28 -07:00
|
|
|
#include <QRandomGenerator>
|
2018-08-27 21:19:38 -04:00
|
|
|
|
|
|
|
|
#include <cmath>
|
|
|
|
|
|
2025-10-12 03:34:27 +02:00
|
|
|
Q_DECLARE_LOGGING_CATEGORY(aprsisclient_js8)
|
|
|
|
|
|
2018-09-05 14:57:40 -04:00
|
|
|
const int PACKET_TIMEOUT_SECONDS = 300;
|
|
|
|
|
|
2026-01-23 16:17:14 -08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-10 21:33:24 -05:00
|
|
|
/**
|
|
|
|
|
* @brief Construct a new APRSISClient::APRSISClient object
|
2026-01-17 15:24:43 -06:00
|
|
|
*
|
|
|
|
|
* @param host
|
|
|
|
|
* @param port
|
|
|
|
|
* @param parent
|
2026-01-10 21:33:24 -05:00
|
|
|
*/
|
2026-01-01 15:12:24 -06:00
|
|
|
APRSISClient::APRSISClient(QString const host, quint16 const port,
|
|
|
|
|
QObject *parent)
|
2026-01-23 16:17:14 -08:00
|
|
|
: QTcpSocket{parent}, m_timer{this}, m_paused{false},
|
|
|
|
|
m_incomingRelayEnabled{false}, m_isLoggedIn{false}, m_skipPercent{0.0F} {
|
2018-09-21 14:43:02 -04:00
|
|
|
setServer(host, port);
|
|
|
|
|
|
2018-08-27 21:19:38 -04:00
|
|
|
connect(&m_timer, &QTimer::timeout, this, &APRSISClient::sendReports);
|
2024-11-02 16:10:10 -07:00
|
|
|
m_timer.start(std::chrono::minutes(1));
|
2026-01-14 10:32:00 -08:00
|
|
|
|
2026-01-17 15:24:43 -06:00
|
|
|
connect(this, &QTcpSocket::connected, this,
|
|
|
|
|
&APRSISClient::onSocketConnected);
|
|
|
|
|
connect(this, &QTcpSocket::readyRead, this,
|
|
|
|
|
&APRSISClient::onSocketReadyRead);
|
2026-01-14 10:32:00 -08:00
|
|
|
connect(this, &QTcpSocket::disconnected, this,
|
|
|
|
|
&APRSISClient::onSocketDisconnected);
|
2026-01-17 15:24:43 -06:00
|
|
|
connect(this, &QTcpSocket::errorOccurred, this,
|
|
|
|
|
&APRSISClient::onSocketError);
|
2018-08-27 21:19:38 -04:00
|
|
|
}
|
|
|
|
|
|
2026-01-10 21:33:24 -05:00
|
|
|
/**
|
2026-01-15 19:26:06 -08:00
|
|
|
* @brief Compute APRS-IS passcode for a callsign.
|
|
|
|
|
*
|
|
|
|
|
* @param callsign
|
|
|
|
|
* @return quint32
|
2026-01-10 21:33:24 -05:00
|
|
|
*/
|
2026-01-01 15:12:24 -06:00
|
|
|
quint32 APRSISClient::hashCallsign(QString callsign) {
|
2018-08-27 21:19:38 -04:00
|
|
|
// based on: https://github.com/hessu/aprsc/blob/master/src/passcode.c
|
2026-01-01 15:12:24 -06:00
|
|
|
QByteArray rootCall =
|
|
|
|
|
QString(callsign.split("-").first().toUpper()).toLocal8Bit() + '\0';
|
2018-08-27 21:19:38 -04:00
|
|
|
quint32 hash = 0x73E2;
|
|
|
|
|
|
2026-01-01 15:12:24 -06:00
|
|
|
qsizetype i = 0;
|
2025-07-15 14:19:05 -07:00
|
|
|
qsizetype const len = rootCall.length();
|
2018-08-27 21:19:38 -04:00
|
|
|
|
2026-01-01 15:12:24 -06:00
|
|
|
while (i + 1 < len) {
|
2018-08-27 21:19:38 -04:00
|
|
|
hash ^= rootCall.at(i) << 8;
|
2026-01-01 15:12:24 -06:00
|
|
|
hash ^= rootCall.at(i + 1);
|
2018-08-27 21:19:38 -04:00
|
|
|
i += 2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return hash & 0x7FFF;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-10 21:33:24 -05:00
|
|
|
/**
|
2026-01-15 19:26:06 -08:00
|
|
|
* @brief Create a login frame for APRS-IS.
|
|
|
|
|
*
|
|
|
|
|
* @param callsign
|
|
|
|
|
* @param filter
|
|
|
|
|
* @return QString
|
2026-01-10 21:33:24 -05:00
|
|
|
*/
|
2026-01-14 10:32:00 -08:00
|
|
|
QString APRSISClient::loginFrame(QString callsign, QString filter) {
|
|
|
|
|
auto loginFrame = QString("user %1 pass %2 ver %3 %4\n");
|
2018-08-27 21:19:38 -04:00
|
|
|
loginFrame = loginFrame.arg(callsign);
|
|
|
|
|
loginFrame = loginFrame.arg(hashCallsign(callsign));
|
2018-10-04 13:52:52 -04:00
|
|
|
loginFrame = loginFrame.arg("JS8Call");
|
2026-01-14 10:32:00 -08:00
|
|
|
if (!filter.isEmpty()) {
|
|
|
|
|
loginFrame = loginFrame.arg(filter);
|
|
|
|
|
} else {
|
|
|
|
|
loginFrame = loginFrame.arg("");
|
|
|
|
|
}
|
2018-08-27 21:19:38 -04:00
|
|
|
return loginFrame;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-10 21:33:24 -05:00
|
|
|
/**
|
|
|
|
|
* @brief Find all matches of a regular expression in a string
|
2026-01-17 15:24:43 -06:00
|
|
|
*
|
|
|
|
|
* @param re
|
|
|
|
|
* @param content
|
|
|
|
|
* @return QList<QStringList>
|
2026-01-10 21:33:24 -05:00
|
|
|
*/
|
2026-01-01 15:12:24 -06:00
|
|
|
QList<QStringList> findall(QRegularExpression re, QString content) {
|
|
|
|
|
qsizetype pos = 0;
|
2018-08-27 21:19:38 -04:00
|
|
|
QList<QStringList> all;
|
2025-10-14 13:12:57 +02:00
|
|
|
|
2026-01-01 15:12:24 -06:00
|
|
|
while (pos < content.length()) {
|
2018-08-27 21:19:38 -04:00
|
|
|
auto match = re.match(content, pos);
|
2026-01-01 15:12:24 -06:00
|
|
|
if (!match.hasMatch()) {
|
2018-08-27 21:19:38 -04:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
all.append(match.capturedTexts());
|
|
|
|
|
pos = match.capturedEnd();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return all;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-10 21:33:24 -05:00
|
|
|
/**
|
|
|
|
|
* @brief Floor division for long integers
|
2026-01-17 15:24:43 -06:00
|
|
|
*
|
|
|
|
|
* @param num
|
|
|
|
|
* @param den
|
|
|
|
|
* @return long
|
2026-01-10 21:33:24 -05:00
|
|
|
*/
|
2026-01-01 15:12:24 -06:00
|
|
|
inline long floordiv(long num, long den) {
|
|
|
|
|
if (0 < (num ^ den))
|
|
|
|
|
return num / den;
|
|
|
|
|
else {
|
|
|
|
|
ldiv_t res = ldiv(num, den);
|
|
|
|
|
return (res.rem) ? res.quot - 1 : res.quot;
|
2018-08-27 21:19:38 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// convert an arbitrary length grid locator to a high precision lat/lon
|
2026-01-10 21:33:24 -05:00
|
|
|
/**
|
|
|
|
|
* @brief Convert grid locator to degrees
|
2026-01-17 15:24:43 -06:00
|
|
|
*
|
|
|
|
|
* @param locator
|
|
|
|
|
* @return QPair<float, float>
|
2026-01-10 21:33:24 -05:00
|
|
|
*/
|
2026-01-01 15:12:24 -06:00
|
|
|
QPair<float, float> APRSISClient::grid2deg(QString locator) {
|
2018-08-27 21:19:38 -04:00
|
|
|
QString grid = locator.toUpper();
|
|
|
|
|
|
|
|
|
|
float lat = -90;
|
|
|
|
|
float lon = -90;
|
|
|
|
|
|
|
|
|
|
auto lats = findall(QRegularExpression("([A-X])([A-X])"), grid);
|
|
|
|
|
auto lons = findall(QRegularExpression("(\\d)(\\d)"), grid);
|
|
|
|
|
|
|
|
|
|
int valx[22];
|
|
|
|
|
int valy[22];
|
|
|
|
|
|
|
|
|
|
int i = 0;
|
|
|
|
|
int tot = 0;
|
|
|
|
|
char A = 'A';
|
2026-01-01 15:12:24 -06:00
|
|
|
foreach (QStringList matched, lats) {
|
2018-08-27 21:19:38 -04:00
|
|
|
char x = matched.at(1).at(0).toLatin1();
|
|
|
|
|
char y = matched.at(2).at(0).toLatin1();
|
2026-04-25 22:18:33 -05:00
|
|
|
if (i * 2 + 1 >= 22) break;
|
2026-01-01 15:12:24 -06:00
|
|
|
valx[i * 2] = x - A;
|
|
|
|
|
valy[i * 2] = y - A;
|
2018-08-27 21:19:38 -04:00
|
|
|
i++;
|
|
|
|
|
tot++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
i = 0;
|
2026-01-01 15:12:24 -06:00
|
|
|
foreach (QStringList matched, lons) {
|
2018-08-27 21:19:38 -04:00
|
|
|
int x = matched.at(1).toInt();
|
|
|
|
|
int y = matched.at(2).toInt();
|
2026-04-25 22:18:33 -05:00
|
|
|
if (i * 2 + 1 >= 22) break;
|
2026-01-01 15:12:24 -06:00
|
|
|
valx[i * 2 + 1] = x;
|
|
|
|
|
valy[i * 2 + 1] = y;
|
2018-08-27 21:19:38 -04:00
|
|
|
i++;
|
|
|
|
|
tot++;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-01 15:12:24 -06:00
|
|
|
for (int i = 0; i < tot; i++) {
|
2018-08-27 21:19:38 -04:00
|
|
|
int x = valx[i];
|
|
|
|
|
int y = valy[i];
|
|
|
|
|
|
|
|
|
|
int z = i - 1;
|
2026-01-01 15:12:24 -06:00
|
|
|
float scale = pow(10, floordiv(-(z - 1), 2)) * pow(24, floordiv(-z, 2));
|
2018-08-27 21:19:38 -04:00
|
|
|
|
|
|
|
|
lon += scale * x;
|
|
|
|
|
lat += scale * y;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lon *= 2;
|
|
|
|
|
|
|
|
|
|
return {lat, lon};
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-01 15:12:24 -06:00
|
|
|
// convert an arbitrary length grid locator to a high precision lat/lon in aprs
|
|
|
|
|
// format
|
2026-01-10 21:33:24 -05:00
|
|
|
/**
|
|
|
|
|
* @brief Convert grid locator to APRS format
|
2026-01-17 15:24:43 -06:00
|
|
|
*
|
|
|
|
|
* @param grid
|
|
|
|
|
* @return QPair<QString, QString>
|
2026-01-10 21:33:24 -05:00
|
|
|
*/
|
2026-01-01 15:12:24 -06:00
|
|
|
QPair<QString, QString> APRSISClient::grid2aprs(QString grid) {
|
2018-08-27 21:19:38 -04:00
|
|
|
auto geo = APRSISClient::grid2deg(grid);
|
|
|
|
|
auto lat = geo.first;
|
|
|
|
|
auto lon = geo.second;
|
|
|
|
|
|
|
|
|
|
QString latDir = "N";
|
2026-01-01 15:12:24 -06:00
|
|
|
if (lat < 0) {
|
2018-08-27 21:19:38 -04:00
|
|
|
lat *= -1;
|
|
|
|
|
latDir = "S";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QString lonDir = "E";
|
2026-01-01 15:12:24 -06:00
|
|
|
if (lon < 0) {
|
2018-08-27 21:19:38 -04:00
|
|
|
lon *= -1;
|
|
|
|
|
lonDir = "W";
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-01 15:12:24 -06:00
|
|
|
double iLat, fLat, iLon, fLon, iLatMin, fLatMin, iLonMin, fLonMin, iLatSec,
|
|
|
|
|
iLonSec;
|
2018-08-27 21:19:38 -04:00
|
|
|
fLat = modf(lat, &iLat);
|
|
|
|
|
fLon = modf(lon, &iLon);
|
|
|
|
|
|
|
|
|
|
fLatMin = modf(fLat * 60, &iLatMin);
|
|
|
|
|
fLonMin = modf(fLon * 60, &iLonMin);
|
|
|
|
|
|
|
|
|
|
iLatSec = round(fLatMin * 60);
|
|
|
|
|
iLonSec = round(fLonMin * 60);
|
|
|
|
|
|
2026-01-01 15:12:24 -06:00
|
|
|
if (iLatSec == 60) {
|
2018-08-27 21:19:38 -04:00
|
|
|
iLatMin += 1;
|
|
|
|
|
iLatSec = 0;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-01 15:12:24 -06:00
|
|
|
if (iLonSec == 60) {
|
2018-08-27 21:19:38 -04:00
|
|
|
iLonMin += 1;
|
|
|
|
|
iLonSec = 0;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-01 15:12:24 -06:00
|
|
|
if (iLatMin == 60) {
|
2018-08-27 21:19:38 -04:00
|
|
|
iLat += 1;
|
|
|
|
|
iLatMin = 0;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-01 15:12:24 -06:00
|
|
|
if (iLonMin == 60) {
|
2018-08-27 21:19:38 -04:00
|
|
|
iLon += 1;
|
|
|
|
|
iLonMin = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
double aprsLat = iLat * 100 + iLatMin + (iLatSec / 60.0);
|
|
|
|
|
double aprsLon = iLon * 100 + iLonMin + (iLonSec / 60.0);
|
|
|
|
|
|
2026-01-01 15:12:24 -06:00
|
|
|
return {QString("%1%2").arg(aprsLat, 7, 'f', 2, QChar('0')).arg(latDir),
|
|
|
|
|
QString("%1%2").arg(aprsLon, 8, 'f', 2, QChar('0')).arg(lonDir)};
|
2018-08-27 21:19:38 -04:00
|
|
|
}
|
|
|
|
|
|
2026-01-10 21:33:24 -05:00
|
|
|
/**
|
|
|
|
|
* @brief Strip SSID from callsign
|
2026-01-17 15:24:43 -06:00
|
|
|
*
|
|
|
|
|
* @param call
|
|
|
|
|
* @return QString
|
2026-01-10 21:33:24 -05:00
|
|
|
*/
|
2026-01-01 15:12:24 -06:00
|
|
|
QString APRSISClient::stripSSID(QString call) {
|
2018-10-02 14:28:33 -04:00
|
|
|
return QString(call.split("-").first().toUpper());
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-10 21:33:24 -05:00
|
|
|
/**
|
|
|
|
|
* @brief Replace callsign suffix with SSID
|
2026-01-17 15:24:43 -06:00
|
|
|
*
|
|
|
|
|
* @param call
|
|
|
|
|
* @param base
|
|
|
|
|
* @return QString
|
2026-01-10 21:33:24 -05:00
|
|
|
*/
|
2026-01-01 15:12:24 -06:00
|
|
|
QString APRSISClient::replaceCallsignSuffixWithSSID(QString call,
|
|
|
|
|
QString base) {
|
|
|
|
|
if (call != base) {
|
2020-04-01 20:42:29 -04:00
|
|
|
QRegularExpression re("[/](?<ssid>(\\d+))");
|
2018-09-21 14:43:02 -04:00
|
|
|
auto matcher = re.globalMatch(call);
|
2026-01-01 15:12:24 -06:00
|
|
|
if (matcher.hasNext()) {
|
2018-09-21 14:43:02 -04:00
|
|
|
auto match = matcher.next();
|
|
|
|
|
auto ssid = match.captured("ssid");
|
|
|
|
|
call = base + "-" + ssid;
|
|
|
|
|
} else {
|
|
|
|
|
call = base;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return call;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-15 19:26:06 -08:00
|
|
|
/**
|
|
|
|
|
* @brief Enable or disable persistent inbound relay connections.
|
|
|
|
|
*
|
|
|
|
|
* When enabled, the client keeps an active connection so inbound APRS
|
|
|
|
|
* messages can be received and relayed. When disabled, connections are
|
|
|
|
|
* closed once the outbound queue drains.
|
|
|
|
|
*
|
|
|
|
|
* @param enabled
|
|
|
|
|
*/
|
2026-01-14 10:32:00 -08:00
|
|
|
void APRSISClient::setIncomingRelayEnabled(bool enabled) {
|
2026-01-17 15:24:43 -06:00
|
|
|
qCDebug(aprsisclient_js8)
|
|
|
|
|
<< "APRSISClient::setIncomingRelayEnabled(" << enabled << ")";
|
2026-01-14 10:32:00 -08:00
|
|
|
if (m_incomingRelayEnabled == enabled)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
m_incomingRelayEnabled = enabled;
|
|
|
|
|
|
|
|
|
|
if (m_incomingRelayEnabled) {
|
|
|
|
|
// if enabled, trigger a connection immediately
|
2026-01-22 19:10:00 -08:00
|
|
|
if (!m_paused) {
|
|
|
|
|
processQueue(false);
|
|
|
|
|
}
|
2026-01-14 10:32:00 -08:00
|
|
|
} else {
|
|
|
|
|
// if disabled, and queue is empty, disconnect
|
|
|
|
|
if (state() == QTcpSocket::ConnectedState && m_frameQueue.isEmpty()) {
|
|
|
|
|
disconnectFromHost();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-10 21:33:24 -05:00
|
|
|
/**
|
|
|
|
|
* @brief Enqueue a spot frame for APRS-IS
|
2026-01-17 15:24:43 -06:00
|
|
|
*
|
|
|
|
|
* @param by_call
|
|
|
|
|
* @param from_call
|
|
|
|
|
* @param grid
|
|
|
|
|
* @param comment
|
2026-01-10 21:33:24 -05:00
|
|
|
*/
|
2026-01-01 15:12:24 -06:00
|
|
|
void APRSISClient::enqueueSpot(QString by_call, QString from_call, QString grid,
|
|
|
|
|
QString comment) {
|
|
|
|
|
if (!isPasscodeValid()) {
|
2020-03-30 23:07:40 -04:00
|
|
|
return;
|
|
|
|
|
}
|
2018-08-27 21:19:38 -04:00
|
|
|
|
|
|
|
|
auto geo = APRSISClient::grid2aprs(grid);
|
2020-03-30 23:07:40 -04:00
|
|
|
auto spotFrame = QString("%1>APJ8CL,qAS,%2:=%3/%4G#JS8 %5\n");
|
|
|
|
|
spotFrame = spotFrame.arg(from_call);
|
|
|
|
|
spotFrame = spotFrame.arg(by_call);
|
2018-08-27 21:19:38 -04:00
|
|
|
spotFrame = spotFrame.arg(geo.first);
|
|
|
|
|
spotFrame = spotFrame.arg(geo.second);
|
2020-03-30 23:07:40 -04:00
|
|
|
spotFrame = spotFrame.arg(comment.left(42));
|
2018-08-27 21:19:38 -04:00
|
|
|
enqueueRaw(spotFrame);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-10 21:33:24 -05:00
|
|
|
/**
|
|
|
|
|
* @brief Enqueue a third-party message frame for APRS-IS
|
2026-01-17 15:24:43 -06:00
|
|
|
*
|
|
|
|
|
* @param by_call
|
|
|
|
|
* @param from_call
|
|
|
|
|
* @param text
|
2026-01-10 21:33:24 -05:00
|
|
|
*/
|
2026-01-01 15:12:24 -06:00
|
|
|
void APRSISClient::enqueueThirdParty(QString by_call, QString from_call,
|
|
|
|
|
QString text) {
|
|
|
|
|
if (!isPasscodeValid()) {
|
2018-09-05 11:33:50 -04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-30 23:07:40 -04:00
|
|
|
auto frame = QString("%1>APJ8CL,qAS,%2:%3\n");
|
|
|
|
|
frame = frame.arg(from_call);
|
|
|
|
|
frame = frame.arg(by_call);
|
|
|
|
|
frame = frame.arg(text);
|
|
|
|
|
|
2018-08-28 15:13:38 -04:00
|
|
|
enqueueRaw(frame);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-23 16:17:14 -08:00
|
|
|
/**
|
|
|
|
|
* @brief Enqueue a standard APRS message ACK frame for APRS-IS.
|
|
|
|
|
*
|
|
|
|
|
* @param from_call Source callsign (appears as the message sender).
|
|
|
|
|
* @param to_call Destination callsign to acknowledge.
|
|
|
|
|
* @param messageId APRS message identifier (preserved exactly).
|
|
|
|
|
*/
|
|
|
|
|
void APRSISClient::enqueueMessageAck(QString from_call, QString to_call,
|
|
|
|
|
QString messageId) {
|
|
|
|
|
if (!isPasscodeValid()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (from_call.isEmpty() || to_call.isEmpty() || messageId.isEmpty()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto dest = to_call.left(9).leftJustified(9, ' ', true);
|
|
|
|
|
auto frame = QString("%1>APRS,TCPIP*::%2:ack%3\n");
|
|
|
|
|
frame = frame.arg(from_call, dest, messageId);
|
|
|
|
|
|
|
|
|
|
qDebug() << "DEBUG: APRS ACK Frame:" << frame.trimmed();
|
|
|
|
|
|
|
|
|
|
enqueueRaw(frame);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-10 21:33:24 -05:00
|
|
|
/**
|
|
|
|
|
* @brief Enqueue a raw APRS frame for APRS-IS
|
2026-01-17 15:24:43 -06:00
|
|
|
*
|
|
|
|
|
* @param aprsFrame
|
2026-01-10 21:33:24 -05:00
|
|
|
*/
|
2026-01-01 15:12:24 -06:00
|
|
|
void APRSISClient::enqueueRaw(QString aprsFrame) {
|
|
|
|
|
m_frameQueue.enqueue({aprsFrame, DriftingDateTime::currentDateTimeUtc()});
|
2018-08-27 21:19:38 -04:00
|
|
|
}
|
|
|
|
|
|
2026-01-10 21:33:24 -05:00
|
|
|
/**
|
|
|
|
|
* @brief Process the APRS-IS frame queue
|
2026-01-17 15:24:43 -06:00
|
|
|
*
|
|
|
|
|
* @param disconnect
|
2026-01-10 21:33:24 -05:00
|
|
|
*/
|
2026-01-01 15:12:24 -06:00
|
|
|
void APRSISClient::processQueue(bool disconnect) {
|
2018-08-30 12:18:20 -04:00
|
|
|
// don't process queue if we haven't set our local callsign
|
2026-01-14 10:32:00 -08:00
|
|
|
if (m_localCall.isEmpty()) {
|
2026-01-17 15:24:43 -06:00
|
|
|
qCDebug(aprsisclient_js8)
|
|
|
|
|
<< "APRSISClient Abort ProcessQueue: No Local Call";
|
2026-01-01 15:12:24 -06:00
|
|
|
return;
|
2026-01-14 10:32:00 -08:00
|
|
|
}
|
2018-08-30 12:18:20 -04:00
|
|
|
|
2026-01-22 19:10:00 -08:00
|
|
|
if (!m_incomingRelayEnabled && m_frameQueue.isEmpty()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-24 15:27:12 -04:00
|
|
|
// don't process queue if there's no host
|
2026-01-01 15:12:24 -06:00
|
|
|
if (m_host.isEmpty() || m_port == 0) {
|
2019-08-24 15:27:12 -04:00
|
|
|
// no host, so let's clear the queue and exit
|
|
|
|
|
m_frameQueue.clear();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-14 10:32:00 -08:00
|
|
|
// if we're not connected, connect
|
|
|
|
|
// the queue will be processed in onSocketConnected
|
2026-01-01 15:12:24 -06:00
|
|
|
if (state() != QTcpSocket::ConnectedState) {
|
|
|
|
|
qCDebug(aprsisclient_js8)
|
|
|
|
|
<< "APRSISClient Connecting:" << m_host << m_port;
|
2018-08-27 21:19:38 -04:00
|
|
|
connectToHost(m_host, m_port);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-14 10:32:00 -08:00
|
|
|
// if we're connected but not logged in, wait
|
|
|
|
|
if (!m_isLoggedIn) {
|
2018-08-30 14:31:31 -04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-14 10:32:00 -08:00
|
|
|
// process queue
|
2018-09-13 22:23:32 -04:00
|
|
|
QQueue<QPair<QString, QDateTime>> delayed;
|
|
|
|
|
|
2026-01-01 15:12:24 -06:00
|
|
|
while (!m_frameQueue.isEmpty()) {
|
2018-09-05 14:57:40 -04:00
|
|
|
auto pair = m_frameQueue.head();
|
|
|
|
|
auto frame = pair.first;
|
|
|
|
|
auto timestamp = pair.second;
|
|
|
|
|
|
|
|
|
|
// if the packet is older than the timeout, drop it.
|
2026-01-01 15:12:24 -06:00
|
|
|
if (timestamp.secsTo(DriftingDateTime::currentDateTimeUtc()) >
|
|
|
|
|
PACKET_TIMEOUT_SECONDS) {
|
|
|
|
|
qCDebug(aprsisclient_js8)
|
|
|
|
|
<< "APRSISClient Packet Timeout:" << frame;
|
2018-09-05 14:57:40 -04:00
|
|
|
m_frameQueue.dequeue();
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2018-08-30 14:31:31 -04:00
|
|
|
|
2026-01-01 15:12:24 -06:00
|
|
|
// random delay 25% of the time for throttling (a skip will add 60
|
|
|
|
|
// seconds to the processing time)
|
|
|
|
|
if (m_skipPercent > 0 && QRandomGenerator::global()->generate() % 100 <=
|
|
|
|
|
(m_skipPercent * 100)) {
|
|
|
|
|
qCDebug(aprsisclient_js8)
|
|
|
|
|
<< "APRSISClient Throttle: Skipping Frame";
|
2018-09-13 22:23:32 -04:00
|
|
|
delayed.enqueue(m_frameQueue.dequeue());
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-05 14:57:40 -04:00
|
|
|
QByteArray data = frame.toLocal8Bit();
|
2026-01-01 15:12:24 -06:00
|
|
|
if (write(data) == -1) {
|
|
|
|
|
qCDebug(aprsisclient_js8)
|
|
|
|
|
<< "APRSISClient Write Error:" << errorString();
|
2018-08-27 21:19:38 -04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-12 03:34:27 +02:00
|
|
|
qCDebug(aprsisclient_js8) << "APRSISClient Write:" << data;
|
2018-08-30 14:31:31 -04:00
|
|
|
m_frameQueue.dequeue();
|
2018-08-27 21:19:38 -04:00
|
|
|
}
|
|
|
|
|
|
2018-09-13 22:23:32 -04:00
|
|
|
// enqueue the delayed frames for later processing
|
2026-01-01 15:12:24 -06:00
|
|
|
while (!delayed.isEmpty()) {
|
2018-09-13 22:23:32 -04:00
|
|
|
m_frameQueue.enqueue(delayed.dequeue());
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-14 10:32:00 -08:00
|
|
|
if (disconnect && !m_incomingRelayEnabled) {
|
2018-08-27 21:19:38 -04:00
|
|
|
disconnectFromHost();
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-12 03:34:27 +02:00
|
|
|
|
2026-01-15 19:26:06 -08:00
|
|
|
/**
|
|
|
|
|
* @brief Handle APRS-IS socket connection and login handshake.
|
|
|
|
|
*/
|
2026-01-14 10:32:00 -08:00
|
|
|
void APRSISClient::onSocketConnected() {
|
|
|
|
|
qCDebug(aprsisclient_js8) << "APRSISClient Connected";
|
|
|
|
|
|
|
|
|
|
QString loginFrameStr;
|
|
|
|
|
if (m_incomingRelayEnabled) {
|
|
|
|
|
// Include filter in login frame
|
|
|
|
|
loginFrameStr = loginFrame(m_localCall, "filter t/m");
|
|
|
|
|
qCDebug(aprsisclient_js8) << "APRSISClient Login with filter t/m";
|
|
|
|
|
} else {
|
|
|
|
|
loginFrameStr = loginFrame(m_localCall);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (write(loginFrameStr.toLocal8Bit()) == -1) {
|
|
|
|
|
qCDebug(aprsisclient_js8)
|
|
|
|
|
<< "APRSISClient Write Login Error:" << errorString();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// assume logged in for now, real verification is harder with async
|
|
|
|
|
// but typically APRS-IS sends a line starting with #
|
|
|
|
|
// we'll handle that in readyRead if needed, but for now allow writing
|
|
|
|
|
m_isLoggedIn = true;
|
|
|
|
|
|
|
|
|
|
// process any pending queue items
|
|
|
|
|
processQueue(!m_incomingRelayEnabled);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-15 19:26:06 -08:00
|
|
|
/**
|
|
|
|
|
* @brief Process incoming APRS-IS data and emit parsed messages.
|
|
|
|
|
*/
|
2026-01-14 10:32:00 -08:00
|
|
|
void APRSISClient::onSocketReadyRead() {
|
|
|
|
|
while (canReadLine()) {
|
|
|
|
|
auto line = QString::fromUtf8(readLine()).trimmed();
|
|
|
|
|
if (line.isEmpty()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
qCDebug(aprsisclient_js8) << "APRSISClient Read:" << line;
|
|
|
|
|
|
|
|
|
|
if (line.startsWith("#")) {
|
|
|
|
|
// server comment / login response
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse message for relay
|
|
|
|
|
// Format: SOURCE>PATH::DEST :MESSAGE
|
|
|
|
|
// Regex: ^([^>]+)>[^:]+::([A-Z0-9 ]{9}):(.*)$
|
2026-01-17 15:24:43 -06:00
|
|
|
static QRegularExpression msgRe("^([^>]+)>[^:]+::([A-Z0-9 ]{9}):(.*)$");
|
2026-01-14 10:32:00 -08:00
|
|
|
auto match = msgRe.match(line);
|
|
|
|
|
if (match.hasMatch()) {
|
|
|
|
|
auto from = match.captured(1);
|
|
|
|
|
auto to = match.captured(2).trimmed();
|
|
|
|
|
auto msg = match.captured(3);
|
2026-01-23 16:17:14 -08:00
|
|
|
auto messageId = extractMessageId(msg);
|
2026-01-14 10:32:00 -08:00
|
|
|
|
2026-01-17 15:24:43 -06:00
|
|
|
qCDebug(aprsisclient_js8)
|
|
|
|
|
<< "APRSISClient Parsed Message:"
|
2026-01-23 16:17:14 -08:00
|
|
|
<< "From:" << from << "To:" << to << "Msg:" << msg
|
|
|
|
|
<< "Id:" << messageId;
|
|
|
|
|
qDebug() << "DEBUG: APRS Parsed Message" << from << to << msg
|
|
|
|
|
<< "Id:" << messageId;
|
2026-01-14 10:32:00 -08:00
|
|
|
|
2026-01-23 16:17:14 -08:00
|
|
|
emit messageReceived(from, to, msg, messageId);
|
2026-01-14 10:32:00 -08:00
|
|
|
} else {
|
2026-01-17 15:24:43 -06:00
|
|
|
qCDebug(aprsisclient_js8)
|
|
|
|
|
<< "APRSISClient: No Regex Match for:" << line;
|
2026-01-14 10:32:00 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-15 19:26:06 -08:00
|
|
|
/**
|
|
|
|
|
* @brief Handle APRS-IS socket disconnects and reconnect as needed.
|
|
|
|
|
*/
|
2026-01-14 10:32:00 -08:00
|
|
|
void APRSISClient::onSocketDisconnected() {
|
|
|
|
|
qCDebug(aprsisclient_js8) << "APRSISClient Disconnected";
|
|
|
|
|
m_isLoggedIn = false;
|
|
|
|
|
|
|
|
|
|
if (m_incomingRelayEnabled) {
|
|
|
|
|
// retry connection if we're supposed to be persistent
|
|
|
|
|
QTimer::singleShot(5000, this, [this]() {
|
2026-01-17 15:24:43 -06:00
|
|
|
if (m_incomingRelayEnabled &&
|
|
|
|
|
state() != QTcpSocket::ConnectedState) {
|
2026-01-14 10:32:00 -08:00
|
|
|
processQueue(false);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-15 19:26:06 -08:00
|
|
|
/**
|
|
|
|
|
* @brief Log APRS-IS socket errors.
|
|
|
|
|
* @param socketError Underlying socket error.
|
|
|
|
|
*/
|
2026-01-14 10:32:00 -08:00
|
|
|
void APRSISClient::onSocketError(QAbstractSocket::SocketError) {
|
|
|
|
|
qCDebug(aprsisclient_js8) << "APRSISClient Error:" << errorString();
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-12 03:34:27 +02:00
|
|
|
Q_LOGGING_CATEGORY(aprsisclient_js8, "aprsisclient.js8", QtWarningMsg)
|