#include "APRSISClient.h" #include #include #include #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 findall(QRegularExpression re, QString content) { qsizetype pos = 0; QList 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 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 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("[/](?(\\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> 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)