diff --git a/plugins/channelrx/demodpager/CMakeLists.txt b/plugins/channelrx/demodpager/CMakeLists.txt index 2b1a99a1a..b92fa7c8e 100644 --- a/plugins/channelrx/demodpager/CMakeLists.txt +++ b/plugins/channelrx/demodpager/CMakeLists.txt @@ -29,16 +29,25 @@ if(NOT SERVER_MODE) pagerdemodgui.ui pagerdemodcharsetdialog.cpp pagerdemodcharsetdialog.ui + pagerdemodnotificationdialog.cpp + pagerdemodnotificationdialog.ui + pagerdemodfilterdialog.cpp + pagerdemodfilterdialog.ui + pagerdemodicons.qrc ) set(demodpager_HEADERS ${demodpager_HEADERS} pagerdemodgui.h pagerdemodcharsetdialog.h + pagerdemodnotificationdialog.h ) set(TARGET_NAME ${PLUGINS_PREFIX}demodpager) set(TARGET_LIB "Qt::Widgets") set(TARGET_LIB_GUI "sdrgui") + if(Qt${QT_DEFAULT_MAJOR_VERSION}TextToSpeech_FOUND) + list(APPEND TARGET_LIB_GUI Qt::TextToSpeech) + endif() set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR}) else() set(TARGET_NAME ${PLUGINSSRV_PREFIX}demodpagersrv) diff --git a/plugins/channelrx/demodpager/icons/filterduplicate.png b/plugins/channelrx/demodpager/icons/filterduplicate.png new file mode 100644 index 000000000..9ea51d40c Binary files /dev/null and b/plugins/channelrx/demodpager/icons/filterduplicate.png differ diff --git a/plugins/channelrx/demodpager/pagerdemod.cpp b/plugins/channelrx/demodpager/pagerdemod.cpp index 811891d2c..a460d3263 100644 --- a/plugins/channelrx/demodpager/pagerdemod.cpp +++ b/plugins/channelrx/demodpager/pagerdemod.cpp @@ -31,6 +31,7 @@ #include "dsp/dspcommands.h" #include "device/deviceapi.h" #include "util/db.h" +#include "util/csv.h" #include "maincore.h" MESSAGE_CLASS_DEFINITION(PagerDemod::MsgConfigurePagerDemod, Message) @@ -141,7 +142,7 @@ bool PagerDemod::handleMessage(const Message& cmd) { if (MsgConfigurePagerDemod::match(cmd)) { - MsgConfigurePagerDemod& cfg = (MsgConfigurePagerDemod&) cmd; + const MsgConfigurePagerDemod& cfg = (const MsgConfigurePagerDemod&) cmd; qDebug() << "PagerDemod::handleMessage: MsgConfigurePagerDemod"; applySettings(cfg.getSettings(), cfg.getForce()); @@ -149,7 +150,7 @@ bool PagerDemod::handleMessage(const Message& cmd) } else if (DSPSignalNotification::match(cmd)) { - DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + const DSPSignalNotification& notif = (const DSPSignalNotification&) cmd; m_basebandSampleRate = notif.getSampleRate(); m_centerFrequency = notif.getCenterFrequency(); // Forward to the sink @@ -166,7 +167,7 @@ bool PagerDemod::handleMessage(const Message& cmd) else if (MsgPagerMessage::match(cmd)) { // Forward to GUI - MsgPagerMessage& report = (MsgPagerMessage&)cmd; + const MsgPagerMessage& report = (const MsgPagerMessage&)cmd; if (getMessageQueueToGUI()) { MsgPagerMessage *msg = new MsgPagerMessage(report); @@ -200,7 +201,7 @@ bool PagerDemod::handleMessage(const Message& cmd) << report.getDateTime().time().toString() << "," << QString("%1").arg(report.getAddress(), 7, 10, QChar('0')) << "," << QString::number(report.getFunctionBits()) << "," - << "\"" << report.getAlphaMessage() << "\"," + << CSV::escape(report.getAlphaMessage()) << "," << report.getNumericMessage() << "," << QString::number(report.getEvenParityErrors()) << "," << QString::number(report.getBCHParityErrors()) << "\n"; diff --git a/plugins/channelrx/demodpager/pagerdemodbaseband.cpp b/plugins/channelrx/demodpager/pagerdemodbaseband.cpp index 07dfbaa00..e6b870d6e 100644 --- a/plugins/channelrx/demodpager/pagerdemodbaseband.cpp +++ b/plugins/channelrx/demodpager/pagerdemodbaseband.cpp @@ -131,7 +131,7 @@ bool PagerDemodBaseband::handleMessage(const Message& cmd) if (MsgConfigurePagerDemodBaseband::match(cmd)) { QMutexLocker mutexLocker(&m_mutex); - MsgConfigurePagerDemodBaseband& cfg = (MsgConfigurePagerDemodBaseband&) cmd; + const MsgConfigurePagerDemodBaseband& cfg = (const MsgConfigurePagerDemodBaseband&) cmd; qDebug() << "PagerDemodBaseband::handleMessage: MsgConfigurePagerDemodBaseband"; applySettings(cfg.getSettings(), cfg.getForce()); @@ -141,7 +141,7 @@ bool PagerDemodBaseband::handleMessage(const Message& cmd) else if (DSPSignalNotification::match(cmd)) { QMutexLocker mutexLocker(&m_mutex); - DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + const DSPSignalNotification& notif = (const DSPSignalNotification&) cmd; qDebug() << "PagerDemodBaseband::handleMessage: DSPSignalNotification: basebandSampleRate: " << notif.getSampleRate(); setBasebandSampleRate(notif.getSampleRate()); m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(notif.getSampleRate())); diff --git a/plugins/channelrx/demodpager/pagerdemodfilterdialog.cpp b/plugins/channelrx/demodpager/pagerdemodfilterdialog.cpp new file mode 100644 index 000000000..91233851f --- /dev/null +++ b/plugins/channelrx/demodpager/pagerdemodfilterdialog.cpp @@ -0,0 +1,46 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include + +#include "pagerdemodfilterdialog.h" + +PagerDemodFilterDialog::PagerDemodFilterDialog(PagerDemodSettings *settings, + QWidget* parent) : + QDialog(parent), + ui(new Ui::PagerDemodFilterDialog), + m_settings(settings) +{ + ui->setupUi(this); + + ui->matchLastOnly->setChecked(m_settings->m_duplicateMatchLastOnly); + ui->matchMessageOnly->setChecked(m_settings->m_duplicateMatchMessageOnly); +} + +PagerDemodFilterDialog::~PagerDemodFilterDialog() +{ + delete ui; +} + +void PagerDemodFilterDialog::accept() +{ + m_settings->m_duplicateMatchLastOnly = ui->matchLastOnly->isChecked(); + m_settings->m_duplicateMatchMessageOnly = ui->matchMessageOnly->isChecked(); + + QDialog::accept(); +} diff --git a/plugins/channelrx/demodpager/pagerdemodfilterdialog.h b/plugins/channelrx/demodpager/pagerdemodfilterdialog.h new file mode 100644 index 000000000..0784531b9 --- /dev/null +++ b/plugins/channelrx/demodpager/pagerdemodfilterdialog.h @@ -0,0 +1,41 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_PAGERDEMODFILTERDIALOG_H +#define INCLUDE_PAGERDEMODFILTERDIALOG_H + + +#include "ui_pagerdemodfilterdialog.h" +#include "pagerdemodsettings.h" + + +class PagerDemodFilterDialog : public QDialog { + Q_OBJECT + +public: + explicit PagerDemodFilterDialog(PagerDemodSettings* settings, QWidget* parent = 0); + ~PagerDemodFilterDialog(); + +private slots: + void accept() override; + +private: + Ui::PagerDemodFilterDialog* ui; + PagerDemodSettings *m_settings; +}; + +#endif // INCLUDE_PAGERDEMODFILTERDIALOG_H diff --git a/plugins/channelrx/demodpager/pagerdemodfilterdialog.ui b/plugins/channelrx/demodpager/pagerdemodfilterdialog.ui new file mode 100644 index 000000000..cf6c81440 --- /dev/null +++ b/plugins/channelrx/demodpager/pagerdemodfilterdialog.ui @@ -0,0 +1,118 @@ + + + PagerDemodFilterDialog + + + + 0 + 0 + 396 + 167 + + + + + Liberation Sans + 9 + + + + Qt::ContextMenuPolicy::PreventContextMenu + + + Duplicate Filtering + + + + + + Duplicate Filtering + + + + + + Match message only + + + + + + + Whether both the address and message must match or only the message to be considered a duplicate + + + + + + + + + + Match last message only + + + + + + + Whether to match with only the last message or any message in the table + + + + + + + + + + + + + Qt::Orientation::Horizontal + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok + + + + + + + + + + + buttonBox + accepted() + PagerDemodFilterDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + PagerDemodFilterDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/plugins/channelrx/demodpager/pagerdemodgui.cpp b/plugins/channelrx/demodpager/pagerdemodgui.cpp index e8186da80..19ccfa4d1 100644 --- a/plugins/channelrx/demodpager/pagerdemodgui.cpp +++ b/plugins/channelrx/demodpager/pagerdemodgui.cpp @@ -16,8 +16,6 @@ // along with this program. If not, see . // /////////////////////////////////////////////////////////////////////////////////// -#include -#include #include #include #include @@ -26,6 +24,7 @@ #include #include #include +#include #include "pagerdemodgui.h" @@ -36,6 +35,8 @@ #include "plugin/pluginapi.h" #include "util/db.h" #include "util/csv.h" +#include "util/units.h" +#include "gui/crightclickenabler.h" #include "gui/basicchannelsettingsdialog.h" #include "dsp/dspengine.h" #include "gui/dialogpositioner.h" @@ -43,6 +44,10 @@ #include "pagerdemod.h" #include "pagerdemodcharsetdialog.h" +#include "pagerdemodnotificationdialog.h" +#include "pagerdemodfilterdialog.h" + +#include "SWGMapItem.h" void PagerDemodGUI::resizeTable() { @@ -50,7 +55,7 @@ void PagerDemodGUI::resizeTable() // Trailing spaces are for sort arrow int row = ui->messages->rowCount(); ui->messages->setRowCount(row + 1); - ui->messages->setItem(row, MESSAGE_COL_DATE, new QTableWidgetItem("Fri Apr 15 2016-")); + ui->messages->setItem(row, MESSAGE_COL_DATE, new QTableWidgetItem("Fri Apr 15 2016--")); ui->messages->setItem(row, MESSAGE_COL_TIME, new QTableWidgetItem("10:17:00")); ui->messages->setItem(row, MESSAGE_COL_ADDRESS, new QTableWidgetItem("1000000")); ui->messages->setItem(row, MESSAGE_COL_MESSAGE, new QTableWidgetItem("ABCEDGHIJKLMNOPQRSTUVWXYZABCEDGHIJKLMNOPQRSTUVWXYZ")); @@ -144,11 +149,101 @@ bool PagerDemodGUI::deserialize(const QByteArray& data) } } +QString PagerDemodGUI::selectMessage(int functionBits, const QString &numericMessage, const QString &alphaMessage) const +{ + QString message; + + // Standard way of choosing numeric or alpha decode isn't followed widely + if (m_settings.m_decode == PagerDemodSettings::Standard) + { + // Encoding is based on function bits + if (functionBits == 0) { + message = numericMessage; + } else { + message = alphaMessage; + } + } + else if (m_settings.m_decode == PagerDemodSettings::Inverted) + { + // Encoding is based on function bits, but inverted from standard + if (functionBits == 3) { + message = numericMessage; + } else { + message = alphaMessage; + } + } + else if (m_settings.m_decode == PagerDemodSettings::Numeric) + { + // Always display as numeric + message = numericMessage; + } + else if (m_settings.m_decode == PagerDemodSettings::Alphanumeric) + { + // Always display as alphanumeric + message = alphaMessage; + } + else + { + // Guess at what the encoding is + QString numeric = numericMessage; + QString alpha = alphaMessage; + bool done = false; + if (!done) + { + // If alpha contains control characters, possibly numeric + for (int i = 0; i < alpha.size(); i++) + { + if (iscntrl(alpha[i].toLatin1()) && !isspace(alpha[i].toLatin1())) + { + message = numeric; + done = true; + break; + } + } + } + if (!done) { + // Possibly not likely to get only longer than 15 digits + if (numeric.size() > 15) + { + done = true; + message = alpha; + } + } + if (!done) { + // Default to alpha + message = alpha; + } + } + + return message; + +} + // Add row to table void PagerDemodGUI::messageReceived(const QDateTime dateTime, int address, int functionBits, const QString &numericMessage, const QString &alphaMessage, int evenParityErrors, int bchParityErrors) { + QString message = selectMessage(functionBits, numericMessage, alphaMessage); + QString addressString = QString("%1").arg(address, 7, 10, QChar('0')); + + // Should we ignore the message if it is a duplicate? + if (m_settings.m_filterDuplicates && (ui->messages->rowCount() > 0)) + { + int startRow = m_settings.m_duplicateMatchLastOnly ? ui->messages->rowCount() - 1 : 0; + for (int row = startRow; row < ui->messages->rowCount(); row++) + { + QString prevAddress = ui->messages->item(row, MESSAGE_COL_ADDRESS)->text(); + QString prevMessage = ui->messages->item(row, MESSAGE_COL_MESSAGE)->text(); + + if ((message == prevMessage) && (m_settings.m_duplicateMatchMessageOnly || (addressString == prevAddress))) + { + // Ignore this message + return; + } + } + } + // Is scroll bar at bottom QScrollBar *sb = ui->messages->verticalScrollBar(); bool scrollToBottom = sb->value() == sb->maximum(); @@ -178,68 +273,8 @@ void PagerDemodGUI::messageReceived(const QDateTime dateTime, int address, int f ui->messages->setItem(row, MESSAGE_COL_BCH_PE, bchPEItem); dateItem->setText(dateTime.date().toString()); timeItem->setText(dateTime.time().toString()); - addressItem->setText(QString("%1").arg(address, 7, 10, QChar('0'))); - // Standard way of choosing numeric or alpha decode isn't followed widely - if (m_settings.m_decode == PagerDemodSettings::Standard) - { - // Encoding is based on function bits - if (functionBits == 0) { - messageItem->setText(numericMessage); - } else { - messageItem->setText(alphaMessage); - } - } - else if (m_settings.m_decode == PagerDemodSettings::Inverted) - { - // Encoding is based on function bits, but inverted from standard - if (functionBits == 3) { - messageItem->setText(numericMessage); - } else { - messageItem->setText(alphaMessage); - } - } - else if (m_settings.m_decode == PagerDemodSettings::Numeric) - { - // Always display as numeric - messageItem->setText(numericMessage); - } - else if (m_settings.m_decode == PagerDemodSettings::Alphanumeric) - { - // Always display as alphanumeric - messageItem->setText(alphaMessage); - } - else - { - // Guess at what the encoding is - QString numeric = numericMessage; - QString alpha = alphaMessage; - bool done = false; - if (!done) - { - // If alpha contains control characters, possibly numeric - for (int i = 0; i < alpha.size(); i++) - { - if (iscntrl(alpha[i].toLatin1()) && !isspace(alpha[i].toLatin1())) - { - messageItem->setText(numeric); - done = true; - break; - } - } - } - if (!done) { - // Possibly not likely to get only longer than 15 digits - if (numeric.size() > 15) - { - done = true; - messageItem->setText(alpha); - } - } - if (!done) { - // Default to alpha - messageItem->setText(alpha); - } - } + addressItem->setText(addressString); + messageItem->setText(message); functionItem->setText(QString("%1").arg(functionBits)); alphaItem->setText(alphaMessage); numericItem->setText(numericMessage); @@ -250,6 +285,7 @@ void PagerDemodGUI::messageReceived(const QDateTime dateTime, int address, int f if (scrollToBottom) { ui->messages->scrollToBottom(); } + checkNotification(row); } bool PagerDemodGUI::handleMessage(const Message& message) @@ -257,7 +293,7 @@ bool PagerDemodGUI::handleMessage(const Message& message) if (PagerDemod::MsgConfigurePagerDemod::match(message)) { qDebug("PagerDemodGUI::handleMessage: PagerDemod::MsgConfigurePagerDemod"); - const PagerDemod::MsgConfigurePagerDemod& cfg = (PagerDemod::MsgConfigurePagerDemod&) message; + const PagerDemod::MsgConfigurePagerDemod& cfg = (const PagerDemod::MsgConfigurePagerDemod&) message; m_settings = cfg.getSettings(); blockApplySettings(true); ui->scopeGUI->updateSettings(); @@ -268,7 +304,7 @@ bool PagerDemodGUI::handleMessage(const Message& message) } else if (PagerDemod::MsgPagerMessage::match(message)) { - PagerDemod::MsgPagerMessage& report = (PagerDemod::MsgPagerMessage&) message; + const PagerDemod::MsgPagerMessage& report = (const PagerDemod::MsgPagerMessage&) message; messageReceived(report.getDateTime(), report.getAddress(), report.getFunctionBits(), report.getNumericMessage(), report.getAlphaMessage(), report.getEvenParityErrors(), report.getBCHParityErrors()); @@ -276,7 +312,7 @@ bool PagerDemodGUI::handleMessage(const Message& message) } else if (DSPSignalNotification::match(message)) { - DSPSignalNotification& notif = (DSPSignalNotification&) message; + const DSPSignalNotification& notif = (const DSPSignalNotification&) message; m_deviceCenterFrequency = notif.getCenterFrequency(); m_basebandSampleRate = notif.getSampleRate(); ui->deltaFrequency->setValueRange(false, 7, -m_basebandSampleRate/2, m_basebandSampleRate/2); @@ -479,7 +515,8 @@ PagerDemodGUI::PagerDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Bas m_deviceCenterFrequency(0), m_basebandSampleRate(1), m_doApplySettings(true), - m_tickCount(0) + m_tickCount(0), + m_speech(nullptr) { setAttribute(Qt::WA_DeleteOnClose, true); m_helpURL = "plugins/channelrx/demodpager/readme.md"; @@ -526,6 +563,9 @@ PagerDemodGUI::PagerDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Bas connect(&m_channelMarker, SIGNAL(highlightedByCursor()), this, SLOT(channelMarkerHighlightedByCursor())); connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + CRightClickEnabler *filterDuplicatesRightClickEnabler = new CRightClickEnabler(ui->filterDuplicates); + connect(filterDuplicatesRightClickEnabler, SIGNAL(rightClick(const QPoint &)), this, SLOT(on_filterDuplicates_rightClicked(const QPoint &))); + // Resize the table using dummy data resizeTable(); // Allow user to reorder columns @@ -575,6 +615,7 @@ void PagerDemodGUI::customContextMenuRequested(QPoint pos) PagerDemodGUI::~PagerDemodGUI() { + clearFromMap(); delete ui; } @@ -638,6 +679,8 @@ void PagerDemodGUI::displaySettings() ui->logFilename->setToolTip(QString(".csv log filename: %1").arg(m_settings.m_logFilename)); ui->logEnable->setChecked(m_settings.m_logEnabled); + ui->filterDuplicates->setChecked(m_settings.m_filterDuplicates); + // Order and size columns QHeaderView *header = ui->messages->horizontalHeader(); @@ -656,6 +699,7 @@ void PagerDemodGUI::displaySettings() getRollupContents()->restoreState(m_rollupState); updateAbsoluteCenterFrequency(); blockApplySettings(false); + enableSpeechIfNeeded(); } void PagerDemodGUI::leaveEvent(QEvent* event) @@ -693,12 +737,39 @@ void PagerDemodGUI::tick() void PagerDemodGUI::on_charset_clicked() { PagerDemodCharsetDialog dialog(&m_settings); - if (dialog.exec() == QDialog::Accepted) - { + new DialogPositioner(&dialog, true); + if (dialog.exec() == QDialog::Accepted) { applySettings(); } } +void PagerDemodGUI::on_notifications_clicked() +{ + PagerDemodNotificationDialog dialog(&m_settings); + new DialogPositioner(&dialog, true); + if (dialog.exec() == QDialog::Accepted) + { + enableSpeechIfNeeded(); + applySettings(); + } +} + +void PagerDemodGUI::on_filterDuplicates_clicked(bool checked) +{ + m_settings.m_filterDuplicates = checked; + applySettings(); +} + +void PagerDemodGUI::on_filterDuplicates_rightClicked(const QPoint &p) +{ + (void) p; + + PagerDemodFilterDialog dialog(&m_settings); + new DialogPositioner(&dialog, true); + if (dialog.exec() == QDialog::Accepted) { + applySettings(); + } +} void PagerDemodGUI::on_logEnable_clicked(bool checked) { @@ -813,6 +884,8 @@ void PagerDemodGUI::makeUIConnections() QObject::connect(ui->udpEnabled, &QCheckBox::clicked, this, &PagerDemodGUI::on_udpEnabled_clicked); QObject::connect(ui->udpAddress, &QLineEdit::editingFinished, this, &PagerDemodGUI::on_udpAddress_editingFinished); QObject::connect(ui->udpPort, &QLineEdit::editingFinished, this, &PagerDemodGUI::on_udpPort_editingFinished); + QObject::connect(ui->notifications, &QToolButton::clicked, this, &PagerDemodGUI::on_notifications_clicked); + QObject::connect(ui->filterDuplicates, &ButtonSwitch::clicked, this, &PagerDemodGUI::on_filterDuplicates_clicked); QObject::connect(ui->logEnable, &ButtonSwitch::clicked, this, &PagerDemodGUI::on_logEnable_clicked); QObject::connect(ui->logFilename, &QToolButton::clicked, this, &PagerDemodGUI::on_logFilename_clicked); QObject::connect(ui->logOpen, &QToolButton::clicked, this, &PagerDemodGUI::on_logOpen_clicked); @@ -824,3 +897,178 @@ void PagerDemodGUI::updateAbsoluteCenterFrequency() { setStatusFrequency(m_deviceCenterFrequency + m_settings.m_inputFrequencyOffset); } + +// Initialise text to speech engine +// This takes 10 seconds on some versions of Linux, so only do it, if user actually +// has speech notifications configured +void PagerDemodGUI::enableSpeechIfNeeded() +{ +#ifdef QT_TEXTTOSPEECH_FOUND + if (m_speech) { + return; + } + for (const auto& notification : m_settings.m_notificationSettings) + { + if (!notification->m_speech.isEmpty()) + { + qDebug() << "PagerDemodGUI: Enabling text to speech"; + m_speech = new QTextToSpeech(this); + return; + } + } +#endif +} + +void PagerDemodGUI::checkNotification(int row) +{ + QString address = ui->messages->item(row, MESSAGE_COL_ADDRESS)->text(); + QString message = ui->messages->item(row, MESSAGE_COL_MESSAGE)->text(); + + for (int i = 0; i < m_settings.m_notificationSettings.size(); i++) + { + QString match; + switch (m_settings.m_notificationSettings[i]->m_matchColumn) + { + case MESSAGE_COL_ADDRESS: + match = address; + break; + case MESSAGE_COL_MESSAGE: + match = message; + break; + } + if (!match.isEmpty()) + { + if (m_settings.m_notificationSettings[i]->m_regularExpression.isValid()) + { + QRegularExpressionMatch matchResult = m_settings.m_notificationSettings[i]->m_regularExpression.match(match); + if (matchResult.hasMatch()) + { + if (m_settings.m_notificationSettings[i]->m_highlight) { + ui->messages->item(row, MESSAGE_COL_MESSAGE)->setTextColor(m_settings.m_notificationSettings[i]->m_highlightColor); + } + + if (!m_settings.m_notificationSettings[i]->m_speech.isEmpty()) + { + QString speech = subStrings(address, message, matchResult, m_settings.m_notificationSettings[i]->m_speech); + + speechNotification(speech); + } + if (!m_settings.m_notificationSettings[i]->m_command.isEmpty()) + { + QString command = subStrings(address, message, matchResult, m_settings.m_notificationSettings[i]->m_command); + + commandNotification(command); + } + if (m_settings.m_notificationSettings[i]->m_plotOnMap) + { + float latitude; + float longitude; + + if (Units::stringToLatitudeAndLongitude(message, latitude, longitude, false)) + { + QDateTime dateTime; + + dateTime.setDate(QDate::fromString(ui->messages->item(row, MESSAGE_COL_DATE)->text())); + dateTime.setTime(QTime::fromString(ui->messages->item(row, MESSAGE_COL_TIME)->text())); + + sendToMap(address, message, latitude, longitude, dateTime); + } + } + } + } + } + } +} + +QString PagerDemodGUI::subStrings(const QString& address, const QString& message, const QRegularExpressionMatch& match, const QString &string) const +{ + QString s = string; + s = s.replace("${address}", address); + s = s.replace("${message}", message); + for (int i = 0; i < match.capturedTexts().size(); i++) + { + QString escape = QString("${%1}").arg(i); + s = s.replace(escape, match.capturedTexts()[i]); + } + return s; +} + +void PagerDemodGUI::speechNotification(const QString &speech) +{ +#ifdef QT_TEXTTOSPEECH_FOUND + if (m_speech) { + m_speech->say(speech); + } else { + qWarning() << "PagerDemodGUI::speechNotification: Unable to say " << speech; + } +#else + qWarning() << "PagerDemodGUI::speechNotification: TextToSpeech not supported. Unable to say " << speech; +#endif +} + +void PagerDemodGUI::commandNotification(const QString &command) +{ +#if QT_CONFIG(process) + QStringList allArgs = QProcess::splitCommand(command); + + if (allArgs.size() > 0) + { + QString program = allArgs[0]; + allArgs.pop_front(); + QProcess::startDetached(program, allArgs); + } +#else + qWarning() << "PagerDemodGUI::commandNotification: QProcess not supported. Can't run: " << command; +#endif +} + +void PagerDemodGUI::sendToMap(const QString& address, const QString& message, float latitude, float longitude, QDateTime dateTime) +{ + QList mapPipes; + MainCore::instance()->getMessagePipes().getMessagePipes(m_pagerDemod, "mapitems", mapPipes); + + for (const auto& pipe : mapPipes) + { + MessageQueue *messageQueue = qobject_cast(pipe->m_element); + SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem(); + swgMapItem->setName(new QString(address)); + swgMapItem->setLatitude(latitude); + swgMapItem->setLongitude(longitude); + swgMapItem->setAltitude(0); + swgMapItem->setAltitudeReference(1); // CLAMP_TO_GROUND + swgMapItem->setFixedPosition(false); + swgMapItem->setPositionDateTime(new QString(dateTime.toString(Qt::ISODateWithMs))); + + swgMapItem->setImageRotation(0); + swgMapItem->setText(new QString(message)); + swgMapItem->setImage(new QString(QString("pager.png"))); + + MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_pagerDemod, swgMapItem); + messageQueue->push(msg); + } + + m_mapItems.insert(address); +} + +// Clear all items from map +void PagerDemodGUI::clearFromMap() +{ + for (const auto& address : m_mapItems) + { + QList mapPipes; + MainCore::instance()->getMessagePipes().getMessagePipes(m_pagerDemod, "mapitems", mapPipes); + + for (const auto& pipe : mapPipes) + { + MessageQueue *messageQueue = qobject_cast(pipe->m_element); + SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem(); + swgMapItem->setName(new QString(address)); + swgMapItem->setImage(new QString(QString(""))); + + MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_pagerDemod, swgMapItem); + messageQueue->push(msg); + } + } + + m_mapItems.clear(); +} diff --git a/plugins/channelrx/demodpager/pagerdemodgui.h b/plugins/channelrx/demodpager/pagerdemodgui.h index 90611b15e..4531c8c60 100644 --- a/plugins/channelrx/demodpager/pagerdemodgui.h +++ b/plugins/channelrx/demodpager/pagerdemodgui.h @@ -20,6 +20,7 @@ #define INCLUDE_PAGERDEMODGUI_H #include +#include #include "channel/channelgui.h" #include "dsp/channelmarker.h" @@ -45,6 +46,18 @@ class PagerDemodGUI : public ChannelGUI { Q_OBJECT public: + enum MessageCol { + MESSAGE_COL_DATE, + MESSAGE_COL_TIME, + MESSAGE_COL_ADDRESS, + MESSAGE_COL_MESSAGE, + MESSAGE_COL_FUNCTION, + MESSAGE_COL_ALPHA, + MESSAGE_COL_NUMERIC, + MESSAGE_COL_EVEN_PE, + MESSAGE_COL_BCH_PE + }; + static PagerDemodGUI* create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel); virtual void destroy(); @@ -86,12 +99,18 @@ private: QMenu *messagesMenu; // Column select context menu +#ifdef QT_TEXTTOSPEECH_FOUND + QTextToSpeech *m_speech; +#endif + QSet m_mapItems; + explicit PagerDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent = 0); virtual ~PagerDemodGUI(); void blockApplySettings(bool block); void applySettings(bool force = false); void displaySettings(); + QString selectMessage(int functionBits, const QString &numericMessage, const QString &alphaMessage) const; void messageReceived(const QDateTime dateTime, int address, int functionBits, const QString &numericMessage, const QString &alphaMessage, int evenParityErrors, int bchParityErrors); @@ -105,17 +124,13 @@ private: void resizeTable(); QAction *createCheckableItem(QString& text, int idx, bool checked, const char *slot); - enum MessageCol { - MESSAGE_COL_DATE, - MESSAGE_COL_TIME, - MESSAGE_COL_ADDRESS, - MESSAGE_COL_MESSAGE, - MESSAGE_COL_FUNCTION, - MESSAGE_COL_ALPHA, - MESSAGE_COL_NUMERIC, - MESSAGE_COL_EVEN_PE, - MESSAGE_COL_BCH_PE - }; + void enableSpeechIfNeeded(); + void checkNotification(int row); + void speechNotification(const QString &speech); + void commandNotification(const QString &command); + QString subStrings(const QString& address, const QString& message, const QRegularExpressionMatch& match, const QString &string) const; + void sendToMap(const QString& address, const QString& message, float latitide, float longitude, QDateTime dateTime); + void clearFromMap(); private slots: void on_deltaFrequency_changed(qint64 value); @@ -131,6 +146,9 @@ private slots: void on_udpPort_editingFinished(); void on_channel1_currentIndexChanged(int index); void on_channel2_currentIndexChanged(int index); + void on_notifications_clicked(); + void on_filterDuplicates_clicked(bool checked=false); + void on_filterDuplicates_rightClicked(const QPoint &); void on_logEnable_clicked(bool checked=false); void on_logFilename_clicked(); void on_logOpen_clicked(); diff --git a/plugins/channelrx/demodpager/pagerdemodgui.ui b/plugins/channelrx/demodpager/pagerdemodgui.ui index 240b42678..d66602c81 100644 --- a/plugins/channelrx/demodpager/pagerdemodgui.ui +++ b/plugins/channelrx/demodpager/pagerdemodgui.ui @@ -29,7 +29,7 @@ - Qt::StrongFocus + Qt::FocusPolicy::StrongFocus Pager Demodulator @@ -110,7 +110,7 @@ PointingHandCursor - Qt::StrongFocus + Qt::FocusPolicy::StrongFocus Demod shift frequency from center in Hz @@ -127,14 +127,14 @@ - Qt::Vertical + Qt::Orientation::Vertical - Qt::Horizontal + Qt::Orientation::Horizontal @@ -152,7 +152,7 @@ Channel power - Qt::RightToLeft + Qt::LayoutDirection::RightToLeft 0.0 @@ -209,7 +209,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -252,7 +252,7 @@ 100 - Qt::Horizontal + Qt::Orientation::Horizontal @@ -268,14 +268,14 @@ 10.0k - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter - Qt::Vertical + Qt::Orientation::Vertical @@ -316,7 +316,7 @@ 24 - Qt::Horizontal + Qt::Orientation::Horizontal @@ -332,7 +332,7 @@ 2.4k - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter @@ -368,7 +368,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -413,7 +413,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -479,7 +479,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -494,22 +494,29 @@ - Qt::Horizontal + Qt::Orientation::Horizontal + + + + UDP + + + Forward messages via UDP - Qt::RightToLeft + Qt::LayoutDirection::RightToLeft - UDP + @@ -522,7 +529,7 @@ - Qt::ClickFocus + Qt::FocusPolicy::ClickFocus Destination UDP address @@ -541,7 +548,7 @@ : - Qt::AlignCenter + Qt::AlignmentFlag::AlignCenter @@ -560,7 +567,7 @@ - Qt::ClickFocus + Qt::FocusPolicy::ClickFocus Destination UDP port @@ -576,7 +583,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -591,7 +598,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -614,7 +621,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -624,6 +631,43 @@ + + + + + 24 + 16777215 + + + + Filter duplicate messages. Right click for options. + + + + + + + :/icons/filterduplicate.png:/icons/filterduplicate.png + + + + + + + Open notifications dialog + + + ... + + + + :/mono.png:/mono.png + + + false + + + @@ -736,7 +780,7 @@ Received messages - QAbstractItemView::NoEditTriggers + QAbstractItemView::EditTrigger::NoEditTriggers @@ -1056,6 +1100,7 @@ + diff --git a/plugins/channelrx/demodpager/pagerdemodicons.qrc b/plugins/channelrx/demodpager/pagerdemodicons.qrc new file mode 100644 index 000000000..f59155cdd --- /dev/null +++ b/plugins/channelrx/demodpager/pagerdemodicons.qrc @@ -0,0 +1,5 @@ + + + icons/filterduplicate.png + + diff --git a/plugins/channelrx/demodpager/pagerdemodnotificationdialog.cpp b/plugins/channelrx/demodpager/pagerdemodnotificationdialog.cpp new file mode 100644 index 000000000..b7979ac2c --- /dev/null +++ b/plugins/channelrx/demodpager/pagerdemodnotificationdialog.cpp @@ -0,0 +1,173 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include + +#include "gui/tablecolorchooser.h" + +#include "pagerdemodnotificationdialog.h" +#include "pagerdemodgui.h" + +// Map main table column numbers to combo box indices +std::vector PagerDemodNotificationDialog::m_columnMap = { + PagerDemodGUI::MESSAGE_COL_ADDRESS, PagerDemodGUI::MESSAGE_COL_MESSAGE +}; + +PagerDemodNotificationDialog::PagerDemodNotificationDialog(PagerDemodSettings *settings, + QWidget* parent) : + QDialog(parent), + ui(new Ui::PagerDemodNotificationDialog), + m_settings(settings) +{ + ui->setupUi(this); + + resizeTable(); + + for (int i = 0; i < m_settings->m_notificationSettings.size(); i++) { + addRow(m_settings->m_notificationSettings[i]); + } +} + +PagerDemodNotificationDialog::~PagerDemodNotificationDialog() +{ + delete ui; + qDeleteAll(m_colorGUIs); +} + +void PagerDemodNotificationDialog::accept() +{ + qDeleteAll(m_settings->m_notificationSettings); + m_settings->m_notificationSettings.clear(); + for (int i = 0; i < ui->table->rowCount(); i++) + { + PagerDemodSettings::NotificationSettings *notificationSettings = new PagerDemodSettings::NotificationSettings(); + int idx = ((QComboBox *)ui->table->cellWidget(i, NOTIFICATION_COL_MATCH))->currentIndex(); + notificationSettings->m_matchColumn = m_columnMap[idx]; + notificationSettings->m_regExp = ui->table->item(i, NOTIFICATION_COL_REG_EXP)->data(Qt::DisplayRole).toString().trimmed(); + notificationSettings->m_speech = ui->table->item(i, NOTIFICATION_COL_SPEECH)->data(Qt::DisplayRole).toString().trimmed(); + notificationSettings->m_command = ui->table->item(i, NOTIFICATION_COL_COMMAND)->data(Qt::DisplayRole).toString().trimmed(); + notificationSettings->m_highlight = !m_colorGUIs[i]->m_noColor; + notificationSettings->m_highlightColor = m_colorGUIs[i]->m_color; + notificationSettings->m_plotOnMap = ((QCheckBox *) ui->table->cellWidget(i, NOTIFICATION_COL_PLOT_ON_MAP))->isChecked(); + notificationSettings->updateRegularExpression(); + m_settings->m_notificationSettings.append(notificationSettings); + } + QDialog::accept(); +} + +void PagerDemodNotificationDialog::resizeTable() +{ + PagerDemodSettings::NotificationSettings dummy; + dummy.m_matchColumn = PagerDemodGUI::MESSAGE_COL_ADDRESS; + dummy.m_regExp = "1234567"; + dummy.m_speech = "${message}"; + dummy.m_command = "cmail.exe -to:user@host.com \"-subject: Paging ${address}\" \"-body: ${message}\""; + dummy.m_highlight = true; + dummy.m_plotOnMap = true; + addRow(&dummy); + ui->table->resizeColumnsToContents(); + ui->table->selectRow(0); + on_remove_clicked(); + ui->table->selectRow(-1); +} + +void PagerDemodNotificationDialog::on_add_clicked() +{ + addRow(); +} + +// Remove selected row +void PagerDemodNotificationDialog::on_remove_clicked() +{ + // Selection mode is single, so only a single row should be returned + QModelIndexList indexList = ui->table->selectionModel()->selectedRows(); + if (!indexList.isEmpty()) + { + int row = indexList.at(0).row(); + ui->table->removeRow(row); + m_colorGUIs.removeAt(row); + } +} + +void PagerDemodNotificationDialog::addRow(PagerDemodSettings::NotificationSettings *settings) +{ + int row = ui->table->rowCount(); + ui->table->setSortingEnabled(false); + ui->table->setRowCount(row + 1); + + QComboBox *match = new QComboBox(); + TableColorChooser *highlight; + if (settings) { + highlight = new TableColorChooser(ui->table, row, NOTIFICATION_COL_HIGHLIGHT, !settings->m_highlight, settings->m_highlightColor); + } else { + highlight = new TableColorChooser(ui->table, row, NOTIFICATION_COL_HIGHLIGHT, false, QColor(Qt::red).rgba()); + } + m_colorGUIs.append(highlight); + QCheckBox *plotOnMap = new QCheckBox(); + plotOnMap->setChecked(false); + QWidget *matchWidget = new QWidget(); + QHBoxLayout *pLayout = new QHBoxLayout(matchWidget); + pLayout->addWidget(match); + pLayout->setAlignment(Qt::AlignCenter); + pLayout->setContentsMargins(0, 0, 0, 0); + matchWidget->setLayout(pLayout); + + match->addItem("Address"); + match->addItem("Message"); + + QTableWidgetItem *regExpItem = new QTableWidgetItem(); + QTableWidgetItem *speechItem = new QTableWidgetItem(); + QTableWidgetItem *commandItem = new QTableWidgetItem(); + + if (settings != nullptr) + { + for (unsigned int i = 0; i < m_columnMap.size(); i++) + { + if (m_columnMap[i] == settings->m_matchColumn) + { + match->setCurrentIndex(i); + break; + } + } + regExpItem->setData(Qt::DisplayRole, settings->m_regExp); + speechItem->setData(Qt::DisplayRole, settings->m_speech); + commandItem->setData(Qt::DisplayRole, settings->m_command); + plotOnMap->setChecked(settings->m_plotOnMap); + } + else + { + match->setCurrentIndex(0); + regExpItem->setData(Qt::DisplayRole, ".*"); + speechItem->setData(Qt::DisplayRole, "${message}"); +#ifdef _MSC_VER + commandItem->setData(Qt::DisplayRole, "cmail.exe -to:user@host.com \"-subject: Paging ${address}\" \"-body: ${message}\""); +#else + commandItem->setData(Qt::DisplayRole, "sendmail -s \"Paging ${address}: ${message}\" user@host.com"); +#endif + } + + ui->table->setCellWidget(row, NOTIFICATION_COL_MATCH, match); + ui->table->setItem(row, NOTIFICATION_COL_REG_EXP, regExpItem); + ui->table->setItem(row, NOTIFICATION_COL_SPEECH, speechItem); + ui->table->setItem(row, NOTIFICATION_COL_COMMAND, commandItem); + ui->table->setCellWidget(row, NOTIFICATION_COL_PLOT_ON_MAP, plotOnMap); + ui->table->setSortingEnabled(true); +} diff --git a/plugins/channelrx/demodpager/pagerdemodnotificationdialog.h b/plugins/channelrx/demodpager/pagerdemodnotificationdialog.h new file mode 100644 index 000000000..a62ec954d --- /dev/null +++ b/plugins/channelrx/demodpager/pagerdemodnotificationdialog.h @@ -0,0 +1,61 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_PAGERDEMODNOTIFICATIONDIALOG_H +#define INCLUDE_PAGERDEMODNOTIFICATIONDIALOG_H + +#include + +#include "ui_pagerdemodnotificationdialog.h" +#include "pagerdemodsettings.h" + +class TableColorChooser; + +class PagerDemodNotificationDialog : public QDialog { + Q_OBJECT + +public: + explicit PagerDemodNotificationDialog(PagerDemodSettings* settings, QWidget* parent = 0); + ~PagerDemodNotificationDialog(); + +private: + void resizeTable(); + +private slots: + void accept() override; + void on_add_clicked(); + void on_remove_clicked(); + void addRow(PagerDemodSettings::NotificationSettings *settings=nullptr); + +private: + Ui::PagerDemodNotificationDialog* ui; + PagerDemodSettings *m_settings; + QList m_colorGUIs; + + enum NotificationCol { + NOTIFICATION_COL_MATCH, + NOTIFICATION_COL_REG_EXP, + NOTIFICATION_COL_SPEECH, + NOTIFICATION_COL_COMMAND, + NOTIFICATION_COL_HIGHLIGHT, + NOTIFICATION_COL_PLOT_ON_MAP + }; + + static std::vector m_columnMap; +}; + +#endif // INCLUDE_PagerDEMODNOTIFICATIONDIALOG_H diff --git a/plugins/channelrx/demodpager/pagerdemodnotificationdialog.ui b/plugins/channelrx/demodpager/pagerdemodnotificationdialog.ui new file mode 100644 index 000000000..5ceded4f7 --- /dev/null +++ b/plugins/channelrx/demodpager/pagerdemodnotificationdialog.ui @@ -0,0 +1,175 @@ + + + PagerDemodNotificationDialog + + + + 0 + 0 + 1100 + 400 + + + + + Liberation Sans + 9 + + + + Qt::ContextMenuPolicy::PreventContextMenu + + + Notifications + + + + + + + + + QAbstractItemView::SelectionMode::SingleSelection + + + QAbstractItemView::SelectionBehavior::SelectRows + + + + Match + + + ADS-B data to match + + + + + Reg Exp + + + Regular expression to match with + + + + + Speech + + + Speech for the computer to read when a match is made + + + + + Command + + + Command/script to execute when a match is made + + + + + Highlight + + + + + Plot on Map + + + + + + + + + + Add device set control + + + + + + + + + + + Remove device set control + + + - + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + Qt::Orientation::Horizontal + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok + + + + + + + table + add + remove + + + + + + + buttonBox + accepted() + PagerDemodNotificationDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + PagerDemodNotificationDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/plugins/channelrx/demodpager/pagerdemodsettings.cpp b/plugins/channelrx/demodpager/pagerdemodsettings.cpp index df737ba90..ab4cdb93d 100644 --- a/plugins/channelrx/demodpager/pagerdemodsettings.cpp +++ b/plugins/channelrx/demodpager/pagerdemodsettings.cpp @@ -20,10 +20,12 @@ #include #include +#include #include "util/simpleserializer.h" #include "settings/serializable.h" #include "pagerdemodsettings.h" +#include "pagerdemodgui.h" PagerDemodSettings::PagerDemodSettings() : m_channelMarker(nullptr), @@ -59,6 +61,9 @@ void PagerDemodSettings::resetToDefaults() m_reverse = false; m_workspaceIndex = 0; m_hidden = false; + m_filterDuplicates = false; + m_duplicateMatchMessageOnly = false; + m_duplicateMatchLastOnly = false; for (int i = 0; i < PAGERDEMOD_MESSAGE_COLUMNS; i++) { @@ -110,6 +115,11 @@ QByteArray PagerDemodSettings::serialize() const s.writeBlob(29, m_geometryBytes); s.writeBool(30, m_hidden); + s.writeList(31, m_notificationSettings); + s.writeBool(32, m_filterDuplicates); + s.writeBool(33, m_duplicateMatchMessageOnly); + s.writeBool(34, m_duplicateMatchLastOnly); + for (int i = 0; i < PAGERDEMOD_MESSAGE_COLUMNS; i++) { s.writeS32(100 + i, m_messageColumnIndexes[i]); } @@ -205,6 +215,12 @@ bool PagerDemodSettings::deserialize(const QByteArray& data) d.readBlob(29, &m_geometryBytes); d.readBool(30, &m_hidden, false); + d.readList(31, &m_notificationSettings); + + d.readBool(32, &m_filterDuplicates); + d.readBool(33, &m_duplicateMatchMessageOnly); + d.readBool(34, &m_duplicateMatchLastOnly); + for (int i = 0; i < PAGERDEMOD_MESSAGE_COLUMNS; i++) { d.readS32(100 + i, &m_messageColumnIndexes[i], i); } @@ -237,3 +253,80 @@ void PagerDemodSettings::deserializeIntList(const QByteArray& data, QList> ints; delete stream; } + +PagerDemodSettings::NotificationSettings::NotificationSettings() : + m_matchColumn(PagerDemodGUI::MESSAGE_COL_ADDRESS), + m_highlight(false), + m_highlightColor(Qt::red), + m_plotOnMap(false) +{ +} + +void PagerDemodSettings::NotificationSettings::updateRegularExpression() +{ + m_regularExpression.setPattern(m_regExp); + m_regularExpression.optimize(); + if (!m_regularExpression.isValid()) { + qDebug() << "PagerDemodSettings::NotificationSettings: Regular expression is not valid: " << m_regExp; + } +} + +QByteArray PagerDemodSettings::NotificationSettings::serialize() const +{ + SimpleSerializer s(1); + + s.writeS32(1, m_matchColumn); + s.writeString(2, m_regExp); + s.writeString(3, m_speech); + s.writeString(4, m_command); + s.writeBool(5, m_highlight); + s.writeS32(6, m_highlightColor); + s.writeBool(7, m_plotOnMap); + + return s.final(); +} + +bool PagerDemodSettings::NotificationSettings::deserialize(const QByteArray& data) +{ + SimpleDeserializer d(data); + + if (!d.isValid()) { + return false; + } + + if (d.getVersion() == 1) + { + QByteArray blob; + + d.readS32(1, &m_matchColumn); + d.readString(2, &m_regExp); + d.readString(3, &m_speech); + d.readString(4, &m_command); + d.readBool(5, &m_highlight, false); + d.readS32(6, &m_highlightColor, QColor(Qt::red).rgba()); + d.readBool(7, &m_plotOnMap, false); + + updateRegularExpression(); + + return true; + } + else + { + return false; + } +} + +QDataStream& operator<<(QDataStream& out, const PagerDemodSettings::NotificationSettings *settings) +{ + out << settings->serialize(); + return out; +} + +QDataStream& operator>>(QDataStream& in, PagerDemodSettings::NotificationSettings*& settings) +{ + settings = new PagerDemodSettings::NotificationSettings(); + QByteArray data; + in >> data; + settings->deserialize(data); + return in; +} diff --git a/plugins/channelrx/demodpager/pagerdemodsettings.h b/plugins/channelrx/demodpager/pagerdemodsettings.h index 6d1380de2..6a223ea5d 100644 --- a/plugins/channelrx/demodpager/pagerdemodsettings.h +++ b/plugins/channelrx/demodpager/pagerdemodsettings.h @@ -23,6 +23,7 @@ #include #include +#include #include "dsp/dsptypes.h" @@ -33,6 +34,23 @@ class Serializable; struct PagerDemodSettings { + struct NotificationSettings { + int m_matchColumn; + QString m_regExp; + QString m_speech; + QString m_command; + bool m_highlight; + qint32 m_highlightColor; + bool m_plotOnMap; + + QRegularExpression m_regularExpression; + + NotificationSettings(); + void updateRegularExpression(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + }; + qint32 m_baud; //!< 512, 1200 or 2400 qint32 m_inputFrequencyOffset; Real m_rfBandwidth; @@ -73,6 +91,12 @@ struct PagerDemodSettings QByteArray m_geometryBytes; bool m_hidden; + QList m_notificationSettings; + + bool m_filterDuplicates; + bool m_duplicateMatchMessageOnly; + bool m_duplicateMatchLastOnly; + int m_messageColumnIndexes[PAGERDEMOD_MESSAGE_COLUMNS];//!< How the columns are ordered in the table int m_messageColumnSizes[PAGERDEMOD_MESSAGE_COLUMNS]; //!< Size of the columns in the table diff --git a/plugins/channelrx/demodpager/readme.md b/plugins/channelrx/demodpager/readme.md index 26751f06f..b6e358c4c 100644 --- a/plugins/channelrx/demodpager/readme.md +++ b/plugins/channelrx/demodpager/readme.md @@ -1,4 +1,4 @@ -

