Add DSC demodulator

pull/1693/head
Jon Beniston 2023-05-15 16:40:43 +01:00
rodzic 2536746047
commit 04db22584a
45 zmienionych plików z 6987 dodań i 5 usunięć

Wyświetl plik

@ -84,6 +84,7 @@ option(ENABLE_CHANNELRX_DEMODFT8 "Enable channelrx demodft8 plugin" ON)
option(ENABLE_CHANNELRX_DEMODNAVTEX "Enable channelrx demodnavtex plugin" ON)
option(ENABLE_CHANNELRX_DEMODRTTY "Enable channelrx demodrtty plugin" ON)
option(ENABLE_CHANNELRX_DEMODILS "Enable channelrx demodils plugin" ON)
option(ENABLE_CHANNELRX_DEMODDSC "Enable channelrx demoddsc plugin" ON)
# Channel Tx enablers
option(ENABLE_CHANNELTX "Enable channeltx plugins" ON)

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 11 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 326 KiB

Wyświetl plik

@ -125,6 +125,10 @@ if (ENABLE_CHANNELRX_DEMODILS)
add_subdirectory(demodils)
endif()
if (ENABLE_CHANNELRX_DEMODDSC)
add_subdirectory(demoddsc)
endif()
if(NOT SERVER_MODE)
add_subdirectory(heatmap)

Wyświetl plik

@ -951,10 +951,7 @@ Aircraft *ADSBDemodGUI::getAircraft(int icao, bool &newAircraft)
QIcon *icon = nullptr;
if (aircraft->m_aircraftInfo->m_operatorICAO.size() > 0)
{
aircraft->m_airlineIconURL = AircraftInformation::getAirlineIconPath(aircraft->m_aircraftInfo->m_operatorICAO);
if (aircraft->m_airlineIconURL.startsWith(':')) {
aircraft->m_airlineIconURL = "qrc://" + aircraft->m_airlineIconURL.mid(1);
}
aircraft->m_airlineIconURL = AircraftInformation::getFlagIconURL(aircraft->m_aircraftInfo->m_operatorICAO);
icon = AircraftInformation::getAirlineIcon(aircraft->m_aircraftInfo->m_operatorICAO);
if (icon != nullptr)
{

Wyświetl plik

@ -191,8 +191,8 @@ void DABDemodGUI::addProgramName(const DABDemod::MsgDABProgramName& program)
frequencyItem->setData(Qt::UserRole, 0.0);
}
ensembleItem->setText(ui->ensemble->text());
ui->programs->setSortingEnabled(true);
filterRow(row);
ui->programs->setSortingEnabled(true);
}
// Tune to the selected program

Wyświetl plik

@ -0,0 +1,63 @@
project(demoddsc)
set(demoddsc_SOURCES
dscdemod.cpp
dscdemodsettings.cpp
dscdemodbaseband.cpp
dscdemodsink.cpp
dscdemodplugin.cpp
dscdemodwebapiadapter.cpp
)
set(demoddsc_HEADERS
dscdemod.h
dscdemodsettings.h
dscdemodbaseband.h
dscdemodsink.h
dscdemodplugin.h
dscdemodwebapiadapter.h
)
include_directories(
${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client
)
if(NOT SERVER_MODE)
set(demoddsc_SOURCES
${demoddsc_SOURCES}
dscdemodgui.cpp
dscdemodgui.ui
)
set(demoddsc_HEADERS
${demoddsc_HEADERS}
dscdemodgui.h
)
set(TARGET_NAME demoddsc)
set(TARGET_LIB "Qt::Widgets")
set(TARGET_LIB_GUI "sdrgui")
set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR})
else()
set(TARGET_NAME demoddscsrv)
set(TARGET_LIB "")
set(TARGET_LIB_GUI "")
set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR})
endif()
add_library(${TARGET_NAME} SHARED
${demoddsc_SOURCES}
)
target_link_libraries(${TARGET_NAME}
Qt::Core
${TARGET_LIB}
sdrbase
${TARGET_LIB_GUI}
)
install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER})
# Install debug symbols
if (WIN32)
install(FILES $<TARGET_PDB_FILE:${TARGET_NAME}> CONFIGURATIONS Debug RelWithDebInfo DESTINATION ${INSTALL_FOLDER} )
endif()

Wyświetl plik

