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/doc/img/SDI_plugin.jpg b/doc/img/SDI_plugin.jpg new file mode 100644 index 000000000..7ce282b74 Binary files /dev/null and b/doc/img/SDI_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_settings.png b/doc/img/SID_plugin_settings.png new file mode 100644 index 000000000..460dfaac2 Binary files /dev/null and b/doc/img/SID_plugin_settings.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/plugins/feature/CMakeLists.txt b/plugins/feature/CMakeLists.txt index 1be829154..83b6b2378 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 (ENABLED_FEATURE_SID) + add_subdirectory(sid) +else() + message(STATUS "Not building SID (ENABLED_FEATURE_SID=${ENABLED_FEATURE_SID})") +endif() 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..aabff327a --- /dev/null +++ b/plugins/feature/sid/readme.md @@ -0,0 +1,252 @@ +

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, to see the corresponding effect on MUF (Maximum Usable Frequency) and foF2 (F2 layer critical frequency). + +![SID feature plugin](../../../doc/img/SID_plugin.jpg) + +

Interface

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

Start/stop

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

Open .csv

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

Save to .csv

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

Save chart to image

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

Clear all data

+ +Press to clear all data. + +

Average

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

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

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

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

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

Display Proton Flux

+ +Check to display 10 MeV and 100 MeV proton flux measurements from the primary GOES satellte 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 are 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. + +

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

Display Solar Flares

+ +Check to display solar flares on the chart as record by the STIX X-ray instrument on the Solar Oribter 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. + +

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

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

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. +- 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. +- Where the chart legend should be positioned. + +

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 visibible in the AIA 131 Å images. + +

Image or Video Selection

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

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

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

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

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

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

Set X-axis to -1 day

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

Set X-axis to +1 day

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

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

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

Min

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

Max

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

Now

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

Date Time

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

Map

+ +Select a Map to link to the SID feature. When a time is selected on the SID charts, the Map feature will have it's time set accordingly. +This allows you, for example, to see the corresponding impact on MUF/foF2. + +

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

