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
+
+ 1
+
+
+ ButtonSwitch
+ QToolButton
+
+
+
+ QChartView
+ QGraphicsView
+
+
+
+
+ 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
+
+
+
+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.
+
+
+
+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