1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2024-11-26 01:39:05 -05:00

Merge CSV code in to a single file.

This commit is contained in:
Jon Beniston 2021-11-04 12:33:43 +00:00
parent 0141639b7a
commit 9f2cb0c29c
44 changed files with 1155 additions and 189 deletions

View File

@ -42,7 +42,6 @@ if(NOT SERVER_MODE)
adsbdemoddisplaydialog.ui adsbdemoddisplaydialog.ui
adsbdemodnotificationdialog.cpp adsbdemodnotificationdialog.cpp
adsbdemodnotificationdialog.ui adsbdemodnotificationdialog.ui
csv.cpp
airlinelogos.qrc airlinelogos.qrc
flags.qrc flags.qrc
map.qrc map.qrc
@ -56,7 +55,6 @@ if(NOT SERVER_MODE)
adsbdemodnotificationdialog.h adsbdemodnotificationdialog.h
ourairports.h ourairports.h
osndb.h osndb.h
csv.h
) )
set(TARGET_NAME demodadsb) set(TARGET_NAME demodadsb)

View File

@ -2127,9 +2127,9 @@ ADSBDemodGUI::ADSBDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Baseb
if (m_airportInfo != nullptr) if (m_airportInfo != nullptr)
AirportInformation::readFrequenciesDB(getAirportFrequenciesDBFilename(), m_airportInfo); AirportInformation::readFrequenciesDB(getAirportFrequenciesDBFilename(), m_airportInfo);
// Read registration prefix to country map // Read registration prefix to country map
m_prefixMap = csvHash(":/flags/regprefixmap.csv"); m_prefixMap = CSV::hash(":/flags/regprefixmap.csv");
// Read operator air force to military map // Read operator air force to military map
m_militaryMap = csvHash(":/flags/militarymap.csv"); m_militaryMap = CSV::hash(":/flags/militarymap.csv");
// Get station position // Get station position
Real stationLatitude = MainCore::instance()->getSettings().getLatitude(); Real stationLatitude = MainCore::instance()->getSettings().getLatitude();
@ -2548,30 +2548,14 @@ void ADSBDemodGUI::on_logOpen_clicked()
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) if (file.open(QIODevice::ReadOnly | QIODevice::Text))
{ {
QTextStream in(&file); QTextStream in(&file);
QString header = in.readLine(); QString error;
QStringList colNames = header.split(","); QHash<QString, int> colIndexes = CSV::readHeader(in, {"Data", "Correlation"}, error);
int dateCol = colNames.indexOf("Date"); if (error.isEmpty())
int timeCol = colNames.indexOf("Time");
int dataCol = colNames.indexOf("Data");
int correlationCol = colNames.indexOf("Correlation");
if (dateCol == -1)
{
QMessageBox::critical(this, "ADS-B", QString(".csv file doesn't contain a column named 'Date'"));
}
else if (timeCol == -1)
{
QMessageBox::critical(this, "ADS-B", QString(".csv file doesn't contain a column named 'Time'"));
}
else if (dataCol == -1)
{
QMessageBox::critical(this, "ADS-B", QString(".csv file doesn't contain a column named 'Data'"));
}
else if (correlationCol == -1)
{
QMessageBox::critical(this, "ADS-B", QString(".csv file doesn't contain a column named 'Correlation'"));
}
else
{ {
int dataCol = colIndexes.value("Data");
int correlationCol = colIndexes.value("Correlation");
int maxCol = std::max(dataCol, correlationCol);
QMessageBox dialog(this); QMessageBox dialog(this);
dialog.setText("Reading ADS-B data"); dialog.setText("Reading ADS-B data");
dialog.addButton(QMessageBox::Cancel); dialog.addButton(QMessageBox::Cancel);
@ -2579,15 +2563,11 @@ void ADSBDemodGUI::on_logOpen_clicked()
QApplication::processEvents(); QApplication::processEvents();
int count = 0; int count = 0;
bool cancelled = false; bool cancelled = false;
while (!in.atEnd() && !cancelled) QStringList cols;
while (!cancelled && CSV::readRow(in, &cols))
{ {
QString row = in.readLine(); if (cols.size() > maxCol)
QStringList cols = row.split(",");
if (cols.size() >= dataCol)
{ {
//QDate date = QDate::fromString(cols[dateCol]);
//QTime time = QTime::fromString(cols[timeCol]);
//QDateTime dateTime(date, time);
QDateTime dateTime = QDateTime::currentDateTime(); // So they aren't removed immediately as too old QDateTime dateTime = QDateTime::currentDateTime(); // So they aren't removed immediately as too old
QByteArray bytes = QByteArray::fromHex(cols[dataCol].toLatin1()); QByteArray bytes = QByteArray::fromHex(cols[dataCol].toLatin1());
float correlation = cols[correlationCol].toFloat(); float correlation = cols[correlationCol].toFloat();
@ -2604,6 +2584,10 @@ void ADSBDemodGUI::on_logOpen_clicked()
} }
dialog.close(); dialog.close();
} }
else
{
QMessageBox::critical(this, "ADS-B", error);
}
} }
else else
{ {

View File

@ -1,69 +0,0 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2020 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 "csv.h"
#include <QString>
#include <QFile>
#include <QByteArray>
#include <QHash>
#include <QList>
#include <QDebug>
// Create a hash map from a CSV file with two columns
QHash<QString, QString> *csvHash(const QString& filename, int reserve)
{
int cnt = 0;
QHash<QString, QString> *map = nullptr;
qDebug() << "csvHash: " << filename;
QFile file(filename);
if (file.open(QIODevice::ReadOnly))
{
// Read header
if (!file.atEnd())
{
QByteArray row = file.readLine().trimmed();
if (row.split(',').size() == 2)
{
map = new QHash<QString, QString>();
if (reserve > 0)
map->reserve(reserve);
// Read data
while (!file.atEnd())
{
row = file.readLine().trimmed();
QList<QByteArray> cols = row.split(',');
map->insert(QString(cols[0]), QString(cols[1]));
cnt++;
}
}
else
qDebug() << "csvHash: Unexpected header";
}
else
qDebug() << "csvHash: Empty file";
file.close();
}
else
qDebug() << "csvHash: Failed to open " << filename;
qDebug() << "csvHash: " << filename << ": read " << cnt << " entries";
return map;
}

View File

@ -1,44 +0,0 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2020 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_CSV_H
#define INCLUDE_CSV_H
#include <QString>
#include <QHash>
// Extract string from CSV line, updating pp to next column
static inline char *csvNext(char **pp, char delimiter=',')
{
char *p = *pp;
if (p[0] == '\0')
return nullptr;
char *start = p;
while ((*p != delimiter) && (*p != '\n'))
p++;
*p++ = '\0';
*pp = p;
return start;
}
QHash<QString, QString> *csvHash(const QString& filename, int reserve=0);
#endif /* INCLUDE_CSV_H */

View File

@ -28,7 +28,7 @@
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
#include "csv.h" #include "util/csv.h"
#define OSNDB_URL "https://opensky-network.org/datasets/metadata/aircraftDatabase.csv" #define OSNDB_URL "https://opensky-network.org/datasets/metadata/aircraftDatabase.csv"

View File

@ -28,7 +28,7 @@
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
#include "csv.h" #include "util/csv.h"
#include "adsbdemodsettings.h" #include "adsbdemodsettings.h"
#define AIRPORTS_URL "https://ourairports.com/data/airports.csv" #define AIRPORTS_URL "https://ourairports.com/data/airports.csv"

View File

@ -173,7 +173,7 @@ Click to specify the name of the .csv file which received ADS-B frames are logge
<h3>Read Data from .csv File</h3> <h3>Read Data from .csv File</h3>
Click to specify a previously written ADS-B .csv log file, which is read and used to updated the ADS-B data table and map. Click to specify a previously written ADS-B .csv log file, which is read and used to update the ADS-B data table and map.
<h3>14: Refresh list of devices</h3> <h3>14: Refresh list of devices</h3>

View File

@ -179,6 +179,25 @@ bool AISDemod::handleMessage(const Message& cmd)
} }
} }
// Write to log file
if (m_logFile.isOpen())
{
AISMessage *ais;
// Decode the message
ais = AISMessage::decode(report.getMessage());
m_logStream << report.getDateTime().date().toString() << ","
<< report.getDateTime().time().toString() << ","
<< report.getMessage().toHex() << ","
<< QString("%1").arg(ais->m_mmsi, 9, 10, QChar('0')) << ","
<< ais->getType() << ","
<< "\"" << ais->toString() << "\"" << ","
<< "\"" << ais->toNMEA() << "\"" << "\n";
delete ais;
}
return true; return true;
} }
else if (MainCore::MsgChannelDemodQuery::match(cmd)) else if (MainCore::MsgChannelDemodQuery::match(cmd))
@ -202,6 +221,8 @@ ScopeVis *AISDemod::getScopeSink()
void AISDemod::applySettings(const AISDemodSettings& settings, bool force) void AISDemod::applySettings(const AISDemodSettings& settings, bool force)
{ {
qDebug() << "AISDemod::applySettings:" qDebug() << "AISDemod::applySettings:"
<< " m_logEnabled: " << settings.m_logEnabled
<< " m_logFilename: " << settings.m_logFilename
<< " m_streamIndex: " << settings.m_streamIndex << " m_streamIndex: " << settings.m_streamIndex
<< " m_useReverseAPI: " << settings.m_useReverseAPI << " m_useReverseAPI: " << settings.m_useReverseAPI
<< " m_reverseAPIAddress: " << settings.m_reverseAPIAddress << " m_reverseAPIAddress: " << settings.m_reverseAPIAddress
@ -236,6 +257,12 @@ void AISDemod::applySettings(const AISDemodSettings& settings, bool force)
if ((settings.m_udpFormat != m_settings.m_udpFormat) || force) { if ((settings.m_udpFormat != m_settings.m_udpFormat) || force) {
reverseAPIKeys.append("udpFormat"); reverseAPIKeys.append("udpFormat");
} }
if ((settings.m_logFilename != m_settings.m_logFilename) || force) {
reverseAPIKeys.append("logFilename");
}
if ((settings.m_logEnabled != m_settings.m_logEnabled) || force) {
reverseAPIKeys.append("logEnabled");
}
if (m_settings.m_streamIndex != settings.m_streamIndex) if (m_settings.m_streamIndex != settings.m_streamIndex)
{ {
if (m_deviceAPI->getSampleMIMO()) // change of stream is possible for MIMO devices only if (m_deviceAPI->getSampleMIMO()) // change of stream is possible for MIMO devices only
@ -262,6 +289,36 @@ void AISDemod::applySettings(const AISDemodSettings& settings, bool force)
webapiReverseSendSettings(reverseAPIKeys, settings, fullUpdate || force); webapiReverseSendSettings(reverseAPIKeys, settings, fullUpdate || force);
} }
if ((settings.m_logEnabled != m_settings.m_logEnabled)
|| (settings.m_logFilename != m_settings.m_logFilename)
|| force)
{
if (m_logFile.isOpen())
{
m_logStream.flush();
m_logFile.close();
}
if (settings.m_logEnabled && !settings.m_logFilename.isEmpty())
{
m_logFile.setFileName(settings.m_logFilename);
if (m_logFile.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text))
{
qDebug() << "AISDemod::applySettings - Logging to: " << settings.m_logFilename;
bool newFile = m_logFile.size() == 0;
m_logStream.setDevice(&m_logFile);
if (newFile)
{
// Write header
m_logStream << "Date,Time,Data,MMSI,Type,Message,NMEA\n";
}
}
else
{
qDebug() << "AISDemod::applySettings - Unable to open log file: " << settings.m_logFilename;
}
}
}
m_settings = settings; m_settings = settings;
} }
@ -371,6 +428,12 @@ void AISDemod::webapiUpdateChannelSettings(
if (channelSettingsKeys.contains("udpFormat")) { if (channelSettingsKeys.contains("udpFormat")) {
settings.m_udpFormat = (AISDemodSettings::UDPFormat)response.getAisDemodSettings()->getUdpFormat(); settings.m_udpFormat = (AISDemodSettings::UDPFormat)response.getAisDemodSettings()->getUdpFormat();
} }
if (channelSettingsKeys.contains("logFilename")) {
settings.m_logFilename = *response.getAisDemodSettings()->getLogFilename();
}
if (channelSettingsKeys.contains("logEnabled")) {
settings.m_logEnabled = response.getAisDemodSettings()->getLogEnabled();
}
if (channelSettingsKeys.contains("rgbColor")) { if (channelSettingsKeys.contains("rgbColor")) {
settings.m_rgbColor = response.getAisDemodSettings()->getRgbColor(); settings.m_rgbColor = response.getAisDemodSettings()->getRgbColor();
} }
@ -407,6 +470,8 @@ void AISDemod::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& resp
response.getAisDemodSettings()->setUdpAddress(new QString(settings.m_udpAddress)); response.getAisDemodSettings()->setUdpAddress(new QString(settings.m_udpAddress));
response.getAisDemodSettings()->setUdpPort(settings.m_udpPort); response.getAisDemodSettings()->setUdpPort(settings.m_udpPort);
response.getAisDemodSettings()->setUdpFormat((int)settings.m_udpFormat); response.getAisDemodSettings()->setUdpFormat((int)settings.m_udpFormat);
response.getAisDemodSettings()->setLogFilename(new QString(settings.m_logFilename));
response.getAisDemodSettings()->setLogEnabled(settings.m_logEnabled);
response.getAisDemodSettings()->setRgbColor(settings.m_rgbColor); response.getAisDemodSettings()->setRgbColor(settings.m_rgbColor);
if (response.getAisDemodSettings()->getTitle()) { if (response.getAisDemodSettings()->getTitle()) {
@ -494,6 +559,12 @@ void AISDemod::webapiFormatChannelSettings(
if (channelSettingsKeys.contains("udpFormat") || force) { if (channelSettingsKeys.contains("udpFormat") || force) {
swgAISDemodSettings->setUdpPort((int)settings.m_udpFormat); swgAISDemodSettings->setUdpPort((int)settings.m_udpFormat);
} }
if (channelSettingsKeys.contains("logFilename") || force) {
swgAISDemodSettings->setLogFilename(new QString(settings.m_logFilename));
}
if (channelSettingsKeys.contains("logEnabled") || force) {
swgAISDemodSettings->setLogEnabled(settings.m_logEnabled);
}
if (channelSettingsKeys.contains("rgbColor") || force) { if (channelSettingsKeys.contains("rgbColor") || force) {
swgAISDemodSettings->setRgbColor(settings.m_rgbColor); swgAISDemodSettings->setRgbColor(settings.m_rgbColor);
} }

View File

@ -25,6 +25,8 @@
#include <QUdpSocket> #include <QUdpSocket>
#include <QThread> #include <QThread>
#include <QDateTime> #include <QDateTime>
#include <QFile>
#include <QTextStream>
#include "dsp/basebandsamplesink.h" #include "dsp/basebandsamplesink.h"
#include "channel/channelapi.h" #include "channel/channelapi.h"
@ -160,6 +162,8 @@ private:
int m_basebandSampleRate; //!< stored from device message used when starting baseband sink int m_basebandSampleRate; //!< stored from device message used when starting baseband sink
qint64 m_centerFrequency; qint64 m_centerFrequency;
QUdpSocket m_udpSocket; QUdpSocket m_udpSocket;
QFile m_logFile;
QTextStream m_logStream;
QNetworkAccessManager *m_networkManager; QNetworkAccessManager *m_networkManager;
QNetworkRequest m_networkRequest; QNetworkRequest m_networkRequest;

View File

@ -26,6 +26,7 @@
#include <QAction> #include <QAction>
#include <QRegExp> #include <QRegExp>
#include <QClipboard> #include <QClipboard>
#include <QFileDialog>
#include "aisdemodgui.h" #include "aisdemodgui.h"
@ -36,6 +37,7 @@
#include "plugin/pluginapi.h" #include "plugin/pluginapi.h"
#include "util/simpleserializer.h" #include "util/simpleserializer.h"
#include "util/ais.h" #include "util/ais.h"
#include "util/csv.h"
#include "util/db.h" #include "util/db.h"
#include "gui/basicchannelsettingsdialog.h" #include "gui/basicchannelsettingsdialog.h"
#include "gui/devicestreamselectiondialog.h" #include "gui/devicestreamselectiondialog.h"
@ -49,8 +51,6 @@
#include "aisdemod.h" #include "aisdemod.h"
#include "aisdemodsink.h" #include "aisdemodsink.h"
#include "SWGMapItem.h"
void AISDemodGUI::resizeTable() void AISDemodGUI::resizeTable()
{ {
// Fill table with a row of dummy data that will size the columns nicely // Fill table with a row of dummy data that will size the columns nicely
@ -150,12 +150,12 @@ bool AISDemodGUI::deserialize(const QByteArray& data)
} }
// Add row to table // Add row to table
void AISDemodGUI::messageReceived(const AISDemod::MsgMessage& message) void AISDemodGUI::messageReceived(const QByteArray& message, const QDateTime& dateTime)
{ {
AISMessage *ais; AISMessage *ais;
// Decode the message // Decode the message
ais = AISMessage::decode(message.getMessage()); ais = AISMessage::decode(message);
// Add to messages table // Add to messages table
ui->messages->setSortingEnabled(false); ui->messages->setSortingEnabled(false);
@ -176,8 +176,8 @@ void AISDemodGUI::messageReceived(const AISDemod::MsgMessage& message)
ui->messages->setItem(row, MESSAGE_COL_DATA, dataItem); ui->messages->setItem(row, MESSAGE_COL_DATA, dataItem);
ui->messages->setItem(row, MESSAGE_COL_NMEA, nmeaItem); ui->messages->setItem(row, MESSAGE_COL_NMEA, nmeaItem);
ui->messages->setItem(row, MESSAGE_COL_HEX, hexItem); ui->messages->setItem(row, MESSAGE_COL_HEX, hexItem);
dateItem->setText(message.getDateTime().date().toString()); dateItem->setText(dateTime.date().toString());
timeItem->setText(message.getDateTime().time().toString()); timeItem->setText(dateTime.time().toString());
mmsiItem->setText(QString("%1").arg(ais->m_mmsi, 9, 10, QChar('0'))); mmsiItem->setText(QString("%1").arg(ais->m_mmsi, 9, 10, QChar('0')));
typeItem->setText(ais->getType()); typeItem->setText(ais->getType());
dataItem->setText(ais->toString()); dataItem->setText(ais->toString());
@ -186,6 +186,8 @@ void AISDemodGUI::messageReceived(const AISDemod::MsgMessage& message)
ui->messages->setSortingEnabled(true); ui->messages->setSortingEnabled(true);
ui->messages->scrollToItem(dateItem); // Will only scroll if not hidden ui->messages->scrollToItem(dateItem); // Will only scroll if not hidden
filterRow(row); filterRow(row);
delete ais;
} }
bool AISDemodGUI::handleMessage(const Message& message) bool AISDemodGUI::handleMessage(const Message& message)
@ -203,7 +205,7 @@ bool AISDemodGUI::handleMessage(const Message& message)
else if (AISDemod::MsgMessage::match(message)) else if (AISDemod::MsgMessage::match(message))
{ {
AISDemod::MsgMessage& report = (AISDemod::MsgMessage&) message; AISDemod::MsgMessage& report = (AISDemod::MsgMessage&) message;
messageReceived(report); messageReceived(report.getMessage(), report.getDateTime());
return true; return true;
} }
@ -579,6 +581,9 @@ void AISDemodGUI::displaySettings()
ui->channel1->setCurrentIndex(m_settings.m_scopeCh1); ui->channel1->setCurrentIndex(m_settings.m_scopeCh1);
ui->channel2->setCurrentIndex(m_settings.m_scopeCh2); ui->channel2->setCurrentIndex(m_settings.m_scopeCh2);
ui->logFilename->setToolTip(QString(".csv log filename: %1").arg(m_settings.m_logFilename));
ui->logEnable->setChecked(m_settings.m_logEnabled);
// Order and size columns // Order and size columns
QHeaderView *header = ui->messages->horizontalHeader(); QHeaderView *header = ui->messages->horizontalHeader();
for (int i = 0; i < AISDEMOD_MESSAGE_COLUMNS; i++) for (int i = 0; i < AISDEMOD_MESSAGE_COLUMNS; i++)
@ -633,3 +638,108 @@ void AISDemodGUI::tick()
m_tickCount++; m_tickCount++;
} }
void AISDemodGUI::on_logEnable_clicked(bool checked)
{
m_settings.m_logEnabled = checked;
applySettings();
}
void AISDemodGUI::on_logFilename_clicked()
{
// Get filename to save to
QFileDialog fileDialog(nullptr, "Select file to log received frames to", "", "*.csv");
fileDialog.setAcceptMode(QFileDialog::AcceptSave);
if (fileDialog.exec())
{
QStringList fileNames = fileDialog.selectedFiles();
if (fileNames.size() > 0)
{
m_settings.m_logFilename = fileNames[0];
ui->logFilename->setToolTip(QString(".csv log filename: %1").arg(m_settings.m_logFilename));
applySettings();
}
}
}
// Read .csv log and process as received frames
void AISDemodGUI::on_logOpen_clicked()
{
QFileDialog fileDialog(nullptr, "Select .csv log file to read", "", "*.csv");
if (fileDialog.exec())
{
QStringList fileNames = fileDialog.selectedFiles();
if (fileNames.size() > 0)
{
QFile file(fileNames[0]);
if (file.open(QIODevice::ReadOnly | QIODevice::Text))
{
QTextStream in(&file);
QString error;
QHash<QString, int> colIndexes = CSV::readHeader(in, {"Date", "Time", "Data"}, error);
if (error.isEmpty())
{
int dateCol = colIndexes.value("Date");
int timeCol = colIndexes.value("Time");
int dataCol = colIndexes.value("Data");
int maxCol = std::max({dateCol, timeCol, dataCol});
QMessageBox dialog(this);
dialog.setText("Reading messages");
dialog.addButton(QMessageBox::Cancel);
dialog.show();
QApplication::processEvents();
int count = 0;
bool cancelled = false;
QStringList cols;
MessagePipes& messagePipes = MainCore::instance()->getMessagePipes();
QList<MessageQueue*> *aisMessageQueues = messagePipes.getMessageQueues(m_aisDemod, "ais");
while (!cancelled && CSV::readRow(in, &cols))
{
if (cols.size() > maxCol)
{
QDate date = QDate::fromString(cols[dateCol]);
QTime time = QTime::fromString(cols[timeCol]);
QDateTime dateTime(date, time);
QByteArray bytes = QByteArray::fromHex(cols[dataCol].toLatin1());
// Add to table
messageReceived(bytes, dateTime);
// Forward to AIS feature
if (aisMessageQueues)
{
QList<MessageQueue*>::iterator it = aisMessageQueues->begin();
for (; it != aisMessageQueues->end(); ++it)
{
MainCore::MsgPacket *msg = MainCore::MsgPacket::create(m_aisDemod, bytes, dateTime);
(*it)->push(msg);
}
}
if (count % 1000 == 0)
{
QApplication::processEvents();
if (dialog.clickedButton()) {
cancelled = true;
}
}
count++;
}
}
dialog.close();
}
else
{
QMessageBox::critical(this, "AIS Demod", error);
}
}
else
{
QMessageBox::critical(this, "AIS Demod", QString("Failed to open file %1").arg(fileNames[0]));
}
}
}
}

View File

@ -85,7 +85,7 @@ private:
void applySettings(bool force = false); void applySettings(bool force = false);
void displaySettings(); void displaySettings();
void displayStreamIndex(); void displayStreamIndex();
void messageReceived(const AISDemod::MsgMessage& message); void messageReceived(const QByteArray& message, const QDateTime& dateTime);
bool handleMessage(const Message& message); bool handleMessage(const Message& message);
void leaveEvent(QEvent*); void leaveEvent(QEvent*);
@ -118,6 +118,9 @@ private slots:
void on_channel1_currentIndexChanged(int index); void on_channel1_currentIndexChanged(int index);
void on_channel2_currentIndexChanged(int index); void on_channel2_currentIndexChanged(int index);
void on_messages_cellDoubleClicked(int row, int column); void on_messages_cellDoubleClicked(int row, int column);
void on_logEnable_clicked(bool checked=false);
void on_logFilename_clicked();
void on_logOpen_clicked();
void filterRow(int row); void filterRow(int row);
void filter(); void filter();
void messages_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex); void messages_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex);