@ -0,0 +1,763 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2015-2018 Edouard Griffiths, F4EXB. //
// Copyright (C) 2023 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 "dscdemod.h"
#include <QTime>
#include <QDebug>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QBuffer>
#include <QThread>
#include <QHostInfo>
#include <stdio.h>
#include <complex.h>
#include "SWGChannelSettings.h"
#include "SWGWorkspaceInfo.h"
#include "SWGDSCDemodSettings.h"
#include "SWGChannelReport.h"
#include "SWGMapItem.h"
#include "dsp/dspengine.h"
#include "dsp/dspcommands.h"
#include "device/deviceapi.h"
#include "feature/feature.h"
#include "settings/serializable.h"
#include "util/db.h"
#include "maincore.h"
MESSAGE_CLASS_DEFINITION(DSCDemod::MsgConfigureDSCDemod, Message)
MESSAGE_CLASS_DEFINITION(DSCDemod::MsgMessage, Message)
const char * const DSCDemod::m_channelIdURI = "sdrangel.channel.dscdemod";
const char * const DSCDemod::m_channelId = "DSCDemod";
DSCDemod::DSCDemod(DeviceAPI *deviceAPI) :
ChannelAPI(m_channelIdURI, ChannelAPI::StreamSingleSink),
m_deviceAPI(deviceAPI),
m_basebandSampleRate(0)
{
setObjectName(m_channelId);
m_basebandSink = new DSCDemodBaseband(this);
m_basebandSink->setMessageQueueToChannel(getInputMessageQueue());
m_basebandSink->setChannel(this);
m_basebandSink->moveToThread(&m_thread);
applySettings(m_settings, true);
m_deviceAPI->addChannelSink(this);
m_deviceAPI->addChannelSinkAPI(this);
m_networkManager = new QNetworkAccessManager();
QObject::connect(
m_networkManager,
&QNetworkAccessManager::finished,
this,
&DSCDemod::networkManagerFinished
);
QObject::connect(
this,
&ChannelAPI::indexInDeviceSetChanged,
this,
&DSCDemod::handleIndexInDeviceSetChanged
);
}
DSCDemod::~DSCDemod()
{
qDebug("DSCDemod::~DSCDemod");
QObject::disconnect(
m_networkManager,
&QNetworkAccessManager::finished,
this,
&DSCDemod::networkManagerFinished
);
delete m_networkManager;
m_deviceAPI->removeChannelSinkAPI(this);
m_deviceAPI->removeChannelSink(this);
if (m_basebandSink->isRunning()) {
stop();
}
delete m_basebandSink;
}
void DSCDemod::setDeviceAPI(DeviceAPI *deviceAPI)
{
if (deviceAPI != m_deviceAPI)
{
m_deviceAPI->removeChannelSinkAPI(this);
m_deviceAPI->removeChannelSink(this);
m_deviceAPI = deviceAPI;
m_deviceAPI->addChannelSink(this);
m_deviceAPI->addChannelSinkAPI(this);
}
}
uint32_t DSCDemod::getNumberOfDeviceStreams() const
{
return m_deviceAPI->getNbSourceStreams();
}
void DSCDemod::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, bool firstOfBurst)
{
(void) firstOfBurst;
m_basebandSink->feed(begin, end);
}
void DSCDemod::start()
{
qDebug("DSCDemod::start");
m_basebandSink->reset();
m_basebandSink->startWork();
m_thread.start();
DSPSignalNotification *dspMsg = new DSPSignalNotification(m_basebandSampleRate, m_centerFrequency);
m_basebandSink->getInputMessageQueue()->push(dspMsg);
DSCDemodBaseband::MsgConfigureDSCDemodBaseband *msg = DSCDemodBaseband::MsgConfigureDSCDemodBaseband::create(m_settings, true);
m_basebandSink->getInputMessageQueue()->push(msg);
}
void DSCDemod::stop()
{
qDebug("DSCDemod::stop");
m_basebandSink->stopWork();
m_thread.quit();
m_thread.wait();
}
bool DSCDemod::handleMessage(const Message& cmd)
{
if (MsgConfigureDSCDemod::match(cmd))
{
MsgConfigureDSCDemod& cfg = (MsgConfigureDSCDemod&) cmd;
qDebug() << "DSCDemod::handleMessage: MsgConfigureDSCDemod";
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() << "DSCDemod::handleMessage: DSPSignalNotification";
m_basebandSink->getInputMessageQueue()->push(rep);
// Forward to GUI if any
if (m_guiMessageQueue) {
m_guiMessageQueue->push(new DSPSignalNotification(notif));
}
return true;
}
else if (DSCDemod::MsgMessage::match(cmd))
{
// Forward to GUI
DSCDemod::MsgMessage& report = (DSCDemod::MsgMessage&)cmd;
if (getMessageQueueToGUI())
{
DSCDemod::MsgMessage *msg = new DSCDemod::MsgMessage(report);
getMessageQueueToGUI()->push(msg);
}
// Forward via UDP
if (m_settings.m_udpEnabled)
{
//qDebug() << "Forwarding to " << m_settings.m_udpAddress << ":" << m_settings.m_udpPort;
QByteArray bytes = report.getMessage().m_data;
m_udpSocket.writeDatagram(bytes, bytes.size(),
QHostAddress(m_settings.m_udpAddress), m_settings.m_udpPort);
}
// Forward valid messages to yaddnet.org
if (m_settings.m_feed)
{
const DSCMessage& message = report.getMessage();
if (message.m_valid)
{
QString yaddnet = message.toYaddNetFormat(MainCore::instance()->getSettings().getStationName(), m_centerFrequency + m_settings.m_inputFrequencyOffset);
qDebug() << "Forwarding to yaddnet.org " << yaddnet;
QByteArray bytes = yaddnet.toLocal8Bit();
QHostInfo info = QHostInfo::fromName("www.yaddnet.org");
if (info.addresses().size() > 0)
{
qint64 sent = m_udpSocket.writeDatagram(bytes.data(), bytes.size(), info.addresses()[0], 50666);
if (bytes.size() != sent) {
qDebug() << "Failed to send datagram to www.yaddnet.org. Sent " << sent << " of " << bytes.size() << " Error " << m_udpSocket.error();
}
}
else
{
qDebug() << "Can't get IP address for www.yaddnet.org";
}
}
}
// Write to log file
if (m_logFile.isOpen())
{
const DSCMessage &dscMsg = report.getMessage();
if (dscMsg.m_valid)
{
m_logStream
<< dscMsg.m_dateTime.date().toString() << ","
<< dscMsg.m_dateTime.time().toString() << ","
<< dscMsg.formatSpecifier() << ","
<< dscMsg.m_selfId << ","
<< dscMsg.m_address << ","
<< dscMsg.m_data.toHex() << ","
<< report.getErrors() << ","
<< report.getRSSI()
<< "\n";
}
}
return true;
}
else if (MainCore::MsgChannelDemodQuery::match(cmd))
{
qDebug() << "DSCDemod::handleMessage: MsgChannelDemodQuery";
sendSampleRateToDemodAnalyzer();
return true;
}
else
{
return false;
}
}
ScopeVis *DSCDemod::getScopeSink()
{
return m_basebandSink->getScopeSink();
}
void DSCDemod::setCenterFrequency(qint64 frequency)
{
DSCDemodSettings settings = m_settings;
settings.m_inputFrequencyOffset = frequency;
applySettings(settings, false);
if (m_guiMessageQueue) // forward to GUI if any
{
MsgConfigureDSCDemod *msgToGUI = MsgConfigureDSCDemod::create(settings, false);
m_guiMessageQueue->push(msgToGUI);
}
}
void DSCDemod::applySettings(const DSCDemodSettings& settings, bool force)
{
qDebug() << "DSCDemod::applySettings:"
<< " m_logEnabled: " << settings.m_logEnabled
<< " m_logFilename: " << settings.m_logFilename
<< " 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_filterInvalid != m_settings.m_filterInvalid) || force) {
reverseAPIKeys.append("filterInvalid");
}
if ((settings.m_filterColumn != m_settings.m_filterColumn) || force) {
reverseAPIKeys.append("filterColumn");
}
if ((settings.m_filter != m_settings.m_filter) || force) {
reverseAPIKeys.append("filter");
}
if ((settings.m_udpEnabled != m_settings.m_udpEnabled) || force) {
reverseAPIKeys.append("udpEnabled");
}
if ((settings.m_udpAddress != m_settings.m_udpAddress) || force) {
reverseAPIKeys.append("udpAddress");
}
if ((settings.m_udpPort != m_settings.m_udpPort) || force) {
reverseAPIKeys.append("udpPort");
}
if ((settings.m_logFilename != m_settings.m_logFilename) || force) {
reverseAPIKeys.append("logFilename");
}
if ((settings.m_logEnabled != m_settings.m_logEnabled) || force) {
reverseAPIKeys.append("logEnabled");
}
if (m_settings.m_streamIndex != settings.m_streamIndex)
{
if (m_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");
}
DSCDemodBaseband::MsgConfigureDSCDemodBaseband *msg = DSCDemodBaseband::MsgConfigureDSCDemodBaseband::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);
}
if ((settings.m_logEnabled != m_settings.m_logEnabled)
|| (settings.m_logFilename != m_settings.m_logFilename)
|| force)
{
if (m_logFile.isOpen())
{
m_logStream.flush();
m_logFile.close();
}
if (settings.m_logEnabled && !settings.m_logFilename.isEmpty())
{
m_logFile.setFileName(settings.m_logFilename);
if (m_logFile.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text))
{
qDebug() << "DSCDemod::applySettings - Logging to: " << settings.m_logFilename;
bool newFile = m_logFile.size() == 0;
m_logStream.setDevice(&m_logFile);
if (newFile)
{
// Write header
m_logStream << "Date,Time,Format,From,To,Message,Errors,RSSI\n";
}
}
else
{
qDebug() << "DSCDemod::applySettings - Unable to open log file: " << settings.m_logFilename;
}
}
}
m_settings = settings;
}
void DSCDemod::sendSampleRateToDemodAnalyzer()
{
QList<ObjectPipe*> pipes;
MainCore::instance()->getMessagePipes().getMessagePipes(this, "reportdemod", pipes);
if (pipes.size() > 0)
{
for (const auto& pipe : pipes)
{
MessageQueue *messageQueue = qobject_cast<MessageQueue*>(pipe->m_element);
MainCore::MsgChannelDemodReport *msg = MainCore::MsgChannelDemodReport::create(
this,
DSCDemodSettings::DSCDEMOD_CHANNEL_SAMPLE_RATE
);
messageQueue->push(msg);
}
}
}
QByteArray DSCDemod::serialize() const
{
return m_settings.serialize();
}
bool DSCDemod::deserialize(const QByteArray& data)
{
if (m_settings.deserialize(data))
{
MsgConfigureDSCDemod *msg = MsgConfigureDSCDemod::create(m_settings, true);
m_inputMessageQueue.push(msg);
return true;
}
else
{
m_settings.resetToDefaults();
MsgConfigureDSCDemod *msg = MsgConfigureDSCDemod::create(m_settings, true);
m_inputMessageQueue.push(msg);
return false;
}
}
int DSCDemod::webapiSettingsGet(
SWGSDRangel::SWGChannelSettings& response,
QString& errorMessage)
{
(void) errorMessage;
response.setDscDemodSettings(new SWGSDRangel::SWGDSCDemodSettings());
response.getDscDemodSettings()->init();
webapiFormatChannelSettings(response, m_settings);
return 200;
}
int DSCDemod::webapiWorkspaceGet(
SWGSDRangel::SWGWorkspaceInfo& response,
QString& errorMessage)
{
(void) errorMessage;
response.setIndex(m_settings.m_workspaceIndex);
return 200;
}
int DSCDemod::webapiSettingsPutPatch(
bool force,
const QStringList& channelSettingsKeys,
SWGSDRangel::SWGChannelSettings& response,
QString& errorMessage)
{
(void) errorMessage;
DSCDemodSettings settings = m_settings;
webapiUpdateChannelSettings(settings, channelSettingsKeys, response);
MsgConfigureDSCDemod *msg = MsgConfigureDSCDemod::create(settings, force);
m_inputMessageQueue.push(msg);
qDebug("DSCDemod::webapiSettingsPutPatch: forward to GUI: %p", m_guiMessageQueue);
if (m_guiMessageQueue) // forward to GUI if any
{
MsgConfigureDSCDemod *msgToGUI = MsgConfigureDSCDemod::create(settings, force);
m_guiMessageQueue->push(msgToGUI);
}
webapiFormatChannelSettings(response, settings);
return 200;
}
int DSCDemod::webapiReportGet(
SWGSDRangel::SWGChannelReport& response,
QString& errorMessage)
{
(void) errorMessage;
response.setDscDemodReport(new SWGSDRangel::SWGDSCDemodReport());
response.getDscDemodReport()->init();
webapiFormatChannelReport(response);
return 200;
}
void DSCDemod::webapiUpdateChannelSettings(
DSCDemodSettings& settings,
const QStringList& channelSettingsKeys,
SWGSDRangel::SWGChannelSettings& response)
{
if (channelSettingsKeys.contains("inputFrequencyOffset")) {
settings.m_inputFrequencyOffset = response.getDscDemodSettings()->getInputFrequencyOffset();
}
if (channelSettingsKeys.contains("rfBandwidth")) {
settings.m_rfBandwidth = response.getDscDemodSettings()->getRfBandwidth();
}
if (channelSettingsKeys.contains("filterInvalid")) {
settings.m_filterInvalid = response.getDscDemodSettings()->getFilterInvalid();
}
if (channelSettingsKeys.contains("filterColumn")) {
settings.m_filterColumn = response.getDscDemodSettings()->getFilterColumn();
}
if (channelSettingsKeys.contains("filter")) {
settings.m_filter = *response.getDscDemodSettings()->getFilter();
}
if (channelSettingsKeys.contains("udpEnabled")) {
settings.m_udpEnabled = response.getDscDemodSettings()->getUdpEnabled();
}
if (channelSettingsKeys.contains("udpAddress")) {
settings.m_udpAddress = *response.getDscDemodSettings()->getUdpAddress();
}
if (channelSettingsKeys.contains("udpPort")) {
settings.m_udpPort = response.getDscDemodSettings()->getUdpPort();
}
if (channelSettingsKeys.contains("logFilename")) {
settings.m_logFilename = *response.getAdsbDemodSettings()->getLogFilename();
}
if (channelSettingsKeys.contains("logEnabled")) {
settings.m_logEnabled = response.getAdsbDemodSettings()->getLogEnabled();
}
if (channelSettingsKeys.contains("rgbColor")) {
settings.m_rgbColor = response.getDscDemodSettings()->getRgbColor();
}
if (channelSettingsKeys.contains("title")) {
settings.m_title = *response.getDscDemodSettings()->getTitle();
}
if (channelSettingsKeys.contains("streamIndex")) {
settings.m_streamIndex = response.getDscDemodSettings()->getStreamIndex();
}
if (channelSettingsKeys.contains("useReverseAPI")) {
settings.m_useReverseAPI = response.getDscDemodSettings()->getUseReverseApi() != 0;
}
if (channelSettingsKeys.contains("reverseAPIAddress")) {
settings.m_reverseAPIAddress = *response.getDscDemodSettings()->getReverseApiAddress();
}
if (channelSettingsKeys.contains("reverseAPIPort")) {
settings.m_reverseAPIPort = response.getDscDemodSettings()->getReverseApiPort();
}
if (channelSettingsKeys.contains("reverseAPIDeviceIndex")) {
settings.m_reverseAPIDeviceIndex = response.getDscDemodSettings()->getReverseApiDeviceIndex();
}
if (channelSettingsKeys.contains("reverseAPIChannelIndex")) {
settings.m_reverseAPIChannelIndex = response.getDscDemodSettings()->getReverseApiChannelIndex();
}
if (settings.m_scopeGUI && channelSettingsKeys.contains("scopeConfig")) {
settings.m_scopeGUI->updateFrom(channelSettingsKeys, response.getDscDemodSettings()->getScopeConfig());
}
if (settings.m_channelMarker && channelSettingsKeys.contains("channelMarker")) {
settings.m_channelMarker->updateFrom(channelSettingsKeys, response.getDscDemodSettings()->getChannelMarker());
}
if (settings.m_rollupState && channelSettingsKeys.contains("rollupState")) {
settings.m_rollupState->updateFrom(channelSettingsKeys, response.getDscDemodSettings()->getRollupState());
}
}
void DSCDemod::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& response, const DSCDemodSettings& settings)
{
response.getDscDemodSettings()->setInputFrequencyOffset(settings.m_inputFrequencyOffset);
response.getDscDemodSettings()->setRfBandwidth(settings.m_rfBandwidth);
response.getDscDemodSettings()->setFilterInvalid(settings.m_filterInvalid);
response.getDscDemodSettings()->setFilterColumn(settings.m_filterColumn);
response.getDscDemodSettings()->setFilter(new QString(settings.m_filter));
response.getDscDemodSettings()->setUdpEnabled(settings.m_udpEnabled);
response.getDscDemodSettings()->setUdpAddress(new QString(settings.m_udpAddress));
response.getDscDemodSettings()->setUdpPort(settings.m_udpPort);
response.getDscDemodSettings()->setLogFilename(new QString(settings.m_logFilename));
response.getDscDemodSettings()->setLogEnabled(settings.m_logEnabled);
response.getDscDemodSettings()->setRgbColor(settings.m_rgbColor);
if (response.getDscDemodSettings()->getTitle()) {
*response.getDscDemodSettings()->getTitle() = settings.m_title;
} else {
response.getDscDemodSettings()->setTitle(new QString(settings.m_title));
}
response.getDscDemodSettings()->setStreamIndex(settings.m_streamIndex);
response.getDscDemodSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0);
if (response.getDscDemodSettings()->getReverseApiAddress()) {
*response.getDscDemodSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress;
} else {
response.getDscDemodSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress));
}
response.getDscDemodSettings()->setReverseApiPort(settings.m_reverseAPIPort);
response.getDscDemodSettings()->setReverseApiDeviceIndex(settings.m_reverseAPIDeviceIndex);
response.getDscDemodSettings()->setReverseApiChannelIndex(settings.m_reverseAPIChannelIndex);
if (settings.m_scopeGUI)
{
if (response.getDscDemodSettings()->getScopeConfig())
{
settings.m_scopeGUI->formatTo(response.getDscDemodSettings()->getScopeConfig());
}
else
{
SWGSDRangel::SWGGLScope *swgGLScope = new SWGSDRangel::SWGGLScope();
settings.m_scopeGUI->formatTo(swgGLScope);
response.getDscDemodSettings()->setScopeConfig(swgGLScope);
}
}
if (settings.m_channelMarker)
{
if (response.getDscDemodSettings()->getChannelMarker())
{
settings.m_channelMarker->formatTo(response.getDscDemodSettings()->getChannelMarker());
}
else
{
SWGSDRangel::SWGChannelMarker *swgChannelMarker = new SWGSDRangel::SWGChannelMarker();
settings.m_channelMarker->formatTo(swgChannelMarker);
response.getDscDemodSettings()->setChannelMarker(swgChannelMarker);
}
}
if (settings.m_rollupState)
{
if (response.getDscDemodSettings()->getRollupState())
{
settings.m_rollupState->formatTo(response.getDscDemodSettings()->getRollupState());
}
else
{
SWGSDRangel::SWGRollupState *swgRollupState = new SWGSDRangel::SWGRollupState();
settings.m_rollupState->formatTo(swgRollupState);
response.getDscDemodSettings()->setRollupState(swgRollupState);
}
}
}
void DSCDemod::webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response)
{
double magsqAvg, magsqPeak;
int nbMagsqSamples;
getMagSqLevels(magsqAvg, magsqPeak, nbMagsqSamples);
response.getDscDemodReport()->setChannelPowerDb(CalcDb::dbPower(magsqAvg));
response.getDscDemodReport()->setChannelSampleRate(m_basebandSink->getChannelSampleRate());
}
void DSCDemod::webapiReverseSendSettings(QList<QString>& channelSettingsKeys, const DSCDemodSettings& 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 DSCDemod::webapiFormatChannelSettings(
QList<QString>& channelSettingsKeys,
SWGSDRangel::SWGChannelSettings *swgChannelSettings,
const DSCDemodSettings& settings,
bool force
)
{
swgChannelSettings->setDirection(0); // Single sink (Rx)
swgChannelSettings->setOriginatorChannelIndex(getIndexInDeviceSet());
swgChannelSettings->setOriginatorDeviceSetIndex(getDeviceSetIndex());
swgChannelSettings->setChannelType(new QString("DSCDemod"));
swgChannelSettings->setDscDemodSettings(new SWGSDRangel::SWGDSCDemodSettings());
SWGSDRangel::SWGDSCDemodSettings *swgDSCDemodSettings = swgChannelSettings->getDscDemodSettings();
// transfer data that has been modified. When force is on transfer all data except reverse API data
if (channelSettingsKeys.contains("inputFrequencyOffset") || force) {
swgDSCDemodSettings->setInputFrequencyOffset(settings.m_inputFrequencyOffset);
}
if (channelSettingsKeys.contains("rfBandwidth") || force) {
swgDSCDemodSettings->setRfBandwidth(settings.m_rfBandwidth);
}
if (channelSettingsKeys.contains("filterInvalid") || force) {
swgDSCDemodSettings->setFilterInvalid(settings.m_filterInvalid);
}
if (channelSettingsKeys.contains("filterColumn") || force) {
swgDSCDemodSettings->setFilterColumn(settings.m_filterColumn);
}
if (channelSettingsKeys.contains("filter") || force) {
swgDSCDemodSettings->setFilter(new QString(settings.m_filter));
}
if (channelSettingsKeys.contains("udpEnabled") || force) {
swgDSCDemodSettings->setUdpEnabled(settings.m_udpEnabled);
}
if (channelSettingsKeys.contains("udpAddress") || force) {
swgDSCDemodSettings->setUdpAddress(new QString(settings.m_udpAddress));
}
if (channelSettingsKeys.contains("udpPort") || force) {
swgDSCDemodSettings->setUdpPort(settings.m_udpPort);
}
if (channelSettingsKeys.contains("logFilename") || force) {
swgDSCDemodSettings->setLogFilename(new QString(settings.m_logFilename));
}
if (channelSettingsKeys.contains("logEnabled") || force) {
swgDSCDemodSettings->setLogEnabled(settings.m_logEnabled);
}
if (channelSettingsKeys.contains("rgbColor") || force) {
swgDSCDemodSettings->setRgbColor(settings.m_rgbColor);
}
if (channelSettingsKeys.contains("title") || force) {
swgDSCDemodSettings->setTitle(new QString(settings.m_title));
}
if (channelSettingsKeys.contains("streamIndex") || force) {
swgDSCDemodSettings->setStreamIndex(settings.m_streamIndex);
}
if (settings.m_scopeGUI && (channelSettingsKeys.contains("scopeConfig") || force))
{
SWGSDRangel::SWGGLScope *swgGLScope = new SWGSDRangel::SWGGLScope();
settings.m_scopeGUI->formatTo(swgGLScope);
swgDSCDemodSettings->setScopeConfig(swgGLScope);
}
if (settings.m_channelMarker && (channelSettingsKeys.contains("channelMarker") || force))
{
SWGSDRangel::SWGChannelMarker *swgChannelMarker = new SWGSDRangel::SWGChannelMarker();
settings.m_channelMarker->formatTo(swgChannelMarker);
swgDSCDemodSettings->setChannelMarker(swgChannelMarker);
}
if (settings.m_rollupState && (channelSettingsKeys.contains("rollupState") || force))
{
SWGSDRangel::SWGRollupState *swgRollupState = new SWGSDRangel::SWGRollupState();
settings.m_rollupState->formatTo(swgRollupState);
swgDSCDemodSettings->setRollupState(swgRollupState);
}
}
void DSCDemod::networkManagerFinished(QNetworkReply *reply)
{
QNetworkReply::NetworkError replyError = reply->error();
if (replyError)
{
qWarning() << "DSCDemod::networkManagerFinished:"
<< " error(" << (int) replyError
<< "): " << replyError
<< ": " << reply->errorString();
}
else
{
QString answer = reply->readAll();
answer.chop(1); // remove last \n
qDebug("DSCDemod::networkManagerFinished: reply:\n%s", answer.toStdString().c_str());
}
reply->deleteLater();
}
void DSCDemod::handleIndexInDeviceSetChanged(int index)
{
if (index < 0) {
return;
}
QString fifoLabel = QString("%1 [%2:%3]")
.arg(m_channelId)
.arg(m_deviceAPI->getDeviceSetIndex())
.arg(index);
m_basebandSink->setFifoLabel(fifoLabel);
}

Wyświetl plik

@ -0,0 +1,205 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2015-2018 Edouard Griffiths, F4EXB. //
// Copyright (C) 2023 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_DSCDEMOD_H
#define INCLUDE_DSCDEMOD_H
#include <vector>
#include <QNetworkRequest>
#include <QUdpSocket>
#include <QThread>
#include <QFile>
#include <QTextStream>
#include <QRegularExpression>
#include <QDateTime>
#include "dsp/basebandsamplesink.h"
#include "channel/channelapi.h"
#include "util/message.h"
#include "util/navtex.h"
#include "dscdemodbaseband.h"
#include "dscdemodsettings.h"
class QNetworkAccessManager;
class QNetworkReply;
class QThread;
class DeviceAPI;
class ScopeVis;
class DSCDemod : public BasebandSampleSink, public ChannelAPI {
public:
class MsgConfigureDSCDemod : public Message {
MESSAGE_CLASS_DECLARATION
public:
const DSCDemodSettings& getSettings() const { return m_settings; }
bool getForce() const { return m_force; }
static MsgConfigureDSCDemod* create(const DSCDemodSettings& settings, bool force)
{
return new MsgConfigureDSCDemod(settings, force);
}
private:
DSCDemodSettings m_settings;
bool m_force;
MsgConfigureDSCDemod(const DSCDemodSettings& settings, bool force) :
Message(),
m_settings(settings),
m_force(force)
{ }
};
class MsgMessage : public Message {
MESSAGE_CLASS_DECLARATION
public:
const DSCMessage& getMessage() const { return m_message; }
int getErrors() const { return m_errors; }
float getRSSI() const { return m_rssi; }
static MsgMessage* create(const DSCMessage& message, int errors, float rssi)
{
return new MsgMessage(message, errors, rssi);
}
private:
DSCMessage m_message;
int m_errors;
float m_rssi;
MsgMessage(const DSCMessage& message, int errors, float rssi) :
m_message(message),
m_errors(errors),
m_rssi(rssi)
{}
};
DSCDemod(DeviceAPI *deviceAPI);
virtual ~DSCDemod();
virtual void destroy() { delete this; }
virtual void setDeviceAPI(DeviceAPI *deviceAPI);
virtual DeviceAPI *getDeviceAPI() { return m_deviceAPI; }
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 void pushMessage(Message *msg) { m_inputMessageQueue.push(msg); }
virtual QString getSinkName() { return objectName(); }
virtual void getIdentifier(QString& id) { id = objectName(); }
virtual QString getIdentifier() const { return objectName(); }
virtual const QString& getURI() const { return getName(); }
virtual void getTitle(QString& title) { title = m_settings.m_title; }
virtual qint64 getCenterFrequency() const { return m_settings.m_inputFrequencyOffset; }
virtual void setCenterFrequency(qint64 frequency);
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 webapiWorkspaceGet(
SWGSDRangel::SWGWorkspaceInfo& response,
QString& errorMessage);
virtual int webapiSettingsPutPatch(
bool force,
const QStringList& channelSettingsKeys,
SWGSDRangel::SWGChannelSettings& response,
QString& errorMessage);
virtual int webapiReportGet(
SWGSDRangel::SWGChannelReport& response,
QString& errorMessage);
static void webapiFormatChannelSettings(
SWGSDRangel::SWGChannelSettings& response,
const DSCDemodSettings& settings);
static void webapiUpdateChannelSettings(
DSCDemodSettings& settings,
const QStringList& channelSettingsKeys,
SWGSDRangel::SWGChannelSettings& response);
ScopeVis *getScopeSink();
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 {
ChannelAPI::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;
DSCDemodBaseband* m_basebandSink;
DSCDemodSettings m_settings;
int m_basebandSampleRate; //!< stored from device message used when starting baseband sink
qint64 m_centerFrequency;
QUdpSocket m_udpSocket;
QFile m_logFile;
QTextStream m_logStream;
QNetworkAccessManager *m_networkManager;
QNetworkRequest m_networkRequest;
virtual bool handleMessage(const Message& cmd);
void applySettings(const DSCDemodSettings& settings, bool force = false);
void sendSampleRateToDemodAnalyzer();
void webapiReverseSendSettings(QList<QString>& channelSettingsKeys, const DSCDemodSettings& settings, bool force);
void webapiFormatChannelSettings(
QList<QString>& channelSettingsKeys,
SWGSDRangel::SWGChannelSettings *swgChannelSettings,
const DSCDemodSettings& settings,
bool force
);
void webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response);
private slots:
void networkManagerFinished(QNetworkReply *reply);
void handleIndexInDeviceSetChanged(int index);
};
#endif // INCLUDE_DSCDEMOD_H

Wyświetl plik

@ -0,0 +1,182 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2019 Edouard Griffiths, F4EXB //
// Copyright (C) 2023 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 "dscdemodbaseband.h"
MESSAGE_CLASS_DEFINITION(DSCDemodBaseband::MsgConfigureDSCDemodBaseband, Message)
DSCDemodBaseband::DSCDemodBaseband(DSCDemod *packetDemod) :
m_sink(packetDemod),
m_running(false)
{
qDebug("DSCDemodBaseband::DSCDemodBaseband");
m_sink.setScopeSink(&m_scopeSink);
m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(48000));
m_channelizer = new DownChannelizer(&m_sink);
}
DSCDemodBaseband::~DSCDemodBaseband()
{
m_inputMessageQueue.clear();
delete m_channelizer;
}
void DSCDemodBaseband::reset()
{
QMutexLocker mutexLocker(&m_mutex);
m_inputMessageQueue.clear();
m_sampleFifo.reset();
}
void DSCDemodBaseband::startWork()
{
QMutexLocker mutexLocker(&m_mutex);
QObject::connect(
&m_sampleFifo,
&SampleSinkFifo::dataReady,
this,
&DSCDemodBaseband::handleData,
Qt::QueuedConnection
);
connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages()));
m_running = true;
}
void DSCDemodBaseband::stopWork()
{
QMutexLocker mutexLocker(&m_mutex);
disconnect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages()));
QObject::disconnect(
&m_sampleFifo,
&SampleSinkFifo::dataReady,
this,
&DSCDemodBaseband::handleData
);
m_running = false;
}
void DSCDemodBaseband::setChannel(ChannelAPI *channel)
{
m_sink.setChannel(channel);
}
void DSCDemodBaseband::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end)
{
m_sampleFifo.write(begin, end);
}
void DSCDemodBaseband::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 DSCDemodBaseband::handleInputMessages()
{
Message* message;
while ((message = m_inputMessageQueue.pop()) != nullptr)
{
if (handleMessage(*message)) {
delete message;
}
}
}
bool DSCDemodBaseband::handleMessage(const Message& cmd)
{
if (MsgConfigureDSCDemodBaseband::match(cmd))
{
QMutexLocker mutexLocker(&m_mutex);
MsgConfigureDSCDemodBaseband& cfg = (MsgConfigureDSCDemodBaseband&) cmd;
qDebug() << "DSCDemodBaseband::handleMessage: MsgConfigureDSCDemodBaseband";
applySettings(cfg.getSettings(), cfg.getForce());
return true;
}
else if (DSPSignalNotification::match(cmd))
{
QMutexLocker mutexLocker(&m_mutex);
DSPSignalNotification& notif = (DSPSignalNotification&) cmd;
qDebug() << "DSCDemodBaseband::handleMessage: DSPSignalNotification: basebandSampleRate: " << notif.getSampleRate();
setBasebandSampleRate(notif.getSampleRate());
// We can run with very slow sample rate (E.g. 4k), but we don't want FIFO getting too small
m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(std::max(notif.getSampleRate(), 48000)));
return true;
}
else
{
return false;
}
}
void DSCDemodBaseband::applySettings(const DSCDemodSettings& settings, bool force)
{
if ((settings.m_inputFrequencyOffset != m_settings.m_inputFrequencyOffset) || force)
{
m_channelizer->setChannelization(DSCDemodSettings::DSCDEMOD_CHANNEL_SAMPLE_RATE, settings.m_inputFrequencyOffset);
m_sink.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset());
}
m_sink.applySettings(settings, force);
m_settings = settings;
}
int DSCDemodBaseband::getChannelSampleRate() const
{
return m_channelizer->getChannelSampleRate();
}
void DSCDemodBaseband::setBasebandSampleRate(int sampleRate)
{
m_channelizer->setBasebandSampleRate(sampleRate);
m_sink.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset());
}

Wyświetl plik

@ -0,0 +1,103 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2019 Edouard Griffiths, F4EXB //
// Copyright (C) 2023 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_DSCDEMODBASEBAND_H
#define INCLUDE_DSCDEMODBASEBAND_H
#include <QObject>
#include <QRecursiveMutex>
#include "dsp/samplesinkfifo.h"
#include "dsp/scopevis.h"
#include "util/message.h"
#include "util/messagequeue.h"
#include "dscdemodsink.h"
class DownChannelizer;
class ChannelAPI;
class DSCDemod;
class ScopeVis;
class DSCDemodBaseband : public QObject
{
Q_OBJECT
public:
class MsgConfigureDSCDemodBaseband : public Message {
MESSAGE_CLASS_DECLARATION
public:
const DSCDemodSettings& getSettings() const { return m_settings; }
bool getForce() const { return m_force; }
static MsgConfigureDSCDemodBaseband* create(const DSCDemodSettings& settings, bool force)
{
return new MsgConfigureDSCDemodBaseband(settings, force);
}
private:
DSCDemodSettings m_settings;
bool m_force;
MsgConfigureDSCDemodBaseband(const DSCDemodSettings& settings, bool force) :
Message(),
m_settings(settings),
m_force(force)
{ }
};
DSCDemodBaseband(DSCDemod *packetDemod);
~DSCDemodBaseband();
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);
int getChannelSampleRate() const;
ScopeVis *getScopeSink() { return &m_scopeSink; }
void setChannel(ChannelAPI *channel);
double getMagSq() const { return m_sink.getMagSq(); }
bool isRunning() const { return m_running; }
void setFifoLabel(const QString& label) { m_sampleFifo.setLabel(label); }
private:
SampleSinkFifo m_sampleFifo;
DownChannelizer *m_channelizer;
DSCDemodSink m_sink;
MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication
DSCDemodSettings m_settings;
ScopeVis m_scopeSink;
bool m_running;
QRecursiveMutex m_mutex;
bool handleMessage(const Message& cmd);
void calculateOffset(DSCDemodSink *sink);
void applySettings(const DSCDemodSettings& settings, bool force = false);
private slots:
void handleInputMessages();
void handleData(); //!< Handle data when samples have to be processed
};
#endif // INCLUDE_DSCDEMODBASEBAND_H

