#include "APRSISClient.h" #include #include "DriftingDateTime.h" #include "varicode.h" const int PACKET_TIMEOUT_SECONDS = 300; APRSISClient::APRSISClient(QString host, quint16 port, QObject *parent): QTcpSocket(parent) { setServer(host, port); connect(&m_timer, &QTimer::timeout, this, &APRSISClient::sendReports); m_timer.setInterval(60*1000); // every 60 seconds m_timer.start(); } 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; int i = 0; int 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){ int 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().sprintf("%07.2f%%1", aprsLat).arg(latDir), QString().sprintf("%08.2f%%1", aprsLon).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("[/](?(P|\\d+))"); auto matcher = re.globalMatch(call); if(matcher.hasNext()){ auto match = matcher.next(); auto ssid = match.captured("ssid"); if(ssid == "P"){ ssid = "16"; } call = base + "-" + ssid; } else { call = base; } } return call; } void APRSISClient::enqueueSpot(QString theircall, QString grid, QString comment){ if(m_localCall.isEmpty()) return; auto geo = APRSISClient::grid2aprs(grid); auto spotFrame = QString("%1>APRS,%2,TCPIP*:=%3/%4nJS8 %5\n"); spotFrame = spotFrame.arg(theircall); spotFrame = spotFrame.arg(stripSSID(m_localCall)); spotFrame = spotFrame.arg(geo.first); spotFrame = spotFrame.arg(geo.second); spotFrame = spotFrame.arg(comment.left(43)); enqueueRaw(spotFrame); } void APRSISClient::enqueueThirdParty(QString theircall, QString payload){ if(!isPasscodeValid()){ return; } auto frame = QString("%1>APRS,%2,TCPIP*:%3\n"); frame = frame.arg(theircall); frame = frame.arg(stripSSID(m_localCall)); frame = frame.arg(payload); 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; // 1. connect (and read) // 2. login (and read) // 3. for each raw frame in queue, send // 4. disconnect if(state() != QTcpSocket::ConnectedState){ qDebug() << "APRSISClient Connecting:" << m_host << m_port; connectToHost(m_host, m_port); if(!waitForConnected(5000)){ qDebug() << "APRSISClient Connection Error:" << errorString(); return; } } auto re = QRegExp("(full|unavailable|busy)"); auto line = QString(readLine()); if(line.toLower().indexOf(re) >= 0){ qDebug() << "APRSISClient Connection Busy:" << line; return; } if(write(loginFrame(m_localCall).toLocal8Bit()) == -1){ qDebug() << "APRSISClient Write Login Error:" << errorString(); return; } if(!waitForReadyRead(5000)){ qDebug() << "APRSISClient Login Error: Server Not Responding"; return; } line = QString(readAll()); if(line.toLower().indexOf(re) >= 0){ qDebug() << "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){ qDebug() << "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(qrand() % 100 <= 25){ qDebug() << "APRSISClient Throttle: Skipping Frame"; delayed.enqueue(m_frameQueue.dequeue()); continue; } QByteArray data = frame.toLocal8Bit(); if(write(data) == -1){ qDebug() << "APRSISClient Write Error:" << errorString(); return; } qDebug() << "APRSISClient Write:" << data; if(waitForReadyRead(5000)){ line = QString(readLine()); qDebug() << "APRSISClient Read:" << line; if(line.toLower().indexOf(re) >= 0){ qDebug() << "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(); } }