1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2024-12-23 01:55:48 -05:00

Add pager notifications.

Add option to ignore duplicates.
Support plotting pager messages on the map.
This commit is contained in:
Jon Beniston 2024-10-11 11:01:43 +01:00
parent f24600b909
commit 4455ac0c55
24 changed files with 1239 additions and 119 deletions

View File

@ -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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -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";

View File

@ -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()));

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QDebug>
#include <QCheckBox>
#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();
}

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#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

View File

@ -0,0 +1,118 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PagerDemodFilterDialog</class>
<widget class="QDialog" name="PagerDemodFilterDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>396</width>
<height>167</height>
</rect>
</property>
<property name="font">
<font>
<family>Liberation Sans</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="contextMenuPolicy">
<enum>Qt::ContextMenuPolicy::PreventContextMenu</enum>
</property>
<property name="windowTitle">
<string>Duplicate Filtering</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Duplicate Filtering</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="matchMessageOnlyLabel">
<property name="text">
<string>Match message only</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="matchMessageOnly">
<property name="toolTip">
<string>Whether both the address and message must match or only the message to be considered a duplicate</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="matchLastOnlyLabel">
<property name="text">
<string>Match last message only</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="matchLastOnly">
<property name="toolTip">
<string>Whether to match with only the last message or any message in the table</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../../../sdrgui/resources/res.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>PagerDemodFilterDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>PagerDemodFilterDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -16,8 +16,6 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <limits>
#include <ctype.h>
#include <QDockWidget>
#include <QDebug>
#include <QAction>
@ -26,6 +24,7 @@
#include <QFileDialog>
#include <QMessageBox>
#include <QScrollBar>
#include <QProcess>
#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<ObjectPipe*> mapPipes;
MainCore::instance()->getMessagePipes().getMessagePipes(m_pagerDemod, "mapitems", mapPipes);
for (const auto& pipe : mapPipes)
{
MessageQueue *messageQueue = qobject_cast<MessageQueue*>(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<ObjectPipe*> mapPipes;
MainCore::instance()->getMessagePipes().getMessagePipes(m_pagerDemod, "mapitems", mapPipes);
for (const auto& pipe : mapPipes)
{
MessageQueue *messageQueue = qobject_cast<MessageQueue*>(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();
}

View File

@ -20,6 +20,7 @@
#define INCLUDE_PAGERDEMODGUI_H
#include <QMenu>
#include <QTextToSpeech>
#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<QString> 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();

View File

@ -29,7 +29,7 @@
</font>
</property>
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
<enum>Qt::FocusPolicy::StrongFocus</enum>
</property>
<property name="windowTitle">
<string>Pager Demodulator</string>
@ -110,7 +110,7 @@
<cursorShape>PointingHandCursor</cursorShape>
</property>
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
<enum>Qt::FocusPolicy::StrongFocus</enum>
</property>
<property name="toolTip">
<string>Demod shift frequency from center in Hz</string>
@ -127,14 +127,14 @@
<item>
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -152,7 +152,7 @@
<string>Channel power</string>
</property>
<property name="layoutDirection">
<enum>Qt::RightToLeft</enum>
<enum>Qt::LayoutDirection::RightToLeft</enum>
</property>
<property name="text">
<string>0.0</string>
@ -209,7 +209,7 @@
<item>
<widget class="Line" name="line_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
</item>
@ -252,7 +252,7 @@
<number>100</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
</item>
@ -268,14 +268,14 @@
<string>10.0k</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
</widget>
</item>
@ -316,7 +316,7 @@
<number>24</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
</item>
@ -332,7 +332,7 @@
<string>2.4k</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
</widget>
</item>
@ -368,7 +368,7 @@
<item>
<widget class="Line" name="line_10">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
</widget>
</item>
@ -413,7 +413,7 @@
<item>
<widget class="Line" name="line_11">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
</widget>
</item>
@ -479,7 +479,7 @@
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -494,22 +494,29 @@
<item>
<widget class="Line" name="line_6">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="udpLayout">
<item>
<widget class="QLabel" name="udpLabel">
<property name="text">
<string>UDP</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="udpEnabled">
<property name="toolTip">
<string>Forward messages via UDP</string>
</property>
<property name="layoutDirection">
<enum>Qt::RightToLeft</enum>
<enum>Qt::LayoutDirection::RightToLeft</enum>
</property>
<property name="text">
<string>UDP</string>
<string/>
</property>
</widget>
</item>
@ -522,7 +529,7 @@
</size>
</property>
<property name="focusPolicy">
<enum>Qt::ClickFocus</enum>
<enum>Qt::FocusPolicy::ClickFocus</enum>
</property>
<property name="toolTip">
<string>Destination UDP address</string>
@ -541,7 +548,7 @@
<string>:</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
@ -560,7 +567,7 @@
</size>
</property>
<property name="focusPolicy">
<enum>Qt::ClickFocus</enum>
<enum>Qt::FocusPolicy::ClickFocus</enum>
</property>
<property name="toolTip">
<string>Destination UDP port</string>
@ -576,7 +583,7 @@
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -591,7 +598,7 @@
<item>
<widget class="Line" name="line_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
</item>
@ -614,7 +621,7 @@
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -624,6 +631,43 @@
</property>
</spacer>
</item>
<item>
<widget class="ButtonSwitch" name="filterDuplicates">
<property name="maximumSize">
<size>
<width>24</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Filter duplicate messages. Right click for options.</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="pagerdemodicons.qrc">
<normaloff>:/icons/filterduplicate.png</normaloff>:/icons/filterduplicate.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="notifications">
<property name="toolTip">
<string>Open notifications dialog</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/mono.png</normaloff>:/mono.png</iconset>
</property>
<property name="checkable">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="logEnable">
<property name="maximumSize">
@ -736,7 +780,7 @@
<string>Received messages</string>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
<set>QAbstractItemView::EditTrigger::NoEditTriggers</set>
</property>
<column>
<property name="text">
@ -1056,6 +1100,7 @@
</customwidgets>
<resources>
<include location="../../../sdrgui/resources/res.qrc"/>
<include location="pagerdemodicons.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -0,0 +1,5 @@
<RCC>
<qresource prefix="/">
<file>icons/filterduplicate.png</file>
</qresource>
</RCC>

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QDebug>
#include <QComboBox>
#include <QPushButton>
#include <QCheckBox>
#include <QHBoxLayout>
#include "gui/tablecolorchooser.h"
#include "pagerdemodnotificationdialog.h"
#include "pagerdemodgui.h"
// Map main table column numbers to combo box indices
std::vector<int> 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);
}

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_PAGERDEMODNOTIFICATIONDIALOG_H
#define INCLUDE_PAGERDEMODNOTIFICATIONDIALOG_H
#include <QHash>
#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<TableColorChooser *> 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<int> m_columnMap;
};
#endif // INCLUDE_PagerDEMODNOTIFICATIONDIALOG_H

