Merge pull request #746 from srcejon/master

Add PacketDemod, APRS, Map and Star Tracker plugins
This commit is contained in:
Edouard Griffiths 2021-01-14 17:55:37 +01:00 committed by GitHub
commit 5bc1d0cd7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
398 changed files with 23937 additions and 650 deletions

View File

@ -81,6 +81,7 @@ for:
qml-module-qtlocation qml-module-qtpositioning qml-module-qtquick-window2 qml-module-qtquick-dialogs \
qml-module-qtquick-controls qml-module-qtquick-layouts \
libqt5serialport5-dev qtdeclarative5-dev qtpositioning5-dev qtlocation5-dev \
libqt5charts5-dev \
libusb-1.0-0-dev libboost-all-dev libasound2-dev libopencv-dev libopencv-imgcodecs-dev \
libxml2-dev bison flex ffmpeg libpostproc-dev libavcodec-dev libavformat-dev \
libopus-dev libcodec2-dev libairspy-dev libhackrf-dev \

View File

@ -308,6 +308,7 @@ if (BUILD_GUI)
find_package(Qt5 COMPONENTS Quick)
find_package(Qt5 COMPONENTS QuickWidgets)
find_package(Qt5 COMPONENTS Positioning)
find_package(Qt5 COMPONENTS Charts)
endif()
# other requirements

3
debian/control vendored
View File

@ -21,6 +21,7 @@ Build-Depends: debhelper (>= 9),
qml-module-qtquick-controls,
qml-module-qtquick-layouts,
libqt5serialport5-dev,
libqt5charts5-dev,
qtdeclarative5-dev,
qtpositioning5-dev,
qtlocation5-dev,
@ -55,7 +56,7 @@ Description: SDR/Analyzer/Generator front-end for various hardware
Builds on Linux, Windows and Mac O/S
Reception modes supported:
Analog: AM, ATV, NFM, WFM, SSB, broadcast FM
Digital: D-Star, Yaesu SF, DMR, dPMR, LoRa, ADS-B
Digital: D-Star, Yaesu SF, DMR, dPMR, LoRa, ADS-B, Packet (AX.25/APRS)
Analyzer: Generic channel
Transmission modes supported:
Analog: AM, ATV, NFM, SSB, WFM

BIN
doc/img/APRS_map.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

BIN
doc/img/APRS_plugin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

BIN
doc/img/Map_plugin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
doc/img/StarTracker_map.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -16,6 +16,7 @@ add_subdirectory(filesink)
add_subdirectory(freqtracker)
add_subdirectory(demodchirpchat)
add_subdirectory(demodvorsc)
add_subdirectory(demodpacket)
if(LIBDSDCC_FOUND AND LIBMBE_FOUND)
add_subdirectory(demoddsd)

View File

@ -34,12 +34,14 @@
#include "SWGADSBDemodSettings.h"
#include "SWGChannelReport.h"
#include "SWGADSBDemodReport.h"
#include "SWGTargetAzimuthElevation.h"
#include "dsp/dspengine.h"
#include "dsp/dspcommands.h"
#include "dsp/devicesamplemimo.h"
#include "device/deviceapi.h"
#include "util/db.h"
#include "maincore.h"
#include "adsbdemod.h"
#include "adsbdemodworker.h"
@ -471,3 +473,27 @@ void ADSBDemod::networkManagerFinished(QNetworkReply *reply)
reply->deleteLater();
}
void ADSBDemod::setTarget(const QString& name, float targetAzimuth, float targetElevation)
{
m_targetAzimuth = targetAzimuth;
m_targetElevation = targetElevation;
m_targetAzElValid = true;
// Send to Rotator Controllers
MessagePipes& messagePipes = MainCore::instance()->getMessagePipes();
QList<MessageQueue*> *mapMessageQueues = messagePipes.getMessageQueues(this, "target");
if (mapMessageQueues)
{
QList<MessageQueue*>::iterator it = mapMessageQueues->begin();
for (; it != mapMessageQueues->end(); ++it)
{
SWGSDRangel::SWGTargetAzimuthElevation *swgTarget = new SWGSDRangel::SWGTargetAzimuthElevation();
swgTarget->setName(new QString(name));
swgTarget->setAzimuth(targetAzimuth);
swgTarget->setElevation(targetElevation);
(*it)->push(MainCore::MsgTargetAzimuthElevation::create(this, swgTarget));
}
}
}

View File

@ -119,12 +119,7 @@ public:
m_basebandSink->setMessageQueueToGUI(queue);
}
void setTarget(float targetAzimuth, float targetElevation)
{
m_targetAzimuth = targetAzimuth;
m_targetElevation = targetElevation;
m_targetAzElValid = true;
}
void setTarget(const QString& name, float targetAzimuth, float targetElevation);
void clearTarget() { m_targetAzElValid = false; }
uint32_t getNumberOfDeviceStreams() const;

View File

@ -29,6 +29,8 @@
#include <QMessageBox>
#include <QDebug>
#include "SWGMapItem.h"
#include "ui_adsbdemodgui.h"
#include "channel/channelwebapiutils.h"
#include "plugin/pluginapi.h"
@ -148,6 +150,104 @@ static Real modulus(double x, double y)
return x - y * std::floor(x/y);
}
QString Aircraft::getImage()
{
if (m_emitterCategory.length() > 0)
{
if (!m_emitterCategory.compare("Heavy"))
return QString("aircraft_4engine.png");
else if (!m_emitterCategory.compare("Large"))
return QString("aircraft_2engine.png");
else if (!m_emitterCategory.compare("Small"))
return QString("aircraft_2enginesmall.png");
else if (!m_emitterCategory.compare("Rotorcraft"))
return QString("aircraft_helicopter.png");
else if (!m_emitterCategory.compare("High performance"))
return QString("aircraft_fighter.png");
else if (!m_emitterCategory.compare("Light")
|| !m_emitterCategory.compare("Ultralight")
|| !m_emitterCategory.compare("Glider/sailplane"))
return QString("aircraft_light.png");
else if (!m_emitterCategory.compare("Space vehicle"))
return QString("aircraft_space.png");
else if (!m_emitterCategory.compare("UAV"))
return QString("aircraft_drone.png");
else if (!m_emitterCategory.compare("Emergency vehicle")
|| !m_emitterCategory.compare("Service vehicle"))
return QString("truck.png");
else
return QString("aircraft_2engine.png");
}
else
return QString("aircraft_2engine.png");
}
QString Aircraft::getText(bool all)
{
QStringList list;
if (m_flight.length() > 0)
{
list.append(QString("Flight: %1").arg(m_flight));
}
else
{
list.append(QString("ICAO: %1").arg(m_icao, 1, 16));
}
if (m_showAll || m_isHighlighted || all)
{
if (m_aircraftInfo != nullptr)
{
if (m_aircraftInfo->m_model.size() > 0)
{
list.append(QString("Aircraft: %1").arg(m_aircraftInfo->m_model));
}
}
if (m_altitudeValid)
{
if (m_gui->useSIUints())
list.append(QString("Altitude: %1 (m)").arg(Units::feetToIntegerMetres(m_altitude)));
else
list.append(QString("Altitude: %1 (ft)").arg(m_altitude));
}
if (m_speedValid)
{
if (m_gui->useSIUints())
list.append(QString("%1: %2 (kph)").arg(m_speedTypeNames[m_speedType]).arg(Units::knotsToIntegerKPH(m_speed)));
else
list.append(QString("%1: %2 (kn)").arg(m_speedTypeNames[m_speedType]).arg(m_speed));
}
if (m_verticalRateValid)
{
QString desc;
Real rate;
QString units;
if (m_gui->useSIUints())
{
rate = Units::feetPerMinToIntegerMetresPerSecond(m_verticalRate);
units = QString("m/s");
}
else
{
rate = m_verticalRate;
units = QString("ft/min");
}
if (m_verticalRate == 0)
desc = "Level flight";
else if (rate > 0)
desc = QString("Climbing: %1 (%2)").arg(rate).arg(units);
else
desc = QString("Descending: %1 (%2)").arg(rate).arg(units);
list.append(QString(desc));
}
if ((m_status.length() > 0) && m_status.compare("No emergency"))
{
list.append(m_status);
}
}
return list.join("\n");
}
QVariant AircraftModel::data(const QModelIndex &index, int role) const
{
int row = index.row();
@ -170,101 +270,12 @@ QVariant AircraftModel::data(const QModelIndex &index, int role) const
else if (role == AircraftModel::adsbDataRole)
{
// Create the text to go in the bubble next to the aircraft
QStringList list;
if (m_aircrafts[row]->m_flight.length() > 0)
{
list.append(QString("Flight: %1").arg(m_aircrafts[row]->m_flight));
}
else
{
list.append(QString("ICAO: %1").arg(m_aircrafts[row]->m_icao, 1, 16));
}
if (m_aircrafts[row]->m_showAll || m_aircrafts[row]->m_isHighlighted)
{
if (m_aircrafts[row]->m_aircraftInfo != nullptr)
{
if (m_aircrafts[row]->m_aircraftInfo->m_model.size() > 0)
{
list.append(QString("Aircraft: %1").arg(m_aircrafts[row]->m_aircraftInfo->m_model));
}
}
if (m_aircrafts[row]->m_altitudeValid)
{
if (m_aircrafts[row]->m_gui->useSIUints())
list.append(QString("Altitude: %1 (m)").arg(Units::feetToIntegerMetres(m_aircrafts[row]->m_altitude)));
else
list.append(QString("Altitude: %1 (ft)").arg(m_aircrafts[row]->m_altitude));
}
if (m_aircrafts[row]->m_speedValid)
{
if (m_aircrafts[row]->m_gui->useSIUints())
list.append(QString("%1: %2 (kph)").arg(m_aircrafts[row]->m_speedTypeNames[m_aircrafts[row]->m_speedType]).arg(Units::knotsToIntegerKPH(m_aircrafts[row]->m_speed)));
else
list.append(QString("%1: %2 (kn)").arg(m_aircrafts[row]->m_speedTypeNames[m_aircrafts[row]->m_speedType]).arg(m_aircrafts[row]->m_speed));
}
if (m_aircrafts[row]->m_verticalRateValid)
{
QString desc;
Real rate;
QString units;
if (m_aircrafts[row]->m_gui->useSIUints())
{
rate = Units::feetPerMinToIntegerMetresPerSecond(m_aircrafts[row]->m_verticalRate);
units = QString("m/s");
}
else
{
rate = m_aircrafts[row]->m_verticalRate;
units = QString("ft/min");
}
if (m_aircrafts[row]->m_verticalRate == 0)
desc = "Level flight";
else if (rate > 0)
desc = QString("Climbing: %1 (%2)").arg(rate).arg(units);
else
desc = QString("Descending: %1 (%2)").arg(rate).arg(units);
list.append(QString(desc));
}
if ((m_aircrafts[row]->m_status.length() > 0) && m_aircrafts[row]->m_status.compare("No emergency"))
{
list.append(m_aircrafts[row]->m_status);
}
}
QString data = list.join("\n");
return QVariant::fromValue(data);
return QVariant::fromValue(m_aircrafts[row]->getText());
}
else if (role == AircraftModel::aircraftImageRole)
{
// Select an image to use for the aircraft
if (m_aircrafts[row]->m_emitterCategory.length() > 0)
{
if (!m_aircrafts[row]->m_emitterCategory.compare("Heavy"))
return QVariant::fromValue(QString("aircraft_4engine.png"));
else if (!m_aircrafts[row]->m_emitterCategory.compare("Large"))
return QVariant::fromValue(QString("aircraft_2engine.png"));
else if (!m_aircrafts[row]->m_emitterCategory.compare("Small"))
return QVariant::fromValue(QString("aircraft_2enginesmall.png"));
else if (!m_aircrafts[row]->m_emitterCategory.compare("Rotorcraft"))
return QVariant::fromValue(QString("aircraft_helicopter.png"));
else if (!m_aircrafts[row]->m_emitterCategory.compare("High performance"))
return QVariant::fromValue(QString("aircraft_fighter.png"));
else if (!m_aircrafts[row]->m_emitterCategory.compare("Light")
|| !m_aircrafts[row]->m_emitterCategory.compare("Ultralight")
|| !m_aircrafts[row]->m_emitterCategory.compare("Glider/sailplane"))
return QVariant::fromValue(QString("aircraft_light.png"));
else if (!m_aircrafts[row]->m_emitterCategory.compare("Space vehicle"))
return QVariant::fromValue(QString("aircraft_space.png"));
else if (!m_aircrafts[row]->m_emitterCategory.compare("UAV"))
return QVariant::fromValue(QString("aircraft_drone.png"));
else if (!m_aircrafts[row]->m_emitterCategory.compare("Emergency vehicle")
|| !m_aircrafts[row]->m_emitterCategory.compare("Service vehicle"))
return QVariant::fromValue(QString("truck.png"));
else
return QVariant::fromValue(QString("aircraft_2engine.png"));
}
else
return QVariant::fromValue(QString("aircraft_2engine.png"));
return QVariant::fromValue(m_aircrafts[row]->getImage());
}
else if (role == AircraftModel::bubbleColourRole)
{
@ -280,7 +291,7 @@ QVariant AircraftModel::data(const QModelIndex &index, int role) const
}
else if (role == AircraftModel::aircraftPathRole)
{
if (m_flightPaths)
if ((m_flightPaths && m_aircrafts[row]->m_isHighlighted) || m_allFlightPaths)
return m_aircrafts[row]->m_coordinates;
else
return QVariantList();
@ -407,7 +418,7 @@ bool AirportModel::setData(const QModelIndex &index, const QVariant& value, int
else if (idx == m_airports[row]->m_frequencies.size())
{
// Set airport as target
m_gui->target(m_azimuth[row], m_elevation[row]);
m_gui->target(m_airports[row]->m_name, m_azimuth[row], m_elevation[row]);
emit dataChanged(index, index);
}
return true;
@ -442,7 +453,29 @@ void ADSBDemodGUI::updatePosition(Aircraft *aircraft)
aircraft->m_rangeItem->setText(QString::number(aircraft->m_range/1000.0, 'f', 1));
aircraft->m_azElItem->setText(QString("%1/%2").arg(std::round(aircraft->m_azimuth)).arg(std::round(aircraft->m_elevation)));
if (aircraft == m_trackAircraft)
m_adsbDemod->setTarget(aircraft->m_azimuth, aircraft->m_elevation);
m_adsbDemod->setTarget(aircraft->targetName(), aircraft->m_azimuth, aircraft->m_elevation);
// Send to Map feature
MessagePipes& messagePipes = MainCore::instance()->getMessagePipes();
QList<MessageQueue*> *mapMessageQueues = messagePipes.getMessageQueues(m_adsbDemod, "mapitems");
if (mapMessageQueues)
{
QList<MessageQueue*>::iterator it = mapMessageQueues->begin();
for (; it != mapMessageQueues->end(); ++it)
{
SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem();
swgMapItem->setName(new QString(QString("%1").arg(aircraft->m_icao, 0, 16)));
swgMapItem->setLatitude(aircraft->m_latitude);
swgMapItem->setLongitude(aircraft->m_longitude);
swgMapItem->setImage(new QString(QString("qrc:///map/%1").arg(aircraft->getImage())));
swgMapItem->setImageRotation(aircraft->m_heading);
swgMapItem->setText(new QString(aircraft->getText(true)));
MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_adsbDemod, swgMapItem);
(*it)->push(msg);
}
}
}
// Called when we have lat & long from local decode and we need to check if it is in a valid range (<180nm/333km)
@ -1359,6 +1392,12 @@ void ADSBDemodGUI::on_flightPaths_clicked(bool checked)
m_aircraftModel.setFlightPaths(checked);
}
void ADSBDemodGUI::on_allFlightPaths_clicked(bool checked)
{
m_settings.m_allFlightPaths = checked;
m_aircraftModel.setAllFlightPaths(checked);
}
QString ADSBDemodGUI::getDataDir()
{
// Get directory to store app data in (aircraft & airport databases and user-definable icons)
@ -1633,7 +1672,7 @@ void ADSBDemodGUI::updateAirports()
}
// Set a static target, such as an airport
void ADSBDemodGUI::target(float az, float el)
void ADSBDemodGUI::target(const QString& name, float az, float el)
{
if (m_trackAircraft)
{
@ -1642,7 +1681,7 @@ void ADSBDemodGUI::target(float az, float el)
m_aircraftModel.aircraftUpdated(m_trackAircraft);
m_trackAircraft = nullptr;
}
m_adsbDemod->setTarget(az, el);
m_adsbDemod->setTarget(name, az, el);
}
void ADSBDemodGUI::targetAircraft(Aircraft *aircraft)
@ -1658,7 +1697,7 @@ void ADSBDemodGUI::targetAircraft(Aircraft *aircraft)
// Track this aircraft
m_trackAircraft = aircraft;
if (aircraft->m_positionValid)
m_adsbDemod->setTarget(aircraft->m_azimuth, aircraft->m_elevation);
m_adsbDemod->setTarget(aircraft->targetName(), aircraft->m_azimuth, aircraft->m_elevation);
// Change colour of new target
aircraft->m_isTarget = true;
m_aircraftModel.aircraftUpdated(aircraft);
@ -1922,6 +1961,8 @@ void ADSBDemodGUI::displaySettings()
ui->flightPaths->setChecked(m_settings.m_flightPaths);
m_aircraftModel.setFlightPaths(m_settings.m_flightPaths);
ui->allFlightPaths->setChecked(m_settings.m_allFlightPaths);
m_aircraftModel.setAllFlightPaths(m_settings.m_allFlightPaths);
displayStreamIndex();
@ -2035,6 +2076,21 @@ void ADSBDemodGUI::tick()
ui->adsbData->removeRow(aircraft->m_icaoItem->row());
// Remove aircraft from hash
i = m_aircraft.erase(i);
// Remove from map feature
MessagePipes& messagePipes = MainCore::instance()->getMessagePipes();
QList<MessageQueue*> *mapMessageQueues = messagePipes.getMessageQueues(m_adsbDemod, "mapitems");
if (mapMessageQueues)
{
QList<MessageQueue*>::iterator it = mapMessageQueues->begin();
for (; it != mapMessageQueues->end(); ++it)
{
SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem();
swgMapItem->setName(new QString(QString("%1").arg(aircraft->m_icao, 0, 16)));
swgMapItem->setImage(new QString(""));
MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_adsbDemod, swgMapItem);
(*it)->push(msg);
}
}
// And finally free its memory
delete aircraft;
}

