diff --git a/doc/img/RadiosondeDemod_plugin.png b/doc/img/RadiosondeDemod_plugin.png new file mode 100644 index 000000000..4998c683a Binary files /dev/null and b/doc/img/RadiosondeDemod_plugin.png differ diff --git a/doc/img/RadiosondeDemod_plugin_table.png b/doc/img/RadiosondeDemod_plugin_table.png new file mode 100644 index 000000000..44740689a Binary files /dev/null and b/doc/img/RadiosondeDemod_plugin_table.png differ diff --git a/doc/img/Radiosonde_plugin.png b/doc/img/Radiosonde_plugin.png new file mode 100644 index 000000000..f7def9822 Binary files /dev/null and b/doc/img/Radiosonde_plugin.png differ diff --git a/doc/img/Radiosonde_plugin_map.png b/doc/img/Radiosonde_plugin_map.png new file mode 100644 index 000000000..615731d04 Binary files /dev/null and b/doc/img/Radiosonde_plugin_map.png differ diff --git a/plugins/channelrx/CMakeLists.txt b/plugins/channelrx/CMakeLists.txt index 9d871f209..7c8c51318 100644 --- a/plugins/channelrx/CMakeLists.txt +++ b/plugins/channelrx/CMakeLists.txt @@ -21,6 +21,7 @@ add_subdirectory(demodais) add_subdirectory(demodpager) add_subdirectory(radioclock) add_subdirectory(radioastronomy) +add_subdirectory(demodradiosonde) if(DAB_FOUND AND ZLIB_FOUND AND FAAD_FOUND) add_subdirectory(demoddab) diff --git a/plugins/channelrx/demodradiosonde/CMakeLists.txt b/plugins/channelrx/demodradiosonde/CMakeLists.txt new file mode 100644 index 000000000..4301cd726 --- /dev/null +++ b/plugins/channelrx/demodradiosonde/CMakeLists.txt @@ -0,0 +1,68 @@ +project(demodradiosonde) + +set(demodradiosonde_SOURCES + radiosondedemod.cpp + radiosondedemodsettings.cpp + radiosondedemodbaseband.cpp + radiosondedemodsink.cpp + radiosondedemodplugin.cpp + radiosondedemodwebapiadapter.cpp +) + +set(demodradiosonde_HEADERS + radiosondedemod.h + radiosondedemodsettings.h + radiosondedemodbaseband.h + radiosondedemodsink.h + radiosondedemodplugin.h + radiosondedemodwebapiadapter.h +) + +include_directories( + ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client +) + +if(NOT SERVER_MODE) + set(demodradiosonde_SOURCES + ${demodradiosonde_SOURCES} + radiosondedemodgui.cpp + radiosondedemodgui.ui + ) + set(demodradiosonde_HEADERS + ${demodradiosonde_HEADERS} + radiosondedemodgui.h + ) + + set(TARGET_NAME demodradiosonde) + set(TARGET_LIB "Qt5::Widgets") + set(TARGET_LIB_GUI "sdrgui") + set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR}) +else() + set(TARGET_NAME demodradiosondesrv) + set(TARGET_LIB "") + set(TARGET_LIB_GUI "") + set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR}) +endif() + +add_library(${TARGET_NAME} SHARED + ${demodradiosonde_SOURCES} +) + +target_link_libraries(${TARGET_NAME} + Qt5::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() + +# Install debug symbols +if (WIN32) + install(FILES $ CONFIGURATIONS Debug RelWithDebInfo DESTINATION ${INSTALL_FOLDER} ) +endif() diff --git a/plugins/channelrx/demodradiosonde/radiosondedemod.cpp b/plugins/channelrx/demodradiosonde/radiosondedemod.cpp new file mode 100644 index 000000000..5e965291c --- /dev/null +++ b/plugins/channelrx/demodradiosonde/radiosondedemod.cpp @@ -0,0 +1,715 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2015-2018 Edouard Griffiths, F4EXB. // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "radiosondedemod.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "SWGChannelSettings.h" +#include "SWGChannelReport.h" + +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "device/deviceapi.h" +#include "feature/feature.h" +#include "util/db.h" +#include "maincore.h" + +MESSAGE_CLASS_DEFINITION(RadiosondeDemod::MsgConfigureRadiosondeDemod, Message) +MESSAGE_CLASS_DEFINITION(RadiosondeDemod::MsgMessage, Message) + +const char * const RadiosondeDemod::m_channelIdURI = "sdrangel.channel.radiosondedemod"; +const char * const RadiosondeDemod::m_channelId = "RadiosondeDemod"; + +RadiosondeDemod::RadiosondeDemod(DeviceAPI *deviceAPI) : + ChannelAPI(m_channelIdURI, ChannelAPI::StreamSingleSink), + m_deviceAPI(deviceAPI), + m_basebandSampleRate(0) +{ + setObjectName(m_channelId); + + m_basebandSink = new RadiosondeDemodBaseband(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(); + connect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*))); + connect(&m_channelMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleChannelMessages())); +} + +RadiosondeDemod::~RadiosondeDemod() +{ + qDebug("RadiosondeDemod::~RadiosondeDemod"); + disconnect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*))); + delete m_networkManager; + m_deviceAPI->removeChannelSinkAPI(this); + m_deviceAPI->removeChannelSink(this); + + if (m_basebandSink->isRunning()) { + stop(); + } + + delete m_basebandSink; +} + +uint32_t RadiosondeDemod::getNumberOfDeviceStreams() const +{ + return m_deviceAPI->getNbSourceStreams(); +} + +void RadiosondeDemod::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, bool firstOfBurst) +{ + (void) firstOfBurst; + m_basebandSink->feed(begin, end); +} + +void RadiosondeDemod::start() +{ + qDebug("RadiosondeDemod::start"); + + m_basebandSink->reset(); + m_basebandSink->startWork(); + m_thread.start(); + + DSPSignalNotification *dspMsg = new DSPSignalNotification(m_basebandSampleRate, m_centerFrequency); + m_basebandSink->getInputMessageQueue()->push(dspMsg); + + RadiosondeDemodBaseband::MsgConfigureRadiosondeDemodBaseband *msg = RadiosondeDemodBaseband::MsgConfigureRadiosondeDemodBaseband::create(m_settings, true); + m_basebandSink->getInputMessageQueue()->push(msg); +} + +void RadiosondeDemod::stop() +{ + qDebug("RadiosondeDemod::stop"); + m_basebandSink->stopWork(); + m_thread.quit(); + m_thread.wait(); +} + +bool RadiosondeDemod::handleMessage(const Message& cmd) +{ + if (MsgConfigureRadiosondeDemod::match(cmd)) + { + MsgConfigureRadiosondeDemod& cfg = (MsgConfigureRadiosondeDemod&) cmd; + qDebug() << "RadiosondeDemod::handleMessage: MsgConfigureRadiosondeDemod"; + 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() << "RadiosondeDemod::handleMessage: DSPSignalNotification"; + m_basebandSink->getInputMessageQueue()->push(rep); + + return true; + } + else if (MsgMessage::match(cmd)) + { + MsgMessage& report = (MsgMessage&)cmd; + + // Decode the message + RS41Frame *frame = RS41Frame::decode(report.getMessage()); + RS41Subframe *subframe = nullptr; + + if (m_subframes.contains(frame->m_serial)) + { + subframe = m_subframes.value(frame->m_serial); + } + else + { + subframe = new RS41Subframe(); + m_subframes.insert(frame->m_serial, subframe); + } + subframe->update(frame); + + // Forward to GUI + if (getMessageQueueToGUI()) + { + MsgMessage *msg = new MsgMessage(report); + getMessageQueueToGUI()->push(msg); + } + + MessagePipesLegacy& messagePipes = MainCore::instance()->getMessagePipes(); + + // Forward to Radiosonde feature + QList *radiosondeMessageQueues = messagePipes.getMessageQueues(this, "radiosonde"); + if (radiosondeMessageQueues) + { + QList::iterator it = radiosondeMessageQueues->begin(); + for (; it != radiosondeMessageQueues->end(); ++it) + { + MainCore::MsgPacket *msg = MainCore::MsgPacket::create(this, report.getMessage(), report.getDateTime()); + (*it)->push(msg); + } + } + + // Forward via UDP + if (m_settings.m_udpEnabled) + { + m_udpSocket.writeDatagram(report.getMessage().data(), report.getMessage().size(), + QHostAddress(m_settings.m_udpAddress), m_settings.m_udpPort); + } + + // Write to log file + if (m_logFile.isOpen()) + { + + m_logStream << report.getDateTime().date().toString() << "," + << report.getDateTime().time().toString() << "," + << report.getMessage().toHex() << ","; + + if (frame->m_statusValid) + { + m_logStream << frame->m_serial << "," + << frame->m_frameNumber << ","; + } + else + { + m_logStream << ",,"; + } + if (frame->m_posValid) + { + m_logStream << frame->m_latitude << "," + << frame->m_longitude << ","; + } + else + { + m_logStream << ",,"; + } + if (frame->m_measValid) + { + m_logStream << frame->getPressureString(subframe) << "," + << frame->getTemperatureString(subframe) << "," + << frame->getHumidityString(subframe) << ","; + } + else + { + m_logStream << ",,,"; + } + m_logStream << "\n"; + } + + delete frame; + + return true; + } + else if (MainCore::MsgChannelDemodQuery::match(cmd)) + { + qDebug() << "RadiosondeDemod::handleMessage: MsgChannelDemodQuery"; + sendSampleRateToDemodAnalyzer(); + + return true; + } + else + { + return false; + } +} + +ScopeVis *RadiosondeDemod::getScopeSink() +{ + return m_basebandSink->getScopeSink(); +} + +void RadiosondeDemod::setCenterFrequency(qint64 frequency) +{ + RadiosondeDemodSettings settings = m_settings; + settings.m_inputFrequencyOffset = frequency; + applySettings(settings, false); + + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureRadiosondeDemod *msgToGUI = MsgConfigureRadiosondeDemod::create(settings, false); + m_guiMessageQueue->push(msgToGUI); + } +} + +void RadiosondeDemod::applySettings(const RadiosondeDemodSettings& settings, bool force) +{ + qDebug() << "RadiosondeDemod::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_baud != m_settings.m_baud) || force) { + reverseAPIKeys.append("baud"); + } + if ((settings.m_inputFrequencyOffset != m_settings.m_inputFrequencyOffset) || force) { + reverseAPIKeys.append("inputFrequencyOffset"); + } + if ((settings.m_rfBandwidth != m_settings.m_rfBandwidth) || force) { + reverseAPIKeys.append("rfBandwidth"); + } + if ((settings.m_fmDeviation != m_settings.m_fmDeviation) || force) { + reverseAPIKeys.append("fmDeviation"); + } + if ((settings.m_correlationThreshold != m_settings.m_correlationThreshold) || force) { + reverseAPIKeys.append("correlationThreshold"); + } + 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"); + } + + RadiosondeDemodBaseband::MsgConfigureRadiosondeDemodBaseband *msg = RadiosondeDemodBaseband::MsgConfigureRadiosondeDemodBaseband::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() << "RadiosondeDemod::applySettings - Logging to: " << settings.m_logFilename; + bool newFile = m_logFile.size() == 0; + m_logStream.setDevice(&m_logFile); + if (newFile) + { + // Write header + m_logStream << "Date,Time,Data,Serial,Frame,Lat,Lon,P (hPa),T (C), U (%)\n"; + } + } + else + { + qDebug() << "RadiosondeDemod::applySettings - Unable to open log file: " << settings.m_logFilename; + } + } + } + + m_settings = settings; +} + +QByteArray RadiosondeDemod::serialize() const +{ + return m_settings.serialize(); +} + +bool RadiosondeDemod::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + MsgConfigureRadiosondeDemod *msg = MsgConfigureRadiosondeDemod::create(m_settings, true); + m_inputMessageQueue.push(msg); + return true; + } + else + { + m_settings.resetToDefaults(); + MsgConfigureRadiosondeDemod *msg = MsgConfigureRadiosondeDemod::create(m_settings, true); + m_inputMessageQueue.push(msg); + return false; + } +} + +void RadiosondeDemod::sendSampleRateToDemodAnalyzer() +{ + QList *messageQueues = MainCore::instance()->getMessagePipes().getMessageQueues(this, "reportdemod"); + + if (messageQueues) + { + QList::iterator it = messageQueues->begin(); + + for (; it != messageQueues->end(); ++it) + { + MainCore::MsgChannelDemodReport *msg = MainCore::MsgChannelDemodReport::create( + this, + RadiosondeDemodSettings::RADIOSONDEDEMOD_CHANNEL_SAMPLE_RATE + ); + (*it)->push(msg); + } + } +} + +int RadiosondeDemod::webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setRadiosondeDemodSettings(new SWGSDRangel::SWGRadiosondeDemodSettings()); + response.getRadiosondeDemodSettings()->init(); + webapiFormatChannelSettings(response, m_settings); + return 200; +} + +int RadiosondeDemod::webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + RadiosondeDemodSettings settings = m_settings; + webapiUpdateChannelSettings(settings, channelSettingsKeys, response); + + MsgConfigureRadiosondeDemod *msg = MsgConfigureRadiosondeDemod::create(settings, force); + m_inputMessageQueue.push(msg); + + qDebug("RadiosondeDemod::webapiSettingsPutPatch: forward to GUI: %p", m_guiMessageQueue); + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureRadiosondeDemod *msgToGUI = MsgConfigureRadiosondeDemod::create(settings, force); + m_guiMessageQueue->push(msgToGUI); + } + + webapiFormatChannelSettings(response, settings); + + return 200; +} + +void RadiosondeDemod::webapiUpdateChannelSettings( + RadiosondeDemodSettings& settings, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response) +{ + if (channelSettingsKeys.contains("baud")) { + settings.m_baud = response.getRadiosondeDemodSettings()->getBaud(); + } + if (channelSettingsKeys.contains("inputFrequencyOffset")) { + settings.m_inputFrequencyOffset = response.getRadiosondeDemodSettings()->getInputFrequencyOffset(); + } + if (channelSettingsKeys.contains("rfBandwidth")) { + settings.m_rfBandwidth = response.getRadiosondeDemodSettings()->getRfBandwidth(); + } + if (channelSettingsKeys.contains("fmDeviation")) { + settings.m_fmDeviation = response.getRadiosondeDemodSettings()->getFmDeviation(); + } + if (channelSettingsKeys.contains("correlationThreshold")) { + settings.m_correlationThreshold = response.getRadiosondeDemodSettings()->getCorrelationThreshold(); + } + if (channelSettingsKeys.contains("udpEnabled")) { + settings.m_udpEnabled = response.getRadiosondeDemodSettings()->getUdpEnabled(); + } + if (channelSettingsKeys.contains("udpAddress")) { + settings.m_udpAddress = *response.getRadiosondeDemodSettings()->getUdpAddress(); + } + if (channelSettingsKeys.contains("udpPort")) { + settings.m_udpPort = response.getRadiosondeDemodSettings()->getUdpPort(); + } + if (channelSettingsKeys.contains("logFilename")) { + settings.m_logFilename = *response.getRadiosondeDemodSettings()->getLogFilename(); + } + if (channelSettingsKeys.contains("logEnabled")) { + settings.m_logEnabled = response.getRadiosondeDemodSettings()->getLogEnabled(); + } + if (channelSettingsKeys.contains("rgbColor")) { + settings.m_rgbColor = response.getRadiosondeDemodSettings()->getRgbColor(); + } + if (channelSettingsKeys.contains("title")) { + settings.m_title = *response.getRadiosondeDemodSettings()->getTitle(); + } + if (channelSettingsKeys.contains("streamIndex")) { + settings.m_streamIndex = response.getRadiosondeDemodSettings()->getStreamIndex(); + } + if (channelSettingsKeys.contains("useReverseAPI")) { + settings.m_useReverseAPI = response.getRadiosondeDemodSettings()->getUseReverseApi() != 0; + } + if (channelSettingsKeys.contains("reverseAPIAddress")) { + settings.m_reverseAPIAddress = *response.getRadiosondeDemodSettings()->getReverseApiAddress(); + } + if (channelSettingsKeys.contains("reverseAPIPort")) { + settings.m_reverseAPIPort = response.getRadiosondeDemodSettings()->getReverseApiPort(); + } + if (channelSettingsKeys.contains("reverseAPIDeviceIndex")) { + settings.m_reverseAPIDeviceIndex = response.getRadiosondeDemodSettings()->getReverseApiDeviceIndex(); + } + if (channelSettingsKeys.contains("reverseAPIChannelIndex")) { + settings.m_reverseAPIChannelIndex = response.getRadiosondeDemodSettings()->getReverseApiChannelIndex(); + } + if (settings.m_scopeGUI && channelSettingsKeys.contains("scopeConfig")) { + settings.m_scopeGUI->updateFrom(channelSettingsKeys, response.getRadiosondeDemodSettings()->getScopeConfig()); + } + if (settings.m_channelMarker && channelSettingsKeys.contains("channelMarker")) { + settings.m_channelMarker->updateFrom(channelSettingsKeys, response.getRadiosondeDemodSettings()->getChannelMarker()); + } + if (settings.m_rollupState && channelSettingsKeys.contains("rollupState")) { + settings.m_rollupState->updateFrom(channelSettingsKeys, response.getRadiosondeDemodSettings()->getRollupState()); + } +} + +void RadiosondeDemod::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& response, const RadiosondeDemodSettings& settings) +{ + response.getRadiosondeDemodSettings()->setBaud(settings.m_baud); + response.getRadiosondeDemodSettings()->setInputFrequencyOffset(settings.m_inputFrequencyOffset); + response.getRadiosondeDemodSettings()->setRfBandwidth(settings.m_rfBandwidth); + response.getRadiosondeDemodSettings()->setFmDeviation(settings.m_fmDeviation); + response.getRadiosondeDemodSettings()->setCorrelationThreshold(settings.m_correlationThreshold); + response.getRadiosondeDemodSettings()->setUdpEnabled(settings.m_udpEnabled); + response.getRadiosondeDemodSettings()->setUdpAddress(new QString(settings.m_udpAddress)); + response.getRadiosondeDemodSettings()->setUdpPort(settings.m_udpPort); + response.getRadiosondeDemodSettings()->setLogFilename(new QString(settings.m_logFilename)); + response.getRadiosondeDemodSettings()->setLogEnabled(settings.m_logEnabled); + + response.getRadiosondeDemodSettings()->setRgbColor(settings.m_rgbColor); + if (response.getRadiosondeDemodSettings()->getTitle()) { + *response.getRadiosondeDemodSettings()->getTitle() = settings.m_title; + } else { + response.getRadiosondeDemodSettings()->setTitle(new QString(settings.m_title)); + } + + response.getRadiosondeDemodSettings()->setStreamIndex(settings.m_streamIndex); + response.getRadiosondeDemodSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0); + + if (response.getRadiosondeDemodSettings()->getReverseApiAddress()) { + *response.getRadiosondeDemodSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress; + } else { + response.getRadiosondeDemodSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress)); + } + + response.getRadiosondeDemodSettings()->setReverseApiPort(settings.m_reverseAPIPort); + response.getRadiosondeDemodSettings()->setReverseApiDeviceIndex(settings.m_reverseAPIDeviceIndex); + response.getRadiosondeDemodSettings()->setReverseApiChannelIndex(settings.m_reverseAPIChannelIndex); + + if (settings.m_scopeGUI) + { + if (response.getRadiosondeDemodSettings()->getScopeConfig()) + { + settings.m_scopeGUI->formatTo(response.getRadiosondeDemodSettings()->getScopeConfig()); + } + else + { + SWGSDRangel::SWGGLScope *swgGLScope = new SWGSDRangel::SWGGLScope(); + settings.m_scopeGUI->formatTo(swgGLScope); + response.getRadiosondeDemodSettings()->setScopeConfig(swgGLScope); + } + } + + if (settings.m_channelMarker) + { + if (response.getRadiosondeDemodSettings()->getChannelMarker()) + { + settings.m_channelMarker->formatTo(response.getRadiosondeDemodSettings()->getChannelMarker()); + } + else + { + SWGSDRangel::SWGChannelMarker *swgChannelMarker = new SWGSDRangel::SWGChannelMarker(); + settings.m_channelMarker->formatTo(swgChannelMarker); + response.getRadiosondeDemodSettings()->setChannelMarker(swgChannelMarker); + } + } + + if (settings.m_rollupState) + { + if (response.getRadiosondeDemodSettings()->getRollupState()) + { + settings.m_rollupState->formatTo(response.getRadiosondeDemodSettings()->getRollupState()); + } + else + { + SWGSDRangel::SWGRollupState *swgRollupState = new SWGSDRangel::SWGRollupState(); + settings.m_rollupState->formatTo(swgRollupState); + response.getRadiosondeDemodSettings()->setRollupState(swgRollupState); + } + } +} + +void RadiosondeDemod::webapiReverseSendSettings(QList& channelSettingsKeys, const RadiosondeDemodSettings& 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 RadiosondeDemod::webapiFormatChannelSettings( + QList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings *swgChannelSettings, + const RadiosondeDemodSettings& settings, + bool force +) +{ + swgChannelSettings->setDirection(0); // Single sink (Rx) + swgChannelSettings->setOriginatorChannelIndex(getIndexInDeviceSet()); + swgChannelSettings->setOriginatorDeviceSetIndex(getDeviceSetIndex()); + swgChannelSettings->setChannelType(new QString("RadiosondeDemod")); + swgChannelSettings->setRadiosondeDemodSettings(new SWGSDRangel::SWGRadiosondeDemodSettings()); + SWGSDRangel::SWGRadiosondeDemodSettings *swgRadiosondeDemodSettings = swgChannelSettings->getRadiosondeDemodSettings(); + + // transfer data that has been modified. When force is on transfer all data except reverse API data + + if (channelSettingsKeys.contains("baud") || force) { + swgRadiosondeDemodSettings->setBaud(settings.m_baud); + } + if (channelSettingsKeys.contains("fmDeviation") || force) { + swgRadiosondeDemodSettings->setFmDeviation(settings.m_fmDeviation); + } + if (channelSettingsKeys.contains("inputFrequencyOffset") || force) { + swgRadiosondeDemodSettings->setInputFrequencyOffset(settings.m_inputFrequencyOffset); + } + if (channelSettingsKeys.contains("rfBandwidth") || force) { + swgRadiosondeDemodSettings->setRfBandwidth(settings.m_rfBandwidth); + } + if (channelSettingsKeys.contains("correlationThreshold") || force) { + swgRadiosondeDemodSettings->setCorrelationThreshold(settings.m_correlationThreshold); + } + if (channelSettingsKeys.contains("udpEnabled") || force) { + swgRadiosondeDemodSettings->setUdpEnabled(settings.m_udpEnabled); + } + if (channelSettingsKeys.contains("udpAddress") || force) { + swgRadiosondeDemodSettings->setUdpAddress(new QString(settings.m_udpAddress)); + } + if (channelSettingsKeys.contains("udpPort") || force) { + swgRadiosondeDemodSettings->setUdpPort(settings.m_udpPort); + } + if (channelSettingsKeys.contains("logFilename") || force) { + swgRadiosondeDemodSettings->setLogFilename(new QString(settings.m_logFilename)); + } + if (channelSettingsKeys.contains("logEnabled") || force) { + swgRadiosondeDemodSettings->setLogEnabled(settings.m_logEnabled); + } + if (channelSettingsKeys.contains("rgbColor") || force) { + swgRadiosondeDemodSettings->setRgbColor(settings.m_rgbColor); + } + if (channelSettingsKeys.contains("title") || force) { + swgRadiosondeDemodSettings->setTitle(new QString(settings.m_title)); + } + if (channelSettingsKeys.contains("streamIndex") || force) { + swgRadiosondeDemodSettings->setStreamIndex(settings.m_streamIndex); + } + + if (settings.m_scopeGUI && (channelSettingsKeys.contains("scopeConfig") || force)) + { + SWGSDRangel::SWGGLScope *swgGLScope = new SWGSDRangel::SWGGLScope(); + settings.m_scopeGUI->formatTo(swgGLScope); + swgRadiosondeDemodSettings->setScopeConfig(swgGLScope); + } + + if (settings.m_channelMarker && (channelSettingsKeys.contains("channelMarker") || force)) + { + SWGSDRangel::SWGChannelMarker *swgChannelMarker = new SWGSDRangel::SWGChannelMarker(); + settings.m_channelMarker->formatTo(swgChannelMarker); + swgRadiosondeDemodSettings->setChannelMarker(swgChannelMarker); + } +} + +void RadiosondeDemod::networkManagerFinished(QNetworkReply *reply) +{ + QNetworkReply::NetworkError replyError = reply->error(); + + if (replyError) + { + qWarning() << "RadiosondeDemod::networkManagerFinished:" + << " error(" << (int) replyError + << "): " << replyError + << ": " << reply->errorString(); + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("RadiosondeDemod::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + } + + reply->deleteLater(); +} + +void RadiosondeDemod::handleChannelMessages() +{ + Message* message; + + while ((message = m_channelMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} diff --git a/plugins/channelrx/demodradiosonde/radiosondedemod.h b/plugins/channelrx/demodradiosonde/radiosondedemod.h new file mode 100644 index 000000000..cec2df55d --- /dev/null +++ b/plugins/channelrx/demodradiosonde/radiosondedemod.h @@ -0,0 +1,195 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2015-2018 Edouard Griffiths, F4EXB. // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_RADIOSONDEDEMOD_H +#define INCLUDE_RADIOSONDEDEMOD_H + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "dsp/basebandsamplesink.h" +#include "channel/channelapi.h" +#include "util/message.h" +#include "util/radiosonde.h" + +#include "radiosondedemodbaseband.h" +#include "radiosondedemodsettings.h" + +class QNetworkAccessManager; +class QNetworkReply; +class QThread; +class DeviceAPI; +class ScopeVis; + +class RadiosondeDemod : public BasebandSampleSink, public ChannelAPI { + +public: + class MsgConfigureRadiosondeDemod : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const RadiosondeDemodSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureRadiosondeDemod* create(const RadiosondeDemodSettings& settings, bool force) + { + return new MsgConfigureRadiosondeDemod(settings, force); + } + + private: + RadiosondeDemodSettings m_settings; + bool m_force; + + MsgConfigureRadiosondeDemod(const RadiosondeDemodSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + class MsgMessage : public Message { + MESSAGE_CLASS_DECLARATION + + public: + QByteArray getMessage() const { return m_message; } + QDateTime getDateTime() const { return m_dateTime; } + int getErrorsCorrected() const { return m_errorsCorrected; } + int getThreshold() const { return m_threshold; } + + static MsgMessage* create(QByteArray message, int errorsCorrected, int threshold) + { + return new MsgMessage(message, QDateTime::currentDateTime(), errorsCorrected, threshold); + } + + private: + QByteArray m_message; + QDateTime m_dateTime; + int m_errorsCorrected; + int m_threshold; + + MsgMessage(QByteArray message, QDateTime dateTime, int errorsCorrected, int threshold) : + Message(), + m_message(message), + m_dateTime(dateTime), + m_errorsCorrected(errorsCorrected), + m_threshold(threshold) + { + } + }; + + RadiosondeDemod(DeviceAPI *deviceAPI); + virtual ~RadiosondeDemod(); + virtual void destroy() { delete this; } + + using BasebandSampleSink::feed; + virtual void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, bool po); + virtual void start(); + virtual void stop(); + virtual void pushMessage(Message *msg) { m_inputMessageQueue.push(msg); } + virtual QString getSinkName() { return objectName(); } + + virtual void getIdentifier(QString& id) { id = objectName(); } + virtual const QString& getURI() const { return getName(); } + virtual void getTitle(QString& title) { title = m_settings.m_title; } + virtual qint64 getCenterFrequency() const { return 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 webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + + static void webapiFormatChannelSettings( + SWGSDRangel::SWGChannelSettings& response, + const RadiosondeDemodSettings& settings); + + static void webapiUpdateChannelSettings( + RadiosondeDemodSettings& 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); + } + + uint32_t getNumberOfDeviceStreams() const; + + static const char * const m_channelIdURI; + static const char * const m_channelId; + +private: + DeviceAPI *m_deviceAPI; + QThread m_thread; + RadiosondeDemodBaseband* m_basebandSink; + RadiosondeDemodSettings 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; + + QHash m_subframes; // Hash of serial to subframes + + virtual bool handleMessage(const Message& cmd); + void applySettings(const RadiosondeDemodSettings& settings, bool force = false); + void sendSampleRateToDemodAnalyzer(); + void webapiReverseSendSettings(QList& channelSettingsKeys, const RadiosondeDemodSettings& settings, bool force); + void webapiFormatChannelSettings( + QList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings *swgChannelSettings, + const RadiosondeDemodSettings& settings, + bool force + ); + +private slots: + void networkManagerFinished(QNetworkReply *reply); + void handleChannelMessages(); +}; + +#endif // INCLUDE_RADIOSONDEDEMOD_H diff --git a/plugins/channelrx/demodradiosonde/radiosondedemodbaseband.cpp b/plugins/channelrx/demodradiosonde/radiosondedemodbaseband.cpp new file mode 100644 index 000000000..a2df39d1e --- /dev/null +++ b/plugins/channelrx/demodradiosonde/radiosondedemodbaseband.cpp @@ -0,0 +1,176 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "dsp/downchannelizer.h" + +#include "radiosondedemodbaseband.h" + +MESSAGE_CLASS_DEFINITION(RadiosondeDemodBaseband::MsgConfigureRadiosondeDemodBaseband, Message) + +RadiosondeDemodBaseband::RadiosondeDemodBaseband(RadiosondeDemod *radiosondeDemod) : + m_sink(radiosondeDemod), + m_running(false), + m_mutex(QMutex::Recursive) +{ + qDebug("RadiosondeDemodBaseband::RadiosondeDemodBaseband"); + + m_sink.setScopeSink(&m_scopeSink); + m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(48000)); + m_channelizer = new DownChannelizer(&m_sink); +} + +RadiosondeDemodBaseband::~RadiosondeDemodBaseband() +{ + m_inputMessageQueue.clear(); + + delete m_channelizer; +} + +void RadiosondeDemodBaseband::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_inputMessageQueue.clear(); + m_sampleFifo.reset(); +} + +void RadiosondeDemodBaseband::startWork() +{ + QMutexLocker mutexLocker(&m_mutex); + QObject::connect( + &m_sampleFifo, + &SampleSinkFifo::dataReady, + this, + &RadiosondeDemodBaseband::handleData, + Qt::QueuedConnection + ); + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + m_running = true; +} + +void RadiosondeDemodBaseband::stopWork() +{ + QMutexLocker mutexLocker(&m_mutex); + disconnect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + QObject::disconnect( + &m_sampleFifo, + &SampleSinkFifo::dataReady, + this, + &RadiosondeDemodBaseband::handleData + ); + m_running = false; +} + +void RadiosondeDemodBaseband::setChannel(ChannelAPI *channel) +{ + m_sink.setChannel(channel); +} + +void RadiosondeDemodBaseband::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end) +{ + m_sampleFifo.write(begin, end); +} + +void RadiosondeDemodBaseband::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 RadiosondeDemodBaseband::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool RadiosondeDemodBaseband::handleMessage(const Message& cmd) +{ + if (MsgConfigureRadiosondeDemodBaseband::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureRadiosondeDemodBaseband& cfg = (MsgConfigureRadiosondeDemodBaseband&) cmd; + qDebug() << "RadiosondeDemodBaseband::handleMessage: MsgConfigureRadiosondeDemodBaseband"; + + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + qDebug() << "RadiosondeDemodBaseband::handleMessage: DSPSignalNotification: basebandSampleRate: " << notif.getSampleRate(); + setBasebandSampleRate(notif.getSampleRate()); + m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(notif.getSampleRate())); + + return true; + } + else + { + return false; + } +} + +void RadiosondeDemodBaseband::applySettings(const RadiosondeDemodSettings& settings, bool force) +{ + if ((settings.m_inputFrequencyOffset != m_settings.m_inputFrequencyOffset) || force) + { + m_channelizer->setChannelization(RadiosondeDemodSettings::RADIOSONDEDEMOD_CHANNEL_SAMPLE_RATE, settings.m_inputFrequencyOffset); + m_sink.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); + } + + m_sink.applySettings(settings, force); + + m_settings = settings; +} + +void RadiosondeDemodBaseband::setBasebandSampleRate(int sampleRate) +{ + m_channelizer->setBasebandSampleRate(sampleRate); + m_sink.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); +} diff --git a/plugins/channelrx/demodradiosonde/radiosondedemodbaseband.h b/plugins/channelrx/demodradiosonde/radiosondedemodbaseband.h new file mode 100644 index 000000000..3ba183ca8 --- /dev/null +++ b/plugins/channelrx/demodradiosonde/radiosondedemodbaseband.h @@ -0,0 +1,100 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_RADIOSONDEDEMODBASEBAND_H +#define INCLUDE_RADIOSONDEDEMODBASEBAND_H + +#include +#include + +#include "dsp/samplesinkfifo.h" +#include "dsp/scopevis.h" +#include "util/message.h" +#include "util/messagequeue.h" + +#include "radiosondedemodsink.h" + +class DownChannelizer; +class ChannelAPI; +class RadiosondeDemod; +class ScopeVis; + +class RadiosondeDemodBaseband : public QObject +{ + Q_OBJECT +public: + class MsgConfigureRadiosondeDemodBaseband : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const RadiosondeDemodSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureRadiosondeDemodBaseband* create(const RadiosondeDemodSettings& settings, bool force) + { + return new MsgConfigureRadiosondeDemodBaseband(settings, force); + } + + private: + RadiosondeDemodSettings m_settings; + bool m_force; + + MsgConfigureRadiosondeDemodBaseband(const RadiosondeDemodSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + RadiosondeDemodBaseband(RadiosondeDemod *radiosondeDemod); + ~RadiosondeDemodBaseband(); + 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); + ScopeVis *getScopeSink() { return &m_scopeSink; } + void setChannel(ChannelAPI *channel); + double getMagSq() const { return m_sink.getMagSq(); } + bool isRunning() const { return m_running; } + +private: + SampleSinkFifo m_sampleFifo; + DownChannelizer *m_channelizer; + RadiosondeDemodSink m_sink; + MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication + RadiosondeDemodSettings m_settings; + ScopeVis m_scopeSink; + bool m_running; + QMutex m_mutex; + + bool handleMessage(const Message& cmd); + void calculateOffset(RadiosondeDemodSink *sink); + void applySettings(const RadiosondeDemodSettings& settings, bool force = false); + +private slots: + void handleInputMessages(); + void handleData(); //!< Handle data when samples have to be processed +}; + +#endif // INCLUDE_RADIOSONDEDEMODBASEBAND_H diff --git a/plugins/channelrx/demodradiosonde/radiosondedemodgui.cpp b/plugins/channelrx/demodradiosonde/radiosondedemodgui.cpp new file mode 100644 index 000000000..a9ac324f3 --- /dev/null +++ b/plugins/channelrx/demodradiosonde/radiosondedemodgui.cpp @@ -0,0 +1,898 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016 Edouard Griffiths, F4EXB // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "radiosondedemodgui.h" + +#include "device/deviceuiset.h" +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "ui_radiosondedemodgui.h" +#include "plugin/pluginapi.h" +#include "util/simpleserializer.h" +#include "util/csv.h" +#include "util/db.h" +#include "util/units.h" +#include "gui/basicchannelsettingsdialog.h" +#include "gui/devicestreamselectiondialog.h" +#include "gui/datetimedelegate.h" +#include "gui/decimaldelegate.h" +#include "gui/timedelegate.h" +#include "dsp/dspengine.h" +#include "dsp/glscopesettings.h" +#include "gui/crightclickenabler.h" +#include "channel/channelwebapiutils.h" +#include "maincore.h" +#include "feature/featurewebapiutils.h" + +#include "radiosondedemod.h" +#include "radiosondedemodsink.h" + +void RadiosondeDemodGUI::resizeTable() +{ + // Fill table with a row of dummy data that will size the columns nicely + // Trailing chars are for sort arrow + int row = ui->frames->rowCount(); + ui->frames->setRowCount(row + 1); + ui->frames->setItem(row, FRAME_COL_DATE, new QTableWidgetItem("2015/04/15-")); + ui->frames->setItem(row, FRAME_COL_TIME, new QTableWidgetItem("10:17:00")); + ui->frames->setItem(row, FRAME_COL_SERIAL, new QTableWidgetItem("S1234567")); + ui->frames->setItem(row, FRAME_COL_FRAME_NUMBER, new QTableWidgetItem("10000")); + ui->frames->setItem(row, FRAME_COL_FLIGHT_PHASE, new QTableWidgetItem("Descent")); + ui->frames->setItem(row, FRAME_COL_LATITUDE, new QTableWidgetItem("-90.00000")); + ui->frames->setItem(row, FRAME_COL_LONGITUDE, new QTableWidgetItem("-180.00000")); + ui->frames->setItem(row, FRAME_COL_ALTITUDE, new QTableWidgetItem("20000.0")); + ui->frames->setItem(row, FRAME_COL_SPEED, new QTableWidgetItem("50.0")); + ui->frames->setItem(row, FRAME_COL_VERTICAL_RATE, new QTableWidgetItem("50.0")); + ui->frames->setItem(row, FRAME_COL_HEADING, new QTableWidgetItem("359.0")); + ui->frames->setItem(row, FRAME_COL_PRESSURE, new QTableWidgetItem("100.0")); + ui->frames->setItem(row, FRAME_COL_TEMP, new QTableWidgetItem("-50.1U")); + ui->frames->setItem(row, FRAME_COL_HUMIDITY, new QTableWidgetItem("100.0")); + ui->frames->setItem(row, FRAME_COL_BATTERY_VOLTAGE, new QTableWidgetItem("2.7")); + ui->frames->setItem(row, FRAME_COL_BATTERY_STATUS, new QTableWidgetItem("Low")); + ui->frames->setItem(row, FRAME_COL_PCB_TEMP, new QTableWidgetItem("21")); + ui->frames->setItem(row, FRAME_COL_HUMIDITY_PWM, new QTableWidgetItem("1000")); + ui->frames->setItem(row, FRAME_COL_TX_POWER, new QTableWidgetItem("7")); + ui->frames->setItem(row, FRAME_COL_MAX_SUBFRAME_NO, new QTableWidgetItem("50")); + ui->frames->setItem(row, FRAME_COL_SUBFRAME_NO, new QTableWidgetItem("50")); + ui->frames->setItem(row, FRAME_COL_SUBFRAME, new QTableWidgetItem("00112233445566778899aabbccddeeff----")); + ui->frames->setItem(row, FRAME_COL_GPS_TIME, new QTableWidgetItem("2015/04/15 10:17:00")); + ui->frames->setItem(row, FRAME_COL_GPS_SATS, new QTableWidgetItem("12")); + ui->frames->setItem(row, FRAME_COL_ECC, new QTableWidgetItem("12")); + ui->frames->setItem(row, FRAME_COL_CORR, new QTableWidgetItem("-500")); + ui->frames->resizeColumnsToContents(); + ui->frames->removeRow(row); +} + +// Columns in table reordered +void RadiosondeDemodGUI::frames_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex) +{ + (void) oldVisualIndex; + + m_settings.m_frameColumnIndexes[logicalIndex] = newVisualIndex; +} + +// Column in table resized (when hidden size is 0) +void RadiosondeDemodGUI::frames_sectionResized(int logicalIndex, int oldSize, int newSize) +{ + (void) oldSize; + + m_settings.m_frameColumnSizes[logicalIndex] = newSize; +} + +// Right click in table header - show column select menu +void RadiosondeDemodGUI::framesColumnSelectMenu(QPoint pos) +{ + framesMenu->popup(ui->frames->horizontalHeader()->viewport()->mapToGlobal(pos)); +} + +// Hide/show column when menu selected +void RadiosondeDemodGUI::framesColumnSelectMenuChecked(bool checked) +{ + (void) checked; + + QAction* action = qobject_cast(sender()); + if (action != nullptr) + { + int idx = action->data().toInt(nullptr); + ui->frames->setColumnHidden(idx, !action->isChecked()); + } +} + +// Create column select menu item +QAction *RadiosondeDemodGUI::createCheckableItem(QString &text, int idx, bool checked, const char *slot) +{ + QAction *action = new QAction(text, this); + action->setCheckable(true); + action->setChecked(checked); + action->setData(QVariant(idx)); + connect(action, SIGNAL(triggered()), this, slot); + return action; +} + +RadiosondeDemodGUI* RadiosondeDemodGUI::create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) +{ + RadiosondeDemodGUI* gui = new RadiosondeDemodGUI(pluginAPI, deviceUISet, rxChannel); + return gui; +} + +void RadiosondeDemodGUI::destroy() +{ + delete this; +} + +void RadiosondeDemodGUI::resetToDefaults() +{ + m_settings.resetToDefaults(); + displaySettings(); + applySettings(true); +} + +QByteArray RadiosondeDemodGUI::serialize() const +{ + return m_settings.serialize(); +} + +bool RadiosondeDemodGUI::deserialize(const QByteArray& data) +{ + if(m_settings.deserialize(data)) { + displaySettings(); + applySettings(true); + return true; + } else { + resetToDefaults(); + return false; + } +} + +// Add row to table +void RadiosondeDemodGUI::frameReceived(const QByteArray& frame, const QDateTime& dateTime, int errorsCorrected, int threshold) +{ + RS41Frame *radiosonde; + + // Decode the frame + radiosonde = RS41Frame::decode(frame); + + // Is scroll bar at bottom + QScrollBar *sb = ui->frames->verticalScrollBar(); + bool scrollToBottom = sb->value() == sb->maximum(); + + // Add to frames table + ui->frames->setSortingEnabled(false); + int row = ui->frames->rowCount(); + ui->frames->setRowCount(row + 1); + + QTableWidgetItem *dateItem = new QTableWidgetItem(); + QTableWidgetItem *timeItem = new QTableWidgetItem(); + QTableWidgetItem *serialItem = new QTableWidgetItem(); + QTableWidgetItem *frameNumberItem = new QTableWidgetItem(); + QTableWidgetItem *flightPhaseItem = new QTableWidgetItem(); + QTableWidgetItem *latitudeItem = new QTableWidgetItem(); + QTableWidgetItem *longitudeItem = new QTableWidgetItem(); + QTableWidgetItem *altitudeItem = new QTableWidgetItem(); + QTableWidgetItem *speedItem = new QTableWidgetItem(); + QTableWidgetItem *verticalRateItem = new QTableWidgetItem(); + QTableWidgetItem *headingItem = new QTableWidgetItem(); + QTableWidgetItem *pressureItem = new QTableWidgetItem(); + QTableWidgetItem *tempItem = new QTableWidgetItem(); + QTableWidgetItem *humidityItem = new QTableWidgetItem(); + QTableWidgetItem *batteryVoltageItem = new QTableWidgetItem(); + QTableWidgetItem *batteryStatusItem = new QTableWidgetItem(); + QTableWidgetItem *pcbTempItem = new QTableWidgetItem(); + QTableWidgetItem *humidityPWMItem = new QTableWidgetItem(); + QTableWidgetItem *txPowerItem = new QTableWidgetItem(); + QTableWidgetItem *maxSubframeNoItem = new QTableWidgetItem(); + QTableWidgetItem *subframeNoItem = new QTableWidgetItem(); + QTableWidgetItem *subframeItem = new QTableWidgetItem(); + QTableWidgetItem *gpsTimeItem = new QTableWidgetItem(); + QTableWidgetItem *gpsSatsItem = new QTableWidgetItem(); + QTableWidgetItem *eccItem = new QTableWidgetItem(); + QTableWidgetItem *thItem = new QTableWidgetItem(); + + ui->frames->setItem(row, FRAME_COL_DATE, dateItem); + ui->frames->setItem(row, FRAME_COL_TIME, timeItem); + ui->frames->setItem(row, FRAME_COL_SERIAL, serialItem); + ui->frames->setItem(row, FRAME_COL_FRAME_NUMBER, frameNumberItem); + ui->frames->setItem(row, FRAME_COL_FLIGHT_PHASE, flightPhaseItem); + ui->frames->setItem(row, FRAME_COL_LATITUDE, latitudeItem); + ui->frames->setItem(row, FRAME_COL_LONGITUDE, longitudeItem); + ui->frames->setItem(row, FRAME_COL_ALTITUDE, altitudeItem); + ui->frames->setItem(row, FRAME_COL_SPEED, speedItem); + ui->frames->setItem(row, FRAME_COL_VERTICAL_RATE, verticalRateItem); + ui->frames->setItem(row, FRAME_COL_HEADING, headingItem); + ui->frames->setItem(row, FRAME_COL_PRESSURE, pressureItem); + ui->frames->setItem(row, FRAME_COL_TEMP, tempItem); + ui->frames->setItem(row, FRAME_COL_HUMIDITY, humidityItem); + ui->frames->setItem(row, FRAME_COL_BATTERY_VOLTAGE, batteryVoltageItem); + ui->frames->setItem(row, FRAME_COL_BATTERY_STATUS, batteryStatusItem); + ui->frames->setItem(row, FRAME_COL_PCB_TEMP, pcbTempItem); + ui->frames->setItem(row, FRAME_COL_HUMIDITY_PWM, humidityPWMItem); + ui->frames->setItem(row, FRAME_COL_TX_POWER, txPowerItem); + ui->frames->setItem(row, FRAME_COL_MAX_SUBFRAME_NO, maxSubframeNoItem); + ui->frames->setItem(row, FRAME_COL_SUBFRAME_NO, subframeNoItem); + ui->frames->setItem(row, FRAME_COL_SUBFRAME, subframeItem); + ui->frames->setItem(row, FRAME_COL_GPS_TIME, gpsTimeItem); + ui->frames->setItem(row, FRAME_COL_GPS_SATS, gpsSatsItem); + ui->frames->setItem(row, FRAME_COL_ECC, eccItem); + ui->frames->setItem(row, FRAME_COL_CORR, thItem); + + dateItem->setData(Qt::DisplayRole, dateTime.date()); + timeItem->setData(Qt::DisplayRole, dateTime.time()); + + RS41Subframe *subframe = nullptr; + + frameNumberItem->setData(Qt::DisplayRole, radiosonde->m_frameNumber); + if (radiosonde->m_statusValid) + { + serialItem->setText(radiosonde->m_serial); + flightPhaseItem->setText(radiosonde->m_flightPhase); + batteryVoltageItem->setData(Qt::DisplayRole, radiosonde->m_batteryVoltage); + batteryStatusItem->setText(radiosonde->m_batteryStatus); + pcbTempItem->setData(Qt::DisplayRole, radiosonde->m_pcbTemperature); + humidityPWMItem->setData(Qt::DisplayRole, (int)round(radiosonde->m_humiditySensorHeating / 1000.0 * 100.0)); + txPowerItem->setData(Qt::DisplayRole, (int)round(radiosonde->m_transmitPower / 7.0 * 100.0)); + maxSubframeNoItem->setData(Qt::DisplayRole, radiosonde->m_maxSubframeNumber); + subframeNoItem->setData(Qt::DisplayRole, radiosonde->m_subframeNumber); + subframeItem->setText(radiosonde->m_subframe.toHex()); + if (m_subframes.contains(radiosonde->m_serial)) + { + subframe = m_subframes.value(radiosonde->m_serial); + } + else + { + subframe = new RS41Subframe(); + m_subframes.insert(radiosonde->m_serial, subframe); + } + subframe->update(radiosonde); + } + + if (radiosonde->m_posValid) + { + latitudeItem->setData(Qt::DisplayRole, radiosonde->m_latitude); + longitudeItem->setData(Qt::DisplayRole, radiosonde->m_longitude); + altitudeItem->setData(Qt::DisplayRole, radiosonde->m_height); + speedItem->setData(Qt::DisplayRole, Units::kmpsToKPH(radiosonde->m_speed/1000.0)); + verticalRateItem->setData(Qt::DisplayRole, radiosonde->m_verticalRate); + headingItem->setData(Qt::DisplayRole, radiosonde->m_heading); + gpsSatsItem->setData(Qt::DisplayRole, radiosonde->m_satellitesUsed); + } + + if (radiosonde->m_gpsInfoValid) + { + gpsTimeItem->setData(Qt::DisplayRole, radiosonde->m_gpsDateTime); + } + + if (radiosonde->m_measValid && subframe) + { + pressureItem->setData(Qt::DisplayRole, radiosonde->getPressureString(subframe)); + tempItem->setData(Qt::DisplayRole, radiosonde->getTemperatureString(subframe)); + humidityItem->setData(Qt::DisplayRole, radiosonde->getHumidityString(subframe)); + } + + eccItem->setData(Qt::DisplayRole, errorsCorrected); + thItem->setData(Qt::DisplayRole, threshold); + + ui->frames->setSortingEnabled(true); + if (scrollToBottom) { + ui->frames->scrollToBottom(); + } + filterRow(row); + + delete radiosonde; +} + +bool RadiosondeDemodGUI::handleMessage(const Message& frame) +{ + if (RadiosondeDemod::MsgConfigureRadiosondeDemod::match(frame)) + { + qDebug("RadiosondeDemodGUI::handleMessage: RadiosondeDemod::MsgConfigureRadiosondeDemod"); + const RadiosondeDemod::MsgConfigureRadiosondeDemod& cfg = (RadiosondeDemod::MsgConfigureRadiosondeDemod&) frame; + 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 (RadiosondeDemod::MsgMessage::match(frame)) + { + RadiosondeDemod::MsgMessage& report = (RadiosondeDemod::MsgMessage&) frame; + frameReceived(report.getMessage(), report.getDateTime(), report.getErrorsCorrected(), report.getThreshold()); + return true; + } + + return false; +} + +void RadiosondeDemodGUI::handleInputMessages() +{ + Message* frame; + + while ((frame = getInputMessageQueue()->pop()) != 0) + { + if (handleMessage(*frame)) + { + delete frame; + } + } +} + +void RadiosondeDemodGUI::channelMarkerChangedByCursor() +{ + ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + applySettings(); +} + +void RadiosondeDemodGUI::channelMarkerHighlightedByCursor() +{ + setHighlighted(m_channelMarker.getHighlighted()); +} + +void RadiosondeDemodGUI::on_deltaFrequency_changed(qint64 value) +{ + m_channelMarker.setCenterFrequency(value); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + applySettings(); +} + +void RadiosondeDemodGUI::on_rfBW_valueChanged(int value) +{ + float bw = value * 100.0f; + ui->rfBWText->setText(QString("%1k").arg(value / 10.0, 0, 'f', 1)); + m_channelMarker.setBandwidth(bw); + m_settings.m_rfBandwidth = bw; + applySettings(); +} + +void RadiosondeDemodGUI::on_fmDev_valueChanged(int value) +{ + ui->fmDevText->setText(QString("%1k").arg(value / 10.0, 0, 'f', 1)); + m_settings.m_fmDeviation = value * 100.0; + applySettings(); +} + +void RadiosondeDemodGUI::on_threshold_valueChanged(int value) +{ + ui->thresholdText->setText(QString("%1").arg(value)); + m_settings.m_correlationThreshold = value; + applySettings(); +} + +void RadiosondeDemodGUI::on_filterSerial_editingFinished() +{ + m_settings.m_filterSerial = ui->filterSerial->text(); + filter(); + applySettings(); +} + +void RadiosondeDemodGUI::on_clearTable_clicked() +{ + ui->frames->setRowCount(0); +} + +void RadiosondeDemodGUI::on_udpEnabled_clicked(bool checked) +{ + m_settings.m_udpEnabled = checked; + applySettings(); +} + +void RadiosondeDemodGUI::on_udpAddress_editingFinished() +{ + m_settings.m_udpAddress = ui->udpAddress->text(); + applySettings(); +} + +void RadiosondeDemodGUI::on_udpPort_editingFinished() +{ + m_settings.m_udpPort = ui->udpPort->text().toInt(); + applySettings(); +} + +void RadiosondeDemodGUI::on_channel1_currentIndexChanged(int index) +{ + m_settings.m_scopeCh1 = index; + applySettings(); +} + +void RadiosondeDemodGUI::on_channel2_currentIndexChanged(int index) +{ + m_settings.m_scopeCh2 = index; + applySettings(); +} + +void RadiosondeDemodGUI::on_frames_cellDoubleClicked(int row, int column) +{ + // Get serial in row double clicked + QString serial = ui->frames->item(row, FRAME_COL_SERIAL)->text(); + if (column == FRAME_COL_SERIAL) + { + // Search for Serial on sondehub + QDesktopServices::openUrl(QUrl(QString("https://sondehub.org/?f=%1#!mt=Mapnik&f=%1&q=%1").arg(serial))); + } + else if ((column == FRAME_COL_LATITUDE) || (column == FRAME_COL_LONGITUDE)) + { + // Find serial on Map + FeatureWebAPIUtils::mapFind(serial); + } +} + +void RadiosondeDemodGUI::filterRow(int row) +{ + bool hidden = false; + if (m_settings.m_filterSerial != "") + { + QRegExp re(m_settings.m_filterSerial); + QTableWidgetItem *fromItem = ui->frames->item(row, FRAME_COL_SERIAL); + if (!re.exactMatch(fromItem->text())) + hidden = true; + } + ui->frames->setRowHidden(row, hidden); +} + +void RadiosondeDemodGUI::filter() +{ + for (int i = 0; i < ui->frames->rowCount(); i++) + { + filterRow(i); + } +} + +void RadiosondeDemodGUI::onWidgetRolled(QWidget* widget, bool rollDown) +{ + if (widget == ui->scopeContainer) + { + if (rollDown) + { + // Make wide enough for scope controls + setMinimumWidth(716); + } + else + { + setMinimumWidth(352); + } + } + + saveState(m_rollupState); + applySettings(); +} + +void RadiosondeDemodGUI::onMenuDialogCalled(const QPoint &p) +{ + if (m_contextMenuType == ContextMenuChannelSettings) + { + BasicChannelSettingsDialog dialog(&m_channelMarker, this); + dialog.setUseReverseAPI(m_settings.m_useReverseAPI); + dialog.setReverseAPIAddress(m_settings.m_reverseAPIAddress); + dialog.setReverseAPIPort(m_settings.m_reverseAPIPort); + dialog.setReverseAPIDeviceIndex(m_settings.m_reverseAPIDeviceIndex); + dialog.setReverseAPIChannelIndex(m_settings.m_reverseAPIChannelIndex); + dialog.move(p); + dialog.exec(); + + m_settings.m_rgbColor = m_channelMarker.getColor().rgb(); + m_settings.m_title = m_channelMarker.getTitle(); + m_settings.m_useReverseAPI = dialog.useReverseAPI(); + m_settings.m_reverseAPIAddress = dialog.getReverseAPIAddress(); + m_settings.m_reverseAPIPort = dialog.getReverseAPIPort(); + m_settings.m_reverseAPIDeviceIndex = dialog.getReverseAPIDeviceIndex(); + m_settings.m_reverseAPIChannelIndex = dialog.getReverseAPIChannelIndex(); + + setWindowTitle(m_settings.m_title); + setTitleColor(m_settings.m_rgbColor); + + applySettings(); + } + else if ((m_contextMenuType == ContextMenuStreamSettings) && (m_deviceUISet->m_deviceMIMOEngine)) + { + DeviceStreamSelectionDialog dialog(this); + dialog.setNumberOfStreams(m_radiosondeDemod->getNumberOfDeviceStreams()); + dialog.setStreamIndex(m_settings.m_streamIndex); + dialog.move(p); + dialog.exec(); + + m_settings.m_streamIndex = dialog.getSelectedStreamIndex(); + m_channelMarker.clearStreamIndexes(); + m_channelMarker.addStreamIndex(m_settings.m_streamIndex); + displayStreamIndex(); + applySettings(); + } + + resetContextMenuType(); +} + +RadiosondeDemodGUI::RadiosondeDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent) : + ChannelGUI(parent), + ui(new Ui::RadiosondeDemodGUI), + m_pluginAPI(pluginAPI), + m_deviceUISet(deviceUISet), + m_channelMarker(this), + m_doApplySettings(true), + m_tickCount(0) +{ + ui->setupUi(this); + m_helpURL = "plugins/channelrx/demodradiosonde/readme.md"; + + setAttribute(Qt::WA_DeleteOnClose, true); + connect(this, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool))); + connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &))); + + m_radiosondeDemod = reinterpret_cast(rxChannel); + m_radiosondeDemod->setMessageQueueToGUI(getInputMessageQueue()); + + connect(&MainCore::instance()->getMasterTimer(), SIGNAL(timeout()), this, SLOT(tick())); // 50 ms + + m_scopeVis = m_radiosondeDemod->getScopeSink(); + m_scopeVis->setGLScope(ui->glScope); + ui->glScope->connectTimer(MainCore::instance()->getMasterTimer()); + ui->scopeGUI->setBuddies(m_scopeVis->getInputMessageQueue(), m_scopeVis, ui->glScope); + + // 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(9600*6); + //m_scopeVis->setFreeRun(false); // FIXME: add method rather than call m_scopeVis->configure() + + ui->deltaFrequencyLabel->setText(QString("%1f").arg(QChar(0x94, 0x03))); + ui->deltaFrequency->setColorMapper(ColorMapper(ColorMapper::GrayGold)); + ui->deltaFrequency->setValueRange(false, 7, -9999999, 9999999); + ui->channelPowerMeter->setColorTheme(LevelMeterSignalDB::ColorGreenAndBlue); + + m_channelMarker.blockSignals(true); + m_channelMarker.setColor(Qt::yellow); + m_channelMarker.setBandwidth(m_settings.m_rfBandwidth); + m_channelMarker.setCenterFrequency(m_settings.m_inputFrequencyOffset); + m_channelMarker.setTitle("Radiosonde 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); + m_deviceUISet->addRollupWidget(this); + + connect(&m_channelMarker, SIGNAL(changedByCursor()), this, SLOT(channelMarkerChangedByCursor())); + connect(&m_channelMarker, SIGNAL(highlightedByCursor()), this, SLOT(channelMarkerHighlightedByCursor())); + connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + + // Resize the table using dummy data + resizeTable(); + // Allow user to reorder columns + ui->frames->horizontalHeader()->setSectionsMovable(true); + // Allow user to sort table by clicking on headers + ui->frames->setSortingEnabled(true); + // Add context menu to allow hiding/showing of columns + framesMenu = new QMenu(ui->frames); + for (int i = 0; i < ui->frames->horizontalHeader()->count(); i++) + { + QString text = ui->frames->horizontalHeaderItem(i)->text(); + framesMenu->addAction(createCheckableItem(text, i, true, SLOT(framesColumnSelectMenuChecked()))); + } + ui->frames->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->frames->horizontalHeader(), SIGNAL(customContextMenuRequested(QPoint)), SLOT(framesColumnSelectMenu(QPoint))); + // Get signals when columns change + connect(ui->frames->horizontalHeader(), SIGNAL(sectionMoved(int, int, int)), SLOT(frames_sectionMoved(int, int, int))); + connect(ui->frames->horizontalHeader(), SIGNAL(sectionResized(int, int, int)), SLOT(frames_sectionResized(int, int, int))); + ui->frames->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->frames, SIGNAL(customContextMenuRequested(QPoint)), SLOT(customContextMenuRequested(QPoint))); + + ui->frames->setItemDelegateForColumn(FRAME_COL_DATE, new DateTimeDelegate("yyyy/MM/dd")); + ui->frames->setItemDelegateForColumn(FRAME_COL_TIME, new TimeDelegate()); + ui->frames->setItemDelegateForColumn(FRAME_COL_LATITUDE, new DecimalDelegate(5)); + ui->frames->setItemDelegateForColumn(FRAME_COL_LONGITUDE, new DecimalDelegate(5)); + ui->frames->setItemDelegateForColumn(FRAME_COL_ALTITUDE, new DecimalDelegate(1)); + ui->frames->setItemDelegateForColumn(FRAME_COL_SPEED, new DecimalDelegate(1)); + ui->frames->setItemDelegateForColumn(FRAME_COL_VERTICAL_RATE, new DecimalDelegate(1)); + ui->frames->setItemDelegateForColumn(FRAME_COL_HEADING, new DecimalDelegate(1)); + ui->frames->setItemDelegateForColumn(FRAME_COL_GPS_TIME, new DateTimeDelegate("yyyy/MM/dd hh:mm:ss")); + + ui->scopeContainer->setVisible(false); + + displaySettings(); + applySettings(true); +} + +void RadiosondeDemodGUI::customContextMenuRequested(QPoint pos) +{ + QTableWidgetItem *item = ui->frames->itemAt(pos); + if (item) + { + int row = item->row(); + QString serial = ui->frames->item(row, FRAME_COL_SERIAL)->text(); + + QMenu* tableContextMenu = new QMenu(ui->frames); + connect(tableContextMenu, &QMenu::aboutToHide, tableContextMenu, &QMenu::deleteLater); + + // Copy current cell + 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 radiosonde on various websites + QAction* mmsiRadiosondeHubAction = new QAction(QString("View %1 on sondehub.net...").arg(serial), tableContextMenu); + connect(mmsiRadiosondeHubAction, &QAction::triggered, this, [serial]()->void { + QDesktopServices::openUrl(QUrl(QString("https://sondehub.org/?f=%1#!mt=Mapnik&f=%1&q=%1").arg(serial))); + }); + tableContextMenu->addAction(mmsiRadiosondeHubAction); + + // Find on Map + tableContextMenu->addSeparator(); + QAction* findMapFeatureAction = new QAction(QString("Find %1 on map").arg(serial), tableContextMenu); + connect(findMapFeatureAction, &QAction::triggered, this, [serial]()->void { + FeatureWebAPIUtils::mapFind(serial); + }); + tableContextMenu->addAction(findMapFeatureAction); + + tableContextMenu->popup(ui->frames->viewport()->mapToGlobal(pos)); + } +} + +RadiosondeDemodGUI::~RadiosondeDemodGUI() +{ + delete ui; + qDeleteAll(m_subframes); +} + +void RadiosondeDemodGUI::blockApplySettings(bool block) +{ + m_doApplySettings = !block; +} + +void RadiosondeDemodGUI::applySettings(bool force) +{ + if (m_doApplySettings) + { + RadiosondeDemod::MsgConfigureRadiosondeDemod* frame = RadiosondeDemod::MsgConfigureRadiosondeDemod::create( m_settings, force); + m_radiosondeDemod->getInputMessageQueue()->push(frame); + } +} + +void RadiosondeDemodGUI::displaySettings() +{ + m_channelMarker.blockSignals(true); + m_channelMarker.setBandwidth(m_settings.m_rfBandwidth); + m_channelMarker.setCenterFrequency(m_settings.m_inputFrequencyOffset); + m_channelMarker.setTitle(m_settings.m_title); + m_channelMarker.blockSignals(false); + m_channelMarker.setColor(m_settings.m_rgbColor); // activate signal on the last setting only + + setTitleColor(m_settings.m_rgbColor); + setWindowTitle(m_channelMarker.getTitle()); + + blockApplySettings(true); + + ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); + + ui->rfBWText->setText(QString("%1k").arg(m_settings.m_rfBandwidth / 1000.0, 0, 'f', 1)); + ui->rfBW->setValue(m_settings.m_rfBandwidth / 100.0); + + ui->fmDevText->setText(QString("%1k").arg(m_settings.m_fmDeviation / 1000.0, 0, 'f', 1)); + ui->fmDev->setValue(m_settings.m_fmDeviation / 100.0); + + ui->thresholdText->setText(QString("%1").arg(m_settings.m_correlationThreshold)); + ui->threshold->setValue(m_settings.m_correlationThreshold); + + displayStreamIndex(); + + ui->filterSerial->setText(m_settings.m_filterSerial); + + ui->udpEnabled->setChecked(m_settings.m_udpEnabled); + ui->udpAddress->setText(m_settings.m_udpAddress); + ui->udpPort->setText(QString::number(m_settings.m_udpPort)); + + ui->channel1->setCurrentIndex(m_settings.m_scopeCh1); + ui->channel2->setCurrentIndex(m_settings.m_scopeCh2); + + ui->logFilename->setToolTip(QString(".csv log filename: %1").arg(m_settings.m_logFilename)); + ui->logEnable->setChecked(m_settings.m_logEnabled); + + // Order and size columns + QHeaderView *header = ui->frames->horizontalHeader(); + for (int i = 0; i < RADIOSONDEDEMOD_FRAME_COLUMNS; i++) + { + bool hidden = m_settings.m_frameColumnSizes[i] == 0; + header->setSectionHidden(i, hidden); + framesMenu->actions().at(i)->setChecked(!hidden); + if (m_settings.m_frameColumnSizes[i] > 0) + ui->frames->setColumnWidth(i, m_settings.m_frameColumnSizes[i]); + header->moveSection(header->visualIndex(i), m_settings.m_frameColumnIndexes[i]); + } + + filter(); + + restoreState(m_rollupState); + blockApplySettings(false); +} + +void RadiosondeDemodGUI::displayStreamIndex() +{ + if (m_deviceUISet->m_deviceMIMOEngine) { + setStreamIndicator(tr("%1").arg(m_settings.m_streamIndex)); + } else { + setStreamIndicator("S"); // single channel indicator + } +} + +void RadiosondeDemodGUI::leaveEvent(QEvent*) +{ + m_channelMarker.setHighlighted(false); +} + +void RadiosondeDemodGUI::enterEvent(QEvent*) +{ + m_channelMarker.setHighlighted(true); +} + +void RadiosondeDemodGUI::tick() +{ + double magsqAvg, magsqPeak; + int nbMagsqSamples; + m_radiosondeDemod->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 RadiosondeDemodGUI::on_logEnable_clicked(bool checked) +{ + m_settings.m_logEnabled = checked; + applySettings(); +} + +void RadiosondeDemodGUI::on_logFilename_clicked() +{ + // Get filename to save to + QFileDialog fileDialog(nullptr, "Select file to log received frames to", "", "*.csv"); + fileDialog.setAcceptMode(QFileDialog::AcceptSave); + if (fileDialog.exec()) + { + QStringList fileNames = fileDialog.selectedFiles(); + if (fileNames.size() > 0) + { + m_settings.m_logFilename = fileNames[0]; + ui->logFilename->setToolTip(QString(".csv log filename: %1").arg(m_settings.m_logFilename)); + applySettings(); + } + } +} + +// Read .csv log and process as received frames +void RadiosondeDemodGUI::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", "Data"}, error); + if (error.isEmpty()) + { + int dateCol = colIndexes.value("Date"); + int timeCol = colIndexes.value("Time"); + int dataCol = colIndexes.value("Data"); + int maxCol = std::max({dateCol, timeCol, dataCol}); + + QMessageBox dialog(this); + dialog.setText("Reading frames"); + dialog.addButton(QMessageBox::Cancel); + dialog.show(); + QApplication::processEvents(); + int count = 0; + bool cancelled = false; + QStringList cols; + + MessagePipesLegacy& framePipes = MainCore::instance()->getMessagePipes(); + QList *radiosondeMessageQueues = framePipes.getMessageQueues(m_radiosondeDemod, "radiosonde"); + + while (!cancelled && CSV::readRow(in, &cols)) + { + if (cols.size() > maxCol) + { + QDate date = QDate::fromString(cols[dateCol]); + QTime time = QTime::fromString(cols[timeCol]); + QDateTime dateTime(date, time); + QByteArray bytes = QByteArray::fromHex(cols[dataCol].toLatin1()); + + // Add to table + frameReceived(bytes, dateTime, 0, 0); + + // Forward to Radiosonde feature + if (radiosondeMessageQueues) + { + QList::iterator it = radiosondeMessageQueues->begin(); + for (; it != radiosondeMessageQueues->end(); ++it) + { + MainCore::MsgPacket *msg = MainCore::MsgPacket::create(m_radiosondeDemod, bytes, dateTime); + (*it)->push(msg); + } + } + + if (count % 100 == 0) + { + QApplication::processEvents(); + if (dialog.clickedButton()) { + cancelled = true; + } + } + count++; + } + } + dialog.close(); + } + else + { + QMessageBox::critical(this, "Radiosonde Demod", error); + } + } + else + { + QMessageBox::critical(this, "Radiosonde Demod", QString("Failed to open file %1").arg(fileNames[0])); + } + } + } +} diff --git a/plugins/channelrx/demodradiosonde/radiosondedemodgui.h b/plugins/channelrx/demodradiosonde/radiosondedemodgui.h new file mode 100644 index 000000000..053802350 --- /dev/null +++ b/plugins/channelrx/demodradiosonde/radiosondedemodgui.h @@ -0,0 +1,155 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016 Edouard Griffiths, F4EXB // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_RADIOSONDEDEMODGUI_H +#define INCLUDE_RADIOSONDEDEMODGUI_H + +#include +#include + +#include "channel/channelgui.h" +#include "dsp/channelmarker.h" +#include "util/messagequeue.h" +#include "settings/rollupstate.h" + +#include "radiosondedemodsettings.h" +#include "radiosondedemod.h" + +class PluginAPI; +class DeviceUISet; +class BasebandSampleSink; +class ScopeVis; +class ScopeVisXY; +class RadiosondeDemod; +class RadiosondeDemodGUI; +class RS41Frame; + +namespace Ui { + class RadiosondeDemodGUI; +} + +class RadiosondeDemodGUI : public ChannelGUI { + Q_OBJECT + +public: + static RadiosondeDemodGUI* create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel); + virtual void destroy(); + + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + virtual MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } + +public slots: + void channelMarkerChangedByCursor(); + void channelMarkerHighlightedByCursor(); + +private: + Ui::RadiosondeDemodGUI* ui; + PluginAPI* m_pluginAPI; + DeviceUISet* m_deviceUISet; + ChannelMarker m_channelMarker; + RollupState m_rollupState; + RadiosondeDemodSettings m_settings; + bool m_doApplySettings; + ScopeVis* m_scopeVis; + + RadiosondeDemod* m_radiosondeDemod; + uint32_t m_tickCount; + MessageQueue m_inputMessageQueue; + + QMenu *framesMenu; // Column select context menu + QMenu *copyMenu; + + QHash m_subframes; // Hash of serial to subframes + + explicit RadiosondeDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent = 0); + virtual ~RadiosondeDemodGUI(); + + void blockApplySettings(bool block); + void applySettings(bool force = false); + void displaySettings(); + void displayStreamIndex(); + void frameReceived(const QByteArray& frame, const QDateTime& dateTime, int errorsCorrected, int threshold); + bool handleMessage(const Message& message); + + void leaveEvent(QEvent*); + void enterEvent(QEvent*); + + void resizeTable(); + QAction *createCheckableItem(QString& text, int idx, bool checked, const char *slot); + + enum MessageCol { + FRAME_COL_DATE, + FRAME_COL_TIME, + FRAME_COL_SERIAL, + FRAME_COL_FRAME_NUMBER, + FRAME_COL_FLIGHT_PHASE, + FRAME_COL_LATITUDE, + FRAME_COL_LONGITUDE, + FRAME_COL_ALTITUDE, + FRAME_COL_SPEED, + FRAME_COL_VERTICAL_RATE, + FRAME_COL_HEADING, + FRAME_COL_PRESSURE, + FRAME_COL_TEMP, + FRAME_COL_HUMIDITY, + FRAME_COL_BATTERY_VOLTAGE, + FRAME_COL_BATTERY_STATUS, + FRAME_COL_PCB_TEMP, + FRAME_COL_HUMIDITY_PWM, + FRAME_COL_TX_POWER, + FRAME_COL_MAX_SUBFRAME_NO, + FRAME_COL_SUBFRAME_NO, + FRAME_COL_SUBFRAME, + FRAME_COL_GPS_TIME, + FRAME_COL_GPS_SATS, + FRAME_COL_ECC, + FRAME_COL_CORR + }; + +private slots: + void on_deltaFrequency_changed(qint64 value); + void on_rfBW_valueChanged(int index); + void on_fmDev_valueChanged(int value); + void on_threshold_valueChanged(int value); + void on_filterSerial_editingFinished(); + void on_clearTable_clicked(); + void on_udpEnabled_clicked(bool checked); + void on_udpAddress_editingFinished(); + void on_udpPort_editingFinished(); + void on_channel1_currentIndexChanged(int index); + void on_channel2_currentIndexChanged(int index); + void on_frames_cellDoubleClicked(int row, int column); + void on_logEnable_clicked(bool checked=false); + void on_logFilename_clicked(); + void on_logOpen_clicked(); + void filterRow(int row); + void filter(); + void frames_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex); + void frames_sectionResized(int logicalIndex, int oldSize, int newSize); + void framesColumnSelectMenu(QPoint pos); + void framesColumnSelectMenuChecked(bool checked = false); + void customContextMenuRequested(QPoint point); + void onWidgetRolled(QWidget* widget, bool rollDown); + void onMenuDialogCalled(const QPoint& p); + void handleInputMessages(); + void tick(); +}; + +#endif // INCLUDE_RADIOSONDEDEMODGUI_H diff --git a/plugins/channelrx/demodradiosonde/radiosondedemodgui.ui b/plugins/channelrx/demodradiosonde/radiosondedemodgui.ui new file mode 100644 index 000000000..0d7ac4612 --- /dev/null +++ b/plugins/channelrx/demodradiosonde/radiosondedemodgui.ui @@ -0,0 +1,1116 @@ + + + RadiosondeDemodGUI + + + + 0 + 0 + 404 + 764 + + + + + 0 + 0 + + + + + 352 + 0 + + + + + 9 + + + + Qt::StrongFocus + + + Radiosonde Demodulator + + + Radiosonde Demodulator + + + + + 0 + 0 + 390 + 151 + + + + + 350 + 0 + + + + Settings + + + + 3 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + 2 + + + + + + 16 + 0 + + + + Df + + + + + + + + 0 + 0 + + + + + 32 + 16 + + + + + 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 + + + + + 8 + + + + Level meter (dB) top trace: average, bottom trace: instantaneous peak, tip: peak hold + + + + + + + + + Qt::Horizontal + + + + + + + + + BW + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + RF bandwidth + + + 10 + + + 400 + + + 1 + + + 100 + + + Qt::Horizontal + + + + + + + + 30 + 0 + + + + 10.0k + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Vertical + + + + + + + Dev + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + Frequency deviation + + + 10 + + + 50 + + + 1 + + + 24 + + + Qt::Horizontal + + + + + + + + 30 + 0 + + + + ±2.4k + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Vertical + + + + + + + TH + + + + + + + + 24 + 24 + + + + Correlation threshold + + + 200 + + + 800 + + + 1 + + + 5 + + + 400 + + + + + + + 400 + + + + + + + + + Qt::Horizontal + + + + + + + + + UDP + + + + + + + Forward received frames via UDP + + + Qt::RightToLeft + + + + + + + + + + + 120 + 0 + + + + Qt::ClickFocus + + + Destination UDP address + + + 000.000.000.000 + + + 127.0.0.1 + + + + + + + : + + + Qt::AlignCenter + + + + + + + + 50 + 0 + + + + + 50 + 16777215 + + + + Qt::ClickFocus + + + Destination UDP port + + + 00000 + + + 9998 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Horizontal + + + + + + + + + Find + + + + + + + Display only frames where the serial number matches the specified regular expression + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 24 + 16777215 + + + + Start/stop logging of received data to .csv file + + + + + + + :/record_off.png:/record_off.png + + + + + + + Set log .csv filename + + + ... + + + + :/save.png:/save.png + + + false + + + + + + + Read data from .csv log file + + + ... + + + + :/load.png:/load.png + + + false + + + + + + + Clear data from table + + + + + + + :/bin.png:/bin.png + + + + + + + + + + + 0 + 210 + 391 + 171 + + + + + 0 + 0 + + + + Received Frames + + + + 2 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + Received packets + + + QAbstractItemView::NoEditTriggers + + + + Date + + + Date frame was received + + + + + Time + + + Time frame was received + + + + + Serial + + + Serial number + + + + + Frame + + + Frame number + + + + + Phase + + + Flight phase + + + + + Lat (°) + + + Latitude in degrees. East positive + + + + + Lon (°) + + + Longitude in degrees. North positive + + + + + Alt (m) + + + Altitude in metres + + + + + Spd (km/h) + + + Speed in kilometres per hour + + + + + VR (m/s) + + + Vertical rate in metres per second + + + + + Hdg (°) + + + Heading in degrees + + + + + P (hPa) + + + Air pressure in hectopascals + + + + + T (°C) + + + Air temperature in degrees Celsius + + + + + U (%) + + + Relative humidity in percent + + + + + Bat (V) + + + Battery voltage + + + + + Bat + + + Battery status + + + + + PCB (°C) + + + PCB temperature in degrees Celsuis + + + + + PWM (%) + + + Humidty sensor heater PWM percentage + + + + + TX (%) + + + Transmit power percent + + + + + Max SF + + + Maximum subframe number + + + + + SF No. + + + Subframe number + + + + + Subframe + + + Subframe data + + + + + GPS Time + + + GPS Time (18 leap seconds offset from UTC) + + + + + GPS Sats + + + Number of GPS satellites used to estimate position + + + + + ECC + + + Number of errors corrected + + + + + Corr + + + Correlation + + + + + + + + + + 20 + 400 + 351 + 341 + + + + Waveforms + + + + 2 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + + Real + + + + + + + + 0 + 0 + + + + + I + + + + + Q + + + + + Mag Sq + + + + + FM demod + + + + + Gaussian + + + + + RX buf + + + + + Correlation + + + + + Threshold met + + + + + Got SOP + + + + + DC offset + + + + + CRC + + + + + + + + + 0 + 0 + + + + Imag + + + + + + + + 0 + 0 + + + + + I + + + + + Q + + + + + Mag Sq + + + + + FM demod + + + + + Gaussian + + + + + RX buf + + + + + Correlation + + + + + Threshold met + + + + + Got SOP + + + + + DC offset + + + + + CRC + + + + + + + + + + + 200 + 250 + + + + + 8 + + + + + + + + + + + + + RollupWidget + QWidget +
gui/rollupwidget.h
+ 1 +
+ + ValueDialZ + QWidget +
gui/valuedialz.h
+ 1 +
+ + LevelMeterSignalDB + QWidget +
gui/levelmeter.h
+ 1 +
+ + ButtonSwitch + QToolButton +
gui/buttonswitch.h
+
+ + GLScope + QWidget +
gui/glscope.h
+ 1 +
+ + GLScopeGUI + QWidget +
gui/glscopegui.h
+ 1 +
+
+ + deltaFrequency + rfBW + fmDev + threshold + udpEnabled + filterSerial + logEnable + logFilename + logOpen + clearTable + frames + channel1 + channel2 + + + + + + +
diff --git a/plugins/channelrx/demodradiosonde/radiosondedemodplugin.cpp b/plugins/channelrx/demodradiosonde/radiosondedemodplugin.cpp new file mode 100644 index 000000000..95236dfa6 --- /dev/null +++ b/plugins/channelrx/demodradiosonde/radiosondedemodplugin.cpp @@ -0,0 +1,92 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016 Edouard Griffiths, F4EXB // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include "plugin/pluginapi.h" + +#ifndef SERVER_MODE +#include "radiosondedemodgui.h" +#endif +#include "radiosondedemod.h" +#include "radiosondedemodwebapiadapter.h" +#include "radiosondedemodplugin.h" + +const PluginDescriptor RadiosondeDemodPlugin::m_pluginDescriptor = { + RadiosondeDemod::m_channelId, + QStringLiteral("Radiosonde Demodulator"), + QStringLiteral("6.20.0"), + QStringLiteral("(c) Jon Beniston, M7RCE"), + QStringLiteral("https://github.com/f4exb/sdrangel"), + true, + QStringLiteral("https://github.com/f4exb/sdrangel") +}; + +RadiosondeDemodPlugin::RadiosondeDemodPlugin(QObject* parent) : + QObject(parent), + m_pluginAPI(0) +{ +} + +const PluginDescriptor& RadiosondeDemodPlugin::getPluginDescriptor() const +{ + return m_pluginDescriptor; +} + +void RadiosondeDemodPlugin::initPlugin(PluginAPI* pluginAPI) +{ + m_pluginAPI = pluginAPI; + + m_pluginAPI->registerRxChannel(RadiosondeDemod::m_channelIdURI, RadiosondeDemod::m_channelId, this); +} + +void RadiosondeDemodPlugin::createRxChannel(DeviceAPI *deviceAPI, BasebandSampleSink **bs, ChannelAPI **cs) const +{ + if (bs || cs) + { + RadiosondeDemod *instance = new RadiosondeDemod(deviceAPI); + + if (bs) { + *bs = instance; + } + + if (cs) { + *cs = instance; + } + } +} + +#ifdef SERVER_MODE +ChannelGUI* RadiosondeDemodPlugin::createRxChannelGUI( + DeviceUISet *deviceUISet, + BasebandSampleSink *rxChannel) const +{ + (void) deviceUISet; + (void) rxChannel; + return 0; +} +#else +ChannelGUI* RadiosondeDemodPlugin::createRxChannelGUI(DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) const +{ + return RadiosondeDemodGUI::create(m_pluginAPI, deviceUISet, rxChannel); +} +#endif + +ChannelWebAPIAdapter* RadiosondeDemodPlugin::createChannelWebAPIAdapter() const +{ + return new RadiosondeDemodWebAPIAdapter(); +} diff --git a/plugins/channelrx/demodradiosonde/radiosondedemodplugin.h b/plugins/channelrx/demodradiosonde/radiosondedemodplugin.h new file mode 100644 index 000000000..0a09a5bc2 --- /dev/null +++ b/plugins/channelrx/demodradiosonde/radiosondedemodplugin.h @@ -0,0 +1,49 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016 Edouard Griffiths, F4EXB // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_RADIOSONDEDEMODPLUGIN_H +#define INCLUDE_RADIOSONDEDEMODPLUGIN_H + +#include +#include "plugin/plugininterface.h" + +class DeviceUISet; +class BasebandSampleSink; + +class RadiosondeDemodPlugin : public QObject, PluginInterface { + Q_OBJECT + Q_INTERFACES(PluginInterface) + Q_PLUGIN_METADATA(IID "sdrangel.channel.radiosondedemod") + +public: + explicit RadiosondeDemodPlugin(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_RADIOSONDEDEMODPLUGIN_H diff --git a/plugins/channelrx/demodradiosonde/radiosondedemodsettings.cpp b/plugins/channelrx/demodradiosonde/radiosondedemodsettings.cpp new file mode 100644 index 000000000..03d22fb0d --- /dev/null +++ b/plugins/channelrx/demodradiosonde/radiosondedemodsettings.cpp @@ -0,0 +1,200 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2015 Edouard Griffiths, F4EXB. // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "dsp/dspengine.h" +#include "util/simpleserializer.h" +#include "settings/serializable.h" +#include "radiosondedemodsettings.h" + +RadiosondeDemodSettings::RadiosondeDemodSettings() : + m_channelMarker(nullptr), + m_scopeGUI(nullptr), + m_rollupState(nullptr) +{ + resetToDefaults(); +} + +void RadiosondeDemodSettings::resetToDefaults() +{ + m_baud = 4800; // Fixed for RS41 - may change for others + m_inputFrequencyOffset = 0; + m_rfBandwidth = 9600.0f; + m_fmDeviation = 2400.0f; + m_correlationThreshold = 450; + m_filterSerial = ""; + m_udpEnabled = false; + m_udpAddress = "127.0.0.1"; + m_udpPort = 9999; + m_scopeCh1 = 5; + m_scopeCh2 = 6; + m_logFilename = "radiosonde_log.csv"; + m_logEnabled = false; + m_rgbColor = QColor(102, 0, 102).rgb(); + m_title = "Radiosonde Demodulator"; + m_streamIndex = 0; + m_useReverseAPI = false; + m_reverseAPIAddress = "127.0.0.1"; + m_reverseAPIPort = 8888; + m_reverseAPIDeviceIndex = 0; + m_reverseAPIChannelIndex = 0; + + for (int i = 0; i < RADIOSONDEDEMOD_FRAME_COLUMNS; i++) + { + m_frameColumnIndexes[i] = i; + m_frameColumnSizes[i] = -1; // Autosize + } +} + +QByteArray RadiosondeDemodSettings::serialize() const +{ + SimpleSerializer s(1); + + s.writeS32(1, m_inputFrequencyOffset); + s.writeFloat(2, m_rfBandwidth); + s.writeFloat(3, m_fmDeviation); + s.writeFloat(4, m_correlationThreshold); + s.writeString(5, m_filterSerial); + s.writeBool(6, m_udpEnabled); + s.writeString(7, m_udpAddress); + s.writeU32(8, m_udpPort); + s.writeS32(10, m_scopeCh1); + s.writeS32(11, m_scopeCh2); + s.writeU32(12, m_rgbColor); + s.writeString(13, m_title); + + if (m_channelMarker) { + s.writeBlob(14, m_channelMarker->serialize()); + } + + s.writeS32(15, m_streamIndex); + s.writeBool(16, m_useReverseAPI); + s.writeString(17, m_reverseAPIAddress); + s.writeU32(18, m_reverseAPIPort); + s.writeU32(19, m_reverseAPIDeviceIndex); + s.writeU32(20, m_reverseAPIChannelIndex); + s.writeBlob(21, m_scopeGUI->serialize()); + s.writeString(22, m_logFilename); + s.writeBool(23, m_logEnabled); + s.writeS32(24, m_baud); + + if (m_rollupState) { + s.writeBlob(25, m_rollupState->serialize()); + } + + for (int i = 0; i < RADIOSONDEDEMOD_FRAME_COLUMNS; i++) + s.writeS32(100 + i, m_frameColumnIndexes[i]); + for (int i = 0; i < RADIOSONDEDEMOD_FRAME_COLUMNS; i++) + s.writeS32(200 + i, m_frameColumnSizes[i]); + + return s.final(); +} + +bool RadiosondeDemodSettings::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.readFloat(2, &m_rfBandwidth, 16000.0f); + d.readFloat(3, &m_fmDeviation, 4800.0f); + d.readFloat(4, &m_correlationThreshold, 450); + d.readString(5, &m_filterSerial, ""); + d.readBool(6, &m_udpEnabled); + d.readString(7, &m_udpAddress); + d.readU32(8, &utmp); + + if ((utmp > 1023) && (utmp < 65535)) { + m_udpPort = utmp; + } else { + m_udpPort = 9999; + } + + d.readS32(10, &m_scopeCh1, 0); + d.readS32(11, &m_scopeCh2, 0); + d.readU32(12, &m_rgbColor, QColor(102, 0, 102).rgb()); + d.readString(13, &m_title, "Radiosonde Demodulator"); + + if (m_channelMarker) + { + d.readBlob(14, &bytetmp); + m_channelMarker->deserialize(bytetmp); + } + + d.readS32(15, &m_streamIndex, 0); + d.readBool(16, &m_useReverseAPI, false); + d.readString(17, &m_reverseAPIAddress, "127.0.0.1"); + d.readU32(18, &utmp, 0); + + if ((utmp > 1023) && (utmp < 65535)) { + m_reverseAPIPort = utmp; + } else { + m_reverseAPIPort = 8888; + } + + d.readU32(19, &utmp, 0); + m_reverseAPIDeviceIndex = utmp > 99 ? 99 : utmp; + d.readU32(20, &utmp, 0); + m_reverseAPIChannelIndex = utmp > 99 ? 99 : utmp; + + if (m_scopeGUI) + { + d.readBlob(21, &bytetmp); + m_scopeGUI->deserialize(bytetmp); + } + + d.readString(22, &m_logFilename, "radiosonde_log.csv"); + d.readBool(23, &m_logEnabled, false); + d.readS32(24, &m_baud, 9600); + + if (m_rollupState) + { + d.readBlob(25, &bytetmp); + m_rollupState->deserialize(bytetmp); + } + + for (int i = 0; i < RADIOSONDEDEMOD_FRAME_COLUMNS; i++) { + d.readS32(100 + i, &m_frameColumnIndexes[i], i); + } + + for (int i = 0; i < RADIOSONDEDEMOD_FRAME_COLUMNS; i++) { + d.readS32(200 + i, &m_frameColumnSizes[i], -1); + } + + return true; + } + else + { + resetToDefaults(); + return false; + } +} + + diff --git a/plugins/channelrx/demodradiosonde/radiosondedemodsettings.h b/plugins/channelrx/demodradiosonde/radiosondedemodsettings.h new file mode 100644 index 000000000..76a4fe839 --- /dev/null +++ b/plugins/channelrx/demodradiosonde/radiosondedemodsettings.h @@ -0,0 +1,75 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2017 Edouard Griffiths, F4EXB. // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_RADIOSONDEDEMODSETTINGS_H +#define INCLUDE_RADIOSONDEDEMODSETTINGS_H + +#include +#include + +#include "dsp/dsptypes.h" + +class Serializable; + +// Number of columns in the tables +#define RADIOSONDEDEMOD_FRAME_COLUMNS 26 + +struct RadiosondeDemodSettings +{ + qint32 m_baud; + qint32 m_inputFrequencyOffset; + Real m_rfBandwidth; + Real m_fmDeviation; + Real m_correlationThreshold; + QString m_filterSerial; + bool m_udpEnabled; + QString m_udpAddress; + uint16_t m_udpPort; + int m_scopeCh1; + int m_scopeCh2; + + QString m_logFilename; + bool m_logEnabled; + + 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; + Serializable *m_scopeGUI; + Serializable *m_rollupState; + + int m_frameColumnIndexes[RADIOSONDEDEMOD_FRAME_COLUMNS];//!< How the columns are ordered in the table + int m_frameColumnSizes[RADIOSONDEDEMOD_FRAME_COLUMNS]; //!< Size of the columns in the table + + static const int RADIOSONDEDEMOD_CHANNEL_SAMPLE_RATE = 57600; //!< 12x 4800 baud rate (use even multiple so Gausian filter has odd number of taps) + + RadiosondeDemodSettings(); + 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_RADIOSONDEDEMODSETTINGS_H */ diff --git a/plugins/channelrx/demodradiosonde/radiosondedemodsink.cpp b/plugins/channelrx/demodradiosonde/radiosondedemodsink.cpp new file mode 100644 index 000000000..8a2220fd7 --- /dev/null +++ b/plugins/channelrx/demodradiosonde/radiosondedemodsink.cpp @@ -0,0 +1,576 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include + +#include "dsp/dspengine.h" +#include "dsp/datafifo.h" +#include "dsp/scopevis.h" +#include "util/db.h" +#include "util/stepfunctions.h" +#include "util/reedsolomon.h" +#include "maincore.h" + +#include "radiosondedemod.h" +#include "radiosondedemodsink.h" + +const uint8_t RadiosondeDemodSink::m_descramble[64] = { + 0x96, 0x83, 0x3E, 0x51, 0xB1, 0x49, 0x08, 0x98, + 0x32, 0x05, 0x59, 0x0E, 0xF9, 0x44, 0xC6, 0x26, + 0x21, 0x60, 0xC2, 0xEA, 0x79, 0x5D, 0x6D, 0xA1, + 0x54, 0x69, 0x47, 0x0C, 0xDC, 0xE8, 0x5C, 0xF1, + 0xF7, 0x76, 0x82, 0x7F, 0x07, 0x99, 0xA2, 0x2C, + 0x93, 0x7C, 0x30, 0x63, 0xF5, 0x10, 0x2E, 0x61, + 0xD0, 0xBC, 0xB4, 0xB6, 0x06, 0xAA, 0xF4, 0x23, + 0x78, 0x6E, 0x3B, 0xAE, 0xBF, 0x7B, 0x4C, 0xC1 +}; + +RadiosondeDemodSink::RadiosondeDemodSink(RadiosondeDemod *radiosondeDemod) : + m_scopeSink(nullptr), + m_radiosondeDemod(radiosondeDemod), + m_channelSampleRate(RadiosondeDemodSettings::RADIOSONDEDEMOD_CHANNEL_SAMPLE_RATE), + m_channelFrequencyOffset(0), + m_magsqSum(0.0f), + m_magsqPeak(0.0f), + m_magsqCount(0), + m_messageQueueToChannel(nullptr), + m_rxBuf(nullptr), + m_train(nullptr), + m_sampleBufferIndex(0) +{ + m_magsq = 0.0; + + m_demodBuffer.resize(1<<12); + m_demodBufferFill = 0; + m_sampleBuffer.resize(m_sampleBufferSize); + + applySettings(m_settings, true); + applyChannelSettings(m_channelSampleRate, m_channelFrequencyOffset, true); +} + +RadiosondeDemodSink::~RadiosondeDemodSink() +{ + delete[] m_rxBuf; + delete[] m_train; +} + +void RadiosondeDemodSink::sampleToScope(Complex sample) +{ + if (m_scopeSink) + { + Real r = std::real(sample) * SDR_RX_SCALEF; + Real i = std::imag(sample) * SDR_RX_SCALEF; + m_sampleBuffer[m_sampleBufferIndex++] = Sample(r, i); + + if (m_sampleBufferIndex == m_sampleBufferSize) + { + std::vector vbegin; + vbegin.push_back(m_sampleBuffer.begin()); + m_scopeSink->feed(vbegin, m_sampleBufferSize); + m_sampleBufferIndex = 0; + } + } +} + +void RadiosondeDemodSink::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 RadiosondeDemodSink::processOneSample(Complex &ci) +{ + Complex ca; + + // FM demodulation + double magsqRaw; + Real deviation; + Real fmDemod = m_phaseDiscri.phaseDiscriminatorDelta(ci, magsqRaw, deviation); + + // Calculate average and peak levels for level meter + Real magsq = magsqRaw / (SDR_RX_SCALED*SDR_RX_SCALED); + m_movingAverage(magsq); + m_magsq = m_movingAverage.asDouble(); + m_magsqSum += magsq; + if (magsq > m_magsqPeak) + { + m_magsqPeak = magsq; + } + m_magsqCount++; + + // Gaussian filter + Real filt = m_pulseShape.filter(fmDemod); + + // An input frequency offset corresponds to a DC offset after FM demodulation + // What frequency offset is RS41 specified too? + // We need to remove this, otherwise it may effect the sampling + // To calculate what it is, we sum the training sequence, which should be zero + + // Clip, as large noise can result in high correlation + // Don't clip to 1.0 - as there may be some DC offset (1k/4.8k max dev=0.2) + Real filtClipped; + filtClipped = std::fmax(-1.4, std::fmin(1.4, filt)); + + // Buffer filtered samples. We buffer enough samples for a max length message + // before trying to demod, so false triggering can't make us miss anything + m_rxBuf[m_rxBufIdx] = filtClipped; + m_rxBufIdx = (m_rxBufIdx + 1) % m_rxBufLength; + m_rxBufCnt = std::min(m_rxBufCnt + 1, m_rxBufLength); + + Real corr = 0.0f; + bool scopeCRCValid = false; + bool scopeCRCInvalid = false; + Real dcOffset = 0.0f; + bool thresholdMet = false; + bool gotSOP = false; + + if ((m_rxBufCnt >= m_rxBufLength)) + { + // Correlate with training sequence + corr = correlate(m_rxBufIdx); + + // If we meet threshold, try to demod + // Take abs value, to account for both initial phases + thresholdMet = fabs(corr) >= m_settings.m_correlationThreshold; + if (thresholdMet) + { + // Try to see if starting at a later sample improves correlation + int maxCorrOffset = 0; + Real maxCorr; + Real initCorr = fabs(corr); + do + { + maxCorr = fabs(corr); + maxCorrOffset++; + corr = correlate(m_rxBufIdx + maxCorrOffset); + } + while (fabs(corr) > maxCorr); + maxCorrOffset--; + + // Calculate mean of preamble as DC offset (as it should be 0 on an ideal signal) + Real trainingSum = 0.0f; + for (int i = 0; i < m_correlationLength; i++) + { + int j = (m_rxBufIdx + maxCorrOffset + i) % m_rxBufLength; + trainingSum += m_rxBuf[j]; + } + dcOffset = trainingSum/m_correlationLength; + + // Start demod after (most of) preamble + int x = (m_rxBufIdx + maxCorrOffset + m_correlationLength*3/4 + 0) % m_rxBufLength; + + // Attempt to demodulate + uint64_t bits = 0; + int bitCount = 0; + int onesCount = 0; + int byteCount = 0; + int symbolPrev = 0; + QList sampleIdxs; + for (int sampleIdx = 0; sampleIdx < m_rxBufLength; sampleIdx += m_samplesPerSymbol) + { + // Sum and slice + // Summing 3 samples seems to give a very small improvement vs just using 1 + int sampleCnt = 3; + int sampleOffset = -1; + Real sampleSum = 0.0f; + for (int i = 0; i < sampleCnt; i++) { + sampleSum += m_rxBuf[(x + sampleOffset + i) % m_rxBufLength] - dcOffset; + sampleIdxs.append((x + sampleOffset + i) % m_rxBufLength); + } + int symbol = sampleSum >= 0.0f ? 1 : 0; + + // Move to next symbol + x = (x + m_samplesPerSymbol) % m_rxBufLength; + + // Symbols map directly to bits + int bit = symbol; + + // Store in shift reg (little endian) + bits |= ((uint64_t)bit) << bitCount; + bitCount++; + + if (gotSOP) + { + if (bitCount == 8) + { + // Got a complete byte + m_bytes[byteCount] = bits; + byteCount++; + bits = 0; + bitCount = 0; + + if (byteCount >= RADIOSONDE_LENGTH_STD) + { + // Get expected length of frame + uint8_t frameType = m_bytes[RADIOSONDE_OFFSET_FRAME_TYPE] ^ m_descramble[RADIOSONDE_OFFSET_FRAME_TYPE]; + int length = RS41Frame::getFrameLength(frameType); + + // Have we received a complete frame? + if (byteCount == length) + { + int firstError; + bool ok = processFrame(length, corr, sampleIdx, &firstError); + scopeCRCValid = ok; + scopeCRCInvalid = !ok; + break; + } + } + } + } + else if (bits == 0xf812962211cab610ULL) // Scrambled header + { + // Start of packet + gotSOP = true; + bits = 0; + bitCount = 0; + m_bytes[0] = 0x10; + m_bytes[1] = 0xb6; + m_bytes[2] = 0xca; + m_bytes[3] = 0x11; + m_bytes[4] = 0x22; + m_bytes[5] = 0x96; + m_bytes[6] = 0x12; + m_bytes[7] = 0xf8; + byteCount = 8; + } + else + { + if (bitCount == 64) + { + bits >>= 1; + bitCount--; + } + if (sampleIdx >= 16 * 8 * m_samplesPerSymbol) + { + // Too many bits without receving header + break; + } + } + } + // printf("\n"); + } + } + + // Select signals to feed to scope + Complex scopeSample; + switch (m_settings.m_scopeCh1) + { + case 0: + scopeSample.real(ci.real() / SDR_RX_SCALEF); + break; + case 1: + scopeSample.real(ci.imag() / SDR_RX_SCALEF); + break; + case 2: + scopeSample.real(magsq); + break; + case 3: + scopeSample.real(fmDemod); + break; + case 4: + scopeSample.real(filt); + break; + case 5: + scopeSample.real(m_rxBuf[m_rxBufIdx]); + break; + case 6: + scopeSample.real(corr / 100.0); + break; + case 7: + scopeSample.real(thresholdMet); + break; + case 8: + scopeSample.real(gotSOP); + break; + case 9: + scopeSample.real(dcOffset); + break; + case 10: + scopeSample.real(scopeCRCValid ? 1.0 : (scopeCRCInvalid ? -1.0 : 0)); + break; + } + switch (m_settings.m_scopeCh2) + { + case 0: + scopeSample.imag(ci.real() / SDR_RX_SCALEF); + break; + case 1: + scopeSample.imag(ci.imag() / SDR_RX_SCALEF); + break; + case 2: + scopeSample.imag(magsq); + break; + case 3: + scopeSample.imag(fmDemod); + break; + case 4: + scopeSample.imag(filt); + break; + case 5: + scopeSample.imag(m_rxBuf[m_rxBufIdx]); + break; + case 6: + scopeSample.imag(corr / 100.0); + break; + case 7: + scopeSample.imag(thresholdMet); + break; + case 8: + scopeSample.imag(gotSOP); + break; + case 9: + scopeSample.imag(dcOffset); + break; + case 10: + scopeSample.imag(scopeCRCValid ? 1.0 : (scopeCRCInvalid ? -1.0 : 0)); + break; + } + sampleToScope(scopeSample); + + // Send demod signal to Demod Analzyer feature + m_demodBuffer[m_demodBufferFill++] = fmDemod * std::numeric_limits::max(); + + if (m_demodBufferFill >= m_demodBuffer.size()) + { + QList dataPipes; + MainCore::instance()->getDataPipes().getDataPipes(m_channel, "demod", dataPipes); + + if (dataPipes.size() > 0) + { + QList::iterator it = dataPipes.begin(); + + for (; it != dataPipes.end(); ++it) + { + DataFifo *fifo = qobject_cast((*it)->m_element); + + if (fifo) { + fifo->write((quint8*) &m_demodBuffer[0], m_demodBuffer.size() * sizeof(qint16), DataFifo::DataTypeI16); + } + } + } + + m_demodBufferFill = 0; + } +} + +// Correlate received signal with training sequence +// Note that DC offset doesn't matter for this +Real RadiosondeDemodSink::correlate(int idx) const +{ + Real corr = 0.0f; + for (int i = 0; i < m_correlationLength; i++) + { + int j = (idx + i) % m_rxBufLength; + corr += m_train[i] * m_rxBuf[j]; + } + return corr; +} + +bool RadiosondeDemodSink::processFrame(int length, float corr, int sampleIdx, int *firstError) +{ + // Descramble + for (int i = 0; i < length; i++) { + m_bytes[i] = m_bytes[i] ^ m_descramble[i & 0x3f]; + } + + // Reed-Solomon Error Correction + int errorsCorrected = reedSolomonErrorCorrection(); + if (errorsCorrected >= 0) + { + // Check per-block CRCs are correct + if (checkCRCs(length)) + { + if (getMessageQueueToChannel()) + { + QByteArray rxPacket((char *)m_bytes, length); + RadiosondeDemod::MsgMessage *msg = RadiosondeDemod::MsgMessage::create(rxPacket, errorsCorrected, corr); + getMessageQueueToChannel()->push(msg); + } + + // Skip over received packet, so we don't try to re-demodulate it + m_rxBufCnt -= sampleIdx; + return true; + } + } + return false; +} + +// Reed Solomon error correction +// Returns number of errors corrected, or -1 if there are uncorrectable errors +int RadiosondeDemodSink::reedSolomonErrorCorrection() +{ + ReedSolomon::RS rs; + int errorsCorrected = 0; + + for (int i = 0; (i < RADIOSONDE_RS_INTERLEAVE) && (errorsCorrected >= 0); i++) + { + // Deinterleave and reverse order + uint8_t rsData[RADIOSONDE_RS_N]; + + memset(rsData, 0, RADIOSONDE_RS_PAD); + for (int j = 0; j < RADIOSONDE_RS_DATA; j++) { + rsData[RADIOSONDE_RS_K-1-j] = m_bytes[RADIOSONDE_OFFSET_FRAME_TYPE+j*RADIOSONDE_RS_INTERLEAVE+i]; + } + for (int j = 0; j < RADIOSONDE_RS_2T; j++) { + rsData[RADIOSONDE_RS_N-1-j] = m_bytes[RADIOSONDE_OFFSET_RS+i*RADIOSONDE_RS_2T+j]; + } + + // Detect and correct errors + int errors = rs.decode(&rsData[0], RADIOSONDE_RS_K); // FIXME: Indicate 0 padding? + if (errors >= 0) { + errorsCorrected += errors; + } else { + // Uncorrectable errors + return -1; + } + + // Restore corrected data + for (int j = 0; j < RADIOSONDE_RS_DATA; j++) { + m_bytes[RADIOSONDE_OFFSET_FRAME_TYPE+j*RADIOSONDE_RS_INTERLEAVE+i] = rsData[RADIOSONDE_RS_K-1-j]; + } + + } + return errorsCorrected; +} + +// Check per-block CRCs +// We could pass partial frames that have some correct CRCs, but for now, whole frame has to be correct +bool RadiosondeDemodSink::checkCRCs(int length) +{ + for (int i = RADIOSONDE_OFFSET_BLOCK_0; i < length; ) + { + uint8_t blockID = m_bytes[i+0]; + uint8_t blockLength = m_bytes[i+1]; + uint16_t rxCrc = m_bytes[i+2+blockLength] | (m_bytes[i+2+blockLength+1] << 8); + // CRC doesn't include ID/len - so these can be wrong + m_crc.init(); + m_crc.calculate(&m_bytes[i+2], blockLength); + uint16_t calcCrc = m_crc.get(); + if (calcCrc != rxCrc) { + return false; + } + i += blockLength+4; + } + return true; +} + +void RadiosondeDemodSink::applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force) +{ + qDebug() << "RadiosondeDemodSink::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) RadiosondeDemodSettings::RADIOSONDEDEMOD_CHANNEL_SAMPLE_RATE; + m_interpolatorDistanceRemain = m_interpolatorDistance; + } + + m_channelSampleRate = channelSampleRate; + m_channelFrequencyOffset = channelFrequencyOffset; + m_samplesPerSymbol = RadiosondeDemodSettings::RADIOSONDEDEMOD_CHANNEL_SAMPLE_RATE / m_settings.m_baud; + qDebug() << "RadiosondeDemodSink::applyChannelSettings: m_samplesPerSymbol: " << m_samplesPerSymbol; +} + +void RadiosondeDemodSink::applySettings(const RadiosondeDemodSettings& settings, bool force) +{ + qDebug() << "RadiosondeDemodSink::applySettings:" + << " 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) RadiosondeDemodSettings::RADIOSONDEDEMOD_CHANNEL_SAMPLE_RATE; + m_interpolatorDistanceRemain = m_interpolatorDistance; + m_lowpass.create(301, RadiosondeDemodSettings::RADIOSONDEDEMOD_CHANNEL_SAMPLE_RATE, settings.m_rfBandwidth / 2.0f); + } + if ((settings.m_fmDeviation != m_settings.m_fmDeviation) || force) + { + m_phaseDiscri.setFMScaling(RadiosondeDemodSettings::RADIOSONDEDEMOD_CHANNEL_SAMPLE_RATE / (2.0f * settings.m_fmDeviation)); + } + + if ((settings.m_baud != m_settings.m_baud) || force) + { + m_samplesPerSymbol = RadiosondeDemodSettings::RADIOSONDEDEMOD_CHANNEL_SAMPLE_RATE / settings.m_baud; + qDebug() << "RadiosondeDemodSink::applySettings: m_samplesPerSymbol: " << m_samplesPerSymbol << " baud " << settings.m_baud; + + // What value to use for BT? RFIC is Si4032 - its datasheet only appears to support 0.5 + m_pulseShape.create(0.5, 3, m_samplesPerSymbol); + + // Recieve buffer, long enough for one max length message + delete[] m_rxBuf; + m_rxBufLength = RADIOSONDEDEMOD_MAX_BYTES*8*m_samplesPerSymbol; + m_rxBuf = new Real[m_rxBufLength]; + m_rxBufIdx = 0; + m_rxBufCnt = 0; + + // Create training sequence for correlation + delete[] m_train; + + const int correlateBits = 200; // Preamble is 320bits - leave some for AGC (and clock recovery eventually) + m_correlationLength = correlateBits*m_samplesPerSymbol; // Don't want to use header, as we want to calculate DC offset + m_train = new Real[m_correlationLength](); + + // Pulse shape filter takes a few symbols before outputting expected shape + for (int j = 0; j < m_samplesPerSymbol; j++) { + m_pulseShape.filter(-1.0f); + } + for (int j = 0; j < m_samplesPerSymbol; j++) { + m_pulseShape.filter(1.0f); + } + for (int i = 0; i < correlateBits; i++) + { + for (int j = 0; j < m_samplesPerSymbol; j++) { + m_train[i*m_samplesPerSymbol+j] = -m_pulseShape.filter((i&1) * 2.0f - 1.0f); + } + } + } + + m_settings = settings; +} diff --git a/plugins/channelrx/demodradiosonde/radiosondedemodsink.h b/plugins/channelrx/demodradiosonde/radiosondedemodsink.h new file mode 100644 index 000000000..9a89fbf52 --- /dev/null +++ b/plugins/channelrx/demodradiosonde/radiosondedemodsink.h @@ -0,0 +1,143 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_RADIOSONDEDEMODSINK_H +#define INCLUDE_RADIOSONDEDEMODSINK_H + +#include + +#include "dsp/channelsamplesink.h" +#include "dsp/phasediscri.h" +#include "dsp/nco.h" +#include "dsp/interpolator.h" +#include "dsp/firfilter.h" +#include "dsp/gaussian.h" +#include "util/movingaverage.h" +#include "util/doublebufferfifo.h" +#include "util/messagequeue.h" +#include "util/crc.h" + +#include "radiosondedemodsettings.h" + +// Length of preamble (40 bytes) and frame (std 320 bytes - extended 518) +#define RADIOSONDEDEMOD_MAX_BYTES (40+518) + +class ChannelAPI; +class RadiosondeDemod; +class ScopeVis; + +class RadiosondeDemodSink : public ChannelSampleSink { +public: + RadiosondeDemodSink(RadiosondeDemod *radiosondeDemod); + ~RadiosondeDemodSink(); + + 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 RadiosondeDemodSettings& 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; + }; + + ScopeVis* m_scopeSink; // Scope GUI to display baseband waveform + RadiosondeDemod *m_radiosondeDemod; + RadiosondeDemodSettings m_settings; + ChannelAPI *m_channel; + int m_channelSampleRate; + int m_channelFrequencyOffset; + int m_samplesPerSymbol; // Number of samples per symbol + + 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_lowpass; // RF input filter + PhaseDiscriminators m_phaseDiscri; // FM demodulator + Gaussian m_pulseShape; // Pulse shaping filter + Real *m_rxBuf; // Receive sample buffer, large enough for one max length messsage + int m_rxBufLength; // Size in elements in m_rxBuf + int m_rxBufIdx; // Index in to circular buffer + int m_rxBufCnt; // Number of valid samples in buffer + Real *m_train; // Training sequence to look for + int m_correlationLength; + + unsigned char m_bytes[RADIOSONDEDEMOD_MAX_BYTES]; + crc16ccitt m_crc; + + QVector m_demodBuffer; + int m_demodBufferFill; + + SampleVector m_sampleBuffer; + static const int m_sampleBufferSize = RadiosondeDemodSettings::RADIOSONDEDEMOD_CHANNEL_SAMPLE_RATE / 20; + int m_sampleBufferIndex; + + static const uint8_t m_descramble[64]; + + MessageQueue *getMessageQueueToChannel() { return m_messageQueueToChannel; } + void processOneSample(Complex &ci); + Real correlate(int idx) const; + bool processFrame(int length, float corr, int sampleIdx, int *firstError); + int reedSolomonErrorCorrection(); + bool checkCRCs(int length); + void sampleToScope(Complex sample); +}; + +#endif // INCLUDE_RADIOSONDEDEMODSINK_H diff --git a/plugins/channelrx/demodradiosonde/radiosondedemodwebapiadapter.cpp b/plugins/channelrx/demodradiosonde/radiosondedemodwebapiadapter.cpp new file mode 100644 index 000000000..5d5133258 --- /dev/null +++ b/plugins/channelrx/demodradiosonde/radiosondedemodwebapiadapter.cpp @@ -0,0 +1,52 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB. // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "SWGChannelSettings.h" +#include "radiosondedemod.h" +#include "radiosondedemodwebapiadapter.h" + +RadiosondeDemodWebAPIAdapter::RadiosondeDemodWebAPIAdapter() +{} + +RadiosondeDemodWebAPIAdapter::~RadiosondeDemodWebAPIAdapter() +{} + +int RadiosondeDemodWebAPIAdapter::webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setRadiosondeDemodSettings(new SWGSDRangel::SWGRadiosondeDemodSettings()); + response.getRadiosondeDemodSettings()->init(); + RadiosondeDemod::webapiFormatChannelSettings(response, m_settings); + + return 200; +} + +int RadiosondeDemodWebAPIAdapter::webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) force; + (void) errorMessage; + RadiosondeDemod::webapiUpdateChannelSettings(m_settings, channelSettingsKeys, response); + + return 200; +} diff --git a/plugins/channelrx/demodradiosonde/radiosondedemodwebapiadapter.h b/plugins/channelrx/demodradiosonde/radiosondedemodwebapiadapter.h new file mode 100644 index 000000000..22f1b0129 --- /dev/null +++ b/plugins/channelrx/demodradiosonde/radiosondedemodwebapiadapter.h @@ -0,0 +1,50 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB. // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_RADIOSONDEDEMOD_WEBAPIADAPTER_H +#define INCLUDE_RADIOSONDEDEMOD_WEBAPIADAPTER_H + +#include "channel/channelwebapiadapter.h" +#include "radiosondedemodsettings.h" + +/** + * Standalone API adapter only for the settings + */ +class RadiosondeDemodWebAPIAdapter : public ChannelWebAPIAdapter { +public: + RadiosondeDemodWebAPIAdapter(); + virtual ~RadiosondeDemodWebAPIAdapter(); + + 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: + RadiosondeDemodSettings m_settings; +}; + +#endif // INCLUDE_RADIOSONDEDEMOD_WEBAPIADAPTER_H diff --git a/plugins/channelrx/demodradiosonde/readme.md b/plugins/channelrx/demodradiosonde/readme.md new file mode 100644 index 000000000..4955997ed --- /dev/null +++ b/plugins/channelrx/demodradiosonde/readme.md @@ -0,0 +1,106 @@ +

Radiosonde demodulator plugin

+ +

Introduction

+ +This plugin can be used to demodulate RS41 radiosonde weather ballon signals. Radiosondes typically transmit on 400-406MHz and are in the sky around the world for around 1 hour around 00:00 UTC. + +RS41 radiosondes transmit data frames every second, containing position, velocity and PTU (Pressure, Temperature and Humidity) readings. The radios use GFSK modulation, with ±2.4kHz deviation at 4,800 baud. Reed Solomon encoding is used for ECC (Error Checking and Correction). + +The Radiosonde demodulator can forward received data to the [Radiosone feature](../../feature/radiosonde/readme.md), which can plot charts showing how altitude and PTU vary over time, and also plot the position of the radiosonde on the 2D and 3D maps. + +

Interface

+ +![Radiosonde Demodulator plugin GUI](../../../doc/img/RadiosondeDemod_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: BW - RF Bandwidth

+ +This specifies the bandwidth of a LPF that is applied to the input signal to limit the RF bandwidth. For RS41 radiosondes, this can be 9.6kHz. + +

5: Dev - Frequency deviation

+ +Adjusts the expected frequency deviation in 0.1 kHz steps from 1 to 5 kHz. Typical value to RS41 is 2.4kHz. + +

6: TH - Correlation Threshold

+ +The correlation threshold between the received signal and the preamble (training sequence). A lower value may be able to demodulate weaker signals, but increases processor usage. The default value is 450. + +

7: UDP

+ +When checked, received receives are forwarded to the specified UDP address (12) and port (13). + +

8: UDP address

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

9: UDP port

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

10: Find

+ +Entering a regular expression in the Find field displays only frames where the radiosonde serial number matches the given regular expression. + +

11: Start/stop Logging Frames to .csv File

+ +When checked, writes all received frames to a .csv file. + +

14: .csv Log Filename

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

15: Read Data from .csv File

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

11: Clear Data from table

+ +Pressing this button clears all data from the table. + +

Received Data Table

+ +The received frames table displays information about each radiosonde frame received. + +![Radiosonde Demodulator plugin table](../../../doc/img/RadiosondeDemod_plugin_table.png) + +* Date - The date the frame was received. +* Time - The time the frame was received. +* Serial - The serial number of the radiosonde. Double clicking on this column will search for the radiosone on https://sondehub.org/ +* Frame - Frame number +* Phase - Flight phase: On ground, Ascent and Descent. +* Lat (°) - Latitude in degrees, North positive. Double clicking on this column will search for the radiosonde on the Map. +* Lon (°) - Longitude in degrees, East positive. Double clicking on this column will search for the radiosonde on the Map. +* Alt (m) - Altitude in metres. +* Spd (km/h) - Speed over ground in kilometres per hour. +* VR (m/s) - Vertical climb rate in metres per second. +* Hdg (°) - Heading in degrees. +* P (hPA) - Air pressure in hectopascals. Not all RS41s include a pressure sensor. A value ending with 'U' indicates a uncalibrated estimate and may be inaccurate. +* T (°C) - Air temperature in degrees Celsius. A value ending with 'U' indicates a uncalibrated estimate and may be inaccurate. +* U (%) - Relative humidity in percent. A value ending with 'U' indicates a uncalibrated estimate and may be inaccurate. +* Bat (V) - Battery voltage in Volts. +* Bat - Battery status: OK or low. +* PCB (°C) - Temperature of PCB. +* PWM (%) - Humidity sensor heater PWM (Pulse Width Modulation) setting, in percent. +* TX (%) - Transmit power in percent. +* Max SF - Maximum subframe number. +* SF No. - Subframe number of subframe data in this frame. +* Subframe - Subframe data. +* GPS Time - GPS date and time on board radiosonde. GPS time is offset 18 seconds from UTC. +* GPS Sats - Number of GPS satellites used in position estimate. +* ECC - Number of symbol errors corrected by Reed Solomon ECC. +* Corr - Premable correlation value calculated for the frame. This can be used to choose a value for TH (6). + +Right clicking on the table header allows you to select which columns to show. The columns can be reorderd by left clicking and dragging the column header. Right clicking on an item in the table allows you to copy the value to the clipboard. diff --git a/plugins/feature/CMakeLists.txt b/plugins/feature/CMakeLists.txt index 0f3280b5c..9a00b324d 100644 --- a/plugins/feature/CMakeLists.txt +++ b/plugins/feature/CMakeLists.txt @@ -31,4 +31,7 @@ else() endif() endif() -add_subdirectory(startracker) +if (Qt5Charts_FOUND) + add_subdirectory(radiosonde) + add_subdirectory(startracker) +endif() diff --git a/plugins/feature/radiosonde/CMakeLists.txt b/plugins/feature/radiosonde/CMakeLists.txt new file mode 100644 index 000000000..3a08fa765 --- /dev/null +++ b/plugins/feature/radiosonde/CMakeLists.txt @@ -0,0 +1,60 @@ +project(radiosonde) + +set(radiosonde_SOURCES + radiosonde.cpp + radiosondesettings.cpp + radiosondeplugin.cpp + radiosondewebapiadapter.cpp +) + +set(radiosonde_HEADERS + radiosonde.h + radiosondesettings.h + radiosondeplugin.h + radiosondewebapiadapter.h +) + +include_directories( + ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client +) + +if(NOT SERVER_MODE) + set(radiosonde_SOURCES + ${radiosonde_SOURCES} + radiosondegui.cpp + radiosondegui.ui + radiosonde.qrc + ) + set(radiosonde_HEADERS + ${radiosonde_HEADERS} + radiosondegui.h + ) + + set(TARGET_NAME featureradiosonde) + set(TARGET_LIB Qt5::Widgets Qt5::Charts) + set(TARGET_LIB_GUI "sdrgui") + set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR}) +else() + set(TARGET_NAME featureradiosondesrv) + set(TARGET_LIB "") + set(TARGET_LIB_GUI "") + set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR}) +endif() + +add_library(${TARGET_NAME} SHARED + ${radiosonde_SOURCES} +) + +target_link_libraries(${TARGET_NAME} + Qt5::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/feature/radiosonde/map/ballon.png b/plugins/feature/radiosonde/map/ballon.png new file mode 100644 index 000000000..0241f72f2 Binary files /dev/null and b/plugins/feature/radiosonde/map/ballon.png differ diff --git a/plugins/feature/radiosonde/map/parachute.png b/plugins/feature/radiosonde/map/parachute.png new file mode 100644 index 000000000..8478957b3 Binary files /dev/null and b/plugins/feature/radiosonde/map/parachute.png differ diff --git a/plugins/feature/radiosonde/radiosonde.cpp b/plugins/feature/radiosonde/radiosonde.cpp new file mode 100644 index 000000000..b16f9ecb0 --- /dev/null +++ b/plugins/feature/radiosonde/radiosonde.cpp @@ -0,0 +1,327 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include + +#include "SWGFeatureSettings.h" +#include "SWGFeatureReport.h" +#include "SWGFeatureActions.h" +#include "SWGDeviceState.h" + +#include "dsp/dspengine.h" +#include "device/deviceset.h" +#include "channel/channelapi.h" +#include "feature/featureset.h" +#include "settings/serializable.h" +#include "maincore.h" + +#include "radiosonde.h" + +MESSAGE_CLASS_DEFINITION(Radiosonde::MsgConfigureRadiosonde, Message) + +const char* const Radiosonde::m_featureIdURI = "sdrangel.feature.radiosonde"; +const char* const Radiosonde::m_featureId = "Radiosonde"; + +Radiosonde::Radiosonde(WebAPIAdapterInterface *webAPIAdapterInterface) : + Feature(m_featureIdURI, webAPIAdapterInterface) +{ + qDebug("Radiosonde::Radiosonde: webAPIAdapterInterface: %p", webAPIAdapterInterface); + setObjectName(m_featureId); + m_state = StIdle; + m_errorMessage = "Radiosonde error"; + connect(&m_updatePipesTimer, SIGNAL(timeout()), this, SLOT(updatePipes())); + m_updatePipesTimer.start(1000); + m_networkManager = new QNetworkAccessManager(); + connect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*))); +} + +Radiosonde::~Radiosonde() +{ + disconnect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*))); + delete m_networkManager; +} + +void Radiosonde::start() +{ + qDebug("Radiosonde::start"); + m_state = StRunning; +} + +void Radiosonde::stop() +{ + qDebug("Radiosonde::stop"); + m_state = StIdle; +} + +bool Radiosonde::handleMessage(const Message& cmd) +{ + if (MsgConfigureRadiosonde::match(cmd)) + { + MsgConfigureRadiosonde& cfg = (MsgConfigureRadiosonde&) cmd; + qDebug() << "Radiosonde::handleMessage: MsgConfigureRadiosonde"; + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (MainCore::MsgPacket::match(cmd)) + { + MainCore::MsgPacket& report = (MainCore::MsgPacket&) cmd; + if (getMessageQueueToGUI()) + { + MainCore::MsgPacket *copy = new MainCore::MsgPacket(report); + getMessageQueueToGUI()->push(copy); + } + return true; + } + else + { + return false; + } +} + +void Radiosonde::updatePipes() +{ + QList availablePipes = updateAvailablePipeSources("radiosonde", RadiosondeSettings::m_pipeTypes, RadiosondeSettings::m_pipeURIs, this); + + if (availablePipes != m_availablePipes) { + m_availablePipes = availablePipes; + } +} + +QByteArray Radiosonde::serialize() const +{ + return m_settings.serialize(); +} + +bool Radiosonde::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + MsgConfigureRadiosonde *msg = MsgConfigureRadiosonde::create(m_settings, true); + m_inputMessageQueue.push(msg); + return true; + } + else + { + m_settings.resetToDefaults(); + MsgConfigureRadiosonde *msg = MsgConfigureRadiosonde::create(m_settings, true); + m_inputMessageQueue.push(msg); + return false; + } +} + +void Radiosonde::applySettings(const RadiosondeSettings& settings, bool force) +{ + qDebug() << "Radiosonde::applySettings:" + << " m_title: " << settings.m_title + << " m_rgbColor: " << settings.m_rgbColor + << " m_useReverseAPI: " << settings.m_useReverseAPI + << " m_reverseAPIAddress: " << settings.m_reverseAPIAddress + << " m_reverseAPIPort: " << settings.m_reverseAPIPort + << " m_reverseAPIFeatureSetIndex: " << settings.m_reverseAPIFeatureSetIndex + << " m_reverseAPIFeatureIndex: " << settings.m_reverseAPIFeatureIndex + << " force: " << force; + + QList reverseAPIKeys; + + if ((m_settings.m_title != settings.m_title) || force) { + reverseAPIKeys.append("title"); + } + if ((m_settings.m_rgbColor != settings.m_rgbColor) || force) { + reverseAPIKeys.append("rgbColor"); + } + + if (settings.m_useReverseAPI) + { + bool fullUpdate = ((m_settings.m_useReverseAPI != settings.m_useReverseAPI) && settings.m_useReverseAPI) || + (m_settings.m_reverseAPIAddress != settings.m_reverseAPIAddress) || + (m_settings.m_reverseAPIPort != settings.m_reverseAPIPort) || + (m_settings.m_reverseAPIFeatureSetIndex != settings.m_reverseAPIFeatureSetIndex) || + (m_settings.m_reverseAPIFeatureIndex != settings.m_reverseAPIFeatureIndex); + webapiReverseSendSettings(reverseAPIKeys, settings, fullUpdate || force); + } + + m_settings = settings; +} + +int Radiosonde::webapiSettingsGet( + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setRadiosondeSettings(new SWGSDRangel::SWGRadiosondeSettings()); + response.getRadiosondeSettings()->init(); + webapiFormatFeatureSettings(response, m_settings); + return 200; +} + +int Radiosonde::webapiSettingsPutPatch( + bool force, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + RadiosondeSettings settings = m_settings; + webapiUpdateFeatureSettings(settings, featureSettingsKeys, response); + + MsgConfigureRadiosonde *msg = MsgConfigureRadiosonde::create(settings, force); + m_inputMessageQueue.push(msg); + + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureRadiosonde *msgToGUI = MsgConfigureRadiosonde::create(settings, force); + m_guiMessageQueue->push(msgToGUI); + } + + webapiFormatFeatureSettings(response, settings); + + return 200; +} + +void Radiosonde::webapiFormatFeatureSettings( + SWGSDRangel::SWGFeatureSettings& response, + const RadiosondeSettings& settings) +{ + if (response.getRadiosondeSettings()->getTitle()) { + *response.getRadiosondeSettings()->getTitle() = settings.m_title; + } else { + response.getRadiosondeSettings()->setTitle(new QString(settings.m_title)); + } + + response.getRadiosondeSettings()->setRgbColor(settings.m_rgbColor); + response.getRadiosondeSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0); + + if (response.getRadiosondeSettings()->getReverseApiAddress()) { + *response.getRadiosondeSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress; + } else { + response.getRadiosondeSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress)); + } + + response.getRadiosondeSettings()->setReverseApiPort(settings.m_reverseAPIPort); + response.getRadiosondeSettings()->setReverseApiFeatureSetIndex(settings.m_reverseAPIFeatureSetIndex); + response.getRadiosondeSettings()->setReverseApiFeatureIndex(settings.m_reverseAPIFeatureIndex); + + if (settings.m_rollupState) + { + if (response.getRadiosondeSettings()->getRollupState()) + { + settings.m_rollupState->formatTo(response.getRadiosondeSettings()->getRollupState()); + } + else + { + SWGSDRangel::SWGRollupState *swgRollupState = new SWGSDRangel::SWGRollupState(); + settings.m_rollupState->formatTo(swgRollupState); + response.getRadiosondeSettings()->setRollupState(swgRollupState); + } + } +} + +void Radiosonde::webapiUpdateFeatureSettings( + RadiosondeSettings& settings, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response) +{ + if (featureSettingsKeys.contains("title")) { + settings.m_title = *response.getRadiosondeSettings()->getTitle(); + } + if (featureSettingsKeys.contains("rgbColor")) { + settings.m_rgbColor = response.getRadiosondeSettings()->getRgbColor(); + } + if (featureSettingsKeys.contains("useReverseAPI")) { + settings.m_useReverseAPI = response.getRadiosondeSettings()->getUseReverseApi() != 0; + } + if (featureSettingsKeys.contains("reverseAPIAddress")) { + settings.m_reverseAPIAddress = *response.getRadiosondeSettings()->getReverseApiAddress(); + } + if (featureSettingsKeys.contains("reverseAPIPort")) { + settings.m_reverseAPIPort = response.getRadiosondeSettings()->getReverseApiPort(); + } + if (featureSettingsKeys.contains("reverseAPIFeatureSetIndex")) { + settings.m_reverseAPIFeatureSetIndex = response.getRadiosondeSettings()->getReverseApiFeatureSetIndex(); + } + if (featureSettingsKeys.contains("reverseAPIFeatureIndex")) { + settings.m_reverseAPIFeatureIndex = response.getRadiosondeSettings()->getReverseApiFeatureIndex(); + } + if (settings.m_rollupState && featureSettingsKeys.contains("rollupState")) { + settings.m_rollupState->updateFrom(featureSettingsKeys, response.getRadiosondeSettings()->getRollupState()); + } +} + +void Radiosonde::webapiReverseSendSettings(QList& featureSettingsKeys, const RadiosondeSettings& settings, bool force) +{ + SWGSDRangel::SWGFeatureSettings *swgFeatureSettings = new SWGSDRangel::SWGFeatureSettings(); + // swgFeatureSettings->setOriginatorFeatureIndex(getIndexInDeviceSet()); + // swgFeatureSettings->setOriginatorFeatureSetIndex(getDeviceSetIndex()); + swgFeatureSettings->setFeatureType(new QString("Radiosonde")); + swgFeatureSettings->setRadiosondeSettings(new SWGSDRangel::SWGRadiosondeSettings()); + SWGSDRangel::SWGRadiosondeSettings *swgRadiosondeSettings = swgFeatureSettings->getRadiosondeSettings(); + + // transfer data that has been modified. When force is on transfer all data except reverse API data + + if (featureSettingsKeys.contains("title") || force) { + swgRadiosondeSettings->setTitle(new QString(settings.m_title)); + } + if (featureSettingsKeys.contains("rgbColor") || force) { + swgRadiosondeSettings->setRgbColor(settings.m_rgbColor); + } + + QString channelSettingsURL = QString("http://%1:%2/sdrangel/featureset/%3/feature/%4/settings") + .arg(settings.m_reverseAPIAddress) + .arg(settings.m_reverseAPIPort) + .arg(settings.m_reverseAPIFeatureSetIndex) + .arg(settings.m_reverseAPIFeatureIndex); + m_networkRequest.setUrl(QUrl(channelSettingsURL)); + m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + QBuffer *buffer = new QBuffer(); + buffer->open((QBuffer::ReadWrite)); + buffer->write(swgFeatureSettings->asJson().toUtf8()); + buffer->seek(0); + + // Always use PATCH to avoid passing reverse API settings + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); + + delete swgFeatureSettings; +} + +void Radiosonde::networkManagerFinished(QNetworkReply *reply) +{ + QNetworkReply::NetworkError replyError = reply->error(); + + if (replyError) + { + qWarning() << "Radiosonde::networkManagerFinished:" + << " error(" << (int) replyError + << "): " << replyError + << ": " << reply->errorString(); + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("Radiosonde::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + } + + reply->deleteLater(); +} diff --git a/plugins/feature/radiosonde/radiosonde.h b/plugins/feature/radiosonde/radiosonde.h new file mode 100644 index 000000000..efbb344a6 --- /dev/null +++ b/plugins/feature/radiosonde/radiosonde.h @@ -0,0 +1,116 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_RADIOSONDE_H_ +#define INCLUDE_FEATURE_RADIOSONDE_H_ + +#include +#include +#include + +#include "feature/feature.h" +#include "util/message.h" + +#include "radiosondesettings.h" + +class WebAPIAdapterInterface; +class QNetworkAccessManager; +class QNetworkReply; + +namespace SWGSDRangel { + class SWGDeviceState; +} + +class Radiosonde : public Feature +{ + Q_OBJECT +public: + class MsgConfigureRadiosonde : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const RadiosondeSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureRadiosonde* create(const RadiosondeSettings& settings, bool force) { + return new MsgConfigureRadiosonde(settings, force); + } + + private: + RadiosondeSettings m_settings; + bool m_force; + + MsgConfigureRadiosonde(const RadiosondeSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + Radiosonde(WebAPIAdapterInterface *webAPIAdapterInterface); + virtual ~Radiosonde(); + virtual void destroy() { delete this; } + virtual bool handleMessage(const Message& cmd); + + virtual void getIdentifier(QString& id) const { id = objectName(); } + virtual void getTitle(QString& title) const { title = m_settings.m_title; } + + virtual QByteArray serialize() const; + virtual bool deserialize(const QByteArray& data); + + virtual int webapiSettingsGet( + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage); + + static void webapiFormatFeatureSettings( + SWGSDRangel::SWGFeatureSettings& response, + const RadiosondeSettings& settings); + + static void webapiUpdateFeatureSettings( + RadiosondeSettings& settings, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response); + + static const char* const m_featureIdURI; + static const char* const m_featureId; + +private: + RadiosondeSettings m_settings; + QList m_availablePipes; + QTimer m_updatePipesTimer; + + QNetworkAccessManager *m_networkManager; + QNetworkRequest m_networkRequest; + + void start(); + void stop(); + void applySettings(const RadiosondeSettings& settings, bool force = false); + void webapiReverseSendSettings(QList& featureSettingsKeys, const RadiosondeSettings& settings, bool force); + +private slots: + void updatePipes(); + void networkManagerFinished(QNetworkReply *reply); +}; + +#endif // INCLUDE_FEATURE_RADIOSONDE_H_ diff --git a/plugins/feature/radiosonde/radiosonde.qrc b/plugins/feature/radiosonde/radiosonde.qrc new file mode 100644 index 000000000..047083d71 --- /dev/null +++ b/plugins/feature/radiosonde/radiosonde.qrc @@ -0,0 +1,6 @@ + + + map/ballon.png + map/parachute.png + + diff --git a/plugins/feature/radiosonde/radiosondegui.cpp b/plugins/feature/radiosonde/radiosondegui.cpp new file mode 100644 index 000000000..1f37fbd91 --- /dev/null +++ b/plugins/feature/radiosonde/radiosondegui.cpp @@ -0,0 +1,857 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include +#include +#include + +#include "feature/featureuiset.h" +#include "feature/featurewebapiutils.h" +#include "gui/basicfeaturesettingsdialog.h" +#include "gui/datetimedelegate.h" +#include "gui/decimaldelegate.h" +#include "mainwindow.h" +#include "device/deviceuiset.h" + +#include "ui_radiosondegui.h" +#include "radiosonde.h" +#include "radiosondegui.h" + +#include "SWGMapItem.h" + +RadiosondeGUI* RadiosondeGUI::create(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature) +{ + RadiosondeGUI* gui = new RadiosondeGUI(pluginAPI, featureUISet, feature); + return gui; +} + +void RadiosondeGUI::destroy() +{ + delete this; +} + +void RadiosondeGUI::resetToDefaults() +{ + m_settings.resetToDefaults(); + displaySettings(); + applySettings(true); +} + +QByteArray RadiosondeGUI::serialize() const +{ + return m_settings.serialize(); +} + +bool RadiosondeGUI::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + displaySettings(); + applySettings(true); + return true; + } + else + { + resetToDefaults(); + return false; + } +} + +bool RadiosondeGUI::handleMessage(const Message& message) +{ + if (Radiosonde::MsgConfigureRadiosonde::match(message)) + { + qDebug("RadiosondeGUI::handleMessage: Radiosonde::MsgConfigureRadiosonde"); + const Radiosonde::MsgConfigureRadiosonde& cfg = (Radiosonde::MsgConfigureRadiosonde&) message; + m_settings = cfg.getSettings(); + blockApplySettings(true); + displaySettings(); + blockApplySettings(false); + + return true; + } + else if (MainCore::MsgPacket::match(message)) + { + MainCore::MsgPacket& report = (MainCore::MsgPacket&) message; + + // Decode the message + RS41Frame *frame = RS41Frame::decode(report.getPacket()); + // Update table + updateRadiosondes(frame, report.getDateTime()); + } + + return false; +} + +void RadiosondeGUI::handleInputMessages() +{ + Message* message; + + while ((message = getInputMessageQueue()->pop())) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +void RadiosondeGUI::onWidgetRolled(QWidget* widget, bool rollDown) +{ + (void) widget; + (void) rollDown; + + saveState(m_rollupState); + applySettings(); +} + +RadiosondeGUI::RadiosondeGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent) : + FeatureGUI(parent), + ui(new Ui::RadiosondeGUI), + m_pluginAPI(pluginAPI), + m_featureUISet(featureUISet), + m_doApplySettings(true), + m_lastFeatureState(0) +{ + ui->setupUi(this); + m_helpURL = "plugins/feature/radiosonde/readme.md"; + setAttribute(Qt::WA_DeleteOnClose, true); + setChannelWidget(false); + connect(this, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool))); + m_radiosonde = reinterpret_cast(feature); + m_radiosonde->setMessageQueueToGUI(&m_inputMessageQueue); + + m_featureUISet->addRollupWidget(this); + + connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &))); + connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + + // Intialise chart + ui->chart->setRenderHint(QPainter::Antialiasing); + + // Resize the table using dummy data + resizeTable(); + // Allow user to reorder columns + ui->radiosondes->horizontalHeader()->setSectionsMovable(true); + // Allow user to sort table by clicking on headers + ui->radiosondes->setSortingEnabled(true); + // Add context menu to allow hiding/showing of columns + radiosondesMenu = new QMenu(ui->radiosondes); + for (int i = 0; i < ui->radiosondes->horizontalHeader()->count(); i++) + { + QString text = ui->radiosondes->horizontalHeaderItem(i)->text(); + radiosondesMenu->addAction(createCheckableItem(text, i, true, SLOT(radiosondesColumnSelectMenuChecked()))); + } + ui->radiosondes->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->radiosondes->horizontalHeader(), SIGNAL(customContextMenuRequested(QPoint)), SLOT(radiosondesColumnSelectMenu(QPoint))); + // Get signals when columns change + connect(ui->radiosondes->horizontalHeader(), SIGNAL(sectionMoved(int, int, int)), SLOT(radiosondes_sectionMoved(int, int, int))); + connect(ui->radiosondes->horizontalHeader(), SIGNAL(sectionResized(int, int, int)), SLOT(radiosondes_sectionResized(int, int, int))); + // Context menu + ui->radiosondes->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->radiosondes, SIGNAL(customContextMenuRequested(QPoint)), SLOT(radiosondes_customContextMenuRequested(QPoint))); + + ui->radiosondes->setItemDelegateForColumn(RADIOSONDE_COL_LATITUDE, new DecimalDelegate(5)); + ui->radiosondes->setItemDelegateForColumn(RADIOSONDE_COL_LONGITUDE, new DecimalDelegate(5)); + ui->radiosondes->setItemDelegateForColumn(RADIOSONDE_COL_ALTITUDE, new DecimalDelegate(1)); + ui->radiosondes->setItemDelegateForColumn(RADIOSONDE_COL_SPEED, new DecimalDelegate(1)); + ui->radiosondes->setItemDelegateForColumn(RADIOSONDE_COL_VERTICAL_RATE, new DecimalDelegate(1)); + ui->radiosondes->setItemDelegateForColumn(RADIOSONDE_COL_HEADING, new DecimalDelegate(1)); + ui->radiosondes->setItemDelegateForColumn(RADIOSONDE_COL_ALT_MAX, new DecimalDelegate(1)); + ui->radiosondes->setItemDelegateForColumn(RADIOSONDE_COL_LAST_UPDATE, new DateTimeDelegate()); + + m_settings.setRollupState(&m_rollupState); + + displaySettings(); + applySettings(true); + + plotChart(); +} + +RadiosondeGUI::~RadiosondeGUI() +{ + qDeleteAll(m_radiosondes); + delete ui; +} + +void RadiosondeGUI::blockApplySettings(bool block) +{ + m_doApplySettings = !block; +} + +void RadiosondeGUI::displaySettings() +{ + setTitleColor(m_settings.m_rgbColor); + setWindowTitle(m_settings.m_title); + blockApplySettings(true); + + // Order and size columns + QHeaderView *header = ui->radiosondes->horizontalHeader(); + for (int i = 0; i < RADIOSONDES_COLUMNS; i++) + { + bool hidden = m_settings.m_radiosondesColumnSizes[i] == 0; + header->setSectionHidden(i, hidden); + radiosondesMenu->actions().at(i)->setChecked(!hidden); + if (m_settings.m_radiosondesColumnSizes[i] > 0) { + ui->radiosondes->setColumnWidth(i, m_settings.m_radiosondesColumnSizes[i]); + } + header->moveSection(header->visualIndex(i), m_settings.m_radiosondesColumnIndexes[i]); + } + + ui->y1->setCurrentIndex((int)m_settings.m_y1); + ui->y2->setCurrentIndex((int)m_settings.m_y2); + + restoreState(m_rollupState); + blockApplySettings(false); + arrangeRollups(); +} + +void RadiosondeGUI::leaveEvent(QEvent*) +{ +} + +void RadiosondeGUI::enterEvent(QEvent*) +{ +} + +void RadiosondeGUI::onMenuDialogCalled(const QPoint &p) +{ + if (m_contextMenuType == ContextMenuChannelSettings) + { + BasicFeatureSettingsDialog dialog(this); + dialog.setTitle(m_settings.m_title); + dialog.setColor(m_settings.m_rgbColor); + dialog.setUseReverseAPI(m_settings.m_useReverseAPI); + dialog.setReverseAPIAddress(m_settings.m_reverseAPIAddress); + dialog.setReverseAPIPort(m_settings.m_reverseAPIPort); + dialog.setReverseAPIFeatureSetIndex(m_settings.m_reverseAPIFeatureSetIndex); + dialog.setReverseAPIFeatureIndex(m_settings.m_reverseAPIFeatureIndex); + + dialog.move(p); + dialog.exec(); + + m_settings.m_rgbColor = dialog.getColor().rgb(); + m_settings.m_title = dialog.getTitle(); + m_settings.m_useReverseAPI = dialog.useReverseAPI(); + m_settings.m_reverseAPIAddress = dialog.getReverseAPIAddress(); + m_settings.m_reverseAPIPort = dialog.getReverseAPIPort(); + m_settings.m_reverseAPIFeatureSetIndex = dialog.getReverseAPIFeatureSetIndex(); + m_settings.m_reverseAPIFeatureIndex = dialog.getReverseAPIFeatureIndex(); + + setWindowTitle(m_settings.m_title); + setTitleColor(m_settings.m_rgbColor); + + applySettings(); + } + + resetContextMenuType(); +} + +void RadiosondeGUI::applySettings(bool force) +{ + if (m_doApplySettings) + { + Radiosonde::MsgConfigureRadiosonde* message = Radiosonde::MsgConfigureRadiosonde::create(m_settings, force); + m_radiosonde->getInputMessageQueue()->push(message); + } +} + +void RadiosondeGUI::resizeTable() +{ + // Fill table with a row of dummy data that will size the columns nicely + int row = ui->radiosondes->rowCount(); + ui->radiosondes->setRowCount(row + 1); + ui->radiosondes->setItem(row, RADIOSONDE_COL_SERIAL, new QTableWidgetItem("123456789")); + ui->radiosondes->setItem(row, RADIOSONDE_COL_TYPE, new QTableWidgetItem("RS41-SGP")); + ui->radiosondes->setItem(row, RADIOSONDE_COL_LATITUDE, new QTableWidgetItem("90.000000-")); + ui->radiosondes->setItem(row, RADIOSONDE_COL_LONGITUDE, new QTableWidgetItem("180.00000-")); + ui->radiosondes->setItem(row, RADIOSONDE_COL_ALTITUDE, new QTableWidgetItem("10000")); + ui->radiosondes->setItem(row, RADIOSONDE_COL_SPEED, new QTableWidgetItem("120")); + ui->radiosondes->setItem(row, RADIOSONDE_COL_VERTICAL_RATE, new QTableWidgetItem("120")); + ui->radiosondes->setItem(row, RADIOSONDE_COL_HEADING, new QTableWidgetItem("360")); + ui->radiosondes->setItem(row, RADIOSONDE_COL_STATUS, new QTableWidgetItem("Ascent")); + ui->radiosondes->setItem(row, RADIOSONDE_COL_PRESSURE, new QTableWidgetItem("1234")); + ui->radiosondes->setItem(row, RADIOSONDE_COL_TEMPERATURE, new QTableWidgetItem("-50.0")); + ui->radiosondes->setItem(row, RADIOSONDE_COL_HUMIDITY, new QTableWidgetItem("100.0")); + ui->radiosondes->setItem(row, RADIOSONDE_COL_ALT_MAX, new QTableWidgetItem("10000")); + ui->radiosondes->setItem(row, RADIOSONDE_COL_FREQUENCY, new QTableWidgetItem("400.000")); + ui->radiosondes->setItem(row, RADIOSONDE_COL_BURSTKILL_STATUS, new QTableWidgetItem("0")); + ui->radiosondes->setItem(row, RADIOSONDE_COL_BURSTKILL_TIMER, new QTableWidgetItem("00:00")); + ui->radiosondes->setItem(row, RADIOSONDE_COL_LAST_UPDATE, new QTableWidgetItem("2022/12/12 12:00:00")); + ui->radiosondes->setItem(row, RADIOSONDE_COL_MESSAGES, new QTableWidgetItem("1000")); + ui->radiosondes->resizeColumnsToContents(); + ui->radiosondes->removeRow(row); +} + +// Columns in table reordered +void RadiosondeGUI::radiosondes_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex) +{ + (void) oldVisualIndex; + + m_settings.m_radiosondesColumnIndexes[logicalIndex] = newVisualIndex; +} + +// Column in table resized (when hidden size is 0) +void RadiosondeGUI::radiosondes_sectionResized(int logicalIndex, int oldSize, int newSize) +{ + (void) oldSize; + + m_settings.m_radiosondesColumnSizes[logicalIndex] = newSize; +} + +// Right click in table header - show column select menu +void RadiosondeGUI::radiosondesColumnSelectMenu(QPoint pos) +{ + radiosondesMenu->popup(ui->radiosondes->horizontalHeader()->viewport()->mapToGlobal(pos)); +} + +// Hide/show column when menu selected +void RadiosondeGUI::radiosondesColumnSelectMenuChecked(bool checked) +{ + (void) checked; + + QAction* action = qobject_cast(sender()); + if (action != nullptr) + { + int idx = action->data().toInt(nullptr); + ui->radiosondes->setColumnHidden(idx, !action->isChecked()); + } +} + +// Create column select menu item +QAction *RadiosondeGUI::createCheckableItem(QString &text, int idx, bool checked, const char *slot) +{ + QAction *action = new QAction(text, this); + action->setCheckable(true); + action->setChecked(checked); + action->setData(QVariant(idx)); + connect(action, SIGNAL(triggered()), this, slot); + return action; +} + +// Send to Map feature +void RadiosondeGUI::sendToMap(const QString &name, const QString &label, + const QString &image, const QString &text, + const QString &model, float labelOffset, + float latitude, float longitude, float altitude, QDateTime positionDateTime, + float heading + ) +{ + MessagePipesLegacy& messagePipes = MainCore::instance()->getMessagePipes(); + QList *mapMessageQueues = messagePipes.getMessageQueues(m_radiosonde, "mapitems"); + if (mapMessageQueues) + { + QList::iterator it = mapMessageQueues->begin(); + + for (; it != mapMessageQueues->end(); ++it) + { + SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem(); + swgMapItem->setName(new QString(name)); + swgMapItem->setLatitude(latitude); + swgMapItem->setLongitude(longitude); + swgMapItem->setAltitude(altitude); + swgMapItem->setAltitudeReference(0); // ABSOLUTE + + if (positionDateTime.isValid()) { + swgMapItem->setPositionDateTime(new QString(positionDateTime.toString(Qt::ISODateWithMs))); + } + + swgMapItem->setImageRotation(heading); + swgMapItem->setText(new QString(text)); + + if (image.isEmpty()) { + swgMapItem->setImage(new QString("")); + } else { + swgMapItem->setImage(new QString(QString("qrc:///radiosonde/map/%1").arg(image))); + } + swgMapItem->setModel(new QString(model)); + swgMapItem->setModelAltitudeOffset(0.0f); + swgMapItem->setLabel(new QString(label)); + swgMapItem->setLabelAltitudeOffset(labelOffset); + swgMapItem->setFixedPosition(false); + swgMapItem->setOrientation(1); + swgMapItem->setHeading(heading); + swgMapItem->setPitch(0.0); + swgMapItem->setRoll(0.0); + + MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_radiosonde, swgMapItem); + (*it)->push(msg); + } + } +} + +// Update table with received message +void RadiosondeGUI::updateRadiosondes(RS41Frame *message, QDateTime dateTime) +{ + QTableWidgetItem *serialItem; + QTableWidgetItem *typeItem; + QTableWidgetItem *latitudeItem; + QTableWidgetItem *longitudeItem; + QTableWidgetItem *alitutudeItem; + QTableWidgetItem *speedItem; + QTableWidgetItem *verticalRateItem; + QTableWidgetItem *headingItem; + QTableWidgetItem *statusItem; + QTableWidgetItem *pressureItem; + QTableWidgetItem *temperatureItem; + QTableWidgetItem *humidityItem; + QTableWidgetItem *altMaxItem; + QTableWidgetItem *frequencyItem; + QTableWidgetItem *burstKillStatusItem; + QTableWidgetItem *burstKillTimerItem; + QTableWidgetItem *lastUpdateItem; + QTableWidgetItem *messagesItem; + + if (!message->m_statusValid) + { + // Nothing to display if no serial number + return; + } + + RadiosondeData *radiosonde; + + // See if radiosonde is already in table + QString messageSerial = message->m_serial; + bool found = false; + for (int row = 0; row < ui->radiosondes->rowCount(); row++) + { + QString itemSerial = ui->radiosondes->item(row, RADIOSONDE_COL_SERIAL)->text(); + if (messageSerial == itemSerial) + { + // Update existing item + serialItem = ui->radiosondes->item(row, RADIOSONDE_COL_SERIAL); + typeItem = ui->radiosondes->item(row, RADIOSONDE_COL_TYPE); + latitudeItem = ui->radiosondes->item(row, RADIOSONDE_COL_LATITUDE); + longitudeItem = ui->radiosondes->item(row, RADIOSONDE_COL_LONGITUDE); + alitutudeItem = ui->radiosondes->item(row, RADIOSONDE_COL_ALTITUDE); + speedItem = ui->radiosondes->item(row, RADIOSONDE_COL_SPEED); + verticalRateItem = ui->radiosondes->item(row, RADIOSONDE_COL_VERTICAL_RATE); + headingItem = ui->radiosondes->item(row, RADIOSONDE_COL_HEADING); + statusItem = ui->radiosondes->item(row, RADIOSONDE_COL_STATUS); + pressureItem = ui->radiosondes->item(row, RADIOSONDE_COL_PRESSURE); + temperatureItem = ui->radiosondes->item(row, RADIOSONDE_COL_TEMPERATURE); + humidityItem = ui->radiosondes->item(row, RADIOSONDE_COL_HUMIDITY); + altMaxItem = ui->radiosondes->item(row, RADIOSONDE_COL_ALT_MAX); + frequencyItem = ui->radiosondes->item(row, RADIOSONDE_COL_FREQUENCY); + burstKillStatusItem = ui->radiosondes->item(row, RADIOSONDE_COL_BURSTKILL_STATUS); + burstKillTimerItem = ui->radiosondes->item(row, RADIOSONDE_COL_BURSTKILL_TIMER); + lastUpdateItem = ui->radiosondes->item(row, RADIOSONDE_COL_LAST_UPDATE); + messagesItem = ui->radiosondes->item(row, RADIOSONDE_COL_MESSAGES); + radiosonde = m_radiosondes.value(messageSerial); + found = true; + break; + } + } + if (!found) + { + // Add new radiosonde + ui->radiosondes->setSortingEnabled(false); + int row = ui->radiosondes->rowCount(); + ui->radiosondes->setRowCount(row + 1); + + serialItem = new QTableWidgetItem(); + typeItem = new QTableWidgetItem(); + latitudeItem = new QTableWidgetItem(); + longitudeItem = new QTableWidgetItem(); + alitutudeItem = new QTableWidgetItem(); + speedItem = new QTableWidgetItem(); + verticalRateItem = new QTableWidgetItem(); + headingItem = new QTableWidgetItem(); + statusItem = new QTableWidgetItem(); + temperatureItem = new QTableWidgetItem(); + humidityItem = new QTableWidgetItem(); + pressureItem = new QTableWidgetItem(); + altMaxItem = new QTableWidgetItem(); + frequencyItem = new QTableWidgetItem(); + burstKillStatusItem = new QTableWidgetItem(); + burstKillTimerItem = new QTableWidgetItem(); + lastUpdateItem = new QTableWidgetItem(); + messagesItem = new QTableWidgetItem(); + ui->radiosondes->setItem(row, RADIOSONDE_COL_SERIAL, serialItem); + ui->radiosondes->setItem(row, RADIOSONDE_COL_TYPE, typeItem); + ui->radiosondes->setItem(row, RADIOSONDE_COL_LATITUDE, latitudeItem); + ui->radiosondes->setItem(row, RADIOSONDE_COL_LONGITUDE, longitudeItem); + ui->radiosondes->setItem(row, RADIOSONDE_COL_ALTITUDE, alitutudeItem); + ui->radiosondes->setItem(row, RADIOSONDE_COL_SPEED, speedItem); + ui->radiosondes->setItem(row, RADIOSONDE_COL_VERTICAL_RATE, verticalRateItem); + ui->radiosondes->setItem(row, RADIOSONDE_COL_HEADING, headingItem); + ui->radiosondes->setItem(row, RADIOSONDE_COL_STATUS, statusItem); + ui->radiosondes->setItem(row, RADIOSONDE_COL_PRESSURE, pressureItem); + ui->radiosondes->setItem(row, RADIOSONDE_COL_TEMPERATURE, temperatureItem); + ui->radiosondes->setItem(row, RADIOSONDE_COL_HUMIDITY, humidityItem); + ui->radiosondes->setItem(row, RADIOSONDE_COL_ALT_MAX, altMaxItem); + ui->radiosondes->setItem(row, RADIOSONDE_COL_FREQUENCY, frequencyItem); + ui->radiosondes->setItem(row, RADIOSONDE_COL_BURSTKILL_STATUS, burstKillStatusItem); + ui->radiosondes->setItem(row, RADIOSONDE_COL_BURSTKILL_TIMER, burstKillTimerItem); + ui->radiosondes->setItem(row, RADIOSONDE_COL_LAST_UPDATE, lastUpdateItem); + ui->radiosondes->setItem(row, RADIOSONDE_COL_MESSAGES, messagesItem); + messagesItem->setData(Qt::DisplayRole, 0); + + radiosonde = new RadiosondeData(); + m_radiosondes.insert(messageSerial, radiosonde); + } + + serialItem->setText(message->m_serial); + lastUpdateItem->setData(Qt::DisplayRole, dateTime); + messagesItem->setData(Qt::DisplayRole, messagesItem->data(Qt::DisplayRole).toInt() + 1); + + if (message->m_posValid) + { + latitudeItem->setData(Qt::DisplayRole, message->m_latitude); + longitudeItem->setData(Qt::DisplayRole, message->m_longitude); + alitutudeItem->setData(Qt::DisplayRole, message->m_height); + float altMax = altMaxItem->data(Qt::DisplayRole).toFloat(); + if (message->m_height > altMax) { + altMaxItem->setData(Qt::DisplayRole, message->m_height); + } + speedItem->setData(Qt::DisplayRole, Units::kmpsToKPH(message->m_speed/1000.0)); + verticalRateItem->setData(Qt::DisplayRole, message->m_verticalRate); + headingItem->setData(Qt::DisplayRole, message->m_heading); + } + statusItem->setText(message->m_flightPhase); + + radiosonde->m_subframe.update(message); + + if (message->m_measValid) + { + pressureItem->setData(Qt::DisplayRole, message->getPressureString(&radiosonde->m_subframe)); + temperatureItem->setData(Qt::DisplayRole, message->getTemperatureString(&radiosonde->m_subframe)); + humidityItem->setData(Qt::DisplayRole, message->getHumidityString(&radiosonde->m_subframe)); + } + + if (message->m_measValid && message->m_posValid) { + radiosonde->addMessage(dateTime, message); + } + + typeItem->setText(radiosonde->m_subframe.getType()); + frequencyItem->setText(radiosonde->m_subframe.getFrequencyMHz()); + burstKillStatusItem->setText(radiosonde->m_subframe.getBurstKillStatus()); + burstKillTimerItem->setText(radiosonde->m_subframe.getBurstKillTimer()); + + ui->radiosondes->setSortingEnabled(true); + + if (message->m_posValid) + { + // Text to display in info box + QStringList text; + QString type = typeItem->text(); + QVariant altitudeV = alitutudeItem->data(Qt::DisplayRole); + QVariant speedV = speedItem->data(Qt::DisplayRole); + QVariant verticalRateV = verticalRateItem->data(Qt::DisplayRole); + QVariant headingV = headingItem->data(Qt::DisplayRole); + QString pressure = pressureItem->text(); + QString temperature = temperatureItem->text(); + QString humidity = humidityItem->text(); + QString status = statusItem->text(); + QString frequency = frequencyItem->text(); + text.append(QString("Serial: %1").arg(serialItem->text())); + if (!type.isEmpty()) { + text.append(QString("Type: %1").arg(type)); + } + if (!altitudeV.isNull()) { + text.append(QString("Altitude: %1m").arg(altitudeV.toDouble(), 0, 'f', 1)); + } + if (!speedV.isNull()) { + text.append(QString("Speed: %1km/h").arg(speedV.toDouble(), 0, 'f', 1)); + } + if (!verticalRateV.isNull()) { + text.append(QString("Vertical rate: %1m/s").arg(verticalRateV.toDouble(), 0, 'f', 1)); + } + if (!headingV.isNull()) { + text.append(QString("Heading: %1%2").arg(headingV.toDouble(), 0, 'f', 1).arg(QChar(0xb0))); + } + if (!status.isEmpty()) { + text.append(QString("Status: %1").arg(status)); + } + if (!pressure.isEmpty()) { + text.append(QString("Pressure: %1hPa").arg(pressure)); + } + if (!temperature.isEmpty()) { + text.append(QString("Temperature: %1C").arg(temperature)); + } + if (!humidity.isEmpty()) { + text.append(QString("Humidity: %1%").arg(humidity)); + } + if (!frequency.isEmpty()) { + text.append(QString("Frequency: %1MHz").arg(frequency)); + } + + QString image = message->m_flightPhase == "Descent" ? "parachute.png" : "ballon.png"; + QString model = message->m_flightPhase == "Descent" ? "radiosondeparachute.glb" : "radiosondeballon.glb"; + // Send to map feature + sendToMap(serialItem->text(), serialItem->text(), + image, text.join("
"), + model, 0.0, + message->m_latitude, message->m_longitude, message->m_height, dateTime, + 0.0f); + } + + // If this is the first row in the table, select it, so that the chart is plotted + QList selectedItems = ui->radiosondes->selectedItems(); + if (selectedItems.size() == 0) + { + QTableWidgetSelectionRange r(0, 0, 0, RADIOSONDES_COLUMNS); + ui->radiosondes->setRangeSelected(r, true); + } + + plotChart(); +} + +void RadiosondeGUI::on_radiosondes_itemSelectionChanged() +{ + plotChart(); +} + +void RadiosondeGUI::on_radiosondes_cellDoubleClicked(int row, int column) +{ + if (column == RADIOSONDE_COL_SERIAL) + { + // Get serial of Radiosonde in row double clicked + QString serial = ui->radiosondes->item(row, RADIOSONDE_COL_SERIAL)->text(); + // Search for MMSI on www.radiosondefinder.com + QDesktopServices::openUrl(QUrl(QString("https://sondehub.org/?f=%1#!mt=Mapnik&f=%1&q=%1").arg(serial))); + } + else if ((column == RADIOSONDE_COL_LATITUDE) || (column == RADIOSONDE_COL_LONGITUDE)) + { + // Get serial of Radiosonde in row double clicked + QString serial = ui->radiosondes->item(row, RADIOSONDE_COL_SERIAL)->text(); + // Find serial on Map + FeatureWebAPIUtils::mapFind(serial); + } +} + +// Table cells context menu +void RadiosondeGUI::radiosondes_customContextMenuRequested(QPoint pos) +{ + QTableWidgetItem *item = ui->radiosondes->itemAt(pos); + if (item) + { + int row = item->row(); + QString serial = ui->radiosondes->item(row, RADIOSONDE_COL_SERIAL)->text(); + QVariant latitudeV = ui->radiosondes->item(row, RADIOSONDE_COL_LATITUDE)->data(Qt::DisplayRole); + QVariant longitudeV = ui->radiosondes->item(row, RADIOSONDE_COL_LONGITUDE)->data(Qt::DisplayRole); + + QMenu* tableContextMenu = new QMenu(ui->radiosondes); + connect(tableContextMenu, &QMenu::aboutToHide, tableContextMenu, &QMenu::deleteLater); + + // Copy current cell + 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); + tableContextMenu->addSeparator(); + + // View radiosonde on various websites + QAction* mmsiRadiosondeHubAction = new QAction(QString("View %1 on sondehub.net...").arg(serial), tableContextMenu); + connect(mmsiRadiosondeHubAction, &QAction::triggered, this, [serial]()->void { + QDesktopServices::openUrl(QUrl(QString("https://sondehub.org/?f=%1#!mt=Mapnik&f=%1&q=%1").arg(serial))); + }); + tableContextMenu->addAction(mmsiRadiosondeHubAction); + + // Find on Map + tableContextMenu->addSeparator(); + QAction* findMapFeatureAction = new QAction(QString("Find %1 on map").arg(serial), tableContextMenu); + connect(findMapFeatureAction, &QAction::triggered, this, [serial]()->void { + FeatureWebAPIUtils::mapFind(serial); + }); + tableContextMenu->addAction(findMapFeatureAction); + + tableContextMenu->popup(ui->radiosondes->viewport()->mapToGlobal(pos)); + } +} + +void RadiosondeGUI::on_y1_currentIndexChanged(int index) +{ + m_settings.m_y1 = (RadiosondeSettings::ChartData)index; + applySettings(); + plotChart(); +} + +void RadiosondeGUI::on_y2_currentIndexChanged(int index) +{ + m_settings.m_y2 = (RadiosondeSettings::ChartData)index; + applySettings(); + plotChart(); +} + +float RadiosondeGUI::getData(RadiosondeSettings::ChartData dataType, RadiosondeData *radiosonde, RS41Frame *message) +{ + float data; + switch (dataType) + { + case RadiosondeSettings::ALTITUDE: + data = message->m_height; + break; + case RadiosondeSettings::TEMPERATURE: + data = message->getTemperatureFloat(&radiosonde->m_subframe); + break; + case RadiosondeSettings::HUMIDITY: + data = message->getHumidityFloat(&radiosonde->m_subframe); + break; + case RadiosondeSettings::PRESSURE: + data = 0.0f; + break; + case RadiosondeSettings::SPEED: + data = Units::kmpsToKPH(message->m_speed/1000.0); + break; + case RadiosondeSettings::VERTICAL_RATE: + data = message->m_verticalRate; + break; + case RadiosondeSettings::HEADING: + data = message->m_speed; + break; + case RadiosondeSettings::BATTERY_VOLTAGE: + data = message->m_batteryVoltage; + break; + default: + data = 0.0f; + break; + } + return data; +} + +static QString getAxisTitle(RadiosondeSettings::ChartData dataType) +{ + switch (dataType) + { + case RadiosondeSettings::ALTITUDE: + return "Altitude (m)"; + break; + case RadiosondeSettings::TEMPERATURE: + return QString("Temperature (%1C)").arg(QChar(0xb0)); + break; + case RadiosondeSettings::HUMIDITY: + return "Relative humidty (%)"; + break; + case RadiosondeSettings::PRESSURE: + return "Pressure (hPa)"; + break; + case RadiosondeSettings::SPEED: + return "Speed (km/h)"; + break; + case RadiosondeSettings::VERTICAL_RATE: + return "Vertical rate (m/s)"; + break; + case RadiosondeSettings::HEADING: + return QString("Heading (%1)").arg(QChar(0xb0)); + break; + case RadiosondeSettings::BATTERY_VOLTAGE: + return "Battery Voltage (V)"; + break; + default: + return ""; + } +} + +void RadiosondeGUI::plotChart() +{ + QChart *oldChart = ui->chart->chart(); + QChart *m_chart; + + m_chart = new QChart(); + m_chart->layout()->setContentsMargins(0, 0, 0, 0); + m_chart->setMargins(QMargins(1, 1, 1, 1)); + m_chart->setTheme(QChart::ChartThemeDark); + + // Get selected radiosonde + QList selectedItems = ui->radiosondes->selectedItems(); + if (selectedItems.size() >= 1) + { + int row = selectedItems[0]->row(); + QString serial = ui->radiosondes->item(row, RADIOSONDE_COL_SERIAL)->text(); + RadiosondeData *radiosonde = m_radiosondes.value(serial); + if (radiosonde) + { + // Plot selected data + QDateTimeAxis *m_chartXAxis; + QValueAxis *m_chartY1Axis; + QValueAxis *m_chartY2Axis; + + m_chartXAxis = new QDateTimeAxis(); + if (m_settings.m_y1 != RadiosondeSettings::NONE) { + m_chartY1Axis = new QValueAxis(); + } + if (m_settings.m_y2 != RadiosondeSettings::NONE) { + m_chartY2Axis = new QValueAxis(); + } + + m_chart->legend()->hide(); + m_chart->addAxis(m_chartXAxis, Qt::AlignBottom); + + QLineSeries *y1Series = new QLineSeries(); + QLineSeries *y2Series = new QLineSeries(); + + int idx = 0; + for (auto message : radiosonde->m_messages) + { + float y1, y2; + if (m_settings.m_y1 != RadiosondeSettings::NONE) + { + y1 = getData(m_settings.m_y1, radiosonde, message); + y1Series->append(radiosonde->m_messagesDateTime[idx].toMSecsSinceEpoch(), y1); + } + if (m_settings.m_y2 != RadiosondeSettings::NONE) + { + y2 = getData(m_settings.m_y2, radiosonde, message); + y2Series->append(radiosonde->m_messagesDateTime[idx].toMSecsSinceEpoch(), y2); + } + idx++; + } + if (m_settings.m_y1 != RadiosondeSettings::NONE) + { + m_chart->addSeries(y1Series); + m_chart->addAxis(m_chartY1Axis, Qt::AlignLeft); + y1Series->attachAxis(m_chartXAxis); + y1Series->attachAxis(m_chartY1Axis); + m_chartY1Axis->setTitleText(getAxisTitle(m_settings.m_y1)); + } + if (m_settings.m_y2 != RadiosondeSettings::NONE) + { + m_chart->addSeries(y2Series); + m_chart->addAxis(m_chartY2Axis, Qt::AlignRight); + y2Series->attachAxis(m_chartXAxis); + y2Series->attachAxis(m_chartY2Axis); + m_chartY2Axis->setTitleText(getAxisTitle(m_settings.m_y2)); + } + } + } + ui->chart->setChart(m_chart); + delete oldChart; +} + +void RadiosondeGUI::on_deleteAll_clicked() +{ + for (int row = ui->radiosondes->rowCount() - 1; row >= 0; row--) + { + QString serial = ui->radiosondes->item(row, RADIOSONDE_COL_SERIAL)->text(); + // Remove from map + sendToMap(serial, "", + "", "", + "", 0.0f, + 0.0f, 0.0f, 0.0f, QDateTime(), + 0.0f); + // Remove from table + ui->radiosondes->removeRow(row); + // Remove from hash + m_radiosondes.remove(serial); + } +} diff --git a/plugins/feature/radiosonde/radiosondegui.h b/plugins/feature/radiosonde/radiosondegui.h new file mode 100644 index 000000000..ace1b973b --- /dev/null +++ b/plugins/feature/radiosonde/radiosondegui.h @@ -0,0 +1,155 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_RADIOSONDEGUI_H_ +#define INCLUDE_FEATURE_RADIOSONDEGUI_H_ + +#include +#include +#include +#include +#include + +#include "feature/featuregui.h" +#include "util/messagequeue.h" +#include "util/radiosonde.h" +#include "pipes/pipeendpoint.h" +#include "settings/rollupstate.h" + +#include "radiosondesettings.h" + +class PluginAPI; +class FeatureUISet; +class Radiosonde; + +namespace Ui { + class RadiosondeGUI; +} + +using namespace QtCharts; + +class RadiosondeGUI : public FeatureGUI { + Q_OBJECT + + // Holds information not in the table + struct RadiosondeData { + + QList m_messagesDateTime; + QList m_messages; + + RS41Subframe m_subframe; + + ~RadiosondeData() + { + qDeleteAll(m_messages); + } + + void addMessage(QDateTime dateTime, RS41Frame *message) + { + m_messagesDateTime.append(dateTime); + m_messages.append(message); + } + + }; + +public: + static RadiosondeGUI* create(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature); + virtual void destroy(); + + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + virtual MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } + +private: + Ui::RadiosondeGUI* ui; + PluginAPI* m_pluginAPI; + FeatureUISet* m_featureUISet; + RadiosondeSettings m_settings; + RollupState m_rollupState; + bool m_doApplySettings; + + Radiosonde* m_radiosonde; + MessageQueue m_inputMessageQueue; + int m_lastFeatureState; + + QHash m_radiosondes; // Hash of serial to radiosondes + + QMenu *radiosondesMenu; // Column select context menu + + explicit RadiosondeGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent = nullptr); + virtual ~RadiosondeGUI(); + + void blockApplySettings(bool block); + void applySettings(bool force = false); + void displaySettings(); + bool handleMessage(const Message& message); + + void leaveEvent(QEvent*); + void enterEvent(QEvent*); + + void sendToMap(const QString &name, const QString &label, + const QString &image, const QString &text, + const QString &model, float labelOffset, + float latitude, float longitude, float altitude, QDateTime positionDateTime, + float heading + ); + void updateRadiosondes(RS41Frame *radiosonde, QDateTime dateTime); + void resizeTable(); + QAction *createCheckableItem(QString& text, int idx, bool checked, const char *slot); + void plotChart(); + float getData(RadiosondeSettings::ChartData dataType, RadiosondeData *radiosonde, RS41Frame *message); + + enum RadiosondeCol { + RADIOSONDE_COL_SERIAL, + RADIOSONDE_COL_TYPE, + RADIOSONDE_COL_LATITUDE, + RADIOSONDE_COL_LONGITUDE, + RADIOSONDE_COL_ALTITUDE, + RADIOSONDE_COL_SPEED, + RADIOSONDE_COL_VERTICAL_RATE, + RADIOSONDE_COL_HEADING, + RADIOSONDE_COL_STATUS, + RADIOSONDE_COL_PRESSURE, + RADIOSONDE_COL_TEMPERATURE, + RADIOSONDE_COL_HUMIDITY, + RADIOSONDE_COL_ALT_MAX, + RADIOSONDE_COL_FREQUENCY, + RADIOSONDE_COL_BURSTKILL_STATUS, + RADIOSONDE_COL_BURSTKILL_TIMER, + RADIOSONDE_COL_LAST_UPDATE, + RADIOSONDE_COL_MESSAGES + }; + +private slots: + void onMenuDialogCalled(const QPoint &p); + void onWidgetRolled(QWidget* widget, bool rollDown); + void handleInputMessages(); + void on_radiosondes_itemSelectionChanged(); + void on_radiosondes_cellDoubleClicked(int row, int column); + void radiosondes_customContextMenuRequested(QPoint pos); + void radiosondes_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex); + void radiosondes_sectionResized(int logicalIndex, int oldSize, int newSize); + void radiosondesColumnSelectMenu(QPoint pos); + void radiosondesColumnSelectMenuChecked(bool checked = false); + void on_y1_currentIndexChanged(int index); + void on_y2_currentIndexChanged(int index); + void on_deleteAll_clicked(); +}; + +#endif // INCLUDE_FEATURE_RADIOSONDEGUI_H_ diff --git a/plugins/feature/radiosonde/radiosondegui.ui b/plugins/feature/radiosonde/radiosondegui.ui new file mode 100644 index 000000000..14a31362f --- /dev/null +++ b/plugins/feature/radiosonde/radiosondegui.ui @@ -0,0 +1,444 @@ + + + RadiosondeGUI + + + + 0 + 0 + 484 + 732 + + + + + 0 + 0 + + + + + 320 + 100 + + + + + 16777215 + 16777215 + + + + + 9 + + + + Radiosonde + + + Qt::LeftToRight + + + + + 10 + 10 + 336 + 508 + + + + + 0 + 0 + + + + Radiosondes + + + + 3 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + + 0 + 0 + + + + Qt::Vertical + + + + Radiosondes + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + + Serial + + + Serial number of the Radiosonde + + + + + Type + + + Type of Radiosonde + + + + + Lat (°) + + + Latitude in degrees. East positive + + + + + Lon (°) + + + Longitude in degrees. North positive. + + + + + Alt (m) + + + Altitude in metres + + + + + Spd (km/h) + + + Speed in kilometers per second + + + + + VR (m/s) + + + Vertical climb rate in metres per second + + + + + Hd (°) + + + Heading in degrees. + + + + + Status + + + + + P (hPa) + + + Pressure in hectpascals + + + + + T (C) + + + Temperature in degrees Celsius + + + + + U (%) + + + Relative humidity in percent + + + + + Alt Max (m) + + + Maximum altitude seen in metres + + + + + Freq (MHz) + + + Transmission frequency in MHz + + + + + BurstKill Status + + + + + BurstKill Timer + + + Time last position was received + + + + + Updated + + + Time last message was received + + + + + Messages + + + Number of messages received + + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Y1 + + + + + + + + 0 + 0 + + + + Select data to plot on left Y axis + + + 0 + + + + None + + + + + Altitude + + + + + Temperature + + + + + Humidity + + + + + Pressure + + + + + Speed + + + + + Vertical rate + + + + + Heading + + + + + Battery Voltage + + + + + + + + Y2 + + + + + + + + 0 + 0 + + + + Select data to plot on right Y axis + + + + None + + + + + Altitude + + + + + Temperature + + + + + Humidity + + + + + Pressure + + + + + Speed + + + + + Vertical rate + + + + + Heading + + + + + Battery Voltage + + + + + + + + Delete all data + + + + + + + :/bin.png:/bin.png + + + + + + + + + + 300 + 100 + + + + + + + + + + + + + + RollupWidget + QWidget +
gui/rollupwidget.h
+ 1 +
+ + QChartView + QGraphicsView +
QtCharts
+
+
+ + + + +
diff --git a/plugins/feature/radiosonde/radiosondeplugin.cpp b/plugins/feature/radiosonde/radiosondeplugin.cpp new file mode 100644 index 000000000..2445e2635 --- /dev/null +++ b/plugins/feature/radiosonde/radiosondeplugin.cpp @@ -0,0 +1,79 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include "plugin/pluginapi.h" + +#ifndef SERVER_MODE +#include "radiosondegui.h" +#endif +#include "radiosonde.h" +#include "radiosondeplugin.h" +#include "radiosondewebapiadapter.h" + +const PluginDescriptor RadiosondePlugin::m_pluginDescriptor = { + Radiosonde::m_featureId, + QStringLiteral("Radiosonde"), + QStringLiteral("6.20.0"), + QStringLiteral("(c) Jon Beniston, M7RCE"), + QStringLiteral("https://github.com/f4exb/sdrangel"), + true, + QStringLiteral("https://github.com/f4exb/sdrangel") +}; + +RadiosondePlugin::RadiosondePlugin(QObject* parent) : + QObject(parent), + m_pluginAPI(nullptr) +{ +} + +const PluginDescriptor& RadiosondePlugin::getPluginDescriptor() const +{ + return m_pluginDescriptor; +} + +void RadiosondePlugin::initPlugin(PluginAPI* pluginAPI) +{ + m_pluginAPI = pluginAPI; + + m_pluginAPI->registerFeature(Radiosonde::m_featureIdURI, Radiosonde::m_featureId, this); +} + +#ifdef SERVER_MODE +FeatureGUI* RadiosondePlugin::createFeatureGUI(FeatureUISet *featureUISet, Feature *feature) const +{ + (void) featureUISet; + (void) feature; + return nullptr; +} +#else +FeatureGUI* RadiosondePlugin::createFeatureGUI(FeatureUISet *featureUISet, Feature *feature) const +{ + return RadiosondeGUI::create(m_pluginAPI, featureUISet, feature); +} +#endif + +Feature* RadiosondePlugin::createFeature(WebAPIAdapterInterface* webAPIAdapterInterface) const +{ + return new Radiosonde(webAPIAdapterInterface); +} + +FeatureWebAPIAdapter* RadiosondePlugin::createFeatureWebAPIAdapter() const +{ + return new RadiosondeWebAPIAdapter(); +} diff --git a/plugins/feature/radiosonde/radiosondeplugin.h b/plugins/feature/radiosonde/radiosondeplugin.h new file mode 100644 index 000000000..c5e5a5ae7 --- /dev/null +++ b/plugins/feature/radiosonde/radiosondeplugin.h @@ -0,0 +1,49 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_RADIOSONDEPLUGIN_H +#define INCLUDE_FEATURE_RADIOSONDEPLUGIN_H + +#include +#include "plugin/plugininterface.h" + +class FeatureGUI; +class WebAPIAdapterInterface; + +class RadiosondePlugin : public QObject, PluginInterface { + Q_OBJECT + Q_INTERFACES(PluginInterface) + Q_PLUGIN_METADATA(IID "sdrangel.feature.radiosonde") + +public: + explicit RadiosondePlugin(QObject* parent = nullptr); + + const PluginDescriptor& getPluginDescriptor() const; + void initPlugin(PluginAPI* pluginAPI); + + virtual FeatureGUI* createFeatureGUI(FeatureUISet *featureUISet, Feature *feature) const; + virtual Feature* createFeature(WebAPIAdapterInterface *webAPIAdapterInterface) const; + virtual FeatureWebAPIAdapter* createFeatureWebAPIAdapter() const; + +private: + static const PluginDescriptor m_pluginDescriptor; + + PluginAPI* m_pluginAPI; +}; + +#endif // INCLUDE_FEATURE_RADIOSONDEPLUGIN_H diff --git a/plugins/feature/radiosonde/radiosondesettings.cpp b/plugins/feature/radiosonde/radiosondesettings.cpp new file mode 100644 index 000000000..565d44877 --- /dev/null +++ b/plugins/feature/radiosonde/radiosondesettings.cpp @@ -0,0 +1,149 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2022 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include + +#include "util/simpleserializer.h" +#include "settings/serializable.h" + +#include "radiosondesettings.h" + +const QStringList RadiosondeSettings::m_pipeTypes = { + QStringLiteral("RadiosondeDemod") +}; + +const QStringList RadiosondeSettings::m_pipeURIs = { + QStringLiteral("sdrangel.channel.radiosondedemod") +}; + +RadiosondeSettings::RadiosondeSettings() : + m_rollupState(nullptr) +{ + resetToDefaults(); +} + +void RadiosondeSettings::resetToDefaults() +{ + m_title = "Radiosonde"; + m_rgbColor = QColor(102, 0, 102).rgb(); + m_useReverseAPI = false; + m_reverseAPIAddress = "127.0.0.1"; + m_reverseAPIPort = 8888; + m_reverseAPIFeatureSetIndex = 0; + m_reverseAPIFeatureIndex = 0; + + m_y1 = ALTITUDE; + m_y2 = TEMPERATURE; + + for (int i = 0; i < RADIOSONDES_COLUMNS; i++) + { + m_radiosondesColumnIndexes[i] = i; + m_radiosondesColumnSizes[i] = -1; // Autosize + } +} + +QByteArray RadiosondeSettings::serialize() const +{ + SimpleSerializer s(1); + + s.writeString(1, m_title); + s.writeU32(2, m_rgbColor); + s.writeBool(3, m_useReverseAPI); + s.writeString(4, m_reverseAPIAddress); + s.writeU32(5, m_reverseAPIPort); + s.writeU32(6, m_reverseAPIFeatureSetIndex); + s.writeU32(7, m_reverseAPIFeatureIndex); + + if (m_rollupState) { + s.writeBlob(8, m_rollupState->serialize()); + } + + s.writeS32(10, (int)m_y1); + s.writeS32(11, (int)m_y2); + + for (int i = 0; i < RADIOSONDES_COLUMNS; i++) { + s.writeS32(300 + i, m_radiosondesColumnIndexes[i]); + } + + for (int i = 0; i < RADIOSONDES_COLUMNS; i++) { + s.writeS32(400 + i, m_radiosondesColumnSizes[i]); + } + + return s.final(); +} + +bool RadiosondeSettings::deserialize(const QByteArray& data) +{ + SimpleDeserializer d(data); + + if (!d.isValid()) + { + resetToDefaults(); + return false; + } + + if (d.getVersion() == 1) + { + QByteArray bytetmp; + uint32_t utmp; + QString strtmp; + QByteArray blob; + + d.readString(1, &m_title, "Radiosonde"); + d.readU32(2, &m_rgbColor, QColor(102, 0, 102).rgb()); + d.readBool(3, &m_useReverseAPI, false); + d.readString(4, &m_reverseAPIAddress, "127.0.0.1"); + d.readU32(5, &utmp, 0); + + if ((utmp > 1023) && (utmp < 65535)) { + m_reverseAPIPort = utmp; + } else { + m_reverseAPIPort = 8888; + } + + d.readU32(6, &utmp, 0); + m_reverseAPIFeatureSetIndex = utmp > 99 ? 99 : utmp; + d.readU32(7, &utmp, 0); + m_reverseAPIFeatureIndex = utmp > 99 ? 99 : utmp; + + if (m_rollupState) + { + d.readBlob(8, &bytetmp); + m_rollupState->deserialize(bytetmp); + } + + d.readS32(10, (int *)&m_y1, (int)ALTITUDE); + d.readS32(11, (int *)&m_y2, (int)TEMPERATURE); + + for (int i = 0; i < RADIOSONDES_COLUMNS; i++) { + d.readS32(300 + i, &m_radiosondesColumnIndexes[i], i); + } + + for (int i = 0; i < RADIOSONDES_COLUMNS; i++) { + d.readS32(400 + i, &m_radiosondesColumnSizes[i], -1); + } + + return true; + } + else + { + resetToDefaults(); + return false; + } +} diff --git a/plugins/feature/radiosonde/radiosondesettings.h b/plugins/feature/radiosonde/radiosondesettings.h new file mode 100644 index 000000000..4c199c731 --- /dev/null +++ b/plugins/feature/radiosonde/radiosondesettings.h @@ -0,0 +1,70 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_RADIOSONDESETTINGS_H_ +#define INCLUDE_FEATURE_RADIOSONDESETTINGS_H_ + +#include +#include + +#include "util/message.h" + +class Serializable; + +// Number of columns in the table +#define RADIOSONDES_COLUMNS 16 + +struct RadiosondeSettings +{ + QString m_title; + quint32 m_rgbColor; + bool m_useReverseAPI; + QString m_reverseAPIAddress; + uint16_t m_reverseAPIPort; + uint16_t m_reverseAPIFeatureSetIndex; + uint16_t m_reverseAPIFeatureIndex; + Serializable *m_rollupState; + + enum ChartData { + NONE, + ALTITUDE, + TEMPERATURE, + HUMIDITY, + PRESSURE, + SPEED, + VERTICAL_RATE, + HEADING, + BATTERY_VOLTAGE + }; + ChartData m_y1; + ChartData m_y2; + + int m_radiosondesColumnIndexes[RADIOSONDES_COLUMNS]; + int m_radiosondesColumnSizes[RADIOSONDES_COLUMNS]; + + RadiosondeSettings(); + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + void setRollupState(Serializable *rollupState) { m_rollupState = rollupState; } + + static const QStringList m_pipeTypes; + static const QStringList m_pipeURIs; +}; + +#endif // INCLUDE_FEATURE_RADIOSONDESETTINGS_H_ diff --git a/plugins/feature/radiosonde/radiosondewebapiadapter.cpp b/plugins/feature/radiosonde/radiosondewebapiadapter.cpp new file mode 100644 index 000000000..ef0f51127 --- /dev/null +++ b/plugins/feature/radiosonde/radiosondewebapiadapter.cpp @@ -0,0 +1,52 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB. // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "SWGFeatureSettings.h" +#include "radiosonde.h" +#include "radiosondewebapiadapter.h" + +RadiosondeWebAPIAdapter::RadiosondeWebAPIAdapter() +{} + +RadiosondeWebAPIAdapter::~RadiosondeWebAPIAdapter() +{} + +int RadiosondeWebAPIAdapter::webapiSettingsGet( + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setRadiosondeSettings(new SWGSDRangel::SWGRadiosondeSettings()); + response.getRadiosondeSettings()->init(); + Radiosonde::webapiFormatFeatureSettings(response, m_settings); + + return 200; +} + +int RadiosondeWebAPIAdapter::webapiSettingsPutPatch( + bool force, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) force; // no action + (void) errorMessage; + Radiosonde::webapiUpdateFeatureSettings(m_settings, featureSettingsKeys, response); + + return 200; +} diff --git a/plugins/feature/radiosonde/radiosondewebapiadapter.h b/plugins/feature/radiosonde/radiosondewebapiadapter.h new file mode 100644 index 000000000..9b402e8ae --- /dev/null +++ b/plugins/feature/radiosonde/radiosondewebapiadapter.h @@ -0,0 +1,50 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB. // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_Radiosonde_WEBAPIADAPTER_H +#define INCLUDE_Radiosonde_WEBAPIADAPTER_H + +#include "feature/featurewebapiadapter.h" +#include "radiosondesettings.h" + +/** + * Standalone API adapter only for the settings + */ +class RadiosondeWebAPIAdapter : public FeatureWebAPIAdapter { +public: + RadiosondeWebAPIAdapter(); + virtual ~RadiosondeWebAPIAdapter(); + + virtual QByteArray serialize() const { return m_settings.serialize(); } + virtual bool deserialize(const QByteArray& data) { return m_settings.deserialize(data); } + + virtual int webapiSettingsGet( + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage); + +private: + RadiosondeSettings m_settings; +}; + +#endif // INCLUDE_Radiosonde_WEBAPIADAPTER_H diff --git a/plugins/feature/radiosonde/readme.md b/plugins/feature/radiosonde/readme.md new file mode 100644 index 000000000..992754e67 --- /dev/null +++ b/plugins/feature/radiosonde/readme.md @@ -0,0 +1,55 @@ +

Radiosonde Feature Plugin

+ +

Introduction

+ +The Radiosonde feature displays a table containing the most recent information received from radiosondes +based on data received via [Radiosonde Demodulators](../../channelrx/demodradiasonde/readme.md). + +The chart can plot two data series vs time for the radiosonde selected in the table. + +The Radiosonde feature can draw ballons objects on the [Map](../../feature/map/readme.md) feature in 2D and 3D. + +

Interface

+ +![Radiosonde feature plugin GUI](../../../doc/img/Radiosonde_plugin.png) + +

Radiosonde Table

+ +The Radiosonde table displays the current status of each radiosonde, based on the latest received data from all Radiosonde Demodulators. + +* Serial - The serial number that uniquely identifiers each radiosonde. +* Type - The type of radiosonde. +* Lat (°) - Latitude in degrees. East positive. Double clicking on this column will center the map on this object. +* Lon (°) - Longitude in degrees. West positive. Double clicking on this column will center the map on this object. +* Alt (m) - The altitude of the radiosonde in metres. +* Spd (km/h) - Speed over ground in kilometres per hour. +* VR (m/s) - Vertical climb rate in metres per second. +* Hdg (°) - Heading in degrees. +* Status - Flight status of the radiosonde (E.g. On ground, ascent or descent). +* P (hPA) - Air pressure in hectopascals. Not all RS41s include a pressure sensor. A value ending with 'U' indicates a uncalibrated estimate and may be inaccurate. +* T (°C) - Air temperature in degrees Celsius. A value ending with 'U' indicates a uncalibrated estimate and may be inaccurate. +* U (%) - Relative humidity in percent. A value ending with 'U' indicates a uncalibrated estimate and may be inaccurate. +* Alt Max (m) - The maximum altitude seen for the radiosonde in metres. +* Freq (MHz) - The transmission frequency in megahertz as indicated by the radiosonde. +* BurstKill status - Whether the BurstKill timer is active. +* BurstKill timer - BurstKill timer. +* Updated - Gives the date and time the last message was received. +* Messages - Displays the number of messages received. + +Right clicking on the table header allows you to select which columns to show. The columns can be reorderd by left clicking and dragging the column header. + +Right clicking on a table cell allows you to copy the cell contents, or find the radiosonde on the map. + +

Map

+ +The Radiosonde feature can plot ballons (during ascent) and parachutes (during descent) on the [Map](../../feature/map/readme.md). +To use, simply open a Map feature and the Radiosonde plugin will display objects based upon the data it receives from that point. +Selecting a radiosonde item on the map will display a text bubble containing information from the above table. +To centre the map on an item in the table, double click in the Lat or Lon columns. + +![Radiosonde on map](../../../doc/img/Radiosonde_plugin_map.png) + +

Attribution

+ +Hot-air-balloon icons created by Freepik - https://www.flaticon.com/free-icons/hot-air-balloon +Parachute icons created by Freepik - https://www.flaticon.com/free-icons/parachute diff --git a/sdrbase/CMakeLists.txt b/sdrbase/CMakeLists.txt index d190b530b..5d5d3a468 100644 --- a/sdrbase/CMakeLists.txt +++ b/sdrbase/CMakeLists.txt @@ -191,6 +191,7 @@ set(sdrbase_SOURCES util/aprs.cpp util/astronomy.cpp util/azel.cpp + util/coordinates.cpp util/crc.cpp util/CRC64.cpp util/csv.cpp @@ -210,6 +211,7 @@ set(sdrbase_SOURCES util/planespotters.cpp util/png.cpp util/prettyprint.cpp + util/radiosonde.cpp util/rtpsink.cpp util/syncmessenger.cpp util/samplesourceserializer.cpp @@ -406,6 +408,7 @@ set(sdrbase_HEADERS util/aprs.h util/astronomy.h util/azel.h + util/coordinates.h util/CRC64.h util/csv.h util/db.h @@ -430,6 +433,7 @@ set(sdrbase_HEADERS util/planespotters.h util/png.h util/prettyprint.h + util/radiosonde.h util/rtpsink.h util/syncmessenger.h util/samplesourceserializer.h diff --git a/sdrbase/util/coordinates.cpp b/sdrbase/util/coordinates.cpp new file mode 100644 index 000000000..b5e6c23ed --- /dev/null +++ b/sdrbase/util/coordinates.cpp @@ -0,0 +1,318 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2011-2020 Cesium Contributors // +// Copyright (C) 2022 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 "coordinates.h" +#include "units.h" + +// Scale cartesian position on to surface of ellipsoid +QVector3D Coordinates::scaleToGeodeticSurface(QVector3D cartesian, QVector3D oneOverRadii, QVector3D oneOverRadiiSquared) +{ + float centerToleranceSquared = 0.1; + + double x2 = cartesian.x() * cartesian.x() * oneOverRadii.x() * oneOverRadii.x(); + double y2 = cartesian.y() * cartesian.y() * oneOverRadii.y() * oneOverRadii.y(); + double z2 = cartesian.z() * cartesian.z() * oneOverRadii.z() * oneOverRadii.z(); + + double squaredNorm = x2 + y2 + z2; + double ratio = sqrt(1.0 / squaredNorm); + + QVector3D intersection = cartesian * ratio; + + if (squaredNorm < centerToleranceSquared) { + return intersection; + } + + QVector3D gradient( + intersection.x() * oneOverRadiiSquared.x() * 2.0, + intersection.y() * oneOverRadiiSquared.y() * 2.0, + intersection.z() * oneOverRadiiSquared.z() * 2.0 + ); + + double lambda = ((1.0 - ratio) * cartesian.length()) / (0.5 * gradient.length()); + + double correction = 0.0; + double func; + double denominator; + double xMultiplier; + double yMultiplier; + double zMultiplier; + double xMultiplier2; + double yMultiplier2; + double zMultiplier2; + double xMultiplier3; + double yMultiplier3; + double zMultiplier3; + + do + { + lambda -= correction; + + xMultiplier = 1.0 / (1.0 + lambda * oneOverRadiiSquared.x()); + yMultiplier = 1.0 / (1.0 + lambda * oneOverRadiiSquared.y()); + zMultiplier = 1.0 / (1.0 + lambda * oneOverRadiiSquared.z()); + + xMultiplier2 = xMultiplier * xMultiplier; + yMultiplier2 = yMultiplier * yMultiplier; + zMultiplier2 = zMultiplier * zMultiplier; + + xMultiplier3 = xMultiplier2 * xMultiplier; + yMultiplier3 = yMultiplier2 * yMultiplier; + zMultiplier3 = zMultiplier2 * zMultiplier; + + func = x2 * xMultiplier2 + y2 * yMultiplier2 + z2 * zMultiplier2 - 1.0; + + denominator = + x2 * xMultiplier3 * oneOverRadiiSquared.x() + + y2 * yMultiplier3 * oneOverRadiiSquared.y() + + z2 * zMultiplier3 * oneOverRadiiSquared.z(); + + double derivative = -2.0 * denominator; + + correction = func / derivative; + } + while (abs(func) > 0.000000000001); + + QVector3D result( + cartesian.x() * xMultiplier, + cartesian.y() * yMultiplier, + cartesian.z() * zMultiplier + ); + return result; +} + +// QVector3D.normalized doesn't work with small numbers +QVector3D Coordinates::normalized(QVector3D vec) +{ + QVector3D result; + float magnitude = vec.length(); + result.setX(vec.x() / magnitude); + result.setY(vec.y() / magnitude); + result.setZ(vec.z() / magnitude); + return result; +} + +// Convert ECEF position to geodetic coordinates +void Coordinates::ecefToGeodetic(double x, double y, double z, double &latitude, double &longitude, double &height) +{ + QVector3D wgs84OneOverRadix(1.0 / 6378137.0, + 1.0 / 6378137.0, + 1.0 / 6356752.3142451793); + QVector3D wgs84OneOverRadiiSquared(1.0 / (6378137.0 * 6378137.0), + 1.0 / (6378137.0 * 6378137.0), + 1.0 / (6356752.3142451793 * 6356752.3142451793)); + + QVector3D cartesian(x, y, z); + + QVector3D p = scaleToGeodeticSurface(cartesian, wgs84OneOverRadix, wgs84OneOverRadiiSquared); + + QVector3D n = p * wgs84OneOverRadiiSquared; + n = normalized(n); + + QVector3D h = cartesian - p; + + longitude = atan2(n.y(), n.x()); + latitude = asin(n.z()); + + longitude = Units::radiansToDegrees(longitude); + latitude = Units::radiansToDegrees(latitude); + + double t = QVector3D::dotProduct(h, cartesian); + double sign = t >= 0.0 ? 1.0 : 0.0; + height = sign * h.length(); +} + +// Convert ECEF velocity to speed and heading +void Coordinates::ecefVelToSpeedHeading(double latitude, double longitude, + double velX, double velY, double velZ, + double &speed, double &verticalRate, double &heading) +{ + if ((velX == 0.0) && (velY == 0.0) && (velZ == 0.0)) + { + speed = 0.0; + heading = 0.0; + verticalRate = 0.0; + return; + } + + double latRad = Units::degreesToRadians(latitude); + double lonRad = Units::degreesToRadians(longitude); + + double sinLat = sin(latRad); + double cosLat = cos(latRad); + double sinLon = sin(lonRad); + double cosLon = cos(lonRad); + + double velEast = -velX * sinLon + velY * cosLon; + double velNorth = -velX * sinLat * cosLon - velY * sinLat * sinLon + velZ * cosLat; + double velUp = velX * cosLat * cosLon + velY * cosLat * sinLon + velZ * sinLat; + + speed = sqrt(velNorth * velNorth + velEast * velEast); + verticalRate = velUp; + + double headingRad = atan2(velEast, velNorth); + heading = Units::radiansToDegrees(headingRad); + if (heading < 0.0) { + heading += 360.0; + } else if (heading >= 360.0) { + heading -= 360.0; + } +} + +// Convert a position specified in longitude, latitude in degrees and height in metres above WGS84 ellipsoid in to +// Earth Centered Earth Fixed frame cartesian coordinates +// See Cesium.Cartesian3.fromDegrees +QVector3D Coordinates::geodeticToECEF(double longitude, double latitude, double height) +{ + return geodeticRadiansToECEF(Units::degreesToRadians(longitude), Units::degreesToRadians(latitude), height); +} + +// FIXME: QVector3D is only float! +// See Cesium.Cartesian3.fromRadians +QVector3D Coordinates::geodeticRadiansToECEF(double longitude, double latitude, double height) +{ + QVector3D wgs84RadiiSquared(6378137.0 * 6378137.0, 6378137.0 * 6378137.0, 6356752.3142451793 * 6356752.3142451793); + + double cosLatitude = cos(latitude); + QVector3D n; + n.setX(cosLatitude * cos(longitude)); + n.setY(cosLatitude * sin(longitude)); + n.setZ(sin(latitude)); + n.normalize(); + QVector3D k; + k = wgs84RadiiSquared * n; + double gamma = sqrt(QVector3D::dotProduct(n, k)); + k = k / gamma; + n = n * height; + return k + n; +} + +// Convert heading, pitch and roll in degrees to a quaternoin +// See: Cesium.Quaternion.fromHeadingPitchRoll +QQuaternion Coordinates::fromHeadingPitchRoll(double heading, double pitch, double roll) +{ + QVector3D xAxis(1, 0, 0); + QVector3D yAxis(0, 1, 0); + QVector3D zAxis(0, 0, 1); + + QQuaternion rollQ = QQuaternion::fromAxisAndAngle(xAxis, roll); + + QQuaternion pitchQ = QQuaternion::fromAxisAndAngle(yAxis, -pitch); + + QQuaternion headingQ = QQuaternion::fromAxisAndAngle(zAxis, -heading); + + QQuaternion temp = rollQ * pitchQ; + + return headingQ * temp; +} + +// Calculate a transformation matrix from a East, North, Up frame at the given position to Earth Centered Earth Fixed frame +// See: Cesium.Transforms.eastNorthUpToFixedFrame +QMatrix4x4 Coordinates::eastNorthUpToECEF(QVector3D origin) +{ + // TODO: Handle special case at centre of earth and poles + QVector3D up = origin.normalized(); + QVector3D east(-origin.y(), origin.x(), 0.0); + east.normalize(); + QVector3D north = QVector3D::crossProduct(up, east); + QMatrix4x4 result( + east.x(), north.x(), up.x(), origin.x(), + east.y(), north.y(), up.y(), origin.y(), + east.z(), north.z(), up.z(), origin.z(), + 0.0, 0.0, 0.0, 1.0 + ); + return result; +} + +// Convert 3x3 rotation matrix to a quaternoin +// Although there is a method for this in Qt: QQuaternion::fromRotationMatrix, it seems to +// result in different signs, so the following is based on Cesium code +QQuaternion Coordinates::fromRotation(QMatrix3x3 mat) +{ + QQuaternion q; + + double trace = mat(0, 0) + mat(1, 1) + mat(2, 2); + + if (trace > 0.0) + { + double root = sqrt(trace + 1.0); + q.setScalar(0.5 * root); + root = 0.5 / root; + + q.setX((mat(2,1) - mat(1,2)) * root); + q.setY((mat(0,2) - mat(2,0)) * root); + q.setZ((mat(1,0) - mat(0,1)) * root); + } + else + { + double next[] = {1, 2, 0}; + int i = 0; + if (mat(1,1) > mat(0,0)) { + i = 1; + } + if (mat(2,2) > mat(0,0) && mat(2,2) > mat(1,1)) { + i = 2; + } + int j = next[i]; + int k = next[j]; + + double root = sqrt(mat(i,i) - mat(j,j) - mat(k,k) + 1); + double quat[] = {0.0, 0.0, 0.0}; + quat[i] = 0.5 * root; + root = 0.5 / root; + + q.setScalar((mat(j,k) - mat(k,j)) * root); + quat[j] = (mat(i,j) + mat(j,i)) * root; + quat[k] = (mat(i,k) + mat(k,i)) * root; + q.setX(-quat[0]); + q.setY(-quat[1]); + q.setZ(-quat[2]); + } + return q; +} + +// Calculate orientation quaternion for a model (such as an aircraft) based on position and (HPR) heading, pitch and roll (in degrees) +// While Cesium supports specifying orientation as HPR, CZML doesn't currently. See https://github.com/CesiumGS/cesium/issues/5184 +// CZML requires the orientation to be in the Earth Centered Earth Fixed (geocentric) reference frame (https://en.wikipedia.org/wiki/Local_tangent_plane_coordinates) +// The orientation therefore depends not only on HPR but also on position +// +// glTF uses a right-handed axis convention; that is, the cross product of right and forward yields up. glTF defines +Y as up, +Z as forward, and -X as right. +// Cesium.Quaternion.fromHeadingPitchRoll Heading is the rotation about the negative z axis. Pitch is the rotation about the negative y axis. Roll is the rotation about the positive x axis. +QQuaternion Coordinates::orientation(double longitude, double latitude, double altitude, double heading, double pitch, double roll) +{ + // Forward direction for gltf models in Cesium seems to be Eastward, rather than Northward, so we adjust heading by -90 degrees + heading = -90 + heading; + + // Convert position to Earth Centered Earth Fixed (ECEF) frame + QVector3D positionECEF = geodeticToECEF(longitude, latitude, altitude); + + // Calculate matrix to transform from East, North, Up (ENU) frame to ECEF frame + QMatrix4x4 enuToECEFTransform = eastNorthUpToECEF(positionECEF); + + // Calculate rotation based on HPR in ENU frame + QQuaternion hprENU = fromHeadingPitchRoll(heading, pitch, roll); + + // Transform rotation from ENU to ECEF + QMatrix3x3 hprENU3 = hprENU.toRotationMatrix(); + QMatrix4x4 hprENU4(hprENU3); + QMatrix4x4 transform = enuToECEFTransform * hprENU4; + + // Convert from 4x4 matrix to 3x3 matrix then to a quaternion + QQuaternion oq = fromRotation(transform.toGenericMatrix<3,3>()); + + return oq; +} diff --git a/sdrbase/util/coordinates.h b/sdrbase/util/coordinates.h new file mode 100644 index 000000000..1876dec6f --- /dev/null +++ b/sdrbase/util/coordinates.h @@ -0,0 +1,52 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2022 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_COORDINATES_H +#define INCLUDE_COORDINATES_H + +#include "export.h" + +#include +#include +#include +#include +#include + +// Functions for transformations between geodetic and ECEF coordinates +class SDRBASE_API Coordinates { + +public: + + static QVector3D geodeticToECEF(double longitude, double latitude, double height=0.0); + static QVector3D geodeticRadiansToECEF(double longitude, double latitude, double height=0.0); + static QMatrix4x4 eastNorthUpToECEF(QVector3D origin); + static void ecefToGeodetic(double x, double y, double z, double &latitude, double &longitude, double &height); + static void ecefVelToSpeedHeading(double latitude, double longitude, + double velX, double velY, double velZ, + double &speed, double &verticalRate, double &heading); + static QQuaternion orientation(double longitude, double latitude, double altitude, double heading, double pitch, double roll); + +protected: + + static QVector3D scaleToGeodeticSurface(QVector3D cartesian, QVector3D oneOverRadii, QVector3D oneOverRadiiSquared); + static QVector3D normalized(QVector3D vec); + static QQuaternion fromHeadingPitchRoll(double heading, double pitch, double roll); + static QQuaternion fromRotation(QMatrix3x3 mat); + +}; + +#endif // INCLUDE_COORDINATES_H diff --git a/sdrbase/util/radiosonde.cpp b/sdrbase/util/radiosonde.cpp new file mode 100644 index 000000000..7b3b46851 --- /dev/null +++ b/sdrbase/util/radiosonde.cpp @@ -0,0 +1,710 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// Based on code and docs by einergehtnochrein, rs1729 and bazjo // +// // +// 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/radiosonde.h" +#include "util/coordinates.h" + +RS41Frame::RS41Frame(const QByteArray ba) : + m_statusValid(false), + m_batteryVoltage(0.0), + m_pcbTemperature(0), + m_humiditySensorHeating(0), + m_transmitPower(0), + m_maxSubframeNumber(0), + m_subframeNumber(0), + m_measValid(false), + m_gpsInfoValid(false), + m_posValid(false), + m_latitude(0.0), + m_longitude(0.0), + m_height(0.0), + m_bytes(ba), + m_temperatureCalibrated(false), + m_pressureCalibrated(false), + m_humidityTemperatureCalibrated(false), + m_humidityCalibrated(false) +{ + int length = getFrameLength(ba[RS41_OFFSET_FRAME_TYPE]); + for (int i = RS41_OFFSET_BLOCK_0; i < length; ) + { + uint8_t blockID = ba[i+0]; + uint8_t blockLength = ba[i+1]; + switch (blockID) + { + case RS41_ID_STATUS: + decodeStatus(ba.mid(i+2, blockLength)); + break; + case RS41_ID_MEAS: + decodeMeas(ba.mid(i+2, blockLength)); + break; + case RS41_ID_GPSINFO: + decodeGPSInfo(ba.mid(i+2, blockLength)); + break; + case RS41_ID_GPSRAW: + break; + case RS41_ID_GPSPOS: + decodeGPSPos(ba.mid(i+2, blockLength)); + break; + case RS41_ID_EMPTY: + break; + } + i += 2 + blockLength + 2; // ID, length, data, CRC + } +} + +QString RS41Frame::toHex() +{ + return m_bytes.toHex(); +} + +uint16_t RS41Frame::getUInt16(const QByteArray ba, int offset) const +{ + return (ba[offset] & 0xff) + | ((ba[offset+1] & 0xff) << 8); +} + +uint32_t RS41Frame::getUInt24(const QByteArray ba, int offset) const +{ + return (ba[offset] & 0xff) + | ((ba[offset+1] & 0xff) << 8) + | ((ba[offset+2] & 0xff) << 16); +} + +uint32_t RS41Frame::getUInt32(const QByteArray ba, int offset) const +{ + return (ba[offset] & 0xff) + | ((ba[offset+1] & 0xff) << 8) + | ((ba[offset+2] & 0xff) << 16) + | ((ba[offset+3] & 0xff) << 24); +} + +void RS41Frame::decodeStatus(const QByteArray ba) +{ + m_statusValid = true; + m_frameNumber = getUInt16(ba, 0); + m_serial = QString(ba.mid(0x2, 8)); + m_batteryVoltage = (ba[0xa] & 0xff) / 10.0; + QStringList phases = {"Ground", "Ascent", "0x2", "Descent"}; + int phase = ba[0xd] & 0x3; + m_flightPhase = phases[phase]; + m_batteryStatus = (ba[0xe] & 0x10) == 0 ? "OK" : "Low"; + m_pcbTemperature = (ba[0x10] & 0xff); + m_humiditySensorHeating = getUInt16(ba, 0x13); + m_transmitPower = ba[0x15] & 0xff; + m_maxSubframeNumber = ba[0x16] & 0xff; + m_subframeNumber = ba[0x17] & 0xff; + m_subframe = ba.mid(0x18, 16); +} + +void RS41Frame::decodeMeas(const QByteArray ba) +{ + m_measValid = true; + m_tempMain = getUInt24(ba, 0x0); + m_tempRef1 = getUInt24(ba, 0x3); + m_tempRef2 = getUInt24(ba, 0x6); + m_humidityMain = getUInt24(ba, 0x9); + m_humidityRef1 = getUInt24(ba, 0xc); + m_humidityRef2 = getUInt24(ba, 0xf); + m_humidityTempMain = getUInt24(ba, 0x12); + m_humidityTempRef1 = getUInt24(ba, 0x15); + m_humidityTempRef2 = getUInt24(ba, 0x18); + m_pressureMain = getUInt24(ba, 0x1b); + m_pressureRef1 = getUInt24(ba, 0x1e); + m_pressureRef2 = getUInt24(ba, 0x21); + m_pressureTemp = getUInt16(ba, 0x26) / 100.0f; +} + +void RS41Frame::decodeGPSInfo(const QByteArray ba) +{ + m_gpsInfoValid = true; + uint16_t gpsWeek = getUInt16(ba, 0x0); + uint32_t gpsTimeOfWeek = getUInt32(ba, 0x2); // Milliseconds + QDateTime epoch(QDate(1980, 1, 6), QTime(0, 0, 0), Qt::OffsetFromUTC, 18); // GPS doesn't count leap seconds + m_gpsDateTime = epoch.addDays(gpsWeek*7).addMSecs(gpsTimeOfWeek); +} + +void RS41Frame::decodeGPSPos(const QByteArray ba) +{ + m_satellitesUsed = ba[0x12] & 0xff; + if (m_satellitesUsed > 0) + { + m_posValid = true; + int32_t ecefX = (int32_t)getUInt32(ba, 0x0); + int32_t ecefY = (int32_t)getUInt32(ba, 0x4); + int32_t ecefZ = (int32_t)getUInt32(ba, 0x8); + // Convert cm to m + // Convert to latitude, longitude and altitude + Coordinates::ecefToGeodetic(ecefX / 100.0, ecefY / 100.0, ecefZ / 100.0, m_latitude, m_longitude, m_height); + int32_t velX = (int16_t)getUInt16(ba, 0xc); + int32_t velY = (int16_t)getUInt16(ba, 0xe); + int32_t velZ = (int16_t)getUInt16(ba, 0x10); + // Convert cm/s to m/s + // Calculate speed / heading + Coordinates::ecefVelToSpeedHeading(m_latitude, m_longitude, velX / 100.0, velY / 100.0, velZ / 100.0, m_speed, m_verticalRate, m_heading); + } +} + +// Find the water vapor saturation pressure for a given temperature. +float waterVapourSaturationPressure(float tCelsius) +{ + // Convert to Kelvin + float T = tCelsius + 273.15f; + + // Correction + T = - 0.4931358f + + (1.0f + 4.6094296e-3f) * T + - 1.3746454e-5f * T * T + + 1.2743214e-8f * T * T * T; + + // Hyland and Wexler equation + float p = expf(-5800.2206f / T + + 1.3914993f + + 6.5459673f * logf(T) + - 4.8640239e-2f * T + + 4.1764768e-5f * T * T + - 1.4452093e-8f * T * T * T); + + // Scale result to hPa + return p / 100.0f; +} + +float calcT(int f, int f1, int f2, float r1, float r2, float *poly, float *cal) +{ + /*float g = (float)(f2-f1) / (r2-r1); // gain + float Rb = (f1*r2-f2*r1) / (float)(f2-f1); // offset + float Rc = f/g - Rb; + float R = Rc * cal[0]; + float T = (poly[0] + poly[1]*R + poly[2]*R*R + cal[1])*(1.0 + cal[2]); + return T; + */ + + // Convert integer measurement to scale factor + float s = (f-f1)/(float)(f2-f1); + + // Calculate resistance (scale between two reference resistors) + float rUncal = r1 + (r2 - r1) * s; + float r = rUncal * cal[0]; + + // Convert resistance to temperature + float tUncal = poly[0] + poly[1]*r + poly[2]*r*r; + + // Correct temperature (5th order polynomial) + float tCal = 0.0f; + for (int i = 6; i > 0; i--) + { + tCal *= tUncal; + tCal += cal[i]; + } + tCal += tUncal; + + return tCal; +} + +float calcU(int cInt, int cMin, int cMax, float c1, float c2, float T, float HT, float *capCal, float *matrixCal) +{ + //qDebug() << "cInt " << cInt << " cMin " << cMin << " cMax " << cMax << " c1 " << c1 << " c2 " << c2 << " T " << T << " HT " << HT << " capCal[0] " << capCal[0] << " capCal[1] " << capCal[1]; + /* + float a0 = 7.5f; + float a1 = 350.0f / capCal[0]; + float fh = (cInt-cMin) / (float)(cMax-cMin); + float rh = 100.0f * (a1*fh - a0); + float T0 = 0.0f; + float T1 = -25.0f; + rh += T0 - T/5.5; + if (T < T1) { + rh *= 1.0 + (T1-T)/90.0; + } + if (rh < 0.0) { + rh = 0.0; + } + if (rh > 100.0) { + rh = 100.0; + } + if (T < -273.0) { + rh = -1.0; + } + + qDebug() << "RH old method: " << rh; */ + + + // Convert integer measurement to scale factor + float s = (cInt - cMin) / (float)(cMax-cMin); + + // Calculate capacitance (scale between two reference caps) + float cUncal = c1 + (c2 - c1) * s; + float cCal = (cUncal / capCal[0] - 1.0f) * capCal[1]; + float uUncal = 0.0f; + float t = (HT - 20.0f) / 180.0f; + float f1 = 1.0f; + for (int i = 0; i < 7; i++) + { + float f2 = 1.0; + for (int j = 0; j < 6; j++) + { + uUncal += f1 * f2 * matrixCal[i*6+j]; + f2 *= t; + } + f1 *= cCal; + } + + // Adjust for difference in outside air temperature and the humidty sensor temperature + float uCal = uUncal * waterVapourSaturationPressure(T) / waterVapourSaturationPressure(HT); + + // Ensure within range of 0..100% + uCal = std::min(100.0f, uCal); + uCal = std::max(0.0f, uCal); + + return uCal; +} + +float calcP(int f, int f1, int f2, float pressureTemp, float *cal) +{ + // Convert integer measurement to scale factor + float s = (f-f1) / (float)(f2-f1); + + float t = pressureTemp; + float t2 = t * t; + float t3 = t2 * t; + + float poly[6]; + poly[0] = cal[0] + cal[7] * t + cal[11] * t2 + cal[15] * t3; + poly[1] = cal[1] + cal[8] * t + cal[12] * t2 + cal[16] * t3; + poly[2] = cal[2] + cal[9] * t + cal[13] * t2 + cal[17] * t3; + poly[3] = cal[3] + cal[10] * t + cal[14] * t2; + poly[4] = cal[4]; + poly[5] = cal[5]; + + float p = cal[6] / s; + float p2 = p * p; + float p3 = p2 * p; + float p4 = p3 * p; + float p5 = p4 * p; + + float pCal = poly[0] + poly[1] * p + poly[2] * p2 + poly[3] * p3 + poly[4] * p4 + poly[5] * p5; + + return pCal; +} + +float RS41Frame::getPressureFloat(const RS41Subframe *subframe) +{ + if (!m_pressureCalibrated) { + calcPressure(subframe); + } + return m_pressure; +} + +QString RS41Frame::getPressureString(const RS41Subframe *subframe) +{ + if (!m_pressureCalibrated) { + calcPressure(subframe); + } + return m_pressureString; +} + +float RS41Frame::getTemperatureFloat(const RS41Subframe *subframe) +{ + if (!m_temperatureCalibrated) { + calcTemperature(subframe); + } + return m_temperature; +} + +QString RS41Frame::getTemperatureString(const RS41Subframe *subframe) +{ + if (!m_temperatureCalibrated) { + calcTemperature(subframe); + } + return m_temperatureString; +} + +void RS41Frame::calcPressure(const RS41Subframe *subframe) +{ + float cal[18]; + + if (m_pressureMain == 0) + { + m_pressure = 0.0f; + m_pressureString = ""; + return; + } + + m_pressureCalibrated = subframe->getPressureCal(cal); + + m_pressure = calcP(m_pressureMain, m_pressureRef1, m_pressureRef2, m_pressureTemp, cal); + + // RS41 pressure resolution of 0.01hPa + m_pressureString = QString::number(m_pressure, 'f', 2); + + if (!m_pressureCalibrated) { + m_pressureString = m_pressureString + "U"; // U for uncalibrated + } +} + +void RS41Frame::calcTemperature(const RS41Subframe *subframe) +{ + float r1, r2; + float poly[3]; + float cal[7]; + + if (m_tempMain == 0) + { + m_temperature = 0.0f; + m_temperatureString = ""; + return; + } + + m_temperatureCalibrated = subframe->getTempCal(r1, r2, poly, cal); + + m_temperature = calcT(m_tempMain, m_tempRef1, m_tempRef2, + r1, r2, + poly, cal); + + // RS41 temperature resolution of 0.01C + m_temperatureString = QString::number(m_temperature, 'f', 2); + + if (!m_temperatureCalibrated) { + m_temperatureString = m_temperatureString + "U"; // U for uncalibrated + } +} + +float RS41Frame::getHumidityTemperatureFloat(const RS41Subframe *subframe) +{ + if (!m_humidityTemperatureCalibrated) { + calcHumidityTemperature(subframe); + } + return m_humidityTemperature; +} + +void RS41Frame::calcHumidityTemperature(const RS41Subframe *subframe) +{ + float r1, r2; + float poly[3]; + float cal[7]; + + if (m_humidityTempMain == 0) + { + m_humidityTemperature = 0.0f; + return; + } + + m_humidityTemperatureCalibrated = subframe->getHumidityTempCal(r1, r2, poly, cal); + + m_humidityTemperature = calcT(m_humidityTempMain, m_humidityTempRef1, m_humidityTempRef2, + r1, r2, + poly, cal); +} + +float RS41Frame::getHumidityFloat(const RS41Subframe *subframe) +{ + if (!m_humidityCalibrated) { + calcHumidity(subframe); + } + return m_humidity; +} + +QString RS41Frame::getHumidityString(const RS41Subframe *subframe) +{ + if (!m_humidityCalibrated) { + calcHumidity(subframe); + } + return m_humidityString; +} + +void RS41Frame::calcHumidity(const RS41Subframe *subframe) +{ + float c1, c2; + float capCal[2]; + float calMatrix[7*6]; + + if (m_humidityMain == 0) + { + m_humidity = 0.0f; + m_humidityString = ""; + return; + } + + float temperature = getTemperatureFloat(subframe); + float humidityTemperature = getHumidityTemperatureFloat(subframe); + + bool humidityCalibrated = subframe->getHumidityCal(c1, c2, capCal, calMatrix); + + m_humidityCalibrated = m_temperatureCalibrated && m_humidityTemperatureCalibrated && humidityCalibrated; + + m_humidity = calcU(m_humidityMain, m_humidityRef1, m_humidityRef2, + c1, c2, + temperature, humidityTemperature, + capCal, calMatrix); + + // RS41 humidity resolution of 0.1% + m_humidityString = QString::number(m_humidity, 'f', 1); + + if (!m_humidityCalibrated) { + m_humidityString = m_humidityString + "U"; // U for uncalibrated + } +} + +RS41Frame* RS41Frame::decode(const QByteArray ba) +{ + return new RS41Frame(ba); +} + +int RS41Frame::getFrameLength(int frameType) +{ + return frameType == RS41_FRAME_STD ? RS41_LENGTH_STD : RS41_LENGTH_EXT; +} + +RS41Subframe::RS41Subframe() : + m_subframe(51*16, (char)0) +{ + for (int i = 0; i < 51; i++) { + m_subframeValid[i] = false; + } +} + +// Update subframe with subframe data from received message +void RS41Subframe::update(RS41Frame *message) +{ + m_subframeValid[message->m_subframeNumber] = true; + int offset = message->m_subframeNumber * 16; + for (int i = 0; i < 16; i++) { + m_subframe[offset+i] = message->m_subframe[i]; + } +} + +// Indicate if we have all the required temperature calibration data +bool RS41Subframe::hasTempCal() const +{ + return m_subframeValid[3] && m_subframeValid[4] && m_subframeValid[5] && m_subframeValid[6] && m_subframeValid[7]; +} + +// Get temperature calibration data +// r1, r2 - Temperature reference resistances (Ohms) +// poly - Resistance to temperature 2nd order polynomial +bool RS41Subframe::getTempCal(float &r1, float &r2, float *poly, float *cal) const +{ + if (hasTempCal()) + { + r1 = getFloat(0x3d); + r2 = getFloat(0x41); + for (int i = 0; i < 3; i++) { + poly[i] = getFloat(0x4d + i * 4); + } + for (int i = 0; i < 7; i++) { + cal[i] = getFloat(0x59 + i * 4); + } + return true; + } + else + { + // Use default values + r1 = 750.0f; + r2 = 1100.0f; + poly[0] = -243.9108f; + poly[1] = 0.187654f; + poly[2] = 8.2e-06f; + cal[0] = 1.279928f; + for (int i = 1; i < 7; i++) { + cal[i] = 0.0f; + } + return false; + } +} + +// Indicate if we have all the required humidty calibration data +bool RS41Subframe::hasHumidityCal() const +{ + return m_subframeValid[4] && m_subframeValid[7] + && m_subframeValid[8] && m_subframeValid[9] && m_subframeValid[0xa] && m_subframeValid[0xb] + && m_subframeValid[0xc] && m_subframeValid[0xd] && m_subframeValid[0xe] && m_subframeValid[0xf] + && m_subframeValid[0x10] && m_subframeValid[0x11] && m_subframeValid[0x12]; +} + +// Get humidty calibration data +bool RS41Subframe::getHumidityCal(float &c1, float &c2, float *capCal, float *calMatrix) const +{ + if (hasHumidityCal()) + { + c1 = getFloat(0x45); + c2 = getFloat(0x49); + for (int i = 0; i < 2; i++) { + capCal[i] = getFloat(0x75 + i * 4); + } + for (int i = 0; i < 7*6; i++) { + calMatrix[i] = getFloat(0x7d + i * 4); + } + return true; + } + else + { + // Use default values + c1 = 0.0f; + c2 = 47.0f; + capCal[0] = 45.9068f; + capCal[1] = 4.92924f; + static const float calMatrixDefault[7*6] = { + -0.002586f, -2.24367f, 9.92294f, -3.61913f, 54.3554f, -93.3012f, + 51.7056f, 38.8709f, 209.437f, -378.437f, 9.17326f, 19.5301f, + 150.257f, -150.907f, -280.315f, 182.293f, 3247.39f, 4083.65f, + -233.568f, 345.375f, 200.217f, -388.246f, -3617.66f, 0.0f, + 225.841f, -233.051f, 0.0f, 0.0f, 0.0f, 0.0f, + -93.0635f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f + }; + std::copy(calMatrixDefault, calMatrixDefault + 7*6, calMatrix); + return false; + } +} + +// Indicate if we have all the required humidty temperature sensor calibration data +bool RS41Subframe::hasHumidityTempCal() const +{ + return m_subframeValid[3] && m_subframeValid[4] && m_subframeValid[0x12] && m_subframeValid[0x13] && m_subframeValid[0x14]; +} + +// Get humidty temperature sensor calibration data +bool RS41Subframe::getHumidityTempCal(float &r1, float &r2, float *poly, float *cal) const +{ + if (hasHumidityTempCal()) + { + r1 = getFloat(0x3d); + r2 = getFloat(0x41); + for (int i = 0; i < 3; i++) { + poly[i] = getFloat(0x125 + i * 4); + } + for (int i = 0; i < 7; i++) { + cal[i] = getFloat(0x131 + i * 4); + } + return true; + } + else + { + // Use default values + r1 = 750.0f; + r2 = 1100.0f; + poly[0] = -243.9108f; + poly[1] = 0.187654f; + poly[2] = 8.2e-06f; + cal[0] = 1.279928f; + for (int i = 1; i < 7; i++) { + cal[i] = 0.0f; + } + return false; + } +} + +// Indicate if we have all the required pressure calibration data +bool RS41Subframe::hasPressureCal() const +{ + return m_subframeValid[0x25] && m_subframeValid[0x26] && m_subframeValid[0x27] + && m_subframeValid[0x28] && m_subframeValid[0x29] && m_subframeValid[0x2a]; +} + +// Get pressure calibration data +bool RS41Subframe::getPressureCal(float *cal) const +{ + if (hasPressureCal()) + { + for (int i = 0; i < 18; i++) { + cal[i] = getFloat(0x25e + i * 4); + } + return true; + } + else + { + // Use default values - TODO: Need to obtain from inflight device + for (int i = 0; i < 18; i++) { + cal[i] = 0.0f; + } + return false; + } +} + +// Get type of RS41. E.g. "RS41-SGP" +QString RS41Subframe::getType() const +{ + if (m_subframeValid[0x21] & m_subframeValid[0x22]) + { + return QString(m_subframe.mid(0x218, 10)).trimmed(); + } + else + { + return "RS41"; + } +} + +// Get transmission frequency in MHz +QString RS41Subframe::getFrequencyMHz() const +{ + if (m_subframeValid[0]) + { + uint8_t lower = m_subframe[2] & 0xff; + uint8_t upper = m_subframe[3] & 0xff; + float freq = 400.0 + (upper + (lower / 255.0)) * 0.04; + return QString::number(freq, 'f', 3); + } + else + { + return ""; + } +} + +QString RS41Subframe::getBurstKillStatus() const +{ + if (m_subframeValid[2]) + { + uint8_t status = m_subframe[0x2b]; + return status == 0 ? "Inactive" : "Active"; + } + else + { + return ""; + } +} + +// Seconds until power-off once active +QString RS41Subframe::getBurstKillTimer() const +{ + if (m_subframeValid[0x31]) + { + uint16_t secs = getUInt16(0x316); + QTime t(0, 0, 0); + t = t.addSecs(secs); + return t.toString("hh:mm:ss"); + } + else + { + return ""; + } +} + +uint16_t RS41Subframe::getUInt16(int offset) const +{ + return (m_subframe[offset] & 0xff) | ((m_subframe[offset+1] & 0xff) << 8); +} + +float RS41Subframe::getFloat(int offset) const +{ + float f; + // Assumes host is little endian with 32-bit float + memcpy(&f, m_subframe.data() + offset, 4); + return f; +} diff --git a/sdrbase/util/radiosonde.h b/sdrbase/util/radiosonde.h new file mode 100644 index 000000000..c21cdb50b --- /dev/null +++ b/sdrbase/util/radiosonde.h @@ -0,0 +1,180 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_RADIOSONDE_H +#define INCLUDE_RADIOSONDE_H + +#include +#include +#include + +#include +#include + +#include "util/units.h" + +#include "export.h" + +#define RS41_LENGTH_STD 320 +#define RS41_LENGTH_EXT 518 + +#define RS41_OFFSET_RS 0x08 +#define RS41_OFFSET_FRAME_TYPE 0x38 +#define RS41_OFFSET_BLOCK_0 0x39 + +#define RS41_FRAME_STD 0x0f +#define RS41_FRAME_EXT 0xf0 + +#define RS41_ID_STATUS 0x79 +#define RS41_ID_MEAS 0x7a +#define RS41_ID_GPSINFO 0x7c +#define RS41_ID_GPSRAW 0x7d +#define RS41_ID_GPSPOS 0x7b +#define RS41_ID_EMPTY 0x76 + +#define RS41_RS_N 255 +#define RS41_RS_K 231 +#define RS41_RS_2T (RS41_RS_N-RS41_RS_K) +#define RS41_RS_INTERLEAVE 2 +#define RS41_RS_DATA (264/RS41_RS_INTERLEAVE) +#define RS41_RS_PAD (RS41_RS_K-RS41_RS_DATA) + +class RS41Subframe; + +// Frame of data transmitted by RS41 radiosonde +class SDRBASE_API RS41Frame { +public: + + // Status + bool m_statusValid; + uint16_t m_frameNumber; // Increments every frame + QString m_serial; // Serial number + float m_batteryVoltage; // In volts + QString m_flightPhase; // On ground, ascent, descent + QString m_batteryStatus; // OK or Low + uint8_t m_pcbTemperature; // In degrees C + uint16_t m_humiditySensorHeating; // 0..1000 + uint8_t m_transmitPower; // 0..7 + uint8_t m_maxSubframeNumber; + uint8_t m_subframeNumber; + QByteArray m_subframe; // 16 bytes of subframe + + // Meas + bool m_measValid; + uint32_t m_tempMain; + uint32_t m_tempRef1; + uint32_t m_tempRef2; + uint32_t m_humidityMain; + uint32_t m_humidityRef1; + uint32_t m_humidityRef2; + uint32_t m_humidityTempMain; + uint32_t m_humidityTempRef1; + uint32_t m_humidityTempRef2; + uint32_t m_pressureMain; + uint32_t m_pressureRef1; + uint32_t m_pressureRef2; + float m_pressureTemp; // Pressure sensor module temperature - In degrees C + + // GPSInfo + bool m_gpsInfoValid; + QDateTime m_gpsDateTime; + + // GPSPos + bool m_posValid; + double m_latitude; // In degrees + double m_longitude; // In degrees + double m_height; // In metres + double m_speed; // In m/s + double m_heading; // In degreees + double m_verticalRate; // In m/s + int m_satellitesUsed; + + RS41Frame(const QByteArray ba); + ~RS41Frame() {} + QString toHex(); + void decodeStatus(const QByteArray ba); + void decodeMeas(const QByteArray ba); + void decodeGPSInfo(const QByteArray ba); + void decodeGPSPos(const QByteArray ba); + + float getPressureFloat(const RS41Subframe *subframe); + QString getPressureString(const RS41Subframe *subframe); + float getTemperatureFloat(const RS41Subframe *subframe); + QString getTemperatureString(const RS41Subframe *subframe); + float getHumidityTemperatureFloat(const RS41Subframe *subframe); + float getHumidityFloat(const RS41Subframe *subframe); + QString getHumidityString(const RS41Subframe *subframe); + + static RS41Frame* decode(const QByteArray ba); + static int getFrameLength(int frameType); + +protected: + uint16_t getUInt16(const QByteArray ba, int offset) const; + uint32_t getUInt24(const QByteArray ba, int offset) const; + uint32_t getUInt32(const QByteArray ba, int offset) const; + + void calcPressure(const RS41Subframe *subframe); + void calcTemperature(const RS41Subframe *subframe); + void calcHumidityTemperature(const RS41Subframe *subframe); + void calcHumidity(const RS41Subframe *subframe); + + QByteArray m_bytes; + + float m_pressure; + QString m_pressureString; + bool m_pressureCalibrated; + float m_temperature; + QString m_temperatureString; + bool m_temperatureCalibrated; + float m_humidityTemperature; + bool m_humidityTemperatureCalibrated; + float m_humidity; + QString m_humidityString; + bool m_humidityCalibrated; + +}; + +// RS41 subframe holding calibration data collected from multiple RS51Frames +class SDRBASE_API RS41Subframe { +public: + + RS41Subframe(); + void update(RS41Frame *message); + bool hasTempCal() const; + bool getTempCal(float &r1, float &r2, float *poly, float *cal) const; + bool hasHumidityCal() const; + bool getHumidityCal(float &c1, float &c2, float *capCal, float *calMatrix) const; + bool hasHumidityTempCal() const; + bool getHumidityTempCal(float &r1, float &r2, float *poly, float *cal) const; + bool hasPressureCal() const; + bool getPressureCal(float *cal) const; + QString getType() const; + QString getFrequencyMHz() const; + QString getBurstKillStatus() const; + QString getBurstKillTimer() const; + +protected: + + bool m_subframeValid[51]; + QByteArray m_subframe; + + uint16_t getUInt16(int offset) const; + float getFloat(int offset) const; + +}; + +#endif // INCLUDE_RADIOSONDE_H diff --git a/sdrbase/util/reedsolomon.h b/sdrbase/util/reedsolomon.h new file mode 100644 index 000000000..7991d32bb --- /dev/null +++ b/sdrbase/util/reedsolomon.h @@ -0,0 +1,650 @@ +/* + * Reed-Solomon -- Reed-Solomon encoder / decoder library + * + * Copyright (c) 2014 Hard Consulting Corporation. + * Copyright (c) 2006 Phil Karn, KA9Q + * + * It may be used under the terms of the GNU Lesser General Public License (LGPL). + * + * Simplified version of https://github.com/pjkundert/ezpwd-reed-solomon which + * seems to be the fastest open-source decoder. + * + */ + +#ifndef REEDSOLOMON_H +#define REEDSOLOMON_H + +#include +#include +#include +#include +#include +#include + +// Preprocessor defines available: +// +// EZPWD_NO_MOD_TAB -- define to force no "modnn" Galois modulo table acceleration +// +//#define EZPWD_NO_MOD_TAB + +namespace ReedSolomon { + +// +// reed_solomon_base - Reed-Solomon codec generic base class +// +class reed_solomon_base { +public: + virtual size_t datum() const = 0; // a data element's bits + virtual size_t symbol() const = 0; // a symbol's bits + virtual int size() const = 0; // R-S block size (maximum total symbols) + virtual int nroots() const = 0; // R-S roots (parity symbols) + virtual int load() const = 0; // R-S net payload (data symbols) + + virtual ~reed_solomon_base() {} + + reed_solomon_base() {} + + // + // {en,de}code -- Compute/Correct errors/erasures in a Reed-Solomon encoded container + // + /// For decode, optionally specify some known erasure positions (up to nroots()). If + /// non-empty 'erasures' is provided, it contains the positions of each erasure. If a + /// non-zero pointer to a 'position' vector is provided, its capacity will be increased to + /// be capable of storing up to 'nroots()' ints; the actual deduced error locations will be + /// returned. + /// + /// RETURN VALUE + /// + /// Return -1 on error. The encode returns the number of parity symbols produced; + /// decode returns the number of symbols corrected. Both errors and erasures are included, + /// so long as they are actually different than the deduced value. In other words, if a + /// symbol is marked as an erasure but it actually turns out to be correct, it's index will + /// NOT be included in the returned count, nor the modified erasure vector! + /// + + virtual int encode(const uint8_t *data, int len, uint8_t *parity) const = 0; + + virtual int decode1(uint8_t *data, int len, uint8_t *parity, + const std::vector &erasure = std::vector(), std::vector *position = 0) const = 0; + + int decode(uint8_t *data, + int len, + int pad = 0, // ignore 'pad' symbols at start of array + const std::vector &erasure = std::vector(), + std::vector *position = 0) const + { + return decode1((uint8_t*)(data + pad), len, (uint8_t*)(data + len), erasure, position); + } + +}; + +// +// gfpoly - default field polynomial generator functor. +// +template +struct gfpoly { + int operator()(int sr) const + { + if (sr == 0) { + sr = 1; + } else { + sr <<= 1; + if (sr & (1 << 8)) + sr ^= PLY; + sr &= ((1 << 8) - 1); + } + return sr; + } +}; + +// +// class reed_solomon_tabs -- R-S tables common to all RS(NN,*) with same SYM, PRM and PLY +// +template +class reed_solomon_tabs : public reed_solomon_base { +public: + typedef uint8_t symbol_t; + static const size_t DATUM = 8; // bits + static const size_t SYMBOL = 8; // bits / symbol + static const int MM = 8; + static const int SIZE = (1 << 8) - 1; // maximum symbols in field + static const int NN = SIZE; + static const int A0 = SIZE; + static const int MODS // modulo table: 1/2 the symbol size squared, up to 4k +#if defined(EZPWD_NO_MOD_TAB) + = 0; +#else + = 8 > 8 ? (1 << 12) : (1 << 8 << 8 / 2); +#endif + + static int iprim; // initialized to -1, below + +protected: + static std::array alpha_to; + static std::array index_of; + static std::array mod_of; + virtual ~reed_solomon_tabs() {} + + reed_solomon_tabs() : reed_solomon_base() + { + // Do init if not already done. We check one value which is initialized to -1; this is + // safe, 'cause the value will not be set 'til the initializing thread has completely + // initialized the structure. Worst case scenario: multiple threads will initialize + // identically. No mutex necessary. + if (iprim >= 0) + return; + + // Generate Galois field lookup tables + index_of[0] = A0; // log(zero) = -inf + alpha_to[A0] = 0; // alpha**-inf = 0 + PLY poly; + int sr = poly(0); + for (int i = 0; i < NN; i++) { + index_of[sr] = i; + alpha_to[i] = sr; + sr = poly(sr); + } + // If it's not primitive, raise exception or abort + if (sr != alpha_to[0]) { + abort(); + } + + // Generate modulo table for some commonly used (non-trivial) values + for (int x = NN; x < NN + MODS; ++x) + mod_of[x - NN] = _modnn(x); + // Find prim-th root of 1, index form, used in decoding. + int iptmp = 1; + while (iptmp % PRM != 0) + iptmp += NN; + iprim = iptmp / PRM; + } + + // + // modnn -- modulo replacement for galois field arithmetics, optionally w/ table acceleration + // + // @x: the value to reduce (will never be -'ve) + // + // where + // MM = number of bits per symbol + // NN = (2^MM) - 1 + // + // Simple arithmetic modulo would return a wrong result for values >= 3 * NN + // + uint8_t _modnn(int x) const + { + while (x >= NN) { + x -= NN; + x = (x >> MM) + (x & NN); + } + return x; + } + + uint8_t modnn(int x) const + { + while (x >= NN + MODS) { + x -= NN; + x = (x >> MM) + (x & NN); + } + if (MODS && x >= NN) + x = mod_of[x - NN]; + return x; + } +}; + +// +// class reed_solomon - Reed-Solomon codec +// +// @TYP: A symbol datum; {en,de}code operates on arrays of these +// @DATUM: Bits per datum (a TYP()) +// @SYM{BOL}, MM: Bits per symbol +// @NN: Symbols per block (== (1< instances with the same template type parameters share a common +// (static) set of alpha_to, index_of and genpoly tables. The first instance to be constructed +// initializes the tables. +// +// Each specialized type of reed_solomon implements a specific encode/decode method +// appropriate to its datum 'TYP'. When accessed via a generic reed_solomon_base pointer, only +// access via "safe" (size specifying) containers or iterators is available. +// +template +class reed_solomon : public reed_solomon_tabs { +public: + typedef reed_solomon_tabs tabs_t; + using tabs_t::A0; + using tabs_t::DATUM; + using tabs_t::MM; + using tabs_t::NN; + using tabs_t::SIZE; + using tabs_t::SYMBOL; + + using tabs_t::iprim; + + using tabs_t::alpha_to; + using tabs_t::index_of; + + using tabs_t::modnn; + + static const int NROOTS = RTS; + static const int LOAD = SIZE - NROOTS; // maximum non-parity symbol payload + +protected: + static std::array genpoly; + +public: + virtual size_t datum() const { return DATUM; } + + virtual size_t symbol() const { return SYMBOL; } + + virtual int size() const { return SIZE; } + + virtual int nroots() const { return NROOTS; } + + virtual int load() const { return LOAD; } + + using reed_solomon_base::decode; + virtual int decode1(uint8_t *data, int len, uint8_t *parity, + const std::vector &erasure = std::vector(), std::vector *position = 0) const + { + return decode_mask(data, len, parity, erasure, position); + } + + // + // decode_mask -- mask INP data into valid SYMBOL data + // + /// Incoming data may be in a variety of sizes, and may contain information beyond the + /// R-S symbol capacity. For example, we might use a 6-bit R-S symbol to correct the lower + /// 6 bits of an 8-bit data character. This would allow us to correct common substitution + /// errors (such as '2' for '3', 'R' for 'T', 'n' for 'm'). + /// + int decode_mask(uint8_t *data, int len, + uint8_t *parity = 0, // either 0, or pointer to all parity symbols + const std::vector &erasure = std::vector(), std::vector *position = 0) const + { + if (!parity) { + len -= NROOTS; + parity = data + len; + } + + std::array tmp; + uint8_t msk = static_cast(~0UL << SYMBOL); + + + int corrects; + if (!erasure.size() && !position) { + // No erasures, and error position info not wanted. + corrects = decode(data, len, parity); + } else { + // Either erasure location info specified, or resultant error position info wanted; + // Prepare pos (a temporary, if no position vector provided), and copy any provided + // erasure positions. After number of corrections is known, resize the position + // vector. Thus, we use any supplied erasure info, and optionally return any + // correction position info separately. + std::vector _pos; + std::vector &pos = position ? *position : _pos; + pos.resize(std::max(size_t(NROOTS), erasure.size())); + std::copy(erasure.begin(), erasure.end(), pos.begin()); + corrects = decode(data, len, parity, &pos.front(), erasure.size()); + if (corrects > int(pos.size())) { + return -1; + } + pos.resize(std::max(0, corrects)); + } + + return corrects; + } + + virtual ~reed_solomon() + { + } + + reed_solomon() : reed_solomon_tabs() + { + // We check one element of the array; this is safe, 'cause the value will not be + // initialized 'til the initializing thread has completely initialized the array. Worst + // case scenario: multiple threads will initialize identically. No mutex necessary. + if (genpoly[0]) + return; + + std::array tmppoly; // uninitialized + // Form RS code generator polynomial from its roots. Only lower-index entries are + // consulted, when computing subsequent entries; only index 0 needs initialization. + tmppoly[0] = 1; + for (int i = 0, root = FCR * PRM; i < NROOTS; i++, root += PRM) { + tmppoly[i + 1] = 1; + // Multiply tmppoly[] by @**(root + x) + for (int j = i; j > 0; j--) { + if (tmppoly[j] != 0) + tmppoly[j] = tmppoly[j - 1] ^ alpha_to[modnn(index_of[tmppoly[j]] + root)]; + else + tmppoly[j] = tmppoly[j - 1]; + } + // tmppoly[0] can never be zero + tmppoly[0] = alpha_to[modnn(index_of[tmppoly[0]] + root)]; + } + // convert NROOTS entries of tmppoly[] to genpoly[] in index form for quicker encoding, + // in reverse order so genpoly[0] is last element initialized. + for (int i = NROOTS; i >= 0; --i) + genpoly[i] = index_of[tmppoly[i]]; + } + + virtual int encode(const uint8_t *data, int len, uint8_t *parity) // at least nroots + const + { + // Check length parameter for validity + int pad = NN - NROOTS - len; + for (int i = 0; i < NROOTS; i++) parity[i] = 0; + for (int i = 0; i < len; i++) { + uint8_t feedback = index_of[data[i] ^ parity[0]]; + if (feedback != A0) { + for (int j = 1; j < NROOTS; j++) + parity[j] ^= alpha_to[modnn(feedback + genpoly[NROOTS - j])]; + } + + std::rotate(parity, parity + 1, parity + NROOTS); + if (feedback != A0) + parity[NROOTS - 1] = alpha_to[modnn(feedback + genpoly[0])]; + else + parity[NROOTS - 1] = 0; + } + return NROOTS; + } + + int decode(uint8_t *data, int len, + uint8_t *parity, // Requires: at least NROOTS + int *eras_pos = 0, // Capacity: at least NROOTS + int no_eras = 0, // Maximum: at most NROOTS + uint8_t *corr = 0) // Capacity: at least NROOTS + const + { + typedef std::array typ_nroots; + typedef std::array typ_nroots_1; + typedef std::array int_nroots; + + typ_nroots_1 lambda{{0}}; + typ_nroots syn; + typ_nroots_1 b; + typ_nroots_1 t; + typ_nroots_1 omega; + int_nroots root; + typ_nroots_1 reg; + int_nroots loc; + int count = 0; + + // Check length parameter and erasures for validity + int pad = NN - NROOTS - len; + if (no_eras) { + if (no_eras > NROOTS) { + return -1; + } + for (int i = 0; i < no_eras; ++i) { + if (eras_pos[i] < 0 || eras_pos[i] >= len + NROOTS) { + return -1; + } + } + } + + // form the syndromes; i.e., evaluate data(x) at roots of g(x) + for (int i = 0; i < NROOTS; i++) + syn[i] = data[0]; + + for (int j = 1; j < len; j++) { + for (int i = 0; i < NROOTS; i++) { + if (syn[i] == 0) { + syn[i] = data[j]; + } else { + syn[i] = data[j] ^ alpha_to[modnn(index_of[syn[i]] + (FCR + i) * PRM)]; + } + } + } + + for (int j = 0; j < NROOTS; j++) { + for (int i = 0; i < NROOTS; i++) { + if (syn[i] == 0) { + syn[i] = parity[j]; + } else { + syn[i] = parity[j] ^ alpha_to[modnn(index_of[syn[i]] + (FCR + i) * PRM)]; + } + } + } + + // Convert syndromes to index form, checking for nonzero condition + uint8_t syn_error = 0; + for (int i = 0; i < NROOTS; i++) { + syn_error |= syn[i]; + syn[i] = index_of[syn[i]]; + } + + int deg_lambda = 0; + int deg_omega = 0; + int r = no_eras; + int el = no_eras; + if (!syn_error) { + // if syndrome is zero, data[] is a codeword and there are no errors to correct. + count = 0; + goto finish; + } + + lambda[0] = 1; + if (no_eras > 0) { + // Init lambda to be the erasure locator polynomial. Convert erasure positions + // from index into data, to index into Reed-Solomon block. + lambda[1] = alpha_to[modnn(PRM * (NN - 1 - (eras_pos[0] + pad)))]; + for (int i = 1; i < no_eras; i++) { + uint8_t u = modnn(PRM * (NN - 1 - (eras_pos[i] + pad))); + for (int j = i + 1; j > 0; j--) { + uint8_t tmp = index_of[lambda[j - 1]]; + if (tmp != A0) { + lambda[j] ^= alpha_to[modnn(u + tmp)]; + } + } + } + } + + for (int i = 0; i < NROOTS + 1; i++) + b[i] = index_of[lambda[i]]; + + // + // Begin Berlekamp-Massey algorithm to determine error+erasure locator polynomial + // + while (++r <= NROOTS) { // r is the step number + // Compute discrepancy at the r-th step in poly-form + uint8_t discr_r = 0; + for (int i = 0; i < r; i++) { + if ((lambda[i] != 0) && (syn[r - i - 1] != A0)) { + discr_r ^= alpha_to[modnn(index_of[lambda[i]] + syn[r - i - 1])]; + } + } + discr_r = index_of[discr_r]; // Index form + if (discr_r == A0) { + // 2 lines below: B(x) <-- x*B(x) + // Rotate the last element of b[NROOTS+1] to b[0] + std::rotate(b.begin(), b.begin() + NROOTS, b.end()); + b[0] = A0; + } else { + // 7 lines below: T(x) <-- lambda(x)-discr_r*x*b(x) + t[0] = lambda[0]; + for (int i = 0; i < NROOTS; i++) { + if (b[i] != A0) { + t[i + 1] = lambda[i + 1] ^ alpha_to[modnn(discr_r + b[i])]; + } else + t[i + 1] = lambda[i + 1]; + } + if (2 * el <= r + no_eras - 1) { + el = r + no_eras - el; + // 2 lines below: B(x) <-- inv(discr_r) * lambda(x) + for (int i = 0; i <= NROOTS; i++) { + b[i] = ((lambda[i] == 0) ? A0 : modnn(index_of[lambda[i]] - discr_r + NN)); + } + } else { + // 2 lines below: B(x) <-- x*B(x) + std::rotate(b.begin(), b.begin() + NROOTS, b.end()); + b[0] = A0; + } + lambda = t; + } + } + + // Convert lambda to index form and compute deg(lambda(x)) + for (int i = 0; i < NROOTS + 1; i++) { + lambda[i] = index_of[lambda[i]]; + if (lambda[i] != NN) + deg_lambda = i; + } + // Find roots of error+erasure locator polynomial by Chien search + reg = lambda; + count = 0; // Number of roots of lambda(x) + for (int i = 1, k = iprim - 1; i <= NN; i++, k = modnn(k + iprim)) { + uint8_t q = 1; // lambda[0] is always 0 + for (int j = deg_lambda; j > 0; j--) { + if (reg[j] != A0) { + reg[j] = modnn(reg[j] + j); + q ^= alpha_to[reg[j]]; + } + } + if (q != 0) + continue; // Not a root + // store root (index-form) and error location number + root[count] = i; + loc[count] = k; + // If we've already found max possible roots, abort the search to save time + if (++count == deg_lambda) + break; + } + if (deg_lambda != count) { + // deg(lambda) unequal to number of roots => uncorrectable error detected + count = -1; + goto finish; + } + // + // Compute err+eras evaluator poly omega(x) = s(x)*lambda(x) (modulo x**NROOTS). in + // index form. Also find deg(omega). + // + deg_omega = deg_lambda - 1; + for (int i = 0; i <= deg_omega; i++) { + uint8_t tmp = 0; + for (int j = i; j >= 0; j--) { + if ((syn[i - j] != A0) && (lambda[j] != A0)) + tmp ^= alpha_to[modnn(syn[i - j] + lambda[j])]; + } + omega[i] = index_of[tmp]; + } + + // + // Compute error values in poly-form. num1 = omega(inv(X(l))), num2 = inv(X(l))**(fcr-1) + // and den = lambda_pr(inv(X(l))) all in poly-form + // + for (int j = count - 1; j >= 0; j--) { + uint8_t num1 = 0; + for (int i = deg_omega; i >= 0; i--) { + if (omega[i] != A0) + num1 ^= alpha_to[modnn(omega[i] + i * root[j])]; + } + uint8_t num2 = alpha_to[modnn(root[j] * (FCR - 1) + NN)]; + uint8_t den = 0; + + // lambda[i+1] for i even is the formal derivative lambda_pr of lambda[i] + for (int i = std::min(deg_lambda, NROOTS - 1) & ~1; i >= 0; i -= 2) { + if (lambda[i + 1] != A0) { + den ^= alpha_to[modnn(lambda[i + 1] + i * root[j])]; + } + } + // Apply error to data. Padding ('pad' unused symbols) begin at index 0. + if (num1 != 0) { + if (loc[j] < pad) { + // If the computed error position is in the 'pad' (the unused portion of the + // R-S data capacity), then our solution has failed -- we've computed a + // correction location outside of the data and parity we've been provided! + count = -1; + goto finish; + } + + uint8_t cor = alpha_to[modnn(index_of[num1] + index_of[num2] + NN - index_of[den])]; + // Store the error correction pattern, if a correction buffer is available + if (corr) + corr[j] = cor; + // If a data/parity buffer is given and the error is inside the message or + // parity data, correct it + if (loc[j] < (NN - NROOTS)) { + if (data) { + data[loc[j] - pad] ^= cor; + } + } else if (loc[j] < NN) { + if (parity) + parity[loc[j] - (NN - NROOTS)] ^= cor; + } + } + } + + finish: + if (eras_pos != NULL) { + for (int i = 0; i < count; i++) + eras_pos[i] = loc[i] - pad; + } + return count; + } +}; + +// +// Define the static reed_solomon...<...> members; allowed in header for template types. +// +// The reed_solomon_tags<...>::iprim < 0 is used to indicate to the first instance that the +// static tables require initialization. +// +template +int reed_solomon_tabs::iprim = -1; + +template +std::array::NN + 1> reed_solomon_tabs::alpha_to; + +template +std::array::NN + 1> reed_solomon_tabs::index_of; +template +std::array::MODS> reed_solomon_tabs::mod_of; + +template +std::array::NROOTS + 1> reed_solomon::genpoly; + +// +// RS( ... ) -- Define a reed-solomon codec +// +// @SYMBOLS: Total number of symbols; must be a power of 2 minus 1, eg 2^8-1 == 255 +// @PAYLOAD: The maximum number of non-parity symbols, eg 253 ==> 2 parity symbols +// @POLY: A primitive polynomial appropriate to the SYMBOLS size +// @FCR: The first consecutive root of the Reed-Solomon generator polynomial +// @PRIM: The primitive root of the generator polynomial +// + +// +// RS -- Standard partial specializations for Reed-Solomon codec type access +// +// Normally, Reed-Solomon codecs are described with terms like RS(255,252). Obtain various +// standard Reed-Solomon codecs using macros of a similar form, eg. RS<255, 252>. Standard PLY, +// FCR and PRM values are provided for various SYMBOL sizes, along with appropriate basic types +// capable of holding all internal Reed-Solomon tabular data. +// +// In order to provide "default initialization" of const RS<...> types, a user-provided +// default constructor must be provided. +// +template +struct RS; +template +struct RS<255, PAYLOAD> : public ReedSolomon::reed_solomon<(255) - (PAYLOAD), 0, 1, ReedSolomon::gfpoly<0x11d>> +{ + RS() + : ReedSolomon::reed_solomon<(255) - (PAYLOAD), 0, 1, ReedSolomon::gfpoly<0x11d>>() + { + } +}; + +} // namespace ReedSolomon + +#endif // REEDSOLOMON_H diff --git a/sdrbase/webapi/webapirequestmapper.cpp b/sdrbase/webapi/webapirequestmapper.cpp index 0f5034bbe..516b455c2 100644 --- a/sdrbase/webapi/webapirequestmapper.cpp +++ b/sdrbase/webapi/webapirequestmapper.cpp @@ -4317,6 +4317,11 @@ bool WebAPIRequestMapper::getChannelSettings( channelSettings->setRadioClockSettings(new SWGSDRangel::SWGRadioClockSettings()); channelSettings->getRadioClockSettings()->fromJsonObject(settingsJsonObject); } + else if (channelSettingsKey == "RadiosondeDemodSettings") + { + channelSettings->setRadiosondeDemodSettings(new SWGSDRangel::SWGRadiosondeDemodSettings()); + channelSettings->getRadiosondeDemodSettings()->fromJsonObject(settingsJsonObject); + } else if (channelSettingsKey == "RemoteSinkSettings") { channelSettings->setRemoteSinkSettings(new SWGSDRangel::SWGRemoteSinkSettings()); @@ -4840,6 +4845,11 @@ bool WebAPIRequestMapper::getFeatureSettings( featureSettings->setStarTrackerSettings(new SWGSDRangel::SWGStarTrackerSettings()); featureSettings->getStarTrackerSettings()->fromJsonObject(settingsJsonObject); } + else if (featureSettingsKey == "RadiosondeSettings") + { + featureSettings->setRadiosondeSettings(new SWGSDRangel::SWGRadiosondeSettings()); + featureSettings->getRadiosondeSettings()->fromJsonObject(settingsJsonObject); + } else if (featureSettingsKey == "RigCtlServerSettings") { featureSettings->setRigCtlServerSettings(new SWGSDRangel::SWGRigCtlServerSettings()); @@ -5060,6 +5070,7 @@ void WebAPIRequestMapper::resetChannelSettings(SWGSDRangel::SWGChannelSettings& channelSettings.setPagerDemodSettings(nullptr); channelSettings.setRadioAstronomySettings(nullptr); channelSettings.setRadioClockSettings(nullptr); + channelSettings.setRadiosondeDemodSettings(nullptr); channelSettings.setRemoteSinkSettings(nullptr); channelSettings.setRemoteSourceSettings(nullptr); channelSettings.setSsbDemodSettings(nullptr); @@ -5091,6 +5102,7 @@ void WebAPIRequestMapper::resetChannelReport(SWGSDRangel::SWGChannelReport& chan channelReport.setPacketModReport(nullptr); channelReport.setRadioAstronomyReport(nullptr); channelReport.setRadioClockReport(nullptr); + channelReport.setRadiosondeDemodReport(nullptr); channelReport.setRemoteSourceReport(nullptr); channelReport.setSsbDemodReport(nullptr); channelReport.setSsbModReport(nullptr); @@ -5130,6 +5142,7 @@ void WebAPIRequestMapper::resetFeatureSettings(SWGSDRangel::SWGFeatureSettings& { featureSettings.cleanup(); featureSettings.setFeatureType(nullptr); + featureSettings.setAisSettings(nullptr); featureSettings.setAntennaToolsSettings(nullptr); featureSettings.setAprsSettings(nullptr); featureSettings.setGs232ControllerSettings(nullptr); @@ -5138,6 +5151,7 @@ void WebAPIRequestMapper::resetFeatureSettings(SWGSDRangel::SWGFeatureSettings& featureSettings.setSatelliteTrackerSettings(nullptr); featureSettings.setSimplePttSettings(nullptr); featureSettings.setStarTrackerSettings(nullptr); + featureSettings.setRadiosondeSettings(nullptr); featureSettings.setRigCtlServerSettings(nullptr); } diff --git a/sdrbase/webapi/webapiutils.cpp b/sdrbase/webapi/webapiutils.cpp index b01330bac..8a11a53d0 100644 --- a/sdrbase/webapi/webapiutils.cpp +++ b/sdrbase/webapi/webapiutils.cpp @@ -58,6 +58,7 @@ const QMap WebAPIUtils::m_channelURIToSettingsKey = { {"sdrangel.channeltx.modpacket", "PacketModSettings"}, {"sdrangel.channeltx.mod802.15.4", "IEEE_802_15_4_ModSettings"}, {"sdrangel.channel.radioclock", "RadioClockSettings"}, + {"sdrangel.channel.radiosondedemod", "RadiosondeDemodSettings"}, {"sdrangel.demod.remotesink", "RemoteSinkSettings"}, {"sdrangel.channeltx.remotesource", "RemoteSourceSettings"}, {"sdrangel.channeltx.modssb", "SSBModSettings"}, @@ -159,6 +160,7 @@ const QMap WebAPIUtils::m_channelTypeToSettingsKey = { {"LocalSource", "LocalSourceSettings"}, {"RadioAstronomy", "RadioAstronomySettings"}, {"RadioClock", "RadioClockSettings"}, + {"RadiosondeDemod", "RadiosondeDemodSettings"}, {"RemoteSink", "RemoteSinkSettings"}, {"RemoteSource", "RemoteSourceSettings"}, {"SSBMod", "SSBModSettings"}, @@ -273,6 +275,7 @@ const QMap WebAPIUtils::m_featureTypeToSettingsKey = { {"GS232Controller", "GS232ControllerSettings"}, // a.k.a Rotator Controller {"Map", "MapSettings"}, {"PERTester", "PERTesterSettings"}, + {"Radiosonde", "RadiosondeSettings"}, {"RigCtlServer", "RigCtlServerSettings"}, {"SatelliteTracker", "SatelliteTrackerSettings"}, {"SimplePTT", "SimplePTTSettings"}, @@ -302,6 +305,7 @@ const QMap WebAPIUtils::m_featureURIToSettingsKey = { {"sdrangel.feature.gs232controller", "GS232ControllerSettings"}, {"sdrangel.feature.map", "MapSettings"}, {"sdrangel.feature.pertester", "PERTesterSettings"}, + {"sdrangel.feature.radiosonde", "RadiosondeSettings"}, {"sdrangel.feature.rigctlserver", "RigCtlServerSettings"}, {"sdrangel.feature.satellitetracker", "SatelliteTrackerSettings"}, {"sdrangel.feature.simpleptt", "SimplePTTSettings"}, diff --git a/sdrgui/CMakeLists.txt b/sdrgui/CMakeLists.txt index 5c9a47db9..261c7ec99 100644 --- a/sdrgui/CMakeLists.txt +++ b/sdrgui/CMakeLists.txt @@ -25,6 +25,8 @@ set(sdrgui_SOURCES gui/crightclickenabler.cpp gui/customtextedit.cpp gui/cwkeyergui.cpp + gui/datetimedelegate.cpp + gui/decimaldelegate.cpp gui/devicestreamselectiondialog.cpp gui/deviceuserargsdialog.cpp gui/dmsspinbox.cpp @@ -62,6 +64,7 @@ set(sdrgui_SOURCES gui/spectrumcalibrationpointsdialog.cpp gui/spectrummarkersdialog.cpp gui/tickedslider.cpp + gui/timedelegate.cpp gui/transverterbutton.cpp gui/transverterdialog.cpp gui/tvscreen.cpp @@ -113,6 +116,8 @@ set(sdrgui_HEADERS gui/crightclickenabler.h gui/customtextedit.h gui/cwkeyergui.h + gui/datetimedelegate.h + gui/decimaldelegate.h gui/devicestreamselectiondialog.h gui/deviceuserargsdialog.h gui/dmsspinbox.h @@ -152,6 +157,7 @@ set(sdrgui_HEADERS gui/spectrumcalibrationpointsdialog.h gui/spectrummarkersdialog.h gui/tickedslider.h + gui/timedelegate.h gui/transverterbutton.h gui/transverterdialog.h gui/tvscreen.h diff --git a/sdrgui/gui/datetimedelegate.cpp b/sdrgui/gui/datetimedelegate.cpp new file mode 100644 index 000000000..4beac8105 --- /dev/null +++ b/sdrgui/gui/datetimedelegate.cpp @@ -0,0 +1,36 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "datetimedelegate.h" + +DateTimeDelegate::DateTimeDelegate(QString format) : + m_format(format) +{ +} + +QString DateTimeDelegate::displayText(const QVariant &value, const QLocale &locale) const +{ + (void) locale; + if (value.toString() == "") { + return ""; + } else { + return value.toDateTime().toString(m_format); + } +} + diff --git a/sdrgui/gui/datetimedelegate.h b/sdrgui/gui/datetimedelegate.h new file mode 100644 index 000000000..c73cadf7f --- /dev/null +++ b/sdrgui/gui/datetimedelegate.h @@ -0,0 +1,37 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef SDRGUI_GUI_DATETIMEDELGATE_H +#define SDRGUI_GUI_DATETIMEDELGATE_H + +#include + +#include "export.h" + +// Delegate for table to display time +class SDRGUI_API DateTimeDelegate : public QStyledItemDelegate { + +public: + DateTimeDelegate(QString format = "yyyy/MM/dd hh:mm:ss"); + virtual QString displayText(const QVariant &value, const QLocale &locale) const override; + +private: + QString m_format; + +}; + +#endif // SDRGUI_GUI_DATETIMEDELGATE_H diff --git a/sdrgui/gui/decimaldelegate.cpp b/sdrgui/gui/decimaldelegate.cpp new file mode 100644 index 000000000..d76e0688b --- /dev/null +++ b/sdrgui/gui/decimaldelegate.cpp @@ -0,0 +1,35 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "decimaldelegate.h" + +DecimalDelegate::DecimalDelegate(int precision) : + m_precision(precision) +{ +} + +QString DecimalDelegate::displayText(const QVariant &value, const QLocale &locale) const +{ + (void) locale; + bool ok; + double d = value.toDouble(&ok); + if (ok) { + return QString::number(d, 'f', m_precision); + } else { + return value.toString(); + } +} diff --git a/sdrgui/gui/decimaldelegate.h b/sdrgui/gui/decimaldelegate.h new file mode 100644 index 000000000..25754c1ef --- /dev/null +++ b/sdrgui/gui/decimaldelegate.h @@ -0,0 +1,38 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef SDRGUI_GUI_DECIMALDELGATE_H +#define SDRGUI_GUI_DECIMALDELGATE_H + +#include + +#include "export.h" + +// Deligate for table to control precision used to display floating point values - also supports strings +class SDRGUI_API DecimalDelegate : public QStyledItemDelegate { + +public: + DecimalDelegate(int precision = 2); + + virtual QString displayText(const QVariant &value, const QLocale &locale) const override; + +private: + int m_precision; + +}; + +#endif // SDRGUI_GUI_DECIMALDELGATE_H diff --git a/sdrgui/gui/timedelegate.cpp b/sdrgui/gui/timedelegate.cpp new file mode 100644 index 000000000..7883f2919 --- /dev/null +++ b/sdrgui/gui/timedelegate.cpp @@ -0,0 +1,36 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "timedelegate.h" + +TimeDelegate::TimeDelegate(QString format) : + m_format(format) +{ +} + +QString TimeDelegate::displayText(const QVariant &value, const QLocale &locale) const +{ + (void) locale; + if (value.toString() == "") { + return ""; + } else { + return value.toTime().toString(m_format); + } +} + diff --git a/sdrgui/gui/timedelegate.h b/sdrgui/gui/timedelegate.h new file mode 100644 index 000000000..6f8a5f004 --- /dev/null +++ b/sdrgui/gui/timedelegate.h @@ -0,0 +1,37 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef SDRGUI_GUI_TIMEDELGATE_H +#define SDRGUI_GUI_TIMEDELGATE_H + +#include + +#include "export.h" + +// Delegate for table to display time +class SDRGUI_API TimeDelegate : public QStyledItemDelegate { + +public: + TimeDelegate(QString format = "hh:mm:ss"); + virtual QString displayText(const QVariant &value, const QLocale &locale) const override; + +private: + QString m_format; + +}; + +#endif // SDRGUI_GUI_DECIMALDELGATE_H diff --git a/swagger/sdrangel/api/swagger/include/ChannelReport.yaml b/swagger/sdrangel/api/swagger/include/ChannelReport.yaml index 7948cda1d..d3650cede 100644 --- a/swagger/sdrangel/api/swagger/include/ChannelReport.yaml +++ b/swagger/sdrangel/api/swagger/include/ChannelReport.yaml @@ -59,6 +59,8 @@ ChannelReport: $ref: "http://swgserver:8081/api/swagger/include/RadioAstronomy.yaml#/RadioAstronomyReport" RadioClockReport: $ref: "http://swgserver:8081/api/swagger/include/RadioClock.yaml#/RadioClockReport" + RadiosondeDemodReport: + $ref: "http://swgserver:8081/api/swagger/include/RadiosondeDemod.yaml#/RadiosondeDemodReport" RemoteSourceReport: $ref: "http://swgserver:8081/api/swagger/include/RemoteSource.yaml#/RemoteSourceReport" PacketDemodReport: diff --git a/swagger/sdrangel/api/swagger/include/ChannelSettings.yaml b/swagger/sdrangel/api/swagger/include/ChannelSettings.yaml index d82a11a04..0ec7f3e1a 100644 --- a/swagger/sdrangel/api/swagger/include/ChannelSettings.yaml +++ b/swagger/sdrangel/api/swagger/include/ChannelSettings.yaml @@ -85,6 +85,8 @@ ChannelSettings: $ref: "http://swgserver:8081/api/swagger/include/RadioAstronomy.yaml#/RadioAstronomySettings" RadioClockSettings: $ref: "http://swgserver:8081/api/swagger/include/RadioClock.yaml#/RadioClockSettings" + RadiosondeDemodSettings: + $ref: "http://swgserver:8081/api/swagger/include/RadiosondeDemod.yaml#/RadiosondeDemodSettings" RemoteSinkSettings: $ref: "http://swgserver:8081/api/swagger/include/RemoteSink.yaml#/RemoteSinkSettings" RemoteSourceSettings: diff --git a/swagger/sdrangel/api/swagger/include/FeatureSettings.yaml b/swagger/sdrangel/api/swagger/include/FeatureSettings.yaml index 85c36496e..da6a8cef2 100644 --- a/swagger/sdrangel/api/swagger/include/FeatureSettings.yaml +++ b/swagger/sdrangel/api/swagger/include/FeatureSettings.yaml @@ -31,6 +31,8 @@ FeatureSettings: $ref: "http://swgserver:8081/api/swagger/include/Map.yaml#/MapSettings" PERTesterSettings: $ref: "http://swgserver:8081/api/swagger/include/PERTester.yaml#/PERTesterSettings" + RadiosondeSettings: + $ref: "http://swgserver:8081/api/swagger/include/Radiosonde.yaml#/RadiosondeSettings" RigCtlServerSettings: $ref: "http://swgserver:8081/api/swagger/include/RigCtlServer.yaml#/RigCtlServerSettings" SatelliteTrackerSettings: diff --git a/swagger/sdrangel/api/swagger/include/Radiosonde.yaml b/swagger/sdrangel/api/swagger/include/Radiosonde.yaml new file mode 100644 index 000000000..ee0b9777c --- /dev/null +++ b/swagger/sdrangel/api/swagger/include/Radiosonde.yaml @@ -0,0 +1,20 @@ +RadiosondeSettings: + description: "Radiosonde settings" + properties: + title: + type: string + rgbColor: + type: integer + useReverseAPI: + description: Synchronize with reverse API (1 for yes, 0 for no) + type: integer + reverseAPIAddress: + type: string + reverseAPIPort: + type: integer + reverseAPIFeatureSetIndex: + type: integer + reverseAPIFeatureIndex: + type: integer + rollupState: + $ref: "http://swgserver:8081/api/swagger/include/RollupState.yaml#/RollupState" diff --git a/swagger/sdrangel/api/swagger/include/RadiosondeDemod.yaml b/swagger/sdrangel/api/swagger/include/RadiosondeDemod.yaml new file mode 100644 index 000000000..f5853f2c6 --- /dev/null +++ b/swagger/sdrangel/api/swagger/include/RadiosondeDemod.yaml @@ -0,0 +1,65 @@ +RadiosondeDemodSettings: + description: RadiosondeDemod + properties: + baud: + type: integer + description: baud rate (nominal is 4800) + inputFrequencyOffset: + type: integer + format: int64 + rfBandwidth: + type: number + format: float + fmDeviation: + type: number + format: float + correlationThreshold: + type: number + format: float + udpEnabled: + description: "Whether to forward received messages to specified UDP port" + type: integer + udpAddress: + description: "UDP address to forward received messages to" + type: string + udpPort: + description: "UDP port to forward received messages to" + type: integer + logFilename: + type: string + logEnabled: + type: integer + rgbColor: + type: integer + title: + type: string + streamIndex: + description: MIMO channel. Not relevant when connected to SI (single Rx). + type: integer + useReverseAPI: + description: Synchronize with reverse API (1 for yes, 0 for no) + type: integer + reverseAPIAddress: + type: string + reverseAPIPort: + type: integer + reverseAPIDeviceIndex: + type: integer + reverseAPIChannelIndex: + type: integer + scopeConfig: + $ref: "http://swgserver:8081/api/swagger/include/GLScope.yaml#/GLScope" + channelMarker: + $ref: "http://swgserver:8081/api/swagger/include/ChannelMarker.yaml#/ChannelMarker" + rollupState: + $ref: "http://swgserver:8081/api/swagger/include/RollupState.yaml#/RollupState" + +RadiosondeDemodReport: + description: RadiosondeDemod + properties: + channelPowerDB: + description: power received in channel (dB) + type: number + format: float + channelSampleRate: + type: integer diff --git a/swagger/sdrangel/code/qt5/client/SWGChannelReport.cpp b/swagger/sdrangel/code/qt5/client/SWGChannelReport.cpp index 1445fa542..265179eff 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelReport.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGChannelReport.cpp @@ -80,6 +80,8 @@ SWGChannelReport::SWGChannelReport() { m_radio_astronomy_report_isSet = false; radio_clock_report = nullptr; m_radio_clock_report_isSet = false; + radiosonde_demod_report = nullptr; + m_radiosonde_demod_report_isSet = false; remote_source_report = nullptr; m_remote_source_report_isSet = false; packet_demod_report = nullptr; @@ -164,6 +166,8 @@ SWGChannelReport::init() { m_radio_astronomy_report_isSet = false; radio_clock_report = new SWGRadioClockReport(); m_radio_clock_report_isSet = false; + radiosonde_demod_report = new SWGRadiosondeDemodReport(); + m_radiosonde_demod_report_isSet = false; remote_source_report = new SWGRemoteSourceReport(); m_remote_source_report_isSet = false; packet_demod_report = new SWGPacketDemodReport(); @@ -268,6 +272,9 @@ SWGChannelReport::cleanup() { if(radio_clock_report != nullptr) { delete radio_clock_report; } + if(radiosonde_demod_report != nullptr) { + delete radiosonde_demod_report; + } if(remote_source_report != nullptr) { delete remote_source_report; } @@ -369,6 +376,8 @@ SWGChannelReport::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&radio_clock_report, pJson["RadioClockReport"], "SWGRadioClockReport", "SWGRadioClockReport"); + ::SWGSDRangel::setValue(&radiosonde_demod_report, pJson["RadiosondeDemodReport"], "SWGRadiosondeDemodReport", "SWGRadiosondeDemodReport"); + ::SWGSDRangel::setValue(&remote_source_report, pJson["RemoteSourceReport"], "SWGRemoteSourceReport", "SWGRemoteSourceReport"); ::SWGSDRangel::setValue(&packet_demod_report, pJson["PacketDemodReport"], "SWGPacketDemodReport", "SWGPacketDemodReport"); @@ -487,6 +496,9 @@ SWGChannelReport::asJsonObject() { if((radio_clock_report != nullptr) && (radio_clock_report->isSet())){ toJsonValue(QString("RadioClockReport"), radio_clock_report, obj, QString("SWGRadioClockReport")); } + if((radiosonde_demod_report != nullptr) && (radiosonde_demod_report->isSet())){ + toJsonValue(QString("RadiosondeDemodReport"), radiosonde_demod_report, obj, QString("SWGRadiosondeDemodReport")); + } if((remote_source_report != nullptr) && (remote_source_report->isSet())){ toJsonValue(QString("RemoteSourceReport"), remote_source_report, obj, QString("SWGRemoteSourceReport")); } @@ -787,6 +799,16 @@ SWGChannelReport::setRadioClockReport(SWGRadioClockReport* radio_clock_report) { this->m_radio_clock_report_isSet = true; } +SWGRadiosondeDemodReport* +SWGChannelReport::getRadiosondeDemodReport() { + return radiosonde_demod_report; +} +void +SWGChannelReport::setRadiosondeDemodReport(SWGRadiosondeDemodReport* radiosonde_demod_report) { + this->radiosonde_demod_report = radiosonde_demod_report; + this->m_radiosonde_demod_report_isSet = true; +} + SWGRemoteSourceReport* SWGChannelReport::getRemoteSourceReport() { return remote_source_report; @@ -990,6 +1012,9 @@ SWGChannelReport::isSet(){ if(radio_clock_report && radio_clock_report->isSet()){ isObjectUpdated = true; break; } + if(radiosonde_demod_report && radiosonde_demod_report->isSet()){ + isObjectUpdated = true; break; + } if(remote_source_report && remote_source_report->isSet()){ isObjectUpdated = true; break; } diff --git a/swagger/sdrangel/code/qt5/client/SWGChannelReport.h b/swagger/sdrangel/code/qt5/client/SWGChannelReport.h index e2f13d5dd..9f91fc4c1 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelReport.h +++ b/swagger/sdrangel/code/qt5/client/SWGChannelReport.h @@ -48,6 +48,7 @@ #include "SWGPagerDemodReport.h" #include "SWGRadioAstronomyReport.h" #include "SWGRadioClockReport.h" +#include "SWGRadiosondeDemodReport.h" #include "SWGRemoteSourceReport.h" #include "SWGSSBDemodReport.h" #include "SWGSSBModReport.h" @@ -156,6 +157,9 @@ public: SWGRadioClockReport* getRadioClockReport(); void setRadioClockReport(SWGRadioClockReport* radio_clock_report); + SWGRadiosondeDemodReport* getRadiosondeDemodReport(); + void setRadiosondeDemodReport(SWGRadiosondeDemodReport* radiosonde_demod_report); + SWGRemoteSourceReport* getRemoteSourceReport(); void setRemoteSourceReport(SWGRemoteSourceReport* remote_source_report); @@ -274,6 +278,9 @@ private: SWGRadioClockReport* radio_clock_report; bool m_radio_clock_report_isSet; + SWGRadiosondeDemodReport* radiosonde_demod_report; + bool m_radiosonde_demod_report_isSet; + SWGRemoteSourceReport* remote_source_report; bool m_remote_source_report_isSet; diff --git a/swagger/sdrangel/code/qt5/client/SWGChannelSettings.cpp b/swagger/sdrangel/code/qt5/client/SWGChannelSettings.cpp index ca2a88d60..56fa8a4d4 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelSettings.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGChannelSettings.cpp @@ -104,6 +104,8 @@ SWGChannelSettings::SWGChannelSettings() { m_radio_astronomy_settings_isSet = false; radio_clock_settings = nullptr; m_radio_clock_settings_isSet = false; + radiosonde_demod_settings = nullptr; + m_radiosonde_demod_settings_isSet = false; remote_sink_settings = nullptr; m_remote_sink_settings_isSet = false; remote_source_settings = nullptr; @@ -210,6 +212,8 @@ SWGChannelSettings::init() { m_radio_astronomy_settings_isSet = false; radio_clock_settings = new SWGRadioClockSettings(); m_radio_clock_settings_isSet = false; + radiosonde_demod_settings = new SWGRadiosondeDemodSettings(); + m_radiosonde_demod_settings_isSet = false; remote_sink_settings = new SWGRemoteSinkSettings(); m_remote_sink_settings_isSet = false; remote_source_settings = new SWGRemoteSourceSettings(); @@ -344,6 +348,9 @@ SWGChannelSettings::cleanup() { if(radio_clock_settings != nullptr) { delete radio_clock_settings; } + if(radiosonde_demod_settings != nullptr) { + delete radiosonde_demod_settings; + } if(remote_sink_settings != nullptr) { delete remote_sink_settings; } @@ -466,6 +473,8 @@ SWGChannelSettings::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&radio_clock_settings, pJson["RadioClockSettings"], "SWGRadioClockSettings", "SWGRadioClockSettings"); + ::SWGSDRangel::setValue(&radiosonde_demod_settings, pJson["RadiosondeDemodSettings"], "SWGRadiosondeDemodSettings", "SWGRadiosondeDemodSettings"); + ::SWGSDRangel::setValue(&remote_sink_settings, pJson["RemoteSinkSettings"], "SWGRemoteSinkSettings", "SWGRemoteSinkSettings"); ::SWGSDRangel::setValue(&remote_source_settings, pJson["RemoteSourceSettings"], "SWGRemoteSourceSettings", "SWGRemoteSourceSettings"); @@ -618,6 +627,9 @@ SWGChannelSettings::asJsonObject() { if((radio_clock_settings != nullptr) && (radio_clock_settings->isSet())){ toJsonValue(QString("RadioClockSettings"), radio_clock_settings, obj, QString("SWGRadioClockSettings")); } + if((radiosonde_demod_settings != nullptr) && (radiosonde_demod_settings->isSet())){ + toJsonValue(QString("RadiosondeDemodSettings"), radiosonde_demod_settings, obj, QString("SWGRadiosondeDemodSettings")); + } if((remote_sink_settings != nullptr) && (remote_sink_settings->isSet())){ toJsonValue(QString("RemoteSinkSettings"), remote_sink_settings, obj, QString("SWGRemoteSinkSettings")); } @@ -1035,6 +1047,16 @@ SWGChannelSettings::setRadioClockSettings(SWGRadioClockSettings* radio_clock_set this->m_radio_clock_settings_isSet = true; } +SWGRadiosondeDemodSettings* +SWGChannelSettings::getRadiosondeDemodSettings() { + return radiosonde_demod_settings; +} +void +SWGChannelSettings::setRadiosondeDemodSettings(SWGRadiosondeDemodSettings* radiosonde_demod_settings) { + this->radiosonde_demod_settings = radiosonde_demod_settings; + this->m_radiosonde_demod_settings_isSet = true; +} + SWGRemoteSinkSettings* SWGChannelSettings::getRemoteSinkSettings() { return remote_sink_settings; @@ -1264,6 +1286,9 @@ SWGChannelSettings::isSet(){ if(radio_clock_settings && radio_clock_settings->isSet()){ isObjectUpdated = true; break; } + if(radiosonde_demod_settings && radiosonde_demod_settings->isSet()){ + isObjectUpdated = true; break; + } if(remote_sink_settings && remote_sink_settings->isSet()){ isObjectUpdated = true; break; } diff --git a/swagger/sdrangel/code/qt5/client/SWGChannelSettings.h b/swagger/sdrangel/code/qt5/client/SWGChannelSettings.h index 074c83cb1..ea6549f45 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelSettings.h +++ b/swagger/sdrangel/code/qt5/client/SWGChannelSettings.h @@ -56,6 +56,7 @@ #include "SWGPagerDemodSettings.h" #include "SWGRadioAstronomySettings.h" #include "SWGRadioClockSettings.h" +#include "SWGRadiosondeDemodSettings.h" #include "SWGRemoteSinkSettings.h" #include "SWGRemoteSourceSettings.h" #include "SWGSSBDemodSettings.h" @@ -201,6 +202,9 @@ public: SWGRadioClockSettings* getRadioClockSettings(); void setRadioClockSettings(SWGRadioClockSettings* radio_clock_settings); + SWGRadiosondeDemodSettings* getRadiosondeDemodSettings(); + void setRadiosondeDemodSettings(SWGRadiosondeDemodSettings* radiosonde_demod_settings); + SWGRemoteSinkSettings* getRemoteSinkSettings(); void setRemoteSinkSettings(SWGRemoteSinkSettings* remote_sink_settings); @@ -352,6 +356,9 @@ private: SWGRadioClockSettings* radio_clock_settings; bool m_radio_clock_settings_isSet; + SWGRadiosondeDemodSettings* radiosonde_demod_settings; + bool m_radiosonde_demod_settings_isSet; + SWGRemoteSinkSettings* remote_sink_settings; bool m_remote_sink_settings_isSet; diff --git a/swagger/sdrangel/code/qt5/client/SWGFeatureSettings.cpp b/swagger/sdrangel/code/qt5/client/SWGFeatureSettings.cpp index 459be10e0..99e1f2835 100644 --- a/swagger/sdrangel/code/qt5/client/SWGFeatureSettings.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGFeatureSettings.cpp @@ -52,6 +52,8 @@ SWGFeatureSettings::SWGFeatureSettings() { m_map_settings_isSet = false; per_tester_settings = nullptr; m_per_tester_settings_isSet = false; + radiosonde_settings = nullptr; + m_radiosonde_settings_isSet = false; rig_ctl_server_settings = nullptr; m_rig_ctl_server_settings_isSet = false; satellite_tracker_settings = nullptr; @@ -94,6 +96,8 @@ SWGFeatureSettings::init() { m_map_settings_isSet = false; per_tester_settings = new SWGPERTesterSettings(); m_per_tester_settings_isSet = false; + radiosonde_settings = new SWGRadiosondeSettings(); + m_radiosonde_settings_isSet = false; rig_ctl_server_settings = new SWGRigCtlServerSettings(); m_rig_ctl_server_settings_isSet = false; satellite_tracker_settings = new SWGSatelliteTrackerSettings(); @@ -140,6 +144,9 @@ SWGFeatureSettings::cleanup() { if(per_tester_settings != nullptr) { delete per_tester_settings; } + if(radiosonde_settings != nullptr) { + delete radiosonde_settings; + } if(rig_ctl_server_settings != nullptr) { delete rig_ctl_server_settings; } @@ -192,6 +199,8 @@ SWGFeatureSettings::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&per_tester_settings, pJson["PERTesterSettings"], "SWGPERTesterSettings", "SWGPERTesterSettings"); + ::SWGSDRangel::setValue(&radiosonde_settings, pJson["RadiosondeSettings"], "SWGRadiosondeSettings", "SWGRadiosondeSettings"); + ::SWGSDRangel::setValue(&rig_ctl_server_settings, pJson["RigCtlServerSettings"], "SWGRigCtlServerSettings", "SWGRigCtlServerSettings"); ::SWGSDRangel::setValue(&satellite_tracker_settings, pJson["SatelliteTrackerSettings"], "SWGSatelliteTrackerSettings", "SWGSatelliteTrackerSettings"); @@ -254,6 +263,9 @@ SWGFeatureSettings::asJsonObject() { if((per_tester_settings != nullptr) && (per_tester_settings->isSet())){ toJsonValue(QString("PERTesterSettings"), per_tester_settings, obj, QString("SWGPERTesterSettings")); } + if((radiosonde_settings != nullptr) && (radiosonde_settings->isSet())){ + toJsonValue(QString("RadiosondeSettings"), radiosonde_settings, obj, QString("SWGRadiosondeSettings")); + } if((rig_ctl_server_settings != nullptr) && (rig_ctl_server_settings->isSet())){ toJsonValue(QString("RigCtlServerSettings"), rig_ctl_server_settings, obj, QString("SWGRigCtlServerSettings")); } @@ -393,6 +405,16 @@ SWGFeatureSettings::setPerTesterSettings(SWGPERTesterSettings* per_tester_settin this->m_per_tester_settings_isSet = true; } +SWGRadiosondeSettings* +SWGFeatureSettings::getRadiosondeSettings() { + return radiosonde_settings; +} +void +SWGFeatureSettings::setRadiosondeSettings(SWGRadiosondeSettings* radiosonde_settings) { + this->radiosonde_settings = radiosonde_settings; + this->m_radiosonde_settings_isSet = true; +} + SWGRigCtlServerSettings* SWGFeatureSettings::getRigCtlServerSettings() { return rig_ctl_server_settings; @@ -484,6 +506,9 @@ SWGFeatureSettings::isSet(){ if(per_tester_settings && per_tester_settings->isSet()){ isObjectUpdated = true; break; } + if(radiosonde_settings && radiosonde_settings->isSet()){ + isObjectUpdated = true; break; + } if(rig_ctl_server_settings && rig_ctl_server_settings->isSet()){ isObjectUpdated = true; break; } diff --git a/swagger/sdrangel/code/qt5/client/SWGFeatureSettings.h b/swagger/sdrangel/code/qt5/client/SWGFeatureSettings.h index 1918aef52..7f3bbec1b 100644 --- a/swagger/sdrangel/code/qt5/client/SWGFeatureSettings.h +++ b/swagger/sdrangel/code/qt5/client/SWGFeatureSettings.h @@ -31,6 +31,7 @@ #include "SWGJogdialControllerSettings.h" #include "SWGMapSettings.h" #include "SWGPERTesterSettings.h" +#include "SWGRadiosondeSettings.h" #include "SWGRigCtlServerSettings.h" #include "SWGSatelliteTrackerSettings.h" #include "SWGSimplePTTSettings.h" @@ -92,6 +93,9 @@ public: SWGPERTesterSettings* getPerTesterSettings(); void setPerTesterSettings(SWGPERTesterSettings* per_tester_settings); + SWGRadiosondeSettings* getRadiosondeSettings(); + void setRadiosondeSettings(SWGRadiosondeSettings* radiosonde_settings); + SWGRigCtlServerSettings* getRigCtlServerSettings(); void setRigCtlServerSettings(SWGRigCtlServerSettings* rig_ctl_server_settings); @@ -147,6 +151,9 @@ private: SWGPERTesterSettings* per_tester_settings; bool m_per_tester_settings_isSet; + SWGRadiosondeSettings* radiosonde_settings; + bool m_radiosonde_settings_isSet; + SWGRigCtlServerSettings* rig_ctl_server_settings; bool m_rig_ctl_server_settings_isSet; diff --git a/swagger/sdrangel/code/qt5/client/SWGModelFactory.h b/swagger/sdrangel/code/qt5/client/SWGModelFactory.h index 5043473cc..bf01101d3 100644 --- a/swagger/sdrangel/code/qt5/client/SWGModelFactory.h +++ b/swagger/sdrangel/code/qt5/client/SWGModelFactory.h @@ -221,6 +221,9 @@ #include "SWGRadioAstronomySettings.h" #include "SWGRadioClockReport.h" #include "SWGRadioClockSettings.h" +#include "SWGRadiosondeDemodReport.h" +#include "SWGRadiosondeDemodSettings.h" +#include "SWGRadiosondeSettings.h" #include "SWGRange.h" #include "SWGRangeFloat.h" #include "SWGRemoteInputReport.h" @@ -1355,6 +1358,21 @@ namespace SWGSDRangel { obj->init(); return obj; } + if(QString("SWGRadiosondeDemodReport").compare(type) == 0) { + SWGRadiosondeDemodReport *obj = new SWGRadiosondeDemodReport(); + obj->init(); + return obj; + } + if(QString("SWGRadiosondeDemodSettings").compare(type) == 0) { + SWGRadiosondeDemodSettings *obj = new SWGRadiosondeDemodSettings(); + obj->init(); + return obj; + } + if(QString("SWGRadiosondeSettings").compare(type) == 0) { + SWGRadiosondeSettings *obj = new SWGRadiosondeSettings(); + obj->init(); + return obj; + } if(QString("SWGRange").compare(type) == 0) { SWGRange *obj = new SWGRange(); obj->init(); diff --git a/swagger/sdrangel/code/qt5/client/SWGRadiosondeDemodReport.cpp b/swagger/sdrangel/code/qt5/client/SWGRadiosondeDemodReport.cpp new file mode 100644 index 000000000..68ff0be60 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGRadiosondeDemodReport.cpp @@ -0,0 +1,131 @@ +/** + * SDRangel + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- + * + * OpenAPI spec version: 6.0.0 + * Contact: f4exb06@gmail.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + + +#include "SWGRadiosondeDemodReport.h" + +#include "SWGHelpers.h" + +#include +#include +#include +#include + +namespace SWGSDRangel { + +SWGRadiosondeDemodReport::SWGRadiosondeDemodReport(QString* json) { + init(); + this->fromJson(*json); +} + +SWGRadiosondeDemodReport::SWGRadiosondeDemodReport() { + channel_power_db = 0.0f; + m_channel_power_db_isSet = false; + channel_sample_rate = 0; + m_channel_sample_rate_isSet = false; +} + +SWGRadiosondeDemodReport::~SWGRadiosondeDemodReport() { + this->cleanup(); +} + +void +SWGRadiosondeDemodReport::init() { + channel_power_db = 0.0f; + m_channel_power_db_isSet = false; + channel_sample_rate = 0; + m_channel_sample_rate_isSet = false; +} + +void +SWGRadiosondeDemodReport::cleanup() { + + +} + +SWGRadiosondeDemodReport* +SWGRadiosondeDemodReport::fromJson(QString &json) { + QByteArray array (json.toStdString().c_str()); + QJsonDocument doc = QJsonDocument::fromJson(array); + QJsonObject jsonObject = doc.object(); + this->fromJsonObject(jsonObject); + return this; +} + +void +SWGRadiosondeDemodReport::fromJsonObject(QJsonObject &pJson) { + ::SWGSDRangel::setValue(&channel_power_db, pJson["channelPowerDB"], "float", ""); + + ::SWGSDRangel::setValue(&channel_sample_rate, pJson["channelSampleRate"], "qint32", ""); + +} + +QString +SWGRadiosondeDemodReport::asJson () +{ + QJsonObject* obj = this->asJsonObject(); + + QJsonDocument doc(*obj); + QByteArray bytes = doc.toJson(); + delete obj; + return QString(bytes); +} + +QJsonObject* +SWGRadiosondeDemodReport::asJsonObject() { + QJsonObject* obj = new QJsonObject(); + if(m_channel_power_db_isSet){ + obj->insert("channelPowerDB", QJsonValue(channel_power_db)); + } + if(m_channel_sample_rate_isSet){ + obj->insert("channelSampleRate", QJsonValue(channel_sample_rate)); + } + + return obj; +} + +float +SWGRadiosondeDemodReport::getChannelPowerDb() { + return channel_power_db; +} +void +SWGRadiosondeDemodReport::setChannelPowerDb(float channel_power_db) { + this->channel_power_db = channel_power_db; + this->m_channel_power_db_isSet = true; +} + +qint32 +SWGRadiosondeDemodReport::getChannelSampleRate() { + return channel_sample_rate; +} +void +SWGRadiosondeDemodReport::setChannelSampleRate(qint32 channel_sample_rate) { + this->channel_sample_rate = channel_sample_rate; + this->m_channel_sample_rate_isSet = true; +} + + +bool +SWGRadiosondeDemodReport::isSet(){ + bool isObjectUpdated = false; + do{ + if(m_channel_power_db_isSet){ + isObjectUpdated = true; break; + } + if(m_channel_sample_rate_isSet){ + isObjectUpdated = true; break; + } + }while(false); + return isObjectUpdated; +} +} + diff --git a/swagger/sdrangel/code/qt5/client/SWGRadiosondeDemodReport.h b/swagger/sdrangel/code/qt5/client/SWGRadiosondeDemodReport.h new file mode 100644 index 000000000..511e88812 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGRadiosondeDemodReport.h @@ -0,0 +1,64 @@ +/** + * SDRangel + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- + * + * OpenAPI spec version: 6.0.0 + * Contact: f4exb06@gmail.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + +/* + * SWGRadiosondeDemodReport.h + * + * RadiosondeDemod + */ + +#ifndef SWGRadiosondeDemodReport_H_ +#define SWGRadiosondeDemodReport_H_ + +#include + + + +#include "SWGObject.h" +#include "export.h" + +namespace SWGSDRangel { + +class SWG_API SWGRadiosondeDemodReport: public SWGObject { +public: + SWGRadiosondeDemodReport(); + SWGRadiosondeDemodReport(QString* json); + virtual ~SWGRadiosondeDemodReport(); + void init(); + void cleanup(); + + virtual QString asJson () override; + virtual QJsonObject* asJsonObject() override; + virtual void fromJsonObject(QJsonObject &json) override; + virtual SWGRadiosondeDemodReport* fromJson(QString &jsonString) override; + + float getChannelPowerDb(); + void setChannelPowerDb(float channel_power_db); + + qint32 getChannelSampleRate(); + void setChannelSampleRate(qint32 channel_sample_rate); + + + virtual bool isSet() override; + +private: + float channel_power_db; + bool m_channel_power_db_isSet; + + qint32 channel_sample_rate; + bool m_channel_sample_rate_isSet; + +}; + +} + +#endif /* SWGRadiosondeDemodReport_H_ */ diff --git a/swagger/sdrangel/code/qt5/client/SWGRadiosondeDemodSettings.cpp b/swagger/sdrangel/code/qt5/client/SWGRadiosondeDemodSettings.cpp new file mode 100644 index 000000000..875661286 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGRadiosondeDemodSettings.cpp @@ -0,0 +1,582 @@ +/** + * SDRangel + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- + * + * OpenAPI spec version: 6.0.0 + * Contact: f4exb06@gmail.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + + +#include "SWGRadiosondeDemodSettings.h" + +#include "SWGHelpers.h" + +#include +#include +#include +#include + +namespace SWGSDRangel { + +SWGRadiosondeDemodSettings::SWGRadiosondeDemodSettings(QString* json) { + init(); + this->fromJson(*json); +} + +SWGRadiosondeDemodSettings::SWGRadiosondeDemodSettings() { + baud = 0; + m_baud_isSet = false; + input_frequency_offset = 0L; + m_input_frequency_offset_isSet = false; + rf_bandwidth = 0.0f; + m_rf_bandwidth_isSet = false; + fm_deviation = 0.0f; + m_fm_deviation_isSet = false; + correlation_threshold = 0.0f; + m_correlation_threshold_isSet = false; + udp_enabled = 0; + m_udp_enabled_isSet = false; + udp_address = nullptr; + m_udp_address_isSet = false; + udp_port = 0; + m_udp_port_isSet = false; + log_filename = nullptr; + m_log_filename_isSet = false; + log_enabled = 0; + m_log_enabled_isSet = false; + rgb_color = 0; + m_rgb_color_isSet = false; + title = nullptr; + m_title_isSet = false; + stream_index = 0; + m_stream_index_isSet = false; + use_reverse_api = 0; + m_use_reverse_api_isSet = false; + reverse_api_address = nullptr; + m_reverse_api_address_isSet = false; + reverse_api_port = 0; + m_reverse_api_port_isSet = false; + reverse_api_device_index = 0; + m_reverse_api_device_index_isSet = false; + reverse_api_channel_index = 0; + m_reverse_api_channel_index_isSet = false; + scope_config = nullptr; + m_scope_config_isSet = false; + channel_marker = nullptr; + m_channel_marker_isSet = false; + rollup_state = nullptr; + m_rollup_state_isSet = false; +} + +SWGRadiosondeDemodSettings::~SWGRadiosondeDemodSettings() { + this->cleanup(); +} + +void +SWGRadiosondeDemodSettings::init() { + baud = 0; + m_baud_isSet = false; + input_frequency_offset = 0L; + m_input_frequency_offset_isSet = false; + rf_bandwidth = 0.0f; + m_rf_bandwidth_isSet = false; + fm_deviation = 0.0f; + m_fm_deviation_isSet = false; + correlation_threshold = 0.0f; + m_correlation_threshold_isSet = false; + udp_enabled = 0; + m_udp_enabled_isSet = false; + udp_address = new QString(""); + m_udp_address_isSet = false; + udp_port = 0; + m_udp_port_isSet = false; + log_filename = new QString(""); + m_log_filename_isSet = false; + log_enabled = 0; + m_log_enabled_isSet = false; + rgb_color = 0; + m_rgb_color_isSet = false; + title = new QString(""); + m_title_isSet = false; + stream_index = 0; + m_stream_index_isSet = false; + use_reverse_api = 0; + m_use_reverse_api_isSet = false; + reverse_api_address = new QString(""); + m_reverse_api_address_isSet = false; + reverse_api_port = 0; + m_reverse_api_port_isSet = false; + reverse_api_device_index = 0; + m_reverse_api_device_index_isSet = false; + reverse_api_channel_index = 0; + m_reverse_api_channel_index_isSet = false; + scope_config = new SWGGLScope(); + m_scope_config_isSet = false; + channel_marker = new SWGChannelMarker(); + m_channel_marker_isSet = false; + rollup_state = new SWGRollupState(); + m_rollup_state_isSet = false; +} + +void +SWGRadiosondeDemodSettings::cleanup() { + + + + + + + if(udp_address != nullptr) { + delete udp_address; + } + + if(log_filename != nullptr) { + delete log_filename; + } + + + if(title != nullptr) { + delete title; + } + + + if(reverse_api_address != nullptr) { + delete reverse_api_address; + } + + + + if(scope_config != nullptr) { + delete scope_config; + } + if(channel_marker != nullptr) { + delete channel_marker; + } + if(rollup_state != nullptr) { + delete rollup_state; + } +} + +SWGRadiosondeDemodSettings* +SWGRadiosondeDemodSettings::fromJson(QString &json) { + QByteArray array (json.toStdString().c_str()); + QJsonDocument doc = QJsonDocument::fromJson(array); + QJsonObject jsonObject = doc.object(); + this->fromJsonObject(jsonObject); + return this; +} + +void +SWGRadiosondeDemodSettings::fromJsonObject(QJsonObject &pJson) { + ::SWGSDRangel::setValue(&baud, pJson["baud"], "qint32", ""); + + ::SWGSDRangel::setValue(&input_frequency_offset, pJson["inputFrequencyOffset"], "qint64", ""); + + ::SWGSDRangel::setValue(&rf_bandwidth, pJson["rfBandwidth"], "float", ""); + + ::SWGSDRangel::setValue(&fm_deviation, pJson["fmDeviation"], "float", ""); + + ::SWGSDRangel::setValue(&correlation_threshold, pJson["correlationThreshold"], "float", ""); + + ::SWGSDRangel::setValue(&udp_enabled, pJson["udpEnabled"], "qint32", ""); + + ::SWGSDRangel::setValue(&udp_address, pJson["udpAddress"], "QString", "QString"); + + ::SWGSDRangel::setValue(&udp_port, pJson["udpPort"], "qint32", ""); + + ::SWGSDRangel::setValue(&log_filename, pJson["logFilename"], "QString", "QString"); + + ::SWGSDRangel::setValue(&log_enabled, pJson["logEnabled"], "qint32", ""); + + ::SWGSDRangel::setValue(&rgb_color, pJson["rgbColor"], "qint32", ""); + + ::SWGSDRangel::setValue(&title, pJson["title"], "QString", "QString"); + + ::SWGSDRangel::setValue(&stream_index, pJson["streamIndex"], "qint32", ""); + + ::SWGSDRangel::setValue(&use_reverse_api, pJson["useReverseAPI"], "qint32", ""); + + ::SWGSDRangel::setValue(&reverse_api_address, pJson["reverseAPIAddress"], "QString", "QString"); + + ::SWGSDRangel::setValue(&reverse_api_port, pJson["reverseAPIPort"], "qint32", ""); + + ::SWGSDRangel::setValue(&reverse_api_device_index, pJson["reverseAPIDeviceIndex"], "qint32", ""); + + ::SWGSDRangel::setValue(&reverse_api_channel_index, pJson["reverseAPIChannelIndex"], "qint32", ""); + + ::SWGSDRangel::setValue(&scope_config, pJson["scopeConfig"], "SWGGLScope", "SWGGLScope"); + + ::SWGSDRangel::setValue(&channel_marker, pJson["channelMarker"], "SWGChannelMarker", "SWGChannelMarker"); + + ::SWGSDRangel::setValue(&rollup_state, pJson["rollupState"], "SWGRollupState", "SWGRollupState"); + +} + +QString +SWGRadiosondeDemodSettings::asJson () +{ + QJsonObject* obj = this->asJsonObject(); + + QJsonDocument doc(*obj); + QByteArray bytes = doc.toJson(); + delete obj; + return QString(bytes); +} + +QJsonObject* +SWGRadiosondeDemodSettings::asJsonObject() { + QJsonObject* obj = new QJsonObject(); + if(m_baud_isSet){ + obj->insert("baud", QJsonValue(baud)); + } + if(m_input_frequency_offset_isSet){ + obj->insert("inputFrequencyOffset", QJsonValue(input_frequency_offset)); + } + if(m_rf_bandwidth_isSet){ + obj->insert("rfBandwidth", QJsonValue(rf_bandwidth)); + } + if(m_fm_deviation_isSet){ + obj->insert("fmDeviation", QJsonValue(fm_deviation)); + } + if(m_correlation_threshold_isSet){ + obj->insert("correlationThreshold", QJsonValue(correlation_threshold)); + } + if(m_udp_enabled_isSet){ + obj->insert("udpEnabled", QJsonValue(udp_enabled)); + } + if(udp_address != nullptr && *udp_address != QString("")){ + toJsonValue(QString("udpAddress"), udp_address, obj, QString("QString")); + } + if(m_udp_port_isSet){ + obj->insert("udpPort", QJsonValue(udp_port)); + } + if(log_filename != nullptr && *log_filename != QString("")){ + toJsonValue(QString("logFilename"), log_filename, obj, QString("QString")); + } + if(m_log_enabled_isSet){ + obj->insert("logEnabled", QJsonValue(log_enabled)); + } + if(m_rgb_color_isSet){ + obj->insert("rgbColor", QJsonValue(rgb_color)); + } + if(title != nullptr && *title != QString("")){ + toJsonValue(QString("title"), title, obj, QString("QString")); + } + if(m_stream_index_isSet){ + obj->insert("streamIndex", QJsonValue(stream_index)); + } + if(m_use_reverse_api_isSet){ + obj->insert("useReverseAPI", QJsonValue(use_reverse_api)); + } + if(reverse_api_address != nullptr && *reverse_api_address != QString("")){ + toJsonValue(QString("reverseAPIAddress"), reverse_api_address, obj, QString("QString")); + } + if(m_reverse_api_port_isSet){ + obj->insert("reverseAPIPort", QJsonValue(reverse_api_port)); + } + if(m_reverse_api_device_index_isSet){ + obj->insert("reverseAPIDeviceIndex", QJsonValue(reverse_api_device_index)); + } + if(m_reverse_api_channel_index_isSet){ + obj->insert("reverseAPIChannelIndex", QJsonValue(reverse_api_channel_index)); + } + if((scope_config != nullptr) && (scope_config->isSet())){ + toJsonValue(QString("scopeConfig"), scope_config, obj, QString("SWGGLScope")); + } + if((channel_marker != nullptr) && (channel_marker->isSet())){ + toJsonValue(QString("channelMarker"), channel_marker, obj, QString("SWGChannelMarker")); + } + if((rollup_state != nullptr) && (rollup_state->isSet())){ + toJsonValue(QString("rollupState"), rollup_state, obj, QString("SWGRollupState")); + } + + return obj; +} + +qint32 +SWGRadiosondeDemodSettings::getBaud() { + return baud; +} +void +SWGRadiosondeDemodSettings::setBaud(qint32 baud) { + this->baud = baud; + this->m_baud_isSet = true; +} + +qint64 +SWGRadiosondeDemodSettings::getInputFrequencyOffset() { + return input_frequency_offset; +} +void +SWGRadiosondeDemodSettings::setInputFrequencyOffset(qint64 input_frequency_offset) { + this->input_frequency_offset = input_frequency_offset; + this->m_input_frequency_offset_isSet = true; +} + +float +SWGRadiosondeDemodSettings::getRfBandwidth() { + return rf_bandwidth; +} +void +SWGRadiosondeDemodSettings::setRfBandwidth(float rf_bandwidth) { + this->rf_bandwidth = rf_bandwidth; + this->m_rf_bandwidth_isSet = true; +} + +float +SWGRadiosondeDemodSettings::getFmDeviation() { + return fm_deviation; +} +void +SWGRadiosondeDemodSettings::setFmDeviation(float fm_deviation) { + this->fm_deviation = fm_deviation; + this->m_fm_deviation_isSet = true; +} + +float +SWGRadiosondeDemodSettings::getCorrelationThreshold() { + return correlation_threshold; +} +void +SWGRadiosondeDemodSettings::setCorrelationThreshold(float correlation_threshold) { + this->correlation_threshold = correlation_threshold; + this->m_correlation_threshold_isSet = true; +} + +qint32 +SWGRadiosondeDemodSettings::getUdpEnabled() { + return udp_enabled; +} +void +SWGRadiosondeDemodSettings::setUdpEnabled(qint32 udp_enabled) { + this->udp_enabled = udp_enabled; + this->m_udp_enabled_isSet = true; +} + +QString* +SWGRadiosondeDemodSettings::getUdpAddress() { + return udp_address; +} +void +SWGRadiosondeDemodSettings::setUdpAddress(QString* udp_address) { + this->udp_address = udp_address; + this->m_udp_address_isSet = true; +} + +qint32 +SWGRadiosondeDemodSettings::getUdpPort() { + return udp_port; +} +void +SWGRadiosondeDemodSettings::setUdpPort(qint32 udp_port) { + this->udp_port = udp_port; + this->m_udp_port_isSet = true; +} + +QString* +SWGRadiosondeDemodSettings::getLogFilename() { + return log_filename; +} +void +SWGRadiosondeDemodSettings::setLogFilename(QString* log_filename) { + this->log_filename = log_filename; + this->m_log_filename_isSet = true; +} + +qint32 +SWGRadiosondeDemodSettings::getLogEnabled() { + return log_enabled; +} +void +SWGRadiosondeDemodSettings::setLogEnabled(qint32 log_enabled) { + this->log_enabled = log_enabled; + this->m_log_enabled_isSet = true; +} + +qint32 +SWGRadiosondeDemodSettings::getRgbColor() { + return rgb_color; +} +void +SWGRadiosondeDemodSettings::setRgbColor(qint32 rgb_color) { + this->rgb_color = rgb_color; + this->m_rgb_color_isSet = true; +} + +QString* +SWGRadiosondeDemodSettings::getTitle() { + return title; +} +void +SWGRadiosondeDemodSettings::setTitle(QString* title) { + this->title = title; + this->m_title_isSet = true; +} + +qint32 +SWGRadiosondeDemodSettings::getStreamIndex() { + return stream_index; +} +void +SWGRadiosondeDemodSettings::setStreamIndex(qint32 stream_index) { + this->stream_index = stream_index; + this->m_stream_index_isSet = true; +} + +qint32 +SWGRadiosondeDemodSettings::getUseReverseApi() { + return use_reverse_api; +} +void +SWGRadiosondeDemodSettings::setUseReverseApi(qint32 use_reverse_api) { + this->use_reverse_api = use_reverse_api; + this->m_use_reverse_api_isSet = true; +} + +QString* +SWGRadiosondeDemodSettings::getReverseApiAddress() { + return reverse_api_address; +} +void +SWGRadiosondeDemodSettings::setReverseApiAddress(QString* reverse_api_address) { + this->reverse_api_address = reverse_api_address; + this->m_reverse_api_address_isSet = true; +} + +qint32 +SWGRadiosondeDemodSettings::getReverseApiPort() { + return reverse_api_port; +} +void +SWGRadiosondeDemodSettings::setReverseApiPort(qint32 reverse_api_port) { + this->reverse_api_port = reverse_api_port; + this->m_reverse_api_port_isSet = true; +} + +qint32 +SWGRadiosondeDemodSettings::getReverseApiDeviceIndex() { + return reverse_api_device_index; +} +void +SWGRadiosondeDemodSettings::setReverseApiDeviceIndex(qint32 reverse_api_device_index) { + this->reverse_api_device_index = reverse_api_device_index; + this->m_reverse_api_device_index_isSet = true; +} + +qint32 +SWGRadiosondeDemodSettings::getReverseApiChannelIndex() { + return reverse_api_channel_index; +} +void +SWGRadiosondeDemodSettings::setReverseApiChannelIndex(qint32 reverse_api_channel_index) { + this->reverse_api_channel_index = reverse_api_channel_index; + this->m_reverse_api_channel_index_isSet = true; +} + +SWGGLScope* +SWGRadiosondeDemodSettings::getScopeConfig() { + return scope_config; +} +void +SWGRadiosondeDemodSettings::setScopeConfig(SWGGLScope* scope_config) { + this->scope_config = scope_config; + this->m_scope_config_isSet = true; +} + +SWGChannelMarker* +SWGRadiosondeDemodSettings::getChannelMarker() { + return channel_marker; +} +void +SWGRadiosondeDemodSettings::setChannelMarker(SWGChannelMarker* channel_marker) { + this->channel_marker = channel_marker; + this->m_channel_marker_isSet = true; +} + +SWGRollupState* +SWGRadiosondeDemodSettings::getRollupState() { + return rollup_state; +} +void +SWGRadiosondeDemodSettings::setRollupState(SWGRollupState* rollup_state) { + this->rollup_state = rollup_state; + this->m_rollup_state_isSet = true; +} + + +bool +SWGRadiosondeDemodSettings::isSet(){ + bool isObjectUpdated = false; + do{ + if(m_baud_isSet){ + isObjectUpdated = true; break; + } + if(m_input_frequency_offset_isSet){ + isObjectUpdated = true; break; + } + if(m_rf_bandwidth_isSet){ + isObjectUpdated = true; break; + } + if(m_fm_deviation_isSet){ + isObjectUpdated = true; break; + } + if(m_correlation_threshold_isSet){ + isObjectUpdated = true; break; + } + if(m_udp_enabled_isSet){ + isObjectUpdated = true; break; + } + if(udp_address && *udp_address != QString("")){ + isObjectUpdated = true; break; + } + if(m_udp_port_isSet){ + isObjectUpdated = true; break; + } + if(log_filename && *log_filename != QString("")){ + isObjectUpdated = true; break; + } + if(m_log_enabled_isSet){ + isObjectUpdated = true; break; + } + if(m_rgb_color_isSet){ + isObjectUpdated = true; break; + } + if(title && *title != QString("")){ + isObjectUpdated = true; break; + } + if(m_stream_index_isSet){ + isObjectUpdated = true; break; + } + if(m_use_reverse_api_isSet){ + isObjectUpdated = true; break; + } + if(reverse_api_address && *reverse_api_address != QString("")){ + isObjectUpdated = true; break; + } + if(m_reverse_api_port_isSet){ + isObjectUpdated = true; break; + } + if(m_reverse_api_device_index_isSet){ + isObjectUpdated = true; break; + } + if(m_reverse_api_channel_index_isSet){ + isObjectUpdated = true; break; + } + if(scope_config && scope_config->isSet()){ + isObjectUpdated = true; break; + } + if(channel_marker && channel_marker->isSet()){ + isObjectUpdated = true; break; + } + if(rollup_state && rollup_state->isSet()){ + isObjectUpdated = true; break; + } + }while(false); + return isObjectUpdated; +} +} + diff --git a/swagger/sdrangel/code/qt5/client/SWGRadiosondeDemodSettings.h b/swagger/sdrangel/code/qt5/client/SWGRadiosondeDemodSettings.h new file mode 100644 index 000000000..7a4db1813 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGRadiosondeDemodSettings.h @@ -0,0 +1,182 @@ +/** + * SDRangel + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- + * + * OpenAPI spec version: 6.0.0 + * Contact: f4exb06@gmail.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + +/* + * SWGRadiosondeDemodSettings.h + * + * RadiosondeDemod + */ + +#ifndef SWGRadiosondeDemodSettings_H_ +#define SWGRadiosondeDemodSettings_H_ + +#include + + +#include "SWGChannelMarker.h" +#include "SWGGLScope.h" +#include "SWGRollupState.h" +#include + +#include "SWGObject.h" +#include "export.h" + +namespace SWGSDRangel { + +class SWG_API SWGRadiosondeDemodSettings: public SWGObject { +public: + SWGRadiosondeDemodSettings(); + SWGRadiosondeDemodSettings(QString* json); + virtual ~SWGRadiosondeDemodSettings(); + void init(); + void cleanup(); + + virtual QString asJson () override; + virtual QJsonObject* asJsonObject() override; + virtual void fromJsonObject(QJsonObject &json) override; + virtual SWGRadiosondeDemodSettings* fromJson(QString &jsonString) override; + + qint32 getBaud(); + void setBaud(qint32 baud); + + qint64 getInputFrequencyOffset(); + void setInputFrequencyOffset(qint64 input_frequency_offset); + + float getRfBandwidth(); + void setRfBandwidth(float rf_bandwidth); + + float getFmDeviation(); + void setFmDeviation(float fm_deviation); + + float getCorrelationThreshold(); + void setCorrelationThreshold(float correlation_threshold); + + qint32 getUdpEnabled(); + void setUdpEnabled(qint32 udp_enabled); + + QString* getUdpAddress(); + void setUdpAddress(QString* udp_address); + + qint32 getUdpPort(); + void setUdpPort(qint32 udp_port); + + QString* getLogFilename(); + void setLogFilename(QString* log_filename); + + qint32 getLogEnabled(); + void setLogEnabled(qint32 log_enabled); + + qint32 getRgbColor(); + void setRgbColor(qint32 rgb_color); + + QString* getTitle(); + void setTitle(QString* title); + + qint32 getStreamIndex(); + void setStreamIndex(qint32 stream_index); + + qint32 getUseReverseApi(); + void setUseReverseApi(qint32 use_reverse_api); + + QString* getReverseApiAddress(); + void setReverseApiAddress(QString* reverse_api_address); + + qint32 getReverseApiPort(); + void setReverseApiPort(qint32 reverse_api_port); + + qint32 getReverseApiDeviceIndex(); + void setReverseApiDeviceIndex(qint32 reverse_api_device_index); + + qint32 getReverseApiChannelIndex(); + void setReverseApiChannelIndex(qint32 reverse_api_channel_index); + + SWGGLScope* getScopeConfig(); + void setScopeConfig(SWGGLScope* scope_config); + + SWGChannelMarker* getChannelMarker(); + void setChannelMarker(SWGChannelMarker* channel_marker); + + SWGRollupState* getRollupState(); + void setRollupState(SWGRollupState* rollup_state); + + + virtual bool isSet() override; + +private: + qint32 baud; + bool m_baud_isSet; + + qint64 input_frequency_offset; + bool m_input_frequency_offset_isSet; + + float rf_bandwidth; + bool m_rf_bandwidth_isSet; + + float fm_deviation; + bool m_fm_deviation_isSet; + + float correlation_threshold; + bool m_correlation_threshold_isSet; + + qint32 udp_enabled; + bool m_udp_enabled_isSet; + + QString* udp_address; + bool m_udp_address_isSet; + + qint32 udp_port; + bool m_udp_port_isSet; + + QString* log_filename; + bool m_log_filename_isSet; + + qint32 log_enabled; + bool m_log_enabled_isSet; + + qint32 rgb_color; + bool m_rgb_color_isSet; + + QString* title; + bool m_title_isSet; + + qint32 stream_index; + bool m_stream_index_isSet; + + qint32 use_reverse_api; + bool m_use_reverse_api_isSet; + + QString* reverse_api_address; + bool m_reverse_api_address_isSet; + + qint32 reverse_api_port; + bool m_reverse_api_port_isSet; + + qint32 reverse_api_device_index; + bool m_reverse_api_device_index_isSet; + + qint32 reverse_api_channel_index; + bool m_reverse_api_channel_index_isSet; + + SWGGLScope* scope_config; + bool m_scope_config_isSet; + + SWGChannelMarker* channel_marker; + bool m_channel_marker_isSet; + + SWGRollupState* rollup_state; + bool m_rollup_state_isSet; + +}; + +} + +#endif /* SWGRadiosondeDemodSettings_H_ */ diff --git a/swagger/sdrangel/code/qt5/client/SWGRadiosondeSettings.cpp b/swagger/sdrangel/code/qt5/client/SWGRadiosondeSettings.cpp new file mode 100644 index 000000000..cb332c9d2 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGRadiosondeSettings.cpp @@ -0,0 +1,275 @@ +/** + * SDRangel + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- + * + * OpenAPI spec version: 6.0.0 + * Contact: f4exb06@gmail.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + + +#include "SWGRadiosondeSettings.h" + +#include "SWGHelpers.h" + +#include +#include +#include +#include + +namespace SWGSDRangel { + +SWGRadiosondeSettings::SWGRadiosondeSettings(QString* json) { + init(); + this->fromJson(*json); +} + +SWGRadiosondeSettings::SWGRadiosondeSettings() { + title = nullptr; + m_title_isSet = false; + rgb_color = 0; + m_rgb_color_isSet = false; + use_reverse_api = 0; + m_use_reverse_api_isSet = false; + reverse_api_address = nullptr; + m_reverse_api_address_isSet = false; + reverse_api_port = 0; + m_reverse_api_port_isSet = false; + reverse_api_feature_set_index = 0; + m_reverse_api_feature_set_index_isSet = false; + reverse_api_feature_index = 0; + m_reverse_api_feature_index_isSet = false; + rollup_state = nullptr; + m_rollup_state_isSet = false; +} + +SWGRadiosondeSettings::~SWGRadiosondeSettings() { + this->cleanup(); +} + +void +SWGRadiosondeSettings::init() { + title = new QString(""); + m_title_isSet = false; + rgb_color = 0; + m_rgb_color_isSet = false; + use_reverse_api = 0; + m_use_reverse_api_isSet = false; + reverse_api_address = new QString(""); + m_reverse_api_address_isSet = false; + reverse_api_port = 0; + m_reverse_api_port_isSet = false; + reverse_api_feature_set_index = 0; + m_reverse_api_feature_set_index_isSet = false; + reverse_api_feature_index = 0; + m_reverse_api_feature_index_isSet = false; + rollup_state = new SWGRollupState(); + m_rollup_state_isSet = false; +} + +void +SWGRadiosondeSettings::cleanup() { + if(title != nullptr) { + delete title; + } + + + if(reverse_api_address != nullptr) { + delete reverse_api_address; + } + + + + if(rollup_state != nullptr) { + delete rollup_state; + } +} + +SWGRadiosondeSettings* +SWGRadiosondeSettings::fromJson(QString &json) { + QByteArray array (json.toStdString().c_str()); + QJsonDocument doc = QJsonDocument::fromJson(array); + QJsonObject jsonObject = doc.object(); + this->fromJsonObject(jsonObject); + return this; +} + +void +SWGRadiosondeSettings::fromJsonObject(QJsonObject &pJson) { + ::SWGSDRangel::setValue(&title, pJson["title"], "QString", "QString"); + + ::SWGSDRangel::setValue(&rgb_color, pJson["rgbColor"], "qint32", ""); + + ::SWGSDRangel::setValue(&use_reverse_api, pJson["useReverseAPI"], "qint32", ""); + + ::SWGSDRangel::setValue(&reverse_api_address, pJson["reverseAPIAddress"], "QString", "QString"); + + ::SWGSDRangel::setValue(&reverse_api_port, pJson["reverseAPIPort"], "qint32", ""); + + ::SWGSDRangel::setValue(&reverse_api_feature_set_index, pJson["reverseAPIFeatureSetIndex"], "qint32", ""); + + ::SWGSDRangel::setValue(&reverse_api_feature_index, pJson["reverseAPIFeatureIndex"], "qint32", ""); + + ::SWGSDRangel::setValue(&rollup_state, pJson["rollupState"], "SWGRollupState", "SWGRollupState"); + +} + +QString +SWGRadiosondeSettings::asJson () +{ + QJsonObject* obj = this->asJsonObject(); + + QJsonDocument doc(*obj); + QByteArray bytes = doc.toJson(); + delete obj; + return QString(bytes); +} + +QJsonObject* +SWGRadiosondeSettings::asJsonObject() { + QJsonObject* obj = new QJsonObject(); + if(title != nullptr && *title != QString("")){ + toJsonValue(QString("title"), title, obj, QString("QString")); + } + if(m_rgb_color_isSet){ + obj->insert("rgbColor", QJsonValue(rgb_color)); + } + if(m_use_reverse_api_isSet){ + obj->insert("useReverseAPI", QJsonValue(use_reverse_api)); + } + if(reverse_api_address != nullptr && *reverse_api_address != QString("")){ + toJsonValue(QString("reverseAPIAddress"), reverse_api_address, obj, QString("QString")); + } + if(m_reverse_api_port_isSet){ + obj->insert("reverseAPIPort", QJsonValue(reverse_api_port)); + } + if(m_reverse_api_feature_set_index_isSet){ + obj->insert("reverseAPIFeatureSetIndex", QJsonValue(reverse_api_feature_set_index)); + } + if(m_reverse_api_feature_index_isSet){ + obj->insert("reverseAPIFeatureIndex", QJsonValue(reverse_api_feature_index)); + } + if((rollup_state != nullptr) && (rollup_state->isSet())){ + toJsonValue(QString("rollupState"), rollup_state, obj, QString("SWGRollupState")); + } + + return obj; +} + +QString* +SWGRadiosondeSettings::getTitle() { + return title; +} +void +SWGRadiosondeSettings::setTitle(QString* title) { + this->title = title; + this->m_title_isSet = true; +} + +qint32 +SWGRadiosondeSettings::getRgbColor() { + return rgb_color; +} +void +SWGRadiosondeSettings::setRgbColor(qint32 rgb_color) { + this->rgb_color = rgb_color; + this->m_rgb_color_isSet = true; +} + +qint32 +SWGRadiosondeSettings::getUseReverseApi() { + return use_reverse_api; +} +void +SWGRadiosondeSettings::setUseReverseApi(qint32 use_reverse_api) { + this->use_reverse_api = use_reverse_api; + this->m_use_reverse_api_isSet = true; +} + +QString* +SWGRadiosondeSettings::getReverseApiAddress() { + return reverse_api_address; +} +void +SWGRadiosondeSettings::setReverseApiAddress(QString* reverse_api_address) { + this->reverse_api_address = reverse_api_address; + this->m_reverse_api_address_isSet = true; +} + +qint32 +SWGRadiosondeSettings::getReverseApiPort() { + return reverse_api_port; +} +void +SWGRadiosondeSettings::setReverseApiPort(qint32 reverse_api_port) { + this->reverse_api_port = reverse_api_port; + this->m_reverse_api_port_isSet = true; +} + +qint32 +SWGRadiosondeSettings::getReverseApiFeatureSetIndex() { + return reverse_api_feature_set_index; +} +void +SWGRadiosondeSettings::setReverseApiFeatureSetIndex(qint32 reverse_api_feature_set_index) { + this->reverse_api_feature_set_index = reverse_api_feature_set_index; + this->m_reverse_api_feature_set_index_isSet = true; +} + +qint32 +SWGRadiosondeSettings::getReverseApiFeatureIndex() { + return reverse_api_feature_index; +} +void +SWGRadiosondeSettings::setReverseApiFeatureIndex(qint32 reverse_api_feature_index) { + this->reverse_api_feature_index = reverse_api_feature_index; + this->m_reverse_api_feature_index_isSet = true; +} + +SWGRollupState* +SWGRadiosondeSettings::getRollupState() { + return rollup_state; +} +void +SWGRadiosondeSettings::setRollupState(SWGRollupState* rollup_state) { + this->rollup_state = rollup_state; + this->m_rollup_state_isSet = true; +} + + +bool +SWGRadiosondeSettings::isSet(){ + bool isObjectUpdated = false; + do{ + if(title && *title != QString("")){ + isObjectUpdated = true; break; + } + if(m_rgb_color_isSet){ + isObjectUpdated = true; break; + } + if(m_use_reverse_api_isSet){ + isObjectUpdated = true; break; + } + if(reverse_api_address && *reverse_api_address != QString("")){ + isObjectUpdated = true; break; + } + if(m_reverse_api_port_isSet){ + isObjectUpdated = true; break; + } + if(m_reverse_api_feature_set_index_isSet){ + isObjectUpdated = true; break; + } + if(m_reverse_api_feature_index_isSet){ + isObjectUpdated = true; break; + } + if(rollup_state && rollup_state->isSet()){ + isObjectUpdated = true; break; + } + }while(false); + return isObjectUpdated; +} +} + diff --git a/swagger/sdrangel/code/qt5/client/SWGRadiosondeSettings.h b/swagger/sdrangel/code/qt5/client/SWGRadiosondeSettings.h new file mode 100644 index 000000000..505d45fdb --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGRadiosondeSettings.h @@ -0,0 +1,102 @@ +/** + * SDRangel + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- + * + * OpenAPI spec version: 6.0.0 + * Contact: f4exb06@gmail.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + +/* + * SWGRadiosondeSettings.h + * + * Radiosonde settings + */ + +#ifndef SWGRadiosondeSettings_H_ +#define SWGRadiosondeSettings_H_ + +#include + + +#include "SWGRollupState.h" +#include + +#include "SWGObject.h" +#include "export.h" + +namespace SWGSDRangel { + +class SWG_API SWGRadiosondeSettings: public SWGObject { +public: + SWGRadiosondeSettings(); + SWGRadiosondeSettings(QString* json); + virtual ~SWGRadiosondeSettings(); + void init(); + void cleanup(); + + virtual QString asJson () override; + virtual QJsonObject* asJsonObject() override; + virtual void fromJsonObject(QJsonObject &json) override; + virtual SWGRadiosondeSettings* fromJson(QString &jsonString) override; + + QString* getTitle(); + void setTitle(QString* title); + + qint32 getRgbColor(); + void setRgbColor(qint32 rgb_color); + + qint32 getUseReverseApi(); + void setUseReverseApi(qint32 use_reverse_api); + + QString* getReverseApiAddress(); + void setReverseApiAddress(QString* reverse_api_address); + + qint32 getReverseApiPort(); + void setReverseApiPort(qint32 reverse_api_port); + + qint32 getReverseApiFeatureSetIndex(); + void setReverseApiFeatureSetIndex(qint32 reverse_api_feature_set_index); + + qint32 getReverseApiFeatureIndex(); + void setReverseApiFeatureIndex(qint32 reverse_api_feature_index); + + SWGRollupState* getRollupState(); + void setRollupState(SWGRollupState* rollup_state); + + + virtual bool isSet() override; + +private: + QString* title; + bool m_title_isSet; + + qint32 rgb_color; + bool m_rgb_color_isSet; + + qint32 use_reverse_api; + bool m_use_reverse_api_isSet; + + QString* reverse_api_address; + bool m_reverse_api_address_isSet; + + qint32 reverse_api_port; + bool m_reverse_api_port_isSet; + + qint32 reverse_api_feature_set_index; + bool m_reverse_api_feature_set_index_isSet; + + qint32 reverse_api_feature_index; + bool m_reverse_api_feature_index_isSet; + + SWGRollupState* rollup_state; + bool m_rollup_state_isSet; + +}; + +} + +#endif /* SWGRadiosondeSettings_H_ */