diff --git a/sdrbase/CMakeLists.txt b/sdrbase/CMakeLists.txt
index 391057dc9..4553695a8 100644
--- a/sdrbase/CMakeLists.txt
+++ b/sdrbase/CMakeLists.txt
@@ -238,7 +238,9 @@ set(sdrbase_SOURCES
util/flightinformation.cpp
util/ft8message.cpp
util/giro.cpp
+ util/goesxray.cpp
util/golay2312.cpp
+ util/grb.cpp
util/httpdownloadmanager.cpp
util/interpolation.cpp
util/kiwisdrlist.cpp
@@ -266,8 +268,10 @@ set(sdrbase_SOURCES
util/samplesourceserializer.cpp
util/simpleserializer.cpp
util/serialutil.cpp
+ util/solardynamicsobservatory.cpp
#util/spinlock.cpp
util/spyserverlist.cpp
+ util/stix.cpp
util/rtty.cpp
util/uid.cpp
util/units.cpp
@@ -487,7 +491,9 @@ set(sdrbase_HEADERS
util/flightinformation.h
util/ft8message.h
util/giro.h
+ util/goesxray.h
util/golay2312.h
+ util/grb.h
util/httpdownloadmanager.h
util/incrementalarray.h
util/incrementalvector.h
@@ -520,8 +526,10 @@ set(sdrbase_HEADERS
util/samplesourceserializer.h
util/simpleserializer.h
util/serialutil.h
+ util/solardynamicsobservatory.h
#util/spinlock.h
util/spyserverlist.h
+ util/stix.h
util/uid.h
util/units.h
util/timeutil.h
diff --git a/sdrbase/util/goesxray.cpp b/sdrbase/util/goesxray.cpp
new file mode 100644
index 000000000..6e976f21c
--- /dev/null
+++ b/sdrbase/util/goesxray.cpp
@@ -0,0 +1,223 @@
+///////////////////////////////////////////////////////////////////////////////////
+// Copyright (C) 2023 Jon Beniston, M7RCE //
+// //
+// This program is free software; you can redistribute it and/or modify //
+// it under the terms of the GNU General Public License as published by //
+// the Free Software Foundation as version 3 of the License, or //
+// (at your option) any later version. //
+// //
+// This program is distributed in the hope that it will be useful, //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
+// GNU General Public License V3 for more details. //
+// //
+// You should have received a copy of the GNU General Public License //
+// along with this program. If not, see . //
+///////////////////////////////////////////////////////////////////////////////////
+
+#include "goesxray.h"
+
+#include
+#include
+#include
+#include
+#include
+
+GOESXRay::GOESXRay()
+{
+ m_networkManager = new QNetworkAccessManager();
+ connect(m_networkManager, &QNetworkAccessManager::finished, this, &GOESXRay::handleReply);
+ connect(&m_dataTimer, &QTimer::timeout, this, &GOESXRay::getData);
+}
+
+
+GOESXRay::~GOESXRay()
+{
+ disconnect(&m_dataTimer, &QTimer::timeout, this, &GOESXRay::getData);
+ disconnect(m_networkManager, &QNetworkAccessManager::finished, this, &GOESXRay::handleReply);
+ delete m_networkManager;
+}
+
+GOESXRay* GOESXRay::create(const QString& service)
+{
+ if (service == "services.swpc.noaa.gov")
+ {
+ return new GOESXRay();
+ }
+ else
+ {
+ qDebug() << "GOESXRay::create: Unsupported service: " << service;
+ return nullptr;
+ }
+}
+
+void GOESXRay::getDataPeriodically(int periodInMins)
+{
+ if (periodInMins > 0)
+ {
+ m_dataTimer.setInterval(periodInMins*60*1000);
+ m_dataTimer.start();
+ getData();
+ }
+ else
+ {
+ m_dataTimer.stop();
+ }
+}
+
+void GOESXRay::getData()
+{
+ // Around 160kB per file
+ QUrl url(QString("https://services.swpc.noaa.gov/json/goes/primary/xrays-6-hour.json"));
+ m_networkManager->get(QNetworkRequest(url));
+
+ QUrl secondaryURL(QString("https://services.swpc.noaa.gov/json/goes/secondary/xrays-6-hour.json"));
+ m_networkManager->get(QNetworkRequest(secondaryURL));
+
+ QUrl protonPrimaryURL(QString("https://services.swpc.noaa.gov/json/goes/primary/integral-protons-plot-6-hour.json"));
+ m_networkManager->get(QNetworkRequest(protonPrimaryURL));
+}
+
+bool GOESXRay::containsNonNull(const QJsonObject& obj, const QString &key) const
+{
+ if (obj.contains(key))
+ {
+ QJsonValue val = obj.value(key);
+ return !val.isNull();
+ }
+ return false;
+}
+
+void GOESXRay::handleReply(QNetworkReply* reply)
+{
+ if (reply)
+ {
+ if (!reply->error())
+ {
+ QByteArray bytes = reply->readAll();
+ bool primary = reply->url().toString().contains("primary");
+
+ if (reply->url().fileName() == "xrays-6-hour.json") {
+ handleXRayJson(bytes, primary);
+ } else if (reply->url().fileName() == "integral-protons-plot-6-hour.json") {
+ handleProtonJson(bytes, primary);
+ } else {
+ qDebug() << "GOESXRay::handleReply: unexpected filename: " << reply->url().fileName();
+ }
+ }
+ else
+ {
+ qDebug() << "GOESXRay::handleReply: error: " << reply->error();
+ }
+ reply->deleteLater();
+ }
+ else
+ {
+ qDebug() << "GOESXRay::handleReply: reply is null";
+ }
+}
+
+void GOESXRay::handleXRayJson(const QByteArray& bytes, bool primary)
+{
+ QJsonDocument document = QJsonDocument::fromJson(bytes);
+ if (document.isArray())
+ {
+ QJsonArray array = document.array();
+ QList data;
+ for (auto valRef : array)
+ {
+ if (valRef.isObject())
+ {
+ QJsonObject obj = valRef.toObject();
+
+ XRayData measurement;
+
+ if (obj.contains(QStringLiteral("satellite"))) {
+ measurement.m_satellite = QString("GOES %1").arg(obj.value(QStringLiteral("satellite")).toInt());
+ }
+ if (containsNonNull(obj, QStringLiteral("time_tag"))) {
+ measurement.m_dateTime = QDateTime::fromString(obj.value(QStringLiteral("time_tag")).toString(), Qt::ISODate);
+ }
+ if (containsNonNull(obj, QStringLiteral("flux"))) {
+ measurement.m_flux = obj.value(QStringLiteral("flux")).toDouble();
+ }
+ if (containsNonNull(obj, QStringLiteral("energy")))
+ {
+ QString energy = obj.value(QStringLiteral("energy")).toString();
+ if (energy == "0.05-0.4nm") {
+ measurement.m_band = XRayData::SHORT;
+ } else if (energy == "0.1-0.8nm") {
+ measurement.m_band = XRayData::LONG;
+ } else {
+ qDebug() << "GOESXRay::handleXRayJson: Unknown energy: " << energy;
+ }
+ }
+
+ data.append(measurement);
+ }
+ else
+ {
+ qDebug() << "GOESXRay::handleXRayJson: Array element is not an object: " << valRef;
+ }
+ }
+ if (data.size() > 0) {
+ emit xRayDataUpdated(data, primary);
+ } else {
+ qDebug() << "GOESXRay::handleXRayJson: No data in array: " << document;
+ }
+ }
+ else
+ {
+ qDebug() << "GOESXRay::handleXRayJson: Document is not an array: " << document;
+ }
+}
+
+void GOESXRay::handleProtonJson(const QByteArray& bytes, bool primary)
+{
+ QJsonDocument document = QJsonDocument::fromJson(bytes);
+ if (document.isArray())
+ {
+ QJsonArray array = document.array();
+ QList data;
+ for (auto valRef : array)
+ {
+ if (valRef.isObject())
+ {
+ QJsonObject obj = valRef.toObject();
+
+ ProtonData measurement;
+
+ if (obj.contains(QStringLiteral("satellite"))) {
+ measurement.m_satellite = QString("GOES %1").arg(obj.value(QStringLiteral("satellite")).toInt());
+ }
+ if (containsNonNull(obj, QStringLiteral("time_tag"))) {
+ measurement.m_dateTime = QDateTime::fromString(obj.value(QStringLiteral("time_tag")).toString(), Qt::ISODate);
+ }
+ if (containsNonNull(obj, QStringLiteral("flux"))) {
+ measurement.m_flux = obj.value(QStringLiteral("flux")).toDouble();
+ }
+ if (containsNonNull(obj, QStringLiteral("energy")))
+ {
+ QString energy = obj.value(QStringLiteral("energy")).toString();
+ QString value = energy.mid(2).split(' ')[0];
+ measurement.m_energy = value.toInt(); // String like: ">=50 MeV"
+ }
+
+ data.append(measurement);
+ }
+ else
+ {
+ qDebug() << "GOESXRay::handleProtonJson: Array element is not an object: " << valRef;
+ }
+ }
+ if (data.size() > 0) {
+ emit protonDataUpdated(data, primary);
+ } else {
+ qDebug() << "GOESXRay::handleProtonJson: No data in array: " << document;
+ }
+ }
+ else
+ {
+ qDebug() << "GOESXRay::handleProtonJson: Document is not an array: " << document;
+ }
+}
diff --git a/sdrbase/util/goesxray.h b/sdrbase/util/goesxray.h
new file mode 100644
index 000000000..c7e6d0fb7
--- /dev/null
+++ b/sdrbase/util/goesxray.h
@@ -0,0 +1,96 @@
+///////////////////////////////////////////////////////////////////////////////////
+// Copyright (C) 2023 Jon Beniston, M7RCE //
+// //
+// This program is free software; you can redistribute it and/or modify //
+// it under the terms of the GNU General Public License as published by //
+// the Free Software Foundation as version 3 of the License, or //
+// (at your option) any later version. //
+// //
+// This program is distributed in the hope that it will be useful, //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
+// GNU General Public License V3 for more details. //
+// //
+// You should have received a copy of the GNU General Public License //
+// along with this program. If not, see . //
+///////////////////////////////////////////////////////////////////////////////////
+
+#ifndef INCLUDE_GOESXRAY_H
+#define INCLUDE_GOESXRAY_H
+
+#include
+#include
+#include
+
+#include "export.h"
+
+class QNetworkAccessManager;
+class QNetworkReply;
+
+// GOES X-Ray data
+// This gets 1-minute averages of solar X-rays the 1-8 Angstrom (0.1-0.8 nm) and 0.5-4.0 Angstrom (0.05-0.4 nm) passbands from the GOES satellites
+// https://www.swpc.noaa.gov/products/goes-x-ray-flux
+// There are primary and secondary data sources, from different satellites, as sometimes they can be in eclipse
+// Also gets Proton flux (Which may be observed on Earth a couple of days after a large flare/CME)
+class SDRBASE_API GOESXRay : public QObject
+{
+ Q_OBJECT
+protected:
+ GOESXRay();
+
+public:
+ struct XRayData {
+ QDateTime m_dateTime;
+ QString m_satellite;
+ double m_flux;
+ enum Band {
+ UNKNOWN,
+ SHORT, // 0.05-0.4nm
+ LONG // 0.1-0.8nm
+ } m_band;
+ XRayData() :
+ m_flux(NAN),
+ m_band(UNKNOWN)
+ {
+ }
+ };
+
+ struct ProtonData {
+ QDateTime m_dateTime;
+ QString m_satellite;
+ double m_flux;
+ int m_energy; // 10=10MeV, 50MeV, 100MeV, 500MeV
+ ProtonData() :
+ m_flux(NAN),
+ m_energy(0)
+ {
+ }
+ };
+
+ static GOESXRay* create(const QString& service="services.swpc.noaa.gov");
+
+ ~GOESXRay();
+ void getDataPeriodically(int periodInMins=10);
+
+public slots:
+ void getData();
+
+private slots:
+ void handleReply(QNetworkReply* reply);
+
+signals:
+ void xRayDataUpdated(const QList& data, bool primary); // Called when new data available.
+ void protonDataUpdated(const QList &data, bool primary);
+
+private:
+ bool containsNonNull(const QJsonObject& obj, const QString &key) const;
+ void handleXRayJson(const QByteArray& bytes, bool primary);
+ void handleProtonJson(const QByteArray& bytes, bool primary);
+
+ QTimer m_dataTimer; // Timer for periodic updates
+ QNetworkAccessManager *m_networkManager;
+
+};
+
+#endif /* INCLUDE_GOESXRAY_H */
+
diff --git a/sdrbase/util/grb.cpp b/sdrbase/util/grb.cpp
new file mode 100644
index 000000000..e42593b68
--- /dev/null
+++ b/sdrbase/util/grb.cpp
@@ -0,0 +1,198 @@
+///////////////////////////////////////////////////////////////////////////////////
+// Copyright (C) 2024 Jon Beniston, M7RCE //
+// //
+// This program is free software; you can redistribute it and/or modify //
+// it under the terms of the GNU General Public License as published by //
+// the Free Software Foundation as version 3 of the License, or //
+// (at your option) any later version. //
+// //
+// This program is distributed in the hope that it will be useful, //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
+// GNU General Public License V3 for more details. //
+// //
+// You should have received a copy of the GNU General Public License //
+// along with this program. If not, see . //
+///////////////////////////////////////////////////////////////////////////////////
+
+#include "grb.h"
+#include "util/csv.h"
+
+#include
+#include
+#include
+#include
+#include
+
+GRB::GRB()
+{
+ connect(&m_dataTimer, &QTimer::timeout, this,&GRB::getData);
+ m_networkManager = new QNetworkAccessManager();
+ connect(m_networkManager, &QNetworkAccessManager::finished, this, &GRB::handleReply);
+
+ QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation);
+ QDir writeableDir(locations[0]);
+ if (!writeableDir.mkpath(QStringLiteral("cache") + QDir::separator() + QStringLiteral("grb"))) {
+ qDebug() << "Failed to create cache/grb";
+ }
+
+ m_cache = new QNetworkDiskCache();
+ m_cache->setCacheDirectory(locations[0] + QDir::separator() + QStringLiteral("cache") + QDir::separator() + QStringLiteral("grb"));
+ m_cache->setMaximumCacheSize(100000000);
+ m_networkManager->setCache(m_cache);
+}
+
+GRB::~GRB()
+{
+ disconnect(&m_dataTimer, &QTimer::timeout, this, &GRB::getData);
+ disconnect(m_networkManager, &QNetworkAccessManager::finished, this, &GRB::handleReply);
+ delete m_networkManager;
+}
+
+GRB* GRB::create()
+{
+ return new GRB();
+}
+
+void GRB::getDataPeriodically(int periodInMins)
+{
+ if (periodInMins > 0)
+ {
+ m_dataTimer.setInterval(periodInMins*60*1000);
+ m_dataTimer.start();
+ getData();
+ }
+ else
+ {
+ m_dataTimer.stop();
+ }
+}
+
+void GRB::getData()
+{
+ QUrl url("https://user-web.icecube.wisc.edu/~grbweb_public/Summary_table.txt");
+
+ m_networkManager->get(QNetworkRequest(url));
+}
+
+void GRB::handleReply(QNetworkReply* reply)
+{
+ if (reply)
+ {
+ if (!reply->error())
+ {
+ if (reply->url().fileName().endsWith(".txt"))
+ {
+ QByteArray bytes = reply->readAll();
+ handleText(bytes);
+ }
+ else
+ {
+ qDebug() << "GRB::handleReply: Unexpected file" << reply->url().fileName();
+ }
+ }
+ else
+ {
+ qDebug() << "GRB::handleReply: Error: " << reply->error();
+ }
+ reply->deleteLater();
+ }
+ else
+ {
+ qDebug() << "GRB::handleReply: Reply is null";
+ }
+}
+
+void GRB::handleText(QByteArray& bytes)
+{
+ // Convert to CSV
+ QString s(bytes);
+ QStringList l = s.split("\n");
+ for (int i = 0; i < l.size(); i++) {
+ l[i] = l[i].simplified().replace(" ", ",");
+ }
+ s = l.join("\n");
+
+ QTextStream in(&s);
+
+ // Skip header
+ for (int i = 0; i < 4; i++) {
+ in.readLine();
+ }
+
+ QList grbs;
+ QStringList cols;
+ while(CSV::readRow(in, &cols))
+ {
+ Data grb;
+
+ if (cols.length() >= 10)
+ {
+ grb.m_name = cols[0];
+ grb.m_fermiName = cols[1];
+ int year = grb.m_name.mid(3, 2).toInt();
+ if (year >= 90) {
+ year += 1900;
+ } else {
+ year += 2000;
+ }
+ QDate date(year, grb.m_name.mid(5, 2).toInt(), grb.m_name.mid(7, 2).toInt());
+ QTime time = QTime::fromString(cols[2]);
+ grb.m_dateTime = QDateTime(date, time);
+ grb.m_ra = cols[3].toFloat();
+ grb.m_dec = cols[4].toFloat();
+ grb.m_fluence = cols[9].toFloat();
+
+ //qDebug() << grb.m_name << grb.m_dateTime.toString() << grb.m_ra << grb.m_dec << grb.m_fluence ;
+
+ if (grb.m_dateTime.isValid()) {
+ grbs.append(grb);
+ }
+ }
+ }
+
+ emit dataUpdated(grbs);
+}
+
+ QString GRB::Data::getFermiURL() const
+{
+ if (m_fermiName.isEmpty() || (m_fermiName == "None")) {
+ return "";
+ }
+ QString base = "https://heasarc.gsfc.nasa.gov/FTP/fermi/data/gbm/bursts/";
+ QString yearDir = "20" + m_fermiName.mid(3, 2);
+ QString dataDir = m_fermiName;
+ dataDir.replace("GRB", "bn");
+ return base + yearDir + "/" + dataDir + "/current/";
+}
+
+QString GRB::Data::getFermiPlotURL() const
+{
+ QString base = getFermiURL();
+ if (base.isEmpty()) {
+ return "";
+ }
+
+ QString name = m_fermiName;
+ name.replace("GRB", "bn");
+ return getFermiURL() + "glg_lc_all_" + name + "_v00.gif"; // Could be v01.gif? How to know without fetching index?
+}
+
+QString GRB::Data::getFermiSkyMapURL() const
+{
+ QString base = getFermiURL();
+ if (base.isEmpty()) {
+ return "";
+ }
+
+ QString name = m_fermiName;
+ name.replace("GRB", "bn");
+ return getFermiURL() + "glg_skymap_all_" + name + "_v00.png";
+}
+
+QString GRB::Data::getSwiftURL() const
+{
+ QString name = m_name;
+ name.replace("GRB", "");
+ return "https://swift.gsfc.nasa.gov/archive/grb_table/" + name;
+}
diff --git a/sdrbase/util/grb.h b/sdrbase/util/grb.h
new file mode 100644
index 000000000..a0075de19
--- /dev/null
+++ b/sdrbase/util/grb.h
@@ -0,0 +1,81 @@
+///////////////////////////////////////////////////////////////////////////////////
+// Copyright (C) 2024 Jon Beniston, M7RCE //
+// //
+// This program is free software; you can redistribute it and/or modify //
+// it under the terms of the GNU General Public License as published by //
+// the Free Software Foundation as version 3 of the License, or //
+// (at your option) any later version. //
+// //
+// This program is distributed in the hope that it will be useful, //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
+// GNU General Public License V3 for more details. //
+// //
+// You should have received a copy of the GNU General Public License //
+// along with this program. If not, see . //
+///////////////////////////////////////////////////////////////////////////////////
+
+#ifndef INCLUDE_GRB_H
+#define INCLUDE_GRB_H
+
+#include
+#include
+
+#include "export.h"
+
+class QNetworkAccessManager;
+class QNetworkReply;
+class QNetworkDiskCache;
+
+// GRB (Gamma Ray Burst) database
+// Gets GRB database from GRBweb https://user-web.icecube.wisc.edu/~grbweb_public/
+// Uses summary .txt file so only contains last 1000 GRBs
+class SDRBASE_API GRB : public QObject
+{
+ Q_OBJECT
+protected:
+ GRB();
+
+public:
+
+ struct SDRBASE_API Data {
+
+ QString m_name; // E.g: GRB240310A
+ QString m_fermiName; // Name used by Fermi telescope. E.g. GRB240310236. Can be None if not detected by Fermi
+ QDateTime m_dateTime;
+ float m_ra; // Right Ascension
+ float m_dec; // Declination
+ float m_fluence; // erg/cm^2
+
+ QString getFermiURL() const; // Get URL where Fermi data is stored
+ QString getFermiPlotURL() const;
+ QString getFermiSkyMapURL() const;
+ QString getSwiftURL() const;
+
+ };
+
+ static GRB* create();
+
+ ~GRB();
+ void getDataPeriodically(int periodInMins=1440); // GRBweb is updated every 24 hours, usually just after 9am UTC
+
+public slots:
+ void getData();
+
+private slots:
+ void handleReply(QNetworkReply* reply);
+
+signals:
+ void dataUpdated(const QListdata); // Called when new data is available.
+
+private:
+
+ QTimer m_dataTimer; // Timer for periodic updates
+ QNetworkAccessManager *m_networkManager;
+ QNetworkDiskCache *m_cache;
+
+ void handleText(QByteArray& bytes);
+
+};
+
+#endif /* INCLUDE_GRB_H */
diff --git a/sdrbase/util/solardynamicsobservatory.cpp b/sdrbase/util/solardynamicsobservatory.cpp
new file mode 100644
index 000000000..11c609852
--- /dev/null
+++ b/sdrbase/util/solardynamicsobservatory.cpp
@@ -0,0 +1,386 @@
+///////////////////////////////////////////////////////////////////////////////////
+// Copyright (C) 2024 Jon Beniston, M7RCE //
+// //
+// This program is free software; you can redistribute it and/or modify //
+// it under the terms of the GNU General Public License as published by //
+// the Free Software Foundation as version 3 of the License, or //
+// (at your option) any later version. //
+// //
+// This program is distributed in the hope that it will be useful, //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
+// GNU General Public License V3 for more details. //
+// //
+// You should have received a copy of the GNU General Public License //
+// along with this program. If not, see . //
+///////////////////////////////////////////////////////////////////////////////////
+
+#include "solardynamicsobservatory.h"
+
+#include
+#include
+#include
+#include
+
+SolarDynamicsObservatory::SolarDynamicsObservatory() :
+ m_size(512)
+{
+ connect(&m_dataTimer, &QTimer::timeout, this, qOverload<>(&SolarDynamicsObservatory::getImage));
+ m_networkManager = new QNetworkAccessManager();
+ connect(m_networkManager, &QNetworkAccessManager::finished, this, &SolarDynamicsObservatory::handleReply);
+
+ QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation);
+ QDir writeableDir(locations[0]);
+ if (!writeableDir.mkpath(QStringLiteral("cache") + QDir::separator() + QStringLiteral("solardynamicsobservatory"))) {
+ qDebug() << "SolarDynamicsObservatory::SolarDynamicsObservatory: Failed to create cache/solardynamicsobservatory";
+ }
+
+ m_cache = new QNetworkDiskCache();
+ m_cache->setCacheDirectory(locations[0] + QDir::separator() + QStringLiteral("cache") + QDir::separator() + QStringLiteral("solardynamicsobservatory"));
+ m_cache->setMaximumCacheSize(100000000);
+ m_networkManager->setCache(m_cache);
+}
+
+SolarDynamicsObservatory::~SolarDynamicsObservatory()
+{
+ disconnect(&m_dataTimer, &QTimer::timeout, this, qOverload<>(&SolarDynamicsObservatory::getImage));
+ disconnect(m_networkManager, &QNetworkAccessManager::finished, this, &SolarDynamicsObservatory::handleReply);
+ delete m_networkManager;
+}
+
+SolarDynamicsObservatory* SolarDynamicsObservatory::create()
+{
+ return new SolarDynamicsObservatory();
+}
+
+QList SolarDynamicsObservatory::getImageSizes()
+{
+ return {512, 1024, 2048, 4096};
+}
+
+QList SolarDynamicsObservatory::getVideoSizes()
+{
+ return {512, 1024};
+}
+
+const QStringList SolarDynamicsObservatory::getImageNames()
+{
+ QChar angstronm(0x212B);
+ QStringList names;
+
+ // SDO
+ names.append(QString("AIA 094 %1").arg(angstronm));
+ names.append(QString("AIA 131 %1").arg(angstronm));
+ names.append(QString("AIA 171 %1").arg(angstronm));
+ names.append(QString("AIA 193 %1").arg(angstronm));
+ names.append(QString("AIA 211 %1").arg(angstronm));
+ names.append(QString("AIA 304 %1").arg(angstronm));
+ names.append(QString("AIA 335 %1").arg(angstronm));
+ names.append(QString("AIA 1600 %1").arg(angstronm));
+ names.append(QString("AIA 1700 %1").arg(angstronm));
+ names.append(QString("AIA 211 %1, 193 %1, 171 %1").arg(angstronm));
+ names.append(QString("AIA 304 %1, 211 %1, 171 %1").arg(angstronm));
+ names.append(QString("AIA 094 %1, 335 %1, 193 %1").arg(angstronm));
+ names.append(QString("AIA 171 %1, HMIB").arg(angstronm));
+ names.append("HMI Magneotgram");
+ names.append("HMI Colorized Magneotgram");
+ names.append("HMI Intensitygram - Colored");
+ names.append("HMI Intensitygram - Flattened");
+ names.append("HMI Intensitygram");
+ names.append("HMI Dopplergram");
+
+ // SOHO
+ names.append("LASCO C2");
+ names.append("LASCO C3");
+
+ return names;
+}
+
+const QStringList SolarDynamicsObservatory::getChannelNames()
+{
+ QStringList channelNames = {
+ "0094",
+ "0131",
+ "0171",
+ "0193",
+ "0211",
+ "0304",
+ "0335",
+ "1600",
+ "1700",
+ "211193171",
+ "304211171",
+ "094335193",
+ "HMImag",
+ "HMIB",
+ "HMIBC",
+ "HMIIC",
+ "HMIIF",
+ "HMII",
+ "HMID",
+ "c2",
+ "c3"
+ };
+
+ return channelNames;
+}
+
+const QStringList SolarDynamicsObservatory::getImageFileNames()
+{
+ // Ordering needs to match getImageNames()
+ // %1 replaced with size
+ QStringList filenames = {
+ "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_0094.jpg",
+ "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_0131.jpg",
+ "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_0171.jpg",
+ "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_0193.jpg",
+ "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_0211.jpg",
+ "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_0304.jpg",
+ "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_0335.jpg",
+ "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_1600.jpg",
+ "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_1700.jpg",
+ "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_211193171.jpg",
+ "https://sdo.gsfc.nasa.gov/assets/img/latest/f_304_211_171_%1.jpg",
+ //"https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_304211171.jpg",
+ "https://sdo.gsfc.nasa.gov/assets/img/latest/f_094_335_193_%1.jpg",
+ //"https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_094335193.jpg",
+ "https://sdo.gsfc.nasa.gov/assets/img/latest/f_HMImag_171_%1.jpg",
+ //"https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_HMImag.jpg",
+ "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_HMIB.jpg",
+ "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_HMIBC.jpg",
+ "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_HMIIC.jpg",
+ "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_HMIIF.jpg",
+ "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_HMII.jpg",
+ "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_HMID.jpg",
+ "https://soho.nascom.nasa.gov/data/realtime/c2/512/latest.jpg",
+ "https://soho.nascom.nasa.gov/data/realtime/c3/512/latest.jpg"
+ };
+
+ return filenames;
+}
+
+const QStringList SolarDynamicsObservatory::getVideoNames()
+{
+ QChar angstronm(0x212B);
+ QStringList names;
+
+ // SDO
+ names.append(QString("AIA 094 %1").arg(angstronm));
+ names.append(QString("AIA 131 %1").arg(angstronm));
+ names.append(QString("AIA 171 %1").arg(angstronm));
+ names.append(QString("AIA 193 %1").arg(angstronm));
+ names.append(QString("AIA 211 %1").arg(angstronm));
+ names.append(QString("AIA 304 %1").arg(angstronm));
+ names.append(QString("AIA 335 %1").arg(angstronm));
+ names.append(QString("AIA 1600 %1").arg(angstronm));
+ names.append(QString("AIA 1700 %1").arg(angstronm));
+
+ // SOHO
+ names.append("LASCO C2");
+ names.append("LASCO C3");
+
+ return names;
+}
+
+const QStringList SolarDynamicsObservatory::getVideoFileNames()
+{
+ const QStringList filenames = {
+ "http://sdo.gsfc.nasa.gov/assets/img/latest/mpeg/latest_%1_0094.mp4", // Videos sometimes fail to load on Windows if https used
+ "http://sdo.gsfc.nasa.gov/assets/img/latest/mpeg/latest_%1_0131.mp4",
+ "http://sdo.gsfc.nasa.gov/assets/img/latest/mpeg/latest_%1_0171.mp4",
+ "http://sdo.gsfc.nasa.gov/assets/img/latest/mpeg/latest_%1_0193.mp4",
+ "http://sdo.gsfc.nasa.gov/assets/img/latest/mpeg/latest_%1_0211.mp4",
+ "http://sdo.gsfc.nasa.gov/assets/img/latest/mpeg/latest_%1_0304.mp4",
+ "http://sdo.gsfc.nasa.gov/assets/img/latest/mpeg/latest_%1_0335.mp4",
+ "http://sdo.gsfc.nasa.gov/assets/img/latest/mpeg/latest_%1_1600.mp4",
+ "http://sdo.gsfc.nasa.gov/assets/img/latest/mpeg/latest_%1_1700.mp4",
+ "http://soho.nascom.nasa.gov/data/LATEST/current_c2.mp4",
+ "http://soho.nascom.nasa.gov/data/LATEST/current_c3.mp4",
+ };
+
+ return filenames;
+}
+
+QString SolarDynamicsObservatory::getImageURL(const QString& image, int size)
+{
+ const QStringList names = SolarDynamicsObservatory::getImageNames();
+ const QStringList filenames = SolarDynamicsObservatory::getImageFileNames();
+ int idx = names.indexOf(image);
+
+ if (idx != -1) {
+ return QString(filenames[idx]).arg(size);
+ } else {
+ return "";
+ }
+}
+
+QString SolarDynamicsObservatory::getVideoURL(const QString& video, int size)
+{
+ const QStringList names = SolarDynamicsObservatory::getVideoNames();
+ const QStringList filenames = SolarDynamicsObservatory::getVideoFileNames();
+ int idx = names.indexOf(video);
+
+ if (idx != -1) {
+ return QString(filenames[idx]).arg(size);
+ } else {
+ return "";
+ }
+}
+
+void SolarDynamicsObservatory::getImagePeriodically(const QString& image, int size, int periodInMins)
+{
+ m_image = image;
+ m_size = size;
+ if (periodInMins > 0)
+ {
+ m_dataTimer.setInterval(periodInMins*60*1000);
+ m_dataTimer.start();
+ getImage();
+ }
+ else
+ {
+ m_dataTimer.stop();
+ }
+}
+
+void SolarDynamicsObservatory::getImage()
+{
+ getImage(m_image, m_size);
+}
+
+void SolarDynamicsObservatory::getImage(const QString& imageName, int size)
+{
+ QString urlString = getImageURL(imageName, size);
+ if (!urlString.isEmpty())
+ {
+ QUrl url(urlString);
+
+ m_networkManager->get(QNetworkRequest(url));
+ }
+}
+
+void SolarDynamicsObservatory::getImage(const QString& imageName, QDateTime dateTime, int size)
+{
+ // Stop periodic updates, if not after latest data
+ m_dataTimer.stop();
+
+ // Get file index, as we don't know what time will be used in the file
+ QDate date = dateTime.date();
+ QString urlString = QString("https://sdo.gsfc.nasa.gov/assets/img/browse/%1/%2/%3/")
+ .arg(date.year())
+ .arg(date.month(), 2, 10, QLatin1Char('0'))
+ .arg(date.day(), 2, 10, QLatin1Char('0'));
+ QUrl url(urlString);
+
+ // Save details of image we are after
+ m_dateTime = dateTime;
+ m_size = size;
+ m_image = imageName;
+
+ m_networkManager->get(QNetworkRequest(url));
+}
+
+void SolarDynamicsObservatory::handleReply(QNetworkReply* reply)
+{
+ if (reply)
+ {
+ if (!reply->error())
+ {
+ if (reply->url().fileName().endsWith(".jpg"))
+ {
+ handleJpeg(reply->readAll());
+ }
+ else
+ {
+ handleIndex(reply->readAll());
+ }
+
+ }
+ else
+ {
+ qDebug() << "SolarDynamicsObservatory::handleReply: Error: " << reply->error();
+ }
+ reply->deleteLater();
+ }
+ else
+ {
+ qDebug() << "SolarDynamicsObservatory::handleReply: Reply is null";
+ }
+}
+
+void SolarDynamicsObservatory::handleJpeg(const QByteArray& bytes)
+{
+ QImage image;
+
+ if (image.loadFromData(bytes)) {
+ emit imageUpdated(image);
+ } else {
+ qWarning() << "SolarDynamicsObservatory::handleJpeg: Failed to load image";
+ }
+}
+
+void SolarDynamicsObservatory::handleIndex(const QByteArray& bytes)
+{
+ const QStringList names = SolarDynamicsObservatory::getImageNames();
+ const QStringList channelNames = SolarDynamicsObservatory::getChannelNames();
+ int idx = names.indexOf(m_image);
+ if (idx < 0) {
+ return;
+ }
+ QString channel = channelNames[idx];
+
+ QString file(bytes);
+ QStringList lines = file.split("\n");
+
+ QString date = m_dateTime.date().toString("yyyyMMdd");
+ QString pattern = QString("\"%1_([0-9]{6})_%2_%3.jpg\"").arg(date).arg(m_size).arg(channel);
+ QRegularExpression re(pattern);
+
+ // Get all times the image is available
+ QList times;
+ for (const auto& line : lines)
+ {
+ QRegularExpressionMatch match = re.match(line);
+ if (match.hasMatch())
+ {
+ QString t = match.capturedTexts()[1];
+ int h = t.left(2).toInt();
+ int m = t.mid(2, 2).toInt();
+ int s = t.right(2).toInt();
+ times.append(QTime(h, m, s));
+ }
+ }
+
+ if (times.length() > 0)
+ {
+ QTime target = m_dateTime.time();
+ QTime current = times[0];
+ for (int i = 1; i < times.size(); i++)
+ {
+ if (target < times[i]) {
+ break;
+ }
+ current = times[i];
+ }
+
+ // Get image
+ QDate date = m_dateTime.date();
+ QString urlString = QString("https://sdo.gsfc.nasa.gov/assets/img/browse/%1/%2/%3/%1%2%3_%4%5%6_%7_%8.jpg")
+ .arg(date.year())
+ .arg(date.month(), 2, 10, QLatin1Char('0'))
+ .arg(date.day(), 2, 10, QLatin1Char('0'))
+ .arg(current.hour(), 2, 10, QLatin1Char('0'))
+ .arg(current.minute(), 2, 10, QLatin1Char('0'))
+ .arg(current.second(), 2, 10, QLatin1Char('0'))
+ .arg(m_size)
+ .arg(channel);
+
+ QUrl url(urlString);
+
+ m_networkManager->get(QNetworkRequest(url));
+ }
+ else
+ {
+ qDebug() << "SolarDynamicsObservatory: No image available";
+ }
+}
diff --git a/sdrbase/util/solardynamicsobservatory.h b/sdrbase/util/solardynamicsobservatory.h
new file mode 100644
index 000000000..f0fd5605d
--- /dev/null
+++ b/sdrbase/util/solardynamicsobservatory.h
@@ -0,0 +1,81 @@
+///////////////////////////////////////////////////////////////////////////////////
+// Copyright (C) 2024 Jon Beniston, M7RCE //
+// //
+// This program is free software; you can redistribute it and/or modify //
+// it under the terms of the GNU General Public License as published by //
+// the Free Software Foundation as version 3 of the License, or //
+// (at your option) any later version. //
+// //
+// This program is distributed in the hope that it will be useful, //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
+// GNU General Public License V3 for more details. //
+// //
+// You should have received a copy of the GNU General Public License //
+// along with this program. If not, see . //
+///////////////////////////////////////////////////////////////////////////////////
+
+#ifndef INCLUDE_SOLARDYNAMICSOBSERVATORY_H
+#define INCLUDE_SOLARDYNAMICSOBSERVATORY_H
+
+#include
+#include
+#include
+
+#include "export.h"
+
+class QNetworkAccessManager;
+class QNetworkReply;
+class QNetworkDiskCache;
+
+// This gets solar imagery from SDO (Solar Dynamics Observatory) - https://sdo.gsfc.nasa.gov/
+// and LASCO images from SOHO - https://soho.nascom.nasa.gov/
+class SDRBASE_API SolarDynamicsObservatory : public QObject
+{
+ Q_OBJECT
+protected:
+ SolarDynamicsObservatory();
+
+public:
+
+ static SolarDynamicsObservatory* create();
+
+ ~SolarDynamicsObservatory();
+ void getImagePeriodically(const QString& image, int size=512, int periodInMins=15);
+ void getImage(const QString& m_image, int size);
+ void getImage(const QString& m_image, QDateTime dateTime, int size=512);
+
+ static QString getImageURL(const QString& image, int size);
+ static QString getVideoURL(const QString& video, int size=512);
+
+ static QList getImageSizes();
+ static const QStringList getChannelNames();
+ static const QStringList getImageNames();
+ static QList getVideoSizes();
+ static const QStringList getVideoNames();
+
+private slots:
+ void getImage();
+ void handleReply(QNetworkReply* reply);
+
+signals:
+ void imageUpdated(const QImage& image); // Called when new image is available.
+
+private:
+
+ QTimer m_dataTimer; // Timer for periodic updates
+ QNetworkAccessManager *m_networkManager;
+ QNetworkDiskCache *m_cache;
+
+ QString m_image;
+ int m_size;
+ QDateTime m_dateTime;
+
+ void handleJpeg(const QByteArray& bytes);
+ void handleIndex(const QByteArray& bytes);
+ static const QStringList getImageFileNames();
+ static const QStringList getVideoFileNames();
+
+};
+
+#endif /* INCLUDE_SOLARDYNAMICSOBSERVATORY_H */
diff --git a/sdrbase/util/stix.cpp b/sdrbase/util/stix.cpp
new file mode 100644
index 000000000..9230141da
--- /dev/null
+++ b/sdrbase/util/stix.cpp
@@ -0,0 +1,176 @@
+///////////////////////////////////////////////////////////////////////////////////
+// Copyright (C) 2024 Jon Beniston, M7RCE //
+// //
+// This program is free software; you can redistribute it and/or modify //
+// it under the terms of the GNU General Public License as published by //
+// the Free Software Foundation as version 3 of the License, or //
+// (at your option) any later version. //
+// //
+// This program is distributed in the hope that it will be useful, //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
+// GNU General Public License V3 for more details. //
+// //
+// You should have received a copy of the GNU General Public License //
+// along with this program. If not, see . //
+///////////////////////////////////////////////////////////////////////////////////
+
+#include "stix.h"
+
+#include
+#include
+#include
+#include
+#include
+
+STIX::STIX()
+{
+ m_networkManager = new QNetworkAccessManager();
+ connect(m_networkManager, &QNetworkAccessManager::finished, this, &STIX::handleReply);
+ connect(&m_dataTimer, &QTimer::timeout, this, &STIX::getData);
+}
+
+
+STIX::~STIX()
+{
+ disconnect(&m_dataTimer, &QTimer::timeout, this, &STIX::getData);
+ disconnect(m_networkManager, &QNetworkAccessManager::finished, this, &STIX::handleReply);
+ delete m_networkManager;
+}
+
+STIX* STIX::create()
+{
+ return new STIX();
+}
+
+void STIX::getDataPeriodically(int periodInMins)
+{
+ if (periodInMins > 0)
+ {
+ m_dataTimer.setInterval(periodInMins*60*1000);
+ m_dataTimer.start();
+ getData();
+ }
+ else
+ {
+ m_dataTimer.stop();
+ }
+}
+
+void STIX::getData()
+{
+ QUrlQuery data(QString("https://datacenter.stix.i4ds.net/api/request/flare-list"));
+ QDateTime start;
+
+ if (m_mostRecent.isValid()) {
+ start = m_mostRecent;
+ } else {
+ start = QDateTime::currentDateTime().addDays(-5);
+ }
+
+ data.addQueryItem("start_utc", start.toString(Qt::ISODate));
+ data.addQueryItem("end_utc", QDateTime::currentDateTime().toString(Qt::ISODate));
+ data.addQueryItem("sort", "time");
+
+ QUrl url("https://datacenter.stix.i4ds.net/api/request/flare-list");
+ QNetworkRequest request(url);
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
+ m_networkManager->post(request, data.toString(QUrl::FullyEncoded).toUtf8());
+}
+
+bool STIX::containsNonNull(const QJsonObject& obj, const QString &key) const
+{
+ if (obj.contains(key))
+ {
+ QJsonValue val = obj.value(key);
+ return !val.isNull();
+ }
+ return false;
+}
+
+void STIX::handleReply(QNetworkReply* reply)
+{
+ if (reply)
+ {
+ if (!reply->error())
+ {
+ QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
+
+ if (document.isArray())
+ {
+ QJsonArray array = document.array();
+ QList data;
+ for (auto valRef : array)
+ {
+ if (valRef.isObject())
+ {
+ QJsonObject obj = valRef.toObject();
+
+ FlareData measurement;
+
+ if (obj.contains(QStringLiteral("flare_id"))) {
+ measurement.m_id = obj.value(QStringLiteral("flare_id")).toString();
+ }
+ if (obj.contains(QStringLiteral("start_UTC")))
+ {
+ measurement.m_startDateTime = QDateTime::fromString(obj.value(QStringLiteral("start_UTC")).toString(), Qt::ISODate);
+ if (!m_mostRecent.isValid() || (measurement.m_startDateTime > m_mostRecent)) {
+ m_mostRecent = measurement.m_startDateTime;
+ }
+ }
+ if (obj.contains(QStringLiteral("end_UTC"))) {
+ measurement.m_endDateTime = QDateTime::fromString(obj.value(QStringLiteral("end_UTC")).toString(), Qt::ISODate);
+ }
+ if (obj.contains(QStringLiteral("peak_UTC"))) {
+ measurement.m_peakDateTime = QDateTime::fromString(obj.value(QStringLiteral("peak_UTC")).toString(), Qt::ISODate);
+ }
+ if (obj.contains(QStringLiteral("duration"))) {
+ measurement.m_duration = obj.value(QStringLiteral("duration")).toInt();
+ }
+ if (obj.contains(QStringLiteral("GOES_flux"))) {
+ measurement.m_flux = obj.value(QStringLiteral("GOES_flux")).toDouble();
+ }
+
+ data.append(measurement);
+ }
+ else
+ {
+ qDebug() << "STIX::handleReply: Array element is not an object: " << valRef;
+ }
+ }
+ if (data.size() > 0)
+ {
+ m_data.append(data);
+ emit dataUpdated(m_data);
+ }
+ else
+ {
+ qDebug() << "STIX::handleReply: No data in array: " << document;
+ }
+ }
+ else
+ {
+ qDebug() << "STIX::handleReply: Document is not an array: " << document;
+ }
+ }
+ else
+ {
+ qDebug() << "STIX::handleReply: error: " << reply->error();
+ }
+ reply->deleteLater();
+ }
+ else
+ {
+ qDebug() << "STIX::handleReply: reply is null";
+ }
+}
+
+ QString STIX::FlareData::getLightCurvesURL() const
+ {
+ return QString("https://datacenter.stix.i4ds.net/view/plot/lightcurves?start=%1&span=%2").arg(m_startDateTime.toSecsSinceEpoch()).arg(m_duration);
+ }
+
+ QString STIX::FlareData::getDataURL() const
+ {
+ return QString("https://datacenter.stix.i4ds.net/view/list/fits/%1/%2").arg(m_startDateTime.toSecsSinceEpoch()).arg(m_duration);
+ }
diff --git a/sdrbase/util/stix.h b/sdrbase/util/stix.h
new file mode 100644
index 000000000..924b8e7ea
--- /dev/null
+++ b/sdrbase/util/stix.h
@@ -0,0 +1,83 @@
+///////////////////////////////////////////////////////////////////////////////////
+// Copyright (C) 2024 Jon Beniston, M7RCE //
+// //
+// This program is free software; you can redistribute it and/or modify //
+// it under the terms of the GNU General Public License as published by //
+// the Free Software Foundation as version 3 of the License, or //
+// (at your option) any later version. //
+// //
+// This program is distributed in the hope that it will be useful, //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
+// GNU General Public License V3 for more details. //
+// //
+// You should have received a copy of the GNU General Public License //
+// along with this program. If not, see . //
+///////////////////////////////////////////////////////////////////////////////////
+
+#ifndef INCLUDE_STIX_H
+#define INCLUDE_STIX_H
+
+#include
+#include
+#include
+#include
+
+#include "export.h"
+
+class QNetworkAccessManager;
+class QNetworkReply;
+
+// Solar Orbiter STIX (Spectrometer/Telescope for Imaging X-rays) instrument
+// Gets solar flare data - Newest data is often about 24 hours old
+class SDRBASE_API STIX : public QObject
+{
+ Q_OBJECT
+protected:
+ STIX();
+
+public:
+ struct SDRBASE_API FlareData {
+ QString m_id;
+ QDateTime m_startDateTime;
+ QDateTime m_endDateTime;
+ QDateTime m_peakDateTime;
+ int m_duration; // In seconds
+ double m_flux;
+ FlareData() :
+ m_duration(0),
+ m_flux(NAN)
+ {
+ }
+
+ QString getLightCurvesURL() const;
+ QString getDataURL() const;
+ };
+
+ static STIX* create();
+
+ ~STIX();
+ void getDataPeriodically(int periodInMins=60);
+
+public slots:
+ void getData();
+
+private slots:
+ void handleReply(QNetworkReply* reply);
+
+signals:
+ void dataUpdated(const QList& data); // Called when new data available.
+
+private:
+ bool containsNonNull(const QJsonObject& obj, const QString &key) const;
+
+ QTimer m_dataTimer; // Timer for periodic updates
+ QNetworkAccessManager *m_networkManager;
+
+ QDateTime m_mostRecent;
+ QList m_data;
+
+};
+
+#endif /* INCLUDE_STIX_H */
+