View File

@ -560,6 +560,60 @@
</property> </property>
</spacer> </spacer>
</item> </item>
<item>
<widget class="ButtonSwitch" name="logEnable">
<property name="maximumSize">
<size>
<width>24</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Start/stop logging of received messages to .csv file</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/record_off.png</normaloff>:/record_off.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="logFilename">
<property name="toolTip">
<string>Set log .csv filename</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/save.png</normaloff>:/save.png</iconset>
</property>
<property name="checkable">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="logOpen">
<property name="toolTip">
<string>Read data from .csv log file</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/load.png</normaloff>:/load.png</iconset>
</property>
<property name="checkable">
<bool>false</bool>
</property>
</widget>
</item>
<item> <item>
<widget class="QPushButton" name="clearTable"> <widget class="QPushButton" name="clearTable">
<property name="toolTip"> <property name="toolTip">
@ -878,6 +932,11 @@
<header>gui/levelmeter.h</header> <header>gui/levelmeter.h</header>
<container>1</container> <container>1</container>
</customwidget> </customwidget>
<customwidget>
<class>ButtonSwitch</class>
<extends>QToolButton</extends>
<header>gui/buttonswitch.h</header>
</customwidget>
<customwidget> <customwidget>
<class>GLScope</class> <class>GLScope</class>
<extends>QWidget</extends> <extends>QWidget</extends>
@ -893,6 +952,7 @@
</customwidgets> </customwidgets>
<resources> <resources>
<include location="../../../sdrgui/resources/res.qrc"/> <include location="../../../sdrgui/resources/res.qrc"/>
<include location="../demodadsb/icons.qrc"/>
</resources> </resources>
<connections/> <connections/>
</ui> </ui>