Pager demodulator plugin

+

Pager demodulator plugin

Introduction

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

Filter Duplicates

+ +Check to filter (discard) duplicate messages. Right click to show the Duplicate Filter options dialog: + +- Match message only: When unchecked, compare address and message. When checked, compare only message, ignoring the address. +- Match last message only: When unchecked the message is compared against all messages in the table. When checked, the message is compared against the last received message only. + +

Open Notifications Dialog

+ +When clicked, opens the Notifications Dialog, which allows speech notifications or programs/scripts to be run when messages matching user-defined rules are received. + +By running a program such as [cmail](https://www.inveigle.net/cmail/download) on Windows or sendmail on Linux, e-mail notifications can be sent. + +Messages can be highlighted in a user-defined colour. + +By checking plot on map, if a message contains a position specified as latitude and longitude, the message can be displayed on the [Map](../../feature/map/readme.md) feature. +The format of the coordinates should follow https://en.wikipedia.org/wiki/ISO_6709, E.g: 50°40′46″N 95°48′26″W or -23.342,5.234 + +Here are a few examples: + +![Notifications Dialog](../../../doc/img/PagerDemod_plugin_notifications.png) + +In the Speech and Command strings, variables can be used to substitute data from the received message: + +* ${address}, +* ${message}, +* ${1}, ${2}... are replaced with the string from the corresponding capture group in the regular expression. + +To experiment with regular expressions, try [https://regexr.com/](https://regexr.com/). +

15: Start/stop Logging Messages to .csv File

When checked, writes all received messages to a .csv file. diff --git a/plugins/feature/map/map.qrc b/plugins/feature/map/map.qrc index 7a0eeaa05..6cd815444 100644 --- a/plugins/feature/map/map.qrc +++ b/plugins/feature/map/map.qrc @@ -25,6 +25,7 @@ map/airport_small.png map/heliport.png map/waypoint.png + map/pager.png map/map3d.html data/transmitters.csv diff --git a/plugins/feature/map/map/pager.png b/plugins/feature/map/map/pager.png new file mode 100644 index 000000000..ec63d645e Binary files /dev/null and b/plugins/feature/map/map/pager.png differ diff --git a/plugins/feature/map/mapsettings.cpp b/plugins/feature/map/mapsettings.cpp index 6c1f4a8ab..96aeecd7a 100644 --- a/plugins/feature/map/mapsettings.cpp +++ b/plugins/feature/map/mapsettings.cpp @@ -38,6 +38,7 @@ const QStringList MapSettings::m_pipeTypes = { QStringLiteral("FT8Demod"), QStringLiteral("HeatMap"), QStringLiteral("ILSDemod"), + QStringLiteral("PagerDemod"), QStringLiteral("Radiosonde"), QStringLiteral("StarTracker"), QStringLiteral("SatelliteTracker"), @@ -55,6 +56,7 @@ const QStringList MapSettings::m_pipeURIs = { QStringLiteral("sdrangel.channel.ft8demod"), QStringLiteral("sdrangel.channel.heatmap"), QStringLiteral("sdrangel.channel.ilsdemod"), + QStringLiteral("sdrangel.channel.pagerdemod"), QStringLiteral("sdrangel.feature.radiosonde"), QStringLiteral("sdrangel.feature.startracker"), QStringLiteral("sdrangel.feature.satellitetracker"), @@ -96,6 +98,7 @@ MapSettings::MapSettings() : m_itemSettings.insert("StarTracker", new MapItemSettings("StarTracker", true, QColor(230, 230, 230), true, true, 3)); m_itemSettings.insert("SatelliteTracker", new MapItemSettings("SatelliteTracker", true, QColor(0, 0, 255), true, false, 0, modelMinPixelSize)); m_itemSettings.insert("Beacons", new MapItemSettings("Beacons", true, QColor(255, 0, 0), false, true, 8)); + m_itemSettings.insert("PagerDemod", new MapItemSettings("PagerDemod", true, QColor(200, 191, 231), true, false, 11)); m_itemSettings.insert("Radiosonde", new MapItemSettings("Radiosonde", true, QColor(102, 0, 102), true, false, 11, modelMinPixelSize)); m_itemSettings.insert("Radio Time Transmitters", new MapItemSettings("Radio Time Transmitters", true, QColor(255, 0, 0), false, true, 8)); m_itemSettings.insert("Radar", new MapItemSettings("Radar", true, QColor(255, 0, 0), false, true, 8)); diff --git a/plugins/feature/map/readme.md b/plugins/feature/map/readme.md index c542c5275..1c53ac54b 100644 --- a/plugins/feature/map/readme.md +++ b/plugins/feature/map/readme.md @@ -16,7 +16,8 @@ On top of this, it can plot data from other plugins, such as: * Radials and estimated position from the VOR localizer feature, * ILS course line and glide path from the ILS Demodulator, * DSC geographic call areas, -* SID paths. +* SID paths, +* Pager messages that contain coordinates. As well as internet and built-in data sources: @@ -313,6 +314,7 @@ Mapbox: https://www.mapbox.com/ Cesium: https://www.cesium.com Bing: https://www Ionosonde data and MUF/coF2 contours from [KC2G](https://prop.kc2g.com/) with source data from [GIRO](https://giro.uml.edu/) and [NOAA NCEI](https://www.ngdc.noaa.gov/stp/iono/ionohome.html). + Sea Marks are from OpenSeaMap: https://www.openseamap.org/ Railways are from OpenRailwayMap: https://www.openrailwaymap.org/ @@ -324,6 +326,7 @@ World icons created by turkkub from Flaticon: https://www.flaticon.com Layers and Boat icons created by Freepik from Flaticon: https://www.flaticon.com Railway icons created by Prosymbols Premium from Flaticon: https://www.flaticon.com Satellite icons created by SyafriStudio from Flaticon: https://www.flaticon.com +Pager icons created by xnimrodx from Flaticon: https://www.flaticon.com 3D models are by various artists under a variety of licenses. See: https://github.com/srcejon/sdrangel-3d-models diff --git a/sdrbase/util/csv.cpp b/sdrbase/util/csv.cpp index d83841791..7354e2a2d 100644 --- a/sdrbase/util/csv.cpp +++ b/sdrbase/util/csv.cpp @@ -167,3 +167,11 @@ QHash CSV::readHeader(QTextStream &in, QStringList requiredColumns return colNumbers; } + +QString CSV::escape(const QString& string) +{ + QString s = string; + s.replace('"', "\"\""); + s = QString("\"%1\"").arg(s); + return s; +} diff --git a/sdrbase/util/csv.h b/sdrbase/util/csv.h index 6756636e1..34117edf5 100644 --- a/sdrbase/util/csv.h +++ b/sdrbase/util/csv.h @@ -52,6 +52,8 @@ struct SDRBASE_API CSV { static bool readRow(QTextStream &in, QStringList *row, char seperator=','); static QHash readHeader(QTextStream &in, QStringList requiredColumns, QString &error, char seperator=','); + static QString escape(const QString& string); + }; #endif /* INCLUDE_CSV_H */ diff --git a/sdrbase/util/units.h b/sdrbase/util/units.h index edfe1f847..4296813b3 100644 --- a/sdrbase/util/units.h +++ b/sdrbase/util/units.h @@ -269,11 +269,15 @@ public: // Try to convert a string to latitude and longitude. Returns false if not recognised format. // https://en.wikipedia.org/wiki/ISO_6709 specifies a standard syntax // We support both decimal and DMS formats - static bool stringToLatitudeAndLongitude(const QString& string, float& latitude, float& longitude) + static bool stringToLatitudeAndLongitude(const QString& string, float& latitude, float& longitude, bool exact=true) { QRegularExpressionMatch match; - QRegularExpression decimal(QRegularExpression::anchoredPattern("(-?[0-9]+(\\.[0-9]+)?) *,? *(-?[0-9]+(\\.[0-9]+)?)")); + QString decimalPattern = "(-?[0-9]+(\\.[0-9]+)?) *,? *(-?[0-9]+(\\.[0-9]+)?)"; + if (exact) { + decimalPattern = QRegularExpression::anchoredPattern(decimalPattern); + } + QRegularExpression decimal(decimalPattern); match = decimal.match(string); if (match.hasMatch()) { @@ -282,7 +286,11 @@ public: return true; } - QRegularExpression dms(QRegularExpression::anchoredPattern(QString("([0-9]+)[%1d]([0-9]+)['m]([0-9]+(\\.[0-9]+)?)[\"s]([NS]) *,? *([0-9]+)[%1d]([0-9]+)['m]([0-9]+(\\.[0-9]+)?)[\"s]([EW])").arg(QChar(0xb0)))); + QString dmsPattern = QString("([0-9]+)[%1d]([0-9]+)['m]([0-9]+(\\.[0-9]+)?)[\"s]([NS]) *,? *([0-9]+)[%1d]([0-9]+)['m]([0-9]+(\\.[0-9]+)?)[\"s]([EW])").arg(QChar(0xb0)); + if (exact) { + dmsPattern = QRegularExpression::anchoredPattern(dmsPattern); + } + QRegularExpression dms(dmsPattern); match = dms.match(string); if (match.hasMatch()) { @@ -303,7 +311,11 @@ public: return true; } - QRegularExpression dms2(QRegularExpression::anchoredPattern(QString("([0-9]+)([NS])([0-9]{2})([0-9]{2}) *,?([0-9]+)([EW])([0-9]{2})([0-9]{2})"))); + QString dms2Pattern = "([0-9]+)([NS])([0-9]{2})([0-9]{2}) *,?([0-9]+)([EW])([0-9]{2})([0-9]{2})"; + if (exact) { + dms2Pattern = QRegularExpression::anchoredPattern(dms2Pattern); + } + QRegularExpression dms2(dms2Pattern); match = dms2.match(string); if (match.hasMatch()) { @@ -325,7 +337,11 @@ public: } // 512255.5900N 0024400.6105W as used on aviation charts - QRegularExpression dms3(QRegularExpression::anchoredPattern(QString("(\\d{2})(\\d{2})((\\d{2})(\\.\\d+)?)([NS]) *,?(\\d{3})(\\d{2})((\\d{2})(\\.\\d+)?)([EW])"))); + QString dms3Pattern = "(\\d{2})(\\d{2})((\\d{2})(\\.\\d+)?)([NS]) *,?(\\d{3})(\\d{2})((\\d{2})(\\.\\d+)?)([EW])"; + if (exact) { + dms3Pattern = QRegularExpression::anchoredPattern(dms3Pattern); + } + QRegularExpression dms3(dms3Pattern); match = dms3.match(string); if (match.hasMatch()) {