View File

@ -0,0 +1,175 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PagerDemodNotificationDialog</class>
<widget class="QDialog" name="PagerDemodNotificationDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1100</width>
<height>400</height>
</rect>
</property>
<property name="font">
<font>
<family>Liberation Sans</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="contextMenuPolicy">
<enum>Qt::ContextMenuPolicy::PreventContextMenu</enum>
</property>
<property name="windowTitle">
<string>Notifications</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<layout class="QGridLayout" name="gridLayout">
<item row="4" column="0">
<widget class="QTableWidget" name="table">
<property name="selectionMode">
<enum>QAbstractItemView::SelectionMode::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectionBehavior::SelectRows</enum>
</property>
<column>
<property name="text">
<string>Match</string>
</property>
<property name="toolTip">
<string>ADS-B data to match</string>
</property>
</column>
<column>
<property name="text">
<string>Reg Exp</string>
</property>
<property name="toolTip">
<string>Regular expression to match with</string>
</property>
</column>
<column>
<property name="text">
<string>Speech</string>
</property>
<property name="toolTip">
<string>Speech for the computer to read when a match is made</string>
</property>
</column>
<column>
<property name="text">
<string>Command</string>
</property>
<property name="toolTip">
<string>Command/script to execute when a match is made</string>
</property>
</column>
<column>
<property name="text">
<string>Highlight</string>
</property>
</column>
<column>
<property name="text">
<string>Plot on Map</string>
</property>
</column>
</widget>
</item>
<item row="5" column="0">
<layout class="QHBoxLayout" name="buttonsHorizontalLayout">
<item>
<widget class="QToolButton" name="add">
<property name="toolTip">
<string>Add device set control</string>
</property>
<property name="text">
<string>+</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="remove">
<property name="toolTip">
<string>Remove device set control</string>
</property>
<property name="text">
<string>-</string>
</property>
</widget>
</item>
<item>
<spacer name="buttonsHorizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>table</tabstop>
<tabstop>add</tabstop>
<tabstop>remove</tabstop>
</tabstops>
<resources>
<include location="../../../sdrgui/resources/res.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>PagerDemodNotificationDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>PagerDemodNotificationDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -20,10 +20,12 @@
#include <QColor>
#include <QDataStream>
#include <QDebug>
#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<qint32
(*stream) >> 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;
}

View File

@ -23,6 +23,7 @@
#include <QByteArray>
#include <QString>
#include <QRegularExpression>
#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<NotificationSettings *> 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

View File

@ -1,4 +1,4 @@
<h1>Pager demodulator plugin</h1>
<h1>Pager demodulator plugin</h1>
<h2>Introduction</h2>
@ -86,6 +86,36 @@ IP address of the host to forward received messages to via UDP.
UDP port number to forward received messages to.
<h3>Filter Duplicates</h3>
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.
<h3>Open Notifications Dialog</h3>
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°4046″N 95°4826″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/).
<h3>15: Start/stop Logging Messages to .csv File</h3>
When checked, writes all received messages to a .csv file.

View File

@ -25,6 +25,7 @@
<file>map/airport_small.png</file>
<file>map/heliport.png</file>
<file>map/waypoint.png</file>
<file>map/pager.png</file>
<file>map/map3d.html</file>
<file>data/transmitters.csv</file>
</qresource>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -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));

View File

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

View File

@ -167,3 +167,11 @@ QHash<QString, int> 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;
}

View File

@ -52,6 +52,8 @@ struct SDRBASE_API CSV {
static bool readRow(QTextStream &in, QStringList *row, char seperator=',');
static QHash<QString, int> readHeader(QTextStream &in, QStringList requiredColumns, QString &error, char seperator=',');
static QString escape(const QString& string);
};
#endif /* INCLUDE_CSV_H */

View File

@ -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())
{