From 4455ac0c5561623f947cbfe38def196cfd99be25 Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Fri, 11 Oct 2024 11:01:43 +0100 Subject: [PATCH 1/6] 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 | 346 +++++++++++++++--- 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, 1217 insertions(+), 97 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 2b1a99a1ae..b92fa7c8ed 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 811891d2c6..a460d32637 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 07dfbaa00d..e6b870d6e9 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 0000000000..91233851fd --- /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 0000000000..0784531b9a --- /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 0000000000..cf6c814404 --- /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 e8186da80d..19ccfa4d1b 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,69 +149,38 @@ bool PagerDemodGUI::deserialize(const QByteArray& data) } } -// 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 PagerDemodGUI::selectMessage(int functionBits, const QString &numericMessage, const QString &alphaMessage) const { - // Is scroll bar at bottom - QScrollBar *sb = ui->messages->verticalScrollBar(); - bool scrollToBottom = sb->value() == sb->maximum(); + QString message; - // Add to messages table - ui->messages->setSortingEnabled(false); - int row = ui->messages->rowCount(); - ui->messages->setRowCount(row + 1); - - QTableWidgetItem *dateItem = new QTableWidgetItem(); - QTableWidgetItem *timeItem = new QTableWidgetItem(); - QTableWidgetItem *addressItem = new QTableWidgetItem(); - QTableWidgetItem *messageItem = new QTableWidgetItem(); - QTableWidgetItem *functionItem = new QTableWidgetItem(); - QTableWidgetItem *alphaItem = new QTableWidgetItem(); - 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); - 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); + message = numericMessage; } else { - messageItem->setText(alphaMessage); + message = 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); + message = numericMessage; } else { - messageItem->setText(alphaMessage); + message = alphaMessage; } } else if (m_settings.m_decode == PagerDemodSettings::Numeric) { // Always display as numeric - messageItem->setText(numericMessage); + message = numericMessage; } else if (m_settings.m_decode == PagerDemodSettings::Alphanumeric) { // Always display as alphanumeric - messageItem->setText(alphaMessage); + message = alphaMessage; } else { @@ -221,7 +195,7 @@ void PagerDemodGUI::messageReceived(const QDateTime dateTime, int address, int f { if (iscntrl(alpha[i].toLatin1()) && !isspace(alpha[i].toLatin1())) { - messageItem->setText(numeric); + message = numeric; done = true; break; } @@ -232,14 +206,75 @@ void PagerDemodGUI::messageReceived(const QDateTime dateTime, int address, int f if (numeric.size() > 15) { done = true; - messageItem->setText(alpha); + message = alpha; } } if (!done) { // Default to alpha - messageItem->setText(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(); + + // Add to messages table + ui->messages->setSortingEnabled(false); + int row = ui->messages->rowCount(); + ui->messages->setRowCount(row + 1); + + QTableWidgetItem *dateItem = new QTableWidgetItem(); + QTableWidgetItem *timeItem = new QTableWidgetItem(); + QTableWidgetItem *addressItem = new QTableWidgetItem(); + QTableWidgetItem *messageItem = new QTableWidgetItem(); + QTableWidgetItem *functionItem = new QTableWidgetItem(); + QTableWidgetItem *alphaItem = new QTableWidgetItem(); + 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); + dateItem->setText(dateTime.date().toString()); + timeItem->setText(dateTime.time().toString()); + 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); + 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 90611b15e6..4531c8c602 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 @@ public slots: 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 @@ public slots: 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 240b42678f..d66602c818 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 0000000000..f59155cdd6 --- /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 0000000000..b7979ac2c1 --- /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 0000000000..a62ec954d0 --- /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 0000000000..5ceded4f7c --- /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 df737ba904..ab4cdb93d8 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 6d1380de29..6a223ea5d5 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 26751f06f6..b6e358c4c3 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 7a0eeaa05b..6cd8154442 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 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 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 6756636e12..34117edf53 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 edfe1f847a..4296813b35 100644 --- a/sdrbase/util/units.h +++ b/sdrbase/util/units.h @@ -269,11 +269,15 @@ class SDRBASE_API Units // 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 @@ class SDRBASE_API Units 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 @@ class SDRBASE_API Units 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 @@ class SDRBASE_API Units } // 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()) { From 944803a276ebfeaa395bb58f77946ba472ae6915 Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Fri, 11 Oct 2024 11:15:34 +0100 Subject: [PATCH 2/6] Fix server compilation. Update docs. --- doc/img/PagerDemod_plugin.png | Bin 13962 -> 15729 bytes doc/img/PagerDemod_plugin_notifications.png | Bin 0 -> 33136 bytes .../channelrx/demodpager/pagerdemodgui.cpp | 56 +++++++++--------- plugins/channelrx/demodpager/pagerdemodgui.h | 11 ---- .../pagerdemodnotificationdialog.cpp | 4 +- .../demodpager/pagerdemodsettings.cpp | 3 +- .../channelrx/demodpager/pagerdemodsettings.h | 12 ++++ 7 files changed, 43 insertions(+), 43 deletions(-) create mode 100644 doc/img/PagerDemod_plugin_notifications.png diff --git a/doc/img/PagerDemod_plugin.png b/doc/img/PagerDemod_plugin.png index 0f6bae41d2ee5d683cc0d7eeb65d5646b8899f05..e5af0073962da41b373b69c1c199a80c164b1ced 100644 GIT binary patch literal 15729 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 MDJ97YapUj*AFdf_IRF3v diff --git a/doc/img/PagerDemod_plugin_notifications.png b/doc/img/PagerDemod_plugin_notifications.png new file mode 100644 index 0000000000000000000000000000000000000000..23247a4d91d0d733dd2dd8a9e43e79020edaa165 GIT binary patch literal 33136 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 z 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 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); } @@ -233,8 +233,8 @@ void PagerDemodGUI::messageReceived(const QDateTime dateTime, int address, int f 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(); + 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))) { @@ -262,15 +262,15 @@ 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(addressString); @@ -434,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; } @@ -921,18 +921,18 @@ void PagerDemodGUI::enableSpeechIfNeeded() 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(); + 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 MESSAGE_COL_ADDRESS: + case PagerDemodSettings::MESSAGE_COL_ADDRESS: match = address; break; - case MESSAGE_COL_MESSAGE: + case PagerDemodSettings::MESSAGE_COL_MESSAGE: match = message; break; } @@ -944,7 +944,7 @@ void PagerDemodGUI::checkNotification(int row) 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); + ui->messages->item(row, PagerDemodSettings::MESSAGE_COL_MESSAGE)->setTextColor(m_settings.m_notificationSettings[i]->m_highlightColor); } if (!m_settings.m_notificationSettings[i]->m_speech.isEmpty()) @@ -968,8 +968,8 @@ void PagerDemodGUI::checkNotification(int row) { 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())); + 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); } diff --git a/plugins/channelrx/demodpager/pagerdemodgui.h b/plugins/channelrx/demodpager/pagerdemodgui.h index 4531c8c602..346f9fcaeb 100644 --- a/plugins/channelrx/demodpager/pagerdemodgui.h +++ b/plugins/channelrx/demodpager/pagerdemodgui.h @@ -46,17 +46,6 @@ 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(); diff --git a/plugins/channelrx/demodpager/pagerdemodnotificationdialog.cpp b/plugins/channelrx/demodpager/pagerdemodnotificationdialog.cpp index b7979ac2c1..5c26f4a755 100644 --- a/plugins/channelrx/demodpager/pagerdemodnotificationdialog.cpp +++ b/plugins/channelrx/demodpager/pagerdemodnotificationdialog.cpp @@ -28,7 +28,7 @@ // Map main table column numbers to combo box indices std::vector PagerDemodNotificationDialog::m_columnMap = { - PagerDemodGUI::MESSAGE_COL_ADDRESS, PagerDemodGUI::MESSAGE_COL_MESSAGE + PagerDemodSettings::MESSAGE_COL_ADDRESS, PagerDemodSettings::MESSAGE_COL_MESSAGE }; PagerDemodNotificationDialog::PagerDemodNotificationDialog(PagerDemodSettings *settings, @@ -76,7 +76,7 @@ void PagerDemodNotificationDialog::accept() void PagerDemodNotificationDialog::resizeTable() { PagerDemodSettings::NotificationSettings dummy; - dummy.m_matchColumn = PagerDemodGUI::MESSAGE_COL_ADDRESS; + 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}\""; diff --git a/plugins/channelrx/demodpager/pagerdemodsettings.cpp b/plugins/channelrx/demodpager/pagerdemodsettings.cpp index ab4cdb93d8..e329a56b8a 100644 --- a/plugins/channelrx/demodpager/pagerdemodsettings.cpp +++ b/plugins/channelrx/demodpager/pagerdemodsettings.cpp @@ -25,7 +25,6 @@ #include "util/simpleserializer.h" #include "settings/serializable.h" #include "pagerdemodsettings.h" -#include "pagerdemodgui.h" PagerDemodSettings::PagerDemodSettings() : m_channelMarker(nullptr), @@ -255,7 +254,7 @@ void PagerDemodSettings::deserializeIntList(const QByteArray& data, QList Date: Fri, 11 Oct 2024 11:19:59 +0100 Subject: [PATCH 3/6] Fix links in docs. --- plugins/channelrx/demodpager/readme.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/plugins/channelrx/demodpager/readme.md b/plugins/channelrx/demodpager/readme.md index b6e358c4c3..15a8e586d8 100644 --- a/plugins/channelrx/demodpager/readme.md +++ b/plugins/channelrx/demodpager/readme.md @@ -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,23 +86,23 @@ IP address of the host to forward received messages to via UDP. UDP port number to forward received messages to. -