Wyświetl plik

@ -0,0 +1,171 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2016 Edouard Griffiths, F4EXB //
// Copyright (C) 2023 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_DSCDEMODGUI_H
#define INCLUDE_DSCDEMODGUI_H
#include "channel/channelgui.h"
#include "dsp/channelmarker.h"
#include "dsp/movingaverage.h"
#include "util/aprsfi.h"
#include "util/messagequeue.h"
#include "settings/rollupstate.h"
#include "dscdemod.h"
#include "dscdemodsettings.h"
class PluginAPI;
class DeviceUISet;
class BasebandSampleSink;
class ScopeVis;
class DSCDemod;
class DSCDemodGUI;
namespace Ui {
class DSCDemodGUI;
}
class DSCDemodGUI;
class DSCDemodGUI : public ChannelGUI {
Q_OBJECT
public:
static DSCDemodGUI* 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; }
virtual void setWorkspaceIndex(int index) { m_settings.m_workspaceIndex = index; };
virtual int getWorkspaceIndex() const { return m_settings.m_workspaceIndex; };
virtual void setGeometryBytes(const QByteArray& blob) { m_settings.m_geometryBytes = blob; };
virtual QByteArray getGeometryBytes() const { return m_settings.m_geometryBytes; };
virtual QString getTitle() const { return m_settings.m_title; };
virtual QColor getTitleColor() const { return m_settings.m_rgbColor; };
virtual void zetHidden(bool hidden) { m_settings.m_hidden = hidden; }
virtual bool getHidden() const { return m_settings.m_hidden; }
virtual ChannelMarker& getChannelMarker() { return m_channelMarker; }
virtual int getStreamIndex() const { return m_settings.m_streamIndex; }
virtual void setStreamIndex(int streamIndex) { m_settings.m_streamIndex = streamIndex; }
public slots:
void channelMarkerChangedByCursor();
void channelMarkerHighlightedByCursor();
private:
Ui::DSCDemodGUI* ui;
PluginAPI* m_pluginAPI;
DeviceUISet* m_deviceUISet;
ChannelMarker m_channelMarker;
RollupState m_rollupState;
DSCDemodSettings m_settings;
qint64 m_deviceCenterFrequency;
bool m_doApplySettings;
ScopeVis* m_scopeVis;
DSCDemod* m_dscDemod;
int m_basebandSampleRate;
uint32_t m_tickCount;
MessageQueue m_inputMessageQueue;
APRSFi *m_aprsFi;
QMenu *m_menu; // Column select context menu
QStringList m_mapItems;
enum MessageCol {
MESSAGE_COL_RX_DATE,
MESSAGE_COL_RX_TIME,
MESSAGE_COL_FORMAT,
MESSAGE_COL_ADDRESS,
MESSAGE_COL_ADDRESS_COUNTRY,
MESSAGE_COL_ADDRESS_TYPE,
MESSAGE_COL_ADDRESS_NAME,
MESSAGE_COL_CATEGORY,
MESSAGE_COL_SELF_ID,
MESSAGE_COL_SELF_ID_COUNTRY,
MESSAGE_COL_SELF_ID_TYPE,
MESSAGE_COL_SELF_ID_NAME,
MESSAGE_COL_SELF_ID_RANGE,
MESSAGE_COL_TELECOMMAND_1,
MESSAGE_COL_TELECOMMAND_2,
MESSAGE_COL_RX,
MESSAGE_COL_TX,
MESSAGE_COL_POSITION,
MESSAGE_COL_DISTRESS_ID,
MESSAGE_COL_DISTRESS,
MESSAGE_COL_NUMBER,
MESSAGE_COL_TIME,
MESSAGE_COL_COMMS,
MESSAGE_COL_EOS,
MESSAGE_COL_ECC,
MESSAGE_COL_ERRORS,
MESSAGE_COL_VALID,
MESSAGE_COL_RSSI
};
explicit DSCDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent = 0);
virtual ~DSCDemodGUI();
void blockApplySettings(bool block);
void applySettings(bool force = false);
void displaySettings();
void messageReceived(const DSCMessage& message, int errors, float rssi);
bool handleMessage(const Message& message);
void makeUIConnections();
void updateAbsoluteCenterFrequency();
void leaveEvent(QEvent*);
void enterEvent(EnterEventType*);
void resizeTable();
QAction *createCheckableItem(QString& text, int idx, bool checked);
void createMenuOpenURLAction(QMenu* tableContextMenu, const QString& text, const QString& url, const QString& arg);
void createMenuFindOnMapAction(QMenu* tableContextMenu, const QString& text, const QString& target);
void sendAreaToMapFeature(const QString& name, const QString& address, const QString& text);
void clearAreaFromMapFeature(const QString& name);
private slots:
void on_deltaFrequency_changed(qint64 value);
void on_filterInvalid_clicked(bool checked=false);
void on_filterColumn_currentIndexChanged(int index);
void on_filter_editingFinished();
void on_clearTable_clicked();
void on_udpEnabled_clicked(bool checked);
void on_udpAddress_editingFinished();
void on_udpPort_editingFinished();
void on_logEnable_clicked(bool checked=false);
void on_logFilename_clicked();
void on_logOpen_clicked();
void on_feed_clicked(bool checked=false);
void on_feed_rightClicked(const QPoint &point);
void filterRow(int row);
void filter();
void messages_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex);
void messages_sectionResized(int logicalIndex, int oldSize, int newSize);
void columnSelectMenu(QPoint pos);
void columnSelectMenuChecked(bool checked = false);
void customContextMenuRequested(QPoint point);
void onWidgetRolled(QWidget* widget, bool rollDown);
void onMenuDialogCalled(const QPoint& p);
void handleInputMessages();
void tick();
void aprsFiDataUpdated(const QList<APRSFi::AISData>& data);
};
#endif // INCLUDE_DSCDEMODGUI_H

Wyświetl plik

@ -0,0 +1,982 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DSCDemodGUI</class>
<widget class="RollupContents" name="DSCDemodGUI">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>411</width>
<height>751</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<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>
<widget class="QWidget" name="settingsContainer" native="true">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>390</width>
<height>121</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="powerLayout">
<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>
<widget class="Line" name="line_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="udpLayout">
<item>
<widget class="QLabel" name="udpLabel">
<property name="text">
<string>UDP</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="udpEnabled">
<property name="toolTip">
<string>Send messages via UDP</string>
</property>
<property name="layoutDirection">
<enum>Qt::RightToLeft</enum>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="udpAddress">
<property name="minimumSize">
<size>
<width>120</width>
<height>0</height>
</size>
</property>
<property name="focusPolicy">
<enum>Qt::ClickFocus</enum>
</property>
<property name="toolTip">
<string>Destination UDP address</string>
</property>
<property name="inputMask">
<string/>
</property>
<property name="text">
<string>127.0.0.1</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="udpSeparator">
<property name="text">
<string>:</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="udpPort">
<property name="minimumSize">
<size>
<width>50</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="focusPolicy">
<enum>Qt::ClickFocus</enum>
</property>
<property name="toolTip">
<string>Destination UDP port</string>
</property>
<property name="inputMask">
<string>00000</string>
</property>
<property name="text">
<string>4530</string>
</property>
</widget>
</item>
<item>
<spacer name="udpSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="Line" name="line_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="toolbarLayout">
<item>
<widget class="QLabel" name="filterLabel">
<property name="text">
<string>Filter</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="filterColumn">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>Display messages only from the specified station</string>
</property>
<item>
<property name="text">
<string>Date</string>
</property>
</item>
<item>
<property name="text">
<string>Time</string>
</property>
</item>
<item>
<property name="text">
<string>Format</string>
</property>
</item>
<item>
<property name="text">
<string>To</string>
</property>
</item>
<item>
<property name="text">
<string>To Country</string>
</property>
</item>
<item>
<property name="text">
<string>To Type</string>
</property>
</item>
<item>
<property name="text">
<string>To Name</string>
</property>
</item>
<item>
<property name="text">
<string>Category</string>
</property>
</item>
<item>
<property name="text">
<string>From</string>
</property>
</item>
<item>
<property name="text">
<string>From Country</string>
</property>
</item>
<item>
<property name="text">
<string>From Type</string>
</property>
</item>
<item>
<property name="text">
<string>From Name</string>
</property>
</item>
<item>
<property name="text">
<string>Range</string>
</property>
</item>
<item>
<property name="text">
<string>Telecommand 1</string>
</property>
</item>
<item>
<property name="text">
<string>Telecommand 2</string>
</property>
</item>
<item>
<property name="text">
<string>RX</string>
</property>
</item>
<item>
<property name="text">
<string>TX</string>
</property>
</item>
<item>
<property name="text">
<string>Position</string>
</property>
</item>
<item>
<property name="text">
<string>Distress Id</string>
</property>
</item>
<item>
<property name="text">
<string>Distress</string>
</property>
</item>
<item>
<property name="text">
<string>Number</string>
</property>
</item>
<item>
<property name="text">
<string>Time</string>
</property>
</item>
<item>
<property name="text">
<string>Comms</string>
</property>
</item>
<item>
<property name="text">
<string>EOS</string>
</property>
</item>
<item>
<property name="text">
<string>ECC</string>
</property>
</item>
<item>
<property name="text">
<string>Errors</string>
</property>
</item>
<item>
<property name="text">
<string>Valid</string>
</property>
</item>
<item>
<property name="text">
<string>RSSI</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QLineEdit" name="filter">
<property name="toolTip">
<string>Filter regular expression</string>
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="filterInvalid">
<property name="maximumSize">
<size>
<width>24</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>When checked, invalid messages are filtered from the table</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/funnel.png</normaloff>:/funnel.png</iconset>
</property>
</widget>
</item>
<item>
<spacer name="toolbarSpacer">
<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="ButtonSwitch" name="feed">
<property name="maximumSize">
<size>
<width>24</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Feed messages to yaddnet.org</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/txon.png</normaloff>:/txon.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="logEnable">
<property name="maximumSize">
<size>
<width>24</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Start/stop logging of received messages to .csv file</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/record_off.png</normaloff>:/record_off.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="logFilename">
<property name="toolTip">
<string>Set log .csv filename</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/save.png</normaloff>:/save.png</iconset>
</property>
<property name="checkable">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="logOpen">
<property name="toolTip">
<string>Read messages from .csv log file</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/load.png</normaloff>:/load.png</iconset>
</property>
<property name="checkable">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="clearTable">
<property name="toolTip">
<string>Clear messages</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>10</x>
<y>140</y>
<width>381</width>
<height>241</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Received Messages</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QTableWidget" name="messages">
<property name="toolTip">
<string>Received messages</string>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<column>
<property name="text">
<string>Date</string>
</property>
<property name="toolTip">
<string>Local date message was received</string>
</property>
</column>
<column>
<property name="text">
<string>Time</string>
</property>
<property name="toolTip">
<string>Local time message was received</string>
</property>
</column>
<column>
<property name="text">
<string>Format</string>
</property>
<property name="toolTip">
<string>Format specifier</string>
</property>
</column>
<column>
<property name="text">
<string>To</string>
</property>
<property name="toolTip">
<string>Address (MMSI or coordinates) of who the message is to</string>
</property>
</column>
<column>
<property name="text">
<string>Country</string>
</property>
<property name="toolTip">
<string>Country with jurisdiction of the destination of the message</string>
</property>
</column>
<column>
<property name="text">
<string>Type</string>
</property>
<property name="toolTip">
<string>MMSI type of the destination of the message</string>
</property>
</column>
<column>
<property name="text">
<string>Name</string>
</property>
<property name="toolTip">
<string>Name of the station the message is to</string>
</property>
</column>
<column>
<property name="text">
<string>Category</string>
</property>
<property name="toolTip">
<string>Message category</string>
</property>
</column>
<column>
<property name="text">
<string>From</string>
</property>
<property name="toolTip">
<string>MMSI of sender of message</string>
</property>
</column>
<column>
<property name="text">
<string>Country</string>
</property>
<property name="toolTip">
<string>Country with jurisdiction of the sender of the message</string>
</property>
</column>
<column>
<property name="text">
<string>Type</string>
</property>
<property name="toolTip">
<string>MMSI type of the sender of the message</string>
</property>
</column>
<column>
<property name="text">
<string>Name</string>
</property>
<property name="toolTip">
<string>Name of the station the message is from</string>
</property>
</column>
<column>
<property name="text">
<string>Range (km)</string>
</property>
<property name="toolTip">
<string>Distance in kilometers from My Position to ship's position obtained from aprs.fi</string>
</property>
</column>
<column>
<property name="text">
<string>Telecommand 1</string>
</property>
<property name="toolTip">
<string>Telecommand</string>
</property>
</column>
<column>
<property name="text">
<string>Telecommand 2</string>
</property>
<property name="toolTip">
<string>Telecommand</string>
</property>
</column>
<column>
<property name="text">
<string>RX</string>
</property>
<property name="toolTip">
<string>RX frequency (Hz) or channel</string>
</property>
</column>
<column>
<property name="text">
<string>TX</string>
</property>
<property name="toolTip">
<string>TX frequency (Hz) or channel</string>
</property>
</column>
<column>
<property name="text">
<string>Position</string>
</property>
<property name="toolTip">
<string>Position of ship in degrees and minutes</string>
</property>
</column>
<column>
<property name="text">
<string>Distress Id</string>
</property>
<property name="toolTip">
<string>MMSI of ship in distress</string>
</property>
</column>
<column>
<property name="text">
<string>Distress</string>
</property>
<property name="toolTip">
<string>Nature of distress</string>
</property>
</column>
<column>
<property name="text">
<string>Number</string>
</property>
<property name="toolTip">
<string>Telephone number</string>
</property>
</column>
<column>
<property name="text">
<string>Time</string>
</property>
<property name="toolTip">
<string>UTC Time</string>
</property>
</column>
<column>
<property name="text">
<string>Comms</string>
</property>
<property name="toolTip">
<string>Subsequent communications</string>
</property>
</column>
<column>
<property name="text">
<string>EOS</string>
</property>
<property name="toolTip">
<string>End of Signal</string>
</property>
</column>
<column>
<property name="text">
<string>ECC</string>
</property>
<property name="toolTip">
<string>Error checking code</string>
</property>
</column>
<column>
<property name="text">
<string>Errors</string>
</property>
<property name="toolTip">
<string>Number of symbols received with errors (which may have been corrected if ECC OK)</string>
</property>
</column>
<column>
<property name="text">
<string>Valid</string>
</property>
<property name="toolTip">
<string>Whether the message is determined to be valid (contains no detected errors)</string>
</property>
</column>
<column>
<property name="text">
<string>RSSI</string>
</property>
<property name="toolTip">
<string>Received signal strenth indicator (Average power in dBFS)</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="scopeContainer" native="true">
<property name="geometry">
<rect>
<x>10</x>
<y>390</y>
<width>716</width>
<height>341</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>714</width>
<height>0</height>
</size>
</property>
<property name="windowTitle">
<string>Waveforms</string>
</property>
<layout class="QVBoxLayout" name="transmittedLayout_2">
<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="GLScope" name="glScope" native="true">
<property name="minimumSize">
<size>
<width>200</width>
<height>250</height>
</size>
</property>
<property name="font">
<font>
<family>Liberation Mono</family>
<pointsize>8</pointsize>
</font>
</property>
</widget>
</item>
<item>
<widget class="GLScopeGUI" name="scopeGUI" native="true"/>
</item>
</layout>
</widget>
</widget>
<customwidgets>
<customwidget>
<class>ButtonSwitch</class>
<extends>QToolButton</extends>
<header>gui/buttonswitch.h</header>
</customwidget>
<customwidget>
<class>RollupContents</class>
<extends>QWidget</extends>
<header>gui/rollupcontents.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>ValueDialZ</class>
<extends>QWidget</extends>
<header>gui/valuedialz.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>LevelMeterSignalDB</class>
<extends>QWidget</extends>
<header>gui/levelmeter.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>GLScope</class>
<extends>QWidget</extends>
<header>gui/glscope.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>GLScopeGUI</class>
<extends>QWidget</extends>
<header>gui/glscopegui.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>deltaFrequency</tabstop>
<tabstop>udpEnabled</tabstop>
<tabstop>filterColumn</tabstop>
<tabstop>logEnable</tabstop>
<tabstop>logFilename</tabstop>
<tabstop>logOpen</tabstop>
<tabstop>clearTable</tabstop>
<tabstop>messages</tabstop>
</tabstops>
<resources>
<include location="../../../sdrgui/resources/res.qrc"/>
</resources>
<connections/>
</ui>

Wyświetl plik

@ -0,0 +1,93 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2016 Edouard Griffiths, F4EXB //
// Copyright (C) 2023 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 "dscdemodgui.h"
#endif
#include "dscdemod.h"
#include "dscdemodwebapiadapter.h"
#include "dscdemodplugin.h"
const PluginDescriptor DSCDemodPlugin::m_pluginDescriptor = {
DSCDemod::m_channelId,
QStringLiteral("DSC Demodulator"),
QStringLiteral("7.14.0"),
QStringLiteral("(c) Jon Beniston, M7RCE"),
QStringLiteral("https://github.com/f4exb/sdrangel"),
true,
QStringLiteral("https://github.com/f4exb/sdrangel")
};
DSCDemodPlugin::DSCDemodPlugin(QObject* parent) :
QObject(parent),
m_pluginAPI(0)
{
}
const PluginDescriptor& DSCDemodPlugin::getPluginDescriptor() const
{
return m_pluginDescriptor;
}
void DSCDemodPlugin::initPlugin(PluginAPI* pluginAPI)
{
m_pluginAPI = pluginAPI;
m_pluginAPI->registerRxChannel(DSCDemod::m_channelIdURI, DSCDemod::m_channelId, this);
}
void DSCDemodPlugin::createRxChannel(DeviceAPI *deviceAPI, BasebandSampleSink **bs, ChannelAPI **cs) const
{
if (bs || cs)
{
DSCDemod *instance = new DSCDemod(deviceAPI);
if (bs) {
*bs = instance;
}
if (cs) {
*cs = instance;
}
}
}
#ifdef SERVER_MODE
ChannelGUI* DSCDemodPlugin::createRxChannelGUI(
DeviceUISet *deviceUISet,
BasebandSampleSink *rxChannel) const
{
(void) deviceUISet;
(void) rxChannel;
return 0;
}
#else
ChannelGUI* DSCDemodPlugin::createRxChannelGUI(DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) const
{
return DSCDemodGUI::create(m_pluginAPI, deviceUISet, rxChannel);
}
#endif
ChannelWebAPIAdapter* DSCDemodPlugin::createChannelWebAPIAdapter() const
{
return new DSCDemodWebAPIAdapter();
}

Wyświetl plik