View File

@ -34,6 +34,7 @@
#include "util/azel.h"
#include "util/movingaverage.h"
#include "util/httpdownloadmanager.h"
#include "maincore.h"
#include "adsbdemodsettings.h"
#include "ourairportsdb.h"
@ -207,6 +208,19 @@ struct Aircraft {
m_correlationItem = new QTableWidgetItem();
m_rssiItem = new QTableWidgetItem();
}
QString getImage();
QString getText(bool all=false);
// Name to use when selected as a target
QString targetName()
{
if (!m_flight.isEmpty())
return QString("Flight: %1").arg(m_flight);
else
return QString("ICAO: %1").arg(m_icao, 0, 16);
}
};
// Aircraft data model used by QML map item
@ -300,9 +314,16 @@ public:
allAircraftUpdated();
}
void setAllFlightPaths(bool allFlightPaths)
{
m_allFlightPaths = allFlightPaths;
allAircraftUpdated();
}
private:
QList<Aircraft *> m_aircrafts;
bool m_flightPaths;
bool m_allFlightPaths;
};
// Airport data model used by QML map item
@ -445,7 +466,7 @@ public:
virtual MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; }
void highlightAircraft(Aircraft *aircraft);
void targetAircraft(Aircraft *aircraft);
void target(float az, float el);
void target(const QString& name, float az, float el);
bool setFrequency(float frequency);
bool useSIUints() { return m_settings.m_siUnits; }
@ -547,6 +568,7 @@ private slots:
void on_getOSNDB_clicked();
void on_getAirportDB_clicked();
void on_flightPaths_clicked(bool checked);
void on_allFlightPaths_clicked(bool checked);
void onWidgetRolled(QWidget* widget, bool rollDown);
void onMenuDialogCalled(const QPoint& p);
void handleInputMessages();

View File

@ -546,7 +546,7 @@
<item>
<widget class="ButtonSwitch" name="flightPaths">
<property name="toolTip">
<string>Display flight paths</string>
<string>Display flight path for selected aircraft</string>
</property>
<property name="text">
<string>^</string>
@ -563,6 +563,26 @@
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="allFlightPaths">
<property name="toolTip">
<string>Display flight paths for all aircraft</string>
</property>
<property name="text">
<string>^</string>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/icons/allflightpaths.png</normaloff>:/icons/allflightpaths.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="feed">
<property name="toolTip">
@ -945,17 +965,17 @@
<extends>QWidget</extends>
<header location="global">QtQuickWidgets/QQuickWidget</header>
</customwidget>
<customwidget>
<class>ButtonSwitch</class>
<extends>QToolButton</extends>
<header>gui/buttonswitch.h</header>
</customwidget>
<customwidget>
<class>RollupWidget</class>
<extends>QWidget</extends>
<header>gui/rollupwidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>ButtonSwitch</class>
<extends>QToolButton</extends>
<header>gui/buttonswitch.h</header>
</customwidget>
<customwidget>
<class>LevelMeterSignalDB</class>
<extends>QWidget</extends>

View File

@ -53,10 +53,11 @@ void ADSBDemodSettings::resetToDefaults()
m_airportMinimumSize = AirportType::Medium;
m_displayHeliports = false;
m_flightPaths = true;
m_allFlightPaths = false;
m_siUnits = false;
m_tableFontName = "Liberation Sans";
m_tableFontSize = 9;
m_displayDemodStats = true;
m_displayDemodStats = false;
m_correlateFullPreamble = true;
m_demodModeS = false;
m_deviceIndex = -1;
@ -109,6 +110,7 @@ QByteArray ADSBDemodSettings::serialize() const
s.writeBool(30, m_autoResizeTableColumns);
s.writeS32(31, m_interpolatorPhaseSteps);
s.writeFloat(32, m_interpolatorTapsPerPhase);
s.writeBool(33, m_allFlightPaths);
for (int i = 0; i < ADSBDEMOD_COLUMNS; i++)
s.writeS32(100 + i, m_columnIndexes[i]);
@ -188,6 +190,7 @@ bool ADSBDemodSettings::deserialize(const QByteArray& data)
d.readBool(30, &m_autoResizeTableColumns, false);
d.readS32(31, &m_interpolatorPhaseSteps, 4);
d.readFloat(32, &m_interpolatorTapsPerPhase, 3.5f);
d.readBool(33, &m_allFlightPaths, false);
for (int i = 0; i < ADSBDEMOD_COLUMNS; i++)
d.readS32(100 + i, &m_columnIndexes[i], i);

View File

@ -66,6 +66,7 @@ struct ADSBDemodSettings
} m_airportMinimumSize; //!< What's the minimum size airport that should be displayed
bool m_displayHeliports; //!< Whether to display heliports on the map
bool m_flightPaths; //!< Whether to display flight paths
bool m_allFlightPaths; //!< Whether to display flight paths for all aircraft
bool m_siUnits; //!< Uses m,kph rather than ft/knts
QString m_tableFontName; //!< Font to use for table
int m_tableFontSize;

View File

@ -2,5 +2,6 @@
<qresource prefix="/">
<file>icons/aircraft.png</file>
<file>icons/controltower.png</file>
<file>icons/allflightpaths.png</file>
</qresource>
</RCC>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,58 @@
project(demodpacket)
set(demodpacket_SOURCES
packetdemod.cpp
packetdemodsettings.cpp
packetdemodbaseband.cpp
packetdemodsink.cpp
packetdemodplugin.cpp
packetdemodwebapiadapter.cpp
)
set(demodpacket_HEADERS
packetdemod.h
packetdemodsettings.h
packetdemodbaseband.h
packetdemodsink.h
packetdemodplugin.h
packetdemodwebapiadapter.h
)
include_directories(
${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client
)
if(NOT SERVER_MODE)
set(demodpacket_SOURCES
${demodpacket_SOURCES}
packetdemodgui.cpp
packetdemodgui.ui
)
set(demodpacket_HEADERS
${demodpacket_HEADERS}
packetdemodgui.h
)
set(TARGET_NAME demodpacket)
set(TARGET_LIB "Qt5::Widgets" Qt5::Quick Qt5::QuickWidgets Qt5::Positioning)
set(TARGET_LIB_GUI "sdrgui")
set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR})
else()
set(TARGET_NAME demodpacketsrv)
set(TARGET_LIB "")
set(TARGET_LIB_GUI "")
set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR})
endif()
add_library(${TARGET_NAME} SHARED
${demodpacket_SOURCES}
)
target_link_libraries(${TARGET_NAME}
Qt5::Core
${TARGET_LIB}
sdrbase
${TARGET_LIB_GUI}
)
install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER})

View File

@ -0,0 +1,434 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2015-2018 Edouard Griffiths, F4EXB. //
// 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 "packetdemod.h"
#include <QTime>
#include <QDebug>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QBuffer>
#include <QThread>
#include <stdio.h>
#include <complex.h>
#include "SWGChannelSettings.h"
#include "SWGPacketDemodSettings.h"
#include "SWGChannelReport.h"
#include "SWGMapItem.h"
#include "dsp/dspengine.h"
#include "dsp/dspcommands.h"
#include "device/deviceapi.h"
#include "feature/feature.h"
#include "util/db.h"
#include "maincore.h"
MESSAGE_CLASS_DEFINITION(PacketDemod::MsgConfigurePacketDemod, Message)
const char * const PacketDemod::m_channelIdURI = "sdrangel.channelrx.packetdemod";
const char * const PacketDemod::m_channelId = "PacketDemod";
PacketDemod::PacketDemod(DeviceAPI *deviceAPI) :
ChannelAPI(m_channelIdURI, ChannelAPI::StreamSingleSink),
m_deviceAPI(deviceAPI),
m_basebandSampleRate(0)
{
setObjectName(m_channelId);
m_basebandSink = new PacketDemodBaseband(this);
m_basebandSink->setMessageQueueToChannel(getInputMessageQueue());
m_basebandSink->moveToThread(&m_thread);
applySettings(m_settings, true);
m_deviceAPI->addChannelSink(this);
m_deviceAPI->addChannelSinkAPI(this);
m_networkManager = new QNetworkAccessManager();
connect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*)));
}
PacketDemod::~PacketDemod()
{
qDebug("PacketDemod::~PacketDemod");
disconnect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*)));
delete m_networkManager;
m_deviceAPI->removeChannelSinkAPI(this);
m_deviceAPI->removeChannelSink(this);
if (m_basebandSink->isRunning()) {
stop();
}
delete m_basebandSink;
}
uint32_t PacketDemod::getNumberOfDeviceStreams() const
{
return m_deviceAPI->getNbSourceStreams();
}
void PacketDemod::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, bool firstOfBurst)
{
(void) firstOfBurst;
m_basebandSink->feed(begin, end);
}
void PacketDemod::start()
{
qDebug("PacketDemod::start");
m_basebandSink->reset();
m_basebandSink->startWork();
m_thread.start();
DSPSignalNotification *dspMsg = new DSPSignalNotification(m_basebandSampleRate, m_centerFrequency);
m_basebandSink->getInputMessageQueue()->push(dspMsg);
PacketDemodBaseband::MsgConfigurePacketDemodBaseband *msg = PacketDemodBaseband::MsgConfigurePacketDemodBaseband::create(m_settings, true);
m_basebandSink->getInputMessageQueue()->push(msg);
}
void PacketDemod::stop()
{
qDebug("PacketDemod::stop");
m_basebandSink->stopWork();
m_thread.quit();
m_thread.wait();
}
bool PacketDemod::handleMessage(const Message& cmd)
{
if (MsgConfigurePacketDemod::match(cmd))
{
MsgConfigurePacketDemod& cfg = (MsgConfigurePacketDemod&) cmd;
qDebug() << "PacketDemod::handleMessage: MsgConfigurePacketDemod";
applySettings(cfg.getSettings(), cfg.getForce());
return true;
}
else if (DSPSignalNotification::match(cmd))
{
DSPSignalNotification& notif = (DSPSignalNotification&) cmd;
m_basebandSampleRate = notif.getSampleRate();
m_centerFrequency = notif.getCenterFrequency();
// Forward to the sink
DSPSignalNotification* rep = new DSPSignalNotification(notif); // make a copy
qDebug() << "PacketDemod::handleMessage: DSPSignalNotification";
m_basebandSink->getInputMessageQueue()->push(rep);
// Forward to GUI if any
if (m_guiMessageQueue)
{
rep = new DSPSignalNotification(notif);
m_guiMessageQueue->push(rep);
}
return true;
}
else if (MainCore::MsgPacket::match(cmd))
{
// Forward to GUI
MainCore::MsgPacket& report = (MainCore::MsgPacket&)cmd;
if (getMessageQueueToGUI())
{
MainCore::MsgPacket *msg = new MainCore::MsgPacket(report);
getMessageQueueToGUI()->push(msg);
}
MessagePipes& messagePipes = MainCore::instance()->getMessagePipes();
// Forward to APRS and other packet features
QList<MessageQueue*> *packetMessageQueues = messagePipes.getMessageQueues(this, "packets");
if (packetMessageQueues)
{
QList<MessageQueue*>::iterator it = packetMessageQueues->begin();
for (; it != packetMessageQueues->end(); ++it)
{
MainCore::MsgPacket *msg = new MainCore::MsgPacket(report);
(*it)->push(msg);
}
}
return true;
}
else
{
return false;
}
}
void PacketDemod::applySettings(const PacketDemodSettings& settings, bool force)
{
qDebug() << "PacketDemod::applySettings:"
<< " m_streamIndex: " << settings.m_streamIndex
<< " m_useReverseAPI: " << settings.m_useReverseAPI
<< " m_reverseAPIAddress: " << settings.m_reverseAPIAddress
<< " m_reverseAPIPort: " << settings.m_reverseAPIPort
<< " m_reverseAPIDeviceIndex: " << settings.m_reverseAPIDeviceIndex
<< " m_reverseAPIChannelIndex: " << settings.m_reverseAPIChannelIndex
<< " force: " << force;
QList<QString> reverseAPIKeys;
if ((settings.m_inputFrequencyOffset != m_settings.m_inputFrequencyOffset) || force) {
reverseAPIKeys.append("inputFrequencyOffset");
}
if ((settings.m_rfBandwidth != m_settings.m_rfBandwidth) || force) {
reverseAPIKeys.append("rfBandwidth");
}
if ((settings.m_fmDeviation != m_settings.m_fmDeviation) || force) {
reverseAPIKeys.append("fmDeviation");
}
if (m_settings.m_streamIndex != settings.m_streamIndex)
{
if (m_deviceAPI->getSampleMIMO()) // change of stream is possible for MIMO devices only
{
m_deviceAPI->removeChannelSinkAPI(this);
m_deviceAPI->removeChannelSink(this, m_settings.m_streamIndex);
m_deviceAPI->addChannelSink(this, settings.m_streamIndex);
m_deviceAPI->addChannelSinkAPI(this);
}
reverseAPIKeys.append("streamIndex");
}
PacketDemodBaseband::MsgConfigurePacketDemodBaseband *msg = PacketDemodBaseband::MsgConfigurePacketDemodBaseband::create(settings, force);
m_basebandSink->getInputMessageQueue()->push(msg);
if (settings.m_useReverseAPI)
{
bool fullUpdate = ((m_settings.m_useReverseAPI != settings.m_useReverseAPI) && settings.m_useReverseAPI) ||
(m_settings.m_reverseAPIAddress != settings.m_reverseAPIAddress) ||
(m_settings.m_reverseAPIPort != settings.m_reverseAPIPort) ||
(m_settings.m_reverseAPIDeviceIndex != settings.m_reverseAPIDeviceIndex) ||
(m_settings.m_reverseAPIChannelIndex != settings.m_reverseAPIChannelIndex);
webapiReverseSendSettings(reverseAPIKeys, settings, fullUpdate || force);
}
m_settings = settings;
}
QByteArray PacketDemod::serialize() const
{
return m_settings.serialize();
}
bool PacketDemod::deserialize(const QByteArray& data)
{
if (m_settings.deserialize(data))
{
MsgConfigurePacketDemod *msg = MsgConfigurePacketDemod::create(m_settings, true);
m_inputMessageQueue.push(msg);
return true;
}
else
{
m_settings.resetToDefaults();
MsgConfigurePacketDemod *msg = MsgConfigurePacketDemod::create(m_settings, true);
m_inputMessageQueue.push(msg);
return false;
}
}
int PacketDemod::webapiSettingsGet(
SWGSDRangel::SWGChannelSettings& response,
QString& errorMessage)
{
(void) errorMessage;
response.setPacketDemodSettings(new SWGSDRangel::SWGPacketDemodSettings());
response.getPacketDemodSettings()->init();
webapiFormatChannelSettings(response, m_settings);
return 200;
}
int PacketDemod::webapiSettingsPutPatch(
bool force,
const QStringList& channelSettingsKeys,
SWGSDRangel::SWGChannelSettings& response,
QString& errorMessage)
{
(void) errorMessage;
PacketDemodSettings settings = m_settings;
webapiUpdateChannelSettings(settings, channelSettingsKeys, response);
MsgConfigurePacketDemod *msg = MsgConfigurePacketDemod::create(settings, force);
m_inputMessageQueue.push(msg);
qDebug("PacketDemod::webapiSettingsPutPatch: forward to GUI: %p", m_guiMessageQueue);
if (m_guiMessageQueue) // forward to GUI if any
{
MsgConfigurePacketDemod *msgToGUI = MsgConfigurePacketDemod::create(settings, force);
m_guiMessageQueue->push(msgToGUI);
}
webapiFormatChannelSettings(response, settings);
return 200;
}
void PacketDemod::webapiUpdateChannelSettings(
PacketDemodSettings& settings,
const QStringList& channelSettingsKeys,
SWGSDRangel::SWGChannelSettings& response)
{
if (channelSettingsKeys.contains("inputFrequencyOffset")) {
settings.m_inputFrequencyOffset = response.getPacketDemodSettings()->getInputFrequencyOffset();
}
if (channelSettingsKeys.contains("fmDeviation")) {
settings.m_fmDeviation = response.getPacketDemodSettings()->getFmDeviation();
}
if (channelSettingsKeys.contains("rfBandwidth")) {
settings.m_rfBandwidth = response.getPacketDemodSettings()->getRfBandwidth();
}
if (channelSettingsKeys.contains("rgbColor")) {
settings.m_rgbColor = response.getPacketDemodSettings()->getRgbColor();
}
if (channelSettingsKeys.contains("title")) {
settings.m_title = *response.getPacketDemodSettings()->getTitle();
}
if (channelSettingsKeys.contains("streamIndex")) {
settings.m_streamIndex = response.getPacketDemodSettings()->getStreamIndex();
}
if (channelSettingsKeys.contains("useReverseAPI")) {
settings.m_useReverseAPI = response.getPacketDemodSettings()->getUseReverseApi() != 0;
}
if (channelSettingsKeys.contains("reverseAPIAddress")) {
settings.m_reverseAPIAddress = *response.getPacketDemodSettings()->getReverseApiAddress();
}
if (channelSettingsKeys.contains("reverseAPIPort")) {
settings.m_reverseAPIPort = response.getPacketDemodSettings()->getReverseApiPort();
}
if (channelSettingsKeys.contains("reverseAPIDeviceIndex")) {
settings.m_reverseAPIDeviceIndex = response.getPacketDemodSettings()->getReverseApiDeviceIndex();
}
if (channelSettingsKeys.contains("reverseAPIChannelIndex")) {
settings.m_reverseAPIChannelIndex = response.getPacketDemodSettings()->getReverseApiChannelIndex();
}
}
void PacketDemod::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& response, const PacketDemodSettings& settings)
{
response.getPacketDemodSettings()->setFmDeviation(settings.m_fmDeviation);
response.getPacketDemodSettings()->setInputFrequencyOffset(settings.m_inputFrequencyOffset);
response.getPacketDemodSettings()->setRfBandwidth(settings.m_rfBandwidth);
response.getPacketDemodSettings()->setRgbColor(settings.m_rgbColor);
if (response.getPacketDemodSettings()->getTitle()) {
*response.getPacketDemodSettings()->getTitle() = settings.m_title;
} else {
response.getPacketDemodSettings()->setTitle(new QString(settings.m_title));
}
response.getPacketDemodSettings()->setStreamIndex(settings.m_streamIndex);
response.getPacketDemodSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0);
if (response.getPacketDemodSettings()->getReverseApiAddress()) {
*response.getPacketDemodSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress;
} else {
response.getPacketDemodSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress));
}
response.getPacketDemodSettings()->setReverseApiPort(settings.m_reverseAPIPort);
response.getPacketDemodSettings()->setReverseApiDeviceIndex(settings.m_reverseAPIDeviceIndex);
response.getPacketDemodSettings()->setReverseApiChannelIndex(settings.m_reverseAPIChannelIndex);
}
void PacketDemod::webapiReverseSendSettings(QList<QString>& channelSettingsKeys, const PacketDemodSettings& settings, bool force)
{
SWGSDRangel::SWGChannelSettings *swgChannelSettings = new SWGSDRangel::SWGChannelSettings();
webapiFormatChannelSettings(channelSettingsKeys, swgChannelSettings, settings, force);
QString channelSettingsURL = QString("http://%1:%2/sdrangel/deviceset/%3/channel/%4/settings")
.arg(settings.m_reverseAPIAddress)
.arg(settings.m_reverseAPIPort)
.arg(settings.m_reverseAPIDeviceIndex)
.arg(settings.m_reverseAPIChannelIndex);
m_networkRequest.setUrl(QUrl(channelSettingsURL));
m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QBuffer *buffer = new QBuffer();
buffer->open((QBuffer::ReadWrite));
buffer->write(swgChannelSettings->asJson().toUtf8());
buffer->seek(0);
// Always use PATCH to avoid passing reverse API settings
QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer);
buffer->setParent(reply);
delete swgChannelSettings;
}
void PacketDemod::webapiFormatChannelSettings(
QList<QString>& channelSettingsKeys,
SWGSDRangel::SWGChannelSettings *swgChannelSettings,
const PacketDemodSettings& settings,
bool force
)
{
swgChannelSettings->setDirection(0); // Single sink (Rx)
swgChannelSettings->setOriginatorChannelIndex(getIndexInDeviceSet());
swgChannelSettings->setOriginatorDeviceSetIndex(getDeviceSetIndex());
swgChannelSettings->setChannelType(new QString("PacketDemod"));
swgChannelSettings->setPacketDemodSettings(new SWGSDRangel::SWGPacketDemodSettings());
SWGSDRangel::SWGPacketDemodSettings *swgPacketDemodSettings = swgChannelSettings->getPacketDemodSettings();
// transfer data that has been modified. When force is on transfer all data except reverse API data
if (channelSettingsKeys.contains("fmDeviation") || force) {
swgPacketDemodSettings->setFmDeviation(settings.m_fmDeviation);
}
if (channelSettingsKeys.contains("inputFrequencyOffset") || force) {
swgPacketDemodSettings->setInputFrequencyOffset(settings.m_inputFrequencyOffset);
}
if (channelSettingsKeys.contains("rfBandwidth") || force) {
swgPacketDemodSettings->setRfBandwidth(settings.m_rfBandwidth);
}
if (channelSettingsKeys.contains("rgbColor") || force) {
swgPacketDemodSettings->setRgbColor(settings.m_rgbColor);
}
if (channelSettingsKeys.contains("title") || force) {
swgPacketDemodSettings->setTitle(new QString(settings.m_title));
}
if (channelSettingsKeys.contains("streamIndex") || force) {
swgPacketDemodSettings->setStreamIndex(settings.m_streamIndex);
}
}
void PacketDemod::networkManagerFinished(QNetworkReply *reply)
{
QNetworkReply::NetworkError replyError = reply->error();
if (replyError)
{
qWarning() << "PacketDemod::networkManagerFinished:"
<< " error(" << (int) replyError
<< "): " << replyError
<< ": " << reply->errorString();
}
else
{
QString answer = reply->readAll();
answer.chop(1); // remove last \n
qDebug("PacketDemod::networkManagerFinished: reply:\n%s", answer.toStdString().c_str());
}
reply->deleteLater();
}

