diff --git a/CMakeLists.txt b/CMakeLists.txt index 88619d1bc..1836dc7e2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/doc/img/DSCDemod_plugin.png b/doc/img/DSCDemod_plugin.png new file mode 100644 index 000000000..e79449ba6 Binary files /dev/null and b/doc/img/DSCDemod_plugin.png differ diff --git a/doc/img/DSCDemod_plugin_geocall.png b/doc/img/DSCDemod_plugin_geocall.png new file mode 100644 index 000000000..a7f3745bf Binary files /dev/null and b/doc/img/DSCDemod_plugin_geocall.png differ diff --git a/plugins/channelrx/CMakeLists.txt b/plugins/channelrx/CMakeLists.txt index 47a6f077b..2c156b81b 100644 --- a/plugins/channelrx/CMakeLists.txt +++ b/plugins/channelrx/CMakeLists.txt @@ -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) diff --git a/plugins/channelrx/demodadsb/adsbdemodgui.cpp b/plugins/channelrx/demodadsb/adsbdemodgui.cpp index ae65c0394..fd898e6d0 100644 --- a/plugins/channelrx/demodadsb/adsbdemodgui.cpp +++ b/plugins/channelrx/demodadsb/adsbdemodgui.cpp @@ -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) { diff --git a/plugins/channelrx/demoddab/dabdemodgui.cpp b/plugins/channelrx/demoddab/dabdemodgui.cpp index efdc47ea4..15587e8ae 100644 --- a/plugins/channelrx/demoddab/dabdemodgui.cpp +++ b/plugins/channelrx/demoddab/dabdemodgui.cpp @@ -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 diff --git a/plugins/channelrx/demoddsc/CMakeLists.txt b/plugins/channelrx/demoddsc/CMakeLists.txt new file mode 100644 index 000000000..ed2164296 --- /dev/null +++ b/plugins/channelrx/demoddsc/CMakeLists.txt @@ -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 $ CONFIGURATIONS Debug RelWithDebInfo DESTINATION ${INSTALL_FOLDER} ) +endif() diff --git a/plugins/channelrx/demoddsc/dscdemod.cpp b/plugins/channelrx/demoddsc/dscdemod.cpp new file mode 100644 index 000000000..7ba80a471 --- /dev/null +++ b/plugins/channelrx/demoddsc/dscdemod.cpp @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "dscdemod.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#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 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 pipes; + MainCore::instance()->getMessagePipes().getMessagePipes(this, "reportdemod", pipes); + + if (pipes.size() > 0) + { + for (const auto& pipe : pipes) + { + MessageQueue *messageQueue = qobject_cast(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& 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& 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); +} diff --git a/plugins/channelrx/demoddsc/dscdemod.h b/plugins/channelrx/demoddsc/dscdemod.h new file mode 100644 index 000000000..bca91b84f --- /dev/null +++ b/plugins/channelrx/demoddsc/dscdemod.h @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_DSCDEMOD_H +#define INCLUDE_DSCDEMOD_H + +#include + +#include +#include +#include +#include +#include +#include +#include + +#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& channelSettingsKeys, const DSCDemodSettings& settings, bool force); + void webapiFormatChannelSettings( + QList& 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 + diff --git a/plugins/channelrx/demoddsc/dscdemodbaseband.cpp b/plugins/channelrx/demoddsc/dscdemodbaseband.cpp new file mode 100644 index 000000000..c154aa2a0 --- /dev/null +++ b/plugins/channelrx/demoddsc/dscdemodbaseband.cpp @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#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()); +} + diff --git a/plugins/channelrx/demoddsc/dscdemodbaseband.h b/plugins/channelrx/demoddsc/dscdemodbaseband.h new file mode 100644 index 000000000..c1dc622d4 --- /dev/null +++ b/plugins/channelrx/demoddsc/dscdemodbaseband.h @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_DSCDEMODBASEBAND_H +#define INCLUDE_DSCDEMODBASEBAND_H + +#include +#include + +#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 + diff --git a/plugins/channelrx/demoddsc/dscdemodgui.cpp b/plugins/channelrx/demoddsc/dscdemodgui.cpp new file mode 100644 index 000000000..13b43895c --- /dev/null +++ b/plugins/channelrx/demoddsc/dscdemodgui.cpp @@ -0,0 +1,1233 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dscdemodgui.h" + +#include "device/deviceuiset.h" +#include "device/deviceset.h" +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "dsp/devicesamplesource.h" +#include "dsp/dspdevicesourceengine.h" +#include "ui_dscdemodgui.h" +#include "plugin/pluginapi.h" +#include "util/simpleserializer.h" +#include "util/csv.h" +#include "util/db.h" +#include "util/mmsi.h" +#include "util/units.h" +#include "gui/basicchannelsettingsdialog.h" +#include "gui/devicestreamselectiondialog.h" +#include "gui/decimaldelegate.h" +#include "dsp/dspengine.h" +#include "dsp/glscopesettings.h" +#include "gui/crightclickenabler.h" +#include "gui/tabletapandhold.h" +#include "gui/dialogpositioner.h" +#include "gui/frequencydelegate.h" +#include "channel/channelwebapiutils.h" +#include "feature/featurewebapiutils.h" +#include "maincore.h" + +#include "dscdemod.h" +#include "dscdemodsink.h" + +#include "SWGMapItem.h" + +void DSCDemodGUI::resizeTable() +{ + // Fill table with a row of dummy data that will size the columns nicely + // Trailing spaces are for sort arrow + int row = ui->messages->rowCount(); + ui->messages->setRowCount(row + 1); + ui->messages->setItem(row, MESSAGE_COL_RX_DATE, new QTableWidgetItem("15/04/2016-")); + ui->messages->setItem(row, MESSAGE_COL_RX_TIME, new QTableWidgetItem("10:17")); + ui->messages->setItem(row, MESSAGE_COL_FORMAT, new QTableWidgetItem("Selective call")); + ui->messages->setItem(row, MESSAGE_COL_ADDRESS, new QTableWidgetItem("123456789")); + ui->messages->setItem(row, MESSAGE_COL_ADDRESS_COUNTRY, new QTableWidgetItem("flag")); + ui->messages->setItem(row, MESSAGE_COL_ADDRESS_TYPE, new QTableWidgetItem("Coast")); + ui->messages->setItem(row, MESSAGE_COL_ADDRESS_NAME, new QTableWidgetItem("A ships name")); + ui->messages->setItem(row, MESSAGE_COL_CATEGORY, new QTableWidgetItem("Distress")); + ui->messages->setItem(row, MESSAGE_COL_SELF_ID, new QTableWidgetItem("123456789")); + ui->messages->setItem(row, MESSAGE_COL_SELF_ID_COUNTRY, new QTableWidgetItem("flag")); + ui->messages->setItem(row, MESSAGE_COL_SELF_ID_TYPE, new QTableWidgetItem("Coast")); + ui->messages->setItem(row, MESSAGE_COL_SELF_ID_NAME, new QTableWidgetItem("A ships name")); + ui->messages->setItem(row, MESSAGE_COL_SELF_ID_RANGE, new QTableWidgetItem("3000.0")); + ui->messages->setItem(row, MESSAGE_COL_TELECOMMAND_1, new QTableWidgetItem("No information")); + ui->messages->setItem(row, MESSAGE_COL_TELECOMMAND_2, new QTableWidgetItem("No information")); + ui->messages->setItem(row, MESSAGE_COL_RX, new QTableWidgetItem("30,000.0 kHz")); + ui->messages->setItem(row, MESSAGE_COL_TX, new QTableWidgetItem("30,000.0 kHz")); + ui->messages->setItem(row, MESSAGE_COL_POSITION, new QTableWidgetItem("-90d60N -180d60W")); + ui->messages->setItem(row, MESSAGE_COL_NUMBER, new QTableWidgetItem("0898123456")); + ui->messages->setItem(row, MESSAGE_COL_TIME, new QTableWidgetItem("12:00")); + ui->messages->setItem(row, MESSAGE_COL_COMMS, new QTableWidgetItem("FSK")); + ui->messages->setItem(row, MESSAGE_COL_EOS, new QTableWidgetItem("Req Ack")); + ui->messages->setItem(row, MESSAGE_COL_ECC, new QTableWidgetItem("Fail")); + ui->messages->setItem(row, MESSAGE_COL_ERRORS, new QTableWidgetItem("9")); + ui->messages->setItem(row, MESSAGE_COL_VALID, new QTableWidgetItem("Invalid")); + ui->messages->setItem(row, MESSAGE_COL_RSSI, new QTableWidgetItem("-50")); + ui->messages->resizeColumnsToContents(); + ui->messages->removeRow(row); +} + +// Columns in table reordered +void DSCDemodGUI::messages_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex) +{ + (void) oldVisualIndex; + + m_settings.m_columnIndexes[logicalIndex] = newVisualIndex; +} + +// Column in table resized (when hidden size is 0) +void DSCDemodGUI::messages_sectionResized(int logicalIndex, int oldSize, int newSize) +{ + (void) oldSize; + + m_settings.m_columnSizes[logicalIndex] = newSize; +} + +// Right click in table header - show column select menu +void DSCDemodGUI::columnSelectMenu(QPoint pos) +{ + m_menu->popup(ui->messages->horizontalHeader()->viewport()->mapToGlobal(pos)); +} + +// Hide/show column when menu selected +void DSCDemodGUI::columnSelectMenuChecked(bool checked) +{ + (void) checked; + + QAction* action = qobject_cast(sender()); + if (action != nullptr) + { + int idx = action->data().toInt(nullptr); + ui->messages->setColumnHidden(idx, !action->isChecked()); + } +} + +// Create column select menu item +QAction *DSCDemodGUI::createCheckableItem(QString &text, int idx, bool checked) +{ + QAction *action = new QAction(text, this); + action->setCheckable(true); + action->setChecked(checked); + action->setData(QVariant(idx)); + connect(action, SIGNAL(triggered()), this, SLOT(columnSelectMenuChecked())); + return action; +} + +DSCDemodGUI* DSCDemodGUI::create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) +{ + DSCDemodGUI* gui = new DSCDemodGUI(pluginAPI, deviceUISet, rxChannel); + return gui; +} + +void DSCDemodGUI::destroy() +{ + delete this; +} + +void DSCDemodGUI::resetToDefaults() +{ + m_settings.resetToDefaults(); + displaySettings(); + applySettings(true); +} + +QByteArray DSCDemodGUI::serialize() const +{ + return m_settings.serialize(); +} + +bool DSCDemodGUI::deserialize(const QByteArray& data) +{ + if(m_settings.deserialize(data)) { + displaySettings(); + applySettings(true); + return true; + } else { + resetToDefaults(); + return false; + } +} + +// Add row to table +void DSCDemodGUI::messageReceived(const DSCMessage& message, int errors, float rssi) +{ + // Is scroll bar at bottom + QScrollBar *sb = ui->messages->verticalScrollBar(); + bool scrollToBottom = sb->value() == sb->maximum(); + + ui->messages->setSortingEnabled(false); + int row = ui->messages->rowCount(); + ui->messages->setRowCount(row + 1); + + QTableWidgetItem *rxDateItem = new QTableWidgetItem(); + QTableWidgetItem *rxTimeItem = new QTableWidgetItem(); + QTableWidgetItem *formatItem = new QTableWidgetItem(); + QTableWidgetItem *addressItem = new QTableWidgetItem(); + QTableWidgetItem *addressCountryItem = new QTableWidgetItem(); + QTableWidgetItem *addressTypeItem = new QTableWidgetItem(); + QTableWidgetItem *addressNameItem = new QTableWidgetItem(); + QTableWidgetItem *categoryItem = new QTableWidgetItem(); + QTableWidgetItem *selfIdItem = new QTableWidgetItem(); + QTableWidgetItem *selfIdCountryItem = new QTableWidgetItem(); + QTableWidgetItem *selfIdTypeItem = new QTableWidgetItem(); + QTableWidgetItem *selfIdNameItem = new QTableWidgetItem(); + QTableWidgetItem *selfIdRangeItem = new QTableWidgetItem(); + QTableWidgetItem *telecommand1Item = new QTableWidgetItem(); + QTableWidgetItem *telecommand2Item = new QTableWidgetItem(); + QTableWidgetItem *rxItem = new QTableWidgetItem(); + QTableWidgetItem *txItem = new QTableWidgetItem(); + QTableWidgetItem *positionItem = new QTableWidgetItem(); + QTableWidgetItem *distressIdItem = new QTableWidgetItem(); + QTableWidgetItem *distressItem = new QTableWidgetItem(); + QTableWidgetItem *numberItem = new QTableWidgetItem(); + QTableWidgetItem *timeItem = new QTableWidgetItem(); + QTableWidgetItem *commsItem = new QTableWidgetItem(); + QTableWidgetItem *eosItem = new QTableWidgetItem(); + QTableWidgetItem *eccItem = new QTableWidgetItem(); + QTableWidgetItem *errorsItem = new QTableWidgetItem(); + QTableWidgetItem *validItem = new QTableWidgetItem(); + QTableWidgetItem *rssiItem = new QTableWidgetItem(); + ui->messages->setItem(row, MESSAGE_COL_RX_DATE, rxDateItem); + ui->messages->setItem(row, MESSAGE_COL_RX_TIME, rxTimeItem); + ui->messages->setItem(row, MESSAGE_COL_FORMAT, formatItem); + ui->messages->setItem(row, MESSAGE_COL_ADDRESS, addressItem); + ui->messages->setItem(row, MESSAGE_COL_ADDRESS_COUNTRY, addressCountryItem); + ui->messages->setItem(row, MESSAGE_COL_ADDRESS_TYPE, addressTypeItem); + ui->messages->setItem(row, MESSAGE_COL_ADDRESS_NAME, addressNameItem); + ui->messages->setItem(row, MESSAGE_COL_CATEGORY, categoryItem); + ui->messages->setItem(row, MESSAGE_COL_SELF_ID, selfIdItem); + ui->messages->setItem(row, MESSAGE_COL_SELF_ID_COUNTRY, selfIdCountryItem); + ui->messages->setItem(row, MESSAGE_COL_SELF_ID_TYPE, selfIdTypeItem); + ui->messages->setItem(row, MESSAGE_COL_SELF_ID_NAME, selfIdNameItem); + ui->messages->setItem(row, MESSAGE_COL_SELF_ID_RANGE, selfIdRangeItem); + ui->messages->setItem(row, MESSAGE_COL_TELECOMMAND_1, telecommand1Item); + ui->messages->setItem(row, MESSAGE_COL_TELECOMMAND_2, telecommand2Item); + ui->messages->setItem(row, MESSAGE_COL_RX, rxItem); + ui->messages->setItem(row, MESSAGE_COL_TX, txItem); + ui->messages->setItem(row, MESSAGE_COL_POSITION, positionItem); + ui->messages->setItem(row, MESSAGE_COL_DISTRESS_ID, distressIdItem); + ui->messages->setItem(row, MESSAGE_COL_DISTRESS, distressItem); + ui->messages->setItem(row, MESSAGE_COL_NUMBER, numberItem); + ui->messages->setItem(row, MESSAGE_COL_TIME, timeItem); + ui->messages->setItem(row, MESSAGE_COL_COMMS, commsItem); + ui->messages->setItem(row, MESSAGE_COL_EOS, eosItem); + ui->messages->setItem(row, MESSAGE_COL_ECC, eccItem); + ui->messages->setItem(row, MESSAGE_COL_ERRORS, errorsItem); + ui->messages->setItem(row, MESSAGE_COL_VALID, validItem); + ui->messages->setItem(row, MESSAGE_COL_RSSI, rssiItem); + + rxDateItem->setData(Qt::DisplayRole, message.m_dateTime.date()); + rxTimeItem->setData(Qt::DisplayRole, message.m_dateTime.time()); + + formatItem->setText(message.formatSpecifier()); + if (message.m_hasCategory) { + categoryItem->setText(message.category()); + } + if (message.m_hasAddress) + { + addressItem->setText(message.m_address); + if (message.m_formatSpecifier != DSCMessage::GEOGRAPHIC_CALL) + { + QIcon *addressFlag = MMSI::getFlagIcon(message.m_address); + if (addressFlag) + { + addressCountryItem->setSizeHint(QSize(40, 20)); + addressCountryItem->setIcon(*addressFlag); + } + addressTypeItem->setText(MMSI::getCategory(message.m_address)); + } + } + selfIdItem->setText(message.m_selfId); + QIcon *selfIdFlag = MMSI::getFlagIcon(message.m_selfId); + if (selfIdFlag) + { + selfIdCountryItem->setSizeHint(QSize(40, 20)); + selfIdCountryItem->setIcon(*selfIdFlag); + } + selfIdTypeItem->setText(MMSI::getCategory(message.m_selfId)); + if (message.m_hasTelecommand1) { + telecommand1Item->setText(DSCMessage::telecommand1(message.m_telecommand1)); + } + if (message.m_hasTelecommand2) { + telecommand2Item->setText(DSCMessage::telecommand2(message.m_telecommand2)); + } + if (message.m_hasFrequency1) { + rxItem->setData(Qt::DisplayRole, message.m_frequency1); + } else if (message.m_hasChannel1) { + rxItem->setText(message.m_channel1); + } + if (message.m_hasFrequency2) { + txItem->setData(Qt::DisplayRole, message.m_frequency2); + } else if (message.m_hasChannel2) { + txItem->setText(message.m_channel2); + } + if (message.m_hasPosition) { + positionItem->setText(message.m_position); + } + if (message.m_hasDistressId) { + distressIdItem->setText(message.m_distressId); + } + if (message.m_hasDistressNature) { + distressItem->setText(DSCMessage::distressNature(message.m_distressNature)); + } + if (message.m_hasNumber) { + numberItem->setText(message.m_number); + } + if (message.m_hasTime) { + timeItem->setData(Qt::DisplayRole, message.m_time); + } + if (message.m_hasSubsequenceComms) { + commsItem->setText(DSCMessage::telecommand1(message.m_subsequenceComms)); + } + eosItem->setText(DSCMessage::endOfSignal(message.m_eos)); + if (message.m_eccOk) { + eccItem->setText("OK"); + } else { + eccItem->setText(QString("Fail (%1 != %2)").arg(message.m_ecc).arg(message.m_calculatedECC)); + } + if (message.m_valid) { + validItem->setText("Valid"); + } else { + validItem->setText("Invalid"); + } + + errorsItem->setData(Qt::DisplayRole, errors); + rssiItem->setData(Qt::DisplayRole, rssi); + + filterRow(row); + ui->messages->setSortingEnabled(true); + ui->messages->resizeRowToContents(row); + if (scrollToBottom) { + ui->messages->scrollToBottom(); + } + + // Get latest APRS.fi data to calculate distance + if (m_aprsFi && message.m_valid) + { + QStringList addresses; + addresses.append(message.m_selfId); + if (message.m_hasAddress + && (message.m_formatSpecifier != DSCMessage::GEOGRAPHIC_CALL) + && (message.m_formatSpecifier != DSCMessage::GROUP_CALL) + && (message.m_formatSpecifier != DSCMessage::ALL_SHIPS) + && (message.m_selfId != message.m_address) + ) { + addresses.append(message.m_address); + } + m_aprsFi->getData(addresses); + } +} + +bool DSCDemodGUI::handleMessage(const Message& message) +{ + if (DSCDemod::MsgConfigureDSCDemod::match(message)) + { + qDebug("DSCDemodGUI::handleMessage: DSCDemod::MsgConfigureDSCDemod"); + const DSCDemod::MsgConfigureDSCDemod& cfg = (DSCDemod::MsgConfigureDSCDemod&) message; + m_settings = cfg.getSettings(); + blockApplySettings(true); + ui->scopeGUI->updateSettings(); + m_channelMarker.updateSettings(static_cast(m_settings.m_channelMarker)); + displaySettings(); + blockApplySettings(false); + return true; + } + else if (DSPSignalNotification::match(message)) + { + DSPSignalNotification& notif = (DSPSignalNotification&) message; + m_deviceCenterFrequency = notif.getCenterFrequency(); + m_basebandSampleRate = notif.getSampleRate(); + ui->deltaFrequency->setValueRange(false, 7, -m_basebandSampleRate/2, m_basebandSampleRate/2); + ui->deltaFrequencyLabel->setToolTip(tr("Range %1 %L2 Hz").arg(QChar(0xB1)).arg(m_basebandSampleRate/2)); + updateAbsoluteCenterFrequency(); + return true; + } + else if (DSCDemod::MsgMessage::match(message)) + { + DSCDemod::MsgMessage& textMsg = (DSCDemod::MsgMessage&) message; + messageReceived(textMsg.getMessage(), textMsg.getErrors(), textMsg.getRSSI()); + return true; + } + + return false; +} + +void DSCDemodGUI::handleInputMessages() +{ + Message* message; + + while ((message = getInputMessageQueue()->pop()) != 0) + { + if (handleMessage(*message)) + { + delete message; + } + } +} + +void DSCDemodGUI::channelMarkerChangedByCursor() +{ + ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + applySettings(); +} + +void DSCDemodGUI::channelMarkerHighlightedByCursor() +{ + setHighlighted(m_channelMarker.getHighlighted()); +} + +void DSCDemodGUI::on_deltaFrequency_changed(qint64 value) +{ + m_channelMarker.setCenterFrequency(value); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + updateAbsoluteCenterFrequency(); + applySettings(); +} + +void DSCDemodGUI::on_filterInvalid_clicked(bool checked) +{ + m_settings.m_filterInvalid = checked; + filter(); + applySettings(); +} + +void DSCDemodGUI::on_filterColumn_currentIndexChanged(int index) +{ + m_settings.m_filterColumn = index; + filter(); + applySettings(); +} + +void DSCDemodGUI::on_filter_editingFinished() +{ + m_settings.m_filter = ui->filter->text(); + filter(); + applySettings(); +} + +void DSCDemodGUI::on_clearTable_clicked() +{ + ui->messages->setRowCount(0); +} + +void DSCDemodGUI::on_udpEnabled_clicked(bool checked) +{ + m_settings.m_udpEnabled = checked; + applySettings(); +} + +void DSCDemodGUI::on_udpAddress_editingFinished() +{ + m_settings.m_udpAddress = ui->udpAddress->text(); + applySettings(); +} + +void DSCDemodGUI::on_udpPort_editingFinished() +{ + m_settings.m_udpPort = ui->udpPort->text().toInt(); + applySettings(); +} + +void DSCDemodGUI::filterRow(int row) +{ + bool hidden = false; + if (m_settings.m_filterInvalid) + { + QTableWidgetItem *validItem = ui->messages->item(row, MESSAGE_COL_VALID); + if (validItem->text() != "Valid") { + hidden = true; + } + } + if (m_settings.m_filter != "") + { + QTableWidgetItem *item = ui->messages->item(row, m_settings.m_filterColumn); + QRegExp re(m_settings.m_filter); + if (!re.exactMatch(item->text())) { + hidden = true; + } + } + ui->messages->setRowHidden(row, hidden); +} + +void DSCDemodGUI::filter() +{ + for (int i = 0; i < ui->messages->rowCount(); i++) { + filterRow(i); + } +} + +void DSCDemodGUI::onWidgetRolled(QWidget* widget, bool rollDown) +{ + (void) widget; + (void) rollDown; + + getRollupContents()->saveState(m_rollupState); + applySettings(); +} + +void DSCDemodGUI::onMenuDialogCalled(const QPoint &p) +{ + if (m_contextMenuType == ContextMenuChannelSettings) + { + BasicChannelSettingsDialog dialog(&m_channelMarker, this); + dialog.setUseReverseAPI(m_settings.m_useReverseAPI); + dialog.setReverseAPIAddress(m_settings.m_reverseAPIAddress); + dialog.setReverseAPIPort(m_settings.m_reverseAPIPort); + dialog.setReverseAPIDeviceIndex(m_settings.m_reverseAPIDeviceIndex); + dialog.setReverseAPIChannelIndex(m_settings.m_reverseAPIChannelIndex); + dialog.setDefaultTitle(m_displayedName); + + if (m_deviceUISet->m_deviceMIMOEngine) + { + dialog.setNumberOfStreams(m_dscDemod->getNumberOfDeviceStreams()); + dialog.setStreamIndex(m_settings.m_streamIndex); + } + + dialog.move(p); + new DialogPositioner(&dialog, false); + dialog.exec(); + + m_settings.m_rgbColor = m_channelMarker.getColor().rgb(); + m_settings.m_title = m_channelMarker.getTitle(); + m_settings.m_useReverseAPI = dialog.useReverseAPI(); + m_settings.m_reverseAPIAddress = dialog.getReverseAPIAddress(); + m_settings.m_reverseAPIPort = dialog.getReverseAPIPort(); + m_settings.m_reverseAPIDeviceIndex = dialog.getReverseAPIDeviceIndex(); + m_settings.m_reverseAPIChannelIndex = dialog.getReverseAPIChannelIndex(); + + setWindowTitle(m_settings.m_title); + setTitle(m_channelMarker.getTitle()); + setTitleColor(m_settings.m_rgbColor); + + if (m_deviceUISet->m_deviceMIMOEngine) + { + m_settings.m_streamIndex = dialog.getSelectedStreamIndex(); + m_channelMarker.clearStreamIndexes(); + m_channelMarker.addStreamIndex(m_settings.m_streamIndex); + updateIndexLabel(); + } + + applySettings(); + } + + resetContextMenuType(); +} + +DSCDemodGUI::DSCDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent) : + ChannelGUI(parent), + ui(new Ui::DSCDemodGUI), + m_pluginAPI(pluginAPI), + m_deviceUISet(deviceUISet), + m_channelMarker(this), + m_deviceCenterFrequency(0), + m_doApplySettings(true), + m_tickCount(0) +{ + setAttribute(Qt::WA_DeleteOnClose, true); + m_helpURL = "plugins/channelrx/demoddsc/readme.md"; + RollupContents *rollupContents = getRollupContents(); + ui->setupUi(rollupContents); + setSizePolicy(rollupContents->sizePolicy()); + rollupContents->arrangeRollups(); + connect(rollupContents, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool))); + connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &))); + + m_dscDemod = reinterpret_cast(rxChannel); + m_dscDemod->setMessageQueueToGUI(getInputMessageQueue()); + + connect(&MainCore::instance()->getMasterTimer(), SIGNAL(timeout()), this, SLOT(tick())); // 50 ms + + ui->deltaFrequencyLabel->setText(QString("%1f").arg(QChar(0x94, 0x03))); + ui->deltaFrequency->setColorMapper(ColorMapper(ColorMapper::GrayGold)); + ui->deltaFrequency->setValueRange(false, 7, -9999999, 9999999); + ui->channelPowerMeter->setColorTheme(LevelMeterSignalDB::ColorGreenAndBlue); + + ui->messages->setItemDelegateForColumn(MESSAGE_COL_RSSI, new DecimalDelegate(1)); + + m_scopeVis = m_dscDemod->getScopeSink(); + m_scopeVis->setGLScope(ui->glScope); + m_scopeVis->setNbStreams(DSCDemodSettings::m_scopeStreams); + ui->glScope->connectTimer(MainCore::instance()->getMasterTimer()); + ui->scopeGUI->setBuddies(m_scopeVis->getInputMessageQueue(), m_scopeVis, ui->glScope); + ui->scopeGUI->setStreams(QStringList({"IQ", "MagSq", "abs1", "abs2", "Unbiased", "Biased", "Data", "Clock", "Bit", "GotSOP"})); + + // Scope settings to display the IQ waveforms + ui->scopeGUI->setPreTrigger(1); + GLScopeSettings::TraceData traceDataI, traceDataQ; + traceDataI.m_projectionType = Projector::ProjectionReal; + traceDataI.m_amp = 1.0; // for -1 to +1 + traceDataI.m_ofs = 0.0; // vertical offset + traceDataQ.m_projectionType = Projector::ProjectionImag; + traceDataQ.m_amp = 1.0; + traceDataQ.m_ofs = 0.0; + ui->scopeGUI->changeTrace(0, traceDataI); + ui->scopeGUI->addTrace(traceDataQ); + ui->scopeGUI->setDisplayMode(GLScopeSettings::DisplayXYV); + ui->scopeGUI->focusOnTrace(0); // re-focus to take changes into account in the GUI + + GLScopeSettings::TriggerData triggerData; + triggerData.m_triggerLevel = 0.1; + triggerData.m_triggerLevelCoarse = 10; + triggerData.m_triggerPositiveEdge = true; + ui->scopeGUI->changeTrigger(0, triggerData); + ui->scopeGUI->focusOnTrigger(0); // re-focus to take changes into account in the GUI + + m_scopeVis->setLiveRate(DSCDemodSettings::DSCDEMOD_CHANNEL_SAMPLE_RATE); + m_scopeVis->configure(500, 1, 0, 0, true); // not working! + //m_scopeVis->setFreeRun(false); // FIXME: add method rather than call m_scopeVis->configure() + + m_channelMarker.blockSignals(true); + m_channelMarker.setColor(Qt::yellow); + m_channelMarker.setBandwidth(m_settings.m_rfBandwidth); + m_channelMarker.setCenterFrequency(m_settings.m_inputFrequencyOffset); + m_channelMarker.setTitle("DSC Demodulator"); + m_channelMarker.blockSignals(false); + m_channelMarker.setVisible(true); // activate signal on the last setting only + + setTitleColor(m_channelMarker.getColor()); + m_settings.setChannelMarker(&m_channelMarker); + m_settings.setScopeGUI(ui->scopeGUI); + m_settings.setRollupState(&m_rollupState); + + m_deviceUISet->addChannelMarker(&m_channelMarker); + + connect(&m_channelMarker, SIGNAL(changedByCursor()), this, SLOT(channelMarkerChangedByCursor())); + connect(&m_channelMarker, SIGNAL(highlightedByCursor()), this, SLOT(channelMarkerHighlightedByCursor())); + connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + + // Resize the table using dummy data + resizeTable(); + // Allow user to reorder columns + ui->messages->horizontalHeader()->setSectionsMovable(true); + // Allow user to sort table by clicking on headers + ui->messages->setSortingEnabled(true); + // Add context menu to allow hiding/showing of columns + m_menu = new QMenu(ui->messages); + for (int i = 0; i < ui->messages->horizontalHeader()->count(); i++) + { + QString text = ui->messages->horizontalHeaderItem(i)->text(); + m_menu->addAction(createCheckableItem(text, i, true)); + } + ui->messages->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->messages->horizontalHeader(), SIGNAL(customContextMenuRequested(QPoint)), SLOT(columnSelectMenu(QPoint))); + // Get signals when columns change + connect(ui->messages->horizontalHeader(), SIGNAL(sectionMoved(int, int, int)), SLOT(messages_sectionMoved(int, int, int))); + connect(ui->messages->horizontalHeader(), SIGNAL(sectionResized(int, int, int)), SLOT(messages_sectionResized(int, int, int))); + + ui->messages->setItemDelegateForColumn(MESSAGE_COL_RX, new FrequencyDelegate()); + ui->messages->setItemDelegateForColumn(MESSAGE_COL_TX, new FrequencyDelegate()); + + ui->messages->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->messages, SIGNAL(customContextMenuRequested(QPoint)), SLOT(customContextMenuRequested(QPoint))); + TableTapAndHold *tableTapAndHold = new TableTapAndHold(ui->messages); + connect(tableTapAndHold, &TableTapAndHold::tapAndHold, this, &DSCDemodGUI::customContextMenuRequested); + + ui->scopeContainer->setVisible(false); + + m_aprsFi = APRSFi::create(); + if (m_aprsFi) { + connect(m_aprsFi, &APRSFi::dataUpdated, this, &DSCDemodGUI::aprsFiDataUpdated); + } + + CRightClickEnabler *feedRightClickEnabler = new CRightClickEnabler(ui->feed); + connect(feedRightClickEnabler, &CRightClickEnabler::rightClick, this, &DSCDemodGUI::on_feed_rightClicked); + + displaySettings(); + makeUIConnections(); + applySettings(true); +} + +void DSCDemodGUI::createMenuOpenURLAction(QMenu* tableContextMenu, const QString& text, const QString& url, const QString& arg) +{ + QAction* action = new QAction(QString(text).arg(arg), tableContextMenu); + connect(action, &QAction::triggered, this, [url, arg]()->void { + QDesktopServices::openUrl(QUrl(QString(url).arg(arg))); + }); + tableContextMenu->addAction(action); +} + +void DSCDemodGUI::createMenuFindOnMapAction(QMenu* tableContextMenu, const QString& text, const QString& target) +{ + QAction* findOnMapAction = new QAction(QString(text).arg(target), tableContextMenu); + connect(findOnMapAction, &QAction::triggered, this, [target]()->void { + FeatureWebAPIUtils::mapFind(target); + }); tableContextMenu->addAction(findOnMapAction); + tableContextMenu->addAction(findOnMapAction); +} + +void DSCDemodGUI::customContextMenuRequested(QPoint pos) +{ + QTableWidgetItem *item = ui->messages->itemAt(pos); + if (item) + { + int row = item->row(); + QString time = ui->messages->item(row, MESSAGE_COL_RX_TIME)->data(Qt::DisplayRole).toTime().toString("hh:mm:ss"); + QString selfId = ui->messages->item(row, MESSAGE_COL_SELF_ID)->text(); + QString address = ui->messages->item(row, MESSAGE_COL_ADDRESS)->text(); + QString position = ui->messages->item(row, MESSAGE_COL_POSITION)->text(); + QString format = ui->messages->item(row, MESSAGE_COL_FORMAT)->text(); + FrequencyDelegate fd; + QString rx = ui->messages->item(row, MESSAGE_COL_RX)->text(); + QString rxFormatted = fd.displayText(rx, QLocale::system()); + QString tx = ui->messages->item(row, MESSAGE_COL_TX)->text(); + QString txFormatted = fd.displayText(tx, QLocale::system()); + + QMenu* tableContextMenu = new QMenu(ui->messages); + connect(tableContextMenu, &QMenu::aboutToHide, tableContextMenu, &QMenu::deleteLater); + + QAction* copyAction = new QAction("Copy", tableContextMenu); + const QString text = item->text(); + connect(copyAction, &QAction::triggered, this, [text]()->void { + QClipboard *clipboard = QGuiApplication::clipboard(); + clipboard->setText(text); + }); + tableContextMenu->addAction(copyAction); + + // View MMSIs on various websites + + createMenuOpenURLAction(tableContextMenu, "View MMSI %1 on aishub.net...", "https://www.aishub.net/vessels?Ship%5Bmmsi%5D=%1&mmsi=%1", selfId); + createMenuOpenURLAction(tableContextMenu, "View MMSI %1 on vesselfinder.com...", "https://www.vesselfinder.com/vessels?name=%1", selfId); + createMenuOpenURLAction(tableContextMenu, "View MMSI %1 on aprs.fi...", "https://aprs.fi/#!mt=roadmap&z=11&call=i/%1", selfId); + createMenuOpenURLAction(tableContextMenu, "View MMSI %1 on yaddnet.org...", "http://yaddnet.org/pages/php/band_today_messages.php?from_mmsi=%1", selfId); + + if (!address.isEmpty() + && (format != "Geographic call") + && (format != "Group call") + && (format != "All ships")) + { + createMenuOpenURLAction(tableContextMenu, "View MMSI %1 on aishub.net...", "https://www.aishub.net/vessels?Ship%5Bmmsi%5D=%1&mmsi=%1", address); + createMenuOpenURLAction(tableContextMenu, "View MMSI %1 on vesselfinder.com...", "https://www.vesselfinder.com/vessels?name=%1", address); + createMenuOpenURLAction(tableContextMenu, "View MMSI %1 on aprs.fi...", "https://aprs.fi/#!mt=roadmap&z=11&call=i/%1", address); + createMenuOpenURLAction(tableContextMenu, "View MMSI %1 on yaddnet.fi...", "http://yaddnet.org/pages/php/band_today_messages.php?from_mmsi=%1", address); + } + + // Find on Map + if (!selfId.isEmpty() || !address.isEmpty() || !position.isEmpty()) + { + tableContextMenu->addSeparator(); + if (!selfId.isEmpty()) { + createMenuFindOnMapAction(tableContextMenu, "Find MMSI %1 on map", selfId); + } + if (!address.isEmpty() && (format != "Geographic call")) { + createMenuFindOnMapAction(tableContextMenu, "Find MMSI %1 on map", address); + } + if (!position.isEmpty()) { + createMenuFindOnMapAction(tableContextMenu, "Center map at %1", position); + } + if (!address.isEmpty() && (format == "Geographic call")) + { + QString name = QString("DSC Call %1 %2").arg(selfId).arg(time); + if (!m_mapItems.contains(name)) + { + QString flag = MMSI::getFlagIconURL(selfId); + QStringList s; + s.append("Geographic call"); + s.append(QString("From: %1 ").arg(selfId).arg(flag)); + s.append(QString("To: %1").arg(address)); + s.append(QString("Category: %1").arg(ui->messages->item(row, MESSAGE_COL_CATEGORY)->text())); + QString tc1 = ui->messages->item(row, MESSAGE_COL_TELECOMMAND_1)->text(); + if (!tc1.isEmpty() && (tc1 != "No information")) { + s.append(QString("Telecommand 1: %1").arg(tc1)); + } + FrequencyDelegate fd; + if (!rx.isEmpty()) { + s.append(QString("RX: %1").arg(rxFormatted)); + } + if (!tx.isEmpty()) { + s.append(QString("TX: %1").arg(txFormatted)); + } + QString info = s.join("
"); + + QAction* sendAreaToMapAction = new QAction(QString("Display %1 on map").arg(address), tableContextMenu); + connect(sendAreaToMapAction, &QAction::triggered, this, [this, name, address, info]()->void { + sendAreaToMapFeature(name, address, info); + QTimer::singleShot(500, [this, name] { + FeatureWebAPIUtils::mapFind(name); + }); + }); + tableContextMenu->addAction(sendAreaToMapAction); + } + else + { + QAction* findAreaOnMapAction = new QAction(QString("Center map on %1").arg(address), tableContextMenu); + connect(findAreaOnMapAction, &QAction::triggered, this, [this, name]()->void { + FeatureWebAPIUtils::mapFind(name); + }); + tableContextMenu->addAction(findAreaOnMapAction); + + QAction* clearAreaFromMapAction = new QAction(QString("Remove %1 from map").arg(address), tableContextMenu); + connect(clearAreaFromMapAction, &QAction::triggered, this, [this, name]()->void { + clearAreaFromMapFeature(name); + }); + tableContextMenu->addAction(clearAreaFromMapAction); + } + } + } + + // Menu to tune SSB demods + bool ok; + qint64 rxFreq = rx.toLongLong(&ok); + if (ok) + { + std::vector& deviceSets = MainCore::instance()->getDeviceSets(); + int deviceSetIndex = 0; + + for (const auto& deviceSet : deviceSets) + { + DSPDeviceSourceEngine *deviceSourceEngine = deviceSet->m_deviceSourceEngine; + + if (deviceSourceEngine) + { + for (int chi = 0; chi < deviceSet->getNumberOfChannels(); chi++) + { + ChannelAPI *channel = deviceSet->getChannelAt(chi); + + if (channel->getURI() == "sdrangel.channel.ssbdemod") + { + DeviceSampleSource *sampleSource = deviceSourceEngine->getSource(); + if (sampleSource) + { + QAction* tuneRxAction = new QAction(QString("Tune SSB Demod %1:%2 to %3").arg(deviceSetIndex).arg(chi).arg(rxFormatted), tableContextMenu); + connect(tuneRxAction, &QAction::triggered, this, [this, deviceSetIndex, chi, rxFreq, sampleSource]()->void { + + int bw = sampleSource->getSampleRate(); + quint64 cf = sampleSource->getCenterFrequency(); + qint64 low = (cf - bw/2 - 2000); + qint64 high = (cf + bw/2 + 2000); + + if ((rxFreq >= low) && (rxFreq <= high)) + { + int offset = rxFreq - cf; + ChannelWebAPIUtils::setFrequencyOffset(deviceSetIndex, chi, offset); + } + else + { + ChannelWebAPIUtils::setCenterFrequency(deviceSetIndex, rxFreq); + ChannelWebAPIUtils::setFrequencyOffset(deviceSetIndex, chi, 0); + } + }); + tableContextMenu->addAction(tuneRxAction); + } + } + } + } + deviceSetIndex++; + } + } + + tableContextMenu->popup(ui->messages->viewport()->mapToGlobal(pos)); + } +} + +void DSCDemodGUI::sendAreaToMapFeature(const QString& name, const QString& address, const QString& text) +{ + QRegularExpression re(QString("(\\d+)%1([NS]) (\\d+)%1([EW]) - (\\d+)%1([NS]) (\\d+)%1([EW])").arg(QChar(0xb0))); + QRegularExpressionMatch match = re.match(address); + if (match.hasMatch()) + { + int lat1 = match.captured(1).toInt(); + if (match.captured(2) == "S") { + lat1 = -lat1; + } + int lon1 = match.captured(3).toInt(); + if (match.captured(4) == "W") { + lon1 = -lon1; + } + int lat2 = match.captured(5).toInt(); + if (match.captured(6) == "S") { + lat2 = -lat2; + } + int lon2 = match.captured(7).toInt(); + if (match.captured(8) == "W") { + lon2 = -lon2; + } + + // Send to Map feature + QList mapPipes; + MainCore::instance()->getMessagePipes().getMessagePipes(m_dscDemod, "mapitems", mapPipes); + + if (mapPipes.size() > 0) + { + if (!m_mapItems.contains(name)) { + m_mapItems.append(name); + } + + for (const auto& pipe : mapPipes) + { + MessageQueue *messageQueue = qobject_cast(pipe->m_element); + SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem(); + + swgMapItem->setName(new QString(name)); + swgMapItem->setLatitude(lat1); + swgMapItem->setLongitude(lon1); + swgMapItem->setAltitude(0.0); + QString image = QString("none"); + swgMapItem->setImage(new QString(image)); + swgMapItem->setImageRotation(0); + swgMapItem->setText(new QString(text)); // Not used - label is used instead for now + swgMapItem->setLabel(new QString(text)); + swgMapItem->setAltitudeReference(0); + QList *coords = new QList(); + + SWGSDRangel::SWGMapCoordinate* c = new SWGSDRangel::SWGMapCoordinate(); + c->setLatitude(lat1); + c->setLongitude(lon1); + c->setAltitude(0.0); + coords->append(c); + + c = new SWGSDRangel::SWGMapCoordinate(); + c->setLatitude(lat1); + c->setLongitude(lon2); + c->setAltitude(0.0); + coords->append(c); + + c = new SWGSDRangel::SWGMapCoordinate(); + c->setLatitude(lat2); + c->setLongitude(lon2); + c->setAltitude(0.0); + coords->append(c); + + c = new SWGSDRangel::SWGMapCoordinate(); + c->setLatitude(lat2); + c->setLongitude(lon1); + c->setAltitude(0.0); + coords->append(c); + + c = new SWGSDRangel::SWGMapCoordinate(); + c->setLatitude(lat1); + c->setLongitude(lon1); + c->setAltitude(0.0); + coords->append(c); + + swgMapItem->setCoordinates(coords); + swgMapItem->setType(3); + + MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_dscDemod, swgMapItem); + messageQueue->push(msg); + } + } + } + else + { + qDebug() << "DSCDemodGUI::sendAreaToMapFeature: Couldn't parse address " << address; + } +} + +void DSCDemodGUI::clearAreaFromMapFeature(const QString& name) +{ + QList mapPipes; + MainCore::instance()->getMessagePipes().getMessagePipes(m_dscDemod, "mapitems", mapPipes); + for (const auto& pipe : mapPipes) + { + MessageQueue *messageQueue = qobject_cast(pipe->m_element); + SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem(); + swgMapItem->setName(new QString(name)); + swgMapItem->setImage(new QString("")); + swgMapItem->setType(3); + MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_dscDemod, swgMapItem); + messageQueue->push(msg); + } + m_mapItems.removeAll(name); +} + +DSCDemodGUI::~DSCDemodGUI() +{ + delete m_aprsFi; + delete ui; +} + +void DSCDemodGUI::blockApplySettings(bool block) +{ + m_doApplySettings = !block; +} + +void DSCDemodGUI::applySettings(bool force) +{ + if (m_doApplySettings) + { + DSCDemod::MsgConfigureDSCDemod* message = DSCDemod::MsgConfigureDSCDemod::create( m_settings, force); + m_dscDemod->getInputMessageQueue()->push(message); + } +} + +void DSCDemodGUI::displaySettings() +{ + m_channelMarker.blockSignals(true); + m_channelMarker.setBandwidth(m_settings.m_rfBandwidth); + m_channelMarker.setCenterFrequency(m_settings.m_inputFrequencyOffset); + m_channelMarker.setTitle(m_settings.m_title); + m_channelMarker.blockSignals(false); + m_channelMarker.setColor(m_settings.m_rgbColor); // activate signal on the last setting only + + setTitleColor(m_settings.m_rgbColor); + setWindowTitle(m_channelMarker.getTitle()); + setTitle(m_channelMarker.getTitle()); + + blockApplySettings(true); + + ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); + + updateIndexLabel(); + + ui->filterInvalid->setChecked(m_settings.m_filterInvalid); + ui->filterColumn->setCurrentIndex(m_settings.m_filterColumn); + ui->filter->setText(m_settings.m_filter); + + ui->udpEnabled->setChecked(m_settings.m_udpEnabled); + ui->udpAddress->setText(m_settings.m_udpAddress); + ui->udpPort->setText(QString::number(m_settings.m_udpPort)); + + ui->logFilename->setToolTip(QString(".csv log filename: %1").arg(m_settings.m_logFilename)); + ui->logEnable->setChecked(m_settings.m_logEnabled); + + ui->feed->setChecked(m_settings.m_feed); + + // Order and size columns + QHeaderView *header = ui->messages->horizontalHeader(); + for (int i = 0; i < DSCDEMOD_COLUMNS; i++) + { + bool hidden = m_settings.m_columnSizes[i] == 0; + header->setSectionHidden(i, hidden); + m_menu->actions().at(i)->setChecked(!hidden); + if (m_settings.m_columnSizes[i] > 0) + ui->messages->setColumnWidth(i, m_settings.m_columnSizes[i]); + header->moveSection(header->visualIndex(i), m_settings.m_columnIndexes[i]); + } + + filter(); + + getRollupContents()->restoreState(m_rollupState); + updateAbsoluteCenterFrequency(); + blockApplySettings(false); +} + +void DSCDemodGUI::leaveEvent(QEvent* event) +{ + m_channelMarker.setHighlighted(false); + ChannelGUI::leaveEvent(event); +} + +void DSCDemodGUI::enterEvent(EnterEventType* event) +{ + m_channelMarker.setHighlighted(true); + ChannelGUI::enterEvent(event); +} + +void DSCDemodGUI::tick() +{ + double magsqAvg, magsqPeak; + int nbMagsqSamples; + m_dscDemod->getMagSqLevels(magsqAvg, magsqPeak, nbMagsqSamples); + double powDbAvg = CalcDb::dbPower(magsqAvg); + double powDbPeak = CalcDb::dbPower(magsqPeak); + ui->channelPowerMeter->levelChanged( + (100.0f + powDbAvg) / 100.0f, + (100.0f + powDbPeak) / 100.0f, + nbMagsqSamples); + + if (m_tickCount % 4 == 0) { + ui->channelPower->setText(QString::number(powDbAvg, 'f', 1)); + } + + m_tickCount++; +} + +void DSCDemodGUI::on_feed_clicked(bool checked) +{ + m_settings.m_feed = checked; + applySettings(); +} + +void DSCDemodGUI::on_feed_rightClicked(const QPoint &point) +{ + QString id = MainCore::instance()->getSettings().getStationName(); + QString url = QString("http://yaddnet.org/pages/php/live_rx.php?rxid=%1").arg(id); + QDesktopServices::openUrl(QUrl(url)); +} + +void DSCDemodGUI::on_logEnable_clicked(bool checked) +{ + m_settings.m_logEnabled = checked; + applySettings(); +} + +void DSCDemodGUI::on_logFilename_clicked() +{ + // Get filename to save to + QFileDialog fileDialog(nullptr, "Select file to log received messages to", "", "*.csv"); + fileDialog.setAcceptMode(QFileDialog::AcceptSave); + if (fileDialog.exec()) + { + QStringList fileNames = fileDialog.selectedFiles(); + if (fileNames.size() > 0) + { + m_settings.m_logFilename = fileNames[0]; + ui->logFilename->setToolTip(QString(".csv log filename: %1").arg(m_settings.m_logFilename)); + applySettings(); + } + } +} + +// Read .csv log and process as received messages +void DSCDemodGUI::on_logOpen_clicked() +{ + QFileDialog fileDialog(nullptr, "Select .csv log file to read", "", "*.csv"); + if (fileDialog.exec()) + { + QStringList fileNames = fileDialog.selectedFiles(); + if (fileNames.size() > 0) + { + QFile file(fileNames[0]); + if (file.open(QIODevice::ReadOnly | QIODevice::Text)) + { + QTextStream in(&file); + QString error; + QHash colIndexes = CSV::readHeader(in, {"Date", "Time", "Message", "Errors", "RSSI"}, error); + if (error.isEmpty()) + { + int dateCol = colIndexes.value("Date"); + int timeCol = colIndexes.value("Time"); + int messageCol = colIndexes.value("Message"); + int errorsCol = colIndexes.value("Errors"); + int rssiCol = colIndexes.value("RSSI"); + int maxCol = std::max({dateCol, timeCol, messageCol, errorsCol, rssiCol}); + + QMessageBox dialog(this); + dialog.setText("Reading message data"); + dialog.addButton(QMessageBox::Cancel); + dialog.show(); + QApplication::processEvents(); + int count = 0; + bool cancelled = false; + QStringList cols; + while (!cancelled && CSV::readRow(in, &cols)) + { + if (cols.size() > maxCol) + { + QDate date = QDate::fromString(cols[dateCol]); + QTime time = QTime::fromString(cols[timeCol]); + QDateTime dateTime(date, time); + QString messageHex = cols[messageCol]; + QByteArray bytes = QByteArray::fromHex(messageHex.toLatin1()); + DSCMessage message(bytes, dateTime); + int errors = cols[errorsCol].toInt(); + float rssi = cols[rssiCol].toFloat(); + messageReceived(message, errors, rssi); + if (count % 1000 == 0) + { + QApplication::processEvents(); + if (dialog.clickedButton()) { + cancelled = true; + } + } + count++; + } + } + dialog.close(); + } + else + { + QMessageBox::critical(this, "DSC Demod", error); + } + } + else + { + QMessageBox::critical(this, "DSC Demod", QString("Failed to open file %1").arg(fileNames[0])); + } + } + } +} + +void DSCDemodGUI::makeUIConnections() +{ + QObject::connect(ui->deltaFrequency, &ValueDialZ::changed, this, &DSCDemodGUI::on_deltaFrequency_changed); + QObject::connect(ui->filterInvalid, &ButtonSwitch::clicked, this, &DSCDemodGUI::on_filterInvalid_clicked); + QObject::connect(ui->filterColumn, QOverload::of(&QComboBox::currentIndexChanged), this, &DSCDemodGUI::on_filterColumn_currentIndexChanged); + QObject::connect(ui->filter, &QLineEdit::editingFinished, this, &DSCDemodGUI::on_filter_editingFinished); + QObject::connect(ui->clearTable, &QPushButton::clicked, this, &DSCDemodGUI::on_clearTable_clicked); + QObject::connect(ui->udpEnabled, &QCheckBox::clicked, this, &DSCDemodGUI::on_udpEnabled_clicked); + QObject::connect(ui->udpAddress, &QLineEdit::editingFinished, this, &DSCDemodGUI::on_udpAddress_editingFinished); + QObject::connect(ui->udpPort, &QLineEdit::editingFinished, this, &DSCDemodGUI::on_udpPort_editingFinished); + QObject::connect(ui->logEnable, &ButtonSwitch::clicked, this, &DSCDemodGUI::on_logEnable_clicked); + QObject::connect(ui->logFilename, &QToolButton::clicked, this, &DSCDemodGUI::on_logFilename_clicked); + QObject::connect(ui->logOpen, &QToolButton::clicked, this, &DSCDemodGUI::on_logOpen_clicked); + QObject::connect(ui->feed, &ButtonSwitch::clicked, this, &DSCDemodGUI::on_feed_clicked); +} + +void DSCDemodGUI::updateAbsoluteCenterFrequency() +{ + setStatusFrequency(m_deviceCenterFrequency + m_settings.m_inputFrequencyOffset); +} + +void DSCDemodGUI::aprsFiDataUpdated(const QList& data) +{ + for (int i = ui->messages->rowCount() - 1; i >= 0; i--) + { + bool match = false; + QString mmsi; + + for (const auto& item : data) + { + mmsi = ui->messages->item(i, MESSAGE_COL_ADDRESS)->text(); + if (mmsi == item.m_mmsi) + { + ui->messages->item(i, MESSAGE_COL_ADDRESS_NAME)->setText(item.m_name); + match = true; + } + mmsi = ui->messages->item(i, MESSAGE_COL_SELF_ID)->text(); + if (mmsi == item.m_mmsi) + { + ui->messages->item(i, MESSAGE_COL_SELF_ID_NAME)->setText(item.m_name); + + if ((item.m_latitude != 0.0) || (item.m_longitude != 0.0)) + { + // Calculate distance from My Position to position of source of message + Real stationLatitude = MainCore::instance()->getSettings().getLatitude(); + Real stationLongitude = MainCore::instance()->getSettings().getLongitude(); + Real stationAltitude = MainCore::instance()->getSettings().getAltitude(); + QGeoCoordinate stationPosition(stationLatitude, stationLongitude, stationAltitude); + QGeoCoordinate shipPosition(item.m_latitude, item.m_longitude, 0.0); + + float distance = stationPosition.distanceTo(shipPosition); + + ui->messages->item(i, MESSAGE_COL_SELF_ID_RANGE)->setData(Qt::DisplayRole, (int)std::round(distance / 1000.0)); + } + + match = true; + } + } + if (match) { + break; + } + } +} diff --git a/plugins/channelrx/demoddsc/dscdemodgui.h b/plugins/channelrx/demoddsc/dscdemodgui.h new file mode 100644 index 000000000..c3297bcdc --- /dev/null +++ b/plugins/channelrx/demoddsc/dscdemodgui.h @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#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& data); +}; + +#endif // INCLUDE_DSCDEMODGUI_H + diff --git a/plugins/channelrx/demoddsc/dscdemodgui.ui b/plugins/channelrx/demoddsc/dscdemodgui.ui new file mode 100644 index 000000000..cb1a0a848 --- /dev/null +++ b/plugins/channelrx/demoddsc/dscdemodgui.ui @@ -0,0 +1,982 @@ + + + DSCDemodGUI + + + + 0 + 0 + 411 + 751 + + + + + 0 + 0 + + + + + 352 + 0 + + + + + Liberation Sans + 9 + + + + Qt::StrongFocus + + + Packet Demodulator + + + + + 0 + 0 + 390 + 121 + + + + + 350 + 0 + + + + Settings + + + + 3 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + 2 + + + + + + 16 + 0 + + + + Df + + + + + + + + 0 + 0 + + + + + 32 + 16 + + + + + Liberation Mono + 12 + + + + PointingHandCursor + + + Qt::StrongFocus + + + Demod shift frequency from center in Hz + + + + + + + Hz + + + + + + + Qt::Vertical + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Channel power + + + Qt::RightToLeft + + + 0.0 + + + + + + + dB + + + + + + + + + + + + + dB + + + + + + + + 0 + 0 + + + + + 0 + 24 + + + + + Liberation Mono + 8 + + + + Level meter (dB) top trace: average, bottom trace: instantaneous peak, tip: peak hold + + + + + + + + + Qt::Horizontal + + + + + + + + + UDP + + + + + + + Send messages via UDP + + + Qt::RightToLeft + + + + + + + + + + + 120 + 0 + + + + Qt::ClickFocus + + + Destination UDP address + + + + + + 127.0.0.1 + + + + + + + : + + + Qt::AlignCenter + + + + + + + + 50 + 0 + + + + + 50 + 16777215 + + + + Qt::ClickFocus + + + Destination UDP port + + + 00000 + + + 4530 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Horizontal + + + + + + + + + Filter + + + + + + + + 0 + 0 + + + + + 80 + 0 + + + + Display messages only from the specified station + + + + Date + + + + + Time + + + + + Format + + + + + To + + + + + To Country + + + + + To Type + + + + + To Name + + + + + Category + + + + + From + + + + + From Country + + + + + From Type + + + + + From Name + + + + + Range + + + + + Telecommand 1 + + + + + Telecommand 2 + + + + + RX + + + + + TX + + + + + Position + + + + + Distress Id + + + + + Distress + + + + + Number + + + + + Time + + + + + Comms + + + + + EOS + + + + + ECC + + + + + Errors + + + + + Valid + + + + + RSSI + + + + + + + + Filter regular expression + + + + + + + + 24 + 16777215 + + + + When checked, invalid messages are filtered from the table + + + + + + + :/funnel.png:/funnel.png + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 24 + 16777215 + + + + Feed messages to yaddnet.org + + + + + + + :/txon.png:/txon.png + + + + + + + + 24 + 16777215 + + + + Start/stop logging of received messages to .csv file + + + + + + + :/record_off.png:/record_off.png + + + + + + + Set log .csv filename + + + ... + + + + :/save.png:/save.png + + + false + + + + + + + Read messages from .csv log file + + + ... + + + + :/load.png:/load.png + + + false + + + + + + + Clear messages + + + + + + + :/bin.png:/bin.png + + + + + + + + + + + 10 + 140 + 381 + 241 + + + + + 0 + 0 + + + + Received Messages + + + + + + Received messages + + + QAbstractItemView::NoEditTriggers + + + + Date + + + Local date message was received + + + + + Time + + + Local time message was received + + + + + Format + + + Format specifier + + + + + To + + + Address (MMSI or coordinates) of who the message is to + + + + + Country + + + Country with jurisdiction of the destination of the message + + + + + Type + + + MMSI type of the destination of the message + + + + + Name + + + Name of the station the message is to + + + + + Category + + + Message category + + + + + 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 + + + + + Name + + + Name of the station the message is from + + + + + Range (km) + + + Distance in kilometers from My Position to ship's position obtained from aprs.fi + + + + + Telecommand 1 + + + Telecommand + + + + + Telecommand 2 + + + 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 + + + + + Number + + + Telephone number + + + + + Time + + + UTC Time + + + + + Comms + + + Subsequent communications + + + + + EOS + + + End of Signal + + + + + ECC + + + Error checking code + + + + + Errors + + + Number of symbols received with errors (which may have been corrected if ECC OK) + + + + + Valid + + + Whether the message is determined to be valid (contains no detected errors) + + + + + RSSI + + + Received signal strenth indicator (Average power in dBFS) + + + + + + + + + + 10 + 390 + 716 + 341 + + + + + 714 + 0 + + + + Waveforms + + + + 2 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + 200 + 250 + + + + + Liberation Mono + 8 + + + + + + + + + + + + + ButtonSwitch + QToolButton +
gui/buttonswitch.h
+
+ + RollupContents + QWidget +
gui/rollupcontents.h
+ 1 +
+ + ValueDialZ + QWidget +
gui/valuedialz.h
+ 1 +
+ + LevelMeterSignalDB + QWidget +
gui/levelmeter.h
+ 1 +
+ + GLScope + QWidget +
gui/glscope.h
+ 1 +
+ + GLScopeGUI + QWidget +
gui/glscopegui.h
+ 1 +
+
+ + deltaFrequency + udpEnabled + filterColumn + logEnable + logFilename + logOpen + clearTable + messages + + + + + +
diff --git a/plugins/channelrx/demoddsc/dscdemodplugin.cpp b/plugins/channelrx/demoddsc/dscdemodplugin.cpp new file mode 100644 index 000000000..48627aa54 --- /dev/null +++ b/plugins/channelrx/demoddsc/dscdemodplugin.cpp @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#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(); +} + diff --git a/plugins/channelrx/demoddsc/dscdemodplugin.h b/plugins/channelrx/demoddsc/dscdemodplugin.h new file mode 100644 index 000000000..18f2ab2bd --- /dev/null +++ b/plugins/channelrx/demoddsc/dscdemodplugin.h @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_DSCDEMODPLUGIN_H +#define INCLUDE_DSCDEMODPLUGIN_H + +#include +#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 + diff --git a/plugins/channelrx/demoddsc/dscdemodsettings.cpp b/plugins/channelrx/demoddsc/dscdemodsettings.cpp new file mode 100644 index 000000000..2562ab281 --- /dev/null +++ b/plugins/channelrx/demoddsc/dscdemodsettings.cpp @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#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; + } +} + diff --git a/plugins/channelrx/demoddsc/dscdemodsettings.h b/plugins/channelrx/demoddsc/dscdemodsettings.h new file mode 100644 index 000000000..9d793333a --- /dev/null +++ b/plugins/channelrx/demoddsc/dscdemodsettings.h @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_DSCDEMODSETTINGS_H +#define INCLUDE_DSCDEMODSETTINGS_H + +#include + +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 */ diff --git a/plugins/channelrx/demoddsc/dscdemodsink.cpp b/plugins/channelrx/demoddsc/dscdemodsink.cpp new file mode 100644 index 000000000..695dcab4f --- /dev/null +++ b/plugins/channelrx/demoddsc/dscdemodsink.cpp @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include + +#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 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::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; +} diff --git a/plugins/channelrx/demoddsc/dscdemodsink.h b/plugins/channelrx/demoddsc/dscdemodsink.h new file mode 100644 index 000000000..3cce471c5 --- /dev/null +++ b/plugins/channelrx/demoddsc/dscdemodsink.h @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_DSCDEMODSINK_H +#define INCLUDE_DSCDEMODSINK_H + +#include +#include +#include + +#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 m_movingAverage; + + Lowpass m_lowpassComplex1; + Lowpass m_lowpassComplex2; + MovingMaximum m_movMax1; + MovingMaximum 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 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 + diff --git a/plugins/channelrx/demoddsc/dscdemodwebapiadapter.cpp b/plugins/channelrx/demoddsc/dscdemodwebapiadapter.cpp new file mode 100644 index 000000000..91fb5f0ea --- /dev/null +++ b/plugins/channelrx/demoddsc/dscdemodwebapiadapter.cpp @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#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; +} diff --git a/plugins/channelrx/demoddsc/dscdemodwebapiadapter.h b/plugins/channelrx/demoddsc/dscdemodwebapiadapter.h new file mode 100644 index 000000000..cf75c968d --- /dev/null +++ b/plugins/channelrx/demoddsc/dscdemodwebapiadapter.h @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#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 diff --git a/plugins/channelrx/demoddsc/readme.md b/plugins/channelrx/demoddsc/readme.md new file mode 100644 index 000000000..4e22d6663 --- /dev/null +++ b/plugins/channelrx/demoddsc/readme.md @@ -0,0 +1,116 @@ +