View File

@ -44,6 +44,8 @@ void AISDemodSettings::resetToDefaults()
m_udpFormat = Binary; m_udpFormat = Binary;
m_scopeCh1 = 5; m_scopeCh1 = 5;
m_scopeCh2 = 6; m_scopeCh2 = 6;
m_logFilename = "ais_log.csv";
m_logEnabled = false;
m_rgbColor = QColor(102, 0, 0).rgb(); m_rgbColor = QColor(102, 0, 0).rgb();
m_title = "AIS Demodulator"; m_title = "AIS Demodulator";
m_streamIndex = 0; m_streamIndex = 0;
@ -88,6 +90,9 @@ QByteArray AISDemodSettings::serialize() const
s.writeU32(20, m_reverseAPIChannelIndex); s.writeU32(20, m_reverseAPIChannelIndex);
s.writeBlob(21, m_scopeGUI->serialize()); s.writeBlob(21, m_scopeGUI->serialize());
s.writeString(22, m_logFilename);
s.writeBool(23, m_logEnabled);
for (int i = 0; i < AISDEMOD_MESSAGE_COLUMNS; i++) for (int i = 0; i < AISDEMOD_MESSAGE_COLUMNS; i++)
s.writeS32(100 + i, m_messageColumnIndexes[i]); s.writeS32(100 + i, m_messageColumnIndexes[i]);
for (int i = 0; i < AISDEMOD_MESSAGE_COLUMNS; i++) for (int i = 0; i < AISDEMOD_MESSAGE_COLUMNS; i++)
@ -154,6 +159,9 @@ bool AISDemodSettings::deserialize(const QByteArray& data)
m_scopeGUI->deserialize(bytetmp); m_scopeGUI->deserialize(bytetmp);
} }
d.readString(22, &m_logFilename, "ais_log.csv");
d.readBool(23, &m_logEnabled, false);
for (int i = 0; i < AISDEMOD_MESSAGE_COLUMNS; i++) for (int i = 0; i < AISDEMOD_MESSAGE_COLUMNS; i++)
d.readS32(100 + i, &m_messageColumnIndexes[i], i); d.readS32(100 + i, &m_messageColumnIndexes[i], i);
for (int i = 0; i < AISDEMOD_MESSAGE_COLUMNS; i++) for (int i = 0; i < AISDEMOD_MESSAGE_COLUMNS; i++)

View File

@ -47,6 +47,9 @@ struct AISDemodSettings
int m_scopeCh1; int m_scopeCh1;
int m_scopeCh2; int m_scopeCh2;
QString m_logFilename;
bool m_logEnabled;
quint32 m_rgbColor; quint32 m_rgbColor;
QString m_title; QString m_title;
Serializable *m_channelMarker; Serializable *m_channelMarker;

View File

@ -68,6 +68,18 @@ UDP port number to forward received messages to.
The format the messages are forwared via UDP in. This can be either binary (which is useful for SDRangel's PERTester feature) or NMEA (which is useful for 3rd party applications such as OpenCPN). The format the messages are forwared via UDP in. This can be either binary (which is useful for SDRangel's PERTester feature) or NMEA (which is useful for 3rd party applications such as OpenCPN).
<h3>13: Start/stop Logging Messages to .csv File</h3>
When checked, writes all received AIS messages to a .csv file.
<h3>14: .csv Log Filename</h3>
Click to specify the name of the .csv file which received AIS messages are logged to.
<h3>15: Read Data from .csv File</h3>
Click to specify a previously written AIS .csv log file, which is read and used to update the table.
<h3>Received Messages Table</h3> <h3>Received Messages Table</h3>
The received messages table displays information about each AIS message received. Only messages with valid CRCs are displayed. The received messages table displays information about each AIS message received. Only messages with valid CRCs are displayed.

View File