View File

@ -0,0 +1,152 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2015-2018 Edouard Griffiths, F4EXB. //
// 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_PACKETDEMOD_H
#define INCLUDE_PACKETDEMOD_H
#include <vector>
#include <QNetworkRequest>
#include <QThread>
#include "dsp/basebandsamplesink.h"
#include "channel/channelapi.h"
#include "util/message.h"
#include "packetdemodbaseband.h"
#include "packetdemodsettings.h"
class QNetworkAccessManager;
class QNetworkReply;
class QThread;
class DeviceAPI;
class PacketDemod : public BasebandSampleSink, public ChannelAPI {
Q_OBJECT
public:
class MsgConfigurePacketDemod : public Message {
MESSAGE_CLASS_DECLARATION
public:
const PacketDemodSettings& getSettings() const { return m_settings; }
bool getForce() const { return m_force; }
static MsgConfigurePacketDemod* create(const PacketDemodSettings& settings, bool force)
{
return new MsgConfigurePacketDemod(settings, force);
}
private:
PacketDemodSettings m_settings;
bool m_force;
MsgConfigurePacketDemod(const PacketDemodSettings& settings, bool force) :
Message(),
m_settings(settings),
m_force(force)
{ }
};
PacketDemod(DeviceAPI *deviceAPI);
virtual ~PacketDemod();
virtual void destroy() { delete this; }
using BasebandSampleSink::feed;
virtual void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, bool po);
virtual void start();
virtual void stop();
virtual bool handleMessage(const Message& cmd);
virtual void getIdentifier(QString& id) { id = objectName(); }
virtual const QString& getURI() const { return getName(); }
virtual void getTitle(QString& title) { title = m_settings.m_title; }
virtual qint64 getCenterFrequency() const { return 0; }
virtual QByteArray serialize() const;
virtual bool deserialize(const QByteArray& data);
virtual int getNbSinkStreams() const { return 1; }
virtual int getNbSourceStreams() const { return 0; }
virtual qint64 getStreamCenterFrequency(int streamIndex, bool sinkElseSource) const
{
(void) streamIndex;
(void) sinkElseSource;
return 0;
}
virtual int webapiSettingsGet(
SWGSDRangel::SWGChannelSettings& response,
QString& errorMessage);
virtual int webapiSettingsPutPatch(
bool force,
const QStringList& channelSettingsKeys,
SWGSDRangel::SWGChannelSettings& response,
QString& errorMessage);
static void webapiFormatChannelSettings(
SWGSDRangel::SWGChannelSettings& response,
const PacketDemodSettings& settings);
static void webapiUpdateChannelSettings(
PacketDemodSettings& settings,
const QStringList& channelSettingsKeys,
SWGSDRangel::SWGChannelSettings& response);
double getMagSq() const { return m_basebandSink->getMagSq(); }
void getMagSqLevels(double& avg, double& peak, int& nbSamples) {
m_basebandSink->getMagSqLevels(avg, peak, nbSamples);
}
/* void setMessageQueueToGUI(MessageQueue* queue) override {
BasebandSampleSink::setMessageQueueToGUI(queue);
m_basebandSink->setMessageQueueToGUI(queue);
}*/
uint32_t getNumberOfDeviceStreams() const;
static const char * const m_channelIdURI;
static const char * const m_channelId;
private:
DeviceAPI *m_deviceAPI;
QThread m_thread;
PacketDemodBaseband* m_basebandSink;
PacketDemodSettings m_settings;
int m_basebandSampleRate; //!< stored from device message used when starting baseband sink
qint64 m_centerFrequency;
QNetworkAccessManager *m_networkManager;
QNetworkRequest m_networkRequest;
void applySettings(const PacketDemodSettings& settings, bool force = false);
void webapiReverseSendSettings(QList<QString>& channelSettingsKeys, const PacketDemodSettings& settings, bool force);
void webapiFormatChannelSettings(
QList<QString>& channelSettingsKeys,
SWGSDRangel::SWGChannelSettings *swgChannelSettings,
const PacketDemodSettings& settings,
bool force
);
private slots:
void networkManagerFinished(QNetworkReply *reply);
};
#endif // INCLUDE_PACKETDEMOD_H

View File

@ -0,0 +1,170 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2019 Edouard Griffiths, F4EXB //
// 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 "dsp/dspengine.h"
#include "dsp/dspcommands.h"
#include "dsp/downchannelizer.h"
#include "packetdemodbaseband.h"
MESSAGE_CLASS_DEFINITION(PacketDemodBaseband::MsgConfigurePacketDemodBaseband, Message)
PacketDemodBaseband::PacketDemodBaseband(PacketDemod *packetDemod) :
m_sink(packetDemod),
m_running(false),
m_mutex(QMutex::Recursive)
{
qDebug("PacketDemodBaseband::PacketDemodBaseband");
m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(48000));
m_channelizer = new DownChannelizer(&m_sink);
}
PacketDemodBaseband::~PacketDemodBaseband()
{
m_inputMessageQueue.clear();
delete m_channelizer;
}
void PacketDemodBaseband::reset()
{
QMutexLocker mutexLocker(&m_mutex);
m_inputMessageQueue.clear();
m_sampleFifo.reset();
}
void PacketDemodBaseband::startWork()
{
QMutexLocker mutexLocker(&m_mutex);
QObject::connect(
&m_sampleFifo,
&SampleSinkFifo::dataReady,
this,
&PacketDemodBaseband::handleData,
Qt::QueuedConnection
);
connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages()));
m_running = true;
}
void PacketDemodBaseband::stopWork()
{
QMutexLocker mutexLocker(&m_mutex);
disconnect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages()));
QObject::disconnect(
&m_sampleFifo,
&SampleSinkFifo::dataReady,
this,
&PacketDemodBaseband::handleData
);
m_running = false;
}
void PacketDemodBaseband::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end)
{
m_sampleFifo.write(begin, end);
}
void PacketDemodBaseband::handleData()
{
QMutexLocker mutexLocker(&m_mutex);
while ((m_sampleFifo.fill() > 0) && (m_inputMessageQueue.size() == 0))
{
SampleVector::iterator part1begin;
SampleVector::iterator part1end;
SampleVector::iterator part2begin;
SampleVector::iterator part2end;
std::size_t count = m_sampleFifo.readBegin(m_sampleFifo.fill(), &part1begin, &part1end, &part2begin, &part2end);
// first part of FIFO data
if (part1begin != part1end) {
m_channelizer->feed(part1begin, part1end);
}
// second part of FIFO data (used when block wraps around)
if(part2begin != part2end) {
m_channelizer->feed(part2begin, part2end);
}
m_sampleFifo.readCommit((unsigned int) count);
}
}
void PacketDemodBaseband::handleInputMessages()
{
Message* message;
while ((message = m_inputMessageQueue.pop()) != nullptr)
{
if (handleMessage(*message)) {
delete message;
}
}
}
bool PacketDemodBaseband::handleMessage(const Message& cmd)
{
if (MsgConfigurePacketDemodBaseband::match(cmd))
{
QMutexLocker mutexLocker(&m_mutex);
MsgConfigurePacketDemodBaseband& cfg = (MsgConfigurePacketDemodBaseband&) cmd;
qDebug() << "PacketDemodBaseband::handleMessage: MsgConfigurePacketDemodBaseband";
applySettings(cfg.getSettings(), cfg.getForce());
return true;
}
else if (DSPSignalNotification::match(cmd))
{
QMutexLocker mutexLocker(&m_mutex);
DSPSignalNotification& notif = (DSPSignalNotification&) cmd;
qDebug() << "PacketDemodBaseband::handleMessage: DSPSignalNotification: basebandSampleRate: " << notif.getSampleRate();
setBasebandSampleRate(notif.getSampleRate());
m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(notif.getSampleRate()));
return true;
}
else
{
return false;
}
}
void PacketDemodBaseband::applySettings(const PacketDemodSettings& settings, bool force)
{
if ((settings.m_inputFrequencyOffset != m_settings.m_inputFrequencyOffset) || force)
{
m_channelizer->setChannelization(PACKETDEMOD_CHANNEL_SAMPLE_RATE, settings.m_inputFrequencyOffset);
m_sink.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset());
}
m_sink.applySettings(settings, force);
m_settings = settings;
}
void PacketDemodBaseband::setBasebandSampleRate(int sampleRate)
{
m_channelizer->setBasebandSampleRate(sampleRate);
m_sink.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset());
}

View File

@ -0,0 +1,94 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2019 Edouard Griffiths, F4EXB //
// 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_PACKETDEMODBASEBAND_H
#define INCLUDE_PACKETDEMODBASEBAND_H
#include <QObject>
#include <QMutex>
#include "dsp/samplesinkfifo.h"
#include "util/message.h"
#include "util/messagequeue.h"
#include "packetdemodsink.h"
class DownChannelizer;
class PacketDemod;
class PacketDemodBaseband : public QObject
{
Q_OBJECT
public:
class MsgConfigurePacketDemodBaseband : public Message {
MESSAGE_CLASS_DECLARATION
public:
const PacketDemodSettings& getSettings() const { return m_settings; }
bool getForce() const { return m_force; }
static MsgConfigurePacketDemodBaseband* create(const PacketDemodSettings& settings, bool force)
{
return new MsgConfigurePacketDemodBaseband(settings, force);
}
private:
PacketDemodSettings m_settings;
bool m_force;
MsgConfigurePacketDemodBaseband(const PacketDemodSettings& settings, bool force) :
Message(),
m_settings(settings),
m_force(force)
{ }
};
PacketDemodBaseband(PacketDemod *packetDemod);
~PacketDemodBaseband();
void reset();
void startWork();
void stopWork();
void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end);
MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } //!< Get the queue for asynchronous inbound communication
void getMagSqLevels(double& avg, double& peak, int& nbSamples) {
m_sink.getMagSqLevels(avg, peak, nbSamples);
}
void setMessageQueueToChannel(MessageQueue *messageQueue) { m_sink.setMessageQueueToChannel(messageQueue); }
void setBasebandSampleRate(int sampleRate);
double getMagSq() const { return m_sink.getMagSq(); }
bool isRunning() const { return m_running; }
private:
SampleSinkFifo m_sampleFifo;
DownChannelizer *m_channelizer;
PacketDemodSink m_sink;
MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication
PacketDemodSettings m_settings;
bool m_running;
QMutex m_mutex;
bool handleMessage(const Message& cmd);
void calculateOffset(PacketDemodSink *sink);
void applySettings(const PacketDemodSettings& settings, bool force = false);
private slots:
void handleInputMessages();
void handleData(); //!< Handle data when samples have to be processed
};
#endif // INCLUDE_PACKETDEMODBASEBAND_H

View File

