diff --git a/CMakePresets.json b/CMakePresets.json index 01dc1c0ad..cb453a905 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -80,6 +80,15 @@ "cacheVariables": { "ENABLE_QT6": "ON" } + }, + { + "name": "default-qt6-windows", + "inherits": "default-windows", + "binaryDir": "${sourceDir}/build-qt6", + "cacheVariables": { + "ENABLE_QT6": "ON", + "CMAKE_PREFIX_PATH": "C:/Qt/6.7.3/msvc2022_64;C:/Applications/boost_1_81_0" + } } ], "buildPresets": [ @@ -94,6 +103,10 @@ { "name": "default-qt6", "configurePreset": "default-qt6" + }, + { + "name": "default-qt6-windows", + "configurePreset": "default-qt6-windows" } ] } diff --git a/doc/img/PagerDemod_plugin.png b/doc/img/PagerDemod_plugin.png index 0f6bae41d..e5af00739 100644 Binary files a/doc/img/PagerDemod_plugin.png and b/doc/img/PagerDemod_plugin.png differ diff --git a/doc/img/PagerDemod_plugin_notifications.png b/doc/img/PagerDemod_plugin_notifications.png new file mode 100644 index 000000000..23247a4d9 Binary files /dev/null and b/doc/img/PagerDemod_plugin_notifications.png differ 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..01088403a --- /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 = nullptr); + ~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..7d05b1a28 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,15 +55,15 @@ 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_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")); - ui->messages->setItem(row, MESSAGE_COL_FUNCTION, new QTableWidgetItem("0")); - ui->messages->setItem(row, MESSAGE_COL_ALPHA, new QTableWidgetItem("ABCEDGHIJKLMNOPQRSTUVWXYZABCEDGHIJKLMNOPQRSTUVWXYZ")); - ui->messages->setItem(row, MESSAGE_COL_NUMERIC, new QTableWidgetItem("123456789123456789123456789123456789123456789123456789")); - ui->messages->setItem(row, MESSAGE_COL_EVEN_PE, new QTableWidgetItem("0")); - ui->messages->setItem(row, MESSAGE_COL_BCH_PE, new QTableWidgetItem("0")); + ui->messages->setItem(row, PagerDemodSettings::MESSAGE_COL_DATE, new QTableWidgetItem("Fri Apr 15 2016--")); + ui->messages->setItem(row, PagerDemodSettings::MESSAGE_COL_TIME, new QTableWidgetItem("10:17:00")); + ui->messages->setItem(row, PagerDemodSettings::MESSAGE_COL_ADDRESS, new QTableWidgetItem("1000000")); + ui->messages->setItem(row, PagerDemodSettings::MESSAGE_COL_MESSAGE, new QTableWidgetItem("ABCEDGHIJKLMNOPQRSTUVWXYZABCEDGHIJKLMNOPQRSTUVWXYZ")); + ui->messages->setItem(row, PagerDemodSettings::MESSAGE_COL_FUNCTION, new QTableWidgetItem("0")); + ui->messages->setItem(row, PagerDemodSettings::MESSAGE_COL_ALPHA, new QTableWidgetItem("ABCEDGHIJKLMNOPQRSTUVWXYZABCEDGHIJKLMNOPQRSTUVWXYZ")); + ui->messages->setItem(row, PagerDemodSettings::MESSAGE_COL_NUMERIC, new QTableWidgetItem("123456789123456789123456789123456789123456789123456789")); + ui->messages->setItem(row, PagerDemodSettings::MESSAGE_COL_EVEN_PE, new QTableWidgetItem("0")); + ui->messages->setItem(row, PagerDemodSettings::MESSAGE_COL_BCH_PE, new QTableWidgetItem("0")); ui->messages->resizeColumnsToContents(); ui->messages->removeRow(row); } @@ -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, PagerDemodSettings::MESSAGE_COL_ADDRESS)->text(); + QString prevMessage = ui->messages->item(row, PagerDemodSettings::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(); @@ -167,79 +262,19 @@ void PagerDemodGUI::messageReceived(const QDateTime dateTime, int address, int f QTableWidgetItem *numericItem = new QTableWidgetItem(); QTableWidgetItem *evenPEItem = new QTableWidgetItem(); QTableWidgetItem *bchPEItem = new QTableWidgetItem(); - ui->messages->setItem(row, MESSAGE_COL_DATE, dateItem); - ui->messages->setItem(row, MESSAGE_COL_TIME, timeItem); - ui->messages->setItem(row, MESSAGE_COL_ADDRESS, addressItem); - ui->messages->setItem(row, MESSAGE_COL_MESSAGE, messageItem); - ui->messages->setItem(row, MESSAGE_COL_FUNCTION, functionItem); - ui->messages->setItem(row, MESSAGE_COL_ALPHA, alphaItem); - ui->messages->setItem(row, MESSAGE_COL_NUMERIC, numericItem); - ui->messages->setItem(row, MESSAGE_COL_EVEN_PE, evenPEItem); - ui->messages->setItem(row, MESSAGE_COL_BCH_PE, bchPEItem); + ui->messages->setItem(row, PagerDemodSettings::MESSAGE_COL_DATE, dateItem); + ui->messages->setItem(row, PagerDemodSettings::MESSAGE_COL_TIME, timeItem); + ui->messages->setItem(row, PagerDemodSettings::MESSAGE_COL_ADDRESS, addressItem); + ui->messages->setItem(row, PagerDemodSettings::MESSAGE_COL_MESSAGE, messageItem); + ui->messages->setItem(row, PagerDemodSettings::MESSAGE_COL_FUNCTION, functionItem); + ui->messages->setItem(row, PagerDemodSettings::MESSAGE_COL_ALPHA, alphaItem); + ui->messages->setItem(row, PagerDemodSettings::MESSAGE_COL_NUMERIC, numericItem); + ui->messages->setItem(row, PagerDemodSettings::MESSAGE_COL_EVEN_PE, evenPEItem); + ui->messages->setItem(row, PagerDemodSettings::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); @@ -398,7 +434,7 @@ void PagerDemodGUI::filterRow(int row) if (m_settings.m_filterAddress != "") { QRegExp re(m_settings.m_filterAddress); - QTableWidgetItem *fromItem = ui->messages->item(row, MESSAGE_COL_ADDRESS); + QTableWidgetItem *fromItem = ui->messages->item(row, PagerDemodSettings::MESSAGE_COL_ADDRESS); if (!re.exactMatch(fromItem->text())) { hidden = true; } @@ -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, PagerDemodSettings::MESSAGE_COL_ADDRESS)->text(); + QString message = ui->messages->item(row, PagerDemodSettings::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 PagerDemodSettings::MESSAGE_COL_ADDRESS: + match = address; + break; + case PagerDemodSettings::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, PagerDemodSettings::MESSAGE_COL_MESSAGE)->setForeground(QBrush(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, PagerDemodSettings::MESSAGE_COL_DATE)->text())); + dateTime.setTime(QTime::fromString(ui->messages->item(row, PagerDemodSettings::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..346f9fcae 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,7 @@ class PagerDemodGUI : public ChannelGUI { Q_OBJECT public: + static PagerDemodGUI* create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel); virtual void destroy(); @@ -86,12 +88,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 +113,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 +135,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..5c26f4a75 --- /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 = { + PagerDemodSettings::MESSAGE_COL_ADDRESS, PagerDemodSettings::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 = PagerDemodSettings::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..e329a56b8 100644 --- a/plugins/channelrx/demodpager/pagerdemodsettings.cpp +++ b/plugins/channelrx/demodpager/pagerdemodsettings.cpp @@ -20,6 +20,7 @@ #include #include +#include #include "util/simpleserializer.h" #include "settings/serializable.h" @@ -59,6 +60,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 +114,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 +214,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 +252,80 @@ void PagerDemodSettings::deserializeIntList(const QByteArray& data, QList> ints; delete stream; } + +PagerDemodSettings::NotificationSettings::NotificationSettings() : + m_matchColumn(PagerDemodSettings::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..6f7a91b90 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,35 @@ class Serializable; struct PagerDemodSettings { + 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 + }; + + 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 +103,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..15a8e586d 100644 --- a/plugins/channelrx/demodpager/readme.md +++ b/plugins/channelrx/demodpager/readme.md @@ -1,4 +1,4 @@ -