@ -0,0 +1,50 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2016 Edouard Griffiths, F4EXB //
// Copyright (C) 2023 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_DSCDEMODPLUGIN_H
#define INCLUDE_DSCDEMODPLUGIN_H
#include <QObject>
#include "plugin/plugininterface.h"
class DeviceUISet;
class BasebandSampleSink;
class DSCDemodPlugin : public QObject, PluginInterface {
Q_OBJECT
Q_INTERFACES(PluginInterface)
Q_PLUGIN_METADATA(IID "sdrangel.channel.dscdemod")
public:
explicit DSCDemodPlugin(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_DSCDEMODPLUGIN_H

Wyświetl plik

@ -0,0 +1,206 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2015 Edouard Griffiths, F4EXB. //
// Copyright (C) 2023 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 "dscdemodsettings.h"
DSCDemodSettings::DSCDemodSettings() :
m_channelMarker(nullptr),
m_scopeGUI(nullptr),
m_rollupState(nullptr)
{
resetToDefaults();
}
void DSCDemodSettings::resetToDefaults()
{
m_inputFrequencyOffset = 0;
m_rfBandwidth = 450.0f; // OBW for 2FSK = 2 * deviation + data rate. Then add a bit for carrier frequency offset
m_filterInvalid = true;
m_filterColumn = 4;
m_filter = "";
m_udpEnabled = false;
m_udpAddress = "127.0.0.1";
m_udpPort = 9999;
m_logFilename = "dsc_log.csv";
m_logEnabled = false;
m_feed = true;
m_rgbColor = QColor(181, 230, 29).rgb();
m_title = "DSC Demodulator";
m_streamIndex = 0;
m_useReverseAPI = false;
m_reverseAPIAddress = "127.0.0.1";
m_reverseAPIPort = 8888;
m_reverseAPIDeviceIndex = 0;
m_reverseAPIChannelIndex = 0;
m_workspaceIndex = 0;
m_hidden = false;
for (int i = 0; i < DSCDEMOD_COLUMNS; i++)
{
m_columnIndexes[i] = i;
m_columnSizes[i] = -1; // Autosize
}
}
QByteArray DSCDemodSettings::serialize() const
{
SimpleSerializer s(1);
s.writeS32(1, m_inputFrequencyOffset);
s.writeS32(2, m_streamIndex);
s.writeBool(3, m_filterInvalid);
s.writeS32(4, m_filterColumn);
s.writeString(5, m_filter);
if (m_channelMarker) {
s.writeBlob(6, m_channelMarker->serialize());
}
s.writeFloat(7, m_rfBandwidth);
s.writeBool(9, m_udpEnabled);
s.writeString(10, m_udpAddress);
s.writeU32(11, m_udpPort);
s.writeString(12, m_logFilename);
s.writeBool(13, m_logEnabled);
s.writeBool(14, m_feed);
s.writeU32(20, m_rgbColor);
s.writeString(21, m_title);
s.writeBool(22, m_useReverseAPI);
s.writeString(23, m_reverseAPIAddress);
s.writeU32(24, m_reverseAPIPort);
s.writeU32(25, m_reverseAPIDeviceIndex);
s.writeU32(26, m_reverseAPIChannelIndex);
if (m_rollupState) {
s.writeBlob(27, m_rollupState->serialize());
}
s.writeS32(28, m_workspaceIndex);
s.writeBlob(29, m_geometryBytes);
s.writeBool(30, m_hidden);
s.writeBlob(31, m_scopeGUI->serialize());
for (int i = 0; i < DSCDEMOD_COLUMNS; i++) {
s.writeS32(100 + i, m_columnIndexes[i]);
}
for (int i = 0; i < DSCDEMOD_COLUMNS; i++) {
s.writeS32(200 + i, m_columnSizes[i]);
}
return s.final();
}
bool DSCDemodSettings::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.readBool(3, &m_filterInvalid, true);
d.readS32(4, &m_filterColumn, 5);
d.readString(5, &m_filter, "");
if (m_channelMarker)
{
d.readBlob(6, &bytetmp);
m_channelMarker->deserialize(bytetmp);
}
d.readFloat(7, &m_rfBandwidth, 450.0f);
d.readBool(9, &m_udpEnabled);
d.readString(10, &m_udpAddress);
d.readU32(11, &utmp);
if ((utmp > 1023) && (utmp < 65535)) {
m_udpPort = utmp;
} else {
m_udpPort = 9999;
}
d.readString(12, &m_logFilename, "dsc_log.csv");
d.readBool(13, &m_logEnabled, false);
d.readBool(14, &m_feed, true);
d.readU32(20, &m_rgbColor, QColor(181, 230, 29).rgb());
d.readString(21, &m_title, "DSC Demodulator");
d.readBool(22, &m_useReverseAPI, false);
d.readString(23, &m_reverseAPIAddress, "127.0.0.1");
d.readU32(24, &utmp, 0);
if ((utmp > 1023) && (utmp < 65535)) {
m_reverseAPIPort = utmp;
} else {
m_reverseAPIPort = 8888;
}
d.readU32(25, &utmp, 0);
m_reverseAPIDeviceIndex = utmp > 99 ? 99 : utmp;
d.readU32(26, &utmp, 0);
m_reverseAPIChannelIndex = utmp > 99 ? 99 : utmp;
if (m_rollupState)
{
d.readBlob(27, &bytetmp);
m_rollupState->deserialize(bytetmp);
}
d.readS32(28, &m_workspaceIndex, 0);
d.readBlob(29, &m_geometryBytes);
d.readBool(30, &m_hidden, false);
if (m_scopeGUI)
{
d.readBlob(31, &bytetmp);
m_scopeGUI->deserialize(bytetmp);
}
for (int i = 0; i < DSCDEMOD_COLUMNS; i++) {
d.readS32(100 + i, &m_columnIndexes[i], i);
}
for (int i = 0; i < DSCDEMOD_COLUMNS; i++) {
d.readS32(200 + i, &m_columnSizes[i], -1);
}
return true;
}
else
{
resetToDefaults();
return false;
}
}

Wyświetl plik

@ -0,0 +1,76 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2017 Edouard Griffiths, F4EXB. //
// Copyright (C) 2023 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_DSCDEMODSETTINGS_H
#define INCLUDE_DSCDEMODSETTINGS_H
#include <QByteArray>
class Serializable;
// Number of columns in the table
#define DSCDEMOD_COLUMNS 28
struct DSCDemodSettings
{
qint32 m_inputFrequencyOffset;
Real m_rfBandwidth; // Not currently in GUI as probably doesn't need to be adjusted
bool m_filterInvalid;
int m_filterColumn;
QString m_filter;
bool m_udpEnabled;
QString m_udpAddress;
uint16_t m_udpPort;
bool m_feed;
quint32 m_rgbColor;
QString m_title;
Serializable *m_channelMarker;
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;
QString m_logFilename;
bool m_logEnabled;
Serializable *m_scopeGUI;
Serializable *m_rollupState;
int m_workspaceIndex;
QByteArray m_geometryBytes;
bool m_hidden;
int m_columnIndexes[DSCDEMOD_COLUMNS];//!< How the columns are ordered in the table
int m_columnSizes[DSCDEMOD_COLUMNS]; //!< Size of the columns in the table
static const int DSCDEMOD_CHANNEL_SAMPLE_RATE = 1000; // Must be integer multiple of baud rate (x10)
static const int DSCDEMOD_BAUD_RATE = 100;
static const int DSCDEMOD_FREQUENCY_SHIFT = 170;
static const int m_scopeStreams = 10;
DSCDemodSettings();
void resetToDefaults();
void setChannelMarker(Serializable *channelMarker) { m_channelMarker = channelMarker; }
void setRollupState(Serializable *rollupState) { m_rollupState = rollupState; }
void setScopeGUI(Serializable *scopeGUI) { m_scopeGUI = scopeGUI; }
QByteArray serialize() const;
bool deserialize(const QByteArray& data);
};
#endif /* INCLUDE_DSCDEMODSETTINGS_H */

Wyświetl plik

@ -0,0 +1,331 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2019 Edouard Griffiths, F4EXB //
// Copyright (C) 2023 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/scopevis.h"
#include "util/db.h"
#include "util/popcount.h"
#include "maincore.h"
#include "dscdemod.h"
#include "dscdemodsink.h"
DSCDemodSink::DSCDemodSink(DSCDemod *packetDemod) :
m_dscDemod(packetDemod),
m_channelSampleRate(DSCDemodSettings::DSCDEMOD_CHANNEL_SAMPLE_RATE),
m_channelFrequencyOffset(0),
m_magsqSum(0.0f),
m_magsqPeak(0.0f),
m_magsqCount(0),
m_messageQueueToChannel(nullptr),
m_exp(nullptr),
m_sampleBufferIndex(0)
{
m_magsq = 0.0;
for (int i = 0; i < DSCDemodSettings::m_scopeStreams; i++) {
m_sampleBuffer[i].resize(m_sampleBufferSize);
}
applySettings(m_settings, true);
applyChannelSettings(m_channelSampleRate, m_channelFrequencyOffset, true);
m_lowpassComplex1.create(301, DSCDemodSettings::DSCDEMOD_CHANNEL_SAMPLE_RATE, DSCDemodSettings::DSCDEMOD_BAUD_RATE * 1.1);
m_lowpassComplex2.create(301, DSCDemodSettings::DSCDEMOD_CHANNEL_SAMPLE_RATE, DSCDemodSettings::DSCDEMOD_BAUD_RATE * 1.1);
}
DSCDemodSink::~DSCDemodSink()
{
delete[] m_exp;
}
void DSCDemodSink::sampleToScope(Complex sample, Real abs1Filt, Real abs2Filt, Real unbiasedData, Real biasedData)
{
if (m_scopeSink)
{
m_sampleBuffer[0][m_sampleBufferIndex] = sample;
m_sampleBuffer[1][m_sampleBufferIndex] = Complex(m_magsq, 0.0f);
m_sampleBuffer[2][m_sampleBufferIndex] = Complex(abs1Filt, 0.0f);
m_sampleBuffer[3][m_sampleBufferIndex] = Complex(abs2Filt, 0.0f);
m_sampleBuffer[4][m_sampleBufferIndex] = Complex(unbiasedData, 0.0f);
m_sampleBuffer[5][m_sampleBufferIndex] = Complex(biasedData, 0.0f);
m_sampleBuffer[6][m_sampleBufferIndex] = Complex(m_data, 0.0f);
m_sampleBuffer[7][m_sampleBufferIndex] = Complex(m_clock, 0.0f);
m_sampleBuffer[8][m_sampleBufferIndex] = Complex(m_bit, 0.0f);
m_sampleBuffer[9][m_sampleBufferIndex] = Complex(m_gotSOP, 0.0f);
m_sampleBufferIndex++;
if (m_sampleBufferIndex == m_sampleBufferSize)
{
std::vector<ComplexVector::const_iterator> vbegin;
for (int i = 0; i < DSCDemodSettings::m_scopeStreams; i++) {
vbegin.push_back(m_sampleBuffer[i].begin());
}
m_scopeSink->feed(vbegin, m_sampleBufferSize);
m_sampleBufferIndex = 0;
}
}
}
void DSCDemodSink::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 DSCDemodSink::processOneSample(Complex &ci)
{
// Calculate average and peak levels for level meter
double magsqRaw = ci.real()*ci.real() + ci.imag()*ci.imag();;
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++;
// Sum power while data is being received
if (m_gotSOP)
{
m_rssiMagSqSum += magsq;
m_rssiMagSqCount++;
}
ci /= SDR_RX_SCALEF;
// Correlate with expected frequencies
Complex exp = m_exp[m_expIdx];
m_expIdx = (m_expIdx + 1) % m_expLength;
Complex corr1 = ci * exp;
Complex corr2 = ci * std::conj(exp);
// Low pass filter
Real abs1Filt = std::abs(m_lowpassComplex1.filter(corr1));
Real abs2Filt = std::abs(m_lowpassComplex2.filter(corr2));
// Envelope calculation
m_movMax1(abs1Filt);
m_movMax2(abs2Filt);
Real env1 = m_movMax1.getMaximum();
Real env2 = m_movMax2.getMaximum();
// Automatic threshold correction to compensate for frequency selective fading
// http://www.w7ay.net/site/Technical/ATC/index.html
Real bias1 = abs1Filt - 0.5 * env1;
Real bias2 = abs2Filt - 0.5 * env2;
Real unbiasedData = abs1Filt - abs2Filt;
Real biasedData = bias1 - bias2;
// Save current data for edge detection
m_dataPrev = m_data;
// Set data according to stongest correlation
m_data = biasedData > 0;
// Calculate timing error (we expect clockCount to be 0 when data changes), and add a proportion of it
if (m_data && !m_dataPrev) {
m_clockCount -= m_clockCount * 0.25;
}
m_clockCount += 1.0;
if (m_clockCount >= m_samplesPerBit/2.0-1.0)
{
// Sample in middle of symbol
receiveBit(m_data);
m_clock = 1;
// Wrap clock counter
m_clockCount -= m_samplesPerBit;
}
else
{
m_clock = 0;
}
sampleToScope(ci, abs1Filt, abs2Filt, unbiasedData, biasedData);
}
const QList<DSCDemodSink::PhasingPattern> DSCDemodSink::m_phasingPatterns = {
{0b1011111001'1111011001'1011111001, 9}, // 125 111 125
{0b1111011001'1011111001'0111011010, 8}, // 111 125 110
{0b1011111001'0111011010'1011111001, 7}, // 125 110 125
{0b0111011010'1011111001'1011011010, 6}, // 110 125 109
{0b1011111001'1011011010'1011111001, 5}, // 125 109 125
{0b1011011010'1011111001'0011011011, 4}, // 109 125 108
{0b1011111001'0011011011'1011111001, 3}, // 125 108 125
{0b0011011011'1011111001'1101011010, 2}, // 108 125 107
{0b1011111001'1101011010'1011111001, 1}, // 125 107 125
{0b1101011010'1011111001'0101011011, 0}, // 107 125 106
};
void DSCDemodSink::receiveBit(bool bit)
{
m_bit = bit;
// Store in shift reg
m_bits = (m_bits << 1) | m_bit;
m_bitCount++;
if (!m_gotSOP)
{
// Dot pattern - 200 1/0s or 20 1/0s
// Phasing pattern - 6 DX=125 RX=111 110 109 108 107 106 105 104
// Phasing is considered to be achieved when two DXs and one RX, or two RXs and one DX, or three RXs in the appropriate DX or RX positions, respectively, are successfully received.
if (m_bitCount == 10*3)
{
m_bitCount--;
unsigned int pat = m_bits & 0x3fffffff;
for (int i = 0; i < m_phasingPatterns.size(); i++)
{
if (pat == m_phasingPatterns[i].m_pattern)
{
m_dscDecoder.init(m_phasingPatterns[i].m_offset);
m_gotSOP = true;
m_bitCount = 0;
break;
}
}
}
}
else
{
if (m_bitCount == 10)
{
if (m_dscDecoder.decodeBits(m_bits & 0x3ff))
{
QByteArray bytes = m_dscDecoder.getMessage();
DSCMessage message(bytes, QDateTime::currentDateTime());
//qDebug() << "RX Bytes: " << bytes.toHex();
//qDebug() << "DSC Message: " << message.toString();
if (getMessageQueueToChannel())
{
float rssi = CalcDb::dbPower(m_rssiMagSqSum / m_rssiMagSqCount);
DSCDemod::MsgMessage *msg = DSCDemod::MsgMessage::create(message, m_dscDecoder.getErrors(), rssi);
getMessageQueueToChannel()->push(msg);
}
// Reset demod
init();
}
m_bitCount = 0;
}
}
}
void DSCDemodSink::applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force)
{
qDebug() << "DSCDemodSink::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, m_settings.m_rfBandwidth / 2.2);
m_interpolatorDistance = (Real) channelSampleRate / (Real) DSCDemodSettings::DSCDEMOD_CHANNEL_SAMPLE_RATE;
m_interpolatorDistanceRemain = m_interpolatorDistance;
}
m_channelSampleRate = channelSampleRate;
m_channelFrequencyOffset = channelFrequencyOffset;
}
void DSCDemodSink::init()
{
m_expIdx = 0;
m_bit = 0;
m_bits = 0;
m_bitCount = 0;
m_gotSOP = false;
m_errorCount = 0;
m_clockCount = -m_samplesPerBit/2.0;
m_clock = 0;
m_int = 0.0;
m_rssiMagSqSum = 0.0;
m_rssiMagSqCount = 0;
m_consecutiveErrors = 0;
m_messageBuffer = "";
}
void DSCDemodSink::applySettings(const DSCDemodSettings& settings, bool force)
{
qDebug() << "DSCDemodSink::applySettings:"
<< " m_rfBandwidth: " << settings.m_rfBandwidth
<< " force: " << force;
if ((settings.m_rfBandwidth != m_settings.m_rfBandwidth) || force)
{
m_interpolator.create(16, m_channelSampleRate, settings.m_rfBandwidth / 2.2);
m_interpolatorDistance = (Real) m_channelSampleRate / (Real) DSCDemodSettings::DSCDEMOD_CHANNEL_SAMPLE_RATE;
m_interpolatorDistanceRemain = m_interpolatorDistance;
}
if (force)
{
delete[] m_exp;
m_exp = new Complex[m_expLength];
Real f0 = 0.0f;
for (int i = 0; i < m_expLength; i++)
{
m_exp[i] = Complex(cos(f0), sin(f0));
f0 += 2.0f * (Real)M_PI * (DSCDemodSettings::DSCDEMOD_FREQUENCY_SHIFT/2.0f) / DSCDemodSettings::DSCDEMOD_CHANNEL_SAMPLE_RATE;
}
init();
m_movMax1.setSize(m_samplesPerBit * 8);
m_movMax2.setSize(m_samplesPerBit * 8);
}
m_settings = settings;
}

Wyświetl plik

@ -0,0 +1,154 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2019 Edouard Griffiths, F4EXB //
// Copyright (C) 2023 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_DSCDEMODSINK_H
#define INCLUDE_DSCDEMODSINK_H
#include <QVector>
#include <QMap>
#include <QDateTime>
#include "dsp/channelsamplesink.h"
#include "dsp/nco.h"
#include "dsp/interpolator.h"
#include "dsp/firfilter.h"
#include "util/movingaverage.h"
#include "util/movingmaximum.h"
#include "util/messagequeue.h"
#include "util/dsc.h"
#include "dscdemodsettings.h"
class ChannelAPI;
class DSCDemod;
class ScopeVis;
class DSCDemodSink : public ChannelSampleSink {
public:
DSCDemodSink(DSCDemod *packetDemod);
~DSCDemodSink();
virtual void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end);
void setScopeSink(ScopeVis* scopeSink) { m_scopeSink = scopeSink; }
void applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force = false);
void applySettings(const DSCDemodSettings& settings, bool force = false);
void setMessageQueueToChannel(MessageQueue *messageQueue) { m_messageQueueToChannel = messageQueue; }
void setChannel(ChannelAPI *channel) { m_channel = channel; }
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;
};
struct PhasingPattern {
unsigned int m_pattern;
unsigned int m_offset;
};
ScopeVis* m_scopeSink; // Scope GUI to display baseband waveform
DSCDemod *m_dscDemod;
DSCDemodSettings m_settings;
ChannelAPI *m_channel;
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_lowpassComplex1;
Lowpass<Complex> m_lowpassComplex2;
MovingMaximum<Real> m_movMax1;
MovingMaximum<Real> m_movMax2;
static const int m_expLength = 600;
static const int m_samplesPerBit = DSCDemodSettings::DSCDEMOD_CHANNEL_SAMPLE_RATE / DSCDemodSettings::DSCDEMOD_BAUD_RATE;
Complex *m_exp;
int m_expIdx;
int m_bit;
bool m_data;
bool m_dataPrev;
double m_clockCount;
double m_clock;
double m_int;
double m_rssiMagSqSum;
int m_rssiMagSqCount;
unsigned int m_bits;
int m_bitCount;
bool m_gotSOP;
int m_errorCount;
int m_consecutiveErrors;
QString m_messageBuffer;
DSCDecoder m_dscDecoder;
static const QList<PhasingPattern> m_phasingPatterns;
ComplexVector m_sampleBuffer[DSCDemodSettings::m_scopeStreams];
static const int m_sampleBufferSize = DSCDemodSettings::DSCDEMOD_CHANNEL_SAMPLE_RATE / 20;
int m_sampleBufferIndex;
void processOneSample(Complex &ci);
MessageQueue *getMessageQueueToChannel() { return m_messageQueueToChannel; }
void sampleToScope(Complex sample, Real abs1Filt, Real abs2Filt, Real unbiasedData, Real biasedData);
void init();
void receiveBit(bool bit);
};
#endif // INCLUDE_DSCDEMODSINK_H