@ -0,0 +1,560 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2016 Edouard Griffiths, F4EXB //
// 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 <limits>
#include <ctype.h>
#include <QDockWidget>
#include <QMainWindow>
#include <QDebug>
#include <QQuickItem>
#include <QGeoLocation>
#include <QGeoCoordinate>
#include <QQmlContext>
#include <QMessageBox>
#include <QAction>
#include <QRegExp>
#include "packetdemodgui.h"
#include "util/ax25.h"
#include "device/deviceuiset.h"
#include "dsp/dspengine.h"
#include "dsp/dspcommands.h"
#include "ui_packetdemodgui.h"
#include "plugin/pluginapi.h"
#include "util/simpleserializer.h"
#include "util/db.h"
#include "util/morse.h"
#include "util/units.h"
#include "gui/basicchannelsettingsdialog.h"
#include "gui/devicestreamselectiondialog.h"
#include "dsp/dspengine.h"
#include "gui/crightclickenabler.h"
#include "channel/channelwebapiutils.h"
#include "maincore.h"
#include "packetdemod.h"
#include "packetdemodsink.h"
#define PACKET_COL_FROM 0
#define PACKET_COL_TO 1
#define PACKET_COL_VIA 2
#define PACKET_COL_TYPE 3
#define PACKET_COL_PID 4
#define PACKET_COL_DATA_ASCII 5
#define PACKET_COL_DATA_HEX 6
void PacketDemodGUI::resizeTable()
{
// Fill table with a row of dummy data that will size the columns nicely
// Trailing spaces are for sort arrow
int row = ui->packets->rowCount();
ui->packets->setRowCount(row + 1);
ui->packets->setItem(row, PACKET_COL_FROM, new QTableWidgetItem("123456-15-"));
ui->packets->setItem(row, PACKET_COL_TO, new QTableWidgetItem("123456-15-"));
ui->packets->setItem(row, PACKET_COL_VIA, new QTableWidgetItem("123456-15-"));
ui->packets->setItem(row, PACKET_COL_TYPE, new QTableWidgetItem("Type-"));
ui->packets->setItem(row, PACKET_COL_PID, new QTableWidgetItem("PID-"));
ui->packets->setItem(row, PACKET_COL_DATA_ASCII, new QTableWidgetItem("ABCEDGHIJKLMNOPQRSTUVWXYZ"));
ui->packets->setItem(row, PACKET_COL_DATA_HEX, new QTableWidgetItem("ABCEDGHIJKLMNOPQRSTUVWXYZ"));
ui->packets->resizeColumnsToContents();
ui->packets->removeRow(row);
}
// Columns in table reordered
void PacketDemodGUI::packets_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex)
{
(void) oldVisualIndex;
m_settings.m_columnIndexes[logicalIndex] = newVisualIndex;
}
// Column in table resized (when hidden size is 0)
void PacketDemodGUI::packets_sectionResized(int logicalIndex, int oldSize, int newSize)
{
(void) oldSize;
m_settings.m_columnSizes[logicalIndex] = newSize;
}
// Right click in table header - show column select menu
void PacketDemodGUI::columnSelectMenu(QPoint pos)
{
menu->popup(ui->packets->horizontalHeader()->viewport()->mapToGlobal(pos));
}
// Hide/show column when menu selected
void PacketDemodGUI::columnSelectMenuChecked(bool checked)
{
(void) checked;
QAction* action = qobject_cast<QAction*>(sender());
if (action != nullptr)
{
int idx = action->data().toInt(nullptr);
ui->packets->setColumnHidden(idx, !action->isChecked());
}
}
// Create column select menu item
QAction *PacketDemodGUI::createCheckableItem(QString &text, int idx, bool checked)
{
QAction *action = new QAction(text, this);
action->setCheckable(true);
action->setChecked(checked);
action->setData(QVariant(idx));
connect(action, SIGNAL(triggered()), this, SLOT(columnSelectMenuChecked()));
return action;
}
PacketDemodGUI* PacketDemodGUI::create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel)
{
PacketDemodGUI* gui = new PacketDemodGUI(pluginAPI, deviceUISet, rxChannel);
return gui;
}
void PacketDemodGUI::destroy()
{
delete this;
}
void PacketDemodGUI::resetToDefaults()
{
m_settings.resetToDefaults();
displaySettings();
applySettings(true);
}
QByteArray PacketDemodGUI::serialize() const
{
return m_settings.serialize();
}
bool PacketDemodGUI::deserialize(const QByteArray& data)
{
if(m_settings.deserialize(data)) {
displaySettings();
applySettings(true);
return true;
} else {
resetToDefaults();
return false;
}
}
// Add row to table
void PacketDemodGUI::packetReceived(QByteArray packet)
{
AX25Packet ax25;
if (ax25.decode(packet))
{
ui->packets->setSortingEnabled(false);
int row = ui->packets->rowCount();
ui->packets->setRowCount(row + 1);
QTableWidgetItem *fromItem = new QTableWidgetItem();
QTableWidgetItem *toItem = new QTableWidgetItem();
QTableWidgetItem *viaItem = new QTableWidgetItem();
QTableWidgetItem *typeItem = new QTableWidgetItem();
QTableWidgetItem *pidItem = new QTableWidgetItem();
QTableWidgetItem *dataASCIIItem = new QTableWidgetItem();
QTableWidgetItem *dataHexItem = new QTableWidgetItem();
ui->packets->setItem(row, PACKET_COL_FROM, fromItem);
ui->packets->setItem(row, PACKET_COL_TO, toItem);
ui->packets->setItem(row, PACKET_COL_VIA, viaItem);
ui->packets->setItem(row, PACKET_COL_TYPE, typeItem);
ui->packets->setItem(row, PACKET_COL_PID, pidItem);
ui->packets->setItem(row, PACKET_COL_DATA_ASCII, dataASCIIItem);
ui->packets->setItem(row, PACKET_COL_DATA_HEX, dataHexItem);
fromItem->setText(ax25.m_from);
toItem->setText(ax25.m_to);
viaItem->setText(ax25.m_via);
typeItem->setText(ax25.m_type);
pidItem->setText(ax25.m_pid);
dataASCIIItem->setText(ax25.m_dataASCII);
dataHexItem->setText(ax25.m_dataHex);
ui->packets->setSortingEnabled(true);
ui->packets->scrollToItem(fromItem);
filterRow(row);
}
else
qDebug() << "Unsupported AX.25 packet: " << packet;
}
bool PacketDemodGUI::handleMessage(const Message& message)
{
if (PacketDemod::MsgConfigurePacketDemod::match(message))
{
qDebug("PacketDemodGUI::handleMessage: PacketDemod::MsgConfigurePacketDemod");
const PacketDemod::MsgConfigurePacketDemod& cfg = (PacketDemod::MsgConfigurePacketDemod&) message;
m_settings = cfg.getSettings();
blockApplySettings(true);
displaySettings();
blockApplySettings(false);
return true;
}
else if (DSPSignalNotification::match(message))
{
DSPSignalNotification& notif = (DSPSignalNotification&) message;
m_basebandSampleRate = notif.getSampleRate();
return true;
}
else if (MainCore::MsgPacket::match(message))
{
MainCore::MsgPacket& report = (MainCore::MsgPacket&) message;
packetReceived(report.getPacket());
return true;
}
return false;
}
void PacketDemodGUI::handleInputMessages()
{
Message* message;
while ((message = getInputMessageQueue()->pop()) != 0)
{
if (handleMessage(*message))
{
delete message;
}
}
}
void PacketDemodGUI::channelMarkerChangedByCursor()
{
ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency());
m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency();
applySettings();
}
void PacketDemodGUI::channelMarkerHighlightedByCursor()
{
setHighlighted(m_channelMarker.getHighlighted());
}
void PacketDemodGUI::on_deltaFrequency_changed(qint64 value)
{
m_channelMarker.setCenterFrequency(value);
m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency();
applySettings();
}
void PacketDemodGUI::on_mode_currentIndexChanged(int value)
{
(void) value;
QString mode = ui->mode->currentText();
// TODO: Support 9600 FSK
}
void PacketDemodGUI::on_rfBW_valueChanged(int value)
{
float bw = value * 100.0f;
ui->rfBWText->setText(QString("%1k").arg(value / 10.0, 0, 'f', 1));
m_channelMarker.setBandwidth(bw);
m_settings.m_rfBandwidth = bw;
applySettings();
}
void PacketDemodGUI::on_fmDev_valueChanged(int value)
{
ui->fmDevText->setText(QString("%1k").arg(value / 10.0, 0, 'f', 1));
m_settings.m_fmDeviation = value * 100.0;
applySettings();
}
void PacketDemodGUI::on_filterFrom_editingFinished()
{
m_settings.m_filterFrom = ui->filterFrom->text();
filter();
applySettings();
}
void PacketDemodGUI::on_filterTo_editingFinished()
{
m_settings.m_filterTo = ui->filterTo->text();
filter();
applySettings();
}
void PacketDemodGUI::on_filterPID_stateChanged(int state)
{
m_settings.m_filterPID = state==Qt::Checked ? "f0" : "";
filter();
applySettings();
}
void PacketDemodGUI::on_clearTable_clicked()
{
ui->packets->setRowCount(0);
}
void PacketDemodGUI::filterRow(int row)
{
bool hidden = false;
if (m_settings.m_filterFrom != "")
{
QRegExp re(m_settings.m_filterFrom);
QTableWidgetItem *fromItem = ui->packets->item(row, PACKET_COL_FROM);
if (!re.exactMatch(fromItem->text()))
hidden = true;
}
if (m_settings.m_filterTo != "")
{
QRegExp re(m_settings.m_filterTo);
QTableWidgetItem *toItem = ui->packets->item(row, PACKET_COL_TO);
if (!re.exactMatch(toItem->text()))
hidden = true;
}
if (m_settings.m_filterPID != "")
{
QTableWidgetItem *pidItem = ui->packets->item(row, PACKET_COL_PID);
if (pidItem->text() != m_settings.m_filterPID)
hidden = true;
}
ui->packets->setRowHidden(row, hidden);
}
void PacketDemodGUI::filter()
{
for (int i = 0; i < ui->packets->rowCount(); i++)
{
filterRow(i);
}
}
void PacketDemodGUI::onWidgetRolled(QWidget* widget, bool rollDown)
{
(void) widget;
(void) rollDown;
}
void PacketDemodGUI::onMenuDialogCalled(const QPoint &p)
{
if (m_contextMenuType == ContextMenuChannelSettings)
{
BasicChannelSettingsDialog dialog(&m_channelMarker, this);
dialog.setUseReverseAPI(m_settings.m_useReverseAPI);
dialog.setReverseAPIAddress(m_settings.m_reverseAPIAddress);
dialog.setReverseAPIPort(m_settings.m_reverseAPIPort);
dialog.setReverseAPIDeviceIndex(m_settings.m_reverseAPIDeviceIndex);
dialog.setReverseAPIChannelIndex(m_settings.m_reverseAPIChannelIndex);
dialog.move(p);
dialog.exec();
m_settings.m_rgbColor = m_channelMarker.getColor().rgb();
m_settings.m_title = m_channelMarker.getTitle();
m_settings.m_useReverseAPI = dialog.useReverseAPI();
m_settings.m_reverseAPIAddress = dialog.getReverseAPIAddress();
m_settings.m_reverseAPIPort = dialog.getReverseAPIPort();
m_settings.m_reverseAPIDeviceIndex = dialog.getReverseAPIDeviceIndex();
m_settings.m_reverseAPIChannelIndex = dialog.getReverseAPIChannelIndex();
setWindowTitle(m_settings.m_title);
setTitleColor(m_settings.m_rgbColor);
applySettings();
}
else if ((m_contextMenuType == ContextMenuStreamSettings) && (m_deviceUISet->m_deviceMIMOEngine))
{
DeviceStreamSelectionDialog dialog(this);
dialog.setNumberOfStreams(m_packetDemod->getNumberOfDeviceStreams());
dialog.setStreamIndex(m_settings.m_streamIndex);
dialog.move(p);
dialog.exec();
m_settings.m_streamIndex = dialog.getSelectedStreamIndex();
m_channelMarker.clearStreamIndexes();
m_channelMarker.addStreamIndex(m_settings.m_streamIndex);
displayStreamIndex();
applySettings();
}
resetContextMenuType();
}
PacketDemodGUI::PacketDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent) :
ChannelGUI(parent),
ui(new Ui::PacketDemodGUI),
m_pluginAPI(pluginAPI),
m_deviceUISet(deviceUISet),
m_channelMarker(this),
m_doApplySettings(true),
m_tickCount(0)
{
ui->setupUi(this);
setAttribute(Qt::WA_DeleteOnClose, true);
connect(this, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool)));
connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &)));
m_packetDemod = reinterpret_cast<PacketDemod*>(rxChannel);
m_packetDemod->setMessageQueueToGUI(getInputMessageQueue());
connect(&MainCore::instance()->getMasterTimer(), SIGNAL(timeout()), this, SLOT(tick())); // 50 ms
ui->deltaFrequencyLabel->setText(QString("%1f").arg(QChar(0x94, 0x03)));
ui->deltaFrequency->setColorMapper(ColorMapper(ColorMapper::GrayGold));
ui->deltaFrequency->setValueRange(false, 7, -9999999, 9999999);
ui->channelPowerMeter->setColorTheme(LevelMeterSignalDB::ColorGreenAndBlue);
m_channelMarker.blockSignals(true);
m_channelMarker.setColor(Qt::yellow);
m_channelMarker.setBandwidth(m_settings.m_rfBandwidth);
m_channelMarker.setCenterFrequency(m_settings.m_inputFrequencyOffset);
m_channelMarker.setTitle("Packet Demodulator");
m_channelMarker.blockSignals(false);
m_channelMarker.setVisible(true); // activate signal on the last setting only
setTitleColor(m_channelMarker.getColor());
m_settings.setChannelMarker(&m_channelMarker);
m_deviceUISet->addChannelMarker(&m_channelMarker);
m_deviceUISet->addRollupWidget(this);
connect(&m_channelMarker, SIGNAL(changedByCursor()), this, SLOT(channelMarkerChangedByCursor()));
connect(&m_channelMarker, SIGNAL(highlightedByCursor()), this, SLOT(channelMarkerHighlightedByCursor()));
connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages()));
// Resize the table using dummy data
resizeTable();
// Allow user to reorder columns
ui->packets->horizontalHeader()->setSectionsMovable(true);
// Allow user to sort table by clicking on headers
ui->packets->setSortingEnabled(true);
// Add context menu to allow hiding/showing of columns
menu = new QMenu(ui->packets);
for (int i = 0; i < ui->packets->horizontalHeader()->count(); i++)
{
QString text = ui->packets->horizontalHeaderItem(i)->text();
menu->addAction(createCheckableItem(text, i, true));
}
ui->packets->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu);
connect(ui->packets->horizontalHeader(), SIGNAL(customContextMenuRequested(QPoint)), SLOT(columnSelectMenu(QPoint)));
// Get signals when columns change
connect(ui->packets->horizontalHeader(), SIGNAL(sectionMoved(int, int, int)), SLOT(packets_sectionMoved(int, int, int)));
connect(ui->packets->horizontalHeader(), SIGNAL(sectionResized(int, int, int)), SLOT(packets_sectionResized(int, int, int)));
displaySettings();
applySettings(true);
}
PacketDemodGUI::~PacketDemodGUI()
{
delete ui;
}
void PacketDemodGUI::blockApplySettings(bool block)
{
m_doApplySettings = !block;
}
void PacketDemodGUI::applySettings(bool force)
{
if (m_doApplySettings)
{
PacketDemod::MsgConfigurePacketDemod* message = PacketDemod::MsgConfigurePacketDemod::create( m_settings, force);
m_packetDemod->getInputMessageQueue()->push(message);
}
}
void PacketDemodGUI::displaySettings()
{
m_channelMarker.blockSignals(true);
m_channelMarker.setBandwidth(m_settings.m_rfBandwidth);
m_channelMarker.setCenterFrequency(m_settings.m_inputFrequencyOffset);
m_channelMarker.setTitle(m_settings.m_title);
m_channelMarker.blockSignals(false);
m_channelMarker.setColor(m_settings.m_rgbColor); // activate signal on the last setting only
setTitleColor(m_settings.m_rgbColor);
setWindowTitle(m_channelMarker.getTitle());
blockApplySettings(true);
ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency());
ui->rfBWText->setText(QString("%1k").arg(m_settings.m_rfBandwidth / 1000.0, 0, 'f', 1));
ui->rfBW->setValue(m_settings.m_rfBandwidth / 100.0);
ui->fmDevText->setText(QString("%1k").arg(m_settings.m_fmDeviation / 1000.0, 0, 'f', 1));
ui->fmDev->setValue(m_settings.m_fmDeviation / 100.0);
displayStreamIndex();
ui->filterFrom->setText(m_settings.m_filterFrom);
ui->filterTo->setText(m_settings.m_filterTo);
ui->filterPID->setChecked(m_settings.m_filterPID == "f0");
// Order and size columns
QHeaderView *header = ui->packets->horizontalHeader();
for (int i = 0; i < PACKETDEMOD_COLUMNS; i++)
{
bool hidden = m_settings.m_columnSizes[i] == 0;
header->setSectionHidden(i, hidden);
menu->actions().at(i)->setChecked(!hidden);
if (m_settings.m_columnSizes[i] > 0)
ui->packets->setColumnWidth(i, m_settings.m_columnSizes[i]);
header->moveSection(header->visualIndex(i), m_settings.m_columnIndexes[i]);
}
filter();
blockApplySettings(false);
}
void PacketDemodGUI::displayStreamIndex()
{
if (m_deviceUISet->m_deviceMIMOEngine) {
setStreamIndicator(tr("%1").arg(m_settings.m_streamIndex));
} else {
setStreamIndicator("S"); // single channel indicator
}
}
void PacketDemodGUI::leaveEvent(QEvent*)
{
m_channelMarker.setHighlighted(false);
}
void PacketDemodGUI::enterEvent(QEvent*)
{
m_channelMarker.setHighlighted(true);
}
void PacketDemodGUI::tick()
{
double magsqAvg, magsqPeak;
int nbMagsqSamples;
m_packetDemod->getMagSqLevels(magsqAvg, magsqPeak, nbMagsqSamples);
double powDbAvg = CalcDb::dbPower(magsqAvg);
double powDbPeak = CalcDb::dbPower(magsqPeak);
ui->channelPowerMeter->levelChanged(
(100.0f + powDbAvg) / 100.0f,
(100.0f + powDbPeak) / 100.0f,
nbMagsqSamples);
if (m_tickCount % 4 == 0) {
ui->channelPower->setText(QString::number(powDbAvg, 'f', 1));
}
m_tickCount++;
}

View File

@ -0,0 +1,119 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2016 Edouard Griffiths, F4EXB //
// 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_PACKETDEMODGUI_H
#define INCLUDE_PACKETDEMODGUI_H
#include <QIcon>
#include <QAbstractListModel>
#include <QModelIndex>
#include <QProgressDialog>
#include <QGeoLocation>
#include <QGeoCoordinate>
#include <QTableWidgetItem>
#include <QPushButton>
#include <QToolButton>
#include <QHBoxLayout>
#include <QMenu>
#include "channel/channelgui.h"
#include "dsp/channelmarker.h"
#include "dsp/movingaverage.h"
#include "util/messagequeue.h"
#include "packetdemodsettings.h"
class PluginAPI;
class DeviceUISet;
class BasebandSampleSink;
class PacketDemod;
class PacketDemodGUI;
namespace Ui {
class PacketDemodGUI;
}
class PacketDemodGUI;
class PacketDemodGUI : public ChannelGUI {
Q_OBJECT
public:
static PacketDemodGUI* create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel);
virtual void destroy();
void resetToDefaults();
QByteArray serialize() const;
bool deserialize(const QByteArray& data);
virtual MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; }
public slots:
void channelMarkerChangedByCursor();
void channelMarkerHighlightedByCursor();
private:
Ui::PacketDemodGUI* ui;
PluginAPI* m_pluginAPI;
DeviceUISet* m_deviceUISet;
ChannelMarker m_channelMarker;
PacketDemodSettings m_settings;
bool m_doApplySettings;
PacketDemod* m_packetDemod;
int m_basebandSampleRate;
uint32_t m_tickCount;
MessageQueue m_inputMessageQueue;
QMenu *menu; // Column select context menu
explicit PacketDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent = 0);
virtual ~PacketDemodGUI();
void blockApplySettings(bool block);
void applySettings(bool force = false);
void displaySettings();
void displayStreamIndex();
void packetReceived(QByteArray packet);
bool handleMessage(const Message& message);
void leaveEvent(QEvent*);
void enterEvent(QEvent*);
void resizeTable();
QAction *createCheckableItem(QString& text, int idx, bool checked);
private slots:
void on_deltaFrequency_changed(qint64 value);
void on_mode_currentIndexChanged(int value);
void on_rfBW_valueChanged(int index);
void on_fmDev_valueChanged(int value);
void on_filterFrom_editingFinished();
void on_filterTo_editingFinished();
void on_filterPID_stateChanged(int state);
void on_clearTable_clicked();
void filterRow(int row);
void filter();
void packets_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex);
void packets_sectionResized(int logicalIndex, int oldSize, int newSize);
void columnSelectMenu(QPoint pos);
void columnSelectMenuChecked(bool checked = false);
void onWidgetRolled(QWidget* widget, bool rollDown);
void onMenuDialogCalled(const QPoint& p);
void handleInputMessages();
void tick();
};
#endif // INCLUDE_PACKETDEMODGUI_H

