diff --git a/CMakeLists.txt b/CMakeLists.txt index 0b13074bd..ec7e299ac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -308,6 +308,7 @@ if (BUILD_GUI) find_package(Qt5 COMPONENTS Quick) find_package(Qt5 COMPONENTS QuickWidgets) find_package(Qt5 COMPONENTS Positioning) + find_package(Qt5 COMPONENTS Charts) endif() # other requirements diff --git a/doc/img/APRS_plugin.png b/doc/img/APRS_plugin.png new file mode 100644 index 000000000..c3f5c18ad Binary files /dev/null and b/doc/img/APRS_plugin.png differ diff --git a/plugins/feature/aprs/CMakeLists.txt b/plugins/feature/aprs/CMakeLists.txt new file mode 100644 index 000000000..30d66a3ed --- /dev/null +++ b/plugins/feature/aprs/CMakeLists.txt @@ -0,0 +1,67 @@ +project(aprs) + +set(aprs_SOURCES + aprs.cpp + aprssettings.cpp + aprsplugin.cpp + aprsworker.cpp + aprswebapiadapter.cpp +) + +set(aprs_HEADERS + aprs.h + aprssettings.h + aprsplugin.h + aprsreport.h + aprsworker.h + aprswebapiadapter.h +) + +include_directories( + ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client +) + +if(NOT SERVER_MODE) + set(aprs_SOURCES + ${aprs_SOURCES} + aprsgui.cpp + aprsgui.ui + aprssettingsdialog.cpp + aprssettingsdialog.ui + aprs.qrc + ) + set(aprs_HEADERS + ${aprs_HEADERS} + aprsgui.h + aprssettingsdialog.h + ) + + set(TARGET_NAME aprs) + set(TARGET_LIB "Qt5::Widgets" Qt5::Charts) + set(TARGET_LIB_GUI "sdrgui") + set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR}) +else() + set(TARGET_NAME aprssrv) + set(TARGET_LIB "") + set(TARGET_LIB_GUI "") + set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR}) +endif() + +add_library(${TARGET_NAME} SHARED + ${aprs_SOURCES} +) + +target_link_libraries(${TARGET_NAME} + Qt5::Core + ${TARGET_LIB} + sdrbase + ${TARGET_LIB_GUI} +) + +install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER}) + +if(WIN32) + # Run deployqt for Charts etc + include(DeployQt) + windeployqt(${TARGET_NAME} ${SDRANGEL_BINARY_BIN_DIR} ${PROJECT_SOURCE_DIR}/aprs) +endif() diff --git a/plugins/feature/aprs/aprs.cpp b/plugins/feature/aprs/aprs.cpp new file mode 100644 index 000000000..ab6a30c59 --- /dev/null +++ b/plugins/feature/aprs/aprs.cpp @@ -0,0 +1,413 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include + +#include "SWGFeatureSettings.h" +#include "SWGFeatureReport.h" +#include "SWGFeatureActions.h" +#include "SWGDeviceState.h" + +#include "dsp/dspengine.h" + +#include "device/deviceset.h" +#include "channel/channelapi.h" +#include "maincore.h" +#include "aprsworker.h" +#include "aprs.h" + +MESSAGE_CLASS_DEFINITION(APRS::MsgConfigureAPRS, Message) +MESSAGE_CLASS_DEFINITION(APRS::MsgReportWorker, Message) + +const char* const APRS::m_featureIdURI = "sdrangel.feature.aprs"; +const char* const APRS::m_featureId = "APRS"; + +APRS::APRS(WebAPIAdapterInterface *webAPIAdapterInterface) : + Feature(m_featureIdURI, webAPIAdapterInterface) +{ + qDebug("APRS::APRS: webAPIAdapterInterface: %p", webAPIAdapterInterface); + setObjectName(m_featureId); + m_worker = new APRSWorker(this, webAPIAdapterInterface); + m_state = StIdle; + m_errorMessage = "APRS error"; + connect(&m_updatePipesTimer, SIGNAL(timeout()), this, SLOT(updatePipes())); + m_updatePipesTimer.start(1000); +} + +APRS::~APRS() +{ + if (m_worker->isRunning()) { + stop(); + } + + delete m_worker; +} + +void APRS::start() +{ + qDebug("APRS::start"); + m_worker->reset(); + m_worker->setMessageQueueToFeature(getInputMessageQueue()); + m_worker->setMessageQueueToGUI(getMessageQueueToGUI()); + bool ok = m_worker->startWork(); + m_state = ok ? StIdle : StError; + m_thread.start(); + + APRSWorker::MsgConfigureAPRSWorker *msg = APRSWorker::MsgConfigureAPRSWorker::create(m_settings, true); + m_worker->getInputMessageQueue()->push(msg); +} + +void APRS::stop() +{ + qDebug("APRS::stop"); + m_worker->stopWork(); + m_state = StIdle; + m_thread.quit(); + m_thread.wait(); +} + +bool APRS::handleMessage(const Message& cmd) +{ + if (MsgConfigureAPRS::match(cmd)) + { + MsgConfigureAPRS& cfg = (MsgConfigureAPRS&) cmd; + qDebug() << "APRS::handleMessage: MsgConfigureAPRS"; + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (MsgReportWorker::match(cmd)) + { + MsgReportWorker& report = (MsgReportWorker&) cmd; + if (report.getMessage() == "Connected") + m_state = StRunning; + else if (report.getMessage() == "Disconnected") + m_state = StIdle; + else + { + m_state = StError; + m_errorMessage = report.getMessage(); + } + return true; + } + else if (MainCore::MsgPacket::match(cmd)) + { + MainCore::MsgPacket& report = (MainCore::MsgPacket&) cmd; + if (getMessageQueueToGUI()) + { + MainCore::MsgPacket *copy = new MainCore::MsgPacket(report); + getMessageQueueToGUI()->push(copy); + } + if (m_state == StRunning) + { + MainCore::MsgPacket *copy = new MainCore::MsgPacket(report); + m_worker->getInputMessageQueue()->push(copy); + } + return true; + } + else + { + return false; + } +} + +void APRS::updatePipes() +{ + QList availablePipes = updateAvailablePipeSources("packets", APRSSettings::m_pipeTypes, APRSSettings::m_pipeURIs, this); + + if (availablePipes != m_availablePipes) + { + m_availablePipes = availablePipes; + if (getMessageQueueToGUI()) + { + MsgReportPipes *msgToGUI = MsgReportPipes::create(); + QList& msgAvailablePipes = msgToGUI->getAvailablePipes(); + msgAvailablePipes.append(availablePipes); + getMessageQueueToGUI()->push(msgToGUI); + } + } +} + +QByteArray APRS::serialize() const +{ + return m_settings.serialize(); +} + +bool APRS::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + MsgConfigureAPRS *msg = MsgConfigureAPRS::create(m_settings, true); + m_inputMessageQueue.push(msg); + return true; + } + else + { + m_settings.resetToDefaults(); + MsgConfigureAPRS *msg = MsgConfigureAPRS::create(m_settings, true); + m_inputMessageQueue.push(msg); + return false; + } +} + +void APRS::applySettings(const APRSSettings& settings, bool force) +{ + qDebug() << "APRS::applySettings:" + << " m_igateEnabled: " << settings.m_igateEnabled + << " m_title: " << settings.m_title + << " m_rgbColor: " << settings.m_rgbColor + << " m_useReverseAPI: " << settings.m_useReverseAPI + << " m_reverseAPIAddress: " << settings.m_reverseAPIAddress + << " m_reverseAPIPort: " << settings.m_reverseAPIPort + << " m_reverseAPIFeatureSetIndex: " << settings.m_reverseAPIFeatureSetIndex + << " m_reverseAPIFeatureIndex: " << settings.m_reverseAPIFeatureIndex + << " force: " << force; + + QList reverseAPIKeys; + + if ((m_settings.m_igateEnabled != settings.m_igateEnabled) || force) + { + if (settings.m_igateEnabled) + start(); + else + stop(); + reverseAPIKeys.append("igateEnabled"); + } + + if ((m_settings.m_title != settings.m_title) || force) { + reverseAPIKeys.append("title"); + } + if ((m_settings.m_rgbColor != settings.m_rgbColor) || force) { + reverseAPIKeys.append("rgbColor"); + } + + APRSWorker::MsgConfigureAPRSWorker *msg = APRSWorker::MsgConfigureAPRSWorker::create( + settings, force + ); + m_worker->getInputMessageQueue()->push(msg); + + if (settings.m_useReverseAPI) + { + bool fullUpdate = ((m_settings.m_useReverseAPI != settings.m_useReverseAPI) && settings.m_useReverseAPI) || + (m_settings.m_reverseAPIAddress != settings.m_reverseAPIAddress) || + (m_settings.m_reverseAPIPort != settings.m_reverseAPIPort) || + (m_settings.m_reverseAPIFeatureSetIndex != settings.m_reverseAPIFeatureSetIndex) || + (m_settings.m_reverseAPIFeatureIndex != settings.m_reverseAPIFeatureIndex); + webapiReverseSendSettings(reverseAPIKeys, settings, fullUpdate || force); + } + + m_settings = settings; +} + +int APRS::webapiRun(bool run, + SWGSDRangel::SWGDeviceState& response, + QString& errorMessage) +{ + (void) errorMessage; + //getFeatureStateStr(*response.getState()); + //MsgStartStopIGate *msg = MsgStartStopIGate::create(run); + //getInputMessageQueue()->push(msg); + return 202; +} + +int APRS::webapiSettingsGet( + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setAprsSettings(new SWGSDRangel::SWGAPRSSettings()); + response.getAprsSettings()->init(); + webapiFormatFeatureSettings(response, m_settings); + return 200; +} + +int APRS::webapiSettingsPutPatch( + bool force, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + APRSSettings settings = m_settings; + webapiUpdateFeatureSettings(settings, featureSettingsKeys, response); + + MsgConfigureAPRS *msg = MsgConfigureAPRS::create(settings, force); + m_inputMessageQueue.push(msg); + + qDebug("APRS::webapiSettingsPutPatch: forward to GUI: %p", m_guiMessageQueue); + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureAPRS *msgToGUI = MsgConfigureAPRS::create(settings, force); + m_guiMessageQueue->push(msgToGUI); + } + + webapiFormatFeatureSettings(response, settings); + + return 200; +} + +void APRS::webapiFormatFeatureSettings( + SWGSDRangel::SWGFeatureSettings& response, + const APRSSettings& settings) +{ + response.getAprsSettings()->setIgateServer(new QString(settings.m_igateServer)); + response.getAprsSettings()->setIgatePort(settings.m_igatePort); + response.getAprsSettings()->setIgateCallsign(new QString(settings.m_igateCallsign)); + response.getAprsSettings()->setIgatePasscode(new QString(settings.m_igatePasscode)); + response.getAprsSettings()->setIgateFilter(new QString(settings.m_igateFilter)); + response.getAprsSettings()->setIgateEnabled(settings.m_igateEnabled ? 1 : 0); + + if (response.getAprsSettings()->getTitle()) { + *response.getAprsSettings()->getTitle() = settings.m_title; + } else { + response.getAprsSettings()->setTitle(new QString(settings.m_title)); + } + + response.getAprsSettings()->setRgbColor(settings.m_rgbColor); + response.getAprsSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0); + + if (response.getAprsSettings()->getReverseApiAddress()) { + *response.getAprsSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress; + } else { + response.getAprsSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress)); + } + + response.getAprsSettings()->setReverseApiPort(settings.m_reverseAPIPort); + response.getAprsSettings()->setReverseApiDeviceIndex(settings.m_reverseAPIFeatureSetIndex); + response.getAprsSettings()->setReverseApiChannelIndex(settings.m_reverseAPIFeatureIndex); +} + +void APRS::webapiUpdateFeatureSettings( + APRSSettings& settings, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response) +{ + if (featureSettingsKeys.contains("igateServer")) { + settings.m_igateServer = *response.getAprsSettings()->getIgateServer(); + } + if (featureSettingsKeys.contains("igatePort")) { + settings.m_igatePort = response.getAprsSettings()->getIgatePort(); + } + if (featureSettingsKeys.contains("igateCallsign")) { + settings.m_igateCallsign = *response.getAprsSettings()->getIgateCallsign(); + } + if (featureSettingsKeys.contains("igatePasscode")) { + settings.m_igatePasscode = *response.getAprsSettings()->getIgatePasscode(); + } + if (featureSettingsKeys.contains("igateFilter")) { + settings.m_igateFilter = *response.getAprsSettings()->getIgateFilter(); + } + if (featureSettingsKeys.contains("title")) { + settings.m_title = *response.getAprsSettings()->getTitle(); + } + if (featureSettingsKeys.contains("rgbColor")) { + settings.m_rgbColor = response.getAprsSettings()->getRgbColor(); + } + if (featureSettingsKeys.contains("useReverseAPI")) { + settings.m_useReverseAPI = response.getAprsSettings()->getUseReverseApi() != 0; + } + if (featureSettingsKeys.contains("reverseAPIAddress")) { + settings.m_reverseAPIAddress = *response.getAprsSettings()->getReverseApiAddress(); + } + if (featureSettingsKeys.contains("reverseAPIPort")) { + settings.m_reverseAPIPort = response.getAprsSettings()->getReverseApiPort(); + } + if (featureSettingsKeys.contains("reverseAPIDeviceIndex")) { + settings.m_reverseAPIFeatureSetIndex = response.getAprsSettings()->getReverseApiDeviceIndex(); + } + if (featureSettingsKeys.contains("reverseAPIChannelIndex")) { + settings.m_reverseAPIFeatureIndex = response.getAprsSettings()->getReverseApiChannelIndex(); + } +} + +void APRS::webapiReverseSendSettings(QList& featureSettingsKeys, const APRSSettings& settings, bool force) +{ + SWGSDRangel::SWGFeatureSettings *swgFeatureSettings = new SWGSDRangel::SWGFeatureSettings(); + // swgFeatureSettings->setOriginatorFeatureIndex(getIndexInDeviceSet()); + // swgFeatureSettings->setOriginatorFeatureSetIndex(getDeviceSetIndex()); + swgFeatureSettings->setFeatureType(new QString("APRS")); + swgFeatureSettings->setAprsSettings(new SWGSDRangel::SWGAPRSSettings()); + SWGSDRangel::SWGAPRSSettings *swgAPRSSettings = swgFeatureSettings->getAprsSettings(); + + // transfer data that has been modified. When force is on transfer all data except reverse API data + + if (featureSettingsKeys.contains("igateServer") || force) { + swgAPRSSettings->setIgateServer(new QString(settings.m_igateServer)); + } + if (featureSettingsKeys.contains("igatePort") || force) { + swgAPRSSettings->setIgatePort(settings.m_igatePort); + } + if (featureSettingsKeys.contains("igateCallsign") || force) { + swgAPRSSettings->setIgateCallsign(new QString(settings.m_igateCallsign)); + } + if (featureSettingsKeys.contains("igatePasscode") || force) { + swgAPRSSettings->setIgatePasscode(new QString(settings.m_igatePasscode)); + } + if (featureSettingsKeys.contains("igateFilter") || force) { + swgAPRSSettings->setIgateFilter(new QString(settings.m_igateFilter)); + } + if (featureSettingsKeys.contains("title") || force) { + swgAPRSSettings->setTitle(new QString(settings.m_title)); + } + if (featureSettingsKeys.contains("rgbColor") || force) { + swgAPRSSettings->setRgbColor(settings.m_rgbColor); + } + + QString channelSettingsURL = QString("http://%1:%2/sdrangel/featureset/%3/feature/%4/settings") + .arg(settings.m_reverseAPIAddress) + .arg(settings.m_reverseAPIPort) + .arg(settings.m_reverseAPIFeatureSetIndex) + .arg(settings.m_reverseAPIFeatureIndex); + m_networkRequest.setUrl(QUrl(channelSettingsURL)); + m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + QBuffer *buffer = new QBuffer(); + buffer->open((QBuffer::ReadWrite)); + buffer->write(swgFeatureSettings->asJson().toUtf8()); + buffer->seek(0); + + // Always use PATCH to avoid passing reverse API settings + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); + + delete swgFeatureSettings; +} + +void APRS::networkManagerFinished(QNetworkReply *reply) +{ + QNetworkReply::NetworkError replyError = reply->error(); + + if (replyError) + { + qWarning() << "APRS::networkManagerFinished:" + << " error(" << (int) replyError + << "): " << replyError + << ": " << reply->errorString(); + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("APRS::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + } + + reply->deleteLater(); +} diff --git a/plugins/feature/aprs/aprs.h b/plugins/feature/aprs/aprs.h new file mode 100644 index 000000000..760b80721 --- /dev/null +++ b/plugins/feature/aprs/aprs.h @@ -0,0 +1,143 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_APRS_H_ +#define INCLUDE_FEATURE_APRS_H_ + +#include +#include +#include +#include + +#include "feature/feature.h" +#include "util/message.h" + +#include "aprssettings.h" + +class WebAPIAdapterInterface; +class APRSWorker; +class QNetworkAccessManager; +class QNetworkReply; + +namespace SWGSDRangel { + class SWGDeviceState; +} + +class APRS : public Feature +{ + Q_OBJECT +public: + class MsgConfigureAPRS : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const APRSSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureAPRS* create(const APRSSettings& settings, bool force) { + return new MsgConfigureAPRS(settings, force); + } + + private: + APRSSettings m_settings; + bool m_force; + + MsgConfigureAPRS(const APRSSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + class MsgReportWorker : public Message { + MESSAGE_CLASS_DECLARATION + + public: + QString getMessage() { return m_message; } + + static MsgReportWorker* create(QString message) { + return new MsgReportWorker(message); + } + + private: + QString m_message; + + MsgReportWorker(QString message) : + Message(), + m_message(message) + {} + }; + + APRS(WebAPIAdapterInterface *webAPIAdapterInterface); + virtual ~APRS(); + virtual void destroy() { delete this; } + virtual bool handleMessage(const Message& cmd); + + virtual void getIdentifier(QString& id) const { id = objectName(); } + virtual void getTitle(QString& title) const { title = m_settings.m_title; } + + virtual QByteArray serialize() const; + virtual bool deserialize(const QByteArray& data); + + virtual int webapiRun(bool run, + SWGSDRangel::SWGDeviceState& response, + QString& errorMessage); + + virtual int webapiSettingsGet( + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage); + + static void webapiFormatFeatureSettings( + SWGSDRangel::SWGFeatureSettings& response, + const APRSSettings& settings); + + static void webapiUpdateFeatureSettings( + APRSSettings& settings, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response); + + static const char* const m_featureIdURI; + static const char* const m_featureId; + +private: + QThread m_thread; + APRSWorker *m_worker; + APRSSettings m_settings; + QList m_availablePipes; + QTimer m_updatePipesTimer; + + QNetworkAccessManager *m_networkManager; + QNetworkRequest m_networkRequest; + + void start(); + void stop(); + void applySettings(const APRSSettings& settings, bool force = false); + void webapiReverseSendSettings(QList& featureSettingsKeys, const APRSSettings& settings, bool force); + +private slots: + void updatePipes(); + void networkManagerFinished(QNetworkReply *reply); +}; + +#endif // INCLUDE_FEATURE_APRS_H_ diff --git a/plugins/feature/aprs/aprs.qrc b/plugins/feature/aprs/aprs.qrc new file mode 100644 index 000000000..5bd0fca3f --- /dev/null +++ b/plugins/feature/aprs/aprs.qrc @@ -0,0 +1,196 @@ + + + aprs/aprs-symbols-24-0-00.png + aprs/aprs-symbols-24-0-01.png + aprs/aprs-symbols-24-0-02.png + aprs/aprs-symbols-24-0-03.png + aprs/aprs-symbols-24-0-04.png + aprs/aprs-symbols-24-0-05.png + aprs/aprs-symbols-24-0-06.png + aprs/aprs-symbols-24-0-07.png + aprs/aprs-symbols-24-0-08.png + aprs/aprs-symbols-24-0-09.png + aprs/aprs-symbols-24-0-10.png + aprs/aprs-symbols-24-0-11.png + aprs/aprs-symbols-24-0-12.png + aprs/aprs-symbols-24-0-13.png + aprs/aprs-symbols-24-0-14.png + aprs/aprs-symbols-24-0-15.png + aprs/aprs-symbols-24-0-16.png + aprs/aprs-symbols-24-0-17.png + aprs/aprs-symbols-24-0-18.png + aprs/aprs-symbols-24-0-19.png + aprs/aprs-symbols-24-0-20.png + aprs/aprs-symbols-24-0-21.png + aprs/aprs-symbols-24-0-22.png + aprs/aprs-symbols-24-0-23.png + aprs/aprs-symbols-24-0-24.png + aprs/aprs-symbols-24-0-25.png + aprs/aprs-symbols-24-0-26.png + aprs/aprs-symbols-24-0-27.png + aprs/aprs-symbols-24-0-28.png + aprs/aprs-symbols-24-0-29.png + aprs/aprs-symbols-24-0-30.png + aprs/aprs-symbols-24-0-31.png + aprs/aprs-symbols-24-0-32.png + aprs/aprs-symbols-24-0-33.png + aprs/aprs-symbols-24-0-34.png + aprs/aprs-symbols-24-0-35.png + aprs/aprs-symbols-24-0-36.png + aprs/aprs-symbols-24-0-37.png + aprs/aprs-symbols-24-0-38.png + aprs/aprs-symbols-24-0-39.png + aprs/aprs-symbols-24-0-40.png + aprs/aprs-symbols-24-0-41.png + aprs/aprs-symbols-24-0-42.png + aprs/aprs-symbols-24-0-43.png + aprs/aprs-symbols-24-0-44.png + aprs/aprs-symbols-24-0-45.png + aprs/aprs-symbols-24-0-46.png + aprs/aprs-symbols-24-0-47.png + aprs/aprs-symbols-24-0-48.png + aprs/aprs-symbols-24-0-49.png + aprs/aprs-symbols-24-0-50.png + aprs/aprs-symbols-24-0-51.png + aprs/aprs-symbols-24-0-52.png + aprs/aprs-symbols-24-0-53.png + aprs/aprs-symbols-24-0-54.png + aprs/aprs-symbols-24-0-55.png + aprs/aprs-symbols-24-0-56.png + aprs/aprs-symbols-24-0-57.png + aprs/aprs-symbols-24-0-58.png + aprs/aprs-symbols-24-0-59.png + aprs/aprs-symbols-24-0-60.png + aprs/aprs-symbols-24-0-61.png + aprs/aprs-symbols-24-0-62.png + aprs/aprs-symbols-24-0-63.png + aprs/aprs-symbols-24-0-64.png + aprs/aprs-symbols-24-0-65.png + aprs/aprs-symbols-24-0-66.png + aprs/aprs-symbols-24-0-67.png + aprs/aprs-symbols-24-0-68.png + aprs/aprs-symbols-24-0-69.png + aprs/aprs-symbols-24-0-70.png + aprs/aprs-symbols-24-0-71.png + aprs/aprs-symbols-24-0-72.png + aprs/aprs-symbols-24-0-73.png + aprs/aprs-symbols-24-0-74.png + aprs/aprs-symbols-24-0-75.png + aprs/aprs-symbols-24-0-76.png + aprs/aprs-symbols-24-0-77.png + aprs/aprs-symbols-24-0-78.png + aprs/aprs-symbols-24-0-79.png + aprs/aprs-symbols-24-0-80.png + aprs/aprs-symbols-24-0-81.png + aprs/aprs-symbols-24-0-82.png + aprs/aprs-symbols-24-0-83.png + aprs/aprs-symbols-24-0-84.png + aprs/aprs-symbols-24-0-85.png + aprs/aprs-symbols-24-0-86.png + aprs/aprs-symbols-24-0-87.png + aprs/aprs-symbols-24-0-88.png + aprs/aprs-symbols-24-0-89.png + aprs/aprs-symbols-24-0-90.png + aprs/aprs-symbols-24-0-91.png + aprs/aprs-symbols-24-0-92.png + aprs/aprs-symbols-24-0-93.png + aprs/aprs-symbols-24-0-94.png + aprs/aprs-symbols-24-0-95.png + aprs/aprs-symbols-24-1-00.png + aprs/aprs-symbols-24-1-01.png + aprs/aprs-symbols-24-1-02.png + aprs/aprs-symbols-24-1-03.png + aprs/aprs-symbols-24-1-04.png + aprs/aprs-symbols-24-1-05.png + aprs/aprs-symbols-24-1-06.png + aprs/aprs-symbols-24-1-07.png + aprs/aprs-symbols-24-1-08.png + aprs/aprs-symbols-24-1-09.png + aprs/aprs-symbols-24-1-10.png + aprs/aprs-symbols-24-1-11.png + aprs/aprs-symbols-24-1-12.png + aprs/aprs-symbols-24-1-13.png + aprs/aprs-symbols-24-1-14.png + aprs/aprs-symbols-24-1-15.png + aprs/aprs-symbols-24-1-16.png + aprs/aprs-symbols-24-1-17.png + aprs/aprs-symbols-24-1-18.png + aprs/aprs-symbols-24-1-19.png + aprs/aprs-symbols-24-1-20.png + aprs/aprs-symbols-24-1-21.png + aprs/aprs-symbols-24-1-22.png + aprs/aprs-symbols-24-1-23.png + aprs/aprs-symbols-24-1-24.png + aprs/aprs-symbols-24-1-25.png + aprs/aprs-symbols-24-1-26.png + aprs/aprs-symbols-24-1-27.png + aprs/aprs-symbols-24-1-28.png + aprs/aprs-symbols-24-1-29.png + aprs/aprs-symbols-24-1-30.png + aprs/aprs-symbols-24-1-31.png + aprs/aprs-symbols-24-1-32.png + aprs/aprs-symbols-24-1-33.png + aprs/aprs-symbols-24-1-34.png + aprs/aprs-symbols-24-1-35.png + aprs/aprs-symbols-24-1-36.png + aprs/aprs-symbols-24-1-37.png + aprs/aprs-symbols-24-1-38.png + aprs/aprs-symbols-24-1-39.png + aprs/aprs-symbols-24-1-40.png + aprs/aprs-symbols-24-1-41.png + aprs/aprs-symbols-24-1-42.png + aprs/aprs-symbols-24-1-43.png + aprs/aprs-symbols-24-1-44.png + aprs/aprs-symbols-24-1-45.png + aprs/aprs-symbols-24-1-46.png + aprs/aprs-symbols-24-1-47.png + aprs/aprs-symbols-24-1-48.png + aprs/aprs-symbols-24-1-49.png + aprs/aprs-symbols-24-1-50.png + aprs/aprs-symbols-24-1-51.png + aprs/aprs-symbols-24-1-52.png + aprs/aprs-symbols-24-1-53.png + aprs/aprs-symbols-24-1-54.png + aprs/aprs-symbols-24-1-55.png + aprs/aprs-symbols-24-1-56.png + aprs/aprs-symbols-24-1-57.png + aprs/aprs-symbols-24-1-58.png + aprs/aprs-symbols-24-1-59.png + aprs/aprs-symbols-24-1-60.png + aprs/aprs-symbols-24-1-61.png + aprs/aprs-symbols-24-1-62.png + aprs/aprs-symbols-24-1-63.png + aprs/aprs-symbols-24-1-64.png + aprs/aprs-symbols-24-1-65.png + aprs/aprs-symbols-24-1-66.png + aprs/aprs-symbols-24-1-67.png + aprs/aprs-symbols-24-1-68.png + aprs/aprs-symbols-24-1-69.png + aprs/aprs-symbols-24-1-70.png + aprs/aprs-symbols-24-1-71.png + aprs/aprs-symbols-24-1-72.png + aprs/aprs-symbols-24-1-73.png + aprs/aprs-symbols-24-1-74.png + aprs/aprs-symbols-24-1-75.png + aprs/aprs-symbols-24-1-76.png + aprs/aprs-symbols-24-1-77.png + aprs/aprs-symbols-24-1-78.png + aprs/aprs-symbols-24-1-79.png + aprs/aprs-symbols-24-1-80.png + aprs/aprs-symbols-24-1-81.png + aprs/aprs-symbols-24-1-82.png + aprs/aprs-symbols-24-1-83.png + aprs/aprs-symbols-24-1-84.png + aprs/aprs-symbols-24-1-85.png + aprs/aprs-symbols-24-1-86.png + aprs/aprs-symbols-24-1-87.png + aprs/aprs-symbols-24-1-88.png + aprs/aprs-symbols-24-1-89.png + aprs/aprs-symbols-24-1-90.png + aprs/aprs-symbols-24-1-91.png + aprs/aprs-symbols-24-1-92.png + aprs/aprs-symbols-24-1-93.png + aprs/aprs-symbols-24-1-94.png + aprs/aprs-symbols-24-1-95.png + + diff --git a/plugins/feature/aprs/aprs/README.txt b/plugins/feature/aprs/aprs/README.txt new file mode 100644 index 000000000..f00b46878 --- /dev/null +++ b/plugins/feature/aprs/aprs/README.txt @@ -0,0 +1,6 @@ +APRS images are from: https://github.com/hessu/aprs-symbols/tree/master/png + +To split in to individual files, using ImageMagick: + + convert aprs-symbols-24-0.png -crop 24x24 aprs-symbols-24-0-%02d.png + convert aprs-symbols-24-1.png -crop 24x24 aprs-symbols-24-1-%02d.png diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-00.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-00.png new file mode 100644 index 000000000..ba8651982 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-00.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-01.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-01.png new file mode 100644 index 000000000..735b857cf Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-01.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-02.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-02.png new file mode 100644 index 000000000..db2be5442 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-02.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-03.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-03.png new file mode 100644 index 000000000..3acaa3ab6 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-03.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-04.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-04.png new file mode 100644 index 000000000..069842bfd Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-04.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-05.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-05.png new file mode 100644 index 000000000..c01f35abd Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-05.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-06.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-06.png new file mode 100644 index 000000000..fc2be8b63 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-06.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-07.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-07.png new file mode 100644 index 000000000..b6c7335d9 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-07.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-08.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-08.png new file mode 100644 index 000000000..2b67505fa Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-08.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-09.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-09.png new file mode 100644 index 000000000..4d6e210a7 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-09.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-10.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-10.png new file mode 100644 index 000000000..a442c3c91 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-10.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-11.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-11.png new file mode 100644 index 000000000..5882e2102 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-11.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-12.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-12.png new file mode 100644 index 000000000..7a8826f7f Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-12.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-13.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-13.png new file mode 100644 index 000000000..f1cc2c93b Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-13.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-14.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-14.png new file mode 100644 index 000000000..2e21d9088 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-14.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-15.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-15.png new file mode 100644 index 000000000..64dc61e45 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-15.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-16.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-16.png new file mode 100644 index 000000000..c1f58c656 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-16.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-17.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-17.png new file mode 100644 index 000000000..43b92dae5 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-17.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-18.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-18.png new file mode 100644 index 000000000..0657289e8 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-18.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-19.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-19.png new file mode 100644 index 000000000..90b2751d1 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-19.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-20.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-20.png new file mode 100644 index 000000000..71bf45363 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-20.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-21.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-21.png new file mode 100644 index 000000000..57c46021a Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-21.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-22.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-22.png new file mode 100644 index 000000000..06adffd0a Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-22.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-23.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-23.png new file mode 100644 index 000000000..9b48f7513 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-23.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-24.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-24.png new file mode 100644 index 000000000..3c6d6f1d4 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-24.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-25.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-25.png new file mode 100644 index 000000000..ec1c55c56 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-25.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-26.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-26.png new file mode 100644 index 000000000..a5aedfc1d Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-26.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-27.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-27.png new file mode 100644 index 000000000..a6ed7dd07 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-27.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-28.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-28.png new file mode 100644 index 000000000..dd7123d65 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-28.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-29.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-29.png new file mode 100644 index 000000000..b39cc6f61 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-29.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-30.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-30.png new file mode 100644 index 000000000..14da48e04 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-30.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-31.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-31.png new file mode 100644 index 000000000..c49ece344 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-31.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-32.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-32.png new file mode 100644 index 000000000..6f5d9ce72 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-32.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-33.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-33.png new file mode 100644 index 000000000..46d764cc5 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-33.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-34.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-34.png new file mode 100644 index 000000000..f5315545c Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-34.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-35.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-35.png new file mode 100644 index 000000000..287674ad1 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-35.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-36.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-36.png new file mode 100644 index 000000000..d82122c75 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-36.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-37.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-37.png new file mode 100644 index 000000000..e7442d7ab Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-37.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-38.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-38.png new file mode 100644 index 000000000..dd820d34e Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-38.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-39.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-39.png new file mode 100644 index 000000000..82d90a9a7 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-39.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-40.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-40.png new file mode 100644 index 000000000..7afd9dcd6 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-40.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-41.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-41.png new file mode 100644 index 000000000..3ff2fb09d Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-41.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-42.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-42.png new file mode 100644 index 000000000..2db826329 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-42.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-43.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-43.png new file mode 100644 index 000000000..2a0d6c51d Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-43.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-44.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-44.png new file mode 100644 index 000000000..5398c3fad Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-44.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-45.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-45.png new file mode 100644 index 000000000..5f799ad20 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-45.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-46.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-46.png new file mode 100644 index 000000000..b653bc526 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-46.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-47.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-47.png new file mode 100644 index 000000000..01a0e0c69 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-47.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-48.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-48.png new file mode 100644 index 000000000..776acf044 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-48.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-49.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-49.png new file mode 100644 index 000000000..b96ebd101 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-49.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-50.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-50.png new file mode 100644 index 000000000..3bdcf7fad Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-50.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-51.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-51.png new file mode 100644 index 000000000..82d30e786 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-51.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-52.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-52.png new file mode 100644 index 000000000..efc51b756 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-52.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-53.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-53.png new file mode 100644 index 000000000..c60350892 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-53.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-54.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-54.png new file mode 100644 index 000000000..b731bfc06 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-54.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-55.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-55.png new file mode 100644 index 000000000..82ec76c38 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-55.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-56.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-56.png new file mode 100644 index 000000000..a9f2bfe8b Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-56.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-57.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-57.png new file mode 100644 index 000000000..2c8b77be9 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-57.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-58.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-58.png new file mode 100644 index 000000000..58b229ccf Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-58.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-59.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-59.png new file mode 100644 index 000000000..ec63d7394 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-59.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-60.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-60.png new file mode 100644 index 000000000..8c0475f17 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-60.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-61.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-61.png new file mode 100644 index 000000000..e21c1e990 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-61.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-62.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-62.png new file mode 100644 index 000000000..f15491e46 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-62.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-63.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-63.png new file mode 100644 index 000000000..98454335a Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-63.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-64.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-64.png new file mode 100644 index 000000000..79cb8f25a Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-64.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-65.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-65.png new file mode 100644 index 000000000..72edab89f Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-65.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-66.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-66.png new file mode 100644 index 000000000..3ed0c63cd Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-66.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-67.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-67.png new file mode 100644 index 000000000..d8dcdc891 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-67.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-68.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-68.png new file mode 100644 index 000000000..6e9c27f9c Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-68.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-69.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-69.png new file mode 100644 index 000000000..813ed0556 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-69.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-70.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-70.png new file mode 100644 index 000000000..94f7e4a85 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-70.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-71.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-71.png new file mode 100644 index 000000000..46bf710ae Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-71.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-72.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-72.png new file mode 100644 index 000000000..8b395ce00 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-72.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-73.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-73.png new file mode 100644 index 000000000..7985337fc Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-73.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-74.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-74.png new file mode 100644 index 000000000..e4956530a Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-74.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-75.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-75.png new file mode 100644 index 000000000..ba584a0fc Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-75.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-76.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-76.png new file mode 100644 index 000000000..9a5b1939f Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-76.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-77.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-77.png new file mode 100644 index 000000000..a67ab9979 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-77.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-78.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-78.png new file mode 100644 index 000000000..b95533ffd Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-78.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-79.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-79.png new file mode 100644 index 000000000..d62722509 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-79.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-80.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-80.png new file mode 100644 index 000000000..e3490311a Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-80.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-81.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-81.png new file mode 100644 index 000000000..1d45164c0 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-81.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-82.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-82.png new file mode 100644 index 000000000..465bcb3b1 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-82.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-83.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-83.png new file mode 100644 index 000000000..9054dc5ce Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-83.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-84.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-84.png new file mode 100644 index 000000000..c12e649e3 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-84.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-85.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-85.png new file mode 100644 index 000000000..8593989da Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-85.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-86.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-86.png new file mode 100644 index 000000000..5d263a734 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-86.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-87.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-87.png new file mode 100644 index 000000000..495e3c52d Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-87.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-88.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-88.png new file mode 100644 index 000000000..8575c84e8 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-88.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-89.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-89.png new file mode 100644 index 000000000..983a9f9c9 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-89.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-90.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-90.png new file mode 100644 index 000000000..c1cee7315 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-90.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-91.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-91.png new file mode 100644 index 000000000..17c34921e Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-91.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-92.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-92.png new file mode 100644 index 000000000..daef46fb9 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-92.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-93.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-93.png new file mode 100644 index 000000000..0e6198dc5 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-93.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-94.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-94.png new file mode 100644 index 000000000..38c3807bc Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-94.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0-95.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0-95.png new file mode 100644 index 000000000..720692a06 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0-95.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-0.png b/plugins/feature/aprs/aprs/aprs-symbols-24-0.png new file mode 100644 index 000000000..92bf11472 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-0.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-00.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-00.png new file mode 100644 index 000000000..48dbf6885 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-00.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-01.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-01.png new file mode 100644 index 000000000..aa021b995 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-01.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-02.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-02.png new file mode 100644 index 000000000..bf282d76d Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-02.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-03.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-03.png new file mode 100644 index 000000000..c7eeb7978 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-03.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-04.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-04.png new file mode 100644 index 000000000..901555085 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-04.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-05.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-05.png new file mode 100644 index 000000000..79516ae45 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-05.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-06.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-06.png new file mode 100644 index 000000000..183866171 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-06.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-07.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-07.png new file mode 100644 index 000000000..61b5fe155 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-07.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-08.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-08.png new file mode 100644 index 000000000..3ceb7afdb Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-08.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-09.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-09.png new file mode 100644 index 000000000..3e2d0876f Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-09.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-10.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-10.png new file mode 100644 index 000000000..a275b4fa9 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-10.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-11.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-11.png new file mode 100644 index 000000000..41ea2ff1a Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-11.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-12.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-12.png new file mode 100644 index 000000000..3180294d7 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-12.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-13.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-13.png new file mode 100644 index 000000000..0ff1d6b3b Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-13.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-14.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-14.png new file mode 100644 index 000000000..859172d78 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-14.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-15.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-15.png new file mode 100644 index 000000000..3de0c5f1a Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-15.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-16.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-16.png new file mode 100644 index 000000000..ff8ba9e33 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-16.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-17.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-17.png new file mode 100644 index 000000000..7afb5fb78 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-17.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-18.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-18.png new file mode 100644 index 000000000..f1408e8c2 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-18.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-19.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-19.png new file mode 100644 index 000000000..9544e474e Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-19.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-20.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-20.png new file mode 100644 index 000000000..5e46ed0f8 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-20.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-21.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-21.png new file mode 100644 index 000000000..48a4f732d Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-21.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-22.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-22.png new file mode 100644 index 000000000..be1aaf802 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-22.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-23.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-23.png new file mode 100644 index 000000000..3ce6fc1df Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-23.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-24.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-24.png new file mode 100644 index 000000000..8824dd5ad Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-24.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-25.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-25.png new file mode 100644 index 000000000..415e56f0b Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-25.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-26.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-26.png new file mode 100644 index 000000000..e917eac4a Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-26.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-27.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-27.png new file mode 100644 index 000000000..d1bc4dc75 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-27.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-28.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-28.png new file mode 100644 index 000000000..35728e2a7 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-28.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-29.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-29.png new file mode 100644 index 000000000..85a00aaec Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-29.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-30.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-30.png new file mode 100644 index 000000000..00704a1b2 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-30.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-31.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-31.png new file mode 100644 index 000000000..267488969 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-31.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-32.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-32.png new file mode 100644 index 000000000..6862fec11 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-32.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-33.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-33.png new file mode 100644 index 000000000..52ad64a16 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-33.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-34.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-34.png new file mode 100644 index 000000000..1aaec30b6 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-34.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-35.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-35.png new file mode 100644 index 000000000..fcc4dc389 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-35.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-36.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-36.png new file mode 100644 index 000000000..4c01a117f Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-36.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-37.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-37.png new file mode 100644 index 000000000..0ce531a8a Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-37.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-38.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-38.png new file mode 100644 index 000000000..7b313423a Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-38.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-39.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-39.png new file mode 100644 index 000000000..896c61554 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-39.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-40.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-40.png new file mode 100644 index 000000000..37aa6b8c6 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-40.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-41.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-41.png new file mode 100644 index 000000000..19a1c226e Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-41.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-42.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-42.png new file mode 100644 index 000000000..486917a8a Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-42.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-43.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-43.png new file mode 100644 index 000000000..b15ac8329 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-43.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-44.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-44.png new file mode 100644 index 000000000..04cfadd13 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-44.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-45.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-45.png new file mode 100644 index 000000000..fb1ca5d87 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-45.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-46.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-46.png new file mode 100644 index 000000000..7303b8f5e Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-46.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-47.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-47.png new file mode 100644 index 000000000..b04b8892d Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-47.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-48.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-48.png new file mode 100644 index 000000000..dbece4dc1 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-48.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-49.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-49.png new file mode 100644 index 000000000..7610e0bee Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-49.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-50.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-50.png new file mode 100644 index 000000000..146ca7c12 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-50.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-51.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-51.png new file mode 100644 index 000000000..343ba0194 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-51.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-52.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-52.png new file mode 100644 index 000000000..082470851 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-52.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-53.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-53.png new file mode 100644 index 000000000..aedfb1f39 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-53.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-54.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-54.png new file mode 100644 index 000000000..a781bf02f Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-54.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-55.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-55.png new file mode 100644 index 000000000..f27359f7a Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-55.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-56.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-56.png new file mode 100644 index 000000000..16b2c98c1 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-56.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-57.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-57.png new file mode 100644 index 000000000..049cd0399 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-57.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-58.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-58.png new file mode 100644 index 000000000..8a2bda7e1 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-58.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-59.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-59.png new file mode 100644 index 000000000..0ff38f59a Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-59.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-60.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-60.png new file mode 100644 index 000000000..44829e453 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-60.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-61.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-61.png new file mode 100644 index 000000000..4466b5e07 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-61.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-62.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-62.png new file mode 100644 index 000000000..3c3144d1d Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-62.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-63.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-63.png new file mode 100644 index 000000000..69087488c Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-63.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-64.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-64.png new file mode 100644 index 000000000..d3001a1fe Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-64.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-65.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-65.png new file mode 100644 index 000000000..340c1806d Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-65.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-66.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-66.png new file mode 100644 index 000000000..2f8909098 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-66.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-67.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-67.png new file mode 100644 index 000000000..20b3dac99 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-67.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-68.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-68.png new file mode 100644 index 000000000..e8e425e9c Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-68.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-69.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-69.png new file mode 100644 index 000000000..1162e9979 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-69.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-70.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-70.png new file mode 100644 index 000000000..dff30cfbd Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-70.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-71.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-71.png new file mode 100644 index 000000000..25c500a08 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-71.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-72.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-72.png new file mode 100644 index 000000000..fecc28648 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-72.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-73.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-73.png new file mode 100644 index 000000000..cc7241976 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-73.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-74.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-74.png new file mode 100644 index 000000000..8800b9f0a Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-74.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-75.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-75.png new file mode 100644 index 000000000..61dd87718 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-75.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-76.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-76.png new file mode 100644 index 000000000..5e2448a80 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-76.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-77.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-77.png new file mode 100644 index 000000000..e03e1d410 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-77.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-78.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-78.png new file mode 100644 index 000000000..be13c4372 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-78.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-79.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-79.png new file mode 100644 index 000000000..893bf2b8e Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-79.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-80.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-80.png new file mode 100644 index 000000000..012f16610 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-80.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-81.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-81.png new file mode 100644 index 000000000..2d77fbf63 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-81.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-82.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-82.png new file mode 100644 index 000000000..d9b112191 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-82.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-83.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-83.png new file mode 100644 index 000000000..f2752c9a2 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-83.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-84.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-84.png new file mode 100644 index 000000000..a99f984cb Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-84.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-85.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-85.png new file mode 100644 index 000000000..7a7548211 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-85.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-86.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-86.png new file mode 100644 index 000000000..4d37f1c9d Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-86.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-87.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-87.png new file mode 100644 index 000000000..a739eb2d4 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-87.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-88.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-88.png new file mode 100644 index 000000000..45fd70a95 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-88.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-89.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-89.png new file mode 100644 index 000000000..75aa62616 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-89.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-90.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-90.png new file mode 100644 index 000000000..c9886d5fb Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-90.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-91.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-91.png new file mode 100644 index 000000000..17ab131d7 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-91.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-92.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-92.png new file mode 100644 index 000000000..9fd8b494f Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-92.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-93.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-93.png new file mode 100644 index 000000000..404040c4d Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-93.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-94.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-94.png new file mode 100644 index 000000000..7a0cc264d Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-94.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1-95.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1-95.png new file mode 100644 index 000000000..a257b9c62 Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1-95.png differ diff --git a/plugins/feature/aprs/aprs/aprs-symbols-24-1.png b/plugins/feature/aprs/aprs/aprs-symbols-24-1.png new file mode 100644 index 000000000..b83d4b14f Binary files /dev/null and b/plugins/feature/aprs/aprs/aprs-symbols-24-1.png differ diff --git a/plugins/feature/aprs/aprsgui.cpp b/plugins/feature/aprs/aprsgui.cpp new file mode 100644 index 000000000..9733dcd4b --- /dev/null +++ b/plugins/feature/aprs/aprsgui.cpp @@ -0,0 +1,1927 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "SWGMapItem.h" + +#include "feature/featureuiset.h" +#include "feature/featurewebapiutils.h" +#include "gui/basicfeaturesettingsdialog.h" +#include "mainwindow.h" +#include "maincore.h" +#include "device/deviceuiset.h" + +#include "ui_aprsgui.h" +#include "aprs.h" +#include "aprsgui.h" +#include "aprssettingsdialog.h" + +#define PACKETS_COL_DATE 0 +#define PACKETS_COL_TIME 1 +#define PACKETS_COL_FROM 2 +#define PACKETS_COL_TO 3 +#define PACKETS_COL_VIA 4 +#define PACKETS_COL_DATA 5 + +#define WEATHER_COL_DATE 0 +#define WEATHER_COL_TIME 1 +#define WEATHER_COL_WIND_DIRECTION 2 +#define WEATHER_COL_WIND_SPEED 3 +#define WEATHER_COL_GUSTS 4 +#define WEATHER_COL_TEMPERATURE 5 +#define WEATHER_COL_HUMIDITY 6 +#define WEATHER_COL_PRESSURE 7 +#define WEATHER_COL_RAIN_LAST_HOUR 8 +#define WEATHER_COL_RAIN_LAST_24_HOURS 9 +#define WEATHER_COL_RAIN_SINCE_MIDNIGHT 10 +#define WEATHER_COL_LUMINOSITY 11 +#define WEATHER_COL_SNOWFALL 12 +#define WEATHER_COL_RADIATION_LEVEL 13 +#define WEATHER_COL_FLOOD_LEVEL 14 + +#define STATUS_COL_DATE 0 +#define STATUS_COL_TIME 1 +#define STATUS_COL_STATUS 2 +#define STATUS_COL_SYMBOL 3 +#define STATUS_COL_MAIDENHEAD 4 +#define STATUS_COL_BEAM_HEADING 5 +#define STATUS_COL_BEAM_POWER 6 + +#define MESSAGE_COL_DATE 0 +#define MESSAGE_COL_TIME 1 +#define MESSAGE_COL_ADDRESSEE 2 +#define MESSAGE_COL_MESSAGE 3 +#define MESSAGE_COL_NO 4 + +#define TELEMETRY_COL_DATE 0 +#define TELEMETRY_COL_TIME 1 +#define TELEMETRY_COL_SEQ_NO 2 +#define TELEMETRY_COL_A1 3 +#define TELEMETRY_COL_A2 4 +#define TELEMETRY_COL_A3 5 +#define TELEMETRY_COL_A4 6 +#define TELEMETRY_COL_A5 7 +#define TELEMETRY_COL_B1 8 +#define TELEMETRY_COL_B2 9 +#define TELEMETRY_COL_B3 10 +#define TELEMETRY_COL_B4 11 +#define TELEMETRY_COL_B5 12 +#define TELEMETRY_COL_B6 13 +#define TELEMETRY_COL_B7 14 +#define TELEMETRY_COL_B8 15 +#define TELEMETRY_COL_COMMENT 16 + +#define MOTION_COL_DATE 0 +#define MOTION_COL_TIME 1 +#define MOTION_COL_LATITUDE 2 +#define MOTION_COL_LONGITUDE 3 +#define MOTION_COL_ALTITUDE 4 +#define MOTION_COL_COURSE 5 +#define MOTION_COL_SPEED 6 + +APRSGUI* APRSGUI::create(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature) +{ + APRSGUI* gui = new APRSGUI(pluginAPI, featureUISet, feature); + return gui; +} + +void APRSGUI::destroy() +{ + delete this; +} + +void APRSGUI::resetToDefaults() +{ + m_settings.resetToDefaults(); + displaySettings(); + applySettings(true); +} + +QByteArray APRSGUI::serialize() const +{ + return m_settings.serialize(); +} + +bool APRSGUI::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + displaySettings(); + applySettings(true); + return true; + } + else + { + resetToDefaults(); + return false; + } +} + +bool APRSGUI::handleMessage(const Message& message) +{ + if (APRS::MsgConfigureAPRS::match(message)) + { + qDebug("APRSGUI::handleMessage: APRS::MsgConfigureAPRS"); + const APRS::MsgConfigureAPRS& cfg = (APRS::MsgConfigureAPRS&) message; + m_settings = cfg.getSettings(); + qDebug() << m_settings.m_igateCallsign; + blockApplySettings(true); + displaySettings(); + blockApplySettings(false); + + return true; + } + else if (PipeEndPoint::MsgReportPipes::match(message)) + { + PipeEndPoint::MsgReportPipes& report = (PipeEndPoint::MsgReportPipes&) message; + m_availablePipes = report.getAvailablePipes(); + updatePipeList(); + + return true; + } + else if (MainCore::MsgPacket::match(message)) + { + MainCore::MsgPacket& report = (MainCore::MsgPacket&) message; + AX25Packet ax25; + APRSPacket *aprs = new APRSPacket(); + if (ax25.decode(report.getPacket())) + { + if (aprs->decode(ax25)) + { + aprs->m_dateTime = report.getDateTime(); + + APRSStation *station; + bool addToStationSel = false; + + // Is packet for an existing object or station? + if (!aprs->m_objectName.isEmpty() && m_stations.contains(aprs->m_objectName)) + { + // Add packet to existing object + station = m_stations.value(aprs->m_objectName); + station->addPacket(aprs); + } + else if (!aprs->m_objectName.isEmpty()) + { + // Add new object + station = new APRSStation(aprs->m_objectName); + station->m_isObject = true; + station->m_reportingStation = aprs->m_from; + station->addPacket(aprs); + m_stations.insert(aprs->m_objectName, station); + addToStationSel = true; + } + else if (m_stations.contains(aprs->m_from)) + { + // Add packet to existing station + station = m_stations.value(aprs->m_from); + station->addPacket(aprs); + } + else + { + // Add new station + station = new APRSStation(aprs->m_from); + station->addPacket(aprs); + m_stations.insert(aprs->m_from, station); + addToStationSel = true; + } + + // Update station + if (aprs->m_hasSymbol) + station->m_symbolImage = aprs->m_symbolImage; + if (aprs->m_hasTimestamp) + station->m_latestPacket = aprs->dateTime(); + if (aprs->m_hasStatus) + station->m_latestStatus = aprs->m_status; + if (!aprs->m_comment.isEmpty()) + station->m_latestComment = aprs->m_comment; + if (aprs->m_hasPosition) + station->m_latestPosition = aprs->position(); + if (aprs->m_hasAltitude) + station->m_latestAltitude = QString("%1").arg(aprs->m_altitudeFt); + if (aprs->m_hasCourseAndSpeed) + { + station->m_latestCourse = QString("%1").arg(aprs->m_course); + station->m_latestSpeed = QString("%1").arg(aprs->m_speed); + } + if (aprs->m_hasStationDetails) + { + station->m_powerWatts = QString("%1").arg(aprs->m_powerWatts); + station->m_antennaHeightFt = QString("%1").arg(aprs->m_antennaHeightFt); + station->m_antennaGainDB = QString("%1").arg(aprs->m_antennaGainDB); + station->m_antennaDirectivity = aprs->m_antennaDirectivity; + } + if (aprs->m_hasRadioRange) + station->m_radioRange = QString("%1").arg(aprs->m_radioRangeMiles); + if (aprs->m_hasWeather) + station->m_hasWeather = true; + if (aprs->m_hasTelemetry) + station->m_hasTelemetry = true; + if (aprs->m_hasCourseAndSpeed) + station->m_hasCourseAndSpeed = true; + + // Update messages, which aren't station specific + if (aprs->m_hasMessage) + { + int row = ui->messagesTable->rowCount(); + ui->messagesTable->setRowCount(row + 1); + QTableWidgetItem *messageDateItem = new QTableWidgetItem(); + QTableWidgetItem *messageTimeItem = new QTableWidgetItem(); + QTableWidgetItem *messageAddresseeItem = new QTableWidgetItem(); + QTableWidgetItem *messageMessageItem = new QTableWidgetItem(); + QTableWidgetItem *messageNoItem = new QTableWidgetItem(); + ui->messagesTable->setItem(row, MESSAGE_COL_DATE, messageDateItem); + ui->messagesTable->setItem(row, MESSAGE_COL_TIME, messageTimeItem); + ui->messagesTable->setItem(row, MESSAGE_COL_ADDRESSEE, messageAddresseeItem); + ui->messagesTable->setItem(row, MESSAGE_COL_MESSAGE, messageMessageItem); + ui->messagesTable->setItem(row, MESSAGE_COL_NO, messageNoItem); + messageDateItem->setData(Qt::DisplayRole, formatDate(aprs)); + messageTimeItem->setData(Qt::DisplayRole, formatTime(aprs)); + messageAddresseeItem->setText(aprs->m_addressee); + messageMessageItem->setText(aprs->m_message); + messageNoItem->setText(aprs->m_messageNo); + + // Process telemetry messages + if ((aprs->m_telemetryNames.size() > 0) || (aprs->m_telemetryLabels.size() > 0) || aprs->m_hasTelemetryCoefficients || aprs->m_hasTelemetryBitSense) + { + APRSStation *telemetryStation; + if (m_stations.contains(aprs->m_addressee)) + telemetryStation = m_stations.value(aprs->m_addressee); + else + { + telemetryStation = new APRSStation(aprs->m_addressee); + m_stations.insert(aprs->m_addressee, telemetryStation); + } + if (aprs->m_telemetryNames.size() > 0) + { + telemetryStation->m_telemetryNames = aprs->m_telemetryNames; + for (int i = 0; i < aprs->m_telemetryNames.size(); i++) + ui->telemetryPlotSelect->setItemText(i, aprs->m_telemetryNames[i]); + } + else + telemetryStation->m_telemetryLabels = aprs->m_telemetryLabels; + if (aprs->m_hasTelemetryCoefficients > 0) + { + for (int j = 0; j < 5; j++) + { + telemetryStation->m_telemetryCoefficientsA[j] = aprs->m_telemetryCoefficientsA[j]; + telemetryStation->m_telemetryCoefficientsB[j] = aprs->m_telemetryCoefficientsB[j]; + telemetryStation->m_telemetryCoefficientsC[j] = aprs->m_telemetryCoefficientsC[j]; + } + telemetryStation->m_hasTelemetryCoefficients = aprs->m_hasTelemetryCoefficients; + } + if (aprs->m_hasTelemetryBitSense) + { + for (int j = 0; j < 8; j++) + telemetryStation->m_telemetryBitSense[j] = aprs->m_telemetryBitSense[j]; + telemetryStation->m_hasTelemetryBitSense; + telemetryStation->m_telemetryProjectName = aprs->m_telemetryProjectName; + } + if (ui->stationSelect->currentText() == aprs->m_addressee) + { + for (int i = 0; i < station->m_telemetryNames.size(); i++) + { + QString header; + if (station->m_telemetryLabels.size() <= i) + header = station->m_telemetryNames[i]; + else + header = QString("%1 (%2)").arg(station->m_telemetryNames[i]).arg(station->m_telemetryLabels[i]); + ui->telemetryTable->horizontalHeaderItem(TELEMETRY_COL_A1+i)->setText(header); + } + } + } + } + + if (addToStationSel) + { + if (!filterStation(station)) + ui->stationSelect->addItem(station->m_station); + } + + // Refresh GUI if currently selected station + if (ui->stationSelect->currentText() == aprs->m_from) + { + updateSummary(station); + addPacketToGUI(station, aprs); + if (aprs->m_hasWeather) + plotWeather(); + if (aprs->m_hasTelemetry) + plotTelemetry(); + if (aprs->m_hasPosition || aprs->m_hasAltitude || aprs->m_hasCourseAndSpeed) + plotMotion(); + } + + // Forward to map + MessagePipes& messagePipes = MainCore::instance()->getMessagePipes(); + QList *mapMessageQueues = messagePipes.getMessageQueues(m_aprs, "mapitems"); + if (mapMessageQueues) + { + if (aprs->m_hasPosition && (aprs->m_from != "")) + { + QList::iterator it = mapMessageQueues->begin(); + + for (; it != mapMessageQueues->end(); ++it) + { + SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem(); + if (!aprs->m_objectName.isEmpty()) + swgMapItem->setName(new QString(aprs->m_objectName)); + else + swgMapItem->setName(new QString(aprs->m_from)); + swgMapItem->setLatitude(aprs->m_latitude); + swgMapItem->setLongitude(aprs->m_longitude); + if (aprs->m_objectKilled) + { + swgMapItem->setImage(new QString("")); + swgMapItem->setText(new QString("")); + } + else + { + swgMapItem->setImage(new QString(QString("qrc:///%1").arg(aprs->m_symbolImage))); + swgMapItem->setText(new QString(aprs->toText())); + } + swgMapItem->setImageFixedSize(0); + + MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_aprs, swgMapItem); + (*it)->push(msg); + } + } + } + + } + else + { + qDebug() << "APRSGUI::handleMessage: Failed to decode as APRS"; + qDebug() << ax25.m_from << " " << ax25.m_to << " " << ax25.m_via << " " << ax25.m_type << " " << ax25.m_pid << " "<< ax25.m_dataASCII; + } + } + else + qDebug() << "APRSGUI::handleMessage: Failed to decode as AX.25"; + return true; + } + + return false; +} + +void APRSGUI::handleInputMessages() +{ + Message* message; + + while ((message = getInputMessageQueue()->pop())) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +void APRSGUI::onWidgetRolled(QWidget* widget, bool rollDown) +{ + (void) widget; + (void) rollDown; +} + +APRSGUI::APRSGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent) : + FeatureGUI(parent), + ui(new Ui::APRSGUI), + m_pluginAPI(pluginAPI), + m_featureUISet(featureUISet), + m_doApplySettings(true), + m_lastFeatureState(0) +{ + ui->setupUi(this); + + setAttribute(Qt::WA_DeleteOnClose, true); + setChannelWidget(false); + connect(this, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool))); + m_aprs = reinterpret_cast(feature); + m_aprs->setMessageQueueToGUI(&m_inputMessageQueue); + + m_featureUISet->addRollupWidget(this); + + connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &))); + connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + + connect(&m_statusTimer, SIGNAL(timeout()), this, SLOT(updateStatus())); + m_statusTimer.start(1000); + + // Resize the table using dummy data + resizeTable(); + // Allow user to reorder columns + ui->weatherTable->horizontalHeader()->setSectionsMovable(true); + ui->packetsTable->horizontalHeader()->setSectionsMovable(true); + ui->statusTable->horizontalHeader()->setSectionsMovable(true); + ui->messagesTable->horizontalHeader()->setSectionsMovable(true); + ui->telemetryTable->horizontalHeader()->setSectionsMovable(true); + ui->motionTable->horizontalHeader()->setSectionsMovable(true); + // Allow user to sort table by clicking on headers + ui->weatherTable->setSortingEnabled(true); + ui->packetsTable->setSortingEnabled(true); + ui->statusTable->setSortingEnabled(true); + ui->messagesTable->setSortingEnabled(true); + ui->telemetryTable->setSortingEnabled(true); + ui->motionTable->setSortingEnabled(true); + // Add context menu to allow hiding/showing of columns + packetsTableMenu = new QMenu(ui->packetsTable); + for (int i = 0; i < ui->packetsTable->horizontalHeader()->count(); i++) + { + QString text = ui->packetsTable->horizontalHeaderItem(i)->text(); + packetsTableMenu->addAction(packetsTable_createCheckableItem(text, i, true)); + } + ui->packetsTable->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->packetsTable->horizontalHeader(), SIGNAL(customContextMenuRequested(QPoint)), SLOT(packetsTable_columnSelectMenu(QPoint))); + + weatherTableMenu = new QMenu(ui->weatherTable); + for (int i = 0; i < ui->weatherTable->horizontalHeader()->count(); i++) + { + QString text = ui->weatherTable->horizontalHeaderItem(i)->text(); + weatherTableMenu->addAction(weatherTable_createCheckableItem(text, i, true)); + } + ui->weatherTable->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->weatherTable->horizontalHeader(), SIGNAL(customContextMenuRequested(QPoint)), SLOT(weatherTable_columnSelectMenu(QPoint))); + + statusTableMenu = new QMenu(ui->statusTable); + for (int i = 0; i < ui->statusTable->horizontalHeader()->count(); i++) + { + QString text = ui->statusTable->horizontalHeaderItem(i)->text(); + statusTableMenu->addAction(statusTable_createCheckableItem(text, i, true)); + } + ui->statusTable->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->statusTable->horizontalHeader(), SIGNAL(customContextMenuRequested(QPoint)), SLOT(statusTable_columnSelectMenu(QPoint))); + + messagesTableMenu = new QMenu(ui->messagesTable); + for (int i = 0; i < ui->messagesTable->horizontalHeader()->count(); i++) + { + QString text = ui->messagesTable->horizontalHeaderItem(i)->text(); + messagesTableMenu->addAction(messagesTable_createCheckableItem(text, i, true)); + } + ui->messagesTable->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->messagesTable->horizontalHeader(), SIGNAL(customContextMenuRequested(QPoint)), SLOT(messagesTable_columnSelectMenu(QPoint))); + + telemetryTableMenu = new QMenu(ui->telemetryTable); + for (int i = 0; i < ui->telemetryTable->horizontalHeader()->count(); i++) + { + QString text = ui->telemetryTable->horizontalHeaderItem(i)->text(); + telemetryTableMenu->addAction(telemetryTable_createCheckableItem(text, i, true)); + } + ui->telemetryTable->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->telemetryTable->horizontalHeader(), SIGNAL(customContextMenuRequested(QPoint)), SLOT(telemetryTable_columnSelectMenu(QPoint))); + + motionTableMenu = new QMenu(ui->motionTable); + for (int i = 0; i < ui->motionTable->horizontalHeader()->count(); i++) + { + QString text = ui->motionTable->horizontalHeaderItem(i)->text(); + motionTableMenu->addAction(motionTable_createCheckableItem(text, i, true)); + } + ui->motionTable->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->motionTable->horizontalHeader(), SIGNAL(customContextMenuRequested(QPoint)), SLOT(motionTable_columnSelectMenu(QPoint))); + // Get signals when columns change + connect(ui->packetsTable->horizontalHeader(), SIGNAL(sectionMoved(int, int, int)), SLOT(packetsTable_sectionMoved(int, int, int))); + connect(ui->packetsTable->horizontalHeader(), SIGNAL(sectionResized(int, int, int)), SLOT(packetsTable_sectionResized(int, int, int))); + connect(ui->weatherTable->horizontalHeader(), SIGNAL(sectionMoved(int, int, int)), SLOT(weatherTable_sectionMoved(int, int, int))); + connect(ui->weatherTable->horizontalHeader(), SIGNAL(sectionResized(int, int, int)), SLOT(weatherTable_sectionResized(int, int, int))); + connect(ui->statusTable->horizontalHeader(), SIGNAL(sectionMoved(int, int, int)), SLOT(statusTable_sectionMoved(int, int, int))); + connect(ui->statusTable->horizontalHeader(), SIGNAL(sectionResized(int, int, int)), SLOT(statusTable_sectionResized(int, int, int))); + connect(ui->messagesTable->horizontalHeader(), SIGNAL(sectionMoved(int, int, int)), SLOT(messagesTable_sectionMoved(int, int, int))); + connect(ui->messagesTable->horizontalHeader(), SIGNAL(sectionResized(int, int, int)), SLOT(messagesTable_sectionResized(int, int, int))); + connect(ui->telemetryTable->horizontalHeader(), SIGNAL(sectionMoved(int, int, int)), SLOT(telemetryTable_sectionMoved(int, int, int))); + connect(ui->telemetryTable->horizontalHeader(), SIGNAL(sectionResized(int, int, int)), SLOT(telemetryTable_sectionResized(int, int, int))); + connect(ui->motionTable->horizontalHeader(), SIGNAL(sectionMoved(int, int, int)), SLOT(motionTable_sectionMoved(int, int, int))); + connect(ui->motionTable->horizontalHeader(), SIGNAL(sectionResized(int, int, int)), SLOT(motionTable_sectionResized(int, int, int))); + + m_weatherChart.legend()->hide(); + ui->weatherChart->setChart(&m_weatherChart); + ui->weatherChart->setRenderHint(QPainter::Antialiasing); + m_weatherChart.addAxis(&m_weatherChartXAxis, Qt::AlignBottom); + m_weatherChart.addAxis(&m_weatherChartYAxis, Qt::AlignLeft); + + m_telemetryChart.legend()->hide(); + ui->telemetryChart->setChart(&m_telemetryChart); + ui->telemetryChart->setRenderHint(QPainter::Antialiasing); + m_telemetryChart.addAxis(&m_telemetryChartXAxis, Qt::AlignBottom); + m_telemetryChart.addAxis(&m_telemetryChartYAxis, Qt::AlignLeft); + + m_motionChart.legend()->hide(); + ui->motionChart->setChart(&m_motionChart); + ui->motionChart->setRenderHint(QPainter::Antialiasing); + m_motionChart.addAxis(&m_motionChartXAxis, Qt::AlignBottom); + m_motionChart.addAxis(&m_motionChartYAxis, Qt::AlignLeft); + + displaySettings(); + applySettings(true); +} + +APRSGUI::~APRSGUI() +{ + delete ui; +} + +void APRSGUI::blockApplySettings(bool block) +{ + m_doApplySettings = !block; +} + +bool APRSGUI::filterStation(APRSStation *station) +{ + switch (m_settings.m_stationFilter) + { + case APRSSettings::ALL: + return false; + case APRSSettings::STATIONS: + return station->m_isObject; + case APRSSettings::OBJECTS: + return !station->m_isObject; + case APRSSettings::WEATHER: + return !station->m_hasWeather; + case APRSSettings::TELEMETRY: + return !station->m_hasTelemetry; + case APRSSettings::COURSE_AND_SPEED: + return !station->m_hasCourseAndSpeed; + } + return false; +} + +void APRSGUI::filterStations() +{ + ui->stationSelect->clear(); + QHashIterator i(m_stations); + while (i.hasNext()) + { + i.next(); + APRSStation *station = i.value(); + if (!filterStation(station)) + { + ui->stationSelect->addItem(station->m_station); + } + } +} + +void APRSGUI::displayTableSettings(QTableWidget *table, QMenu *menu, int *columnSizes, int *columnIndexes, int columns) +{ + QHeaderView *header = table->horizontalHeader(); + for (int i = 0; i < columns; i++) + { + bool hidden = columnSizes[i] == 0; + header->setSectionHidden(i, hidden); + menu->actions().at(i)->setChecked(!hidden); + if (columnSizes[i] > 0) + table->setColumnWidth(i, columnSizes[i]); + header->moveSection(header->visualIndex(i), columnIndexes[i]); + } +} + +void APRSGUI::displaySettings() +{ + setTitleColor(m_settings.m_rgbColor); + setWindowTitle(m_settings.m_title); + blockApplySettings(true); + ui->igate->setChecked(m_settings.m_igateEnabled); + ui->stationFilter->setCurrentIndex((int)m_settings.m_stationFilter); + ui->filterAddressee->setText(m_settings.m_filterAddressee); + + // Order and size columns + displayTableSettings(ui->packetsTable, packetsTableMenu, m_settings.m_packetsTableColumnSizes, m_settings.m_packetsTableColumnIndexes, APRS_PACKETS_TABLE_COLUMNS); + displayTableSettings(ui->weatherTable, weatherTableMenu, m_settings.m_weatherTableColumnSizes, m_settings.m_weatherTableColumnIndexes, APRS_WEATHER_TABLE_COLUMNS); + displayTableSettings(ui->statusTable, statusTableMenu, m_settings.m_statusTableColumnSizes, m_settings.m_statusTableColumnIndexes, APRS_STATUS_TABLE_COLUMNS); + displayTableSettings(ui->messagesTable, messagesTableMenu, m_settings.m_messagesTableColumnSizes, m_settings.m_messagesTableColumnIndexes, APRS_MESSAGES_TABLE_COLUMNS); + displayTableSettings(ui->telemetryTable, telemetryTableMenu, m_settings.m_telemetryTableColumnSizes, m_settings.m_telemetryTableColumnIndexes, APRS_TELEMETRY_TABLE_COLUMNS); + displayTableSettings(ui->motionTable, motionTableMenu, m_settings.m_motionTableColumnSizes, m_settings.m_motionTableColumnIndexes, APRS_MOTION_TABLE_COLUMNS); + + blockApplySettings(false); +} + +void APRSGUI::updatePipeList() +{ + ui->sourcePipes->blockSignals(true); + ui->sourcePipes->clear(); + QList::const_iterator it = m_availablePipes.begin(); + + for (int i = 0; it != m_availablePipes.end(); ++it, i++) + { + ui->sourcePipes->addItem(it->getName()); + } + + ui->sourcePipes->blockSignals(false); +} + +void APRSGUI::leaveEvent(QEvent*) +{ +} + +void APRSGUI::enterEvent(QEvent*) +{ +} + +void APRSGUI::resizeEvent(QResizeEvent* size) +{ + // Replot graphs to ensure Axis are visible + plotWeather(); + plotTelemetry(); + plotMotion(); + FeatureGUI::resizeEvent(size); +} + +void APRSGUI::onMenuDialogCalled(const QPoint &p) +{ + if (m_contextMenuType == ContextMenuChannelSettings) + { + BasicFeatureSettingsDialog dialog(this); + dialog.setTitle(m_settings.m_title); + dialog.setColor(m_settings.m_rgbColor); + dialog.setUseReverseAPI(m_settings.m_useReverseAPI); + dialog.setReverseAPIAddress(m_settings.m_reverseAPIAddress); + dialog.setReverseAPIPort(m_settings.m_reverseAPIPort); + dialog.setReverseAPIFeatureSetIndex(m_settings.m_reverseAPIFeatureSetIndex); + dialog.setReverseAPIFeatureIndex(m_settings.m_reverseAPIFeatureIndex); + + dialog.move(p); + dialog.exec(); + + m_settings.m_rgbColor = dialog.getColor().rgb(); + m_settings.m_title = dialog.getTitle(); + m_settings.m_useReverseAPI = dialog.useReverseAPI(); + m_settings.m_reverseAPIAddress = dialog.getReverseAPIAddress(); + m_settings.m_reverseAPIPort = dialog.getReverseAPIPort(); + m_settings.m_reverseAPIFeatureSetIndex = dialog.getReverseAPIFeatureSetIndex(); + m_settings.m_reverseAPIFeatureIndex = dialog.getReverseAPIFeatureIndex(); + + setWindowTitle(m_settings.m_title); + setTitleColor(m_settings.m_rgbColor); + + applySettings(); + } + + resetContextMenuType(); +} + +void APRSGUI::updateSummary(APRSStation *station) +{ + ui->station->setText(station->m_station); + ui->reportingStation->setText(station->m_reportingStation); + ui->symbol->setPixmap(QPixmap(QString(":%1").arg(station->m_symbolImage))); + ui->status->setText(station->m_latestStatus); + ui->comment->setText(station->m_latestComment); + ui->position->setText(station->m_latestPosition); + ui->altitude->setText(station->m_latestAltitude); + ui->course->setText(station->m_latestCourse); + ui->speed->setText(station->m_latestSpeed); + ui->txPower->setText(station->m_powerWatts); + ui->antennaHeight->setText(station->m_antennaHeightFt); + ui->antennaGain->setText(station->m_antennaGainDB); + ui->antennaDirectivity->setText(station->m_antennaDirectivity); + ui->radioRange->setText(station->m_radioRange); + if (!station->m_packets.isEmpty()) + ui->lastPacket->setText(station->m_packets.last()->m_dateTime.toString()); + else + ui->lastPacket->setText(""); +} + +QString APRSGUI::formatDate(APRSPacket *aprs) +{ + if (aprs->m_hasTimestamp) + return aprs->date(); + else + return aprs->m_dateTime.date().toString("yyyy/MM/dd"); +} + +QString APRSGUI::formatTime(APRSPacket *aprs) +{ + // Add suffix T to indicate timestamp used + if (aprs->m_hasTimestamp) + return QString("%1 T").arg(aprs->time()); + else + return aprs->m_dateTime.time().toString("hh:mm:ss"); +} + +double APRSGUI::applyCoefficients(int idx, int value, APRSStation *station) +{ + if (station->m_hasTelemetryCoefficients > idx) + return station->m_telemetryCoefficientsA[idx] * value * value + station->m_telemetryCoefficientsB[idx] * value + station->m_telemetryCoefficientsC[idx]; + else + return (double)idx; +} + +void APRSGUI::addPacketToGUI(APRSStation *station, APRSPacket *aprs) +{ + int row; + // Weather table + if (aprs->m_hasWeather) + { + row = ui->weatherTable->rowCount(); + ui->weatherTable->setRowCount(row + 1); + QTableWidgetItem *weatherDateItem = new QTableWidgetItem(); + QTableWidgetItem *weatherTimeItem = new QTableWidgetItem(); + QTableWidgetItem *windDirectionItem = new QTableWidgetItem(); + QTableWidgetItem *windSpeedItem = new QTableWidgetItem(); + QTableWidgetItem *gustsItem = new QTableWidgetItem(); + QTableWidgetItem *temperatureItem = new QTableWidgetItem(); + QTableWidgetItem *humidityItem = new QTableWidgetItem(); + QTableWidgetItem *pressureItem = new QTableWidgetItem(); + QTableWidgetItem *rainLastHourItem = new QTableWidgetItem(); + QTableWidgetItem *rainLast24HoursItem = new QTableWidgetItem(); + QTableWidgetItem *rainSinceMidnightItem = new QTableWidgetItem(); + QTableWidgetItem *luminosityItem = new QTableWidgetItem(); + QTableWidgetItem *snowfallItem = new QTableWidgetItem(); + QTableWidgetItem *radiationLevelItem = new QTableWidgetItem(); + QTableWidgetItem *floodLevelItem = new QTableWidgetItem(); + ui->weatherTable->setItem(row, WEATHER_COL_DATE, weatherDateItem); + ui->weatherTable->setItem(row, WEATHER_COL_TIME, weatherTimeItem); + ui->weatherTable->setItem(row, WEATHER_COL_WIND_DIRECTION, windDirectionItem); + ui->weatherTable->setItem(row, WEATHER_COL_WIND_SPEED, windSpeedItem); + ui->weatherTable->setItem(row, WEATHER_COL_GUSTS, gustsItem); + ui->weatherTable->setItem(row, WEATHER_COL_TEMPERATURE, temperatureItem); + ui->weatherTable->setItem(row, WEATHER_COL_HUMIDITY, humidityItem); + ui->weatherTable->setItem(row, WEATHER_COL_PRESSURE, pressureItem); + ui->weatherTable->setItem(row, WEATHER_COL_RAIN_LAST_HOUR, rainLastHourItem); + ui->weatherTable->setItem(row, WEATHER_COL_RAIN_LAST_24_HOURS, rainLast24HoursItem); + ui->weatherTable->setItem(row, WEATHER_COL_RAIN_SINCE_MIDNIGHT, rainSinceMidnightItem); + ui->weatherTable->setItem(row, WEATHER_COL_LUMINOSITY, luminosityItem); + ui->weatherTable->setItem(row, WEATHER_COL_SNOWFALL, snowfallItem); + ui->weatherTable->setItem(row, WEATHER_COL_RADIATION_LEVEL, radiationLevelItem); + ui->weatherTable->setItem(row, WEATHER_COL_FLOOD_LEVEL, floodLevelItem); + weatherDateItem->setData(Qt::DisplayRole, formatDate(aprs)); + weatherTimeItem->setData(Qt::DisplayRole, formatTime(aprs)); + if (aprs->m_hasWindDirection) + windDirectionItem->setData(Qt::DisplayRole, aprs->m_windDirection); + if (aprs->m_hasWindSpeed) + windSpeedItem->setData(Qt::DisplayRole, aprs->m_windSpeed); + if (aprs->m_hasGust) + gustsItem->setData(Qt::DisplayRole, aprs->m_gust); + if (aprs->m_hasTemp) + temperatureItem->setData(Qt::DisplayRole, aprs->m_temp); + if (aprs->m_hasHumidity) + humidityItem->setData(Qt::DisplayRole, aprs->m_humidity); + if (aprs->m_hasBarometricPressure) + pressureItem->setData(Qt::DisplayRole, aprs->m_barometricPressure/10.0f); + if (aprs->m_hasRainLastHr) + rainLastHourItem->setData(Qt::DisplayRole, aprs->m_rainLastHr); + if (aprs->m_hasRainLast24Hrs) + rainLast24HoursItem->setData(Qt::DisplayRole, aprs->m_rainLast24Hrs); + if (aprs->m_hasRainSinceMidnight) + rainSinceMidnightItem->setData(Qt::DisplayRole, aprs->m_rainSinceMidnight); + if (aprs->m_hasLuminsoity) + luminosityItem->setData(Qt::DisplayRole, aprs->m_luminosity); + if (aprs->m_hasSnowfallLast24Hrs) + snowfallItem->setData(Qt::DisplayRole, aprs->m_snowfallLast24Hrs); + if (aprs->m_hasRadiationLevel) + radiationLevelItem->setData(Qt::DisplayRole, aprs->m_hasRadiationLevel); + if (aprs->m_hasFloodLevel) + floodLevelItem->setData(Qt::DisplayRole, aprs->m_floodLevel); + } + + // Status table + if (aprs->m_hasStatus) + { + row = ui->statusTable->rowCount(); + ui->statusTable->setRowCount(row + 1); + QTableWidgetItem *statusDateItem = new QTableWidgetItem(); + QTableWidgetItem *statusTimeItem = new QTableWidgetItem(); + QTableWidgetItem *statusItem = new QTableWidgetItem(); + QTableWidgetItem *statusSymbolItem = new QTableWidgetItem(); + QTableWidgetItem *statusMaidenheadItem = new QTableWidgetItem(); + QTableWidgetItem *statusBeamHeadingItem = new QTableWidgetItem(); + QTableWidgetItem *statusBeamPowerItem = new QTableWidgetItem(); + ui->statusTable->setItem(row, STATUS_COL_DATE, statusDateItem); + ui->statusTable->setItem(row, STATUS_COL_TIME, statusTimeItem); + ui->statusTable->setItem(row, STATUS_COL_STATUS, statusItem); + ui->statusTable->setItem(row, STATUS_COL_SYMBOL, statusSymbolItem); + ui->statusTable->setItem(row, STATUS_COL_MAIDENHEAD, statusMaidenheadItem); + ui->statusTable->setItem(row, STATUS_COL_BEAM_HEADING, statusBeamHeadingItem); + ui->statusTable->setItem(row, STATUS_COL_BEAM_POWER, statusBeamPowerItem); + statusDateItem->setData(Qt::DisplayRole, formatDate(aprs)); + statusTimeItem->setData(Qt::DisplayRole, formatTime(aprs)); + statusItem->setText(aprs->m_status); + if (aprs->m_hasSymbol) + { + statusSymbolItem->setSizeHint(QSize(24, 24)); + statusSymbolItem->setIcon(QIcon(QString(":%1").arg(station->m_symbolImage))); + } + statusMaidenheadItem->setText(aprs->m_maidenhead); + if (aprs->m_hasBeam) + { + statusBeamHeadingItem->setData(Qt::DisplayRole, aprs->m_beamHeading); + statusBeamPowerItem->setData(Qt::DisplayRole, aprs->m_beamPower); + } + } + + // Telemetry table + if (aprs->m_hasTelemetry) + { + row = ui->telemetryTable->rowCount(); + ui->telemetryTable->setRowCount(row + 1); + QTableWidgetItem *telemetryDateItem = new QTableWidgetItem(); + QTableWidgetItem *telemetryTimeItem = new QTableWidgetItem(); + QTableWidgetItem *telemetrySeqNoItem = new QTableWidgetItem(); + QTableWidgetItem *telemetryA1Item = new QTableWidgetItem(); + QTableWidgetItem *telemetryA2Item = new QTableWidgetItem(); + QTableWidgetItem *telemetryA3Item = new QTableWidgetItem(); + QTableWidgetItem *telemetryA4Item = new QTableWidgetItem(); + QTableWidgetItem *telemetryA5Item = new QTableWidgetItem(); + QTableWidgetItem *telemetryB1Item = new QTableWidgetItem(); + QTableWidgetItem *telemetryB2Item = new QTableWidgetItem(); + QTableWidgetItem *telemetryB3Item = new QTableWidgetItem(); + QTableWidgetItem *telemetryB4Item = new QTableWidgetItem(); + QTableWidgetItem *telemetryB5Item = new QTableWidgetItem(); + QTableWidgetItem *telemetryB6Item = new QTableWidgetItem(); + QTableWidgetItem *telemetryB7Item = new QTableWidgetItem(); + QTableWidgetItem *telemetryB8Item = new QTableWidgetItem(); + QTableWidgetItem *telemetryCommentItem = new QTableWidgetItem(); + ui->telemetryTable->setItem(row, TELEMETRY_COL_DATE, telemetryDateItem); + ui->telemetryTable->setItem(row, TELEMETRY_COL_TIME, telemetryTimeItem); + ui->telemetryTable->setItem(row, TELEMETRY_COL_SEQ_NO, telemetrySeqNoItem); + ui->telemetryTable->setItem(row, TELEMETRY_COL_A1, telemetryA1Item); + ui->telemetryTable->setItem(row, TELEMETRY_COL_A2, telemetryA2Item); + ui->telemetryTable->setItem(row, TELEMETRY_COL_A3, telemetryA3Item); + ui->telemetryTable->setItem(row, TELEMETRY_COL_A4, telemetryA4Item); + ui->telemetryTable->setItem(row, TELEMETRY_COL_A5, telemetryA5Item); + ui->telemetryTable->setItem(row, TELEMETRY_COL_B1, telemetryB1Item); + ui->telemetryTable->setItem(row, TELEMETRY_COL_B2, telemetryB2Item); + ui->telemetryTable->setItem(row, TELEMETRY_COL_B3, telemetryB3Item); + ui->telemetryTable->setItem(row, TELEMETRY_COL_B4, telemetryB4Item); + ui->telemetryTable->setItem(row, TELEMETRY_COL_B5, telemetryB5Item); + ui->telemetryTable->setItem(row, TELEMETRY_COL_B6, telemetryB6Item); + ui->telemetryTable->setItem(row, TELEMETRY_COL_B7, telemetryB7Item); + ui->telemetryTable->setItem(row, TELEMETRY_COL_B8, telemetryB8Item); + ui->telemetryTable->setItem(row, TELEMETRY_COL_COMMENT, telemetryCommentItem); + telemetryDateItem->setData(Qt::DisplayRole, formatDate(aprs)); + telemetryTimeItem->setData(Qt::DisplayRole, formatTime(aprs)); + if (aprs->m_hasSeqNo) + telemetrySeqNoItem->setData(Qt::DisplayRole, aprs->m_seqNo); + if (aprs->m_a1HasValue) + telemetryA1Item->setData(Qt::DisplayRole, applyCoefficients(0, aprs->m_a1, station)); + if (aprs->m_a2HasValue) + telemetryA2Item->setData(Qt::DisplayRole, applyCoefficients(1, aprs->m_a2, station)); + if (aprs->m_a3HasValue) + telemetryA3Item->setData(Qt::DisplayRole, applyCoefficients(2, aprs->m_a3, station)); + if (aprs->m_a4HasValue) + telemetryA4Item->setData(Qt::DisplayRole, applyCoefficients(3, aprs->m_a4, station)); + if (aprs->m_a5HasValue) + telemetryA5Item->setData(Qt::DisplayRole, applyCoefficients(4, aprs->m_a5, station)); + if (aprs->m_bHasValue) + { + telemetryB1Item->setData(Qt::DisplayRole, aprs->m_b[0] ? 1 : 0); + telemetryB2Item->setData(Qt::DisplayRole, aprs->m_b[1] ? 1 : 0); + telemetryB3Item->setData(Qt::DisplayRole, aprs->m_b[2] ? 1 : 0); + telemetryB4Item->setData(Qt::DisplayRole, aprs->m_b[3] ? 1 : 0); + telemetryB5Item->setData(Qt::DisplayRole, aprs->m_b[4] ? 1 : 0); + telemetryB6Item->setData(Qt::DisplayRole, aprs->m_b[5] ? 1 : 0); + telemetryB7Item->setData(Qt::DisplayRole, aprs->m_b[6] ? 1 : 0); + telemetryB8Item->setData(Qt::DisplayRole, aprs->m_b[7] ? 1 : 0); + } + telemetryCommentItem->setText(aprs->m_telemetryComment); + + for (int i = 0; i < station->m_telemetryNames.size(); i++) + { + QString header; + if (station->m_telemetryLabels.size() <= i) + header = station->m_telemetryNames[i]; + else + header = QString("%1 (%2)").arg(station->m_telemetryNames[i]).arg(station->m_telemetryLabels[i]); + ui->telemetryTable->horizontalHeaderItem(TELEMETRY_COL_A1+i)->setText(header); + } + } + + // Motion table + if (aprs->m_hasPosition || aprs->m_hasAltitude || aprs->m_hasCourseAndSpeed) + { + row = ui->motionTable->rowCount(); + ui->motionTable->setRowCount(row + 1); + QTableWidgetItem *motionDateItem = new QTableWidgetItem(); + QTableWidgetItem *motionTimeItem = new QTableWidgetItem(); + QTableWidgetItem *latitudeItem = new QTableWidgetItem(); + QTableWidgetItem *longitudeItem = new QTableWidgetItem(); + QTableWidgetItem *altitudeItem = new QTableWidgetItem(); + QTableWidgetItem *courseItem = new QTableWidgetItem(); + QTableWidgetItem *speedItem = new QTableWidgetItem(); + ui->motionTable->setItem(row, MOTION_COL_DATE, motionDateItem); + ui->motionTable->setItem(row, MOTION_COL_TIME, motionTimeItem); + ui->motionTable->setItem(row, MOTION_COL_LATITUDE, latitudeItem); + ui->motionTable->setItem(row, MOTION_COL_LONGITUDE, longitudeItem); + ui->motionTable->setItem(row, MOTION_COL_ALTITUDE, altitudeItem); + ui->motionTable->setItem(row, MOTION_COL_COURSE, courseItem); + ui->motionTable->setItem(row, MOTION_COL_SPEED, speedItem); + motionDateItem->setData(Qt::DisplayRole, formatDate(aprs)); + motionTimeItem->setData(Qt::DisplayRole, formatTime(aprs)); + if (aprs->m_hasPosition) + { + latitudeItem->setData(Qt::DisplayRole, aprs->m_latitude); + longitudeItem->setData(Qt::DisplayRole, aprs->m_longitude); + } + if (aprs->m_hasAltitude) + altitudeItem->setData(Qt::DisplayRole, aprs->m_altitudeFt); + if (aprs->m_hasCourseAndSpeed) + { + courseItem->setData(Qt::DisplayRole, aprs->m_course); + speedItem->setData(Qt::DisplayRole, aprs->m_speed); + } + } + + // Packets table + row = ui->packetsTable->rowCount(); + ui->packetsTable->setRowCount(row + 1); + QTableWidgetItem *dateItem = new QTableWidgetItem(); + QTableWidgetItem *timeItem = new QTableWidgetItem(); + QTableWidgetItem *fromItem = new QTableWidgetItem(); + QTableWidgetItem *toItem = new QTableWidgetItem(); + QTableWidgetItem *viaItem = new QTableWidgetItem(); + QTableWidgetItem *dataItem = new QTableWidgetItem(); + ui->packetsTable->setItem(row, PACKETS_COL_DATE, dateItem); + ui->packetsTable->setItem(row, PACKETS_COL_TIME, timeItem); + ui->packetsTable->setItem(row, PACKETS_COL_FROM, fromItem); + ui->packetsTable->setItem(row, PACKETS_COL_TO, toItem); + ui->packetsTable->setItem(row, PACKETS_COL_VIA, viaItem); + ui->packetsTable->setItem(row, PACKETS_COL_DATA, dataItem); + dateItem->setData(Qt::DisplayRole, formatDate(aprs)); + timeItem->setData(Qt::DisplayRole, formatTime(aprs)); + fromItem->setText(aprs->m_from); + toItem->setText(aprs->m_to); + viaItem->setText(aprs->m_via); + dataItem->setText(aprs->m_data); +} + +void APRSGUI::on_stationFilter_currentIndexChanged(int index) +{ + m_settings.m_stationFilter = static_cast(index); + applySettings(); + filterStations(); +} + +void APRSGUI::on_stationSelect_currentIndexChanged(int index) +{ + QString stationCallsign = ui->stationSelect->currentText(); + + APRSStation *station = m_stations.value(stationCallsign); + if (station == nullptr) + { + qDebug() << "APRSGUI::on_stationSelect_currentIndexChanged - station==nullptr"; + return; + } + + // Clear tables + ui->weatherTable->setRowCount(0); + ui->packetsTable->setRowCount(0); + ui->statusTable->setRowCount(0); + ui->telemetryTable->setRowCount(0); + ui->motionTable->setRowCount(0); + + // Set telemetry plot select combo text + const char *telemetryNames[] = {"A1", "A2", "A3", "A4", "A5", "B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8"}; + int telemetryNamesSize = station->m_telemetryNames.size(); + for (int i = 0; i < 12; i++) + { + if (i < telemetryNamesSize) + ui->telemetryPlotSelect->setItemText(i, station->m_telemetryNames[i]); + else + ui->telemetryPlotSelect->setItemText(i, telemetryNames[i]); + } + + // Update summary table + updateSummary(station); + + // Display/hide fields depending if station or object + ui->stationObjectLabel->setText(station->m_isObject ? "Object" : "Station"); + ui->station->setToolTip(station->m_isObject ? "Object name" : "Station callsign and substation ID"); + ui->reportingStation->setVisible(station->m_isObject); + ui->reportingStationLabel->setVisible(station->m_isObject); + ui->status->setVisible(!station->m_isObject); + ui->statusLabel->setVisible(!station->m_isObject); + if (station->m_isObject) + { + if ( ui->stationsTabWidget->count() == 6) + { + ui->stationsTabWidget->removeTab(3); + ui->stationsTabWidget->removeTab(3); + } + } + else + { + if ( ui->stationsTabWidget->count() != 6) + { + ui->stationsTabWidget->insertTab(3, ui->telemetryTab, "Telemetry"); + ui->stationsTabWidget->insertTab(4, ui->statusTab, "Status"); + } + } + + // Add packets to tables + ui->packetsTable->setSortingEnabled(false); + ui->weatherTable->setSortingEnabled(false); + ui->statusTable->setSortingEnabled(false); + ui->telemetryTable->setSortingEnabled(false); + ui->motionTable->setSortingEnabled(false); + QListIterator i(station->m_packets); + while (i.hasNext()) + { + APRSPacket *aprs = i.next(); + addPacketToGUI(station, aprs); + } + ui->packetsTable->setSortingEnabled(true); + ui->weatherTable->setSortingEnabled(true); + ui->statusTable->setSortingEnabled(true); + ui->telemetryTable->setSortingEnabled(true); + ui->motionTable->setSortingEnabled(true); + plotWeather(); + plotTelemetry(); + plotMotion(); +} + +void APRSGUI::on_filterAddressee_editingFinished() +{ + m_settings.m_filterAddressee = ui->filterAddressee->text(); + filterMessages(); + applySettings(); +} + +void APRSGUI::filterMessageRow(int row) +{ + bool hidden = false; + if (m_settings.m_filterAddressee != "") + { + QRegExp re(m_settings.m_filterAddressee); + QTableWidgetItem *addressee = ui->messagesTable->item(row, MESSAGE_COL_ADDRESSEE); + if (!re.exactMatch(addressee->text())) + hidden = true; + } + ui->messagesTable->setRowHidden(row, hidden); +} + +void APRSGUI::filterMessages() +{ + for (int i = 0; i < ui->messagesTable->rowCount(); i++) + filterMessageRow(i); +} + +void APRSGUI::on_deleteMessages_clicked() +{ + QList list = ui->messagesTable->selectedItems(); + QList rows; + if (list.isEmpty()) + { + // Delete all messages + if (QMessageBox::Yes == QMessageBox::question(this, "Delete all messages", "Delete all messages?", QMessageBox::Yes|QMessageBox::No)) + ui->messagesTable->setRowCount(0); + } + else + { + // Delete selected messages (in reverse order) + for (int i = 0; i < list.size(); i++) + { + int row = list[i]->row(); + if (!rows.contains(row)) + rows.append(row); + } + std::sort(rows.begin(), rows.end(), std::greater()); + QListIterator itr(rows); + while (itr.hasNext()) + ui->messagesTable->removeRow(itr.next()); + } +} + +static void addToSeries(QLineSeries *series, const QDateTime& dt, double value, double& min, double &max) +{ + series->append(dt.toMSecsSinceEpoch(), value); + if (value < min) + min = value; + if (value > max) + max = value; +} + +void APRSGUI::plotWeather() +{ + QString stationCallsign = ui->stationSelect->currentText(); + if (stationCallsign.isEmpty()) + return; + APRSStation *station = m_stations.value(stationCallsign); + if (station == nullptr) + return; + + QLineSeries *series = new QLineSeries(); + double minValue = INFINITY; + double maxValue = -INFINITY; + + int timeSelectIdx = ui->weatherTimeSelect->currentIndex(); + int plotSelectIdx = ui->weatherPlotSelect->currentIndex(); + QDateTime limit = calcTimeLimit(timeSelectIdx); + + QListIterator i(station->m_packets); + while (i.hasNext()) + { + APRSPacket *aprs = i.next(); + if (aprs->m_hasWeather) + { + QDateTime dt; + if (aprs->m_hasTimestamp) + dt = aprs->m_timestamp; + else + dt = aprs->m_dateTime; + + if (dt >= limit) + { + if (plotSelectIdx == 0 && aprs->m_hasWindDirection) + addToSeries(series, dt, aprs->m_windDirection, minValue, maxValue); + else if (plotSelectIdx == 1 && aprs->m_hasWindSpeed) + addToSeries(series, dt, aprs->m_windSpeed, minValue, maxValue); + else if (plotSelectIdx == 2 && aprs->m_hasGust) + addToSeries(series, dt, aprs->m_gust, minValue, maxValue); + else if (plotSelectIdx == 3 && aprs->m_hasTemp) + addToSeries(series, dt, aprs->m_temp, minValue, maxValue); + else if (plotSelectIdx == 4 && aprs->m_hasHumidity) + addToSeries(series, dt, aprs->m_humidity, minValue, maxValue); + else if (plotSelectIdx == 5 && aprs->m_hasBarometricPressure) + addToSeries(series, dt, aprs->m_barometricPressure/10.0, minValue, maxValue); + else if (plotSelectIdx == 6 && aprs->m_hasRainLastHr) + addToSeries(series, dt, aprs->m_rainLastHr, minValue, maxValue); + else if (plotSelectIdx == 7 && aprs->m_hasRainLast24Hrs) + addToSeries(series, dt, aprs->m_rainLast24Hrs, minValue, maxValue); + else if (plotSelectIdx == 8 && aprs->m_hasRainSinceMidnight) + addToSeries(series, dt, aprs->m_rainSinceMidnight, minValue, maxValue); + else if (plotSelectIdx == 9 && aprs->m_hasLuminsoity) + addToSeries(series, dt, aprs->m_luminosity, minValue, maxValue); + else if (plotSelectIdx == 10 && aprs->m_hasSnowfallLast24Hrs) + addToSeries(series, dt, aprs->m_snowfallLast24Hrs, minValue, maxValue); + else if (plotSelectIdx == 11 && aprs->m_hasRadiationLevel) + addToSeries(series, dt, aprs->m_radiationLevel, minValue, maxValue); + else if (plotSelectIdx == 12 && aprs->m_hasFloodLevel) + addToSeries(series, dt, aprs->m_floodLevel, minValue, maxValue); + } + } + } + m_weatherChart.removeAllSeries(); + m_weatherChart.removeAxis(&m_weatherChartXAxis); + m_weatherChart.removeAxis(&m_weatherChartYAxis); + + m_weatherChart.addSeries(series); + + calcTimeAxis(timeSelectIdx, &m_weatherChartXAxis, series, ui->weatherChart->width()); + + m_weatherChart.addAxis(&m_weatherChartXAxis, Qt::AlignBottom); + + series->attachAxis(&m_weatherChartXAxis); + + m_weatherChartYAxis.setTitleText(ui->weatherPlotSelect->currentText()); + calcYAxis(minValue, maxValue, &m_weatherChartYAxis); + m_weatherChart.addAxis(&m_weatherChartYAxis, Qt::AlignLeft); + series->attachAxis(&m_weatherChartYAxis); +} + +void APRSGUI::on_weatherTimeSelect_currentIndexChanged(int index) +{ + plotWeather(); +} + +void APRSGUI::on_weatherPlotSelect_currentIndexChanged(int index) +{ + plotWeather(); +} + +void APRSGUI::calcTimeAxis(int timeSelectIdx, QDateTimeAxis *axis, QLineSeries *series, int width) +{ + QDateTime startX = QDateTime::currentDateTime(); + QDateTime finishX = QDateTime::currentDateTime(); + finishX.setTime(QTime(0,0,0)); + finishX = finishX.addDays(1); + int ticksScale = width < 650 ? 1 : 2; // FIXME: Should probably measure the width of some actual text + switch (timeSelectIdx) + { + case 0: // Today + startX.setTime(QTime(0,0,0)); + axis->setTickCount(6*ticksScale+1); + axis->setFormat("hh:mm"); + axis->setTitleText(QString("Time (%1)").arg(startX.date().toString())); + break; + case 1: // Last hour + startX.setTime(startX.time().addSecs(-60*60)); + finishX = QDateTime::currentDateTime(); + ticksScale = width < 750 ? 1 : 2; + axis->setTickCount(8*ticksScale+1); + axis->setFormat("hh:mm"); + axis->setTitleText(QString("Time (%1)").arg(startX.date().toString())); + break; + case 2: // Last 24h + startX.setDate(startX.date().addDays(-1)); + finishX = QDateTime::currentDateTime(); + axis->setTickCount(6*ticksScale+1); + axis->setFormat("hh:mm"); + axis->setTitleText(QString("Time (%1)").arg(startX.date().toString())); + break; + case 3: // Last 7 days + startX.setTime(QTime(0,0,0)); + startX.setDate(finishX.date().addDays(-7)); + axis->setTickCount(4*ticksScale); + axis->setFormat("MMM d"); + axis->setTitleText("Date"); + break; + case 4: // Last 30 days + startX.setTime(QTime(0,0,0)); + startX.setDate(finishX.date().addDays(-30)); + axis->setTickCount(5*ticksScale+1); + axis->setFormat("MMM d"); + axis->setTitleText("Date"); + break; + case 5: // All + startX = QDateTime::fromMSecsSinceEpoch(series->at(0).x()); + finishX = QDateTime::fromMSecsSinceEpoch(series->at(series->count()-1).x()); + // FIXME: Problem when startX == finishX - axis not drawn + if (startX.msecsTo(finishX) > 1000*60*60*24) + { + axis->setFormat("MMM d"); + axis->setTitleText("Date"); + } + else if (startX.msecsTo(finishX) > 1000*60*60) + { + axis->setFormat("hh:mm"); + axis->setTitleText(QString("Time (hours on %1)").arg(startX.date().toString())); + } + else + { + axis->setFormat("mm:ss"); + axis->setTitleText(QString("Time (minutes on %1)").arg(startX.date().toString())); + } + axis->setTickCount(5*ticksScale); + break; + } + axis->setRange(startX, finishX); +} + +void APRSGUI::calcYAxis(double minValue, double maxValue, QValueAxis *axis, bool binary, int precision) +{ + double range = std::abs(maxValue - minValue); + double ticks = binary ? 2 : 5; + + axis->setTickCount(ticks); + + if (binary) + { + minValue = 0.0; + maxValue = 1.0; + } + else if (range == 0.0) + { + // Nothing is plotted if min and max are the same, so adjust range to non-zero + if (precision == 1) + { + if ((minValue >= (ticks-1)/2) || (minValue < 0.0)) + { + maxValue += (ticks-1)/2; + minValue -= (ticks-1)/2; + } + else + maxValue = maxValue + (ticks - 1 - minValue); + } + else if (maxValue == 0.0) + maxValue = ticks-1; + else + { + minValue -= minValue * (1.0/std::pow(10.0,precision)); + maxValue += maxValue * (1.0/std::pow(10.0,precision)); + } + range = std::abs(maxValue - minValue); + } + axis->setRange(minValue, maxValue); + if (((range < (ticks-1)) || (precision > 1)) && !binary) + axis->setLabelFormat(QString("%.%1f").arg(precision)); + else + axis->setLabelFormat("%d"); +} + +QDateTime APRSGUI::calcTimeLimit(int timeSelectIdx) +{ + QDateTime limit = QDateTime::currentDateTime(); + switch (timeSelectIdx) + { + case 0: // Today + limit.setTime(QTime(0,0,0)); + break; + case 1: // Last hour + limit.setTime(limit.time().addSecs(-60*60)); + break; + case 2: // Last 24h + limit.setDate(limit.date().addDays(-1)); + break; + case 3: // Last 7 days + limit.setTime(QTime(0,0,0)); + limit.setDate(limit.date().addDays(-7)); + break; + case 4: // Last 30 days + limit.setTime(QTime(0,0,0)); + limit.setDate(limit.date().addDays(-30)); + break; + case 5: // All + limit = QDateTime(QDate(1970, 1, 1), QTime()); + break; + } + return limit; +} + +void APRSGUI::plotMotion() +{ + QString stationCallsign = ui->stationSelect->currentText(); + if (stationCallsign.isEmpty()) + return; + APRSStation *station = m_stations.value(stationCallsign); + if (station == nullptr) + return; + + QLineSeries *series = new QLineSeries(); + double minValue = INFINITY; + double maxValue = -INFINITY; + + int timeSelectIdx = ui->motionTimeSelect->currentIndex(); + int plotSelectIdx = ui->motionPlotSelect->currentIndex(); + QDateTime limit = calcTimeLimit(timeSelectIdx); + + QListIterator i(station->m_packets); + while (i.hasNext()) + { + APRSPacket *aprs = i.next(); + + if (aprs->m_hasPosition || aprs->m_hasAltitude || aprs->m_hasCourseAndSpeed) + { + QDateTime dt; + if (aprs->m_hasTimestamp) + dt = aprs->m_timestamp; + else + dt = aprs->m_dateTime; + + if (dt >= limit) + { + if (plotSelectIdx == 0 && aprs->m_hasPosition) + addToSeries(series, dt, aprs->m_latitude, minValue, maxValue); + else if (plotSelectIdx == 1 && aprs->m_hasPosition) + addToSeries(series, dt, aprs->m_longitude, minValue, maxValue); + else if (plotSelectIdx == 2 && aprs->m_hasAltitude) + addToSeries(series, dt, aprs->m_altitudeFt, minValue, maxValue); + else if (plotSelectIdx == 3 && aprs->m_hasCourseAndSpeed) + addToSeries(series, dt, aprs->m_course, minValue, maxValue); + else if (plotSelectIdx == 4 && aprs->m_hasCourseAndSpeed) + addToSeries(series, dt, aprs->m_speed, minValue, maxValue); + } + } + } + + m_motionChart.removeAllSeries(); + m_motionChart.removeAxis(&m_motionChartXAxis); + m_motionChart.removeAxis(&m_motionChartYAxis); + + m_motionChart.addSeries(series); + calcTimeAxis(timeSelectIdx, &m_motionChartXAxis, series, ui->motionChart->width()); + m_motionChart.addAxis(&m_motionChartXAxis, Qt::AlignBottom); + series->attachAxis(&m_motionChartXAxis); + + m_motionChartYAxis.setTitleText(ui->motionPlotSelect->currentText()); + calcYAxis(minValue, maxValue, &m_motionChartYAxis, false, plotSelectIdx <= 1 ? 5 : 1); + m_motionChart.addAxis(&m_motionChartYAxis, Qt::AlignLeft); + series->attachAxis(&m_motionChartYAxis); +} + +void APRSGUI::on_motionTimeSelect_currentIndexChanged(int index) +{ + plotMotion(); +} + +void APRSGUI::on_motionPlotSelect_currentIndexChanged(int index) +{ + plotMotion(); +} + +void APRSGUI::plotTelemetry() +{ + QString stationCallsign = ui->stationSelect->currentText(); + if (stationCallsign.isEmpty()) + return; + APRSStation *station = m_stations.value(stationCallsign); + if (station == nullptr) + return; + + QLineSeries *series = new QLineSeries(); + double minValue = INFINITY; + double maxValue = -INFINITY; + + int timeSelectIdx = ui->telemetryTimeSelect->currentIndex(); + int plotSelectIdx = ui->telemetryPlotSelect->currentIndex(); + QDateTime limit = calcTimeLimit(timeSelectIdx); + + QListIterator i(station->m_packets); + while (i.hasNext()) + { + APRSPacket *aprs = i.next(); + if (aprs->m_hasTelemetry) + { + if (aprs->m_dateTime >= limit) + { + if (plotSelectIdx == 0 && aprs->m_a1HasValue) + addToSeries(series, aprs->m_dateTime, applyCoefficients(0, aprs->m_a1, station), minValue, maxValue); + else if (plotSelectIdx == 1 && aprs->m_a2HasValue) + addToSeries(series, aprs->m_dateTime, applyCoefficients(1, aprs->m_a2, station), minValue, maxValue); + else if (plotSelectIdx == 2 && aprs->m_a3HasValue) + addToSeries(series, aprs->m_dateTime, applyCoefficients(2, aprs->m_a3, station), minValue, maxValue); + else if (plotSelectIdx == 3 && aprs->m_a4HasValue) + addToSeries(series, aprs->m_dateTime, applyCoefficients(3, aprs->m_a4, station), minValue, maxValue); + else if (plotSelectIdx == 4 && aprs->m_a5HasValue) + addToSeries(series, aprs->m_dateTime, applyCoefficients(4, aprs->m_a5, station), minValue, maxValue); + else if (plotSelectIdx == 5 && aprs->m_bHasValue) + addToSeries(series, aprs->m_dateTime, aprs->m_b[0], minValue, maxValue); + else if (plotSelectIdx == 6 && aprs->m_bHasValue) + addToSeries(series, aprs->m_dateTime, aprs->m_b[1], minValue, maxValue); + else if (plotSelectIdx == 7 && aprs->m_bHasValue) + addToSeries(series, aprs->m_dateTime, aprs->m_b[2], minValue, maxValue); + else if (plotSelectIdx == 8 && aprs->m_bHasValue) + addToSeries(series, aprs->m_dateTime, aprs->m_b[3], minValue, maxValue); + else if (plotSelectIdx == 9 && aprs->m_bHasValue) + addToSeries(series, aprs->m_dateTime, aprs->m_b[4], minValue, maxValue); + else if (plotSelectIdx == 10 && aprs->m_bHasValue) + addToSeries(series, aprs->m_dateTime, aprs->m_b[5], minValue, maxValue); + else if (plotSelectIdx == 11 && aprs->m_bHasValue) + addToSeries(series, aprs->m_dateTime, aprs->m_b[6], minValue, maxValue); + else if (plotSelectIdx == 12 && aprs->m_bHasValue) + addToSeries(series, aprs->m_dateTime, aprs->m_b[7], minValue, maxValue); + } + } + } + + m_telemetryChart.removeAllSeries(); + m_telemetryChart.removeAxis(&m_telemetryChartXAxis); + m_telemetryChart.removeAxis(&m_telemetryChartYAxis); + + m_telemetryChart.addSeries(series); + calcTimeAxis(timeSelectIdx, &m_telemetryChartXAxis, series, ui->telemetryChart->width()); + m_telemetryChart.addAxis(&m_telemetryChartXAxis, Qt::AlignBottom); + + series->attachAxis(&m_telemetryChartXAxis); + m_telemetryChartYAxis.setTitleText(ui->telemetryPlotSelect->currentText()); + calcYAxis(minValue, maxValue, &m_telemetryChartYAxis, plotSelectIdx >= 5); + m_telemetryChart.addAxis(&m_telemetryChartYAxis, Qt::AlignLeft); + series->attachAxis(&m_telemetryChartYAxis); +} + +void APRSGUI::on_telemetryTimeSelect_currentIndexChanged(int index) +{ + plotTelemetry(); +} + +void APRSGUI::on_telemetryPlotSelect_currentIndexChanged(int index) +{ + plotTelemetry(); +} + +void APRSGUI::updateStatus() +{ + int state = m_aprs->getState(); + + if (m_lastFeatureState != state) + { + switch (state) + { + case Feature::StNotStarted: + ui->igate->setStyleSheet("QToolButton { background:rgb(79,79,79); }"); + break; + case Feature::StIdle: + ui->igate->setStyleSheet("QToolButton { background:rgb(79,79,79); }"); + break; + case Feature::StRunning: + ui->igate->setStyleSheet("QToolButton { background-color : green; }"); + break; + case Feature::StError: + ui->igate->setStyleSheet("QToolButton { background-color : red; }"); + QMessageBox::information(this, tr("Message"), m_aprs->getErrorMessage()); + break; + default: + break; + } + + m_lastFeatureState = state; + } +} + +void APRSGUI::applySettings(bool force) +{ + if (m_doApplySettings) + { + APRS::MsgConfigureAPRS* message = APRS::MsgConfigureAPRS::create(m_settings, force); + m_aprs->getInputMessageQueue()->push(message); + } +} + +void APRSGUI::resizeTable() +{ + int row; + + // Fill tables with a row of dummy data that will size the columns nicely + row = ui->packetsTable->rowCount(); + ui->packetsTable->setRowCount(row + 1); + ui->packetsTable->setItem(row, PACKETS_COL_DATE, new QTableWidgetItem("31/12/2020")); + ui->packetsTable->setItem(row, PACKETS_COL_TIME, new QTableWidgetItem("23:59:39 T")); + ui->packetsTable->setItem(row, PACKETS_COL_FROM, new QTableWidgetItem("123456-15-")); + ui->packetsTable->setItem(row, PACKETS_COL_TO, new QTableWidgetItem("123456-15-")); + ui->packetsTable->setItem(row, PACKETS_COL_VIA, new QTableWidgetItem("123456-15-")); + ui->packetsTable->setItem(row, PACKETS_COL_DATA, new QTableWidgetItem("ABCEDGHIJKLMNOPQRSTUVWXYZABCEDGHIJKLMNOPQRSTUVWXYZABCEDGHIJKLMNOPQRSTUVWXYZ")); + ui->packetsTable->resizeColumnsToContents(); + ui->packetsTable->removeRow(row); + + row = ui->weatherTable->rowCount(); + ui->weatherTable->setRowCount(row + 1); + ui->weatherTable->setItem(row, WEATHER_COL_DATE, new QTableWidgetItem("31/12/2020")); + ui->weatherTable->setItem(row, WEATHER_COL_TIME, new QTableWidgetItem("23:59:39 T")); + ui->weatherTable->setItem(row, WEATHER_COL_WIND_DIRECTION, new QTableWidgetItem("Dir (o)-")); + ui->weatherTable->setItem(row, WEATHER_COL_WIND_SPEED, new QTableWidgetItem("Speed (mph)-")); + ui->weatherTable->setItem(row, WEATHER_COL_GUSTS, new QTableWidgetItem("Gusts (mph)-")); + ui->weatherTable->setItem(row, WEATHER_COL_TEMPERATURE, new QTableWidgetItem("Temp (F)-")); + ui->weatherTable->setItem(row, WEATHER_COL_HUMIDITY, new QTableWidgetItem("Humidity (%)-")); + ui->weatherTable->setItem(row, WEATHER_COL_PRESSURE, new QTableWidgetItem("Pressure (mbar)-")); + ui->weatherTable->setItem(row, WEATHER_COL_RAIN_LAST_HOUR, new QTableWidgetItem("Rain 1h-")); + ui->weatherTable->setItem(row, WEATHER_COL_RAIN_LAST_24_HOURS, new QTableWidgetItem("Rain 24h-")); + ui->weatherTable->setItem(row, WEATHER_COL_RAIN_SINCE_MIDNIGHT, new QTableWidgetItem("Rain-")); + ui->weatherTable->setItem(row, WEATHER_COL_LUMINOSITY, new QTableWidgetItem("Luminosity-")); + ui->weatherTable->setItem(row, WEATHER_COL_SNOWFALL, new QTableWidgetItem("Snowfall-")); + ui->weatherTable->setItem(row, WEATHER_COL_RADIATION_LEVEL, new QTableWidgetItem("Radiation-")); + ui->weatherTable->setItem(row, WEATHER_COL_FLOOD_LEVEL, new QTableWidgetItem("Flood level-")); + ui->weatherTable->resizeColumnsToContents(); + ui->weatherTable->removeRow(row); + + row = ui->statusTable->rowCount(); + ui->statusTable->setRowCount(row + 1); + ui->statusTable->setIconSize(QSize(24, 24)); + ui->statusTable->setItem(row, STATUS_COL_DATE, new QTableWidgetItem("31/12/2020")); + ui->statusTable->setItem(row, STATUS_COL_TIME, new QTableWidgetItem("23:59:39 T")); + ui->statusTable->setItem(row, STATUS_COL_STATUS, new QTableWidgetItem("ABCDEFGHIJKLMNOPQRSTUVWXYZ")); + ui->statusTable->setItem(row, STATUS_COL_SYMBOL, new QTableWidgetItem("WWW")); + ui->statusTable->setItem(row, STATUS_COL_MAIDENHEAD, new QTableWidgetItem("WW00WW")); + ui->statusTable->setItem(row, STATUS_COL_BEAM_HEADING, new QTableWidgetItem("359")); + ui->statusTable->setItem(row, STATUS_COL_BEAM_POWER, new QTableWidgetItem("8000")); + ui->statusTable->resizeColumnsToContents(); + ui->statusTable->removeRow(row); + + row = ui->messagesTable->rowCount(); + ui->messagesTable->setRowCount(row + 1); + ui->messagesTable->setIconSize(QSize(24, 24)); + ui->messagesTable->setItem(row, MESSAGE_COL_DATE, new QTableWidgetItem("31/12/2020")); + ui->messagesTable->setItem(row, MESSAGE_COL_TIME, new QTableWidgetItem("23:59:39 T")); + ui->messagesTable->setItem(row, MESSAGE_COL_ADDRESSEE, new QTableWidgetItem("WWWWWWWWW")); + ui->messagesTable->setItem(row, MESSAGE_COL_MESSAGE, new QTableWidgetItem("ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ")); + ui->messagesTable->setItem(row, MESSAGE_COL_NO, new QTableWidgetItem("Message No")); + ui->messagesTable->resizeColumnsToContents(); + ui->messagesTable->removeRow(row); + + row = ui->motionTable->rowCount(); + ui->motionTable->setRowCount(row + 1); + ui->motionTable->setItem(row, MOTION_COL_DATE, new QTableWidgetItem("31/12/2020")); + ui->motionTable->setItem(row, MOTION_COL_TIME, new QTableWidgetItem("23:59:39 T")); + ui->motionTable->setItem(row, MOTION_COL_LATITUDE, new QTableWidgetItem("Latitude")); + ui->motionTable->setItem(row, MOTION_COL_LONGITUDE, new QTableWidgetItem("Longitude")); + ui->motionTable->setItem(row, MOTION_COL_ALTITUDE, new QTableWidgetItem("Message No")); + ui->motionTable->setItem(row, MOTION_COL_ALTITUDE, new QTableWidgetItem("Course")); + ui->motionTable->setItem(row, MOTION_COL_ALTITUDE, new QTableWidgetItem("Speed")); + ui->motionTable->resizeColumnsToContents(); + ui->motionTable->removeRow(row); + + row = ui->telemetryTable->rowCount(); + ui->telemetryTable->setRowCount(row + 1); + ui->telemetryTable->setItem(row, TELEMETRY_COL_DATE, new QTableWidgetItem("31/12/2020")); + ui->telemetryTable->setItem(row, TELEMETRY_COL_TIME, new QTableWidgetItem("23:59:39 T")); + ui->telemetryTable->setItem(row, TELEMETRY_COL_SEQ_NO, new QTableWidgetItem("Seq No")); + ui->telemetryTable->setItem(row, TELEMETRY_COL_A1, new QTableWidgetItem("ABCEDF")); + ui->telemetryTable->setItem(row, TELEMETRY_COL_A2, new QTableWidgetItem("ABCEDF")); + ui->telemetryTable->setItem(row, TELEMETRY_COL_A3, new QTableWidgetItem("ABCEDF")); + ui->telemetryTable->setItem(row, TELEMETRY_COL_A4, new QTableWidgetItem("ABCEDF")); + ui->telemetryTable->setItem(row, TELEMETRY_COL_A5, new QTableWidgetItem("ABCEDF")); + ui->telemetryTable->setItem(row, TELEMETRY_COL_B1, new QTableWidgetItem("ABCEDF")); + ui->telemetryTable->setItem(row, TELEMETRY_COL_B2, new QTableWidgetItem("ABCEDF")); + ui->telemetryTable->setItem(row, TELEMETRY_COL_B3, new QTableWidgetItem("ABCEDF")); + ui->telemetryTable->setItem(row, TELEMETRY_COL_B4, new QTableWidgetItem("ABCEDF")); + ui->telemetryTable->setItem(row, TELEMETRY_COL_B5, new QTableWidgetItem("ABCEDF")); + ui->telemetryTable->setItem(row, TELEMETRY_COL_B6, new QTableWidgetItem("ABCEDF")); + ui->telemetryTable->setItem(row, TELEMETRY_COL_B7, new QTableWidgetItem("ABCEDF")); + ui->telemetryTable->setItem(row, TELEMETRY_COL_B8, new QTableWidgetItem("ABCEDF")); + ui->telemetryTable->resizeColumnsToContents(); + ui->telemetryTable->removeRow(row); +} + +// Columns in table reordered +void APRSGUI::packetsTable_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex) +{ + (void) oldVisualIndex; + + m_settings.m_packetsTableColumnIndexes[logicalIndex] = newVisualIndex; +} + +// Column in table resized (when hidden size is 0) +void APRSGUI::packetsTable_sectionResized(int logicalIndex, int oldSize, int newSize) +{ + (void) oldSize; + + m_settings.m_packetsTableColumnSizes[logicalIndex] = newSize; +} + +// Right click in table header - show column select menu +void APRSGUI::packetsTable_columnSelectMenu(QPoint pos) +{ + packetsTableMenu->popup(ui->packetsTable->horizontalHeader()->viewport()->mapToGlobal(pos)); +} + +// Hide/show column when menu selected +void APRSGUI::packetsTable_columnSelectMenuChecked(bool checked) +{ + (void) checked; + + QAction* action = qobject_cast(sender()); + if (action != nullptr) + { + int idx = action->data().toInt(nullptr); + ui->packetsTable->setColumnHidden(idx, !action->isChecked()); + } +} + +// Columns in table reordered +void APRSGUI::weatherTable_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex) +{ + (void) oldVisualIndex; + + m_settings.m_weatherTableColumnIndexes[logicalIndex] = newVisualIndex; +} + +// Column in table resized (when hidden size is 0) +void APRSGUI::weatherTable_sectionResized(int logicalIndex, int oldSize, int newSize) +{ + (void) oldSize; + + m_settings.m_weatherTableColumnSizes[logicalIndex] = newSize; +} + +// Right click in table header - show column select menu +void APRSGUI::weatherTable_columnSelectMenu(QPoint pos) +{ + weatherTableMenu->popup(ui->weatherTable->horizontalHeader()->viewport()->mapToGlobal(pos)); +} + +// Hide/show column when menu selected +void APRSGUI::weatherTable_columnSelectMenuChecked(bool checked) +{ + (void) checked; + + QAction* action = qobject_cast(sender()); + if (action != nullptr) + { + int idx = action->data().toInt(nullptr); + ui->weatherTable->setColumnHidden(idx, !action->isChecked()); + } +} + +// Columns in table reordered +void APRSGUI::statusTable_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex) +{ + (void) oldVisualIndex; + + m_settings.m_statusTableColumnIndexes[logicalIndex] = newVisualIndex; +} + +// Column in table resized (when hidden size is 0) +void APRSGUI::statusTable_sectionResized(int logicalIndex, int oldSize, int newSize) +{ + (void) oldSize; + + m_settings.m_statusTableColumnSizes[logicalIndex] = newSize; +} + +// Right click in table header - show column select menu +void APRSGUI::statusTable_columnSelectMenu(QPoint pos) +{ + statusTableMenu->popup(ui->statusTable->horizontalHeader()->viewport()->mapToGlobal(pos)); +} + +// Hide/show column when menu selected +void APRSGUI::statusTable_columnSelectMenuChecked(bool checked) +{ + (void) checked; + + QAction* action = qobject_cast(sender()); + if (action != nullptr) + { + int idx = action->data().toInt(nullptr); + ui->statusTable->setColumnHidden(idx, !action->isChecked()); + } +} + +// Columns in table reordered +void APRSGUI::messagesTable_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex) +{ + (void) oldVisualIndex; + + m_settings.m_messagesTableColumnIndexes[logicalIndex] = newVisualIndex; +} + +// Column in table resized (when hidden size is 0) +void APRSGUI::messagesTable_sectionResized(int logicalIndex, int oldSize, int newSize) +{ + (void) oldSize; + + m_settings.m_messagesTableColumnSizes[logicalIndex] = newSize; +} + +// Right click in table header - show column select menu +void APRSGUI::messagesTable_columnSelectMenu(QPoint pos) +{ + messagesTableMenu->popup(ui->messagesTable->horizontalHeader()->viewport()->mapToGlobal(pos)); +} + +// Hide/show column when menu selected +void APRSGUI::messagesTable_columnSelectMenuChecked(bool checked) +{ + (void) checked; + + QAction* action = qobject_cast(sender()); + if (action != nullptr) + { + int idx = action->data().toInt(nullptr); + ui->messagesTable->setColumnHidden(idx, !action->isChecked()); + } +} + +// Columns in table reordered +void APRSGUI::telemetryTable_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex) +{ + (void) oldVisualIndex; + + m_settings.m_telemetryTableColumnIndexes[logicalIndex] = newVisualIndex; +} + +// Column in table resized (when hidden size is 0) +void APRSGUI::telemetryTable_sectionResized(int logicalIndex, int oldSize, int newSize) +{ + (void) oldSize; + + m_settings.m_telemetryTableColumnSizes[logicalIndex] = newSize; +} + +// Right click in table header - show column select menu +void APRSGUI::telemetryTable_columnSelectMenu(QPoint pos) +{ + telemetryTableMenu->popup(ui->telemetryTable->horizontalHeader()->viewport()->mapToGlobal(pos)); +} + +// Hide/show column when menu selected +void APRSGUI::telemetryTable_columnSelectMenuChecked(bool checked) +{ + (void) checked; + + QAction* action = qobject_cast(sender()); + if (action != nullptr) + { + int idx = action->data().toInt(nullptr); + ui->telemetryTable->setColumnHidden(idx, !action->isChecked()); + } +} + +// Columns in table reordered +void APRSGUI::motionTable_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex) +{ + (void) oldVisualIndex; + + m_settings.m_motionTableColumnIndexes[logicalIndex] = newVisualIndex; +} + +// Column in table resized (when hidden size is 0) +void APRSGUI::motionTable_sectionResized(int logicalIndex, int oldSize, int newSize) +{ + (void) oldSize; + + m_settings.m_motionTableColumnSizes[logicalIndex] = newSize; +} + +// Right click in table header - show column select menu +void APRSGUI::motionTable_columnSelectMenu(QPoint pos) +{ + motionTableMenu->popup(ui->motionTable->horizontalHeader()->viewport()->mapToGlobal(pos)); +} + +// Hide/show column when menu selected +void APRSGUI::motionTable_columnSelectMenuChecked(bool checked) +{ + (void) checked; + + QAction* action = qobject_cast(sender()); + if (action != nullptr) + { + int idx = action->data().toInt(nullptr); + ui->motionTable->setColumnHidden(idx, !action->isChecked()); + } +} + +// Create column select menu items + +QAction *APRSGUI::packetsTable_createCheckableItem(QString &text, int idx, bool checked) +{ + QAction *action = new QAction(text, this); + action->setCheckable(true); + action->setChecked(checked); + action->setData(QVariant(idx)); + connect(action, SIGNAL(triggered()), this, SLOT(packetsTable_columnSelectMenuChecked())); + return action; +} + +QAction *APRSGUI::weatherTable_createCheckableItem(QString &text, int idx, bool checked) +{ + QAction *action = new QAction(text, this); + action->setCheckable(true); + action->setChecked(checked); + action->setData(QVariant(idx)); + connect(action, SIGNAL(triggered()), this, SLOT(weatherTable_columnSelectMenuChecked())); + return action; +} + +QAction *APRSGUI::statusTable_createCheckableItem(QString &text, int idx, bool checked) +{ + QAction *action = new QAction(text, this); + action->setCheckable(true); + action->setChecked(checked); + action->setData(QVariant(idx)); + connect(action, SIGNAL(triggered()), this, SLOT(statusTable_columnSelectMenuChecked())); + return action; +} + +QAction *APRSGUI::messagesTable_createCheckableItem(QString &text, int idx, bool checked) +{ + QAction *action = new QAction(text, this); + action->setCheckable(true); + action->setChecked(checked); + action->setData(QVariant(idx)); + connect(action, SIGNAL(triggered()), this, SLOT(messagesTable_columnSelectMenuChecked())); + return action; +} + +QAction *APRSGUI::telemetryTable_createCheckableItem(QString &text, int idx, bool checked) +{ + QAction *action = new QAction(text, this); + action->setCheckable(true); + action->setChecked(checked); + action->setData(QVariant(idx)); + connect(action, SIGNAL(triggered()), this, SLOT(telemetryTable_columnSelectMenuChecked())); + return action; +} + +QAction *APRSGUI::motionTable_createCheckableItem(QString &text, int idx, bool checked) +{ + QAction *action = new QAction(text, this); + action->setCheckable(true); + action->setChecked(checked); + action->setData(QVariant(idx)); + connect(action, SIGNAL(triggered()), this, SLOT(motionTable_columnSelectMenuChecked())); + return action; +} + +// Show settings dialog +void APRSGUI::on_displaySettings_clicked() +{ + APRSSettingsDialog dialog(m_settings.m_igateServer, m_settings.m_igateCallsign, + m_settings.m_igatePasscode, m_settings.m_igateFilter); + if (dialog.exec() == QDialog::Accepted) + { + m_settings.m_igateServer = dialog.m_igateServer; + m_settings.m_igateCallsign = dialog.m_igateCallsign; + m_settings.m_igatePasscode = dialog.m_igatePasscode; + m_settings.m_igateFilter = dialog.m_igateFilter; + applySettings(); + } +} + +void APRSGUI::on_igate_toggled(bool checked) +{ + m_settings.m_igateEnabled = checked; + applySettings(); +} + +// Find the selected station on the Map +void APRSGUI::on_viewOnMap_clicked() +{ + QString stationName = ui->stationSelect->currentText(); + if (!stationName.isEmpty()) + { + APRSStation *station = m_stations.value(stationName); + if (station != nullptr) + { + FeatureWebAPIUtils::mapFind(station->m_station); + } + } +} diff --git a/plugins/feature/aprs/aprsgui.h b/plugins/feature/aprs/aprsgui.h new file mode 100644 index 000000000..9523cf4ef --- /dev/null +++ b/plugins/feature/aprs/aprsgui.h @@ -0,0 +1,227 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_APRSGUI_H_ +#define INCLUDE_FEATURE_APRSGUI_H_ + +#include +#include +#include +#include +#include +#include + +#include "feature/featuregui.h" +#include "util/messagequeue.h" +#include "pipes/pipeendpoint.h" +#include "util/aprs.h" +#include "aprssettings.h" + +class PluginAPI; +class FeatureUISet; +class APRS; + +namespace Ui { + class APRSGUI; +} + +using namespace QtCharts; + +class APRSGUI; + +class APRSStation { +public: + + APRSStation(QString& station) : + m_station(station), + m_isObject(false), + m_hasWeather(false), + m_hasTelemetry(false), + m_hasCourseAndSpeed(false) + { + } + + void addPacket(APRSPacket *packet) + { + m_packets.append(packet); + } + +private: + friend APRSGUI; + QString m_station; // Station callsign + QList m_packets; // Packets received for the station + QString m_symbolImage; + QString m_latestStatus; + QString m_latestComment; + QString m_latestPosition; + QString m_latestAltitude; + QString m_latestCourse; + QString m_latestSpeed; + QString m_latestPacket; + QString m_powerWatts; + QString m_antennaHeightFt; + QString m_antennaGainDB; + QString m_antennaDirectivity; + QString m_radioRange; + bool m_isObject; // Is an object or item rather than a station + QString m_reportingStation; + QList m_telemetryNames; + QList m_telemetryLabels; + double m_telemetryCoefficientsA[5]; + double m_telemetryCoefficientsB[5]; + double m_telemetryCoefficientsC[5]; + int m_hasTelemetryCoefficients; + int m_telemetryBitSense[8]; + bool m_hasTelemetryBitSense; + QString m_telemetryProjectName; + bool m_hasWeather; + bool m_hasTelemetry; + bool m_hasCourseAndSpeed; +}; + +class APRSGUI : public FeatureGUI { + Q_OBJECT +public: + static APRSGUI* create(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature); + virtual void destroy(); + + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + virtual MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } + +protected: + void resizeEvent(QResizeEvent* size); + +private: + Ui::APRSGUI* ui; + PluginAPI* m_pluginAPI; + FeatureUISet* m_featureUISet; + APRSSettings m_settings; + bool m_doApplySettings; + QList m_availablePipes; + + APRS* m_aprs; + MessageQueue m_inputMessageQueue; + QTimer m_statusTimer; + int m_lastFeatureState; + + QHash m_stations; // All stations we've recieved packets for. Hashed on callsign + + QMenu *packetsTableMenu; // Column select context menus + QMenu *weatherTableMenu; + QMenu *statusTableMenu; + QMenu *messagesTableMenu; + QMenu *telemetryTableMenu; + QMenu *motionTableMenu; + + QChart m_weatherChart; + QDateTimeAxis m_weatherChartXAxis; + QValueAxis m_weatherChartYAxis; + + QChart m_telemetryChart; + QDateTimeAxis m_telemetryChartXAxis; + QValueAxis m_telemetryChartYAxis; + + QChart m_motionChart; + QDateTimeAxis m_motionChartXAxis; + QValueAxis m_motionChartYAxis; + + explicit APRSGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent = nullptr); + virtual ~APRSGUI(); + + void blockApplySettings(bool block); + void applySettings(bool force = false); + void displayTableSettings(QTableWidget *table, QMenu *menu, int *columnIndexes, int *columnSizes, int columns); + bool filterStation(APRSStation *station); + void filterStations(); + void displaySettings(); + void updatePipeList(); + bool handleMessage(const Message& message); + + void leaveEvent(QEvent*); + void enterEvent(QEvent*); + + void filterMessageRow(int row); + void filterMessages(); + void resizeTable(); + QAction *packetsTable_createCheckableItem(QString& text, int idx, bool checked); + QAction *weatherTable_createCheckableItem(QString& text, int idx, bool checked); + QAction *statusTable_createCheckableItem(QString& text, int idx, bool checked); + QAction *messagesTable_createCheckableItem(QString& text, int idx, bool checked); + QAction *telemetryTable_createCheckableItem(QString& text, int idx, bool checked); + QAction *motionTable_createCheckableItem(QString& text, int idx, bool checked); + + void updateSummary(APRSStation *station); + void addPacketToGUI(APRSStation *station, APRSPacket *aprs); + +private slots: + void onMenuDialogCalled(const QPoint &p); + void onWidgetRolled(QWidget* widget, bool rollDown); + void handleInputMessages(); + void on_stationFilter_currentIndexChanged(int index); + void on_stationSelect_currentIndexChanged(int index); + void on_filterAddressee_editingFinished(); + void on_deleteMessages_clicked(); + QDateTime calcTimeLimit(int timeSelectIdx); + void calcTimeAxis(int timeSelectIdx, QDateTimeAxis *axis, QLineSeries *series, int width); + void calcYAxis(double minValue, double maxValue, QValueAxis *axis, bool binary=false, int precision=1); + QString formatDate(APRSPacket *aprs); + QString formatTime(APRSPacket *aprs); + double applyCoefficients(int idx, int value, APRSStation *station); + void plotWeather(); + void on_weatherTimeSelect_currentIndexChanged(int index); + void on_weatherPlotSelect_currentIndexChanged(int index); + void plotTelemetry(); + void on_telemetryTimeSelect_currentIndexChanged(int index); + void on_telemetryPlotSelect_currentIndexChanged(int index); + void plotMotion(); + void on_motionTimeSelect_currentIndexChanged(int index); + void on_motionPlotSelect_currentIndexChanged(int index); + void updateStatus(); + void packetsTable_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex); + void packetsTable_sectionResized(int logicalIndex, int oldSize, int newSize); + void packetsTable_columnSelectMenu(QPoint pos); + void packetsTable_columnSelectMenuChecked(bool checked = false); + void weatherTable_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex); + void weatherTable_sectionResized(int logicalIndex, int oldSize, int newSize); + void weatherTable_columnSelectMenu(QPoint pos); + void weatherTable_columnSelectMenuChecked(bool checked = false); + void statusTable_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex); + void statusTable_sectionResized(int logicalIndex, int oldSize, int newSize); + void statusTable_columnSelectMenu(QPoint pos); + void statusTable_columnSelectMenuChecked(bool checked = false); + void messagesTable_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex); + void messagesTable_sectionResized(int logicalIndex, int oldSize, int newSize); + void messagesTable_columnSelectMenu(QPoint pos); + void messagesTable_columnSelectMenuChecked(bool checked = false); + void telemetryTable_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex); + void telemetryTable_sectionResized(int logicalIndex, int oldSize, int newSize); + void telemetryTable_columnSelectMenu(QPoint pos); + void telemetryTable_columnSelectMenuChecked(bool checked = false); + void motionTable_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex); + void motionTable_sectionResized(int logicalIndex, int oldSize, int newSize); + void motionTable_columnSelectMenu(QPoint pos); + void motionTable_columnSelectMenuChecked(bool checked = false); + void on_displaySettings_clicked(); + void on_igate_toggled(bool checked); + void on_viewOnMap_clicked(); +}; + + +#endif // INCLUDE_FEATURE_APRSGUI_H_ diff --git a/plugins/feature/aprs/aprsgui.ui b/plugins/feature/aprs/aprsgui.ui new file mode 100644 index 000000000..9efa848c0 --- /dev/null +++ b/plugins/feature/aprs/aprsgui.ui @@ -0,0 +1,1514 @@ + + + APRSGUI + + + + 0 + 0 + 469 + 761 + + + + + 0 + 0 + + + + + 462 + 0 + + + + + Liberation Sans + 9 + + + + Qt::StrongFocus + + + APRS + + + APRS + + + + + 0 + 0 + 461 + 31 + + + + + 350 + 0 + + + + Settings + + + + 3 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + + + Sources + + + + + + + + 150 + 0 + + + + APRS packet source channels + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Enable APRS-IS IGate (Internet gateway) + + + ... + + + + :/txon.png:/txon.png + + + true + + + + + + + Show settings dialog + + + + + + + :/listing.png:/listing.png + + + + + + + + + + + 0 + 40 + 461 + 532 + + + + + 0 + 0 + + + + APRS + + + + 2 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QTabWidget::West + + + 1 + + + + Stations and Objects + + + + + + + + + 140 + 0 + + + + Selects which stations and objects to list in the adjacent list + + + + All stations/objects + + + + + Stations + + + + + Objects + + + + + Weather stations + + + + + Telemetry + + + + + Course and speed + + + + + + + + + 100 + 0 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Find on the map + + + + + + + :/gridpolar.png:/gridpolar.png + + + + + + + + + 0 + + + + Summary + + + + + + Position + + + + + + + Radio Range + + + + + + + Last course + + + + + + + Antenna Height + + + + + + + Status + + + + + + + Antenna direction + + + + + + + Last comment for this station + + + true + + + + + + + Feet + + + + + + + Antenna Gain + + + + + + + Last position + + + true + + + + + + + Speed + + + + + + + Last status for this station + + + true + + + + + + + Altitude + + + + + + + + + + + + + + Range of radio + + + + + + + dB + + + + + + + Antenna gain + + + + + + + Time last packet was received from this station + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Last speed + + + + + + + Symbol + + + + + + + Antenna Directivity + + + + + + + ° + + + + + + + Comment + + + + + + + + + + Course + + + + + + + Watts + + + + + + + Antenna height + + + + + + + TX Power + + + + + + + Feet + + + + + + + Transmit power + + + + + + + Station callsign and substation ID + + + true + + + + + + + Last altitude + + + + + + + Station/Object + + + + + + + Knots + + + + + + + Miles + + + + + + + Last Packet: + + + + + + + Reporting Station + + + + + + + + Weather + + + + + + + 0 + 150 + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::SelectRows + + + + Date + + + + + Time + + + + + Dir (°) + + + + + Speed (mph) + + + + + Gusts (mph) + + + + + Temp (F) + + + + + Humidity (%) + + + + + Pressure (mbar) + + + + + Rain (last hour) + + + + + Rain (last 24 hours) + + + + + Rain (since midnight) + + + + + Luminosity + + + + + Snowfall + + + + + Radiation + + + + + Flood level + + + + + + + + + + + 0 + 0 + + + + Plot + + + + + + + + 0 + 0 + + + + + 150 + 0 + + + + Select data to plot + + + + Wind direction + + + + + Wind speed + + + + + Gusts + + + + + Temperature + + + + + Humidity + + + + + Pressure + + + + + Rain (last hour) + + + + + Rain (last 24 hours) + + + + + Rain (since midnight) + + + + + Luminosity + + + + + Snowfall + + + + + Radiation + + + + + Flood level + + + + + + + + Qt::Vertical + + + + + + + Time + + + + + + + + 100 + 0 + + + + Time range to plot data for + + + + Today + + + + + Last hour + + + + + Last 24 hours + + + + + Last 7 days + + + + + Last 30 days + + + + + All + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + Motion + + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::SelectRows + + + + Date + + + + + Time + + + + + Latitude + + + + + Longitude + + + + + Altitude + + + + + Course + + + + + Speed + + + + + + + + + + Plot + + + + + + + + 150 + 0 + + + + Select data to plot + + + + Latitude + + + + + Longitude + + + + + Altitude + + + + + Course + + + + + Speed + + + + + + + + Time + + + + + + + + 100 + 0 + + + + Time range to plot data for + + + + Today + + + + + Last hour + + + + + Last 24 hours + + + + + Last 7 days + + + + + Last 30 days + + + + + All + + + + + + + + Qt::Horizontal + + + + 373 + 20 + + + + + + + + + + + + + + Telemetry + + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::SelectRows + + + + Date + + + + + Time + + + + + Seq No + + + + + A1 + + + + + A2 + + + + + A3 + + + + + A4 + + + + + A5 + + + + + B1 + + + + + B2 + + + + + B3 + + + + + B4 + + + + + B5 + + + + + B6 + + + + + B7 + + + + + B8 + + + + + Comment + + + + + + + + + + Plot + + + + + + + + 150 + 0 + + + + Select data to plot + + + + A1 + + + + + A2 + + + + + A3 + + + + + A4 + + + + + A5 + + + + + B1 + + + + + B2 + + + + + B3 + + + + + B4 + + + + + B5 + + + + + B6 + + + + + B7 + + + + + B8 + + + + + + + + Qt::Vertical + + + + + + + Time + + + + + + + + 100 + 0 + + + + Time range to plot data for + + + + Today + + + + + Last hour + + + + + Last 24 hours + + + + + Last 7 days + + + + + Last 30 days + + + + + All + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + Status + + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::SelectRows + + + + Date + + + + + Time + + + + + Status + + + + + Symbol + + + + + Maidenhead + + + + + Beam Heading (°) + + + + + Beam Power (W) + + + + + + + + + Packets + + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::SelectRows + + + + Date + + + + + Time + + + + + From + + + + + To + + + + + Via + + + + + Data + + + + + + + + + + + + + Messages + + + + + + + + Addressee + + + + + + + Display only messages where the addressee matches the specified regular expression + + + + + + + Delete the selected message or all messages if none selected + + + + + + + :/bin.png:/bin.png + + + + + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::SelectRows + + + + Date + + + + + Time + + + + + Addressee + + + + + Message + + + + + Message No + + + + + + + + + + + + + + RollupWidget + QWidget +
gui/rollupwidget.h
+ 1 +
+ + ButtonSwitch + QToolButton +
gui/buttonswitch.h
+
+ + QChartView + QGraphicsView +
QtCharts
+
+
+ + sourcePipes + igate + displaySettings + topTabWidget + stationSelect + viewOnMap + stationsTabWidget + station + reportingStation + status + comment + position + altitude + course + speed + txPower + antennaHeight + antennaGain + antennaDirectivity + radioRange + lastPacket + weatherTable + weatherPlotSelect + weatherTimeSelect + weatherChart + motionTable + motionPlotSelect + motionTimeSelect + motionChart + telemetryTable + telemetryPlotSelect + telemetryTimeSelect + telemetryChart + statusTable + packetsTable + filterAddressee + deleteMessages + messagesTable + + + + + +
diff --git a/plugins/feature/aprs/aprsplugin.cpp b/plugins/feature/aprs/aprsplugin.cpp new file mode 100644 index 000000000..737089aaa --- /dev/null +++ b/plugins/feature/aprs/aprsplugin.cpp @@ -0,0 +1,80 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + + +#include +#include "plugin/pluginapi.h" + +#ifndef SERVER_MODE +#include "aprsgui.h" +#endif +#include "aprs.h" +#include "aprsplugin.h" +#include "aprswebapiadapter.h" + +const PluginDescriptor APRSPlugin::m_pluginDescriptor = { + APRS::m_featureId, + QStringLiteral("APRS"), + QStringLiteral("6.4.0"), + QStringLiteral("(c) Jon Beniston, M7RCE"), + QStringLiteral("https://github.com/f4exb/sdrangel"), + true, + QStringLiteral("https://github.com/f4exb/sdrangel") +}; + +APRSPlugin::APRSPlugin(QObject* parent) : + QObject(parent), + m_pluginAPI(nullptr) +{ +} + +const PluginDescriptor& APRSPlugin::getPluginDescriptor() const +{ + return m_pluginDescriptor; +} + +void APRSPlugin::initPlugin(PluginAPI* pluginAPI) +{ + m_pluginAPI = pluginAPI; + + m_pluginAPI->registerFeature(APRS::m_featureIdURI, APRS::m_featureId, this); +} + +#ifdef SERVER_MODE +FeatureGUI* APRSPlugin::createFeatureGUI(FeatureUISet *featureUISet, Feature *feature) const +{ + (void) featureUISet; + (void) feature; + return nullptr; +} +#else +FeatureGUI* APRSPlugin::createFeatureGUI(FeatureUISet *featureUISet, Feature *feature) const +{ + return APRSGUI::create(m_pluginAPI, featureUISet, feature); +} +#endif + +Feature* APRSPlugin::createFeature(WebAPIAdapterInterface* webAPIAdapterInterface) const +{ + return new APRS(webAPIAdapterInterface); +} + +FeatureWebAPIAdapter* APRSPlugin::createFeatureWebAPIAdapter() const +{ + return new APRSWebAPIAdapter(); +} diff --git a/plugins/feature/aprs/aprsplugin.h b/plugins/feature/aprs/aprsplugin.h new file mode 100644 index 000000000..c2767581e --- /dev/null +++ b/plugins/feature/aprs/aprsplugin.h @@ -0,0 +1,49 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_APRSPLUGIN_H +#define INCLUDE_FEATURE_APRSPLUGIN_H + +#include +#include "plugin/plugininterface.h" + +class FeatureGUI; +class WebAPIAdapterInterface; + +class APRSPlugin : public QObject, PluginInterface { + Q_OBJECT + Q_INTERFACES(PluginInterface) + Q_PLUGIN_METADATA(IID "sdrangel.feature.aprs") + +public: + explicit APRSPlugin(QObject* parent = nullptr); + + const PluginDescriptor& getPluginDescriptor() const; + void initPlugin(PluginAPI* pluginAPI); + + virtual FeatureGUI* createFeatureGUI(FeatureUISet *featureUISet, Feature *feature) const; + virtual Feature* createFeature(WebAPIAdapterInterface *webAPIAdapterInterface) const; + virtual FeatureWebAPIAdapter* createFeatureWebAPIAdapter() const; + +private: + static const PluginDescriptor m_pluginDescriptor; + + PluginAPI* m_pluginAPI; +}; + +#endif // INCLUDE_FEATURE_APRSPLUGIN_H diff --git a/plugins/feature/aprs/aprssettings.cpp b/plugins/feature/aprs/aprssettings.cpp new file mode 100644 index 000000000..6219d8613 --- /dev/null +++ b/plugins/feature/aprs/aprssettings.cpp @@ -0,0 +1,210 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "util/simpleserializer.h" +#include "settings/serializable.h" + +#include "aprssettings.h" + +const QStringList APRSSettings::m_pipeTypes = { + QStringLiteral("PacketDemod") +}; + +const QStringList APRSSettings::m_pipeURIs = { + QStringLiteral("sdrangel.channelrx.packetdemod"), +}; + +APRSSettings::APRSSettings() +{ + resetToDefaults(); +} + +void APRSSettings::resetToDefaults() +{ + m_igateServer = "noam.aprs2.net"; + m_igatePort = 14580; + m_igateCallsign = ""; + m_igatePasscode = ""; + m_igateFilter = "m/10"; + m_igateEnabled = false; + m_stationFilter = ALL; + m_filterAddressee = ""; + m_title = "APRS"; + m_rgbColor = QColor(225, 25, 99).rgb(); + m_useReverseAPI = false; + m_reverseAPIAddress = "127.0.0.1"; + m_reverseAPIPort = 8888; + m_reverseAPIFeatureSetIndex = 0; + m_reverseAPIFeatureIndex = 0; + for (int i = 0; i < APRS_PACKETS_TABLE_COLUMNS; i++) + { + m_packetsTableColumnIndexes[i] = i; + m_packetsTableColumnSizes[i] = -1; // Autosize + } + for (int i = 0; i < APRS_WEATHER_TABLE_COLUMNS; i++) + { + m_weatherTableColumnIndexes[i] = i; + m_weatherTableColumnSizes[i] = -1; // Autosize + } + for (int i = 0; i < APRS_STATUS_TABLE_COLUMNS; i++) + { + m_statusTableColumnIndexes[i] = i; + m_statusTableColumnSizes[i] = -1; // Autosize + } + for (int i = 0; i < APRS_MESSAGES_TABLE_COLUMNS; i++) + { + m_messagesTableColumnIndexes[i] = i; + m_messagesTableColumnSizes[i] = -1; // Autosize + } + for (int i = 0; i < APRS_TELEMETRY_TABLE_COLUMNS; i++) + { + m_telemetryTableColumnIndexes[i] = i; + m_telemetryTableColumnSizes[i] = -1; // Autosize + } + for (int i = 0; i < APRS_MOTION_TABLE_COLUMNS; i++) + { + m_motionTableColumnIndexes[i] = i; + m_motionTableColumnSizes[i] = -1; // Autosize + } +} + +QByteArray APRSSettings::serialize() const +{ + SimpleSerializer s(1); + + s.writeString(1, m_igateServer); + s.writeS32(2, m_igatePort); + s.writeString(3, m_igateCallsign); + s.writeString(4, m_igatePasscode); + s.writeString(5, m_igateFilter); + s.writeBool(6, m_igateEnabled); + s.writeS32(7, m_stationFilter); + s.writeString(8, m_filterAddressee); + s.writeString(9, m_title); + s.writeU32(10, m_rgbColor); + s.writeBool(11, m_useReverseAPI); + s.writeString(12, m_reverseAPIAddress); + s.writeU32(13, m_reverseAPIPort); + s.writeU32(14, m_reverseAPIFeatureSetIndex); + s.writeU32(15, m_reverseAPIFeatureIndex); + + for (int i = 0; i < APRS_PACKETS_TABLE_COLUMNS; i++) + s.writeS32(100 + i, m_packetsTableColumnIndexes[i]); + for (int i = 0; i < APRS_PACKETS_TABLE_COLUMNS; i++) + s.writeS32(200 + i, m_packetsTableColumnSizes[i]); + for (int i = 0; i < APRS_WEATHER_TABLE_COLUMNS; i++) + s.writeS32(300 + i, m_weatherTableColumnIndexes[i]); + for (int i = 0; i < APRS_WEATHER_TABLE_COLUMNS; i++) + s.writeS32(400 + i, m_weatherTableColumnSizes[i]); + for (int i = 0; i < APRS_STATUS_TABLE_COLUMNS; i++) + s.writeS32(500 + i, m_statusTableColumnIndexes[i]); + for (int i = 0; i < APRS_STATUS_TABLE_COLUMNS; i++) + s.writeS32(600 + i, m_statusTableColumnSizes[i]); + for (int i = 0; i < APRS_MESSAGES_TABLE_COLUMNS; i++) + s.writeS32(700 + i, m_messagesTableColumnIndexes[i]); + for (int i = 0; i < APRS_MESSAGES_TABLE_COLUMNS; i++) + s.writeS32(800 + i, m_messagesTableColumnSizes[i]); + for (int i = 0; i < APRS_TELEMETRY_TABLE_COLUMNS; i++) + s.writeS32(900 + i, m_telemetryTableColumnIndexes[i]); + for (int i = 0; i < APRS_TELEMETRY_TABLE_COLUMNS; i++) + s.writeS32(1000 + i, m_telemetryTableColumnSizes[i]); + for (int i = 0; i < APRS_MOTION_TABLE_COLUMNS; i++) + s.writeS32(1100 + i, m_motionTableColumnIndexes[i]); + for (int i = 0; i < APRS_MOTION_TABLE_COLUMNS; i++) + s.writeS32(1200 + i, m_motionTableColumnSizes[i]); + + return s.final(); +} + +bool APRSSettings::deserialize(const QByteArray& data) +{ + SimpleDeserializer d(data); + + if (!d.isValid()) + { + resetToDefaults(); + return false; + } + + if (d.getVersion() == 1) + { + QByteArray bytetmp; + uint32_t utmp; + QString strtmp; + + d.readString(1, &m_igateServer, "noam.aprs2.net"); + d.readS32(2, &m_igatePort, 14580); + d.readString(3, &m_igateCallsign, ""); + d.readString(4, &m_igatePasscode, ""); + d.readString(5, &m_igateFilter, ""); + d.readBool(6, &m_igateEnabled, false); + d.readS32(7, (int*)&m_stationFilter, 0); + d.readString(8, &m_filterAddressee, ""); + + d.readString(9, &m_title, "APRS"); + d.readU32(10, &m_rgbColor, QColor(225, 25, 99).rgb()); + d.readBool(11, &m_useReverseAPI, false); + d.readString(12, &m_reverseAPIAddress, "127.0.0.1"); + d.readU32(13, &utmp, 0); + + if ((utmp > 1023) && (utmp < 65535)) { + m_reverseAPIPort = utmp; + } else { + m_reverseAPIPort = 8888; + } + + d.readU32(14, &utmp, 0); + m_reverseAPIFeatureSetIndex = utmp > 99 ? 99 : utmp; + d.readU32(15, &utmp, 0); + m_reverseAPIFeatureIndex = utmp > 99 ? 99 : utmp; + + for (int i = 0; i < APRS_PACKETS_TABLE_COLUMNS; i++) + d.readS32(100 + i, &m_packetsTableColumnIndexes[i], i); + for (int i = 0; i < APRS_PACKETS_TABLE_COLUMNS; i++) + d.readS32(200 + i, &m_packetsTableColumnSizes[i], -1); + for (int i = 0; i < APRS_WEATHER_TABLE_COLUMNS; i++) + d.readS32(300 + i, &m_weatherTableColumnIndexes[i], i); + for (int i = 0; i < APRS_WEATHER_TABLE_COLUMNS; i++) + d.readS32(400 + i, &m_weatherTableColumnSizes[i], -1); + for (int i = 0; i < APRS_STATUS_TABLE_COLUMNS; i++) + d.readS32(500 + i, &m_statusTableColumnIndexes[i], i); + for (int i = 0; i < APRS_STATUS_TABLE_COLUMNS; i++) + d.readS32(600 + i, &m_statusTableColumnSizes[i], -1); + for (int i = 0; i < APRS_MESSAGES_TABLE_COLUMNS; i++) + d.readS32(700 + i, &m_messagesTableColumnIndexes[i], i); + for (int i = 0; i < APRS_MESSAGES_TABLE_COLUMNS; i++) + d.readS32(800 + i, &m_messagesTableColumnSizes[i], -1); + for (int i = 0; i < APRS_TELEMETRY_TABLE_COLUMNS; i++) + d.readS32(900 + i, &m_telemetryTableColumnIndexes[i], i); + for (int i = 0; i < APRS_TELEMETRY_TABLE_COLUMNS; i++) + d.readS32(1000 + i, &m_telemetryTableColumnSizes[i], -1); + for (int i = 0; i < APRS_MOTION_TABLE_COLUMNS; i++) + d.readS32(1100 + i, &m_motionTableColumnIndexes[i], i); + for (int i = 0; i < APRS_MOTION_TABLE_COLUMNS; i++) + d.readS32(1200 + i, &m_motionTableColumnSizes[i], -1); + + return true; + } + else + { + resetToDefaults(); + return false; + } +} diff --git a/plugins/feature/aprs/aprssettings.h b/plugins/feature/aprs/aprssettings.h new file mode 100644 index 000000000..65b4312d5 --- /dev/null +++ b/plugins/feature/aprs/aprssettings.h @@ -0,0 +1,79 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_APRSSETTINGS_H_ +#define INCLUDE_FEATURE_APRSSETTINGS_H_ + +#include +#include + +#include "util/message.h" + +class Serializable; + +// Number of columns in the tables +#define APRS_PACKETS_TABLE_COLUMNS 6 +#define APRS_WEATHER_TABLE_COLUMNS 15 +#define APRS_STATUS_TABLE_COLUMNS 7 +#define APRS_MESSAGES_TABLE_COLUMNS 5 +#define APRS_TELEMETRY_TABLE_COLUMNS 17 +#define APRS_MOTION_TABLE_COLUMNS 7 + +struct APRSSettings +{ + QString m_igateServer; + int m_igatePort; + QString m_igateCallsign; + QString m_igatePasscode; + QString m_igateFilter; + bool m_igateEnabled; + enum StationFilter { + ALL, STATIONS, OBJECTS, WEATHER, TELEMETRY, COURSE_AND_SPEED + } m_stationFilter; + QString m_filterAddressee; + QString m_title; + quint32 m_rgbColor; + bool m_useReverseAPI; + QString m_reverseAPIAddress; + uint16_t m_reverseAPIPort; + uint16_t m_reverseAPIFeatureSetIndex; + uint16_t m_reverseAPIFeatureIndex; + + int m_packetsTableColumnIndexes[APRS_PACKETS_TABLE_COLUMNS];//!< How the columns are ordered in the table + int m_packetsTableColumnSizes[APRS_PACKETS_TABLE_COLUMNS]; //!< Size of the columns in the table + int m_weatherTableColumnIndexes[APRS_WEATHER_TABLE_COLUMNS]; + int m_weatherTableColumnSizes[APRS_WEATHER_TABLE_COLUMNS]; + int m_statusTableColumnIndexes[APRS_STATUS_TABLE_COLUMNS]; + int m_statusTableColumnSizes[APRS_STATUS_TABLE_COLUMNS]; + int m_messagesTableColumnIndexes[APRS_MESSAGES_TABLE_COLUMNS]; + int m_messagesTableColumnSizes[APRS_MESSAGES_TABLE_COLUMNS]; + int m_telemetryTableColumnIndexes[APRS_TELEMETRY_TABLE_COLUMNS]; + int m_telemetryTableColumnSizes[APRS_TELEMETRY_TABLE_COLUMNS]; + int m_motionTableColumnIndexes[APRS_MOTION_TABLE_COLUMNS]; + int m_motionTableColumnSizes[APRS_MOTION_TABLE_COLUMNS]; + + APRSSettings(); + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + + static const QStringList m_pipeTypes; + static const QStringList m_pipeURIs; +}; + +#endif // INCLUDE_FEATURE_APRSSETTINGS_H_ diff --git a/plugins/feature/aprs/aprssettingsdialog.cpp b/plugins/feature/aprs/aprssettingsdialog.cpp new file mode 100644 index 000000000..4dc1a8fb7 --- /dev/null +++ b/plugins/feature/aprs/aprssettingsdialog.cpp @@ -0,0 +1,43 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "aprssettingsdialog.h" + +APRSSettingsDialog::APRSSettingsDialog(QString igateServer, QString igateCallsign, QString igatePasscode, QString igateFilter, QWidget* parent) : + QDialog(parent), + ui(new Ui::APRSSettingsDialog) +{ + ui->setupUi(this); + ui->igateServer->setCurrentText(igateServer); + ui->igateCallsign->setText(igateCallsign); + ui->igatePasscode->setText(igatePasscode); + ui->igateFilter->setText(igateFilter); +} + +APRSSettingsDialog::~APRSSettingsDialog() +{ + delete ui; +} + +void APRSSettingsDialog::accept() +{ + m_igateServer = ui->igateServer->currentText(); + m_igateCallsign = ui->igateCallsign->text(); + m_igatePasscode = ui->igatePasscode->text(); + m_igateFilter = ui->igateFilter->text(); + QDialog::accept(); +} diff --git a/plugins/feature/aprs/aprssettingsdialog.h b/plugins/feature/aprs/aprssettingsdialog.h new file mode 100644 index 000000000..76dc892ef --- /dev/null +++ b/plugins/feature/aprs/aprssettingsdialog.h @@ -0,0 +1,43 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_APRSSETTINGSDIALOG_H +#define INCLUDE_APRSSETTINGSDIALOG_H + +#include "ui_aprssettingsdialog.h" +#include "aprssettings.h" + +class APRSSettingsDialog : public QDialog { + Q_OBJECT + +public: + explicit APRSSettingsDialog(QString igateServer, QString igateCallsign, QString igatePasscode, QString igateFilter, QWidget* parent = 0); + ~APRSSettingsDialog(); + + QString m_igateServer; + QString m_igateCallsign; + QString m_igatePasscode; + QString m_igateFilter; + +private slots: + void accept(); + +private: + Ui::APRSSettingsDialog* ui; +}; + +#endif // INCLUDE_APRSSETTINGSDIALOG_H diff --git a/plugins/feature/aprs/aprssettingsdialog.ui b/plugins/feature/aprs/aprssettingsdialog.ui new file mode 100644 index 000000000..d2ee9b07c --- /dev/null +++ b/plugins/feature/aprs/aprssettingsdialog.ui @@ -0,0 +1,173 @@ + + + APRSSettingsDialog + + + + 0 + 0 + 351 + 179 + + + + + Liberation Sans + 9 + + + + APRS Settings + + + + + + + + + Name of APRS-IS server to connect to + + + true + + + + noam.aprs2.net + + + + + soam.aprs2.net + + + + + euro.aprs2.net + + + + + asia.aprs2.net + + + + + aunz.aprs2.net + + + + + + + + IGate Server + + + + + + + IGate Passcode + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + IGate Callsign + + + + + + + Server side filter + + + + + + + IGate Filter + + + + + + + Callsign to use when connecting to APRS-IS server + + + + + + + Passcode corresponding to APRS-IS callsign + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + APRSSettingsDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + APRSSettingsDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/plugins/feature/aprs/aprswebapiadapter.cpp b/plugins/feature/aprs/aprswebapiadapter.cpp new file mode 100644 index 000000000..9bb122fb0 --- /dev/null +++ b/plugins/feature/aprs/aprswebapiadapter.cpp @@ -0,0 +1,52 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB. // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "SWGFeatureSettings.h" +#include "aprs.h" +#include "aprswebapiadapter.h" + +APRSWebAPIAdapter::APRSWebAPIAdapter() +{} + +APRSWebAPIAdapter::~APRSWebAPIAdapter() +{} + +int APRSWebAPIAdapter::webapiSettingsGet( + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setSimplePttSettings(new SWGSDRangel::SWGSimplePTTSettings()); + response.getSimplePttSettings()->init(); + APRS::webapiFormatFeatureSettings(response, m_settings); + + return 200; +} + +int APRSWebAPIAdapter::webapiSettingsPutPatch( + bool force, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) force; // no action + (void) errorMessage; + APRS::webapiUpdateFeatureSettings(m_settings, featureSettingsKeys, response); + + return 200; +} diff --git a/plugins/feature/aprs/aprswebapiadapter.h b/plugins/feature/aprs/aprswebapiadapter.h new file mode 100644 index 000000000..e0405813f --- /dev/null +++ b/plugins/feature/aprs/aprswebapiadapter.h @@ -0,0 +1,50 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB. // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_APRS_WEBAPIADAPTER_H +#define INCLUDE_APRS_WEBAPIADAPTER_H + +#include "feature/featurewebapiadapter.h" +#include "aprssettings.h" + +/** + * Standalone API adapter only for the settings + */ +class APRSWebAPIAdapter : public FeatureWebAPIAdapter { +public: + APRSWebAPIAdapter(); + virtual ~APRSWebAPIAdapter(); + + virtual QByteArray serialize() const { return m_settings.serialize(); } + virtual bool deserialize(const QByteArray& data) { return m_settings.deserialize(data); } + + virtual int webapiSettingsGet( + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage); + +private: + APRSSettings m_settings; +}; + +#endif // INCLUDE_APRS_WEBAPIADAPTER_H diff --git a/plugins/feature/aprs/aprsworker.cpp b/plugins/feature/aprs/aprsworker.cpp new file mode 100644 index 000000000..fd55bb63f --- /dev/null +++ b/plugins/feature/aprs/aprsworker.cpp @@ -0,0 +1,251 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include + +#include "webapi/webapiadapterinterface.h" +#include "webapi/webapiutils.h" +#include "maincore.h" +#include "util/ax25.h" +#include "util/aprs.h" + +#include "aprs.h" +#include "aprsworker.h" + +MESSAGE_CLASS_DEFINITION(APRSWorker::MsgConfigureAPRSWorker, Message) + +APRSWorker::APRSWorker(APRS *aprs, WebAPIAdapterInterface *webAPIAdapterInterface) : + m_aprs(aprs), + m_webAPIAdapterInterface(webAPIAdapterInterface), + m_msgQueueToFeature(nullptr), + m_msgQueueToGUI(nullptr), + m_running(false), + m_mutex(QMutex::Recursive) +{ + connect(&m_socket, SIGNAL(readyRead()),this, SLOT(recv())); + connect(&m_socket, SIGNAL(connected()), this, SLOT(connected())); + connect(&m_socket, SIGNAL(disconnected()), this, SLOT(disconnected())); +#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) + connect(&m_socket, QOverload::of(&QAbstractSocket::error), this, &APRSWorker::errorOccurred); +#else + connect(&m_socket, &QAbstractSocket::errorOccurred, this, &APRSWorker::errorOccurred); +#endif +} + +APRSWorker::~APRSWorker() +{ + m_inputMessageQueue.clear(); +} + +void APRSWorker::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_inputMessageQueue.clear(); +} + +bool APRSWorker::startWork() +{ + QMutexLocker mutexLocker(&m_mutex); + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + m_running = true; + return m_running; +} + +void APRSWorker::stopWork() +{ + QMutexLocker mutexLocker(&m_mutex); + disconnect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + m_running = false; +} + +void APRSWorker::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool APRSWorker::handleMessage(const Message& cmd) +{ + if (MsgConfigureAPRSWorker::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureAPRSWorker& cfg = (MsgConfigureAPRSWorker&) cmd; + + applySettings(cfg.getSettings(), cfg.getForce()); + return true; + } + else if (MainCore::MsgPacket::match(cmd)) + { + MainCore::MsgPacket& report = (MainCore::MsgPacket&) cmd; + AX25Packet ax25; + APRSPacket *aprs = new APRSPacket(); + if (ax25.decode(report.getPacket())) + { + if (aprs->decode(ax25)) + { + // See: http://www.aprs-is.net/IGateDetails.aspx for gating rules + if (!aprs->m_via.contains("TCPIP") + && !aprs->m_via.contains("TCPXX") + && !aprs->m_via.contains("NOGATE") + && !aprs->m_via.contains("RFONLY")) + { + aprs->m_dateTime = report.getDateTime(); + QString igateMsg = aprs->toTNC2(m_settings.m_igateCallsign); + send(igateMsg.toUtf8(), igateMsg.length()); + } + } + } + return true; + } + else + { + return false; + } +} + +void APRSWorker::applySettings(const APRSSettings& settings, bool force) +{ + qDebug() << "APRSWorker::applySettings:" + << " m_igateEnabled: " << settings.m_igateEnabled + << " m_igateServer: " << settings.m_igateServer + << " m_igatePort: " << settings.m_igatePort + << " m_igateCallsign: " << settings.m_igateCallsign + << " m_igateFilter: " << settings.m_igateFilter + << " force: " << force; + + if ((settings.m_igateEnabled != m_settings.m_igateEnabled) + || (settings.m_igateServer != m_settings.m_igateServer) + || (settings.m_igatePort != m_settings.m_igatePort) + || (settings.m_igateFilter != m_settings.m_igateFilter) + || force) + { + // Close any existing connection + if (m_socket.isOpen()) + m_socket.close(); + // Open connection + if (settings.m_igateEnabled) + { + if (settings.m_igateServer.isEmpty()) + { + if (m_msgQueueToFeature) + m_msgQueueToFeature->push(APRS::MsgReportWorker::create("IGate server name must be specified")); + } + else if (settings.m_igateCallsign.isEmpty()) + { + if (m_msgQueueToFeature) + m_msgQueueToFeature->push(APRS::MsgReportWorker::create("IGate callsign must be specified")); + } + else if (settings.m_igatePasscode.isEmpty()) + { + if (m_msgQueueToFeature) + m_msgQueueToFeature->push(APRS::MsgReportWorker::create("IGate passcode must be specified")); + } + else + { + qDebug() << "APRSWorker::applySettings: Connecting to " << settings.m_igateServer << ":" << settings.m_igatePort; + m_socket.setSocketOption(QAbstractSocket::LowDelayOption, 1); + m_socket.connectToHost(settings.m_igateServer, settings.m_igatePort); + } + } + } + + m_settings = settings; +} + +void APRSWorker::connected() +{ + qDebug() << "APRSWorker::connected " << m_settings.m_igateServer; + m_loggedIn = false; +} + +void APRSWorker::disconnected() +{ + qDebug() << "APRSWorker::disconnected"; + if (m_msgQueueToFeature) + m_msgQueueToFeature->push(APRS::MsgReportWorker::create("Disconnected")); +} + +void APRSWorker::errorOccurred(QAbstractSocket::SocketError socketError) +{ + qDebug() << "APRSWorker::errorOccurred: " << socketError; + if (m_msgQueueToFeature) + m_msgQueueToFeature->push(APRS::MsgReportWorker::create(m_socket.errorString() + " " + socketError)); +} + +void APRSWorker::recv() +{ + char buffer[2048]; + + while (m_socket.readLine(buffer, sizeof(buffer)) > 0) + { + QString packet(buffer); + qDebug() << "APRSWorker::recv: " << packet; + if (packet.startsWith("#")) + { + if (!m_loggedIn) + { + // Log in with callsign and passcode + QString login = QString("user %1 pass %2 vers SDRangel 6.4.0%3\r\n").arg(m_settings.m_igateCallsign).arg(m_settings.m_igatePasscode).arg(m_settings.m_igateFilter.isEmpty() ? "" : QString(" filter %1").arg(m_settings.m_igateFilter)); + send(login.toLatin1(), login.length()); + m_loggedIn = true; + if (m_msgQueueToFeature) + m_msgQueueToFeature->push(APRS::MsgReportWorker::create("Connected")); + } + else if (packet.indexOf(QString("logresp %1 unverified").arg(m_settings.m_igateCallsign)) >= 0) + { + if (m_msgQueueToFeature) + m_msgQueueToFeature->push(APRS::MsgReportWorker::create("Invalid IGate callsign or passcode")); + } + } + else if (packet.length() > 0) + { + // Forward to GUI + if (getMessageQueueToGUI()) + { + // Convert TNC2 to AX25 raw bytes + QByteArray ax25Packet = APRSPacket::toByteArray(packet); + getMessageQueueToGUI()->push(MainCore::MsgPacket::create(m_aprs, ax25Packet, QDateTime::currentDateTime())); + } + } + } +} + +void APRSWorker::send(const char *data, int length) +{ + if (m_settings.m_igateEnabled) + { + // Reopen connection if it was lost + if (!m_socket.isOpen()) + { + qDebug() << "APRSWorker::send: Reconnecting to " << m_settings.m_igateServer << ":" << m_settings.m_igatePort; + m_socket.connectToHost(m_settings.m_igateServer, m_settings.m_igatePort); + } + // Send data + qDebug() << "APRSWorker::send: " << QString(QByteArray(data, length)); + m_socket.write(data, length); + } +} diff --git a/plugins/feature/aprs/aprsworker.h b/plugins/feature/aprs/aprsworker.h new file mode 100644 index 000000000..2c9c36dd1 --- /dev/null +++ b/plugins/feature/aprs/aprsworker.h @@ -0,0 +1,97 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_APRSWORKER_H_ +#define INCLUDE_FEATURE_APRSWORKER_H_ + +#include +#include +#include + +#include "util/message.h" +#include "util/messagequeue.h" + +#include "aprs.h" +#include "aprssettings.h" + +class WebAPIAdapterInterface; + +class APRSWorker : public QObject +{ + Q_OBJECT +public: + class MsgConfigureAPRSWorker : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const APRSSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureAPRSWorker* create(const APRSSettings& settings, bool force) + { + return new MsgConfigureAPRSWorker(settings, force); + } + + private: + APRSSettings m_settings; + bool m_force; + + MsgConfigureAPRSWorker(const APRSSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + APRSWorker(APRS *m_aprs, WebAPIAdapterInterface *webAPIAdapterInterface); + ~APRSWorker(); + void reset(); + bool startWork(); + void stopWork(); + bool isRunning() const { return m_running; } + MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } + void setMessageQueueToFeature(MessageQueue *messageQueue) { m_msgQueueToFeature = messageQueue; } + void setMessageQueueToGUI(MessageQueue *messageQueue) { m_msgQueueToGUI = messageQueue; } + +private: + + APRS *m_aprs; + WebAPIAdapterInterface *m_webAPIAdapterInterface; + MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication + MessageQueue *m_msgQueueToFeature; //!< Queue to report channel change to main feature object + MessageQueue *m_msgQueueToGUI; + APRSSettings m_settings; + bool m_running; + QMutex m_mutex; + QTcpSocket m_socket; + bool m_loggedIn; + + bool handleMessage(const Message& cmd); + void applySettings(const APRSSettings& settings, bool force = false); + MessageQueue *getMessageQueueToGUI() { return m_msgQueueToGUI; } + void send(const char *data, int length); + +private slots: + void handleInputMessages(); + void connected(); + void disconnected(); + void errorOccurred(QAbstractSocket::SocketError socketError); + void recv(); +}; + +#endif // INCLUDE_FEATURE_APRSWORKER_H_ diff --git a/plugins/feature/aprs/readme.md b/plugins/feature/aprs/readme.md new file mode 100644 index 000000000..7271bbf8f --- /dev/null +++ b/plugins/feature/aprs/readme.md @@ -0,0 +1,47 @@ +