+ +On Windows, you may need to install an mp4 codec to view the SDO videos. Try [K-Lite Codecs](https://www.codecguide.com/download_k-lite_codec_pack_basic.htm). + +

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..0bdbccf54 --- /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; } + QString getId() const { return m_id; } + double getMeasurement() const { return m_measurement; } + + static MsgMeasurement* create(QDateTime dateTime, const QString& id, double measurement) { + return new MsgMeasurement(dateTime, id, measurement); + } + + private: + QDateTime m_dateTime; + const QString m_id; + double m_measurement; + + MsgMeasurement(QDateTime dateTime, const QString& id, double measurement) : + Message(), + m_dateTime(dateTime), + m_id(id), + m_measurement(measurement) + {} + }; + + 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..526eb2cff --- /dev/null +++ b/plugins/feature/sid/sidgui.cpp @@ -0,0 +1,2253 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "ui_sidgui.h" +#include "sid.h" +#include "sidgui.h" +#include "sidsettingsdialog.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)) + { + const SIDMain::MsgMeasurement& measurement = (SIDMain::MsgMeasurement&) message; + addMeasurement(measurement.getId(), measurement.getDateTime(), measurement.getMeasurement()); + 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_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(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 + ); + 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(); + 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) +{ + m_settings.m_chartSplitterSizes = ui->chartSplitter->sizes(); + applySetting("chartSplitterSizes"); +} + +void SIDGUI::sdoSplitterMoved(int pos, int 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) + { + qreal closestDistance; + 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) + { + QContextMenuEvent *contextEvent = static_cast(event); + + showContextMenu(contextEvent); + contextEvent->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)) { + 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); + 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->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) && (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 = nullptr; + switch (measurement.m_band) + { + case GOESXRay::XRayData::SHORT: + measurements = &m_xrayShortMeasurements[idx]; + break; + case GOESXRay::XRayData::LONG: + measurements = &m_xrayLongMeasurements[idx]; + 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) +{ + 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) +{ +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) + const QPixmap *currentPixmap = ui->sdoImage->pixmap(); + bool setSize = (currentPixmap == nullptr) || currentPixmap->isNull(); +#else + bool setSize = ui->sdoImage->pixmap().isNull(); +#endif + + 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 codec, such as: K-Lite codedcs."); + } +#else + if (error == QMediaPlayer::FormatError) { + QMessageBox::warning(this, "Video Error", "Unable to play video. Please try installing an mp4 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(); +} + +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::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()); + } + + 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); + } + } + } + + 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(); + } +} + +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..52446c4be --- /dev/null +++ b/plugins/feature/sid/sidgui.h @@ -0,0 +1,311 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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; + + 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 openSkyMap(); + + 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 featuresChanged(const QStringList& renameFrom, const QStringList& renameTo); +}; + +#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..8fcf77445 --- /dev/null +++ b/plugins/feature/sid/sidgui.ui @@ -0,0 +1,767 @@ + + + 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 + + + + + + + + + + + + 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 + + + + + + + + + + + + + + RollupContents + QWidget +
gui/rollupcontents.h
+ 1 +
+ + ButtonSwitch + QToolButton +
gui/buttonswitch.h
+
+ + 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..3de9987d3 --- /dev/null +++ b/plugins/feature/sid/sidsettings.cpp @@ -0,0 +1,650 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "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; +} + +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..38b559b56 --- /dev/null +++ b/plugins/feature/sid/sidsettings.h @@ -0,0 +1,115 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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); + + 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..81fc2289f --- /dev/null +++ b/plugins/feature/sid/sidsettingsdialog.cpp @@ -0,0 +1,214 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "device/deviceset.h" +#include "device/deviceapi.h" +#include "channel/channelwebapiutils.h" +#include "gui/colordialog.h" +#include "gui/tablecolorchooser.h" +#include "maincore.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); + + 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 = m_settings->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_settings->m_channelSettings.append(newSettings); + } + } + + // 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); +} + +// Get channels that have channelPowerDB value in their report +void SIDSettingsDialog::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 (unsigned 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); + } + } + } +} + +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(); + + 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; + } + + 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; + + 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]); + } + } +} diff --git a/plugins/feature/sid/sidsettingsdialog.h b/plugins/feature/sid/sidsettingsdialog.h new file mode 100644 index 000000000..b8a699880 --- /dev/null +++ b/plugins/feature/sid/sidsettingsdialog.h @@ -0,0 +1,63 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_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 getChannels(QStringList& ids, QStringList& titles); + void addColor(const QString& name, QRgb rgb); + +private slots: + void accept(); + void on_browse_clicked(); + +private: + Ui::SIDSettingsDialog* ui; + SIDSettings *m_settings; + QList m_channelColorGUIs; + QList m_colorGUIs; + QFileDialog m_fileDialog; + + enum ChannelsRows { + CHANNELS_COL_ID, + CHANNELS_COL_ENABLED, + CHANNELS_COL_LABEL, + CHANNELS_COL_COLOR + }; + + enum ColorsRows { + COLORS_COL_NAME, + COLORS_COL_COLOR + }; +}; + +#endif // INCLUDE_SIDSETTINGSDIALOG_H diff --git a/plugins/feature/sid/sidsettingsdialog.ui b/plugins/feature/sid/sidsettingsdialog.ui new file mode 100644 index 000000000..e5a9b6d28 --- /dev/null +++ b/plugins/feature/sid/sidsettingsdialog.ui @@ -0,0 +1,338 @@ + + + SIDSettingsDialog + + + + 0 + 0 + 441 + 800 + + + + + Liberation Sans + 9 + + + + APRS Settings + + + + + + Data + + + + + + Channels to record power from: + + + + + + + QAbstractItemView::NoSelection + + + 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 + + + + + + + + + + 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..06fd2280e --- /dev/null +++ b/plugins/feature/sid/sidworker.cpp @@ -0,0 +1,153 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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(); + + 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()) + { + SIDMain::MsgMeasurement *msgToGUI = SIDMain::MsgMeasurement::create(dateTime, channelSettings.m_id, power); + getMessageQueueToGUI()->push(msgToGUI); + } + } + else + { + qDebug() << "SIDWorker::update: Failed to get power for channel " << channelSettings.m_id; + } + } + } + } + else + { + qDebug() << "SIDWorker::update: Malformed channel id: " << channelSettings.m_id; + } + } + } +} 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/sdrbase/channel/channelwebapiutils.cpp b/sdrbase/channel/channelwebapiutils.cpp index e36478de4..ad706b2bd 100644 --- a/sdrbase/channel/channelwebapiutils.cpp +++ b/sdrbase/channel/channelwebapiutils.cpp @@ -1440,13 +1440,13 @@ bool ChannelWebAPIUtils::patchFeatureSetting(unsigned int featureSetIndex, unsig if (httpRC/100 == 2) { - qDebug("ChannelWebAPIUtils::patchFeatureSetting: set feature setting %s to %s OK", qPrintable(setting), value.toVariantList()); + qDebug("ChannelWebAPIUtils::patchFeatureSetting: set feature setting %s OK", qPrintable(setting)); return true; } else { - qWarning("ChannelWebAPIUtils::patchFeatureSetting: set feature setting %s to %s error %d: %s", - qPrintable(setting), value.toVariantList(), httpRC, qPrintable(*errorResponse2.getMessage())); + qWarning("ChannelWebAPIUtils::patchFeatureSetting: set feature setting %s error %d: %s", + qPrintable(setting), httpRC, qPrintable(*errorResponse2.getMessage())); return false; } } 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"},