View File

@ -0,0 +1,579 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PacketDemodGUI</class>
<widget class="RollupWidget" name="PacketDemodGUI">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>398</width>
<height>519</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>352</width>
<height>0</height>
</size>
</property>
<property name="font">
<font>
<family>Liberation Sans</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
<property name="windowTitle">
<string>Packet Demodulator</string>
</property>
<property name="statusTip">
<string>Packet Demodulator</string>
</property>
<widget class="QWidget" name="settingsContainer" native="true">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>390</width>
<height>101</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>350</width>
<height>0</height>
</size>
</property>
<property name="windowTitle">
<string>Settings</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>3</number>
</property>
<property name="leftMargin">
<number>2</number>
</property>
<property name="topMargin">
<number>2</number>
</property>
<property name="rightMargin">
<number>2</number>
</property>
<property name="bottomMargin">
<number>2</number>
</property>
<item>
<layout class="QHBoxLayout" name="powLayout">
<property name="topMargin">
<number>2</number>
</property>
<item>
<widget class="QLabel" name="deltaFrequencyLabel">
<property name="minimumSize">
<size>
<width>16</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Df</string>
</property>
</widget>
</item>
<item>
<widget class="ValueDialZ" name="deltaFrequency" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>32</width>
<height>16</height>
</size>
</property>
<property name="font">
<font>
<family>Liberation Mono</family>
<pointsize>12</pointsize>
</font>
</property>
<property name="cursor">
<cursorShape>PointingHandCursor</cursorShape>
</property>
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
<property name="toolTip">
<string>Demod shift frequency from center in Hz</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="deltaUnits">
<property name="text">
<string>Hz </string>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="channelPowerLayout">
<item>
<widget class="QLabel" name="channelPower">
<property name="toolTip">
<string>Channel power</string>
</property>
<property name="layoutDirection">
<enum>Qt::RightToLeft</enum>
</property>
<property name="text">
<string>0.0</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="channelPowerUnits">
<property name="text">
<string> dB</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="buttonLayout">
<item>
<widget class="QLabel" name="channelPowerMeterUnits">
<property name="text">
<string>dB</string>
</property>
</widget>
</item>
<item>
<widget class="LevelMeterSignalDB" name="channelPowerMeter" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>24</height>
</size>
</property>
<property name="font">
<font>
<family>Liberation Mono</family>
<pointsize>8</pointsize>
</font>
</property>
<property name="toolTip">
<string>Level meter (dB) top trace: average, bottom trace: instantaneous peak, tip: peak hold</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QComboBox" name="mode">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>Baud rate and modulation</string>
</property>
<item>
<property name="text">
<string>1200 AFSK</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="Line" name="line_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="rfBWLabel">
<property name="text">
<string>BW</string>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="rfBW">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>RF bandwidth</string>
</property>
<property name="minimum">
<number>10</number>
</property>
<property name="maximum">
<number>400</number>
</property>
<property name="pageStep">
<number>1</number>
</property>
<property name="value">
<number>100</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="rfBWText">
<property name="minimumSize">
<size>
<width>30</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>10.0k</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="fmDevLabel">
<property name="text">
<string>Dev</string>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="fmDev">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>Frequency deviation</string>
</property>
<property name="minimum">
<number>10</number>
</property>
<property name="maximum">
<number>60</number>
</property>
<property name="pageStep">
<number>1</number>
</property>
<property name="value">
<number>50</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="fmDevText">
<property name="minimumSize">
<size>
<width>30</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>5.0k</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="filterFromLabel">
<property name="text">
<string>From</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="filterFrom">
<property name="toolTip">
<string>Display only packets where the source address (From) matches the specified regular expression</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="filterToLabel">
<property name="text">
<string>To:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="filterTo">
<property name="toolTip">
<string>Display only packets where the destination address (To) matches the specified regular expression</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="filterPID">
<property name="toolTip">
<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 name="text">
<string>PID No L3</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="clearTable">
<property name="toolTip">
<string>Clear packets from table</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/bin.png</normaloff>:/bin.png</iconset>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="dataContainer" native="true">
<property name="geometry">
<rect>
<x>0</x>
<y>110</y>
<width>391</width>
<height>261</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Received Packets</string>
</property>
<layout class="QVBoxLayout" name="verticalLayoutTable">
<property name="spacing">
<number>2</number>
</property>
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<item>
<widget class="QTableWidget" name="packets">
<property name="toolTip">
<string>Received packets</string>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<column>
<property name="text">
<string>From</string>
</property>
<property name="toolTip">
<string>Source callsign/address</string>
</property>
</column>
<column>
<property name="text">
<string>To</string>
</property>
<property name="toolTip">
<string>Destination callsign/address</string>
</property>
</column>
<column>
<property name="text">
<string>Via</string>
</property>
<property name="toolTip">
<string>Repeater addresses</string>
</property>
</column>
<column>
<property name="text">
<string>Type</string>
</property>
<property name="toolTip">
<string>AX.25 frame type</string>
</property>
</column>
<column>
<property name="text">
<string>PID</string>
</property>
<property name="toolTip">
<string>Layer 3 protocol ID</string>
</property>
</column>
<column>
<property name="text">
<string>Data (ASCII)</string>
</property>
<property name="toolTip">
<string>Packet data as ASCII</string>
</property>
</column>
<column>
<property name="text">
<string>Data (Hex)</string>
</property>
<property name="toolTip">
<string>Packet data as hex</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
</widget>
<customwidgets>
<customwidget>
<class>RollupWidget</class>
<extends>QWidget</extends>
<header>gui/rollupwidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>LevelMeterSignalDB</class>
<extends>QWidget</extends>
<header>gui/levelmeter.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>ValueDialZ</class>
<extends>QWidget</extends>
<header>gui/valuedialz.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>packets</tabstop>
</tabstops>
<resources>
<include location="../../../sdrgui/resources/res.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -0,0 +1,92 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2016 Edouard Griffiths, F4EXB //
// 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 <QtPlugin>
#include "plugin/pluginapi.h"
#ifndef SERVER_MODE
#include "packetdemodgui.h"
#endif
#include "packetdemod.h"
#include "packetdemodwebapiadapter.h"
#include "packetdemodplugin.h"
const PluginDescriptor PacketDemodPlugin::m_pluginDescriptor = {
PacketDemod::m_channelId,
QStringLiteral("Packet Demodulator"),
QStringLiteral("6.4.0"),
QStringLiteral("(c) Jon Beniston, M7RCE"),
QStringLiteral("https://github.com/f4exb/sdrangel"),
true,
QStringLiteral("https://github.com/f4exb/sdrangel")
};
PacketDemodPlugin::PacketDemodPlugin(QObject* parent) :
QObject(parent),
m_pluginAPI(0)
{
}
const PluginDescriptor& PacketDemodPlugin::getPluginDescriptor() const
{
return m_pluginDescriptor;
}
void PacketDemodPlugin::initPlugin(PluginAPI* pluginAPI)
{
m_pluginAPI = pluginAPI;
m_pluginAPI->registerRxChannel(PacketDemod::m_channelIdURI, PacketDemod::m_channelId, this);
}
void PacketDemodPlugin::createRxChannel(DeviceAPI *deviceAPI, BasebandSampleSink **bs, ChannelAPI **cs) const
{
if (bs || cs)
{
PacketDemod *instance = new PacketDemod(deviceAPI);
if (bs) {
*bs = instance;
}
if (cs) {
*cs = instance;
}
}
}
#ifdef SERVER_MODE
ChannelGUI* PacketDemodPlugin::createRxChannelGUI(
DeviceUISet *deviceUISet,
BasebandSampleSink *rxChannel) const
{
(void) deviceUISet;
(void) rxChannel;
return 0;
}
#else
ChannelGUI* PacketDemodPlugin::createRxChannelGUI(DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) const
{
return PacketDemodGUI::create(m_pluginAPI, deviceUISet, rxChannel);
}
#endif
ChannelWebAPIAdapter* PacketDemodPlugin::createChannelWebAPIAdapter() const
{
return new PacketDemodWebAPIAdapter();
}

View File

@ -0,0 +1,49 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2016 Edouard Griffiths, F4EXB //
// 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_PACKETDEMODPLUGIN_H
#define INCLUDE_PACKETDEMODPLUGIN_H
#include <QObject>
#include "plugin/plugininterface.h"
class DeviceUISet;
class BasebandSampleSink;
class PacketDemodPlugin : public QObject, PluginInterface {
Q_OBJECT
Q_INTERFACES(PluginInterface)
Q_PLUGIN_METADATA(IID "sdrangel.channel.packetdemod")
public:
explicit PacketDemodPlugin(QObject* parent = NULL);
const PluginDescriptor& getPluginDescriptor() const;
void initPlugin(PluginAPI* pluginAPI);
virtual void createRxChannel(DeviceAPI *deviceAPI, BasebandSampleSink **bs, ChannelAPI **cs) const;
virtual ChannelGUI* createRxChannelGUI(DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) const;
virtual ChannelWebAPIAdapter* createChannelWebAPIAdapter() const;
private:
static const PluginDescriptor m_pluginDescriptor;
PluginAPI* m_pluginAPI;
};
#endif // INCLUDE_PACKETDEMODPLUGIN_H

View File

@ -0,0 +1,145 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2015 Edouard Griffiths, F4EXB. //
// 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 <QColor>
#include "dsp/dspengine.h"
#include "util/simpleserializer.h"
#include "settings/serializable.h"
#include "packetdemodsettings.h"
PacketDemodSettings::PacketDemodSettings() :
m_channelMarker(0)
{
resetToDefaults();
}
void PacketDemodSettings::resetToDefaults()
{
m_inputFrequencyOffset = 0;
m_baud = 1200;
m_rfBandwidth = 12500.0f;
m_fmDeviation = 2500.0f;
m_filterFrom = "";
m_filterTo = "";
m_filterPID = "";
m_rgbColor = QColor(255, 255, 0).rgb();
m_title = "Packet Demodulator";
m_streamIndex = 0;
m_useReverseAPI = false;
m_reverseAPIAddress = "127.0.0.1";
m_reverseAPIPort = 8888;
m_reverseAPIDeviceIndex = 0;
m_reverseAPIChannelIndex = 0;
for (int i = 0; i < PACKETDEMOD_COLUMNS; i++)
{
m_columnIndexes[i] = i;
m_columnSizes[i] = -1; // Autosize
}
}
QByteArray PacketDemodSettings::serialize() const
{
SimpleSerializer s(1);
s.writeS32(1, m_inputFrequencyOffset);
s.writeS32(2, m_streamIndex);
s.writeString(4, m_filterFrom);
s.writeString(4, m_filterTo);
s.writeString(5, m_filterPID);
if (m_channelMarker) {
s.writeBlob(6, m_channelMarker->serialize());
}
s.writeU32(7, m_rgbColor);
s.writeString(9, m_title);
s.writeBool(14, m_useReverseAPI);
s.writeString(15, m_reverseAPIAddress);
s.writeU32(16, m_reverseAPIPort);
s.writeU32(17, m_reverseAPIDeviceIndex);
s.writeU32(18, m_reverseAPIChannelIndex);
for (int i = 0; i < PACKETDEMOD_COLUMNS; i++)
s.writeS32(100 + i, m_columnIndexes[i]);
for (int i = 0; i < PACKETDEMOD_COLUMNS; i++)
s.writeS32(200 + i, m_columnSizes[i]);
return s.final();
}
bool PacketDemodSettings::deserialize(const QByteArray& data)
{
SimpleDeserializer d(data);
if(!d.isValid())
{
resetToDefaults();
return false;
}
if(d.getVersion() == 1)
{
QByteArray bytetmp;
uint32_t utmp;
QString strtmp;
d.readS32(1, &m_inputFrequencyOffset, 0);
d.readS32(2, &m_streamIndex, 0);
d.readString(3, &m_filterFrom, "");
d.readString(4, &m_filterTo, "");
d.readString(5, &m_filterPID, "");
d.readBlob(6, &bytetmp);
if (m_channelMarker) {
m_channelMarker->deserialize(bytetmp);
}
d.readU32(7, &m_rgbColor);
d.readString(9, &m_title, "Packet Demodulator");
d.readBool(14, &m_useReverseAPI, false);
d.readString(15, &m_reverseAPIAddress, "127.0.0.1");
d.readU32(16, &utmp, 0);
if ((utmp > 1023) && (utmp < 65535)) {
m_reverseAPIPort = utmp;
} else {
m_reverseAPIPort = 8888;
}
d.readU32(17, &utmp, 0);
m_reverseAPIDeviceIndex = utmp > 99 ? 99 : utmp;
d.readU32(18, &utmp, 0);
m_reverseAPIChannelIndex = utmp > 99 ? 99 : utmp;
for (int i = 0; i < PACKETDEMOD_COLUMNS; i++)
d.readS32(100 + i, &m_columnIndexes[i], i);
for (int i = 0; i < PACKETDEMOD_COLUMNS; i++)
d.readS32(200 + i, &m_columnSizes[i], -1);
return true;
}
else
{
resetToDefaults();
return false;
}
}

View File

@ -0,0 +1,61 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2017 Edouard Griffiths, F4EXB. //
// 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_PACKETDEMODSETTINGS_H
#define INCLUDE_PACKETDEMODSETTINGS_H
#include <QByteArray>
#include <QHash>
class Serializable;
// Number of columns in the table
#define PACKETDEMOD_COLUMNS 7
struct PacketDemodSettings
{
qint32 m_inputFrequencyOffset;
qint32 m_baud;
Real m_rfBandwidth;
Real m_fmDeviation;
QString m_filterFrom;
QString m_filterTo;
QString m_filterPID;
quint32 m_rgbColor;
QString m_title;
Serializable *m_channelMarker;
QString m_audioDeviceName;
int m_streamIndex; //!< MIMO channel. Not relevant when connected to SI (single Rx).
bool m_useReverseAPI;
QString m_reverseAPIAddress;
uint16_t m_reverseAPIPort;
uint16_t m_reverseAPIDeviceIndex;
uint16_t m_reverseAPIChannelIndex;
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
PacketDemodSettings();
void resetToDefaults();
void setChannelMarker(Serializable *channelMarker) { m_channelMarker = channelMarker; }
QByteArray serialize() const;
bool deserialize(const QByteArray& data);
};
#endif /* INCLUDE_PACKETDEMODSETTINGS_H */

View File