Pager demodulator plugin

+

Pager demodulator plugin

Introduction

@@ -38,7 +38,7 @@ Specifies the pager modulation. Currently only POCSAG is supported. POCSAG uses FSK with 4.5kHz frequency shift, at 512, 1200 or 2400 baud. High frequency is typically 0, with low 1, but occasionally this appears to be reversed, so the demodulator supports either. -Data is framed as specified in ITU-R M.584-2: https://www.itu.int/dms_pubrec/itu-r/rec/m/R-REC-M.584-2-199711-I!!PDF-E.pdf +Data is framed as specified in [ITU-R M.584-2](https://www.itu.int/dms_pubrec/itu-r/rec/m/R-REC-M.584-2-199711-I!!PDF-E.pdf)

7: Baud

@@ -86,15 +86,45 @@ IP address of the host to forward received messages to via UDP. UDP port number to forward received messages to. -

15: Start/stop Logging Messages to .csv File

+

15: 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. + +

16: 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 containing the received message. + +Messages can be highlighted in a user-defined colour, selected in the Highlight column. + +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 [ISO 6709](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/). + +

17: Start/stop Logging Messages to .csv File

When checked, writes all received messages to a .csv file. -

16: .csv Log Filename

+

18: .csv Log Filename

Click to specify the name of the .csv file which received messages are logged to. -

17: Read Data from .csv File

+

19: Read Data from .csv File

Click to specify a previously written .csv log file, which is read and used to update the table. 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()) {