DSC (Digital Selective Calling) Demodulator Plugin

+ +

Introduction

+ +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). + +

Interface

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

1: Frequency shift from center frequency of reception

+ +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. + +

2: Channel power

+ +Average total power in dB relative to a +/- 1.0 amplitude signal received in the pass band. + +

3: Level meter in dB

+ + - top bar (green): average value + - bottom bar (blue green): instantaneous peak value + - tip vertical bar (bright green): peak hold value + +

4: UDP

+ +When checked, received messages are forwarded to the specified UDP address (5) and port (6). + +

5: UDP address

+ +IP address of the host to forward received messages to via UDP. + +

6: UDP port

+ +UDP port number to forward received messages to. + +

7: Filter

+ +This drop down displays a list of all columns which can be used for filtering (8). + +

8: Filter Reg Exp

+ +Specifes a regular expression used to filter data in the table, using data in the column specified by (8). + +

9: Filter Invalid

+ +When checked, invalid messages will be filtered from the table. + +

10: Feed to YaDDNet

+ +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. + +

11: Start/stop Logging Messages to .csv File

+ +When checked, writes all received messages to a .csv file, specified by (12). + +

12: .csv Log Filename

+ +Click to specify the name of the .csv file which received messasges are logged to. + +

13: Read Data from .csv File

