From 4455ac0c5561623f947cbfe38def196cfd99be25 Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Fri, 11 Oct 2024 11:01:43 +0100 Subject: [PATCH] Add pager notifications. Add option to ignore duplicates. Support plotting pager messages on the map. --- plugins/channelrx/demodpager/CMakeLists.txt | 9 + .../demodpager/icons/filterduplicate.png | Bin 0 -> 3262 bytes plugins/channelrx/demodpager/pagerdemod.cpp | 9 +- .../demodpager/pagerdemodbaseband.cpp | 4 +- .../demodpager/pagerdemodfilterdialog.cpp | 46 +++ .../demodpager/pagerdemodfilterdialog.h | 41 ++ .../demodpager/pagerdemodfilterdialog.ui | 118 ++++++ .../channelrx/demodpager/pagerdemodgui.cpp | 390 ++++++++++++++---- plugins/channelrx/demodpager/pagerdemodgui.h | 40 +- plugins/channelrx/demodpager/pagerdemodgui.ui | 93 +++-- .../channelrx/demodpager/pagerdemodicons.qrc | 5 + .../pagerdemodnotificationdialog.cpp | 173 ++++++++ .../demodpager/pagerdemodnotificationdialog.h | 61 +++ .../pagerdemodnotificationdialog.ui | 175 ++++++++ .../demodpager/pagerdemodsettings.cpp | 93 +++++ .../channelrx/demodpager/pagerdemodsettings.h | 24 ++ plugins/channelrx/demodpager/readme.md | 32 +- plugins/feature/map/map.qrc | 1 + plugins/feature/map/map/pager.png | Bin 0 -> 4607 bytes plugins/feature/map/mapsettings.cpp | 3 + plugins/feature/map/readme.md | 5 +- sdrbase/util/csv.cpp | 8 + sdrbase/util/csv.h | 2 + sdrbase/util/units.h | 26 +- 24 files changed, 1239 insertions(+), 119 deletions(-) create mode 100644 plugins/channelrx/demodpager/icons/filterduplicate.png create mode 100644 plugins/channelrx/demodpager/pagerdemodfilterdialog.cpp create mode 100644 plugins/channelrx/demodpager/pagerdemodfilterdialog.h create mode 100644 plugins/channelrx/demodpager/pagerdemodfilterdialog.ui create mode 100644 plugins/channelrx/demodpager/pagerdemodicons.qrc create mode 100644 plugins/channelrx/demodpager/pagerdemodnotificationdialog.cpp create mode 100644 plugins/channelrx/demodpager/pagerdemodnotificationdialog.h create mode 100644 plugins/channelrx/demodpager/pagerdemodnotificationdialog.ui create mode 100644 plugins/feature/map/map/pager.png 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 0000000000000000000000000000000000000000..9ea51d40c47a014c8061100f3a1cd1544f326fa6 GIT binary patch literal 3262 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ec63d645e1bde84fa3ee692e7abb4ed6e55fff1e GIT binary patch literal 4607 zcmVStO&>uS)ve<0AYj>5AR{$W90N^4L=L-RlQUJ&DC0@ZjPh;=*jPLSYvv5M~MFBAl0-BNIsH z15C~g000{K(ZT*WKal6<?_01!^k@7iDG<<3=fuAC~28EsPoqkpK{9G%|Vj005J}`Hw&=0RYXHq~ibpyyzHQsFW8>#s~laM4*8xut5h5 z!4#~(4xGUqyucR%VFpA%3?#rj5JCpzfE)^;7?wd9RKPme1hudO8lVxH;SjXJF*pt9 z;1XPc>u?taU>Kgl7`%oF1VP9M6Ja4bh!J9r*dopd7nzO(B4J20l7OTj>4+3jBE`sZ zqynizYLQ(?Bl0bB6giDtK>Co|$RIL`{EECsF_eL_Q3KQhbwIhO9~z3rpmWi5G!I>X zmZEFX8nhlgfVQHi(M#xcbO3#dj$?q)F%D*o*1Pf{>6$SWH+$s3q(pv=X`qR|$iJF~TPzlc-O$C3+J1 z#CT#lv5;6stS0Uu9wDA3UMCI{Uz12A4#|?_P6{CkNG+sOq(0IRX`DyT~9-sA|ffUF>wk++Z!kWZ5P$;0Hg6gtI-;!FvmBvPc55=u2?Kjj3apE5$3psG>L zsh-pbs)#zDT1jo7c2F-(3)vyY4>O^>2$gY-Gd%Qm(Z8e zYv>2*=jns=cMJ`N4THx>VkjAF8G9M07`GWOnM|ey)0dgZR4~^v8<}UA514ONSSt1^ zd=-((5|uiYR+WC0=c-gyb5%dpd8!Lkt5pxHURHgkMpd&=fR^vEcAI*_=wwAG2sV%zY%w@v@XU~7=xdm1xY6*0;iwVIXu6TaXrs|dqbIl~ z?uTdNHFy_3W~^@g_pF#!K2~{F^;XxcN!DEJEbDF7 zS8PxlSDOr*I-AS3sI8l=#CDr)-xT5$k15hA^;2%zG3@;83hbKf2JJcaVfH2VZT8O{ z%p4LO);n}Nd~$Sk%yw*Wyz8XlG{dRHsl(}4XB%gsbDi@w7p6;)%MzD%mlsoQr;4X; zpL)xc%+^yMd)ZNTI#eJ*$O)i@o$z8)e??LqN_gLa_%;TM>o2SC_ zkmoO6c3xRt`@J4dvz#WL)-Y|z+r(Soy~}%GIzByR`p)SCKE^%*pL(B%zNWq+-#xw~ ze%5}Oeh2)X`#bu}{g3#+;d$~F@lFL`0l@*~0lk45fwKc^10MvL1f>Tx1&sx}1}_Xg z6+#RN4Ot&@lW)Km@*DYMGu&q^n$Z=?2%QyL8~QNJCQKgI5srq>2;UHXZ>IT7>CCnW zh~P(Th`1kV8JQRPeH1AwGO8}>QM6NZadh`A)~w`N`)9q5@sFvDxjWlxwsLl7tZHmh zY-8-3xPZ8-xPf?w_(k!T5_A(J3GIpG#Ms0=iQ{tu=WLoYoaCBRmULsT<=mpV7v|~C z%bs^USv6UZd^m-e5|^?+<%1wXP%juy<)>~<9TW0|n}ttBzM_qyQL(qUN<5P0omQ3h zINdvaL;7fjPeygdGYL;pD|wL_lDQ-EO;$wK-mK5raoH_7l$?~Dqf!lNmb5F^Ft;eT zPi8AClMUo~=55LwlZVRpxOiFd;3B_8yA~shQx|tGF!j;$toK>JuS&gYLDkTP@C~gS@r~shUu{a>bfJ1` z^^VQ7&C1OKHDNXFTgC{M|V%fo{xK_dk6MK@9S!GZ*1JJzrV5xZBjOk z9!NTH<(q(S+MDf~ceQX@Dh|Ry<-sT4rhI$jQ0Sq~!`#Eo-%($2E^vo}is5J@NVEf|KK?WT&2;PCq@=ncR8z zO#GQ^T~S@VXG71PKNocFOt)Y6$@AXlk6rM*aP%VgV%sIRORYVwJx6|U{ozQjTW{-S z_si{9Jg#)~P3t?+@6&(!YQWWV*Z9{iU7vZq@5byKw{9lg9JnRA_4s!7?H6|n?o8ZW zdXIRo{Jz@#>IeD{>VLHUv1Pz*;P_y`V9&!@5AO~Mho1hF|I>%z(nrik)gwkDjgOrl z9~%uCz4Bzvli{bbrxVZ0epdf^>vOB;-~HnIOV3#R*zgPai_gEVd8zYq@2jb=I>#f& zAH2?aJ@Kaet0;l(KJP3O8BCZ_<;n2CQTqj1f@u%Y+t7B zc4v3zb!X@8J}-Wl?MUrv7BS^~x_Rz%@AEt7-19%@ip^8gIBV}Q#(YC*JuZX*@A*hf z2)y@LV-}RwPb;M!*Us5HJ^W;BB9D-QPP>f|@+0B%-VupLk%+|NLh&(ctq_yB+~Qa+ zpBYLdIPljuF~*Qer_oyDojWUFLQ1SLR4Nq!wq1M)opy^8g~EzfN+*?4y!FN#eBq0? zaOYiLwLU9=#u$c^2`;_tQl8(pkG*^LFn#W3bP#AQgy?iS?Z7(2&O2_W zxzgmx-A@rjB19vRvjW)dhIBg}o_OjUcHVJ2zkc>vTFn)N5TORV5Q0o5%Pad|;ipeN z&Bu3~;D#GEQ8Kl49};@chKweYQ-tB#MW(c%+xE=OhByaO^w!fl;N?Hg@%69X&HX?4 z4jZ#sR^Qi-RSd_2ax0!2lX-*QZ3r+w@6#I`fdFj~(!zqOjXYOk*JF=6S z&+kYRLC{^r$f6nD9_6*?T9g)RXsrp20iX})oFftmhz>+pD6}}93t3#r1G4*sRX z{@*m&uu&n_AgnA{wrr2^r5#CP^zH`;8dy%e+a(Azkw}1hUvd85s-WXNsjT9{%c3NP z6>BM#_n-s#;kOnDT`%(0vgMY$lVm3~2z$meeLx`u)>yoE42>vaNMd@8JpBjTfTCEi z?Eh_((VSj0#X^ELV7k5!T%0p}dS`-}F%1~3b*DxDvbvBhl zbh!SuI5*xF#{lbb&94Ha(Y)}dMHc4j2;ok9R`)#dRI3%Py!r~fgYVz`AbYngf>Ix- z>q!YZvi_)_GS{}uzE@(`&Tn${wV&iy&pgfGQ2bwjQYuy&E6lz(%f0vCgAfrGi^~i| z&yEgux?QfnHpV^QzKfUt@G_NhnM5)PfS8)d*C(fE5-ZIW<_ZNS@_DkOqd+gRXTyXL zAOuSb3mhvH7@L?NF`T4uXm(k9pxf<`$!sL-cB$5C)Ef<)Jyq%+AtX{tJRU)B;&9Hw z>a|imS?N3U!Ltk=tg~dYqioEMQm@tUfH-&4RNXn30MJ?!L?RetD3?k=Pak8k7$d0^ zVYf>ViO_5|sa2~erLe}592p@#6sOba^x1@5b;^63?a7B!3V{GT&h?y>QX$pqKIg!D zOc>(5M=G_ft+kA#(+tK2K?sfl~gvNTpmsYmG66kyMJQ=^2u#6jDn5_U0RG-@cvC+;}68 zKm0I5iD8N-=lJYRH*wh~E~i$j@ywn*v|4SN%_i4fe;wOCb`iTDdWiG3UO+OHqEaex z^2G7w(}dcDA<=;t=WaQl>CNZSAOA&9imbH^#^YSL?IOJQ{lXAJAf&`tiw4T2GFM)8 z6+3RdmB)7P#yUrGB*ohY-r~^g?CM#DczlRrvB>#bFW}3&cJZS}AEni9F+#e}bVe`s z-XWz#DfMsbz7HeinT_`Sk`#$VsMqTpKXwdjEwNaPbUICOejZ~M0YM@$#KzH4jvhHo ztybgsv17>9!g{}H)({2vUq<~?pljM{5AAllB!-6>&*hL((r&kDEY}%M455Rd|BaOv z7je#!NF*4~A{rg&k@X@Oh{^F>b>rB0ax9l448sro%c)ii=O1ss zO{>);lgW^unxpB@#AzRLnIo-JI}gDm^Fjw34#b=7_zd`U?4is`<0ZEm6ZmaP6sI^Lqmz) 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()) {