@ -37,6 +37,7 @@
#include "dsp/dspcommands.h" #include "dsp/dspcommands.h"
#include "device/deviceapi.h" #include "device/deviceapi.h"
#include "feature/feature.h" #include "feature/feature.h"
#include "util/ax25.h"
#include "util/db.h" #include "util/db.h"
#include "maincore.h" #include "maincore.h"
@ -176,6 +177,32 @@ bool PacketDemod::handleMessage(const Message& cmd)
QHostAddress(m_settings.m_udpAddress), m_settings.m_udpPort); QHostAddress(m_settings.m_udpAddress), m_settings.m_udpPort);
} }
// Write to log file
if (m_logFile.isOpen())
{
AX25Packet ax25;
if (ax25.decode(report.getPacket()))
{
m_logStream << report.getDateTime().date().toString() << ","
<< report.getDateTime().time().toString() << ","
<< report.getPacket().toHex() << ","
<< "\"" << ax25.m_from << "\","
<< "\"" << ax25.m_to << "\","
<< "\"" << ax25.m_via << "\","
<< ax25.m_type << ","
<< ax25.m_pid << ","
<< "\"" << ax25.m_dataASCII << "\","
<< "\"" << ax25.m_dataHex << "\"\n";
}
else
{
m_logStream << report.getDateTime().date().toString() << ","
<< report.getDateTime().time().toString() << ","
<< report.getPacket().toHex() << "\n";
}
}
return true; return true;
} }
else if (MainCore::MsgChannelDemodQuery::match(cmd)) else if (MainCore::MsgChannelDemodQuery::match(cmd))
@ -194,6 +221,8 @@ bool PacketDemod::handleMessage(const Message& cmd)
void PacketDemod::applySettings(const PacketDemodSettings& settings, bool force) void PacketDemod::applySettings(const PacketDemodSettings& settings, bool force)
{ {
qDebug() << "PacketDemod::applySettings:" qDebug() << "PacketDemod::applySettings:"
<< " m_logEnabled: " << settings.m_logEnabled
<< " m_logFilename: " << settings.m_logFilename
<< " m_streamIndex: " << settings.m_streamIndex << " m_streamIndex: " << settings.m_streamIndex
<< " m_useReverseAPI: " << settings.m_useReverseAPI << " m_useReverseAPI: " << settings.m_useReverseAPI
<< " m_reverseAPIAddress: " << settings.m_reverseAPIAddress << " m_reverseAPIAddress: " << settings.m_reverseAPIAddress
@ -222,6 +251,12 @@ void PacketDemod::applySettings(const PacketDemodSettings& settings, bool force)
if ((settings.m_udpPort != m_settings.m_udpPort) || force) { if ((settings.m_udpPort != m_settings.m_udpPort) || force) {
reverseAPIKeys.append("udpPort"); reverseAPIKeys.append("udpPort");
} }
if ((settings.m_logFilename != m_settings.m_logFilename) || force) {
reverseAPIKeys.append("logFilename");
}
if ((settings.m_logEnabled != m_settings.m_logEnabled) || force) {
reverseAPIKeys.append("logEnabled");
}
if (m_settings.m_streamIndex != settings.m_streamIndex) if (m_settings.m_streamIndex != settings.m_streamIndex)
{ {
if (m_deviceAPI->getSampleMIMO()) // change of stream is possible for MIMO devices only if (m_deviceAPI->getSampleMIMO()) // change of stream is possible for MIMO devices only
@ -248,6 +283,36 @@ void PacketDemod::applySettings(const PacketDemodSettings& settings, bool force)
webapiReverseSendSettings(reverseAPIKeys, settings, fullUpdate || force); webapiReverseSendSettings(reverseAPIKeys, settings, fullUpdate || force);
} }
if ((settings.m_logEnabled != m_settings.m_logEnabled)
|| (settings.m_logFilename != m_settings.m_logFilename)
|| force)
{
if (m_logFile.isOpen())
{
m_logStream.flush();
m_logFile.close();
}
if (settings.m_logEnabled && !settings.m_logFilename.isEmpty())
{
m_logFile.setFileName(settings.m_logFilename);
if (m_logFile.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text))
{
qDebug() << "PacketDemod::applySettings - Logging to: " << settings.m_logFilename;
bool newFile = m_logFile.size() == 0;
m_logStream.setDevice(&m_logFile);
if (newFile)
{
// Write header
m_logStream << "Date,Time,Data,From,To,Via,Type,PID,Data ASCII,Data Hex\n";
}
}
else
{
qDebug() << "PacketDemod::applySettings - Unable to open log file: " << settings.m_logFilename;
}
}
}
m_settings = settings; m_settings = settings;
} }
@ -351,6 +416,12 @@ void PacketDemod::webapiUpdateChannelSettings(
if (channelSettingsKeys.contains("udpPort")) { if (channelSettingsKeys.contains("udpPort")) {
settings.m_udpPort = response.getPacketDemodSettings()->getUdpPort(); settings.m_udpPort = response.getPacketDemodSettings()->getUdpPort();
} }
if (channelSettingsKeys.contains("logFilename")) {
settings.m_logFilename = *response.getAdsbDemodSettings()->getLogFilename();
}
if (channelSettingsKeys.contains("logEnabled")) {
settings.m_logEnabled = response.getAdsbDemodSettings()->getLogEnabled();
}
if (channelSettingsKeys.contains("rgbColor")) { if (channelSettingsKeys.contains("rgbColor")) {
settings.m_rgbColor = response.getPacketDemodSettings()->getRgbColor(); settings.m_rgbColor = response.getPacketDemodSettings()->getRgbColor();
} }
@ -385,6 +456,8 @@ void PacketDemod::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& r
response.getPacketDemodSettings()->setUdpEnabled(settings.m_udpEnabled); response.getPacketDemodSettings()->setUdpEnabled(settings.m_udpEnabled);
response.getPacketDemodSettings()->setUdpAddress(new QString(settings.m_udpAddress)); response.getPacketDemodSettings()->setUdpAddress(new QString(settings.m_udpAddress));
response.getPacketDemodSettings()->setUdpPort(settings.m_udpPort); response.getPacketDemodSettings()->setUdpPort(settings.m_udpPort);
response.getPacketDemodSettings()->setLogFilename(new QString(settings.m_logFilename));
response.getPacketDemodSettings()->setLogEnabled(settings.m_logEnabled);
response.getPacketDemodSettings()->setRgbColor(settings.m_rgbColor); response.getPacketDemodSettings()->setRgbColor(settings.m_rgbColor);
if (response.getPacketDemodSettings()->getTitle()) { if (response.getPacketDemodSettings()->getTitle()) {
@ -466,6 +539,12 @@ void PacketDemod::webapiFormatChannelSettings(
if (channelSettingsKeys.contains("udpPort") || force) { if (channelSettingsKeys.contains("udpPort") || force) {
swgPacketDemodSettings->setUdpPort(settings.m_udpPort); swgPacketDemodSettings->setUdpPort(settings.m_udpPort);
} }
if (channelSettingsKeys.contains("logFilename") || force) {
swgPacketDemodSettings->setLogFilename(new QString(settings.m_logFilename));
}
if (channelSettingsKeys.contains("logEnabled") || force) {
swgPacketDemodSettings->setLogEnabled(settings.m_logEnabled);
}
if (channelSettingsKeys.contains("rgbColor") || force) { if (channelSettingsKeys.contains("rgbColor") || force) {
swgPacketDemodSettings->setRgbColor(settings.m_rgbColor); swgPacketDemodSettings->setRgbColor(settings.m_rgbColor);
} }

View File

@ -24,6 +24,8 @@
#include <QNetworkRequest> #include <QNetworkRequest>
#include <QUdpSocket> #include <QUdpSocket>
#include <QThread> #include <QThread>
#include <QFile>
#include <QTextStream>
#include "dsp/basebandsamplesink.h" #include "dsp/basebandsamplesink.h"
#include "channel/channelapi.h" #include "channel/channelapi.h"
@ -133,6 +135,8 @@ private:
int m_basebandSampleRate; //!< stored from device message used when starting baseband sink int m_basebandSampleRate; //!< stored from device message used when starting baseband sink
qint64 m_centerFrequency; qint64 m_centerFrequency;
QUdpSocket m_udpSocket; QUdpSocket m_udpSocket;
QFile m_logFile;
QTextStream m_logStream;
QNetworkAccessManager *m_networkManager; QNetworkAccessManager *m_networkManager;
QNetworkRequest m_networkRequest; QNetworkRequest m_networkRequest;

View File

@ -24,6 +24,7 @@
#include <QMessageBox> #include <QMessageBox>
#include <QAction> #include <QAction>
#include <QRegExp> #include <QRegExp>
#include <QFileDialog>
#include "packetdemodgui.h" #include "packetdemodgui.h"
#include "util/ax25.h" #include "util/ax25.h"
@ -34,6 +35,7 @@
#include "ui_packetdemodgui.h" #include "ui_packetdemodgui.h"
#include "plugin/pluginapi.h" #include "plugin/pluginapi.h"
#include "util/simpleserializer.h" #include "util/simpleserializer.h"
#include "util/csv.h"
#include "util/db.h" #include "util/db.h"
#include "util/morse.h" #include "util/morse.h"
#include "util/units.h" #include "util/units.h"
@ -521,6 +523,9 @@ void PacketDemodGUI::displaySettings()
ui->udpAddress->setText(m_settings.m_udpAddress); ui->udpAddress->setText(m_settings.m_udpAddress);
ui->udpPort->setText(QString::number(m_settings.m_udpPort)); ui->udpPort->setText(QString::number(m_settings.m_udpPort));
ui->logFilename->setToolTip(QString(".csv log filename: %1").arg(m_settings.m_logFilename));
ui->logEnable->setChecked(m_settings.m_logEnabled);
// Order and size columns // Order and size columns
QHeaderView *header = ui->packets->horizontalHeader(); QHeaderView *header = ui->packets->horizontalHeader();
for (int i = 0; i < PACKETDEMOD_COLUMNS; i++) for (int i = 0; i < PACKETDEMOD_COLUMNS; i++)
@ -576,3 +581,90 @@ void PacketDemodGUI::tick()
m_tickCount++; m_tickCount++;
} }
void PacketDemodGUI::on_logEnable_clicked(bool checked)
{
m_settings.m_logEnabled = checked;
applySettings();
}
void PacketDemodGUI::on_logFilename_clicked()
{
// Get filename to save to
QFileDialog fileDialog(nullptr, "Select file to log received frames to", "", "*.csv");
fileDialog.setAcceptMode(QFileDialog::AcceptSave);
if (fileDialog.exec())
{
QStringList fileNames = fileDialog.selectedFiles();
if (fileNames.size() > 0)
{
m_settings.m_logFilename = fileNames[0];
ui->logFilename->setToolTip(QString(".csv log filename: %1").arg(m_settings.m_logFilename));
applySettings();
}
}
}
// Read .csv log and process as received frames
void PacketDemodGUI::on_logOpen_clicked()
{
QFileDialog fileDialog(nullptr, "Select .csv log file to read", "", "*.csv");
if (fileDialog.exec())
{
QStringList fileNames = fileDialog.selectedFiles();
if (fileNames.size() > 0)
{
QFile file(fileNames[0]);
if (file.open(QIODevice::ReadOnly | QIODevice::Text))
{
QTextStream in(&file);
QString error;
QHash<QString, int> colIndexes = CSV::readHeader(in, {"Date", "Time", "Data"}, error);
if (error.isEmpty())
{
int dateCol = colIndexes.value("Date");
int timeCol = colIndexes.value("Time");
int dataCol = colIndexes.value("Data");
int maxCol = std::max({dateCol, timeCol, dataCol});
QMessageBox dialog(this);
dialog.setText("Reading packet data");
dialog.addButton(QMessageBox::Cancel);
dialog.show();
QApplication::processEvents();
int count = 0;
bool cancelled = false;
QStringList cols;
while (!cancelled && CSV::readRow(in, &cols))
{
if (cols.size() > maxCol)
{
QDate date = QDate::fromString(cols[dateCol]);
QTime time = QTime::fromString(cols[timeCol]);
QDateTime dateTime(date, time);
QByteArray bytes = QByteArray::fromHex(cols[dataCol].toLatin1());
packetReceived(bytes);
if (count % 1000 == 0)
{
QApplication::processEvents();
if (dialog.clickedButton()) {
cancelled = true;
}
}
count++;
}
}
dialog.close();
}
else
{
QMessageBox::critical(this, "Packet Demod", error);
}
}
else
{
QMessageBox::critical(this, "Packet Demod", QString("Failed to open file %1").arg(fileNames[0]));
}
}
}
}

View File

@ -105,6 +105,9 @@ private slots:
void on_udpEnabled_clicked(bool checked); void on_udpEnabled_clicked(bool checked);
void on_udpAddress_editingFinished(); void on_udpAddress_editingFinished();
void on_udpPort_editingFinished(); void on_udpPort_editingFinished();
void on_logEnable_clicked(bool checked=false);
void on_logFilename_clicked();
void on_logOpen_clicked();
void filterRow(int row); void filterRow(int row);
void filter(); void filter();
void packets_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex); void packets_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex);

View File

@ -43,7 +43,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>390</width> <width>390</width>
<height>131</height> <height>151</height>
</rect> </rect>
</property> </property>
<property name="minimumSize"> <property name="minimumSize">
@ -522,7 +522,7 @@
<string>Check to display only packets with PID set to No L3 (f0). This is typically used for APRS and BBS packets.</string> <string>Check to display only packets with PID set to No L3 (f0). This is typically used for APRS and BBS packets.</string>
</property> </property>
<property name="text"> <property name="text">
<string>PID No L3</string> <string>No L3</string>
</property> </property>
</widget> </widget>
</item> </item>
@ -539,6 +539,60 @@
</property> </property>
</spacer> </spacer>
</item> </item>
<item>
<widget class="ButtonSwitch" name="logEnable">
<property name="maximumSize">
<size>
<width>24</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Start/stop logging of received packets to .csv file</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/record_off.png</normaloff>:/record_off.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="logFilename">
<property name="toolTip">
<string>Set log .csv filename</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/save.png</normaloff>:/save.png</iconset>
</property>
<property name="checkable">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="logOpen">
<property name="toolTip">
<string>Read data from .csv log file</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/load.png</normaloff>:/load.png</iconset>
</property>
<property name="checkable">
<bool>false</bool>
</property>
</widget>
</item>
<item> <item>
<widget class="QPushButton" name="clearTable"> <widget class="QPushButton" name="clearTable">
<property name="toolTip"> <property name="toolTip">
@ -561,7 +615,7 @@
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>0</x> <x>0</x>
<y>140</y> <y>170</y>
<width>391</width> <width>391</width>
<height>261</height> <height>261</height>
</rect> </rect>
@ -679,6 +733,11 @@
<header>gui/levelmeter.h</header> <header>gui/levelmeter.h</header>
<container>1</container> <container>1</container>
</customwidget> </customwidget>
<customwidget>
<class>ButtonSwitch</class>
<extends>QToolButton</extends>
<header>gui/buttonswitch.h</header>
</customwidget>
</customwidgets> </customwidgets>
<tabstops> <tabstops>
<tabstop>packets</tabstop> <tabstop>packets</tabstop>

View File

@ -41,6 +41,8 @@ void PacketDemodSettings::resetToDefaults()
m_udpEnabled = false; m_udpEnabled = false;
m_udpAddress = "127.0.0.1"; m_udpAddress = "127.0.0.1";
m_udpPort = 9999; m_udpPort = 9999;
m_logFilename = "packet_log.csv";
m_logEnabled = false;
m_rgbColor = QColor(0, 105, 2).rgb(); m_rgbColor = QColor(0, 105, 2).rgb();
m_title = "Packet Demodulator"; m_title = "Packet Demodulator";
@ -85,6 +87,9 @@ QByteArray PacketDemodSettings::serialize() const
s.writeString(23, m_udpAddress); s.writeString(23, m_udpAddress);
s.writeU32(24, m_udpPort); s.writeU32(24, m_udpPort);
s.writeString(25, m_logFilename);
s.writeBool(26, m_logEnabled);
for (int i = 0; i < PACKETDEMOD_COLUMNS; i++) for (int i = 0; i < PACKETDEMOD_COLUMNS; i++)
s.writeS32(100 + i, m_columnIndexes[i]); s.writeS32(100 + i, m_columnIndexes[i]);
for (int i = 0; i < PACKETDEMOD_COLUMNS; i++) for (int i = 0; i < PACKETDEMOD_COLUMNS; i++)
@ -149,6 +154,9 @@ bool PacketDemodSettings::deserialize(const QByteArray& data)
m_udpPort = 9999; m_udpPort = 9999;
} }
d.readString(25, &m_logFilename, "pager_log.csv");
d.readBool(26, &m_logEnabled, false);
for (int i = 0; i < PACKETDEMOD_COLUMNS; i++) for (int i = 0; i < PACKETDEMOD_COLUMNS; i++)
d.readS32(100 + i, &m_columnIndexes[i], i); d.readS32(100 + i, &m_columnIndexes[i], i);
for (int i = 0; i < PACKETDEMOD_COLUMNS; i++) for (int i = 0; i < PACKETDEMOD_COLUMNS; i++)

