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 */
+