@ -0,0 +1,306 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2019 Edouard Griffiths, F4EXB //
// 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 <complex.h>
#include "dsp/dspengine.h"
#include "dsp/dspengine.h"
#include "util/db.h"
#include "util/stepfunctions.h"
#include "pipes/pipeendpoint.h"
#include "maincore.h"
#include "packetdemod.h"
#include "packetdemodsink.h"
PacketDemodSink::PacketDemodSink(PacketDemod *packetDemod) :
m_packetDemod(packetDemod),
m_channelSampleRate(PACKETDEMOD_CHANNEL_SAMPLE_RATE),
m_channelFrequencyOffset(0),
m_magsqSum(0.0f),
m_magsqPeak(0.0f),
m_magsqCount(0),
m_messageQueueToChannel(nullptr),
m_f1(nullptr),
m_f0(nullptr),
m_corrBuf(nullptr),
m_corrIdx(0),
m_corrCnt(0)
{
m_magsq = 0.0;
applySettings(m_settings, true);
applyChannelSettings(m_channelSampleRate, m_channelFrequencyOffset, true);
}
PacketDemodSink::~PacketDemodSink()
{
}
void PacketDemodSink::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end)
{
Complex ci;
for (SampleVector::const_iterator it = begin; it != end; ++it)
{
Complex c(it->real(), it->imag());
c *= m_nco.nextIQ();
if (m_interpolatorDistance < 1.0f) // interpolate
{
while (!m_interpolator.interpolate(&m_interpolatorDistanceRemain, c, &ci))
{
processOneSample(ci);
m_interpolatorDistanceRemain += m_interpolatorDistance;
}
}
else // decimate
{
if (m_interpolator.decimate(&m_interpolatorDistanceRemain, c, &ci))
{
processOneSample(ci);
m_interpolatorDistanceRemain += m_interpolatorDistance;
}
}
}
}
void PacketDemodSink::processOneSample(Complex &ci)
{
Complex ca;
// FM demodulation
double magsqRaw;
Real deviation;
Real fmDemod = m_phaseDiscri.phaseDiscriminatorDelta(ci, magsqRaw, deviation);
// Calculate average and peak levels for level meter
Real magsq = magsqRaw / (SDR_RX_SCALED*SDR_RX_SCALED);
m_movingAverage(magsq);
m_magsq = m_movingAverage.asDouble();
m_magsqSum += magsq;
if (magsq > m_magsqPeak)
{
m_magsqPeak = magsq;
}
m_magsqCount++;
m_corrBuf[m_corrIdx] = fmDemod;
if (m_corrCnt >= m_correlationLength && magsq > 1e-7)
{
// Correlate with 1200 + 2200 baud complex exponentials
Complex corrF0 = 0.0f;
Complex corrF1 = 0.0f;
for (int i = 0; i < m_correlationLength; i++)
{
int j = m_corrIdx - i;
if (j < 0)
j += m_correlationLength;
corrF0 += m_f0[i] * m_corrBuf[j];
corrF1 += m_f1[i] * m_corrBuf[j];
}
m_corrCnt--; // Avoid overflow in increment below
// Low pass filter, to minimize changes above the baud rate
Real f0Filt = m_lowpassF0.filter(std::abs(corrF0));
Real f1Filt = m_lowpassF1.filter(std::abs(corrF1));
// Determine which is the closest match and then quantise to 1 or -1
// FIXME: We should try to account for the fact that higher frequencies can have preemphasis
float diff = f1Filt - f0Filt;
int sample = diff >= 0.0f ? 1 : 0;
// Look for edge
if (sample != m_samplePrev)
{
m_syncCount = PACKETDEMOD_CHANNEL_SAMPLE_RATE/m_settings.m_baud/2;
}
else
{
m_syncCount--;
if (m_syncCount <= 0)
{
// HDLC deframing
// Should be in the middle of the symbol
// NRZI decoding
int bit;
if (sample != m_symbolPrev)
bit = 0;
else
bit = 1;
m_symbolPrev = sample;
// Store in shift reg
m_bits |= bit << m_bitCount;
m_bitCount++;
if (bit == 1)
{
m_onesCount++;
// Shouldn't ever get 7 1s in a row
if ((m_onesCount == 7) && m_gotSOP)
{
m_gotSOP = false;
m_byteCount = 0;
}
}
else if (bit == 0)
{
if (m_onesCount == 5)
{
// Remove bit-stuffing (5 1s followed by a 0)
m_bitCount--;
}
else if (m_onesCount == 6)
{
// Start/end of packet
if ((m_bitCount == 8) && (m_bits == 0x7e) && (m_byteCount > 0))
{
// End of packet
// Check CRC is valid
m_crc.init();
m_crc.calculate(m_bytes, m_byteCount - 2);
uint16_t calcCrc = m_crc.get();
uint16_t rxCrc = m_bytes[m_byteCount-2] | (m_bytes[m_byteCount-1] << 8);
if (calcCrc == rxCrc)
{
QByteArray rxPacket((char *)m_bytes, m_byteCount);
qDebug() << "RX: " << rxPacket.toHex();
if (getMessageQueueToChannel())
{
MainCore::MsgPacket *msg = MainCore::MsgPacket::create(m_packetDemod, rxPacket, QDateTime::currentDateTime()); // FIXME pointer
getMessageQueueToChannel()->push(msg);
}
}
else
qDebug() << QString("CRC mismatch: %1 %2").arg(calcCrc, 4, 16, QLatin1Char('0')).arg(rxCrc, 4, 16, QLatin1Char('0'));
// Reset state to start receiving next packet
m_gotSOP = false;
m_bits = 0;
m_bitCount = 0;
m_byteCount = 0;
}
else
{
// Start of packet
m_gotSOP = true;
m_bits = 0;
m_bitCount = 0;
m_byteCount = 0;
}
}
m_onesCount = 0;
}
if (m_gotSOP)
{
if (m_bitCount == 8)
{
if (m_byteCount >= 512)
{
// Too many bytes
m_gotSOP = false;
m_byteCount = 0;
}
else
{
m_bytes[m_byteCount] = m_bits;
m_byteCount++;
}
m_bits = 0;
m_bitCount = 0;
}
}
m_syncCount = PACKETDEMOD_CHANNEL_SAMPLE_RATE/m_settings.m_baud;
}
}
m_samplePrev = sample;
}
m_corrIdx = (m_corrIdx + 1) % m_correlationLength;
m_corrCnt++;
}
void PacketDemodSink::applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force)
{
qDebug() << "PacketDemodSink::applyChannelSettings:"
<< " channelSampleRate: " << channelSampleRate
<< " channelFrequencyOffset: " << channelFrequencyOffset;
if ((m_channelFrequencyOffset != channelFrequencyOffset) ||
(m_channelSampleRate != channelSampleRate) || force)
{
m_nco.setFreq(-channelFrequencyOffset, channelSampleRate);
}
if ((m_channelSampleRate != channelSampleRate) || force)
{
m_interpolator.create(16, channelSampleRate, PACKETDEMOD_CHANNEL_BANDWIDTH);
m_interpolatorDistanceRemain = 0;
m_interpolatorDistance = (Real) channelSampleRate / (Real) PACKETDEMOD_CHANNEL_SAMPLE_RATE;
}
m_channelSampleRate = channelSampleRate;
m_channelFrequencyOffset = channelFrequencyOffset;
}
void PacketDemodSink::applySettings(const PacketDemodSettings& settings, bool force)
{
qDebug() << "PacketDemodSink::applySettings:"
<< " force: " << force;
if (force)
{
m_lowpass.create(301, PACKETDEMOD_CHANNEL_SAMPLE_RATE, settings.m_rfBandwidth / 2.0f);
m_phaseDiscri.setFMScaling(PACKETDEMOD_CHANNEL_SAMPLE_RATE / (2.0f * settings.m_fmDeviation));
delete m_f1;
delete m_f0;
delete m_corrBuf;
m_correlationLength = PACKETDEMOD_CHANNEL_SAMPLE_RATE/settings.m_baud;
m_f1 = new Complex[m_correlationLength]();
m_f0 = new Complex[m_correlationLength]();
m_corrBuf = new Complex[m_correlationLength]();
m_corrIdx = 0;
m_corrCnt = 0;
Real f0 = 0.0f;
Real f1 = 0.0f;
for (int i = 0; i < m_correlationLength; i++)
{
m_f0[i] = Complex(cos(f0), sin(f0));
m_f1[i] = Complex(cos(f1), sin(f1));
f0 += 2.0f*(Real)M_PI*2200.0f/PACKETDEMOD_CHANNEL_SAMPLE_RATE;
f1 += 2.0f*(Real)M_PI*1200.0f/PACKETDEMOD_CHANNEL_SAMPLE_RATE;
}
m_lowpassF1.create(301, PACKETDEMOD_CHANNEL_SAMPLE_RATE, settings.m_baud * 1.1f);
m_lowpassF0.create(301, PACKETDEMOD_CHANNEL_SAMPLE_RATE, settings.m_baud * 1.1f);
m_samplePrev = 0;
m_syncCount = 0;
m_symbolPrev = 0;
m_bits = 0;
m_bitCount = 0;
m_onesCount = 0;
m_gotSOP = false;
m_byteCount = 0;
}
m_settings = settings;
}

View File

@ -0,0 +1,135 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2019 Edouard Griffiths, F4EXB //
// 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_PACKETDEMODSINK_H
#define INCLUDE_PACKETDEMODSINK_H
#include "dsp/channelsamplesink.h"
#include "dsp/phasediscri.h"
#include "dsp/nco.h"
#include "dsp/interpolator.h"
#include "dsp/firfilter.h"
#include "util/movingaverage.h"
#include "util/doublebufferfifo.h"
#include "util/messagequeue.h"
#include "util/crc.h"
#include "packetdemodsettings.h"
#include <vector>
#include <iostream>
#include <fstream>
#define PACKETDEMOD_CHANNEL_BANDWIDTH 9600
// Must be integer multiple of m_baud=1200
#define PACKETDEMOD_CHANNEL_SAMPLE_RATE 38400
class PacketDemod;
class PacketDemodSink : public ChannelSampleSink {
public:
PacketDemodSink(PacketDemod *packetDemod);
~PacketDemodSink();
virtual void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end);
void applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force = false);
void applySettings(const PacketDemodSettings& settings, bool force = false);
void setMessageQueueToChannel(MessageQueue *messageQueue) { m_messageQueueToChannel = messageQueue; }
double getMagSq() const { return m_magsq; }
void getMagSqLevels(double& avg, double& peak, int& nbSamples)
{
if (m_magsqCount > 0)
{
m_magsq = m_magsqSum / m_magsqCount;
m_magSqLevelStore.m_magsq = m_magsq;
m_magSqLevelStore.m_magsqPeak = m_magsqPeak;
}
avg = m_magSqLevelStore.m_magsq;
peak = m_magSqLevelStore.m_magsqPeak;
nbSamples = m_magsqCount == 0 ? 1 : m_magsqCount;
m_magsqSum = 0.0f;
m_magsqPeak = 0.0f;
m_magsqCount = 0;
}
private:
struct MagSqLevelsStore
{
MagSqLevelsStore() :
m_magsq(1e-12),
m_magsqPeak(1e-12)
{}
double m_magsq;
double m_magsqPeak;
};
PacketDemod *m_packetDemod;
PacketDemodSettings m_settings;
int m_channelSampleRate;
int m_channelFrequencyOffset;
NCO m_nco;
Interpolator m_interpolator;
Real m_interpolatorDistance;
Real m_interpolatorDistanceRemain;
double m_magsq;
double m_magsqSum;
double m_magsqPeak;
int m_magsqCount;
MagSqLevelsStore m_magSqLevelStore;
MessageQueue *m_messageQueueToChannel;
MovingAverageUtil<Real, double, 16> m_movingAverage;
Lowpass<Complex> m_lowpass;
PhaseDiscriminators m_phaseDiscri;
int m_correlationLength;
Complex *m_f1;
Complex *m_f0;
Complex *m_corrBuf;
int m_corrIdx;
int m_corrCnt;
Lowpass<Real> m_lowpassF1;
Lowpass<Real> m_lowpassF0;
int m_samplePrev;
int m_syncCount;
int m_symbolPrev;
unsigned char m_bits;
int m_bitCount;
int m_onesCount;
bool m_gotSOP;
unsigned char m_bytes[512]; // Info field can be 256 bytes
int m_byteCount;
crc16x25 m_crc;
void processOneSample(Complex &ci);
MessageQueue *getMessageQueueToChannel() { return m_messageQueueToChannel; }
};
#endif // INCLUDE_PACKETDEMODSINK_H

View File

@ -0,0 +1,52 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2019 Edouard Griffiths, F4EXB. //
// 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 "SWGChannelSettings.h"
#include "packetdemod.h"
#include "packetdemodwebapiadapter.h"
PacketDemodWebAPIAdapter::PacketDemodWebAPIAdapter()
{}
PacketDemodWebAPIAdapter::~PacketDemodWebAPIAdapter()
{}
int PacketDemodWebAPIAdapter::webapiSettingsGet(
SWGSDRangel::SWGChannelSettings& response,
QString& errorMessage)
{
(void) errorMessage;
response.setPacketDemodSettings(new SWGSDRangel::SWGPacketDemodSettings());
response.getPacketDemodSettings()->init();
PacketDemod::webapiFormatChannelSettings(response, m_settings);
return 200;
}
int PacketDemodWebAPIAdapter::webapiSettingsPutPatch(
bool force,
const QStringList& channelSettingsKeys,
SWGSDRangel::SWGChannelSettings& response,
QString& errorMessage)
{
(void) force;
(void) errorMessage;
PacketDemod::webapiUpdateChannelSettings(m_settings, channelSettingsKeys, response);
return 200;
}

View File

@ -0,0 +1,50 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2019 Edouard Griffiths, F4EXB. //
// 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_PACKETDEMOD_WEBAPIADAPTER_H
#define INCLUDE_PACKETDEMOD_WEBAPIADAPTER_H
#include "channel/channelwebapiadapter.h"
#include "packetdemodsettings.h"
/**
* Standalone API adapter only for the settings
*/
class PacketDemodWebAPIAdapter : public ChannelWebAPIAdapter {
public:
PacketDemodWebAPIAdapter();
virtual ~PacketDemodWebAPIAdapter();
virtual QByteArray serialize() const { return m_settings.serialize(); }
virtual bool deserialize(const QByteArray& data) { return m_settings.deserialize(data); }
virtual int webapiSettingsGet(
SWGSDRangel::SWGChannelSettings& response,
QString& errorMessage);
virtual int webapiSettingsPutPatch(
bool force,
const QStringList& channelSettingsKeys,
SWGSDRangel::SWGChannelSettings& response,
QString& errorMessage);
private:
PacketDemodSettings m_settings;
};
#endif // INCLUDE_PACKETDEMOD_WEBAPIADAPTER_H

View File

@ -0,0 +1,63 @@
<h1>Packet radio demodulator plugin</h1>
<h2>Introduction</h2>
This plugin can be used to demodulate packet radio (APRS/AX.25) data packets. Received packets can be sent to the APRS Feature for decoding and display.
<h2>Interface</h2>
![Packet Demodulator plugin GUI](../../../doc/img/PacketDemod_plugin.png)
<h3>1: Frequency shift from center frequency of reception</h3>
Use the wheels to adjust the frequency shift in Hz from the center frequency of reception. Left click on a digit sets the cursor position at this digit. Right click on a digit sets all digits on the right to zero. This effectively floors value at the digit position. Wheels are moved with the mousewheel while pointing at the wheel or by selecting the wheel with the left mouse click and using the keyboard arrows. Pressing shift simultaneously moves digit by 5 and pressing control moves it by 2.
<h3>2: Channel power</h3>
Average total power in dB relative to a +/- 1.0 amplitude signal received in the pass band.
<h3>3: Level meter in dB</h3>
- top bar (green): average value
- bottom bar (blue green): instantaneous peak value
- tip vertical bar (bright green): peak hold value
<h3>4: Modulation</h3>
This specifies the baud rate and modulation that is used for the packet transmission. Currently 1200 baud AFSK is supported.
<h3>5: RF Bandwidth</h3>
This specifies the bandwidth of a LPF that is applied to the input signal to limit the RF bandwidth.
<h3>6: Frequency deviation</h3>
Adjusts the expected frequency deviation in 0.1 kHz steps from 1 to 6 kHz. Typical values are 2.5 kHz and 5 kHz.
<h3>7: Filter Packets From</h3>
Entering a regular expression in the From field displays only packets where the source address, displayed in the From column, matches the regular expression.
<h3>8: Filter Packets To</h3>
Entering a regular expression in the To field displays only packets where the destination address, displayed in the To column, matches the regular expression.
<h3>9: Filter PID No L3</h3>
Checking this option displays only packets where the PID (Protocol ID) field is 0xf0 (no L3). This value is used by APRS and BBS data packets, and helps to filter out control packets.
<h3>10: Clear Packets from table</h3>
Pressing this button clears all packets from the table.
<h3>Received Packets Table</h3>
The received packets table displays the contexts of the packets that have been received. Only packets with valid CRCs are displayed.
* From - The source address / callsign of the sender of the packet.
* To - The destination address.
* Via - List of addresses of repeaters the packet has passed through or directed via.
* Type - The AX.25 frame type.
* PID - Protocol Identifier.
* Data (ASCII) - The AX.25 information field displayed as ASCII.
* Data (Hex) - The AX.25 information field displayed as hexidecimal.

View File

@ -473,10 +473,10 @@ void VORDemod::webapiFormatChannelSettings(
swgVORDemodSettings->setStreamIndex(settings.m_streamIndex);
}
if (channelSettingsKeys.contains("identThreshold") || force) {
swgVORDemodSettings->setAudioMute(settings.m_identThreshold);
swgVORDemodSettings->setIdentThreshold(settings.m_identThreshold);
}
if (channelSettingsKeys.contains("magDecAdjust") || force) {
swgVORDemodSettings->setAudioMute(settings.m_magDecAdjust ? 1 : 0);
swgVORDemodSettings->setMagDecAdjust(settings.m_magDecAdjust ? 1 : 0);
}
}

View File

@ -384,8 +384,8 @@ static bool calcIntersectionPoint(float lat1, float lon1, float bearing1, float
double lat3Rad = asin(sinLat1*cos(delta13)+cosLat1*sin(delta13)*cos(theta13));
double lon3Rad = lon1Rad + atan2(sin(theta13)*sin(delta13)*cosLat1, cos(delta13)-sinLat1*sin(lat3Rad));
intersectLat = Units::radiansToDegress(lat3Rad);
intersectLon = Units::radiansToDegress(lon3Rad);
intersectLat = Units::radiansToDegrees(lat3Rad);
intersectLon = Units::radiansToDegrees(lon3Rad);
return true;
}

View File