View File

@ -50,6 +50,9 @@ struct PacketDemodSettings
uint16_t m_reverseAPIDeviceIndex; uint16_t m_reverseAPIDeviceIndex;
uint16_t m_reverseAPIChannelIndex; uint16_t m_reverseAPIChannelIndex;
QString m_logFilename;
bool m_logEnabled;
int m_columnIndexes[PACKETDEMOD_COLUMNS];//!< How the columns are ordered in the table int m_columnIndexes[PACKETDEMOD_COLUMNS];//!< How the columns are ordered in the table
int m_columnSizes[PACKETDEMOD_COLUMNS]; //!< Size of the columns in the table int m_columnSizes[PACKETDEMOD_COLUMNS]; //!< Size of the columns in the table

View File

@ -62,6 +62,18 @@ IP address of the host to forward received packets to via UDP.
UDP port number to forward received packets to. UDP port number to forward received packets to.
<h3>12: Start/stop Logging Packets to .csv File</h3>
When checked, writes all received packets to a .csv file.
<h3>13: .csv Log Filename</h3>
Click to specify the name of the .csv file which received packets are logged to.
<h3>14: Read Data from .csv File</h3>
Click to specify a previously written .csv log file, which is read and used to update the table.
<h3>Received Packets Table</h3> <h3>Received Packets Table</h3>
The received packets table displays the contents of the packets that have been received. Only packets with valid CRCs are displayed. The received packets table displays the contents of the packets that have been received. Only packets with valid CRCs are displayed.

View File

@ -155,6 +155,19 @@ bool PagerDemod::handleMessage(const Message& cmd)
QHostAddress(m_settings.m_udpAddress), m_settings.m_udpPort); QHostAddress(m_settings.m_udpAddress), m_settings.m_udpPort);
} }
// Write to log file
if (m_logFile.isOpen())
{
m_logStream << report.getDateTime().date().toString() << ","
<< report.getDateTime().time().toString() << ","
<< QString("%1").arg(report.getAddress(), 7, 10, QChar('0')) << ","
<< QString::number(report.getFunctionBits()) << ","
<< "\"" << report.getAlphaMessage() << "\","
<< report.getNumericMessage() << ","
<< QString::number(report.getEvenParityErrors()) << ","
<< QString::number(report.getBCHParityErrors()) << "\n";
}
return true; return true;
} }
else if (MainCore::MsgChannelDemodQuery::match(cmd)) else if (MainCore::MsgChannelDemodQuery::match(cmd))
@ -178,6 +191,8 @@ ScopeVis *PagerDemod::getScopeSink()
void PagerDemod::applySettings(const PagerDemodSettings& settings, bool force) void PagerDemod::applySettings(const PagerDemodSettings& settings, bool force)
{ {
qDebug() << "PagerDemod::applySettings:" qDebug() << "PagerDemod::applySettings:"
<< " m_logEnabled: " << settings.m_logEnabled
<< " m_logFilename: " << settings.m_logFilename
<< " m_streamIndex: " << settings.m_streamIndex << " m_streamIndex: " << settings.m_streamIndex
<< " m_useReverseAPI: " << settings.m_useReverseAPI << " m_useReverseAPI: " << settings.m_useReverseAPI
<< " m_reverseAPIAddress: " << settings.m_reverseAPIAddress << " m_reverseAPIAddress: " << settings.m_reverseAPIAddress
@ -209,6 +224,12 @@ void PagerDemod::applySettings(const PagerDemodSettings& settings, bool force)
if ((settings.m_udpPort != m_settings.m_udpPort) || force) { if ((settings.m_udpPort != m_settings.m_udpPort) || force) {
reverseAPIKeys.append("udpPort"); reverseAPIKeys.append("udpPort");
} }
if ((settings.m_logFilename != m_settings.m_logFilename) || force) {
reverseAPIKeys.append("logFilename");
}
if ((settings.m_logEnabled != m_settings.m_logEnabled) || force) {
reverseAPIKeys.append("logEnabled");
}
if (m_settings.m_streamIndex != settings.m_streamIndex) if (m_settings.m_streamIndex != settings.m_streamIndex)
{ {
if (m_deviceAPI->getSampleMIMO()) // change of stream is possible for MIMO devices only if (m_deviceAPI->getSampleMIMO()) // change of stream is possible for MIMO devices only
@ -235,6 +256,36 @@ void PagerDemod::applySettings(const PagerDemodSettings& settings, bool force)
webapiReverseSendSettings(reverseAPIKeys, settings, fullUpdate || force); webapiReverseSendSettings(reverseAPIKeys, settings, fullUpdate || force);
} }
if ((settings.m_logEnabled != m_settings.m_logEnabled)
|| (settings.m_logFilename != m_settings.m_logFilename)
|| force)
{
if (m_logFile.isOpen())
{
m_logStream.flush();
m_logFile.close();
}
if (settings.m_logEnabled && !settings.m_logFilename.isEmpty())
{
m_logFile.setFileName(settings.m_logFilename);
if (m_logFile.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text))
{
qDebug() << "PagerDemod::applySettings - Logging to: " << settings.m_logFilename;
bool newFile = m_logFile.size() == 0;
m_logStream.setDevice(&m_logFile);
if (newFile)
{
// Write header
m_logStream << "Date,Time,Address,Function Bits,Alpha,Numeric,Even Parity Errors,BCH Parity Errors\n";
}
}
else
{
qDebug() << "PagerDemod::applySettings - Unable to open log file: " << settings.m_logFilename;
}
}
}
m_settings = settings; m_settings = settings;
} }
@ -341,6 +392,12 @@ void PagerDemod::webapiUpdateChannelSettings(
if (channelSettingsKeys.contains("udpPort")) { if (channelSettingsKeys.contains("udpPort")) {
settings.m_udpPort = response.getPagerDemodSettings()->getUdpPort(); settings.m_udpPort = response.getPagerDemodSettings()->getUdpPort();
} }
if (channelSettingsKeys.contains("logFilename")) {
settings.m_logFilename = *response.getAdsbDemodSettings()->getLogFilename();
}
if (channelSettingsKeys.contains("logEnabled")) {
settings.m_logEnabled = response.getAdsbDemodSettings()->getLogEnabled();
}
if (channelSettingsKeys.contains("rgbColor")) { if (channelSettingsKeys.contains("rgbColor")) {
settings.m_rgbColor = response.getPagerDemodSettings()->getRgbColor(); settings.m_rgbColor = response.getPagerDemodSettings()->getRgbColor();
} }
@ -376,6 +433,8 @@ void PagerDemod::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& re
response.getPagerDemodSettings()->setUdpEnabled(settings.m_udpEnabled); response.getPagerDemodSettings()->setUdpEnabled(settings.m_udpEnabled);
response.getPagerDemodSettings()->setUdpAddress(new QString(settings.m_udpAddress)); response.getPagerDemodSettings()->setUdpAddress(new QString(settings.m_udpAddress));
response.getPagerDemodSettings()->setUdpPort(settings.m_udpPort); response.getPagerDemodSettings()->setUdpPort(settings.m_udpPort);
response.getPagerDemodSettings()->setLogFilename(new QString(settings.m_logFilename));
response.getPagerDemodSettings()->setLogEnabled(settings.m_logEnabled);
response.getPagerDemodSettings()->setRgbColor(settings.m_rgbColor); response.getPagerDemodSettings()->setRgbColor(settings.m_rgbColor);
if (response.getPagerDemodSettings()->getTitle()) { if (response.getPagerDemodSettings()->getTitle()) {
@ -460,6 +519,12 @@ void PagerDemod::webapiFormatChannelSettings(
if (channelSettingsKeys.contains("udpPort") || force) { if (channelSettingsKeys.contains("udpPort") || force) {
swgPagerDemodSettings->setUdpPort(settings.m_udpPort); swgPagerDemodSettings->setUdpPort(settings.m_udpPort);
} }
if (channelSettingsKeys.contains("logFilename") || force) {
swgPagerDemodSettings->setLogFilename(new QString(settings.m_logFilename));
}
if (channelSettingsKeys.contains("logEnabled") || force) {
swgPagerDemodSettings->setLogEnabled(settings.m_logEnabled);
}
if (channelSettingsKeys.contains("rgbColor") || force) { if (channelSettingsKeys.contains("rgbColor") || force) {
swgPagerDemodSettings->setRgbColor(settings.m_rgbColor); swgPagerDemodSettings->setRgbColor(settings.m_rgbColor);
} }

View File

@ -25,6 +25,8 @@
#include <QUdpSocket> #include <QUdpSocket>
#include <QThread> #include <QThread>
#include <QDateTime> #include <QDateTime>
#include <QFile>
#include <QTextStream>
#include "dsp/basebandsamplesink.h" #include "dsp/basebandsamplesink.h"
#include "channel/channelapi.h" #include "channel/channelapi.h"
@ -175,6 +177,8 @@ private:
int m_basebandSampleRate; //!< stored from device message used when starting baseband sink int m_basebandSampleRate; //!< stored from device message used when starting baseband sink
qint64 m_centerFrequency; qint64 m_centerFrequency;
QUdpSocket m_udpSocket; QUdpSocket m_udpSocket;
QFile m_logFile;
QTextStream m_logStream;
QNetworkAccessManager *m_networkManager; QNetworkAccessManager *m_networkManager;
QNetworkRequest m_networkRequest; QNetworkRequest m_networkRequest;

View File

@ -23,6 +23,8 @@
#include <QAction> #include <QAction>
#include <QRegExp> #include <QRegExp>
#include <QClipboard> #include <QClipboard>
#include <QFileDialog>
#include <QMessageBox>
#include "pagerdemodgui.h" #include "pagerdemodgui.h"
@ -33,6 +35,7 @@
#include "plugin/pluginapi.h" #include "plugin/pluginapi.h"
#include "util/simpleserializer.h" #include "util/simpleserializer.h"
#include "util/db.h" #include "util/db.h"
#include "util/csv.h"
#include "gui/basicchannelsettingsdialog.h" #include "gui/basicchannelsettingsdialog.h"
#include "gui/devicestreamselectiondialog.h" #include "gui/devicestreamselectiondialog.h"
#include "dsp/dspengine.h" #include "dsp/dspengine.h"
@ -144,7 +147,9 @@ bool PagerDemodGUI::deserialize(const QByteArray& data)
} }
// Add row to table // Add row to table
void PagerDemodGUI::messageReceived(const PagerDemod::MsgPagerMessage& message) void PagerDemodGUI::messageReceived(const QDateTime dateTime, int address, int functionBits,
const QString &numericMessage, const QString &alphaMessage,
int evenParityErrors, int bchParityErrors)
{ {
// Add to messages table // Add to messages table
ui->messages->setSortingEnabled(false); ui->messages->setSortingEnabled(false);
@ -169,43 +174,43 @@ void PagerDemodGUI::messageReceived(const PagerDemod::MsgPagerMessage& message)
ui->messages->setItem(row, MESSAGE_COL_NUMERIC, numericItem); ui->messages->setItem(row, MESSAGE_COL_NUMERIC, numericItem);
ui->messages->setItem(row, MESSAGE_COL_EVEN_PE, evenPEItem); ui->messages->setItem(row, MESSAGE_COL_EVEN_PE, evenPEItem);
ui->messages->setItem(row, MESSAGE_COL_BCH_PE, bchPEItem); ui->messages->setItem(row, MESSAGE_COL_BCH_PE, bchPEItem);
dateItem->setText(message.getDateTime().date().toString()); dateItem->setText(dateTime.date().toString());
timeItem->setText(message.getDateTime().time().toString()); timeItem->setText(dateTime.time().toString());
addressItem->setText(QString("%1").arg(message.getAddress(), 7, 10, QChar('0'))); addressItem->setText(QString("%1").arg(address, 7, 10, QChar('0')));
// Standard way of choosing numeric or alpha decode isn't followed widely // Standard way of choosing numeric or alpha decode isn't followed widely
if (m_settings.m_decode == PagerDemodSettings::Standard) if (m_settings.m_decode == PagerDemodSettings::Standard)
{ {
// Encoding is based on function bits // Encoding is based on function bits
if (message.getFunctionBits() == 0) { if (functionBits == 0) {
messageItem->setText(message.getNumericMessage()); messageItem->setText(numericMessage);
} else { } else {
messageItem->setText(message.getAlphaMessage()); messageItem->setText(alphaMessage);
} }
} }
else if (m_settings.m_decode == PagerDemodSettings::Inverted) else if (m_settings.m_decode == PagerDemodSettings::Inverted)
{ {
// Encoding is based on function bits, but inverted from standard // Encoding is based on function bits, but inverted from standard
if (message.getFunctionBits() == 3) { if (functionBits == 3) {
messageItem->setText(message.getNumericMessage()); messageItem->setText(numericMessage);
} else { } else {
messageItem->setText(message.getAlphaMessage()); messageItem->setText(alphaMessage);
} }
} }
else if (m_settings.m_decode == PagerDemodSettings::Numeric) else if (m_settings.m_decode == PagerDemodSettings::Numeric)
{ {
// Always display as numeric // Always display as numeric
messageItem->setText(message.getNumericMessage()); messageItem->setText(numericMessage);
} }
else if (m_settings.m_decode == PagerDemodSettings::Alphanumeric) else if (m_settings.m_decode == PagerDemodSettings::Alphanumeric)
{ {
// Always display as alphanumeric // Always display as alphanumeric
messageItem->setText(message.getAlphaMessage()); messageItem->setText(alphaMessage);
} }
else else
{ {
// Guess at what the encoding is // Guess at what the encoding is
QString numeric = message.getNumericMessage(); QString numeric = numericMessage;
QString alpha = message.getAlphaMessage(); QString alpha = alphaMessage;
bool done = false; bool done = false;
if (!done) if (!done)
{ {
@ -233,11 +238,11 @@ void PagerDemodGUI::messageReceived(const PagerDemod::MsgPagerMessage& message)
messageItem->setText(alpha); messageItem->setText(alpha);
} }
} }
functionItem->setText(QString("%1").arg(message.getFunctionBits())); functionItem->setText(QString("%1").arg(functionBits));
alphaItem->setText(message.getAlphaMessage()); alphaItem->setText(alphaMessage);
numericItem->setText(message.getNumericMessage()); numericItem->setText(numericMessage);
evenPEItem->setText(QString("%1").arg(message.getEvenParityErrors())); evenPEItem->setText(QString("%1").arg(evenParityErrors));
bchPEItem->setText(QString("%1").arg(message.getBCHParityErrors())); bchPEItem->setText(QString("%1").arg(bchParityErrors));
ui->messages->setSortingEnabled(true); ui->messages->setSortingEnabled(true);
ui->messages->scrollToItem(dateItem); // Will only scroll if not hidden ui->messages->scrollToItem(dateItem); // Will only scroll if not hidden
filterRow(row); filterRow(row);
@ -258,7 +263,9 @@ bool PagerDemodGUI::handleMessage(const Message& message)
else if (PagerDemod::MsgPagerMessage::match(message)) else if (PagerDemod::MsgPagerMessage::match(message))
{ {
PagerDemod::MsgPagerMessage& report = (PagerDemod::MsgPagerMessage&) message; PagerDemod::MsgPagerMessage& report = (PagerDemod::MsgPagerMessage&) message;
messageReceived(report); messageReceived(report.getDateTime(), report.getAddress(), report.getFunctionBits(),
report.getNumericMessage(), report.getAlphaMessage(),
report.getEvenParityErrors(), report.getBCHParityErrors());
return true; return true;
} }
@ -606,6 +613,9 @@ void PagerDemodGUI::displaySettings()
ui->channel1->setCurrentIndex(m_settings.m_scopeCh1); ui->channel1->setCurrentIndex(m_settings.m_scopeCh1);
ui->channel2->setCurrentIndex(m_settings.m_scopeCh2); ui->channel2->setCurrentIndex(m_settings.m_scopeCh2);
ui->logFilename->setToolTip(QString(".csv log filename: %1").arg(m_settings.m_logFilename));
ui->logEnable->setChecked(m_settings.m_logEnabled);
// Order and size columns // Order and size columns
QHeaderView *header = ui->messages->horizontalHeader(); QHeaderView *header = ui->messages->horizontalHeader();
for (int i = 0; i < PAGERDEMOD_MESSAGE_COLUMNS; i++) for (int i = 0; i < PAGERDEMOD_MESSAGE_COLUMNS; i++)
@ -669,3 +679,104 @@ void PagerDemodGUI::on_charset_clicked()
applySettings(); applySettings();
} }
} }
void PagerDemodGUI::on_logEnable_clicked(bool checked)
{
m_settings.m_logEnabled = checked;
applySettings();
}
void PagerDemodGUI::on_logFilename_clicked()
{
// Get filename to save to
QFileDialog fileDialog(nullptr, "Select file to log received messages to", "", "*.csv");
fileDialog.setAcceptMode(QFileDialog::AcceptSave);
if (fileDialog.exec())
{
QStringList fileNames = fileDialog.selectedFiles();
if (fileNames.size() > 0)
{
m_settings.m_logFilename = fileNames[0];
ui->logFilename->setToolTip(QString(".csv log filename: %1").arg(m_settings.m_logFilename));
applySettings();
}
}
}
// Read .csv log and process as received messages
void PagerDemodGUI::on_logOpen_clicked()
{
QFileDialog fileDialog(nullptr, "Select .csv log file to read", "", "*.csv");
if (fileDialog.exec())
{
QStringList fileNames = fileDialog.selectedFiles();
if (fileNames.size() > 0)
{
QFile file(fileNames[0]);
if (file.open(QIODevice::ReadOnly | QIODevice::Text))
{
QTextStream in(&file);
QString error;
QHash<QString, int> colIndexes = CSV::readHeader(in, {"Date", "Time", "Address", "Function Bits", "Alpha", "Numeric", "Even Parity Errors", "BCH Parity Errors"}, error);
if (error.isEmpty())
{
int dateCol = colIndexes.value("Date");
int timeCol = colIndexes.value("Time");
int addressCol = colIndexes.value("Address");
int functionCol = colIndexes.value("Function Bits");
int alphaCol = colIndexes.value("Alpha");
int numericCol = colIndexes.value("Numeric");
int evenCol = colIndexes.value("Even Parity Errors");
int bchCol = colIndexes.value("BCH Parity Errors");
int maxCol = std::max({dateCol, timeCol, addressCol, functionCol, alphaCol, numericCol, evenCol, bchCol});
QMessageBox dialog(this);
dialog.setText("Reading messages");
dialog.addButton(QMessageBox::Cancel);
dialog.show();
QApplication::processEvents();
int count = 0;
bool cancelled = false;
QStringList cols;
while (!cancelled && CSV::readRow(in, &cols))
{
if (cols.size() > maxCol)
{
QDate date = QDate::fromString(cols[dateCol]);
QTime time = QTime::fromString(cols[timeCol]);
QDateTime dateTime(date, time);
int address = cols[addressCol].toInt();
int functionBits = cols[functionCol].toInt();
int evenErrors = cols[evenCol].toInt();
int bchErrors = cols[bchCol].toInt();
messageReceived(dateTime, address, functionBits,
cols[numericCol], cols[alphaCol],
evenErrors, bchErrors);
if (count % 1000 == 0)
{
QApplication::processEvents();
if (dialog.clickedButton()) {
cancelled = true;
}
}
count++;
}
}
dialog.close();
}
else
{
QMessageBox::critical(this, "Pager Demod", error);
}
}
else
{
QMessageBox::critical(this, "Pager Demod", QString("Failed to open file %1").arg(fileNames[0]));
}
}
}
}