APRS Feature Plugin

+ +

Introduction

+ +The APRS plugin displays APRS (Automatic Packet Reporting System) packets. APRS packets can be received over RF via one or more Packet Demodulator source channels or from the Internet via an APRS-IS IGate. + +

Interface

+ +![APRS feature plugin GUI](../../../doc/img/APRS_plugin.png) + +

1: Source channels

+ +This displays a list of the Packet Demodulator channels the APRS feature is receiving packets from. + +

2: Enable APRS-IS IGate

+ +When checked, enables the APRS-IS IGate (Internet gateway). The IGate forwards packets received via the Packet Demodulators to APRS-IS internet servers. +These servers collate packets from all IGates and allow them to be viewed on web sites such as https://aprs.fi, https://www.aprsdirect.com/ and http://ariss.net/ (the latter being for packets repeated via satellites and the ISS). +It is also possible to receive packets via the IGate, which allows you to see packets that you cannot receive via RF. + +

3: Show APRS Settings

+ +Pressing this button shows the APRS Settings Dialog. This dialog allows you to enter: + +* The APRS-IS server the IGate should connect to. Please choose your local server. (noam = North America, euro = Europe, etc). +* The callsign the IGate should connect with. +* The passcode corresponding to the given callsign. +* A serverside filter, that specifies which packets should be forwarded from the internet to SDRangel. See http://www.aprs-is.net/javAPRSFilter.aspx +m/50 will send you packets within 50 km of the last known position of the station corresponding to the callsign used to log in with. +If you do not have a corresponding station, you can specify a location by passing a latitude and longitude. E.g: r/lat/lon/50 + +

