diff --git a/plugins/channelrx/demodpager/CMakeLists.txt b/plugins/channelrx/demodpager/CMakeLists.txt
index 2b1a99a1a..b92fa7c8e 100644
--- a/plugins/channelrx/demodpager/CMakeLists.txt
+++ b/plugins/channelrx/demodpager/CMakeLists.txt
@@ -29,16 +29,25 @@ if(NOT SERVER_MODE)
pagerdemodgui.ui
pagerdemodcharsetdialog.cpp
pagerdemodcharsetdialog.ui
+ pagerdemodnotificationdialog.cpp
+ pagerdemodnotificationdialog.ui
+ pagerdemodfilterdialog.cpp
+ pagerdemodfilterdialog.ui
+ pagerdemodicons.qrc
)
set(demodpager_HEADERS
${demodpager_HEADERS}
pagerdemodgui.h
pagerdemodcharsetdialog.h
+ pagerdemodnotificationdialog.h
)
set(TARGET_NAME ${PLUGINS_PREFIX}demodpager)
set(TARGET_LIB "Qt::Widgets")
set(TARGET_LIB_GUI "sdrgui")
+ if(Qt${QT_DEFAULT_MAJOR_VERSION}TextToSpeech_FOUND)
+ list(APPEND TARGET_LIB_GUI Qt::TextToSpeech)
+ endif()
set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR})
else()
set(TARGET_NAME ${PLUGINSSRV_PREFIX}demodpagersrv)
diff --git a/plugins/channelrx/demodpager/icons/filterduplicate.png b/plugins/channelrx/demodpager/icons/filterduplicate.png
new file mode 100644
index 000000000..9ea51d40c
Binary files /dev/null and b/plugins/channelrx/demodpager/icons/filterduplicate.png differ
diff --git a/plugins/channelrx/demodpager/pagerdemod.cpp b/plugins/channelrx/demodpager/pagerdemod.cpp
index 811891d2c..a460d3263 100644
--- a/plugins/channelrx/demodpager/pagerdemod.cpp
+++ b/plugins/channelrx/demodpager/pagerdemod.cpp
@@ -31,6 +31,7 @@
#include "dsp/dspcommands.h"
#include "device/deviceapi.h"
#include "util/db.h"
+#include "util/csv.h"
#include "maincore.h"
MESSAGE_CLASS_DEFINITION(PagerDemod::MsgConfigurePagerDemod, Message)
@@ -141,7 +142,7 @@ bool PagerDemod::handleMessage(const Message& cmd)
{
if (MsgConfigurePagerDemod::match(cmd))
{
- MsgConfigurePagerDemod& cfg = (MsgConfigurePagerDemod&) cmd;
+ const MsgConfigurePagerDemod& cfg = (const MsgConfigurePagerDemod&) cmd;
qDebug() << "PagerDemod::handleMessage: MsgConfigurePagerDemod";
applySettings(cfg.getSettings(), cfg.getForce());
@@ -149,7 +150,7 @@ bool PagerDemod::handleMessage(const Message& cmd)
}
else if (DSPSignalNotification::match(cmd))
{
- DSPSignalNotification& notif = (DSPSignalNotification&) cmd;
+ const DSPSignalNotification& notif = (const DSPSignalNotification&) cmd;
m_basebandSampleRate = notif.getSampleRate();
m_centerFrequency = notif.getCenterFrequency();
// Forward to the sink
@@ -166,7 +167,7 @@ bool PagerDemod::handleMessage(const Message& cmd)
else if (MsgPagerMessage::match(cmd))
{
// Forward to GUI
- MsgPagerMessage& report = (MsgPagerMessage&)cmd;
+ const MsgPagerMessage& report = (const MsgPagerMessage&)cmd;
if (getMessageQueueToGUI())
{
MsgPagerMessage *msg = new MsgPagerMessage(report);
@@ -200,7 +201,7 @@ bool PagerDemod::handleMessage(const Message& cmd)
<< report.getDateTime().time().toString() << ","
<< QString("%1").arg(report.getAddress(), 7, 10, QChar('0')) << ","
<< QString::number(report.getFunctionBits()) << ","
- << "\"" << report.getAlphaMessage() << "\","
+ << CSV::escape(report.getAlphaMessage()) << ","
<< report.getNumericMessage() << ","
<< QString::number(report.getEvenParityErrors()) << ","
<< QString::number(report.getBCHParityErrors()) << "\n";
diff --git a/plugins/channelrx/demodpager/pagerdemodbaseband.cpp b/plugins/channelrx/demodpager/pagerdemodbaseband.cpp
index 07dfbaa00..e6b870d6e 100644
--- a/plugins/channelrx/demodpager/pagerdemodbaseband.cpp
+++ b/plugins/channelrx/demodpager/pagerdemodbaseband.cpp
@@ -131,7 +131,7 @@ bool PagerDemodBaseband::handleMessage(const Message& cmd)
if (MsgConfigurePagerDemodBaseband::match(cmd))
{
QMutexLocker mutexLocker(&m_mutex);
- MsgConfigurePagerDemodBaseband& cfg = (MsgConfigurePagerDemodBaseband&) cmd;
+ const MsgConfigurePagerDemodBaseband& cfg = (const MsgConfigurePagerDemodBaseband&) cmd;
qDebug() << "PagerDemodBaseband::handleMessage: MsgConfigurePagerDemodBaseband";
applySettings(cfg.getSettings(), cfg.getForce());
@@ -141,7 +141,7 @@ bool PagerDemodBaseband::handleMessage(const Message& cmd)
else if (DSPSignalNotification::match(cmd))
{
QMutexLocker mutexLocker(&m_mutex);
- DSPSignalNotification& notif = (DSPSignalNotification&) cmd;
+ const DSPSignalNotification& notif = (const DSPSignalNotification&) cmd;
qDebug() << "PagerDemodBaseband::handleMessage: DSPSignalNotification: basebandSampleRate: " << notif.getSampleRate();
setBasebandSampleRate(notif.getSampleRate());
m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(notif.getSampleRate()));
diff --git a/plugins/channelrx/demodpager/pagerdemodfilterdialog.cpp b/plugins/channelrx/demodpager/pagerdemodfilterdialog.cpp
new file mode 100644
index 000000000..91233851f
--- /dev/null
+++ b/plugins/channelrx/demodpager/pagerdemodfilterdialog.cpp
@@ -0,0 +1,46 @@
+///////////////////////////////////////////////////////////////////////////////////
+// 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
+#include
+
+#include "pagerdemodfilterdialog.h"
+
+PagerDemodFilterDialog::PagerDemodFilterDialog(PagerDemodSettings *settings,
+ QWidget* parent) :
+ QDialog(parent),
+ ui(new Ui::PagerDemodFilterDialog),
+ m_settings(settings)
+{
+ ui->setupUi(this);
+
+ ui->matchLastOnly->setChecked(m_settings->m_duplicateMatchLastOnly);
+ ui->matchMessageOnly->setChecked(m_settings->m_duplicateMatchMessageOnly);
+}
+
+PagerDemodFilterDialog::~PagerDemodFilterDialog()
+{
+ delete ui;
+}
+
+void PagerDemodFilterDialog::accept()
+{
+ m_settings->m_duplicateMatchLastOnly = ui->matchLastOnly->isChecked();
+ m_settings->m_duplicateMatchMessageOnly = ui->matchMessageOnly->isChecked();
+
+ QDialog::accept();
+}
diff --git a/plugins/channelrx/demodpager/pagerdemodfilterdialog.h b/plugins/channelrx/demodpager/pagerdemodfilterdialog.h
new file mode 100644
index 000000000..0784531b9
--- /dev/null
+++ b/plugins/channelrx/demodpager/pagerdemodfilterdialog.h
@@ -0,0 +1,41 @@
+///////////////////////////////////////////////////////////////////////////////////
+// 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_PAGERDEMODFILTERDIALOG_H
+#define INCLUDE_PAGERDEMODFILTERDIALOG_H
+
+
+#include "ui_pagerdemodfilterdialog.h"
+#include "pagerdemodsettings.h"
+
+
+class PagerDemodFilterDialog : public QDialog {
+ Q_OBJECT
+
+public:
+ explicit PagerDemodFilterDialog(PagerDemodSettings* settings, QWidget* parent = 0);
+ ~PagerDemodFilterDialog();
+
+private slots:
+ void accept() override;
+
+private:
+ Ui::PagerDemodFilterDialog* ui;
+ PagerDemodSettings *m_settings;
+};
+
+#endif // INCLUDE_PAGERDEMODFILTERDIALOG_H
diff --git a/plugins/channelrx/demodpager/pagerdemodfilterdialog.ui b/plugins/channelrx/demodpager/pagerdemodfilterdialog.ui
new file mode 100644
index 000000000..cf6c81440
--- /dev/null
+++ b/plugins/channelrx/demodpager/pagerdemodfilterdialog.ui
@@ -0,0 +1,118 @@
+
+
+ PagerDemodFilterDialog
+
+
+
+ 0
+ 0
+ 396
+ 167
+
+
+
+
+ Liberation Sans
+ 9
+
+
+
+ Qt::ContextMenuPolicy::PreventContextMenu
+
+
+ Duplicate Filtering
+
+
+ -
+
+
+ Duplicate Filtering
+
+
+
-
+
+
+ Match message only
+
+
+
+ -
+
+
+ Whether both the address and message must match or only the message to be considered a duplicate
+
+
+
+
+
+
+ -
+
+
+ Match last message only
+
+
+
+ -
+
+
+ Whether to match with only the last message or any message in the table
+
+
+
+
+
+
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+ QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok
+
+
+
+
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ PagerDemodFilterDialog
+ accept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ PagerDemodFilterDialog
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+
diff --git a/plugins/channelrx/demodpager/pagerdemodgui.cpp b/plugins/channelrx/demodpager/pagerdemodgui.cpp
index e8186da80..19ccfa4d1 100644
--- a/plugins/channelrx/demodpager/pagerdemodgui.cpp
+++ b/plugins/channelrx/demodpager/pagerdemodgui.cpp
@@ -16,8 +16,6 @@
// along with this program. If not, see . //
///////////////////////////////////////////////////////////////////////////////////
-#include
-#include
#include
#include
#include
@@ -26,6 +24,7 @@
#include
#include
#include
+#include
#include "pagerdemodgui.h"
@@ -36,6 +35,8 @@
#include "plugin/pluginapi.h"
#include "util/db.h"
#include "util/csv.h"
+#include "util/units.h"
+#include "gui/crightclickenabler.h"
#include "gui/basicchannelsettingsdialog.h"
#include "dsp/dspengine.h"
#include "gui/dialogpositioner.h"
@@ -43,6 +44,10 @@
#include "pagerdemod.h"
#include "pagerdemodcharsetdialog.h"
+#include "pagerdemodnotificationdialog.h"
+#include "pagerdemodfilterdialog.h"
+
+#include "SWGMapItem.h"
void PagerDemodGUI::resizeTable()
{
@@ -50,7 +55,7 @@ void PagerDemodGUI::resizeTable()
// Trailing spaces are for sort arrow
int row = ui->messages->rowCount();
ui->messages->setRowCount(row + 1);
- ui->messages->setItem(row, MESSAGE_COL_DATE, new QTableWidgetItem("Fri Apr 15 2016-"));
+ ui->messages->setItem(row, MESSAGE_COL_DATE, new QTableWidgetItem("Fri Apr 15 2016--"));
ui->messages->setItem(row, MESSAGE_COL_TIME, new QTableWidgetItem("10:17:00"));
ui->messages->setItem(row, MESSAGE_COL_ADDRESS, new QTableWidgetItem("1000000"));
ui->messages->setItem(row, MESSAGE_COL_MESSAGE, new QTableWidgetItem("ABCEDGHIJKLMNOPQRSTUVWXYZABCEDGHIJKLMNOPQRSTUVWXYZ"));
@@ -144,11 +149,101 @@ bool PagerDemodGUI::deserialize(const QByteArray& data)
}
}
+QString PagerDemodGUI::selectMessage(int functionBits, const QString &numericMessage, const QString &alphaMessage) const
+{
+ QString message;
+
+ // Standard way of choosing numeric or alpha decode isn't followed widely
+ if (m_settings.m_decode == PagerDemodSettings::Standard)
+ {
+ // Encoding is based on function bits
+ if (functionBits == 0) {
+ message = numericMessage;
+ } else {
+ message = alphaMessage;
+ }
+ }
+ else if (m_settings.m_decode == PagerDemodSettings::Inverted)
+ {
+ // Encoding is based on function bits, but inverted from standard
+ if (functionBits == 3) {
+ message = numericMessage;
+ } else {
+ message = alphaMessage;
+ }
+ }
+ else if (m_settings.m_decode == PagerDemodSettings::Numeric)
+ {
+ // Always display as numeric
+ message = numericMessage;
+ }
+ else if (m_settings.m_decode == PagerDemodSettings::Alphanumeric)
+ {
+ // Always display as alphanumeric
+ message = alphaMessage;
+ }
+ else
+ {
+ // Guess at what the encoding is
+ QString numeric = numericMessage;
+ QString alpha = alphaMessage;
+ bool done = false;
+ if (!done)
+ {
+ // If alpha contains control characters, possibly numeric
+ for (int i = 0; i < alpha.size(); i++)
+ {
+ if (iscntrl(alpha[i].toLatin1()) && !isspace(alpha[i].toLatin1()))
+ {
+ message = numeric;
+ done = true;
+ break;
+ }
+ }
+ }
+ if (!done) {
+ // Possibly not likely to get only longer than 15 digits
+ if (numeric.size() > 15)
+ {
+ done = true;
+ message = alpha;
+ }
+ }
+ if (!done) {
+ // Default to alpha
+ message = alpha;
+ }
+ }
+
+ return message;
+
+}
+
// Add row to table
void PagerDemodGUI::messageReceived(const QDateTime dateTime, int address, int functionBits,
const QString &numericMessage, const QString &alphaMessage,
int evenParityErrors, int bchParityErrors)
{
+ QString message = selectMessage(functionBits, numericMessage, alphaMessage);
+ QString addressString = QString("%1").arg(address, 7, 10, QChar('0'));
+
+ // Should we ignore the message if it is a duplicate?
+ if (m_settings.m_filterDuplicates && (ui->messages->rowCount() > 0))
+ {
+ int startRow = m_settings.m_duplicateMatchLastOnly ? ui->messages->rowCount() - 1 : 0;
+ for (int row = startRow; row < ui->messages->rowCount(); row++)
+ {
+ QString prevAddress = ui->messages->item(row, MESSAGE_COL_ADDRESS)->text();
+ QString prevMessage = ui->messages->item(row, MESSAGE_COL_MESSAGE)->text();
+
+ if ((message == prevMessage) && (m_settings.m_duplicateMatchMessageOnly || (addressString == prevAddress)))
+ {
+ // Ignore this message
+ return;
+ }
+ }
+ }
+
// Is scroll bar at bottom
QScrollBar *sb = ui->messages->verticalScrollBar();
bool scrollToBottom = sb->value() == sb->maximum();
@@ -178,68 +273,8 @@ void PagerDemodGUI::messageReceived(const QDateTime dateTime, int address, int f
ui->messages->setItem(row, MESSAGE_COL_BCH_PE, bchPEItem);
dateItem->setText(dateTime.date().toString());
timeItem->setText(dateTime.time().toString());
- addressItem->setText(QString("%1").arg(address, 7, 10, QChar('0')));
- // Standard way of choosing numeric or alpha decode isn't followed widely
- if (m_settings.m_decode == PagerDemodSettings::Standard)
- {
- // Encoding is based on function bits
- if (functionBits == 0) {
- messageItem->setText(numericMessage);
- } else {
- messageItem->setText(alphaMessage);
- }
- }
- else if (m_settings.m_decode == PagerDemodSettings::Inverted)
- {
- // Encoding is based on function bits, but inverted from standard
- if (functionBits == 3) {
- messageItem->setText(numericMessage);
- } else {
- messageItem->setText(alphaMessage);
- }
- }
- else if (m_settings.m_decode == PagerDemodSettings::Numeric)
- {
- // Always display as numeric
- messageItem->setText(numericMessage);
- }
- else if (m_settings.m_decode == PagerDemodSettings::Alphanumeric)
- {
- // Always display as alphanumeric
- messageItem->setText(alphaMessage);
- }
- else
- {
- // Guess at what the encoding is
- QString numeric = numericMessage;
- QString alpha = alphaMessage;
- bool done = false;
- if (!done)
- {
- // If alpha contains control characters, possibly numeric
- for (int i = 0; i < alpha.size(); i++)
- {
- if (iscntrl(alpha[i].toLatin1()) && !isspace(alpha[i].toLatin1()))
- {
- messageItem->setText(numeric);
- done = true;
- break;
- }
- }
- }
- if (!done) {
- // Possibly not likely to get only longer than 15 digits
- if (numeric.size() > 15)
- {
- done = true;
- messageItem->setText(alpha);
- }
- }
- if (!done) {
- // Default to alpha
- messageItem->setText(alpha);
- }
- }
+ addressItem->setText(addressString);
+ messageItem->setText(message);
functionItem->setText(QString("%1").arg(functionBits));
alphaItem->setText(alphaMessage);
numericItem->setText(numericMessage);
@@ -250,6 +285,7 @@ void PagerDemodGUI::messageReceived(const QDateTime dateTime, int address, int f
if (scrollToBottom) {
ui->messages->scrollToBottom();
}
+ checkNotification(row);
}
bool PagerDemodGUI::handleMessage(const Message& message)
@@ -257,7 +293,7 @@ bool PagerDemodGUI::handleMessage(const Message& message)
if (PagerDemod::MsgConfigurePagerDemod::match(message))
{
qDebug("PagerDemodGUI::handleMessage: PagerDemod::MsgConfigurePagerDemod");
- const PagerDemod::MsgConfigurePagerDemod& cfg = (PagerDemod::MsgConfigurePagerDemod&) message;
+ const PagerDemod::MsgConfigurePagerDemod& cfg = (const PagerDemod::MsgConfigurePagerDemod&) message;
m_settings = cfg.getSettings();
blockApplySettings(true);
ui->scopeGUI->updateSettings();
@@ -268,7 +304,7 @@ bool PagerDemodGUI::handleMessage(const Message& message)
}
else if (PagerDemod::MsgPagerMessage::match(message))
{
- PagerDemod::MsgPagerMessage& report = (PagerDemod::MsgPagerMessage&) message;
+ const PagerDemod::MsgPagerMessage& report = (const PagerDemod::MsgPagerMessage&) message;
messageReceived(report.getDateTime(), report.getAddress(), report.getFunctionBits(),
report.getNumericMessage(), report.getAlphaMessage(),
report.getEvenParityErrors(), report.getBCHParityErrors());
@@ -276,7 +312,7 @@ bool PagerDemodGUI::handleMessage(const Message& message)
}
else if (DSPSignalNotification::match(message))
{
- DSPSignalNotification& notif = (DSPSignalNotification&) message;
+ const DSPSignalNotification& notif = (const DSPSignalNotification&) message;
m_deviceCenterFrequency = notif.getCenterFrequency();
m_basebandSampleRate = notif.getSampleRate();
ui->deltaFrequency->setValueRange(false, 7, -m_basebandSampleRate/2, m_basebandSampleRate/2);
@@ -479,7 +515,8 @@ PagerDemodGUI::PagerDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Bas
m_deviceCenterFrequency(0),
m_basebandSampleRate(1),
m_doApplySettings(true),
- m_tickCount(0)
+ m_tickCount(0),
+ m_speech(nullptr)
{
setAttribute(Qt::WA_DeleteOnClose, true);
m_helpURL = "plugins/channelrx/demodpager/readme.md";
@@ -526,6 +563,9 @@ PagerDemodGUI::PagerDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Bas
connect(&m_channelMarker, SIGNAL(highlightedByCursor()), this, SLOT(channelMarkerHighlightedByCursor()));
connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages()));
+ CRightClickEnabler *filterDuplicatesRightClickEnabler = new CRightClickEnabler(ui->filterDuplicates);
+ connect(filterDuplicatesRightClickEnabler, SIGNAL(rightClick(const QPoint &)), this, SLOT(on_filterDuplicates_rightClicked(const QPoint &)));
+
// Resize the table using dummy data
resizeTable();
// Allow user to reorder columns
@@ -575,6 +615,7 @@ void PagerDemodGUI::customContextMenuRequested(QPoint pos)
PagerDemodGUI::~PagerDemodGUI()
{
+ clearFromMap();
delete ui;
}
@@ -638,6 +679,8 @@ void PagerDemodGUI::displaySettings()
ui->logFilename->setToolTip(QString(".csv log filename: %1").arg(m_settings.m_logFilename));
ui->logEnable->setChecked(m_settings.m_logEnabled);
+ ui->filterDuplicates->setChecked(m_settings.m_filterDuplicates);
+
// Order and size columns
QHeaderView *header = ui->messages->horizontalHeader();
@@ -656,6 +699,7 @@ void PagerDemodGUI::displaySettings()
getRollupContents()->restoreState(m_rollupState);
updateAbsoluteCenterFrequency();
blockApplySettings(false);
+ enableSpeechIfNeeded();
}
void PagerDemodGUI::leaveEvent(QEvent* event)
@@ -693,12 +737,39 @@ void PagerDemodGUI::tick()
void PagerDemodGUI::on_charset_clicked()
{
PagerDemodCharsetDialog dialog(&m_settings);
- if (dialog.exec() == QDialog::Accepted)
- {
+ new DialogPositioner(&dialog, true);
+ if (dialog.exec() == QDialog::Accepted) {
applySettings();
}
}
+void PagerDemodGUI::on_notifications_clicked()
+{
+ PagerDemodNotificationDialog dialog(&m_settings);
+ new DialogPositioner(&dialog, true);
+ if (dialog.exec() == QDialog::Accepted)
+ {
+ enableSpeechIfNeeded();
+ applySettings();
+ }
+}
+
+void PagerDemodGUI::on_filterDuplicates_clicked(bool checked)
+{
+ m_settings.m_filterDuplicates = checked;
+ applySettings();
+}
+
+void PagerDemodGUI::on_filterDuplicates_rightClicked(const QPoint &p)
+{
+ (void) p;
+
+ PagerDemodFilterDialog dialog(&m_settings);
+ new DialogPositioner(&dialog, true);
+ if (dialog.exec() == QDialog::Accepted) {
+ applySettings();
+ }
+}
void PagerDemodGUI::on_logEnable_clicked(bool checked)
{
@@ -813,6 +884,8 @@ void PagerDemodGUI::makeUIConnections()
QObject::connect(ui->udpEnabled, &QCheckBox::clicked, this, &PagerDemodGUI::on_udpEnabled_clicked);
QObject::connect(ui->udpAddress, &QLineEdit::editingFinished, this, &PagerDemodGUI::on_udpAddress_editingFinished);
QObject::connect(ui->udpPort, &QLineEdit::editingFinished, this, &PagerDemodGUI::on_udpPort_editingFinished);
+ QObject::connect(ui->notifications, &QToolButton::clicked, this, &PagerDemodGUI::on_notifications_clicked);
+ QObject::connect(ui->filterDuplicates, &ButtonSwitch::clicked, this, &PagerDemodGUI::on_filterDuplicates_clicked);
QObject::connect(ui->logEnable, &ButtonSwitch::clicked, this, &PagerDemodGUI::on_logEnable_clicked);
QObject::connect(ui->logFilename, &QToolButton::clicked, this, &PagerDemodGUI::on_logFilename_clicked);
QObject::connect(ui->logOpen, &QToolButton::clicked, this, &PagerDemodGUI::on_logOpen_clicked);
@@ -824,3 +897,178 @@ void PagerDemodGUI::updateAbsoluteCenterFrequency()
{
setStatusFrequency(m_deviceCenterFrequency + m_settings.m_inputFrequencyOffset);
}
+
+// Initialise text to speech engine
+// This takes 10 seconds on some versions of Linux, so only do it, if user actually
+// has speech notifications configured
+void PagerDemodGUI::enableSpeechIfNeeded()
+{
+#ifdef QT_TEXTTOSPEECH_FOUND
+ if (m_speech) {
+ return;
+ }
+ for (const auto& notification : m_settings.m_notificationSettings)
+ {
+ if (!notification->m_speech.isEmpty())
+ {
+ qDebug() << "PagerDemodGUI: Enabling text to speech";
+ m_speech = new QTextToSpeech(this);
+ return;
+ }
+ }
+#endif
+}
+
+void PagerDemodGUI::checkNotification(int row)
+{
+ QString address = ui->messages->item(row, MESSAGE_COL_ADDRESS)->text();
+ QString message = ui->messages->item(row, MESSAGE_COL_MESSAGE)->text();
+
+ for (int i = 0; i < m_settings.m_notificationSettings.size(); i++)
+ {
+ QString match;
+ switch (m_settings.m_notificationSettings[i]->m_matchColumn)
+ {
+ case MESSAGE_COL_ADDRESS:
+ match = address;
+ break;
+ case MESSAGE_COL_MESSAGE:
+ match = message;
+ break;
+ }
+ if (!match.isEmpty())
+ {
+ if (m_settings.m_notificationSettings[i]->m_regularExpression.isValid())
+ {
+ QRegularExpressionMatch matchResult = m_settings.m_notificationSettings[i]->m_regularExpression.match(match);
+ if (matchResult.hasMatch())
+ {
+ if (m_settings.m_notificationSettings[i]->m_highlight) {
+ ui->messages->item(row, MESSAGE_COL_MESSAGE)->setTextColor(m_settings.m_notificationSettings[i]->m_highlightColor);
+ }
+
+ if (!m_settings.m_notificationSettings[i]->m_speech.isEmpty())
+ {
+ QString speech = subStrings(address, message, matchResult, m_settings.m_notificationSettings[i]->m_speech);
+
+ speechNotification(speech);
+ }
+ if (!m_settings.m_notificationSettings[i]->m_command.isEmpty())
+ {
+ QString command = subStrings(address, message, matchResult, m_settings.m_notificationSettings[i]->m_command);
+
+ commandNotification(command);
+ }
+ if (m_settings.m_notificationSettings[i]->m_plotOnMap)
+ {
+ float latitude;
+ float longitude;
+
+ if (Units::stringToLatitudeAndLongitude(message, latitude, longitude, false))
+ {
+ QDateTime dateTime;
+
+ dateTime.setDate(QDate::fromString(ui->messages->item(row, MESSAGE_COL_DATE)->text()));
+ dateTime.setTime(QTime::fromString(ui->messages->item(row, MESSAGE_COL_TIME)->text()));
+
+ sendToMap(address, message, latitude, longitude, dateTime);
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+QString PagerDemodGUI::subStrings(const QString& address, const QString& message, const QRegularExpressionMatch& match, const QString &string) const
+{
+ QString s = string;
+ s = s.replace("${address}", address);
+ s = s.replace("${message}", message);
+ for (int i = 0; i < match.capturedTexts().size(); i++)
+ {
+ QString escape = QString("${%1}").arg(i);
+ s = s.replace(escape, match.capturedTexts()[i]);
+ }
+ return s;
+}
+
+void PagerDemodGUI::speechNotification(const QString &speech)
+{
+#ifdef QT_TEXTTOSPEECH_FOUND
+ if (m_speech) {
+ m_speech->say(speech);
+ } else {
+ qWarning() << "PagerDemodGUI::speechNotification: Unable to say " << speech;
+ }
+#else
+ qWarning() << "PagerDemodGUI::speechNotification: TextToSpeech not supported. Unable to say " << speech;
+#endif
+}
+
+void PagerDemodGUI::commandNotification(const QString &command)
+{
+#if QT_CONFIG(process)
+ QStringList allArgs = QProcess::splitCommand(command);
+
+ if (allArgs.size() > 0)
+ {
+ QString program = allArgs[0];
+ allArgs.pop_front();
+ QProcess::startDetached(program, allArgs);
+ }
+#else
+ qWarning() << "PagerDemodGUI::commandNotification: QProcess not supported. Can't run: " << command;
+#endif
+}
+
+void PagerDemodGUI::sendToMap(const QString& address, const QString& message, float latitude, float longitude, QDateTime dateTime)
+{
+ QList mapPipes;
+ MainCore::instance()->getMessagePipes().getMessagePipes(m_pagerDemod, "mapitems", mapPipes);
+
+ for (const auto& pipe : mapPipes)
+ {
+ MessageQueue *messageQueue = qobject_cast(pipe->m_element);
+ SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem();
+ swgMapItem->setName(new QString(address));
+ swgMapItem->setLatitude(latitude);
+ swgMapItem->setLongitude(longitude);
+ swgMapItem->setAltitude(0);
+ swgMapItem->setAltitudeReference(1); // CLAMP_TO_GROUND
+ swgMapItem->setFixedPosition(false);
+ swgMapItem->setPositionDateTime(new QString(dateTime.toString(Qt::ISODateWithMs)));
+
+ swgMapItem->setImageRotation(0);
+ swgMapItem->setText(new QString(message));
+ swgMapItem->setImage(new QString(QString("pager.png")));
+
+ MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_pagerDemod, swgMapItem);
+ messageQueue->push(msg);
+ }
+
+ m_mapItems.insert(address);
+}
+
+// Clear all items from map
+void PagerDemodGUI::clearFromMap()
+{
+ for (const auto& address : m_mapItems)
+ {
+ QList mapPipes;
+ MainCore::instance()->getMessagePipes().getMessagePipes(m_pagerDemod, "mapitems", mapPipes);
+
+ for (const auto& pipe : mapPipes)
+ {
+ MessageQueue *messageQueue = qobject_cast(pipe->m_element);
+ SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem();
+ swgMapItem->setName(new QString(address));
+ swgMapItem->setImage(new QString(QString("")));
+
+ MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_pagerDemod, swgMapItem);
+ messageQueue->push(msg);
+ }
+ }
+
+ m_mapItems.clear();
+}
diff --git a/plugins/channelrx/demodpager/pagerdemodgui.h b/plugins/channelrx/demodpager/pagerdemodgui.h
index 90611b15e..4531c8c60 100644
--- a/plugins/channelrx/demodpager/pagerdemodgui.h
+++ b/plugins/channelrx/demodpager/pagerdemodgui.h
@@ -20,6 +20,7 @@
#define INCLUDE_PAGERDEMODGUI_H
#include
+#include
#include "channel/channelgui.h"
#include "dsp/channelmarker.h"
@@ -45,6 +46,18 @@ class PagerDemodGUI : public ChannelGUI {
Q_OBJECT
public:
+ enum MessageCol {
+ MESSAGE_COL_DATE,
+ MESSAGE_COL_TIME,
+ MESSAGE_COL_ADDRESS,
+ MESSAGE_COL_MESSAGE,
+ MESSAGE_COL_FUNCTION,
+ MESSAGE_COL_ALPHA,
+ MESSAGE_COL_NUMERIC,
+ MESSAGE_COL_EVEN_PE,
+ MESSAGE_COL_BCH_PE
+ };
+
static PagerDemodGUI* create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel);
virtual void destroy();
@@ -86,12 +99,18 @@ private:
QMenu *messagesMenu; // Column select context menu
+#ifdef QT_TEXTTOSPEECH_FOUND
+ QTextToSpeech *m_speech;
+#endif
+ QSet m_mapItems;
+
explicit PagerDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent = 0);
virtual ~PagerDemodGUI();
void blockApplySettings(bool block);
void applySettings(bool force = false);
void displaySettings();
+ QString selectMessage(int functionBits, const QString &numericMessage, const QString &alphaMessage) const;
void messageReceived(const QDateTime dateTime, int address, int functionBits,
const QString &numericMessage, const QString &alphaMessage,
int evenParityErrors, int bchParityErrors);
@@ -105,17 +124,13 @@ private:
void resizeTable();
QAction *createCheckableItem(QString& text, int idx, bool checked, const char *slot);
- enum MessageCol {
- MESSAGE_COL_DATE,
- MESSAGE_COL_TIME,
- MESSAGE_COL_ADDRESS,
- MESSAGE_COL_MESSAGE,
- MESSAGE_COL_FUNCTION,
- MESSAGE_COL_ALPHA,
- MESSAGE_COL_NUMERIC,
- MESSAGE_COL_EVEN_PE,
- MESSAGE_COL_BCH_PE
- };
+ void enableSpeechIfNeeded();
+ void checkNotification(int row);
+ void speechNotification(const QString &speech);
+ void commandNotification(const QString &command);
+ QString subStrings(const QString& address, const QString& message, const QRegularExpressionMatch& match, const QString &string) const;
+ void sendToMap(const QString& address, const QString& message, float latitide, float longitude, QDateTime dateTime);
+ void clearFromMap();
private slots:
void on_deltaFrequency_changed(qint64 value);
@@ -131,6 +146,9 @@ private slots:
void on_udpPort_editingFinished();
void on_channel1_currentIndexChanged(int index);
void on_channel2_currentIndexChanged(int index);
+ void on_notifications_clicked();
+ void on_filterDuplicates_clicked(bool checked=false);
+ void on_filterDuplicates_rightClicked(const QPoint &);
void on_logEnable_clicked(bool checked=false);
void on_logFilename_clicked();
void on_logOpen_clicked();
diff --git a/plugins/channelrx/demodpager/pagerdemodgui.ui b/plugins/channelrx/demodpager/pagerdemodgui.ui
index 240b42678..d66602c81 100644
--- a/plugins/channelrx/demodpager/pagerdemodgui.ui
+++ b/plugins/channelrx/demodpager/pagerdemodgui.ui
@@ -29,7 +29,7 @@
- Qt::StrongFocus
+ Qt::FocusPolicy::StrongFocus
Pager Demodulator
@@ -110,7 +110,7 @@
PointingHandCursor
- Qt::StrongFocus
+ Qt::FocusPolicy::StrongFocus
Demod shift frequency from center in Hz
@@ -127,14 +127,14 @@
-
- Qt::Vertical
+ Qt::Orientation::Vertical
-
- Qt::Horizontal
+ Qt::Orientation::Horizontal
@@ -152,7 +152,7 @@
Channel power
- Qt::RightToLeft
+ Qt::LayoutDirection::RightToLeft
0.0
@@ -209,7 +209,7 @@
-
- Qt::Horizontal
+ Qt::Orientation::Horizontal
@@ -252,7 +252,7 @@
100
- Qt::Horizontal
+ Qt::Orientation::Horizontal
@@ -268,14 +268,14 @@
10.0k
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+ Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
-
- Qt::Vertical
+ Qt::Orientation::Vertical
@@ -316,7 +316,7 @@
24
- Qt::Horizontal
+ Qt::Orientation::Horizontal
@@ -332,7 +332,7 @@
2.4k
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+ Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
@@ -368,7 +368,7 @@
-
- Qt::Vertical
+ Qt::Orientation::Vertical
@@ -413,7 +413,7 @@
-
- Qt::Vertical
+ Qt::Orientation::Vertical
@@ -479,7 +479,7 @@
-
- Qt::Horizontal
+ Qt::Orientation::Horizontal
@@ -494,22 +494,29 @@
-
- Qt::Horizontal
+ Qt::Orientation::Horizontal
-
+
-
+
+
+ UDP
+
+
+
-
Forward messages via UDP
- Qt::RightToLeft
+ Qt::LayoutDirection::RightToLeft
- UDP
+
@@ -522,7 +529,7 @@
- Qt::ClickFocus
+ Qt::FocusPolicy::ClickFocus
Destination UDP address
@@ -541,7 +548,7 @@
:
- Qt::AlignCenter
+ Qt::AlignmentFlag::AlignCenter
@@ -560,7 +567,7 @@
- Qt::ClickFocus
+ Qt::FocusPolicy::ClickFocus
Destination UDP port
@@ -576,7 +583,7 @@
-
- Qt::Horizontal
+ Qt::Orientation::Horizontal
@@ -591,7 +598,7 @@
-
- Qt::Horizontal
+ Qt::Orientation::Horizontal
@@ -614,7 +621,7 @@
-
- Qt::Horizontal
+ Qt::Orientation::Horizontal
@@ -624,6 +631,43 @@
+ -
+
+
+
+ 24
+ 16777215
+
+
+
+ Filter duplicate messages. Right click for options.
+
+
+
+
+
+
+ :/icons/filterduplicate.png:/icons/filterduplicate.png
+
+
+
+ -
+
+
+ Open notifications dialog
+
+
+ ...
+
+
+
+ :/mono.png:/mono.png
+
+
+ false
+
+
+
-
@@ -736,7 +780,7 @@
Received messages
- QAbstractItemView::NoEditTriggers
+ QAbstractItemView::EditTrigger::NoEditTriggers
@@ -1056,6 +1100,7 @@
+
diff --git a/plugins/channelrx/demodpager/pagerdemodicons.qrc b/plugins/channelrx/demodpager/pagerdemodicons.qrc
new file mode 100644
index 000000000..f59155cdd
--- /dev/null
+++ b/plugins/channelrx/demodpager/pagerdemodicons.qrc
@@ -0,0 +1,5 @@
+
+
+ icons/filterduplicate.png
+
+
diff --git a/plugins/channelrx/demodpager/pagerdemodnotificationdialog.cpp b/plugins/channelrx/demodpager/pagerdemodnotificationdialog.cpp
new file mode 100644
index 000000000..b7979ac2c
--- /dev/null
+++ b/plugins/channelrx/demodpager/pagerdemodnotificationdialog.cpp
@@ -0,0 +1,173 @@
+///////////////////////////////////////////////////////////////////////////////////
+// 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
+#include
+#include
+#include
+#include
+
+#include "gui/tablecolorchooser.h"
+
+#include "pagerdemodnotificationdialog.h"
+#include "pagerdemodgui.h"
+
+// Map main table column numbers to combo box indices
+std::vector PagerDemodNotificationDialog::m_columnMap = {
+ PagerDemodGUI::MESSAGE_COL_ADDRESS, PagerDemodGUI::MESSAGE_COL_MESSAGE
+};
+
+PagerDemodNotificationDialog::PagerDemodNotificationDialog(PagerDemodSettings *settings,
+ QWidget* parent) :
+ QDialog(parent),
+ ui(new Ui::PagerDemodNotificationDialog),
+ m_settings(settings)
+{
+ ui->setupUi(this);
+
+ resizeTable();
+
+ for (int i = 0; i < m_settings->m_notificationSettings.size(); i++) {
+ addRow(m_settings->m_notificationSettings[i]);
+ }
+}
+
+PagerDemodNotificationDialog::~PagerDemodNotificationDialog()
+{
+ delete ui;
+ qDeleteAll(m_colorGUIs);
+}
+
+void PagerDemodNotificationDialog::accept()
+{
+ qDeleteAll(m_settings->m_notificationSettings);
+ m_settings->m_notificationSettings.clear();
+ for (int i = 0; i < ui->table->rowCount(); i++)
+ {
+ PagerDemodSettings::NotificationSettings *notificationSettings = new PagerDemodSettings::NotificationSettings();
+ int idx = ((QComboBox *)ui->table->cellWidget(i, NOTIFICATION_COL_MATCH))->currentIndex();
+ notificationSettings->m_matchColumn = m_columnMap[idx];
+ notificationSettings->m_regExp = ui->table->item(i, NOTIFICATION_COL_REG_EXP)->data(Qt::DisplayRole).toString().trimmed();
+ notificationSettings->m_speech = ui->table->item(i, NOTIFICATION_COL_SPEECH)->data(Qt::DisplayRole).toString().trimmed();
+ notificationSettings->m_command = ui->table->item(i, NOTIFICATION_COL_COMMAND)->data(Qt::DisplayRole).toString().trimmed();
+ notificationSettings->m_highlight = !m_colorGUIs[i]->m_noColor;
+ notificationSettings->m_highlightColor = m_colorGUIs[i]->m_color;
+ notificationSettings->m_plotOnMap = ((QCheckBox *) ui->table->cellWidget(i, NOTIFICATION_COL_PLOT_ON_MAP))->isChecked();
+ notificationSettings->updateRegularExpression();
+ m_settings->m_notificationSettings.append(notificationSettings);
+ }
+ QDialog::accept();
+}
+
+void PagerDemodNotificationDialog::resizeTable()
+{
+ PagerDemodSettings::NotificationSettings dummy;
+ dummy.m_matchColumn = PagerDemodGUI::MESSAGE_COL_ADDRESS;
+ dummy.m_regExp = "1234567";
+ dummy.m_speech = "${message}";
+ dummy.m_command = "cmail.exe -to:user@host.com \"-subject: Paging ${address}\" \"-body: ${message}\"";
+ dummy.m_highlight = true;
+ dummy.m_plotOnMap = true;
+ addRow(&dummy);
+ ui->table->resizeColumnsToContents();
+ ui->table->selectRow(0);
+ on_remove_clicked();
+ ui->table->selectRow(-1);
+}
+
+void PagerDemodNotificationDialog::on_add_clicked()
+{
+ addRow();
+}
+
+// Remove selected row
+void PagerDemodNotificationDialog::on_remove_clicked()
+{
+ // Selection mode is single, so only a single row should be returned
+ QModelIndexList indexList = ui->table->selectionModel()->selectedRows();
+ if (!indexList.isEmpty())
+ {
+ int row = indexList.at(0).row();
+ ui->table->removeRow(row);
+ m_colorGUIs.removeAt(row);
+ }
+}
+
+void PagerDemodNotificationDialog::addRow(PagerDemodSettings::NotificationSettings *settings)
+{
+ int row = ui->table->rowCount();
+ ui->table->setSortingEnabled(false);
+ ui->table->setRowCount(row + 1);
+
+ QComboBox *match = new QComboBox();
+ TableColorChooser *highlight;
+ if (settings) {
+ highlight = new TableColorChooser(ui->table, row, NOTIFICATION_COL_HIGHLIGHT, !settings->m_highlight, settings->m_highlightColor);
+ } else {
+ highlight = new TableColorChooser(ui->table, row, NOTIFICATION_COL_HIGHLIGHT, false, QColor(Qt::red).rgba());
+ }
+ m_colorGUIs.append(highlight);
+ QCheckBox *plotOnMap = new QCheckBox();
+ plotOnMap->setChecked(false);
+ QWidget *matchWidget = new QWidget();
+ QHBoxLayout *pLayout = new QHBoxLayout(matchWidget);
+ pLayout->addWidget(match);
+ pLayout->setAlignment(Qt::AlignCenter);
+ pLayout->setContentsMargins(0, 0, 0, 0);
+ matchWidget->setLayout(pLayout);
+
+ match->addItem("Address");
+ match->addItem("Message");
+
+ QTableWidgetItem *regExpItem = new QTableWidgetItem();
+ QTableWidgetItem *speechItem = new QTableWidgetItem();
+ QTableWidgetItem *commandItem = new QTableWidgetItem();
+
+ if (settings != nullptr)
+ {
+ for (unsigned int i = 0; i < m_columnMap.size(); i++)
+ {
+ if (m_columnMap[i] == settings->m_matchColumn)
+ {
+ match->setCurrentIndex(i);
+ break;
+ }
+ }
+ regExpItem->setData(Qt::DisplayRole, settings->m_regExp);
+ speechItem->setData(Qt::DisplayRole, settings->m_speech);
+ commandItem->setData(Qt::DisplayRole, settings->m_command);
+ plotOnMap->setChecked(settings->m_plotOnMap);
+ }
+ else
+ {
+ match->setCurrentIndex(0);
+ regExpItem->setData(Qt::DisplayRole, ".*");
+ speechItem->setData(Qt::DisplayRole, "${message}");
+#ifdef _MSC_VER
+ commandItem->setData(Qt::DisplayRole, "cmail.exe -to:user@host.com \"-subject: Paging ${address}\" \"-body: ${message}\"");
+#else
+ commandItem->setData(Qt::DisplayRole, "sendmail -s \"Paging ${address}: ${message}\" user@host.com");
+#endif
+ }
+
+ ui->table->setCellWidget(row, NOTIFICATION_COL_MATCH, match);
+ ui->table->setItem(row, NOTIFICATION_COL_REG_EXP, regExpItem);
+ ui->table->setItem(row, NOTIFICATION_COL_SPEECH, speechItem);
+ ui->table->setItem(row, NOTIFICATION_COL_COMMAND, commandItem);
+ ui->table->setCellWidget(row, NOTIFICATION_COL_PLOT_ON_MAP, plotOnMap);
+ ui->table->setSortingEnabled(true);
+}
diff --git a/plugins/channelrx/demodpager/pagerdemodnotificationdialog.h b/plugins/channelrx/demodpager/pagerdemodnotificationdialog.h
new file mode 100644
index 000000000..a62ec954d
--- /dev/null
+++ b/plugins/channelrx/demodpager/pagerdemodnotificationdialog.h
@@ -0,0 +1,61 @@
+///////////////////////////////////////////////////////////////////////////////////
+// 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_PAGERDEMODNOTIFICATIONDIALOG_H
+#define INCLUDE_PAGERDEMODNOTIFICATIONDIALOG_H
+
+#include
+
+#include "ui_pagerdemodnotificationdialog.h"
+#include "pagerdemodsettings.h"
+
+class TableColorChooser;
+
+class PagerDemodNotificationDialog : public QDialog {
+ Q_OBJECT
+
+public:
+ explicit PagerDemodNotificationDialog(PagerDemodSettings* settings, QWidget* parent = 0);
+ ~PagerDemodNotificationDialog();
+
+private:
+ void resizeTable();
+
+private slots:
+ void accept() override;
+ void on_add_clicked();
+ void on_remove_clicked();
+ void addRow(PagerDemodSettings::NotificationSettings *settings=nullptr);
+
+private:
+ Ui::PagerDemodNotificationDialog* ui;
+ PagerDemodSettings *m_settings;
+ QList m_colorGUIs;
+
+ enum NotificationCol {
+ NOTIFICATION_COL_MATCH,
+ NOTIFICATION_COL_REG_EXP,
+ NOTIFICATION_COL_SPEECH,
+ NOTIFICATION_COL_COMMAND,
+ NOTIFICATION_COL_HIGHLIGHT,
+ NOTIFICATION_COL_PLOT_ON_MAP
+ };
+
+ static std::vector m_columnMap;
+};
+
+#endif // INCLUDE_PagerDEMODNOTIFICATIONDIALOG_H
diff --git a/plugins/channelrx/demodpager/pagerdemodnotificationdialog.ui b/plugins/channelrx/demodpager/pagerdemodnotificationdialog.ui
new file mode 100644
index 000000000..5ceded4f7
--- /dev/null
+++ b/plugins/channelrx/demodpager/pagerdemodnotificationdialog.ui
@@ -0,0 +1,175 @@
+
+
+ PagerDemodNotificationDialog
+
+
+
+ 0
+ 0
+ 1100
+ 400
+
+
+
+
+ Liberation Sans
+ 9
+
+
+
+ Qt::ContextMenuPolicy::PreventContextMenu
+
+
+ Notifications
+
+
+
-
+
+
+
-
+
+
+ QAbstractItemView::SelectionMode::SingleSelection
+
+
+ QAbstractItemView::SelectionBehavior::SelectRows
+
+
+
+ Match
+
+
+ ADS-B data to match
+
+
+
+
+ Reg Exp
+
+
+ Regular expression to match with
+
+
+
+
+ Speech
+
+
+ Speech for the computer to read when a match is made
+
+
+
+
+ Command
+
+
+ Command/script to execute when a match is made
+
+
+
+
+ Highlight
+
+
+
+
+ Plot on Map
+
+
+
+
+ -
+
+
-
+
+
+ Add device set control
+
+
+ +
+
+
+
+ -
+
+
+ Remove device set control
+
+
+ -
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+ QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok
+
+
+
+
+
+
+ table
+ add
+ remove
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ PagerDemodNotificationDialog
+ accept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ PagerDemodNotificationDialog
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+
diff --git a/plugins/channelrx/demodpager/pagerdemodsettings.cpp b/plugins/channelrx/demodpager/pagerdemodsettings.cpp
index df737ba90..ab4cdb93d 100644
--- a/plugins/channelrx/demodpager/pagerdemodsettings.cpp
+++ b/plugins/channelrx/demodpager/pagerdemodsettings.cpp
@@ -20,10 +20,12 @@
#include
#include
+#include
#include "util/simpleserializer.h"
#include "settings/serializable.h"
#include "pagerdemodsettings.h"
+#include "pagerdemodgui.h"
PagerDemodSettings::PagerDemodSettings() :
m_channelMarker(nullptr),
@@ -59,6 +61,9 @@ void PagerDemodSettings::resetToDefaults()
m_reverse = false;
m_workspaceIndex = 0;
m_hidden = false;
+ m_filterDuplicates = false;
+ m_duplicateMatchMessageOnly = false;
+ m_duplicateMatchLastOnly = false;
for (int i = 0; i < PAGERDEMOD_MESSAGE_COLUMNS; i++)
{
@@ -110,6 +115,11 @@ QByteArray PagerDemodSettings::serialize() const
s.writeBlob(29, m_geometryBytes);
s.writeBool(30, m_hidden);
+ s.writeList(31, m_notificationSettings);
+ s.writeBool(32, m_filterDuplicates);
+ s.writeBool(33, m_duplicateMatchMessageOnly);
+ s.writeBool(34, m_duplicateMatchLastOnly);
+
for (int i = 0; i < PAGERDEMOD_MESSAGE_COLUMNS; i++) {
s.writeS32(100 + i, m_messageColumnIndexes[i]);
}
@@ -205,6 +215,12 @@ bool PagerDemodSettings::deserialize(const QByteArray& data)
d.readBlob(29, &m_geometryBytes);
d.readBool(30, &m_hidden, false);
+ d.readList(31, &m_notificationSettings);
+
+ d.readBool(32, &m_filterDuplicates);
+ d.readBool(33, &m_duplicateMatchMessageOnly);
+ d.readBool(34, &m_duplicateMatchLastOnly);
+
for (int i = 0; i < PAGERDEMOD_MESSAGE_COLUMNS; i++) {
d.readS32(100 + i, &m_messageColumnIndexes[i], i);
}
@@ -237,3 +253,80 @@ void PagerDemodSettings::deserializeIntList(const QByteArray& data, QList> ints;
delete stream;
}
+
+PagerDemodSettings::NotificationSettings::NotificationSettings() :
+ m_matchColumn(PagerDemodGUI::MESSAGE_COL_ADDRESS),
+ m_highlight(false),
+ m_highlightColor(Qt::red),
+ m_plotOnMap(false)
+{
+}
+
+void PagerDemodSettings::NotificationSettings::updateRegularExpression()
+{
+ m_regularExpression.setPattern(m_regExp);
+ m_regularExpression.optimize();
+ if (!m_regularExpression.isValid()) {
+ qDebug() << "PagerDemodSettings::NotificationSettings: Regular expression is not valid: " << m_regExp;
+ }
+}
+
+QByteArray PagerDemodSettings::NotificationSettings::serialize() const
+{
+ SimpleSerializer s(1);
+
+ s.writeS32(1, m_matchColumn);
+ s.writeString(2, m_regExp);
+ s.writeString(3, m_speech);
+ s.writeString(4, m_command);
+ s.writeBool(5, m_highlight);
+ s.writeS32(6, m_highlightColor);
+ s.writeBool(7, m_plotOnMap);
+
+ return s.final();
+}
+
+bool PagerDemodSettings::NotificationSettings::deserialize(const QByteArray& data)
+{
+ SimpleDeserializer d(data);
+
+ if (!d.isValid()) {
+ return false;
+ }
+
+ if (d.getVersion() == 1)
+ {
+ QByteArray blob;
+
+ d.readS32(1, &m_matchColumn);
+ d.readString(2, &m_regExp);
+ d.readString(3, &m_speech);
+ d.readString(4, &m_command);
+ d.readBool(5, &m_highlight, false);
+ d.readS32(6, &m_highlightColor, QColor(Qt::red).rgba());
+ d.readBool(7, &m_plotOnMap, false);
+
+ updateRegularExpression();
+
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+}
+
+QDataStream& operator<<(QDataStream& out, const PagerDemodSettings::NotificationSettings *settings)
+{
+ out << settings->serialize();
+ return out;
+}
+
+QDataStream& operator>>(QDataStream& in, PagerDemodSettings::NotificationSettings*& settings)
+{
+ settings = new PagerDemodSettings::NotificationSettings();
+ QByteArray data;
+ in >> data;
+ settings->deserialize(data);
+ return in;
+}
diff --git a/plugins/channelrx/demodpager/pagerdemodsettings.h b/plugins/channelrx/demodpager/pagerdemodsettings.h
index 6d1380de2..6a223ea5d 100644
--- a/plugins/channelrx/demodpager/pagerdemodsettings.h
+++ b/plugins/channelrx/demodpager/pagerdemodsettings.h
@@ -23,6 +23,7 @@
#include
#include
+#include
#include "dsp/dsptypes.h"
@@ -33,6 +34,23 @@ class Serializable;
struct PagerDemodSettings
{
+ struct NotificationSettings {
+ int m_matchColumn;
+ QString m_regExp;
+ QString m_speech;
+ QString m_command;
+ bool m_highlight;
+ qint32 m_highlightColor;
+ bool m_plotOnMap;
+
+ QRegularExpression m_regularExpression;
+
+ NotificationSettings();
+ void updateRegularExpression();
+ QByteArray serialize() const;
+ bool deserialize(const QByteArray& data);
+ };
+
qint32 m_baud; //!< 512, 1200 or 2400
qint32 m_inputFrequencyOffset;
Real m_rfBandwidth;
@@ -73,6 +91,12 @@ struct PagerDemodSettings
QByteArray m_geometryBytes;
bool m_hidden;
+ QList m_notificationSettings;
+
+ bool m_filterDuplicates;
+ bool m_duplicateMatchMessageOnly;
+ bool m_duplicateMatchLastOnly;
+
int m_messageColumnIndexes[PAGERDEMOD_MESSAGE_COLUMNS];//!< How the columns are ordered in the table
int m_messageColumnSizes[PAGERDEMOD_MESSAGE_COLUMNS]; //!< Size of the columns in the table
diff --git a/plugins/channelrx/demodpager/readme.md b/plugins/channelrx/demodpager/readme.md
index 26751f06f..b6e358c4c 100644
--- a/plugins/channelrx/demodpager/readme.md
+++ b/plugins/channelrx/demodpager/readme.md
@@ -1,4 +1,4 @@
-Pager demodulator plugin
+Pager demodulator plugin
Introduction
@@ -86,6 +86,36 @@ IP address of the host to forward received messages to via UDP.
UDP port number to forward received messages to.
+Filter Duplicates
+
+Check to filter (discard) duplicate messages. Right click to show the Duplicate Filter options dialog:
+
+- Match message only: When unchecked, compare address and message. When checked, compare only message, ignoring the address.
+- Match last message only: When unchecked the message is compared against all messages in the table. When checked, the message is compared against the last received message only.
+
+Open Notifications Dialog
+
+When clicked, opens the Notifications Dialog, which allows speech notifications or programs/scripts to be run when messages matching user-defined rules are received.
+
+By running a program such as [cmail](https://www.inveigle.net/cmail/download) on Windows or sendmail on Linux, e-mail notifications can be sent.
+
+Messages can be highlighted in a user-defined colour.
+
+By checking plot on map, if a message contains a position specified as latitude and longitude, the message can be displayed on the [Map](../../feature/map/readme.md) feature.
+The format of the coordinates should follow https://en.wikipedia.org/wiki/ISO_6709, E.g: 50°40′46″N 95°48′26″W or -23.342,5.234
+
+Here are a few examples:
+
+![Notifications Dialog](../../../doc/img/PagerDemod_plugin_notifications.png)
+
+In the Speech and Command strings, variables can be used to substitute data from the received message:
+
+* ${address},
+* ${message},
+* ${1}, ${2}... are replaced with the string from the corresponding capture group in the regular expression.
+
+To experiment with regular expressions, try [https://regexr.com/](https://regexr.com/).
+
15: Start/stop Logging Messages to .csv File
When checked, writes all received messages to a .csv file.
diff --git a/plugins/feature/map/map.qrc b/plugins/feature/map/map.qrc
index 7a0eeaa05..6cd815444 100644
--- a/plugins/feature/map/map.qrc
+++ b/plugins/feature/map/map.qrc
@@ -25,6 +25,7 @@
map/airport_small.png
map/heliport.png
map/waypoint.png
+ map/pager.png
map/map3d.html
data/transmitters.csv
diff --git a/plugins/feature/map/map/pager.png b/plugins/feature/map/map/pager.png
new file mode 100644
index 000000000..ec63d645e
Binary files /dev/null and b/plugins/feature/map/map/pager.png differ
diff --git a/plugins/feature/map/mapsettings.cpp b/plugins/feature/map/mapsettings.cpp
index 6c1f4a8ab..96aeecd7a 100644
--- a/plugins/feature/map/mapsettings.cpp
+++ b/plugins/feature/map/mapsettings.cpp
@@ -38,6 +38,7 @@ const QStringList MapSettings::m_pipeTypes = {
QStringLiteral("FT8Demod"),
QStringLiteral("HeatMap"),
QStringLiteral("ILSDemod"),
+ QStringLiteral("PagerDemod"),
QStringLiteral("Radiosonde"),
QStringLiteral("StarTracker"),
QStringLiteral("SatelliteTracker"),
@@ -55,6 +56,7 @@ const QStringList MapSettings::m_pipeURIs = {
QStringLiteral("sdrangel.channel.ft8demod"),
QStringLiteral("sdrangel.channel.heatmap"),
QStringLiteral("sdrangel.channel.ilsdemod"),
+ QStringLiteral("sdrangel.channel.pagerdemod"),
QStringLiteral("sdrangel.feature.radiosonde"),
QStringLiteral("sdrangel.feature.startracker"),
QStringLiteral("sdrangel.feature.satellitetracker"),
@@ -96,6 +98,7 @@ MapSettings::MapSettings() :
m_itemSettings.insert("StarTracker", new MapItemSettings("StarTracker", true, QColor(230, 230, 230), true, true, 3));
m_itemSettings.insert("SatelliteTracker", new MapItemSettings("SatelliteTracker", true, QColor(0, 0, 255), true, false, 0, modelMinPixelSize));
m_itemSettings.insert("Beacons", new MapItemSettings("Beacons", true, QColor(255, 0, 0), false, true, 8));
+ m_itemSettings.insert("PagerDemod", new MapItemSettings("PagerDemod", true, QColor(200, 191, 231), true, false, 11));
m_itemSettings.insert("Radiosonde", new MapItemSettings("Radiosonde", true, QColor(102, 0, 102), true, false, 11, modelMinPixelSize));
m_itemSettings.insert("Radio Time Transmitters", new MapItemSettings("Radio Time Transmitters", true, QColor(255, 0, 0), false, true, 8));
m_itemSettings.insert("Radar", new MapItemSettings("Radar", true, QColor(255, 0, 0), false, true, 8));
diff --git a/plugins/feature/map/readme.md b/plugins/feature/map/readme.md
index c542c5275..1c53ac54b 100644
--- a/plugins/feature/map/readme.md
+++ b/plugins/feature/map/readme.md
@@ -16,7 +16,8 @@ On top of this, it can plot data from other plugins, such as:
* Radials and estimated position from the VOR localizer feature,
* ILS course line and glide path from the ILS Demodulator,
* DSC geographic call areas,
-* SID paths.
+* SID paths,
+* Pager messages that contain coordinates.
As well as internet and built-in data sources:
@@ -313,6 +314,7 @@ Mapbox: https://www.mapbox.com/ Cesium: https://www.cesium.com Bing: https://www
Ionosonde data and MUF/coF2 contours from [KC2G](https://prop.kc2g.com/) with source data from [GIRO](https://giro.uml.edu/) and [NOAA NCEI](https://www.ngdc.noaa.gov/stp/iono/ionohome.html).
+
Sea Marks are from OpenSeaMap: https://www.openseamap.org/
Railways are from OpenRailwayMap: https://www.openrailwaymap.org/
@@ -324,6 +326,7 @@ World icons created by turkkub from Flaticon: https://www.flaticon.com
Layers and Boat icons created by Freepik from Flaticon: https://www.flaticon.com
Railway icons created by Prosymbols Premium from Flaticon: https://www.flaticon.com
Satellite icons created by SyafriStudio from Flaticon: https://www.flaticon.com
+Pager icons created by xnimrodx from Flaticon: https://www.flaticon.com
3D models are by various artists under a variety of licenses. See: https://github.com/srcejon/sdrangel-3d-models
diff --git a/sdrbase/util/csv.cpp b/sdrbase/util/csv.cpp
index d83841791..7354e2a2d 100644
--- a/sdrbase/util/csv.cpp
+++ b/sdrbase/util/csv.cpp
@@ -167,3 +167,11 @@ QHash CSV::readHeader(QTextStream &in, QStringList requiredColumns
return colNumbers;
}
+
+QString CSV::escape(const QString& string)
+{
+ QString s = string;
+ s.replace('"', "\"\"");
+ s = QString("\"%1\"").arg(s);
+ return s;
+}
diff --git a/sdrbase/util/csv.h b/sdrbase/util/csv.h
index 6756636e1..34117edf5 100644
--- a/sdrbase/util/csv.h
+++ b/sdrbase/util/csv.h
@@ -52,6 +52,8 @@ struct SDRBASE_API CSV {
static bool readRow(QTextStream &in, QStringList *row, char seperator=',');
static QHash readHeader(QTextStream &in, QStringList requiredColumns, QString &error, char seperator=',');
+ static QString escape(const QString& string);
+
};
#endif /* INCLUDE_CSV_H */
diff --git a/sdrbase/util/units.h b/sdrbase/util/units.h
index edfe1f847..4296813b3 100644
--- a/sdrbase/util/units.h
+++ b/sdrbase/util/units.h
@@ -269,11 +269,15 @@ public:
// Try to convert a string to latitude and longitude. Returns false if not recognised format.
// https://en.wikipedia.org/wiki/ISO_6709 specifies a standard syntax
// We support both decimal and DMS formats
- static bool stringToLatitudeAndLongitude(const QString& string, float& latitude, float& longitude)
+ static bool stringToLatitudeAndLongitude(const QString& string, float& latitude, float& longitude, bool exact=true)
{
QRegularExpressionMatch match;
- QRegularExpression decimal(QRegularExpression::anchoredPattern("(-?[0-9]+(\\.[0-9]+)?) *,? *(-?[0-9]+(\\.[0-9]+)?)"));
+ QString decimalPattern = "(-?[0-9]+(\\.[0-9]+)?) *,? *(-?[0-9]+(\\.[0-9]+)?)";
+ if (exact) {
+ decimalPattern = QRegularExpression::anchoredPattern(decimalPattern);
+ }
+ QRegularExpression decimal(decimalPattern);
match = decimal.match(string);
if (match.hasMatch())
{
@@ -282,7 +286,11 @@ public:
return true;
}
- QRegularExpression dms(QRegularExpression::anchoredPattern(QString("([0-9]+)[%1d]([0-9]+)['m]([0-9]+(\\.[0-9]+)?)[\"s]([NS]) *,? *([0-9]+)[%1d]([0-9]+)['m]([0-9]+(\\.[0-9]+)?)[\"s]([EW])").arg(QChar(0xb0))));
+ QString dmsPattern = QString("([0-9]+)[%1d]([0-9]+)['m]([0-9]+(\\.[0-9]+)?)[\"s]([NS]) *,? *([0-9]+)[%1d]([0-9]+)['m]([0-9]+(\\.[0-9]+)?)[\"s]([EW])").arg(QChar(0xb0));
+ if (exact) {
+ dmsPattern = QRegularExpression::anchoredPattern(dmsPattern);
+ }
+ QRegularExpression dms(dmsPattern);
match = dms.match(string);
if (match.hasMatch())
{
@@ -303,7 +311,11 @@ public:
return true;
}
- QRegularExpression dms2(QRegularExpression::anchoredPattern(QString("([0-9]+)([NS])([0-9]{2})([0-9]{2}) *,?([0-9]+)([EW])([0-9]{2})([0-9]{2})")));
+ QString dms2Pattern = "([0-9]+)([NS])([0-9]{2})([0-9]{2}) *,?([0-9]+)([EW])([0-9]{2})([0-9]{2})";
+ if (exact) {
+ dms2Pattern = QRegularExpression::anchoredPattern(dms2Pattern);
+ }
+ QRegularExpression dms2(dms2Pattern);
match = dms2.match(string);
if (match.hasMatch())
{
@@ -325,7 +337,11 @@ public:
}
// 512255.5900N 0024400.6105W as used on aviation charts
- QRegularExpression dms3(QRegularExpression::anchoredPattern(QString("(\\d{2})(\\d{2})((\\d{2})(\\.\\d+)?)([NS]) *,?(\\d{3})(\\d{2})((\\d{2})(\\.\\d+)?)([EW])")));
+ QString dms3Pattern = "(\\d{2})(\\d{2})((\\d{2})(\\.\\d+)?)([NS]) *,?(\\d{3})(\\d{2})((\\d{2})(\\.\\d+)?)([EW])";
+ if (exact) {
+ dms3Pattern = QRegularExpression::anchoredPattern(dms3Pattern);
+ }
+ QRegularExpression dms3(dms3Pattern);
match = dms3.match(string);
if (match.hasMatch())
{