View File

@ -78,7 +78,9 @@ private:
void applySettings(bool force = false); void applySettings(bool force = false);
void displaySettings(); void displaySettings();
void displayStreamIndex(); void displayStreamIndex();
void messageReceived(const PagerDemod::MsgPagerMessage& message); void messageReceived(const QDateTime dateTime, int address, int functionBits,
const QString &numericMessage, const QString &alphaMessage,
int evenParityErrors, int bchParityErrors);
bool handleMessage(const Message& message); bool handleMessage(const Message& message);
void leaveEvent(QEvent*); void leaveEvent(QEvent*);
@ -113,6 +115,9 @@ private slots:
void on_udpPort_editingFinished(); void on_udpPort_editingFinished();
void on_channel1_currentIndexChanged(int index); void on_channel1_currentIndexChanged(int index);
void on_channel2_currentIndexChanged(int index); void on_channel2_currentIndexChanged(int index);
void on_logEnable_clicked(bool checked=false);
void on_logFilename_clicked();
void on_logOpen_clicked();
void filterRow(int row); void filterRow(int row);
void filter(); void filter();
void messages_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex); void messages_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex);

View File

@ -627,6 +627,60 @@
</property> </property>
</spacer> </spacer>
</item> </item>
<item>
<widget class="ButtonSwitch" name="logEnable">
<property name="maximumSize">
<size>
<width>24</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Start/stop logging of received messages to .csv file</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/record_off.png</normaloff>:/record_off.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="logFilename">
<property name="toolTip">
<string>Set log .csv filename</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/save.png</normaloff>:/save.png</iconset>
</property>
<property name="checkable">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="logOpen">
<property name="toolTip">
<string>Read data from .csv log file</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/load.png</normaloff>:/load.png</iconset>
</property>
<property name="checkable">
<bool>false</bool>
</property>
</widget>
</item>
<item> <item>
<widget class="QToolButton" name="clearTable"> <widget class="QToolButton" name="clearTable">
<property name="toolTip"> <property name="toolTip">
@ -979,6 +1033,11 @@
<header>gui/levelmeter.h</header> <header>gui/levelmeter.h</header>
<container>1</container> <container>1</container>
</customwidget> </customwidget>
<customwidget>
<class>ButtonSwitch</class>
<extends>QToolButton</extends>
<header>gui/buttonswitch.h</header>
</customwidget>
<customwidget> <customwidget>
<class>GLScope</class> <class>GLScope</class>
<extends>QWidget</extends> <extends>QWidget</extends>

View File

@ -44,6 +44,8 @@ void PagerDemodSettings::resetToDefaults()
m_udpPort = 9999; m_udpPort = 9999;
m_scopeCh1 = 4; m_scopeCh1 = 4;
m_scopeCh2 = 9; m_scopeCh2 = 9;
m_logFilename = "pager_log.csv";
m_logEnabled = false;
m_rgbColor = QColor(200, 191, 231).rgb(); m_rgbColor = QColor(200, 191, 231).rgb();
m_title = "Pager Demodulator"; m_title = "Pager Demodulator";
m_streamIndex = 0; m_streamIndex = 0;
@ -92,6 +94,9 @@ QByteArray PagerDemodSettings::serialize() const
s.writeBlob(23, serializeIntList(m_sevenbit)); s.writeBlob(23, serializeIntList(m_sevenbit));
s.writeBlob(24, serializeIntList(m_unicode)); s.writeBlob(24, serializeIntList(m_unicode));
s.writeString(25, m_logFilename);
s.writeBool(26, m_logEnabled);
for (int i = 0; i < PAGERDEMOD_MESSAGE_COLUMNS; i++) { for (int i = 0; i < PAGERDEMOD_MESSAGE_COLUMNS; i++) {
s.writeS32(100 + i, m_messageColumnIndexes[i]); s.writeS32(100 + i, m_messageColumnIndexes[i]);
} }
@ -166,6 +171,9 @@ bool PagerDemodSettings::deserialize(const QByteArray& data)
d.readBlob(24, &blob); d.readBlob(24, &blob);
deserializeIntList(blob, m_unicode); deserializeIntList(blob, m_unicode);
d.readString(25, &m_logFilename, "pager_log.csv");
d.readBool(26, &m_logEnabled, false);
for (int i = 0; i < PAGERDEMOD_MESSAGE_COLUMNS; i++) { for (int i = 0; i < PAGERDEMOD_MESSAGE_COLUMNS; i++) {
d.readS32(100 + i, &m_messageColumnIndexes[i], i); d.readS32(100 + i, &m_messageColumnIndexes[i], i);
} }