@ -222,7 +222,7 @@ void VORDemodSink::processOneSample(Complex &ci)
if (m_varGoertzel.size() == VORDEMOD_CHANNEL_SAMPLE_RATE - 1)
{
m_varGoertzel.goertzel(mag);
varPhase = Units::radiansToDegress(m_varGoertzel.phase());
varPhase = Units::radiansToDegrees(m_varGoertzel.phase());
varMag = m_varGoertzel.mag();
m_varGoertzel.reset();
}
@ -245,7 +245,7 @@ void VORDemodSink::processOneSample(Complex &ci)
if (m_refGoertzel.size() == VORDEMOD_CHANNEL_SAMPLE_RATE - 1)
{
m_refGoertzel.goertzel(phi);
float phaseDeg = Units::radiansToDegress(m_refGoertzel.phase());
float phaseDeg = Units::radiansToDegrees(m_refGoertzel.phase());
double refMag = m_refGoertzel.mag();
int groupDelay = (301-1)/2;
float filterPhaseShift = 360.0*30.0*groupDelay/VORDEMOD_CHANNEL_SAMPLE_RATE;

View File

@ -211,7 +211,7 @@ void VORDemodSCSink::processOneSample(Complex &ci)
if (m_varGoertzel.size() == VORDemodSCSettings::VORDEMOD_CHANNEL_SAMPLE_RATE - 1)
{
m_varGoertzel.goertzel(mag);
varPhase = Units::radiansToDegress(m_varGoertzel.phase());
varPhase = Units::radiansToDegrees(m_varGoertzel.phase());
varMag = m_varGoertzel.mag();
m_varGoertzel.reset();
}
@ -234,7 +234,7 @@ void VORDemodSCSink::processOneSample(Complex &ci)
if (m_refGoertzel.size() == VORDemodSCSettings::VORDEMOD_CHANNEL_SAMPLE_RATE - 1)
{
m_refGoertzel.goertzel(phi);
float phaseDeg = Units::radiansToDegress(m_refGoertzel.phase());
float phaseDeg = Units::radiansToDegrees(m_refGoertzel.phase());
double refMag = m_refGoertzel.mag();
int groupDelay = (301-1)/2;
float filterPhaseShift = 360.0*30.0*groupDelay/VORDemodSCSettings::VORDEMOD_CHANNEL_SAMPLE_RATE;

View File

@ -25,6 +25,7 @@
#include "util/crc.h"
#include "util/messagequeue.h"
#include "maincore.h"
#include "channel/channelapi.h"
PacketModSource::PacketModSource() :
m_channelSampleRate(48000),
@ -424,33 +425,56 @@ void PacketModSource::applyChannelSettings(int channelSampleRate, int channelFre
}
}
static bool ax25_ssid(QByteArray& b, int i, int len, uint8_t& ssid)
{
if (b[i] == '-')
{
if (len > i + 1)
{
ssid = b[i+1] - '0';
if ((len > i + 2) && isdigit(b[i+2])) {
ssid = (ssid*10) + (b[i+2] - '0');
}
if (ssid >= 16)
{
qDebug() << "ax25_address: SSID greater than 15 not supported";
ssid = ssid & 0xf;
return false;
}
else
{
return true;
}
}
else
{
qDebug() << "ax25_address: SSID number missing";
return false;
}
}
else
return false;
}
static uint8_t *ax25_address(uint8_t *p, QString address, uint8_t crrl)
{
int len;
int i;
QByteArray b;
int ssid;
uint8_t ssid = 0;
bool hyphenSeen = false;
len = address.length();
b = address.toUtf8();
ssid = 0;
for (i = 0; i < 6; i++)
{
if ((i < len) && (ssid == 0))
if ((i < len) && !hyphenSeen)
{
if (b[i] == '-')
{
if (len > i + 1)
{
ssid = b[i+1] - '0';
if ((len > i + 2) && isdigit(b[i+2])) {
ssid = (ssid*10) + (b[i+1] - '0');
}
if (ssid >= 16)
qDebug() << "ax25_address: SSID greater than 15 not supported";
}
else
qDebug() << "ax25_address: SSID number missing";
ax25_ssid(b, i, len, ssid);
hyphenSeen = true;
*p++ = ' ' << 1;
}
else
@ -463,6 +487,10 @@ static uint8_t *ax25_address(uint8_t *p, QString address, uint8_t crrl)
*p++ = ' ' << 1;
}
}
if (b[i] == '-')
{
ax25_ssid(b, i, len, ssid);
}
*p++ = crrl | (ssid << 1);
return p;
@ -534,6 +562,7 @@ void PacketModSource::addTXPacket(QString callsign, QString to, QString via, QSt
{
uint8_t packet[AX25_MAX_BYTES];
uint8_t *crc_start;
uint8_t *packet_end;
uint8_t *p;
crc16x25 crc;
uint16_t crcValue;
@ -565,6 +594,7 @@ void PacketModSource::addTXPacket(QString callsign, QString to, QString via, QSt
crcValue = crc.get();
*p++ = crcValue & 0xff;
*p++ = (crcValue >> 8);
packet_end = p;
// Flag
for (int i = 0; i < std::min(m_settings.m_ax25PostFlags, AX25_MAX_FLAGS); i++)
*p++ = AX25_FLAG;
@ -582,8 +612,17 @@ void PacketModSource::addTXPacket(QString callsign, QString to, QString via, QSt
for (int j = 0; j < 8; j++)
{
int tx_bit = (packet[i] >> j) & 1;
// Stuff 0 if last 5 bits are 1s
if ((packet[i] != AX25_FLAG) && (m_last5Bits == 0x1f))
// Stuff 0 if last 5 bits are 1s, unless transmitting flag
// Except for special case of when last 5 bits of CRC are 1s
if ( ( (packet[i] != AX25_FLAG)
|| ( (&packet[i] >= crc_start)
&& ( (&packet[i] < packet_end)
|| ((&packet[i] == packet_end) && (j == 0))
)
)
)
&& (m_last5Bits == 0x1f)
)
addBit(0);
addBit(tx_bit);
}

View File

@ -5,10 +5,13 @@ if (Qt5SerialPort_FOUND)
endif()
if (Qt5Quick_FOUND AND Qt5QuickWidgets_FOUND AND Qt5Positioning_FOUND)
add_subdirectory(map)
add_subdirectory(vorlocalizer)
endif()
add_subdirectory(afc)
add_subdirectory(aprs)
add_subdirectory(demodanalyzer)
add_subdirectory(rigctlserver)
add_subdirectory(simpleptt)
add_subdirectory(startracker)

View File

@ -135,8 +135,7 @@ bool AFC::handleMessage(const Message& cmd)
qDebug() << "AFC::handleMessage: MessagePipesCommon::MsgReportChannelDeleted";
MessagePipesCommon::MsgReportChannelDeleted& report = (MessagePipesCommon::MsgReportChannelDeleted&) cmd;
const MessagePipesCommon::ChannelRegistrationKey& channelKey = report.getChannelRegistrationKey();
const ChannelAPI *channel = channelKey.m_key;
MainCore::instance()->getMessagePipes().unregisterChannelToFeature(channel, this, "settings");
MainCore::instance()->getMessagePipes().unregisterChannelToFeature(channelKey.m_key, this, "settings");
return true;
}

View File

@ -0,0 +1,67 @@
project(aprs)
set(aprs_SOURCES
aprs.cpp
aprssettings.cpp
aprsplugin.cpp
aprsworker.cpp
aprswebapiadapter.cpp
)
set(aprs_HEADERS
aprs.h
aprssettings.h
aprsplugin.h
aprsreport.h
aprsworker.h
aprswebapiadapter.h
)
include_directories(
${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client
)
if(NOT SERVER_MODE)
set(aprs_SOURCES
${aprs_SOURCES}
aprsgui.cpp
aprsgui.ui
aprssettingsdialog.cpp
aprssettingsdialog.ui
aprs.qrc
)
set(aprs_HEADERS
${aprs_HEADERS}
aprsgui.h
aprssettingsdialog.h
)
set(TARGET_NAME aprs)
set(TARGET_LIB "Qt5::Widgets" Qt5::Charts)
set(TARGET_LIB_GUI "sdrgui")
set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR})
else()
set(TARGET_NAME aprssrv)
set(TARGET_LIB "")
set(TARGET_LIB_GUI "")
set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR})
endif()
add_library(${TARGET_NAME} SHARED
${aprs_SOURCES}
)
target_link_libraries(${TARGET_NAME}
Qt5::Core
${TARGET_LIB}
sdrbase
${TARGET_LIB_GUI}
)
install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER})
if(WIN32)
# Run deployqt for Charts etc
include(DeployQt)
windeployqt(${TARGET_NAME} ${SDRANGEL_BINARY_BIN_DIR} ${PROJECT_SOURCE_DIR}/aprs)
endif()

View File

@ -0,0 +1,415 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2021 Jon Beniston, M7RCE //
// Copyright (C) 2020 Edouard Griffiths, F4EXB //
// //
// 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 <QNetworkAccessManager>
#include <QNetworkReply>
#include <QBuffer>
#include "SWGFeatureSettings.h"
#include "SWGFeatureReport.h"
#include "SWGFeatureActions.h"
#include "SWGDeviceState.h"
#include "dsp/dspengine.h"
#include "device/deviceset.h"
#include "channel/channelapi.h"
#include "maincore.h"
#include "aprsworker.h"
#include "aprs.h"
MESSAGE_CLASS_DEFINITION(APRS::MsgConfigureAPRS, Message)
MESSAGE_CLASS_DEFINITION(APRS::MsgReportWorker, Message)
const char* const APRS::m_featureIdURI = "sdrangel.feature.aprs";
const char* const APRS::m_featureId = "APRS";
APRS::APRS(WebAPIAdapterInterface *webAPIAdapterInterface) :
Feature(m_featureIdURI, webAPIAdapterInterface)
{
qDebug("APRS::APRS: webAPIAdapterInterface: %p", webAPIAdapterInterface);
setObjectName(m_featureId);
m_worker = new APRSWorker(this, webAPIAdapterInterface);
m_state = StIdle;
m_errorMessage = "APRS error";
connect(&m_updatePipesTimer, SIGNAL(timeout()), this, SLOT(updatePipes()));
m_updatePipesTimer.start(1000);
}
APRS::~APRS()
{
if (m_worker->isRunning()) {
stop();
}
delete m_worker;
}
void APRS::start()
{
qDebug("APRS::start");
m_worker->reset();
m_worker->setMessageQueueToFeature(getInputMessageQueue());
m_worker->setMessageQueueToGUI(getMessageQueueToGUI());
bool ok = m_worker->startWork();
m_state = ok ? StIdle : StError;
m_thread.start();
APRSWorker::MsgConfigureAPRSWorker *msg = APRSWorker::MsgConfigureAPRSWorker::create(m_settings, true);
m_worker->getInputMessageQueue()->push(msg);
}
void APRS::stop()
{
qDebug("APRS::stop");
m_worker->stopWork();
m_state = StIdle;
m_thread.quit();
m_thread.wait();
}
bool APRS::handleMessage(const Message& cmd)
{
if (MsgConfigureAPRS::match(cmd))
{
MsgConfigureAPRS& cfg = (MsgConfigureAPRS&) cmd;
qDebug() << "APRS::handleMessage: MsgConfigureAPRS";
applySettings(cfg.getSettings(), cfg.getForce());
return true;
}
else if (MsgReportWorker::match(cmd))
{
MsgReportWorker& report = (MsgReportWorker&) cmd;
if (report.getMessage() == "Connected")
m_state = StRunning;
else if (report.getMessage() == "Disconnected")
m_state = StIdle;
else
{
m_state = StError;
m_errorMessage = report.getMessage();
}
return true;
}
else if (MainCore::MsgPacket::match(cmd))
{
MainCore::MsgPacket& report = (MainCore::MsgPacket&) cmd;
if (getMessageQueueToGUI())
{
MainCore::MsgPacket *copy = new MainCore::MsgPacket(report);
getMessageQueueToGUI()->push(copy);
}
if (m_state == StRunning)
{
MainCore::MsgPacket *copy = new MainCore::MsgPacket(report);
m_worker->getInputMessageQueue()->push(copy);
}
return true;
}
else
{
return false;
}
}
void APRS::updatePipes()
{
QList<AvailablePipeSource> availablePipes = updateAvailablePipeSources("packets", APRSSettings::m_pipeTypes, APRSSettings::m_pipeURIs, this);
if (availablePipes != m_availablePipes)
{
m_availablePipes = availablePipes;
if (getMessageQueueToGUI())
{
MsgReportPipes *msgToGUI = MsgReportPipes::create();
QList<AvailablePipeSource>& msgAvailablePipes = msgToGUI->getAvailablePipes();
msgAvailablePipes.append(availablePipes);
getMessageQueueToGUI()->push(msgToGUI);
}
}
}
QByteArray APRS::serialize() const
{
return m_settings.serialize();
}
bool APRS::deserialize(const QByteArray& data)
{
if (m_settings.deserialize(data))
{
MsgConfigureAPRS *msg = MsgConfigureAPRS::create(m_settings, true);
m_inputMessageQueue.push(msg);
return true;
}
else
{
m_settings.resetToDefaults();
MsgConfigureAPRS *msg = MsgConfigureAPRS::create(m_settings, true);
m_inputMessageQueue.push(msg);
return false;
}
}
void APRS::applySettings(const APRSSettings& settings, bool force)
{
qDebug() << "APRS::applySettings:"
<< " m_igateEnabled: " << settings.m_igateEnabled
<< " m_title: " << settings.m_title
<< " m_rgbColor: " << settings.m_rgbColor
<< " m_useReverseAPI: " << settings.m_useReverseAPI
<< " m_reverseAPIAddress: " << settings.m_reverseAPIAddress
<< " m_reverseAPIPort: " << settings.m_reverseAPIPort
<< " m_reverseAPIFeatureSetIndex: " << settings.m_reverseAPIFeatureSetIndex
<< " m_reverseAPIFeatureIndex: " << settings.m_reverseAPIFeatureIndex
<< " force: " << force;
QList<QString> reverseAPIKeys;
if ((m_settings.m_igateEnabled != settings.m_igateEnabled) || force)
{
if (settings.m_igateEnabled)
start();
else
stop();
reverseAPIKeys.append("igateEnabled");
}
if ((m_settings.m_title != settings.m_title) || force) {
reverseAPIKeys.append("title");
}
if ((m_settings.m_rgbColor != settings.m_rgbColor) || force) {
reverseAPIKeys.append("rgbColor");
}
APRSWorker::MsgConfigureAPRSWorker *msg = APRSWorker::MsgConfigureAPRSWorker::create(
settings, force
);
m_worker->getInputMessageQueue()->push(msg);
if (settings.m_useReverseAPI)
{
bool fullUpdate = ((m_settings.m_useReverseAPI != settings.m_useReverseAPI) && settings.m_useReverseAPI) ||
(m_settings.m_reverseAPIAddress != settings.m_reverseAPIAddress) ||
(m_settings.m_reverseAPIPort != settings.m_reverseAPIPort) ||
(m_settings.m_reverseAPIFeatureSetIndex != settings.m_reverseAPIFeatureSetIndex) ||
(m_settings.m_reverseAPIFeatureIndex != settings.m_reverseAPIFeatureIndex);
webapiReverseSendSettings(reverseAPIKeys, settings, fullUpdate || force);
}
m_settings = settings;
}
int APRS::webapiRun(bool run,
SWGSDRangel::SWGDeviceState& response,
QString& errorMessage)
{
(void) run;
(void) response;
(void) errorMessage;
//getFeatureStateStr(*response.getState());
//MsgStartStopIGate *msg = MsgStartStopIGate::create(run);
//getInputMessageQueue()->push(msg);
return 202;
}
int APRS::webapiSettingsGet(
SWGSDRangel::SWGFeatureSettings& response,
QString& errorMessage)
{
(void) errorMessage;
response.setAprsSettings(new SWGSDRangel::SWGAPRSSettings());
response.getAprsSettings()->init();
webapiFormatFeatureSettings(response, m_settings);
return 200;
}
int APRS::webapiSettingsPutPatch(
bool force,
const QStringList& featureSettingsKeys,
SWGSDRangel::SWGFeatureSettings& response,
QString& errorMessage)
{
(void) errorMessage;
APRSSettings settings = m_settings;
webapiUpdateFeatureSettings(settings, featureSettingsKeys, response);
MsgConfigureAPRS *msg = MsgConfigureAPRS::create(settings, force);
m_inputMessageQueue.push(msg);
qDebug("APRS::webapiSettingsPutPatch: forward to GUI: %p", m_guiMessageQueue);
if (m_guiMessageQueue) // forward to GUI if any
{
MsgConfigureAPRS *msgToGUI = MsgConfigureAPRS::create(settings, force);
m_guiMessageQueue->push(msgToGUI);
}
webapiFormatFeatureSettings(response, settings);
return 200;
}
void APRS::webapiFormatFeatureSettings(
SWGSDRangel::SWGFeatureSettings& response,
const APRSSettings& settings)
{
response.getAprsSettings()->setIgateServer(new QString(settings.m_igateServer));
response.getAprsSettings()->setIgatePort(settings.m_igatePort);
response.getAprsSettings()->setIgateCallsign(new QString(settings.m_igateCallsign));
response.getAprsSettings()->setIgatePasscode(new QString(settings.m_igatePasscode));
response.getAprsSettings()->setIgateFilter(new QString(settings.m_igateFilter));
response.getAprsSettings()->setIgateEnabled(settings.m_igateEnabled ? 1 : 0);
if (response.getAprsSettings()->getTitle()) {
*response.getAprsSettings()->getTitle() = settings.m_title;
} else {
response.getAprsSettings()->setTitle(new QString(settings.m_title));
}
response.getAprsSettings()->setRgbColor(settings.m_rgbColor);
response.getAprsSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0);
if (response.getAprsSettings()->getReverseApiAddress()) {
*response.getAprsSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress;
} else {
response.getAprsSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress));
}
response.getAprsSettings()->setReverseApiPort(settings.m_reverseAPIPort);
response.getAprsSettings()->setReverseApiDeviceIndex(settings.m_reverseAPIFeatureSetIndex);
response.getAprsSettings()->setReverseApiChannelIndex(settings.m_reverseAPIFeatureIndex);
}
void APRS::webapiUpdateFeatureSettings(
APRSSettings& settings,
const QStringList& featureSettingsKeys,
SWGSDRangel::SWGFeatureSettings& response)
{
if (featureSettingsKeys.contains("igateServer")) {
settings.m_igateServer = *response.getAprsSettings()->getIgateServer();
}
if (featureSettingsKeys.contains("igatePort")) {
settings.m_igatePort = response.getAprsSettings()->getIgatePort();
}
if (featureSettingsKeys.contains("igateCallsign")) {
settings.m_igateCallsign = *response.getAprsSettings()->getIgateCallsign();
}
if (featureSettingsKeys.contains("igatePasscode")) {
settings.m_igatePasscode = *response.getAprsSettings()->getIgatePasscode();
}
if (featureSettingsKeys.contains("igateFilter")) {
settings.m_igateFilter = *response.getAprsSettings()->getIgateFilter();
}
if (featureSettingsKeys.contains("title")) {
settings.m_title = *response.getAprsSettings()->getTitle();
}
if (featureSettingsKeys.contains("rgbColor")) {
settings.m_rgbColor = response.getAprsSettings()->getRgbColor();
}
if (featureSettingsKeys.contains("useReverseAPI")) {
settings.m_useReverseAPI = response.getAprsSettings()->getUseReverseApi() != 0;
}
if (featureSettingsKeys.contains("reverseAPIAddress")) {
settings.m_reverseAPIAddress = *response.getAprsSettings()->getReverseApiAddress();
}
if (featureSettingsKeys.contains("reverseAPIPort")) {
settings.m_reverseAPIPort = response.getAprsSettings()->getReverseApiPort();
}
if (featureSettingsKeys.contains("reverseAPIDeviceIndex")) {
settings.m_reverseAPIFeatureSetIndex = response.getAprsSettings()->getReverseApiDeviceIndex();
}
if (featureSettingsKeys.contains("reverseAPIChannelIndex")) {
settings.m_reverseAPIFeatureIndex = response.getAprsSettings()->getReverseApiChannelIndex();
}
}
void APRS::webapiReverseSendSettings(QList<QString>& featureSettingsKeys, const APRSSettings& settings, bool force)
{
SWGSDRangel::SWGFeatureSettings *swgFeatureSettings = new SWGSDRangel::SWGFeatureSettings();
// swgFeatureSettings->setOriginatorFeatureIndex(getIndexInDeviceSet());
// swgFeatureSettings->setOriginatorFeatureSetIndex(getDeviceSetIndex());
swgFeatureSettings->setFeatureType(new QString("APRS"));
swgFeatureSettings->setAprsSettings(new SWGSDRangel::SWGAPRSSettings());
SWGSDRangel::SWGAPRSSettings *swgAPRSSettings = swgFeatureSettings->getAprsSettings();
// transfer data that has been modified. When force is on transfer all data except reverse API data
if (featureSettingsKeys.contains("igateServer") || force) {
swgAPRSSettings->setIgateServer(new QString(settings.m_igateServer));
}
if (featureSettingsKeys.contains("igatePort") || force) {
swgAPRSSettings->setIgatePort(settings.m_igatePort);
}
if (featureSettingsKeys.contains("igateCallsign") || force) {
swgAPRSSettings->setIgateCallsign(new QString(settings.m_igateCallsign));
}
if (featureSettingsKeys.contains("igatePasscode") || force) {
swgAPRSSettings->setIgatePasscode(new QString(settings.m_igatePasscode));
}
if (featureSettingsKeys.contains("igateFilter") || force) {
swgAPRSSettings->setIgateFilter(new QString(settings.m_igateFilter));
}
if (featureSettingsKeys.contains("title") || force) {
swgAPRSSettings->setTitle(new QString(settings.m_title));
}
if (featureSettingsKeys.contains("rgbColor") || force) {
swgAPRSSettings->setRgbColor(settings.m_rgbColor);
}
QString channelSettingsURL = QString("http://%1:%2/sdrangel/featureset/%3/feature/%4/settings")
.arg(settings.m_reverseAPIAddress)
.arg(settings.m_reverseAPIPort)
.arg(settings.m_reverseAPIFeatureSetIndex)
.arg(settings.m_reverseAPIFeatureIndex);
m_networkRequest.setUrl(QUrl(channelSettingsURL));
m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QBuffer *buffer = new QBuffer();
buffer->open((QBuffer::ReadWrite));
buffer->write(swgFeatureSettings->asJson().toUtf8());
buffer->seek(0);
// Always use PATCH to avoid passing reverse API settings
QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer);
buffer->setParent(reply);
delete swgFeatureSettings;
}
void APRS::networkManagerFinished(QNetworkReply *reply)
{
QNetworkReply::NetworkError replyError = reply->error();
if (replyError)
{
qWarning() << "APRS::networkManagerFinished:"
<< " error(" << (int) replyError
<< "): " << replyError
<< ": " << reply->errorString();
}
else
{
QString answer = reply->readAll();
answer.chop(1); // remove last \n
qDebug("APRS::networkManagerFinished: reply:\n%s", answer.toStdString().c_str());
}
reply->deleteLater();
}

