diff --git a/APRSISClient.cpp b/APRSISClient.cpp new file mode 100644 index 0000000..c2f92b0 --- /dev/null +++ b/APRSISClient.cpp @@ -0,0 +1,250 @@ +#include "APRSISClient.h" + +#include + +#include "Radio.hpp" +#include "varicode.h" + + + +APRSISClient::APRSISClient(QString host, quint16 port, QObject *parent): + QTcpSocket(parent), + m_host(host), + m_port(port) +{ + connect(&m_timer, &QTimer::timeout, this, &APRSISClient::sendReports); + m_timer.setInterval(60*1000); // every minute + 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("FT8Call"); + 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) + }; +} + +void APRSISClient::enqueueSpot(QString theircall, QString grid, quint64 frequency, int snr){ + if(m_localCall.isEmpty()) return; + + auto geo = APRSISClient::grid2aprs(grid); + auto spotFrame = QString("%1>APRS,TCPIP*:=%2/%3nFT8CALL %4 %5 %6MHz %7dB\n"); + spotFrame = spotFrame.arg(theircall); + spotFrame = spotFrame.arg(geo.first); + spotFrame = spotFrame.arg(geo.second); + spotFrame = spotFrame.arg(m_localCall); + spotFrame = spotFrame.arg(m_localGrid.left(4)); + spotFrame = spotFrame.arg(Radio::frequency_MHz_string(frequency)); + spotFrame = spotFrame.arg(Varicode::formatSNR(snr)); + enqueueRaw(spotFrame); +} + +void APRSISClient::enqueueMessage(QString tocall, QString message){ + if(m_localCall.isEmpty()) return; + + auto messageFrame = QString("%1>APRS,TCPIP*::%2:%3\n"); + messageFrame = messageFrame.arg(m_localCall); + messageFrame = messageFrame.arg(tocall + QString(" ").repeated(9-tocall.length())); + messageFrame = messageFrame.arg(message); + enqueueRaw(messageFrame); +} + +void APRSISClient::enqueueRaw(QString aprsFrame){ + m_frameQueue.enqueue(aprsFrame); +} + +void APRSISClient::processQueue(bool disconnect){ + if(m_localCall.isEmpty()) return; + + // 1. connect (and read) + // 2. login (and read) + // 3. for each raw frame in queue, send + // 4. disconnect + + if(state() != QTcpSocket::ConnectedState){ + connectToHost(m_host, m_port); + if(!waitForConnected(5000)){ + qDebug() << "APRSISClient Connection Error:" << errorString(); + return; + } + } + + if(write(loginFrame(m_localCall).toLocal8Bit()) == -1){ + qDebug() << "APRSISClient Write Login Error:" << errorString(); + return; + } + + while(!m_frameQueue.isEmpty()){ + if(write(m_frameQueue.head().toLocal8Bit()) == -1){ + qDebug() << "APRSISClient Write Error:" << errorString(); + return; + } + + auto frame = m_frameQueue.dequeue(); + qDebug() << "APRISISClient Write:" << frame; + } + + if(disconnect){ + disconnectFromHost(); + } +} diff --git a/APRSISClient.h b/APRSISClient.h new file mode 100644 index 0000000..4cc7bc1 --- /dev/null +++ b/APRSISClient.h @@ -0,0 +1,44 @@ +#ifndef APRSISCLIENT_H +#define APRSISCLIENT_H + +#include +#include +#include +#include + +class APRSISClient : public QTcpSocket +{ +public: + APRSISClient(QString host, quint16 port, QObject *parent = nullptr); + + static quint32 hashCallsign(QString callsign); + static QString loginFrame(QString callsign); + static QPair grid2deg(QString grid); + static QPair grid2aprs(QString grid); + + void setLocalStation(QString mycall, QString mygrid){ + m_localCall = mycall; + m_localGrid = mygrid; + } + + void enqueueSpot(QString theircall, QString grid, quint64 frequency, int snr); + void enqueueMessage(QString tocall, QString message); + void enqueueMail(QString email, QString message); + void enqueueRaw(QString aprsFrame); + + void processQueue(bool disconnect=false); + +public slots: + void sendReports(){ processQueue(true); } + +private: + QString m_localCall; + QString m_localGrid; + + QQueue m_frameQueue; + QString m_host; + quint16 m_port; + QTimer m_timer; +}; + +#endif // APRSISCLIENT_H diff --git a/CMakeLists.txt b/CMakeLists.txt index b1f7d97..a5812dc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -305,6 +305,7 @@ set (wsjtx_CXXSRCS WsprTxScheduler.cpp varicode.cpp SelfDestructMessageBox.cpp + APRSISClient.cpp mainwindow.cpp Configuration.cpp main.cpp diff --git a/Configuration.cpp b/Configuration.cpp index f15b501..c7f623c 100644 --- a/Configuration.cpp +++ b/Configuration.cpp @@ -576,7 +576,7 @@ private: double txDelay_; bool id_after_73_; bool tx_QSY_allowed_; - bool spot_to_psk_reporter_; + bool spot_to_reporting_networks_; bool transmit_directed_; bool autoreply_off_at_startup_; bool monitor_off_at_startup_; @@ -678,15 +678,15 @@ double Configuration::txDelay() const {return m_->txDelay_;} qint32 Configuration::RxBandwidth() const {return m_->RxBandwidth_;} bool Configuration::id_after_73 () const {return m_->id_after_73_;} bool Configuration::tx_QSY_allowed () const {return m_->tx_QSY_allowed_;} -bool Configuration::spot_to_psk_reporter () const +bool Configuration::spot_to_reporting_networks () const { // rig must be open and working to spot externally - return is_transceiver_online () && m_->spot_to_psk_reporter_; + return is_transceiver_online () && m_->spot_to_reporting_networks_; } -void Configuration::set_spot_to_psk_reporter (bool spot) +void Configuration::set_spot_to_reporting_networks (bool spot) { - if(m_->spot_to_psk_reporter_ != spot){ - m_->spot_to_psk_reporter_ = spot; + if(m_->spot_to_reporting_networks_ != spot){ + m_->spot_to_reporting_networks_ = spot; m_->write_settings(); } } @@ -1264,7 +1264,7 @@ void Configuration::impl::initialize_models () ui_->azel_path_display_label->setText (azel_directory_.absolutePath ()); ui_->CW_id_after_73_check_box->setChecked (id_after_73_); ui_->tx_QSY_check_box->setChecked (tx_QSY_allowed_); - ui_->psk_reporter_check_box->setChecked (spot_to_psk_reporter_); + ui_->psk_reporter_check_box->setChecked (spot_to_reporting_networks_); ui_->transmit_directed_check_box->setChecked(transmit_directed_); ui_->autoreply_off_check_box->setChecked (autoreply_off_at_startup_); ui_->monitor_off_check_box->setChecked (monitor_off_at_startup_); @@ -1473,7 +1473,7 @@ void Configuration::impl::read_settings () autoreply_off_at_startup_ = settings_->value ("AutoreplyOFF", false).toBool (); monitor_off_at_startup_ = settings_->value ("MonitorOFF", false).toBool (); monitor_last_used_ = settings_->value ("MonitorLastUsed", false).toBool (); - spot_to_psk_reporter_ = settings_->value ("PSKReporter", true).toBool (); + spot_to_reporting_networks_ = settings_->value ("PSKReporter", true).toBool (); id_after_73_ = settings_->value ("After73", false).toBool (); tx_QSY_allowed_ = settings_->value ("TxQSYAllowed", false).toBool (); use_dynamic_info_ = settings_->value ("AutoGrid", false).toBool (); @@ -1613,7 +1613,7 @@ void Configuration::impl::write_settings () settings_->setValue ("AutoreplyOFF", autoreply_off_at_startup_); settings_->setValue ("MonitorOFF", monitor_off_at_startup_); settings_->setValue ("MonitorLastUsed", monitor_last_used_); - settings_->setValue ("PSKReporter", spot_to_psk_reporter_); + settings_->setValue ("PSKReporter", spot_to_reporting_networks_); settings_->setValue ("After73", id_after_73_); settings_->setValue ("TxQSYAllowed", tx_QSY_allowed_); settings_->setValue ("Macros", macros_.stringList ()); @@ -2033,7 +2033,7 @@ void Configuration::impl::accept () my_qth_ = ui_->qth_message_line_edit->text().toUpper(); callsign_aging_ = ui_->callsign_aging_spin_box->value(); activity_aging_ = ui_->activity_aging_spin_box->value(); - spot_to_psk_reporter_ = ui_->psk_reporter_check_box->isChecked (); + spot_to_reporting_networks_ = ui_->psk_reporter_check_box->isChecked (); id_interval_ = ui_->CW_id_interval_spin_box->value (); ntrials_ = ui_->sbNtrials->value (); txDelay_ = ui_->sbTxDelay->value (); diff --git a/Configuration.hpp b/Configuration.hpp index 5fa8b5a..e72ea5a 100644 --- a/Configuration.hpp +++ b/Configuration.hpp @@ -112,8 +112,8 @@ public: double txDelay() const; bool id_after_73 () const; bool tx_QSY_allowed () const; - bool spot_to_psk_reporter () const; - void set_spot_to_psk_reporter (bool); + bool spot_to_reporting_networks () const; + void set_spot_to_reporting_networks (bool); bool transmit_directed() const; bool autoreply_off_at_startup () const; bool monitor_off_at_startup () const; diff --git a/Configuration.ui b/Configuration.ui index 798f123..87158ce 100644 --- a/Configuration.ui +++ b/Configuration.ui @@ -80,7 +80,7 @@ <html><head/><body><p>4-digit Maidenhead Locator</p></body></html> - 16 + 12 diff --git a/aprs.py b/aprs.py new file mode 100644 index 0000000..26f088b --- /dev/null +++ b/aprs.py @@ -0,0 +1,64 @@ +import socket + +KKEY = 0x73e2 + +def do_hash(callsign): + rootCall = callsign.split("-")[0].upper() + '\0' + + hash = KKEY + i = 0 + length = len(rootCall) + + while (i+1 < length): + hash ^= ord(rootCall[i])<<8 + hash ^= ord(rootCall[i+1]) + i += 2 + + return int(hash & 0x7fff) + +HOST = 'rotate.aprs2.net' +PORT = 14580 + +print "Connecting..." + +s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +s.connect((HOST, PORT)) + +print "Connected..." + +data = s.recv(1024) +print data + +call = 'KN4CRD' +pw = do_hash(call) +ver = "FT8Call" + +login = "user {} pass {} ver {}\n".format(call, pw, ver) +s.send(login) + +print "Login sent...", login + +data = s.recv(1024) +print data + +if 1: + message = "{}>APRS,TCPIP*::EMAIL-2 :kn4crd@gmail.com testing456{{AA}}\n".format(call) + s.send(message) + +if 0: + payload = ":This is a test message" + message = "{}>APRS,TCPIP*::{} {}\n".format(call, call, payload) + s.send(message) +if 0: + position = "=3352.45N/08422.71Wn" + status = "FT8CALL VIA XX9XXX/XXXX 14.082500MHz -20dB" + payload = "".join((position, status)) + message = "{}>APRS,TCPIP*:{}\n".format(call, payload) + s.send(message) + +print "Spot sent...", message + +data = s.recv(1024) +print data + +s.close() diff --git a/mainwindow.cpp b/mainwindow.cpp index 504dc97..e1dbf14 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -1,5 +1,6 @@ //---------------------------------------------------------- MainWindow #include "mainwindow.h" +#include #include #include #include @@ -25,6 +26,7 @@ #include #include +#include "APRSISClient.h" #include "revision_utils.hpp" #include "qt_helpers.hpp" #include "NetworkAccessManager.hpp" @@ -466,6 +468,7 @@ MainWindow::MainWindow(QDir const& temp_directory, bool multiple, version (), revision (), m_config.udp_server_name (), m_config.udp_server_port (), this}}, + m_aprsClient {new APRSISClient{"rotate.aprs2.net", 14580, this}}, psk_Reporter {new PSK_Reporter {m_messageClient, this}}, m_i3bit {0}, m_manual {&m_network_manager}, @@ -1014,7 +1017,7 @@ MainWindow::MainWindow(QDir const& temp_directory, bool multiple, setFreqOffsetForRestore(f, false); ui->cbVHFcontest->setChecked(false); // this needs to always be false - ui->spotButton->setChecked(m_config.spot_to_psk_reporter()); + ui->spotButton->setChecked(m_config.spot_to_reporting_networks()); auto enterFilter = new EnterKeyPressEater(); connect(enterFilter, &EnterKeyPressEater::enterKeyPressed, this, [this](QObject *, QKeyEvent *, bool *pProcessed){ @@ -1279,6 +1282,17 @@ MainWindow::MainWindow(QDir const& temp_directory, bool multiple, quint8 r = 0; quint64 v = Varicode::unpack72bits(Varicode::pack72bits((((quint64)1)<<62)-1, (1<<7)-1), &r); qDebug() << "packing" << Varicode::pack72bits((((quint64)1)<<62)-1, (1<<7)-1) << v << r; + + qDebug() << APRSISClient::grid2deg("EM73"); + qDebug() << APRSISClient::grid2deg("EM73TU"); + qDebug() << APRSISClient::grid2deg("EM73TU49NT"); + + qDebug() << APRSISClient::grid2aprs("EM73"); + qDebug() << APRSISClient::grid2aprs("EM73TU"); + qDebug() << APRSISClient::grid2aprs("EM73TU49NT"); + + qDebug() << APRSISClient::grid2aprs("FI08VE49"); + qDebug() << APRSISClient::grid2aprs("OM25CU"); #endif // this must be the last statement of constructor @@ -2035,7 +2049,7 @@ void MainWindow::fastSink(qint64 frames) writeAllTxt(message); bool stdMsg = decodedtext.report(m_baseCall, Radio::base_callsign(ui->dxCallEntry->text()),m_rptRcvd); - if (stdMsg) pskPost (decodedtext); + //if (stdMsg) pskPost (decodedtext); } float fracTR=float(k)/(12000.0*m_TRperiod); @@ -2183,7 +2197,7 @@ void MainWindow::on_actionSettings_triggered() //Setup Dialog on_dxGridEntry_textChanged (m_hisGrid); // recalculate distances in case of units change enable_DXCC_entity (m_config.DXCC ()); // sets text window proportions and (re)inits the logbook - preparePSKReporter(); + prepareSpotting(); if(m_config.restart_audio_input ()) { Q_EMIT startAudioInputStream (m_config.audio_input_device (), @@ -2233,9 +2247,10 @@ void MainWindow::on_actionSettings_triggered() //Setup Dialog } } -void MainWindow::preparePSKReporter(){ - if(m_config.spot_to_psk_reporter ()){ - pskSetLocal (); +void MainWindow::prepareSpotting(){ + if(m_config.spot_to_reporting_networks ()){ + pskSetLocal(); + aprsSetLocal(); ui->spotButton->setChecked(true); } else { ui->spotButton->setChecked(false); @@ -2244,10 +2259,10 @@ void MainWindow::preparePSKReporter(){ void MainWindow::on_spotButton_clicked(bool checked){ // 1. save setting - m_config.set_spot_to_psk_reporter(checked); + m_config.set_spot_to_reporting_networks(checked); // 2. prepare - preparePSKReporter(); + prepareSpotting(); } void MainWindow::on_monitorButton_clicked (bool checked) @@ -3247,12 +3262,12 @@ void::MainWindow::fast_decode_done() writeAllTxt(message); if(m_mode=="JT9" or m_mode=="MSK144") { -// find and extract any report for myCall + // find and extract any report for myCall bool stdMsg = decodedtext.report(m_baseCall, Radio::base_callsign(ui->dxCallEntry->text()), m_rptRcvd); -// extract details and send to PSKreporter - if (stdMsg) pskPost (decodedtext); + // extract details and send to PSKreporter + //if (stdMsg) pskPost (decodedtext); } if (tmax >= 0.0) auto_sequence (decodedtext, ui->sbFtol->value (), ui->sbFtol->value ()); } @@ -3688,7 +3703,7 @@ void MainWindow::readFromStdout() //readFromStdout m_mode=="JT9") auto_sequence (decodedtext, 25, 50); postDecode (true, decodedtext.string ()); -// find and extract any report for myCall, but save in m_rptRcvd only if it's from DXcall + // find and extract any report for myCall, but save in m_rptRcvd only if it's from DXcall QString rpt; bool stdMsg = decodedtext.report(m_baseCall, Radio::base_callsign(ui->dxCallEntry->text()), rpt); @@ -3699,10 +3714,11 @@ void MainWindow::readFromStdout() //readFromStdout QString t=Radio::base_callsign(ui->dxCallEntry->text()); if((t==deCall or t=="") and rpt!="") m_rptRcvd=rpt; } -// extract details and send to PSKreporter + // extract details and send to PSKreporter int nsec=QDateTime::currentMSecsSinceEpoch()/1000-m_secBandChanged; bool okToPost=(nsec>(4*m_TRperiod)/5); - if (stdMsg && okToPost) pskPost(decodedtext); + + //if (stdMsg && okToPost) pskPost(decodedtext); if((m_mode=="JT4" or m_mode=="JT65" or m_mode=="QRA64") and m_msgAvgWidget!=NULL) { if(m_msgAvgWidget->isVisible()) { @@ -3811,7 +3827,8 @@ void MainWindow::auto_sequence (DecodedText const& message, unsigned start_toler void MainWindow::pskPost (DecodedText const& decodedtext) { - if (m_diskData || !m_config.spot_to_psk_reporter() || decodedtext.isLowConfidence ()) return; +#if 0 + if (m_diskData || !m_config.spot_to_reporting_networks() || decodedtext.isLowConfidence ()) return; QString msgmode=m_mode; if(m_mode=="JT9+JT65") { @@ -3826,6 +3843,7 @@ void MainWindow::pskPost (DecodedText const& decodedtext) audioFrequency=decodedtext.string().mid(16,4).toInt(); } int snr = decodedtext.snr(); + pskSetLocal (); if(grid.contains (grid_regexp)) { // qDebug() << "To PSKreporter:" << deCall << grid << frequency << msgmode << snr; @@ -3833,10 +3851,11 @@ void MainWindow::pskPost (DecodedText const& decodedtext) // QString::number(snr),QString::number(QDateTime::currentDateTime().toTime_t())); pskLogReport(msgmode, audioFrequency, snr, deCall, grid); } +#endif } void MainWindow::pskLogReport(QString mode, int offset, int snr, QString callsign, QString grid){ - if(!m_config.spot_to_psk_reporter()) return; + if(!m_config.spot_to_reporting_networks()) return; Frequency frequency = m_freqNominal + offset; @@ -3849,6 +3868,19 @@ void MainWindow::pskLogReport(QString mode, int offset, int snr, QString callsig QString::number(QDateTime::currentDateTime().toTime_t())); } +void MainWindow::aprsLogReport(int offset, int snr, QString callsign, QString grid){ + if(!m_config.spot_to_reporting_networks()) return; + + Frequency frequency = m_freqNominal + offset; + + if(grid.length() < 6){ + qDebug() << "APRSISClient Spot Skipped:" << callsign << grid; + return; + } + + m_aprsClient->enqueueSpot(Radio::base_callsign(callsign), grid, frequency, snr); +} + void MainWindow::killFile () { if (m_fnameWE.size () && @@ -6946,6 +6978,7 @@ void MainWindow::band_changed (Frequency f) m_bandEdited = false; psk_Reporter->sendReport(); // Upload any queued spots before changing band + m_aprsClient->processQueue(); if (!m_transmitting) monitor (true); if ("FreqCal" == m_mode) { @@ -7122,18 +7155,15 @@ void MainWindow::on_qtcMacroButton_clicked(){ if(qtc.isEmpty()){ return; } - addMessageText(qtc); + addMessageText(QString("QTC %1").arg(qtc)); } void MainWindow::on_qthMacroButton_clicked(){ QString qth = m_config.my_qth(); - if(qth.isEmpty()){ - qth = m_config.my_grid(); - } if(qth.isEmpty()){ return; } - addMessageText(qth); + addMessageText(QString("QTH %1").arg(qth)); } void MainWindow::setSortBy(QString key, QString value){ @@ -7284,6 +7314,20 @@ void MainWindow::buildQueryMenu(QMenu * menu, QString call){ if(m_config.transmit_directed()) toggleTx(true); }); + auto gridQueryAction = menu->addAction(QString("%1^ - What is your current grid locator?").arg(call)); + gridQueryAction->setDisabled(isAllCall); + connect(gridQueryAction, &QAction::triggered, this, [this](){ + + QString selectedCall = callsignSelected(); + if(selectedCall.isEmpty()){ + return; + } + + addMessageText(QString("%1^").arg(selectedCall), true); + + if(m_config.transmit_directed()) toggleTx(true); + }); + auto stationMessageQueryAction = menu->addAction(QString("%1&& - What is your station message?").arg(call).trimmed()); stationMessageQueryAction->setDisabled(isAllCall); connect(stationMessageQueryAction, &QAction::triggered, this, [this](){ @@ -7362,6 +7406,49 @@ void MainWindow::buildQueryMenu(QMenu * menu, QString call){ addMessageText(QString("%1!").arg(selectedCall), true); }); + menu->addSeparator(); + + auto qtcAction = menu->addAction(QString("%1 QTC message - Send my station message").arg(call).trimmed()); + connect(qtcAction, &QAction::triggered, this, [this](){ + + QString selectedCall = callsignSelected(); + if(selectedCall.isEmpty()){ + return; + } + + addMessageText(QString("%1 QTC %2").arg(selectedCall).arg(m_config.my_station()), true); + + if(m_config.transmit_directed()) toggleTx(true); + }); + + auto qthAction = menu->addAction(QString("%1 QTH message - Send my station location message").arg(call).trimmed()); + connect(qthAction, &QAction::triggered, this, [this](){ + + QString selectedCall = callsignSelected(); + if(selectedCall.isEmpty()){ + return; + } + + addMessageText(QString("%1 QTH %2").arg(selectedCall).arg(m_config.my_qth()), true); + + if(m_config.transmit_directed()) toggleTx(true); + }); + + auto grid = m_config.my_grid(); + auto gridAction = menu->addAction(QString("%1 GRID %2 - Send my current station grid location").arg(call).arg(grid).trimmed()); + connect(gridAction, &QAction::triggered, this, [this](){ + + QString selectedCall = callsignSelected(); + if(selectedCall.isEmpty()){ + return; + } + + addMessageText(QString("%1 GRID %2").arg(selectedCall).arg(m_config.my_grid()), true); + + if(m_config.transmit_directed()) toggleTx(true); + }); + + menu->addSeparator(); auto agnAction = menu->addAction(QString("%1 AGN? - Please repeat your last transmission").arg(call).trimmed()); @@ -7908,8 +7995,9 @@ void MainWindow::handle_transceiver_update (Transceiver::TransceiverState const& } } - if (m_config.spot_to_psk_reporter ()) { - pskSetLocal (); + if (m_config.spot_to_reporting_networks ()) { + pskSetLocal(); + aprsSetLocal(); } statusChanged(); m_wideGraph->setDialFreq(m_freqNominal / 1.e6); @@ -8167,6 +8255,11 @@ void MainWindow::pskSetLocal () m_config.my_station(), QString {"FT8Call v" + version() }.simplified ()); } +void MainWindow::aprsSetLocal () +{ + m_aprsClient->setLocalStation(Radio::base_callsign(m_config.my_callsign()), m_config.my_grid()); +} + void MainWindow::transmitDisplay (bool transmitting) { if (transmitting == m_transmitting) { @@ -8979,15 +9072,20 @@ void MainWindow::processCommandActivity() { else if (d.cmd == "@" && !isAllCall) { QString qth = m_config.my_qth(); if (qth.isEmpty()) { - QString grid = m_config.my_grid(); - if (grid.isEmpty()) { - continue; - } - qth = grid; + continue; } reply = QString("%1 QTH %2").arg(d.from).arg(qth); } + // QUERIED GRID + else if (d.cmd == "^" && !isAllCall) { + QString grid = m_config.my_grid(); + if (grid.isEmpty()) { + continue; + } + + reply = QString("%1 GRID %2").arg(d.from).arg(grid); + } // QUERIED STATION MESSAGE else if (d.cmd == "&" && !isAllCall) { reply = QString("%1 QTC %2").arg(d.from).arg(m_config.my_station()); @@ -9031,9 +9129,30 @@ void MainWindow::processCommandActivity() { else if (d.cmd == "#" && !isAllCall) { reply = QString("%1 ACK").arg(d.from); } + // PROCESS AGN + else if (d.cmd == " AGN?" && !isAllCall && !m_lastTxMessage.isEmpty()) { + reply = m_lastTxMessage; + } + // PROCESS BUFFERED QTH + else if (d.cmd == " GRID"){ + // 1. parse grids + // 2. log it to reporting networks + auto grids = Varicode::parseGrids(d.text); + foreach(auto grid, grids){ + CallDetail cd = {}; + cd.bits = d.bits; + cd.call = d.from; + cd.freq = d.freq; + cd.grid = grid; + cd.snr = d.snr; + cd.utcTimestamp = d.utcTimestamp; + logCallActivity(cd, true); + } + + reply = QString("%1 ACK").arg(d.from); + } // PROCESS ALERT else if (d.cmd == "!" && !isAllCall) { - QMessageBox * msgBox = new QMessageBox(this); msgBox->setIcon(QMessageBox::Information); @@ -9056,10 +9175,6 @@ void MainWindow::processCommandActivity() { }); msgBox->show(); - - continue; - } else if (d.cmd == " AGN?" && !isAllCall && !m_lastTxMessage.isEmpty()) { - reply = m_lastTxMessage; } if (reply.isEmpty()) { @@ -9098,14 +9213,16 @@ void MainWindow::processSpots() { // Process spots to be sent... pskSetLocal(); + aprsSetLocal(); while(!m_rxCallQueue.isEmpty()){ CallDetail d = m_rxCallQueue.dequeue(); if(d.call.isEmpty()){ continue; } - qDebug() << "spotting call to psk reporter" << d.call << d.snr << d.freq; + qDebug() << "spotting call to reporting networks" << d.call << d.snr << d.freq; pskLogReport("FT8CALL", d.freq, d.snr, d.call, d.grid); + aprsLogReport(d.freq, d.snr, d.call, d.grid); } } diff --git a/mainwindow.h b/mainwindow.h index 6a4694f..a4ea39c 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -44,6 +44,7 @@ #include "qpriorityqueue.h" #include "varicode.h" #include "MessageClient.hpp" +#include "APRSISClient.h" #define NUM_JT4_SYMBOLS 206 //(72+31)*2, embedded sync #define NUM_JT65_SYMBOLS 126 //63 data + 63 sync @@ -167,7 +168,7 @@ private slots: void on_actionShow_Waterfall_triggered(bool checked); void on_actionReset_Window_Sizes_triggered(); void on_actionSettings_triggered(); - void preparePSKReporter(); + void prepareSpotting(); void on_spotButton_clicked(bool checked); void on_monitorButton_clicked (bool); void on_actionAbout_triggered(); @@ -805,6 +806,7 @@ private: QTimer m_heartbeat; MessageClient * m_messageClient; PSK_Reporter *psk_Reporter; + APRSISClient * m_aprsClient; DisplayManual m_manual; QHash m_pwrBandTxMemory; // Remembers power level by band QHash m_pwrBandTuneMemory; // Remembers power level by band for tuning @@ -831,8 +833,10 @@ private: void transmit (double snr = 99.); void rigFailure (QString const& reason); void pskSetLocal (); + void aprsSetLocal (); void pskPost(DecodedText const& decodedtext); void pskLogReport(QString mode, int offset, int snr, QString callsign, QString grid); + void aprsLogReport(int offset, int snr, QString callsign, QString grid); Radio::Frequency dialFrequency(); void displayDialFrequency (); void transmitDisplay (bool); diff --git a/mainwindow.ui b/mainwindow.ui index ea04b1f..765dc3f 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -1250,6 +1250,9 @@ QTextEdit[transmitting="true"] { + + false + 0 @@ -1301,15 +1304,15 @@ QTextEdit[transmitting="true"] { + + false + 0 30 - - true - <html><head/><body><p>Send your station message</p></body></html> diff --git a/varicode.cpp b/varicode.cpp index 872c566..85a8a76 100644 --- a/varicode.cpp +++ b/varicode.cpp @@ -30,7 +30,7 @@ const int nalphabet = 41; QString alphabet = {"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ+-./?"}; // alphabet to encode _into_ for FT8 freetext transmission QString alphabet72 = {"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-+/?."}; -QString grid_pattern = {R"((?[A-R]{2}[0-9]{2})+)"}; +QString grid_pattern = {R"((?[A-X]{2}[0-9]{2}(?:[A-X]{2}(?:[0-9]{2})?)*)+)"}; QString orig_compound_callsign_pattern = {R"((?(\d|[A-Z])+\/?((\d|[A-Z]){2,})(\/(\d|[A-Z])+)?(\/(\d|[A-Z])+)?))"}; QString compound_callsign_pattern = {R"((?\b(?[A-Z0-9]{1,4}\/)?(?([0-9A-Z])?([0-9A-Z])([0-9])([A-Z])?([A-Z])?([A-Z])?)(?\/[A-Z0-9]{1,4})?)\b)"}; QString pack_callsign_pattern = {R"(([0-9A-Z ])([0-9A-Z])([0-9])([A-Z ])([A-Z ])([A-Z ]))"}; @@ -44,6 +44,7 @@ QMap directed_cmds = { {"@", 1 }, // query qth {"&", 2 }, // query station message {"$", 3 }, // query station(s) heard + {"^", 4 }, // query grid {"%", 5 }, // query pwr {"|", 6 }, // retransmit message @@ -54,10 +55,11 @@ QMap directed_cmds = { // {"/", 10 }, // unused? (can we even use stroke?) // directed responses - {" QTC", 16 }, // this is my qtc - {" QTH", 17 }, // this is my qth + {" GRID", 15 }, // this is my current grid locator + {" QTC", 16 }, // this is my qtc message + {" QTH", 17 }, // this is my qth message {" FB", 18 }, // fine business - {" HW CPY?", 19 }, // how do you copy? + {" HW CPY?", 19 }, // how do you copy? {" HEARING", 20 }, // i am hearing the following stations {" RR", 21 }, // roger roger {" QSL?", 22 }, // do you copy? @@ -72,12 +74,12 @@ QMap directed_cmds = { {" ", 31 }, // send freetext }; -QSet allowed_cmds = {0, 1, 2, 3, /*4,*/ 5, 6, 7, 8, /*...*/ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; +QSet allowed_cmds = {0, 1, 2, 3, 4, 5, 6, 7, 8, /*...*/ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; -QSet buffered_cmds = {6, 7, 8}; +QSet buffered_cmds = {6, 7, 8, 15}; QString callsign_pattern = QString("(?[A-Z0-9/]+)"); -QString optional_cmd_pattern = QString("(?\\s?(?:AGN[?]|ACK|73|YES|NO|SNR|PWR|QSL[?]?|RR|HEARING|HW CPY[?]|FB|QTH|QTC|[?@&$%|!# ]))?"); +QString optional_cmd_pattern = QString("(?\\s?(?:AGN[?]|ACK|73|YES|NO|SNR|PWR|QSL[?]?|RR|HEARING|HW CPY[?]|FB|QTH|QTC|GRID|[?@&$%|!#^ ]))?"); QString optional_grid_pattern = QString("(?\\s?[A-R]{2}[0-9]{2})?"); QString optional_extended_grid_pattern = QString("^(?\\s?(?:[A-R]{2}[0-9]{2}(?:[A-X]{2}(?:[0-9]{2})?)*))?"); QString optional_pwr_pattern = QString("(?(?<=PWR)\\s?\\d+\\s?[KM]?W)?"); @@ -951,7 +953,7 @@ QString Varicode::unpackCallsign(quint32 value){ return callsign; } -QString deg2grid(float dlong, float dlat){ +QString Varicode::deg2grid(float dlong, float dlat){ QChar grid[6]; if(dlong < -180){ @@ -984,7 +986,7 @@ QString deg2grid(float dlong, float dlat){ return QString(grid, 6); } -QPair grid2deg(QString const &grid){ +QPair Varicode::grid2deg(QString const &grid){ QPair longLat; QString g = grid; @@ -1016,7 +1018,7 @@ quint16 Varicode::packGrid(QString const& value){ return (1<<15)-1; } - auto pair = grid2deg(grid.left(4)); + auto pair = Varicode::grid2deg(grid.left(4)); int ilong = pair.first; int ilat = pair.second + 90; @@ -1031,7 +1033,7 @@ QString Varicode::unpackGrid(quint16 value){ float dlat = value % 180 - 90; float dlong = value / 180 * 2 - 180 + 2; - return deg2grid(dlong, dlat).left(4); + return Varicode::deg2grid(dlong, dlat).left(4); } // pack a number or snr into an integer between 0 & 62 diff --git a/varicode.h b/varicode.h index 5334dac..3ec2991 100644 --- a/varicode.h +++ b/varicode.h @@ -109,6 +109,8 @@ public: static quint32 packCallsign(QString const& value); static QString unpackCallsign(quint32 value); + static QString deg2grid(float dlong, float dlat); + static QPair grid2deg(QString const &grid); static quint16 packGrid(QString const& value); static QString unpackGrid(quint16 value); diff --git a/wsjtx.pro b/wsjtx.pro index 0fc9aee..f070f20 100644 --- a/wsjtx.pro +++ b/wsjtx.pro @@ -71,7 +71,8 @@ SOURCES += \ varicode.cpp \ NetworkMessage.cpp \ MessageClient.cpp \ - SelfDestructMessageBox.cpp + SelfDestructMessageBox.cpp \ + APRSISClient.cpp HEADERS += qt_helpers.hpp \ pimpl_h.hpp pimpl_impl.hpp \ @@ -94,7 +95,8 @@ HEADERS += qt_helpers.hpp \ crc.h \ NetworkMessage.hpp \ MessageClient.hpp \ - SelfDestructMessageBox.h + SelfDestructMessageBox.h \ + APRSISClient.h INCLUDEPATH += qmake_only