View File

@ -64,6 +64,9 @@ struct PagerDemodSettings
QList<qint32> m_sevenbit; QList<qint32> m_sevenbit;
QList<qint32> m_unicode; QList<qint32> m_unicode;
QString m_logFilename;
bool m_logEnabled;
int m_messageColumnIndexes[PAGERDEMOD_MESSAGE_COLUMNS];//!< How the columns are ordered in the table 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 int m_messageColumnSizes[PAGERDEMOD_MESSAGE_COLUMNS]; //!< Size of the columns in the table

View File

@ -84,6 +84,18 @@ IP address of the host to forward received messages to via UDP.
UDP port number to forward received messages to. UDP port number to forward received messages to.
<h3>15: Start/stop Logging Messages to .csv File</h3>
When checked, writes all received messages to a .csv file.
<h3>16: .csv Log Filename</h3>
Click to specify the name of the .csv file which received messages are logged to.
<h3>17: Read Data from .csv File</h3>
Click to specify a previously written .csv log file, which is read and used to update the table.
<h3>Received Messages Table</h3> <h3>Received Messages Table</h3>
The received messages table displays each pager message received. The received messages table displays each pager message received.

View File

@ -30,7 +30,7 @@
#include <string.h> #include <string.h>
#include "util/units.h" #include "util/units.h"
#include "../demodadsb/csv.h" #include "util/csv.h"
#define OURAIRPORTS_NAVAIDS_URL "https://ourairports.com/data/navaids.csv" #define OURAIRPORTS_NAVAIDS_URL "https://ourairports.com/data/navaids.csv"

View File

@ -26,7 +26,7 @@
#include "util/units.h" #include "util/units.h"
#include "util/maidenhead.h" #include "util/maidenhead.h"
#include "../../channelrx/demodadsb/csv.h" #include "util/csv.h"
#define IARU_BEACONS_URL "https://iaru-r1-c5-beacons.org/wp-content/uploads/beacons.csv" #define IARU_BEACONS_URL "https://iaru-r1-c5-beacons.org/wp-content/uploads/beacons.csv"

View File

@ -25,7 +25,7 @@
#include <QDebug> #include <QDebug>
// Create a hash map from a CSV file with two columns // Create a hash map from a CSV file with two columns
QHash<QString, QString> *csvHash(const QString& filename, int reserve) QHash<QString, QString> *CSV::hash(const QString& filename, int reserve)
{ {
int cnt = 0; int cnt = 0;
QHash<QString, QString> *map = nullptr; QHash<QString, QString> *map = nullptr;
@ -67,3 +67,100 @@ QHash<QString, QString> *csvHash(const QString& filename, int reserve)
return map; return map;
} }
// Read a row from a CSV file (handling quotes)
// https://stackoverflow.com/questions/27318631/parsing-through-a-csv-file-in-qt
bool CSV::readRow(QTextStream &in, QStringList *row)
{
static const int delta[][5] = {
// , " \n ? eof
{ 1, 2, -1, 0, -1 }, // 0: parsing (store char)
{ 1, 2, -1, 0, -1 }, // 1: parsing (store column)
{ 3, 4, 3, 3, -2 }, // 2: quote entered (no-op)
{ 3, 4, 3, 3, -2 }, // 3: parsing inside quotes (store char)
{ 1, 3, -1, 0, -1 }, // 4: quote exited (no-op)
// -1: end of row, store column, success
// -2: eof inside quotes
};
row->clear();
if (in.atEnd())
return false;
int state = 0, t;
char ch;
QString cell;
while (state >= 0)
{
if (in.atEnd())
{
t = 4;
}
else
{
in >> ch;
if (ch == ',') {
t = 0;
} else if (ch == '\"') {
t = 1;
} else if (ch == '\n') {
t = 2;
} else {
t = 3;
}
}
state = delta[state][t];
switch (state) {
case 0:
case 3:
cell += ch;
break;
case -1:
case 1:
row->append(cell);
cell = "";
break;
}
}
if (state == -2) {
return false;
}
return true;
}
// Read header row from CSV file and return a hash mapping names to column numbers
// Returns error if header row can't be read, or if all of requiredColumns aren't found
QHash<QString, int> CSV::readHeader(QTextStream &in, QStringList requiredColumns, QString &error)
{
QHash<QString, int> colNumbers;
QStringList row;
// Read column names
if (CSV::readRow(in, &row))
{
// Create hash mapping column names to indices
for (int i = 0; i < row.size(); i++) {
colNumbers.insert(row[i], i);
}
// Check all required columns exist
for (const auto col : requiredColumns)
{
if (!colNumbers.contains(col)) {
error = QString("Missing column %1").arg(col);
}
}
}
else
{
error = "Failed to read header row";
}
return colNumbers;
}

View File

@ -20,9 +20,12 @@
#include <QString> #include <QString>
#include <QHash> #include <QHash>
#include <QTextStream>
// Extract string from CSV line, updating pp to next column #include "export.h"
static inline char *csvNext(char **pp)
// Extract string from CSV line, updating pp to next column (This doesn't handle , inside quotes)
static inline char *csvNext(char **pp, char delimiter=',')
{ {
char *p = *pp; char *p = *pp;
@ -31,7 +34,7 @@ static inline char *csvNext(char **pp)
char *start = p; char *start = p;
while ((*p != ',') && (*p != '\n')) while ((*p != delimiter) && (*p != '\n'))
p++; p++;
*p++ = '\0'; *p++ = '\0';
*pp = p; *pp = p;
@ -39,6 +42,13 @@ static inline char *csvNext(char **pp)
return start; return start;
} }
QHash<QString, QString> *csvHash(const QString& filename, int reserve=0); struct SDRBASE_API CSV {
static QHash<QString, QString> *hash(const QString& filename, int reserve=0);
static bool readRow(QTextStream &in, QStringList *row);
static QHash<QString, int> readHeader(QTextStream &in, QStringList requiredColumns, QString &error);
};
#endif /* INCLUDE_CSV_H */ #endif /* INCLUDE_CSV_H */

View File

@ -25,6 +25,10 @@ AISDemodSettings:
udpFormat: udpFormat:
description: "0 for binary, 1 for NMEA" description: "0 for binary, 1 for NMEA"
type: integer type: integer
logFilename:
type: string
logEnabled:
type: integer
rgbColor: rgbColor:
type: integer type: integer
title: title:

View File

@ -24,6 +24,10 @@ PacketDemodSettings:
udpPort: udpPort:
description: "UDP port to forward received packets to" description: "UDP port to forward received packets to"
type: integer type: integer
logFilename:
type: string
logEnabled:
type: integer
rgbColor: rgbColor:
type: integer type: integer
title: title:

View File

@ -28,6 +28,10 @@ PagerDemodSettings:
udpFormat: udpFormat:
description: "0 for binary, 1 for NMEA" description: "0 for binary, 1 for NMEA"
type: integer type: integer
logFilename:
type: string
logEnabled:
type: integer
rgbColor: rgbColor:
type: integer type: integer
title: title:

View File