143
plugins/feature/aprs/aprs.h Normal file
View File

@ -0,0 +1,143 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2021 Jon Beniston, M7RCE //
// Copyright (C) 2020 Edouard Griffiths, F4EXB //
// //
// 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_FEATURE_APRS_H_
#define INCLUDE_FEATURE_APRS_H_
#include <QThread>
#include <QHash>
#include <QNetworkRequest>
#include <QTimer>
#include "feature/feature.h"
#include "util/message.h"
#include "aprssettings.h"
class WebAPIAdapterInterface;
class APRSWorker;
class QNetworkAccessManager;
class QNetworkReply;
namespace SWGSDRangel {
class SWGDeviceState;
}
class APRS : public Feature
{
Q_OBJECT
public:
class MsgConfigureAPRS : public Message {
MESSAGE_CLASS_DECLARATION
public:
const APRSSettings& getSettings() const { return m_settings; }
bool getForce() const { return m_force; }
static MsgConfigureAPRS* create(const APRSSettings& settings, bool force) {
return new MsgConfigureAPRS(settings, force);
}
private:
APRSSettings m_settings;
bool m_force;
MsgConfigureAPRS(const APRSSettings& settings, bool force) :
Message(),
m_settings(settings),
m_force(force)
{ }
};
class MsgReportWorker : public Message {
MESSAGE_CLASS_DECLARATION
public:
QString getMessage() { return m_message; }
static MsgReportWorker* create(QString message) {
return new MsgReportWorker(message);
}
private:
QString m_message;
MsgReportWorker(QString message) :
Message(),
m_message(message)
{}
};
APRS(WebAPIAdapterInterface *webAPIAdapterInterface);
virtual ~APRS();
virtual void destroy() { delete this; }
virtual bool handleMessage(const Message& cmd);
virtual void getIdentifier(QString& id) const { id = objectName(); }
virtual void getTitle(QString& title) const { title = m_settings.m_title; }
virtual QByteArray serialize() const;
virtual bool deserialize(const QByteArray& data);
virtual int webapiRun(bool run,
SWGSDRangel::SWGDeviceState& response,
QString& errorMessage);
virtual int webapiSettingsGet(
SWGSDRangel::SWGFeatureSettings& response,
QString& errorMessage);
virtual int webapiSettingsPutPatch(
bool force,
const QStringList& featureSettingsKeys,
SWGSDRangel::SWGFeatureSettings& response,
QString& errorMessage);
static void webapiFormatFeatureSettings(
SWGSDRangel::SWGFeatureSettings& response,
const APRSSettings& settings);
static void webapiUpdateFeatureSettings(
APRSSettings& settings,
const QStringList& featureSettingsKeys,
SWGSDRangel::SWGFeatureSettings& response);
static const char* const m_featureIdURI;
static const char* const m_featureId;
private:
QThread m_thread;
APRSWorker *m_worker;
APRSSettings m_settings;
QList<PipeEndPoint::AvailablePipeSource> m_availablePipes;
QTimer m_updatePipesTimer;
QNetworkAccessManager *m_networkManager;
QNetworkRequest m_networkRequest;
void start();
void stop();
void applySettings(const APRSSettings& settings, bool force = false);
void webapiReverseSendSettings(QList<QString>& featureSettingsKeys, const APRSSettings& settings, bool force);
private slots:
void updatePipes();
void networkManagerFinished(QNetworkReply *reply);
};
#endif // INCLUDE_FEATURE_APRS_H_

View File

@ -0,0 +1,196 @@
<RCC>
<qresource prefix="/aprs/">
<file>aprs/aprs-symbols-24-0-00.png</file>
<file>aprs/aprs-symbols-24-0-01.png</file>
<file>aprs/aprs-symbols-24-0-02.png</file>
<file>aprs/aprs-symbols-24-0-03.png</file>
<file>aprs/aprs-symbols-24-0-04.png</file>
<file>aprs/aprs-symbols-24-0-05.png</file>
<file>aprs/aprs-symbols-24-0-06.png</file>
<file>aprs/aprs-symbols-24-0-07.png</file>
<file>aprs/aprs-symbols-24-0-08.png</file>
<file>aprs/aprs-symbols-24-0-09.png</file>
<file>aprs/aprs-symbols-24-0-10.png</file>
<file>aprs/aprs-symbols-24-0-11.png</file>
<file>aprs/aprs-symbols-24-0-12.png</file>
<file>aprs/aprs-symbols-24-0-13.png</file>
<file>aprs/aprs-symbols-24-0-14.png</file>
<file>aprs/aprs-symbols-24-0-15.png</file>
<file>aprs/aprs-symbols-24-0-16.png</file>
<file>aprs/aprs-symbols-24-0-17.png</file>
<file>aprs/aprs-symbols-24-0-18.png</file>
<file>aprs/aprs-symbols-24-0-19.png</file>
<file>aprs/aprs-symbols-24-0-20.png</file>
<file>aprs/aprs-symbols-24-0-21.png</file>
<file>aprs/aprs-symbols-24-0-22.png</file>
<file>aprs/aprs-symbols-24-0-23.png</file>
<file>aprs/aprs-symbols-24-0-24.png</file>
<file>aprs/aprs-symbols-24-0-25.png</file>
<file>aprs/aprs-symbols-24-0-26.png</file>
<file>aprs/aprs-symbols-24-0-27.png</file>
<file>aprs/aprs-symbols-24-0-28.png</file>
<file>aprs/aprs-symbols-24-0-29.png</file>
<file>aprs/aprs-symbols-24-0-30.png</file>
<file>aprs/aprs-symbols-24-0-31.png</file>
<file>aprs/aprs-symbols-24-0-32.png</file>
<file>aprs/aprs-symbols-24-0-33.png</file>
<file>aprs/aprs-symbols-24-0-34.png</file>
<file>aprs/aprs-symbols-24-0-35.png</file>
<file>aprs/aprs-symbols-24-0-36.png</file>
<file>aprs/aprs-symbols-24-0-37.png</file>
<file>aprs/aprs-symbols-24-0-38.png</file>
<file>aprs/aprs-symbols-24-0-39.png</file>
<file>aprs/aprs-symbols-24-0-40.png</file>
<file>aprs/aprs-symbols-24-0-41.png</file>
<file>aprs/aprs-symbols-24-0-42.png</file>
<file>aprs/aprs-symbols-24-0-43.png</file>
<file>aprs/aprs-symbols-24-0-44.png</file>
<file>aprs/aprs-symbols-24-0-45.png</file>
<file>aprs/aprs-symbols-24-0-46.png</file>
<file>aprs/aprs-symbols-24-0-47.png</file>
<file>aprs/aprs-symbols-24-0-48.png</file>
<file>aprs/aprs-symbols-24-0-49.png</file>
<file>aprs/aprs-symbols-24-0-50.png</file>
<file>aprs/aprs-symbols-24-0-51.png</file>
<file>aprs/aprs-symbols-24-0-52.png</file>
<file>aprs/aprs-symbols-24-0-53.png</file>
<file>aprs/aprs-symbols-24-0-54.png</file>
<file>aprs/aprs-symbols-24-0-55.png</file>
<file>aprs/aprs-symbols-24-0-56.png</file>
<file>aprs/aprs-symbols-24-0-57.png</file>
<file>aprs/aprs-symbols-24-0-58.png</file>
<file>aprs/aprs-symbols-24-0-59.png</file>
<file>aprs/aprs-symbols-24-0-60.png</file>
<file>aprs/aprs-symbols-24-0-61.png</file>
<file>aprs/aprs-symbols-24-0-62.png</file>
<file>aprs/aprs-symbols-24-0-63.png</file>
<file>aprs/aprs-symbols-24-0-64.png</file>
<file>aprs/aprs-symbols-24-0-65.png</file>
<file>aprs/aprs-symbols-24-0-66.png</file>
<file>aprs/aprs-symbols-24-0-67.png</file>
<file>aprs/aprs-symbols-24-0-68.png</file>
<file>aprs/aprs-symbols-24-0-69.png</file>
<file>aprs/aprs-symbols-24-0-70.png</file>
<file>aprs/aprs-symbols-24-0-71.png</file>
<file>aprs/aprs-symbols-24-0-72.png</file>
<file>aprs/aprs-symbols-24-0-73.png</file>
<file>aprs/aprs-symbols-24-0-74.png</file>
<file>aprs/aprs-symbols-24-0-75.png</file>
<file>aprs/aprs-symbols-24-0-76.png</file>
<file>aprs/aprs-symbols-24-0-77.png</file>
<file>aprs/aprs-symbols-24-0-78.png</file>
<file>aprs/aprs-symbols-24-0-79.png</file>
<file>aprs/aprs-symbols-24-0-80.png</file>
<file>aprs/aprs-symbols-24-0-81.png</file>
<file>aprs/aprs-symbols-24-0-82.png</file>
<file>aprs/aprs-symbols-24-0-83.png</file>
<file>aprs/aprs-symbols-24-0-84.png</file>
<file>aprs/aprs-symbols-24-0-85.png</file>
<file>aprs/aprs-symbols-24-0-86.png</file>
<file>aprs/aprs-symbols-24-0-87.png</file>
<file>aprs/aprs-symbols-24-0-88.png</file>
<file>aprs/aprs-symbols-24-0-89.png</file>
<file>aprs/aprs-symbols-24-0-90.png</file>
<file>aprs/aprs-symbols-24-0-91.png</file>
<file>aprs/aprs-symbols-24-0-92.png</file>
<file>aprs/aprs-symbols-24-0-93.png</file>
<file>aprs/aprs-symbols-24-0-94.png</file>
<file>aprs/aprs-symbols-24-0-95.png</file>
<file>aprs/aprs-symbols-24-1-00.png</file>
<file>aprs/aprs-symbols-24-1-01.png</file>
<file>aprs/aprs-symbols-24-1-02.png</file>
<file>aprs/aprs-symbols-24-1-03.png</file>
<file>aprs/aprs-symbols-24-1-04.png</file>
<file>aprs/aprs-symbols-24-1-05.png</file>
<file>aprs/aprs-symbols-24-1-06.png</file>
<file>aprs/aprs-symbols-24-1-07.png</file>
<file>aprs/aprs-symbols-24-1-08.png</file>
<file>aprs/aprs-symbols-24-1-09.png</file>
<file>aprs/aprs-symbols-24-1-10.png</file>
<file>aprs/aprs-symbols-24-1-11.png</file>
<file>aprs/aprs-symbols-24-1-12.png</file>
<file>aprs/aprs-symbols-24-1-13.png</file>
<file>aprs/aprs-symbols-24-1-14.png</file>
<file>aprs/aprs-symbols-24-1-15.png</file>
<file>aprs/aprs-symbols-24-1-16.png</file>
<file>aprs/aprs-symbols-24-1-17.png</file>
<file>aprs/aprs-symbols-24-1-18.png</file>
<file>aprs/aprs-symbols-24-1-19.png</file>
<file>aprs/aprs-symbols-24-1-20.png</file>
<file>aprs/aprs-symbols-24-1-21.png</file>
<file>aprs/aprs-symbols-24-1-22.png</file>
<file>aprs/aprs-symbols-24-1-23.png</file>
<file>aprs/aprs-symbols-24-1-24.png</file>
<file>aprs/aprs-symbols-24-1-25.png</file>
<file>aprs/aprs-symbols-24-1-26.png</file>
<file>aprs/aprs-symbols-24-1-27.png</file>
<file>aprs/aprs-symbols-24-1-28.png</file>
<file>aprs/aprs-symbols-24-1-29.png</file>
<file>aprs/aprs-symbols-24-1-30.png</file>
<file>aprs/aprs-symbols-24-1-31.png</file>
<file>aprs/aprs-symbols-24-1-32.png</file>
<file>aprs/aprs-symbols-24-1-33.png</file>
<file>aprs/aprs-symbols-24-1-34.png</file>
<file>aprs/aprs-symbols-24-1-35.png</file>
<file>aprs/aprs-symbols-24-1-36.png</file>
<file>aprs/aprs-symbols-24-1-37.png</file>
<file>aprs/aprs-symbols-24-1-38.png</file>
<file>aprs/aprs-symbols-24-1-39.png</file>
<file>aprs/aprs-symbols-24-1-40.png</file>
<file>aprs/aprs-symbols-24-1-41.png</file>
<file>aprs/aprs-symbols-24-1-42.png</file>
<file>aprs/aprs-symbols-24-1-43.png</file>
<file>aprs/aprs-symbols-24-1-44.png</file>
<file>aprs/aprs-symbols-24-1-45.png</file>
<file>aprs/aprs-symbols-24-1-46.png</file>
<file>aprs/aprs-symbols-24-1-47.png</file>
<file>aprs/aprs-symbols-24-1-48.png</file>
<file>aprs/aprs-symbols-24-1-49.png</file>
<file>aprs/aprs-symbols-24-1-50.png</file>
<file>aprs/aprs-symbols-24-1-51.png</file>
<file>aprs/aprs-symbols-24-1-52.png</file>
<file>aprs/aprs-symbols-24-1-53.png</file>
<file>aprs/aprs-symbols-24-1-54.png</file>
<file>aprs/aprs-symbols-24-1-55.png</file>
<file>aprs/aprs-symbols-24-1-56.png</file>
<file>aprs/aprs-symbols-24-1-57.png</file>
<file>aprs/aprs-symbols-24-1-58.png</file>
<file>aprs/aprs-symbols-24-1-59.png</file>
<file>aprs/aprs-symbols-24-1-60.png</file>
<file>aprs/aprs-symbols-24-1-61.png</file>
<file>aprs/aprs-symbols-24-1-62.png</file>
<file>aprs/aprs-symbols-24-1-63.png</file>
<file>aprs/aprs-symbols-24-1-64.png</file>
<file>aprs/aprs-symbols-24-1-65.png</file>
<file>aprs/aprs-symbols-24-1-66.png</file>
<file>aprs/aprs-symbols-24-1-67.png</file>
<file>aprs/aprs-symbols-24-1-68.png</file>
<file>aprs/aprs-symbols-24-1-69.png</file>
<file>aprs/aprs-symbols-24-1-70.png</file>
<file>aprs/aprs-symbols-24-1-71.png</file>
<file>aprs/aprs-symbols-24-1-72.png</file>
<file>aprs/aprs-symbols-24-1-73.png</file>
<file>aprs/aprs-symbols-24-1-74.png</file>
<file>aprs/aprs-symbols-24-1-75.png</file>
<file>aprs/aprs-symbols-24-1-76.png</file>
<file>aprs/aprs-symbols-24-1-77.png</file>
<file>aprs/aprs-symbols-24-1-78.png</file>
<file>aprs/aprs-symbols-24-1-79.png</file>
<file>aprs/aprs-symbols-24-1-80.png</file>
<file>aprs/aprs-symbols-24-1-81.png</file>
<file>aprs/aprs-symbols-24-1-82.png</file>
<file>aprs/aprs-symbols-24-1-83.png</file>
<file>aprs/aprs-symbols-24-1-84.png</file>
<file>aprs/aprs-symbols-24-1-85.png</file>
<file>aprs/aprs-symbols-24-1-86.png</file>
<file>aprs/aprs-symbols-24-1-87.png</file>
<file>aprs/aprs-symbols-24-1-88.png</file>
<file>aprs/aprs-symbols-24-1-89.png</file>
<file>aprs/aprs-symbols-24-1-90.png</file>
<file>aprs/aprs-symbols-24-1-91.png</file>
<file>aprs/aprs-symbols-24-1-92.png</file>
<file>aprs/aprs-symbols-24-1-93.png</file>
<file>aprs/aprs-symbols-24-1-94.png</file>
<file>aprs/aprs-symbols-24-1-95.png</file>
</qresource>
</RCC>

View File

@ -0,0 +1,6 @@
APRS images are from: https://github.com/hessu/aprs-symbols/tree/master/png
To split in to individual files, using ImageMagick:
convert aprs-symbols-24-0.png -crop 24x24 aprs-symbols-24-0-%02d.png
convert aprs-symbols-24-1.png -crop 24x24 aprs-symbols-24-1-%02d.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 617 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 661 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 867 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 511 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 623 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 538 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 847 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 747 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 855 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 764 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 725 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 713 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 688 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 771 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 691 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 730 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 716 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 534 B

Some files were not shown because too many files have changed in this diff Show More