+ +Click to specify a previously written .csv log file, which is read and used to update the table. + +

12: Received Messages Table

+ +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. diff --git a/sdrbase/CMakeLists.txt b/sdrbase/CMakeLists.txt index c82864707..eca2ad3c4 100644 --- a/sdrbase/CMakeLists.txt +++ b/sdrbase/CMakeLists.txt @@ -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 diff --git a/sdrbase/util/aprsfi.cpp b/sdrbase/util/aprsfi.cpp new file mode 100644 index 000000000..00a993c26 --- /dev/null +++ b/sdrbase/util/aprsfi.cpp @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "aprsfi.h" + +#include +#include +#include +#include +#include +#include + +QMutex APRSFi::m_mutex; +QHash 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 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 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"; + } +} diff --git a/sdrbase/util/aprsfi.h b/sdrbase/util/aprsfi.h new file mode 100644 index 000000000..32caa4926 --- /dev/null +++ b/sdrbase/util/aprsfi.h @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_APRSFI_H +#define INCLUDE_APRSFI_H + +#include +#include + +#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& 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 m_aisCache; +}; + +#endif /* INCLUDE_APRSFI_H */ diff --git a/sdrbase/util/dsc.cpp b/sdrbase/util/dsc.cpp new file mode 100644 index 000000000..6f55b77ce --- /dev/null +++ b/sdrbase/util/dsc.cpp @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "util/dsc.h" +#include "util/popcount.h" + +// "short" strings are meant to be compatible with YaDDNet + +QMap 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::m_formatSpecifierShortStrings = { + {GEOGRAPHIC_CALL, "AREA"}, + {DISTRESS_ALERT, "DIS"}, + {GROUP_CALL, "GRP"}, + {ALL_SHIPS, "ALL"}, + {SELECTIVE_CALL, "SEL"}, + {AUTOMATIC_CALL, "AUT"} +}; + +QMap DSCMessage::m_categoryStrings = { + {ROUTINE, "Routine"}, + {SAFETY, "Safety"}, + {URGENCY, "Urgency"}, + {DISTRESS, "Distress"} +}; + +QMap DSCMessage::m_categoryShortStrings = { + {ROUTINE, "RTN"}, + {SAFETY, "SAF"}, + {URGENCY, "URG"}, + {DISTRESS, "DIS"} +}; + +QMap 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::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::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::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::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::m_endOfSignalStrings = { + {REQ, "Req ACK"}, + {ACK, "ACK"}, + {EOS, "EOS"} +}; + +QMap 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; +} diff --git a/sdrbase/util/dsc.h b/sdrbase/util/dsc.h new file mode 100644 index 000000000..04007dd49 --- /dev/null +++ b/sdrbase/util/dsc.h @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_UTIL_DSC_H +#define INCLUDE_UTIL_DSC_H + +#include "export.h" + +#include +#include +#include + +// 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 m_formatSpecifierStrings; + static QMap m_formatSpecifierShortStrings; + static QMap m_categoryStrings; + static QMap m_categoryShortStrings; + static QMap m_telecommand1Strings; + static QMap m_telecommand1ShortStrings; + static QMap m_telecommand2Strings; + static QMap m_telecommand2ShortStrings; + static QMap m_distressNatureStrings; + static QMap m_endOfSignalStrings; + static QMap 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 */ diff --git a/sdrbase/util/mmsi.cpp b/sdrbase/util/mmsi.cpp new file mode 100644 index 000000000..526c1840a --- /dev/null +++ b/sdrbase/util/mmsi.cpp @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include + +#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 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"; +} diff --git a/sdrbase/util/mmsi.h b/sdrbase/util/mmsi.h new file mode 100644 index 000000000..14ed823ab --- /dev/null +++ b/sdrbase/util/mmsi.h @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef UTIL_MMSI_H +#define UTIL_MMSI_H + +#include +#include + +#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 m_mid; + + static void checkFlags(); +}; + +#endif /* UTIL_MMSI_H */ diff --git a/sdrbase/util/movingaverage.h b/sdrbase/util/movingaverage.h index b37afd1e2..758528b5a 100644 --- a/sdrbase/util/movingaverage.h +++ b/sdrbase/util/movingaverage.h @@ -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 m_samples; diff --git a/sdrbase/util/osndb.cpp b/sdrbase/util/osndb.cpp index 5c6998a7f..0b556cda9 100644 --- a/sdrbase/util/osndb.cpp +++ b/sdrbase/util/osndb.cpp @@ -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)) diff --git a/sdrbase/util/osndb.h b/sdrbase/util/osndb.h index 9a6ede114..1b2f5fb9f 100644 --- a/sdrbase/util/osndb.h +++ b/sdrbase/util/osndb.h @@ -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); diff --git a/sdrbase/webapi/webapirequestmapper.cpp b/sdrbase/webapi/webapirequestmapper.cpp index 94d15364d..c9cfff591 100644 --- a/sdrbase/webapi/webapirequestmapper.cpp +++ b/sdrbase/webapi/webapirequestmapper.cpp @@ -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()); diff --git a/sdrbase/webapi/webapiutils.cpp b/sdrbase/webapi/webapiutils.cpp index 500713f76..c5e93a86e 100644 --- a/sdrbase/webapi/webapiutils.cpp +++ b/sdrbase/webapi/webapiutils.cpp @@ -41,6 +41,7 @@ const QMap 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 WebAPIUtils::m_channelTypeToSettingsKey = { {"DATVMod", "DATVModSettings"}, {"DABDemod", "DABDemodSettings"}, {"DOA2", "DOA2Settings"}, + {"DSCDemod", "DSCDemodSettings"}, {"DSDDemod", "DSDDemodSettings"}, {"FileSink", "FileSinkSettings"}, {"FileSource", "FileSourceSettings"}, diff --git a/sdrgui/CMakeLists.txt b/sdrgui/CMakeLists.txt index 3e8adea62..ebaa856d6 100644 --- a/sdrgui/CMakeLists.txt +++ b/sdrgui/CMakeLists.txt @@ -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 diff --git a/sdrgui/gui/frequencydelegate.cpp b/sdrgui/gui/frequencydelegate.cpp new file mode 100644 index 000000000..3b063ad81 --- /dev/null +++ b/sdrgui/gui/frequencydelegate.cpp @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#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(); + } +} diff --git a/sdrgui/gui/frequencydelegate.h b/sdrgui/gui/frequencydelegate.h new file mode 100644 index 000000000..4899bbee6 --- /dev/null +++ b/sdrgui/gui/frequencydelegate.h @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef SDRGUI_GUI_FREQUENCYDELGATE_H +#define SDRGUI_GUI_FREQUENCYDELGATE_H + +#include + +#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 diff --git a/swagger/sdrangel/api/swagger/include/ChannelReport.yaml b/swagger/sdrangel/api/swagger/include/ChannelReport.yaml index 5c55e0b53..1489c596a 100644 --- a/swagger/sdrangel/api/swagger/include/ChannelReport.yaml +++ b/swagger/sdrangel/api/swagger/include/ChannelReport.yaml @@ -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: diff --git a/swagger/sdrangel/api/swagger/include/ChannelSettings.yaml b/swagger/sdrangel/api/swagger/include/ChannelSettings.yaml index daa36f511..7f671b0d2 100644 --- a/swagger/sdrangel/api/swagger/include/ChannelSettings.yaml +++ b/swagger/sdrangel/api/swagger/include/ChannelSettings.yaml @@ -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: diff --git a/swagger/sdrangel/code/qt5/client/SWGChannelReport.cpp b/swagger/sdrangel/code/qt5/client/SWGChannelReport.cpp index e47906200..a99447f1a 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelReport.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGChannelReport.cpp @@ -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; } diff --git a/swagger/sdrangel/code/qt5/client/SWGChannelReport.h b/swagger/sdrangel/code/qt5/client/SWGChannelReport.h index 863e38004..cdbac4782 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelReport.h +++ b/swagger/sdrangel/code/qt5/client/SWGChannelReport.h @@ -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; diff --git a/swagger/sdrangel/code/qt5/client/SWGChannelSettings.cpp b/swagger/sdrangel/code/qt5/client/SWGChannelSettings.cpp index 849d66b0a..9ddb27740 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelSettings.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGChannelSettings.cpp @@ -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; } diff --git a/swagger/sdrangel/code/qt5/client/SWGChannelSettings.h b/swagger/sdrangel/code/qt5/client/SWGChannelSettings.h index f0ddcab8d..8cf514dba 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelSettings.h +++ b/swagger/sdrangel/code/qt5/client/SWGChannelSettings.h @@ -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; diff --git a/swagger/sdrangel/code/qt5/client/SWGModelFactory.h b/swagger/sdrangel/code/qt5/client/SWGModelFactory.h index c628a7f18..4945e7ee9 100644 --- a/swagger/sdrangel/code/qt5/client/SWGModelFactory.h +++ b/swagger/sdrangel/code/qt5/client/SWGModelFactory.h @@ -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();