@ -44,6 +44,10 @@ SWGAISDemodSettings::SWGAISDemodSettings() {
m_udp_port_isSet = false; m_udp_port_isSet = false;
udp_format = 0; udp_format = 0;
m_udp_format_isSet = false; m_udp_format_isSet = false;
log_filename = nullptr;
m_log_filename_isSet = false;
log_enabled = 0;
m_log_enabled_isSet = false;
rgb_color = 0; rgb_color = 0;
m_rgb_color_isSet = false; m_rgb_color_isSet = false;
title = nullptr; title = nullptr;
@ -84,6 +88,10 @@ SWGAISDemodSettings::init() {
m_udp_port_isSet = false; m_udp_port_isSet = false;
udp_format = 0; udp_format = 0;
m_udp_format_isSet = false; m_udp_format_isSet = false;
log_filename = new QString("");
m_log_filename_isSet = false;
log_enabled = 0;
m_log_enabled_isSet = false;
rgb_color = 0; rgb_color = 0;
m_rgb_color_isSet = false; m_rgb_color_isSet = false;
title = new QString(""); title = new QString("");
@ -114,6 +122,10 @@ SWGAISDemodSettings::cleanup() {
} }
if(log_filename != nullptr) {
delete log_filename;
}
if(title != nullptr) { if(title != nullptr) {
delete title; delete title;
@ -155,6 +167,10 @@ SWGAISDemodSettings::fromJsonObject(QJsonObject &pJson) {
::SWGSDRangel::setValue(&udp_format, pJson["udpFormat"], "qint32", ""); ::SWGSDRangel::setValue(&udp_format, pJson["udpFormat"], "qint32", "");
::SWGSDRangel::setValue(&log_filename, pJson["logFilename"], "QString", "QString");
::SWGSDRangel::setValue(&log_enabled, pJson["logEnabled"], "qint32", "");
::SWGSDRangel::setValue(&rgb_color, pJson["rgbColor"], "qint32", ""); ::SWGSDRangel::setValue(&rgb_color, pJson["rgbColor"], "qint32", "");
::SWGSDRangel::setValue(&title, pJson["title"], "QString", "QString"); ::SWGSDRangel::setValue(&title, pJson["title"], "QString", "QString");
@ -211,6 +227,12 @@ SWGAISDemodSettings::asJsonObject() {
if(m_udp_format_isSet){ if(m_udp_format_isSet){
obj->insert("udpFormat", QJsonValue(udp_format)); obj->insert("udpFormat", QJsonValue(udp_format));
} }
if(log_filename != nullptr && *log_filename != QString("")){
toJsonValue(QString("logFilename"), log_filename, obj, QString("QString"));
}
if(m_log_enabled_isSet){
obj->insert("logEnabled", QJsonValue(log_enabled));
}
if(m_rgb_color_isSet){ if(m_rgb_color_isSet){
obj->insert("rgbColor", QJsonValue(rgb_color)); obj->insert("rgbColor", QJsonValue(rgb_color));
} }
@ -319,6 +341,26 @@ SWGAISDemodSettings::setUdpFormat(qint32 udp_format) {
this->m_udp_format_isSet = true; this->m_udp_format_isSet = true;
} }
QString*
SWGAISDemodSettings::getLogFilename() {
return log_filename;
}
void
SWGAISDemodSettings::setLogFilename(QString* log_filename) {
this->log_filename = log_filename;
this->m_log_filename_isSet = true;
}
qint32
SWGAISDemodSettings::getLogEnabled() {
return log_enabled;
}
void
SWGAISDemodSettings::setLogEnabled(qint32 log_enabled) {
this->log_enabled = log_enabled;
this->m_log_enabled_isSet = true;
}
qint32 qint32
SWGAISDemodSettings::getRgbColor() { SWGAISDemodSettings::getRgbColor() {
return rgb_color; return rgb_color;
@ -428,6 +470,12 @@ SWGAISDemodSettings::isSet(){
if(m_udp_format_isSet){ if(m_udp_format_isSet){
isObjectUpdated = true; break; isObjectUpdated = true; break;
} }
if(log_filename && *log_filename != QString("")){
isObjectUpdated = true; break;
}
if(m_log_enabled_isSet){
isObjectUpdated = true; break;
}
if(m_rgb_color_isSet){ if(m_rgb_color_isSet){
isObjectUpdated = true; break; isObjectUpdated = true; break;
} }

View File

@ -66,6 +66,12 @@ public:
qint32 getUdpFormat(); qint32 getUdpFormat();
void setUdpFormat(qint32 udp_format); void setUdpFormat(qint32 udp_format);
QString* getLogFilename();
void setLogFilename(QString* log_filename);
qint32 getLogEnabled();
void setLogEnabled(qint32 log_enabled);
qint32 getRgbColor(); qint32 getRgbColor();
void setRgbColor(qint32 rgb_color); void setRgbColor(qint32 rgb_color);
@ -118,6 +124,12 @@ private:
qint32 udp_format; qint32 udp_format;
bool m_udp_format_isSet; bool m_udp_format_isSet;
QString* log_filename;
bool m_log_filename_isSet;
qint32 log_enabled;
bool m_log_enabled_isSet;
qint32 rgb_color; qint32 rgb_color;
bool m_rgb_color_isSet; bool m_rgb_color_isSet;

View File

@ -42,6 +42,10 @@ SWGPacketDemodSettings::SWGPacketDemodSettings() {
m_udp_address_isSet = false; m_udp_address_isSet = false;
udp_port = 0; udp_port = 0;
m_udp_port_isSet = false; m_udp_port_isSet = false;
log_filename = nullptr;
m_log_filename_isSet = false;
log_enabled = 0;
m_log_enabled_isSet = false;
rgb_color = 0; rgb_color = 0;
m_rgb_color_isSet = false; m_rgb_color_isSet = false;
title = nullptr; title = nullptr;
@ -80,6 +84,10 @@ SWGPacketDemodSettings::init() {
m_udp_address_isSet = false; m_udp_address_isSet = false;
udp_port = 0; udp_port = 0;
m_udp_port_isSet = false; m_udp_port_isSet = false;
log_filename = new QString("");
m_log_filename_isSet = false;
log_enabled = 0;
m_log_enabled_isSet = false;
rgb_color = 0; rgb_color = 0;
m_rgb_color_isSet = false; m_rgb_color_isSet = false;
title = new QString(""); title = new QString("");
@ -111,6 +119,10 @@ SWGPacketDemodSettings::cleanup() {
delete udp_address; delete udp_address;
} }
if(log_filename != nullptr) {
delete log_filename;
}
if(title != nullptr) { if(title != nullptr) {
delete title; delete title;
@ -150,6 +162,10 @@ SWGPacketDemodSettings::fromJsonObject(QJsonObject &pJson) {
::SWGSDRangel::setValue(&udp_port, pJson["udpPort"], "qint32", ""); ::SWGSDRangel::setValue(&udp_port, pJson["udpPort"], "qint32", "");
::SWGSDRangel::setValue(&log_filename, pJson["logFilename"], "QString", "QString");
::SWGSDRangel::setValue(&log_enabled, pJson["logEnabled"], "qint32", "");
::SWGSDRangel::setValue(&rgb_color, pJson["rgbColor"], "qint32", ""); ::SWGSDRangel::setValue(&rgb_color, pJson["rgbColor"], "qint32", "");
::SWGSDRangel::setValue(&title, pJson["title"], "QString", "QString"); ::SWGSDRangel::setValue(&title, pJson["title"], "QString", "QString");
@ -203,6 +219,12 @@ SWGPacketDemodSettings::asJsonObject() {
if(m_udp_port_isSet){ if(m_udp_port_isSet){
obj->insert("udpPort", QJsonValue(udp_port)); obj->insert("udpPort", QJsonValue(udp_port));
} }
if(log_filename != nullptr && *log_filename != QString("")){
toJsonValue(QString("logFilename"), log_filename, obj, QString("QString"));
}
if(m_log_enabled_isSet){
obj->insert("logEnabled", QJsonValue(log_enabled));
}
if(m_rgb_color_isSet){ if(m_rgb_color_isSet){
obj->insert("rgbColor", QJsonValue(rgb_color)); obj->insert("rgbColor", QJsonValue(rgb_color));
} }
@ -301,6 +323,26 @@ SWGPacketDemodSettings::setUdpPort(qint32 udp_port) {
this->m_udp_port_isSet = true; this->m_udp_port_isSet = true;
} }
QString*
SWGPacketDemodSettings::getLogFilename() {
return log_filename;
}
void
SWGPacketDemodSettings::setLogFilename(QString* log_filename) {
this->log_filename = log_filename;
this->m_log_filename_isSet = true;
}
qint32
SWGPacketDemodSettings::getLogEnabled() {
return log_enabled;
}
void
SWGPacketDemodSettings::setLogEnabled(qint32 log_enabled) {
this->log_enabled = log_enabled;
this->m_log_enabled_isSet = true;
}
qint32 qint32
SWGPacketDemodSettings::getRgbColor() { SWGPacketDemodSettings::getRgbColor() {
return rgb_color; return rgb_color;
@ -407,6 +449,12 @@ SWGPacketDemodSettings::isSet(){
if(m_udp_port_isSet){ if(m_udp_port_isSet){
isObjectUpdated = true; break; isObjectUpdated = true; break;
} }
if(log_filename && *log_filename != QString("")){
isObjectUpdated = true; break;
}
if(m_log_enabled_isSet){
isObjectUpdated = true; break;
}
if(m_rgb_color_isSet){ if(m_rgb_color_isSet){
isObjectUpdated = true; break; isObjectUpdated = true; break;
} }

View File

@ -63,6 +63,12 @@ public:
qint32 getUdpPort(); qint32 getUdpPort();
void setUdpPort(qint32 udp_port); void setUdpPort(qint32 udp_port);
QString* getLogFilename();
void setLogFilename(QString* log_filename);
qint32 getLogEnabled();
void setLogEnabled(qint32 log_enabled);
qint32 getRgbColor(); qint32 getRgbColor();
void setRgbColor(qint32 rgb_color); void setRgbColor(qint32 rgb_color);
@ -112,6 +118,12 @@ private:
qint32 udp_port; qint32 udp_port;
bool m_udp_port_isSet; bool m_udp_port_isSet;
QString* log_filename;
bool m_log_filename_isSet;
qint32 log_enabled;
bool m_log_enabled_isSet;
qint32 rgb_color; qint32 rgb_color;
bool m_rgb_color_isSet; bool m_rgb_color_isSet;

View File

@ -46,6 +46,10 @@ SWGPagerDemodSettings::SWGPagerDemodSettings() {
m_udp_port_isSet = false; m_udp_port_isSet = false;
udp_format = 0; udp_format = 0;
m_udp_format_isSet = false; m_udp_format_isSet = false;
log_filename = nullptr;
m_log_filename_isSet = false;
log_enabled = 0;
m_log_enabled_isSet = false;
rgb_color = 0; rgb_color = 0;
m_rgb_color_isSet = false; m_rgb_color_isSet = false;
title = nullptr; title = nullptr;
@ -88,6 +92,10 @@ SWGPagerDemodSettings::init() {
m_udp_port_isSet = false; m_udp_port_isSet = false;
udp_format = 0; udp_format = 0;
m_udp_format_isSet = false; m_udp_format_isSet = false;
log_filename = new QString("");
m_log_filename_isSet = false;
log_enabled = 0;
m_log_enabled_isSet = false;
rgb_color = 0; rgb_color = 0;
m_rgb_color_isSet = false; m_rgb_color_isSet = false;
title = new QString(""); title = new QString("");
@ -119,6 +127,10 @@ SWGPagerDemodSettings::cleanup() {
} }
if(log_filename != nullptr) {
delete log_filename;
}
if(title != nullptr) { if(title != nullptr) {
delete title; delete title;
@ -162,6 +174,10 @@ SWGPagerDemodSettings::fromJsonObject(QJsonObject &pJson) {
::SWGSDRangel::setValue(&udp_format, pJson["udpFormat"], "qint32", ""); ::SWGSDRangel::setValue(&udp_format, pJson["udpFormat"], "qint32", "");
::SWGSDRangel::setValue(&log_filename, pJson["logFilename"], "QString", "QString");
::SWGSDRangel::setValue(&log_enabled, pJson["logEnabled"], "qint32", "");
::SWGSDRangel::setValue(&rgb_color, pJson["rgbColor"], "qint32", ""); ::SWGSDRangel::setValue(&rgb_color, pJson["rgbColor"], "qint32", "");
::SWGSDRangel::setValue(&title, pJson["title"], "QString", "QString"); ::SWGSDRangel::setValue(&title, pJson["title"], "QString", "QString");
@ -221,6 +237,12 @@ SWGPagerDemodSettings::asJsonObject() {
if(m_udp_format_isSet){ if(m_udp_format_isSet){
obj->insert("udpFormat", QJsonValue(udp_format)); obj->insert("udpFormat", QJsonValue(udp_format));
} }
if(log_filename != nullptr && *log_filename != QString("")){
toJsonValue(QString("logFilename"), log_filename, obj, QString("QString"));
}
if(m_log_enabled_isSet){
obj->insert("logEnabled", QJsonValue(log_enabled));
}
if(m_rgb_color_isSet){ if(m_rgb_color_isSet){
obj->insert("rgbColor", QJsonValue(rgb_color)); obj->insert("rgbColor", QJsonValue(rgb_color));
} }
@ -339,6 +361,26 @@ SWGPagerDemodSettings::setUdpFormat(qint32 udp_format) {
this->m_udp_format_isSet = true; this->m_udp_format_isSet = true;
} }
QString*
SWGPagerDemodSettings::getLogFilename() {
return log_filename;
}
void
SWGPagerDemodSettings::setLogFilename(QString* log_filename) {
this->log_filename = log_filename;
this->m_log_filename_isSet = true;
}
qint32
SWGPagerDemodSettings::getLogEnabled() {
return log_enabled;
}
void
SWGPagerDemodSettings::setLogEnabled(qint32 log_enabled) {
this->log_enabled = log_enabled;
this->m_log_enabled_isSet = true;
}
qint32 qint32
SWGPagerDemodSettings::getRgbColor() { SWGPagerDemodSettings::getRgbColor() {
return rgb_color; return rgb_color;
@ -451,6 +493,12 @@ SWGPagerDemodSettings::isSet(){
if(m_udp_format_isSet){ if(m_udp_format_isSet){
isObjectUpdated = true; break; isObjectUpdated = true; break;
} }
if(log_filename && *log_filename != QString("")){
isObjectUpdated = true; break;
}
if(m_log_enabled_isSet){
isObjectUpdated = true; break;
}
if(m_rgb_color_isSet){ if(m_rgb_color_isSet){
isObjectUpdated = true; break; isObjectUpdated = true; break;
} }

View File

@ -69,6 +69,12 @@ public:
qint32 getUdpFormat(); qint32 getUdpFormat();
void setUdpFormat(qint32 udp_format); void setUdpFormat(qint32 udp_format);
QString* getLogFilename();
void setLogFilename(QString* log_filename);
qint32 getLogEnabled();
void setLogEnabled(qint32 log_enabled);
qint32 getRgbColor(); qint32 getRgbColor();
void setRgbColor(qint32 rgb_color); void setRgbColor(qint32 rgb_color);
@ -124,6 +130,12 @@ private:
qint32 udp_format; qint32 udp_format;
bool m_udp_format_isSet; bool m_udp_format_isSet;
QString* log_filename;
bool m_log_filename_isSet;
qint32 log_enabled;
bool m_log_enabled_isSet;
qint32 rgb_color; qint32 rgb_color;
bool m_rgb_color_isSet; bool m_rgb_color_isSet;