Wyświetl plik

@ -0,0 +1,52 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2019 Edouard Griffiths, F4EXB. //
// Copyright (C) 2023 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 "dscdemod.h"
#include "dscdemodwebapiadapter.h"
DSCDemodWebAPIAdapter::DSCDemodWebAPIAdapter()
{}
DSCDemodWebAPIAdapter::~DSCDemodWebAPIAdapter()
{}
int DSCDemodWebAPIAdapter::webapiSettingsGet(
SWGSDRangel::SWGChannelSettings& response,
QString& errorMessage)
{
(void) errorMessage;
response.setDscDemodSettings(new SWGSDRangel::SWGDSCDemodSettings());
response.getDscDemodSettings()->init();
DSCDemod::webapiFormatChannelSettings(response, m_settings);
return 200;
}
int DSCDemodWebAPIAdapter::webapiSettingsPutPatch(
bool force,
const QStringList& channelSettingsKeys,
SWGSDRangel::SWGChannelSettings& response,
QString& errorMessage)
{
(void) force;
(void) errorMessage;
DSCDemod::webapiUpdateChannelSettings(m_settings, channelSettingsKeys, response);
return 200;
}

Wyświetl plik

@ -0,0 +1,50 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2019 Edouard Griffiths, F4EXB. //
// Copyright (C) 2023 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_DSCDEMOD_WEBAPIADAPTER_H
#define INCLUDE_DSCDEMOD_WEBAPIADAPTER_H
#include "channel/channelwebapiadapter.h"
#include "dscdemodsettings.h"
/**
* Standalone API adapter only for the settings
*/
class DSCDemodWebAPIAdapter : public ChannelWebAPIAdapter {
public:
DSCDemodWebAPIAdapter();
virtual ~DSCDemodWebAPIAdapter();
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:
DSCDemodSettings m_settings;
};
#endif // INCLUDE_DSCDEMOD_WEBAPIADAPTER_H

Wyświetl plik

