diff --git a/CMakeLists.txt b/CMakeLists.txt index dc2c5156c..1c5b14947 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -144,6 +144,7 @@ option(ENABLE_FEATURE_PERTESTER "Enable feature pertester plugin" ON) option(ENABLE_FEATURE_GS232CONTROLLER "Enable feature gs232controller plugin" ON) option(ENABLE_FEATURE_REMOTECONTROL "Enable feature remote control plugin" ON) option(ENABLE_FEATURE_SKYMAP "Enable feature sky map plugin" ON) +option(ENABLE_FEATURE_SID "Enable feature sid plugin" ON) # on windows always build external libraries if(WIN32) diff --git a/debian/control b/debian/control index 510bc75f2..710d236a6 100644 --- a/debian/control +++ b/debian/control @@ -69,6 +69,7 @@ Depends: ${shlibs:Depends}, qtspeech5-speechd-plugin, pulseaudio, ffmpeg, + gstreamer1.0-libav, qml-module-qtlocation, qml-module-qtpositioning, qml-module-qtquick-window2, diff --git a/doc/img/ChannelPower_plugin_settings.png b/doc/img/ChannelPower_plugin_settings.png new file mode 100644 index 000000000..c29ec6e57 Binary files /dev/null and b/doc/img/ChannelPower_plugin_settings.png differ diff --git a/doc/img/SID_plugin.jpg b/doc/img/SID_plugin.jpg new file mode 100644 index 000000000..adeb0c088 Binary files /dev/null and b/doc/img/SID_plugin.jpg differ diff --git a/doc/img/SID_plugin_eclipse.png b/doc/img/SID_plugin_eclipse.png new file mode 100644 index 000000000..20b8c4534 Binary files /dev/null and b/doc/img/SID_plugin_eclipse.png differ diff --git a/doc/img/SID_plugin_paths.png b/doc/img/SID_plugin_paths.png new file mode 100644 index 000000000..004f1b6fd Binary files /dev/null and b/doc/img/SID_plugin_paths.png differ diff --git a/doc/img/SID_plugin_settings.png b/doc/img/SID_plugin_settings.png new file mode 100644 index 000000000..fa30c699b Binary files /dev/null and b/doc/img/SID_plugin_settings.png differ diff --git a/doc/img/SID_plugin_settings_dialog.png b/doc/img/SID_plugin_settings_dialog.png new file mode 100644 index 000000000..18e321021 Binary files /dev/null and b/doc/img/SID_plugin_settings_dialog.png differ diff --git a/doc/img/SID_plugin_xray.png b/doc/img/SID_plugin_xray.png new file mode 100644 index 000000000..220dbd1d5 Binary files /dev/null and b/doc/img/SID_plugin_xray.png differ diff --git a/doc/img/SkyMap_Moon.png b/doc/img/SkyMap_Moon.png new file mode 100644 index 000000000..3a87d9d51 Binary files /dev/null and b/doc/img/SkyMap_Moon.png differ diff --git a/plugins/channelrx/channelpower/channelpowergui.cpp b/plugins/channelrx/channelpower/channelpowergui.cpp index dd83e053c..c05eba4e1 100644 --- a/plugins/channelrx/channelpower/channelpowergui.cpp +++ b/plugins/channelrx/channelpower/channelpowergui.cpp @@ -1,6 +1,6 @@ /////////////////////////////////////////////////////////////////////////////////// // Copyright (C) 2016 Edouard Griffiths, F4EXB // -// Copyright (C) 2023 Jon Beniston, M7RCE // +// Copyright (C) 2023-2024 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 // @@ -18,8 +18,6 @@ #include -#include "channelpowergui.h" - #include "device/deviceuiset.h" #include "device/deviceapi.h" #include "dsp/dspengine.h" @@ -36,6 +34,7 @@ #include "maincore.h" #include "channelpower.h" +#include "channelpowergui.h" ChannelPowerGUI* ChannelPowerGUI::create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) { @@ -62,11 +61,14 @@ QByteArray ChannelPowerGUI::serialize() const bool ChannelPowerGUI::deserialize(const QByteArray& data) { - if(m_settings.deserialize(data)) { + if (m_settings.deserialize(data)) + { displaySettings(); applyAllSettings(); return true; - } else { + } + else + { resetToDefaults(); return false; } @@ -90,8 +92,7 @@ bool ChannelPowerGUI::handleMessage(const Message& message) DSPSignalNotification& notif = (DSPSignalNotification&) message; m_deviceCenterFrequency = notif.getCenterFrequency(); m_basebandSampleRate = notif.getSampleRate(); - ui->deltaFrequency->setValueRange(false, 7, -m_basebandSampleRate/2, m_basebandSampleRate/2); - ui->deltaFrequencyLabel->setToolTip(tr("Range %1 %L2 Hz").arg(QChar(0xB1)).arg(m_basebandSampleRate/2)); + calcOffset(); ui->rfBW->setValueRange(floor(log10(m_basebandSampleRate))+1, 0, m_basebandSampleRate); updateAbsoluteCenterFrequency(); return true; @@ -115,9 +116,23 @@ void ChannelPowerGUI::handleInputMessages() void ChannelPowerGUI::channelMarkerChangedByCursor() { - ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); - applySetting("inputFrequencyOffset"); + m_settings.m_frequency = m_deviceCenterFrequency + m_settings.m_inputFrequencyOffset; + + qint64 value = 0; + + if (m_settings.m_frequencyMode == ChannelPowerSettings::Offset) { + value = m_settings.m_inputFrequencyOffset; + } else if (m_settings.m_frequencyMode == ChannelPowerSettings::Absolute) { + value = m_settings.m_frequency; + } + + ui->deltaFrequency->blockSignals(true); + ui->deltaFrequency->setValue(value); + ui->deltaFrequency->blockSignals(false); + + updateAbsoluteCenterFrequency(); + applySettings({"frequency", "inputFrequencyOffset"}); } void ChannelPowerGUI::channelMarkerHighlightedByCursor() @@ -127,10 +142,23 @@ void ChannelPowerGUI::channelMarkerHighlightedByCursor() void ChannelPowerGUI::on_deltaFrequency_changed(qint64 value) { - m_channelMarker.setCenterFrequency(value); + qint64 offset = 0; + + if (m_settings.m_frequencyMode == ChannelPowerSettings::Offset) + { + offset = value; + m_settings.m_frequency = m_deviceCenterFrequency + offset; + } + else if (m_settings.m_frequencyMode == ChannelPowerSettings::Absolute) + { + m_settings.m_frequency = value; + offset = m_settings.m_frequency - m_deviceCenterFrequency; + } + + m_channelMarker.setCenterFrequency(offset); m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); updateAbsoluteCenterFrequency(); - applySetting("inputFrequencyOffset"); + applySettings({"frequency", "inputFrequencyOffset"}); } void ChannelPowerGUI::on_rfBW_changed(qint64 value) @@ -255,7 +283,6 @@ ChannelPowerGUI::ChannelPowerGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, connect(&MainCore::instance()->getMasterTimer(), SIGNAL(timeout()), this, SLOT(tick())); // 50 ms - ui->deltaFrequencyLabel->setText(QString("%1f").arg(QChar(0x94, 0x03))); ui->deltaFrequency->setColorMapper(ColorMapper(ColorMapper::GrayGold)); ui->deltaFrequency->setValueRange(false, 7, -9999999, 9999999); @@ -334,7 +361,8 @@ void ChannelPowerGUI::displaySettings() blockApplySettings(true); - ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); + ui->frequencyMode->setCurrentIndex((int) m_settings.m_frequencyMode); + on_frequencyMode_currentIndexChanged((int) m_settings.m_frequencyMode); ui->rfBW->setValue(m_settings.m_rfBandwidth); @@ -427,6 +455,47 @@ void ChannelPowerGUI::tick() m_tickCount++; } +void ChannelPowerGUI::on_frequencyMode_currentIndexChanged(int index) +{ + m_settings.m_frequencyMode = (ChannelPowerSettings::FrequencyMode) index; + ui->deltaFrequency->blockSignals(true); + + if (m_settings.m_frequencyMode == ChannelPowerSettings::Offset) + { + ui->deltaFrequency->setValueRange(false, 7, -9999999, 9999999); + ui->deltaFrequency->setValue(m_settings.m_inputFrequencyOffset); + ui->deltaUnits->setText("Hz"); + } + else if (m_settings.m_frequencyMode == ChannelPowerSettings::Absolute) + { + ui->deltaFrequency->setValueRange(true, 11, 0, 99999999999, 0); + ui->deltaFrequency->setValue(m_settings.m_frequency); + ui->deltaUnits->setText("Hz"); + } + + ui->deltaFrequency->blockSignals(false); + + updateAbsoluteCenterFrequency(); + applySetting("frequencyMode"); +} + +// Calculate input frequency offset, when device center frequency changes +void ChannelPowerGUI::calcOffset() +{ + if (m_settings.m_frequencyMode == ChannelPowerSettings::Offset) + { + ui->deltaFrequency->setValueRange(false, 7, -m_basebandSampleRate/2, m_basebandSampleRate/2); + } + else + { + qint64 offset = m_settings.m_frequency - m_deviceCenterFrequency; + m_channelMarker.setCenterFrequency(offset); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + updateAbsoluteCenterFrequency(); + applySetting("inputFrequencyOffset"); + } +} + void ChannelPowerGUI::on_clearMeasurements_clicked() { m_channelPower->resetMagLevels(); @@ -434,6 +503,7 @@ void ChannelPowerGUI::on_clearMeasurements_clicked() void ChannelPowerGUI::makeUIConnections() { + QObject::connect(ui->frequencyMode, QOverload::of(&QComboBox::currentIndexChanged), this, &ChannelPowerGUI::on_frequencyMode_currentIndexChanged); QObject::connect(ui->deltaFrequency, &ValueDialZ::changed, this, &ChannelPowerGUI::on_deltaFrequency_changed); QObject::connect(ui->rfBW, &ValueDial::changed, this, &ChannelPowerGUI::on_rfBW_changed); QObject::connect(ui->pulseTH, QOverload::of(&QDial::valueChanged), this, &ChannelPowerGUI::on_pulseTH_valueChanged); @@ -443,5 +513,12 @@ void ChannelPowerGUI::makeUIConnections() void ChannelPowerGUI::updateAbsoluteCenterFrequency() { - setStatusFrequency(m_deviceCenterFrequency + m_settings.m_inputFrequencyOffset); + setStatusFrequency(m_settings.m_frequency); + if ( (m_basebandSampleRate > 1) + && ( (m_settings.m_inputFrequencyOffset >= m_basebandSampleRate / 2) + || (m_settings.m_inputFrequencyOffset < -m_basebandSampleRate / 2))) { + setStatusText("Frequency out of band"); + } else { + setStatusText(""); + } } diff --git a/plugins/channelrx/channelpower/channelpowergui.h b/plugins/channelrx/channelpower/channelpowergui.h index a145def73..9715bd81b 100644 --- a/plugins/channelrx/channelpower/channelpowergui.h +++ b/plugins/channelrx/channelpower/channelpowergui.h @@ -92,6 +92,7 @@ private: void displaySettings(); bool handleMessage(const Message& message); void makeUIConnections(); + void calcOffset(); void updateAbsoluteCenterFrequency(); void on_clearMeasurements_clicked(); @@ -99,6 +100,7 @@ private: void enterEvent(EnterEventType*); private slots: + void on_frequencyMode_currentIndexChanged(int index); void on_deltaFrequency_changed(qint64 value); void on_rfBW_changed(qint64 value); void on_clearChannelPower_clicked(); diff --git a/plugins/channelrx/channelpower/channelpowergui.ui b/plugins/channelrx/channelpower/channelpowergui.ui index 3adbd1c2a..97631aeeb 100644 --- a/plugins/channelrx/channelpower/channelpowergui.ui +++ b/plugins/channelrx/channelpower/channelpowergui.ui @@ -74,16 +74,32 @@ 2 - + - 16 + 40 0 - - Df + + + 40 + 16777215 + + + Select frequency entry mode. + + + + Δf + + + + + f + + diff --git a/plugins/channelrx/channelpower/channelpowersettings.cpp b/plugins/channelrx/channelpower/channelpowersettings.cpp index 5fc0fa06e..b8f97429c 100644 --- a/plugins/channelrx/channelpower/channelpowersettings.cpp +++ b/plugins/channelrx/channelpower/channelpowersettings.cpp @@ -36,6 +36,8 @@ void ChannelPowerSettings::resetToDefaults() m_rfBandwidth = 10000.0f; m_pulseThreshold= -50.0f; m_averagePeriodUS = 100000; + m_frequencyMode = Offset; + m_frequency = 0; m_rgbColor = QColor(102, 40, 220).rgb(); m_title = "Channel Power"; m_streamIndex = 0; @@ -56,6 +58,8 @@ QByteArray ChannelPowerSettings::serialize() const s.writeFloat(2, m_rfBandwidth); s.writeFloat(3, m_pulseThreshold); s.writeS32(4, m_averagePeriodUS); + s.writeS32(5, (int) m_frequencyMode); + s.writeS64(6, m_frequency); s.writeU32(21, m_rgbColor); s.writeString(22, m_title); @@ -102,6 +106,8 @@ bool ChannelPowerSettings::deserialize(const QByteArray& data) d.readFloat(2, &m_rfBandwidth, 10000.0f); d.readFloat(3, &m_pulseThreshold, 50.0f); d.readS32(4, &m_averagePeriodUS, 100000); + d.readS32(5, (int *) &m_frequencyMode, (int) Offset); + d.readS64(6, &m_frequency); d.readU32(21, &m_rgbColor, QColor(102, 40, 220).rgb()); d.readString(22, &m_title, "Channel Power"); @@ -161,6 +167,18 @@ void ChannelPowerSettings::applySettings(const QStringList& settingsKeys, const if (settingsKeys.contains("averagePeriodUS")) { m_averagePeriodUS = settings.m_averagePeriodUS; } + if (settingsKeys.contains("frequencyMode")) { + m_frequencyMode = settings.m_frequencyMode; + } + if (settingsKeys.contains("frequency")) { + m_frequency = settings.m_frequency; + } + if (settingsKeys.contains("rgbColor")) { + m_rgbColor = settings.m_rgbColor; + } + if (settingsKeys.contains("title")) { + m_title = settings.m_title; + } if (settingsKeys.contains("useReverseAPI")) { m_useReverseAPI = settings.m_useReverseAPI; } @@ -191,6 +209,12 @@ QString ChannelPowerSettings::getDebugString(const QStringList& settingsKeys, bo if (settingsKeys.contains("averagePeriodUS") || force) { ostr << " m_averagePeriodUS: " << m_averagePeriodUS; } + if (settingsKeys.contains("frequencyMode") || force) { + ostr << " m_frequencyMode: " << m_frequencyMode; + } + if (settingsKeys.contains("frequency") || force) { + ostr << " m_frequency: " << m_frequency; + } if (settingsKeys.contains("useReverseAPI") || force) { ostr << " m_useReverseAPI: " << m_useReverseAPI; } @@ -200,7 +224,7 @@ QString ChannelPowerSettings::getDebugString(const QStringList& settingsKeys, bo if (settingsKeys.contains("reverseAPIPort") || force) { ostr << " m_reverseAPIPort: " << m_reverseAPIPort; } - if (settingsKeys.contains("everseAPIDeviceIndex") || force) { + if (settingsKeys.contains("reverseAPIDeviceIndex") || force) { ostr << " m_reverseAPIDeviceIndex: " << m_reverseAPIDeviceIndex; } diff --git a/plugins/channelrx/channelpower/channelpowersettings.h b/plugins/channelrx/channelpower/channelpowersettings.h index a875c36bb..b8a9d9dbb 100644 --- a/plugins/channelrx/channelpower/channelpowersettings.h +++ b/plugins/channelrx/channelpower/channelpowersettings.h @@ -1,6 +1,6 @@ /////////////////////////////////////////////////////////////////////////////////// // Copyright (C) 2017 Edouard Griffiths, F4EXB. // -// Copyright (C) 2023 Jon Beniston, M7RCE // +// Copyright (C) 2023-2024 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 // @@ -32,6 +32,11 @@ struct ChannelPowerSettings Real m_rfBandwidth; float m_pulseThreshold; int m_averagePeriodUS; + enum FrequencyMode { + Offset, + Absolute + } m_frequencyMode; + qint64 m_frequency; quint32 m_rgbColor; QString m_title; diff --git a/plugins/channelrx/demodais/aisdemodsink.cpp b/plugins/channelrx/demodais/aisdemodsink.cpp index ddd3560cf..4b6f2a375 100644 --- a/plugins/channelrx/demodais/aisdemodsink.cpp +++ b/plugins/channelrx/demodais/aisdemodsink.cpp @@ -413,7 +413,6 @@ void AISDemodSink::applySettings(const AISDemodSettings& settings, bool force) m_interpolator.create(16, m_channelSampleRate, settings.m_rfBandwidth / 2.2); m_interpolatorDistance = (Real) m_channelSampleRate / (Real) AISDemodSettings::AISDEMOD_CHANNEL_SAMPLE_RATE; m_interpolatorDistanceRemain = m_interpolatorDistance; - m_lowpass.create(301, AISDemodSettings::AISDEMOD_CHANNEL_SAMPLE_RATE, settings.m_rfBandwidth / 2.0f); } if ((settings.m_fmDeviation != m_settings.m_fmDeviation) || force) { @@ -423,7 +422,7 @@ void AISDemodSink::applySettings(const AISDemodSettings& settings, bool force) if ((settings.m_baud != m_settings.m_baud) || force) { m_samplesPerSymbol = AISDemodSettings::AISDEMOD_CHANNEL_SAMPLE_RATE / settings.m_baud; - qDebug() << "ISDemodSink::applySettings: m_samplesPerSymbol: " << m_samplesPerSymbol << " baud " << settings.m_baud; + qDebug() << "AISDemodSink::applySettings: m_samplesPerSymbol: " << m_samplesPerSymbol << " baud " << settings.m_baud; m_pulseShape.create(0.5, 3, m_samplesPerSymbol); // Recieve buffer, long enough for one max length message diff --git a/plugins/channelrx/demodais/aisdemodsink.h b/plugins/channelrx/demodais/aisdemodsink.h index a7ce27aba..3f0a9f667 100644 --- a/plugins/channelrx/demodais/aisdemodsink.h +++ b/plugins/channelrx/demodais/aisdemodsink.h @@ -113,7 +113,6 @@ private: 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 diff --git a/plugins/channelrx/demodam/amdemodgui.cpp b/plugins/channelrx/demodam/amdemodgui.cpp index 960a7950b..960706ef2 100644 --- a/plugins/channelrx/demodam/amdemodgui.cpp +++ b/plugins/channelrx/demodam/amdemodgui.cpp @@ -268,7 +268,7 @@ void AMDemodGUI::channelMarkerChangedByCursor() } m_settings.m_frequency = m_deviceCenterFrequency + m_settings.m_inputFrequencyOffset; - int value = 0; + qint64 value = 0; if (m_settings.m_frequencyMode == AMDemodSettings::Offset) { value = m_settings.m_inputFrequencyOffset; @@ -292,8 +292,6 @@ void AMDemodGUI::channelMarkerChangedByCursor() void AMDemodGUI::on_deltaFrequency_changed(qint64 value) { - (void) value; - qint64 offset = 0; if (m_settings.m_frequencyMode == AMDemodSettings::Offset) diff --git a/plugins/channelrx/demodm17/m17demodprocessor.cpp b/plugins/channelrx/demodm17/m17demodprocessor.cpp index c0bc4c2f4..ba264ced3 100644 --- a/plugins/channelrx/demodm17/m17demodprocessor.cpp +++ b/plugins/channelrx/demodm17/m17demodprocessor.cpp @@ -390,7 +390,7 @@ bool M17DemodProcessor::decode_packet(modemm17::M17FrameDecoder::packet_buffer_t << " Via: " << ax25.m_via << " Type: " << ax25.m_type << " PID: " << ax25.m_pid - << " Data: " << ax25.m_dataASCII; + << " Data: " << QString::fromUtf8(ax25.m_data); if (m_demodInputMessageQueue) { @@ -402,7 +402,7 @@ bool M17DemodProcessor::decode_packet(modemm17::M17FrameDecoder::packet_buffer_t ax25.m_via, ax25.m_type, ax25.m_pid, - ax25.m_dataASCII + ax25.m_data ); msg->getPacket() = packet; m_demodInputMessageQueue->push(msg); diff --git a/plugins/channelrx/demodpacket/packetdemod.cpp b/plugins/channelrx/demodpacket/packetdemod.cpp index 926700635..1e58a7c47 100644 --- a/plugins/channelrx/demodpacket/packetdemod.cpp +++ b/plugins/channelrx/demodpacket/packetdemod.cpp @@ -216,7 +216,7 @@ bool PacketDemod::handleMessage(const Message& cmd) << "\"" << ax25.m_via << "\"," << ax25.m_type << "," << ax25.m_pid << "," - << "\"" << ax25.m_dataASCII << "\"," + << "\"" << QString::fromUtf8(ax25.m_data) << "\"," << "\"" << ax25.m_dataHex << "\"\n"; } else @@ -348,7 +348,7 @@ void PacketDemod::applySettings(const PacketDemodSettings& settings, bool force) if (newFile) { // Write header - m_logStream << "Date,Time,Data,From,To,Via,Type,PID,Data ASCII,Data Hex\n"; + m_logStream << "Date,Time,Data,From,To,Via,Type,PID,Data UTF-8,Data Hex\n"; } } else diff --git a/plugins/channelrx/demodpacket/packetdemodgui.cpp b/plugins/channelrx/demodpacket/packetdemodgui.cpp index b013e9410..26256648a 100644 --- a/plugins/channelrx/demodpacket/packetdemodgui.cpp +++ b/plugins/channelrx/demodpacket/packetdemodgui.cpp @@ -59,7 +59,7 @@ void PacketDemodGUI::resizeTable() ui->packets->setItem(row, PACKET_COL_VIA, new QTableWidgetItem("123456-15-")); ui->packets->setItem(row, PACKET_COL_TYPE, new QTableWidgetItem("Type-")); ui->packets->setItem(row, PACKET_COL_PID, new QTableWidgetItem("PID-")); - ui->packets->setItem(row, PACKET_COL_DATA_ASCII, new QTableWidgetItem("ABCEDGHIJKLMNOPQRSTUVWXYZ")); + ui->packets->setItem(row, PACKET_COL_DATA_STRING, new QTableWidgetItem("ABCEDGHIJKLMNOPQRSTUVWXYZ")); ui->packets->setItem(row, PACKET_COL_DATA_HEX, new QTableWidgetItem("ABCEDGHIJKLMNOPQRSTUVWXYZ")); ui->packets->resizeColumnsToContents(); ui->packets->removeRow(row); @@ -168,7 +168,7 @@ void PacketDemodGUI::packetReceived(const QByteArray& packet, QDateTime dateTime QTableWidgetItem *viaItem = new QTableWidgetItem(); QTableWidgetItem *typeItem = new QTableWidgetItem(); QTableWidgetItem *pidItem = new QTableWidgetItem(); - QTableWidgetItem *dataASCIIItem = new QTableWidgetItem(); + QTableWidgetItem *dataStringItem = new QTableWidgetItem(); QTableWidgetItem *dataHexItem = new QTableWidgetItem(); ui->packets->setItem(row, PACKET_COL_DATE, dateItem); ui->packets->setItem(row, PACKET_COL_TIME, timeItem); @@ -177,7 +177,7 @@ void PacketDemodGUI::packetReceived(const QByteArray& packet, QDateTime dateTime ui->packets->setItem(row, PACKET_COL_VIA, viaItem); ui->packets->setItem(row, PACKET_COL_TYPE, typeItem); ui->packets->setItem(row, PACKET_COL_PID, pidItem); - ui->packets->setItem(row, PACKET_COL_DATA_ASCII, dataASCIIItem); + ui->packets->setItem(row, PACKET_COL_DATA_STRING, dataStringItem); ui->packets->setItem(row, PACKET_COL_DATA_HEX, dataHexItem); dateItem->setText(dateTime.date().toString()); timeItem->setText(dateTime.time().toString()); @@ -186,7 +186,7 @@ void PacketDemodGUI::packetReceived(const QByteArray& packet, QDateTime dateTime viaItem->setText(ax25.m_via); typeItem->setText(ax25.m_type); pidItem->setText(ax25.m_pid); - dataASCIIItem->setText(ax25.m_dataASCII); + dataStringItem->setText(QString::fromUtf8(ax25.m_data)); // Should possibly support different encodings here. PacketMod uses UTF8. dataHexItem->setText(ax25.m_dataHex); filterRow(row); ui->packets->setSortingEnabled(true); diff --git a/plugins/channelrx/demodpacket/packetdemodgui.h b/plugins/channelrx/demodpacket/packetdemodgui.h index b89fd5229..b4a0c0d3c 100644 --- a/plugins/channelrx/demodpacket/packetdemodgui.h +++ b/plugins/channelrx/demodpacket/packetdemodgui.h @@ -116,7 +116,7 @@ private: PACKET_COL_VIA, PACKET_COL_TYPE, PACKET_COL_PID, - PACKET_COL_DATA_ASCII, + PACKET_COL_DATA_STRING, PACKET_COL_DATA_HEX }; diff --git a/plugins/channelrx/demodpacket/packetdemodgui.ui b/plugins/channelrx/demodpacket/packetdemodgui.ui index f81552e44..126996967 100644 --- a/plugins/channelrx/demodpacket/packetdemodgui.ui +++ b/plugins/channelrx/demodpacket/packetdemodgui.ui @@ -736,10 +736,10 @@ - Data (ASCII) + Data - Packet data as ASCII + Packet data as UTF-8 character string @@ -757,9 +757,10 @@ - ButtonSwitch - QToolButton -
gui/buttonswitch.h
+ RollupContents + QWidget +
gui/rollupcontents.h
+ 1
ValueDialZ @@ -768,10 +769,9 @@ 1 - RollupContents - QWidget -
gui/rollupcontents.h
- 1 + ButtonSwitch + QToolButton +
gui/buttonswitch.h
LevelMeterSignalDB diff --git a/plugins/channelrx/demodpacket/readme.md b/plugins/channelrx/demodpacket/readme.md index 0747558cb..57e4176a7 100644 --- a/plugins/channelrx/demodpacket/readme.md +++ b/plugins/channelrx/demodpacket/readme.md @@ -92,5 +92,5 @@ The received packets table displays the contents of the packets that have been r * Via - List of addresses of repeaters the packet has passed through or directed via. * Type - The AX.25 frame type. * PID - Protocol Identifier. -* Data (ASCII) - The AX.25 information field displayed as ASCII. +* Data - The AX.25 information field displayed as UTF-8 character string. * Data (Hex) - The AX.25 information field displayed as hexadecimal. diff --git a/plugins/channelrx/demodradiosonde/radiosondedemodgui.cpp b/plugins/channelrx/demodradiosonde/radiosondedemodgui.cpp index e23ec98c4..4991b2a59 100644 --- a/plugins/channelrx/demodradiosonde/radiosondedemodgui.cpp +++ b/plugins/channelrx/demodradiosonde/radiosondedemodgui.cpp @@ -879,7 +879,7 @@ void RadiosondeDemodGUI::on_logOpen_clicked() QStringList cols; QList radiosondePipes; - MainCore::instance()->getMessagePipes().getMessagePipes(this, "radiosonde", radiosondePipes); + MainCore::instance()->getMessagePipes().getMessagePipes(m_radiosondeDemod, "radiosonde", radiosondePipes); while (!cancelled && CSV::readRow(in, &cols)) { diff --git a/plugins/channelrx/heatmap/heatmapsink.cpp b/plugins/channelrx/heatmap/heatmapsink.cpp index b6c90fa32..e7a6bc3de 100644 --- a/plugins/channelrx/heatmap/heatmapsink.cpp +++ b/plugins/channelrx/heatmap/heatmapsink.cpp @@ -198,7 +198,7 @@ void HeatMapSink::applySettings(const HeatMapSettings& settings, bool force) || (settings.m_sampleRate != m_settings.m_sampleRate) || force) { - m_averageCnt = (int)((settings.m_averagePeriodUS * settings.m_sampleRate / 1e6)); + m_averageCnt = (int)((settings.m_averagePeriodUS * (qint64)settings.m_sampleRate / 1e6)); // For low sample rates, we want a small buffer, so scope update isn't too slow if (settings.m_sampleRate < 100) { m_sampleBufferSize = 1; diff --git a/plugins/channeltx/modpacket/packetmodsource.cpp b/plugins/channeltx/modpacket/packetmodsource.cpp index b08f44c7c..a166e335d 100644 --- a/plugins/channeltx/modpacket/packetmodsource.cpp +++ b/plugins/channeltx/modpacket/packetmodsource.cpp @@ -594,8 +594,9 @@ void PacketModSource::addTXPacket(QString callsign, QString to, QString via, QSt // PID *p++ = m_settings.m_ax25PID; // Data - len = data.length(); - memcpy(p, data.toUtf8(), len); + QByteArray dataBytes = data.toUtf8(); + len = dataBytes.length(); + memcpy(p, dataBytes, len); p += len; // CRC (do not include flags) crc.calculate(crc_start, p-crc_start); diff --git a/plugins/channeltx/modpacket/readme.md b/plugins/channeltx/modpacket/readme.md index c38e07f56..21af80f69 100644 --- a/plugins/channeltx/modpacket/readme.md +++ b/plugins/channeltx/modpacket/readme.md @@ -74,7 +74,7 @@ Enter the routing for the packet. To have the packet repeated by digipeaters, us

16: Data

-The packet of data to send. To send an APRS status message, use the format >Status. The APRS specification can be found at: http://www.aprs.org/doc/APRS101.PDF. APRS messages can be tracked on https://aprs.fi +The packet of data to send. This is encoded using UTF-8. To send an APRS status message, use the format >Status. The APRS specification can be found at: http://www.aprs.org/doc/APRS101.PDF. APRS messages can be tracked on https://aprs.fi

17: TX

diff --git a/plugins/feature/CMakeLists.txt b/plugins/feature/CMakeLists.txt index 1be829154..5911b3c5f 100644 --- a/plugins/feature/CMakeLists.txt +++ b/plugins/feature/CMakeLists.txt @@ -120,3 +120,9 @@ if (ENABLE_FEATURE_REMOTECONTROL) else() message(STATUS "Not building remotecontrol (ENABLE_FEATURE_REMOTECONTROL=${ENABLE_FEATURE_REMOTECONTROL})") endif() + +if (ENABLE_FEATURE_SID) + add_subdirectory(sid) +else() + message(STATUS "Not building SID (ENABLED_FEATURE_SID=${ENABLED_FEATURE_SID})") +endif() diff --git a/plugins/feature/aprs/aprsgui.cpp b/plugins/feature/aprs/aprsgui.cpp index 4abcdd5b2..eea47c40b 100644 --- a/plugins/feature/aprs/aprsgui.cpp +++ b/plugins/feature/aprs/aprsgui.cpp @@ -408,7 +408,7 @@ bool APRSGUI::handleMessage(const Message& message) else { qDebug() << "APRSGUI::handleMessage: Failed to decode as APRS"; - qDebug() << ax25.m_from << " " << ax25.m_to << " " << ax25.m_via << " " << ax25.m_type << " " << ax25.m_pid << " "<< ax25.m_dataASCII; + qDebug() << "From:" << ax25.m_from << "To:" << ax25.m_to << "Via:" << ax25.m_via << "Type:" << ax25.m_type << "PID:" << ax25.m_pid << "Data:" << QString::fromLatin1(ax25.m_data); } } else diff --git a/plugins/feature/aprs/aprsworker.cpp b/plugins/feature/aprs/aprsworker.cpp index c75c95017..2a830dd7b 100644 --- a/plugins/feature/aprs/aprsworker.cpp +++ b/plugins/feature/aprs/aprsworker.cpp @@ -102,20 +102,25 @@ bool APRSWorker::handleMessage(const Message& cmd) { MainCore::MsgPacket& report = (MainCore::MsgPacket&) cmd; AX25Packet ax25; - APRSPacket *aprs = new APRSPacket(); + if (ax25.decode(report.getPacket())) { - if (aprs->decode(ax25)) + APRSPacket aprs; + + // #2029 - Forward data even if we can't decode it fully + aprs.decode(ax25); + + if (!aprs.m_data.isEmpty()) { // See: http://www.aprs-is.net/IGateDetails.aspx for gating rules - if (!aprs->m_via.contains("TCPIP") - && !aprs->m_via.contains("TCPXX") - && !aprs->m_via.contains("NOGATE") - && !aprs->m_via.contains("RFONLY")) + if (!aprs.m_via.contains("TCPIP") + && !aprs.m_via.contains("TCPXX") + && !aprs.m_via.contains("NOGATE") + && !aprs.m_via.contains("RFONLY")) { - aprs->m_dateTime = report.getDateTime(); - QString igateMsg = aprs->toTNC2(m_settings.m_igateCallsign); - send(igateMsg.toUtf8(), igateMsg.length()); + aprs.m_dateTime = report.getDateTime(); + QByteArray igateMsg = aprs.toTNC2(m_settings.m_igateCallsign); + send(igateMsg.data(), igateMsg.length()); } } } @@ -207,7 +212,7 @@ void APRSWorker::recv() if (!m_loggedIn) { // Log in with callsign and passcode - QString login = QString("user %1 pass %2 vers SDRangel 6.4.0%3\r\n").arg(m_settings.m_igateCallsign).arg(m_settings.m_igatePasscode).arg(m_settings.m_igateFilter.isEmpty() ? "" : QString(" filter %1").arg(m_settings.m_igateFilter)); + QString login = QString("user %1 pass %2 vers SDRangel 7.19.2%3\r\n").arg(m_settings.m_igateCallsign).arg(m_settings.m_igatePasscode).arg(m_settings.m_igateFilter.isEmpty() ? "" : QString(" filter %1").arg(m_settings.m_igateFilter)); send(login.toLatin1(), login.length()); m_loggedIn = true; if (m_msgQueueToFeature) diff --git a/plugins/feature/map/CMakeLists.txt b/plugins/feature/map/CMakeLists.txt index 975546db9..028397eb8 100644 --- a/plugins/feature/map/CMakeLists.txt +++ b/plugins/feature/map/CMakeLists.txt @@ -54,7 +54,6 @@ if(NOT SERVER_MODE) mapibpbeacondialog.ui mapradiotimedialog.cpp mapradiotimedialog.ui - mapcolordialog.cpp mapmodel.cpp mapitem.cpp mapwebsocketserver.cpp @@ -75,7 +74,6 @@ if(NOT SERVER_MODE) mapbeacondialog.h mapibpbeacon.h mapradiotimedialog.h - mapcolordialog.h mapmodel.h mapitem.h mapwebsocketserver.h diff --git a/plugins/feature/map/cesiuminterface.cpp b/plugins/feature/map/cesiuminterface.cpp index e9a3e39a2..0f797f4d7 100644 --- a/plugins/feature/map/cesiuminterface.cpp +++ b/plugins/feature/map/cesiuminterface.cpp @@ -275,3 +275,13 @@ void CesiumInterface::setPosition(const QGeoCoordinate& position) { m_czml.setPosition(position); } + +void CesiumInterface::save(const QString& filename, const QString& dataDir) +{ + QJsonObject obj { + {"command", "save"}, + {"filename", filename}, + {"dataDir", dataDir} + }; + send(obj); +} diff --git a/plugins/feature/map/cesiuminterface.h b/plugins/feature/map/cesiuminterface.h index 418d0bf07..d4790aa25 100644 --- a/plugins/feature/map/cesiuminterface.h +++ b/plugins/feature/map/cesiuminterface.h @@ -48,7 +48,6 @@ public: bool m_reverse; bool m_loop; bool m_stop; // Stop looped animation - float m_delay; // Delay in seconds before animation starts float m_startOffset; // [0..1] What point to start playing animation float m_duration; // How long to play animation for float m_multiplier; // Speed to play animation at @@ -80,6 +79,7 @@ public: void update(PolygonMapItem *mapItem); void update(PolylineMapItem *mapItem); void setPosition(const QGeoCoordinate& position); + void save(const QString& filename, const QString& dataDir); protected: diff --git a/plugins/feature/map/czml.cpp b/plugins/feature/map/czml.cpp index 002e1e5d1..26aca7e22 100644 --- a/plugins/feature/map/czml.cpp +++ b/plugins/feature/map/czml.cpp @@ -264,7 +264,9 @@ QJsonObject CZML::update(ObjectMapItem *mapItem, bool isTarget, bool isSelected) const QStringList heightReferences = {"NONE", "CLAMP_TO_GROUND", "RELATIVE_TO_GROUND", "NONE"}; QString dt; - if (mapItem->m_takenTrackDateTimes.size() > 0) { + if (mapItem->m_availableFrom.isValid()) { + dt = mapItem->m_availableFrom.toString(Qt::ISODateWithMs); + } else if (mapItem->m_takenTrackDateTimes.size() > 0) { dt = mapItem->m_takenTrackDateTimes.last()->toString(Qt::ISODateWithMs); } else { dt = QDateTime::currentDateTimeUtc().toString(Qt::ISODateWithMs); @@ -580,6 +582,14 @@ QJsonObject CZML::update(ObjectMapItem *mapItem, bool isTarget, bool isSelected) } } } + else + { + if (mapItem->m_availableUntil.isValid()) + { + QString period = QString("%1/%2").arg(m_ids[id]).arg(mapItem->m_availableUntil.toString(Qt::ISODateWithMs)); + obj.insert("availability", period); + } + } m_lastPosition.insert(id, coords); } else diff --git a/plugins/feature/map/map/map3d.html b/plugins/feature/map/map/map3d.html index 7c529afee..e6208c196 100644 --- a/plugins/feature/map/map/map3d.html +++ b/plugins/feature/map/map/map3d.html @@ -364,6 +364,52 @@ ["railways", railwaysLayer] ]); + function downloadBlob(filename, blob) { + if (window.navigator.msSaveOrOpenBlob) { + window.navigator.msSaveBlob(blob, filename); + } else { + const elem = window.document.createElement("a"); + elem.href = window.URL.createObjectURL(blob); + elem.download = filename; + document.body.appendChild(elem); + elem.click(); + document.body.removeChild(elem); + } + } + + function downloadText(filename, text) { + var element = document.createElement('a'); + element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text)); + element.setAttribute('download', filename); + + element.style.display = 'none'; + document.body.appendChild(element); + + element.click(); + + document.body.removeChild(element); + } + + var dataDir = ""; // Directory where 3D models are stored + function modelCallback(modelGraphics, time, externalFiles) { + const resource = modelGraphics.uri.getValue(time); + console.log("modelcallback " + resource); + + const regex = /http:\/\/127.0.0.1:\d+/; + + var file = resource.url.replace(regex, dataDir); + + // KML only supports Collada files. User will have to convert the models if required + file = file.replace(/glb$/, "dae"); + file = file.replace(/gltf$/, "dae"); + + if (navigator.platform.indexOf('Win') > -1) { + file = file.replace(/\//g, "\\"); + } + + return file; + } + // Use WebSockets for handling commands from MapPlugin // (CZML doesn't support camera control, for example) // and sending events back to it @@ -469,30 +515,40 @@ viewer.scene.postProcessStages.fxaa.enabled = false; } } else if (command.command == "showMUF") { - if (mufGeoJSONStream != null) { - viewer.dataSources.remove(mufGeoJSONStream, true); - mufGeoJSONStream = null; - } if (command.show == true) { viewer.dataSources.add( Cesium.GeoJsonDataSource.load( "muf.geojson", { describe: describeMUF } ) - ).then(function (dataSource) { mufGeoJSONStream = dataSource; }); + ).then(function (dataSource) { + if (mufGeoJSONStream != null) { + viewer.dataSources.remove(mufGeoJSONStream, true); + mufGeoJSONStream = null; + } + mufGeoJSONStream = dataSource; + }); + } else { + viewer.dataSources.remove(mufGeoJSONStream, true); + mufGeoJSONStream = null; } } else if (command.command == "showfoF2") { - if (foF2GeoJSONStream != null) { - viewer.dataSources.remove(foF2GeoJSONStream, true); - foF2GeoJSONStream = null; - } if (command.show == true) { viewer.dataSources.add( Cesium.GeoJsonDataSource.load( "fof2.geojson", { describe: describefoF2 } ) - ).then(function (dataSource) { foF2GeoJSONStream = dataSource; }); + ).then(function (dataSource) { + if (foF2GeoJSONStream != null) { + viewer.dataSources.remove(foF2GeoJSONStream, true); + foF2GeoJSONStream = null; + } + foF2GeoJSONStream = dataSource; + }); + } else { + viewer.dataSources.remove(foF2GeoJSONStream, true); + foF2GeoJSONStream = null; } } else if (command.command == "showLayer") { layers.get(command.layer).show = command.show; @@ -639,7 +695,7 @@ czmlStream.process(command); } else { var promise = Cesium.sampleTerrainMostDetailed(viewer.terrainProvider, [position]); - Cesium.when(promise, function(updatedPositions) { + Cesium.when(promise, function (updatedPositions) { if (height < updatedPositions[0].height) { if (size == 3) { command.position.cartographicDegrees[2] = updatedPositions[0].height; @@ -648,7 +704,7 @@ } } czmlStream.process(command); - }, function() { + }, function () { console.log(`Terrain doesn't support sampleTerrainMostDetailed`); czmlStream.process(command); }); @@ -657,47 +713,47 @@ console.log(`Can't currently use altitudeReference when more than one position`); czmlStream.process(command); } - } else if ( (command.hasOwnProperty('polygon') && command.polygon.hasOwnProperty('altitudeReference')) - || (command.hasOwnProperty('polyline') && command.polyline.hasOwnProperty('altitudeReference'))) { + } else if ((command.hasOwnProperty('polygon') && command.polygon.hasOwnProperty('altitudeReference')) + || (command.hasOwnProperty('polyline') && command.polyline.hasOwnProperty('altitudeReference'))) { // Support per vertex height reference in polygons and CLIP_TO_GROUND in polylines var prim = command.hasOwnProperty('polygon') ? command.polygon : command.polyline; var clipToGround = prim.altitudeReference == "CLIP_TO_GROUND"; var clampToGround = prim.altitudeReference == "CLAMP_TO_GROUND"; var size = prim.positions.cartographicDegrees.length; - var positionCount = size/3; + var positionCount = size / 3; var positions = new Array(positionCount); if (viewer.terrainProvider instanceof Cesium.EllipsoidTerrainProvider) { if (clampToGround) { for (let i = 0; i < positionCount; i++) { - prim.positions.cartographicDegrees[i*3+2] = 0; + prim.positions.cartographicDegrees[i * 3 + 2] = 0; } } else if (clipToGround) { for (let i = 0; i < positionCount; i++) { - if (prim.positions.cartographicDegrees[i*3+2] < 0) { - prim.positions.cartographicDegrees[i*3+2] = 0; + if (prim.positions.cartographicDegrees[i * 3 + 2] < 0) { + prim.positions.cartographicDegrees[i * 3 + 2] = 0; } } } czmlStream.process(command); } else { for (let i = 0; i < positionCount; i++) { - positions[i] = Cesium.Cartographic.fromDegrees(prim.positions.cartographicDegrees[i*3+0], prim.positions.cartographicDegrees[i*3+1]); + positions[i] = Cesium.Cartographic.fromDegrees(prim.positions.cartographicDegrees[i * 3 + 0], prim.positions.cartographicDegrees[i * 3 + 1]); } var promise = Cesium.sampleTerrainMostDetailed(viewer.terrainProvider, positions); - Cesium.when(promise, function(updatedPositions) { + Cesium.when(promise, function (updatedPositions) { if (clampToGround) { for (let i = 0; i < positionCount; i++) { - prim.positions.cartographicDegrees[i*3+2] = updatedPositions[i].height; + prim.positions.cartographicDegrees[i * 3 + 2] = updatedPositions[i].height; } } else if (clipToGround) { for (let i = 0; i < positionCount; i++) { - if (prim.positions.cartographicDegrees[i*3+2] < updatedPositions[i].height) { - prim.positions.cartographicDegrees[i*3+2] = updatedPositions[i].height; + if (prim.positions.cartographicDegrees[i * 3 + 2] < updatedPositions[i].height) { + prim.positions.cartographicDegrees[i * 3 + 2] = updatedPositions[i].height; } } } czmlStream.process(command); - }, function() { + }, function () { console.log(`Terrain doesn't support sampleTerrainMostDetailed`); czmlStream.process(command); }); @@ -705,7 +761,20 @@ } else { czmlStream.process(command); } - + } else if (command.command == "save") { + // Export to kml/kmz + dataDir = command.dataDir; + Cesium.exportKml({ + entities: czmlStream.entities, + kmz: command.filename.endsWith("kmz"), + modelCallback: modelCallback + }).then(function (result) { + if (command.filename.endsWith("kmz")) { + downloadBlob(command.filename, result.kmz); + } else { + downloadText(command.filename, result.kml); + } + }); } else { console.log(`Unknown command ${command.command}`); } @@ -759,10 +828,13 @@ Cesium.knockout.getObservable(viewer.clockViewModel, 'multiplier').subscribe(function(multiplier) { reportClock(); }); - // This is called every frame + // This is called every frame, which is too fast, so instead use setInterval with 1 second period //Cesium.knockout.getObservable(viewer.clockViewModel, 'currentTime').subscribe(function(currentTime) { //reportClock(); //}); + setInterval(function () { + reportClock(); + }, 1000); viewer.timeline.addEventListener('settime', reportClock, false); socket.onopen = () => { diff --git a/plugins/feature/map/mapgui.cpp b/plugins/feature/map/mapgui.cpp index e192af0b5..ea1987642 100644 --- a/plugins/feature/map/mapgui.cpp +++ b/plugins/feature/map/mapgui.cpp @@ -48,6 +48,7 @@ #include "util/maidenhead.h" #include "util/morse.h" #include "util/navtex.h" +#include "util/vlftransmitters.h" #include "maplocationdialog.h" #include "mapmaidenheaddialog.h" #include "mapsettingsdialog.h" @@ -306,6 +307,9 @@ MapGUI::MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur connect(ui->web->page(), &QWebEnginePage::loadingChanged, this, &MapGUI::loadingChanged); connect(ui->web, &QWebEngineView::renderProcessTerminated, this, &MapGUI::renderProcessTerminated); #endif + + QWebEngineProfile *profile = QWebEngineProfile::defaultProfile(); + connect(profile, &QWebEngineProfile::downloadRequested, this, &MapGUI::downloadRequested); #endif // Get station position @@ -489,41 +493,24 @@ void MapGUI::addIBPBeacons() } } -// https://sidstation.loudet.org/stations-list-en.xhtml -// https://core.ac.uk/download/pdf/224769021.pdf -- Table 1 -// GQD/GQZ callsigns: https://groups.io/g/VLF/message/19212?p=%2C%2C%2C20%2C0%2C0%2C0%3A%3Arecentpostdate%2Fsticky%2C%2C19.6%2C20%2C2%2C0%2C38924431 -const QList MapGUI::m_vlfTransmitters = { - // Other signals possibly seen: 13800, 19000 - {"VTX2", 17000, 8.387015, 77.752762, -1}, // South Vijayanarayanam, India - {"GQD", 19580, 54.911643, -3.278456, 100}, // Anthorn, UK, Often referred to as GBZ - {"NWC", 19800, -21.816325, 114.16546, 1000}, // Exmouth, Aus - {"ICV", 20270, 40.922946, 9.731881, 50}, // Isola di Tavolara, Italy (Can be distorted on 3D map if terrain used) - {"FTA", 20900, 48.544632, 2.579429, 50}, // Sainte-Assise, France (Satellite imagary obfuscated) - {"NPM", 21400, 21.420166, -158.151140, 600}, // Pearl Harbour, Lualuahei, USA (Not seen?) - {"HWU", 21750, 46.713129, 1.245248, 200}, // Rosnay, France - {"GQZ", 22100, 54.731799, -2.883033, 100}, // Skelton, UK (GVT in paper) - {"DHO38", 23400, 53.078900, 7.615000, 300}, // Rhauderfehn, Germany - Off air 7-8 UTC - Not seen on air! - {"NAA", 24000, 44.644506, -67.284565, 1000}, // Cutler, Maine, USA - {"TFK/NRK", 37500, 63.850365, -22.466773, 100}, // Grindavik, Iceland - {"SRC/SHR", 38000, 57.120328, 16.153083, -1}, // Ruda, Sweden -}; - void MapGUI::addVLF() { - for (int i = 0; i < m_vlfTransmitters.size(); i++) + for (int i = 0; i < VLFTransmitters::m_transmitters.size(); i++) { SWGSDRangel::SWGMapItem vlfMapItem; - // Need to suffix frequency, as there are multiple becaons with same callsign at different locations - QString name = QString("%1").arg(m_vlfTransmitters[i].m_callsign); + QString name = QString("%1").arg(VLFTransmitters::m_transmitters[i].m_callsign); vlfMapItem.setName(new QString(name)); - vlfMapItem.setLatitude(m_vlfTransmitters[i].m_latitude); - vlfMapItem.setLongitude(m_vlfTransmitters[i].m_longitude); + vlfMapItem.setLatitude(VLFTransmitters::m_transmitters[i].m_latitude); + vlfMapItem.setLongitude(VLFTransmitters::m_transmitters[i].m_longitude); vlfMapItem.setAltitude(0.0); vlfMapItem.setImage(new QString("antenna.png")); vlfMapItem.setImageRotation(0); QString text = QString("VLF Transmitter\nCallsign: %1\nFrequency: %2 kHz") - .arg(m_vlfTransmitters[i].m_callsign) - .arg(m_vlfTransmitters[i].m_frequency/1000.0); + .arg(VLFTransmitters::m_transmitters[i].m_callsign) + .arg(VLFTransmitters::m_transmitters[i].m_frequency/1000.0); + if (VLFTransmitters::m_transmitters[i].m_power > 0) { + text.append(QString("\nPower: %1 kW").arg(VLFTransmitters::m_transmitters[i].m_power)); + } vlfMapItem.setText(new QString(text)); vlfMapItem.setModel(new QString("antenna.glb")); vlfMapItem.setFixedPosition(true); @@ -535,7 +522,6 @@ void MapGUI::addVLF() } } - const QList MapGUI::m_radioTimeTransmitters = { {"MSF", 60000, 54.9075f, -3.27333f, 17}, // UK {"DCF77", 77500, 50.01611111f, 9.00805556f, 50}, // Germany @@ -722,12 +708,18 @@ void MapGUI::addIonosonde() m_giro = GIRO::create(); if (m_giro) { + connect(m_giro, &GIRO::indexUpdated, this, &MapGUI::giroIndexUpdated); connect(m_giro, &GIRO::dataUpdated, this, &MapGUI::giroDataUpdated); connect(m_giro, &GIRO::mufUpdated, this, &MapGUI::mufUpdated); connect(m_giro, &GIRO::foF2Updated, this, &MapGUI::foF2Updated); } } +void MapGUI::giroIndexUpdated(const QList& data) +{ + (void) data; +} + void MapGUI::giroDataUpdated(const GIRO::GIROStationData& data) { if (!data.m_station.isEmpty()) @@ -761,6 +753,8 @@ void MapGUI::giroDataUpdated(const GIRO::GIROStationData& data) ionosondeStationMapItem.setLabel(new QString(station->m_label)); ionosondeStationMapItem.setLabelAltitudeOffset(4.5); ionosondeStationMapItem.setAltitudeReference(1); + ionosondeStationMapItem.setAvailableFrom(new QString(data.m_dateTime.toString(Qt::ISODateWithMs))); + ionosondeStationMapItem.setAvailableUntil(new QString(data.m_dateTime.addDays(3).toString(Qt::ISODateWithMs))); // Remove after data is too old update(m_map, &ionosondeStationMapItem, "Ionosonde Stations"); } } @@ -786,6 +780,24 @@ void MapGUI::foF2Updated(const QJsonDocument& document) } } +void MapGUI::updateGIRO(const QDateTime& mapDateTime) +{ + if (m_giro) + { + if (m_settings.m_displayMUF || m_settings.m_displayfoF2) + { + QString giroRunId = m_giro->getRunId(mapDateTime); + if (m_giroRunId.isEmpty() || (!giroRunId.isEmpty() && (giroRunId != m_giroRunId))) + { + m_giro->getMUF(giroRunId); + m_giro->getMUF(giroRunId); + m_giroRunId = giroRunId; + m_giroDateTime = mapDateTime; + } + } + } +} + void MapGUI::pathUpdated(const QString& radarPath, const QString& satellitePath) { m_radarPath = radarPath; @@ -1683,6 +1695,7 @@ void MapGUI::displayToolbar() ui->displayNASAGlobalImagery->setVisible(overlayButtons); ui->displayMUF->setVisible(!narrow && m_settings.m_map3DEnabled); ui->displayfoF2->setVisible(!narrow && m_settings.m_map3DEnabled); + ui->save->setVisible(m_settings.m_map3DEnabled); } void MapGUI::setEnableOverlay() @@ -1803,11 +1816,10 @@ void MapGUI::applyMap3DSettings(bool reloadMap) m_polylineMapModel.allUpdated(); } MapSettings::MapItemSettings *ionosondeItemSettings = getItemSettings("Ionosonde Stations"); + m_giro->getIndexPeriodically((m_settings.m_displayMUF || m_settings.m_displayfoF2) ? 15 : 0); if (ionosondeItemSettings) { m_giro->getDataPeriodically(ionosondeItemSettings->m_enabled ? 2 : 0); } - m_giro->getMUFPeriodically(m_settings.m_displayMUF ? 15 : 0); - m_giro->getfoF2Periodically(m_settings.m_displayfoF2 ? 15 : 0); #else ui->displayMUF->setVisible(false); ui->displayfoF2->setVisible(false); @@ -2211,7 +2223,7 @@ void MapGUI::on_displayMUF_clicked(bool checked) m_settings.m_displayMUF = checked; // Only call show if disabling, so we don't get two updates // (as getMUFPeriodically results in a call to showMUF when the data is available) - m_giro->getMUFPeriodically(m_settings.m_displayMUF ? 15 : 0); + m_giro->getIndexPeriodically((m_settings.m_displayMUF || m_settings.m_displayfoF2) ? 15 : 0); if (m_cesium && !m_settings.m_displayMUF) { m_cesium->showMUF(m_settings.m_displayMUF); } @@ -2226,7 +2238,7 @@ void MapGUI::on_displayfoF2_clicked(bool checked) m_displayfoF2->setChecked(checked); } m_settings.m_displayfoF2 = checked; - m_giro->getfoF2Periodically(m_settings.m_displayfoF2 ? 15 : 0); + m_giro->getIndexPeriodically((m_settings.m_displayMUF || m_settings.m_displayfoF2) ? 15 : 0); if (m_cesium && !m_settings.m_displayfoF2) { m_cesium->showfoF2(m_settings.m_displayfoF2); } @@ -2442,6 +2454,22 @@ void MapGUI::track3D(const QString& target) } } +void MapGUI::on_save_clicked() +{ + if (m_cesium) + { + m_fileDialog.setAcceptMode(QFileDialog::AcceptSave); + m_fileDialog.setNameFilter("*.kml *.kmz"); + if (m_fileDialog.exec()) + { + QStringList fileNames = m_fileDialog.selectedFiles(); + if (fileNames.size() > 0) { + m_cesium->save(fileNames[0], getDataDir()); + } + } + } +} + void MapGUI::on_deleteAll_clicked() { m_objectMapModel.removeAll(); @@ -2543,6 +2571,7 @@ void MapGUI::receivedCesiumEvent(const QJsonObject &obj) bool canAnimate = obj.value("canAnimate").toBool(); bool shouldAnimate = obj.value("shouldAnimate").toBool(); m_map->setMapDateTime(mapDateTime, systemDateTime, canAnimate && shouldAnimate ? multiplier : 0.0); + updateGIRO(mapDateTime); } } else if (event == "link") @@ -2715,6 +2744,17 @@ void MapGUI::fullScreenRequested(QWebEngineFullScreenRequest fullScreenRequest) ui->splitter->addWidget(ui->web); } } +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) +void MapGUI::downloadRequested(QWebEngineDownloadRequest *download) +{ + download->accept(); +} +#else +void MapGUI::downloadRequested(QWebEngineDownloadItem *download) +{ + download->accept(); +} +#endif #endif void MapGUI::preferenceChanged(int elementType) @@ -2787,6 +2827,7 @@ void MapGUI::makeUIConnections() QObject::connect(ui->displayfoF2, &ButtonSwitch::clicked, this, &MapGUI::on_displayfoF2_clicked); QObject::connect(ui->find, &QLineEdit::returnPressed, this, &MapGUI::on_find_returnPressed); QObject::connect(ui->maidenhead, &QToolButton::clicked, this, &MapGUI::on_maidenhead_clicked); + QObject::connect(ui->save, &QToolButton::clicked, this, &MapGUI::on_save_clicked); QObject::connect(ui->deleteAll, &QToolButton::clicked, this, &MapGUI::on_deleteAll_clicked); QObject::connect(ui->displaySettings, &QToolButton::clicked, this, &MapGUI::on_displaySettings_clicked); QObject::connect(ui->mapTypes, qOverload(&QComboBox::currentIndexChanged), this, &MapGUI::on_mapTypes_currentIndexChanged); diff --git a/plugins/feature/map/mapgui.h b/plugins/feature/map/mapgui.h index b541f4318..5e0a12746 100644 --- a/plugins/feature/map/mapgui.h +++ b/plugins/feature/map/mapgui.h @@ -24,6 +24,7 @@ #include #include #include +#include #ifdef QT_WEBENGINE_FOUND #include #include @@ -194,6 +195,7 @@ private: RollupState m_rollupState; bool m_doApplySettings; AvailableChannelOrFeatureList m_availableChannelOrFeatures; + QFileDialog m_fileDialog; Map* m_map; MessageQueue m_inputMessageQueue; @@ -217,6 +219,8 @@ private: MapTileServer *m_mapTileServer; QTimer m_redrawMapTimer; GIRO *m_giro; + QDateTime m_giroDateTime; + QString m_giroRunId; QHash m_ionosondeStations; QSharedPointer> m_navAids; QSharedPointer> m_airspaces; @@ -279,6 +283,7 @@ private: void openKiwiSDR(const QString& url); void openSpyServer(const QString& url); QString formatFrequency(qint64 frequency) const; + void updateGIRO(const QDateTime& mapDateTime); static QString getDataDir(); static const QList m_radioTimeTransmitters; @@ -315,6 +320,7 @@ private slots: void on_layersMenu_clicked(); void on_find_returnPressed(); void on_maidenhead_clicked(); + void on_save_clicked(); void on_deleteAll_clicked(); void on_displaySettings_clicked(); void on_mapTypes_currentIndexChanged(int index); @@ -330,10 +336,14 @@ private slots: void renderProcessTerminated(QWebEnginePage::RenderProcessTerminationStatus terminationStatus, int exitCode); #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) void loadingChanged(const QWebEngineLoadingInfo &loadingInfo); + void downloadRequested(QWebEngineDownloadRequest *download); +#else + void downloadRequested(QWebEngineDownloadItem *download); #endif #endif void statusChanged(QQuickWidget::Status status); void preferenceChanged(int elementType); + void giroIndexUpdated(const QList& data); void giroDataUpdated(const GIRO::GIROStationData& data); void mufUpdated(const QJsonDocument& document); void foF2Updated(const QJsonDocument& document); diff --git a/plugins/feature/map/mapguiwebengine.ui b/plugins/feature/map/mapguiwebengine.ui index 3bfb11781..49dedf0f0 100644 --- a/plugins/feature/map/mapguiwebengine.ui +++ b/plugins/feature/map/mapguiwebengine.ui @@ -424,6 +424,20 @@
+ + + + Save to .kml + + + + + + + :/save.png:/save.png + + + diff --git a/plugins/feature/map/mapitem.cpp b/plugins/feature/map/mapitem.cpp index bfe9b1728..644edebc3 100644 --- a/plugins/feature/map/mapitem.cpp +++ b/plugins/feature/map/mapitem.cpp @@ -96,6 +96,11 @@ void ObjectMapItem::update(SWGSDRangel::SWGMapItem *mapItem) updateTrack(mapItem->getTrack()); updatePredictedTrack(mapItem->getPredictedTrack()); } + if (mapItem->getAvailableFrom()) { + m_availableFrom = QDateTime::fromString(*mapItem->getAvailableFrom(), Qt::ISODateWithMs); + } else { + m_availableFrom = QDateTime(); + } if (mapItem->getAvailableUntil()) { m_availableUntil = QDateTime::fromString(*mapItem->getAvailableUntil(), Qt::ISODateWithMs); } else { diff --git a/plugins/feature/map/mapitem.h b/plugins/feature/map/mapitem.h index f49b9e1c3..e76406bd7 100644 --- a/plugins/feature/map/mapitem.h +++ b/plugins/feature/map/mapitem.h @@ -61,6 +61,7 @@ protected: float m_latitude; // Position for label float m_longitude; float m_altitude; // In metres + QDateTime m_availableFrom; // Date & time this item is visible from. Invalid date/time is forever QDateTime m_availableUntil; // Date & time this item is visible until (for 3D map). Invalid date/time is forever }; diff --git a/plugins/feature/map/mapsettings.cpp b/plugins/feature/map/mapsettings.cpp index 577d7e769..4ae6b6cbc 100644 --- a/plugins/feature/map/mapsettings.cpp +++ b/plugins/feature/map/mapsettings.cpp @@ -41,6 +41,7 @@ const QStringList MapSettings::m_pipeTypes = { QStringLiteral("Radiosonde"), QStringLiteral("StarTracker"), QStringLiteral("SatelliteTracker"), + QStringLiteral("SID"), QStringLiteral("VORLocalizer") }; @@ -57,6 +58,7 @@ const QStringList MapSettings::m_pipeURIs = { QStringLiteral("sdrangel.feature.radiosonde"), QStringLiteral("sdrangel.feature.startracker"), QStringLiteral("sdrangel.feature.satellitetracker"), + QStringLiteral("sdrangel.feature.sid"), QStringLiteral("sdrangel.feature.vorlocalizer") }; @@ -125,6 +127,7 @@ MapSettings::MapSettings() : stationSettings->m_display3DTrack = false; m_itemSettings.insert("Station", stationSettings); m_itemSettings.insert("VORLocalizer", new MapItemSettings("VORLocalizer", true, QColor(255, 255, 0), false, true, 11)); + m_itemSettings.insert("SID", new MapItemSettings("SID", true, QColor(255, 255, 0), false, true, 3)); MapItemSettings *ionosondeItemSettings = new MapItemSettings("Ionosonde Stations", true, QColor(255, 255, 0), false, true, 4); ionosondeItemSettings->m_display2DIcon = false; diff --git a/plugins/feature/map/mapsettingsdialog.cpp b/plugins/feature/map/mapsettingsdialog.cpp index 062e27db3..7ec0f9c88 100644 --- a/plugins/feature/map/mapsettingsdialog.cpp +++ b/plugins/feature/map/mapsettingsdialog.cpp @@ -28,66 +28,10 @@ #endif #include "util/units.h" +#include "gui/colordialog.h" #include "mapsettingsdialog.h" #include "maplocationdialog.h" -#include "mapcolordialog.h" - -static QString rgbToColor(quint32 rgb) -{ - QColor color = QColor::fromRgba(rgb); - return QString("%1,%2,%3").arg(color.red()).arg(color.green()).arg(color.blue()); -} - -static QString backgroundCSS(quint32 rgb) -{ - // Must specify a border, otherwise we end up with a gradient instead of solid background - return QString("QToolButton { background-color: rgb(%1); border: none; }").arg(rgbToColor(rgb)); -} - -static QString noColorCSS() -{ - return "QToolButton { background-color: black; border: none; }"; -} - -MapColorGUI::MapColorGUI(QTableWidget *table, int row, int col, bool noColor, quint32 color) : - m_noColor(noColor), - m_color(color) -{ - m_colorButton = new QToolButton(table); - m_colorButton->setFixedSize(22, 22); - if (!m_noColor) - { - m_colorButton->setStyleSheet(backgroundCSS(m_color)); - } - else - { - m_colorButton->setStyleSheet(noColorCSS()); - m_colorButton->setText("-"); - } - table->setCellWidget(row, col, m_colorButton); - connect(m_colorButton, &QToolButton::clicked, this, &MapColorGUI::on_color_clicked); -} - -void MapColorGUI::on_color_clicked() -{ - MapColorDialog dialog(QColor::fromRgba(m_color), m_colorButton); - if (dialog.exec() == QDialog::Accepted) - { - m_noColor = dialog.noColorSelected(); - if (!m_noColor) - { - m_colorButton->setText(""); - m_color = dialog.selectedColor().rgba(); - m_colorButton->setStyleSheet(backgroundCSS(m_color)); - } - else - { - m_colorButton->setText("-"); - m_colorButton->setStyleSheet(noColorCSS()); - } - } -} MapItemSettingsGUI::MapItemSettingsGUI(QTableWidget *table, int row, MapSettings::MapItemSettings *settings) : m_track2D(table, row, MapSettingsDialog::COL_2D_TRACK, !settings->m_display2DTrack, settings->m_2DTrackColor), diff --git a/plugins/feature/map/mapsettingsdialog.h b/plugins/feature/map/mapsettingsdialog.h index f6fe3af64..5e86daa1d 100644 --- a/plugins/feature/map/mapsettingsdialog.h +++ b/plugins/feature/map/mapsettingsdialog.h @@ -27,6 +27,7 @@ #include #include "gui/httpdownloadmanagergui.h" +#include "gui/tablecolorchooser.h" #include "util/openaip.h" #include "util/ourairportsdb.h" #include "util/waypoints.h" @@ -34,34 +35,15 @@ #include "ui_mapsettingsdialog.h" #include "mapsettings.h" -class MapColorGUI : public QObject { - Q_OBJECT -public: - - MapColorGUI(QTableWidget *table, int row, int col, bool noColor, quint32 color); - -public slots: - void on_color_clicked(); - -private: - QToolButton *m_colorButton; - -public: - // Have copies of settings, so we don't change unless main dialog is accepted - bool m_noColor; - quint32 m_color; - -}; - class MapItemSettingsGUI : public QObject { Q_OBJECT public: MapItemSettingsGUI(QTableWidget *table, int row, MapSettings::MapItemSettings *settings); - MapColorGUI m_track2D; - MapColorGUI m_point3D; - MapColorGUI m_track3D; + TableColorChooser m_track2D; + TableColorChooser m_point3D; + TableColorChooser m_track3D; QSpinBox *m_minZoom; QSpinBox *m_minPixels; QDoubleSpinBox *m_labelScale; diff --git a/plugins/feature/map/readme.md b/plugins/feature/map/readme.md index 3969ec24f..a1de95cd4 100644 --- a/plugins/feature/map/readme.md +++ b/plugins/feature/map/readme.md @@ -14,10 +14,11 @@ On top of this, it can plot data from other plugins, such as: * Weather balloons from the Radiosonde feature, * RF Heat Maps from the Heap Map channel, * Radials and estimated position from the VOR localizer feature, -* ILS course line and glide path from the ILS Demodulator. -* DSC geographic call areas. +* ILS course line and glide path from the ILS Demodulator, +* DSC geographic call areas, +* SID paths. -As well as internet data sources: +As well as internet and built-in data sources: * AM, FM and DAB transmitters in the UK and DAB transmitters in France, * Airports, NavAids and airspaces, @@ -40,7 +41,7 @@ It can also create tracks showing the path aircraft, ships, radiosondes and APRS ![3D Map feature](../../../doc/img/Map_plugin_apt.png) -3D Models are not included with SDRangel. They must be downloaded by pressing the Download 3D Models button in the Display Settings dialog (20). +3D Models are not included with SDRangel. They must be downloaded by pressing the Download 3D Models button in the Display Settings dialog (21).

Interface

@@ -143,14 +144,14 @@ This is only supported on 2D raster maps and the 3D map.

11: Display MUF Contours

When checked, contours will be downloaded and displayed on the 3D map, showing the MUF (Maximum Usable Frequency) for a 3000km path that reflects off the ionosphere. -The contours will be updated every 15 minutes. The latest contour data will always be displayed, irrespective of the time set on the 3D Map. +The contours will be updated every 15 minutes. MUF contour data is available for the preceeding 5 days. ![MUF contours](../../../doc/img/Map_plugin_muf.png)

12: Display coF2 Contours

When checked, contours will be downloaded and displayed on the 3D map, showing coF2 (F2 layer critical frequency), the maximum frequency at which radio waves will be reflected vertically from the F2 region of the ionosphere. -The contours will be updated every 15 minutes. The latest contour data will always be displayed, irrespective of the time set on the 3D Map. +The contours will be updated every 15 minutes. coF2 contour data is available for the preceeding 5 days.

13: Display NASA GIBS Data

@@ -185,11 +186,19 @@ When checked, displays the track (taken or predicted) for the selected object. When checked, displays the track (taken or predicted) for the all objects. -

19: Delete

+

19: Save to .kml

+ +When clicked, items and tracks on the map will be saved to a [KML](https://en.wikipedia.org/wiki/Keyhole_Markup_Language) (.kml or .kmz) file, for use in other applications. + +Note that the KML format requires 3D models in the Collada (.dae) format. However, SDRangel's models are in glTF (.glb or .gltf) format. +If you wish to view the models in a KML viewer, you will need to manually convert them. Note that you should still be able to view tracks without the models. +Note that the .glbe files cannot be converted to .dae. + +

20: Delete

When clicked, all items will be deleted from the map. -

20: Display settings

+

21: Display settings

When clicked, opens the Map Display Settings dialog: @@ -281,6 +290,17 @@ MUF and foF2 can be displayed as contours: The contours can be clicked on which will display the data for that contour in the info box. +

VLF Transmitters

+ +The Map contains a built-in list of VLF transmitters. This can be overridden by a user-defined list contained in a file `vlftransmitters.csv` in the application data directory. + +The file must have the following columns: + +``` +Callsign,Frequency,Latitude,Longitude,Power +GQD,19580,54.911643,-3.278456,10 +``` +

Attribution

IARU Region 1 beacon list used with permission from: https://iaru-r1-c5-beacons.org/ To add or update a beacon, see: https://iaru-r1-c5-beacons.org/index.php/beacon-update/ diff --git a/plugins/feature/map/webserver.cpp b/plugins/feature/map/webserver.cpp index 546697e36..3a2eb2924 100644 --- a/plugins/feature/map/webserver.cpp +++ b/plugins/feature/map/webserver.cpp @@ -53,7 +53,7 @@ void WebServer::incomingConnection(qintptr socket) // Don't include leading or trailing / in from void WebServer::addPathSubstitution(const QString &from, const QString &to) { - qDebug() << "Mapping " << from << " to " << to; + //qDebug() << "Mapping " << from << " to " << to; m_pathSubstitutions.insert(from, to); } @@ -125,7 +125,7 @@ void WebServer::readClient() if (socket->canReadLine()) { QString line = socket->readLine(); - qDebug() << "WebServer HTTP Request: " << line; + //qDebug() << "WebServer HTTP Request: " << line; QStringList tokens = QString(line).split(QRegularExpression("[ \r\n][ \r\n]*")); if (tokens[0] == "GET") diff --git a/plugins/feature/satellitetracker/satelliteselectiondialog.cpp b/plugins/feature/satellitetracker/satelliteselectiondialog.cpp index 3184be204..73751a8f2 100644 --- a/plugins/feature/satellitetracker/satelliteselectiondialog.cpp +++ b/plugins/feature/satellitetracker/satelliteselectiondialog.cpp @@ -197,6 +197,13 @@ void SatelliteSelectionDialog::displaySatInfo(const QString& name) { SatNogsSatellite *sat = m_satellites[name]; m_satInfo = sat; + if (!sat) + { + // Might not be null if satellite name entered via API + ui->satInfo->setText(""); + ui->satImage->setPixmap(QPixmap()); + return; + } QStringList info; info.append(QString("Name: %1").arg(sat->m_name)); if (sat->m_names.size() > 0) diff --git a/plugins/feature/satellitetracker/satellitetrackersettings.cpp b/plugins/feature/satellitetracker/satellitetrackersettings.cpp index a703c2999..0c1866756 100644 --- a/plugins/feature/satellitetracker/satellitetrackersettings.cpp +++ b/plugins/feature/satellitetracker/satellitetrackersettings.cpp @@ -26,7 +26,7 @@ #include "satellitetrackersettings.h" #define DEAFULT_TARGET "ISS" -#define DEFAULT_TLES {"https://db.satnogs.org/api/tle/", "https://www.amsat.org/tle/current/nasabare.txt", "http://celestrak.org/NORAD/elements/gp.php?GROUP=weather&FORMAT=tle", "https://celestrak.org/NORAD/elements/gp.php?GROUP=gps-ops&FORMAT=tle"} +#define DEFAULT_TLES {"https://db.satnogs.org/api/tle/", "https://www.amsat.org/tle/current/nasabare.txt", "http://celestrak.org/NORAD/elements/gp.php?GROUP=weather&FORMAT=tle", "https://celestrak.org/NORAD/elements/gp.php?GROUP=gps-ops&FORMAT=tle", "https://celestrak.org/NORAD/elements/gp.php?CATNR=36395&FORMAT=tle"} #define DEFAULT_DATE_FORMAT "yyyy/MM/dd" #define DEFAULT_AOS_SPEECH "${name} is visible for ${duration} minutes. Max elevation, ${elevation} degrees." #define DEFAULT_LOS_SPEECH "${name} is no longer visible." diff --git a/plugins/feature/sid/CMakeLists.txt b/plugins/feature/sid/CMakeLists.txt new file mode 100644 index 000000000..e24703036 --- /dev/null +++ b/plugins/feature/sid/CMakeLists.txt @@ -0,0 +1,72 @@ +project(sid) + +set(sid_SOURCES + sid.cpp + sidsettings.cpp + sidplugin.cpp + sidwebapiadapter.cpp + sidworker.cpp +) + +set(sid_HEADERS + sid.h + sidsettings.h + sidplugin.h + sidwebapiadapter.h + sidworker.h +) + +include_directories( + ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client +) + +if(NOT SERVER_MODE) + set(sid_SOURCES + ${sid_SOURCES} + sidgui.cpp + sidgui.ui + sidsettingsdialog.cpp + sidsettingsdialog.ui + icons.qrc + ) + set(sid_HEADERS + ${sid_HEADERS} + sidgui.h + sidsettingsdialog.h + ) + + set(TARGET_NAME featuresid) + set(TARGET_LIB Qt::Widgets Qt::Charts Qt::Multimedia Qt::MultimediaWidgets) + set(TARGET_LIB_GUI "sdrgui") + set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR}) +else() + set(TARGET_NAME featuresidsrv) + set(TARGET_LIB "") + set(TARGET_LIB_GUI "") + set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR}) +endif() + +add_library(${TARGET_NAME} SHARED + ${sid_SOURCES} +) + +target_link_libraries(${TARGET_NAME} + Qt::Core + ${TARGET_LIB} + sdrbase + ${TARGET_LIB_GUI} +) + +install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER}) + +if(WIN32) + # Run deployqt for MultimediaWidgets, which isn't used in other plugins + include(DeployQt) + windeployqt(${TARGET_NAME} ${SDRANGEL_BINARY_BIN_DIR} "") +endif() + + +# Install debug symbols +if (WIN32) + install(FILES $ CONFIGURATIONS Debug RelWithDebInfo DESTINATION ${INSTALL_FOLDER} ) +endif() diff --git a/plugins/feature/sid/icons.qrc b/plugins/feature/sid/icons.qrc new file mode 100644 index 000000000..b766f4754 --- /dev/null +++ b/plugins/feature/sid/icons.qrc @@ -0,0 +1,16 @@ + + + icons/sun.png + icons/chartcombined.png + icons/chartseparate.png + icons/legend.png + icons/xlp.svg + icons/xls.svg + icons/xsp.svg + icons/xss.svg + icons/delta.svg + icons/gamma.svg + icons/proton.svg + icons/solar-orbiter.svg + + diff --git a/plugins/feature/sid/icons/chartcombined.png b/plugins/feature/sid/icons/chartcombined.png new file mode 100644 index 000000000..caab87f92 Binary files /dev/null and b/plugins/feature/sid/icons/chartcombined.png differ diff --git a/plugins/feature/sid/icons/chartseparate.png b/plugins/feature/sid/icons/chartseparate.png new file mode 100644 index 000000000..3548c0fca Binary files /dev/null and b/plugins/feature/sid/icons/chartseparate.png differ diff --git a/plugins/feature/sid/icons/delta.svg b/plugins/feature/sid/icons/delta.svg new file mode 100644 index 000000000..53e96bb9b --- /dev/null +++ b/plugins/feature/sid/icons/delta.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + diff --git a/plugins/feature/sid/icons/gamma.svg b/plugins/feature/sid/icons/gamma.svg new file mode 100644 index 000000000..90de948d4 --- /dev/null +++ b/plugins/feature/sid/icons/gamma.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + diff --git a/plugins/feature/sid/icons/legend.png b/plugins/feature/sid/icons/legend.png new file mode 100644 index 000000000..dbef748b5 Binary files /dev/null and b/plugins/feature/sid/icons/legend.png differ diff --git a/plugins/feature/sid/icons/proton.svg b/plugins/feature/sid/icons/proton.svg new file mode 100644 index 000000000..fca02154c --- /dev/null +++ b/plugins/feature/sid/icons/proton.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/feature/sid/icons/solar-orbiter.svg b/plugins/feature/sid/icons/solar-orbiter.svg new file mode 100644 index 000000000..6c5373978 --- /dev/null +++ b/plugins/feature/sid/icons/solar-orbiter.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/feature/sid/icons/sun.png b/plugins/feature/sid/icons/sun.png new file mode 100644 index 000000000..e1d0824de Binary files /dev/null and b/plugins/feature/sid/icons/sun.png differ diff --git a/plugins/feature/sid/icons/xlp.svg b/plugins/feature/sid/icons/xlp.svg new file mode 100644 index 000000000..fb2d86505 --- /dev/null +++ b/plugins/feature/sid/icons/xlp.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/feature/sid/icons/xls.svg b/plugins/feature/sid/icons/xls.svg new file mode 100644 index 000000000..6f2fc9919 --- /dev/null +++ b/plugins/feature/sid/icons/xls.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/feature/sid/icons/xsp.svg b/plugins/feature/sid/icons/xsp.svg new file mode 100644 index 000000000..e3cb553a1 --- /dev/null +++ b/plugins/feature/sid/icons/xsp.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/feature/sid/icons/xss.svg b/plugins/feature/sid/icons/xss.svg new file mode 100644 index 000000000..adf398da0 --- /dev/null +++ b/plugins/feature/sid/icons/xss.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/feature/sid/readme.md b/plugins/feature/sid/readme.md new file mode 100644 index 000000000..3796d8920 --- /dev/null +++ b/plugins/feature/sid/readme.md @@ -0,0 +1,274 @@ +

SID Feature Plugin

+ +

Introduction

+ +The SID feature displays a chart that plots channel power vs time, which can be useful for detecting solar flares, CMEs (Coronal Mass Ejections) +and GRBs (Gamma Ray Bursts) via SIDs (Sudden Ionospheric Disturbances). +The signal source for which power is measured should typically be a VLF signal from a distant transmitter with near constant day-time power output, +such as the VLF transmitters that are used by navies to communicate with submarines +(E.g. [GQD](https://en.wikipedia.org/wiki/Anthorn_Radio_Station) / [HWU](https://en.wikipedia.org/wiki/HWU_transmitter) / [NAA](https://en.wikipedia.org/wiki/VLF_Transmitter_Cutler)). +This can be measured within SDRangel using the [Channel Power](../../channelrx/channelpower/readme.md) plugin. + +When a solar flare occurs, EUV (Extreme Ultraviolet) and X-ray radiation is emitted from the Sun. When the radiation reaches the Earth's atmosphere (after ~8 minutes), +it can increase the ionization of the D and E regions in the ionosphere, enhancing VLF propagation. Gamma-rays from powerful GRBs can also have a similar effect on the ionosphere. +The enhancement of the VLF sky-wave can interfere with the ground-wave, causing constructive or destructive interference depending on the phase. If strong enough, this can be observed +in the plot of received power vs time. + +The SID chart can plot multiple series, allowing different signals from different transmitters to be monitored. +This can be useful as SIDs can be localized to specific regions in the atmosphere, thus not all signals may be affected. +Data can come from multiple [Channel Power](../../channelrx/channelpower/readme.md) plugins within a single device, or separate devices. + +To help determine the cause of a SID, addtional data can be plotted from a variety of sources: + +* the chart can plot X-ray data from the GOES satellites, to allow visual correlation of spikes in the X-ray flux measurement with spikes in the VLF power measurements, +* it can display images and video from the Solar Dynamics Observatory at EUV wavelengths, which may visually show the solar flare, +* it can display GRB events on the chart, measured by satellites such as Fermi and Swift, +* it can display solar flare events detected by the STIX X-ray instrument on the Solar Orbiter satellite, +* it can display proton flux measured by the GOES satellites, +* it can control the time in a 3D [Map](../../feature/map/readme.md), to see the corresponding effect on MUF (Maximum Usable Frequency) and foF2 (F2 layer critical frequency). + +The SID feature can record power from any RX channel plugin that has a channelPowerDB value in its channel report, so can be used for recording and plotting power vs time for purposes other than SID monitoring. + +![SID feature plugin](../../../doc/img/SID_plugin.jpg) + +

Interface

+ +![SID feature plugin GUI](../../../doc/img/SID_plugin_settings.png) + +

1: Start/stop

+ +Press to start/stop collection and plotting of data. + +

2: Open .csv

+ +Press to open a .csv file to read data from. + +

3: Save to .csv

+ +Press to select a .csv file to write data to. + +

4: Save chart to image

+ +Press to save the chart to a .png or .jpg file. + +

5: Clear all data

+ +Press to clear all data. + +

6: Average

+ +Number of samples to use in a moving average filter that can be applied to the data. Set to 1 for no filtering. + +

7: Display Primary Long Wavelength X-Ray Data

+ +Check to display long wavelength (0.1-0.8nm) X-Ray data from the primary GOES satellite (Currently GOES 16) on the chart. + +This is probably the most useful data in order to see when a solar flare has occured, as there will typically be a sharp peak. +The GOES satellites are in a geostationary orbit around the Earth, so the measured increase in X-ray flux from a flare will be approximately 8 minutes +after it has occured. +The Y-axis indicates the flare classification. M and X class flares are those most likely to have a measurable impact on the ionosphere. + +![X-Ray data showing M class flare](../../../doc/img/SID_plugin_xray.png) + +

8: Display Secondary Long Wavelength X-Ray Data

+ +Check to display long wavelength (0.1-0.8nm) X-Ray data from the secondary GOES satellite (Currently GOES 18) on the chart. +Data from the secondary satellite may be useful when the primary is unavailable, such as when it is in eclipse. +In the following plot we can see the primary and secondary data is nearly identical, apart from where there are dropouts +while in eclipse: + +![X-Ray data during eclipse](../../../doc/img/SID_plugin_eclipse.png) + +

9: Display Primary Short Wavelength X-Ray Data

+ +Check to display short wavelength (0.1-0.8nm) X-Ray data from the primary GOES satellite (Currently GOES 16) on the chart. + +

10: Display Secondary Short Wavelength X-Ray Data

+ +Check to display short wavelength (0.05-0.4nm) X-Ray data from the secondary GOES satellite (Currently GOES 18) on the chart. +Data from the secondary satellite may be useful when the primary is in eclipse. + +

11: Display Proton Flux

+ +Check to display 10 MeV and 100 MeV proton flux measurements from the primary GOES satellite on the chart. +A peak in the proton flux can occur one to three days after a CME (Coronal Mass Ejection) is directed towards Earth. +Whereas X-rays from flares can impact any part of the ionosphere that is facing the sun, the Earth's magnetosphere typically directs +the particles in the CME towards the poles, so a corresponding SID is most likely to be detected if you are receiving +a signal from a transmitter crossing the polar region. + +

12: Display GRBs

+ +Check to display Gamma Ray Bursts (GRB) on the chart. GRBs are plotted as a scatter plot. You can right click on a GRB to display the context +menu, which contains a number of links to additional data from the Fermi satellite for the GRB. The GRB data is not realtime, and it may take +up to 7 days for a GRB to appear in the data, so this is typically only useful for the analysis of historical data. +The context menu also has an item to display the location of the GRB in the [Sky Map](../../feature/skymap/readme.md) feature. + +

13: Display Solar Flares

+ +Check to display solar flares on the chart as record by the STIX X-ray instrument on the Solar Orbiter satellite. +You can right click on a solar flare to display the context menu, which contains a number of links to additional data from the STIX instrument. +The solar flare data is not realtime and can sometimes be delayed by 24 hours. + +

14: Combined or Separate Charts

+ +When unchecked, data from [Channel Power](../../channelrx/channelpower/readme.md) plugins is displayed on a separate chart to other data such as X-ray and proton flux and GRBs. +When checked, all data is displayed on a single combined chart. + +

15: Display Legend

+ +Check to display a legend on the chart. When unchecked the legend will be hidden. You can click on items in the legend to temporarily hide and then show the corresponding series on the chart. +The position of the legend can be set in the Settings Dialog. + +

16: Open Settings Dialog

+ +Click to open the Settings Dialog. The settings dialog allows a user to: + +- Select which channels data is recorded from. +- What colours are used for the data series. +- What labels will be used for the series. +- Whether auto-save is enabled. When auto-save is enabled, data will be automatically saved as the specified interval. +- Whether auto-load is enabled. When auto-load is enabled, auto-save data will be automatically loaded when the SID feature is opened. +- The filename is use for auto-save. +- How often, in minutes, the data is auto-saved. +- Where the chart legend should be positioned. + +![SID settings dialog](../../../doc/img/SID_plugin_settings_dialog.png) + +

17: Display SDO/SOHO Imagery

+ +When checked, displays imagary from NASA's SDO (Solar Dynamic Observatory) and ESA/NASA's SOHO (Solar and Heliospheric Observatory) satellites. + +SDOs images the Sun in a variety of UV and EUV wavelengths. SOHO shows images of the solar corona. The images are near real-time, updated every 15 minutes. + +Solar flares are particularly visible in the AIA 131 Å images. + +

18: Image or Video Selection

+ +Selects whether to display images (unchecked) or video (checked). + +

19: Image/Wavelength Selection

+ +Selects which image / wavelength to view. + +* AIA 94 Å to 1700 Å - The AIA (Atmospheric Imaging Assembly) images the solar atmosphere at multiple EUV (Extreme Ultraviolet) and UV (Ultraviolet) wavelengths: + +| Band | Region | +|---------|-----------------------------------------| +| 94 Å | Flaring | +| 131 Å | Flaring | +| 171 Å | Quiet corona, upper transition region | +| 193 Å | Corona and hot flare plasma | +| 211 Å | Active corona | +| 304 Å | Chromosphere, transition region | +| 335 Å | Active corona | +| 1600 Å | Transition region, upper photoshere | +| 1700 Å | Temperature minimum, photosphere | + +[Ref](https://sdo.gsfc.nasa.gov/data/channels.php) + +* MHI Magnetogram - HMI (Helioseismic and Magnetic Imager) Magnetogram shows the magnetic field in the photosphere, with black and white indicating opposite polarities. +* MHI Intensitygram - Brightness in a visible light band (6173 Å - Red - Iron spectral line), useful for observing sun spots. +* Dopplergram - Shows velocities along the line-of-sight. + +* LASCO (Large Angle Spectrometric Coronagraph) shows solar corona. C2 shows corona up to 8.4Mkm. C3 shows corona up to 23Mkm. + +

20: Show GOES 16, 18 and SDO

+ +When checked, opens a [Satellite Tracker](../../feature/satellitetracker/readme.md) feature and sets it to display data for the GOES 16, GOES 18 and SDO satellites. +The position and tracks of the satellites will then be visible on a [Map](../../feature/map/readme.md) feature. + +

21: Autoscale X

+ +When clicked, the chart X-axis is automatically scaled so that all power data is visible. When right-clicked, autoscaling of the X-axis will occur whenever new data is added to the chart. + +

22: Autoscale Y

+ +When clicked, the chart Y-axis is automatically scaled so that all power data is visible. When right-clicked, autoscaling of the Y-axis will occur whenever new data is added to the chart. + +

23: Set X-axis to Today

+ +When clicked, the X-axis is set to show today, from midnight to midnight. + +When right-clicked, the X-axis is set to show sunrise to sunset. This uses latitude and longitude from Preferences > My position. + +

24: Set X-axis to -1 day

+ +When clicked, the X-axis is set 1 day earlier than the current setting, at the same time. + +

25: Set X-axis to +1 day

+ +When clicked, the X-axis is set 1 day later than the current setting, at the same time. + +

26: Start Time

+ +Displays/sets the current start time of the chart (X-axis minimum). It's possible to scroll through hours/days/months by clicking on the relevent segment and using the mouse scroll wheel. + +

27: End Time

+ +Displays/sets the current end time of the chart (X-axis maximum). It's possible to scroll through hours/days/months by clicking on the relevent segment and using the mouse scroll wheel. + +

28: Min

+ +Displays/sets the minimum Y-axis value. + +

29: Max

+ +Displays/sets the maximum Y-axis value. + +

30: Now

+ +When checked, the latest SDO imagery is displayed. When unchecked, you can enter a date and time for which imagery should be displayed. + +

31: Date Time

+ +Specifies the date and time for which SDO imagery should be displayed. Images are updated every 15 minutes. The date and time can also be set by clicking on the chart. + +

32: Map

+ +Select a Map to link to the SID feature. When a time is selected on the SID charts, the [Map](../../feature/map/readme.md) feature will have it's time set accordingly. +This allows you, for example, to see the corresponding impact on MUF/foF2 displayed on the 3D map. + +

33: Show Paths on Map

+ +When clicked, shows the great circle paths between transmitters and receivers on a [Map](../../feature/map/readme.md). + +![SID paths](../../../doc/img/SID_plugin_paths.png) + +The positions of the transmitters are taken from the Map's VLF database. The position of the receiver is for most devices taken from Preferences > My Position. +For KiwiSDRs, the position is taken from the GPS position indicated by the device. + +In order to match a transmitter in the Map's VLF database, the label used in the SID chart must match the transmitter's name. It is possible to add user-defined VLF transmitters via +a `vlftransmitters.csv` file. See the [Map](../../feature/map/readme.md) documentation. + +

Tips

+ +In order to check that a peak in the spectrum is a real VLF signal, you can: + +* If using a magnetic loop or other directional antenna, rotate it and make sure the amplitude varies, as mag loops should have a null orthogonal to the plane of the loop. +* Check that the signal has diurnal variation (it should vary with the time of day, due to the changes in the ionosphere). +* Check with online lists of VLF signals (E.g. https://sidstation.loudet.org/stations-list-en.xhtml or https://www.mwlist.org/vlf.php). A number of these are plotted on the [Map](../../feature/map/readme.md) feature. + +Occasionally, the X-ray flux data may drop to 0. This is typically when the GOES satellite is in eclipse (The Earth or moon is inbetween the satellite and the Sun). + +SIDs are most likely to be detected when it's day time in the path between the signal source and receiver, as at night, the atmosphere is shielded from the X-rays by the Earth. +Also, as the D layer in the ionosphere essentially disappears at night, the received power is not as constant as during the day. + +

Codecs

+ +You may need to install an mp4/h264 codec to view the SDO videos. + +On Windows, try [K-Lite Codecs](https://www.codecguide.com/download_k-lite_codec_pack_basic.htm). + +On Linux, install gstreamer libav. This can be installed on Ubuntu with: `sudo apt install gstreamer1.0-libav` + +

Attribution

+ +X-Ray and proton data is from [NOAA](https://www.swpc.noaa.gov/products/goes-x-ray-flux). + +Solar images are from [SDO | Solar Dynamics Observatory](https://sdo.gsfc.nasa.gov/). + +Corona images are from [SOHO](https://soho.nascom.nasa.gov/home.html). + +GRB data is from [GRBweb](https://user-web.icecube.wisc.edu/~grbweb_public/index.html). + +Solar flare data is from [Solar Orbiter STIX Data Center](https://datacenter.stix.i4ds.net/). diff --git a/plugins/feature/sid/sid.cpp b/plugins/feature/sid/sid.cpp new file mode 100644 index 000000000..e655a69c2 --- /dev/null +++ b/plugins/feature/sid/sid.cpp @@ -0,0 +1,361 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 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 "SWGFeatureSettings.h" +#include "SWGDeviceState.h" + +#include "dsp/dspengine.h" +#include "device/deviceset.h" +#include "feature/featureset.h" +#include "settings/serializable.h" +#include "maincore.h" + +#include "sid.h" +#include "sidworker.h" + +MESSAGE_CLASS_DEFINITION(SIDMain::MsgConfigureSID, Message) +MESSAGE_CLASS_DEFINITION(SIDMain::MsgStartStop, Message) +MESSAGE_CLASS_DEFINITION(SIDMain::MsgReportWorker, Message) +MESSAGE_CLASS_DEFINITION(SIDMain::MsgMeasurement, Message) + +const char* const SIDMain::m_featureIdURI = "sdrangel.feature.sid"; +const char* const SIDMain::m_featureId = "SID"; + +SIDMain::SIDMain(WebAPIAdapterInterface *webAPIAdapterInterface) : + Feature(m_featureIdURI, webAPIAdapterInterface), + m_thread(nullptr), + m_worker(nullptr) +{ + qDebug("SIDMain::SID: webAPIAdapterInterface: %p", webAPIAdapterInterface); + setObjectName(m_featureId); + m_state = StIdle; + m_errorMessage = "SID error"; + m_networkManager = new QNetworkAccessManager(); + QObject::connect( + m_networkManager, + &QNetworkAccessManager::finished, + this, + &SIDMain::networkManagerFinished + ); +} + +SIDMain::~SIDMain() +{ + QObject::disconnect( + m_networkManager, + &QNetworkAccessManager::finished, + this, + &SIDMain::networkManagerFinished + ); + delete m_networkManager; +} + +void SIDMain::start() +{ + qDebug("SIDMain::start"); + m_thread = new QThread(); + m_worker = new SIDWorker(this, m_webAPIAdapterInterface); + m_worker->moveToThread(m_thread); + QObject::connect(m_thread, &QThread::started, m_worker, &SIDWorker::startWork); + QObject::connect(m_thread, &QThread::finished, m_worker, &QObject::deleteLater); + QObject::connect(m_thread, &QThread::finished, m_thread, &QThread::deleteLater); + m_worker->setMessageQueueToFeature(getInputMessageQueue()); + m_worker->setMessageQueueToGUI(getMessageQueueToGUI()); + m_thread->start(); + m_state = StRunning; + MsgConfigureSID *msg = MsgConfigureSID::create(m_settings, QList(), true); + m_worker->getInputMessageQueue()->push(msg); +} + +void SIDMain::stop() +{ + qDebug("SIDMain::stop"); + m_state = StIdle; + if (m_thread) + { + m_thread->quit(); + m_thread->wait(); + m_thread = nullptr; + m_worker = nullptr; + } +} + +bool SIDMain::handleMessage(const Message& cmd) +{ + if (MsgConfigureSID::match(cmd)) + { + MsgConfigureSID& cfg = (MsgConfigureSID&) cmd; + qDebug() << "SIDMain::handleMessage: MsgConfigureSID"; + applySettings(cfg.getSettings(), cfg.getSettingsKeys(), cfg.getForce()); + + return true; + } + else if (MsgStartStop::match(cmd)) + { + MsgStartStop& cfg = (MsgStartStop&) cmd; + qDebug() << "SIDMain::handleMessage: MsgStartStop: start:" << cfg.getStartStop(); + + if (cfg.getStartStop()) { + start(); + } else { + stop(); + } + + return true; + } + else if (MsgReportWorker::match(cmd)) + { + MsgReportWorker& report = (MsgReportWorker&) cmd; + m_state = StError; + m_errorMessage = report.getMessage(); + return true; + } + else + { + return false; + } +} + +QByteArray SIDMain::serialize() const +{ + return m_settings.serialize(); +} + +bool SIDMain::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + MsgConfigureSID *msg = MsgConfigureSID::create(m_settings, QList(), true); + m_inputMessageQueue.push(msg); + return true; + } + else + { + m_settings.resetToDefaults(); + MsgConfigureSID *msg = MsgConfigureSID::create(m_settings, QList(), true); + m_inputMessageQueue.push(msg); + return false; + } +} + +void SIDMain::applySettings(const SIDSettings& settings, const QList& settingsKeys, bool force) +{ + qDebug() << "SIDMain::applySettings:" << settings.getDebugString(settingsKeys, force) << " force: " << force; + + + if (m_worker) + { + MsgConfigureSID *msg = MsgConfigureSID::create(settings, settingsKeys, force); + m_worker->getInputMessageQueue()->push(msg); + } + + if (settingsKeys.contains("useReverseAPI")) + { + bool fullUpdate = (settingsKeys.contains("useReverseAPI") && settings.m_useReverseAPI) || + settingsKeys.contains("reverseAPIAddress") || + settingsKeys.contains("reverseAPIPort") || + settingsKeys.contains("reverseAPIFeatureSetIndex") || + settingsKeys.contains("m_reverseAPIFeatureIndex"); + webapiReverseSendSettings(settingsKeys, settings, fullUpdate || force); + } + + if (force) { + m_settings = settings; + } else { + m_settings.applySettings(settingsKeys, settings); + } +} + +int SIDMain::webapiRun(bool run, + SWGSDRangel::SWGDeviceState& response, + QString& errorMessage) +{ + (void) errorMessage; + getFeatureStateStr(*response.getState()); + MsgStartStop *msg = MsgStartStop::create(run); + getInputMessageQueue()->push(msg); + return 202; +} + +int SIDMain::webapiSettingsGet( + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setSidSettings(new SWGSDRangel::SWGSIDSettings()); + response.getSidSettings()->init(); + webapiFormatFeatureSettings(response, m_settings); + return 200; +} + +int SIDMain::webapiSettingsPutPatch( + bool force, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + SIDSettings settings = m_settings; + webapiUpdateFeatureSettings(settings, featureSettingsKeys, response); + + MsgConfigureSID *msg = MsgConfigureSID::create(settings, featureSettingsKeys, force); + m_inputMessageQueue.push(msg); + + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureSID *msgToGUI = MsgConfigureSID::create(settings, featureSettingsKeys, force); + m_guiMessageQueue->push(msgToGUI); + } + + webapiFormatFeatureSettings(response, settings); + + return 200; +} + +void SIDMain::webapiFormatFeatureSettings( + SWGSDRangel::SWGFeatureSettings& response, + const SIDSettings& settings) +{ + if (response.getSidSettings()->getTitle()) { + *response.getSidSettings()->getTitle() = settings.m_title; + } else { + response.getSidSettings()->setTitle(new QString(settings.m_title)); + } + + response.getSidSettings()->setRgbColor(settings.m_rgbColor); + response.getSidSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0); + + if (response.getSidSettings()->getReverseApiAddress()) { + *response.getSidSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress; + } else { + response.getSidSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress)); + } + + response.getSidSettings()->setReverseApiPort(settings.m_reverseAPIPort); + response.getSidSettings()->setReverseApiFeatureSetIndex(settings.m_reverseAPIFeatureSetIndex); + response.getSidSettings()->setReverseApiFeatureIndex(settings.m_reverseAPIFeatureIndex); + + if (settings.m_rollupState) + { + if (response.getSidSettings()->getRollupState()) + { + settings.m_rollupState->formatTo(response.getSidSettings()->getRollupState()); + } + else + { + SWGSDRangel::SWGRollupState *swgRollupState = new SWGSDRangel::SWGRollupState(); + settings.m_rollupState->formatTo(swgRollupState); + response.getSidSettings()->setRollupState(swgRollupState); + } + } +} + +void SIDMain::webapiUpdateFeatureSettings( + SIDSettings& settings, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response) +{ + if (featureSettingsKeys.contains("title")) { + settings.m_title = *response.getSidSettings()->getTitle(); + } + if (featureSettingsKeys.contains("rgbColor")) { + settings.m_rgbColor = response.getSidSettings()->getRgbColor(); + } + if (featureSettingsKeys.contains("useReverseAPI")) { + settings.m_useReverseAPI = response.getSidSettings()->getUseReverseApi() != 0; + } + if (featureSettingsKeys.contains("reverseAPIAddress")) { + settings.m_reverseAPIAddress = *response.getSidSettings()->getReverseApiAddress(); + } + if (featureSettingsKeys.contains("reverseAPIPort")) { + settings.m_reverseAPIPort = response.getSidSettings()->getReverseApiPort(); + } + if (featureSettingsKeys.contains("reverseAPIFeatureSetIndex")) { + settings.m_reverseAPIFeatureSetIndex = response.getSidSettings()->getReverseApiFeatureSetIndex(); + } + if (featureSettingsKeys.contains("reverseAPIFeatureIndex")) { + settings.m_reverseAPIFeatureIndex = response.getSidSettings()->getReverseApiFeatureIndex(); + } + if (settings.m_rollupState && featureSettingsKeys.contains("rollupState")) { + settings.m_rollupState->updateFrom(featureSettingsKeys, response.getSidSettings()->getRollupState()); + } +} + +void SIDMain::webapiReverseSendSettings(const QList& featureSettingsKeys, const SIDSettings& settings, bool force) +{ + SWGSDRangel::SWGFeatureSettings *swgFeatureSettings = new SWGSDRangel::SWGFeatureSettings(); + // swgFeatureSettings->setOriginatorFeatureIndex(getIndexInDeviceSet()); + // swgFeatureSettings->setOriginatorFeatureSetIndex(getDeviceSetIndex()); + swgFeatureSettings->setFeatureType(new QString("SID")); + swgFeatureSettings->setSidSettings(new SWGSDRangel::SWGSIDSettings()); + SWGSDRangel::SWGSIDSettings *swgSIDSettings = swgFeatureSettings->getSidSettings(); + + // transfer data that has been modified. When force is on transfer all data except reverse API data + + if (featureSettingsKeys.contains("title") || force) { + swgSIDSettings->setTitle(new QString(settings.m_title)); + } + if (featureSettingsKeys.contains("rgbColor") || force) { + swgSIDSettings->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 SIDMain::networkManagerFinished(QNetworkReply *reply) +{ + QNetworkReply::NetworkError replyError = reply->error(); + + if (replyError) + { + qWarning() << "SIDMain::networkManagerFinished:" + << " error(" << (int) replyError + << "): " << replyError + << ": " << reply->errorString(); + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("SIDMain::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + } + + reply->deleteLater(); +} diff --git a/plugins/feature/sid/sid.h b/plugins/feature/sid/sid.h new file mode 100644 index 000000000..8e0e08bbd --- /dev/null +++ b/plugins/feature/sid/sid.h @@ -0,0 +1,188 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 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_SID_H_ +#define INCLUDE_FEATURE_SID_H_ + +#include +#include +#include + +#include "feature/feature.h" +#include "util/message.h" + +#include "sidsettings.h" + +class WebAPIAdapterInterface; +class QNetworkAccessManager; +class QNetworkReply; +class SIDWorker; + +namespace SWGSDRangel { + class SWGDeviceState; +} + +// There's a structure in winnt.h named SID +class SIDMain : public Feature +{ + Q_OBJECT +public: + class MsgConfigureSID : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const SIDSettings& getSettings() const { return m_settings; } + const QList& getSettingsKeys() const { return m_settingsKeys; } + bool getForce() const { return m_force; } + + static MsgConfigureSID* create(const SIDSettings& settings, const QList& settingsKeys, bool force) { + return new MsgConfigureSID(settings, settingsKeys, force); + } + + private: + SIDSettings m_settings; + QList m_settingsKeys; + bool m_force; + + MsgConfigureSID(const SIDSettings& settings, const QList& settingsKeys, bool force) : + Message(), + m_settings(settings), + m_settingsKeys(settingsKeys), + m_force(force) + { } + }; + + class MsgStartStop : public Message { + MESSAGE_CLASS_DECLARATION + + public: + bool getStartStop() const { return m_startStop; } + + static MsgStartStop* create(bool startStop) { + return new MsgStartStop(startStop); + } + + protected: + bool m_startStop; + + MsgStartStop(bool startStop) : + Message(), + m_startStop(startStop) + { } + }; + + class MsgReportWorker : public Message { + MESSAGE_CLASS_DECLARATION + + public: + QString getMessage() { return m_message; } + + static MsgReportWorker* create(QString message) { + return new MsgReportWorker(message); + } + + private: + QString m_message; + + MsgReportWorker(QString message) : + Message(), + m_message(message) + {} + }; + + class MsgMeasurement : public Message { + MESSAGE_CLASS_DECLARATION + + public: + QDateTime getDateTime() const { return m_dateTime; } + const QStringList& getIds() const { return m_ids; } + const QList& getMeasurements() const { return m_measurements; } + + static MsgMeasurement* create(QDateTime dateTime, const QStringList& ids, const QList& measurements) { + return new MsgMeasurement(dateTime, ids, measurements); + } + + private: + QDateTime m_dateTime; + QStringList m_ids; + QList m_measurements; + + MsgMeasurement(QDateTime dateTime, const QStringList& ids, const QList& measurements) : + Message(), + m_dateTime(dateTime), + m_ids(ids), + m_measurements(measurements) + {} + }; + + SIDMain(WebAPIAdapterInterface *webAPIAdapterInterface); + virtual ~SIDMain(); + virtual void destroy() { delete this; } + virtual bool handleMessage(const Message& cmd); + + virtual void getIdentifier(QString& id) const { id = objectName(); } + virtual QString getIdentifier() const { return objectName(); } + virtual void getTitle(QString& title) const { title = m_settings.m_title; } + + virtual QByteArray serialize() const; + virtual bool deserialize(const QByteArray& data); + + virtual int webapiRun(bool run, + SWGSDRangel::SWGDeviceState& response, + QString& errorMessage); + + virtual int webapiSettingsGet( + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage); + + static void webapiFormatFeatureSettings( + SWGSDRangel::SWGFeatureSettings& response, + const SIDSettings& settings); + + static void webapiUpdateFeatureSettings( + SIDSettings& settings, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response); + + static const char* const m_featureIdURI; + static const char* const m_featureId; + +private: + QThread *m_thread; + SIDWorker *m_worker; + SIDSettings m_settings; + + QNetworkAccessManager *m_networkManager; + QNetworkRequest m_networkRequest; + + void start(); + void stop(); + void applySettings(const SIDSettings& settings, const QList& settingsKeys, bool force = false); + void webapiReverseSendSettings(const QList& featureSettingsKeys, const SIDSettings& settings, bool force); + +private slots: + void networkManagerFinished(QNetworkReply *reply); +}; + +#endif // INCLUDE_FEATURE_SID_H_ diff --git a/plugins/feature/sid/sidgui.cpp b/plugins/feature/sid/sidgui.cpp new file mode 100644 index 000000000..a8a5a0102 --- /dev/null +++ b/plugins/feature/sid/sidgui.cpp @@ -0,0 +1,2526 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023-2024 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 "feature/featureuiset.h" +#include "feature/featurewebapiutils.h" +#include "channel/channelwebapiutils.h" +#include "gui/crightclickenabler.h" +#include "gui/basicfeaturesettingsdialog.h" +#include "gui/tabletapandhold.h" +#include "gui/dialogpositioner.h" +#include "mainwindow.h" +#include "device/deviceuiset.h" +#include "util/csv.h" +#include "util/astronomy.h" +#include "util/vlftransmitters.h" + +#include "ui_sidgui.h" +#include "sid.h" +#include "sidgui.h" +#include "sidsettingsdialog.h" + +#include "SWGMapItem.h" + +SIDGUI* SIDGUI::create(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature) +{ + SIDGUI* gui = new SIDGUI(pluginAPI, featureUISet, feature); + return gui; +} + +void SIDGUI::destroy() +{ + delete this; +} + +void SIDGUI::resetToDefaults() +{ + m_settings.resetToDefaults(); + displaySettings(); + applyAllSettings(); +} + +QByteArray SIDGUI::serialize() const +{ + return m_settings.serialize(); +} + +bool SIDGUI::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + m_feature->setWorkspaceIndex(m_settings.m_workspaceIndex); + displaySettings(); + applyAllSettings(); + return true; + } + else + { + resetToDefaults(); + return false; + } +} + +bool SIDGUI::handleMessage(const Message& message) +{ + if (SIDMain::MsgConfigureSID::match(message)) + { + qDebug("SIDGUI::handleMessage: SID::MsgConfigureSID"); + const SIDMain::MsgConfigureSID& cfg = (SIDMain::MsgConfigureSID&) message; + + if (cfg.getForce()) { + m_settings = cfg.getSettings(); + } else { + m_settings.applySettings(cfg.getSettingsKeys(), cfg.getSettings()); + } + + blockApplySettings(true); + displaySettings(); + blockApplySettings(false); + + return true; + } + else if (SIDMain::MsgMeasurement::match(message)) + { + // Measurements from SIDWorker + const SIDMain::MsgMeasurement& measurementsMsg = (SIDMain::MsgMeasurement&) message; + QDateTime dt = measurementsMsg.getDateTime(); + const QStringList& ids = measurementsMsg.getIds(); + const QList& measurements = measurementsMsg.getMeasurements(); + + for (int i = 0; i < ids.size(); i++) { + addMeasurement(ids[i], dt, measurements[i]); + } + return true; + } + + return false; +} + +void SIDGUI::handleInputMessages() +{ + Message* message; + + while ((message = getInputMessageQueue()->pop())) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +void SIDGUI::onWidgetRolled(QWidget* widget, bool rollDown) +{ + (void) widget; + (void) rollDown; + + RollupContents *rollupContents = getRollupContents(); + rollupContents->saveState(m_rollupState); + applySetting("rollupState"); +} + +SIDGUI::SIDGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent) : + FeatureGUI(parent), + ui(new Ui::SIDGUI), + m_pluginAPI(pluginAPI), + m_featureUISet(featureUISet), + m_doApplySettings(true), + m_lastFeatureState(0), + m_fileDialog(nullptr, "Select CSV file", "", "*.csv"), + m_chartXAxis(nullptr), + m_chartY1Axis(nullptr), + m_chartY2Axis(nullptr), + m_minMeasurement(std::numeric_limits::quiet_NaN()), + m_maxMeasurement(std::numeric_limits::quiet_NaN()), + m_xRayChartXAxis(nullptr), + m_xRayChartYAxis(nullptr), + m_goesXRay(nullptr), + m_solarDynamicsObservatory(nullptr), + m_player(nullptr), + m_grb(nullptr), + m_grbSeries(nullptr), + m_stix(nullptr), + m_stixSeries(nullptr), + m_availableFeatureHandler({"sdrangel.feature.map"}), + m_availableChannelHandler({}, "RM") +{ + m_feature = feature; + setAttribute(Qt::WA_DeleteOnClose, true); + m_helpURL = "plugins/feature/sid/readme.md"; + RollupContents *rollupContents = getRollupContents(); + ui->setupUi(rollupContents); + rollupContents->arrangeRollups(); + connect(rollupContents, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool))); + + m_sid = reinterpret_cast(feature); + m_sid->setMessageQueueToGUI(&m_inputMessageQueue); + + connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &))); + connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + + ui->startDateTime->blockSignals(true); + ui->endDateTime->blockSignals(true); + ui->startDateTime->setDateTime(QDateTime(QDate::currentDate(), QTime(0, 0, 0))); + ui->endDateTime->setDateTime(QDateTime(QDate::currentDate().addDays(1), QTime(0, 0, 0))); + ui->startDateTime->blockSignals(false); + ui->endDateTime->blockSignals(false); + + // Intialise chart + ui->chart->setRenderHint(QPainter::Antialiasing); + ui->xRayChart->setRenderHint(QPainter::Antialiasing); + + connect(&m_statusTimer, &QTimer::timeout, this, &SIDGUI::updateStatus); + m_statusTimer.start(250); + + connect(&m_autosaveTimer, &QTimer::timeout, this, &SIDGUI::autosave); + + m_settings.setRollupState(&m_rollupState); + + CRightClickEnabler *autoscaleXRightClickEnabler = new CRightClickEnabler(ui->autoscaleX); + connect(autoscaleXRightClickEnabler, &CRightClickEnabler::rightClick, this, &SIDGUI::autoscaleXRightClicked); + CRightClickEnabler *autoscaleYRightClickEnabler = new CRightClickEnabler(ui->autoscaleY); + connect(autoscaleYRightClickEnabler, &CRightClickEnabler::rightClick, this, &SIDGUI::autoscaleYRightClicked); + CRightClickEnabler *todayRightClickEnabler = new CRightClickEnabler(ui->today); + connect(todayRightClickEnabler, &CRightClickEnabler::rightClick, this, &SIDGUI::todayRightClicked); + + makeUIConnections(); // Enable connections before displaySettings, so autoscaling works + displaySettings(); + applyAllSettings(); + m_resizer.enableChildMouseTracking(); + + // Intialisation for Solar Dynamics Observatory image/video display + ui->sdoEnabled->setChecked(true); + ui->sdoProgressBar->setVisible(false); + ui->sdoImage->setStyleSheet("background-color: black;"); + ui->sdoVideo->setStyleSheet("background-color: black;"); + m_solarDynamicsObservatory = SolarDynamicsObservatory::create(); + if (m_solarDynamicsObservatory) + { + m_player = new QMediaPlayer(); +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) + connect(m_player, qOverload(&QMediaPlayer::error), this, &SIDGUI::sdoVideoError); + connect(m_player, &QMediaPlayer::bufferStatusChanged, this, &SIDGUI::sdoBufferStatusChanged); +#else + connect(m_player, &QMediaPlayer::errorOccurred, this, &SIDGUI::sdoVideoError); + connect(m_player, &QMediaPlayer::bufferProgressChanged, this, &SIDGUI::sdoBufferProgressChanged); +#endif + connect(m_player, &QMediaPlayer::mediaStatusChanged, this, &SIDGUI::sdoVideoStatusChanged); + m_player->setVideoOutput(ui->sdoVideo); + + ui->sdoData->blockSignals(true); + connect(m_solarDynamicsObservatory, &SolarDynamicsObservatory::imageUpdated, this, &SIDGUI::sdoImageUpdated); + for (const auto& name : SolarDynamicsObservatory::getImageNames()) { + ui->sdoData->addItem(name); + } + ui->sdoData->blockSignals(false); + ui->sdoData->setCurrentIndex(1); + m_settings.m_sdoData = ui->sdoData->currentText(); + } + + // Intialisation for GOES X-Ray data + m_goesXRay = GOESXRay::create(); + if (m_goesXRay) + { + connect(m_goesXRay, &GOESXRay::xRayDataUpdated, this, &SIDGUI::xRayDataUpdated); + connect(m_goesXRay, &GOESXRay::protonDataUpdated, this, &SIDGUI::protonDataUpdated); + m_goesXRay->getDataPeriodically(); + } + + // Get Gamma Ray Bursts + m_grb = GRB::create(); + if (m_grb) + { + connect(m_grb, &GRB::dataUpdated, this, &SIDGUI::grbDataUpdated); + m_grb->getDataPeriodically(); + } + + // Get STIX Solar Flare data + m_stix = STIX::create(); + if (m_stix) + { + connect(m_stix, &STIX::dataUpdated, this, &SIDGUI::stixDataUpdated); + m_stix->getDataPeriodically(); + } + + plotChart(); + + QObject::connect( + &m_availableFeatureHandler, + &AvailableChannelOrFeatureHandler::channelsOrFeaturesChanged, + this, + &SIDGUI::featuresChanged + ); + m_availableFeatureHandler.scanAvailableChannelsAndFeatures(); + QObject::connect( + &m_availableChannelHandler, + &AvailableChannelOrFeatureHandler::channelsOrFeaturesChanged, + this, + &SIDGUI::channelsChanged + ); + m_availableChannelHandler.scanAvailableChannelsAndFeatures(); + + QObject::connect(ui->chartSplitter, &QSplitter::splitterMoved, this, &SIDGUI::chartSplitterMoved); + QObject::connect(ui->sdoSplitter, &QSplitter::splitterMoved, this, &SIDGUI::sdoSplitterMoved); +} + +SIDGUI::~SIDGUI() +{ + QObject::disconnect(ui->chartSplitter, &QSplitter::splitterMoved, this, &SIDGUI::chartSplitterMoved); + QObject::disconnect(ui->sdoSplitter, &QSplitter::splitterMoved, this, &SIDGUI::sdoSplitterMoved); + + QObject::disconnect(&m_availableFeatureHandler, + &AvailableChannelOrFeatureHandler::channelsOrFeaturesChanged, + this, + &SIDGUI::featuresChanged + ); + QObject::disconnect(&m_availableChannelHandler, + &AvailableChannelOrFeatureHandler::channelsOrFeaturesChanged, + this, + &SIDGUI::channelsChanged + ); + disconnectDataUpdates(); + if (m_grb) { + disconnect(m_grb, &GRB::dataUpdated, this, &SIDGUI::grbDataUpdated); + } + if (m_stix) { + disconnect(m_stix, &STIX::dataUpdated, this, &SIDGUI::stixDataUpdated); + } + m_statusTimer.stop(); + + clearFromMap(); + + delete m_goesXRay; + delete ui; +} + + +void SIDGUI::connectDataUpdates() +{ + if (m_goesXRay) + { + connect(m_goesXRay, &GOESXRay::xRayDataUpdated, this, &SIDGUI::xRayDataUpdated); + connect(m_goesXRay, &GOESXRay::protonDataUpdated, this, &SIDGUI::protonDataUpdated); + } +} + +void SIDGUI::disconnectDataUpdates() +{ + if (m_goesXRay) + { + disconnect(m_goesXRay, &GOESXRay::xRayDataUpdated, this, &SIDGUI::xRayDataUpdated); + disconnect(m_goesXRay, &GOESXRay::protonDataUpdated, this, &SIDGUI::protonDataUpdated); + } +} + +void SIDGUI::getData() +{ + if (m_goesXRay) { + m_goesXRay->getData(); + } +} + +void SIDGUI::setWorkspaceIndex(int index) +{ + m_settings.m_workspaceIndex = index; + m_feature->setWorkspaceIndex(index); + m_settingsKeys.append("workspaceIndex"); +} + +void SIDGUI::blockApplySettings(bool block) +{ + m_doApplySettings = !block; +} + +void SIDGUI::displaySettings() +{ + setTitleColor(m_settings.m_rgbColor); + setWindowTitle(m_settings.m_title); + setTitle(m_settings.m_title); + blockApplySettings(true); + + ui->samples->setValue(m_settings.m_samples); + ui->separateCharts->setChecked(m_settings.m_separateCharts); + ui->displayLegend->setChecked(m_settings.m_displayLegend); + ui->plotXRayLongPrimary->setChecked(m_settings.m_plotXRayLongPrimary); + ui->plotXRayLongSecondary->setChecked(m_settings.m_plotXRayLongSecondary); + ui->plotXRayShortPrimary->setChecked(m_settings.m_plotXRayShortPrimary); + ui->plotXRayShortSecondary->setChecked(m_settings.m_plotXRayShortSecondary); + ui->plotGRB->setChecked(m_settings.m_plotGRB); + ui->plotSTIX->setChecked(m_settings.m_plotSTIX); + ui->plotProton->setChecked(m_settings.m_plotProton); + ui->autoscaleX->setChecked(m_settings.m_autoscaleX); + ui->autoscaleY->setChecked(m_settings.m_autoscaleY); + ui->startDateTime->clearMaximumDateTime(); + ui->endDateTime->clearMinimumDateTime(); + if (m_settings.m_startDateTime.isValid()) { + ui->startDateTime->setDateTime(m_settings.m_startDateTime); + } + if (m_settings.m_endDateTime.isValid()) { + ui->endDateTime->setDateTime(m_settings.m_endDateTime); + } + ui->startDateTime->setMaximumDateTime(ui->endDateTime->dateTime()); + ui->endDateTime->setMinimumDateTime(ui->startDateTime->dateTime()); + ui->y1Min->setValue(m_settings.m_y1Min); + ui->y1Max->setValue(m_settings.m_y1Max); + setAutoscaleX(); + setAutoscaleY(); + setXAxisRange(); + setY1AxisRange(); + setAutosaveTimer(); + + ui->sdoEnabled->setChecked(m_settings.m_sdoEnabled); + ui->sdoVideoEnabled->setChecked(m_settings.m_sdoVideoEnabled); + ui->sdoData->setCurrentText(m_settings.m_sdoData); + ui->sdoNow->setChecked(m_settings.m_sdoNow); + ui->sdoDateTime->setEnabled(!m_settings.m_sdoNow); + ui->mapLabel->setEnabled(!m_settings.m_sdoNow); + ui->map->setEnabled(!m_settings.m_sdoNow); + ui->sdoDateTime->setDateTime(m_settings.m_sdoDateTime); + ui->map->setCurrentText(m_settings.m_map); + applySDO(); + applyDateTime(); + + if (m_settings.m_autoload) { + readCSV(m_settings.m_filename, true); + } + + getRollupContents()->restoreState(m_rollupState); + + if (m_settings.m_chartSplitterSizes.size() > 0) { + ui->chartSplitter->setSizes(m_settings.m_chartSplitterSizes); + } + if (m_settings.m_sdoSplitterSizes.size() > 0) { + ui->sdoSplitter->setSizes(m_settings.m_sdoSplitterSizes); + } + + blockApplySettings(false); + getRollupContents()->arrangeRollups(); +} + +void SIDGUI::setAutosaveTimer() +{ + if (m_settings.m_autosave) { + m_autosaveTimer.start(1000*60*m_settings.m_autosavePeriod); + } else { + m_autosaveTimer.stop(); + } +} + +void SIDGUI::onMenuDialogCalled(const QPoint &p) +{ + if (m_contextMenuType == ContextMenuChannelSettings) + { + BasicFeatureSettingsDialog dialog(this); + dialog.setTitle(m_settings.m_title); + 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.setDefaultTitle(m_displayedName); + + dialog.move(p); + new DialogPositioner(&dialog, false); + dialog.exec(); + + 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(); + + setTitle(m_settings.m_title); + setTitleColor(m_settings.m_rgbColor); + + QStringList settingsKeys({ + "rgbColor", + "title", + "useReverseAPI", + "reverseAPIAddress", + "reverseAPIPort", + "reverseAPIDeviceIndex", + "reverseAPIChannelIndex" + }); + + applySettings(m_settingsKeys); + } + + resetContextMenuType(); +} + +void SIDGUI::applySetting(const QString& settingsKey) +{ + applySettings({settingsKey}); +} + +void SIDGUI::applySettings(const QStringList& settingsKeys, bool force) +{ + m_settingsKeys.append(settingsKeys); + if (m_doApplySettings) + { + SIDMain::MsgConfigureSID* message = SIDMain::MsgConfigureSID::create(m_settings, m_settingsKeys, force); + m_sid->getInputMessageQueue()->push(message); + m_settingsKeys.clear(); + } + + m_settingsKeys.clear(); +} + +void SIDGUI::applyAllSettings() +{ + applySettings(QStringList(), true); +} + +void SIDGUI::chartSplitterMoved(int pos, int index) +{ + (void) pos; + (void) index; + + m_settings.m_chartSplitterSizes = ui->chartSplitter->sizes(); + applySetting("chartSplitterSizes"); +} + +void SIDGUI::sdoSplitterMoved(int pos, int index) +{ + (void) pos; + (void) index; + + m_settings.m_sdoSplitterSizes = ui->sdoSplitter->sizes(); + applySetting("chartSplitterSizes"); +} + +void SIDGUI::on_samples_valueChanged(int value) +{ + m_settings.m_samples = value; + applySetting("samples"); + plotChart(); +} + +void SIDGUI::on_separateCharts_toggled(bool checked) +{ + m_settings.m_separateCharts = checked; + applySetting("separateCharts"); + plotChart(); +} + +void SIDGUI::on_displayLegend_toggled(bool checked) +{ + m_settings.m_displayLegend = checked; + applySetting("displayLegend"); + plotChart(); +} + +void SIDGUI::on_plotXRayLongPrimary_toggled(bool checked) +{ + m_settings.m_plotXRayLongPrimary = checked; + applySetting("plotXRayLongPrimary"); + plotChart(); +} + +void SIDGUI::on_plotXRayLongSecondary_toggled(bool checked) +{ + m_settings.m_plotXRayLongSecondary = checked; + applySetting("plotXRayLongSecondary"); + plotChart(); +} + +void SIDGUI::on_plotXRayShortPrimary_toggled(bool checked) +{ + m_settings.m_plotXRayShortPrimary = checked; + applySetting("plotXRayShortPrimary"); + plotChart(); +} + +void SIDGUI::on_plotXRayShortSecondary_toggled(bool checked) +{ + m_settings.m_plotXRayShortSecondary = checked; + applySetting("plotXRayShortSecondary"); + plotChart(); +} + +void SIDGUI::on_plotGRB_toggled(bool checked) +{ + m_settings.m_plotGRB = checked; + applySetting("plotGRB"); + plotChart(); +} + +void SIDGUI::on_plotSTIX_toggled(bool checked) +{ + m_settings.m_plotSTIX = checked; + applySetting("plotSTIX"); + plotChart(); +} + +void SIDGUI::on_plotProton_toggled(bool checked) +{ + m_settings.m_plotProton = checked; + applySetting("plotProton"); + plotChart(); +} + +void SIDGUI::on_startStop_toggled(bool checked) +{ + if (m_doApplySettings) + { + SIDMain::MsgStartStop *message = SIDMain::MsgStartStop::create(checked); + m_sid->getInputMessageQueue()->push(message); + } +} + +void SIDGUI::createGRBSeries(QChart *chart, QDateTimeAxis *xAxis, QLogValueAxis *yAxis) +{ + bool secondaryAxis = plotAnyXRay() || m_settings.m_plotSTIX; + yAxis->setLabelFormat("%.0e"); + yAxis->setGridLineVisible(!secondaryAxis); + yAxis->setTitleText("GRB Fluence (erg/cm2)"); + yAxis->setTitleVisible(m_settings.m_displayAxisTitles); + yAxis->setVisible(!secondaryAxis || m_settings.m_displaySecondaryAxis); + + if (m_settings.m_plotGRB) + { + m_grbSeries = new QScatterSeries(); + m_grbSeries->setName("GRB"); + m_grbSeries->setColor(m_settings.m_grbColor); + m_grbSeries->setBorderColor(m_settings.m_grbColor); + m_grbSeries->setMarkerSize(8); + + for (int i = 0; i < m_grbData.size(); i++) + { + float value = m_grbData[i].m_fluence; + if ((value <= 0.0f) || std::isnan(value)) { + value = m_grbMin; // <= 0 will result in series not being plotted, as log axis used + } + m_grbSeries->append(m_grbData[i].m_dateTime.toMSecsSinceEpoch(), value); + } + yAxis->setMin(m_grbMin); + yAxis->setMax(m_grbMax); + chart->addSeries(m_grbSeries); + m_grbSeries->attachAxis(xAxis); + m_grbSeries->attachAxis(yAxis); + } + else + { + m_grbSeries = nullptr; + } +} + +void SIDGUI::createFlareAxis(QCategoryAxis *yAxis) +{ + // Solar flare classification + yAxis->setMin(-8); + yAxis->setMax(-3); + yAxis->setStartValue(-8); + yAxis->append("A", -7); + yAxis->append("B", -6); + yAxis->append("C", -5); + yAxis->append("M", -4); + yAxis->append("X", -3); + yAxis->setTitleText("Flare Class"); + yAxis->setTitleVisible(m_settings.m_displayAxisTitles); + yAxis->setLineVisible(m_settings.m_displaySecondaryAxis); + yAxis->setGridLineVisible(m_settings.m_separateCharts); +} + +void SIDGUI::createXRaySeries(QChart *chart, QDateTimeAxis *xAxis, QCategoryAxis *yAxis) +{ + createFlareAxis(yAxis); + + for (int i = 0; i < 2; i++) + { + QString name = i == 0 ? "Primary" : "Secondary"; + + if (((i == 0) && m_settings.m_plotXRayShortPrimary) || ((i == 1) && m_settings.m_plotXRayShortSecondary)) + { + m_xrayShortMeasurements[i].m_series = new QLineSeries(); + m_xrayShortMeasurements[i].m_series->setName(QString("0.05-0.4nm X-Ray %1").arg(name)); + m_xrayShortMeasurements[i].m_series->setColor(m_settings.m_xrayShortColors[i]); + for (int j = 0; j < m_xrayShortMeasurements[i].m_measurements.size(); j++) { + m_xrayShortMeasurements[i].m_series->append(m_xrayShortMeasurements[i].m_measurements[j].m_dateTime.toMSecsSinceEpoch(), m_xrayShortMeasurements[i].m_measurements[j].m_measurement); + } + chart->addSeries(m_xrayShortMeasurements[i].m_series); + m_xrayShortMeasurements[i].m_series->attachAxis(xAxis); + m_xrayShortMeasurements[i].m_series->attachAxis(yAxis); + } + else + { + m_xrayShortMeasurements[i].m_series = nullptr; + } + + if (((i == 0) && m_settings.m_plotXRayLongPrimary) || ((i == 1) && m_settings.m_plotXRayLongSecondary)) + { + m_xrayLongMeasurements[i].m_series = new QLineSeries(); + m_xrayLongMeasurements[i].m_series->setName(QString("0.1-0.8nm X-Ray %1").arg(name)); + m_xrayLongMeasurements[i].m_series->setColor(m_settings.m_xrayLongColors[i]); + for (int j = 0; j < m_xrayLongMeasurements[i].m_measurements.size(); j++) { + m_xrayLongMeasurements[i].m_series->append(m_xrayLongMeasurements[i].m_measurements[j].m_dateTime.toMSecsSinceEpoch(), m_xrayLongMeasurements[i].m_measurements[j].m_measurement); + } + chart->addSeries(m_xrayLongMeasurements[i].m_series); + m_xrayLongMeasurements[i].m_series->attachAxis(xAxis); + m_xrayLongMeasurements[i].m_series->attachAxis(yAxis); + } + else + { + m_xrayLongMeasurements[i].m_series = nullptr; + } + } +} + +const QStringList SIDGUI::m_protonEnergies = {"10 MeV", "50 MeV", "100 MeV", "500 MeV"}; + + +void SIDGUI::createProtonSeries(QChart *chart, QDateTimeAxis *xAxis, QLogValueAxis *yAxis) +{ + bool secondaryAxis = plotAnyXRay() || m_settings.m_plotSTIX || m_settings.m_plotGRB; + yAxis->setLabelFormat("%.0e"); + yAxis->setMin(0.01); + yAxis->setMax(1000.0); + yAxis->setGridLineVisible(!secondaryAxis); + yAxis->setTitleText("Proton Flux (Particles / (cm2 s sr))"); + yAxis->setTitleVisible(m_settings.m_displayAxisTitles); + yAxis->setVisible(!secondaryAxis || m_settings.m_displaySecondaryAxis); + + for (int i = 0; i < 4; i += 2) // Only plot 10 and 100 MeV so graph isn't too cluttered + //for (int i = 0; i < 4; i++) + { + m_protonMeasurements[i].m_series = new QLineSeries(); + m_protonMeasurements[i].m_series->setName(QString("%1 Proton").arg(SIDGUI::m_protonEnergies[i])); + m_protonMeasurements[i].m_series->setColor(m_settings.m_protonColors[i]); + + for (int j = 0; j < m_protonMeasurements[i].m_measurements.size(); j++) + { + double value = m_protonMeasurements[i].m_measurements[j].m_measurement; + if (value >= 0.0) { + m_protonMeasurements[i].m_series->append(m_protonMeasurements[i].m_measurements[j].m_dateTime.toMSecsSinceEpoch(), value); + } + } + chart->addSeries(m_protonMeasurements[i].m_series); + m_protonMeasurements[i].m_series->attachAxis(xAxis); + m_protonMeasurements[i].m_series->attachAxis(yAxis); + } +} + +void SIDGUI::createSTIXSeries(QChart *chart, QDateTimeAxis *xAxis, QCategoryAxis *yAxis) +{ + createFlareAxis(yAxis); + + if (m_settings.m_plotSTIX) + { + m_stixSeries = new QScatterSeries(); + m_stixSeries->setName("STIX"); + m_stixSeries->setColor(m_settings.m_stixColor); + m_stixSeries->setBorderColor(m_settings.m_stixColor); + m_stixSeries->setMarkerSize(5); + for (int i = 0; i < m_stixData.size(); i++) + { + double value = m_stixData[i].m_flux; + if (value == 0.0) + { + value = -8; + } + else + { + value = log10(value); + } + m_stixSeries->append(m_stixData[i].m_startDateTime.toMSecsSinceEpoch(), value); + } + chart->addSeries(m_stixSeries); + m_stixSeries->attachAxis(xAxis); + m_stixSeries->attachAxis(yAxis); + } + else + { + m_stixSeries = nullptr; + } +} + +void SIDGUI::plotChart() +{ + QChart *oldChart = ui->chart->chart(); + QChart *chart; + + chart = new QChart(); + chart->layout()->setContentsMargins(0, 0, 0, 0); + chart->setMargins(QMargins(1, 1, 1, 1)); + chart->setTheme(QChart::ChartThemeDark); + chart->legend()->setVisible(m_settings.m_displayLegend); + chart->legend()->setAlignment(m_settings.m_legendAlignment); + + m_chartXAxis = new QDateTimeAxis(); + m_chartY1Axis = new QValueAxis(); + m_chartY2Axis = nullptr; + m_chartY3Axis = nullptr; + m_chartProtonAxis = nullptr; + if (!m_settings.m_separateCharts) + { + // XRay flux + if (plotAnyXRay() || m_settings.m_plotSTIX) + { + m_chartY2Axis = new QCategoryAxis(); + chart->addAxis(m_chartY2Axis, Qt::AlignRight); + } + + // GRB fluence + if (m_settings.m_plotGRB) + { + m_chartY3Axis = new QLogValueAxis(); + chart->addAxis(m_chartY3Axis, Qt::AlignRight); + } + + // Proton flux + if (m_settings.m_plotProton) + { + m_chartProtonAxis = new QLogValueAxis(); + chart->addAxis(m_chartProtonAxis, Qt::AlignRight); + } + } + + chart->addAxis(m_chartXAxis, Qt::AlignBottom); + chart->addAxis(m_chartY1Axis, Qt::AlignLeft); + m_chartY1Axis->setTitleText("Power (dB)"); + m_chartY1Axis->setTitleVisible(m_settings.m_displayAxisTitles); + + // Power measurements + for (auto& measurement : m_channelMeasurements) + { + SIDSettings::ChannelSettings *channelSettings = m_settings.getChannelSettings(measurement.m_id); + if (!channelSettings) { + qDebug() << "SIDGUI::plotChart: No settings for channel" << measurement.m_id; + } + if (channelSettings && channelSettings->m_enabled) + { + QLineSeries *series = new QLineSeries(); + series->setName(channelSettings->m_label); + series->setColor(channelSettings->m_color); + + measurement.newSeries(series, m_settings.m_samples); + for (int i = 0; i < measurement.m_measurements.size(); i++) + { + measurement.appendSeries(measurement.m_measurements[i].m_dateTime, measurement.m_measurements[i].m_measurement); + updateMeasurementRange(measurement.m_measurements[i].m_measurement); + updateTimeRange(measurement.m_measurements[i].m_dateTime); + } + chart->addSeries(measurement.m_series); + measurement.m_series->attachAxis(m_chartXAxis); + measurement.m_series->attachAxis(m_chartY1Axis); + } + } + + for (int i = 0; i < 2; i++) + { + m_xrayShortMeasurements[i].m_series = nullptr; + m_xrayLongMeasurements[i].m_series = nullptr; + } + m_grbSeries = nullptr; + m_stixSeries = nullptr; + for (int i = 0; i < 4; i++) { + m_protonMeasurements[i].m_series = nullptr; + } + + if (!m_settings.m_separateCharts) + { + // XRay + if (plotAnyXRay()) { + createXRaySeries(chart, m_chartXAxis, m_chartY2Axis); + } + + // GRB + if (m_settings.m_plotGRB) { + createGRBSeries(chart, m_chartXAxis, m_chartY3Axis); + } + + // STIX flares + if (m_settings.m_plotSTIX) { + createSTIXSeries(chart, m_chartXAxis, m_chartY2Axis); + } + + // Proton flux + if (m_settings.m_plotProton) { + createProtonSeries(chart, m_chartXAxis, m_chartProtonAxis); + } + } + + autoscaleX(); + autoscaleY(); + setXAxisRange(); + setY1AxisRange(); + + ui->chart->setChart(chart); + ui->chart->installEventFilter(this); + + delete oldChart; + + const auto markers = chart->legend()->markers(); + for (QLegendMarker *marker : markers) + { + connect(marker, &QLegendMarker::clicked, this, &SIDGUI::legendMarkerClicked); + } + + for (const auto series : chart->series()) + { + QXYSeries *s = qobject_cast(series); + if (s) { + connect(s, &QXYSeries::clicked, this, &SIDGUI::seriesClicked); + } + } + + if (m_settings.m_separateCharts) + { + ui->xRayChart->setVisible(true); + plotXRayChart(); + } + else + { + ui->xRayChart->setVisible(false); + } +} + +bool SIDGUI::plotAnyXRay() const +{ + return m_settings.m_plotXRayLongPrimary || m_settings.m_plotXRayLongSecondary + || m_settings.m_plotXRayShortPrimary || m_settings.m_plotXRayShortSecondary; +} + +void SIDGUI::plotXRayChart() +{ + QChart *oldChart = ui->xRayChart->chart(); + QChart *chart; + + chart = new QChart(); + chart->layout()->setContentsMargins(0, 0, 0, 0); + chart->setMargins(QMargins(1, 1, 1, 1)); + chart->setTheme(QChart::ChartThemeDark); + chart->legend()->setVisible(m_settings.m_displayLegend); + chart->legend()->setAlignment(m_settings.m_legendAlignment); + + m_xRayChartXAxis = new QDateTimeAxis(); + chart->addAxis(m_xRayChartXAxis, Qt::AlignBottom); + + if (plotAnyXRay() || m_settings.m_plotSTIX) + { + m_xRayChartYAxis = new QCategoryAxis(); + chart->addAxis(m_xRayChartYAxis, Qt::AlignLeft); + } + + if (m_settings.m_plotGRB) + { + m_chartY3Axis = new QLogValueAxis(); + chart->addAxis(m_chartY3Axis, (plotAnyXRay() || m_settings.m_plotSTIX) ? Qt::AlignRight : Qt::AlignLeft); + } + + if (m_settings.m_plotProton) + { + m_chartProtonAxis = new QLogValueAxis(); + chart->addAxis(m_chartProtonAxis, (plotAnyXRay() || m_settings.m_plotSTIX || m_settings.m_plotGRB) ? Qt::AlignRight : Qt::AlignLeft); + } + + // XRay + if (plotAnyXRay()) { + createXRaySeries(chart, m_xRayChartXAxis, m_xRayChartYAxis); + } + + // GRB + if (m_settings.m_plotGRB) { + createGRBSeries(chart, m_xRayChartXAxis, m_chartY3Axis); + } + + // STIX flares + if (m_settings.m_plotSTIX) { + createSTIXSeries(chart, m_xRayChartXAxis, m_xRayChartYAxis); + } + + // Proton flux + if (m_settings.m_plotProton) { + createProtonSeries(chart, m_xRayChartXAxis, m_chartProtonAxis); + } + + setXAxisRange(); + + ui->xRayChart->setChart(chart); + ui->xRayChart->installEventFilter(this); + + delete oldChart; + + const auto markers = chart->legend()->markers(); + for (QLegendMarker *marker : markers) { + connect(marker, &QLegendMarker::clicked, this, &SIDGUI::legendMarkerClicked); + } + + for (const auto series : chart->series()) + { + QXYSeries *s = qobject_cast(series); + if (s) { + connect(s, &QXYSeries::clicked, this, &SIDGUI::seriesClicked); + } + } + + if (!(plotAnyXRay() || m_settings.m_plotGRB || m_settings.m_plotSTIX || m_settings.m_plotProton)) + { + ui->xRayChart->setVisible(false); // Hide empty chart + } +} + +void SIDGUI::legendMarkerClicked() +{ + QLegendMarker* marker = qobject_cast(sender()); + marker->series()->setVisible(!marker->series()->isVisible()); + marker->setVisible(true); + + // Dim the marker, if series is not visible + qreal alpha = 1.0; + + if (!marker->series()->isVisible()) { + alpha = 0.5; + } + + QColor color; + QBrush brush = marker->labelBrush(); + color = brush.color(); + color.setAlphaF(alpha); + brush.setColor(color); + marker->setLabelBrush(brush); + + brush = marker->brush(); + color = brush.color(); + color.setAlphaF(alpha); + brush.setColor(color); + marker->setBrush(brush); + + QPen pen = marker->pen(); + color = pen.color(); + color.setAlphaF(alpha); + pen.setColor(color); + marker->setPen(pen); +} + +void SIDGUI::seriesClicked(const QPointF &point) +{ + QDateTime dt = QDateTime::fromMSecsSinceEpoch(point.x()); + ui->sdoDateTime->setDateTime(dt); +} + +static qreal distance(const QPointF& a, const QPointF& b) +{ + qreal dx = a.x() - b.x(); + qreal dy = a.y() - b.y(); + return qSqrt(dx * dx + dy * dy); +} + +qreal SIDGUI::pixelDistance(QChart *chart, QAbstractSeries *series, QPointF a, QPointF b) +{ + a = chart->mapToPosition(a, series); + b = chart->mapToPosition(b, series); + return distance(a, b); +} + +void SIDGUI::sendToSkyMap(const AvailableChannelOrFeature& skymap, float ra, float dec) +{ + QString target = QString("%1 %2").arg(ra).arg(dec); + FeatureWebAPIUtils::skyMapFind(target, skymap.m_superIndex, skymap.m_index); +} + +void SIDGUI::showGRBContextMenu(QContextMenuEvent *contextEvent, QChartView *chartView, int closestPoint) +{ + QMenu *contextMenu = new QMenu(chartView); + connect(contextMenu, &QMenu::aboutToHide, contextMenu, &QMenu::deleteLater); + + contextMenu->addSection(m_grbData[closestPoint].m_name); + + // Display GRB Fermi data + QString url = m_grbData[closestPoint].getFermiURL(); + if (!url.isEmpty()) + { + QAction* fermiDataAction = new QAction("View Fermi data directory...", contextMenu); + connect(fermiDataAction, &QAction::triggered, this, [url]()->void { + QDesktopServices::openUrl(QUrl(url)); + }); + contextMenu->addAction(fermiDataAction); + + QString plotURL = m_grbData[closestPoint].getFermiPlotURL(); + QAction* fermiPlotAction = new QAction("View Fermi data plot...", contextMenu); + connect(fermiPlotAction, &QAction::triggered, this, [plotURL]()->void { + QDesktopServices::openUrl(QUrl(plotURL)); + }); + contextMenu->addAction(fermiPlotAction); + + QString mapURL = m_grbData[closestPoint].getFermiSkyMapURL(); + QAction* fermiMapDataAction = new QAction("View Fermi sky map...", contextMenu); + connect(fermiMapDataAction, &QAction::triggered, this, [mapURL]()->void { + QDesktopServices::openUrl(QUrl(mapURL)); + }); + contextMenu->addAction(fermiMapDataAction); + } + + // Display Swift link + if (!m_grbData[closestPoint].m_name.endsWith("*")) + { + QAction* swiftDataAction = new QAction("View Swift data...", contextMenu); + QString switftURL = m_grbData[closestPoint].getSwiftURL(); + connect(swiftDataAction, &QAction::triggered, this, [switftURL]()->void { + QDesktopServices::openUrl(QUrl(switftURL)); + }); + contextMenu->addAction(swiftDataAction); + } + + // View GRB ra/dec in SkyMap + AvailableChannelOrFeatureHandler skymaps({"sdrangel.feature.skymap"}); + skymaps.scanAvailableChannelsAndFeatures(); + if (skymaps.getAvailableChannelOrFeatureList().size() > 0) + { + for (const auto& skymap : skymaps.getAvailableChannelOrFeatureList()) + { + QString label = QString("View coords in %1...").arg(skymap.getLongId()); + QAction* skyMapAction = new QAction(label, contextMenu); + float ra = m_grbData[closestPoint].m_ra; + float dec = m_grbData[closestPoint].m_dec; + connect(skyMapAction, &QAction::triggered, this, [this, skymap, ra, dec]()->void { + sendToSkyMap(skymap, ra, dec); + }); + contextMenu->addAction(skyMapAction); + } + } + else + { + QAction* skyMapAction = new QAction("View coords in SkyMap...", contextMenu); + float ra = m_grbData[closestPoint].m_ra; + float dec = m_grbData[closestPoint].m_dec; + QString target = QString("%1 %2").arg(ra).arg(dec); + connect(skyMapAction, &QAction::triggered, this, [target]()->void { + FeatureWebAPIUtils::openSkyMapAndFind(target); + }); + contextMenu->addAction(skyMapAction); + } + + contextMenu->popup(chartView->viewport()->mapToGlobal(contextEvent->pos())); +} + +void SIDGUI::showStixContextMenu(QContextMenuEvent *contextEvent, QChartView *chartView, int closestPoint) +{ + QMenu *contextMenu = new QMenu(chartView); + connect(contextMenu, &QMenu::aboutToHide, contextMenu, &QMenu::deleteLater); + + contextMenu->addSection(m_stixData[closestPoint].m_id); + + // Display GRB Fermi data + QString lcURL = m_stixData[closestPoint].getLightCurvesURL(); + QAction* lcAction = new QAction("View light curves...", contextMenu); + connect(lcAction, &QAction::triggered, this, [lcURL]()->void { + QDesktopServices::openUrl(QUrl(lcURL)); + }); + contextMenu->addAction(lcAction); + + QString dataURL = m_stixData[closestPoint].getDataURL(); + QAction* stixDataAction = new QAction("View STIX data...", contextMenu); + connect(stixDataAction, &QAction::triggered, this, [dataURL]()->void { + QDesktopServices::openUrl(QUrl(dataURL)); + }); + contextMenu->addAction(stixDataAction); + + contextMenu->popup(chartView->viewport()->mapToGlobal(contextEvent->pos())); +} + +bool SIDGUI::findClosestPoint(QContextMenuEvent *contextEvent, QChart *chart, QScatterSeries *series, int& closestPoint) +{ + QPointF point = chart->mapToValue(contextEvent->pos(), series); + QDateTime dt = QDateTime::fromMSecsSinceEpoch(point.x()); + + // Find nearest point - GRB/Stix data is ordered newest first + QVector points = series->pointsVector(); + if (points.size() > 0) + { + qint64 startTime = m_settings.m_startDateTime.toMSecsSinceEpoch(); + qreal closestDistance = pixelDistance(chart, series, point, points[0]); + closestPoint = 0; + + for (int i = 1; i < points.size(); i++) + { + qreal d = pixelDistance(chart, series, point, points[i]); + if (d < closestDistance) + { + closestDistance = d; + closestPoint = i; + } + if (points[i].x() < startTime) { + break; + } + } + return closestDistance <= series->markerSize(); + } + else + { + return false; + } +} + +void SIDGUI::showContextMenu(QContextMenuEvent *contextEvent) +{ + QChartView *chartView; + + if (m_settings.m_separateCharts) { + chartView = ui->xRayChart; + } else { + chartView = ui->chart; + } + + if (chartView) + { + int closestPoint; + + if (m_grbSeries && findClosestPoint(contextEvent, chartView->chart(), m_grbSeries, closestPoint)) { + showGRBContextMenu(contextEvent, chartView, closestPoint); + } else if (m_stixSeries && findClosestPoint(contextEvent, chartView->chart(), m_stixSeries, closestPoint)) { + showStixContextMenu(contextEvent, chartView, closestPoint); + } + } +} + +bool SIDGUI::eventFilter(QObject *obj, QEvent *event) +{ + if ((obj == ui->chart) || (obj == ui->xRayChart)) + { + if (event->type() == QEvent::ContextMenu) + { + // Show context menu on chart for GRBs/Flares + QContextMenuEvent *contextEvent = static_cast(event); + + showContextMenu(contextEvent); + contextEvent->accept(); + return true; + } + else if (event->type() == QEvent::Wheel) + { + // Use wheel to zoom in / out of X axis or Y axis if shift held + QWheelEvent *wheelEvent = static_cast(event); + + int delta = wheelEvent->angleDelta().y(); // delta is typically 120 for one click of wheel + + if (wheelEvent->modifiers() & Qt::ShiftModifier) + { + double min = ui->y1Min->value(); + double max = ui->y1Max->value(); + double adj = (max - min) * 0.20 * delta / 120.0; + min += adj; + max -= adj; + ui->y1Min->setValue(min); + ui->y1Max->setValue(max); + } + else + { + QDateTime start = ui->startDateTime->dateTime(); + QDateTime end = ui->endDateTime->dateTime(); + qint64 startMS = start.toMSecsSinceEpoch(); + qint64 endMS = end.toMSecsSinceEpoch(); + qint64 diff = endMS - startMS; + qint64 adj = diff * 0.20 * delta / 120.0; + endMS -= adj; + startMS += adj; + start = QDateTime::fromMSecsSinceEpoch(startMS); + end = QDateTime::fromMSecsSinceEpoch(endMS); + ui->startDateTime->setDateTime(start); + ui->endDateTime->setDateTime(end); + } + + wheelEvent->accept(); + return true; + } + } + return FeatureGUI::eventFilter(obj, event); +} + +void SIDGUI::updateMeasurementRange(double measurement) +{ + if (std::isnan(m_minMeasurement)) { + m_minMeasurement = measurement; + } else { + m_minMeasurement = std::min(m_minMeasurement, measurement); + } + if (std::isnan(m_maxMeasurement)) { + m_maxMeasurement = measurement; + } else { + m_maxMeasurement = std::max(m_maxMeasurement, measurement); + } +} + +void SIDGUI::updateTimeRange(QDateTime dateTime) +{ + if (!m_minDateTime.isValid() || (dateTime < m_minDateTime)) { + m_minDateTime = dateTime; + } + if (!m_maxDateTime.isValid() || (dateTime > m_maxDateTime)) { + m_maxDateTime = dateTime; + } +} + +void SIDGUI::setXAxisRange() +{ + if (m_chartXAxis) { + m_chartXAxis->setRange(m_settings.m_startDateTime, m_settings.m_endDateTime); + } + if (m_xRayChartXAxis) { + m_xRayChartXAxis->setRange(m_settings.m_startDateTime, m_settings.m_endDateTime); + } +} + +void SIDGUI::setY1AxisRange() +{ + if (m_chartY1Axis) { + m_chartY1Axis->setRange(m_settings.m_y1Min, m_settings.m_y1Max); + } +} + +void SIDGUI::setButtonBackground(QToolButton *button, bool checked) +{ + if (!checked) + { + button->setStyleSheet(""); + } + else + { + button->setStyleSheet(QString("QToolButton{ background-color: %1; }") + .arg(palette().highlight().color().darker(150).name())); + } +} + +void SIDGUI::setAutoscaleX() +{ + setButtonBackground(ui->autoscaleX, m_settings.m_autoscaleX); +} + +void SIDGUI::setAutoscaleY() +{ + setButtonBackground(ui->autoscaleY, m_settings.m_autoscaleY); +} + +void SIDGUI::on_autoscaleX_clicked() +{ + ui->startDateTime->clearMaximumDateTime(); + ui->endDateTime->clearMinimumDateTime(); + + if (m_minDateTime.isValid()) + { + ui->startDateTime->setDateTime(m_minDateTime); + } + if (m_maxDateTime.isValid()) + { + ui->endDateTime->setDateTime(m_maxDateTime); + } + + ui->startDateTime->setMaximumDateTime(ui->endDateTime->dateTime()); + ui->endDateTime->setMinimumDateTime(ui->startDateTime->dateTime()); +} + +void SIDGUI::on_autoscaleY_clicked() +{ + if (!std::isnan(m_minMeasurement) && !std::isnan(m_maxMeasurement) && (m_minMeasurement == m_maxMeasurement)) + { + // Graph doesn't display properly if min is the same as max + ui->y1Min->setValue(m_minMeasurement * 0.99); + ui->y1Max->setValue(m_maxMeasurement * 1.01); + } + else + { + if (!std::isnan(m_minMeasurement)) { + ui->y1Min->setValue(m_minMeasurement); + } + if (!std::isnan(m_maxMeasurement)) { + ui->y1Max->setValue(m_maxMeasurement); + } + } +} + +void SIDGUI::on_today_clicked() +{ + QDate today = QDate::currentDate(); + QDateTime start = QDateTime(today, QTime(0,0)); + QDateTime end = QDateTime(today.addDays(1), QTime(0,0)); + + ui->startDateTime->clearMaximumDateTime(); + ui->endDateTime->clearMinimumDateTime(); + + ui->startDateTime->setDateTime(start); + ui->endDateTime->setDateTime(end); + + ui->startDateTime->setMaximumDateTime(ui->endDateTime->dateTime()); + ui->endDateTime->setMinimumDateTime(ui->startDateTime->dateTime()); +} + +void SIDGUI::todayRightClicked() +{ + float stationLatitude = MainCore::instance()->getSettings().getLatitude(); + float stationLongitude = MainCore::instance()->getSettings().getLongitude(); + + QDate today = QDate::currentDate(); + + QDateTime sunRise, sunSet; + Astronomy::sunrise(today, stationLatitude, stationLongitude, sunRise, sunSet); + + ui->startDateTime->clearMaximumDateTime(); + ui->endDateTime->clearMinimumDateTime(); + + ui->startDateTime->setDateTime(sunRise); + ui->endDateTime->setDateTime(sunSet); + + ui->startDateTime->setMaximumDateTime(ui->endDateTime->dateTime()); + ui->endDateTime->setMinimumDateTime(ui->startDateTime->dateTime()); +} + +void SIDGUI::on_prevDay_clicked() +{ + ui->startDateTime->clearMaximumDateTime(); + ui->endDateTime->clearMinimumDateTime(); + + ui->startDateTime->setDateTime(ui->startDateTime->dateTime().addDays(-1)); + ui->endDateTime->setDateTime(ui->endDateTime->dateTime().addDays(-1)); + + ui->startDateTime->setMaximumDateTime(ui->endDateTime->dateTime()); + ui->endDateTime->setMinimumDateTime(ui->startDateTime->dateTime()); +} + +void SIDGUI::on_nextDay_clicked() +{ + ui->startDateTime->clearMaximumDateTime(); + ui->endDateTime->clearMinimumDateTime(); + + ui->endDateTime->setDateTime(ui->endDateTime->dateTime().addDays(1)); + ui->startDateTime->setDateTime(ui->startDateTime->dateTime().addDays(1)); + + ui->startDateTime->setMaximumDateTime(ui->endDateTime->dateTime()); + ui->endDateTime->setMinimumDateTime(ui->startDateTime->dateTime()); +} + +void SIDGUI::autoscaleXRightClicked() +{ + m_settings.m_autoscaleX = !m_settings.m_autoscaleX; + applySetting("autoscaleX"); + setAutoscaleX(); +} + +void SIDGUI::autoscaleYRightClicked() +{ + m_settings.m_autoscaleY = !m_settings.m_autoscaleY; + applySetting("autoscaleY"); + setAutoscaleY(); +} + +void SIDGUI::on_startDateTime_dateTimeChanged(QDateTime value) +{ + m_settings.m_startDateTime = value; + applySetting("startDateTime"); + setXAxisRange(); + ui->endDateTime->setMinimumDateTime(value); +} + +void SIDGUI::on_endDateTime_dateTimeChanged(QDateTime value) +{ + m_settings.m_endDateTime = value; + applySetting("endDateTime"); + setXAxisRange(); + ui->startDateTime->setMaximumDateTime(value); +} + +void SIDGUI::on_y1Min_valueChanged(double value) +{ + m_settings.m_y1Min = (float) value; + applySetting("y1Min"); + setY1AxisRange(); +} + +void SIDGUI::on_y1Max_valueChanged(double value) +{ + m_settings.m_y1Max = (float) value; + applySetting("y1Max"); + setY1AxisRange(); +} + +void SIDGUI::clearMinMax() +{ + m_minDateTime = QDateTime(); + m_maxDateTime = QDateTime(); + m_minMeasurement = std::numeric_limits::quiet_NaN(); + m_maxMeasurement = std::numeric_limits::quiet_NaN(); +} + +void SIDGUI::clearAllData() +{ + m_channelMeasurements.clear(); + for (int i = 0; i < 2; i++) + { + m_xrayShortMeasurements[i].clear(); + m_xrayLongMeasurements[i].clear(); + } + for (int i = 0; i < 4; i++) { + m_protonMeasurements[i].clear(); + } + clearMinMax(); +} + +void SIDGUI::on_deleteAll_clicked() +{ + clearAllData(); + plotChart(); + getData(); +} + +void SIDGUI::on_settings_clicked() +{ + SIDSettingsDialog dialog(&m_settings); + + QObject::connect( + &dialog, + &SIDSettingsDialog::removeChannels, + this, + &SIDGUI::removeChannels + ); + + if (dialog.exec() == QDialog::Accepted) + { + setAutosaveTimer(); + QStringList settingsKeys; + settingsKeys.append("period"); + settingsKeys.append("autosave"); + settingsKeys.append("autoload"); + settingsKeys.append("filename"); + settingsKeys.append("autosavePeriod"); + settingsKeys.append("legendAlignment"); + settingsKeys.append("displayAxisTitles"); + settingsKeys.append("displayAxisLabels"); + settingsKeys.append("channelSettings"); + settingsKeys.append("xrayShortColors"); + settingsKeys.append("xrayLongColors"); + settingsKeys.append("protonColors"); + settingsKeys.append("grbColor"); + settingsKeys.append("stixColor"); + applySettings(settingsKeys); + plotChart(); + } +} + +void SIDGUI::updateStatus() +{ + int state = m_sid->getState(); + + if (m_lastFeatureState != state) + { + // We set checked state of start/stop button, in case it was changed via API + bool oldState; + switch (state) + { + case Feature::StNotStarted: + ui->startStop->setStyleSheet("QToolButton { background:rgb(79,79,79); }"); + break; + case Feature::StIdle: + oldState = ui->startStop->blockSignals(true); + ui->startStop->setChecked(false); + ui->startStop->blockSignals(oldState); + ui->startStop->setStyleSheet("QToolButton { background-color : blue; }"); + break; + case Feature::StRunning: + oldState = ui->startStop->blockSignals(true); + ui->startStop->setChecked(true); + ui->startStop->blockSignals(oldState); + ui->startStop->setStyleSheet("QToolButton { background-color : green; }"); + break; + case Feature::StError: + ui->startStop->setStyleSheet("QToolButton { background-color : red; }"); + QMessageBox::critical(this, m_settings.m_title, m_sid->getErrorMessage()); + break; + default: + break; + } + + m_lastFeatureState = state; + } +} + +void SIDGUI::makeUIConnections() +{ + QObject::connect(ui->startStop, &ButtonSwitch::toggled, this, &SIDGUI::on_startStop_toggled); + QObject::connect(ui->samples, QOverload::of(&QSpinBox::valueChanged), this, &SIDGUI::on_samples_valueChanged); + QObject::connect(ui->separateCharts, &ButtonSwitch::toggled, this, &SIDGUI::on_separateCharts_toggled); + QObject::connect(ui->displayLegend, &ButtonSwitch::toggled, this, &SIDGUI::on_displayLegend_toggled); + QObject::connect(ui->plotXRayLongPrimary, &ButtonSwitch::toggled, this, &SIDGUI::on_plotXRayLongPrimary_toggled); + QObject::connect(ui->plotXRayLongSecondary, &ButtonSwitch::toggled, this, &SIDGUI::on_plotXRayLongSecondary_toggled); + QObject::connect(ui->plotXRayShortPrimary, &ButtonSwitch::toggled, this, &SIDGUI::on_plotXRayShortPrimary_toggled); + QObject::connect(ui->plotXRayShortSecondary, &ButtonSwitch::toggled, this, &SIDGUI::on_plotXRayShortSecondary_toggled); + QObject::connect(ui->plotGRB, &ButtonSwitch::toggled, this, &SIDGUI::on_plotGRB_toggled); + QObject::connect(ui->plotSTIX, &ButtonSwitch::toggled, this, &SIDGUI::on_plotSTIX_toggled); + QObject::connect(ui->plotProton, &ButtonSwitch::toggled, this, &SIDGUI::on_plotProton_toggled); + QObject::connect(ui->sdoEnabled, &ButtonSwitch::toggled, this, &SIDGUI::on_sdoEnabled_toggled); + QObject::connect(ui->sdoVideoEnabled, &ButtonSwitch::toggled, this, &SIDGUI::on_sdoVideoEnabled_toggled); + QObject::connect(ui->sdoData, qOverload(&QComboBox::currentIndexChanged), this, &SIDGUI::on_sdoData_currentIndexChanged); + QObject::connect(ui->sdoNow, &ButtonSwitch::toggled, this, &SIDGUI::on_sdoNow_toggled); + QObject::connect(ui->sdoDateTime, &WrappingDateTimeEdit::dateTimeChanged, this, &SIDGUI::on_sdoDateTime_dateTimeChanged); + QObject::connect(ui->showSats, &QToolButton::clicked, this, &SIDGUI::on_showSats_clicked); + QObject::connect(ui->map, &QComboBox::currentTextChanged, this, &SIDGUI::on_map_currentTextChanged); + QObject::connect(ui->showPaths, &QToolButton::clicked, this, &SIDGUI::on_showPaths_clicked); + QObject::connect(ui->autoscaleX, &QPushButton::clicked, this, &SIDGUI::on_autoscaleX_clicked); + QObject::connect(ui->autoscaleY, &QPushButton::clicked, this, &SIDGUI::on_autoscaleY_clicked); + QObject::connect(ui->today, &QPushButton::clicked, this, &SIDGUI::on_today_clicked); + QObject::connect(ui->prevDay, &QPushButton::clicked, this, &SIDGUI::on_prevDay_clicked); + QObject::connect(ui->nextDay, &QPushButton::clicked, this, &SIDGUI::on_nextDay_clicked); + QObject::connect(ui->startDateTime, &WrappingDateTimeEdit::dateTimeChanged, this, &SIDGUI::on_startDateTime_dateTimeChanged); + QObject::connect(ui->endDateTime, &WrappingDateTimeEdit::dateTimeChanged, this, &SIDGUI::on_endDateTime_dateTimeChanged); + QObject::connect(ui->y1Min, QOverload::of(&QDoubleSpinBox::valueChanged), this, &SIDGUI::on_y1Min_valueChanged); + QObject::connect(ui->y1Max, QOverload::of(&QDoubleSpinBox::valueChanged), this, &SIDGUI::on_y1Max_valueChanged); + QObject::connect(ui->deleteAll, &QToolButton::clicked, this, &SIDGUI::on_deleteAll_clicked); + QObject::connect(ui->saveData, &QToolButton::clicked, this, &SIDGUI::on_saveData_clicked); + QObject::connect(ui->loadData, &QToolButton::clicked, this, &SIDGUI::on_loadData_clicked); + QObject::connect(ui->saveChartImage, &QToolButton::clicked, this, &SIDGUI::on_saveChartImage_clicked); + QObject::connect(ui->settings, &QToolButton::clicked, this, &SIDGUI::on_settings_clicked); +} + +SIDGUI::ChannelMeasurement& SIDGUI::addMeasurements(const QString& id) +{ + ChannelMeasurement measurements = ChannelMeasurement(id, m_settings.m_samples); + m_channelMeasurements.append(measurements); + return m_channelMeasurements.last(); +} + +SIDGUI::ChannelMeasurement& SIDGUI::getMeasurements(const QString& id) +{ + for (int i = 0; i < m_channelMeasurements.size(); i++) + { + if (m_channelMeasurements[i].m_id == id) + { + return m_channelMeasurements[i]; + } + } + return addMeasurements(id); +} + +void SIDGUI::addMeasurement(const QString& id, QDateTime dateTime, double measurement) +{ + ChannelMeasurement& measurements = getMeasurements(id); + measurements.append(dateTime, measurement); + if (m_chartXAxis) + { + if (measurements.m_series) + { + updateMeasurementRange(measurement); + updateTimeRange(dateTime); + autoscaleX(); + autoscaleY(); + } + else + { + qDebug() << "addMeasurement - measurement has no series calling plotChart"; + plotChart(); + } + } + else + { + qDebug() << "addMeasurement with no m_chartXAxis - calling plotChart"; + plotChart(); + } +} + +void SIDGUI::autoscaleX() +{ + if (m_settings.m_autoscaleX) + { + if (m_maxDateTime.isValid() && (!m_settings.m_endDateTime.isValid() || (m_maxDateTime > m_settings.m_endDateTime))) { + ui->endDateTime->setDateTime(m_maxDateTime); + } + if (m_minDateTime.isValid() && (!m_settings.m_startDateTime.isValid() || (m_minDateTime < m_settings.m_startDateTime))) { + ui->startDateTime->setDateTime(m_minDateTime); + } + } +} + +void SIDGUI::autoscaleY() +{ + if (m_settings.m_autoscaleY) + { + if (!std::isnan(m_minMeasurement) && !std::isnan(m_maxMeasurement) && (m_minMeasurement == m_maxMeasurement)) + { + // Graph doesn't display properly if min is the same as max + ui->y1Min->setValue(m_minMeasurement * 0.99); + ui->y1Max->setValue(m_maxMeasurement * 1.01); + } + else + { + if (!std::isnan(m_minMeasurement) && (m_minMeasurement != m_settings.m_y1Min)) { + ui->y1Min->setValue(m_minMeasurement); + } + if (!std::isnan(m_maxMeasurement) && (m_maxMeasurement != m_settings.m_y1Max)) { + ui->y1Max->setValue(m_maxMeasurement); + } + } + } +} + +void SIDGUI::xRayDataUpdated(const QList& data, bool primary) +{ + // Data is at 1-minute intervals, for last 6 hours, so we want to merge with data with already have + // Assuems oldest data is first in the array + QDateTime start; + int idx = primary ? 0 : 1; + if (m_xrayShortMeasurements[idx].m_measurements.size() > 0) { + start = m_xrayShortMeasurements[idx].m_measurements.last().m_dateTime; + } + + for (const auto& measurement : data) + { + if (!start.isValid() || (measurement.m_dateTime > start)) + { + ChannelMeasurement* measurements; + + switch (measurement.m_band) + { + case GOESXRay::XRayData::SHORT: + measurements = &m_xrayShortMeasurements[idx]; + break; + case GOESXRay::XRayData::LONG: + measurements = &m_xrayLongMeasurements[idx]; + break; + default: + measurements = nullptr; + break; + } + // Ignore flux measurements of 0, as log10(0) is -Inf + if (measurements && (measurement.m_flux != 0.0)) + { + double logFlux = log10(measurement.m_flux); + measurements->append(measurement.m_dateTime, logFlux); + } + } + } + + plotChart(); +} + +void SIDGUI::protonDataUpdated(const QList& data, bool primary) +{ + (void) primary; + + QDateTime start; + + if (m_protonMeasurements[0].m_measurements.size() > 0) { + start = m_protonMeasurements[0].m_measurements.last().m_dateTime; + } + for (const auto& measurement : data) + { + if (!start.isValid() || (measurement.m_dateTime > start)) + { + ChannelMeasurement* measurements = nullptr; + + switch (measurement.m_energy) + { + case 10: + measurements = &m_protonMeasurements[0]; + break; + case 50: + measurements = &m_protonMeasurements[1]; + break; + case 100: + measurements = &m_protonMeasurements[2]; + break; + case 500: + measurements = &m_protonMeasurements[3]; + break; + } + + if (measurements) { + measurements->append(measurement.m_dateTime, measurement.m_flux); + } + } + } + + plotChart(); +} + +void SIDGUI::stixDataUpdated(const QList& data) +{ + m_stixData = data; + plotChart(); +} + +void SIDGUI::grbDataUpdated(const QList& data) +{ + m_grbData = data; + + // Calculate min/max of data + if (m_grbData.size() > 0) + { + m_grbMin = std::numeric_limits::max(); + m_grbMax = std::numeric_limits::min(); + for (int i = 0; i < m_grbData.size(); i++) + { + if ((m_grbData[i].m_fluence != 0.0f) && (m_grbData[i].m_fluence != -999.0f)) + { + m_grbMin = std::min(m_grbMin, m_grbData[i].m_fluence); + m_grbMax = std::max(m_grbMax, m_grbData[i].m_fluence); + } + } + } + + plotChart(); +} + +void SIDGUI::sdoImageUpdated(const QImage& image) +{ + bool setSize = ui->sdoImage->pixmap(Qt::ReturnByValueConstant()).isNull(); + + QPixmap pixmap; + pixmap.convertFromImage(image); + ui->sdoImage->setPixmap(pixmap); + + if (setSize) + { + QList sizes = ui->sdoSplitter->sizes(); + if (!((sizes[0] == 0) && (sizes[1] == 0))) + { + sizes[1] = std::max(sizes[1], 256); // Default size can be a bit small + ui->sdoSplitter->setSizes(sizes); + } + } +} + +void SIDGUI::on_sdoEnabled_toggled(bool checked) +{ + m_settings.m_sdoEnabled = checked; + ui->sdoData->setVisible(checked); + ui->sdoVideoEnabled->setVisible(checked); + ui->sdoContainer->setVisible(checked); + ui->sdoNow->setVisible(checked); + ui->sdoDateTime->setVisible(checked); + applySetting("sdoEnabled"); + applySDO(); +} + +void SIDGUI::on_sdoVideoEnabled_toggled(bool checked) +{ + m_settings.m_sdoVideoEnabled = checked; + applySetting("sdoVideoEnabled"); + + QString currentText = ui->sdoData->currentText(); + ui->sdoData->blockSignals(true); + ui->sdoData->clear(); + if (checked) + { + for (const auto& name : SolarDynamicsObservatory::getVideoNames()) { + ui->sdoData->addItem(name); + } + } + else + { + for (const auto& name : SolarDynamicsObservatory::getImageNames()) { + ui->sdoData->addItem(name); + } + } + ui->sdoData->blockSignals(false); + int idx = ui->sdoData->findText(currentText); + if (idx != -1) { + ui->sdoData->setCurrentIndex(idx); + } else { + ui->sdoData->setCurrentIndex(0); + } + + applySDO(); +} + +void SIDGUI::on_sdoNow_toggled(bool checked) +{ + m_settings.m_sdoNow = checked; + applySetting("sdoNow"); + ui->sdoDateTime->setEnabled(!m_settings.m_sdoNow); + ui->mapLabel->setEnabled(!m_settings.m_sdoNow); + ui->map->setEnabled(!m_settings.m_sdoNow); + applySDO(); + applyDateTime(); +} + +void SIDGUI::on_sdoData_currentIndexChanged(int index) +{ + (void) index; + + m_settings.m_sdoData = ui->sdoData->currentText(); + applySetting("sdoData"); + applySDO(); +} + +void SIDGUI::on_sdoDateTime_dateTimeChanged(QDateTime value) +{ + m_settings.m_sdoDateTime = value; + applySetting("sdoDateTime"); + if (!m_settings.m_sdoNow) + { + applySDO(); + applyDateTime(); + } +} + +void SIDGUI::applySDO() +{ + if (m_solarDynamicsObservatory) + { + ui->sdoImage->setVisible(!m_settings.m_sdoVideoEnabled); + ui->sdoVideo->setVisible(m_settings.m_sdoVideoEnabled); + if (m_player) { + m_player->stop(); + } + if (m_settings.m_sdoVideoEnabled) + { + QString videoURL = SolarDynamicsObservatory::getVideoURL(m_settings.m_sdoData); + if (!videoURL.isEmpty() && m_player) + { +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) + m_player->setMedia(QUrl(videoURL)); +#else + m_player->setSource(QUrl(videoURL)); +#endif + m_player->play(); + } + // Stop image updates + m_solarDynamicsObservatory->getImagePeriodically(m_settings.m_sdoData, 512, 0); + } + else + { + if (m_settings.m_sdoNow) { + m_solarDynamicsObservatory->getImagePeriodically(m_settings.m_sdoData); + } else { + m_solarDynamicsObservatory->getImage(m_settings.m_sdoData, m_settings.m_sdoDateTime); + } + } + } +} + +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) +// This doesn't seem to get called on Qt5 on Windows +void SIDGUI::sdoBufferStatusChanged(int percentFilled) +{ + ui->sdoProgressBar->setValue(percentFilled); +} +#else +void SIDGUI::sdoBufferProgressChanged(float filled) +{ + ui->sdoProgressBar->setValue((int)std::round(filled * 100.0f)); +} +#endif + +void SIDGUI::sdoVideoError(QMediaPlayer::Error error) +{ + qWarning() << "SIDGUI::sdoVideoError: " << error << m_player->errorString(); +#ifdef _MSC_VER + // Qt5/Windows doesn't support mp4 by default, so suggest K-Lite codecs + // Qt6 doesn't need these + if (error == QMediaPlayer::FormatError) { + QMessageBox::warning(this, "Video Error", "Unable to play video. Please try installing mp4/h264 codec, such as: K-Lite codedcs."); + } +#elif LINUX + if (error == QMediaPlayer::FormatError) { + QMessageBox::warning(this, "Video Error", "Unable to play video. Please try installing mp4/h264 codec, such as gstreamer libav."); + } +#else + if (error == QMediaPlayer::FormatError) { + QMessageBox::warning(this, "Video Error", "Unable to play video. Please try installing an mp4/h264 codec."); + } +#endif +} + +void SIDGUI::sdoVideoStatusChanged(QMediaPlayer::MediaStatus status) +{ + if (status == QMediaPlayer::LoadingMedia) + { + ui->sdoProgressBar->setValue(0); + ui->sdoProgressBar->setVisible(true); + } + else if (status == QMediaPlayer::BufferedMedia) + { + ui->sdoProgressBar->setValue(100); + ui->sdoProgressBar->setVisible(false); + } + else if (status == QMediaPlayer::EndOfMedia) + { + m_player->setPosition(0); + m_player->play(); + } +} + +void SIDGUI::applyDateTime() +{ + if (!m_settings.m_map.isEmpty() && (m_settings.m_map != "None")) + { + if (m_settings.m_sdoNow) { + FeatureWebAPIUtils::mapSetDateTime(QDateTime::currentDateTime()); + } else { + FeatureWebAPIUtils::mapSetDateTime(m_settings.m_sdoDateTime); + } + } +} + +void SIDGUI::on_showSats_clicked() +{ + // Create a Satellite Tracker feature + MainCore *mainCore = MainCore::instance(); + PluginAPI::FeatureRegistrations *featureRegistrations = mainCore->getPluginManager()->getFeatureRegistrations(); + int nbRegistrations = featureRegistrations->size(); + int index = 0; + + for (; index < nbRegistrations; index++) + { + if (featureRegistrations->at(index).m_featureId == "SatelliteTracker") { + break; + } + } + + if (index < nbRegistrations) + { + connect(mainCore, &MainCore::featureAdded, this, &SIDGUI::onSatTrackerAdded); + + MainCore::MsgAddFeature *msg = MainCore::MsgAddFeature::create(0, index); + mainCore->getMainMessageQueue()->push(msg); + } + else + { + QMessageBox::warning(this, "Error", "Satellite Tracker feature not available"); + } +} + +void SIDGUI::onSatTrackerAdded(int featureSetIndex, Feature *feature) +{ + if (feature->getURI() == "sdrangel.feature.satellitetracker") + { + disconnect(MainCore::instance(), &MainCore::featureAdded, this, &SIDGUI::onSatTrackerAdded); + + QJsonArray sats = {"SDO", "GOES 16", "GOES-18"}; + + ChannelWebAPIUtils::patchFeatureSetting(featureSetIndex, feature->getIndexInFeatureSet(), "satellites", sats); + + ChannelWebAPIUtils::patchFeatureSetting(featureSetIndex, feature->getIndexInFeatureSet(), "target", "SDO"); + + ChannelWebAPIUtils::runFeature(featureSetIndex, feature->getIndexInFeatureSet()); + } +} + +void SIDGUI::on_map_currentTextChanged(const QString& text) +{ + m_settings.m_map = text; + applySetting("map"); + applyDateTime(); +} + +// Plot paths from transmitters to receivers on map +void SIDGUI::on_showPaths_clicked() +{ + clearFromMap(); + + for (int i = 0; i < m_settings.m_channelSettings.size(); i++) + { + unsigned int deviceSetIndex; + unsigned int channelIndex; + + if (MainCore::getDeviceAndChannelIndexFromId(m_settings.m_channelSettings[i].m_id, deviceSetIndex, channelIndex)) + { + // Get position of device, defaulting to My Position + QGeoCoordinate rxPosition; + if (!ChannelWebAPIUtils::getDevicePosition(deviceSetIndex, rxPosition)) + { + rxPosition.setLatitude(MainCore::instance()->getSettings().getLatitude()); + rxPosition.setLongitude(MainCore::instance()->getSettings().getLongitude()); + rxPosition.setAltitude(MainCore::instance()->getSettings().getAltitude()); + } + + // Get position of transmitter + if (VLFTransmitters::m_callsignHash.contains(m_settings.m_channelSettings[i].m_label)) + { + const VLFTransmitters::Transmitter *transmitter = VLFTransmitters::m_callsignHash.value(m_settings.m_channelSettings[i].m_label); + QGeoCoordinate txPosition; + txPosition.setLatitude(transmitter->m_latitude); + txPosition.setLongitude(transmitter->m_longitude); + txPosition.setAltitude(0); + + // Calculate mid point for position of label + qreal distance = txPosition.distanceTo(rxPosition); + qreal az = txPosition.azimuthTo(rxPosition); + QGeoCoordinate midPoint = txPosition.atDistanceAndAzimuth(distance / 2.0, az); + + // Create a path from transmitter to receiver + QList mapPipes; + MainCore::instance()->getMessagePipes().getMessagePipes(m_sid, "mapitems", mapPipes); + if (mapPipes.size() > 0) + { + for (const auto& pipe : mapPipes) + { + MessageQueue *messageQueue = qobject_cast(pipe->m_element); + SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem(); + + QString deviceId = QString("%1%2").arg(m_settings.m_channelSettings[i].m_id[0]).arg(deviceSetIndex); + + QString name = QString("SID %1 to %2").arg(m_settings.m_channelSettings[i].m_label).arg(deviceId); + QString details = QString("%1
Distance: %2 km").arg(name).arg((int) std::round(distance / 1000.0)); + + swgMapItem->setName(new QString(name)); + swgMapItem->setLatitude(midPoint.latitude()); + swgMapItem->setLongitude(midPoint.longitude()); + swgMapItem->setAltitude(midPoint.altitude()); + QString image = QString("none"); + swgMapItem->setImage(new QString(image)); + swgMapItem->setImageRotation(0); + swgMapItem->setText(new QString(details)); // Not used - label is used instead for now + swgMapItem->setFixedPosition(true); + swgMapItem->setLabel(new QString(details)); + swgMapItem->setAltitudeReference(0); + QList *coords = new QList(); + + SWGSDRangel::SWGMapCoordinate* c = new SWGSDRangel::SWGMapCoordinate(); + c->setLatitude(rxPosition.latitude()); + c->setLongitude(rxPosition.longitude()); + c->setAltitude(rxPosition.altitude()); + coords->append(c); + + c = new SWGSDRangel::SWGMapCoordinate(); + c->setLatitude(txPosition.latitude()); + c->setLongitude(txPosition.longitude()); + c->setAltitude(txPosition.altitude()); + coords->append(c); + + swgMapItem->setColorValid(1); + swgMapItem->setColor(m_settings.m_channelSettings[i].m_color.rgba()); + + swgMapItem->setCoordinates(coords); + swgMapItem->setType(3); + + MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_sid, swgMapItem); + messageQueue->push(msg); + + m_mapItemNames.append(name); + } + } + } + } + } +} + +void SIDGUI::clearFromMap() +{ + QList mapPipes; + MainCore::instance()->getMessagePipes().getMessagePipes(m_sid, "mapitems", mapPipes); + + for (const auto& name : m_mapItemNames) + { + for (const auto& pipe : mapPipes) + { + MessageQueue *messageQueue = qobject_cast(pipe->m_element); + SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem(); + swgMapItem->setName(new QString(name)); + swgMapItem->setImage(new QString("")); + swgMapItem->setType(3); + MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_sid, swgMapItem); + messageQueue->push(msg); + } + } +} + +void SIDGUI::featuresChanged(const QStringList& renameFrom, const QStringList& renameTo) +{ + const AvailableChannelOrFeatureList availableFeatures = m_availableFeatureHandler.getAvailableChannelOrFeatureList(); + + if (renameFrom.contains(m_settings.m_map)) + { + m_settings.m_map = renameTo[renameFrom.indexOf(m_settings.m_map)]; + applySetting("map"); + } + + ui->map->blockSignals(true); + ui->map->clear(); + ui->map->addItem("None"); + for (const auto& map : availableFeatures) { + ui->map->addItem(map.getId()); + } + + int idx = ui->map->findText(m_settings.m_map); + if (idx >= 0) { + ui->map->setCurrentIndex(idx); + } else { + ui->map->setCurrentIndex(-1); + } + + ui->map->blockSignals(false); + + // If no setting, default to first available map + if (m_settings.m_map.isEmpty() && (ui->map->count() >= 2)) { + ui->map->setCurrentIndex(1); + } +} + +void SIDGUI::channelsChanged(const QStringList& renameFrom, const QStringList& renameTo, const QStringList& removed, const QStringList& added) +{ + removeChannels(removed); + + // Rename measurements and settings that have had their id changed + for (int i = 0; i < renameFrom.size(); i++) + { + for (int j = 0; j < m_channelMeasurements.size(); j++) + { + if (m_channelMeasurements[j].m_id == renameFrom[i]) { + m_channelMeasurements[j].m_id = renameTo[i]; + } + } + for (int j = 0; j < m_settings.m_channelSettings.size(); j++) + { + if (m_settings.m_channelSettings[j].m_id == renameFrom[i]) { + m_settings.m_channelSettings[j].m_id = renameTo[i]; + } + } + } + + // Create settings for any new channels + // Don't call createChannelSettings when channels are removed, as ids might not have been updated yet + if (added.size() > 0) + { + if (m_settings.createChannelSettings()) { + applySetting("channelSettings"); + } + } +} + +void SIDGUI::removeChannels(const QStringList& ids) +{ + for (int i = 0; i < ids.size(); i++) + { + for (int j = 0; j < m_channelMeasurements.size(); j++) + { + if (ids[i] == m_channelMeasurements[j].m_id) + { + m_channelMeasurements.removeAt(j); + break; + } + } + + for (int j = 0; j < m_settings.m_channelSettings.size(); j++) + { + if (ids[i] == m_settings.m_channelSettings[j].m_id) + { + m_settings.m_channelSettings.removeAt(j); + break; + } + } + } +} + +void SIDGUI::autosave() +{ + qDebug() << "SIDGUI::autosave start"; + writeCSV(m_settings.m_filename); + qDebug() << "SIDGUI::autosave done"; +} + +void SIDGUI::on_saveData_clicked() +{ + m_fileDialog.setAcceptMode(QFileDialog::AcceptSave); + if (m_fileDialog.exec()) + { + QStringList fileNames = m_fileDialog.selectedFiles(); + if (fileNames.size() > 0) { + writeCSV(fileNames[0]); + } + } +} + +void SIDGUI::on_loadData_clicked() +{ + m_fileDialog.setAcceptMode(QFileDialog::AcceptOpen); + if (m_fileDialog.exec()) + { + QStringList fileNames = m_fileDialog.selectedFiles(); + if (fileNames.size() > 0) { + readCSV(fileNames[0], false); + } + } +} + +void SIDGUI::writeCSV(const QString& filename) +{ + if (m_channelMeasurements.size() < 1) { + return; + } + + QFile file(filename); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) + { + QMessageBox::critical(this, "SID", QString("Failed to open file %1").arg(filename)); + return; + } + QTextStream out(&file); + + // Create a CSV file from the values in the table + QList idx; + QList measurements; + out << "Date and Time,"; + for (int i = 0; i < m_channelMeasurements.size(); i++) + { + SIDSettings::ChannelSettings *channelSettings = m_settings.getChannelSettings(m_channelMeasurements[i].m_id); + QString name = m_channelMeasurements[i].m_id; + if (channelSettings) + { + name.append("-"); + name.append(channelSettings->m_label); + } + out << name << ","; + measurements.append(&m_channelMeasurements[i]); + idx.append(0); + } + + out << "X-Ray Primary Short,"; + measurements.append(&m_xrayShortMeasurements[0]); + idx.append(0); + out << "X-Ray Primary Long,"; + measurements.append(&m_xrayLongMeasurements[0]); + idx.append(0); + out << "X-Ray Secondary Short,"; + measurements.append(&m_xrayShortMeasurements[1]); + idx.append(0); + out << "X-Ray Secondary Long,"; + measurements.append(&m_xrayLongMeasurements[1]); + idx.append(0); + + for (int i = 0; i < 4; i++) + { + out << QString("%1 Proton,").arg(SIDGUI::m_protonEnergies[i]); + measurements.append(&m_protonMeasurements[i]); + idx.append(0); + } + + out << "\n"; + + // Find earliest time + QDateTime t; + for (int i = 0; i < measurements.size(); i++) + { + ChannelMeasurement *cm = measurements[i]; + Measurement *m = &cm->m_measurements[idx[i]]; + if (!t.isValid() || (m->m_dateTime < t)) { + t = m->m_dateTime; + } + } + + bool done = false; + while (!done) + { + out << t.toUTC().toString(Qt::ISODateWithMs); + out << ","; + + // Output data at this time + for (int i = 0; i < measurements.size(); i++) + { + ChannelMeasurement *cm = measurements[i]; + if (cm->m_measurements.size() > idx[i]) + { + Measurement *m = &cm->m_measurements[idx[i]]; + if (m->m_dateTime == t) + { + out << m->m_measurement; + idx[i]++; + } + } + out << ","; + } + out << "\n"; + + // Find next time + t = QDateTime(); + for (int i = 0; i < measurements.size(); i++) + { + ChannelMeasurement *cm = measurements[i]; + if (cm->m_measurements.size() > idx[i]) + { + Measurement *m = &cm->m_measurements[idx[i]]; + if (!t.isValid() || (m->m_dateTime < t)) { + t = m->m_dateTime; + } + } + } + if (!t.isValid()) { + done = true; + } + } +} + +void SIDGUI::readCSV(const QString& filename, bool autoload) +{ + QFile file(filename); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + { + if (!autoload) { + QMessageBox::critical(this, "SID", QString("Failed to open file %1").arg(filename)); + } + return; + } + QTextStream in(&file); + + // Prevent data updates while reading CSV + disconnectDataUpdates(); + + // Delete existing data + + clearAllData(); + + // Get list of colors to use + QList colors = SIDSettings::m_defaultColors; + for (const auto& channelSettings : m_settings.m_channelSettings) { + colors.removeAll(channelSettings.m_color.rgb()); + } + + bool channelSettingsChanged = false; + QStringList colNames; + if (CSV::readRow(in, &colNames)) + { + QList measurements; + for (int i = 0; i < colNames.size() - 1; i++) { + measurements.append(nullptr); + } + for (int i = 1; i < colNames.size(); i++) + { + QString name = colNames[i]; + if (name == "X-Ray Primary Short") + { + measurements[i-1] = &m_xrayShortMeasurements[0]; + } + else if (name == "X-Ray Primary Long") + { + measurements[i-1] = &m_xrayLongMeasurements[0]; + } + else if (name == "X-Ray Secondary Short") + { + measurements[i-1] = &m_xrayShortMeasurements[1]; + } + else if (name == "X-Ray Secondary Long") + { + measurements[i-1] = &m_xrayLongMeasurements[1]; + } + else if (name.endsWith("Proton")) + { + for (int j = 0; j < m_protonEnergies.size(); j++) + { + if (name.startsWith(m_protonEnergies[j])) + { + measurements[i-1] = &m_protonMeasurements[j]; + break; + } + } + } + else if (name.contains(":")) + { + QString id; + + int idx = name.indexOf('-'); + if (idx >= 0) { + id = name.left(idx); + } else { + id = name; + } + measurements[i-1] = &addMeasurements(id); + + // Create settings, if we don't have them + SIDSettings::ChannelSettings *channelSettings = m_settings.getChannelSettings(id); + if (!channelSettings) + { + if (colors.size() == 0) { + colors = SIDSettings::m_defaultColors; + } + + SIDSettings::ChannelSettings newSettings; + newSettings.m_id = id; + newSettings.m_enabled = true; + newSettings.m_label = name.mid(idx + 1); + newSettings.m_color = colors.takeFirst(); + m_settings.m_channelSettings.append(newSettings); + channelSettingsChanged = true; + } + } + } + + QMessageBox dialog(this); + dialog.setText("Reading data"); + dialog.addButton(QMessageBox::Cancel); + dialog.show(); + QApplication::processEvents(); + + bool cancelled = false; + QStringList cols; + int row = 1; + + while(!cancelled && CSV::readRow(in, &cols)) + { + if (cols.size() == measurements.size() + 1) + { + QDateTime dateTime = QDateTime::fromString(cols[0], Qt::ISODateWithMs); + + for (int i = 0; i < measurements.size(); i++) + { + QString valueStr = cols[i+1]; + if (!valueStr.isEmpty()) + { + double value = valueStr.toDouble(); + measurements[i]->append(dateTime, value, false); + } + } + } + else + { + qDebug() << "SIDGUI::readCSV: Not enough data on row " << row; + } + if (row % 10000 == 0) + { + QApplication::processEvents(); + if (dialog.clickedButton()) { + cancelled = true; + } + } + row++; + } + + dialog.close(); + + autoscaleX(); + autoscaleY(); + plotChart(); + connectDataUpdates(); + getData(); + if (channelSettingsChanged) { + applySetting("channelSettings"); + } + } +} + +void SIDGUI::on_saveChartImage_clicked() +{ + QFileDialog fileDialog(nullptr, "Select file to save image to", "", "*.png *.jpg *.jpeg *.bmp *.ppm *.xbm *.xpm"); + fileDialog.setAcceptMode(QFileDialog::AcceptSave); + if (fileDialog.exec()) + { + QStringList fileNames = fileDialog.selectedFiles(); + if (fileNames.size() > 0) + { + QImage image(ui->chart->size(), QImage::Format_ARGB32); + image.fill(Qt::transparent); + QPainter painter(&image); + ui->chart->render(&painter); + if (!image.save(fileNames[0])) { + QMessageBox::critical(this, "SID", QString("Failed to save image to %1").arg(fileNames[0])); + } + } + } +} diff --git a/plugins/feature/sid/sidgui.h b/plugins/feature/sid/sidgui.h new file mode 100644 index 000000000..7807450d9 --- /dev/null +++ b/plugins/feature/sid/sidgui.h @@ -0,0 +1,317 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023-2024 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_SIDGUI_H_ +#define INCLUDE_FEATURE_SIDGUI_H_ + +#include +#include +#include +#include +#include +#include + +#include "feature/featuregui.h" +#include "util/messagequeue.h" +#include "util/movingaverage.h" +#include "util/grb.h" +#include "util/goesxray.h" +#include "util/solardynamicsobservatory.h" +#include "util/stix.h" +#include "settings/rollupstate.h" +#include "availablechannelorfeaturehandler.h" + +#include "sidsettings.h" + +class PluginAPI; +class FeatureUISet; +class SIDMain; + +namespace Ui { + class SIDGUI; +} + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +using namespace QtCharts; +#endif + +class SIDGUI : public FeatureGUI { + Q_OBJECT + + struct Measurement { + QDateTime m_dateTime; + double m_measurement; + Measurement(QDateTime dateTime, double measurement) : + m_dateTime(dateTime), + m_measurement(measurement) + { + } + }; + + struct ChannelMeasurement { + QString m_id; + QList m_measurements; + QXYSeries *m_series; + double m_minMeasurement; + double m_maxMeasurement; + MovingAverageUtilVar m_movingAverage; + + ChannelMeasurement() : + m_series(nullptr), + m_minMeasurement(std::numeric_limits::quiet_NaN()), + m_maxMeasurement(std::numeric_limits::quiet_NaN()), + m_movingAverage(1) + { + } + + ChannelMeasurement(const QString& id, int averageSamples) : + m_id(id), + m_series(nullptr), + m_minMeasurement(std::numeric_limits::quiet_NaN()), + m_maxMeasurement(std::numeric_limits::quiet_NaN()), + m_movingAverage(averageSamples) + { + } + + void append(QDateTime dateTime, double measurement, bool updateSeries=true) + { + m_measurements.append(Measurement(dateTime, measurement)); + if (std::isnan(m_minMeasurement)) { + m_minMeasurement = measurement; + } else { + m_minMeasurement = std::min(m_minMeasurement, measurement); + } + if (std::isnan(m_maxMeasurement)) { + m_maxMeasurement = measurement; + } else { + m_maxMeasurement = std::max(m_maxMeasurement, measurement); + } + if (m_series && updateSeries) { + appendSeries(dateTime, measurement); + } + } + + void newSeries(QXYSeries *series, int samples) + { + m_series = series; + m_movingAverage.resize(samples); + } + + void appendSeries(QDateTime dateTime, double measurement) + { + m_movingAverage(measurement); + m_series->append(dateTime.toMSecsSinceEpoch(), m_movingAverage.instantAverage()); + } + + void clear() + { + m_minMeasurement = std::numeric_limits::quiet_NaN(); + m_maxMeasurement = std::numeric_limits::quiet_NaN(); + m_measurements.clear(); + m_series = nullptr; + } + + }; + +public: + static SIDGUI* 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; } + virtual void setWorkspaceIndex(int index); + virtual int getWorkspaceIndex() const { return m_settings.m_workspaceIndex; } + virtual void setGeometryBytes(const QByteArray& blob) { m_settings.m_geometryBytes = blob; } + virtual QByteArray getGeometryBytes() const { return m_settings.m_geometryBytes; } + +private: + Ui::SIDGUI* ui; + PluginAPI* m_pluginAPI; + FeatureUISet* m_featureUISet; + SIDSettings m_settings; + QList m_settingsKeys; + RollupState m_rollupState; + bool m_doApplySettings; + + SIDMain* m_sid; + MessageQueue m_inputMessageQueue; + QTimer m_statusTimer; + QTimer m_autosaveTimer; + int m_lastFeatureState; + + QFileDialog m_fileDialog; + + QList m_channelMeasurements; + QDateTimeAxis *m_chartXAxis; + QValueAxis *m_chartY1Axis; + QCategoryAxis *m_chartY2Axis; + QLogValueAxis *m_chartY3Axis; + QLogValueAxis *m_chartProtonAxis; + double m_minMeasurement; + double m_maxMeasurement; + QDateTime m_minDateTime; + QDateTime m_maxDateTime; + + QDateTimeAxis *m_xRayChartXAxis; + QCategoryAxis *m_xRayChartYAxis; + + GOESXRay *m_goesXRay; + ChannelMeasurement m_xrayShortMeasurements[2]; // Primary and secondary + ChannelMeasurement m_xrayLongMeasurements[2]; + ChannelMeasurement m_protonMeasurements[4]; // 4 energy bands + static const QStringList m_protonEnergies; + + SolarDynamicsObservatory *m_solarDynamicsObservatory; + QStringList m_sdoImageNames; + QMediaPlayer *m_player; + + GRB *m_grb; + QList m_grbData; + QScatterSeries *m_grbSeries; + float m_grbMin; + float m_grbMax; + + STIX *m_stix; + QList m_stixData; + QScatterSeries *m_stixSeries; + + AvailableChannelOrFeatureHandler m_availableFeatureHandler; + AvailableChannelOrFeatureHandler m_availableChannelHandler; + + QStringList m_mapItemNames; + + explicit SIDGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent = nullptr); + virtual ~SIDGUI(); + + void blockApplySettings(bool block); + void applySetting(const QString& settingsKey); + void applySettings(const QStringList& settingsKeys, bool force = false); + void applyAllSettings(); + void displaySettings(); + bool handleMessage(const Message& message); + void makeUIConnections(); + + void writeCSV(const QString& filename); + void readCSV(const QString& filename, bool autoload); + void setAutosaveTimer(); + + ChannelMeasurement& addMeasurements(const QString& id); + ChannelMeasurement& getMeasurements(const QString& id); + void addMeasurement(const QString& id, QDateTime dateTime, double measurement); + + void plotChart(); + void plotXRayChart(); + void createGRBSeries(QChart *chart, QDateTimeAxis *xAxis, QLogValueAxis *yAxis); + void createXRaySeries(QChart *chart, QDateTimeAxis *xAxis, QCategoryAxis *yAxis); + void createProtonSeries(QChart *chart, QDateTimeAxis *xAxis, QLogValueAxis *yAxis); + void createSTIXSeries(QChart *chart, QDateTimeAxis *xAxis, QCategoryAxis *yAxis); + void createFlareAxis(QCategoryAxis *yAxis); + void setXAxisRange(); + void setY1AxisRange(); + void setAutoscaleX(); + void setAutoscaleY(); + void autoscaleX(); + void autoscaleY(); + void setButtonBackground(QToolButton *button, bool checked); + void updateMeasurementRange(double measurement); + void updateTimeRange(QDateTime dateTime); + void applySDO(); + void applyDateTime(); + bool eventFilter(QObject *obj, QEvent *event) override; + void sendToSkyMap(const AvailableChannelOrFeature& skymap, float ra, float dec); + void showGRBContextMenu(QContextMenuEvent *contextEvent, QChartView *chartView, int closestPoint); + void showStixContextMenu(QContextMenuEvent *contextEvent, QChartView *chartView, int closestPoint); + void showContextMenu(QContextMenuEvent *contextEvent); + bool findClosestPoint(QContextMenuEvent *contextEvent, QChart *chart, QScatterSeries *series, int& closestPoint); + void clearMinMax(); + bool plotAnyXRay() const; + void clearAllData(); + void connectDataUpdates(); + void disconnectDataUpdates(); + void getData(); + void clearFromMap(); + + static qreal pixelDistance(QChart *chart, QAbstractSeries *series, QPointF a, QPointF b); + +private slots: + void onMenuDialogCalled(const QPoint &p); + void onWidgetRolled(QWidget* widget, bool rollDown); + void handleInputMessages(); + void on_startStop_toggled(bool checked); + void on_samples_valueChanged(int value); + void on_separateCharts_toggled(bool checked); + void on_displayLegend_toggled(bool checked); + void on_plotXRayLongPrimary_toggled(bool checked); + void on_plotXRayLongSecondary_toggled(bool checked); + void on_plotXRayShortPrimary_toggled(bool checked); + void on_plotXRayShortSecondary_toggled(bool checked); + void on_plotGRB_toggled(bool checked); + void on_plotSTIX_toggled(bool checked); + void on_plotProton_toggled(bool checked); + void on_deleteAll_clicked(); + void on_autoscaleX_clicked(); + void on_autoscaleY_clicked(); + void on_today_clicked(); + void on_prevDay_clicked(); + void on_nextDay_clicked(); + void on_startDateTime_dateTimeChanged(QDateTime value); + void on_endDateTime_dateTimeChanged(QDateTime value); + void on_y1Min_valueChanged(double value); + void on_y1Max_valueChanged(double value); + void on_saveData_clicked(); + void on_loadData_clicked(); + void on_saveChartImage_clicked(); + void autoscaleXRightClicked(); + void autoscaleYRightClicked(); + void todayRightClicked(); + void updateStatus(); + void autosave(); + void on_settings_clicked(); + void xRayDataUpdated(const QList& data, bool primary); + void protonDataUpdated(const QList& data, bool primary); + void grbDataUpdated(const QList& data); + void stixDataUpdated(const QList& data); + void legendMarkerClicked(); + void seriesClicked(const QPointF &point); + void chartSplitterMoved(int pos, int index); + void sdoSplitterMoved(int pos, int index); + void on_sdoEnabled_toggled(bool checked); + void on_sdoVideoEnabled_toggled(bool checked); + void on_sdoData_currentIndexChanged(int index); + void on_sdoNow_toggled(bool checked); + void on_sdoDateTime_dateTimeChanged(QDateTime value); + void sdoImageUpdated(const QImage& image); + void sdoVideoError(QMediaPlayer::Error error); + void sdoVideoStatusChanged(QMediaPlayer::MediaStatus status); +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) + void sdoBufferStatusChanged(int percentFilled); +#else + void sdoBufferProgressChanged(float filled); +#endif + void on_showSats_clicked(); + void onSatTrackerAdded(int featureSetIndex, Feature *feature); + void on_map_currentTextChanged(const QString& text); + void on_showPaths_clicked(); + void featuresChanged(const QStringList& renameFrom, const QStringList& renameTo); + void channelsChanged(const QStringList& renameFrom, const QStringList& renameTo, const QStringList& removed, const QStringList& added); + void removeChannels(const QStringList& ids); +}; + +#endif // INCLUDE_FEATURE_SIDGUI_H_ diff --git a/plugins/feature/sid/sidgui.ui b/plugins/feature/sid/sidgui.ui new file mode 100644 index 000000000..71a17c413 --- /dev/null +++ b/plugins/feature/sid/sidgui.ui @@ -0,0 +1,781 @@ + + + SIDGUI + + + + 0 + 0 + 1044 + 580 + + + + + 0 + 0 + + + + + 320 + 100 + + + + + 9 + + + + SID + + + Qt::LeftToRight + + + + + 10 + 10 + 964 + 80 + + + + Settings + + + + 3 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + + + Start/stop measurements + + + + + + + :/play.png + :/stop.png:/play.png + + + + + + + Load data from a .csv file + + + + + + + :/load.png:/load.png + + + + + + + Save data to a .csv file + + + + + + + :/save.png:/save.png + + + + + + + Save chart to an image file + + + + + + + :/picture.png:/picture.png + + + + + + + Delete all data + + + + + + + :/bin.png:/bin.png + + + + + + + Qt::Vertical + + + + + + + Avg + + + + + + + Number of samples in average + + + 1 + + + 1000 + + + + + + + Qt::Vertical + + + + + + + + 0 + 0 + + + + Display primary long wavelength X-Ray data on chart + + + + + + + :/sid/icons/xlp.svg:/sid/icons/xlp.svg + + + + + + + + 0 + 0 + + + + Display secondary long wavelength X-Ray data on chart + + + + + + + :/sid/icons/xls.svg:/sid/icons/xls.svg + + + + + + + Display primary short wavelength X-Ray data on chart + + + + + + + :/sid/icons/xsp.svg:/sid/icons/xsp.svg + + + + + + + Display secondary short wavelength X-Ray data on chart + + + + + + + :/sid/icons/xss.svg:/sid/icons/xss.svg + + + + + + + + 0 + 0 + + + + Display proton flux data on chart + + + + + + + :/sid/icons/proton.svg:/sid/icons/proton.svg + + + + + + + Display GRBs on chart + + + + + + + :/sid/icons/gamma.svg:/sid/icons/gamma.svg + + + + + + + Display solar flares from STIX on chart + + + + + + + :/sid/icons/solar-orbiter.svg:/sid/icons/solar-orbiter.svg + + + + + + + Qt::Vertical + + + + + + + Display as a single chart or multiple charts + + + + + + + :/sid/icons/chartcombined.png + :/sid/icons/chartseparate.png:/sid/icons/chartcombined.png + + + + + + + Display legend + + + + + + + :/sid/icons/legend.png:/sid/icons/legend.png + + + + + + + Qt::Vertical + + + + + + + Open settings dialog + + + + + + + :/listing.png:/listing.png + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + SDO/SOHO + + + + + + + Display SDO/SOHO imagery + + + + + + + :/sid/icons/sun.png:/sid/icons/sun.png + + + + + + + Select image or video + + + + + + + :/picture.png + :/film.png:/picture.png + + + + + + + Image/wavelength selection + + + + + + + Show GOES 16, 18 and SDO in Satellite Tracker + + + + + + + :/gps.png:/gps.png + + + + + + + + + + + Autoscale X-axis. Right click to continually autoscale + + + X + + + + + + + Autoscale Y-axis. Right click to continually autoscale + + + Y + + + + + + + Set X-axis range to today. Right click to set to today's daylight hours. + + + T + + + + + + + Set X-axis range to -1 day + + + -1 + + + + + + + Set X-axis range to +1 day + + + +1 + + + + + + + Qt::Vertical + + + + + + + Start + + + + + + + X axis start time + + + + + + + End + + + + + + + X axis end time + + + + + + + Qt::Vertical + + + + + + + Min + + + + + + + 1 + + + -150.000000000000000 + + + -100.000000000000000 + + + + + + + dB + + + + + + + Max + + + + + + + 1 + + + -150.000000000000000 + + + + + + + dB + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + When checked SDO data is the latest available. When unchecked, date and time may be set manually + + + Now + + + + + + + Date and time for SDO data + + + + + + + Map + + + + + + + 3D Map feature to send date and time to + + + + None + + + + + + + + Show propagation paths on map + + + + + + + :/world.png:/world.png + + + + + + + + + + + 10 + 100 + 661 + 384 + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + + 0 + 0 + + + + Qt::Vertical + + + + + 100 + 100 + + + + Power vs Time + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + + + + + 0 + 0 + + + + + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + + + + + + + + + + ButtonSwitch + QToolButton +
gui/buttonswitch.h
+
+ + RollupContents + QWidget +
gui/rollupcontents.h
+ 1 +
+ + QChartView + QGraphicsView +
QtCharts
+
+ + WrappingDateTimeEdit + QDateTimeEdit +
gui/wrappingdatetimeedit.h
+ 1 +
+ + ScaledImage + QLabel +
gui/scaledimage.h
+
+ + QVideoWidget + QWidget +
qvideowidget.h
+ 1 +
+
+ + + + + +
diff --git a/plugins/feature/sid/sidplugin.cpp b/plugins/feature/sid/sidplugin.cpp new file mode 100644 index 000000000..dd22bdf9a --- /dev/null +++ b/plugins/feature/sid/sidplugin.cpp @@ -0,0 +1,79 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 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 "sidgui.h" +#endif +#include "sid.h" +#include "sidplugin.h" +#include "sidwebapiadapter.h" + +const PluginDescriptor SIDPlugin::m_pluginDescriptor = { + SIDMain::m_featureId, + QStringLiteral("SID"), + QStringLiteral("7.20.0"), + QStringLiteral("(c) Jon Beniston, M7RCE"), + QStringLiteral("https://github.com/f4exb/sdrangel"), + true, + QStringLiteral("https://github.com/f4exb/sdrangel") +}; + +SIDPlugin::SIDPlugin(QObject* parent) : + QObject(parent), + m_pluginAPI(nullptr) +{ +} + +const PluginDescriptor& SIDPlugin::getPluginDescriptor() const +{ + return m_pluginDescriptor; +} + +void SIDPlugin::initPlugin(PluginAPI* pluginAPI) +{ + m_pluginAPI = pluginAPI; + + m_pluginAPI->registerFeature(SIDMain::m_featureIdURI, SIDMain::m_featureId, this); +} + +#ifdef SERVER_MODE +FeatureGUI* SIDPlugin::createFeatureGUI(FeatureUISet *featureUISet, Feature *feature) const +{ + (void) featureUISet; + (void) feature; + return nullptr; +} +#else +FeatureGUI* SIDPlugin::createFeatureGUI(FeatureUISet *featureUISet, Feature *feature) const +{ + return SIDGUI::create(m_pluginAPI, featureUISet, feature); +} +#endif + +Feature* SIDPlugin::createFeature(WebAPIAdapterInterface* webAPIAdapterInterface) const +{ + return new SIDMain(webAPIAdapterInterface); +} + +FeatureWebAPIAdapter* SIDPlugin::createFeatureWebAPIAdapter() const +{ + return new SIDWebAPIAdapter(); +} diff --git a/plugins/feature/sid/sidplugin.h b/plugins/feature/sid/sidplugin.h new file mode 100644 index 000000000..009b22772 --- /dev/null +++ b/plugins/feature/sid/sidplugin.h @@ -0,0 +1,49 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 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_SIDPLUGIN_H +#define INCLUDE_FEATURE_SIDPLUGIN_H + +#include +#include "plugin/plugininterface.h" + +class FeatureGUI; +class WebAPIAdapterInterface; + +class SIDPlugin : public QObject, PluginInterface { + Q_OBJECT + Q_INTERFACES(PluginInterface) + Q_PLUGIN_METADATA(IID "sdrangel.feature.sid") + +public: + explicit SIDPlugin(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_SIDPLUGIN_H diff --git a/plugins/feature/sid/sidsettings.cpp b/plugins/feature/sid/sidsettings.cpp new file mode 100644 index 000000000..7b3dadc68 --- /dev/null +++ b/plugins/feature/sid/sidsettings.cpp @@ -0,0 +1,710 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023-2024 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 "channel/channelwebapiutils.h" +#include "device/deviceset.h" +#include "device/deviceapi.h" +#include "maincore.h" + +#include "sidsettings.h" + +// https://medialab.github.io/iwanthue/ +// Restricted dark colours and chroma at either end +const QList SIDSettings::m_defaultColors = { + 0xdd4187, + 0x7ce048, + 0xc944db, + 0xd5d851, + 0x826add, + 0x5da242, + 0xc97bc1, + 0x85e49b, + 0xdf5035, + 0x57d6d9, + 0xd28e2e, + 0x7091d3, + 0xa3a052, + 0xd36d76, + 0x4aa47d, + 0xc9895a, + }; + +const QList SIDSettings::m_defaultXRayShortColors = { + 0x8a3ffc, + 0x8a3ffc +}; + +const QList SIDSettings::m_defaultXRayLongColors = { + 0x4589ff, + 0x0f62fe +}; + +const QList SIDSettings::m_defaultProtonColors = { + 0x9ef0f0, + 0x3ddbd9, + 0x08bdba, + 0x009d9a +}; + +const QRgb SIDSettings::m_defaultGRBColor = 0xffffff; +const QRgb SIDSettings::m_defaultSTIXColor = 0xcccc00; + +SIDSettings::SIDSettings() : + m_rollupState(nullptr), + m_workspaceIndex(0) +{ + resetToDefaults(); +} + +void SIDSettings::resetToDefaults() +{ + m_channelSettings = {}; + m_period = 10.0f; + + m_autosave = true; + m_autoload = true; + m_filename = "sid_autosave.csv"; + m_autosavePeriod = 10; + + m_samples = 1; + m_autoscaleX = true; + m_autoscaleY = true; + m_separateCharts = true; + m_displayLegend = true; + m_legendAlignment = Qt::AlignTop; + m_displayAxisTitles = true; + m_displaySecondaryAxis = true; + m_plotXRayLongPrimary = true; + m_plotXRayLongSecondary = false; + m_plotXRayShortPrimary = true; + m_plotXRayShortSecondary = false; + m_plotGRB = true; + m_plotSTIX = true; + m_plotProton = true; + m_y1Min = -100.0f; + m_y1Max = 0.0f; + m_startDateTime = QDateTime(); + m_endDateTime = QDateTime(); + m_xrayShortColors = m_defaultXRayShortColors; + m_xrayLongColors = m_defaultXRayLongColors; + m_protonColors = m_defaultProtonColors; + m_grbColor = m_defaultGRBColor; + m_stixColor =m_defaultSTIXColor; + + m_sdoEnabled = true; + m_sdoVideoEnabled = false; + m_sdoData = ""; + m_sdoNow = true; + m_sdoDateTime = QDateTime(); + m_map = ""; + + m_title = "SID"; + 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; +} + +QByteArray SIDSettings::serialize() const +{ + SimpleSerializer s(1); + + s.writeList(1, m_channelSettings); + s.writeFloat(2, m_period); + + s.writeBool(10, m_autosave); + s.writeBool(11, m_autoload); + s.writeString(12, m_filename); + s.writeS32(13, m_autosavePeriod); + + s.writeS32(20, m_samples); + s.writeBool(21, m_autoscaleX); + s.writeBool(22, m_autoscaleY); + s.writeBool(23, m_separateCharts); + s.writeBool(24, m_displayLegend); + s.writeS32(25, (int) m_legendAlignment); + s.writeBool(26, m_displayAxisTitles); + s.writeBool(27, m_displaySecondaryAxis); + s.writeBool(28, m_plotXRayLongPrimary); + s.writeBool(29, m_plotXRayLongSecondary); + s.writeBool(30, m_plotXRayShortPrimary); + s.writeBool(31, m_plotXRayShortSecondary); + s.writeBool(32, m_plotGRB); + s.writeBool(33, m_plotSTIX); + s.writeBool(34, m_plotProton); + + s.writeFloat(36, m_y1Min); + s.writeFloat(37, m_y1Max); + if (m_startDateTime.isValid()) { + s.writeS64(38, m_startDateTime.toMSecsSinceEpoch()); + } + if (m_endDateTime.isValid()) { + s.writeS64(39, m_endDateTime.toMSecsSinceEpoch()); + } + + s.writeList(40, m_xrayShortColors); + s.writeList(41, m_xrayLongColors); + s.writeList(42, m_protonColors); + s.writeU32(43, m_grbColor); + s.writeU32(44, m_stixColor); + + s.writeBool(50, m_sdoEnabled); + s.writeBool(51, m_sdoVideoEnabled); + s.writeString(52, m_sdoData); + s.writeBool(53, m_sdoNow); + if (m_sdoDateTime.isValid()) { + s.writeS64(54, m_sdoDateTime.toMSecsSinceEpoch()); + } + s.writeString(55, m_map); + + s.writeList(60, m_sdoSplitterSizes); + s.writeList(61, m_chartSplitterSizes); + + s.writeString(70, m_title); + s.writeU32(71, m_rgbColor); + s.writeBool(72, m_useReverseAPI); + s.writeString(73, m_reverseAPIAddress); + s.writeU32(74, m_reverseAPIPort); + s.writeU32(75, m_reverseAPIFeatureSetIndex); + s.writeU32(76, m_reverseAPIFeatureIndex); + + if (m_rollupState) { + s.writeBlob(77, m_rollupState->serialize()); + } + + s.writeS32(78, m_workspaceIndex); + s.writeBlob(79, m_geometryBytes); + + return s.final(); +} + +bool SIDSettings::deserialize(const QByteArray& data) +{ + SimpleDeserializer d(data); + + if (!d.isValid()) + { + resetToDefaults(); + return false; + } + + if (d.getVersion() == 1) + { + QByteArray bytetmp; + uint32_t utmp; + qint64 tmp64; + QString strtmp; + QByteArray blob; + + d.readList(1, &m_channelSettings); + d.readFloat(2, &m_period, 10.0f); + + d.readBool(10, &m_autosave, true); + d.readBool(11, &m_autoload, true); + d.readString(12, &m_filename, "sid_autosave.csv"); + d.readS32(13, &m_autosavePeriod, 10); + + + d.readS32(20, &m_samples, 1); + d.readBool(21, &m_autoscaleX, true); + d.readBool(22, &m_autoscaleY, true); + d.readBool(23, &m_separateCharts, true); + d.readBool(24, &m_displayLegend, true); + d.readS32(25, (int *) &m_legendAlignment, Qt::AlignTop); + d.readBool(26, &m_displayAxisTitles, true); + d.readBool(27, &m_displaySecondaryAxis, true); + d.readBool(28, &m_plotXRayLongPrimary, true); + d.readBool(29, &m_plotXRayLongSecondary, false); + d.readBool(30, &m_plotXRayShortPrimary, true); + d.readBool(31, &m_plotXRayShortSecondary, false); + d.readBool(32, &m_plotGRB, true); + d.readBool(33, &m_plotSTIX, true); + d.readBool(34, &m_plotProton, false); + + d.readFloat(36, &m_y1Min, -100.0f); + d.readFloat(37, &m_y1Max, 0.0f); + if (d.readS64(38, &tmp64)) { + m_startDateTime = QDateTime::fromMSecsSinceEpoch(tmp64); + } else { + m_startDateTime = QDateTime(); + } + if (d.readS64(39, &tmp64)) { + m_endDateTime = QDateTime::fromMSecsSinceEpoch(tmp64); + } else { + m_endDateTime = QDateTime(); + } + + d.readList(40, &m_xrayShortColors); + if (m_xrayShortColors.size() != 2) { + m_xrayShortColors = m_defaultXRayShortColors; + } + d.readList(41, &m_xrayLongColors); + if (m_xrayLongColors.size() != 2) { + m_xrayLongColors = m_defaultXRayLongColors; + } + d.readList(42, &m_protonColors); + if (m_protonColors.size() != 4) { + m_protonColors = m_defaultProtonColors; + } + d.readU32(43, &m_grbColor, m_defaultGRBColor); + d.readU32(44, &m_stixColor, m_defaultSTIXColor); + + d.readBool(50, &m_sdoEnabled, true); + d.readBool(51, &m_sdoVideoEnabled, false); + d.readString(52, &m_sdoData, ""); + d.readBool(53, &m_sdoNow); + if (d.readS64(54, &tmp64)) { + m_sdoDateTime = QDateTime::fromMSecsSinceEpoch(tmp64); + } else { + m_sdoDateTime = QDateTime(); + } + d.readString(55, &m_map, ""); + + d.readList(60, &m_sdoSplitterSizes); + d.readList(61, &m_chartSplitterSizes); + + d.readString(70, &m_title, "SID"); + d.readU32(71, &m_rgbColor, QColor(102, 0, 102).rgb()); + d.readBool(72, &m_useReverseAPI, false); + d.readString(73, &m_reverseAPIAddress, "127.0.0.1"); + d.readU32(74, &utmp, 0); + + if ((utmp > 1023) && (utmp < 65535)) { + m_reverseAPIPort = utmp; + } else { + m_reverseAPIPort = 8888; + } + + d.readU32(75, &utmp, 0); + m_reverseAPIFeatureSetIndex = utmp > 99 ? 99 : utmp; + d.readU32(76, &utmp, 0); + m_reverseAPIFeatureIndex = utmp > 99 ? 99 : utmp; + + if (m_rollupState) + { + d.readBlob(77, &bytetmp); + m_rollupState->deserialize(bytetmp); + } + + d.readS32(78, &m_workspaceIndex, 0); + d.readBlob(79, &m_geometryBytes); + + return true; + } + else + { + resetToDefaults(); + return false; + } +} + +void SIDSettings::applySettings(const QStringList& settingsKeys, const SIDSettings& settings) +{ + if (settingsKeys.contains("channelSettings")) { + m_channelSettings = settings.m_channelSettings; + } + if (settingsKeys.contains("period")) { + m_period = settings.m_period; + } + if (settingsKeys.contains("autosave")) { + m_autosave = settings.m_autosave; + } + if (settingsKeys.contains("autoload")) { + m_autoload = settings.m_autoload; + } + if (settingsKeys.contains("autosavePeriod")) { + m_autosavePeriod = settings.m_autosavePeriod; + } + if (settingsKeys.contains("filename")) { + m_filename = settings.m_filename; + } + if (settingsKeys.contains("samples")) { + m_samples = settings.m_samples; + } + if (settingsKeys.contains("autoscaleX")) { + m_autoscaleX = settings.m_autoscaleX; + } + if (settingsKeys.contains("autoscaleY")) { + m_autoscaleY = settings.m_autoscaleY; + } + if (settingsKeys.contains("separateCharts")) { + m_separateCharts = settings.m_separateCharts; + } + if (settingsKeys.contains("displayLegend")) { + m_displayLegend = settings.m_displayLegend; + } + if (settingsKeys.contains("legendAlignment")) { + m_legendAlignment = settings.m_legendAlignment; + } + if (settingsKeys.contains("displayAxisTitles")) { + m_displayAxisTitles = settings.m_displayAxisTitles; + } + if (settingsKeys.contains("displayAxisLabels")) { + m_displaySecondaryAxis = settings.m_displaySecondaryAxis; + } + if (settingsKeys.contains("plotXRayLongPrimary")) { + m_plotXRayLongPrimary = settings.m_plotXRayLongPrimary; + } + if (settingsKeys.contains("plotXRayLongSecondary")) { + m_plotXRayLongSecondary = settings.m_plotXRayLongSecondary; + } + if (settingsKeys.contains("plotXRayShortPrimary")) { + m_plotXRayShortPrimary = settings.m_plotXRayShortPrimary; + } + if (settingsKeys.contains("plotXRayShorSecondary")) { + m_plotXRayShortSecondary = settings.m_plotXRayShortSecondary; + } + if (settingsKeys.contains("plotGRB")) { + m_plotGRB = settings.m_plotGRB; + } + if (settingsKeys.contains("plotSTIX")) { + m_plotSTIX = settings.m_plotSTIX; + } + if (settingsKeys.contains("plotProton")) { + m_plotProton = settings.m_plotProton; + } + if (settingsKeys.contains("startDateTime")) { + m_startDateTime = settings.m_startDateTime; + } + if (settingsKeys.contains("endDateTime")) { + m_endDateTime = settings.m_endDateTime; + } + if (settingsKeys.contains("y1Min")) { + m_y1Min = settings.m_y1Min; + } + if (settingsKeys.contains("y1Max")) { + m_y1Max = settings.m_y1Max; + } + if (settingsKeys.contains("xrayShortColors")) { + m_xrayShortColors = settings.m_xrayShortColors; + } + if (settingsKeys.contains("xrayLongColors")) { + m_xrayLongColors = settings.m_xrayLongColors; + } + if (settingsKeys.contains("protonColors")) { + m_protonColors = settings.m_protonColors; + } + if (settingsKeys.contains("grbColor")) { + m_grbColor = settings.m_grbColor; + } + if (settingsKeys.contains("stixColor")) { + m_stixColor = settings.m_stixColor; + } + if (settingsKeys.contains("sdoEnabled")) { + m_sdoEnabled = settings.m_sdoEnabled; + } + if (settingsKeys.contains("sdoVideoEnabled")) { + m_sdoVideoEnabled = settings.m_sdoVideoEnabled; + } + if (settingsKeys.contains("sdoData")) { + m_sdoData = settings.m_sdoData; + } + if (settingsKeys.contains("sdoNow")) { + m_sdoNow = settings.m_sdoNow; + } + if (settingsKeys.contains("sdoDateTime")) { + m_sdoDateTime = settings.m_sdoDateTime; + } + if (settingsKeys.contains("map")) { + m_map = settings.m_map; + } + if (settingsKeys.contains("sdoSplitterSizes")) { + m_sdoSplitterSizes = settings.m_sdoSplitterSizes; + } + if (settingsKeys.contains("chartSplitterSizes")) { + m_chartSplitterSizes = settings.m_chartSplitterSizes; + } + if (settingsKeys.contains("title")) { + m_title = settings.m_title; + } + if (settingsKeys.contains("rgbColor")) { + m_rgbColor = settings.m_rgbColor; + } + if (settingsKeys.contains("useReverseAPI")) { + m_useReverseAPI = settings.m_useReverseAPI; + } + if (settingsKeys.contains("reverseAPIAddress")) { + m_reverseAPIAddress = settings.m_reverseAPIAddress; + } + if (settingsKeys.contains("reverseAPIPort")) { + m_reverseAPIPort = settings.m_reverseAPIPort; + } + if (settingsKeys.contains("reverseAPIFeatureSetIndex")) { + m_reverseAPIFeatureSetIndex = settings.m_reverseAPIFeatureSetIndex; + } + if (settingsKeys.contains("reverseAPIFeatureIndex")) { + m_reverseAPIFeatureIndex = settings.m_reverseAPIFeatureIndex; + } + if (settingsKeys.contains("workspaceIndex")) { + m_workspaceIndex = settings.m_workspaceIndex; + } +} + +QString SIDSettings::getDebugString(const QStringList& settingsKeys, bool force) const +{ + std::ostringstream ostr; + + if (settingsKeys.contains("channelSettings")) + { + QStringList s; + for (auto cs : m_channelSettings) { + s.append(cs.m_id); + } + ostr << " m_channelSettings: " << s.join(",").toStdString(); + } + if (settingsKeys.contains("period") || force) { + ostr << " m_period: " << m_period; + } + if (settingsKeys.contains("autosave") || force) { + ostr << " m_autosave: " << m_autosave; + } + if (settingsKeys.contains("autoload") || force) { + ostr << " m_autoload: " << m_autoload; + } + if (settingsKeys.contains("filename") || force) { + ostr << " m_filename: " << m_filename.toStdString(); + } + if (settingsKeys.contains("samples") || force) { + ostr << " m_samples: " << m_samples; + } + if (settingsKeys.contains("autoscaleX") || force) { + ostr << " m_autoscaleX: " << m_autoscaleX; + } + if (settingsKeys.contains("autoscaleY") || force) { + ostr << " m_autoscaleY: " << m_autoscaleY; + } + if (settingsKeys.contains("separateCharts") || force) { + ostr << " m_separateCharts: " << m_separateCharts; + } + if (settingsKeys.contains("displayLegend") || force) { + ostr << " m_displayLegend: " << m_displayLegend; + } + if (settingsKeys.contains("legendAlignment") || force) { + ostr << " m_legendAlignment: " << m_legendAlignment; + } + if (settingsKeys.contains("displayAxisTitles") || force) { + ostr << " m_displayAxisTitles: " << m_displayAxisTitles; + } + if (settingsKeys.contains("displayAxisLabels") || force) { + ostr << " m_displaySecondaryAxis: " << m_displaySecondaryAxis; + } + if (settingsKeys.contains("plotXRayLongPrimary") || force) { + ostr << " m_plotXRayLongPrimary: " << m_plotXRayLongPrimary; + } + if (settingsKeys.contains("plotXRayLongSecondary") || force) { + ostr << " m_plotXRayLongSecondary: " << m_plotXRayLongSecondary; + } + if (settingsKeys.contains("plotXRayShortPrimary") || force) { + ostr << " m_plotXRayShortPrimary: " << m_plotXRayShortPrimary; + } + if (settingsKeys.contains("plotXRayShortSecondary") || force) { + ostr << " m_plotXRayShortSecondary: " << m_plotXRayShortSecondary; + } + if (settingsKeys.contains("plotGRB") || force) { + ostr << " m_plotGRB: " << m_plotGRB; + } + if (settingsKeys.contains("plotSTIX") || force) { + ostr << " m_plotSTIX: " << m_plotSTIX; + } + if (settingsKeys.contains("plotProton") || force) { + ostr << " m_plotProton: " << m_plotProton; + } + if (settingsKeys.contains("startDateTime") || force) { + ostr << " m_startDateTime: " << m_startDateTime.toString().toStdString(); + } + if (settingsKeys.contains("endDateTime") || force) { + ostr << " m_endDateTime: " << m_endDateTime.toString().toStdString(); + } + if (settingsKeys.contains("y1Min") || force) { + ostr << " m_y1Min: " << m_y1Min; + } + if (settingsKeys.contains("y1Max") || force) { + ostr << " m_y1Max: " << m_y1Max; + } + if (settingsKeys.contains("sdoEnabled") || force) { + ostr << " m_sdoEnabled: " << m_sdoEnabled; + } + if (settingsKeys.contains("sdoVideoEnabled") || force) { + ostr << " m_sdoVideoEnabled: " << m_sdoVideoEnabled; + } + if (settingsKeys.contains("sdoData") || force) { + ostr << " m_sdoData: " << m_sdoData.toStdString(); + } + if (settingsKeys.contains("sdoNow") || force) { + ostr << " m_sdoNow: " << m_sdoNow; + } + if (settingsKeys.contains("sdoDateTime") || force) { + ostr << " m_sdoDateTime: " << m_sdoDateTime.toString().toStdString(); + } + if (settingsKeys.contains("map") || force) { + ostr << " m_map: " << m_map.toStdString(); + } + if (settingsKeys.contains("title") || force) { + ostr << " m_title: " << m_title.toStdString(); + } + if (settingsKeys.contains("rgbColor") || force) { + ostr << " m_rgbColor: " << m_rgbColor; + } + if (settingsKeys.contains("useReverseAPI") || force) { + ostr << " m_useReverseAPI: " << m_useReverseAPI; + } + if (settingsKeys.contains("reverseAPIAddress") || force) { + ostr << " m_reverseAPIAddress: " << m_reverseAPIAddress.toStdString(); + } + if (settingsKeys.contains("reverseAPIPort") || force) { + ostr << " m_reverseAPIPort: " << m_reverseAPIPort; + } + if (settingsKeys.contains("reverseAPIFeatureSetIndex") || force) { + ostr << " m_reverseAPIFeatureSetIndex: " << m_reverseAPIFeatureSetIndex; + } + if (settingsKeys.contains("reverseAPIFeatureIndex") || force) { + ostr << " m_reverseAPIFeatureIndex: " << m_reverseAPIFeatureIndex; + } + if (settingsKeys.contains("workspaceIndex") || force) { + ostr << " m_workspaceIndex: " << m_workspaceIndex; + } + + return QString(ostr.str().c_str()); +} + +SIDSettings::ChannelSettings *SIDSettings::getChannelSettings(const QString& id) +{ + for (int i = 0; i < m_channelSettings.size(); i++) + { + if (m_channelSettings[i].m_id == id) { + return &m_channelSettings[i]; + } + } + return nullptr; +} + +bool SIDSettings::createChannelSettings() +{ + bool settingsChanged = false; + QStringList ids; + QStringList titles; + + getChannels(ids, titles); + + // Create settings for channels we don't currently have settings for + for (int i = 0; i < ids.size(); i++) + { + SIDSettings::ChannelSettings *channelSettings = getChannelSettings(ids[i]); + if (!channelSettings) + { + SIDSettings::ChannelSettings newSettings; + newSettings.m_id = ids[i]; + newSettings.m_enabled = true; + newSettings.m_label = titles[i]; + newSettings.m_color = SIDSettings::m_defaultColors[i % SIDSettings::m_defaultColors.size()]; + m_channelSettings.append(newSettings); + settingsChanged = true; + } + } + + return settingsChanged; +} + +// Get channels that have channelPowerDB value in their report +void SIDSettings::getChannels(QStringList& ids, QStringList& titles) +{ + MainCore *mainCore = MainCore::instance(); + + std::vector deviceSets = mainCore->getDeviceSets(); + for (unsigned int deviceSetIndex = 0; deviceSetIndex < deviceSets.size(); deviceSetIndex++) + { + DeviceSet *deviceSet = deviceSets[deviceSetIndex]; + + for (int channelIndex = 0; channelIndex < deviceSet->getNumberOfChannels(); channelIndex++) + { + QString title; + ChannelWebAPIUtils::getChannelSetting(deviceSetIndex, channelIndex, "title", title); + + double power; + if (ChannelWebAPIUtils::getChannelReportValue(deviceSetIndex, channelIndex, "channelPowerDB", power)) + { + ChannelAPI *channel = mainCore->getChannel(deviceSetIndex, channelIndex); + + QString id = mainCore->getChannelId(channel); + + ids.append(id); + titles.append(title); + } + } + } +} + +QByteArray SIDSettings::ChannelSettings::serialize() const +{ + SimpleSerializer s(1); + + s.writeString(1, m_id); + s.writeBool(2, m_enabled); + s.writeString(3, m_label); + s.writeU32(4, m_color.rgb()); + + return s.final(); +} + +bool SIDSettings::ChannelSettings::deserialize(const QByteArray& data) +{ + SimpleDeserializer d(data); + + if (!d.isValid()) { + return false; + } + + if (d.getVersion() == 1) + { + QByteArray blob; + quint32 utmp; + + d.readString(1, &m_id); + d.readBool(2, &m_enabled, true); + d.readString(3, &m_label); + d.readU32(4, &utmp); + m_color = utmp; + + return true; + } + else + { + return false; + } +} + +QDataStream& operator<<(QDataStream& out, const SIDSettings::ChannelSettings& settings) +{ + out << settings.serialize(); + return out; +} + +QDataStream& operator>>(QDataStream& in, SIDSettings::ChannelSettings& settings) +{ + QByteArray data; + in >> data; + settings.deserialize(data); + return in; +} diff --git a/plugins/feature/sid/sidsettings.h b/plugins/feature/sid/sidsettings.h new file mode 100644 index 000000000..10414ee33 --- /dev/null +++ b/plugins/feature/sid/sidsettings.h @@ -0,0 +1,117 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023-2024 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_SIDSETTINGS_H_ +#define INCLUDE_FEATURE_SIDSETTINGS_H_ + +#include +#include +#include +#include + +#include "util/message.h" + +class Serializable; + +struct SIDSettings +{ + struct ChannelSettings + { + QString m_id; + bool m_enabled; + QColor m_color; + QString m_label; + + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + }; + + QList m_channelSettings; // Channels to record power from + float m_period; // Mesaurement period, in seconds + + bool m_autosave; + bool m_autoload; + QString m_filename; // Filename for autosave + int m_autosavePeriod; // In minutes + + int m_samples; // Number of samples in average + bool m_autoscaleX; + bool m_autoscaleY; + bool m_separateCharts; + bool m_displayLegend; + Qt::Alignment m_legendAlignment; + bool m_displayAxisTitles; + bool m_displaySecondaryAxis; + bool m_plotXRayLongPrimary; + bool m_plotXRayLongSecondary; + bool m_plotXRayShortPrimary; + bool m_plotXRayShortSecondary; + bool m_plotGRB; + bool m_plotSTIX; + bool m_plotProton; + QDateTime m_startDateTime; + QDateTime m_endDateTime; + float m_y1Min; + float m_y1Max; + QList m_xrayShortColors; + QList m_xrayLongColors; + QList m_protonColors; + QRgb m_grbColor; + QRgb m_stixColor; + + bool m_sdoEnabled; + bool m_sdoVideoEnabled; + QString m_sdoData; + bool m_sdoNow; + QDateTime m_sdoDateTime; + QString m_map; // 3D map Id to send date/time to + + QList m_sdoSplitterSizes; + QList m_chartSplitterSizes; + + 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; + int m_workspaceIndex; + QByteArray m_geometryBytes; + + SIDSettings(); + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + void setRollupState(Serializable *rollupState) { m_rollupState = rollupState; } + void applySettings(const QStringList& settingsKeys, const SIDSettings& settings); + QString getDebugString(const QStringList& settingsKeys, bool force=false) const; + ChannelSettings *getChannelSettings(const QString& id); + void getChannels(QStringList& ids, QStringList& titles); + bool createChannelSettings(); + + static const QList m_defaultColors; + static const QList m_defaultXRayShortColors; + static const QList m_defaultXRayLongColors; + static const QList m_defaultProtonColors; + static const QRgb m_defaultGRBColor; + static const QRgb m_defaultSTIXColor; +}; + +#endif // INCLUDE_FEATURE_SIDSETTINGS_H_ diff --git a/plugins/feature/sid/sidsettingsdialog.cpp b/plugins/feature/sid/sidsettingsdialog.cpp new file mode 100644 index 000000000..98aa21c64 --- /dev/null +++ b/plugins/feature/sid/sidsettingsdialog.cpp @@ -0,0 +1,178 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023-2024 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 "util/units.h" +#include "gui/colordialog.h" +#include "gui/tablecolorchooser.h" + +#include "sidsettingsdialog.h" + +SIDSettingsDialog::SIDSettingsDialog(SIDSettings *settings, QWidget* parent) : + QDialog(parent), + ui(new Ui::SIDSettingsDialog), + m_settings(settings), + m_fileDialog(nullptr, "Select file to write autosave CSV data to", "", "*.csv") +{ + ui->setupUi(this); + ui->period->setValue(m_settings->m_period); + ui->autosave->setChecked(m_settings->m_autosave); + ui->autoload->setChecked(m_settings->m_autoload); + ui->filename->setText(m_settings->m_filename); + ui->autosavePeriod->setValue(m_settings->m_autosavePeriod); + switch (m_settings->m_legendAlignment) { + case Qt::AlignTop: + ui->legendAlignment->setCurrentIndex(0); + break; + case Qt::AlignRight: + ui->legendAlignment->setCurrentIndex(1); + break; + case Qt::AlignBottom: + ui->legendAlignment->setCurrentIndex(2); + break; + case Qt::AlignLeft: + ui->legendAlignment->setCurrentIndex(3); + break; + } + ui->displayAxisTitles->setChecked(m_settings->m_displayAxisTitles); + ui->displaySecondaryAxis->setChecked(m_settings->m_displaySecondaryAxis); + + m_settings->createChannelSettings(); + + // Add settings to table + for (int i = 0; i < m_settings->m_channelSettings.size(); i++) + { + SIDSettings::ChannelSettings *channelSettings = &m_settings->m_channelSettings[i]; + + int row = ui->channels->rowCount(); + ui->channels->setRowCount(row+1); + + ui->channels->setItem(row, CHANNELS_COL_ID, new QTableWidgetItem(channelSettings->m_id)); + + QTableWidgetItem *enableItem = new QTableWidgetItem(); + enableItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled); + enableItem->setCheckState(channelSettings->m_enabled ? Qt::Checked : Qt::Unchecked); + ui->channels->setItem(row, CHANNELS_COL_ENABLED, enableItem); + + ui->channels->setItem(row, CHANNELS_COL_LABEL, new QTableWidgetItem(channelSettings->m_label)); + + TableColorChooser *colorGUI = new TableColorChooser(ui->channels, row, CHANNELS_COL_COLOR, false, channelSettings->m_color.rgba()); + m_channelColorGUIs.append(colorGUI); + } + ui->channels->resizeColumnsToContents(); + + addColor("Primary Long X-Ray", m_settings->m_xrayLongColors[0]); + addColor("Secondary Long X-Ray", m_settings->m_xrayLongColors[1]); + addColor("Primary Short X-Ray ", m_settings->m_xrayShortColors[0]); + addColor("Secondary Short X-Ray", m_settings->m_xrayShortColors[1]); + addColor("GRB", m_settings->m_grbColor); + addColor("STIX", m_settings->m_stixColor); + addColor("10 MeV Proton", m_settings->m_protonColors[0]); + addColor("100 MeV Proton", m_settings->m_protonColors[2]); + ui->colors->resizeColumnsToContents(); +} + +void SIDSettingsDialog::addColor(const QString& name, QRgb rgb) +{ + int row = ui->colors->rowCount(); + ui->colors->setRowCount(row+1); + + ui->colors->setItem(row, COLORS_COL_NAME, new QTableWidgetItem(name)); + + TableColorChooser *colorGUI = new TableColorChooser(ui->colors, row, COLORS_COL_COLOR, false, rgb); + m_colorGUIs.append(colorGUI); +} + +SIDSettingsDialog::~SIDSettingsDialog() +{ + delete ui; + qDeleteAll(m_channelColorGUIs); + qDeleteAll(m_colorGUIs); +} + +void SIDSettingsDialog::accept() +{ + m_settings->m_period = ui->period->value(); + m_settings->m_autosave = ui->autosave->isChecked(); + m_settings->m_autoload = ui->autoload->isChecked(); + m_settings->m_filename = ui->filename->text(); + m_settings->m_autosavePeriod = ui->autosavePeriod->value(); + + switch (ui->legendAlignment->currentIndex() ) { + case 0: + m_settings->m_legendAlignment = Qt::AlignTop; + break; + case 1: + m_settings->m_legendAlignment = Qt::AlignRight; + break; + case 2: + m_settings->m_legendAlignment = Qt::AlignBottom; + break; + case 3: + m_settings->m_legendAlignment = Qt::AlignLeft; + break; + } + m_settings->m_displayAxisTitles = ui->displayAxisTitles->isChecked(); + m_settings->m_displaySecondaryAxis = ui->displaySecondaryAxis->isChecked(); + + m_settings->m_xrayLongColors[0] = m_colorGUIs[0]->m_color; + m_settings->m_xrayLongColors[1] = m_colorGUIs[1]->m_color; + m_settings->m_xrayShortColors[0] = m_colorGUIs[2]->m_color; + m_settings->m_xrayShortColors[1] = m_colorGUIs[3]->m_color; + m_settings->m_grbColor = m_colorGUIs[4]->m_color; + m_settings->m_stixColor = m_colorGUIs[5]->m_color; + m_settings->m_protonColors[0] = m_colorGUIs[6]->m_color; + m_settings->m_protonColors[2] = m_colorGUIs[7]->m_color; + + if (m_removeIds.size() > 0) { + emit removeChannels(m_removeIds); + } + for (int i = 0; i < m_settings->m_channelSettings.size(); i++) + { + SIDSettings::ChannelSettings *channelSettings = &m_settings->m_channelSettings[i]; + + channelSettings->m_id = ui->channels->item(i, CHANNELS_COL_ID)->text(); + channelSettings->m_enabled = ui->channels->item(i, CHANNELS_COL_ENABLED)->checkState() == Qt::Checked; + channelSettings->m_label = ui->channels->item(i, CHANNELS_COL_LABEL)->text(); + channelSettings->m_color = m_channelColorGUIs[i]->m_color; + } + + QDialog::accept(); +} + +void SIDSettingsDialog::on_browse_clicked() +{ + m_fileDialog.setAcceptMode(QFileDialog::AcceptSave); + if (m_fileDialog.exec()) + { + QStringList fileNames = m_fileDialog.selectedFiles(); + if (fileNames.size() > 0) { + ui->filename->setText(fileNames[0]); + } + } +} + +void SIDSettingsDialog::on_remove_clicked() +{ + QItemSelectionModel *select = ui->channels->selectionModel(); + while (select->hasSelection()) + { + QModelIndexList list = select->selectedRows(); + int row = list[0].row(); + m_removeIds.append(ui->channels->item(row, CHANNELS_COL_ID)->text()); + ui->channels->removeRow(row); + } +} diff --git a/plugins/feature/sid/sidsettingsdialog.h b/plugins/feature/sid/sidsettingsdialog.h new file mode 100644 index 000000000..4cc02ba33 --- /dev/null +++ b/plugins/feature/sid/sidsettingsdialog.h @@ -0,0 +1,68 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023-2024 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_SIDSETTINGSDIALOG_H +#define INCLUDE_SIDSETTINGSDIALOG_H + +#include + +#include "ui_sidsettingsdialog.h" +#include "sidsettings.h" + +class TableColorChooser; + +class SIDSettingsDialog : public QDialog { + Q_OBJECT + +public: + explicit SIDSettingsDialog(SIDSettings *settings, QWidget* parent = 0); + ~SIDSettingsDialog(); + +private: + void addColor(const QString& name, QRgb rgb); + +private slots: + void accept(); + void on_browse_clicked(); + void on_remove_clicked(); + +private: + Ui::SIDSettingsDialog* ui; + SIDSettings *m_settings; + QList m_channelColorGUIs; + QList m_colorGUIs; + QFileDialog m_fileDialog; + QStringList m_removeIds; + + enum ChannelsRows { + CHANNELS_COL_ID, + CHANNELS_COL_ENABLED, + CHANNELS_COL_LABEL, + CHANNELS_COL_COLOR + }; + + enum ColorsRows { + COLORS_COL_NAME, + COLORS_COL_COLOR + }; + +signals: + void removeChannels(const QStringList& ids); + +}; + +#endif // INCLUDE_SIDSETTINGSDIALOG_H diff --git a/plugins/feature/sid/sidsettingsdialog.ui b/plugins/feature/sid/sidsettingsdialog.ui new file mode 100644 index 000000000..499bdc6ff --- /dev/null +++ b/plugins/feature/sid/sidsettingsdialog.ui @@ -0,0 +1,348 @@ + + + SIDSettingsDialog + + + + 0 + 0 + 441 + 800 + + + + + Liberation Sans + 9 + + + + SID Settings + + + + + + Data + + + + + + Channels to record power from: + + + + + + + QAbstractItemView::MultiSelection + + + QAbstractItemView::SelectRows + + + true + + + + ID + + + + + Enabled + + + + + Label + + + + + Colour + + + + + + + + + 150 + 0 + + + + Measurement period (s) + + + + + + + Specifies the time period in seconds between each power measurement + + + 3 + + + 0.001000000000000 + + + 100000.000000000000000 + + + 10.000000000000000 + + + + + + + Remove selected channels and data + + + Remove + + + + + + + + + + Autosave + + + + + + Autosave Period (min) + + + + + + + + + + + + + + Autoload + + + + + + + Autosave filename + + + + + + + + + + + 150 + 0 + + + + Autosave + + + + + + + 1 + + + 0.100000000000000 + + + 1000.000000000000000 + + + 10.000000000000000 + + + + + + + Automatically load last autosave data + + + + + + + + + + ... + + + + + + + + + + Charts + + + + + + + 150 + 0 + + + + Legend Position + + + + + + + Position of legend + + + + Top + + + + + Right + + + + + Bottom + + + + + Left + + + + + + + + Display axis titles + + + + + + + + + + + + + + Display secondary axis + + + + + + + Whether to display secondary axis scale (E.g. for GRB / Proton flux on right of chart) + + + + + + + + + + + Series + + + + + Colour + + + + + + + + Colours: + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + SIDSettingsDialog + accept() + + + 257 + 31 + + + 157 + 274 + + + + + buttonBox + rejected() + SIDSettingsDialog + reject() + + + 325 + 31 + + + 286 + 274 + + + + + diff --git a/plugins/feature/sid/sidwebapiadapter.cpp b/plugins/feature/sid/sidwebapiadapter.cpp new file mode 100644 index 000000000..607a74833 --- /dev/null +++ b/plugins/feature/sid/sidwebapiadapter.cpp @@ -0,0 +1,52 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 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 "sid.h" +#include "sidwebapiadapter.h" + +SIDWebAPIAdapter::SIDWebAPIAdapter() +{} + +SIDWebAPIAdapter::~SIDWebAPIAdapter() +{} + +int SIDWebAPIAdapter::webapiSettingsGet( + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setSidSettings(new SWGSDRangel::SWGSIDSettings()); + response.getSidSettings()->init(); + SIDMain::webapiFormatFeatureSettings(response, m_settings); + + return 200; +} + +int SIDWebAPIAdapter::webapiSettingsPutPatch( + bool force, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) force; // no action + (void) errorMessage; + SIDMain::webapiUpdateFeatureSettings(m_settings, featureSettingsKeys, response); + + return 200; +} diff --git a/plugins/feature/sid/sidwebapiadapter.h b/plugins/feature/sid/sidwebapiadapter.h new file mode 100644 index 000000000..d1e15d5b4 --- /dev/null +++ b/plugins/feature/sid/sidwebapiadapter.h @@ -0,0 +1,50 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 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_SID_WEBAPIADAPTER_H +#define INCLUDE_SID_WEBAPIADAPTER_H + +#include "feature/featurewebapiadapter.h" +#include "sidsettings.h" + +/** + * Standalone API adapter only for the settings + */ +class SIDWebAPIAdapter : public FeatureWebAPIAdapter { +public: + SIDWebAPIAdapter(); + virtual ~SIDWebAPIAdapter(); + + 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: + SIDSettings m_settings; +}; + +#endif // INCLUDE_SID_WEBAPIADAPTER_H diff --git a/plugins/feature/sid/sidworker.cpp b/plugins/feature/sid/sidworker.cpp new file mode 100644 index 000000000..ab7b234fb --- /dev/null +++ b/plugins/feature/sid/sidworker.cpp @@ -0,0 +1,161 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 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 "webapi/webapiadapterinterface.h" +#include "webapi/webapiutils.h" +#include "channel/channelwebapiutils.h" +#include "device/deviceset.h" +#include "device/deviceapi.h" +#include "maincore.h" + +#include "sid.h" +#include "sidworker.h" + +SIDWorker::SIDWorker(SIDMain *sid, WebAPIAdapterInterface *webAPIAdapterInterface) : + m_sid(sid), + m_webAPIAdapterInterface(webAPIAdapterInterface), + m_msgQueueToFeature(nullptr), + m_msgQueueToGUI(nullptr), + m_pollTimer(this) +{ +} + +SIDWorker::~SIDWorker() +{ + stopWork(); + m_inputMessageQueue.clear(); +} + +void SIDWorker::startWork() +{ + qDebug("SIDWorker::startWork"); + QMutexLocker mutexLocker(&m_mutex); + connect(&m_pollTimer, &QTimer::timeout, this, &SIDWorker::update); + m_pollTimer.start(1000); + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + // Handle any messages already on the queue + handleInputMessages(); +} + +void SIDWorker::stopWork() +{ + qDebug("SIDWorker::stopWork"); + QMutexLocker mutexLocker(&m_mutex); + disconnect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + m_pollTimer.stop(); + disconnect(&m_pollTimer, &QTimer::timeout, this, &SIDWorker::update); +} + +void SIDWorker::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool SIDWorker::handleMessage(const Message& cmd) +{ + if (SIDMain::MsgConfigureSID::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + SIDMain::MsgConfigureSID& cfg = (SIDMain::MsgConfigureSID&) cmd; + + applySettings(cfg.getSettings(), cfg.getSettingsKeys(), cfg.getForce()); + return true; + } + else + { + return false; + } +} + +void SIDWorker::applySettings(const SIDSettings& settings, const QList& settingsKeys, bool force) +{ + qDebug() << "SIDWorker::applySettings:" << settings.getDebugString(settingsKeys, force) << force; + + if (settingsKeys.contains("period") || force) + { + m_pollTimer.stop(); + m_pollTimer.start(settings.m_period * 1000); + } + + if (force) { + m_settings = settings; + } else { + m_settings.applySettings(settingsKeys, settings); + } +} + +void SIDWorker::update() +{ + // Get powers from each channel + QDateTime dateTime = QDateTime::currentDateTime(); + QStringList ids; + QList measurements; + + for (const auto& channelSettings : m_settings.m_channelSettings) + { + if (channelSettings.m_enabled) + { + unsigned int deviceSetIndex, channelIndex; + + if (MainCore::getDeviceAndChannelIndexFromId(channelSettings.m_id, deviceSetIndex, channelIndex)) + { + // Check device is running + std::vector deviceSets = MainCore::instance()->getDeviceSets(); + if (deviceSetIndex < deviceSets.size()) + { + DeviceSet *deviceSet = deviceSets[deviceSetIndex]; + if (deviceSet && (deviceSet->m_deviceAPI->state() == DeviceAPI::StRunning)) + { + double power; + if (ChannelWebAPIUtils::getChannelReportValue(deviceSetIndex, channelIndex, "channelPowerDB", power)) + { + if (getMessageQueueToGUI()) + { + ids.append(channelSettings.m_id); + measurements.append(power); + } + } + else + { + qDebug() << "SIDWorker::update: Failed to get power for channel " << channelSettings.m_id; + } + } + } + } + else + { + qDebug() << "SIDWorker::update: Malformed channel id: " << channelSettings.m_id; + } + } + } + + if (getMessageQueueToGUI() && (ids.size() > 0)) + { + SIDMain::MsgMeasurement *msgToGUI = SIDMain::MsgMeasurement::create(dateTime, ids, measurements); + getMessageQueueToGUI()->push(msgToGUI); + } +} diff --git a/plugins/feature/sid/sidworker.h b/plugins/feature/sid/sidworker.h new file mode 100644 index 000000000..44f12b880 --- /dev/null +++ b/plugins/feature/sid/sidworker.h @@ -0,0 +1,67 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 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_SIDWORKER_H_ +#define INCLUDE_FEATURE_SIDWORKER_H_ + +#include +#include + +#include "util/message.h" +#include "util/messagequeue.h" + +#include "sid.h" +#include "sidsettings.h" + +class WebAPIAdapterInterface; +class SIDMain; + +class SIDWorker : public QObject +{ + Q_OBJECT +public: + + SIDWorker(SIDMain *m_sid, WebAPIAdapterInterface *webAPIAdapterInterface); + ~SIDWorker(); + void startWork(); + void stopWork(); + MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } + void setMessageQueueToFeature(MessageQueue *messageQueue) { m_msgQueueToFeature = messageQueue; } + void setMessageQueueToGUI(MessageQueue *messageQueue) { m_msgQueueToGUI = messageQueue; } + +private: + + SIDMain *m_sid; + WebAPIAdapterInterface *m_webAPIAdapterInterface; + MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication + MessageQueue *m_msgQueueToFeature; //!< Queue to report channel change to main feature object + MessageQueue *m_msgQueueToGUI; + SIDSettings m_settings; + QRecursiveMutex m_mutex; + QTimer m_pollTimer; + + bool handleMessage(const Message& cmd); + void applySettings(const SIDSettings& settings, const QList& settingsKeys, bool force = false); + MessageQueue *getMessageQueueToGUI() { return m_msgQueueToGUI; } + +private slots: + void handleInputMessages(); + void update(); +}; + +#endif // INCLUDE_FEATURE_SIDWORKER_H_ diff --git a/plugins/feature/skymap/readme.md b/plugins/feature/skymap/readme.md index b1f16d390..11430978b 100644 --- a/plugins/feature/skymap/readme.md +++ b/plugins/feature/skymap/readme.md @@ -13,6 +13,10 @@ With the ESASky map, a host of astronomical data is available from JWST, Hubble, ![Sky Map feature ESASky](../../../doc/img/SkyMap_ESASky.png) +Additionally, a map of the Moon can be viewed in 2D or 3D, that supports a variety of data layers from various Lunar missions, such as LRO (Lunar Reconnaissance Orbiter). + +![Moon map](../../../doc/img/SkyMap_Moon.png) +

Interface

![Sky Map feature plugin GUI](../../../doc/img/SkyMap_settings.png) @@ -38,6 +42,7 @@ Allows you to select the sky map: * WWT - World Wide Telescope * ESASky * Aladin +* Moon - Map of the Moon Each map provides different features, image and data sets. @@ -128,6 +133,14 @@ ESASky and Aladin are able to overlay catalog data:

Attribution

+WWT is from [NumFOCUS](https://worldwidetelescope.org/about/). + +ESASky is from [ESAC | ESA](https://www.cosmos.esa.int/web/esdc/esasky-credits). + +Aladin Sky Atlas is from [Centre de Données astronomiques de Strasbourg | CDS](https://aladin.cds.unistra.fr/). + +Moon map is from [Applied Coherent Technology | ACT](https://www.actgate.com/). + Constellation icons created by Freepik - https://www.flaticon.com

API

diff --git a/plugins/feature/skymap/skymapgui.cpp b/plugins/feature/skymap/skymapgui.cpp index 1cc7e9655..0da9aa04d 100644 --- a/plugins/feature/skymap/skymapgui.cpp +++ b/plugins/feature/skymap/skymapgui.cpp @@ -200,7 +200,8 @@ SkyMapGUI::SkyMapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature * m_featureUISet(featureUISet), m_doApplySettings(true), m_source(nullptr), - m_availableChannelOrFeatureHandler(SkyMapSettings::m_pipeURIs, {"target", "skymap.target"}) + m_availableChannelOrFeatureHandler(SkyMapSettings::m_pipeURIs, {"target", "skymap.target"}), + m_ready(false) { m_feature = feature; setAttribute(Qt::WA_DeleteOnClose, true); @@ -294,12 +295,17 @@ void SkyMapGUI::on_map_currentIndexChanged(int index) m_settings.m_map = ui->map->currentText(); applySetting("map"); + m_ready = false; if (m_settings.m_map == "WWT") { ui->web->load(QUrl(QString("http://127.0.0.1:%1/skymap/html/wwt.html").arg(m_webPort))); } else if (m_settings.m_map == "ESASky") { ui->web->load(QUrl(QString("http://127.0.0.1:%1/skymap/html/esasky.html").arg(m_webPort))); } else if (m_settings.m_map == "Aladin") { ui->web->load(QUrl(QString("http://127.0.0.1:%1/skymap/html/aladin.html").arg(m_webPort))); + } else if (m_settings.m_map == "Moon") { + ui->web->load(QUrl(QString("http://quickmap.lroc.asu.edu/"))); // Jumping straight to 3D view doesn't seem to work + setStatusText(""); + m_ready = true; } updateToolbar(); updateBackgrounds(); @@ -497,13 +503,21 @@ void SkyMapGUI::applyAllSettings() void SkyMapGUI::find(const QString& text) { - float ra, dec; + if (!m_ready) + { + // Save for when ready + m_find = text; + } + else + { + float ra, dec; - // WWT's find doesn't support coordinates, so we check here - if (Units::stringToRADec(text, ra, dec)) { - m_webInterface->setView(ra, dec); - } else { - m_webInterface->track(text); + // WWT's find doesn't support coordinates, so we check here + if (Units::stringToRADec(text, ra, dec)) { + m_webInterface->setView(ra, dec); + } else { + m_webInterface->track(text); + } } } @@ -765,7 +779,15 @@ void SkyMapGUI::receivedEvent(const QJsonObject &obj) } else if (event == "ready") { + m_ready = true; initSkyMap(); + + // Run find that was requested while map was initialising + if (!m_find.isEmpty()) + { + find(m_find); + m_find = ""; + } } } else @@ -965,6 +987,7 @@ void SkyMapGUI::updateToolbar() bool namesVisible = false; bool projectionVisible = true; bool backgroundVisible = true; + bool basicVisible = true; if (m_settings.m_map == "WWT") { @@ -977,6 +1000,13 @@ void SkyMapGUI::updateToolbar() backgroundVisible = false; reticleVisible = false; } + else if (m_settings.m_map == "Moon") + { + projectionVisible = false; + backgroundVisible = false; + reticleVisible = false; + basicVisible = false; + } ui->background->setVisible(backgroundVisible); ui->projection->setVisible(projectionVisible); @@ -985,6 +1015,14 @@ void SkyMapGUI::updateToolbar() ui->displayConstellations->setVisible(constellationsVisible); ui->displayReticle->setVisible(reticleVisible); + ui->find->setVisible(basicVisible); + ui->findLabel->setVisible(basicVisible); + ui->displayGrid->setVisible(basicVisible); + ui->displayReticle->setVisible(basicVisible); + ui->displayAntennaFoV->setVisible(basicVisible); + ui->track->setVisible(basicVisible); + ui->source->setVisible(basicVisible); + updateProjection(); } diff --git a/plugins/feature/skymap/skymapgui.h b/plugins/feature/skymap/skymapgui.h index efaff4168..37d93db15 100644 --- a/plugins/feature/skymap/skymapgui.h +++ b/plugins/feature/skymap/skymapgui.h @@ -88,6 +88,8 @@ private: quint16 m_webPort; WTML m_wtml; WebInterface *m_webInterface; + bool m_ready; //!< Web app is ready + QString m_find; double m_ra; //!< Target from source plugin double m_dec; diff --git a/plugins/feature/skymap/skymapgui.ui b/plugins/feature/skymap/skymapgui.ui index 4845b04a0..a3da04658 100644 --- a/plugins/feature/skymap/skymapgui.ui +++ b/plugins/feature/skymap/skymapgui.ui @@ -116,6 +116,11 @@ Aladin
+ + + Moon + + @@ -271,7 +276,7 @@ - F0:1 StarTracker + diff --git a/plugins/feature/skymap/skymapsettings.cpp b/plugins/feature/skymap/skymapsettings.cpp index 56bc21b65..79154d6d2 100644 --- a/plugins/feature/skymap/skymapsettings.cpp +++ b/plugins/feature/skymap/skymapsettings.cpp @@ -28,13 +28,6 @@ #include "skymapsettings.h" -const QStringList SkyMapSettings::m_pipeTypes = { - QStringLiteral("StarTracker"), - QStringLiteral("SatelliteTracker"), - QStringLiteral("GS232Controller"), - QStringLiteral("Map") -}; - const QStringList SkyMapSettings::m_pipeURIs = { QStringLiteral("sdrangel.feature.startracker"), QStringLiteral("sdrangel.feature.satellitetracker"), diff --git a/plugins/feature/skymap/skymapsettings.h b/plugins/feature/skymap/skymapsettings.h index 5731fde16..67abd9a27 100644 --- a/plugins/feature/skymap/skymapsettings.h +++ b/plugins/feature/skymap/skymapsettings.h @@ -66,7 +66,6 @@ struct SkyMapSettings void applySettings(const QStringList& settingsKeys, const SkyMapSettings& settings); QString getDebugString(const QStringList& settingsKeys, bool force=false) const; - static const QStringList m_pipeTypes; static const QStringList m_pipeURIs; }; diff --git a/plugins/samplesource/kiwisdr/kiwisdrinput.cpp b/plugins/samplesource/kiwisdr/kiwisdrinput.cpp index 50c6db2af..d4c31653a 100644 --- a/plugins/samplesource/kiwisdr/kiwisdrinput.cpp +++ b/plugins/samplesource/kiwisdr/kiwisdrinput.cpp @@ -49,7 +49,10 @@ KiwiSDRInput::KiwiSDRInput(DeviceAPI *deviceAPI) : m_kiwiSDRWorkerThread(nullptr), m_deviceDescription("KiwiSDR"), m_running(false), - m_masterTimer(deviceAPI->getMasterTimer()) + m_masterTimer(deviceAPI->getMasterTimer()), + m_latitude(std::numeric_limits::quiet_NaN()), + m_longitude(std::numeric_limits::quiet_NaN()), + m_altitude(std::numeric_limits::quiet_NaN()) { m_sampleFifo.setLabel(m_deviceDescription); m_deviceAPI->setNbSourceStreams(1); @@ -237,6 +240,16 @@ bool KiwiSDRInput::handleMessage(const Message& message) return true; } + else if (KiwiSDRWorker::MsgReportPosition::match(message)) + { + KiwiSDRWorker::MsgReportPosition& report = (KiwiSDRWorker::MsgReportPosition&) message; + + m_latitude = report.getLatitude(); + m_longitude = report.getLongitude(); + m_altitude = report.getAltitude(); + + return true; + } else if (MsgStartStop::match(message)) { MsgStartStop& cmd = (MsgStartStop&) message; @@ -455,6 +468,9 @@ void KiwiSDRInput::webapiFormatDeviceSettings(SWGSDRangel::SWGDeviceSettings& re void KiwiSDRInput::webapiFormatDeviceReport(SWGSDRangel::SWGDeviceReport& response) { response.getKiwiSdrReport()->setStatus(getStatus()); + response.getKiwiSdrReport()->setLatitude(m_latitude); + response.getKiwiSdrReport()->setLongitude(m_longitude); + response.getKiwiSdrReport()->setAltitude(m_altitude); } void KiwiSDRInput::webapiReverseSendSettings(const QList& deviceSettingsKeys, const KiwiSDRSettings& settings, bool force) diff --git a/plugins/samplesource/kiwisdr/kiwisdrinput.h b/plugins/samplesource/kiwisdr/kiwisdrinput.h index ecefec8c2..d8a2bfe8c 100644 --- a/plugins/samplesource/kiwisdr/kiwisdrinput.h +++ b/plugins/samplesource/kiwisdr/kiwisdrinput.h @@ -167,6 +167,9 @@ private: const QTimer& m_masterTimer; QNetworkAccessManager *m_networkManager; QNetworkRequest m_networkRequest; + float m_latitude; + float m_longitude; + float m_altitude; int getStatus() const; bool applySettings(const KiwiSDRSettings& settings, const QList& settingsKeys, bool force); diff --git a/plugins/samplesource/kiwisdr/kiwisdrworker.cpp b/plugins/samplesource/kiwisdr/kiwisdrworker.cpp index 1b8f43295..09220cdf5 100644 --- a/plugins/samplesource/kiwisdr/kiwisdrworker.cpp +++ b/plugins/samplesource/kiwisdr/kiwisdrworker.cpp @@ -21,6 +21,7 @@ #include "kiwisdrworker.h" MESSAGE_CLASS_DEFINITION(KiwiSDRWorker::MsgReportSampleRate, Message) +MESSAGE_CLASS_DEFINITION(KiwiSDRWorker::MsgReportPosition, Message) KiwiSDRWorker::KiwiSDRWorker(SampleSinkFifo* sampleFifo) : QObject(), @@ -124,6 +125,41 @@ void KiwiSDRWorker::onBinaryMessageReceived(const QByteArray &message) emit updateStatus(2); } } + else if ((al.size() >= 2) && al[1].startsWith("load_cfg=")) + { + QByteArray urlEncoded = al[1].mid(9).toLatin1(); + QString json = QUrl::fromPercentEncoding(urlEncoded); + QJsonDocument doc = QJsonDocument::fromJson(json.toUtf8()); + + if (doc.isObject()) + { + QJsonObject obj = doc.object(); + + if (obj.contains("rx_gps")) + { + QString gps = obj.value("rx_gps").toString(); + QRegularExpression re("\\((-?[0-9]+(\\.[0-9]+)?), *(-?[0-9]+(\\.[0-9]+)?)\\)"); + QRegularExpressionMatch match = re.match(gps); + + if (match.hasMatch()) + { + float latitude = match.captured(1).toFloat(); + float longitude = match.captured(3).toFloat(); + float altitude = 0.0f; + if (obj.contains("rx_asl")) { + altitude = (float) obj.value("rx_asl").toInt(); + } + if (m_inputMessageQueue) { + m_inputMessageQueue->push(MsgReportPosition::create(latitude, longitude, altitude)); + } + } + } + } + else + { + qDebug() << "KiwiSDRWorker::onBinaryMessageReceived - Document is not an object"; + } + } } else if (message[0] == 'S' && message[1] == 'N' && message[2] == 'D') { diff --git a/plugins/samplesource/kiwisdr/kiwisdrworker.h b/plugins/samplesource/kiwisdr/kiwisdrworker.h index f018c4d1e..0f2ef2df6 100644 --- a/plugins/samplesource/kiwisdr/kiwisdrworker.h +++ b/plugins/samplesource/kiwisdr/kiwisdrworker.h @@ -53,6 +53,31 @@ public: { } }; + class MsgReportPosition : public Message { + MESSAGE_CLASS_DECLARATION + + public: + float getLatitude() const { return m_latitude; } + float getLongitude() const { return m_longitude; } + float getAltitude() const { return m_altitude; } + + static MsgReportPosition* create(float latitude, float longitude, float altitude) { + return new MsgReportPosition(latitude, longitude, altitude); + } + + private: + float m_latitude; + float m_longitude; + float m_altitude; + + MsgReportPosition(float latitude, float longitude, float altitude) : + Message(), + m_latitude(latitude), + m_longitude(longitude), + m_altitude(altitude) + { } + }; + KiwiSDRWorker(SampleSinkFifo* sampleFifo); int getStatus() const { return m_status; } void setInputMessageQueue(MessageQueue *messageQueue) { m_inputMessageQueue = messageQueue; } diff --git a/plugins/samplesource/sdrplayv3/sdrplayv3settings.cpp b/plugins/samplesource/sdrplayv3/sdrplayv3settings.cpp index c14f4667f..32a7a2518 100644 --- a/plugins/samplesource/sdrplayv3/sdrplayv3settings.cpp +++ b/plugins/samplesource/sdrplayv3/sdrplayv3settings.cpp @@ -345,7 +345,7 @@ QString SDRPlayV3Settings::getDebugString(const QStringList& settingsKeys, bool if (settingsKeys.contains("reverseAPIPort") || force) { ostr << " m_reverseAPIPort: " << m_reverseAPIPort; } - if (settingsKeys.contains("everseAPIDeviceIndex") || force) { + if (settingsKeys.contains("reverseAPIDeviceIndex") || force) { ostr << " m_reverseAPIDeviceIndex: " << m_reverseAPIDeviceIndex; } diff --git a/sdrbase/CMakeLists.txt b/sdrbase/CMakeLists.txt index 391057dc9..8de90e62f 100644 --- a/sdrbase/CMakeLists.txt +++ b/sdrbase/CMakeLists.txt @@ -238,7 +238,9 @@ set(sdrbase_SOURCES util/flightinformation.cpp util/ft8message.cpp util/giro.cpp + util/goesxray.cpp util/golay2312.cpp + util/grb.cpp util/httpdownloadmanager.cpp util/interpolation.cpp util/kiwisdrlist.cpp @@ -266,13 +268,16 @@ set(sdrbase_SOURCES util/samplesourceserializer.cpp util/simpleserializer.cpp util/serialutil.cpp + util/solardynamicsobservatory.cpp #util/spinlock.cpp util/spyserverlist.cpp + util/stix.cpp util/rtty.cpp util/uid.cpp util/units.cpp util/timeutil.cpp util/visa.cpp + util/vlftransmitters.cpp util/waypoints.cpp util/weather.cpp util/iot/device.cpp @@ -487,7 +492,9 @@ set(sdrbase_HEADERS util/flightinformation.h util/ft8message.h util/giro.h + util/goesxray.h util/golay2312.h + util/grb.h util/httpdownloadmanager.h util/incrementalarray.h util/incrementalvector.h @@ -520,12 +527,15 @@ set(sdrbase_HEADERS util/samplesourceserializer.h util/simpleserializer.h util/serialutil.h + util/solardynamicsobservatory.h #util/spinlock.h util/spyserverlist.h + util/stix.h util/uid.h util/units.h util/timeutil.h util/visa.h + util/vlftransmitters.h util/waypoints.h util/weather.h util/iot/device.h diff --git a/sdrbase/availablechannelorfeaturehandler.cpp b/sdrbase/availablechannelorfeaturehandler.cpp index eb08af7ff..af0d82ad7 100644 --- a/sdrbase/availablechannelorfeaturehandler.cpp +++ b/sdrbase/availablechannelorfeaturehandler.cpp @@ -81,11 +81,28 @@ void AvailableChannelOrFeatureHandler::scanAvailableChannelsAndFeatures() } } + // Create lists of which channels and features have been added or removed + QStringList added; + QStringList removed; + + for (const auto& channelOrFeature : availableChannelOrFeatureList) + { + if (m_availableChannelOrFeatureList.indexOfObject(channelOrFeature.m_object) < 0) { + added.append(channelOrFeature.getId()); + } + } + for (const auto& channelOrFeature : m_availableChannelOrFeatureList) + { + if (availableChannelOrFeatureList.indexOfObject(channelOrFeature.m_object) < 0) { + removed.append(channelOrFeature.getId()); + } + } + m_availableChannelOrFeatureList = availableChannelOrFeatureList; // Signal if list has changed if (changes) { - emit channelsOrFeaturesChanged(renameFrom, renameTo); + emit channelsOrFeaturesChanged(renameFrom, renameTo, removed, added); } } diff --git a/sdrbase/availablechannelorfeaturehandler.h b/sdrbase/availablechannelorfeaturehandler.h index 6c94c7c50..5568799d5 100644 --- a/sdrbase/availablechannelorfeaturehandler.h +++ b/sdrbase/availablechannelorfeaturehandler.h @@ -78,7 +78,7 @@ private slots: void handleFeatureRemoved(int featureSetIndex, Feature *feature); signals: - void channelsOrFeaturesChanged(const QStringList& renameFrom, const QStringList& renameTo); //!< Emitted when list of channels or features has changed + void channelsOrFeaturesChanged(const QStringList& renameFrom, const QStringList& renameTo, const QStringList& removed, const QStringList& added); //!< Emitted when list of channels or features has changed void messageEnqueued(MessageQueue *messageQueue); //!< Emitted when message enqueued to a pipe }; diff --git a/sdrbase/channel/channelwebapiutils.cpp b/sdrbase/channel/channelwebapiutils.cpp index fd8be370a..42292d54c 100644 --- a/sdrbase/channel/channelwebapiutils.cpp +++ b/sdrbase/channel/channelwebapiutils.cpp @@ -1156,6 +1156,119 @@ bool ChannelWebAPIUtils::getDeviceReportList(unsigned int deviceIndex, const QSt return false; } + +bool ChannelWebAPIUtils::getDevicePosition(unsigned int deviceIndex, QGeoCoordinate& position) +{ + SWGSDRangel::SWGDeviceReport deviceReport; + + if (getDeviceReport(deviceIndex, deviceReport)) + { + QJsonObject *jsonObj = deviceReport.asJsonObject(); + double latitude, longitude, altitude; + + if (WebAPIUtils::getSubObjectDouble(*jsonObj, "latitude", latitude) + && WebAPIUtils::getSubObjectDouble(*jsonObj, "longitude", longitude) + && WebAPIUtils::getSubObjectDouble(*jsonObj, "altitude", altitude)) + { + position.setLatitude(latitude); + position.setLongitude(longitude); + position.setAltitude(altitude); + // Done + return true; + } + else + { + //qWarning("ChannelWebAPIUtils::getDevicePosition: no latitude/longitude/altitude in device report"); + return false; + } + } + return false; +} + +bool ChannelWebAPIUtils::runFeature(unsigned int featureSetIndex, unsigned int featureIndex) +{ + SWGSDRangel::SWGDeviceState runResponse; + QString errorResponse; + int httpRC; + FeatureSet *featureSet; + Feature *feature; + + std::vector featureSets = MainCore::instance()->getFeatureeSets(); + if (featureSetIndex < featureSets.size()) + { + runResponse.setState(new QString()); + featureSet = featureSets[featureSetIndex]; + + if (featureIndex < (unsigned int)featureSet->getNumberOfFeatures()) + { + feature = featureSet->getFeatureAt(featureIndex); + + httpRC = feature->webapiRun(1, runResponse, errorResponse); + } + else + { + qDebug() << "ChannelWebAPIUtils::runFeature - no feature " << featureSetIndex << ':' << featureIndex; + return false; + } + } + else + { + qDebug() << "ChannelWebAPIUtils::runFeature - no feature set " << featureSetIndex; + return false; + } + + if (httpRC/100 != 2) + { + qWarning("ChannelWebAPIUtils::runFeature: run error %d: %s", + httpRC, qPrintable(errorResponse)); + return false; + } + + return true; +} + +bool ChannelWebAPIUtils::stopFeature(unsigned int featureSetIndex, unsigned int featureIndex) +{ + SWGSDRangel::SWGDeviceState runResponse; + QString errorResponse; + int httpRC; + FeatureSet *featureSet; + Feature *feature; + + std::vector featureSets = MainCore::instance()->getFeatureeSets(); + if (featureSetIndex < featureSets.size()) + { + runResponse.setState(new QString()); + featureSet = featureSets[featureSetIndex]; + + if (featureIndex < (unsigned int)featureSet->getNumberOfFeatures()) + { + feature = featureSet->getFeatureAt(featureIndex); + + httpRC = feature->webapiRun(0, runResponse, errorResponse); + } + else + { + qDebug() << "ChannelWebAPIUtils::stopFeature - no feature " << featureSetIndex << ':' << featureIndex; + return false; + } + } + else + { + qDebug() << "ChannelWebAPIUtils::stopFeature - no feature set " << featureSetIndex; + return false; + } + + if (httpRC/100 != 2) + { + qWarning("ChannelWebAPIUtils::stopFeature: run error %d: %s", + httpRC, qPrintable(errorResponse)); + return false; + } + + return true; +} + bool ChannelWebAPIUtils::patchDeviceSetting(unsigned int deviceIndex, const QString &setting, int value) { SWGSDRangel::SWGDeviceSettings deviceSettingsResponse; @@ -1299,6 +1412,80 @@ bool ChannelWebAPIUtils::patchFeatureSetting(unsigned int featureSetIndex, unsig } } +bool ChannelWebAPIUtils::patchFeatureSetting(unsigned int featureSetIndex, unsigned int featureIndex, const QString &setting, const QJsonArray& value) +{ + SWGSDRangel::SWGFeatureSettings featureSettingsResponse; + QString errorResponse; + int httpRC; + Feature *feature; + + if (getFeatureSettings(featureSetIndex, featureIndex, featureSettingsResponse, feature)) + { + // Patch settings + QJsonObject *jsonObj = featureSettingsResponse.asJsonObject(); + + // Set value + bool found = false; + for (QJsonObject::iterator it = jsonObj->begin(); it != jsonObj->end(); it++) + { + QJsonValue jsonValue = it.value(); + + if (jsonValue.isObject()) + { + QJsonObject subObject = jsonValue.toObject(); + + if (subObject.contains(setting)) + { + subObject[setting] = value; + it.value() = subObject; + found = true; + break; + } + } + } + if (!found) + { + for (QJsonObject::iterator it = jsonObj->begin(); it != jsonObj->end(); it++) + { + QJsonValueRef jsonValue = it.value(); + + if (jsonValue.isObject()) + { + QJsonObject subObject = jsonValue.toObject(); + + subObject.insert(setting, value); + jsonValue = subObject; + } + } + } + + QStringList featureSettingsKeys; + featureSettingsKeys.append(setting); + featureSettingsResponse.init(); + featureSettingsResponse.fromJsonObject(*jsonObj); + SWGSDRangel::SWGErrorResponse errorResponse2; + + httpRC = feature->webapiSettingsPutPatch(false, featureSettingsKeys, featureSettingsResponse, *errorResponse2.getMessage()); + + if (httpRC/100 == 2) + { + qDebug("ChannelWebAPIUtils::patchFeatureSetting: set feature setting %s OK", qPrintable(setting)); + return true; + } + else + { + qWarning("ChannelWebAPIUtils::patchFeatureSetting: set feature setting %s error %d: %s", + qPrintable(setting), httpRC, qPrintable(*errorResponse2.getMessage())); + return false; + } + } + else + { + return false; + } +} + + bool ChannelWebAPIUtils::patchChannelSetting(unsigned int deviceSetIndex, unsigned int channelIndex, const QString &setting, double value) { SWGSDRangel::SWGChannelSettings channelSettingsResponse; diff --git a/sdrbase/channel/channelwebapiutils.h b/sdrbase/channel/channelwebapiutils.h index 1552c2c44..8a9d25c4c 100644 --- a/sdrbase/channel/channelwebapiutils.h +++ b/sdrbase/channel/channelwebapiutils.h @@ -22,6 +22,8 @@ #define SDRBASE_CHANNEL_CHANNELWEBAPIUTILS_H_ #include +#include +#include #include "SWGDeviceSettings.h" #include "SWGDeviceReport.h" @@ -70,9 +72,13 @@ public: static bool getDeviceSetting(unsigned int deviceIndex, const QString &setting, int &value); static bool getDeviceReportValue(unsigned int deviceIndex, const QString &key, QString &value); static bool getDeviceReportList(unsigned int deviceIndex, const QString &key, const QString &subKey, QList &values); + static bool getDevicePosition(unsigned int deviceIndex, QGeoCoordinate& position); static bool patchDeviceSetting(unsigned int deviceIndex, const QString &setting, int value); + static bool runFeature(unsigned int featureSetIndex, unsigned int featureIndex); + static bool stopFeature(unsigned int featureSetIndex, unsigned int featureIndex); static bool patchFeatureSetting(unsigned int featureSetIndex, unsigned int featureIndex, const QString &setting, const QString &value); static bool patchFeatureSetting(unsigned int featureSetIndex, unsigned int featureIndex, const QString &setting, double value); + static bool patchFeatureSetting(unsigned int featureSetIndex, unsigned int featureIndex, const QString &setting, const QJsonArray& value); static bool patchChannelSetting(unsigned int deviceSetIndex, unsigned int channeIndex, const QString &setting, double value); static bool patchChannelSetting(unsigned int deviceSetIndex, unsigned int channeIndex, const QString &setting, const QJsonArray& value); static bool getFeatureSetting(unsigned int featureSetIndex, unsigned int featureIndex, const QString &setting, int &value); diff --git a/sdrbase/feature/featurewebapiutils.cpp b/sdrbase/feature/featurewebapiutils.cpp index 43381ba55..45832bb96 100644 --- a/sdrbase/feature/featurewebapiutils.cpp +++ b/sdrbase/feature/featurewebapiutils.cpp @@ -86,6 +86,36 @@ bool FeatureWebAPIUtils::mapSetDateTime(const QDateTime& dateTime, int featureSe } } +// Find the specified target on the sky map +bool FeatureWebAPIUtils::skyMapFind(const QString& target, int featureSetIndex, int featureIndex) +{ + Feature *feature = FeatureWebAPIUtils::getFeature(featureSetIndex, featureIndex, "sdrangel.feature.skymap"); + if (feature != nullptr) + { + QString errorMessage; + QStringList featureActionKeys = {"find"}; + SWGSDRangel::SWGFeatureActions query; + SWGSDRangel::SWGSkyMapActions *skyMapActions = new SWGSDRangel::SWGSkyMapActions(); + + skyMapActions->setFind(new QString(target)); + query.setSkyMapActions(skyMapActions); + + int httpRC = feature->webapiActionsPost(featureActionKeys, query, errorMessage); + if (httpRC/100 != 2) + { + qWarning() << "FeatureWebAPIUtils::skyMapFind: error " << httpRC << ":" << errorMessage; + return false; + } + + return true; + } + else + { + qWarning("FeatureWebAPIUtils::skyMapFind: no Sky Map feature"); + return false; + } +} + // Get first feature with the given URI Feature* FeatureWebAPIUtils::getFeature(int& featureSetIndex, int& featureIndex, const QString& uri) { @@ -180,3 +210,56 @@ bool FeatureWebAPIUtils::satelliteLOS(const QString name) // Not currently required by any features return true; } + +// Open a Sky Map feature and find the specified target +bool FeatureWebAPIUtils::openSkyMapAndFind(const QString& target) +{ + return SkyMapOpener::open(target); +} + +bool SkyMapOpener::open(const QString& target) +{ + // Create a SkyMap feature + MainCore *mainCore = MainCore::instance(); + PluginAPI::FeatureRegistrations *featureRegistrations = mainCore->getPluginManager()->getFeatureRegistrations(); + int nbRegistrations = featureRegistrations->size(); + int index = 0; + + for (; index < nbRegistrations; index++) + { + if (featureRegistrations->at(index).m_featureId == "SkyMap") { + break; + } + } + + if (index < nbRegistrations) + { + new SkyMapOpener(target); + + MainCore::MsgAddFeature *msg = MainCore::MsgAddFeature::create(0, index); + mainCore->getMainMessageQueue()->push(msg); + + return true; + } + else + { + qWarning() << "Sky Map feature not available"; + return false; + } +} + +SkyMapOpener::SkyMapOpener(const QString& target) : + m_target(target) +{ + connect(MainCore::instance(), &MainCore::featureAdded, this, &SkyMapOpener::onSkyMapAdded); +} + +void SkyMapOpener::onSkyMapAdded(int featureSetIndex, Feature *feature) +{ + if (feature->getURI() == "sdrangel.feature.skymap") + { + disconnect(MainCore::instance(), &MainCore::featureAdded, this, &SkyMapOpener::onSkyMapAdded); + FeatureWebAPIUtils::skyMapFind(m_target, featureSetIndex, feature->getIndexInFeatureSet()); + deleteLater(); + } +} diff --git a/sdrbase/feature/featurewebapiutils.h b/sdrbase/feature/featurewebapiutils.h index 93296fa59..2f8d6b32a 100644 --- a/sdrbase/feature/featurewebapiutils.h +++ b/sdrbase/feature/featurewebapiutils.h @@ -27,11 +27,29 @@ class Feature; +class SkyMapOpener : public QObject { + Q_OBJECT + + QString m_target; + +private: + SkyMapOpener(const QString& target); + +public: + static bool open(const QString& target); + +private slots: + void onSkyMapAdded(int featureSetIndex, Feature *feature); + +}; + class SDRBASE_API FeatureWebAPIUtils { public: static bool mapFind(const QString& target, int featureSetIndex=-1, int featureIndex=-1); static bool mapSetDateTime(const QDateTime& dateTime, int featureSetIndex=-1, int featureIndex=-1); + static bool skyMapFind(const QString& target, int featureSetIndex=-1, int featureIndex=-1); + static bool openSkyMapAndFind(const QString& target); static Feature *getFeature(int& featureSetIndex, int& featureIndex, const QString& uri); static bool satelliteAOS(const QString name, const QDateTime aos, const QDateTime los); static bool satelliteLOS(const QString name); diff --git a/sdrbase/maincore.cpp b/sdrbase/maincore.cpp index 18e1c3a8b..6eca8f287 100644 --- a/sdrbase/maincore.cpp +++ b/sdrbase/maincore.cpp @@ -612,12 +612,16 @@ bool MainCore::getDeviceAndChannelIndexFromId(const QString& channelId, unsigned bool MainCore::getFeatureIndexFromId(const QString& featureId, unsigned int &featureSetIndex, unsigned int &featureIndex) { - const QRegularExpression re("[F]([0-9]+):([0-9]+)"); + const QRegularExpression re("F([0-9]+)?:([0-9]+)"); QRegularExpressionMatch match = re.match(featureId); if (match.hasMatch()) { - featureSetIndex = match.capturedTexts()[1].toInt(); + if (match.capturedTexts()[1].isEmpty()) { + featureSetIndex = 0; + } else { + featureSetIndex = match.capturedTexts()[1].toInt(); + } featureIndex = match.capturedTexts()[2].toInt(); return true; } diff --git a/sdrbase/resources/webapi/doc/html2/index.html b/sdrbase/resources/webapi/doc/html2/index.html index db44689f9..038c51178 100644 --- a/sdrbase/resources/webapi/doc/html2/index.html +++ b/sdrbase/resources/webapi/doc/html2/index.html @@ -6684,6 +6684,9 @@ margin-bottom: 20px; "SatelliteTrackerSettings" : { "$ref" : "#/definitions/SatelliteTrackerSettings" }, + "SIDSettings" : { + "$ref" : "#/definitions/SIDSettings" + }, "SimplePTTSettings" : { "$ref" : "#/definitions/SimplePTTSettings" }, @@ -8555,6 +8558,18 @@ margin-bottom: 20px; "status" : { "type" : "integer", "description" : "0 for Idle, 1 for Connecting, 2 for Connected, 3 for Error, 4 for Disconnected" + }, + "latitude" : { + "type" : "number", + "format" : "float" + }, + "longitude" : { + "type" : "number", + "format" : "float" + }, + "altitude" : { + "type" : "number", + "format" : "float" } }, "description" : "KiwiSDR" @@ -10050,6 +10065,10 @@ margin-bottom: 20px; "format" : "float", "description" : "Extruded height (from surface) for polygons" }, + "availableFrom" : { + "type" : "string", + "description" : "Date and time at which the item should first appear on 3D map" + }, "availableUntil" : { "type" : "string", "description" : "Date and time until after which this item should no longer appear on 3D map" @@ -10210,6 +10229,10 @@ margin-bottom: 20px; "format" : "float", "description" : "Extruded height (from surface) for polygons" }, + "availableFrom" : { + "type" : "string", + "description" : "Date and time at which the item should first appear on 3D map" + }, "availableUntil" : { "type" : "string", "description" : "Date and time until after which this item should no longer appear on 3D map" @@ -13761,6 +13784,52 @@ margin-bottom: 20px; } }, "description" : "SDRplayV3" +}; + defs.SIDSettings = { + "properties" : { + "period" : { + "type" : "number", + "format" : "float" + }, + "autosave" : { + "type" : "integer" + }, + "autoload" : { + "type" : "integer" + }, + "filename" : { + "type" : "integer" + }, + "autosavePeriod" : { + "type" : "integer" + }, + "title" : { + "type" : "string" + }, + "rgbColor" : { + "type" : "integer" + }, + "useReverseAPI" : { + "type" : "integer", + "description" : "Synchronize with reverse API (1 for yes, 0 for no)" + }, + "reverseAPIAddress" : { + "type" : "string" + }, + "reverseAPIPort" : { + "type" : "integer" + }, + "reverseAPIFeatureSetIndex" : { + "type" : "integer" + }, + "reverseAPIFeatureIndex" : { + "type" : "integer" + }, + "rollupState" : { + "$ref" : "#/definitions/RollupState" + } + }, + "description" : "SID settings" }; defs.SSBDemodReport = { "properties" : { @@ -58865,7 +58934,7 @@ except ApiException as e:
- Generated 2024-03-13T16:45:40.518+01:00 + Generated 2024-04-04T16:23:36.765+02:00
diff --git a/sdrbase/resources/webapi/doc/swagger/include/FeatureSettings.yaml b/sdrbase/resources/webapi/doc/swagger/include/FeatureSettings.yaml index c5a5b5eea..1c56dbecf 100644 --- a/sdrbase/resources/webapi/doc/swagger/include/FeatureSettings.yaml +++ b/sdrbase/resources/webapi/doc/swagger/include/FeatureSettings.yaml @@ -41,6 +41,8 @@ FeatureSettings: $ref: "/doc/swagger/include/RigCtlServer.yaml#/RigCtlServerSettings" SatelliteTrackerSettings: $ref: "/doc/swagger/include/SatelliteTracker.yaml#/SatelliteTrackerSettings" + SIDSettings: + $ref: "/doc/swagger/include/SID.yaml#/SIDSettings" SimplePTTSettings: $ref: "/doc/swagger/include/SimplePTT.yaml#/SimplePTTSettings" SkyMapSettings: diff --git a/sdrbase/resources/webapi/doc/swagger/include/KiwiSDR.yaml b/sdrbase/resources/webapi/doc/swagger/include/KiwiSDR.yaml index d02981dbe..8042a9fc5 100644 --- a/sdrbase/resources/webapi/doc/swagger/include/KiwiSDR.yaml +++ b/sdrbase/resources/webapi/doc/swagger/include/KiwiSDR.yaml @@ -30,3 +30,12 @@ KiwiSDRReport: status: description: 0 for Idle, 1 for Connecting, 2 for Connected, 3 for Error, 4 for Disconnected type: integer + latitude: + type: number + format: float + longitude: + type: number + format: float + altitude: + type: number + format: float diff --git a/sdrbase/resources/webapi/doc/swagger/include/Map.yaml b/sdrbase/resources/webapi/doc/swagger/include/Map.yaml index 3d4b74232..1352c59e4 100644 --- a/sdrbase/resources/webapi/doc/swagger/include/Map.yaml +++ b/sdrbase/resources/webapi/doc/swagger/include/Map.yaml @@ -156,6 +156,9 @@ MapItem: description: "Extruded height (from surface) for polygons" type: number format: float + availableFrom: + description: "Date and time at which the item should first appear on 3D map" + type: string availableUntil: description: "Date and time until after which this item should no longer appear on 3D map" type: string diff --git a/sdrbase/resources/webapi/doc/swagger/include/SID.yaml b/sdrbase/resources/webapi/doc/swagger/include/SID.yaml new file mode 100644 index 000000000..c7f56bdde --- /dev/null +++ b/sdrbase/resources/webapi/doc/swagger/include/SID.yaml @@ -0,0 +1,31 @@ +SIDSettings: + description: "SID settings" + properties: + period: + type: number + format: float + autosave: + type: integer + autoload: + type: integer + filename: + type: integer + autosavePeriod: + type: integer + 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: "/doc/swagger/include/RollupState.yaml#/RollupState" diff --git a/sdrbase/util/aprs.cpp b/sdrbase/util/aprs.cpp index 44d726181..10fab703f 100644 --- a/sdrbase/util/aprs.cpp +++ b/sdrbase/util/aprs.cpp @@ -44,7 +44,7 @@ inline int charToIntAscii(QString&s, int idx) bool APRSPacket::decode(AX25Packet packet) { // Check type, PID and length of packet - if ((packet.m_type == "UI") && (packet.m_pid == "f0") && (packet.m_dataASCII.length() >= 1)) + if ((packet.m_type == "UI") && (packet.m_pid == "f0") && (packet.m_data.length() >= 1)) { // Check destination address QRegularExpression re("^(AIR.*|ALL.*|AP.*|BEACON|CQ.*|GPS.*|DF.*|DGPS.*|DRILL.*|DX.*|ID.*|JAVA.*|MAIL.*|MICE.*|QST.*|QTH.*|RTCM.*|SKY.*|SPACE.*|SPC.*|SYM.*|TEL.*|TEST.*|TLM.*|WX.*|ZIP.*)"); @@ -54,7 +54,10 @@ bool APRSPacket::decode(AX25Packet packet) m_from = packet.m_from; m_to = packet.m_to; m_via = packet.m_via; - m_data = packet.m_dataASCII; + m_data = packet.m_data; + + // UTF-8 is supported: http://aprs.org/aprs12/utf-8.txt + QString data = QString::fromUtf8(packet.m_data); if (packet.m_to.startsWith("GPS") || packet.m_to.startsWith("SPC") || packet.m_to.startsWith("SYM")) { @@ -64,20 +67,20 @@ bool APRSPacket::decode(AX25Packet packet) // Source address SSID can be used to specify a symbol // First byte of information field is data type ID - char dataType = packet.m_dataASCII[0].toLatin1(); + char dataType = data[0].toLatin1(); int idx = 1; switch (dataType) { case '!': // Position without timestamp or Ultimeter 2000 WX Station - parsePosition(packet.m_dataASCII, idx); + parsePosition(data, idx); if (m_symbolCode == '_') - parseWeather(packet.m_dataASCII, idx, false); + parseWeather(data, idx, false); else if (m_symbolCode == '@') - parseStorm(packet.m_dataASCII, idx); + parseStorm(data, idx); else { - parseDataExension(packet.m_dataASCII, idx); - parseComment(packet.m_dataASCII, idx); + parseDataExension(data, idx); + parseComment(data, idx); } break; case '#': // Peet Bros U-II Weather Station @@ -85,85 +88,85 @@ bool APRSPacket::decode(AX25Packet packet) case '%': // Agrelo DFJr / MicroFinder break; case ')': // Item - parseItem(packet.m_dataASCII, idx); - parsePosition(packet.m_dataASCII, idx); - parseDataExension(packet.m_dataASCII, idx); - parseComment(packet.m_dataASCII, idx); + parseItem(data, idx); + parsePosition(data, idx); + parseDataExension(data, idx); + parseComment(data, idx); break; case '*': // Peet Bros U-II Weather Station break; case '/': // Position with timestamp (no APRS messaging) - parseTime(packet.m_dataASCII, idx); - parsePosition(packet.m_dataASCII, idx); + parseTime(data, idx); + parsePosition(data, idx); if (m_symbolCode == '_') - parseWeather(packet.m_dataASCII, idx, false); + parseWeather(data, idx, false); else if (m_symbolCode == '@') - parseStorm(packet.m_dataASCII, idx); + parseStorm(data, idx); else { - parseDataExension(packet.m_dataASCII, idx); - parseComment(packet.m_dataASCII, idx); + parseDataExension(data, idx); + parseComment(data, idx); } break; case ':': // Message - parseMessage(packet.m_dataASCII, idx); + parseMessage(data, idx); break; case ';': // Object - parseObject(packet.m_dataASCII, idx); - parseTime(packet.m_dataASCII, idx); - parsePosition(packet.m_dataASCII, idx); + parseObject(data, idx); + parseTime(data, idx); + parsePosition(data, idx); if (m_symbolCode == '_') - parseWeather(packet.m_dataASCII, idx, false); + parseWeather(data, idx, false); else if (m_symbolCode == '@') - parseStorm(packet.m_dataASCII, idx); + parseStorm(data, idx); else { - parseDataExension(packet.m_dataASCII, idx); - parseComment(packet.m_dataASCII, idx); + parseDataExension(data, idx); + parseComment(data, idx); } break; case '<': // Station Capabilities break; case '=': // Position without timestamp (with APRS messaging) - parsePosition(packet.m_dataASCII, idx); + parsePosition(data, idx); if (m_symbolCode == '_') - parseWeather(packet.m_dataASCII, idx, false); + parseWeather(data, idx, false); else if (m_symbolCode == '@') - parseStorm(packet.m_dataASCII, idx); + parseStorm(data, idx); else { - parseDataExension(packet.m_dataASCII, idx); - parseComment(packet.m_dataASCII, idx); + parseDataExension(data, idx); + parseComment(data, idx); } break; case '>': // Status - parseStatus(packet.m_dataASCII, idx); + parseStatus(data, idx); break; case '?': // Query break; case '@': // Position with timestamp (with APRS messaging) - parseTime(packet.m_dataASCII, idx); - parsePosition(packet.m_dataASCII, idx); + parseTime(data, idx); + parsePosition(data, idx); if (m_symbolCode == '_') - parseWeather(packet.m_dataASCII, idx, false); + parseWeather(data, idx, false); else if (m_symbolCode == '@') - parseStorm(packet.m_dataASCII, idx); + parseStorm(data, idx); else { - parseDataExension(packet.m_dataASCII, idx); - parseComment(packet.m_dataASCII, idx); + parseDataExension(data, idx); + parseComment(data, idx); } break; case 'T': // Telemetry data - parseTelemetry(packet.m_dataASCII, idx); + parseTelemetry(data, idx); break; case '_': // Weather report (without position) - parseTimeMDHM(packet.m_dataASCII, idx); - parseWeather(packet.m_dataASCII, idx, true); + parseTimeMDHM(data, idx); + parseWeather(data, idx, true); break; case '`': // Mic-E Information Field Data (current) case '\'': // Mic-E Information Field Data (old) - parseMicE(packet.m_dataASCII, idx, m_to); + parseMicE(data, idx, m_to); break; case '{': // User-defined APRS packet format break; @@ -182,10 +185,9 @@ bool APRSPacket::decode(AX25Packet packet) qDebug() << "APRSPacket::decode: AX.25 Destination did not match known regexp " << m_to; } } else { - qDebug() << "APRSPacket::decode: Invalid value in type=" << packet.m_type << " pid=" << packet.m_pid << " length of " << packet.m_dataASCII; + qDebug() << "APRSPacket::decode: Not APRS: type=" << packet.m_type << " pid=" << packet.m_pid << " length=" << packet.m_data.length(); } - return false; } diff --git a/sdrbase/util/aprs.h b/sdrbase/util/aprs.h index 22fff583a..1f1cf8762 100644 --- a/sdrbase/util/aprs.h +++ b/sdrbase/util/aprs.h @@ -32,7 +32,7 @@ struct SDRBASE_API APRSPacket { QString m_from; QString m_to; QString m_via; - QString m_data; // Original ASCII data + QByteArray m_data; // Original binary data QDateTime m_dateTime; // Date/time of reception / decoding @@ -244,9 +244,36 @@ struct SDRBASE_API APRSPacket { return QString("%1,%2").arg(m_latitude).arg(m_longitude); } - QString toTNC2(QString igateCallsign) + QByteArray toTNC2(QString igateCallsign) { - return m_from + ">" + m_to + (m_via.isEmpty() ? "" : ("," + m_via)) + ",qAR," + igateCallsign + ":" + m_data + "\r\n"; + QByteArray data; + + data.append(m_from.toLatin1()); + data.append('>'); + data.append(m_to.toLatin1()); + if (!m_via.isEmpty()) + { + data.append(','); + data.append(m_via.toLatin1()); + } + data.append(",qAR,"); + data.append(igateCallsign.toLatin1()); + data.append(':'); + + // #2028 - Protect against APRS-IS server command injection, by only sending up to first CR/LF + int idx = m_data.indexOf("\r"); + if (idx == -1) { + idx = m_data.indexOf("\n"); + } + if (idx >= 0) { + data.append(m_data.left(idx)); + } else { + data.append(m_data); + } + data.append('\r'); + data.append('\n'); + + return data; } // Convert a TNC2 formatted packet (as sent by APRS-IS Igates) to an AX25 byte array diff --git a/sdrbase/util/astronomy.cpp b/sdrbase/util/astronomy.cpp index a8f46fcd0..9c86ab4cf 100644 --- a/sdrbase/util/astronomy.cpp +++ b/sdrbase/util/astronomy.cpp @@ -76,6 +76,12 @@ double Astronomy::modifiedJulianDate(QDateTime dt) return Astronomy::julianDate(dt) - 2400000.5; } +// Convert Julian date to QDateTime +QDateTime Astronomy::julianDateToDateTime(double jd) +{ + return QDateTime::fromSecsSinceEpoch((jd - 2440587.5) * 24.0*60.0*60.0); +} + // Get Julian date of J2000 Epoch double Astronomy::jd_j2000(void) { @@ -903,6 +909,49 @@ double Astronomy::observerVelocityLSRK(RADec rd, double latitude, double longitu return vRot + vOrbit + vSun; } +// Calculate sunrise and sunset time +// From: https://en.wikipedia.org/wiki/Sunrise_equation +// Probably accurate to within a couple of minutes +void Astronomy::sunrise(QDate date, double latitude, double longitude, QDateTime& rise, QDateTime& set) +{ + // Calculate Julian day + double n = std::ceil(Astronomy::julianDate(QDateTime(date, QTime(0, 0, 0))) - 2451545.0 + (69.184 / 86400.0)); + + // Mean solar time + double jStar = n - longitude / 360.0; + + // Solar mean anomaly + double m = Astronomy::modulo(357.5291 + 0.98560028 * jStar, 360.0); + double mRad = Units::degreesToRadians(m); + + // Equation of the center + double c = 1.9148 * sin(mRad) + 0.02 * sin(2.0 * mRad) + 0.0003 * sin(3 * mRad); + + // Ecliptic longitude + double lambda = Astronomy::modulo(m + c + 180.0 + 102.9372, 360.0); + double lambdaRad = Units::degreesToRadians(lambda); + + // Solar transit + double jTransit = 2451545.0 + jStar + 0.0053 * sin(mRad) - 0.0069 * sin(2.0 * lambdaRad); + + // Declination of the Sun + const double tilt = 23.4397; + const double tiltRad = Units::degreesToRadians(tilt); + double sunDecRad = asin(sin(lambdaRad) * sin(tiltRad)); + + // Hour angle + double latitudeRad = Units::degreesToRadians(latitude); + double omega0Rad = acos((sin(Units::degreesToRadians(-0.833)) - sin(latitudeRad) * sin(sunDecRad)) / (cos(latitudeRad) * cos(sunDecRad))); + double omega0 = Units::radiansToDegrees(omega0Rad); + + // Rise and set times + double jRise = jTransit - omega0 / 360.0; + double jSet = jTransit + omega0 / 360.0; + + rise = Astronomy::julianDateToDateTime(jRise); + set = Astronomy::julianDateToDateTime(jSet); +} + // Calculate thermal noise power for a given temperature in Kelvin and bandwidth in Hz double Astronomy::noisePowerdBm(double temp, double bw) { diff --git a/sdrbase/util/astronomy.h b/sdrbase/util/astronomy.h index 154f5dc1f..7d9665a90 100644 --- a/sdrbase/util/astronomy.h +++ b/sdrbase/util/astronomy.h @@ -40,6 +40,7 @@ public: static double julianDate(int year, int month, int day, int hours, int minutes, int seconds); static double julianDate(QDateTime dt); static double modifiedJulianDate(QDateTime dt); + static QDateTime julianDateToDateTime(double jd); static double jd_j2000(void); static double jd_b1950(void); @@ -80,6 +81,8 @@ public: static double sunVelocityLSRK(RADec rd); static double observerVelocityLSRK(RADec rd, double latitude, double longitude, QDateTime dt); + static void sunrise(QDate date, double latitude, double longitude, QDateTime& rise, QDateTime& set); + static double noisePowerdBm(double temp, double bw); static double noiseTemp(double dBm, double bw); diff --git a/sdrbase/util/ax25.cpp b/sdrbase/util/ax25.cpp index 0e674f781..625eae818 100644 --- a/sdrbase/util/ax25.cpp +++ b/sdrbase/util/ax25.cpp @@ -139,7 +139,7 @@ bool AX25Packet::decode(QByteArray packet) infoStart = i; infoEnd = packet.size()-2-i; QByteArray info(packet.mid(infoStart, infoEnd)); - m_dataASCII = QString::fromLatin1(info); + m_data = info; m_dataHex = QString(info.toHex()); return true; diff --git a/sdrbase/util/ax25.h b/sdrbase/util/ax25.h index 012aebafd..d2c70c0a8 100644 --- a/sdrbase/util/ax25.h +++ b/sdrbase/util/ax25.h @@ -32,7 +32,7 @@ struct SDRBASE_API AX25Packet { QString m_via; QString m_type; QString m_pid; - QString m_dataASCII; + QByteArray m_data; QString m_dataHex; bool decode(QByteArray packet); diff --git a/sdrbase/util/giro.cpp b/sdrbase/util/giro.cpp index 2e1771161..09969a5f2 100644 --- a/sdrbase/util/giro.cpp +++ b/sdrbase/util/giro.cpp @@ -21,22 +21,36 @@ #include #include #include +#include #include GIRO::GIRO() { + connect(&m_indexTimer, &QTimer::timeout, this, &GIRO::getIndex); connect(&m_dataTimer, &QTimer::timeout, this, &GIRO::getData); - connect(&m_mufTimer, &QTimer::timeout, this, &GIRO::getMUF); - connect(&m_foF2Timer, &QTimer::timeout, this, &GIRO::getfoF2); + connect(&m_mufTimer, &QTimer::timeout, this, qOverload<>(&GIRO::getMUF)); + connect(&m_foF2Timer, &QTimer::timeout, this, qOverload<>(&GIRO::getfoF2)); m_networkManager = new QNetworkAccessManager(); connect(m_networkManager, &QNetworkAccessManager::finished, this, &GIRO::handleReply); + + QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation); + QDir writeableDir(locations[0]); + if (!writeableDir.mkpath(QStringLiteral("cache") + QDir::separator() + QStringLiteral("giro"))) { + qDebug() << "Failed to create cache/giro"; + } + + m_cache = new QNetworkDiskCache(); + m_cache->setCacheDirectory(locations[0] + QDir::separator() + QStringLiteral("cache") + QDir::separator() + QStringLiteral("giro")); + m_cache->setMaximumCacheSize(100000000); + m_networkManager->setCache(m_cache); } GIRO::~GIRO() { + disconnect(&m_indexTimer, &QTimer::timeout, this, &GIRO::getIndex); disconnect(&m_dataTimer, &QTimer::timeout, this, &GIRO::getData); - disconnect(&m_mufTimer, &QTimer::timeout, this, &GIRO::getMUF); - disconnect(&m_foF2Timer, &QTimer::timeout, this, &GIRO::getfoF2); + disconnect(&m_mufTimer, &QTimer::timeout, this, qOverload<>(&GIRO::getMUF)); + disconnect(&m_foF2Timer, &QTimer::timeout, this, qOverload<>(&GIRO::getfoF2)); disconnect(m_networkManager, &QNetworkAccessManager::finished, this, &GIRO::handleReply); delete m_networkManager; } @@ -54,6 +68,20 @@ GIRO* GIRO::create(const QString& service) } } +void GIRO::getIndexPeriodically(int periodInMins) +{ + if (periodInMins > 0) + { + m_indexTimer.setInterval(periodInMins*60*1000); + m_indexTimer.start(); + getIndex(); + } + else + { + m_indexTimer.stop(); + } +} + void GIRO::getDataPeriodically(int periodInMins) { if (periodInMins > 0) @@ -96,21 +124,37 @@ void GIRO::getfoF2Periodically(int periodInMins) } } +void GIRO::getIndex() +{ + QUrl url(QString("https://prop.kc2g.com/api/available_nowcasts.json?days=5")); + m_networkManager->get(QNetworkRequest(url)); +} + void GIRO::getData() { QUrl url(QString("https://prop.kc2g.com/api/stations.json")); m_networkManager->get(QNetworkRequest(url)); } +void GIRO::getfoF2() +{ + getMUF("current"); +} + void GIRO::getMUF() { - QUrl url(QString("https://prop.kc2g.com/renders/current/mufd-normal-now.geojson")); + getMUF("current"); +} + +void GIRO::getfoF2(const QString& runId) +{ + QUrl url(QString("https://prop.kc2g.com/renders/%1/fof2-normal-now.geojson").arg(runId)); m_networkManager->get(QNetworkRequest(url)); } -void GIRO::getfoF2() +void GIRO::getMUF(const QString& runId) { - QUrl url(QString("https://prop.kc2g.com/renders/current/fof2-normal-now.geojson")); + QUrl url(QString("https://prop.kc2g.com/renders/%1/mufd-normal-now.geojson").arg(runId)); m_networkManager->get(QNetworkRequest(url)); } @@ -132,86 +176,26 @@ void GIRO::handleReply(QNetworkReply* reply) { QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); - if (reply->url().fileName() == "stations.json") + QString fileName = reply->url().fileName(); + if (fileName == "available_nowcasts.json") { - if (document.isArray()) - { - QJsonArray array = document.array(); - for (auto valRef : array) - { - if (valRef.isObject()) - { - QJsonObject obj = valRef.toObject(); - - GIROStationData data; - - if (obj.contains(QStringLiteral("station"))) - { - QJsonObject stationObj = obj.value(QStringLiteral("station")).toObject(); - - if (stationObj.contains(QStringLiteral("name"))) { - data.m_station = stationObj.value(QStringLiteral("name")).toString(); - } - if (stationObj.contains(QStringLiteral("latitude"))) { - data.m_latitude = (float)stationObj.value(QStringLiteral("latitude")).toString().toFloat(); - } - if (stationObj.contains(QStringLiteral("longitude"))) { - data.m_longitude = (float)stationObj.value(QStringLiteral("longitude")).toString().toFloat(); - if (data.m_longitude >= 180.0f) { - data.m_longitude -= 360.0f; - } - } - } - - if (containsNonNull(obj, QStringLiteral("time"))) { - data.m_dateTime = QDateTime::fromString(obj.value(QStringLiteral("time")).toString(), Qt::ISODateWithMs); - } - if (containsNonNull(obj, QStringLiteral("mufd"))) { - data.m_mufd = (float)obj.value(QStringLiteral("mufd")).toDouble(); - } - if (containsNonNull(obj, QStringLiteral("md"))) { - data.m_md = obj.value(QStringLiteral("md")).toString().toFloat(); - } - if (containsNonNull(obj, QStringLiteral("tec"))) { - data.m_tec = (float)obj.value(QStringLiteral("tec")).toDouble(); - } - if (containsNonNull(obj, QStringLiteral("fof2"))) { - data.m_foF2 = (float)obj.value(QStringLiteral("fof2")).toDouble(); - } - if (containsNonNull(obj, QStringLiteral("hmf2"))) { - data.m_hmF2 = (float)obj.value(QStringLiteral("hmf2")).toDouble(); - } - if (containsNonNull(obj, QStringLiteral("foe"))) { - data.m_foE = (float)obj.value(QStringLiteral("foe")).toDouble(); - } - if (containsNonNull(obj, QStringLiteral("cs"))) { - data.m_confidence = (int)obj.value(QStringLiteral("cs")).toDouble(); - } - - emit dataUpdated(data); - } - else - { - qDebug() << "GIRO::handleReply: Array element is not an object: " << valRef; - } - } - } - else - { - qDebug() << "GIRO::handleReply: Document is not an array: " << document; - } + handleIndex(document); } - else if (reply->url().fileName() == "mufd-normal-now.geojson") + else if (fileName == "stations.json") + { + handleStations(document); + } + else if (fileName == "mufd-normal-now.geojson") { emit mufUpdated(document); } - else if (reply->url().fileName() == "fof2-normal-now.geojson") + else if (fileName == "fof2-normal-now.geojson") { emit foF2Updated(document); } else { - qDebug() << "GIRO::handleReply: unexpected filename: " << reply->url().fileName(); + qDebug() << "GIRO::handleReply: unexpected filename: " << fileName; } } else @@ -225,3 +209,116 @@ void GIRO::handleReply(QNetworkReply* reply) qDebug() << "GIRO::handleReply: reply is null"; } } + +void GIRO::handleStations(QJsonDocument& document) +{ + if (document.isArray()) + { + QJsonArray array = document.array(); + for (auto valRef : array) + { + if (valRef.isObject()) + { + QJsonObject obj = valRef.toObject(); + + GIROStationData data; + + if (obj.contains(QStringLiteral("station"))) + { + QJsonObject stationObj = obj.value(QStringLiteral("station")).toObject(); + + if (stationObj.contains(QStringLiteral("name"))) { + data.m_station = stationObj.value(QStringLiteral("name")).toString(); + } + if (stationObj.contains(QStringLiteral("latitude"))) { + data.m_latitude = (float)stationObj.value(QStringLiteral("latitude")).toString().toFloat(); + } + if (stationObj.contains(QStringLiteral("longitude"))) { + data.m_longitude = (float)stationObj.value(QStringLiteral("longitude")).toString().toFloat(); + if (data.m_longitude >= 180.0f) { + data.m_longitude -= 360.0f; + } + } + } + + if (containsNonNull(obj, QStringLiteral("time"))) { + data.m_dateTime = QDateTime::fromString(obj.value(QStringLiteral("time")).toString(), Qt::ISODateWithMs); + } + if (containsNonNull(obj, QStringLiteral("mufd"))) { + data.m_mufd = (float)obj.value(QStringLiteral("mufd")).toDouble(); + } + if (containsNonNull(obj, QStringLiteral("md"))) { + data.m_md = obj.value(QStringLiteral("md")).toString().toFloat(); + } + if (containsNonNull(obj, QStringLiteral("tec"))) { + data.m_tec = (float)obj.value(QStringLiteral("tec")).toDouble(); + } + if (containsNonNull(obj, QStringLiteral("fof2"))) { + data.m_foF2 = (float)obj.value(QStringLiteral("fof2")).toDouble(); + } + if (containsNonNull(obj, QStringLiteral("hmf2"))) { + data.m_hmF2 = (float)obj.value(QStringLiteral("hmf2")).toDouble(); + } + if (containsNonNull(obj, QStringLiteral("foe"))) { + data.m_foE = (float)obj.value(QStringLiteral("foe")).toDouble(); + } + if (containsNonNull(obj, QStringLiteral("cs"))) { + data.m_confidence = (int)obj.value(QStringLiteral("cs")).toDouble(); + } + + emit dataUpdated(data); + } + else + { + qDebug() << "GIRO::handleReply: Array element is not an object: " << valRef; + } + } + } + else + { + qDebug() << "GIRO::handleReply: Document is not an array: " << document; + } +} + +void GIRO::handleIndex(QJsonDocument& document) +{ + if (document.isArray()) + { + QJsonArray array = document.array(); + + m_index.clear(); + + for (auto valRef : array) + { + if (valRef.isObject()) + { + QJsonObject obj = valRef.toObject(); + + DataSet item; + + int ts = obj.value(QStringLiteral("ts")).toInt(); + item.m_dateTime = QDateTime::fromSecsSinceEpoch(ts); + + item.m_runId = QString::number(obj.value(QStringLiteral("run_id")).toInt()); + + qDebug() << item.m_dateTime << item.m_runId; + + m_index.append(item); + } + } + + emit indexUpdated(m_index); + } +} + +QString GIRO::getRunId(const QDateTime& dateTime) +{ + // Index is ordered newest first + for (int i = 0; i < m_index.size(); i++) + { + if (dateTime > m_index[i].m_dateTime) { + return m_index[i].m_runId; + } + } + return ""; +} diff --git a/sdrbase/util/giro.h b/sdrbase/util/giro.h index 248bb9ab1..1c10ee092 100644 --- a/sdrbase/util/giro.h +++ b/sdrbase/util/giro.h @@ -26,6 +26,7 @@ class QNetworkAccessManager; class QNetworkReply; +class QNetworkDiskCache; // GIRO - Global Ionosphere Radio Observatory // Gets MUFD, TEC, foF2 and other data for various stations around the world @@ -66,14 +67,26 @@ public: } }; + struct DataSet { + QDateTime m_dateTime; + QString m_runId; + }; + static GIRO* create(const QString& service="prop.kc2g.com"); ~GIRO(); - void getDataPeriodically(int periodInMins); - void getMUFPeriodically(int periodInMins); - void getfoF2Periodically(int periodInMins); + void getIndexPeriodically(int periodInMins=15); + void getDataPeriodically(int periodInMins=2); + void getMUFPeriodically(int periodInMins=15); + void getfoF2Periodically(int periodInMins=15); -private slots: + void getMUF(const QString& runId); + void getfoF2(const QString& runId); + + QString getRunId(const QDateTime& dateTime); + +public slots: + void getIndex(); void getData(); void getMUF(); void getfoF2(); @@ -82,17 +95,23 @@ private slots: void handleReply(QNetworkReply* reply); signals: + void indexUpdated(const QList& data); void dataUpdated(const GIROStationData& data); // Called when new data available. void mufUpdated(const QJsonDocument& doc); void foF2Updated(const QJsonDocument& doc); private: bool containsNonNull(const QJsonObject& obj, const QString &key) const; + void handleIndex(QJsonDocument& document); + void handleStations(QJsonDocument& document); + QTimer m_indexTimer; QTimer m_dataTimer; // Timer for periodic updates QTimer m_mufTimer; QTimer m_foF2Timer; QNetworkAccessManager *m_networkManager; + QNetworkDiskCache *m_cache; + QList m_index; }; diff --git a/sdrbase/util/goesxray.cpp b/sdrbase/util/goesxray.cpp new file mode 100644 index 000000000..6e976f21c --- /dev/null +++ b/sdrbase/util/goesxray.cpp @@ -0,0 +1,223 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "goesxray.h" + +#include +#include +#include +#include +#include + +GOESXRay::GOESXRay() +{ + m_networkManager = new QNetworkAccessManager(); + connect(m_networkManager, &QNetworkAccessManager::finished, this, &GOESXRay::handleReply); + connect(&m_dataTimer, &QTimer::timeout, this, &GOESXRay::getData); +} + + +GOESXRay::~GOESXRay() +{ + disconnect(&m_dataTimer, &QTimer::timeout, this, &GOESXRay::getData); + disconnect(m_networkManager, &QNetworkAccessManager::finished, this, &GOESXRay::handleReply); + delete m_networkManager; +} + +GOESXRay* GOESXRay::create(const QString& service) +{ + if (service == "services.swpc.noaa.gov") + { + return new GOESXRay(); + } + else + { + qDebug() << "GOESXRay::create: Unsupported service: " << service; + return nullptr; + } +} + +void GOESXRay::getDataPeriodically(int periodInMins) +{ + if (periodInMins > 0) + { + m_dataTimer.setInterval(periodInMins*60*1000); + m_dataTimer.start(); + getData(); + } + else + { + m_dataTimer.stop(); + } +} + +void GOESXRay::getData() +{ + // Around 160kB per file + QUrl url(QString("https://services.swpc.noaa.gov/json/goes/primary/xrays-6-hour.json")); + m_networkManager->get(QNetworkRequest(url)); + + QUrl secondaryURL(QString("https://services.swpc.noaa.gov/json/goes/secondary/xrays-6-hour.json")); + m_networkManager->get(QNetworkRequest(secondaryURL)); + + QUrl protonPrimaryURL(QString("https://services.swpc.noaa.gov/json/goes/primary/integral-protons-plot-6-hour.json")); + m_networkManager->get(QNetworkRequest(protonPrimaryURL)); +} + +bool GOESXRay::containsNonNull(const QJsonObject& obj, const QString &key) const +{ + if (obj.contains(key)) + { + QJsonValue val = obj.value(key); + return !val.isNull(); + } + return false; +} + +void GOESXRay::handleReply(QNetworkReply* reply) +{ + if (reply) + { + if (!reply->error()) + { + QByteArray bytes = reply->readAll(); + bool primary = reply->url().toString().contains("primary"); + + if (reply->url().fileName() == "xrays-6-hour.json") { + handleXRayJson(bytes, primary); + } else if (reply->url().fileName() == "integral-protons-plot-6-hour.json") { + handleProtonJson(bytes, primary); + } else { + qDebug() << "GOESXRay::handleReply: unexpected filename: " << reply->url().fileName(); + } + } + else + { + qDebug() << "GOESXRay::handleReply: error: " << reply->error(); + } + reply->deleteLater(); + } + else + { + qDebug() << "GOESXRay::handleReply: reply is null"; + } +} + +void GOESXRay::handleXRayJson(const QByteArray& bytes, bool primary) +{ + QJsonDocument document = QJsonDocument::fromJson(bytes); + if (document.isArray()) + { + QJsonArray array = document.array(); + QList data; + for (auto valRef : array) + { + if (valRef.isObject()) + { + QJsonObject obj = valRef.toObject(); + + XRayData measurement; + + if (obj.contains(QStringLiteral("satellite"))) { + measurement.m_satellite = QString("GOES %1").arg(obj.value(QStringLiteral("satellite")).toInt()); + } + if (containsNonNull(obj, QStringLiteral("time_tag"))) { + measurement.m_dateTime = QDateTime::fromString(obj.value(QStringLiteral("time_tag")).toString(), Qt::ISODate); + } + if (containsNonNull(obj, QStringLiteral("flux"))) { + measurement.m_flux = obj.value(QStringLiteral("flux")).toDouble(); + } + if (containsNonNull(obj, QStringLiteral("energy"))) + { + QString energy = obj.value(QStringLiteral("energy")).toString(); + if (energy == "0.05-0.4nm") { + measurement.m_band = XRayData::SHORT; + } else if (energy == "0.1-0.8nm") { + measurement.m_band = XRayData::LONG; + } else { + qDebug() << "GOESXRay::handleXRayJson: Unknown energy: " << energy; + } + } + + data.append(measurement); + } + else + { + qDebug() << "GOESXRay::handleXRayJson: Array element is not an object: " << valRef; + } + } + if (data.size() > 0) { + emit xRayDataUpdated(data, primary); + } else { + qDebug() << "GOESXRay::handleXRayJson: No data in array: " << document; + } + } + else + { + qDebug() << "GOESXRay::handleXRayJson: Document is not an array: " << document; + } +} + +void GOESXRay::handleProtonJson(const QByteArray& bytes, bool primary) +{ + QJsonDocument document = QJsonDocument::fromJson(bytes); + if (document.isArray()) + { + QJsonArray array = document.array(); + QList data; + for (auto valRef : array) + { + if (valRef.isObject()) + { + QJsonObject obj = valRef.toObject(); + + ProtonData measurement; + + if (obj.contains(QStringLiteral("satellite"))) { + measurement.m_satellite = QString("GOES %1").arg(obj.value(QStringLiteral("satellite")).toInt()); + } + if (containsNonNull(obj, QStringLiteral("time_tag"))) { + measurement.m_dateTime = QDateTime::fromString(obj.value(QStringLiteral("time_tag")).toString(), Qt::ISODate); + } + if (containsNonNull(obj, QStringLiteral("flux"))) { + measurement.m_flux = obj.value(QStringLiteral("flux")).toDouble(); + } + if (containsNonNull(obj, QStringLiteral("energy"))) + { + QString energy = obj.value(QStringLiteral("energy")).toString(); + QString value = energy.mid(2).split(' ')[0]; + measurement.m_energy = value.toInt(); // String like: ">=50 MeV" + } + + data.append(measurement); + } + else + { + qDebug() << "GOESXRay::handleProtonJson: Array element is not an object: " << valRef; + } + } + if (data.size() > 0) { + emit protonDataUpdated(data, primary); + } else { + qDebug() << "GOESXRay::handleProtonJson: No data in array: " << document; + } + } + else + { + qDebug() << "GOESXRay::handleProtonJson: Document is not an array: " << document; + } +} diff --git a/sdrbase/util/goesxray.h b/sdrbase/util/goesxray.h new file mode 100644 index 000000000..c7e6d0fb7 --- /dev/null +++ b/sdrbase/util/goesxray.h @@ -0,0 +1,96 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_GOESXRAY_H +#define INCLUDE_GOESXRAY_H + +#include +#include +#include + +#include "export.h" + +class QNetworkAccessManager; +class QNetworkReply; + +// GOES X-Ray data +// This gets 1-minute averages of solar X-rays the 1-8 Angstrom (0.1-0.8 nm) and 0.5-4.0 Angstrom (0.05-0.4 nm) passbands from the GOES satellites +// https://www.swpc.noaa.gov/products/goes-x-ray-flux +// There are primary and secondary data sources, from different satellites, as sometimes they can be in eclipse +// Also gets Proton flux (Which may be observed on Earth a couple of days after a large flare/CME) +class SDRBASE_API GOESXRay : public QObject +{ + Q_OBJECT +protected: + GOESXRay(); + +public: + struct XRayData { + QDateTime m_dateTime; + QString m_satellite; + double m_flux; + enum Band { + UNKNOWN, + SHORT, // 0.05-0.4nm + LONG // 0.1-0.8nm + } m_band; + XRayData() : + m_flux(NAN), + m_band(UNKNOWN) + { + } + }; + + struct ProtonData { + QDateTime m_dateTime; + QString m_satellite; + double m_flux; + int m_energy; // 10=10MeV, 50MeV, 100MeV, 500MeV + ProtonData() : + m_flux(NAN), + m_energy(0) + { + } + }; + + static GOESXRay* create(const QString& service="services.swpc.noaa.gov"); + + ~GOESXRay(); + void getDataPeriodically(int periodInMins=10); + +public slots: + void getData(); + +private slots: + void handleReply(QNetworkReply* reply); + +signals: + void xRayDataUpdated(const QList& data, bool primary); // Called when new data available. + void protonDataUpdated(const QList &data, bool primary); + +private: + bool containsNonNull(const QJsonObject& obj, const QString &key) const; + void handleXRayJson(const QByteArray& bytes, bool primary); + void handleProtonJson(const QByteArray& bytes, bool primary); + + QTimer m_dataTimer; // Timer for periodic updates + QNetworkAccessManager *m_networkManager; + +}; + +#endif /* INCLUDE_GOESXRAY_H */ + diff --git a/sdrbase/util/grb.cpp b/sdrbase/util/grb.cpp new file mode 100644 index 000000000..e42593b68 --- /dev/null +++ b/sdrbase/util/grb.cpp @@ -0,0 +1,198 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 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 "grb.h" +#include "util/csv.h" + +#include +#include +#include +#include +#include + +GRB::GRB() +{ + connect(&m_dataTimer, &QTimer::timeout, this,&GRB::getData); + m_networkManager = new QNetworkAccessManager(); + connect(m_networkManager, &QNetworkAccessManager::finished, this, &GRB::handleReply); + + QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation); + QDir writeableDir(locations[0]); + if (!writeableDir.mkpath(QStringLiteral("cache") + QDir::separator() + QStringLiteral("grb"))) { + qDebug() << "Failed to create cache/grb"; + } + + m_cache = new QNetworkDiskCache(); + m_cache->setCacheDirectory(locations[0] + QDir::separator() + QStringLiteral("cache") + QDir::separator() + QStringLiteral("grb")); + m_cache->setMaximumCacheSize(100000000); + m_networkManager->setCache(m_cache); +} + +GRB::~GRB() +{ + disconnect(&m_dataTimer, &QTimer::timeout, this, &GRB::getData); + disconnect(m_networkManager, &QNetworkAccessManager::finished, this, &GRB::handleReply); + delete m_networkManager; +} + +GRB* GRB::create() +{ + return new GRB(); +} + +void GRB::getDataPeriodically(int periodInMins) +{ + if (periodInMins > 0) + { + m_dataTimer.setInterval(periodInMins*60*1000); + m_dataTimer.start(); + getData(); + } + else + { + m_dataTimer.stop(); + } +} + +void GRB::getData() +{ + QUrl url("https://user-web.icecube.wisc.edu/~grbweb_public/Summary_table.txt"); + + m_networkManager->get(QNetworkRequest(url)); +} + +void GRB::handleReply(QNetworkReply* reply) +{ + if (reply) + { + if (!reply->error()) + { + if (reply->url().fileName().endsWith(".txt")) + { + QByteArray bytes = reply->readAll(); + handleText(bytes); + } + else + { + qDebug() << "GRB::handleReply: Unexpected file" << reply->url().fileName(); + } + } + else + { + qDebug() << "GRB::handleReply: Error: " << reply->error(); + } + reply->deleteLater(); + } + else + { + qDebug() << "GRB::handleReply: Reply is null"; + } +} + +void GRB::handleText(QByteArray& bytes) +{ + // Convert to CSV + QString s(bytes); + QStringList l = s.split("\n"); + for (int i = 0; i < l.size(); i++) { + l[i] = l[i].simplified().replace(" ", ","); + } + s = l.join("\n"); + + QTextStream in(&s); + + // Skip header + for (int i = 0; i < 4; i++) { + in.readLine(); + } + + QList grbs; + QStringList cols; + while(CSV::readRow(in, &cols)) + { + Data grb; + + if (cols.length() >= 10) + { + grb.m_name = cols[0]; + grb.m_fermiName = cols[1]; + int year = grb.m_name.mid(3, 2).toInt(); + if (year >= 90) { + year += 1900; + } else { + year += 2000; + } + QDate date(year, grb.m_name.mid(5, 2).toInt(), grb.m_name.mid(7, 2).toInt()); + QTime time = QTime::fromString(cols[2]); + grb.m_dateTime = QDateTime(date, time); + grb.m_ra = cols[3].toFloat(); + grb.m_dec = cols[4].toFloat(); + grb.m_fluence = cols[9].toFloat(); + + //qDebug() << grb.m_name << grb.m_dateTime.toString() << grb.m_ra << grb.m_dec << grb.m_fluence ; + + if (grb.m_dateTime.isValid()) { + grbs.append(grb); + } + } + } + + emit dataUpdated(grbs); +} + + QString GRB::Data::getFermiURL() const +{ + if (m_fermiName.isEmpty() || (m_fermiName == "None")) { + return ""; + } + QString base = "https://heasarc.gsfc.nasa.gov/FTP/fermi/data/gbm/bursts/"; + QString yearDir = "20" + m_fermiName.mid(3, 2); + QString dataDir = m_fermiName; + dataDir.replace("GRB", "bn"); + return base + yearDir + "/" + dataDir + "/current/"; +} + +QString GRB::Data::getFermiPlotURL() const +{ + QString base = getFermiURL(); + if (base.isEmpty()) { + return ""; + } + + QString name = m_fermiName; + name.replace("GRB", "bn"); + return getFermiURL() + "glg_lc_all_" + name + "_v00.gif"; // Could be v01.gif? How to know without fetching index? +} + +QString GRB::Data::getFermiSkyMapURL() const +{ + QString base = getFermiURL(); + if (base.isEmpty()) { + return ""; + } + + QString name = m_fermiName; + name.replace("GRB", "bn"); + return getFermiURL() + "glg_skymap_all_" + name + "_v00.png"; +} + +QString GRB::Data::getSwiftURL() const +{ + QString name = m_name; + name.replace("GRB", ""); + return "https://swift.gsfc.nasa.gov/archive/grb_table/" + name; +} diff --git a/sdrbase/util/grb.h b/sdrbase/util/grb.h new file mode 100644 index 000000000..a0075de19 --- /dev/null +++ b/sdrbase/util/grb.h @@ -0,0 +1,81 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 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_GRB_H +#define INCLUDE_GRB_H + +#include +#include + +#include "export.h" + +class QNetworkAccessManager; +class QNetworkReply; +class QNetworkDiskCache; + +// GRB (Gamma Ray Burst) database +// Gets GRB database from GRBweb https://user-web.icecube.wisc.edu/~grbweb_public/ +// Uses summary .txt file so only contains last 1000 GRBs +class SDRBASE_API GRB : public QObject +{ + Q_OBJECT +protected: + GRB(); + +public: + + struct SDRBASE_API Data { + + QString m_name; // E.g: GRB240310A + QString m_fermiName; // Name used by Fermi telescope. E.g. GRB240310236. Can be None if not detected by Fermi + QDateTime m_dateTime; + float m_ra; // Right Ascension + float m_dec; // Declination + float m_fluence; // erg/cm^2 + + QString getFermiURL() const; // Get URL where Fermi data is stored + QString getFermiPlotURL() const; + QString getFermiSkyMapURL() const; + QString getSwiftURL() const; + + }; + + static GRB* create(); + + ~GRB(); + void getDataPeriodically(int periodInMins=1440); // GRBweb is updated every 24 hours, usually just after 9am UTC + +public slots: + void getData(); + +private slots: + void handleReply(QNetworkReply* reply); + +signals: + void dataUpdated(const QListdata); // Called when new data is available. + +private: + + QTimer m_dataTimer; // Timer for periodic updates + QNetworkAccessManager *m_networkManager; + QNetworkDiskCache *m_cache; + + void handleText(QByteArray& bytes); + +}; + +#endif /* INCLUDE_GRB_H */ diff --git a/sdrbase/util/solardynamicsobservatory.cpp b/sdrbase/util/solardynamicsobservatory.cpp new file mode 100644 index 000000000..22d80acda --- /dev/null +++ b/sdrbase/util/solardynamicsobservatory.cpp @@ -0,0 +1,433 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 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 "solardynamicsobservatory.h" + +#include +#include +#include +#include + +SolarDynamicsObservatory::SolarDynamicsObservatory() : + m_size(512), + m_todayCache(nullptr) +{ + connect(&m_dataTimer, &QTimer::timeout, this, qOverload<>(&SolarDynamicsObservatory::getImage)); + m_networkManager = new QNetworkAccessManager(); + connect(m_networkManager, &QNetworkAccessManager::finished, this, &SolarDynamicsObservatory::handleReply); + + QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation); + QDir writeableDir(locations[0]); + if (!writeableDir.mkpath(QStringLiteral("cache") + QDir::separator() + QStringLiteral("solardynamicsobservatory"))) { + qDebug() << "SolarDynamicsObservatory::SolarDynamicsObservatory: Failed to create cache/solardynamicsobservatory"; + } + + m_cache = new QNetworkDiskCache(); + m_cache->setCacheDirectory(locations[0] + QDir::separator() + QStringLiteral("cache") + QDir::separator() + QStringLiteral("solardynamicsobservatory")); + m_cache->setMaximumCacheSize(100000000); + m_networkManager->setCache(m_cache); +} + +SolarDynamicsObservatory::~SolarDynamicsObservatory() +{ + disconnect(&m_dataTimer, &QTimer::timeout, this, qOverload<>(&SolarDynamicsObservatory::getImage)); + disconnect(m_networkManager, &QNetworkAccessManager::finished, this, &SolarDynamicsObservatory::handleReply); + delete m_networkManager; + delete m_todayCache; +} + +SolarDynamicsObservatory* SolarDynamicsObservatory::create() +{ + return new SolarDynamicsObservatory(); +} + +QList SolarDynamicsObservatory::getImageSizes() +{ + return {512, 1024, 2048, 4096}; +} + +QList SolarDynamicsObservatory::getVideoSizes() +{ + return {512, 1024}; +} + +const QStringList SolarDynamicsObservatory::getImageNames() +{ + QChar angstronm(0x212B); + QStringList names; + + // SDO + names.append(QString("AIA 094 %1").arg(angstronm)); + names.append(QString("AIA 131 %1").arg(angstronm)); + names.append(QString("AIA 171 %1").arg(angstronm)); + names.append(QString("AIA 193 %1").arg(angstronm)); + names.append(QString("AIA 211 %1").arg(angstronm)); + names.append(QString("AIA 304 %1").arg(angstronm)); + names.append(QString("AIA 335 %1").arg(angstronm)); + names.append(QString("AIA 1600 %1").arg(angstronm)); + names.append(QString("AIA 1700 %1").arg(angstronm)); + names.append(QString("AIA 211 %1, 193 %1, 171 %1").arg(angstronm)); + names.append(QString("AIA 304 %1, 211 %1, 171 %1").arg(angstronm)); + names.append(QString("AIA 094 %1, 335 %1, 193 %1").arg(angstronm)); + names.append(QString("AIA 171 %1, HMIB").arg(angstronm)); + names.append("HMI Magneotgram"); + names.append("HMI Colorized Magneotgram"); + names.append("HMI Intensitygram - Colored"); + names.append("HMI Intensitygram - Flattened"); + names.append("HMI Intensitygram"); + names.append("HMI Dopplergram"); + + // SOHO + names.append("LASCO C2"); + names.append("LASCO C3"); + + return names; +} + +const QStringList SolarDynamicsObservatory::getChannelNames() +{ + QStringList channelNames = { + "0094", + "0131", + "0171", + "0193", + "0211", + "0304", + "0335", + "1600", + "1700", + "211193171", + "304211171", + "094335193", + "HMImag", + "HMIB", + "HMIBC", + "HMIIC", + "HMIIF", + "HMII", + "HMID", + "c2", + "c3" + }; + + return channelNames; +} + +const QStringList SolarDynamicsObservatory::getImageFileNames() +{ + // Ordering needs to match getImageNames() + // %1 replaced with size + QStringList filenames = { + "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_0094.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_0131.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_0171.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_0193.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_0211.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_0304.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_0335.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_1600.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_1700.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_211193171.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/f_304_211_171_%1.jpg", + //"https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_304211171.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/f_094_335_193_%1.jpg", + //"https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_094335193.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/f_HMImag_171_%1.jpg", + //"https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_HMImag.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_HMIB.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_HMIBC.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_HMIIC.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_HMIIF.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_HMII.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_HMID.jpg", + "https://soho.nascom.nasa.gov/data/realtime/c2/512/latest.jpg", + "https://soho.nascom.nasa.gov/data/realtime/c3/512/latest.jpg" + }; + + return filenames; +} + +const QStringList SolarDynamicsObservatory::getVideoNames() +{ + QChar angstronm(0x212B); + QStringList names; + + // SDO + names.append(QString("AIA 094 %1").arg(angstronm)); + names.append(QString("AIA 131 %1").arg(angstronm)); + names.append(QString("AIA 171 %1").arg(angstronm)); + names.append(QString("AIA 193 %1").arg(angstronm)); + names.append(QString("AIA 211 %1").arg(angstronm)); + names.append(QString("AIA 304 %1").arg(angstronm)); + names.append(QString("AIA 335 %1").arg(angstronm)); + names.append(QString("AIA 1600 %1").arg(angstronm)); + names.append(QString("AIA 1700 %1").arg(angstronm)); + + // SOHO + names.append("LASCO C2"); + names.append("LASCO C3"); + + return names; +} + +const QStringList SolarDynamicsObservatory::getVideoFileNames() +{ + const QStringList filenames = { + "http://sdo.gsfc.nasa.gov/assets/img/latest/mpeg/latest_%1_0094.mp4", // Videos sometimes fail to load on Windows if https used + "http://sdo.gsfc.nasa.gov/assets/img/latest/mpeg/latest_%1_0131.mp4", + "http://sdo.gsfc.nasa.gov/assets/img/latest/mpeg/latest_%1_0171.mp4", + "http://sdo.gsfc.nasa.gov/assets/img/latest/mpeg/latest_%1_0193.mp4", + "http://sdo.gsfc.nasa.gov/assets/img/latest/mpeg/latest_%1_0211.mp4", + "http://sdo.gsfc.nasa.gov/assets/img/latest/mpeg/latest_%1_0304.mp4", + "http://sdo.gsfc.nasa.gov/assets/img/latest/mpeg/latest_%1_0335.mp4", + "http://sdo.gsfc.nasa.gov/assets/img/latest/mpeg/latest_%1_1600.mp4", + "http://sdo.gsfc.nasa.gov/assets/img/latest/mpeg/latest_%1_1700.mp4", + "http://soho.nascom.nasa.gov/data/LATEST/current_c2.mp4", + "http://soho.nascom.nasa.gov/data/LATEST/current_c3.mp4", + }; + + return filenames; +} + +QString SolarDynamicsObservatory::getImageURL(const QString& image, int size) +{ + const QStringList names = SolarDynamicsObservatory::getImageNames(); + const QStringList filenames = SolarDynamicsObservatory::getImageFileNames(); + int idx = names.indexOf(image); + + if (idx != -1) { + return QString(filenames[idx]).arg(size); + } else { + return ""; + } +} + +QString SolarDynamicsObservatory::getVideoURL(const QString& video, int size) +{ + const QStringList names = SolarDynamicsObservatory::getVideoNames(); + const QStringList filenames = SolarDynamicsObservatory::getVideoFileNames(); + int idx = names.indexOf(video); + + if (idx != -1) { + return QString(filenames[idx]).arg(size); + } else { + return ""; + } +} + +void SolarDynamicsObservatory::getImagePeriodically(const QString& image, int size, int periodInMins) +{ + m_image = image; + m_size = size; + if (periodInMins > 0) + { + m_dataTimer.setInterval(periodInMins*60*1000); + m_dataTimer.start(); + getImage(); + } + else + { + m_dataTimer.stop(); + } +} + +void SolarDynamicsObservatory::getImage() +{ + getImage(m_image, m_size); +} + +void SolarDynamicsObservatory::getImage(const QString& imageName, int size) +{ + QString urlString = getImageURL(imageName, size); + if (!urlString.isEmpty()) + { + QUrl url(urlString); + + m_networkManager->get(QNetworkRequest(url)); + } +} + +void SolarDynamicsObservatory::getImage(const QString& imageName, QDateTime dateTime, int size) +{ + // Stop periodic updates, if not after latest data + m_dataTimer.stop(); + + // Save details of image we are after + Request request; + request.m_dateTime = dateTime; + request.m_size = size; + request.m_image = imageName; + + // Get file index, as we don't know what time will be used in the file + QDate date = dateTime.date(); + if (m_indexCache.contains(date)) + { + handleIndex(m_indexCache.take(date), request); + } + else if ((m_todayCache != nullptr) && (date == m_todayCacheDateTime.date()) && (dateTime < m_todayCacheDateTime.addSecs(-60 * 60))) + { + handleIndex(m_todayCache, request); + } + else + { + QString urlString = QString("https://sdo.gsfc.nasa.gov/assets/img/browse/%1/%2/%3/") + .arg(date.year()) + .arg(date.month(), 2, 10, QLatin1Char('0')) + .arg(date.day(), 2, 10, QLatin1Char('0')); + QUrl url(urlString); + + request.m_url = urlString; + m_requests.append(request); + + m_networkManager->get(QNetworkRequest(url)); + } +} + +void SolarDynamicsObservatory::handleReply(QNetworkReply* reply) +{ + if (reply) + { + if (!reply->error()) + { + if (reply->url().fileName().endsWith(".jpg")) + { + handleJpeg(reply->readAll()); + } + else + { + // Find corresponding request + QString urlString = reply->url().toString(); + + for (int i = 0; i < m_requests.size(); i++) + { + if (m_requests[i].m_url == urlString) + { + QByteArray *bytes = new QByteArray(reply->readAll()); + + handleIndex(bytes, m_requests[i]); + m_requests.removeAt(i); + break; + } + } + } + } + else + { + qDebug() << "SolarDynamicsObservatory::handleReply: Error: " << reply->error(); + } + reply->deleteLater(); + } + else + { + qDebug() << "SolarDynamicsObservatory::handleReply: Reply is null"; + } +} + +void SolarDynamicsObservatory::handleJpeg(const QByteArray& bytes) +{ + QImage image; + + if (image.loadFromData(bytes)) { + emit imageUpdated(image); + } else { + qWarning() << "SolarDynamicsObservatory::handleJpeg: Failed to load image"; + } +} + +void SolarDynamicsObservatory::handleIndex(QByteArray* bytes, const Request& request) +{ + const QStringList names = SolarDynamicsObservatory::getImageNames(); + const QStringList channelNames = SolarDynamicsObservatory::getChannelNames(); + int idx = names.indexOf(request.m_image); + if (idx < 0) { + return; + } + QString channel = channelNames[idx]; + + QString file(*bytes); + QStringList lines = file.split("\n"); + + QString date = request.m_dateTime.date().toString("yyyyMMdd"); + QString pattern = QString("\"%1_([0-9]{6})_%2_%3.jpg\"").arg(date).arg(request.m_size).arg(channel); + QRegularExpression re(pattern); + + // Get all times the image is available + QList times; + for (const auto& line : lines) + { + QRegularExpressionMatch match = re.match(line); + if (match.hasMatch()) + { + QString t = match.capturedTexts()[1]; + int h = t.left(2).toInt(); + int m = t.mid(2, 2).toInt(); + int s = t.right(2).toInt(); + times.append(QTime(h, m, s)); + } + } + + if (times.length() > 0) + { + QTime target = request.m_dateTime.time(); + QTime current = times[0]; + for (int i = 1; i < times.size(); i++) + { + if (target < times[i]) { + break; + } + current = times[i]; + } + + // Get image + QDate date = request.m_dateTime.date(); + QString urlString = QString("https://sdo.gsfc.nasa.gov/assets/img/browse/%1/%2/%3/%1%2%3_%4%5%6_%7_%8.jpg") + .arg(date.year()) + .arg(date.month(), 2, 10, QLatin1Char('0')) + .arg(date.day(), 2, 10, QLatin1Char('0')) + .arg(current.hour(), 2, 10, QLatin1Char('0')) + .arg(current.minute(), 2, 10, QLatin1Char('0')) + .arg(current.second(), 2, 10, QLatin1Char('0')) + .arg(request.m_size) + .arg(channel); + + QUrl url(urlString); + + m_networkManager->get(QNetworkRequest(url)); + } + else + { + qDebug() << "SolarDynamicsObservatory: No image available for " << request.m_dateTime; + } + + // Save index in cache + if (request.m_dateTime.date() == QDate::currentDate()) + { + if (m_todayCache != bytes) + { + m_todayCache = bytes; + m_todayCacheDateTime = QDateTime::currentDateTime(); + } + } + else if (request.m_dateTime.date() < QDate::currentDate()) + { + m_indexCache.insert(request.m_dateTime.date(), bytes); + } + else + { + delete bytes; + } +} diff --git a/sdrbase/util/solardynamicsobservatory.h b/sdrbase/util/solardynamicsobservatory.h new file mode 100644 index 000000000..0e3925fb8 --- /dev/null +++ b/sdrbase/util/solardynamicsobservatory.h @@ -0,0 +1,95 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 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_SOLARDYNAMICSOBSERVATORY_H +#define INCLUDE_SOLARDYNAMICSOBSERVATORY_H + +#include +#include +#include +#include + +#include "export.h" + +class QNetworkAccessManager; +class QNetworkReply; +class QNetworkDiskCache; + +// This gets solar imagery from SDO (Solar Dynamics Observatory) - https://sdo.gsfc.nasa.gov/ +// and LASCO images from SOHO - https://soho.nascom.nasa.gov/ +class SDRBASE_API SolarDynamicsObservatory : public QObject +{ + Q_OBJECT +protected: + SolarDynamicsObservatory(); + +public: + + static SolarDynamicsObservatory* create(); + + ~SolarDynamicsObservatory(); + void getImagePeriodically(const QString& image, int size=512, int periodInMins=15); + void getImage(const QString& m_image, int size); + void getImage(const QString& m_image, QDateTime dateTime, int size=512); + + static QString getImageURL(const QString& image, int size); + static QString getVideoURL(const QString& video, int size=512); + + static QList getImageSizes(); + static const QStringList getChannelNames(); + static const QStringList getImageNames(); + static QList getVideoSizes(); + static const QStringList getVideoNames(); + +private slots: + void getImage(); + void handleReply(QNetworkReply* reply); + +signals: + void imageUpdated(const QImage& image); // Called when new image is available. + +private: + + struct Request { + QString m_url; + QDateTime m_dateTime; + int m_size; + QString m_image; + }; + + QTimer m_dataTimer; // Timer for periodic updates + QString m_image; // Saved parameters for periodic updates + int m_size; + + QNetworkAccessManager *m_networkManager; + QNetworkDiskCache *m_cache; + + // Index page isn't cachable (using network cache), so we cache it ourselves, as it can take up to 5 seconds to fetch + QCache m_indexCache; + QDateTime m_todayCacheDateTime; + QByteArray *m_todayCache; + + QList m_requests; + + void handleJpeg(const QByteArray& bytes); + void handleIndex(QByteArray *bytes, const Request& request); + static const QStringList getImageFileNames(); + static const QStringList getVideoFileNames(); + +}; + +#endif /* INCLUDE_SOLARDYNAMICSOBSERVATORY_H */ diff --git a/sdrbase/util/stix.cpp b/sdrbase/util/stix.cpp new file mode 100644 index 000000000..9230141da --- /dev/null +++ b/sdrbase/util/stix.cpp @@ -0,0 +1,176 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 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 "stix.h" + +#include +#include +#include +#include +#include + +STIX::STIX() +{ + m_networkManager = new QNetworkAccessManager(); + connect(m_networkManager, &QNetworkAccessManager::finished, this, &STIX::handleReply); + connect(&m_dataTimer, &QTimer::timeout, this, &STIX::getData); +} + + +STIX::~STIX() +{ + disconnect(&m_dataTimer, &QTimer::timeout, this, &STIX::getData); + disconnect(m_networkManager, &QNetworkAccessManager::finished, this, &STIX::handleReply); + delete m_networkManager; +} + +STIX* STIX::create() +{ + return new STIX(); +} + +void STIX::getDataPeriodically(int periodInMins) +{ + if (periodInMins > 0) + { + m_dataTimer.setInterval(periodInMins*60*1000); + m_dataTimer.start(); + getData(); + } + else + { + m_dataTimer.stop(); + } +} + +void STIX::getData() +{ + QUrlQuery data(QString("https://datacenter.stix.i4ds.net/api/request/flare-list")); + QDateTime start; + + if (m_mostRecent.isValid()) { + start = m_mostRecent; + } else { + start = QDateTime::currentDateTime().addDays(-5); + } + + data.addQueryItem("start_utc", start.toString(Qt::ISODate)); + data.addQueryItem("end_utc", QDateTime::currentDateTime().toString(Qt::ISODate)); + data.addQueryItem("sort", "time"); + + QUrl url("https://datacenter.stix.i4ds.net/api/request/flare-list"); + QNetworkRequest request(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + m_networkManager->post(request, data.toString(QUrl::FullyEncoded).toUtf8()); +} + +bool STIX::containsNonNull(const QJsonObject& obj, const QString &key) const +{ + if (obj.contains(key)) + { + QJsonValue val = obj.value(key); + return !val.isNull(); + } + return false; +} + +void STIX::handleReply(QNetworkReply* reply) +{ + if (reply) + { + if (!reply->error()) + { + QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); + + if (document.isArray()) + { + QJsonArray array = document.array(); + QList data; + for (auto valRef : array) + { + if (valRef.isObject()) + { + QJsonObject obj = valRef.toObject(); + + FlareData measurement; + + if (obj.contains(QStringLiteral("flare_id"))) { + measurement.m_id = obj.value(QStringLiteral("flare_id")).toString(); + } + if (obj.contains(QStringLiteral("start_UTC"))) + { + measurement.m_startDateTime = QDateTime::fromString(obj.value(QStringLiteral("start_UTC")).toString(), Qt::ISODate); + if (!m_mostRecent.isValid() || (measurement.m_startDateTime > m_mostRecent)) { + m_mostRecent = measurement.m_startDateTime; + } + } + if (obj.contains(QStringLiteral("end_UTC"))) { + measurement.m_endDateTime = QDateTime::fromString(obj.value(QStringLiteral("end_UTC")).toString(), Qt::ISODate); + } + if (obj.contains(QStringLiteral("peak_UTC"))) { + measurement.m_peakDateTime = QDateTime::fromString(obj.value(QStringLiteral("peak_UTC")).toString(), Qt::ISODate); + } + if (obj.contains(QStringLiteral("duration"))) { + measurement.m_duration = obj.value(QStringLiteral("duration")).toInt(); + } + if (obj.contains(QStringLiteral("GOES_flux"))) { + measurement.m_flux = obj.value(QStringLiteral("GOES_flux")).toDouble(); + } + + data.append(measurement); + } + else + { + qDebug() << "STIX::handleReply: Array element is not an object: " << valRef; + } + } + if (data.size() > 0) + { + m_data.append(data); + emit dataUpdated(m_data); + } + else + { + qDebug() << "STIX::handleReply: No data in array: " << document; + } + } + else + { + qDebug() << "STIX::handleReply: Document is not an array: " << document; + } + } + else + { + qDebug() << "STIX::handleReply: error: " << reply->error(); + } + reply->deleteLater(); + } + else + { + qDebug() << "STIX::handleReply: reply is null"; + } +} + + QString STIX::FlareData::getLightCurvesURL() const + { + return QString("https://datacenter.stix.i4ds.net/view/plot/lightcurves?start=%1&span=%2").arg(m_startDateTime.toSecsSinceEpoch()).arg(m_duration); + } + + QString STIX::FlareData::getDataURL() const + { + return QString("https://datacenter.stix.i4ds.net/view/list/fits/%1/%2").arg(m_startDateTime.toSecsSinceEpoch()).arg(m_duration); + } diff --git a/sdrbase/util/stix.h b/sdrbase/util/stix.h new file mode 100644 index 000000000..924b8e7ea --- /dev/null +++ b/sdrbase/util/stix.h @@ -0,0 +1,83 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 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_STIX_H +#define INCLUDE_STIX_H + +#include +#include +#include +#include + +#include "export.h" + +class QNetworkAccessManager; +class QNetworkReply; + +// Solar Orbiter STIX (Spectrometer/Telescope for Imaging X-rays) instrument +// Gets solar flare data - Newest data is often about 24 hours old +class SDRBASE_API STIX : public QObject +{ + Q_OBJECT +protected: + STIX(); + +public: + struct SDRBASE_API FlareData { + QString m_id; + QDateTime m_startDateTime; + QDateTime m_endDateTime; + QDateTime m_peakDateTime; + int m_duration; // In seconds + double m_flux; + FlareData() : + m_duration(0), + m_flux(NAN) + { + } + + QString getLightCurvesURL() const; + QString getDataURL() const; + }; + + static STIX* create(); + + ~STIX(); + void getDataPeriodically(int periodInMins=60); + +public slots: + void getData(); + +private slots: + void handleReply(QNetworkReply* reply); + +signals: + void dataUpdated(const QList& data); // Called when new data available. + +private: + bool containsNonNull(const QJsonObject& obj, const QString &key) const; + + QTimer m_dataTimer; // Timer for periodic updates + QNetworkAccessManager *m_networkManager; + + QDateTime m_mostRecent; + QList m_data; + +}; + +#endif /* INCLUDE_STIX_H */ + diff --git a/sdrbase/util/vlftransmitters.cpp b/sdrbase/util/vlftransmitters.cpp new file mode 100644 index 000000000..8b8b73549 --- /dev/null +++ b/sdrbase/util/vlftransmitters.cpp @@ -0,0 +1,116 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 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 "util/csv.h" + +#include "vlftransmitters.h" + +// https://sidstation.loudet.org/stations-list-en.xhtml +// https://core.ac.uk/download/pdf/224769021.pdf -- Table 1 +// GQD/GQZ callsigns: https://groups.io/g/VLF/message/19212?p=%2C%2C%2C20%2C0%2C0%2C0%3A%3Arecentpostdate%2Fsticky%2C%2C19.6%2C20%2C2%2C0%2C38924431 +QList VLFTransmitters::m_transmitters = { + {QStringLiteral("JXN"), 16400, 66.974353, 13.873617, -1}, // Novik, Norway (Only transmits 6 times a day) + {QStringLiteral("VTX2"), 17000, 8.387015, 77.752762, -1}, // South Vijayanarayanam, India + {QStringLiteral("RDL"), 18100, 44.773333, 39.547222, -1}, // Krasnodar, Russia (Transmits short bursts, possibly FSK) + {QStringLiteral("GQD"), 19580, 54.911643, -3.278456, 100}, // Anthorn, UK, Often referred to as GBZ + {QStringLiteral("NWC"), 19800, -21.816325, 114.16546, 1000}, // Exmouth, Aus + {QStringLiteral("ICV"), 20270, 40.922946, 9.731881, 50}, // Isola di Tavolara, Italy (Can be distorted on 3D map if terrain used) + {QStringLiteral("FTA"), 20900, 48.544632, 2.579429, 50}, // Sainte-Assise, France (Satellite imagary obfuscated) + {QStringLiteral("NPM"), 21400, 21.420166, -158.151140, 600}, // Pearl Harbour, Lualuahei, USA (Not seen?) + {QStringLiteral("HWU"), 21750, 46.713129, 1.245248, 200}, // Rosnay, France + {QStringLiteral("GQZ"), 22100, 54.731799, -2.883033, 100}, // Skelton, UK (GVT in paper) + {QStringLiteral("DHO38"), 23400, 53.078900, 7.615000, 300}, // Rhauderfehn, Germany - Off air 7-8 UTC + {QStringLiteral("NAA"), 24000, 44.644506, -67.284565, 1000}, // Cutler, Maine, USA + {QStringLiteral("TBB"), 26700, 37.412725, 27.323342, -1}, // Bafa, Turkey + {QStringLiteral("TFK/NRK"), 37500, 63.850365, -22.466773, 100}, // Grindavik, Iceland + {QStringLiteral("SRC"), 40400, 57.120328, 16.153083, -1}, // Grimeton, Sweden + {QStringLiteral("NSY"), 45900, 37.125660, 14.436416, -1}, // Niscemi, Italy + {QStringLiteral("SXA"), 49000, 38.145155, 24.019718, -1}, // Marathon, Greece + {QStringLiteral("GYW1"), 51950, 57.617463, -1.887589, -1}, // Crimond, UK + {QStringLiteral("FUE"), 65800, 48.637673, -4.350758, -1}, // Kerlouan, France +}; + +QHash VLFTransmitters::m_callsignHash; + +VLFTransmitters::Init VLFTransmitters::m_init; + +VLFTransmitters::Init::Init() +{ + // Get directory to store app data in + QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation); + // First dir is writable + QString dir = locations[0]; + + // Try reading transmitters from .csv file + QString filename = QString("%1/%2/%3/vlftransmitters.csv").arg(dir).arg(COMPANY).arg(APPLICATION_NAME); // Because this method is called before main(), we need to add f4exb/SDRangel + QFile file(filename); + if (file.open(QIODevice::ReadOnly | QIODevice::Text)) + { + QTextStream in(&file); + + QString error; + QHash colIndexes = CSV::readHeader(in, { + QStringLiteral("Callsign"), + QStringLiteral("Frequency"), + QStringLiteral("Latitude"), + QStringLiteral("Longitude"), + QStringLiteral("Power") + }, error); + if (error.isEmpty()) + { + QStringList cols; + int callsignCol = colIndexes.value(QStringLiteral("Callsign")); + int frequencyCol = colIndexes.value(QStringLiteral("Frequency")); + int latitudeCol = colIndexes.value(QStringLiteral("Latitude")); + int longitudeCol = colIndexes.value(QStringLiteral("Longitude")); + int powerCol = colIndexes.value(QStringLiteral("Power")); + int maxCol = std::max({callsignCol, frequencyCol, latitudeCol, longitudeCol, powerCol}); + + m_transmitters.clear(); // Replace builtin list + + while(CSV::readRow(in, &cols)) + { + if (cols.size() > maxCol) + { + Transmitter transmitter; + + transmitter.m_callsign = cols[callsignCol]; + transmitter.m_frequency = cols[frequencyCol].toLongLong(); + transmitter.m_latitude = cols[latitudeCol].toFloat(); + transmitter.m_longitude = cols[longitudeCol].toFloat(); + transmitter.m_power = cols[powerCol].toInt(); + + m_transmitters.append(transmitter); + } + } + } + else + { + qWarning() << filename << "did not contain expected headers."; + } + } + + // Create hash table for faster searching + for (const auto& transmitter : VLFTransmitters::m_transmitters) { + VLFTransmitters::m_callsignHash.insert(transmitter.m_callsign, &transmitter); + } +} diff --git a/sdrbase/util/vlftransmitters.h b/sdrbase/util/vlftransmitters.h new file mode 100644 index 000000000..2fc7c1a7c --- /dev/null +++ b/sdrbase/util/vlftransmitters.h @@ -0,0 +1,56 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 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_VLFTRANSMITTERS_H +#define INCLUDE_VLFTRANSMITTERS_H + +#include +#include +#include + +#include "export.h" + +// List of VLF transmitters +// Built-in list can be overriden by user supplied vlftransmitters.csv file, that is read at startup, from the app data dir +class SDRBASE_API VLFTransmitters +{ + +public: + + struct Transmitter { + QString m_callsign; + qint64 m_frequency; // In Hz + float m_latitude; + float m_longitude; + int m_power; // In kW + }; + + static QList m_transmitters; + + static QHash m_callsignHash; + +private: + + friend struct Init; + struct Init { + Init(); + }; + static Init m_init; + +}; + +#endif /* VLFTransmitters */ diff --git a/sdrbase/webapi/webapirequestmapper.cpp b/sdrbase/webapi/webapirequestmapper.cpp index 162617597..50891d249 100644 --- a/sdrbase/webapi/webapirequestmapper.cpp +++ b/sdrbase/webapi/webapirequestmapper.cpp @@ -5280,6 +5280,11 @@ bool WebAPIRequestMapper::getFeatureSettings( featureSettings->getSatelliteTrackerSettings()->init(); featureSettings->getSatelliteTrackerSettings()->fromJsonObject(settingsJsonObject); } + else if (featureSettingsKey == "SIDSettings") + { + featureSettings->setSidSettings(new SWGSDRangel::SWGSIDSettings()); + featureSettings->getSidSettings()->fromJsonObject(settingsJsonObject); + } else if (featureSettingsKey == "SimplePTTSettings") { featureSettings->setSimplePttSettings(new SWGSDRangel::SWGSimplePTTSettings()); @@ -5648,6 +5653,7 @@ void WebAPIRequestMapper::resetFeatureSettings(SWGSDRangel::SWGFeatureSettings& featureSettings.setMapSettings(nullptr); featureSettings.setPerTesterSettings(nullptr); featureSettings.setSatelliteTrackerSettings(nullptr); + featureSettings.setSidSettings(nullptr); featureSettings.setSimplePttSettings(nullptr); featureSettings.setSkyMapSettings(nullptr); featureSettings.setStarTrackerSettings(nullptr); diff --git a/sdrbase/webapi/webapiutils.cpp b/sdrbase/webapi/webapiutils.cpp index 17948bf31..3ceb398b4 100644 --- a/sdrbase/webapi/webapiutils.cpp +++ b/sdrbase/webapi/webapiutils.cpp @@ -323,6 +323,7 @@ const QMap WebAPIUtils::m_featureTypeToSettingsKey = { {"Radiosonde", "RadiosondeSettings"}, {"RigCtlServer", "RigCtlServerSettings"}, {"SatelliteTracker", "SatelliteTrackerSettings"}, + {"SID", "SIDSettings"}, {"SimplePTT", "SimplePTTSettings"}, {"SkyMap", "SkyMapSettings"}, {"StarTracker", "StarTrackerSettings"}, diff --git a/sdrgui/CMakeLists.txt b/sdrgui/CMakeLists.txt index 39bec49ac..f1089b92a 100644 --- a/sdrgui/CMakeLists.txt +++ b/sdrgui/CMakeLists.txt @@ -17,6 +17,7 @@ set(sdrgui_SOURCES gui/buttonswitch.cpp gui/channeladddialog.cpp gui/clickablelabel.cpp + gui/colordialog.cpp gui/colormapper.cpp gui/commanditem.cpp gui/commandsdialog.cpp @@ -88,6 +89,7 @@ set(sdrgui_SOURCES gui/spectrummarkersdialog.cpp gui/spectrummeasurementsdialog.cpp gui/spectrummeasurements.cpp + gui/tablecolorchooser.cpp gui/tabletapandhold.cpp gui/tickedslider.cpp gui/timedelegate.cpp @@ -142,6 +144,7 @@ set(sdrgui_HEADERS gui/basicfeaturesettingsdialog.h gui/buttonswitch.h gui/channeladddialog.h + gui/colordialog.h gui/colormapper.h gui/commanditem.h gui/commandsdialog.h @@ -217,6 +220,7 @@ set(sdrgui_HEADERS gui/spectrummarkersdialog.h gui/spectrummeasurementsdialog.h gui/spectrummeasurements.h + gui/tablecolorchooser.h gui/tabletapandhold.h gui/tickedslider.h gui/timedelegate.h diff --git a/plugins/feature/map/mapcolordialog.cpp b/sdrgui/gui/colordialog.cpp similarity index 88% rename from plugins/feature/map/mapcolordialog.cpp rename to sdrgui/gui/colordialog.cpp index 46e2d6bf2..8918eb63a 100644 --- a/plugins/feature/map/mapcolordialog.cpp +++ b/sdrgui/gui/colordialog.cpp @@ -19,10 +19,11 @@ #include #include #include +#include -#include "mapcolordialog.h" +#include "colordialog.h" -MapColorDialog::MapColorDialog(const QColor &initial, QWidget *parent) : +ColorDialog::ColorDialog(const QColor &initial, QWidget *parent) : QDialog(parent) { m_colorDialog = new QColorDialog(); @@ -41,30 +42,30 @@ MapColorDialog::MapColorDialog(const QColor &initial, QWidget *parent) : h->addWidget(m_okButton); v->addLayout(h); - connect(m_noColorButton, &QPushButton::clicked, this, &MapColorDialog::noColorClicked); + connect(m_noColorButton, &QPushButton::clicked, this, &ColorDialog::noColorClicked); connect(m_cancelButton, &QPushButton::clicked, this, &QDialog::reject); connect(m_okButton, &QPushButton::clicked, this, &QDialog::accept); m_noColorSelected = false; } -QColor MapColorDialog::selectedColor() const +QColor ColorDialog::selectedColor() const { return m_colorDialog->selectedColor(); } -bool MapColorDialog::noColorSelected() const +bool ColorDialog::noColorSelected() const { return m_noColorSelected; } -void MapColorDialog::accept() +void ColorDialog::accept() { m_colorDialog->accept(); QDialog::accept(); } -void MapColorDialog::noColorClicked() +void ColorDialog::noColorClicked() { m_noColorSelected = true; accept(); diff --git a/plugins/feature/map/mapcolordialog.h b/sdrgui/gui/colordialog.h similarity index 73% rename from plugins/feature/map/mapcolordialog.h rename to sdrgui/gui/colordialog.h index a4615e354..c4dc3874b 100644 --- a/plugins/feature/map/mapcolordialog.h +++ b/sdrgui/gui/colordialog.h @@ -1,8 +1,5 @@ /////////////////////////////////////////////////////////////////////////////////// -// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // -// written by Christian Daniel // -// Copyright (C) 2015-2019 Edouard Griffiths, F4EXB // -// Copyright (C) 2021-2022 Jon Beniston, M7RCE // +// Copyright (C) 2023-2024 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 // @@ -18,16 +15,22 @@ // along with this program. If not, see . // /////////////////////////////////////////////////////////////////////////////////// -#ifndef INCLUDE_FEATURE_MAPCOLORDIALOG_H -#define INCLUDE_FEATURE_MAPCOLORDIALOG_H +#ifndef INCLUDE_GUI_COLORDIALOG_H +#define INCLUDE_GUI_COLORDIALOG_H -#include +#include +#include -class MapColorDialog : public QDialog { +#include "export.h" + +class QColorDialog; +class QPushButton; + +class SDRGUI_API ColorDialog : public QDialog { Q_OBJECT public: - explicit MapColorDialog(const QColor &initial, QWidget *parent = nullptr); + explicit ColorDialog(const QColor &initial, QWidget *parent = nullptr); QColor selectedColor() const; bool noColorSelected() const; @@ -45,4 +48,4 @@ private: bool m_noColorSelected; }; -#endif // INCLUDE_FEATURE_MAPCOLORDIALOG_H +#endif // INCLUDE_GUI_COLORDIALOG_H diff --git a/sdrgui/gui/tablecolorchooser.cpp b/sdrgui/gui/tablecolorchooser.cpp new file mode 100644 index 000000000..ada6ddaea --- /dev/null +++ b/sdrgui/gui/tablecolorchooser.cpp @@ -0,0 +1,81 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023-2024 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 "tablecolorchooser.h" +#include "colordialog.h" + +static QString rgbToColor(quint32 rgb) +{ + QColor color = QColor::fromRgba(rgb); + return QString("%1,%2,%3").arg(color.red()).arg(color.green()).arg(color.blue()); +} + +static QString backgroundCSS(quint32 rgb) +{ + // Must specify a border, otherwise we end up with a gradient instead of solid background + return QString("QToolButton { background-color: rgb(%1); border: none; }").arg(rgbToColor(rgb)); +} + +static QString noColorCSS() +{ + return "QToolButton { background-color: black; border: none; }"; +} + +TableColorChooser::TableColorChooser(QTableWidget *table, int row, int col, bool noColor, quint32 color) : + m_noColor(noColor), + m_color(color) +{ + m_colorButton = new QToolButton(table); + m_colorButton->setFixedSize(22, 22); + if (!m_noColor) + { + m_colorButton->setStyleSheet(backgroundCSS(m_color)); + } + else + { + m_colorButton->setStyleSheet(noColorCSS()); + m_colorButton->setText("-"); + } + table->setCellWidget(row, col, m_colorButton); + connect(m_colorButton, &QToolButton::clicked, this, &TableColorChooser::on_color_clicked); +} + +void TableColorChooser::on_color_clicked() +{ + ColorDialog dialog(QColor::fromRgba(m_color), m_colorButton); + if (dialog.exec() == QDialog::Accepted) + { + m_noColor = dialog.noColorSelected(); + if (!m_noColor) + { + m_colorButton->setText(""); + m_color = dialog.selectedColor().rgba(); + m_colorButton->setStyleSheet(backgroundCSS(m_color)); + } + else + { + m_colorButton->setText("-"); + m_colorButton->setStyleSheet(noColorCSS()); + } + } +} diff --git a/sdrgui/gui/tablecolorchooser.h b/sdrgui/gui/tablecolorchooser.h new file mode 100644 index 000000000..aea6cfceb --- /dev/null +++ b/sdrgui/gui/tablecolorchooser.h @@ -0,0 +1,48 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023-2024 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_GUI_TABLECOLORCHOOSER_H +#define INCLUDE_GUI_TABLECOLORCHOOSER_H + +#include + +#include "export.h" + +class QTableWidget; +class QToolButton; + +// An widget for use in tables, that displays a color, and when clicked, opens a ColorDialog, allowing the user to select a color +class SDRGUI_API TableColorChooser : public QObject { + Q_OBJECT +public: + + TableColorChooser(QTableWidget *table, int row, int col, bool noColor, quint32 color); + +public slots: + void on_color_clicked(); + +private: + QToolButton *m_colorButton; + +public: + // Have copies of settings, so we don't change unless main dialog is accepted + bool m_noColor; + quint32 m_color; + +}; + +#endif // INCLUDE_GUI_TABLECOLORDIALOG_H diff --git a/sdrgui/gui/wrappingdatetimeedit.cpp b/sdrgui/gui/wrappingdatetimeedit.cpp index 0626bebd3..eac441e43 100644 --- a/sdrgui/gui/wrappingdatetimeedit.cpp +++ b/sdrgui/gui/wrappingdatetimeedit.cpp @@ -29,7 +29,11 @@ WrappingDateTimeEdit::WrappingDateTimeEdit(QWidget *parent) : void WrappingDateTimeEdit::stepBy(int steps) { - if (currentSection() == QDateTimeEdit::MonthSection) + if (currentSection() == QDateTimeEdit::YearSection) + { + clipAndSetDate(date().addYears(steps)); + } + else if (currentSection() == QDateTimeEdit::MonthSection) { clipAndSetDate(date().addMonths(steps)); } diff --git a/swagger/sdrangel/api/swagger/include/FeatureSettings.yaml b/swagger/sdrangel/api/swagger/include/FeatureSettings.yaml index 96929de07..c33b9228d 100644 --- a/swagger/sdrangel/api/swagger/include/FeatureSettings.yaml +++ b/swagger/sdrangel/api/swagger/include/FeatureSettings.yaml @@ -41,6 +41,8 @@ FeatureSettings: $ref: "http://swgserver:8081/api/swagger/include/RigCtlServer.yaml#/RigCtlServerSettings" SatelliteTrackerSettings: $ref: "http://swgserver:8081/api/swagger/include/SatelliteTracker.yaml#/SatelliteTrackerSettings" + SIDSettings: + $ref: "http://swgserver:8081/api/swagger/include/SID.yaml#/SIDSettings" SimplePTTSettings: $ref: "http://swgserver:8081/api/swagger/include/SimplePTT.yaml#/SimplePTTSettings" SkyMapSettings: diff --git a/swagger/sdrangel/api/swagger/include/KiwiSDR.yaml b/swagger/sdrangel/api/swagger/include/KiwiSDR.yaml index d02981dbe..8042a9fc5 100644 --- a/swagger/sdrangel/api/swagger/include/KiwiSDR.yaml +++ b/swagger/sdrangel/api/swagger/include/KiwiSDR.yaml @@ -30,3 +30,12 @@ KiwiSDRReport: status: description: 0 for Idle, 1 for Connecting, 2 for Connected, 3 for Error, 4 for Disconnected type: integer + latitude: + type: number + format: float + longitude: + type: number + format: float + altitude: + type: number + format: float diff --git a/swagger/sdrangel/api/swagger/include/Map.yaml b/swagger/sdrangel/api/swagger/include/Map.yaml index fc6d6f22d..c011d0c27 100644 --- a/swagger/sdrangel/api/swagger/include/Map.yaml +++ b/swagger/sdrangel/api/swagger/include/Map.yaml @@ -156,6 +156,9 @@ MapItem: description: "Extruded height (from surface) for polygons" type: number format: float + availableFrom: + description: "Date and time at which the item should first appear on 3D map" + type: string availableUntil: description: "Date and time until after which this item should no longer appear on 3D map" type: string diff --git a/swagger/sdrangel/api/swagger/include/SID.yaml b/swagger/sdrangel/api/swagger/include/SID.yaml new file mode 100644 index 000000000..6bd0d7bab --- /dev/null +++ b/swagger/sdrangel/api/swagger/include/SID.yaml @@ -0,0 +1,31 @@ +SIDSettings: + description: "SID settings" + properties: + period: + type: number + format: float + autosave: + type: integer + autoload: + type: integer + filename: + type: integer + autosavePeriod: + type: integer + 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/code/html2/index.html b/swagger/sdrangel/code/html2/index.html index db44689f9..038c51178 100644 --- a/swagger/sdrangel/code/html2/index.html +++ b/swagger/sdrangel/code/html2/index.html @@ -6684,6 +6684,9 @@ margin-bottom: 20px; "SatelliteTrackerSettings" : { "$ref" : "#/definitions/SatelliteTrackerSettings" }, + "SIDSettings" : { + "$ref" : "#/definitions/SIDSettings" + }, "SimplePTTSettings" : { "$ref" : "#/definitions/SimplePTTSettings" }, @@ -8555,6 +8558,18 @@ margin-bottom: 20px; "status" : { "type" : "integer", "description" : "0 for Idle, 1 for Connecting, 2 for Connected, 3 for Error, 4 for Disconnected" + }, + "latitude" : { + "type" : "number", + "format" : "float" + }, + "longitude" : { + "type" : "number", + "format" : "float" + }, + "altitude" : { + "type" : "number", + "format" : "float" } }, "description" : "KiwiSDR" @@ -10050,6 +10065,10 @@ margin-bottom: 20px; "format" : "float", "description" : "Extruded height (from surface) for polygons" }, + "availableFrom" : { + "type" : "string", + "description" : "Date and time at which the item should first appear on 3D map" + }, "availableUntil" : { "type" : "string", "description" : "Date and time until after which this item should no longer appear on 3D map" @@ -10210,6 +10229,10 @@ margin-bottom: 20px; "format" : "float", "description" : "Extruded height (from surface) for polygons" }, + "availableFrom" : { + "type" : "string", + "description" : "Date and time at which the item should first appear on 3D map" + }, "availableUntil" : { "type" : "string", "description" : "Date and time until after which this item should no longer appear on 3D map" @@ -13761,6 +13784,52 @@ margin-bottom: 20px; } }, "description" : "SDRplayV3" +}; + defs.SIDSettings = { + "properties" : { + "period" : { + "type" : "number", + "format" : "float" + }, + "autosave" : { + "type" : "integer" + }, + "autoload" : { + "type" : "integer" + }, + "filename" : { + "type" : "integer" + }, + "autosavePeriod" : { + "type" : "integer" + }, + "title" : { + "type" : "string" + }, + "rgbColor" : { + "type" : "integer" + }, + "useReverseAPI" : { + "type" : "integer", + "description" : "Synchronize with reverse API (1 for yes, 0 for no)" + }, + "reverseAPIAddress" : { + "type" : "string" + }, + "reverseAPIPort" : { + "type" : "integer" + }, + "reverseAPIFeatureSetIndex" : { + "type" : "integer" + }, + "reverseAPIFeatureIndex" : { + "type" : "integer" + }, + "rollupState" : { + "$ref" : "#/definitions/RollupState" + } + }, + "description" : "SID settings" }; defs.SSBDemodReport = { "properties" : { @@ -58865,7 +58934,7 @@ except ApiException as e:
- Generated 2024-03-13T16:45:40.518+01:00 + Generated 2024-04-04T16:23:36.765+02:00
diff --git a/swagger/sdrangel/code/qt5/client/SWGFeatureSettings.cpp b/swagger/sdrangel/code/qt5/client/SWGFeatureSettings.cpp index 8e007f1ca..608503174 100644 --- a/swagger/sdrangel/code/qt5/client/SWGFeatureSettings.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGFeatureSettings.cpp @@ -62,6 +62,8 @@ SWGFeatureSettings::SWGFeatureSettings() { m_rig_ctl_server_settings_isSet = false; satellite_tracker_settings = nullptr; m_satellite_tracker_settings_isSet = false; + sid_settings = nullptr; + m_sid_settings_isSet = false; simple_ptt_settings = nullptr; m_simple_ptt_settings_isSet = false; sky_map_settings = nullptr; @@ -112,6 +114,8 @@ SWGFeatureSettings::init() { m_rig_ctl_server_settings_isSet = false; satellite_tracker_settings = new SWGSatelliteTrackerSettings(); m_satellite_tracker_settings_isSet = false; + sid_settings = new SWGSIDSettings(); + m_sid_settings_isSet = false; simple_ptt_settings = new SWGSimplePTTSettings(); m_simple_ptt_settings_isSet = false; sky_map_settings = new SWGSkyMapSettings(); @@ -171,6 +175,9 @@ SWGFeatureSettings::cleanup() { if(satellite_tracker_settings != nullptr) { delete satellite_tracker_settings; } + if(sid_settings != nullptr) { + delete sid_settings; + } if(simple_ptt_settings != nullptr) { delete simple_ptt_settings; } @@ -230,6 +237,8 @@ SWGFeatureSettings::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&satellite_tracker_settings, pJson["SatelliteTrackerSettings"], "SWGSatelliteTrackerSettings", "SWGSatelliteTrackerSettings"); + ::SWGSDRangel::setValue(&sid_settings, pJson["SIDSettings"], "SWGSIDSettings", "SWGSIDSettings"); + ::SWGSDRangel::setValue(&simple_ptt_settings, pJson["SimplePTTSettings"], "SWGSimplePTTSettings", "SWGSimplePTTSettings"); ::SWGSDRangel::setValue(&sky_map_settings, pJson["SkyMapSettings"], "SWGSkyMapSettings", "SWGSkyMapSettings"); @@ -305,6 +314,9 @@ SWGFeatureSettings::asJsonObject() { if((satellite_tracker_settings != nullptr) && (satellite_tracker_settings->isSet())){ toJsonValue(QString("SatelliteTrackerSettings"), satellite_tracker_settings, obj, QString("SWGSatelliteTrackerSettings")); } + if((sid_settings != nullptr) && (sid_settings->isSet())){ + toJsonValue(QString("SIDSettings"), sid_settings, obj, QString("SWGSIDSettings")); + } if((simple_ptt_settings != nullptr) && (simple_ptt_settings->isSet())){ toJsonValue(QString("SimplePTTSettings"), simple_ptt_settings, obj, QString("SWGSimplePTTSettings")); } @@ -491,6 +503,16 @@ SWGFeatureSettings::setSatelliteTrackerSettings(SWGSatelliteTrackerSettings* sat this->m_satellite_tracker_settings_isSet = true; } +SWGSIDSettings* +SWGFeatureSettings::getSidSettings() { + return sid_settings; +} +void +SWGFeatureSettings::setSidSettings(SWGSIDSettings* sid_settings) { + this->sid_settings = sid_settings; + this->m_sid_settings_isSet = true; +} + SWGSimplePTTSettings* SWGFeatureSettings::getSimplePttSettings() { return simple_ptt_settings; @@ -587,6 +609,9 @@ SWGFeatureSettings::isSet(){ if(satellite_tracker_settings && satellite_tracker_settings->isSet()){ isObjectUpdated = true; break; } + if(sid_settings && sid_settings->isSet()){ + isObjectUpdated = true; break; + } if(simple_ptt_settings && simple_ptt_settings->isSet()){ isObjectUpdated = true; break; } diff --git a/swagger/sdrangel/code/qt5/client/SWGFeatureSettings.h b/swagger/sdrangel/code/qt5/client/SWGFeatureSettings.h index 9c13ff370..bd29350f5 100644 --- a/swagger/sdrangel/code/qt5/client/SWGFeatureSettings.h +++ b/swagger/sdrangel/code/qt5/client/SWGFeatureSettings.h @@ -35,6 +35,7 @@ #include "SWGPERTesterSettings.h" #include "SWGRadiosondeSettings.h" #include "SWGRigCtlServerSettings.h" +#include "SWGSIDSettings.h" #include "SWGSatelliteTrackerSettings.h" #include "SWGSimplePTTSettings.h" #include "SWGSkyMapSettings.h" @@ -111,6 +112,9 @@ public: SWGSatelliteTrackerSettings* getSatelliteTrackerSettings(); void setSatelliteTrackerSettings(SWGSatelliteTrackerSettings* satellite_tracker_settings); + SWGSIDSettings* getSidSettings(); + void setSidSettings(SWGSIDSettings* sid_settings); + SWGSimplePTTSettings* getSimplePttSettings(); void setSimplePttSettings(SWGSimplePTTSettings* simple_ptt_settings); @@ -178,6 +182,9 @@ private: SWGSatelliteTrackerSettings* satellite_tracker_settings; bool m_satellite_tracker_settings_isSet; + SWGSIDSettings* sid_settings; + bool m_sid_settings_isSet; + SWGSimplePTTSettings* simple_ptt_settings; bool m_simple_ptt_settings_isSet; diff --git a/swagger/sdrangel/code/qt5/client/SWGKiwiSDRReport.cpp b/swagger/sdrangel/code/qt5/client/SWGKiwiSDRReport.cpp index 2ec8dcd47..e2b62345b 100644 --- a/swagger/sdrangel/code/qt5/client/SWGKiwiSDRReport.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGKiwiSDRReport.cpp @@ -30,6 +30,12 @@ SWGKiwiSDRReport::SWGKiwiSDRReport(QString* json) { SWGKiwiSDRReport::SWGKiwiSDRReport() { status = 0; m_status_isSet = false; + latitude = 0.0f; + m_latitude_isSet = false; + longitude = 0.0f; + m_longitude_isSet = false; + altitude = 0.0f; + m_altitude_isSet = false; } SWGKiwiSDRReport::~SWGKiwiSDRReport() { @@ -40,11 +46,20 @@ void SWGKiwiSDRReport::init() { status = 0; m_status_isSet = false; + latitude = 0.0f; + m_latitude_isSet = false; + longitude = 0.0f; + m_longitude_isSet = false; + altitude = 0.0f; + m_altitude_isSet = false; } void SWGKiwiSDRReport::cleanup() { + + + } SWGKiwiSDRReport* @@ -60,6 +75,12 @@ void SWGKiwiSDRReport::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&status, pJson["status"], "qint32", ""); + ::SWGSDRangel::setValue(&latitude, pJson["latitude"], "float", ""); + + ::SWGSDRangel::setValue(&longitude, pJson["longitude"], "float", ""); + + ::SWGSDRangel::setValue(&altitude, pJson["altitude"], "float", ""); + } QString @@ -79,6 +100,15 @@ SWGKiwiSDRReport::asJsonObject() { if(m_status_isSet){ obj->insert("status", QJsonValue(status)); } + if(m_latitude_isSet){ + obj->insert("latitude", QJsonValue(latitude)); + } + if(m_longitude_isSet){ + obj->insert("longitude", QJsonValue(longitude)); + } + if(m_altitude_isSet){ + obj->insert("altitude", QJsonValue(altitude)); + } return obj; } @@ -93,6 +123,36 @@ SWGKiwiSDRReport::setStatus(qint32 status) { this->m_status_isSet = true; } +float +SWGKiwiSDRReport::getLatitude() { + return latitude; +} +void +SWGKiwiSDRReport::setLatitude(float latitude) { + this->latitude = latitude; + this->m_latitude_isSet = true; +} + +float +SWGKiwiSDRReport::getLongitude() { + return longitude; +} +void +SWGKiwiSDRReport::setLongitude(float longitude) { + this->longitude = longitude; + this->m_longitude_isSet = true; +} + +float +SWGKiwiSDRReport::getAltitude() { + return altitude; +} +void +SWGKiwiSDRReport::setAltitude(float altitude) { + this->altitude = altitude; + this->m_altitude_isSet = true; +} + bool SWGKiwiSDRReport::isSet(){ @@ -101,6 +161,15 @@ SWGKiwiSDRReport::isSet(){ if(m_status_isSet){ isObjectUpdated = true; break; } + if(m_latitude_isSet){ + isObjectUpdated = true; break; + } + if(m_longitude_isSet){ + isObjectUpdated = true; break; + } + if(m_altitude_isSet){ + isObjectUpdated = true; break; + } }while(false); return isObjectUpdated; } diff --git a/swagger/sdrangel/code/qt5/client/SWGKiwiSDRReport.h b/swagger/sdrangel/code/qt5/client/SWGKiwiSDRReport.h index c4c119470..340e46209 100644 --- a/swagger/sdrangel/code/qt5/client/SWGKiwiSDRReport.h +++ b/swagger/sdrangel/code/qt5/client/SWGKiwiSDRReport.h @@ -44,6 +44,15 @@ public: qint32 getStatus(); void setStatus(qint32 status); + float getLatitude(); + void setLatitude(float latitude); + + float getLongitude(); + void setLongitude(float longitude); + + float getAltitude(); + void setAltitude(float altitude); + virtual bool isSet() override; @@ -51,6 +60,15 @@ private: qint32 status; bool m_status_isSet; + float latitude; + bool m_latitude_isSet; + + float longitude; + bool m_longitude_isSet; + + float altitude; + bool m_altitude_isSet; + }; } diff --git a/swagger/sdrangel/code/qt5/client/SWGMapItem.cpp b/swagger/sdrangel/code/qt5/client/SWGMapItem.cpp index 439be987c..b8f3aec32 100644 --- a/swagger/sdrangel/code/qt5/client/SWGMapItem.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGMapItem.cpp @@ -88,6 +88,8 @@ SWGMapItem::SWGMapItem() { m_coordinates_isSet = false; extruded_height = 0.0f; m_extruded_height_isSet = false; + available_from = nullptr; + m_available_from_isSet = false; available_until = nullptr; m_available_until_isSet = false; color_valid = 0; @@ -162,6 +164,8 @@ SWGMapItem::init() { m_coordinates_isSet = false; extruded_height = 0.0f; m_extruded_height_isSet = false; + available_from = new QString(""); + m_available_from_isSet = false; available_until = new QString(""); m_available_until_isSet = false; color_valid = 0; @@ -240,6 +244,9 @@ SWGMapItem::cleanup() { delete coordinates; } + if(available_from != nullptr) { + delete available_from; + } if(available_until != nullptr) { delete available_until; } @@ -318,6 +325,8 @@ SWGMapItem::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&coordinates, pJson["coordinates"], "QList", "SWGMapCoordinate"); ::SWGSDRangel::setValue(&extruded_height, pJson["extrudedHeight"], "float", ""); + ::SWGSDRangel::setValue(&available_from, pJson["availableFrom"], "QString", "QString"); + ::SWGSDRangel::setValue(&available_until, pJson["availableUntil"], "QString", "QString"); ::SWGSDRangel::setValue(&color_valid, pJson["colorValid"], "qint32", ""); @@ -430,6 +439,9 @@ SWGMapItem::asJsonObject() { if(m_extruded_height_isSet){ obj->insert("extrudedHeight", QJsonValue(extruded_height)); } + if(available_from != nullptr && *available_from != QString("")){ + toJsonValue(QString("availableFrom"), available_from, obj, QString("QString")); + } if(available_until != nullptr && *available_until != QString("")){ toJsonValue(QString("availableUntil"), available_until, obj, QString("QString")); } @@ -743,6 +755,16 @@ SWGMapItem::setExtrudedHeight(float extruded_height) { this->m_extruded_height_isSet = true; } +QString* +SWGMapItem::getAvailableFrom() { + return available_from; +} +void +SWGMapItem::setAvailableFrom(QString* available_from) { + this->available_from = available_from; + this->m_available_from_isSet = true; +} + QString* SWGMapItem::getAvailableUntil() { return available_until; @@ -868,6 +890,9 @@ SWGMapItem::isSet(){ if(m_extruded_height_isSet){ isObjectUpdated = true; break; } + if(available_from && *available_from != QString("")){ + isObjectUpdated = true; break; + } if(available_until && *available_until != QString("")){ isObjectUpdated = true; break; } diff --git a/swagger/sdrangel/code/qt5/client/SWGMapItem.h b/swagger/sdrangel/code/qt5/client/SWGMapItem.h index 99e36836d..fa0aeab1f 100644 --- a/swagger/sdrangel/code/qt5/client/SWGMapItem.h +++ b/swagger/sdrangel/code/qt5/client/SWGMapItem.h @@ -135,6 +135,9 @@ public: float getExtrudedHeight(); void setExtrudedHeight(float extruded_height); + QString* getAvailableFrom(); + void setAvailableFrom(QString* available_from); + QString* getAvailableUntil(); void setAvailableUntil(QString* available_until); @@ -238,6 +241,9 @@ private: float extruded_height; bool m_extruded_height_isSet; + QString* available_from; + bool m_available_from_isSet; + QString* available_until; bool m_available_until_isSet; diff --git a/swagger/sdrangel/code/qt5/client/SWGMapItem_2.cpp b/swagger/sdrangel/code/qt5/client/SWGMapItem_2.cpp index 5dfa91130..cc7db1fc0 100644 --- a/swagger/sdrangel/code/qt5/client/SWGMapItem_2.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGMapItem_2.cpp @@ -88,6 +88,8 @@ SWGMapItem_2::SWGMapItem_2() { m_coordinates_isSet = false; extruded_height = 0.0f; m_extruded_height_isSet = false; + available_from = nullptr; + m_available_from_isSet = false; available_until = nullptr; m_available_until_isSet = false; color_valid = 0; @@ -162,6 +164,8 @@ SWGMapItem_2::init() { m_coordinates_isSet = false; extruded_height = 0.0f; m_extruded_height_isSet = false; + available_from = new QString(""); + m_available_from_isSet = false; available_until = new QString(""); m_available_until_isSet = false; color_valid = 0; @@ -240,6 +244,9 @@ SWGMapItem_2::cleanup() { delete coordinates; } + if(available_from != nullptr) { + delete available_from; + } if(available_until != nullptr) { delete available_until; } @@ -318,6 +325,8 @@ SWGMapItem_2::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&coordinates, pJson["coordinates"], "QList", "SWGMapCoordinate"); ::SWGSDRangel::setValue(&extruded_height, pJson["extrudedHeight"], "float", ""); + ::SWGSDRangel::setValue(&available_from, pJson["availableFrom"], "QString", "QString"); + ::SWGSDRangel::setValue(&available_until, pJson["availableUntil"], "QString", "QString"); ::SWGSDRangel::setValue(&color_valid, pJson["colorValid"], "qint32", ""); @@ -430,6 +439,9 @@ SWGMapItem_2::asJsonObject() { if(m_extruded_height_isSet){ obj->insert("extrudedHeight", QJsonValue(extruded_height)); } + if(available_from != nullptr && *available_from != QString("")){ + toJsonValue(QString("availableFrom"), available_from, obj, QString("QString")); + } if(available_until != nullptr && *available_until != QString("")){ toJsonValue(QString("availableUntil"), available_until, obj, QString("QString")); } @@ -743,6 +755,16 @@ SWGMapItem_2::setExtrudedHeight(float extruded_height) { this->m_extruded_height_isSet = true; } +QString* +SWGMapItem_2::getAvailableFrom() { + return available_from; +} +void +SWGMapItem_2::setAvailableFrom(QString* available_from) { + this->available_from = available_from; + this->m_available_from_isSet = true; +} + QString* SWGMapItem_2::getAvailableUntil() { return available_until; @@ -868,6 +890,9 @@ SWGMapItem_2::isSet(){ if(m_extruded_height_isSet){ isObjectUpdated = true; break; } + if(available_from && *available_from != QString("")){ + isObjectUpdated = true; break; + } if(available_until && *available_until != QString("")){ isObjectUpdated = true; break; } diff --git a/swagger/sdrangel/code/qt5/client/SWGMapItem_2.h b/swagger/sdrangel/code/qt5/client/SWGMapItem_2.h index e5c7a6722..68c8f6daa 100644 --- a/swagger/sdrangel/code/qt5/client/SWGMapItem_2.h +++ b/swagger/sdrangel/code/qt5/client/SWGMapItem_2.h @@ -135,6 +135,9 @@ public: float getExtrudedHeight(); void setExtrudedHeight(float extruded_height); + QString* getAvailableFrom(); + void setAvailableFrom(QString* available_from); + QString* getAvailableUntil(); void setAvailableUntil(QString* available_until); @@ -238,6 +241,9 @@ private: float extruded_height; bool m_extruded_height_isSet; + QString* available_from; + bool m_available_from_isSet; + QString* available_until; bool m_available_until_isSet; diff --git a/swagger/sdrangel/code/qt5/client/SWGModelFactory.h b/swagger/sdrangel/code/qt5/client/SWGModelFactory.h index 8342dd721..d961907a9 100644 --- a/swagger/sdrangel/code/qt5/client/SWGModelFactory.h +++ b/swagger/sdrangel/code/qt5/client/SWGModelFactory.h @@ -301,6 +301,7 @@ #include "SWGSDRPlaySettings.h" #include "SWGSDRPlayV3Report.h" #include "SWGSDRPlayV3Settings.h" +#include "SWGSIDSettings.h" #include "SWGSSBDemodReport.h" #include "SWGSSBDemodSettings.h" #include "SWGSSBModReport.h" @@ -1819,6 +1820,11 @@ namespace SWGSDRangel { obj->init(); return obj; } + if(QString("SWGSIDSettings").compare(type) == 0) { + SWGSIDSettings *obj = new SWGSIDSettings(); + obj->init(); + return obj; + } if(QString("SWGSSBDemodReport").compare(type) == 0) { SWGSSBDemodReport *obj = new SWGSSBDemodReport(); obj->init(); diff --git a/swagger/sdrangel/code/qt5/client/SWGSIDSettings.cpp b/swagger/sdrangel/code/qt5/client/SWGSIDSettings.cpp new file mode 100644 index 000000000..332ebfabf --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGSIDSettings.cpp @@ -0,0 +1,390 @@ +/** + * 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: 7.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 "SWGSIDSettings.h" + +#include "SWGHelpers.h" + +#include +#include +#include +#include + +namespace SWGSDRangel { + +SWGSIDSettings::SWGSIDSettings(QString* json) { + init(); + this->fromJson(*json); +} + +SWGSIDSettings::SWGSIDSettings() { + period = 0.0f; + m_period_isSet = false; + autosave = 0; + m_autosave_isSet = false; + autoload = 0; + m_autoload_isSet = false; + filename = 0; + m_filename_isSet = false; + autosave_period = 0; + m_autosave_period_isSet = false; + 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; +} + +SWGSIDSettings::~SWGSIDSettings() { + this->cleanup(); +} + +void +SWGSIDSettings::init() { + period = 0.0f; + m_period_isSet = false; + autosave = 0; + m_autosave_isSet = false; + autoload = 0; + m_autoload_isSet = false; + filename = 0; + m_filename_isSet = false; + autosave_period = 0; + m_autosave_period_isSet = false; + 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 +SWGSIDSettings::cleanup() { + + + + + + if(title != nullptr) { + delete title; + } + + + if(reverse_api_address != nullptr) { + delete reverse_api_address; + } + + + + if(rollup_state != nullptr) { + delete rollup_state; + } +} + +SWGSIDSettings* +SWGSIDSettings::fromJson(QString &json) { + QByteArray array (json.toStdString().c_str()); + QJsonDocument doc = QJsonDocument::fromJson(array); + QJsonObject jsonObject = doc.object(); + this->fromJsonObject(jsonObject); + return this; +} + +void +SWGSIDSettings::fromJsonObject(QJsonObject &pJson) { + ::SWGSDRangel::setValue(&period, pJson["period"], "float", ""); + + ::SWGSDRangel::setValue(&autosave, pJson["autosave"], "qint32", ""); + + ::SWGSDRangel::setValue(&autoload, pJson["autoload"], "qint32", ""); + + ::SWGSDRangel::setValue(&filename, pJson["filename"], "qint32", ""); + + ::SWGSDRangel::setValue(&autosave_period, pJson["autosavePeriod"], "qint32", ""); + + ::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 +SWGSIDSettings::asJson () +{ + QJsonObject* obj = this->asJsonObject(); + + QJsonDocument doc(*obj); + QByteArray bytes = doc.toJson(); + delete obj; + return QString(bytes); +} + +QJsonObject* +SWGSIDSettings::asJsonObject() { + QJsonObject* obj = new QJsonObject(); + if(m_period_isSet){ + obj->insert("period", QJsonValue(period)); + } + if(m_autosave_isSet){ + obj->insert("autosave", QJsonValue(autosave)); + } + if(m_autoload_isSet){ + obj->insert("autoload", QJsonValue(autoload)); + } + if(m_filename_isSet){ + obj->insert("filename", QJsonValue(filename)); + } + if(m_autosave_period_isSet){ + obj->insert("autosavePeriod", QJsonValue(autosave_period)); + } + 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; +} + +float +SWGSIDSettings::getPeriod() { + return period; +} +void +SWGSIDSettings::setPeriod(float period) { + this->period = period; + this->m_period_isSet = true; +} + +qint32 +SWGSIDSettings::getAutosave() { + return autosave; +} +void +SWGSIDSettings::setAutosave(qint32 autosave) { + this->autosave = autosave; + this->m_autosave_isSet = true; +} + +qint32 +SWGSIDSettings::getAutoload() { + return autoload; +} +void +SWGSIDSettings::setAutoload(qint32 autoload) { + this->autoload = autoload; + this->m_autoload_isSet = true; +} + +qint32 +SWGSIDSettings::getFilename() { + return filename; +} +void +SWGSIDSettings::setFilename(qint32 filename) { + this->filename = filename; + this->m_filename_isSet = true; +} + +qint32 +SWGSIDSettings::getAutosavePeriod() { + return autosave_period; +} +void +SWGSIDSettings::setAutosavePeriod(qint32 autosave_period) { + this->autosave_period = autosave_period; + this->m_autosave_period_isSet = true; +} + +QString* +SWGSIDSettings::getTitle() { + return title; +} +void +SWGSIDSettings::setTitle(QString* title) { + this->title = title; + this->m_title_isSet = true; +} + +qint32 +SWGSIDSettings::getRgbColor() { + return rgb_color; +} +void +SWGSIDSettings::setRgbColor(qint32 rgb_color) { + this->rgb_color = rgb_color; + this->m_rgb_color_isSet = true; +} + +qint32 +SWGSIDSettings::getUseReverseApi() { + return use_reverse_api; +} +void +SWGSIDSettings::setUseReverseApi(qint32 use_reverse_api) { + this->use_reverse_api = use_reverse_api; + this->m_use_reverse_api_isSet = true; +} + +QString* +SWGSIDSettings::getReverseApiAddress() { + return reverse_api_address; +} +void +SWGSIDSettings::setReverseApiAddress(QString* reverse_api_address) { + this->reverse_api_address = reverse_api_address; + this->m_reverse_api_address_isSet = true; +} + +qint32 +SWGSIDSettings::getReverseApiPort() { + return reverse_api_port; +} +void +SWGSIDSettings::setReverseApiPort(qint32 reverse_api_port) { + this->reverse_api_port = reverse_api_port; + this->m_reverse_api_port_isSet = true; +} + +qint32 +SWGSIDSettings::getReverseApiFeatureSetIndex() { + return reverse_api_feature_set_index; +} +void +SWGSIDSettings::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 +SWGSIDSettings::getReverseApiFeatureIndex() { + return reverse_api_feature_index; +} +void +SWGSIDSettings::setReverseApiFeatureIndex(qint32 reverse_api_feature_index) { + this->reverse_api_feature_index = reverse_api_feature_index; + this->m_reverse_api_feature_index_isSet = true; +} + +SWGRollupState* +SWGSIDSettings::getRollupState() { + return rollup_state; +} +void +SWGSIDSettings::setRollupState(SWGRollupState* rollup_state) { + this->rollup_state = rollup_state; + this->m_rollup_state_isSet = true; +} + + +bool +SWGSIDSettings::isSet(){ + bool isObjectUpdated = false; + do{ + if(m_period_isSet){ + isObjectUpdated = true; break; + } + if(m_autosave_isSet){ + isObjectUpdated = true; break; + } + if(m_autoload_isSet){ + isObjectUpdated = true; break; + } + if(m_filename_isSet){ + isObjectUpdated = true; break; + } + if(m_autosave_period_isSet){ + isObjectUpdated = true; break; + } + 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/SWGSIDSettings.h b/swagger/sdrangel/code/qt5/client/SWGSIDSettings.h new file mode 100644 index 000000000..e56ed7ca3 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGSIDSettings.h @@ -0,0 +1,132 @@ +/** + * 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: 7.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. + */ + +/* + * SWGSIDSettings.h + * + * SID settings + */ + +#ifndef SWGSIDSettings_H_ +#define SWGSIDSettings_H_ + +#include + + +#include "SWGRollupState.h" +#include + +#include "SWGObject.h" +#include "export.h" + +namespace SWGSDRangel { + +class SWG_API SWGSIDSettings: public SWGObject { +public: + SWGSIDSettings(); + SWGSIDSettings(QString* json); + virtual ~SWGSIDSettings(); + void init(); + void cleanup(); + + virtual QString asJson () override; + virtual QJsonObject* asJsonObject() override; + virtual void fromJsonObject(QJsonObject &json) override; + virtual SWGSIDSettings* fromJson(QString &jsonString) override; + + float getPeriod(); + void setPeriod(float period); + + qint32 getAutosave(); + void setAutosave(qint32 autosave); + + qint32 getAutoload(); + void setAutoload(qint32 autoload); + + qint32 getFilename(); + void setFilename(qint32 filename); + + qint32 getAutosavePeriod(); + void setAutosavePeriod(qint32 autosave_period); + + 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: + float period; + bool m_period_isSet; + + qint32 autosave; + bool m_autosave_isSet; + + qint32 autoload; + bool m_autoload_isSet; + + qint32 filename; + bool m_filename_isSet; + + qint32 autosave_period; + bool m_autosave_period_isSet; + + 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 /* SWGSIDSettings_H_ */