Filter Duplicates

+

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. -

Open Notifications Dialog

+

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. +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. +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 https://en.wikipedia.org/wiki/ISO_6709, E.g: 50°40′46″N 95°48′26″W or -23.342,5.234 +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: @@ -116,15 +116,15 @@ In the Speech and Command strings, variables can be used to substitute data from To experiment with regular expressions, try [https://regexr.com/](https://regexr.com/). -

15: Start/stop Logging Messages to .csv File

+

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. From c4003533f4638953629d8d32d6f12da1b1011fd7 Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Fri, 11 Oct 2024 12:26:35 +0100 Subject: [PATCH 4/6] Lint fit --- plugins/channelrx/demodpager/pagerdemodfilterdialog.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/channelrx/demodpager/pagerdemodfilterdialog.h b/plugins/channelrx/demodpager/pagerdemodfilterdialog.h index 0784531b9a..01088403a5 100644 --- a/plugins/channelrx/demodpager/pagerdemodfilterdialog.h +++ b/plugins/channelrx/demodpager/pagerdemodfilterdialog.h @@ -27,7 +27,7 @@ class PagerDemodFilterDialog : public QDialog { Q_OBJECT public: - explicit PagerDemodFilterDialog(PagerDemodSettings* settings, QWidget* parent = 0); + explicit PagerDemodFilterDialog(PagerDemodSettings* settings, QWidget* parent = nullptr); ~PagerDemodFilterDialog(); private slots: From 558730eb741048af877afa63958e041343962b6f Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Fri, 11 Oct 2024 15:23:16 +0100 Subject: [PATCH 5/6] Fix Qt6 compilation --- plugins/channelrx/demodpager/pagerdemodgui.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/channelrx/demodpager/pagerdemodgui.cpp b/plugins/channelrx/demodpager/pagerdemodgui.cpp index 8d44cd2e6a..7d05b1a28d 100644 --- a/plugins/channelrx/demodpager/pagerdemodgui.cpp +++ b/plugins/channelrx/demodpager/pagerdemodgui.cpp @@ -944,7 +944,7 @@ void PagerDemodGUI::checkNotification(int row) if (matchResult.hasMatch()) { if (m_settings.m_notificationSettings[i]->m_highlight) { - ui->messages->item(row, PagerDemodSettings::MESSAGE_COL_MESSAGE)->setTextColor(m_settings.m_notificationSettings[i]->m_highlightColor); + 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()) From 91be77909dd811a048736b2ccba5b905e31ed6f8 Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Fri, 11 Oct 2024 15:23:39 +0100 Subject: [PATCH 6/6] Add default-qt6-windows cmake config. --- CMakePresets.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CMakePresets.json b/CMakePresets.json index 01dc1c0ad2..cb453a9057 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" } ] }