@ -0,0 +1,116 @@
<h1>DSC (Digital Selective Calling) Demodulator Plugin</h1>
<h2>Introduction</h2>
This plugin can be used to demodulate DSC (Digital Selective Calling) transmissions, which are short, pre-defined digital messages transmitted by marine radios.
DSC messages are transmitted using FSK with 170Hz separation at 100 baud, as specified by [ITU-R M.493](https://www.itu.int/dms_pubrec/itu-r/rec/m/R-REC-M.493-15-201901-I!!PDF-E.pdf]).
DSC messages can be transmitted on a variety of frequencies, but are most commonly found on: 2,187.5kHz, 8,414.5kHz, 16,804.5kHz and 156.525 MHz (VHF Ch. 70).
<h2>Interface</h2>
The top and bottom bars of the channel window are described [here](../../../sdrgui/channel/readme.md)
![DSC Demodulator plugin GUI](../../../doc/img/DSCDemod_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: UDP</h3>
When checked, received messages are forwarded to the specified UDP address (5) and port (6).
<h3>5: UDP address</h3>
IP address of the host to forward received messages to via UDP.
<h3>6: UDP port</h3>
UDP port number to forward received messages to.
<h3>7: Filter</h3>
This drop down displays a list of all columns which can be used for filtering (8).
<h3>8: Filter Reg Exp</h3>
Specifes a regular expression used to filter data in the table, using data in the column specified by (8).
<h3>9: Filter Invalid</h3>
When checked, invalid messages will be filtered from the table.
<h3>10: Feed to YaDDNet</h3>
When checked, valid messages will be forwarded to [YaDDNet](http://yaddnet.org/).
YaDDNet aggregates DSD messages from different users around the world storing them in a searchable database.
The messages are submitted with Preferences > My Position... > Station name used as the ID.
Right click to open http://yaddnet.org/ in your browser, showing recent messages received from this ID.
<h3>11: Start/stop Logging Messages to .csv File</h3>
When checked, writes all received messages to a .csv file, specified by (12).
<h3>12: .csv Log Filename</h3>
Click to specify the name of the .csv file which received messasges are logged to.
<h3>13: Read Data from .csv File</h3>
Click to specify a previously written .csv log file, which is read and used to update the table.
<h3>12: Received Messages Table</h3>
The received messages table displays the contents of the messages that have been received. Most of the fields are decoded directly from the message,
however, a few, such as ships names, are found by querying aprs.fi with the MMSI.
* Date - Date the message was received.
* Time - Time the message was received.
* Format - The message format (Selective call, Geographic call, Group call, Distress alert, All ships, Automatic call).
* To - Who the message is to (The address field). This is typically an MMSI, but can also be a geographic area.
* Country - Country with jurisdiction of the destination of the message
* Type - MMSI type of the destination of the message (Ship / Coast station).
* Name - The name of ship / station the message is for (From aprs.fi).
* Category - The message category (Safety, Routine, Urgency, Distress).
* From - MMSI of sender of message.
* Country - Country with jurisdiction of the sender of the message.
* Type - MMSI type of the sender of the message (Ship / Coast station).
* Name - The name of ship / station sending the message (From aprs.fi).
* Range (km) - The distance in kilometers from My Position (specified under Preferences > My Position) to the position of the sender of the message, as reported by aprs.fi (usually from AIS data).
* Telecommand 1 - First telecommand (Test / J3E (SSB) telephony and so on).
* Telecommand 2 - Second telecommand.
* RX - RX frequency (Hz) or channel.
* TX - TX frequency (Hz) or channel.
* Position - Position of ship in degrees and minutes.
* Distress Id - MMSI of ship in distress.
* Distress - Nature of distress (Sinking, Collision, Man overboard and so on).
* Number - Telephone number.
* Time - UTC Time.
* Comms - Subsequent communications.
* EOS - End of Signal (Req ACK, ACK, EOS).
* ECC - Indicates if calculated ECC (Error Checking Code) matches received ECC.
* Errors - Number of symbols received with errors (which may have been corrected if ECC is OK)
* Valid - Whether the message is determined to be valid (contains no detected errors).
* RSSI - Average channel power in dB, while receiving the message.
Right clicking on the header will open a menu allowing you to select which columns are visible.
To reorder the columns, left click and drag left or right a column header.
Left click on a header to sort the table by the data in that column.
Right clicking on a cell will open a pop-up menu that that allows:
* MMSIs to be looked up on some popular web sites,
* georaphical call areas to be drawn on the map,
* ships to be located on the [Map](../../feature/map/readme.md) if also being tracked via AIS, or
* tune SSB Demods to the RX frequency.

Wyświetl plik

@ -172,6 +172,7 @@ set(sdrbase_SOURCES
util/ais.cpp
util/android.cpp
util/aprsfi.cpp
util/aviationweather.cpp
util/ax25.cpp
util/aprs.cpp
@ -184,6 +185,7 @@ set(sdrbase_SOURCES
util/CRC64.cpp
util/csv.cpp
util/db.cpp
util/dsc.cpp
util/fixedtraits.cpp
util/fits.cpp
util/flightinformation.cpp
@ -196,6 +198,7 @@ set(sdrbase_SOURCES
util/maidenhead.cpp
util/message.cpp
util/messagequeue.cpp
util/mmsi.cpp
util/morse.cpp
util/navtex.cpp
util/openaip.cpp
@ -398,6 +401,7 @@ set(sdrbase_HEADERS
util/ais.h
util/android.h
util/aprsfi.h
util/aviationweather.h
util/ax25.h
util/aprs.h
@ -409,6 +413,7 @@ set(sdrbase_HEADERS
util/CRC64.h
util/csv.h
util/db.h
util/dsc.h
util/doublebuffer.h
util/doublebufferfifo.h
util/doublebuffermultiple.h
@ -426,6 +431,7 @@ set(sdrbase_HEADERS
util/maidenhead.h
util/message.h
util/messagequeue.h
util/mmsi.h
util/morse.h
util/movingaverage.h
util/movingmaximum.h

Wyświetl plik

@ -0,0 +1,187 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2023 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 "aprsfi.h"
#include <QDebug>
#include <QUrl>
#include <QUrlQuery>
#include <QNetworkReply>
#include <QJsonObject>
#include <QJsonDocument>
QMutex APRSFi::m_mutex;
QHash<QString, APRSFi::AISData> APRSFi::m_aisCache;
APRSFi::APRSFi(const QString& apiKey, int cacheValidMins) :
m_apiKey(apiKey),
m_cacheValidMins(cacheValidMins)
{
m_networkManager = new QNetworkAccessManager();
connect(m_networkManager, &QNetworkAccessManager::finished, this, &APRSFi::handleReply);
}
APRSFi::~APRSFi()
{
disconnect(m_networkManager, &QNetworkAccessManager::finished, this, &APRSFi::handleReply);
delete m_networkManager;
}
APRSFi* APRSFi::create(const QString& apiKey, int cacheValidMins)
{
return new APRSFi(apiKey, cacheValidMins);
}
void APRSFi::getData(const QStringList& names)
{
QStringList nonCachedNames;
QDateTime currentDateTime = QDateTime::currentDateTime();
QMutexLocker locker(&m_mutex);
for (const auto& name : names)
{
bool cached = false;
QList<AISData> dataList;
if (m_aisCache.contains(name))
{
const AISData& d = m_aisCache[name];
if (d.m_dateTime.secsTo(currentDateTime) < m_cacheValidMins*60)
{
dataList.append(d);
cached = true;
}
}
if (dataList.size() > 0) {
emit dataUpdated(dataList);
}
if (!cached) {
nonCachedNames.append(name);
}
}
if (nonCachedNames.size() > 0)
{
QString nameList = nonCachedNames.join(",");
QUrl url(QString("https://api.aprs.fi/api/get"));
QUrlQuery query;
query.addQueryItem("name", nameList);
query.addQueryItem("what", "loc");
query.addQueryItem("apikey", m_apiKey);
query.addQueryItem("format", "json");
url.setQuery(query);
m_networkManager->get(QNetworkRequest(url));
}
}
void APRSFi::getData(const QString& name)
{
QStringList names;
names.append(name);
getData(names);
}
bool APRSFi::containsNonNull(const QJsonObject& obj, const QString &key) const
{
if (obj.contains(key))
{
QJsonValue val = obj.value(key);
return !val.isNull();
}
return false;
}
void APRSFi::handleReply(QNetworkReply* reply)
{
if (reply)
{
if (!reply->error())
{
QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
if (document.isObject())
{
QJsonObject docObj = document.object();
QDateTime receivedDateTime = QDateTime::currentDateTime();
if (docObj.contains(QStringLiteral("entries")))
{
QJsonArray array = docObj.value(QStringLiteral("entries")).toArray();
QList<AISData> data;
for (auto valRef : array)
{
if (valRef.isObject())
{
QJsonObject obj = valRef.toObject();
AISData measurement;
measurement.m_dateTime = receivedDateTime;
if (obj.contains(QStringLiteral("name"))) {
measurement.m_name = obj.value(QStringLiteral("name")).toString();
}
if (obj.contains(QStringLiteral("mmsi"))) {
measurement.m_mmsi = obj.value(QStringLiteral("mmsi")).toString();
}
if (containsNonNull(obj, QStringLiteral("time"))) {
measurement.m_firstTime = QDateTime::fromString(obj.value(QStringLiteral("time")).toString(), Qt::ISODate);
}
if (containsNonNull(obj, QStringLiteral("lastTime"))) {
measurement.m_lastTime = QDateTime::fromString(obj.value(QStringLiteral("lastTime")).toString(), Qt::ISODate);
}
if (containsNonNull(obj, QStringLiteral("lat"))) {
measurement.m_latitude = obj.value(QStringLiteral("lat")).toDouble();
}
if (containsNonNull(obj, QStringLiteral("lng"))) {
measurement.m_longitude = obj.value(QStringLiteral("lng")).toDouble();
}
data.append(measurement);
if (!measurement.m_mmsi.isEmpty())
{
QMutexLocker locker(&m_mutex);
m_aisCache.insert(measurement.m_mmsi, measurement);
}
}
else
{
qDebug() << "APRSFi::handleReply: Array element is not an object: " << valRef;
}
}
if (data.size() > 0) {
emit dataUpdated(data);
} else {
qDebug() << "APRSFi::handleReply: No data in array: " << document;
}
}
}
else
{
qDebug() << "APRSFi::handleReply: Document is not an object: " << document;
}
}
else
{
qWarning() << "APRSFi::handleReply: error: " << reply->error();
}
reply->deleteLater();
}
else
{
qWarning() << "APRSFi::handleReply: reply is null";
}
}

Wyświetl plik

@ -0,0 +1,89 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2023 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_APRSFI_H
#define INCLUDE_APRSFI_H
#include <QtCore>
#include <QMutex>
#include "export.h"
class QNetworkAccessManager;
class QNetworkReply;
// aprs.fi API
// Allows querying APRS and AIS data
// Data can be cached to help avoid rate limiting on the server
class SDRBASE_API APRSFi : public QObject
{
Q_OBJECT
protected:
APRSFi(const QString& apiKey, int cacheValidMins);
public:
struct LocationData {
QString m_name;
QDateTime m_firstTime; // First time this position was reported
QDateTime m_lastTime; // Last time this position was reported
float m_latitude;
float m_longitude;
QString m_callsign;
QDateTime m_dateTime; // Data/time this data was received from APRS.fi
LocationData() :
m_latitude(NAN),
m_longitude(NAN)
{
}
};
struct AISData : LocationData {
QString m_mmsi;
QString m_imo;
AISData()
{
}
};
// Keys are free from https://aprs.fi/ - so get your own
static APRSFi* create(const QString& apiKey="184212.WhYgz2jqu3l2O", int cacheValidMins=10);
~APRSFi();
void getData(const QStringList& names);
void getData(const QString& name);
private slots:
void handleReply(QNetworkReply* reply);
signals:
void dataUpdated(const QList<AISData>& data); // Called when new data available.
private:
bool containsNonNull(const QJsonObject& obj, const QString &key) const;
QNetworkAccessManager *m_networkManager;
QString m_apiKey;
int m_cacheValidMins;
static QMutex m_mutex;
static QHash<QString, AISData> m_aisCache;
};
#endif /* INCLUDE_APRSFI_H */

Wyświetl plik

@ -0,0 +1,968 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2023 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 "util/dsc.h"
#include "util/popcount.h"
// "short" strings are meant to be compatible with YaDDNet
QMap<DSCMessage::FormatSpecifier, QString> DSCMessage::m_formatSpecifierStrings = {
{GEOGRAPHIC_CALL, "Geographic call"},
{DISTRESS_ALERT, "Distress alert"},
{GROUP_CALL, "Group call"},
{ALL_SHIPS, "All ships"},
{SELECTIVE_CALL, "Selective call"},
{AUTOMATIC_CALL, "Automatic call"}
};
QMap<DSCMessage::FormatSpecifier, QString> DSCMessage::m_formatSpecifierShortStrings = {
{GEOGRAPHIC_CALL, "AREA"},
{DISTRESS_ALERT, "DIS"},
{GROUP_CALL, "GRP"},
{ALL_SHIPS, "ALL"},
{SELECTIVE_CALL, "SEL"},
{AUTOMATIC_CALL, "AUT"}
};
QMap<DSCMessage::Category, QString> DSCMessage::m_categoryStrings = {
{ROUTINE, "Routine"},
{SAFETY, "Safety"},
{URGENCY, "Urgency"},
{DISTRESS, "Distress"}
};
QMap<DSCMessage::Category, QString> DSCMessage::m_categoryShortStrings = {
{ROUTINE, "RTN"},
{SAFETY, "SAF"},
{URGENCY, "URG"},
{DISTRESS, "DIS"}
};
QMap<DSCMessage::FirstTelecommand, QString> DSCMessage::m_telecommand1Strings = {
{F3E_G3E_ALL_MODES_TP, "F3E (FM speech)/G3E (phase modulated speech) all modes telephony"},
{F3E_G3E_DUPLEX_TP, "F3E (FM speech)/G3E (phase modulated speech) duplex telephony"},
{POLLING, "Polling"},
{UNABLE_TO_COMPLY, "Unable to comply"},
{END_OF_CALL, "End of call"},
{DATA, "Data"},
{J3E_TP, "J3E (SSB) telephony"},
{DISTRESS_ACKNOWLEDGEMENT, "Distress acknowledgement"},
{DISTRESS_ALERT_RELAY, "Distress alert relay"},
{F1B_J2B_TTY_FEC, "F1B (FSK) J2B (FSK via SSB) TTY FEC"},
{F1B_J2B_TTY_AQR, "F1B (FSK) J2B (FSK via SSB) TTY AQR"},
{TEST, "Test"},
{POSITION_UPDATE, "Position update"},
{NO_INFORMATION, "No information"}
};
QMap<DSCMessage::FirstTelecommand, QString> DSCMessage::m_telecommand1ShortStrings = {
{F3E_G3E_ALL_MODES_TP, "F3E/G3E"},
{F3E_G3E_DUPLEX_TP, "F3E/G3E, Duplex TP"},
{POLLING, "POLL"},
{UNABLE_TO_COMPLY, "UNABLE TO COMPLY"},
{END_OF_CALL, "EOC"},
{DATA, "DATA"},
{J3E_TP, "J3E TP"},
{DISTRESS_ACKNOWLEDGEMENT, "DISTRESS ACK"},
{DISTRESS_ALERT_RELAY, "DISTRESS RELAY"},
{F1B_J2B_TTY_FEC, "F1B/J2B TTY-FEC"},
{F1B_J2B_TTY_AQR, "F1B/J2B TTY-ARQ"},
{TEST, "TEST"},
{POSITION_UPDATE, "POSUPD"},
{NO_INFORMATION, "NOINF"}
};
QMap<DSCMessage::SecondTelecommand, QString> DSCMessage::m_telecommand2Strings = {
{NO_REASON, "No reason"},
{CONGESTION, "Congestion at switching centre"},
{BUSY, "Busy"},
{QUEUE, "Queue indication"},
{BARRED, "Station barred"},
{NO_OPERATOR, "No operator available"},
{OPERATOR_UNAVAILABLE, "Operator temporarily unavailable"},
{EQUIPMENT_DISABLED, "Equipment disabled"},
{UNABLE_TO_USE_CHANNEL, "Unable to use proposed channel"},
{UNABLE_TO_USE_MODE, "Unable to use proposed mode"},
{NOT_PARTIES_TO_CONFLICT, "Ships and aircraft of States not parties to an armed conflict"},
{MEDICAL_TRANSPORTS, "Medical transports"},
{PAY_PHONE, "Pay-phone/public call office"},
{FAX, "Facsimile"},
{NO_INFORMATION_2, "No information"}
};
QMap<DSCMessage::SecondTelecommand, QString> DSCMessage::m_telecommand2ShortStrings = {
{NO_REASON, "NO REASON GIVEN"},
{CONGESTION, "CONGESTION AT MARITIME CENTRE"},
{BUSY, "BUSY"},
{QUEUE, "QUEUE INDICATION"},
{BARRED, "STATION BARRED"},
{NO_OPERATOR, "NO OPERATOR AVAILABLE"},
{OPERATOR_UNAVAILABLE, "OPERATOR TEMPORARILY UNAVAILABLE"},
{EQUIPMENT_DISABLED, "EQUIPMENT DISABLED"},
{UNABLE_TO_USE_CHANNEL, "UNABLE TO USE PROPOSED CHANNEL"},
{UNABLE_TO_USE_MODE, "UNABLE TO USE PROPOSED MODE"},
{NOT_PARTIES_TO_CONFLICT, "SHIPS/AIRCRAFT OF STATES NOT PARTIES TO ARMED CONFLICT"},
{MEDICAL_TRANSPORTS, "MEDICAL TRANSPORTS"},
{PAY_PHONE, "PAY-PHONE/PUBLIC CALL OFFICE"},
{FAX, "FAX/DATA ACCORDING ITU-R M1081"},
{NO_INFORMATION_2, "NOINF"}
};
QMap<DSCMessage::DistressNature, QString> DSCMessage::m_distressNatureStrings = {
{FIRE, "Fire, explosion"},
{FLOODING, "Flooding"},
{COLLISION, "Collision"},
{GROUNDING, "Grounding"},
{LISTING, "Listing"},
{SINKING, "Sinking"},
{ADRIFT, "Adrift"},
{UNDESIGNATED, "Undesignated"},
{ABANDONING_SHIP, "Abandoning ship"},
{PIRACY, "Piracy, armed attack"},
{MAN_OVERBOARD, "Man overboard"},
{EPIRB, "EPIRB"}
};
QMap<DSCMessage::EndOfSignal, QString> DSCMessage::m_endOfSignalStrings = {
{REQ, "Req ACK"},
{ACK, "ACK"},
{EOS, "EOS"}
};
QMap<DSCMessage::EndOfSignal, QString> DSCMessage::m_endOfSignalShortStrings = {
{REQ, "REQ"},
{ACK, "ACK"},
{EOS, "EOS"}
};
DSCMessage::DSCMessage(const QByteArray& data, QDateTime dateTime) :
m_dateTime(dateTime),
m_data(data)
{
decode(data);
}
QString DSCMessage::toString(const QString separator) const
{
QStringList s;
s.append(QString("Format specifier: %1").arg(formatSpecifier()));
if (m_hasAddress) {
s.append(QString("Address: %1").arg(m_address));
}
if (m_hasCategory) {
s.append(QString("Category: %1").arg(category()));
}
s.append(QString("Self Id: %1").arg(m_selfId));
if (m_hasTelecommand1) {
s.append(QString("Telecommand 1: %1").arg(telecommand1(m_telecommand1)));
}
if (m_hasTelecommand2) {
s.append(QString("Telecommand 2: %1").arg(telecommand2(m_telecommand2)));
}
if (m_hasDistressId) {
s.append(QString("Distress Id: %1").arg(m_distressId));
}
if (m_hasDistressNature)
{
s.append(QString("Distress nature: %1").arg(distressNature(m_distressNature)));
s.append(QString("Distress coordinates: %1").arg(m_position));
}
else if (m_hasPosition)
{
s.append(QString("Position: %1").arg(m_position));
}
if (m_hasFrequency1) {
s.append(QString("RX Frequency: %1Hz").arg(m_frequency1));
}
if (m_hasChannel1) {
s.append(QString("RX Channel: %1").arg(m_channel1));
}
if (m_hasFrequency2) {
s.append(QString("TX Frequency: %1Hz").arg(m_frequency2));
}
if (m_hasChannel2) {
s.append(QString("TX Channel: %1").arg(m_channel2));
}
if (m_hasNumber) {
s.append(QString("Phone Number: %1").arg(m_number));
}
if (m_hasTime) {
s.append(QString("Time: %1").arg(m_time.toString()));
}
if (m_hasSubsequenceComms) {
s.append(QString("Subsequent comms: %1").arg(telecommand1(m_subsequenceComms)));
}
return s.join(separator);
}
QString DSCMessage::toYaddNetFormat(const QString& id, qint64 frequency) const
{
QStringList s;
// rx_id
s.append(QString("[%1]").arg(id));
// rx_freq
float frequencyKHZ = frequency / 1000.0f;
s.append(QString("%1").arg(frequencyKHZ, 0, 'f', 1));
// fmt
s.append(formatSpecifier(true));
// to
if (m_hasAddress)
{
if (m_formatSpecifier == GEOGRAPHIC_CALL)
{
char ns = m_addressLatitude >= 0 ? 'N' : 'S';
char ew = m_addressLongitude >= 0 ? 'E' : 'W';
int lat = abs(m_addressLatitude);
int lon = abs(m_addressLongitude);
s.append(QString("AREA %2%1%6=>%4%1 %3%1%7=>%5%1")
.arg(QChar(0xb0)) // degree
.arg(lat, 2, 10, QChar('0'))
.arg(lon, 3, 10, QChar('0'))
.arg(m_addressLatAngle, 2, 10, QChar('0'))
.arg(m_addressLonAngle, 2, 10, QChar('0'))
.arg(ns)
.arg(ew));
}
else
{
s.append(m_address);
}
}
else
{
s.append("");
}
// cat
s.append(category(true));
// from
s.append(m_selfId);
// tc1
if (m_hasTelecommand1) {
s.append(telecommand1(m_telecommand1, true));
} else {
s.append("--");
}
// tc2
if (m_hasTelecommand2) {
s.append(telecommand2(m_telecommand2, true));
} else {
s.append("--");
}
// distress fields don't appear to be used!
// freq
if (m_hasFrequency1 && m_hasFrequency2) {
s.append(QString("%1/%2KHz").arg(m_frequency1/1000.0, 7, 'f', 1, QChar('0')).arg(m_frequency2/1000.0, 7, 'f', 1, QChar('0')));
} else if (m_hasFrequency1) {
s.append(QString("%1KHz").arg(m_frequency1/1000.0, 7, 'f', 1, QChar('0')));
} else if (m_hasFrequency2) {
s.append(QString("%1KHz").arg(m_frequency2/1000.0, 7, 'f', 1, QChar('0')));
} else if (m_hasChannel1 && m_hasChannel2) {
s.append(QString("%1/%2").arg(m_channel1).arg(m_channel2));
} else if (m_hasChannel1) {
s.append(QString("%1").arg(m_channel1));
} else if (m_hasChannel2) {
s.append(QString("%1").arg(m_channel2));
} else {
s.append("--");
}
// pos
if (m_hasPosition) {
s.append(m_position); // FIXME: Format??
} else {
s.append("--"); // Sometimes this is " -- ". in YaDD Why?
}
// eos
s.append(endOfSignal(m_eos, true));
// ecc
s.append(QString("ECC %1 %2").arg(m_calculatedECC).arg(m_eccOk ? "OK" : "ERR"));
return s.join(";");
}
QString DSCMessage::formatSpecifier(bool shortString) const
{
if (shortString)
{
if (m_formatSpecifierShortStrings.contains(m_formatSpecifier)) {
return m_formatSpecifierShortStrings[m_formatSpecifier];
} else {
return QString("UNK/ERR").arg(m_formatSpecifier);
}
}
else
{
if (m_formatSpecifierStrings.contains(m_formatSpecifier)) {
return m_formatSpecifierStrings[m_formatSpecifier];
} else {
return QString("Unknown (%1)").arg(m_formatSpecifier);
}
}
}
QString DSCMessage::category(bool shortString) const
{
if (shortString)
{
if (m_categoryShortStrings.contains(m_category)) {
return m_categoryShortStrings[m_category];
} else {
return QString("UNK/ERR").arg(m_category);
}
}
else
{
if (!m_hasCategory) {
return "N/A";
} else if (m_categoryStrings.contains(m_category)) {
return m_categoryStrings[m_category];
} else {
return QString("Unknown (%1)").arg(m_category);
}
}
}
QString DSCMessage::telecommand1(FirstTelecommand telecommand, bool shortString)
{
if (shortString)
{
if (m_telecommand1ShortStrings.contains(telecommand)) {
return m_telecommand1ShortStrings[telecommand];
} else {
return QString("UNK/ERR").arg(telecommand);
}
}
else
{
if (m_telecommand1Strings.contains(telecommand)) {
return m_telecommand1Strings[telecommand];
} else {
return QString("Unknown (%1)").arg(telecommand);
}
}
}
QString DSCMessage::telecommand2(SecondTelecommand telecommand, bool shortString)
{
if (shortString)
{
if (m_telecommand2ShortStrings.contains(telecommand)) {
return m_telecommand2ShortStrings[telecommand];
} else {
return QString("UNK/ERR").arg(telecommand);
}
}
else
{
if (m_telecommand2Strings.contains(telecommand)) {
return m_telecommand2Strings[telecommand];
} else {
return QString("Unknown (%1)").arg(telecommand);
}
}
}
QString DSCMessage::distressNature(DistressNature nature)
{
if (m_distressNatureStrings.contains(nature)) {
return m_distressNatureStrings[nature];
} else {
return QString("Unknown (%1)").arg(nature);
}
}
QString DSCMessage::endOfSignal(EndOfSignal eos, bool shortString)
{
if (shortString)
{
if (m_endOfSignalShortStrings.contains(eos)) {
return m_endOfSignalShortStrings[eos];
} else {
return QString("UNK/ERR").arg(eos);
}
}
else
{
if (m_endOfSignalStrings.contains(eos)) {
return m_endOfSignalStrings[eos];
} else {
return QString("Unknown (%1)").arg(eos);
}
}
}
QString DSCMessage::symbolsToDigits(const QByteArray data, int startIdx, int length)
{
QString s;
for (int i = 0; i < length; i++)
{
QString digits = QString("%1").arg((int)data[startIdx+i], 2, 10, QChar('0'));
s = s.append(digits);
}
return s;
}
QString DSCMessage::formatCoordinates(int latitude, int longitude)
{
QString lat, lon;
if (latitude >= 0) {
lat = QString("%1%2N").arg(latitude).arg(QChar(0xb0));
} else {
lat = QString("%1%2S").arg(-latitude).arg(QChar(0xb0));
}
if (longitude >= 0) {
lon = QString("%1%2E").arg(longitude).arg(QChar(0xb0));
} else {
lon = QString("%1%2W").arg(-longitude).arg(QChar(0xb0));
}
return QString("%1 %2").arg(lat).arg(lon);
}
void DSCMessage::decode(const QByteArray& data)
{
int idx = 0;
// Format specifier
m_formatSpecifier = (FormatSpecifier) data[idx++];
m_formatSpecifierMatch = m_formatSpecifier == data[idx++];
// Address and category
if (m_formatSpecifier != DISTRESS_ALERT)
{
if (m_formatSpecifier != ALL_SHIPS)
{
m_address = symbolsToDigits(data, idx, 5);
idx += 5;
m_hasAddress = true;
if (m_formatSpecifier == SELECTIVE_CALL)
{
m_address = formatAddress(m_address);
}
else if (m_formatSpecifier == GEOGRAPHIC_CALL)
{
// Address definines a geographic rectangle. We have NW coord + 2 angles
QChar azimuthSector = m_address[0];
m_addressLatitude = m_address[1].digitValue() * 10 + m_address[2].digitValue(); // In degrees
m_addressLongitude = m_address[3].digitValue() * 100 + m_address[4].digitValue() * 10 + m_address[5].digitValue(); // In degrees
switch (azimuthSector.toLatin1())
{
case '0': // NE
break;
case '1': // NW
m_addressLongitude = -m_addressLongitude;
break;
case '2': // SE
m_addressLatitude = -m_addressLatitude;
break;
case '3': // SW
m_addressLongitude = -m_addressLongitude;
m_addressLatitude = -m_addressLatitude;
break;
default:
break;
}
m_addressLatAngle = m_address[6].digitValue() * 10 + m_address[7].digitValue();
m_addressLonAngle = m_address[8].digitValue() * 10 + m_address[9].digitValue();
int latitude2 = m_addressLatitude + m_addressLatAngle;
int longitude2 = m_addressLongitude + m_addressLonAngle;
/*m_address = QString("Lat %2%1 Lon %3%1 %4%5%6%1 %4%7%8%1")
.arg(QChar(0xb0)) // degree
.arg(m_addressLatitude)
.arg(m_addressLongitude)
.arg(QChar(0x0394)) // delta
.arg(QChar(0x03C6)) // phi
.arg(m_addressLatAngle)
.arg(QChar(0x03BB)) // lambda
.arg(m_addressLonAngle);*/
m_address = QString("%1 - %2")
.arg(formatCoordinates(m_addressLatitude, m_addressLongitude))
.arg(formatCoordinates(latitude2, longitude2));
}
}
else
{
m_hasAddress = false;
}
m_category = (Category) data[idx++];
m_hasCategory = true;
}
else
{
m_hasAddress = false;
m_hasCategory = true;
}
// Self Id
m_selfId = symbolsToDigits(data, idx, 5);
m_selfId = formatAddress(m_selfId);
idx += 5;
// Telecommands
if (m_formatSpecifier != DISTRESS_ALERT)
{
m_telecommand1 = (FirstTelecommand) data[idx++];
m_hasTelecommand1 = true;
if (m_category != DISTRESS) // Not Distress Alert Ack / Relay
{
m_telecommand2 = (SecondTelecommand) data[idx++];
m_hasTelecommand2 = true;
}
else
{
m_hasTelecommand2 = false;
}
}
else
{
m_hasTelecommand1 = false;
m_hasTelecommand2 = false;
}
// ID of source of distress for relays and acks
if (m_hasCategory && m_category == DISTRESS)
{
m_distressId = symbolsToDigits(data, idx, 5);
m_distressId = formatAddress(m_distressId);
idx += 5;
m_hasDistressId = true;
}
else
{
m_hasDistressId = false;
}
if (m_formatSpecifier == DISTRESS_ALERT)
{
m_distressNature = (DistressNature) data[idx++];
m_position = formatCoordinates(symbolsToDigits(data, idx, 5));
idx += 5;
m_hasDistressNature = true;
m_hasPosition = true;
m_hasFrequency1 = false;
m_hasChannel1 = false;
m_hasFrequency2 = false;
m_hasChannel2 = false;
}
else if (m_hasCategory && (m_category != DISTRESS))
{
m_hasDistressNature = false;
// Frequency or position
if (data[idx] == 55)
{
// Position 6
m_position = formatCoordinates(symbolsToDigits(data, idx, 5));
idx += 5;
m_hasPosition = true;
m_hasFrequency1 = false;
m_hasChannel1 = false;
m_hasFrequency2 = false;
m_hasChannel2 = false;
}
else
{
m_hasPosition = false;
// Frequency
m_frequency1 = 0;
decodeFrequency(data, idx, m_frequency1, m_channel1);
m_hasFrequency1 = m_frequency1 != 0;
m_hasChannel1 = !m_channel1.isEmpty();
if (m_formatSpecifier != AUTOMATIC_CALL)
{
m_frequency2 = 0;
decodeFrequency(data, idx, m_frequency2, m_channel2);
m_hasFrequency2 = m_frequency2 != 0;
m_hasChannel2 = !m_channel2.isEmpty();
}
else
{
m_hasFrequency2 = false;
m_hasChannel2 = false;
}
}
}
else
{
m_hasDistressNature = false;
m_hasPosition = false;
m_hasFrequency1 = false;
m_hasChannel1 = false;
m_hasFrequency2 = false;
m_hasChannel2 = false;
}
if (m_formatSpecifier == AUTOMATIC_CALL)
{
signed char oddEven = data[idx++];
int len = data.size() - idx - 2; // EOS + ECC
m_number = symbolsToDigits(data, idx, len);
idx += len;
if (oddEven == 105) { // Is number an odd number?
m_number = m_number.mid(1); // Drop leading digit (which should be a 0)
}
m_hasNumber = true;
}
else
{
m_hasNumber = false;
}
// Time
if ( (m_formatSpecifier == DISTRESS_ALERT)
|| (m_hasCategory && (m_category == DISTRESS))
//|| (m_formatSpecifier == SELECTIVE_CALL) && (m_category == SAFETY) && (m_telecommand1 == POSITION_UPDATE) && (m_telecommand2 == 126) && (m_frequency == pos4))
)
{
// 8 8 8 8 for no time
QString time = symbolsToDigits(data, idx, 2);
if (time != "8888")
{
m_time = QTime(time.left(2).toInt(), time.right(2).toInt());
m_hasTime = true;
}
else
{
m_hasTime = false;
}
// FIXME: Convert to QTime?
}
else
{
m_hasTime = false;
}
// Subsequent communications
if ((m_formatSpecifier == DISTRESS_ALERT) || (m_hasCategory && (m_category == DISTRESS)))
{
m_subsequenceComms = (FirstTelecommand)data[idx++];
m_hasSubsequenceComms = true;
}
else
{
m_hasSubsequenceComms = false;
}
m_eos = (EndOfSignal) data[idx++];
m_ecc = data[idx++];
checkECC(data);
// Indicate message as being invalid if any unexpected data, too long, or ECC didn't match
if ( m_formatSpecifierStrings.contains(m_formatSpecifier)
&& (!m_hasCategory || (m_hasCategory && m_categoryStrings.contains(m_category)))
&& (!m_hasTelecommand1 || (m_hasTelecommand1 && m_telecommand1Strings.contains(m_telecommand1)))
&& (!m_hasTelecommand2 || (m_hasTelecommand2 && m_telecommand2Strings.contains(m_telecommand2)))
&& (!m_hasDistressNature || (m_hasDistressNature && m_distressNatureStrings.contains(m_distressNature)))
&& m_endOfSignalStrings.contains(m_eos)
&& (!data.contains(-1))
&& (data.size() < DSCDecoder::m_maxBytes)
&& m_eccOk
) {
m_valid = true;
} else {
m_valid = false;
}
}
void DSCMessage::checkECC(const QByteArray& data)
{
m_calculatedECC = 0;
// Only use one format specifier and one EOS
for (int i = 1; i < data.size() - 1; i++) {
m_calculatedECC ^= data[i];
}
m_eccOk = m_calculatedECC == m_ecc;
}
void DSCMessage::decodeFrequency(const QByteArray& data, int& idx, int& frequency, QString& channel)
{
// No frequency information is indicated by 126 repeated 3 times
if ((data[idx] == 126) && (data[idx+1] == 126) && (data[idx+2] == 126))
{
idx += 3;
return;
}
// Extract frequency digits
QString s = symbolsToDigits(data, idx, 3);
idx += 3;
if (s[0] == '4')
{
s = s.append(symbolsToDigits(data, idx, 1));
idx++;
}
if ((s[0] == '0') || (s[0] == '1') || (s[0] == '2'))
{
frequency = s.toInt() * 100;
}
else if (s[0] == '3')
{
channel = "CH" + s.mid(1); // HF/MF
}
else if (s[0] == '4')
{
frequency = s.mid(1).toInt() * 10; // Frequency in multiples of 10Hz
}
else if (s[0] == '9')
{
channel = "CH" + s.mid(2) + "VHF"; // VHF
}
}
QString DSCMessage::formatAddress(const QString &address) const
{
// First 9 digits should be MMSI
// Last digit should always be 0, except for ITU-R M.1080, which allows 10th digit to specify different equipement on same vessel
if (address.right(1) == "0") {
return address.left(9);
} else {
return QString("%1-%2").arg(address.left(9)).arg(address.right(1));
}
}
QString DSCMessage::formatCoordinates(const QString& coords)
{
if (coords == "9999999999")
{
return "Not available";
}
else
{
QChar quadrant = coords[0];
QString latitude = QString("%1%3%2\'")
.arg(coords.mid(1, 2))
.arg(coords.mid(3, 2))
.arg(QChar(0xb0));
QString longitude = QString("%1%3%2\'")
.arg(coords.mid(1, 3))
.arg(coords.mid(4, 2))
.arg(QChar(0xb0));
switch (quadrant.toLatin1())
{
case '0':
latitude = latitude.append('N');
longitude = longitude.append('E');
break;
case '1':
latitude = latitude.append('N');
longitude = longitude.append('W');
break;
case '2':
latitude = latitude.append('S');
longitude = longitude.append('E');
break;
case '3':
latitude = latitude.append('S');
longitude = longitude.append('W');
break;
}
return QString("%1 %2").arg(latitude).arg(longitude);
}
}
// Doesn't include 125 111 125 as these will have be detected already, in DSDDemodSink
const signed char DSCDecoder::m_expectedSymbols[] = {
110,
125, 109,
125, 108,
125, 107,
125, 106
};
int DSCDecoder::m_maxBytes = 40; // Max bytes in any message
void DSCDecoder::init(int offset)
{
if (offset == 0)
{
m_state = FILL_DX;
}
else
{
m_phaseIdx = offset;
m_state = PHASING;
}
m_idx = 0;
m_errors = 0;
m_bytes = QByteArray();
m_eos = false;
}
bool DSCDecoder::decodeSymbol(signed char symbol)
{
bool ret = false;
switch (m_state)
{
case PHASING:
// Check if received phasing signals are as expected
if (symbol != m_expectedSymbols[9-m_phaseIdx]) {
m_errors++;
}
m_phaseIdx--;
if (m_phaseIdx == 0) {
m_state = FILL_DX;
}
break;
case FILL_DX:
// Fill up buffer
m_buf[m_idx++] = symbol;
if (m_idx == BUFFER_SIZE)
{
m_state = RX;
m_idx = 0;
}
else
{
m_state = FILL_RX;
}
break;
case FILL_RX:
if ( ((m_idx == 1) && (symbol != 106))
|| ((m_idx == 2) && (symbol != 105))
)
{
m_errors++;
}
m_state = FILL_DX;
break;
case RX:
{
signed char a = selectSymbol(m_buf[m_idx], symbol);
if (DSCMessage::m_endOfSignalStrings.contains((DSCMessage::EndOfSignal) a)) {
m_state = DX_EOS;
} else {
m_state = DX;
}
if (m_bytes.size() > m_maxBytes)
{
ret = true;
m_state = NO_EOS;
}
}
break;
case DX:
// Save received character in buffer
m_buf[m_idx] = symbol;
m_idx = (m_idx + 1) % BUFFER_SIZE;
m_state = RX;
break;
case DX_EOS:
// Save, EOS symbol
m_buf[m_idx] = symbol;
m_idx = (m_idx + 1) % BUFFER_SIZE;
m_state = RX_EOS;
break;
case RX_EOS:
selectSymbol(m_buf[m_idx], symbol);
m_state = DONE;
ret = true;
break;
}
return ret;
}
// Reverse order of bits in a byte
unsigned char DSCDecoder::reverse(unsigned char b)
{
b = (b & 0xF0) >> 4 | (b & 0x0F) << 4;
b = (b & 0xCC) >> 2 | (b & 0x33) << 2;
b = (b & 0xAA) >> 1 | (b & 0x55) << 1;
return b;
}
// Convert 10 bits to a symbol
// Returns -1 if error detected
signed char DSCDecoder::bitsToSymbol(unsigned int bits)
{
signed char data = reverse(bits >> 3) >> 1;
int zeros = 7-popcount(data);
int expectedZeros = bits & 0x7;
if (zeros == expectedZeros) {
return data;
} else {
return -1;
}
}
// Decode 10-bits to symbols then remove errors using repeated symbols
bool DSCDecoder::decodeBits(int bits)
{
signed char symbol = bitsToSymbol(bits);
//qDebug() << "Bits2sym: " << Qt::hex << bits << Qt::hex << symbol;
return decodeSymbol(symbol);
}
// Select time diversity symbol without errors
signed char DSCDecoder::selectSymbol(signed char dx, signed char rx)
{
signed char s;
if (dx != -1)
{
s = dx; // First received character has no detectable error
if (dx != rx) {
m_errors++;
}
}
else if (rx != -1)
{
s = rx; // Second received character has no detectable error
m_errors++;
}
else
{
s = '*'; // Both received characters have errors
m_errors += 2;
}
m_bytes.append(s);
return s;
}

234
sdrbase/util/dsc.h 100644
Wyświetl plik

@ -0,0 +1,234 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2023 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_UTIL_DSC_H
#define INCLUDE_UTIL_DSC_H
#include "export.h"
#include <QByteArray>
#include <QString>
#include <QDateTime>
// Digital Select Calling
// https://www.itu.int/dms_pubrec/itu-r/rec/m/R-REC-M.493-15-201901-I!!PDF-E.pdf
class SDRBASE_API DSCDecoder {
public:
void init(int offset);
bool decodeBits(int bits);
QByteArray getMessage() const { return m_bytes; }
int getErrors() const { return m_errors; }
static int m_maxBytes;
private:
static const int BUFFER_SIZE = 3;
signed char m_buf[3];
enum State {
PHASING,
FILL_DX,
FILL_RX,
DX,
RX,
DX_EOS,
RX_EOS,
DONE,
NO_EOS
} m_state;
int m_idx;
int m_errors;
int m_phaseIdx;
bool m_eos;
static const signed char m_expectedSymbols[];
QByteArray m_bytes;
bool decodeSymbol(signed char symbol);
static signed char bitsToSymbol(unsigned int bits);
static unsigned char reverse(unsigned char b);
signed char selectSymbol(signed char dx, signed char rx);
};
class SDRBASE_API DSCMessage {
public:
enum FormatSpecifier {
GEOGRAPHIC_CALL = 102,
DISTRESS_ALERT = 112,
GROUP_CALL = 114,
ALL_SHIPS = 116,
SELECTIVE_CALL = 120,
AUTOMATIC_CALL = 123
};
enum Category {
ROUTINE = 100,
SAFETY = 108,
URGENCY = 110,
DISTRESS = 112
};
enum FirstTelecommand {
F3E_G3E_ALL_MODES_TP = 100,
F3E_G3E_DUPLEX_TP = 101,
POLLING = 103,
UNABLE_TO_COMPLY = 104,
END_OF_CALL = 105,
DATA = 106,
J3E_TP = 109,
DISTRESS_ACKNOWLEDGEMENT = 110,
DISTRESS_ALERT_RELAY = 112,
F1B_J2B_TTY_FEC = 113,
F1B_J2B_TTY_AQR = 115,
TEST = 118,
POSITION_UPDATE = 121,
NO_INFORMATION = 126
};
enum SecondTelecommand {
NO_REASON = 100,
CONGESTION = 101,
BUSY = 102,
QUEUE = 103,
BARRED = 104,
NO_OPERATOR = 105,
OPERATOR_UNAVAILABLE = 106,
EQUIPMENT_DISABLED = 107,
UNABLE_TO_USE_CHANNEL = 108,
UNABLE_TO_USE_MODE = 109,
NOT_PARTIES_TO_CONFLICT = 110,
MEDICAL_TRANSPORTS = 111,
PAY_PHONE = 112,
FAX = 113,
NO_INFORMATION_2 = 126
};
enum DistressNature {
FIRE = 100,
FLOODING = 101,
COLLISION = 102,
GROUNDING = 103,
LISTING = 104,
SINKING = 105,
ADRIFT = 106,
UNDESIGNATED = 107,
ABANDONING_SHIP = 108,
PIRACY = 109,
MAN_OVERBOARD = 110,
EPIRB = 112
};
enum EndOfSignal {
REQ = 117,
ACK = 122,
EOS = 127
};
static QMap<FormatSpecifier, QString> m_formatSpecifierStrings;
static QMap<FormatSpecifier, QString> m_formatSpecifierShortStrings;
static QMap<Category, QString> m_categoryStrings;
static QMap<Category, QString> m_categoryShortStrings;
static QMap<FirstTelecommand, QString> m_telecommand1Strings;
static QMap<FirstTelecommand, QString> m_telecommand1ShortStrings;
static QMap<SecondTelecommand, QString> m_telecommand2Strings;
static QMap<SecondTelecommand, QString> m_telecommand2ShortStrings;
static QMap<DistressNature, QString> m_distressNatureStrings;
static QMap<EndOfSignal, QString> m_endOfSignalStrings;
static QMap<EndOfSignal, QString> m_endOfSignalShortStrings;
FormatSpecifier m_formatSpecifier;
bool m_formatSpecifierMatch;
QString m_address;
bool m_hasAddress;
int m_addressLatitude; // For GEOGRAPHIC_CALL
int m_addressLongitude;
int m_addressLatAngle;
int m_addressLonAngle;
Category m_category;
bool m_hasCategory;
QString m_selfId;
FirstTelecommand m_telecommand1;
bool m_hasTelecommand1;
SecondTelecommand m_telecommand2;
bool m_hasTelecommand2;
QString m_distressId;
bool m_hasDistressId;
DistressNature m_distressNature;
bool m_hasDistressNature;
QString m_position;
bool m_hasPosition;
int m_frequency1; // Rx
bool m_hasFrequency1;
QString m_channel1;
bool m_hasChannel1;
int m_frequency2; // Tx
bool m_hasFrequency2;
QString m_channel2;
bool m_hasChannel2;
QString m_number; // Phone number
bool m_hasNumber;
QTime m_time;
bool m_hasTime;
FirstTelecommand m_subsequenceComms;
bool m_hasSubsequenceComms;
EndOfSignal m_eos;
signed char m_ecc; // Error checking code (parity)
signed char m_calculatedECC;
bool m_eccOk;
bool m_valid; // Data is within defined values
QDateTime m_dateTime; // Date/time when received
QByteArray m_data;
DSCMessage(const QByteArray& data, QDateTime dateTime);
QString toString(const QString separator = " ") const;
QString toYaddNetFormat(const QString& id, qint64 frequency) const;
QString formatSpecifier(bool shortString=false) const;
QString category(bool shortString=false) const;
static QString telecommand1(FirstTelecommand telecommand, bool shortString=false);
static QString telecommand2(SecondTelecommand telecommand, bool shortString=false);
static QString distressNature(DistressNature nature);
static QString endOfSignal(EndOfSignal eos, bool shortString=false);
protected:
QString symbolsToDigits(const QByteArray data, int startIdx, int length);
QString formatCoordinates(int latitude, int longitude);
void decode(const QByteArray& data);
void checkECC(const QByteArray& data);
void decodeFrequency(const QByteArray& data, int& idx, int& frequency, QString& channel);
QString formatAddress(const QString &address) const;
QString formatCoordinates(const QString& coords);
};
#endif /* INCLUDE_UTIL_DSC_H */

Wyświetl plik

@ -0,0 +1,421 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2023 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 <QResource>
#include <QDebug>
#include "util/mmsi.h"
#include "util/osndb.h"
// https://www.itu.int/en/ITU-R/terrestrial/fmd/Pages/mid.aspx
// Names used match up with names used for flags in ADS-B directory
QMap<int, QString> MMSI::m_mid = {
{201, "albania"},
{202, "andorra"},
{203, "austria"},
{204, "portugal"},
{205, "belgium"},
{206, "belarus"},
{207, "bulgaria"},
{208, "vatican_city"},
{209, "cyprus"},
{210, "cyprus"},
{211, "germany"},
{212, "cyprus"},
{213, "georgia"},
{214, "moldova"},
{215, "malta"},
{216, "armenia"},
{218, "germany"},
{219, "denmark"},
{220, "denmark"},
{224, "spain"},
{225, "spain"},
{226, "france"},
{227, "france"},
{228, "france"},
{229, "malta"},
{230, "finland"},
{231, "denmark"},
{232, "united_kingdom"},
{233, "united_kingdom"},
{234, "united_kingdom"},
{235, "united_kingdom"},
{236, "united_kingdom"},
{237, "greece"},
{238, "croatia"},
{239, "greece"},
{240, "greece"},
{241, "greece"},
{242, "morocco"},
{243, "hungary"},
{244, "netherlands"},
{245, "netherlands"},
{246, "netherlands"},
{247, "italy"},
{248, "malta"},
{249, "malta"},
{250, "ireland"},
{251, "iceland"},
{252, "liechtenstein"},
{253, "luxembourg"},
{254, "monaco"},
{255, "portugal"},
{256, "malta"},
{257, "norway"},
{258, "norway"},
{259, "norway"},
{261, "poland"},
{262, "montenegro"},
{263, "portugal"},
{264, "romania"},
{265, "sweden"},
{266, "sweden"},
{267, "slovakia"},
{268, "san_marino"},
{269, "switzerland"},
{270, "czech_republic"},
{271, "turkey"},
{272, "ukraine"},
{273, "russia"},
{274, "macedonia"},
{275, "latvia"},
{276, "estonia"},
{277, "slovenia"},
{279, "serbia"},
{301, "united_kingdom"},
{303, "united_states"},
{304, "antigua_and_barbuda"},
{305, "antigua_and_barbuda"},
{306, "netherlands"},
{307, "netherlands"},
{308, "bahamas"},
{309, "bahamas"},
{310, "bermuda"},
{311, "bahamas"},
{312, "belize"},
{314, "barbados"},
{316, "canada"},
{319, "cayman_isles"},
{321, "costa_rica"},
{323, "cuba"},
{325, "dominica"},
{327, "dominican_republic"},
{329, "france"},
{330, "grenada"},
{331, "denmark"}, // greenland
{332, "guatemala"},
{334, "honduras"},
{336, "haiti"},
{338, "united_states"},
{339, "jamaica"},
{341, "st_kitts_and_nevis"},
{343, "st_lucia"},
{345, "mexico"},
{347, "france"}, // martinique
{348, "united_kingdom"}, // montserrat
{350, "nicaragua"},
{351, "panama"},
{352, "panama"},
{353, "panama"},
{354, "panama"},
{355, "panama"},
{356, "panama"},
{357, "panama"},
{358, "united_states"}, // puerto_rico
{359, "el_salvador"},
{361, "france"},
{362, "trinidad_and_tobago"},
{364, "turks_and_caicos"},
{366, "united_states"},
{367, "united_states"},
{368, "united_states"},
{369, "united_states"},
{370, "panama"},
{371, "panama"},
{372, "panama"},
{373, "panama"},
{374, "panama"},
{375, "st_vincent"},
{376, "st_vincent"},
{377, "st_vincent"},
{378, "virgin_isles"},
{401, "afghanistan"},
{403, "saudi_arabia"},
{405, "bangladesh"},
{408, "bahrain"},
{410, "bhutan"},
{412, "china"},
{413, "china"},
{414, "china"},
{416, "taiwan"},
{417, "sri_lanka"},
{419, "india"},
{422, "iran"},
{423, "azerbaijan"},
{425, "iraq"},
{428, "israel"},
{431, "japan"},
{432, "japan"},
{434, "turkmenistan"},
{436, "kazakhstan"},
{437, "uzbekistan"},
{438, "jordan"},
{440, "korea_south"},
{441, "korea_south"},
{443, "palestine"},
{445, "korea_north"},
{447, "kuwait"},
{450, "lebanon"},
{451, "kyrgyzstan"},
{453, "china"}, // macao
{455, "maldives"},
{457, "mongolia"},
{459, "nepal"},
{461, "oman"},
{463, "pakistan"},
{466, "qatar"},
{468, "syria"},
{470, "united_arab_emirates"},
{471, "united_arab_emirates"},
{472, "tajikistan"},
{473, "yemen"},
{474, "yemen"},
{477, "hong_kong"},
{478, "bosnia"},
{501, "france"},
{503, "australia"},
{506, "myanmar"},
{508, "brunei"},
{510, "micronesia"},
{511, "palau"},
{512, "new_zealand"},
{514, "cambodia"},
{515, "cambodia"},
{516, "australia"},
{518, "cook_islands"},
{520, "fiji"},
{523, "australia"},
{525, "indonesia"},
{529, "kiribati"},
{531, "laos"},
{533, "malaysia"},
{536, "united_states"},
{538, "marshall islands"},
{540, "france"},
{542, "new_zealand"},
{544, "nauru"},
{546, "france"},
{548, "philippines"},
{550, "timorleste"},
{553, "papua_new_guinea"},
{555, "united_kingdom"},
{557, "solomon_islands"},
{559, "united_states"}, // american_samoa
{561, "samoa"},
{563, "singapore"},
{564, "singapore"},
{565, "singapore"},
{566, "singapore"},
{567, "thailand"},
{570, "tonga"},
{572, "tuvalu"},
{574, "vietnam"},
{576, "vanuatu"},
{577, "vanuatu"},
{578, "france"},
{601, "south_africa"},
{603, "angola"},
{605, "algeria"},
{607, "france"},
{608, "united_kingdom"}, // ascension_island
{609, "burundi"},
{610, "benin"},
{611, "botswana"},
{612, "central_african_republic"},
{613, "cameroun"}, // cameroon
{615, "congoroc"},
{616, "comoros"},
{617, "cape_verde"},
{618, "france"},
{619, "ivory_coast"},
{620, "comoros"},
{621, "djibouti"},
{622, "egypt"},
{624, "ethiopia"},
{625, "eritrea"},
{626, "gabon"},
{627, "ghana"},
{629, "gambia"},
{630, "guinea_bissau"},
{631, "equatorial_guinea"},
{632, "guinea"},
{633, "burkina_faso"},
{634, "kenya"},
{635, "france"},
{636, "liberia"},
{637, "liberia"},
{638, "south_sudan"},
{642, "libya"},
{644, "lesotho"},
{645, "mauritius"},
{647, "madagascar"},
{649, "mali"},
{650, "mozambique"},
{654, "mauritania"},
{655, "malawi"},
{656, "niger"},
{657, "nigeria"},
{659, "namibia"},
{660, "france"}, // reunion
{661, "rwanda"},
{662, "sudan"},
{663, "senegal"},
{664, "seychelles"},
{665, "united_kingdom"}, // saint_helena
{666, "somalia"},
{667, "sierra_leone"},
{668, "sao_tome_principe"},
{669, "swaziland"}, // eswatini
{670, "chad"},
{671, "togo"},
{672, "tunisia"},
{674, "tanzania"},
{675, "uganda"},
{676, "congodrc"},
{677, "tanzania"},
{678, "zambia"},
{679, "zimbabwe"},
{701, "argentina"},
{710, "brazil"},
{720, "bolivia"},
{725, "chile"},
{730, "colombia"},
{735, "ecuador"},
{740, "falkland_isles"},
{745, "france"}, // guiana
{750, "guyana"},
{755, "paraguay"},
{760, "peru"},
{765, "suriname"},
{770, "uruguay"},
{775, "venezuela"},
};
QString MMSI::getMID(const QString &mmsi)
{
if (mmsi.startsWith("00") || mmsi.startsWith("99") || mmsi.startsWith("98")) {
return mmsi.mid(2, 3);
} else if (mmsi.startsWith("0") || mmsi.startsWith("8")) {
return mmsi.mid(1, 3);
} else if (mmsi.startsWith("111")) {
return mmsi.mid(3, 3);
} else {
return mmsi.left(3);
}
}
QString MMSI::getCountry(const QString &mmsi)
{
return m_mid[MMSI::getMID(mmsi).toInt()];
}
void MMSI::checkFlags()
{
// Loop through all MIDs and check to see if we have a flag icon
for (auto id : m_mid.keys())
{
QString country = m_mid.value(id);
QString path = QString(":/flags/%1.bmp").arg(country);
QResource res(path);
if (!res.isValid()) {
qDebug() << "MMSI::checkFlags: Resource invalid " << path;
}
}
}
QIcon *MMSI::getFlagIcon(const QString &mmsi)
{
QString country = getCountry(mmsi);
return AircraftInformation::getFlagIcon(country);
}
QString MMSI::getFlagIconURL(const QString &mmsi)
{
QString country = getCountry(mmsi);
return AircraftInformation::getFlagIconURL(country);
}
QString MMSI::getCategory(const QString &mmsi)
{
switch (mmsi[0].toLatin1())
{
case '0':
if (mmsi.startsWith("00")) {
return "Coast";
} else {
return "Group"; // Group of ships
}
case '1':
// Search and rescue
if (mmsi[6] == '1') {
return "SAR Aircraft";
} else if (mmsi[6] == '5') {
return "SAR Helicopter";
} else {
return "SAR";
}
case '8':
return "Handheld";
case '9':
if (mmsi.startsWith("970"))
{
return "SAR";
}
else if (mmsi.startsWith("972"))
{
return "Man overboard";
}
else if (mmsi.startsWith("974"))
{
return "EPIRB"; // Emergency Becaon
}
else if (mmsi.startsWith("979"))
{
return "AMRD"; // Autonomous
}
else if (mmsi.startsWith("98"))
{
return "Craft with parent ship";
}
else if (mmsi.startsWith("99"))
{
if (mmsi[5] == '1') {
return "Physical AtoN";
} else if (mmsi[5] == '6') {
return "Virtual AtoN";
} else if (mmsi[5] == '8') {
return "Mobile AtoN";
} else {
return "AtoN"; // Aid-to-navigation
}
}
default:
return "Ship"; // Vessel better?
}
return "Unknown";
}

Wyświetl plik

@ -0,0 +1,47 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2023 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 UTIL_MMSI_H
#define UTIL_MMSI_H
#include <QMap>
#include <QIcon>
#include "export.h"
// Maritime mobile service identities (basically ship identifiers)
// MMSIs defined by ITU-R M.585
// https://www.itu.int/dms_pubrec/itu-r/rec/m/R-REC-M.585-9-202205-I!!PDF-E.pdf
class SDRBASE_API MMSI {
public:
static QString getMID(const QString &mmsi);
static QString getCountry(const QString &mmsi);
static QString getCategory(const QString &mmsi);
static QIcon *getFlagIcon(const QString &mmsi);
static QString getFlagIconURL(const QString &mmsi);
private:
static QMap<int, QString> m_mid;
static void checkFlags();
};
#endif /* UTIL_MMSI_H */

Wyświetl plik

@ -120,6 +120,7 @@ class MovingAverageUtilVar
double asDouble() const { return m_total * m_samplesSizeInvD; }
float asFloat() const { return m_total * m_samplesSizeInvF; }
operator T() const { return m_total / m_samples.size(); }
T instantAverage() const { return m_total / (m_num_samples == 0 ? 1 : m_num_samples); }
private:
std::vector<T> m_samples;

Wyświetl plik

@ -527,6 +527,7 @@ QIcon *AircraftInformation::getAirlineIcon(const QString &operatorICAO)
return icon;
}
}
QString AircraftInformation::getFlagIconPath(const QString &country)
{
QString endPath = QString("/flags/%1.bmp").arg(country);
@ -550,6 +551,15 @@ QString AircraftInformation::getFlagIconPath(const QString &country)
return QString();
}
QString AircraftInformation::getFlagIconURL(const QString &country)
{
QString path = getFlagIconPath(country);
if (path.startsWith(':')) {
path = "qrc://" + path.mid(1);
}
return path;
}
QIcon *AircraftInformation::getFlagIcon(const QString &country)
{
if (m_flagIcons.contains(country))

Wyświetl plik

@ -69,6 +69,7 @@ struct SDRBASE_API AircraftInformation {
static QIcon *getAirlineIcon(const QString &operatorICAO);
static QString getFlagIconPath(const QString &country);
static QString getFlagIconURL(const QString &country);
// Try to find an flag logo based on a country
static QIcon *getFlagIcon(const QString &country);

Wyświetl plik

@ -4493,6 +4493,11 @@ bool WebAPIRequestMapper::getChannelSettings(
channelSettings->setDoa2Settings(new SWGSDRangel::SWGDOA2Settings());
channelSettings->getDoa2Settings()->fromJsonObject(settingsJsonObject);
}
else if (channelSettingsKey == "DSCDemodSettings")
{
channelSettings->setDscDemodSettings(new SWGSDRangel::SWGDSCDemodSettings());
channelSettings->getDscDemodSettings()->fromJsonObject(settingsJsonObject);
}
else if (channelSettingsKey == "DSDDemodSettings")
{
channelSettings->setDsdDemodSettings(new SWGSDRangel::SWGDSDDemodSettings());

Wyświetl plik

@ -41,6 +41,7 @@ const QMap<QString, QString> WebAPIUtils::m_channelURIToSettingsKey = {
{"sdrangel.channel.demoddatv", "DATVDemodSettings"},
{"sdrangel.channel.dabdemod", "DABDemodSettings"},
{"sdrangel.channel.doa2", "DOA2Settings"},
{"sdrangel.channel.dscdemod", "DSCDemodSettings"},
{"sdrangel.channel.dsddemod", "DSDDemodSettings"},
{"sdrangel.channel.filesink", "FileSinkSettings"},
{"sdrangel.channeltx.filesource", "FileSourceSettings"},
@ -155,6 +156,7 @@ const QMap<QString, QString> WebAPIUtils::m_channelTypeToSettingsKey = {
{"DATVMod", "DATVModSettings"},
{"DABDemod", "DABDemodSettings"},
{"DOA2", "DOA2Settings"},
{"DSCDemod", "DSCDemodSettings"},
{"DSDDemod", "DSDDemodSettings"},
{"FileSink", "FileSinkSettings"},
{"FileSource", "FileSourceSettings"},

Wyświetl plik

@ -44,6 +44,7 @@ set(sdrgui_SOURCES
gui/fftwisdomdialog.cpp
gui/flowlayout.cpp
gui/framelesswindowresizer.cpp
gui/frequencydelegate.cpp
gui/glscope.cpp
gui/glscopegui.cpp
gui/glshadercolors.cpp
@ -157,6 +158,7 @@ set(sdrgui_HEADERS
gui/fftwisdomdialog.h
gui/flowlayout.h
gui/framelesswindowresizer.h
gui/frequencydelegate.h
gui/glscope.h
gui/glscopegui.h
gui/glshadercolors.h

Wyświetl plik

@ -0,0 +1,58 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2023 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 "frequencydelegate.h"
FrequencyDelegate::FrequencyDelegate(QString units, int precision, bool group) :
m_units(units),
m_precision(precision),
m_group(group)
{
}
QString FrequencyDelegate::displayText(const QVariant &value, const QLocale &locale) const
{
bool ok;
qlonglong v = value.toLongLong(&ok);
if (ok)
{
double d;
if (m_units == "GHz") {
d = v / 1000000000.0;
} else if (m_units == "MHz") {
d = v / 1000000.0;
} else if (m_units == "kHz") {
d = v / 1000.0;
} else {
d = v;
}
QLocale l(locale);
if (m_group) {
l.setNumberOptions(l.numberOptions() & ~QLocale::OmitGroupSeparator);
} else {
l.setNumberOptions(l.numberOptions() | QLocale::OmitGroupSeparator);
}
QString s = l.toString(d, 'f', m_precision);
return QString("%1 %2").arg(s).arg(m_units);
}
else
{
return value.toString();
}
}

Wyświetl plik

@ -0,0 +1,39 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2023 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 SDRGUI_GUI_FREQUENCYDELGATE_H
#define SDRGUI_GUI_FREQUENCYDELGATE_H
#include <QStyledItemDelegate>
#include "export.h"
// Delegate for table to display frequency
class SDRGUI_API FrequencyDelegate : public QStyledItemDelegate {
public:
FrequencyDelegate(QString units = "kHz", int precision=1, bool group=true);
virtual QString displayText(const QVariant &value, const QLocale &locale) const override;
private:
QString m_units;
int m_precision;
bool m_group;
};
#endif // SDRGUI_GUI_FREQUENCYDELGATE_H

Wyświetl plik

@ -35,6 +35,8 @@ ChannelReport:
$ref: "http://swgserver:8081/api/swagger/include/DATVMod.yaml#/DATVModReport"
DOA2Report:
$ref: "http://swgserver:8081/api/swagger/include/DOA2.yaml#/DOA2Report"
DSCDemodReport:
$ref: "http://swgserver:8081/api/swagger/include/DSCDemod.yaml#/DSCDemodReport"
DSDDemodReport:
$ref: "http://swgserver:8081/api/swagger/include/DSDDemod.yaml#/DSDDemodReport"
IEEE_802_15_4_ModReport:

Wyświetl plik

@ -51,6 +51,8 @@ ChannelSettings:
$ref: "http://swgserver:8081/api/swagger/include/DABDemod.yaml#/DABDemodSettings"
DOA2Settings:
$ref: "http://swgserver:8081/api/swagger/include/DOA2.yaml#/DOA2Settings"
DSCDemodSettings:
$ref: "http://swgserver:8081/api/swagger/include/DSCDemod.yaml#/DSCDemodSettings"
DSDDemodSettings:
$ref: "http://swgserver:8081/api/swagger/include/DSDDemod.yaml#/DSDDemodSettings"
FileSinkSettings:

Wyświetl plik

@ -56,6 +56,8 @@ SWGChannelReport::SWGChannelReport() {
m_datv_mod_report_isSet = false;
doa2_report = nullptr;
m_doa2_report_isSet = false;
dsc_demod_report = nullptr;
m_dsc_demod_report_isSet = false;
dsd_demod_report = nullptr;
m_dsd_demod_report_isSet = false;
ieee_802_15_4_mod_report = nullptr;
@ -156,6 +158,8 @@ SWGChannelReport::init() {
m_datv_mod_report_isSet = false;
doa2_report = new SWGDOA2Report();
m_doa2_report_isSet = false;
dsc_demod_report = new SWGDSCDemodReport();
m_dsc_demod_report_isSet = false;
dsd_demod_report = new SWGDSDDemodReport();
m_dsd_demod_report_isSet = false;
ieee_802_15_4_mod_report = new SWGIEEE_802_15_4_ModReport();
@ -264,6 +268,9 @@ SWGChannelReport::cleanup() {
if(doa2_report != nullptr) {
delete doa2_report;
}
if(dsc_demod_report != nullptr) {
delete dsc_demod_report;
}
if(dsd_demod_report != nullptr) {
delete dsd_demod_report;
}
@ -401,6 +408,8 @@ SWGChannelReport::fromJsonObject(QJsonObject &pJson) {
::SWGSDRangel::setValue(&doa2_report, pJson["DOA2Report"], "SWGDOA2Report", "SWGDOA2Report");
::SWGSDRangel::setValue(&dsc_demod_report, pJson["DSCDemodReport"], "SWGDSCDemodReport", "SWGDSCDemodReport");
::SWGSDRangel::setValue(&dsd_demod_report, pJson["DSDDemodReport"], "SWGDSDDemodReport", "SWGDSDDemodReport");
::SWGSDRangel::setValue(&ieee_802_15_4_mod_report, pJson["IEEE_802_15_4_ModReport"], "SWGIEEE_802_15_4_ModReport", "SWGIEEE_802_15_4_ModReport");
@ -523,6 +532,9 @@ SWGChannelReport::asJsonObject() {
if((doa2_report != nullptr) && (doa2_report->isSet())){
toJsonValue(QString("DOA2Report"), doa2_report, obj, QString("SWGDOA2Report"));
}
if((dsc_demod_report != nullptr) && (dsc_demod_report->isSet())){
toJsonValue(QString("DSCDemodReport"), dsc_demod_report, obj, QString("SWGDSCDemodReport"));
}
if((dsd_demod_report != nullptr) && (dsd_demod_report->isSet())){
toJsonValue(QString("DSDDemodReport"), dsd_demod_report, obj, QString("SWGDSDDemodReport"));
}
@ -763,6 +775,16 @@ SWGChannelReport::setDoa2Report(SWGDOA2Report* doa2_report) {
this->m_doa2_report_isSet = true;
}
SWGDSCDemodReport*
SWGChannelReport::getDscDemodReport() {
return dsc_demod_report;
}
void
SWGChannelReport::setDscDemodReport(SWGDSCDemodReport* dsc_demod_report) {
this->dsc_demod_report = dsc_demod_report;
this->m_dsc_demod_report_isSet = true;
}
SWGDSDDemodReport*
SWGChannelReport::getDsdDemodReport() {
return dsd_demod_report;
@ -1130,6 +1152,9 @@ SWGChannelReport::isSet(){
if(doa2_report && doa2_report->isSet()){
isObjectUpdated = true; break;
}
if(dsc_demod_report && dsc_demod_report->isSet()){
isObjectUpdated = true; break;
}
if(dsd_demod_report && dsd_demod_report->isSet()){
isObjectUpdated = true; break;
}

Wyświetl plik

@ -34,6 +34,7 @@
#include "SWGDATVDemodReport.h"
#include "SWGDATVModReport.h"
#include "SWGDOA2Report.h"
#include "SWGDSCDemodReport.h"
#include "SWGDSDDemodReport.h"
#include "SWGFT8DemodReport.h"
#include "SWGFileSinkReport.h"
@ -128,6 +129,9 @@ public:
SWGDOA2Report* getDoa2Report();
void setDoa2Report(SWGDOA2Report* doa2_report);
SWGDSCDemodReport* getDscDemodReport();
void setDscDemodReport(SWGDSCDemodReport* dsc_demod_report);
SWGDSDDemodReport* getDsdDemodReport();
void setDsdDemodReport(SWGDSDDemodReport* dsd_demod_report);
@ -270,6 +274,9 @@ private:
SWGDOA2Report* doa2_report;
bool m_doa2_report_isSet;
SWGDSCDemodReport* dsc_demod_report;
bool m_dsc_demod_report_isSet;
SWGDSDDemodReport* dsd_demod_report;
bool m_dsd_demod_report_isSet;

Wyświetl plik

@ -70,6 +70,8 @@ SWGChannelSettings::SWGChannelSettings() {
m_dab_demod_settings_isSet = false;
doa2_settings = nullptr;
m_doa2_settings_isSet = false;
dsc_demod_settings = nullptr;
m_dsc_demod_settings_isSet = false;
dsd_demod_settings = nullptr;
m_dsd_demod_settings_isSet = false;
file_sink_settings = nullptr;
@ -194,6 +196,8 @@ SWGChannelSettings::init() {
m_dab_demod_settings_isSet = false;
doa2_settings = new SWGDOA2Settings();
m_doa2_settings_isSet = false;
dsc_demod_settings = new SWGDSCDemodSettings();
m_dsc_demod_settings_isSet = false;
dsd_demod_settings = new SWGDSDDemodSettings();
m_dsd_demod_settings_isSet = false;
file_sink_settings = new SWGFileSinkSettings();
@ -329,6 +333,9 @@ SWGChannelSettings::cleanup() {
if(doa2_settings != nullptr) {
delete doa2_settings;
}
if(dsc_demod_settings != nullptr) {
delete dsc_demod_settings;
}
if(dsd_demod_settings != nullptr) {
delete dsd_demod_settings;
}
@ -495,6 +502,8 @@ SWGChannelSettings::fromJsonObject(QJsonObject &pJson) {
::SWGSDRangel::setValue(&doa2_settings, pJson["DOA2Settings"], "SWGDOA2Settings", "SWGDOA2Settings");
::SWGSDRangel::setValue(&dsc_demod_settings, pJson["DSCDemodSettings"], "SWGDSCDemodSettings", "SWGDSCDemodSettings");
::SWGSDRangel::setValue(&dsd_demod_settings, pJson["DSDDemodSettings"], "SWGDSDDemodSettings", "SWGDSDDemodSettings");
::SWGSDRangel::setValue(&file_sink_settings, pJson["FileSinkSettings"], "SWGFileSinkSettings", "SWGFileSinkSettings");
@ -648,6 +657,9 @@ SWGChannelSettings::asJsonObject() {
if((doa2_settings != nullptr) && (doa2_settings->isSet())){
toJsonValue(QString("DOA2Settings"), doa2_settings, obj, QString("SWGDOA2Settings"));
}
if((dsc_demod_settings != nullptr) && (dsc_demod_settings->isSet())){
toJsonValue(QString("DSCDemodSettings"), dsc_demod_settings, obj, QString("SWGDSCDemodSettings"));
}
if((dsd_demod_settings != nullptr) && (dsd_demod_settings->isSet())){
toJsonValue(QString("DSDDemodSettings"), dsd_demod_settings, obj, QString("SWGDSDDemodSettings"));
}
@ -973,6 +985,16 @@ SWGChannelSettings::setDoa2Settings(SWGDOA2Settings* doa2_settings) {
this->m_doa2_settings_isSet = true;
}
SWGDSCDemodSettings*
SWGChannelSettings::getDscDemodSettings() {
return dsc_demod_settings;
}
void
SWGChannelSettings::setDscDemodSettings(SWGDSCDemodSettings* dsc_demod_settings) {
this->dsc_demod_settings = dsc_demod_settings;
this->m_dsc_demod_settings_isSet = true;
}
SWGDSDDemodSettings*
SWGChannelSettings::getDsdDemodSettings() {
return dsd_demod_settings;
@ -1411,6 +1433,9 @@ SWGChannelSettings::isSet(){
if(doa2_settings && doa2_settings->isSet()){
isObjectUpdated = true; break;
}
if(dsc_demod_settings && dsc_demod_settings->isSet()){
isObjectUpdated = true; break;
}
if(dsd_demod_settings && dsd_demod_settings->isSet()){
isObjectUpdated = true; break;
}

Wyświetl plik

@ -39,6 +39,7 @@
#include "SWGDATVDemodSettings.h"
#include "SWGDATVModSettings.h"
#include "SWGDOA2Settings.h"
#include "SWGDSCDemodSettings.h"
#include "SWGDSDDemodSettings.h"
#include "SWGFT8DemodSettings.h"
#include "SWGFileSinkSettings.h"
@ -159,6 +160,9 @@ public:
SWGDOA2Settings* getDoa2Settings();
void setDoa2Settings(SWGDOA2Settings* doa2_settings);
SWGDSCDemodSettings* getDscDemodSettings();
void setDscDemodSettings(SWGDSCDemodSettings* dsc_demod_settings);
SWGDSDDemodSettings* getDsdDemodSettings();
void setDsdDemodSettings(SWGDSDDemodSettings* dsd_demod_settings);
@ -337,6 +341,9 @@ private:
SWGDOA2Settings* doa2_settings;
bool m_doa2_settings_isSet;
SWGDSCDemodSettings* dsc_demod_settings;
bool m_dsc_demod_settings_isSet;
SWGDSDDemodSettings* dsd_demod_settings;
bool m_dsd_demod_settings_isSet;

Wyświetl plik

@ -99,6 +99,8 @@
#include "SWGDATVModSettings.h"
#include "SWGDOA2Report.h"
#include "SWGDOA2Settings.h"
#include "SWGDSCDemodReport.h"
#include "SWGDSCDemodSettings.h"
#include "SWGDSDDemodReport.h"
#include "SWGDSDDemodSettings.h"
#include "SWGDVSerialDevice.h"
@ -780,6 +782,16 @@ namespace SWGSDRangel {
obj->init();
return obj;
}
if(QString("SWGDSCDemodReport").compare(type) == 0) {
SWGDSCDemodReport *obj = new SWGDSCDemodReport();
obj->init();
return obj;
}
if(QString("SWGDSCDemodSettings").compare(type) == 0) {
SWGDSCDemodSettings *obj = new SWGDSCDemodSettings();
obj->init();
return obj;
}
if(QString("SWGDSDDemodReport").compare(type) == 0) {
SWGDSDDemodReport *obj = new SWGDSDDemodReport();
obj->init();