js8call/JS8_Main/APRSISClient.cpp
Chris-AC9KH 356bdc0f50 Code cleanup - move loose source and header files in source tree to group folders
Rename all header files with .h extension
Fix function declaration in HamlibTransceiver.h that overrode member function but was not marked override
Remove unused variable in Modulator.cpp
Remove unused files from source tree
Remove duplicate LazyFillComboBox files
Reformat codebase to LLVM C++ standard
2026-01-02 16:46:38 -06:00

356 lines
9.4 KiB
C++

#include "APRSISClient.h"
#include <QLoggingCategory>
#include <QRandomGenerator>
#include <cmath>
#include "DriftingDateTime.h"
#include "varicode.h"
Q_DECLARE_LOGGING_CATEGORY(aprsisclient_js8)
const int PACKET_TIMEOUT_SECONDS = 300;
APRSISClient::APRSISClient(QString const host, quint16 const port,
QObject *parent)
: QTcpSocket{parent}, m_timer{this} {
setServer(host, port);
connect(&m_timer, &QTimer::timeout, this, &APRSISClient::sendReports);
m_timer.start(std::chrono::minutes(1));
}
quint32 APRSISClient::hashCallsign(QString callsign) {
// based on: https://github.com/hessu/aprsc/blob/master/src/passcode.c
QByteArray rootCall =
QString(callsign.split("-").first().toUpper()).toLocal8Bit() + '\0';
quint32 hash = 0x73E2;
qsizetype i = 0;
qsizetype const len = rootCall.length();
while (i + 1 < len) {
hash ^= rootCall.at(i) << 8;
hash ^= rootCall.at(i + 1);
i += 2;
}
return hash & 0x7FFF;
}
QString APRSISClient::loginFrame(QString callsign) {
auto loginFrame = QString("user %1 pass %2 ver %3\n");
loginFrame = loginFrame.arg(callsign);
loginFrame = loginFrame.arg(hashCallsign(callsign));
loginFrame = loginFrame.arg("JS8Call");
return loginFrame;
}
QList<QStringList> findall(QRegularExpression re, QString content) {
qsizetype pos = 0;
QList<QStringList> all;
while (pos < content.length()) {
auto match = re.match(content, pos);
if (!match.hasMatch()) {
break;
}
all.append(match.capturedTexts());
pos = match.capturedEnd();
}
return all;
}
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;
}
}
// convert an arbitrary length grid locator to a high precision lat/lon
QPair<float, float> APRSISClient::grid2deg(QString locator) {
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';
foreach (QStringList matched, lats) {
char x = matched.at(1).at(0).toLatin1();
char y = matched.at(2).at(0).toLatin1();
valx[i * 2] = x - A;
valy[i * 2] = y - A;
i++;
tot++;
}
i = 0;
foreach (QStringList matched, lons) {
int x = matched.at(1).toInt();
int y = matched.at(2).toInt();
valx[i * 2 + 1] = x;
valy[i * 2 + 1] = y;
i++;
tot++;
}
for (int i = 0; i < tot; i++) {
int x = valx[i];
int y = valy[i];
int z = i - 1;
float scale = pow(10, floordiv(-(z - 1), 2)) * pow(24, floordiv(-z, 2));
lon += scale * x;
lat += scale * y;
}
lon *= 2;
return {lat, lon};
}
// convert an arbitrary length grid locator to a high precision lat/lon in aprs
// format
QPair<QString, QString> APRSISClient::grid2aprs(QString grid) {
auto geo = APRSISClient::grid2deg(grid);
auto lat = geo.first;
auto lon = geo.second;
QString latDir = "N";
if (lat < 0) {
lat *= -1;
latDir = "S";
}
QString lonDir = "E";
if (lon < 0) {
lon *= -1;
lonDir = "W";
}
double iLat, fLat, iLon, fLon, iLatMin, fLatMin, iLonMin, fLonMin, iLatSec,
iLonSec;
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);
if (iLatSec == 60) {
iLatMin += 1;
iLatSec = 0;
}
if (iLonSec == 60) {
iLonMin += 1;
iLonSec = 0;
}
if (iLatMin == 60) {
iLat += 1;
iLatMin = 0;
}
if (iLonMin == 60) {
iLon += 1;
iLonMin = 0;
}
double aprsLat = iLat * 100 + iLatMin + (iLatSec / 60.0);
double aprsLon = iLon * 100 + iLonMin + (iLonSec / 60.0);
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)};
}
QString APRSISClient::stripSSID(QString call) {
return QString(call.split("-").first().toUpper());
}
QString APRSISClient::replaceCallsignSuffixWithSSID(QString call,
QString base) {
if (call != base) {
QRegularExpression re("[/](?<ssid>(\\d+))");
auto matcher = re.globalMatch(call);
if (matcher.hasNext()) {
auto match = matcher.next();
auto ssid = match.captured("ssid");
call = base + "-" + ssid;
} else {
call = base;
}
}
return call;
}
void APRSISClient::enqueueSpot(QString by_call, QString from_call, QString grid,
QString comment) {
if (!isPasscodeValid()) {
return;
}
auto geo = APRSISClient::grid2aprs(grid);
auto spotFrame = QString("%1>APJ8CL,qAS,%2:=%3/%4G#JS8 %5\n");
spotFrame = spotFrame.arg(from_call);
spotFrame = spotFrame.arg(by_call);
spotFrame = spotFrame.arg(geo.first);
spotFrame = spotFrame.arg(geo.second);
spotFrame = spotFrame.arg(comment.left(42));
enqueueRaw(spotFrame);
}
void APRSISClient::enqueueThirdParty(QString by_call, QString from_call,
QString text) {
if (!isPasscodeValid()) {
return;
}
auto frame = QString("%1>APJ8CL,qAS,%2:%3\n");
frame = frame.arg(from_call);
frame = frame.arg(by_call);
frame = frame.arg(text);
enqueueRaw(frame);
}
void APRSISClient::enqueueRaw(QString aprsFrame) {
m_frameQueue.enqueue({aprsFrame, DriftingDateTime::currentDateTimeUtc()});
}
void APRSISClient::processQueue(bool disconnect) {
// don't process queue if we haven't set our local callsign
if (m_localCall.isEmpty())
return;
// don't process queue if there's nothing to process
if (m_frameQueue.isEmpty())
return;
// don't process queue if there's no host
if (m_host.isEmpty() || m_port == 0) {
// no host, so let's clear the queue and exit
m_frameQueue.clear();
return;
}
// 1. connect (and read)
// 2. login (and read)
// 3. for each raw frame in queue, send
// 4. disconnect
if (state() != QTcpSocket::ConnectedState) {
qCDebug(aprsisclient_js8)
<< "APRSISClient Connecting:" << m_host << m_port;
connectToHost(m_host, m_port);
if (!waitForConnected(5000)) {
qCDebug(aprsisclient_js8)
<< "APRSISClient Connection Error:" << errorString();
return;
}
}
auto re = QRegularExpression("(full|unavailable|busy)");
auto line = QString(readLine());
if (line.toLower().indexOf(re) >= 0) {
qCDebug(aprsisclient_js8) << "APRSISClient Connection Busy:" << line;
return;
}
if (write(loginFrame(m_localCall).toLocal8Bit()) == -1) {
qCDebug(aprsisclient_js8)
<< "APRSISClient Write Login Error:" << errorString();
return;
}
if (!waitForReadyRead(5000)) {
qCDebug(aprsisclient_js8)
<< "APRSISClient Login Error: Server Not Responding";
return;
}
line = QString(readAll());
if (line.toLower().indexOf(re) >= 0) {
qCDebug(aprsisclient_js8) << "APRSISClient Server Busy:" << line;
return;
}
QQueue<QPair<QString, QDateTime>> delayed;
while (!m_frameQueue.isEmpty()) {
auto pair = m_frameQueue.head();
auto frame = pair.first;
auto timestamp = pair.second;
// if the packet is older than the timeout, drop it.
if (timestamp.secsTo(DriftingDateTime::currentDateTimeUtc()) >
PACKET_TIMEOUT_SECONDS) {
qCDebug(aprsisclient_js8)
<< "APRSISClient Packet Timeout:" << frame;
m_frameQueue.dequeue();
continue;
}
// 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";
delayed.enqueue(m_frameQueue.dequeue());
continue;
}
QByteArray data = frame.toLocal8Bit();
if (write(data) == -1) {
qCDebug(aprsisclient_js8)
<< "APRSISClient Write Error:" << errorString();
return;
}
qCDebug(aprsisclient_js8) << "APRSISClient Write:" << data;
if (waitForReadyRead(5000)) {
line = QString(readLine());
qCDebug(aprsisclient_js8) << "APRSISClient Read:" << line;
if (line.toLower().indexOf(re) >= 0) {
qCDebug(aprsisclient_js8)
<< "APRSISClient Cannot Write Error:" << line;
return;
}
}
m_frameQueue.dequeue();
}
// enqueue the delayed frames for later processing
while (!delayed.isEmpty()) {
m_frameQueue.enqueue(delayed.dequeue());
}
if (disconnect) {
disconnectFromHost();
}
}
Q_LOGGING_CATEGORY(aprsisclient_js8, "aprsisclient.js8", QtWarningMsg)