mirror of
https://github.com/JS8Call-improved/JS8Call-improved
synced 2026-08-13 17:47:36 -04:00
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
356 lines
9.4 KiB
C++
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)
|