Map

+ +The APRS feature can plot APRS symbols and data on the Map. To use, simply open a Map feature and the APRS plugin will display packets it receives from that point on it. +Selecting an APRS item on the map will display a text bubble containing APRS status, position and weather data. + +![APRS map](../../../doc/img/APRS_map.png) + +

Attribution

+ +APRS icons are from: https://github.com/hessu/aprs-symbols + +

API

+ +Full details of the API can be found in the Swagger documentation. Here is a quick example of how to enable the APRS-IS IGate: + + curl -X PATCH "http://127.0.0.1:8091/sdrangel/featureset/0/feature/0/settings" -d '{"featureType": "APRS", "APRSSettings": { "igateCallsign": "MYCALLSIGN", "igatePasscode": "12345", "igateFilter": "r/50.2/10.2/25", "igateEnabled": 1 }}' diff --git a/sdrbase/util/aprs.cpp b/sdrbase/util/aprs.cpp new file mode 100644 index 000000000..634e91b16 --- /dev/null +++ b/sdrbase/util/aprs.cpp @@ -0,0 +1,992 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include + +#include "aprs.h" + +// See: http://www.aprs.org/doc/APRS101.PDF + +// Currently we only decode what we want to display on the map +bool APRSPacket::decode(AX25Packet packet) +{ + // Check type, PID and length of packet + if ((packet.m_type == "UI") && (packet.m_pid == "f0") && (packet.m_dataASCII.length() >= 1)) + { + // Check destination address + QRegExp re("^(AIR.*|ALL.*|AP.*|BEACON|CQ.*|GPS.*|DF.*|DGPS.*|DRILL.*|DX.*|ID.*|JAVA.*|MAIL.*|MICE.*|QST.*|QTH.*|RTCM.*|SKY.*|SPACE.*|SPC.*|SYM.*|TEL.*|TEST.*|TLM.*|WX.*|ZIP.*)"); + if (re.exactMatch(packet.m_to)) + { + m_from = packet.m_from; + m_to = packet.m_to; + m_via = packet.m_via; + m_data = packet.m_dataASCII; + + if (packet.m_to.startsWith("GPS") || packet.m_to.startsWith("SPC") || packet.m_to.startsWith("SYM")) + { + // FIXME: Trailing letters xyz specify a symbol + } + + // Source address SSID can be used to specify a symbol + + // First byte of information field is data type ID + char dataType = packet.m_dataASCII[0].toLatin1(); + bool timestamp = false; + int idx = 1; + switch (dataType) + { + case '!': // Position without timestamp or Ultimeter 2000 WX Station + parsePosition(packet.m_dataASCII, idx); + if (m_symbolCode == '_') + parseWeather(packet.m_dataASCII, idx, false); + else if (m_symbolCode == '@') + parseStorm(packet.m_dataASCII, idx); + else + { + parseDataExension(packet.m_dataASCII, idx); + parseComment(packet.m_dataASCII, idx); + } + break; + case '#': // Peet Bros U-II Weather Station + case '$': // Raw GPS data or Ultimeter 2000 + case '%': // Agrelo DFJr / MicroFinder + break; + case ')': // Item + parseItem(packet.m_dataASCII, idx); + parsePosition(packet.m_dataASCII, idx); + parseDataExension(packet.m_dataASCII, idx); + parseComment(packet.m_dataASCII, idx); + break; + case '*': // Peet Bros U-II Weather Station + break; + case '/': // Position with timestamp (no APRS messaging) + parseTime(packet.m_dataASCII, idx); + parsePosition(packet.m_dataASCII, idx); + if (m_symbolCode == '_') + parseWeather(packet.m_dataASCII, idx, false); + else if (m_symbolCode == '@') + parseStorm(packet.m_dataASCII, idx); + else + { + parseDataExension(packet.m_dataASCII, idx); + parseComment(packet.m_dataASCII, idx); + } + break; + case ':': // Message + parseMessage(packet.m_dataASCII, idx); + break; + case ';': // Object + parseObject(packet.m_dataASCII, idx); + parseTime(packet.m_dataASCII, idx); + parsePosition(packet.m_dataASCII, idx); + if (m_symbolCode == '_') + parseWeather(packet.m_dataASCII, idx, false); + else if (m_symbolCode == '@') + parseStorm(packet.m_dataASCII, idx); + else + { + parseDataExension(packet.m_dataASCII, idx); + parseComment(packet.m_dataASCII, idx); + } + break; + case '<': // Station Capabilities + break; + case '=': // Position without timestamp (with APRS messaging) + parsePosition(packet.m_dataASCII, idx); + if (m_symbolCode == '_') + parseWeather(packet.m_dataASCII, idx, false); + else if (m_symbolCode == '@') + parseStorm(packet.m_dataASCII, idx); + else + { + parseDataExension(packet.m_dataASCII, idx); + parseComment(packet.m_dataASCII, idx); + } + break; + case '>': // Status + parseStatus(packet.m_dataASCII, idx); + break; + case '?': // Query + break; + case '@': // Position with timestamp (with APRS messaging) + parseTime(packet.m_dataASCII, idx); + parsePosition(packet.m_dataASCII, idx); + if (m_symbolCode == '_') + parseWeather(packet.m_dataASCII, idx, false); + else if (m_symbolCode == '@') + parseStorm(packet.m_dataASCII, idx); + else + { + parseDataExension(packet.m_dataASCII, idx); + parseComment(packet.m_dataASCII, idx); + } + break; + case 'T': // Telemetry data + parseTelemetry(packet.m_dataASCII, idx); + break; + case '_': // Weather report (without position) + parseTimeMDHM(packet.m_dataASCII, idx); + parseWeather(packet.m_dataASCII, idx, true); + break; + case '{': // User-defined APRS packet format + break; + default: + return false; + } + + if (m_hasSymbol) + { + int num = m_symbolCode - '!'; + m_symbolImage = QString("aprs/aprs/aprs-symbols-24-%1-%2.png").arg(m_symbolTable == '/' ? 0 : 1).arg(num, 2, 10, QChar('0')); + } + + return true; + } + } + + return false; +} + +int APRSPacket::charToInt(QString&s, int idx) +{ + char c = s[idx].toLatin1(); + return c == ' ' ? 0 : c - '0'; +} + +bool APRSPacket::parseTime(QString& info, int& idx) +{ + if (info.length() < idx+7) + return false; + + QDateTime currentDateTime; + + if (info[idx+6]=='h') + { + // HMS format + if (info[idx].isDigit() + && info[idx+1].isDigit() + && info[idx+2].isDigit() + && info[idx+3].isDigit() + && info[idx+4].isDigit() + && info[idx+5].isDigit()) + { + int hour = charToInt(info, idx) * 10 + charToInt(info, idx+1); + int min = charToInt(info, idx+2) * 10 + charToInt(info, idx+3); + int sec = charToInt(info, idx+4) * 10 + charToInt(info, idx+5); + + if (hour > 23) + return false; + if (min > 59) + return false; + if (sec > 60) // Can have 60 seconds when there's a leap second + return false; + + m_utc = true; + m_timestamp = QDateTime(QDate::currentDate(), QTime(hour, min, sec)); + m_hasTimestamp = true; + + idx += 7; + return true; + } + else + return false; + } + else if ((info[idx+6]=='z') || (info[idx+6]=='/')) + { + // DHM format + if (info[idx].isDigit() + && info[idx+1].isDigit() + && info[idx+2].isDigit() + && info[idx+3].isDigit() + && info[idx+4].isDigit() + && info[idx+5].isDigit()) + { + int day = charToInt(info, idx) * 10 + charToInt(info, idx+1); + int hour = charToInt(info, idx+2) * 10 + charToInt(info, idx+3); + int min = charToInt(info, idx+4) * 10 + charToInt(info, idx+5); + + if (day > 31) + return false; + if (hour > 23) + return false; + if (min > 59) + return false; + + m_utc = info[idx+6]=='z'; + currentDateTime = m_utc ? QDateTime::currentDateTimeUtc() : QDateTime::currentDateTime(); + m_timestamp = QDateTime(QDate(currentDateTime.date().year(), currentDateTime.date().month(), day), QTime(hour, min, 0)); + m_hasTimestamp = true; + + idx += 7; + return true; + } + else + return false; + } + else + return false; +} + +// Time format used in weather reports without position +bool APRSPacket::parseTimeMDHM(QString& info, int& idx) +{ + if (info.length() < idx+8) + return false; + + if (info[idx].isDigit() + && info[idx+1].isDigit() + && info[idx+2].isDigit() + && info[idx+3].isDigit() + && info[idx+4].isDigit() + && info[idx+5].isDigit() + && info[idx+6].isDigit() + && info[idx+7].isDigit()) + { + int month = charToInt(info, idx) * 10 + charToInt(info, idx+1); + int day = charToInt(info, idx+2) * 10 + charToInt(info, idx+3); + int hour = charToInt(info, idx+4) * 10 + charToInt(info, idx+5); + int min = charToInt(info, idx+6) * 10 + charToInt(info, idx+7); + + if (month > 12) + return false; + if (day > 31) + return false; + if (hour > 23) + return false; + if (min > 59) + return false; + + m_utc = true; + QDateTime currentDateTime = QDateTime::currentDateTimeUtc(); + m_timestamp = QDateTime(QDate(currentDateTime.date().year(), month, day), QTime(hour, min, 0)); + m_hasTimestamp = true; + + return true; + } + else + return false; +} + +// Position ambigutiy can be specified by using spaces instead of digits in lats and longs +bool APRSPacket::isLatLongChar(QCharRef c) +{ + return (c.isDigit() || c == ' '); +} + +bool APRSPacket::parsePosition(QString& info, int& idx) +{ + float latitude; + float longitude; + char table; + char code; + + if (info.length() < idx+8+1+9+1) + return false; + + // Latitude + if (info[idx].isDigit() + && info[idx+1].isDigit() + && isLatLongChar(info[idx+2]) + && isLatLongChar(info[idx+3]) + && (info[idx+4]=='.') + && isLatLongChar(info[idx+5]) + && isLatLongChar(info[idx+6]) + && ((info[idx+7]=='N') || (info[idx+7]=='S'))) + { + int deg = charToInt(info, idx) * 10 + charToInt(info, idx+1); + int min = charToInt(info, idx+2) * 10 + charToInt(info, idx+3); + int hundreths = charToInt(info, idx+5) * 10 + charToInt(info, idx+6); + bool north = (info[idx+7]=='N'); + if (deg > 90) + return false; + else if ((deg == 90) && ((min != 0) || (hundreths != 0))) + return false; + latitude = ((float)deg) + min/60.0 + hundreths/60.0/100.0; + if (!north) + latitude = -latitude; + idx += 8; + } + else + return false; + + // Symbol table identifier + table = info[idx++].toLatin1(); + + // Longitude + if (info[idx].isDigit() + && info[idx+1].isDigit() + && info[idx+2].isDigit() + && isLatLongChar(info[idx+3]) + && isLatLongChar(info[idx+4]) + && (info[idx+5]=='.') + && isLatLongChar(info[idx+6]) + && isLatLongChar(info[idx+7]) + && ((info[idx+8]=='E') || (info[idx+8]=='W'))) + { + int deg = charToInt(info, idx) * 100 + charToInt(info, idx+1) * 10 + charToInt(info, idx+2); + int min = charToInt(info, idx+3) * 10 + charToInt(info, idx+4); + int hundreths = charToInt(info, idx+6) * 10 + charToInt(info, idx+7); + bool east = (info[idx+8]=='E'); + if (deg > 180) + return false; + else if ((deg == 180) && ((min != 0) || (hundreths != 0))) + return false; + longitude = ((float)deg) + min/60.0 + hundreths/60.0/100.0; + if (!east) + longitude = -longitude; + idx += 9; + } + else + return false; + + // Symbol table code + code = info[idx++].toLatin1(); + + // Update state as we have a valid position + m_latitude = latitude; + m_longitude = longitude; + m_hasPosition = true; + m_symbolTable = table; + m_symbolCode = code; + m_hasSymbol = true; + return true; +} + +bool APRSPacket::parseDataExension(QString& info, int& idx) +{ + int heightMap[] = {10, 20, 40, 80, 160, 320, 640, 1280, 2560, 5120}; + QStringList directivityMap = {"Omni", "NE", "E", "SE", "S", "SW", "W", "NW", "N", ""}; + + int remainingLength = info.length() - idx; + if (remainingLength < 7) + return true; + QString s = info.right(remainingLength); + + // Course and speed + QRegExp courseSpeed("^([0-9]{3})\\/([0-9]{3})"); + if (courseSpeed.indexIn(s) >= 0) + { + m_course = courseSpeed.capturedTexts()[1].toInt(); + m_speed = courseSpeed.capturedTexts()[2].toInt(); + m_hasCourseAndSpeed = true; + idx += 7; + return true; + } + + // Station radio details + QRegExp phg("^PHG([0-9])([0-9])([0-9])([0-9])"); + if (phg.indexIn(s) >= 0) + { + // Transmitter power + int powerCode = phg.capturedTexts()[1].toInt(); + int powerMap[] = {0, 1, 4, 9, 16, 25, 36, 49, 64, 81}; + m_powerWatts = powerMap[powerCode]; + + // Antenna height + int heightCode = phg.capturedTexts()[2].toInt(); + m_antennaHeightFt = heightMap[heightCode]; + + // Antenna gain + m_antennaGainDB = phg.capturedTexts()[3].toInt(); + + // Antenna directivity + int directivityCode = phg.capturedTexts()[4].toInt(); + m_antennaDirectivity = directivityMap[directivityCode]; + + m_hasStationDetails = true; + + idx += 7; + return true; + } + + // Radio range + QRegExp rng("^RNG([0-9]{4})"); + if (rng.indexIn(s) >= 0) + { + m_radioRangeMiles = rng.capturedTexts()[1].toInt(); + m_hasRadioRange = true; + idx += 7; + return true; + } + + // Omni-DF strength + QRegExp dfs("^DFS([0-9])([0-9])([0-9])([0-9])"); + if (dfs.indexIn(s) >= 0) + { + // Strength S-points + m_dfStrength = dfs.capturedTexts()[1].toInt(); + + // Antenna height + int heightCode = dfs.capturedTexts()[2].toInt(); + m_dfHeightFt = heightMap[heightCode]; + + // Antenna gain + m_dfGainDB = dfs.capturedTexts()[3].toInt(); + + // Antenna directivity + int directivityCode = dfs.capturedTexts()[4].toInt(); + m_dfAntennaDirectivity = directivityMap[directivityCode]; + + m_hasDf = true; + idx += 7; + return true; + } + + return true; +} + +bool APRSPacket::parseComment(QString& info, int& idx) +{ + int commentLength = info.length() - idx; + if (commentLength > 0) + { + m_comment = info.right(commentLength); + + // Comment can contain altitude anywhere in it. Of the form /A=001234 in feet + QRegExp re("\\/A=([0-9]{6})"); + int pos = re.indexIn(m_comment); + if (pos >= 0) + { + m_altitudeFt = re.capturedTexts()[1].toInt(); + m_hasAltitude = true; + // Strip it out of comment if at start of string + if (pos == 0) + m_comment = m_comment.mid(9); + } + } + + return true; +} + +bool APRSPacket::parseInt(QString& info, int& idx, int chars, int& value, bool& hasValue) +{ + int total = 0; + bool negative = false; + bool noValue = false; + + for (int i = 0; i < chars; i++) + { + if (info[idx].isDigit()) + { + total = total * 10; + total += info[idx].toLatin1() - '0'; + } + else if ((i == 0) && (info[idx] == '-')) + negative = true; + else if ((info[idx] == '.') || (info[idx] == ' ')) + noValue = true; + else + return false; + idx++; + } + if (!noValue) + { + if (negative) + value = -total; + else + value = total; + hasValue = true; + } + else + hasValue = false; + return true; +} + +bool APRSPacket::parseWeather(QString& info, int& idx, bool positionLess) +{ + if (!positionLess) + { + if (!parseInt(info, idx, 3, m_windDirection, m_hasWindDirection)) + return false; + if (info[idx++] != '/') + return false; + if (!parseInt(info, idx, 3, m_windSpeed, m_hasWindSpeed)) + return false; + } + + // Weather data + bool done = false; + while (!done && (idx < info.length())) + { + switch (info[idx++].toLatin1()) + { + case 'c': // Wind direction + if (!parseInt(info, idx, 3, m_windDirection, m_hasWindDirection)) + return false; + break; + case 's': // Wind speed + if (!parseInt(info, idx, 3, m_windSpeed, m_hasWindSpeed)) + return false; + break; + case 'g': // Gust + if (!parseInt(info, idx, 3, m_gust, m_hasGust)) + return false; + break; + case 't': // Temp + if (!parseInt(info, idx, 3, m_temp, m_hasTemp)) + { + qDebug() << "Failed parseing temp: idx" << idx; + return false; + } + break; + case 'r': // Rain last hour + if (!parseInt(info, idx, 3, m_rainLastHr, m_hasRainLastHr)) + return false; + break; + case 'p': // Rain last 24 hours + if (!parseInt(info, idx, 3, m_rainLast24Hrs, m_hasRainLast24Hrs)) + return false; + break; + case 'P': // Rain since midnight + if (!parseInt(info, idx, 3, m_rainSinceMidnight, m_hasRainSinceMidnight)) + return false; + break; + case 'h': // Humidity + if (!parseInt(info, idx, 2, m_humidity, m_hasHumidity)) + return false; + break; + case 'b': // Barometric pressure + if (!parseInt(info, idx, 5, m_barometricPressure, m_hasBarometricPressure)) + return false; + break; + case 'L': // Luminosity <999 + if (!parseInt(info, idx, 3, m_luminosity, m_hasLuminsoity)) + return false; + break; + case 'l': // Luminosity >= 1000 + if (!parseInt(info, idx, 3, m_luminosity, m_hasLuminsoity)) + return false; + m_luminosity += 1000; + break; + case 'S': // Snowfall + if (!parseInt(info, idx, 3, m_snowfallLast24Hrs, m_hasSnowfallLast24Hrs)) + return false; + break; + case '#': // Raw rain counter + if (!parseInt(info, idx, 3, m_rawRainCounter, m_hasRawRainCounter)) + return false; + break; + case 'X': // Radiation level + if (!parseInt(info, idx, 3, m_radiationLevel, m_hasRadiationLevel)) + return false; + break; + case 'F': // Floor water level + if (!parseInt(info, idx, 4, m_floodLevel, m_hasFloodLevel)) + return false; + break; + case 'V': // Battery volts + if (!parseInt(info, idx, 3, m_batteryVolts, m_hasBatteryVolts)) + return false; + break; + default: + done = true; + break; + } + } + if (done) + { + // APRS 1.1 spec says remaining fields are s/w and weather unit type + // But few real-world packets actually seem to conform to the spec + idx--; + int remaining = info.length() - idx; + m_weatherUnitType = info.right(remaining); + idx += remaining; + } + + m_hasWeather = true; + return true; +} + + +bool APRSPacket::parseStorm(QString& info, int& idx) +{ + bool unused; + + if (!parseInt(info, idx, 3, m_stormDirection, unused)) + return false; + if (info[idx++] != '/') + return false; + if (!parseInt(info, idx, 3, m_stormSpeed, unused)) + return false; + if (info[idx++] != '/') + return false; + QString type = info.mid(idx, 2); + idx += 2; + if (type == "TS") + m_stormType = "Tropical storm"; + else if (type == "HC") + m_stormType = "Hurrican"; + else if (type == "TD") + m_stormType = "Tropical depression"; + else + m_stormType = type; + + if (info[idx++] != '/') // Sustained wind speed + return false; + if (!parseInt(info, idx, 3, m_stormSustainedWindSpeed, unused)) + return false; + if (info[idx++] != '^') // Peak wind gusts + return false; + if (!parseInt(info, idx, 3, m_stormPeakWindGusts, unused)) + return false; + if (info[idx++] != '/') // Central pressure + return false; + if (!parseInt(info, idx, 4, m_stormCentralPresure, unused)) + return false; + if (info[idx++] != '>') // Radius hurrican winds + return false; + if (!parseInt(info, idx, 3, m_stormRadiusHurricanWinds, unused)) + return false; + if (info[idx++] != '&') // Radius tropical storm winds + return false; + if (!parseInt(info, idx, 3, m_stormRadiusTropicalStormWinds, unused)) + return false; + m_hasStormData = true; + // Optional field + if (info.length() >= idx + 4) + { + if (info[idx] != '%') // Radius whole gail + return true; + idx++; + if (!parseInt(info, idx, 3, m_stormRadiusWholeGail, m_hasStormRadiusWholeGail)) + return false; + } + return true; +} + +bool APRSPacket::parseObject(QString& info, int& idx) +{ + if (info.length() < idx+10) + return false; + + // Object names are 9 chars + m_objectName = info.mid(idx, 9).trimmed(); + idx += 9; + + if (info[idx] == '*') + m_objectLive = true; + else if (info[idx] == '_') + m_objectKilled = true; + else + return false; + idx++; + + return true; +} + +bool APRSPacket::parseItem(QString& info, int& idx) +{ + if (info.length() < idx+3) + return false; + + // Item names are 3-9 chars long, excluding ! or _ + m_objectName = ""; + int i; + for (i = 0; i < 10; i++) + { + if (info.length() >= idx) + { + QChar c = info[idx]; + if (c == '!' || c == '_') + break; + else + { + m_objectName.append(c); + idx++; + } + } + } + if (i == 11) + return false; + if (info[idx] == '!') + m_objectLive = true; + else if (info[idx] == '_') + m_objectKilled = true; + idx++; + + return true; +} + +bool APRSPacket::parseStatus(QString& info, int& idx) +{ + QString remaining = info.mid(idx); + + QRegExp timestampRE("^([0-9]{6})z"); // DHM timestamp + QRegExp maidenheadRE("^([A-Z]{2}[0-9]{2}[A-Z]{0,2})[/\\\\]."); // Maidenhead grid locator and symbol + + if (timestampRE.indexIn(remaining) >= 0) + { + parseTime(info, idx); + m_status = info.mid(idx); + idx += m_status.length(); + } + else if (maidenheadRE.indexIn(remaining) >= 0) + { + m_maidenhead = maidenheadRE.capturedTexts()[1]; + idx += m_maidenhead.length(); + m_symbolTable = info[idx++].toLatin1(); + m_symbolCode = info[idx++].toLatin1(); + m_hasSymbol = true; + if (info[idx] == ' ') + { + idx++; + m_status = info.mid(idx); + idx += m_status.length(); + } + } + else + { + m_status = remaining; + idx += m_status.length(); + } + m_hasStatus = true; + + // Check for beam heading and power in meteor scatter status reports + int len = m_status.length(); + if (len >= 3) + { + if (m_status[len-3] == '^') + { + bool error = false; + char h = m_status[len-2].toLatin1(); + char p = m_status[len-1].toLatin1(); + + if (isdigit(h)) + m_beamHeading = (h - '0') * 10; + else if (isupper(h)) + m_beamHeading = (h - 'A') * 10 + 100; + else + error = true; + + switch (p) + { + case '1': m_beamPower = 10; break; + case '2': m_beamPower = 40; break; + case '3': m_beamPower = 90; break; + case '4': m_beamPower = 160; break; + case '5': m_beamPower = 250; break; + case '6': m_beamPower = 360; break; + case '7': m_beamPower = 490; break; + case '8': m_beamPower = 640; break; + case '9': m_beamPower = 810; break; + case ':': m_beamPower = 1000; break; + case ';': m_beamPower = 1210; break; + case '<': m_beamPower = 1440; break; + case '=': m_beamPower = 1690; break; + case '>': m_beamPower = 1960; break; + case '?': m_beamPower = 2250; break; + case '@': m_beamPower = 2560; break; + case 'A': m_beamPower = 2890; break; + case 'B': m_beamPower = 3240; break; + case 'C': m_beamPower = 3610; break; + case 'D': m_beamPower = 4000; break; + case 'E': m_beamPower = 4410; break; + case 'F': m_beamPower = 4840; break; + case 'G': m_beamPower = 5290; break; + case 'H': m_beamPower = 5760; break; + case 'I': m_beamPower = 6250; break; + case 'J': m_beamPower = 6760; break; + case 'K': m_beamPower = 7290; break; + default: error = true; break; + } + if (!error) + { + m_hasBeam = true; + m_status = m_status.left(len - 3); + } + } + } + return true; +} + +bool APRSPacket::parseMessage(QString& info, int& idx) +{ + if (info.length() < idx+10) + return false; + + // Addressee is fixed width + if (info[idx+9] != ':') + return false; + m_addressee = info.mid(idx, 9).trimmed(); + idx += 10; + + // Message + m_message = info.mid(idx); + idx += m_message.length(); + + // Check if telemetry parameter/unit names + if (m_message.startsWith("PARM.")) + { + bool done = false; + QString s(""); + int i = 5; + while (!done) + { + if (i >= m_message.length()) + { + if (!s.isEmpty()) + m_telemetryNames.append(s); + done = true; + } + else if (m_message[i] == ',') + { + if (!s.isEmpty()) + m_telemetryNames.append(s); + i++; + s = ""; + } + else + s.append(m_message[i++]); + } + } + else if (m_message.startsWith("UNIT.")) + { + bool done = false; + QString s(""); + int i = 5; + while (!done) + { + if (i >= m_message.length()) + { + if (!s.isEmpty()) + m_telemetryLabels.append(s); + done = true; + } + else if (m_message[i] == ',') + { + if (!s.isEmpty()) + m_telemetryLabels.append(s); + i++; + s = ""; + } + else + s.append(m_message[i++]); + } + } + else if (m_message.startsWith("EQNS.")) + { + bool done = false; + QString s(""); + int i = 5; + QList telemetryCoefficients; + while (!done) + { + if (i >= m_message.length()) + { + if (!s.isEmpty()) + telemetryCoefficients.append(s); + done = true; + } + else if (m_message[i] == ',') + { + if (!s.isEmpty()) + telemetryCoefficients.append(s); + i++; + s = ""; + } + else + s.append(m_message[i++]); + } + m_hasTelemetryCoefficients = 0; + for (int j = 0; j < telemetryCoefficients.length() / 3; j++) + { + m_telemetryCoefficientsA[j] = telemetryCoefficients[j*3].toDouble(); + m_telemetryCoefficientsB[j] = telemetryCoefficients[j*3+1].toDouble(); + m_telemetryCoefficientsC[j] = telemetryCoefficients[j*3+2].toDouble(); + m_hasTelemetryCoefficients++; + } + } + else if (m_message.startsWith("BITS.")) + { + bool done = false; + QString s(""); + int i = 5; + for (int j = 0; j < 8; j++) + { + if (i >= m_message.length()) + m_telemetryBitSense[j] = m_message[i] == '1'; + else + m_telemetryBitSense[j] = true; + i++; + } + m_hasTelemetryBitSense = true; + m_telemetryProjectName = m_message.mid(i); + i += m_telemetryProjectName.length(); + } + else + { + // Check for message number + QRegExp noRE("\\{([0-9]{1,5})$"); + if (noRE.indexIn(m_message) >= 0) + { + m_messageNo = noRE.capturedTexts()[1]; + m_message = m_message.left(m_message.length() - m_messageNo.length() - 1); + } + } + m_hasMessage = true; + + return true; +} + +bool APRSPacket::parseTelemetry(QString& info, int& idx) +{ + if (info[idx] == '#') + { + // Telemetry report + idx++; + if ((info[idx] == 'M') && (info[idx+1] == 'I') && (info[idx+2] == 'C')) + idx += 3; + else if (isdigit(info[idx].toLatin1()) && isdigit(info[idx+1].toLatin1()) && isdigit(info[idx+2].toLatin1())) + { + m_seqNo = info.mid(idx, 3).toInt(); + m_hasSeqNo = true; + idx += 3; + } + else + return false; + + if (info[idx] == ',') + idx++; + parseInt(info, idx, 3, m_a1, m_a1HasValue); + if (info[idx++] != ',') + return false; + parseInt(info, idx, 3, m_a2, m_a2HasValue); + if (info[idx++] != ',') + return false; + parseInt(info, idx, 3, m_a3, m_a3HasValue); + if (info[idx++] != ',') + return false; + parseInt(info, idx, 3, m_a4, m_a4HasValue); + if (info[idx++] != ',') + return false; + parseInt(info, idx, 3, m_a5, m_a5HasValue); + if (info[idx++] != ',') + return false; + for (int i = 0; i < 8; i++) + m_b[i] = info[idx++] == '1'; + m_bHasValue = true; + + m_telemetryComment = info.mid(idx); + idx += m_telemetryComment.length(); + m_hasTelemetry = true; + return true; + } + else + return false; +} diff --git a/sdrbase/util/aprs.h b/sdrbase/util/aprs.h new file mode 100644 index 000000000..9e2d6d3bb --- /dev/null +++ b/sdrbase/util/aprs.h @@ -0,0 +1,445 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_APRS_H +#define INCLUDE_APRS_H + +#include +#include +#include +#include + +#include "export.h" +#include "ax25.h" +#include "util/units.h" + +struct SDRBASE_API APRSPacket { + QString m_from; + QString m_to; + QString m_via; + QString m_data; // Original ASCII data + + QDateTime m_dateTime; // Date/time of reception / decoding + + // Timestamp (where fields are not transmitted, time of decoding is used) + QDateTime m_timestamp; + bool m_utc; // Whether UTC (true) or local time (false) + bool m_hasTimestamp; + + // Position + float m_latitude; + float m_longitude; + bool m_hasPosition; + + float m_altitudeFt; + bool m_hasAltitude; + + // Symbol + char m_symbolTable; + char m_symbolCode; + bool m_hasSymbol; + QString m_symbolImage; // Image filename for the symbol + + // Course and speed + int m_course; + int m_speed; + bool m_hasCourseAndSpeed; + + // Power, antenna height, gain, directivity + int m_powerWatts; + int m_antennaHeightFt; + int m_antennaGainDB; + QString m_antennaDirectivity; // Omni, or N, NE... + bool m_hasStationDetails; + + // Radio range + int m_radioRangeMiles; + bool m_hasRadioRange; + + // Omni-DF + int m_dfStrength; + int m_dfHeightFt; + int m_dfGainDB; + QString m_dfAntennaDirectivity; + bool m_hasDf; + + QString m_objectName; // Also used for items + bool m_objectLive; + bool m_objectKilled; + + QString m_comment; + + // Weather reports + int m_windDirection; // In degrees + bool m_hasWindDirection; + int m_windSpeed; // In mph + bool m_hasWindSpeed; + int m_gust; // Peak wind speed in last 5 minutes in mph + bool m_hasGust; + int m_temp; // Fahrenheit, can be negative down to -99 + bool m_hasTemp; + int m_rainLastHr; // Hundreths of an inch + bool m_hasRainLastHr; + int m_rainLast24Hrs; + bool m_hasRainLast24Hrs; + int m_rainSinceMidnight; + bool m_hasRainSinceMidnight; + int m_humidity; // % + bool m_hasHumidity; + int m_barometricPressure; // Tenths of millibars / tenths of hPascal + bool m_hasBarometricPressure; + int m_luminosity; // Watts per m^2 + bool m_hasLuminsoity; + int m_snowfallLast24Hrs; // In inches + bool m_hasSnowfallLast24Hrs; + int m_rawRainCounter; + bool m_hasRawRainCounter; + int m_radiationLevel; + bool m_hasRadiationLevel; + int m_floodLevel; // Tenths of a foot. Can be negative + bool m_hasFloodLevel; + int m_batteryVolts; // Tenths of a volt + bool m_hasBatteryVolts; + QString m_weatherUnitType; + bool m_hasWeather; + + int m_stormDirection; + int m_stormSpeed; + QString m_stormType; + int m_stormSustainedWindSpeed; // knots + int m_stormPeakWindGusts; // knots + int m_stormCentralPresure; // millibars/hPascal + int m_stormRadiusHurricanWinds; // nautical miles + int m_stormRadiusTropicalStormWinds;// nautical miles + int m_stormRadiusWholeGail; // nautical miles + bool m_hasStormRadiusWholeGail; + bool m_hasStormData; + + // Status messages + QString m_status; + QString m_maidenhead; + int m_beamHeading; + int m_beamPower; + bool m_hasBeam; + bool m_hasStatus; + + // Messages + QString m_addressee; + QString m_message; + QString m_messageNo; + bool m_hasMessage; + QList m_telemetryNames; + QList m_telemetryLabels; + double m_telemetryCoefficientsA[5]; + double m_telemetryCoefficientsB[5]; + double m_telemetryCoefficientsC[5]; + int m_hasTelemetryCoefficients; + int m_telemetryBitSense[8]; + bool m_hasTelemetryBitSense; + QString m_telemetryProjectName; + + // Telemetry + int m_seqNo; + bool m_hasSeqNo; + int m_a1; + bool m_a1HasValue; + int m_a2; + bool m_a2HasValue; + int m_a3; + bool m_a3HasValue; + int m_a4; + bool m_a4HasValue; + int m_a5; + bool m_a5HasValue; + bool m_b[8]; + bool m_bHasValue; + QString m_telemetryComment; + bool m_hasTelemetry; + + bool decode(AX25Packet packet); + + APRSPacket() : + m_hasTimestamp(false), + m_hasPosition(false), + m_hasAltitude(false), + m_hasSymbol(false), + m_hasCourseAndSpeed(false), + m_hasStationDetails(false), + m_hasRadioRange(false), + m_hasDf(false), + m_objectLive(false), + m_objectKilled(false), + m_hasWindDirection(false), + m_hasWindSpeed(false), + m_hasGust(false), + m_hasTemp(false), + m_hasRainLastHr(false), + m_hasRainLast24Hrs(false), + m_hasRainSinceMidnight(false), + m_hasHumidity(false), + m_hasBarometricPressure(false), + m_hasLuminsoity(false), + m_hasSnowfallLast24Hrs(false), + m_hasRawRainCounter(false), + m_hasRadiationLevel(false), + m_hasFloodLevel(false), + m_hasBatteryVolts(false), + m_hasWeather(false), + m_hasStormRadiusWholeGail(false), + m_hasStormData(false), + m_hasBeam(false), + m_hasStatus(false), + m_hasMessage(false), + m_hasTelemetryCoefficients(0), + m_hasTelemetryBitSense(false), + m_hasSeqNo(false), + m_a1HasValue(false), + m_a2HasValue(false), + m_a3HasValue(false), + m_a4HasValue(false), + m_a5HasValue(false), + m_bHasValue(false), + m_hasTelemetry(false) + { + } + + QString date() + { + if (m_hasTimestamp) + return m_timestamp.date().toString("yyyy/MM/dd"); + else + return QString(""); + } + + QString time() + { + if (m_hasTimestamp) + return m_timestamp.time().toString("hh:mm:ss"); + else + return QString(""); + } + + QString dateTime() + { + return QString("%1 %2").arg(date()).arg(time()); + } + + QString position() + { + return QString("%1,%2").arg(m_latitude).arg(m_longitude); + } + + QString toTNC2(QString igateCallsign) + { + return m_from + ">" + m_to + (m_via.isEmpty() ? "" : ("," + m_via)) + ",qAR," + igateCallsign + ":" + m_data + "\r\n"; + } + + // Convert a TNC2 formatted packet (as sent by APRS-IS Igates) to an AX25 byte array + static QByteArray toByteArray(QString tnc2) + { + QByteArray bytes; + QString tmp = ""; + QString from; + int state = 0; + + for (int i = 0; i < tnc2.length(); i++) + { + if (state == 0) + { + // From + if (tnc2[i] == '>') + { + from = tmp; + tmp = ""; + state = 1; + } + else + tmp.append(tnc2[i]); + } + else if (state == 1) + { + // To + if (tnc2[i] == ':') + { + bytes.append(AX25Packet::encodeAddress(tmp)); + bytes.append(AX25Packet::encodeAddress(from, 1)); + state = 3; + } + else if (tnc2[i] == ',') + { + bytes.append(AX25Packet::encodeAddress(tmp)); + bytes.append(AX25Packet::encodeAddress(from)); + tmp = ""; + state = 2; + } + else + tmp.append(tnc2[i]); + } + else if (state == 2) + { + // Via + if (tnc2[i] == ':') + { + bytes.append(AX25Packet::encodeAddress(tmp, 1)); + state = 3; + } + else if (tnc2[i] == ',') + { + bytes.append(AX25Packet::encodeAddress(tmp)); + tmp = ""; + } + else + tmp.append(tnc2[i]); + } + else if (state == 3) + { + // UI Type and PID + bytes.append(3); + bytes.append(-16); // 0xf0 + // APRS message + bytes.append(tnc2.mid(i).toLatin1().trimmed()); + // CRC + bytes.append((char)0); + bytes.append((char)0); + break; + } + } + + return bytes; + } + + QString toText(bool includeFrom=true, bool includePosition=false, char separator='\n') + { + QStringList text; + + if (!m_objectName.isEmpty()) + { + text.append(QString("%1 (%2)").arg(m_objectName).arg(m_from)); + } + else + { + if (includeFrom) + text.append(m_from); + } + + if (m_hasTimestamp) + { + QStringList time; + time.append(this->date()); + time.append(this->time()); + if (m_utc) + time.append("UTC"); + else + time.append("local"); + text.append(time.join(' ')); + } + + if (includePosition && m_hasPosition) + text.append(QString("Latitude: %1 Longitude: %2").arg(m_latitude).arg(m_longitude)); + if (m_hasAltitude) + text.append(QString("Altitude: %1 ft").arg(m_altitudeFt)); + if (m_hasCourseAndSpeed) + text.append(QString("Course: %1%3 Speed: %2 knts").arg(m_course).arg(m_speed).arg(QChar(0xb0))); + + if (m_hasStationDetails) + text.append(QString("TX Power: %1 Watts Antenna Height: %2 m Gain: %3 dB Direction: %4").arg(m_powerWatts).arg(std::round(Units::feetToMetres(m_antennaHeightFt))).arg(m_antennaGainDB).arg(m_antennaDirectivity)); + if (m_hasRadioRange) + text.append(QString("Range: %1 km").arg(Units::milesToKilometres(m_radioRangeMiles))); + if (m_hasDf) + text.append(QString("DF Strength: S %1 Height: %2 m Gain: %3 dB Direction: %4").arg(m_dfStrength).arg(std::round(Units::feetToMetres(m_dfHeightFt))).arg(m_dfGainDB).arg(m_dfAntennaDirectivity)); + + if (m_hasWeather) + { + QStringList weather, wind, air, rain; + + wind.append(QString("Wind")); + if (m_hasWindDirection) + wind.append(QString("%1%2").arg(m_windDirection).arg(QChar(0xb0))); + if (m_hasWindSpeed) + wind.append(QString("%1 mph").arg(m_windSpeed)); + if (m_hasGust) + wind.append(QString("Gusts %1 mph").arg(m_gust)); + weather.append(wind.join(' ')); + + if (m_hasTemp || m_hasHumidity || m_hasBarometricPressure) + { + air.append("Air"); + if (m_hasTemp) + air.append(QString("Temperature %1C").arg(Units::fahrenheitToCelsius(m_temp), 0, 'f', 1)); + if (m_hasHumidity) + air.append(QString("Humidity %1%").arg(m_humidity)); + if (m_hasBarometricPressure) + air.append(QString("Pressure %1 mbar").arg(m_barometricPressure/10.0)); + weather.append(air.join(' ')); + } + + if (m_hasRainLastHr || m_hasRainLast24Hrs || m_hasRainSinceMidnight) + { + rain.append("Rain"); + if (m_hasRainLastHr) + rain.append(QString("%1 mm last hour").arg(std::round(Units::inchesToMilimetres(m_rainLastHr/100.0)))); + if (m_hasRainLast24Hrs) + rain.append(QString("%1 mm last 24 hours").arg(std::round(Units::inchesToMilimetres(m_rainLast24Hrs/100.0)))); + if (m_hasRainSinceMidnight) + rain.append(QString("%1 mm since midnight").arg(std::round(Units::inchesToMilimetres(m_rainSinceMidnight/100.0)))); + weather.append(rain.join(' ')); + } + + if (!m_weatherUnitType.isEmpty()) + weather.append(m_weatherUnitType); + + text.append(weather.join(separator)); + } + + if (m_hasStormData) + { + QStringList storm; + + storm.append(m_stormType); + + storm.append(QString("Direction: %1%3 Speed: %2").arg(m_stormDirection).arg(m_stormSpeed).arg(QChar(0xb0))); + storm.append(QString("Sustained wind speed: %1 knots Peak wind gusts: %2 knots Central pressure: %3 mbar").arg(m_stormSustainedWindSpeed).arg(m_stormPeakWindGusts).arg(m_stormCentralPresure)); + storm.append(QString("Hurrican winds radius: %1 nm Tropical storm winds radius: %2 nm%3").arg(m_stormRadiusHurricanWinds).arg(m_stormRadiusTropicalStormWinds).arg(m_hasStormRadiusWholeGail ? QString("") : QString(" Whole gail radius: %3 nm").arg(m_stormRadiusWholeGail))); + + text.append(storm.join(separator)); + } + + if (!m_comment.isEmpty()) + text.append(m_comment); + + return text.join(separator); + } + +private: + int charToInt(QString &s, int idx); + bool parseTime(QString& info, int& idx); + bool parseTimeMDHM(QString& info, int& idx); + bool isLatLongChar(QCharRef c); + bool parsePosition(QString& info, int& idx); + bool parseDataExension(QString& info, int& idx); + bool parseComment(QString& info, int& idx); + bool parseInt(QString& info, int& idx, int chars, int& value, bool& hasValue); + bool parseWeather(QString& info, int& idx, bool positionLess); + bool parseStorm(QString& info, int& idx); + bool parseObject(QString& info, int& idx); + bool parseItem(QString& info, int& idx); + bool parseStatus(QString& info, int& idx); + bool parseMessage(QString& info, int& idx); + bool parseTelemetry(QString& info, int& idx); +}; + +#endif // INCLUDE_APRS_H